Source code for riskcal.analysis.conversions

"""Privacy risk metrics and conversions.

This module provides functions for computing privacy metrics (beta, advantage,
Bayes risk) from different privacy representations (PLDs, ADP, GDP,
RDP, zCDP).
"""

from typing import Union, Optional
import warnings
import numpy as np
from scipy import stats, optimize
from dp_accounting.pld import privacy_loss_distribution

from riskcal.analysis import _plrv
from riskcal.utils import _ensure_array

from riskcal.analysis.rdp import get_FNR as _get_FNR_from_rdp


# =============================================================================
# Internal conversions
# =============================================================================


[docs] def pld_to_plrvs( pld: privacy_loss_distribution.PrivacyLossDistribution, ) -> _plrv.PLRVs: """ Convert PLD to internal PLRVs representation. Converts a Google dp_accounting Privacy Loss Distribution object into the PLRVs (Privacy Loss Random Variables) format used internally. Args: pld: Privacy loss distribution from Google's dp_accounting library. Returns: PLRVs object containing the privacy loss random variables. Note: This is an internal conversion function. Most users should use the higher-level `get_beta_from_pld()`, `get_advantage_from_pld()`, etc. """ def _get_plrv(pld): pld = pld.to_dense_pmf() pmf = pld._probs lower_loss = pld._lower_loss infinity_mass = pld._infinity_mass return lower_loss, infinity_mass, pmf lower_loss_Y, infinity_mass_Y, pmf_Y = _get_plrv(pld._pmf_remove) lower_loss_Z, infinity_mass_Z, pmf_Z = _get_plrv(pld._pmf_add) upper_loss_Z = lower_loss_Z + len(pmf_Z) - 1 # clean pmfs. Sometimes float errors cause probs to be negative pmf_Y = np.where(pmf_Y < 0, 0, pmf_Y) pmf_Z = np.where(pmf_Z < 0, 0, pmf_Z) pmf_Y = pmf_Y * (1 - infinity_mass_Y) / np.sum(pmf_Y) pmf_Z = pmf_Z * (1 - infinity_mass_Z) / np.sum(pmf_Z) is_symmetric = pld._symmetric return _plrv.PLRVs( y0=lower_loss_Y, x0=-upper_loss_Z, pmf_Y=pmf_Y, pmf_X=pmf_Z[::-1], minus_infinity_mass_X=infinity_mass_Z, infinity_mass_Y=infinity_mass_Y, is_symmetric=is_symmetric, )
# Backward compatibility alias plrvs_from_pld = pld_to_plrvs plrvs_from_pld.__deprecated__ = "2.0.0" # ============================================================================= # PLD (Privacy Loss Distribution) # =============================================================================
[docs] def get_beta_from_pld( pld: privacy_loss_distribution.PrivacyLossDistribution, alpha: Union[float, np.ndarray] = None, alphas: Union[float, np.ndarray] = None, # Deprecated ) -> Union[float, np.ndarray]: """ Compute false negative rate (FNR) for given false positive rate (FPR) from PLD. Uses the direct method from Algorithm 1 (Kulynych et al., 2024) to compute the optimal trade-off between FNR (beta) and FPR (alpha). .. deprecated:: 1.2.0 Parameter 'alphas' is deprecated and will be removed in version 2.0.0. Use 'alpha' instead. Args: pld: Privacy loss distribution from Google's dp_accounting library. alpha: False positive rate(s) in [0, 1]. Can be scalar or array. alphas: (Deprecated) Use 'alpha' instead. Returns: False negative rate(s) corresponding to input alpha. Example: >>> from dp_accounting.pld import privacy_loss_distribution as pld_module >>> pld = pld_module.from_gaussian_mechanism(1.0) >>> beta = get_beta_from_pld(pld, alpha=0.01) References: Kulynych & Gomez et al. (2024), Algorithm 1. https://arxiv.org/abs/2407.02191 """ if alpha is None and alphas is None: raise ValueError("Must specify alpha.") elif alpha is not None and alphas is not None: raise ValueError("Must pass either alpha or alphas.") elif alphas is not None: warnings.warn( "Parameter 'alphas' is deprecated. Use 'alpha' instead.", DeprecationWarning, stacklevel=2, ) alpha = alphas return _plrv.get_beta(pld_to_plrvs(pld), alpha)
[docs] def get_advantage_from_pld( pld: privacy_loss_distribution.PrivacyLossDistribution, ) -> float: """ Compute attack advantage from PLD. Advantage is the maximum value of (TPR - FPR) achievable by any attacker, which equals delta at epsilon=0. Args: pld: Privacy loss distribution from Google's dp_accounting library. Returns: Maximum attack advantage. References: Kulynych & Gomez et al. (2024). https://arxiv.org/abs/2407.02191 """ return pld.get_delta_for_epsilon(0)
[docs] def get_bayes_risk_from_pld(pld, prior): """ Compute Bayes Risk from PLD for given prior probability. Bayes risk shows the maximum accuracy of an attack against privacy of a single record under a binary prior (e.g., accuracy of attribute inference). Args: pld: Privacy loss distribution from Google's dp_accounting library. prior: Prior probability (scalar or array). Probability that the sensitive attribute takes value 1. Returns: Bayes risk value(s). Float if prior is scalar, array if prior is array. Example: >>> from dp_accounting.pld import privacy_loss_distribution as pld_module >>> pld = pld_module.from_laplace_mechanism(1.0) >>> risk = get_bayes_risk_from_pld(pld, prior=0.5) References: Kulynych et al. (2025), Proposition D.1 / Eq. 35. https://arxiv.org/abs/2507.06969 """ prior, is_scalar = _ensure_array(prior) epsilon = np.log(prior / (1 - prior)) delta = np.array([pld.get_delta_for_epsilon(e) for e in epsilon]) bayes_risk = (1 - prior) * (1 - delta) if is_scalar: return bayes_risk[0] return np.array(bayes_risk)
# ============================================================================= # GDP (Gaussian Differential Privacy) # =============================================================================
[docs] def get_beta_from_gdp( mu: float, alpha: Union[float, np.ndarray] ) -> Union[float, np.ndarray]: """ Compute FNR for FPR using the analytical formula for Gaussian DP. Args: mu: Gaussian noise scale parameter (sigma). alpha: False positive rate(s) in [0, 1]. Returns: False negative rate(s) corresponding to input alpha. References: Dong et al. (2019), Eq. 6. https://arxiv.org/abs/1905.02383 """ return stats.norm.cdf(-stats.norm.ppf(alpha) - mu)
[docs] def get_advantage_from_gdp(mu: Union[float, np.ndarray]) -> Union[float, np.ndarray]: """ Compute attack advantage using analytical formula for Gaussian mechanism. Args: mu: Gaussian noise scale parameter (sigma). Can be scalar or array. Returns: Attack advantage value(s). References: Dong et al. (2019), Corollary 2.13. https://arxiv.org/abs/1905.02383 """ return stats.norm.cdf(mu / 2) - stats.norm.cdf(-mu / 2)
[docs] def get_bayes_risk_from_gdp(mu, prior): """ Compute Bayes Risk for Gaussian mechanism using analytical formula. Args: mu: Gaussian noise scale parameter (sigma). prior: Prior probability (scalar or array). Returns: Bayes risk value(s). Float if prior is scalar, array if prior is array. References: - Kulynych et al. (2025), Proposition D.1 / Eq. 35. https://arxiv.org/abs/2507.06969 - Dong et al. (2019), Corollary 2.13. https://arxiv.org/abs/1905.02383 """ assert mu >= 0, "mu must be >= 0" prior, is_scalar = _ensure_array(prior) epsilon = np.log(prior / (1 - prior)) # Gaussian privacy profile: Dong et al. (2019), Corollary 2.13 delta = stats.norm.cdf(-epsilon / mu + mu / 2) - np.exp(epsilon) * stats.norm.cdf( -epsilon / mu - mu / 2 ) bayes_risk = (1 - prior) * (1 - delta) if is_scalar: return bayes_risk[0] return np.array(bayes_risk)
# Backward compatibility aliases get_beta_for_mu = get_beta_from_gdp get_beta_for_mu.__deprecated__ = "2.0.0" get_advantage_for_mu = get_advantage_from_gdp get_advantage_for_mu.__deprecated__ = "2.0.0" get_bayes_risk_for_mu = get_bayes_risk_from_gdp get_bayes_risk_for_mu.__deprecated__ = "2.0.0" # ============================================================================= # ADP (Approximate Differential Privacy) # =============================================================================
[docs] def get_beta_from_adp( epsilon: float, delta: float, alpha: Union[float, np.ndarray] ) -> Union[float, np.ndarray]: """ Compute FNR for FPR from (epsilon, delta)-DP parameters. Args: epsilon: Privacy parameter epsilon. delta: Privacy parameter delta. alpha: False positive rate(s) in [0, 1]. Returns: False negative rate(s) corresponding to input alpha. Example: >>> import numpy as np >>> np.round(get_beta_from_adp(1.0, 0.001, 0.8), 3) 0.073 References: Dong et al. (2019), Eq. 5. https://arxiv.org/abs/1905.02383 """ form1 = np.array(1 - delta - np.exp(epsilon) * alpha) form2 = np.array(np.exp(-epsilon) * (1 - delta - alpha)) return np.maximum.reduce([form1, form2, np.zeros_like(form1)])
[docs] def get_advantage_from_adp(epsilon: float, delta: float) -> float: """ Compute attack advantage from (epsilon, delta)-DP parameters. Args: epsilon: Privacy parameter epsilon. delta: Privacy parameter delta. Returns: Attack advantage. Example: >>> import numpy as np >>> np.round(get_advantage_from_adp(0., 0.001), 3) 0.001 References: Dong et al. (2019). https://arxiv.org/abs/1905.02383 """ return (np.exp(epsilon) + 2 * delta - 1) / (np.exp(epsilon) + 1)
[docs] def get_epsilon_from_err_rates(delta: float, alpha: float, beta: float) -> float: """ Convert f-DP error rates (alpha, beta) to epsilon for (epsilon, delta)-DP. Args: delta: Target delta parameter for (epsilon, delta)-DP. alpha: False positive rate (FPR) from f-DP. beta: False negative rate (FNR) from f-DP. Returns: Epsilon value corresponding to the error rates. Example: >>> import numpy as np >>> np.round(get_epsilon_from_err_rates(0.001, 0.001, 0.8), 3) 5.293 References: Dong et al. (2019). https://arxiv.org/abs/1905.02383 """ epsilon1 = np.log((1 - delta - alpha) / beta) epsilon2 = np.log((1 - delta - beta) / alpha) return np.maximum.reduce([epsilon1, epsilon2, np.zeros_like(epsilon1)])
# Backward compatibility aliases get_beta_for_epsilon_delta = get_beta_from_adp get_beta_for_epsilon_delta.__deprecated__ = "2.0.0" get_advantage_for_epsilon_delta = get_advantage_from_adp get_advantage_for_epsilon_delta.__deprecated__ = "2.0.0" get_epsilon_for_err_rates = get_epsilon_from_err_rates get_epsilon_for_err_rates.__deprecated__ = "2.0.0" # ============================================================================= # RDP (Renyi Differential Privacy) # =============================================================================
[docs] def get_beta_from_rdp( epsilon: float, alpha: Union[float, np.ndarray], order: float, linear_search_step: float = 1e-3, # unused; kept for backward compatibility max_bisection_steps: int = 50, # unused; kept for backward compatibility tol: float = 1e-7, ) -> Union[float, np.ndarray]: """ Compute FNR for FPR from Renyi DP parameters. Uses the optimal conversion from a single RDP guarantee to a tradeoff function (Riess et al., 2026). Accepts scalar or array FPR values. Args: epsilon: Renyi divergence parameter (privacy budget). alpha: False positive rate(s) (FPR) in [0, 1]. Scalar or array. order: Order of Renyi divergence (alpha in Renyi DP literature). linear_search_step: Unused. Kept for backward compatibility. max_bisection_steps: Unused. Kept for backward compatibility. tol: Numerical tolerance for bisection convergence. Returns: False negative rate (FNR) corresponding to input alpha. References: - Zhu et al. (2022), Appendix F.1. https://arxiv.org/abs/2106.08567 - Riess et al. (2026). https://arxiv.org/abs/2602.04562 """ return _get_FNR_from_rdp(alpha_array=alpha, order=order, epsilon=epsilon, tol=tol)
# ============================================================================= # zCDP (Zero-Concentrated Differential Privacy) # =============================================================================
[docs] def get_beta_from_zcdp( rho: float, alpha: Union[float, np.ndarray], tol: float = 1e-9, min_order: float = 1.0, max_order: float = 1024.0, grid_size=1000, ) -> Union[float, np.ndarray]: """ Compute FNR for FPR from zCDP parameter rho. Zero-Concentrated Differential Privacy (zCDP) is characterized by a single parameter rho. This function computes the optimal trade-off between false negative rate (beta) and false positive rate (alpha) by optimizing over Renyi divergence orders. Args: rho: Zero-concentrated differential privacy parameter. alpha: False positive rate(s) in [0, 1]. Can be scalar or array. tol: Tolerance for numerical convergence and avoiding log(0). max_order: Maximum Renyi DP order to consider. Returns: False negative rate(s) corresponding to input alpha. Returns float if alpha is scalar, array if alpha is array. Example: >>> import numpy as np >>> # Single alpha value >>> beta = get_beta_from_zcdp(rho=0.5, alpha=0.1) >>> np.round(beta, 3) 0.517 >>> # Multiple alpha values >>> betas = get_beta_from_zcdp(rho=0.5, alpha=np.array([0.1, 0.2, 0.3])) >>> np.round(betas, 3) array([0.517, 0.34, 0.232]) References: - Bun & Steinke (2016). https://arxiv.org/abs/1605.02065 - Zhu et al. (2022), Appendix F.1. https://arxiv.org/abs/2106.08567 - Riess et al. (2026). https://arxiv.org/abs/2602.04562 """ scalar_input = isinstance(alpha, (int, float)) alpha_arr = np.atleast_1d(np.asarray(alpha, dtype=float)) result = np.empty_like(alpha_arr) low = alpha_arr <= tol high = alpha_arr >= 1 - tol interior = ~low & ~high result[low] = 1.0 result[high] = 0.0 orders_grid = np.concatenate( [ np.linspace(min_order, 2, grid_size // 3)[:-1], np.linspace(2, 16, grid_size // 3)[:-1], np.logspace(np.log10(16), np.log10(max_order), grid_size // 3), ] ) if interior.any(): alpha_interior = alpha_arr[interior] best_vals = np.zeros(alpha_interior.shape) with warnings.catch_warnings(): warnings.simplefilter("ignore", RuntimeWarning) for order in orders_grid: best_vals = np.maximum( best_vals, np.atleast_1d( get_beta_from_rdp( epsilon=rho * order, alpha=alpha_interior, order=order, tol=tol, ) ), ) result[interior] = best_vals return float(result[0]) if scalar_input else result
[docs] def get_advantage_from_zcdp( rho: float, tol: float = 1e-9, min_order: float = 1.0, max_order: float = 1024.0, grid_size: int = 1000, ) -> float: """ Compute attack advantage from zCDP parameter rho. Advantage is the maximum value of (TPR - FPR) achievable by any attacker. This function optimizes over all possible threshold choices to find the maximum advantage. Args: rho: Zero-concentrated differential privacy parameter. tol: Tolerance for numerical convergence and avoiding log(0). min_order: Minimum Renyi DP order to consider. max_order: Maximum Renyi DP order to consider. grid_size: Number of orders to evaluate in the grid search. Returns: Maximum attack advantage. Example: >>> import numpy as np >>> adv = get_advantage_from_zcdp(rho=0.5) >>> np.round(adv, 3) 0.47 References: Bun & Steinke (2016). https://arxiv.org/abs/1605.02065 """ result = optimize.minimize_scalar( lambda alpha: -( 1 - alpha - get_beta_from_zcdp( rho=rho, alpha=alpha, tol=tol, min_order=min_order, max_order=max_order, grid_size=grid_size, ) ), bounds=(0, 1), method="bounded", options={"xatol": 1e-12}, ) if not result.success: warnings.warn("Optimization failed for advantage calculation") return np.nan else: return -result.fun
def get_mu_from_zcdp_approx(rho: float) -> float: """Deprecated. Use get_beta_from_zcdp or get_advantage_from_zcdp instead.""" warnings.warn( "get_mu_from_zcdp_approx is deprecated and will be removed in a future version. " "Use get_beta_from_zcdp or get_advantage_from_zcdp for exact computations.", DeprecationWarning, stacklevel=2, ) return ( 1.5822558654881096 * np.sqrt(rho) + 0.08064620797681155 * rho + 0.05485600531538526 ) def get_beta_from_zcdp_approx( rho: float, alpha: Union[float, np.ndarray] ) -> Union[float, np.ndarray]: """Deprecated. Use get_beta_from_zcdp instead.""" warnings.warn( "get_beta_from_zcdp_approx is deprecated and will be removed in a future version. " "Use get_beta_from_zcdp for exact computation.", DeprecationWarning, stacklevel=2, ) return get_beta_from_zcdp(rho, alpha=alpha) def get_advantage_from_zcdp_approx(rho: float) -> float: """Deprecated. Use get_advantage_from_zcdp instead.""" warnings.warn( "get_advantage_from_zcdp_approx is deprecated and will be removed in a future version. " "Use get_advantage_from_zcdp for exact computation.", DeprecationWarning, stacklevel=2, ) return get_advantage_from_zcdp(rho)