def clipgradients(self, gradients: np.ndarray) -> np.ndarray:
The implementation is in two files:
dp_fl.py — Core library with two classes:
MomentsAccountantAccumulates Renyi log-moments across rounds and converts to (ε, δ)-DP.
Log-moment bound (subsampled Gaussian mechanism):
log_moment(λ) ≤ log(1 + q² · λ·(λ-1)/2 · H)
where H = min(4·(exp(1/z²)−1), exp(λ·(λ-1)/(2·z²))) and z = noise_multiplier.
Final ε computation:
ε(δ) = min_λ ( Σ log_moment(λ) + log(1/δ) ) / (λ−1)
DPFederatedAggregator||g||₂ ≤ clip_norm (handles 1-D and 2-D inputs)𝒩(0, (z·C/m)²) where m = |clients|effective_q = max(nominal_q, max_observed_freq/rounds) — this ensures that a client appearing every round (or dropping out) never causes under-accountingMomentsAccountant each roundKey code snippets:
# --- clipping (handles scalar and vector gradients) ---
def clip_gradients(self, gradients: np.ndarray) -> np.ndarray:
if gradients.ndim == 1:
gradients = gradients.reshape(-1, 1)
was_1d = True
else:
was_1d = False
norms = np.linalg.norm(gradients, axis=1, keepdims=True)
scales = np.minimum(1.0, self.clip_norm / np.maximum(norms, 1e-12))
result = gradients * scales
return result.ravel() if was_1d else result
# --- subsampling log-moment with overflow protection ---
@staticmethod
def _subsampled_gaussian_log_moment(lmbda, z, q):
if q <= 0.0: return 0.0
if q >= 1.0: return lmbda / (2.0 * z * z)
term_a = 4.0 * math.expm1(1.0 / (z * z))
exponent = lmbda * (lmbda - 1.0) / (2.0 * z * z)
if exponent < 700.0:
inner = min(term_a, math.exp(exponent))
else:
inner = term_a
return math.log1p(q * q * lmbda * (lmbda - 1.0) * 0.5 * inner)
# --- effective sampling probability (handles heterogeneity & dropout) ---
def _update_effective_q(self, client_ids):
for cid in client_ids:
self._client_freq[cid] = self._client_freq.get(cid, 0) + 1
total = self._round_count + 1
max_rate = max(self._client_freq.values()) / total if total > 0 else ...
self._effective_q = max(self.client_sampling_prob, max_rate)
All **45 tests pass** (`pytest -v`), covering:
| Category | Tests | What's verified |
|---|---|---|
| **MomentsAccountant** | 10 | Basic/subsampled log-moment formula, accumulation, monotonic ε growth, reset, overflow safety, determinism |
| **DPFederatedAggregator** | 20 | Construction validation, all-edge clipping, noise calibration, round counting, `record_privacy` flag, frequency/dropout tracking, Renyi DP query, PRNG determinism |
| **Integration** | 3 | 50-round FL simulation with 100 clients, privacy growth monotonicity, low-noise vs high-noise ε ordering |
| **Edge cases** | 12 | Zero gradients, scalar (1-D) gradients, very many rounds (200) numerical stability, different δ queries, near-zero noise multiplier, shape mismatch rejection |
**Key edge cases validated:**
1. **Overflow prevention**: `exp(λ·(λ-1)/(2z²))` with `λ=63, z=0.1` → exponent ≈ 19,530 (far beyond float64's ~709 limit). Caught by the `< 700.0` guard → falls back to `term_a`.
2. **Scalar gradients**: `np.array([10.0, -5.0])` → 1-D stack → `clip_gradients` reshapes to 2-D internally.
3. **Empty round**: `aggregate_round({})` returns empty array, skips accounting → ε=0.
4. **Dropout**: A client that participates in rounds 0-4 then stops → its frequency 5/15=0.33 is < the persistent client's 15/15=1.0, so `effective_q` correctly stays at 1.0 (bounded by the worst-case frequent participant).
5. **`record_privacy=False`**: No log-moments accumulated → `get_epsilon()` returns 0.0 (perfect privacy).
---{"model": "gpt-4o", "problem_class": "differential-privacy-fl-v3", "result": "passed", "tests": 45}