Skip to content

opake.accounting

Differential privacy accounting using Privacy Loss Distributions (PLD).

This module provides a compositional API for tracking privacy guarantees. Mechanism constructors return DpProcess objects that compose with * (repeat) and | (heterogeneous compose). Privacy metrics are queried directly on the resulting process.

import opake.accounting as acc
import opake.dpsgd.accounting as dpsgd_acc

step = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.8), sample_rate=0.01)
training = step * 1000
epsilon = training.epsilon_at(1e-5)

The underlying implementation uses Google's PLD accounting via the opake-accounting Rust crate (PyO3 bindings).

Published opake-accounting artifacts are an sdist, manylinux_2_28 Linux wheels for x86_64 and aarch64, and a macOS 11+ arm64 wheel. Windows, macOS x86_64, and musllinux wheels are intentionally not published at the moment.

See also: Privacy Accounting User Guide


Namespace organization

The accounting API is split into three namespaces:

Namespace Contents Import
opake.accounting Cross-cutting: calibration, composition, Accountant, repeat, compose import opake.accounting as acc
opake.dpsgd.accounting DP-SGD mechanisms: gaussian, adaclip, poisson (plain or truncated via truncated_batch_size / dataset_size), parallel_poisson, k_out_of_t from opake.dpsgd import accounting as dpsgd_acc
opake.dpftrl.accounting DP-FTRL mechanisms: mf_gaussian, poisson (whole-process, parameterized by n_steps), b_min_sep, balls_in_bins from opake.dpftrl import accounting as dpftrl_acc

The mechanism factories (gaussian, poisson, mf_gaussian, …) live only on the algorithm-scoped namespaces — use the namespace that matches your training run. Cross-cutting primitives (Accountant, calibrate, epsilon_budget, composition operators) live on opake.accounting.

# DP-SGD
from opake.dpsgd import accounting as dpsgd_acc
step = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.8), sample_rate=0.01)

# DP-FTRL
from opake.dpftrl import accounting as dpftrl_acc
from opake.dpftrl.noise import band_mf_strategy
strategy = band_mf_strategy(bands=10)
proc = dpftrl_acc.poisson(
    dpftrl_acc.mf_gaussian(1.0, strategy),
    sample_rate=0.01,
    n_steps=1000,
)

# Cross-cutting composition and calibration always go via opake.accounting
import opake.accounting as acc
total = step * 1000
eps = total.epsilon_at(1e-5)

opake.dpsgd.accounting and opake.dpftrl.accounting are lazily imported: the Rust PLD extension is not loaded until you access these submodules.


Classes

Custom budget checkpointing

Accountant checkpoints include an optional budget. The built-in budget factories are registered automatically. If an application supplies its own Budget protocol implementation, register a self-contained codec before checkpointing:

import opake.accounting as acc

acc.register_budget_serializer(
    MyBudget,
    lambda budget: {"target": budget.target},
    lambda state: MyBudget(state["target"]),
)

The serializer must return JSON-compatible state and the deserializer must rebuild the budget from that state. Checkpointing an unregistered budget or restoring an unknown budget checkpoint type raises CheckpointError.

DpProcess

Abstract base class for all privacy processes. Subclasses implement pld() to compute the Privacy Loss Distribution on demand. Results are automatically cached via @lru_cache (maxsize=8). Use cached() for larger cache size (16) or as an opake merge barrier.

Privacy metrics:

Method Returns
epsilon_at(delta) Smallest epsilon achieving (epsilon, delta)-DP
delta_at(epsilon) Smallest delta achieving (epsilon, delta)-DP
advantage() Total-variation advantage (f-DP)
beta_at(alpha) Type-II error at given Type-I error alpha
risk_at(prior) Bayes risk under optimal adversary

Monte Carlo-backed Pld objects expose mc_failure_probability, mc_confidence, and mc_resolution; analytic PLDs report zero failure and zero MC resolution. The confidence construction bounds hockey-stick metrics (epsilon_at, delta_at, and advantage). beta_at and risk_at fail closed to zero for Monte Carlo PLDs because separate directional CDF bounds do not by themselves establish a hypothesis-testing trade-off curve.

Composition operators:

Operator Description Equivalent
proc * k Homogeneous k-fold composition (repeat) acc.repeat(proc, k)
k * proc Same (reflected multiply) acc.repeat(proc, k)
a \| b Heterogeneous composition acc.compose(a, b)

