diff --git a/.github/workflows/javaTests.yml b/.github/workflows/javaTests.yml index 9dddcfb25d6..a8018e5ea86 100644 --- a/.github/workflows/javaTests.yml +++ b/.github/workflows/javaTests.yml @@ -83,6 +83,7 @@ jobs: "**.functions.frame.**,**.functions.indexing.**,**.functions.io.**,**.functions.iogen.**", "**.functions.dnn.**,**.functions.ooc.**", "**.functions.paramserv.**", + "**.functions.privacy.**", "**.functions.recompile.**,**.functions.misc.**", "**.functions.mlcontext.**", "**.functions.nary.**,**.functions.quaternary.**", diff --git a/src/main/java/org/apache/sysds/common/Builtins.java b/src/main/java/org/apache/sysds/common/Builtins.java index d6e1954bc16..338e683c0e0 100644 --- a/src/main/java/org/apache/sysds/common/Builtins.java +++ b/src/main/java/org/apache/sysds/common/Builtins.java @@ -127,6 +127,7 @@ public enum Builtins { SETDIFF("setdiff", true), DIST("dist", true), DMV("dmv", true), + DP_SET_BUDGET("dp_set_budget", false), DROP_INVALID_TYPE("dropInvalidType", false), DROP_INVALID_LENGTH("dropInvalidLength", false), EIGEN("eigen", false, ReturnType.MULTI_RETURN), @@ -377,6 +378,8 @@ public enum Builtins { COUNT_DISTINCT_APPROX_ROW("rowCountDistinctApprox", false, true), COUNT_DISTINCT_APPROX_COL("colCountDistinctApprox", false, true), CVLM("cvlm", true, false), + DP_GAUSSIAN("dp_gaussian", false, true), + DP_LAPLACE("dp_laplace", false, true), GROUPEDAGG("aggregate", "groupedAggregate", false, true), INVCDF("icdf", false, true), LISTNV("list", false, true), //note: builtin and parbuiltin diff --git a/src/main/java/org/apache/sysds/common/Opcodes.java b/src/main/java/org/apache/sysds/common/Opcodes.java index 9a894dde13b..61482ebebcf 100644 --- a/src/main/java/org/apache/sysds/common/Opcodes.java +++ b/src/main/java/org/apache/sysds/common/Opcodes.java @@ -196,24 +196,26 @@ public enum Opcodes { //Parametrized builtin functions AUTODIFF("autoDiff", InstructionType.ParameterizedBuiltin), - CONTAINS("contains", InstructionType.ParameterizedBuiltin), - PARAMSERV("paramserv", InstructionType.ParameterizedBuiltin), - NVLIST("nvlist", InstructionType.ParameterizedBuiltin), CDF("cdf", InstructionType.ParameterizedBuiltin), - INVCDF("invcdf", InstructionType.ParameterizedBuiltin), + CONTAINS("contains", InstructionType.ParameterizedBuiltin), + DP_GAUSSIAN("dp_gaussian", InstructionType.ParameterizedBuiltin), + DP_LAPLACE("dp_laplace", InstructionType.ParameterizedBuiltin), GROUPEDAGG("groupedagg", InstructionType.ParameterizedBuiltin), - RMEMPTY("rmempty", InstructionType.ParameterizedBuiltin), - REPLACE("replace", InstructionType.ParameterizedBuiltin), + INVCDF("invcdf", InstructionType.ParameterizedBuiltin), LOWERTRI("lowertri", InstructionType.ParameterizedBuiltin), - UPPERTRI("uppertri", InstructionType.ParameterizedBuiltin), + NVLIST("nvlist", InstructionType.ParameterizedBuiltin), + PARAMSERV("paramserv", InstructionType.ParameterizedBuiltin), + REPLACE("replace", InstructionType.ParameterizedBuiltin), REXPAND("rexpand", InstructionType.ParameterizedBuiltin), - TOSTRING("toString", InstructionType.ParameterizedBuiltin), + RMEMPTY("rmempty", InstructionType.ParameterizedBuiltin), TOKENIZE("tokenize", InstructionType.ParameterizedBuiltin), + TOSTRING("toString", InstructionType.ParameterizedBuiltin), TRANSFORMAPPLY("transformapply", InstructionType.ParameterizedBuiltin), TRANSFORMDECODE("transformdecode", InstructionType.ParameterizedBuiltin), TRANSFORMCOLMAP("transformcolmap", InstructionType.ParameterizedBuiltin), TRANSFORMMETA("transformmeta", InstructionType.ParameterizedBuiltin), TRANSFORMENCODE("transformencode", InstructionType.MultiReturnParameterizedBuiltin, InstructionType.MultiReturnBuiltin), + UPPERTRI("uppertri", InstructionType.ParameterizedBuiltin), GET_CATEGORICAL_MASK("get_categorical_mask", InstructionType.Binary), diff --git a/src/main/java/org/apache/sysds/common/Types.java b/src/main/java/org/apache/sysds/common/Types.java index 624c9eed3c6..f71d3566c79 100644 --- a/src/main/java/org/apache/sysds/common/Types.java +++ b/src/main/java/org/apache/sysds/common/Types.java @@ -806,10 +806,9 @@ 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, - LOWER_TRI, UPPER_TRI, - TRANSFORMAPPLY, TRANSFORMDECODE, TRANSFORMCOLMAP, TRANSFORMMETA, - TOKENIZE, TOSTRING, LIST, PARAMSERV + AUTODIFF, CDF, CONTAINS, DP_GAUSSIAN, DP_LAPLACE, INVALID, INVCDF, GROUPEDAGG, RMEMPTY, REPLACE, REXPAND, + LOWER_TRI, UPPER_TRI, TRANSFORMAPPLY, TRANSFORMDECODE, TRANSFORMCOLMAP, TRANSFORMMETA, TOKENIZE, TOSTRING, LIST, + PARAMSERV } /** Deep Neural Network specific operations */ diff --git a/src/main/java/org/apache/sysds/hops/ParameterizedBuiltinOp.java b/src/main/java/org/apache/sysds/hops/ParameterizedBuiltinOp.java index 61a4b8b8f91..abc53aafec4 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,10 +194,12 @@ public Lop constructLops() case TOSTRING: case PARAMSERV: case LIST: - case AUTODIFF:{ + case AUTODIFF: + case DP_GAUSSIAN: + case DP_LAPLACE: { ParameterizedBuiltin pbilop = new ParameterizedBuiltin( inputlops, _op, getDataType(), getValueType(), et); - if( isMultiThreadedOpType() ) + if(isMultiThreadedOpType()) pbilop.setNumThreads(OptimizerUtils.getConstrainedNumThreads(_maxNumThreads)); setOutputDimensions(pbilop); setLineNumbers(pbilop); @@ -688,7 +690,17 @@ else if( _op == ParamBuiltinOp.TRANSFORMAPPLY ) { return new MatrixCharacteristics(dc.getRows(), dc.getCols(), -1, dc.getLength()); } } - + else if(_op == ParamBuiltinOp.DP_GAUSSIAN || _op == ParamBuiltinOp.DP_LAPLACE) { + if(dc.dimsKnown()) { + Hop query = getParameterHop("query"); + String queryVal = (query instanceof LiteralOp) ? ((LiteralOp) query).getStringValue() : null; + if("colMeans".equals(queryVal) || "colSums".equals(queryVal)) + ret = new MatrixCharacteristics(1, dc.getCols(), -1, dc.getCols()); + else if("identity".equals(queryVal)) + ret = new MatrixCharacteristics(dc.getRows(), dc.getCols(), -1, dc.getLength()); + } + } + return ret; } @Override @@ -755,10 +767,10 @@ && getTargetHop().areDimsBelowThreshold() ) { // 2. For paramserv function, always be CP mode so that // the parameter server could have a central instruction // to determine the local or remote workers - if (_op == ParamBuiltinOp.TRANSFORMCOLMAP || _op == ParamBuiltinOp.TRANSFORMMETA - || _op == ParamBuiltinOp.TOSTRING || _op == ParamBuiltinOp.LIST - || _op == ParamBuiltinOp.CDF || _op == ParamBuiltinOp.INVCDF - || _op == ParamBuiltinOp.PARAMSERV) { + if(_op == ParamBuiltinOp.TRANSFORMCOLMAP || _op == ParamBuiltinOp.TRANSFORMMETA || + _op == ParamBuiltinOp.TOSTRING || _op == ParamBuiltinOp.LIST || _op == ParamBuiltinOp.CDF || + _op == ParamBuiltinOp.INVCDF || _op == ParamBuiltinOp.PARAMSERV || _op == ParamBuiltinOp.DP_GAUSSIAN || + _op == ParamBuiltinOp.DP_LAPLACE) { _etype = ExecType.CP; } @@ -940,17 +952,19 @@ public boolean compare( Hop that ) { if( !(that instanceof ParameterizedBuiltinOp) ) return false; - - ParameterizedBuiltinOp that2 = (ParameterizedBuiltinOp)that; - boolean ret = (_op == that2._op - && _paramIndexMap!=null && that2._paramIndexMap!=null - && _paramIndexMap.size() == that2._paramIndexMap.size() - && _outputEmptyBlocks == that2._outputEmptyBlocks - && _outputPermutationMatrix == that2._outputPermutationMatrix ); - if( ret ) - { - for( Entry e : _paramIndexMap.entrySet() ) - { + + // NOTE: dp_gaussian, dp_laplace draw fresh random noise on every call and record a + // privacy-budget charge as a side effect (see DPBuiltinOps.release), so two + // syntactically identical calls must never be merged into one execution. + if(_op == ParamBuiltinOp.DP_GAUSSIAN || _op == ParamBuiltinOp.DP_LAPLACE) + return false; + + ParameterizedBuiltinOp that2 = (ParameterizedBuiltinOp) that; + boolean ret = (_op == that2._op && _paramIndexMap != null && that2._paramIndexMap != null && + _paramIndexMap.size() == that2._paramIndexMap.size() && _outputEmptyBlocks == that2._outputEmptyBlocks && + _outputPermutationMatrix == that2._outputPermutationMatrix); + if(ret) { + for(Entry e : _paramIndexMap.entrySet()) { String key1 = e.getKey(); int pos1 = e.getValue(); int pos2 = that2._paramIndexMap.get(key1); diff --git a/src/main/java/org/apache/sysds/lops/ParameterizedBuiltin.java b/src/main/java/org/apache/sysds/lops/ParameterizedBuiltin.java index 3604121aac8..b71fdf3423b 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_GAUSSIAN: { + sb.append(Opcodes.DP_GAUSSIAN); + sb.append(OPERAND_DELIMITOR); + compileGenericParamMap(sb, _inputParams); + break; + } + case DP_LAPLACE: { + sb.append(Opcodes.DP_LAPLACE); + 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..8c0aaf15ccb 100644 --- a/src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java +++ b/src/main/java/org/apache/sysds/parser/BuiltinFunctionExpression.java @@ -1995,7 +1995,7 @@ else if(this.getOpCode() == Builtins.MAX_POOL || this.getOpCode() == Builtins.AV } break; case LOCAL: - if(OptimizerUtils.ALLOW_SCRIPT_LEVEL_LOCAL_COMMAND){ + if(OptimizerUtils.ALLOW_SCRIPT_LEVEL_LOCAL_COMMAND) { checkNumParameters(1); checkMatrixParam(getFirstExpr()); output.setDataType(DataType.MATRIX); @@ -2003,8 +2003,32 @@ else if(this.getOpCode() == Builtins.MAX_POOL || this.getOpCode() == Builtins.AV output.setBlocksize (id.getBlocksize()); output.setValueType(id.getValueType()); } - else + else { raiseValidateError("Local instruction not allowed in dml script"); + } + break; + case DP_SET_BUDGET: { + checkNumParameters(2); + checkScalarParam(getFirstExpr()); + checkScalarParam(getSecondExpr()); + // resolved entirely at compile time (see DMLTranslator), so both + // arguments must be known before the HOP DAG is built. + if (!isConstant(getFirstExpr()) || !isConstant(getSecondExpr())) + raiseValidateError(getOpCode() + ": 'epsilon' and 'delta' must be compile-time numeric literals", + false, LanguageErrorCodes.INVALID_PARAMETERS); + double dpSetBudgetEpsilon = getDoubleValue(getFirstExpr()); + double dpSetBudgetDelta = getDoubleValue(getSecondExpr()); + if (dpSetBudgetEpsilon <= 0) + raiseValidateError(getOpCode() + ": epsilon must be > 0, got " + dpSetBudgetEpsilon, + false, LanguageErrorCodes.INVALID_PARAMETERS); + if ((dpSetBudgetDelta <= 0) || (dpSetBudgetDelta >= 1)) + raiseValidateError(getOpCode() + ": delta must be in (0,1), got " + dpSetBudgetDelta, + false, LanguageErrorCodes.INVALID_PARAMETERS); + output.setDataType(DataType.SCALAR); + output.setValueType(ValueType.FP64); + output.setDimensions(0, 0); + break; + } case COMPRESS: case DECOMPRESS: if(OptimizerUtils.ALLOW_SCRIPT_LEVEL_COMPRESS_COMMAND){ diff --git a/src/main/java/org/apache/sysds/parser/DMLProgram.java b/src/main/java/org/apache/sysds/parser/DMLProgram.java index 2f69cb7ea0d..53725a38ac7 100644 --- a/src/main/java/org/apache/sysds/parser/DMLProgram.java +++ b/src/main/java/org/apache/sysds/parser/DMLProgram.java @@ -36,7 +36,13 @@ public class DMLProgram private ArrayList _blocks; private Map> _namespaces; private boolean _containsRemoteParfor; - + + /** + * Session-wide differential privacy budget resolved at compile time during HOP construction. + */ + private Double _dpBudgetEpsilon; + private Double _dpBudgetDelta; + public DMLProgram(){ _blocks = new ArrayList<>(); _namespaces = new HashMap<>(); @@ -67,6 +73,23 @@ public void setContainsRemoteParfor(boolean flag) { public boolean containsRemoteParfor() { return _containsRemoteParfor; } + + public void setDPBudget(double epsilon, double delta) { + _dpBudgetEpsilon = epsilon; + _dpBudgetDelta = delta; + } + + public boolean hasDPBudget() { + return _dpBudgetEpsilon != null; + } + + public double getDPBudgetEpsilon() { + return _dpBudgetEpsilon; + } + + public double getDPBudgetDelta() { + return _dpBudgetDelta; + } public static boolean isInternalNamespace(String namespace) { return DEFAULT_NAMESPACE.equals(namespace) diff --git a/src/main/java/org/apache/sysds/parser/DMLTranslator.java b/src/main/java/org/apache/sysds/parser/DMLTranslator.java index 0db739cc901..4b05707c49d 100644 --- a/src/main/java/org/apache/sysds/parser/DMLTranslator.java +++ b/src/main/java/org/apache/sysds/parser/DMLTranslator.java @@ -2015,6 +2015,8 @@ private Hop processParameterizedBuiltinFunctionExpression(ParameterizedBuiltinFu case TRANSFORMMETA: case PARAMSERV: case AUTODIFF: + case DP_GAUSSIAN: + case DP_LAPLACE: currBuiltinOp = new ParameterizedBuiltinOp(target.getName(), target.getDataType(), target.getValueType(), ParamBuiltinOp.valueOf(source.getOpCode().name()), paramHops); break; @@ -2590,6 +2592,19 @@ else if ( sop.equalsIgnoreCase(Opcodes.NOTEQUAL.toString()) ) case DECOMPRESS: currBuiltinOp = new UnaryOp(target.getName(), target.getDataType(), ValueType.FP64, OpOp1.DECOMPRESS, expr); break; + case DP_SET_BUDGET: + // Resolved entirely at compile time; the budget is applied directly to the DMLProgram (reachable later from + // ExecutionContext via Program.getDMLProg()) + if (_dmlProg.hasDPBudget()) + throw new LanguageException(source.getOpCode() + ": dp_set_budget may only be called once per " + + "script (already set to epsilon=" + _dmlProg.getDPBudgetEpsilon() + + ", delta=" + _dmlProg.getDPBudgetDelta() + ")"); + if (!(expr instanceof LiteralOp) || !(expr2 instanceof LiteralOp)) + throw new LanguageException(source.getOpCode() + + ": epsilon and delta must be compile-time numeric literals"); + _dmlProg.setDPBudget(((LiteralOp) expr).getDoubleValue(), ((LiteralOp) expr2).getDoubleValue()); + currBuiltinOp = expr; // echo epsilon back as confirmation + 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/parser/ParameterizedBuiltinFunctionExpression.java b/src/main/java/org/apache/sysds/parser/ParameterizedBuiltinFunctionExpression.java index 314440628e0..8150eb0130f 100644 --- a/src/main/java/org/apache/sysds/parser/ParameterizedBuiltinFunctionExpression.java +++ b/src/main/java/org/apache/sysds/parser/ParameterizedBuiltinFunctionExpression.java @@ -268,6 +268,11 @@ public void validateExpression(HashMap ids, HashMap varParams = getVarParams(); + if(varParams.containsKey(null)) + varParams.put("target", varParams.remove(null)); + + boolean gaussian = getOpCode() == Builtins.DP_GAUSSIAN; + Set valid = gaussian ? + CollectionUtils.asSet("target", "query", "sensitivity", "epsilon", "delta") : + CollectionUtils.asSet("target", "query", "sensitivity", "epsilon"); + checkInvalidParameters(getOpCode(), varParams, valid); + + Expression target = getVarParam("target"); + checkTargetParam(target, conditional); + for(String param : valid) + if(!param.equals("target")) + checkScalarParam(getOpCode().getName(), param, conditional); + + long[] dims = getDPOutputDims(getVarParam("query"), target.getOutput().getDim1(), target.getOutput().getDim2()); + output.setDataType(DataType.MATRIX); + output.setValueType(ValueType.FP64); + output.setDimensions(dims[0], dims[1]); + } + + /** Output dimensions of T %*% X for the given named query, X being n x d. */ + private long[] getDPOutputDims(Expression queryExpr, long n, long d) { + // dp_gaussian/dp_laplace require the "query" parameter to be a compile-time string literal so that the output + // shape + // (and thus the transformation matrix T built at runtime) is known during validation. + if(!(queryExpr instanceof StringIdentifier)) + raiseValidateError(getOpCode() + ": 'query' must be a string literal", false, + LanguageErrorCodes.INVALID_PARAMETERS); + String query = ((StringIdentifier) queryExpr).getValue(); + + switch(query) { + case "colMeans": + case "colSums": + return new long[] {1, d}; + case "identity": + return new long[] {n, d}; + default: + raiseValidateError( + getOpCode() + ": unknown query type '" + query + "' (expected colMeans, colSums, or identity)", + false, LanguageErrorCodes.INVALID_PARAMETERS); + return null; // unreachable + } + } + private void checkScalarParam(String group, String param, boolean conditional) { Expression eparam = getVarParam(param); if( eparam==null ) { 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..5cd9b7bed5a 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 @@ -28,6 +28,7 @@ import org.apache.sysds.conf.ConfigurationManager; import org.apache.sysds.hops.OptimizerUtils; import org.apache.sysds.hops.fedplanner.FTypes.FType; +import org.apache.sysds.parser.DMLProgram; import org.apache.sysds.runtime.DMLRuntimeException; import org.apache.sysds.runtime.controlprogram.LocalVariableMap; import org.apache.sysds.runtime.controlprogram.Program; @@ -62,6 +63,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.DPBudgetAccountant; import org.apache.sysds.utils.Statistics; import java.util.ArrayList; @@ -90,6 +92,8 @@ public class ExecutionContext { protected SEALClient _seal_client; + private DPBudgetAccountant _dpBudgetAccountant = null; + //parfor temporary functions (created by eval) protected Set _fnNames; @@ -144,6 +148,20 @@ public void setLineage(Lineage lineage) { _lineage = lineage; } + /** + * Returns the session-scoped {@link DPBudgetAccountant}, lazily initialised on first use. If the DML script called + * dp_set_budget(epsilon, delta) with compile-time literal arguments, that value (resolved onto the DMLProgram + * during HOP construction - see DMLTranslator's DP_SET_BUDGET case) is used instead of the hardcoded defaults. + */ + public DPBudgetAccountant getDPBudgetAccountant() { + if(_dpBudgetAccountant == null) { + DMLProgram dmlProg = (_prog != null) ? _prog.getDMLProg() : null; + _dpBudgetAccountant = (dmlProg != null && dmlProg.hasDPBudget()) ? new DPBudgetAccountant( + dmlProg.getDPBudgetEpsilon(), dmlProg.getDPBudgetDelta()) : new DPBudgetAccountant(); + } + return _dpBudgetAccountant; + } + 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..599f1dfafed 100644 --- a/src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java +++ b/src/main/java/org/apache/sysds/runtime/instructions/CPInstructionParser.java @@ -226,7 +226,7 @@ public static CPInstruction parseSingleInstruction ( InstructionType cptype, Str case EINSUM: return EinsumCPInstruction.parseInstruction(str); - + default: throw new DMLRuntimeException("Invalid CP Instruction Type: " + 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 new file mode 100644 index 00000000000..ecadb1dc0ee --- /dev/null +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinCPInstruction.java @@ -0,0 +1,83 @@ +/* + * 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 java.util.LinkedHashMap; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.sysds.common.Opcodes; +import org.apache.sysds.runtime.DMLRuntimeException; +import org.apache.sysds.runtime.controlprogram.context.ExecutionContext; +import org.apache.sysds.runtime.instructions.InstructionUtils; +import org.apache.sysds.runtime.lineage.LineageItem; +import org.apache.sysds.runtime.matrix.data.MatrixBlock; +import org.apache.sysds.runtime.matrix.operators.Operator; + +/** + * CP instruction for the dp_laplace/dp_gaussian opcodes. Subclasses {@link ParameterizedBuiltinCPInstruction}: + * parse-time validation, execution, and lineage handling are owned here rather than as inline opcode branches in the + * shared class, since the DP release charges a privacy budget and draws fresh randomness on every call - side effects + * that don't fit the shared class's other (pure, replayable) opcodes. + * + * The DP math itself (transform construction, noise generation, sigma calibration) lives in {@link DPBuiltinOps}, which + * this class calls into. + * + * NOTE: Differential privacy builtin functions are still experimental. + */ +public class DPBuiltinCPInstruction extends ParameterizedBuiltinCPInstruction { + + public DPBuiltinCPInstruction(Operator op, LinkedHashMap paramsMap, CPOperand out, String opcode, + String istr) { + super(op, paramsMap, out, opcode, istr); + } + + static DPBuiltinCPInstruction parse(String[] parts, LinkedHashMap paramsMap, CPOperand out, + String opcode, String istr) { + LOG.warn("Differential privacy features are still experimental."); + InstructionUtils.checkNumFields(parts, 5, 6); // laplace=5, gaussian=6 + if(!paramsMap.containsKey("query")) + throw new DMLRuntimeException(opcode + ": missing 'query'"); + if(!paramsMap.containsKey("sensitivity")) + throw new DMLRuntimeException(opcode + ": missing 'sensitivity'"); + if(!paramsMap.containsKey("epsilon")) + throw new DMLRuntimeException(opcode + ": missing 'epsilon'"); + if(opcode.equalsIgnoreCase(Opcodes.DP_GAUSSIAN.toString()) && !paramsMap.containsKey("delta")) + throw new DMLRuntimeException(opcode + ": missing 'delta'"); + return new DPBuiltinCPInstruction(null, paramsMap, out, opcode, istr); + } + + @Override + public void processInstruction(ExecutionContext ec) { + String opcode = getOpcode(); + String target = params.get("target"); + MatrixBlock X = ec.getMatrixInput(target); + MatrixBlock outBlock = DPBuiltinOps.release(X, opcode, params, ec.getDPBudgetAccountant()); + ec.releaseMatrixInput(target); + ec.setMatrixOutput(output.getName(), outBlock); + } + + @Override + public Pair getLineageItem(ExecutionContext ec) { + // dp_laplace/dp_gaussian draw fresh randomness and charge a privacy-budget side effect on every + // call, so a cached lineage-based reuse of a prior release would be unsound. + throw new DMLRuntimeException(getOpcode() + ": lineage tracing not supported (draws fresh randomness " + + "and charges a privacy budget on every call)"); + } +} diff --git a/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinOps.java b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinOps.java new file mode 100755 index 00000000000..72fd71741d7 --- /dev/null +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/DPBuiltinOps.java @@ -0,0 +1,257 @@ +/* + * 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.commons.math3.distribution.LaplaceDistribution; +import org.apache.commons.math3.distribution.NormalDistribution; +import org.apache.commons.math3.random.Well1024a; + +import org.apache.sysds.common.Opcodes; +import org.apache.sysds.runtime.DMLRuntimeException; +import org.apache.sysds.runtime.functionobjects.Multiply; +import org.apache.sysds.runtime.matrix.data.LibMatrixMult; +import org.apache.sysds.runtime.matrix.data.LibMatrixReorg; +import org.apache.sysds.runtime.matrix.data.MatrixBlock; +import org.apache.sysds.runtime.matrix.operators.RightScalarOperator; +import org.apache.sysds.runtime.privacy.dp.DPBudgetAccountant; + +import java.util.Map; + +/** + * Differential-privacy release of a linear query over the original matrix, invoked from + * {@link ParameterizedBuiltinCPInstruction} for the dp_laplace/dp_gaussian opcodes. + * + * DML syntax (raw-matrix form): + * result = dp_laplace(X, query="colMeans", sensitivity=1.0, epsilon=0.5) + * result = dp_gaussian(X, query="colMeans", sensitivity=1.0, epsilon=0.5, delta=1e-5) + */ +public class DPBuiltinOps { + + private static final NormalDistribution normal = new NormalDistribution(); + + private DPBuiltinOps() { + // static utility class + } + + /** + * Executes the DP release. + * + * @param X the original input matrix (caller pins/releases it around this call) + * @param opcode dp_laplace or dp_gaussian + * @param params named parameters: "query", "sensitivity", "epsilon", "delta" (Gaussian only) + * @param accountant the session-scoped privacy budget accountant to charge this release against + * @return the noisy release T %*% X + N + */ + static MatrixBlock release(MatrixBlock X, String opcode, Map params, + DPBudgetAccountant accountant) { + + // ── 1. Parse DP parameters ────────────────────────────────────────── + double epsilon = parsePositiveDouble(opcode, params, "epsilon"); + double delta = opcode.equalsIgnoreCase(Opcodes.DP_GAUSSIAN.toString()) ? + parsePositiveDouble(opcode, params, "delta") : 0.0; + String query = params.get("query"); + + // ── 2. Build the transformation matrix T (k x n) ──────────────────── + MatrixBlock T = buildTransform(query, X.getNumRows()); + + // ── 3. Determine sensitivity (caller-supplied constant) ───────────── + double sensitivity = sensitivityOf(opcode, params, T); + + // ── 4. Generate noise shaped like the release T %*% X (k x d) ─────── + MatrixBlock noiseBlock = generateNoise(opcode, T.getNumRows(), X.getNumColumns(), sensitivity, epsilon, delta); + + // ── 5. Fuse T %*% X + noise into a single matrix multiply ─────────── + MatrixBlock Ik = identity(T.getNumRows()); + MatrixBlock Tp = T.append(Ik, null, true); // [T | I_k] + MatrixBlock Xp = X.append(noiseBlock, null, false); // [X ; noise] + MatrixBlock outBlock = LibMatrixMult.matrixMult(Tp, Xp); + + // ── 6. Record release and enforce budget ──────────────────────────── + accountant.compose(epsilon, delta, sensitivity); // throws on exhaustion + + return outBlock; + } + + /** + * Builds the k x n transformation matrix T for the given named query, to be left-multiplied against + * the n x d input X as T %*% X. + */ + private static MatrixBlock buildTransform(String query, int n) { + switch(query) { + case "colMeans": { + MatrixBlock T = new MatrixBlock(1, n, false); + T.allocateDenseBlock(); + double v = 1.0 / n; + for(int c = 0; c < n; c++) + T.set(0, c, v); + T.recomputeNonZeros(); + return T; + } + case "colSums": { + MatrixBlock T = new MatrixBlock(1, n, false); + T.allocateDenseBlock(); + for(int c = 0; c < n; c++) + T.set(0, c, 1.0); + T.recomputeNonZeros(); + return T; + } + case "identity": + return identity(n); + default: + throw new DMLRuntimeException("dp_laplace/dp_gaussian: unknown query type '" + query + + "' (expected colMeans, colSums, or identity)"); + } + } + + /** + * Builds a k x k identity matrix, sparsely, by reusing the existing {@link LibMatrixReorg#diag} reorg operator (the + * same runtime path DML's diag() builtin uses to expand a vector into a diagonal matrix). Keeps memory O(k) rather + * than O(k^2), which matters for the query="identity" case where k equals the number of rows of X. + */ + private static MatrixBlock identity(int k) { + MatrixBlock ones = new MatrixBlock(k, 1, false); + ones.allocateDenseBlock(); + for(int i = 0; i < k; i++) + ones.set(i, 0, 1.0); + ones.recomputeNonZeros(); + return LibMatrixReorg.diag(ones, new MatrixBlock(k, k, true)); + } + + /** + * Returns the sensitivity of the release T %*% X to a single-record change, in the norm required by the mechanism + * actually invoked: L1 for dp_laplace, L2 for dp_gaussian. The two only coincide when the release is scalar. + * + * @param T the transformation matrix (unused for now; kept as the seam for a future sensitivity-derivation pass) + * @return caller-supplied sensitivity constant, expected to already be in the L1 norm (Laplace) or L2 norm + * (Gaussian) + */ + private static double sensitivityOf(String opcode, Map params, MatrixBlock T) { + return parsePositiveDouble(opcode, params, "sensitivity"); + } + + /** + * Generates a rows x cols noise {@link MatrixBlock} - matching the shape of the release T %*% X - filled with + * samples from the mechanism-appropriate distribution calibrated to (sensitivity, epsilon, delta). + */ + private static MatrixBlock generateNoise(String opcode, int rows, int cols, double sensitivity, double epsilon, + double delta) { + + MatrixBlock noise; + + if(opcode.equalsIgnoreCase(Opcodes.DP_LAPLACE.toString())) { + // Laplace mechanism + // For a given epsilon, noise is drawn from the Laplace distribution at scale b = sensitivity / epsilon + noise = fillLaplaceNoise(rows, cols, sensitivity / epsilon); + } + else { + // Gaussian mechanism + // For a given epsilon and delta, noise is drawn from the Gaussian distribution N(0, sigma^2) + double sigma = computeGaussianSigma(sensitivity, epsilon, delta); + noise = fillGaussianNoise(rows, cols, sigma); + } + + return noise; + } + + /** + * Compute the optimal sigma for the Analytic Gaussian Mechanism (Balle, Wang 2018). Returns the smallest sigma + * such that the Gaussian mechanism is (epsilon, delta)-DP. + * + * @param sensitivity L2 sensitivity + * @param epsilon target epsilon + * @param delta target delta + * @return optimal sigma + */ + public static double computeGaussianSigma(double sensitivity, double epsilon, double delta) { + + // Upper bound: classical Gaussian mechanism (loose but safe) + double sigmaHigh = (sensitivity * Math.sqrt(2 * Math.log(1.25 / delta))) / epsilon; + double sigmaLow = 1e-12; + + for(int i = 0; i < 100; i++) { + double sigmaMid = 0.5 * (sigmaLow + sigmaHigh); + + if(deltaUpperBound(sigmaMid, epsilon, delta, sensitivity) > delta) { + sigmaLow = sigmaMid; + } + else { + sigmaHigh = sigmaMid; + } + } + + return sigmaHigh; + } + + /** + * Analytic Gaussian DP inequality from Balle & Wang (2018). + */ + private static double deltaUpperBound(double sigma, double epsilon, double delta, double sensitivity) { + double c = sensitivity / (2 * sigma); + + double term1 = normal.cumulativeProbability(c - epsilon * sigma / sensitivity); + double term2 = Math.exp(epsilon) * normal.cumulativeProbability(-c - epsilon * sigma / sensitivity); + + return term1 - term2; + } + + /** + * Fills block with i.i.d. Laplace(0, scale) samples. + */ + private static MatrixBlock fillLaplaceNoise(int rows, int cols, double scale) { + MatrixBlock noise = new MatrixBlock(rows, cols, false); + noise.allocateDenseBlock(); + LaplaceDistribution laplace = new LaplaceDistribution(new Well1024a(), 0, scale); + for(int r = 0; r < rows; r++) { + for(int c = 0; c < cols; c++) { + noise.set(r, c, laplace.sample()); + } + } + return noise; + } + + /** + * Generates a rows x cols block of i.i.d. N(0, sigma^2) samples. + */ + private static MatrixBlock fillGaussianNoise(int rows, int cols, double sigma) { + MatrixBlock std = MatrixBlock.randOperations(rows, cols, 1.0, 0, 1, "normal", -1); + return std.scalarOperations(new RightScalarOperator(Multiply.getMultiplyFnObject(), sigma), null); + } + + /** + * Parses a parameter value as a positive double. + * + * @throws DMLRuntimeException if the key is absent, unparseable, or non-positive + */ + private static double parsePositiveDouble(String opcode, Map params, String key) { + String raw = params.get(key); + if(raw == null) + throw new DMLRuntimeException(opcode + ": parameter '" + key + "' is missing"); + double v; + try { + v = Double.parseDouble(raw); + } + catch(NumberFormatException e) { + throw new DMLRuntimeException(opcode + ": parameter '" + key + "' is not a valid number: " + raw); + } + if(v <= 0.0) + throw new DMLRuntimeException(opcode + ": parameter '" + key + "' must be strictly positive, got " + v); + return v; + } +} diff --git a/src/main/java/org/apache/sysds/runtime/instructions/cp/ParameterizedBuiltinCPInstruction.java b/src/main/java/org/apache/sysds/runtime/instructions/cp/ParameterizedBuiltinCPInstruction.java index e53958ac4b8..29a15a3176d 100644 --- a/src/main/java/org/apache/sysds/runtime/instructions/cp/ParameterizedBuiltinCPInstruction.java +++ b/src/main/java/org/apache/sysds/runtime/instructions/cp/ParameterizedBuiltinCPInstruction.java @@ -69,7 +69,7 @@ import org.apache.sysds.utils.stats.InfrastructureAnalyzer; public class ParameterizedBuiltinCPInstruction extends ComputationCPInstruction { - private static final Log LOG = LogFactory.getLog(ParameterizedBuiltinCPInstruction.class.getName()); + protected static final Log LOG = LogFactory.getLog(ParameterizedBuiltinCPInstruction.class.getName()); private static final int TOSTRING_MAXROWS = 100; private static final int TOSTRING_MAXCOLS = 100; private static final int TOSTRING_DECIMAL = 3; @@ -161,6 +161,10 @@ else if(opcode.equals(Opcodes.TRANSFORMAPPLY.toString()) || opcode.equals(Opcode else if(Opcodes.PARAMSERV.toString().equals(opcode)) { return new ParamservBuiltinCPInstruction(null, paramsMap, out, opcode, str); } + else if(opcode.equalsIgnoreCase(Opcodes.DP_GAUSSIAN.toString()) || + opcode.equalsIgnoreCase(Opcodes.DP_LAPLACE.toString())) { + return DPBuiltinCPInstruction.parse(parts, paramsMap, out, opcode, str); + } else { throw new DMLRuntimeException("Unknown opcode (" + opcode + ") for ParameterizedBuiltin Instruction."); } @@ -554,7 +558,7 @@ else if (opcode.equalsIgnoreCase(Opcodes.NVLIST.toString()) || opcode.equalsIgno CPOperand[] listOperands = names.stream().map(n -> ec.containsVariable(params.get(n)) ? new CPOperand(n, ec.getVariable(params.get(n))) : getStringLiteral(n)).toArray(CPOperand[]::new); - return Pair.of(output.getName(), + return Pair.of(output.getName(), new LineageItem(getOpcode(), LineageItemUtils.getLineage(ec, listOperands))); } else { 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..e8a2aecae59 --- /dev/null +++ b/src/main/java/org/apache/sysds/runtime/privacy/dp/DPBudgetAccountant.java @@ -0,0 +1,162 @@ +/* + * 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; +import org.apache.sysds.runtime.instructions.cp.DPBuiltinOps; + +/** + * Session-scoped differential privacy budget accountant. + * One instance is created per ExecutionContext. Each call to {@link #compose} records one release and checks whether + * the cumulative privacy cost has exceeded the user-specified budget. + * + * The mechanism type (Laplace vs Gaussian) is inferred from the delta argument passed to {@link #compose}: + * - Laplace (delta == 0): pure epsilon-DP. + * - Gaussian (delta > 0): (epsilon, delta)-DP via Renyi DP composition. + * When both mechanisms are used in the same script the total cost is: + * epsilon_total = epsilon_Laplace_sum + epsilon_Gaussian_RDP + * This follows from basic composition of a pure-DP mechanism with an approximate-DP mechanism, which is additive in epsilon. + * + * Not thread-safe. A single DML script executes instructions sequentially on one thread, so no synchronisation is + * needed. + */ +public class DPBudgetAccountant { + private static final double DEFAULT_EPSILON_BUDGET = 1.0; + private static final double DEFAULT_DELTA = 1e-5; + /** + * Discrete set of Renyi orders alpha. 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}; + /** Accumulated Renyi divergence at each order (Gaussian releases only). */ + private final double[] _rdpSum = new double[ORDERS.length]; + + /** + * Running sum of pure epsilon from Laplace releases. Each Laplace release adds its + * epsilon here; the total is added directly in {@link #totalEpsilonSpent()}. + */ + private double _pureEpsilonSum = 0.0; + /** Number of releases recorded so far (for error messages). */ + private int _releaseCount = 0; + /** Whether at least one Gaussian release has been recorded. */ + private boolean _hasGaussianReleases = false; + + /** Total privacy budget (epsilon) for the script execution. */ + private final double _epsilonBudget; + /** delta used for the Gaussian RDP-to-(epsilon,delta) conversion. */ + private final double _delta; + + /** + * Creates an accountant with the given global budget. + * + * @param epsilonBudget total epsilon budget for the script execution (must be > 0) + * @param delta delta used for the Gaussian RDP-to-(epsilon,delta) 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; + } + + public DPBudgetAccountant(double epsilonBudget) { + this(epsilonBudget, DEFAULT_DELTA); + } + + public DPBudgetAccountant() { + this(DEFAULT_EPSILON_BUDGET, DEFAULT_DELTA); + } + + /** + * 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. + * + * @param epsilon per-release epsilon parameter (must be >= 0) + * @param delta per-release delta parameter (0 for Laplace, >= 0 for Gaussian) + * @param sensitivity sensitivity of the released quantity (must be > 0). The norm depends on the mechanism selected + * by delta: callers must supply the L1 sensitivity when delta == 0 (Laplace), and the L2 + * sensitivity when 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 epsilon after this release would exceed the budget + */ + public void compose(double epsilon, double delta, double sensitivity) { + _releaseCount++; + + if(delta == 0.0) { + // Laplace: pure epsilon-DP, basic composition; cost is exactly epsilon. + _pureEpsilonSum += epsilon; + } + else { + // Gaussian: accumulate Renyi divergence at each order, then convert. + _hasGaussianReleases = true; + for(int i = 0; i < ORDERS.length; i++) { + double sigma = DPBuiltinOps.computeGaussianSigma(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 epsilon %.6f exceeds budget epsilon = %.6f (delta = %.2e). " + + "Reduce the number of releases or widen the budget.", + _releaseCount, spentEpsilon, _epsilonBudget, _delta)); + } + } + + /** + * Returns the current total privacy cost as an epsilon value. + */ + public double totalEpsilonSpent() { + if(!_hasGaussianReleases) + return _pureEpsilonSum; + + 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 / _delta) / (alpha - 1.0); + if(eps < gaussianEps) + gaussianEps = eps; + } + return _pureEpsilonSum + Math.max(gaussianEps, 0.0); + } + + /** Returns the remaining epsilon 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; + } + + /** + * Renyi divergence of order alpha for the Gaussian mechanism (Mironov 2017, Proposition 3, example 2): + */ + private static double rdpGaussian(double alpha, double sensitivity, double sigma) { + return alpha * (sensitivity * sensitivity) / (2.0 * sigma * sigma); + } +} diff --git a/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinOpsTest.java b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinOpsTest.java new file mode 100755 index 00000000000..2da8cfd4c94 --- /dev/null +++ b/src/test/java/org/apache/sysds/test/component/cp/DPBuiltinOpsTest.java @@ -0,0 +1,267 @@ +/* + * 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.component.cp; + +import org.apache.sysds.runtime.DMLRuntimeException; +import org.apache.sysds.runtime.instructions.cp.DPBuiltinOps; +import org.apache.sysds.runtime.matrix.data.MatrixBlock; +import org.apache.sysds.runtime.privacy.dp.DPBudgetAccountant; + +import java.lang.reflect.Method; + +import org.junit.Test; +import org.junit.Assert; + +/** + * Tests for DPBuiltinOps, DPBuiltinCPInstruction (dp_laplace/dp_gaussian), and DPBudgetAccountant. + */ +public class DPBuiltinOpsTest { + + private static final double ASSERT_TOLERANCE = 1e-9; + + // ======================================================================= + // 1. DPBudgetAccountant unit tests + // ======================================================================= + + @Test + public void testAccountantInitialisesAtZeroCostThenAcceptsSingleReleases() { + DPBudgetAccountant acc = new DPBudgetAccountant(1.0, 1e-5); + // No releases yet: remainingBudget() should be positive. + Assert.assertTrue("No releases should leave budget intact", acc.remainingBudget() > 0); + Assert.assertEquals(0, acc.releaseCount()); + + // epsilon=0.5, budget=1.0: one Laplace release should stay within the budget. + acc.compose(0.5, 0.0, 1.0); // Laplace + Assert.assertEquals(1, acc.releaseCount()); + Assert.assertTrue("Single release within budget", acc.totalEpsilonSpent() <= 1.0); + + // Likewise, a single Gaussian release on a fresh accountant should stay within budget. + DPBudgetAccountant gaussianAcc = new DPBudgetAccountant(1.0, 1e-5); + gaussianAcc.compose(0.5, 1e-5, 1.0); // Gaussian + Assert.assertEquals(1, gaussianAcc.releaseCount()); + Assert.assertTrue("Single Gaussian release within budget", gaussianAcc.totalEpsilonSpent() <= 1.0); + } + + @Test(expected = DMLRuntimeException.class) + public void testBudgetExhaustionThrows() { + // Budget is set to only 0.5, but we try to make 10 releases with epsilon of 0.1 each. + // After enough releases the budget must be exceeded. + DPBudgetAccountant acc = new DPBudgetAccountant(0.5, 1e-5); + double prevEpsilonSpent = acc.totalEpsilonSpent(); + double prevRemainingBudget = acc.remainingBudget(); + for(int i = 0; i < 10; i++) { + acc.compose(0.1, 0.0, 1.0); // will throw before the 10th + double currentEpsilonSpent = acc.totalEpsilonSpent(); + double currentRemainingBudget = acc.remainingBudget(); + int releaseCount = acc.releaseCount(); + Assert.assertTrue("Epsilon spent must increase with each release", currentEpsilonSpent > prevEpsilonSpent); + Assert.assertTrue("Remaining budget must decrease", currentRemainingBudget < prevRemainingBudget); + Assert.assertEquals("Release count must match", i + 1, releaseCount); + prevEpsilonSpent = currentEpsilonSpent; + prevRemainingBudget = currentRemainingBudget; + } + } + + @Test + public void testGaussianTighterThanLaplaceForSameEpsilon() { + // Gaussian uses RDP composition which is tighter than Laplace with basic composition. + double eps = 0.5; + double delta = 1e-5; + + 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); + laplace.compose(eps, 0.0, 1.0); + } + + Assert.assertTrue("Gaussian RDP bound should be tighter than Laplace bound after 5 releases", + gaussian.totalEpsilonSpent() <= laplace.totalEpsilonSpent() + 1e-6); + } + + @Test + public void testHigherEpsilonCostMoreForLaplace() { + // Increasing the epsilon results in higher budget costs. + 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 + + Assert.assertTrue("Higher epsilon costs more budget (Laplace basic composition)", + acc1.totalEpsilonSpent() < acc2.totalEpsilonSpent()); + } + + // --- Constructor error paths ------------------------------------ + + @Test + public void testConstructorRejectsInvalidBudgetParameters() { + assertConstructorRejects(0.0, 1e-5); // zero epsilon budget + assertConstructorRejects(-0.5, 1e-5); // negative epsilon budget + assertConstructorRejects(1.0, 0.0); // delta = 0 + assertConstructorRejects(1.0, 1.0); // delta = 1 + } + + private static void assertConstructorRejects(double epsilonBudget, double delta) { + try { + new DPBudgetAccountant(epsilonBudget, delta); + Assert.fail("Expected DMLRuntimeException for epsilonBudget=" + epsilonBudget + ", delta=" + delta); + } + catch(DMLRuntimeException e) { + // expected + } + } + + @Test(expected = DMLRuntimeException.class) + public void testGaussianBudgetExhaustionThrows() { + // Budget = 0.5. Each Gaussian release is composed at epsilon=0.3, delta=1e-5, + // so the RDP-converted cost accumulates well past the budget within 20 releases. + DPBudgetAccountant acc = new DPBudgetAccountant(0.5, 1e-5); + for(int i = 0; i < 20; i++) { + acc.compose(0.3, 1e-5, 1.0); + } + } + + @Test + public void testMixedCompositionExceedsEitherAlone() { + // Compose one Laplace and one Gaussian release. The total cost must + // exceed what either mechanism contributes alone. + 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 + + lapOnly.compose(0.5, 0.0, 1.0); + gauOnly.compose(0.5, 1e-5, 1.0); + + Assert.assertTrue("Mixed cost must exceed Laplace-only cost", + mixed.totalEpsilonSpent() > lapOnly.totalEpsilonSpent()); + Assert.assertTrue("Mixed cost must exceed Gaussian-only cost", + mixed.totalEpsilonSpent() > gauOnly.totalEpsilonSpent()); + } + + @Test + public void testGaussianSensitivityCancelsInRDP() { + // Two accountants with the same (epsilon,delta) but different sensitivity must + // report identical totalEpsilonSpent(). + DPBudgetAccountant acc1 = new DPBudgetAccountant(100.0, 1e-5); + DPBudgetAccountant acc2 = new DPBudgetAccountant(100.0, 1e-5); + acc1.compose(0.5, 1e-5, 1.0); + acc2.compose(0.5, 1e-5, 100.0); + Assert.assertEquals("Gaussian RDP cost must be independent of sensitivity when (epsilon,delta) are fixed", + acc1.totalEpsilonSpent(), acc2.totalEpsilonSpent(), ASSERT_TOLERANCE); + } + + @Test + public void testGaussianLargerEpsilonCostsMoreBudget() { + // Increasing epsilon results in higher budget costs for Gaussian. + 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); + Assert.assertTrue("Larger epsilon per Gaussian release must cost more budget", + highEps.totalEpsilonSpent() > lowEps.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. + + @Test + public void testLaplaceNoiseDistribution() throws ReflectiveOperationException { + // For 10000 samples the empirical mean should be within 5*sigma/sqrt(n) of 0, + // and Var[Laplace(0, b)] = 2b^2 should match within 10% relative error. + int n = 10_000; + double scale = 1.5; + double[] samples = sampleLaplace(n, scale); + + double mean = mean(samples); + double theoreticalStdErr = scale * Math.sqrt(2.0) / Math.sqrt(n); + Assert.assertTrue("Laplace mean should be near 0", Math.abs(mean) < 5 * theoreticalStdErr); + + double variance = variance(samples); + double expected = 2.0 * scale * scale; + Assert.assertEquals("Laplace variance", expected, variance, 0.1 * expected); + } + + @Test + public void testGaussianNoiseDistribution() throws ReflectiveOperationException { + // Same idea as testLaplaceNoiseDistribution: check mean and variance from one sample set. + int n = 10_000; + double sigma = 2.0; + double[] samples = sampleGaussian(n, sigma); + + double mean = mean(samples); + double theoreticalStdErr = sigma / Math.sqrt(n); + Assert.assertTrue("Gaussian mean should be near 0", Math.abs(mean) < 5 * theoreticalStdErr); + + double variance = variance(samples); + double expected = sigma * sigma; + Assert.assertEquals("Gaussian variance", expected, variance, 0.1 * expected); + } + + // ----------------------------------------------------------------------- + // Helpers for noise distribution tests + // ----------------------------------------------------------------------- + + /** Sample n Laplace(0, scale) values via the fillLaplaceNoise method. */ + private static double[] sampleLaplace(int n, double scale) throws ReflectiveOperationException { + Method m = DPBuiltinOps.class.getDeclaredMethod("fillLaplaceNoise", int.class, int.class, double.class); + m.setAccessible(true); + MatrixBlock block = (MatrixBlock) m.invoke(null, n, 1, scale); + + double[] out = new double[n]; + for(int i = 0; i < n; i++) + out[i] = block.get(i, 0); + return out; + } + + /** Sample n N(0, sigma^2) values via the production fillGaussianNoise method. */ + private static double[] sampleGaussian(int n, double sigma) throws ReflectiveOperationException { + Method m = DPBuiltinOps.class.getDeclaredMethod("fillGaussianNoise", int.class, int.class, double.class); + m.setAccessible(true); + MatrixBlock block = (MatrixBlock) m.invoke(null, n, 1, sigma); + + double[] out = new double[n]; + for(int i = 0; i < n; i++) + out[i] = block.get(i, 0); + 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); + } +} diff --git a/src/test/java/org/apache/sysds/test/functions/privacy/DPBuiltinDMLTest.java b/src/test/java/org/apache/sysds/test/functions/privacy/DPBuiltinDMLTest.java new file mode 100644 index 00000000000..965b6917422 --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/privacy/DPBuiltinDMLTest.java @@ -0,0 +1,201 @@ +/* + * 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; + + +import java.util.HashMap; + +import org.apache.sysds.parser.LanguageException; +import org.apache.sysds.parser.ParseException; +import org.apache.sysds.runtime.DMLRuntimeException; +import org.apache.sysds.runtime.matrix.data.MatrixValue.CellIndex; +import org.apache.sysds.test.AutomatedTestBase; +import org.apache.sysds.test.TestConfiguration; +import org.junit.Assert; +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 double[][] data; + + @Override + public void setUp() { + addTestConfiguration("DPLaplace", new TestConfiguration(TEST_CLASS, "DPLaplaceTest")); + addTestConfiguration("DPGaussian", new TestConfiguration(TEST_CLASS, "DPGaussianTest")); + addTestConfiguration("DPSetBudget", new TestConfiguration(TEST_CLASS, "DPSetBudgetTest")); + addTestConfiguration("DPSetBudgetTwice", new TestConfiguration(TEST_CLASS, "DPSetBudgetTwiceTest")); + addTestConfiguration("DPSetBudgetNonLiteral", new TestConfiguration(TEST_CLASS, "DPSetBudgetNonLiteralTest")); + data = this.getRandomMatrix(ROWS, COLS, 0, 1, 1.0, 42); + } + + @Test + public void testLaplaceOutputDiffersFromCleanMean() { + runColMeansDPTest("DPLaplace", "0.5"); + } + + @Test + public void testGaussianOutputDiffersFromCleanMean() { + runColMeansDPTest("DPGaussian", "0.5"); + } + + @Test + public void testLaplaceColSums() { + HashMap result = runAndGetResult("DPLaplace", "colSums", + "0.5", data); + assertShape(result, 1, COLS); + double maxDiff = maxAbsDiffFromClean(data, result, DPBuiltinDMLTest::colSum); + Assert.assertTrue("Result should differ from the clean column sums", maxDiff > 0); + } + + @Test + public void testGaussianIdentity() { + HashMap result = runAndGetResult("DPGaussian", "identity", "0.5", data); + assertShape(result, ROWS, COLS); + double maxCellDiff = 0; + for(int r = 0; r < ROWS; r++) { + for(int c = 0; c < COLS; c++) { + double noisy = result.get(new CellIndex(r + 1, c + 1)); + maxCellDiff = Math.max(maxCellDiff, Math.abs(noisy - data[r][c])); + } + } + Assert.assertTrue("Result should differ from the clean matrix", maxCellDiff > 0); + } + + @Test + public void testHighEpsilonIsCloserToTruth() { + // Higher epsilon => less noise => result closer to the true mean. + // NOTE: the DPBudgetAccountant caps total spend at the default budget + // (epsilon = 1.0) regardless of the per-release epsilon requested, so epsilon values + // here must stay well under that cap or the release is rejected. + double noisyLow = runAndGetMaxAbsColMeansDiffFromClean(data, "DPGaussian", "0.1"); + double noisyHigh = runAndGetMaxAbsColMeansDiffFromClean(data, "DPGaussian", "0.5"); + Assert.assertTrue("epsilon=0.5 should give less noise than epsilon=0.1", noisyHigh < noisyLow); + } + + @Test + public void testSetBudgetLiteralAllowsExceedingDefaultBudget() { + // Default budget is 1; a release at epsilon=1.5 will be rejected. + HashMap result = runAndGetResult("DPSetBudget", + "3.0", "1.5", data); + assertShape(result, 1, COLS); + } + + @Test + public void testSetBudgetNarrowBudgetStillEnforced() { + // Moderate epsilon of 0.8 exceeds the budget of 0.5 and throws an exception. + runExpectingException("DPSetBudget", "0.5", "0.8", data, + DMLRuntimeException.class); + } + + @Test + public void testSetBudgetCalledTwiceFailsAtCompileTime() { + // Setting the budget twice already throws an exception while parsing. + runExpectingException("DPSetBudgetTwice", "3.0", "0.5", data, ParseException.class); + } + + @Test + public void testSetBudgetRejectsNonLiteralArgs() { + runExpectingException("DPSetBudgetNonLiteral", "unused", "0.5", data, LanguageException.class); + } + + private void runColMeansDPTest(String testName, String epsilonStr) { + HashMap result = runAndGetResult(testName, "colMeans", epsilonStr, data); + assertShape(result, 1, 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, DPBuiltinDMLTest::colMean); + Assert.assertTrue("Result should differ from the clean mean", maxDiff > 0); + } + + private double runAndGetMaxAbsColMeansDiffFromClean(double[][] data, String testName, + String epsilonStr) { + HashMap result = runAndGetResult(testName, "colMeans", epsilonStr, data); + return maxAbsDiffFromClean(data, result, DPBuiltinDMLTest::colMean); + } + + private static void assertShape(HashMap result, int expectedRows, int expectedCols) { + int maxRow = 0, maxCol = 0; + for(CellIndex ci : result.keySet()) { + maxRow = Math.max(maxRow, ci.row); + maxCol = Math.max(maxCol, ci.column); + } + Assert.assertEquals("Result should have " + expectedRows + " row(s)", expectedRows, maxRow); + Assert.assertEquals("Result should have " + expectedCols + " column(s)", expectedCols, maxCol); + } + + @FunctionalInterface + private interface CleanColumnFn { + double apply(double[][] data, int col); + } + + /** Computes the maximum difference between the row-vector releases and the clean vector. */ + private static double maxAbsDiffFromClean(double[][] data, HashMap result, + CleanColumnFn cleanFn) { + double maxDiff = 0; + for(int c = 0; c < COLS; c++) { + double clean = cleanFn.apply(data, c); + double noisy = result.get(new CellIndex(1, c + 1)); + maxDiff = Math.max(maxDiff, Math.abs(noisy - clean)); + } + return maxDiff; + } + + private static double colMean(double[][] data, int c) { + double sum = 0; + for(int r = 0; r < ROWS; r++) + sum += data[r][c]; + return sum / ROWS; + } + + private static double colSum(double[][] data, int c) { + double sum = 0; + for(int r = 0; r < ROWS; r++) + sum += data[r][c]; + return sum; + } + + private HashMap runAndGetResult(String testName, String queryOrBudgetParam, String epsilonStr, + double[][] data) { + prepareScript(testName, queryOrBudgetParam, epsilonStr, data); + runTest(true, false, null, -1); + return readDMLMatrixFromOutputDir("result"); + } + + private void runExpectingException(String testName, String queryOrBudgetParam, String epsilonStr, double[][] data, + Class expectedException) { + prepareScript(testName, queryOrBudgetParam, epsilonStr, data); + runTest(true, true, expectedException, -1); + } + + private void prepareScript(String testName, String queryOrBudgetParam, String epsilonStr, double[][] data) { + getAndLoadTestConfiguration(testName); + writeInputMatrixWithMTD("X", data, false); + + String HOME = SCRIPT_DIR + TEST_DIR; + fullDMLScriptName = HOME + testName + "Test.dml"; + + programArgs = new String[] {"-args", input("X"), queryOrBudgetParam, epsilonStr, output("result")}; + } +} diff --git a/src/test/scripts/functions/privacy/dp/DPGaussianTest.dml b/src/test/scripts/functions/privacy/dp/DPGaussianTest.dml new file mode 100644 index 00000000000..90510f01f83 --- /dev/null +++ b/src/test/scripts/functions/privacy/dp/DPGaussianTest.dml @@ -0,0 +1,24 @@ +#------------------------------------------------------------- +# +# 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. +# +#------------------------------------------------------------- + +X = read($1); +result = dp_gaussian(X, query=$2, sensitivity=1.0, epsilon=$3, delta=1e-5); +write(result, $4, format="text"); diff --git a/src/test/scripts/functions/privacy/dp/DPLaplaceTest.dml b/src/test/scripts/functions/privacy/dp/DPLaplaceTest.dml new file mode 100644 index 00000000000..f1fa9260f09 --- /dev/null +++ b/src/test/scripts/functions/privacy/dp/DPLaplaceTest.dml @@ -0,0 +1,24 @@ +#------------------------------------------------------------- +# +# 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. +# +#------------------------------------------------------------- + +X = read($1); +result = dp_laplace(X, query=$2, sensitivity=1.0, epsilon=$3); +write(result, $4, format="text"); diff --git a/src/test/scripts/functions/privacy/dp/DPSetBudgetNonLiteralTest.dml b/src/test/scripts/functions/privacy/dp/DPSetBudgetNonLiteralTest.dml new file mode 100644 index 00000000000..fbde50379b1 --- /dev/null +++ b/src/test/scripts/functions/privacy/dp/DPSetBudgetNonLiteralTest.dml @@ -0,0 +1,26 @@ +#------------------------------------------------------------- +# +# 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. +# +#------------------------------------------------------------- + +X = read($1); +computed = sum(X) / nrow(X); +eps = dp_set_budget(computed, 1e-6); +result = dp_laplace(X, query="colMeans", sensitivity=1.0, epsilon=$3); +write(result, $4, format="text"); diff --git a/src/test/scripts/functions/privacy/dp/DPSetBudgetTest.dml b/src/test/scripts/functions/privacy/dp/DPSetBudgetTest.dml new file mode 100644 index 00000000000..2545cfe5dd1 --- /dev/null +++ b/src/test/scripts/functions/privacy/dp/DPSetBudgetTest.dml @@ -0,0 +1,25 @@ +#------------------------------------------------------------- +# +# 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. +# +#------------------------------------------------------------- + +X = read($1); +eps = dp_set_budget($2, 1e-6); +result = dp_laplace(X, query="colMeans", sensitivity=1.0, epsilon=$3); +write(result, $4, format="text"); diff --git a/src/test/scripts/functions/privacy/dp/DPSetBudgetTwiceTest.dml b/src/test/scripts/functions/privacy/dp/DPSetBudgetTwiceTest.dml new file mode 100644 index 00000000000..5e5d7451ef8 --- /dev/null +++ b/src/test/scripts/functions/privacy/dp/DPSetBudgetTwiceTest.dml @@ -0,0 +1,26 @@ +#------------------------------------------------------------- +# +# 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. +# +#------------------------------------------------------------- + +X = read($1); +eps = dp_set_budget($2, 1e-6); +eps = dp_set_budget($2, 1e-6); +result = dp_laplace(X, query="colMeans", sensitivity=1.0, epsilon=$3); +write(result, $4, format="text");