Questions or feedback?

Source code for opendp.extras.mbi._fixed

"""Releases all queries in a fixed workload."""

from dataclasses import dataclass
from typing import Any, TYPE_CHECKING

from opendp._internal import _new_pure_function
from opendp._lib import import_optional_dependency
from opendp.extras.mbi._utilities import (
    get_associated_metric,
    get_cardinalities,
    then_noise_marginals,
    make_stable_marginals,
    Algorithm,
    Count,
    Marginals,
    OnewayType,
    ONEWAY_UNKEYED,
)
from opendp.mod import (
    FrameDistance,
    LazyFrameDomain,
    Measure,
    Measurement,
    binary_search_chain,
)

if TYPE_CHECKING:  # pragma: no cover
    from opendp.extras.polars import Bound


[docs] @dataclass(kw_only=True, frozen=True) class Fixed(Algorithm): """Releases all queries in a fixed workload.""" queries: list[Count] """Workload of queries.""" oneway: OnewayType = ONEWAY_UNKEYED """Only fit one-way marginals for columns missing keys. The fixed algorithm differs from other algorithms in that it only estimates marginals with missing keys, not all unknown first-order marginals. """ @property def needs_model(self) -> bool: # Fixed workloads are predetermined and do not need a model for # workload selection. The fixed release supplies the constraint used # to fit the final model. return False def __post_init__(self): super().__post_init__() if not self.queries: raise ValueError("queries must have at least one element") if not all(isinstance(q, Count) for q in self.queries): raise ValueError("queries must be of type Count")
[docs] def make_marginals( self, input_domain: LazyFrameDomain, input_metric: FrameDistance, output_measure: Measure, d_in: list["Bound"], d_out: float, *, marginals: Marginals, model: Any, # MarkovRandomField ): """Implements a "Fixed" algorithm over ordinal data. The algorithm estimates fixed cliques. :param input_domain: domain of input data :param input_metric: how to compute distance between datasets :param output_measure: how to measure privacy of release :param d_in: distance between adjacent input datasets :param d_out: upper bound on the privacy loss :param marginals: prior marginal releases :param model: warm-start fit of MarkovRandomField """ import_optional_dependency("mbi") from mbi import Domain, MarkovRandomField # type: ignore[import-untyped,import-not-found] if model is not None and not isinstance(model, MarkovRandomField): raise ValueError("model must be a MarkovRandomField") lp_metric = get_associated_metric(output_measure) cliques = [q.by for q in self.queries] weights = [q.weight for q in self.queries] def make(scale: float) -> Measurement: return make_stable_marginals( input_domain, input_metric, lp_metric, cliques, # type: ignore[arg-type] ) >> then_noise_marginals(output_measure, cliques, scale, weights) # type: ignore[return-type] m_marginals = binary_search_chain(make, d_in, d_out, T=float) def function( new_releases: list, ) -> tuple[Marginals, MarkovRandomField]: all_marginals = marginals.add(*new_releases) if model is None: model_domain = Domain.fromdict(get_cardinalities(input_domain)) potentials = None else: model_domain = model.domain potentials = model.potentials new_model = self.estimator( model_domain, all_marginals.flatten(), potentials=potentials, ) return all_marginals, new_model return m_marginals >> _new_pure_function(function)