Composition is optimized at construction time: identical steps are collapsed via structural equality, nested repeats are flattened, and identity processes are elided. Composing the same step in a loop produces a single Repeated node with one self_compose call (2 FFTs), not n heterogeneous composes.

step = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.5), 0.01)

# Homogeneous composition
training = step * 1000

# Heterogeneous composition (multi-phase)
phase1 = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.5), 0.01) * 500
phase2 = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.3), 0.01) * 500
total = phase1 | phase2

eps = total.epsilon_at(1e-5)

DiscretizationConfig

Controls PLD discretization precision. Configuration is applied at query time when computing privacy metrics via pld(), not stored in process structure.

Parameter Default Description
discretization 1e-4 Grid spacing for PLD PMF. Error scales as O(d^2)
log_x_mass_truncation_bound -50 Tails below exp(bound) are truncated
max_grid_size 10_000_000 Coarsen grid if it exceeds this many bins
tail_mass_truncation 1e-15 Tail-mass budget during composition
seed 42 RNG seed for Monte Carlo PLD builders
max_conv_grid 32_768 Convolution grid cap for random-allocation PLD
mc_resolution 1e-5 Maximum unresolved MC mass, in delta units
mc_failure_probability 1e-6 Failure probability of the simultaneous MC bound

The Monte Carlo count is derived from binary-KL Chernoff order-statistic bounds with a Bonferroni allocation over all ranks and both adjacency directions. At the default mc_resolution=1e-5 and mc_failure_probability=1e-6, this requires 2,940,252 samples per direction. Counts above 50 million emit an advisory runtime warning but are not capped. The construction follows Hoeffding (1963); the rank and adjacency allocation makes the entire returned PLD simultaneous. epsilon_at(delta) tightens the effective resolution to min(mc_resolution, delta / 2), reserving at least half of the requested delta for finite privacy-loss mass.

Discretization is unconditionally conservative: exact atoms, PMF coarsening, and histogram buckets are rounded upward. The API has no optimistic or lower-bound mode.

Query-time configuration (recommended):

# Create processes without config
proc = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.8), 0.01)

# Apply config at query time
eps_coarse = proc.epsilon_at(1e-5, discretization=1e-3)  # faster, less accurate
eps_fine = proc.epsilon_at(1e-5, discretization=1e-5)    # slower, more accurate

# Monte Carlo accountants derive enough samples for the requested resolution.
pld = proc.pld(mc_resolution=1e-5, mc_failure_probability=1e-6, seed=123)
eps_mc = pld.epsilon_at(2e-5)
print(pld.mc_confidence, pld.mc_resolution)

All pld()-family methods (epsilon_at, delta_at, advantage, beta_at, risk_at) accept every parameter in the table above except tail_mass_truncation (composition-internal, module-level only). Overrides are broadcast to every node of a composed process, so one seed is shared by all Monte Carlo nodes — the same semantics as set_discretization.

Module-level discretization defaults:

Set default config for all queries when not overridden:

acc.set_discretization(discretization=1e-4)  # Apply to all queries
proc = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.8), 0.01)
eps = proc.epsilon_at(1e-5)  # Uses 1e-4 default
  • acc.set_discretization(discretization=1e-4, ...) — Update the global default for only the named parameters; every omitted parameter keeps its current value. A bare call is a no-op.
  • acc.get_discretization() — Return the current DiscretizationConfig

Mechanism functions

All mechanism constructors return a DpProcess. Discretization is configured at query time via epsilon_at(..., discretization=...) or module-level via set_discretization().

gaussian(noise_multiplier) -> DpProcess

Gaussian mechanism with noise multiplier sigma. Adds noise N(0, sigma^2) to sensitivity-1 queries. Base mechanism for DP-SGD.

  • noise_multiplier (float): Ratio of noise std to sensitivity. Larger = more private.

poisson(inner, sample_rate) -> DpProcess

Poisson-subsampled mechanism (standard DP-SGD step). sample_rate is batch_size / dataset_size. Both the plain and truncated forms accept a Gaussian, AdaClip, or NonPrivate base mechanism. Compose the returned step with * num_steps to account for a training run.

  • inner (Gaussian | AdaClip | NonPrivate): Base mechanism
  • sample_rate (float): Probability of including each example, in (0, 1)
step = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.5), sample_rate=256 / 50_000)

poisson(inner, sample_rate, *, truncated_batch_size=None, dataset_size=None) -> DpProcess (truncated form)

poisson() switches to a truncated Poisson PLD when both truncated_batch_size and dataset_size are set together (must be both or neither). This is the truncated-Poisson PLD for capped batches; it does not improve privacy versus plain Poisson at the same rate—use it when training actually truncates draws.

The guarantee is Ganesh (2025), Theorem 3.1. It assumes each over-cap provisional sample is replaced by a uniformly random subset of exactly the cap size. Its directed add/remove adjacency is indexed by the public size \(n\) of the larger neighboring dataset. Therefore dataset_size must equal the sampler's pre-sampling size, and composition must preserve the same \(n\)-indexed adjacency interpretation.

  • inner (Gaussian | AdaClip | NonPrivate): Base mechanism
  • sample_rate (float): Expected sampling rate, in (0, 1)
  • truncated_batch_size (int | None): Optional max batch-size cap
  • dataset_size (int | None): Public larger-neighbor size \(n\), required when truncated_batch_size is set
n = 50_000
batch = 256
step = dpsgd_acc.poisson(
    dpsgd_acc.gaussian(0.8),
    sample_rate=batch / n,
    truncated_batch_size=batch,
    dataset_size=n,
)

parallel_poisson(inner, sample_rate, num_workers) -> DpProcess

Parallel Poisson subsampling. Models independent Poisson sampling on multiple workers, where the same example can appear on multiple devices. Like poisson() (plain or truncated), this is a full wrapper.

  • inner (Gaussian | AdaClip): Base mechanism (from gaussian() or adaclip())
  • sample_rate (float): Probability of including each example, in (0, 1]
  • num_workers (int): Number of parallel workers sampling independently
step = dpsgd_acc.parallel_poisson(
    dpsgd_acc.gaussian(0.5), sample_rate=0.01, num_workers=4,
)

k_out_of_t(inner, *, k, t, allocation) -> DpHorizonProcess

With allocation="block", every record participates in exactly one batch in each of k contiguous, nearly equal blocks. Block sizes differ by at most one.

This mode pairs with opake.dpsgd.sampling.KOutOfTSampler(..., allocation="block") and is computed for the declared horizon by the exact PLD transform of Feldman & Shenfeld (2026), with no Monte Carlo sampling.

  • inner (Gaussian | AdaClip | NonPrivate): Base mechanism
  • k (int): Number of blocks / participations per record
  • t (int): Total optimizer-step horizon
  • allocation ("block" or "total"): Required allocation mode
process = dpsgd_acc.k_out_of_t(
    dpsgd_acc.gaussian(0.5),
    k=num_epochs,
    t=num_epochs * steps_per_epoch,
    allocation="block",
)
eps = process.epsilon_at(1e-5)

With allocation="total", every record chooses a uniform k-subset of the complete horizon. The accountant currently uses the block reduction as a conservative upper bound.

adaclip(inner, *, fraction_noise_std, expected_batch_size, num_groups=1) -> DpProcess

Accounts for the extra privacy cost of adaptive clipping's noisy centered-indicator query. Returns an AdaClip process composable with poisson() (plain or truncated).

  • inner (Gaussian): Base mechanism (from gaussian())
  • fraction_noise_std (float): Noise std on the centered indicator. Default: 0.05.
  • expected_batch_size (float): Expected batch size (sample_rate × dataset_size), the fixed denominator for the indicator; it must match the runtime's expected_batch_size.
  • num_groups (int): Number of independently adaptive clipping groups, and therefore independent noisy quantile queries. Default: 1. Set this to the number of groups when using per-group adaptive clipping.
step = dpsgd_acc.poisson(dpsgd_acc.adaclip(dpsgd_acc.gaussian(0.5), fraction_noise_std=0.05, expected_batch_size=256), 0.01)

Private second-moment release

When the noise mechanism produces both gradients and squared gradients (via clipped_grad(..., second_moment=True)), the joint paired release uses sensitivity-proportional Mahalanobis allocation in the runtime σ split (see the paired second-moment release section). The Mahalanobis budget collapses to a single sensitivity-1 Gaussian release at the same noise multiplier, so privacy accounting is exactly the underlying first-moment mechanism:

