From f19938a7b6036e2c1ec105803dcb87ca6b39d5ff Mon Sep 17 00:00:00 2001 From: ruv Date: Wed, 2 Sep 2026 02:15:33 -0400 Subject: [PATCH 01/27] docs(ruforecast): real zero-shot pretrained-checkpoint comparison beats naive baselines Chronos-Bolt (base/small), Chronos-T5-small, and TimesFM-2.5-200M run zero-shot (no fine-tuning) on the same BIDMC cross-entity test windows, baselines, and WQL/MAE metrics as the prior from-scratch RuForecast BIDMC result. Unlike every from-scratch RuForecast configuration tried this session, all four pretrained checkpoints beat both last-value and seasonal-naive baselines on WQL, on both independent patient-partition judges (12-42% margin), consistent with the massive-pretraining-corpus hypothesis from the prior literature pass. --- docs/benchmarks/ruforecast.md | 154 ++++++++++++++ scripts/ruforecast-zeroshot/bidmc_windows.py | 160 +++++++++++++++ .../ruforecast-zeroshot/run_zeroshot_eval.py | 193 ++++++++++++++++++ 3 files changed, 507 insertions(+) create mode 100644 scripts/ruforecast-zeroshot/bidmc_windows.py create mode 100644 scripts/ruforecast-zeroshot/run_zeroshot_eval.py diff --git a/docs/benchmarks/ruforecast.md b/docs/benchmarks/ruforecast.md index a8393c53e..f032ac7d6 100644 --- a/docs/benchmarks/ruforecast.md +++ b/docs/benchmarks/ruforecast.md @@ -378,3 +378,157 @@ Reproducer: `v2/crates/ruforecast/crates/ruforecast-train/examples/bidmc_prepare one 76-row (context 64 + horizon 12) window per eligible patient, and writes governed `train.jsonl`/`test.jsonl`/`train-local.toml` per judge split. Raw data cached at `/tmp/bidmc-raw/` on the training host only. + +### Zero-shot pretrained-checkpoint comparison (2026-09-02) + +Every RuForecast result above this line is a **from-scratch-trained** model +(RuForecast's own Burn/Rust architecture, trained on a tiny real or synthetic +corpus) evaluated against trivial baselines. All of them lost. A literature +pass after the BIDMC result above (arXiv:2505.11349, "context parroting"; +arXiv:2607.20027, zero-shot HRV forecasting with TimesFM/Chronos/MOIRAI on +wearables) converged on an explanation: published foundation-model wins over +naive baselines on short-horizon, smooth physiological signals come from +**massive real pretraining corpora** (Chronos: ~10M real+augmented series; +TimesFM: ~100B real time points), not from architecture alone — and +RuForecast's from-scratch training used a real corpus many orders of +magnitude smaller (tens to low hundreds of windows), so re-implementing +these architectures from scratch and training small was never going to +replicate their wins. + +This test checks the other half of that hypothesis directly: does a **real, +public, pretrained checkpoint**, run **zero-shot** (no fine-tuning, no +training of any kind on this repo's data) on the **exact same BIDMC +cross-entity test windows and metrics** as the BIDMC result above, actually +beat the naive baselines? If the massive-pretraining-corpus explanation is +right, it should — unlike every from-scratch attempt this session. + +**Setup (kept identical to the BIDMC test above for a fair comparison):** +same `/tmp/bidmc-raw/` cache (53 real ICU patients, PhysioNet BIDMC, Open +Data Commons Attribution License v1.0), same window construction (context +64s + horizon 12s, 1 Hz, variates HR/RESP/SpO2, one window per patient from +that patient's first 76 rows), same two disjoint patient-partition judges (A: +train 1-37/test 38-53 contiguous; B: train odd/test even interleaved — train +patients are irrelevant here since nothing is trained, only the *test* +partition is used), same quantile set +`[0.05,0.10,0.25,0.50,0.75,0.90,0.95]`, same seasonal period (12), and the +same WQL formula (`2*sum(pinball)/(numQuantiles*sum|actual|)`) and MAE +definition as `ruforecast-core::metrics.rs`. Baselines were independently +re-derived from `ruforecast-core::baseline.rs`'s exact logic in Python and +cross-checked against the BIDMC section above: reproduced last-value WQL was +0.01171 (A) / 0.01013 (B) versus the Rust CLI's originally reported +0.01159 / 0.01002 — a ~1% gap attributable to f32 (Rust) vs f64 (Python) +accumulation, not a methodology difference, so this reproduction is treated +as validated. + +Four real, public, pretrained-checkpoint models were run zero-shot on GPU +(RTX 5080, `ruvultra`), per-variate univariate (none of these checkpoints has +a native multivariate mode for these two families, so HR/RESP/SpO2 were +forecast independently and recombined into the same window-level WQL/MAE as +the baselines and the from-scratch RuForecast model): + +- **Chronos-Bolt base/small** (`amazon/chronos-bolt-base`, + `amazon/chronos-bolt-small`) — direct quantile-regression heads, + deterministic given input (no sampling). +- **Chronos (T5) small** (`amazon/chronos-t5-small`) — the original + sampling-based Chronos family; `predict_quantiles` internally draws + samples (default `num_samples`), so this one model's numbers below carry + run-to-run sampling variance the other three don't. +- **TimesFM 2.5 (200M, PyTorch)** (`google/timesfm-2.5-200m-pytorch`) — + quantile output uses 10 channels; per the installed package's source + (`timesfm_2p5_base.py`, `decode_index: int = 5`) and empirical + verification across multiple windows/variates, channel 5 is the point + forecast and channels 1-9 are the native quantile levels [0.1..0.9] in + order (channel 0 is an unused duplicate, not a real quantile). Levels 0.05 + and 0.95 were clamped to the nearest native level (0.10/0.90); 0.25 and + 0.75 were linearly interpolated between the adjacent native levels — the + same class of approximation Chronos-Bolt's own pipeline applies + internally when asked for quantile levels outside its trained range (a + warning it printed for 0.05/0.95 on every call). + +| Judge | Model | Test windows | MAE | WQL | WQL vs last-value | +|---|---|---:|---:|---:|---:| +| A | last-value (baseline) | 16 | 0.7882 | 0.01171 | — | +| A | seasonal-naive (baseline) | 16 | 1.0885 | 0.01617 | worse | +| A | Chronos-Bolt-base (zero-shot) | 16 | 0.7802 | 0.00756 | **-35.4%** | +| A | Chronos-Bolt-small (zero-shot) | 16 | 0.7212 | 0.00684 | **-41.6%** | +| A | Chronos-T5-small (zero-shot) | 16 | 0.9487 | 0.00955 | **-18.4%** | +| A | TimesFM-2.5-200M (zero-shot) | 16 | 0.7490 | 0.00731 | **-37.6%** | +| B | last-value (baseline) | 26 | 0.6944 | 0.01013 | — | +| B | seasonal-naive (baseline) | 26 | 0.8697 | 0.01269 | worse | +| B | Chronos-Bolt-base (zero-shot) | 26 | 0.7096 | 0.00719 | **-29.0%** | +| B | Chronos-Bolt-small (zero-shot) | 26 | 0.6852 | 0.00676 | **-33.3%** | +| B | Chronos-T5-small (zero-shot) | 26 | 0.8254 | 0.00886 | **-12.5%** | +| B | TimesFM-2.5-200M (zero-shot) | 26 | 0.6511 | 0.00698 | **-31.1%** | + +**Honest result: this is the first test all session where a model beats the +trivial baselines, and it does so decisively and consistently** — all four +pretrained checkpoints beat both last-value and seasonal-naive on WQL, on +both independently-partitioned judges, by double-digit-to-40% margins. This +is a materially larger and more consistent margin than the closest published +direct analog found in the literature pass (zero-shot HRV forecasting, +arXiv:2607.20027: Chronos beat EWMA by ~3%, MOIRAI showed no significant +advantage over a mean baseline, p=0.953) — plausibly because BIDMC's 1 Hz +ICU HR/RESP/SpO2 signals are smoother and more autocorrelated than the HRV +series in that paper, which is itself a fair, disclosed difference between +the two comparisons rather than a claim of a stronger general result. + +On MAE (point accuracy) the picture is more mixed: Chronos-Bolt-small and +TimesFM beat last-value's MAE on both judges, but Chronos-Bolt-base and +Chronos-T5-small do not consistently (e.g. Chronos-Bolt-base's MAE is +essentially tied with last-value on judge A, worse on judge B, despite a +much better WQL there) — the pretrained models' advantage is concentrated in +calibrated quantile spread (what WQL scores), not in shifting the central +point forecast much past what last-value already gets from strong +autocorrelation in these signals. + +**This directly confirms the massive-pretraining-corpus hypothesis from the +literature pass**: the same short-horizon, smooth physiological-signal task +on which every from-scratch RuForecast configuration this session lost to +naive baselines is won, cleanly, by checkpoints that carry a pretraining +corpus many orders of magnitude larger than any real dataset available to +this project. This is evidence *for* zero-shot/foundation-model inference +(or later, fine-tuning a pretrained checkpoint) as the credible next lever +for RuForecast-style short-horizon vitals forecasting — not for continuing +to scale from-scratch training on this project's own small real corpora, +which the BIDMC and household tests above already showed does not close +this gap even with genuine, cross-entity real data. + +**Scope and caveats, stated plainly:** +- This is zero-shot inference only — no fine-tuning was attempted on any + checkpoint. A fine-tuned comparison (e.g. LoRA-adapting Chronos-Bolt on + RuForecast's own training windows) is a natural, not-yet-run follow-up. +- Univariate-per-variate forecasting was used for all four models because + neither Chronos nor TimesFM in these releases has a native multivariate + mode matching RuForecast's joint HR/RESP/SpO2 modeling; a true + multivariate foundation model (e.g. MOIRAI, which does support cross-variate + attention) was not tested here and is a natural next step. +- Chronos-T5-small's `predict_quantiles` samples internally (unlike + Chronos-Bolt's direct quantile heads or TimesFM's quantile head); its + numbers above are one run and carry unquantified sampling variance the + other three models' numbers don't. +- No inference-cost/latency comparison against RuForecast's own (much + smaller, ~tens-of-KB) model is included; all four pretrained checkpoints + are orders of magnitude larger (small-to-200M+ parameters) and ran on a + discrete GPU (RTX 5080), not the ESP32-class edge target RuForecast's own + architecture is designed for. A model that wins on WQL here is not + automatically deployable to the same edge budget RuForecast targets — that + tradeoff is unresolved by this test and is itself a reason zero-shot + inference (call out to a hosted/cloud checkpoint) and from-scratch + small-model training remain different tools for different deployment + constraints, not a strict replacement of one by the other. + +Reproducer: `scripts/ruforecast-zeroshot/bidmc_windows.py` (window builder, +baselines, and metrics — ported line-for-line from +`ruforecast-train/examples/bidmc_prepare.rs` and +`ruforecast-core/src/{baseline,metrics}.rs`, verified against this document's +own BIDMC last-value/seasonal-naive numbers) and +`scripts/ruforecast-zeroshot/run_zeroshot_eval.py` (zero-shot inference and +scoring), both in this worktree (`research/ruforecast-zeroshot-eval`). Run +against `/tmp/bidmc-raw/` (downloaded fresh from PhysioNet if not already +cached) with `python run_zeroshot_eval.py --models chronos-bolt-base +chronos-bolt-small chronos-t5-small timesfm-2.5-200m --judges a b` inside a +venv with `chronos-forecasting`, `timesfm`, and a CUDA-enabled `torch` +installed (`amazon/chronos-*` and `google/timesfm-2.5-200m-pytorch` download +automatically from HuggingFace Hub on first run; no HF token was required). +Model weights and the raw BIDMC CSVs are not committed — `/tmp/bidmc-raw/` +and the HuggingFace cache live only on the host that ran this evaluation. diff --git a/scripts/ruforecast-zeroshot/bidmc_windows.py b/scripts/ruforecast-zeroshot/bidmc_windows.py new file mode 100644 index 000000000..2c4348fc5 --- /dev/null +++ b/scripts/ruforecast-zeroshot/bidmc_windows.py @@ -0,0 +1,160 @@ +""" +Faithful Python port of the window/baseline/metric definitions in +v2/crates/ruforecast/crates/ruforecast-train/examples/bidmc_prepare.rs and +v2/crates/ruforecast/crates/ruforecast-core/src/{baseline,metrics}.rs so the +zero-shot pretrained-model comparison is apples-to-apples with RuForecast's +own real-data evaluation. + +CONTEXT=64, HORIZON=12, VARIATES=3 (HR, RESP, SpO2), quantiles as in +ForecastModelConfig::tiny_ci(), seasonal_period=12 (the `evaluate` CLI default). +""" +import csv +import math +import os + +CONTEXT = 64 +HORIZON = 12 +WINDOW_ROWS = CONTEXT + HORIZON +VARIATE_NAMES = ["HR", "RESP", "SpO2"] +QUANTILES = [0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95] +SEASONAL_PERIOD = 12 +RAW_DIR = "/tmp/bidmc-raw" + + +def read_patient(pid: int): + path = os.path.join(RAW_DIR, f"bidmc_{pid:02d}_Numerics.csv") + if not os.path.exists(path): + return None + rows = [] + with open(path, newline="") as f: + reader = csv.reader(f) + next(reader) # header + for parts in reader: + if len(parts) < 5: + continue + parts = [p.strip() for p in parts] + try: + hr = float(parts[1]) + resp = float(parts[3]) + spo2 = float(parts[4]) + except ValueError: + return None # malformed row -> treat like the Rust `?` early-return + rows.append((hr, resp, spo2)) + return rows + + +def build_window(pid: int): + """One window per patient: first CONTEXT rows are context, next HORIZON + rows are targets. Mirrors build_window() in bidmc_prepare.rs exactly + (context_start = row 0, no scanning for a "best" window).""" + rows = read_patient(pid) + if rows is None or len(rows) < WINDOW_ROWS: + return None + first = rows[:WINDOW_ROWS] + for row in first: + for v in row: + if not math.isfinite(v): + return None + context = first[:CONTEXT] # list of (hr, resp, spo2), CONTEXT rows + targets = first[CONTEXT:WINDOW_ROWS] # HORIZON rows + return {"patient": pid, "context": context, "targets": targets} + + +def judge_split(judge: str): + if judge == "a": + train_ids = list(range(1, 38)) + test_ids = list(range(38, 54)) + elif judge == "b": + train_ids = [i for i in range(1, 54) if i % 2 == 1] + test_ids = [i for i in range(1, 54) if i % 2 == 0] + else: + raise ValueError(judge) + return train_ids, test_ids + + +def build_test_windows(judge: str): + _, test_ids = judge_split(judge) + windows = [] + excluded = [] + for pid in test_ids: + w = build_window(pid) + if w is None: + excluded.append(pid) + else: + windows.append(w) + return windows, excluded + + +# ---- baselines (mirror ruforecast-core::baseline.rs) ---- + +def last_value_forecast(context): + """Repeat the last observed context row for every future step (every + quantile gets the same point value -> degenerate distribution).""" + last = context[-1] + return [[last[v] for _ in QUANTILES] for _ in range(HORIZON) for v in range(len(last))] + # NOTE: shape handled explicitly in metric functions below instead; see + # last_value_predictions() for the actually-used [horizon][variate] form. + + +def last_value_predictions(context): + last = context[-1] + return [[last[v] for v in range(len(last))] for _ in range(HORIZON)] + + +def seasonal_naive_predictions(context, period=SEASONAL_PERIOD): + """context has CONTEXT=64 rows; base = len(context) - period; for step in + 0..HORIZON: row = base + step % period.""" + base = len(context) - period + preds = [] + for step in range(HORIZON): + row = base + (step % period) + preds.append(list(context[row])) + return preds + + +# ---- metrics (mirror ruforecast-core::metrics.rs) ---- + +def mae(actual_flat, predicted_flat): + total, count = 0.0, 0 + for a, p in zip(actual_flat, predicted_flat): + total += abs(a - p) + count += 1 + return total / count if count else float("nan") + + +def weighted_quantile_loss(actual_flat, quantile_preds_flat): + """actual_flat: list of HORIZON*VARIATES actuals in step-major order + (step0-var0,step0-var1,step0-var2, step1-var0, ...). + quantile_preds_flat: same order, each entry is a list over QUANTILES. + Matches weighted_quantile_loss() in metrics.rs: sum pinball over every + (cell, quantile), normalize by 2 / (num_quantiles * sum(|actual|)).""" + loss = 0.0 + scale = 0.0 + for actual, preds in zip(actual_flat, quantile_preds_flat): + scale += abs(actual) + for q, pred in zip(QUANTILES, preds): + residual = actual - pred + loss += q * residual if residual >= 0 else (q - 1.0) * residual + if scale == 0.0: + raise ValueError("weighted quantile loss undefined: all-zero actual targets") + return 2.0 * loss / (len(QUANTILES) * scale) + + +def flatten_targets_step_major(targets): + """targets: HORIZON rows of VARIATES values -> flat step-major list.""" + out = [] + for row in targets: + out.extend(row) + return out + + +def point_to_quantile_preds(point_preds): + """point_preds: HORIZON rows of VARIATES point values (deterministic + baseline) -> step-major flat list where every quantile equals the point + value, matching how LastValue/SeasonalNaive fill every quantile slot + with the same repeated value.""" + out = [] + for row in point_preds: + for v in row: + out.append([v for _ in QUANTILES]) + return out diff --git a/scripts/ruforecast-zeroshot/run_zeroshot_eval.py b/scripts/ruforecast-zeroshot/run_zeroshot_eval.py new file mode 100644 index 000000000..051b53581 --- /dev/null +++ b/scripts/ruforecast-zeroshot/run_zeroshot_eval.py @@ -0,0 +1,193 @@ +""" +Real zero-shot pretrained-checkpoint comparison for RuForecast (see +docs/benchmarks/ruforecast.md). Runs Amazon Chronos-Bolt (and TimesFM, if +installed) zero-shot inference against the SAME BIDMC test windows, +baselines, and metrics RuForecast's own `evaluate` CLI uses, and prints a +markdown table. + +No fine-tuning. No training. Every "model" forecast below comes from a +public pretrained checkpoint's zero-shot .predict()/.forecast() call. +""" +import sys +import time +import numpy as np + +sys.path.insert(0, "/tmp") # bidmc_windows.py copied there +from bidmc_windows import ( + CONTEXT, HORIZON, VARIATE_NAMES, QUANTILES, SEASONAL_PERIOD, + build_test_windows, judge_split, + last_value_predictions, seasonal_naive_predictions, + mae, weighted_quantile_loss, flatten_targets_step_major, point_to_quantile_preds, +) + + +def eval_baselines(windows): + """Returns dict: name -> (mae, wql) aggregated over ALL test windows and + variates, matching how the Rust evaluate CLI aggregates (one running + mean/scale accumulation across the whole test set, not averaged per + window).""" + results = {} + for name, fn in [("last-value", last_value_predictions), + ("seasonal-naive", lambda ctx: seasonal_naive_predictions(ctx, SEASONAL_PERIOD))]: + all_actual, all_point_pred, all_qpred = [], [], [] + for w in windows: + preds = fn(w["context"]) # HORIZON rows x VARIATES + actual_flat = flatten_targets_step_major(w["targets"]) + point_flat = flatten_targets_step_major(preds) + qpred_flat = point_to_quantile_preds(preds) + all_actual.extend(actual_flat) + all_point_pred.extend(point_flat) + all_qpred.extend(qpred_flat) + results[name] = (mae(all_actual, all_point_pred), weighted_quantile_loss(all_actual, all_qpred)) + return results + + +def chronos_zero_shot_predictions(windows, model_name="amazon/chronos-bolt-base", device="cuda"): + """Per-variate univariate zero-shot forecast (Chronos has no native + multivariate mode) via ChronosPipeline. Returns list parallel to + `windows`, each entry: HORIZON rows x VARIATES quantile-lists.""" + from chronos import BaseChronosPipeline + pipeline = BaseChronosPipeline.from_pretrained(model_name, device_map=device, torch_dtype="bfloat16" if device == "cuda" else "float32") + import torch + + all_preds = [] + for w in windows: + ctx = w["context"] # HORIZON... actually CONTEXT rows x VARIATES + per_variate_q = [] + for v in range(len(VARIATE_NAMES)): + series = torch.tensor([row[v] for row in ctx], dtype=torch.float32) + quantiles, _mean = pipeline.predict_quantiles( + inputs=series, + prediction_length=HORIZON, + quantile_levels=QUANTILES, + ) + # quantiles shape: [1, HORIZON, len(QUANTILES)] + per_variate_q.append(quantiles[0].numpy()) # [HORIZON, Q] + # reshape to HORIZON rows, each VARIATES entries of Q-length lists + rows = [] + for h in range(HORIZON): + row = [per_variate_q[v][h].tolist() for v in range(len(VARIATE_NAMES))] + rows.append(row) + all_preds.append(rows) + return all_preds + + +def timesfm_zero_shot_predictions(windows, model_repo="google/timesfm-2.5-200m-pytorch"): + """Per-variate univariate zero-shot forecast via TimesFM 2.5 200M torch. + Quantile output has 10 columns; per the installed package's source + (timesfm_2p5_base.py: `decode_index: int = 5`), column 5 is the + point/mean forecast and columns 1..9 are quantiles [0.1..0.9] in order + (verified empirically: columns 1-9 are monotonic and column 5 exactly + equals the returned point forecast on every sampled window/variate; + column 0 is an unused duplicate channel, not a real quantile level). + TimesFM has no 0.05/0.95 levels (clamped to 0.1/0.9, same treatment + Chronos-Bolt applies for out-of-range levels) and no native 0.25/0.75 + (linearly interpolated between the adjacent 0.1-spaced levels it does + have).""" + import timesfm + import numpy as np + + m = timesfm.TimesFM_2p5_200M_torch.from_pretrained(model_repo) + m.compile(timesfm.ForecastConfig( + max_context=CONTEXT, max_horizon=HORIZON, normalize_inputs=True, + use_continuous_quantile_head=True, fix_quantile_crossing=True, + )) + native_levels = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] # columns 1..9 + + def sample_at(row10, level): + if level <= native_levels[0]: + return row10[1] + if level >= native_levels[-1]: + return row10[9] + for i in range(len(native_levels) - 1): + lo, hi = native_levels[i], native_levels[i + 1] + if lo <= level <= hi: + col_lo, col_hi = 1 + i, 1 + i + 1 + if hi == lo: + return row10[col_lo] + frac = (level - lo) / (hi - lo) + return row10[col_lo] + frac * (row10[col_hi] - row10[col_lo]) + raise ValueError(level) + + all_preds = [] + for w in windows: + ctx = w["context"] + per_variate_rows = [] + for v in range(len(VARIATE_NAMES)): + series = np.array([row[v] for row in ctx], dtype=np.float32) + _point, quant = m.forecast(horizon=HORIZON, inputs=[series]) + per_variate_rows.append(quant[0]) # [HORIZON, 10] + rows = [] + for h in range(HORIZON): + row = [] + for v in range(len(VARIATE_NAMES)): + row10 = per_variate_rows[v][h] + row.append([sample_at(row10, q) for q in QUANTILES]) + rows.append(row) + all_preds.append(rows) + return all_preds + + +def eval_pretrained(name, predict_fn, windows): + t0 = time.time() + preds = predict_fn(windows) + elapsed = time.time() - t0 + all_actual, all_point_pred, all_qpred = [], [], [] + median_idx = QUANTILES.index(0.50) + for w, pred_rows in zip(windows, preds): + actual_flat = flatten_targets_step_major(w["targets"]) + qpred_flat = [] + point_flat = [] + for row in pred_rows: # HORIZON rows, each VARIATES quantile-lists + for var_qs in row: + qpred_flat.append(list(var_qs)) + point_flat.append(var_qs[median_idx]) + all_actual.extend(actual_flat) + all_point_pred.extend(point_flat) + all_qpred.extend(qpred_flat) + m = mae(all_actual, all_point_pred) + wql = weighted_quantile_loss(all_actual, all_qpred) + return m, wql, elapsed + + +def main(): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("--models", nargs="+", default=["chronos-bolt-base"]) + parser.add_argument("--judges", nargs="+", default=["a", "b"]) + args = parser.parse_args() + + print("| Judge | Model | Test windows | MAE | WQL | Wall time (s) |") + print("|---|---|---:|---:|---:|---:|") + + for judge in args.judges: + windows, excluded = build_test_windows(judge) + train_ids, test_ids = judge_split(judge) + print(f"", file=sys.stderr) + + baselines = eval_baselines(windows) + for name, (m, wql) in baselines.items(): + print(f"| {judge} | {name} (baseline) | {len(windows)} | {m:.4f} | {wql:.5f} | - |") + + for model in args.models: + if model == "chronos-bolt-base": + fn = lambda w: chronos_zero_shot_predictions(w, "amazon/chronos-bolt-base") + elif model == "chronos-bolt-small": + fn = lambda w: chronos_zero_shot_predictions(w, "amazon/chronos-bolt-small") + elif model == "chronos-t5-small": + fn = lambda w: chronos_zero_shot_predictions(w, "amazon/chronos-t5-small") + elif model == "timesfm-2.5-200m": + fn = timesfm_zero_shot_predictions + else: + print(f"unknown model {model}", file=sys.stderr) + continue + try: + m, wql, elapsed = eval_pretrained(model, fn, windows) + print(f"| {judge} | {model} (zero-shot) | {len(windows)} | {m:.4f} | {wql:.5f} | {elapsed:.1f} |") + except Exception as e: + print(f"| {judge} | {model} (zero-shot) | {len(windows)} | FAILED | FAILED | - |") + print(f"ERROR running {model} on judge {judge}: {e}", file=sys.stderr) + + +if __name__ == "__main__": + main() From 5c50fa8588b3db3979ae40f8019078042bdc2820 Mon Sep 17 00:00:00 2001 From: ruv Date: Wed, 2 Sep 2026 08:45:23 -0400 Subject: [PATCH 02/27] feat: train real fal.ai synthetic visual teacher LoRA (ADR-353) Trains a real fal.ai LoRA fine-tune (fal-ai/flux-lora-fast-training) on frames extracted from the real fal.ai-generated room-visualization MP4s already captured this session, so display-layer generation has a specific trained style target instead of prompt engineering against a stock model. Architecture boundary: this model only ever produces supplementary synthetic visual/pixel output. It has no path into measured/derived RF, pose, vitals, identity, or confidence values, which remain local and authoritative. Real run: request 01a060b2-c940-7972-abf9-ea48847eb2ae, 1000 steps, 27 training images, ~$2.04 spend, validated with a real inference call against the trained LoRA (assets/fal-visual-teacher-sample.png). --- .gitignore | 6 + assets/fal-visual-teacher-sample.json | 7 + assets/fal-visual-teacher-sample.png | Bin 0 -> 599210 bytes docs/adr/ADR-353-fal-visual-teacher-lora.md | 162 +++++++++++++++++++ scripts/fal-visual-teacher/extract_frames.py | 99 ++++++++++++ scripts/fal-visual-teacher/train_lora.py | 124 ++++++++++++++ scripts/fal-visual-teacher/validate.py | 95 +++++++++++ 7 files changed, 493 insertions(+) create mode 100644 assets/fal-visual-teacher-sample.json create mode 100644 assets/fal-visual-teacher-sample.png create mode 100644 docs/adr/ADR-353-fal-visual-teacher-lora.md create mode 100644 scripts/fal-visual-teacher/extract_frames.py create mode 100644 scripts/fal-visual-teacher/train_lora.py create mode 100644 scripts/fal-visual-teacher/validate.py diff --git a/.gitignore b/.gitignore index 00b2441a2..5197ca4dc 100644 --- a/.gitignore +++ b/.gitignore @@ -311,3 +311,9 @@ v2/crates/wifi-densepose-sensing-server/data/ # ADR-324: wasm-bindgen output for ruview-offaxis is generated locally # (see the crate README); never commit generated artifacts. v2/crates/ruview-offaxis/pkg/ + +# fal.ai visual-teacher LoRA training data/weights -- regenerable, never +# commit training images or model weights (ADR-353) +scripts/fal-visual-teacher/training-data/ +scripts/fal-visual-teacher/training-data.zip +scripts/fal-visual-teacher/output/ diff --git a/assets/fal-visual-teacher-sample.json b/assets/fal-visual-teacher-sample.json new file mode 100644 index 000000000..3a4a28315 --- /dev/null +++ b/assets/fal-visual-teacher-sample.json @@ -0,0 +1,7 @@ +{ + "endpoint": "fal-ai/flux-lora", + "lora_url": "https://v3b.fal.media/files/b/0aa8c60d/OjaBs6Gi8pMhZqMo79iym_pytorch_lora_weights.safetensors", + "prompt": "ruviewstyle room visualization, a living room with a glowing cyan pose skeleton overlay, wifi signal arcs radiating across the space, a vitals readout panel, dark tech aesthetic, synthetic sensor visualization", + "scale": 1.0, + "source_image_url": "https://v3b.fal.media/files/b/0aa8cf67/JH1qyC3GCKQ9ojkrAUXe9.png" +} \ No newline at end of file diff --git a/assets/fal-visual-teacher-sample.png b/assets/fal-visual-teacher-sample.png new file mode 100644 index 0000000000000000000000000000000000000000..011e3ae41da29b65608e5cf045eb3b8dc9d3c2a9 GIT binary patch literal 599210 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
yZG7xEgXGW9vJX$}t(7Ag?Dl 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

;VWMFZ`@rkVh9zI7nKae1* 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

|Or#U!C*Q;Nx+6`$U`qBeynapquy)u#?I2WQQA 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�~%%8RdO}itKAB-H9c@W&WJnm^mPF zBnZ(tMi{6}OvU3;Z)u073(H2fELsRa3`ZAoK>%P)3K$9QAQ?!-;3$T;l)Vi&nZTk) z5M;|P=GlX2beBdmW@ceEUzpm6cW?aOd{mxg#NOV=3G1?)|$DyX|(ryVW`idugl*k$8SOIEfK^=;fyz zFoTljA`y2(;g*Fti!hl<*>}y4WfB}a5p`3DG54)goe$&j$&pBGs%o{887p(0Y|{Pb z%Hv_P9_Va!d%K^kZcaAS{-9&ks=_i?8&*YwSfuoFOk%#t&9D@w4dZke>xbR=>D|ZE zv$MyKAAkAs>!**OeZKf~dHMM;?RB21)~Z!#CY_ncNSKP5f?`SNxnwdib-*1bwjOWJ zlmdf-h$yxJEKJ7aoIbX$=Ax|CX02A={r2-TjsNsd|NP@W{ZX~P`}J30rD|5q%L7Ai zE!`|*a{#PtqgWA0R^6g(fsh76?K~`j1rgXDxF*O7wcK&oy5|nsA-rak{K}sQ4Ky!_ z%a?NZ&cu=h%W2*&fB+YSgH5WKo*}0Q+DF+bfS7eYF$}rUr1sM281Hdovnbi!6;Q8E^V(nJ6n}L~$^9UR$bH$6(o#7=z z`YAOF6NpR~&N>L88;lliHOe&eDFj_i&EfRI>G6Y}F^3z9WyJ2H+t|Z98%H@^>WC2& zcJu0#7lxyjm1CU#Ixf!87Iy_z>~)tZDnt|ev)`?6{xn|EDgMe zx&jQU#uQZs;V^x+%@>m^CTJ)ox!{uUVoTMbrl9^p0m53&gGR`0&D-4>w)`U*a{mpc zKr&&uRItEw^)MsjPhyI7=}FIxJJ@V&vq_h@%~y0@f)PYU&k3_C&kXQPY^bI(jTDTRgn5LG5@0v0O-#5$Bo-%CMF zV;f~ldt@eNl2}Ys<0P*#td1T&d~oml!TH$<5#QY0Twh)7_xrx&ePoSNN295YGhoLrklMUcIOYz zk2Yl*q*g2I)jUn)lUhXv5|(16@KA_(o@ynQN;2tqd$_pzG+ylW_R=qYu7*rV%mfIadcYK{l9}Oj%4=WLBHTo5Sw*;?wc*+4=nk4<0^# z@Z{;uc6WJoaeH|+?r*hHAxm28r%CvUX&G-ISr-}_s2ir?$%|-$2$PZcU>cdPpCR?y zBX)P{9HM$b53QJNl8g6m{#)n|fB56~KmMrm^!e=@B4QSrtJ#DDq=bUb#T(c)7Czss zQWm?Xx-5Pc5E2=XK#O*%`vV5?3`k1nnqu&4moU^|5zvqt9eg%%#!{HP$SjAG>HtNW zH^Cq*_R>VREG2;lF;JXg`Nsp8ugg>O$js-*LJ!#!=TP8AaWYfgzdkx>xSHSv&DeAIPEXt- z%qWiK^4AiX#rfo>m_5Tt1`$BS6Xc%Ru$QH`go*t4!XO5zf1L}Xc)KB7s_e127=>ZF zf66U|&e5hYL-kD3W4Wt+g8kH~Zihi?wNqa<@iHQ~P zeEs<8v)_LH`Tgs^muV8KI!#5)NKJhU9YZAQZ}r%lm}AjlQ2#_>!hBauWfN4xTO!ON zsWuT3h><9X48yQKI=_Ga{)0zH$46T2^X2u;)#YKooeu}1>f5ub8ZlRQtwL&5k~n1u zl)!BSA$K_oZ!5Q3IwcV`@`KD{9za;hm`O>AieJP7ZMi%F$+6Z-zB{nSPyXVUbvhEh-_7HWNl7d0)HByJvsSId#+=6v#oR<^ISeA6$wO;c z84#$*`w&GLdUR8(G80#+MxWol`!maT-+%YVfBEM>7ykJ6Elm^CjICCTW7knu6hQ?~ z>GPgG=#l7MRzwh#2yLOxb8-8lO)Gcln6?cyHTm(4K(0n%m!I^lTB6sPC}fsFMci(y!ZEdB}`LK=fG-yBU-Bzct7 zz^VNR6|B4U2KtC3q=Vt(;VqjL>^K||#07eu;;<;4=w$>SYE7|0ml?8yFhL!>bto+C z#+_3K(*?Q_=aNZoW752Bix1lyqgjw`Wbr%!1J4RSXK?cnUJ~@idyEi*rLGf!fZ`f`~RVHRDQr5@!9z3}B;PK}8XgrMX-ha5hyqpdPoukFB zd9~tUHP@P(AQ74Pf>u=)7E;oBxW3`TVRP?%eRNDz#bO3Tt5WewHmhXDc6$Hp{=<9g z&C&V!iHX$R{z&Q5n|Jg6Kx$N#v0Dy|RtX0&gF-o-kNuj6Jg>=l!iJk_my2^tij0hK zVe&j;WmT=S%S5=0xAXS)>eHu>_Z~ia@bK}=ufDwh=;6nAZ@1SMmrd3!pb7-w$K{7iF^G?v+A5Pg#j`-2 z{^5^5x_NWXWSzV6W_ua>23<4AXYufiXEGO0$mVyh0~ec7mSc2ow7Jh5A0Lx|rI>lT zLzUpkjmWnZr^csaLSG;T%zyB5c zljkO42oItO3a1kAY;i2Aq8o9snVG3Zp`Ll!N{DJX-YtbgF}~~>#W*jCg8K!wmBfE3QH5grVfir}&$ySuS*nAlR{@O{HXC zyDr1)Q?KG(>!Z=3BoWQV*BwblO(*1PV4e9?%S5T>fCZQtqVD)Bkj@LJk?As~ND>%7c~7;}@^K`10$wuiw1==_j6cg=U+pi5QvsMxR9LX;(w8oG|H{ z{ff!bO2otxuGQeVt+9XaiyuMYzDgh%1o6^2B}m_ zSyQe1+x_O=JnF1Eo|Th&7G zI#*L;otcSQqkhZ^jK*TsG5{^~7Z?OD7yqXHA%?6TKZqav?ANKv%0yRYu z?|=O9hd=z`e@^q|$B(+NEQ1hN_4Vt@Qeq!fGU7Z%MPoyG^ZoKW>$LaDO|X%o_8dT&A)J+ z!%w&A5t^pP!T_e={hj0h&+ZOexfxlaeJ251Jin(G21stXUtnTW9>b;}NZUDu_T^7O zOfAap-BK`Fk0&^Ok{QSTnaKdKpsUwZcyB~I{#;TVJe;j!pB8{Vq*GUTcE(!>>CyPm z=OHYj0FAxuTsf>Y5j-%@Q}T9awmk>+PE=rRN?Y4(LmLiT9s+gjvZfXoD`sc=6MGErY9s@H4T=)Qv&h#8Kl4pNWS7^V3N;TP3XV0C{n# z<`cR^k~=y|v*ZLT4)2I!1kMBn^*O$QIQ={x&~NwKtOvd6hp!t;5I4lUjixLz#I^$yqZzV&`p=P04vNMFL~7Av!XLWJZ353jue6 z95WLYKN+;G^9XW5P{WjU)ALr20iVt=HOl*dvy{a`K z!XW#g$#OZ0b+k9Ai{a3PZcH1}`BW~5b+~hAQVWqWDumjpekg^-7i6e0QdW*!n!}R4 zaM}>ylH$)W#5ukXU&@I{F{w(OJYxQnr5i)g0H*9xsI1EkL?jMb^S+_59vYidVSx@t zWa=k?3bRrd@it>g1MID>lR3v;tgJD5M#(i}hSe@Rx|NPmD z^YioF;qd<5`|GQ#aopQH4MgjK$+xpq^%INJ2#v7q9WfJ^0-K1<2e4k8*!YB+jy|H;c|?|%L(&q}olkqOngIuTPTgA8VB zlNKeKYptqdEyKDLQDZKicOYdd!lpVO#+&QgqsjKihuy{P<;Tm_Z1Z8RQzbGkRO>vj znO3ICR?Sqs&L>=v`=iPhTM;E1Fq_533ziAUjD!3zxMpImY#OiF-_mvmnT8(Wx zejaajSD)|Qe|-Mn(f5D+2sjW0FH2irf`UO6C-M&@Uuxg;KG^Ghfo8c?t^CYUhC^chK6YZZa2j3@UJe2 zz|1_W$I{1f|C+|};7sxmH|t*H!df^NWQyRKhl(5af4j*nmo6s!R%J#1fsiyZ2O=%rP zrU&VV{G5q!+pKk3n_^L6e-7~0D4C2T}2Vl36HNFfm`E9LUw z$@A}i{PFVY`j@}`w;gUp56rdJNv(P_#N2D%sf4yA>@ZOl_IbuiLV}FONMU1^!IN0V z!Xnj(gh|T!`1tYD=TDwKtF!+0=H1ohe1cb()Pu*_ea}KOD3WOBn`Ut!OCgqm@>*NsmrfGScP8s}-+l-s?2e zG#9O;v#_dFwwbMxX>I}34U2KsAvDH7xos;{lX`~6v4kn?FT!1epNfYot5T6dY%Dt0 z@%H-TVZ6S*IlKSh?B1h4{p-J6eY*JVZ-3q0UYHC-wOVDCB89oCR*Ho%OkM$|EtYop zkGc)dz+m*?N}TQ#zlJgXb*^PtYgINaOw(@r>CIc6>AN3)_?LhCxBvJb|9<`PV<{^p zCM6bQQq!sPc!-lz$phCj#*fKW*c0s1LfpvOf=go<7ECtHw0+tQPzI%8Q*o1PrMg3l z)I*RFLq4Oqr-0;4p;(*<<*tF5loJMnDOu|dw`a!7duYP7%Z_!#B#0M#vZ0@3YK0e# zqlEn?Nkgn|7z}O@=n;6r@I`H5+PT{2&^FB(_tia7ECXH#scrYRO3;;)rqVQOG zjDvY4k98ufW`tgG5FzSX7JjJh?AHbXY^Kuz5DDlN_PbwzjpsYV}LD)ilnw6jAyh{FoK;Lf`kq*)&JxVhk{J^cC-i%G8bf6kJmaggx zZloD&*s_FV7QodH0&)NCV!REz5HLP+Rvc=TVr~#egi6+!ymP$A2=niT*ux3mW6zgo zU_OI#x~Q$lq0HcrEzs2vuAm*5>BTspvzA7S@mpY7OwnS%yNLyt2ISYlMn9Y5`QX$a zY$HvvBYAA)1fSXE95M^q@gfY+U6ENgUa5)JToL=aFQ}zsE2^MjHcfvWqwdUv0-xdP z(+ZLAa=YuyUUJ^9VGa=q`{weTWXaf!3(0VD|M9m!{?lCTx1awuUSF+k7FNa4xz^rF zX+Z>H$f@yde3s1*6cAAG;zW)(&csw=Dy3}JXZIgId;VfI44*%KzPz~HZMSur3maQ4 zWTutbf|dxg@50ta(qJ|r0pX%44K7HsFp-~qCIs;0T-)OpSQi=sI+VyfjAZsiy;q3MyY&5>2pNlB;TM*}daPFBU33t1_{Y7icV zYR>XxwO*Z`og5AaHagxMU0q&JyFF`Vty(LY5>>NWID+lQL4uLNgLaj^KAS~k?a*2S z!G`KD;%Uozk_@EDDE78;O(zEv zqIjV|(_^Vjj9?UorrAiBHlg&V3>xIK$b^(9V8v%PX0BYArtR&=_ivBSPrmu~+i$-A z;eQ=Ks0GJwSbxf>%tkqX4{wH?9Up}{sqgb>xe*r%VE8GDRz z$K5k~O#l;sUyQTGBeON}s3qijstMK!58J6|E74{~tv2IGS-d`to4WyyM>;#`9vNdq z349#+kF2N>$qfuiWB+q=#E6G52z&s2o5FIyNA@cn8Cmv@G33^b6chA;m_wN&frly` z1}p7-vG2YTR+OH>VjRLrHSPrOiI;}*EOKN$TY;41%V1nDF<)d%lJUwyN-+=;yd{zaSrHN79Z-NG-Sk3TYqG6=DKGR74Wi|bD=br+ zBfx|fOv$WoDY_kNLX$WcqMS^S_>+?gW@|Kn%b0;PWMr}02Ja1@)OF4`sBI01>s%Q* z@P->GUH6n@auqc+ovngDmgi}p>G`PanfL0k8KD-f;Of<%3aDK3tzaC`)J}%h0 zo@J<}zV8l~Ri&~zI{WUA|GYUm`j7wd|GEAALF!m*B_`F%W*Tcu17p1C0&F4P-a>$& zhtXyt^TqDwF?tdP?$x?SFx?ZPLqA%6z* z7Oq5VIF!iqn2|-DLwv;56R4zW6OY&Po7?Nlv-{6pJb&@U%hS`7Pwzk6-dtH7nbfqh z4W?>36GiQoVI2>eX0W;D+3hYhp+%<4by9PwI&)#7q0Wcbzx+zZUw!>b|M-W${O5m7 zx3^ShW)T@wXBPIBz*G3?$au7-`Qb$|ArWpc&C?qeMc(Ps7FgnTn^BY*_XELQFB#Oj zWk7oek&AzNA4SxM3D(j=cw2_I7)N4?^BEXA(9sr7d~3$n1mHOsAasx$;YGrrIl)F3 z;$d)!+4hzK>yod%z^RvLT-cm35qG34yFkH=`x_Z;i=eds)xjlg2aA|jMnk(C#A{ST?6Hd*UAW3hHZ76c8sQO2qL<2o8g zWU={hu2t(b2bBnBmP;9_Q>={4@%)}R=VeX^s(iB)VsJY*=)zvLMqp+<;0-J)2* z=`F(wmZj`)PwInVnWw6W_<~dvuBE32?TZIkzTR51taWseGwuey83iRJ>nyZYfr0~O zdlBg+gMh(>opW}z{rrQM-uYw^9)}h}XCPWPQcL6K{&{Y{QGJ-Ko%>$KJphi#@Ka)H z)3Ejz6ZtWk85=-Vn^i}PQ_MIrgW_{=N6MK{oN|bUbt2`39Qpt8(`&ajr>4-Jh(Kb(AHpoiq%oUs$RmYCR7&MxI6nRI`yU@Xc=F%>>p!kPzTOw_8%l&qMav5mQka=UNQ$Zs!}{UVr!T*FWybGczrVb^ zm=1fZT8PMuOqtEdv}&4o)03F`V3+y!$vG_Ecwz0v@uS1{%Yy zIBqMk<(8~VIP20LX>$HwS9?v*O33;%0cV_ln5i$|7A{6?YQ~ks*p$^sXX9aeIlZmB z2M?c|pWplPtFJzP`gn2idB5E%(NGG}Ov0o!=wt54PjI4?ePe1gD3JtKo)NmQT_z(n zGBK%EnRzNEf&KG<4Vl&U=dS}jiHF47vZtOu&jsY zhBfJvEC8@$e=#V=?LmgV=5st*Mx0yoQDmvQE&@czFi+c3#+J`Xry987(9U2i?d2{o z5g}Fdz|A}nFlW!Qwag;Ul)05VrW5R@eG|nOfgEt7CBrGEmYLF*EnHaw<{gJ=i~qn$ zB-{s^LL~VqzhCPLgZCA$H5+lOouArD3Cv@~C zoguu%U}q$;Kx8BKEdo8jgrHknd#Hgib>BCUnibEACB%7y9lBdK)ViVVq{cn#T(pt6 z{oPqDObdAwgC@G|5mbCJyU*P9NU)<{gE@?IzRWE%xi|s&ymeiI4OxMdPxMXLuwhd6 z4;@CHrDwuf<=L75$7GRn15YodVx+wZ1`{U#+U(!#Wc0|eai-!Qw^)Oot020JpQLr?s0;|6Xe;)jxU^IA<(u|(LKfYKXK;ukXt)AN*UW<+U@ z_h7Fg#zL}YS@G)V`K#~0{OappfBE^t>t97@BK2yQI*&}se!v!!FIrI6Btn47mRQ38 zQ?`wnQE3y$=(-&mv6+eR>Dl?qFTXy&|KRfC>cfW*``zt4&Ez%KY7{#@X=EL`A$AXD z_$`bhs(sj)NW_RqV$H5d>~_j5O2nlokuOAUNju_*2O6IEoGM4G$r|ya3CSR3U0H0s zDyJt;Uw!fT$&;(w%iZoubehM}&-RK_mP_GkY*be3_3=@yCabl~R#t;)rNSyansO;p zR#lmtRm_#Ad2>wTVSK<39v*q7Zao0wHU41q|@YRt8o3KOaBt}f@pxWB!9 z^!VwMr%#Vh&OUwmcy;}Gp2ji^OsX@HRSqfvhw5i?#UkB1Sx^VCezKL5#|RPCnYcv8 zij{P-$%I#jtNQa_{&sqD@4FxVFdxPbzx`52GgB@_s}c*blAkgbCz8S2aHe_F-=$OR zfg)_fL(*^uJy+y-p~OlK{?o|w0+8A(0w*&M6a4`3G7=Yic%qIf zgex*k7}yf8x_%f-BKaNO9xgcom*_(Vmg!(EF)`Zbc}Bw?91VSYKA+B!;A@kjar-0; zpt+>>0E-E(u20%ZyqdQHu91;%LKh$(0aX}FTqV9hI7yTdiC|}Qj~P*>mrl@{+`}`& zzk4Ds4pj^(_O39xTLNuxhWBYNp{AmZab(uqX5=fX(dFzhFn}T~=v%Bda z_qho)?!_H`6#8q3nlmwgTU#vOe1DRGuH*5#{u1J@D^M(=P=jvkqIh_de56NWSg5-& zg5bE=4}|FFDwj^8SF{|}4zcnNX?_wHMt(SLA&r;4@{-$w{I-Q@T@}mhUn2!BYJKrUSbYi9*g2(uh-j8L3B>Qq8!O zvKr3sJ^bdo@08`Y*RL->U)IBDT7|66Q#zek7lcJ69etvBS;WCn*3Tm5byXe!u#gm@ zf?tHCbN}AcS1(_@e0BNx;_~B%GS610 zc{)_BMA)6i&4h(Z;WC&`J}%6{WhJYjGKui|-sx&{JRJ@z9+-yVJAd%-@v~=TU9K)Z*J*FcBr4`4 zLe00&h$bt+Z3Uc}F_W4Kw;4YR%;cuw_kq#@t^+m?GJyL z=kfFVxAmZ=#!|B6$*UKfRA9SH>kKdDW%)XvfCRm^jvZN^+1M*Dhd>)Jc5Etf3^}q8 zMynlX2hqeMpfVHuT65skaK_X1K@m037ZZ-mzEc)qm>bT7=TSD)Rgn>vXJ?QQQ36H} zEikj&@t}sY6{cL zA{SVAYD1V*k>d2_Lvh?}$Tpfu!fe7;{p}vuFq6wYxv__BY-U1YJ>v^*&@`i$FT+wo z)!8kt!(PT$5T)z5*{TIaakg)ZKYfH1W=nZ$2m%eAF-;im43lM+OUwtZ4;>M13;{PN=s+B7>F!bTn6TCBpLM7@5E^=u~CDB zi!zRV?wN%}jhO_SY%+K=KOb(dc%E2icdn{dVq}ej#`z$q zTV=zG1u_b;$14_xiy1RBi>P`DkSK9kt;%Ng^!f8=&tF_!-@bqMetUacr=7%|6zXygl#+vabH zk4_lUs6%z4*A!}|R6>p%SP#g|`v{`mRg(>vSmZJG|3S9BQbJlQ-)n!xv9EQMDq z6H!&ImCU9}Wf;f?7TKJvAHRHce0F|&eIqlicsM#eKfQN89336%c>C$|r#Eje-n<(x zZ}c#7odJcBF*0QSGm+qm*(Dl~PldgbMHm`vfx~$wb}<|ZR5#Q(&yxz~;Y;nwH`x{7 zsi@AvB`Yar)oGq@x7(ZB?c*m;o6<(26Au>{~O60{wwwN?dq&9y^*tg+I>@G#Zi2!-CNs-cVckb8)9M-#juGJS?7hU+( zoD3RDz7TDXNSsF%;L}#1fCz+OK5ZQC&?ZoAqnyu>tHo+)@$Gm|c%KNum9%oJtT?O2rvfXIr)h=#BZ;Za0QGPlC5EvLSwfmmnNb`El%ScgV`Qjb$D2H$ zG{e)7#EBPnxe>osV^i>m7Kn}FTpmHE!yhTS9}<=_3c zv(FZ9;@!-o;UuE{%-z~A_vw(yqN_;eSzxG-Hxwf=nwFz)O%!Al+oTZ3p2v!9K#{GC z*%xM3>b6tsz1Y&aIEFmo$m^^qfsWiKdS?_Cz#IgZMKfE^Lc&D`lflZ0k2c@@;Rh1> z&wu~-!_5WNk>TJ}Nr=@eqbaI;lS*W)8^>_BA2y-blUE8EHX&u_=%Oq{!o%wL-ucTf zUmc$u|N8p%_0`p3x3jr=*9*JNshJ9kZ=e}ePsG#%CnLg!YoSC@S;VFIwvJ}b(WM0M zFC@$|5C{8bDfpK)jjE{yf;87y-~9$_h-D}yESsbI4<9Da`PTqZd|M~5^df3r^ zU$;A{bDd^V-=j#hs&8<~QjU&}iWIFi%f#FPsnO=>oSGU*u{f-YxWj*c3albui z-R-t@yPt2i^LA_Ff$C`HPcSw%C2WTp|MpJG!P^N-Ga$fH=TqPwV#j2IRRN2LU>BsS zIT@C-!SS0a#R(6AE)Fz0&nDt+-d=s$Z?7Lce*EOwlcUYiZ@>Ps-*1`3sxpgkr6@&L zqC%#zTM=jYt00-YNM%o`QzLfReucxeDoUJFuVx2z<`iq z@F5Kv(;jYZu!wy>5+Ict7k4^=2p84HrxXxR7uNsVDZ-RJY{>!Zqjp?m;=v0dQ{X zoz?W?hLe#h_zRxF>|2Njzl&H1LumM~qB34p9sfV^+et?$oF* zhR;SaIC882Bw{1+gp#X|&ktnkiqk7o-+_UE&YCG`6t;9?{YO8*R+v~~I>kqv>sQ)W z9a(N8$aq}(hj_pOkZ@Zk7yRA0%bm5}=^*mOz1lQs!oxs|0B;^Fa=YAOBx4rzjvC@^ z{q$)Q<3b&&NzU?J{e$oezV%Boi0sG9foS1P;WWE-!d=phnxAg3YjXxIQVk2%rf*&v z4A8XX58So3wLE~Cpa{-0e1HwQi0nNEB2<x&E$^6b}?|Z9+0V0lv#9l2JF7@roQPysRIdW$K3$wiY!Z8KpVJiu>j$Q{9s9_*EgIoady(TP6{ym8A?M>&nBk z=g%KKy#L?-^*?Vuzo%)>R^y?n?CWmLj1|?Z$af)9H9$?!hZyT1A`o;iGBf5PY9zzz z-jgR^eEDit{p-)a?60rq!@=h28_kd^Kopr)ivD?>v3QYBnwN+YafCXBSSU^uW)mS1 zW{KQC6)pppLM2zMl5z2nKqSrzh{G8?P$o9a0kQ`dQi_zaUY*=OJw7{s{>7{Nj~`s$ z-v0LX^~J{z^UYSbd)e;kFwr#AJiE7!je2~6FY$WVh_Oyps}XY^2wW!HlX`b0;)0wHp;k8Dc`wIZd;Rr6KslW{)KvlJU3_lQ| zec)fqU&he$66o`h9cbZ1aUoJeu9ww}RqI5R=ed4-_wM%k=IOI%Uw{4Wr_UcgfBd96 zGpkpo6yZ8kY}^(#Mcxa|mm556Q%YZiqnWbtL#O>qVK4sDnMCUD_Ud!_eDCbpi|6~@ z{@2}(ri0Ga17$Vx+B%;wW>;oS_-Yd;4w8$?ft(a4V3akxZdk&1@T8|(cN{djzJQ0g z!xlFK?m{FPIfTnjUjj8z&*4Pc10zfXF3v`Nz;uQh-6U`})}qX*wX#bMga-8$7ZmQ) zq7WO}k^Q1*Y^q-KpDDGL9rXVqcLi?}#*jrp4Z_0-G=T(lprFl}qV}$|`GJ5pU-TSa zmO}wvezTs&JNYyOy+Tnq0vrpja8z5PYvm$C_}TMU;pT%FHta#1?07n@_1k=wM8Q z8VrMAOV>JT+?4Eh#NXRz!x8Peb|fY`!WW%F^41$7k7J$H}G zIjZ5(6ZH^5H%T#6hWe=*a{t%1$BSGeB|srQXna8oQ(K0#8fAGDbR~wi`HE<}b!HT= z><5i3_IZR`?Mvu>H^_@0>jym_#z0O)Oc*#rhhz1d<+&BTzlP*G8}YjhGU*YxR6)&joFMuqVyp$D?~($qy3mlsYbHiJbCuw>C2bf-TvLXH`|*VnnzY; zQZhC5gX=TEw+Khcogopid4YcPQYai>r1OcHq%cRZ6XzNL7M6j^U?N-wV=^fuB0Nwj zEGs3U!puyne$2CZ@hF*x5=6FItu~v@$@%H&`MnokzIgWH#m(*R_3PK~-@MyiU(MUC z?e@0a&$~TM(>#r&m9?5x^7#`}v!M*@_4GPYL%a7x} z>Kw%YLKZm)=SPt#@#?mGGe9zGDiS+d+J$@5&3`#7VhC~50wua^v|j?c7r0}=#k!sc z$?k152c8L$k$;0T4vgHyxd$DWa0K#wqJd(DnVGTkdZt`b2|9n*@Pa*CZ;av~Gt47~ zzwY!2RXRPTu{dPc5;fcGMPnb7tH4_T)T>5Fbm0_oFaMOBQf$G)m}u%#Ir#{ql#oSw zKIQv6;O|7ipe@l~!{kG!9tP1z5E=^~7_7iw(9LZG;3mhnwx%xMryXTLL(w>fL@9PV z;;xs5Cv|l{jQcqS&9uBFUlTlp@hYt0v~aRH0x`|fO;Gs70v#e|ic&ZVB_uF1OLo>I z>AOwf0}cs=fwuNq4s2>=?`l1NW*Jv=7=HL$ta$0m1F?`dng_FK+Q7ub0|&*>UfI#g z=5YZ8fdN9)Pq2hs%`+5H0ZS3KQd(*9S*ME@2kA7u?7G}rNJ~{Wx8G!%9-y5lj?f1X zP5sY;3FXXF3{Dn33AZUJ=vX_aoS=|+5A+SmnBXop1m^XfXz&ynGH&`qiI@Pu06Pqz zr2s&Es3m)}gX$dQyGiKKo_t6e6BMAGU(hErfEwJChu~yGF@7iK<{4oW4aS&!gAFD& z;%=n`5#=d}t+VEpNY*>10BjSi)*!HV3@W;eG50}f@i)XzO{h&@tVyzc(TlbNB3s;t zuqp5&Y8o{dG7`+~Ih|KryzlPR#ZthX5dj|}P>a(8DI@sqmh?!>K>Y-i*0BUJOrkGB zH$`q8*9-Znc1GQ4wG#O~Ch>}wQkVu=9Z@-w)#jV;zOPz;`s?3xzaLn&&Sq+=Y=mi8 z_c;J$RGUf84`$Gb83|t$C2@u>Jf}!CViH-cUcUP3(UYg2K3#nH@P0b%tWIo7rtB0} z%uj7(D(#R1%yfrsiBMFx`XXL%k=R_w!+^q3qc2QMQn;+R3?zk1v0-2lDuZY9i3`a} zO384Qm`Fr%PJ#@wIbIzdZBCC*&ri>vzkKoV>7(s<`1Q@3zy0;^hszr}O!IbIw>vxR zX&h}FX`XGK$kghTg>gh|MP##HZ8nF)PA6p|7FISEkzsv)|G~7|-@gAOyP4;isR~z8 z9gK;EjQr4Cs+D7(A0kyIRZ=1~(w6yx-+>JR8<~>?g}Y)=GofNs(`R+I1(IPMjyfOk z(!i$B(MJF-$~W54j-fEft*O@8(QexB-oJixe0KKu+2iArlh?ogI_-^(ZJLQxM9ivi zLF&$W7UR_m6#ORrj=?c{yOsTRA@ePaOsy(2>CD#`7eCqG|M}nk^z9#i-0d!}K8;*S zXJryHWf3nxGO}PEJq1N3weyGUhh_v^Nx@-Tw7?qw(Y!ZPtd!4g9u$v`Y-fIhvEQoU_HqY)y8V?}3ra)giJS(&@{vKKCA=;; ziHFIIa|OD8!{sK3g=52_XhygN?G~^#hFfgJ($XFyU`w%)QOtv8mz~R+*+53QKVu_G zU87oBuZ854u}~v@7>H0jbkbUINMQqr?Ifo@nsxn73&1WH61lWH58-JcgZickfq`e2bCNCtp^z3)&Taprk@|-}%&>CrPbhM_)269+9}$q$ zWRxBWvSXrH*P^kMi;<3IcP?pi=KD-9v}nPxvzLjPNhg|u#!CM?*E^tcuV{Tn->@4_ z$c|e^t=V169yA8wLTJ-TZBZ{EhBzy447N#3>G=K8X@t$nIZ09<>R1Y<>mWI{hu|1P z<5FTcGS1LZH2x%FVkt&qCS|pj)sdFf(=Wbw_~_w({^$Qa-CmbEu};iNrsBo$reu|v z&3t1SMQ$Ud%UvQ2zc3Mz2>YfMPREtFl8}_LIr`$&SLgTdzj^)o>f&-9_pB=ZE;917 z##+RsZ8ig^m&LG!ou?5qu^&6i#!=JHQhbpv7hx$RMTRxWz{9}9U{ZKBuoPvoQdq>8 zxe%F(5D&#jR_oPzy*@oZKRG)-J~}=J2^dJmFw;HZ$JP2?c4VkpD(1cZ4bKJ z)Ba%FElrcn6W2*~W~#n5CkBx{Q9m55%TT84QRkVIOjt!s==|Q<+3DGb_wRH+7N3S8 zt4RYZtLGxEvZ<+(Rx)FXz4N``#?0U%-GN$*usuiV6X?*cut^7=k$X*}H(0oT>?>=Lbq?ajr9kMF<#;oIk5e!1P>*6p5s zTh-h+pP9+`IrW^PD8#uN^UP zCKrO2e1480USycoaSc-UjecWlK^t%G!%J9;R{_EK_0|AU81&C5nLsNHNu6uv7w>m~ zh`_n)twJnJWZZU2N*dOUM1&HSbw6RX(PK+T<(qPv6AvcC{D*rkkPsR`Qkq5ih1mBd zn__7PnAJ%Wh`unU?ZBHt5e;Y*qUyiy9`wKYi7e*tIBx1o=!(-2!!&eDvi|OQ%m3y> zvm;^cY0j2Rz2hJN0PCI>{ayZ8j9R{idC@MfbY3KKdK$>xyZWb4CW~z$!(_#gHsDwu z8rs>fS&9R6LU}{P4e-PL!u7WF4#TY?%hrJsb(8V76f!O{B@0GnP3r<6(lk&fz?@-H zr!$MXbHv*ep^o`lD0IO%o0vw49xn9`xEvlFxDZHYnegGhYnB~rUw6ZlcHTzaeH<3> zL4i26fLhjEU_~_+VU;O&(&zB$H$UM?K5mnQfi9Cx;q<(;OkZ^3;kbWYJ z#rMxisvf_iuPoJsSfs30M@KJTeRXtv^2;y3Y;SLLn%GmYaZI%a`*%LqZLs9$cJ*kG z%3{nczS%#M=T!XsX%P};-|@2);=-$e%3x*1!-|GMO5v5P)~jJPY>qZZC!3?q`e?Hn zL`Y;f*=&waSF5AVdTm^WRZ*jHKi=*R?>=Ad_lNC%taY9z6B?%dt{y6nRSyRt)jEmI z^L$`3vRO!d^kY_on1okHYh#@|cHX-)o5tN&r!bOrn_c->c$^&~t?~rt6_>IWWDIgtFRH={F<{S;1NKkJ{PJ@7g zW{Q-Cph-CHg^B$50%8**R-N?rLVp|QXU|`J^Ue2fk6wRx_g?48D$UbWg60~C3+y7O zPFj&W;D+FBP9&35>cw47RE2f2dHDGH-MS1F)RUTf(%+a?t{A- z{;~xL7+?auOtq8_yg4Tr{+UD z^3*-0e{g$&=TVwsghnmf7s(;+dXYDGPj7mSfBW}z7=&BI^Fa>2op+Y4|2-TtywCQf3@7_~54I+{wSG z&7IL??zAIO2`PuAu>5lP@dQXh+-Zw@z|a>8%IqwI5M%q-*1am78MVY?|A@f+OeP|% z%$vLBE>UdR_f$7vMo8H`hQAZxHuB49^hvh8Gl@!gMTnvsc3;C*EmiH#3`ZLaOUJrM zWusSt^4+L$s!E!Pwb3`P)e27-67wanS@_7zg+z#o4Qm}%tJBl3zx(d``ttSP|4RG4 z=qxc`sTQeOBCGL0i{(o#GLSkHWy*^a?^!euIc7J`lzq-9WOm&)cR^3mt z8BIz|gPLuQPiY zdUp2d&3m4esvnSUlIaH~BcBpb(@Lts25M`8VVME8=D=WKna)qe$()W-<50Li9vc&b z$nA$}M_-W93YhQk7tSewOhMV1%tH%jLS84s#KeWEvXNEOst2~;e*O9E-o2;Ko*kW> zy!rL@VRyr{Hmzh7yDu_RtZA`uv&O`R>La3NivD&b5~j9&nV)Q@O3bs(dlA`PUH-(J&P$SO&R9Tl~j2{W5j-)GFlVll@HTjw6D?s9{yHv!ghV1YHd z5xyJZba8ba)AB_f5?Vr7gL<^W@PI|Dde6;Rd!L`+exP0Yt3)~W(e$>*zA=6Kxpy?p zbtEKX8oNs@BH3eiWEhV?rvZh{3sTFJwaNB{pWxs3eo00m?}~z?j2T+{(>xlbh*>pl zT@QJ>Q2eRT491L1gSm*oz3A5)uH7TLX2&cSb`Z+!4`ZXAoUm)|09nIu03)TXC3nWO3nBwQ{ne3k2~nBlMgb98BfanbDr$t{ zu|vjcnM7%FT3SGWD*j{1u5!jI%ma9m&%vrI2+{G!w?HE5MDx4%<+{`uzz*aW*EP}l z9vV65X?+x4qeZWbZgk^9oTKj&Y|IQvWo}AN)c6yIhTCoED(2ZZol^s41JvRAW(!O{ zW7#-eA}r_d1Vh6ZTc*e?6ryEd8XxbOdBkZAsuK$d{E)*b=fZ#-$0e%wvi#jIz1TGg zfnlCH9cNCj#^DG=WY$uFc_HAqYc6yU@W|BRLOKS5dKhJ$DVi$2$#kF+_r3xCuIm~6 zNH_3!5cdclV8WEJgJJl+>I3<>ZRD3q93dG2zBy+wU9ZyAqd|VLG}SWvLx4>}WSQkX zk>#;)DgLpB-GJAvM9$daR}~et+KAN`s%B%j!%yM^8fw6=j|=)$mXoI za&R(-Fl(chBOkyCJ#m3R;y&wR-d`4yV#YG8k50~Cef3SP`nSLRq;=GJ5;h@aQol2x^+HQBd-5pG) zah&&)FwshelQq>rr$SX!L^<~7ivcvXsE0BYrd1g>>sfWKT1rteUaxF5oIZG{%=_J; zaGA)9ibaU68k?Ars_#wYS>{aQfQ2(eK6R2{mN1*Acdt`uL1+BOt5E_E8`X+DgH-G2M!m*0H#&9`rU`{nlf z+UjWJM>UYfQTOiPy+)r>DH6x19g{>%7R48A@G^0sTBUguUQJqm{rk^9{__t{ov4BAQ7qaH_L!{j#NhHFJh)9xYsz~0F175QOI}3S+T=Z| zS!1BB&dkgxcDlivmIX#{6ak2JF_ocZIHC!vX z%WSjr5CVnNog5n`pr=K;9E)kX-Hwp3bz9w>O z9d)5g`T)!j8O8Se5ynP*%hEk9Q;PxJIjDx=C#)~%z?XV=K?uCvFd*8-isLPrKO$3A z)TJ_F#C~Gad2%8RgPp)xQFe#BcZC_ml?+>J;`9H+@W)B<+%H29odJ^i^OW%e4s9VB zV+4w=3L_94N6UmgOJSD}7QTj84&BH`r7g0b=xzub0*;xO3$v)P@SrqQDUV*fc=7!C zU;q5Sc2}3I6InH1S}J+eJe&Cm2E?%;G#gb?U%lPg9phk6CSpHtSeV(U7_+hPdUbk! z|BEla+HUu+fBQ{H*>|%sHJzD>tg4bv5gWznh_ObT0bQ2CeCD}AewgK8DdMYWO^Qh& zDJI2aASpZyvR>0_C7bp7Xmfu5?8VC`uU|D!x&K8OP#O zpX-e*KFGJ3tFfAKpqa!TM6Rh-nX!M1t8g6Jz_10FVmDjtAA5 znWQkV%B!U!yKhK85jhzPZ|%TirW|G;v9ghwst~i*I_`$e@%6{gfBWyBzW?F7>C4?O z|NUpG%2sWjb0a^aLc~OsIMNj@_s3}%nSnD)2LVWN$PQQ4;0ALlNGkvm9p-{qG7uo{ zX(tLXTymQi2$--;;Fu99adB=JxeVX&%Hx~}rj41Ea;JPh^op>eBD)jp%KuPhY7j@* zr`NxgjxfZlGaLY{!O?bZ8*T+Y*Fhk-KclA}-Qk`<(rlO!qE}9pbav8lt1M&zJ9S}z zJcTE)MmFv^>98d~`i7&uhC#|wkuFTjqAzBPb(`#GsvuActYz+JfMZ-ceTuKV05y6Z zSQ#X55t_yS_X?TJrBUa$Z9(=eH`NXKKoLUJ;rHVvsC<{farc^$DstrrYkNcJ#y-^9 zmo6az+_(W{JQjMvx!Z8lQ#MmS`ZZ>VU9f)EOss>A2s8+%x6ifMh?qsYAu#BWQ0B1N z_sI2n)!3neoZFBei$OS6HNSD0v;Y8)3Lb;jpo1Jf-MSeHf{_7K(jYjsA_7J*w?GQm z1^2qJP>ZG*(O*n>bVx^-U}8k9L|!13DJtp>4@*gyQ$pR#T|cn4Wl3it97i;ww2LH| z)ERV$qZRhY>%*?1q_4NAgpwY1nGri>;@8@@UBmNU%jMR1;sU!`_RON0+!-@|G43R9 zp;~Rc`T+yKr3Joa6Bd{C-*74bm{stGWjxN^qng%r0%Jxcg;4Z@(5sKYECBadctrQvx4J(tuRs*kB zyjhpkYJI$Z^yI-e-+%G-S1+DEeQvMM4<>-GAm3}+`ND z#~WF#^>!yBq$KNNoAtdXk5|KL+8v6?SXD$MPGS%eF^#Av(o*c{EM_hi)h}r%0F8zv zpcEXZ(1{c^qvRyw=iq4UbZKU0B7-yINICeoWEL)yEc28E7r^i`q0QEC0(qitfPG_r zGpl4`BFv1F3k#W+!qv>Cd3SyNoBs0j>GN;D`|kDYH}BuSo2H$q2vs39Ra0y2X%*nX z291CLuqB`!juj({6Yf|@n02ZnU4FW}eZGD5?RVFgH=o|UwP|FbIEz47V#j|IrXjG*5O)%IoD)DIw6K9k`K^d+|76eCn;h}4hg;I=Cy-f zz{8TY1~>8Zon~hfm11gwBmLJUTSB_c*ZJ(F7qKwXHQ95H8SM}ig_C8RXVv)@%a!@u zh8#j@`7Pw=Jn5AY2~!FZwrDX!XKf8k>MG?&9B{&9RxOn_mnLWP+OUM%OCP!PT`gKm zrj`$7xRIRkftaWt3b=BD{KfSu^c63cc;cd=uKE}wOPhx?F&DLv?_t2nKq45AT_{@k zo~|0BWNCES;?@jij(zDg50hE64!4Iqh%f^Xy@lN3iVP(B{$&h-8=H>fv)G?Lhj<|+ zz%V5QWl9m`CWnl=Tm%7YdWj<|f%H6E9zM}cLUXgRp3?DBkDxA|d8FkJ-}%+CBlFdS zEU)d`Ln|rfK_^EL71&igY-`XSU#_>2I`KgfT(K@C!|*9RjAj+n3=0U}*TF z6G;+3&QPEUB=PTela7g!xI5$UXg-X))4UiLOWbVF@hjW!7!KFq--(fc5!@L~aG4NK z(*TA=hW0d<-TLPkoFLhR&v{2Dl;|6Gs1*2xVOVklhr`ZyC#ZJ)vVJH-OzCBDTZF3+ zE^I9nh*XY{N>Hf4(m9p)!D2X zRWgm8e-gh4WdMOWl=&|R!Bb(9zlap#;>XPr2}@xvEQN=`h7}Ei$-t`>tyi*H9i48T zJbC!zk6-`kUw?f3_OM&3 zaoQjDzOm1W%SsBFP4hhNZs;(ca2Ym7N2h0Ev@;_XHa1nnW@Ke0o6YLvWH{dFCCe}n z)B4_-ot`{+{ABz2#tm^8hB{X=7A`7Am2t>MC9STz{)u)+T&BY$&**n3huAIiTtviD ziVTIAO>3^&fZ050glKZF2AmH;ct)2J9mU`X6*t-)|? zyuG@dr@5*=eg0zDY~H?mJ?^%3+$1BRA!6^ntP$i90#)(L`)Iv;}Bc#51r zpB;uz?Ei~i5L~+d+tisHpYAFK8P1T#U~n^vE5?X z#z0)MM(B{*Z(Q7#qPzRUO=$gG&C=&ucXh|v(>BQ>ec0V$i@O(k_P=RL_hA@zH$Y9; z>6@vO-133Zo5e&#YG&rT2zf>{)zVNne#>3Aq2f81z(NdQOO*xOGm)^>Wupk+Ct7!X zbg=9o%Nxh3>O}CX$ktJ0Z#oIVb)h|N04OK+sD0kNzXb)4p+Z#*5wjE{=2B#}Hd=l0 z>Z@l@p8Wa0|9tu3z0D(WwK~UbL?QTqURm$=4@#*?lwQMggX&wukY}Q9~ zHs0PcOPwdNLCu(@O5ydRS&CrGe~u00 zh|Evxs?;SsiDIf7&?v|xh%vEg#-tH7Ftc!BUT=<$PmWjXwGr+1`~ChI23vooRAZ`CCuX)fW3M4~y9#_zr1DaalJ*dhsZcR8Gxefi7FCuqcqF3S?~fc$}RtxAW20x?<$^e`e;v%*j+jt#29>^W{?JU4PhhQI)z=5O>NOq6bn zd4Zjs2g&@{@&c%GATZo{M|cqB_(xbn(lFQBd$@NSO&NdBnewLvi)hk9d*S=3Pv{zI zL0N>sBQi^!1eoLO85H*$V=y@#KGINK(2T14M(tdEx(OKHccKws!iG{D%L#gJ(jDBA zT{O-eT+={1o`Au`0K>oZPLH|(m^mseLj0*CZg9-3IM~J8Y@I@PKaTg-i@!16jAhw{ z>{D)*U^LjFKME#X4@8SP??i3hprS7Kq z2>ycvB{xJI+tV?#i6}8k5m`}LnG6pfJ^t>yZ$G|!|KYb^Y}|{@ra|x2G!8cc_2)PW zAVW59QafleCB?)=7|VKCjbvEefAsX_%U9PoHy_`B)M+BE;-~y#PfQ{|FDx=%F$#%f z5CjH-vK6fokjRv{Ft8?$IWs@}xC~TQRt7D@uo=!C-uvo@FTeib%j0_|7dIEGDk7#N z!ytu}j!oTqMH5boTo$ine*fs~Erc^apotXAuzqs__L zto5)zY;U*Yc$mjg)l93f2^rZOb@_f6mMPt_@6KgdGRvm4Jr*YZh9>!#quJ3<*EJ?q zt5h*}SLywmpS6xpo;@E#-o1T0ANI8xQT3@O&xl&k4K&4(0aa#}qRhj>L+jMIGI^EM zYzH=>ix2NUKDhVz$+N4^SC?d+X1-SccjSwM*@aXnN@Y7DLa;>c zv44TN@xlP{h-4au0|eXrD;f@m>Jd=Orh&($O105T;Vhl4OL39DS}=qBap&JOB+8SQ zG$kiA7SuB~YVX9`UJS*H(J>sh#l559^a;-3LP`cR!4M+ai6yoEK-7V0y9#p_jej+n zh2ghsVb7{8Q=0ve$Ubf%Imt!}D z&ZuTb5^#Zw42PEI01!qkv=C1eGQ$bjp;4AOpuUUtH;bRy5r>oFa^Edpv+02fotpmxZ{b#EfiNf z_l_|W`5ud8=4Sv~ZkQO}&zO8SXdgEzKF4azV$94kD{(3J?mv9>}q?syt=u)9nFM| zj!LOy<6#=d!?fG2%KGf|WOI5x&*NdgpOn z;6NY0t~KVuuu&|vylm3xUY8RL7N5aG2LJ;dGKmhW#~dx2RwJK(uFQo@N#}Yve0uk8 zHLM;ze#Ay^U%#2AQHV&C$f7=KVQxXWVkr)1vn+PMU_TB(jmT7u3X!o%t;EFBZuk1< zU;pV}|McpsulHBC)9uWvYL&UvYRpDLR4bVnRU$S_X>>SE43=$X9UL@*Wbl?jQJDG& zDj~_*FVuNtppbtEJ_V}KkqR)jE`8D%yMZV{SDffVh1gooCQ{1?*qrg0v=4s2q>UY? zxb*~c6H7|b1}?&){K(ooZ4jIMvlCU*x#eItefZsv0scEa--aggcxVZC!)EP~MxLK0 zZBh?3ca83oXpszeZ3v%)KoJwyVZ5yvAwBcb>*w&KSP?}cLSR5A2to`K<^_8nZPp;S znRRpaPMWcMeG^2$3Oth!pW^!@w74fGmluP8>}`t%%@Xy^j7X@euPRqElTaXkA~ubK z-(Tnzrjl}kWh!!Z7~15L&ydGl z)m+?}@|E0y$)Y_0Wavj|4d#u~a)3JwLfQ-!J6t+Yyv}0i8*bnuo}(TYvwPH>X8F z9{89W()6(svd91d002ouK~$F1idlMD5qUZ$;UY$$OX)R2uoxYDioiRthFREmLWw_p z1Bt{cOR0pGaKH6;nqE{JbHhFg7=uHEsEvFopw&&sb9(_J|Ih>k#?mGgAbR*GCL+d3 zrEy_%t9@l~4porj9t(+xqo6sEAwLJgb0ivsSg?d%jD<@f8i>oo$4?$Tc=YSvf4=_w zk#we5`|F8+MRiCLh+Y#dwt-@X9}tR#qg|58H&QO;^xlK#FTdRFruT2(P5Zsg6YK0J z*9zuK;0HVQZ!oAk${pUytDM-<$2aHh=qN_MCRmhNJQ9$?!)kqc^7!SmuYdgZ=;Y+$ z_L^AFkB@fyaaKKAZDc(h_WQ#)Uf=BZ`;lbW9_IaSJYEmuq|>49x4Zc;2}_Y-C^8(a z2jOWyj)(olJeIOLK0X>&E3GpV>o{_;VYSv;wc2Vk$XY6Cou+Ajut|wUj!$HLBogVtPtbuRaPouYM#}` zENh2QM&=tM3TZVaV~GIO%ygdj^L#jrG7P8Z=Vxd4j*d@H&(3!HaeI5azrERSx6@&7 zHBMGAWB0+!crun`VfVPY|xr`xN`H?QBkdimbt?p}n9*nqT$BrPhBDkl-S`!!=L-j&{f`&qy_DQ6E;Wv*f_=%p7#o>MVPYCs zd_Eamc_F%>1D-#Emx+@B2rRnE(Af9ffYgJX1c~VUQ$m)v58;zP!OtmiGw^kxIXHf^ zFJ|*b}&f(&5f-I*V&m zsH0b@v5dF1H<|f9Vw|0JT(K5AW-}>FxVP?{RKfs+hhs8Ni!Q*zx)_0T3KJ5J;26hs zL&#~MIn(0zdR*;-cSnM~BV0$NL0FR{8G?MYKBXNYTXe9SEKk#KvVb-b`^MP|WN5b~ z6|(kGzSf#{wC1GgacfrB59@{9+RsZ`3guh2p&Q3eDHa!?uBLB+fnt*R9`Z(>Y>qQL z+C219F@o`DSe}74_l-9u<~-&`!#n2x9MU|)L3h>?<COE+uW zB$3tC7WyctXKhK6kP&(tlZk$Xh40`HSBxY)4>Lvm5i^OI@GxvP$6tN<ktHZET7Bg8NpT2nYrHQtWp6;vcd!k`yHcH)j0GVgUM%0$9k3JFUoG?d|Z^WgELufF-} z^!)78)x|W8=jSIkhv{~=J2^S7gI(>n(>xva)AiN%h`mGJ6hGah(9PP0wZRLW3J&d;Xlu)Wb~oDSp2s#uT7P&01FLqmxrF z-{o^Xu{IKnG(I;bOcpV$~{A znZ#86hz^osLMMJDd6%n!j1Ffvk`RO`jZ!_~#*>G}Qh^9M)A zC(ob1sCB-%zPY~me0zOa$87{^UNRoMrx_3YLYD#VEI&f#%+GlSO#p~#OfUt19yUdX zW7K(4EvE0@{AOe?U%iwSy?gVPh7Hx(eWj|G_XMNP?Fha6AQFV;h76PjuQO8-60&)o zh&^hOcfbAi@czTEzWe(6^3(M^v69u9jEz`$AgVF1qL{lNB5AP(00gNnho_D_Q9@{< zVskk}&H4!OuO?LHHoHjY$@|y`^`| zR7-kATJ#a+Y%Pl{kP z+`XkkjL~eO^U>@?ac2aZh+Q`{DxTmgIA0vCkDf`!tN?pLgVPe+qdQTJ4ZVD@^7J^S zBua+mxlNl9nB_all1WQ8VRM{SGEjT7VuwXTGuuL}vCV@!Vm4=6aL?JqGF4>&Da%~(i3tzFlc=9tdYP#Ln4q& z-AR!fEXne`0Bo+}+QCfNsUzjb+4c^$Qp6ZtmqX}T9ODr8h?$8Lin<^cDaOoPj7uf@ z^5x5W_s{?Gm%r}!TW$+*O~{Kz*t9bFUO*5p6uwgrv;&|gBFxN%E0G$nj!#~^`jX4= z`t`4e-EEx@Y?V!!&HQjNGiJ)vU130=<$jF*I?qEeXCu-8Tcm}pKgkcv@hHkpyeT#wJAy8L`GAN)}yq(qfa8poXP3Ug#8Ol24$ zX-DeI0)%u{-vw1?VdFaLVfXRDBApl99kr*2hbYnzkCI#T5kuB#TMeMb zq&y+?cm7CilHQkXJ8-o3rmq0lbBi~uB@`j1?%)@OTT73gUXE1EnlW-!khG6cVK&oJeD+xoYk6mpKg?mW zm@;Cf!J4BZ?zm`#WNqFd41_x#hJY$*7cz@JJgx%0%7V%Dm)r5!5vzK2Sot(UT_inenuG4|F#s)IA=JPE}E;4HJAMJ&Zy>&43 z9%83|ku)IVSPSSLSG*9&gk|7ikj?7h)5p(VJRRrx@_NffHpjzuUybVdy_0dS*W26i zFqbmi-i)`~aT;fxXn(!m??%#tnAu#d&Z1fm(>xy7i+rW%G*Kx>C&%l;x7Yje=62p~ zHpj>7qxI_aY&EQIudnvEdsDN~Sc=X%Q>{f;$A!5}l`F~Vy)!+UX0>UqBzk#qBTQoS zx4-`F(bMP8zy0Fm?Bu6^|F4@5S5}FXga?+1OVNo1XoK%(O{XFirw;w-#JUu&maCUFKT0gjVU#G+Nc4F+`>W0$U zH7&*z_Nt`P*=1^#p$;T78gZsv`_wO2MEPP?83&V1yWPd-Po)fBfBWsP<>&YB-W!|F z%EC5Nf~vKx2Sfdch>-L*Gwf>=ipY`FB&LCxNoSqL%TJ#^e*E;stFJ!2fB*KUpU6a{ zgfFSNyg&jPM2L+|32vX9GXcr(h$;{&XBFW%;-Y4K=@%ntaXGP1XZoa~=j=Fp>S#r9 zA$Vw{Fr(o(vbp0jhRmoLDf$=jO=?yUR08!+{z9=~7n9{CN0$DXk_&L_V*hstw($;M zdLGwWcO4uP2mk<=LH@yqn`Ge=qc^=`x?rO;fsEiWdKk|!yK}2G(OPP*#Mx~a2*N0* zCHg#AP=~y%;{u&O%W)*?2|~n!?yfWraX??N_`i2caDPM|_Tu9%AY)9+W#HGHuhAlU zN_Iy5wM}W4X@S1`-9%rvk)CZmmu8WeF2DGT$H(VjtIYRNQ%y(Kb7+?@8Q%7s0DQr!5>i! z)#p2-C@b_W$lSsjfEP)ospEVdVQK%S7g)e4CxyUM`-k)5!tkFt*O|%YgoDhLMnu#h z7v{wQplEPRwpn&&$s3z-x8CG0%$-o~m^Or;PMP}{pdW<%v`adG+mbI&${>>aM*RM# zb7R!N7DHs*WJD>G1@BGG41l0qxcfU1v`26T$oZE6?E9Mpg=j4hMxWf!p+TPI@oky{ z!Z>w;LV%HfvNm+7?*_*lhxFa?7%6;C_m_$q3gkA|IYErGbQg>Hb~i}&6X%iTjw$RX z&Z=|b)$r9fUk~fm>p%bbaJywwvdX^3my{@T^BU<;i&S9hG-d9Jn1%DC8uA6XTvVy7 zHxHjZKRbKy>EiSD=DJP?(uqt>74;jI7!f(V%xM5)eUKtXiVdXQ<l)vU6t#EcJh79m;{E;Q+UeKm?IE7eioy?cLj|Mb-l zUmp#_fBnb*ynKJDTttaRGg30KnQ16YY%^I^k-^kr-oyM9qY?@Vu2?vBqEwWhscFa^ z|C%b3us8{pd78#~8n-vMpFSRM)`Kvc4n@=#kcU&HnD$I9PnLy`JhX_@*YTNkrk6H? zDelhhIXo>HnQuu`D|`O4@VMQ+d-Hm|**tyve81aYU41qco&7KhQmvR+XlD+Fw}P7k zt@3Rlygb86l*W=`W+FMiJ^`iJH_c2dd6xrj zd@m-))qZ0gc$IJyU`8(6=E2PWS@KW@k#%1kG=+(cG$jg|-*w~u;(#8XH`3s&guhOo773vm_Sw-Ltv@L1VW27w{RmUFVou#p6eJ@iR%T)@L_)d#Pa--S4f z;5+q02$Q)W7_E)W9sHpCQ0#au<^-bg4K&*HNlFuoMH?hHI|xf>X;CFZGJw4WCZl^f z$3ZbLeXGHr7#CaK+f?^1ns!QoQGz-(^lc$$jvD80860$%W?fkmP%h*^lREH9Uf$q8 zI>>En2Wy>2oZA^|8_57$HjrXrA$X+hi&uu>9T2SbIvL#wyXgkPqN$!vP3PQ%WAM4q zh#ouG$Rga%&W1$sPMmV=FG^|>g6jj~P|by~En{?|1Y*yQoB0f-5Hkszh>1LW_~^xp z=WkxWz4-Xi=E>^J5de7BA4~j#eRMB}9b0Nnnp6x2GmEh(mj@3XK7IE5^TpN0hmV>2 zH=Tvraj4DX1z>fO^#=;0u1O*#Lmcd%h~o%xtZpD?DNZek{gAF=GEf<4Sn<)SoNOLG zd-D9%v+cOQ-tSiHBQC3}>#Yp5K03SJju)5LhjG-ou2$>A_Hc8xtK&THW*w(#9H()# zx%w$zTC122g;!F>snT4n&f_@Sq^D=+C&xzv$zdGzFy3x=LezHC~didmIci4^7 zxZfSDRu)#(`B1Hl>-DM>9<6RR!{+``J&denyxDGVw$nHZuclgm{rT7X=l8z**YAhp z;s5&o{l~@IPkK->Du%l;BBYhgOuf>GDYpada(Det_z_|u$*EmKmQ;9_OFkhKKt$GpNb4tXH)WXfxTkUu=BoWP&tK4F*j#sa33v~ z8>R(^4JS^7ymG(?j%j920hxKJx2ZI5oe6u&ms8R~g47qGKICDYPY-Bh#$mP``*Z*= zqrQ?YvYK}?MJ8dK+-FJ%FukBlIV>H(bZuzJ*e>c^Zbu)|9s8?8(fa&T_Xh&kOeS@J z)VM%2gem6-&}PQ1sE6ZFxz45JL`(NKAqChRxS=Bq?_d$sb*5mUT1w84niPG0iQ48d zEr4`N-(^6-N!Sl~jBaNdKKtzgMhNZZt*JnryQj|dJB@?VXO=x5YM6j>Ip4{LQBoZv z)i|F%pF$aWdM~5A$Adq_!7${=bZC}YWGfj=BeN+0ameT#f{tVw5Vn0>qS8 zkm+c0MAD6=38}J~uv->WZ0knpZ5P5={!0GDoO3`w+g;vnOu2$#E$pqu^bA%mNJ3C5 zZ$H=8wWJX>G`XYFUHqMijKD8sF<}Fg@j(B4?+=@B_h1GJCN{}Fsb?;;3qM3z_g>amQOWs2igOWBNAY$pJ1PtjGzAMvCo^7|WJAKMV zQD>*zG{XpiYhojHBN1P;5yK||J8jh9fVxx+?1S=YNZvJ@Um}0stzgs`fw<0sIQMUk zyL@7;&n;jyI$env0Yesmooqc@oqpBTD)>4^h@Dz=LUR8Qiv_zNkpb1{L5pYN+l2HM z^7K;3LB$aC?=)Zx3NNi!5=7R~Cr_gaS!{~N#4$zw|7YtzyX{DlYe8_EdBjmy14yK* ztRngNdS-gfr(W~_|3j~yS<~W?ERg~Ub=A2?M1-652NUi$Nfe0$aL~&2MUE^brvDSKYkV{n#>`HM94Rns0cEDk=Skm7BH0& z<9hSv`u$&im`OiBK1`z9)q1XVT9%8=Zdvr{;pOS!rHU-mTyUOF)A{98=UUGbRjKD` zIvlegeJpHsvtSyusHiNETI=~VF<)L@4%;;oP4k4hOfOYso|ozJ_U7{Xl7@0PotJs8 zi`E51JZv}PZhiIU605S=a6NLJRx2)t)gY&d3+ptk)~ibT`0(Z3o2$S4%b&`M{?Gs4 z|9=15m*sgWI$|m)BopgAD=mbgRY_T^-GsnW)iMBK_k~DBBBj<2?@Ca%Fs&N)8Bnpq z!mz?y6yFlal+cA`B{| zi!S*6!!LjN=YRg;r=R}z@Be<7D=|TcXz?c}gG)q<7=cFEGW#1ga^Y+PgoQwL(T9gm zzkT@d)4%-F)ARk|@g79Ptg^uG!OJ9Xh9vrk9_Qr)c9DX%PbZ^^6(y%W-LDp?)NUTg zCB&=xSIVVtv0G$DB8h^XBem`bdF)m#h|wT1wjvH&A$1v{q0wWR2&%$C@jd+KGih%R zUf9Z$sh@3=rrv5Kg#IUD(nOG`&n%&VihU`pbIdgHNV|u&rETmb6Z@KnYbl1O@Wf7g zVGEfcX6ozb$p4>cfTleS7u`(+!5kHC``omI$WJv=e8p{Ni_@#BCq|6Q`_6Edc(lnF z8>bM0wl?rK6&YL0F^A!8tEXt;)8Kg_0-BYc9t8`JvI~U8Bqj4xrtKUCR<)x#p$u-@ zxiFKm=Zs??=~Y%TBLcRa18*-qviC5Tpp0KW+lrTA8Y5UjDIt_dv4M0RJqD;}H z!$gO9013KtPmP;)uQaX>+3@VKQG{pi$=9>=Q1Z2 z1mGkPcrq?z-R~oV18=|@d8#=mmC1CB(^;kadNw(e6RRU^WF+v7Lao|p2`!Z2`ki{6 zYS@EiP>sN{rkJrp4sJqWA=2G4M+Fhf&K3240_BFe>+7HBUed(}vhE^(_YcJCc9lZp z`wF1XQ8jIjWN;@v`NRa$fQVN=^{vPZbbB~*m*OAVH}j`GI(NhE<`zO7PHJ;#kyWV( z`~;CZw5@1>D8HSno(Ps8-yUQKxJ+g=UxkoP1UCt14dEuF2D#|E09XW-qPL+EhkQmF zwjF1qsQ>F0ARhT@bF^Q$ZU9ZXY^+4;wSS0cqNjQk%1X>!Km|PB-oCxLx%s#M@$bj| zo=H)wtyz^?*!z?<01a!`Dm>$8g(0G!d?&wuSHMcNT5rF3|9+AB@zd|~=|oy(S(Jpr z&icJf)(zN#_nQ0MG&3geyX4bXB`OV68{_xEGRK|AL?9Lr4~55(R_lxFH@Dxs`~Cjm z@p#&-w*bGqJo7M&!)m`j+<$$Z&a;yaahjh`8-Ya`FWnpYTRtMWn3xAveY_H%VS;6^XB^M;^yjdx1(_cvs4{`!yq`1SmB zst_tnOqD>Dl@=EORiLUcaabi5Mc6nOC=I$`=hFQ6hwX*}(Q1z(#3-mLs)lt0CUku= zIvq+|*u44(pM{}MEe(HwnztOeR8j>qB08N>^iE4u1%AqziR{eo0{Z12zx>O8`)_~x z^FRIlKmOxD4T>GPZY8%&j^re+-W|a{_*qA@Bcsl%lAM1 z^dE;q&DN`0rO78l{P=ZJSOgxwLf4j)jKxeoW=h@p2-#s?l2sxse_)+wE#o;J&(fx6 z3S=dPc~iy)Jmd*-UF}cg$A+dGh9G9@+ZM647F!|jh|d)FJP;f*8>dUN2CrK0mm%Q8 zH-*}ennM_Z7q&IlGR6^qX#q;)yOEw*V;cee{jbPG#!q7A!3u&Og_{@uB`fWSS%=Ab zW%qn-W0Djq?8A+ii<2FHRSRPV`wlG($Zq3IY9YaN+~gB!|D|B#faFn_T|SLL(^<%seu99&;CbTHU;?`yth zs@)F~?ObT7*KhT3Ob2>ta=WljinOaz2%EOo1y*z$N2=P|GK8K)7znAnp$<)i{n=SL zZ~vV0?PlttnuE^8pjfEf&E9DTy#Hs+vU9}#uqmBUYHDLB!LFMGbZ(p*0;ZNian&aU z6O)2LMQPaVE`Ip&`-jJ;`_G^2JQuYHur6Xpa;^T`8=GP-gbi%x-ES%v7>pPcrBVd(c`>4H`$ifdn@!JRh5NT5X7>_TO#$!#z znTSYAZ{D1b)2FZZ#6^|&heNH)_3mOiPxoK$U-pN2T6B?mKI^FX6Kb5+W45EQQOsDzXr&=v=2|Da)!bz`Py@Ce&KzX`be~oaX6xT3ueOuP=5t z*W1nJ^7>-fj&rHwpl>dQcQ=%aWv zPj@$$7Z(>F{_*?$=f~-AR?@kBKT%{U$t3)I;-Q=O`(YNl6m%+}QLPIyH3VvhoHA3`F$jQx>6G zkNf@K|Ni$s|MNfp_~*a;)7DcDLVw@@O&8tc?EM;luLmAPRk^Upa(H?A z^yTY!fBNb3r%w+bKV_6Jjsd7e*Fu<&<4TrPNE!wc3daPMU1sO2N>h*016SW z`y(BWA@%A=@q><4CrPpW6wOz}vCt+9l*{R#X_m@g-VA2HzzFvz2A`j6xZgr3O*)Vd zi$y1o$tJCFB!H&jX}2}^bI*V1-r+7#E}?Ubl!~ySIrV73epl= zk3O$WfI{rJ3*vv|n9zpb#1DvyEDf)wGsbF-bu)t{p=_GIG_=I^S1u?SZ=iJIG*+SG zB>IXB*)fFJ1I`O$y~}6EBJw-#>P5&kDQA=*A+R*#c&h3RNQgj0L*pzq*Ie!tp3p;% zj8bw%N=g#(O{AhwRGv^9Zbz+5s5)MiI6-YZoj$aGPn`?#?Q}n6C?269#<>RhVBbYV z1UA81lzo?a6Hh>E`Hv^$k$d^Ck_{fzGSd>Od|| zH8d3dgXu)~)n-MUpeaDnd01cV1Ch&alj>JVyVtWs0OTuYRsl|-FtzNQjxcoy?B-Bq z-ye<4Wj1M`*E?-`7S)v6D-q>?T42SV8bj{#oAjUm8>tOPR1;?k{ZcgtQqxOHk++`{ zC{FPZGqHeq9B<#exxKynKmPas<8*kzJSnM4C1@cMF+LQaLLrCod{?G07|9`+iYr!O zF5~**=IZwD)2Gib&o4z(WudrD3=9!*d{l+^7U5TAsdcd`jbb)z5dwyx$Xtmq$ux|^xLL2)1J!YQIg&yreR-;7wc1|n z*6VUSDX5~>!o&1*JfG`fp3bstFLpf2Zrp9g;cm0};bQm0-R0A2*xx@N=ks)$rsK57 zFqER8&2}}{^=`W!K+B|cBeyr#|F8e|fB)wF&ENj*=g+@CyxhM`FQ?_`cihqtB|C+r zE*|L>Bc`fhX+6!ATIkCip>7g#(IB|Fjk*FOr7E^ltt3oS7ZG=81T?xM7tWi8t@Zla zK=Z)fKc@CG2p8ZJ+Uyb|P*ts30kBrB`uO$hm&>d7-+uf2wEz6^ce|bu*8g102%A5X zTsE;IRMA$$G~9ZdlSXrKVU;>fAAbJzyKle0d;87PQCN+uhx~z=pI7u{#r{N7-MGsYku1%iNwb%)Y*ZU(w{`JYr$G@7w;$$e zrTJ^d#sH7wi&iHS+}O!h!=bPjeOYtbi-x4ltoySO{LWphn3{KwG8R!=j8&c^$(8S0D-vyC0h*Csnv`vF^4WmOwzr4-Phx|oiBpwt2P!A~joZIh7 zA<$$l_S3&=*Xxl-t&_cKZlb;}kdipVZJ0x$WQf7IEO+i|>b+}?*98Pih|z=`wzr4x z=a|^swQg}VR~ivniNSOVXPxwX^@a?5bu#&S<@z{zszNf`zz6#bu@xz3G$eWb*z_OT zAQ8I#Ur@#%Lm{t0F3GXQnOcj-=L+WbQ5$n&dKVV$g-k>Iy4}v_2SQtHCZ}UhVSw1~ z#G$k*p~4N`A}8QfNBuW{b2`I>0Jzvqh1kDxbRLZ5P`k>zsYigIs)je`cd7b<c=OI^{SGmBN=P zpfDUULex*WKv<-=9~4D;)kV=HnLHK~4n*vm<`bIDekDE>VdW&8erB$_`K!I(8NCuI z`OP!dN*?4_5qAll01LAa8rQNI(ot8yXUXqKYZI)o5Qv~+_nX3ss8b=x6*$~0zptll z1}G!_1z=D5DKW7U2+YJ(22~<1R7PH{fBMUxzkK=pc>h(GWnh8@{xd7mt)Y6f%t%$> z4CPItj&y~JPmYR!H$GSUsK{Kg)M*h}CaTKNx>PP@SPe7`woTj3`gA-`r!y60D&xSzYO7VI z+PCLnUB*ra6wTjsynt5}%F2RpQo2(%9+X5U&K$WhQ1OeE$7c5WfHMhtv7=^zfCK zp@krZNrsSwICu+%_x@M&(%EAXYHOvU%tZvmc6r83%!lXwKmPuYAAb7j>G|R7FCU~X zM8y~yp{f+MV37G4BwjD@%GNbgk*p&qnKmvQlqi-3K_il8Ibb+sQScJCB#{g7<;|Oc zwXm&LLU_l7DMAb6H>GGJYaZj(ZsHXvm&;}eumQwh8g0B7y1vv zI8YNb zo8L_vn{KPcu|=F-_uRZjw9ySC5|5eK3G$_(F@(4a(9zBWU)8b$ zUb7UL6>85X3}n&EV5=r&F7pJA{CmfN1YuFp&4;L3WO2WrPne0F}KX*vF&mtm3seJiT4HeZ88vC|GejuKO3vC49r3H_&VsBG2QN+1y zABxY}8GMW#A+FHol-uf3*fk2X6u8>xu^S~N?P_n4U$y#M3k#_Vp#_C#A;x9(^oMr@Ggvb_3H@P*Xe{Aqr?xohg8s(@zN zDo52>HVeDwOI~FpTzn|N-Y5jN4GNt%7KBo3C__k(`siI{n) z0%8)SVO;&`r=K?K&ACVE}Pe;epK)0(qAW@@)px zk|5Uk#|}IcA_haatS&FMHatA71tV{^SEzh`Imo=IERf+`Pi3pC zfti^Z^K>R2c`OC2ixw&*khwx+1u6_#1WR2NUJb)$vmsg)8vLT&0Rp@nr{%O@w<$%L z2ATlK+qbv3S2uUJcYnY5$FG0?_4)pJA+d9UstdI8Ky?uaD;0(i7od_dgb{5hO@K9d zuqvSmsw*In*Isn?xO4NJacRUQ4YxeTz+fc?vr$^M#*lR(9z+%fzY@GqUt|a~lCTG;?z zdxtbHF~93tY7D_yFo%H#%##s~6K%l`+HY;gMFa|oEjw*OEhN za5EQ_z-HC#VTDW(jc}0VecUM9QPH@hvT-h7n2rgDub?`>{ka&5d*!B#5wnfWu=i1r z42^BJ12^Pmz6)KVb4gO{?3vxs8A=P_iyc_wFNx2!r}`k+mLj6h>i0Oh0qI|;$RGGW zH0A!or(AC$U>P1jF#cQ#O6Q((z=452NPA9rsNKH@BBl_eNWU6Syg8Kh^$$asDG<&X zsPBu1vf&5mqKD8QPypWUusOOEaoK!9hlb(x%Cx&RY3$YM+A4vT6I@P0|{mk4k zx~Lp--!;FSqf%;O)y9(#cL`{&sY@aC;S#^CgOY<-%~lh56T*mlr90Gq9PW7+9Z=0E zVVW8WU<@ZdL?@=^5FqW*y&C|ic~Wl732ADHsI~geQ#lS#)C^1~qT!iR7k|qcw=itC zyi8maenoC52>arsq!O>z>;L}W|Mk}ozdt`d%TkM)O%%1p3fA&p^D5%d=nO*=qM2d% zJU%S4YZjIGn{VEaqh(#To6UB&g)XPl>gjpX(^Oxk-R|P@=5}{= zMbMY0=X$P#66qqRI-_zK2j(hL>x_lvR0fa?XW6W#{dwAq`~C4^RW_SJ3Mmg%hH*WV zk%uw@T4gB%m!YuIhKJo|wcBoX8=mF#{PZ-hrp;>e{=2s~*H>@f-v0I9{`%qPUk|b{ zt(XUy&N@{pezzu3@#G>~6o{c5Br>^F?)+=`%fD!_!-T|OEZT@x_f7NtN&zzsWxd&~ z*PHV+osUOSf`>&FQezD9WOZe0Djqx|G=eCPj3>?$L^O%l`DOp*)9*k2`7b~G_~U;Z z&oZ5}qd${%7paNiMqU8z$^Z}{$OyMXp(HAW3A#Y01Wa&UkD7*3w@Z{HCJ;jjV=k^X)*WUXY#cul{TU zM}snNR^H1%N`wYO?eX6EnElzZzLFQa#To$ph}6N{@rPWfBHWh7SDUycsA^{i!h}<^ z5$rs& zHSxxi+V|1#TH6?o9c0f>1YwJj_uM2`-eU&jEohbC*Yr{FbwfRJ>tQJ?3n&M>NI8k} zm557+-u1-V#nK!zR%<;RGqDI^ZgVj9Cj3T?m}p4cF{*3yPs0Sq_W7eEP%p;SXSBt1P0Mq8NM@_SJz5Z<2@0isaDz?B(OQ7p7d7KRw)WBM zAX8Vq(D6n1It6@Ng&eop#jq8CofeIlO8i)3_Sy2O9f5zXP#S&=b=%Q|AR#O?2aZto z+4yRxR?yMdub8=1?RF!#BbHUEnZ>5fp+AUGX)jx3I|EQAIL*gl?RSO8-+ujlI-NMqYkDO#pa!U6M>LPuyZ;hfVxv^` zD10F*EL0ZY=KALP`sVYO`_th75!4D2k%hw07)GI6^=5lGF-T$A3ow*aiHDWaFe_bM zUH|znKRrD@e*E=UC@Z4WB>4_JPv}6z7H(m1S#P%&mzVqf{^fZuRd>VY@&4)d=3<^s z_a8qW9-sAe*7J;cAzgG{!cN<|pQOgJ}f&^*}J496&bvYgX{vUta{?8Y0-@ku; zdLo%2OSSev9-NEw%I?m%IRlY3NkO-6ngPy+HJzbuWTSy4(zEEyQ~SK;Cu1g_KkUAY zICrcN&YlakJV2}-<$UAyx!wxOmZvuCARuU@kBM5(n~W;=TL~uw)Nc@g zG)1DGnrSOREpDiB>j{eWK?fzYS6HmYLra@)n^I4V()!9Z(f zVs<1%C}d92ZC(gnA3Ub+5A9PX)~z1GcThxIu)tUlxm{8>xk&SUZDmeGSo4>9KXj`l0Vr) z6S@*{npf*jVaLjFzOxOe&#d|M-F^`Av~|3skL9GWQtv`Vh61 zq2QAd7a_a7C`e#zS!_iVHmfHk;8xY$J~z$)2$@HwSj7yCd#WKM!{sT10~+AoLA#yb zCl{{}5Vvp!>gLY5&lV`qqn7^B66eCE&(O(fGiM#G^zS0iu~M9dHby}aIaa%4fi#uP zbhx`(GB_Z*Uw#8+u8GSOc?_UQpvy}W^{m0OHiV`ah;@8b9uX4f!LHXo@=Qm_DI6%x zT(}<=1mB<{%aOX})({u~p3jVEcfg+l#0A6(2DmCQjk}xcfBKhy`T3V$p6(y03X!Ua z<2aeXLo8vfs_pK@$VcRF8B=t(omB~3R-5g+Z@zhc-akJ+pjJ|WimG_dR7F**3YU`U zHPaMH%gXM35s2+dV`47jtVD&^+ugT6{&4r^?ZelvQYGYF`+hw0x&QoiKAm}3FVlGduD09HA3hwP_i{Yx>5O@%T16Htl~f@DRMcvr46}3!ixop5 zr}?;ld|F=4BMmDm%w?(bJkO{3v_9{y-`uWuYpruVO}YpPs4PTO>q6_*xLz%b$h_3) zR6xu5d_M5=6CR&lZf`Cw7c3PbsLOIb&F4vmQW->rRWY#6Dyuc-D*My22#q6OU*4@& zbUGXl$NhOaU0m$m|LL3cZoS%W{`!CZ{QPmRDg#EDfhtr~RTYv~nWzgTYnm>EqD1;V zbR7s++2tp+2a&N&sn27zMn{QAv;w3mQtLcT0R8yGpLUzezyIy$^XUL(Sb^aw5-C01f7$IW-hcPqbe^8?A5=|N6(DeQLvOv? zJq*%06dng5QdqCgz$ld&;4hy)|NimQ`}glZfBty?`+-X#DJn|>OeQ@w|K7nzkj1tK z$WTsxtS6sI0_?wn?wQ&Q9~y4FX8qkG66f-?fz8*n)kAtoHa*5>JfZg)5{Vk#G4e-V z6#+{>T0}*Auq0|REyNEKxBJQYjFMEiAKD3OY#0)8*zBoyj$(=fBprxo%k{o;p)^YT z%`C?aYrz7Ph#*oam{SU%50sF1j|JP%=Ger3Cb+lAo!gl#R>+AV{G~3s`|9W_Tri=< zf5?D{+UA+4YdD&IhllpG%|0C-acp3VZN)2c6_EShMw<0KPEJBzL-I6-9qzt+fw6i7 zz*?&lhQf9rAs!AwUKYtjMM2_cRFvA8GCL0AY*#e;_z>9vQD++Vzzk@Hki>7+i{f()|ya}qLB~KxDTQiZrhId zqn#pf*y;FqjyXTqyzp*<9Y}ENdTm=B+Y#>)&2vwa%}V}0dn^5`94-x zP4fp>iIoUcl!x!X`_sk6#sB)h{%^}PDXK^1$;eMMeNU4S4o(V3#~@-k6vPFfDg>st z@7@pN@cW0~=kwW$wv6Vv0FsIdSnX!i@N2qNwfS0@h3qslD{&%9r&D<>`d6x$IcQnvfQAVg)my}rFE10N3ut#z{<4*TbK-`t({&(B})r{@EX zC!Ee$CQ{U8VF*MZRl|(|ktj+TnF~~xX`c7GT9xHIPp5MkG0>pkvxv&fAUU1SFNgJZ zv$?o9ik?r4o#vuNm*Z)(8phSCY^Y8`0Odd$zfwfz=2Ea_hDxNYWKv6@T0;ee4-H1?_dtkrYeu!i?FB5(g zrpk{JdsIQ>^m6#yfBa3P{P^Qfg}1-{_UrS*!@`5=LNr5E?doXt;(N05RMIZhPNPFD z8jbwD@p$uA>O2gqdY(Rh__({ezP@{NJe-#EQGw!jW>b%d{sTN`M~7pg&O@s?_c+_Q0m036ZbkzJyreSc~}g zSNn)&xf&it2p}m0e3SA^?eo2T2jD{P*PQw?1p2`Tt=<)iBNNoYU`Ghu+!DODiDPUG zC==JHk4M;1iYTQkPGeeW5IU!u0Lea>VMJi5Da{Y&yhJtlfwf%o9}B{PAgGuVKn}}F zsuBg7ZA|LP;by{OlPJx6N7K5A1TBrNvDIpfuT4LNffCE(N230ZVTyvNsoKFIv%3WQ ztAe=7&|)AWd`ZJFL0z7A2-m$Pf?84UQ#_g~~r_ z=x%(iU$3U6uD#^4a;JoCoHBFv(m=#{ZvEm_y*@l;h-28wXF%yLaor;?JHEG8A{~W{K4GTm?)Q= z!il1^K>lrbccXxd{}dyL#WY_t)}{rcMh?Z--d7oHGEnMIH#~MN$XY9dqVRcsF%es zs!Zy|GXDOE&r?pR3A25xlNllsqVOnfnD$;LGQ`TN%PZZpFjXuIB5Y?;;Xp*zR)v|` zYu)w~5HlA6gNBQXtM9-6;pbmI?Du<>%Bms@`$BsN*fYK=MBUV43jpfw<*`MzzcLYT zFRrdHt{?6nr_v|NiT5)6-s#CmfDE zO(a#lo4vpehC;pA#j`((DyTXi&-3}Ty1w3&&0LR5ovC0L$KB<2o=xuf{Bj0{?RG0$ zD4l8*20=-Mu&7ii!??cM=z@6`VV#%d<#2e~uReWUS72PNs-l!)%T#N%UjB8dGA*?f zT`C}jbsTB8t*h(H1D9b~FLgQ|riYhh82Ik?^7``PFaP>aa?-yovVTyiqJyeb(ivup z3q^k08tD6JV5jYQ`E^RBNvTL@m_S6#7oPrv>8>+}BWVgHPoptUvIwqUIf$W5s_ei~qz7A>FNH< z{q@c5)!p6wmqqH#DrWe@v~^||&i?)GuxQ$5(PC0TmJ!%@lLcy~??Oo_`58J;HQ-CS zVI03R1=>t=aCq{o43l96k%MRiQq`~V4sIH63`nEI_b#Jhu}yVx0GfZ5(nT3ebY5VP zVZ24#yIT29JU%kp8W(W}HA45d%!)XP$k-V*!_}70H_GJLV5d1s47=pt5 z2XS*4`xs9Q2ngPU#_!!AxJr8bs8Kv}e-W@F*BObVS2AO09#^{>Bst9^MyS7i!|jPR z0OKh2BrYO^YAQ*bhAcdX;P(oTSY$jR!|0Ap2KInPT)q(+q?3&JZb)XRmhG6jmTIn@ zC&O*>ejAlboTbCO#6PX<;9<|mwjq?o1%cb9Bynl74L9(iTjb74H(6&iNsE$Ek*y%9 zxP#u&qFA8j`#OP4Ztb7&m2y_<3l*Sl%j6NGP#!nju6Ig(TXjQ8=WEEjE=5PWU(FeQjlU6?$dL0I=RRLOai`-XVN@==ludD$-iLJh#2~BA^ z8xPaQebIy@mo+0JaI|f%p-G{tC-Cx+9<>EZ;Z9)bfin$2Vs!iy;%!JHlSIeI#vq9Z z5D{5}G;LK?@I8+9uV%Vf5tXTaG&jt?~yavG1zc@oHWvtDh=QpeL_TBb$M zb;5Zp<92shR;!oi=X#viWu?OPupH|%t;g-<#b&+PUR+`*3(>G!t+rbXBZiSzn~R(A z1nkdM^(2GnB87~dGZ&Pxtkxr!Fn+MQFP&z2c%GDo!Zxfx?l-P!;;q<}K+Ey` z^WXnIE%nd;^xwYy?#I>Rrq*9)P?e+AK8UfXsDHLIh?Gf6N9u4b?Jtf48HXgh30XvV zfT})z{d#eIb#r^Oe}0~C zfBu($x_kTP>FHjVqO}@j@$1J7LrlX(!;X0GTcr9YOp{wKco@L3BOx;p(5<|uVM50< z!ucwu8EY8O>8MRZ9Y|4-m}8fvp>(#dC6sO`(_Q8PHYI(YkOL}###0mLB4dmyCs;j|m+&S~ zVS~t-lkc2oV$<@44npk~1W9w6!Cc=j^Tdo%Ne(LvtcZdko|#byh%cHL3WPKL>WKo);ZmnJwqT8k`9Qj@ zww01d*7(|`gG_mmFIr5N!rnVVF|&GS&h$f4>B5_`K2}~A(6Sl*!iEH~iIFzxNrgp& z$*?@xtejE?6(BI@k(I3Fata+l>E$*dqCEwpbY%kFY!>}E;+LCH_@cBQxbnn9|Ic#3-5T0z8PVJd~dBEkfcMQ4m< z)67Fb8HGhjN`cPP%kzHAm)pzTJe?n&URY%u207ERpdNXir|IJ2vhaG?t=8)@4pr$? z>wcPL7`d#ttU$cXi!MTyO(D5}6m_aonJr>}x!Kf3$DtII&1y}0I-eFE2BG=1JS|K8 z_RY{y^T6SDi+95wb&kAQ7ivC8>#&9gy=@yB0({qfI#e*gZP-~RD)h23La)x16cc4mmdU<*ZoNI@#m z!QF#vtPQypQkg5HK79Fld3W>n?YmDOe}9?IN|Q!QM5lr(%mfh!s#XO^XjhU^Mg(GI zlWJtKM$kS@oyy@TWkwq9{-yv^e}1f<$4%7;kh>8e_+RP%E<)_^mtM8wu4xbx42Q! zDk3UcjYv3Fn)*_n{iVqH`Ouh{7+no?V~>P%UsV9Hv=+zV1``^MqcqN!piUf|FhsK5 z&ApmUFp6(|S&Z8gs1zR;Ge_9T7A_9ExHrb4Ql%0RvoZ+?)lXbgLQt%)0vZL0U?z$6 zDWnBTu=a=MR1OtqtXbdr;xj=Y|F8L7D+XR>89^oKfKrpVag8 zDN~|S@2LB0$VdNgjSw2yZ61ZkB7~b{cTkj*3Zb`2J1f(eQiwG<&+9OzU*)8!8no4R z#0hE4CZ1v(Q=$`+dEKIJIy9C9(F9cd8y+%%-31tF0foHsqg1rPkc(qVsM)88ERcGz zpS_bVQbAcGCB`Ag-DEvQ;GRM*P8c(Z0c4QL{akH3YdTonlX+z-G;XA(KW&Z%62+B}W z{^lHo0P^}nA}B{Q{?v=u)?5IDV{Y%W*n-)K5Nr?;m{6P<6Z0^>ef!ONz4`aQ{$n}M zR@q`#|JqdAV%dGwc9j80jY_*fb-t-9F9`_D!)kSRcY8d&9FH$`K1a1|JlT-JL5D;n zOC1o<_1}kNR@P=}hy2Wd;PO3}~%HB#RLF%~-6Wa}~SV zl?lu`taMn};*Z3{w47&%(l890)q1l&tJK4}D9QPJcoHhOd3VbN`%j;X)Ivj@wT$ce z2N&GQU>9Hc|cvv>U7k}z>h=$X4M7W z89M^c4l(Kq*yD1~Svz(VY>y&tLN20ceEo8Lcm4j`Z~y-MG%FF8JYwOTxTEie@*;u%JzfQB za^!yd*rqn(p{dEX4naqpYIBRHr&c$NhEd~y%&<5aAwWPJg`m)X^@whjTo!s$VD-96 z;J9AbofGPTQlnxf-uZ`DbdVm&<9=^)k8%Us+kNd~y{L^>BiNCP1Go)nqygI59wK`6 z-nNY1AkB?D<-f2I3V^U8Jh2ia;|a zPR+{&G#nc{5aFJHD?pRPwgpkx;Iv69MS?CQ&Z~D+m4Y6b&0-=h9N|VHL^Zg(0bw3t zVR-nxgcLgA7ya}*?yrOU9g*R(J*HTz*azFg=n461|D%@A#rE}@+uxctbp*4CS`q>B zN0SHD6htJdq*hY{w%oP1&u&BTMT@3NL^Pxy*n*&Fl3+h;wwhf20>fYLTq6@PllYh9 zeU0V6qF=qvrDz22dPV7j(Ae5pcq3Mh(x-$ERy}hRxdq#iAg!8z^lv!!k?DPFCG2nY zagcJisBK1VuN25{Q%=u6+5I_}Ek2nK4BsKaA?nj8U(h*FW&o>16`tSyW~H%7ncU(~ z+|6PDBA!}}d&5HX+NzKz0_*`2*A>}zL`=XQXV0O$raAn0Uu55HrZ6fd6PDmkOh&uf zuw}u*?MepzD%=s@%Y>CRVepH<{($J{tcQ)8&>~x@#UZZ71<7%ZQ~J|Ry%zuQs6B=4 zk4{VqWHK1(EiECgkOR=}6K*O*L=a{Bxsr0Ci6>5Md)eJ^YEgt11*2RrmvZ!}FneDR zuapB0fs0u<|0XEWIIh0=_S=Vtr-!c(GA~}pPHKfZVNL|`l>pTS4)j{M74SCFU~sv4 zbEm2g_xH;@Lp8gzg@F-x;uG1=z%n%{IW9lUVw980qI7k4`~LeM<^^BB-XHfb%lSO8 zsw~vn3OBLw?Wdy3P+~52DSdS%V z1@la*(Z)dt8eJxcpju^_7OhpK!b*UKqQjcjRTy;`R=bNbmg%Ir%mWc>UGLUf>*>5` zl|oofrHNL%j^QM3f&(HIsTt-+&r1Lz3q$(5b=#s|`FenBAD-|9_9tI*>mW3db;^l+Y(>)kiEkDnf={b@R$m5N%X zsBm2tfRtFhbizT?B+<Vu;C8%4`}f7vDgKSFEO1W`#n|hyA_OrQUt> z&DG7_`yc=G=>t7Id|@gacjBomUE6>VNV9%X0qx{1UGz@4RTX5{ekrLweEz&yt>3}R`#*aj9Cbh1ZlQX>=alH;}M_^mBKg)GkxU+=%%e*gWq_a8srAC4;3Ty-K1 zJUIBbAT2$s(Jek}Z|AVd12PNdX-_@W# zKbPN3q|*3sFd@(R0*D)&o1Eph*)b|B6WTBR!=Dvxfo!&~Jc1ZF#Y8CiPl_MHGO2b( zqX}v^O%o-+X5nZVj5~&cjPN*AB^#r#J3XmGT>Y6-X)zOx;d^wEqDllVqO}4*6;<;D z01qr9j{VsO!zT~+8jOjeh&)`IFyxWbj*XxU^C3g0aHl?Ozsm*_tU0cqu}*8p9QRBk ze}I6ACODp{Xa=p(w~ofSI^;IHiZW4L2HQW<8Kx?-8KCQk3#fuT?$<=AbHzjdx@Wfk z`bWAA%Ljl$4MIb#4N-XMnz9px9+ZnfL>Q8Zz7}*6n}@Vv&IMVOQ_4TaEKY;)lGMT{y%YQc)PQctXBH!5U9hK$JvT0gUje-w-~XCJx$o z5$w~gh%2nRj>Lq-4P-=C(8MSL6uF3KK3B&N0#EJ#dcAT{kMbF(lT1nLQy{yoax;*! z3<{^qN@~*6H>C-Iwh44ZgPMfo-ce`66kxyRGlow!(AA8r`$DcCy;X~y(e5u%-A>4-9A1(9uEh*iIG5nkd;NTlwKd2InN1-DC5{m@m0=NDZ+Gv%`<6=i z<>z0H`)5)iRft$LNJM<@8pWZ+IW`4xDMVa`q5}_`_0{#Yio85OZ^u!kmT_2*{PK8T zUyhg-S}LkAL!u!jm13n_5S3|06|FO>7(OsVwL-15k!4sB(eCCF1*iQ{waUCKGp%;( z3Y?!`P%BBrvF<;9!CQX!{htnR_FwPs&&Qcmi;`B=;{qvXebJ>(i!R{RjVywS0U#RJ zWxXz|%8LrBpZft5@h}Xl_3Gm4aaYlregxoIe=Aib6%r-Z zI-g#i?&o=47QKCQ_x}6ut1hRPXPqa=V)k9VP^TF*gD-E*rWvMg*N~bNPal8y?%TKTzJ2_9 zFLepvw)l7L)Xaf7h@P(-=MTbAISCKWI-`tmlMvja`2;@E z5N?e?7~JYM27us6apH@--pu8pBNgehA(r3lU%Ze2zx%xntnHlUM`FB+@VH&I%(6Qj zR%`X6sw!2r+7KM*S&0&5m)$=xlF#2O3!)5DQDxmNe`x zh3aY;r7%|-vZjuKpS`evfiAqHyP~r3F&hx_23DB;-*2haJhS&KIR_=<7|;c7`K-o+dcdo*I6vnHjd? z6Zd3X!_vF#-<5v|pmN=5JDboM@*QFX=jhF|dxdHd$4a53_S4K)Hs7QlrX^%evBOt( zQS6r7A=v^-A|lEHf&1s}G$*=0#mZ`q&|tP(7VT%?8>Qi&H*Vc@xZ4$vToF-~#Si@T z8|w?~-|9ip%^!KkFp4f(0LKw$d#|Q6k_YBl&}<|EdtBU!3I*Q2mTsgJ`k51i!J#JJ zuV6Tb=@v*+X9G!HJv6S73PbD1OI~>zuXsbt%#^4 zCAh@w^N%iZQrIKd=tiebKEva^8 zKfxbT3Y;3+xn>bsZPs^h-b~Z{{QRsnV2bVDdnFQ8p)MJRKbU#5Ld5X42drQPjoZ!p z@4qcPeERU=w0~A>$_bI`C6l6FiWSDli%XRNCM|wGK|#v2z1&>x*3b7}>*=`LTpp(5 zySMM3A0Lkok9t1wJn2$N&8HwRmqK>q=W;rsiq-{HNL-k$PES>d8FeNeQ7Z^nw>O*1 z?e9PTx*R60kQv*}YIn7TF7xBTe9Psu>>r<3yUXi0?}pvQ{r$uFc`xS*LMg;Va-L^k zsdN&Q^KxA33?5gj3dJz+dZ@BU+|R(oARdNscd;AB)#)_v_s^YPEO&qHg)%FH)7U~560)nPeelrBuylH zMv6wjDdSO1LqSwPLPUf@P?bu=RHy0i{NasTvn7~X&X{j$9M{AsC^C<(`3XEMC)Oxwr2?^lc3G5CxMxL(R{2 zYugb51C<~~4Sv%!YS4I}Z2Q05N0)j3Cp!a>R>b}zn@cxfJ*~64P)Vtis~Ej zM*R}jEP=8;^VNT}A8*1||B)c}-NFWw>M3OZyzZA%V&^S*#n!_n!fAp8dxNiy1$-cZ zdk9A;tg}BaEtrJZwyo2)beRv6`?ICFv3fL1#z%kJ&32^-8VO9yqM!)LZ<7_o*EM#%mpcO5rD$&Fg41_gAlLxqAzN?J zsm^LCd;l6ca~wGjN3k@4Z6KEW&-vz`ZKF*f;!6>j4Ump-34r&r@1m=5 z03aw>rzg}Ln5KlJ0D}YBDEC04#l07f#TR`@QRb9M1k=n85HxU)+_8)E8QifkbiXoI z99wDwkCLR=4By6B!DwWpfuI3F>KxBCPe~LisA62AF5;Cu673!$VwH-G00>9k+)7aQ zd(6YWs;~=9gg^w5kgEJ2d|_3CWD_9W`#rbNEes-0DO60{UA>kfkpkNK;|L#$UmI_l34yk7J0gmvUeG1X1G_fHB8mmmy({v)G zp^V$xx9>iE{CqmRKo`;qT@2#gkJbt}WpA%;cCCZwh?g+4fVm92i>ux4;?t*(ved9` z;y0?0kXAN;u4eU%oy)K0<}0s#a#IfOQH2Tf-W5 zho8G%ab%~_SeccH$KmGsYAm$B|EhD9ap5)Zwws^-um3SUzu&;dtsnawq3xo7{ zdid~Zndj~8)wkcgKV0md9`@(M8B~;g?+hJ?X%d;1nO00gp|NNo4A4qawQynLG7RHp zq{2^6hsXVs>ayJo>-7nuU<@msCS8`LF7@zqUfy2GBrp5p?s{{5clC8UoQ`%vjX^~q zL`6y5^qJwdunFi780h#BLM?uDB$XpJnE*R6oGO#Tn+k2}uB=3EIkl>&h}09l{ywYJ z+c$6Cy!#fahbNUqp_MhlC&7d$WhxXSZ*uZcQy=1hs*tcq<$}8C)8pgi&F#&bZ(fea z{lns?13S*#C^tIl=kWxE3b;swW}ua{lAVvN^6}H>i_5Fa+c*1%N6c0NSVY7+i-w^L zB9f7o2wygf>|`t{j30w**b*hzp{yEL(-M-?z#M%oUPslje}98lT(-!*(Gh1?S+d_S zR`SA^<_4g#2x{x@R=7pTIQPi6h2Zt2l*_DLx|;s*a5X74V%eVc`cstDE(J8LN3(gx zef1Tq>n7tXK#tUD=YtkSvdPJSTbxt`gcp()>ea9R^6ySYf_o{N3Mwpk3{nOY+x>td z!fH~bK26c?aq;XtjBeQsC`CytGew(Dbk+#Xx`5;MH#P|>%*L`EB4eAqV$Ug>r+t*H z_#s2{-AZeYj_e$o*RV!x9t1C4Ccw&=jkd+%bu{_(Hgqn0t$YLVG22t8Fcc*fg_2(+ zXm}A#{(FojkSwJqB*A{>aRd7qd$iM#lv>>ovI_@i934F!8jCCqgU zDCrILyWf;k8=Znnri6U|7vjFm$x6a@KfwUJl2@;XvW$RkA(??*6F8EhxTbk;I8jF&fX-tCUI^ zhwC?QuCH#Mp7zgAPg-YE$rCXE5tk|g5@L2t_aha4st|=xilf;uY_4~AZ|~;md^jGc z43+Ta?#*&OA0MCeaMtsQL{ONRcqlY5MCvpz(^;3rZtH-mR%-#}H%U7jD4|x+glU@h z$H&iK#`XC2&E3o6^K>{5#LM}twH7X`&3Y*$M>7O+etMdx<2)U%-@V;#w)}3re>u(b zEJDo8!??P*#O`u?cQ;<`wwJqMAO)><+vPNi(pZ$L48(<)2c|;jX_B)rqYShf=sZoU zaaiwG<4k3mt1b-HS(ej+%FoX)D_URO-dx>XO;3BBSXGDz)rxA8#i|u4`|5Vg0WN4m zVtQsJU4UDiq&A#Q=x|Qt%1d?@2n<%$>P{?Fr4~ZLGEepMZ@(T7hadj@=eOT{w=DB% zUr`s2yF(Cxzp%ggn^J_WwCO}xBUQ2siHQQ@moHyFueZC)>)Z3;xSRyyPK8JuHcN#K zRcX2gL}`VeGV&Wx9jG3Oa5Rlhg=`F^ zLna@cvAEbA3hy8g(&pkWN9WV|^D?^_FaTI1Wa>SjWO~hFQX*7nwT&#=m#FINQq!EU zx(U?=eUw0$pGP1(yiY|l4@B16PSJfgEmS~n1nyp$uioA@+3A8j@o%xpWJDPf3mBlD zAF?wb`M(6Z&O5fX?f6mCq8L~gU8T+d0BE3W&$b3xM9ihAlBkj$`E}PLd^pYR3w zuNyrRV3&V&y@<$qC{3pbW_P?>F(P4-fisQEID4_}2rRcPBp0ebY*}Y#Lw#P+ie~VY zm`MN<#I68vHbOuqBmwv#^;gVez$+2SZj;ouBFX7%R3c>YO?15R_0#Z8+)Yiv4-Cq+ zP7Gk>t2FWYboz~=?skvgt!BjbYTaU7}#YzlFZAbvgOp1FW zQAcIk-f`&HTHPH2u^OI?V6{Kk5E}#Ui6ezVP&~?|pn;BB&9v59=XsfCS;X(z&W5KJ zx%KsWonvvyRNF(ZkAkZAWApWi4ebTb>9FKad`l$itopUE!$MlkREQ){`r8z+~Y`g6aMU_m+q;u^18#~|4nmpxYH%z7ArfR=3 zB0yv6A^kQq6Km$2eQ(=C#59D?`{ie`P$XLn7061%`$#@iD{)by#R{TmH45Kbbr;-s z-Dm3C7w@GutllvJC~9|9LB+Iy2s{j{_4^;bdwhP{@1Gzuqe@+%3P!OTAwr0H!SgtM zXKBpemN4VQ%v9E!-FmbA{PWMtys%bcu@YxD`d(*ijr>wQLXoT)LWayr;W8?f_3rB3 zyZ7g5`ts%TGR;~S60vSab*Ws2YE4uSHZ0Gev7dtQLQ^hf9CS78uCA_bu0Q=$K9LT>v!M0 zd4IV-9iLwg^CH8*Qh2=@*5k0B)BX>Fk4LI)XuOJj_N%s;npp~}o!2-LJtcsH+9 z_YD1~wQezi9=Uph>A>R5xS5Wuoj_Fhc*HbqoSAU52Nv20vT9lpY|J9E1fb(aITBJ8 zD+mFIUI|LDHhXZ~=^4Zs+A&LV`{qGmAlV7Xdk?e;WWXfYPFgTGNt)(6c#YfJAWmtb z{1fS{<&O2X)ENr}IZU3@$cy`C&hPy?SeMUO>~2$06d*)}aY~5WVI>RLyBDckSVapJ z5rraDp!)mO5!e~AyP>c~J4^)PGL*s`gGBiK&Y!(qHIeJEUljttj){gvCL(5LmJ}0< z4>F5dlX^E;Y=`#SP-7uLP*e|a8$%353l>iie-k^85xm-e7GL5*!>DoX$F%*YnymjWtWrWWX436A-B8!h(Ok7!Z#cs*dZ`@2QP|5 z7ll}7*k0q@K%+ba_~o0dMx6QgFF9IW?LQ@I*v_a6XTEIOrb0-YW9d zNq6%*)Bs4_K%{j0=g{Tt@PdkZv|$jUi!*-0XsQ+bdhWl z*DJRQO)MlQe0V! z&<$hwu{`mFaGC&B)xwD6X2N{zRNJ{I|3}7kH0(N@-ho37;!1Krp?M>gs#qwPZSmY? zi(-0$TLQ;&FGT2D-GoQWNCzp?Fa8deps%UDCRt?ad9!^O$HE(g(csW5)98W1uGEgC9 zmcrZ1tL^UM-~avJwN?+ZSmm@jk#o<%^dB|~HCNwO+i#o@vg#Th1`$Zr)pm7#d-L+L zpXW1sD`;Ut6#+p-j4Sw6B<@KP%sKQpx^gKBA}T~hm4@~9{deEZ%kt^dr)8RSS(uba z8RW`i!uCnlIjmmXe zNT7=z4yW_9T-@9YoAvbcV%>E_G62GWH_?x{8zr|1$67-L8W#2DP9p{uD*cLxHp=6~ zPa#kUz#_-{qe_fv-$1se_alHs0Hc8+iVly7bba5z>Jou zdIDjDcT{xYsG3u)L{Ke{4_~(zmv7&HJ5SRz*O<27geC1VLnjQWj$yDo#z6!XQB)A= zvJj8cbY7PE&AYevpFZNeC_uzOQHDzG%^q0=r>&s1!Iy|&`qUx@;jG;+iVk{_n-Vy$ z6ox4+Nc%k^J0{*fJyEqJg-5K|?~fGKLcydB5~1)?JTLaroQzV72}V#30wn{|U|YdK zVtC#yG0mXMl1IqGT>6e`N)koTDrZY=@;!I-A#ukT!n(yUfUJ%7j!FI=g@YGpRM-rYX1xd#l(gm2Lt5He#`)R^@;V{3KdM(dTKRFBRt2PR z$tO|N(B*DZ%6Px)DKq(Ram-rXHMMhbMt_8un5l}jBr>ANX2e7#?f_5%4WT+f7j#Iu zeKj!WNpX-185oU}oQFo5$kVHb#X67&)BFXDKWIvL6i#71=p3}dJVOyl5=JF-&@dQ+ zjBGgJXc5PzRx!Wap6*{0EgM-T19NzI)s~ey>}$?JKxPnX<&3{H>y}}DXbYNju<5K1 za0ii+oU1`nW+zAq%0L;)YCW=hmI`Y!-YB-iyaNXUiZW7~n?#6fsSrjK{=!D?+jCP` z0yMN=26F%;UTTV=c}6*H9#Iq-Il#s_Ja?@lQO*Eqgk2WvQWgOZ2ULQHlu}YQo9CKz zIo!?+NW}66Id_sw9LG%K+aZunoa5_6L6F^u1fuyma@wPaB-@I(jchS`-(0^)%dHN% z2tYmk0up#56lRW?lV8{-O2zMqku2Kn!D)N;uf;CVH#I&-+wbL z=fm?8WJXjBnn(8^+<|r;n$@L8Y2g(eV}XEi*ZElUkG+%xW~Y5DIG{ zB9_9No12?Ax1T?KetLWyOWExN`6XIYCRvBNl(MW{lm@O?VtYgPsgY8%fs_(ED%{v%bIz&yXGre=BGMM zs-Q9yE_JGppTC}_`D&5f?d{FY&G|GPri%3_tU{t(6@JAT5o8ga7n+q99f(kcZQBL2 zqO5pF>-B16fJv?{R_B*x7;;^pdSg$jlMQV5Gw;$c2dU%%YHxw+ZycE{<+%xYUlYGF|*#l@qk^d6~p z#&56GFi&AIwL9|H*(i0(>MpYFpG81@8?uMCzcg&5#{sQXBB?;Vd38?HX{yEsMqWP$c2T~*L30R3cvl%yYQRUCiAWbE5CKjw%!K!+&@`9B#j%UynWb(yXieAF zrx}kSnrPwjs(aU()KV{_s0B?*us7iL%h zi_zpD7S%o#VM=Gg$uBj+{?um=-q#@R^o35fR2{z;*SdVj*PNYGta0HP& zv6y0q-M*EIWVG4_*VZN2l|9oEL0OZ~v7q+;pr0Y_PG791MGBL3s_u{}f)Sd->eM}R zKTSK*L8h2w;0#>AqkRxSUQ0!x%ncMh8l*YMpeC`95vPdsK(}=i&ul^vq$|{rI_#o+ zW`L7*zhVZ6R;%Iq>S7!V5h4i?ik-wgp{9XQ+nxARaI~C(X4I|wblN~4FNvzC5!Vi4 zBR^%YPI?UMgWWBPdeB;97jsh(oY=(7^?9j+Xmoii|l7GG4Y@qeFnnvsR17$XXHR40;WP-pits*wYq!z=F`WIbw0B-<5%$^Imhp0 z1sY63$vkN{Hm3G=+b)Tcg146!*S9zOm*>;*MP(rsX&yynTmWzOufl0W{9i2mK?-6n z3Z^pbuCFdGFCQPCUY?(|E}nC-x&fuWV8nB#Q08LYen4b5%vdxON?NFF)^~5;u2!etl$)OnGm7A_zN5tXvtu4S2~mwlB=Or#_# z1W;ixQ(*>IRT2@cyhu^dqMv{L^wW=je*fL~KcCJb0#Tw;=X%&5hGDh4x}1+k6)o%u zbXn^3`1tvFygVLn-+!~cyuP^GV6&RmJZ?s-6=!B1nFi*82rA2>OBKsJswxQ=9>-y` zU2S(eRl0wADTQ~t-S%QBBbU`Uo%DPA0^Y6d>a&`A+ zySK7d%f4N-tB$j}PXmS+n6H#5x&yQcPu5a$%ygj|V>>tjc z!VE%XVms^HO5-#@38^*yCL(}TR8%W0$iK{wPY>_ky}iA=IX&HHK3iJBYw%G5<(a{> z*J05#u_#zz;72#%?iYnQBqDF_+_XNb>g^+vH-T_a0Tt6ie^p06($3|%fh1fxsI*iW zO;aR?pfKzCH_9)^fl&Y*$a?8=0%J4sjo^g9r3FQ|Y;8?UK-l3$Aa;#semt;4H9UsN|UfyyJT9fL9wp>DxxSU(2O~P8r;QJ2g}m%8FS}C zbGx^-LAq~GzAeh+tch}AG(hkAgR?TDffh2!~R^O3!%B_b1ptY&=Lqgu=--N=VolN6U zHtSVkc+rs;W;oMzbx5Btq`9*?ak{s&r3iCjmwFg~jxQuF_FPyEP!AZ>h*t&a^i6Md zv}%)U5~SL{0dA7KGqEc#3qW&SmSv%8y&*l$VRhRGiH)5%cY zOm@Zr$hyG@oM_mSSjF5vjf`ndC7pY+gdvBAC}K7eu|%DS#x)iUOHBr1kf(qhTsi{f z<{98Nk682kWR|Lh?T^;r;ZPatCXfhtEJRFYz%bt2-i@pA^QTWT&#aY6w2GbjHip*- zkwM{=|F%k(dGQoD{MlJe)*l}|6R0=@K4C3{6 z^Y-1_({#GO|1zJa!ggfEhZL%wWR$c`I#9rm5woQhF!JVV_wMc8bUq*U`!Wpc-FCa% zK0QCaJUr?&6}txkRECkb%+pzBVRJ}GF_vMwx_*0eeBMu|lhRDC%6VOi8(1piA}EDb zq@L$8uJ1qHug1-v{`Aw+{^`?)-;2^nrJhCWNmg}zu_j)>98ObRh@e%>UlGy$moMk@ zX?J^j_3qtBSF82aNK8WWybR+=z&H%6^(dS5yr34U2C&Su;$gkoUTrUKF3UJPA5X{A z@%H-a`pss&J&o%^gPxbE2w+K#DeTXH?ATe)#dzr|Mhe_-Msnc>iU|oywn+xy3AxP zjI<)41E9Ed&cUNIe>NSDa8tY!tcqot4qv~1{PD*>U)|ljygZ6j0?ASVHUGZR2ntZL zf1nUhf~W|%>cV9(r;1hQ)A8`~e0_8Cb+bLcEK0;+RbpnprPb@RlQkxv$c1g}7rhOG zQ=c{p|8@h{NWMpW&>x){zlLMM0U8X& ztrfyGqHaGTf}fk%FLUf<%o}|`uCWiaulwfY+EG++gx1;GG!MmzV*fU3aZlI8w1*k6 zTdJy}iim0z@eA_o=l~Ijix$LpM6Ldf1PU-vO}51w1!^~kWH4mvhfq=V=|G99iaEq0 zTKLsO8dHt|YfxI71GT!GKm1CQB*df;D|2(_Qg@J?J#kLEK|boqB1|l|(j8q{t0!kQ@TXpc);}DfC{UfJj~H;^)L%!ZUQz+fB!T z$frA?m{oHV?cXw|#*|0GP;^`q8u=gvfC8gXp(P+t@oOpJ*D{%?*k_X3h2S{_PQN`D zcd4S}Hyqo;td`zSZGw~VcQP0mr_Po7BY0}=Z>T`n-Q0F1Yvh=TWH*r^hE2%L{&hqk zR5<+K=Gup^o?9anNeA0gFXD8ZOJkjK8*u{iHj^gg#97>EG&Vso%PIVFtLR(h3z|N1 zgrJ%O&#X#?Sp#K*y`}1`2fvdoxKcr#ee7Y1{$f-zn5hK{{{8exn$LlwD#C-qd^E^!qB@v3NmqY@) zOqnC>0Osmv&xoinF;G|;rBotey1cvEUhMw%x4+eS+3a?^t1DGJe!idfhe1`V0Lo)2 ztD#C=mPM}g=*w}DmLMi(KdeqgmDalrjhn-10%#ZuR8i|{wfg$- zcyWDm`^~%Om*-`Fw60t%D)V_hfydQqvw3-WA@%nEU>XW*t<%d(UFvi`uO4rG!2oq z4c{FEI$jRD9!ph}*ox>>mBEB6^(2!XpNHGGZ|~l{o2JuwKT{Db)tEpisOKg$-{!#) z?Jglrwi*zw6$Hrg{CI!AyL|iZ?bGAK^Yq1eVGA22S~qr0(6~0h*^R1_MLifY&jg;1 zr-!d!fB50s%j@goeotUxEmnbSoWR{ny9Ki|IdHtCG*p4rHsOrd1luvS>%4_nTo1Z6 zqouE=14KYrGr{X*;FY)0Xs04_qfW&F5ySp8Vr;-eX@DC9#ZcTsi|Nq{4|d9 z4YhXqRaFU9RJ=AAs%i(kSgF?Pwak9^aKnCUjwR8PHz5G3Vg22XJI!_48m^wQ%3^hB zd5E-w8H6zSBT1wF%AJE(Rx`$6nNAmhmTl7@T%qr9hndu?pEcACuP8((8cjyfzc6SW zv_(8zP_q(1$;*ctMv-zp>&aH9&4?9I$RL zp+LEKeK6%PZn?=l7Yyh_Kws1V`ue=`%LzGGsEXFA3RqPJ6d+M$P8|>bFrdXvu)#Hy zygO%SYtq@$q{>JJhK{VKAEf?+mUC)MKg>E(JUdeBBGU(Z+$`CDyfsIzAH3&3%x6>s zdOrk4XdB^g$VUQRCW5Y`5t{J($86C;>XgzRL5VtD&24N8o3ElUz&XbR8Gm3_d4+U% zkie~6g1Dcj7Un*_o^y-fksXt;vBy`_^jY6MQH=vx`4ox^gEC~}VCRBBB%-AhD#K>C z+g`5ojES^;m{-JRa4X3$Z9;HLNZ!F0OBs zuq+~?)-AWz#aiAKW}+fgXxwaXZm>BYo(|T8SC_iptsYOOPoF>k`Jevu{`+q~|KsQR z`MfR+0Ta*1X~pIG>T0vzyu7^3(HGZnguBb*@ZUt;YgXnX4$ONL3<|T2JTmZo3{<<4gcE z*y>b!;8OEQ9RLV@NX)?`GI6Ft8u{M9*cO!amcfZ>{&~2hQR=Lzx^Y3#3Tqdjs1P9% zS@d))^L%-8`_1#!)%kePx-b{tK|co^4uCU_G|;OPwU+=IUy%)+!BJ zdy3KU1IZL}X*0ay$`;KnJZfTu-iEE+Gw*ht;EX zOe2($xCo6Pe<52zfh1{8I)pAU*_#?lmGMu?lalB(8Q` zpDY8L-W0`qUzv5^O6GjHTW2Vu*>P1NkidhKcD1{havED95)~|qsMNX`(N|TVkk=Ce z)&eM`2*F!}_4+!S!KY*v8k*^fY-bc3Niw}_-mI;dwbRjvT!h1w`x)$roW9tBm0)~9 z5`C0TMH?mW!hgO$x%E9`jUStEq)XPM(4?fBUwpV_QOOP;6M&(8!h#S^z}&?ov$*X+ z{_0(6SD3w#{Jp@@UQ!pC@HiM1HyhP;*Wa89Hlj}f}z3fqmj*WP*T8q7E-s6 ze^g?n@Ev&NbH-h)$)ASLZ%>lpRsRlv^098e`4P&fp3~ z99ojtA=#iZP?!oSGVHDJ-pmM@u=*#Z z{D276$7hMyofcDI_FaZ2io>N3_L82G=4lZMtVwloFtQ&=h^_+K={;er~ql zWWpfB9nW?KB`aKWssHG!IXF_lkV_ba=-G-n;kLnB`{`1O2o0-p_s!c<`1#?!PE&yp zBp{-DuZ8!ca*D_&3e7sY}fn!(|kUY&0V~WFEyK-=!DqNh@z_6eHF|E zE|qw--R-WfpI;8o&wE*F=~FZo%2k&oFq3yhi3|y`Qt_L=95$H1JPzBN>)qw<%h&s5 zuB&l9j;jKCd3-oO@3AZd2r!I=3)f|_jyj}3VWmOKNUQC7cRe3ov{In~s6eH{qEZS2 z1yqQJDj3%5-FkJNb)JQ4ElZuxry@AN93CH^ZtvdGA|K^LohMdhCKj#Jd^(<2+ua4q z3mp~}5S4LI9(Wjd7|Uus3@he=xo8E1nMfBb({ese(=^Fa3xP#SAtJ8Oftd<{3J)W# zhC-uM%yTURkL#6ABFj8YGtUb_%W5FSyezBDh1R?rb9cZ|r|_#xBbi4uX6?rDYLk5vxi;<)L{>v))PfK=Q#;N@5y z$V9Oly|^QJT$^0m(byLkUmGU-sMW}gsa7m{I7Xq{vEnw(n4>f9X41Mn%QedL0}00w zma%nH4aNevn@uTWem}MNh&Z4zVg?!&EQBbBupiLtGYkSwsMfS0oKIUDSMP8M z5FfDVnbQ05f199{0TArySoYbVz~7WdVsacyl+Ar>?k_=Y5#Kn`|tD1VIZY}D-n$=Vm_Zvb)JVZ5)+rQFzad@cbCIzH@{3;3!xAf z8;{2E(ozOOk*ZZ>bA7c_etg&$D#Lmu(^8nV&ijYQtBcF)ci+rJUw{8_emRT+66kqa z6lGjpT-}VjtNr14JfBp-S_%y$1Jf|BHk*r!i|ecPcDvcG3j;h&Gp0%E%b_06%Th^{ znW)$~Jf?wp99NsdtY(j=W!Y?o^=5UNCY5D5pI4&rmac%*)v(%(n-?A`M9l7zUx!Cb z5Q2h@JlSaBiqXZ~|#|zGrl4Y(wKCO;41Z!yw4*f}1xx0j+Vll6lc8Qs=|- zBM;@~_I7)*InL)Qi_!8VM}YcS6xX&;Syd0~=B#q>HhP_+uQ4_o12&C zN1dkV2Wf*fO|QYZDEouu-GHbnsLaL2Bl9#2R3P%@(Yth%KKffdSe0aPr#naA)U-4Y>tT}D(T#h`f-N)QnuRS^!$=nMth%-65Ru>ZOkbvW=9Bsw{eTOEr{|!aCCw(V%L}t$f>^_{AdQO(F_gZ2qfWj z-fHvi_>r4Ua(hR1#}MTg)jW0U6Rpv68=gxcg{O}wl|&UHbrD(SDk9$aN38;fsFQw%iG1g*&PhqQO~(rHl<^NC#*lp$9NX=VKzO`Fwx z;~^M(@^9n}jcA07JAZX*8xi-={dg{dlE%XbfT-OBZIVF&9TYc-Bd|S1FCG?S$p!;{ z6Hu-?`OUNfPuT>+ji3)&$v}+D)q+yW@#>gT2sb&$qxh8V4&f1KR$Rf%+#+j4 zS@Y{oo+V~VxwyXCY`4Gu`+wAFE{I4F+!+%QrlMM{Z$n%z5VDTgvY20@%D5VCt}o~5 zJWVIq-5uT=(bt!>-7W3GLyPz1qim3YZQIMs&1QT5^{dV^X$92E`@~uzm^Q<%WgsZC z_55*FV>?a3#AFpL%*sW}!0YYy^5*GqI-O?$q0nkIeEI#u@$reJ5_nMz+ie*}Ky{f1 zC8<(YtIDJ+-rVhOzPVeiHubPut=30n6|xN~g`svEv6jLBUiQcB&F$U$x3!e%FqLt| z70aT9%Y2?ce)@d(=9_mv{BS&lr~p)7gyWL`}1@b5t@ZqYSFq@ zD?Td2$fYpTFv@DXp66wHIV#QbG?BnMNdk(2Ny{*f%oqlufoK2?wBD>PcDv)(7p_&# zvnYUt2kf4yIt=CZ&CTtH=jX*oWp7PHvjguwR1b&x2`NW= zj`|t3@^tf$__W? zs2J~Yt#DB>7lem2pvD#KGAVC;-_Bq(iAh|rHD!X@?Fu1CYK=6a|EJ%^W1%Q(1E+9&w*G!r)n9uCCS4EaPSQAR5$@GbPep;@q;xJJYs zzo2Qo8;Rae@CbyL)A|fNrhvwgOmO>mC;X(+XmA}RfH-paOceQiE?&OWlt0o42?aqL z(JZx4ujh@4MvIC`eZq+Rcep5TfIdy94zX+bVbyP?Yu>^`YyR`6O99Wb`ue_stTlv zh*r@m4$>N=5DGu+6Osf##4Z4le31{p2ZM&eZ1PLd;QHuDSqRCZ^(xjMe zkwV4IhDThYQIu&XG#c65QtScw;< zf!^8RM zVSv7QbNA-$+s(xVl|cwqWSZymc|I@8T$j4kx`8eHhiK#)7sI&(X_{G&N)Q34bv_;+K7Xl8y}7*| z*K5Z}BK49UZdjT@B1NPL63KRvJ3o6U9??1qR2F-+0!85SQ^SEG(+1Sn|!u>+o0Hj#3_ zG}AH^(d-D>Qru7lSHGsZfyvD?O9?PNWWR>4^ zD7uOtI^je_K-(TRdh%+$+objN4^qu2tZmGpdumXyuvl(uGz#|9X;81xaVDievJYZr zE?gK)RJaVqehotzOBuL~Wf;mZa4F1%IrR|n(xCFWi7BCCm2wVfrjHi#RUyPU6>0_9 zOkpRR!9gdcPJx=2)X0%f(mYz5yT#x{R3+}a@Mx*8WzXRKdEFMI*uPTG;LhE)Qmg~D z-Dxx^q#!=e@hQUKOGQ3TcYE;4JN3KDg9Y_^;7&AJEn2=tp)C;WdKO+k+_Oe-?M~NR zM;#Qny~=P}yF7?bn>wcCFH99if@TXqT2RzW9bWk=_9?Flr8FpxUe<2E=`*K(q^`Uc zn$(hZ^p$d%JnI^Gr7gCVziWB%nvG^aDba*dy!=QFF$)OH zREFKfWg&jtKS&jZl9&FAW>D3oE^-N(;S8v^zMo12W@YA5go(#tdwpF&htqjkWOuRK zZ8u+k|NZ#1r)mdUFbu@F{_E@xm-RUU@TW_@{4R>Lq3Whlc?lt^bPg_S_1@L-ub zt<^(l!&u5#Xao<$11Y1{Wu9i5mwEq0^HNkqq}Hj7D_!O~@pL+l>lKwjf*`t3d6iql z9;37-17FnOwdq8lRGj&wR6OLrYdkMICfmOlCRoX&3!uNd*Mb7=S6@(NM?m{wE64(hGA z#5iUq4`rUG`}_Mh-+p^_bMy4{6?Rl$shwSP(w}iV@^CQ4!X)JTbpZ`_%=VepbW4}) zBpflPHI9A0OX@L>j_IpkpXR=I{K#myD>+BSW2VP5!ykKQEAKN4Sg<|THns@rA*e;{ z4`A2Syn?8BNwY-c&DH9#j1iqSd7n8C& z(CXGU&8qt$Vb>aaij~U6Y;?Z+6}&NJ4*6ijR}3E`CtX}lykbQ`7R$5Ct&1kD;Sd?7Ug@NLCU4A$AvD-X z1&b(WB3(+a%c~_bh7e;3Z0N+%^;qq+R;YHH9TCAxzd()6!7QrBW&>Iz(<->V`DYKWPYbXyzlr-M%Y>GT{7uEzVjL_sP zE}0@X1R+ul6p%-VI?%M?Xn1!RYiGX>+s?}+5KGU%>2(3L#J;|clGArs22&r+oaTH}zh`H3n z07WlBFE*m?F&2_1M1G_LwD97SV27*A%mk`kI}uIgicgW-PNMd6mw>r&g6R%bqrt8e;_!Fl0|E~5^%OsXT zsVK7&53AMX?d|i+>2RJzaCLb#PxIraFPLVA_pUDEK#b{huJgiR=0S&X26mTM@87?d zW%>P=UoUUp?!LWVU0syzTI*Q>lo86rT*_K$prQmVp!qZ(US8h)@IzUzj>o^t>5OG5 zrI4Ti(`kD6{AIhlxV*fP@0QP^$EPP+W>P>z&*y2Gmh;PaeK%a(43}4BwH_~a)`xU` zvD#hkHmfqMSLgFAFDDGN)TP#WnJ2E5AOIFs)-sSXW3yddTyA%}wKC4rbUvMBS(a(i zrIJ9FS!I?5Iu%8k&(koj%F6jc^A%l8C`tu_(jsVLq+eYRl9Z6w_XQge5ZWcZ4$Ap^ z#2O_q+rMsagZ3w*+(wd_1FQ6qd3kxxu_uZ6CPejt{53RzkH@ktW!{f=FbQe zg;?sce|)^Uyu7%)c-}uwhYF{Orb?8u8XO&=Nr{$mI)Dmb?XBv|%ggC}dh_nhZ@>P% z*5qxro`DQutjXJ|*~FX8F%8XTG6oe64or#n&f25OnF;Fsrfg5dul;gs z)8ZIm?}-z`Aa6Xw&U8E!g?>a(@yMvjl9(VeV#>Z4>_wy!ilyD+kslv&VgXVnuGNIW z>}T|&R#35H7*O>>3sVQgr)}tN>9YfA+s#&%7LN4moi=S^Zw=;bB6Ne9YaDoU@}kKa zeJ;jN26aZRA%ZRnN)Tv)MS#?f?Zi-=l{Q+`D(VTnj37R(?ctR3G!^F%Lge$^X$;pV zHUL0tO3>bs-%4TcxpZ=2?T)+?02Mz5ZEbD23V1X*T4E25l6Dbn0T{ zgkA%ELLc)K8AXCV#-o%Tu`fm_p~bA!Q6y0+<^c>kr#xk0uI#CQJ#s5Kh?FkbR|y&V;T}rxz{C+_p9& z0KTv0Y>QKhcwsJ2bowW#GP7S)n1UV#OTE=V3&9XmUJufnDTg$3-kRQ0NFbSE;dR+v zU7pY9<9_co5Ft6Hc;Q@8Dr-n?2nUGBZcq{*vKsJWKA%8E)h~WZ1dIr(wH=XW$Ahp% zT7_Zv6^yHk%S)+p*gvz>!bKpFlFcSx&9QOD#uh2C3R(gc?@P=@iIrJFWwY8|U-LNb z56=RaNe|h`ecPPWBUw=70J?eQXDnlXBTF-L4U&{H!)3ID#SJa}54k}oK z>Ri_=)M;MkI#bcrX3%+*GefJYav>gh7>8j!u6OJ0#d04SUFM@1}r0rzw7v)CVkfpJ=F-Pe&TgG-g9kCORJv`=`h2 zyPJ!Pi|KfFkEjKO0U~cBp2LhNQ%u9qT8J1bP^ruG^z``M58tge+rxDBYG0T37?g7n z@l}rd#a~fZ1+q~`!%q7Nkyr2uU^=6|o1xMklF1WmiFA1wLfb#vis#-q>!KVAwO|vJ z<5aZ9rYaI~0_t5F$XR{F@_efTXh{JQili(6$J>re1eu7Zq<{sfjSOxqPFxh-@&`Q| zqHYOe{+b3c#JO8->K+t4%Tt#PXuV0D{oEsO;uWW%NWO;J+5@)r1;~v<^S7CE1o<&% zL*e*=nBr8P&xbs|s9r>@T1w<;3CKzhqB5{nVM2AHY&(Qlb5+$^)q5HmCDHW8wDSbh zPcj=C9MV}=EOgg)k8)2tB5E6)-fQX?=GQ1bJM5^be*i=*k&9^* zL|A;FQ!|QHSADhr-CpEeg4`#KS3hAG^s1rZGdXI5yFa*zO2yY7I-OPU72 zL*vwx6D{~07C)}u%A5ej2SUqSJ3NHojwZb_6!DRCM_(&Qs+P0h_%k72psgf3vX@Lo zGR&`~Sh?7p;I$zG+R`yBEJX*6S(Ux!i1XZp%zSASjx z7(ApCSdZ|j;~lY98lHp?Ani1W!m!0^g^Z9V7rez=u1btO6ifZyYtD_!!KO;T5~18{ zZ9bmKkY7rMg@bwsAck!Vo*}{!bUnK4fcxUyP$oc_T zZcRk;L4kM<@)SnEqJI&CF+M;MzV~ki9ip(#z9xo&L7c*cN@!A!KQzAQNhTXD5Wz>X zBvn2+ZEr+=UBCiAx`&1Q#r8({9W7@ZHB>PhLmP^Dp&d=j*S5KYB}-fIM|BC?lJO)$ z6gaiHI1x8Hm4gTZ7p_y-g9f$a!=gq+Kv0MiZIE51MWj(X>u3B#*^k#Mg>wQBl)>OC zf^0ahP*P7jsj6y)7Gk>`(T^dpC&@H+sPhUM%ePDmIU$HhVk%XCWW_`ms_CnfQx;;F zP5}|DvMb(W#~L%RxkVYG2l>b&Pu#>lmQRB_AR!( z?NXqnu?4XCx8z?V5Dfs}_rHevz?C~WL-cooH`zmn1)V$4B?$bNAC&D}f}jfFDp02j z&c7`pu5rC-y0F>=6iLRDkG9eK`oH?s@+lOsnto;KSFCZY1c9QQln8dn-b*1e>4<;@ zw<5k~ZL9qkp}RZdl~sx#$KTw5z!my+wTVlI2Z@RE(x%via4%G4nx^Avb{jS?i2}*) z$qr)VhEg|V>IJcLbj-0$u=T21l2!+jVG2`MP%z@iDl{L$K9tL5pdwoB^t63IMAXq5 z%q7m~XdX(U+)46RQTP)SH{XTwhIsoDdF##aW7{}7morC7Hob0FRol{di|e%X-LAzj zEqw5bX@)T%f(nB?i3x9PXNEAPouc-P+E${CFn~C((1c^a8@s=`D=8B5W5(RpT|3r90T4P0m(%D7$)o7H-~A;NSzpN`X9Re<4gs4B!N1ybyKpM_Lpnq*q*TIacf z%X+&lrC7?{cEU`m0jODD`;YS$^^T1lfoh3&q#lQ+2b=1oRl!^W4to)9Jjbxl2EZY{ z5xej@N%Iy7)}|z@^C^Hqot&Du`<(_r#0R5mWO~|Nbk{mOsuIg|K0ZA?U0z;X>~@dm zljeosNmnSIh=|W+X-%k~DnNGgS5;Y#`{%>qaC3L}`O_yXK%JeLT0|?%Izo9&|DA9L zp5$RA$IyriA;68}9|K4T;raM9xr8OAusgnmK@~&wJKJa0DF;Fk-csKnP2otop@D=H zWuVDb4G9Fcv-g7N{CA@}Ym`Pyo>4`XJP3YP_C!a&|EHAhRy*{PXseH9}MOh{guept>% z4FRKC1?g3*o7|43m<~zzJPi#^tyXkpc0?&D6ws>e#gy{wst-fA4!pt>_bD1T<*52k zU3aC8n_?)SL*YS%h71vxp^%>D4N+h~$Am(watZuDp{4X3UCbLRLlxqa|ay2^X#nqbHPA}V$TFL<*rs4|UT1p*HR`lYw6mHgV1o z7B6}LQ))`*mQx_RiJEVvg!%MnCKpgqA}&K2%KiQQbULrcLFNioDlAg{M6b&{WwlSS zD{_ee7Jy12Vk4-;MVM(EYvHopY_6|pJsjuruo~aIx%u*spPxQ`*)Y*k!BmOX<4{VO zkCUF~qErggFw}uAuP?Tj+pnKKKYh6`02I^lJk96LX1Bh+;LQq)6k^rNOai5GSOhcZ zyZ7HMg+G4!EXrR$e?7e%{@Z{1>HqQH|3wKu|NMzW=F_Y)bHV=cahZ?H`LMgZy12L; zhT-Y{{(RW8C@-o|72>f}1xN(7EEAp1in3HJ$7NUz15lU?q1HN|kIS@BtrOG#&DMWz zS(4*equ>Tevm;!Fjuc%jKDp-oALh=TnblogU6q-kTpeX*ByfKKAkFiesLIT6_v2<1 z5E~mH$mfB_A#X;z^<0J0xJhGXB7#&AC83d#P)k8wQYMuuwV+f{9k*M~NttV@AW|Yx zJB(zgzA?_^a3E}Om!7byJt^r}wj(8|_go8SXIf|dMuO}m3V&;g$3_uYw>$qz)xNJa zsAv@;ie~LFI!-l;{>{?0d^9Sd&vPx)jp1%**rB z&i%x#%s*f`K#@-A+GZFG76ZsKP0x={cW>{8VJvkq?uwWpeyUde%$gsZg8I?w2$mH= zZyHHrTjH5o$|^1{f7i?)o%ek-7%gE$H(4c?2qGKAx?ojcOG# zXWh^wp{iC@R_pWN^{6fMvc(NPRa>;&BPEU2qkc9Q>OFgcV|ukL;UhKRr8WLy1}IU0 zo)$(1Ibh$d3cH!W`Vm8Q90ma;PZMesj*!~XL-9VN091qk?|leB#36O`B}Ra_=y$MI zoQAKOBSymnZC_$?YnshJ+VxT6HEpXYK%Z|lNv#GJiPSo-X0yUX%0NHV6hIs6K{Y*V zZ|pI6OrE-Q$E-Snr6`Ry@9LUGubxS5V*}ry<*GEM*jKITlW|84;G?mguE!b;-2p^C zQlnL~a~&&&*M{l)i@4L;u5I(9Y+c8x&%1r64Fr**B4-cmrwtRfU2cq#|56KN+C={h zZu!-v3K9L8oD!#;Y-d^vQU#)zNn@BwoExQT8se&k$VhtJMzxBY6xX*dF0s$HtX`yH zK88UgN}viQViAAvYVlx=K|CHn5GXzoV~B;72+B;Pu$EfBBh>>n`$vOSaNyW&Et=Wz z|1{muS1!h>M-OxCS#A(l%O90?EA?QIIMQ(8u%`XdC&Vr3DirCFLl|rE9!d_FmB0*^ z)*!G2mbS2s2zjMF6xKvN!>-M=i;p!@{XunkpM_?N{P#n$Rf(8y?rzWL%RF6(K#n%d zaBCp(R|Xb9B)HvU5rK#W9+#@S7=m?23AbZHU`yn;eW}{jFccS}d9bFaf}B!SDACo` z)%kp0mMIZwtqugF+My^A5v4|vsDhd8oHDt~@N6|PAtmHYX+X}j8T0l2_WirlGVze! zzkesCeEsbYE}EEBR8)m24Ovy^%Y>@TOnIo8csIWJ@b+|mdHVicOOXl?$uwWi=UaNS zz22qGP|u6qxkUuZ%wx*n!*qH+op0~ny&R6Uh}7k@EMNcl_G$R^)6c)olYaZ{3s-`Q zR03f-UcO%r%l-ZR+xL01+uytckEi1aDwzo?V5FQd4wMs^t4IQol%6} zE);h@xReU44O8nToFXmNEQkl8>em!7laM%TE=^;ge&rmxMe26W*%rH*OJSbma=AP` zJ@2lrhs|!joQVMIAg)5iMhOavi~rm^+cQ-?#VM-1!p{m7Gs?uCPjODyR@e4~!qV7Q zvZyw%VCfDDpht+NGhsyo1v;FEsz~)D5NZ_6t11$6fD1bU9&u!txwf!~XQRnMICBIl zarfkV>Dy3IJIZU&t))_8#_i`R7&MA|wrmZlpZ;ni5uk=}arhmzv)8cmU}P24W|LzY z5sw8F^jU}1YXICAk;JUJJFkwlVv$s=_Dv}fL&bWbO9S^_6)vi^Dzo27X19_PIf%Di zP!YSB1wJdEzXUoGRS|GX=-H1Bh?&XnNC@fY)Q)intddo!XWUb1!f$gWf`}|qi#LjJ zhH}VR?Q%eofOD*XR7e%8x$}__si;Vm%1o%R3zLW~ldPh(T5JHy&dJNzg3T+WKjSqRpO}D~0DE zRKprj|6T3)q)D4X;rBRq0Bm5fzoDVohzk+GH~zTj!lCnupN(_qjr|Qe(|3!zeU7m; zRM@5(X3ti_(Y8yDVGtW%A9EW+7f7Z+sD2Y@n~=G5-k!i^=w! zMOV1mn(9J9XhRHl01;(oaNHy6x*<(AV)tUl>Rx(0g+0SiCwqGal_1)(+Sc2#HK8(EQ6gE zaNFsnpH35WO=h;9Kj1_;S7M~R*%xEb#uSlUGEd~`Ea2FS5m9yH6@~|5D*K9 zk&uT(BM-ZLbu;Eos&f%lR6!LLF=-d6T4r9R`EuT-aoFz9&j)6fQmyrlcETkL7+`%o z3~f2GY&MG`6{+fG#igmCOvIH>!GHx-sUl?ezB%Gg0Ew@24j7keEnq{~%67exzkpsh zqombQNxObT5HNy*XAWcNF5kfkH2R0KK&?#{F{{?&@wnaYZ*T7&mPKoUQfG#-E_ZRN zBN}!aiQXn$8L3KzEXU)^;c$5K_TBO2ML5;M%YC6# zWn(p@??%!F+hTwfwd&-PHnieHZM*x*xHTyS1OiV81$EUyP&2bcJsj<0GY8NXGftN` zLSEG{tGjEpG1*#KfaQuqinwdCy+kcMXpXMMKInt$q@!=I`g(B$bOJHqpxAB|(Ta$W zfQXCOj%sVQzHEq{2}ENtyieF1mo!Q*Rsl-EAo&si#Efe0&$>03L^#Q1QAhLdtBq+x zb}?*?tF6(HR7niyZS5ykKEZ~tO}h!O0E39Lm8e&#ezhc`R3W(lQ;U&f6R6!^6j#@) zx(BupAo?sJRIIt-7KE96GzMJZ9A3j}p+W)gB1olJtX69e5c28{-jJr&e;0A-pQS+q z7Xd~+gmaEqoC2kjt2~AFuXX~Y-hZuaeuLTm{>Tc4u*9sL`UulNu8b>N5&15|2Vt8? zSEpUksva9R6F#PGRR<_hJwnum7jbiZhCQ45=g=Mj|q11n1r`K)Nopp)lQ_}|u%?srJ%Fdf-c2A5yPga}S_@mWs z!V`ci0bpigyQID8zJE5XoY`C=Ch;mKb)T&uOXnfnge{)i>}ImN)_AZH*1OHs_&~Hw zOxu8#L^bVMI~nFSMTGTo2MLe6C~Ua>N{dA)9?51}$h|}4ldL|0`hJ8+!+oJEm7P$C zITA;~f^R>C7XaeM8)|{C(*sS2v`<{Sqs6M*cyj}Wx=bu&95Rz$&Ihf<`i+=lNs;Gi z?`dRHSgF{W7;W2=Kz_c`?w%tSSdH0jWzz@yYg`7$s^CQ1?S@p#GDE=eeg|Qz4T-=y zniaamVkIU{%8bO|tZA^Lz``jF!_ED@G8dKGn_DUM4mDlrmL=FFUiVYu3FuCDiaJ64eboolTXB9N-6@Z3g*Oj2q|T6UXseY?AV zf4{lPIj3?_EuxEPRRZ}HgrZuCPV@2QW&iH;_U88G$0O!Ks7&njEw0pR3G@(5|Guig zdEbVC5SFXSMLVxk28a|^Dn>*}m6(`PGDeV?5+|z#$_%YS#7F=^s&LAP zo?PCC3aUn9H_IOxE7w7@qYd@3zvt za1RXNclB7dK=U$P9v^=E^wZB@|1nm+;8i_y4{e&YX|`O$Pojlk{*_wie#GYtFB%J@ zQPbO~4aWf)XN9#josD9uV|Rr zFe(8hebZvs+)qaY8h)5)#r(A~!|iT^5)KhU3c}c~IFL4RV~nL%TbMr6ptQ{UQ{(Mo zAm;H@UtjCmp5BBo7)F)a=hjPr0%yb`SO`er%fC>w6Xf)^iKtzF+)o2)4rqBeuey; zjl)_E&NZR}o0uClYM+aJv*^IDHT90;h~;eu4Ba|8>#yMe;<6WeYO@f!+BL(;eS zpy3rJ-P+RXrBQvdPH(obO54+V8`+L9n{FUNm;Q0J2%DhFF$@#I^FtXZ~L~%JHR*@K9^eIwRQeNYy&}nmwq;hPi6-8i^a^TNsik2aO%^eQ^OKmk4K5%qtNQ6HU`3)hp0^|J>&I9bqot z%vi+_UmrnmCR5=I%~PdH()XP78xUY^TFWF`wr$)&1>udvo=ezbgP?QDI7y1|lL%by^Nj zPw(EndH?bARO<2hFbty>eE-MStm*CB_xj)H_rLw^d_3wR0}*MmcBrM)^V8#WIBs^k z{q^nb-EFPcr#j83%2_h0U13fnRZfRVFEddplaglBPj4elq*bAk31Fm6y1v`Ld3!f* z$HU8WW>Bf5VAWcSYZVb7kV>*#jtBjq`};fmBU=uSkyLBgQ@|^l4L5sVO7Ap5E4&w; za3yKlqQ?*vlVpRUtRQWbKSbU%%n$w~BBGQOOo>xUloJe+)GG4=kV<7v3Q7r9E(K=Q z3aLzF{jaQ^XT^WnIQ@K!=t{KRL_6qq@l-eLAX3q3I&X$?yB*7N5pkBvgx!x;n#iy1 zoc}L;uo**ELM5rEm*@1$ue+=L;rW?Uf)tHzk@m*$=8gSt=-;>=2j1=s#50h4-%2xnbEfD!B>3QLNKib<{|o{gH6H9co&vrz_7 zS#4GqMAS~uc)}!>IN)US+X`nZK~&Z2q`Xi8NQpVJNz611Ip@U5B-Cg=0Lq-iF{bpb zS62jE4-4=t6myAMYylyc7i{Z$Z7YfZ`R_s@V+fSvVLGk;7N)H!olqP_h9@O7Qf1Ef1}q3N6=(qLKoh@lJa=nCUEZzJ zt*c4Z|Hf?m)jO!O9!}$q!WrwxN~m#>hdI5e}jt#Hc;RYw?{h zvVDVfp`f^Xcx76{2^wHrwV|-XW#oZ1`m_z+wvxcc$*zbIpqYcF_%7_G?+wkzs@cVw zoe+1i_35`U!}YEGh?-Kdt1FS1mPe@HVh8)u24=ySiV%nmqu7ahUME9TU8>D_n;>dPbypAaI-@}OI$h2`zJ8g{7gABP zxdd35*6P*XvD1mcm2Ae4{nvEG-Vqx1AYz5%2#ruPw$Mo;j|u%uIo!9ZF4Me}1uES} z5D*yvc6CEUEe^~{cOo)5?K>3X}%&}8OF;{L2!nVGau%JccOlxcr^owvi%Y^6{@GE%bc z1c;gnva@QglUVtXZX3yEHo{rB=u$|MrNpg5fTPJHF(tLQI}Hj3CmO~)WCbhhkke)u zmh*Y4N1)oNuL@KJa)uQaqoOQY7eXPh)P(>QFK!GL&6x5^?A)PS5)HZu>pvt?iAd_Q zOw%~z?RIlMaW#wPl{9Vs4fdJIi=441B;ANYMQcfUm`m`C!DJ8qsFnAsJ2T%beGl~tn#OrPRL4LpTN(4hCH4c!pUkif^muoaD ztpG%-i<$_#o2U2(R; zb2IMuLRJXD{95?u%GfWrriNtk|L0G@LHmS=c$t!_r%eRPBv2s+F>xkN%*ni|5JlBD zGZYo8td3uO3`L}yV$AO0;=QId?HXh1F?$>Rt4J{xUzx3FdgbfTyrMSvK}0#sX4efv z3Dm~bWu>p$dZEI1$8%#S+U@~z0bVB@vHGpPe_<|r0Ns&_*HEqiZh+DKqx;$*EoKLQ}Yo94KY@es(ZfD?vm&_;bZ!g~0Ew$<>zk#nqAlMUn!PJyUQ zUafiC^=40dh)%@bPSH((jfg#a($HvIyg*WY^=({s(5fM+_))l9Gyb6_VGW(Ow>s3= zK99&$p2|n?@dPovrOI5U^HQsVkTWY16H-o`QyRy-&BSTCEUK5e7D{Ot zHdJR28`w3FAyTD+1vF@xm&@hy?%n&{)&5N1$+LrYZjn%%bq&q4ChY77U$GLJFeYpN z7(TE$EA2JpAcpelU;rx!oRO09KzSHdDpV*T zr@Y&3uD3&Cs>}K1r5q2s%&3Ktq!#kF3KZy&S~0rKOb~}68;EK@Bdg;tRq1j$CA&Q| z{;N{+l+^)23*9IWa@}uE%y6J1~C&IyS&+V5< z)eTFKz$@6~1|$r2(+&;iWA6y6J9cO#B|g@*PQ}RqUkws`8 zq7H3k7(BkD9y7f{lcBTb;=MJzXTF+8R&pj@<3X{Yz8e0Qe$W;sf{3i3zi1WE#rigB z2U>ax4y+3dI%bAqsar158lGddZyja=t6?^=^nF+-RN;k{PWwWG4FrHH ztvj$sL7tsm9ptm%&fZ{snV2}dkgm)dR;%PTxH$q^|J)rwjMLxKcTcTE4|bmb0;nt5 zs9I7gv_hjcv<<_0Tafg(5>a%+c^&H-AR#|x<$pxm``TGz`?*<9E4897WzFfCe|8l? zB+}aUxO=hCx^YEaJz^Earp`(ru(yqKA5UKQX!kT5;wRNoo#5|-LN6lf?$ch%_C{4@ zrsk#G9J|pn)4GafqJ%Fjy(#;j^IYM3DTXIRREu`Qg7BU0X*q0>I&R?`SGM2194Dt0!sis&L0Qjj;BVaP-*wTjeQtG5VK zsESkoiZXIemzU@C;p6q~-P7;~`9WcW2;w-O0-LCkwHihRl4^OS*%~ZG*lM^}BN6ou z(L|OLXHYZ&Om5w6yE!YTG;T3&^7W0T;j%3G`s&x8KE1uW|MJ^!i-3l$B4fzIe%y?^ z{r%1L+qdI>^YQNf{P69s|M&mrWm&{itU^c-QS19BY~`h|w6*_Npw6o`n9;$l)XVu? zN+F=s8a}IkB>b9&lW&>5W+ELoQnE;(3iHe1<)>eNy}G%X&IeS2ACB_C3$b6#O09-# z_8iQq`=QW2C=j=G1Ws{`9Z1sATpUU4`~1pgH|#+TSE4`5?e=T?AGnWJEW#^+@YPVF z)CLvjF0gWvJstDfv;=5;^?kRj8W_k{D#XzfWzD^;WHvBdRG2u>3`~fQ3^axf7%mDS z^0jdlv6sS|nSszuqdOOm5zu}ROgfquwz;^=jyaQ~-o#u~;I+P};)J_K^F7BRZim}C z@yfc}Ur1G}Dpk89Ks1(6;YodebSzn`u~;FrCMRS?5U&2vxQaWrF!qN0avy-8ex_E* zJ&r30YlN~Efj1}-!rxUb78bGBDk4OjsO=Y}hQb{9wTATw@orGMeaxY~x3}>4sq0=B ziHJyiz#S!8(+GlI-SW&ykObBHQ={VHof<6X+twu*&qsK zULAl5wAVB({BSU@&ewQ=;+@4nh5|uAbW?KT$Z|;?AvghHvK`B-c3`2YZ@ZnPp+An3 z=CL#ZMeH-IYNx+L9LSYKnOejfAfsXZPEIu0?H8*y`^^oInP~hUmk)%*nSl7t!~oqY z+WLrd3+`~tRKiYyhL>1BvW4v?6&4C0qN&al_*5cbP^Ady9hiznkk&1EI6S)%i~!M; zLLj*Z5WWjtxSQgF%&e1CZ?9#6OZ&OiEQ@LiCt>o~P$ytE&i&;|Y*m}(QNyEfn#4?t z^^tM(7{b+et?d}%i{`nh6A(U$kaUV**ajO>K9gV3L1AzjEcQ9A-7WdqsTPuId$>>9 zDYZ`7bqVsnL`=;fXItDshk6r{wjR0u7L$nN!9eU85&>Qi47Kx5taiStqZxKS#~!)L z#I!#qh7?YM3@LQy6jC0aZo6TA)O~dLTrw(=NouqgTy3FQAB&O9YH|22?*-X<{zTg! z&Lh%5lsFG*Gj2Egl=I{B<4>PI&gaYNbU>|4u)d!>47Dz0Daj2MNltmczj^oWe3>sV zrzEJCCGdi61rb4(dOjWJW!~KE$KBQC`7|a5RJ5MwdArPS-@Gm7^X2IUgwyHx$8W#C z`~3Oso4Z8yd#N(bPt zl{(FN9Jg27{mp(i=5m|F?7j_kA=Ch=tr(1&XEssLP?aV= zOayQb@Hi33qB}}iQ%>6*je8pRdA}W)K7IP|zyDwV>&G8oe*9nm_f)XCzDX$!*Za3W zy}!A8dv$kvF7^5FG!q_Q4(Ibl)g{OHYb62;L6vF%N4C--AmEqTLF%StWe<~`M^TYw znyka7DUrt{U!)%UMN%p?p-RzpI{UQ-d~+aq>I zaORa5aR}7;YPaMPL5JHaisMn?@ij`%<8go=S)vAn?4n-{-y3({J24c_&tB-T#JP?r zwSnV}MjN+Jd8&`1R#w9+c2L<}2HIEJ%?%pl#;ZTg?%1z|6M>6p{3uoAJ5XVr>kO5}7~Pvh{ZW?(fY|VG)Eh8WhA4{dl`XS#!VD0Yjgt54l41?L z0gN|4=ru6}Wu{sMWUO>ZFV^a2Ij2BhHZ>cz2ac9(x21Rb(r!PIr~$VcJgesACIfCx zk()@E0B9u1J-OoNOgqoo&}#r+|4-nLpfS6#3cYGIZA27(%_)SFxi`h6@Yr!|K-VoZ z&6;rZ3Tlw2K|Vt}h$UsuXIZX68+arFq9Le%CoV_nIFbpfHa3188?kRggL#NKQfR&- z#r{JZExYw+@(vY&Po3cptk9UT{%`VnK!2Dj?A(u)*fY1Izm&N3JqlbFF?j1A-P|%3jree)8+z?gGHhrjwvr%n{4aUXU9p!6`wUas#FnB*hz!z5Z z^`Q^v9Ne^4&MkqXA+YmWBPi4jJ4g?$u#Grd@>0YCo2MyD2X)_Da)K4TS zS>c2WTbPM(tG-TTs-CC^?V@&-IE$V2ZHR-DYqU`bHdJOS;RIQ0oPqMdDT6bmf$}i! zcXw}ZpAOGNcyoXI*YDr!Tp;oL~R?$BJc9dT`_XAUHBRoxi3o)yo1@nX( zhmATKVMap*zY4EB9;iB);+#|OB*&e|J^^;-ez9K;WQ$k2=CDT8?SKW-56MpUBU%8 z%JoeZ7)b^p61(7p$?gYe2uGD@-AAVJ9PozXYIE`x)>VjMd7(cAo`E{Z6VMKzeN(M< zGAt1dsv;^<%|SE)45Ge65n8W?*<1isT1;dw3~L^O8(yp<5{d?26s4{3%?X&)zuErV zPEZ)4FdU`vTy6Dh3S}k_Hx+QcJb2*13JKK)TpJ`e3_+rSTm*!CHPq%8^1D%xEC;auyIF^ z7y==;cvbtJP@7xdlMa1-9mzEZYcZhsNyKV_L?~Ds#00{y*~}?8)2=24#G91Z107~G zL5YVES&lWJ7n&k>O!gX=wa8yr-w4x2ZRWO89Q_pod>gEcbXPDJVJ2LHDUhS{t^Q1n zU?&U@Tj5=U{LM-P=xo2M?T5xAGyBk5kf5Z9_H^2PM_8-Arfs{mZKtoK4<=j~&@GYW z%+P9jpur&dutJKcZLuv*?M8tJWaXcb=-g%Tj~_);B2O5C0H$h%6Pb+)jDlL1M?ZVe zO{@0lbfzLUjoRR1obIcYo1bWQE?Ae9R{LLUWjWNhy~;(0!fQl+#*CNkWayDaQxppu zd(q=$w6NQ0QD58@66&^Av0h#!za-p6knM6FZ6pM#qOM*tT}>ryVqGmB)>htLWJ2Y47_aBeLZhO5O z$NiotLu##3ibxjt6*MFwsx=V^s8vQvIp?dJ-S%obj7+SRWKk(q2#{K+R4vsuBaK5! zd9&Hol(aKmUJk(@(iys2mJ68in$SIgpQY7V+hK&v*jl1pb-M{>g|4HNK zFkb*1m*wC8zyI;&_lI%V?ys-!KYn`i`O~o9{{G|f;rVf3y4zjTG94eD#$mvATb9Lj zr!1(&f`1`#v1>NP{#lU|>HwU{Sl+NBX#T53yl7ez~;^qHcPJFZbe&Jwaqb}>5pqws($dsPOWp3lQySlT7w`pUoF83F|FNa zTM3dXQI%s5*F@1DD*fpidmB~S4I;)On27e#(YhGU2SEKP9;kNc*hnk&+Icv4q_3Q# z_Fan(zzsEOYu`x`^w?n8Tj_;B5RuiSxJTuxxRfeY7!ZMjSy6Jnf8|L75H)##rTn!a zy7rn?MAP9`t3Y4FG;N>g%cY^Uie@$mASbK+YC~WtzSVcb?U&XodF=CwsbH!5|h^u|+7?Gf8 z1zU&>aZ_Mre)3cf@=mg!-@`VVzl12G31me>3Q`gd(!!S9gh*U7V$1#Gp2qv3S~cCEP!)#<6-pQ~2iJ!n-tEVqzG!fT*R( z4gCY`d+MF~RILYtjX16qrq)`g{p0hofx7uer)U`M*g34Dj**zuSHzUfYYnyeFiS7n zGBz?Ze0htfwEN1KY|nO}XB%7#V|P~q7{?pJ)DXyr%}I$lXR+d+lqqFUM#_1!+1y;e z`SkYlUw)cp+HCTtj~`B_V_Aw+1*LH}^1!7|Qz=-4suHIn%A3ub4<8it{Nqv1GZ)b~ z@96f#$sK{B)_Oi3=Xt)my&iVkdO3~5Q0fArT9%i`AJ@0HZ{NSWoKKg-@oLC*njio8 zI_BZ+$4}+%?Rqi&Rndk{eF- zg|e#1^UKp4{b{_t!jSc{5D|o-Y{%;f;*u*ZuWj4kj%|bySA?f1YqUxL zlPH)HQWj!Svf{41+uwcs_?LhEAJ=#HfBpO4pHIhM|MF`Ej;FH{=iP35`{wG+yQkyn z;g5d^)w}Dv`~Cj#@crq_*ZDj{Rq~K;t`qiDm*sp}&PSSOm4(2Hie?fd^xT_)GzKaR z%*tGLQviUvr;i_Ms6|)E6@N`DBjtP?L;)cZ)Ovb;rjMWYyZv-{fm8!d>UT4BZ=g|f z(B9ex-LOh9AeJ!*wf25%=rug6$smAG3(kFu1N))5Ml-4rFA(`Tw@x($6ng!5bmk5Q z$Xc-k;9RXkPjcMcd>meHjDx3UpjTGBy&DJz5G^3X4zC{L|`(R!p3mc}XK;&ay*}~?dLQYy%+Dxd%b65TsZkQt*-|89wSf2!{%_bEF}Q+7IK}7<^+L4Vxa={5a8UC$8YWo;rT0F30w1p9@^Rhhf?@VrJsu< z(CQ*%ggbUxNfoMGg_4qT0tn11#efp|scsgnP-Rt7N=(_?BN(3|D-r3tyFxzgXXf0S zU^U4gGhtm{ysU+~cr^>=x)7_`P)=K)HUNq1-I$=GJ%2Fwur=h&CPgtW ze}7!DPM37mH(D{aiv7Q~wm=dj~=JC2oM#L2{Oop`QG#ziY)&qvv52uTuR?Xa` z*_Ouyn|oNIAtitQvodQMc-TKoZC(UG0wqdJ!PQk?J=KFTph3ju=bF&CHB?v>+Ok<; zupEuLnRGFL=P#kI+Uz-&6-Ts|)A*jYIHje6Vi@jDm6}hZmDD$HOU>u?NucrBwi+(V zZTp(z>8jzstv!`QgqD{>gRadhywbow?cGY)sI`1%fJj5G;)FG^wQ7d7LjjSWSSCN< z)wEW7{TZnRxa^&b!o*PP-RBD3x7`&(Ehyl2s{lGa!%Ce=!_1q3_xi9%!LKFCPM*ZA z@T>JQS|JFP@U-ncUUTUOfke$&GniG`djBiPS^(KqU&Ki%5i{kSHX{#X+HH5&`@1){ zcX!)=e0_ZX?(XL1`s18_mheE#cy{p0uVU$@)syv)D-@%yC~ z+6=qf`~BPd|NO^q1$BSD`RVBuAe!m@eL&j46`0clEfBT!D z*qBu-g+(LtrXH;-&=PtP+qS>1xDda;CvqGu|} zF9|iNXt>865ZvvkQD(Ff3JgRXj$8g2VPsRz&12hUq)D=0)f);Lr5sp^XkSx@Zwd^@bh%-TMBw|s}7TUh;`(}p3;Jtyd5K9*oD-$GE6#)xUQA%La1YuAr z&;=Ik8TDxWSIGc?Rrcn-Jpp<=fC(9K`xT607b9BLG*Sn|4(wKF8!aseGx9+X62ba- zlAkp|%M|$9g}AiswENP?l&c3_jY^<4MsE=0;|m`TUWTQQ*>KIsp(f5>c5bX#krKqx0ks6(8cBIkM(+6dyF z&I`lgD(NCf!S7g22>D41uu}8_Vy48ziE>Kgn0A}Z&F<=Me}BIxnlqR8AKz77rgh_6VFbwl`FtKYr8MMWOnKlj4~bCS%sNpu%P)&;S>2f}uuCH&# z{dP((TFfnHEw!%GeCZ`S#=c z4?q37y}rMF|MtuC+eE0b}Wp1yy3 z`Tk?MEaTPg_RanN`ugtfIuXj{QZMIWvlRt_2`mtTmGzsLJ0++gw=4fnE9)GJ9%L1o z&by&)-`16-Uz@!CE1>XOq$(9elya%dJY6>9$V62g3NlDvbs6o(1z4M-sGM$ogWAwM zbf9Jk{!gaKgR~Z1^dE$3n(5JQSi<=e5ZX%`eLUN;Q5afkU?qwc9D8V(+;)4B4MrN? zd1d=!g;yUX*uA!()aI^+!Mrk?+6$Y_3hU%6q%lWbEl+qxf4dq9?Y<<0B-|CP$_M;i z;t-A2xW#9}UjE_YWauG)Jlqq8Opzb6ag!NnOS|7$fpTzt$m*2+bZFINP??Z!M!`zsx|$HdaCRbpc|?Y23+CMg(WZyE<3T%ab3|f9g0GpcfDPAjDm8a6Ec`qk<>66!-}(X9QS(Q}pY3t=`1k#}mh8u-w$N`0DzZVs8kUmzl004@P0 zqC`~$H26ThsdDHftRS@O|!xh6u>_>hLwuNaT=9G|A8V4Hkuo12T=?DHet+}*;Th004Ewt=FH+CvQi~LYfGDLzLb~1WcH7O@@89S1M56HH zzkUV)s;W$2>z*W9v|^d(=fh!)(D;)HDnO@26>!W#+OxfB8Kz-oJTILeD?G z&vPN9Ivs!i>%W)F%mU0+?K!IZDEtIb5O@@S4HDv?Sl$HU9* z&27%9G^*YUJ43|ijg*CguM^EZRn4i$5t=CAejGhn8k2n=+#nEIN|mi@iQ~dF+Uq~pqU=N z^=)gAcSSBDx|#S!IxujLL|{8<9q2w_M@!NAW0+Y*IVB=xzoOJYRIDvEveVffT}{f? zkGrR`8#4{4x@&HWOz_sckrnWd*sxZcXtQj6RP0!MlV@Fd9H9UbiJ9!S9MTk?)#HKa zX4vRglCWh&l5Yr70%OR71w!-BvnYDYNz8hF5^_xDxz9z zEw!pD(Ig-N36g+Bln5i!Zp{15I3@tBsv;l)G36%uUEZ6ZgspXK6QxlBQbX*+1@|bu zt7caUb;AQ#&qZ>72yK(KydJGthZbp|@eDD|Ht?!NYkAx{$p(7-;sM(Js5ft9v zz#Bx_iA%WGan{y4fW}kTK|ICMy!w6%;bR@*n3S$R=F1tas~QN^kW7j;Y_3&_o0Eil zyoOL=`2r?NDNM-98j)K{YCC3Y@AGmH)?HYeNsrgNH*0Fu1;Fj135K=v*T|z#-}`2- z(W5C)OXoHT^ZCbkC_dNxC`ZH>l1F`%E75_2X49z%$~2Td`u4VSSpRJ*rUU%nV8qWSFCK;~r&zEhe!&a?oQ>);>IH>xmWAZCmPU=oMTBPNTY_ z`Dl;L-1!j^l9KhzNNv*ULA>&`DzjJg7SVL!m)pDb&o}X(c zxgukys=X^nOc{PVb!|~Gq(cI8 zBQ`1|Qeu@{bvc}u(|mn>yV>n%UQXxJX1Cewc9&_|7QLj zn7MACU;WYc{gnfYX9daY3eb2hf0MOS5G@Os&c~N`Z{CdKIJ4cA0j#uX{l1>$U;Ts{ zhKmue^P}*yLq33JKfP`f4P#L^pxR4e%}aTwK6XR1axG4LMDPHsbbjSq+fcO`AqsTR z#`Jok?%lDTsDBlgjdi@d-j1+K@J<`?n+|Q)P#?r(7vjX5`kLD68Nr*Z)1cieACn-o zZjwX_rj|=?uF;D-Df9?3RNPDhlHpqks^b| zTB2Iro{v^l)vg>!=OtpyGjg|n}U3&`=WK6^~ zBqmi>FzkkujKI1f3RaJ@l2kVgAulFO9qoG&RRc!4+U1qi3N30s4ujb5O>Ez6zd^5l zxud|9=C+gCyhg78tbJJfUqFv;c0&+CFx8QwS`&IT$uYF!cxDa1Ivn2y?R>qTO?$dpm##I9=TfTh`m4zHcGpgAer`Y;UlrDYmW`dW&vWPWayt#LZV?e>~DAPKivQH;qG?JnKr|)`*B|OSKHmyZZ4!X zEr*LNm5G^285O`l<7V7nU4Q@jrCy3Ig^Q31L&O<4jgGl-12eNq;U&w<`Skd5{nO26 zcQq}~^Doj zz1|;|%lYvsr=)Y44yRgWceD4_Tq*H5@Q|y@vJ}sunIr&Hc@JS;lF;xw|V;mzQb3z5e+AU6u0h|Nb9Pe)s0B%=P=X2VuH- z_vYsM`uhHMn&!*ttW`F=sndKuALV>m=4G?LDk^f(x>RP?O2ia(BEi2lA7J^dm<^0I zSi`UY_o0D3U~n|8qOCUF{FZiZ!X!Vimq1x-A?5SoP|KVh4+ud-sxgqx>LO^Vu;cEA z;wqcwJt)cm5O&PWf^+tSs0qOKJ|X*;p{y3N0XZvV#-t40~a`T=H@@h#6$t&V?mmT0up^5JCs7`mp0cT z+MS=O3$VE)uZ+-mibtN(0IQ>VUsR3G`o9PDlhG zxz*4rDt0%62!NzQnp8B0prD0`8c@ldJHivUb+h$tJ>PpUV055&Ere2HHl9>PEDz5F zCrQYoF5l*m58nz3Uc5_#y8|<<97v1edZ9$nA|vasN)&%w{Egxf)y~a8M7SO8yp&RUv8+)=z8u4RqAF79cTm zV%V|eW_%he8nBqPIV7hqv}JR>yFENhh+}Eyz#2{Hr+kPISBhzaqU48U+xNmC zhJo`Twfrv;;9ci~7YM7NLK=N`)g!AF`ezB3A4=0%@91ULd?*lou0XbEEGiz5hOByOhQkMO5)ktMA~hLz%L*cvV1x)P zL`oc`#G#e#&@dvSYJcG{CaS8X1u2$$HPQ5;+xcfdYv3*d1g2z@XbGW~ccZXUL?C93 z!V*_2Vi56=dCa^SuHWo``sMw*H~TG0lFA8Jo8k86`ugUiygNRhR8))VB2t8ygj9*| z-o2gXc{yLOEL?@Gv{aR%rMcRD8dN!Dv4vBiQhBMzrx*N0*EhH2be=EEGB3k8rtM}v zp9B~aAD$n7`|a<4`Jey&^G`qj*X45h{Yt`K41_uv0`|MC6(-TiG@emp(S(`C2YZ}WyA-@bjH%kuvHyAL1VL3Aq1 z%i%O1&!x?{9C1o9k0fFW&T zA^x|MXHHw@dBNSx|E_&^9DqaI$thJX1hbZhzLkkMI=lGqUf)ooZuHd@w5t6 zi3xTqxo@I_qPVfyX6dpmBDEs1iq=|_FevBT`*Mb`bfNFDtoD$g0}k&8-(*W0yk!al z6#&!GlG7FTSXxQ>3MxASqIP^ItVzJ1xLnNuTzsHKEC^XxD=y-P)bwXnuI|6(0b=7^ zy$H`L7!t#)xZM&dcK{zI5i!4*QwMIgGAjenW`t&+!ulABF*ehnq>-<--4ukife3=K zBP*oD$&bOqtV1kI7$T#MZH>$>I5p8KVeG?Z>wC1X4%s3EYMT?`1GTT3;5fD;FYXS~ z#{(;h_jXp>pXH}}dv7sUb<1@yft|i3mm^;_H3u7m@U@j#9}hV_!c~qWJx34|aH3ms z+iRw_Ulvw*q_Fl7=0O;+dl{_Pqlrs2m?ik0!GpGVo~1S2QOSA*TLhu-dL2@cl*n(P zfEDvl3$%JsO;bw48U&>-+MLJ)DO@QDKun^_%)ZJZN;SCvX?eU$^fONDjL6e0K_K@D7mR#!s;tyQCE6nQCX z#QGiws#p2h*Q>Zb0vlH`PD*4YK%1-0?rKa71`mr5Ep~QB+$epQaNXkXf5;UG@a+`o9lv(fD z9aSN`;N{2nzyEvsr@#E=U;g=D|62az^zf9aFcFpI^jt*r`uh56v)y0cY1NZFmokwq zQds6xIU&u;v*tO?m8YW2dB02Ju)W%ioF2bDFzPsrs4|_;%kd(mFaw#GG6+LTbtxtB zFm87H?RGboX`YS;Eux}seV92V2s5Wz?ZPaG6lhJLY{w#HcYXcg-TN>9`111fOoX@Z-`~Hz6Ty!kkC)4-oF<*i z)pq>LFaIRVvRsZY&rgS^$LVyeWkId0Or6`u6*g~v+#D1B1pOP@4U4MrT|^kLavBXw zX-KgfcJkFzX^nKu7)V(jetfUXyv!G9G2DjGZ?hG#zGyz(F(;7(4h_vhbh3``Q5@b5 z&lpq*%&NhU^CPG%pIrEez{?QhU$d9BqdBSQ+`sert1cw%gdL%TT7oGrWm%+Q}NQK%xWLB>hi%)Y1waig3>{D?#BU&lZ=A1X*PpOKrYfQ$iAlk#k~uov3#55VL=;IeoF4jj z3`-5r1^}keGS4G*F>Vv-xP~as845djAy%O7=bus#ekNTBCSH zsDVkUJxO69gFsTxqV``QScWAGXOt#8LGRTG>X)N%QnY&?Uxi{}ZrlJx1knK|A|_bd z(zYh#dCstH@g%bf6h~70EWAS?bd!cvm57E7GO+5t0x`dkz`(nG4q?QN_%UTrYvvUa z8Pe+-xwf+Ys8|7W_Wq5Sbx4RyGMWMf*Gx^eO>aChLBP8fIfUjQEPu?xbF;<=kZb2k zs2Y0aaBDzN5RvT;qFKEcm&rYMfuopLTi`I#kT064@2!Mg@sorB2E~ z^-MD*CW&5#NzH;a?hqm8-c^#r{F47AcF0?*GFj-tIakHY=~5E5!!XbF<#0J1FHoN6 zsmuk6#JreblICH!ySZI1({jGlrDzps1+0N-7&^G7#7(&jCIv$UwM@s;@#*RA&6~}B zOIU=Us9@~(H{&Kh{qZ$v+Gaj{|MqPde*Mco|K-5N)RNWQ6Z9v#*}!GRiL?rwhhr+=9j`S#;MpjWpWQ7L61;^};mQg_4V^m3d;u5LfQ{qR-=3*_bek}|)2 z_ja=xFXzkgcsL%;BK2l}b^rc)pVRZ>!?!QLpARo6b2(cZDADRx2ac}HJZPBQK-Qq} ztBwAMV0|47bSu^WXtO>D;@Q8guRp3Bg};t)L`3J~%acfvT7c?EOM{*F`xiYyXLd>5 zi!}>sW|q3iL%+AgEwUH5`H9BvJiFB;Rj-{V=eq560p;8z63Bbp1v3Le^)?JHAL8XO z>SjG`95o84!xrna7mF7DYaK?!fVDdgbDzGt>`pYWsjrH*7c>&Ep#?PO>IFjLNF#ui z*Km0KOstsWH=-awjoa~{*B2zp=EKDR@<0v0hnHdZ*b$E)Fm7|IUTfEasOB&bn7PTO zeGe3u5|~*v=VlVZY`3jv3%)HncQi%B>{pX{1YvH~^UkW;{C*F2)Ng$@U}47yC2$di z_j6SWN-v})JTVXe8UQ5?*=)`(q|p~Aa6hSaYz9iP09dQ2s!G*TrHIT`ilT~T4Lsck z1))$QL0vZdngsDT826;X&%vc$8zIh)q5>+#;8s|QUSpFktt}JeVK&@)y3n@Fq-|(K zLIM>P%1w`=ml-Un5cT9HH9}vikt=m!YHq{k|3Txxox9Ob*sAW5u5x8vi_UKp>x=tZ}Q7aZkKN?21 zQ{im``+IxX-o(;*&K0JF$2_X-w*%&xM?~^MI*r9}Fq4wq1^{YU5Fu&m5r~FucAyPJ ze+nS7U_Fr%@p-JRSpnz%TC2Vld~b{27SXd;S`7ec>+Kj+7T|X*2AYpyL*wFXwLNMw zuMuU06I#EXtVOVL+(K*X5P40t_d#Rm@yGW_jdWM;{a-b@>?0CSKCMVY`EG?x3vDE` z$DNq!{|;Gis@{Bi6B0MLCR&~;>;NEaM$mu+vPiEWG{%- zvjOIA21Qm3(2Zaa-XIOgo0KW|-lBh^p8{?q4w`SoxA@z>>0G7^i(T*~>fDC%~zz1io@ zc9#cPYMrZA2uM>>&NOV&W;^WnyZv?;vqGoi;rt?HnsZ{E%6vT4^MqwqMHS`kn0Fi9 z49hfS;@i8c-PPt?CN1gma-LpJo6Tm}Z(m*x4B}y|Qx+s5Nt8+v;v}NAmXWv9Tux8V zZ$7;nueNwRYE`xKA|MEh<0EbcTPcN3q4C^Z!P#JuwEzS&X-b8ZGUd&HVR-lH)9stP z|M<`Ud^sGiuCFe~%UqZJ_DaiwDzxaV^X_JNb9KAp&3u}dy40#uDIedw9e3mDbUL3; z3hb};4BBtD^RgVC9v}bsHm^uH0K32Cj{lQg91q`8m83DO~S} zKz4K&ai>7IIWvR^nyHOZ`=T_T)Xku&HVao&>huQ)EUf^^R!QJtp_-PW$E4g+;`j%{S>j6g?^EW2^@tzcUNqN3Z#VH_ zhgU%jS`g7TuM1)O^m>uP&>;Ah*jAT-5V=@f@h5`l?WiL(KJ@`u{(cj`YKKpVk^(q4JT;}wtl zVY5%;Hg9&D?Pl1H<7VI?h-d3hOf{je3F;!WGk%UHmH zm1v0y16nJUf@Pjwo>}T{yZtUzMQW*I%9qPotA70X^Wo|F+wWh-oTZlE{{FWG^8WLu zpa1!nFaPylm&a$BE-1wmd681Uz(Psbt^po+7(viwzSOy@iXt;F^IV7) z;^}fO#}nE4A4L_ZDsMi#hw6F0NTRt;3MAxuS(fu8o6Wf23#mYIN}N)q#3;22RG=(X z6osmArR8`+dAHr|^X^#ZLPVU%13EU-WxTQ#{To3LOhgby!5a}gjbpWsOgVGPwAtt; z-+p-a>DQkhU!G6r6A9+iJYQxWNYnOkJ}$=#Eds4NFFKURFW(*yhhaCq`}qEPzc01^ zcz6_*-FCMf$5P~czI=atI6pnnG7o|=(K21?`Gm_wm*P=rt<=!}iskR_)r5}-@KOOZ z&`OaoU)i1ZD?RQBFT8Tu@Wnw`mXL5l*km|Z^2CWW=&xuc(-a0#n~Q8FkI+xtwedIv zBsb4+Yu^ofWGlj5w`ipQN-wnyFBN}?Kn5kz7sOu3-rWCG zAh7k=wJC7ar8@Nw0ev`x(GeEdzS;-$$J zh#|LK9U)zIauVMJ`cY-U1vsstCZK4#}lfsH#@4w{eJX z6+3{yzT=-(^()&&TNOPu1k*J8wF25WLZqoxf}k};b%fiL2M$@{t9Y2-&0&6-VQ0pC zMOFxI_4a@-4Lnic4vtURtNfgjJDor^B9bZM=lHb$CA1=@+pusqSnc;5;h=pt3Wz!k z#%j>*(1vL~MVzv#`XD!RHc)rk+%G7Nr#BrYJNfT$-i}Ezu@Z92FouR8bkK;!U_e}3 zJz@-V*mNM;PMRa?C=E^JI?g7S1+(gQRQDC8TrW=JxB%E8o4#D@*ap;WjUT3AFNg{# zX*=J_fNUtum`fNxwv0{$jO1xPZOYlbY%CEXhHm~z72Y7rZaGEc4A9=H4Q0^^A0rY~ z>QK~0%t5@NU9~RIEwySUNPVlQvs)3;!Ed(f_W;=58UiEu4=Kd9u_A4K{G^Xtx3!B! z&mXUB4_^PBR@>I%hKK_Ku~2w*m&ny1Jf&e0Wk;K-p&ol4*@+cZD<09^A* zVlLB!Wr1o{O~X)^a+#)E(lm^9nWwsJhpU_2epu*1M3y=)Q{D{7xt3We5hbvnepOXS z1z1(evZS<;NzRA!_U2}@dpSHGIkOf9GsN^(y(u1qD2e+Z4*gTFK(TsZf)H~`N`%Bp z%z2=3L%VIhx_)-$5a=Z+jmom?#Y&PRdIWE&QA17vFq4{_&vsA*2%9*Fj zrIvbH7OB|p_IVhWqUZDF;o-4N(`FcMZ?5O3r^DBSo({asD2ow=WQ{~r;YD9)K(~X+ zX87F!0o`M|nWt8)+%s1zf4}mzO{-Tq*9KK7SX2ZM3NKq8lX9E?Hf^0(ts3s{Y>Br)z>F+{rdz29HIc5 z#8y5NoNy3f=GM)3H}FF>ykU5XR>CZ#1eYlFTg_fi-uH!w?GP9UcKw1QguZ>TzXW45 zNyLuw!W%A$hw3V-KyqhCE$yNKbksV$sY(zpesNSFZtFfr9w-w6li0|$x4}WAR-LuC zS3Am&qsF9$|IHv9Edvl7>szH$50iB@Wq7b1o7M|f0Iyjc59u<2uUK3+K5EN z4Ly0&4v%=uTX*{gjU_fKVxhDh@zi!;%pS@u6bUV)c(QHrDj@8P2DKl?D@mb|dUILw zh|C5Q&yTO6Ybqer$_uy=0h|j+IK{Sm;TGHgnXdF*Mw_1{+3e~5It}RW#th+ERxZV& zIq`@|Bc1|zqE8vb(CS(xqIMXUw0h+(8q%n3Y(Oi3dR@9x9UZMtPey zw|R4u$D3h$o${7bPALPdA~G*k7tuxPEHc+RtJYFykXRkOXfd8CWd>0mhhf}ow)@R~ zH*9y~)ovKaakm|J+wIkMyT96OccSkvKh2lp>E+vx=WpL$zI{7CzSPUS%!|||b78Gi z7DDx6dl$H-(~)R1-G#zP5hu2ozzTNJF)OPePQ$nn(BX91Znx*6Mdji7^z?GxZTNhd z%X#Kf>%2gen8(eq88(NPmohJQSWHDoE3l&f!)B-`DceOqDvAWDLR=wK%Q9U~&ySC9 ze*VlkD?w^GJUwqWCDiozqv4($yoDyeFywpPD zChs@^?tc9@DT2ZAIm8v|dfQn+g-fp*BR^E&ofJk{+mgo7nToxrAhRw~@ z)w}Df4e5{h{D=Z6R2PxT&?;h`ksDMt4ug3(;LtNe59negUGJo9FQI#)~Oj5sBmb01=B+8-Cm@i-v1`JpB%NX)|NgP>ubhg$A*tO2pQEk9z#=p>qUSf&MjKk5{9&SeE@CPXqbHnd4lC zs(ndKnjE+VKyKq{kiHuaqhS2FgpIucHcwxML36%X-J*z+Q@H*Zh7ZHuYQZ3RVZF;s zw72ad2Q--@GX`z}dF5*0=Gx{WZs=_;tV}A>%nDjk0U@PL8U__YTq^-+00`xb*Hd{|YZNQ_=&YfWW zF8BS+Sm5O6~7Xes5aIz#7DE-Fh|W+JIlOzsE} zh@H);ObVKr6r6^E^T3-iZ?<_H$K8H=b9H@xyT94*ulM6-d%M4RbMxlo`;Vv7@%!&z zzkmJq^6laH@GR%UGEcnBOq`@}U5H5`-orGJnHcIBD+MfpMcRVLxHp>=DoR$1!ik4* zuJtr6#KZZrTu@&Q=ks}%nhwv0`E*J1QsWDo45*?W56|b*QI;|vN~UzRzkWVlw3cxg zrHpkt6EUYa$wR873L(|Yd0I-@-`>!WXku{+Fngaom2eWOWQV>Ptsa)VEgGOo2`MXO z9tKJoIrDatu68%?-v8^r{Wqobay+Xf9+FmQrR{zga;{RB%R)q)co79K4tcj3x7Yja z^?uy$mRc2jI-XAFDKV#<_q*MGH*SW^wJgWyQm33yq-rgwMIn$9vAYJSY>80yIuLE}cE@azh| zy<=7A9<~fbc8q=H(Hg$)el`eTn6IyWsBGmVMCx~w_t`dF>jxu(+jj`m(=hHtgL5vA+kyyHl$f1jvJ*Ov zG=e!d`SVLxD z(^PT`gJ99EHZfU)TjQkF%!*geMHv659q*C8rxn%hfk}&@`DPm<#}wpD0Az0@X$4H) zxxIoQIRKbuQ{pBhvonpr@a^K7Y~VMycV$JK7wZTC0VH~07VAKqTwTyL+h zuCK4gf7{)Be*f){9}i!DynKB){rFzaCqAEtav8u{79wvGBw9e`!l3{&8xtRWWGq<% z%RzyYijrl$RZ>d(-L}kSnaYCYoF^51emu<6f=cJZv0SElS*)UT81rT~PRD~RGfH7q ztpy?~)vOAI-vh!|v@lQna2^z>0;;l<=ZB~1eA(}}hkH?#J1q!JI z$g_j52vNv zMnzSC6K!Nm-2JkHaP#T`2^_?Gve>5+)h1!=J$_(w(E@7}gBJY6&4uk6cM^|Q=w`)yMGZ#8q+fJxggM;(BszyJU-C1pyzY?M^12||GLaJPMFeGKHRp*8?x zTSXy;w#1a!H=WxL0-rF6tjKZ&bE>td+KC}w9*gKjmAyZ!#{kyvOGJA7s(`9URZ&qP zQm@Nj2hC`q0#<^6l|`j4uO<@#%tMC7H{68Zw6w*Y)Zfy!%x!nulYq62Ad7!glXt^*L&Yr`L@ZTP>IhjFfo)x8eBBj5 zPun#+ONJgij|U{;o<$)zdM7hwgUh&ws;u&m(Y*ABLrK zfef$kN>1Ff_yr5@5$#|u&^QEjqu@X8V%pWYhr#YXV<3$gazo~sR*GM40Ww8GZN)WM zh|=>+Z3z*-NZN+iLM@^=w-75GbV+zL6%Zv0)D$3M;;cwY$=db{5;zM6wM8#P!}l^p+E{yQ_=(>6rd2Ph<6)o|MGrT0E9h|BPNi}bEd3d z(n7M#m-CW`^S6i7&BODTuh;i?x9{HW?yfiY*Zb@Jr|tIk!~2KdpC5nwGCe$8zJAry z(=wfjI8#+wh=eIu6Bbrd6%i&Sun4TdX>5urAS#I=5Yoyj%wXc_cv((UK`z^+RxFo! zJ}>nm=fiQR;9BfB-Y{koJ-?iEsZ<5ElGNaWl>rg!&mf3Mjbag;SFJ_|D^#_}GEK+h zao7xrX)d#tGEdXD-+o)B%LtrL$M5IM-J7>J_ji=j%fq8yE?COTA7ATbdHc&>-hTdg zwYmN=d^tXT*D{d^s-lWQQKXitMZjF8P|9osVak?Vt+g1F0)ch9mQqvB05Nf2TU@x8rViemPYjjbpha%g-gwwN{9N1X8t@oHxhEgZwP} zo9pT6NvXKtl6bI`330h`Tuv;Xw#uYdXP|F*l{|MvU0 z(^O~}d83&)#ig1|7;cVwo<7eJyZ9A$KG>1MAZ zU&KWr9aJc+3xyL@{#Bs5xCToF(POxqvSyB&qv;7u!?kvmr%&fkf3z7lbj1qPX;f?V z68zrq9B5HWJVl#51t~KN0*C4AV94L-T-K|Fb@DY#AX8sP_XR-m&@lmZEA!!)8w%&5(Y z5va>nC*1uVCMyQVsRb+og+Tn9Fi}!#j1%uN5-g}NlB!;qV_ZK2b`+*m04mkoWi1tG zG>DOeUJ>e;Uc7L{y6pt=_>+Wop=ln|XVL;TsP`|A4~Gy06lq$f?T4;U@Me@oQOTcv z*0E%hY-&#Y6_vY&#|=Nb^2TE#ebiJs=H-PdYdjk-bBVHTh>oaDDjK61UZsPr2r@nM z=AxrU3$_4Dm2#t|+{!f2yU&%QnZsQWZx;TjpbTK@F|9f7ZMG+m+GIySq%0$J2x z4IEv0$4_m&;fXv&utIybcu)=v@4~T?ldhGq`9ZPT*;~JCIl_*1`~z{Ls%Ib&ccMh- zk5x3j%HX`(XC;{HXmL%Oq+1+E{zQqaPk_cU+4*8yDen#DbF<%phbSwn7UFC+oq~NL zCsOS7Y*|PR7lJ4pi6~@U8K~k%b}7`%fw14d;z?Z4`r?c5P_G9@-HwOd4iMeE>?@(8 zc%R1`+71!5wTyN4m@Vu^-}P48)U3M=#^=-v;nu2+<`3FlQV4#q4QS>*wvxyLD%-ZT zNVr*S66hal{@9xiB-KD5~97Y4=Nci>hdDO0Hy~o$$5~R;ZIwN|ffyRL)c3aoLpVJWYpF zEmG=bemUm6(NaVTB_229ye!k{jHRMfsQq8Ukx54>F2D$f%szcXDIySsXuVA5$ zdb8OqRZ%KQJv=_jvb=r!_IlhrJ--~%!~L80w|94t`n)VuY7#zteSCR7J$(CeeRo^R zvWS#<;aa$gh}0^mcF(*aYfuqQpsXn+Q6W{0mZ@6BubXGs-SMP^DzwOSK5vFAu1V&3 zIi6)&uvDm4)brtGf3-jFH?>Sd&YZI5jG|R4e6dxf)OpIozLd-Pc)q#6fBF7nSp>xp zL$Ryw3EotI#C1q)-K^ykv9sJMYs$JAWjk!{Za@F)U;f+w`tL&gx8MJ`T$b(K^;oJt zy-*osnq*#vL@DRZkePUz=X$C0X_9G?MMR`-hO(5$$EQ3DH&-_U(fsoAay(3j=lOV8 zjxWpcST9GNFEme7i`E%h?EpH|gxNM*i2VKs4JW=bRe=YZ`o7ZsRn>sy6Ab@G0bm4^ zYlBIzKn(zmi^j|n$05CN$zk#|43-_g4m!2*=m;O=zY>PCVXiRnD_Yye?y$=z**@oh z-j-?YA$-)~d?;yG7PNRd;KkY|YR6g#%O2pCJQE&q4fTauITy9++m6P%A#VS6GahqY zvrwxq6=*pGjo<;W_Fp!8vf6QItPjFVF+9E#Fr3}NAaPaQzY zOzjMSufiHMncTK)2?FlLd>aUgb1VHZy%4a&tazf&r;X4j#IdYB29VxifVu?uID=0o zt3ZoYbX(_*cK{C);$%6V)Me8pM$)*xg-BZ$AoeS{Vf;bz#DFh?0UeY65IXxXH_lrD zKS5*H#M8-ORM%ZiipJ(OhX^TU&W#^E0Iqx-i(!#v(*5_sSHvgJ8#N?0UQS_ zF(`w^icxt$8hO~JVLxoH5^Yc+GS#K16x3PD44okh>5`}tDUv|FV8i|*V$Be(#HiWY zCeABA?wf2!x*ANeGb|aL>s0DI)A=&J93Q`pKfZjqdVl}s(}(^2+s*a;?Yo<+`~CFG z+vl&h5C6D%`TK9n_wRZ+#C7rl5*R~fmOVg}M{=RxOm+ak`US|Cb7rpdqH~o^m0ITW zc|M&{RF*22nfHU%QfnD(cP z^K>}g-ra77@tmzeGnI;RIZwyK)%DG^8DG9XlHmQPk8kepb4rhYe80S$7@+C=__zOh z`F+^mUFU5U#d5iDsZuNKa2!aZELcIL6_km=Ln@_;DpI19t4=B|tYxVwX;~Il>~|ZL zvf1oL-aI}(F2~7kXOmJ?_41-uSDUNbtEY#F02)TfVvRYN$+>)yDkW>Bs!vZ(pML(h zxw)F>S(k4ZXlc>!h-vE+Un*YA3bbpjvq4*V5`5qlOa%+4mdy>ytu0~+ryQBSlozS2# zw~f!CzQokSvgVH(fJAo!RoLRZGRFLXCDXs4uEos}vChirpP2(x+gLh@yNGqz0)S*G9^# zc`%KO;o9Q3MeMO??2O^kKh=YTy>2gB0n=7J>5K0nWnXRo1@#2D%cBoKS8!E59*aRY zrq*?Ig%)BzT0|a_v>tEpkwyxycRGTC_??EqLcd*GSM4kM!zmz1*bRHUX=p14YN`^8 z(ifk6k=j}^+08$>$7JEB?G@c_wb7$}iME2VJge1f!(K56)ZP%pb|5SY5VMMT8}V%h zh*%rV-}q`c6*$eH4Xipl z)sQPfmD_&U%NwoIzUAm3Xl;{SNAj@R_Y|5JF{Zqa~hw z@s;t)NNou>QX&C^ve1CMLE7@L&EuZan0U}yb(y3rkb*j?6p{j}q($LfZU`2ntD;h; zkWAWy*{+gUO$@7utXb3!761|=f)-K*ghU_}vPijHPS2MgKaPKV*}r-B_LraUKY!ld z-wsz-<4qdxH`|-t%iqWIA79Gz3zmylNss|hZh3HcBEnV^#;v)LQw1;#11XmCROgaG z2_&WHJkwI^W#(EkQ58{09tTxDACIz>Y;vtmva8l8k<*pwvz4(OV6_YBiC}Sn3aRK^ zFE1x8G7e)(sWO$zl2l}x50Ao%oBdvv^78mF4E)p2zuf-%%hh)GD96(<42M=4#k( z$L+Wo_Br$A`K8tcbt&hIRw+bQPAyfLIVE1of=pLeH}~(~USI8}!{O^6UoVeOmxsrC zIZ!Q-k|0uMhKN)Zi!T-Ne+rE4cCB@3am(xRK|8)`M~h6mb**2L|m`F?6nuC1t>;Xo(d(AlW8kD|E-ADcVJwh-2Bw!yCchV_8Z?wj@v zwENS6j&9=jW0NU@g!Ui_ENT8y1A;X?WSD@$)T{c1^dMH$N)bC6Sqb700uqprRx@5i zRaA`N2~E+!McO^C_=QNSsw&kE=CJoElj?U9id6BtqO4)Qn=Bir#L}9m`ASEmgbrg7 zC@KObXfv;lM12VVSxP(*8YtC``#j|nBuiACAta>qVIMH zWQ0;}<1`)|m>q3Epp+1e(V%hu~PPPWRLLaL(-GV!7 z@>(%Fs%vd%?FrT$l)OO4sc;Jw?AQ%4EU88bh@%^^|FCbNixvP#>wp_Yd;Yv+niadZ zj?Nq+#@12;UFa8mn*nUQjv#Oy1@^oFXti_Yf-C<`katS;w+Epj5GJm6N`B67raaectRjZ#fO5D9c1;k$F;CNET=zEvki}#;n8ri>eBf z`N-gn$y~GTkDDGvThA`cqC(2C7fK+lc0ya=7hMPu6EOrNNs3IE`3x&HeTD+dCf8=63UAyFLE>_xkjJ`66W|PWHN0F|Frllx)E8`{Hcu0}tCxRXLx| zWfBHRYgsP3T=YEG%M92x1Imf_yIrkyo-V9PBC3_h^7G!!hT~#w0wU?mXUN6yeUe=YzD3QdBl*jG%>iTBb?gY=L(_t9KaU9Ar>x9fq%#_llNZODx zW-U{l2`cAldVZ;wvn(?~fGS0$;`H)zb$7eJx_&&LF=Vczlv#ypRko9bloVW4(vZtC zU#9bBw@;g`%pjRDWUVTye!Bw`Q5Nf6kbJ8u6K_)5Z{NOucl+VP&HdeGyFZ>U-~RJQ zRiwP(O-^}pb$zqhj0BNVYFWxW{oY*l z_yZ0XeUuCqh27@>L_aR_4jmzt9e%l)V`>R%Slfe!C~9{dguoQ!?^Z$}1Se&vT?ImJ zgUl(QU&V!Bz}C2o;0?0ZK0Z*HhPT!3JAk+cwBwT10{a^J-#ZL)!we!q3a`Y3MAna# znSoXxMBW>dNi2kfii(I7ky4F?5s;!doUpE#V(l@Z-Co;<8^@|D@r6bg9x&T)!kO@D z&Mk1s3g4{(sl)MD7+rhV_&pO1@$GY`(9oEBk5+QB3SvFpZI55W1V_NG5FAyOU5gMe zQgmt+FyOtyqNMDy(~)VwjSlozauCfz;|UFADAvEx{QDZCG$f{gS2p^Ddt}s>+XD#I zS1aCdYtUJ4E8;VaRHX>2si6>PK!+T?QG$tE3Aa7bq&r};jj8!;cm+rMjR0>Q#5ImP zzIgrPHpljq_SwjtD-_C6(E%U7-5fw3EK8Hbs;pCXeBJZtcXC4{9fKp+XAfC{oM;l z%-&g3Yz~Le9!5>|`uyFKmT z)FvYlZZtsFryX+-tQtC_u8Of?Xv1{TLAWhjGB2Tv5V$#BQ2PSPT{RpW=}LOn(%R(O z1U^8yyX{B~E4|u4H%J5}=2nYBu7niMwOKTe&eiTeu{rg4HJqMf?e3i-$Uz9B?UJ9K zW8W$xoUPp_5ohIGF$#@HyEN|8xaB+&ah0OWqGiT18T1!fhy?B@6i~3SjG|Cx>+u-q z9|Xj0)=I6BZ_j#B^k81)6q|>NMYFTJN+b%>B1FuT6J;zwt;=+Iczij1|8e)z=eK|P zWxT(+y1p5P49&whp8lTZhaY-Ym1>k&<@=ZA_WsTFX26mj{`lkc_;_=3bA5Z8GGE>UZ%}Bl=Edi9%Wfn zD^#^s1(>m%FNepMyZ7%-#}}EWVY8{zL}jL^)TER^EFuZes^`OLe|vL%e|Hv?TC}o) znMO-6s*+NY#9-D0Wu-BV+j0Nq_T8tCpMLsjGmeiBPrv{EkK^&2H{;da9d9<{)$aD~ z+dSms@i1R5Pd^^Zvea5c1a=-Fu`)Q(FpQjso7=0~{Z$4%egFRS_3O*S11(EhD(a$T z5hpmWldVJFTW;il4v7OovSE?FCn8X@I39*!p z?0XpoNWI+Lbf~qUYz%2DnlQc@tG6`+3k2*p-)EMmJrsb7({R?Fm1*_a@NfuVN9}G| zOLO_Xux?4+g+@%=evz0VLSgUWw1}++HKhumMv>}_YL``+MDTZRk2Si?s_UI^0Xz5X zCbs`OFjfpM7Q0O_aLei^kzgk;WC$9ZD6uq}+{a|dwiV{V?+syyBXWhEw++WyL>CdL zXf3r?6_qMwE&$Zdqnk^#bG>#W)+(fOzp61AH`*EuaPXTkz!fg}KRl!R!zO{@!#wL3 zNv$?h#AjPD7`{-Otl83Q;Ge#HtMB-`A`3trNXIxMvIM|!nzWtG?D`VEKc0_B@5fY? z0?@g>daQv6k_jESHUOimlj|+lxB3$+7%00R-ZO00vx{+i6#_9eLF|k>0_wH!!dr+C z!n=h0_F>qrn$OM=?f4)%RuAm)r>%eWhg#-so$m3aTv;Ct0clR(R#YN3(e*R?D*Afc zxYQbAs(Wrp?%lhA3bxtxp4qxah)b(wnM2bX@L!G3jzc*vN2q)NPwS8i4f26~GrWwu zS>vcdQm@pD#c2dU>=?UFkfQbI^-256nphGMK|rLUDH%IK0#GHQXpm_)9+^OMhmK7+ z!v(F@IT|RVf17WiK9?E7f)}rMDqV*`^anyw~(jj3Pv^V3kjnS4jf(IP% zYAtgltH%B04X{HsOm4*RRm5B2%v=psgQaeGk*w!}GhRU}#S&QaJUd=N(R>14&85NL zko7-iLRO4|0co2y`*FLYG^(PM3u@72p)yIiBosh^N(74|#C<3f5D|@M=KFw0uJya<`2z7OBt>-+h`j>Pp~xzHVxlo;nHQZ(=8*{VvRp2cEORXj zp=vEsmQu@RyJ6z_aHd%-?<*2au=xYgl|+U?567!L05*B{d=&)svMkfQ+2wIF97u^^ z(3}Y(IxmM8mHqB&%SkECFUP0z<@oZ0`m=WOne0f=%+0@?02o0OiueLFop=`urVS!(jTPe_(JpU#>1 z>iYWSaUQn2<#f@qKo>g`q97#*WMBZa6e*(D_qS!5mg$07xss<)a;hLonTDJ;DUFo2 zrb1kS(5WG@NtjGJX@{rdhhMqw>LC; zMFU_1ASiA!iX(|4^+7}0e40Nk9-S_Zk2T@V<;CUewID7u?MsIrr2MjCvP0`zu>RHntx2Gq^;KW6U)Xjl3gx zC6WaJJe*0hyKTT`eoUMCoS`MtT^b&C)9n<{O(OFrw78=6mP2y_tgT(c>~qKx`g`VA zQyUuLITY>X8YY`)YVO(YtntW!>)M0Uk6PP%pjIt4m^3K@XjKud0?YQPBBRuz6>0@nL?9_%?Bw-n)XIDrWQqP7a^p@S z6y^sK$opb9O&AH2j?H6Y^t6R;;~c!qv)5vJJtzj2O(B6uHJQ=4j*83{xNkQ9>;}EU zf=%zz*@`1h4?V!2h|LME7~xa85I^v$XY%Ing%Y)nv{NOHLUnd0v1%Isaf})8CbFc$ zK@KF~_v}7A(e$%LVpN0hb^)Ep&e?I8_QZF_V<^xT3xqpm4Y}Hp4`7{}B+gVz90-a^ z!v^T5k5u+tl+X>%21L$_#v6k{aP47sg$;=$n#q%IWTcvS!cXjy2#7S1SF@(Xh|@$6 z2&F{Kob2FxS%6_4AbJAC%CV}%cvrTz1QC3dO?gIeKWZ5{H$kwN6b21lU$_|^{*%>r7 z9##QY*{V9~SW8JnRvv4jL{9B$N}Q=fVyOi{6;-e&5duUI5%p6JCTk~4@?g2mpK)MK z;eWL=VO_38ltC$RA|@yk)fW9hJPA2?pXD>TQ-nzEq$b64*=f!!Mh>F zyG){D>CA~Ss^4+9!ZHI0L=Z~05q1(4;j1~p<;w)HB4%2dhme@vtI*kPd%~joL~4Ue zn3&i*QU&8@psyNDNvceWq||X*Q@>9AN{K~ADHY>DQfM4VO2TNFH<9HZDvSU~#Atlw z{2r=ZnL()v11v-!Rwi&#cX2~ul8_oEv<$Tdp2Tuz6zcY?76maS2#Hhym@`O7Ri)mQ za(8omx4S!k|J_ILKEe7-^oz*2U>@FFVZWmSMg?)L#n~2thFXKFih$LjvC!S!o$L!O zxiw&QNUTr8NlgVtd<)B%b5#X5r4 zTB}Ic_lfB8_1oReR!T(`a$u*b1f_Bq-oAcye0DM{S29YsT1gpH#g2u5D6vP2C|A(! z{cgEgEl!Si`+Zd?3Mp%%ktwei-Fnrpm#dTY>G{!Wy*eC^>h7bT{p|Mga@g(| zlozY6%j?zVcyp9fy1u%)zu)fTRYJvF@H=P}3$RY)abN4+{}euP0IN|+SY zA2^C6-U4XxRKlwXPrkTe_~g6RN=KzF}@S)j|;2chs9yd-y8sWo#ZN}?Qvx};!6B1(xUCr<2b zl7h^Lpldsgam+)2CI!%R6x96z*`cS+Uioc%W3&sz#mp#IniR_F{hm zJXFxu(0m(;uqXflC090H-%K1)ju@9Q(W?_!*A|9^Np-8nHcdkT%E8P~uXT>S*S5bJ zLBK{q5CD}R24+?vp2EFNCY7nY)Pp&GefJg2DoqZcZ+9He_@7RnOhiCMm^rw~q-myx zYq#hlAnp{o#}(yltdu}~$_q+81GQGEBWj^Ckc?DEf)G_eH0IhmJP6(IyHq{M31 z%tI=cGXD z`^9>5baFflWh|AsQ}Whnsq2>O$0_WF8%b#u7i*8RTj4^#^e zBh_j{+yHozq7?PsR;K=}JhEj|5N62J=d>dJKBc~J@SPfSSjD`~IPgk7m}xfIFRXsk zGp;=eP_G9iu+92)}rk$uwjKL7#>hH^d*mgxgF<_BJTjm17{TzyNtSLNYlN>(>kT* zeOf(Ljjd0vG-+dK1TR24)WSTkJWmB<1lT4r?XniZjL8wyFz5Og2c?OjftYG1RF9o z*zF?0ZO&FaJwh_z2SdF8A!uPt3aSZ-%e%%CAiPu6nD6In-sl{=huBR&!=^aql@ya# z)>Iosoq>>1018S;Q(|UH?9M1BPRwAVP>4OS;GTZlX0@y+*Ne$Z*!;N9G_Qi1J~8Kt zJ^9DJr$ou_RaMM#y6>o#fWkyoTFI<0nH3a-wP{AkSHG=JXc@PgmRaHhY@M7BDV;<- zl)|KboBuPD%dilb)C&BX?YtwXEx&-fNi@}&`9@K^w}sJ-wF+IT;iVW6rXgd&6T1Y= z45&X+Y+8F%`-dncGoLLV$o4H5o^Uu~CtQ$KNy%DPCX;TJT8pSwEdy(%I*|BmBY3Ny z@ZkI=?e3U3DZGpr)PZ}-OHjvYp}9+4pHiQ@KJ{H*cB$_5}MP>#*PMcDldU`+Ir%{P^Vf z{K>QXH*bc^D=GzLu>PbW8|uM$jd@%mnUGRe!ESdTC8;8{l2ocyN>LGwk%}qhM0|aF zD~FM4RjF)A4wLq1=FcfuXBix7uzCk66F>NT7KeGV-|vTU;FLgYI!#N+6^s&uNCk+A zSqYk`NYN@#+3yoGY9*VGPNdA-FZ$IAl*VxE`PCu-mB!YSm&HVoqV8RvpJu#{K>M(|6wmXXDlS|Ip=(TcYAexv)%1AC8h*U`&x!!f4p8lJ3l|_yTkqcr=P#PxwzP0 z-`3q8W6@F=6{V_FSkprlXf@9iP;K(>k+g>Qe3$)rc`h4_SRmUZBTFf}r3&!F0FYF~ zl0J0OZj6vFa&w?=k2x$wl+lu7B=M+ z%uvvq?$O$OF@r32sI0k6C`5!&aJhBG=9mP;%t@sJWX=o}lqNLbmWynXvU>i;uY)j< zvguQ|XU@9$WsH5bLyX;*r%tvkd=+WGi12c>_Ev}f;XC;=Hqz6+nq{7N z$!0NaNt!IpFmHTG@TTr&VuJG42s1Dz2&B|PSazBiGkiaZ?%ztCC&X1Iq~aSkfd_;v zko&RIISsr;4d)?X`vV_f5)+sEaNH*MCJuz39kmEmWY9EU#c*rzlmT%kec--AG+Fe- zP*qj-k1<#duc{*=@d0-_2^O_Zi_@33U5XF2r_#h!t@U zo&qq2uN3e9H3;Qwr^K$?*2GS^4w^+lLs8KrofA+J%eVz64@d+7(2$NHl=uPe4-iGQ zU)un3yrr=jwqDFhwVes8<&DUrJ1rD=%+3q73&Uk!X?g(rbs#y7=A>cKJy*jo5m%-O zu9(nz5ipc2_-aGdq0E$`#?*rWi7gr1#v`yXG!$rrDJpo`HL_MBvZ^C^GI>*;O3*gu zCb4xH@_EBdNSr`q6rU)>Cj^q!g<7tK&lJ}%l@whL{Cv(VPQ&~z18ACPQt53ib#^Q< z6CRrEqOF~u@Yz|QFw|&-tOp*$<)7R)Kj+@a(6y7*z{P#G!#KUA1y#bSh`nUXq>qd7 z1B9THJ00q}nKl>GnPaLNkiZaayM+)Uog7Gn^(7)dIgny8@u}4hgh@aGRP|B5UKmbJ zD`qGwvPh@204+H!!F}pi+^tf#;C{`0&wZEsj#J{yxlfcc=S0k@OO!ZwnQgKjfk~^3 z!(kW>yJ5TA-rOASuJ$*V)VzWw}_P%He*8 zTA`JuZtA9RH1g=04u?^&)U>egFl#eDjWJX`?DuMhJ+rcrNhDRlidw9RhWM;h5+&kl zO^uj<>b-AC1FOPmKdDut!6C~B!vNX4f& zh(LtAi>p!lHJorqPt+itX)p7%$=y>wb;_Y$-0P>tNKfj-PPz5JGD}3w$UA_8=}15N zhPFKqB04v?G=&Hwz?*|c(?ZOxanWtk9J|jnc(Nuxf zK|HiwGkZK=E{8)1lN2zK++8tM(PXVP%xj;8$zIR4-)^_5P-?kJ@pf_v_rulhJdOvo zC>Mbc3Dr$Sqg5gk!P~-_qiRC;YY=-zh&!|bkSyz?;cTn1vC|lqJs!_*vblvc#SY1G zo^5&yQOtyn+YNbl4Cp-upq^2Nhae!hj^rfY>f`%u&h00iz!7{>SC7#eb0W%|iwn`22Ecn`9NCLrsF&fTZEz)^E2 z-A~}CnhECKcRRFS9ARx22t?@IxKc9jEH-}fAGqOl*N)Dp#qx(|GP^@(A)4x)UP#oG zV4P9=D;qfi_iPctlIeb!|D_qTL6$O+lNJvO)p~QXP zrNm&$i)FW2AMvTGj{D(oe|vZJ`ugJ4-Q`8y@5^qNAzVdk34cflBy|iQm9!QjpoomM z?uOf;@;GwB{^qtEMjZ~U5?MMFFk@oeZ6BGq>$*1ShEzzjj-?J&sz=cXsHLt>R^xtO zhk?X3l9@-4nhK_{&@(5%v_7lrdO`#On+r@APZqI?e~WRusB+uoF1Q? zA8RcaZ{FVD+?M^IrIK<2C+4yr7e}ji-+5=fTE6}4lk3l4?XGXj{(w5NR4OBC@g~?P zjt*=!&E2(94Y+?ugowX~8_X;NaUlsOUw{2N==2=NpapOl2@ z1r=|5PeG1GFkbp*cY8fe5_EQkF;Sc9PK?3JxufP1(KC2>H14ZW zx@{K``_+z6bJXHOEx-_0Y|whiHGQ=ce0b+V^cN%!a{>K5TnF;DmGuY}n*|~2KHNa>G z$lAcx5(uq@Beh}nwwfCM^Bq^mN|}8c2-+?vV96dc*|SK5UrGQY#PtD<9?y^IWTPe6 zneMP%@SE^6i`j-FJbSdehQ_wXi5Fq-x?`$Q#Q6wI5H}BiEZcy{M&}ay%mAJwiQ<3} zr3f?xY*r*zPuC%8?RpSlR;8van48XV^5Bg1(Q-2l_aO*3iS z5s|MO7)<7paKH^;$?yiD(_Xt#MwB`j|CE$G(?nDVwT^p5A`cZ;MiD89Sty`zoZ^WT zEW(1UyyAS4(sAmJQ+GspL)>wnQ)Xo?QfVBy%CHlL3TpVbN(3Zgiv%pXVcGY6pZhK^ z7pY5}Go`fLtTv~c^LHLU*}i{wb9MdZ_3g{o<@UPVZ%H&M>_}6vAW}t<02Lj*Rw^w%;S9M>K0zxXhJM1gdYO`8yHk;E8fVVGR-duwGz}R}l{*2p z;^Kx(ZMYT9jFtjQB%A;v3uZeGjl|8sLI}{I4Qw{`l;AAo?CL~XC4%on&1_BzPXmpH zd?^AwHp3jq*eC~t*to5mQI@WWsHxiajO2DPMt7URqJRJ)6DVZ zIA_+_E{s?#n``la5n-Fiv6J(?UeDSfG5`wmO-z(POiF@47#bU@5e{(p)c{wkcSc*J zwnELbihQERd6CZkM(ytXVR?et&5l`Y%OBPwKK>~-vD<~joyP~(+!35heb~z>$!C(i z_-p&^M73)$mf^rOW9#x1kVd@eV`xoplm!);pH1?*hzHMNVXk;$Oi_WrbRZQ+x$Ln7 z&u0#{G$0ou-yupg0HWkcuqH7SIMp*_Z7*`}9CpTotB8u!VK94Cr9!Gy5v?MXXI|WF z;iqNPHXb#sK{O^L0%fyVe)sX1?a!ud1mVxMow5*E2wWV83C)|ji~&iZty~?3d^^N( z&OzImqG>j5R8b%n!_~4oE#aMs3XuFtMC@lOiv6P;#E&D<#AyT_gQ@h#ql&6ZXe@r9 zKln$qEw_oMQ556{z*&w^Pc+*Q;-LFNZiq}yG3~mCYV9{H(8MYbwvizUz$jjKc-URf zn1Rq*1CTL*F7v7YSvIqVuWC!Lo)}8r-$GTHNjy=8h`e>mP_2R!1gIbpWE84JiK_MR z6D!YU^*V3nmR z^uF!kT!h`wh*z4wdNB*Xw?>UT&6onNQ9iZO+b)&!65sz1+Tib^H3|{^D)DyGIccAyu~S5Gkop zwPNQgq@q+<%6L04LuFvCDudK9Gor7TF`(isjzXoTq)^SAYAIT3Ewx{EP_1Q@Izm;Y zGACl{`#yJh9QKd`trjg&5u0*L-X%sF)@WQBh_q!U`e;w$fFjIE;SD+yr!IG}5L|fk z7Wj8vD?;RhJ;|#K>^zB0%#HH^N`%Cm^X7QNLT?X;GL~d&ky5`{tk)a#T^R>vW@|5E zW7uqez>`~EW3jYMN1*A-{n7IH?Ck#Tjs)zw zN+@M8QBHZ)Wkp^t4n;ow?A6=1uim_TbGW|aqI6Ihq$+BywMw^Ho}3)7PmU7d>hkjD z?Zx44t3`9lER>QBDUsae&FRVM{T-EoL{LqWiPQp!?UzAk3i8WF zpH9}Zb&?~6f}_|JZsr`lDV$NakT0h+kql1BG{w-MC3yjzBacuJ?ERdT2$bxz5#YhD zz|nd+I@5)A1CwRq1i3I%G9E87bgGs(>ys+{Pcs6L0_;1W7mc8kjfjN>Nz6hisX!`h zM4AY+vH~ikiYihoRJ2yrYGc%xl%ztG6026Yb#5v7v9MtZCtJ`mcY=N+xT1ow6|Qp0 z(^m|n$FOi=&yb2N%J^&@CwqV@CD2E3_ z?JQbnDjSrh*7iazRm}RUvY8J`$)uC50+pFtwh2*6iBk}q1OiRqK0F+MCJ|9pQ7N@T zRaK-)ttwJ$)ml+SrAoDGB~|qtMeqHp6X}j$`--?m;80gV7KqM3%VUF|xgZG)AxsGk zp%iF3kImm;CyPTUOps;>P0{*u#l}x(W!e+6t2K%V?ary*3KqLc^<5w#e2 z3yr`X97kl>$lp_1hQ(Y)DOgy(ua!KByndaKZ zh0okE_Rat_b0xzoBeUZmNjA(J1}O|qi`f}-Lpjn4oMR-P&dZ3yVC<9nf60BGh~^jv z!zAhx4=7-@ZC6!crSMe%P&L^r)dRsyBI*na5fh7QcI`I#cUDtCDTz{zVls7rs-c3! zrZ@0+ve56#b*EH7m6f>w?FkOq7Wif}Zkm*BzyNlDcD}70}lzQuaCNIBZ4tB0G@- zY1KMP9er}JSGpe{#Nm z|Huu#o!OAEgtU~Cz zR7!=?IF|KlS!ylAh$5`8L^ptzi)Afk81{s!b%gi1wfvk4l9-ba?t3(gVRJSJ@X!=_ zS%osxhbPzpW}@VXh?o))3xR=Z%uN*nLn=Jds)(9G*O_9WHEWvNr+$%gzd1VUIbU5~ zmSIp*5fK(noEDw-Np}{cCStOjc&U<8PCfR+NIB)MKiu#3Z*OFKV38_9HUKUR45_uw z^n73~_3rxm$?^K=!_cfoY;HU9p%|fYU){B4_qhgP>Ls z2*k>r{RFU$%26j;5RkzpQHiRUc~CKhEwIMSQlyM!I7k__j*uc2aI?N%rIy1eWrT{< zQKdpFlW2h_w37zvBv%g2&vhrqdM0_*nwZXYF2?j>lteaj=`2ck+lfhhQjq!zxlxNW zP72$IfCqu(XEwzx$OL9)-Z&mwZTN5e$tHA(Hmbne3wk~gb0SNIRzz`}m5QkZm`C_! z?QYy6hsw4EOkoULH%tvj}%{9BCM_ z3kd)Mf9$sEwqLP*WYa?2RSnJV57Qil@bb)Sowf??ZZXeYnTUCKIGvg#cppFYs_T}u z?a-aPP%R$Uvb(Hb0_99muAOqGAWWM%iUrbIZF*oOf{GLovGHvpTCJUffmwA{JDy0S z+Qer2J-LywQcyt%uuX#|Y7aJZr(%x+YEWC%#v-U{M*GYO=?9yQM!u_8`dA`CT*zQ= z_;d=yI0pm>Qeeu!i_@kdH-VK6I)j2Z56|3Y!u!g)6iiRxX^u=HHfqe-rPLc9W@huBlYSB!bY%?>4VxX32nH?s3<{Fhy(x;YyO}i93fu}eXu}KRgOFj zUv0B*)4^{6c0@j;dM(`y{A2;bhs6T{aepBaw}Jv}AML!FHZu`>d6V)HbsJ7=;-yMft%{x2QDxl89vGAkS_eWQXo89qMS*{~H!pxeM2-YC ztF4u!!%isgZ!Qj({cXRv-fXTPou52@eD?0sqbJ9UM~{{#>ysysE}lGl^U^9R}OTaPJC21TZNRGIM7&7hT`y zoX2t0DlQLFp~7ZTHKNq+WWWr9?O|kRAmwD8j!>zLrIs?r1ao30%9J~`=4i~65_MS% zkQj-;)H!&Cs-(mz@wnZN+Z~IbR)$l&e!QR}l{rgM&KWfd$B)*Vqf!Kl zzUz^a)XK}WKHD6f9#iI0WP86ayCGA@$f5@oMe2`FR*ThQxymSIx7*%a-P~Q>jQf#_ zbRZBX&ZTwl}wRyThTN)XvH$RD6hx6Kt&v5kK@t$DQcj6;PC0w?28+>pUYj+#Q%mnP z&D1aYF6We!pPD$hc6x(*bqJAQj3bMW+47tegPS5MRUjf-TA?As@v?@#u6q)qFaNnp={nIKd+^A-t-x~&Q7#do(1 z@rv+0;8PF$GpO->8c)GYngE87RPoUb_IQ>CZ2E%T4~$P@Oza9%xE#&>3ozw?PM|!W z#B0&@SY(gvw@Ow6Ni7Biw^*#yViyh(BP0=|im_6R5LoqujzD}9MrfAU@o0NV_}Uu& zS5vED)rmkLiMQD9n}XE1IlEmZa$D@zVPg0UCC<4gPARde6?lURGUq=49T}A-=K8`K zpLm~bsUlKK;Chv6EzngBpA7jy|2sB=-p&`n1lz@EcXMWJ*5=am-9) zrxO*vdVBJS$&iPT2+?{)?T2>W9L`z3Wuy*rh`tdV=Jf`^bw|$Bc2EfIF+-f2M>2)$ zEQV;?xSAk83x?g)0}WH4H?VaSwoqd_d=aN^2$7=?XUEmCdAPIwvGmYA znYP>49_qebtH(4e8!C0G7gFD&kQ&U)2b#``1)1D>PoDI{=T&f4Kj@y7Ux(W*yAZ~) zcIZ#dGYO^3#G>9ekJ_C&1klOtGQP8#Vy^xy9NBIRQDtHkC?%zZ(mLfMN*l^+&;sb7 zRK~qZ6&bV+z#uv@7eZkaNbywmS}o7Tkhd0%q%^3PEH>JjnV57WVT@JFxIY|*ySwW* z7jIs_IC=i~@q5qCo;_JVeedM>Y<+z8dbz&+?9=_lm3l9H7J#85)halh7SswB(yCrk zM=eC|7MCJ;h!&uwVHgRdRl3A74myl%lT$%J%DG?ki_7aP8AnJVf{L(DjEWIaq2jc~ zqT26TGiEr$3*+a=2Y4_u5--+E%CsN$MQYVbD&kc`mPc)VMKqRnvY#Zx83YDN6@unP zN-0;RG8V&LX3bnf>Hr@HN^C!=q+;6upUcBB72aIJL z1|qE2%k}B$s$UVK)Wddrcl-A3{mtFDJIGiwvCkR=gGGUq_-L~}IX^oL!_}MDySuxx z-=h?a6{0GYRa9%k%ZjR6$O0vPcFdy3SRX(uKqysE+)|iggZ+BT$!O6Q@Bl0}mAQ>wk8pZ7oh*Lr$Ay_*>3P#UxfmDS> ztIWHxee@K`#^VC&-7sT^)%}O2OYN*#5s0egih2s1h#zv*1hSWj)MPa&Dn>u3^@M35 z)Y*4nGIoB3G*m0iP++WfOG1OT?&NMToAGs9I_vuHh67>M{4n`&7_<+FPxKSb+)M&d zSm(HDutq8tO;i^qyAYaL+c;1Q-#gY9!)FvzrHB%9N?zAxo9`gZXT60NffiT^v_H84 z#B9T=VkN^`5xg+d-HpK3A?8iK+>P`VA}<9AH`#HcdlbIBY7N|+c=mJ&32AIFFpOjT z9Czp6Z7-&wtPNiIcgu+;n)W=U+;P-Jl5wFHUNyL%a>^+srev)X{1L^{>P5Axn$KEG z@w!_P%V*P4in<%%IXh5=s0|4*OsL}dv;l?%J_s@-dE(_}Xy4tbmKfg-Z5*yZGD?vQ z;a9OpTKhg7APQaT|~Gc=h<%`LlPLv|JsXtQKdRbv+FG-EevF=KAg1{q5bj+iMl7k`$!Y zk%^V1iuUV%b8@^nKHl&5Z(hDC+j~6>R7OaxWwc~;IF$}nn}uENm!_EfP&sn7-ng(? zy(Ylcy~AWg!@rwOn}%takO?_YXbk9trCb4+17O%rVKcQ|N2HXAXj1%Ph-DP<4134K zvo?*P7=xm-(S1T%EpI2(4;qd$pF1%TG1$f)$JlUV)7^8eIw>u6zNGa*P=PgmBj?Bz z7FRa9j4_!Cm1<|i*bdK6YCulF z-aAv6p-w3dldr93*2J@iBDa^dl=>MCiJP5#LpKx-D@Ya0tZ_eR56+xKf(r5bZ!e*+ z=_iaP^hZ4IFjFl@*~;a1-SF*J$EE2y0~wUB%BatzVM z7SGbk{4F7z&LX;k1~57Xg~D0M0}62GQhWlEgjX75ggP>%$PLsGA=PNKAYrQ_E#E>we&D6ut2V_n z6LoC|bdp1NHb}{$49znoPozY!Jcu*x=Wdc%zvGY%bGe$nZ(g598k~#1r2w^~kMK|Y zJ`p~VnLTPZ-b~Jew!MjeV)vFa;OR!&{NsAU|sTa?jS^0!z|Q&r9)`#-4d zjiW*Hrrov?aZ*a{#43=wZhdrozunhiw4_F|Y8Ri}r@ns_nAEV8%nGj5+Y3pRaV#Tt zbr@@{q^hNu8*dqXHvY<-(Zor{0#&a87zf-g=yUUy5?!MgJ>p`fB(Ku$0bIPJ3B>ie}`uKFUKDxcVyL|n2xZiOZsTR~xYhjJ? zUar#sRG>mCsX}a~a&7l55N<~Zh&or3+P18da+~aX(_EoW{M3g!G+P&d)c}EK2QW1Q z!~j7`)AJf?hSYL-Bo3+z*o0_d3XClqL_&QLYJ(~2Ekp&Dh@wFCG6VH-Eef56UfD#` zXmoFPVg3w3k#bV9N_2HPynxIPCxt{8VyjLRWgA>W!U1v!R#HGBJM5#@OHemD=9cLh~BrPzXkB zXvhULbW!BrGE?Mo1X3f8yao%r*rg422X`JAivk5%_=m{UyceL)7Kgn)3ISx{P=2z& zq_(YWR6-ahwZTCm533WyF1hPyTjhIYu9;_HGY%~*Oh9z#@WRe_R7Jq-2-e+i7u4~M_~;G_ShwH3I}tif@}q`}*f4~;R) zi;As{J<&iB>Wr$)=JvW+c(%p#{&w|-OEBT=$-IYCts$W8jiHhnY3euXTS#s#)JfOy zC^b)T9mWO&?YbJE#MMeOcJXB!?T;&VMf$bBk^b77(46o9!c1hTdrm?SroYG82sk^H z*%AUs0xGH`;w}D2wW^W|gRDeYBPk{*kQNZIf}8mn=UB{kvl`8hDb$M>&qf`vrHRrE zE^=fSS*{W(QKC??Xe!M?QTWMGW7-5FTmV;T08DHJ>IG#}fXAH(b>~MSECr}C(W1{? zPF9g@*Q`~os@$-zsz~)rH4!-sWAVlq*6dlMoHOvNMj;@P_tcoe6mi3e=kX?0D6u*; z^nPb*9Zh`DlNS>Q%>wpzHAtf_E4wBlI*(SMCcbgE&tu)hsmTWnLqz=cY zc*k^3C;q6_+-oN=KLNh6j-Ni_*|oEDsV5Q`Qm-iSc8s0@S(6sF4X1|Wc1!nyfv>bgTc z&oSI4OqwJZ6WceFtf+QY9Sv|K%86KZb$>W~zPrD@dOO_TJ$wHP$H&L{%U@E}7pc4a zfzWD>Z~D#VW#Ai%3;OO06X&!zdLZTD%aFbLv;i!+uu} zha{p>h@2rdDRTNFcp`1CX@E7i0n`55HYff~s#>aTC#%2=t`Xe|Oc?9-25fA;x!yVFC#Sa_(JS(T(}DK+H;B4UW>@_6;=>Eo{JFD~9(zrMmy zQW-G}>NA@wp-L^}R@@5fE3}g3iJ5pHs1#q+hW`RKH|hrD^Owl-a^-=FGlK;QjwMX7 zDETO7jxAD!suUHg!ilavAS6J}8x>wW;wljT(Q{CM87W$Y{WG z0IJ0(39;sNmTxcBCY5+aibkHNZ&R(7pD%7RBi7t@javT}RnI6;n}8|tK*0{o^a4CC z;um!kG)Kj*a7+YEoMifILv#=08jWDKLkF?x2ZkvaKdt z0_XlB0^ri=V96(c5Gi>#%qIWsc|(W|5t3hn8XlQWBO&zbbi>YzgvO1?n)$0O#L-?K z1THxoGDq8f_IRnPRN}0no+n$YP(&Q9HBe)6nQUG+Ir7#tSBR7NI|rad5YJpT%Na#N ziC{zyqBIBL_PAJ6?1V`=d{7GluTP&k!;q6`keMJ~Vmq#uOQ)(x#ECg2=EP0|dETCd zjKxx;Q7h+^D4P!ez}iZbQif8*>OhSbjjA#cU5vSuDoQ>SO0>@Pajk}4?BH8XLNuca z8FvK)crRzI0uTwfl@eJ>f+(uOe34q*b4LrsPLK(+6{7IjCVcBh!`1hUrf8cJPiiv4 z(Pfs}sSHVWf%lKJkJl!Q=Gg&Oz@VvADauk#Qsyub9_X-{O&74a{W5C3ex=gXB+70I8cif|7tCM0wSpJb6T! zhvASxQMw95z+K7CU>pDififra-Ceyg8{Ff}mC!F1>+>^WzQ4Y$AQIIo;3OhtEW>t> z{aEhq^)M!8RWaB~tcgiQ04)1%+-`@vdnqGng;o}^CUoLZ85A(brhJnsv=X=;4u`w@ zqbKK^_44|#RieIMY*t6B<@&JS4iW`f=Pgt2^o=AoN# z8o`FEY=I@(skJs3fI{H?X^4o((>W%z>kX9IeKtEP4&mGbQD~Hkfx=K3ZyuFn_HmxE zs+d3;aboKq4^~lPuMvbJT{~zpBOL8n9mKRvG;QDj+0~&3UtO;fSyn`sbIz&OG7Rnt ziWDYNsb<5XxfK9oy~0VRNez&vMj(83m*`oxvTMHP#1N4}jFv$hvS5Fr#52%80FGST zXujK;^39Yig+t{QBhIZWZ!{~?j5|WRlFLqzc@~XBq!|GSiHiDzcJ`-q`|wX5=zEK- zxh-RY=PPpa2;;Q`^#)T2XUYJPg=^F+5|SlH6G!V+4*=R@I6R_uyWWElN?s9W$by(* zX^Y9*jGK++lmP2oM$Hlvdn+D}mzhu-2yh^;#R>VUY9;24+4qKZ{v#p)=wPuq;Vnsm zCgEb@WIdx|dtDmZpb6ulJnW4SZ;J?6?TSPuiu2}<8N7cJ8iEI*b;WJ?J0yG-9rZte zB5;bC6_2-sQ4Edm=4%e6;h_q93%SWL8|viRuTk)U!s2K+0iX!q*NT9rgJd7x2m^}W z(!-ay_)RW(TSw9sz-uBsTbsGWJ99+TKFFRTR6q+g#b^f-f|D$f8$xjojf}TDSCv3E zwYrllz%gnojzWrL4!U}n!shk~s6k1HL&-`I(kD-z3Y zdVlKp?(*t-jbek^GJ3sQ737?9^7cPUl$g9McJB6{;+vQj5w+7)cAXDoH!W_^dQB9>f=jo8g2; z=BcUM%23YAP!VAZAxZ6s>7e&7_MhC}*WLF0uY7fLes+9v+^;$$QA_Y}++H2n#KKK$ zs}?GckbQ`Xf*4GeI3<-6L8aC*4kG}7RgiOL!tU-?#xX&w&PR7maBA}5Gzb!!iIz#B z!G5ReNG&-yQ-8ERee!r12dQPX= zEmq4_9mjFEgF>thQz4}e+dZIVyT>?c?j%_p99l(11k9N?N2|-XZ|Y(1F&@u|Z4Pr7 z1$$l#cZ)@Iwe0TpH~r!M{OI}F(YUr|LCjq(q{xJ>R=j?7@#%-3++E*rkwjD!!){1K zAQC7iA_e6<)}F})@<-g+Q#sU?h!6=;_Hp)AY$6+ZUX2-2L3rchD5~A|3}D+Q1@wRkr~QsSgH21R zf{qhIi7M$x{wi_j$BEKdA`KHy0Z+m#+97WG`oz6IhuWf5>E}}eRY}*jqBM0vQ3k;P=ft&o(-zuR7He18U;=G z)rloiAvK0lBN*W#>+U5eW{CjFR6VDp?I96m0|0RjDng(MA=9jEo@WEBcAqJ+4e&uT zH->xtamn-Un@usTWcEa>bMsnCDgMynPA+UbxwW_QUJOPXHXt;aPUCzhw5ZlC!wrXq zE9^Lm6tl>fr@xw4&*TlgLidJDx%jse0!s{0wU}Xp&kkx@pU8HPhV@*6qV)#NjLPh_ z%}IO+n5Qd+Ep){mZ=PSHp=o1~9$ePtQ72LJcTB<@FvMdC{(|i9@aR-5Iw8V>b7V3l ztF?$;L-76z6#^`=v;q0HrR{0r&`z$uwYHcZGDJ(MO(|$6yw#_gm{Hd(nFNjf zwufS7iHRdUQDA)Dgk#wWKWjK;rj#r#*p;rjrP2u=Fp=lDP8o2j3Q_OmV}hgvr6|8p zQm;#hBhFEU%`VGf^T+OK=ay(}`%3}m@?}t4X$$@@A=Fna)tqqO=Ah7Yr&Js^K zKT47F+qw-n%i$|#6^BDDP#w+PA}x#p9Z3c77D5U@T9ln!CM_+Fpx`PD?i38L_}oN_ zb<*Hq%qg`!izS1Wab#=Htc97hC<|xeI^up9KQB`E!}AY5xZmE?-96QjShQ9^s*3lm zRhMT13#?av+&2aEcVUFvQV|cGF-Z}vMQbs|DWH^lk#g8=rHri7tfq@I*{AZ-MY z(byAr`#AC`>|ThFn38fr-z|@ij?Yiuyn0c_vRSPu`GMm$-qI9++A-CjFqm@6eOIJN zkqT(7oRg)zj$_&Fw#)Tub9Qondv`dDOq9D`AcQJ1GBKoDD>+V#OnsMSKd5S6bQQE% z4LR`u_Uj=<&(%W_`6k><{Job|-kdTKD_? zaC?7u@#@vZi#Ox_9;1xAQAIeVT1Bd4=9JP{YN=z_^`~dY=Z_w35BpayU*26^r79>z zO373J>p&F02`I&K8`MHTGG+AIur_U4xZ(Bo4~^_OKWOo2aEHICl{kNa3&{2~Qb*uz z)U7L*D;9OD3ilS&yQ{%^j1nNQGcFSm2AeGgkf2UbBh;6N&UD&0`j_AiE- zTMPFU7*Q|*KVLNAe+?_x9;=yZsP_VL0)+ycdPt$s@S!a+!3dpwTs(cKerIlI>YXV8 z%M4f2gG<^UyvtF$G&LMKQ_n`UDKnWy4Q?hrRh#B$D;KRrjcYfE&ldavSuU6-n=vPV z%m5H)^V`YEe&N~GxXrABQZ(G=XecDc$4f0zrPdG{vmXs*K>_s%plY^#S``A>%-xWK z<{a5SOj4#HVB!^r4KJxm3=?fW2?VXOs~K+Bh9=Xh-rpi}a?}h=3*XzDOnhN+6Zc0# zwR<_ATiSS>D2P{SkKliI)bikqXp+{pn?W(_?8P&mr2S@gpKt_Bs;!%ok}+PSL?l(X zC19w3$|OB;1_6a91iEI+fq?>dcwT^b!K!+js%fo*;+^p7JcXLvDhm1R^yw}rZXljG z-fuB0ZceUml?CpJVk6-Wq4yAVl%OJtExiows~tr0mf=Gkl5ry zDr{8^u38a-=T(RXI3Oi4Dl&|#v~!f0MYNQ$2n-sMabF@zBtTUeOZBs+9tekM%9O|) z5{93F2c{~{m$@5EiezON&h8Qe-X)mZd3D>+oL)lv0Z=ffmf1qC%Uq-RN6pD1w^T4I z&QZz51RvZ>fK~{xwe+?_<_wHqht2T2#ZYM`E-Z0a2zXtIF&r&GL;+>&VED9JA!K_r zYl*H}3wP-CWRq9AuNUQh!l>u5UK&@I?tkXY86H!V?RLkM=^7GyP^3|uK zl>Ox;jeFFhQqe|0x`W|u=WLZdRz5L=6n4zRbWmr$2_0Z!SxL2srp02lUdmW?EF61a zJ>z%^4@RA@x!S}rE8T6nYd4?4N1~RVOT>(vXqAtjoabe}-`?-<_Gyt)?kFYFLQrPX zYM!NOK+DwwaT-MXoV(QmTK0E4ErqSlM;RbuXPpHdlz+sijLz*QnRN0Y7!|Jj$bf=R12lO-7r zsq1dvyj>h09l!tXuF7t=fA!|#^Ve^lJU(aA{cd~x_NMNKaX09Gl*34>_IA#!q6ev! zwOg*vA3Zu+FK=&dUcY*M*zR}`R$&oV(OR^jL$jq?No@p=xX@Zxg3w(J{HyDZMzI>^ z@JtOd$`nXI)k@AbsDnE|VZ_j6>D1bp$ACRIlmdW6E5QJ9G*dzJs5K%Kuo?gxObPd4 zEvV&+pTXv95ZhbSZh&lNIijP3_ixaKamgC-Ot5mIsq@Ti zoJNqbPITDE5rL>uxL?+@ zKO$B2*7Z^|{0>~NG&Jiw6zxBnArI()uuo0S0)Ijy zI0O*caHu$9#)Cw=4YS2ciHM?;SoFC}i4uctE~t&EVmRi3_wkkjSX*HjLn_hiNkvGj z({SPE7!(wZ3|}UQ1~aT)ucA(FxR|Ns|5$^B@B?ixM3~s8Svk8L&E~DvLKSIhg8>jz zLTjc)&DvO|o6DbuLur`Y$zZUEh*plyRZbV$W@|(06gnWtTidZVTBU`>W*ev>=7Adx zoGHR}quIVoQx+iqr|HdBYchEj&EIYMY;p@U&PV$VLT*)Uyn_S_NDpKV4}1**tWwSd zIZtN+El)7ylj1l;4SHLHFlRtC)C)jQ8p#OZ6&TxQw`kXlw#L&Lpim@!sEJu2?8#=9 zBI|$fqwGf45NDJrP+J=H)e}*$k|4b#9Og08j8RG3T{qRwc?V-J$!m;dFkfMfa z!89z_*Uy~#S!s*I3Y!=ic+oYKi%dL+*C=t=o&H|3@Mg3dqc0 z%FHQQI#4)nBG!9>iF3*@0v_7+9a)UqoD)^fNtJ{(0LtHHB+flDt0K9R<-}Yk_Jqrx2bg1LFoM_|eH)Sq8ZEbbCMq zus;cThbU9K6UXc`8>Z8&YGILBfoLk>i$f9o4cD3)NNS(y(G_c#D5OeMeO=sh@eyb= z50w}VV~&-uQm5R3d!&TaCGI&dQ@>8#D)+10FFAJt*zdRZWxp%?t&RsN z0{~#;WEcig3#+I~VM(N5Wk7~HjQjolrKr|%Nap9j^O~R%H8K=ryseZFB{$0LAuRXR z+Ii{JUP`Leanx!!Mp8~G<->lfKJzFnVw2HETY6B%IZ1>;wNoB_Ki&7_l$6*m)U6gL zk52c)u;1;rw>#NUmls{mGLCk;2~;XLWz!5`VJ1op<)I3MR>!B5``z_T*&h-Uw8~*b zpRBYQM5*sm-zTKLTS&K9uGgne9;dFm+1~GlffMB}cZ)uCxl}2oib%J}+nby1^{p0B z&*0Ng>b{PS{>6Jh`aY0Yf1x@a^nQDDaj|*wXqEHzc6&IK)TK9XF8dC{Zigxak|HvS zDs^Ci5U~KG7H~Q_KY8@{@i+``-@duKygJz=w&H6=+Ar=kp_Dn!+eq$gl`_gq90kU*8eB(U)<1R#b&B(`x7qGAyVQ%bdkzd%+p z0apszmLN1~tK^yBju-YjrZat#6cvr>o(KCUR#a7$Xf|)GdEsvNLw`3&;N3agB3Qh| z1g;?*SK!v;#_X4651Hu<+MjF;v%j%*631Mn2QO{8DIg|Mf#Hv4x7_0C2(Vc(&*uI+1BzK$;ccHdE2qEzch7auj7q(ZM z9Vcek1Z5UeYlcG2-S;C)V%4h9h$^UtVM2}%Amc4ms!6b`neb9H9*EhRf;rlsxkt_x zP;B`0;M0>ksPNhr729~yNoV@8rqB;=uG@k0a6?nu>dJ}2w+=F*-9IAgT5BO>n}ejD zdCXv8iqb0wd)jzJZH{uSRm|&7fD*^FCRGI|LQCT@4}dvh3jiU-a4uU34w()CXsN>u z9{Q7qOA-ns9uY)|C}qxF&Ry=hT*kr_4pY>W5m91_k?%-}7K<(=&MB)Rr6Sgi4hkj} zvCeD+%Xvs#tTBc4$_1b_Q|=zOQ^!o0elLM4tv8b@ftV5zaVk;;C{?CD)7E*&*(OsC zF{;*Uxxk?tEb17$-+qXN)>uS@{~ogJ2zK%`<9*J;L~JDl)=X2ZVh{mD)`-M&1tZCG<^>}5ZVE__Uuk;T_sYrRAz}p4I!0`=ojHssBxd_ zvH|N2XZnNH1I*?Zg->N7p|Do#ZJAV~J%noCF9@{1y{(4>+ssABNj4u(W)WB4lo>fd z@w@OZ$+84kRLDSK$H$LOPtK2TFYoJEWvsiyxX8Kh7Ki;lHr!?vu$}5ICnaI#ocrzm zup7rs-=7|>rO38a*EVDm!c!gP=H_m@-P@4STD4H6QU;YpCLyiR-n(ok5K1jdN(r0e z_4a-{?#D12=3EJR+YmuCcO;sysY#@{PgII*_q&U$>sN2O^RuL^A{}+RoBLfEP>T+u z?n}=Zi)FQ9JSt>QcmC$xq+EdCdw8eLdimx{6p*kqw-Sr?5BxsM+TR9({E{ z)h`)rBO|w7ES-|O_xOrAI{4uGmhJMlTOiKZp zJ_Qfl6g8c#P=v9|>fqkmQn>19Ua3D7O&(7B?7-j(t1%B}-YdD<(yPCWwyxxk;Ladv1veNN-;XaR8A6R(v-5Ql&V(9FB0tlp*}vZ8hr9XDpj%M zqL#7`B`exfRc5szg+?fpNbxks$ZlON6;UlZVp`i`zcBu}1%6H85h5qi4jj!!_{GzqVntvsAg*nFmO1GJEX|8$;kni6(n##>j5bF5+_>T~UxBQ77{~m3D$m zaj+*ek7j%A;ledRV-v#U*tHygktZgQMWx*i1%M&fo#cm8DaaUhEoUizqy&ak@|oGt zmM@VKl%2OGQexEvB&AN!bJ}pXNy`l{R>&Q9eM-6O7AYqpCJQ}l6)h@^#6($?I8n-J zxn3_eYtZR9p59*{-CZuWR~Kb_Pvh7VC8S!T+or8etssy}im0xt)My;x3m2IMu1N@> z6#k4(@-ef~L3pS~x+A`0ZSfY%1u>0>J!)-8-*gwCp)hB}rxUrb9WNSw*WVz!CLDI^y+AJ@x zFJ-I{fmD@BDyr3bCV7s4dLC+{uhEKSrJ@wjSnqc?pS?U%S{xmZweGez<#0$+S>$kY z-zj($GyA(?4@kdWJU)H2J~|%8@zu*0cW>U-?G9z6GA7Gm)dH0&BSVeGR;dJUULP3W zI-SIsi(ZEl%GdUPXeQ3MIZros&}G`#bTymY`9-abt0x?jM0K~70T z6bu2u`V>=@+QhI*VL+=8GqHUVi@-{|tCo~{4>eeAj&&LZz{H$X5_u+x28C+0cZ$A1 zAb^txtI5k2Y?$gqS7B1Cwjyp-A9O1)O>UY|B=GzUZD^dQAh0HIs{wHhT=;w_n<@}` zJ6w@q%{6>%rN9lQ4b{RU4o>6bnZ5IIyB66Fv~S?qK~jJP_O~+tcBtzBC$JE)*Tgvj zp+K)qdjo0)6x4jP_Gk4Ve_KI{93HqGnxRS!FDUGf1DRMh$9)RREy#+LQdT5zCrHYj zcqdSkdLgc)1(iiAF?*s4`LG^DEhNM$H#||+nkVMu>@PD>5^{uM&TsYG;pXiCP~v1w zINW8RSP&Z~U~`V6(Y`N7a(Bz%q31*dzuPRLszVq{@6@Ka4ZtfrnOp(lWKF0X1q6O+ z=G6THb%a8ISg24DmCB7caLJ+&{P>6ovU@vfwio+oO}#T&XNQPk>`DTvjr%1q<}mwB*CLyX!b{Ce8lr%6<1 zUrlE?ekOXjv<{-(+Yj6we{U{zd{C@>K!FI-Ag=6Wl!=^JNu}>$yx$YYreon!8=`Ih znuWq*J)%kX4A=W{3!@t&SHg=l7c&Mvv0)TMHi|ZyC4}7QL|bQfU>F=L0EarT_#amV zz!fQ_h47NoQCe(Lw@&>MDJw(DD0Lk7TL4(k}w=IH6;o6A=>uV3vhuIhfT!^kPYnvQF=?rhdK&eEv8ajjO%KGP~re7RZ=W<`Vh zS8APu&78XHIq?XFp}fDIaaya@GLdQ-v{rLor!bTgOTIrLc=n<8l4zqH`p??_5R)>Y z%i5=Yvwr&Sb5S|$_fiWK0FPtMociTj4ue!xs>Eaoj};<>&B<}?^6vg{yxJ&rhy8FU zWwBm&M|%;G0t*vL5mxFK%dy`L2g@Ac+;@xRadx|lh__d_<9@$jD&v?$%Hc2$yE2Xy z+HaPp$0ui}CtCIT;^N}XoBiD_9S$r)Dq063%U4#BqAC^E^T%^t0!y2PpLu%W>4JRj ztsmp2Jeso)XQqOTiBs2J5$DD!9!uha2La3}qE_i8Rvu-!Z%PVIEKZ(V>uRW2rn0w= zkRpkdIH{`lUSz5e=Fa>OCL%@^X=4PunI1=H(k|t+ABi*oX@ho?NSz5^}c&1O4I9I z60L&30+7T?OeCS-LB!$?VO&zfEHqGN7K>OpaY{%XQU}UNoe(Sc+Vxnhy48|W>bfLV zwUm0;m;DaIfux|0DixY+^TYy}oS*>Rb0u%<=MX9+25ptSNpL>a5 zFO~K8npo;gv_ekJ7yb1W_TZT6WEh-FYE%csdzxHMj{|p8-3wH;3Ua@p=KE=j1*$g6 z#2zSob9Xc>Wf(H0l~Wi*KU)UVNu$TMHpj%)fSQ#K^zTYZRkhma^>7Z%IyhyZ1aO9; ziXxFx0xuT>G(Znat*ZI;JJ z>$9_?)8nJ3C+E+fUO#>3?dP9vU%n|fy9ML+`KZ~!ET4-JzdP~+FQ z=~oTqKoJ6(p*mX<({HiaEtj>=xzDu>QU=l}gLb88_|IBBPSD>_lq)OpW%s``z(kpm zm^cY1$=%7LN2~Sv?VF43-EBDxWh^9mD5G}VuNHM&AEZhdp$bvTtW~$$L+XdePo7@o z+hIEjNR`Skt~RUGFL*i9NC|AE_9>;svOn})$p=eQ?3c^UX1(ZpqRh?ZwWd~)g=Ol* zVZDkf>V!Gn7C0!=2%7l;nqDYvQG74-Iuv(K933bb18f_r4LPKGaSj{ZKCdqkU-f)l?gVSFiZ1T}2s!3{!ZnNAFULG_ zV~wSmEC(A0Ij2o296MHlmoNGm&SDMHp}c!3161 zR5m0Sz$KJi2r7Z39d(0;)EecP37r%QIg>lN7Y{&3d!eyZlMmp3b7(t(?&;0VqT6o* z9GM=D!rAtDlQV$a?6LsSGRe6TV}$2e)Og?42!|*>q#3}L$J2mIY?r|k2S3e&@)u4y zAI%w{8Gjdd1(0_?VDAZKTQh@(q34MpDha8Y}?dngSShI zNR7!9P9J*wLVUnvdrT7Rh|5YTRTWmSTC~mXoQz~6cy!Wbfz)~`DUc$Wh+1GH5(WZU zx}dUpak0_pmJmqO0$NLNGgOrT(V}4HoHBKN$3){;M7c^~HdybHTEI(w7^t9@3~XNwd?6(<--9)f|xd(U{zp$R?N_y?_%mKLc*n{0c1` zGEpfLVx`%q!LS$*Zh{jhes$AOWQl*ict)c(1Z3b~BEgg_0n~QURDzl|mgjiYddry)${N)mg8A5xZ_@@DNv_cvI?nYPQ#&W?{=G0dGz?%6)yIdcezWb zq{TuY38X3IQe@Z<<1lok7tt!p8LBF^N|D{|RtR+{Qbia@loLx8sRF^G@1@ANJ4me- zD+E+YL@WiU&Us&ucau~V0*j7BS#M8%-Cfz<^DwX!(yDa?lt^ucm*p*o>+A%Np#lFHfcGnk3_#Oi;TR(B+bMg$e%@di;vj!^^nHMj`xTo3C8rLMZou0;~oWZRl=fmjWKsZDFXWE(*Y%HU3@ ztJG1qO3O`me0Kcw>Dlw=XV0H3kB+F%l-S1TLh9jWyLohV^YZ2O%NOJA6~;Z4gB}hH zsMN%$wWORDeJ`rxSePm0lsKzO84EyCD}lJ@E_WTJF6H~%+j2Opk57P0W5`Yn#UE@r{^%1i~4 z9B^BQ0J}l!_3O41UJt+`L7%GyQOv0bxis`m6g}y6en8^xEnceXO+a0AW0K*+uUJ2~ z3e?bT3%|_(e&97H63G8e)5T&BsJS@9zwJCgx&YV%1cwGch7f2P?HqDPMSx z_O|_KR^PVDy0%!8ylTs(FnntoIOUvD8uy1f7Md;{3Pvb&*z_L+&Y6sc?G5?t1QE@N zDQD^W`~{kz{=E&3n+ z@IStO`}Vze-}~loe)Hz`>h+tKzyEuGXBZBLaY&p}qKmiJAAa`n)y>s!f9qRc|KKbC z;Rk>Emw)ru0s=U9JPsoP#Q2S`fAydKXKmPeol`>JviPxLOes@1eq09tM zNMkLV&Bmr_i;POFWG=ob2ofTvN>!2~aU#NDC^m_`G2+hn1i#+&6%kcW+aa}?8H(WE z$mhq2HX55kf0%wz5h2yomXvgc92(h06Q+*ua2np-Y)5Zss%e7Ys+N~-vtZrIff9Mw zOh7>_S~*!aQ3@r&W-KgJp26x}2NlG=e%i1TngKC{=bAFihZ_d~p2}iA1ljyVRn;g- zfOT8*+l-}B0>uEHIATOZY(o(VoWKbwfs=BVmYe?c(doPIo`2!}(|4XMj*ilLsfn%e zSMI4xiK-kO9Y1;Y^y2I^E%L=D7_P5XDsdsvoG2wC#d6tw{p(+c%CA2BFfo7oo8JQA z=O6y+czt?%d%NG>Jvl$8o*zGb{2O2Sjf>ZB{@4HI|Gio-fA9Oh{piWrFMsv(4}bab z-ThFDb{)O@-aFs>t#2>8{_gtvmmhsp2vY0I7q7;{m~+2bZ&J>x^=5lhN@3>HljF_N z=KAX5U;VfL^56b{{^(a9{c5pXjf%U>D} zhd=$npI+bIQ}Q|6N1NkCw_0{9X6bWgYqOQOij*oUMYT}sODQU9y*(14q7Yl4Ox4nc z$PxrvO3nPs(@1B4BA~=914p68%Z2-AFPg!23Wye$vADtf6Ls<+!quJbBKqFBkW&NB zgelBsAd(^iW=B4orJt2mK!Hrm(Pbdg3gI)&i5e~JKf+<0Griu?f3_T!uw&*|F`YdH z*{4KptH;dk5Lnd4fkO*KM3SRjAe;m!%sO&nJAw8mQDGvH$~FfZ3GiW&MkhFcWmx0t zIg{P0DFxOCNG!ciM3`COGf)*IK&_;bRFzWzJ=F?#!4x1HtlfEQ0ngQOR@|e>hVN+3 zT3h%p`Pcwgcd7tHNA`+)we;MmJZ^|qqxua*%A70|BvPfawGm4>bp#pE@HXu&c zQjDx#LZo^Y9jp_x5Jft*PEgqNv=#Jw1i^S5_RL#nrX!bZdZTZ8bArk2pI>$ z4IIrWB+Fl>W~|z|AQvr*W&l8$4vn7x4N`nF+fa;O4c)O~RaO%Dm z;xNa><2MEpyHvMFDZlR+441!YD^#rdrJpvr1NF zb697HW^zTEQVKu1j72noh%)Dt6Oq(1j@w#_)IvHim8>J7Ku3sJYO%KNgB0#;yDla0 z$SIQ+m67Vc9@1fVd$_&a-dx;1f9Jgq-h2Me)9ss=Z$JLf)<~o}<*91UH=3*@FjdP3u~>K8B2jD9llqQx8q28FM%CI8RD`ZzN}@HMEojt; zn}okk#0+NR6H{U^D-kdH<42FU%e&paj&<1W%Wg+yL>Ur9r4FSKr*5%aot|tVdf3&` z{Al5nrPjAs7iZ7E{_IQdUtRrJiH|m$%eSw!NY{6q6LN-BTJ*eJ&~{Hb*_>xUYc0Fo zF6XS(+X9aJLrz(XXobZS5BpsmM}~+yT`;l1l!omZpcOa+i1uK@WcH;flv%ZaMO9>1 z#>4eZw_Z?}YpLZhjE9391|3I~F{`MIY_;R0S}og0$VXp^dghV(NL8&6J0{9O)Tyfl zh5Mg@?zK$-GkpXjt32?i)+!*f;fh1Z0>+A3mjscz==(qTz2E!nvro47_uu`- zH{N;Y>Fduw+i$n;zVqxK{)0dG(U1P}<6r&F?|kQ5>-FL<|NJj=*FAgZ3!C-vzxm(( z*B|`J%cqadzx|DG{n?-Y1woMV7r*%aSHJN7DG>o$K`pnIyyTuWYyr@YkFg!D*&5=Xw3ay}Henu~Lds0Z zSZ7nwWWB{xuzl|In%Ut9{71o^sV(-tTg}uokY;+*w04f9Gil1%OpX7RRLsjzPi|$h zi49C1YKM1MfYotxm^Zr@YZ?Xuu`{bF31#gTM~|OA{o2>g-g|#_e%2pvz%&fQ{;(g1 zv6Mmz2CbHh)05NHdh`BQeq*&cQo)PeUJkpQGmT>clI5_K@}>9RJv}-8+28!tlV?x= z=|BGauU^0S_*Xys!{7bwAN=W`EY6Sr?)SfceSdYo-II*J`sL4<^z?M|pZ}wO_|6xe z{P;gT{o%>UU;OZ=w|Co|`O)LY-}>ga&W<-PKl$u0fB2V+qs<3j{NhKy_*EIll1u7P zcIEu>$@Am$H*YQp{Jrn}*7@1VkN)CEt8SrMk5;Si{MPq=@ylOaUtQZsBM?a~N5?0* z?=RoJDKdWj>tB8Q=HlY*#cH+u&98s;{_5^O{@~A-DRbX%*8PhYpN>P(TEG0k2anE9 zj*r&6`@@S@uV21;_04bn=JR)+kK^|Fv*&;OgCG3lr$0}eYqj2xZA>^JY-7yGi|9?H zdKz}oAQsz~VHuYWho^?=Nv%-L8um}d8yeVdPCOtGee*Y+3q`x+ghK#rfAurtlsg5C zLEMc)c7rlj1K@WV-X`M30mntd&GGy3#iDrNhMy3*e{ti~o-~{Ua{tI8GNw7)zmI44 z*nKE&_^ZO=l0!)WvsUBP;6tv(=1y^xswi2)Cal$YRZv0#iI7j>BY4B0ck6z`7aP$mBk%y1a zu&l!S_fzY%6Edk`AU%#8-i@le)|d<`sZ0*x%(a~#k>($fn{5YR{zntsAke9umI=X5 z_uVi`+*62t@P96Ho^TfyVu~O54fQ_IT-Et9;ygGElMqB{Zgj$)fSl|&}rfXg+jcOrc|j11uMo{RBKTk z358e^tV>K$Ra6^42_hB=mW|qlL8z*XT4K6hW=bfX?8^P;!+5{b!xry98@Ah829=St zYOSOq@VQ70B1FQZs)|+b1;OFarRk#ZxI$5dYo{$E!eU=cMuJ0;x=x`|i?td8gv<-2 z2V3c~E=QtlSZ zrPR${L}XyV+Vzm&u-#wWUcdkT`^T?dmAkrJF1y4dla7VDd~|ZuDHfaKe!1M=ANIF< zN}Q1_7d__$@)<#-T88n!2L^GMkdQL(?+;o=tz{ye)TLH@?ZS68*l&>}KQyFL*dZAp z%CNbNBBj*bVZ3D}QmH5vT3HoRQHy$cM>VVu6=-FMV(Ru2kwoiB;>A~S=4~gVa(c0#gGNzO`Q(or%VQ(EA`n)l@$r%fI~dcOIWTdHUqf z{`#->!!A<>a!Q$qyRL)kI1Uhf?~BjB`#azH`LBL1Bt#Vil92#HZw;TxlU+3Il23VU z;w_vTaa65_8nzd?p{U4VIWXSOo>*T z&Dr^*_rLJPcb>ho`mJ=7`O}~Mq}*Og8EdH|U{WaulJUv;NtbYba&)vvS18AQe(#-g zO2a$vy>ohgvfJPP+kgK@xBL5d-+lM*{=xS@{rKbDC9U-v-}ue5r%zwKz5U70Kg!)= z9P8`XFMssIkAL}#4{z>n{_B7A55D`|Z~x$r|M=|W_#5B)&BssAKmP3V#b)u*sy8OMr^W8TuUh_D7`Tci(^Xngc`D?#%d~(_^7R%-FkAL!$ zi#IR-@jv*_o<2GI=}&(0jjw%cKOX+|fB&!FfA{IPzWvQ#efo?4^1uFPKmX;g@HapA zCcNV4U!-A+w#!z_Boy&Kb$|2!^G<-^Z(mpAXaws`VbYp% z7NaQINj3Cf=Tu#KMSV;hDWEo11{C?mX0rkeX(&ZBm=p)rKHAGb$&o$Jkkx}=YXeC^ zxB{Mj5OKifWVa(0br|t$BXQjaP@7-j%pN6>z(&Xj2h6caxW*1gPx^SJYbLTRd00Y{ zC-$|2M=?d(aBFoFGueB0n3#!^8C5H~MOZ{j!DC~hsG^o}JRBM~Nj~b#sR;6VQ6@|8 zPt24!F)?#at|P+%u+=x*CbdfST4ji(PkNeDcw_)^Rj{InCL3s`01`1M5i3=YTEHs- zRjO3yB7Dk<*;QCS3npJb2Qy6G21GoT>bX5uZ3Y6hy>#^NQWPjG`q0rN0AbZVab&`D zjRJDi28zvygP;mZ6EX`r$r-+OqRf3a-zKErRn*yl9^fY8`&rMjXU!9R-1HF`D0N zpeLqY5qDixDYYm>v^pema6eg!;FV23hHnB(DGDFjAr6C*a!QN-{K?bR(ec~M%k^S0 z9*WdTsw!hr(ORIYRf_VKbaQ^PI6fA|PH(9SE2D}WM%f+iudWYIo~@3LFK%D=>pm?y zy|L13t-~lpQbiTSDdh|haaPWiQ_e|6x%07rL^Rf#Qc@;jsL3|Z=XCqee&qE42S(;5CM<~^x3DME(QPH zKmWh{;qU$K=6LzdZ+y95@$df5cOE@|q+S2lfer+6RxHKmP3cv$t>R>H27Ykej<*Mcv*X5-?U#E&Kg0XHJPTq3_ev zr)P_fU%q&~-4&wF*!?Qi1zC0Sh`jEFT!XvgZF$);<27ao%8^3ArT|wn)k)3j&qx+O z-`Y;u15N7tLUHIk(SDkzOYNLSYrgjX@d~$H03o%8e54BRW@CmyftYtc-Ly7tC?rZ- z60HWG83#elIcec<5XIaFGtc1y#FY@3Y(h&g6KTN`X6uv*X6pOAUM@T0i`TDTy?k}? z^5yPsTgM@#1WEwqoI!H+`RkYc$8SFU{QdXefBNY0`J=~IC&$}ix8H4pm>~%2IIcI# z55D+?^G7Fl+v~1Rzwy;Cfuz*&^z4*6JbmYhlAatN{i}cZf8E~Qy!`yr>+2iUqL6pq z{lfEi-~I7VKZ5e^es^FL@3{%3!h_3&T(7ysmFwfw!``~IK*`Jey! z5C7~R{-b|#c6yYkEHYlb`1Ftd-M{-g-~Yqk{r>MfdvbPma{SpRzxu&{_>=E^>vwm% zZP#Tx`pnSkYv1-^(&--U2}szalpYEuCXqn1)4`?9Vpb1@nc9T{vHfl5VullfbDL)}E>JcKJ1VodTM>y`>6olVzsE;Yfeg5!7 zWT{7Xoxm@F>JY%kJ(ve@O9D->1rbzRD`9)LsIq!MP5@$2VMBWghE(NX0yGS^J1a(M zPYYzP5fKrw3z6C!7BHvPYQ0*m`o8aSPANMB&l9aX?e9z@Mo3vEAQ557y)l(`70(QMHB?Y=`;WsYTda77}3Q8gcC{!zW z9CzC{Z!Z#!G#p5a*1?X>L}nOC82Nz>D>(0>{#Z0CxJT<4pk(7G*j;wEdR0ovs%mUR zs)AGQO08CTYGc1NR?8;d!Cf8K;W|kMfgsK@oAa5#%qbyd^j&{+^yt~M{ZMwhAqfxr z1H7;jsFg%aSxY$_?nlkEIX^vl@|ZdAE^oLhp(X$$hO7P6tBdt&^NQc@8JhE`=yRv4 z_qTU@QQ@p9RTZRE3y^YNuhP+m*Gp0zs{$xh`gPAKLxG%ZcA`{!DyLLcEge<}kqxNv z-Q{P_JwZ`}PgR8wWfdiosDrjhnWMsLbsnPR4JoWgh}d)x^{HNB5daIByF-Gxg4OsF zRK0h5LxXB(D|%f#Ji6`;dKChf~g^YPJYv)OcAx7n+s$0eM4zHKRH_V_}%Y+?<==oJ~?^v@h6}DPydhY*T4RokN)9*_9uV(XTSTs-~Z~D zzVtu-_y2mC%G2|cFFt=~b+kTxbo_t+zx}IBbhKRG-|hd+AN_ytZtuSNjc@deg)0Bx z&;F#A()Wwwwoj_{7rRnrwfs8? zNmmUZ?&!ioGu9AR1K0{_R3^GM(1xTMCPAby(u9|qVvSHt9Ii+l@biq8|K$eV=GeH0 zLVzWr!h1Av(C?PxA5FQ{`&*c`adqu3K`0u>G-}K#T6{Qi23+;4h9rVL-pbC>hI2A$ z@Mg-~4m=3_M(o{8ITU0`cOt`fd-eL&?d{!*7jLfLUP>K_&@Xbo>{IHy)YT$o7(~k9 zcK7o3SG$X=%jZwH9!M=`*t)2yRpE|f>HGeJ557=`!`_>*xgpS}G2yWjoo!@iax=pZ7Sqm%Fd{`XPGAO7$^ zefHwB^GBy$w>UdL|M&m?fB*dDXaCKA^Dln(SAX+Y`%7X51251)Sc+5Wq`Pd@ta&6^kXx4-+>Kl;(>qbDb4`J-QadKic8-JRC*#rNO; zXaCiI`Q@*DO_X-SVSB&*(SQ2!Pk!;MZ+`oGx#!fS!#LC`DW%+{)AQ5k&mJEi9gSuG z{_|&xoXX+g#e^Cjryc6x7;Pts;FL#8c}4^D6Y!o%al6=T58aCm!18NvB8`uWBSC2L z;EFB0=r}{_;@xmk2xqtw?PB4ZBJxSXxy=D^$g#33Y=3WnvxfeSMdA4|^CxIo`qS|Y zH^>j0pJDGx+1}Pbe+-Lg3|m{KP&A->0)Q z1`_?k@r;oK8WtoZfSHeuH|J;P>-9QuO4(;!G58_H8n;a}X$D2Df=FDjmIorLUM4G5 zg4wtK8)}hKOR-pBV(z*=r3|saw5n9CrH%*bauKanyqk|%aS}lpix43VsmnD*9|Q&L zwWUg}wbm*Ec!xh_N;Y5Eo|~$ACk-*AIjmenS?q_7$X$HcC*17f?;YA{(QeNN}KD49aeX0}?wJ~yHCg7t|jAs!UtqVN=V6k zdMHv(uowanQR;FpwOT2&K<$SHA7^L~p`BfUNn}vOZI~OMh_iA+%BPRdk4}$&_1Oyu zl|#{DEjt7v3}Fa_m{Wz;TK4zXsZVE*pPrmO){(}Wn`BvpO4ya%)&0rSM}5C2N@>w^ zPKj9sRSI%muh(hO?RHFkPF+gNb$@=cdGcs;c2f3-t5IY~iE#AftnXLboBREtD2h=G zP$f$O)vzh%V+F`|1+8%K?HdiXF+!eUuLM>tL@eUDiW8B9cIm+NJI$CbT}MtZ*HDE zfBf{>qbkGI#qQlNzH@bXu~{w?c(LeK>t)}klcPs}_xFCE`hKYUllA5&KmD6u{_JNT zeDKwZ()Br~REDzX7En11yH%fl_S2ugcysZk55D;3<;(xypZwt4-}&CNr_WAL){oCm zzVn@LfBw-Yi$qTzAD^FZ6hqPB!;e0?xxe}TAAEn2mp}jMPf8sC073u0zWuG=S}xMr z@w31C`@i$~=bwJ}Tfe=(yZG}T{dF12vdiE2+E>2!?eDyJ@#&*y?|k)ZzwyH#{j>s{ zxOxCk8#RVV5%h+zsy^@pAXN|2x{cJpIGzOU6|JP8G{ZFNSZiWL9sr{$p3t}_#A};o zAxGF`sz4pb5x_CkKGKd1<4_C!xs{k401$x@cNp?9b5I_Vy|Y{mo&ABV?=1i#5mmA| z_vU-Fd{?zlv>N%~*+*a(PH2S2WBJ=HF#r{%DlAIFu)n-~B_CGK_xn)<{d%!F-5ftX zKYetTxl710$gn%yUtQf^Uff(<94_9z*vxQuLN-BypFK-FP?AmH(%$De)r@y*@+ z@#g6K$)itSzJTh{$!6FeR!7V4eD@n)`pO4DxxK!+c>DJM_&!M^WOa3U zeS3Fzd%L}%{rA4N2J2$8_~Hle55so3T%VquzW3f2p1%7ICCFh^)qb(!+~v*U?D5$! z3}qaZbg^D8H>>4p$r!suULLJ>w~}&7J&)sIwOr(sj*lPz&ToJ3XtONi!Tfs}MQlXT zq)a9dXjFpP^uWg<4?4Ygt|OW==Mt$VfPV}3|E;fP+RfjXvq_kM?liZ1b|G!&McYZC zK%EXNJfkdLteJuVZNd3C9T7Nl-5w|;+~6>?z6uj zAif>|ffM(A*LB(Q`ME({8~*MXizvXzv_p!VwS@vAUqTUX3L3YjP)Mm#$~cT;t+fn8 zm1@p`)M^PHq&kknZo9KC%i}Ow{iUe7qajMD3T2|?T}Vj-@me8Lsnv3a8idUm31nTm zK%`OG3m|dwzC~&kRyGjFgw7|jfk`XEXZ7_okqaly!6(6IFvkc!KT(e9nIh&8$H6O} zDZ&Lar|#D^xVj|Ai3ptLD*%^I3cn18X56{5CvZUdz1QV~aLj|?LyIMN^f?w*d{R36s8sj!v~r~M zKo~Z59%QB2|9`svtXY;VNe{z5_Z1PDxu-MTTXk#dCK`YM@IeOB0Fa_d28c45iBFR0 zN9ut_n$ctwB%?s01vEgY(Ez%E?yl~t>KgBShCSyHv6g>&aQ78?x~W~a&e>=0%*cqf zeE9rZKDZQnFBlTg!IQz9M~4(Y!ZMR+vx6q6MTy?bSfmtXGlT9fF0Nj_uwmfcofO&bF2{@O%U3Ty z`QCRfuC5;M@1O3Uj*Zjh=H+)j*-X3T)C^B5qnR!9iOp6Ve4d)jeAEQ|8CP`>CaGqb z#qB!?HLu)~bsGo@%*?b)xAXLtX+M4@Ajo3(b@N5MHp90)-EPN^>3NyiY@z4&QJipQ z6S}T5jaK>`GTxrf!mLRVQje$k?&;xg{_Vg1=F>O#4|kg~eE<93efRd;7q4$Nn`xM) z-Nm*Hr8WEa|KUHpzrA17{!jna|8bs|_wVjrzIyhgHZ+!LIG#?ocX#u=TyCeYKL7IO zPyxNWfBM5e{PCO5e(SrR{g9NoHCx(;4|gn||Hps#Z(YB+dH?iarF{MF-OqpVi~r;w z{Db}e^e_J9zj^iM^=Ci)8~0E9^0JsMzxn0QK6~@so13fJmS$B|e)NMMe);+5|KxZ7 z>Hqd0{qI#r)zT`OP*lxoSi+851dOyeKfh6U(PYmMO|;C6?J_cFk}P!6xqHG04MZS$ z6u#Qk{3ds%XMP1m67FNo5jxA8NQ0lyAx_XGOr%IOigweJaHk&wBwZotw|g{_)Ct1K zxP3+%B!EyA5=M$rpbXs>f%ndn{w|pHeCr5O1%BmDvi7y;B&4^NP#WsIJU&^|Rz{ZL z;`Pf{Kltp`cRzXg>FXD-Uy{Xp6PzCRr>7TpUvJxdxc~OCJ{~C?lx?12X4ZUEg{tn4 z$7N~bxV^pK-`qZIFD`dCSNmU|HrvfGl>7aDv)lfSzxg+dO0&8@JfD_&d-r&8b@S@g zn~UAWukL^8gwFGEzqIB#%n{nK34SK9q z$KmFamsa%oc+!jM;luO(a6B&a^I*v?}D=!a{FoK5I_s%K?0PmWUBAU!y+zI%X#71NILVpjK z_H2lch?d&aWESNk)W51^k3xDeU0296iDid0{?G0sGp_G5*BsYnVn-V5znilu`m22ihKd-asE&hfHWSV2Qs>irJRauLV#^|4U}UtV&6rix zAoJnWYO_W&HiOS6@GCd4HFHc!dh2)Z_9>Rl_a5~q>;4-ol^=Ujn zq(cxeqM4azI;qdFiW9etiPT`ZsIg|~paO}Xdz(}7h)|_g6m^o};6hj(GAGT_F6MXjzsID6vpo;Pc%*y5LzbkUN45lbCnX-J2ji zS&k^Ma|QJqJ~Jb0BoVh7y=xgMs(x4>XYXzZq(fL@MbmL?=Se$-Mo5V?f#|jvfqIl> zLMVkY=&;c;LW*ipDGJnjwB;E#D$U(rN`wvGT!+vs0p6pN=s6m?+jI zX%Gsd6*xqE%CAO$55{_idC^kJIQkSZNYNsy#agBF)8fIc30=54770-7G8eq22t`y& zq`Qc!s)<62j^p*~S2wR;KRi6PTBU57wcfRWfHgyFpjoXtmO-eLdRq4HZi@}C-n^05 zuikw-x8+oXZ1wc`{QTj57!>4CBD--cN}{&5SZlSN>I|W7hVgncY^UAj_Tpk%PLmD= zW8pZIi!G@0vXoLbLlKc$7k3mIOpJ&jJwgg}XQ7yp{zuZt3=4X!bQZ6H@Sw=$lqQr& z#Wh2=L-hO~pHM2KALyl}W)w%J3at}kA{e)+|hU;OZU-@krweVk9@IF>RP*^-}9xmj+alS`2EU)lf>=@AtLU-EOnY zRWTTeq8Q{f&!vnfF0Iv9uRo=XG7M!J=i2s%)AM0}ee(i39FNCWZ(e@y)6aI7SAYNS z|NV!D$3Oe?pQ{W-2^F&{ku8}fBs{U6qn6xv6On9C%LMdD7Zcz&m>P7FH4p!hCP<8+ zJFV7Q%#T~;mfx%sB6+sI&&`oK+;BG z5&Ma8p5j`+5kbOvA(X{v(GNbzK145TX7j07Q=X|*pL-ip%rss8Kyb9IsNdrfAHdyH^VrhDvhT<2aXc-rU%&bGo3H2N?2+Fv>ebb5cd@&;nCg5~Ljg{Q z)8pOa+qYl6e(~zJf9rRC`pcid_;~kJ7u%H0ZnqQaFqGZy;_!6bKkiv=*`Maasa4LW z`Qlr~YKkI;<#2-Wv_I9kDa!yI6e=P` zt+m!_MKkg=FGCS)Z9dJr?e^Ed_|?yT@oP~nWvtcv3OLq^z!lNM!vT?R?Ha4C2tMgx zNn4WN^lt-4B=%c>@e!>42%`TlMiSxH`g{6WL4QMDywe>1mh^`L2rL_#x>p~%yhG9g z!sY+3WEZS~K*v&Nfkp%$DY70Uh08=n>O#7jCGwB}4|? z(ue_^Ffg*GSurh6?=KlrSG-ReOIrkFt<|g{yR15qKhH}em)g!EnH1FUT4+p==eU=> z$CVdT`2aq1&=Ppm5umE~19h?Rdig+R=1w(7sH)V~yp+!eeh8#Ryd|z(8q|+tQ`OiL zPr2hq1p@5*o-tp9Bw;TlGRNFF`yTZ!kyv*wSF+>cBrKD+r&2$xo0BW5OQ>Ok-HD^{xIc$$2P@ zkLhZh5API!e}ZO~mt^Z}i5S{iV=2W&Cr_>gLe+sfh)y!@#_h$lxs)=LVKYwCIF!Tl z?fr*u+OYxNRJNt>ql!l1XRAUJHWe6~r-F!&9`c1wsNYXLGJ2Gb07KVO2SwF^e6(;; zZRA+87+;H&L5I;h;ObC3SXeP`uVv2~?kEq<52UMD+6M73g8)=ZffgBxY(~A@z5MhO z-3;%)eQ4FnW@x8sjS}60rFk7lbI;VYjiNfJEXU*H?Yr&8u)V(6%~ub%%UtUS!F)R1 zKaMXhTRrVAx5MBDNW@E+QD#9HQyGdo(wM57P^NUiPh@u`$yv)g|8 zUEGmzP@=J3c*+Qlx>_j>m(`>N1~}Wf8?NmbO$jVUt$5-#_hk z+i@IfUD)jI;qFJj^V_Z9Pyh6%?{DvyYDBSWIuv8Gp6l+);VB7uJQzMBfC#&~^$NIL zAGa`>Bh-zUBqVmOB3s8Aeav&GRFg7X@lC>kgbeJUEv;A5hRJM#6l!dl`yX*%>UUMt zpTk@AN*p(*^$3q2K!7P{>4yjw2U+T#J38)p?q#%NFjB3VMw9S3d(TElosmi}w3Z(r z0cg^VF$75jtaTA7vK&QFbQ0xe8V1q#U%k8iaKFrzR1U*5jW<_Ywu(Uws7qyCR9X?E zSVMF7Tm<{$@w8Mk+`hk?=k{B_{oBy;@bs+1SR227`|i!F*B8^pPk!>#zy3SFQ(K$o z`PJ*;`g$`>)6>Jlyv$k@g4U{8Q!PLJi@z}P<;#~I_@JPa;o;%o+i$-8@cwQX#s)jh zr^m;qy3A$LX(|^NyWjlgbKMT#`TloxGhIzreEW6TZRS?L|HB{2I6NH>Kl<#u<8F6Y zmd)kG<<-@AvE5wkn#uzEn}6qTJ-3rild4?4d|57bIBe>GLtA7T#!?>dZa@9I|Lgz! zAN+6b4^MyiCx80Y7w?j4(-b-gG)Fukts4aDxgxPs(#dwNRwhKS6Ab4EIe|5?#sKF5 zK-Z0P1hPgEfq>87zz#He#!*iaTDfR~=(r@qOt+mz8xk0%ye5FHUaJnpv6DK^L;blqh}2AEWbLOt=Bh%fs#z>aXnTo zMRW8Akz>`*yZ=VWKs0p!E|8oZM>7rT8mbC$S}!_#OUz7gwm`E{j&wefAw@|Z(4hiJ zp?QKe;A~dnm$lU(=?BfR(j~I?3#9@( zA>&Xtj~XJRK|c-x1vH;WM3_j4`|&fuKJ1)O$=30n5arEtR<&g6PzabA#H-UpVBRt% z&}YW-ag<>o(4w1#GnjyjP+UBs0_KMHGpjBX;j_Vp^(Du-omfuQ~6ZpXU>6RE~SUcCUWU{--AJrlMFIeIx6J`<<`*Drdn?(i##$ ztj1`15mB#fp{T0TTWw9lFikp)km9AfQWQfGNqiHx8L1Jz%@)(>gsTe&Vg;0NE1=LK z&NHe)i%jFi>ld#-`{efF>3BQ|MIqJgfvOa>s#=t$pt3bIEXzq$beP6rs>|X1`?s%# zPhNlada3jC``bZv1ee2}1ILTu=K5;fjO}m&ga(_JQE+{EHD4c2$JuI~PsbY&bDf_L z%VAc-po2`)I8E(&hBaxl8T51_hcA(&61b&Dq;||ZZbieWHM_W zx+fqMkS!EE>_Qw0^_$6GFn0aSfywGl+|-ztcI227B%#M}IOzeC@N+^|LwNy(c(u2i z(G2ZFx~#b!w&%lfZfMl^AMPKY_qE#RUwr-I`bJ8*{c!hi_xRQ4-`?InQ3^2-W&eEq z&98s`c>g%RJhE9)EvhD@v7y!4>e7~_E#BW@S?V$$PX}6qEv+p;+wI2V)6?_){q@!E z=4wN$0BdVT)>;q8!{hU_S*y$3S`(Ox9S_gfuU=nlM*$!1AOG;*{o(bC>p%LlAAkDU z4}bL6e)Rd5-wfj@lrXdT)VZY~;FW4Bu0rfJ9vXH+I#kHG&l6g_LtcDYR@nHc0>3_h z=-n3o$cZfyQ3PV$CG|e8rlRJETH{zgQte4=rbQs?XWibT1L}FOLiovY0C5Kh8jXXZ z+B+bdAVSpqaLGg6e$dg+N>Knqi#nEcI3@e{F^Yss&4bshZC8nlpB zTNMuFWv&G<3@&YvmKuEAdhbJ^2snOV~iLJwm zh-&S5e^{1Qjr-#~w};2)Lo*ytr_1ZBrM92{{8tx$=lT|Rzy8%1b*|Gm7S)$8u3x^m zYL#Dn`PF=!OWCx#{NQ_^{ou3jZ^rWHfBt93KRx}!|MGvgz1Wp;eE({SK8B}$5 zadCThFGX)&yd1}2S?a^xQ$hJV|Es_K^zht_|Hj%@BGeRzj^WUpZt@5`s&5aI%k1YpuEWO=|jGwRJU^ASF}#jpn-5#a9qm`doFxQ7G8d9iS$4+rW>OxahF zsph?hdQe27K!P*&3WhNGewG%g_x;e0U6C9SdI(`1pFy%&r@}9e?iiBDmH;9mBGedF zD@p$N9%;r`8Uwgu2AYJmKrTk6B}pt+B7DMQ2F1F6x$GdcL?l^MBa1c%6r!xVBr&^Na+7CSIlZOnb{Uog_kpV%u( zw9hn1I3a$&kyfvwj3QJlD?MxI?C`_?9AmIyhO zy&}OsNRt3Uo(M_8gtOhtR|GHIrBQu=S_iEnp8t@16~N4^%L6FyKe9gdDnbfr!JSEx zMUG0s-1Kekr$5ZT>Hf2z4U$BU29CL6-R|WZ{O3L@A`5}Y7CR-y2L*VgH-Tm*W~>$C zxGc5W39OBxs`KeE$g<3}=)h7ywKjWxzc;7#XD1t33!?P1pU*_5oLk=1!Gworf}2c1 zE~lSm`8P_aN8P2UI2;&?Zib?x6czR9d&(j$lp0-U0j%&e#5T^>GLNd;Fm=PB9&eO^ zV;L{Duim^FH=C!shgy9?pGKjeXep!Vi9>0vB1jK{WNRdvHOh3_@1Gu?Hq-R#<%@dQ z+i9*<0Ad;Tk5AJymffbFmH}D>v)M8q>*-XV4~PA;OdE(iUtCVp=2@Pf?w|LMPfajw zH&SWyBCTn(M<=4=s>PtFkD_CRH2^W38gUYMCO0PHq(dGYSY|yWYkQ{1p%s##Nq{&C z@bpB+ls1bw#H&gWXBj<$K6?&Xv?n<~I}M(j6$PWTJ>5n;O%Ed(1~mu^2{a~WZa8d# zf%i{`x9=Yg%kuDiIL>yQ?VIv3uK_m9h5ji_~zA{5N6>NrlD%`gqaX1ITTmNE@xb2=@%-9?nFiuyrr z7~C*vu^=kmT&HwI9sr@+Ag0QNflJSykT+a^x8&W=pa*e)2ujqPSXPI}W(O;gGPCM< z9Q%i7F{8nhG1y$XA7)8^?VO;fMk$aAVxFdY_8vVq>0{d-4l%+@u^65%#-w5?ei*<8 zsE|q;8r2&EsIbv{47qZ7DMcE+_@Fr4&ZUnBWm74R+Jy}i)+#b71x=2(&!7L~=hNlY z^ZtY;*Vi|fH#hUVJUuv}44V%h9)J9k zzqq@7{_~&y{PN=B;cowL|IHshJU#vFFMht=Y}!(P{l%pJ+l`B_wSaCHr1U;P4C*GHERx*np06;gb$h01dYlb}+7Y}n^z0lhpQBIg5ui3|ytgpA`gM_NQu z`Tq9c=arh}{}5;q1s$*FU=TQkJV@z>iBbJXG+Lzb#>q#f4tp*^Je(l0t!9OgtfnH4 zLBp1LWolxx{;l*rV(^AxiO>8d%ZeWT;6aL(!O2ZT3w9x3Pw*X0*PaZl-Iz^{bZ_3NKd@&uoz6J7S84j_PhU$ zaXXShLek(!N6ZvQ-^7dx!sfH@N_?(_Qc8FZ7ylF_WVE|#RdUaP3gRWqpea5)s4{Pw z*&S6zIcG^W0j|URi>kLh^b>%f&GMoi(G$TgF`|{#MEGM`W+=kH5qJ+yrc0r7b<9AHy&sf{*~uKgf597K(BhH`mv%zVpfR;dI!ageXNf7t`^i zC@N)WRPj+=!$7i8(5k>2rB+%Mp_0qP!-L56&5PHcyuSVRU0tlu(0F=2oGve}U%#w} zSsDwp)EO1ecaP5>?oRuoY-Yi@-#l%{-Ey2CZXb`QSvS+T-R^H6AK%|D`{Rhg2G&Ju z_0GMK2kDb61Nky{%_W284Vk0W;zv_1d|RdGhpnO---eiW*Feyk!G$fNdS>L2devjp zlv3*|fz@BSW`?$e(d!5maZ)L(Z22xrw-u?}?)xFDvvU{Z_Men*zrFpVKm3#1hugWz z{o~=6zx@3E?%95z-+ud^(96pg?{DvkGEFCcV!SM8On5OLye)QXa@ylQT?Z5MPKY8=n7oUG!rp^81-P7YzXKpqZ z+ub##v<4lfr|0A2GG5d%Oz+89ah#-!`$$EG#*p$AZbeEa_RDSKGnGdRH> z>7O~pZo!zY)tjVA;4F6`X>1|X_=>PRzE{NGuB=RD52wC5<+z^zL^JpSKq&oaDxj42 zKOsG>Nog?ey(|=2kBr?lFiT9lOJP5tMjTtk=`e!n%!M>cfthJD%nO@5yuH<@%kLdv|+(d;56*Kr6M@dTPJ^ z#jo!l?jD{VpPx=e^^gAD9}j>0XP^J-tEcDZFMsvbCvRTQ%kO>j?YCcj@%EoR{7aJG z)^~qVe{nn={^>vc7vnh9W%=pPer4eE(^F9$%g|~Y#`5rVoR>O|e4dQiZ%mD#-5p^ zK+#Ovq8EGz1xKLIo9*^7CB{~yjI=*JtH}MGtYY3BO84>U_>*`E!0a~6snTprC+HB% zX>268iW?0b44_QokYh4}zD<=*QhU9b@V}krB9IA3sUaV|IMGW$<9Pmv{5=yUS~Md| z@Vle#AqFIqi5S#-%tw`&jvMQuMbBU%GNV_-OAdOSu6PDObQYpZM~B^qYpkO;Ma>=% zUicLslworh@sAWa<@ro=bH^NEBcKygH6XJG^f>}ZeMI7HM0~^bP3OM)uYN@f4i!}gG8AZ$GHRJr z2Mn8{MW@{~mSJ`F6fBZAp3ja$f-asB9x213>+>RG-UT!Wg zzWVlETWS#56kg_!hs4!M6i-OA&pNyMnb8&e&zo<{Qk2I>$ zPVM;o^zzNi`=PK^m!j6}@bGkcJlMQok+!MJVOfsz{^9xX@MMiDMVg(S52yW6NGT|) zW-Sm%#Gh{Ytp~9ysbB#ENw3EV$~Vwa>|vsRf0;xrt@FDv+9gk{Qo0Zm%q{RzCWWjj zk=mI9Tuexd>tvJS{#xl554gFC7DHhXm7uGLd*=Juc|wo)^GY7Ue)Z+sfBL(>XXNp? z{PCas+2@~s@$I+o{+#OFp`_1R? z?+%T_Pk!^&!-7Bm^Plg2{qxV!3=Igg#eDMW@tu5hc z15!DL?)(R{OPoM8a{qb_|7yR2C-3SQG(^Av{aCRPWf*a297a8f6X(@+6^Z2gdt?%3 z2_MBKr0EMRSIDQGd>9H5skl85Kxpa0~i`{%=Qs&(PdfBMT`{KYTd zzrDM=ef;T9e)0Uc|M8#xd66>d_Q!wxQ=jbd)z|M-(3Tk#QMF2;e0X>J;lqap6B(@O zAOF#h8%eWKr_a%vsX?axfl-Vemu6sch)w+^V%vM9NTH z48>(61WcN>9BZVlH7QW&GFL)M8CoLm!)+lpA2z8Yzs)?+W^*~02|J_t(bd&y+GN_GKdLSz7)?2IciG$Wvf zd+MGx)9pkDDH6CVnkq<7G?7Tg^!syM8e*S7Ip3HekpwR9D7JWPf%L_);-mn45|@ZG z!T{xQv#x($mY4Vwf&^!NHFZ#pepOkO+3z%fup|_X-}Fuf0+8M!d1b=^5k&&-=5pc% zf}VmIN0pfYbjH&7EvSfiTc5`}^cXc?=Es4bA(SZ^y&RWT!>)*2y% zS<65GC`Cjt__Sbn3#M2UgNRx8<+7qq1dSY4UWiD2>EIr*&FQSAA5W(S_VxFTjDq6T z_jdrFL1vF+`hS$bC|pwtLc?^Gu5@#29GvJHd3*5z38uq`$ysnG=_CMn<^@_Mt*b=xz!p&#$0rN}VHmDnto zChKecJV8xknt_mnddSy^8E76Pn2ErOS`HI3hEVj4G(QgqgZ@vA2}{H5qZAeF$ub1I zKa+;_pbY}isLGA(|KgRtQpzxm({?lMwwq;J%CNbZrZViVcBf^jwV{+Uj9S8!YSgb6 zN`n+gL*Gn^oyrvxz!M96m!v7CtLe>WuPf{Ecr02B5Gli0R)H)7q<}@V5Yzz#pwbr4 z!ts%OCUvpX;rZ@v+)S^(`$=8e;r_`^^Dvdu)1h8omZG(p1rFwEf3Ue0Dh*Ptpjfju zS1gW9C`4NpCB|VaqN>_XNBU&qv{NE$^cPzZHZvTSdr$}Tow#uo2xUom@`FbLA~aYY zeCDZwJC+c_#7bC8$1n%(DCJkb`r_w5|FzGE`SG9q420@H+TH#A`@1``6)pD< zkH7oR|CtleG!u#x`I8_2M08lHsV)EHcYn_b=P$na=KucX|In5;jl)0wfBuhtXaD^_ z{vV4BLjAA*oBv)^q~QPY|N4IuXef_QhkyA$|3QHq-`@T=|J^?(>~J`eRFwbQfBoMU zElaci;eYyP(=;t@Ii3zE3Ns(^ZnPM#*)7+4mg$QeCtzz?UbeVC{vMOMV_K zB-bSzQS z6uaDE;Mqq?r)S|1OALc&j+v@Nx4p_1<-d*~AyMb-&_5{*tPlrze}Q8_7`1?rpq14I z&d;#Xnh0QvK<N;g9#BvT08<7xPuSOeTGlN zc*<%`&u>m7N=3VM=-5^ehkTMKB2x+CVxc!^UQ%)#Oa!W{zjmN*FkJVPl{r{beDBdLCyA3;8e$f}dA0l{N) zEPG=hMsoyg^qhD?8@x&qNSjquo5u6?^rXQ3fG0Y56x264ZZaZFamFR~K8BEms*u)5 zpdsTcKb`ba7@E&8hL0OIi||HOOVKtAr8rdbMo;4LF+1Fg{X_IgAx1Cyaz#r# z(rto!FvZGSrDMa{9SXy#`dZm~GM*ofFnlZC5%KgBvOl92v~`7uRs0BDN#0Y)!uk>Y zzQv!!Lu6l}m9Gu-M{9(H`Y@~ytx=X9GSjH)RfawQxJADOCTJ!gF$W6$hT@KTNDax7 zg@6x<2l_n+ij)EYR4nQ`B}e|Kl%nH+Qif?7x0~s5y0qOel*@~$P%f@E`{UG>YGaXU zWCjc{)o6g?M**7ql!2f~G`JFF2&9NAiprojuU@=(d2@UJ#M;1Okh0s3+l{IUqa;#9 zpoOit8t5QGQ_&j7)rQ)J5m@T+d4GJ~UtV5cUtXUc_q1giru|&o(xz!sNu1F zGw-hHgT<$z3{+GTV<{@2LfTTtaj2?RP0bFEPnfHxLs;NC{{vl2JD-#il|Si7Kk)w< zKO7sG5pRyU&VX}ul+FvJ@68~wio1 zSmUa;_R|rTiMgrcE)b4R1La3_!)qxWpi5*oFA8e6osXlv)4+V1#PQN=T_!B(pkW| zAzkA@H%lQyU2cq2&&lzepW@j9-tV{PJ%|XTh@V)-_Dh6=JxcToCg$-Prl=8zm%|sL zwWi+cI;%y=e76MS)|sdNMC>ohfI0#sao1~%L{oz`)uFYy44XF3DB8%!5AQzz#m_FT zuOA-nKYac6^!Ui*LE9{>v_>{Ww=U0Y4Or(){Necsmj*2WQ4K>Gq*U=P`DpMKAldSW z>QI}c7KK@BO(c)_U}h*~sMgE~&{`{6L<&jx1bGlJPb3yCBG#H$MmeeEj)uq0%=;7R|%bZiORv3*uOtkypVuJxd+z*gz64 z^rs;pLz>Rjzs5)8Bg7SCeUGen+kYC47zvUjD|dZzU3zx!x|0NWT10?YjYF_S+0Z5J zPnT1_toDh1Nw4|sXhPwD;XL@km<-}wPM1RPm{brP@i!hdGxBuy-D;6Q57vt|BI-ovls4|bbUq@G~YAL zETBO8UXVvLV~e9pVRnWQWXm9=nL|3{RZbZv03L3qoT$oBbR33h9K54KOdIgG`1k`A zha>CGdz9QiQspcnKtNSwE0Ps+2g=o0yIzmcgUPf1LhZuXilA3zNXogKUDO{k1U#l> zvzLBL5a)^g)*}TuU$-70_God%)G67X!-JLSTEkCK&y-W-xx%_=9n3Ri%~@4nL?yhF zsxfkU@{W*bEQ5#mp?EK&0QkBxzhuiH-Rm>P$pm2LpvWQ}(FH8-S332k>P?MA1lFAE zF`xk!9re@FWbPspt)V;c2spg#DMq!1PwnSJ2{TqW*DoG@3q{nEojpmeD2kR*H=FT_ zn^MZfZiI1jIhMykXga8l12n09>4&DkIvqJ~wZ}@xJV5a?0WDD7TwlKa&ZiU{PNx)9 zPTO%D2T?&O7)BW$S1k8iTe^PrbF)FyQ>VC=ifd4a0)V@6q!Fj z40Zmv%OB1jECFoTJV$}n(WdWi&>|8YHk@U&Dk>^kt-4-#W2DAb$^f!eD?>p5ee?cr zAXExHtHB?^sHOPoHj92}Mn4&Y7KPEA^c5wkPzUUehQ%wJ1+6ubLn&s~^5jrNoK-EQ z0ICqDl?i&wIHvxDPSHEHF%d6vBulK*x`sV;b4^h1)vH;zEvXp-XcqCi(>&V(S+~-& zk2GTe?+s0QZ_WIi{6o4>twI^cgu3Qoh_y!X#&uy1RJ{TqhIzv4e<1>tFxsmT>1kwH zVU@kq;@m3|&_r{jr2wF3DTpef;p>VXVnxp9D@d2g)l~tVG0_(!x>J?IOB69>8?5@Wx8=R~3uIF6&x0XM?( zuiTlNhwzmmNN|!nylO&FgK03?DTU5Zxb`?1CnD~w$hHQW5a+6+vdkSQ4J6sn1ul;E z^C%-7A@NsSA^^xB-KM3@m9Bl>p3D6htD zA$(;et9bj(JC@R*w;_?=58=Kh}An9;F{o48b~WJ4AZ+1Xs#5NVVqN4;uYkG1UjQYimDEUX43lxnv=dh z`9b@hGI52ts}V`grFRZo8IcgPe7bxv>G=T(_}#O(4U*0mvF9J72heeBvZ_nR-(4E> zL(ugq!_GvfuGMn31ISP$&x+DNo~sSLx4l1QgrP3}jHFc&xR9;kQ7|`*_lU=Ea<=b` z$lf(4;{pLaxiZ3*JoJ%%YT4P06w5GlmV8f_sW@89O_W%42I42G-?Rvb0!XjkAx&ta zPV%=dHXbDkqF0j0nu_7l#&m>PyP3ZiL=U2VBHHCiVuL*sk-<=ih)O9aTBbn<9fq>m z4C8dMnMPHq&B~-@(xK^4REDA|9DFQ+s7KRz80NQaFzxFf`!1qNiWJqM$T(cw+`Rtu z)6-m+xj~}y&u%;Iw&QlYnJzDND0Uod9?G)FvanXG)#?nZTxzkQEVa4fHfwXEEOy-6 zy=*=kFJE3fK0ef1i)cN}o81Mq9Oqh8mSwS}3ZbeMEMq|d)*c{2VWSz@=7ppVGHoW8 zTw^A61=Ny03?PL7h-_RZB^~&l3wQ!_$!e5t+}O=xSfd`uA;HesCijrre`RNRw_<5T zkQyXsAB6Q|yYf*|P1)5ZV0|iCOd^xjSB3U>1D>`bVg`_CZ14sW9lJ+gdpcBd&#ut4 zWnyMfDeC758rkFkz*59ov&x|q14;2w3E7~6#lbrx^C2i|;|e^?oOEPBANRy+2EeE# z0&d?4UklYpYJhc;&W}Q)XN@N9CCOg^XDbqlKoN>XI;^M9SI=+n2ZdLo;R@ z$Zq-xcv8CyP1<{hj^T6jaD&MD#Eu(d3B#OvR&4IIQ*eQNNJ2@UGh(eRMb#>lrl+!h zcPrxnVLKkB&7ddzkVY@8cZ4b4MAE}YrRM?~n@~lySwwI?8;@pMqN`JEb(5&l$q6k1 zK@o1;TJtqmQ5|5dAxwlPskcs|Wgc}PB$rr$`ASOI6vChiBfY0}pk|?Xxs~}bT5;pz zsstwpRRBcku#-^nQEFB*#-9waDuM!ta|)GCrsT=4xM)$OD)VV*jd*tm6?R;hc1dq}xdl=0?$k8gmVzzf;_5Q}p zqI5tss~>!)qsR=>m6mJW;n@j5N(S>FJ~F!!yoY57f~iGJk-+zSK8L`i!ZcKzQNZj8 z9_Yv0>GB*U_>0ek*a>@dQ0QQ~+l@q|>6pt zGC&(#8CMJ>^oDc30%5G&YI##`b&w~Cy<&c)Q?pVM18Cy$IH3zGk<009uk_p~78s(m z&vQegvyKa{H`1rUq)(hi;{HHizD_2pv4gZ2r?Yz2J{7QYx9+_asy$q1Xh?o|_U4yC z$IYuyGchagHl*M1Q*c)p{R~!EW#16wp3Y~9Zy18nh}kluyHxf zN9>Q6*VmWh<+fVGVuy$QZg;hrHc$IKPj%m(Imfs>9aTrw5l}O$Fi%;52oK~PX4=g|Tt0^<4Z zuC*HJ8fM;=!JV5@3%mq2TLeX+Ba@)iz%`xrCqNI|=si6A43-r2{x*b}vsUp+s3^^u zZxNo3s8w2NMzIC0(u`)bMr%qMP5dT}o)8?u;Jc+_H1GHYptaay74ahQNN1vm7(t^& zWt88Lbx{is9{bW#z``$?y1S$xlVaQf`C*j=q*}qJN}+pm2fL|g8Ke5=sxjy+S$lQ6 z0Wn8)v=&qZK~|BpY!VJB?N9QvOyko{J_O;TMY2B_^FS-Xs^@1rZ7&i;B>Ln$ga82m z07*naREop21d4gJ&{0jOo{++!nMXjeiPmbfYOTVoElaQq5y7X4k{o3fCexx~>&z@e zFr=oZ*UDVo0H9$M-1>&?igWx3h*vZ1@v;16DrdgL zk_(4~#sM<};!9uUMEy~tD*)FHKD}yIV^ClukT|MJBTNT36!oKRNt+LdMyoMhVP(on z9{qO|!hlU{tcR}VDYNl*uKw-JIZ9Z$F6B5cMu@DZGi$}OU3X=6@peoWWPJFLzrSNibF*DKyU)=}p!Uoti}qgv4|;_2Mc;vu1`M9s)Uuqxmqno#{Uls&hSK z9sx0+ZD{*B;U3}{4<%sFH|UNH-F2`NFsq!S2cLl#b3eE?qSme(q-E{5M-=@-9BZ!r zIgw01F)$D`3+OLrh78@_bmhsE8^)8I2~etC*U<25frD~zNeGXVKhS_O%sh=;jD#lA z@z#1fvi3q?syuVy`~8?_$T7K}=tU4O7TRK701&Ap=E*H)RYa`W`E#NBd!%cRJwjy3 zM?aE?vRAO!SJT(L8z%4fT@-#mucE5d0xepGX)|75Z(r=j?I`my-ygUcix$BkHt4Y3 z>a>wEz{!zj5VeR)1DW|kCsK7~5si+5LJ^(D%bRNomeWkLg0h%xc3T|=s6e$IPRrAC zJ3JLGt<7r9Tb7PPDWc=JDU;f?t>czuIvt+caulm&fvG$`?YA$kw=ZuFA0E`2w#wEn zFE5_=$97ue)Wm$k21KN&77<&PWv+lw0hLzkG;Ee?N`a~sT|^o&y6Ljq?!MzGYTafL zvbK=59*M=T+(+r^tCM4JjZPm+@>~m;3KiGr&TN&s{`xZoI#VYhm6<$&HB1TBazmVm z-=&FXW-5J4yd#x5e5knC_gE!=>VM?Zp(EZfp4#=o&hn4g4=4N6rNVt23#}ly`-XK9 z;>1{b2;}#pY2`H30`aiI= zuAaH@X=BpJGxNd{mLW{>nFz+YQ{U>mMW*ZS^b6zA7{Qcvr_;<-#hO@Y)}T(NS!6i* zvU{JP5OzRSX^etFV}A;Tn)jeNACDZK$t#7#TSfU8wTuS4Dj`)V{C$SS@e5ral3PH+Le4I?@jgL@Vg52E zK%fUTiL?y~13&T^AiN2Yw}?nt0N}iSdH@KZhVYr5?4Hu3p2EM9i0oP}c|Ari9d;fsqpqS?&5nUxAf+l-Ldq03sC9mzz9Q;ThS>o0J=a$zh5S1 zYB3Y_yPH{2vFnRtO=O8t=nwNSMx8O2z&`6r$9?n|B#cvF!S4POxUr1n=Ed{{Pb3! zsxgovCvlpH7@Ip>n%9LJyxzpjtb);tI;TE$J?bV`=!Y}8g6^C0#4{ByAHOI1$A)L! zLU<{oR|Q8q4lNWNHq-86d$XNhT}+#TW3g&BOa&@UV1sO~E;g5!?cqtxC{;ww>m^)S zRij&xuSZIXw{!=TqEcksPFF9k4XCYpw+e#W%>?zN5wm&5an@5+Hfv_SN`%#_Kq*5R z#%a3PY%a>hMSB?Pp5~=p++5F3hk80ltMfb$7dr@8+ftWIpMsV$4CA0> znU}hpMx88Xhl@gOt{`MA!}W#iHW(ChSTO|i9YT0!+ry)TL(Bc}5FZKSRwDZ8eL6wN5+lRYk4F)%7HfY%fkYMv1a{^_aBgveFqLfWye6N-&jZd!nQ1zG z5>X-PBgHME)ClNkq&4&NA3wZtLaw#y$ewIqKiIMOeYcu%J;J=dx{3|!@P0tF%!_#9#NYbKT3H<4-Y^F$%7NhC3J zK$8WzxrKR&6-Nrz>m!evB)cCZ#7m$r{c@?mAtahj-*fYmsXlx1e0j%+E*9`|nc%cl zvuSLXuGPwl^bn%od-rhbasiNavQ4@tGGl)ss8R8h1$0O=XI`_EySlQ{K!$u0@fbva zX6iYziKt9asAerX8To)8XI?D3KW8ouRQ#yRj3Pc!IEsGav7*d45+8Lf4IydXXaOi1 z>Fi{jpv!sprcRxbkUoG7-F`%-OYU`l=)fKlKsuyM)8&hoxvkrj(#6DTi41yPO%f4I z3H6O|EEiFa#6g*K^!!(q!XK+Ukp9awl^K#rnE90Eu42}&F6rcCjZs1z$UH*;OjyvP zvUHmoPBj`XMEHO(`+2c&w2TE!kd3ofyD64{-Tj6=;nKaWhyURn=-uwTPSJHlhtZmu zH3YlEeCRk<{>Yc3TfYtpV<|F*T<6)vT4$KL)<|bFVMOX#04wQ#3Z&>}C>P^!U4~I@ zv%5GnOq)rkQf#s!&@x?I9LqqP#fhj6srm_}BqkFf6snS=2fbt0?s9u|b$OVV)<`K$ zD1&S-wq|XqORMGzI}Q^TPsSBfh)8SIU~^rBXk*#zF2?K4#iep1rw?~+u0xf!pZ6ag z-hAiHFb)ey;BY)%Zg*+ciY(SI)Eh2J6A-Jq>HPCz@0}O=#QR?!_HoxsZobMU4FW;Sl^lW zityR7Y3DlDVyt<*)iPUSHJZ5){X;_XbJZAAoa3Xy)wsbkKX30Y+5pg?8W^>;R_ns3 z81v4;U>b-toM-#De)j6J5k>}SKy|Gy@yF`H4+!@*vhMe~0KH0tL@%_AOazJNa|jRb zMd4JlSA432_*T7T4+nu5GAorr??9+MsE@eL`uc)|53TdHZmrSGTjRCXs^@E%A^UNx zW;>oAX}}|X@iie4-SPmy;lEfMVg$G%=-r*+T|Gp^i#7?geu@yy)?(pIqN?W{*{>Ke zwHzP8)4c;WGfCj;PR5r!CE(0ih9IXBreh6K&fy-k#L+E85dB&l zq@UGNp;&J=dLaHf&r;)iqN6yAFU zQW^d9M*gA540q?SFGP6-WFOUIA}zv73v~OPAK;)_`fEcxR!gx4X5sDA*T%gG;HCaK z=TpRcA36%pc^;A2r=GtL1W<6a9bh2BmVoghqUPN`yvE#zjCnh@>^vSQV0DmvFV40X z-En28ms-cO;i+k`BwVTgC8=a5+^)@ z=;su|q{sxHSRy-%TLqbQ12VGpB<=K>I!MacM?WRp47;BA{Io71-D~~juN33;vK^7c zZ78(U)baXrk9lUHVnN)3~1*X0mABkT=mdhO(>GYQxS>mnFmv` zm!O}HC&R}6r_&-LMMTXqX4If6@;+hYFo-Ay9hOQWx_qvIMec$VE3UU>GRu9bx5csaeBkm&?SaQCmMdWV~O-b-5suw7ny>vN>gyj z0U6GKVhU=?QefkN@95!XSB>a5b%wmdda@x=K*)@i4p-RG6?&@xH$4h1j5N>=m#Cet z3d7JJPNIi56Vk@L$ocIsA#7KDAfl~q0nc(9&c4oZa4OnFY^$YXckR*ABMo4Nh6x@FEaBiFVZVi-qT~#a4m9JP z?3NtJ9Zy@Ac!KLKoY~1lAsvL$8nl3C!~nfFr4VLpO0fnNg9=S`lxVQmksaZ6dojb# z%-F0^K!^R&8oOB}ZAUx;fyCU=;Cx6B)_N0sNxv^5qF!$pOdLs1Bwmc4J0L;{AJ<1A%OZg1Tg zM=wk;BFMB2uh!B|@%RfJJeg%uZZ{}-6v1Y0l!Ao!wLny=2USlJ7MW(gU%3UyLmlgr z&eM}cM3gLP8kKyOs2xwiJy&=DmXmJ$3|t}E@wRi9u4yzg_1346wc8!FLlNsT7gIe#+*GJAn1^`gQy}I>{YkxkjqUK#MUSIPV~PYl!5^M0lF^C=@TmscU0eAZFTj z#zrm@0p{2#iu9YS9mu=w5eJ!(YzKmQo%KCj+j$BQhdWV@=i;j{#pUHinNM|Itg$Us z0dF9B7MJB3L3)cTf~{3gab=z;BTW|JsHJ0rtSgHB1xd69DKvI6*WFAJ)dri}a%gpc zDvnE?XIqX78-;S%AD1drVj6H%5h+74Gf^+Ui~9+npJ0y zb<}Y;4P&WwHh{{;%7RAKaig0-H^a0Ura|3r(onPrty-;OEEr#GUzE)_O#64Y&B$eW zJ|4ztlKG)(~a`^Q>m$0cqZ z*N{AgLg4M5TsSi?1^L^sy=Z4RJlSAxKw|+G!!jmBOAGRsaBqH5GTaXqG_4TQB_?_~ zL%ztDk!x;_WKW)sis928m?ijGVPCY-t5>}z9tokYT?eF_o3p4RBB67u6HC}7FhbP~ znld7aw<8(WSe9OWBXSlsrydMU#9!cX-(#Fv7o4S$ZebG_}Twf(%#o2XT)Gh1`d!XjnL9E3ZJO7^Oovk%m z*p=+X7S~H);0=M8;_M#QZ^}aByGVK-*LfV#`!O;uDjst9Hlq2uex!eFaSq|(M}Da& zEz8-Ff)oefE5#H;*oMXjwo#p2^I`Do19%uAeH2XEho=H!kTm`4Q zi1dEzhUVw~C1GRtB0a8HF>mg%1l*5{YnScyB8rc{!g;yJPJ-lP`DX00C9XI}9ZPVw?HU14le z@r*~+l3mpR2}ZnuWts}CwHgr#u!ddk0^AnIT}w3^bO5}BIa;T5NQ6#Ke`GO4j6SVW z07UmXxSMgBh1SH3t+BRxn4fQ-@2;N*6_a+T%e&{})6;1_)OKoZZi+JPcI|REANN|D zHb$iCM?F=-OiLNIn{jhF>VSf( zR@t79hli(Aq%N2#F%E-(912IBw%gr{mp4)d)rZ@=)@=WLxVXI3p@?ER%+{J$PZ_O9 zAw*P$aTL|1E>^4C$!Rn0cGKywKRxbku4TK0U|AN8ll0KmpyIqw7&~K};)~d?EQ1U{ zi&cuxTuHzZ7yFKqyUY4mIaf`KpRb5|6?m@y1_@K|IpsMCfp_zWH4bd^N*Y4vH>%|L?;83;IVuq+1Z%b6I%7uw7+kpralW_cETCjdZIhoKB(F;1HunvGFg zM>Z3OBi4%Rs6a@QNPG#)mN}+=3p}GzWwnbj(a7H+{Q>jVLmGqET)6{T!A2Lr2vcL8 z9U=fA#kpSZ&O$eh(Rvfl^GI*M%Gwb038ymF#N;E<}#}MA@Ac<9B0%3E!sDAkK@h}0#9IGJuLN^pK zp5Tlqh%^_hMIbmspJC-COMTaD?9^lg0MoPA3Oce9`mF@@l_8fV9tuZ)j@M%^d=dz0 zF#{{bK~e^D6Z5UXt%Wdw!KRSs9*c>LfS9iyzteOX35sQX9YjSNTGRlXc}d@}sDoh~ zC26d`59STenh-hOb(W{ON?SII(cQ)>_zBSEFZ~XyxW-l!T|=2iT1++RAxeL`Ad%oNHXYAPmvhM=mo)po2J6`Qshrea*4pWE@|av}}VV9O$A(Y%5H#ViLX z1$+tTM=%%MMa!b8akh#UpxJr+&A;?_*PR~ytCc_}WFlY&DM*yug zvu3plt+vI?gY(wR(?~o%Hg90h#G?*eqb``%n)Pj1eNBpcEc-A_+wCxIq!f`NMLi3v z=pd>+0B{pY&&m?b#OKl-P#SYDS9PQ302>x3c+RbaC(LDZO5TDLWLA*n6dA0j->@c$ zWGE9xBbtKHGbmL(5EX}PKC6O4Hn0CmXdi%{(4Hd`J(fh!33f@_<3I1`>fYgeWpce=F1rm&-8>WEQ0%PZPk1Bf ziw{5Y$(9Z2@XuU#+;k74(i)wAP*-qD|G7WL%4`P9Jp@e)um?qPBf*kufbnz!Yhzdc zI#)=~QV60t0u_ztM#hGM{xM0J#VcLVBzNDp4Lug{#YsPpJT6K3&ZY1}hYpF>MG4J{ zLy71bgfrCT?Lu=))Thia)wB=ehB`}r71*G?=BRHaNQ+DToDN}@!sTsK+i)X8GW6@r};|y z&;UQ64SfLnIABlPFk>|vQoLJ(#Q3$?Kgrnv3QejIYadC{Venejo#93Lz7!HfwqfpX zu(L^?MM%InoRM6WP@^aur(}X0`t1b}kFAIV*ibD=^U<}c%V2;(t~QG)%81nxfr?rL zgyDPq6pLwt1-*Eq2dI`l!yI3Ej%XhniTkC4oBYrg;1KAs(PxJ zZ+X`e5gDg(8Yf!giF5PRWSX7ti=?khCW3B7HEleUWEad~`3vo6vQlulqcS!j|uLLq(ef1;6VRu=`x%uA-Hmjn|D z<~oh*RwyonzhcIFWFG!VlRx6}m2e4LKH+K%=V+(9WKD3Y( z5l=x8bP551yFzq4x60>6AuSsEjdqhkocnwGWtjWgB)T!k)OonOeX z_+5i-P=%IZ9N0`%ilRl|N(?Ae-$keW(7bC+VBlz)^;)ivB2wS-t}(HJn6v1qFn*i)pGVv%y= zqby^|NUs4%tq9x{jSWeIicow&SV+`I+9N3^Uhr#FU-PJ&A?S$3^6~rNROt(P&Z9;Km78U`Yp~Ld_cA zJPT9ra+iIRj1(1C>sB>_H=45>krEpajb>ctYAQub2%Lw=-F8dY9EnNB%(DD+RwApY ztUaE(m0kTV`p#v}O~>*deM#7gcwyHcuJbHC7LyLy6SpVw2n~@C+4XslfIrPZ|NM+l z(tT7|e{bJ`oB-_Ph5*s% znqgl2e(5^M-e5VD--}`z1uW1(hhbS7N)bX;tidQ0@BPnVC|8%4uU_3eJwKFEYO6jn zkb>r}J?cx8oznF4t*~pVT4*iYcrSd6hnF;fIKvX6*?73GIp_mkP0YAdDXkvoWvt3!W7|xWrq-m+Y%q&tiBM_hye0KcghepwxShsn zT9#U?8GRTGM%AedljwvpiqhB&)5Y#`y4Xw`n-?rFkkeHw)vUJpM3@PivZ1y{wZZ0U zQ@MWgo!5uu{V#ugTIx8CFeru1Fby`07_H59C<@`S%v;;GrLs1Nn6=|!zqHy;%Q#N% z(>J4PYqnHXK~Av^`CJv?W3}@2?V+K28>VAmH+hmG<*wH&%fl`p^)gme(jbQ|OPmya zc+w3d4sx}OPl>2Q8Jt3i(3;-OEIZmTDK+<$9kvGs4;=>pwvG<~IsxdzHl%3Yo&+~2 zl5jN4-3sU-s>REZ0}fN*rvvWZaR3)LKNK%#}LfS%$hkA)^Kd;-;73&8}h!QE&zbQ8t2o@r8bu>Pew3zmYR4i zvz&XnkGoMR(Xb(Kf*W|BMyCNHy*;0Ht_?}3g)+Hv^?)^E1;5k0XJ?nXW5{+0)c8n< zs+plEK`A;8V{5g|RYYVgKGjM!^QDU#*jiJOqMkcIsJB!#RdsBCeRcif#fx|EzFn4C z1kLD`#nw!Vs_HV=qC&!~m7*lHXpF-PM%~j$k;5lbdHedHmv4T=6u`NIkS0I&(&(X> z#BK_00+Ws&!jNRH@7ec>cd*_I;v>#XEmwU=^^IF&w9s-n>C9#ZGmdZa(z&YcSBFqe zm9SRvP>Qv)oeNC1mx_l8t0Wy{r|$Qg?Qu*jdzi=bl#&c3*QaDc@BmWMml`pYDl#?F z_?)NxCC$1f1jpL?YtCbo{_EAI2+c%Gzwv(kv%o3)fl1mElC4qANmt|wIw691e-yfL zB|FY4SJF2!t*+m62eb?;-Qo>I-w5dln(1@T1aOs;Sd(*Cxk_t@wSQ{D#K;$dtoxb& z`GcqgQ|rAdxmK)(+iK0v3OSU*q6%xxtTA2sx(D4b1Mm_&|0z2Hh&rz zXZ%QwyZP{2mK<0S@fkB>StT|i{gh={ZzvF*A3-n}^b`(BxR!RqyT1{d45{=56?nT! zEl@4vsMDybWtm`2h?t&DDMiarHk-}sS1+z_u1Zl6Xen9-r)T?vN>BoT1AbK_YBTt- zb@9Ut>4b&r<7)Im!PB5aI~?~92(M-)YSNmTNo|}fnz0dskUB5Rw8c<{L3J}IRrk-w zr{i(Fn0A*}y5R8cPQtW%a2wmzGj2VkhKSFwLy^sHQ-FRis?&wwJO40S%WWdj8Vux}gSR6nr*a@FJO} zL+vw$B3#KT^YKIN1(6gff>Jzn2tf293`zEC&6G|fjCwY(oV`w9y#W9LqkQ??$VD1D zp_VDCahPBUy9w_x66K&hbl2d7K``d$ zL=Br}{-@)z*eWwAN^3*}@Gzrj1Bslt1d!I+P)cdmTC2tueIfm;8KYSjnq^?%(SbFy za4$5^jAt`KJqZU4i46K2O3%S<^jdxoCZcJcz-uieZMu-6B?G`Nq|Ps|p^`T<*B-}< ziow%|2=Aq0-5YsC=1E^6M-=pmpady&2|#WXRx~Ig;;9J3IBYL=)!HzWSFc{qr}^%~ z?NS%kuaiK`r=tZZOaK;~Teua(KY%k02_TToNZi!)p;Lswi;~H-3=sxP zoQ(w|l8kfw_8f-UdQOzMjbTuXjN!-Ux3eO^K=o-)$rZY#wLE*Lf1)GM!A~?Y_+w-# z#t~b$OM$(Z@L^Tp_^dz(LFYTBbmW=_$Q3q@pZr)Vj0T9FEE+fVaoCh*VC@$H4PGw{ zW7KRVIlwd@h+~$(imn-{6677pTw?@;FBe0$ zEyK!Yt)YTgFm_a7^UjlQ;7PSy-SDJwW4$4lJG5%7WTSr~q7uv_a#Zaq z>rJkhNVLCjix^L86jBBqDg;G~4z=iXv4fV@T30Ne!CZ=pih(+oGU#Ey zZ>L4s1S2NOQa+AR{9SUNyNMwnuVk#xwu~UWmf({LyGI6;=B1D=(*# zQq32|UXNB-HUN5r{*(S&1j^b|(#j!$}VSgn|;Vf(D4C()bJ9HzoXY%_1bo zLWg}MHAZIoXs{KHg@;fyI03JD04w6h!s=jp1B7O3DZjJy^gcj9o8leao4aneeL3ls5<~dq)23`d!sxl76 z=l+UHDXOKEq9Uq;x*sZ~lv310M2j^uQ*Qy(XkykH&}wbgYHh6%t))zp;aRPFU?aTW zd^GozwZnvaHAChQOPrgeQ2Rqkw9@v+OPZ6hVm-+Xq^MbQe%ld013R&YvVKGney_t1 z>7omVe9|(_QpBvO`e2}>3lTx1q|~Zncd`57Z~w@E+LrHp^2zaVtgYVP-3wHU6fLbb zx0ajDq@uMho6VN6S{F0h>^93>kMr^S-}^odclQs&cG79QdG+eu?FXJ_RTPokZUUjA zhr`Kjn#LS722UMJ)*<{mg^=V7$&!o!j3&Vib|G41Cr(- z^e6m4ilILQJ9$H7OrGd8iu5x8NY*x6b;SBR;pHv=fPRkex71Ahe9x=hLMSt&48b+= zV|R}zq)+yyxCIw&7RAp|k09ceRuuK#E0xTXem`H*ZZssBgOJPYElG_1fG9DkBT$cW0%_y zv=r_MxfRfzSj1rT@b{R^Vrs?2560Rbtb^{yAd`BO`mdSo748p_TU6$6=M5(EaH zZ>2Oq3$%!+3|^)-4C7Q(T5GL$x)4B6N}0Bs-F9on{qx>4KD3nWX46{JS_uxNShLm| z$)X@pL{$bIR3QyZYgH7grCEblaY^JxcPyC}QwVVZOP>axQ7m-NDxjfOTNZ(`4I)r2 ztr`ULVXm8by1rV9TC-+Vl{hZ*)4bnIFJHX6yuNwbpD@=_2DVl$+a=@d5XMVvTv-)5 z6o^`mfQQ5lL8OcY!l&n_2D^T7Em|5Y^?Erh7^|XLHCCIK<+MN8d|c)^Is%ok4AXRV zIb2>Gt}pQEoA3S3e^f4}-~8m4^}{1ek)l!t6)}+_S|OOmi<_IeG&;yBgTX;EhM^FU zBDx)iX{fie7}YIROjePuX-xQ@g1@eGAl`&BBFMTXe_>0_N!qF_!AIY%7k8_Gex?d0 zU6s0>4#rU}Q?Nrdi#i7skwAnE5Gm1pT>Ck@LXeHh19L|zS+D6;bfJR1KKn@45)`S`kfatMF76?Fw)orSj7$PY# zO;Vz7fWV?C+u^=nKVU}&2aTJfiFv}OGlajs5pKtL~E@R1OaSxaLhHH~Lc5sqb; z#&UUmwVB3Y9DL4CDMd@1FZ!VkNmb86^x6>b`QN0`*{#hQOi*qfUWr_9o?I^A<4=UX+ZIgzoNx_REvj}G^b;IPPylFNP!8} z1D-7ZkQgNc5k+HG1Ykvv#vt8R@GtZMWC*O`1t-N9T%6PMl1kB1uCA}Ca({mZ6)7@I z<>uwh#pUHNj#rnL%d&iUcUxP1{rc5zvw7a{U*5cUcz6)u<>jSlsjc1L-+%hu?-U(S zblPm@R&^Rb{p_=4K8-`Uyx3mtE{359Fdyc>_=}%CJUoy+F)1K{AxQWz0G(?NA&YE) zMD+^Cc>P=ei7@l^N#?o^=AmfVrT)(dpyEz)L)|zgdP#5Z#6he!`in)*M}5aJ+Zu4J zUp^n^XFUE~gY?QdH=kj2(ng;x6jtz%Ml?Y^T`ZCIsAHS>g})P8;`-v&c#EhM9FSwsN>zzGA7m>>WwxN{MaNJVl|#UE33pwJy*i}m)7 zVAfiTNx|OME2KV!6dC6DS`j3(l}qSmck&`hb*~F{zEljz=O2^D=Rs9{46wUmr(V*# zM4us>{-M&ra;%5Ax6W0~vxyLoe#jt__jj(?ofSy)ntDT?NySH7CS6BJ4{1FbQlcw7 zx_@-NC(WQHeOY{KwO4F2xyrs4qJB-Mg;b=Fl#;c-G3&}*S%zU4HxnV_G^&qI6kc9jLh$^&UzSA()uK8GblQvxnU84J zqLqkusS1|Cj{~~!_Gega-g;Ud=-<78Knrc5m10%FLG@JYFbo4!8ewd0S?au;j+hra zomlzw{_b*j_2Tu*<$1r{J`e(h7BySVkqc|gyh!sTsutv2+{>Uv%zI*J17^XZ@-WZR zFpfIOc(vQrqQ?ie<6=ZRE<7w;mZ6z|LXlR*q&@GO5vq@y=a;Wuy!p-#?Nr|DZyq7@ ze6q37REAMdDrMU3rtR){cmPDl;#}!4j1bjQmRea_MB02gM@ ztnZ!oyl9Jn1No2WPl1y?W@@nJFc;ZP2+ntxQUFTxtQbFhS5Q^h79*_HRf~O)YBHUH z`$e%rRe5jbA5fe+iZuSsE>Cjo~J}lD|ef!nE~uL z-a`No%zFCM=~!!(Czv&g(okD9L!;BUnW7DlY>nn&l*+0ap$k5UD#nbcC{226sk5E; zj9Fg~gD4~4#H5rSk&2cqt0__nO3_jxK9cneN(dJrZNsfDP<_b=zjFY&9#o>&CQGF{r;0r zU)@~aeDUQsFK%vB5VwTg2tIkpjttOLQ3)D)OaF4?1$$#dI4b;-vGiat|cNS@@a%-4RH+CU1yv(6LHFM z{-2=VKq^h@ha@tP{@aL#&V=X;`{$N)6?0*>1T*ofjL4{6q$70H6fpDn)OE&Xx>Go3 z%;PFfslCdxFlbf$Ob7b-0sS)TOqP5@T%p6Jz8KwSGNYfaT*w%uJ8rj^vBBQ{0|L=9 z=roP3H6TxCL_~|0aVW**u{AS`wsBXNS9$?4JI%A1UEkbnc3b!Q^>o@y+hIJ^)(8|8 zh>YX#>36@QDj#m|j>qHaxOf+98pdf;MC!5t5LNGH=d`6ah|p31ZC;0$7$^4}VUfd9 z2N(}6utuw}#oEAD+p@G;g=%v>JRa`K{c%3HfD9rirLncsQcs87ilM7>a5sTBzC-WxL&Nb`>%o_VR43)mj}?$~awYYvpXB zP-(8<#w@ruShHnOV>`{?eEI(7)vGt3{$MliJ}*C8-oIb`Y^Y2ED@AuVHz>ntS(xct zP|;GL3eg79aU6HsBBh>cmca=m+vKbq1u|3JNtsoSn3yz9KS^dQjZ9-*d=V52wZXhU))HZ`=%dtq&Mt_j*_~G8dO^!EvcTOqD908d5>j>BE zC7P|7^rUP>k&?5yB|?etT%@@VrB#cV#Puc4f}<~c#$JRE{z{A=&9B@VOOV9rW7u6X zU|>UVkxj%~v6=z}5HqU_TV<=TDlkQZ8N{1NNCZK7`$(k**yC$wKmb#RrY@=ZvJf=( zBZ5|!RScs}n`yI|rg0pGBEIh;9@oXjm%^S{mgFa!84Ju<*wk}QY#0W>8gL6zHGH_et9GFxcXxNSE}PA!*5%#1Z!a&l(=>hi z_S^4$@3Yh4xS2L)3u)74bJ*{7lzE=tzkm15m*4ERo3`85wiHz(U@g2szsrC^MCL_X zW`cCY-X8*IRRN$I4+^t^brJZ{V4R_WB4IM)q#@QqCGR1-qe?(B$#2|vB?)jAazwzH zrZs!1G0!mqV|SF0AZHC(07rO%8AQ^wf&hlDD*3LsTQ`S!r4)I|KVm_`-1hZLVmu_W zzd6rQ(TNQWh!zjQtgn!8lVqMEN6eh5{1zwc0k7g@Uz9XT>0t=Vc*dDas(ID$U$2!4;sc zfrsytXD<=~CE+}i^BjqkYvCC^JLCS4zK!)g< zLA1hXfgmRQbw7NjQ*<7WNv$F1!&RzsPeWKb5bvrK$ET&W-R^RpXRExro>-3CtIKi!yd3tb`=O{>G82ybQ)JJ)Z`yjOBr0PEYjinv-E>E z=det)FjDm>Fn<_G@lLyJwbo@ACX%Q5sQc2;wimnld_c2tDt2rzJ3jBHX*=$w>FQ#C zJXtAROjM&nDbHlEx<>D&kYRRFm`YKB7A>x@4JeywGfWWOZFXdP|LwO$%VwPJ@9&@9 zzFVG;+K6f@0!1S#+MtCnVHK9+({mZdLw#IM?ag<;^Wr<7VuSM4^ziP1&&#;EXs1Oc z-CSLpYBf_WG7ilwRKR=VlmR-7o1LoGQ?=F;>yt|W!fpjQDg8VwS3Lb|@BFvgReys7 zgAxbeK|zFXPI|Kg3V1t8$rylPT1zIQ2Wg<94%RghPbnVU44aK4a917t1hQyxgqPuj zf2Wb_R0b5qP|7f9aY|S{;7vHFp4lTJgbrg-RZ&6F0Ozr)dCLd~%SP3rIi-La!D0W? z{XibLI>PYF^)Wah!X@iFb+Cs8HPbUXe=eLlDf+) z0x6#FmH1i+A3$IV(xga1DWwd<;LYzf(rYD?oUCQeJ(GskqDm1&RrH$X(H%z8LY)vq zhRjH=2BboR?lfxgMN~);8H~^}Nf}Wl8F!o}-R{c84%OUyi`GSbKmCA9fz~txF>Wg$AhX03lGD4E)+)VIkH}K17ZWba{Ovd2ym|fU zv>A`bz(scr^3UQ^tRI2|3S@K|J-<&`yKg3zlpwXw=!R8a{Ay`_v!zHX z2s#{s3`mUO2GrkvR_oKllz@l*mfim0EI{xBxEPlkpd-#><@_Qsg$`I(IaxWfzm@0* zGvQi-{TDi{PYNQjmiM*~yG)WkIK=xA9uu|fRQ zA?i7D&IOSDbz`T!m>9^WQhxdF-Bx%Xd%+&Lqy=M|nKeT5&2fI(8m+;qRX5q)`~k^x zg{(Rs$)YN%&eX;2C zMxcpJv`6Ul5;~`O&|`@nZX`dtN+~kH4O;HM^Ne}fK0L+sE+I7`P=$B2FT*fyF0L*wFLzg0m(P#SUw!e_W;=cQ z-S5_FhvVt``f5JTMn3PKi}>7@c5{7ovD?17dHL@4!}||+^HSg6-R^c5I!>+e>gu8l zqe$6qH?Lm5y1BaE48zsswTN6@T|GVRi|EbuO|ACyynplQo4PD--+nEsWb;uHLqTg6 z!s$uLacJYF-fd&F9|aAJAs?xpM57=k1NeD+U6y5;FR!kON^5qp+cg^MX&I;c5BHOZ z)mp~!@&0jhaS7FNv)No;?B}Czo*^n>+_*_m83s$9S%i9E*;n31IW#Ato{)DtGf5q|@h|;oMPgV3hK#%=|2%t5e(0)e-r>GmmDRzKWL=sJPYrvUJfS2&978F5IDJq%%;VRoTDL=>GrqeWus1#3y7k}(LGEZK|LZy2^0hL|= zitb*y1^`H3>FX=KXl+dh5D6RNL=df+Xfi4q-610P%(|}wfoNO-BdA4J^pkDX5yNDB zpjC^OmDWc$1W^W{HqS(jt~gQsLkdC9FAWdZyh<6Z`aD{;X7$82!>Xq?6{kyE1zEhS zUhI2<^;mL=?aqlW=~`AviZQ=&?(XzpAR1@*X+~`%Rcst|7*$ktC@Kn%=d>iFBI3 zkdqZIKzz^@gi?s2QYfViqsho|E8DB#>Sp`$<#cm>@#>~rY=`Ys1|5e%1X_wfU_i6e zet&#?c)WdoyuX|89}adENUcy$T~qd*JTBD!gUAgK@v3gI z4CXo=6(^xR38XcOczb*_uv&D3R%J6cEXlDtR+n4^Uth`9_Zvr2+g+36sq&lNmcnm7 zS_`}JY_(alXh!T=ZtgC;kJH({gxUW~>Ci|j!sT7p?bj?}kt~1>$&*YPfl?G~j5v=` zyaXVokoaa~Y7^p!Ib}oozdEFi45X;mPRx4tp9ai|{fQvw?4b}x2l3(+rKbS8&y#)v zlXAZP37}_xg?z^WNbXI-dbcR9TYZf}4}0|6KVeO8>cL*Dzn@?AB@T7&iyv!p7MR4} z`7uGm_D>6Mkm`&;Pm*^o=toXKG|39PpajY=KvjoLBc^G3^~onMUcS1%zP`NJU0q$> z-QGzVuCH&t|NZZs=F|HR_uK8J)mBu${N}6co2%pDa5%K>X0zRGU%h!fxBB7kp_xH+ z+-$bH?X=lU<7V1yHnncH7u(&%>({TZFRuu*)~+ruZ(dxB=+*Vj^Zsyqf4|voH{0pO z%NLvN=K1M~w4w?vD%Il_Z@rgJ*(6zL$uK~KW-KVqVyAb78KXc^L{yhrm%4~z*p%bL zzMhVv`uy;8-0xehEM*uA1$C*-tku?*`R3+kd3-t^PJ?F41UG|G0U)aCBe>XNz#ofn zC{~n3bTgPxikuF|&1Rab&CA@{!bYp@=`=sx-hZ;$ee&J!-16(EclTT>mj+bSMWTD9 zngV5Rhf_Np%jIsAtA;vG+-%Fm&5IxX&foY}ci)`8nCIuR8Fkzo@9tSqM2nOPD_T5! zC!xbQ?zY1;Rr5B!0JC>lruj-A{kJM^hNq<+G@vtAbxl z)_?Y>S^+b7P5f#*J!xABq0W-!8D~AHGpdn1DaNYDRM1D-rrNvCrRD6EsbMXKp>%vE zaTFx|{+*riNv$;VB4e&sT*(@AlYl${IOqMWpCJZVNS7fIV1QB&*~n!xAd(*yJqfgt*Pvm;Ovq4)c7j$cK@!y!)-E5!|? z10Kx=A%d7SYt~c?$sz@2raF|UA5Mvj*0a$RPrD@cR zWn_@R%>ONs3bTvE z!zgd0Ez9%a;r{L0`>($`ez-k;xaadTmKoAS#xju032Z)6-G|#}1Hphc4$VZh6sgMs zveic8FsfPGKR)db`|o`E=`h}a_V{pjIvqay?6c>m{WG7Ro}Ml*E)U1!GA|Fe4_cls zcDuUF-+uj7o11{Ai@HB6&fj4#QK;o)zCrf~cV z=cXgwH#COG*gX}|D4i|I;2fLm>!m&QKB8y;A{yZl1t(7%x4wBNUV>w!357y7FA}A-R%@M)a~aI@JXK476rMRV&7cr{lEC`th-FGri)$dI zq<2YYCNp|FeKWE)a}v~=)upx8A}t*Tp$b${G>(+w;-1t<5NiFrF(=Zb(d+^smSgh3 zIPpjffR2yj9Dq@TbRNku5E!ub?+f9{Kzj{);_xtWvG3HHf8b<7V!Q6=dw$F+J0JaH zji{u5cE$}UpYgG07&=8u(rrk-M!VEzO)=7+rv5qMn*oL-$Y-}1XLsj+OA4;6I-g6| zf7RCp37EQjTOYLtHT{!1dz*UdF|BkNPeRilff510wAsioOuLIuKmFv>Prq|@b+Z|# zu?%eX*>^s@xVXB!xGbeSO^11|MRgpI6OT)jzig9UhH-|wl>cv z6=_zBYS7L-11x_FgS9%Jo*$n!yN%7ud^~M8 zqZ$Vp7hoy{fT$c!$2YI9Hq$hV=Y@D89gSpb@bNSiG7ccvqSo077>YJQE3iS9#eC4P zaj}XzH=gFLmi_*CdOn<_?6%wCVq3P))@IC=brEY~rXp;hP=_LAFc@uVR@-TQL~FIa z{%(8q`jg?e%i(!`T9)bRV%Y8KQU?H}u>qbsEVcTvo0Kw)(`1V+wRs(90*pYM-b*Jr zsx{hL+fgKy)_q2z#!#s4y}V{7vlb)bmsM8tb>ON;B3-&AIw^*0=(ASRj|)U8Dpat* zdhT-Cjn(IOP?sS$61n-6Rs;&1>05l~Juk^D?{fkxZCq9bDAW^gedb+G_x!8QFB=SI zl%7F|Fs4BH2g>TEATf190HR3%&2&8=TlA=Q{z-)f&0~iaZGxn;pKMK|HZE2+Zp|{N zKt~OIFT-=H_fBydFx-B;NQIi?&O8B5gs>*8!lp3z(Q1w$h&}I!ctte1<4o5N0_%ve z#BsnH+9glqRy{Da0#-yz8A>Tb8QlJ;ilDeo0};==Rd>oI07~&t$I0JfW}Xo_4#QZ6 zP9CcYAWAAhO7o*yx=2U_7qT8+`EEvbb4n)(Cz89cB<>S|k7|c7Dn;NkdP-p#mSN=X zYWK;fpM3wfzWak8TwdRlQJdA%^W*;Mxt>nT;aKO|+G5R0k#U?h7Z;Z|H!t73dHLe{ z>iX*Sr*H1ReRucOH&5Swz5j442h#?DSzSa5n>nU&q>MZ^_fsHIG4ire=v z7K*x50LxN;`HNpULOJXYs)yw;pH3%`hvRWMF0ECwX03rd?GMXw`SA7wY2SSL&FOet zmRd$V?oY?Ve!qV{?N39|i_PZvysyWTiY(Ry@N_lvGKO_p5gc7PdRg;>QfAym1gEV4 za@;|OmJX~k+BGxjeO}xttvp~dp-591LYw=T--gbKCSvX9T_M+8Z}MCSgjr(jKV`~{ zWM>{A`50L*Ktff9R8T?814Ms01N+FCs$+#K`9gnYNhw@^k&CKPV8!GE!YoD5$_#M{ z519HqL;QI-GOAuEAcM}@>91HGMI$mZk;tA}8NVO($>Khwq zXu+cc@XQJ&XtlND;Zz$+X&K_I2(dNN&zvQn9)LH$jjEe<6*WL=T2=9m175Th@)W~J z{EIv-%Til3?|0Nu+b|i;6yny_1)+#U{{dAB6n>y9Xk8$EsS)Go)6=PfF-ran#=1+F z@C<%u={wGT=fWIFGs5ijAS>n7jeKl+c+lR%Sp2{&{eXLSr1zVRfSo{8h^%Hl zvBt_7;OyhF%*M6i%r-!x{i0VLxeLrEiW~JR7e96YBQAzH=|@wu<#c-Y_T6EB8m4i( z*$R2uAD*8dr|r{++j}$K-apj3Y&V{xi*bMW$SZnigIv!7VA3jXu)Y?*pQfu2k??GH%U7k+$u-||E)m%hQ z$3rP)zu(iWs9LQW2GeOCb;DxTO)b~W_UE-ogNP|R!Q~j64I)Y$kH^#DIG<)StIIMk z%kJW0oW@}&^>7+hkEhcxZMK{7xIbLIyiwKT=?I^5?BRawYrq#)lj4W^+Nyhm=-($4 z5v|Q^8V^fr#KpKdEd-_Rm(#fb+YaWXOGj`zt(l&+@ z|Jc%}iB5xTc_UzwpW#~tw9JjAh)=5(5fs%yOZ5IJQST9bj-*hSZ?hiiuYrpg9XWuK+aM-G2GWSHJk#-B({N_YVasQNN@RW%LLkjV06bg6*K4 zj>mWNEUINFR(*e=MOE~4JbH1U#|-R!R& zfZEz-Lz;g5RzmGbcv9$Ub(ZAy^S3 zV>k`FFNOOf(M)gdBrAICN~S-TsKQS@TepARcw{V?wrkbMo*2|kLr3B&O6fJFAkbRV z;%5`u0|0bwL1N_o&ogY+zXIII+1*Lz-O7qgn82CnhZpQO3Lq#bX3>1Sm;G*5I~GB* zK(d8HhyF-tpRtxVL62a7iEi?917!6&5wf4Ot&^Z5>i2H25EzS!(Gv&@APstIw# z!JXeV!T*n~KV6a|%dv#P1K_HfnY(j~Eh8f%BQvY2`|B1NnfHH<85#Lnq`IdoyDDR4 zM3BDNqN*3b`~YyxSTd|E2I+J+Q&YtS4i10=#Cl58kUc|C8wQI$rifomLG6$uJY7xn zfCwDPWe#|nXOiX{tJo?CmRFle93sx^%k1MV^gnd8EfXW%VNesfK}WV`&_# z+q0Evn2$4GNe@cZDnb{L5fR-Al)0lS?{y+Q1Z*>G4VMvNGWcUe$c#ImcSKdYZmyu# z9OvifXQE$-csI}F&~3Y1F4Mf5_XlR))Jfpm=Djn~>E-2oK3&cixZgiKy#M+AW!uij z%kkwXEbo8*S(<$I?%jGm|NPU>m(z)e`nK)nDdt({-L7way_|`7x7(T1`E+u(*5qhIi^8xGoCyNbbz9cOwVQ4nzR^vH zjA&iAzIC1_H7D(&joj|;c9+Bc!-o%gxlqi-XLpsvF4j2n>;wZF`DWam&B$CJWTcy& z&dY9+)l`TlmbtaN-HtBim+f*nGU;Ka%Vk^aw7Y+3huJ6gl~Rd3lAV}kf)f$@M!Ag< zYPt}k+mD8~U;p9t7w^8scbC(0Ii7XvGsxH#K!^b{b;mT%&Kx|ds=)`Lp6Tq^BEc!3 zQ6!G1yN&=RtH>*=Ih2T~`+Yc*t^=y(8eUCeBz zkt|mFG01buUXt>*QQjD>aI~Vd|rb-)#HF8{6bzVn>!!|OHoVa!LR{jvEwfREqQq?i=LL^4f7a9 z#S9D%piE{O5-|m`(~CtL0s@XRG@I22z@ZxzpW)&>P%IJEGEGO^VF!pTn5=lCWW1}8 zLql#YLqO8EK%{di*EDj-0leTGI>%Shxt#mr+z=5z8KJDiDn?%`6Ipf-Gm&`$@VGg_ z$+MGZYkn9KS>mg-GpZZmqeNJCT>&H?G4gO%{opoGI1w>eh{Q+~;?sWj=8HGqe*4vT z|NQ2&FP6*c{SUu9{`%|XdsS~jcJ!v;~vvoP2PRCz=`{h6W@%Ec9KmX=W zpMCZ9XJ34_|L&hyq^mzJtFP){0f|UV2Z!(Dij#3X&5R`#u-r_0<~hu4YG9}-5t*7H zOQ#YlYE0UDyp9o=cwH3)w@yO3Zg27){Oq0+0<)=>zj~CKyji0~)kzG`AGXOrvvS0P3t)P#)1h|FOo+axq|h zRLZ02(rq{3M#=+MWR&jH?$#9!)y{Rg)iyZ%e4(litee`_K_pXyunD=!+}Pc$chkOHE{DV6 z`1G=>Ze5?AKDztI-+w2f*AEZg`*J>?pPwlvXSyx{H@hv%vTduIpPpa1H8q>(dB58& zr*q%7SjedE3|BRHDxyd3YGxk0tNhv{5jN$p7Fj?H3%L?vS-c9lyRDb=Wxbg2vYbuT z-L~_x?UzO}&(r;G_xj=C*Z1%H)?rG$_f2>E-Q%+O)_1Wz)C@&GaLR8JK&}vDU%M_{ zxP|yoY#}>cmc!i+#Je2}TfdwIIG;{TzMIfBgEt-GP>JoDNqrCAP-X-G1#ooL@v= zhg)jL3{3~W$+xe+GUAUokSlP?t(VPu6~0S{u6}Y%Iu-1xXnj46c?L(KI~+;n&-tUN zO&U^Ih_W6FlVyHDH`H<+oQ#O^31danATa?%Ht!B50gO4dH8qtUh@`>Y%qS`(#Yxj( zS>VEKIaU>Jg1Ff5JepBKA8wHHJPr*?gIVuIgcR7m#@etrP7Ma4&_jq&A3h-VFQ!^? zj6Mpf_HZE<^Zy~J&752OJWIbpP7$KCUK&acm=QTga+Rx=>Un{Pshb5qZ0W=@dWWkk zv8X(@DL%q%t_45X)JMSt>537Xt+@2e(+()-*125jP+&}Ll4ycTAQFFtFiV!@8MWNA zZZMK+qH&{ZSTb&Xis7agXQoM{HDRX6&BOfTrNTA1S!6P?&$;%kpx(03{xvnoY8mi_E>ab~=A6~!v`cL2d<)80fzkU4e_g{YckB{$v?#Gk&Zfg&ZsZMA_ zQv)-xr&J1oy871p)6;LKmzQ6D`|#yYPk;Q=mtX(!-J3t0zCN9Q`*`~JA}RZ3g3tyc zriO}E1+@r(nHwO&FlS&1y&M6zj`)hIB2!1!=B~qwRAkFIN#zL$(JEJI6A_3@Wi4Dr4SEuJ zNvOr%6$-v>h${ysi>F7itf(}L^YnTWKJl@Mt`EhNF2tC4%QoX&sz=hMY2NSm_xroM z-QliHGf9XB#QCM1HX{Nsw~2*=x`-y3?BybY2QfndVVJ2!tDu|NW_=^d*%6D`19A|V zBivII!Xg3q;fEi7`SHEw{SbheL@cJEu#y&_DF-PmF@Ol_peWHdvq{Xvdm;D98rnpjhznHR_2+#Zl8QLLSY_FcqIcxkztU85hMny zaz*axv2U}2`m*C{dYOKtuT7Jqfe1#v5RbJ8x=-KbQ_CN(T_>!LSst96cE52Tlt~eW z9p*P%WhEnzx#IQugz;le<713VOX$x?=i1nKBCi{1@)#6L3|%qb$aIkQBC zQ`JIAF<2_=3y7KB4PBrU$54@*F_Ed7>iK-)X=*~t-2JPE_xRK<+|Gj%l* zQq#yET;1Ipfrt)=-TmF+^!#j{v^%+n*5N1=C02AtuIx)V7ll<+=>^eKL5RvyOgfrd zbb@Z%2FE$;k2?`D2E9$y~r_GX}or~Tf6by?ND3HrJ;mT8{)x>>g}{WSaPWB|ZI zM0k08SuWdmfBNHRUw(Cd zI-buLPC03~!K$0tv#WhwABsfX;dUG}~v^DZ{CPc4*L$}cX_8^vFhZSC1&#$2BlVj$H$;p^q z%1Y@?x7+Xy<3!$`mGH_toIQ~ z!@YSN+~7Sj-o$ZqWcwN_L3N~3_w^VfIclfsEsZT-dAF-hSnnBv*ag5rU?vvC4mJ=9Q)sjca$GUw$`0$@{}I|`0%6pzOP@VIcTX{LzboC|Zv={oNQ zWGF9>Hy(i6~jZIQL#GjdS+Z#_fsC4PXkXCyu0@nKJ@qTP8p` z5qFb)giQpCY!q2liB59N0WCAv-*{toy;ji!8djW9RM4_osOOFx z8mCaqioL}sikYZ#GLlBus2NWW@yNpBVi|%k<$zaMK<=oEVpAOc0R0#bG6F*!Wls3d z1P&$J%=W`Zy^?T?Lz~{9;_x`>nIMO&iHe76Niv$F7R8U9)|JIp)T$)FZxR41Z*kKH zmR>cTybs$PuXc-eu7C6J8L^gv5Oz*(L8DJi+SOrK+Hy^Q!i%Q`!Uv&S$IY|DCo2d^ zM%FE_7x96~2vj_K0<{9piGGMGkq2j+PvW!41YCH?Dj|ylsi?*rGQH$1sRb>$@r4RB z)|LAJgj4`8+bOE<;zpsZn?*S@__BO_d_0|(_rLuv^W5fLYm-P5nWTwKQ=3|wTHaz} zX-%5MRl$?_h(c;4pv>Zg5RqIj>$a_vkn7slCH8^K2%#|m_3rdxk{dfwJf zEUh(ZEjSz^nx-~!YZ7bz)XbdFM7XglObh~Lbqyy)<}fGKsE%={Z>qTru<0HPl;gBT z+8N6i;~tL8hi*;r6TYxMjwkNC{U#nI&9LsA$*^>9g>i?M!L2d=r?(vMTZUOd-HF&> znVnwah<{Rd{ea(tHd=w8)1RvuZwM83LD=$z#nVJ)oWZb^mZ5l9Mdz4E)!R%$A zRH;$i4;~08kDjBHl;{eF{f5L!+*}`-@(=8$xn^kiUS=nFS9Nl8C1nRmYfhnz)=3Dg z=Igeosktk`b=#KZ;zW0^9;8i|x2d%@ zPj`0*5kksjtL8@wW@WgBXh`tk5cBuMC~+^m0+qoTX50h-%*jlg{`AK`&i8kx<|%8>$z_;iAOC@ z+!izAZL`C?Bg5&`Pv`Y=xvc#>Pqe?=-@khEay;nQ9W&bW&9{H~>tCh$`=5TqN5|^o?8H8`>HdCyclhw} zqj$$8m#(RV!hH$cPKdm z5v5x%h1tSE`DDjDz?FzBD*&akYEikv^-_u-gJX}AUkQH)YJe|3OhD3HVilSJ19l;Y z4FfY&b6l8FZjDHSD&Aj^i%Jooau7xsrdbzpC9EqAK->x+FPM)o{~rMfD3}{p;?^eK zP2$V>mw){I@weYD&o8cJ*iV+^`6VuFN&wFKoK&=4W3oON`hPF|kOST5Hm#X*W;P+@>b2Wo?e)vk?-dP`Ff) zCen6?!#vOPypxID@cx%ye*69X>-&2l-PXmtQ?^M4v>x!3ov53xy>DyZmVP>(y6ZGe z@e#JQ2S;V<1a92i)QAGAh9hmln5W)*tg)M?nMmBdt8G2nC}OUhEVvy_3pJRT^{uNp zGlSS;ic*b;SSZ&Mpt6B$?!{)#qwt^Aj;am~Z^#gq*Z}E(lP5zXl%675EhB)+Fvw9I z0_DO@$~y1BhlG6dtpD5w1t82x7;4uFJJOGZV`47wUe`;N?NE)X2kZ_SERnZ6!GmFf z!_-;8!mOnp>c~_m8wx`jB0OVymoV$V>_lL8W)I>Q6%L8S$$+%>!ok6>Bu?gBNQJU{ zk3CS~X@H!+j~h?~u*AeT1->MX9vhfu?Z*rX+qT7hTeqm72gX}YCo{XdyJPm#)6)qr z+xZN~%ZCrmolIjFHxU~1JWoze9P;b2)P{GvX?NH&VY!@LJK)U4$yn@~j;AP5%Fp%> z&1IV)z#?!;K|O{o~Wehu3fZ_|=!^zV=P;_Xjq6d3s)! z_3qV!lD_-mvw6S&`KO?!%VDzNlg{c+L^)C-LP)<;raOOSFf9}^-b7G zA&td_mDhDWJ*|(wJ#VK=U)IgA2(?L|wBx4cTCOfMxt+F&?)~Ki$nm$|f9m+#|MgG*?f?1TRrJTp_Y8xHxzD@({r$tHeQNVO z@8)Tq+Qi&Qnp=;D?rA{=qac-tq7%Ky@eyg}!Ev-5A;%I@)&Vy#dVt%ccD=Z8-0Z61o9#93%#=&zqE!qaf)+5NSo{<|O_+GJ z*&rdQ1qz976E;WV=58>JZfIw?MI|k2z}d2#nFB{pX%Lm!AwnXQ*TbQzff2nNa1EnF z?4kw{HIW8pchxK@ZzNNw=mwg03tq|KP9sV(REvB_s*NmL?Iv+wtj# zvqEjXkcR#^Mp-%bKUB+h&BhQ-O}I6W$p=`GZsrgKF=^_mR8Z0#DdB&G;T7hBHw-LJ z$gE&2FQQocYsuyn03YBp;%WWChUGAmT5MLSlr?{@dd2e^G~fnttqX+1-AsLefn=tl zloy0loNtd*iEg+>8i0TNHJ|a7yvyV&0K&Dhk;uXeV~6384Q`fzaqfID8HHLXDv+)5 zG*z|5GqH%hi(c*eqfa6(qY!+)sgqOc`HU|mfF3`Qrh9|_iEeQpWtu@EnO9UIKtz&+ z7Tx6TStt-@%1NE@?qhiwVybV<2{5;OxlkV9I6s&OU*5=*Ll;P~grs!LLdiwDGm!I|Bn^zBaOx|)6 zRxS9g;)~g)z}C0)IP7hb{eCA*Fl%jMVQI}(k!9zc74$Nwh*FJ+MU`cuBJ9J&l^gQ22B=EZ3uf;I#o!Xuf@h27RVD#IS)31svD5= z2#ymRhZ4xw$i1-1DisE=&x-5?2-h}lSmguhe!PM%@{`g{MxCYX_*}Wc?s_xw)oy!- z+-tpbv_c}|G-X?4GMvs=ca*a&NlaO0P+-Yd4cTKwH}AZeyHF^3cpw;QZDJy`%ki}J z?pt^F<#g7&yGVFgNQjyck;4sI?+b^U>ee?_4N0Cgmeo;3{R&(u(d-_*b}983mE}0a zE(FBgY~U%m*xC&+U@7_db!)WOm;b6 zWKVamAJ+Tb<@v=_#ge;277?>U+SOt1>X+li&4ri@eeHB!7-qe1ou58FpP!cH`RL1K z=hQFjhnGuh^5)HJ+3jD}Uu|9c`SSba=hqMWufO|tTl8`J9ovSk-nE#ke3V;PX7R0a zYyD)T?n=6XOn&|Thp+eF{PE9!x}3I;KmOVm7n$PM<>frjbDNqobybppVfaDltO|qBbVL4zR~Mn-jyAWYvWBpMgoxStpnKuPQ>Kb=^(1eox(LlrK)~4?|R}v z%qJSf5H!fd7w%a>GNSCa@NCd1n1F;-mVHOp7jOWS6B zAr#x<#SEUB@;(X%h&kOjf#&Yy=2k!v8r(;YyT*uG4Vr<-!Fdfazsi6xvAPqp+6D?d z5dzS3brB#Mn_~#MCd&w_R1_lOgMo{PVlN^M`Q}+0c1z+)n1CVsFsN&j3b;7XCiiO01CRX{BgcY_Ow^kFwU@BrnnSRDrw2kHOi-_-J4mEb5Wk8ER;F<+Hw{uLLW@-y#cQPi3vrzP zemQ3}>Y_Jv`ih@#WjEzdpQq!#i=?UOs+2pN`A2LaiAlv}w1W5BKxKJu52XdJh)^n%{+Tqk_$6Rm80@4BQJ5(#UvG2R*X8+Na0e7oEVv6Xhp`UK*?<;triztGXK_XC>9S~CM$X}dy7Ks?rgQ4kBr@?NMHnJb(&b7K>Db7B(pR4Fo+Cc!jhvpmR@ zp|-7EVA`ck+qzzs)A98DaKCTu)qb}t{Ac8Fk4;AaxtVoSnI!XK^=;qv?rvEY>z#;L zWSXaC-MZP_)ts1B%yJbsvrG+~NpL9miwKI55J6Xv+NLR+F`hdRnvy0rDthE8-{zDh zV}JzLYILB+B*qFW@{ziL*3Wm^`qz4>EugQBYoqXVx0(P<2UTv?Y&M+?DY$@+{x< z%~g(^!41iUWO0*f10_nrHFa+>O- zFPHP_e2RM}LZ)7w76s@)7*gfY^pvUcO~j#ybj1~5lncBd^~kYD^tsw$G{jt!^)$?J zY)Vdb9GLV*BUKI$3eQ@R#j?aWxVODZaRE8-3LujFSg`HssG@77(1Rf6XX`XN$U?#) zAVrkr<71k+5ZFm#-pi~1Hm;DvA0Y8@#YADxj}HcO>{1dY#0V*FPVy(eyZ}QS-g0ZP z(N;hKv~uU*UwJgNV8<&=k?FJ(hAi(gW z_4Gp+!HGGw)AG5M@F=PaA}^KeEpBc^nb?gR<#O5x+A`_FAd(y)cDw3U-MmixaV&Z8 ztWo8;FwbeEux&^6E|(9_idG$=gJFJVr$Lj{qFwo>a(}6-@V&SyY1!p^3&tsL3fTI#+uO3*gbT2bPq5>XpcUSk8s~od$Cdzk23=OrP9KHt;w-RV^r%c&pXI%B74g_%NsFJZGl%2^0!^GhWs|(O*LyaT=FSRC!Fk9aZ8$b^ht< zlJYq6?WxR{4aA@9>*(#tlqLP5Cv>WQYRLmUekT@UW-gj0`hrASV0I+ej=_GP}8L+L!HoS^BzxK*FsFFKbTf%GA4#S4jSW zQZx#dop4e15O$A6Mg}98k0~AZFnCi$mrE+%!WpfRd-8Gu5ZJCz;o3V|XV%IVa~pGz zF0roeC4Vts$`&~f*cMcmtXo(1LOz8P$-15W%(Mk z21d^qHjezLVNk~G3a9x^T=j)Gv4ifZ*Q{8W7jV5cy2%R)Q6!^S$R}uXPn?&$*GP+! zk;XXSb+-nLbCh727w#X0qIl@hkYtW*?M*q60+}Ax?vW8#uDw8XB7wT<#}AL|y2kdd zxHMBeosP@0bk*>s%c5Z$9Kj%tqq+93y_=hvF>)9ppAXW%P}q+uG2eiTa-;Ek^kPPA zceq&y2XS_G8m!y4^>qd98NjMoI-NURHoL5@hOV}DzN7p5hiRGs+VO-<*UMt=-iW4p z@!0qqj$a*QMCRIk>+9*VE`6T&WETc`ciUi_ZRc${UAEI==S43UV-*&5-j@FS_;H#@ z2^}u3jrj5Xhjm%M``ceW`{U=!c3HQxvq8=KRw@<*kdq0Kslk=K8!m3Nx^Mk*(mw5X zUw-{L{q;{j|M1IlUVC4?lbP@DcB^i|XQq>NA}wbf17BA8SIPf_VHV$Pxl#b+u-o_) zG+Jf4zt{19y)-ez{pdRY~B2ESvLoRy>Xq10qs!W51lL4_onMU(r*4jp3 zN@jsGUpJ#1;s^#hyh1Gjjg!knLK5eYYY4s0Y~UKp0nJPiM-Cdl$6}VK7o|sI5Llw1 z@3E4~Sx7(-W(Z$t6ckN2JO}_FcHrb^j)qxqBKvvM-uT=y50D$hoFtG$QZ)XCIL`3Imd^~HQKi=8xra0jvJ^0&5{668m%c`> zD<~YyGy%lqr66#vslajD&Yh8<{Jps&sb+y^cvld)k+_Pm91i=t!(O-Lm+$}M<8QxT zo}TS|64gc!a~D?dg;ui@{rW8X{rcwd^7_^5cc0zg-yQDX&~E3#*qp7KZphU`aYi}r zW9qZS0d_>q-($nT$nZR9en^a4vt(v2IolaaB;1Kb05|P*XRcTA2VL6rSFDhgY&sAx1Tx|-8j;w-;qYSW`QT|#F3vwqis3yd2@RZuVA$-dI3e6`# z#o)>Ff2nepZg>2Aq$OGI3wtb8>HHLM=cNCsjzk!GG%Hmo%d-$OCh49_@B-sdC(y<_M=7D7Zh46IB3?s1O7sJiDJ=qj%z#Scp3w52+p$uFZRC_LRXa;n1ilIHFjS zJRB&~uME(t(OMJvAe$>i+jOgqU#JQ5rtO0;3s5tpz>P@fB?%nfR`ZI_pmxTBkHy`L}5oz>7)M8pYQ z3E1!N%zWFn3A~*ymzR_5c%G;2xR_bYYz0wCQT{T(l-;e{df9rndADbfsxrj3zMj_g zY^TT5c0SwXvYn2^CL)Jv*A6@F>$)tjUfrR;eR=-@=FR!^`11b8AHVqg-P_MzZ|9fG zc0pG}M>JpUv5dmpJBZD!yOOQc@c6^D7y9gr&ziJ<{GWfkoL&TNqULVgq)jabY$9Y< z!7JUTUvX5T_#~S>Gg9KL`#FMCunGf%R!RfUrxgXn_@~-}dp!s~4lCW9v_!C+8=BHv z)4=_MLG+>m!k&X}=V89(oHFwUqC!sD^p@#^XD@h^7Tq#r0bu5-O}=$UW<+tXJcQn^ zJ&MVRx?6IchHEKe5CKRUik&tXE zAccsh?qF`r3<-HUH&?RZcCHR8+_d>X?Kd9l7W%l;kod%SyKdhlc z>ZGo!TR%R2Jf44kc|O|l#BTd0YHDC5Hku%fB6IKEo?o6$r^|;Ab~@p+x9fW9%Nmnl zW|n+P`sxTp0mL=U;HFxEfJ;*&=GewddvB9WbL*Wz*!sr8(|!^bY3*`8Z@mXzCn9x^G0_Y(M9kc~8mtKu)4FbONJ~2j zgAs|vc4oPgBKOM^QA+kDU`$BtSAdXZC$C7G2$ZX+lLu;8c^PS?+=T#Wfbik5#yml} z#^C|z#w0+a$e(c7MF_dK)`e1kG8sRnI(1&f`n>*6S_R_u=_c!5|`_oU7 z;3k>W;aGFrXoM4L37PY-%795s9e@B9TVIMASq?1Yi;7(EX9vc$nYM%q;O;f@sfdOxz?= zMu!_sZ4zmR!`=P;tKF`d8@O1UY%a(GnXy|;9fc|O4ky)IqM>f_cHPX0+Sc97)h%jp zNnK?#QZxymhzSF_s+o5+GXp4=8AmpAd zn{MlNI$fswgPQhjV{<^Z^Ro5zay;AVY^M|YX6R}@?QIh7m-YF>hd=z`^M~E;sq1AlTVnk4=Dl>(KiZE4seRnm2ST`&xM}GX?#4s^GynX%U zAHLYuzMfWw!OC|0Mn$oaHMTpM=l2kSeB@fVTgetRNr z%V5!%LiM!cC8)mc$<-7H%dwaO_jG;xI8YYmoz!3+zxo4{Yi7D5Ovl6J=kU)_O-)Z)y$I!#EnAEzHj<9xJ@6~#=C8of zG`cAYSdk+%orz#qo+Nr%P$^ioAaOpKkz9qZWuj_3%$rF8kYY-t@)3eZ7hVCBINv;A zcOn3Dl4$8-AUeO(4}4-Sfj6$=aaM0LN=N0MZ{Wmms(IrhQc*tZ3WuQ5z;Sbj8G{_g z(GwL>f=B5gK9dh=glnh^Ga(8tFmEg3aTMoOE;6;q>T7@_yQ{8bTKesQ$p)^B^y50j zp8p{W0|3b2Y^u~)V9>t2JfF|Y`LcS~IVQ`c&G&Dnhgb8fdvAN5CNkLCx6|qJ^t7Cg zr_)J)`0?`LH*&S(i*FlLGuwwTrzWTBj9s`YP;P2%5}w-IcW>?=_PzJ>a(Q}r+3ojl-n`lG_PgEg zbUGf7M>U(LNt(R8oYu?IRf(|M?cv^}t;;gaZC#gTT>w(k&?#U-6Cv0%wP_N#UoMyP z=@?NiXxq%)D))2Gof|&RTH*ZAi~^OUOd*{*D!>TRNRDqf!Z^BKef9akD@7g&aDD%c z=N&0QmS&>4CI-0z?R#^tKI!4?aHZSwprj__HRl|Dv%pfElrV#MNHmfJs=NXah;07NXv;SDop356alq}qGe?%SqNV`&qEnYlG15c9gM9)ZK4Y5@RI;1)SBlc>tv znOV)4Em_-^{KmK;p5K|sHewB&N|d17T>g+{qJqK9dEwp>7kGgAtc?YP9B)#o16Q+W zdDTxT{yc01WSdj80}!DGWDD7s^`;3$H;70g9M?TYwvXOwe&Xa(QF(b5vRsfrDaEMqHJbE@);sDN+rJhEg9tpSf7Q z=K%Z_p9mBUM&nMEgf~Z+p#Tk3gGkk76WOn+TB=0_+S2HJ^GpB?H*_f~GL8|Y@gLJM zT}ovgpWYrHdHi7Ek{Y}|$?|Q4(VrvPHyPfT(jV?s#J;cb#DBXSc-gUY7WqSJk>FqRuVXiRs&9=)$ zwcE1Vvii1?Zt$+XU$&KYJKMJPa(|Be@WnX7H}h$NHd~~!eLZ$jUHEoXvE6daUiCN2Ba@y$qhNs zoF9rQBFc^L`}Dt8XMRnl0QW!$zsC3EpQuiE^pQqOTY3l@|5y$&?oi}#DS=7=6h=fA z2t*?=q;f?Jw8S3etbC1dXX>MOHZFw1kA3o10GSy|)T9DU<*?=EG6{?DQz7GeIuHp0 zyLseLwSx+X*yL_oXWsPuVmwbS&TYDT*uQ=I@Wt16pM5^xJ+!;o^2~{E)|ch<@#F75 z{^R)TFSwj8PbboibcJhLDzC=eSCac*7fz<*JLD2 z=ku9`-o1T8?!W%_>$)zcN(^ZNkal%9X7X$KH%?Y)BwD$qnz!*J^!(~uFc49k{Rm%H zzAMwy5#g+Cibmdm|Np;6jH`DK+)D03U~*z(l9slY*rv)JK5PVQMoA zO}ly1B}ORbY3-Yub~oR;YGgB(M>g5!!AS!M#T$v1d*uvPiMG;4fYE?sMO^?9NOI!R zJw<+qICc~(yt)>XL`S|5#m*GRoG!z&DzP-!GMi97~-5dOj~o3`ujJ4vo00#mjt z5*)6c6Hfr3f#_2w+G2kY2o9ctm1lQ1vmhbj!$gx?sK~`wTMCs71Qm8lm7By{*GElp zfb>aGZLhmHejIhLz0xKRNkW)g=s4=S>bkYVVSjfBcOxQdN=)-Sp9yAqe13U-_?%dRv5D(<$a zL)p#NuEY#)%%|h=;q|L8{`l47u06j0%~xwpe6!zv`rSVBw_kj*xozD{&lKZL9B&`{ zE;>;&2dNVvE^0E(2dwt;`;*D{Uw`|j&)&RQ&zHyN7iwsYTe(nXu}}tZbptWOoLE8} zGZKyQgsz!AjRT!@aCmL=+iuhVG;7#Y}JtnWT*klH1GEN{dzoV^e#t=62Q@OBh3|# zjy$RU6Ivw!N-g^nf&q;yV0W;z!`+^k{NmfXnf7#7mPtu;l*SYl9K@_WRSwg8*WIh% zMl4UimcpQgSqEX5-R!-%$s_+)4_t zyi~#^kI9YNs(h`JhWq01S7j%WX{eh;|Ef?ZyO^0UyBRUN8Dgs@92||c${rn*tmCmo z$sq?9vtCo+4en-^>%G#cx@k0ulmjNB>~PJDWbj&Xx3c~+>K&Pzq1u(Br7*4}ph|vr zkKHJofQXVtF^fL8@{|s;-WQ3&pzQA7y?g!jw{O4s^X}a{KHM45y}RkQb_jTGySq2@ z{f=jvn!Wt+qh3y;o0~#AVlc(ZiqbB$xEaMrO_8K6M0mfS?;dzJv$R+D59i~{`Q`N4 z7hk-7_wKjfe?ROG^KQPozhhz3etJ2IG=M+;{_%@9Z~s64@Bf#p4G%Lsi^Fp99sF`R zt8JejKAWUX(&pU`z_P3_FUPL>?(G|C)3R=x_SWR9KYX#S+p?_pcXx0<9bfM6@Aiis zIbJ`!{^H9oMB3InH-5N(*iG~MU*C7t{oTX5T+Cr$LsYhfKx#b`GxFg;qVni5AYxvn zb`-}QhqQPKPcfM?i29WrNs z{0iK!uR@=^BVCi}$_3K1QF;7C`2qUVBX~>|##AJaYF)PYCtnx=y|n=AAj&0ggs5CF zh1yR{2}eaU(;IGWqVl@TEK&m@5?bzio4C0#0Dvh?pt%`~+AB zK|DgC!yQKRwBPMji05g?!u$Ph)wL_s!7RevP0h5KZClqKy=>mQI#JBon!B5;sc>^Z zh*XUyvHImHbU;Dagexx=CV?Q=Y)VjHA?gy*w41fCtXzCUju79)AgkRy_7Jxi9ts{S z`$3Z{5@r~AcIHx|(Mm(Y{{cfb3Pd5f0(gw2GRCTaK$V`$o&)b-D^Q(fhmm;KTQY8s zWoA0jOj7YOmPd!Vch!g;(e6S?38dQWaO-u@M%pprXo!vn@(7W07iNUg8NsWavC{84 zwd5oWB~VNVP=YTq1YLoIQ%=Z%Y`^B*`GM)?HdRY^EoF#fK|&-HZJq+aE&Cyfpyoma z&r;aiotZeF6){yaC5RwbfUil4MJ2~Zg`m(A z5l1yyNqb+HtVk?zMP?ETX$7oyKfu%s>=o19o_my<{M0=MvUX}1m>^5 z`I6<`>G}EcbhORk?hP-`Pxo)`zxevAZC#K3>_#xeF1D2LA8UGo+&~vH+g4u|nfUVi zhab1?o4@|~n{U3}zW?F8u12o$49y*2A?qGG2BsbpEMX~}MXoTVNWv{M)|&$I2xlHk z8AhI6!RluHZN;c7qeR8cjA(Ry@vEU$Ha-f5G)$MG1(b9UQE+4}uXF<`>r0MnF3mLp zLDm0HVOa(ib|Z72chmmvKqB*QXMN+s&wAZxTM1CYA%aouC;&27A>1^qr$fjd8#dGE z8A#la;?x9>25&MmCemvTL*x(s2C=P^5Kuc_BlgFZzI)hk zbSom#fr5kOhl7%5vgphAGRp9&k%NG0Tc`u(S#_mq03OI>z{p%N;w3fVFc@J>xKT!* zJQy%>^WKeUzCXPA>Z{Mc`F47Ev#DR6p3djf`S`M~D??J&DAd6KX)-@;J;a^Q(7XeEYY*{r>!-oBs2E`Iqm%|Htm(`1bQJ z?(Xj1|Nh(SH?L%xU(V;xzxo3UJ)XA>%uFxa_Q!|&!^5lN$6g(TMg-HXcri|RMQBat zXuhTA-`7 zW+u+et~rwcqUe4OiAeWkG<@_Il(p2nUN#fnLi6!QALrx7?NY^W&mtQd6I&v`#@xaw z-lQJDXzINp{Bq7=)jq^)tDAhB-cQ|frTY1D6~G508^r?8ua0UnQ_Yi)sGetPp#S*A zNba8~Bternp-==87RiZ$%mbKflpv0GX3A6-S3b)llaH}2u#hT%sZG)vQSkXfh;_dc zGlNN*v}vvifH0G^5(bJ9VQxeeDFkzi1}JXQT9?+iNo!4}sZEl>h@28%23LXLtQ!Cz zTJqcMZJOt~&utQEOe}2{K&Dw_vgSl$y=(6dl}602s!`?aUEMSqNEFIocZBW%FC_ch#wl_h*a=nYFwj>QI!2NW0_mu2kPJ>oQ!*|2o}RbTXB6y*ku)l-Kj?d!cX*Rkv!PnE>~L z1iGc}Z!Irh>eB*Zp8SZKZn=mifmedx_&zs>RKEo?EIvJtZ^*leRP!8f@X76PCoanN z7Ey6_!)FnX<$5|<++AH=)l%X$ev&;?l)NqJ4&2kL6Gr_M#mgoN^vq1C>@8l?yi=}( zOpr~eR2~E7d-ZONG+_KgXy-**V@||y?OR_KIbYiO^zi0&@7v{g+U@tpZ86y4)!pu8 ze|&k_)=OU&2*rL#s4M|X0Fz| z_Z593>mY@8n5wh8^4x^bx6AY6H`^6U@RPC|~Xk8W(djHe=&%S#5Pygk+f9vPR zpB~#Rq^{ZvWl*d`iv?|Ns(qoAoO~7&+OJN(|MdRrKY#nxmtTDU>o2@(G&Kb|iJMb6 z1TtsNp$=mwx^~l$Bl>cN4;_Xyj#&EGtAWutr18i+p>my*>n{Rj;c(E&^)Ej1{&DW< zYRM6M++8Ts46Qg*LYis3xXI`%S|-FuxzVuLBqD`K%m~ENCXy*isl?4)<4iG25X_k- z2i2Cec<7~F9aSF4)R|z*ajxTe7p`{s%S3Av8)vBGH4-k{ME7+$S5UP#(?At70F#VK($(U{fo8P>4dfJ@a;l z*8+s3eJU%1khw9&VWxUOl>pSkCt^TLSLRyJ`4s%5M=XjepK)Z6A+Swa9ls%pt@(2C z7NOr1wW@TI&bLWstewgv*sq@@`f$^-iDGucs|rvbf&A9;+{Xv3bo2^K!kmY@9)8W? zlmi=azAP;^2GyNHq16@c+;&r&1w!k#e0=}Y%gbq9F6ZaR?Xp_$(i%x?huw5I+`oGD z`t6(OF;MO3E7?X+xH=5vZboi!quiHUSzmq-isoh_W|`*Q!~Xu=)ETt!d_V19&pfG2 zG&MLL?(Q#Z|HnUmIjuAuc0WJ9+#T*`=F4iUoPTG4gTyeytiD{Hg5v8Xn z(M0>b`RHrPuv~pTS;?UrJBbHww|UpC=P0*ZKC4_?n*1opCHfDD9Pu(lGFu&s;Y&TQ zDq*|dxT(rOi$@>BAC)Z-)lP?-AEMhVg1BCjlNCXM`;mqOR2tVEzKCyffDrkSDpgBD z5>73k(VZVDfFRX7S(r$If+rNV&d=@R-sM^krO>)EBetH*N^5OO)gKmOj<3Yjue6u} z;7|dmexXzcs;iECB~uZKe3^@Dh~4d!v_Ig?IXvpQcYua#2q|M>iwZ}TtaDIDxJ7?m zxNUckdjlyM5p{HP3ckUz^WMsWV$Li%DhpY|k$_UH=7tyL<4AQKFT_qDPpN=_c@;by z?$C;_8ILP~M;~Z_c?U(3kbCd~H|H#3YypYQi5bZ9v{a<1+%W?pP&q|q&m-2CFCNBN zA;$}=dYSLdp-bQG;7IJVaRIoO_{W=RcIBgFdkC7i;ulb6C2jzMF7sr@l%$w-R z%Lc!g54>KNM!2Vp!*Q!eO5ioli^6S%!t(>=2QYVv%8n8k-hu3(KyqMV7+0v~=v3wG zkZy#-jZ7^}ivXmyx#d4o_B^GSOwj!G_-<@w#LkF%bbX?E|MD|X&0{5eq1=j{rbd7p ztV8>HIr|Gw^W4|It?T{auCMFmau${}HZx(G_IsJ9ZhAQ#8onOtpi2FAQm?$Cn@I z$3OkccklG=);ri^f=DEhZZu1Pqhp(VfDBI~5B1OFT)k{`EJOZJfvZqtSwlUz@o(3M zdqww=&s_o2IJ(yd9p_;r`!$kVF2QTS{Wedi94U?FIzb9@P-*Pcsq`y8WN9+ZEV5iK z>)Pw2vAYp7ghZMn6sXuKRTKvC<#ny$W)X~a9$|7$7fQs4c>{8d1OSNrm{BHXkto%~ zRWh-u!HJ|1$l#@i#FPVBwTv|ilazOZxj7MpO*LEchhM!y0Ip8l49wAD8%?F`b({+h zV5D;Mf^?GYG6D&X$yBQdBa7#8DkNj{$M_y%)hP_$8m zrAkBvb5nG(sA&;fkn&ubTA(OsWQAv#R?NATC8mV7TiieL$_gsKp&RKAsjP?%j0lBvaeX`)QiyCR2O;?#<)-Uv1goF4O$_&BF`qblf=lB3R&L-H+SZ z9II^rwblseY9fuC`ns)`W!u)n{xG+RnCFRfOIbPs`nKu1&GY2DedDfcfB);Ry7k`s z%gYhKyx$S?>(5@B+Id;`hr6ez$K5>d_w#=D`SIzaUM`+VDL~{#@K7M7CK7V8F&iMQ zHD4XPh9X0}e#`-K>R((l2Osr>dMPR4j+;6|x==vSCUh=p0=&jR8}S`&$MAbAu53Jb^4baPcT)2enVd3U&oG?|j) zkN-#$nWDTPt#O-JLMXIx3?4}ACWy&_fc@c*k@qSfeJbf?CT3RYAWN)Et%_JEk1w6t9Dz6KZ%SUWY(z31PFdgR&TaSh5Mf z8X?75sD9Qs3T1!@fCChEpzFT`WR4?5S13Ul2C_(ZJ!3p1FK~tG1M^$9=u!o;yg}(S zgB}BlAmqX(N&VCzt*tj#d=xeB+@4pTBcl~Oipk_zhtJXyKpq@_{JEZlV-)6V!okN0 zB{x^s+CiQ>!iK$$JR=Rlax+@!od4%)q8APTd^Cq=;0$MH`SjdD2B#8(E8Z_%O>|{d z$I?wOtp6q$udGSDZXn=^&)lHh`>N~GpKNZsX_~BenDz7J_~CKlHZ_@>*rt71=6M&5 zSh{Y~a*>`V0Lp$~H6s%9&fKi8A`V@!w*K_EJiU4I?z8o@8No~$X0AkT+6AtvWbQqg zWo#;Qizjz{dfv(FyNA2;!@eKSY?!8<8;-xf{Kxtq|NLM6`j>zH%fJ4QfAytL)7;n1 zL4Xo!>cI)%CkQQ2`&E(8z#7|Hs& z^|~EZD2t&TDtAX&3*_JtQtmT4rZ}U+jfBuO+rr>`2ymxz$xI-(#1-Msv%W*+zz1!D zD?v_7wKF%mhK9T6;%35X;u(jqztvDFf!ry!J-EX`YR;LWIh>H~VTt(5*}B6SWNOg> z<3sr0E#{FZXF%PeMiBmCxcl^bSp-go?h4_kx)A_JDgC7H^r}sb?5EBN+yF8VyDKr( z-gnAYKCo`99-q6qVb;wjnS4_sMw{ero_6!P_I2ItxGcxx)A{AFpY(Jj?F54xEFns* zyKvmlg78=;MB^f3Tb|_Sq}Ojh+ly>{m4^Gf`SIm>>l=s+^z!n2ci7L9xbR`Wn`fD4 znfHwtkp(*#zPS;xfXUsW(qwAdb&VT(I$v~Cw|-C-HPz?GWnC`-wI&Q&E|-nhd7fJn zQ#0*;F*g;FZR^WrxtuT4Zuk8B`1||!MEv`2zwHitH>mnyH``8`q_wu+&Hw&y|Nidv zo8H$~ukKzxeso8uPMXJ4V32^Fog{SOnaq?J^EAU zAq!eW86KjmI4?AhrKe&^Qu~386R}-!+DKe#GAoJxm`O_MeyUb&@{QZWxbXBlXD>sA`*9aZm-NM2}~Y&#OOJ%{AzIf*ZV$XD3Q8BxcXO5z?wyX z8i%DaU5rM(5!&+gy*#xSW0Yr+8QDaO@gn1Zpwj+A;J4ZSIOXF^Qytj&FQa6YDMao8 z2MxM$yi`;L1EV{@R`LIO;43*9_amQ}S#*dYpHni29y&kq&1N2auhtB2?KSkmU3sU9 zWgT3kyJvR5b0q7yvRTc=(%SN(A>JioZ(%% zEz3)r9$tH2T{niWr;EQ@*}x#%`swNAaJY|dK zr#PU@=qUK)>{B+YC(QTEhdu?v;g1+#uZH8w3-fLx+r3hVIpIUeLL|(f$k{wG2g7NYi4#hk6;>UvvDv2bRwn}-Jp`J@kjw}MUVV)D#7pe@Y7}(i z!iGN{zmo^MLWTYduQ;-rV`N~EYevA)N~9aXeT$575@*KF(AspE-@JbK^6lNLyX}1X z{pVlK9~WJ>{(L&UfOMt4KsR^2#sjmIZ!Cm4QmlN(xb}(`ee0L=J~%D4Pt*f0@p!ykF8lp{ci6kC!;a4{=1y&5 zVO3u@z1vOqcXzwFy?Om=YVGCusRKSIL0n_ySv@~5Dl&(vO656NiLU*n@&x7 z-_~vQz7^-6$0gW&%KL_#Pzr=~P4$Wc%@AB7gPCJf9gX2?uk6~ z=u^3v>who^3Sh9+RDZah(?O3?n8uYzEsle(*OjB|o(9cz)QcqKtJhDxfiYxp6?dkp zN2XJ`^72BhF-1>~1V(Q|i+`4>wJAP)G%{CY$nYD+C1R$bO`_U`L8dxQ(=^Y~qshX= z!cC?a2xj3Z4u}9$)68Stz{q{bJ&2On;2rr#WgbLUCpnZP$F?T~6uRER$mig}|GRDL zdcIsv=hLz+>$)xLrm7Y@hGWGSpk^iQAAjzaD|-f&8+=c?akI!N+)AonZN$p(K3;Nj z8FU?}2Jk8Y+c5HUyJgOaSxZ#>7V?f{EKNIx7Tldk9SpK=zHWK_<(M3ttzyjN?kw4e zpU{Qio-@jEAzb+Z0F zRHM0d9vn1=hsiMpgW_r{y{N0Y@|Ynd7DesBup(H!AdB3Syr~ZkLNs^h6Nx`nP>PH& z?@20_Fq797UeD5ZIjY!!%E`PsXERTH!BNc#<=r%Nw|ppVr1n<=3-cVkqf|}FIz}V~ zJ}T+uWipw_VhDlaQ&*2!eCk9#%%rYlr0a#aZO0dz+uh8KeLqhMTrTI`w4Zm=%W+*U z3*d)`dzq(xzObw7=DLN^Qkr!LK*fr*jY&Y{w))y!?eg(yJ6-PX@3=|7Y~n7CsJ5lM z_vo?i=$o6Ib%{bE0mly?ndI*6yNBcD`Ip~jVq$1lW`6qZ`G@(3Z@&HGaz6k1{ZABg z`GCd&v+e*Ondlb?$b_UOIh&no6+?Pst8wXS9|*(ls=d1!nG?m5#H7j6 zH{IB-7m^A}r9QeB3UgF~IEKm!3uYQrh_PN7AhS*^8DPTUxiWwauFBJ1opp1g-Tr>} z`peHBK6`z4zwi5LJwDo*y7s;)PGCZ`OVxlPkF?S#eMUcY_w zaDVso{Ip!o^VHVmd_Emb)zrvB<)ny7@=B|&9uDK#-f$&xAd2?#f-PR|EPwO{+kgOJ zYQhW`B9R;j8Wn&#RyTbAur*M@?s`GG{>%0E$jxYgfI{;W-mRMaP&zDJ^FGN849`@^ z{$^?+b?M>%VQ)kTKa7&64Q2}s{ zwjfQ%Dbm_BO_=}?6E{hLq3lBn*ciCUgcvX$T_n};GI)`AN*4VKh;3a_X;X4Bx0`xp zSccU=T)trZaevsqe*I=Y@2BH)w|+do9FNbZ<7w-CUDnI#ys7q0yP2u?u4t@ z@u02*Agv%0aT1@M&o*GS}i^oXKfCHm%ZkX z#l?9cvijJ09_#jCMBEBd5up>5d3-nrZoq$Bj(ZFPTmxD9Ff|8qt6U9pCz)9pVEmz+ zVl};bWc{A(p5Fw*%!97&C3VoT^tn~D3Pr{lak*tYB@?oQ|9 z`S96cf7rhqkIQ8}pU!u8cl-VR_GH6BdP@YVCLr=ER)Wd(`1 zxLD{sIf#bD0;kt!4x*@55izJqo2IF+Le)n({KLr5;m93dj*K?BwA)FbMpajXBN%16b}bSKJl9x<#DlN{!XiQ>B6ByFHpw(GlWdD@ zEpH~AatkscdH{(GPN8`X+}%IiKRooO=jF0`eMG9(L9V5i-T(*@J-m5EQ+s}XUe`|1 z1s`<}VVF_uU<~&r!bG8W*ZbzZ({&GkXVI=my&SlAy>csFc!t?)unnN1{jy3yg>p_^^J zlUe+gL#@tgEk&+f0Nf3WK^$ymx4m-#`-^?RxEpbpsB)5<<~S;kHtr4b_GPAYjZ-5s zxTVw);6ySi@WMps?6AIo#hjd{wrhvGgLZ%Z_{_R_H)wBUa5dc+oy^P?DQ^&q1@rWe z*pV*#rf6>NMTD7Je|b5+>k8m>I`3y*w+*Vxa%sfJ=aJzm>LsJt*II_hb~sv?wb*2-E3Jd zY8ncG5m(mBYTX?0*qhYdx=pRMCcAk)KR-V|KChQmSzKMW&Y&;|?dAZ9Z@THpUcH{+ zyk1s09*ODY<>h=nA9k}@@2cH`?aEl~nGjaI8jpA+v^QQAWdgVE=+@`nq!3X!8h57WJ63j>td>CFVyHL$v>E zPZUBLW4@UH5VKHfdV)x}(xY%(7CxtfPIw&(x)DI4ejw*(6C)_c`X~b>W0CzGQAhQN zf^pN$)t#B=d3yckwZF2vhr4;VI~)#=caP7{&zJN0a=9E^yPUTRR_dx`Zkw6Ht8qLe zO!{`y{*=0NQQQpPLd`uF3BxR<%8+GE^DGKyBsL%cIlSD2hhxgu#|V?gC=YTa zVRX!H7wF_1Xek8&_#&qnVJ44 zNIb4OY|||nyE=gmb69T67&iq}KqbH5QTAO?z$=3@%akv|cmOSOaFPQf6^g<>%4OxP ziW_5Iu`z&|6(EetLq^3?p-9AhO^-a8h}bfgecQpY-KH1eNX^w9+BXOb8jQ}zr+b!< zFE6{h`)PkTotJf4TsCE zc>DV9<>|>*YprR>)SAPqcdKp+12C|VsWuiO|Lun#cHe&c?u##d3rsz8fZ!)BJ@}Ut%YIO5Ja&JUt zW#u%Y16K2NJ+J5)u%P)!W^zyu&&(>D}EF)`0G>BG$1C#<_cDf1^H<>0k zaBEH+#alCH5fH_)5ssjvrl!(bXuxx8=DyqSce|bWqla^`oD+6;VG6NiZX(2R>F&G3 z!I`?McAZ#qLLuAc$-tJ7oslzcZJOHUB5-!Z)CW`UCXd2Gd{TDP8zE3QEFNeIHm6r+ z)C}?M^S?Hk_{d?d;K%EEr2|e%J^vS0W98yCq?2oLggctF-QD5-)vLRQhr7GGdD@Ba zyqj6Li3FkOZrlWbv}USXR}eK3ZY`jeZkzS)=3Q;;>Sldgw$pi8`_^-8#2vYLpzN5K zJoMR6MU?&^pDFE*qCO99+7RYrFCax!_@&vtE#HB+EN`*J#N>t<@}x{=B8@#yH= z+NXKyW*~9rZPP_NG4`+Sg&6Mpc{=QN&yAUQyR5zUZQES4pdv}PSgc;`ecQH;DEcj3 zh+H)qH&#<-W@%=&F6-&oce{xh>(`w0NPUe{RyRNpszq^092lKSs_1>4u zLiZ15z3V28;6@Z<#124BqxXP?0B9-wt=nqP-MpWU$8BAAlOW`i;8Bzzq8u*?#K2^^ zi28OAWJJiqCMasH%=!9{H=+KD=mS=dUittk%*51gU{E3XdgFMlB61662M!}9n7K=N z;{@bI<|j=9X9=G$`snh)IMrQ?)P_Z-|>EbIP4F*Y5(~6 zu}PcSwCvVp?U6ihn?_eqR|C-1%}u+ysav)p*{E5jNs&1;WsY4<3!euAVu95L6El|B zdBLlU)8i`+&y;@n@%*O&%d6odz%b=}G{My!U=E*10wIZsb{D25n3x(ED8<^X{^pUX&4wI-5Enl=|MB^szJ2rN?&EJ4S8^hc;l)i0 z>IeWe7iR6->KhZ-q60Objhc3Y+d9E{`97`QiJ&{q5Vw55NEL_aDU? z0k(`bMFvYkc?~^ElOQ-aw7pWkmWZ+osA&2+je+b7c|-l$)EptFN?`nnQ;44Gv2B0> z3)Gm$%hSlntB;HwtcGrph;VkI!C2_J6uro1B98iDW6>tfnmI+M&9+%~m|R0-uF*6= zvlJiU%p9Jm)PnP_nR}JUK!9bYTfLp`6iG9&YPta* zhu&je#|aXXXj}yy&qtmZIiqS6rU?Ltx@YJJgdU(96VYb+aynhMtvx;Ocl*|+)|f$+YpPPVNo`}z6#L=d6I;O(-){Oz09@qA-HqgjcQFqp_iNXWanX?}8{-ELy0uAKmO z7D8izn`+l>o2L12ci8RsFV8Q_vbw=dx2@0n9ZTD~uFK2T)pPxciExu?o~OfMPr~cg zU(V;_<70ny-}|Q9>T2u|W;Yh$DANYK&{f?xCO)4|Q)}kEZ+<$O_H}A3LK+g2iJAfl zM0(}0*diLCFc^ix!-K1^;!Qj%uEAnip{rqHx5=P0Atw9<5T=3C0kjdA0&PGJVwvc!-5(Bz{r<4u9S-~b z^UKTWbiAA|=kvMu-n#}-vGv|n-4X9-8Z*$*9boP-QyNQEL!!tst5qG?e~>!vxzs&M zpqUW3=aQK~LspK-2b!;}!7Hf^s9Y&=ykkBdO^1@Hf*C;K9u|P;5@bSS5-P!{E3b&m z(<0XlL7B9-FbgW0f8dmnE8vqa#b;mwWjq_XbMY*hEo1Q6SxlqrM=qW_->z=bc&Dm{ zR6kytW=(c^;^Trmd_9^OEj$wHkUx*fMNeweQLBK8*Ua-A=;2B-6_rygtTU&$oYytP zpOT8WopK~f`z55tMxAj-2%N`VVMsmWgjk$6VBGTRxdjlbu&-I74@7n)Fe(>IS3}Nz zbA>mQV&oO^_aLX?Zs^^i02!T`CtEN5a+!e3w#u}-f4F;mdO1G5%pmV3fOfr|hKuGW z0?d_SpwnZ2j+4NQh>7~@3qSq*a3s=5KDT_LnB z8}&A~S77?^{{0u9eX+bb{{H?qV|H`f)OC5m^S}EK-~HuJfBmoj_22*3znveS=V|Io z@48k-uIc(**;v;F9=wIOHpw!5{OSGOZvRjJ^q14i@#Bw=O#~vY%G^AAw7^XI_-GrjOlls;P9C|v=Xid;qril#@CKmD>l$bh-K2%84tjq4*3?iOpX$w6>(19KyAYaH|R*453ZF$?cnbN6|-o2GW`&pCP0 z-3d|Pc6X6B&%1fvYgZQOoAH>_br8ukAKJXz`o+{lWSVz@xZ}!Lf8q z!g~fF7a~FYFAAn+ATm(xMy!UP@b(}uM-e;M2@(t>%&y@~f)F7%@AsQ_fKTW1 zwyo|kP1?-Odhc*kRgyN%WTtR4?Ql2iIbtYe1~Yet85EMGsEP#nVauC&b%~(d%W$#C zcKZq-C|~FmYT{NoYHOT0jL6iKcw%z6ZRgX=#}9|Qd+4&9`gVFj-y8}zGYhJiGC$43 zYIQ1Iy{NGa_t$Xd#1aw8Ot7#- zeS8Ahwr#)s^6S&nqnjBq7^Lu6J^+}RA&3&5WxwC;)ZE=wHMU&ENR=?%KRg@`hc-=H z-?nY-y)!peCs?BNgw zdXn^jt84UEch$q;Ftv%{);klmChp$1SOO3`@5wZYV2TP#xUU@Z_DpLji$1~K(KuIE zQ=q)*qb77^8*kW=`rq}3KrPM*MX;_l1nHg!c}Dg9Woz|qc|oAE*Bj`I;v>_sS4uMe zo}o9plgOlGa&>x#FuAgw^ zOoVG^DlwZdF~OKfqM0Nvrl14j1(U+LDS-tQY3^AXh&m(iikZ06Rr_r7giyINOj zH&}||0Ot1xtT8x))t!uJIJ?5Q+}hpU-84^k_lMKV>G}ET@$vcj`DtC(ZQHu)wr%Uy z)q39|(kNM`*Tmfd9FRqbh zc;<|XRIYcJ_yD4j2t*R&<0WVqa&kQp;`|Dq!wx;@c^VL(ee6!i`Ts;56p3<7xW&2h zaaSqbP238Wcnt2b<}I|EDtwGcSnjm|LLFp%V)1% z{d!)gale3~Stxpc%~h3Q@D7-H&mw#8U6^B=+R{#8qKU|jW`T3FE$lZHiUyeuD zUk>xvUw`$@cYprnAOAQ#9$DDCmvx2V88n}?bv3%EnMy-z7pEWj`@j8P|MKU*{pIrV zuj|J%iBMxUm^Luny2HUvPR^c7<6;5|5_3}{eJo=c+n~yRGJm{1GS?^Ng4I;oW}E60JMT05h8@!AS_}#lT4&Sf?iKZYF69+|=f223&hH z_t-k=j+pyuZCbY;TF>IJ=rf*VcYn9LziYd_pU>19NQhfR*phv5Vj^i2odWX>-?}L-xQlO^5q^-}=^dzPq2N_Huf0&?F3ZnHq^egw&bayxW6W=6TvpeT(UY z+}B1dJndxKIjs)k)(-avUuoLUFym=z^8{5VGJ?b;KAdHczC=tiO)@uL#Z*j9sDac| zGZG>kg0p$p;1B^z{Im;$Jk}jJ=r)J5YzJc}vIx|Yt|+L^0RJd9C5eBM@KK4TnPO}= zq?sv23(6G`Ns}>Yh;ZadwHtwjqS1+_iABhiz%k~t^?q^3ww!Z5+6q(=T4Psmn;h=V zJg4irtC@;>EEi2yUCqouklJr^b$M5ZRp+IQGjQf$kOsx&@G(P?%xlC4s=83TK%vY) zltyH3-TUHOBU`mEunn$&x@~ZG(vnqnxUDab z&8<%avu;ENJ{_5yp#Jpu`*~UXvYE(-Up_$0`nEnFxAV!nD#ad+emR|h%{6h4yM;W& z<#Or0n^_c}*KJ)ctGi?3U}P@mv%7BFs@enUZCeN2jGeG<+xdJU z5DB?i-?rb*7uD`+`4rSTdO;pUgISEHAv>t8HFni)S%a1qjs!C!DgiCgt{1KYJe+4z z;+||Ar1FFa%yGjb4<1BndayyvRe1C=`w_W|?W8u42qpJV75zR<7s?}p%1ViV8qB{k z)2RG4dAKmfN1Vh_5fzAxv_{{gY)&UVLtuaUwh}P5u(*mz?9t`b8b7q^+}0 zF&_mgUQ?o_sy~mI5$LKC>DMYz{;Ne|K|mL8gyb$u{=*Gz28DMuxA6g?2lJp_u{Sj;UHU0B}^O6{zw+EHZ-0fZ8#)_@?KEGJpX z`UW<2H8<|7Dr{XRkzJc+@$GyhH`f)`U3+k5$l5v?Ia`t#$yUdGGi!`f;+VKeA?yZW#@z){r=(i!|VI6zx$^DeYret zFeM^a-7d?ApMTS}fBnZV{_y8N{@eNQm&P7BfA`1l{^h&B|KI=LuG)OIWo2oBl2YlLlnkk-6PZ=ceMUer)UhY3BIwQ837e{} zo^X(PtIDmhyXQMpCK^d}n4yhc!u&xa7UYZqOKbNJ_h$MM{(BN~BjW(;7>x@YEi~jp za@aq7_NL9dK5<|B-R_|4X6n&`;6e0A3*yO^Vm=AERhMJJc9Lo4c`x&w%okme*c%2S z5sMMEXug7bC@q7WgmRI56B6mBYRW>=c-l|*Z(hCo{Ii$i@#*>H;q4m{p^v|jh-H$` zGEKzIT&LZ>wH>kS@AunwriIA~Mgju3P1A0-bD+Dmsqr++G)-jEnwj#{SSFT>vX;n#u^S27sw~7$}qq z#v1g-?@-=%B=|r$qg1J<;#oK(1Q{MVRDdl>S}?bH26|4>?i`P1k*EOVLLm^T<;qb2 zyItFQHuy4g&ch%?Q)78hjPIMfz|>SswUd&&IGH;en}eLyiw+8O#FLAJVCUsTt1Wg$3|_SHco^J-w>iRk#Wc8whaZF%0%9A;Zzv2`jO zVAj{u2G`gi8F+x*`?5X(14hj&nGf207>@V(Ws+m`2zh=^p zYKCotgBwQ->sA@&0Lnl$zZEPYl_V`i%ZZ7Fr#36x9Lu`GbX^u^YQpo>LV5+}jd)0N zg;YZIK01=38%dRA0vF&VQ;ty$x`Om@m;<1QTkcf)oE*82z{{VXxu!yTo2{rW=%jdOk^Sx&zXtyZfoN>HhwH z{y4oHcbCh#_rCAz+E?wS+PB_4ssI*&2%(yV$OK{+n3l+aA-E8ag#d^Rm!69he1LYk zQlhZyzahL5dDRqAfGqB&7fgz~?>D)vXC{Os95U*&JB;Ia&b?eU`biY6NvTo;#tFoDUCvvx*O$v@AOjA6PLTDgv zFF19 zr|0KAiJtMxk3ZkNx%PLN@0rU3+FQikA7gh@io2;599+Ruy_ zu0(S6QPIe6Su=@qnQl4h3`A-fj;qo+8aTcQ3FR~b)i$9@07zKkn=>8Awt`d)4|gps zWkys@gm=?K%+u5^mrI7ULBM(7Oqi;9bImlsRaLun$`KKV8M+!dK(bkmFOH7R^x~BS zVBn$*5R9vz%%B2LbiU-nxo%c0$_DI_Hm7)0Ebr*#WTsu&trLTU;5lEH7eQ_g&q_+2 z>++i(4R~JEPb4XDWZ(fv+>~;!C=`Jh)Ir<;B6BFQnh^v>*^+S47im1NGHkWm`e9ev}Dh z_}pTs2co^Qe0(kE6H$vF^eW{iB|9o|H2#>wAVoGOL_u0z#MZ3QFAI|-`$$*$f1(uVVwRBlN%~*7z9mV9I!ReGCaAlZd$eK24dP04 z?2{Y@pF*-qx_ktt5Xp+6$^cQ6&@tYQH8KqYgCiGB!zUtlGLPo`kcKsLHZxNLAvLa| zuZlBF4#P}BbOhHrl{w~xknv%bc{jHvVBgL2=`?Tax~|LCS2b1dx~Zze5t4ysP*ZnT zRdbDQA4~o}B#kQpsDieR5rSeZF#x@XSW$p`FN+v2f`R$mVZ{Rufgp1qd2Bp{&>_tV zL$(0Hojr3b9+n+GH4SwnZ>UV73^I;%KJHH*^60k<<42vxq;bw}n? z9j8@2EZWJrz8Dxks#0XQ_HO4#3u=n83l6c5p|npcy$NSQrkO@Z3hntMlR-}VPCnws z-%$la)LRc)0Ujx#jV8#fsaEwnlAl;w3&ez37$Gs;IQwh+PQI!;vsvGWJv2c!@4B4f z?wh)5i9p6jw>Ww?ZI=#^S)@*x6lJsk!K^Xs`TX<$`H!#v{Oz095AQE$1ne+z?*QD4 z5nT}qHd9Ti-Qinb&Zn8+#^3*+|MT5<-~IWY{`T*G|M$!BqaEMy#H`<`}Qy2 z9S*zi|L6ZU-^fEC+^F!kyOFDPHyDxV%5*-HDailvA3q%ScmML={>$^@>GvOhkN#=% zb@-bq;yI#)l(PS?@II$LJ0hE{_*W;8GalrrKtwOJnzg^ zwIhaA97Gd{m&>ANA`<|T7NNUM6UE(31oL@h@@<}=YOjDa; z0GgW2^I^K%9q#U5mgC{>{<7)TyF2E2_G!knYx5isd3SfW+%0XITupToVeefp3yDm3 zJ125eCNeiCnxacM05Q|FpOx_NaCbQDRr~33nGQQJ&-?w|!#%gQoX^fY9S(Q%40m(P zyPZ1`ZM)rJYV%=#55npB<;c(XhkId(;V+iinc6(xzj_65h(YfTySJac^$#G!MABI9 zUccJi?R#If_cn_Vvy-i>skhx;rg=K-r)>qOuSI6LwMkf5gm$~8%etK})6__0>8t8y zYEC5cL}oh9M~wJBR>v1G%Jbn^UUqQ{WF`a^DD?P@Xw@3u5&Vfn!F+;jTud z#*=e#y}Rfe=W^_sTsR$${3(T+6>nrbb{WWJ3Xdc;xHCl70UzerJ|ZP7Bq| zNR!Mg2EYbM0K{xbO;1*B4KZfuBbQD|?kfhhf)x&TB^HYso)DZ|^R@;eWv1vjviunC zHc})95iy&q_D#1x%<~#kBFmp|8_|h$nFmN#x4oU;=dz8LBcse$K=jT1nqvg zERGNFe|dQG=I;LPd^-8&Bn-{Wu&fXW&1<-K6Bak$jwd&p4~KQze){>BZ@>NXAOHOA zPyhJG<#;3`a@W3X$Nt>>{O;|$FTXk;&mZ6a)-N61Osz=5$eIjr7BSnzr|ohPcQ8Hw z`tke25C84I{nzjQ`7h_wa(O(5Vuh(YOS(>r8oCf-QOV$FkxWUXhe7h@$*KsLVI)CI zT~R#noXkcMDjwiPqAVs+E{Se97HRWr*24jWEJ^gNKffG_gN2b$Gn2VZyZz;I!4w=k zcy8Q;0A=Df8+hHeb=|tTIZUYC?Pl2l?8I$1-|g>AXqwwJ&-*s5rxQtQjA^&uVwbu* zfH!Ym`!|1l^Xj#(%kky-{POg0e-HQPr$>=?e}CxORr_JLTQ8U8vOGN8YwsT)pPXnq z?56$Bh2DMs?(Y83*Y&WjYwu2Y^Vyr}un&*%g{V`d3t<#csNY+ z^yJZ|fDczDYc<8MK zd15fNAkkda-1^3<+&j^RzCq2%i8-i8rk+&QK$9UD8iLD1vdk^oiNoK=4ogN2G{)p~ zI7zQ*0H;7RvCE<=06`v+U{GX~N_`PQn4&YSXz%P`bROP6X2nuN!XjRPfz;YgqAm+x7%~rBZA2?`5>jF}ZvS$p_!=2p>M#Mry(W@LWCg4Kd4@z`V zAU@UJDnh!uyE0J>EHRCHi~OI)l}8EwV7NE zTVHiuyLo70Cs7+pFzkQ?pf}Z#U;qv#&t*!1me{ks?#h6HhTp6&6t4D!V@X0KrZ3dc z+`Tla!vL;%M5Ylp`9TFGr18`u# z#x^7jDj|VUV^oeB^^cNS^+Qz_16(u-1`?A=XWWDx5wCx@J^gOhT2oT}AZhONoHyQk zaqqe3?7jAEq)(ruPXW=P9#Z;Kt9MxxR)ezBGjOTnpnU`Ql8J&iNT%Q=DDHyiNW8?z zA3hS$K9SpfWTC?Tti7HiLUpEiLXWCl`U4=2Ap@CTaT|JQ%FhH(92(zV3ef1yHBoME z0S;6}x`{M0lV&PGko6KHzi>X<av0hZFeo(HcbBl12osEEha6mgWt8XC7gQiW@J@8emFZ_y;yMCADRy0&eBz{e`MLeDZYJ@7iIDv+7-ui7JqfH3FG6FxxWk zM9v;RoTuGqUwr<;&6~E_I^CtJ zKCY5XDX6zbycki%pl#7Dy?gcM%hS^zeE)klmp5N*zjP^aGvPwkh)9T{8wG5tjk$XI z!tb(^kRVu=5qFax@fs;*%MG%O3eg!ESIq$8j1^;T3K@AB0SusGWzZYD%m1YB*X?rZvZdjO*1f4hA$c;U|t&WYBf}{hYuef4TC+Av6gRNfAjh0?Zt~1<2bx~w<=s8 zoS*IXyZy9#aCWB4vbo!wpPlb_I|EI!5VM$;p%~e`%uso7c52|0^RrBxwsBlNdh`IgoS&SEh^P$XdUbsK_TuXF?4;KE z?L5(}tJQdXd^9a|c6MBur|rI=o~%z#ADmoVUtV5aA04k(rO4bwRhD*iv>r-buU1D- zpGaG-E-uecPiq;jZmu3ZdZ@ZA%XEHndTF@3*{s(`AU$23tXAvYcC)$NuoAGBwoq`* zLPV_zGbxq8K-pT9bVC{<^cooTG!}=Yb&KZO;Dv}wDUOGktW@ggzSimlVy(4QE=&O; zBA(=04iyBo4s|F5kZ~=g)`4po%~TXtxUg4dx-m|1JhaZS*sViW0PUgK#hO6=iTZw;~zA;bt&e7|F% z6Py?UT)bO2nk*6pB^w{V=R7MtK(KCcalj?SRKuidViCJ{$k1ucaMl4{96z{_xG$A+ zV3DZEfA8a|UYr&e*9sph`6&%OIhHXWe8YvPaABg7oxXkqTh}OcUI+P1gI)`Gy^X>kzl^=A_N-7 zk(h@zENz|_Q4xP@HDi<_EP?VJhFd8XDnM!m6%l9*KlkPUW)C1tny8tT6g?}36b=#z zMM?lZIZxF;dD624A}h?1La?~ZVSK{mxM}H!rguJ3k|n*^Bj}3zWl|P#YQL?)^ajbD zciRHbIbB|@}oCJAURFF0>F*H>lqK;C^z-&xl1q$FZ^D=bl29En9MnpmkpHftq zxeynl0%r0|KUKuw1J8AEY&wS$H5- z@hVaxn2SzF&$_F0Ec3iHX>FNYrcFf%>c<8!Q)z9{WhPVe=2~s(Uc*mhjwlUQG=4>I zj#ojs$MQukL{U~0MnhEzhdNrQxyWP22r5S8rO^eV%LLW^HLlg3JbL){%{v7|m6@GY zcXq}(dsSm1Q!`y0(b9m;&E4wkfpGiw+i#vddi?nDqj&S084?w|vu)-pS(erD@skff zzS}L=(+m@9&fTzf#t~cyrUI3Do+~q`>3;s^i!UENIRD9C_|fIt%e#v^NFk|!L>o5Z zfFqieC8Bflmoq3qyf@dwcO@YCGcCdCIwxbDU&;tI4x+&{eh_{X;~SU@6|d^7!!Qh^ zNE2y=y~!VS82$E_!UnQd#-r2Wcy&~GwORu(xO669R>6SH^HNYN={Sz(56_4Cn3QU% zW@xhxwW=)G0;(fdl|~>gr8UvXf=GtRJYT=RxVgGw*u(R4YYU{wJWb2gw!78I@!8R; zkm|ClfD%@v%-ike?oQjx1YMTlXtf$wvb4+h@7eI+;ln3SpBBUAyLUG?H|OW)Wxc+5 z`*wGG^Wgm9ZnwR-xSZQEjKkH{HKTy|`ubv18P>E-+pPf*Z8Gf^U0};HZ?#oz^R(Y? zH#ftG&2IDH@%iR%fA#i)m2QUh`Q3xfetWaow6>PjF+{h!JyZ$UZ8m48$LqocT4CB2 zHEYwf-)#1~?RI;&S>5b+I~CpU_D$yp_566fuFSH??rtX{#B2~#pbTJI9j%?%fGW4< zMWvMebzTza+zNtkWTgWcx7pYf2cLS=6IPxPlPG7 zj)ovG!7vQtSV~cmS}Mp3gb3oKev&U{S={k$t}UbKm-aY02{W*;Kz z?ZhF09gc&UnB#-HZ4BHOX)osj;opM*oj0kSw;Q>H)aRi${>0jc4b%rINMBwQ_ zkAx5`ff+2QMH9ijIX@YrIIIE}R>d$Bs>K$SrKJ)|G$cc<5EK%sc1#up)!Nl^5g}hk zW0sNu$yJ+qdz`lp2494ZTg@cdv;K7__ktudBp+u=y#OFZ&f^EHSN#Ok*2ru|FCpMQ zBMNk3>YxHtdU_#Q{g4Vu?k|If2;MUD>`;ctTN`^U5@cX(6v5q*^+uF?hyrBfnjdC| zPeRVW1+cC06Hei0xn|GW!Zgd9;ngs{gzkhgvy&Q&X3>3+oszD7l8`2svbU(DhwZY6 zy|g~Ygyon0>l_t)kpei3>?oYa!#rxwzzAU%9FZ4Gn0Oefiq1xvmlqx$Dhb)ZBO0Xu zgw(F_(y=@dme2w4H3!&dBKD&g4XoB*-#}S3+0d^$JB(@p-mJWF&|-JA=YCPki8$ zk9Z+drQ&ixP?)HO%48IDvDc9Yk5`D;TvP~Q5;QqM3l+wUWn*U%2dv-kds`8Ozj@38 zR8XH+6+0dA_Q6^u$_UOoQn8d?hLcu+0+kY<(7li)U)W;74W@@d75S*4jsj=3Ox`a@8r)u#X=QPsg{H5A zJd4Yv>5l*t)21dWjmW&giwx=oplZ@unWnZZrh|DVzAYhL@69Yth)4jH1*3_`-8-G8mrtL`G{1lUPMkR_3`3jS z``7Ouy(lj}{D{%aENr=R@s z-Jh}_U?DP973S_wl~AGCa$z32D-l^CczzF2>P?`y5s}?KTtpm>ecUf(emQ9nQdOd| zT8*pqy41m7%)DN&0jRdNEVT~qD~+fvjhR=gk+>|X>ysl8R>!BNI?pqJrZ(-T)*6fp zlc_Rc7^<0)VZB;W%+Up`P1Amw_G-G>+!0~gZe*UPX=$w;ogA;mftcE|>^67vyu5q; zy6va;XD6+-X|sFVUN5`d&CS*M*$HG>mgU|1_ZLGY(Xo!(-R^F4*HpxaYTex4u2)Cv zqoaA+&&zysd%M|g$6>g*xVX8wefspt(Q17C)z`btc7L~9mgR1eI5 z=0)1gP)|-yR^#gS>e`x2Ls6A^x4FH%EKIxYwou{1cXxMp*Vk=XrfFYGVSr27@AsSS z?KIDu%}%6E)1-??YeQvEiDh|vpSSg*s2yP zgy<>KXmC?gR4+FuO$*mT1D7_;(nLh7U^X71Nrv`f$NTV;4%jIGMv@d>6i`Z9E&*7T z%yZ9CdSD7A=7tss%ikc;@V&b21H4V&boi^DfVBt!JXddSw@WFG*&HIOSMpGGW}KPU z>#-D@tR?yYQE++Y;>Bi#*+9%3TAmvH@PY8VLGU&}pR-A6G(2b_Q&R@r`+C-P)4c+f zNGDmGB~_miMM0m%C**YKcF@rw1XpinmUY-3f;!Sg^cu#)1_}-F4WgcMw%#a~TsbB2 z#MyxsfXyXhq)D4avcwTED03!G(?51x)DKVO@^cV;|0_4O2CIiX&g4;|D9R9{hZ^5K zh&}F!TIz7x9q}eMioiW$8qodCIGB8k=pIPGl|RKE(7L`FhtwzMrS+e&SS%;H!EnK! zj1RM>#1Wm>s)qDY<^l3}h1`vob9wqnQ?W+xBZJI1hSX=A761|a`MUV@!6+{>0bHs+ zT0|%-YTTQPO0UKT5OX*z*4T18RMSxSskDq$0>Q+l;QlWP{QT#0oVZ|~+M zpMT6KeyyV~#R*3*tTIp}Nc^~^Muea1ZaNRN`n@0+iOe19`;&57#Jmu3T>`9xnF#}s z2R1Bdzj6I5fD;0lM@0k_aeB}Lxp)k(zM;N-0ryj)z`P$O>i7=82(YfX5{=2EkwOdn zM70_hV{+9N$_^@xo63svy)=m_ZHVu%5b|4Y6`>&9#ig9IGl3S*mJk6Vpbf z*;Z(IIu6+LPA85YnYfWCXpc0X`yj-^ZxS7&%XNT!;jv+dH?Rs zdsY*JsgT1DWt2i>P%+nyOb*E7M^cJRT!d1--l?qRcgDyimBXVQ%ESDd4l9fd9^w!| z%t%$2c{VRgXp8HDXDy@F!XmAXqZ#HsFVc>WkM8a^o4dPGD>E&NxTR{|#0)XUvfJNY zUrSrmOlDzbZMwPLfM^)%cDq@vc-l^dut?J;CbG;kxU_j*YMpkw{oQtReZ9H9VrEfY z)TZrz*-kf0v!<$YcYC`hs>G*fr_*M$?03eb2DEm!EXz2{yqugIGt=$n=Hl(UUYGTX0%=5h4-0ilvwSu%cN7uwtf-FY6-LCL}W;(a+ z&Fwtx*C(s%Wtnz&vMi-xv1Pg4pcDfpCf!f-xR%}2=2;7uLbuaCuvZL?qSj>DPk^ba zEv=TaTCGHcjHc~=-cPQ6Ke)c8vwIS%C@Rvy6NtG~Gvn~t#^gvbl56l*Z`a$UFv5-o zT)2}9)xylEl?zj4l*&%~kE@}2*}yOi#mj*T`xG#C8P@nhDW!6SnW`~!YpoRK%BHoI zM!+!CQj4lqE}p9eAtkiKjF>2xt=3xVSeLm@(xgcn6a&iM=14aXxpU}_%01e>+GHb7 zDTJGGKjcCXCGpZdpx-+r++U{;P!R1)2p930c$UhJO^*re=lz9F6Rm5=(JE z0-Wp4&k*JR2Y+L6bghCzAS((e(x~@P2={j-%@Gop^&d$8;Z}({iye@FlE8>vNG3el zV@Co=B|TAN|E|1-3NRYGkn8MCtQ-*KdG*KQz{fME9r!j;=x?gqlz9PdHQ`(>;Y2j$B^Qg5O)&Q!H3h)V>6A?Jy{kVb$+vMah)Nuy+3)nhu(>_kCPUn>O9 zZ3^R3B9^68#&dMSIUWdUEX+i_{N6z_#RmJvWF$%V&bLNUvf==mf|2(Qr4)yO%^HkNgvk-M#Ve6A!^Z$S`yA0zSd0U(M;;Y&UZy~mEbO&|JTvrR z$51scL`E88#slXt#o)%&f{T!f;Zd3S3V`UKw&O=HhNeXkMp)4Zm*cL4!^pb)gAa*U z>GwTxa0h;r{UJ<;xSNndx`=vrWK#jf*~ssUXN%iTK^9R~zT~XRd+~E1JxBRwR=ma5 z3oi4%kscU})SNLgsERGp%yela0zwI0R*@8D=?zzDc}VEyIX_z*Fx#3xUl3ufxA9F&`zO7s4hL9Ik++#Q)&c2R`2?wf9^ zWJ<}&pv!T=GWzoHhx78pr?5jCqUW*Q2T;az1h@owoSO7I)}zM*Znr!EY)qM)R5fYS zZnxWQPfkzw^K3#?*o)t}6m1sC290hEg^@w$#w)@y&v$oqeI&-;zJC4jC!aj|;Kg>{ zFT071a2^wxzy2GgGk#iufF;A z{Pf{RpMH3Ice_=!*1Yb(1SkygmPCu07D2_h#Zt@aB4}8)@Yg^OCtVE3a|e`%Id?Gm zve*uP0om{O^E^jVgd+eoRTFV0P0cmtF{uzXCcElA#FGOy&tMC|9vgqb^C$c~l#3;5*Q=1phTCjIFii*-iI5|10%*$?(sTE?K7ZTNJme$Y& zs`GY7Ohm=zr7cvCjtyu_Yjc~n`&Pi#pt>|=uIBBn#JcEW%fN*NHp{fgP|8+z^SoP? zO4_U`R78bJQNga=&AgMQ)|v=oV3Wm6Rb?m~B5q8rEeLN2HIuoiYUS!y5uj4aIF9UN zoO3F&%nijr;F2AvOkAr;V=9EmOnS$;dp}F76Qz(r!h+}k6&x=Vrc%f|-^OuOhdPY4 zaP2f=Fo{ecO4%PSKH`m;Ts6|G$s^OrwHCli@d}SZT%rOZ>^#WSyabLE%4W(;o_kp4 z)Q`J)`2^YKA5S$9Da zijNu&B9h+fMb7@?a)Y^fWnnwG}y(1R|RRJUjL?$gj zYTi{B5Q`s+qXp|&YAY*MNDkUm)1mZnTe!qh{joKxp5YK`mJ0q7ZGq_l=h6ZKg zJ9qMCF%!s`BLX4H65`^aL&O6__gZuSGZY^m6K(pwt`JiOgiO0=S>pd#;yeV@V?@Uq zp+1oR^yc`(FFIV{%xocw;wY(H>Opc^ykxV(G^dr;-mehZOr%vmU~E@IzW0Py5d%#cQFAMWhi|#q@*&gJl4Wq}wej}*@v0m~a5>OFbQbFbndI=J_R36o$Q_s8*}Jd{2T z#KK9(0ZU`pk74wZL8kv zxrmc}xP5UgT~h53*~Mao`yd!hq_q|Yk_#tlPy$(VAA_L|%)Cr0O=Ow5u*@*nMw+Ry zsOV&}lv17TC|q0tnW@M;?{`~f(xzrM4uj8?8i!$C_S1f+Ln+K?BC}WSEX{0y&C|Ys z_IJBw+M9?^R5jRsx1~5u)WzP!>(kVhwrIB>Ol+juR2I`#hk?m-Y17@E%u8F^QVKz> zHETS{ICQcDT~Sc_(@NMSn`WP^xb6=;OqCT2}YQHN?vYtqQE)uIM6=4FvW zF%^Jei!`rqP-ze$Gb&ZpTsPCy8ezLJ0HQ)nAaz}9G9{3-My3}1zUIRy;$-;eH&bGV z&wf0kzsM9YiEKbrZ*W-k?3D^lAC!VxxKtE;N@}5-XRk;PxYB>h*{mRNd||j2Vp3_88hPQSf|uyyP?O$2a6o^~E%tCA!DrlI zCQ>40NFpx}OT-KWS@yVD_RG?8qhr`QCfd4<8S#MX;XznF|nd<-$ z)N^{?q3l&jNYKhrPL(W=#|+_@5!IO*vQp84Z^60t(`QLVFgWMEI9l*93B^B5cB}Y_ zjE)1Zx?%&2T|xUW73$9g;)jsaS0xo5<9t%|RpikLFil357&Jj%1pTqu;W4SlV{u8o zV;C$i1z7@RILZZOWI`>WYCpO?qV=tJ1G~k{1{pK+ zSxX9XB^*<7Ng4Iux1J%zPqDJcAbyyLa>{ zre6N!M~DFAWCtL*n%qo^(L}lR{-31GjH1Tf-^;p!A$sKCB{!x81tQ=QZ%v^IOkAG7 z{BYXu_t$%-qLCb@tOM+Eq_dVJO~95#KYj6h-tXUh^$nrH#7lQ2P`Sd0WC88LMHQ%nPW9^IVzb+Q^u156H=AXe zOel8iu4Rsy25Sf*>xb=G@}ig$}^Tc&9!MRb<7$T9;cV>Lr-tun91L0w51P(SWuA}z+T1$>Opt|lVO zFjR77yJeAO5mhrV8ALpK%zOZn)X+q$)v~AR=r^#M;Cu949WBN!tP<_aUlM$Ev_GO{S*O3V~Fa zPzb6@g&D9EY*4 z##OC#7^*`e&Kd$L#D%_FT9Cp)#0F*JT8p#VoYHGfpsJnj-B$hR^9v9Jq9u%S<;011n@?)(B)!Dn2|kQW3^jtye^t_tF+g{yQ8w zl~TuAN;MJBaXD9Mo#Tbeqfyv9N92)}@5OiqW2l6>nq}(Sz(i~$)6y_$YlX=~lO_YP z*JAh}rZk!4I)(@wZ$?bXiuy;=aJ4ZbEO`2WLY9au+U&f_)7Q-nXRy5JWkvp}Bo3SZ zsbpJe7{>m&B4B@}syxzr5Si{Bgyok*XugBbjhR6PKWk`lO?v_Sb9P(D~j zf~#*)B0z#4C`%{OlK?)z&xoU@+zbZw>Q<(V7V_x#D3A;LTso)hh*;xE$x8+eQ1MAD z7XO#>JH(XQREGFL2h4#J?NBo4;2E*4x%)suI(YkFKAC^45G>M+qcf~wskI_#^z#{u?R#-nB`NytPoBBIC`e!=E>d35&R==gNL+sLxeP^k%lRTU2MBH;D? zL;=CHEk+<=#<*Io*O%`vP3tEwKash8{@E`Ltum8Z0hZnL{>|I<@$t(~J~6e6cW-oA zV9o8gB5ku7smd~yfo+-E!l?Ast5-k#@lT$8@O;1B?e)$x(o_j9!|IO1Tgu%q#2{H% zuRsNH&V(U=nVq0c8P*7!ebu-tMA5x|5znyZ`AprjZouQHxwTOdXk{W|NE4X)Y+zG0 zF#<@%tjo$WX>oMIWU8J$BdAGhjfG2rp$!8dP!-jo7H?E-Z2>@qb+LKc7oz=cyUdfH zSZ`!1wbWWn#0=WJg_@-`A}d57FnPhQdWw&j+cMA7o|)USXlu?UhxLgz7?mb|$W;aA z(%zyffT{|EnhC+@XF*j}nun(8?;vfV!qS?`(w0SMrKPzRr*EQ(f&|2>N~S2K6e@lR zOkEMsRG5pI$*iR?^6I#`ZX%_ysTe_An9DaRN?5~Mg)0fHY9i{hBwgM*OY_X_54l8i zz=sGCiHz91@}~|o)KZ7K8i!#ujN{16rMf(@WdZ14!ezZ)F;ScM1oP@g6R$u9feTY9EGmeq{EV%Wf&n>naE%T7j%f>y#zO}8 znn8X>m9Ouv3Z*J%zGSjxE*zjng}JbZW?6Mw)}ZXkkCUk*YIDPyF^`YMQ39!gy&xvz&|CuH@w)LmZEzKC3JAlqbVGIB@;>HtJ~xoIGA z&7jb6CZ#(Iti(|V?mjd!I31sMEE4fgkg{I=NQvnzu@3Tq;$YT%rVKvvrDBEM5>Y%J zauxa}u`4!P40|hODRaB;%L7-~ z_1Ue4$VvouQH^<9CMqhC+Yyo8IFPJ)5!Eh)hRFBiCo;?R4175A3QA1@C^}hn?;?4er6G+Q^j7Hj z86av-697FfwLDI)R$)e+Yo>?}VbKDSRyWS09|R`|`Z?=f)E~%8@V!((Ts{2w5v%}AmnPND_HBu%(hh_?OydOx2&e8j`i+izZ-AD=$^;KkndZ{B>{wo?(* z%4BwZb<;lo;zvLE(FfoEWR_)fbwkSxQB&hIWJG|8NfVy26q*>dynFZV;j?E?pFO?4 zxLl^i06;*(L}*K)s;yC6WsiTA5%WD^3R5m@3P3aO4ss_L0KuZDDo(Uxk^lh!07*na zROyt=i(8gM1STa4#qNHWHT8VM{V;{h9}`zbJK!{CGMMO6nTgd}Q)QnpqOPdUTq>)J zy_%`G9E~a<+~#FA@-j0N9)TE-$V`AqYuc9m&i31_%T;lY?F_A9nWwfaF4zdf1l1-j zd=ZFB@(M5zmbR3(sEJ1{x`+Z)VCJ-|y5J1jf&!TMk2YDF&MHfbTjYrnfq~RlPa?=y zVKQ)4X)upv7HW;+ta)g!0M}9j*f1eRix>liSX?Z_Yr;5!S`YzRYcUlzRW(KtlR6Y4 ztECizE=pv?QLRlG@*{^$ipWq{REoP(C}2??jLafAxy#{cn1~s*mZ4CsbsXwA4nwI! zsWB}GM&{Mg;TMPxE9_7lMf1Ip-Xuwbn!= z+73)&yL-hfTn!~`Hgy2mlM&&f4#3otIVz5ds1D;$E7Yd_Ud?MYp{C3Qh+PxUYDpt6 z=TmgGMk4Y=nR>I53#PIM1f=4ePw5;CTt+#VdbsRmI4m51h?r2oKr+j`P38=~L+Br1 zAtcq$)ASQOg%=Xf_X=S;Xb&)rkj3!V>*zD!Pe#KQ)zSA5iALX7&mjW|u%l*(;?9tn zW|WgbiFpl~7jp-s$xEs=4NV6|=&r^32{ec*`OpP2M4uNCP`pWvKo*o`eEuG6S(v}# zO8dlqo|phrX71rNby(=Pkc>dC?w|j7J1}AcOpLs6KOwQ1h`4~5sxq1Nd8`qF5)%;@ zPXZ`2VoEOoS(%E=?OVToE=g=eBz;goNYM}hs#3B!*gru;hEZe;P)ryF5EQ=4@+g$7 zHEE(^5H(jhr@V^}pxmbcFl_*N$!0$vc``wOB^Q>VIwDZ^I6ezylABLxlPSN@0Ln1F zaHY{l95QTaZRvK0qYOfm9=2ZE=Lews=0wr?k26n^RgALu=;(DE=4U-LZ8$QI;es-_j4BM; zn2MSS2qxZuDdtn3d@j0*&nxg;n16cW2)#TSb0#u;0U5Ggr`tIqc59v`o5>vn+)pD) z5o`Ivs3KIpK{2<1$ZOt2jpI<4djSzejW^&m`hKbjFcYT2#el1q0&fdP?fd-*tKPzC$9(e?`g|SURta^Da{ZX7^F;o55rJu%~s!_8QrltOtOzyLXP$f1;!KHO->A`L*0ywy*a3q4<0EkEc z-R}DKW?32q+ht}}YaPecGEXK=RXugk0SJS+QmIu%mw7rre;^{#7B1!d?4*=oyW4Iy z8-tCjaU2IG7HLBr?zWrV-9|$3ymtZJivf}xrJz*_hunC$bxOFC-yi<5*9#@qzgMte z_rEoe`hgVc$Sz^#fm{Ulm zUX9{vfMNtfWWTASP3NdU@HPeiIZpo1KXxAu;EP6alAXT^u-|KdIEwzmryBMBa6{q| zE%!U1<}TluW6m>JOoJTq+2ekC6^7z;9#Mzc{=gph$Bt{%hj|=HloV#NQ(i}%F#OMV9LK*j6!GV&sZ z*BJIk=!H&zmlWTNd8}^$`alK00cFq|$yXxLprQ}(8mSxIv%7ux#yzZx1HjEK4Q`sk zRL0e#L+s`MvWKLxGt?gpaZvN(Tl{cv2EF1i$xq97ai%{gYEm$lVoXFumPHLfF;|nR zrbeiPa$QtQsa{AxCbY~TBNV0vLNt;@45Fr1xP*s_6?GkwNOqZlgHa87HLQ#oxoZ{> zoQi^qcr9}GWp02dK8PD@&mOP?7t6(Uv!kYmkq$oS$-k4MVvT$Z1qgj8VV3RrEg`YO z+|WsS=jJu1X^;xaz&^wvKCMfF)S_q>*xmQYufKKM?_G#q6mI<%ktZU;zU5P8a?$Gy ziIABM>LFCnR_?)JP!lncW!mpITV2}e(dyyj$JZBcU5tgy)Te7}9}XYR#UmEZaW*FFKnKXZ(bLFQY$ zp&Zu3r+c+n5oK8~k&ciuNsGAS2?;FXP!%tUu_Pe;mw@7+^1MMgCW&{fsEMtHm(Mxr-Akrvf9D|0P4An)mFw5a*Eoy<%P{)7Ht@JIpX2f@^_Zk%X0@zif9frRd4}u9Y-}O zbucwu)Jwrb+K9N+VW=YnOKT8S6)sRG)VZ$4acOOt=fDQy$=>cctZ-ou87%_t*kFQ$ z_#(!kJb3i*@zbXazPP@@yUX?Q8FQHyd2n|A{^G4$?>Y_y+U+-I56=JUU;Fj9@812v zAN*k*MlSXC_LhKgT-RFG>(w&ve3F|s1)~tHR$~D%QByUh;bgVnZC}28$;@}Rw`%tC z<%^+?*VmU97Z(>`{d)X zy#t32)DiO1I%4dLNZ|_9Lt7Mcy-y>!nq*AlblNEtp4dh0{b4K-I*|e-iCQAM&3X!E zi%v&nCoo~~(N&hDho|Z+HU1)%=4c8`oQzfMpNNX8`S>02TuV5@pcqmPD4}x%l!ks# z4}n@gWoGxzq&vEVD=L8AYNN(|ApPn2Am?*_+`AzsQNCz;V%kD=2G zCH9UJmG{qjh)+2d#6y+CYaH&~VHL7)CNqFIa0Qf~%nNhxZB%?XI|MzE&PdBaUh`wj zU>0Y3N^K^nm4?yEprRk$qXjiLvV3K;v)STJ_zh%S^QowMWuC` z5Q^r0^F|j0omnb@ynh`DYvX)z@+FZ9s~{9@rl#z}FmwAo=wtS7hmwlXX>O4N%x2J>n%oT&P>8B{o`aa!rzJ2Tut-Q+?>;L*$oboaOJG9C>h-9LEK0I%&UA|S>8?jlF z-bWGSs40oqybu+o{pIVoj~_jK{P^+3yQ^8$mX;Z9ftr$%S3*0?^)#D0)jxo!C#JkPNNXH6*$z-Xu5U(w>22Z!ibz`^+Oi=KnseVGRIt2b-H2KmHw{iE zkRixaD5uza9au)rFhE)ZL4mf+WU8&fakQ$DD{6YxzJCZ$XawGk`Yta427)jl5)+wQ zE3>p_q7A|QQ#_`hQH2XHKvB@<#fPKe-hFYyPG+v!=-9y72UR5!%ZY3~ry8);p)tsO z)Kps(#Oj?wSx5-jf$s}U6QL9UVxI|`W;f~`kr7*z>~cnAY3vY`O27lk8gL2)leWl(=dF8 zd)IKJ!!+O|l zn~0FpVdAI*;9j}zha$pY*E2QnYPC8&J)NiJMs7?L@PhrgVhUI13o@z04(kQ(IMBXC zr8Ob8x?Vkg_W0?uCuVwlI)3o%C4XVVFo^2$(RwWP+gINn9j(W4SdGJ@M~}`QJ~qZ@ zzxwR_?ELif?Dpp7?CfN{sz>Ac=xDVp?e^}jwdL&e=xlvd>v(Z>HLKLsx)wS)K0aC> z5wPFySHl2cwHiMB;Q95<%?Hn)Z>Q~0>*?9qIId2P&z`^3yW5-Vi%VBx@eQG5>$}x< zYnxn9eAqbiiHZ-C0mAwJxlA|$2-?BJcOccBB|0oS_y;dG%b)9UMeKUVdgyyWlzY+v zNq&z0mFxkC44U%@6jcp=Q;GAKWO+3LzsJEuT)Y5B=$9>+LickAa-Z`zKgT;2uU(3c_0-ihouPhb+M>7O5rDp>%w_jVQ5)n1i2BiE)e85d+RYWQ34WaB-GVLZ2 z4h4dghXVaPQWhF{HlzDSKR{8Smb0`Su~{PaFkQZY?zDPz2~Z6DBbbq)iHT|o@xr$a z2B`h{!x*_ri%CtY24*fMc5EHkS9*XeBUW98OFTGTaYq_j%#bW?C(Q6LSk;z3r94ZiDgG9(< zrb!w~2{InrPORlV8Q&vX_CCzkQc=pWY8FCeJsJk6z)aTs$8OrfX(Wrd3QxYBT5 z_dH;{vn$QmL2Cl;!5!zM|8T044r>!!)8X|jld9x}4@~gh!mvL?OvYY+T8LgbzB{-k53hQ-tM`Owq`o3NNA}?IXeZ$Gbc$M zJw{9#T!d+pCep@XJb!Td@Zp2=2M=zpZ-{VycKU&QG}LjPm!s9V8tO8&VW<}u*YncO zPEH=4oh#FeXU{+S=%Z=Bmt}r%etvR%^z6}-ajd)D{_5stv%P(Ia`NQCqf+bKmd&)U z<1m%lnl93ow#@UgEX&ebYwgAJXLYqce{lZhmv7pP{oJ0vcy@Apx*Erm;}f=WcYC$p zZW%Ew;Xtw9gXMNOC5hy-`7=PjV}=eLvQfo7!y(S|<92`)fjlz?wER*)fwWLT9ET%7 z=ExFRq{ZyVrvKM6q>aO8W))F7mcQuU=ZLqDav*SzMWy z6iGnt5x1x34p%LBCKs~TzQcBacCkb3>r3|nM~5?o|0P1mt@Is3cDBThnH#JKy^vlo%x*fzj8Qb6zY9ym*Tcl6tqUy6gkh*;unKF7 zs6S{NSO0)<(DQa63T-WHu$rj0CcAxWO@Qjg#G@ANA0Z#iA$C$O;aWVp^MJsK7D*i$ zGpMOo?RuH*YAEAS3!tjmfSyH;&F%Z2V_lk;?z)WI9g{>4TML{%<;GxtJW#FSD9@i_XAev3*dGSh} z0=8nPLds-RjG1hfd8jKTD}`z;vWSWj5u5AN5|IlE!3ZyuOcv%N~SUnBM~{4^Pg| zr>V8p7;vK42u3L0?gz-aS!4{=+d1NJfUi8 zo?C|RDX}X=nJIDc#VX{5y+$69cu#QF{vuBR1>OwMdyfa|DFHh$j(Pa(U~8c&vMgz} zJj>*axLVJXBsF2)O8~GpWdaBdf6v1?$D~;kX!j31{8l6$qR{41*C#c^EE6`6X(`Om zfw({!u`L8MW{}UWfs)7fT!&i6T5EBal7(2LrW8yR$MrNf+8#@o6Z`>P5Ou&NCL;4R z592V@!MZ;+Q6Z>;p)jW6N5{+y@p}?QqgBaA^1+CT0V<|0YMjMO$@V1dM|ZA&?pN*` z6Ua%G;*}0u7+ly`L83;Wxix{6ajbyOAR@45X`z5?I{}4Dg{MNuKxG`)TvyFX15q8v zVX&oToM}W*R$?~M44m9>X90wvFsWK8g;Ct=O}kl|oIX4^Gu5_Ut?#zm_0bW4BF$j8 zcX!@PHnnBH%$JwfV*L2o({WtQ)AZ)eTaY*B*sva!dAYv5IXYf%wl_`i_Tuuv!*e5u zV%g22s=$7lZ*O-*X3SK|ez%zMye!kAM&;p?hi_h8s1l5s>)F|Z-Q9M#xr@P66jDE~ zI(?uA5PWv}{k+bJ>u~rxOStzP+JgG+%S#vdHx8s_7?2R*{{765;_&sJ&q5rbL^y{Z z$sm$4qKxAU&j28QGJltmW^84DdEe+@Z1Ny>H`Z@<__D|z5I}t{an@WDq5l|@Tej@? z^`am40su3Yb|PeXG{VkgH?X(DsfJz(LCw7U)Ri-l=@%j*aeSWD6maoj^QOd{nsm5= zk*9X{$Wdv>di|xtxB_4^4akw}8EI9ONO%wq)G?WB0)zmNnOF=R=D^iHy^N6Q!H%TR zgVMyIxisDj;eSEB_8<2mOW)jH`lEgHZUBf-_YB|SQVj7mGK>7o2y*t_nyIQpq!3(F zTCQGdW&x0lh*(6fZtuSS_U&%hz=LJEi$xt)fCESS2Q&C9oFj*-y4y_DjT%(T7ig2x^QmxF)>(y|wwmsQQ z0znxI8QM}n#;UWOo~(ZK!yin`?&A7lv)wE_lXx+!+e8#+(?P0QO3CVcD_n?}3awYG zCyyS@ZF=+iotknfNDoMcT3~9TT59$e#!4!c;&|7fWnht<#^MkP%*pGTN*TImu}l>jktrb378CDq9%P>0 zzIro^!^6jq4EFUGU&*`}Kuj_%m+vlX9Znvc&2tk`+3%T4ljiM-24ZF-Ry9Z?X}Zk2 zyY21G-IEWWKYI3b-Y>1~iBuOeD=-GBk+N%ZS;hd~{-kk3z^p!7J?Is;>5`#zCYRl& z#Auw1tGl;?g|HP4Uy0mm-yL5OH;Y6loNob(@}3^dY5EFun*>rak5=NyN}JV8q`@FC zwzOUv>T^h&2x3!MXkv<^;!YQ2V)LF2C&yUP2H=%wFqP(z#wWA1tka8q4_brrXsKpb zrn-QI<+60r-7@$Lw9z>@1dHNB#NY!rV5UM;{Hcg*cJ!o~50{wG#Yr?Gkcmp5Z=iI8 zYsMk_9rg|V3?Bm z9K_CF!xf&$kj*aS(09NB##aGpZJwjWfBvxmiuF&~Ni^4X?8t!e0)!9sm ztqAAPdovGv@!bHR|DHG*VP>vX&6ta-!-HZ5aN#=eFf6T^VtstHVs5*EOgw5Gz~UFX zlu9@`SutpuCt?B>Dy6Ph!ZeG_t*HpR=tn%5Gotq*mh@>MQf{urY|NF6RCQVA+uOVS zJU@8+`1QNDGVj;x)%DHI`T5zjpO?0PY_r`=%d$Q>sjHRl_mjxf=GD=9T(7_S_SN(zMr=+V5itDF7t`pJXyv!~A=zrWt>E;q!b8EdIcbTJ->A}9(_ zS5oxP72(T2LxGPiTv4nk6<+{Kr+W! zvvQ9epu}JY&OWK}fbSF*GygHa>gH&v)BgPz2+8idvd}%AhVI#;Ts{Lr9L5krnlZ%7 z*VzY0WFFw4pkx3+RYDunr~jnxh_9Cf;c85fG$ug-z<~qQr}UbS9k?F=cofkur^5M; zyv?B0v_UMYQEy-J{mb58QBxDq)|z+P(z$1H{hGev5fe~44F6iJ&%v!H$3>v*3_V70dwDtSwx&dG)D|%&E_QZ&@(=&s z|M24FlW*U?Qqw>B!$0^p|LecmO*8u-Y_Lyn8*0HYc$)bCF5^c@I=MXVBtNlZ(nwlt0kLNAFXR%Rn+Dz%#G zzR9Df59WE^+}(~tm8BKpQsU$@2vlmRTo|n4ay_M(Le0vsD(fTET56>-mSI)Kk;k!s zXdKsP=QOM)k<-(M51+TOuCPojO_o_?0Yp`_wRu_0l#LhGuFf7lG@|SGm$nFbgQ$W)#1%xI zT!l87C!Y4VH-Sa`cUcsnS8XW10$ZY^jJ~%7Py91(B46zcy1xgTwfj~UK(Vl_xQG0FmQ2_PekTrVj?frHM7=Qtwnq;z);NLLHcEn48n2~ ziAMXZce#;x0O0N{orPMI6ncnENX|1hDRzLUTSyQYRd6*?qsk>lpb=3WhPpoD^}3F= zmbzZ8&X3k3l>$ujTuL2=QPil^_0bWTogA-fEq9v@fN5EF`)L?fbsP&9otI^|hf(2S zI2s60Y4bdR;B6s!j4i!AXJAMsLJEjcWEmTCGpdAJjVTcJt-s?ZZcpPLEHgd9Le|_38Q7@7`_OJWWPCG!;cr z6ay((w5XL@i!9ss?{3C=L|h5=^UuC|^zhMs+KK4t$vJeHc9Vyt(&WH9!qp~xpkbbT zk8;vK$0bVrIdkp+F{jX%Cw#=g_22u|Bj@lF-LiGWWky-1k!puT2bdm+-wp>R=F)jg zn|=o!FLLlt_1@`11_Oy?q882W!TbJ(y%xv-y5G^|elcNux6SBJ!}kOnoNsXUIa>4% zl|o@%$OT+8iJ*PysQ|{&yARI+kir;Ix++=sgV6dD->= zkWY&T_tbox58rC;Z{i=eh*4RMv|6oBP9J~x!3UrIfX4OhZd%Ot`{nfXIGVhjoXJRHwULs}6Bx>_C-CkYlpZxKE`+I-y?>;^~`Tc+W?-!~k4^9i$ z{X7G7A0MyQ+uio- z*WaF;oF1)@S<8BT{Ml!ppFKGL{`Wq;z1`H+@b$M}t;YvC&qO#mUX`&9KK!K+XlIYEd<91e@Ew)_Q(^PNa7?H()jwdoxa(pDLL>Eve!n&7!T;%8<6-zy9{s zZkm4l(;tp?^~L93wM9(S)HZiJEf+67`tbDegY7hJZ??=UYf>shK`6w<4`mZ6A~a2R zSJyPY`t-*?di?y^em_rKj2lFBSx6TGOvK40fWczkM#uyeV~es7cy(V)pRiyx%taL9 zcUc3;nPUllgaHjEOhZn!UrRC*0voG=)x@!;-**dl(+ySrSag5-Kn9II*C34$*u?q} zjfiI~q)TB*P!K`HlN;(t--CKco(PKAz|_q9EhuYb((Ak7pjv#Yg}cLhEL-?ZH%EXo z3rg(wJ3%DKqMgfQdY53&^F{<@GRn-n?%(sQFqk0xPOO&(1PYSGI{qh1>D~xog7PS_ z#3AGrCpD)P}y;Q}*y8=i~G_u#O@GF3nE7P&-1lr>g0Ssfj(*T?4%9}cUPnyycd*C!`ecXzec zv(uAG7uH~ZaoyWiK<`0Y2Z+!8jG+uJ)68fd({y*gT* zuf0g*Hgb z6(mxE2uk1)_pj>%#9vcjbGvs`GK<{VdJ5eI^Ae2U8`3tQ_MeFlVi^fISfw7pM@L70 z<3OQCot1Hq#@s=wWi}Qm)DPGE@iCYT6sF?6|D0Kf7|jb+C62~iH&U+q&Fo|E$PnzMEct587(u4u^AF)tmraFw{Y8@vC3N!9; zat|l^eYArRO)`VYxkQ%}u~BwJL<9-*QFatgmNSaY2g*9EbTvId$}8#vaV-Oby#4VU zDL_tSL|jU#Tn3cEN*#_*&YnCuI)8AypEmnx+AqgPYbLa%E%UOuy}rG;oNhO}-EQEb zpq}Ru5p9iN%RIe)b^ULD@AsZQe)yvw{^;z%+4Gk#{^DQywNF3(u|L%uB`2J6R@{?bE_RD8ao_+88Klm5_@?ZS&SHJwtzw&E8`tgtMZtiX` zuiw0R_vwc(f9JP<%kAau-1a~JvtRu8f9LPD*8cbxKY#!B;;;RcpZ&&P{j1XC zU;fK~^`HOXKfQSN@c;6+e}`;-{pS78e*M=!`|OMV>)-!-|LgDlo|^u}pZxUpb~-sa z`ueNSfBfT5f9JRVI&=9~|N8eT+yDK4{J+inWw+hFefN$k{l;(n=AZuQ&reQH{_Gcj z_V&%Y|L$-8_VL;AfBrB3?aiyV&6L4TgD%s&h#IJl1GrEj5VxiPno?OESt)q@VA^di z-o4lTo<-Vzv&=gnB;K1e0DO#ET!K-f4ni~qyE;pve$mmXV?*A0pQiHNli-mG6t0S! zY?=!|vZ<*SK&PFy^@HTK6=#D+IAvkbJYQF1y03ocav!K=H~YO<;9B+ zKDgWL-pubx8D*LQvISIvBq1gfpHZOZ9T7w@Vv?xoV;2y0o9%Nwee$RhJXuU|_QLMx zRUB!MYdcW-XO9OgFs?FJ2P-cA--{x90^aR;{GNk>a9r$}YD1)v&4t}OvZ7wkm4rfa zUTOC<{VAJ@OArtaQqQ2|x~O!Xo)Sk<4yt2BGCA`62@StPG~nRE15ihkYPNV~+EZ02 zyY)=Ee}@lNasyGA8d;%03q(w&Y%nnbnFtkvnY0$ID=sD;)own|9IhT^S^p#6 z!X2xj>iV=9FUDCQc#j{&QM43g3VcW%Ydhb?bzGf4dVIItj_Z^0`1mjW?AMM@PQL#3 z+hH}Ho}HDUzI^%e<;xelyRGb(lau4!?cKc0Lmh@;SXyhc?6-P-bvbQ!g~~9Lv*VZR zQqc6M*3~d*3%olHBM;-z(Rv)pZgcng?dy4-nusfSySq|TdAc0@h zl5s}o=y0M3Xz>c!0(!3pz@PU(@gICva-gB;m7&8udqWuwxE^YUPQm5oYkBF8%krQf z=s4cVYdfv$*qgFm#nZ(=FLE^ls~Gd4a9mx+#~J$^u58GnOfm*LC(}_K07;NZ%!F)V zaaR=e+)e;^9lW{&G+(PY8SVp;E}-0pU}>TWDPBBcCljx3S664$7)R<6fH}nk7?dQ1 z2x9PEuDZBrgo9zfaLh5>$v<3LAp_7{11HKALXzLeX9o5-HWff!DG%vrI)e94qMDXI z#Aw}J)mqK08Cr}qR?2ih90(YJ6tpy*7g@vvY&nO|_aW2NanK@4OE>5L%|_vnAu!7% zKo0=&L=jOf)zcJ&tnWpdCc6KJ9?_=#qL`nOuhze_A9nLbJ&ypa-w^dkjMF+;Mm0TX zvAEIbe}7Pt>=#SJp?Kv4%&SX@xWr&10~@%hwzjk;$Hzwlm+fY=xx2f&z8xxWuCI5u zx4Wx5o#w%eK@ii~iCAUOP`OlE1RsC&!ILLXzWn;@x+=f@+rI(Ot2bZ$`mg`S`>StH zPFBD6mwx&m|M&lmOddWs{p`>F^qVg~|K6t`SJt2Yd`3|K0!hH~;!?{6GH3 zfAGuCKl}H;|G)i>zwz6$Y-w4>TK?l7{)dlWeDrHS{_(&4-~auqufF=zKl!77@DKjs zt8dr4^YzD{eDI(CDPe2leKpXhKbMGMQM0;UvFgp`HH=oP z^^>QMuP<*@l+}2#+xK_dNj~`Ahfl`g&6lrLmqLVQT*wG)CGL@$%nJ|W(%RL{?W5<< zA3uL`eY>4E6G}xf07~G()S5Pjj06Z=sz#AR%s|Q>`M3^@F`&fx$AgP8*NQRAf#c{m zHvXKY5l(SYk_hgHRb12{d0`ylh4i!Ct0P~W3?`O+0a4ZCHxZem4}(CNc2SK&te)+U zavk-tRxp<$Fo5ngI)@FXU_bm5dwNz6G`uRYdlL(HPFac#IDqhzr>ZS<+7wYA6eYgj zZ6C-ir5RLpS%PWyhcE@$;{*d-tqh4m{Tob`V8{`Y!Hz{(KrugnfM{MbP9Z@Hv0V;_ zPca)|;FwJ$oO1%zO5p;J0f@;8Q(-D4UmPkvZk+>CMLyk_%@kTVcGQLM5K^7$UBe}_ zA!a@1apNCd?WQo7=aZ(|{4BxM`#hu*3FSmD84ljZi_d%+&F3zw=E*V>wFd9;QFRyZ zXD`r29_~Dr1ee+J3V{w@X^u9>D1bOn)b{5?)}C3=!k1ImCfDl)x|h% zxAU}IU0+?jzr1|+?r2qmB*PH$3Zc~O* z>rnxZwHzO>kJhWgxW2hu=KZ}})M-Nh>@5`>Jrp$yF(^`LP?IK&OjWnnH?lr5)7BOb zm1n%bR6U+%*-3)yS0in4X_wBDuj? z?~}ozcL4zXLIkZJCg0#3VYDQ|{VYN=Ne?EB$N*8B=QcNS-Jbrgaii!vU_AH|hyD~d zi$I!nJlWKihx!ng#yW+*0xZ`*^un(czB6bOo95NA=`vlvzqq=%642R$2m8(T&FfdwZoj#` zh3T@Nb!pRfi)mpl)C8g-NnxbYh-nzg2Ooa_gAZOldi3~r|Jnawo#x z=U@EnXFuC+?pEVq5Hs2Db}(6%y~@(sv|0`8_0hYx@BZmO{U_r%{P%z7Z$CUcK3cD@ zF5mt8fBWw)uWpuk{s;f??_a)q_p4w0@+Uw3i7m_B)%BZKug@Qxeg64pkIo;hhw=Dm zT}$2E+&q2u;zvLH!B<~?{#XC(Tc<`E+x2eSL8`ZT90(iD{Z9nA}`lzyIbni$ax*j1<&}#rrBq zn3y1?j!HBTkulX`g+!N$+16vRVv{UnQoY0^)eISA>eET9wU(%pVlFs91~Lok8XXcL z3J`FpxC=uQ2caJuM8TyQR8)mr4oJn!V6wlHWuA}E&QDLzxbW@GZQy=K795k$)r^uW zH*@?a7Kevv;pz77sL(}$$azv3Sy;19PT4K zxWq&$Q?sVqyY2gT7mpr4e(?Cg+vy#ou=kCUsYD=dB000lySUtjC=pvHmI609;~cm& z18D2hctn%SoA-?d~kyP*@b|)i}t)-%gOY1OSw*>*S_w=*YtiA^%FIV%B`HeDnau zN+ma$OGl~GBUlzVSKXW=AcC4ik{ra89iC>;5NeA;npoI`8aM=R?iIb|H*gdiTsTod z1@O6RFajzuMqqd2A9# zx`>8I2{BBJ7z~DzK_U-hP*ah%_++TYxU?$x`i zM;9tmOWE({SKoZAZK=d+wzNh>>(zQ3hV|;Gj^owM&E@qam$KW<`=!x15JAPrY?=4F z*7m#2dOfzbEXxu&Dt{gN+08&E*4W5fDJ&9R(dWa=#1K&?Q(5M{5=ga?tHO&oKLs+= z26)QIK&sj2;gt}9rox3AmsnYLBkKu23V^5LQFj~)6N@qiX0wRe+{uRJpy=;PDu5{I zcFQow&3$qK9v(zJUJ!{{_kVEf6hF#Q^x!Gh05OT(-RuPJ=#brQm&0kZp04z361c4Q zRV5&CMK?=kA@6SnF*PWv;LIJ_5JJn%s87W02e(@&XI?Vfk>wB;c)C{t`X!H=n&85a z=}*oqC`Y3%vUJZOR7~t53T~U>swvSMYF>}2AY%2>AEGF3vBbV0|GWYv*W-!sBb;&= zbmoA)RD5kB`C(?R&*KHGIk_M`Ya~{wpL|Br=xGf@rZ@Hb1A+%Dqhi@tgy;lt{bqkS z%Q1%pLgo!D)7IMY0{K_ss3iS$A8>l5IYrwt0 zEnEoVc%@$px5KvsJ>;s_^Ahxj(XFI_dl{c`d(7C@gvhw8h~?%pf>pHLrgh!^uc zawq*i@p;mfleKx?TwOQQH*el6@%s4q`Lh>SSC`*@@fCr!Ev+?(GFazj@DHT|Qx&mL zwSt({)yRa;e)Y@aC5Nq^-%$T^X1p8GEVy)k*$vEu&O7=$ET;KXJ;pm zA3xmfrfE0tc6${i@Y(qZ*mnE95j}eH=*hDuWK>J}>0kKi(E8t{X4()@`D#`yZzOl{ps6pzx~-?{s;)54gcUMYua&lalOC5jbw|@KL_4{9b{snm^ zQCq21CLyi>xDca=HZcq<9_pZsHtp*$>^65prPK2j=5c>_yDU>Bg<4_PZnSxs)ueKf z*0eS6Z5458kNXwnfh<7*f%!;b02DVD*TeD2 z!{<*Ik?C%$O-WFwh?%Pmrp!OsNcPL^)y>iI@xvz%?{>TG<;D!w3|5gykU&P_t|)zv zde4vNCY?Yv3k*O+uE?2-*xN}XAdfgc2L!sK?-waOt|1F5&<$g_xu7*d1MQHXNB`HH zhIHvecOmJRI+;hwjfbAH1(b95eEI>*Fm z6c~-71QklmuGeHBHLZ@NqXo^2IeeeIQYcz$)U358LDs2?Xp6!yF@*>~OcC&_-M8@f zT7u)49lWZa8dZfxDkS&)p8by#>KM+482Go@wBNO5F;E#-MBGHA>2S1$KAiXayPMnA zmi0KcXAXuu@x73*2B=PT~tY#O8R^iXl4l(c8x4Bx10RR-{8#-$&UnlVZAA!wO0qm zo8$+N74%P_J;pNg&MGznvql##Q4{ahsG199}QJQNz5n*WHXNR%x`fU99jQ!pe?P!jTF#2&?ZJmwA1 zxL1Qp!Qv;6OP`guB?gP43+5vn=gIPu+pu%iJ0f80~f`~+66Hu6p zY!PXqZvQQhT<~|jJten1L&1YV4V!41r_kc|-#du}a_x>flvaYLSUweD;=KcFSEA zM8C5kZ z^7^ZKLGYFU|Kb9=X1uOGj7e#!55H(Mrnc`umNoH8W^3dj`GG+$j^KmX{(v*(Y# z+D%QA%(+(MgF;b$R<8|Yr*7S`!o;)0K;~!792WTGU|%Ut|CG#2hVT7?*u6XIcRDe< zuS#5hSJTW(m_*yXD+eG#W$(W2wj!N=Y+e4mAHN_Ma|S%fH;(#4TK>OB^cL|zcoB~b zCfh{CCRR>Jy5r%^Ie6;Lr#Rmr};d`J%!!uMwy8DSg!l&QhIxg~2>?U2O+ zyigvj>XyX1yF9=Rq{g4Ya%3-CV1ph5WFUqx#KeWElrmP7Le5c$tAQB`851h8|E4f= zq2lo0bMH80f+8#oFO|WQq7(`*7f##bCG}w?;Zg`bdnD>>HQ<&DbTlF_XVLI_>Vm5p z${!pn7C`E|D8Vu?Fb@|=)D+ZIyr(wlSva*FVA*1+V%d7A7$47ICQV?ZvM^V8Iz?o6 zyOAb0*H^<(+dN;rzbr&tYAs|6(MEu&=+qi`7)BymTHEb*%QCAOmqAT0-o3xQz20r_ z_PgD{WYV;0oA;L6ZQIRqY~7OorNL+lR^z1gxI%T!bwK?w{cD$t0e1CCQhUxAPUGoZtg z0}0qq^`USe2wzLQpK$unOK=|cHb( zJf)#Vckq-RA7$KPvDcO{fVT*1k|zcOR3aDq7-JY112UWZr=YC zv4qa8iS@x=p7rj;eB7gcs*ovwB)#&ny3gzP$od124@53ZwNi=h2}75j3*Id5ulvui z(y)HHgVW{g!ipV3k0p|sC&bJe*T1sEv)KLXb=a8v55<-pB8@~bdB?S*Ml%e*xYQM8>vqF>CKzBpMCbL7f+tw+-)wduC6yXzx%uYEdUqq-i9==N^&;PJ!1@|%C{@xw=(?dGemzWUi;`KwLTh@U)u zyxZ-kS-$uEPp9qv=H~A7XhX>Y9^t;h9x&}E!9i}&jRWU5p`#W#7Q zv=e5*!Y2B^2N07G=)FUJo?~`r%%{M;D8PY&Mg4<+E#{CIfw-8;vY&3Qu8xn69zS|w zt=-+;f*gAy6p!i&ze)Z6PDE9LozA3giv<(u;6_GU-L7`nT@ zyRP;5hc6$$eEtS++jfBAymwfzs-kyG(UZmUtAF zJ_$zBRMlfVgkg=yoFSSfSpicK(`F!N?L>rPP6C+{74FnnI+4ES z6z%6OC6P^QS$M7%wNm$du?4>IoKT+kXl_vzbN33US>a-8rYNX^M&t%Ys zdEZo9txP7WOKSpnb`b!JYOPgT^G*jNsa%X~b9=qp+?upnT=PI>X^yU#snkNH45bXE z5Mb&JBBeKi62R4!M=SsxLE^s7B|11nQ7`|h?(T@pRO}D{uo*#(JdVyNAm%c7Xx%X< z8NlQ#MglWXJ%Cr=0}Xo-7nwMQj{-TUp-1)Rtv`TyhUp%AchJ*uwUY$(9e)R~?%A~n z($nT7VDl1Yhj3X0X+~ZD!7TEGX)1lcRGX`l1>pYfHymIF?gi%5?`0SThtKY5YWOU4 zdZIHE_o5FqxYRLaUz_hx9!I}9{msH)P=`l3nmWY)L+(EeRlxNyn_cs*5c?lF`S9We zA=DXYBj)g~41q-lwW$%?J-rC)q%*(+Po>>?J*f zl=lELQ_ZUN)swkj|Fw)Nf2KAIHll!u$qWe)w`6$2QGB{K1b1e0qLDxE&i1>%!|KV;oiQ)A0{P^tj=;Zh(KmGAHUwyrw z+sB`N^6>Gawk%^EnCSZQW*Elx(fZ`%KSZpUP@9>!VUE9886RmO~ zV>Z`qAp=N6udZ)j9zTEf!P9wOHa8nI(y;IBYh?x|QfWZ}3qUT35n{&VqwiwHlmHhG zNkfso3_Ud+>q%Kb>Yti&*a{Ht!#5*LTgLYhq9Z7=wB-T2q=~Ax?HDnHLEY09rY1eKbPS>a zF&(m}Vag9}hwqF=%W%y3Vegw?H-Xs$5U=T&kBH1!zKqhC6ANUkC9;*-C15S;D`3P_ z*c(}hlO|&(f)yqrc;hcqV0Fg>%vlIT0evD!Q8h2@jkz}lqfi%AkhxkQ)WGalQ7Os@ zqMX1eA+wI(f_%`{+>53xO0CPAEr35`oJhq;w;4KQ$b3 zhh{{iDh7?FLL(Rzf@x4{vY3LwXv+d73msLMip+~Pv^IGrgNQ{{!9>)SWtyjH-ZyQc zD)XYIvMlqwC$d@qwbmA-wrr5O3{nJc5m8YGKopliJ(8mcME&5Kmq{H64khgYKoBj| zITM5|at3TLBTi56ev}O!$1^iQ9MrP|eMTXmE6Pw{{3toIprfff-pC}d8JcR#e$`;B zozhh@^Y#L-dFXI4tpJnH@#8S>{lcg5aU<2uQvdP1fSMe)Li_E8stUwRy-tPVzITo^ z?_iQ6$WXx2A2OII8d>GinuloT&Donc-;*g&XjKUmr3v-6n$s(k-eg-vR zP7Tok3}R1C$Lss&P$lMj(BDhEDauKMf&|!$O7?DC8C0mxHjW2zV_0u97%p}$%O`(xzAK+?|O2ir}f_aJj zB9S0_pjBH4CAjsB+80la9t8XIeO+@@1E{*a^I+559daa@!|0nJ&Ae*9ghLHa2fO8r zQm!oN4><$%m>?os>vWg4kxBE)`O-8}ijcxl-I_V-`DXZ~Q^ zFIyNKH!xXKb~sMx-w+=+&XB4OSqNLmOqC!csx~RHX)`r#q-?boAHmC|SZhmLOwGIR z)V$10Wod6;zq!7;SyX=U*=K+F$A7w;rvLiK|Mi29KKSU3?sn>&% zbFdW`5bE0D$p&gvt#arIt4ZNv(zbWEAUuEZ?8(!o%j>q^?zj+vp$d54>^&ZY$kdq3 z!~iXZ)*9EM)BbK*rlqaV&R%}_!R5QF&CPBcx$Wl5*Y5><`tU(B+g#t07!;&Rm8cf0 z%v8+Hl+m=vW*tIX*MgGr-O2?8xSP#I}I-Fdj7hRZpc zr|Dw@GiTD#xckCf@Sp+?1Q^VkkHJW^ng+)G7j-C1QI##?itc^4(I3yfD`yg0(+oHV zWzsJ%rTq%=|f=25q8~+q{Z?zswBK!}{nVD(y#^64%QDYX zAp)$m*+~6bxC3miuV7lhq>v^i68$|yjx>qEezu_qjmrRoh-eUs=2yu9Y9QDQo@uym1+4M-ohK}fPM_a0GRX8*5}Q22^2zaf(ri8 z5KbA0XaR_dA1r2L)`H4W!j~&&PUE{Z(d3RoP0f`_(-v6Q8S7l2IiH}Ka59Xd z`7{={3{g?zFdO+h!{N<@$hB+;FcA?|VkRyvG3bkIYm`E0 z5?OGT4%Gt`N|S^)mK!4K*<)>7D1e3Dw|EMx+{5NrR~{`wRg869XtAbZrp ztsS1(h*?Ezb@$^zvie>zGjpLHErfG-yPtu2e#kTg>BWPS@N4EI81-zqihvtb;MpE7 zq+$lOMrzen--+sK7`d*G*4AV{?U!YaDRvGb3`8{Vr~l>u{f|wRP`~}=^*{PY|7aZQ z=I-YI`9J@^hGCrc`_`82X7|7RfBrF1nV0s9Km7%R-hBJ^5C8B_0NPK}ufF(l90oQL zmAjkUfBrB2^*9bNo2IFbRhpQh*80m|e8q(qk@eB~KhXa%Z}&|^MdtPgD21u~vw!w` zwUlLGt+|nWx|V-QNY%t5TzXQQ8xhZ5WFv!B`OOD z5gQ$o!!i=@_&60={PmTI6W|*~DTjG4)ptx`_}=vW9^6MIOz?63w&X*vqfRpU14_uA zsUwTbOt>B2Yn6c0zK?C1$kg#`vLc*4!ztMrtH5g7S}XIc5_1oT4yYhLO;BKDy(G<` z!3Bl!Rn=(Zyg$W;a(t$U=Ls^CZPxp>EQ1dU^zZ4*z)A2JC?qCa)YxM@h?=O&d$s_Y znsh(8v|mvD)C)SiBqW`-pKv$;wO-<3@Ht=6#_Z21YLp3txD*Cto=Wr(4`>Mc`|UHB ziAWP6l)}YN*V3dG@~K8<-XG!BI}x4uGg~%zn7Mdqr+8e4CO(!s;RKEjBGVqI0V!Jq zFJmJER3!`peDG6q-5WC5Z*FCo$JLq$CZgKnI(Uka3K7~o$ucoRnrPE9)WUELMrqAV ziCpIl-h-Gz%dUdL5DhX>_7F%0u2P>I^etfZl^m`TbEj zikLV~gx;VSk$*U+8Z+ynnna1&l6yw_tTY!6B`E~=0ZPdoq!A%Plc-ejW9rGjQpkje z$eI8)FRd__Qih?{It)VvF&G67gQ=+%E{=7HIfTMEa_|&>fV^=;B*UpiRkY2ssZ}nu z*1}~Nhg!MTT52ewmO`c`QXEf4s>=3vv zK0b)tjB!|VM}X|TO$21atp@D36c}&?9k-LrE~*c}?T%;4L5#v`)SALv7L96Q1$5EY zW&%pBrEuXIoR*k@N|HT?;L;Aiu`o~lVCGRo?+q}GmI?Ky>TrSIDXeboD-A@wb zd|AK$5xgf%P?)-`8k!i9k=E6Ey*jFOHH>w)zi#uw?#Nq9oJ1y2!(L`pg;7zuzPtiR zMR(hUfu${_6o7P@EyFQ#CT6oEpRC6;@uE%4Oqz+c-EQ&Y%u_Ql7!8AuaWUeu-)#_` zhA5=~)pOTP#IU&3{A7!XZnoE=M$FsYVn)ki%e<|%n%Zu6!G%qlskw--ihT3Uw?rsR zpMCZPF*or$%1H>Ci*cnzgP(xbrm_q-S67?6J6)QIuxYc#1XFFxq53v^I^C$-;cNy) z@Hh1*u)0mxyou7#@nmwhmXe>AHBG^yxiPwL9C*iyY2ey z^z_N2g)U{$0B^Mr7ZA}cBeyiw}B5Yk-0I%IcSbl6{W7BnNs-Z7$%4dasr?pMut!+D2J zPKbLIXRMGY(%;@cvU_wQ7sBbE-ws;lX`13W)58az5IAqeIq@(Ye>jE6gZXW6YY~r# zIQwy`4aJ8;Q9|b8?I}j^iN8@A&_Lruww*Q7g2 zV>T!GvMh?25mj>@xd$vu3_vOhK-4L2Q7thVf_Ra}C^2o*3A})Q{$K{C+!adw9~F@x z*SceKcuR}}LtsG(_cifDXavocarhN6DgY_8*7Lu$yWRi)k5*3PL{4UBdR})w zl2w(N5$*=)eE?t{)w?A{GMO0};qGPzC=`Ig=5d5ix*;^)}3AH<# zk*IQ9bzqoTinWHL$_GU)pN0Y@RnQ!)=5J=zmWV~u+@%iLe3_^K<{<$|fg{yiG>V!j z80_+EfeeUh&YdF}VG6OdYJ;;1o;^Z_1p-;iM3{}NC^+^9l9H~lFoWbkcRn-+skOP1 zpVjIe$nusIkXPTJfKDl*Cz)0Klun>=mYnqw%^p`wB>m^~0Tn?Ah|vU; zObJe0GpYaDP+5h%VoR0 z+%B@2s+y{q4%xUx#4!(dpL4qB%phe*rYWA(eu|AJs^>rPK62#&Bx#h!_Vfg0%vLRT z89t9RiW%&?$8;|xAZL~pVgZQ)hV0}ov-gOUfI4wZ`ivY{XeO8I5OUui_xlHf+cvJZ z>+SY3#z^pCYDJu6sNG>snj|i`t}DmY+*y?yv5I!;ds%Lu35*o&%2K5=)+J^wr8F_a z@B8C8X4<)Bg6hCD`vj~cik#oekVx3{9XmwGGf;;+Id*2{sc8g{V-lg6=4+P)fSJ*5 zN$UuPuLMlF!mPQFvS#JW7pW>Ex*Cbssj~M+qfP0WslZ6qB*}p8%er$`n}4pBc3FT+G6zPDg#teZ5h0lp6T*| zid{vJ6-huh?Nwz%3T!kMEJvvo*>)PUY-u!oQANPzSp@>Ff~9it3{%PaoMFOf2VBoL zBNPcjJ0Lg%mfACtlVr&V&@$Qu5qTm)b3Rywl+@Z(Xz4XdQ8g5)>7t(1G^}Y;s61V` zriM4hs3&U!2^5;+NF=VWg0s<7(3>(^Ol`RT=#m!VG) zgvb7IpYs4?`T>vd-V#q)hGXAe#?dhP;3L$Xq^%?RHZUpH7R)o`E_>S&xW>3ss>=4mA(Yxwqi~F1^h~CKk;I zp(d+aMtQxUPCQvqq$U%vPFxf3?Rq+){A4WaC+w6|hOWtQksJ^y?J^PfIF4ELfvTC8 z1PvfSsJP?6VFuMg_NFR;LPEu~>cmx`qGqZh{j%iaWrF}bj`{wMegAm-xC6L;++SW^ zu9wU0df7G`+a@B6Lu5+TcroY<&*?ZQQzDC&$&8YzY(qD*VVeXzCWDXT82kR%_v_=b zT`t=;uGekbQXg{Y7~y^#$8k)CPoExQrWj`)Fc|=nDFRv(10oR{%ba(iK#~r~X&1c& ze%2QX^0*{G0X)LHz%bErRt9A$!stYrhpS219S=f};N$T3kNy4Q@wh)CVhp=pH&ytt zKOXmp7(;K@>+9>M+x4OYMoIproQk7WomKkwZz3@To?TL`FvhQYpI$ z4^}RzqLwrb^~NxBkNt5R)7h>TIia>=iQcYjg%_wg%c+<3Ieq0~BeX)W{G(=oxI1Gi zLIG9Pk-04iN@`>{;erCq zo?9tS7+DjXFUUoaPP{)MiVlkso8~eyVtP1_$0SULTrS&nZ2O!afBsNFrEmZdmHk!- z#JwdLlLsmDR5rqfg5p8I^PXm;^O`_GG(>0LB`=5XO@%r`GuYdW=$9-2DoOrz5->z@3zka!U9P=1zLWo0E%}mD_o7u3jjmh}_ z`1tGRumAF2zs~pj+wtK(W`s3o1&g2s3EILdC=9Aupl&-JbMXNENxH)KK|J6tZpvSD<#e)jqsyWtN9TOY~R+da29HM@`k9 zsIw42I?p_J3YI)ZVSplgEAG?8U@0Th7ofGl>UN~wvS>od2ry2Z(C=7JNEbiBCf865 zVKxnu%8dDySrvS87Hy=YQVUNZtyP{&q_8Y2sTJtD>s(I}lDY;~Dl+3QfC^15Bl08{ z6C`U>%PS=sBB2`6@m~`*ksW>AJdj1J9*wEHAhe=lfI?+6-O}t+rVfJH2;}YnGWd?p z=>X{vshR_)%Dp0sLd%78kc28MY?h$T`qMI01wmJ+a#;-N)IkS`eF}6{`4SN-LT5h9 z8FzOVNKUnA-n8D-R2jgG>5m8psHz0!zQ29{R&*qxVzv7^VGd9rQSpcn!t#u$YFH5f zs@zPp_Zv_mB7ayHGCM^>VpI_~ZD<7A1J-7~6Jz zx!i6qpZ@EA{jWcMy#F8n$NvWqyKJ9-`7#gx_WcLwWKeUO2tR-QY{R~Pd?#_c-Ckcm z9goM`+xN^mdQ4G~%?P1VhpAXNWL$5ziv{<67c+mv?RI;4ef{;fumAe5|Ly(l`-;82 zGD1qgOtrJ1K_15ez)ZsF!OP`xyIej#-rZ-J6+%SQr0E(an}$7cBWee*3I~MMT9`>o z)^az!zc`69aahGTH9fW~ILalR!DgZ}Mg&KKHTRevp6#d|Wsu=c$tK^-NDG+wG2mw+ zp$2-Xs3@{xeVC?bSHYIdKv8X&nGG{hQImMXV}HDV+^5HVJMNFUUAFBKBQ zIOe|Z`<{*dl2vTM?ArCnJRRIo0IdYmgbQ*?_K%eJLJcS`9Nn>cGvDQ&wW^B}R3;3K zutd@b&jLAy`;^FKf*bN-5rBw)&6RzpP%owAdHiKjW zAW=j}wzE1zW=txCDglPi{rB=Yv$1UoKM#AglUI+WqtC#f*~f(kY!px6%F^o z44OM6Z%dD(lKyGCOf4h`MGt{^aNr~8a|0~vF`vapRoHB-v|@%|@peSP`#<;%zW z-H#)EI>OnX>Pg)d+1qRE0e9bb(IHnm9`_&Lf4qMAx_y0lf7|c-F~;`x{Rgg>FTZ{L z{`SV81;>``j@f05OED_C2{ml^Uw?gl{Ph#w_769aZQjcZK?0UGXQ1kB0;Zs5O;_G+ z9W51sq|Pu!TDebaBtXUo9sbvztP@v2iq)~ldb)`kQHZ@@b*oeso_<;|K%&C&Wn!wf zOP;`gea+KK^~;z$KUY$bu}br~=l-dmR{$dAS5IuZIu?M=mtwu3&*=%nQ3VN64>Cg_ z>4fST0e~e-TBQpj{q0VbW}RntoAx6Gq^f*$wLvHN6V%M#I(QvZ^=k@AKx)$D5P7AMntfURR(gY0})l>s&xQTGedqp$E$Z9 zNPFIz{{fLy>=2ZO2O9uXTSxD+6pWgl)-}>nJE6*z-oofODb&I0HP z+jIF>e}qa#CT+x|!l0~YWp~{_rRd8Bvf^tNy67RpDM%F(Z(485uRF%98L2 zL2p|K;n~U;p_1_q#t{ zuGjs(BPc^6T)}M^LJ+XcK7IM}Pyh8_Aol&+Hxc>q$NS}av2FbJm%shvKmPI1{q}MZ z#r}9)uA4zscQyU;mtP_BIOe_|0oZYb&lJCM_uK8|<@GidpT;0#-|r@>s{4M3$n~3tqvYJ=O9g}TMSoVl)3({?;9MK&vC`S-P ziEX7t7^W%QzYR0XR$-`8QBnW`n24!~ilvuoyV|&=08Ul4(^Yj0eR;V-p>Ohd?Dxn1 zaX(BS5N#4$NA&%AlwZ;}~YfVRB7hrs4Ff<9Zp_i={qpg!r8E@WVeI z$9NpaKIe?MY}<=q(_uC)8R)3YmQ50q5OG7qZ_Bkj4tu$+QFa;g6>`_HZgr>MQKEsK}_h}_v6R+w{PFx9>*NpHZ~(PJcCFf zG!;d$PZhgfqYiK%&qAo)aaKc;{PpxkEM_T>dd5412$Ur#1qneLzNq9~TX9c|Q+`Z5 zVt0?Eo+PKVd4i_?Xf`Tb`R=D}jdd~0-mIKz!O5~ZfT@m@j4r5Lwf)nlsK^X6i?n)j z1YtJz!#%&%F0RiY`tqumoqb|6Jx-tsx2md;SyHrEA5=a7g&F&A1CBD91zZXlRHibs z51yTu<%njLpa9vQ9+6^PB*TyW5s~xTMG$k6p(aVJWZ@G4h=yn70E$gm3b)lLosl~s z;(;ROYKT&b0t@^5l&nY<&pELwO`DnnAXPQvKBmA}LcIpWvWTPx-&HuPk~#rSw$Mmr zmMIjqIXx4!iBPE|LScax;wnCm4Qd`svg4c5xL9h3Itu<#wgn`}^Al zy=0bGb!=l?FE)m4YKBdLxqtff`d|Oc7u=8E?c?Le{t!)vH6c89(b3Js3K|&DQqRaf z->m&DhDEd(ClR5teaji4F6i2wc9m@T88zmEF3{6C%!1Tu@rqnVQ6yL^C2OWYFW6!o z8s!%r?aij^WQ$}JQM_J{O)WtJVD*_S(B1kWtF8wnhNxAi!Y52{2I?7-t3V?ZfXWsr z9hg%m@SI5P@`1`)fJ-=APFn=PAz~UGpaPN}8HMBf0d;u;AS&VzJqW=SwSolFJ*)Cg_HMFSEa*;MFpl}MZBgOjli1;O7IY(dK!)ZG$#sV zB&fBUGAg5kT9uz2>JgN)qmV)sVH)N!2J6G6qpx$GYWk7ZYL^r1L!{k`SY9N^% z|9Br`yuQ4KPez#P`^OuJ%Vm3i`|)xAxV>Kg<$w8?W4aAH#9v-6*Xwrd$EROjFPBZk zUtg{YiLYPYetiG!moK-=H9mj(+pmB7C+${_1{2z(hYj9tQjL^67fn{`p`2>2ew0zW=!&haa);hhFsm^}qdZ-+%o0 z{m*Zo$L0EZ+wUKL{PA5?zWnmb$NNVPyh5cGyC!5`o@U^SyZCLm(>D@6sdr*<|e_l(TlBDUk zF;d9aOh++sOkAp`Bh9v$R`gWV%p4@gDHW{;C_cm0G}~=oZdVoEw(a}(_xpVwvGd`g z2UWes??6pO4NTK1$U^uCM(hFe5aCdH*LmCQ?c;GB_UYyFdfS}0G1Np=6gCjkBVs?Y zm6pMIyMXlVa@jUh9ot1!huLvV_nDtk`e;chAS6=Mu2_WhSKWnEHnjOgYS;CpsZ66e zXO^#Mkl}nBb3bN8)d>XMeF6?nRPldFYq@(w%)@zjyg%OGkH7!Npa1svKfk@*ow&Wc zy2ltc4639%8KMwsKofKaF(ZaHljSvMtu{`C*5tjC$C)!bAIn-Jt*wZ|XRyGsC{UF% z3K5u;|7sSzQYIRKefn{@pG{hcaNCpi4sTafVL?H+wmb#BSTL#6k!O$qP(;m4Rr_7D z>%SdD5Gm0?i16cn?8l*E_ubn7D%QCPc0Ms*7@#NI=!WIeUlSY1E~jkSGh+)>=Mb0N z(TNn*loYYz{nFM{kCG0qOBhvB5)qc;c9{xBH8PJ8VoIngg0!5?ZIU-`V^wWO(Rs19 zf+Rli1X!9P02INDs5JT*HM}UYXeX-|(>x;6)Cp*qMcAmZG&w1<2SiJ~gQ&EMS3lzg zvRcGY&^r?q?iP}7^iM7T-|y4Q3TYIDW`+xeaWTq4usXLgu*_dc9kOmz&A!T-BBsf9 zO9&_ZxWB(2?k}G|*=5_}I`^H^3!qfr1CRo26^3S>$ zdQ0A`G{8JKtUGAtVo+7YKu=;78;eoKXe_;n$@7~sDaw(DE!%5t{CfLAmlVG zT$UoGGLUMPQ9FszGh|k*+B>=4lf6aH&=zD)%4-huo)KCQz+}qMl)if~|5oBX z>%4S@RmJTwZG%X=m}(SShi^#;a=IVK3`Th9NG6W%QB$l~t#Q)p5_kgc*^U~aIix`i z0!Hd}%A6_sg=l=1J3vHEcTf~UZ?N236$2+qu@xeq>JAjyswBPr$h6XRWdv$ou8ZVI zgmX*eJ9jN+1DI%Xr1np4vED9Sf>Obbf|)0=EVaLU{pYVgzW=yv`~C6b&+lsb z`ueiZ!(=6nWZ(B;2FQ;e?|7Bpe!UWS8+VoW2yWZ<@$nJvejEql=B7DzuAVwspenf2uRCAF_}pBCJdb~b5h&=!lmK)t6h_I>k9)|}$K91l2eEa>6x3`b`F)3qv%!tP^j(NKc z(-MkNF$ow8`g>(XYb%6Q{TT$|L?+lFghX(L??=hEfB+YT3}-y%;modo%dmm)p|Xvf zh|=J<5KW@)n9~n;Cp{}(JEz?EUjQNEguJUdWnm`vmToWD=3N$+VW-A=r%`}N+hb_^Ph@#bm>mC zJ!OK+B-&-6H=flXppc5H$`usv4k6+wiQ9;%CZqtNtYR69$T)Aw;sWMg~M zGl@|p+cZm(M4%EuQOQjrI=XR_CzDmCCdJ<&$y)WHQp@9!?yQESma$_enwwltf6anq zUP*vL8d8O{X-XxFhVwcrg0Imgl+``b?yf1}9Qsx)eSVCf>af;8Xs$$mdF5uFGt#Rj z^0v+Ol4T=nfjg#<;4Gou1f@MXIGMl@RcEM(#~}y_i3hVL?B(<4 zF~-OH`+k2k1W3oAAyhE}s)mwQl!Q}89LIjWzDRJtKVlo7uAeTZZt5!YzQ2y~`Ipag z?h2^sw&|w&dfSG|%Wd3V^wVv8-t?33avkyO>-Rr?+`nTVGEHn~#2_+eHBrmL4sbe zzakZlXE;C!YCUeq)9+U1C=WvI#pu&l0kgK!cx`h*HGi{)dTM`S?ft zOTxWtfYS!l)8-l230Cf|Hi*`f?U1Z4uW@0P8Hq#{!O03B*qXF8i4576hQ9F%SK1=w zwX|WAR8flDmD;Y20_na#>38X%ilhfUUar^AuP?TZ-+%v)fBvU`Ld-w^@$J*+*Iz!p zsOo+kLTqD*Fy7{VzaRTCZg#s~UteE8eSUqv-@kwV@%Hxaj>r9Y`8@VHw{86Tm#>ks zBJNj_%eEO9bNW6X_xt_v9uYD5k3ar#zdy__GHm+1KOS%I?{DuPaeaBaKR%B6=ljPV zVdJ7^68`D+CD+IOxLoxzwvUhRfB*a2Z-4zwY<&6kD^&jY{^y^6e0zJ`|M&m*|Ni&? z`1>FK`18}}&o7^@!?wrAVlpS$5XBzr|UvG?%p8FL*<^T-6R5V95b?x-YT7| zYSP`kqNT%GS;98cyJQP zc1%x_qpCRk!vl|*?A*&mRm*~iP-RZ~oDbf?h#3q2Aj>Eq zX4~z><~$yEx=%k+;EwKL&{Q!@S}CI~MpA>#=au$NV$Mo6Cu+F${+Rp2f4uL< zjP3gJ3K`?#q!XHKDLR%SvgI+Hq677OvxJNgrRd=d1U7SmSrh`?Hd8Z(WUw)hnj?|% zSa?WDDfF@hsp8=?;(kAVyxkv^_sjO+DfjNyKee!Yy(ueaOC z`MVMkW=XgbkZ{~T9><<@9jZCFC{~^^s;upS|MZ7d5XYZ?zFN?pC^0cyHXAu9Ju;$B z>~=;P9@&=$h*9YtGw2Bd$gat%Xrp2o=6B{4SW}ZIgMb>*&^4@$ahnP5jS|J();_6-tT@fONI_F80#swDWF9Z;m=-*e6k^xf zaTVGZU~G@uZ+?D_r;B*zXX-nR0K(n06aZJ#003b1AlGsTQga#sGPgs_oHDrI>2ZAe z{N?j6zudRW{r(X%a$tz4TDE5;O`A`kEWILNkZcQsj$)3{VzUY_R<+kZ<)62l?HeQu4hrbxN&$su#e16>bIqxy!fYtVhAXQDPDe;XupQ*CSim%oV zT7OqG*I_OSK}lW~suYH(iKt*?n{O7Yq{)@`*>z4EIn}Q=jUklG7uKbxBdPtIT^#Fj z>fW=^6G2eL7UHYZ5*hVy-3y=J0@B%lM7h$jPrA0WR8_AerV3qTaB^L3>s5erNK;$0 zdd!h|GfT-sF%wZ81BdB;*>sbcx&@*UbP|GaA0ngPCnZoFf&%1-NeLFqBV))gs0L*s zEUgWzzaoOET(amGM{;6$i+Kx_ta}s@TaD6;h;VY$R0el;HXD(nj?Q?kNVvk)Wp1ab zi7627bOL6}TY?97g zYwbRs`|+oWp4^We2lm0Jy+)u0U(^mnN63s}dK+VdeE;H*otO`kwJcbeKEEDaaYjYzkdDo^Xup1F&~fJ#!yjJ+wA)5Z@&q7yS}8m|9JcG z{n0`!vf1VOauukW#b+It4Wzq&`SRtLUp@!pU;qC1$Kg&nj`;M;ub{XG=X`zr^y9~O zpFT`M#`NnLw{83J?QKSU{q@)B6YlhRe|x{bzxm_w`ugd18Q=c=_;ibHY!Z%efqlHa z&CBKUFRz#D7@Le?W7x01{c`#1U;h60zweI^5z?oYPE<(x`{q5R^<$ZUq3w|@J5ky} z(oRIs3yJE&5J5zQ(E68Ur9=q~8{05b%|^T8v&^hR`~G|ea#&$bQ_5*ab<~+hIa|9r z{Lcw_DkMzYYYJyDWhj{XiVO}j5!r@aFXMI{hx6`fqBJ^EWt%rZ=p2@}msQ=8qxd+( z1XDOE!SDbT`VciyQ7S?t1wsH3s2}qYkML-I-EY_1?RFj8P*n-pHW88i@i_MVh^*3y zJ{tgPOa@uX-);4ZLkv($zq4>feG&M0Jih;U`~LQE9C1wf*e6BRw%z&t$A^(Gw{aQe zoZU%gS#?%vVB4ll~KkP1SH zi2}zy_s2Yr3^ZzEWivr$k~_DRTk}jWwJ8CbbZ_>df1C_o)=_G*&@_>uEviui8FaB# z1tr^mr|qSbJQ3NgZ{-S`#}bKvNcG=}WPYGB#VTTpk|C1G>3v(sxBowbpSpoG$~+G( zSemMt4-rxlcO%*^w8-xO>l_1?sll2$Od~p`1Rx#_s z6BVGHre`I^>w>FdFmnTjb@#D23lVAcz)ljgimD1YRMxXhhg9QYV`x@wO17YMbkPVd zx$Ast?0%yzWO0I(A8k&pkrmn7rV>;I;IlH9jL7B7RSA}^AgYYZ!@YQ~AZxw^ntWL? zW-WDZ<;3%BnG^Zxw}c&q6gcZ(wT#Ac^$UU)*IeESO z6As8laUos=pM;+-rh7JrCR7BYjBo&CP*iNRN-c^G17w&% zFgDp#uA5#*VsZ-TL3bw*!A&rLE3pYSYqgCMq~LKJKKIxU4SyN-y6LzKY6Q!A0+5_u z8EfDJ>L6mB#nRepLoBLQE|lgXqS&M|Sjql}r@{otL~Jo#O+0*Y4Z&dU`%WxImvlvV zM0#iOR3EL7+(fl9Ek!g+`p4Bs(1{jy_zE$_Q@REn*szTf8z#09&wbi%>7=?=kdQT^ zNkyLqPM+L!1{7okzf~mN?>q1J|M`FZ_uv2RKRo<+zyIU!{}aTVhtGXJJ|g_sr^@uK3+e4VobB`{{AE4n2-B|e7xP~eV2%lO2>-(N4$OivELt`K7HEv zeYseSd^cq}!@oW&gV{eHV<1tOtKaUu(@uM2Jl-HCd5%Hm4%wx8!NrAt1(f zxtdBR^m4zneFCzWp`+Xqr4|7_Qw#;4@=`2Q$rbXGAOlmP=`E!|4Jl9+DI3Lbfe2Og zdK;ho=A!X2Lvf&5Xv+SvytL$Y?7(F0z3I#dnKL}NPiD*iHa5Y*!OO5x4Khm+?KvkC z+9O9eObHPtK}BZ42?r@}9r$_v14%Z!mk$egi1W_1A6pUhH~3o09owfRvvuXfX-Op|z9 zOnJHH%6BriVvXUgA_`|2T{#t+n;vNnRLf$q5|~|!(m1f{MbqpC%#;M;2_9>LDJGr$ z6LkxSJWVcpLUF=wPi%abIt=ZG3Zs!!g{1P>Fwvo76B}8$5}M^PS^t{)cc=hGRCPcM zuqGR*^*`Zt;>~-u>vlySfbU`v*zl4_ME2zoF>~Du(wnDOD8JAQ zMFau51Tc@ukF5IJuD9)W^T;8`Qj~;fDk%89InR7}&XlIM(=J}FR8og-*9)TOMl zhZ8>Mob&LHyMix0yC3=Nmn@Blb1s;Wi^81Fy6-{mbM+Q<8m z@ap6H_xq3U=xDQ)ph>K@WOw&C9{2a}KSc0&9COCM{m=hyW4qsX5P$yrzvrV7)9G{0 z1LXbVgYZ9Mh|eSU1)_uv1x0H;7$ zzkhtF+2iBG{Q%|9Kfi%Okz*dm{T}Y~alC)~p{oD!KmO--z5IUsyZiKy$NPQfzCS+h z5q=Tl^v8XF{P_6(=eO7=1pE8@kMBP|zW)05{rls7pGWPmC$^zo?t18Dk5qk zKmMBntCN(^cV}ASg_Q#*!zyG*@Ji^0|$&bhJIA#!!{n#J-ca?A7w_ks` z{r2UTZM)dTJX~^WcPgN(irRL$T(6gH+icQ9OrioLQ2~psuPSL3h&!i;M}&8JIn7QT z;{+aQL%3Fd8#cyeY#Tz486S@y_s2fT=>ZWl3-Z42V^|J+m`*xPV|OsVRjP8*=x{>Yu7d^W7b%j!P|)D6 zaIh541iEi7L{x&Q9D~!!J49(_g^Y#G^Pl&$04?hZPLsaqN?<)2SW%(% z;q6QNoB6AX>c8yJMpp#X_r8kaWj&I1K&-WE%p>cgPD7ZVOlI{+#d2r-z{dY%E5_#@ zks?tN28GCZm(jKT`GVr>Iyz0T_T)7aoW|neDtX>6wdvK6vafyPy%Qp08)hQ2uDKp2 z&kq3C{R1d2rRZanYY0IIBaZ$3$J;pk@^bt1<@4k5nER1zg{W4Sl1OTUrbW%D%=7rD z2D0UGn&EQ2KIVi0n~cqF>R)gA>&y7&_ zOOP@nApuU6p*n$bZm#mF&&i$I-cpQbS>g00=b|E$hCK|5O}mH$7OB6+OqB~y_`iU6 zzb7E_bmID-jBQvosY^^SA7+I`5V56(lS#0A(9eto*Y=Wn0z+y(iAXhDd&)I#rcme- zQ%&6kDW_M$IvE%6(oCo)PFkXl0r^~pS!kOGpH4IjOz9*3SWqUdo z;*6Zg9wbA} zw2tR*tZjLQJ3abF>m7l`^7FJkX#lyRk7anJ8yQe(^N{Q!%h|w%)moH7rM6QaDn>v{ zkvzf^&gCZc!;k&ms&V@9_6NGwbOLaSLQJx_d6+&vrm955_ivAwPGStpaxbwAZ6hW{ zL~T<_%*VGs{w`VnDA?cQn^%{{w?FGDgrL(*p>UsM`1F7K*Za0@^O)P^GN%I}ratF4 zR;2y@?VAmY2neUgC@EKtxj+4i`YVmuVbKdVZ#_e{A$TCt~H@jS4f}%sW zkM|>(YL5Z9`{9!ekzr#KA~>CxK`7zO(&7-zh$9J<#AT2(sl(-6`wO05FX}WqUsjb& zAA*)VL&?|)V5(cjBy<;=iYKHipT~u@%Wp~7S0ySxE5WmftF%+vwMhzXp!dFN5e-5* z!&1nwXy*F6dIS<=))$qLKTUteCD#3IcE^p;@n>?SDSejTO4)?Q zdr!nadXsYP51!H%ipg0S$#0B)GZW~phpB`e%@|>`{nh2{_^tgFCXvsxgQ}@#FDXB9u2kDbCO2Q z3#?^hQ>JBD=}VSiZxSD2fV1*a%%Y3jyexXFAWHAtvVLe6N$M)ZLkD~5J5T6e!I(Vl zx5j7debLFD6#z(63uMMFsp7Zf5x}4J(%K}Yk)JKbPXH@>$yW1vW4sk8t~)50U)78C zY}2TS=0#QLT3cQEO&SB%rRDtZbcJIR0T=?BU>hn5Q%QO3@;KV~FzKfP$))YE_vgZK|HJZC>GWZ3+0wG1VhX z_$Gib2ei$yram#j)NzR#Dk5`dwaiqP0<$SJvoBTsO5qWdB7O2wA`on26rg8=NO%NP zeBXV)o9TSa@PH6g>ITy%9&pnvC)J_H4CTyJ$K&zFphCtlpvGOx3QY6HwBj#xpawA9 zQJOu#**V7cwr99lhgdSGOR$infeNHK02%w-d#{w?)s<$HsA_wbf`?_|%B)M$bBiDY zT0%T%x}<>Gm((a0nZb*m91Uw9hKOZ>4TP7WuP>K9_5(g9TvtzR8QiMUvm)WCr5B!0 zAtU`QcL3sW-XDhn2;gEmMFeItOw^2m(Gbl&zzL#TSa7{u)FchBh;Ek)36GdQBfKUc z5K?v2>lB@pe^L&uVrxcx-0$Chy#09FA%ie?)>vGDsv%7&l#uC|j$?XoAbVLo=p9mJ zb<<{-&9+0CBT<8zBN|k*K+Cg~+0283IA%n^yA1=&454NZu``m}u-oO5x&1L{csOJc_qB%5^myg5;?%mCH6ZGkOYO&*Du|Zeo2%o)S^Z#L%4Ze7*}|)dyx>oA$oV zqO|fNC5kNsBj^aKQlAL5gkvrKz%p@*>FF0mS3&nZw(d;N@D(zcjm^k|A6P{jL6}+; zeHkfyAwpG|IS2lj>4hy=!hmF^_`yXbEO41SL=!#KatYwl(-G?XE&p93Dx1%5-m)z< zdg^CFru^v=))A4Fbz$c`6V3GRpqPXPb(LdV@Oc@5jwl3y;#1qVbK!9TMO3r4Aw4u! zSuSfd5=&KpK+aBI?{hBg6N|b`UhmoS2U%yv=LIkLUmGgUi;M8-A}VHW``31qV$#6n z!O!TeLy25Wsm&T$9utoF@HhGR^!f9rPrvNralhZgp)D%17PSgvRFWe>ga+gAw1{g`iABt=k&ATI{n5hbMMbCvq2Fyw_vWwuP3d%^< zF5}tzTTMDV-=>@qd!g&GYGkJOX-9UxuTLAuVhXERUR7l7Ns5YK0GFH)L4b}bPbZ=p zsDR#JMT>?ohN`+$BU*X&P>&1ei;OQrP4(f&!|!_RU|8n0ylrL{@-$H?Bss;W%o)d| zkTD{hK9OiPylQmQ2E`McZlou*qEJwT@rhs%DXTnr1B24IMkpcA51i8C}W6DLUYRrl8TbzKx0 zMaQSrjD#!5%ve#|i4bcRBvehRGfjxh21%hcKq+bx-c6jhk87zk#=q|~Lr>4@p~dEdqrlw&$mPz#N58tkv=EzLc2o#jQ;)~@K# zXyK7QzqRi9o29u`$vm`-M!6l1IX#EarFR*L-}C6IdVmzmXpWSSfVdFQsMRUQp?P>D zb(!nQsi}aX7A;&dJqQ_fQA&h~)rc4nA~xhIx;+j%Xo8_o(L5cA%~$nOK7}k+0neT} zCMXjD#e?_zG$ctmL_o&%vZe5?s;QB129M*A%Vm1RLh5 zmaiQ{iA%^K(>*4{+9#Hw%o1DVd~8HY9tC*#d>s4qls(fBW{OCV197tUw{}5YQiptK zKfn4n`&At#pbcF5evx51EDs|y&zli!RXzYY10{QIR58q0zRFo#?|Cf&X8tWA>dY2J zQC_*~;8X&%)UbvoW29&r`9%Jwan6`K8eup4vTnbuAhCWL?FI5QuQDT1wX(M?4Jd)O z;w*)Z@O+!4(ojA=tDoCQVl`NTNkKIX&Gkv_EayEfH^f5ly`OT|pN*AV`1@(&6(zy) zC{>lyL5Ms>NO`sW_}5#l=gIDX%Al@5Og=&8+<X5UY;0e?7 zqO^c@!PIAal>g;=Xl)jrqJ%UM^|JKORj|uUnvvlyDmF9|q}n|O^+*a1EO&l>dWCAM zX&#cLy+Yuz4TaaKeafAqiTD1~*u5Zo zvc?&D6xW#cRZ#A`14~9XceicGL!Y2IOKr1{U4lIf7=3uQGk)$z@T8L5K= zptNpPQzepq^)Zz8441f#Ca-FX_wkmCaOmTK%t}y zXfmXSSBMG_;SLFbs<_0CfQB2UVuB6|XJjppo_I!U4Mh+xkt4Cg;d9O|YE9e#sstmI z^{NOTlob$`eSlIUTtvj9t~zOjGG0m|yjzFbR(OP}I>}5XXhl zTLCy~74nOx1Z`u>=JiAo$V%z(DT0WoE}Io==W-BeiK(_~{l2x03#nI3AnDb!70lE0 ziDYkBm*{zjxRjV>9)XZz339)nhJmgdXm^)aw}f%E(ryUvS5YdrZ&tT1D$!GPLhF)@ z0ON?1UlY1iW3f(W^MWgpD}D2_9yHEuacknV0~YCw_?ZoOUgcWdbU)FGvkX#mu$_%y zDWV&saIj0}f!>#g|1zb>f0Y78A6gduv1|op5jcp7t z_Q(BlvFpoq8=H;C$NiC_?|tzSf@Avo`@@v4*X?@QOeJMcoo?FzLy)aWsWG3DQidL4;^X<*)8@Qmxc(L6Oa9Xm=Tv@ zY9o7kA|zCtJlywrOi!&E3K;Z)NLpcO!rhE+y1-(gu5xu5x}1o!;3`(8P(;SC%Ql9o znxr{N>cn<Z# z(c)$jdqx@SVsoA%gszP#=TlHP-(#7GGnf!@W+Wx>1n%8rqw!NtKT(26j=ZYFj{!34X(d!b0B#OIl?^)FT&-JT`iWYe3XG>Ix!UEN7E~N0v z%89Nr?By&XHFFVX4pFQdN zGBu)Bn{fSEX&#_GD%!QTKCg&X?8I7t^ho*e%HB(J)evL>Pgzw2F)XzmB%~}+{{N5f z_uDV}`Rmv5`sw?(x0sFwRbrNAQHV76AS1JolIbGOd2E{wgBgM|LK(+Uy(lb)398IV z44I*i>6l>`d%eBhuGh5Vbtvc_n z(mn{%oe|+4)6=Bad*t#tYcUk&PPHo9>Zg z+9y?rgbtXOU2jKN2t3bA*@+fj;3{z_*8BvC1zJ{sQL9@Au+_#+hqJ1>A*SIrVo&X3_K^WQU=_rl{l}|{RuN~rNT$FjB zL^&age@pU0wER(03wRIvy#E--GM`ymXtA3F+ z;F@;di}^$@32Ja*ox-FtGo)Eggi-I7k84E$@_Z6Ay3AwYF;tDr9BNwAw1O_85)oOG z;6;_?k&j7$4SzG$3=_#Je3i%=b7#mDQ?ZeqFbVsn&zZ6@!$w*p4XEY%rP+$t+a&-z z95W*^i3rb5j#5QPS#%Lia=O?NHATzyswzc-ydO@PVu>{URrE1EJ5h#0Hi__LrI<&V zoQSu#J0REF)l}WX=aI87x9#E)vEQdpEY4P-5GV41goDV2V97%#qA-1Vx&7t0uOIhE z_{X;4_4T4-Tpp$-?qOpDIel(J$F`{rias7Z9v`M}fBE${X247dC&ThZl#Dfr$6>%KaT9v2TS=Q_hXPj&#W>dq&ZfpZVD-uoxtj3Yg5$FPqNT0_n3-Y zE@PO00il_?Ln$pyQv|J`k^v^w6uxcS<1tyq{+-8^`e*Sm6_FPVYaW<6oC2a6&e~6D zVzdxXt<7qIC=)M`8t2z^h*XJGUqh1a5iIf|M}G%IO+-|+#$2VI04wH8>p*(M;+bS& z@r+!`ZVS+J>+0uquWXK}zEwywm2=Bvgo>fP3YpR)8Bl1IP}cCK1eQFrHt8RpVuG`n z5x^>KKZCAw^`E@~8?5z@o1-C^2%e4dnN)2Ny7_?8Lkh5dExl0`pIRST-e^S|S(j1z zs?TUOQQGp96_2gEW<*X0tgx^sWg zJ!Z%xt!O}?`Zz)k|L1@CUw-{3|JVQiKlYC!@jtQXneMuprZ-LVWt|Hm*8E zo#8IP7SROCxJx>a1$1Io#cF{wUyeidtUboF@dh)vm9i(7A4P_2z)|P zaVf)f0#qs4233GTcL9e=DB!Kfg`>cWkcLG!^=$Y<@B%;*SknY%LiF=A6DvQMk zIAErNj>ghnNpBD=qby0)Epwb*rxL- zcYx+*(yW!uLDs6FU{CG>K>@|Yt#xu5i#Fa}$43_OFDRR_cPCY=51tuTwf#fIuyYDPfkF1d19WQp3^uS zeOcuSx_q^_yK(&`3H;Wcj%Q0fp<2b@ZRb-!`>M9}wHms`=tGOm`)q}@z z-0uQ#Y?}y=c^t=XR^t*>ElBqX5}>H2FVakHHN_Y7V@^|3voS8)<@)v4FCULbK&E3q z?iv;z+mOq)iCHj4Kmk=w_xJl_nrirVk^BBwlNmKd`$?&~%q&^!oso5qNiS)R{DQiK zN4Wb@DqDH%h>$=|X&@0<(G*-rP}F+&iQ1UQ@%`J6-~aLFx9=bCACCwb+jhHdfJF#~ zG3E4~;XXFOCS=UreL9G38=_mNN@hQ!8l+?$HA&7jx0YqfV@a z>-m)5YPPB!%1_i8M5Xnt7w3ukTO$k9fFp zDMXR<)x?(dX6I`&SsG+yW;u^uMkvxZ+E0@Z%^7m+-6y?jx!cypq%@!l0!G>N^7-pu z|7CwXw%>mM51-Th(4lGysgoiGM`+CH(^3QF@W(w7`w>4Te@swha}33# ze|Y?uaSwe!1Bf|2I7F2yJxMNt^iy#sd(=uuy6b70-eNDWaNS-J-c_KV+`;J#*&aMy zBF@^3eKmp1`{L{sa$;)JtlvKyz53fjXt!G209#LheHV@ zetP(MsDzXf<S8y+wzCY2%r?X+PgoweB}I8p_#RBMnW#PkUF@RV%|CN2waetQXt z5lo#>g25I7N+p2+Kuw?(K+st^#ax)S0FojQl6Bxki8s@MHi^a2chZY(Fx&D7z<3G> z&J9e4P-4;Is4j>TPLtjHc18|Qe^#PwCC8+km?RKiC&97MSoRGsaNE&qTQxKhBW>1s z!sprx8L9Oq28-!VN74xnSs#8rmddGB!e>3vIy}xA{XeoACBy5=QMQX&HL@8|i4ZbG zElJa~CK={uU5MnEF%gh0j+|Wx3DHDK(FrS3k+o<*!jXJ4EiOEn11Td<3gKOf5zfgl z5mgyOuNX(fW2Ss-PPkxJF~xIfGSh-f2PVm(g@n3LhZ1y;1GK}l;ak%Md&q4Qz_fN* z#56GiX6AHG=ZAxFW!TUZ$d{cZBdemcAQ27-8K`T^k!lv{@9D?%Fj0s<4&b;y4v1~r zM)70roU&-9B4~nvm@_;MH+_Jc;!1me+=oa`Z3}80SXGFqsb>Xjqa_hWHvdKH%4?iS zOs{;mK+Zs=j07byOv*u3C$Wl&ikKe9{PFhoAOHT3fBW}8KJLdcNs&uXWQ=VbaY!)s zV}A%@jv<%p7{kMl_m2lCuP-lhxgDZ2i)5ChONwr{thRJ&>&_^60v z;k(3y5S1}>yIfS2Ip;Y+eE&kq(9~8rE9VL@pw3gl+ax0e+0MbG!dI_IM_ccO!kF1Q zDP@@mM)u>BeghXMMKx>IdC&JrXYvFjihk)&EmPNi-?Nq_SWY!Zx+Mcphcehw4IoMk z4~d-eX3`zBp?b2Pg(>U%6{hj&<}K}Sep-o7*BmBY0IYo5V(W3$#FB zN`cV;C3~Q?waQTiODxC}y@2)3Pgb^E6x$*vrf5VV5D`UCSH(!0yxLjIZ?*|7v|a81 zeY45CtbHx$Onc*YwDTk&1q21_0SQuQ!aBVZUQ6D`wST*mtE|cpQ`57Vo_^+{nTG+S z3UCpGU9MmL@;AG_{OkYuKR)jBy4^%3R3bC-YBojy;Z#fXG(pF7zaNJ?zI+)-{B@^WdDll)#$l1#j8#=QBM9V0!@??GY?FLpX(;KDbyPm(DyC`J2 zxIUkqIb@ybT!)`Rvh`fg&&7JkxtMKRo@@rr=u7$;f)LS^BI>uS6PmBj{;5vGV9Ytm z$V0YFn|qx|%s+QJUo}QUlFrr2hGGVjlbSx9>lG{P^+y{{GnAmv%M@go$i6l*rWVW_o+sYz);D zAkxd_W}-3dNZKFXg_0F@fEjh*IOgMVa6d>`!9-z{hh)AKC^dVj#EpcDO7;I{YB++7 zW6tSmY6-DA4wX^>$q_kGrXt_etSzIEL2uBWBHMNNrB2;)>I=k3tcYUShmLzsyV5YQ z?3YNXotz&)DFGAhPw)3*2qa>rxn?#SF_L2Oa8+FaMvps7W>@Pr*#HzNYnxwZE#I2a zmZ+nl@R^4xdm>nNpbvbz)IL`EJ_Wq{vE}Vn2oQNCg%v7Qv2cF`1sV&~Rn9>(8Q7{G ztmp!3qRl!DZQ4`Vz$5*!sDK96@Ts1^Mp+VxZEnjw_6-nK5xvZMVZZXN1!A6y1;cq87tarZ!{$^cOx|+`}Wwbx^gKQ4WZUf0K^bs%UGmgXLdE zSdgJXA*$X0Nib_m_trw>YbvK&epLFTu21WaT4=lDw8o7motIdPTsn{aX-o&lML6e2 zd#xUO=kV#6wNa?ctrT2+N)qBLcwvLKXZ+KrqQ-q^U@65-)FP}_>0;A?#L&_YE{$6{ zZ`76p2qh&bRtQzI8?!89hgFL|WuMDlOGY&Zu4WA8Oy7v`OdWV|Mx?gbIUE7Pg3LeN zMu3r-12WY#I)$CPDC*ppN{|#PHBD{9aR?+3Dq4Jg$IuiB!inioqog8B{IfmU*ms=V zbmlJzhNlc}xeT_XK*Qt0DXTP7b6mX?l6{7J!941XKcD^-^HJOABo^{b7tBru2OXla zG2nW`%8J(mTcgdNcst)?e@Cjk>}3~=1UjMq|<*6XJmQJ-pkACpVUni zyPXfIqL=z4R<26e=l1#Puk+9{Zl@>lPqWQ7XI9UCQ}4 zXz9i1q$HT`OFSaU7puC!K3CC^5UG@jFwKFz%>y6_%ZfFUCNL9G9jdR_YfwdwkKH|B zrUA*(*(hoB+b$W3?pxIM4-S^|TA9H^k`<&_cs!m3AddwN&^zhUJ@lv8>rYSa4 z7ff)EX%J6*M-kx2v81uMoU%$)(Ad>*>bYv_V2FmQ5FswDKT}y}MyJK4YH?CXnUv`e zK^)UV)m~p-=9f5}9teSosp`m9_%x5s@R-g}vpLm-#H2ZeYNi`BSSn~99@2CU36FyT zkK|v}3yleaSo#lh9&;W(XOMIHoT&$z5~$Lq4wdMTEdI$UT3NDl z;>uvE&YteJ&vDWDs^^)F2%2^9a30w{jzIg13h&EAloeVBEXbAf=&_Iu4O*j31eR8; z@k$=eB794|FU$C}=3Sy5pBCwz0XZdy$mZKOWt<7S4tdJiuNvAQEiF&t+td1{d8^k0 zlIQ(2e8thq89khE>wIMOSCPsYah86SJ(TFUs|t?6Wi&gD+b)q(`F_!*rUC`5O1rxI zD5DNM+1^^P(=azkT%hZuoS#CEzS(6o>;KPw)j}D|V?OV&2&$>7cGvk6Y$y;atJg!T z$f4RilR*e%t)~eLT*(3TkPv!NrM;IeTZl>pELYp; z|9o0ScR4#gW5;y~1)e=;oQG^_DJ}=yNcUVw#QHMWKsMQCHbpIkikEuR(}k_0*&7=` z%_tTSt=!tzC`rUUwT?8EQ|7m^KpkjQDx|@Kv;lmE7rm$D-2HhW+1DT8Qlf3LY}dN# zRFIMSpuR&9q$tSKqfN3}B2=?$TO`raPsWZ!GA67HMg|yCK|~t76T%vHs&^93E9^UEx*Nn$xoZNf(PjU zC3km)+=XqkP5l#SFx#%R6%T~!#5SSiPJ6*^QIG6*_d!=)Iikz;!K@nI5cL~QKTGd6G z-)(Vofy@*~aZjhVnO?UsCnr3JVU|6jah__31deP%Nf`-5c=((Gs>yJw(x>n4`#$gceEWF(c)LIL zqXrI(%^-p5C78*D3qe+6ThbC?ipP%YWfOb7T(&GKBB4GG=bY8N!4$CU00<&M6%hwH z=S86qu|csEaVVmy@RS4*(4c2ijZYV~hywy03I`Fu;l%69?Qehm?aP<1!8m3hWtsr9 z5jh(r5RM24g-Q?KkGUT{J#5=V2h3D#x)-QYjm|v;;y65lb4CP3#m2BPYTpzIRPKX7 zC8?Q(IuVmX5tZXOgp>4Ez7nyq7CZN8$^H>$S<$6{LQ-qBaN`r4tTKbPt!y`0_CwnV zv|f2(+9cZ9$32247gHHhc1c0>+X!=Hadgd1W5hgMn!i?vH9sH}D;vkmA{D7ugGPm7 zIiW5X6!Y?VteW4Q>CC&R|9 z@jThb0(5N_fiCzC@w5I1kxDMT3>l^@U{d(9dA^hd)Eq!B$W?M7q@;dURbgI50(TeW zGhN;&(-e3VNBuz1#VBBko!OJ8>sSRfKq&IMuyL|nOspqEQipQi1V}i1;R7bU)RsRyl3wJ{|;vJ`Ty3D4t6|I!R1M# z_ZLk$orKg{3-z?&*d%KRiJ;t?|f}M-}c%e zEMC8jl*pJ7S*99v2`T0Ni~&`oG{U)*0rQHhMuG#i38pQ=HLjSQ59;@Mg2vkad=DtM{+XVc^U*x%kBKi(cQm|M9* zchTU%OwtyLseZDGTO?Y-ODG`~ec$0h;5k7I^M_995*!06aIAKw~@hxVVUDF++VQJj-N%uKS9$ULa3 znQ*9IpA&i*#e-0-QvFz51*A$fPa6tp%-j|xPbv$@Q%ZlQOtzE*_cov`q$}BE7ScGr z=!g82w8&u2N+^2^YGcy&;l&yun40J)rn76UWyla6Dq<*!Z&3<#sA%*l%^0}I&OPXs9bb}d!85RBIK+ian_W-YTT>eimc@eVfb;t=iDEN1M_HM_!E&X`_+cD8LQ=T zvtA{hyk`a?(y3}W?IJQU^W>KMeKWhYJUTNdtsqJ@NtJZUtD0>AS6Ozpu2~=wr3#eJ zry}T3xqW(heSP`;AOAi-?gMEksWu`a9>>@&s^$>Ds$oipm_aoN_t+V4@Apu=jE$kk z0}qdV;IYpkaxq>n2x2E6A!8?X?uRp6r|cecrR@sVG8ieTjgvP6fC%dHhmFSbt5txt zoc^iHu^itscGZhpxK9y-pB=P(3Mwi_|Ig2)Wf&@O)CkWX zq*t!nOGdAFJt|e{Il8iuUDWZFsFD&g^=n&jvkaa#`%xQ#H84v63Dz8~2!tRvCt7qY z&;DA0>}04yrrZnZjmV^bTE4kDmGWHV$k((N$dXUQ$^wxhIT5VyK1xi6Rb!bssHj;o ztmePu3v7(EP9EMV+EuY*XCLlpw7DN6{F=cot7%2 zw6zP@u&`X7Cu5(+Qbl!)tvb{5heWvhk>wJLChEi67C?GmW(xYJoi7Uow-sNOy^qx* zr`D&Q(M2BNbqg%Jvb@F5M+{gMQUB>0SB4l*zqAsNt2w7@d+N*=>Y=P>!fJv#KcXss z(AINhDqAR80LVy{=mo)pk?LNV2rx+yc=lRGWrC}-bR`UyLt}}7Y6*Syb!DyJbO_0b+xIYx8> z(QMNZLIwBfANR*Gy%d?(At)w8B2}f;gc8R*+<}&~2E6r`J(9o9F&MS)hN>#YFddek zo|@JOh>gTR9;8%v8j5Ye1mPJc%OJ&6pT;VtRqd#R4$X5^!X#lm-D`pe2-4>yFvbvAhg;>aE7T(yga;UY%$W1oAMs+kQvbDVkt)#pn{DSHI`_E`o$}v|EsO7$OeV+s+ z*}xOA$#OaEJXtM+Wj)(&^eUv7ymZD4A~b`IFTec?@Z0ad! z4Gtq!DMCoVkI9Hh{&;_P!S?#fsdJC%@R0pDHj|;IcURR3ca_T^J?^`U%`0p=ql(f& z2Ey|`BFnv-JW?PkQ`wtndB|99xBi z*C>ERsTLd60WlU@*K^yTn)%Y)lnLtNAdN^wpe55FMCL;BY<0Wv(p_V9mocW>5Vnk7 z`C@@W-IK$KPVi!F=4bmL&9>IzO#}NAKrai?-|mF%!o|p#C6LXYPw&5;IOIRSX`y)5 zZFND;)Ac=(hIoFfzWx5cW&fAm=|HZ-f#q$>tO%GXcCtmtQUA@5RFj9o$RRAr@~@B2 zFhE3BX>Trmb2kf?bQj2DPBL)a5HVC#l?*dW z6@mpK!b44nNMx)cl{<%p7`qs1Mv5zd1)~GX z-Vh856(K&uL~Lvk%<_E66lH3Y#p*jvgB+?9)d;GENPPSD!;hnE)132|?io5%_qOR6 zHpbY-uuq>ptLdD_$Nj_Eog4rKo&|Ev>{j+d>kO<@Rx;h<5K-Y~LSl#SeiUPK9+;}j zCuEj*RPKINQ&6*JOj8tvlJAXJhcPz-gMyLmBSV12##N^qk=0@~eJ{{L9O5L8xgU{J z%1|>S!xWNjv?e6D_wqYY)y^(25xPAuQ4 z0DUQrm-{acru-KG@2Iy7SAZd+3N@*2$||?WO8}%Zuu`@=AgUT7V{A1&5pbX@=cUTy zML?=bq*zS{0Bo+0c50qJIS5#IjMHP=g#5@k4M;ucS+61c9QWw!kdOvRfJ!ZSg?t3xnM<+TfBiu`Q z8u5+?Qp8FUUNn4*LUj@z?sL1EZW<4Th^Xc;zG{{3D)UMiWbApe$-=hW^|EgDrkHNm zFJFKA{{6@P@sVt9&d5_;B^d##M@T>@3Q?$pN+9-`wUqaL%9xLbi) zEtC6@&%c<=-_JX$!vn4;sTRLO>jp4!j)sk#@S}P57ET0NYLx*r-&dVbc?dzMDm@y+ zWzxEt3?eGOQ9*(fNF-~#5rxSyRb>$nLC+1JnhQTIH*$tzL#QGGdTIw3S`Cy^YIy+^ z110%o%Dtx9D$G;OVh})d5#cTMTklRnGl7tz?w)T0$=r5XGetBdz#8>MiM;FZs0ZSn zTp4-?HTXrwt^E9Q`G5ZNUq8QWe|~@e@qXW@@B6V&-={kwprx*YF5{N6AGy*|2C8y1 zO`l|qLQh;S-&|zDk`K}57uryy8_#&7YT*PYxecJ%$trG*pdeIrDM=;qQ51geRB4o! zl$(f%G097j#4(*4kcEA#J-n1xnjS)eRe&t?QmC5etvms9K%&ND<-D+Z^ENW2Ow$-S zi`k!ene+&`X{rDu&~;SJ)T6fU`JuIu(?~3qzpr(@u3dug$pkd?KMfUEiuVcQw;_<{ zFra!uD$vg*^my+4UqMK|Y7p6C4(MO2{5IVgebGn$?wNYXQbfFd@PiwTEB#PlP3H`A1v=GaAnfFj{8 z1T|}}qqJ>QggE8|2#iW9i3-C+?79gkrd{qK?pZ+KBp`~JTU)g%RChHK6^hBwBFKZ` z5QwV8Oxj6oTt0pJ<#PMQfrKjI5rr_$=roNevqF<1w`$0)ukwam>k$Oz)5TeIEP$k#^TT zeQzW-uW&y-K%Ghg$QqkSD4`0;8OEZdDJ~{O#jdf0>{vUe-F3VwAe<47&NHcMCTG1; zTu9C`4~eSf9BV)+2wzPA345zKFv*H2)Gp2tlL#S@!?{ySUweR#w$YLd#eP001MkZ_|7(mkEH`jD0*l zj+oR^KoSWDXn@9uu`6~!VIsEO)n6HWO-dQeN=n@X)`?moDKrY3)Vy_3c-LVh6??FJ7U7Up+CCB zrfqO9$Z{cV#Bo~cHi=jZw5)RmFtVOqt!;EBH!qW!OvuuM_2P;Qt3_mWF_(>(_EaK$ zdg|>(CFu`Q4G7hK`SNv)?LYqG-+UejUtM<11#0=70Gvn6+ie(hvn>}&MDP1OOy$_$ zK0dCu+i~CBHA58Nr>Lr^y9QjsNSSdQ$FWaQ;oLo|1L@@12b`Z*IxvjVapO~aWxMb? z^aTwHBoy0RYq}y@6xpv>**UJ9;WJ)8BLX>}YXRG?RcJo2SDt7&OFaK(MPpCLxqg=Q ze&{;>y!8;S8bl!6AyQWEdTb`fy{EFV+C!?Rg!1mp_bt~RfO#-E!Og}{vX~$Gr z+j$I46v-y0!4m2F=?%U>0JT6$ztFmk4rAmjhARt}ajc%O@{Gq?|A}?wh4yRD^IWz< zIxLIV|7|L0wbErTF%1p&J>-0B z^fiA5wJp<~hnLA)qY@yJRRdpMuQ&Vh<>hwYkLld!e0#sYJ@$|Nc+A)jpY9++ivEv! z*wQFjL$sZhLBKQAt(K~0SORHXYSFShOxcyI2nxI{(n6PKR=Ru+%gmUZ3td2*=K(5< zxJ+QZtT$!7e?D`~jfgIDX(&#TF{9r`%Xt1L#(kX~`A=Jl9Th~K&3t2#M*r#E>a;J{ zQ#-nnwCjS-lm6tK!E?*66JOz0IjyyA&z-}*)6@Jc?QZ=1w4XwY^Q^6dAdv3$Dt}Pu zgMBjsdeX!lXcX?k2+wR!R~4l54xl@S2t4U~B`=v9IQsL7%*wi8HAxg4Wjf{AAe4tL zc$iE{b=~x^9cJg0O1o2mTDJPiWmsGWgHxPI&66Su!lzW>8o7oqf;1h{I?aVp1IG+A zsEy0*vR!PLjxltMgb8XOUBn`C(qJu*d(7amr;-mv#LQBxBh_jWTIU5LNbYm`W1mie zZMVy)u0A$fpH53FEKu*`R8%%Jf$ZsiGn>eX6Nt zHs4Z&sXrn^C{?kX&1qHz8X62x#Po8#Kp4U4Q?-%j!hLlRmjkH%8~74zzuhH1kmKvn**9XzXKizX5sD&*BR4Ff{5jWR`_+T^AfUZ2usHVdxEd92XncRuetb=?CYeOEGalI#QmJPeq7PQ1di11v*J2Ndf^x&7zH( zFee{sIqTX7Eql_Cz)Vdo3bDJMr=4Jqo-0GCVt`g8mG7mAlGx%rL;~Y1xyXCU?0UKi zkLF*&RR&iVT=u790!0dV3;j3*R5-%R7D_KvnWZP>b$S?{9gL zQE|0rlKnyvGzB*$rzd8Ym7J@+{7cC)*OMw^U&&X6UO_nhg$9kp@!YA z0^R34j(NR}gLCdPX7HGFFAS9NK=uGsT91i7v2p?+TI>w#*|)z&8xUC;bxBi_Il+?{ z??ZE*vsH4xfPAwGJl*p1S5JSQY}C{0cj-dHisbWZlbjf7;rF&Pc=o=zS)Mxd3 zB&bPNS~b6Uuykr!AS!K6Xp|zuT9sA8vci`ZxGR5;w9F@t&TR0eYS+s)#utdF&4`c3 z{^R50$NS@b-|zS1m@bg;m~*;^XJsil8C6!-a!)9_SbBS4Q4$?PGNl9MtZI+$5y?eT z)N~@U4SAjzd9KxwzGq;yr?QJAE1mlk<2~)(Sf7SxY+e3t+0Wv4D$m2rO(lS;JzqCk zkNUg`u9Ne0jM_{$x?rEVa%E4a=;RRyVWN!l$l(diuIL60g$nOh>LlOi>AK`6h=wQA zv-WOUfu=ya6sBK~C;CvRNyte|vCRd;1ID4DWwNv@n^#ba9LfO|HMQq} ztDz={DVa$Uk(wDGDjJF`WXpV8#d~>yS%)1JYICS;8ZE6MCOH=16dtIZTX=IbJIptQ z*U)q+W0Ny%lA7p{%mfmlrYeYFM3|YXnCZA&#${Y@FE6+2W$30hG$(~rav4%of+sW= z?$bX$_P6)@ejG{tjx0D4v0)hqPG8$q=u$q{ zS-CTyd9;xlNux<+Q5}@7nZo;U{*SMo25bJ^iDok~FjJZJyS=`C`u!h&;FuMOrY4&$NX%@h zh6vr&l=KMAddM+M1pYW8CT{V#T<(|s@p0!aVcVQX>kOHRa_}NUpvQ3>#}RrtJob-0 z9ur=wike#KEbbrff~u1)x~6&qg5g<&dIqRJA=E}*omH)XKI440V_Z(YlXVS)S0KUu zCfbQ6?pc_%s6*t#E13NgICy?LG=gb(lSZI2^-VM^E^#5E-b`s0)<9O2Kukl524-km znXjt*?MrT`C*^d`hp;XDX&U#5^NXu5G1qLiM!CeTvY5NQKDtX&F7UPg)+Y61HA!TkmbH;>^0Q2J>wk>QbNL zqSMsK(@9==d8wm$zUt}Su+DnEt_r={K;>HGvj|Z&OL={Wr$2zZ54dWAzB3kK0qH0+ zlx|czE{PRrucMgeRI51JehS12nx<6|u3B44=;02bC{-*6*T{MlQT6L0NEx3hGype~ z!*r_(lr9(%NY*YfB-OCBC}A6>n#n*4psHpjsu8$9cK5h!HdKbH63Q^8X-bGiG=&_L z{qXmX$J_gT_p+uVU=jfIn5sI4tw1F_+Xt{8^Y9>cwG^U}te(o&$y5x;>Ip_j>7%6K zg_=dC2ohh406GI;sIc0f8}Okmc-0MHJ(mS8(Q$Ta_K4=9h-7k8;G$Ba&@y_kl8%O0 zVrEra;Y;$_(w=s?MgX1>dyK?V>Y+x6jxCC=_^F8WME`7@F73(Pv&%rr@AX+#DYkJz z=jlG@jHpT{Q8h?aL1?!YP{1lXEA-j9Y?OcnrAx3x;H4xT_59O~mrW5TP0E88G*D>5 zO)=^S`r*XGQ`(|ZCgs-|#Hgnx4JB4I(}>{dy!X>R`LRA|c_XQw!8J%S3zm?j23@5v&;S>AspXG~GmWYL?Zto>|-9hQ;-iJ0Y`- z+p@|{fR*{w+$Ti2iv09q;4&uYk<37jj&4CDbX2HP`agAk3nV}m%Ppv(>=|};>NLM* zo&2sF1Bd?d+ixCme}Bge%k!1lt>T5nv)W2E6EzA4pfY?;OsLv^-~E2jC7{P;9@l+; zf5aXfSJIu~6PGb&1c!|65~} z8h^$p=q-}&iPPv2drw>KdpghE8= z;8fuFpAf^Uaq6Vu^C?dlCqihKaRY2xeO)Pk-i#-uDTR;)6~mlY&2=5JVvYLfw6%gt zyp*9|7@9n92aHtqlCE?tw2$^Qjr!N&YZR3vLY+d9!aTtcF)Y4ykuL($1sV|zlNhXy zOmE|ss5GManuw+9yKXNS5YpFCBPfATQwF;Y2?RZ;npQ;?R}*Y7lNAh61%;&$Sb=B5 zqG0cf%J_RWqJgHCPPPP8s%|N!=+Ts5(kNlf{V@T_xqytSHTLH&_sY$U(f3`uIT#J(4q!$lCoH zsF{h|Je%dY5@!Ts28=mnBLEpHT*)Iyu2j{%Ld;ZUOPoE%cFb@BMTB=qT}BOKSauvn zP=`sM!fedn_x=9Z-3yLfw=q<&m&?U$h+HqW8HRzSk*e7PG@bW-zCVtD=r#%&RiCJ6 zAZA#CqlkH=U6OsyeV?A#PDC=2seaEA>T6wsAgZP!?TuPCzE=5rA0Hqi9F%jb-C0{f z3n&ptH8J;gJfL}_Mmx~Yujjmggb@&(k`Iwxu8L7dBZQiuu1^n7eFr@J4Ai$K9)vW;zQ13ej6D!quL znCgJnDM$;56(5vEtwX;M0Fs?0MV6#R#Y)YJMxfh z-58V~9zadNNtfK_j7a?!m3bVhn%2Qo)p*P>%kjM#&nL=PWYb7MSM32=J*hyA#Ln2L zT0l#9s+I?(4dOGJ04)9Yw4hI!o7_-{`b3A25jxV^kxc-ruCj%0AYG{#k<-DRK<5cH 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⪼bR=3Me-O?rVloyW$qv3YbCY;tbg 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%0Aacom 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 FV1jkR#cKcn literal 0 HcmV?d00001 diff --git a/docs/adr/ADR-353-fal-visual-teacher-lora.md b/docs/adr/ADR-353-fal-visual-teacher-lora.md new file mode 100644 index 000000000..e6444e54b --- /dev/null +++ b/docs/adr/ADR-353-fal-visual-teacher-lora.md @@ -0,0 +1,162 @@ +# ADR-353: fal.ai synthetic visual teacher LoRA + +- **Status**: Accepted +- **Date**: 2026-09-02 +- **Deciders**: ruv +- **Owners**: RuView visualization / neural-styling maintainers +- **Tags**: fal, lora, synthetic-visualization, display-only, cost-control +- **Parent**: None +- **Extends**: None +- **Supersedes**: None + +## Decision + +RuView will use one real, fal.ai-trained LoRA fine-tune (a "synthetic visual +teacher") to steer fal.ai image/video generation toward a consistent +"RuView-style" room visualization — glowing pose overlays, WiFi signal arcs, +vitals readout panels, dark tech aesthetic — instead of relying on ad hoc +prompt engineering against a stock model. + +**The architecture boundary is non-negotiable:** + +> The implementation boundary is now clear: fal.ai will train an optional +> synthetic visual teacher, while RuView's measured RF, pose, vitals, +> identity, and confidence remain local and authoritative. + +Concretely: + +- The trained model produced here is ONLY ever allowed to generate + supplementary synthetic visual/pixel output for display or broadcast. +- It must NEVER be a source of, or influence, any measured/derived value: RF/ + CSI measurements, pose estimates, vitals (heart rate/breathing rate), + identity, or confidence/quality scores. Those stay local, deterministic, + and authoritative — the existing `wifi-densepose-engine` / + `ruview-twin` / `ruview-witness` stack — completely untouched by this work. +- Any output this model produces must be clearly framed as SYNTHETIC per this + repo's rule: "Never present WiFi sensing as camera-grade." +- This model has no read or write path into the RF/pose/vitals/identity + pipeline. It consumes only a text prompt (and optionally a reference + image) and returns pixels for display. + +**Evidence status:** the training data extraction, training submission +script, and validation script are real and committed. The training run +completed and validation succeeded — trained model id, actual spend, and the +validation sample are recorded in "Result" below. Actual-vs-billed cost +reconciliation against the fal.ai account dashboard remains UNMEASURED (the +figure below is a documented-pricing estimate). + +## Context + +A concurrent workstream (`ruview-fal-room-viz` / `ruview-room-viz-h3-upgrade`) +generated real fal.ai room-visualization videos across several style presets +(architectural, abstract, branded, and an "h3" refresh: architectural, +cinematic, minimal, abstract) using stock fal.ai video models driven by +prompt engineering alone. A separate concurrently-running agent +("realtime-dual-renderer") is building a neural-styling layer that needs a +*specific* trained model id to call, rather than continuing to rely on prompt +engineering against a generic model. + +This ADR's job is narrow: turn the real captured style presets into a real +trained fal.ai LoRA, verify it actually works, and hand off the model id. It +does not implement the renderer integration — that is +`realtime-dual-renderer`'s scope — and it does not touch the measured +RF/pose/vitals/identity/confidence pipeline in any way. + +## Training data + +Source: the real fal.ai-generated room-visualization MP4s already captured +this session (read from `/tmp/room-viz-*/room-viz.mp4` on `ruvultra`, one per +style preset — not committed to the repo; the repo has no camera or person +data in these clips, they are synthetic renders). `scripts/fal-visual-teacher/extract_frames.py` +extracts one JPEG frame every 1.5 seconds from each of the 7 real style-preset +videos (architectural, abstract, branded, h3-architectural, h3-cinematic, +h3-minimal, h3-abstract), producing 27 frames total, each paired with a +caption `.txt` describing the shared RuView visual motifs. Frames and the +training zip are gitignored (`scripts/fal-visual-teacher/training-data/`, +`training-data.zip`) — they are regenerable from the source MP4s and are not +committed, consistent with the repo's no-generated-artifacts rule. + +## Training endpoint and pricing (verified 2026-09-02) + +- **Endpoint**: `fal-ai/flux-lora-fast-training` — a real, currently-listed + fal.ai LoRA training endpoint for FLUX.1, verified against + `https://fal.ai/models/fal-ai/flux-lora-fast-training/api`. +- **Pricing**: $2 per training run at the default 1000 steps, scaling + linearly with steps (verified against fal.ai's own pricing text on the + model page). +- **Mode**: submitted with `is_style: true` (style LoRA, not + subject/character LoRA) and explicit per-frame caption files, so fal.ai's + auto-captioning/segmentation is skipped in favor of our own captions. +- **Inference endpoint** (validation only): `fal-ai/flux-lora`, $0.035 per + megapixel, accepting `loras: [{ path, scale }]` pointing at the trained + `diffusers_lora_file` URL. + +## Budget + +- **Original cap**: $15 total (this task's cap only; not shared with other + concurrent tasks' budgets this session). +- **Raised cap**: the user explicitly raised this task's cap to **$100** on + 2026-09-02, mid-run, as real headroom for a larger/better run or retries — + not an instruction to spend all of it. +- **Planned spend**: ~$2 for the training run (1000 steps, default) + ~$0.04 + per validation inference image. +- **Actual spend**: ~$2.04 (one 1000-step training run at $2/run + one + landscape_16_9 validation image at $0.035/megapixel, rounded up to 1 + megapixel). This is a documented-pricing estimate (`CLAIMED`), not yet + reconciled against the fal.ai account billing dashboard (`MEASURED`). + Comfortably under both the original $15 cap and the raised $100 cap. +- **Why no larger follow-up run**: the first validation image already showed + a clean, on-style result (glowing pose overlay, WiFi arcs, vitals panels, + dark aesthetic, all present and legible) on the first attempt. Spending + further into the raised budget for marginal improvement was judged not + worth it against a result that already meets the bar; the fallback path in + "Consequences" was not needed. + +## Result + +- **Trained model id / LoRA weights URL**: + `https://v3b.fal.media/files/b/0aa8c60d/OjaBs6Gi8pMhZqMo79iym_pytorch_lora_weights.safetensors` + (131 MB `diffusers_lora_file`, paired `config.json` at + `https://v3b.fal.media/files/b/0aa8c60d/-GU6dd_2ZFqq18I0CuPOB_config.json`). + Use with `fal-ai/flux-lora` (or any fal.ai FLUX.1 endpoint accepting a + `loras: [{ path, scale }]` array) — trigger word `ruviewstyle`. Full + training record: `scripts/fal-visual-teacher/output/training-result.json` + (gitignored — regenerate by re-running `train_lora.py`, or copy the URL + above). +- **Training request id**: `01a060b2-c940-7972-abf9-ea48847eb2ae` (fal.ai + `fal-ai/flux-lora-fast-training`, 1000 steps, `is_style: true`, 27 training + images, completed in 376s on an H100/H200 worker). +- **Actual spend**: ~$2.04 (see Budget above). +- **Validation sample**: `assets/fal-visual-teacher-sample.png` (+ sibling + `assets/fal-visual-teacher-sample.json` with the exact prompt, LoRA scale + 1.0, and source fal.ai URL) — a real `fal-ai/flux-lora` inference call + using the trained LoRA, generated 2026-09-02. The image shows a glowing + cyan pose skeleton, WiFi signal arcs, vitals-style readout panels, and a + HUD-style TV overlay in a dark living room — confirming the LoRA learned + and reproduces the intended RuView visual motifs. +- **Consumer**: `realtime-dual-renderer` (separate worktree + `~/projects/ruview-worktrees/realtime-render`) — notified directly with the + model id above. + +## Consequences + +A real trained LoRA gives the display layer a specific, repeatable style +target instead of prompt engineering against a stock model, at a bounded and +small real cost ($2 training + cents per generated image). Because the model +only ever produces pixels for a prompt, and has no schema path into any +measured/derived RuView value, it cannot silently become a second source of +truth for pose, vitals, identity, or confidence — that boundary is structural +(no shared types, no shared data path), not just documented convention. + +If the trained LoRA does not meaningfully improve style consistency over the +stock model (a genuine risk with 27 training images and default steps), the +fallback is to keep using prompt engineering against the stock model and note +that outcome honestly here rather than claim a training win that isn't real. + +## References + +- `scripts/fal-visual-teacher/extract_frames.py` — real frame extraction +- `scripts/fal-visual-teacher/train_lora.py` — real training submission +- `scripts/fal-visual-teacher/validate.py` — real validation inference +- [fal.ai Flux LoRA Fast Training](https://fal.ai/models/fal-ai/flux-lora-fast-training) +- [fal.ai FLUX.1 with LoRAs (inference)](https://fal.ai/models/fal-ai/flux-lora) diff --git a/scripts/fal-visual-teacher/extract_frames.py b/scripts/fal-visual-teacher/extract_frames.py new file mode 100644 index 000000000..130e3ea6e --- /dev/null +++ b/scripts/fal-visual-teacher/extract_frames.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Extract training stills from the real fal.ai room-visualization MP4s. + +Reads the source videos (real fal.ai-generated room visualizations, one per +style preset) and writes JPEG frames + a caption .txt per frame into a local +(gitignored) training-data directory, ready to be zipped for fal.ai LoRA +training. Frame interval is time-based (not frame-count-based) so each +source contributes visual diversity proportional to its length. +""" + +import argparse +import shutil +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] + +# (source mp4 path, style tag, caption) -- sources are real fal.ai output, +# captured earlier this session by the fal-room-viz / room-viz-h3-upgrade +# agents. Caption describes the RuView visual style so the LoRA learns the +# recurring motifs (glowing pose overlay, WiFi signal arcs, vitals readout, +# dark tech aesthetic) rather than any single room layout. +SOURCES = [ + ("/tmp/room-viz-architectural/room-viz.mp4", "architectural", + "ruview style room visualization, architectural wireframe overlay, " + "glowing cyan pose skeleton, wifi signal arcs, dark tech aesthetic"), + ("/tmp/room-viz-abstract/room-viz.mp4", "abstract", + "ruview style room visualization, abstract particle field, " + "glowing pose overlay, wifi signal arcs, dark tech aesthetic"), + ("/tmp/room-viz-branded/room-viz.mp4", "branded", + "ruview style room visualization, branded hud overlay, " + "glowing cyan and emerald pose skeleton, vitals readout, dark tech aesthetic"), + ("/tmp/room-viz-h3-architectural/room-viz.mp4", "h3-architectural", + "ruview style room visualization, architectural wireframe overlay, " + "glowing pose skeleton, wifi signal arcs, vitals readout, dark tech aesthetic"), + ("/tmp/room-viz-h3-cinematic/room-viz.mp4", "h3-cinematic", + "ruview style room visualization, cinematic lighting, " + "glowing pose overlay, wifi signal arcs, vitals readout, dark tech aesthetic"), + ("/tmp/room-viz-h3-minimal/room-viz.mp4", "h3-minimal", + "ruview style room visualization, minimal clean overlay, " + "glowing pose skeleton, subtle wifi signal arcs, dark tech aesthetic"), + ("/tmp/room-viz-h3-abstract/room-viz.mp4", "h3-abstract", + "ruview style room visualization, abstract particle field, " + "glowing pose overlay, wifi signal arcs, dark tech aesthetic"), +] + +FRAME_INTERVAL_SEC = 1.5 + + +def extract(src: Path, style: str, caption: str, out_dir: Path) -> int: + pattern = out_dir / f"{style}_%03d.jpg" + cmd = [ + "ffmpeg", "-y", "-loglevel", "error", + "-i", str(src), + "-vf", f"fps=1/{FRAME_INTERVAL_SEC}", + "-q:v", "2", + str(pattern), + ] + subprocess.run(cmd, check=True) + frames = sorted(out_dir.glob(f"{style}_*.jpg")) + for frame in frames: + frame.with_suffix(".txt").write_text(caption, encoding="utf-8") + return len(frames) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--out", default=str(REPO_ROOT / "scripts" / "fal-visual-teacher" / "training-data"), + help="Output directory for extracted frames + captions (gitignored)", + ) + args = parser.parse_args() + + out_dir = Path(args.out) + if out_dir.exists(): + shutil.rmtree(out_dir) + out_dir.mkdir(parents=True) + + total = 0 + for src_str, style, caption in SOURCES: + src = Path(src_str) + if not src.exists(): + print(f"SKIP (missing): {src}", file=sys.stderr) + continue + n = extract(src, style, caption, out_dir) + print(f"{style}: {n} frames from {src}") + total += n + + print(f"\nTotal frames extracted: {total}") + print(f"Training data dir: {out_dir}") + if total < 4: + print("ERROR: fal.ai training needs at least 4 images", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/fal-visual-teacher/train_lora.py b/scripts/fal-visual-teacher/train_lora.py new file mode 100644 index 000000000..ff408ed27 --- /dev/null +++ b/scripts/fal-visual-teacher/train_lora.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Submit a real fal.ai LoRA training job for the RuView synthetic visual teacher. + +Zips the extracted training-data directory, uploads it to fal.ai storage, +submits a training job to fal-ai/flux-lora-fast-training, polls until it +completes, and writes the resulting trained-model reference (diffusers LoRA +weights URL + config URL) to a local (gitignored) JSON file. + +Architecture boundary (ADR-353): the model trained here is ONLY ever a +source of supplementary synthetic visual/pixel output. It must never +influence, or be treated as a source of, measured/derived RF, pose, vitals, +identity, or confidence values. + +Requires FAL_KEY in the environment (fetch from GCP Secret Manager, +project cognitum-20260110, secret MINIMAX_MUSIC_FAL_KEY, before running). +""" + +import argparse +import json +import os +import sys +import time +import zipfile +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +TRAINING_ENDPOINT = "fal-ai/flux-lora-fast-training" + + +def zip_training_data(src_dir: Path, zip_path: Path) -> None: + if zip_path.exists(): + zip_path.unlink() + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: + for f in sorted(src_dir.iterdir()): + if f.is_file(): + zf.write(f, arcname=f.name) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--training-data", default=str(SCRIPT_DIR / "training-data"), + help="Directory of extracted frames + captions (from extract_frames.py)", + ) + parser.add_argument( + "--steps", type=int, default=1000, + help="Training steps (fal-ai/flux-lora-fast-training default is 1000, ~$2)", + ) + parser.add_argument( + "--trigger-word", default="ruviewstyle", + help="Trigger token to prepend so the LoRA is invocable at inference time", + ) + parser.add_argument( + "--out", default=str(SCRIPT_DIR / "output" / "training-result.json"), + help="Where to write the trained model reference JSON (gitignored)", + ) + args = parser.parse_args() + + if not os.environ.get("FAL_KEY"): + print("ERROR: FAL_KEY not set in environment", file=sys.stderr) + return 1 + + import fal_client # imported after the FAL_KEY check for a clearer error + + training_dir = Path(args.training_data) + if not training_dir.exists(): + print(f"ERROR: {training_dir} not found -- run extract_frames.py first", file=sys.stderr) + return 1 + + zip_path = training_dir.parent / "training-data.zip" + print(f"Zipping {training_dir} -> {zip_path}") + zip_training_data(training_dir, zip_path) + print(f"Zip size: {zip_path.stat().st_size / 1024:.1f} KB") + + print("Uploading training zip to fal.ai storage...") + images_data_url = fal_client.upload_file(str(zip_path)) + print(f"Uploaded: {images_data_url}") + + training_args = { + "images_data_url": images_data_url, + "steps": args.steps, + "trigger_word": args.trigger_word, + # This is a style LoRA (recurring visual aesthetic across varied room + # layouts), not a subject/character LoRA -- fal.ai's is_style flag + # disables segmentation/auto-captioning so our own caption .txt files + # (one per frame, describing the shared style) are used as-is. + "is_style": True, + } + print(f"Submitting training job to {TRAINING_ENDPOINT} (steps={args.steps})...") + handle = fal_client.submit(TRAINING_ENDPOINT, arguments=training_args) + print(f"request_id: {handle.request_id}") + + start = time.time() + for event in handle.iter_events(with_logs=True, interval=5.0): + print(f"[{time.time() - start:.0f}s] {event}") + + result = handle.get() + elapsed = time.time() - start + print(f"Training completed in {elapsed:.0f}s") + + out_path = Path(args.out) + out_path.parent.mkdir(parents=True, exist_ok=True) + record = { + "endpoint": TRAINING_ENDPOINT, + "request_id": handle.request_id, + "steps": args.steps, + "trigger_word": args.trigger_word, + "training_images": sum(1 for f in training_dir.glob("*.jpg")), + "elapsed_seconds": round(elapsed), + "result": result, + } + out_path.write_text(json.dumps(record, indent=2), encoding="utf-8") + print(f"Wrote {out_path}") + + lora_file = (result or {}).get("diffusers_lora_file", {}).get("url") + if lora_file: + print(f"\nTrained LoRA weights URL: {lora_file}") + else: + print("\nWARNING: no diffusers_lora_file.url in result -- inspect the JSON", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/fal-visual-teacher/validate.py b/scripts/fal-visual-teacher/validate.py new file mode 100644 index 000000000..0a5573917 --- /dev/null +++ b/scripts/fal-visual-teacher/validate.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Run a real validation inference call against the trained visual-teacher LoRA. + +Loads the trained model reference written by train_lora.py, calls +fal-ai/flux-lora with the trained LoRA weights, and saves the generated +image as evidence that training actually produced a working style LoRA. + +This inference call, and the LoRA it exercises, produce SYNTHETIC display +imagery only (ADR-353) -- never a source of measured RF/pose/vitals/identity +or confidence values. + +Requires FAL_KEY in the environment. +""" + +import argparse +import json +import os +import sys +import urllib.request +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT_DIR = Path(__file__).resolve().parent +INFERENCE_ENDPOINT = "fal-ai/flux-lora" + +DEFAULT_PROMPT = ( + "ruviewstyle room visualization, a living room with a glowing cyan pose " + "skeleton overlay, wifi signal arcs radiating across the space, a vitals " + "readout panel, dark tech aesthetic, synthetic sensor visualization" +) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--training-result", default=str(SCRIPT_DIR / "output" / "training-result.json"), + ) + parser.add_argument("--prompt", default=DEFAULT_PROMPT) + parser.add_argument("--scale", type=float, default=1.0, help="LoRA application strength") + parser.add_argument( + "--out", default=str(REPO_ROOT / "assets" / "fal-visual-teacher-sample.png"), + ) + args = parser.parse_args() + + if not os.environ.get("FAL_KEY"): + print("ERROR: FAL_KEY not set in environment", file=sys.stderr) + return 1 + + import fal_client + + training_result_path = Path(args.training_result) + record = json.loads(training_result_path.read_text(encoding="utf-8")) + lora_url = record["result"]["diffusers_lora_file"]["url"] + print(f"Using trained LoRA: {lora_url}") + + print(f"Submitting validation inference to {INFERENCE_ENDPOINT}...") + result = fal_client.subscribe( + INFERENCE_ENDPOINT, + arguments={ + "prompt": args.prompt, + "loras": [{"path": lora_url, "scale": args.scale}], + "image_size": "landscape_16_9", + "num_images": 1, + "output_format": "png", + }, + with_logs=True, + ) + + images = result.get("images", []) + if not images: + print(f"ERROR: no images in result: {result}", file=sys.stderr) + return 1 + + image_url = images[0]["url"] + print(f"Generated image: {image_url}") + + out_path = Path(args.out) + out_path.parent.mkdir(parents=True, exist_ok=True) + urllib.request.urlretrieve(image_url, out_path) + print(f"Saved validation sample to {out_path}") + + meta_path = out_path.with_suffix(".json") + meta_path.write_text(json.dumps({ + "endpoint": INFERENCE_ENDPOINT, + "lora_url": lora_url, + "prompt": args.prompt, + "scale": args.scale, + "source_image_url": image_url, + }, indent=2), encoding="utf-8") + print(f"Saved validation metadata to {meta_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From f1ea275d5d5c9885514fcf33aee34994d2e42890 Mon Sep 17 00:00:00 2001 From: ruv Date: Wed, 2 Sep 2026 08:53:08 -0400 Subject: [PATCH 03/27] chore(forecast): bump ruforecast submodule to the governance/spend fix Points v2/crates/ruforecast at ruvnet/RuForecast@a802067f, the tip of fix/governance-and-spend-enforcement (ruvnet/RuForecast PR #2), which closes 14 code-review findings against the training stack merged as #1766 -- most critically, ADR-349's governance-verification and spend-approval-record requirements were specified but never actually enforced by the fal.ai hosted path; they are now. Adds ADR-354 recording this as a RuView-level integration decision and pointing to the submodule's own ADR-350 for the full technical record, since that repository is now the source of truth for the forecast subsystem's decision history. Depends on ruvnet/RuForecast#2 merging with green CI first; see that PR for status before merging this one. --- ...nce-enforcement-in-ruforecast-submodule.md | 89 +++++++++++++++++++ docs/adr/README.md | 1 + v2/crates/ruforecast | 2 +- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 docs/adr/ADR-354-forecast-governance-enforcement-in-ruforecast-submodule.md diff --git a/docs/adr/ADR-354-forecast-governance-enforcement-in-ruforecast-submodule.md b/docs/adr/ADR-354-forecast-governance-enforcement-in-ruforecast-submodule.md new file mode 100644 index 000000000..9fc039159 --- /dev/null +++ b/docs/adr/ADR-354-forecast-governance-enforcement-in-ruforecast-submodule.md @@ -0,0 +1,89 @@ +# ADR-354: fal.ai forecast governance and spend enforcement now real, in the vendored RuForecast submodule + +- **Status**: Accepted +- **Date**: 2026-09-02 +- **Deciders**: ruv +- **Owners**: RuView forecast training, security, and release maintainers +- **Tags**: forecasting, training, fal, governance, submodule, cost-control +- **Parent**: ADR-349 +- **Extends**: ADR-348, ADR-349 +- **Supersedes**: None + +## Decision + +ADR-349 specified two governance controls for RuView Forecast's fal.ai +hosted path: operator-verified dataset/schema/policy/recipe authorization, +and an explicit local spend-approval record bound to the request digest and +maximum units. A 9-agent code review of the training stack merged as PR +#1766 found that neither control was actually wired into the code path that +builds a submittable, fal-destined training contract — `FalGovernanceVerifier` +existed with zero callers outside its own tests, and no spend-approval type +existed anywhere in the codebase. + +That code no longer lives directly in this repository. Between the review +and this fix, `v2/crates/ruview-forecast-{core,model,train}` was extracted +into its own repository, `github.com/ruvnet/RuForecast`, and is now vendored +back into `v2/crates/ruforecast` as a git submodule (crates renamed to +`ruforecast-{core,model,train}`). This ADR records two things at the RuView +level: + +1. **The governance and spend-approval gaps are fixed**, in + `ruvnet/RuForecast` PR #2 (branch `fix/governance-and-spend-enforcement`). + The full technical decision record — what changed, why, and the + requirements/acceptance table — lives in that repository's own + [ADR-350](https://github.com/ruvnet/RuForecast/blob/main/docs/adr/ADR-350-fal-governance-and-spend-enforcement.md), + which this ADR defers to rather than duplicating. In summary: a + fal-destined `TrainSpec` can no longer be constructed without a real, + operator-signed, single-use-verified governance receipt; a hosted + reservation can no longer be built without a real, operator-signed spend + approval bound to the exact request digest and cost ceiling; and several + related correctness/resilience gaps on the same trust boundary + (unauthenticated Direct Server webhook, no execution timeout, mismatched + local/hosted artifact budgets, cancellation permanently wedging a job) + were fixed in the same pass. +2. **The submodule pointer in this repository is bumped** to the commit + containing that fix, once `ruvnet/RuForecast`'s own CI is green on that + PR and it merges. This RuView-side change is deliberately small: a + pointer bump plus this ADR, not a re-implementation or re-review of code + that now lives, is tested, and is reviewed in its own repository. + +## Context + +RuView's own copies of ADR-348 and ADR-349 predate the extraction and still +describe the crates as if they lived directly under `v2/crates/`. They are +left as-is here (not corrected path-by-path) since the canonical, actively +maintained copies of the forecast-specific ADRs now live in +`ruvnet/RuForecast`'s own `docs/adr/`, which was seeded with copies of +ADR-348/349 at extraction time and now also carries ADR-350. Consult that +repository for the forecast subsystem's own decision history going forward; +this repository's ADR series should record RuView-level integration +decisions about the vendored submodule (like this one), not attempt to +re-host the submodule's internal history. + +This repo's CLAUDE.md requires an ADR for load-bearing upstream changes. +The governance-enforcement fix is load-bearing for RuView even though none +of its source lines are: it changes what RuView's vendored forecast +subsystem actually requires before it will reach real, billable fal.ai +infrastructure on behalf of a RuView deployment. + +## Consequences + +Once the submodule pointer here is bumped, any RuView-side code, CLI +wrapper, or documentation that assumes the old, unenforced fal.ai +submission path (a bare `SourceState::synthetic` claim with no verified +governance receipt or spend approval) will no longer compile or run against +the vendored crate — `TrainSpec::new_fal_synthetic` and +`ReservedSyntheticSubmission::reserve` both gained new required parameters. +A repository-wide search found no such RuView-side caller today; this ADR +records the change in case one is added later without checking the +submodule's current API first. + +This pointer bump must not merge before `ruvnet/RuForecast` PR #2 merges and +its CI is green — see that PR for status. + +## References + +- [ADR-349](./ADR-349-governed-local-and-fal-forecast-training.md) (this repository's copy; canonical copy is in `ruvnet/RuForecast`) +- [ruvnet/RuForecast PR #2](https://github.com/ruvnet/RuForecast/pull/2) +- [ruvnet/RuForecast ADR-350](https://github.com/ruvnet/RuForecast/blob/main/docs/adr/ADR-350-fal-governance-and-spend-enforcement.md) +- [`v2/crates/ruforecast`](../../v2/crates/ruforecast) (submodule) diff --git a/docs/adr/README.md b/docs/adr/README.md index d317a01c5..83a12eba2 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -95,6 +95,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme | [ADR-348](ADR-348-independent-rust-multivariate-forecasting.md) | Independent Rust multivariate forecasting for RuView | Proposed | | [ADR-349](ADR-349-governed-local-and-fal-forecast-training.md) | Governed local and fal.ai forecast training | Proposed | | [ADR-350](ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md) | RuVector predictive memory and RuVLLM authority boundary | Proposed | +| [ADR-354](ADR-354-forecast-governance-enforcement-in-ruforecast-submodule.md) | fal.ai forecast governance and spend enforcement now real, in the vendored RuForecast submodule | Accepted | ### Platform and UI diff --git a/v2/crates/ruforecast b/v2/crates/ruforecast index 448c035cc..a802067f0 160000 --- a/v2/crates/ruforecast +++ b/v2/crates/ruforecast @@ -1 +1 @@ -Subproject commit 448c035cc18f2fa7c05aab711c6ae0fd5cfc4e4b +Subproject commit a802067f0444e10f267c5052119524de1b21747d From 5419bff2a88a890e7c04d71e335e67def74d614f Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 30 Aug 2026 07:37:08 -0400 Subject: [PATCH 04/27] feat(firmware): thermal monitoring, radio throttling, reset reason, heap health The C6 carries a temperature sensor this firmware has never read. Tolerable on a bench board; not once nine nodes are sealed in printed enclosures on a wall with the radio transmitting continuously. What is protected is the ENCLOSURE, not the silicon. The part is rated far past anything it sees here, but PLA softens at its glass transition (~55-60 C) and sags under its own weight over weeks -- a failure that never announces itself. Thresholds are set against DIE temperature with that offset in mind: ~72 C on the die in a small vented box is roughly a 50-55 C enclosure. Setting the trip at 55 would throttle a perfectly healthy node. Throttling reduces WiFi transmit power in 3 dB steps, the dominant heat source and the one that degrades gracefully -- links weaken but keep working, whereas cutting capture rate would blind the node. Recovery has 8 C of hysteresis because oscillating around the trip point would put a visible RSSI step on every link the node has, repeatedly. `thermal_tx_dbm()` and `thermal_state()` are exported for a specific reason: a throttled node's RSSI falls at both ends, which is indistinguishable from an obstruction in the link diagnostics. Anything reading link strength must be able to tell a hot node from a wall. Also records two things nothing tracked before: - **Reset reason at boot.** A node that reboots tells you almost nothing; one that reboots with ESP_RST_BROWNOUT tells you its supply sagged, which on shared outlets is a wiring problem rather than a firmware one. Three nodes dropping together on 2026-08-28 would have been answered by this one line. - **Free and minimum-free heap** in the periodic log. A slow leak and an overheat look identical after ten days unless the minimum has been visibly walking downward the whole time. Kconfig-gated (default y on C6), thresholds configurable. Builds clean at 0xfe990, 45% partition free. Co-Authored-By: claude-flow (cherry picked from commit 898295fba7b3873b37e85a5ffc0ec87fe609aa23) --- firmware/esp32-csi-node/main/CMakeLists.txt | 3 +- .../esp32-csi-node/main/Kconfig.projbuild | 53 ++++++ .../esp32-csi-node/main/adaptive_controller.c | 5 + firmware/esp32-csi-node/main/main.c | 31 +++ firmware/esp32-csi-node/main/thermal.c | 176 ++++++++++++++++++ firmware/esp32-csi-node/main/thermal.h | 46 +++++ 6 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 firmware/esp32-csi-node/main/thermal.c create mode 100644 firmware/esp32-csi-node/main/thermal.h diff --git a/firmware/esp32-csi-node/main/CMakeLists.txt b/firmware/esp32-csi-node/main/CMakeLists.txt index 0cabf5df2..dc527813a 100644 --- a/firmware/esp32-csi-node/main/CMakeLists.txt +++ b/firmware/esp32-csi-node/main/CMakeLists.txt @@ -1,5 +1,6 @@ set(SRCS - "main.c" "csi_collector.c" "stream_sender.c" "nvs_config.c" + "main.c" "csi_collector.c" + "thermal.c" "stream_sender.c" "nvs_config.c" "edge_processing.c" "ota_update.c" "power_mgmt.c" "wasm_runtime.c" "wasm_upload.c" "rvf_parser.c" "mmwave_sensor.c" diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index 6f4f5bfd8..9487cf82d 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -517,3 +517,56 @@ menu "Onboard LED (ADR-183)" colormap, in milli-units (250 = 0.25). Lower = more sensitive (reaches yellow with less motion). endmenu + +menu "Thermal monitoring" + +config THERMAL_MONITOR + bool "Read the on-chip temperature sensor and throttle the radio" + depends on IDF_TARGET_ESP32C6 + default y + help + The C6 has a temperature sensor this firmware otherwise never reads. + Harmless on a bare bench board; not once a node is sealed in a printed + enclosure on a wall with the radio transmitting continuously. + + What is being protected is the ENCLOSURE, not the silicon. The part is + rated far past anything it sees here, but PLA softens at its glass + transition (~55-60 C) and sags slowly under its own weight, which never + announces itself. + +config THERMAL_WARN_C + int "Warn above this die temperature (C)" + depends on THERMAL_MONITOR + range 40 100 + default 65 + +config THERMAL_THROTTLE_C + int "Reduce transmit power above this die temperature (C)" + depends on THERMAL_MONITOR + range 45 105 + default 72 + help + Die temperature, not enclosure temperature -- the sensor is on the chip + and runs hotter than the plastic around it. ~72 C on the die in a small + vented box is roughly a 50-55 C enclosure, which is where PLA begins to + matter. Setting this at 55 would throttle a perfectly healthy node. + + PETG (glass transition ~80 C) removes the concern; raise this if the + whole fleet is printed in it. + +config THERMAL_CRITICAL_C + int "Minimum transmit power above this die temperature (C)" + depends on THERMAL_MONITOR + range 50 110 + default 80 + +config THERMAL_HYSTERESIS_C + int "Degrees below the throttle point before recovering" + depends on THERMAL_MONITOR + range 1 20 + default 8 + help + Recovering at the same threshold that tripped would oscillate, and every + oscillation is a visible RSSI step on every link the node has. + +endmenu diff --git a/firmware/esp32-csi-node/main/adaptive_controller.c b/firmware/esp32-csi-node/main/adaptive_controller.c index fe4bf9d5b..7c80cf83e 100644 --- a/firmware/esp32-csi-node/main/adaptive_controller.c +++ b/firmware/esp32-csi-node/main/adaptive_controller.c @@ -13,6 +13,7 @@ */ #include "adaptive_controller.h" +#include "thermal.h" #include "rv_radio_ops.h" #include "rv_feature_state.h" #include "rv_mesh.h" @@ -331,6 +332,10 @@ static void emit_feature_state(void) static void slow_loop_cb(TimerHandle_t t) { (void)t; + /* Sample the die and apply the thermal policy. The slow loop (30 s) is the + * right cadence: the die does not change quickly, and reading it more + * often buys nothing but noise. No-op unless CONFIG_THERMAL_MONITOR. */ + thermal_tick(); /* ADR-081 L3: publish a HEALTH mesh message every slow tick * (default 30 s). The coordinator uses these to track liveness and * detect sync-error drift. */ diff --git a/firmware/esp32-csi-node/main/main.c b/firmware/esp32-csi-node/main/main.c index 6991efe68..5771dc0e2 100644 --- a/firmware/esp32-csi-node/main/main.c +++ b/firmware/esp32-csi-node/main/main.c @@ -21,6 +21,7 @@ #include "led_strip.h" #include "csi_collector.h" +#include "thermal.h" #include "stream_sender.h" #include "nvs_config.h" #include "edge_processing.h" @@ -318,6 +319,36 @@ void app_main(void) ESP_LOGI(TAG, "Mock CSI active (scenario=%d)", CONFIG_CSI_MOCK_SCENARIO); } #else + /* Why did we just start? Nothing recorded this before, and it is the + * single most useful line in a fleet log: a node that reboots tells you + * almost nothing, but a node that reboots with ESP_RST_BROWNOUT tells you + * its supply sagged, which on shared outlets is a wiring problem rather + * than a firmware one. Three nodes dropping together on 2026-08-28 would + * have been answered by this line. */ + { + esp_reset_reason_t rr = esp_reset_reason(); + const char *why = + rr == ESP_RST_POWERON ? "power-on" : + rr == ESP_RST_EXT ? "external reset" : + rr == ESP_RST_SW ? "software restart" : + rr == ESP_RST_PANIC ? "PANIC (crash)" : + rr == ESP_RST_INT_WDT ? "interrupt watchdog" : + rr == ESP_RST_TASK_WDT ? "task watchdog" : + rr == ESP_RST_WDT ? "other watchdog" : + rr == ESP_RST_BROWNOUT ? "BROWNOUT (supply sagged)" : + rr == ESP_RST_DEEPSLEEP? "deep-sleep wake" : "unknown"; + if (rr == ESP_RST_PANIC || rr == ESP_RST_BROWNOUT || + rr == ESP_RST_INT_WDT || rr == ESP_RST_TASK_WDT) { + ESP_LOGW(TAG, "reset reason: %s (%d) <-- not a clean start", why, (int)rr); + } else { + ESP_LOGI(TAG, "reset reason: %s (%d)", why, (int)rr); + } + } + + /* Bring thermal monitoring up before the radio is loaded, so the first + * reading is taken against a known-idle baseline rather than mid-burst. */ + thermal_init(); + csi_collector_init(); /* ADR-073: Start multi-frequency channel hopping if configured in NVS. */ diff --git a/firmware/esp32-csi-node/main/thermal.c b/firmware/esp32-csi-node/main/thermal.c new file mode 100644 index 000000000..df411fbf8 --- /dev/null +++ b/firmware/esp32-csi-node/main/thermal.c @@ -0,0 +1,176 @@ +/* On-chip thermal monitoring and radio throttling. + * + * The ESP32-C6 carries a temperature sensor (SOC_TEMP_SENSOR_SUPPORTED) that + * nothing in this firmware has ever read. That was tolerable on a bare board + * on a bench; it is not once nine nodes are sealed in printed enclosures and + * left on a wall for months with the radio transmitting continuously. + * + * What we are protecting is the enclosure, not the silicon. The part is rated + * well past anything it will see here, but PLA goes soft at its glass + * transition around 55-60 C -- and that is sagging under its own weight over + * weeks, not a dramatic failure, so it would never announce itself. + * + * The sensor reads DIE temperature, which runs hotter than the plastic around + * it. Thresholds are therefore set against the die with that offset in mind: + * a die at ~72 C in a small vented box is roughly a 50-55 C enclosure, which + * is where PLA starts to matter. They are deliberately not set at 55 C -- that + * would throttle a perfectly healthy node. + * + * Throttling reduces WiFi transmit power, which is the dominant heat source + * and degrades gracefully: links weaken but keep working, where cutting the + * capture rate would blind the node entirely. + * + * IMPORTANT for anyone reading link diagnostics: a throttled node's RSSI drops + * at both ends, which looks exactly like an obstruction. `thermal_state()` and + * `thermal_tx_dbm()` exist so that can be distinguished from a cage or a wall + * rather than misdiagnosed as one. + */ + +#include "sdkconfig.h" +#include "thermal.h" + +#if defined(CONFIG_THERMAL_MONITOR) && defined(CONFIG_IDF_TARGET_ESP32C6) + +#include "driver/temperature_sensor.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "esp_system.h" +#include "esp_heap_caps.h" + +static const char *TAG = "thermal"; + +static temperature_sensor_handle_t s_sensor; +static bool s_ready; +static float s_last_c = -273.0f; +static float s_peak_c = -273.0f; +static thermal_state_t s_state = THERMAL_OK; + +/* esp_wifi_set_max_tx_power() takes 0.25 dBm units. 80 = 20 dBm, the default + * on this part; each step down is 3 dB, which halves radiated power. */ +#define TXP_FULL 80 /* 20 dBm */ +#define TXP_STEP1 68 /* 17 dBm */ +#define TXP_STEP2 56 /* 14 dBm */ +#define TXP_MIN 44 /* 11 dBm */ + +static int8_t s_txp = TXP_FULL; + +static void apply_tx_power(int8_t q) +{ + if (q == s_txp) { + return; + } + esp_err_t r = esp_wifi_set_max_tx_power(q); + if (r == ESP_OK) { + ESP_LOGW(TAG, "tx power %d.%02d dBm -> %d.%02d dBm (thermal)", + s_txp / 4, (s_txp % 4) * 25, q / 4, (q % 4) * 25); + s_txp = q; + } else { + ESP_LOGW(TAG, "esp_wifi_set_max_tx_power(%d) failed: %s", + q, esp_err_to_name(r)); + } +} + +esp_err_t thermal_init(void) +{ + temperature_sensor_config_t cfg = + TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 110); + esp_err_t r = temperature_sensor_install(&cfg, &s_sensor); + if (r != ESP_OK) { + ESP_LOGW(TAG, "sensor install failed: %s (continuing unmonitored)", + esp_err_to_name(r)); + return r; + } + r = temperature_sensor_enable(s_sensor); + if (r != ESP_OK) { + ESP_LOGW(TAG, "sensor enable failed: %s", esp_err_to_name(r)); + return r; + } + s_ready = true; + ESP_LOGI(TAG, "monitoring on: warn %d C, throttle %d C, critical %d C " + "(die temperature; protecting the enclosure, not the chip)", + CONFIG_THERMAL_WARN_C, CONFIG_THERMAL_THROTTLE_C, + CONFIG_THERMAL_CRITICAL_C); + return ESP_OK; +} + +void thermal_tick(void) +{ + if (!s_ready) { + return; + } + float c; + if (temperature_sensor_get_celsius(s_sensor, &c) != ESP_OK) { + return; + } + s_last_c = c; + if (c > s_peak_c) { + s_peak_c = c; + } + + /* Hysteresis on the way down: recovering at the same threshold that + * tripped would oscillate, and each oscillation is a visible RSSI step on + * every link this node has. */ + const float recover = (float)CONFIG_THERMAL_THROTTLE_C + - (float)CONFIG_THERMAL_HYSTERESIS_C; + + thermal_state_t next = s_state; + if (c >= (float)CONFIG_THERMAL_CRITICAL_C) { + next = THERMAL_CRITICAL; + } else if (c >= (float)CONFIG_THERMAL_THROTTLE_C) { + next = THERMAL_THROTTLED; + } else if (c >= (float)CONFIG_THERMAL_WARN_C) { + if (s_state != THERMAL_THROTTLED && s_state != THERMAL_CRITICAL) { + next = THERMAL_WARN; + } + } else if (c < recover) { + next = THERMAL_OK; + } + + if (next != s_state) { + ESP_LOGW(TAG, "%.1f C: %s -> %s", + c, thermal_state_name(s_state), thermal_state_name(next)); + s_state = next; + } + + switch (s_state) { + case THERMAL_CRITICAL: apply_tx_power(TXP_MIN); break; + case THERMAL_THROTTLED: apply_tx_power(TXP_STEP2); break; + case THERMAL_WARN: apply_tx_power(TXP_STEP1); break; + case THERMAL_OK: apply_tx_power(TXP_FULL); break; + } + + static int64_t s_last_log_us; + int64_t now = esp_timer_get_time(); + if (now - s_last_log_us > 60LL * 1000 * 1000) { + s_last_log_us = now; + /* Heap rides along because it is the other thing that fails slowly + * and silently over weeks. A node that reboots mysteriously after ten + * days looks identical to one that overheated, unless the minimum + * free heap has been visibly walking downwards the whole time. */ + ESP_LOGI(TAG, "die %.1f C (peak %.1f C) state=%s tx=%d.%02d dBm " + "heap %u min %u", + s_last_c, s_peak_c, thermal_state_name(s_state), + s_txp / 4, (s_txp % 4) * 25, + (unsigned)esp_get_free_heap_size(), + (unsigned)esp_get_minimum_free_heap_size()); + } +} + +float thermal_celsius(void) { return s_last_c; } +float thermal_peak_celsius(void) { return s_peak_c; } +thermal_state_t thermal_state(void) { return s_state; } +int8_t thermal_tx_dbm(void) { return (int8_t)(s_txp / 4); } + +const char *thermal_state_name(thermal_state_t s) +{ + switch (s) { + case THERMAL_OK: return "ok"; + case THERMAL_WARN: return "warn"; + case THERMAL_THROTTLED: return "throttled"; + case THERMAL_CRITICAL: return "critical"; + } + return "?"; +} + +#endif /* CONFIG_THERMAL_MONITOR && ESP32C6 */ diff --git a/firmware/esp32-csi-node/main/thermal.h b/firmware/esp32-csi-node/main/thermal.h new file mode 100644 index 000000000..a6b0addb5 --- /dev/null +++ b/firmware/esp32-csi-node/main/thermal.h @@ -0,0 +1,46 @@ +/* On-chip thermal monitoring and radio throttling. See thermal.c for why the + * thresholds are set against die temperature rather than the plastic. */ +#pragma once + +#include "esp_err.h" +#include + +typedef enum { + THERMAL_OK = 0, + THERMAL_WARN, + THERMAL_THROTTLED, + THERMAL_CRITICAL, +} thermal_state_t; + +#if defined(CONFIG_THERMAL_MONITOR) && defined(CONFIG_IDF_TARGET_ESP32C6) + +esp_err_t thermal_init(void); + +/* Sample and apply policy. Call from a slow loop -- the die does not change + * fast, and reading it more often buys nothing. */ +void thermal_tick(void); + +float thermal_celsius(void); +float thermal_peak_celsius(void); +thermal_state_t thermal_state(void); + +/* Current WiFi transmit ceiling in whole dBm. + * + * Exposed because a throttled node's RSSI falls at both ends, which is + * indistinguishable from an obstruction in the link diagnostics. Anything + * reading link strength should check this before concluding a wall moved. */ +int8_t thermal_tx_dbm(void); + +const char *thermal_state_name(thermal_state_t s); + +#else /* stubs: S3 build and when monitoring is off */ + +static inline esp_err_t thermal_init(void) { return ESP_OK; } +static inline void thermal_tick(void) {} +static inline float thermal_celsius(void) { return -273.0f; } +static inline float thermal_peak_celsius(void) { return -273.0f; } +static inline thermal_state_t thermal_state(void) { return THERMAL_OK; } +static inline int8_t thermal_tx_dbm(void) { return 20; } +static inline const char *thermal_state_name(thermal_state_t s) { (void)s; return "n/a"; } + +#endif From ed006abd58d06dcab5d98e350deb108c2004b429 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 30 Aug 2026 08:55:48 -0400 Subject: [PATCH 05/27] refactor(firmware): separate acting on temperature from measuring it Reading the sensor is inert. Reducing transmit power moves every RSSI the node has, at both ends of every link -- introducing that during a three-to-nine node bring-up would give any surprise two candidate causes and no way to separate them. So CONFIG_THERMAL_THROTTLE is now its own option, default off, while monitoring stays on. The first flash therefore reports die temperature and peak without ever changing behaviour, which is exactly what is needed to answer whether these boards run hot in enclosures at all. If none of them ever does -- the likely outcome at ~0.4 W average in a vented box -- throttling never needs enabling and the thresholds cost nothing. Co-Authored-By: claude-flow (cherry picked from commit 187eb17ba9549a96aa63ce0fda37888c8c65650c) --- firmware/esp32-csi-node/main/Kconfig.projbuild | 13 +++++++++++++ firmware/esp32-csi-node/main/thermal.c | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index 9487cf82d..0d5daa7a5 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -534,6 +534,19 @@ config THERMAL_MONITOR transition (~55-60 C) and sags slowly under its own weight, which never announces itself. +config THERMAL_THROTTLE + bool "Act on the temperature by reducing transmit power" + depends on THERMAL_MONITOR + default n + help + Separate from monitoring on purpose. Reading the sensor is inert; + reducing transmit power moves every RSSI the node has, and doing that + mid-experiment gives any surprise two candidate causes. + + Leave off until the fleet is stable and the logs show a node genuinely + running hot. If none ever does -- which is the likely outcome -- this + never needs enabling, and the thresholds below cost nothing. + config THERMAL_WARN_C int "Warn above this die temperature (C)" depends on THERMAL_MONITOR diff --git a/firmware/esp32-csi-node/main/thermal.c b/firmware/esp32-csi-node/main/thermal.c index df411fbf8..407d8c2e5 100644 --- a/firmware/esp32-csi-node/main/thermal.c +++ b/firmware/esp32-csi-node/main/thermal.c @@ -133,12 +133,20 @@ void thermal_tick(void) s_state = next; } +#ifdef CONFIG_THERMAL_THROTTLE + /* Acting on the reading is separate from taking it. Measuring is inert; + * changing transmit power mid-experiment moves every RSSI this node has, + * which is not something to introduce during a fleet bring-up when any + * surprise needs a single candidate cause. Turn this on once the fleet is + * stable AND the logs show a node actually gets hot -- if none ever does, + * it never needs turning on at all. */ switch (s_state) { case THERMAL_CRITICAL: apply_tx_power(TXP_MIN); break; case THERMAL_THROTTLED: apply_tx_power(TXP_STEP2); break; case THERMAL_WARN: apply_tx_power(TXP_STEP1); break; case THERMAL_OK: apply_tx_power(TXP_FULL); break; } +#endif static int64_t s_last_log_us; int64_t now = esp_timer_get_time(); From 29b3968731346fb3cbcc6fddb5be3bb31aca0a5b Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 2 Sep 2026 23:53:02 -0400 Subject: [PATCH 06/27] fix(firmware): size the subcarrier buffers for HE20, not just pre-HE EDGE_MAX_SUBCARRIERS is a flat 128, which silently truncates CSI on any part that carries HE20. An ESP32-C6 or C5 reports 256 subcarriers per frame; every buffer sized by this constant holds half of them, and edge_process_frame() returns early on n_subcarriers > EDGE_MAX_SUBCARRIERS, so on those parts the edge pipeline stops entirely -- no log past init and no vitals packet ever reaching the sink. Gated on CONFIG_SOC_WIFI_HE_SUPPORT, the same switch csi_collector.c already uses to pick its rx_ctrl layout, so pre-HE parts keep 128 and do not pay ~3.5 KB of .bss they can never use. Co-Authored-By: claude-flow --- .../esp32-csi-node/main/edge_processing.h | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/firmware/esp32-csi-node/main/edge_processing.h b/firmware/esp32-csi-node/main/edge_processing.h index cb53ad66c..0fe280b0a 100644 --- a/firmware/esp32-csi-node/main/edge_processing.h +++ b/firmware/esp32-csi-node/main/edge_processing.h @@ -33,7 +33,31 @@ #define EDGE_MAX_IQ_BYTES 1024 /**< Max I/Q payload per slot. */ #define EDGE_PHASE_HISTORY_LEN 256 /**< Phase history buffer depth. */ #define EDGE_TOP_K 8 /**< Top-K subcarriers to track. */ -#define EDGE_MAX_SUBCARRIERS 128 /**< Max subcarriers per frame. */ +/** + * Max subcarriers per frame the edge pipeline will process. + * + * Target-conditional since 2026-08-28. The original 128 was an ESP32-S3 + * assumption: pre-HE chips report at most 128 CSI bins. ADR-110 onboarded the + * ESP32-C6 to the *capture* path but not to this one, and a C6 associated to an + * HE-capable AP delivers HE20 frames with 256 bins (iq_len = 512 bytes). + * + * `process_frame()` guards with `n_subcarriers > EDGE_MAX_SUBCARRIERS -> return`, + * so on C6 every frame was rejected and the whole edge pipeline — vitals, + * presence, fall detection, per-slot counting — silently did nothing. The Edge + * DSP task started, logged its banner, and never processed a frame. Confirmed + * on hardware: no edge_proc log past init and no vitals packet ever reaching + * the sink. + * + * HE-capable parts get 256; pre-HE parts keep 128 so they don't pay ~3.5 KB of + * .bss they can never use. CONFIG_SOC_WIFI_HE_SUPPORT is the same switch + * csi_collector.c uses to pick its rx_ctrl layout. + */ +#if CONFIG_SOC_WIFI_HE_SUPPORT +#define EDGE_MAX_SUBCARRIERS 256 /**< HE20 carries 256 CSI bins (C6/C5). */ +#else +#define EDGE_MAX_SUBCARRIERS 128 /**< Pre-HE parts (S3 etc). */ +#endif + /* ---- Measured sample-rate tracking ---- * From 25d7be3c25da08077f37c823714f4f4009475a55 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 25 Aug 2026 22:38:22 -0400 Subject: [PATCH 07/27] fix(sensing-server): wire --node-positions into live NodeInfo output Node position parsing/storage for multistatic fusion worked correctly, but the per-node JSON builder (both the vitals-only and governed/ privacy-gated ESP32 live-data paths) never read the parsed positions - it always emitted the hardcoded fallback [2.0, 0.0, 1.5] regardless of --node-positions. Store the parsed positions in AppStateInner, keyed by 0-based index (= node_id), and read from it at both call sites. Matches upstream issues #228, #249, and the "Known Issues" #301 symptom class ("same output regardless of position"). Co-Authored-By: claude-flow (cherry picked from commit d34d76c54e7ba4a466cf9dc27060684f49756b6b) --- .../wifi-densepose-sensing-server/src/main.rs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 3c6ca0428..286cad1dc 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -1417,6 +1417,9 @@ struct AppStateInner { last_tracker_instant: Option, /// Attention-weighted multi-node CSI fusion engine. multistatic_fuser: MultistaticFuser, + /// Node positions parsed from --node-positions, keyed by 0-based index + /// into the parsed list (which corresponds to node_id). + node_positions_config: HashMap, /// Governed trust-path bridge (ADR-135..146): runs the same live frames /// through the privacy/provenance/witness control plane. Does not alter /// person-count behavior; its trust state (witness, effective class, @@ -1678,6 +1681,7 @@ impl AppStateInner { pose_tracker: PoseTracker::new(), last_tracker_instant: None, multistatic_fuser: MultistaticFuser::new(), + node_positions_config: HashMap::new(), engine_bridge: engine_bridge::EngineBridge::new( wifi_densepose_bfld::PrivacyMode::PrivateHome, 1, @@ -6526,7 +6530,11 @@ async fn udp_receiver_task( .map(|(&id, n)| NodeInfo { node_id: id, rssi_dbm: n.rssi_history.back().copied().unwrap_or(0.0), - position: [2.0, 0.0, 1.5], + position: s + .node_positions_config + .get(&id) + .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64]) + .unwrap_or([2.0, 0.0, 1.5]), amplitude: vec![], subcarrier_count: 0, // Vitals-only path; still expose the sync snapshot @@ -7010,7 +7018,11 @@ async fn udp_receiver_task( .map(|(&id, n)| NodeInfo { node_id: id, rssi_dbm: n.rssi_history.back().copied().unwrap_or(0.0), - position: [2.0, 0.0, 1.5], + position: s + .node_positions_config + .get(&id) + .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64]) + .unwrap_or([2.0, 0.0, 1.5]), amplitude: if suppress_raw { vec![] } else { @@ -8581,7 +8593,7 @@ async fn main() { // threaded into `engine_bridge` so both fusion paths honor the same // WDP_TDM_SLOTS/WDP_GUARD_INTERVAL_US-derived guard (#1049/#1057). let mut engine_bridge_multistatic_cfg: Option = None; - + let mut node_positions_config: HashMap = HashMap::new(); let state: SharedState = Arc::new(RwLock::new(AppStateInner { latest_update: None, rssi_history: VecDeque::new(), @@ -8672,6 +8684,9 @@ async fn main() { "Configured {} node positions for multistatic fusion", positions.len() ); + for (idx, p) in positions.iter().enumerate() { + node_positions_config.insert(idx as u8, *p); + } fuser.set_node_positions(positions); } } @@ -8681,6 +8696,7 @@ async fn main() { }); fuser }, + node_positions_config, engine_bridge: engine_bridge::EngineBridge::new( wifi_densepose_bfld::PrivacyMode::PrivateHome, 1, From 00737441c400cfedbe1d7a70d546e4c7e6a69321 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 30 Aug 2026 00:07:21 -0400 Subject: [PATCH 08/27] perf(firmware): stop the C6 sharing its radio with a stack that cannot receive Every C6 node spends coexistence time-slicing on 802.15.4 -- a single 2.4 GHz transceiver is shared between WiFi, BLE and 15.4 -- against a receive path that provably delivers nothing. That time competes directly with CSI capture, which is the node's entire purpose, and it matters most on a large fleet where the 50 Hz receive gate is already the binding constraint. The evidence that it cannot receive is upstream's own (ADR-110 D1, ruvnet/RuView#762): five configurations tested -- channel 15 and 26, OpenThread on and off, promiscuous on and off, PAN-matched and broadcast -- and every one gave rx=0 while TX stayed 100% reliable on IDF v5.4. ESP-NOW shipped as the workaround and is what this firmware actually syncs on. The stack was nonetheless still initialised at boot and beaconing every 100 ms. Safe to remove: the ADR-018 byte 19 bit 4 "sync valid" flag is ORed from both transports and the 15.4 branch is #ifdef-gated on this symbol, so the working ESP-NOW path keeps setting it. c6_timesync_init is gated on the same symbol and is no longer called. Co-Authored-By: claude-flow --- .../esp32-csi-node/sdkconfig.defaults.esp32c6 | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/firmware/esp32-csi-node/sdkconfig.defaults.esp32c6 b/firmware/esp32-csi-node/sdkconfig.defaults.esp32c6 index e67ee9ade..9e8051b09 100644 --- a/firmware/esp32-csi-node/sdkconfig.defaults.esp32c6 +++ b/firmware/esp32-csi-node/sdkconfig.defaults.esp32c6 @@ -77,3 +77,22 @@ CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=8192 # ── Power: keep CPU at max 160 MHz (C6 ceiling) for DSP throughput ── CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160 + +# ADR-110 D1 / ruvnet/RuView#762: the 802.15.4 RX path never delivers a +# protocol frame on IDF v5.4. Upstream tested five configurations -- channel +# 15 and 26, OpenThread on and off, promiscuous on and off, PAN-matched and +# broadcast -- and every one gave rx=0 with TX at 100% reliability. ESP-NOW +# shipped as the workaround and is what this firmware actually syncs on +# (measured 99.56% cross-board delivery, 104 us smoothed offset). +# +# The C6 has a single 2.4 GHz transceiver shared by WiFi, BLE and 802.15.4, +# so leaving the 15.4 stack running costs coexistence time-slicing against +# the CSI capture that is the entire point of the node -- beaconing every +# 100 ms and listening on a radio that provably returns nothing. Disable it +# and give the airtime back to WiFi, which matters most at nine nodes where +# the 50 Hz receive gate is already the binding constraint. +# +# Safe to turn off: the ADR-018 byte 19 bit 4 "sync valid" flag is ORed from +# both transports and the 15.4 branch is #ifdef-gated on this symbol, so the +# working ESP-NOW path keeps setting it. +CONFIG_C6_TIMESYNC_ENABLE=n From fbecae4e0244eef2d5333e8049777bc2c20e4c48 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 3 Sep 2026 11:42:16 -0400 Subject: [PATCH 09/27] fix(ui): never present invented data as measured The dashboard flip-flopped between "absent" and "present_still" after navigating away and back. The cause was not sensing. On a dropped socket the client fell back to a built-in simulation and rendered synthetic values that were indistinguishable from live ones, so the UI was showing made-up readings and saying nothing about it. That is the most damaging failure this UI can have. A sensing system that invents data silently is worse than one that shows nothing, because every downstream judgement -- including which nodes to move and whether a change helped -- is then made against fiction. Changes: - Client-side simulation is default-OFF and must be opted into explicitly. - An unreachable server retries forever instead of substituting data. - A banner states plainly whenever what is displayed is not live. The labels say what they mean: "NO DATA - SERVER UNREACHABLE" for an unreachable server, and "INVENTED DATA - NOT MEASURED" for simulation, replacing "OFFLINE - CLIENT SIMULATION", which read like a status rather than a warning. Also fixes a reconnect race in the same path: two in-flight connects could each install handlers, so a stale socket kept overwriting fresher state. That race is what made the flip-flop intermittent and hard to attribute. Co-Authored-By: claude-flow --- ui/app.js | 21 ++++++ ui/components/DashboardTab.js | 3 +- ui/components/LiveDemoTab.js | 6 +- ui/components/SensingTab.js | 3 +- ui/services/sensing.service.js | 134 ++++++++++++++++++++++++++++++--- ui/style.css | 8 ++ ui/utils/data-source-banner.js | 131 ++++++++++++++++++++++++++++++++ 7 files changed, 290 insertions(+), 16 deletions(-) create mode 100644 ui/utils/data-source-banner.js diff --git a/ui/app.js b/ui/app.js index 5c5bada66..55e4f741e 100644 --- a/ui/app.js +++ b/ui/app.js @@ -28,6 +28,7 @@ import { i18n } from './utils/i18n.js'; import { ScreenshotTool } from './utils/screenshot.js'; import { UptimeClock } from './utils/uptime-clock.js'; import { QuickSettings } from './utils/quick-settings.js'; +import { dataSourceBanner } from './utils/data-source-banner.js'; class WiFiDensePoseApp { constructor() { @@ -98,6 +99,10 @@ class WiFiDensePoseApp { this.showBackendStatus('Backend unavailable — start sensing-server', 'warning'); } + // Mount the global provenance bar before the service starts, so the very + // first non-live state is visible on whichever tab is showing. + dataSourceBanner.init(); + // Start the sensing WebSocket service early so the dashboard and // live-demo tabs can show the correct data-source status immediately. sensingService.start(); @@ -360,6 +365,22 @@ class WiFiDensePoseApp { window.addEventListener('beforeunload', () => { this.cleanup(); }); + + // `beforeunload` above tears down every component and disconnects the + // sensing WebSocket. If the browser restores this page from bfcache on + // Back/Forward instead of doing a fresh navigation, that torn-down state + // comes back frozen exactly as-is — nothing re-runs DOMContentLoaded, so + // sensingService.start() never fires again and disposed components (e.g. + // Room Builder) never get recreated. `pageshow`'s `event.persisted` is + // the standard signal for "this is a bfcache restore, not a fresh load"; + // forcing a real reload here is simpler and more robust than trying to + // hand-reinitialize every component/connection back to a clean state. + window.addEventListener('pageshow', (event) => { + if (event.persisted) { + console.log('Page restored from bfcache after cleanup() — forcing reload'); + window.location.reload(); + } + }); } // Handle window resize diff --git a/ui/components/DashboardTab.js b/ui/components/DashboardTab.js index 9ecd02262..5e505fa99 100644 --- a/ui/components/DashboardTab.js +++ b/ui/components/DashboardTab.js @@ -95,7 +95,8 @@ export class DashboardTab { 'live': { text: 'ESP32', status: 'healthy', msg: 'Real hardware connected' }, 'server-simulated': { text: 'SIMULATED', status: 'warning', msg: 'Server running without hardware' }, 'reconnecting': { text: 'RECONNECTING', status: 'degraded', msg: 'Attempting to connect...' }, - 'simulated': { text: 'OFFLINE', status: 'unhealthy', msg: 'Server unreachable, local fallback' }, + 'unreachable': { text: 'NO DATA', status: 'unhealthy', msg: 'Server unreachable — readings below are stale' }, + 'simulated': { text: 'INVENTED', status: 'unhealthy', msg: 'Browser-generated data, not measured' }, }; const cfg = config[ds] || config['reconnecting']; el.className = `component-status status-${cfg.status}`; diff --git a/ui/components/LiveDemoTab.js b/ui/components/LiveDemoTab.js index 4dec767d2..b4de941c3 100644 --- a/ui/components/LiveDemoTab.js +++ b/ui/components/LiveDemoTab.js @@ -1318,7 +1318,8 @@ export class LiveDemoTab { const ds = sensingService.dataSource; if (ds === 'live') return 'Active \u2014 ESP32 Live'; if (ds === 'server-simulated') return 'Active \u2014 Simulated Data'; - if (ds === 'simulated') return 'Active \u2014 Offline Simulation'; + if (ds === 'unreachable') return 'No Data \u2014 Server Unreachable'; + if (ds === 'simulated') return 'INVENTED DATA \u2014 Not Measured'; return 'Connecting...'; } @@ -1331,7 +1332,8 @@ export class LiveDemoTab { 'live': { text: 'LIVE \u2014 ESP32 Hardware Connected', cls: 'demo-source-live' }, 'server-simulated': { text: 'SIMULATED DATA \u2014 No Hardware Detected', cls: 'demo-source-sim' }, 'reconnecting': { text: 'RECONNECTING TO SERVER...', cls: 'demo-source-reconnecting' }, - 'simulated': { text: 'OFFLINE \u2014 Server Unreachable, Local Sim', cls: 'demo-source-offline' }, + 'unreachable': { text: 'NO DATA \u2014 Server Unreachable, Display Is Stale', cls: 'demo-source-offline' }, + 'simulated': { text: 'INVENTED DATA \u2014 Generated By Your Browser, Not Measured', cls: 'demo-source-offline' }, }; const cfg = config[ds] || config['reconnecting']; banner.textContent = cfg.text; diff --git a/ui/components/SensingTab.js b/ui/components/SensingTab.js index 33387eefe..4ff233bf9 100644 --- a/ui/components/SensingTab.js +++ b/ui/components/SensingTab.js @@ -228,7 +228,8 @@ export class SensingTab { 'live': { text: 'LIVE \u2014 ESP32 HARDWARE', cls: 'sensing-source-live' }, 'server-simulated': { text: 'SIMULATED \u2014 NO HARDWARE', cls: 'sensing-source-server-sim' }, 'reconnecting': { text: 'RECONNECTING...', cls: 'sensing-source-reconnecting' }, - 'simulated': { text: 'OFFLINE \u2014 CLIENT SIMULATION', cls: 'sensing-source-simulated' }, + 'unreachable': { text: 'NO DATA \u2014 SERVER UNREACHABLE', cls: 'sensing-source-simulated' }, + 'simulated': { text: 'INVENTED DATA \u2014 NOT MEASURED', cls: 'sensing-source-simulated' }, }; const cfg = bannerConfig[dataSource] || bannerConfig.reconnecting; banner.textContent = cfg.text; diff --git a/ui/services/sensing.service.js b/ui/services/sensing.service.js index 57e37cdc2..09f2e6b94 100644 --- a/ui/services/sensing.service.js +++ b/ui/services/sensing.service.js @@ -37,6 +37,39 @@ const MAX_RECONNECT_ATTEMPTS = 20; const SIM_FALLBACK_AFTER_ATTEMPTS = 5; const SIMULATION_INTERVAL = 500; // ms +export const SIM_FALLBACK_STORAGE_KEY = 'ruview-client-simulation'; + +/** + * Whether the client may invent sensing frames when the server is unreachable. + * + * Default **off**, deliberately. The generated frames are plausible — presence + * derived from `Math.sin(t * 0.3)` crossing a fixed threshold produces a steady + * absent/present_still alternation that reads exactly like a real classifier + * struggling at its decision boundary. A reader with no reason to suspect the + * server had dropped will diagnose the sensing pipeline, and be wrong. That is + * not a hypothetical: it cost a full debugging session on 2026-08-28, chasing a + * "flip-flop" that was this function all along while the server sat at a + * perfectly steady presence=true. + * + * For an instrument, "no signal" is the honest reading and a useful one. + * Synthetic motion is only appropriate for a demo, so it must be asked for: + * `?simulate=1`, or `localStorage['ruview-client-simulation'] = 'true'`. + * + * Repository rule: never present synthetic output as measurement (CLAUDE.md). + */ +function clientSimulationAllowed() { + try { + const params = new URLSearchParams(window.location.search); + if (params.get('simulate') === '1') return true; + if (params.get('simulate') === '0') return false; + } catch { /* no window/location — non-browser test context */ } + try { + return localStorage.getItem(SIM_FALLBACK_STORAGE_KEY) === 'true'; + } catch { + return false; + } +} + class SensingService { constructor() { /** @type {WebSocket|null} */ @@ -46,6 +79,11 @@ class SensingService { this._reconnectAttempt = 0; this._reconnectTimer = null; this._simTimer = null; + // Guards the await window inside `_connect()` (see the comment there). + this._connectInFlight = false; + // Bumped per connect attempt and by `stop()`; lets a socket recognise that + // it has been abandoned and stop driving reconnect state. + this._connectEpoch = 0; // Connection state: disconnected | connecting | connected | reconnecting | simulated this._state = 'disconnected'; // Data-source label exposed to the UI: @@ -72,6 +110,10 @@ class SensingService { /** Stop the service entirely. */ stop() { + // Invalidate any attempt still inside its await window, so a socket opened + // after this call does not resurrect the service. + this._connectEpoch++; + this._connectInFlight = false; this._clearTimers(); if (this._ws) { this._ws.close(1000, 'client stop'); @@ -114,7 +156,10 @@ class SensingService { * Current data source label. * "live" — frames are arriving from the real ESP32 over WebSocket * "reconnecting" — WebSocket disconnected; actively retrying, no frames emitted - * "simulated" — max reconnect attempts exhausted; emitting synthetic frames + * "unreachable" — retries exhausted, client simulation off; NO frames emitted + * and whatever is on screen is the last live reading, stale + * "simulated" — client simulation explicitly enabled and running; every + * frame on screen is invented */ get dataSource() { return this._dataSource; @@ -129,6 +174,22 @@ class SensingService { // reconnects would fail on the second attempt. async _connect() { if (this._ws && this._ws.readyState <= WebSocket.OPEN) return; + // The readyState guard above cannot stand alone: this function awaits + // ticket minting before it assigns `this._ws`, so two callers entering + // during that window BOTH pass the guard and BOTH open a socket. The + // second assignment orphans the first — a socket nobody references, still + // open on the server, whose `onclose` fires later and calls + // `_scheduleReconnect()` even though a healthy socket is live. That walks + // `_reconnectAttempt` upward with no outage and eventually trips the + // simulation fallback. Observed on the server as connect/disconnect churn + // on every page load. + if (this._connectInFlight) return; + this._connectInFlight = true; + + // Identifies this attempt for the lifetime of its socket. `stop()` and any + // superseding attempt bump it, so a late event from an abandoned socket is + // recognised and ignored rather than driving reconnect state. + const epoch = ++this._connectEpoch; this._setState('connecting'); @@ -140,15 +201,34 @@ class SensingService { // predating ADR-272, connecting without a ticket is correct. } + if (epoch !== this._connectEpoch) { + // Superseded or stopped while minting. The ticket is single-use and now + // spent; the current attempt mints its own. + this._connectInFlight = false; + return; + } + + let ws; try { - this._ws = new WebSocket(url); + ws = new WebSocket(url); } catch (err) { console.warn('[Sensing] WebSocket constructor failed:', err.message); + this._connectInFlight = false; this._fallbackToSimulation(); return; } - this._ws.onopen = () => { + this._ws = ws; + this._connectInFlight = false; + + /** True while `ws` is still the socket this service is driving. */ + const isCurrent = () => epoch === this._connectEpoch && this._ws === ws; + + ws.onopen = () => { + if (!isCurrent()) { + ws.close(1000, 'superseded'); + return; + } console.info('[Sensing] Connected to', SENSING_WS_URL); this._reconnectAttempt = 0; this._stopSimulation(); @@ -158,7 +238,8 @@ class SensingService { this._detectServerSource(); }; - this._ws.onmessage = (evt) => { + ws.onmessage = (evt) => { + if (!isCurrent()) return; try { const data = JSON.parse(evt.data); this._handleData(data); @@ -167,11 +248,17 @@ class SensingService { } }; - this._ws.onerror = () => { + ws.onerror = () => { // onerror is always followed by onclose, so we handle reconnect there }; - this._ws.onclose = (evt) => { + ws.onclose = (evt) => { + if (!isCurrent()) { + // An abandoned socket closing is not an outage. Reconnect state + // belongs to whichever attempt is current. + console.debug('[Sensing] Ignoring close from superseded socket'); + return; + } console.info('[Sensing] Connection closed (code=%d)', evt.code); this._ws = null; if (evt.code !== 1000) { @@ -184,18 +271,31 @@ class SensingService { } _scheduleReconnect() { + // A retry is already pending; stacking timers would multiply the attempt + // count for a single outage. + if (this._reconnectTimer) return; + if (this._reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) { - console.warn('[Sensing] Max reconnect attempts (%d) reached, switching to simulation', MAX_RECONNECT_ATTEMPTS); - this._fallbackToSimulation(); - return; + if (clientSimulationAllowed()) { + console.warn('[Sensing] Max reconnect attempts (%d) reached, switching to simulation', MAX_RECONNECT_ATTEMPTS); + this._fallbackToSimulation(); + return; + } + // With simulation off there is nothing to give up *for*: keep retrying at + // the ceiling delay so the UI recovers by itself when the server returns, + // rather than requiring a hard refresh. + this._setDataSource('unreachable'); } const delay = RECONNECT_DELAYS[Math.min(this._reconnectAttempt, RECONNECT_DELAYS.length - 1)]; - this._reconnectAttempt++; + // Hold at the cap rather than counting into the thousands over a long + // outage; the delay is already saturated and the number is only read by a + // human in the console. + this._reconnectAttempt = Math.min(this._reconnectAttempt + 1, MAX_RECONNECT_ATTEMPTS); console.info('[Sensing] Reconnecting in %dms (attempt %d/%d)', delay, this._reconnectAttempt, MAX_RECONNECT_ATTEMPTS); this._setState('reconnecting'); - this._setDataSource('reconnecting'); + if (this._dataSource !== 'unreachable') this._setDataSource('reconnecting'); this._reconnectTimer = setTimeout(() => { this._reconnectTimer = null; @@ -205,13 +305,23 @@ class SensingService { // Only start simulation after several failed attempts so a brief hiccup // does not immediately switch the UI to "SIMULATED DATA". if (this._reconnectAttempt >= SIM_FALLBACK_AFTER_ATTEMPTS && this._state !== 'simulated') { - this._fallbackToSimulation(); + if (clientSimulationAllowed()) { + this._fallbackToSimulation(); + } else { + // Say "the server is gone" instead of inventing a room. + this._setDataSource('unreachable'); + } } } // ---- Simulation fallback ----------------------------------------------- _fallbackToSimulation() { + if (!clientSimulationAllowed()) { + this._setDataSource('unreachable'); + this._scheduleReconnect(); + return; + } this._setState('simulated'); this._setDataSource('simulated'); if (this._simTimer) return; // already running diff --git a/ui/style.css b/ui/style.css index 55c1905e1..f8ffd952b 100644 --- a/ui/style.css +++ b/ui/style.css @@ -1804,7 +1804,11 @@ canvas { flex-direction: column; gap: var(--space-12); overflow-y: auto; + /* Without this, `overflow-y: auto` alone still resolves the x axis to auto, + so anything marginally too wide scrolls sideways instead of wrapping. */ + overflow-x: hidden; max-height: 600px; + min-width: 0; } .sensing-card { @@ -1812,6 +1816,10 @@ canvas { border: 1px solid var(--color-card-border); border-radius: var(--radius-md); padding: var(--space-12); + /* Flex items default to min-width:auto, which refuses to shrink below their + content — one wide child then widens the whole 320px column and puts a + horizontal scrollbar on the panel. Cards must shrink to the column. */ + min-width: 0; } .sensing-card-title { diff --git a/ui/utils/data-source-banner.js b/ui/utils/data-source-banner.js new file mode 100644 index 000000000..d5a412eb1 --- /dev/null +++ b/ui/utils/data-source-banner.js @@ -0,0 +1,131 @@ +// Global data-source banner. +// +// The per-tab banner in SensingTab was correct and still invisible where it +// mattered: a reader on Dashboard or Live Demo never saw it, so a screen full +// of client-invented sensing frames was indistinguishable from measurement. +// This bar is fixed to the viewport and present on every tab, so the provenance +// of what is on screen is never more than a glance away. +// +// It shows nothing at all while data is live. An always-on badge becomes +// wallpaper; appearing only when the reading is not trustworthy keeps it +// meaningful. + +import { sensingService } from '../services/sensing.service.js'; + +const STYLE_ID = 'ruview-data-source-banner-style'; +const BAR_ID = 'ruview-data-source-banner'; + +const CONFIG = { + // 'live' is intentionally absent: no banner is shown for trustworthy data. + 'server-simulated': { + text: 'SYNTHETIC DATA — the server is generating this, no hardware is being read', + bg: '#7c3a00', + fg: '#ffd9a0', + }, + reconnecting: { + text: 'RECONNECTING — the display is frozen at the last live reading', + bg: '#33302a', + fg: '#e8dcc0', + }, + unreachable: { + text: 'SERVER UNREACHABLE — nothing on this screen is current', + bg: '#5c1010', + fg: '#ffc9c9', + }, + simulated: { + text: 'SIMULATED — every value on this screen is invented by your browser, not measured', + bg: '#6b0f6b', + fg: '#ffc9ff', + }, +}; + +const CSS = ` +#${BAR_ID} { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100000; + display: none; + padding: 7px 14px; + font: 600 13px/1.35 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + letter-spacing: 0.04em; + text-align: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.45); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35); +} +#${BAR_ID}.is-visible { display: block; } +/* Simulated data is the dangerous case — it looks real. Make the bar move so + it cannot be mistaken for a static decoration. */ +#${BAR_ID}.is-simulated { animation: ruview-dsb-pulse 1.6s ease-in-out infinite; } +@keyframes ruview-dsb-pulse { 50% { filter: brightness(1.45); } } +@media (prefers-reduced-motion: reduce) { + #${BAR_ID}.is-simulated { animation: none; } +} +body.ruview-has-data-source-banner { padding-top: 30px; } +`; + +export class DataSourceBanner { + constructor() { + this._bar = null; + this._unsubscribe = null; + } + + init() { + if (typeof document === 'undefined') return; + + if (!document.getElementById(STYLE_ID)) { + const style = document.createElement('style'); + style.id = STYLE_ID; + style.textContent = CSS; + document.head.appendChild(style); + } + + this._bar = document.getElementById(BAR_ID); + if (!this._bar) { + this._bar = document.createElement('div'); + this._bar.id = BAR_ID; + this._bar.setAttribute('role', 'status'); + // Announce changes, but don't interrupt: the reader is mid-task and the + // message describes context, not an error to act on right now. + this._bar.setAttribute('aria-live', 'polite'); + document.body.appendChild(this._bar); + } + + // dataSource changes are published on the state-listener channel. + this._unsubscribe = sensingService.onStateChange(() => this.render()); + this.render(); + } + + render() { + if (!this._bar) return; + const cfg = CONFIG[sensingService.dataSource]; + + if (!cfg) { + this._bar.classList.remove('is-visible', 'is-simulated'); + document.body.classList.remove('ruview-has-data-source-banner'); + return; + } + + this._bar.textContent = cfg.text; + this._bar.style.background = cfg.bg; + this._bar.style.color = cfg.fg; + this._bar.classList.add('is-visible'); + this._bar.classList.toggle('is-simulated', sensingService.dataSource === 'simulated'); + document.body.classList.add('ruview-has-data-source-banner'); + } + + dispose() { + if (this._unsubscribe) { + this._unsubscribe(); + this._unsubscribe = null; + } + if (this._bar && this._bar.parentNode) { + this._bar.parentNode.removeChild(this._bar); + } + this._bar = null; + document.body.classList.remove('ruview-has-data-source-banner'); + } +} + +export const dataSourceBanner = new DataSourceBanner(); From 5eb8ddde12bd563f049313b8a5a2001e73933cc2 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 25 Aug 2026 23:57:00 -0400 Subject: [PATCH 10/27] fix(sensing-server): scale per-node smoothing to actual CSI rate DEBOUNCE_FRAMES, MOTION_EMA_ALPHA, and BASELINE_EMA_ALPHA were tuned assuming CSI frames arrive at ~10 FPS (per their own doc comments). smooth_and_classify_node() runs once per raw incoming UDP frame, and ESP32-C6 boards measured in the field run at ~48-50 FPS - about 5x faster. That silently compressed a ~0.4s debounce window to ~0.08s and a ~1s motion EMA to ~0.2s of real smoothing, letting per-frame CSI noise flip the reported motion_level almost directly instead of being smoothed - the reported UI "seizure" of rapid label flicker. Rather than hardcode a different fixed rate (which would just move the same bug to whatever chip/capture-mode/config next diverges from it), derive each frame's effective alpha and frame-count thresholds from the node's own measured csi_fps_ema, which is already tracked and burst-filtered elsewhere (observe_csi_frame_arrival). Time constants are back-derived from the original alphas at the assumed 10 FPS point, so behavior is unchanged for a node at exactly 10 FPS and now scales correctly to whatever rate a node actually runs at. smooth_and_classify() (simulated/WiFi-scan sources, already tick- gated at a known fixed cadence) is unaffected - not exhibiting this bug, left as-is. Co-Authored-By: claude-flow (cherry picked from commit d29d9fb02078b066be8557af24023c22e99fa3d1) --- .../wifi-densepose-sensing-server/src/main.rs | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 3c6ca0428..81401b0ed 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -2639,14 +2639,44 @@ fn raw_classify(score: f64) -> String { } /// Debounce frames required before state transition (at ~10 FPS = ~0.4s). +/// Used only by [`smooth_and_classify`] (simulated / WiFi-scan sources, +/// which run on a fixed tick cadence close to this design point). const DEBOUNCE_FRAMES: u32 = 4; /// EMA alpha for motion smoothing (~1s time constant at 10 FPS). +/// Used only by [`smooth_and_classify`]; see above. const MOTION_EMA_ALPHA: f64 = 0.15; /// EMA alpha for slow-adapting baseline (~30s time constant at 10 FPS). +/// Used only by [`smooth_and_classify`]; see above. const BASELINE_EMA_ALPHA: f64 = 0.003; /// Number of warm-up frames before baseline subtraction kicks in. +/// Used only by [`smooth_and_classify`]; see above. const BASELINE_WARMUP: u64 = 50; +/// Debounce duration required before [`smooth_and_classify_node`] accepts a +/// state transition. Equivalent to the original `DEBOUNCE_FRAMES = 4` at the +/// assumed 10 FPS design point. +const NODE_DEBOUNCE_DURATION_SECS: f64 = 0.4; +/// Time constant for [`smooth_and_classify_node`]'s motion-score EMA, +/// derived from `MOTION_EMA_ALPHA` at the assumed 10 FPS design point +/// (τ = -T / ln(1-α)) so behavior is unchanged for a node running at +/// exactly 10 FPS. +/// +/// Frame-count/fixed-alpha smoothing silently scales with actual CSI +/// arrival rate. ESP32-C6 boards observed in the field run CSI at +/// ~48-50 FPS, not the assumed 10 FPS — ~5x faster — which made the +/// debounce trigger in ~0.08s instead of ~0.4s and compressed the "1s" +/// motion EMA to ~0.2s of real smoothing, letting raw per-frame noise +/// flip the reported label almost directly (the reported UI "seizure"). +/// Deriving a per-frame alpha from the node's actual measured +/// `csi_fps_ema` makes the smoothing strength invariant to arrival rate. +const NODE_MOTION_TIME_CONSTANT_SECS: f64 = 0.6154; +/// Time constant for [`smooth_and_classify_node`]'s baseline EMA, same +/// derivation as `NODE_MOTION_TIME_CONSTANT_SECS` from `BASELINE_EMA_ALPHA`. +const NODE_BASELINE_TIME_CONSTANT_SECS: f64 = 33.28; +/// Baseline warm-up duration before baseline subtraction kicks in for +/// [`smooth_and_classify_node`] (originally 50 frames at 10 FPS = 5s). +const NODE_BASELINE_WARMUP_SECS: f64 = 5.0; + /// Apply EMA smoothing, adaptive baseline subtraction, and hysteresis debounce /// to the raw classification. Mutates the smoothing state in `AppStateInner`. fn smooth_and_classify(state: &mut AppStateInner, raw: &mut ClassificationInfo, raw_motion: f64) { @@ -2700,18 +2730,27 @@ fn smooth_and_classify(state: &mut AppStateInner, raw: &mut ClassificationInfo, /// Per-node variant of `smooth_and_classify` that operates on a `NodeState` /// instead of `AppStateInner` (issue #249). fn smooth_and_classify_node(ns: &mut NodeState, raw: &mut ClassificationInfo, raw_motion: f64) { + // Derive this frame's effective time step from the node's actual + // measured CSI rate (already EMA-tracked, burst-filtered elsewhere — + // see `observe_csi_frame_arrival`) rather than assuming 10 FPS. + let fps = ns.csi_fps_ema.max(1.0); + let dt = 1.0 / fps; + let motion_alpha = 1.0 - (-dt / NODE_MOTION_TIME_CONSTANT_SECS).exp(); + let baseline_alpha = 1.0 - (-dt / NODE_BASELINE_TIME_CONSTANT_SECS).exp(); + let debounce_frames_needed = ((NODE_DEBOUNCE_DURATION_SECS * fps).round() as u32).max(1); + let warmup_frames_needed = ((NODE_BASELINE_WARMUP_SECS * fps).round() as u64).max(1); + ns.baseline_frames += 1; - if ns.baseline_frames < BASELINE_WARMUP { + if ns.baseline_frames < warmup_frames_needed { ns.baseline_motion = ns.baseline_motion * 0.9 + raw_motion * 0.1; } else if raw_motion < ns.smoothed_motion + 0.05 { ns.baseline_motion = - ns.baseline_motion * (1.0 - BASELINE_EMA_ALPHA) + raw_motion * BASELINE_EMA_ALPHA; + ns.baseline_motion * (1.0 - baseline_alpha) + raw_motion * baseline_alpha; } let adjusted = (raw_motion - ns.baseline_motion * 0.7).max(0.0); - ns.smoothed_motion = - ns.smoothed_motion * (1.0 - MOTION_EMA_ALPHA) + adjusted * MOTION_EMA_ALPHA; + ns.smoothed_motion = ns.smoothed_motion * (1.0 - motion_alpha) + adjusted * motion_alpha; let sm = ns.smoothed_motion; let candidate = raw_classify(sm); @@ -2721,7 +2760,7 @@ fn smooth_and_classify_node(ns: &mut NodeState, raw: &mut ClassificationInfo, ra ns.debounce_candidate = candidate; } else if candidate == ns.debounce_candidate { ns.debounce_counter += 1; - if ns.debounce_counter >= DEBOUNCE_FRAMES { + if ns.debounce_counter >= debounce_frames_needed { ns.current_motion_level = candidate; ns.debounce_counter = 0; } From 3621a211bdfa419aa71a8984d1c610d3d52d0fc5 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 27 Aug 2026 21:23:02 -0400 Subject: [PATCH 11/27] fix(sensing-server): debounce the room-level classification too `fuse_room` recomputes its plurality vote fresh every cycle with no memory of its own, unlike each node's classification, which is debounced individually. With per-node confidences sitting near the classification boundary -- 44%, 83% and 53% observed simultaneously across three nodes -- whichever two of three happened to agree on a given cycle could flip the room-level result while every node's own reading was individually stable. The room read absent / present_still / absent / present_still in rapid succession. That cascaded into everything gating on presence: the persons array, the rendered figure appearing and disappearing, the live position dot. Each of those looked like its own bug, and none of them was. Adds the missing room-level hysteresis. A new candidate classification must be observed continuously for ROOM_DEBOUNCE_DURATION_SECS before it is adopted; an interrupted run restarts it. The threshold is a DURATION rather than a cycle count, for the same reason as the per-node fix in the previous commit: a count means a different real interval at every rate. Debouncing at the room level does not make the per-node debounce redundant. They answer different questions -- "is this node's reading stable" and "do the nodes agree stably" -- and the observed failure was agreement flapping while every individual reading held steady. Co-Authored-By: claude-flow --- .../wifi-densepose-sensing-server/src/main.rs | 163 +++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 81401b0ed..60487c9cf 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -491,6 +491,57 @@ fn classification_from_room(room: &RoomInference) -> ClassificationInfo { } } +/// Minimum time a candidate room-level classification must be observed +/// *consistently* before the debounced room state actually flips to it. +/// +/// `fuse_room` recomputes its plurality vote fresh every cycle with no +/// memory of its own — unlike each node's own classification, which is +/// individually debounced (`smooth_and_classify_node`). Confirmed live +/// (2026-08-28) as a real source of "flapping": with per-node confidences +/// sitting near the classification boundary (44%/83%/53% observed), whichever +/// 2 of 3 nodes happen to agree can flip cycle-to-cycle even while each +/// node's own reading is individually stable. This adds the missing +/// room-level hysteresis, same idea as the per-node debounce, one layer up. +const ROOM_DEBOUNCE_DURATION_SECS: f64 = 1.5; + +/// Debounced counterpart to [`classification_from_room`] — apply this at +/// live call sites instead, so the room's `presence`/`motion_level` only +/// flips after `raw`'s new candidate class has been observed continuously +/// for [`ROOM_DEBOUNCE_DURATION_SECS`], not on the very first cycle it wins +/// `fuse_room`'s plurality vote. `confidence` is passed through un-debounced +/// (it's a continuous value, not a discrete state, so there's nothing to +/// flap) — only `motion_level`/`presence` are held back. +fn debounce_room_classification(state: &mut AppStateInner, raw: &RoomInference) -> ClassificationInfo { + let candidate = if raw.classification == "unavailable" { + "absent" + } else { + raw.classification.as_str() + }; + let now = std::time::Instant::now(); + + if candidate == state.room_debounced_level { + // Already stable in this state — reset any in-flight candidate. + state.room_debounce_candidate = candidate.to_string(); + state.room_debounce_since = None; + } else if candidate == state.room_debounce_candidate { + let since = *state.room_debounce_since.get_or_insert(now); + if now.duration_since(since).as_secs_f64() >= ROOM_DEBOUNCE_DURATION_SECS { + state.room_debounced_level = candidate.to_string(); + state.room_debounce_since = None; + } + } else { + // New candidate — restart the hold timer. + state.room_debounce_candidate = candidate.to_string(); + state.room_debounce_since = Some(now); + } + + ClassificationInfo { + motion_level: state.room_debounced_level.clone(), + presence: state.room_debounced_level != "absent", + confidence: raw.confidence, + } +} + /// ADR-297 — the window a node may be silent before it stops contributing to /// the fused room aggregate (its entities go stale/unavailable rather than /// holding a frozen online value). Mirrors the 10 s active-node filter used to @@ -612,6 +663,98 @@ mod issue_1554_room_classification_tests { } } +#[cfg(test)] +mod debounce_room_classification_tests { + //! `fuse_room`'s plurality vote has no memory of its own — confirmed live + //! (2026-08-28) as a real "flapping" source when per-node confidences sit + //! near the classification boundary: whichever 2 of 3 nodes happen to + //! agree on a given cycle can flip the room result even while each + //! node's own reading is individually stable. `debounce_room_classification` + //! adds the missing room-level hysteresis (same idea as each node's own + //! debounce, one layer up). + use super::{debounce_room_classification, AppStateInner, RoomInference}; + use std::time::{Duration, Instant}; + + fn room(classification: &str, confidence: f64) -> RoomInference { + RoomInference { + classification: classification.to_string(), + confidence, + contributing_nodes: 3, + } + } + + #[test] + fn single_cycle_candidate_does_not_flip_immediately() { + let mut state = AppStateInner::minimal(); + let c = debounce_room_classification(&mut state, &room("present_still", 0.6)); + assert_eq!(c.motion_level, "absent", "one cycle must not be enough to flip"); + assert!(!c.presence); + } + + #[test] + fn repeated_but_recent_candidate_still_does_not_flip() { + let mut state = AppStateInner::minimal(); + debounce_room_classification(&mut state, &room("present_still", 0.6)); + // Second cycle, essentially no time elapsed — still under the hold duration. + let c = debounce_room_classification(&mut state, &room("present_still", 0.6)); + assert_eq!(c.motion_level, "absent"); + } + + #[test] + fn candidate_sustained_past_hold_duration_flips() { + let mut state = AppStateInner::minimal(); + // Simulate "present_still" having already been the candidate for 2s + // (> ROOM_DEBOUNCE_DURATION_SECS = 1.5s) without needing to sleep in + // the test. + state.room_debounce_candidate = "present_still".to_string(); + state.room_debounce_since = Some(Instant::now() - Duration::from_secs(2)); + + let c = debounce_room_classification(&mut state, &room("present_still", 0.7)); + assert_eq!(c.motion_level, "present_still"); + assert!(c.presence); + } + + #[test] + fn flapping_candidate_resets_the_hold_timer() { + let mut state = AppStateInner::minimal(); + // "present_still" has been pending 1s (not yet enough to flip). + state.room_debounce_candidate = "present_still".to_string(); + state.room_debounce_since = Some(Instant::now() - Duration::from_secs(1)); + + // A different candidate arrives — this is exactly the flapping + // pattern (2-of-3 node agreement flipping cycle to cycle). The hold + // timer must restart for the new candidate, not carry over. + let c = debounce_room_classification(&mut state, &room("active", 0.5)); + assert_eq!(c.motion_level, "absent", "still debounced to the old stable state"); + assert_eq!(state.room_debounce_candidate, "active"); + assert!( + state.room_debounce_since.unwrap().elapsed() < Duration::from_millis(100), + "hold timer must have restarted for the new candidate" + ); + } + + #[test] + fn already_stable_state_reports_immediately_without_waiting() { + let mut state = AppStateInner::minimal(); + state.room_debounced_level = "present_moving".to_string(); + let c = debounce_room_classification(&mut state, &room("present_moving", 0.9)); + assert_eq!(c.motion_level, "present_moving", "no need to re-debounce an already-stable state"); + assert!(c.presence); + } + + #[test] + fn unavailable_room_debounces_toward_absent_like_any_other_candidate() { + let mut state = AppStateInner::minimal(); + state.room_debounced_level = "present_still".to_string(); + state.room_debounce_candidate = "absent".to_string(); + state.room_debounce_since = Some(Instant::now() - Duration::from_secs(2)); + + let c = debounce_room_classification(&mut state, &RoomInference::unavailable()); + assert_eq!(c.motion_level, "absent"); + assert!(!c.presence); + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] struct SignalField { grid_size: [usize; 3], @@ -1410,6 +1553,15 @@ struct AppStateInner { /// Per-node sensing state for multi-node deployments. /// Keyed by `node_id` from the ESP32 frame header. node_states: HashMap, + /// Debounced room-level classification state — see + /// `debounce_room_classification`. `fuse_room` is a fresh, memoryless + /// plurality vote every cycle with no debounce of its own (unlike each + /// node's own classification, which is debounced individually), so with + /// per-node confidences sitting near a boundary the room-level result + /// can flip cycle-to-cycle even though each node's own reading is stable. + room_debounced_level: String, + room_debounce_candidate: String, + room_debounce_since: Option, // ── Accuracy sprint: Kalman tracker, multistatic fusion, eigenvalue counting ── /// Global Kalman-based pose tracker for stable person IDs and smoothed keypoints. pose_tracker: PoseTracker, @@ -1675,6 +1827,9 @@ impl AppStateInner { training_progress_tx: broadcast::channel::(256).0, adaptive_model: None, node_states: HashMap::new(), + room_debounced_level: "absent".to_string(), + room_debounce_candidate: "absent".to_string(), + room_debounce_since: None, pose_tracker: PoseTracker::new(), last_tracker_instant: None, multistatic_fuser: MultistaticFuser::new(), @@ -6607,7 +6762,7 @@ async fn udp_receiver_task( // node's own reading no longer overwrites the room's. The // old ad-hoc "boost confidence by node count" is replaced by // `room_inference`'s freshness-weighted multi-node confidence. - let classification = classification_from_room(&room_inference); + let classification = debounce_room_classification(&mut s, &room_inference); let signal_field = generate_signal_field( fused_features.mean_rssi, @@ -7076,6 +7231,7 @@ async fn udp_receiver_task( active_nodes.iter().filter_map(|ni| ni.node_inference.as_ref()), NODE_STALE_AFTER_MS, ); + let room_classification = debounce_room_classification(&mut s, &room_inference); let mut update = SensingUpdate { msg_type: "sensing_update".to_string(), @@ -7089,7 +7245,7 @@ async fn udp_receiver_task( // `classification` (this node's own smoothed reading) // still drives `motion_score`/`total_persons` above, // which are legitimately this-packet-local. - classification: classification_from_room(&room_inference), + classification: room_classification, signal_field: generate_signal_field( fused_features.mean_rssi, motion_score, @@ -8686,6 +8842,9 @@ async fn main() { ); }), node_states: HashMap::new(), + room_debounced_level: "absent".to_string(), + room_debounce_candidate: "absent".to_string(), + room_debounce_since: None, // Accuracy sprint pose_tracker: PoseTracker::new(), last_tracker_instant: None, From f5744505160f2a9a0c8c1898ff5e8286edff46b5 Mon Sep 17 00:00:00 2001 From: Hsukqi Lee Date: Fri, 4 Sep 2026 20:15:38 +0800 Subject: [PATCH 12/27] fix(calibration): gate finalize on wall-clock duration, expose elapsed_s and frames/s Signed-off-by: Hsukqi Lee --- .../wifi-densepose-sensing-server/src/main.rs | 34 +++- .../benches/dsp_perf_bench.rs | 1 + .../src/ruvsense/field_model.rs | 168 +++++++++++++++++- 3 files changed, 195 insertions(+), 8 deletions(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 3c6ca0428..5792a6585 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -5822,13 +5822,31 @@ async fn calibration_stop(State(state): State) -> Json 0.0 && elapsed_s < need_s) { + let mut resp = serde_json::json!({ "success": false, - "error": "Not enough calibration frames yet — keep the room empty and poll /calibration/status until frame_count reaches the target.", + "error": "Calibration not complete yet — keep the room empty and poll /calibration/status until both frame_count and elapsed_s reach their targets.", "frame_count": have, "frames_needed": need, - })); + "elapsed_s": elapsed_s, + "duration_needed_s": need_s, + "frames_per_second": fm.calibration_frames_per_second(), + }); + // #1756: a fast fleet satisfies the frame gate in ~1/N of the + // intended wall-clock window. Make that explicit instead of + // silently collecting, so the operator sees why the session + // keeps running. + if have >= need && elapsed_s < need_s { + tracing::warn!( + "Calibration frame target reached in {elapsed_s:.1}s — substantially less than the intended {need_s:.0}s window; continuing to collect for slow environmental variation (#1756)" + ); + resp["warning"] = serde_json::json!(format!( + "frame target reached in {elapsed_s:.1}s — substantially less than the intended {need_s:.0}s window; continuing to collect for slow environmental variation" + )); + } + return Json(resp); } let ts = chrono::Utc::now().timestamp_micros() as u64; match fm.finalize_calibration(ts, 0) { @@ -5841,6 +5859,7 @@ async fn calibration_stop(State(state): State) -> Json) -> Json) -> Json { let s = state.read().await; match s.field_model.as_ref() { + // #1756: expose elapsed wall-clock time and the effective aggregate + // sample rate alongside the frame count, so a client can see the + // shape of the capture instead of frames alone. Some(fm) => Json(serde_json::json!({ "active": true, "status": format!("{:?}", fm.status()), "frame_count": fm.calibration_frame_count(), + "min_frames": fm.min_calibration_frames(), + "elapsed_s": fm.calibration_elapsed_s(), + "frames_per_second": fm.calibration_frames_per_second(), + "min_duration_s": fm.min_calibration_duration_s(), })), None => Json(serde_json::json!({ "active": false, diff --git a/v2/crates/wifi-densepose-signal/benches/dsp_perf_bench.rs b/v2/crates/wifi-densepose-signal/benches/dsp_perf_bench.rs index 9d65980b1..b0f2a0838 100644 --- a/v2/crates/wifi-densepose-signal/benches/dsp_perf_bench.rs +++ b/v2/crates/wifi-densepose-signal/benches/dsp_perf_bench.rs @@ -287,6 +287,7 @@ mod eig { n_links, n_modes: 3, min_calibration_frames: 20, + min_calibration_duration_s: 0.0, baseline_expiry_s: 86_400.0, }; let mut model = FieldModel::new(config).unwrap(); diff --git a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs index 210e6d0a0..9ea6a6ef1 100644 --- a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs +++ b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs @@ -7,7 +7,8 @@ //! (the residual). //! //! # Algorithm -//! 1. Collect CSI during empty-room calibration (>=10 min at 20 Hz) +//! 1. Collect CSI during empty-room calibration (>=10 min wall-clock; the +//! frame target is this duration at the assumed 20 Hz single-node rate) //! 2. Compute per-link baseline mean (Welford online accumulator) //! 3. Decompose covariance via SVD to extract environmental modes //! 4. At runtime: observation - baseline, project out top-K modes, keep residual @@ -23,6 +24,24 @@ use ndarray_linalg::Eigh; #[cfg(feature = "eigenvalue")] use ndarray_linalg::UPLO; +// --------------------------------------------------------------------------- +// Calibration window constants +// --------------------------------------------------------------------------- + +/// Intended wall-clock duration of the empty-room calibration window (s). +/// +/// The Welford statistics exist to absorb slow environmental variation — +/// HVAC cycles, thermal drift — so the calibration gate is expressed in +/// wall-clock time. The historical frame target of 12,000 is this duration at +/// the assumed single-node rate. Every accepted node packet advances the same +/// field model, so N nodes reach any frame target N times faster; frames alone +/// therefore cannot gate calibration (#1756). +pub const CALIBRATION_DURATION_S: f64 = 600.0; + +/// Assumed per-node CSI rate (Hz) used to relate the frame target to +/// [`CALIBRATION_DURATION_S`] ("10 min at 20 Hz"). +pub const ASSUMED_CALIBRATION_RATE_HZ: f64 = 20.0; + // --------------------------------------------------------------------------- // Error types // --------------------------------------------------------------------------- @@ -34,6 +53,12 @@ pub enum FieldModelError { #[error("Insufficient calibration frames: need {needed}, got {got}")] InsufficientCalibration { needed: usize, got: usize }, + /// Calibration window shorter than the intended wall-clock duration. + /// A frame count alone cannot gate calibration: N nodes streaming in + /// aggregate reach any frame target in 1/N of the intended window (#1756). + #[error("Calibration window too short: need {needed_s:.1}s of wall-clock time, got {got_s:.1}s")] + InsufficientCalibrationDuration { needed_s: f64, got_s: f64 }, + /// Dimensionality mismatch between observation and baseline. #[error("Dimension mismatch: baseline has {expected} subcarriers, observation has {got}")] DimensionMismatch { expected: usize, got: usize }, @@ -244,8 +269,18 @@ pub struct FieldModelConfig { pub n_subcarriers: usize, /// Number of environmental modes to retain (K). Max 5. pub n_modes: usize, - /// Minimum calibration frames before baseline is valid (10 min at 20 Hz = 12000). + /// Minimum calibration frames before baseline is valid. Derived from the + /// intended wall-clock window at the assumed single-node rate + /// (`CALIBRATION_DURATION_S * ASSUMED_CALIBRATION_RATE_HZ` = 12,000). + /// A frame count alone is not sufficient: N nodes streaming in aggregate + /// reach it in 1/N of the window, so `min_calibration_duration_s` also + /// gates finalization (#1756). pub min_calibration_frames: usize, + /// Minimum wall-clock duration of the calibration window in seconds. + /// Slow environmental variation (HVAC cycles, thermal drift) can only be + /// observed over time, regardless of how many frames a fast fleet + /// delivers (#1756). 0 disables the duration gate (tests only). + pub min_calibration_duration_s: f64, /// Baseline expiry in seconds (default 86400 = 24 hours). pub baseline_expiry_s: f64, } @@ -256,7 +291,9 @@ impl Default for FieldModelConfig { n_links: 6, n_subcarriers: 56, n_modes: 3, - min_calibration_frames: 12_000, + min_calibration_frames: (CALIBRATION_DURATION_S * ASSUMED_CALIBRATION_RATE_HZ) + as usize, + min_calibration_duration_s: CALIBRATION_DURATION_S, baseline_expiry_s: 86_400.0, } } @@ -345,6 +382,9 @@ pub struct FieldModel { covariance_sum: Option>, /// Number of frames accumulated into covariance_sum. covariance_count: u64, + /// Monotonic timestamp of the first accepted calibration frame of the + /// current session; `None` until collection starts (#1756). + calibration_started: Option, } /// Diagonal variance fallback for when full covariance SVD is unavailable. @@ -429,6 +469,7 @@ impl FieldModel { last_calibration_us: 0, covariance_sum: None, covariance_count: 0, + calibration_started: None, }) } @@ -454,6 +495,32 @@ impl FieldModel { self.config.min_calibration_frames } + /// Minimum wall-clock duration (s) of the calibration window required + /// before `finalize_calibration` will succeed (#1756). + pub fn min_calibration_duration_s(&self) -> f64 { + self.config.min_calibration_duration_s + } + + /// Wall-clock seconds elapsed since the first accepted calibration frame + /// of the current session, or 0.0 if collection has not started (#1756). + pub fn calibration_elapsed_s(&self) -> f64 { + self.calibration_started + .map(|t| t.elapsed().as_secs_f64()) + .unwrap_or(0.0) + } + + /// Effective aggregate frames per second since collection started, or 0.0 + /// if collection has not started (#1756). With N nodes streaming, this is + /// roughly N times the single-node rate. + pub fn calibration_frames_per_second(&self) -> f64 { + let elapsed = self.calibration_elapsed_s(); + if elapsed > 0.0 { + self.calibration_frame_count() as f64 / elapsed + } else { + 0.0 + } + } + /// Feed a calibration frame (one CSI observation per link during empty room). /// /// `observations` is `[n_links][n_subcarriers]` amplitude data. @@ -467,6 +534,11 @@ impl FieldModel { for (link_stat, obs) in self.link_stats.iter_mut().zip(observations.iter()) { link_stat.update(obs)?; } + // Start the wall-clock calibration window on the first accepted frame + // (#1756): finalize gates on elapsed duration as well as frame count. + if self.calibration_started.is_none() { + self.calibration_started = Some(std::time::Instant::now()); + } if self.status == CalibrationStatus::Uncalibrated { self.status = CalibrationStatus::Collecting; } @@ -501,7 +573,8 @@ impl FieldModel { /// Finalize calibration: compute SVD to extract environmental modes. /// - /// Requires at least `min_calibration_frames` observations. + /// Requires at least `min_calibration_frames` observations collected over + /// at least `min_calibration_duration_s` of wall-clock time (#1756). /// `timestamp_us` is the current timestamp in microseconds. /// `geometry_hash` identifies the mesh geometry at calibration time. pub fn finalize_calibration( @@ -516,6 +589,19 @@ impl FieldModel { got: count as usize, }); } + // #1756: the frame gate encodes "duration at the single-node rate", + // but every accepted node packet advances this model, so a fleet of N + // nodes satisfies it in ~1/N of the intended window. Gate on the + // wall-clock duration as well so the baseline covers the slow + // environmental variation the Welford statistics are meant to absorb. + let elapsed_s = self.calibration_elapsed_s(); + let need_s = self.config.min_calibration_duration_s; + if need_s > 0.0 && elapsed_s < need_s { + return Err(FieldModelError::InsufficientCalibrationDuration { + needed_s: need_s, + got_s: elapsed_s, + }); + } let n_sc = self.config.n_subcarriers; let n_modes = self.config.n_modes.min(n_sc); @@ -895,6 +981,7 @@ impl FieldModel { self.status = CalibrationStatus::Uncalibrated; self.covariance_sum = None; self.covariance_count = 0; + self.calibration_started = None; } } @@ -912,6 +999,8 @@ mod tests { n_subcarriers: n_sc, n_modes: 3, min_calibration_frames: min_frames, + // Tests feed frames instantly; disable the wall-clock gate (#1756). + min_calibration_duration_s: 0.0, baseline_expiry_s: 86_400.0, } } @@ -1114,6 +1203,7 @@ mod tests { n_subcarriers: 8, n_modes: 2, min_calibration_frames: 5, + min_calibration_duration_s: 0.0, baseline_expiry_s: 86_400.0, }; let mut model = FieldModel::new(config).unwrap(); @@ -1307,6 +1397,7 @@ mod tests { n_subcarriers: 8, n_modes: 3, min_calibration_frames: 20, + min_calibration_duration_s: 0.0, baseline_expiry_s: 86_400.0, }; let mut model = FieldModel::new(config).unwrap(); @@ -1369,6 +1460,7 @@ mod tests { n_subcarriers: 8, n_modes: 3, min_calibration_frames: 20, + min_calibration_duration_s: 0.0, baseline_expiry_s: 86_400.0, }; let mut model = FieldModel::new(config).unwrap(); @@ -1417,6 +1509,7 @@ mod tests { n_subcarriers: 8, n_modes: 3, min_calibration_frames: 20, + min_calibration_duration_s: 0.0, baseline_expiry_s: 86_400.0, }; let mut model = FieldModel::new(config).unwrap(); @@ -1473,4 +1566,71 @@ mod tests { "Environmental projection should be non-zero for drifting subcarrier" ); } + + // ----------------------------------------------------------------- + // Wall-clock calibration gate (#1756) + // ----------------------------------------------------------------- + + #[test] + fn test_default_frame_target_derives_from_duration() { + let cfg = FieldModelConfig::default(); + assert_eq!(cfg.min_calibration_duration_s, CALIBRATION_DURATION_S); + assert_eq!( + cfg.min_calibration_frames, + (CALIBRATION_DURATION_S * ASSUMED_CALIBRATION_RATE_HZ) as usize, + "frame target must be the intended window at the assumed rate" + ); + } + + #[test] + fn test_finalize_requires_wall_clock_window() { + // Enough frames, but fed instantly: the fleet-fast scenario from #1756. + let mut config = make_config(1, 4, 5); + config.min_calibration_duration_s = 600.0; + let mut model = FieldModel::new(config).unwrap(); + for _ in 0..10 { + model + .feed_calibration(&make_observations(1, 4, 1.0)) + .unwrap(); + } + assert!(model.calibration_frame_count() >= 5); + match model.finalize_calibration(1_000_000, 0) { + Err(FieldModelError::InsufficientCalibrationDuration { needed_s, got_s }) => { + assert!((needed_s - 600.0).abs() < 1e-9); + assert!(got_s < 1.0, "test feeds frames instantly, got {got_s}s"); + } + other => panic!("expected InsufficientCalibrationDuration, got {other:?}"), + } + } + + #[test] + fn test_duration_gate_disabled_with_zero() { + // min_calibration_duration_s = 0 keeps the legacy frame-only gate. + let mut model = FieldModel::new(make_config(1, 4, 5)).unwrap(); + for _ in 0..5 { + model + .feed_calibration(&make_observations(1, 4, 1.0)) + .unwrap(); + } + assert!(model.finalize_calibration(1_000_000, 0).is_ok()); + } + + #[test] + fn test_calibration_clock_accessors() { + let mut model = FieldModel::new(make_config(1, 4, 5)).unwrap(); + // No frames yet: the session clock has not started. + assert_eq!(model.calibration_elapsed_s(), 0.0); + assert_eq!(model.calibration_frames_per_second(), 0.0); + + model + .feed_calibration(&make_observations(1, 4, 1.0)) + .unwrap(); + assert!(model.calibration_elapsed_s() >= 0.0); + assert!(model.calibration_frames_per_second() >= 0.0); + + // Reset clears the session clock. + model.reset_calibration(); + assert_eq!(model.calibration_elapsed_s(), 0.0); + assert_eq!(model.calibration_frames_per_second(), 0.0); + } } From 7f9df373807c7f077b5eb96b0c19c376969922ab Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 4 Sep 2026 11:29:27 -0400 Subject: [PATCH 13/27] fix(firmware): damp every thermal step down, not just the one into OK Follow-up to the review on #1797. The CRITICAL -> THROTTLED edge was unconditional: any reading below critical_c dropped the state immediately, so a node sitting on the critical boundary flapped between TXP_MIN and TXP_STEP2 indefinitely. Every oscillation moves that node's RSSI at both ends, which a reader cannot distinguish from an obstruction -- exactly the failure the hysteresis on the other edges was added to prevent. Now every falling edge requires cooling a full hysteresis band below the threshold it crossed on the way up; rising edges stay immediate, because heat is a reason to act now. Verified by dithering across the boundary: six readings either side of 80 C previously produced five state changes, now zero. The decision moved into thermal.h as a pure function, following the convention edge_processing.h already uses for edge_evidence_person_count -- a host test then exercises the exact function the device build runs rather than a copy free to drift. BEHAVIOUR CHANGE worth calling out, beyond what the review asked for. The old `recover` point was a single global threshold (throttle_c - hyst) that every state fell back through, so leaving WARN required cooling to 64 C -- a bound derived from the throttle threshold, not WARN's own. Hysteresis is now relative to each state's own entry point, so WARN releases at 57 C instead. Leaving THROTTLED is unchanged at 64 C. Kconfig help updated, since it described the old throttle-relative semantics. One consequence the test pins explicitly: with the default constants the critical exit point (80 - 8 = 72) coincides exactly with the throttle threshold, so a node cool enough to leave CRITICAL is already below THROTTLED's entry and lands in WARN. THROTTLED is unreachable descending from CRITICAL at hysteresis 8. That falls out of the chosen constants rather than the logic, and the test covers hysteresis 4 as well to show the intermediate step reappears when the bands do not coincide. My first version of the test asserted THROTTLED there and was simply wrong -- running it caught that. Verified in espressif/idf:v5.4: against the unfixed transition the test fails five checks including the dither, passes with the fix, and the C6 firmware builds. Still no hardware evidence for this PR -- a real boot log is coming before merge, as promised earlier in the thread. Co-Authored-By: claude-flow --- .../esp32-csi-node/main/Kconfig.projbuild | 8 +- firmware/esp32-csi-node/main/thermal.c | 23 +-- firmware/esp32-csi-node/main/thermal.h | 60 +++++++ firmware/esp32-csi-node/test/Makefile | 21 ++- .../test/test_thermal_hysteresis.c | 147 ++++++++++++++++++ 5 files changed, 238 insertions(+), 21 deletions(-) create mode 100644 firmware/esp32-csi-node/test/test_thermal_hysteresis.c diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index 0d5daa7a5..a3aaa7e15 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -574,7 +574,7 @@ config THERMAL_CRITICAL_C default 80 config THERMAL_HYSTERESIS_C - int "Degrees below the throttle point before recovering" + int "Degrees of cooling required before stepping down a thermal state" depends on THERMAL_MONITOR range 1 20 default 8 @@ -582,4 +582,10 @@ config THERMAL_HYSTERESIS_C Recovering at the same threshold that tripped would oscillate, and every oscillation is a visible RSSI step on every link the node has. + Applies to EVERY downward transition, each relative to the threshold that + was crossed on the way up: leaving CRITICAL needs CRITICAL_C minus this, + leaving THROTTLED needs THROTTLE_C minus this, leaving WARN needs WARN_C + minus this. Rising transitions are immediate at the plain thresholds -- + heat is a reason to act now. + endmenu diff --git a/firmware/esp32-csi-node/main/thermal.c b/firmware/esp32-csi-node/main/thermal.c index 407d8c2e5..20e67ff99 100644 --- a/firmware/esp32-csi-node/main/thermal.c +++ b/firmware/esp32-csi-node/main/thermal.c @@ -110,22 +110,13 @@ void thermal_tick(void) /* Hysteresis on the way down: recovering at the same threshold that * tripped would oscillate, and each oscillation is a visible RSSI step on - * every link this node has. */ - const float recover = (float)CONFIG_THERMAL_THROTTLE_C - - (float)CONFIG_THERMAL_HYSTERESIS_C; - - thermal_state_t next = s_state; - if (c >= (float)CONFIG_THERMAL_CRITICAL_C) { - next = THERMAL_CRITICAL; - } else if (c >= (float)CONFIG_THERMAL_THROTTLE_C) { - next = THERMAL_THROTTLED; - } else if (c >= (float)CONFIG_THERMAL_WARN_C) { - if (s_state != THERMAL_THROTTLED && s_state != THERMAL_CRITICAL) { - next = THERMAL_WARN; - } - } else if (c < recover) { - next = THERMAL_OK; - } + * every link this node has. The decision itself lives in thermal.h as a + * pure function so a host test can exercise the same code this runs. */ + const thermal_state_t next = thermal_next_state(s_state, c, + CONFIG_THERMAL_WARN_C, + CONFIG_THERMAL_THROTTLE_C, + CONFIG_THERMAL_CRITICAL_C, + CONFIG_THERMAL_HYSTERESIS_C); if (next != s_state) { ESP_LOGW(TAG, "%.1f C: %s -> %s", diff --git a/firmware/esp32-csi-node/main/thermal.h b/firmware/esp32-csi-node/main/thermal.h index a6b0addb5..c4529f34d 100644 --- a/firmware/esp32-csi-node/main/thermal.h +++ b/firmware/esp32-csi-node/main/thermal.h @@ -12,6 +12,66 @@ typedef enum { THERMAL_CRITICAL, } thermal_state_t; +/** + * Decide the next thermal state from a reading and the current state. + * + * Pure: no globals, no hardware. Kept in the public header so a host test can + * exercise the exact function the device build runs, rather than a copy of it + * that is free to drift. + * + * Rising edges act immediately -- heat is a reason to react now. EVERY falling + * edge requires cooling a full `hyst_c` below the threshold that was crossed on + * the way up, because recovering at the same point that tripped oscillates, and + * each oscillation steps this node's transmit power, which moves the RSSI of + * every link it has. A reader cannot distinguish that from a wall moving. + * + * The CRITICAL -> THROTTLED edge is the reason this exists as a function. It + * used to be unconditional: any reading below `critical_c` dropped the state + * immediately, so a node sitting on the critical boundary flapped between + * minimum and step-2 transmit power indefinitely -- precisely the oscillation + * the hysteresis elsewhere was added to prevent. With defaults (critical 80, + * hysteresis 8) leaving CRITICAL now needs 72 C, which is exactly the THROTTLED + * entry point. + * + * @param cur Current state. + * @param c Die temperature, degrees C. + * @param warn_c Warn threshold. + * @param throttle_c Throttle threshold. + * @param critical_c Critical threshold. + * @param hyst_c Degrees of cooling required before any step down. + * @return The state to adopt. + */ +static inline thermal_state_t thermal_next_state(thermal_state_t cur, float c, + int warn_c, int throttle_c, + int critical_c, int hyst_c) +{ + const float h = (float)hyst_c; + + /* Rising: immediate, at the plain thresholds. */ + if (c >= (float)critical_c) return THERMAL_CRITICAL; + + /* Falling out of CRITICAL needs a full hysteresis band, like every other + * downward step. Without this the state tracks `c` across critical_c with + * no damping at all. */ + if (cur == THERMAL_CRITICAL && c >= (float)critical_c - h) { + return THERMAL_CRITICAL; + } + + if (c >= (float)throttle_c) return THERMAL_THROTTLED; + + if (cur == THERMAL_THROTTLED && c >= (float)throttle_c - h) { + return THERMAL_THROTTLED; + } + + if (c >= (float)warn_c) return THERMAL_WARN; + + if (cur == THERMAL_WARN && c >= (float)warn_c - h) { + return THERMAL_WARN; + } + + return THERMAL_OK; +} + #if defined(CONFIG_THERMAL_MONITOR) && defined(CONFIG_IDF_TARGET_ESP32C6) esp_err_t thermal_init(void); diff --git a/firmware/esp32-csi-node/test/Makefile b/firmware/esp32-csi-node/test/Makefile index b55899455..e2b46699f 100644 --- a/firmware/esp32-csi-node/test/Makefile +++ b/firmware/esp32-csi-node/test/Makefile @@ -44,9 +44,11 @@ FUZZ_DURATION ?= 30 FUZZ_JOBS ?= 1 .PHONY: all clean run_serialize run_edge run_nvs run_all test_adr110 run_adr110 \ - test_vitals run_vitals test_mmwave_detect run_mmwave_detect host_tests + test_vitals run_vitals test_mmwave_detect run_mmwave_detect host_tests \ + test_thermal run_thermal -all: fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect +all: fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \ + test_thermal # --- ADR-110 encoding unit tests --- # Host-side, no libFuzzer needed — plain C99 deterministic table tests @@ -80,8 +82,19 @@ test_mmwave_detect: test_mmwave_detect.c $(MAIN_DIR)/mmwave_detect.h run_mmwave_detect: test_mmwave_detect ./test_mmwave_detect -host_tests: run_adr110 run_vitals run_mmwave_detect - @echo "Host tests passed (ADR-110 + vitals #998/#996 + mmwave detect #1135)" +# --- Thermal state hysteresis --- +# Host-side. Exercises thermal_next_state() from ../main/thermal.h directly -- +# the same function the device build runs -- so the test and the firmware cannot +# drift. Pins every edge in both directions, including the CRITICAL -> THROTTLED +# flap that had no hysteresis at all. +test_thermal: test_thermal_hysteresis.c $(MAIN_DIR)/thermal.h + cc -std=c99 -Wall -Wextra -Istubs -I$(MAIN_DIR) -o $@ $< + +run_thermal: test_thermal + ./test_thermal + +host_tests: run_adr110 run_vitals run_mmwave_detect run_thermal + @echo "Host tests passed (ADR-110 + vitals #998/#996 + mmwave detect #1135 + thermal)" # --- Serialize fuzzer --- # Tests csi_serialize_frame() with random wifi_csi_info_t inputs. diff --git a/firmware/esp32-csi-node/test/test_thermal_hysteresis.c b/firmware/esp32-csi-node/test/test_thermal_hysteresis.c new file mode 100644 index 000000000..24630a22c --- /dev/null +++ b/firmware/esp32-csi-node/test/test_thermal_hysteresis.c @@ -0,0 +1,147 @@ +/** + * @file test_thermal_hysteresis.c + * @brief Pins every thermal transition, in both directions. + * + * Exercises `thermal_next_state()` from ../main/thermal.h directly -- the same + * function the device build runs, not a copy -- so the test and the firmware + * cannot drift apart. + * + * The regression that motivated this: the CRITICAL -> THROTTLED edge was + * unconditional. Any reading below `critical_c` dropped the state at once, so a + * node sitting on the critical boundary flapped between minimum and step-2 + * transmit power forever. Every oscillation moves the RSSI of every link that + * node has, at both ends, which a reader cannot tell apart from an obstruction + * -- exactly the failure the hysteresis on the other edges exists to prevent. + * + * Defaults under test: warn 65, throttle 72, critical 80, hysteresis 8. + */ + +#include + +#include "thermal.h" + +#define WARN_C 65 +#define THROTTLE_C 72 +#define CRITICAL_C 80 +#define HYST_C 8 + +static int g_failures = 0; + +static const char *name(thermal_state_t s) +{ + switch (s) { + case THERMAL_OK: return "OK"; + case THERMAL_WARN: return "WARN"; + case THERMAL_THROTTLED: return "THROTTLED"; + case THERMAL_CRITICAL: return "CRITICAL"; + } + return "?"; +} + +static thermal_state_t step(thermal_state_t cur, float c) +{ + return thermal_next_state(cur, c, WARN_C, THROTTLE_C, CRITICAL_C, HYST_C); +} + +static void expect(thermal_state_t cur, float c, thermal_state_t want, + const char *why) +{ + thermal_state_t got = step(cur, c); + if (got != want) { + printf(" FAIL: from %-9s at %5.1f C -> %s, expected %s\n %s\n", + name(cur), c, name(got), name(want), why); + g_failures++; + } +} + +int main(void) +{ + printf("thermal hysteresis (warn %d, throttle %d, critical %d, hyst %d)\n", + WARN_C, THROTTLE_C, CRITICAL_C, HYST_C); + + /* ---- Rising edges are immediate. Heat is a reason to act now. ---- */ + expect(THERMAL_OK, 64.9f, THERMAL_OK, "below warn"); + expect(THERMAL_OK, 65.0f, THERMAL_WARN, "warn trips at the threshold"); + expect(THERMAL_WARN, 72.0f, THERMAL_THROTTLED, "throttle trips at the threshold"); + expect(THERMAL_THROTTLED, 80.0f, THERMAL_CRITICAL, "critical trips at the threshold"); + /* A jump straight past intermediate bands must not be damped on the way up. */ + expect(THERMAL_OK, 95.0f, THERMAL_CRITICAL, "a large rise skips straight to critical"); + + /* ---- THE REGRESSION: falling out of CRITICAL. ---- + * + * Before the fix, 79.9 C dropped the state to THROTTLED immediately, so a + * node hovering at 80 alternated TXP_MIN and TXP_STEP2 indefinitely. */ + expect(THERMAL_CRITICAL, 79.9f, THERMAL_CRITICAL, + "0.1 C below critical must NOT step down -- this is the flap"); + expect(THERMAL_CRITICAL, 75.0f, THERMAL_CRITICAL, + "still inside the hysteresis band"); + expect(THERMAL_CRITICAL, 72.1f, THERMAL_CRITICAL, + "just inside the band (critical 80 - hyst 8 = 72)"); + /* Once it has cooled a full band it adopts the band it is ACTUALLY in, + * which under these defaults is WARN, not THROTTLED: the critical exit + * point (80 - 8 = 72) coincides exactly with the throttle threshold, so + * anything cool enough to leave CRITICAL is already below THROTTLED's + * entry. THROTTLED is therefore unreachable descending from CRITICAL at + * hysteresis 8 -- a consequence of the chosen constants, not of the logic. + * With a smaller hysteresis the intermediate step reappears, which the + * next assertion pins. */ + expect(THERMAL_CRITICAL, 71.9f, THERMAL_WARN, + "a full band below critical, and 71.9 is in the warn band"); + + /* Same descent with hysteresis 4: the exit point is 76, which lands inside + * THROTTLED's band, so the intermediate power step is used. */ + { + thermal_state_t got = thermal_next_state(THERMAL_CRITICAL, 75.9f, + WARN_C, THROTTLE_C, CRITICAL_C, 4); + if (got != THERMAL_THROTTLED) { + printf(" FAIL: from CRITICAL at 75.9 C with hyst 4 -> %s, " + "expected THROTTLED\n", name(got)); + g_failures++; + } + thermal_state_t held = thermal_next_state(THERMAL_CRITICAL, 76.1f, + WARN_C, THROTTLE_C, CRITICAL_C, 4); + if (held != THERMAL_CRITICAL) { + printf(" FAIL: from CRITICAL at 76.1 C with hyst 4 -> %s, " + "expected CRITICAL\n", name(held)); + g_failures++; + } + } + + /* Prove the flap is gone by walking across the boundary repeatedly: a node + * dithering either side of 80 must not change state on the down-swings. */ + { + thermal_state_t s = THERMAL_CRITICAL; + int changes = 0; + const float dither[] = {80.2f, 79.8f, 80.1f, 79.7f, 80.3f, 79.9f}; + for (unsigned i = 0; i < sizeof(dither) / sizeof(dither[0]); i++) { + thermal_state_t n = step(s, dither[i]); + if (n != s) changes++; + s = n; + } + if (changes != 0) { + printf(" FAIL: dithering around the critical boundary produced %d " + "state change(s); each one steps transmit power\n", changes); + g_failures++; + } + } + + /* ---- Falling out of THROTTLED. Behaviour preserved from before. ---- */ + expect(THERMAL_THROTTLED, 71.9f, THERMAL_THROTTLED, "just below the throttle point holds"); + expect(THERMAL_THROTTLED, 64.1f, THERMAL_THROTTLED, "still inside the band"); + expect(THERMAL_THROTTLED, 63.9f, THERMAL_OK, "throttle 72 - hyst 8 = 64 releases"); + + /* ---- Falling out of WARN. ---- */ + expect(THERMAL_WARN, 64.9f, THERMAL_WARN, "just below warn holds"); + expect(THERMAL_WARN, 57.1f, THERMAL_WARN, "still inside the band"); + expect(THERMAL_WARN, 56.9f, THERMAL_OK, "warn 65 - hyst 8 = 57 releases"); + + /* ---- OK is stable well below everything. ---- */ + expect(THERMAL_OK, 30.0f, THERMAL_OK, "a cold node stays OK"); + + if (g_failures == 0) { + printf(" PASS\n"); + return 0; + } + printf(" %d check(s) FAILED\n", g_failures); + return 1; +} From 944b7bea7794efd4cd708f8738da07e0c249612c Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 30 Aug 2026 20:56:25 -0400 Subject: [PATCH 14/27] fix(firmware): retry WiFi forever; add an uplink watchdog A node that lost WiFi could give up permanently. The disconnect handler counted retries in s_retry_num, which is only ever cleared on a successful IP_EVENT_STA_GOT_IP, so it accumulated across the node's entire uptime. After ten cumulative disconnects it set WIFI_FAIL_BIT and never called esp_wifi_connect() again. No backoff reset, no periodic retry, no reboot. Only a power cycle cleared it. MEASURED 2026-08-30: the AP serving the fleet took a firmware update and rebooted at ~03:38. All three wall-mounted nodes stopped delivering within three minutes of each other (nodes 0 and 1 at 03:37:03, to the second) and none recovered for 5.5-7.5 hours, until each was physically unplugged. CSI capture ran the whole time -- node 0's sync sequence advanced 16600 -> 46340 at a steady ~30/s while it was invisible to the server -- because capture is driver-level and needs no IP. The boards looked alive on a bench and dead to the aggregator. Retry is now unbounded with 500 ms -> 30 s exponential backoff, scheduled on an esp_timer one-shot so esp_wifi_connect() is never reached through a delay inside the event handler. Two things that were conflated are now separate: WIFI_BOOT_WAIT_ATTEMPTS only releases app_main from its startup wait so the rest of the node can boot; nothing stops the node retrying. The uplink watchdog covers the failures we have not diagnosed: no successful send for CONFIG_UPLINK_WATCHDOG_TIMEOUT_S restarts the node. The signal is a successful sendto(), which on UDP means the stack accepted the datagram, NOT that the server received it -- so stopping the aggregator cannot reboot the fleet. It arms on the first successful send rather than at boot, so a node powered up while the AP is down does not reboot-loop; the unbounded reconnect owns that case. The timeout is 1200 s, sized from a 112-minute soak rather than a guess: 30 observed silences across two nodes, median 225 s, one 510 s outlier that looked like two cycles running together. An earlier 120 s would have rebooted healthy nodes every few minutes, and even 600 s left only 90 s of margin over an event already seen once. Do not lower it below ~900 s while the ~225 s stalls are unexplained; a needless reboot re-runs ESP-NOW leader election and discards mesh time-sync state. Also guards thermal.c's apply_tx_power behind CONFIG_THERMAL_THROTTLE so a monitor-only build (the default, and what is on the wall) stops warning about an unused function. Kconfig gains CSI_GATE_MESH_ALIGNED here alongside the watchdog options; its implementation lands in a later commit and the option is inert until then. Co-Authored-By: claude-flow (cherry picked from commit bb6d954de68fd4d542e191fd5e82c753f67a9874) --- .../esp32-csi-node/main/Kconfig.projbuild | 62 +++++++++ firmware/esp32-csi-node/main/main.c | 121 ++++++++++++++++-- firmware/esp32-csi-node/main/stream_sender.c | 16 +++ firmware/esp32-csi-node/main/stream_sender.h | 12 ++ firmware/esp32-csi-node/main/thermal.c | 36 +++++- 5 files changed, 234 insertions(+), 13 deletions(-) diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index a3aaa7e15..9a51c4fd2 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -518,6 +518,7 @@ menu "Onboard LED (ADR-183)" (reaches yellow with less motion). endmenu + menu "Thermal monitoring" config THERMAL_MONITOR @@ -589,3 +590,64 @@ config THERMAL_HYSTERESIS_C heat is a reason to act now. endmenu + +menu "Network supervision" + +config UPLINK_WATCHDOG + bool "Restart the node if the CSI uplink stops for too long" + default y + help + Bounds how long a node can sit powered, capturing, and unable to reach + the network. + + MEASURED 2026-08-30: an AP-side event at 03:37 EDT disconnected all three + wall-mounted nodes within three minutes of each other, and every one of + them stayed dead for 5.5-7.5 hours until it was physically unplugged. The + specific cause of that outage (a reconnect counter that gave up + permanently) is fixed in main.c, but this exists so the NEXT unknown + wedge costs minutes instead of a night of data. + + The watchdog counts a successful sendto(), which on UDP means the network + stack accepted the datagram -- not that the aggregator received it. That + is deliberate: stopping the server does NOT reboot the fleet, because + sendto keeps succeeding into a socket whose peer is gone. It fires only + when the node's own network path is broken. + + It arms on the first successful send rather than at boot, so a node + powered on while the AP is down does not reboot-loop; the unbounded + reconnect handles that case instead. + +config UPLINK_WATCHDOG_TIMEOUT_S + int "Restart after this many seconds with no successful send" + depends on UPLINK_WATCHDOG + range 30 3600 + default 1200 + help + Must sit above the worst SELF-HEALING gap, or the watchdog reboots nodes + that were about to recover on their own -- which is worse than the fault + it guards against, because every reboot re-runs ESP-NOW leader election + and discards the mesh time-sync state. + + This was 120 s on the reasoning that the ENOMEM backoff caps at 2 s so a + healthy node is never quiet for long. A 58-minute soak on 2026-08-30 + falsified that: nodes 0 and 2 each went silent repeatedly with a median + gap of 210 s and a maximum of 225 s, and recovered every time without + intervention. 120 s would have rebooted them every few minutes forever. + (The cause of those 210 s gaps is still unknown -- it is not the ENOMEM + backoff, which cannot produce a gap that long.) + + Sized from the completed 112-minute soak, not the partial one: 30 + silences across two nodes, median 225 s, p90 225 s, and a single 510 s + outlier that looks like two cycles running together. A third such merge + would reach ~735 s, so 600 s was too tight -- it left only 90 s of margin + over an event already observed once. + + 1200 s is 2.35x the observed maximum while still bounding a true wedge to + twenty minutes rather than the 7.5 hours measured the same day. Revisit + it if the ~225 s flapping is ever explained and fixed; until then do not + lower it below ~900 s. + + If nodes do restart unexpectedly, check the reset reason in the sync + packet's health field before assuming the watchdog is responsible. + +endmenu diff --git a/firmware/esp32-csi-node/main/main.c b/firmware/esp32-csi-node/main/main.c index 5771dc0e2..660899708 100644 --- a/firmware/esp32-csi-node/main/main.c +++ b/firmware/esp32-csi-node/main/main.c @@ -60,7 +60,52 @@ nvs_config_t g_nvs_config; static EventGroupHandle_t s_wifi_event_group; static int s_retry_num = 0; -#define MAX_RETRY 10 + +/* WiFi reconnection. + * + * This used to be `#define MAX_RETRY 10` with the disconnect handler giving up + * permanently once s_retry_num reached it: it set WIFI_FAIL_BIT and never + * called esp_wifi_connect() again. s_retry_num is only cleared on a successful + * IP_EVENT_STA_GOT_IP, so it accumulated over the node's entire uptime -- ten + * disconnects spread across days left the node permanently off the network + * with nothing but a power cycle to clear it. + * + * MEASURED 2026-08-30: a single AP-side event at 03:37:03 EDT disconnected all + * three wall-mounted nodes within three minutes of each other. Every one of + * them latched off and stayed off for 5.5-7.5 hours, until it was physically + * unplugged. CSI capture kept running throughout (it is driver-level and needs + * no IP), so the boards looked alive on the bench and dead to the server. + * + * Retry is now unbounded with exponential backoff. Two things that were + * conflated are now separate: WIFI_BOOT_WAIT_ATTEMPTS only releases app_main + * from its startup wait so the rest of the node can boot; it does not stop the + * node trying to reconnect. Nothing stops the node trying to reconnect. */ +#define WIFI_BOOT_WAIT_ATTEMPTS 10 +#define WIFI_RETRY_BASE_MS 500 +#define WIFI_RETRY_MAX_MS 30000 + +static esp_timer_handle_t s_reconnect_timer; + +/* 500 ms doubling to a 30 s ceiling. Backoff matters because the failure this + * guards against is an AP that is down or rebooting: hammering + * esp_wifi_connect() at full rate for the minutes an AP takes to come back + * wastes power and floods the log without reconnecting any sooner. */ +static uint32_t wifi_retry_delay_ms(int attempt) +{ + int shift = attempt - 1; + if (shift < 0) shift = 0; + if (shift > 6) shift = 6; + uint32_t d = (uint32_t)WIFI_RETRY_BASE_MS << shift; + return d > WIFI_RETRY_MAX_MS ? WIFI_RETRY_MAX_MS : d; +} + +/* Runs on the esp_timer task, not the event loop task -- esp_wifi_connect() + * must not be reached through a delay inside the event handler itself. */ +static void wifi_reconnect_cb(void *arg) +{ + (void)arg; + esp_wifi_connect(); +} static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) @@ -70,13 +115,22 @@ static void event_handler(void *arg, esp_event_base_t event_base, } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { wifi_event_sta_disconnected_t *disc = (wifi_event_sta_disconnected_t *)event_data; ESP_LOGW(TAG, "WiFi disconnected, reason=%d rssi=%d", disc->reason, disc->rssi); - if (s_retry_num < MAX_RETRY) { - esp_wifi_connect(); - s_retry_num++; - ESP_LOGI(TAG, "Retrying WiFi connection (%d/%d)", s_retry_num, MAX_RETRY); - } else { + s_retry_num++; + /* Release the boot wait once, so a node that comes up while the AP is + * down still starts CSI capture and the mesh instead of blocking in + * app_main. Retrying continues regardless. */ + if (s_retry_num == WIFI_BOOT_WAIT_ATTEMPTS) { xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } + uint32_t delay_ms = wifi_retry_delay_ms(s_retry_num); + if (s_reconnect_timer != NULL) { + esp_timer_stop(s_reconnect_timer); /* may not be running; harmless */ + esp_timer_start_once(s_reconnect_timer, (uint64_t)delay_ms * 1000); + } else { + esp_wifi_connect(); /* timer unavailable: retry now */ + } + ESP_LOGI(TAG, "Reconnecting in %lu ms (attempt %d)", + (unsigned long)delay_ms, s_retry_num); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip)); @@ -85,6 +139,41 @@ static void event_handler(void *arg, esp_event_base_t event_base, } } +#ifdef CONFIG_UPLINK_WATCHDOG +/* Uplink supervision. + * + * The reconnect fix above addresses the one wedge whose mechanism we have + * proven. This catches the ones we have not: any state in which the node is + * powered, capturing, and unable to put a packet on the wire. It bounds such a + * state to CONFIG_UPLINK_WATCHDOG_TIMEOUT_S instead of the 7.5 hours measured + * on 2026-08-30. + * + * The signal is a successful sendto(), which on UDP means the stack accepted + * the datagram -- NOT that the server received it. That distinction is what + * makes this safe: stopping the aggregator does not reboot the fleet, because + * sendto keeps succeeding into a socket whose peer is gone. It fires only when + * the node's own network path is broken. + * + * It arms on the first successful send rather than at boot, so a node that has + * never reached the network does not reboot-loop while the AP is down; the + * unbounded reconnect above owns that case. */ +static void uplink_watchdog_tick(void) +{ + int64_t last = stream_sender_last_success_us(); + if (last == 0) { + return; /* never delivered -- not armed yet */ + } + int64_t idle_s = (esp_timer_get_time() - last) / 1000000; + if (idle_s < CONFIG_UPLINK_WATCHDOG_TIMEOUT_S) { + return; + } + ESP_LOGE(TAG, "uplink watchdog: no successful send for %lld s " + "(limit %d s) -- restarting", + (long long)idle_s, CONFIG_UPLINK_WATCHDOG_TIMEOUT_S); + esp_restart(); +} +#endif /* CONFIG_UPLINK_WATCHDOG */ + static void wifi_init_sta(void) { s_wifi_event_group = xEventGroupCreate(); @@ -136,6 +225,17 @@ static void wifi_init_sta(void) } #endif + const esp_timer_create_args_t reconnect_args = { + .callback = &wifi_reconnect_cb, + .name = "wifi_reconnect", + }; + esp_err_t rc_ret = esp_timer_create(&reconnect_args, &s_reconnect_timer); + if (rc_ret != ESP_OK) { + ESP_LOGW(TAG, "reconnect timer create failed: %s (falling back to " + "immediate retry)", esp_err_to_name(rc_ret)); + s_reconnect_timer = NULL; + } + ESP_ERROR_CHECK(esp_wifi_start()); ESP_LOGI(TAG, "WiFi STA initialized, connecting to SSID: %s", g_nvs_config.wifi_ssid); @@ -148,7 +248,9 @@ static void wifi_init_sta(void) if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "Connected to WiFi"); } else if (bits & WIFI_FAIL_BIT) { - ESP_LOGE(TAG, "Failed to connect to WiFi after %d retries", MAX_RETRY); + ESP_LOGW(TAG, "Not connected after %d attempts -- continuing boot; " + "reconnection keeps retrying in the background", + WIFI_BOOT_WAIT_ATTEMPTS); } } @@ -541,8 +643,11 @@ void app_main(void) (swarm_ret == ESP_OK) ? g_nvs_config.seed_url : "off", (adapt_ret == ESP_OK) ? "on" : "off"); - /* Main loop — keep alive */ + /* Main loop — keep alive, and supervise the uplink. */ while (1) { vTaskDelay(pdMS_TO_TICKS(10000)); +#ifdef CONFIG_UPLINK_WATCHDOG + uplink_watchdog_tick(); +#endif } } diff --git a/firmware/esp32-csi-node/main/stream_sender.c b/firmware/esp32-csi-node/main/stream_sender.c index ecfc6ec9f..b766aff52 100644 --- a/firmware/esp32-csi-node/main/stream_sender.c +++ b/firmware/esp32-csi-node/main/stream_sender.c @@ -37,6 +37,12 @@ static uint32_t s_enomem_suppressed = 0; * succeeds. */ static uint32_t s_enomem_streak = 0; +/* Timestamp of the last sendto() the stack accepted, on either path; 0 until + * the node has ever delivered anything. Read by the uplink watchdog in main.c + * -- see the rationale there for why "accepted by the stack" is deliberately + * not "received by the server". */ +static int64_t s_last_success_us = 0; + static int sender_init_internal(const char *ip, uint16_t port) { s_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); @@ -118,6 +124,7 @@ int stream_sender_send(const uint8_t *data, size_t len) /* A send got through — buffer pressure cleared; reset the backoff streak. */ s_enomem_streak = 0; + s_last_success_us = esp_timer_get_time(); return sent; } @@ -146,9 +153,18 @@ int stream_sender_send_priority(const uint8_t *data, size_t len) } return -1; } + /* Counts for the watchdog: the mesh sync packet rides this path at ~1 Hz, + * and its getting through proves the node's network path is intact even + * when the bulk CSI stream is being suppressed by the ENOMEM backoff. */ + s_last_success_us = esp_timer_get_time(); return sent; } +int64_t stream_sender_last_success_us(void) +{ + return s_last_success_us; +} + void stream_sender_deinit(void) { if (s_sock >= 0) { diff --git a/firmware/esp32-csi-node/main/stream_sender.h b/firmware/esp32-csi-node/main/stream_sender.h index 198bbee4e..8dedca431 100644 --- a/firmware/esp32-csi-node/main/stream_sender.h +++ b/firmware/esp32-csi-node/main/stream_sender.h @@ -50,6 +50,18 @@ int stream_sender_send(const uint8_t *data, size_t len); */ int stream_sender_send_priority(const uint8_t *data, size_t len); +/** + * esp_timer timestamp (microseconds) of the last sendto() the network stack + * accepted, on either the bulk or the priority path. Returns 0 if the node has + * not successfully sent anything since boot. + * + * This reports that the datagram was accepted for transmission, NOT that the + * aggregator received it -- UDP gives no such signal. Used by the uplink + * watchdog to detect a node that is powered and capturing but cannot reach the + * network. + */ +int64_t stream_sender_last_success_us(void); + /** * Close the UDP sender socket. */ diff --git a/firmware/esp32-csi-node/main/thermal.c b/firmware/esp32-csi-node/main/thermal.c index 20e67ff99..9759a7f15 100644 --- a/firmware/esp32-csi-node/main/thermal.c +++ b/firmware/esp32-csi-node/main/thermal.c @@ -55,6 +55,10 @@ static thermal_state_t s_state = THERMAL_OK; static int8_t s_txp = TXP_FULL; +#ifdef CONFIG_THERMAL_THROTTLE +/* Only referenced from the throttling block in thermal_tick(). Guarded so a + * monitor-only build (the default, and what is on the wall) does not warn + * about an unused function. */ static void apply_tx_power(int8_t q) { if (q == s_txp) { @@ -70,15 +74,37 @@ static void apply_tx_power(int8_t q) q, esp_err_to_name(r)); } } +#endif /* CONFIG_THERMAL_THROTTLE */ esp_err_t thermal_init(void) { - temperature_sensor_config_t cfg = - TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 110); - esp_err_t r = temperature_sensor_install(&cfg, &s_sensor); + /* The requested span must fit ENTIRELY inside one of the sensor's + * predefined measurement ranges -- the driver walks a table and takes the + * first that contains it, so a span crossing two boundaries is rejected + * with ESP_ERR_INVALID_ARG rather than clamped. (-10,110) looks reasonable + * and is invalid for exactly that reason; it failed on real hardware. + * + * (20,100) is the right first choice here: the warn/throttle/critical + * thresholds all sit comfortably inside it, and indoor ambient is + * essentially never below 20 C. The wider fallbacks exist so an unheated + * room, or a different chip revision with a different table, degrades to a + * usable range instead of leaving the node unmonitored. */ + static const struct { int lo, hi; } spans[] = { + {20, 100}, {-10, 80}, {-30, 50}, {50, 125}, + }; + esp_err_t r = ESP_ERR_INVALID_ARG; + for (size_t i = 0; i < sizeof(spans) / sizeof(spans[0]); i++) { + temperature_sensor_config_t cfg = + TEMPERATURE_SENSOR_CONFIG_DEFAULT(spans[i].lo, spans[i].hi); + r = temperature_sensor_install(&cfg, &s_sensor); + if (r == ESP_OK) { + ESP_LOGI(TAG, "sensor range %d..%d C", spans[i].lo, spans[i].hi); + break; + } + } if (r != ESP_OK) { - ESP_LOGW(TAG, "sensor install failed: %s (continuing unmonitored)", - esp_err_to_name(r)); + ESP_LOGW(TAG, "sensor install failed on every range: %s " + "(continuing unmonitored)", esp_err_to_name(r)); return r; } r = temperature_sensor_enable(s_sensor); From 9b595b8333dc35174ebc8e9bf419a48098047848 Mon Sep 17 00:00:00 2001 From: ruv Date: Fri, 4 Sep 2026 11:48:11 -0400 Subject: [PATCH 15/27] fix(sensing-server): key --node-positions by active-node rank, not node_id The prior fix stored the parsed --node-positions list keyed by list index under the assumption that node_id == index, then looked it up by the node's real node_id. Real fleets use non-sequential logical node IDs (11, 12, 13, ...), so that lookup silently misses for every realistic deployment and falls through to the same [2.0, 0.0, 1.5] placeholder the original bug report was about. MultistaticFuser::fuse assigns node_positions[i] to the i-th frame in its node_frames argument, which callers build sorted ascending by node_id. Match that same convention in the NodeInfo JSON builder: rank currently- active node_ids ascending and index into the configured position list by rank, so live position output agrees with what fusion actually used. Adds node_positions_by_active_id() (shared by both call sites) plus regression tests for non-sequential IDs, stale-node exclusion from rank assignment, and the missing-config fallback. --- .../wifi-densepose-sensing-server/src/main.rs | 125 ++++++++++++++++-- 1 file changed, 111 insertions(+), 14 deletions(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 286cad1dc..7dcbd8e6a 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -1417,9 +1417,13 @@ struct AppStateInner { last_tracker_instant: Option, /// Attention-weighted multi-node CSI fusion engine. multistatic_fuser: MultistaticFuser, - /// Node positions parsed from --node-positions, keyed by 0-based index - /// into the parsed list (which corresponds to node_id). - node_positions_config: HashMap, + /// Node positions parsed from --node-positions, in the same order given + /// to `multistatic_fuser.set_node_positions`. Real fleets use + /// non-sequential logical node IDs, so this is NOT keyed by node_id — + /// entry `i` applies to the i-th smallest currently-active node_id, the + /// same convention `MultistaticFuser::fuse` uses. See + /// `node_positions_by_active_id`. + node_positions_config: Vec<[f32; 3]>, /// Governed trust-path bridge (ADR-135..146): runs the same live frames /// through the privacy/provenance/witness control plane. Does not alter /// person-count behavior; its trust state (witness, effective class, @@ -1681,7 +1685,7 @@ impl AppStateInner { pose_tracker: PoseTracker::new(), last_tracker_instant: None, multistatic_fuser: MultistaticFuser::new(), - node_positions_config: HashMap::new(), + node_positions_config: Vec::new(), engine_bridge: engine_bridge::EngineBridge::new( wifi_densepose_bfld::PrivacyMode::PrivateHome, 1, @@ -6271,6 +6275,99 @@ async fn info_page() -> Html { ) } +/// Resolve `--node-positions` entries to the currently active node IDs. +/// +/// `MultistaticFuser::fuse` assigns `node_positions[i]` to the i-th node in +/// its `node_frames` argument, which callers build sorted ascending by +/// node_id (see `node_frames_from_states_with_guard`). Real fleets use +/// non-sequential logical node IDs (e.g. 11, 12, 13), so a naive +/// `node_id -> node_positions_config[node_id]` lookup silently misses for +/// every real deployment. This mirrors the same ascending-rank convention so +/// the live `NodeInfo.position` field agrees with what fusion actually used. +fn node_positions_by_active_id( + node_positions_config: &[[f32; 3]], + node_states: &HashMap, + now: std::time::Instant, +) -> HashMap { + let mut active_ids: Vec = node_states + .iter() + .filter(|(_, n)| { + n.last_frame_time + .is_some_and(|t| now.duration_since(t).as_secs() < 10) + }) + .map(|(&id, _)| id) + .collect(); + active_ids.sort_unstable(); + active_ids + .into_iter() + .enumerate() + .filter_map(|(rank, id)| { + node_positions_config + .get(rank) + .map(|p| (id, [p[0] as f64, p[1] as f64, p[2] as f64])) + }) + .collect() +} + +#[cfg(test)] +mod node_positions_by_active_id_tests { + use super::*; + + fn active_node(now: std::time::Instant) -> NodeState { + let mut ns = NodeState::new(); + ns.last_frame_time = Some(now); + ns + } + + #[test] + fn non_sequential_node_ids_get_positions_by_ascending_rank() { + // Real fleets use logical IDs like 11, 12, 13 — not 0, 1, 2. The + // configured position list must map by ascending node_id rank, not + // by treating node_id as a direct index into the list. + let now = std::time::Instant::now(); + let mut node_states = HashMap::new(); + node_states.insert(13, active_node(now)); + node_states.insert(11, active_node(now)); + node_states.insert(12, active_node(now)); + + let configured = [[1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]]; + let resolved = node_positions_by_active_id(&configured, &node_states, now); + + assert_eq!(resolved.get(&11), Some(&[1.0, 0.0, 0.0])); + assert_eq!(resolved.get(&12), Some(&[2.0, 0.0, 0.0])); + assert_eq!(resolved.get(&13), Some(&[3.0, 0.0, 0.0])); + } + + #[test] + fn stale_nodes_are_excluded_from_rank_assignment() { + let now = std::time::Instant::now(); + let mut node_states = HashMap::new(); + node_states.insert(11, active_node(now)); + let mut stale = NodeState::new(); + stale.last_frame_time = + Some(now - std::time::Duration::from_secs(30)); + node_states.insert(12, stale); + node_states.insert(13, active_node(now)); + + let configured = [[1.0, 0.0, 0.0], [3.0, 0.0, 0.0]]; + let resolved = node_positions_by_active_id(&configured, &node_states, now); + + assert_eq!(resolved.get(&11), Some(&[1.0, 0.0, 0.0])); + assert_eq!(resolved.get(&12), None, "stale node must not consume a rank"); + assert_eq!(resolved.get(&13), Some(&[3.0, 0.0, 0.0])); + } + + #[test] + fn missing_config_entries_fall_through_to_the_caller_default() { + let now = std::time::Instant::now(); + let mut node_states = HashMap::new(); + node_states.insert(11, active_node(now)); + + let resolved = node_positions_by_active_id(&[], &node_states, now); + assert_eq!(resolved.get(&11), None); + } +} + // ── UDP receiver task ──────────────────────────────────────────────────────── async fn udp_receiver_task( @@ -6520,6 +6617,8 @@ async fn udp_receiver_task( } // Build nodes array with all active nodes. + let resolved_positions = + node_positions_by_active_id(&s.node_positions_config, &s.node_states, now); let active_nodes: Vec = s .node_states .iter() @@ -6530,10 +6629,9 @@ async fn udp_receiver_task( .map(|(&id, n)| NodeInfo { node_id: id, rssi_dbm: n.rssi_history.back().copied().unwrap_or(0.0), - position: s - .node_positions_config + position: resolved_positions .get(&id) - .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64]) + .copied() .unwrap_or([2.0, 0.0, 1.5]), amplitude: vec![], subcarrier_count: 0, @@ -7008,6 +7106,8 @@ async fn udp_receiver_task( // privacy gate applies at Restricted (drop amplitude/phase // proxies). let suppress_raw = s.engine_bridge.suppress_raw_outputs(); + let resolved_positions = + node_positions_by_active_id(&s.node_positions_config, &s.node_states, now); let active_nodes: Vec = s .node_states .iter() @@ -7018,10 +7118,9 @@ async fn udp_receiver_task( .map(|(&id, n)| NodeInfo { node_id: id, rssi_dbm: n.rssi_history.back().copied().unwrap_or(0.0), - position: s - .node_positions_config + position: resolved_positions .get(&id) - .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64]) + .copied() .unwrap_or([2.0, 0.0, 1.5]), amplitude: if suppress_raw { vec![] @@ -8593,7 +8692,7 @@ async fn main() { // threaded into `engine_bridge` so both fusion paths honor the same // WDP_TDM_SLOTS/WDP_GUARD_INTERVAL_US-derived guard (#1049/#1057). let mut engine_bridge_multistatic_cfg: Option = None; - let mut node_positions_config: HashMap = HashMap::new(); + let mut node_positions_config: Vec<[f32; 3]> = Vec::new(); let state: SharedState = Arc::new(RwLock::new(AppStateInner { latest_update: None, rssi_history: VecDeque::new(), @@ -8684,9 +8783,7 @@ async fn main() { "Configured {} node positions for multistatic fusion", positions.len() ); - for (idx, p) in positions.iter().enumerate() { - node_positions_config.insert(idx as u8, *p); - } + node_positions_config = positions.clone(); fuser.set_node_positions(positions); } } From 2e9e87c65c23ef901a45e1b03480ae3f8dc6a9c9 Mon Sep 17 00:00:00 2001 From: rUv Date: Fri, 4 Sep 2026 16:31:38 -0400 Subject: [PATCH 16/27] feat(calibration): persist safe empty room bootstrap prior (#1826) --- ...DR-355-local-empty-room-bootstrap-prior.md | 82 +++ docs/adr/README.md | 1 + .../wifi-densepose-sensing-server/README.md | 19 + .../src/bearer_auth.rs | 5 + .../src/bootstrap_baseline.rs | 388 +++++++++++++ .../wifi-densepose-sensing-server/src/lib.rs | 1 + .../wifi-densepose-sensing-server/src/main.rs | 540 ++++++++++++++++-- .../src/ruvsense/field_model.rs | 167 +++++- 8 files changed, 1157 insertions(+), 46 deletions(-) create mode 100644 docs/adr/ADR-355-local-empty-room-bootstrap-prior.md create mode 100644 v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs diff --git a/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md b/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md new file mode 100644 index 000000000..c14f78271 --- /dev/null +++ b/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md @@ -0,0 +1,82 @@ +# ADR 355: Local empty room bootstrap prior + +Status: Accepted + +Date: 2026-09-04 + +## Context + +Most installations are occupied when RuView first starts. An uncalibrated +signal heuristic can interpret HVAC, clock error, neighboring motion, or a +short covariance window as a person. It can also surface plausible numeric +vitals before a room model establishes occupant attribution. + +One empty home is not representative training data for all homes and contains +no positive human examples. It is useful only as a recent negative prior for +the same installation. It cannot define a universal human signal model. + +The field model was memory only. The server also used a relative `data/` +directory, which is unreliable for embedded application hosts. + +## Decision + +RuView may persist a completed empty room field model as a local bootstrap +prior only after two server controlled stages: + +1. Normal calibration reaches both 12,000 accepted frames and 600 seconds. +2. Promotion observes 12 new one second samples. At least 10 must resolve + empty, all ticks must be fresh, and none may publish numeric vital signs. + +The stored image contains aggregate means, environmental modes, mode energies, +noise statistics, configuration, contributing node IDs, and a server generated +model ID. It excludes raw CSI, waveforms, device addresses, room names, pose, +identity, credentials, and positive human labels. + +The image is bound to a SHA 256 digest of the stable installation ID, carries +an integrity digest, is capped at 1 MiB, and inherits the field model expiry. +Files with an invalid schema, dimensions, values, node set, installation +binding, digest, size, or lifetime fail closed. + +Restoring an image creates a field model in `bootstrap_only` mode. It may +reduce an uncalibrated false person count, but it cannot authorize calibrated +evidence or numeric heart and breathing rates. Explicit calibration replaces +it with fresh statistics. Cancelling an unfinished capture restores a valid +prior. Administrator scoped reset removes it. + +The server accepts `--data-dir` or `RUVIEW_DATA_DIR`, allowing an application +host to provide a protected state directory. + +## Consequences + +Startup can use recent measured background structure instead of only generic +heuristics. No accuracy lift is claimed until occupied and empty held out data +from multiple installations is recorded. + +The largest failure mode is overfitting to a temporarily quiet home and +suppressing a weak occupant. Controls are low authority, 24 hour expiry, +installation binding, vital abstention, explicit recalibration, and reset. + +A human signal model still requires positive, consented, leakage separated +occupied sequences. Empty data is never relabeled as human training. + +## Implementation + +1. `wifi-densepose-signal::ruvsense::field_model` exports and validates + `FieldModelSnapshotV1`. +2. `wifi-densepose-sensing-server::bootstrap_baseline` owns schema, integrity, + storage, lifetime, and the held out gate. +3. `POST /api/v1/calibration/bootstrap/promote` measures the validation window + and stores the image only on pass. +4. `GET /api/v1/calibration/status` reports bootstrap authority and explicitly + denies calibrated evidence and numeric vital authority. +5. Numeric vitals fail closed unless an explicit fresh calibration resolves + exactly one occupant at sufficient signal quality and confidence. + +## Acceptance test + +With a physically empty room, complete calibration on stable nodes, promote +the model, and restart the server with the same installation ID and data +directory. Status must report `binding_mode=bootstrap_only`, zero collection +frames, and false authority for calibrated evidence and numeric vitals. Twelve +fresh empty samples must contain no numeric vital signs. Starting a new room +calibration must replace the prior and begin at frame zero. diff --git a/docs/adr/README.md b/docs/adr/README.md index 83a12eba2..84b858b22 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -96,6 +96,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme | [ADR-349](ADR-349-governed-local-and-fal-forecast-training.md) | Governed local and fal.ai forecast training | Proposed | | [ADR-350](ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md) | RuVector predictive memory and RuVLLM authority boundary | Proposed | | [ADR-354](ADR-354-forecast-governance-enforcement-in-ruforecast-submodule.md) | fal.ai forecast governance and spend enforcement now real, in the vendored RuForecast submodule | Accepted | +| [ADR-355](ADR-355-local-empty-room-bootstrap-prior.md) | Local empty room bootstrap prior | Accepted | ### Platform and UI diff --git a/v2/crates/wifi-densepose-sensing-server/README.md b/v2/crates/wifi-densepose-sensing-server/README.md index dd8ae39c2..2224a6ddf 100644 --- a/v2/crates/wifi-densepose-sensing-server/README.md +++ b/v2/crates/wifi-densepose-sensing-server/README.md @@ -43,6 +43,8 @@ per ADR-022 Phase 3. - **WebSocket broadcast** -- Real-time sensing updates pushed to all connected clients at `ws://localhost:8765/ws/sensing`. - **Static file serving** -- Hosts the sensing UI on port 8080 with CORS headers. +- **Private startup baseline** -- Restores a recent, installation-bound empty-room field model + without storing raw CSI or authorizing numeric vital signs (ADR-355). ## Modules @@ -74,6 +76,23 @@ cargo run -p wifi-densepose-sensing-server -- \ --static-dir ./ui ``` +### Empty-room startup baseline + +Provide a stable installation ID and an application-owned private state directory: + +```bash +cargo run -p wifi-densepose-sensing-server -- \ + --installation-id installation-01 \ + --data-dir /path/to/private/application-state +``` + +Complete the normal empty-room calibration, then call +`POST /api/v1/calibration/bootstrap/promote`. The server measures a separate 12-sample holdout +before storing an aggregate field-model snapshot. On a later start with the same installation ID, +the snapshot is restored with `bootstrap_only` authority. It can reduce startup background false +positives, but cannot authorize calibrated evidence or numeric heart and breathing rates. Use +`POST /api/v1/calibration/reset` with administrator scope to remove it. + ### Using as a library ```rust diff --git a/v2/crates/wifi-densepose-sensing-server/src/bearer_auth.rs b/v2/crates/wifi-densepose-sensing-server/src/bearer_auth.rs index 783ed7c17..d8a9cbfa5 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/bearer_auth.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/bearer_auth.rs @@ -491,6 +491,10 @@ const READ_SAFE_MUTATIONS: &[&str] = &[ // Capture and calibration: create data, never destroy it. "/api/v1/calibration/start", "/api/v1/calibration/stop", + // Promotion consumes only server measured held out evidence and stores a + // bounded privacy reduced prior. Cancel discards only an unfinished run. + "/api/v1/calibration/bootstrap/promote", + "/api/v1/calibration/cancel", "/api/v1/pose/calibrate", "/api/v1/recording/start", "/api/v1/recording/stop", @@ -1970,6 +1974,7 @@ mod scope_gate_polarity_tests { (Method::DELETE, "/api/v1/models/m1"), (Method::DELETE, "/api/v1/recording/r1"), (Method::POST, "/api/v1/config/ground-truth"), + (Method::POST, "/api/v1/calibration/reset"), ] { assert_eq!( required_scope_for(&m, p), diff --git a/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs b/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs new file mode 100644 index 000000000..30fb6a11f --- /dev/null +++ b/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs @@ -0,0 +1,388 @@ +//! Local, privacy reduced empty room bootstrap model persistence. +//! +//! A bootstrap image is deliberately lower authority than a completed room +//! calibration. It can suppress background perturbations while the server +//! starts, but restoring it never authorizes calibrated evidence or vitals. + +use rand::{rngs::OsRng, RngCore}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::fs::{self, OpenOptions}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use thiserror::Error; +use wifi_densepose_signal::ruvsense::field_model::{ + FieldModel, FieldModelError, FieldModelSnapshotV1, +}; + +pub const BOOTSTRAP_BASELINE_SCHEMA: &str = "ruview.bootstrap-empty-field-model.v1"; +pub const BOOTSTRAP_BASELINE_AUTHORITY: &str = "bootstrap_only"; +pub const BOOTSTRAP_VALIDATION_SAMPLES: usize = 12; +pub const BOOTSTRAP_VALIDATION_MIN_EMPTY: usize = 10; +const BOOTSTRAP_MAX_FILE_BYTES: u64 = 1_048_576; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct BootstrapPayloadV1 { + schema: String, + authority: String, + installation_binding_sha256: String, + source_node_ids: Vec, + source_model_id: String, + created_at_unix_ms: u64, + expires_at_unix_ms: u64, + field_model: FieldModelSnapshotV1, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct BootstrapImageV1 { + payload: BootstrapPayloadV1, + content_sha256: String, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct BootstrapBaselineMetadata { + pub authority: &'static str, + pub source_node_ids: Vec, + pub source_model_id: String, + pub created_at_unix_ms: u64, + pub expires_at_unix_ms: u64, + pub content_sha256: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BootstrapValidationSample { + pub fresh_tick: bool, + pub calibrated_empty: bool, + pub vital_signs_absent: bool, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +pub struct BootstrapValidationResult { + pub sample_count: usize, + pub empty_sample_count: usize, + pub stale_sample_count: usize, + pub vital_sign_sample_count: usize, + pub passed: bool, +} + +#[derive(Debug, Error)] +pub enum BootstrapBaselineError { + #[error("a stable installation_id is required for a local bootstrap baseline")] + MissingInstallationId, + #[error("bootstrap baseline path is invalid")] + InvalidPath, + #[error("bootstrap baseline file is too large")] + FileTooLarge, + #[error("bootstrap baseline image is malformed: {0}")] + Malformed(String), + #[error("bootstrap baseline belongs to another installation")] + InstallationMismatch, + #[error("bootstrap baseline has expired")] + Expired, + #[error("bootstrap baseline digest does not verify")] + DigestMismatch, + #[error("field model snapshot is invalid: {0}")] + FieldModel(#[from] FieldModelError), + #[error("bootstrap baseline storage failed: {0}")] + Io(#[from] std::io::Error), + #[error("bootstrap baseline serialization failed: {0}")] + Json(#[from] serde_json::Error), +} + +pub fn path_in(data_dir: &Path) -> PathBuf { + data_dir.join("calibration").join("bootstrap-empty-v1.json") +} + +pub fn installation_binding(installation_id: &str) -> Result { + let installation_id = installation_id.trim(); + if installation_id.is_empty() || installation_id.len() > 128 { + return Err(BootstrapBaselineError::MissingInstallationId); + } + Ok(hex_sha256(installation_id.as_bytes())) +} + +pub fn evaluate_validation(samples: &[BootstrapValidationSample]) -> BootstrapValidationResult { + let empty_sample_count = samples + .iter() + .filter(|sample| sample.fresh_tick && sample.calibrated_empty) + .count(); + let stale_sample_count = samples.iter().filter(|sample| !sample.fresh_tick).count(); + let vital_sign_sample_count = samples + .iter() + .filter(|sample| !sample.vital_signs_absent) + .count(); + BootstrapValidationResult { + sample_count: samples.len(), + empty_sample_count, + stale_sample_count, + vital_sign_sample_count, + passed: samples.len() == BOOTSTRAP_VALIDATION_SAMPLES + && empty_sample_count >= BOOTSTRAP_VALIDATION_MIN_EMPTY + && stale_sample_count == 0 + && vital_sign_sample_count == 0, + } +} + +pub fn store( + path: &Path, + installation_id: &str, + source_node_ids: &[u8], + source_model_id: &str, + created_at_unix_ms: u64, + field_model: &FieldModel, +) -> Result { + let snapshot = field_model.export_snapshot()?; + let calibrated_at_ms = snapshot.modes.calibrated_at_us / 1_000; + let expiry_ms = (snapshot.config.baseline_expiry_s * 1_000.0) as u64; + let payload = BootstrapPayloadV1 { + schema: BOOTSTRAP_BASELINE_SCHEMA.to_string(), + authority: BOOTSTRAP_BASELINE_AUTHORITY.to_string(), + installation_binding_sha256: installation_binding(installation_id)?, + source_node_ids: source_node_ids.to_vec(), + source_model_id: source_model_id.to_string(), + created_at_unix_ms, + expires_at_unix_ms: calibrated_at_ms.saturating_add(expiry_ms), + field_model: snapshot, + }; + validate_payload(&payload, installation_id, created_at_unix_ms)?; + let content_sha256 = payload_digest(&payload)?; + let image = BootstrapImageV1 { + payload, + content_sha256, + }; + let bytes = serde_json::to_vec(&image)?; + if bytes.len() as u64 > BOOTSTRAP_MAX_FILE_BYTES { + return Err(BootstrapBaselineError::FileTooLarge); + } + atomic_write_private(path, &bytes)?; + Ok(metadata(&image)) +} + +pub fn load( + path: &Path, + installation_id: &str, + current_unix_ms: u64, +) -> Result<(FieldModel, BootstrapBaselineMetadata), BootstrapBaselineError> { + let file_metadata = fs::symlink_metadata(path)?; + if file_metadata.file_type().is_symlink() || !file_metadata.is_file() { + return Err(BootstrapBaselineError::InvalidPath); + } + if file_metadata.len() > BOOTSTRAP_MAX_FILE_BYTES { + return Err(BootstrapBaselineError::FileTooLarge); + } + let mut bytes = Vec::with_capacity(file_metadata.len() as usize); + OpenOptions::new() + .read(true) + .open(path)? + .take(BOOTSTRAP_MAX_FILE_BYTES + 1) + .read_to_end(&mut bytes)?; + if bytes.len() as u64 > BOOTSTRAP_MAX_FILE_BYTES { + return Err(BootstrapBaselineError::FileTooLarge); + } + let image: BootstrapImageV1 = serde_json::from_slice(&bytes)?; + validate_payload(&image.payload, installation_id, current_unix_ms)?; + if payload_digest(&image.payload)? != image.content_sha256 { + return Err(BootstrapBaselineError::DigestMismatch); + } + let model = FieldModel::from_snapshot( + image.payload.field_model.clone(), + current_unix_ms.saturating_mul(1_000), + )?; + Ok((model, metadata(&image))) +} + +pub fn remove(path: &Path) -> Result { + match fs::symlink_metadata(path) { + Ok(metadata) => { + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(BootstrapBaselineError::InvalidPath); + } + fs::remove_file(path)?; + Ok(true) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(error) => Err(error.into()), + } +} + +fn validate_payload( + payload: &BootstrapPayloadV1, + installation_id: &str, + current_unix_ms: u64, +) -> Result<(), BootstrapBaselineError> { + if payload.schema != BOOTSTRAP_BASELINE_SCHEMA + || payload.authority != BOOTSTRAP_BASELINE_AUTHORITY + || payload.source_model_id.is_empty() + || payload.source_model_id.len() > 128 + || payload.source_node_ids.is_empty() + || payload.source_node_ids.len() > 16 + || payload + .source_node_ids + .windows(2) + .any(|pair| pair[0] >= pair[1]) + || payload.created_at_unix_ms == 0 + || payload.expires_at_unix_ms <= payload.created_at_unix_ms + { + return Err(BootstrapBaselineError::Malformed( + "invalid schema, authority, identity, node set, or lifetime".into(), + )); + } + if payload.installation_binding_sha256 != installation_binding(installation_id)? { + return Err(BootstrapBaselineError::InstallationMismatch); + } + if current_unix_ms > payload.expires_at_unix_ms { + return Err(BootstrapBaselineError::Expired); + } + Ok(()) +} + +fn payload_digest(payload: &BootstrapPayloadV1) -> Result { + Ok(hex_sha256(&serde_json::to_vec(payload)?)) +} + +fn metadata(image: &BootstrapImageV1) -> BootstrapBaselineMetadata { + BootstrapBaselineMetadata { + authority: BOOTSTRAP_BASELINE_AUTHORITY, + source_node_ids: image.payload.source_node_ids.clone(), + source_model_id: image.payload.source_model_id.clone(), + created_at_unix_ms: image.payload.created_at_unix_ms, + expires_at_unix_ms: image.payload.expires_at_unix_ms, + content_sha256: image.content_sha256.clone(), + } +} + +fn hex_sha256(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + digest.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn atomic_write_private(path: &Path, bytes: &[u8]) -> Result<(), BootstrapBaselineError> { + let parent = path.parent().ok_or(BootstrapBaselineError::InvalidPath)?; + fs::create_dir_all(parent)?; + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .ok_or(BootstrapBaselineError::InvalidPath)?; + let mut nonce = [0_u8; 8]; + OsRng.fill_bytes(&mut nonce); + let temporary = parent.join(format!( + ".{file_name}.{:016x}.tmp", + u64::from_le_bytes(nonce) + )); + + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let write_result = (|| -> Result<(), std::io::Error> { + let mut file = options.open(&temporary)?; + file.write_all(bytes)?; + file.sync_all()?; + fs::rename(&temporary, path)?; + Ok(()) + })(); + if write_result.is_err() { + let _ = fs::remove_file(&temporary); + } + write_result.map_err(Into::into) +} + +#[cfg(test)] +mod tests { + use super::*; + use wifi_densepose_signal::ruvsense::field_model::FieldModelConfig; + + fn completed_model(now_us: u64) -> FieldModel { + let mut model = FieldModel::new(FieldModelConfig { + n_links: 1, + n_subcarriers: 4, + n_modes: 2, + min_calibration_frames: 3, + min_calibration_duration_s: 0.0, + baseline_expiry_s: 3_600.0, + }) + .unwrap(); + for index in 0..3 { + model + .feed_calibration(&[vec![1.0 + index as f64, 2.0, 3.0, 4.0]]) + .unwrap(); + } + model.finalize_calibration(now_us, 7).unwrap(); + model + } + + #[test] + fn round_trip_is_installation_bound_and_contains_no_raw_frames() { + let dir = tempfile::tempdir().unwrap(); + let path = path_in(dir.path()); + let now_ms = 1_000_000; + let model = completed_model(now_ms * 1_000); + let stored = store(&path, "home-a", &[5], "model", now_ms, &model).unwrap(); + let text = String::from_utf8(fs::read(&path).unwrap()).unwrap(); + assert!(!text.contains("home-a")); + assert!(!text.contains("raw_csi")); + + let (restored, loaded) = load(&path, "home-a", now_ms + 1).unwrap(); + assert_eq!(stored, loaded); + assert_eq!( + restored.export_snapshot().unwrap(), + model.export_snapshot().unwrap() + ); + assert!(matches!( + load(&path, "home-b", now_ms + 1), + Err(BootstrapBaselineError::InstallationMismatch) + )); + } + + #[test] + fn tamper_and_expiry_fail_closed() { + let dir = tempfile::tempdir().unwrap(); + let path = path_in(dir.path()); + let now_ms = 1_000_000; + let model = completed_model(now_ms * 1_000); + store(&path, "home-a", &[5], "model", now_ms, &model).unwrap(); + + let mut image: serde_json::Value = + serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); + image["payload"]["source_model_id"] = serde_json::json!("forged"); + fs::write(&path, serde_json::to_vec(&image).unwrap()).unwrap(); + assert!(matches!( + load(&path, "home-a", now_ms + 1), + Err(BootstrapBaselineError::DigestMismatch) + )); + + store(&path, "home-a", &[5], "model", now_ms, &model).unwrap(); + assert!(matches!( + load(&path, "home-a", now_ms + 3_600_001), + Err(BootstrapBaselineError::Expired) + | Err(BootstrapBaselineError::FieldModel( + FieldModelError::BaselineExpired { .. } + )) + )); + } + + #[test] + fn promotion_gate_requires_twelve_fresh_samples_and_zero_vitals() { + let passing = vec![ + BootstrapValidationSample { + fresh_tick: true, + calibrated_empty: true, + vital_signs_absent: true, + }; + BOOTSTRAP_VALIDATION_SAMPLES + ]; + assert!(evaluate_validation(&passing).passed); + + let mut stale = passing.clone(); + stale[0].fresh_tick = false; + assert!(!evaluate_validation(&stale).passed); + let mut vital = passing; + vital[0].vital_signs_absent = false; + assert!(!evaluate_validation(&vital).passed); + } +} diff --git a/v2/crates/wifi-densepose-sensing-server/src/lib.rs b/v2/crates/wifi-densepose-sensing-server/src/lib.rs index 93d00f118..0cf0214f1 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/lib.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/lib.rs @@ -9,6 +9,7 @@ //! - Real-time CSI introspection / low-latency tap (`introspection`, ADR-099) pub mod bearer_auth; +pub mod bootstrap_baseline; pub mod browser_session; pub mod ws_ticket; pub mod cli; diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index cc5cb0285..111c91cd3 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -39,11 +39,15 @@ use wifi_densepose_sensing_server::{ dataset, embedding, error_response, graph_transformer, rufield_surface, semconv, telemetry, trainer, }; +use wifi_densepose_sensing_server::bootstrap_baseline::{ + self, BootstrapBaselineMetadata, BootstrapValidationSample, BOOTSTRAP_VALIDATION_SAMPLES, +}; // ADR-295 / ADR-297: canonical provenance state + per-node/room inference. use wifi_densepose_sensing_server::inference::{fuse_room, NodeInference, RoomInference}; use wifi_densepose_sensing_server::provenance::SourceState; use ruvector_mincut::{DynamicMinCut, MinCutBuilder}; +use rand::{rngs::OsRng, RngCore}; use std::collections::{BTreeMap, HashMap, VecDeque}; use std::net::SocketAddr; use std::path::PathBuf; @@ -142,6 +146,11 @@ struct Args { #[arg(long, env = "RUVIEW_INSTALLATION_ID")] installation_id: Option, + /// Private server state directory used for runtime configuration and the + /// installation bound empty room bootstrap prior. + #[arg(long, default_value = "data", env = "RUVIEW_DATA_DIR")] + data_dir: PathBuf, + /// Human-readable local service name shown by commissioning clients. #[arg( long, @@ -1585,6 +1594,16 @@ struct AppStateInner { engine_bridge: engine_bridge::EngineBridge, /// SVD-based room field model for eigenvalue person counting (None until calibration). field_model: Option, + /// Stable installation identity used only to bind local persisted state. + installation_id: Option, + /// Metadata for the privacy reduced empty room image, when available. + bootstrap_baseline: Option, + /// A restored prior can affect startup occupancy but cannot authorize vitals. + bootstrap_baseline_active: bool, + /// Server generated identity for the current explicit calibration model. + calibration_model_id: Option, + /// Nodes that actually contributed frames to the current calibration. + calibration_source_node_ids: std::collections::BTreeSet, // ── ADR-044 §5.2: adaptive rolling-p95 normalization ───────────────────── /// Rolling P95 of `FeatureInfo.variance` over the last ~30 s (600 frames @ 20 Hz). pub(crate) p95_variance: RollingP95, @@ -1849,6 +1868,11 @@ impl AppStateInner { None, ), field_model: None, + installation_id: None, + bootstrap_baseline: None, + bootstrap_baseline_active: false, + calibration_model_id: None, + calibration_source_node_ids: std::collections::BTreeSet::new(), p95_variance: RollingP95::new(600, 60), p95_motion_band_power: RollingP95::new(600, 60), p95_spectral_power: RollingP95::new(600, 60), @@ -3364,7 +3388,7 @@ async fn windows_wifi_task(state: SharedState, tick_ms: u64) { feat_variance.min(1.0), &sub_variances, ), - vital_signs: Some(vitals), + vital_signs: None, enhanced_motion, enhanced_breathing, posture: posture_str, @@ -3528,7 +3552,7 @@ async fn windows_wifi_fallback_tick(state: &SharedState, seq: u32) { feat_variance.min(1.0), &sub_variances, ), - vital_signs: Some(vitals), + vital_signs: None, enhanced_motion: None, enhanced_breathing: None, posture: None, @@ -5979,10 +6003,96 @@ async fn adaptive_unload(State(state): State) -> Json Option { + if !explicit_calibration_fresh || person_count != 1 { + return None; + } + let signal_quality = candidates.signal_quality.clamp(0.0, 1.0); + if signal_quality < VITAL_PUBLICATION_MIN_SIGNAL_QUALITY { + return None; + } + let breathing_rate_bpm = (candidates.breathing_confidence + >= VITAL_PUBLICATION_MIN_CONFIDENCE) + .then_some(candidates.breathing_rate_bpm) + .flatten(); + let heart_rate_bpm = + (candidates.heartbeat_confidence >= VITAL_PUBLICATION_MIN_CONFIDENCE) + .then_some(candidates.heart_rate_bpm) + .flatten(); + if breathing_rate_bpm.is_none() && heart_rate_bpm.is_none() { + return None; + } + Some(VitalSigns { + breathing_rate_bpm, + heart_rate_bpm, + breathing_confidence: candidates.breathing_confidence.clamp(0.0, 1.0), + heartbeat_confidence: candidates.heartbeat_confidence.clamp(0.0, 1.0), + signal_quality, + }) +} + +fn opaque_calibration_model_id() -> String { + let mut bytes = [0_u8; 16]; + OsRng.fill_bytes(&mut bytes); + let suffix: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); + format!("cal-model-{suffix}") +} + +#[cfg(test)] +mod bootstrap_vital_publication_tests { + use super::*; + + fn strong_candidates() -> VitalSigns { + VitalSigns { + breathing_rate_bpm: Some(15.0), + heart_rate_bpm: Some(72.0), + breathing_confidence: 0.9, + heartbeat_confidence: 0.9, + signal_quality: 0.9, + } + } + + #[test] + fn uncalibrated_empty_and_multi_person_rooms_publish_no_vitals() { + let candidates = strong_candidates(); + assert!(vitals_for_publication(&candidates, false, 1).is_none()); + assert!(vitals_for_publication(&candidates, true, 0).is_none()); + assert!(vitals_for_publication(&candidates, true, 2).is_none()); + } + + #[test] + fn one_explicitly_calibrated_occupant_may_publish_qualified_vitals() { + let published = vitals_for_publication(&strong_candidates(), true, 1).unwrap(); + assert_eq!(published.breathing_rate_bpm, Some(15.0)); + assert_eq!(published.heart_rate_bpm, Some(72.0)); + } + + #[test] + fn weak_vital_candidates_fail_closed() { + let mut candidates = strong_candidates(); + candidates.signal_quality = 0.2; + assert!(vitals_for_publication(&candidates, true, 1).is_none()); + } +} + // ── Field model calibration endpoints (eigenvalue person counting) ────────── async fn calibration_start(State(state): State) -> Json { let mut s = state.write().await; + if s.bootstrap_baseline_active { + s.field_model = None; + s.bootstrap_baseline_active = false; + } // Guard: don't discard an in-progress or fresh calibration if let Some(ref fm) = s.field_model { match fm.status() { @@ -6005,9 +6115,12 @@ async fn calibration_start(State(state): State) -> Json { s.field_model = Some(fm); + s.calibration_model_id = Some(opaque_calibration_model_id()); + s.calibration_source_node_ids.clear(); Json(serde_json::json!({ "success": true, "message": "Calibration started — keep room empty while frames accumulate.", + "model_id": s.calibration_model_id, })) } // ADR-080 #2: FieldModel init error chain stays server-side only. @@ -6017,6 +6130,8 @@ async fn calibration_start(State(state): State) -> Json) -> Json { let mut s = state.write().await; + let model_id = s.calibration_model_id.clone(); + let source_node_ids: Vec = s.calibration_source_node_ids.iter().copied().collect(); if let Some(ref mut fm) = s.field_model { // Guard: finalizing before enough empty-room frames have accumulated // is a client-side sequencing error, not a server fault. Return a @@ -6062,6 +6177,8 @@ async fn calibration_stop(State(state): State) -> Json) -> Json) -> Json { let s = state.read().await; - match s.field_model.as_ref() { - // #1756: expose elapsed wall-clock time and the effective aggregate - // sample rate alongside the frame count, so a client can see the - // shape of the capture instead of frames alone. - Some(fm) => Json(serde_json::json!({ - "active": true, - "status": format!("{:?}", fm.status()), - "frame_count": fm.calibration_frame_count(), - "min_frames": fm.min_calibration_frames(), - "elapsed_s": fm.calibration_elapsed_s(), - "frames_per_second": fm.calibration_frames_per_second(), - "min_duration_s": fm.min_calibration_duration_s(), - })), - None => Json(serde_json::json!({ + let (active, status, frame_count, min_frames, elapsed_s, frames_per_second, min_duration_s) = + s.field_model.as_ref().map_or( + (false, "none".to_string(), 0, 0, 0.0, 0.0, 0.0), + |model| { + ( + true, + format!("{:?}", model.status()).to_lowercase(), + model.calibration_frame_count(), + model.min_calibration_frames(), + model.calibration_elapsed_s(), + model.calibration_frames_per_second(), + model.min_calibration_duration_s(), + ) + }, + ); + Json(serde_json::json!({ + "active": active, + "status": status, + "frame_count": frame_count, + "min_frames": min_frames, + "elapsed_s": elapsed_s, + "frames_per_second": frames_per_second, + "min_duration_s": min_duration_s, + "model_id": s.calibration_model_id, + "source_node_ids": s.calibration_source_node_ids, + "binding_mode": if s.bootstrap_baseline_active { "bootstrap_only" } else if active { "runtime" } else { "none" }, + "bootstrap_baseline": s.bootstrap_baseline.as_ref().map(|metadata| serde_json::json!({ + "stored": true, + "active": s.bootstrap_baseline_active, + "authority": metadata.authority, + "source_node_ids": metadata.source_node_ids, + "source_model_id": metadata.source_model_id, + "created_at_unix_ms": metadata.created_at_unix_ms, + "expires_at_unix_ms": metadata.expires_at_unix_ms, + "content_sha256": metadata.content_sha256, + "calibrated_evidence_authorized": false, + "numeric_vitals_authorized": false, + })).unwrap_or_else(|| serde_json::json!({ + "stored": false, "active": false, - "status": "none", + "authority": "none", + "calibrated_evidence_authorized": false, + "numeric_vitals_authorized": false, })), + })) +} + +/// Validate a completed empty room model against twelve fresh server observed +/// samples, then persist only aggregate statistics. The request cannot supply +/// scores, labels, raw CSI, or a replacement model. +async fn calibration_promote_bootstrap( + State(state): State, +) -> Json { + let expected_model_id = { + let s = state.read().await; + if s.bootstrap_baseline_active + || !s.field_model.as_ref().is_some_and(|model| { + matches!(model.status(), CalibrationStatus::Fresh) + }) + { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_not_finalized", + "error": "Finalize an explicit room calibration before validating a startup baseline.", + })); + } + match s.calibration_model_id.clone() { + Some(model_id) => model_id, + None => { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_model_identity_missing", + "error": "The active calibration has no server generated model identity.", + })); + } + } + }; + + let mut samples = Vec::with_capacity(BOOTSTRAP_VALIDATION_SAMPLES); + let mut last_tick = None; + for _ in 0..BOOTSTRAP_VALIDATION_SAMPLES { + let interval_deadline = tokio::time::Instant::now() + Duration::from_secs(1); + let mut sample = BootstrapValidationSample { + fresh_tick: false, + calibrated_empty: false, + vital_signs_absent: true, + }; + while tokio::time::Instant::now() < interval_deadline { + let observed = { + let s = state.read().await; + if s.calibration_model_id.as_deref() != Some(expected_model_id.as_str()) + || s.bootstrap_baseline_active + { + None + } else { + s.latest_update.as_ref().and_then(|update| { + (Some(update.tick) != last_tick).then_some(( + update.tick, + s.person_count() == 0, + update.vital_signs.is_none(), + )) + }) + } + }; + if let Some((tick, calibrated_empty, vital_signs_absent)) = observed { + last_tick = Some(tick); + sample = BootstrapValidationSample { + fresh_tick: true, + calibrated_empty, + vital_signs_absent, + }; + break; + } + tokio::time::sleep(Duration::from_millis(50)).await; + } + samples.push(sample); + tokio::time::sleep_until(interval_deadline).await; } + + let validation = bootstrap_baseline::evaluate_validation(&samples); + if !validation.passed { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_validation_failed", + "error": "The held out empty room check did not pass. The model was not stored.", + "validation": validation, + })); + } + + let mut s = state.write().await; + if s.calibration_model_id.as_deref() != Some(expected_model_id.as_str()) + || s.bootstrap_baseline_active + { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_model_changed", + "error": "Calibration changed during validation. The model was not stored.", + })); + } + let Some(installation_id) = s.installation_id.clone() else { + return Json(serde_json::json!({ + "success": false, + "error_code": "installation_id_required", + "error": "A stable installation identity is required to store a local startup baseline.", + })); + }; + let source_node_ids: Vec = s.calibration_source_node_ids.iter().copied().collect(); + if source_node_ids.is_empty() || source_node_ids.len() > 16 { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_source_nodes_missing", + "error": "The active calibration must contain between 1 and 16 contributing ESP32 nodes.", + })); + } + let Some(field_model) = s.field_model.as_ref() else { + return Json(serde_json::json!({ + "success": false, + "error_code": "no_field_model", + "error": "Calibration changed during validation. The model was not stored.", + })); + }; + let path = bootstrap_baseline::path_in(&s.data_dir); + let created_at_unix_ms = chrono::Utc::now().timestamp_millis() as u64; + let stored = match bootstrap_baseline::store( + &path, + &installation_id, + &source_node_ids, + &expected_model_id, + created_at_unix_ms, + field_model, + ) { + Ok(metadata) => metadata, + Err(error) => { + return error_response::internal_error_json("bootstrap baseline store", error) + } + }; + s.bootstrap_baseline = Some(stored.clone()); + info!( + model_id = %stored.source_model_id, + source_node_ids = ?stored.source_node_ids, + content_sha256 = %stored.content_sha256, + "Stored validated privacy reduced startup baseline" + ); + Json(serde_json::json!({ + "success": true, + "message": "Validated startup baseline stored locally.", + "validation": validation, + "bootstrap_baseline": stored, + "calibrated_evidence_authorized": false, + "numeric_vitals_authorized": false, + })) +} + +/// Cancel only an unfinished capture. Completed model deletion remains on the +/// administrator scoped reset route. +async fn calibration_cancel(State(state): State) -> Json { + let mut s = state.write().await; + let cancellable = s.field_model.as_ref().is_some_and(|model| { + matches!( + model.status(), + CalibrationStatus::Uncalibrated | CalibrationStatus::Collecting + ) + }); + if !cancellable { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_not_collecting", + "error": "Only an unfinished empty room capture can be cancelled.", + })); + } + s.field_model = None; + s.calibration_model_id = None; + s.calibration_source_node_ids.clear(); + if let Some(installation_id) = s.installation_id.clone() { + let path = bootstrap_baseline::path_in(&s.data_dir); + if let Ok((model, metadata)) = bootstrap_baseline::load( + &path, + &installation_id, + chrono::Utc::now().timestamp_millis() as u64, + ) { + s.field_model = Some(model); + s.bootstrap_baseline = Some(metadata); + s.bootstrap_baseline_active = true; + } + } + Json(serde_json::json!({ + "success": true, + "message": "Unfinished empty room capture cancelled.", + "status": if s.bootstrap_baseline_active { "bootstrap" } else { "none" }, + })) +} + +async fn calibration_reset(State(state): State) -> Json { + let mut s = state.write().await; + s.field_model = None; + s.calibration_model_id = None; + s.calibration_source_node_ids.clear(); + s.bootstrap_baseline_active = false; + let path = bootstrap_baseline::path_in(&s.data_dir); + let bootstrap_removed = match bootstrap_baseline::remove(&path) { + Ok(removed) => { + s.bootstrap_baseline = None; + removed + } + Err(error) => { + warn!(%error, "Could not remove stored bootstrap baseline during reset"); + false + } + }; + Json(serde_json::json!({ + "success": true, + "message": "Calibration model reset.", + "status": "none", + "bootstrap_removed": bootstrap_removed, + })) } /// Compatibility surface used by the bundled dashboard. Activity history is @@ -6119,16 +6473,27 @@ fn chrono_timestamp() -> u64 { async fn vital_signs_endpoint(State(state): State) -> Json { let s = state.read().await; - let vs = &s.latest_vitals; + let person_count = s.person_count(); + let explicit_calibration_fresh = !s.bootstrap_baseline_active + && s.field_model + .as_ref() + .is_some_and(|model| matches!(model.status(), CalibrationStatus::Fresh)); + let published = vitals_for_publication( + &s.latest_vitals, + explicit_calibration_fresh, + person_count, + ); let (br_len, br_cap, hb_len, hb_cap) = s.vital_detector.buffer_status(); Json(serde_json::json!({ "vital_signs": { - "breathing_rate_bpm": vs.breathing_rate_bpm, - "heart_rate_bpm": vs.heart_rate_bpm, - "breathing_confidence": vs.breathing_confidence, - "heartbeat_confidence": vs.heartbeat_confidence, - "signal_quality": vs.signal_quality, + "breathing_rate_bpm": published.as_ref().and_then(|value| value.breathing_rate_bpm), + "heart_rate_bpm": published.as_ref().and_then(|value| value.heart_rate_bpm), + "breathing_confidence": published.as_ref().map(|value| value.breathing_confidence), + "heartbeat_confidence": published.as_ref().map(|value| value.heartbeat_confidence), + "signal_quality": published.as_ref().map(|value| value.signal_quality), }, + "authority": if published.is_some() { "explicit_calibration" } else { "abstained" }, + "abstention_reason": if published.is_some() { serde_json::Value::Null } else { serde_json::json!("fresh explicit calibration with exactly one occupant and qualified evidence required") }, "buffer_status": { "breathing_samples": br_len, "breathing_capacity": br_cap, @@ -6193,6 +6558,18 @@ async fn edge_registry_endpoint( /// GET /api/v1/edge-vitals — latest edge vitals from ESP32 (ADR-039). async fn edge_vitals_endpoint(State(state): State) -> Json { let s = state.read().await; + let explicit_single_occupant = !s.bootstrap_baseline_active + && s.field_model + .as_ref() + .is_some_and(|model| matches!(model.status(), CalibrationStatus::Fresh)) + && s.person_count() == 1; + if !explicit_single_occupant { + return Json(serde_json::json!({ + "status": "abstained", + "edge_vitals": null, + "message": "Fresh explicit calibration with exactly one occupant is required.", + })); + } match &s.edge_vitals { Some(v) => Json(serde_json::json!({ "status": "ok", @@ -6831,8 +7208,15 @@ async fn udp_receiver_task( .get(&node_id) .map(|ns| ns.frame_history.clone()) { - if let Some(ref mut fm) = s.field_model { + let accepted = if let Some(ref mut fm) = s.field_model { + let before = fm.calibration_frame_count(); field_bridge::maybe_feed_calibration(fm, &frame_history); + fm.calibration_frame_count() > before + } else { + false + }; + if accepted { + s.calibration_source_node_ids.insert(node_id); } } @@ -6903,6 +7287,24 @@ async fn udp_receiver_task( (vitals.presence_score as f64).min(1.0), &[], ); + let vital_candidates = VitalSigns { + breathing_rate_bpm: (vitals.breathing_rate_bpm > 0.0) + .then_some(vitals.breathing_rate_bpm), + heart_rate_bpm: (vitals.heartrate_bpm > 0.0) + .then_some(vitals.heartrate_bpm), + breathing_confidence: if vitals.presence { 0.7 } else { 0.0 }, + heartbeat_confidence: if vitals.presence { 0.7 } else { 0.0 }, + signal_quality: vitals.presence_score as f64, + }; + let explicit_calibration_fresh = !s.bootstrap_baseline_active + && s.field_model.as_ref().is_some_and(|model| { + matches!(model.status(), CalibrationStatus::Fresh) + }); + let published_vitals = vitals_for_publication( + &vital_candidates, + explicit_calibration_fresh, + total_persons, + ); let mut update = SensingUpdate { msg_type: "sensing_update".to_string(), @@ -6913,21 +7315,7 @@ async fn udp_receiver_task( features: fused_features.clone(), classification, signal_field, - vital_signs: Some(VitalSigns { - breathing_rate_bpm: if vitals.breathing_rate_bpm > 0.0 { - Some(vitals.breathing_rate_bpm) - } else { - None - }, - heart_rate_bpm: if vitals.heartrate_bpm > 0.0 { - Some(vitals.heartrate_bpm) - } else { - None - }, - breathing_confidence: if vitals.presence { 0.7 } else { 0.0 }, - heartbeat_confidence: if vitals.presence { 0.7 } else { 0.0 }, - signal_quality: vitals.presence_score as f64, - }), + vital_signs: published_vitals, enhanced_motion: None, enhanced_breathing: None, posture: None, @@ -7312,8 +7700,15 @@ async fn udp_receiver_task( .get(&node_id) .map(|ns| ns.frame_history.clone()) { - if let Some(ref mut fm) = s.field_model { + let accepted = if let Some(ref mut fm) = s.field_model { + let before = fm.calibration_frame_count(); field_bridge::maybe_feed_calibration(fm, &frame_history); + fm.calibration_frame_count() > before + } else { + false + }; + if accepted { + s.calibration_source_node_ids.insert(node_id); } } @@ -7369,6 +7764,15 @@ async fn udp_receiver_task( NODE_STALE_AFTER_MS, ); let room_classification = debounce_room_classification(&mut s, &room_inference); + let explicit_calibration_fresh = !s.bootstrap_baseline_active + && s.field_model.as_ref().is_some_and(|model| { + matches!(model.status(), CalibrationStatus::Fresh) + }); + let published_vitals = vitals_for_publication( + &vitals, + explicit_calibration_fresh, + total_persons, + ); let mut update = SensingUpdate { msg_type: "sensing_update".to_string(), @@ -7390,7 +7794,7 @@ async fn udp_receiver_task( fused_features.variance.min(1.0), &sub_variances, ), - vital_signs: Some(vitals), + vital_signs: published_vitals, enhanced_motion: None, enhanced_breathing: None, posture: None, @@ -7645,7 +8049,7 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) { features.variance.min(1.0), &sub_variances, ), - vital_signs: Some(vitals), + vital_signs: None, enhanced_motion: None, enhanced_breathing: None, posture: None, @@ -8810,12 +9214,49 @@ async fn main() { ); // ADR-044 §5.3: load persisted runtime config from the data directory. - let data_dir = std::path::PathBuf::from("data"); + let data_dir = args.data_dir.clone(); + if let Err(error) = std::fs::create_dir_all(&data_dir) { + warn!(path = %data_dir.display(), %error, "Could not create server data directory"); + } let runtime_config = load_runtime_config(&data_dir); // ADR-271: resolve (or generate + persist) the browser-session signing key // before any request can arrive. Zero-config for a single appliance; the // env var still wins for a multi-instance deployment that must share one. wifi_densepose_sensing_server::browser_session::init_secret(&data_dir); + let (bootstrap_field_model, bootstrap_metadata, bootstrap_baseline_active) = + if !args.calibrate { + args.installation_id.as_deref().map_or( + (None, None, false), + |installation_id| { + let path = bootstrap_baseline::path_in(&data_dir); + match bootstrap_baseline::load( + &path, + installation_id, + chrono::Utc::now().timestamp_millis() as u64, + ) { + Ok((model, metadata)) => { + info!( + source_model_id = %metadata.source_model_id, + source_node_ids = ?metadata.source_node_ids, + "Restored privacy reduced empty room bootstrap prior" + ); + (Some(model), Some(metadata), true) + } + Err(bootstrap_baseline::BootstrapBaselineError::Io(error)) + if error.kind() == std::io::ErrorKind::NotFound => + { + (None, None, false) + } + Err(error) => { + warn!(%error, "Ignored invalid empty room bootstrap prior"); + (None, None, false) + } + } + }, + ) + } else { + (None, None, false) + }; info!( "Loaded runtime config: dedup_factor={:.2}", runtime_config.dedup_factor @@ -9029,8 +9470,13 @@ async fn main() { info!("Field model calibration enabled — room should be empty during startup"); FieldModel::new(field_bridge::single_link_config()).ok() } else { - None + bootstrap_field_model }, + installation_id: args.installation_id.clone(), + bootstrap_baseline: bootstrap_metadata, + bootstrap_baseline_active, + calibration_model_id: args.calibrate.then(opaque_calibration_model_id), + calibration_source_node_ids: std::collections::BTreeSet::new(), // ADR-044 §5.2: rolling-P95 over ~30 s at 20 Hz; warm-up after 60 samples. p95_variance: RollingP95::new(600, 60), p95_motion_band_power: RollingP95::new(600, 60), @@ -9326,7 +9772,13 @@ async fn main() { // Field model calibration (eigenvalue-based person counting) .route("/api/v1/calibration/start", post(calibration_start)) .route("/api/v1/calibration/stop", post(calibration_stop)) + .route( + "/api/v1/calibration/bootstrap/promote", + post(calibration_promote_bootstrap), + ) + .route("/api/v1/calibration/cancel", post(calibration_cancel)) .route("/api/v1/calibration/status", get(calibration_status)) + .route("/api/v1/calibration/reset", post(calibration_reset)) // ADR-044 §5.3: runtime-configurable dedup factor .route( "/api/v1/config/dedup-factor", diff --git a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs index 9ea6a6ef1..14c9e65d9 100644 --- a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs +++ b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs @@ -23,6 +23,7 @@ use ndarray::Array2; use ndarray_linalg::Eigh; #[cfg(feature = "eigenvalue")] use ndarray_linalg::UPLO; +use serde::{Deserialize, Serialize}; // --------------------------------------------------------------------------- // Calibration window constants @@ -42,6 +43,11 @@ pub const CALIBRATION_DURATION_S: f64 = 600.0; /// [`CALIBRATION_DURATION_S`] ("10 min at 20 Hz"). pub const ASSUMED_CALIBRATION_RATE_HZ: f64 = 20.0; +/// A recent window whose mean remains within this residual energy of the +/// empty room manifold is treated as background before eigenvalue counting. +#[cfg(feature = "eigenvalue")] +const EMPTY_ROOM_RESIDUAL_ENERGY_MAX: f64 = 1.0; + // --------------------------------------------------------------------------- // Error types // --------------------------------------------------------------------------- @@ -261,7 +267,8 @@ impl LinkBaselineStats { // --------------------------------------------------------------------------- /// Configuration for field model calibration and runtime. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] pub struct FieldModelConfig { /// Number of links in the mesh. pub n_links: usize, @@ -304,7 +311,8 @@ impl Default for FieldModelConfig { /// Learned from SVD on the covariance of CSI amplitudes during /// empty-room calibration. The top-K modes capture environmental /// variation (temperature, humidity, time-of-day effects). -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] pub struct FieldNormalMode { /// Per-link baseline mean: `[n_links][n_subcarriers]`. pub baseline: Vec>, @@ -362,6 +370,18 @@ pub enum CalibrationStatus { Expired, } +/// Privacy reduced restart image for a completed field model. +/// +/// This contains only aggregate baseline statistics and environmental modes. +/// It excludes raw CSI frames, device addresses, room names, credentials, and +/// calibration authority. A restored image is only a bootstrap prior. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldModelSnapshotV1 { + pub config: FieldModelConfig, + pub modes: FieldNormalMode, +} + /// The persistent field model for a single room. /// /// Maintains per-link Welford statistics during calibration, then @@ -483,6 +503,96 @@ impl FieldModel { self.modes.as_ref() } + /// Export aggregate model state suitable for local restart persistence. + /// Raw calibration observations and Welford accumulators are not exported. + pub fn export_snapshot(&self) -> Result { + let modes = self.modes.clone().ok_or(FieldModelError::NotCalibrated)?; + Ok(FieldModelSnapshotV1 { + config: self.config.clone(), + modes, + }) + } + + /// Restore a bounded, validated aggregate snapshot. + pub fn from_snapshot( + snapshot: FieldModelSnapshotV1, + current_us: u64, + ) -> Result { + let config = &snapshot.config; + if config.n_links > 16 || config.n_subcarriers > 2_048 { + return Err(FieldModelError::InvalidConfig( + "snapshot dimensions exceed bounded limits".into(), + )); + } + if !config.min_calibration_duration_s.is_finite() + || config.min_calibration_duration_s < 0.0 + || !config.baseline_expiry_s.is_finite() + || config.baseline_expiry_s <= 0.0 + || config.baseline_expiry_s > 604_800.0 + { + return Err(FieldModelError::InvalidConfig( + "snapshot timing values are invalid".into(), + )); + } + + let modes = &snapshot.modes; + let baseline_shape_valid = modes.baseline.len() == config.n_links + && modes + .baseline + .iter() + .all(|link| link.len() == config.n_subcarriers); + let mode_shape_valid = modes.environmental_modes.len() == modes.mode_energies.len() + && modes.environmental_modes.len() <= config.n_modes + && modes + .environmental_modes + .iter() + .all(|mode| mode.len() == config.n_subcarriers); + let values_valid = modes + .baseline + .iter() + .flatten() + .chain(modes.environmental_modes.iter().flatten()) + .all(|value| value.is_finite()) + && modes + .mode_energies + .iter() + .all(|value| value.is_finite() && *value >= 0.0) + && modes.variance_explained.is_finite() + && (0.0..=10.0).contains(&modes.variance_explained) + && modes.baseline_noise_var.is_finite() + && modes.baseline_noise_var >= 0.0 + && modes.baseline_eigenvalue_count <= config.n_subcarriers; + if !baseline_shape_valid || !mode_shape_valid || !values_valid { + return Err(FieldModelError::InvalidConfig( + "snapshot modes are malformed or non finite".into(), + )); + } + if modes.calibrated_at_us == 0 + || modes.calibrated_at_us > current_us.saturating_add(300_000_000) + { + return Err(FieldModelError::InvalidConfig( + "snapshot calibration time is invalid".into(), + )); + } + let elapsed_s = current_us.saturating_sub(modes.calibrated_at_us) as f64 / 1_000_000.0; + if elapsed_s > config.baseline_expiry_s { + return Err(FieldModelError::BaselineExpired { + elapsed_s, + max_s: config.baseline_expiry_s, + }); + } + + let mut model = Self::new(snapshot.config)?; + model.status = if elapsed_s > model.config.baseline_expiry_s * 0.5 { + CalibrationStatus::Stale + } else { + CalibrationStatus::Fresh + }; + model.last_calibration_us = modes.calibrated_at_us; + model.modes = Some(snapshot.modes); + Ok(model) + } + /// Number of calibration frames collected so far. pub fn calibration_frame_count(&self) -> u64 { self.link_stats @@ -883,6 +993,17 @@ impl FieldModel { *m /= count as f64; } + // Rank alone is not person evidence. A continuation window can expose + // more significant eigenvalues while its mean remains on the learned + // empty room manifold. Fail toward empty only inside the same + // conservative residual energy boundary used by the server fallback. + if self + .extract_perturbation(&[mean.clone()]) + .is_ok_and(|perturbation| perturbation.total_energy <= EMPTY_ROOM_RESIDUAL_ENERGY_MAX) + { + return Ok(0); + } + let mut cov = Array2::::zeros((n, n)); for frame in recent_frames { if frame.len() >= n { @@ -1538,6 +1659,48 @@ mod tests { ); } + #[test] + fn snapshot_round_trip_preserves_aggregate_modes_without_raw_frames() { + let config = make_config(1, 4, 10); + let mut model = FieldModel::new(config).unwrap(); + for i in 0..10 { + model + .feed_calibration(&[vec![1.0 + i as f64 * 0.01, 2.0, 3.0, 4.0]]) + .unwrap(); + } + model.finalize_calibration(1_000_000, 0xCA1).unwrap(); + + let snapshot = model.export_snapshot().unwrap(); + let restored = FieldModel::from_snapshot(snapshot.clone(), 2_000_000).unwrap(); + + assert_eq!(restored.status(), CalibrationStatus::Fresh); + assert_eq!(restored.calibration_frame_count(), 0); + assert_eq!(restored.export_snapshot().unwrap(), snapshot); + } + + #[test] + fn snapshot_restore_rejects_expired_and_malformed_images() { + let config = make_config(1, 4, 10); + let mut model = FieldModel::new(config).unwrap(); + for _ in 0..10 { + model.feed_calibration(&[vec![1.0, 2.0, 3.0, 4.0]]).unwrap(); + } + model.finalize_calibration(1_000_000, 0).unwrap(); + let snapshot = model.export_snapshot().unwrap(); + + assert!(matches!( + FieldModel::from_snapshot(snapshot.clone(), 90_000_000_000), + Err(FieldModelError::BaselineExpired { .. }) + )); + + let mut malformed = snapshot; + malformed.modes.baseline[0].pop(); + assert!(matches!( + FieldModel::from_snapshot(malformed, 2_000_000), + Err(FieldModelError::InvalidConfig(_)) + )); + } + #[test] fn test_environmental_projection_removes_drift() { let config = make_config(1, 4, 10); From 2227b42841a230f1df00e5d9099e45b454de648b Mon Sep 17 00:00:00 2001 From: rUv Date: Fri, 4 Sep 2026 23:02:06 -0400 Subject: [PATCH 17/27] feat(calibration): learn empty room startup confidence (#1840) --- ...DR-355-local-empty-room-bootstrap-prior.md | 55 +- .../wifi-densepose-sensing-server/Cargo.toml | 2 +- .../src/bootstrap_baseline.rs | 54 +- .../src/field_bridge.rs | 300 ++++++++- .../wifi-densepose-sensing-server/src/main.rs | 327 ++++++++-- .../src/ruvsense/field_model.rs | 590 ++++++++++++++++-- 6 files changed, 1218 insertions(+), 110 deletions(-) diff --git a/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md b/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md index c14f78271..5eb4ba732 100644 --- a/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md +++ b/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md @@ -23,17 +23,26 @@ directory, which is unreliable for embedded application hosts. RuView may persist a completed empty room field model as a local bootstrap prior only after two server controlled stages: -1. Normal calibration reaches both 12,000 accepted frames and 600 seconds. -2. Promotion observes 12 new one second samples. At least 10 must resolve - empty, all ticks must be fresh, and none may publish numeric vital signs. +1. Normal calibration reaches both 1,000 accepted frames and 600 seconds. + The frame target represents twenty complete fifty frame runtime windows. + Collection binds to the first accepted ESP32 source and rejects frames from + other radios for that single link model. +2. Promotion observes 12 new samples spaced by at least one second. Each + sample has a four second bounded acquisition timeout. At least 10 must + resolve empty, all ticks must be fresh, and none may publish numeric vital + signs. The stored image contains aggregate means, environmental modes, mode energies, -noise statistics, configuration, contributing node IDs, and a server generated -model ID. It excludes raw CSI, waveforms, device addresses, room names, pose, -identity, credentials, and positive human labels. +noise statistics, sorted scalar residual references, configuration, one source +node ID, and a server generated model ID. It excludes raw CSI, waveforms, +device addresses, room names, pose, identity, credentials, and positive human +labels. Runtime matching compares a fifty frame mean with the learned residual +distribution. Its score is empirical background conformance, not an absence +probability. The image is bound to a SHA 256 digest of the stable installation ID, carries -an integrity digest, is capped at 1 MiB, and inherits the field model expiry. +an integrity digest over the exact stored payload bytes, is capped at 1 MiB, +and inherits the field model expiry. Files with an invalid schema, dimensions, values, node set, installation binding, digest, size, or lifetime fail closed. @@ -43,6 +52,11 @@ evidence or numeric heart and breathing rates. Explicit calibration replaces it with fresh statistics. Cancelling an unfinished capture restores a valid prior. Administrator scoped reset removes it. +The status endpoint reports the learned runtime window, residual threshold, +reference count, current background conformance, and the negative only +authority boundary. A restored model never resumes collection and never +persists raw calibration frames. + The server accepts `--data-dir` or `RUVIEW_DATA_DIR`, allowing an application host to provide a protected state directory. @@ -71,12 +85,29 @@ occupied sequences. Empty data is never relabeled as human training. denies calibrated evidence and numeric vital authority. 5. Numeric vitals fail closed unless an explicit fresh calibration resolves exactly one occupant at sufficient signal quality and confidence. +6. The legacy `edge_vitals` WebSocket message applies the same publication + gate as `sensing_update`. When the bootstrap background matches, it emits + absent state, zero people, null numeric rates, and an explicit abstention + reason instead of forwarding raw edge candidates. + +## Measured installation evidence + +One physical empty home run on source Node 5 collected 1,762 accepted frames +over 655 seconds and produced 35 privacy reduced reference windows. Promotion +observed 12 of 12 fresh empty samples, zero stale samples, and zero numeric +vital samples. After process restart, 12 of 12 fresh samples matched the empty +background and contained no numeric vital signs. Background conformance scores +ranged from 50.0 to 64.3 percent with a median of 54.3 percent. + +This is measured negative evidence for one installation. No occupied positive +holdout was recorded, so it does not prove person recall, person counting, or +cross home accuracy improvement. ## Acceptance test With a physically empty room, complete calibration on stable nodes, promote -the model, and restart the server with the same installation ID and data -directory. Status must report `binding_mode=bootstrap_only`, zero collection -frames, and false authority for calibrated evidence and numeric vitals. Twelve -fresh empty samples must contain no numeric vital signs. Starting a new room -calibration must replace the prior and begin at frame zero. +the single source model, and restart the server with the same installation ID +and data directory. Status must report `binding_mode=bootstrap_only`, zero +collection frames, and false authority for calibrated evidence and numeric +vitals. Twelve fresh empty samples must contain no numeric vital signs. +Starting a new room calibration must replace the prior and begin at frame zero. diff --git a/v2/crates/wifi-densepose-sensing-server/Cargo.toml b/v2/crates/wifi-densepose-sensing-server/Cargo.toml index 3ab1a3c30..eaae6dfc3 100644 --- a/v2/crates/wifi-densepose-sensing-server/Cargo.toml +++ b/v2/crates/wifi-densepose-sensing-server/Cargo.toml @@ -30,7 +30,7 @@ mdns-sd = "0.11" # Serialization serde = { workspace = true } -serde_json.workspace = true +serde_json = { workspace = true, features = ["raw_value"] } # Logging tracing.workspace = true diff --git a/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs b/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs index 30fb6a11f..291e7e1c3 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs @@ -6,6 +6,7 @@ use rand::{rngs::OsRng, RngCore}; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; use sha2::{Digest, Sha256}; use std::fs::{self, OpenOptions}; use std::io::{Read, Write}; @@ -19,6 +20,8 @@ pub const BOOTSTRAP_BASELINE_SCHEMA: &str = "ruview.bootstrap-empty-field-model. pub const BOOTSTRAP_BASELINE_AUTHORITY: &str = "bootstrap_only"; pub const BOOTSTRAP_VALIDATION_SAMPLES: usize = 12; pub const BOOTSTRAP_VALIDATION_MIN_EMPTY: usize = 10; +pub const BOOTSTRAP_VALIDATION_MIN_SPACING_MS: u64 = 1_000; +pub const BOOTSTRAP_VALIDATION_SAMPLE_TIMEOUT_MS: u64 = 4_000; const BOOTSTRAP_MAX_FILE_BYTES: u64 = 1_048_576; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -41,6 +44,13 @@ struct BootstrapImageV1 { content_sha256: String, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct BootstrapImageRawV1 { + payload: Box, + content_sha256: String, +} + #[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct BootstrapBaselineMetadata { pub authority: &'static str, @@ -181,11 +191,19 @@ pub fn load( if bytes.len() as u64 > BOOTSTRAP_MAX_FILE_BYTES { return Err(BootstrapBaselineError::FileTooLarge); } - let image: BootstrapImageV1 = serde_json::from_slice(&bytes)?; - validate_payload(&image.payload, installation_id, current_unix_ms)?; - if payload_digest(&image.payload)? != image.content_sha256 { + // Verify the exact payload bytes written by storage before decoding the + // floating point model. Equivalent JSON number spellings may serialize + // differently, so decode and reserialize is not an integrity contract. + let raw_image: BootstrapImageRawV1 = serde_json::from_slice(&bytes)?; + if hex_sha256(raw_image.payload.get().as_bytes()) != raw_image.content_sha256 { return Err(BootstrapBaselineError::DigestMismatch); } + let payload: BootstrapPayloadV1 = serde_json::from_str(raw_image.payload.get())?; + validate_payload(&payload, installation_id, current_unix_ms)?; + let image = BootstrapImageV1 { + payload, + content_sha256: raw_image.content_sha256, + }; let model = FieldModel::from_snapshot( image.payload.field_model.clone(), current_unix_ms.saturating_mul(1_000), @@ -368,6 +386,11 @@ mod tests { #[test] fn promotion_gate_requires_twelve_fresh_samples_and_zero_vitals() { + assert_eq!(BOOTSTRAP_VALIDATION_MIN_SPACING_MS, 1_000); + assert_eq!(BOOTSTRAP_VALIDATION_SAMPLE_TIMEOUT_MS, 4_000); + assert!( + BOOTSTRAP_VALIDATION_SAMPLE_TIMEOUT_MS >= BOOTSTRAP_VALIDATION_MIN_SPACING_MS + ); let passing = vec![ BootstrapValidationSample { fresh_tick: true, @@ -385,4 +408,29 @@ mod tests { vital[0].vital_signs_absent = false; assert!(!evaluate_validation(&vital).passed); } + + #[test] + fn restart_verifies_the_exact_stored_payload_bytes() { + let dir = tempfile::tempdir().unwrap(); + let path = path_in(dir.path()); + let now_ms = 1_000_000; + let model = completed_model(now_ms * 1_000); + store(&path, "home-a", &[5], "model", now_ms, &model).unwrap(); + + let original = fs::read_to_string(&path).unwrap(); + let image: BootstrapImageRawV1 = serde_json::from_str(&original).unwrap(); + let rewritten_payload = image.payload.get().replacen( + "\"min_calibration_duration_s\":0.0", + "\"min_calibration_duration_s\":0.00", + 1, + ); + assert_ne!(rewritten_payload, image.payload.get()); + let digest = hex_sha256(rewritten_payload.as_bytes()); + let rewritten = + format!("{{\"payload\":{rewritten_payload},\"content_sha256\":\"{digest}\"}}"); + fs::write(&path, rewritten).unwrap(); + + let (_, loaded) = load(&path, "home-a", now_ms + 1).unwrap(); + assert_eq!(loaded.content_sha256, digest); + } } diff --git a/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs b/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs index 5b7f588fd..a60c266ca 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs @@ -40,6 +40,103 @@ const ENERGY_THRESH_3: f64 = 25.0; /// reliable — genuine higher counts come from the multistatic fusion path. const MAX_SINGLE_LINK_OCCUPANCY: usize = 3; +/// Low-authority comparison of the current source window with a restored +/// empty-room bootstrap image. This is background conformance only and cannot +/// authorize presence, person count, pose, identity, or vital signs. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct BootstrapBackgroundMatch { + pub matches_empty: bool, + pub score: Option, + pub residual_energy: f64, + pub residual_energy_threshold: f64, + pub window_size: usize, + pub reference_window_count: usize, +} + +fn mean_frame(frames: &[Vec]) -> Option> { + let width = frames.first()?.len(); + if width == 0 { + return None; + } + let mut mean = vec![0.0_f64; width]; + let mut count = 0_usize; + for frame in frames { + if frame.len() != width { + continue; + } + for (sum, value) in mean.iter_mut().zip(frame.iter()) { + *sum += value; + } + count += 1; + } + if count == 0 { + return None; + } + for value in &mut mean { + *value /= count as f64; + } + Some(mean) +} + +fn perturbation_occupancy(field: &FieldModel, frames: &[Vec]) -> Option { + let adaptive_threshold = field.empty_room_residual_energy_threshold(); + let frame = match adaptive_threshold { + Some(_) => mean_frame(frames)?, + None => frames.first()?.clone(), + }; + let perturbation = field.extract_perturbation(&[frame]).ok()?; + let count = match adaptive_threshold { + Some(threshold) => { + let empty_threshold = threshold.max(1.0); + if perturbation.total_energy > empty_threshold * 25.0 { + 3 + } else if perturbation.total_energy > empty_threshold * 12.0 { + 2 + } else if perturbation.total_energy > empty_threshold { + 1 + } else { + 0 + } + } + None => { + if perturbation.total_energy > ENERGY_THRESH_3 { + 3 + } else if perturbation.total_energy > ENERGY_THRESH_2 { + 2 + } else if perturbation.total_energy > 1.0 { + 1 + } else { + 0 + } + } + }; + Some(count) +} + +/// Provenance for a count produced by the calibrated field model. Callers +/// that attach calibration evidence must never substitute the score heuristic +/// when the calibrated path cannot evaluate the observation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CalibratedOccupancyMethod { + Eigenvalue, + PerturbationEnergy, +} + +impl CalibratedOccupancyMethod { + pub const fn wire_name(self) -> &'static str { + match self { + Self::Eigenvalue => "field_model_eigenvalue_v1", + Self::PerturbationEnergy => "field_model_perturbation_energy_v1", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CalibratedOccupancy { + pub person_count: usize, + pub method: CalibratedOccupancyMethod, +} + /// Create a FieldModelConfig for single-link mode (one ESP32 node = one link). /// This avoids the DimensionMismatch error when feeding single-frame observations. pub fn single_link_config() -> FieldModelConfig { @@ -67,7 +164,7 @@ pub fn occupancy_or_fallback( .iter() .rev() .take(OCCUPANCY_WINDOW) - .cloned() + .map(|frame| CALIB_NORMALIZER.resample_to_canonical(frame)) .collect(); if frames.is_empty() { @@ -87,26 +184,86 @@ pub fn occupancy_or_fallback( // Fallback: perturbation energy thresholds. // FieldModel expects [n_links][n_subcarriers] — we use n_links=1. - let observation = vec![frames[0].clone()]; - match field.extract_perturbation(&observation) { - Ok(perturbation) => { - if perturbation.total_energy > ENERGY_THRESH_3 { - 3 - } else if perturbation.total_energy > ENERGY_THRESH_2 { - 2 - } else if perturbation.total_energy > 1.0 { - 1 - } else { - 0 - } - } - Err(_) => score_to_person_count(smoothed_score, prev_count), - } + perturbation_occupancy(field, &frames) + .unwrap_or_else(|| score_to_person_count(smoothed_score, prev_count)) } _ => score_to_person_count(smoothed_score, prev_count), } } +/// Return true only when a restored low-authority bootstrap model has a full +/// runtime window and that model resolves the window as empty. This helper is +/// deliberately one-way: a bootstrap prior may suppress an obvious background +/// false positive, but cannot authorize presence or vital signs. +pub fn bootstrap_empty_prior_applies( + field: &FieldModel, + frame_history: &VecDeque>, + observed_at_us: u64, +) -> bool { + bootstrap_background_match(field, frame_history, observed_at_us) + .is_some_and(|result| result.matches_empty) +} + +/// Score the current canonical runtime window against the learned empty-room +/// residual references. Legacy models without scalar references preserve the +/// binary match but intentionally return no score. +pub fn bootstrap_background_match( + field: &FieldModel, + frame_history: &VecDeque>, + observed_at_us: u64, +) -> Option { + if frame_history.len() < OCCUPANCY_WINDOW + || field.check_freshness(observed_at_us) != CalibrationStatus::Fresh + { + return None; + } + let frames: Vec> = frame_history + .iter() + .rev() + .take(OCCUPANCY_WINDOW) + .map(|frame| CALIB_NORMALIZER.resample_to_canonical(frame)) + .collect(); + let result = field.empty_room_match(&frames)?; + Some(BootstrapBackgroundMatch { + matches_empty: result.matches_empty, + score: result.score, + residual_energy: result.residual_energy, + residual_energy_threshold: result.residual_energy_threshold, + window_size: result.window_size, + reference_window_count: result.reference_window_count, + }) +} + +/// Produce a fail-closed occupancy result from a fresh calibrated model. +pub fn calibrated_occupancy( + field: &FieldModel, + frame_history: &VecDeque>, + observed_at_us: u64, +) -> Option { + if field.check_freshness(observed_at_us) != CalibrationStatus::Fresh { + return None; + } + let frames: Vec> = frame_history + .iter() + .rev() + .take(OCCUPANCY_WINDOW) + .map(|frame| CALIB_NORMALIZER.resample_to_canonical(frame)) + .collect(); + if frames.is_empty() { + return None; + } + if let Ok(person_count) = field.estimate_occupancy(&frames) { + return Some(CalibratedOccupancy { + person_count: person_count.min(MAX_SINGLE_LINK_OCCUPANCY), + method: CalibratedOccupancyMethod::Eigenvalue, + }); + } + Some(CalibratedOccupancy { + person_count: perturbation_occupancy(field, &frames)?, + method: CalibratedOccupancyMethod::PerturbationEnergy, + }) +} + /// Feed the latest frame to the FieldModel during calibration collection. /// /// Acts while the model is `Uncalibrated` or `Collecting`. The first fed frame @@ -174,6 +331,33 @@ pub fn parse_node_positions(input: &str) -> Vec<[f32; 3]> { mod tests { use super::*; + fn fresh_test_model() -> FieldModel { + let config = FieldModelConfig { + n_links: 1, + n_subcarriers: 56, + n_modes: 3, + min_calibration_frames: 10, + min_calibration_duration_s: 0.0, + baseline_expiry_s: 2.0, + }; + let mut field = FieldModel::new(config).expect("field model"); + for index in 0..20 { + let phase = index as f64 * 0.1; + let observation = vec![(0..56) + .map(|subcarrier| { + 1.0 + subcarrier as f64 * 0.01 + (phase + subcarrier as f64 * 0.03).sin() * 0.01 + }) + .collect()]; + field + .feed_calibration(&observation) + .expect("calibration frame"); + } + field + .finalize_calibration(1_000_000, 7) + .expect("finalize calibration"); + field + } + #[test] fn test_parse_node_positions() { let positions = parse_node_positions("0,0,1.5;3,0,1.5;1.5,3,1.5"); @@ -263,4 +447,88 @@ mod tests { "wide frame must accumulate, not be silently dropped" ); } + + #[test] + fn calibrated_occupancy_is_fresh_model_only_and_normalizes_runtime_frames() { + let field = fresh_test_model(); + let mut history = VecDeque::new(); + history.push_back(vec![1.02; 128]); + let result = + calibrated_occupancy(&field, &history, 1_500_000).expect("fresh calibrated inference"); + assert!(result.person_count <= MAX_SINGLE_LINK_OCCUPANCY); + assert!(matches!( + result.method, + CalibratedOccupancyMethod::Eigenvalue | CalibratedOccupancyMethod::PerturbationEnergy + )); + assert_eq!(calibrated_occupancy(&field, &history, 4_000_000), None); + assert_eq!( + calibrated_occupancy(&field, &VecDeque::new(), 1_500_000), + None + ); + } + + #[test] + fn adaptive_runtime_reference_scores_empty_and_rejects_shift() { + let config = FieldModelConfig { + n_links: 1, + n_subcarriers: 56, + n_modes: 3, + min_calibration_frames: 500, + min_calibration_duration_s: 0.0, + baseline_expiry_s: 60.0, + }; + let mut field = FieldModel::new(config).expect("field model"); + let mut calibration = Vec::new(); + for frame_index in 0..600 { + let time = frame_index as f64 * 0.071; + let frame: Vec = (0..56) + .map(|subcarrier| { + let carrier = subcarrier as f64; + 18.0 + carrier * 0.08 + + (time + carrier * 0.13).sin() * 0.8 + + (time * 0.37 + carrier * 0.031).cos() * 0.35 + }) + .collect(); + field.feed_calibration(&[frame.clone()]).unwrap(); + calibration.push(frame); + } + field.finalize_calibration(1_000_000, 7).unwrap(); + + let empty_history: VecDeque> = calibration[550..600].iter().cloned().collect(); + let empty = calibrated_occupancy(&field, &empty_history, 1_500_000) + .expect("adaptive perturbation result"); + assert_eq!(empty.person_count, 0); + let background = bootstrap_background_match(&field, &empty_history, 1_500_000) + .expect("background score"); + assert!(background.matches_empty); + assert_eq!(background.reference_window_count, 12); + assert!(background + .score + .is_some_and(|score| (0.5..=1.0).contains(&score))); + + let short_history: VecDeque> = empty_history.iter().take(49).cloned().collect(); + assert!(!bootstrap_empty_prior_applies( + &field, + &short_history, + 1_500_000 + )); + + let shifted_history: VecDeque> = calibration[550..600] + .iter() + .map(|frame| { + frame + .iter() + .enumerate() + .map(|(index, value)| value + if index % 3 == 0 { 8.0 } else { -5.0 }) + .collect() + }) + .collect(); + let shifted = calibrated_occupancy(&field, &shifted_history, 1_500_000) + .expect("shifted perturbation result"); + assert!(shifted.person_count >= 1); + let shifted_background = bootstrap_background_match(&field, &shifted_history, 1_500_000) + .expect("shifted background result"); + assert!(!shifted_background.matches_empty); + assert_eq!(shifted_background.score, Some(0.0)); + } } diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 111c91cd3..0c0628301 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -40,7 +40,9 @@ use wifi_densepose_sensing_server::{ trainer, }; use wifi_densepose_sensing_server::bootstrap_baseline::{ - self, BootstrapBaselineMetadata, BootstrapValidationSample, BOOTSTRAP_VALIDATION_SAMPLES, + self, BootstrapBaselineMetadata, BootstrapValidationSample, + BOOTSTRAP_VALIDATION_MIN_SPACING_MS, BOOTSTRAP_VALIDATION_SAMPLES, + BOOTSTRAP_VALIDATION_SAMPLE_TIMEOUT_MS, }; // ADR-295 / ADR-297: canonical provenance state + per-node/room inference. use wifi_densepose_sensing_server::inference::{fuse_room, NodeInference, RoomInference}; @@ -1715,12 +1717,27 @@ impl AppStateInner { fn person_count(&self) -> usize { match self.field_model.as_ref() { Some(fm) => { - // Prefer global frame_history (populated by wifi/simulate paths). - // Fall back to freshest per-node history (populated by ESP32 paths). - let history = if !self.frame_history.is_empty() { + // A single-link model may score only the source node it was + // calibrated against. Applying one node's baseline to a + // different radio creates deterministic false occupancy from + // hardware-specific amplitude offsets. + let bound_source_node_id = (self.calibration_source_node_ids.len() == 1) + .then(|| self.calibration_source_node_ids.iter().next().copied()) + .flatten() + .or_else(|| { + self.bootstrap_baseline.as_ref().and_then(|metadata| { + (metadata.source_node_ids.len() == 1) + .then_some(metadata.source_node_ids[0]) + }) + }); + let history = if let Some(node_id) = bound_source_node_id { + self.node_states + .get(&node_id) + .map(|state| &state.frame_history) + .unwrap_or(&self.frame_history) + } else if !self.frame_history.is_empty() { &self.frame_history } else { - // Find the node with the most recent frame self.node_states .values() .filter(|ns| !ns.frame_history.is_empty()) @@ -1739,6 +1756,36 @@ impl AppStateInner { } } + /// A restored startup image has negative-only authority. It may suppress + /// an empty-room false positive after one complete runtime window, but it + /// cannot establish positive presence or publish vital signs. + fn bootstrap_empty_prior_applies(&self, observed_at_unix_ms: u64) -> bool { + self.bootstrap_background_match(observed_at_unix_ms) + .is_some_and(|result| result.matches_empty) + } + + /// Return current background conformance for the exact source node bound + /// into the restored image. This remains diagnostic and negative-only. + fn bootstrap_background_match( + &self, + observed_at_unix_ms: u64, + ) -> Option { + if !self.bootstrap_baseline_active { + return None; + } + let field_model = self.field_model.as_ref()?; + let metadata = self.bootstrap_baseline.as_ref()?; + let [source_node_id] = metadata.source_node_ids.as_slice() else { + return None; + }; + let history = &self.node_states.get(source_node_id)?.frame_history; + field_bridge::bootstrap_background_match( + field_model, + history, + observed_at_unix_ms.saturating_mul(1_000), + ) + } + fn effective_source(&self) -> String { if self.source == "esp32" { if let Some(last) = self.last_esp32_frame { @@ -6041,6 +6088,50 @@ fn vitals_for_publication( }) } +fn edge_vitals_message_for_publication( + raw: &Esp32VitalsPacket, + published_vitals: Option<&VitalSigns>, + bootstrap_empty: bool, + explicit_calibration_fresh: bool, + person_count: usize, +) -> serde_json::Value { + let numeric_vitals_authorized = published_vitals.is_some(); + let abstention_reason = if numeric_vitals_authorized { + None + } else if bootstrap_empty { + Some("bootstrap_empty_background") + } else if !explicit_calibration_fresh { + Some("explicit_calibration_required") + } else if person_count != 1 { + Some("exactly_one_occupant_required") + } else { + Some("vital_quality_gate_failed") + }; + let effective_presence = raw.presence && !bootstrap_empty; + let effective_motion = raw.motion && !bootstrap_empty; + let effective_person_count = if bootstrap_empty { 0 } else { person_count }; + + serde_json::json!({ + "type": "edge_vitals", + "node_id": raw.node_id, + "presence": effective_presence, + "fall_detected": raw.fall_detected && !bootstrap_empty, + "motion": effective_motion, + "breathing_rate_bpm": published_vitals + .and_then(|vitals| vitals.breathing_rate_bpm), + "heartrate_bpm": published_vitals + .and_then(|vitals| vitals.heart_rate_bpm), + "n_persons": effective_person_count, + "person_count_valid": raw.person_count_valid && !bootstrap_empty, + "motion_energy": if bootstrap_empty { 0.0 } else { raw.motion_energy }, + "presence_score": if bootstrap_empty { 0.0 } else { raw.presence_score }, + "rssi": raw.rssi, + "calibrated_evidence_authorized": explicit_calibration_fresh, + "numeric_vitals_authorized": numeric_vitals_authorized, + "abstention_reason": abstention_reason, + }) +} + fn opaque_calibration_model_id() -> String { let mut bytes = [0_u8; 16]; OsRng.fill_bytes(&mut bytes); @@ -6048,10 +6139,26 @@ fn opaque_calibration_model_id() -> String { format!("cal-model-{suffix}") } +fn calibration_source_accepts( + source_node_ids: &std::collections::BTreeSet, + node_id: u8, +) -> bool { + source_node_ids.is_empty() || source_node_ids.contains(&node_id) +} + #[cfg(test)] mod bootstrap_vital_publication_tests { use super::*; + #[test] + fn single_link_calibration_binds_to_first_source() { + let mut sources = std::collections::BTreeSet::new(); + assert!(calibration_source_accepts(&sources, 5)); + sources.insert(5); + assert!(calibration_source_accepts(&sources, 5)); + assert!(!calibration_source_accepts(&sources, 3)); + } + fn strong_candidates() -> VitalSigns { VitalSigns { breathing_rate_bpm: Some(15.0), @@ -6083,6 +6190,61 @@ mod bootstrap_vital_publication_tests { candidates.signal_quality = 0.2; assert!(vitals_for_publication(&candidates, true, 1).is_none()); } + + fn raw_edge_vitals() -> Esp32VitalsPacket { + Esp32VitalsPacket { + node_id: 5, + presence: true, + fall_detected: true, + motion: true, + breathing_rate_bpm: 15.0, + heartrate_bpm: 72.0, + rssi: -42, + n_persons: 1, + person_count_valid: true, + motion_energy: 0.8, + presence_score: 0.9, + timestamp_ms: 42, + } + } + + #[test] + fn bootstrap_empty_background_suppresses_legacy_edge_vitals() { + let message = edge_vitals_message_for_publication( + &raw_edge_vitals(), + None, + true, + false, + 0, + ); + assert_eq!(message["presence"], false); + assert_eq!(message["motion"], false); + assert_eq!(message["fall_detected"], false); + assert_eq!(message["n_persons"], 0); + assert!(message["breathing_rate_bpm"].is_null()); + assert!(message["heartrate_bpm"].is_null()); + assert_eq!(message["numeric_vitals_authorized"], false); + assert_eq!(message["abstention_reason"], "bootstrap_empty_background"); + } + + #[test] + fn explicit_single_occupant_may_publish_legacy_edge_vitals() { + let published = vitals_for_publication(&strong_candidates(), true, 1).unwrap(); + let message = edge_vitals_message_for_publication( + &raw_edge_vitals(), + Some(&published), + false, + true, + 1, + ); + assert_eq!(message["presence"], true); + assert_eq!(message["fall_detected"], true); + assert_eq!(message["n_persons"], 1); + assert_eq!(message["breathing_rate_bpm"], 15.0); + assert_eq!(message["heartrate_bpm"], 72.0); + assert_eq!(message["numeric_vitals_authorized"], true); + assert!(message["abstention_reason"].is_null()); + } } // ── Field model calibration endpoints (eigenvalue person counting) ────────── @@ -6194,6 +6356,17 @@ async fn calibration_stop(State(state): State) -> Json) -> Json { let s = state.read().await; + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; + let bootstrap_background_match = s.bootstrap_background_match(observed_at_unix_ms); + let runtime_reference = s.field_model.as_ref().and_then(|model| { + model.modes().map(|modes| serde_json::json!({ + "window_size": modes.baseline_runtime_window_size, + "significant_eigenvalue_p95": modes.baseline_runtime_eigenvalue_count, + "residual_energy_threshold": modes.empty_room_residual_energy_threshold, + "residual_reference_window_count": modes.empty_room_residual_energy_reference.len(), + "raw_calibration_frames_persisted": false, + })) + }); let (active, status, frame_count, min_frames, elapsed_s, frames_per_second, min_duration_s) = s.field_model.as_ref().map_or( (false, "none".to_string(), 0, 0, 0.0, 0.0, 0.0), @@ -6219,6 +6392,7 @@ async fn calibration_status(State(state): State) -> Json) -> Json = s.calibration_source_node_ids.iter().copied().collect(); - if source_node_ids.is_empty() || source_node_ids.len() > 16 { + if source_node_ids.len() != 1 { return Json(serde_json::json!({ "success": false, "error_code": "calibration_source_nodes_missing", - "error": "The active calibration must contain between 1 and 16 contributing ESP32 nodes.", + "error": "The single-link startup baseline must contain exactly one contributing ESP32 node.", })); } let Some(field_model) = s.field_model.as_ref() else { @@ -6825,6 +7012,7 @@ async fn mesh_endpoint(State(state): State) -> Json) -> Json { let s = state.read().await; let now = std::time::Instant::now(); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; let nodes: Vec = s .node_states .iter() @@ -6836,13 +7024,24 @@ async fn nodes_endpoint(State(state): State) -> Json 5000; let status = if stale { "stale" } else { "active" }; let rssi = ns.rssi_history.back().copied().unwrap_or(-90.0); + let bootstrap_empty = s.bootstrap_baseline_active + && s.bootstrap_baseline.as_ref().is_some_and(|metadata| { + metadata.source_node_ids.as_slice() == [id] + }) + && s.field_model.as_ref().is_some_and(|field_model| { + field_bridge::bootstrap_empty_prior_applies( + field_model, + &ns.frame_history, + observed_at_unix_ms.saturating_mul(1_000), + ) + }); serde_json::json!({ "node_id": id, "status": status, "last_seen_ms": elapsed_ms, "rssi_dbm": rssi, - "motion_level": &ns.current_motion_level, - "person_count": ns.prev_person_count, + "motion_level": if bootstrap_empty { "absent" } else { &ns.current_motion_level }, + "person_count": if bootstrap_empty { 0 } else { ns.prev_person_count }, "person_count_valid": ns.edge_vitals .as_ref() .map(|vitals| vitals.person_count_valid), @@ -7088,24 +7287,6 @@ async fn udp_receiver_task( vitals.presence ); let mut s = state.write().await; - // Broadcast vitals via WebSocket. - if let Ok(json) = serde_json::to_string(&serde_json::json!({ - "type": "edge_vitals", - "node_id": vitals.node_id, - "presence": vitals.presence, - "fall_detected": vitals.fall_detected, - "motion": vitals.motion, - "breathing_rate_bpm": vitals.breathing_rate_bpm, - "heartrate_bpm": vitals.heartrate_bpm, - "n_persons": vitals.n_persons, - "person_count_valid": vitals.person_count_valid, - "motion_energy": vitals.motion_energy, - "presence_score": vitals.presence_score, - "rssi": vitals.rssi, - })) { - let _ = s.tx.send(json); - } - // Issue #323: Also emit a sensing_update so the UI renders // detections for ESP32 nodes running the edge DSP pipeline // (Tier 2+). Without this, vitals arrive but the UI shows @@ -7153,9 +7334,18 @@ async fn udp_receiver_task( 0.05 }; - // Aggregate person count: gate on presence first (matching WiFi path). + let observed_at_unix_ms = + chrono::Utc::now().timestamp_millis().max(0) as u64; + let bootstrap_empty = + s.bootstrap_empty_prior_applies(observed_at_unix_ms); + + // A startup prior has negative-only authority. Once a full + // runtime window matches the stored background, raw edge + // presence cannot force the count back to one. let now = std::time::Instant::now(); - let total_persons = if vitals.presence { + let total_persons = if bootstrap_empty { + 0 + } else if vitals.presence { let dedup = s.dedup_factor; let (fused, fallback_count) = multistatic_bridge::fuse_or_fallback( &s.multistatic_fuser, @@ -7208,10 +7398,16 @@ async fn udp_receiver_task( .get(&node_id) .map(|ns| ns.frame_history.clone()) { - let accepted = if let Some(ref mut fm) = s.field_model { - let before = fm.calibration_frame_count(); - field_bridge::maybe_feed_calibration(fm, &frame_history); - fm.calibration_frame_count() > before + let source_allowed = + calibration_source_accepts(&s.calibration_source_node_ids, node_id); + let accepted = if source_allowed { + if let Some(ref mut fm) = s.field_model { + let before = fm.calibration_frame_count(); + field_bridge::maybe_feed_calibration(fm, &frame_history); + fm.calibration_frame_count() > before + } else { + false + } } else { false }; @@ -7278,7 +7474,15 @@ async fn udp_receiver_task( // node's own reading no longer overwrites the room's. The // old ad-hoc "boost confidence by node count" is replaced by // `room_inference`'s freshness-weighted multi-node confidence. - let classification = debounce_room_classification(&mut s, &room_inference); + let classification = if bootstrap_empty { + ClassificationInfo { + motion_level: "absent".to_string(), + presence: false, + confidence: 0.0, + } + } else { + debounce_room_classification(&mut s, &room_inference) + }; let signal_field = generate_signal_field( fused_features.mean_rssi, @@ -7306,6 +7510,21 @@ async fn udp_receiver_task( total_persons, ); + // Keep the legacy edge_vitals stream aligned with the + // governed sensing envelope. Raw numeric candidates are + // never published before the explicit calibration and + // single-occupant gates have passed. + let edge_vitals_message = edge_vitals_message_for_publication( + &vitals, + published_vitals.as_ref(), + bootstrap_empty, + explicit_calibration_fresh, + total_persons, + ); + if let Ok(json) = serde_json::to_string(&edge_vitals_message) { + let _ = s.tx.send(json); + } + let mut update = SensingUpdate { msg_type: "sensing_update".to_string(), timestamp: chrono::Utc::now().timestamp_millis() as f64 / 1000.0, @@ -7645,9 +7864,17 @@ async fn udp_receiver_task( 0.05 }; - // Aggregate person count: gate on presence first (matching WiFi path). + let observed_at_unix_ms = + chrono::Utc::now().timestamp_millis().max(0) as u64; + let bootstrap_empty = + s.bootstrap_empty_prior_applies(observed_at_unix_ms); + + // A restored prior can suppress a background-only raw + // classification. It cannot authorize positive presence. let now = std::time::Instant::now(); - let total_persons = if classification.presence { + let total_persons = if bootstrap_empty { + 0 + } else if classification.presence { let dedup = s.dedup_factor; let (fused, fallback_count) = multistatic_bridge::fuse_or_fallback( &s.multistatic_fuser, @@ -7700,10 +7927,16 @@ async fn udp_receiver_task( .get(&node_id) .map(|ns| ns.frame_history.clone()) { - let accepted = if let Some(ref mut fm) = s.field_model { - let before = fm.calibration_frame_count(); - field_bridge::maybe_feed_calibration(fm, &frame_history); - fm.calibration_frame_count() > before + let source_allowed = + calibration_source_accepts(&s.calibration_source_node_ids, node_id); + let accepted = if source_allowed { + if let Some(ref mut fm) = s.field_model { + let before = fm.calibration_frame_count(); + field_bridge::maybe_feed_calibration(fm, &frame_history); + fm.calibration_frame_count() > before + } else { + false + } } else { false }; @@ -7763,7 +7996,15 @@ async fn udp_receiver_task( active_nodes.iter().filter_map(|ni| ni.node_inference.as_ref()), NODE_STALE_AFTER_MS, ); - let room_classification = debounce_room_classification(&mut s, &room_inference); + let room_classification = if bootstrap_empty { + ClassificationInfo { + motion_level: "absent".to_string(), + presence: false, + confidence: 0.0, + } + } else { + debounce_room_classification(&mut s, &room_inference) + }; let explicit_calibration_fresh = !s.bootstrap_baseline_active && s.field_model.as_ref().is_some_and(|model| { matches!(model.status(), CalibrationStatus::Fresh) diff --git a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs index 14c9e65d9..56fd09011 100644 --- a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs +++ b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs @@ -7,8 +7,8 @@ //! (the residual). //! //! # Algorithm -//! 1. Collect CSI during empty-room calibration (>=10 min wall-clock; the -//! frame target is this duration at the assumed 20 Hz single-node rate) +//! 1. Collect CSI during empty-room calibration (>=10 min wall-clock and at +//! least twenty complete runtime-sized background windows) //! 2. Compute per-link baseline mean (Welford online accumulator) //! 3. Decompose covariance via SVD to extract environmental modes //! 4. At runtime: observation - baseline, project out top-K modes, keep residual @@ -24,6 +24,7 @@ use ndarray_linalg::Eigh; #[cfg(feature = "eigenvalue")] use ndarray_linalg::UPLO; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; // --------------------------------------------------------------------------- // Calibration window constants @@ -33,21 +34,30 @@ use serde::{Deserialize, Serialize}; /// /// The Welford statistics exist to absorb slow environmental variation — /// HVAC cycles, thermal drift — so the calibration gate is expressed in -/// wall-clock time. The historical frame target of 12,000 is this duration at -/// the assumed single-node rate. Every accepted node packet advances the same -/// field model, so N nodes reach any frame target N times faster; frames alone -/// therefore cannot gate calibration (#1756). +/// wall-clock time. Every accepted node packet advances the same field model, +/// so N nodes reach any frame target N times faster; frames alone therefore +/// cannot gate calibration (#1756). pub const CALIBRATION_DURATION_S: f64 = 600.0; -/// Assumed per-node CSI rate (Hz) used to relate the frame target to -/// [`CALIBRATION_DURATION_S`] ("10 min at 20 Hz"). -pub const ASSUMED_CALIBRATION_RATE_HZ: f64 = 20.0; - /// A recent window whose mean remains within this residual energy of the /// empty room manifold is treated as background before eigenvalue counting. -#[cfg(feature = "eigenvalue")] const EMPTY_ROOM_RESIDUAL_ENERGY_MAX: f64 = 1.0; +/// Runtime occupancy is evaluated over fifty frames. Keep ten raw windows +/// from the end of calibration for covariance rank learning, plus bounded +/// privacy reduced window means spanning the full capture for residual +/// conformance scoring. +const RUNTIME_OCCUPANCY_WINDOW: usize = 50; +const CALIBRATION_REFERENCE_FRAMES: usize = RUNTIME_OCCUPANCY_WINDOW * 10; +/// Minimum independent runtime sized means required to learn an empirical +/// empty room residual distribution. Wall clock duration remains the primary +/// coverage gate, so slow hardware is not forced to imitate a nominal rate. +pub const MIN_BACKGROUND_REFERENCE_WINDOWS: usize = 20; +pub const MIN_CALIBRATION_FRAMES: usize = + RUNTIME_OCCUPANCY_WINDOW * MIN_BACKGROUND_REFERENCE_WINDOWS; +const CALIBRATION_BACKGROUND_WINDOWS_MAX: usize = 512; +const EMPTY_ROOM_RESIDUAL_MARGIN: f64 = 1.5; + // --------------------------------------------------------------------------- // Error types // --------------------------------------------------------------------------- @@ -276,12 +286,11 @@ pub struct FieldModelConfig { pub n_subcarriers: usize, /// Number of environmental modes to retain (K). Max 5. pub n_modes: usize, - /// Minimum calibration frames before baseline is valid. Derived from the - /// intended wall-clock window at the assumed single-node rate - /// (`CALIBRATION_DURATION_S * ASSUMED_CALIBRATION_RATE_HZ` = 12,000). - /// A frame count alone is not sufficient: N nodes streaming in aggregate - /// reach it in 1/N of the window, so `min_calibration_duration_s` also - /// gates finalization (#1756). + /// Minimum calibration frames before baseline is valid. This is the + /// runtime window size times the minimum number of independent background + /// references. A frame count alone is not sufficient: N nodes streaming + /// in aggregate reach it in 1/N of the window, so + /// `min_calibration_duration_s` also gates finalization (#1756). pub min_calibration_frames: usize, /// Minimum wall-clock duration of the calibration window in seconds. /// Slow environmental variation (HVAC cycles, thermal drift) can only be @@ -298,8 +307,7 @@ impl Default for FieldModelConfig { n_links: 6, n_subcarriers: 56, n_modes: 3, - min_calibration_frames: (CALIBRATION_DURATION_S * ASSUMED_CALIBRATION_RATE_HZ) - as usize, + min_calibration_frames: MIN_CALIBRATION_FRAMES, min_calibration_duration_s: CALIBRATION_DURATION_S, baseline_expiry_s: 86_400.0, } @@ -336,6 +344,37 @@ pub struct FieldNormalMode { /// per-window sample size. Defaults to 0.0 in the diagonal-fallback path. /// Issue #942. pub baseline_noise_var: f64, + /// Empty room significant eigenvalue count learned from runtime sized + /// windows. The percentile is stored instead of raw CSI. `None` identifies + /// snapshots created before runtime window calibration was introduced. + #[serde(default)] + pub baseline_runtime_eigenvalue_count: Option, + /// Number of frames used for the runtime window reference above. + #[serde(default)] + pub baseline_runtime_window_size: Option, + /// Hardware adaptive upper bound for the residual energy of an empty room + /// window mean. Only this aggregate boundary is persisted. + #[serde(default)] + pub empty_room_residual_energy_threshold: Option, + /// Sorted residual energies of privacy reduced runtime window means sampled + /// across the full calibration. Raw CSI is never persisted. These scalar + /// references support an empirical background match score without turning + /// an empty room model into positive person evidence. + #[serde(default)] + pub empty_room_residual_energy_reference: Vec, +} + +/// A bounded comparison between one runtime window and the learned empty room +/// residual distribution. `score` is an empirical conformance score, not a +/// probability that a person is absent. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct EmptyRoomMatch { + pub matches_empty: bool, + pub score: Option, + pub residual_energy: f64, + pub residual_energy_threshold: f64, + pub window_size: usize, + pub reference_window_count: usize, } /// Body perturbation extracted from a CSI observation. @@ -405,6 +444,16 @@ pub struct FieldModel { /// Monotonic timestamp of the first accepted calibration frame of the /// current session; `None` until collection starts (#1756). calibration_started: Option, + /// Bounded, memory only tail used to learn inference sized empty room + /// covariance statistics. It is never included in a snapshot. + calibration_reference_tail: VecDeque>>, + /// Per link sum for the current privacy reduced runtime sized window. + calibration_window_sum: Vec>, + /// Number of accepted observations in `calibration_window_sum`. + calibration_window_count: usize, + /// Runtime sized per link means spanning the full calibration. This is + /// bounded, memory only, and reduced to scalar residuals at finalization. + calibration_window_means: VecDeque>>, } /// Diagonal variance fallback for when full covariance SVD is unavailable. @@ -460,6 +509,133 @@ fn diagonal_fallback( (mode_energies, environmental_modes, baseline_count) } +#[cfg(feature = "eigenvalue")] +fn significant_eigenvalue_count( + frames: &[Vec], + n_subcarriers: usize, + baseline_noise_var: f64, +) -> Option { + if frames.len() < 10 { + return None; + } + + let mut mean = vec![0.0_f64; n_subcarriers]; + let mut count = 0_usize; + for frame in frames { + if frame.len() >= n_subcarriers { + for index in 0..n_subcarriers { + mean[index] += frame[index]; + } + count += 1; + } + } + if count < 2 { + return None; + } + for value in &mut mean { + *value /= count as f64; + } + + let mut covariance = Array2::::zeros((n_subcarriers, n_subcarriers)); + for frame in frames { + if frame.len() >= n_subcarriers { + for row in 0..n_subcarriers { + let centered_row = frame[row] - mean[row]; + for column in row..n_subcarriers { + let value = centered_row * (frame[column] - mean[column]); + covariance[[row, column]] += value; + if row != column { + covariance[[column, row]] += value; + } + } + } + } + } + covariance *= 1.0 / (count as f64 - 1.0); + + let (eigenvalues, _) = covariance.eigh(UPLO::Upper).ok()?; + let mut positive: Vec = eigenvalues + .iter() + .copied() + .filter(|value| *value > 1e-10) + .collect(); + positive.sort_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal)); + let local_noise_var = if positive.len() >= 4 { + let half = positive.len() / 2; + positive[..half].iter().sum::() / half as f64 + } else { + positive.first().copied()? + }; + let noise_var = if baseline_noise_var > 0.0 { + local_noise_var.max(baseline_noise_var) + } else { + local_noise_var + }; + let ratio = n_subcarriers as f64 / count as f64; + let threshold = noise_var * (1.0 + ratio.sqrt()).powi(2); + Some( + eigenvalues + .iter() + .filter(|&&value| value > threshold) + .count(), + ) +} + +fn residual_energy_for_link( + modes: &FieldNormalMode, + link_index: usize, + observation: &[f64], +) -> Option { + let baseline = modes.baseline.get(link_index)?; + if observation.len() != baseline.len() { + return None; + } + let mut residual: Vec = observation + .iter() + .zip(baseline.iter()) + .map(|(value, reference)| value - reference) + .collect(); + for mode in &modes.environmental_modes { + let projection: f64 = residual + .iter() + .zip(mode.iter()) + .map(|(value, basis)| value * basis) + .sum(); + for (value, basis) in residual.iter_mut().zip(mode.iter()) { + *value -= projection * basis; + } + } + Some( + residual + .iter() + .map(|value| value * value) + .sum::() + .sqrt(), + ) +} + +fn percentile_95(mut values: Vec) -> Option { + values.retain(|value| value.is_finite()); + values.sort_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal)); + let index = values + .len() + .saturating_mul(95) + .div_ceil(100) + .saturating_sub(1); + values.get(index).copied() +} + +#[cfg(feature = "eigenvalue")] +fn percentile_95_usize(mut values: Vec) -> Option { + values.sort_unstable(); + let index = values + .len() + .saturating_mul(95) + .div_ceil(100) + .saturating_sub(1); + values.get(index).copied() +} + impl FieldModel { /// Create a new field model for the given configuration. pub fn new(config: FieldModelConfig) -> Result { @@ -480,6 +656,7 @@ impl FieldModel { let link_stats = (0..config.n_links) .map(|_| LinkBaselineStats::new(config.n_subcarriers)) .collect(); + let calibration_window_sum = vec![vec![0.0_f64; config.n_subcarriers]; config.n_links]; Ok(Self { config, @@ -490,6 +667,10 @@ impl FieldModel { covariance_sum: None, covariance_count: 0, calibration_started: None, + calibration_reference_tail: VecDeque::with_capacity(CALIBRATION_REFERENCE_FRAMES), + calibration_window_sum, + calibration_window_count: 0, + calibration_window_means: VecDeque::with_capacity(CALIBRATION_BACKGROUND_WINDOWS_MAX), }) } @@ -503,6 +684,14 @@ impl FieldModel { self.modes.as_ref() } + /// Hardware adaptive empty room boundary learned at finalization. Returns + /// `None` for legacy snapshots that predate runtime window calibration. + pub fn empty_room_residual_energy_threshold(&self) -> Option { + self.modes + .as_ref() + .and_then(|modes| modes.empty_room_residual_energy_threshold) + } + /// Export aggregate model state suitable for local restart persistence. /// Raw calibration observations and Welford accumulators are not exported. pub fn export_snapshot(&self) -> Result { @@ -561,7 +750,26 @@ impl FieldModel { && (0.0..=10.0).contains(&modes.variance_explained) && modes.baseline_noise_var.is_finite() && modes.baseline_noise_var >= 0.0 - && modes.baseline_eigenvalue_count <= config.n_subcarriers; + && modes.baseline_eigenvalue_count <= config.n_subcarriers + && modes + .baseline_runtime_eigenvalue_count + .is_none_or(|count| count <= config.n_subcarriers) + && modes + .baseline_runtime_window_size + .is_none_or(|size| (10..=CALIBRATION_REFERENCE_FRAMES).contains(&size)) + && modes + .empty_room_residual_energy_threshold + .is_none_or(|threshold| threshold.is_finite() && threshold >= 0.0) + && modes.empty_room_residual_energy_reference.len() + <= CALIBRATION_BACKGROUND_WINDOWS_MAX + && modes + .empty_room_residual_energy_reference + .iter() + .all(|value| value.is_finite() && *value >= 0.0) + && modes + .empty_room_residual_energy_reference + .windows(2) + .all(|window| window[0] <= window[1]); if !baseline_shape_valid || !mode_shape_valid || !values_valid { return Err(FieldModelError::InvalidConfig( "snapshot modes are malformed or non finite".into(), @@ -678,6 +886,39 @@ impl FieldModel { // Count once per frame (not per link) for correct MP ratio self.covariance_count += 1; + if self.calibration_reference_tail.len() == CALIBRATION_REFERENCE_FRAMES { + self.calibration_reference_tail.pop_front(); + } + self.calibration_reference_tail + .push_back(observations.to_vec()); + + for (link_sum, observation) in self + .calibration_window_sum + .iter_mut() + .zip(observations.iter()) + { + for (sum, value) in link_sum.iter_mut().zip(observation.iter()) { + *sum += value; + } + } + self.calibration_window_count += 1; + if self.calibration_window_count == RUNTIME_OCCUPANCY_WINDOW { + let divisor = self.calibration_window_count as f64; + let means: Vec> = self + .calibration_window_sum + .iter() + .map(|link_sum| link_sum.iter().map(|value| value / divisor).collect()) + .collect(); + if self.calibration_window_means.len() == CALIBRATION_BACKGROUND_WINDOWS_MAX { + self.calibration_window_means.pop_front(); + } + self.calibration_window_means.push_back(means); + for link_sum in &mut self.calibration_window_sum { + link_sum.fill(0.0); + } + self.calibration_window_count = 0; + } + Ok(()) } @@ -812,8 +1053,7 @@ impl FieldModel { } Err(_) => { // Fallback to diagonal approximation on SVD failure - let (e, m, b) = - diagonal_fallback(&self.link_stats, n_sc, n_modes); + let (e, m, b) = diagonal_fallback(&self.link_stats, n_sc, n_modes); (e, m, b, 0.0_f64) } } @@ -865,7 +1105,7 @@ impl FieldModel { 0.0 }; - let field_mode = FieldNormalMode { + let mut field_mode = FieldNormalMode { baseline, environmental_modes, mode_energies, @@ -874,15 +1114,142 @@ impl FieldModel { geometry_hash, baseline_eigenvalue_count: baseline_eig_count, baseline_noise_var, + baseline_runtime_eigenvalue_count: None, + baseline_runtime_window_size: None, + empty_room_residual_energy_threshold: None, + empty_room_residual_energy_reference: Vec::new(), }; + // The full calibration covariance and a fifty frame runtime covariance + // have different Marcenko Pastur aspect ratios. This first version is + // deliberately single link: combining link specific residual scales + // requires a separate multistatic calibration model. + if self.config.n_links == 1 { + let reference_frames: Vec> = self + .calibration_reference_tail + .iter() + .filter_map(|observation| observation.first().cloned()) + .collect(); + let complete_windows: Vec<&[Vec]> = reference_frames + .chunks(RUNTIME_OCCUPANCY_WINDOW) + .filter(|window| window.len() == RUNTIME_OCCUPANCY_WINDOW) + .collect(); + + #[cfg(feature = "eigenvalue")] + { + let counts: Vec = complete_windows + .iter() + .filter_map(|window| { + significant_eigenvalue_count(window, n_sc, baseline_noise_var) + }) + .collect(); + field_mode.baseline_runtime_eigenvalue_count = percentile_95_usize(counts); + } + + let mut residual_energies: Vec = self + .calibration_window_means + .iter() + .filter_map(|means| means.first()) + .filter_map(|mean| residual_energy_for_link(&field_mode, 0, mean)) + .filter(|value| value.is_finite() && *value >= 0.0) + .collect(); + if residual_energies.is_empty() { + residual_energies = complete_windows + .iter() + .filter_map(|window| { + let mut mean = vec![0.0_f64; n_sc]; + for frame in *window { + for (sum, value) in mean.iter_mut().zip(frame.iter()) { + *sum += value; + } + } + for value in &mut mean { + *value /= window.len() as f64; + } + residual_energy_for_link(&field_mode, 0, &mean) + }) + .collect(); + } + residual_energies.sort_by(|left, right| { + left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) + }); + field_mode.empty_room_residual_energy_threshold = + percentile_95(residual_energies.clone()).map(|value| { + (value * EMPTY_ROOM_RESIDUAL_MARGIN).max(EMPTY_ROOM_RESIDUAL_ENERGY_MAX) + }); + field_mode.empty_room_residual_energy_reference = residual_energies; + if field_mode.baseline_runtime_eigenvalue_count.is_some() + || field_mode.empty_room_residual_energy_threshold.is_some() + { + field_mode.baseline_runtime_window_size = Some(RUNTIME_OCCUPANCY_WINDOW); + } + } + self.modes = Some(field_mode); self.status = CalibrationStatus::Fresh; self.last_calibration_us = timestamp_us; + self.calibration_reference_tail.clear(); + self.calibration_window_means.clear(); + for link_sum in &mut self.calibration_window_sum { + link_sum.fill(0.0); + } + self.calibration_window_count = 0; Ok(self.modes.as_ref().unwrap()) } + /// Compare a runtime sized single link window with the learned empty room + /// distribution. The score is empirical conformance while the learned + /// boundary holds. Crossing the boundary returns zero. Legacy snapshots + /// without residual references retain binary suppression but no score. + pub fn empty_room_match(&self, recent_frames: &[Vec]) -> Option { + let modes = self.modes.as_ref()?; + if self.config.n_links != 1 { + return None; + } + let window_size = modes.baseline_runtime_window_size?; + if recent_frames.len() < window_size { + return None; + } + let mut mean = vec![0.0_f64; self.config.n_subcarriers]; + for frame in recent_frames.iter().take(window_size) { + if frame.len() < self.config.n_subcarriers { + return None; + } + for (sum, value) in mean.iter_mut().zip(frame.iter()) { + *sum += value; + } + } + for value in &mut mean { + *value /= window_size as f64; + } + let residual_energy = residual_energy_for_link(modes, 0, &mean)?; + let residual_energy_threshold = modes + .empty_room_residual_energy_threshold? + .max(EMPTY_ROOM_RESIDUAL_ENERGY_MAX); + let matches_empty = residual_energy <= residual_energy_threshold; + let reference_window_count = modes.empty_room_residual_energy_reference.len(); + let score = if !matches_empty { + Some(0.0) + } else if reference_window_count == 0 { + None + } else { + let at_or_below = modes + .empty_room_residual_energy_reference + .partition_point(|value| *value <= residual_energy); + let empirical_quantile = at_or_below as f64 / reference_window_count as f64; + Some((1.0 - empirical_quantile * 0.5).clamp(0.5, 1.0)) + }; + Some(EmptyRoomMatch { + matches_empty, + score, + residual_energy, + residual_energy_threshold, + window_size, + reference_window_count, + }) + } + /// Extract body perturbation from a runtime observation. /// /// Subtracts baseline, projects out environmental modes, returns residual. @@ -993,14 +1360,19 @@ impl FieldModel { *m /= count as f64; } - // Rank alone is not person evidence. A continuation window can expose - // more significant eigenvalues while its mean remains on the learned - // empty room manifold. Fail toward empty only inside the same - // conservative residual energy boundary used by the server fallback. - if self + // Rank alone is not person evidence. A short continuation window can + // expose more significant eigenvalues while its mean remains on the + // learned empty room manifold. Use the hardware adaptive residual + // boundary when available. + let empty_room_residual_energy_max = modes + .empty_room_residual_energy_threshold + .unwrap_or(EMPTY_ROOM_RESIDUAL_ENERGY_MAX) + .max(EMPTY_ROOM_RESIDUAL_ENERGY_MAX); + let mean_residual_energy = self .extract_perturbation(&[mean.clone()]) - .is_ok_and(|perturbation| perturbation.total_energy <= EMPTY_ROOM_RESIDUAL_ENERGY_MAX) - { + .ok() + .map(|perturbation| perturbation.total_energy); + if mean_residual_energy.is_some_and(|energy| energy <= empty_room_residual_energy_max) { return Ok(0); } @@ -1064,7 +1436,23 @@ impl FieldModel { let mp_threshold = noise_var * (1.0 + ratio.sqrt()).powi(2); let significant = eigenvalues.iter().filter(|&&ev| ev > mp_threshold).count(); - let occupancy = significant.saturating_sub(modes.baseline_eigenvalue_count); + let reference_count = if modes.baseline_runtime_window_size == Some(count) { + modes + .baseline_runtime_eigenvalue_count + .unwrap_or(modes.baseline_eigenvalue_count) + } else { + modes.baseline_eigenvalue_count + }; + let rank_occupancy = significant.saturating_sub(reference_count); + // A stable person can shift the window mean without increasing its + // covariance rank. Preserve at least one occupant after the learned + // empty room residual boundary is exceeded. + let occupancy = + if mean_residual_energy.is_some_and(|energy| energy > empty_room_residual_energy_max) { + rank_occupancy.max(1) + } else { + rank_occupancy + }; Ok(occupancy.min(10)) // Cap at 10 persons } @@ -1103,6 +1491,12 @@ impl FieldModel { self.covariance_sum = None; self.covariance_count = 0; self.calibration_started = None; + self.calibration_reference_tail.clear(); + self.calibration_window_count = 0; + for link_sum in &mut self.calibration_window_sum { + link_sum.fill(0.0); + } + self.calibration_window_means.clear(); } } @@ -1623,6 +2017,131 @@ mod tests { assert_eq!(occupancy, 0, "Noise-only frames should yield 0 occupancy"); } + #[cfg(feature = "eigenvalue")] + #[test] + fn runtime_sized_empty_reference_scores_held_out_background() { + let config = FieldModelConfig { + n_links: 1, + n_subcarriers: 56, + n_modes: 3, + min_calibration_frames: 500, + min_calibration_duration_s: 0.0, + baseline_expiry_s: 86_400.0, + }; + let mut model = FieldModel::new(config).unwrap(); + let mut calibration = Vec::new(); + for frame_index in 0..600 { + let time = frame_index as f64 * 0.071; + let frame: Vec = (0..56) + .map(|subcarrier| { + let carrier = subcarrier as f64; + 18.0 + carrier * 0.08 + + (time + carrier * 0.13).sin() * 0.8 + + (time * 0.37 + carrier * 0.031).cos() * 0.35 + }) + .collect(); + model.feed_calibration(&[frame.clone()]).unwrap(); + calibration.push(frame); + } + model.finalize_calibration(1_000_000, 0).unwrap(); + + let modes = model.modes().unwrap(); + assert_eq!( + modes.baseline_runtime_window_size, + Some(RUNTIME_OCCUPANCY_WINDOW) + ); + assert!(modes.baseline_runtime_eigenvalue_count.is_some()); + assert!(modes.empty_room_residual_energy_threshold.is_some()); + assert_eq!(modes.empty_room_residual_energy_reference.len(), 12); + + let held_out_empty = calibration[550..600].to_vec(); + assert_eq!(model.estimate_occupancy(&held_out_empty).unwrap(), 0); + let background = model + .empty_room_match(&held_out_empty) + .expect("background match"); + assert!(background.matches_empty); + assert_eq!(background.reference_window_count, 12); + assert!(background + .score + .is_some_and(|score| (0.5..=1.0).contains(&score))); + } + + #[cfg(feature = "eigenvalue")] + #[test] + fn runtime_reference_rejects_shifted_held_out_window() { + let config = FieldModelConfig { + n_links: 1, + n_subcarriers: 56, + n_modes: 3, + min_calibration_frames: 500, + min_calibration_duration_s: 0.0, + baseline_expiry_s: 86_400.0, + }; + let mut model = FieldModel::new(config).unwrap(); + for frame_index in 0..600 { + let time = frame_index as f64 * 0.071; + let frame: Vec = (0..56) + .map(|subcarrier| { + let carrier = subcarrier as f64; + 18.0 + carrier * 0.08 + + (time + carrier * 0.13).sin() * 0.8 + + (time * 0.37 + carrier * 0.031).cos() * 0.35 + }) + .collect(); + model.feed_calibration(&[frame]).unwrap(); + } + model.finalize_calibration(1_000_000, 0).unwrap(); + + let shifted: Vec> = (0..RUNTIME_OCCUPANCY_WINDOW) + .map(|frame_index| { + let time = (600 + frame_index) as f64 * 0.071; + (0..56) + .map(|subcarrier| { + let carrier = subcarrier as f64; + let body_shift = if subcarrier % 3 == 0 { 8.0 } else { -5.0 }; + 18.0 + carrier * 0.08 + + (time + carrier * 0.13).sin() * 0.8 + + (time * 0.37 + carrier * 0.031).cos() * 0.35 + + body_shift + }) + .collect() + }) + .collect(); + assert!(model.estimate_occupancy(&shifted).unwrap() >= 1); + let background = model.empty_room_match(&shifted).expect("shifted match"); + assert!(!background.matches_empty); + assert_eq!(background.score, Some(0.0)); + } + + #[test] + fn runtime_background_reference_is_single_link_only() { + let config = FieldModelConfig { + n_links: 2, + n_subcarriers: 8, + n_modes: 2, + min_calibration_frames: 100, + min_calibration_duration_s: 0.0, + baseline_expiry_s: 86_400.0, + }; + let mut model = FieldModel::new(config).unwrap(); + for frame_index in 0..100 { + let phase = frame_index as f64 * 0.1; + model + .feed_calibration(&[ + (0..8).map(|index| phase + index as f64).collect(), + (0..8).map(|index| phase - index as f64).collect(), + ]) + .unwrap(); + } + model.finalize_calibration(1_000_000, 0).unwrap(); + + let modes = model.modes().unwrap(); + assert_eq!(modes.baseline_runtime_window_size, None); + assert_eq!(modes.baseline_runtime_eigenvalue_count, None); + assert_eq!(modes.empty_room_residual_energy_threshold, None); + assert!(modes.empty_room_residual_energy_reference.is_empty()); + } + #[test] fn test_baseline_eigenvalue_count_stored() { let config = FieldModelConfig { @@ -1735,14 +2254,15 @@ mod tests { // ----------------------------------------------------------------- #[test] - fn test_default_frame_target_derives_from_duration() { + fn test_default_frame_target_covers_background_reference_windows() { let cfg = FieldModelConfig::default(); assert_eq!(cfg.min_calibration_duration_s, CALIBRATION_DURATION_S); assert_eq!( - cfg.min_calibration_frames, - (CALIBRATION_DURATION_S * ASSUMED_CALIBRATION_RATE_HZ) as usize, - "frame target must be the intended window at the assumed rate" + cfg.min_calibration_frames, MIN_CALIBRATION_FRAMES, + "frame target must cover the minimum independent runtime windows" ); + assert_eq!(MIN_CALIBRATION_FRAMES, 1_000); + assert_eq!(MIN_BACKGROUND_REFERENCE_WINDOWS, 20); } #[test] From c4021f98846cb03759140dcb265b157082b2ef81 Mon Sep 17 00:00:00 2001 From: rUv Date: Sat, 5 Sep 2026 09:42:43 -0400 Subject: [PATCH 18/27] docs: add WiFlow reproduction protocol (#1787) --- docs/research/wiflow-reproduction-protocol.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/research/wiflow-reproduction-protocol.md diff --git a/docs/research/wiflow-reproduction-protocol.md b/docs/research/wiflow-reproduction-protocol.md new file mode 100644 index 000000000..5bd807717 --- /dev/null +++ b/docs/research/wiflow-reproduction-protocol.md @@ -0,0 +1,63 @@ +# WiFlow Reproduction Protocol + +Status: Proposed research protocol + +Tracks issue #1786. + +## Objective + +Determine whether a CSI optical flow style representation improves reusable camera free motion intelligence in RuView rather than only improving a proxy flow metric. + +## Required capture metadata + +Every run records hardware family, firmware, antenna count and geometry, channel width, center frequency, packet rate, timestamp source, synchronization method, room identity, participant identity class, label source, calibration, packet loss, missing subcarriers, preprocessing version, model commit, accelerator, software versions, power mode, and random seeds. + +## Baselines + +1. current RuView CSI preprocessing plus strongest task specific sensing head +2. WiFlow style motion field representation +3. simpler temporal derivative or phase and amplitude motion baseline + +All conditions use identical train, validation, and held out environment splits. + +## Metrics + +Primary intermediate metrics: + +* endpoint error or equivalent flow error +* directional consistency +* temporal stability +* uncertainty calibration + +Required downstream metrics: + +* tracking quality +* occupancy or presence quality +* pose or activity metric where available +* spatial state consistency in RuField or WorldGraph + +Systems metrics: + +* p50 and p95 inference latency +* peak memory +* CPU, GPU, or accelerator utilization +* energy per inference where measurable +* bytes per observation and sustained bandwidth + +## Stress tests + +Test unseen rooms, unseen participants, changed antenna geometry, a second RF hardware family, packet loss, burst loss, missing subcarriers, lower packet rate, clock jitter, and partial calibration failure. + +## Promotion gate + +The representation graduates only when the same learned representation improves at least two downstream RuView tasks on held out environments, or reaches equivalent downstream quality with materially lower edge cost. + +A better optical flow proxy score alone is insufficient. + +## Security and privacy + +Visual or LiDAR labels may be used as privileged training evidence but must not become a runtime dependency for camera free deployment claims. Dataset provenance and consent requirements remain explicit. + +## Rollback + +The experiment is isolated behind a representation interface. Existing RuView sensing heads remain the production fallback. From 30404dd9cfd4d0193d5ce54b3786c3611ea89e6b Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 5 Sep 2026 08:50:44 -0400 Subject: [PATCH 19/27] feat(calibration): select explicit baseline source --- ...DR-355-local-empty-room-bootstrap-prior.md | 11 +++++- .../wifi-densepose-sensing-server/src/main.rs | 39 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md b/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md index 5eb4ba732..1ec6ee36d 100644 --- a/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md +++ b/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md @@ -25,8 +25,10 @@ prior only after two server controlled stages: 1. Normal calibration reaches both 1,000 accepted frames and 600 seconds. The frame target represents twenty complete fifty frame runtime windows. - Collection binds to the first accepted ESP32 source and rejects frames from - other radios for that single link model. + A caller may select one currently live ESP32 source. The server checks its + freshness before replacing an active bootstrap prior, binds collection to + that source, and rejects frames from other radios. When no source is + selected, the first accepted ESP32 source preserves legacy behavior. 2. Promotion observes 12 new samples spaced by at least one second. Each sample has a four second bounded acquisition timeout. At least 10 must resolve empty, all ticks must be fresh, and none may publish numeric vital @@ -89,6 +91,9 @@ occupied sequences. Empty data is never relabeled as human training. gate as `sensing_update`. When the bootstrap background matches, it emits absent state, zero people, null numeric rates, and an explicit abstention reason instead of forwarding raw edge candidates. +7. `POST /api/v1/calibration/start?source_node_id=N` selects a live source for + deterministic single link capture. A missing or stale selection fails + without disabling the active bootstrap prior. ## Measured installation evidence @@ -111,3 +116,5 @@ and data directory. Status must report `binding_mode=bootstrap_only`, zero collection frames, and false authority for calibrated evidence and numeric vitals. Twelve fresh empty samples must contain no numeric vital signs. Starting a new room calibration must replace the prior and begin at frame zero. +When several radios are live, selecting Node 5 must leave Nodes 3 and 7 out of +the source set for the entire capture. diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 0c0628301..16261c1a1 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -6159,6 +6159,14 @@ mod bootstrap_vital_publication_tests { assert!(!calibration_source_accepts(&sources, 3)); } + #[test] + fn explicit_single_link_source_rejects_other_live_nodes() { + let sources = std::collections::BTreeSet::from([5]); + assert!(calibration_source_accepts(&sources, 5)); + assert!(!calibration_source_accepts(&sources, 3)); + assert!(!calibration_source_accepts(&sources, 7)); + } + fn strong_candidates() -> VitalSigns { VitalSigns { breathing_rate_bpm: Some(15.0), @@ -6249,8 +6257,33 @@ mod bootstrap_vital_publication_tests { // ── Field model calibration endpoints (eigenvalue person counting) ────────── -async fn calibration_start(State(state): State) -> Json { +#[derive(Debug, Default, Deserialize)] +struct CalibrationStartQuery { + /// Optional explicit single-link source. When omitted, the first accepted + /// ESP32 packet preserves the legacy binding behavior. + source_node_id: Option, +} + +async fn calibration_start( + State(state): State, + Query(query): Query, +) -> Json { let mut s = state.write().await; + if let Some(source_node_id) = query.source_node_id { + let now = std::time::Instant::now(); + let source_is_live = s.node_states.get(&source_node_id).is_some_and(|node| { + node.last_frame_time + .is_some_and(|seen| now.saturating_duration_since(seen) < ESP32_OFFLINE_TIMEOUT) + }); + if !source_is_live { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_source_unavailable", + "error": "The requested calibration source is missing or stale.", + "source_node_id": source_node_id, + })); + } + } if s.bootstrap_baseline_active { s.field_model = None; s.bootstrap_baseline_active = false; @@ -6279,10 +6312,14 @@ async fn calibration_start(State(state): State) -> Json Date: Sat, 5 Sep 2026 09:15:07 -0400 Subject: [PATCH 20/27] fix(calibration): learn held out empty residuals --- ...DR-355-local-empty-room-bootstrap-prior.md | 14 ++ .../wifi-densepose-sensing-server/src/main.rs | 233 ++++++++++++++++++ .../src/ruvsense/field_model.rs | 126 ++++++++++ 3 files changed, 373 insertions(+) diff --git a/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md b/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md index 1ec6ee36d..e03ef06d7 100644 --- a/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md +++ b/docs/adr/ADR-355-local-empty-room-bootstrap-prior.md @@ -54,6 +54,14 @@ evidence or numeric heart and breathing rates. Explicit calibration replaces it with fresh statistics. Cancelling an unfinished capture restores a valid prior. Administrator scoped reset removes it. +A restored image may receive one operator confirmed held out empty refinement. +The server measures 12 fresh, source bound runtime residuals at one second +spacing and requires zero stale samples and zero numeric vital samples. It +persists only the scalar residuals. The new boundary is the larger of the +existing threshold and 110 percent of the held out 95th percentile, capped at +125 percent of the prior threshold. The one use marker persists in the model, +so repeated requests cannot ratchet the threshold upward. + The status endpoint reports the learned runtime window, residual threshold, reference count, current background conformance, and the negative only authority boundary. A restored model never resumes collection and never @@ -94,6 +102,9 @@ occupied sequences. Empty data is never relabeled as human training. 7. `POST /api/v1/calibration/start?source_node_id=N` selects a live source for deterministic single link capture. A missing or stale selection fails without disabling the active bootstrap prior. +8. `POST /api/v1/calibration/bootstrap/refine?confirmed_empty=true&source_node_id=N` + performs the bounded one time held out refinement. It requires administrator + authority when transport authentication is enabled. ## Measured installation evidence @@ -118,3 +129,6 @@ vitals. Twelve fresh empty samples must contain no numeric vital signs. Starting a new room calibration must replace the prior and begin at frame zero. When several radios are live, selecting Node 5 must leave Nodes 3 and 7 out of the source set for the entire capture. +After restart, a confirmed empty holdout may refine the restored boundary once. +The stored snapshot must reject a second refinement and must continue to deny +calibrated evidence and numeric vital authority. diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 16261c1a1..cbbed1461 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -6264,6 +6264,13 @@ struct CalibrationStartQuery { source_node_id: Option, } +#[derive(Debug, Default, Deserialize)] +struct BootstrapRefineQuery { + #[serde(default)] + confirmed_empty: bool, + source_node_id: Option, +} + async fn calibration_start( State(state): State, Query(query): Query, @@ -6401,6 +6408,7 @@ async fn calibration_status(State(state): State) -> Json, + Query(query): Query, +) -> Json { + if !query.confirmed_empty { + return Json(serde_json::json!({ + "success": false, + "error_code": "empty_confirmation_required", + "error": "Explicit empty-room confirmation is required for bootstrap refinement.", + })); + } + + let (expected_model_id, expected_digest, source_node_id) = { + let s = state.read().await; + let Some(metadata) = s.bootstrap_baseline.as_ref().filter(|_| s.bootstrap_baseline_active) + else { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_not_active", + "error": "A restored startup baseline must be active before refinement.", + })); + }; + let [source_node_id] = metadata.source_node_ids.as_slice() else { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_source_invalid", + "error": "Bootstrap refinement requires exactly one bound source node.", + })); + }; + if query + .source_node_id + .is_some_and(|requested| requested != *source_node_id) + { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_source_mismatch", + "error": "The requested source does not match the stored bootstrap source.", + })); + } + ( + metadata.source_model_id.clone(), + metadata.content_sha256.clone(), + *source_node_id, + ) + }; + + let mut residuals = Vec::with_capacity(BOOTSTRAP_VALIDATION_SAMPLES); + let mut stale_sample_count = 0usize; + let mut vital_sign_sample_count = 0usize; + let mut last_sequence = None; + let mut next_sample_at = tokio::time::Instant::now(); + for _ in 0..BOOTSTRAP_VALIDATION_SAMPLES { + tokio::time::sleep_until(next_sample_at).await; + let interval_deadline = tokio::time::Instant::now() + + Duration::from_millis(BOOTSTRAP_VALIDATION_SAMPLE_TIMEOUT_MS); + let mut observed = None; + while tokio::time::Instant::now() < interval_deadline { + observed = { + let s = state.read().await; + let identity_matches = s.bootstrap_baseline_active + && s.bootstrap_baseline.as_ref().is_some_and(|metadata| { + metadata.source_model_id == expected_model_id + && metadata.content_sha256 == expected_digest + && metadata.source_node_ids.as_slice() == [source_node_id] + }); + if !identity_matches { + None + } else { + s.node_states.get(&source_node_id).and_then(|node| { + let sequence = node.latest_csi_sequence?; + let fresh = node.last_frame_time.is_some_and(|seen| { + std::time::Instant::now().saturating_duration_since(seen) + < ESP32_OFFLINE_TIMEOUT + }) && Some(sequence) != last_sequence; + if !fresh { + return None; + } + let model = s.field_model.as_ref()?; + let recent_frames: Vec> = + node.frame_history.iter().cloned().collect(); + let residual = model.empty_room_match(&recent_frames)?.residual_energy; + let vital_signs_absent = s + .latest_update + .as_ref() + .is_some_and(|update| update.vital_signs.is_none()); + Some((sequence, residual, vital_signs_absent)) + }) + } + }; + if observed.is_some() { + break; + } + tokio::time::sleep(Duration::from_millis(50)).await; + } + if let Some((sequence, residual, vital_signs_absent)) = observed { + last_sequence = Some(sequence); + residuals.push(residual); + if !vital_signs_absent { + vital_sign_sample_count += 1; + } + } else { + stale_sample_count += 1; + } + next_sample_at = tokio::time::Instant::now() + + Duration::from_millis(BOOTSTRAP_VALIDATION_MIN_SPACING_MS); + } + + if residuals.len() != BOOTSTRAP_VALIDATION_SAMPLES + || stale_sample_count != 0 + || vital_sign_sample_count != 0 + { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_refinement_validation_failed", + "error": "The fresh empty-room refinement gate did not pass. The stored baseline was not changed.", + "validation": { + "sample_count": residuals.len(), + "stale_sample_count": stale_sample_count, + "vital_sign_sample_count": vital_sign_sample_count, + }, + })); + } + + let mut s = state.write().await; + let identity_matches = s.bootstrap_baseline_active + && s.bootstrap_baseline.as_ref().is_some_and(|metadata| { + metadata.source_model_id == expected_model_id + && metadata.content_sha256 == expected_digest + && metadata.source_node_ids.as_slice() == [source_node_id] + }); + if !identity_matches { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_changed", + "error": "The bootstrap identity changed during refinement. The stored baseline was not changed.", + })); + } + let candidate_snapshot = match s + .field_model + .as_ref() + .ok_or(wifi_densepose_signal::ruvsense::field_model::FieldModelError::NotCalibrated) + .and_then(|model| model.export_snapshot()) + { + Ok(snapshot) => snapshot, + Err(_) => { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_refinement_rejected", + "error": "The bootstrap model rejected this bounded refinement.", + })); + } + }; + let now_us = chrono::Utc::now().timestamp_micros() as u64; + let mut candidate = match FieldModel::from_snapshot(candidate_snapshot, now_us) { + Ok(model) => model, + Err(_) => { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_refinement_rejected", + "error": "The bootstrap model rejected this bounded refinement.", + })); + } + }; + let refinement = match candidate.refine_empty_room_residual_boundary(&residuals) { + Ok(refinement) => refinement, + Err(_) => { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_refinement_rejected", + "error": "The bootstrap model rejected this bounded refinement.", + })); + } + }; + let Some(installation_id) = s.installation_id.clone() else { + return Json(serde_json::json!({ + "success": false, + "error_code": "installation_id_required", + "error": "A stable installation identity is required to store a local startup baseline.", + })); + }; + let path = bootstrap_baseline::path_in(&s.data_dir); + let created_at_unix_ms = chrono::Utc::now().timestamp_millis() as u64; + let stored = match bootstrap_baseline::store( + &path, + &installation_id, + &[source_node_id], + &expected_model_id, + created_at_unix_ms, + &candidate, + ) { + Ok(metadata) => metadata, + Err(error) => { + return error_response::internal_error_json("bootstrap baseline refinement store", error) + } + }; + s.field_model = Some(candidate); + s.bootstrap_baseline = Some(stored.clone()); + Json(serde_json::json!({ + "success": true, + "message": "The local empty-room startup baseline was refined.", + "validation": { + "sample_count": residuals.len(), + "stale_sample_count": stale_sample_count, + "vital_sign_sample_count": vital_sign_sample_count, + }, + "refinement": { + "threshold_before": refinement.threshold_before, + "threshold_after": refinement.threshold_after, + "held_out_p95": refinement.held_out_p95, + "held_out_sample_count": refinement.held_out_sample_count, + "reference_window_count": refinement.reference_window_count, + }, + "bootstrap_baseline": stored, + "calibrated_evidence_authorized": false, + "numeric_vitals_authorized": false, + })) +} + /// Cancel only an unfinished capture. Completed model deletion remains on the /// administrator scoped reset route. async fn calibration_cancel(State(state): State) -> Json { @@ -10054,6 +10283,10 @@ async fn main() { "/api/v1/calibration/bootstrap/promote", post(calibration_promote_bootstrap), ) + .route( + "/api/v1/calibration/bootstrap/refine", + post(calibration_refine_bootstrap), + ) .route("/api/v1/calibration/cancel", post(calibration_cancel)) .route("/api/v1/calibration/status", get(calibration_status)) .route("/api/v1/calibration/reset", post(calibration_reset)) diff --git a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs index 56fd09011..7bba49eff 100644 --- a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs +++ b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs @@ -57,6 +57,10 @@ pub const MIN_CALIBRATION_FRAMES: usize = RUNTIME_OCCUPANCY_WINDOW * MIN_BACKGROUND_REFERENCE_WINDOWS; const CALIBRATION_BACKGROUND_WINDOWS_MAX: usize = 512; const EMPTY_ROOM_RESIDUAL_MARGIN: f64 = 1.5; +const EMPTY_ROOM_HELD_OUT_MARGIN: f64 = 1.10; +const EMPTY_ROOM_REFINEMENT_MAX_LIFT: f64 = 1.25; +const EMPTY_ROOM_REFINEMENT_MIN_SAMPLES: usize = 10; +const EMPTY_ROOM_REFINEMENT_MAX_SAMPLES: usize = 64; // --------------------------------------------------------------------------- // Error types @@ -362,6 +366,11 @@ pub struct FieldNormalMode { /// an empty room model into positive person evidence. #[serde(default)] pub empty_room_residual_energy_reference: Vec, + /// Number of operator-confirmed held-out empty refinements. Version one + /// permits one bounded refinement so repeated calls cannot ratchet the + /// background boundary upward until a weak occupant is suppressed. + #[serde(default)] + pub empty_room_residual_refinement_count: u8, } /// A bounded comparison between one runtime window and the learned empty room @@ -377,6 +386,16 @@ pub struct EmptyRoomMatch { pub reference_window_count: usize, } +/// Receipt for one bounded, operator-confirmed empty-room refinement. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct EmptyRoomRefinement { + pub threshold_before: f64, + pub threshold_after: f64, + pub held_out_p95: f64, + pub held_out_sample_count: usize, + pub reference_window_count: usize, +} + /// Body perturbation extracted from a CSI observation. /// /// After subtracting the baseline and projecting out environmental @@ -692,6 +711,73 @@ impl FieldModel { .and_then(|modes| modes.empty_room_residual_energy_threshold) } + /// Refine a completed single-link empty-room boundary with scalar residuals + /// measured during a separate operator-confirmed empty holdout. + /// + /// This never ingests or persists raw CSI. The lift is capped at 25 percent + /// and may happen only once per snapshot, preventing repeated calls from + /// silently erasing weak occupied evidence. + pub fn refine_empty_room_residual_boundary( + &mut self, + held_out_residuals: &[f64], + ) -> Result { + if self.config.n_links != 1 || self.status != CalibrationStatus::Fresh { + return Err(FieldModelError::NotCalibrated); + } + if !(EMPTY_ROOM_REFINEMENT_MIN_SAMPLES..=EMPTY_ROOM_REFINEMENT_MAX_SAMPLES) + .contains(&held_out_residuals.len()) + || held_out_residuals + .iter() + .any(|value| !value.is_finite() || *value < 0.0) + { + return Err(FieldModelError::InvalidConfig( + "held-out empty residuals are missing, excessive, or non-finite".into(), + )); + } + let modes = self.modes.as_mut().ok_or(FieldModelError::NotCalibrated)?; + if modes.empty_room_residual_refinement_count != 0 { + return Err(FieldModelError::InvalidConfig( + "the empty-room boundary already has its one permitted held-out refinement" + .into(), + )); + } + if modes.empty_room_residual_energy_reference.len() + held_out_residuals.len() + > CALIBRATION_BACKGROUND_WINDOWS_MAX + { + return Err(FieldModelError::InvalidConfig( + "the empty-room residual reference limit would be exceeded".into(), + )); + } + let threshold_before = modes + .empty_room_residual_energy_threshold + .ok_or(FieldModelError::NotCalibrated)? + .max(EMPTY_ROOM_RESIDUAL_ENERGY_MAX); + let held_out_p95 = percentile_95(held_out_residuals.to_vec()) + .ok_or(FieldModelError::NotCalibrated)?; + let proposed = held_out_p95 * EMPTY_ROOM_HELD_OUT_MARGIN; + let threshold_after = threshold_before + .max(proposed.min(threshold_before * EMPTY_ROOM_REFINEMENT_MAX_LIFT)); + + modes + .empty_room_residual_energy_reference + .extend_from_slice(held_out_residuals); + modes + .empty_room_residual_energy_reference + .sort_by(|left, right| { + left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) + }); + modes.empty_room_residual_energy_threshold = Some(threshold_after); + modes.empty_room_residual_refinement_count = 1; + + Ok(EmptyRoomRefinement { + threshold_before, + threshold_after, + held_out_p95, + held_out_sample_count: held_out_residuals.len(), + reference_window_count: modes.empty_room_residual_energy_reference.len(), + }) + } + /// Export aggregate model state suitable for local restart persistence. /// Raw calibration observations and Welford accumulators are not exported. pub fn export_snapshot(&self) -> Result { @@ -762,6 +848,7 @@ impl FieldModel { .is_none_or(|threshold| threshold.is_finite() && threshold >= 0.0) && modes.empty_room_residual_energy_reference.len() <= CALIBRATION_BACKGROUND_WINDOWS_MAX + && modes.empty_room_residual_refinement_count <= 1 && modes .empty_room_residual_energy_reference .iter() @@ -1118,6 +1205,7 @@ impl FieldModel { baseline_runtime_window_size: None, empty_room_residual_energy_threshold: None, empty_room_residual_energy_reference: Vec::new(), + empty_room_residual_refinement_count: 0, }; // The full calibration covariance and a fifty frame runtime covariance @@ -2066,6 +2154,44 @@ mod tests { .is_some_and(|score| (0.5..=1.0).contains(&score))); } + #[test] + fn held_out_empty_refinement_is_bounded_and_single_use() { + let config = FieldModelConfig { + n_links: 1, + n_subcarriers: 8, + n_modes: 2, + min_calibration_frames: 100, + min_calibration_duration_s: 0.0, + baseline_expiry_s: 86_400.0, + }; + let mut model = FieldModel::new(config).unwrap(); + for frame_index in 0..1_000 { + let phase = frame_index as f64 * 0.071; + let frame = (0..8) + .map(|index| 20.0 + index as f64 * 0.1 + (phase + index as f64).sin()) + .collect(); + model.feed_calibration(&[frame]).unwrap(); + } + model.finalize_calibration(1_000_000, 0).unwrap(); + + let before = model.empty_room_residual_energy_threshold().unwrap(); + let residuals = vec![before * 1.4; 12]; + let receipt = model + .refine_empty_room_residual_boundary(&residuals) + .unwrap(); + assert_eq!(receipt.threshold_before, before); + assert!(receipt.threshold_after > before); + assert!(receipt.threshold_after <= before * EMPTY_ROOM_REFINEMENT_MAX_LIFT); + assert_eq!(receipt.held_out_sample_count, 12); + assert_eq!( + model.modes().unwrap().empty_room_residual_refinement_count, + 1 + ); + assert!(model + .refine_empty_room_residual_boundary(&residuals) + .is_err()); + } + #[cfg(feature = "eigenvalue")] #[test] fn runtime_reference_rejects_shifted_held_out_window() { From a2c662f9f734ad4df2b3a3cd028b719b38dca120 Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 5 Sep 2026 10:28:19 -0400 Subject: [PATCH 21/27] fix: harden CSI timing and baseline reliability --- ...ul-csi-and-reliability-gated-adaptation.md | 151 +++++++++++++++++ docs/adr/README.md | 1 + firmware/esp32-csi-node/README.md | 9 +- firmware/esp32-csi-node/main/csi_collector.c | 17 +- firmware/esp32-csi-node/main/csi_collector.h | 3 + firmware/esp32-csi-node/test/Makefile | 21 ++- .../esp32-csi-node/test/fuzz_csi_serialize.c | 12 ++ .../esp32-csi-node/test/stubs/esp_stubs.h | 1 + .../esp32-csi-node/test/test_csi_sanitize.c | 56 +++++++ .../wifi-densepose-hardware/src/csi_frame.rs | 7 +- .../src/esp32_parser.rs | 7 +- .../src/adaptive_classifier.rs | 51 ++++++ .../wifi-densepose-sensing-server/src/csi.rs | 155 ++++++++++++++++-- .../src/field_bridge.rs | 10 ++ .../wifi-densepose-sensing-server/src/main.rs | 99 ++++++++--- .../src/vital_signs.rs | 41 +++++ .../src/ruvsense/field_model.rs | 97 ++++++++++- 17 files changed, 690 insertions(+), 48 deletions(-) create mode 100644 docs/adr/ADR-356-rate-truthful-csi-and-reliability-gated-adaptation.md create mode 100644 firmware/esp32-csi-node/test/test_csi_sanitize.c diff --git a/docs/adr/ADR-356-rate-truthful-csi-and-reliability-gated-adaptation.md b/docs/adr/ADR-356-rate-truthful-csi-and-reliability-gated-adaptation.md new file mode 100644 index 000000000..9562ce1a2 --- /dev/null +++ b/docs/adr/ADR-356-rate-truthful-csi-and-reliability-gated-adaptation.md @@ -0,0 +1,151 @@ +# ADR 356: Rate truthful CSI and reliability gated room adaptation + +Status: Accepted + +Date: 2026-09-05 + +## Context + +RuView mixed three different notions of time. ESP32 firmware requested a packet +rate, the server observed a delivered rate, and several server paths still used +fixed 2 Hz, 10 Hz, or 20 Hz assumptions. One feature named +`dominant_freq_hz` was not a temporal frequency at all. It was the index of the +strongest subcarrier multiplied by 0.05. A second defect compared a newly +inserted frame with itself, forcing that motion delta to zero. + +These errors can move a spectral peak into the breathing or heartbeat band and +produce plausible but false values. They also make models sensitive to WiFi +traffic and processor scheduling rather than physical motion. + +ESP-IDF documents that `wifi_csi_info_t.first_word_invalid` marks the first four +CSI bytes invalid because of a hardware limitation. It also documents that the +CSI receive callback runs in the WiFi task and should hand work to a lower +priority task rather than perform lengthy processing. RuView serialized the +invalid bytes unchanged and fed the original buffer to edge processing. + +Recent work suggests four useful directions with different maturity: + +1. OpenCSI represents each link as a dimensionless residual against its own + quiet period temporal standard deviation and exposes baseline maturity. Its + reported transfer results are limited to binary presence. +2. MORIC derives motion centered delay and Doppler views to reduce dependence + on static environment specific CSI structure. +3. Parameter efficient CSI crowd counting combines self supervised pretraining, + small room adapters, and a stateful counting machine. +4. WiFi JEPA masks complete radio links and predicts their latent embeddings, + using real and ray traced CSI for pose representation learning. + +The papers report promising external results. None is measured RuView evidence, +and none authorizes deployment without leakage free occupied and empty held out +evaluation. + +## Decision + +### Firmware ingestion + +1. When ESP-IDF marks the first CSI word invalid, zero the first four serialized + CSI bytes while retaining the fixed packet shape. +2. Set ADR-018 byte 19 bit 5 to record that sanitation occurred. +3. Feed edge DSP from the sanitized serialized payload, never the original + invalid driver buffer. +4. Keep callback work bounded to sanitation, serialization, and the existing + queue handoff. Do not add transforms to the WiFi task. + +### Server time model + +1. Estimate delivered CSI rate from accepted monotonic frame timestamps after + five samples. Use 20 Hz only during startup. +2. Cap the physical input rate at the firmware contract of 50 Hz so a burst or + timestamp artifact cannot retune downstream filters above their supported + range. +3. Reconfigure the vital detector only after a 20 percent rate change. Clear + temporal and smoothing state whenever its clock changes. Samples calculated + with different clocks must never be blended. +4. Compute temporal dominant frequency from the recent mean amplitude series + with demeaning, a Hann window, and bounded direct Fourier bins from 0.1 Hz to + the smaller of 4 Hz or 45 percent of sample rate. +5. Return zero for short, flat, nonfinite, or spectrally ambiguous windows. +6. Compare the current frame with the prior frame, not itself, for motion delta. +7. Export zero sample rate until the estimate is mature. Never present a startup + fallback as a measured node property. + +### Model compatibility and room reliability + +1. Preserve the historical fifth input feature for version 1 adaptive models at + the model boundary. Version 2 and later models consume the true temporal + frequency. This prevents an existing model from silently receiving a + different input distribution after a server update. +2. A background model is unreliable until it contains at least ten finite, + nonnegative residual references. An unreliable model cannot suppress a + runtime change and exposes no conformance score. +3. Expose background maturity, reliability, and a robust residual Z score based + on the median and median absolute deviation. +4. Keep the persisted empty home prior negative only. It cannot authorize + calibrated presence, person count, pose, or numeric vitals. + +### Model roadmap + +The following work remains proposal only: + +1. Evaluate an OpenCSI style per link quiet residual normalization for binary + presence without replacing absolute magnitude features used for motion. +2. Evaluate delay and Doppler views after phase sanitation for activity. +3. Pretrain a frozen multilink encoder and adapt small room specific modules for + counting. +4. Evaluate link masked self supervision for pose only when at least three + spatially distinct links and synchronized positive pose labels exist. + +Every candidate uses immutable manifests and splits by room, subject, +recording, and time. It is compared with the frozen baseline under identical +seeds. Counting includes confusion matrices and mean absolute error. Pose +includes PCK at 20 centimetres, MPJPE, and the mean pose baseline. A shuffled +label control must fail. + +Promotion requires at least 10 percent held out lift, less than 10 milliseconds +added p95 inference latency, no more than 20 percent peak memory overhead, zero +leakage or secret findings, frozen anchor retention, and no regression in empty +rejection or occupied recall. Promotion remains an explicit maintainer action. + +## Consequences + +The firmware no longer preserves a known invalid prefix as sensor evidence. The +server uses an observed physical clock for spectral features and vital filters. +Existing version 1 adaptive models remain compatible. Sparse background samples +fail open to observation and fail closed for suppression. + +The direct Fourier estimator is intentionally small and bounded to 128 temporal +samples. It is portable and deterministic, but it is not a replacement for a +phase sanitized delay and Doppler pipeline. + +The largest remaining uncertainty is human recall. Empty home evidence can +measure false presence but cannot show whether weak occupied signals are being +suppressed. The fix path is a consented, leakage separated occupied holdout with +multiple spatial links and synchronized reference labels. + +## Research basis + +1. Espressif ESP-IDF WiFi CSI programming guide: + https://docs.espressif.com/projects/esp-idf/en/v5.5.4/esp32/api-guides/wifi.html +2. Espressif ESP CSI reference implementation: + https://github.com/espressif/esp-csi +3. OpenCSI: + https://arxiv.org/abs/2607.26665 +4. MORIC: + https://arxiv.org/abs/2506.12997 +5. Parameter efficient CSI crowd counting: + https://arxiv.org/abs/2601.02203 +6. WiFi JEPA: + https://arxiv.org/abs/2607.11064 + +## Acceptance test + +Run the firmware serializer host tests and the focused hardware, signal, and +sensing server Rust suites. The deterministic serializer test must prove the +first four bytes are zero only when ESP-IDF marks them invalid and bit 5 must +round trip through the Rust parser. The temporal estimator must recover a +1.2 Hz synthetic signal within 0.21 Hz, abstain on short and flat inputs, and +remain below 10 milliseconds p95 on the target Mac. Existing version 1 model +input must retain the legacy proxy. A background model with nine references +must never report a match or suppress a runtime change. Physical firmware is +not qualified until an S3 and C6 boot and stream the sanitized flag without +watchdog, memory, or transport regressions during a five minute burn in. diff --git a/docs/adr/README.md b/docs/adr/README.md index 84b858b22..095c4bccf 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -97,6 +97,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme | [ADR-350](ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md) | RuVector predictive memory and RuVLLM authority boundary | Proposed | | [ADR-354](ADR-354-forecast-governance-enforcement-in-ruforecast-submodule.md) | fal.ai forecast governance and spend enforcement now real, in the vendored RuForecast submodule | Accepted | | [ADR-355](ADR-355-local-empty-room-bootstrap-prior.md) | Local empty room bootstrap prior | Accepted | +| [ADR-356](ADR-356-rate-truthful-csi-and-reliability-gated-adaptation.md) | Rate truthful CSI and reliability gated room adaptation | Accepted | ### Platform and UI diff --git a/firmware/esp32-csi-node/README.md b/firmware/esp32-csi-node/README.md index ebdddc2d0..2469814f7 100644 --- a/firmware/esp32-csi-node/README.md +++ b/firmware/esp32-csi-node/README.md @@ -257,10 +257,17 @@ Offset Size Field 12 4 Sequence number (LE u32) 16 1 RSSI (i8) 17 1 Noise floor (i8) -18 2 Reserved +18 1 PPDU type (ADR-110; zero when tagging is disabled) +19 1 Flags: bit0=40 MHz, bit2=STBC, bit4=sync valid, + bit5=invalid first CSI word zeroed by firmware 20 N*2 I/Q pairs (n_antennas * n_subcarriers * 2 bytes) ``` +When ESP-IDF marks `first_word_invalid`, the firmware preserves packet geometry, +zeros the first four CSI bytes, and sets byte 19 bit 5. The same sanitized bytes +feed the edge DSP path. This prevents a documented hardware artifact from being +learned as motion while keeping older readers wire compatible. + ### Vitals Packet (32 bytes) ``` diff --git a/firmware/esp32-csi-node/main/csi_collector.c b/firmware/esp32-csi-node/main/csi_collector.c index 6a15443fa..dfa6398bb 100644 --- a/firmware/esp32-csi-node/main/csi_collector.c +++ b/firmware/esp32-csi-node/main/csi_collector.c @@ -263,8 +263,16 @@ size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf buf[19] = 0; #endif - /* I/Q data */ + /* I/Q data. ESP-IDF documents that the first four CSI bytes are invalid + * when first_word_invalid is set. Never let those hardware artifacts feed + * either the host or edge DSP path. Preserve the fixed ADR-018 geometry by + * zeroing rather than removing bytes, and mark the sanitation in bit 5. */ memcpy(&buf[CSI_HEADER_SIZE], info->buf, iq_len); + if (info->first_word_invalid && iq_len > 0) { + size_t invalid_len = iq_len < 4 ? iq_len : 4; + memset(&buf[CSI_HEADER_SIZE], 0, invalid_len); + buf[19] |= CSI_FLAG_FIRST_WORD_SANITIZED; + } return frame_size; } @@ -330,13 +338,16 @@ static void wifi_csi_callback(void *ctx, wifi_csi_info_t *info) * while the on-device Tier 1/2 pipeline receives a uniform, sustainable * stream. Enqueuing every burst frame overloaded the unicore C6 DSP and * turned 30-40 callback pps into an irregular approximately 8 Hz subset. */ - if (info->buf && info->len > 0) { + if (frame_len > CSI_HEADER_SIZE) { if (s_next_edge_enqueue_us == 0) { s_next_edge_enqueue_us = now_us; } if (now_us >= s_next_edge_enqueue_us) { - (void)edge_enqueue_csi((const uint8_t *)info->buf, (uint16_t)info->len, + /* Reuse the sanitized ADR-018 payload. Feeding info->buf here + * would reintroduce first_word_invalid artifacts on device. */ + (void)edge_enqueue_csi(&frame_buf[CSI_HEADER_SIZE], + (uint16_t)(frame_len - CSI_HEADER_SIZE), (int8_t)info->rx_ctrl.rssi, info->rx_ctrl.channel); /* Preserve the configured sample clock instead of resetting it to diff --git a/firmware/esp32-csi-node/main/csi_collector.h b/firmware/esp32-csi-node/main/csi_collector.h index 92f431053..cce99393c 100644 --- a/firmware/esp32-csi-node/main/csi_collector.h +++ b/firmware/esp32-csi-node/main/csi_collector.h @@ -17,6 +17,9 @@ /** ADR-018 header size in bytes. */ #define CSI_HEADER_SIZE 20 +/** ADR-018 byte 19: ESP-IDF reported and firmware sanitized invalid CSI prefix. */ +#define CSI_FLAG_FIRST_WORD_SANITIZED (1U << 5) + /** Maximum frame buffer size (header + 4 antennas * 256 subcarriers * 2 bytes). */ #define CSI_MAX_FRAME_SIZE (CSI_HEADER_SIZE + 4 * 256 * 2) diff --git a/firmware/esp32-csi-node/test/Makefile b/firmware/esp32-csi-node/test/Makefile index e2b46699f..19c82dbec 100644 --- a/firmware/esp32-csi-node/test/Makefile +++ b/firmware/esp32-csi-node/test/Makefile @@ -45,10 +45,20 @@ FUZZ_JOBS ?= 1 .PHONY: all clean run_serialize run_edge run_nvs run_all test_adr110 run_adr110 \ test_vitals run_vitals test_mmwave_detect run_mmwave_detect host_tests \ - test_thermal run_thermal + test_thermal run_thermal test_csi_sanitize run_csi_sanitize all: fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \ - test_thermal + test_thermal test_csi_sanitize + +# --- ESP-IDF invalid first-word sanitation --- +# Links the production serializer. Function sections and dead stripping keep +# unrelated device-only callback code out of this host executable. +test_csi_sanitize: test_csi_sanitize.c $(MAIN_DIR)/csi_collector.c $(STUBS_SRC) + cc -std=c99 -Wall -Wextra -Istubs -I$(MAIN_DIR) -ffunction-sections \ + $^ -Wl,-dead_strip -o $@ -lm + +run_csi_sanitize: test_csi_sanitize + ./test_csi_sanitize # --- ADR-110 encoding unit tests --- # Host-side, no libFuzzer needed — plain C99 deterministic table tests @@ -93,8 +103,8 @@ test_thermal: test_thermal_hysteresis.c $(MAIN_DIR)/thermal.h run_thermal: test_thermal ./test_thermal -host_tests: run_adr110 run_vitals run_mmwave_detect run_thermal - @echo "Host tests passed (ADR-110 + vitals #998/#996 + mmwave detect #1135 + thermal)" +host_tests: run_adr110 run_vitals run_mmwave_detect run_thermal run_csi_sanitize + @echo "Host tests passed (ADR-110 + CSI sanitation + vitals + mmwave + thermal)" # --- Serialize fuzzer --- # Tests csi_serialize_frame() with random wifi_csi_info_t inputs. @@ -130,5 +140,6 @@ run_nvs: fuzz_nvs run_all: run_serialize run_edge run_nvs clean: - rm -f fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals + rm -f fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \ + test_thermal test_csi_sanitize rm -rf corpus_serialize/ corpus_edge/ corpus_nvs/ diff --git a/firmware/esp32-csi-node/test/fuzz_csi_serialize.c b/firmware/esp32-csi-node/test/fuzz_csi_serialize.c index 1eb3af855..4aade0421 100644 --- a/firmware/esp32-csi-node/test/fuzz_csi_serialize.c +++ b/firmware/esp32-csi-node/test/fuzz_csi_serialize.c @@ -90,6 +90,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) info.rx_ctrl.sig_mode = legacy_inputs & 0x03; info.rx_ctrl.cwb = (legacy_inputs >> 2) & 0x01; info.rx_ctrl.stbc = (legacy_inputs >> 3) & 0x01; + info.first_word_invalid = (test_case & 0x80) != 0; /* Use remaining fuzz data as I/Q buffer content. */ uint16_t iq_len; @@ -136,6 +137,17 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) if (result > out_len) { __builtin_trap(); /* Buffer overflow detected. */ } + if (result >= CSI_HEADER_SIZE + iq_len && info.first_word_invalid && iq_len > 0) { + size_t invalid_len = iq_len < 4 ? iq_len : 4; + for (size_t i = 0; i < invalid_len; i++) { + if (out_buf[CSI_HEADER_SIZE + i] != 0) { + __builtin_trap(); /* Invalid hardware prefix escaped sanitation. */ + } + } + if ((out_buf[19] & CSI_FLAG_FIRST_WORD_SANITIZED) == 0) { + __builtin_trap(); /* Sanitized payload must carry provenance. */ + } + } /* --- Test case 1: NULL info pointer --- */ if (test_case & 0x01) { diff --git a/firmware/esp32-csi-node/test/stubs/esp_stubs.h b/firmware/esp32-csi-node/test/stubs/esp_stubs.h index 3d849a89b..e2dc08abc 100644 --- a/firmware/esp32-csi-node/test/stubs/esp_stubs.h +++ b/firmware/esp32-csi-node/test/stubs/esp_stubs.h @@ -90,6 +90,7 @@ typedef struct { typedef struct { wifi_pkt_rx_ctrl_t rx_ctrl; uint8_t mac[6]; + bool first_word_invalid; int16_t len; /**< Length of the I/Q buffer in bytes. */ int8_t *buf; /**< Pointer to I/Q data. */ } wifi_csi_info_t; diff --git a/firmware/esp32-csi-node/test/test_csi_sanitize.c b/firmware/esp32-csi-node/test/test_csi_sanitize.c new file mode 100644 index 000000000..f0776a76a --- /dev/null +++ b/firmware/esp32-csi-node/test/test_csi_sanitize.c @@ -0,0 +1,56 @@ +/** Deterministic host test for ESP-IDF first_word_invalid sanitation. */ + +#include "esp_stubs.h" +#include "nvs_config.h" +#include "csi_collector.h" + +#include +#include +#include + +nvs_config_t g_nvs_config; + +static void make_info(wifi_csi_info_t *info, int8_t *iq, uint16_t len, bool invalid) +{ + memset(info, 0, sizeof(*info)); + info->rx_ctrl.channel = 6; + info->rx_ctrl.rssi = -42; + info->rx_ctrl.noise_floor = -91; + info->len = (int16_t)len; + info->buf = iq; + info->first_word_invalid = invalid; +} + +int main(void) +{ + int8_t iq[] = {1, 2, 3, 4, 5, 6, 7, 8}; + uint8_t frame[CSI_MAX_FRAME_SIZE]; + wifi_csi_info_t info; + + make_info(&info, iq, sizeof(iq), true); + size_t len = csi_serialize_frame(&info, frame, sizeof(frame)); + assert(len == CSI_HEADER_SIZE + sizeof(iq)); + assert(frame[CSI_HEADER_SIZE] == 0); + assert(frame[CSI_HEADER_SIZE + 1] == 0); + assert(frame[CSI_HEADER_SIZE + 2] == 0); + assert(frame[CSI_HEADER_SIZE + 3] == 0); + assert(frame[CSI_HEADER_SIZE + 4] == 5); + assert((frame[19] & CSI_FLAG_FIRST_WORD_SANITIZED) != 0); + + make_info(&info, iq, sizeof(iq), false); + len = csi_serialize_frame(&info, frame, sizeof(frame)); + assert(len == CSI_HEADER_SIZE + sizeof(iq)); + assert(memcmp(&frame[CSI_HEADER_SIZE], iq, sizeof(iq)) == 0); + assert((frame[19] & CSI_FLAG_FIRST_WORD_SANITIZED) == 0); + + int8_t short_iq[] = {9, 10}; + make_info(&info, short_iq, sizeof(short_iq), true); + len = csi_serialize_frame(&info, frame, sizeof(frame)); + assert(len == CSI_HEADER_SIZE + sizeof(short_iq)); + assert(frame[CSI_HEADER_SIZE] == 0); + assert(frame[CSI_HEADER_SIZE + 1] == 0); + assert((frame[19] & CSI_FLAG_FIRST_WORD_SANITIZED) != 0); + + puts("CSI invalid-prefix sanitation: PASS"); + return 0; +} diff --git a/v2/crates/wifi-densepose-hardware/src/csi_frame.rs b/v2/crates/wifi-densepose-hardware/src/csi_frame.rs index 412240eca..19cfa0d34 100644 --- a/v2/crates/wifi-densepose-hardware/src/csi_frame.rs +++ b/v2/crates/wifi-densepose-hardware/src/csi_frame.rs @@ -145,13 +145,16 @@ impl PpduType { /// bit 2 : STBC /// bit 3 : LDPC (reserved — not yet populated by firmware) /// bit 4 : 802.15.4 time-sync valid (C6 only) -/// bit 5-7 : reserved +/// bit 5 : ESP-IDF invalid first CSI word was zeroed by firmware +/// bit 6-7 : reserved #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Adr018Flags { pub bw40: bool, pub stbc: bool, pub ldpc: bool, pub ieee802154_sync_valid: bool, + #[serde(default)] + pub first_word_sanitized: bool, } impl Adr018Flags { @@ -161,6 +164,7 @@ impl Adr018Flags { stbc: (b & 0x04) != 0, ldpc: (b & 0x08) != 0, ieee802154_sync_valid: (b & 0x10) != 0, + first_word_sanitized: (b & 0x20) != 0, } } pub fn to_byte(self) -> u8 { @@ -169,6 +173,7 @@ impl Adr018Flags { if self.stbc { b |= 0x04; } if self.ldpc { b |= 0x08; } if self.ieee802154_sync_valid { b |= 0x10; } + if self.first_word_sanitized { b |= 0x20; } b } } diff --git a/v2/crates/wifi-densepose-hardware/src/esp32_parser.rs b/v2/crates/wifi-densepose-hardware/src/esp32_parser.rs index fffe3a22f..20d060666 100644 --- a/v2/crates/wifi-densepose-hardware/src/esp32_parser.rs +++ b/v2/crates/wifi-densepose-hardware/src/esp32_parser.rs @@ -413,15 +413,16 @@ mod tests { #[test] fn adr110_flags_round_trip_all_bits() { - // All known flag bits set: bw40 (0x01) + STBC (0x04) + LDPC (0x08) + 15.4-sync (0x10) = 0x1D - let data = build_test_frame_with_he(6, 1, &[(0, 0); 56], 1, 0x1D); + // All known flag bits set: bw40 + STBC + LDPC + sync + sanitized prefix = 0x3D. + let data = build_test_frame_with_he(6, 1, &[(0, 0); 56], 1, 0x3D); let (frame, _) = Esp32CsiParser::parse_frame(&data).unwrap(); assert!(frame.metadata.adr018_flags.bw40); assert!(frame.metadata.adr018_flags.stbc); assert!(frame.metadata.adr018_flags.ldpc); assert!(frame.metadata.adr018_flags.ieee802154_sync_valid); + assert!(frame.metadata.adr018_flags.first_word_sanitized); // Round-trip the encoder - assert_eq!(frame.metadata.adr018_flags.to_byte(), 0x1D); + assert_eq!(frame.metadata.adr018_flags.to_byte(), 0x3D); } #[test] diff --git a/v2/crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs b/v2/crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs index 3a662d07a..f5baecbf7 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs @@ -139,6 +139,39 @@ pub fn features_from_runtime(feat: &serde_json::Value, amps: &[f64]) -> [f64; N_ ] } +/// Preserve the fifth feature's historical meaning for models trained before +/// the server began measuring temporal frequency from a frame window. +/// +/// Version 1 models learned a proxy equal to the strongest subcarrier index +/// multiplied by 0.05. Version 2 and later models consume the measured +/// temporal frequency in hertz. Keeping this translation at the model boundary +/// prevents a server upgrade from silently changing an existing model's input +/// distribution. +pub fn compatible_dominant_frequency( + model_version: u32, + temporal_frequency_hz: f64, + amplitudes: &[f64], +) -> f64 { + if model_version >= 2 { + return if temporal_frequency_hz.is_finite() { + temporal_frequency_hz.max(0.0) + } else { + 0.0 + }; + } + + amplitudes + .iter() + .enumerate() + .filter(|(_, amplitude)| amplitude.is_finite()) + .max_by(|(_, left), (_, right)| { + left.partial_cmp(right) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|(index, _)| index as f64 * 0.05) + .unwrap_or(0.0) +} + /// Compute statistical features from raw subcarrier amplitudes. fn subcarrier_stats(amps: &[f64]) -> (f64, f64, f64, f64, f64, f64, f64, f64) { if amps.is_empty() { @@ -642,3 +675,21 @@ pub fn train_from_recordings(recordings_dir: &Path) -> Result PathBuf { PathBuf::from("data/adaptive_model.json") } + +#[cfg(test)] +mod compatibility_tests { + use super::compatible_dominant_frequency; + + #[test] + fn version_one_keeps_legacy_subcarrier_proxy() { + let amplitudes = [1.0, 2.0, 8.0, 3.0]; + assert_eq!(compatible_dominant_frequency(1, 1.25, &litudes), 0.10); + } + + #[test] + fn version_two_uses_measured_temporal_frequency() { + let amplitudes = [1.0, 20.0, 2.0]; + assert_eq!(compatible_dominant_frequency(2, 1.25, &litudes), 1.25); + assert_eq!(compatible_dominant_frequency(2, f64::NAN, &litudes), 0.0); + } +} diff --git a/v2/crates/wifi-densepose-sensing-server/src/csi.rs b/v2/crates/wifi-densepose-sensing-server/src/csi.rs index 6d0cd9004..a49e97c16 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/csi.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/csi.rs @@ -11,6 +11,141 @@ use crate::vital_signs::VitalSigns; const EDGE_MAX_PERSONS: u8 = 4; +/// Estimate a real temporal motion frequency from recent CSI amplitude +/// history. Each frame is reduced to its mean amplitude, detrended, Hann +/// windowed, and evaluated on exact DFT bins. +pub fn estimate_temporal_dominant_frequency_hz( + frame_history: &VecDeque>, + sample_rate_hz: f64, +) -> f64 { + if !sample_rate_hz.is_finite() || sample_rate_hz <= 0.0 { + return 0.0; + } + + let recent_frames: Vec<&Vec> = frame_history + .iter() + .rev() + .take(128) + .collect(); + if recent_frames + .iter() + .any(|frame| frame.is_empty() || frame.iter().any(|value| !value.is_finite())) + { + return 0.0; + } + let samples: Vec = recent_frames + .into_iter() + .rev() + .map(|frame| frame.iter().sum::() / frame.len() as f64) + .collect(); + let n = samples.len(); + if n < 16 { + return 0.0; + } + + let mean = samples.iter().sum::() / n as f64; + let upper_hz = 4.0_f64.min(sample_rate_hz * 0.45); + if upper_hz < 0.1 { + return 0.0; + } + + let mut powers = Vec::new(); + for bin in 1..=(n / 2) { + let frequency_hz = bin as f64 * sample_rate_hz / n as f64; + if !(0.1..=upper_hz).contains(&frequency_hz) { + continue; + } + let mut real = 0.0; + let mut imag = 0.0; + let angle_step = 2.0 * std::f64::consts::PI * bin as f64 / n as f64; + let (step_sin, step_cos) = angle_step.sin_cos(); + let mut angle_cos = 1.0; + let mut angle_sin = 0.0; + for (index, sample) in samples.iter().enumerate() { + let window = + 0.5 * (1.0 - (2.0 * std::f64::consts::PI * index as f64 / (n - 1) as f64).cos()); + let value = (sample - mean) * window; + real += value * angle_cos; + imag -= value * angle_sin; + let next_cos = angle_cos * step_cos - angle_sin * step_sin; + angle_sin = angle_sin * step_cos + angle_cos * step_sin; + angle_cos = next_cos; + } + powers.push((frequency_hz, real * real + imag * imag)); + } + if powers.is_empty() { + return 0.0; + } + let mean_power = powers.iter().map(|(_, power)| power).sum::() / powers.len() as f64; + let (peak_hz, peak_power) = powers + .into_iter() + .max_by(|left, right| { + left.1 + .partial_cmp(&right.1) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .unwrap_or((0.0, 0.0)); + if mean_power <= f64::EPSILON || peak_power / mean_power < 2.0 { + 0.0 + } else { + peak_hz + } +} + +#[cfg(test)] +mod temporal_frequency_tests { + use super::*; + + #[test] + fn recovers_temporal_frequency_in_hertz() { + let sample_rate_hz = 20.0; + let target_hz = 1.2; + let mut history = VecDeque::new(); + for sample in 0..100 { + let t = sample as f64 / sample_rate_hz; + let value = 20.0 + 2.0 * (2.0 * std::f64::consts::PI * target_hz * t).sin(); + history.push_back(vec![value; 56]); + } + let measured = estimate_temporal_dominant_frequency_hz(&history, sample_rate_hz); + assert!((measured - target_hz).abs() <= 0.21, "measured {measured} Hz"); + } + + #[test] + fn abstains_on_short_or_flat_history() { + let short = VecDeque::from(vec![vec![20.0; 56]; 15]); + assert_eq!(estimate_temporal_dominant_frequency_hz(&short, 20.0), 0.0); + let flat = VecDeque::from(vec![vec![20.0; 56]; 100]); + assert_eq!(estimate_temporal_dominant_frequency_hz(&flat, 20.0), 0.0); + let mut corrupt = flat; + corrupt[50][4] = f64::NAN; + assert_eq!(estimate_temporal_dominant_frequency_hz(&corrupt, 20.0), 0.0); + } + + #[test] + fn temporal_frequency_p95_stays_below_server_budget() { + let sample_rate_hz = 50.0; + let mut history = VecDeque::new(); + for sample in 0..128 { + let t = sample as f64 / sample_rate_hz; + let value = 20.0 + 2.0 * (2.0 * std::f64::consts::PI * 1.2 * t).sin(); + history.push_back(vec![value; 256]); + } + let mut timings = Vec::with_capacity(1_000); + for _ in 0..1_000 { + let start = std::time::Instant::now(); + std::hint::black_box(estimate_temporal_dominant_frequency_hz( + std::hint::black_box(&history), + sample_rate_hz, + )); + timings.push(start.elapsed()); + } + timings.sort_unstable(); + let p95 = timings[949]; + eprintln!("temporal dominant frequency p95: {p95:?}"); + assert!(p95 < std::time::Duration::from_millis(10), "p95 {p95:?}"); + } +} + /// Person count is supporting evidence, never an independent occupancy claim. /// Return zero and mark invalid for contradictory or out-of-range firmware. fn sanitize_edge_person_count(presence: bool, raw: u8) -> (u8, bool) { @@ -469,14 +604,7 @@ pub fn extract_features_from_frame( 0.0 }; - let peak_idx = frame - .amplitudes - .iter() - .enumerate() - .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal)) - .map(|(i, _)| i) - .unwrap_or(0); - let dominant_freq_hz = peak_idx as f64 * 0.05; + let dominant_freq_hz = estimate_temporal_dominant_frequency_hz(frame_history, sample_rate_hz); let threshold = mean_amp * 1.2; let change_points = frame @@ -485,7 +613,9 @@ pub fn extract_features_from_frame( .filter(|w| (w[0] < threshold) != (w[1] < threshold)) .count(); - let temporal_motion_score = if let Some(prev_frame) = frame_history.back() { + // The caller appends the current frame before extraction, so the true + // predecessor is the second item from the back. + let temporal_motion_score = if let Some(prev_frame) = frame_history.iter().rev().nth(1) { let n_cmp = n_sub.min(prev_frame.len()); if n_cmp > 0 { let diff_energy: f64 = (0..n_cmp) @@ -636,13 +766,18 @@ pub fn adaptive_override( .back() .map(|v| v.as_slice()) .unwrap_or(&[]); + let dominant_frequency = adaptive_classifier::compatible_dominant_frequency( + model.version, + features.dominant_freq_hz, + amps, + ); let feat_arr = adaptive_classifier::features_from_runtime( &serde_json::json!({ "variance": features.variance, "motion_band_power": features.motion_band_power, "breathing_band_power": features.breathing_band_power, "spectral_power": features.spectral_power, - "dominant_freq_hz": features.dominant_freq_hz, + "dominant_freq_hz": dominant_frequency, "change_points": features.change_points, "mean_rssi": features.mean_rssi, }), diff --git a/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs b/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs index a60c266ca..980f04cfb 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs @@ -47,6 +47,9 @@ const MAX_SINGLE_LINK_OCCUPANCY: usize = 3; pub struct BootstrapBackgroundMatch { pub matches_empty: bool, pub score: Option, + pub normalized_residual_z: Option, + pub maturity: f64, + pub reliable: bool, pub residual_energy: f64, pub residual_energy_threshold: f64, pub window_size: usize, @@ -227,6 +230,9 @@ pub fn bootstrap_background_match( Some(BootstrapBackgroundMatch { matches_empty: result.matches_empty, score: result.score, + normalized_residual_z: result.normalized_residual_z, + maturity: result.maturity, + reliable: result.reliable, residual_energy: result.residual_energy, residual_energy_threshold: result.residual_energy_threshold, window_size: result.window_size, @@ -502,6 +508,9 @@ mod tests { .expect("background score"); assert!(background.matches_empty); assert_eq!(background.reference_window_count, 12); + assert!(background.reliable); + assert_eq!(background.maturity, 0.6); + assert!(background.normalized_residual_z.is_some()); assert!(background .score .is_some_and(|score| (0.5..=1.0).contains(&score))); @@ -530,5 +539,6 @@ mod tests { .expect("shifted background result"); assert!(!shifted_background.matches_empty); assert_eq!(shifted_background.score, Some(0.0)); + assert!(shifted_background.reliable); } } diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index cbbed1461..5c159c417 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -915,10 +915,10 @@ const NOVELTY_SKETCH_VERSION: u16 = 1; /// bursts whose intra-burst arrival deltas are tens of microseconds apart — /// a 36 µs delta yields `1/dt ≈ 27 kHz`, which the old `< 1 s` guard let /// straight into the EMA and inflated `csi_fps_ema` by 1–3 orders of -/// magnitude (issue #1180). We reject any delta implying more than 200 fps -/// (4× the physical ceiling, leaving slack for benign arrival jitter); such -/// deltas are burst artifacts, not distinct production intervals. +/// magnitude (issue #1180). We reject sub-5 ms deltas as burst artifacts and +/// cap accepted estimates to the firmware's 50 fps physical ceiling. pub(crate) const MIN_PLAUSIBLE_CSI_DT_SEC: f64 = 0.005; +pub(crate) const MAX_PHYSICAL_CSI_FPS: f64 = 50.0; /// ADR-110 iter 18 — EMA update for per-node CSI fps tracking. /// @@ -934,7 +934,7 @@ pub(crate) fn update_csi_fps_ema(prev_fps: f64, dt_sec: f64) -> Option { if !(dt_sec >= MIN_PLAUSIBLE_CSI_DT_SEC && dt_sec < 1.0) { return None; } - let instantaneous = 1.0 / dt_sec; + let instantaneous = (1.0 / dt_sec).min(MAX_PHYSICAL_CSI_FPS); // y[n] = y[n-1] + (x - y[n-1]) / 8 Some(prev_fps + (instantaneous - prev_fps) / 8.0) } @@ -983,6 +983,15 @@ mod fps_ema_tests { assert!(update_csi_fps_ema(40.0, 0.005).is_some()); } + #[test] + fn accepted_burst_edge_is_capped_to_firmware_ceiling() { + let mut fps = 50.0; + for _ in 0..32 { + fps = update_csi_fps_ema(fps, 0.005).unwrap(); + } + assert!(fps <= 50.0, "reported {fps} Hz above firmware ceiling"); + } + #[test] fn burst_interleaved_with_nominal_stays_in_band() { // A true ~40 fps node whose frames arrive in sub-ms bursts: feeding @@ -1004,6 +1013,18 @@ mod fps_ema_tests { } impl NodeState { + /// Measured sample rate only after enough inter-frame deltas have warmed + /// the EMA. The physical ceiling is enforced again at the consumption + /// boundary so restored or malformed state cannot overclock DSP math. + fn measured_sample_rate_hz(&self) -> Option { + (self.csi_fps_samples >= 5 && self.csi_fps_ema.is_finite()) + .then(|| self.csi_fps_ema.clamp(1.0, MAX_PHYSICAL_CSI_FPS)) + } + + fn effective_sample_rate_hz(&self) -> f64 { + self.measured_sample_rate_hz().unwrap_or(20.0) + } + /// ADR-110 §A0.12 timestamp recovery: given a CSI frame's node-local /// `esp_timer_get_time()` snapshot, return the mesh-aligned epoch /// computed from this node's most recent sync packet — or `None` @@ -1051,7 +1072,7 @@ impl NodeState { // samples; until then fall back to the 20 Hz firmware ceiling. The // §A0.12 capture showed real bench fps ≈ 10, so the measured value // is significantly more accurate than the constant fallback. - let fps = if self.csi_fps_samples >= 5 { self.csi_fps_ema } else { 20.0 }; + let fps = self.effective_sample_rate_hz(); Some(sync.mesh_aligned_us_for_sequence(frame_sequence, fps)) } @@ -1099,7 +1120,7 @@ impl NodeState { is_valid: sync.flags.is_valid, smoothed: sync.flags.smoothed_used, sequence: sync.sequence, - csi_fps_ema: self.csi_fps_ema, + csi_fps_ema: self.effective_sample_rate_hz(), csi_fps_samples: self.csi_fps_samples, staleness_ms: self.latest_sync_at.map(|t| t.elapsed().as_millis() as u64), }) @@ -1163,7 +1184,7 @@ impl NodeState { hr_buffer: VecDeque::with_capacity(8), br_buffer: VecDeque::with_capacity(8), rssi_history: VecDeque::new(), - vital_detector: VitalSignDetector::new(10.0), + vital_detector: VitalSignDetector::new(20.0), latest_vitals: VitalSigns::default(), last_frame_time: None, edge_vitals: None, @@ -1349,7 +1370,7 @@ fn build_node_features( }, rssi_dbm: ns.rssi_history.back().copied().unwrap_or(0.0), last_seen_ms, - frame_rate_hz: 0.0, // Computed elsewhere; not yet plumbed here. + frame_rate_hz: ns.measured_sample_rate_hz().unwrap_or(0.0), stale, novelty_score: ns.last_novelty_score, } @@ -2768,15 +2789,9 @@ fn extract_features_from_frame( 0.0 }; - // ── Dominant frequency via peak subcarrier index ── - let peak_idx = frame - .amplitudes - .iter() - .enumerate() - .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal)) - .map(|(i, _)| i) - .unwrap_or(0); - let dominant_freq_hz = peak_idx as f64 * 0.05; + // A subcarrier index is a spatial-frequency bin, not temporal hertz. + let dominant_freq_hz = + csi::estimate_temporal_dominant_frequency_hz(frame_history, sample_rate_hz); // ── Change point detection (threshold-crossing count in current frame) ── let threshold = mean_amp * 1.2; @@ -2789,7 +2804,9 @@ fn extract_features_from_frame( // ── Motion score: sliding-window temporal difference ── // Compare current frame against the most recent historical frame. // The difference is normalised by the mean amplitude to be scale-invariant. - let temporal_motion_score = if let Some(prev_frame) = frame_history.back() { + // Callers append the current frame first, making the true predecessor the + // second item from the back. + let temporal_motion_score = if let Some(prev_frame) = frame_history.iter().rev().nth(1) { let n_cmp = n_sub.min(prev_frame.len()); if n_cmp > 0 { let diff_energy: f64 = (0..n_cmp) @@ -3022,13 +3039,18 @@ fn adaptive_override( .back() .map(|v| v.as_slice()) .unwrap_or(&[]); + let dominant_frequency = adaptive_classifier::compatible_dominant_frequency( + model.version, + features.dominant_freq_hz, + amps, + ); let feat_arr = adaptive_classifier::features_from_runtime( &serde_json::json!({ "variance": features.variance, "motion_band_power": features.motion_band_power, "breathing_band_power": features.breathing_band_power, "spectral_power": features.spectral_power, - "dominant_freq_hz": features.dominant_freq_hz, + "dominant_freq_hz": dominant_frequency, "change_points": features.change_points, "mean_rssi": features.mean_rssi, }), @@ -6452,6 +6474,9 @@ async fn calibration_status(State(state): State) -> Json= 5 + && ns.vital_detector.reconfigure_sample_rate(sample_rate_hz) + { + // Never smooth estimates computed against two clocks. + ns.smoothed_hr = 0.0; + ns.smoothed_br = 0.0; + ns.smoothed_hr_conf = 0.0; + ns.smoothed_br_conf = 0.0; + ns.hr_buffer.clear(); + ns.br_buffer.clear(); + } + let raw_vitals = ns .vital_detector .process_frame(&frame.amplitudes, &frame.phases); @@ -10620,6 +10663,20 @@ mod sync_snapshot_helper_tests { assert_eq!(snap.csi_fps_samples, 42); } + #[test] + fn exported_node_rate_is_mature_and_physically_bounded() { + let mut ns = NodeState::new(); + assert_eq!(ns.measured_sample_rate_hz(), None); + + ns.csi_fps_ema = 74.0; + ns.csi_fps_samples = 5; + assert_eq!(ns.measured_sample_rate_hz(), Some(50.0)); + + ns.latest_sync = Some(populated_sync(9)); + ns.latest_sync_at = Some(std::time::Instant::now()); + assert_eq!(ns.sync_snapshot().unwrap().csi_fps_ema, 50.0); + } + #[test] fn observe_csi_frame_arrival_ignores_subms_bursts() { // Issue #1180 regression: a ~40 fps node whose frames are delivered diff --git a/v2/crates/wifi-densepose-sensing-server/src/vital_signs.rs b/v2/crates/wifi-densepose-sensing-server/src/vital_signs.rs index 10558c6cd..d6a310685 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/vital_signs.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/vital_signs.rs @@ -346,6 +346,31 @@ impl VitalSignDetector { self.frame_count = 0; } + /// Retune temporal windows to an observed CSI clock. Changes smaller than + /// twenty percent are ignored to avoid resetting on normal arrival jitter. + /// A real clock change clears history so samples from different clocks are + /// never interpreted in one spectrum. + pub fn reconfigure_sample_rate(&mut self, sample_rate: f64) -> bool { + if !sample_rate.is_finite() || sample_rate < 1.0 { + return false; + } + let relative_change = (sample_rate - self.sample_rate).abs() / self.sample_rate.max(1.0); + if relative_change < 0.20 { + return false; + } + self.sample_rate = sample_rate; + self.breathing_capacity = + ((sample_rate * self.breathing_window_secs) as usize).max(1); + self.heartbeat_capacity = + ((sample_rate * self.heartbeat_window_secs) as usize).max(1); + self.reset(); + true + } + + pub fn sample_rate_hz(&self) -> f64 { + self.sample_rate + } + /// Current buffer fill levels for diagnostics. /// Returns (breathing_len, breathing_capacity, heartbeat_len, heartbeat_capacity). pub fn buffer_status(&self) -> (usize, usize, usize, usize) { @@ -842,6 +867,22 @@ mod tests { assert_eq!(hb_len, 0); } + #[test] + fn measured_clock_reconfigures_once_and_clears_mixed_history() { + let mut detector = VitalSignDetector::new(20.0); + let amp = vec![10.0; 56]; + let phase = vec![0.0; 56]; + for _ in 0..32 { + detector.process_frame(&, &phase); + } + assert!(!detector.reconfigure_sample_rate(18.0)); + assert_eq!(detector.sample_rate_hz(), 20.0); + assert!(detector.reconfigure_sample_rate(12.0)); + assert_eq!(detector.sample_rate_hz(), 12.0); + assert_eq!(detector.buffer_status(), (0, 360, 0, 180)); + assert!(!detector.reconfigure_sample_rate(f64::NAN)); + } + #[test] fn test_vital_signs_default() { let vs = VitalSigns::default(); diff --git a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs index 7bba49eff..41596d179 100644 --- a/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs +++ b/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs @@ -380,6 +380,12 @@ pub struct FieldNormalMode { pub struct EmptyRoomMatch { pub matches_empty: bool, pub score: Option, + /// Robust residual distance from the quiet reference median. + pub normalized_residual_z: Option, + /// Reference coverage relative to the normal calibration target. + pub maturity: f64, + /// Whether the reference set is large and valid enough to suppress change. + pub reliable: bool, pub residual_energy: f64, pub residual_energy_threshold: f64, pub window_size: usize, @@ -644,6 +650,27 @@ fn percentile_95(mut values: Vec) -> Option { values.get(index).copied() } +fn robust_residual_z(reference: &[f64], residual: f64) -> Option { + if !residual.is_finite() || reference.is_empty() { + return None; + } + let mut sorted: Vec = reference + .iter() + .copied() + .filter(|value| value.is_finite() && *value >= 0.0) + .collect(); + if sorted.len() != reference.len() { + return None; + } + sorted.sort_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal)); + let median = sorted[sorted.len() / 2]; + let mut deviations: Vec = sorted.iter().map(|value| (value - median).abs()).collect(); + deviations.sort_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal)); + let mad = deviations[deviations.len() / 2]; + let robust_scale = (1.4826 * mad).max((median.abs() * 0.01).max(f64::EPSILON)); + Some((residual - median) / robust_scale) +} + #[cfg(feature = "eigenvalue")] fn percentile_95_usize(mut values: Vec) -> Option { values.sort_unstable(); @@ -1315,12 +1342,20 @@ impl FieldModel { let residual_energy_threshold = modes .empty_room_residual_energy_threshold? .max(EMPTY_ROOM_RESIDUAL_ENERGY_MAX); - let matches_empty = residual_energy <= residual_energy_threshold; let reference_window_count = modes.empty_room_residual_energy_reference.len(); - let score = if !matches_empty { - Some(0.0) - } else if reference_window_count == 0 { + let maturity = (reference_window_count as f64 + / MIN_BACKGROUND_REFERENCE_WINDOWS as f64) + .clamp(0.0, 1.0); + let reliable = reference_window_count >= 10 + && modes + .empty_room_residual_energy_reference + .iter() + .all(|value| value.is_finite() && *value >= 0.0); + let matches_empty = reliable && residual_energy <= residual_energy_threshold; + let score = if !reliable { None + } else if !matches_empty { + Some(0.0) } else { let at_or_below = modes .empty_room_residual_energy_reference @@ -1331,6 +1366,12 @@ impl FieldModel { Some(EmptyRoomMatch { matches_empty, score, + normalized_residual_z: robust_residual_z( + &modes.empty_room_residual_energy_reference, + residual_energy, + ), + maturity, + reliable, residual_energy, residual_energy_threshold, window_size, @@ -2149,6 +2190,9 @@ mod tests { .expect("background match"); assert!(background.matches_empty); assert_eq!(background.reference_window_count, 12); + assert!(background.reliable); + assert_eq!(background.maturity, 0.6); + assert!(background.normalized_residual_z.is_some()); assert!(background .score .is_some_and(|score| (0.5..=1.0).contains(&score))); @@ -2237,6 +2281,51 @@ mod tests { let background = model.empty_room_match(&shifted).expect("shifted match"); assert!(!background.matches_empty); assert_eq!(background.score, Some(0.0)); + assert!(background.reliable); + assert!(background.normalized_residual_z.is_some()); + } + + #[cfg(feature = "eigenvalue")] + #[test] + fn sparse_background_reference_cannot_suppress_runtime_change() { + let config = FieldModelConfig { + n_links: 1, + n_subcarriers: 56, + n_modes: 3, + min_calibration_frames: 500, + min_calibration_duration_s: 0.0, + baseline_expiry_s: 86_400.0, + }; + let mut model = FieldModel::new(config).unwrap(); + let mut calibration = Vec::new(); + for frame_index in 0..600 { + let time = frame_index as f64 * 0.071; + let frame: Vec = (0..56) + .map(|subcarrier| { + let carrier = subcarrier as f64; + 18.0 + carrier * 0.08 + + (time + carrier * 0.13).sin() * 0.8 + + (time * 0.37 + carrier * 0.031).cos() * 0.35 + }) + .collect(); + model.feed_calibration(&[frame.clone()]).unwrap(); + calibration.push(frame); + } + model.finalize_calibration(1_000_000, 0).unwrap(); + model + .modes + .as_mut() + .unwrap() + .empty_room_residual_energy_reference + .truncate(9); + + let result = model + .empty_room_match(&calibration[550..600]) + .expect("background comparison"); + assert!(!result.reliable); + assert!(!result.matches_empty); + assert_eq!(result.score, None); + assert_eq!(result.maturity, 0.45); } #[test] From 82e05d06640bdc2ed1cedb3b7f3323a2e5679b77 Mon Sep 17 00:00:00 2001 From: ruv Date: Sun, 6 Sep 2026 09:37:03 -0400 Subject: [PATCH 22/27] fix(calibration): expire live field models safely --- .../src/field_bridge.rs | 45 +++- .../wifi-densepose-sensing-server/src/main.rs | 228 ++++++++++++++---- .../src/types.rs | 5 + 3 files changed, 228 insertions(+), 50 deletions(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs b/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs index 980f04cfb..21c4ee461 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs @@ -149,6 +149,18 @@ pub fn single_link_config() -> FieldModelConfig { } } +/// Resolve the model status at the observation wall clock. `FieldModel::status` +/// records the state at collection or restore time and does not advance as a +/// long-running server crosses the stale and expiry boundaries. +pub fn calibration_status_at(field: &FieldModel, observed_at_us: u64) -> CalibrationStatus { + match field.status() { + CalibrationStatus::Uncalibrated | CalibrationStatus::Collecting => field.status(), + CalibrationStatus::Fresh | CalibrationStatus::Stale | CalibrationStatus::Expired => { + field.check_freshness(observed_at_us) + } + } +} + /// Estimate occupancy using the FieldModel when calibrated, falling back /// to the score-based heuristic otherwise. /// @@ -158,10 +170,11 @@ pub fn single_link_config() -> FieldModelConfig { pub fn occupancy_or_fallback( field: &FieldModel, frame_history: &VecDeque>, + observed_at_us: u64, smoothed_score: f64, prev_count: usize, ) -> usize { - match field.status() { + match calibration_status_at(field, observed_at_us) { CalibrationStatus::Fresh | CalibrationStatus::Stale => { let frames: Vec> = frame_history .iter() @@ -473,6 +486,36 @@ mod tests { ); } + #[test] + fn occupancy_falls_back_after_expiry_without_a_process_restart() { + let field = fresh_test_model(); + let baseline = field.modes().unwrap().baseline[0].clone(); + let history: VecDeque> = + (0..OCCUPANCY_WINDOW).map(|_| baseline.clone()).collect(); + + assert_eq!(field.status(), CalibrationStatus::Fresh); + assert_eq!( + calibration_status_at(&field, 1_500_000), + CalibrationStatus::Fresh + ); + assert_eq!( + occupancy_or_fallback(&field, &history, 1_500_000, 0.0, 0), + 0, + "a fresh field model may resolve its learned baseline as empty" + ); + + assert_eq!(field.status(), CalibrationStatus::Fresh); + assert_eq!( + calibration_status_at(&field, 4_000_000), + CalibrationStatus::Expired + ); + assert_eq!( + occupancy_or_fallback(&field, &history, 4_000_000, 0.0, 0), + score_to_person_count(0.0, 0), + "an expired in-memory model must fall back even though its cached status is fresh" + ); + } + #[test] fn adaptive_runtime_reference_scores_empty_and_rejects_shift() { let config = FieldModelConfig { diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 5c159c417..c2f8790dd 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -1730,12 +1730,38 @@ mod adr323_pose_physics_http_tests { const ESP32_OFFLINE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); impl AppStateInner { + fn field_model_status_at(&self, observed_at_unix_ms: u64) -> Option { + self.field_model.as_ref().map(|model| { + field_bridge::calibration_status_at(model, observed_at_unix_ms.saturating_mul(1_000)) + }) + } + + fn field_model_active_at(&self, observed_at_unix_ms: u64) -> bool { + self.field_model_status_at(observed_at_unix_ms) + .is_some_and(|status| status != CalibrationStatus::Expired) + } + + fn bootstrap_baseline_active_at(&self, observed_at_unix_ms: u64) -> bool { + self.bootstrap_baseline_active && self.field_model_active_at(observed_at_unix_ms) + } + + fn explicit_calibration_fresh_at(&self, observed_at_unix_ms: u64) -> bool { + !self.bootstrap_baseline_active + && self.field_model_status_at(observed_at_unix_ms) == Some(CalibrationStatus::Fresh) + } + /// Return the effective data source, accounting for ESP32 frame timeout. /// If the source is "esp32" but no frame has arrived in 5 seconds, returns /// "esp32:offline" so the UI can distinguish active vs stale connections. /// Person count: eigenvalue-based if field model is calibrated, else heuristic. /// Uses global frame_history if populated, otherwise the freshest per-node history. - fn person_count(&self) -> usize { + fn person_count_at(&self, observed_at_unix_ms: u64) -> usize { + // A persisted bootstrap model has negative-only authority. Its only + // allowed occupancy effect is the explicit empty-background suppression + // in `bootstrap_empty_prior_applies`; it must never infer positive count. + if self.bootstrap_baseline_active { + return score_to_person_count(self.smoothed_person_score, self.prev_person_count); + } match self.field_model.as_ref() { Some(fm) => { // A single-link model may score only the source node it was @@ -1769,6 +1795,7 @@ impl AppStateInner { field_bridge::occupancy_or_fallback( fm, history, + observed_at_unix_ms.saturating_mul(1_000), self.smoothed_person_score, self.prev_person_count, ) @@ -1777,6 +1804,10 @@ impl AppStateInner { } } + fn person_count(&self) -> usize { + self.person_count_at(chrono::Utc::now().timestamp_millis().max(0) as u64) + } + /// A restored startup image has negative-only authority. It may suppress /// an empty-room false positive after one complete runtime window, but it /// cannot establish positive presence or publish vital signs. @@ -1955,6 +1986,95 @@ impl AppStateInner { } } +#[cfg(test)] +mod calibration_expiry_tests { + use super::*; + use wifi_densepose_signal::ruvsense::field_model::FieldModelConfig; + + fn finalized_model(calibrated_at_unix_ms: u64) -> FieldModel { + let mut model = FieldModel::new(FieldModelConfig { + n_links: 1, + n_subcarriers: 56, + n_modes: 3, + min_calibration_frames: 10, + min_calibration_duration_s: 0.0, + baseline_expiry_s: 2.0, + }) + .unwrap(); + for frame_index in 0..20 { + let frame = (0..56) + .map(|subcarrier| { + 10.0 + subcarrier as f64 * 0.01 + + (frame_index as f64 * 0.1 + subcarrier as f64 * 0.03).sin() * 0.01 + }) + .collect(); + model.feed_calibration(&[frame]).unwrap(); + } + model + .finalize_calibration(calibrated_at_unix_ms.saturating_mul(1_000), 7) + .unwrap(); + model + } + + fn state_with_model(bootstrap: bool) -> AppStateInner { + let model = finalized_model(1_000); + let baseline = model.modes().unwrap().baseline[0].clone(); + let mut state = AppStateInner::minimal(); + state.frame_history = (0..50).map(|_| baseline.clone()).collect(); + state.field_model = Some(model); + state.bootstrap_baseline_active = bootstrap; + if bootstrap { + state.bootstrap_baseline = Some(BootstrapBaselineMetadata { + authority: bootstrap_baseline::BOOTSTRAP_BASELINE_AUTHORITY, + source_node_ids: vec![5], + source_model_id: "test-model".to_string(), + created_at_unix_ms: 1_000, + expires_at_unix_ms: 3_000, + content_sha256: "00".repeat(32), + }); + } + state + } + + #[test] + fn bootstrap_field_model_remains_negative_only_for_person_count() { + let runtime = state_with_model(false); + assert_eq!(runtime.person_count_at(1_500), 0); + + let bootstrap = state_with_model(true); + assert_eq!( + bootstrap.person_count_at(1_500), + score_to_person_count(0.0, 0), + "a bootstrap prior must not derive a positive or negative count through the runtime model path" + ); + } + + #[tokio::test] + async fn long_running_expiry_is_not_reported_active_for_runtime_or_bootstrap() { + let runtime = Arc::new(RwLock::new(state_with_model(false))); + let Json(runtime_status) = calibration_status(State(runtime)).await; + assert_eq!(runtime_status["active"], false); + assert_eq!(runtime_status["status"], "expired"); + assert_eq!(runtime_status["binding_mode"], "none"); + + let bootstrap = Arc::new(RwLock::new(state_with_model(true))); + let Json(bootstrap_status) = calibration_status(State(bootstrap)).await; + assert_eq!(bootstrap_status["active"], false); + assert_eq!(bootstrap_status["status"], "expired"); + assert_eq!(bootstrap_status["binding_mode"], "none"); + assert_eq!(bootstrap_status["bootstrap_baseline"]["stored"], true); + assert_eq!(bootstrap_status["bootstrap_baseline"]["active"], false); + assert_eq!( + bootstrap_status["bootstrap_baseline"]["calibrated_evidence_authorized"], + false + ); + assert_eq!( + bootstrap_status["bootstrap_baseline"]["numeric_vitals_authorized"], + false + ); + } +} + // ── ESP32 Edge Vitals Packet (ADR-039, magic 0xC511_0002) ──────────────────── /// Decoded vitals packet from ESP32 edge processing pipeline. @@ -6319,7 +6439,9 @@ async fn calibration_start( } // Guard: don't discard an in-progress or fresh calibration if let Some(ref fm) = s.field_model { - match fm.status() { + let observed_at_us = + (chrono::Utc::now().timestamp_millis().max(0) as u64).saturating_mul(1_000); + match field_bridge::calibration_status_at(fm, observed_at_us) { CalibrationStatus::Collecting => { return Json(serde_json::json!({ "success": false, @@ -6423,6 +6545,9 @@ async fn calibration_stop(State(state): State) -> Json) -> Json { let s = state.read().await; let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; + let effective_status = s.field_model_status_at(observed_at_unix_ms); + let active = s.field_model_active_at(observed_at_unix_ms); + let bootstrap_active = s.bootstrap_baseline_active_at(observed_at_unix_ms); let bootstrap_background_match = s.bootstrap_background_match(observed_at_unix_ms); let runtime_reference = s.field_model.as_ref().and_then(|model| { model.modes().map(|modes| serde_json::json!({ @@ -6434,21 +6559,22 @@ async fn calibration_status(State(state): State) -> Json) -> Json Json { let expected_model_id = { let s = state.read().await; - if s.bootstrap_baseline_active - || !s.field_model.as_ref().is_some_and(|model| { - matches!(model.status(), CalibrationStatus::Fresh) - }) - { + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; + if s.bootstrap_baseline_active || !s.explicit_calibration_fresh_at(observed_at_unix_ms) { return Json(serde_json::json!({ "success": false, "error_code": "calibration_not_finalized", @@ -6540,8 +6663,9 @@ async fn calibration_promote_bootstrap( while tokio::time::Instant::now() < interval_deadline { let observed = { let s = state.read().await; + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; if s.calibration_model_id.as_deref() != Some(expected_model_id.as_str()) - || s.bootstrap_baseline_active + || !s.explicit_calibration_fresh_at(observed_at_unix_ms) { None } else { @@ -6581,8 +6705,9 @@ async fn calibration_promote_bootstrap( } let mut s = state.write().await; + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; if s.calibration_model_id.as_deref() != Some(expected_model_id.as_str()) - || s.bootstrap_baseline_active + || !s.explicit_calibration_fresh_at(observed_at_unix_ms) { return Json(serde_json::json!({ "success": false, @@ -6661,7 +6786,11 @@ async fn calibration_refine_bootstrap( let (expected_model_id, expected_digest, source_node_id) = { let s = state.read().await; - let Some(metadata) = s.bootstrap_baseline.as_ref().filter(|_| s.bootstrap_baseline_active) + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; + let Some(metadata) = s + .bootstrap_baseline + .as_ref() + .filter(|_| s.bootstrap_baseline_active_at(observed_at_unix_ms)) else { return Json(serde_json::json!({ "success": false, @@ -6706,7 +6835,8 @@ async fn calibration_refine_bootstrap( while tokio::time::Instant::now() < interval_deadline { observed = { let s = state.read().await; - let identity_matches = s.bootstrap_baseline_active + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; + let identity_matches = s.bootstrap_baseline_active_at(observed_at_unix_ms) && s.bootstrap_baseline.as_ref().is_some_and(|metadata| { metadata.source_model_id == expected_model_id && metadata.content_sha256 == expected_digest @@ -6771,7 +6901,8 @@ async fn calibration_refine_bootstrap( } let mut s = state.write().await; - let identity_matches = s.bootstrap_baseline_active + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; + let identity_matches = s.bootstrap_baseline_active_at(observed_at_unix_ms) && s.bootstrap_baseline.as_ref().is_some_and(|metadata| { metadata.source_model_id == expected_model_id && metadata.content_sha256 == expected_digest @@ -6951,11 +7082,10 @@ fn chrono_timestamp() -> u64 { async fn vital_signs_endpoint(State(state): State) -> Json { let s = state.read().await; - let person_count = s.person_count(); - let explicit_calibration_fresh = !s.bootstrap_baseline_active - && s.field_model - .as_ref() - .is_some_and(|model| matches!(model.status(), CalibrationStatus::Fresh)); + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; + let person_count = s.person_count_at(observed_at_unix_ms); + let explicit_calibration_fresh = + s.explicit_calibration_fresh_at(observed_at_unix_ms); let published = vitals_for_publication( &s.latest_vitals, explicit_calibration_fresh, @@ -7036,11 +7166,9 @@ async fn edge_registry_endpoint( /// GET /api/v1/edge-vitals — latest edge vitals from ESP32 (ADR-039). async fn edge_vitals_endpoint(State(state): State) -> Json { let s = state.read().await; - let explicit_single_occupant = !s.bootstrap_baseline_active - && s.field_model - .as_ref() - .is_some_and(|model| matches!(model.status(), CalibrationStatus::Fresh)) - && s.person_count() == 1; + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; + let explicit_single_occupant = s.explicit_calibration_fresh_at(observed_at_unix_ms) + && s.person_count_at(observed_at_unix_ms) == 1; if !explicit_single_occupant { return Json(serde_json::json!({ "status": "abstained", @@ -7654,7 +7782,10 @@ async fn udp_receiver_task( // #803: don't let the saturating activity score // discard count-aware per-node estimates. let count = - aggregate_person_count(s.person_count(), &s.node_states); + aggregate_person_count( + s.person_count_at(observed_at_unix_ms), + &s.node_states, + ); s.prev_person_count = count; count.max(1) // presence=true => at least 1 } @@ -7791,10 +7922,8 @@ async fn udp_receiver_task( heartbeat_confidence: if vitals.presence { 0.7 } else { 0.0 }, signal_quality: vitals.presence_score as f64, }; - let explicit_calibration_fresh = !s.bootstrap_baseline_active - && s.field_model.as_ref().is_some_and(|model| { - matches!(model.status(), CalibrationStatus::Fresh) - }); + let explicit_calibration_fresh = + s.explicit_calibration_fresh_at(observed_at_unix_ms); let published_vitals = vitals_for_publication( &vital_candidates, explicit_calibration_fresh, @@ -8201,7 +8330,10 @@ async fn udp_receiver_task( // #803: don't let the saturating activity score // discard count-aware per-node estimates. let count = - aggregate_person_count(s.person_count(), &s.node_states); + aggregate_person_count( + s.person_count_at(observed_at_unix_ms), + &s.node_states, + ); s.prev_person_count = count; count.max(1) } @@ -8314,10 +8446,8 @@ async fn udp_receiver_task( } else { debounce_room_classification(&mut s, &room_inference) }; - let explicit_calibration_fresh = !s.bootstrap_baseline_active - && s.field_model.as_ref().is_some_and(|model| { - matches!(model.status(), CalibrationStatus::Fresh) - }); + let explicit_calibration_fresh = + s.explicit_calibration_fresh_at(observed_at_unix_ms); let published_vitals = vitals_for_publication( &vitals, explicit_calibration_fresh, diff --git a/v2/crates/wifi-densepose-sensing-server/src/types.rs b/v2/crates/wifi-densepose-sensing-server/src/types.rs index da2630497..e86ceaba9 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/types.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/types.rs @@ -544,6 +544,10 @@ impl AppStateInner { pub fn person_count(&self) -> usize { use crate::csi::score_to_person_count; use crate::field_bridge; + let observed_at_us = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_micros() as u64) + .unwrap_or(0); match self.field_model.as_ref() { Some(fm) => { let history = if !self.frame_history.is_empty() { @@ -559,6 +563,7 @@ impl AppStateInner { field_bridge::occupancy_or_fallback( fm, history, + observed_at_us, self.smoothed_person_score, self.prev_person_count, ) From 2deb6d2d64ee2bad5236f260d55329524f0b3429 Mon Sep 17 00:00:00 2001 From: ruv Date: Sun, 6 Sep 2026 10:11:14 -0400 Subject: [PATCH 23/27] fix(calibration): require unique raw CSI samples --- ...57-raw-csi-calibration-sample-integrity.md | 89 +++++ .../src/field_bridge.rs | 56 +-- .../wifi-densepose-sensing-server/src/main.rs | 320 +++++++++++++++--- 3 files changed, 388 insertions(+), 77 deletions(-) create mode 100644 docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md diff --git a/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md b/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md new file mode 100644 index 000000000..e4eef61f3 --- /dev/null +++ b/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md @@ -0,0 +1,89 @@ +# ADR 357: Raw CSI calibration sample integrity + +Status: Accepted + +Date: 2026-09-06 + +## Context + +The empty room field model requires 1,000 accepted observations over at least +600 seconds. The sensing server previously invoked calibration from both the +raw CSI path and the one second edge vital path. Edge vital packets did not add +a CSI observation, so that path repeatedly fed the existing history tail. A +counter could therefore satisfy the frame gate without representing 1,000 +independent CSI packets. + +Node liveness also combined edge vital and CSI traffic. A source emitting only +edge vital summaries could appear live enough to start calibration even though +it had no fresh raw CSI. Finally, cloning the complete per node history before +each feed copied as much as 204,800 bytes for a 100 frame, 256 subcarrier +window even though calibration consumes only the current observation. + +The earlier single installation result recorded in ADR 355 predates this +admission boundary. Its frame count is not accepted as evidence of independent +CSI coverage. It remains useful only as diagnosis of the failure mode and must +not be promoted as a production bootstrap prior. + +## Decision + +1. Only a grid admitted raw CSI packet may advance field calibration. Edge + vital packets update their own health and presentation state but never feed + the field model. +2. Each calibration session stores the last admitted wrapping sequence for + every contributing node. Duplicate sequences are ignored. Normal forward + progress and the `u32::MAX` to zero wrap are accepted. +3. A backward or ambiguous half range sequence latches that node as faulted for + the remainder of the session. Status exposes `sequence_fault_node_ids`, and + finalization returns `calibration_sequence_discontinuity`. The operator must + cancel and restart the empty room capture. +4. A requested source may start calibration only when it has a grid admitted + raw CSI packet newer than five seconds. General node liveness is not enough. + The node endpoint reports both `csi_status` and `csi_last_seen_ms` so clients + can explain the gate before the operator begins. +5. Start, cancel, reset, and process initialization clear sequence admission + state. A failed feed does not advance the sequence cursor, so the same + packet may be retried after a transient model input failure. +6. The field bridge receives the current amplitude slice rather than a cloned + history. Canonical grid normalization remains unchanged. + +## Consequences + +Calibration frame count now means unique, forward raw CSI observations within +one uninterrupted sequence epoch. A vitals only node cannot produce a +bootstrap model. + +One legitimately reordered UDP packet invalidates a ten minute capture. This +is an intentional fail closed tradeoff because the current wire format has no +cryptographic boot epoch and cannot distinguish reordering from a device +restart. A future firmware protocol may add a signed boot identifier and a +bounded reordering window. + +No new raw CSI is persisted. Sequence cursors and fault markers remain bounded +in memory to active calibration sources. The persisted bootstrap format and +its negative only authority are unchanged. + +## Implementation + +1. `NodeState` records a separate accepted CSI arrival clock. +2. `AppStateInner::maybe_feed_calibration_frame` owns source binding, sequence + ordering, fault latching, model feed, and cursor commit. +3. `field_bridge::maybe_feed_calibration` accepts exactly one current amplitude + slice and rejects an empty observation. +4. Calibration start, status, stop, cancel, reset, and node status expose and + enforce the new admission contract. + +## Acceptance test + +Unit tests must prove first packet acceptance, forward progress, duplicate +rejection, source isolation, maximum counter wrap, empty input retry, backward +sequence latching, half range rejection, session clear, status visibility, and +typed stop refusal. + +On physical hardware, begin a Node 5 empty room capture and observe a fresh +sixty second interval. Calibration frame growth must match unique grid admitted +Node 5 CSI sequences. Edge vital packets alone must produce zero growth. The +source must retain `csi_status=active`, sequence faults must remain empty, and +numeric vital output must remain absent. Complete both the 1,000 frame and 600 +second gates, then run the twelve sample server controlled bootstrap holdout. +Do not claim human detection improvement until a separate consented occupied +holdout demonstrates retained recall. diff --git a/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs b/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs index 21c4ee461..4ba83b0e7 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/field_bridge.rs @@ -283,31 +283,36 @@ pub fn calibrated_occupancy( }) } -/// Feed the latest frame to the FieldModel during calibration collection. +/// Feed the current CSI frame to the FieldModel during calibration. /// /// Acts while the model is `Uncalibrated` or `Collecting`. The first fed frame /// flips a freshly-started (`Uncalibrated`) model to `Collecting` inside /// `feed_calibration`; without accepting the `Uncalibrated` state here the two /// gates deadlock and the frame count never leaves 0 (calibration/start yields -/// an `Uncalibrated` model that nothing would ever advance). Wraps the latest -/// frame as a single-link observation (n_links=1) and feeds it. -pub fn maybe_feed_calibration(field: &mut FieldModel, frame_history: &VecDeque>) { +/// an `Uncalibrated` model that nothing would ever advance). Edge-vitals +/// packets have no CSI observation and must never call this helper. The caller +/// owns the stateful sequence admission gate. Wraps the current frame as a +/// single-link observation (n_links=1) and feeds it. +pub fn maybe_feed_calibration(field: &mut FieldModel, amplitudes: &[f64]) -> bool { if !matches!( field.status(), CalibrationStatus::Uncalibrated | CalibrationStatus::Collecting - ) { - return; + ) || amplitudes.is_empty() + { + return false; } - if let Some(latest) = frame_history.back() { - // Resample the raw amplitude vector onto the FieldModel's canonical - // 56-tone grid before feeding. Real HT40 nodes stream 128-wide frames; - // feeding those raw made every `feed_calibration` fail DimensionMismatch - // (swallowed at debug level), pinning frame_count at 0 even after the - // status-gate deadlock was fixed. Single-link observation: [1][56]. - let canonical = CALIB_NORMALIZER.resample_to_canonical(latest); - let observations = vec![canonical]; - if let Err(e) = field.feed_calibration(&observations) { + // Resample the raw amplitude vector onto the FieldModel's canonical + // 56-tone grid before feeding. Real HT40 nodes stream 128-wide frames; + // feeding those raw made every `feed_calibration` fail DimensionMismatch + // (swallowed at debug level), pinning frame_count at 0 even after the + // status-gate deadlock was fixed. Single-link observation: [1][56]. + let canonical = CALIB_NORMALIZER.resample_to_canonical(amplitudes); + let observations = vec![canonical]; + match field.feed_calibration(&observations) { + Ok(()) => true, + Err(e) => { tracing::debug!("FieldModel calibration feed: {e}"); + false } } } @@ -418,10 +423,7 @@ mod tests { // n_subcarriers defaults to 56; one single-link frame of that width. let frame = vec![0.5_f64; 56]; - let mut history: VecDeque> = VecDeque::new(); - history.push_back(frame); - - maybe_feed_calibration(&mut field, &history); + assert!(maybe_feed_calibration(&mut field, &frame)); assert_eq!( field.status(), @@ -434,8 +436,8 @@ mod tests { "frame count must advance past 0" ); - // Subsequent frames keep accumulating while Collecting. - maybe_feed_calibration(&mut field, &history); + // A subsequent unique frame keeps accumulating while Collecting. + assert!(maybe_feed_calibration(&mut field, &frame)); assert_eq!(field.calibration_frame_count(), 2); } @@ -450,10 +452,7 @@ mod tests { // 128-wide frame (HT40), NOT the model's 56 — would DimensionMismatch raw. let wide = vec![0.5_f64; 128]; - let mut history: VecDeque> = VecDeque::new(); - history.push_back(wide); - - maybe_feed_calibration(&mut field, &history); + assert!(maybe_feed_calibration(&mut field, &wide)); assert_eq!( field.status(), @@ -467,6 +466,13 @@ mod tests { ); } + #[test] + fn maybe_feed_calibration_rejects_an_empty_observation() { + let mut field = FieldModel::new(single_link_config()).expect("field model"); + assert!(!maybe_feed_calibration(&mut field, &[])); + assert_eq!(field.calibration_frame_count(), 0); + } + #[test] fn calibrated_occupancy_is_fresh_model_only_and_normalizes_runtime_frames() { let field = fresh_test_model(); diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index c2f8790dd..8b2bc921b 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -849,6 +849,9 @@ struct NodeState { latest_sync: Option, /// Last time a sync packet from this node was received (for staleness). latest_sync_at: Option, + /// Arrival time of the newest grid-admitted raw CSI frame. Edge-vitals + /// packets intentionally do not refresh this clock. + latest_accepted_csi_at: Option, /// Sequence number of the newest CSI frame admitted to `frame_history`. /// Kept alongside the history so multistatic fusion can timestamp the /// exact sample it consumes, rather than the host's UDP arrival time. @@ -1161,6 +1164,7 @@ impl NodeState { sync_valid: bool, now: std::time::Instant, ) -> bool { + self.latest_accepted_csi_at = Some(now); self.latest_csi_sequence = Some(sequence); self.latest_csi_sync_valid = sync_valid; self.observe_csi_frame_arrival(now) @@ -1190,6 +1194,7 @@ impl NodeState { edge_vitals: None, latest_sync: None, latest_sync_at: None, + latest_accepted_csi_at: None, latest_csi_sequence: None, latest_csi_sync_valid: false, csi_fps_ema: 20.0, @@ -1627,6 +1632,12 @@ struct AppStateInner { calibration_model_id: Option, /// Nodes that actually contributed frames to the current calibration. calibration_source_node_ids: std::collections::BTreeSet, + /// Last admitted raw CSI sequence per contributing node. Edge-vitals + /// packets do not carry a new CSI observation and cannot advance this map. + calibration_last_sequences: HashMap, + /// Nodes whose sequence moved backward during the current session. Without + /// a wire boot epoch, the session must stay failed closed after regression. + calibration_sequence_fault_node_ids: std::collections::BTreeSet, // ── ADR-044 §5.2: adaptive rolling-p95 normalization ───────────────────── /// Rolling P95 of `FeatureInfo.variance` over the last ~30 s (600 frames @ 20 Hz). pub(crate) p95_variance: RollingP95, @@ -1729,7 +1740,74 @@ mod adr323_pose_physics_http_tests { /// If no ESP32 frame arrives within this duration, source reverts to offline. const ESP32_OFFLINE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CalibrationSequenceOrder { + First, + Forward, + Duplicate, + Regression, +} + +fn calibration_sequence_order(previous: Option, sequence: u32) -> CalibrationSequenceOrder { + let Some(previous) = previous else { + return CalibrationSequenceOrder::First; + }; + let delta = sequence.wrapping_sub(previous); + if delta == 0 { + CalibrationSequenceOrder::Duplicate + } else if delta < (1_u32 << 31) { + CalibrationSequenceOrder::Forward + } else { + CalibrationSequenceOrder::Regression + } +} + impl AppStateInner { + /// Admit at most one raw CSI observation per forward node sequence into + /// the current calibration session. This stateful boundary prevents a + /// caller from advancing the model with a replayed history tail. + fn maybe_feed_calibration_frame( + &mut self, + node_id: u8, + sequence: u32, + amplitudes: &[f64], + ) -> bool { + if !calibration_source_accepts(&self.calibration_source_node_ids, node_id) + || self.calibration_sequence_fault_node_ids.contains(&node_id) + { + return false; + } + let previous_sequence = self.calibration_last_sequences.get(&node_id).copied(); + match calibration_sequence_order(previous_sequence, sequence) { + CalibrationSequenceOrder::Duplicate => return false, + CalibrationSequenceOrder::Regression => { + self.calibration_sequence_fault_node_ids.insert(node_id); + warn!( + node_id, + previous_sequence, + sequence, + "Calibration source sequence regressed; restart the empty-room capture" + ); + return false; + } + CalibrationSequenceOrder::First | CalibrationSequenceOrder::Forward => {} + } + let accepted = self + .field_model + .as_mut() + .is_some_and(|field| field_bridge::maybe_feed_calibration(field, amplitudes)); + if accepted { + self.calibration_source_node_ids.insert(node_id); + self.calibration_last_sequences.insert(node_id, sequence); + } + accepted + } + + fn clear_calibration_sequence_state(&mut self) { + self.calibration_last_sequences.clear(); + self.calibration_sequence_fault_node_ids.clear(); + } + fn field_model_status_at(&self, observed_at_unix_ms: u64) -> Option { self.field_model.as_ref().map(|model| { field_bridge::calibration_status_at(model, observed_at_unix_ms.saturating_mul(1_000)) @@ -1972,6 +2050,8 @@ impl AppStateInner { bootstrap_baseline_active: false, calibration_model_id: None, calibration_source_node_ids: std::collections::BTreeSet::new(), + calibration_last_sequences: HashMap::new(), + calibration_sequence_fault_node_ids: std::collections::BTreeSet::new(), p95_variance: RollingP95::new(600, 60), p95_motion_band_power: RollingP95::new(600, 60), p95_spectral_power: RollingP95::new(600, 60), @@ -6288,6 +6368,11 @@ fn calibration_source_accepts( source_node_ids.is_empty() || source_node_ids.contains(&node_id) } +fn calibration_source_has_fresh_csi(node: &NodeState, now: std::time::Instant) -> bool { + node.latest_accepted_csi_at + .is_some_and(|seen| now.saturating_duration_since(seen) < ESP32_OFFLINE_TIMEOUT) +} + #[cfg(test)] mod bootstrap_vital_publication_tests { use super::*; @@ -6309,6 +6394,148 @@ mod bootstrap_vital_publication_tests { assert!(!calibration_source_accepts(&sources, 7)); } + #[test] + fn edge_vitals_liveness_cannot_authorize_raw_csi_calibration() { + let now = std::time::Instant::now(); + let mut node = NodeState::new(); + node.last_frame_time = Some(now); + assert!(!calibration_source_has_fresh_csi(&node, now)); + + node.observe_accepted_csi_frame(9, false, now); + assert!(calibration_source_has_fresh_csi(&node, now)); + assert!(!calibration_source_has_fresh_csi( + &node, + now + ESP32_OFFLINE_TIMEOUT + )); + } + + #[test] + fn calibration_session_counts_each_unique_bound_csi_sequence_once() { + let mut state = AppStateInner::minimal(); + state.field_model = Some( + FieldModel::new(field_bridge::single_link_config()).expect("field model"), + ); + state.calibration_source_node_ids.insert(5); + + let first = vec![0.25; 56]; + assert!(state.maybe_feed_calibration_frame(5, 41, &first)); + assert!(!state.maybe_feed_calibration_frame(5, 41, &first)); + + let second = vec![0.5; 56]; + assert!(state.maybe_feed_calibration_frame(5, 42, &second)); + assert!(!state.maybe_feed_calibration_frame(3, 43, &second)); + + let model = state.field_model.as_ref().expect("field model remains active"); + assert_eq!(model.calibration_frame_count(), 2); + assert_eq!(state.calibration_last_sequences.get(&5), Some(&42)); + assert!(!state.calibration_last_sequences.contains_key(&3)); + } + + #[test] + fn calibration_sequence_regression_latches_session_failure() { + let mut state = AppStateInner::minimal(); + state.field_model = Some( + FieldModel::new(field_bridge::single_link_config()).expect("field model"), + ); + state.calibration_source_node_ids.insert(5); + let frame = vec![0.25; 56]; + + assert!(state.maybe_feed_calibration_frame(5, 10, &frame)); + assert!(state.maybe_feed_calibration_frame(5, 11, &frame)); + assert!(!state.maybe_feed_calibration_frame(5, 10, &frame)); + assert!(!state.maybe_feed_calibration_frame(5, 12, &frame)); + assert!(state.calibration_sequence_fault_node_ids.contains(&5)); + assert_eq!( + state + .field_model + .as_ref() + .expect("field model") + .calibration_frame_count(), + 2 + ); + + state.clear_calibration_sequence_state(); + assert!(state.calibration_sequence_fault_node_ids.is_empty()); + assert!(state.calibration_last_sequences.is_empty()); + } + + #[test] + fn calibration_sequence_wraps_and_failed_empty_input_can_retry() { + let mut state = AppStateInner::minimal(); + state.field_model = Some( + FieldModel::new(field_bridge::single_link_config()).expect("field model"), + ); + state.calibration_source_node_ids.insert(5); + let frame = vec![0.25; 56]; + + assert!(!state.maybe_feed_calibration_frame(5, u32::MAX, &[])); + assert!(state.calibration_last_sequences.is_empty()); + assert!(state.maybe_feed_calibration_frame(5, u32::MAX, &frame)); + assert!(state.maybe_feed_calibration_frame(5, 0, &frame)); + assert!(!state.maybe_feed_calibration_frame(5, 0, &frame)); + assert!(state.maybe_feed_calibration_frame(5, 1, &frame)); + assert_eq!( + state + .field_model + .as_ref() + .expect("field model") + .calibration_frame_count(), + 3 + ); + } + + #[test] + fn calibration_sequence_order_rejects_backward_and_half_range() { + assert_eq!( + calibration_sequence_order(None, 7), + CalibrationSequenceOrder::First + ); + assert_eq!( + calibration_sequence_order(Some(7), 8), + CalibrationSequenceOrder::Forward + ); + assert_eq!( + calibration_sequence_order(Some(8), 8), + CalibrationSequenceOrder::Duplicate + ); + assert_eq!( + calibration_sequence_order(Some(8), 7), + CalibrationSequenceOrder::Regression + ); + assert_eq!( + calibration_sequence_order(Some(0), 1_u32 << 31), + CalibrationSequenceOrder::Regression + ); + assert_eq!( + calibration_sequence_order(Some(u32::MAX), 0), + CalibrationSequenceOrder::Forward + ); + } + + #[tokio::test] + async fn calibration_status_and_stop_fail_closed_after_sequence_regression() { + let mut inner = AppStateInner::minimal(); + inner.field_model = Some( + FieldModel::new(field_bridge::single_link_config()).expect("field model"), + ); + inner.calibration_model_id = Some("cal-model-test".to_string()); + inner.calibration_source_node_ids.insert(5); + inner.calibration_sequence_fault_node_ids.insert(5); + let state = Arc::new(RwLock::new(inner)); + + let Json(status) = calibration_status(State(state.clone())).await; + assert_eq!(status["sequence_fault_node_ids"], serde_json::json!([5])); + + let Json(stopped) = calibration_stop(State(state)).await; + assert_eq!(stopped["success"], false); + assert_eq!( + stopped["error_code"], + "calibration_sequence_discontinuity" + ); + assert_eq!(stopped["model_id"], "cal-model-test"); + assert_eq!(stopped["source_node_ids"], serde_json::json!([5])); + } + fn strong_candidates() -> VitalSigns { VitalSigns { breathing_rate_bpm: Some(15.0), @@ -6420,15 +6647,15 @@ async fn calibration_start( let mut s = state.write().await; if let Some(source_node_id) = query.source_node_id { let now = std::time::Instant::now(); - let source_is_live = s.node_states.get(&source_node_id).is_some_and(|node| { - node.last_frame_time - .is_some_and(|seen| now.saturating_duration_since(seen) < ESP32_OFFLINE_TIMEOUT) - }); + let source_is_live = s + .node_states + .get(&source_node_id) + .is_some_and(|node| calibration_source_has_fresh_csi(node, now)); if !source_is_live { return Json(serde_json::json!({ "success": false, "error_code": "calibration_source_unavailable", - "error": "The requested calibration source is missing or stale.", + "error": "The requested calibration source is missing or has no fresh raw CSI.", "source_node_id": source_node_id, })); } @@ -6463,6 +6690,7 @@ async fn calibration_start( s.field_model = Some(fm); s.calibration_model_id = Some(opaque_calibration_model_id()); s.calibration_source_node_ids.clear(); + s.clear_calibration_sequence_state(); if let Some(source_node_id) = query.source_node_id { s.calibration_source_node_ids.insert(source_node_id); } @@ -6482,6 +6710,16 @@ async fn calibration_stop(State(state): State) -> Json = s.calibration_source_node_ids.iter().copied().collect(); + if !s.calibration_sequence_fault_node_ids.is_empty() { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_sequence_discontinuity", + "error": "Raw CSI sequence moved backward during capture. Cancel and restart the empty-room calibration.", + "model_id": model_id, + "source_node_ids": source_node_ids, + "sequence_fault_node_ids": s.calibration_sequence_fault_node_ids, + })); + } if let Some(ref mut fm) = s.field_model { // Guard: finalizing before enough empty-room frames have accumulated // is a client-side sequencing error, not a server fault. Return a @@ -6585,6 +6823,7 @@ async fn calibration_status(State(state): State) -> Json) -> Json) -> Json) -> Json 5000; let status = if stale { "stale" } else { "active" }; + let csi_status = if csi_elapsed_ms.is_some_and(|age| { + age < ESP32_OFFLINE_TIMEOUT.as_millis() as u64 + }) { + "active" + } else { + "stale" + }; let rssi = ns.rssi_history.back().copied().unwrap_or(-90.0); let bootstrap_empty = s.bootstrap_baseline_active && s.bootstrap_baseline.as_ref().is_some_and(|metadata| { @@ -7458,6 +7709,9 @@ async fn nodes_endpoint(State(state): State) -> Json before - } else { - false - } - } else { - false - }; - if accepted { - s.calibration_source_node_ids.insert(node_id); - } - } - // Build nodes array with all active nodes. let resolved_positions = node_positions_by_active_id(&s.node_positions_config, &s.node_states, now); @@ -8362,29 +8592,13 @@ async fn udp_receiver_task( sref.engine_bridge.observe_cycle(&sref.node_states, now_ms); } - // Feed field model calibration if active (use per-node history for ESP32). - if let Some(frame_history) = s - .node_states - .get(&node_id) - .map(|ns| ns.frame_history.clone()) - { - let source_allowed = - calibration_source_accepts(&s.calibration_source_node_ids, node_id); - let accepted = if source_allowed { - if let Some(ref mut fm) = s.field_model { - let before = fm.calibration_frame_count(); - field_bridge::maybe_feed_calibration(fm, &frame_history); - fm.calibration_frame_count() > before - } else { - false - } - } else { - false - }; - if accepted { - s.calibration_source_node_ids.insert(node_id); - } - } + // Feed only this grid-admitted raw CSI observation. The + // session boundary deduplicates its wrapping sequence. + s.maybe_feed_calibration_frame( + node_id, + frame.sequence, + &frame.amplitudes, + ); // Build nodes array with all active nodes. ADR-141 output // gating (review finding 1c): when the governed engine @@ -10157,6 +10371,8 @@ async fn main() { bootstrap_baseline_active, calibration_model_id: args.calibrate.then(opaque_calibration_model_id), calibration_source_node_ids: std::collections::BTreeSet::new(), + calibration_last_sequences: HashMap::new(), + calibration_sequence_fault_node_ids: std::collections::BTreeSet::new(), // ADR-044 §5.2: rolling-P95 over ~30 s at 20 Hz; warm-up after 60 samples. p95_variance: RollingP95::new(600, 60), p95_motion_band_power: RollingP95::new(600, 60), From 224c9eb6710426e182e630be8eb55a39535e749a Mon Sep 17 00:00:00 2001 From: ruv Date: Sun, 6 Sep 2026 10:15:27 -0400 Subject: [PATCH 24/27] feat(calibration): expose CSI admission receipts --- docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md | 4 +++- v2/crates/wifi-densepose-sensing-server/src/main.rs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md b/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md index e4eef61f3..74e205db7 100644 --- a/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md +++ b/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md @@ -39,7 +39,9 @@ not be promoted as a production bootstrap prior. 4. A requested source may start calibration only when it has a grid admitted raw CSI packet newer than five seconds. General node liveness is not enough. The node endpoint reports both `csi_status` and `csi_last_seen_ms` so clients - can explain the gate before the operator begins. + can explain the gate before the operator begins. Calibration status reports + `last_sequence_by_node` so admission growth and sequence identity can be + observed atomically without exposing CSI values. 5. Start, cancel, reset, and process initialization clear sequence admission state. A failed feed does not advance the sequence cursor, so the same packet may be retried after a transient model input failure. diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 8b2bc921b..3947c2186 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -6520,10 +6520,12 @@ mod bootstrap_vital_publication_tests { ); inner.calibration_model_id = Some("cal-model-test".to_string()); inner.calibration_source_node_ids.insert(5); + inner.calibration_last_sequences.insert(5, 41); inner.calibration_sequence_fault_node_ids.insert(5); let state = Arc::new(RwLock::new(inner)); let Json(status) = calibration_status(State(state.clone())).await; + assert_eq!(status["last_sequence_by_node"]["5"], 41); assert_eq!(status["sequence_fault_node_ids"], serde_json::json!([5])); let Json(stopped) = calibration_stop(State(state)).await; @@ -6823,6 +6825,7 @@ async fn calibration_status(State(state): State) -> Json Date: Sun, 6 Sep 2026 10:38:55 -0400 Subject: [PATCH 25/27] fix(calibration): tolerate measured UDP reordering --- ...57-raw-csi-calibration-sample-integrity.md | 44 +++--- .../wifi-densepose-sensing-server/src/main.rs | 126 +++++++++++++++--- 2 files changed, 138 insertions(+), 32 deletions(-) diff --git a/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md b/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md index 74e205db7..2bf39cf0f 100644 --- a/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md +++ b/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md @@ -32,9 +32,13 @@ not be promoted as a production bootstrap prior. 2. Each calibration session stores the last admitted wrapping sequence for every contributing node. Duplicate sequences are ignored. Normal forward progress and the `u32::MAX` to zero wrap are accepted. -3. A backward or ambiguous half range sequence latches that node as faulted for - the remainder of the session. Status exposes `sequence_fault_node_ids`, and - finalization returns `calibration_sequence_discontinuity`. The operator must +3. A packet one through four sequence values behind the current high water mark + is bounded UDP reordering. It is recorded and dropped without feeding the + model or moving the high water mark. A backward depth above four, or an + ambiguous half range sequence, latches that node as faulted for the remainder + of the session. Status exposes the policy, reorder window, per node reorder + count, maximum observed depth, and `sequence_fault_node_ids`. Finalization + returns `calibration_sequence_discontinuity` after a fault. The operator must cancel and restart the empty room capture. 4. A requested source may start calibration only when it has a grid admitted raw CSI packet newer than five seconds. General node liveness is not enough. @@ -52,13 +56,19 @@ not be promoted as a production bootstrap prior. Calibration frame count now means unique, forward raw CSI observations within one uninterrupted sequence epoch. A vitals only node cannot produce a -bootstrap model. +bootstrap model. Late packets never become calibration evidence. -One legitimately reordered UDP packet invalidates a ten minute capture. This -is an intentional fail closed tradeoff because the current wire format has no -cryptographic boot epoch and cannot distinguish reordering from a device -restart. A future firmware protocol may add a signed boot identifier and a -bounded reordering window. +A physical Node 5 capture observed sequence 4,119,566 followed by 4,119,563, +4,119,564, and 4,119,565 in one receive batch, then normal forward progress. +The window of four covers that measured depth of three with one frame of +margin. At the firmware acceptance ceiling of 50 Hz, the window represents no +more than 80 milliseconds. Larger regressions remain failed closed. + +The current wire format still has no authenticated boot epoch or stable device +identity. A restart very early in a session or a same endpoint adversary cannot +be distinguished cryptographically. A future ADR 018 revision must carry an +authenticated device identifier and a random per boot identifier before the +server claims exact restart or identity collision detection. No new raw CSI is persisted. Sequence cursors and fault markers remain bounded in memory to active calibration sources. The persisted bootstrap format and @@ -77,15 +87,17 @@ its negative only authority are unchanged. ## Acceptance test Unit tests must prove first packet acceptance, forward progress, duplicate -rejection, source isolation, maximum counter wrap, empty input retry, backward -sequence latching, half range rejection, session clear, status visibility, and -typed stop refusal. +rejection, source isolation, maximum counter wrap, empty input retry, the +measured depth three reorder pattern without a model feed, depth four +acceptance, depth five latching, half range rejection, session clear, receipt +visibility, and typed stop refusal. On physical hardware, begin a Node 5 empty room capture and observe a fresh sixty second interval. Calibration frame growth must match unique grid admitted Node 5 CSI sequences. Edge vital packets alone must produce zero growth. The source must retain `csi_status=active`, sequence faults must remain empty, and -numeric vital output must remain absent. Complete both the 1,000 frame and 600 -second gates, then run the twelve sample server controlled bootstrap holdout. -Do not claim human detection improvement until a separate consented occupied -holdout demonstrates retained recall. +numeric vital output must remain absent. Any bounded late packet must appear in +the reorder receipts while leaving the model count unchanged. Complete both the +1,000 frame and 600 second gates, then run the twelve sample server controlled +bootstrap holdout. Do not claim human detection improvement until a separate +consented occupied holdout demonstrates retained recall. diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 3947c2186..68f6ada78 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -1635,8 +1635,13 @@ struct AppStateInner { /// Last admitted raw CSI sequence per contributing node. Edge-vitals /// packets do not carry a new CSI observation and cannot advance this map. calibration_last_sequences: HashMap, - /// Nodes whose sequence moved backward during the current session. Without - /// a wire boot epoch, the session must stay failed closed after regression. + /// Late packets inside the bounded UDP reorder window are ignored rather + /// than counted. These receipts make the transport behavior auditable. + calibration_reordered_packets: HashMap, + calibration_max_reorder_depth: HashMap, + /// Nodes whose sequence moved backward beyond the bounded UDP reorder + /// window during the current session. Without a wire boot epoch, the + /// session must stay failed closed after a material discontinuity. calibration_sequence_fault_node_ids: std::collections::BTreeSet, // ── ADR-044 §5.2: adaptive rolling-p95 normalization ───────────────────── /// Rolling P95 of `FeatureInfo.variance` over the last ~30 s (600 frames @ 20 Hz). @@ -1745,9 +1750,16 @@ enum CalibrationSequenceOrder { First, Forward, Duplicate, - Regression, + Reordered(u32), + Discontinuity, } +/// Live Node 5 evidence observed a same-source receive batch ordered as high +/// water mark, then minus three, minus two, minus one. Four covers that +/// measured UDP reorder with one frame of margin. At the firmware's 50 Hz +/// ceiling it spans at most 80 ms; larger backward jumps stay failed closed. +const CALIBRATION_SEQUENCE_REORDER_WINDOW: u32 = 4; + fn calibration_sequence_order(previous: Option, sequence: u32) -> CalibrationSequenceOrder { let Some(previous) = previous else { return CalibrationSequenceOrder::First; @@ -1758,7 +1770,12 @@ fn calibration_sequence_order(previous: Option, sequence: u32) -> Calibrati } else if delta < (1_u32 << 31) { CalibrationSequenceOrder::Forward } else { - CalibrationSequenceOrder::Regression + let reorder_depth = previous.wrapping_sub(sequence); + if reorder_depth <= CALIBRATION_SEQUENCE_REORDER_WINDOW { + CalibrationSequenceOrder::Reordered(reorder_depth) + } else { + CalibrationSequenceOrder::Discontinuity + } } } @@ -1780,13 +1797,31 @@ impl AppStateInner { let previous_sequence = self.calibration_last_sequences.get(&node_id).copied(); match calibration_sequence_order(previous_sequence, sequence) { CalibrationSequenceOrder::Duplicate => return false, - CalibrationSequenceOrder::Regression => { + CalibrationSequenceOrder::Reordered(depth) => { + *self + .calibration_reordered_packets + .entry(node_id) + .or_default() += 1; + self.calibration_max_reorder_depth + .entry(node_id) + .and_modify(|observed| *observed = (*observed).max(depth)) + .or_insert(depth); + debug!( + node_id, + previous_sequence, + sequence, + depth, + "Ignored bounded late CSI packet during calibration" + ); + return false; + } + CalibrationSequenceOrder::Discontinuity => { self.calibration_sequence_fault_node_ids.insert(node_id); warn!( node_id, previous_sequence, sequence, - "Calibration source sequence regressed; restart the empty-room capture" + "Calibration source sequence discontinuity exceeded reorder window; restart the empty-room capture" ); return false; } @@ -1805,6 +1840,8 @@ impl AppStateInner { fn clear_calibration_sequence_state(&mut self) { self.calibration_last_sequences.clear(); + self.calibration_reordered_packets.clear(); + self.calibration_max_reorder_depth.clear(); self.calibration_sequence_fault_node_ids.clear(); } @@ -2051,6 +2088,8 @@ impl AppStateInner { calibration_model_id: None, calibration_source_node_ids: std::collections::BTreeSet::new(), calibration_last_sequences: HashMap::new(), + calibration_reordered_packets: HashMap::new(), + calibration_max_reorder_depth: HashMap::new(), calibration_sequence_fault_node_ids: std::collections::BTreeSet::new(), p95_variance: RollingP95::new(600, 60), p95_motion_band_power: RollingP95::new(600, 60), @@ -6432,7 +6471,7 @@ mod bootstrap_vital_publication_tests { } #[test] - fn calibration_sequence_regression_latches_session_failure() { + fn calibration_bounded_reorder_is_dropped_without_feeding_model() { let mut state = AppStateInner::minimal(); state.field_model = Some( FieldModel::new(field_bridge::single_link_config()).expect("field model"), @@ -6440,11 +6479,14 @@ mod bootstrap_vital_publication_tests { state.calibration_source_node_ids.insert(5); let frame = vec![0.25; 56]; - assert!(state.maybe_feed_calibration_frame(5, 10, &frame)); - assert!(state.maybe_feed_calibration_frame(5, 11, &frame)); - assert!(!state.maybe_feed_calibration_frame(5, 10, &frame)); - assert!(!state.maybe_feed_calibration_frame(5, 12, &frame)); - assert!(state.calibration_sequence_fault_node_ids.contains(&5)); + assert!(state.maybe_feed_calibration_frame(5, 4_119_566, &frame)); + assert!(!state.maybe_feed_calibration_frame(5, 4_119_563, &frame)); + assert!(!state.maybe_feed_calibration_frame(5, 4_119_564, &frame)); + assert!(!state.maybe_feed_calibration_frame(5, 4_119_565, &frame)); + assert!(state.maybe_feed_calibration_frame(5, 4_119_567, &frame)); + assert!(state.calibration_sequence_fault_node_ids.is_empty()); + assert_eq!(state.calibration_reordered_packets.get(&5), Some(&3)); + assert_eq!(state.calibration_max_reorder_depth.get(&5), Some(&3)); assert_eq!( state .field_model @@ -6453,10 +6495,35 @@ mod bootstrap_vital_publication_tests { .calibration_frame_count(), 2 ); + } + + #[test] + fn calibration_sequence_discontinuity_latches_session_failure() { + let mut state = AppStateInner::minimal(); + state.field_model = Some( + FieldModel::new(field_bridge::single_link_config()).expect("field model"), + ); + state.calibration_source_node_ids.insert(5); + let frame = vec![0.25; 56]; + + assert!(state.maybe_feed_calibration_frame(5, 100, &frame)); + assert!(!state.maybe_feed_calibration_frame(5, 95, &frame)); + assert!(!state.maybe_feed_calibration_frame(5, 101, &frame)); + assert!(state.calibration_sequence_fault_node_ids.contains(&5)); + assert_eq!( + state + .field_model + .as_ref() + .expect("field model") + .calibration_frame_count(), + 1 + ); state.clear_calibration_sequence_state(); assert!(state.calibration_sequence_fault_node_ids.is_empty()); assert!(state.calibration_last_sequences.is_empty()); + assert!(state.calibration_reordered_packets.is_empty()); + assert!(state.calibration_max_reorder_depth.is_empty()); } #[test] @@ -6485,7 +6552,7 @@ mod bootstrap_vital_publication_tests { } #[test] - fn calibration_sequence_order_rejects_backward_and_half_range() { + fn calibration_sequence_order_drops_bounded_late_and_rejects_discontinuity() { assert_eq!( calibration_sequence_order(None, 7), CalibrationSequenceOrder::First @@ -6500,11 +6567,19 @@ mod bootstrap_vital_publication_tests { ); assert_eq!( calibration_sequence_order(Some(8), 7), - CalibrationSequenceOrder::Regression + CalibrationSequenceOrder::Reordered(1) + ); + assert_eq!( + calibration_sequence_order(Some(8), 4), + CalibrationSequenceOrder::Reordered(4) + ); + assert_eq!( + calibration_sequence_order(Some(8), 3), + CalibrationSequenceOrder::Discontinuity ); assert_eq!( calibration_sequence_order(Some(0), 1_u32 << 31), - CalibrationSequenceOrder::Regression + CalibrationSequenceOrder::Discontinuity ); assert_eq!( calibration_sequence_order(Some(u32::MAX), 0), @@ -6521,11 +6596,16 @@ mod bootstrap_vital_publication_tests { inner.calibration_model_id = Some("cal-model-test".to_string()); inner.calibration_source_node_ids.insert(5); inner.calibration_last_sequences.insert(5, 41); + inner.calibration_reordered_packets.insert(5, 3); + inner.calibration_max_reorder_depth.insert(5, 3); inner.calibration_sequence_fault_node_ids.insert(5); let state = Arc::new(RwLock::new(inner)); let Json(status) = calibration_status(State(state.clone())).await; assert_eq!(status["last_sequence_by_node"]["5"], 41); + assert_eq!(status["reorder_window_sequences"], 4); + assert_eq!(status["reordered_packets_by_node"]["5"], 3); + assert_eq!(status["max_reorder_depth_by_node"]["5"], 3); assert_eq!(status["sequence_fault_node_ids"], serde_json::json!([5])); let Json(stopped) = calibration_stop(State(state)).await; @@ -6536,6 +6616,8 @@ mod bootstrap_vital_publication_tests { ); assert_eq!(stopped["model_id"], "cal-model-test"); assert_eq!(stopped["source_node_ids"], serde_json::json!([5])); + assert_eq!(stopped["reordered_packets_by_node"]["5"], 3); + assert_eq!(stopped["max_reorder_depth_by_node"]["5"], 3); } fn strong_candidates() -> VitalSigns { @@ -6712,13 +6794,17 @@ async fn calibration_stop(State(state): State) -> Json = s.calibration_source_node_ids.iter().copied().collect(); + let reordered_packets_by_node = s.calibration_reordered_packets.clone(); + let max_reorder_depth_by_node = s.calibration_max_reorder_depth.clone(); if !s.calibration_sequence_fault_node_ids.is_empty() { return Json(serde_json::json!({ "success": false, "error_code": "calibration_sequence_discontinuity", - "error": "Raw CSI sequence moved backward during capture. Cancel and restart the empty-room calibration.", + "error": "Raw CSI sequence moved backward beyond the bounded UDP reorder window. Cancel and restart the empty-room calibration.", "model_id": model_id, "source_node_ids": source_node_ids, + "reordered_packets_by_node": reordered_packets_by_node, + "max_reorder_depth_by_node": max_reorder_depth_by_node, "sequence_fault_node_ids": s.calibration_sequence_fault_node_ids, })); } @@ -6769,6 +6855,8 @@ async fn calibration_stop(State(state): State) -> Json) -> Json Date: Sun, 6 Sep 2026 11:18:59 -0400 Subject: [PATCH 26/27] fix(calibration): bind field models to stable CSI grids --- ...57-raw-csi-calibration-sample-integrity.md | 110 +- .../src/bootstrap_baseline.rs | 154 ++- .../wifi-densepose-sensing-server/src/main.rs | 972 ++++++++++++++++-- 3 files changed, 1080 insertions(+), 156 deletions(-) diff --git a/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md b/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md index 2bf39cf0f..6cdd25a3a 100644 --- a/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md +++ b/docs/adr/ADR-357-raw-csi-calibration-sample-integrity.md @@ -26,31 +26,49 @@ not be promoted as a production bootstrap prior. ## Decision -1. Only a grid admitted raw CSI packet may advance field calibration. Edge - vital packets update their own health and presentation state but never feed - the field model. +1. Only a packet on the frozen, model specific CSI grid may advance field + calibration. Edge vital packets update their own health and presentation + state but never feed the field model. The independent live inference grid + remains unchanged. 2. Each calibration session stores the last admitted wrapping sequence for every contributing node. Duplicate sequences are ignored. Normal forward progress and the `u32::MAX` to zero wrap are accepted. -3. A packet one through four sequence values behind the current high water mark +3. A packet one through sixteen sequence values behind the current high water mark is bounded UDP reordering. It is recorded and dropped without feeding the - model or moving the high water mark. A backward depth above four, or an + model or moving the high water mark. A backward depth above sixteen, or an ambiguous half range sequence, latches that node as faulted for the remainder of the session. Status exposes the policy, reorder window, per node reorder count, maximum observed depth, and `sequence_fault_node_ids`. Finalization returns `calibration_sequence_discontinuity` after a fault. The operator must cancel and restart the empty room capture. -4. A requested source may start calibration only when it has a grid admitted - raw CSI packet newer than five seconds. General node liveness is not enough. - The node endpoint reports both `csi_status` and `csi_last_seen_ms` so clients - can explain the gate before the operator begins. Calibration status reports - `last_sequence_by_node` so admission growth and sequence identity can be - observed atomically without exposing CSI values. +4. A requested source may start calibration only after a header only, bounded + observation window identifies one stable grid. The selected grid needs at + least ten seconds of evidence, a measured rate of at least 2 Hz, a latest + sample newer than five seconds, and no observed gap of five seconds or more. + Calibration status reports the immutable grid, selection evidence, current + grid age, and `last_sequence_by_node` without exposing CSI values. 5. Start, cancel, reset, and process initialization clear sequence admission state. A failed feed does not advance the sequence cursor, so the same packet may be retried after a transient model input failure. 6. The field bridge receives the current amplitude slice rather than a cloned - history. Canonical grid normalization remains unchanged. + history. A separate bounded runtime history retains only that same frozen + grid, so the model is never trained on one symbol layout and evaluated on a + different layout. Canonical grid normalization remains unchanged. +7. The exact source grid is stored inside the digest protected bootstrap v2 + payload. A restored model rebuilds only its matching runtime history. An + unbound legacy image cannot become an active startup prior. +8. Successful finalization clears the selected grid runtime history. Bootstrap + promotion waits up to 35 seconds for a complete fresh runtime window before + scoring twelve additional samples. This covers a 50 frame window at the 2 Hz + admission floor plus ten seconds of bounded jitter. Calibration observations + therefore cannot enter the held out decision window. +9. Stop and promotion require the frozen grid to remain fresh and reject any + latched sequence fault immediately before persistence. Bootstrap v2 accepts + exactly one source node because the current runtime supports one grid bound + field model. +10. The legacy `--calibrate` startup switch fails closed before opening ports. + Grid evidence does not exist at process start, so operators start the server, + wait ten seconds, then call the explicit single source calibration API. ## Consequences @@ -58,11 +76,20 @@ Calibration frame count now means unique, forward raw CSI observations within one uninterrupted sequence epoch. A vitals only node cannot produce a bootstrap model. Late packets never become calibration evidence. -A physical Node 5 capture observed sequence 4,119,566 followed by 4,119,563, -4,119,564, and 4,119,565 in one receive batch, then normal forward progress. -The window of four covers that measured depth of three with one frame of -margin. At the firmware acceptance ceiling of 50 Hz, the window represents no -more than 80 milliseconds. Larger regressions remain failed closed. +One physical Node 5 capture observed sequence 4,119,566 followed by 4,119,563, +4,119,564, and 4,119,565 in one receive batch. A second independent 90 second +capture observed bounded late packets at depths 9, 10, and 11. The window of +sixteen covers the measured maximum with five positions of margin. At the +firmware acceptance ceiling of 50 Hz, the window represents no more than 320 +milliseconds. Late packets are always dropped. Larger regressions remain +failed closed. + +The same 90 second capture measured three interleaved HT grids on Node 5. The +64 subcarrier stream delivered 436 frames at 4.84 Hz with a 1.56 second maximum +gap. The 128 stream delivered 100 frames at 1.11 Hz with a 5.05 second maximum +gap. The 192 stream delivered 49 frames at 0.54 Hz with an 11.53 second maximum +gap. Only the 64 grid exceeded the 2 Hz admission floor and stayed inside the +five second freshness boundary, so the final field model binds to that grid. The current wire format still has no authenticated boot epoch or stable device identity. A restart very early in a session or a same endpoint adversary cannot @@ -71,33 +98,46 @@ authenticated device identifier and a random per boot identifier before the server claims exact restart or identity collision detection. No new raw CSI is persisted. Sequence cursors and fault markers remain bounded -in memory to active calibration sources. The persisted bootstrap format and -its negative only authority are unchanged. +in memory to active calibration sources. The bootstrap schema advances to v2 +to protect the grid identity while its negative only authority remains +unchanged. ## Implementation -1. `NodeState` records a separate accepted CSI arrival clock. +1. `NodeState` records header only recent grid evidence and a separate bounded + field model history. 2. `AppStateInner::maybe_feed_calibration_frame` owns source binding, sequence ordering, fault latching, model feed, and cursor commit. 3. `field_bridge::maybe_feed_calibration` accepts exactly one current amplitude slice and rejects an empty observation. -4. Calibration start, status, stop, cancel, reset, and node status expose and - enforce the new admission contract. +4. Calibration start, status, stop, cancel, reset, promotion, refinement, and + restart enforce the frozen source and grid contract. +5. `bootstrap_baseline` persists the source grid inside the verified v2 image. +6. Finalization clears training history, and promotion blocks until the model + has a complete fresh scoring window plus twelve separately spaced samples. +7. Startup calibration without measured grid evidence exits with a typed + operator instruction instead of creating a model that can never receive a + frame. ## Acceptance test -Unit tests must prove first packet acceptance, forward progress, duplicate -rejection, source isolation, maximum counter wrap, empty input retry, the -measured depth three reorder pattern without a model feed, depth four -acceptance, depth five latching, half range rejection, session clear, receipt -visibility, and typed stop refusal. +Unit tests must prove stable grid selection, rate and gap rejection, first +packet acceptance, forward progress, duplicate rejection, source and grid +isolation, maximum counter wrap, empty input retry, measured depth eleven +reordering without a model feed, depth sixteen acceptance, depth seventeen +latching, half range rejection, session clear, receipt visibility, protected +grid persistence, and typed stop refusal. +Tests must also prove startup flag refusal, missing and stale grid refusal, +complete holdout isolation, and sequence fault rejection during holdout. On physical hardware, begin a Node 5 empty room capture and observe a fresh -sixty second interval. Calibration frame growth must match unique grid admitted -Node 5 CSI sequences. Edge vital packets alone must produce zero growth. The -source must retain `csi_status=active`, sequence faults must remain empty, and -numeric vital output must remain absent. Any bounded late packet must appear in -the reorder receipts while leaving the model count unchanged. Complete both the -1,000 frame and 600 second gates, then run the twelve sample server controlled -bootstrap holdout. Do not claim human detection improvement until a separate -consented occupied holdout demonstrates retained recall. +sixty second interval. Calibration frame growth must match unique 64 subcarrier +Node 5 sequences while the independent global inference grid remains unchanged. +Edge vital packets and other grids must produce zero growth. The bound grid +must remain active, sequence faults must remain empty, and numeric vital output +must remain absent. Any bounded late packet must appear in the reorder receipts +while leaving the model count unchanged. Complete both the 1,000 frame and 600 +second gates, then run the twelve sample server controlled bootstrap holdout. +Restart and prove that only the persisted grid rebuilds the runtime history. +Do not claim human detection improvement until a separate consented occupied +holdout demonstrates retained recall. diff --git a/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs b/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs index 291e7e1c3..b6ce978c8 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/bootstrap_baseline.rs @@ -16,7 +16,7 @@ use wifi_densepose_signal::ruvsense::field_model::{ FieldModel, FieldModelError, FieldModelSnapshotV1, }; -pub const BOOTSTRAP_BASELINE_SCHEMA: &str = "ruview.bootstrap-empty-field-model.v1"; +pub const BOOTSTRAP_BASELINE_SCHEMA: &str = "ruview.bootstrap-empty-field-model.v2"; pub const BOOTSTRAP_BASELINE_AUTHORITY: &str = "bootstrap_only"; pub const BOOTSTRAP_VALIDATION_SAMPLES: usize = 12; pub const BOOTSTRAP_VALIDATION_MIN_EMPTY: usize = 10; @@ -24,13 +24,25 @@ pub const BOOTSTRAP_VALIDATION_MIN_SPACING_MS: u64 = 1_000; pub const BOOTSTRAP_VALIDATION_SAMPLE_TIMEOUT_MS: u64 = 4_000; const BOOTSTRAP_MAX_FILE_BYTES: u64 = 1_048_576; +/// Exact CSI layout used to train the persisted empty room model. +/// +/// Restoring code must require incoming frames to match this binding before +/// the bootstrap image can influence inference. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct BootstrapCsiGrid { + pub n_subcarriers: u16, + pub ppdu_type: u8, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] -struct BootstrapPayloadV1 { +struct BootstrapPayloadV2 { schema: String, authority: String, installation_binding_sha256: String, source_node_ids: Vec, + source_grid: BootstrapCsiGrid, source_model_id: String, created_at_unix_ms: u64, expires_at_unix_ms: u64, @@ -39,14 +51,14 @@ struct BootstrapPayloadV1 { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] -struct BootstrapImageV1 { - payload: BootstrapPayloadV1, +struct BootstrapImageV2 { + payload: BootstrapPayloadV2, content_sha256: String, } #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] -struct BootstrapImageRawV1 { +struct BootstrapImageRawV2 { payload: Box, content_sha256: String, } @@ -55,6 +67,7 @@ struct BootstrapImageRawV1 { pub struct BootstrapBaselineMetadata { pub authority: &'static str, pub source_node_ids: Vec, + pub source_grid: BootstrapCsiGrid, pub source_model_id: String, pub created_at_unix_ms: u64, pub expires_at_unix_ms: u64, @@ -139,6 +152,7 @@ pub fn store( path: &Path, installation_id: &str, source_node_ids: &[u8], + source_grid: BootstrapCsiGrid, source_model_id: &str, created_at_unix_ms: u64, field_model: &FieldModel, @@ -146,11 +160,12 @@ pub fn store( let snapshot = field_model.export_snapshot()?; let calibrated_at_ms = snapshot.modes.calibrated_at_us / 1_000; let expiry_ms = (snapshot.config.baseline_expiry_s * 1_000.0) as u64; - let payload = BootstrapPayloadV1 { + let payload = BootstrapPayloadV2 { schema: BOOTSTRAP_BASELINE_SCHEMA.to_string(), authority: BOOTSTRAP_BASELINE_AUTHORITY.to_string(), installation_binding_sha256: installation_binding(installation_id)?, source_node_ids: source_node_ids.to_vec(), + source_grid, source_model_id: source_model_id.to_string(), created_at_unix_ms, expires_at_unix_ms: calibrated_at_ms.saturating_add(expiry_ms), @@ -158,7 +173,7 @@ pub fn store( }; validate_payload(&payload, installation_id, created_at_unix_ms)?; let content_sha256 = payload_digest(&payload)?; - let image = BootstrapImageV1 { + let image = BootstrapImageV2 { payload, content_sha256, }; @@ -194,13 +209,13 @@ pub fn load( // Verify the exact payload bytes written by storage before decoding the // floating point model. Equivalent JSON number spellings may serialize // differently, so decode and reserialize is not an integrity contract. - let raw_image: BootstrapImageRawV1 = serde_json::from_slice(&bytes)?; + let raw_image: BootstrapImageRawV2 = serde_json::from_slice(&bytes)?; if hex_sha256(raw_image.payload.get().as_bytes()) != raw_image.content_sha256 { return Err(BootstrapBaselineError::DigestMismatch); } - let payload: BootstrapPayloadV1 = serde_json::from_str(raw_image.payload.get())?; + let payload: BootstrapPayloadV2 = serde_json::from_str(raw_image.payload.get())?; validate_payload(&payload, installation_id, current_unix_ms)?; - let image = BootstrapImageV1 { + let image = BootstrapImageV2 { payload, content_sha256: raw_image.content_sha256, }; @@ -226,7 +241,7 @@ pub fn remove(path: &Path) -> Result { } fn validate_payload( - payload: &BootstrapPayloadV1, + payload: &BootstrapPayloadV2, installation_id: &str, current_unix_ms: u64, ) -> Result<(), BootstrapBaselineError> { @@ -234,17 +249,14 @@ fn validate_payload( || payload.authority != BOOTSTRAP_BASELINE_AUTHORITY || payload.source_model_id.is_empty() || payload.source_model_id.len() > 128 - || payload.source_node_ids.is_empty() - || payload.source_node_ids.len() > 16 - || payload - .source_node_ids - .windows(2) - .any(|pair| pair[0] >= pair[1]) + || payload.source_node_ids.len() != 1 + || !(1..=2_048).contains(&payload.source_grid.n_subcarriers) + || payload.source_grid.ppdu_type > 3 || payload.created_at_unix_ms == 0 || payload.expires_at_unix_ms <= payload.created_at_unix_ms { return Err(BootstrapBaselineError::Malformed( - "invalid schema, authority, identity, node set, or lifetime".into(), + "invalid schema, authority, identity, node set, source grid, or lifetime".into(), )); } if payload.installation_binding_sha256 != installation_binding(installation_id)? { @@ -256,14 +268,15 @@ fn validate_payload( Ok(()) } -fn payload_digest(payload: &BootstrapPayloadV1) -> Result { +fn payload_digest(payload: &BootstrapPayloadV2) -> Result { Ok(hex_sha256(&serde_json::to_vec(payload)?)) } -fn metadata(image: &BootstrapImageV1) -> BootstrapBaselineMetadata { +fn metadata(image: &BootstrapImageV2) -> BootstrapBaselineMetadata { BootstrapBaselineMetadata { authority: BOOTSTRAP_BASELINE_AUTHORITY, source_node_ids: image.payload.source_node_ids.clone(), + source_grid: image.payload.source_grid, source_model_id: image.payload.source_model_id.clone(), created_at_unix_ms: image.payload.created_at_unix_ms, expires_at_unix_ms: image.payload.expires_at_unix_ms, @@ -315,6 +328,11 @@ mod tests { use super::*; use wifi_densepose_signal::ruvsense::field_model::FieldModelConfig; + const TEST_GRID: BootstrapCsiGrid = BootstrapCsiGrid { + n_subcarriers: 192, + ppdu_type: 2, + }; + fn completed_model(now_us: u64) -> FieldModel { let mut model = FieldModel::new(FieldModelConfig { n_links: 1, @@ -340,10 +358,11 @@ mod tests { let path = path_in(dir.path()); let now_ms = 1_000_000; let model = completed_model(now_ms * 1_000); - let stored = store(&path, "home-a", &[5], "model", now_ms, &model).unwrap(); + let stored = store(&path, "home-a", &[5], TEST_GRID, "model", now_ms, &model).unwrap(); let text = String::from_utf8(fs::read(&path).unwrap()).unwrap(); assert!(!text.contains("home-a")); assert!(!text.contains("raw_csi")); + assert_eq!(stored.source_grid, TEST_GRID); let (restored, loaded) = load(&path, "home-a", now_ms + 1).unwrap(); assert_eq!(stored, loaded); @@ -363,18 +382,18 @@ mod tests { let path = path_in(dir.path()); let now_ms = 1_000_000; let model = completed_model(now_ms * 1_000); - store(&path, "home-a", &[5], "model", now_ms, &model).unwrap(); + store(&path, "home-a", &[5], TEST_GRID, "model", now_ms, &model).unwrap(); let mut image: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); - image["payload"]["source_model_id"] = serde_json::json!("forged"); + image["payload"]["source_grid"]["n_subcarriers"] = serde_json::json!(64); fs::write(&path, serde_json::to_vec(&image).unwrap()).unwrap(); assert!(matches!( load(&path, "home-a", now_ms + 1), Err(BootstrapBaselineError::DigestMismatch) )); - store(&path, "home-a", &[5], "model", now_ms, &model).unwrap(); + store(&path, "home-a", &[5], TEST_GRID, "model", now_ms, &model).unwrap(); assert!(matches!( load(&path, "home-a", now_ms + 3_600_001), Err(BootstrapBaselineError::Expired) @@ -384,13 +403,90 @@ mod tests { )); } + #[test] + fn store_rejects_invalid_source_grid() { + let dir = tempfile::tempdir().unwrap(); + let path = path_in(dir.path()); + let now_ms = 1_000_000; + let model = completed_model(now_ms * 1_000); + + for source_grid in [ + BootstrapCsiGrid { + n_subcarriers: 0, + ppdu_type: 0, + }, + BootstrapCsiGrid { + n_subcarriers: 2_049, + ppdu_type: 0, + }, + BootstrapCsiGrid { + n_subcarriers: 192, + ppdu_type: 4, + }, + ] { + assert!(matches!( + store(&path, "home-a", &[5], source_grid, "model", now_ms, &model,), + Err(BootstrapBaselineError::Malformed(_)) + )); + } + } + + #[test] + fn store_requires_exactly_one_source_node() { + let dir = tempfile::tempdir().unwrap(); + let path = path_in(dir.path()); + let now_ms = 1_000_000; + let model = completed_model(now_ms * 1_000); + + for source_node_ids in [&[][..], &[5, 7][..]] { + assert!(matches!( + store( + &path, + "home-a", + source_node_ids, + TEST_GRID, + "model", + now_ms, + &model, + ), + Err(BootstrapBaselineError::Malformed(_)) + )); + } + } + + #[test] + fn source_grid_is_required_when_loading_a_v2_image() { + let dir = tempfile::tempdir().unwrap(); + let path = path_in(dir.path()); + let now_ms = 1_000_000; + let model = completed_model(now_ms * 1_000); + store(&path, "home-a", &[5], TEST_GRID, "model", now_ms, &model).unwrap(); + + let mut image: serde_json::Value = + serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); + image["payload"] + .as_object_mut() + .unwrap() + .remove("source_grid"); + let payload = serde_json::to_vec(&image["payload"]).unwrap(); + let rewritten = format!( + "{{\"payload\":{},\"content_sha256\":\"{}\"}}", + String::from_utf8(payload.clone()).unwrap(), + hex_sha256(&payload) + ); + fs::write(&path, rewritten).unwrap(); + + assert!(matches!( + load(&path, "home-a", now_ms + 1), + Err(BootstrapBaselineError::Json(_)) + )); + } + #[test] fn promotion_gate_requires_twelve_fresh_samples_and_zero_vitals() { assert_eq!(BOOTSTRAP_VALIDATION_MIN_SPACING_MS, 1_000); assert_eq!(BOOTSTRAP_VALIDATION_SAMPLE_TIMEOUT_MS, 4_000); - assert!( - BOOTSTRAP_VALIDATION_SAMPLE_TIMEOUT_MS >= BOOTSTRAP_VALIDATION_MIN_SPACING_MS - ); + assert!(BOOTSTRAP_VALIDATION_SAMPLE_TIMEOUT_MS >= BOOTSTRAP_VALIDATION_MIN_SPACING_MS); let passing = vec![ BootstrapValidationSample { fresh_tick: true, @@ -415,10 +511,10 @@ mod tests { let path = path_in(dir.path()); let now_ms = 1_000_000; let model = completed_model(now_ms * 1_000); - store(&path, "home-a", &[5], "model", now_ms, &model).unwrap(); + store(&path, "home-a", &[5], TEST_GRID, "model", now_ms, &model).unwrap(); let original = fs::read_to_string(&path).unwrap(); - let image: BootstrapImageRawV1 = serde_json::from_str(&original).unwrap(); + let image: BootstrapImageRawV2 = serde_json::from_str(&original).unwrap(); let rewritten_payload = image.payload.get().replacen( "\"min_calibration_duration_s\":0.0", "\"min_calibration_duration_s\":0.00", diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 68f6ada78..b843efce2 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -329,6 +329,59 @@ impl Esp32Frame { } } +/// Exact CSI symbol grid used by one field model. Subcarrier count alone is +/// insufficient because HT-LTF and HE-LTF use different tone layouts. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +struct CsiGridKey { + n_subcarriers: u16, + ppdu_type: u8, +} + +impl CsiGridKey { + fn from_frame(frame: &Esp32Frame) -> Self { + Self { + n_subcarriers: frame.n_subcarriers, + ppdu_type: frame.ppdu_type.to_byte(), + } + } + + fn as_bootstrap_grid(self) -> bootstrap_baseline::BootstrapCsiGrid { + bootstrap_baseline::BootstrapCsiGrid { + n_subcarriers: self.n_subcarriers, + ppdu_type: self.ppdu_type, + } + } + + fn from_bootstrap_grid(grid: bootstrap_baseline::BootstrapCsiGrid) -> Self { + Self { + n_subcarriers: grid.n_subcarriers, + ppdu_type: grid.ppdu_type, + } + } +} + +#[derive(Debug, Clone, Copy)] +struct RawGridObservation { + grid: CsiGridKey, + observed_at: std::time::Instant, +} + +#[derive(Debug, Clone, Copy, Serialize)] +struct CalibrationGridEvidence { + sample_count: usize, + span_s: f64, + rate_hz: f64, + max_gap_s: f64, + latest_age_s: f64, +} + +#[derive(Debug, Clone, Copy)] +struct CalibrationGridBinding { + source_node_id: u8, + grid: CsiGridKey, + evidence: Option, +} + /// Sensing update broadcast to WebSocket clients #[derive(Debug, Clone, Serialize, Deserialize)] struct SensingUpdate { @@ -886,6 +939,15 @@ struct NodeState { /// the two symbol grids in `frame_history` corrupts variance/baseline /// statistics. See [`NodeState::accept_grid`]. active_grid: Option<(u16, wifi_densepose_hardware::PpduType)>, + /// Header-only recent raw grid observations used to choose one stable, + /// comparable calibration stream. No CSI amplitudes are retained here. + raw_grid_observations: VecDeque, + /// Runtime history for the exact grid bound into the active field model. + /// This must not reuse `frame_history`, whose independent inference grid + /// may be wider and therefore not bin-comparable. + field_model_history: VecDeque>, + field_model_latest_sequence: Option, + field_model_latest_seen: Option, } /// Default EMA alpha for temporal keypoint smoothing (RuVector Phase 2). @@ -909,6 +971,14 @@ const NOVELTY_HISTORY_CAPACITY: usize = 64; /// ADR-084 Pass 3 — feature-vector schema version. Bump on changes to /// subcarrier ordering / normalisation so banks reject stale data. const NOVELTY_SKETCH_VERSION: u16 = 1; +/// Recent header-only evidence window used to choose a stable field-model +/// stream. The rate floor is 20% above the 1,000 / 600 s completion minimum. +const CALIBRATION_GRID_EVIDENCE_WINDOW: std::time::Duration = + std::time::Duration::from_secs(20); +const CALIBRATION_GRID_EVIDENCE_CAPACITY: usize = 1_024; +const CALIBRATION_GRID_MIN_SPAN_S: f64 = 10.0; +const CALIBRATION_GRID_MIN_RATE_HZ: f64 = 2.0; +const CALIBRATION_GRID_MAX_GAP_S: f64 = 5.0; /// Lower plausibility floor (seconds) for a CSI inter-frame delta. /// @@ -1212,9 +1282,129 @@ impl NodeState { ), last_novelty_score: None, active_grid: None, + raw_grid_observations: VecDeque::with_capacity( + CALIBRATION_GRID_EVIDENCE_CAPACITY, + ), + field_model_history: VecDeque::with_capacity(FRAME_HISTORY_CAPACITY), + field_model_latest_sequence: None, + field_model_latest_seen: None, } } + fn observe_raw_grid(&mut self, grid: CsiGridKey, now: std::time::Instant) { + while self + .raw_grid_observations + .front() + .is_some_and(|sample| { + now.saturating_duration_since(sample.observed_at) + > CALIBRATION_GRID_EVIDENCE_WINDOW + }) + { + self.raw_grid_observations.pop_front(); + } + if self.raw_grid_observations.len() == CALIBRATION_GRID_EVIDENCE_CAPACITY { + self.raw_grid_observations.pop_front(); + } + self.raw_grid_observations.push_back(RawGridObservation { + grid, + observed_at: now, + }); + } + + fn calibration_grid_candidates( + &self, + now: std::time::Instant, + ) -> Vec<(CsiGridKey, CalibrationGridEvidence)> { + let mut grouped: Vec<(CsiGridKey, Vec)> = Vec::new(); + for sample in self.raw_grid_observations.iter().filter(|sample| { + now.saturating_duration_since(sample.observed_at) + <= CALIBRATION_GRID_EVIDENCE_WINDOW + }) { + if let Some((_, arrivals)) = grouped + .iter_mut() + .find(|(candidate, _)| *candidate == sample.grid) + { + arrivals.push(sample.observed_at); + } else { + grouped.push((sample.grid, vec![sample.observed_at])); + } + } + grouped + .into_iter() + .filter_map(|(grid, arrivals)| { + let first = *arrivals.first()?; + let last = *arrivals.last()?; + let span_s = last.saturating_duration_since(first).as_secs_f64(); + let latest_age_s = now.saturating_duration_since(last).as_secs_f64(); + let mut max_gap_s = latest_age_s; + for pair in arrivals.windows(2) { + max_gap_s = max_gap_s.max( + pair[1] + .saturating_duration_since(pair[0]) + .as_secs_f64(), + ); + } + let rate_hz = if span_s > 0.0 { + arrivals.len().saturating_sub(1) as f64 / span_s + } else { + 0.0 + }; + Some(( + grid, + CalibrationGridEvidence { + sample_count: arrivals.len(), + span_s, + rate_hz, + max_gap_s, + latest_age_s, + }, + )) + }) + .collect() + } + + fn select_calibration_grid( + &self, + now: std::time::Instant, + ) -> Option<(CsiGridKey, CalibrationGridEvidence)> { + self.calibration_grid_candidates(now) + .into_iter() + .filter(|(grid, evidence)| { + grid.ppdu_type <= 3 + && evidence.span_s >= CALIBRATION_GRID_MIN_SPAN_S + && evidence.rate_hz >= CALIBRATION_GRID_MIN_RATE_HZ + && evidence.max_gap_s < CALIBRATION_GRID_MAX_GAP_S + && evidence.latest_age_s < CALIBRATION_GRID_MAX_GAP_S + }) + .min_by(|(left_grid, left), (right_grid, right)| { + left.max_gap_s + .total_cmp(&right.max_gap_s) + .then_with(|| right.rate_hz.total_cmp(&left.rate_hz)) + .then_with(|| right_grid.n_subcarriers.cmp(&left_grid.n_subcarriers)) + .then_with(|| left_grid.ppdu_type.cmp(&right_grid.ppdu_type)) + }) + } + + fn clear_field_model_history(&mut self) { + self.field_model_history.clear(); + self.field_model_latest_sequence = None; + self.field_model_latest_seen = None; + } + + fn push_field_model_frame( + &mut self, + sequence: u32, + amplitudes: &[f64], + now: std::time::Instant, + ) { + self.field_model_history.push_back(amplitudes.to_vec()); + if self.field_model_history.len() > FRAME_HISTORY_CAPACITY { + self.field_model_history.pop_front(); + } + self.field_model_latest_sequence = Some(sequence); + self.field_model_latest_seen = Some(now); + } + /// ADR-110 / issue #1005 grid gate: decide whether a frame on `grid` /// may enter this node's feature path, and update `active_grid`. /// @@ -1632,6 +1822,9 @@ struct AppStateInner { calibration_model_id: Option, /// Nodes that actually contributed frames to the current calibration. calibration_source_node_ids: std::collections::BTreeSet, + /// Immutable source and CSI symbol grid selected from recent header-only + /// evidence for the current field model. + calibration_grid_binding: Option, /// Last admitted raw CSI sequence per contributing node. Edge-vitals /// packets do not carry a new CSI observation and cannot advance this map. calibration_last_sequences: HashMap, @@ -1754,11 +1947,12 @@ enum CalibrationSequenceOrder { Discontinuity, } -/// Live Node 5 evidence observed a same-source receive batch ordered as high -/// water mark, then minus three, minus two, minus one. Four covers that -/// measured UDP reorder with one frame of margin. At the firmware's 50 Hz -/// ceiling it spans at most 80 ms; larger backward jumps stay failed closed. -const CALIBRATION_SEQUENCE_REORDER_WINDOW: u32 = 4; +/// Two independent live Node 5 captures observed bounded same-source UDP +/// reordering through depth eleven. Sixteen covers that measured maximum with +/// five positions of margin. At the firmware's 50 Hz ceiling it spans at most +/// 320 ms; larger backward jumps stay failed closed. Late packets inside this +/// window are always dropped and never become model evidence. +const CALIBRATION_SEQUENCE_REORDER_WINDOW: u32 = 16; fn calibration_sequence_order(previous: Option, sequence: u32) -> CalibrationSequenceOrder { let Some(previous) = previous else { @@ -1786,10 +1980,16 @@ impl AppStateInner { fn maybe_feed_calibration_frame( &mut self, node_id: u8, + grid: CsiGridKey, sequence: u32, amplitudes: &[f64], + observed_at: std::time::Instant, ) -> bool { - if !calibration_source_accepts(&self.calibration_source_node_ids, node_id) + let binding_matches = self.calibration_grid_binding.is_some_and(|binding| { + binding.source_node_id == node_id && binding.grid == grid + }); + if !binding_matches + || !calibration_source_accepts(&self.calibration_source_node_ids, node_id) || self.calibration_sequence_fault_node_ids.contains(&node_id) { return false; @@ -1827,13 +2027,25 @@ impl AppStateInner { } CalibrationSequenceOrder::First | CalibrationSequenceOrder::Forward => {} } - let accepted = self - .field_model - .as_mut() - .is_some_and(|field| field_bridge::maybe_feed_calibration(field, amplitudes)); + let accepted = self.field_model.as_mut().is_some_and(|field| { + if matches!( + field.status(), + CalibrationStatus::Uncalibrated | CalibrationStatus::Collecting + ) { + field_bridge::maybe_feed_calibration(field, amplitudes) + } else { + field.check_freshness( + (chrono::Utc::now().timestamp_millis().max(0) as u64) + .saturating_mul(1_000), + ) == CalibrationStatus::Fresh + } + }); if accepted { self.calibration_source_node_ids.insert(node_id); self.calibration_last_sequences.insert(node_id, sequence); + if let Some(node) = self.node_states.get_mut(&node_id) { + node.push_field_model_frame(sequence, amplitudes, observed_at); + } } accepted } @@ -1845,6 +2057,51 @@ impl AppStateInner { self.calibration_sequence_fault_node_ids.clear(); } + fn clear_field_model_binding(&mut self) { + self.calibration_grid_binding = None; + for node in self.node_states.values_mut() { + node.clear_field_model_history(); + } + } + + fn calibration_grid_is_fresh( + &self, + binding: CalibrationGridBinding, + now: std::time::Instant, + ) -> bool { + !self + .calibration_sequence_fault_node_ids + .contains(&binding.source_node_id) + && self + .node_states + .get(&binding.source_node_id) + .and_then(|node| node.field_model_latest_seen) + .is_some_and(|seen| { + now.saturating_duration_since(seen) < ESP32_OFFLINE_TIMEOUT + }) + } + + fn field_model_holdout_ready( + &self, + binding: CalibrationGridBinding, + required_window_size: usize, + now: std::time::Instant, + ) -> bool { + self.calibration_grid_binding.is_some_and(|current| { + current.source_node_id == binding.source_node_id && current.grid == binding.grid + }) && self.calibration_grid_is_fresh(binding, now) + && self + .node_states + .get(&binding.source_node_id) + .is_some_and(|node| node.field_model_history.len() >= required_window_size) + } + + fn begin_field_model_holdout(&mut self, binding: CalibrationGridBinding) { + if let Some(node) = self.node_states.get_mut(&binding.source_node_id) { + node.clear_field_model_history(); + } + } + fn field_model_status_at(&self, observed_at_unix_ms: u64) -> Option { self.field_model.as_ref().map(|model| { field_bridge::calibration_status_at(model, observed_at_unix_ms.saturating_mul(1_000)) @@ -1895,7 +2152,7 @@ impl AppStateInner { let history = if let Some(node_id) = bound_source_node_id { self.node_states .get(&node_id) - .map(|state| &state.frame_history) + .map(|state| &state.field_model_history) .unwrap_or(&self.frame_history) } else if !self.frame_history.is_empty() { &self.frame_history @@ -1945,7 +2202,23 @@ impl AppStateInner { let [source_node_id] = metadata.source_node_ids.as_slice() else { return None; }; - let history = &self.node_states.get(source_node_id)?.frame_history; + let binding = self.calibration_grid_binding?; + if binding.source_node_id != *source_node_id + || binding.grid != CsiGridKey::from_bootstrap_grid(metadata.source_grid) + || self + .calibration_sequence_fault_node_ids + .contains(source_node_id) + { + return None; + } + let node = self.node_states.get(source_node_id)?; + if !node.field_model_latest_seen.is_some_and(|seen| { + std::time::Instant::now().saturating_duration_since(seen) + < ESP32_OFFLINE_TIMEOUT + }) { + return None; + } + let history = &node.field_model_history; field_bridge::bootstrap_background_match( field_model, history, @@ -2004,6 +2277,10 @@ impl AppStateInner { /// Number of frames retained in `frame_history` for temporal analysis. /// At 500 ms ticks this covers ~50 seconds; at 100 ms ticks ~10 seconds. const FRAME_HISTORY_CAPACITY: usize = 100; +/// A qualified grid may run at the 2 Hz admission floor. Rebuilding a 50 frame +/// runtime scoring window then needs 25 seconds, so promotion grants ten more +/// seconds for ordinary bounded jitter before failing closed. +const BOOTSTRAP_HOLDOUT_PREWARM_TIMEOUT_MS: u64 = 35_000; type SharedState = Arc>; @@ -2087,6 +2364,7 @@ impl AppStateInner { bootstrap_baseline_active: false, calibration_model_id: None, calibration_source_node_ids: std::collections::BTreeSet::new(), + calibration_grid_binding: None, calibration_last_sequences: HashMap::new(), calibration_reordered_packets: HashMap::new(), calibration_max_reorder_depth: HashMap::new(), @@ -2140,12 +2418,30 @@ mod calibration_expiry_tests { let baseline = model.modes().unwrap().baseline[0].clone(); let mut state = AppStateInner::minimal(); state.frame_history = (0..50).map(|_| baseline.clone()).collect(); + let mut node = NodeState::new(); + node.field_model_history = (0..50).map(|_| baseline.clone()).collect(); + node.field_model_latest_sequence = Some(50); + node.field_model_latest_seen = Some(std::time::Instant::now()); + state.node_states.insert(5, node); + state.calibration_source_node_ids.insert(5); + state.calibration_grid_binding = Some(CalibrationGridBinding { + source_node_id: 5, + grid: CsiGridKey { + n_subcarriers: 64, + ppdu_type: 0, + }, + evidence: None, + }); state.field_model = Some(model); state.bootstrap_baseline_active = bootstrap; if bootstrap { state.bootstrap_baseline = Some(BootstrapBaselineMetadata { authority: bootstrap_baseline::BOOTSTRAP_BASELINE_AUTHORITY, source_node_ids: vec![5], + source_grid: bootstrap_baseline::BootstrapCsiGrid { + n_subcarriers: 64, + ppdu_type: 0, + }, source_model_id: "test-model".to_string(), created_at_unix_ms: 1_000, expires_at_unix_ms: 3_000, @@ -6416,6 +6712,38 @@ fn calibration_source_has_fresh_csi(node: &NodeState, now: std::time::Instant) - mod bootstrap_vital_publication_tests { use super::*; + fn test_grid() -> CsiGridKey { + CsiGridKey { + n_subcarriers: 64, + ppdu_type: 0, + } + } + + fn bind_test_calibration(state: &mut AppStateInner) { + state.calibration_source_node_ids.insert(5); + state.calibration_grid_binding = Some(CalibrationGridBinding { + source_node_id: 5, + grid: test_grid(), + evidence: None, + }); + state.node_states.entry(5).or_insert_with(NodeState::new); + } + + fn feed_test_frame( + state: &mut AppStateInner, + node_id: u8, + sequence: u32, + amplitudes: &[f64], + ) -> bool { + state.maybe_feed_calibration_frame( + node_id, + test_grid(), + sequence, + amplitudes, + std::time::Instant::now(), + ) + } + #[test] fn single_link_calibration_binds_to_first_source() { let mut sources = std::collections::BTreeSet::new(); @@ -6448,21 +6776,136 @@ mod bootstrap_vital_publication_tests { )); } + #[test] + fn calibration_grid_selection_prefers_stable_rate_qualified_stream() { + let start = std::time::Instant::now(); + let mut node = NodeState::new(); + let stable = CsiGridKey { + n_subcarriers: 64, + ppdu_type: 0, + }; + let sparse = CsiGridKey { + n_subcarriers: 192, + ppdu_type: 0, + }; + for index in 0..=48 { + node.observe_raw_grid( + stable, + start + std::time::Duration::from_millis(index * 250), + ); + } + for index in 0..=6 { + node.observe_raw_grid( + sparse, + start + std::time::Duration::from_secs(index * 2), + ); + } + let (selected, evidence) = node + .select_calibration_grid(start + std::time::Duration::from_secs(12)) + .expect("stable grid is eligible"); + assert_eq!(selected, stable); + assert!(evidence.rate_hz >= 3.9); + assert!(evidence.max_gap_s < 5.0); + } + + #[tokio::test] + async fn calibration_start_requires_evidence_then_binds_the_qualified_grid() { + let mut inner = AppStateInner::minimal(); + inner.node_states.insert(5, NodeState::new()); + let state = Arc::new(RwLock::new(inner)); + + let Json(unavailable) = calibration_start( + State(state.clone()), + Query(CalibrationStartQuery { + source_node_id: Some(5), + }), + ) + .await; + assert_eq!(unavailable["success"], false); + assert_eq!(unavailable["error_code"], "calibration_grid_unavailable"); + { + let state = state.read().await; + assert!(state.field_model.is_none()); + assert!(state.calibration_model_id.is_none()); + assert!(state.calibration_source_node_ids.is_empty()); + assert!(state.calibration_grid_binding.is_none()); + } + + let now = std::time::Instant::now(); + let start = now - std::time::Duration::from_secs(10); + { + let mut state = state.write().await; + let node = state.node_states.get_mut(&5).expect("node"); + for index in 0..=40 { + node.observe_raw_grid( + test_grid(), + start + std::time::Duration::from_millis(index * 250), + ); + } + } + let Json(started) = calibration_start( + State(state.clone()), + Query(CalibrationStartQuery { + source_node_id: Some(5), + }), + ) + .await; + assert_eq!(started["success"], true); + assert_eq!(started["source_node_ids"], serde_json::json!([5])); + assert_eq!(started["source_grid"]["n_subcarriers"], 64); + assert_eq!(started["source_grid"]["ppdu_type"], 0); + let state = state.read().await; + assert!(state.field_model.is_some()); + assert!(state.calibration_model_id.is_some()); + assert_eq!(state.calibration_source_node_ids, [5].into_iter().collect()); + assert_eq!( + state.calibration_grid_binding.map(|binding| binding.grid), + Some(test_grid()) + ); + } + + #[test] + fn non_bound_grid_never_enters_field_model_history() { + let mut state = AppStateInner::minimal(); + state.field_model = Some( + FieldModel::new(field_bridge::single_link_config()).expect("field model"), + ); + bind_test_calibration(&mut state); + let wrong_grid = CsiGridKey { + n_subcarriers: 192, + ppdu_type: 0, + }; + assert!(!state.maybe_feed_calibration_frame( + 5, + wrong_grid, + 41, + &[0.25; 192], + std::time::Instant::now(), + )); + assert!(state + .node_states + .get(&5) + .expect("node") + .field_model_history + .is_empty()); + assert!(state.calibration_last_sequences.is_empty()); + } + #[test] fn calibration_session_counts_each_unique_bound_csi_sequence_once() { let mut state = AppStateInner::minimal(); state.field_model = Some( FieldModel::new(field_bridge::single_link_config()).expect("field model"), ); - state.calibration_source_node_ids.insert(5); + bind_test_calibration(&mut state); let first = vec![0.25; 56]; - assert!(state.maybe_feed_calibration_frame(5, 41, &first)); - assert!(!state.maybe_feed_calibration_frame(5, 41, &first)); + assert!(feed_test_frame(&mut state, 5, 41, &first)); + assert!(!feed_test_frame(&mut state, 5, 41, &first)); let second = vec![0.5; 56]; - assert!(state.maybe_feed_calibration_frame(5, 42, &second)); - assert!(!state.maybe_feed_calibration_frame(3, 43, &second)); + assert!(feed_test_frame(&mut state, 5, 42, &second)); + assert!(!feed_test_frame(&mut state, 3, 43, &second)); let model = state.field_model.as_ref().expect("field model remains active"); assert_eq!(model.calibration_frame_count(), 2); @@ -6476,17 +6919,17 @@ mod bootstrap_vital_publication_tests { state.field_model = Some( FieldModel::new(field_bridge::single_link_config()).expect("field model"), ); - state.calibration_source_node_ids.insert(5); + bind_test_calibration(&mut state); let frame = vec![0.25; 56]; - assert!(state.maybe_feed_calibration_frame(5, 4_119_566, &frame)); - assert!(!state.maybe_feed_calibration_frame(5, 4_119_563, &frame)); - assert!(!state.maybe_feed_calibration_frame(5, 4_119_564, &frame)); - assert!(!state.maybe_feed_calibration_frame(5, 4_119_565, &frame)); - assert!(state.maybe_feed_calibration_frame(5, 4_119_567, &frame)); + assert!(feed_test_frame(&mut state, 5, 4_119_566, &frame)); + assert!(!feed_test_frame(&mut state, 5, 4_119_555, &frame)); + assert!(!feed_test_frame(&mut state, 5, 4_119_556, &frame)); + assert!(!feed_test_frame(&mut state, 5, 4_119_557, &frame)); + assert!(feed_test_frame(&mut state, 5, 4_119_567, &frame)); assert!(state.calibration_sequence_fault_node_ids.is_empty()); assert_eq!(state.calibration_reordered_packets.get(&5), Some(&3)); - assert_eq!(state.calibration_max_reorder_depth.get(&5), Some(&3)); + assert_eq!(state.calibration_max_reorder_depth.get(&5), Some(&11)); assert_eq!( state .field_model @@ -6503,12 +6946,12 @@ mod bootstrap_vital_publication_tests { state.field_model = Some( FieldModel::new(field_bridge::single_link_config()).expect("field model"), ); - state.calibration_source_node_ids.insert(5); + bind_test_calibration(&mut state); let frame = vec![0.25; 56]; - assert!(state.maybe_feed_calibration_frame(5, 100, &frame)); - assert!(!state.maybe_feed_calibration_frame(5, 95, &frame)); - assert!(!state.maybe_feed_calibration_frame(5, 101, &frame)); + assert!(feed_test_frame(&mut state, 5, 100, &frame)); + assert!(!feed_test_frame(&mut state, 5, 83, &frame)); + assert!(!feed_test_frame(&mut state, 5, 101, &frame)); assert!(state.calibration_sequence_fault_node_ids.contains(&5)); assert_eq!( state @@ -6532,15 +6975,15 @@ mod bootstrap_vital_publication_tests { state.field_model = Some( FieldModel::new(field_bridge::single_link_config()).expect("field model"), ); - state.calibration_source_node_ids.insert(5); + bind_test_calibration(&mut state); let frame = vec![0.25; 56]; - assert!(!state.maybe_feed_calibration_frame(5, u32::MAX, &[])); + assert!(!feed_test_frame(&mut state, 5, u32::MAX, &[])); assert!(state.calibration_last_sequences.is_empty()); - assert!(state.maybe_feed_calibration_frame(5, u32::MAX, &frame)); - assert!(state.maybe_feed_calibration_frame(5, 0, &frame)); - assert!(!state.maybe_feed_calibration_frame(5, 0, &frame)); - assert!(state.maybe_feed_calibration_frame(5, 1, &frame)); + assert!(feed_test_frame(&mut state, 5, u32::MAX, &frame)); + assert!(feed_test_frame(&mut state, 5, 0, &frame)); + assert!(!feed_test_frame(&mut state, 5, 0, &frame)); + assert!(feed_test_frame(&mut state, 5, 1, &frame)); assert_eq!( state .field_model @@ -6570,11 +7013,11 @@ mod bootstrap_vital_publication_tests { CalibrationSequenceOrder::Reordered(1) ); assert_eq!( - calibration_sequence_order(Some(8), 4), - CalibrationSequenceOrder::Reordered(4) + calibration_sequence_order(Some(32), 16), + CalibrationSequenceOrder::Reordered(16) ); assert_eq!( - calibration_sequence_order(Some(8), 3), + calibration_sequence_order(Some(32), 15), CalibrationSequenceOrder::Discontinuity ); assert_eq!( @@ -6603,7 +7046,7 @@ mod bootstrap_vital_publication_tests { let Json(status) = calibration_status(State(state.clone())).await; assert_eq!(status["last_sequence_by_node"]["5"], 41); - assert_eq!(status["reorder_window_sequences"], 4); + assert_eq!(status["reorder_window_sequences"], 16); assert_eq!(status["reordered_packets_by_node"]["5"], 3); assert_eq!(status["max_reorder_depth_by_node"]["5"], 3); assert_eq!(status["sequence_fault_node_ids"], serde_json::json!([5])); @@ -6620,6 +7063,77 @@ mod bootstrap_vital_publication_tests { assert_eq!(stopped["max_reorder_depth_by_node"]["5"], 3); } + #[tokio::test] + async fn calibration_stop_rejects_missing_or_stale_grid_identity() { + let mut inner = AppStateInner::minimal(); + inner.field_model = Some( + FieldModel::new(field_bridge::single_link_config()).expect("field model"), + ); + inner.calibration_model_id = Some("cal-model-test".to_string()); + inner.calibration_source_node_ids.insert(5); + let state = Arc::new(RwLock::new(inner)); + + let Json(missing) = calibration_stop(State(state.clone())).await; + assert_eq!(missing["success"], false); + assert_eq!(missing["error_code"], "calibration_grid_identity_missing"); + + { + let mut state = state.write().await; + state.calibration_grid_binding = Some(CalibrationGridBinding { + source_node_id: 5, + grid: test_grid(), + evidence: None, + }); + state.node_states.insert(5, NodeState::new()); + } + let Json(stale) = calibration_stop(State(state)).await; + assert_eq!(stale["success"], false); + assert_eq!(stale["error_code"], "calibration_grid_stale"); + } + + #[test] + fn post_finalize_holdout_excludes_training_history_and_sequence_faults() { + let mut state = AppStateInner::minimal(); + bind_test_calibration(&mut state); + let binding = state.calibration_grid_binding.expect("binding"); + let now = std::time::Instant::now(); + { + let node = state.node_states.get_mut(&5).expect("node"); + for sequence in 0..50 { + node.push_field_model_frame(sequence, &[0.25; 64], now); + } + } + assert!(state.field_model_holdout_ready(binding, 50, now)); + + state.begin_field_model_holdout(binding); + assert!(state + .node_states + .get(&5) + .expect("node") + .field_model_history + .is_empty()); + assert!(!state.field_model_holdout_ready(binding, 50, now)); + + { + let node = state.node_states.get_mut(&5).expect("node"); + for sequence in 50..100 { + node.push_field_model_frame(sequence, &[0.5; 64], now); + } + } + assert!(state.field_model_holdout_ready(binding, 50, now)); + state.calibration_sequence_fault_node_ids.insert(5); + assert!(!state.field_model_holdout_ready(binding, 50, now)); + } + + #[test] + fn startup_calibration_flag_fails_closed_before_state_creation() { + assert!(validate_startup_calibration_request(false).is_ok()); + let error = validate_startup_calibration_request(true).expect_err("must fail closed"); + assert!(error.contains("--calibrate")); + assert!(error.contains("10 seconds")); + assert!(error.contains("source_node_id")); + } + fn strong_candidates() -> VitalSigns { VitalSigns { breathing_rate_bpm: Some(15.0), @@ -6729,21 +7243,62 @@ async fn calibration_start( Query(query): Query, ) -> Json { let mut s = state.write().await; - if let Some(source_node_id) = query.source_node_id { - let now = std::time::Instant::now(); - let source_is_live = s - .node_states - .get(&source_node_id) - .is_some_and(|node| calibration_source_has_fresh_csi(node, now)); - if !source_is_live { + let now = std::time::Instant::now(); + let selected_binding = if let Some(source_node_id) = query.source_node_id { + let Some(node) = s.node_states.get(&source_node_id) else { return Json(serde_json::json!({ "success": false, "error_code": "calibration_source_unavailable", - "error": "The requested calibration source is missing or has no fresh raw CSI.", + "error": "The requested calibration source is missing.", "source_node_id": source_node_id, })); + }; + let candidates = node.calibration_grid_candidates(now); + let Some((grid, evidence)) = node.select_calibration_grid(now) else { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_grid_unavailable", + "error": "No raw CSI grid has at least 10 seconds of evidence, a 2 Hz measured rate, and a maximum gap below 5 seconds.", + "source_node_id": source_node_id, + "candidates": candidates.into_iter().map(|(grid, evidence)| serde_json::json!({ + "grid": grid, + "evidence": evidence, + })).collect::>(), + })); + }; + CalibrationGridBinding { + source_node_id, + grid, + evidence: Some(evidence), } - } + } else { + let eligible: Vec = s + .node_states + .iter() + .filter_map(|(&source_node_id, node)| { + node.select_calibration_grid(now).map(|(grid, evidence)| { + CalibrationGridBinding { + source_node_id, + grid, + evidence: Some(evidence), + } + }) + }) + .collect(); + let [binding] = eligible.as_slice() else { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_source_required", + "error": "Choose exactly one eligible ESP32 source for room calibration.", + "eligible_sources": eligible.iter().map(|binding| serde_json::json!({ + "source_node_id": binding.source_node_id, + "grid": binding.grid, + "evidence": binding.evidence, + })).collect::>(), + })); + }; + *binding + }; if s.bootstrap_baseline_active { s.field_model = None; s.bootstrap_baseline_active = false; @@ -6774,15 +7329,18 @@ async fn calibration_start( s.field_model = Some(fm); s.calibration_model_id = Some(opaque_calibration_model_id()); s.calibration_source_node_ids.clear(); + s.clear_field_model_binding(); s.clear_calibration_sequence_state(); - if let Some(source_node_id) = query.source_node_id { - s.calibration_source_node_ids.insert(source_node_id); - } + s.calibration_source_node_ids + .insert(selected_binding.source_node_id); + s.calibration_grid_binding = Some(selected_binding); Json(serde_json::json!({ "success": true, "message": "Calibration started — keep room empty while frames accumulate.", "model_id": s.calibration_model_id, "source_node_ids": s.calibration_source_node_ids, + "source_grid": selected_binding.grid, + "grid_evidence": selected_binding.evidence, })) } // ADR-080 #2: FieldModel init error chain stays server-side only. @@ -6794,6 +7352,7 @@ async fn calibration_stop(State(state): State) -> Json = s.calibration_source_node_ids.iter().copied().collect(); + let source_grid = s.calibration_grid_binding.map(|binding| binding.grid); let reordered_packets_by_node = s.calibration_reordered_packets.clone(); let max_reorder_depth_by_node = s.calibration_max_reorder_depth.clone(); if !s.calibration_sequence_fault_node_ids.is_empty() { @@ -6803,11 +7362,43 @@ async fn calibration_stop(State(state): State) -> Json) -> Json { let baseline = modes.baseline_eigenvalue_count; let variance_explained = modes.variance_explained; + let holdout_window_size = modes.baseline_runtime_window_size; + let final_frame_count = fm.calibration_frame_count(); + s.begin_field_model_holdout(binding); info!("Field model calibrated: baseline_eigenvalues={baseline}, variance_explained={variance_explained:.2}"); Json(serde_json::json!({ "success": true, "baseline_eigenvalue_count": baseline, "variance_explained": variance_explained, - "frame_count": fm.calibration_frame_count(), + "frame_count": final_frame_count, "elapsed_s": elapsed_s, + "holdout_window_size": holdout_window_size, "model_id": model_id, "source_node_ids": source_node_ids, + "source_grid": source_grid, "reordered_packets_by_node": reordered_packets_by_node, "max_reorder_depth_by_node": max_reorder_depth_by_node, })) @@ -6872,6 +7468,7 @@ async fn calibration_stop(State(state): State) -> Json) -> Json { let s = state.read().await; + let now = std::time::Instant::now(); let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; let effective_status = s.field_model_status_at(observed_at_unix_ms); let active = s.field_model_active_at(observed_at_unix_ms); @@ -6903,6 +7500,23 @@ async fn calibration_status(State(state): State) -> Json) -> Json) -> Json) -> Json, ) -> Json { - let expected_model_id = { + let (expected_model_id, expected_binding, holdout_window_size) = { let s = state.read().await; let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; if s.bootstrap_baseline_active || !s.explicit_calibration_fresh_at(observed_at_unix_ms) { @@ -6970,7 +7586,7 @@ async fn calibration_promote_bootstrap( "error": "Finalize an explicit room calibration before validating a startup baseline.", })); } - match s.calibration_model_id.clone() { + let model_id = match s.calibration_model_id.clone() { Some(model_id) => model_id, None => { return Json(serde_json::json!({ @@ -6979,11 +7595,101 @@ async fn calibration_promote_bootstrap( "error": "The active calibration has no server generated model identity.", })); } + }; + let Some(binding) = s.calibration_grid_binding else { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_grid_identity_missing", + "error": "The completed calibration has no frozen CSI grid identity.", + })); + }; + if s.calibration_sequence_fault_node_ids + .contains(&binding.source_node_id) + { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_sequence_discontinuity", + "error": "The calibration CSI sequence faulted before validation. The model was not stored.", + })); } + let Some(runtime_window_size) = s + .field_model + .as_ref() + .and_then(|model| model.modes()) + .and_then(|modes| modes.baseline_runtime_window_size) + .filter(|window_size| *window_size > 0 && *window_size <= FRAME_HISTORY_CAPACITY) + else { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_runtime_window_invalid", + "error": "The completed calibration has no valid runtime scoring window.", + })); + }; + (model_id, binding, runtime_window_size) }; + let prewarm_deadline = tokio::time::Instant::now() + + Duration::from_millis(BOOTSTRAP_HOLDOUT_PREWARM_TIMEOUT_MS); + loop { + let (identity_matches, sequence_faulted, ready, current_window_size) = { + let s = state.read().await; + let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; + let identity_matches = s.calibration_model_id.as_deref() + == Some(expected_model_id.as_str()) + && s.explicit_calibration_fresh_at(observed_at_unix_ms) + && s.calibration_grid_binding.is_some_and(|binding| { + binding.source_node_id == expected_binding.source_node_id + && binding.grid == expected_binding.grid + }); + let sequence_faulted = s + .calibration_sequence_fault_node_ids + .contains(&expected_binding.source_node_id); + let current_window_size = s + .node_states + .get(&expected_binding.source_node_id) + .map_or(0, |node| node.field_model_history.len()); + ( + identity_matches, + sequence_faulted, + s.field_model_holdout_ready( + expected_binding, + holdout_window_size, + std::time::Instant::now(), + ), + current_window_size, + ) + }; + if sequence_faulted { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_sequence_discontinuity", + "error": "The calibration CSI sequence faulted while preparing held out validation. The model was not stored.", + })); + } + if !identity_matches { + return Json(serde_json::json!({ + "success": false, + "error_code": "calibration_model_changed", + "error": "Calibration changed while preparing held out validation. The model was not stored.", + })); + } + if ready { + break; + } + if tokio::time::Instant::now() >= prewarm_deadline { + return Json(serde_json::json!({ + "success": false, + "error_code": "bootstrap_holdout_prewarm_timeout", + "error": "A complete fresh CSI scoring window was not available before the held out validation timeout. The model was not stored.", + "required_window_size": holdout_window_size, + "current_window_size": current_window_size, + })); + } + tokio::time::sleep(Duration::from_millis(50)).await; + } + let mut samples = Vec::with_capacity(BOOTSTRAP_VALIDATION_SAMPLES); - let mut last_tick = None; + let mut last_sequence = None; let mut next_sample_at = tokio::time::Instant::now(); for _ in 0..BOOTSTRAP_VALIDATION_SAMPLES { tokio::time::sleep_until(next_sample_at).await; @@ -7000,20 +7706,40 @@ async fn calibration_promote_bootstrap( let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; if s.calibration_model_id.as_deref() != Some(expected_model_id.as_str()) || !s.explicit_calibration_fresh_at(observed_at_unix_ms) + || !s.calibration_grid_binding.is_some_and(|binding| { + binding.source_node_id == expected_binding.source_node_id + && binding.grid == expected_binding.grid + }) + || s.calibration_sequence_fault_node_ids + .contains(&expected_binding.source_node_id) { None } else { - s.latest_update.as_ref().and_then(|update| { - (Some(update.tick) != last_tick).then_some(( - update.tick, - s.person_count() == 0, - update.vital_signs.is_none(), - )) - }) + s.node_states + .get(&expected_binding.source_node_id) + .and_then(|node| { + let sequence = node.field_model_latest_sequence?; + let fresh = node.field_model_latest_seen.is_some_and(|seen| { + std::time::Instant::now().saturating_duration_since(seen) + < ESP32_OFFLINE_TIMEOUT + }) && Some(sequence) != last_sequence; + let held_out = s.field_model_holdout_ready( + expected_binding, + holdout_window_size, + std::time::Instant::now(), + ); + (fresh && held_out).then_some(( + sequence, + s.person_count() == 0, + s.latest_update + .as_ref() + .is_none_or(|update| update.vital_signs.is_none()), + )) + }) } }; - if let Some((tick, calibrated_empty, vital_signs_absent)) = observed { - last_tick = Some(tick); + if let Some((sequence, calibrated_empty, vital_signs_absent)) = observed { + last_sequence = Some(sequence); sample = BootstrapValidationSample { fresh_tick: true, calibrated_empty, @@ -7042,6 +7768,12 @@ async fn calibration_promote_bootstrap( let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; if s.calibration_model_id.as_deref() != Some(expected_model_id.as_str()) || !s.explicit_calibration_fresh_at(observed_at_unix_ms) + || !s.calibration_grid_binding.is_some_and(|binding| { + binding.source_node_id == expected_binding.source_node_id + && binding.grid == expected_binding.grid + }) + || s.calibration_sequence_fault_node_ids + .contains(&expected_binding.source_node_id) { return Json(serde_json::json!({ "success": false, @@ -7077,6 +7809,7 @@ async fn calibration_promote_bootstrap( &path, &installation_id, &source_node_ids, + expected_binding.grid.as_bootstrap_grid(), &expected_model_id, created_at_unix_ms, field_model, @@ -7097,6 +7830,7 @@ async fn calibration_promote_bootstrap( "success": true, "message": "Validated startup baseline stored locally.", "validation": validation, + "holdout_window_size": holdout_window_size, "bootstrap_baseline": stored, "calibrated_evidence_authorized": false, "numeric_vitals_authorized": false, @@ -7118,7 +7852,7 @@ async fn calibration_refine_bootstrap( })); } - let (expected_model_id, expected_digest, source_node_id) = { + let (expected_model_id, expected_digest, source_node_id, expected_grid) = { let s = state.read().await; let observed_at_unix_ms = chrono::Utc::now().timestamp_millis().max(0) as u64; let Some(metadata) = s @@ -7153,6 +7887,7 @@ async fn calibration_refine_bootstrap( metadata.source_model_id.clone(), metadata.content_sha256.clone(), *source_node_id, + CsiGridKey::from_bootstrap_grid(metadata.source_grid), ) }; @@ -7175,13 +7910,19 @@ async fn calibration_refine_bootstrap( metadata.source_model_id == expected_model_id && metadata.content_sha256 == expected_digest && metadata.source_node_ids.as_slice() == [source_node_id] + && CsiGridKey::from_bootstrap_grid(metadata.source_grid) + == expected_grid + }) + && s.calibration_grid_binding.is_some_and(|binding| { + binding.source_node_id == source_node_id + && binding.grid == expected_grid }); if !identity_matches { None } else { s.node_states.get(&source_node_id).and_then(|node| { - let sequence = node.latest_csi_sequence?; - let fresh = node.last_frame_time.is_some_and(|seen| { + let sequence = node.field_model_latest_sequence?; + let fresh = node.field_model_latest_seen.is_some_and(|seen| { std::time::Instant::now().saturating_duration_since(seen) < ESP32_OFFLINE_TIMEOUT }) && Some(sequence) != last_sequence; @@ -7190,7 +7931,7 @@ async fn calibration_refine_bootstrap( } let model = s.field_model.as_ref()?; let recent_frames: Vec> = - node.frame_history.iter().cloned().collect(); + node.field_model_history.iter().cloned().collect(); let residual = model.empty_room_match(&recent_frames)?.residual_energy; let vital_signs_absent = s .latest_update @@ -7241,6 +7982,10 @@ async fn calibration_refine_bootstrap( metadata.source_model_id == expected_model_id && metadata.content_sha256 == expected_digest && metadata.source_node_ids.as_slice() == [source_node_id] + && CsiGridKey::from_bootstrap_grid(metadata.source_grid) == expected_grid + }) + && s.calibration_grid_binding.is_some_and(|binding| { + binding.source_node_id == source_node_id && binding.grid == expected_grid }); if !identity_matches { return Json(serde_json::json!({ @@ -7298,6 +8043,7 @@ async fn calibration_refine_bootstrap( &path, &installation_id, &[source_node_id], + expected_grid.as_bootstrap_grid(), &expected_model_id, created_at_unix_ms, &candidate, @@ -7350,6 +8096,7 @@ async fn calibration_cancel(State(state): State) -> Json) -> Json) -> Json) -> Json std::path::PathBuf { initial } +fn validate_startup_calibration_request(calibrate: bool) -> Result<(), &'static str> { + if calibrate { + Err("--calibrate cannot safely select a source and CSI grid at process start. Start the server normally, wait at least 10 seconds for live grid evidence, then POST /api/v1/calibration/start with one explicit source_node_id.") + } else { + Ok(()) + } +} + #[tokio::main] async fn main() { // Initialize tracing; with the `otel` feature and @@ -9510,6 +10285,11 @@ async fn main() { let mut args = Args::parse(); args.ui_path = coalesce_ui_path(args.ui_path); + if let Err(error) = validate_startup_calibration_request(args.calibrate) { + eprintln!("Error: {error}"); + std::process::exit(2); + } + // Handle --benchmark mode: run vital sign benchmark and exit if args.benchmark { eprintln!("Running vital sign detection benchmark (1000 frames)..."); @@ -10455,17 +11235,25 @@ async fn main() { "Default Room", engine_bridge_multistatic_cfg, ), - field_model: if args.calibrate { - info!("Field model calibration enabled — room should be empty during startup"); - FieldModel::new(field_bridge::single_link_config()).ok() - } else { - bootstrap_field_model - }, + field_model: bootstrap_field_model, installation_id: args.installation_id.clone(), - bootstrap_baseline: bootstrap_metadata, + bootstrap_baseline: bootstrap_metadata.clone(), bootstrap_baseline_active, - calibration_model_id: args.calibrate.then(opaque_calibration_model_id), - calibration_source_node_ids: std::collections::BTreeSet::new(), + calibration_model_id: None, + calibration_source_node_ids: bootstrap_metadata + .as_ref() + .map(|metadata| metadata.source_node_ids.iter().copied().collect()) + .unwrap_or_default(), + calibration_grid_binding: bootstrap_metadata.as_ref().and_then(|metadata| { + let [source_node_id] = metadata.source_node_ids.as_slice() else { + return None; + }; + Some(CalibrationGridBinding { + source_node_id: *source_node_id, + grid: CsiGridKey::from_bootstrap_grid(metadata.source_grid), + evidence: None, + }) + }), calibration_last_sequences: HashMap::new(), calibration_reordered_packets: HashMap::new(), calibration_max_reorder_depth: HashMap::new(), From 97578ee401469570adfbc2450abcf3a18d6dda38 Mon Sep 17 00:00:00 2001 From: ruv Date: Sun, 6 Sep 2026 11:51:09 -0400 Subject: [PATCH 27/27] fix(firmware): make host dead stripping portable --- firmware/esp32-csi-node/test/Makefile | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/firmware/esp32-csi-node/test/Makefile b/firmware/esp32-csi-node/test/Makefile index 19c82dbec..f07145574 100644 --- a/firmware/esp32-csi-node/test/Makefile +++ b/firmware/esp32-csi-node/test/Makefile @@ -39,6 +39,15 @@ CFLAGS = -fsanitize=fuzzer,address,undefined -g -O1 \ STUBS_SRC = stubs/esp_stubs.c MAIN_DIR = ../main +# GNU ld and Apple's ld spell dead section elimination differently. +# Keep the host sanitation test portable across the Linux CI runner and macOS. +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) +DEAD_STRIP_LDFLAGS := -Wl,-dead_strip +else +DEAD_STRIP_LDFLAGS := -Wl,--gc-sections +endif + # Default fuzz duration (seconds) and jobs FUZZ_DURATION ?= 30 FUZZ_JOBS ?= 1 @@ -55,7 +64,7 @@ all: fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detec # unrelated device-only callback code out of this host executable. test_csi_sanitize: test_csi_sanitize.c $(MAIN_DIR)/csi_collector.c $(STUBS_SRC) cc -std=c99 -Wall -Wextra -Istubs -I$(MAIN_DIR) -ffunction-sections \ - $^ -Wl,-dead_strip -o $@ -lm + $^ $(DEAD_STRIP_LDFLAGS) -o $@ -lm run_csi_sanitize: test_csi_sanitize ./test_csi_sanitize