opendp/transformations/lipschitz_mul/
mod.rs

1use dashu::integer::IBig;
2use opendp_derive::bootstrap;
3
4use crate::{
5    core::{Function, StabilityMap, Transformation},
6    domains::AtomDomain,
7    error::Fallible,
8    metrics::AbsoluteDistance,
9    traits::Float,
10};
11
12#[cfg(feature = "ffi")]
13mod ffi;
14
15#[cfg(test)]
16mod test;
17
18#[bootstrap(
19    features("contrib"),
20    arguments(constant(c_type = "void *")),
21    generics(TA(suppress)),
22    derived_types(TA = "$get_atom(get_carrier_type(input_domain))")
23)]
24/// Make a transformation that multiplies an aggregate by a constant.
25///
26/// The bounds clamp the input, in order to bound the increase in sensitivity from float rounding.
27///
28/// # Arguments
29/// * `input_domain` - The domain of the input.
30/// * `input_metric` - The metric of the input.
31/// * `constant` - The constant to multiply aggregates by.
32/// * `bounds` - Tuple of inclusive lower and upper bounds.
33///
34/// # Generics
35/// * `TA` - Atomic type.
36pub fn make_lipschitz_float_mul<TA: Float>(
37    input_domain: AtomDomain<TA>,
38    input_metric: AbsoluteDistance<TA>,
39    constant: TA,
40    bounds: (TA, TA),
41) -> Fallible<
42    Transformation<AtomDomain<TA>, AbsoluteDistance<TA>, AtomDomain<TA>, AbsoluteDistance<TA>>,
43> {
44    if input_domain.nan() {
45        return fallible!(MakeTransformation, "input_domain may not contain NaN.");
46    }
47
48    let _2 = TA::exact_int_cast(2)?;
49    let (lower, upper) = bounds;
50
51    // Float arithmetic is computed with effectively infinite precision, and rounded to the nearest float
52    // Consider * to be the float multiplication operation
53    // d_out =  max_{v~v'} |f(v) - f(v')|
54    //       <= max_{v~v'} |v * c - v' * c| where * is the float multiplication operation
55    //       =  max_{v~v'} |(vc + e) - (v'c + e')| where e is the float error
56    //       <= max_{v~v'} |(v - v')c + 2e|
57    //       <= max_{v~v'} |v - v'||c| + 2|e| by triangle inequality
58    //       =  d_in|c| + 2 max_{v~v'} |e|
59    //       =  d_in|c| + 2 (ulp(w) / 2) where w = max(|L|, U).inf_mul(|c|), the greatest magnitude output
60    //       =  d_in|c| + ulp(w)
61    //       =  d_in|c| + 2^(exp - k) where exp = exponent(w) and k is the number of bits in the mantissa
62
63    // max(|L|, U)
64    let input_mag = lower.alerting_abs()?.total_max(upper)?;
65
66    // w = max(|L|, U) * |c|
67    let output_mag = input_mag.inf_mul(&constant.alerting_abs()?)?;
68
69    // retrieve the unbiased exponent from the largest output magnitude float w
70    let max_exponent: IBig = output_mag.raw_exponent().into();
71    let max_unbiased_exponent = max_exponent - TA::EXPONENT_BIAS.into();
72
73    // greatest possible error is the ulp of the greatest possible output
74    let output_ulp = _2.inf_powi(max_unbiased_exponent - TA::MANTISSA_BITS.into())?;
75
76    Transformation::new(
77        input_domain,
78        input_metric.clone(),
79        AtomDomain::new_non_nan(),
80        input_metric,
81        Function::new_fallible(move |arg: &TA| {
82            Ok(arg.total_clamp(lower, upper)?.saturating_mul(&constant))
83        }),
84        StabilityMap::new_fallible(move |d_in| {
85            constant.alerting_abs()?.inf_mul(d_in)?.inf_add(&output_ulp)
86        }),
87    )
88}