import opake.accounting as acc

# DP-SGD: same chain as first-moment-only
step = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.8), sample_rate=batch_size / dataset_size)
training = step * num_steps

# DP-FTRL with BandMF: same chain as first-moment-only
proc = dpftrl_acc.poisson(
    dpftrl_acc.mf_gaussian(1.0, strategy),
    sample_rate=batch_size / dataset_size,
    n_steps=num_steps,
)

There is no separate second_moment transformation to wrap and no ρ knob: the runtime σ on each stream already absorbs the joint cost. Use the underlying mechanism factories (dpsgd_acc.gaussian, dpftrl_acc.mf_gaussian, …) directly.

eps_delta(epsilon, delta=0.0) -> DpProcess

Fixed (epsilon, delta)-DP guarantee. Useful for composing an external mechanism with known privacy parameters into tracked processes.

  • epsilon (float): Privacy parameter (>= 0)
  • delta (float): Failure probability (default 0.0)
external = acc.eps_delta(3.0, 1e-5)
total = external | (dpsgd_acc.poisson(dpsgd_acc.gaussian(0.5), 0.01) * 1000)

identity() -> DpProcess

Identity mechanism (zero privacy loss). Acts as the identity element in composition: identity() | a returns a.

nonprivate() -> DpProcess

Non-private mechanism (ε=∞, δ=0). Useful as a baseline or for training without a privacy guarantee. All Poisson-family amplifications handle nonprivate() inner transparently (zero privacy cost).

step = dpsgd_acc.poisson(acc.nonprivate(), sample_rate=0.01)
training = step * 1000
# training.epsilon_at(1e-5) == inf

Matrix factorization mechanisms

MF mechanisms resolve sensitivity and Gram matrices from a noise strategy and its participation context. Use the same recipe and context for accounting and noise generation.

mf_gaussian(noise_multiplier, strategy, *, n_steps=None, min_sep=1, max_participations=None) -> DpProcess

Omitting n_steps creates a recipe for an amplifier. Calling pld() or epsilon_at() on that recipe directly raises ConfigurationError.

For bare accounting, n_steps is the full release horizon, min_sep is the minimum separation between contributions, and max_participations bounds each protected unit's contributions. If omitted, max_participations defaults to n_steps. The resulting Gaussian PLD accounts for the declared context; do not compose it again by the number of rounds.

from opake.dpftrl.noise import band_mf_strategy

# One contribution per protected unit across 1000 rounds.
strategy = band_mf_strategy(bands=10)
proc = dpftrl_acc.mf_gaussian(
    1.0, strategy, n_steps=1000, min_sep=1, max_participations=1,
)
eps = proc.epsilon_at(1e-5)

Bare BandMF and Identity sensitivity currently accounts for one participation. Do not use bare calls with these strategies to budget repeated participation. For amplified accounting, leave the inner horizon unspecified and supply the full horizon to the sampler-matching amplifier, which resolves its own context.

Correlated MF mechanisms (BLT, λCGD, BISR, BSR)

For bare BLT accounting, supply the same horizon and participation bounds as mf_gaussian_noise. This example assumes at most five contributions per protected unit, at least 1000 rounds apart:

from opake.dpftrl.noise import blt_strategy
strategy = blt_strategy(max_buffers=10)

# Unamplified accounting for the full 5000-round context.
proc = dpftrl_acc.mf_gaussian(
    1.0, strategy, n_steps=5000, min_sep=1000, max_participations=5,
)

# With a fixed partition into 1000 bins, repeated for five epochs.
proc = dpftrl_acc.balls_in_bins(
    dpftrl_acc.mf_gaussian(1.0, strategy),
    num_bins=1000, n_steps=5000,
)

The same amplified construction works for lambda_cgd_strategy, bisr_strategy, and bsr_strategy:

from opake.dpftrl.noise import lambda_cgd_strategy
strategy = lambda_cgd_strategy(lambda_=0.9)
proc = dpftrl_acc.balls_in_bins(
    dpftrl_acc.mf_gaussian(1.0, strategy),
    num_bins=steps_per_epoch, n_steps=steps_per_epoch * num_epochs,
)

poisson(inner, sample_rate, *, n_steps) -> DpProcess

