From ef3c0c044d84ad776acb230c8aa0183c8716773e Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Thu, 21 May 2026 14:32:48 +0200 Subject: [PATCH 01/20] Intro notebooks --- src/main/python/notebooks/python_e2e_l1.ipynb | 520 ++++++++++++++++++ src/main/python/notebooks/quick_start.ipynb | 234 ++++++++ src/main/python/requirements.txt | 9 + 3 files changed, 763 insertions(+) create mode 100644 src/main/python/notebooks/python_e2e_l1.ipynb create mode 100644 src/main/python/notebooks/quick_start.ipynb create mode 100644 src/main/python/requirements.txt diff --git a/src/main/python/notebooks/python_e2e_l1.ipynb b/src/main/python/notebooks/python_e2e_l1.ipynb new file mode 100644 index 00000000000..34564cffbd6 --- /dev/null +++ b/src/main/python/notebooks/python_e2e_l1.ipynb @@ -0,0 +1,520 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "26ef529d", + "metadata": {}, + "source": [ + "This example shows how one can work the SystemDS framework. More precisely, we will make use of the built-in DataManager, Multinomial Logistic Regression function, and the Confusion Matrix function. The dataset used in this tutorial is a preprocessed version of the “UCI Adult Data Set”. If one wants to skip the explanation then the full script is available at the end of this level.\n", + "\n", + "We will train a Multinomial Logistic Regression model on the training dataset and subsequently use the test dataset to assess how well our model can predict if the income is above or below $50K/yr based on the features." + ] + }, + { + "cell_type": "markdown", + "id": "18ac25e2", + "metadata": {}, + "source": [ + "# Step 1: Load and prepare data\n", + "First, we get our training and testing data from the built-in DataManager. Since the multiLogReg function requires the labels (Y) to be > 0, we add 1 to all labels. This ensures that the smallest label is >= 1. Additionally we will only take a fraction of the training and test set into account to speed up the execution." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "40c4453f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: Scuro dependencies missing or wrong version installed: torch 2.4.1, torchvision 0.19.1, librosa 0.10.2, opencv-python 4.10.0.84, opt-einsum 3.3.0, h5py 3.11.0, transformers 4.46.3, nltk 3.9.1, gensim 4.3.3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: Using incubator modules: jdk.incubator.vector\n", + "26/05/06 15:19:28 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n" + ] + } + ], + "source": [ + "from systemds.context import SystemDSContext\n", + "from systemds.examples.tutorials.adult import DataManager\n", + "from systemds.operator.algorithm import multiLogReg\n", + "from systemds.operator.algorithm import multiLogRegPredict\n", + "from systemds.operator.algorithm import confusionMatrix\n", + "\n", + "with SystemDSContext() as sds:\n", + " d = DataManager()\n", + "\n", + " # limit the sample size\n", + " train_count = 15000\n", + " test_count = 5000\n", + "\n", + " # Get train and test datasets.\n", + " X_frame, Y_frame, Xt_frame, Yt_frame = d.get_preprocessed_dataset(sds)\n", + "\n", + " # Transformation specification\n", + " jspec_data = d.get_jspec(sds)\n", + " jspec_labels = sds.scalar(f'\"{ {\"recode\": [\"income\"]} }\"')\n", + "\n", + " # Transform frames to matrices.\n", + " X, M1 = X_frame.transform_encode(spec=jspec_data)\n", + " Xt = Xt_frame.transform_apply(spec=jspec_data, meta=M1) \n", + " Y, M2 = Y_frame.transform_encode(spec=jspec_labels)\n", + " Yt = Yt_frame.transform_apply(spec=jspec_labels, meta=M2) \n", + " \n", + " # Subsample to make training faster\n", + " X = X[0:train_count]\n", + " Y = Y[0:train_count]\n", + " Xt = Xt[0:test_count]\n", + " Yt = Yt[0:test_count]" + ] + }, + { + "cell_type": "markdown", + "id": "349c0d0b", + "metadata": {}, + "source": [ + "Here the DataManager contains the code for downloading and setting up either Pandas DataFrames or internal SystemDS Frames, for the best performance and no data transfer from pandas to SystemDS it is recommended to read directly from disk into SystemDS." + ] + }, + { + "cell_type": "markdown", + "id": "4a180e28", + "metadata": {}, + "source": [ + "# Step 2: Training\n", + "Now that we prepared the data, we can use the multiLogReg function. First, we will train the model on our training data. Afterward, we can make predictions on the test data and assess the performance of the model." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "079a61b4", + "metadata": {}, + "outputs": [], + "source": [ + "betas = multiLogReg(X, Y, verbose=False)" + ] + }, + { + "cell_type": "markdown", + "id": "d145d87a", + "metadata": {}, + "source": [ + "Note that nothing has been calculated yet. In SystemDS the calculation is executed once `compute()` is called. E.g. `betas_res = betas.compute()`.\n", + "\n", + "We can now use the trained model to make predictions on the test data." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6b734c4d", + "metadata": {}, + "outputs": [], + "source": [ + "[_, y_pred, acc] = multiLogRegPredict(Xt, betas, Y=Yt)" + ] + }, + { + "cell_type": "markdown", + "id": "db63e4ab", + "metadata": {}, + "source": [ + "The multiLogRegPredict function has three return values:\n", + "\n", + "`m`, a matrix with the mean probability of correctly classifying each label. We do not use it further in this example.\n", + "\n", + "`y_pred`, is the predictions made using the model\n", + "\n", + "`acc`, is the accuracy achieved by the model." + ] + }, + { + "cell_type": "markdown", + "id": "bffaa936", + "metadata": {}, + "source": [ + "# Step 3: Confusion Matrix\n", + "A confusion matrix is a useful tool to analyze the performance of the model and to obtain a better understanding which classes the model has difficulties separating. The confusionMatrix function takes the predicted labels and the true labels. It then returns the confusion matrix for the predictions and the confusion matrix averages of each true class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3cd2e62", + "metadata": {}, + "outputs": [], + "source": [ + "confusion_matrix_abs, _ = confusionMatrix(y_pred, Yt).compute()" + ] + }, + { + "cell_type": "markdown", + "id": "37bfe729", + "metadata": {}, + "source": [ + "# Full Script" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "54190e79", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: Scuro dependencies missing or wrong version installed: torch 2.4.1, torchvision 0.19.1, librosa 0.10.2, opencv-python 4.10.0.84, opt-einsum 3.3.0, h5py 3.11.0, transformers 4.46.3, nltk 3.9.1, gensim 4.3.3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: Using incubator modules: jdk.incubator.vector\n", + "26/05/06 16:39:07 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", + "INFO:python_e2e_l1_example:Confusion Matrix\n", + "INFO:python_e2e_l1_example:[[3580. 488.]\n", + " [ 248. 684.]]\n" + ] + } + ], + "source": [ + "from systemds.context import SystemDSContext\n", + "from systemds.examples.tutorials.adult import DataManager\n", + "from systemds.operator.algorithm import multiLogReg\n", + "from systemds.operator.algorithm import multiLogRegPredict\n", + "from systemds.operator.algorithm import confusionMatrix\n", + "from systemds.operator.algorithm import getAccuracy\n", + "\n", + "import logging\n", + "logger = logging.getLogger('python_e2e_l1_example')\n", + "logger.setLevel(logging.INFO)\n", + "\n", + "with SystemDSContext() as sds:\n", + " d = DataManager()\n", + "\n", + " # limit the sample size\n", + " train_count = 15000\n", + " test_count = 5000\n", + "\n", + " # Get train and test datasets.\n", + " X_frame, Y_frame, Xt_frame, Yt_frame = d.get_preprocessed_dataset(sds)\n", + "\n", + " # Transformation specification\n", + " jspec_data = d.get_jspec(sds)\n", + " jspec_labels = sds.scalar(f'\"{ {\"recode\": [\"income\"]} }\"')\n", + "\n", + " # Transform frames to matrices.\n", + " X, M1 = X_frame.transform_encode(spec=jspec_data)\n", + " Xt = Xt_frame.transform_apply(spec=jspec_data, meta=M1) \n", + " Y, M2 = Y_frame.transform_encode(spec=jspec_labels)\n", + " Yt = Yt_frame.transform_apply(spec=jspec_labels, meta=M2) \n", + " \n", + " # Subsample to make training faster\n", + " X = X[0:train_count]\n", + " Y = Y[0:train_count]\n", + " Xt = Xt[0:test_count]\n", + " Yt = Yt[0:test_count]\n", + "\n", + " # Train model \n", + " betas = multiLogReg(X, Y, verbose=False)\n", + "\n", + " # Apply model\n", + " [_, y_pred, acc] = multiLogRegPredict(Xt, betas, Y=Yt)\n", + "\n", + " # Confusion Matrix\n", + " confusion_matrix_abs, _ = confusionMatrix(y_pred, Yt).compute()\n", + " \n", + " # accuracy = getAccuracy(Yt, y_pred).compute()\n", + "\n", + "\n", + " logger.info(\"Confusion Matrix\")\n", + " logger.info(confusion_matrix_abs)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c2d7049a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from sklearn.metrics import ConfusionMatrixDisplay\n", + "\n", + "disp = ConfusionMatrixDisplay(confusion_matrix=confusion_matrix_abs)\n", + "disp.plot(cmap=plt.cm.Blues)\n", + "plt.title('Confusion Matrix')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a9360ea0", + "metadata": {}, + "source": [ + "The Confusion Matrix created has four different quadrants:\n", + "\n", + "- True Negative (Top-Left Quadrant)\n", + "- False Positive (Top-Right Quadrant)\n", + "- False Negative (Bottom-Left Quadrant)\n", + "- True Positive (Bottom-Right Quadrant)\n", + "\n", + "True means that the values were accurately predicted, False means that there was an error or wrong prediction." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "90b88789", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[3580., 488.],\n", + " [ 248., 684.]])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "confusion_matrix_abs" + ] + }, + { + "cell_type": "markdown", + "id": "c42dc06f", + "metadata": {}, + "source": [ + "# Accuracy\n", + "\n", + "Accuracy measures how often the model is correct.\n", + "\n", + "(True Positive + True Negative) / Total Predictions\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d69262cd", + "metadata": {}, + "outputs": [], + "source": [ + "TN, FP, FN, TP = confusion_matrix_abs[0,0], confusion_matrix_abs[0,1], confusion_matrix_abs[1,0], confusion_matrix_abs[1,1]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "263ca554", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(0.8528)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# from systemds.operator.algorithm import getAccuracy\n", + "# accuracy = getAccuracy(Yt, y_pred).compute()\n", + "\n", + "\n", + "accuracy = (TP + TN) / test_count\n", + "accuracy" + ] + }, + { + "cell_type": "markdown", + "id": "54ea1ecf", + "metadata": {}, + "source": [ + "# Precision\n", + "Of the positives predicted, what percentage is truly positive?\n", + "\n", + "True Positive / (True Positive + False Positive)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d7daf015", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(0.5836177474402731)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "precision = TP / (TP + FP)\n", + "precision" + ] + }, + { + "cell_type": "markdown", + "id": "9b24ea83", + "metadata": {}, + "source": [ + "# Sensitivity (Recall)\n", + "Of all the positive cases, what percentage are predicted positive?\n", + "\n", + "Sensitivity (sometimes called Recall) measures how good the model is at predicting positives.\n", + "\n", + "This means it looks at true positives and false negatives (which are positives that have been incorrectly predicted as negative).\n", + "\n", + "True Positive / (True Positive + False Negative)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "57e04ed3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(0.7339055793991416)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sensitivity = TP /(TP + FN)\n", + "sensitivity" + ] + }, + { + "cell_type": "markdown", + "id": "52abc77a", + "metadata": {}, + "source": [ + "# Specificity\n", + "How well the model is at prediciting negative results?\n", + "\n", + "Specificity is similar to sensitivity, but looks at it from the persepctive of negative results.\n", + "\n", + "True Negative / (True Negative + False Positive)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3dcfa6b8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(0.880039331366765)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "specificity = TN / (TN + FP)\n", + "specificity" + ] + }, + { + "cell_type": "markdown", + "id": "127fe71d", + "metadata": {}, + "source": [ + "# F-score\n", + "F-score is the \"harmonic mean\" of precision and sensitivity.\n", + "\n", + "It considers both false positive and false negative cases and is good for imbalanced datasets.\n", + "\n", + "2 * ((Precision * Sensitivity) / (Precision + Sensitivity))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bceed706", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(0.6501901140684411)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "f_score = 2 * ((precision * sensitivity) / (precision + sensitivity))\n", + "f_score" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python_venv (3.12.3)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/main/python/notebooks/quick_start.ipynb b/src/main/python/notebooks/quick_start.ipynb new file mode 100644 index 00000000000..5801f0882a0 --- /dev/null +++ b/src/main/python/notebooks/quick_start.ipynb @@ -0,0 +1,234 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0213111f", + "metadata": {}, + "source": [ + "# Matrix Operations" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f3d1b80e", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: Using incubator modules: jdk.incubator.vector\n", + "26/05/06 14:56:07 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", + "INFO:simple_example:[[13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]\n", + " [13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]\n", + " [13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]\n", + " [13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]\n", + " [13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]]\n" + ] + } + ], + "source": [ + "import logging\n", + "\n", + "from systemds.context import SystemDSContext\n", + "\n", + "logger = logging.getLogger('simple_example')\n", + "logger.setLevel(logging.INFO)\n", + "\n", + "# Create a context and if necessary (no SystemDS py4j instance running)\n", + "# it starts a subprocess which does the execution in SystemDS\n", + "with SystemDSContext() as sds:\n", + " # Full generates a matrix completely filled with one number.\n", + " # Generate a 5x10 matrix filled with 4.2\n", + " m = sds.full((5, 10), 4.20)\n", + " # multiply with scalar. Nothing is executed yet!\n", + " m_res = m * 3.1\n", + " # Do the calculation in SystemDS by calling compute().\n", + " # The returned value is an numpy array that can be directly printed.\n", + " logger.info(m_res.compute())\n", + " # context will automatically be closed and process stopped" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cadb888c", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: Using incubator modules: jdk.incubator.vector\n", + "26/05/06 15:02:21 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", + "26/05/06 15:02:21 WARNING SystemDSContext: Deprecated method from_numpy. Use from_py instead.\n", + "26/05/06 15:02:21 WARNING SystemDSContext: Deprecated method from_numpy. Use from_py instead.\n", + "INFO:simple_example:[[ 50.01 125.05 160.05 32.01 204.04]\n", + " [ 37.01 81.03 268.04 110.02 6.03]\n", + " [ 39.01 244.04 430.05 140.04 368.04]\n", + " [246.03 154.02 267.03 104.04 72.02]\n", + " [ 14.02 186.02 475.05 168.02 315.05]]\n" + ] + } + ], + "source": [ + "import logging\n", + "\n", + "import numpy as np\n", + "from systemds.context import SystemDSContext\n", + "\n", + "logger = logging.getLogger('simple_example')\n", + "logger.setLevel(logging.INFO)\n", + "\n", + "# create a random array\n", + "m1 = np.array(np.random.randint(100, size=5 * 5) + 1.01, dtype=np.double)\n", + "m1 = m1.reshape(5, 5)\n", + "# create another random array\n", + "m2 = np.array(np.random.randint(5, size=5 * 5) + 1, dtype=np.double)\n", + "m2 = m2.reshape(5, 5)\n", + "\n", + "# Create a context\n", + "with SystemDSContext() as sds:\n", + " # element-wise matrix multiplication, note that nothing is executed yet!\n", + " m_res = sds.from_numpy(m1) * sds.from_numpy(m2)\n", + " # lets do the actual computation in SystemDS! The result is an numpy array\n", + " m_res_np = m_res.compute()\n", + " logger.info(m_res_np)" + ] + }, + { + "cell_type": "markdown", + "id": "87cc9e28", + "metadata": {}, + "source": [ + "# Algorithms as built-in functions" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "bf202fb1", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: Using incubator modules: jdk.incubator.vector\n", + "26/05/06 15:05:20 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", + "26/05/06 15:05:20 WARNING SystemDSContext: Deprecated method from_numpy. Use from_py instead.\n", + "26/05/06 15:05:20 WARNING SystemDSContext: Deprecated method from_numpy. Use from_py instead.\n", + "INFO:simple_example:[[ 0.02033445]\n", + " [-0.00324092]\n", + " [ 0.0014692 ]\n", + " [ 0.02649209]\n", + " [-0.00616902]\n", + " [-0.0095087 ]\n", + " [ 0.01039221]\n", + " [-0.0011352 ]\n", + " [-0.01686351]\n", + " [-0.03839821]]\n" + ] + } + ], + "source": [ + "import logging\n", + "\n", + "import numpy as np\n", + "from systemds.context import SystemDSContext\n", + "from systemds.operator.algorithm import l2svm\n", + "\n", + "logger = logging.getLogger('simple_example')\n", + "logger.setLevel(logging.INFO)\n", + "\n", + "# Set a seed\n", + "np.random.seed(0)\n", + "# Generate random features and labels in numpy\n", + "# This can easily be exchanged with a data set.\n", + "features = np.array(np.random.randint(\n", + " 100, size=10 * 10) + 1.01, dtype=np.double)\n", + "features = features.reshape(10, 10)\n", + "labels = np.zeros((10, 1))\n", + "\n", + "# l2svm labels can only be 0 or 1\n", + "for i in range(10):\n", + " if np.random.random() > 0.5:\n", + " labels[i][0] = 1\n", + "\n", + "# compute our model\n", + "with SystemDSContext() as sds:\n", + " model = l2svm(sds.from_numpy(features),\n", + " sds.from_numpy(labels), verbose=False).compute()\n", + " logger.info(model)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "266e1c4b", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: Using incubator modules: jdk.incubator.vector\n", + "26/05/06 15:07:21 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", + "INFO:simple_example:[[-0.02674835]\n", + " [-0.01684308]\n", + " [ 0.02429331]\n", + " [ 0.03332674]\n", + " [-0.00411165]\n", + " [ 0.00514915]\n", + " [ 0.01035419]\n", + " [ 0.00611115]\n", + " [-0.02550657]\n", + " [-0.0117999 ]]\n" + ] + } + ], + "source": [ + "import logging\n", + "\n", + "from systemds.context import SystemDSContext\n", + "from systemds.operator.algorithm import l2svm\n", + "\n", + "logger = logging.getLogger('simple_example')\n", + "logger.setLevel(logging.INFO)\n", + "\n", + "with SystemDSContext() as sds:\n", + " # Generate 10 by 10 matrix with values in range 0 to 100.\n", + " features2 = sds.rand(10, 10, 0, 100)\n", + " # Add value to all cells in features\n", + " features2 += 1.1\n", + " # Generate labels of all ones and zeros\n", + " labels2 = sds.rand(10, 1, 1, 1, sparsity=0.5)\n", + "\n", + " model2 = l2svm(features2, labels2, verbose=False).compute()\n", + " logger.info(model2)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python_venv (3.12.3)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/main/python/requirements.txt b/src/main/python/requirements.txt new file mode 100644 index 00000000000..3c7af412a4c --- /dev/null +++ b/src/main/python/requirements.txt @@ -0,0 +1,9 @@ +numpy +scipy +py4j +wheel +requests +setuptools + +scikit-learn +matplotlib From 021bb7de77d8a4531e8697271c9f520fd6a42db1 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Thu, 21 May 2026 14:33:53 +0200 Subject: [PATCH 02/20] Add CLAUDE.md --- CLAUDE.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..d3fcc2be49c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,113 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +Apache SystemDS — an end-to-end ML system that compiles DML scripts (R-like syntax) into hybrid execution plans across local (CP), Apache Spark, GPU, and federated backends. + +## Build Commands + +```bash +# Full build (skips tests) +mvn clean package -DskipTests + +# Build and run default tests +mvn clean package + +# Run a single Java test class +mvn test -Dtest=FullMatrixMultiplicationTest + +# Run a test with a specific method +mvn test -Dtest=FullMatrixMultiplicationTest#testMethod + +# Run checkstyle (disabled by default) +mvn checkstyle:check -Dcheckstyle.skip=false + +# Python tests (from src/main/python/) +cd src/main/python && source python_venv/bin/activate && pip install -e . && python -m pytest tests/ + +# Run a DML script directly (after building) +./bin/systemds hello.dml +``` + +The surefire default test is `org.apache.sysds.test.usertest.**`. Override with `-Dtest=` to target specific classes. Test JVM is configured with `-Xmx3000m`. + +## Code Style + +Apply the Eclipse formatter profile at [dev/CodeStyle_eclipse.xml](dev/CodeStyle_eclipse.xml) before committing. Checkstyle rules are at [dev/checkstyle/checkstyle.xml](dev/checkstyle/checkstyle.xml). + +## Commit Tags + +All commits must be prefixed: `[SYSTEMDS-#]` for Jira issues, `[MINOR]` for small changes, `[DOC]` for docs, `[HOTFIX]` for release patches. The project uses linear history — rebase, never merge commits. + +## Compilation Pipeline + +The full compilation sequence is in [DMLScript.java:460-510](src/main/java/org/apache/sysds/api/DMLScript.java#L460): + +1. **Parse** — DML source → `DMLProgram` AST (ANTLR-based, `parser/dml/`) +2. **Live Variable Analysis + Validate** — `DMLTranslator.liveVariableAnalysis()` / `validateParseTree()` +3. **Construct HOPs** — AST → HOP DAG (`DMLTranslator.constructHops()`) +4. **Rewrite HOP DAGs** — algebraic simplifications, IPA, memory estimates, CSE (`hops/rewrite/`) +5. **Construct LOPs** — HOP DAG → LOP DAG; exec type (CP/Spark/GPU/Fed) selected here +6. **Rewrite LOP DAGs** — inject prefetch, broadcast, OOC tee operators +7. **Generate runtime program** — LOPs emit instruction strings; codegen (Spoof) fuses operators +8. **Execute** — `ProgramBlock` hierarchy interprets instructions via `ExecutionContext` + +## Architecture Patterns + +### Two-Level IR: HOPs → LOPs +`Hop` ([hops/Hop.java](src/main/java/org/apache/sysds/hops/Hop.java)) is the algebraic IR. Each concrete subclass (`BinaryOp`, `AggBinaryOp`, `UnaryOp`, etc.) implements abstract template methods: +- `constructLops()` — lowers this operator to backend-specific LOPs +- `optFindExecType()` — selects CP / Spark / GPU / Fed based on cost model +- `inferOutputCharacteristics()` — size/sparsity estimation +- `computeOutputMemEstimate()` / `computeIntermediateMemEstimate()` — memory cost + +`Lop` ([lops/Lop.java](src/main/java/org/apache/sysds/lops/Lop.java)) is the backend-specific IR. LOPs emit instruction strings consumed by the runtime. + +### DAGs, not Trees +Both HOPs and LOPs are DAGs with explicit `_input`/`_parent` lists and a `_visited` boolean. All rewrite/lowering passes do DFS traversal with the visited flag. This enables CSE — shared subcomputations appear once in the DAG. + +### Chain of Responsibility for Rewrites +`ProgramRewriter` ([hops/rewrite/ProgramRewriter.java](src/main/java/org/apache/sysds/hops/rewrite/ProgramRewriter.java)) holds an ordered list of `HopRewriteRule` subclasses and fires each over the full DAG. To add an optimization: subclass `HopRewriteRule`, implement `rewriteHopDAGs()` / `rewriteHopDAG()`, and register it. Existing rules include loop vectorization, loop-invariant hoisting, Spark checkpoint injection, OOC tee injection, and compressed reblock. + +### Parallel AST ↔ Runtime Hierarchies +The parser (`Statement`/`StatementBlock`) and runtime (`ProgramBlock`) mirror each other exactly: `ForStatement` → `ForProgramBlock`, `ParForStatement` → `ParForProgramBlock`, etc. The AST is structural only; all execution behavior lives in the runtime mirror. + +### String-Serialized Instruction Boundary +LOPs emit plain strings as instructions. At runtime, `InstructionParser.parseSingleInstruction()` reads the exec-type prefix (`CP·`, `SPARK·`, `GPU·`, `FED·`, `OOC·`) and dispatches to the appropriate backend parser. This decouples the compiler from all backend implementations. + +### Caching Layer +`CacheableData` ([runtime/controlprogram/caching/CacheableData.java](src/main/java/org/apache/sysds/runtime/controlprogram/caching/CacheableData.java)) is a generic abstract envelope for large data objects. Subclasses (`MatrixObject`, `FrameObject`, `TensorObject`) inherit full eviction/spill-to-disk lifecycle. + +## Key Source Locations + +| Area | Path | +|---|---| +| Compiler entry point | `src/main/java/org/apache/sysds/api/DMLScript.java` | +| DML → HOP translator | `src/main/java/org/apache/sysds/parser/DMLTranslator.java` | +| HOP base + optimizer | `src/main/java/org/apache/sysds/hops/` | +| HOP rewrites | `src/main/java/org/apache/sysds/hops/rewrite/` | +| LOP base | `src/main/java/org/apache/sysds/lops/` | +| Runtime control flow | `src/main/java/org/apache/sysds/runtime/controlprogram/` | +| Runtime instructions | `src/main/java/org/apache/sysds/runtime/instructions/` | +| Compressed Linear Algebra | `src/main/java/org/apache/sysds/runtime/compress/` | +| Operator fusion (Spoof) | `src/main/java/org/apache/sysds/runtime/codegen/` | +| Out-of-core execution | `src/main/java/org/apache/sysds/runtime/ooc/` | +| Built-in algorithms (DML) | `scripts/builtin/` | +| Staging area (new algos) | `scripts/staging/` | +| Python API | `src/main/python/systemds/` | + +## Testing + +Java tests extend `AutomatedTestBase`. The default exec mode is `ExecMode.HYBRID`; tests call `setExecMode()` / `resetExecMode()` to test specific backends. + +- **`src/test/java/.../test/functions/`** — end-to-end integration tests that run DML scripts through the full pipeline +- **`src/test/java/.../test/component/`** — unit tests targeting specific subsystems (compress, matrix, codegen, ooc, parfor, etc.) +- **`src/test/java/.../test/applications/`** — full ML algorithm correctness tests + +Each function test has a paired DML script under `src/test/scripts/functions/`. + +## Adding a New Built-in Algorithm + +New algorithms live in `scripts/staging/` until they have sufficient test coverage, then move to `scripts/builtin/`. The Python API wrappers in `src/main/python/systemds/operator/algorithm/builtin/` are auto-generated — see `src/main/python/generator/` for the generation scripts. From 4e9bdd10e6949fb3321211fbbe02980d642ccbe8 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Fri, 26 Jun 2026 20:20:31 +0200 Subject: [PATCH 03/20] Initial DPBuiltinCPInstruction and RDPAccountant --- .../cp/DPBuiltinCPInstruction.java | 349 +++++++++++++++++ .../runtime/privacy/dp/RDPAccountant.java | 283 ++++++++++++++ .../cp/DPBuiltinCPInstructionTest.java | 350 ++++++++++++++++++ 3 files changed, 982 insertions(+) create mode 100755 src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java create mode 100755 src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java create mode 100755 src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java diff --git a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java new file mode 100755 index 00000000000..f75ccb13cdb --- /dev/null +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java @@ -0,0 +1,349 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sysds.runtime.instructions.cp; + +import org.apache.sysds.runtime.DMLRuntimeException; +import org.apache.sysds.runtime.controlprogram.context.ExecutionContext; +import org.apache.sysds.runtime.functionobjects.Plus; +import org.apache.sysds.runtime.instructions.InstructionUtils; +import org.apache.sysds.runtime.matrix.data.MatrixBlock; +import org.apache.sysds.runtime.matrix.operators.BinaryOperator; +import org.apache.sysds.runtime.privacy.dp.RDPAccountant; + +import java.util.LinkedHashMap; +import java.util.concurrent.ThreadLocalRandom; + +/** + * CP instruction for differential-privacy release of an already-computed + * aggregate. + * + *

DML syntax (post-aggregate form, Option A): + *

+ *   result = dp_laplace(aggregate, sensitivity=1.0, epsilon=0.5)
+ *   result = dp_gaussian(aggregate, sensitivity=1.0, epsilon=0.5, delta=1e-5)
+ * 
+ * + *

The instruction receives a materialised matrix (the aggregate result), + * injects calibrated noise element-wise, records the release with the + * session-scoped {@link RDPAccountant}, and returns the noisy matrix. + * + *

Noise is generated in Java and added via a {@code MatrixBlock} binary + * operation so that the output allocation path is identical to every other + * CP instruction (no special memory-management required). + * + *

The {@link #sensitivityOf} method is deliberately separated from the + * noise-scale computation. In Phase 1 it returns the caller-supplied + * constant. In the future HOP-level rewrite pass (Phase 2) the body of this + * single method is replaced with a static analysis that reads the + * sensitivity bound computed by the compiler; every other line in this class + * stays unchanged. + * + *

Registration required in: + *

    + *
  • {@code org.apache.sysds.common.Builtins} – add + * {@code DP_LAPLACE("dp_laplace", false)} and + * {@code DP_GAUSSIAN("dp_gaussian", false)}
  • + *
  • {@code org.apache.sysds.runtime.instructions.CPInstructionParser} – + * add opcode-to-type mappings and a parse branch that returns a + * {@code DPBuiltinCPInstruction}
  • + *
  • {@code org.apache.sysds.runtime.controlprogram.context.ExecutionContext} + * – add {@code getRDPAccountant()} returning a session-scoped + * {@link RDPAccountant} (lazy-initialised field)
  • + *
+ */ +public class DPBuiltinCPInstruction extends ComputationCPInstruction { + + // ----------------------------------------------------------------------- + // Constants + // ----------------------------------------------------------------------- + + /** Opcode registered in Builtins and CPInstructionParser. */ + public static final String OPCODE_LAPLACE = "dp_laplace"; + public static final String OPCODE_GAUSSIAN = "dp_gaussian"; + + // ----------------------------------------------------------------------- + // Fields + // ----------------------------------------------------------------------- + + /** + * Named parameters extracted from the serialised instruction string. + * Keys: "target", "sensitivity", "epsilon", "delta" (Gaussian only). + * + * Using the same LinkedHashMap convention as + * ParameterizedBuiltinCPInstruction so that CPInstructionParser can + * call the shared constructParameterMap() helper unchanged. + */ + private final LinkedHashMap _params; + + // ----------------------------------------------------------------------- + // Constructor (private – use parseInstruction) + // ----------------------------------------------------------------------- + + private DPBuiltinCPInstruction( + CPOperand input, + CPOperand output, + String opcode, + String istr, + LinkedHashMap params) { + // input1 = the aggregate matrix; input2/3 unused at this level + // (scalars come from _params, not CPOperand fields, so that they + // can be either literals or DML variable names transparently). + super(null, input, null, null, output, opcode, istr); + _params = params; + } + + // ----------------------------------------------------------------------- + // Static factory / parser + // ----------------------------------------------------------------------- + + /** + * Reconstructs a {@code DPBuiltinCPInstruction} from its serialised + * instruction string produced by the LOP layer. + * + *

Expected format (INSTRUCTION_DELIM = '\u00b0'): + *

+     *   dp_gaussian°target=mVar1·MATRIX·FP64°sensitivity=1.0·SCALAR·FP64·true
+     *              °epsilon=0.5·SCALAR·FP64·true°delta=1e-5·SCALAR·FP64·true
+     *              °_mVar2·MATRIX·FP64
+     * 
+ * + * The first token is always the opcode; the last token is always the + * output operand; the tokens in between are key=value pairs. This matches + * the convention used by ParameterizedBuiltinCPInstruction exactly. + */ + public static DPBuiltinCPInstruction parseInstruction(String str) { + String[] parts = InstructionUtils.getInstructionPartsWithValueType(str); + InstructionUtils.checkNumFields(parts, 4, 5); // laplace=4, gaussian=5 + String opcode = parts[0]; + + // Output operand is always the last token. + CPOperand output = new CPOperand(parts[parts.length - 1]); + + // The "target" parameter holds the variable name of the input matrix. + // ParameterizedBuiltinCPInstruction.constructParameterMap strips the + // type suffixes and returns bare key=value pairs. + LinkedHashMap params = + ParameterizedBuiltinCPInstruction.constructParameterMap(parts); + + // The target CPOperand is needed by ComputationCPInstruction's + // getInputs() / getLineageItem() machinery. + CPOperand input = new CPOperand(params.get("target"), + org.apache.sysds.common.Types.ValueType.FP64, + org.apache.sysds.common.Types.DataType.MATRIX); + + // Validate required keys. + if (!params.containsKey("sensitivity")) + throw new DMLRuntimeException(opcode + ": missing 'sensitivity'"); + if (!params.containsKey("epsilon")) + throw new DMLRuntimeException(opcode + ": missing 'epsilon'"); + if (opcode.equals(OPCODE_GAUSSIAN) && !params.containsKey("delta")) + throw new DMLRuntimeException(opcode + ": missing 'delta'"); + + return new DPBuiltinCPInstruction(input, output, opcode, str, params); + } + + // ----------------------------------------------------------------------- + // Core execution + // ----------------------------------------------------------------------- + + /** + * Executes the DP release. + * + *
    + *
  1. Read the aggregate {@link MatrixBlock} from the variable table.
  2. + *
  3. Determine sensitivity via {@link #sensitivityOf} (Phase-1 stub).
  4. + *
  5. Generate a noise {@link MatrixBlock} of the same shape.
  6. + *
  7. Add noise element-wise using the existing binary-operator path.
  8. + *
  9. Record the release with the session-scoped + * {@link RDPAccountant}; throw if budget is exhausted.
  10. + *
  11. Write the noisy block back to the variable table and release + * the input pin.
  12. + *
+ */ + @Override + public void processInstruction(ExecutionContext ec) { + + // ── 1. Read aggregate input ───────────────────────────────────────── + // getMatrixInput pins the block in memory and increments the + // reference count; we must call releaseMatrixInput afterwards. + MatrixBlock inBlock = ec.getMatrixInput(_params.get("target")); + + // ── 2. Parse DP parameters ────────────────────────────────────────── + double epsilon = parsePositiveDouble("epsilon"); + double delta = instOpcode.equals(OPCODE_GAUSSIAN) + ? parsePositiveDouble("delta") : 0.0; + + // ── 3. Determine sensitivity (Phase-1: caller-supplied constant) ──── + double sensitivity = sensitivityOf(inBlock); + + // ── 4. Generate and add noise ──────────────────────────────────────── + MatrixBlock noiseBlock = generateNoise(inBlock, sensitivity, epsilon, delta); + + // Element-wise addition via the standard binary-operator path. + // binaryOperations allocates the output block internally. + BinaryOperator plusOp = new BinaryOperator(Plus.getPlusFnObject()); + MatrixBlock outBlock = new MatrixBlock(); + inBlock.binaryOperations(plusOp, noiseBlock, outBlock); + + // ── 5. Record release and enforce budget ──────────────────────────── + // getRDPAccountant() returns a lazy-initialised RDPAccountant that is + // owned by this ExecutionContext (added in a companion EC patch). + RDPAccountant accountant = ec.getRDPAccountant(); + accountant.compose(epsilon, delta, sensitivity); // throws on exhaustion + + // ── 6. Write output and release input pin ─────────────────────────── + ec.releaseMatrixInput(_params.get("target")); + ec.setMatrixOutput(output.getName(), outBlock); + } + + // ----------------------------------------------------------------------- + // Sensitivity seam (Phase-1 stub; Phase-2 replaces this body only) + // ----------------------------------------------------------------------- + + /** + * Returns the sensitivity of {@code aggregate} to a single-record change. + * + *

Phase 1 (now): returns the caller-supplied literal from the + * DML script. Sensitivity analysis is the caller's responsibility. + * + *

Phase 2 (HOP-level rewrite pass): replace this body with a + * call that inspects the HOP node that produced {@code aggregate}, reads + * the {@code sensitivityBound} field computed during compilation, and + * returns it. No other line in this class changes. + * + * @param aggregate the already-computed aggregate block (ignored in + * Phase 1; used in Phase 2 to look up lineage) + * @return caller-supplied sensitivity constant + */ + private double sensitivityOf(MatrixBlock aggregate) { + // Phase 1: unwrap the literal or variable value from the param map. + // In Phase 2, replace the body below with HOP-annotation lookup. + return parsePositiveDouble("sensitivity"); + } + + // ----------------------------------------------------------------------- + // Noise generation + // ----------------------------------------------------------------------- + + /** + * Generates a noise {@link MatrixBlock} of the same shape as + * {@code aggregate}, filled with samples from the mechanism-appropriate + * distribution calibrated to ({@code sensitivity}, {@code epsilon}, + * {@code delta}). + * + *

Both mechanisms produce a dense block. Sparsity exploitation is + * left for future work; for the aggregate outputs targeted here (e.g. + * column means, row sums) the aggregate is already dense. + */ + private MatrixBlock generateNoise( + MatrixBlock aggregate, + double sensitivity, + double epsilon, + double delta) { + + int rows = aggregate.getNumRows(); + int cols = aggregate.getNumColumns(); + MatrixBlock noise = new MatrixBlock(rows, cols, false); // dense + noise.allocateDenseBlock(); + + if (instOpcode.equals(OPCODE_LAPLACE)) { + fillLaplaceNoise(noise, sensitivity / epsilon); + } else { + // Gaussian mechanism: calibrate sigma for (epsilon, delta)-DP. + // Standard formula: sigma >= sensitivity * sqrt(2 * ln(1.25/delta)) / epsilon + double sigma = sensitivity + * Math.sqrt(2.0 * Math.log(1.25 / delta)) + / epsilon; + fillGaussianNoise(noise, sigma); + } + + noise.recomputeNonZeros(); + return noise; + } + + /** + * Fills {@code block} with i.i.d. Laplace(0, scale) samples using the + * inverse-CDF method. + * + *

For u ~ Uniform(0, 1): X = -scale * sign(u - 0.5) * ln(1 - 2|u - 0.5|) + */ + private static void fillLaplaceNoise(MatrixBlock block, double scale) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + int rows = block.getNumRows(); + int cols = block.getNumColumns(); + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + double u = rng.nextDouble(); // u in (0, 1) + double v = u - 0.5; + // Guard against the degenerate u == 0.5 case (ln(0) = -inf). + if (v == 0.0) v = 1e-15; + double sample = -scale * Math.signum(v) * Math.log(1.0 - 2.0 * Math.abs(v)); + block.set(r, c, sample); + } + } + } + + /** + * Fills {@code block} with i.i.d. N(0, sigma²) samples. + * + *

Uses {@link ThreadLocalRandom#nextGaussian()} which is thread-safe + * and does not require external libraries. + */ + private static void fillGaussianNoise(MatrixBlock block, double sigma) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + int rows = block.getNumRows(); + int cols = block.getNumColumns(); + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + block.set(r, c, sigma * rng.nextGaussian()); + } + } + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + /** + * Parses a parameter value as a positive {@code double}. + * + * @throws DMLRuntimeException if the key is absent, unparseable, or + * non-positive + */ + private double parsePositiveDouble(String key) { + String raw = _params.get(key); + if (raw == null) + throw new DMLRuntimeException( + instOpcode + ": parameter '" + key + "' is missing"); + double v; + try { + v = Double.parseDouble(raw); + } catch (NumberFormatException e) { + throw new DMLRuntimeException( + instOpcode + ": parameter '" + key + + "' is not a valid number: " + raw); + } + if (!(v > 0.0)) + throw new DMLRuntimeException( + instOpcode + ": parameter '" + key + + "' must be strictly positive, got " + v); + return v; + } +} diff --git a/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java b/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java new file mode 100755 index 00000000000..884012e4ee6 --- /dev/null +++ b/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sysds.runtime.privacy.dp; + +import org.apache.sysds.runtime.DMLRuntimeException; + +/** + * Session-scoped Rényi Differential Privacy (RDP) budget accountant. + * + *

Purpose

+ * Tracks composition of DP releases across the lifetime of a DML script + * execution. Each call to {@link #compose} records one release and checks + * whether the cumulative privacy cost has exceeded the user-specified budget. + * + *

Why Rényi DP?

+ * Basic composition adds epsilons linearly, giving very loose bounds with + * many releases. Rényi DP divergences compose additively at the + * same order α. Converting the running Rényi sum to (ε, δ) via the standard + * conversion formula yields substantially tighter bounds — particularly for + * Gaussian mechanisms, which are common in federated learning. + * + *

Orders tracked

+ * We track a discrete set of Rényi orders α ∈ {2, 4, 8, 16, 32, 64, 128, + * 256, 512, 1024}. At query time we take the minimum converted ε across all + * orders, which is the tightest available bound. + * + *

Composition rules

+ * For the Gaussian mechanism with noise scale σ and sensitivity Δf, the + * Rényi divergence of order α between outputs on neighbouring datasets is: + *
+ *   D_α = α · Δf² / (2σ²)
+ * 
+ * where σ is back-derived from the caller's (ε, δ) parameters via the + * standard calibration formula. See {@link #rdpGaussian} for details. + * + * For the Laplace mechanism with scale b = Δf/ε, the Rényi divergence at + * order α is: + *
+ *   D_α = (1/(α-1)) · ln( α/(2α-1) · exp((α-1)/b) + (α-1)/(2α-1) · exp(-α/b) )
+ *       (for α > 1; the limit as α → 1 is 1/b, i.e. the KL divergence)
+ * 
+ * + *

Conversion: Rényi DP → (ε, δ)-DP

+ * Given accumulated Rényi divergence R[α] at order α and a target δ: + *
+ *   ε(α) = R[α] + log(1 - 1/α) - log(δ · (α - 1)) / α
+ * 
+ * The reported total cost is min_α ε(α). + * + *

Lifecycle

+ * One instance is created per {@code ExecutionContext} (lazy init). It is + * garbage-collected with the context when the script finishes; no state + * leaks between script executions or between concurrent scripts. + * + *

Thread safety

+ * Not thread-safe. A single DML script executes instructions sequentially + * on one thread, so no synchronisation is needed. + * + * @see DPBuiltinCPInstruction + */ +public class RDPAccountant { + + // ----------------------------------------------------------------------- + // Rényi orders to track + // ----------------------------------------------------------------------- + + /** + * Discrete set of Rényi orders α. All must be > 1. + * Finer grids give tighter bounds; this set is a reasonable default + * that covers the range relevant for typical ML workloads. + */ + private static final double[] ORDERS = { + 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 + }; + + // ----------------------------------------------------------------------- + // State + // ----------------------------------------------------------------------- + + /** Running accumulated Rényi divergence at each order. */ + private final double[] _rdpSum = new double[ORDERS.length]; + + /** User-specified total privacy budget (ε). */ + private final double _epsilonBudget; + + /** User-specified δ used for the RDP-to-(ε,δ) conversion. */ + private final double _delta; + + /** Number of releases recorded so far (for error messages). */ + private int _releaseCount = 0; + + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates an accountant with the given global budget. + * + *

Typical usage: the DML script sets the budget once at the top + * (future work: a {@code dp_set_budget(epsilon, delta)} built-in), + * or the accountant is created with defaults and the budget is checked + * after each release. + * + * @param epsilonBudget total ε budget for the script execution (must be > 0) + * @param delta δ used for the RDP-to-(ε,δ) conversion (must be in (0,1)) + */ + public RDPAccountant(double epsilonBudget, double delta) { + if (!(epsilonBudget > 0)) + throw new DMLRuntimeException( + "RDPAccountant: epsilonBudget must be > 0, got " + epsilonBudget); + if (!(delta > 0 && delta < 1)) + throw new DMLRuntimeException( + "RDPAccountant: delta must be in (0,1), got " + delta); + _epsilonBudget = epsilonBudget; + _delta = delta; + } + + /** + * Convenience constructor using a liberal default δ = 1e-5. + * Suitable when the calling script does not specify δ explicitly. + */ + public RDPAccountant(double epsilonBudget) { + this(epsilonBudget, 1e-5); + } + + // ----------------------------------------------------------------------- + // Core API + // ----------------------------------------------------------------------- + + /** + * Records one DP release and checks the budget. + * + *

This method must be called before the result is written to + * the variable table. If the budget is exhausted, it throws and the + * caller's result is discarded, preventing an unaccounted release. + * + *

The mechanism type (Laplace vs Gaussian) is inferred from the + * parameters: if {@code delta == 0} the release is treated as Laplace; + * otherwise it is treated as Gaussian. + * + * @param epsilon the ε parameter for this individual release (> 0) + * @param delta the δ parameter for this release (0 for Laplace) + * @param sensitivity the L2 sensitivity Δf of the released quantity (> 0) + * @throws DMLRuntimeException if the cumulative ε after this release + * would exceed the budget + */ + public void compose(double epsilon, double delta, double sensitivity) { + _releaseCount++; + + // Accumulate Rényi divergence at each order. + for (int i = 0; i < ORDERS.length; i++) { + double alpha = ORDERS[i]; + double rdp; + if (delta == 0.0) { + rdp = rdpLaplace(alpha, sensitivity, epsilon); + } else { + // Back-derive σ from the (ε, δ) calibration formula for the + // Gaussian mechanism, then compute the RDP contribution. + double sigma = gaussianSigma(sensitivity, epsilon, delta); + rdp = rdpGaussian(alpha, sensitivity, sigma); + } + _rdpSum[i] += rdp; + } + + // Convert accumulated RDP to (ε, δ) and check. + double spentEpsilon = totalEpsilonSpent(); + if (spentEpsilon > _epsilonBudget) { + throw new DMLRuntimeException(String.format( + "Privacy budget exhausted after %d release(s): " + + "spent ε ≈ %.6f exceeds budget ε = %.6f (δ = %.2e). " + + "Reduce the number of releases or widen the budget.", + _releaseCount, spentEpsilon, _epsilonBudget, _delta)); + } + } + + // ----------------------------------------------------------------------- + // Inspection + // ----------------------------------------------------------------------- + + /** + * Returns the current total privacy cost as an ε value under the + * accountant's δ, using the tightest available Rényi order. + */ + public double totalEpsilonSpent() { + double minEpsilon = Double.MAX_VALUE; + for (int i = 0; i < ORDERS.length; i++) { + double alpha = ORDERS[i]; + // Standard RDP-to-(ε,δ) conversion: + // ε(α) = R[α] + log(1 - 1/α) - log(δ·(α-1)) / α + // Reference: Mironov 2017, Proposition 3. + double eps = _rdpSum[i] + + Math.log(1.0 - 1.0 / alpha) + - Math.log(_delta * (alpha - 1.0)) / alpha; + if (eps < minEpsilon) + minEpsilon = eps; + } + return minEpsilon; + } + + /** Returns the remaining ε budget (may be negative if budget is exceeded). */ + public double remainingBudget() { + return _epsilonBudget - totalEpsilonSpent(); + } + + /** Returns the number of DP releases recorded so far. */ + public int releaseCount() { + return _releaseCount; + } + + // ----------------------------------------------------------------------- + // Mechanism-specific RDP contributions + // ----------------------------------------------------------------------- + + /** + * Rényi divergence of order α for the Laplace mechanism with scale + * b = sensitivity / epsilon. + * + *

For α > 1 and integer α, the closed form is: + *

+     *   D_α = (1/(α-1)) · ln( α/(2α-1)·exp((α-1)/b) + (α-1)/(2α-1)·exp(-α/b) )
+     * 
+ * + *

For non-integer α we use the same formula, which is the natural + * analytic continuation (see Mironov 2017, Proposition 3, example 1). + * We clamp the log argument to avoid NaN when inputs are degenerate. + */ + private static double rdpLaplace(double alpha, double sensitivity, double epsilon) { + double b = sensitivity / epsilon; // Laplace scale + double t1 = alpha / (2.0 * alpha - 1.0) * Math.exp((alpha - 1.0) / b); + double t2 = (alpha - 1.0) / (2.0 * alpha - 1.0) * Math.exp(-alpha / b); + double arg = t1 + t2; + if (arg <= 0) return 0.0; // degenerate: treat as zero cost + return Math.log(arg) / (alpha - 1.0); + } + + /** + * Rényi divergence of order α for the Gaussian mechanism with noise + * scale σ and L2 sensitivity Δf. + * + *

For α > 1: + *

+     *   D_α = α · Δf² / (2σ²)
+     * 
+ * + *

This is the standard result for the Gaussian mechanism (see + * Mironov 2017, Proposition 3, example 2). + */ + private static double rdpGaussian(double alpha, double sensitivity, double sigma) { + return alpha * (sensitivity * sensitivity) / (2.0 * sigma * sigma); + } + + /** + * Back-derives the Gaussian noise scale σ from the (ε, δ)-DP parameters + * using the standard calibration inequality: + *

+     *   σ = Δf · sqrt(2 · ln(1.25 / δ)) / ε
+     * 
+ * + *

This is the formula used by {@code DPBuiltinCPInstruction} to + * generate the actual noise, so the RDP contribution it records is + * exactly consistent with the noise injected. + */ + private static double gaussianSigma(double sensitivity, double epsilon, double delta) { + return sensitivity * Math.sqrt(2.0 * Math.log(1.25 / delta)) / epsilon; + } +} diff --git a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java new file mode 100755 index 00000000000..e9bc2c6ea9e --- /dev/null +++ b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sysds.test.functions.privacy.dp; + +import org.apache.sysds.runtime.DMLRuntimeException; +import org.apache.sysds.runtime.privacy.dp.RDPAccountant; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Tests for {@code DPBuiltinCPInstruction} and {@code RDPAccountant}. + * + *

The tests are grouped into three levels: + *

    + *
  1. Unit tests on RDPAccountant — verify composition, conversion, + * and budget enforcement in isolation, with no dependency on the full + * SystemDS runtime.
  2. + *
  3. Noise distribution tests — verify that the noise blocks + * generated by the Laplace and Gaussian mechanisms have statistically + * correct means and variances (Kolmogorov-Smirnov style sanity checks).
  4. + *
  5. DML integration tests — run complete DML scripts and verify + * end-to-end correctness via the existing AutomatedTestBase machinery.
  6. + *
+ * + *

The DML integration tests require a built SystemDS jar and are separated + * into a companion class {@code DPBuiltinDMLTest} (shown at the bottom of + * this file as a skeleton). + */ +public class DPBuiltinCPInstructionTest { + + private static final double EPS = 1e-9; + + // ======================================================================= + // 1. RDPAccountant unit tests + // ======================================================================= + + @Test + public void testAccountantInitialisesAtZeroCost() { + RDPAccountant acc = new RDPAccountant(1.0, 1e-5); + // No releases yet: total cost should be a large negative number + // (conversion formula gives -∞ when rdpSum = 0 for all orders), + // so remainingBudget() should exceed the budget. + assertTrue("No releases should leave budget intact", + acc.remainingBudget() > 0); + assertEquals(0, acc.releaseCount()); + } + + @Test + public void testSingleLaplaceReleaseDoesNotExceedBudget() { + // epsilon=0.5, budget=1.0: one release should consume < budget. + RDPAccountant acc = new RDPAccountant(1.0, 1e-5); + acc.compose(0.5, 0.0, 1.0); // Laplace, sensitivity=1 + assertEquals(1, acc.releaseCount()); + assertTrue("Single release within budget", + acc.totalEpsilonSpent() <= 1.0); + } + + @Test + public void testSingleGaussianReleaseDoesNotExceedBudget() { + RDPAccountant acc = new RDPAccountant(1.0, 1e-5); + acc.compose(0.5, 1e-5, 1.0); // Gaussian + assertEquals(1, acc.releaseCount()); + assertTrue("Single Gaussian release within budget", + acc.totalEpsilonSpent() <= 1.0); + } + + @Test(expected = DMLRuntimeException.class) + public void testBudgetExhaustionThrows() { + // Budget = 0.1, but we try to make 10 releases at epsilon=0.5 each. + // After enough releases the budget must be exceeded. + RDPAccountant acc = new RDPAccountant(0.1, 1e-5); + for (int i = 0; i < 10; i++) { + acc.compose(0.5, 0.0, 1.0); // will throw before the 10th + } + } + + @Test + public void testCompositionIsMonotonicallyIncreasing() { + RDPAccountant acc = new RDPAccountant(100.0, 1e-5); // large budget + double prev = acc.totalEpsilonSpent(); + for (int i = 0; i < 5; i++) { + acc.compose(0.3, 1e-5, 1.0); + double current = acc.totalEpsilonSpent(); + assertTrue("Epsilon spent must increase with each release", + current > prev); + prev = current; + } + } + + @Test + public void testGaussianTighterThanLaplaceForSameEpsilon() { + // For the same nominal (ε, δ), Gaussian uses RDP composition which + // is tighter than Laplace with basic composition. After 5 releases: + // Laplace (basic, worst-case): 5ε + // Gaussian (RDP) : something < 5ε + double eps = 0.5; + double delta = 1e-5; + + RDPAccountant gaussian = new RDPAccountant(100.0, delta); + RDPAccountant laplace = new RDPAccountant(100.0, delta); + + for (int i = 0; i < 5; i++) { + gaussian.compose(eps, delta, 1.0); + laplace.compose(eps, 0.0, 1.0); + } + + // After 5 releases, Gaussian RDP bound should be tighter. + // (Both may be < 5*eps; the point is Gaussian <= Laplace.) + assertTrue("Gaussian RDP bound should be <= Laplace bound after 5 releases", + gaussian.totalEpsilonSpent() <= laplace.totalEpsilonSpent() + 1e-6); + } + + @Test + public void testRemainingBudgetDecreasesMonotonically() { + RDPAccountant acc = new RDPAccountant(2.0, 1e-5); + double prev = acc.remainingBudget(); + for (int i = 0; i < 3; i++) { + acc.compose(0.2, 1e-5, 1.0); + double current = acc.remainingBudget(); + assertTrue("Remaining budget must decrease", current < prev); + prev = current; + } + } + + @Test + public void testSmallerSensitivityCheaper() { + // A release with sensitivity 0.1 should consume less budget than + // one with sensitivity 1.0 at the same (ε, δ). + RDPAccountant acc1 = new RDPAccountant(100.0, 1e-5); + RDPAccountant acc2 = new RDPAccountant(100.0, 1e-5); + acc1.compose(0.5, 1e-5, 0.1); // low sensitivity + acc2.compose(0.5, 1e-5, 1.0); // high sensitivity + + assertTrue("Lower sensitivity must cost less budget", + acc1.totalEpsilonSpent() < acc2.totalEpsilonSpent()); + } + + // ======================================================================= + // 2. Noise distribution tests (statistical sanity checks) + // ======================================================================= + // These tests generate many samples and verify that the empirical mean + // is near zero and the empirical variance matches the theoretical value + // within a reasonable tolerance. + // + // Note: these tests exercise the static fill* methods indirectly by + // calling the noise-generation logic via reflection or by making the + // methods package-private. The simplest approach for a student project + // is to make fillLaplaceNoise / fillGaussianNoise package-private and + // call them directly from the test (same package). + + @Test + public void testLaplaceNoiseMeanNearZero() { + // For 10000 samples the empirical mean should be within 3σ/√n of 0. + int n = 10_000; + double scale = 2.0; + double[] samples = sampleLaplace(n, scale); + double mean = mean(samples); + double theoreticalStdErr = scale * Math.sqrt(2.0) / Math.sqrt(n); + assertTrue("Laplace mean should be near 0", + Math.abs(mean) < 5 * theoreticalStdErr); + } + + @Test + public void testLaplaceNoiseVarianceCorrect() { + // Var[Laplace(0, b)] = 2b². Allow 10% relative error for n=10000. + int n = 10_000; + double scale = 1.5; + double[] samples = sampleLaplace(n, scale); + double variance = variance(samples); + double expected = 2.0 * scale * scale; + assertEquals("Laplace variance", expected, variance, 0.1 * expected); + } + + @Test + public void testGaussianNoiseMeanNearZero() { + int n = 10_000; + double sigma = 3.0; + double[] samples = sampleGaussian(n, sigma); + double mean = mean(samples); + double theoreticalStdErr = sigma / Math.sqrt(n); + assertTrue("Gaussian mean should be near 0", + Math.abs(mean) < 5 * theoreticalStdErr); + } + + @Test + public void testGaussianNoiseVarianceCorrect() { + int n = 10_000; + double sigma = 2.0; + double[] samples = sampleGaussian(n, sigma); + double variance = variance(samples); + double expected = sigma * sigma; + assertEquals("Gaussian variance", expected, variance, 0.1 * expected); + } + + // ----------------------------------------------------------------------- + // Helpers for noise distribution tests + // ----------------------------------------------------------------------- + + /** Sample n Laplace(0, scale) values using the inverse-CDF method. */ + private static double[] sampleLaplace(int n, double scale) { + java.util.concurrent.ThreadLocalRandom rng = + java.util.concurrent.ThreadLocalRandom.current(); + double[] out = new double[n]; + for (int i = 0; i < n; i++) { + double u = rng.nextDouble(); + double v = u - 0.5; + if (v == 0.0) v = 1e-15; + out[i] = -scale * Math.signum(v) * Math.log(1.0 - 2.0 * Math.abs(v)); + } + return out; + } + + /** Sample n N(0, sigma²) values. */ + private static double[] sampleGaussian(int n, double sigma) { + java.util.concurrent.ThreadLocalRandom rng = + java.util.concurrent.ThreadLocalRandom.current(); + double[] out = new double[n]; + for (int i = 0; i < n; i++) { + out[i] = sigma * rng.nextGaussian(); + } + return out; + } + + private static double mean(double[] xs) { + double s = 0; + for (double x : xs) s += x; + return s / xs.length; + } + + private static double variance(double[] xs) { + double m = mean(xs); + double s = 0; + for (double x : xs) s += (x - m) * (x - m); + return s / (xs.length - 1); + } +} + + +// ========================================================================== +// 3. DML integration test skeleton +// ========================================================================== +// +// Full integration tests extend AutomatedTestBase and drive the DML runner. +// Each test: +// (a) Writes a DML script to a temp file. +// (b) Provides input matrices via TestUtils. +// (c) Calls runTest() and reads the output MatrixBlock. +// (d) Verifies that the noisy result differs from the clean result by a +// statistically plausible amount (not zero, not astronomically large). +// +// The test below is a skeleton that compiles but needs the full SystemDS +// test infrastructure to run. + +/* +package org.apache.sysds.test.functions.privacy.dp; + +import org.apache.sysds.common.Types; +import org.apache.sysds.runtime.matrix.data.MatrixBlock; +import org.apache.sysds.test.AutomatedTestBase; +import org.apache.sysds.test.TestConfiguration; +import org.apache.sysds.test.TestUtils; +import org.junit.Test; + +public class DPBuiltinDMLTest extends AutomatedTestBase { + + private static final String TEST_DIR = "functions/privacy/dp/"; + private static final String TEST_CLASS = TEST_DIR + DPBuiltinDMLTest.class.getSimpleName() + "/"; + private static final String DML_LAPLACE = + "X = read($1);\n" + + "result = dp_laplace(colMeans(X), sensitivity=1.0, epsilon=$2);\n" + + "write(result, $3, format=\"binary\");\n"; + private static final String DML_GAUSSIAN = + "X = read($1);\n" + + "result = dp_gaussian(colMeans(X), sensitivity=1.0, epsilon=$2, delta=1e-5);\n" + + "write(result, $3, format=\"binary\");\n"; + + @Override + public void setUp() { + addTestConfiguration("DPLaplace", new TestConfiguration(TEST_CLASS, "DPLaplace")); + addTestConfiguration("DPGaussian", new TestConfiguration(TEST_CLASS, "DPGaussian")); + } + + @Test + public void testLaplaceOutputDiffersFromCleanMean() { + runDPTest("DPLaplace", DML_LAPLACE, "0.5"); + } + + @Test + public void testGaussianOutputDiffersFromCleanMean() { + runDPTest("DPGaussian", DML_GAUSSIAN, "0.5"); + } + + @Test + public void testHighEpsilonIsCloserToTruth() { + // Higher ε → less noise → result closer to the true mean. + double noisyLow = maxAbsDiff("DPGaussian", DML_GAUSSIAN, "0.1"); + double noisyHigh = maxAbsDiff("DPGaussian", DML_GAUSSIAN, "4.0"); + assertTrue("ε=4 should give less noise than ε=0.1", noisyHigh < noisyLow); + } + + private void runDPTest(String testName, String dml, String epsilonStr) { + getAndLoadTestConfiguration(testName); + int rows = 100, cols = 10; + double[][] data = TestUtils.generateTestMatrix(rows, cols, 0, 1, 1.0, 42); + writeInputMatrixWithMTD("X", data, false); + writeScriptFile(testName + ".dml", dml); + programArgs = new String[]{ input("X"), epsilonStr, output("result") }; + runTest(true, false, null, -1); + MatrixBlock result = readDMLMatrixFromHDFS("result"); + // The noisy result should be a (1 × cols) row vector. + assertEquals(1, result.getNumRows()); + assertEquals(cols, result.getNumColumns()); + // Must differ from the exact mean by a non-trivial amount. + // (A single-seed exact-equality check is fragile; use range check.) + double maxNoise = maxAbsValue(result); + assertTrue("Result should not be exactly zero", maxNoise > 0); + } + + private double maxAbsDiff(String testName, String dml, String epsilonStr) { + // Omitted for brevity: run the test, compute max |noisy - clean|. + return 0; // placeholder + } + + private static double maxAbsValue(MatrixBlock m) { + double max = 0; + for (int r = 0; r < m.getNumRows(); r++) + for (int c = 0; c < m.getNumColumns(); c++) + max = Math.max(max, Math.abs(m.get(r, c))); + return max; + } +} +*/ From 19c2504b25915a1979917737f2a5a633b0341769 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Fri, 26 Jun 2026 21:01:04 +0200 Subject: [PATCH 04/20] Add dp_gaussian and dp_laplace operations --- .../org/apache/sysds/common/Builtins.java | 2 ++ .../apache/sysds/common/InstructionType.java | 1 + .../java/org/apache/sysds/common/Opcodes.java | 4 +++ .../java/org/apache/sysds/common/Types.java | 2 +- .../sysds/hops/ParameterizedBuiltinOp.java | 15 ++++++++--- .../sysds/lops/ParameterizedBuiltin.java | 14 ++++++++++- .../parser/BuiltinFunctionExpression.java | 25 +++++++++++++++++++ .../apache/sysds/parser/DMLTranslator.java | 23 +++++++++++++++++ .../context/ExecutionContext.java | 9 +++++++ .../instructions/CPInstructionParser.java | 6 ++++- .../instructions/cp/CPInstruction.java | 3 ++- .../cp/DPBuiltinCPInstruction.java | 5 +--- .../cp/DPBuiltinCPInstructionTest.java | 14 +++++++---- 13 files changed, 106 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/apache/sysds/common/Builtins.java b/src/main/java/org/apache/sysds/common/Builtins.java index f5719641df7..be6ee0f33db 100644 --- a/src/main/java/org/apache/sysds/common/Builtins.java +++ b/src/main/java/org/apache/sysds/common/Builtins.java @@ -116,6 +116,8 @@ public enum Builtins { DECISIONTREEPREDICT("decisionTreePredict", true), DECOMPRESS("decompress", false), DEDUP("dedup", true), + DP_LAPLACE("dp_laplace", false), + DP_GAUSSIAN("dp_gaussian", false), DEEPWALK("deepWalk", true), DET("det", false), DETECTSCHEMA("detectSchema", false), diff --git a/src/main/java/org/apache/sysds/common/InstructionType.java b/src/main/java/org/apache/sysds/common/InstructionType.java index e0e77c46c59..ed795d038e2 100644 --- a/src/main/java/org/apache/sysds/common/InstructionType.java +++ b/src/main/java/org/apache/sysds/common/InstructionType.java @@ -63,6 +63,7 @@ public enum InstructionType { MMChain, Union, EINSUM, + DPBuiltin, //SP Types MAPMM, diff --git a/src/main/java/org/apache/sysds/common/Opcodes.java b/src/main/java/org/apache/sysds/common/Opcodes.java index 9a894dde13b..f9ed57eae06 100644 --- a/src/main/java/org/apache/sysds/common/Opcodes.java +++ b/src/main/java/org/apache/sysds/common/Opcodes.java @@ -194,6 +194,10 @@ public enum Opcodes { LIST("list", InstructionType.BuiltinNary), EINSUM("einsum", InstructionType.BuiltinNary), + //DP built-in functions + DP_LAPLACE("dp_laplace", InstructionType.DPBuiltin), + DP_GAUSSIAN("dp_gaussian", InstructionType.DPBuiltin), + //Parametrized builtin functions AUTODIFF("autoDiff", InstructionType.ParameterizedBuiltin), CONTAINS("contains", InstructionType.ParameterizedBuiltin), diff --git a/src/main/java/org/apache/sysds/common/Types.java b/src/main/java/org/apache/sysds/common/Types.java index 624c9eed3c6..250fc45a5c7 100644 --- a/src/main/java/org/apache/sysds/common/Types.java +++ b/src/main/java/org/apache/sysds/common/Types.java @@ -806,7 +806,7 @@ public static ReOrgOp valueOfByOpcode(String opcode) { /** Parameterized operations that require named variable arguments */ public enum ParamBuiltinOp { - AUTODIFF, CDF, CONTAINS, INVALID, INVCDF, GROUPEDAGG, RMEMPTY, REPLACE, REXPAND, + AUTODIFF, CDF, CONTAINS, DP_LAPLACE, DP_GAUSSIAN, INVALID, INVCDF, GROUPEDAGG, RMEMPTY, REPLACE, REXPAND, LOWER_TRI, UPPER_TRI, TRANSFORMAPPLY, TRANSFORMDECODE, TRANSFORMCOLMAP, TRANSFORMMETA, TOKENIZE, TOSTRING, LIST, PARAMSERV diff --git a/src/main/java/org/apache/sysds/hops/ParameterizedBuiltinOp.java b/src/main/java/org/apache/sysds/hops/ParameterizedBuiltinOp.java index 61a4b8b8f91..7761521e415 100644 --- a/src/main/java/org/apache/sysds/hops/ParameterizedBuiltinOp.java +++ b/src/main/java/org/apache/sysds/hops/ParameterizedBuiltinOp.java @@ -182,7 +182,7 @@ public Lop constructLops() } case CONTAINS: case CDF: - case INVCDF: + case INVCDF: case REPLACE: case LOWER_TRI: case UPPER_TRI: @@ -194,7 +194,9 @@ public Lop constructLops() case TOSTRING: case PARAMSERV: case LIST: - case AUTODIFF:{ + case AUTODIFF: + case DP_LAPLACE: + case DP_GAUSSIAN:{ ParameterizedBuiltin pbilop = new ParameterizedBuiltin( inputlops, _op, getDataType(), getValueType(), et); if( isMultiThreadedOpType() ) @@ -688,7 +690,11 @@ else if( _op == ParamBuiltinOp.TRANSFORMAPPLY ) { return new MatrixCharacteristics(dc.getRows(), dc.getCols(), -1, dc.getLength()); } } - + else if( _op == ParamBuiltinOp.DP_LAPLACE || _op == ParamBuiltinOp.DP_GAUSSIAN ) { + if( dc.dimsKnown() ) + ret = new MatrixCharacteristics(dc.getRows(), dc.getCols(), -1, dc.getLength()); + } + return ret; } @Override @@ -758,7 +764,8 @@ && getTargetHop().areDimsBelowThreshold() ) { if (_op == ParamBuiltinOp.TRANSFORMCOLMAP || _op == ParamBuiltinOp.TRANSFORMMETA || _op == ParamBuiltinOp.TOSTRING || _op == ParamBuiltinOp.LIST || _op == ParamBuiltinOp.CDF || _op == ParamBuiltinOp.INVCDF - || _op == ParamBuiltinOp.PARAMSERV) { + || _op == ParamBuiltinOp.PARAMSERV + || _op == ParamBuiltinOp.DP_LAPLACE || _op == ParamBuiltinOp.DP_GAUSSIAN) { _etype = ExecType.CP; } diff --git a/src/main/java/org/apache/sysds/lops/ParameterizedBuiltin.java b/src/main/java/org/apache/sysds/lops/ParameterizedBuiltin.java index 3604121aac8..48ecbaa72df 100644 --- a/src/main/java/org/apache/sysds/lops/ParameterizedBuiltin.java +++ b/src/main/java/org/apache/sysds/lops/ParameterizedBuiltin.java @@ -204,7 +204,19 @@ public String getInstructions(String output) compileGenericParamMap(sb, _inputParams); break; } - + case DP_LAPLACE: { + sb.append(Opcodes.DP_LAPLACE); + sb.append(OPERAND_DELIMITOR); + compileGenericParamMap(sb, _inputParams); + break; + } + case DP_GAUSSIAN: { + sb.append(Opcodes.DP_GAUSSIAN); + sb.append(OPERAND_DELIMITOR); + compileGenericParamMap(sb, _inputParams); + break; + } + default: throw new LopsException(this.printErrorLocation() + "In ParameterizedBuiltin Lop, Unknown operation: " + _operation); } diff --git a/src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java b/src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java index ab0c7993b4e..1425a794575 100644 --- a/src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java +++ b/src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java @@ -2005,6 +2005,31 @@ else if(this.getOpCode() == Builtins.MAX_POOL || this.getOpCode() == Builtins.AV } else raiseValidateError("Local instruction not allowed in dml script"); + case DP_LAPLACE: { + checkNumParameters(3); + checkMatrixParam(getFirstExpr()); + checkScalarParam(getSecondExpr()); + checkScalarParam(getThirdExpr()); + output.setDataType(DataType.MATRIX); + output.setValueType(ValueType.FP64); + output.setDimensions( + getFirstExpr().getOutput().getDim1(), + getFirstExpr().getOutput().getDim2()); + break; + } + case DP_GAUSSIAN: { + checkNumParameters(4); + checkMatrixParam(getFirstExpr()); + checkScalarParam(getSecondExpr()); + checkScalarParam(getThirdExpr()); + checkScalarParam(getFourthExpr()); + output.setDataType(DataType.MATRIX); + output.setValueType(ValueType.FP64); + output.setDimensions( + getFirstExpr().getOutput().getDim1(), + getFirstExpr().getOutput().getDim2()); + break; + } case COMPRESS: case DECOMPRESS: if(OptimizerUtils.ALLOW_SCRIPT_LEVEL_COMPRESS_COMMAND){ diff --git a/src/main/java/org/apache/sysds/parser/DMLTranslator.java b/src/main/java/org/apache/sysds/parser/DMLTranslator.java index a8e1667d049..7950868e0b5 100644 --- a/src/main/java/org/apache/sysds/parser/DMLTranslator.java +++ b/src/main/java/org/apache/sysds/parser/DMLTranslator.java @@ -2310,6 +2310,10 @@ private Hop processBuiltinFunctionExpression(BuiltinFunctionExpression source, D if (source.getThirdExpr() != null) { expr3 = processExpression(source.getThirdExpr(), null, hops); } + Hop expr4 = null; + if (source.getFourthExpr() != null) { + expr4 = processExpression(source.getFourthExpr(), null, hops); + } Hop currBuiltinOp = null; target = (target == null) ? createTarget(source) : target; @@ -2589,6 +2593,25 @@ else if ( sop.equalsIgnoreCase(Opcodes.NOTEQUAL.toString()) ) case DECOMPRESS: currBuiltinOp = new UnaryOp(target.getName(), target.getDataType(), ValueType.FP64, OpOp1.DECOMPRESS, expr); break; + case DP_LAPLACE: { + LinkedHashMap dpLaplaceParams = new LinkedHashMap<>(); + dpLaplaceParams.put("target", expr); + dpLaplaceParams.put("sensitivity", expr2); + dpLaplaceParams.put("epsilon", expr3); + currBuiltinOp = new ParameterizedBuiltinOp(target.getName(), DataType.MATRIX, + ValueType.FP64, ParamBuiltinOp.DP_LAPLACE, dpLaplaceParams); + break; + } + case DP_GAUSSIAN: { + LinkedHashMap dpGaussianParams = new LinkedHashMap<>(); + dpGaussianParams.put("target", expr); + dpGaussianParams.put("sensitivity", expr2); + dpGaussianParams.put("epsilon", expr3); + dpGaussianParams.put("delta", expr4); + currBuiltinOp = new ParameterizedBuiltinOp(target.getName(), DataType.MATRIX, + ValueType.FP64, ParamBuiltinOp.DP_GAUSSIAN, dpGaussianParams); + break; + } case QUANTIZE_COMPRESS: currBuiltinOp = new BinaryOp(target.getName(), target.getDataType(), target.getValueType(), OpOp2.valueOf(source.getOpCode().name()), expr, expr2); break; diff --git a/src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java b/src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java index 67cda352a73..baa6e74df7d 100644 --- a/src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java +++ b/src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java @@ -62,6 +62,7 @@ import org.apache.sysds.runtime.meta.MetaData; import org.apache.sysds.runtime.meta.MetaDataFormat; import org.apache.sysds.runtime.util.HDFSTool; +import org.apache.sysds.runtime.privacy.dp.RDPAccountant; import org.apache.sysds.utils.Statistics; import java.util.ArrayList; @@ -90,6 +91,8 @@ public class ExecutionContext { protected SEALClient _seal_client; + private RDPAccountant _rdpAccountant = null; + //parfor temporary functions (created by eval) protected Set _fnNames; @@ -144,6 +147,12 @@ public void setLineage(Lineage lineage) { _lineage = lineage; } + public RDPAccountant getRDPAccountant() { + if (_rdpAccountant == null) + _rdpAccountant = new RDPAccountant(1.0, 1e-5); + return _rdpAccountant; + } + public boolean isAutoCreateVars() { return _autoCreateVars; } diff --git a/src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java b/src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java index 92e11b425dd..ca6baf058b4 100644 --- a/src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java +++ b/src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java @@ -65,6 +65,7 @@ import org.apache.sysds.runtime.instructions.cp.UnaryCPInstruction; import org.apache.sysds.runtime.instructions.cp.VariableCPInstruction; import org.apache.sysds.runtime.instructions.cp.UnionCPInstruction; +import org.apache.sysds.runtime.instructions.cp.DPBuiltinCPInstruction; import org.apache.sysds.runtime.instructions.cp.EinsumCPInstruction; import org.apache.sysds.runtime.instructions.cpfile.MatrixIndexingCPFileInstruction; @@ -226,7 +227,10 @@ public static CPInstruction parseSingleInstruction ( InstructionType cptype, Str case EINSUM: return EinsumCPInstruction.parseInstruction(str); - + + case DPBuiltin: + return DPBuiltinCPInstruction.parseInstruction(str); + default: throw new DMLRuntimeException("Invalid CP Instruction Type: " + cptype ); } diff --git a/src/main/java/org/apache/sysds/runtime/instructions/cp/CPInstruction.java b/src/main/java/org/apache/sysds/runtime/instructions/cp/CPInstruction.java index b8d84ca3898..668d2f36978 100644 --- a/src/main/java/org/apache/sysds/runtime/instructions/cp/CPInstruction.java +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/CPInstruction.java @@ -48,7 +48,8 @@ public enum CPType { EvictLineageCache, EINSUM, NoOp, Union, - QuantizeCompression + QuantizeCompression, + DPBuiltin } protected final CPType _cptype; diff --git a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java index f75ccb13cdb..746c8471ff8 100755 --- a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java @@ -102,10 +102,7 @@ private DPBuiltinCPInstruction( String opcode, String istr, LinkedHashMap params) { - // input1 = the aggregate matrix; input2/3 unused at this level - // (scalars come from _params, not CPOperand fields, so that they - // can be either literals or DML variable names transparently). - super(null, input, null, null, output, opcode, istr); + super(CPType.DPBuiltin, null, input, null, output, opcode, istr); _params = params; } diff --git a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java index e9bc2c6ea9e..6363b925b99 100755 --- a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java +++ b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java @@ -142,14 +142,18 @@ public void testRemainingBudgetDecreasesMonotonically() { @Test public void testSmallerSensitivityCheaper() { - // A release with sensitivity 0.1 should consume less budget than - // one with sensitivity 1.0 at the same (ε, δ). + // For the Gaussian mechanism, noise scales proportionally with sensitivity + // (sigma = sensitivity * C), so sensitivity² cancels in the RDP formula + // alpha * sensitivity² / (2 * sigma²). Budget consumed is epsilon-determined. + // For Laplace, higher sensitivity means a larger noise scale b = sensitivity/epsilon, + // which yields LOWER RDP divergence (more noise → better privacy). + // Test the Laplace case: higher sensitivity at same epsilon costs less budget. RDPAccountant acc1 = new RDPAccountant(100.0, 1e-5); RDPAccountant acc2 = new RDPAccountant(100.0, 1e-5); - acc1.compose(0.5, 1e-5, 0.1); // low sensitivity - acc2.compose(0.5, 1e-5, 1.0); // high sensitivity + acc1.compose(0.5, 0.0, 1.0); // high sensitivity, Laplace + acc2.compose(0.5, 0.0, 0.1); // low sensitivity, Laplace - assertTrue("Lower sensitivity must cost less budget", + assertTrue("Higher sensitivity costs less budget (Laplace: more noise for same epsilon)", acc1.totalEpsilonSpent() < acc2.totalEpsilonSpent()); } From 7dc493c87b4e1c8a7c6505969e44f1b83651ad10 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Thu, 2 Jul 2026 14:34:36 +0200 Subject: [PATCH 05/20] CLAUDE_CODE_PLAN.md --- CLAUDE_CODE_PLAN.md | 364 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100755 CLAUDE_CODE_PLAN.md diff --git a/CLAUDE_CODE_PLAN.md b/CLAUDE_CODE_PLAN.md new file mode 100755 index 00000000000..c08b79e80e4 --- /dev/null +++ b/CLAUDE_CODE_PLAN.md @@ -0,0 +1,364 @@ +# Claude Code Plan: Differential-Privacy Built-ins for Apache SystemDS + +## Goal +Add `dp_laplace` and `dp_gaussian` as native (non-script) DML built-in +functions backed by a session-scoped Rényi-DP budget accountant. + +This plan is written for Claude Code. Follow the steps in order. Read each +file before editing it. Do not guess at class names, field names, or method +signatures — grep to verify every assumption before writing code. + +--- + +## Step 1 — Orient: understand the instruction routing mechanism + +```bash +# 1a. Find how opcodes are mapped to CPInstruction subclasses. +# We are looking for whatever replaces (or still is) the opcode→type map. +grep -rn "parseSingleInstruction\|CPType\|CPInstructionParser" \ + src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java \ + | head -60 +``` + +```bash +# 1b. Look for how a recently-added native instruction (e.g. lstm, compress) +# is wired in. This gives us the exact pattern to copy. +grep -n "lstm\|compress\|bias_add" \ + src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java \ + | head -30 +``` + +```bash +# 1c. Read the full parseSingleInstruction method to understand the switch/map +# dispatch that leads to parseInstruction() on a specific class. +grep -n "parseSingleInstruction\|case Dnn\|case Builtin\|DnnCPInstruction\ +\|ParameterizedBuiltin\|parseInstruction" \ + src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java \ + | head -60 +``` + +> **Note for Claude Code**: the exact field/method name may differ from +> `String2CPInstructionType`. Use the output of step 1b to find the real +> registration pattern. Copy it exactly — do not invent names. + +--- + +## Step 2 — Orient: understand the Builtins enum constructor + +```bash +# 2a. Read the constructor and the first ~60 enum entries to confirm the +# exact parameter signature (name, script) vs (name, script, ReturnType). +sed -n '35,100p' \ + src/main/java/org/apache/sysds/common/Builtins.java +``` + +```bash +# 2b. Confirm no "parameterized" field exists in the constructor. +grep -n "parameterized\|Parameterized\|boolean" \ + src/main/java/org/apache/sysds/common/Builtins.java | head -20 +``` + +> Expected: constructor is `(String name, boolean script)` with `script=false` +> for native built-ins. Verify before proceeding. + +--- + +## Step 3 — Orient: understand BuiltinFunctionExpression validation + +```bash +# 3a. Find how an existing similar native built-in (e.g. colMeans, abs) +# is validated in the parser and how it maps to a HOP. +grep -n "COLMEAN\|ABS\|BuiltinFunctionExpression\|case COLMEAN\|case ABS" \ + src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java \ + | head -30 +``` + +```bash +# 3b. Find DMLTranslator to understand how BuiltinFunctionExpression +# creates a HOP node. +grep -n "createBuiltin\|BuiltinOp\|UnaryOp\|colMeans\|case COLMEAN" \ + src/main/java/org/apache/sysds/parser/DMLTranslator.java \ + | head -20 +``` + +```bash +# 3c. Find how the HOP emits a LOP that becomes a CPInstruction opcode string. +grep -n "getOpCode\|getLops\|addLop\|Lops" \ + src/main/java/org/apache/sysds/hops/UnaryOp.java \ + | head -20 +``` + +> This tells us whether we need a new HOP type, a new LOP type, or whether +> dp_laplace/dp_gaussian can reuse an existing HOP+LOP path. + +--- + +## Step 4 — Create the new files + +Create the following files. All paths are relative to the repository root. + +### 4a. RDPAccountant + +**File**: `src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java` + +Verify the package declaration matches the target path: +```bash +head -5 src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java +``` + +### 4b. DPBuiltinCPInstruction + +**File**: `src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java` + +Verify the imports compile against the actual codebase: + +```bash +# Confirm BinaryOperator and Plus exist at the expected paths. +find src/main/java -name "BinaryOperator.java" -o -name "Plus.java" | head -5 + +# Confirm MatrixBlock.binaryOperations signature. +grep -n "binaryOperations" \ + src/main/java/org/apache/sysds/runtime/matrix/data/MatrixBlock.java \ + | head -10 +``` + +If `MatrixBlock.binaryOperations` has a different signature, update the call +in `processInstruction` to match. + +--- + +## Step 5 — Patch ExecutionContext to carry the accountant + +```bash +# 5a. Read the end of ExecutionContext to find where to add the new field. +grep -n "class ExecutionContext\|private.*Map\|private.*List\|getMatrix\ +\|releaseMatrix\|setScalar" \ + src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java \ + | tail -40 +``` + +Add the following to `ExecutionContext.java`: + +```java +// --- DP budget accountant (lazy-initialised, one per script execution) --- +private RDPAccountant _rdpAccountant = null; + +public RDPAccountant getRDPAccountant() { + if (_rdpAccountant == null) + // Default budget: ε=1.0, δ=1e-5. Future work: set via DML built-in. + _rdpAccountant = new RDPAccountant(1.0, 1e-5); + return _rdpAccountant; +} +``` + +Add the import at the top of `ExecutionContext.java`: +```java +import org.apache.sysds.runtime.privacy.dp.RDPAccountant; +``` + +--- + +## Step 6 — Register in Builtins.java + +```bash +# 6a. Find a good alphabetical insertion point between "D" entries. +grep -n "^[[:space:]]*D[A-Z_]*(" \ + src/main/java/org/apache/sysds/common/Builtins.java | head -20 +``` + +Insert after the last `D`-prefixed entry (or before the first `E` entry): + +```java +DP_LAPLACE("dp_laplace", false), +DP_GAUSSIAN("dp_gaussian", false), +``` + +Confirm `script=false` is correct by checking a nearby native built-in: +```bash +grep -A1 "DIAG\|DECOMPRESS\|DET" \ + src/main/java/org/apache/sysds/common/Builtins.java | head -10 +``` + +--- + +## Step 7 — Wire into the parser + +Use the routing pattern discovered in Step 1. The pattern will be one of: + +**Pattern A — opcode map + switch** (most likely based on commit history): +```bash +# Find the CPType enum to add a new entry if needed. +grep -n "enum CPType\|Dnn,\|BuiltinNary,\|ParameterizedBuiltin," \ + src/main/java/org/apache/sysds/runtime/instructions/cp/CPInstruction.java \ + | head -20 +``` + +If a new `CPType.DPBuiltin` is needed, add it to the `CPType` enum, then add +the map entry and switch case following the exact pattern of `CPType.Dnn`. + +**Pattern B — direct opcode string match in parseSingleInstruction**: +Add a branch: +```java +else if (opcode.equals("dp_laplace") || opcode.equals("dp_gaussian")) + return DPBuiltinCPInstruction.parseInstruction(str); +``` + +> Use whichever pattern the codebase actually uses. Do not mix patterns. + +--- + +## Step 8 — Wire into BuiltinFunctionExpression + +```bash +# 8a. Find the validate() switch to add parameter checking. +grep -n "case DIAG\|case ABS\|case COLMEAN\|checkNumParameters\|checkMatrixParam" \ + src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java \ + | head -20 +``` + +Add cases for `DP_LAPLACE` and `DP_GAUSSIAN` in the `validate()` switch: + +```java +case DP_LAPLACE: { + // dp_laplace(aggregate, sensitivity, epsilon) + checkNumParameters(3); + checkMatrixParam(getFirstExpr()); // aggregate matrix + checkScalarParam(getSecondExpr()); // sensitivity + checkScalarParam(getThirdExpr()); // epsilon + output.setDataType(DataType.MATRIX); + output.setValueType(ValueType.FP64); + // Output shape matches input shape; dimensions copied from input. + output.setDimensions( + getFirstExpr().getOutput().getDim1(), + getFirstExpr().getOutput().getDim2()); + break; +} +case DP_GAUSSIAN: { + // dp_gaussian(aggregate, sensitivity, epsilon, delta) + checkNumParameters(4); + checkMatrixParam(getFirstExpr()); + checkScalarParam(getSecondExpr()); // sensitivity + checkScalarParam(getThirdExpr()); // epsilon + checkScalarParam(getFourthExpr()); // delta + output.setDataType(DataType.MATRIX); + output.setValueType(ValueType.FP64); + output.setDimensions( + getFirstExpr().getOutput().getDim1(), + getFirstExpr().getOutput().getDim2()); + break; +} +``` + +> Verify that `checkScalarParam` and `getFourthExpr` exist: +> ```bash +> grep -n "checkScalarParam\|getFourthExpr\|getThirdExpr" \ +> src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java \ +> | head -10 +> ``` + +--- + +## Step 9 — Wire into DMLTranslator + +```bash +# 9a. Find how colMeans (COLMEAN) translates to a HOP in DMLTranslator. +grep -n "COLMEAN\|case COLMEAN\|createBuiltinOp\|UnaryOp" \ + src/main/java/org/apache/sysds/parser/DMLTranslator.java \ + | head -20 +``` + +```bash +# 9b. Find where to add new cases — likely inside a large switch on Builtins. +grep -n "case ABS\|case DIAG\|case CEIL" \ + src/main/java/org/apache/sysds/parser/DMLTranslator.java | head -10 +``` + +The simplest approach: map `DP_LAPLACE` and `DP_GAUSSIAN` to a `UnaryOp` HOP +with a custom opcode string. The LOP produced by `UnaryOp` will carry the +opcode string `"dp_laplace"` or `"dp_gaussian"`, which the `CPInstructionParser` +will then route to `DPBuiltinCPInstruction.parseInstruction`. + +Add cases following the existing unary pattern: +```java +case DP_LAPLACE: +case DP_GAUSSIAN: + // Reuse UnaryOp HOP — the opcode string routes to DPBuiltinCPInstruction. + currBuiltinOp = new UnaryOp(target.getName(), DataType.MATRIX, + ValueType.FP64, OpOp1.valueOf(bi.name()), expr); + break; +``` + +> Verify `OpOp1` has a compatible entry or whether a different HOP class is +> needed. If `OpOp1` does not work, fall back to creating a `FunctionOp`. + +--- + +## Step 10 — Build and smoke test + +```bash +# Build only the affected modules to get fast feedback. +mvn compile -pl src/main/java -am -q 2>&1 | tail -30 +``` + +Fix any compilation errors before proceeding. + +```bash +# Run the self-contained unit tests (no SystemDS runtime needed). +mvn test -pl src/test/java \ + -Dtest=DPBuiltinCPInstructionTest \ + -Dsurefire.failIfNoSpecifiedTests=false \ + 2>&1 | tail -40 +``` + +```bash +# Smoke-test end-to-end with a minimal DML script. +cat > /tmp/dp_smoke.dml << 'EOF' +X = rand(rows=100, cols=10, min=0, max=1); +mu = colMeans(X); +noisy = dp_laplace(mu, sensitivity=0.1, epsilon=1.0); +print(toString(noisy)); +EOF + +./bin/systemds /tmp/dp_smoke.dml 2>&1 | tail -20 +``` + +If the smoke test fails with an opcode-not-found error, re-check Steps 7 and 9. +If it fails with a budget error, reduce the sensitivity or widen epsilon. + +--- + +## Step 11 — Run the federated benchmark + +```bash +cat > /tmp/dp_fedavg_benchmark.dml << 'EOF' +# Federated averaging with DP release of column means. +# Sweep epsilon across {0.5, 1, 4, 8} by passing $epsilon as an arg. +X = read($1); +mu = colMeans(X); +noisy = dp_gaussian(mu, sensitivity=$sensitivity, epsilon=$epsilon, delta=1e-5); +write(noisy, $2, format="csv"); +EOF + +for eps in 0.5 1 4 8; do + echo "--- epsilon=$eps ---" + ./bin/systemds /tmp/dp_fedavg_benchmark.dml \ + -args data/adult.csv /tmp/result_eps${eps}.csv \ + -nvargs sensitivity=0.01 epsilon=$eps \ + 2>&1 | tail -5 +done +``` + +--- + +## Files modified (summary) + +| File | Change | +|---|---| +| `src/main/java/org/apache/sysds/common/Builtins.java` | Add `DP_LAPLACE`, `DP_GAUSSIAN` entries | +| `src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java` | Add validate cases | +| `src/main/java/org/apache/sysds/parser/DMLTranslator.java` | Add HOP-creation cases | +| `src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java` | Register opcodes using actual routing pattern | +| `src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java` | **New file** | +| `src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java` | **New file** | +| `src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java` | Add `getRDPAccountant()` | +| `src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinCPInstructionTest.java` | **New file** (unit tests) | From bd7a9563c8b82304ccf69be2bdb0f25db7c7c9c4 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Thu, 2 Jul 2026 14:39:38 +0200 Subject: [PATCH 06/20] Fix test package name. --- src/main/java/org/apache/sysds/common/Types.java | 3 ++- .../sysds/test/component/cp/DPBuiltinCPInstructionTest.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/sysds/common/Types.java b/src/main/java/org/apache/sysds/common/Types.java index 250fc45a5c7..611d5011fbb 100644 --- a/src/main/java/org/apache/sysds/common/Types.java +++ b/src/main/java/org/apache/sysds/common/Types.java @@ -806,7 +806,8 @@ public static ReOrgOp valueOfByOpcode(String opcode) { /** Parameterized operations that require named variable arguments */ public enum ParamBuiltinOp { - AUTODIFF, CDF, CONTAINS, DP_LAPLACE, DP_GAUSSIAN, INVALID, INVCDF, GROUPEDAGG, RMEMPTY, REPLACE, REXPAND, + AUTODIFF, CDF, CONTAINS, DP_LAPLACE, DP_GAUSSIAN, INVALID, INVCDF, + GROUPEDAGG, RMEMPTY, REPLACE, REXPAND, LOWER_TRI, UPPER_TRI, TRANSFORMAPPLY, TRANSFORMDECODE, TRANSFORMCOLMAP, TRANSFORMMETA, TOKENIZE, TOSTRING, LIST, PARAMSERV diff --git a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java index 6363b925b99..68b40d4d170 100755 --- a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java +++ b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.sysds.test.functions.privacy.dp; +package org.apache.sysds.test.component.cp; import org.apache.sysds.runtime.DMLRuntimeException; import org.apache.sysds.runtime.privacy.dp.RDPAccountant; From 5d31fdaa855e3aac46c0d1aad6b619a81f39f1df Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Mon, 6 Jul 2026 11:12:59 +0200 Subject: [PATCH 07/20] Fix Laplace accountant --- .../runtime/privacy/dp/RDPAccountant.java | 88 ++++++++++--------- .../cp/DPBuiltinCPInstructionTest.java | 18 ++-- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java b/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java index 884012e4ee6..98d87e3a9e4 100755 --- a/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java +++ b/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java @@ -94,9 +94,19 @@ public class RDPAccountant { // State // ----------------------------------------------------------------------- - /** Running accumulated Rényi divergence at each order. */ + /** Running accumulated Rényi divergence at each order (Gaussian releases only). */ private final double[] _rdpSum = new double[ORDERS.length]; + /** + * Running sum of pure ε from Laplace releases. + * + * Laplace gives pure ε-DP (no δ). Basic composition is exact and tighter + * than the RDP-to-(ε,δ) conversion path for Laplace, which introduces an + * unneeded δ and produces a looser bound. We accumulate Laplace cost here + * and add it directly in {@link #totalEpsilonSpent()}. + */ + private double _pureEpsilonSum = 0.0; + /** User-specified total privacy budget (ε). */ private final double _epsilonBudget; @@ -164,19 +174,20 @@ public RDPAccountant(double epsilonBudget) { public void compose(double epsilon, double delta, double sensitivity) { _releaseCount++; - // Accumulate Rényi divergence at each order. - for (int i = 0; i < ORDERS.length; i++) { - double alpha = ORDERS[i]; - double rdp; - if (delta == 0.0) { - rdp = rdpLaplace(alpha, sensitivity, epsilon); - } else { - // Back-derive σ from the (ε, δ) calibration formula for the - // Gaussian mechanism, then compute the RDP contribution. + if (delta == 0.0) { + // Laplace mechanism: pure ε-DP. Basic composition is exact and + // tighter than converting through RDP (which would introduce an + // unnecessary δ and often produce a looser ε bound). + _pureEpsilonSum += epsilon; + } else { + // Gaussian mechanism: accumulate Rényi divergence at each order. + // Back-derive σ from the (ε, δ) calibration formula, then add the + // RDP contribution. + for (int i = 0; i < ORDERS.length; i++) { + double alpha = ORDERS[i]; double sigma = gaussianSigma(sensitivity, epsilon, delta); - rdp = rdpGaussian(alpha, sensitivity, sigma); + _rdpSum[i] += rdpGaussian(alpha, sensitivity, sigma); } - _rdpSum[i] += rdp; } // Convert accumulated RDP to (ε, δ) and check. @@ -195,23 +206,36 @@ public void compose(double epsilon, double delta, double sensitivity) { // ----------------------------------------------------------------------- /** - * Returns the current total privacy cost as an ε value under the - * accountant's δ, using the tightest available Rényi order. + * Returns the current total privacy cost as an ε value. + * + *

The total cost combines two independent composition paths: + *

    + *
  • Laplace releases: pure ε-DP, accumulated via basic composition + * (exact and tighter than the RDP conversion path for Laplace).
  • + *
  • Gaussian releases: accumulated via Rényi DP, then converted to + * (ε, δ) using the tightest available order α.
  • + *
+ * + *

The combined guarantee is (ε_total, δ)-DP where ε_total is the sum + * of the two contributions (basic composition of a pure-DP mechanism with + * an approximate-DP mechanism is additive in ε). */ public double totalEpsilonSpent() { - double minEpsilon = Double.MAX_VALUE; + // Gaussian contribution via RDP → (ε, δ) conversion (Mironov 2017, Prop. 3). + double gaussianEps = Double.MAX_VALUE; for (int i = 0; i < ORDERS.length; i++) { double alpha = ORDERS[i]; - // Standard RDP-to-(ε,δ) conversion: - // ε(α) = R[α] + log(1 - 1/α) - log(δ·(α-1)) / α - // Reference: Mironov 2017, Proposition 3. double eps = _rdpSum[i] + Math.log(1.0 - 1.0 / alpha) - Math.log(_delta * (alpha - 1.0)) / alpha; - if (eps < minEpsilon) - minEpsilon = eps; + if (eps < gaussianEps) + gaussianEps = eps; } - return minEpsilon; + // If no Gaussian releases have occurred, the RDP conversion yields a + // large positive value (log-delta term dominates). Clamp to zero so + // it doesn't inflate the total when only Laplace releases are present. + if (gaussianEps < 0) gaussianEps = 0.0; + return _pureEpsilonSum + gaussianEps; } /** Returns the remaining ε budget (may be negative if budget is exceeded). */ @@ -228,28 +252,6 @@ public int releaseCount() { // Mechanism-specific RDP contributions // ----------------------------------------------------------------------- - /** - * Rényi divergence of order α for the Laplace mechanism with scale - * b = sensitivity / epsilon. - * - *

For α > 1 and integer α, the closed form is: - *

-     *   D_α = (1/(α-1)) · ln( α/(2α-1)·exp((α-1)/b) + (α-1)/(2α-1)·exp(-α/b) )
-     * 
- * - *

For non-integer α we use the same formula, which is the natural - * analytic continuation (see Mironov 2017, Proposition 3, example 1). - * We clamp the log argument to avoid NaN when inputs are degenerate. - */ - private static double rdpLaplace(double alpha, double sensitivity, double epsilon) { - double b = sensitivity / epsilon; // Laplace scale - double t1 = alpha / (2.0 * alpha - 1.0) * Math.exp((alpha - 1.0) / b); - double t2 = (alpha - 1.0) / (2.0 * alpha - 1.0) * Math.exp(-alpha / b); - double arg = t1 + t2; - if (arg <= 0) return 0.0; // degenerate: treat as zero cost - return Math.log(arg) / (alpha - 1.0); - } - /** * Rényi divergence of order α for the Gaussian mechanism with noise * scale σ and L2 sensitivity Δf. diff --git a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java index 68b40d4d170..fdeac21ff1b 100755 --- a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java +++ b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java @@ -141,19 +141,17 @@ public void testRemainingBudgetDecreasesMonotonically() { } @Test - public void testSmallerSensitivityCheaper() { - // For the Gaussian mechanism, noise scales proportionally with sensitivity - // (sigma = sensitivity * C), so sensitivity² cancels in the RDP formula - // alpha * sensitivity² / (2 * sigma²). Budget consumed is epsilon-determined. - // For Laplace, higher sensitivity means a larger noise scale b = sensitivity/epsilon, - // which yields LOWER RDP divergence (more noise → better privacy). - // Test the Laplace case: higher sensitivity at same epsilon costs less budget. + public void testHigherEpsilonCostMoreForLaplace() { + // For Laplace, the accountant uses basic (pure ε-DP) composition: cost = epsilon. + // Sensitivity determines noise scale but NOT the budget consumed — that is set + // entirely by the caller's epsilon parameter. + // A release at epsilon=1.0 costs more budget than one at epsilon=0.5. RDPAccountant acc1 = new RDPAccountant(100.0, 1e-5); RDPAccountant acc2 = new RDPAccountant(100.0, 1e-5); - acc1.compose(0.5, 0.0, 1.0); // high sensitivity, Laplace - acc2.compose(0.5, 0.0, 0.1); // low sensitivity, Laplace + acc1.compose(0.5, 0.0, 1.0); // epsilon=0.5, Laplace + acc2.compose(1.0, 0.0, 1.0); // epsilon=1.0, same sensitivity - assertTrue("Higher sensitivity costs less budget (Laplace: more noise for same epsilon)", + assertTrue("Higher epsilon costs more budget (Laplace basic composition)", acc1.totalEpsilonSpent() < acc2.totalEpsilonSpent()); } From 4b58bc402b2f49fab1444140bcc4f58d6f14e9b6 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Mon, 6 Jul 2026 17:22:33 +0200 Subject: [PATCH 08/20] Add unit tests --- .../cp/DPBuiltinCPInstructionTest.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java index fdeac21ff1b..c04a901e650 100755 --- a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java +++ b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java @@ -155,6 +155,122 @@ public void testHigherEpsilonCostMoreForLaplace() { acc1.totalEpsilonSpent() < acc2.totalEpsilonSpent()); } + // --- Item 1: constructor error paths ------------------------------------ + + @Test(expected = DMLRuntimeException.class) + public void testConstructorRejectsZeroEpsilonBudget() { + new RDPAccountant(0.0, 1e-5); + } + + @Test(expected = DMLRuntimeException.class) + public void testConstructorRejectsNegativeEpsilonBudget() { + new RDPAccountant(-0.5, 1e-5); + } + + @Test(expected = DMLRuntimeException.class) + public void testConstructorRejectsDeltaZero() { + new RDPAccountant(1.0, 0.0); + } + + @Test(expected = DMLRuntimeException.class) + public void testConstructorRejectsDeltaOne() { + new RDPAccountant(1.0, 1.0); + } + + // --- Item 2: single-argument convenience constructor ------------------- + + @Test + public void testConvenienceConstructorDefaultsDeltaTo1e5() { + // The one-arg form delegates to (epsilonBudget, 1e-5). A Gaussian + // release whose per-release delta matches that default must produce + // identical totalEpsilonSpent() from both construction paths. + RDPAccountant oneArg = new RDPAccountant(10.0); + RDPAccountant twoArg = new RDPAccountant(10.0, 1e-5); + oneArg.compose(0.5, 1e-5, 1.0); + twoArg.compose(0.5, 1e-5, 1.0); + assertEquals("Convenience constructor must default to delta=1e-5", + twoArg.totalEpsilonSpent(), oneArg.totalEpsilonSpent(), EPS); + } + + // --- Item 3: budget exhaustion via Gaussian releases ------------------- + + @Test(expected = DMLRuntimeException.class) + public void testGaussianBudgetExhaustionThrows() { + // Budget = 0.1. Each Gaussian release costs more than 0.005, so 20 + // releases must exceed the budget well before the loop ends. + RDPAccountant acc = new RDPAccountant(0.1, 1e-5); + for (int i = 0; i < 20; i++) { + acc.compose(0.3, 1e-5, 1.0); + } + } + + // --- Item 4: mixed Laplace + Gaussian composition ---------------------- + + @Test + public void testMixedCompositionExceedsEitherAlone() { + // Compose one Laplace and one Gaussian release. The total cost must + // exceed what either mechanism contributes alone, exercising the + // _pureEpsilonSum + gaussianEps addition path in totalEpsilonSpent(). + RDPAccountant mixed = new RDPAccountant(100.0, 1e-5); + RDPAccountant lapOnly = new RDPAccountant(100.0, 1e-5); + RDPAccountant gauOnly = new RDPAccountant(100.0, 1e-5); + + mixed.compose(0.5, 0.0, 1.0); // Laplace + mixed.compose(0.5, 1e-5, 1.0); // Gaussian + + lapOnly.compose(0.5, 0.0, 1.0); + gauOnly.compose(0.5, 1e-5, 1.0); + + assertTrue("Mixed cost must exceed Laplace-only cost", + mixed.totalEpsilonSpent() > lapOnly.totalEpsilonSpent()); + assertTrue("Mixed cost must exceed Gaussian-only cost", + mixed.totalEpsilonSpent() > gauOnly.totalEpsilonSpent()); + } + + // --- Item 6: release count across multiple mixed releases -------------- + + @Test + public void testReleaseCountTracksAllReleases() { + RDPAccountant acc = new RDPAccountant(100.0, 1e-5); + assertEquals(0, acc.releaseCount()); + acc.compose(0.1, 0.0, 1.0); // Laplace + assertEquals(1, acc.releaseCount()); + acc.compose(0.1, 1e-5, 1.0); // Gaussian + assertEquals(2, acc.releaseCount()); + acc.compose(0.1, 0.0, 1.0); // Laplace + acc.compose(0.1, 0.0, 1.0); // Laplace + acc.compose(0.1, 1e-5, 1.0); // Gaussian + assertEquals(5, acc.releaseCount()); + } + + // --- Item 8: edge-case inputs for rdpGaussian / gaussianSigma ---------- + + @Test + public void testGaussianSensitivityCancelsInRDP() { + // For the Gaussian mechanism: σ = Δf·sqrt(2·ln(1.25/δ))/ε, so + // D_α = α·Δf²/(2σ²) = α·ε²/(4·ln(1.25/δ)). + // Sensitivity cancels. Two accountants with the same (ε,δ) but + // different sensitivity must report identical totalEpsilonSpent(). + RDPAccountant acc1 = new RDPAccountant(100.0, 1e-5); + RDPAccountant acc2 = new RDPAccountant(100.0, 1e-5); + acc1.compose(0.5, 1e-5, 1.0); // sensitivity = 1 + acc2.compose(0.5, 1e-5, 100.0); // sensitivity = 100, same (ε,δ) + assertEquals("Gaussian RDP cost must be independent of sensitivity when (ε,δ) are fixed", + acc1.totalEpsilonSpent(), acc2.totalEpsilonSpent(), EPS); + } + + @Test + public void testGaussianLargerEpsilonCostsMoreBudget() { + // D_α ∝ ε², so a release declared at a higher ε (less noise, more + // privacy loss) must cost more budget than one at a lower ε. + RDPAccountant lowEps = new RDPAccountant(100.0, 1e-5); + RDPAccountant highEps = new RDPAccountant(100.0, 1e-5); + lowEps.compose(0.1, 1e-5, 1.0); + highEps.compose(0.5, 1e-5, 1.0); + assertTrue("Larger epsilon per Gaussian release must cost more budget", + highEps.totalEpsilonSpent() > lowEps.totalEpsilonSpent()); + } + // ======================================================================= // 2. Noise distribution tests (statistical sanity checks) // ======================================================================= From 29369700fddc63b2c30d1ef129e3c01e7643b80a Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Mon, 6 Jul 2026 17:27:44 +0200 Subject: [PATCH 09/20] Rename RDPAccountant to DPBudgetAccountant --- .../context/ExecutionContext.java | 12 +- .../cp/DPBuiltinCPInstruction.java | 14 +- .../privacy/dp/DPBudgetAccountant.java | 271 ++++++++++++++++++ .../runtime/privacy/dp/RDPAccountant.java | 264 +---------------- .../cp/DPBuiltinCPInstructionTest.java | 58 ++-- 5 files changed, 320 insertions(+), 299 deletions(-) create mode 100644 src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java diff --git a/src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java b/src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java index baa6e74df7d..eaf50da88c7 100644 --- a/src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java +++ b/src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java @@ -62,7 +62,7 @@ import org.apache.sysds.runtime.meta.MetaData; import org.apache.sysds.runtime.meta.MetaDataFormat; import org.apache.sysds.runtime.util.HDFSTool; -import org.apache.sysds.runtime.privacy.dp.RDPAccountant; +import org.apache.sysds.runtime.privacy.dp.DPBudgetAccountant; import org.apache.sysds.utils.Statistics; import java.util.ArrayList; @@ -91,7 +91,7 @@ public class ExecutionContext { protected SEALClient _seal_client; - private RDPAccountant _rdpAccountant = null; + private DPBudgetAccountant _dpBudgetAccountant = null; //parfor temporary functions (created by eval) protected Set _fnNames; @@ -147,10 +147,10 @@ public void setLineage(Lineage lineage) { _lineage = lineage; } - public RDPAccountant getRDPAccountant() { - if (_rdpAccountant == null) - _rdpAccountant = new RDPAccountant(1.0, 1e-5); - return _rdpAccountant; + public DPBudgetAccountant getDPBudgetAccountant() { + if (_dpBudgetAccountant == null) + _dpBudgetAccountant = new DPBudgetAccountant(1.0, 1e-5); + return _dpBudgetAccountant; } public boolean isAutoCreateVars() { diff --git a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java index 746c8471ff8..be02be1cae7 100755 --- a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java @@ -25,7 +25,7 @@ import org.apache.sysds.runtime.instructions.InstructionUtils; import org.apache.sysds.runtime.matrix.data.MatrixBlock; import org.apache.sysds.runtime.matrix.operators.BinaryOperator; -import org.apache.sysds.runtime.privacy.dp.RDPAccountant; +import org.apache.sysds.runtime.privacy.dp.DPBudgetAccountant; import java.util.LinkedHashMap; import java.util.concurrent.ThreadLocalRandom; @@ -42,7 +42,7 @@ * *

The instruction receives a materialised matrix (the aggregate result), * injects calibrated noise element-wise, records the release with the - * session-scoped {@link RDPAccountant}, and returns the noisy matrix. + * session-scoped {@link DPBudgetAccountant}, and returns the noisy matrix. * *

Noise is generated in Java and added via a {@code MatrixBlock} binary * operation so that the output allocation path is identical to every other @@ -64,8 +64,8 @@ * add opcode-to-type mappings and a parse branch that returns a * {@code DPBuiltinCPInstruction} *

  • {@code org.apache.sysds.runtime.controlprogram.context.ExecutionContext} - * – add {@code getRDPAccountant()} returning a session-scoped - * {@link RDPAccountant} (lazy-initialised field)
  • + * – add {@code getDPBudgetAccountant()} returning a session-scoped + * {@link DPBudgetAccountant} (lazy-initialised field) * */ public class DPBuiltinCPInstruction extends ComputationCPInstruction { @@ -169,7 +169,7 @@ public static DPBuiltinCPInstruction parseInstruction(String str) { *
  • Generate a noise {@link MatrixBlock} of the same shape.
  • *
  • Add noise element-wise using the existing binary-operator path.
  • *
  • Record the release with the session-scoped - * {@link RDPAccountant}; throw if budget is exhausted.
  • + * {@link DPBudgetAccountant}; throw if budget is exhausted. *
  • Write the noisy block back to the variable table and release * the input pin.
  • * @@ -200,9 +200,9 @@ public void processInstruction(ExecutionContext ec) { inBlock.binaryOperations(plusOp, noiseBlock, outBlock); // ── 5. Record release and enforce budget ──────────────────────────── - // getRDPAccountant() returns a lazy-initialised RDPAccountant that is + // getDPBudgetAccountant() returns a lazy-initialised DPBudgetAccountant that is // owned by this ExecutionContext (added in a companion EC patch). - RDPAccountant accountant = ec.getRDPAccountant(); + DPBudgetAccountant accountant = ec.getDPBudgetAccountant(); accountant.compose(epsilon, delta, sensitivity); // throws on exhaustion // ── 6. Write output and release input pin ─────────────────────────── diff --git a/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java b/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java new file mode 100644 index 00000000000..ce5afcb7850 --- /dev/null +++ b/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sysds.runtime.privacy.dp; + +import org.apache.sysds.runtime.DMLRuntimeException; + +/** + * Session-scoped differential privacy budget accountant. + * + *

    Purpose

    + * Tracks composition of DP releases across the lifetime of a DML script + * execution. Each call to {@link #compose} records one release and checks + * whether the cumulative privacy cost has exceeded the user-specified budget. + * + *

    Composition strategy

    + * The mechanism type (Laplace vs Gaussian) is inferred from the {@code delta} + * argument passed to {@link #compose}: + * + *
      + *
    • Laplace (delta == 0): pure ε-DP. The budget cost is tracked via + * basic composition — each release contributes exactly its ε to a running + * sum. This is the tightest possible bound for pure DP and avoids the + * looser estimate that results from routing Laplace through the RDP + * conversion path (which would introduce an unnecessary δ).
    • + *
    • Gaussian (delta > 0): (ε, δ)-DP via Rényi DP composition. + * Rényi divergences at a discrete set of orders α compose additively; + * the accumulated sum is converted to (ε, δ) at query time using the + * formula from Mironov 2017. This is substantially tighter than basic + * composition for repeated Gaussian releases, which is the common case + * in federated learning.
    • + *
    + * + *

    When both mechanisms are used in the same script the total cost is: + *

    + *   ε_total = ε_Laplace_sum + ε_Gaussian_RDP
    + * 
    + * This follows from basic composition of a pure-DP mechanism with an + * approximate-DP mechanism, which is additive in ε. + * + *

    Rényi orders tracked (Gaussian path)

    + * α ∈ {2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}. At query time the minimum + * converted ε across all orders is taken as the tightest available bound. + * + *

    Gaussian RDP divergence

    + * For the Gaussian mechanism with noise scale σ and L2 sensitivity Δf: + *
    + *   D_α = α · Δf² / (2σ²)
    + * 
    + * σ is back-derived from the caller's (ε, δ) via the standard calibration + * formula (see {@link #gaussianSigma}). Note that sensitivity cancels in the + * final expression, so the RDP cost depends only on the (ε, δ) parameters. + * + *

    RDP → (ε, δ) conversion (Mironov 2017, Proposition 3)

    + *
    + *   ε(α) = R[α] + log(1 − 1/α) − log(δ·(α−1)) / α
    + * 
    + * + *

    Lifecycle

    + * One instance is created per {@code ExecutionContext} (lazy init). It is + * garbage-collected with the context when the script finishes; no state + * leaks between script executions or between concurrent scripts. + * + *

    Thread safety

    + * Not thread-safe. A single DML script executes instructions sequentially + * on one thread, so no synchronisation is needed. + * + * @see DPBuiltinCPInstruction + */ +public class DPBudgetAccountant { + + // ----------------------------------------------------------------------- + // Rényi orders used for Gaussian composition + // ----------------------------------------------------------------------- + + /** + * Discrete set of Rényi orders α. All must be > 1. + * Finer grids give tighter bounds; this set covers the range relevant + * for typical ML workloads. + */ + private static final double[] ORDERS = { + 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 + }; + + // ----------------------------------------------------------------------- + // State + // ----------------------------------------------------------------------- + + /** Accumulated Rényi divergence at each order (Gaussian releases only). */ + private final double[] _rdpSum = new double[ORDERS.length]; + + /** + * Running sum of pure ε from Laplace releases. + * + *

    Laplace gives pure ε-DP (no δ). Basic composition is exact and + * tighter than the RDP conversion path for Laplace (which would introduce + * an unnecessary δ and produce a looser bound). Each Laplace release adds + * its ε here; the total is added directly in {@link #totalEpsilonSpent()}. + */ + private double _pureEpsilonSum = 0.0; + + /** Total privacy budget (ε) for the script execution. */ + private final double _epsilonBudget; + + /** δ used for the Gaussian RDP-to-(ε,δ) conversion. */ + private final double _delta; + + /** Number of releases recorded so far (for error messages). */ + private int _releaseCount = 0; + + // ----------------------------------------------------------------------- + // Constructors + // ----------------------------------------------------------------------- + + /** + * Creates an accountant with the given global budget. + * + *

    Typical usage: the DML script sets the budget once at the top + * (future work: a {@code dp_set_budget(epsilon, delta)} built-in), + * or the accountant is created with defaults and the budget is checked + * after each release. + * + * @param epsilonBudget total ε budget for the script execution (must be > 0) + * @param delta δ used for the Gaussian RDP-to-(ε,δ) conversion (must be in (0,1)) + */ + public DPBudgetAccountant(double epsilonBudget, double delta) { + if (!(epsilonBudget > 0)) + throw new DMLRuntimeException( + "DPBudgetAccountant: epsilonBudget must be > 0, got " + epsilonBudget); + if (!(delta > 0 && delta < 1)) + throw new DMLRuntimeException( + "DPBudgetAccountant: delta must be in (0,1), got " + delta); + _epsilonBudget = epsilonBudget; + _delta = delta; + } + + /** + * Convenience constructor using a liberal default δ = 1e-5. + * Suitable when the calling script does not specify δ explicitly. + */ + public DPBudgetAccountant(double epsilonBudget) { + this(epsilonBudget, 1e-5); + } + + // ----------------------------------------------------------------------- + // Core API + // ----------------------------------------------------------------------- + + /** + * Records one DP release and checks the budget. + * + *

    This method must be called before the result is written to + * the variable table. If the budget is exhausted it throws and the + * caller's result is discarded, preventing an unaccounted release. + * + *

    Mechanism selection (see class-level Javadoc for details): + *

      + *
    • {@code delta == 0} → Laplace, pure ε-DP basic composition
    • + *
    • {@code delta > 0} → Gaussian, Rényi DP composition
    • + *
    + * + * @param epsilon per-release ε parameter (must be > 0) + * @param delta per-release δ parameter (0 for Laplace, >0 for Gaussian) + * @param sensitivity L2 sensitivity Δf of the released quantity (must be > 0) + * @throws DMLRuntimeException if the cumulative ε after this release + * would exceed the budget + */ + public void compose(double epsilon, double delta, double sensitivity) { + _releaseCount++; + + if (delta == 0.0) { + // Laplace: pure ε-DP, basic composition — cost is exactly epsilon. + _pureEpsilonSum += epsilon; + } else { + // Gaussian: accumulate Rényi divergence at each order, then convert. + for (int i = 0; i < ORDERS.length; i++) { + double sigma = gaussianSigma(sensitivity, epsilon, delta); + _rdpSum[i] += rdpGaussian(ORDERS[i], sensitivity, sigma); + } + } + + double spentEpsilon = totalEpsilonSpent(); + if (spentEpsilon > _epsilonBudget) { + throw new DMLRuntimeException(String.format( + "Privacy budget exhausted after %d release(s): " + + "spent ε ≈ %.6f exceeds budget ε = %.6f (δ = %.2e). " + + "Reduce the number of releases or widen the budget.", + _releaseCount, spentEpsilon, _epsilonBudget, _delta)); + } + } + + // ----------------------------------------------------------------------- + // Inspection + // ----------------------------------------------------------------------- + + /** + * Returns the current total privacy cost as an ε value. + * + *

    Total = Laplace pure-ε sum + Gaussian RDP-converted ε (clamped to + * zero when no Gaussian releases have been recorded). + */ + public double totalEpsilonSpent() { + double gaussianEps = Double.MAX_VALUE; + for (int i = 0; i < ORDERS.length; i++) { + double alpha = ORDERS[i]; + double eps = _rdpSum[i] + + Math.log(1.0 - 1.0 / alpha) + - Math.log(_delta * (alpha - 1.0)) / alpha; + if (eps < gaussianEps) + gaussianEps = eps; + } + // Clamp: with no Gaussian releases the RDP sum is 0 and the log-delta + // term alone drives gaussianEps to a small positive value; clamp to 0 + // so Laplace-only scripts are not penalised by δ they never requested. + if (gaussianEps < 0) gaussianEps = 0.0; + return _pureEpsilonSum + gaussianEps; + } + + /** Returns the remaining ε budget (negative if the budget is exceeded). */ + public double remainingBudget() { + return _epsilonBudget - totalEpsilonSpent(); + } + + /** Returns the number of DP releases recorded so far. */ + public int releaseCount() { + return _releaseCount; + } + + // ----------------------------------------------------------------------- + // Private helpers + // ----------------------------------------------------------------------- + + /** + * Rényi divergence of order α for the Gaussian mechanism (Mironov 2017, + * Proposition 3, example 2): + *

    +     *   D_α = α · Δf² / (2σ²)
    +     * 
    + */ + private static double rdpGaussian(double alpha, double sensitivity, double sigma) { + return alpha * (sensitivity * sensitivity) / (2.0 * sigma * sigma); + } + + /** + * Gaussian noise scale σ calibrated to (ε, δ)-DP: + *
    +     *   σ = Δf · sqrt(2 · ln(1.25 / δ)) / ε
    +     * 
    + * Must match the formula used in {@link DPBuiltinCPInstruction} so that + * the RDP cost recorded here is consistent with the noise actually injected. + */ + private static double gaussianSigma(double sensitivity, double epsilon, double delta) { + return sensitivity * Math.sqrt(2.0 * Math.log(1.25 / delta)) / epsilon; + } +} diff --git a/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java b/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java index 98d87e3a9e4..0cd3785db7b 100755 --- a/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java +++ b/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java @@ -19,267 +19,17 @@ package org.apache.sysds.runtime.privacy.dp; -import org.apache.sysds.runtime.DMLRuntimeException; - /** - * Session-scoped Rényi Differential Privacy (RDP) budget accountant. - * - *

    Purpose

    - * Tracks composition of DP releases across the lifetime of a DML script - * execution. Each call to {@link #compose} records one release and checks - * whether the cumulative privacy cost has exceeded the user-specified budget. - * - *

    Why Rényi DP?

    - * Basic composition adds epsilons linearly, giving very loose bounds with - * many releases. Rényi DP divergences compose additively at the - * same order α. Converting the running Rényi sum to (ε, δ) via the standard - * conversion formula yields substantially tighter bounds — particularly for - * Gaussian mechanisms, which are common in federated learning. - * - *

    Orders tracked

    - * We track a discrete set of Rényi orders α ∈ {2, 4, 8, 16, 32, 64, 128, - * 256, 512, 1024}. At query time we take the minimum converted ε across all - * orders, which is the tightest available bound. - * - *

    Composition rules

    - * For the Gaussian mechanism with noise scale σ and sensitivity Δf, the - * Rényi divergence of order α between outputs on neighbouring datasets is: - *
    - *   D_α = α · Δf² / (2σ²)
    - * 
    - * where σ is back-derived from the caller's (ε, δ) parameters via the - * standard calibration formula. See {@link #rdpGaussian} for details. - * - * For the Laplace mechanism with scale b = Δf/ε, the Rényi divergence at - * order α is: - *
    - *   D_α = (1/(α-1)) · ln( α/(2α-1) · exp((α-1)/b) + (α-1)/(2α-1) · exp(-α/b) )
    - *       (for α > 1; the limit as α → 1 is 1/b, i.e. the KL divergence)
    - * 
    - * - *

    Conversion: Rényi DP → (ε, δ)-DP

    - * Given accumulated Rényi divergence R[α] at order α and a target δ: - *
    - *   ε(α) = R[α] + log(1 - 1/α) - log(δ · (α - 1)) / α
    - * 
    - * The reported total cost is min_α ε(α). - * - *

    Lifecycle

    - * One instance is created per {@code ExecutionContext} (lazy init). It is - * garbage-collected with the context when the script finishes; no state - * leaks between script executions or between concurrent scripts. - * - *

    Thread safety

    - * Not thread-safe. A single DML script executes instructions sequentially - * on one thread, so no synchronisation is needed. - * - * @see DPBuiltinCPInstruction + * @deprecated Use {@link DPBudgetAccountant} instead. */ -public class RDPAccountant { - - // ----------------------------------------------------------------------- - // Rényi orders to track - // ----------------------------------------------------------------------- - - /** - * Discrete set of Rényi orders α. All must be > 1. - * Finer grids give tighter bounds; this set is a reasonable default - * that covers the range relevant for typical ML workloads. - */ - private static final double[] ORDERS = { - 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 - }; - - // ----------------------------------------------------------------------- - // State - // ----------------------------------------------------------------------- - - /** Running accumulated Rényi divergence at each order (Gaussian releases only). */ - private final double[] _rdpSum = new double[ORDERS.length]; - - /** - * Running sum of pure ε from Laplace releases. - * - * Laplace gives pure ε-DP (no δ). Basic composition is exact and tighter - * than the RDP-to-(ε,δ) conversion path for Laplace, which introduces an - * unneeded δ and produces a looser bound. We accumulate Laplace cost here - * and add it directly in {@link #totalEpsilonSpent()}. - */ - private double _pureEpsilonSum = 0.0; - - /** User-specified total privacy budget (ε). */ - private final double _epsilonBudget; - - /** User-specified δ used for the RDP-to-(ε,δ) conversion. */ - private final double _delta; - - /** Number of releases recorded so far (for error messages). */ - private int _releaseCount = 0; - - // ----------------------------------------------------------------------- - // Constructor - // ----------------------------------------------------------------------- - - /** - * Creates an accountant with the given global budget. - * - *

    Typical usage: the DML script sets the budget once at the top - * (future work: a {@code dp_set_budget(epsilon, delta)} built-in), - * or the accountant is created with defaults and the budget is checked - * after each release. - * - * @param epsilonBudget total ε budget for the script execution (must be > 0) - * @param delta δ used for the RDP-to-(ε,δ) conversion (must be in (0,1)) - */ +@Deprecated +public class RDPAccountant extends DPBudgetAccountant { + @Deprecated public RDPAccountant(double epsilonBudget, double delta) { - if (!(epsilonBudget > 0)) - throw new DMLRuntimeException( - "RDPAccountant: epsilonBudget must be > 0, got " + epsilonBudget); - if (!(delta > 0 && delta < 1)) - throw new DMLRuntimeException( - "RDPAccountant: delta must be in (0,1), got " + delta); - _epsilonBudget = epsilonBudget; - _delta = delta; + super(epsilonBudget, delta); } - - /** - * Convenience constructor using a liberal default δ = 1e-5. - * Suitable when the calling script does not specify δ explicitly. - */ + @Deprecated public RDPAccountant(double epsilonBudget) { - this(epsilonBudget, 1e-5); - } - - // ----------------------------------------------------------------------- - // Core API - // ----------------------------------------------------------------------- - - /** - * Records one DP release and checks the budget. - * - *

    This method must be called before the result is written to - * the variable table. If the budget is exhausted, it throws and the - * caller's result is discarded, preventing an unaccounted release. - * - *

    The mechanism type (Laplace vs Gaussian) is inferred from the - * parameters: if {@code delta == 0} the release is treated as Laplace; - * otherwise it is treated as Gaussian. - * - * @param epsilon the ε parameter for this individual release (> 0) - * @param delta the δ parameter for this release (0 for Laplace) - * @param sensitivity the L2 sensitivity Δf of the released quantity (> 0) - * @throws DMLRuntimeException if the cumulative ε after this release - * would exceed the budget - */ - public void compose(double epsilon, double delta, double sensitivity) { - _releaseCount++; - - if (delta == 0.0) { - // Laplace mechanism: pure ε-DP. Basic composition is exact and - // tighter than converting through RDP (which would introduce an - // unnecessary δ and often produce a looser ε bound). - _pureEpsilonSum += epsilon; - } else { - // Gaussian mechanism: accumulate Rényi divergence at each order. - // Back-derive σ from the (ε, δ) calibration formula, then add the - // RDP contribution. - for (int i = 0; i < ORDERS.length; i++) { - double alpha = ORDERS[i]; - double sigma = gaussianSigma(sensitivity, epsilon, delta); - _rdpSum[i] += rdpGaussian(alpha, sensitivity, sigma); - } - } - - // Convert accumulated RDP to (ε, δ) and check. - double spentEpsilon = totalEpsilonSpent(); - if (spentEpsilon > _epsilonBudget) { - throw new DMLRuntimeException(String.format( - "Privacy budget exhausted after %d release(s): " - + "spent ε ≈ %.6f exceeds budget ε = %.6f (δ = %.2e). " - + "Reduce the number of releases or widen the budget.", - _releaseCount, spentEpsilon, _epsilonBudget, _delta)); - } - } - - // ----------------------------------------------------------------------- - // Inspection - // ----------------------------------------------------------------------- - - /** - * Returns the current total privacy cost as an ε value. - * - *

    The total cost combines two independent composition paths: - *

      - *
    • Laplace releases: pure ε-DP, accumulated via basic composition - * (exact and tighter than the RDP conversion path for Laplace).
    • - *
    • Gaussian releases: accumulated via Rényi DP, then converted to - * (ε, δ) using the tightest available order α.
    • - *
    - * - *

    The combined guarantee is (ε_total, δ)-DP where ε_total is the sum - * of the two contributions (basic composition of a pure-DP mechanism with - * an approximate-DP mechanism is additive in ε). - */ - public double totalEpsilonSpent() { - // Gaussian contribution via RDP → (ε, δ) conversion (Mironov 2017, Prop. 3). - double gaussianEps = Double.MAX_VALUE; - for (int i = 0; i < ORDERS.length; i++) { - double alpha = ORDERS[i]; - double eps = _rdpSum[i] - + Math.log(1.0 - 1.0 / alpha) - - Math.log(_delta * (alpha - 1.0)) / alpha; - if (eps < gaussianEps) - gaussianEps = eps; - } - // If no Gaussian releases have occurred, the RDP conversion yields a - // large positive value (log-delta term dominates). Clamp to zero so - // it doesn't inflate the total when only Laplace releases are present. - if (gaussianEps < 0) gaussianEps = 0.0; - return _pureEpsilonSum + gaussianEps; - } - - /** Returns the remaining ε budget (may be negative if budget is exceeded). */ - public double remainingBudget() { - return _epsilonBudget - totalEpsilonSpent(); - } - - /** Returns the number of DP releases recorded so far. */ - public int releaseCount() { - return _releaseCount; - } - - // ----------------------------------------------------------------------- - // Mechanism-specific RDP contributions - // ----------------------------------------------------------------------- - - /** - * Rényi divergence of order α for the Gaussian mechanism with noise - * scale σ and L2 sensitivity Δf. - * - *

    For α > 1: - *

    -     *   D_α = α · Δf² / (2σ²)
    -     * 
    - * - *

    This is the standard result for the Gaussian mechanism (see - * Mironov 2017, Proposition 3, example 2). - */ - private static double rdpGaussian(double alpha, double sensitivity, double sigma) { - return alpha * (sensitivity * sensitivity) / (2.0 * sigma * sigma); - } - - /** - * Back-derives the Gaussian noise scale σ from the (ε, δ)-DP parameters - * using the standard calibration inequality: - *

    -     *   σ = Δf · sqrt(2 · ln(1.25 / δ)) / ε
    -     * 
    - * - *

    This is the formula used by {@code DPBuiltinCPInstruction} to - * generate the actual noise, so the RDP contribution it records is - * exactly consistent with the noise injected. - */ - private static double gaussianSigma(double sensitivity, double epsilon, double delta) { - return sensitivity * Math.sqrt(2.0 * Math.log(1.25 / delta)) / epsilon; + super(epsilonBudget); } } diff --git a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java index c04a901e650..ac7d4f9bd4f 100755 --- a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java +++ b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java @@ -20,17 +20,17 @@ package org.apache.sysds.test.component.cp; import org.apache.sysds.runtime.DMLRuntimeException; -import org.apache.sysds.runtime.privacy.dp.RDPAccountant; +import org.apache.sysds.runtime.privacy.dp.DPBudgetAccountant; import org.junit.Test; import static org.junit.Assert.*; /** - * Tests for {@code DPBuiltinCPInstruction} and {@code RDPAccountant}. + * Tests for {@code DPBuiltinCPInstruction} and {@code DPBudgetAccountant}. * *

    The tests are grouped into three levels: *

      - *
    1. Unit tests on RDPAccountant — verify composition, conversion, + *
    2. Unit tests on DPBudgetAccountant — verify composition, conversion, * and budget enforcement in isolation, with no dependency on the full * SystemDS runtime.
    3. *
    4. Noise distribution tests — verify that the noise blocks @@ -49,12 +49,12 @@ public class DPBuiltinCPInstructionTest { private static final double EPS = 1e-9; // ======================================================================= - // 1. RDPAccountant unit tests + // 1. DPBudgetAccountant unit tests // ======================================================================= @Test public void testAccountantInitialisesAtZeroCost() { - RDPAccountant acc = new RDPAccountant(1.0, 1e-5); + DPBudgetAccountant acc = new DPBudgetAccountant(1.0, 1e-5); // No releases yet: total cost should be a large negative number // (conversion formula gives -∞ when rdpSum = 0 for all orders), // so remainingBudget() should exceed the budget. @@ -66,7 +66,7 @@ public void testAccountantInitialisesAtZeroCost() { @Test public void testSingleLaplaceReleaseDoesNotExceedBudget() { // epsilon=0.5, budget=1.0: one release should consume < budget. - RDPAccountant acc = new RDPAccountant(1.0, 1e-5); + DPBudgetAccountant acc = new DPBudgetAccountant(1.0, 1e-5); acc.compose(0.5, 0.0, 1.0); // Laplace, sensitivity=1 assertEquals(1, acc.releaseCount()); assertTrue("Single release within budget", @@ -75,7 +75,7 @@ public void testSingleLaplaceReleaseDoesNotExceedBudget() { @Test public void testSingleGaussianReleaseDoesNotExceedBudget() { - RDPAccountant acc = new RDPAccountant(1.0, 1e-5); + DPBudgetAccountant acc = new DPBudgetAccountant(1.0, 1e-5); acc.compose(0.5, 1e-5, 1.0); // Gaussian assertEquals(1, acc.releaseCount()); assertTrue("Single Gaussian release within budget", @@ -86,7 +86,7 @@ public void testSingleGaussianReleaseDoesNotExceedBudget() { public void testBudgetExhaustionThrows() { // Budget = 0.1, but we try to make 10 releases at epsilon=0.5 each. // After enough releases the budget must be exceeded. - RDPAccountant acc = new RDPAccountant(0.1, 1e-5); + DPBudgetAccountant acc = new DPBudgetAccountant(0.1, 1e-5); for (int i = 0; i < 10; i++) { acc.compose(0.5, 0.0, 1.0); // will throw before the 10th } @@ -94,7 +94,7 @@ public void testBudgetExhaustionThrows() { @Test public void testCompositionIsMonotonicallyIncreasing() { - RDPAccountant acc = new RDPAccountant(100.0, 1e-5); // large budget + DPBudgetAccountant acc = new DPBudgetAccountant(100.0, 1e-5); // large budget double prev = acc.totalEpsilonSpent(); for (int i = 0; i < 5; i++) { acc.compose(0.3, 1e-5, 1.0); @@ -114,8 +114,8 @@ public void testGaussianTighterThanLaplaceForSameEpsilon() { double eps = 0.5; double delta = 1e-5; - RDPAccountant gaussian = new RDPAccountant(100.0, delta); - RDPAccountant laplace = new RDPAccountant(100.0, delta); + DPBudgetAccountant gaussian = new DPBudgetAccountant(100.0, delta); + DPBudgetAccountant laplace = new DPBudgetAccountant(100.0, delta); for (int i = 0; i < 5; i++) { gaussian.compose(eps, delta, 1.0); @@ -130,7 +130,7 @@ public void testGaussianTighterThanLaplaceForSameEpsilon() { @Test public void testRemainingBudgetDecreasesMonotonically() { - RDPAccountant acc = new RDPAccountant(2.0, 1e-5); + DPBudgetAccountant acc = new DPBudgetAccountant(2.0, 1e-5); double prev = acc.remainingBudget(); for (int i = 0; i < 3; i++) { acc.compose(0.2, 1e-5, 1.0); @@ -146,8 +146,8 @@ public void testHigherEpsilonCostMoreForLaplace() { // Sensitivity determines noise scale but NOT the budget consumed — that is set // entirely by the caller's epsilon parameter. // A release at epsilon=1.0 costs more budget than one at epsilon=0.5. - RDPAccountant acc1 = new RDPAccountant(100.0, 1e-5); - RDPAccountant acc2 = new RDPAccountant(100.0, 1e-5); + DPBudgetAccountant acc1 = new DPBudgetAccountant(100.0, 1e-5); + DPBudgetAccountant acc2 = new DPBudgetAccountant(100.0, 1e-5); acc1.compose(0.5, 0.0, 1.0); // epsilon=0.5, Laplace acc2.compose(1.0, 0.0, 1.0); // epsilon=1.0, same sensitivity @@ -159,22 +159,22 @@ public void testHigherEpsilonCostMoreForLaplace() { @Test(expected = DMLRuntimeException.class) public void testConstructorRejectsZeroEpsilonBudget() { - new RDPAccountant(0.0, 1e-5); + new DPBudgetAccountant(0.0, 1e-5); } @Test(expected = DMLRuntimeException.class) public void testConstructorRejectsNegativeEpsilonBudget() { - new RDPAccountant(-0.5, 1e-5); + new DPBudgetAccountant(-0.5, 1e-5); } @Test(expected = DMLRuntimeException.class) public void testConstructorRejectsDeltaZero() { - new RDPAccountant(1.0, 0.0); + new DPBudgetAccountant(1.0, 0.0); } @Test(expected = DMLRuntimeException.class) public void testConstructorRejectsDeltaOne() { - new RDPAccountant(1.0, 1.0); + new DPBudgetAccountant(1.0, 1.0); } // --- Item 2: single-argument convenience constructor ------------------- @@ -184,8 +184,8 @@ public void testConvenienceConstructorDefaultsDeltaTo1e5() { // The one-arg form delegates to (epsilonBudget, 1e-5). A Gaussian // release whose per-release delta matches that default must produce // identical totalEpsilonSpent() from both construction paths. - RDPAccountant oneArg = new RDPAccountant(10.0); - RDPAccountant twoArg = new RDPAccountant(10.0, 1e-5); + DPBudgetAccountant oneArg = new DPBudgetAccountant(10.0); + DPBudgetAccountant twoArg = new DPBudgetAccountant(10.0, 1e-5); oneArg.compose(0.5, 1e-5, 1.0); twoArg.compose(0.5, 1e-5, 1.0); assertEquals("Convenience constructor must default to delta=1e-5", @@ -198,7 +198,7 @@ public void testConvenienceConstructorDefaultsDeltaTo1e5() { public void testGaussianBudgetExhaustionThrows() { // Budget = 0.1. Each Gaussian release costs more than 0.005, so 20 // releases must exceed the budget well before the loop ends. - RDPAccountant acc = new RDPAccountant(0.1, 1e-5); + DPBudgetAccountant acc = new DPBudgetAccountant(0.1, 1e-5); for (int i = 0; i < 20; i++) { acc.compose(0.3, 1e-5, 1.0); } @@ -211,9 +211,9 @@ public void testMixedCompositionExceedsEitherAlone() { // Compose one Laplace and one Gaussian release. The total cost must // exceed what either mechanism contributes alone, exercising the // _pureEpsilonSum + gaussianEps addition path in totalEpsilonSpent(). - RDPAccountant mixed = new RDPAccountant(100.0, 1e-5); - RDPAccountant lapOnly = new RDPAccountant(100.0, 1e-5); - RDPAccountant gauOnly = new RDPAccountant(100.0, 1e-5); + DPBudgetAccountant mixed = new DPBudgetAccountant(100.0, 1e-5); + DPBudgetAccountant lapOnly = new DPBudgetAccountant(100.0, 1e-5); + DPBudgetAccountant gauOnly = new DPBudgetAccountant(100.0, 1e-5); mixed.compose(0.5, 0.0, 1.0); // Laplace mixed.compose(0.5, 1e-5, 1.0); // Gaussian @@ -231,7 +231,7 @@ public void testMixedCompositionExceedsEitherAlone() { @Test public void testReleaseCountTracksAllReleases() { - RDPAccountant acc = new RDPAccountant(100.0, 1e-5); + DPBudgetAccountant acc = new DPBudgetAccountant(100.0, 1e-5); assertEquals(0, acc.releaseCount()); acc.compose(0.1, 0.0, 1.0); // Laplace assertEquals(1, acc.releaseCount()); @@ -251,8 +251,8 @@ public void testGaussianSensitivityCancelsInRDP() { // D_α = α·Δf²/(2σ²) = α·ε²/(4·ln(1.25/δ)). // Sensitivity cancels. Two accountants with the same (ε,δ) but // different sensitivity must report identical totalEpsilonSpent(). - RDPAccountant acc1 = new RDPAccountant(100.0, 1e-5); - RDPAccountant acc2 = new RDPAccountant(100.0, 1e-5); + DPBudgetAccountant acc1 = new DPBudgetAccountant(100.0, 1e-5); + DPBudgetAccountant acc2 = new DPBudgetAccountant(100.0, 1e-5); acc1.compose(0.5, 1e-5, 1.0); // sensitivity = 1 acc2.compose(0.5, 1e-5, 100.0); // sensitivity = 100, same (ε,δ) assertEquals("Gaussian RDP cost must be independent of sensitivity when (ε,δ) are fixed", @@ -263,8 +263,8 @@ public void testGaussianSensitivityCancelsInRDP() { public void testGaussianLargerEpsilonCostsMoreBudget() { // D_α ∝ ε², so a release declared at a higher ε (less noise, more // privacy loss) must cost more budget than one at a lower ε. - RDPAccountant lowEps = new RDPAccountant(100.0, 1e-5); - RDPAccountant highEps = new RDPAccountant(100.0, 1e-5); + DPBudgetAccountant lowEps = new DPBudgetAccountant(100.0, 1e-5); + DPBudgetAccountant highEps = new DPBudgetAccountant(100.0, 1e-5); lowEps.compose(0.1, 1e-5, 1.0); highEps.compose(0.5, 1e-5, 1.0); assertTrue("Larger epsilon per Gaussian release must cost more budget", From c6b7c826ef67a0d4638fec729680bb004e4d6b94 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Mon, 6 Jul 2026 17:55:33 +0200 Subject: [PATCH 10/20] Delete RDPAccountant --- .../runtime/privacy/dp/RDPAccountant.java | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100755 src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java diff --git a/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java b/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java deleted file mode 100755 index 0cd3785db7b..00000000000 --- a/src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.sysds.runtime.privacy.dp; - -/** - * @deprecated Use {@link DPBudgetAccountant} instead. - */ -@Deprecated -public class RDPAccountant extends DPBudgetAccountant { - @Deprecated - public RDPAccountant(double epsilonBudget, double delta) { - super(epsilonBudget, delta); - } - @Deprecated - public RDPAccountant(double epsilonBudget) { - super(epsilonBudget); - } -} From ef4680f565888fe3abbaf1f30d3a71f6cb5ea025 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Mon, 6 Jul 2026 23:34:11 +0200 Subject: [PATCH 11/20] Fix parseInstruction() comment --- .../sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java index be02be1cae7..a474c075a96 100755 --- a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java @@ -114,7 +114,7 @@ private DPBuiltinCPInstruction( * Reconstructs a {@code DPBuiltinCPInstruction} from its serialised * instruction string produced by the LOP layer. * - *

      Expected format (INSTRUCTION_DELIM = '\u00b0'): + *

      Expected format (OPERAND_DELIM = '\u00b0'): *

            *   dp_gaussian°target=mVar1·MATRIX·FP64°sensitivity=1.0·SCALAR·FP64·true
            *              °epsilon=0.5·SCALAR·FP64·true°delta=1e-5·SCALAR·FP64·true
      
      From 1d0fac3f87e75a580564e9b046119cc5c06a4d9e Mon Sep 17 00:00:00 2001
      From: Maya Anderson 
      Date: Tue, 7 Jul 2026 00:18:37 +0200
      Subject: [PATCH 12/20] Integration tests
      
      ---
       .../cp/DPBuiltinCPInstructionTest.java        |  99 +------------
       .../privacy/dp/DPBuiltinDMLTest.java          | 134 ++++++++++++++++++
       2 files changed, 135 insertions(+), 98 deletions(-)
       create mode 100644 src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java
      
      diff --git a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java
      index ac7d4f9bd4f..14c11567cef 100755
      --- a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java
      +++ b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java
      @@ -41,8 +41,7 @@
        * 
    * *

    The DML integration tests require a built SystemDS jar and are separated - * into a companion class {@code DPBuiltinDMLTest} (shown at the bottom of - * this file as a skeleton). + * into a companion class {@link org.apache.sysds.test.functions.privacy.dp.DPBuiltinDMLTest}. */ public class DPBuiltinCPInstructionTest { @@ -370,99 +369,3 @@ private static double variance(double[] xs) { return s / (xs.length - 1); } } - - -// ========================================================================== -// 3. DML integration test skeleton -// ========================================================================== -// -// Full integration tests extend AutomatedTestBase and drive the DML runner. -// Each test: -// (a) Writes a DML script to a temp file. -// (b) Provides input matrices via TestUtils. -// (c) Calls runTest() and reads the output MatrixBlock. -// (d) Verifies that the noisy result differs from the clean result by a -// statistically plausible amount (not zero, not astronomically large). -// -// The test below is a skeleton that compiles but needs the full SystemDS -// test infrastructure to run. - -/* -package org.apache.sysds.test.functions.privacy.dp; - -import org.apache.sysds.common.Types; -import org.apache.sysds.runtime.matrix.data.MatrixBlock; -import org.apache.sysds.test.AutomatedTestBase; -import org.apache.sysds.test.TestConfiguration; -import org.apache.sysds.test.TestUtils; -import org.junit.Test; - -public class DPBuiltinDMLTest extends AutomatedTestBase { - - private static final String TEST_DIR = "functions/privacy/dp/"; - private static final String TEST_CLASS = TEST_DIR + DPBuiltinDMLTest.class.getSimpleName() + "/"; - private static final String DML_LAPLACE = - "X = read($1);\n" - + "result = dp_laplace(colMeans(X), sensitivity=1.0, epsilon=$2);\n" - + "write(result, $3, format=\"binary\");\n"; - private static final String DML_GAUSSIAN = - "X = read($1);\n" - + "result = dp_gaussian(colMeans(X), sensitivity=1.0, epsilon=$2, delta=1e-5);\n" - + "write(result, $3, format=\"binary\");\n"; - - @Override - public void setUp() { - addTestConfiguration("DPLaplace", new TestConfiguration(TEST_CLASS, "DPLaplace")); - addTestConfiguration("DPGaussian", new TestConfiguration(TEST_CLASS, "DPGaussian")); - } - - @Test - public void testLaplaceOutputDiffersFromCleanMean() { - runDPTest("DPLaplace", DML_LAPLACE, "0.5"); - } - - @Test - public void testGaussianOutputDiffersFromCleanMean() { - runDPTest("DPGaussian", DML_GAUSSIAN, "0.5"); - } - - @Test - public void testHighEpsilonIsCloserToTruth() { - // Higher ε → less noise → result closer to the true mean. - double noisyLow = maxAbsDiff("DPGaussian", DML_GAUSSIAN, "0.1"); - double noisyHigh = maxAbsDiff("DPGaussian", DML_GAUSSIAN, "4.0"); - assertTrue("ε=4 should give less noise than ε=0.1", noisyHigh < noisyLow); - } - - private void runDPTest(String testName, String dml, String epsilonStr) { - getAndLoadTestConfiguration(testName); - int rows = 100, cols = 10; - double[][] data = TestUtils.generateTestMatrix(rows, cols, 0, 1, 1.0, 42); - writeInputMatrixWithMTD("X", data, false); - writeScriptFile(testName + ".dml", dml); - programArgs = new String[]{ input("X"), epsilonStr, output("result") }; - runTest(true, false, null, -1); - MatrixBlock result = readDMLMatrixFromHDFS("result"); - // The noisy result should be a (1 × cols) row vector. - assertEquals(1, result.getNumRows()); - assertEquals(cols, result.getNumColumns()); - // Must differ from the exact mean by a non-trivial amount. - // (A single-seed exact-equality check is fragile; use range check.) - double maxNoise = maxAbsValue(result); - assertTrue("Result should not be exactly zero", maxNoise > 0); - } - - private double maxAbsDiff(String testName, String dml, String epsilonStr) { - // Omitted for brevity: run the test, compute max |noisy - clean|. - return 0; // placeholder - } - - private static double maxAbsValue(MatrixBlock m) { - double max = 0; - for (int r = 0; r < m.getNumRows(); r++) - for (int c = 0; c < m.getNumColumns(); c++) - max = Math.max(max, Math.abs(m.get(r, c))); - return max; - } -} -*/ diff --git a/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java b/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java new file mode 100644 index 00000000000..df799d59c63 --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java @@ -0,0 +1,134 @@ +// ========================================================================== +// 3. DML integration test skeleton +// ========================================================================== +// +// Full integration tests extend AutomatedTestBase and drive the DML runner. +// Each test: +// (a) Writes a DML script to a temp file. +// (b) Provides input matrices via TestUtils. +// (c) Calls runTest() and reads the output MatrixBlock. +// (d) Verifies that the noisy result differs from the clean result by a +// statistically plausible amount (not zero, not astronomically large). +// + + +package org.apache.sysds.test.functions.privacy.dp; + +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashMap; + +import org.apache.sysds.runtime.matrix.data.MatrixValue.CellIndex; +import org.apache.sysds.test.AutomatedTestBase; +import org.apache.sysds.test.TestConfiguration; +import org.apache.sysds.test.TestUtils; +import org.junit.Test; + +public class DPBuiltinDMLTest extends AutomatedTestBase { + + private static final String TEST_DIR = "functions/privacy/dp/"; + private static final String TEST_CLASS = TEST_DIR + DPBuiltinDMLTest.class.getSimpleName() + "/"; + private static final int ROWS = 100; + private static final int COLS = 10; + private static final String DML_LAPLACE = + "X = read($1);\n" + + "result = dp_laplace(colMeans(X), sensitivity=1.0, epsilon=$2);\n" + + "write(result, $3, format=\"text\");\n"; + private static final String DML_GAUSSIAN = + "X = read($1);\n" + + "result = dp_gaussian(colMeans(X), sensitivity=1.0, epsilon=$2, delta=1e-5);\n" + + "write(result, $3, format=\"text\");\n"; + + @Override + public void setUp() { + addTestConfiguration("DPLaplace", new TestConfiguration(TEST_CLASS, "DPLaplace")); + addTestConfiguration("DPGaussian", new TestConfiguration(TEST_CLASS, "DPGaussian")); + } + + @Test + public void testLaplaceOutputDiffersFromCleanMean() { + runDPTest("DPLaplace", DML_LAPLACE, "0.5"); + } + + @Test + public void testGaussianOutputDiffersFromCleanMean() { + runDPTest("DPGaussian", DML_GAUSSIAN, "0.5"); + } + + @Test + public void testHighEpsilonIsCloserToTruth() { + // Higher ε → less noise → result closer to the true mean. + // NOTE: the DPBudgetAccountant caps total spend at the default budget + // (ε = 1.0) regardless of the per-release ε requested, so ε values + // here must stay well under that cap or the release is rejected. + double noisyLow = maxAbsDiff("DPGaussian", DML_GAUSSIAN, "0.1"); + double noisyHigh = maxAbsDiff("DPGaussian", DML_GAUSSIAN, "0.5"); + assertTrue("ε=0.5 should give less noise than ε=0.1", noisyHigh < noisyLow); + } + + private void runDPTest(String testName, String dml, String epsilonStr) { + double[][] data = TestUtils.generateTestMatrix(ROWS, COLS, 0, 1, 1.0, 42); + HashMap result = runAndGetResult(testName, dml, epsilonStr, data); + + // The noisy result should be a (1 × cols) row vector. + int maxRow = 0, maxCol = 0; + for (CellIndex ci : result.keySet()) { + maxRow = Math.max(maxRow, ci.row); + maxCol = Math.max(maxCol, ci.column); + } + assertTrue("Result should have 1 row", maxRow == 1); + assertTrue("Result should have " + COLS + " columns", maxCol == COLS); + // Must differ from the exact mean by a non-trivial amount. + // (A single-seed exact-equality check is fragile; use range check.) + double maxNoise = maxAbsValue(result); + assertTrue("Result should not be exactly zero", maxNoise > 0); + } + + private double maxAbsDiff(String testName, String dml, String epsilonStr) { + double[][] data = TestUtils.generateTestMatrix(ROWS, COLS, 0, 1, 1.0, 42); + HashMap result = runAndGetResult(testName, dml, epsilonStr, data); + + double maxDiff = 0; + for (int c = 0; c < COLS; c++) { + double sum = 0; + for (int r = 0; r < ROWS; r++) + sum += data[r][c]; + double cleanMean = sum / ROWS; + double noisy = result.get(new CellIndex(1, c + 1)); + maxDiff = Math.max(maxDiff, Math.abs(noisy - cleanMean)); + } + return maxDiff; + } + + private HashMap runAndGetResult(String testName, String dml, String epsilonStr, + double[][] data) + { + getAndLoadTestConfiguration(testName); + writeInputMatrixWithMTD("X", data, false); + + fullDMLScriptName = getScript(); + try { + File scriptFile = new File(fullDMLScriptName); + scriptFile.getParentFile().mkdirs(); + Files.write(scriptFile.toPath(), dml.getBytes()); + } + catch (IOException e) { + throw new RuntimeException(e); + } + + programArgs = new String[]{ "-args", input("X"), epsilonStr, output("result") }; + runTest(true, false, null, -1); + return readDMLMatrixFromOutputDir("result"); + } + + private static double maxAbsValue(HashMap m) { + double max = 0; + for (double v : m.values()) + max = Math.max(max, Math.abs(v)); + return max; + } +} + From ae2afcb91198585ab876c2be3925a188d42ea3e6 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Tue, 7 Jul 2026 00:31:26 +0200 Subject: [PATCH 13/20] Update test comments --- .../cp/DPBuiltinCPInstructionTest.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java index 14c11567cef..750451b2991 100755 --- a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java +++ b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinCPInstructionTest.java @@ -154,7 +154,7 @@ public void testHigherEpsilonCostMoreForLaplace() { acc1.totalEpsilonSpent() < acc2.totalEpsilonSpent()); } - // --- Item 1: constructor error paths ------------------------------------ + // --- Constructor error paths ------------------------------------ @Test(expected = DMLRuntimeException.class) public void testConstructorRejectsZeroEpsilonBudget() { @@ -176,7 +176,7 @@ public void testConstructorRejectsDeltaOne() { new DPBudgetAccountant(1.0, 1.0); } - // --- Item 2: single-argument convenience constructor ------------------- + // --- Single-argument convenience constructor ------------------- @Test public void testConvenienceConstructorDefaultsDeltaTo1e5() { @@ -191,8 +191,6 @@ public void testConvenienceConstructorDefaultsDeltaTo1e5() { twoArg.totalEpsilonSpent(), oneArg.totalEpsilonSpent(), EPS); } - // --- Item 3: budget exhaustion via Gaussian releases ------------------- - @Test(expected = DMLRuntimeException.class) public void testGaussianBudgetExhaustionThrows() { // Budget = 0.1. Each Gaussian release costs more than 0.005, so 20 @@ -203,8 +201,6 @@ public void testGaussianBudgetExhaustionThrows() { } } - // --- Item 4: mixed Laplace + Gaussian composition ---------------------- - @Test public void testMixedCompositionExceedsEitherAlone() { // Compose one Laplace and one Gaussian release. The total cost must @@ -226,7 +222,7 @@ public void testMixedCompositionExceedsEitherAlone() { mixed.totalEpsilonSpent() > gauOnly.totalEpsilonSpent()); } - // --- Item 6: release count across multiple mixed releases -------------- + // --- Release count across multiple mixed releases -------------- @Test public void testReleaseCountTracksAllReleases() { @@ -242,7 +238,7 @@ public void testReleaseCountTracksAllReleases() { assertEquals(5, acc.releaseCount()); } - // --- Item 8: edge-case inputs for rdpGaussian / gaussianSigma ---------- + // --- Edge-case inputs for rdpGaussian / gaussianSigma ---------- @Test public void testGaussianSensitivityCancelsInRDP() { @@ -277,11 +273,6 @@ public void testGaussianLargerEpsilonCostsMoreBudget() { // is near zero and the empirical variance matches the theoretical value // within a reasonable tolerance. // - // Note: these tests exercise the static fill* methods indirectly by - // calling the noise-generation logic via reflection or by making the - // methods package-private. The simplest approach for a student project - // is to make fillLaplaceNoise / fillGaussianNoise package-private and - // call them directly from the test (same package). @Test public void testLaplaceNoiseMeanNearZero() { From 394c02105d51eb90b36eaa943a4dc67b437becd9 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Tue, 7 Jul 2026 11:18:01 +0200 Subject: [PATCH 14/20] Fix integration test to compare clean to noisy data --- .../functions/privacy/dp/DPBuiltinDMLTest.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java b/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java index df799d59c63..c5ad6d78b63 100644 --- a/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java +++ b/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java @@ -81,16 +81,19 @@ private void runDPTest(String testName, String dml, String epsilonStr) { } assertTrue("Result should have 1 row", maxRow == 1); assertTrue("Result should have " + COLS + " columns", maxCol == COLS); - // Must differ from the exact mean by a non-trivial amount. + // Must differ from the exact (clean) mean by a non-trivial amount. // (A single-seed exact-equality check is fragile; use range check.) - double maxNoise = maxAbsValue(result); - assertTrue("Result should not be exactly zero", maxNoise > 0); + double maxDiff = maxAbsDiffFromClean(data, result); + assertTrue("Result should differ from the clean mean", maxDiff > 0); } private double maxAbsDiff(String testName, String dml, String epsilonStr) { double[][] data = TestUtils.generateTestMatrix(ROWS, COLS, 0, 1, 1.0, 42); HashMap result = runAndGetResult(testName, dml, epsilonStr, data); + return maxAbsDiffFromClean(data, result); + } + private static double maxAbsDiffFromClean(double[][] data, HashMap result) { double maxDiff = 0; for (int c = 0; c < COLS; c++) { double sum = 0; @@ -123,12 +126,5 @@ private HashMap runAndGetResult(String testName, String dml, runTest(true, false, null, -1); return readDMLMatrixFromOutputDir("result"); } - - private static double maxAbsValue(HashMap m) { - double max = 0; - for (double v : m.values()) - max = Math.max(max, Math.abs(v)); - return max; - } } From 67fe1b2d4afa0eb9419b86bbbebe188f2813bc8b Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Tue, 7 Jul 2026 15:56:42 +0200 Subject: [PATCH 15/20] Make integration test more readable --- .../functions/privacy/dp/DPBuiltinDMLTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java b/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java index c5ad6d78b63..048c9501e21 100644 --- a/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java +++ b/src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinDMLTest.java @@ -1,5 +1,5 @@ // ========================================================================== -// 3. DML integration test skeleton +// DML integration test // ========================================================================== // // Full integration tests extend AutomatedTestBase and drive the DML runner. @@ -60,12 +60,13 @@ public void testGaussianOutputDiffersFromCleanMean() { @Test public void testHighEpsilonIsCloserToTruth() { + double[][] data = TestUtils.generateTestMatrix(ROWS, COLS, 0, 1, 1.0, 42); // Higher ε → less noise → result closer to the true mean. // NOTE: the DPBudgetAccountant caps total spend at the default budget // (ε = 1.0) regardless of the per-release ε requested, so ε values // here must stay well under that cap or the release is rejected. - double noisyLow = maxAbsDiff("DPGaussian", DML_GAUSSIAN, "0.1"); - double noisyHigh = maxAbsDiff("DPGaussian", DML_GAUSSIAN, "0.5"); + double noisyLow = runAndGetMaxAbsColMeansDiffFromClean(data, "DPGaussian", DML_GAUSSIAN, "0.1"); + double noisyHigh = runAndGetMaxAbsColMeansDiffFromClean(data, "DPGaussian", DML_GAUSSIAN, "0.5"); assertTrue("ε=0.5 should give less noise than ε=0.1", noisyHigh < noisyLow); } @@ -83,17 +84,16 @@ private void runDPTest(String testName, String dml, String epsilonStr) { assertTrue("Result should have " + COLS + " columns", maxCol == COLS); // Must differ from the exact (clean) mean by a non-trivial amount. // (A single-seed exact-equality check is fragile; use range check.) - double maxDiff = maxAbsDiffFromClean(data, result); + double maxDiff = maxAbsColMeansDiffFromClean(data, result); assertTrue("Result should differ from the clean mean", maxDiff > 0); } - private double maxAbsDiff(String testName, String dml, String epsilonStr) { - double[][] data = TestUtils.generateTestMatrix(ROWS, COLS, 0, 1, 1.0, 42); + private double runAndGetMaxAbsColMeansDiffFromClean(double[][] data, String testName, String dml, String epsilonStr) { HashMap result = runAndGetResult(testName, dml, epsilonStr, data); - return maxAbsDiffFromClean(data, result); + return maxAbsColMeansDiffFromClean(data, result); } - private static double maxAbsDiffFromClean(double[][] data, HashMap result) { + private static double maxAbsColMeansDiffFromClean(double[][] data, HashMap result) { double maxDiff = 0; for (int c = 0; c < COLS; c++) { double sum = 0; From fbe18f095b409d96999a4c3440cc64c43f070c9a Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Tue, 7 Jul 2026 18:29:12 +0200 Subject: [PATCH 16/20] Comment fix --- .../cp/DPBuiltinCPInstruction.java | 21 ++++++------------- .../privacy/dp/DPBudgetAccountant.java | 4 +++- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java index a474c075a96..01d744ee26c 100755 --- a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java @@ -34,7 +34,7 @@ * CP instruction for differential-privacy release of an already-computed * aggregate. * - *

    DML syntax (post-aggregate form, Option A): + *

    DML syntax (post-aggregate form): *

      *   result = dp_laplace(aggregate, sensitivity=1.0, epsilon=0.5)
      *   result = dp_gaussian(aggregate, sensitivity=1.0, epsilon=0.5, delta=1e-5)
    @@ -54,19 +54,6 @@
      * single method is replaced with a static analysis that reads the
      * sensitivity bound computed by the compiler; every other line in this class
      * stays unchanged.
    - *
    - * 

    Registration required in: - *

      - *
    • {@code org.apache.sysds.common.Builtins} – add - * {@code DP_LAPLACE("dp_laplace", false)} and - * {@code DP_GAUSSIAN("dp_gaussian", false)}
    • - *
    • {@code org.apache.sysds.runtime.instructions.CPInstructionParser} – - * add opcode-to-type mappings and a parse branch that returns a - * {@code DPBuiltinCPInstruction}
    • - *
    • {@code org.apache.sysds.runtime.controlprogram.context.ExecutionContext} - * – add {@code getDPBudgetAccountant()} returning a session-scoped - * {@link DPBudgetAccountant} (lazy-initialised field)
    • - *
    */ public class DPBuiltinCPInstruction extends ComputationCPInstruction { @@ -261,10 +248,14 @@ private MatrixBlock generateNoise( noise.allocateDenseBlock(); if (instOpcode.equals(OPCODE_LAPLACE)) { + // Laplace mechanism + // For a given epsilon, noise is drawn from the Laplace distribution at + // scale b = sensitivity / epsilon fillLaplaceNoise(noise, sensitivity / epsilon); } else { // Gaussian mechanism: calibrate sigma for (epsilon, delta)-DP. - // Standard formula: sigma >= sensitivity * sqrt(2 * ln(1.25/delta)) / epsilon + // For a given epsilon, noise is drawn from the normal distribution at + // sigma^2 = 2 * sensitivity^2 * log(1.25/delta) / epsilon^2 double sigma = sensitivity * Math.sqrt(2.0 * Math.log(1.25 / delta)) / epsilon; diff --git a/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java b/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java index ce5afcb7850..707c296906f 100644 --- a/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java +++ b/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java @@ -20,6 +20,7 @@ package org.apache.sysds.runtime.privacy.dp; import org.apache.sysds.runtime.DMLRuntimeException; +import org.apache.sysds.runtime.instructions.cp.DPBuiltinCPInstruction; /** * Session-scoped differential privacy budget accountant. @@ -216,6 +217,7 @@ public void compose(double epsilon, double delta, double sensitivity) { * zero when no Gaussian releases have been recorded). */ public double totalEpsilonSpent() { + // Take min_α(ε_α) as the current total privacy cost double gaussianEps = Double.MAX_VALUE; for (int i = 0; i < ORDERS.length; i++) { double alpha = ORDERS[i]; @@ -260,7 +262,7 @@ private static double rdpGaussian(double alpha, double sensitivity, double sigma /** * Gaussian noise scale σ calibrated to (ε, δ)-DP: *
    -     *   σ = Δf · sqrt(2 · ln(1.25 / δ)) / ε
    +     *   σ = Δf · sqrt(2 · log(1.25 / δ)) / ε
          * 
    * Must match the formula used in {@link DPBuiltinCPInstruction} so that * the RDP cost recorded here is consistent with the noise actually injected. From f22c283522f677d23b76ceda81963db6b537fb42 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Tue, 7 Jul 2026 18:58:50 +0200 Subject: [PATCH 17/20] Fix L1 L2 sensitivity parameter documentation --- .../cp/DPBuiltinCPInstruction.java | 27 +++++++++++++++---- .../privacy/dp/DPBudgetAccountant.java | 14 ++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java index 01d744ee26c..63938f21881 100755 --- a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java @@ -40,6 +40,16 @@ * result = dp_gaussian(aggregate, sensitivity=1.0, epsilon=0.5, delta=1e-5) *
    * + *

    Sensitivity norm: {@code sensitivity} is not interchangeable + * between the two builtins. {@code dp_laplace} calibrates its noise scale + * to the L1 sensitivity of {@code aggregate} to a single-record + * change; {@code dp_gaussian} calibrates its σ to the L2 sensitivity. + * For a scalar aggregate (e.g. a single sum or mean) the two norms coincide, + * but for a vector-valued aggregate (e.g. column means of a multi-column + * matrix) they generally differ (L2 ≤ L1 ≤ √d·L2 for d entries) — the caller + * is responsible for supplying the norm matching the builtin invoked (see + * {@link #sensitivityOf}). + * *

    The instruction receives a materialised matrix (the aggregate result), * injects calibrated noise element-wise, records the release with the * session-scoped {@link DPBudgetAccountant}, and returns the noisy matrix. @@ -202,19 +212,26 @@ public void processInstruction(ExecutionContext ec) { // ----------------------------------------------------------------------- /** - * Returns the sensitivity of {@code aggregate} to a single-record change. + * Returns the sensitivity of {@code aggregate} to a single-record change, + * in the norm required by the mechanism actually invoked: L1 for + * {@code dp_laplace}, L2 for {@code dp_gaussian} (see the class + * Javadoc). The two only coincide when {@code aggregate} is scalar. * *

    Phase 1 (now): returns the caller-supplied literal from the - * DML script. Sensitivity analysis is the caller's responsibility. + * DML script as-is, with no norm conversion or validation — the DML + * author must compute the sensitivity in the correct norm for the + * builtin they call. Sensitivity analysis is the caller's responsibility. * *

    Phase 2 (HOP-level rewrite pass): replace this body with a * call that inspects the HOP node that produced {@code aggregate}, reads - * the {@code sensitivityBound} field computed during compilation, and - * returns it. No other line in this class changes. + * the {@code sensitivityBound} field computed during compilation (in the + * norm matching {@code instOpcode}), and returns it. No other line in + * this class changes. * * @param aggregate the already-computed aggregate block (ignored in * Phase 1; used in Phase 2 to look up lineage) - * @return caller-supplied sensitivity constant + * @return caller-supplied sensitivity constant, expected to already be + * in the L1 norm (Laplace) or L2 norm (Gaussian) */ private double sensitivityOf(MatrixBlock aggregate) { // Phase 1: unwrap the literal or variable value from the param map. diff --git a/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java b/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java index 707c296906f..9dd011fcb1e 100644 --- a/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java +++ b/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java @@ -39,7 +39,8 @@ * basic composition — each release contributes exactly its ε to a running * sum. This is the tightest possible bound for pure DP and avoids the * looser estimate that results from routing Laplace through the RDP - * conversion path (which would introduce an unnecessary δ). + * conversion path (which would introduce an unnecessary δ). Noise scale + * is calibrated to L1 sensitivity (see {@link #compose}). *

  • Gaussian (delta > 0): (ε, δ)-DP via Rényi DP composition. * Rényi divergences at a discrete set of orders α compose additively; * the accumulated sum is converted to (ε, δ) at query time using the @@ -178,7 +179,16 @@ public DPBudgetAccountant(double epsilonBudget) { * * @param epsilon per-release ε parameter (must be > 0) * @param delta per-release δ parameter (0 for Laplace, >0 for Gaussian) - * @param sensitivity L2 sensitivity Δf of the released quantity (must be > 0) + * @param sensitivity sensitivity Δf of the released quantity (must be > 0). + * The norm depends on the mechanism selected by + * {@code delta}: callers must supply the + * L1 sensitivity ‖f(D) − f(D′)‖₁ when + * {@code delta == 0} (Laplace), and the L2 + * sensitivity ‖f(D) − f(D′)‖₂ when {@code delta > 0} + * (Gaussian). The two coincide for scalar-valued + * releases but diverge for vector-valued ones, so + * passing the wrong norm silently under- or + * over-calibrates the noise. * @throws DMLRuntimeException if the cumulative ε after this release * would exceed the budget */ From 461da60554575863f001ca8fa0cc6d308dd5f316 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Tue, 7 Jul 2026 23:30:17 +0200 Subject: [PATCH 18/20] junit output format --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 068bed2e8ea..724ee4b1f1d 100644 --- a/pom.xml +++ b/pom.xml @@ -408,6 +408,7 @@ maven-surefire-plugin ${maven-surefire-plugin.version} + plain ${maven.test.skip} ${test-parallel} ${test-threadCount} From b253d141cdc0489da44f320d6a7440bf5a0a194f Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Tue, 7 Jul 2026 23:41:06 +0200 Subject: [PATCH 19/20] Remove Claude files --- CLAUDE.md | 113 -------------- CLAUDE_CODE_PLAN.md | 364 -------------------------------------------- 2 files changed, 477 deletions(-) delete mode 100644 CLAUDE.md delete mode 100755 CLAUDE_CODE_PLAN.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d3fcc2be49c..00000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,113 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project - -Apache SystemDS — an end-to-end ML system that compiles DML scripts (R-like syntax) into hybrid execution plans across local (CP), Apache Spark, GPU, and federated backends. - -## Build Commands - -```bash -# Full build (skips tests) -mvn clean package -DskipTests - -# Build and run default tests -mvn clean package - -# Run a single Java test class -mvn test -Dtest=FullMatrixMultiplicationTest - -# Run a test with a specific method -mvn test -Dtest=FullMatrixMultiplicationTest#testMethod - -# Run checkstyle (disabled by default) -mvn checkstyle:check -Dcheckstyle.skip=false - -# Python tests (from src/main/python/) -cd src/main/python && source python_venv/bin/activate && pip install -e . && python -m pytest tests/ - -# Run a DML script directly (after building) -./bin/systemds hello.dml -``` - -The surefire default test is `org.apache.sysds.test.usertest.**`. Override with `-Dtest=` to target specific classes. Test JVM is configured with `-Xmx3000m`. - -## Code Style - -Apply the Eclipse formatter profile at [dev/CodeStyle_eclipse.xml](dev/CodeStyle_eclipse.xml) before committing. Checkstyle rules are at [dev/checkstyle/checkstyle.xml](dev/checkstyle/checkstyle.xml). - -## Commit Tags - -All commits must be prefixed: `[SYSTEMDS-#]` for Jira issues, `[MINOR]` for small changes, `[DOC]` for docs, `[HOTFIX]` for release patches. The project uses linear history — rebase, never merge commits. - -## Compilation Pipeline - -The full compilation sequence is in [DMLScript.java:460-510](src/main/java/org/apache/sysds/api/DMLScript.java#L460): - -1. **Parse** — DML source → `DMLProgram` AST (ANTLR-based, `parser/dml/`) -2. **Live Variable Analysis + Validate** — `DMLTranslator.liveVariableAnalysis()` / `validateParseTree()` -3. **Construct HOPs** — AST → HOP DAG (`DMLTranslator.constructHops()`) -4. **Rewrite HOP DAGs** — algebraic simplifications, IPA, memory estimates, CSE (`hops/rewrite/`) -5. **Construct LOPs** — HOP DAG → LOP DAG; exec type (CP/Spark/GPU/Fed) selected here -6. **Rewrite LOP DAGs** — inject prefetch, broadcast, OOC tee operators -7. **Generate runtime program** — LOPs emit instruction strings; codegen (Spoof) fuses operators -8. **Execute** — `ProgramBlock` hierarchy interprets instructions via `ExecutionContext` - -## Architecture Patterns - -### Two-Level IR: HOPs → LOPs -`Hop` ([hops/Hop.java](src/main/java/org/apache/sysds/hops/Hop.java)) is the algebraic IR. Each concrete subclass (`BinaryOp`, `AggBinaryOp`, `UnaryOp`, etc.) implements abstract template methods: -- `constructLops()` — lowers this operator to backend-specific LOPs -- `optFindExecType()` — selects CP / Spark / GPU / Fed based on cost model -- `inferOutputCharacteristics()` — size/sparsity estimation -- `computeOutputMemEstimate()` / `computeIntermediateMemEstimate()` — memory cost - -`Lop` ([lops/Lop.java](src/main/java/org/apache/sysds/lops/Lop.java)) is the backend-specific IR. LOPs emit instruction strings consumed by the runtime. - -### DAGs, not Trees -Both HOPs and LOPs are DAGs with explicit `_input`/`_parent` lists and a `_visited` boolean. All rewrite/lowering passes do DFS traversal with the visited flag. This enables CSE — shared subcomputations appear once in the DAG. - -### Chain of Responsibility for Rewrites -`ProgramRewriter` ([hops/rewrite/ProgramRewriter.java](src/main/java/org/apache/sysds/hops/rewrite/ProgramRewriter.java)) holds an ordered list of `HopRewriteRule` subclasses and fires each over the full DAG. To add an optimization: subclass `HopRewriteRule`, implement `rewriteHopDAGs()` / `rewriteHopDAG()`, and register it. Existing rules include loop vectorization, loop-invariant hoisting, Spark checkpoint injection, OOC tee injection, and compressed reblock. - -### Parallel AST ↔ Runtime Hierarchies -The parser (`Statement`/`StatementBlock`) and runtime (`ProgramBlock`) mirror each other exactly: `ForStatement` → `ForProgramBlock`, `ParForStatement` → `ParForProgramBlock`, etc. The AST is structural only; all execution behavior lives in the runtime mirror. - -### String-Serialized Instruction Boundary -LOPs emit plain strings as instructions. At runtime, `InstructionParser.parseSingleInstruction()` reads the exec-type prefix (`CP·`, `SPARK·`, `GPU·`, `FED·`, `OOC·`) and dispatches to the appropriate backend parser. This decouples the compiler from all backend implementations. - -### Caching Layer -`CacheableData` ([runtime/controlprogram/caching/CacheableData.java](src/main/java/org/apache/sysds/runtime/controlprogram/caching/CacheableData.java)) is a generic abstract envelope for large data objects. Subclasses (`MatrixObject`, `FrameObject`, `TensorObject`) inherit full eviction/spill-to-disk lifecycle. - -## Key Source Locations - -| Area | Path | -|---|---| -| Compiler entry point | `src/main/java/org/apache/sysds/api/DMLScript.java` | -| DML → HOP translator | `src/main/java/org/apache/sysds/parser/DMLTranslator.java` | -| HOP base + optimizer | `src/main/java/org/apache/sysds/hops/` | -| HOP rewrites | `src/main/java/org/apache/sysds/hops/rewrite/` | -| LOP base | `src/main/java/org/apache/sysds/lops/` | -| Runtime control flow | `src/main/java/org/apache/sysds/runtime/controlprogram/` | -| Runtime instructions | `src/main/java/org/apache/sysds/runtime/instructions/` | -| Compressed Linear Algebra | `src/main/java/org/apache/sysds/runtime/compress/` | -| Operator fusion (Spoof) | `src/main/java/org/apache/sysds/runtime/codegen/` | -| Out-of-core execution | `src/main/java/org/apache/sysds/runtime/ooc/` | -| Built-in algorithms (DML) | `scripts/builtin/` | -| Staging area (new algos) | `scripts/staging/` | -| Python API | `src/main/python/systemds/` | - -## Testing - -Java tests extend `AutomatedTestBase`. The default exec mode is `ExecMode.HYBRID`; tests call `setExecMode()` / `resetExecMode()` to test specific backends. - -- **`src/test/java/.../test/functions/`** — end-to-end integration tests that run DML scripts through the full pipeline -- **`src/test/java/.../test/component/`** — unit tests targeting specific subsystems (compress, matrix, codegen, ooc, parfor, etc.) -- **`src/test/java/.../test/applications/`** — full ML algorithm correctness tests - -Each function test has a paired DML script under `src/test/scripts/functions/`. - -## Adding a New Built-in Algorithm - -New algorithms live in `scripts/staging/` until they have sufficient test coverage, then move to `scripts/builtin/`. The Python API wrappers in `src/main/python/systemds/operator/algorithm/builtin/` are auto-generated — see `src/main/python/generator/` for the generation scripts. diff --git a/CLAUDE_CODE_PLAN.md b/CLAUDE_CODE_PLAN.md deleted file mode 100755 index c08b79e80e4..00000000000 --- a/CLAUDE_CODE_PLAN.md +++ /dev/null @@ -1,364 +0,0 @@ -# Claude Code Plan: Differential-Privacy Built-ins for Apache SystemDS - -## Goal -Add `dp_laplace` and `dp_gaussian` as native (non-script) DML built-in -functions backed by a session-scoped Rényi-DP budget accountant. - -This plan is written for Claude Code. Follow the steps in order. Read each -file before editing it. Do not guess at class names, field names, or method -signatures — grep to verify every assumption before writing code. - ---- - -## Step 1 — Orient: understand the instruction routing mechanism - -```bash -# 1a. Find how opcodes are mapped to CPInstruction subclasses. -# We are looking for whatever replaces (or still is) the opcode→type map. -grep -rn "parseSingleInstruction\|CPType\|CPInstructionParser" \ - src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java \ - | head -60 -``` - -```bash -# 1b. Look for how a recently-added native instruction (e.g. lstm, compress) -# is wired in. This gives us the exact pattern to copy. -grep -n "lstm\|compress\|bias_add" \ - src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java \ - | head -30 -``` - -```bash -# 1c. Read the full parseSingleInstruction method to understand the switch/map -# dispatch that leads to parseInstruction() on a specific class. -grep -n "parseSingleInstruction\|case Dnn\|case Builtin\|DnnCPInstruction\ -\|ParameterizedBuiltin\|parseInstruction" \ - src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java \ - | head -60 -``` - -> **Note for Claude Code**: the exact field/method name may differ from -> `String2CPInstructionType`. Use the output of step 1b to find the real -> registration pattern. Copy it exactly — do not invent names. - ---- - -## Step 2 — Orient: understand the Builtins enum constructor - -```bash -# 2a. Read the constructor and the first ~60 enum entries to confirm the -# exact parameter signature (name, script) vs (name, script, ReturnType). -sed -n '35,100p' \ - src/main/java/org/apache/sysds/common/Builtins.java -``` - -```bash -# 2b. Confirm no "parameterized" field exists in the constructor. -grep -n "parameterized\|Parameterized\|boolean" \ - src/main/java/org/apache/sysds/common/Builtins.java | head -20 -``` - -> Expected: constructor is `(String name, boolean script)` with `script=false` -> for native built-ins. Verify before proceeding. - ---- - -## Step 3 — Orient: understand BuiltinFunctionExpression validation - -```bash -# 3a. Find how an existing similar native built-in (e.g. colMeans, abs) -# is validated in the parser and how it maps to a HOP. -grep -n "COLMEAN\|ABS\|BuiltinFunctionExpression\|case COLMEAN\|case ABS" \ - src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java \ - | head -30 -``` - -```bash -# 3b. Find DMLTranslator to understand how BuiltinFunctionExpression -# creates a HOP node. -grep -n "createBuiltin\|BuiltinOp\|UnaryOp\|colMeans\|case COLMEAN" \ - src/main/java/org/apache/sysds/parser/DMLTranslator.java \ - | head -20 -``` - -```bash -# 3c. Find how the HOP emits a LOP that becomes a CPInstruction opcode string. -grep -n "getOpCode\|getLops\|addLop\|Lops" \ - src/main/java/org/apache/sysds/hops/UnaryOp.java \ - | head -20 -``` - -> This tells us whether we need a new HOP type, a new LOP type, or whether -> dp_laplace/dp_gaussian can reuse an existing HOP+LOP path. - ---- - -## Step 4 — Create the new files - -Create the following files. All paths are relative to the repository root. - -### 4a. RDPAccountant - -**File**: `src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java` - -Verify the package declaration matches the target path: -```bash -head -5 src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java -``` - -### 4b. DPBuiltinCPInstruction - -**File**: `src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java` - -Verify the imports compile against the actual codebase: - -```bash -# Confirm BinaryOperator and Plus exist at the expected paths. -find src/main/java -name "BinaryOperator.java" -o -name "Plus.java" | head -5 - -# Confirm MatrixBlock.binaryOperations signature. -grep -n "binaryOperations" \ - src/main/java/org/apache/sysds/runtime/matrix/data/MatrixBlock.java \ - | head -10 -``` - -If `MatrixBlock.binaryOperations` has a different signature, update the call -in `processInstruction` to match. - ---- - -## Step 5 — Patch ExecutionContext to carry the accountant - -```bash -# 5a. Read the end of ExecutionContext to find where to add the new field. -grep -n "class ExecutionContext\|private.*Map\|private.*List\|getMatrix\ -\|releaseMatrix\|setScalar" \ - src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java \ - | tail -40 -``` - -Add the following to `ExecutionContext.java`: - -```java -// --- DP budget accountant (lazy-initialised, one per script execution) --- -private RDPAccountant _rdpAccountant = null; - -public RDPAccountant getRDPAccountant() { - if (_rdpAccountant == null) - // Default budget: ε=1.0, δ=1e-5. Future work: set via DML built-in. - _rdpAccountant = new RDPAccountant(1.0, 1e-5); - return _rdpAccountant; -} -``` - -Add the import at the top of `ExecutionContext.java`: -```java -import org.apache.sysds.runtime.privacy.dp.RDPAccountant; -``` - ---- - -## Step 6 — Register in Builtins.java - -```bash -# 6a. Find a good alphabetical insertion point between "D" entries. -grep -n "^[[:space:]]*D[A-Z_]*(" \ - src/main/java/org/apache/sysds/common/Builtins.java | head -20 -``` - -Insert after the last `D`-prefixed entry (or before the first `E` entry): - -```java -DP_LAPLACE("dp_laplace", false), -DP_GAUSSIAN("dp_gaussian", false), -``` - -Confirm `script=false` is correct by checking a nearby native built-in: -```bash -grep -A1 "DIAG\|DECOMPRESS\|DET" \ - src/main/java/org/apache/sysds/common/Builtins.java | head -10 -``` - ---- - -## Step 7 — Wire into the parser - -Use the routing pattern discovered in Step 1. The pattern will be one of: - -**Pattern A — opcode map + switch** (most likely based on commit history): -```bash -# Find the CPType enum to add a new entry if needed. -grep -n "enum CPType\|Dnn,\|BuiltinNary,\|ParameterizedBuiltin," \ - src/main/java/org/apache/sysds/runtime/instructions/cp/CPInstruction.java \ - | head -20 -``` - -If a new `CPType.DPBuiltin` is needed, add it to the `CPType` enum, then add -the map entry and switch case following the exact pattern of `CPType.Dnn`. - -**Pattern B — direct opcode string match in parseSingleInstruction**: -Add a branch: -```java -else if (opcode.equals("dp_laplace") || opcode.equals("dp_gaussian")) - return DPBuiltinCPInstruction.parseInstruction(str); -``` - -> Use whichever pattern the codebase actually uses. Do not mix patterns. - ---- - -## Step 8 — Wire into BuiltinFunctionExpression - -```bash -# 8a. Find the validate() switch to add parameter checking. -grep -n "case DIAG\|case ABS\|case COLMEAN\|checkNumParameters\|checkMatrixParam" \ - src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java \ - | head -20 -``` - -Add cases for `DP_LAPLACE` and `DP_GAUSSIAN` in the `validate()` switch: - -```java -case DP_LAPLACE: { - // dp_laplace(aggregate, sensitivity, epsilon) - checkNumParameters(3); - checkMatrixParam(getFirstExpr()); // aggregate matrix - checkScalarParam(getSecondExpr()); // sensitivity - checkScalarParam(getThirdExpr()); // epsilon - output.setDataType(DataType.MATRIX); - output.setValueType(ValueType.FP64); - // Output shape matches input shape; dimensions copied from input. - output.setDimensions( - getFirstExpr().getOutput().getDim1(), - getFirstExpr().getOutput().getDim2()); - break; -} -case DP_GAUSSIAN: { - // dp_gaussian(aggregate, sensitivity, epsilon, delta) - checkNumParameters(4); - checkMatrixParam(getFirstExpr()); - checkScalarParam(getSecondExpr()); // sensitivity - checkScalarParam(getThirdExpr()); // epsilon - checkScalarParam(getFourthExpr()); // delta - output.setDataType(DataType.MATRIX); - output.setValueType(ValueType.FP64); - output.setDimensions( - getFirstExpr().getOutput().getDim1(), - getFirstExpr().getOutput().getDim2()); - break; -} -``` - -> Verify that `checkScalarParam` and `getFourthExpr` exist: -> ```bash -> grep -n "checkScalarParam\|getFourthExpr\|getThirdExpr" \ -> src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java \ -> | head -10 -> ``` - ---- - -## Step 9 — Wire into DMLTranslator - -```bash -# 9a. Find how colMeans (COLMEAN) translates to a HOP in DMLTranslator. -grep -n "COLMEAN\|case COLMEAN\|createBuiltinOp\|UnaryOp" \ - src/main/java/org/apache/sysds/parser/DMLTranslator.java \ - | head -20 -``` - -```bash -# 9b. Find where to add new cases — likely inside a large switch on Builtins. -grep -n "case ABS\|case DIAG\|case CEIL" \ - src/main/java/org/apache/sysds/parser/DMLTranslator.java | head -10 -``` - -The simplest approach: map `DP_LAPLACE` and `DP_GAUSSIAN` to a `UnaryOp` HOP -with a custom opcode string. The LOP produced by `UnaryOp` will carry the -opcode string `"dp_laplace"` or `"dp_gaussian"`, which the `CPInstructionParser` -will then route to `DPBuiltinCPInstruction.parseInstruction`. - -Add cases following the existing unary pattern: -```java -case DP_LAPLACE: -case DP_GAUSSIAN: - // Reuse UnaryOp HOP — the opcode string routes to DPBuiltinCPInstruction. - currBuiltinOp = new UnaryOp(target.getName(), DataType.MATRIX, - ValueType.FP64, OpOp1.valueOf(bi.name()), expr); - break; -``` - -> Verify `OpOp1` has a compatible entry or whether a different HOP class is -> needed. If `OpOp1` does not work, fall back to creating a `FunctionOp`. - ---- - -## Step 10 — Build and smoke test - -```bash -# Build only the affected modules to get fast feedback. -mvn compile -pl src/main/java -am -q 2>&1 | tail -30 -``` - -Fix any compilation errors before proceeding. - -```bash -# Run the self-contained unit tests (no SystemDS runtime needed). -mvn test -pl src/test/java \ - -Dtest=DPBuiltinCPInstructionTest \ - -Dsurefire.failIfNoSpecifiedTests=false \ - 2>&1 | tail -40 -``` - -```bash -# Smoke-test end-to-end with a minimal DML script. -cat > /tmp/dp_smoke.dml << 'EOF' -X = rand(rows=100, cols=10, min=0, max=1); -mu = colMeans(X); -noisy = dp_laplace(mu, sensitivity=0.1, epsilon=1.0); -print(toString(noisy)); -EOF - -./bin/systemds /tmp/dp_smoke.dml 2>&1 | tail -20 -``` - -If the smoke test fails with an opcode-not-found error, re-check Steps 7 and 9. -If it fails with a budget error, reduce the sensitivity or widen epsilon. - ---- - -## Step 11 — Run the federated benchmark - -```bash -cat > /tmp/dp_fedavg_benchmark.dml << 'EOF' -# Federated averaging with DP release of column means. -# Sweep epsilon across {0.5, 1, 4, 8} by passing $epsilon as an arg. -X = read($1); -mu = colMeans(X); -noisy = dp_gaussian(mu, sensitivity=$sensitivity, epsilon=$epsilon, delta=1e-5); -write(noisy, $2, format="csv"); -EOF - -for eps in 0.5 1 4 8; do - echo "--- epsilon=$eps ---" - ./bin/systemds /tmp/dp_fedavg_benchmark.dml \ - -args data/adult.csv /tmp/result_eps${eps}.csv \ - -nvargs sensitivity=0.01 epsilon=$eps \ - 2>&1 | tail -5 -done -``` - ---- - -## Files modified (summary) - -| File | Change | -|---|---| -| `src/main/java/org/apache/sysds/common/Builtins.java` | Add `DP_LAPLACE`, `DP_GAUSSIAN` entries | -| `src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java` | Add validate cases | -| `src/main/java/org/apache/sysds/parser/DMLTranslator.java` | Add HOP-creation cases | -| `src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java` | Register opcodes using actual routing pattern | -| `src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java` | **New file** | -| `src/main/java/org/apache/sysds/runtime/privacy/dp/RDPAccountant.java` | **New file** | -| `src/main/java/org/apache/sysds/runtime/controlprogram/context/ExecutionContext.java` | Add `getRDPAccountant()` | -| `src/test/java/org/apache/sysds/test/functions/privacy/dp/DPBuiltinCPInstructionTest.java` | **New file** (unit tests) | From 379ddcd6b5158cfec26ed6cb272d169be48ec028 Mon Sep 17 00:00:00 2001 From: Maya Anderson Date: Thu, 9 Jul 2026 23:44:53 +0200 Subject: [PATCH 20/20] Remove files irrelevant to the PR --- src/main/python/notebooks/python_e2e_l1.ipynb | 520 ------------------ src/main/python/notebooks/quick_start.ipynb | 234 -------- src/main/python/requirements.txt | 9 - 3 files changed, 763 deletions(-) delete mode 100644 src/main/python/notebooks/python_e2e_l1.ipynb delete mode 100644 src/main/python/notebooks/quick_start.ipynb delete mode 100644 src/main/python/requirements.txt diff --git a/src/main/python/notebooks/python_e2e_l1.ipynb b/src/main/python/notebooks/python_e2e_l1.ipynb deleted file mode 100644 index 34564cffbd6..00000000000 --- a/src/main/python/notebooks/python_e2e_l1.ipynb +++ /dev/null @@ -1,520 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "26ef529d", - "metadata": {}, - "source": [ - "This example shows how one can work the SystemDS framework. More precisely, we will make use of the built-in DataManager, Multinomial Logistic Regression function, and the Confusion Matrix function. The dataset used in this tutorial is a preprocessed version of the “UCI Adult Data Set”. If one wants to skip the explanation then the full script is available at the end of this level.\n", - "\n", - "We will train a Multinomial Logistic Regression model on the training dataset and subsequently use the test dataset to assess how well our model can predict if the income is above or below $50K/yr based on the features." - ] - }, - { - "cell_type": "markdown", - "id": "18ac25e2", - "metadata": {}, - "source": [ - "# Step 1: Load and prepare data\n", - "First, we get our training and testing data from the built-in DataManager. Since the multiLogReg function requires the labels (Y) to be > 0, we add 1 to all labels. This ensures that the smallest label is >= 1. Additionally we will only take a fraction of the training and test set into account to speed up the execution." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "40c4453f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Warning: Scuro dependencies missing or wrong version installed: torch 2.4.1, torchvision 0.19.1, librosa 0.10.2, opencv-python 4.10.0.84, opt-einsum 3.3.0, h5py 3.11.0, transformers 4.46.3, nltk 3.9.1, gensim 4.3.3\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: Using incubator modules: jdk.incubator.vector\n", - "26/05/06 15:19:28 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n" - ] - } - ], - "source": [ - "from systemds.context import SystemDSContext\n", - "from systemds.examples.tutorials.adult import DataManager\n", - "from systemds.operator.algorithm import multiLogReg\n", - "from systemds.operator.algorithm import multiLogRegPredict\n", - "from systemds.operator.algorithm import confusionMatrix\n", - "\n", - "with SystemDSContext() as sds:\n", - " d = DataManager()\n", - "\n", - " # limit the sample size\n", - " train_count = 15000\n", - " test_count = 5000\n", - "\n", - " # Get train and test datasets.\n", - " X_frame, Y_frame, Xt_frame, Yt_frame = d.get_preprocessed_dataset(sds)\n", - "\n", - " # Transformation specification\n", - " jspec_data = d.get_jspec(sds)\n", - " jspec_labels = sds.scalar(f'\"{ {\"recode\": [\"income\"]} }\"')\n", - "\n", - " # Transform frames to matrices.\n", - " X, M1 = X_frame.transform_encode(spec=jspec_data)\n", - " Xt = Xt_frame.transform_apply(spec=jspec_data, meta=M1) \n", - " Y, M2 = Y_frame.transform_encode(spec=jspec_labels)\n", - " Yt = Yt_frame.transform_apply(spec=jspec_labels, meta=M2) \n", - " \n", - " # Subsample to make training faster\n", - " X = X[0:train_count]\n", - " Y = Y[0:train_count]\n", - " Xt = Xt[0:test_count]\n", - " Yt = Yt[0:test_count]" - ] - }, - { - "cell_type": "markdown", - "id": "349c0d0b", - "metadata": {}, - "source": [ - "Here the DataManager contains the code for downloading and setting up either Pandas DataFrames or internal SystemDS Frames, for the best performance and no data transfer from pandas to SystemDS it is recommended to read directly from disk into SystemDS." - ] - }, - { - "cell_type": "markdown", - "id": "4a180e28", - "metadata": {}, - "source": [ - "# Step 2: Training\n", - "Now that we prepared the data, we can use the multiLogReg function. First, we will train the model on our training data. Afterward, we can make predictions on the test data and assess the performance of the model." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "079a61b4", - "metadata": {}, - "outputs": [], - "source": [ - "betas = multiLogReg(X, Y, verbose=False)" - ] - }, - { - "cell_type": "markdown", - "id": "d145d87a", - "metadata": {}, - "source": [ - "Note that nothing has been calculated yet. In SystemDS the calculation is executed once `compute()` is called. E.g. `betas_res = betas.compute()`.\n", - "\n", - "We can now use the trained model to make predictions on the test data." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "6b734c4d", - "metadata": {}, - "outputs": [], - "source": [ - "[_, y_pred, acc] = multiLogRegPredict(Xt, betas, Y=Yt)" - ] - }, - { - "cell_type": "markdown", - "id": "db63e4ab", - "metadata": {}, - "source": [ - "The multiLogRegPredict function has three return values:\n", - "\n", - "`m`, a matrix with the mean probability of correctly classifying each label. We do not use it further in this example.\n", - "\n", - "`y_pred`, is the predictions made using the model\n", - "\n", - "`acc`, is the accuracy achieved by the model." - ] - }, - { - "cell_type": "markdown", - "id": "bffaa936", - "metadata": {}, - "source": [ - "# Step 3: Confusion Matrix\n", - "A confusion matrix is a useful tool to analyze the performance of the model and to obtain a better understanding which classes the model has difficulties separating. The confusionMatrix function takes the predicted labels and the true labels. It then returns the confusion matrix for the predictions and the confusion matrix averages of each true class." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f3cd2e62", - "metadata": {}, - "outputs": [], - "source": [ - "confusion_matrix_abs, _ = confusionMatrix(y_pred, Yt).compute()" - ] - }, - { - "cell_type": "markdown", - "id": "37bfe729", - "metadata": {}, - "source": [ - "# Full Script" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "54190e79", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Warning: Scuro dependencies missing or wrong version installed: torch 2.4.1, torchvision 0.19.1, librosa 0.10.2, opencv-python 4.10.0.84, opt-einsum 3.3.0, h5py 3.11.0, transformers 4.46.3, nltk 3.9.1, gensim 4.3.3\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: Using incubator modules: jdk.incubator.vector\n", - "26/05/06 16:39:07 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", - "INFO:python_e2e_l1_example:Confusion Matrix\n", - "INFO:python_e2e_l1_example:[[3580. 488.]\n", - " [ 248. 684.]]\n" - ] - } - ], - "source": [ - "from systemds.context import SystemDSContext\n", - "from systemds.examples.tutorials.adult import DataManager\n", - "from systemds.operator.algorithm import multiLogReg\n", - "from systemds.operator.algorithm import multiLogRegPredict\n", - "from systemds.operator.algorithm import confusionMatrix\n", - "from systemds.operator.algorithm import getAccuracy\n", - "\n", - "import logging\n", - "logger = logging.getLogger('python_e2e_l1_example')\n", - "logger.setLevel(logging.INFO)\n", - "\n", - "with SystemDSContext() as sds:\n", - " d = DataManager()\n", - "\n", - " # limit the sample size\n", - " train_count = 15000\n", - " test_count = 5000\n", - "\n", - " # Get train and test datasets.\n", - " X_frame, Y_frame, Xt_frame, Yt_frame = d.get_preprocessed_dataset(sds)\n", - "\n", - " # Transformation specification\n", - " jspec_data = d.get_jspec(sds)\n", - " jspec_labels = sds.scalar(f'\"{ {\"recode\": [\"income\"]} }\"')\n", - "\n", - " # Transform frames to matrices.\n", - " X, M1 = X_frame.transform_encode(spec=jspec_data)\n", - " Xt = Xt_frame.transform_apply(spec=jspec_data, meta=M1) \n", - " Y, M2 = Y_frame.transform_encode(spec=jspec_labels)\n", - " Yt = Yt_frame.transform_apply(spec=jspec_labels, meta=M2) \n", - " \n", - " # Subsample to make training faster\n", - " X = X[0:train_count]\n", - " Y = Y[0:train_count]\n", - " Xt = Xt[0:test_count]\n", - " Yt = Yt[0:test_count]\n", - "\n", - " # Train model \n", - " betas = multiLogReg(X, Y, verbose=False)\n", - "\n", - " # Apply model\n", - " [_, y_pred, acc] = multiLogRegPredict(Xt, betas, Y=Yt)\n", - "\n", - " # Confusion Matrix\n", - " confusion_matrix_abs, _ = confusionMatrix(y_pred, Yt).compute()\n", - " \n", - " # accuracy = getAccuracy(Yt, y_pred).compute()\n", - "\n", - "\n", - " logger.info(\"Confusion Matrix\")\n", - " logger.info(confusion_matrix_abs)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "c2d7049a", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
    " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "from sklearn.metrics import ConfusionMatrixDisplay\n", - "\n", - "disp = ConfusionMatrixDisplay(confusion_matrix=confusion_matrix_abs)\n", - "disp.plot(cmap=plt.cm.Blues)\n", - "plt.title('Confusion Matrix')\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "a9360ea0", - "metadata": {}, - "source": [ - "The Confusion Matrix created has four different quadrants:\n", - "\n", - "- True Negative (Top-Left Quadrant)\n", - "- False Positive (Top-Right Quadrant)\n", - "- False Negative (Bottom-Left Quadrant)\n", - "- True Positive (Bottom-Right Quadrant)\n", - "\n", - "True means that the values were accurately predicted, False means that there was an error or wrong prediction." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "90b88789", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([[3580., 488.],\n", - " [ 248., 684.]])" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "confusion_matrix_abs" - ] - }, - { - "cell_type": "markdown", - "id": "c42dc06f", - "metadata": {}, - "source": [ - "# Accuracy\n", - "\n", - "Accuracy measures how often the model is correct.\n", - "\n", - "(True Positive + True Negative) / Total Predictions\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "d69262cd", - "metadata": {}, - "outputs": [], - "source": [ - "TN, FP, FN, TP = confusion_matrix_abs[0,0], confusion_matrix_abs[0,1], confusion_matrix_abs[1,0], confusion_matrix_abs[1,1]" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "263ca554", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "np.float64(0.8528)" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# from systemds.operator.algorithm import getAccuracy\n", - "# accuracy = getAccuracy(Yt, y_pred).compute()\n", - "\n", - "\n", - "accuracy = (TP + TN) / test_count\n", - "accuracy" - ] - }, - { - "cell_type": "markdown", - "id": "54ea1ecf", - "metadata": {}, - "source": [ - "# Precision\n", - "Of the positives predicted, what percentage is truly positive?\n", - "\n", - "True Positive / (True Positive + False Positive)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "d7daf015", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "np.float64(0.5836177474402731)" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "precision = TP / (TP + FP)\n", - "precision" - ] - }, - { - "cell_type": "markdown", - "id": "9b24ea83", - "metadata": {}, - "source": [ - "# Sensitivity (Recall)\n", - "Of all the positive cases, what percentage are predicted positive?\n", - "\n", - "Sensitivity (sometimes called Recall) measures how good the model is at predicting positives.\n", - "\n", - "This means it looks at true positives and false negatives (which are positives that have been incorrectly predicted as negative).\n", - "\n", - "True Positive / (True Positive + False Negative)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "57e04ed3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "np.float64(0.7339055793991416)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sensitivity = TP /(TP + FN)\n", - "sensitivity" - ] - }, - { - "cell_type": "markdown", - "id": "52abc77a", - "metadata": {}, - "source": [ - "# Specificity\n", - "How well the model is at prediciting negative results?\n", - "\n", - "Specificity is similar to sensitivity, but looks at it from the persepctive of negative results.\n", - "\n", - "True Negative / (True Negative + False Positive)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "3dcfa6b8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "np.float64(0.880039331366765)" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "specificity = TN / (TN + FP)\n", - "specificity" - ] - }, - { - "cell_type": "markdown", - "id": "127fe71d", - "metadata": {}, - "source": [ - "# F-score\n", - "F-score is the \"harmonic mean\" of precision and sensitivity.\n", - "\n", - "It considers both false positive and false negative cases and is good for imbalanced datasets.\n", - "\n", - "2 * ((Precision * Sensitivity) / (Precision + Sensitivity))" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "bceed706", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "np.float64(0.6501901140684411)" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "f_score = 2 * ((precision * sensitivity) / (precision + sensitivity))\n", - "f_score" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "python_venv (3.12.3)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/src/main/python/notebooks/quick_start.ipynb b/src/main/python/notebooks/quick_start.ipynb deleted file mode 100644 index 5801f0882a0..00000000000 --- a/src/main/python/notebooks/quick_start.ipynb +++ /dev/null @@ -1,234 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0213111f", - "metadata": {}, - "source": [ - "# Matrix Operations" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "f3d1b80e", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: Using incubator modules: jdk.incubator.vector\n", - "26/05/06 14:56:07 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", - "INFO:simple_example:[[13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]\n", - " [13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]\n", - " [13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]\n", - " [13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]\n", - " [13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02 13.02]]\n" - ] - } - ], - "source": [ - "import logging\n", - "\n", - "from systemds.context import SystemDSContext\n", - "\n", - "logger = logging.getLogger('simple_example')\n", - "logger.setLevel(logging.INFO)\n", - "\n", - "# Create a context and if necessary (no SystemDS py4j instance running)\n", - "# it starts a subprocess which does the execution in SystemDS\n", - "with SystemDSContext() as sds:\n", - " # Full generates a matrix completely filled with one number.\n", - " # Generate a 5x10 matrix filled with 4.2\n", - " m = sds.full((5, 10), 4.20)\n", - " # multiply with scalar. Nothing is executed yet!\n", - " m_res = m * 3.1\n", - " # Do the calculation in SystemDS by calling compute().\n", - " # The returned value is an numpy array that can be directly printed.\n", - " logger.info(m_res.compute())\n", - " # context will automatically be closed and process stopped" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "cadb888c", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: Using incubator modules: jdk.incubator.vector\n", - "26/05/06 15:02:21 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", - "26/05/06 15:02:21 WARNING SystemDSContext: Deprecated method from_numpy. Use from_py instead.\n", - "26/05/06 15:02:21 WARNING SystemDSContext: Deprecated method from_numpy. Use from_py instead.\n", - "INFO:simple_example:[[ 50.01 125.05 160.05 32.01 204.04]\n", - " [ 37.01 81.03 268.04 110.02 6.03]\n", - " [ 39.01 244.04 430.05 140.04 368.04]\n", - " [246.03 154.02 267.03 104.04 72.02]\n", - " [ 14.02 186.02 475.05 168.02 315.05]]\n" - ] - } - ], - "source": [ - "import logging\n", - "\n", - "import numpy as np\n", - "from systemds.context import SystemDSContext\n", - "\n", - "logger = logging.getLogger('simple_example')\n", - "logger.setLevel(logging.INFO)\n", - "\n", - "# create a random array\n", - "m1 = np.array(np.random.randint(100, size=5 * 5) + 1.01, dtype=np.double)\n", - "m1 = m1.reshape(5, 5)\n", - "# create another random array\n", - "m2 = np.array(np.random.randint(5, size=5 * 5) + 1, dtype=np.double)\n", - "m2 = m2.reshape(5, 5)\n", - "\n", - "# Create a context\n", - "with SystemDSContext() as sds:\n", - " # element-wise matrix multiplication, note that nothing is executed yet!\n", - " m_res = sds.from_numpy(m1) * sds.from_numpy(m2)\n", - " # lets do the actual computation in SystemDS! The result is an numpy array\n", - " m_res_np = m_res.compute()\n", - " logger.info(m_res_np)" - ] - }, - { - "cell_type": "markdown", - "id": "87cc9e28", - "metadata": {}, - "source": [ - "# Algorithms as built-in functions" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "bf202fb1", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: Using incubator modules: jdk.incubator.vector\n", - "26/05/06 15:05:20 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", - "26/05/06 15:05:20 WARNING SystemDSContext: Deprecated method from_numpy. Use from_py instead.\n", - "26/05/06 15:05:20 WARNING SystemDSContext: Deprecated method from_numpy. Use from_py instead.\n", - "INFO:simple_example:[[ 0.02033445]\n", - " [-0.00324092]\n", - " [ 0.0014692 ]\n", - " [ 0.02649209]\n", - " [-0.00616902]\n", - " [-0.0095087 ]\n", - " [ 0.01039221]\n", - " [-0.0011352 ]\n", - " [-0.01686351]\n", - " [-0.03839821]]\n" - ] - } - ], - "source": [ - "import logging\n", - "\n", - "import numpy as np\n", - "from systemds.context import SystemDSContext\n", - "from systemds.operator.algorithm import l2svm\n", - "\n", - "logger = logging.getLogger('simple_example')\n", - "logger.setLevel(logging.INFO)\n", - "\n", - "# Set a seed\n", - "np.random.seed(0)\n", - "# Generate random features and labels in numpy\n", - "# This can easily be exchanged with a data set.\n", - "features = np.array(np.random.randint(\n", - " 100, size=10 * 10) + 1.01, dtype=np.double)\n", - "features = features.reshape(10, 10)\n", - "labels = np.zeros((10, 1))\n", - "\n", - "# l2svm labels can only be 0 or 1\n", - "for i in range(10):\n", - " if np.random.random() > 0.5:\n", - " labels[i][0] = 1\n", - "\n", - "# compute our model\n", - "with SystemDSContext() as sds:\n", - " model = l2svm(sds.from_numpy(features),\n", - " sds.from_numpy(labels), verbose=False).compute()\n", - " logger.info(model)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "266e1c4b", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: Using incubator modules: jdk.incubator.vector\n", - "26/05/06 15:07:21 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", - "INFO:simple_example:[[-0.02674835]\n", - " [-0.01684308]\n", - " [ 0.02429331]\n", - " [ 0.03332674]\n", - " [-0.00411165]\n", - " [ 0.00514915]\n", - " [ 0.01035419]\n", - " [ 0.00611115]\n", - " [-0.02550657]\n", - " [-0.0117999 ]]\n" - ] - } - ], - "source": [ - "import logging\n", - "\n", - "from systemds.context import SystemDSContext\n", - "from systemds.operator.algorithm import l2svm\n", - "\n", - "logger = logging.getLogger('simple_example')\n", - "logger.setLevel(logging.INFO)\n", - "\n", - "with SystemDSContext() as sds:\n", - " # Generate 10 by 10 matrix with values in range 0 to 100.\n", - " features2 = sds.rand(10, 10, 0, 100)\n", - " # Add value to all cells in features\n", - " features2 += 1.1\n", - " # Generate labels of all ones and zeros\n", - " labels2 = sds.rand(10, 1, 1, 1, sparsity=0.5)\n", - "\n", - " model2 = l2svm(features2, labels2, verbose=False).compute()\n", - " logger.info(model2)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "python_venv (3.12.3)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/src/main/python/requirements.txt b/src/main/python/requirements.txt deleted file mode 100644 index 3c7af412a4c..00000000000 --- a/src/main/python/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -numpy -scipy -py4j -wheel -requests -setuptools - -scikit-learn -matplotlib