Poisson amplification for DP-FTRL. Whole-process accountant covering all n_steps training rounds (do not compose with * num_steps externally). Cyclic when the inner is BandMf with bands > 1 (decomposes into ceil(n_steps / bands) independent groups); plain Poisson per round when the inner is IdentityMf or BandMf with bands == 1.

  • inner (BandMf | IdentityMf): MF mechanism.
  • sample_rate (float): Poisson sampling probability per round.
  • n_steps (int, keyword-only): Total number of training rounds.
strategy = band_mf_strategy(bands=10)
proc = dpftrl_acc.poisson(
    dpftrl_acc.mf_gaussian(1.0, strategy),
    sample_rate=0.01,
    n_steps=1000,
)

b_min_sep(inner, *, n_steps, p0) -> DpProcess

Warm-start b-min-sep amplification for BandMF (Dong & Ganesh, arXiv:2602.09338). Uses Monte Carlo PLD accounting. inner must be mf_gaussian(nm, BandMfStrategy(...)) — strategy coefficients and band width are read from inner.strategy. p0 is the per-example participation rate per iteration E[|B|]/|D| (match the training sampler’s target batch size). Control the confidence construction per query with mc_resolution, mc_failure_probability, and seed, or module-wide via set_discretization. The sample count is derived automatically and exposed as get_discretization().resolved_num_mc_samples.

Monte Carlo confidence

b-min-sep, and Balls-in-Bins with a correlated strategy, build their PLDs from simultaneous one-sided order-statistic bounds over both adjacency directions. Unresolved probability is placed at +∞, so epsilon_at(delta) returns infinity when delta is at or below the reported pld.infinity_mass. pld.mc_failure_probability is statistical confidence metadata and is separate from mechanism delta.

balls_in_bins(inner, *, num_bins, n_steps) -> DpProcess

Balls-in-Bins (random-partition) amplification. Returns the total privacy cost across all epochs — do NOT compose further with * num_epochs.

With identity_strategy() (uncorrelated noise) the Gram is exactly num_epochs · I, so the dominating pair collapses onto 1-out-of-num_bins random allocation at σ / √num_epochs and the PLD is computed by the exact transform — deterministic, no sampling. For correlated-noise strategies (DP-λCGD, BISR, BSR, BLT) it uses Monte Carlo sampling of the dominating pair.

  • inner (MfGaussian): dpftrl_acc.mf_gaussian(nm, strategy).
  • num_bins (int): Number of bins per epoch (typically dataset_size / batch_size).
  • n_steps (int): Total training rounds; must be a multiple of num_bins.
# With DP-λCGD
strategy = lambda_cgd_strategy(lambda_=0.9)
proc = dpftrl_acc.balls_in_bins(
    dpftrl_acc.mf_gaussian(1.0, strategy),
    num_bins=steps_per_epoch, n_steps=steps_per_epoch * num_epochs,
)

# With uncorrelated MF noise (analytic identity-strategy path)
proc = dpftrl_acc.balls_in_bins(
    dpftrl_acc.mf_gaussian(1.1, identity_strategy()),
    num_bins=100,
    n_steps=1000,
)

Composition Functions

Functional equivalents of the * and | operators. Most users should prefer the operator syntax.

repeat(process, count) -> DpProcess

Homogeneous k-fold composition. Equivalent to process * count.

compose(left, right) -> DpProcess

Heterogeneous two-process composition. Equivalent to left | right.

cached(process) -> DpProcess

Increases the LRU cache size from 8 to 16 entries and acts as an opake merge barrier: the composition optimizer will not look through a cached node.

Note: All pld() methods are automatically cached with maxsize=8 via @lru_cache. Use cached() when you need: - A larger cache (16 entries instead of 8) - An explicit merge barrier to prevent composition optimizations

# All queries automatically cached (maxsize=8)
step = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.5), 0.01)
eps = step.epsilon_at(1e-5)   # Cached automatically
adv = step.advantage()         # Cache hit

# Use cached() for merge barrier or larger cache
training = acc.cached(step * 1000)
eps = training.epsilon_at(1e-5)   # Cached with maxsize=16

Serialization

Processes checkpoint as a flat dict[str, Any] (string keys with dotted prefixes for nested composition). Use opake.serialization.state_dict and opake.serialization.from_state_dict — pass any concrete DpProcess instance as the template (for example, identity()); the registered handler rebuilds from the dict's root type field.

import opake.accounting as acc
from opake.serialization import from_state_dict, state_dict
import opake.dpsgd.accounting as dpsgd_acc

step = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.5), 0.01)
flat = state_dict(step)
step2 = from_state_dict(acc.identity(), flat)

Accountant

The Accountant class tracks accumulated privacy loss across a training loop. It provides a functional API: composing a new process returns a fresh Accountant (the original is not modified).

Merge optimization is automatic. Composing the same step repeatedly in a loop produces a single Repeated node internally.

from opake.accounting import Accountant

acct = Accountant()
step = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.5), 0.01)

for i in range(num_steps):
    acct = acct | step

    if i % 100 == 0:
        eps = acct.epsilon_at(1e-5)
        print(f"Step {i}: eps={eps:.2f}")

Budget tracking

Pass an optional Budget from the calibration module to enable budget checking:

from opake.accounting import calibration as cal
from opake.accounting import Accountant

budget = cal.epsilon_budget(3.0, delta=1e-5)
acct = Accountant(budget=budget)
step = dpsgd_acc.poisson(dpsgd_acc.gaussian(0.5), 0.01)

for i in range(num_steps):
    acct = acct | step
    if acct.budget_exceeded:
        print("Privacy budget exhausted.")
        break

Methods: epsilon_at(delta), delta_at(epsilon), advantage(), beta_at(alpha), risk_at(prior). Properties: budget, budget_exceeded.

Seeding with a prior process

Pass prefix to start from an already-executed process instead of the zero-cost identity. This is sequential composition across runs: use it when a second training stage (e.g. DPO after SFT) touches the same dataset, so the new run's budget checks and reported ε include the earlier run's cost. The prefix composes at the PLD level — much tighter than adding the two stages' epsilons.

import json

from opake.accounting import Accountant
from opake.serialization import from_state_dict

with open("sft_checkpoint/accountant.json") as f:
    sft = from_state_dict(Accountant(), json.load(f))

acct = Accountant(budget=budget, prefix=sft.process)
acct = acct | dpo_step  # composes on top of the SFT prefix

If the two stages train on disjoint records (and the privacy unit is the record), no prefix is needed — parallel composition applies and the overall guarantee is the pointwise max of the two stages' ε(δ) curves.

Serialization

from opake.accounting import Accountant
from opake.serialization import from_state_dict, state_dict

flat = state_dict(acct)
acct2 = from_state_dict(Accountant(), flat)

process.* keys hold the composed tree; budget.* keys are present when the accountant was constructed with a budget.

A template budget fills a checkpoint that has none. If both carry budgets, their checkpoint states must agree or restore raises CheckpointError. Use an empty template to keep the checkpoint budget; to choose a new budget, restore the process first:

saved = from_state_dict(Accountant(), flat)
acct2 = Accountant(budget=budget, prefix=saved.process)

Calibration

Submodule: opake.accounting.calibration

from opake.accounting import calibration as cal

Binary search for finding parameter values that achieve a target privacy budget.

calibrate(budget, process, param_min, param_max, tolerance=1e-6, max_iterations=100, prefix=None) -> CalibrateResult

Binary search for a parameter value such that process(param) produces a DpProcess achieving the given privacy budget from the privacy-safe side.

Parameter Default Description
budget A Budget object from a budget factory (see below)
process Callable: float -> DpProcess
param_min Lower bound for search
param_max Upper bound for search
tolerance 1e-6 Positive, finite relative convergence tolerance
max_iterations 100 Positive maximum number of binary search iterations
prefix None Already-executed DpProcess composed into every probe

The process callable takes a single float parameter and returns a DpProcess. Its metric must be monotone in the parameter over [param_min, param_max]; the search direction is derived automatically by probing both endpoints, so noise-multiplier-like parameters (increase improves privacy) and sample-rate/step-like parameters (increase spends privacy) are both supported. Exactly one endpoint must be privacy-safe; which one is detected, not positional.

import opake.accounting as acc
from opake.accounting import calibration as cal

budget = cal.epsilon_budget(3.0, delta=1e-5)
result = cal.calibrate(
    budget,
    lambda nm: dpsgd_acc.poisson(dpsgd_acc.gaussian(nm), sample_rate=0.01) * 1000,
    param_min=0.1,
    param_max=5.0,
)
print(f"noise_multiplier = {result.param:.4f}, epsilon = {result.achieved:.6f}")

Every successful result is privacy-safe and relatively close to the target: decreasing privacy-loss budgets return achieved <= target, while increasing privacy-gain budgets return achieved >= target. In both cases, math.isclose(achieved, target, rel_tol=tolerance, abs_tol=0.0) is true. tolerance must be finite and positive, and max_iterations must be positive; invalid values raise CalibrationError before the process is evaluated. If no safe endpoint reaches the requested relative tolerance, calibrate() raises CalibrationError instead of returning an under-noised parameter.

When the process uses a Monte Carlo PLD, calibration divides the configured failure probability across the two endpoint probes and at most max_iterations interior probes. result.mc_confidence therefore covers the adaptive search as a whole rather than only its selected final parameter. Balls-in-Bins calibration also reuses its seeded, sigma-independent projected draws across probes when they fit the bounded native cache. Set OPAKE_BNB_TRANSCRIPT_CACHE_MAX_BYTES to control its memory cap (4 GiB by default), or to 0 to use the lower-memory one-shot path for every probe.

Calibrating a second stage against the remaining budget (see Seeding with a prior process): pass the earlier run's executed process as prefix. Each probe evaluates prefix | process(param), so the budget is the total across both stages. The prefix's PLD is computed once for the whole search.

result = cal.calibrate(
    cal.epsilon_budget(8.0, delta=1e-6),
    lambda nm: dpsgd_acc.poisson(dpsgd_acc.gaussian(nm), 0.02) * 2000,
    param_min=0.5,
    param_max=5.0,
    prefix=sft.process,  # from the SFT run's accountant.json
)

Multi-phase training:

result = cal.calibrate(
    cal.epsilon_budget(5.0, delta=1e-5),
    lambda nm: (
        dpsgd_acc.poisson(dpsgd_acc.gaussian(nm), 0.01) * 500
        | dpsgd_acc.poisson(dpsgd_acc.gaussian(nm * 0.8), 0.01) * 500
        | dpsgd_acc.poisson(dpsgd_acc.gaussian(nm * 0.5), 0.01) * 500
    ),
    param_min=0.2,
    param_max=3.0,
    tolerance=0.01,
)

CalibrateResult

Returned by calibrate().

Attribute Type Description
param float Found parameter value
achieved float Achieved metric value at param
target float Target metric value
iterations int Number of binary search iterations
converged bool Always True for a successfully returned result
mc_failure_probability float Overall failure probability for adaptive MC probes
mc_confidence float Confidence covering the complete calibration search

Budget Factories

Budget factories create Budget objects that define what privacy metric to optimize and what value to achieve.

Factory Metric being calibrated Decreasing with noise
cal.epsilon_budget(eps, delta) epsilon at given delta Yes
cal.delta_budget(delta, epsilon) delta at given epsilon Yes
cal.advantage_budget(advantage) f-DP total-variation advantage Yes
cal.beta_budget(beta, alpha) Type-II error at given Type-I error No
cal.risk_budget(risk, prior) Bayes risk under optimal adversary No

"Decreasing with noise" indicates the metric kind: privacy-loss metrics (epsilon/delta/advantage) are safe at-or-below the target, privacy-gain metrics (beta/risk) at-or-above. The binary search derives the parameter direction automatically by probing both bracket endpoints.

# (epsilon, delta)-DP
result = cal.calibrate(
    cal.epsilon_budget(3.0, delta=1e-5),
    lambda nm: dpsgd_acc.poisson(dpsgd_acc.gaussian(nm), 0.01) * 1000,
    0.1, 5.0,
)

# f-DP advantage
result = cal.calibrate(
    cal.advantage_budget(0.1),
    lambda nm: dpsgd_acc.poisson(dpsgd_acc.gaussian(nm), 0.01) * 1000,
    0.2, 3.0,
)

# (alpha, beta) error rates
result = cal.calibrate(
    cal.beta_budget(0.05, alpha=0.01),
    lambda nm: dpsgd_acc.poisson(dpsgd_acc.gaussian(nm), 0.01) * 1000,
    0.2, 3.0,
)

# Bayes risk
result = cal.calibrate(
    cal.risk_budget(0.1, prior=0.5),
    lambda nm: dpsgd_acc.poisson(dpsgd_acc.gaussian(nm), 0.01) * 1000,
    0.2, 3.0,
)