From 6dee88d03a18fc377e623bda38cb72ea43b3862f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 12:13:07 +0000 Subject: [PATCH 01/29] build(deps-dev): bump eslint-plugin-react-hooks in /ui Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 7.0.1 to 7.1.1. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.1/packages/eslint-plugin-react-hooks) --- updated-dependencies: - dependency-name: eslint-plugin-react-hooks dependency-version: 7.1.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- ui/package-lock.json | 10 +++++----- ui/package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 44d2c2073..6daea8388 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -19,7 +19,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^17.5.0", "prettier": "^3.8.3", @@ -2328,9 +2328,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { @@ -2344,7 +2344,7 @@ "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react-refresh": { diff --git a/ui/package.json b/ui/package.json index 54bbc7c45..9e194606c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -24,7 +24,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^17.5.0", "prettier": "^3.8.3", From 2064d26dc2fe946ba124a4602a31c3a643cc2feb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 12:13:16 +0000 Subject: [PATCH 02/29] build(deps-dev): bump globals from 17.5.0 to 17.6.0 in /ui Bumps [globals](https://github.com/sindresorhus/globals) from 17.5.0 to 17.6.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v17.5.0...v17.6.0) --- updated-dependencies: - dependency-name: globals dependency-version: 17.6.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- ui/package-lock.json | 8 ++++---- ui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 44d2c2073..6c66e56a1 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -21,7 +21,7 @@ "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^17.5.0", + "globals": "^17.6.0", "prettier": "^3.8.3", "typescript": "~6.0.3", "typescript-eslint": "^8.46.4", @@ -2600,9 +2600,9 @@ } }, "node_modules/globals": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", - "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { diff --git a/ui/package.json b/ui/package.json index 54bbc7c45..aac80442c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -26,7 +26,7 @@ "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^17.5.0", + "globals": "^17.6.0", "prettier": "^3.8.3", "typescript": "~6.0.3", "typescript-eslint": "^8.46.4", From 77f849eca38eed87ffc27b1fd5bb72c6500828c6 Mon Sep 17 00:00:00 2001 From: JedS Date: Sun, 12 Apr 2026 20:10:48 +0200 Subject: [PATCH 03/29] feat: add physics-based ball flight simulator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `ballistics.py` with a deterministic drag + Magnus ODE simulator (RK4, 500 Hz) and a `resolve_launch()` helper that substitutes club-typical spin when measured spin confidence is below the "high" threshold. Output is a single committed trajectory — no probabilistic range — so it composes cleanly with future simulator handoffs. Aerodynamic model uses Hill-type Cl(Sp) and linear Cd(Sp) fits consistent with Bearman & Harvey (1976) and Kensrud & Smith (2018) for dimpled balls past the drag crisis. Spin decays exponentially at 4%/s per Kiratidis & Leinweber (2018). No UI or server changes — module is standalone. Tests cover launch-resolution policy and end-to-end trajectory sanity across driver, iron, fade/draw, and launch-angle sweeps. Co-Authored-By: Claude Opus 4.6 --- src/openflight/ballistics.py | 363 +++++++++++++++++++++++++++++++++++ tests/test_ballistics.py | 170 ++++++++++++++++ 2 files changed, 533 insertions(+) create mode 100644 src/openflight/ballistics.py create mode 100644 tests/test_ballistics.py diff --git a/src/openflight/ballistics.py b/src/openflight/ballistics.py new file mode 100644 index 000000000..ec6f18d2f --- /dev/null +++ b/src/openflight/ballistics.py @@ -0,0 +1,363 @@ +""" +Physics-based golf ball flight simulator. + +Integrates the drag + Magnus ODE using RK4 to produce a deterministic +trajectory from launch conditions. When measured spin is low-confidence +or missing, `resolve_launch` substitutes a club-typical spin value so the +output remains committable (no probabilistic range). + +Coordinate system (world frame): + x — downrange, target direction + y — lateral, +right + z — height, up + +Aerodynamic model: Cd and Cl are functions of the spin parameter +Sp = r*omega / v. Fits are consistent with Bearman & Harvey (1976) and +Kensrud & Smith (2018) measurements for dimpled golf balls in the +post-drag-crisis regime (Re ~ 5e4 to 2e5). Spin decay follows +Kiratidis & Leinweber (2018) at ~4%/s. +""" + +import math +from dataclasses import dataclass +from typing import Literal, Optional + +from .launch_monitor import ClubType, Shot + +MPH_TO_MPS = 0.44704 +MPS_TO_MPH = 1.0 / MPH_TO_MPS +M_TO_YD = 1.09361 + +# USGA maximum-conforming ball (45.93 g, 42.7 mm diameter). +# Using the max — not an average — so carry estimates are upper-bounded by +# the rules rather than by a guess at the specific ball in play. +BALL_MASS_KG = 0.04593 +BALL_RADIUS_M = 0.02135 +BALL_AREA_M2 = math.pi * BALL_RADIUS_M ** 2 +AIR_DENSITY_STD = 1.225 # kg/m³ at sea level, 15 °C ISA + +# Cd = CD_BASE + CD_SPIN_COEFF * Sp +# Linear rise with spin parameter Sp = r·ω/v. +# Cl = CL_SATURATION * Sp / (CL_HALF_SP + Sp) +# Hill-type saturating form: Cl → CL_SATURATION as Sp → ∞, +# reaches CL_SATURATION/2 at Sp = CL_HALF_SP. +# These are simple parametric forms consistent with Bearman & Harvey (1976) +# and Kensrud & Smith (2018) for dimpled balls past the drag crisis +# (Re ~ 5e4–2e5), which covers the full range of realistic golf shots. +CD_BASE = 0.24 +CD_SPIN_COEFF = 0.18 +CL_SATURATION = 0.30 +CL_HALF_SP = 0.15 + +# Exponential spin decay: ω(t) = ω₀·exp(-rate·t). +# ~4%/s per Kiratidis & Leinweber (2018); small but matters over ~6 s flights. +SPIN_DECAY_RATE = 0.04 + +GRAVITY = 9.81 +# 500 Hz integration. RK4 error is O(dt⁵) so this is effectively exact for +# the timescales involved; larger dt starts to visibly shorten long drives. +DT_SECONDS = 0.002 +# Safety cap — real shots terminate in 5–9 s; anything longer implies the +# solver went unstable or physical inputs are pathological. +MAX_FLIGHT_SECONDS = 15.0 +# Trajectory sample cadence for the returned point list. Integration still +# runs at DT_SECONDS; this only controls how many points the caller sees, +# keeping payload size reasonable for UI/log consumers. +SAMPLE_INTERVAL_S = 0.05 + +# Must match the "high" threshold in Shot.spin_quality (launch_monitor.py). +# Below this we treat the radar measurement as unreliable and substitute +# club-typical spin rather than letting noise poison the trajectory. +SPIN_CONFIDENCE_HIGH = 0.7 + +# Club-typical spin (RPM) from TrackMan PGA Tour averages. +# Used as fallback when measured spin is missing or low-confidence. +CLUB_TYPICAL_SPIN_RPM: dict[ClubType, float] = { + ClubType.DRIVER: 2700, + ClubType.WOOD_3: 3500, + ClubType.WOOD_5: 4200, + ClubType.WOOD_7: 4800, + ClubType.HYBRID_3: 4400, + ClubType.HYBRID_5: 4900, + ClubType.HYBRID_7: 5300, + ClubType.HYBRID_9: 5800, + ClubType.IRON_2: 4000, + ClubType.IRON_3: 4500, + ClubType.IRON_4: 5000, + ClubType.IRON_5: 5400, + ClubType.IRON_6: 6000, + ClubType.IRON_7: 6500, + ClubType.IRON_8: 7500, + ClubType.IRON_9: 8500, + ClubType.PW: 9000, + ClubType.GW: 9500, + ClubType.SW: 10000, + ClubType.LW: 10500, + ClubType.UNKNOWN: 5000, +} + + +@dataclass +class LaunchConditions: + """Deterministic launch parameters for the physics model or simulator export.""" + + ball_speed_mph: float + launch_angle_v: float + launch_angle_h: float + spin_rpm: float + spin_axis_deg: float + spin_source: Literal["measured", "club_typical"] + + +@dataclass +class TrajectoryPoint: + t: float + x: float + y: float + z: float + speed_mph: float + spin_rpm: float + + +@dataclass +class Trajectory: + points: list[TrajectoryPoint] + carry_yards: float + apex_yards: float + lateral_yards: float + flight_time_s: float + landing_speed_mph: float + landing_angle_deg: float + + @property + def total_yards(self) -> float: + """Carry plus a simple rollout estimate (flatter landings roll farther).""" + # Rough heuristic, not a physical model: a flat landing (~20°) adds + # ~28 yd, a steep wedge landing (~60°) adds ~15 yd. Surface firmness, + # wetness, and slope are ignored — fine for a display hint, not for + # course-play distance. + rollout = max(0.0, 30.0 * math.cos(math.radians(self.landing_angle_deg))) + return self.carry_yards + rollout + + +def resolve_launch(shot: Shot) -> Optional[LaunchConditions]: + """ + Produce committed launch conditions from a shot. + + Returns None if the vertical launch angle is unavailable (no physics + simulation possible without it). Spin is taken from the measurement only + when confidence >= SPIN_CONFIDENCE_HIGH; otherwise a club-typical value + is substituted and `spin_source` is set to "club_typical". + """ + if shot.launch_angle_vertical is None: + return None + + use_measured = ( + shot.spin_rpm is not None + and shot.spin_confidence is not None + and shot.spin_confidence >= SPIN_CONFIDENCE_HIGH + ) + if use_measured: + spin_rpm = float(shot.spin_rpm) + source: Literal["measured", "club_typical"] = "measured" + else: + spin_rpm = CLUB_TYPICAL_SPIN_RPM.get( + shot.club, CLUB_TYPICAL_SPIN_RPM[ClubType.UNKNOWN] + ) + source = "club_typical" + + return LaunchConditions( + ball_speed_mph=shot.ball_speed_mph, + launch_angle_v=shot.launch_angle_vertical, + launch_angle_h=shot.launch_angle_horizontal or 0.0, + spin_rpm=spin_rpm, + spin_axis_deg=shot.spin_axis_deg or 0.0, + spin_source=source, + ) + + +def _cd(sp: float) -> float: + return CD_BASE + CD_SPIN_COEFF * sp + + +def _cl(sp: float) -> float: + return CL_SATURATION * sp / (CL_HALF_SP + sp) if sp > 0 else 0.0 + + +def _derivatives( + state: tuple, + omega: float, + axis: tuple, + air_density: float, +) -> tuple: + """d/dt of (x, y, z, vx, vy, vz) under gravity + drag + Magnus.""" + _, _, _, vx, vy, vz = state + v = math.sqrt(vx * vx + vy * vy + vz * vz) + # At v ≈ 0 both drag and Magnus vanish (F ∝ v²) and division below + # would be unsafe. Should only happen at the instantaneous apex of a + # pathological straight-up launch. + if v < 1e-6: + return (vx, vy, vz, 0.0, 0.0, -GRAVITY) + + # Spin parameter Sp drives both Cd and Cl. Note ω is tracked separately + # (not in the state tuple) because its decay is exponential, not an ODE + # we want to couple into RK4. + sp = BALL_RADIUS_M * omega / v + cd = _cd(sp) + cl = _cl(sp) + + # q: dynamic-pressure × reference-area / mass = acceleration per unit C. + # Pre-dividing by mass here lets us return accelerations directly. + q = 0.5 * air_density * v * v * BALL_AREA_M2 / BALL_MASS_KG + drag_a = cd * q + lift_a = cl * q + + # Drag opposes velocity: -v̂ · |F_drag|. + ax = -drag_a * vx / v + ay = -drag_a * vy / v + az = -drag_a * vz / v + + # Magnus direction is (ω̂ × v̂). We compute axis × velocity and normalize + # by the cross-product magnitude rather than |ω|·|v|·sin(θ), which avoids + # an extra sin() and gracefully degrades when axis ∥ v (spin does no work). + ox, oy, oz = axis + cx = oy * vz - oz * vy + cy = oz * vx - ox * vz + cz = ox * vy - oy * vx + c_mag = math.sqrt(cx * cx + cy * cy + cz * cz) + if c_mag > 1e-6: + ax += lift_a * cx / c_mag + ay += lift_a * cy / c_mag + az += lift_a * cz / c_mag + + az -= GRAVITY + + return (vx, vy, vz, ax, ay, az) + + +def _rk4_step( + state: tuple, + omega: float, + axis: tuple, + air_density: float, + dt: float, +) -> tuple: + k1 = _derivatives(state, omega, axis, air_density) + s2 = tuple(state[i] + 0.5 * dt * k1[i] for i in range(6)) + k2 = _derivatives(s2, omega, axis, air_density) + s3 = tuple(state[i] + 0.5 * dt * k2[i] for i in range(6)) + k3 = _derivatives(s3, omega, axis, air_density) + s4 = tuple(state[i] + dt * k3[i] for i in range(6)) + k4 = _derivatives(s4, omega, axis, air_density) + return tuple( + state[i] + (dt / 6.0) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) + for i in range(6) + ) + + +def simulate( + conditions: LaunchConditions, + air_density: float = AIR_DENSITY_STD, + dt: float = DT_SECONDS, +) -> Trajectory: + """ + Integrate flight from launch to first ground contact (z = 0). + """ + v0 = conditions.ball_speed_mph * MPH_TO_MPS + la_v = math.radians(conditions.launch_angle_v) + la_h = math.radians(conditions.launch_angle_h) + + # Initial velocity in world frame (x downrange, y right, z up). + vx = v0 * math.cos(la_v) * math.cos(la_h) + vy = v0 * math.cos(la_v) * math.sin(la_h) + vz = v0 * math.sin(la_v) + + # Spin axis convention: + # spin_axis_deg = 0 → pure backspin, axis = -y + # (ω × v with v ≈ +x gives Magnus = +z, i.e. lift up — correct) + # spin_axis_deg > 0 → top of axis tilts toward +y (right); + # Magnus gains a +y component → fade/slice. + # spin_axis_deg < 0 → draw/hook. + # World-frame (not velocity-frame) definition, matching launch-monitor + # and simulator conventions where spin axis is specified at launch. + axis_rad = math.radians(conditions.spin_axis_deg) + axis = (0.0, -math.cos(axis_rad), math.sin(axis_rad)) + + # rpm → rad/s. Decays exponentially each step; see loop below. + omega = conditions.spin_rpm * 2 * math.pi / 60.0 + + points: list[TrajectoryPoint] = [ + TrajectoryPoint(0.0, 0.0, 0.0, 0.0, conditions.ball_speed_mph, conditions.spin_rpm) + ] + + state = (0.0, 0.0, 0.0, vx, vy, vz) + t = 0.0 + max_z = 0.0 + last_sample_t = 0.0 + + while t < MAX_FLIGHT_SECONDS: + new_state = _rk4_step(state, omega, axis, air_density, dt) + t += dt + omega *= math.exp(-SPIN_DECAY_RATE * dt) + + _, _, z, _, _, _ = new_state + if z > max_z: + max_z = z + + # Ground contact. Linear interpolation between prev and new state + # is sufficient because dt (2 ms) is tiny relative to the timescale + # of the descent — the state is effectively linear across one step + # even though the full trajectory is not. + if z <= 0.0 and t > dt: + prev_z = state[2] + denom = prev_z - z + frac = prev_z / denom if abs(denom) > 1e-9 else 0.0 + t_hit = (t - dt) + frac * dt + final = tuple(state[i] + frac * (new_state[i] - state[i]) for i in range(6)) + fx, fy, fz, fvx, fvy, fvz = final + v_final = math.sqrt(fvx * fvx + fvy * fvy + fvz * fvz) + landing_angle = math.degrees( + math.atan2(-fvz, math.sqrt(fvx * fvx + fvy * fvy)) + ) + points.append(TrajectoryPoint( + t_hit, + fx * M_TO_YD, fy * M_TO_YD, max(fz, 0.0) * M_TO_YD, + v_final * MPS_TO_MPH, + omega * 60 / (2 * math.pi), + )) + return Trajectory( + points=points, + carry_yards=fx * M_TO_YD, + apex_yards=max_z * M_TO_YD, + lateral_yards=fy * M_TO_YD, + flight_time_s=t_hit, + landing_speed_mph=v_final * MPS_TO_MPH, + landing_angle_deg=landing_angle, + ) + + state = new_state + + if t - last_sample_t >= SAMPLE_INTERVAL_S: + sx_, sy_, sz_, svx, svy, svz = state + v = math.sqrt(svx * svx + svy * svy + svz * svz) + points.append(TrajectoryPoint( + t, + sx_ * M_TO_YD, sy_ * M_TO_YD, sz_ * M_TO_YD, + v * MPS_TO_MPH, + omega * 60 / (2 * math.pi), + )) + last_sample_t = t + + # Flight did not terminate — return current state as best-effort + fx, fy, fz, fvx, fvy, fvz = state + v_final = math.sqrt(fvx * fvx + fvy * fvy + fvz * fvz) + landing_angle = math.degrees(math.atan2(-fvz, math.sqrt(fvx * fvx + fvy * fvy))) + return Trajectory( + points=points, + carry_yards=fx * M_TO_YD, + apex_yards=max_z * M_TO_YD, + lateral_yards=fy * M_TO_YD, + flight_time_s=t, + landing_speed_mph=v_final * MPS_TO_MPH, + landing_angle_deg=landing_angle, + ) diff --git a/tests/test_ballistics.py b/tests/test_ballistics.py new file mode 100644 index 000000000..4b4ff085c --- /dev/null +++ b/tests/test_ballistics.py @@ -0,0 +1,170 @@ +"""Tests for the ballistics flight simulator and launch resolution.""" + +from datetime import datetime + +import pytest + +from openflight.ballistics import ( + CLUB_TYPICAL_SPIN_RPM, + LaunchConditions, + resolve_launch, + simulate, +) +from openflight.launch_monitor import ClubType, Shot + + +def _shot(**kwargs) -> Shot: + defaults = dict( + ball_speed_mph=160.0, + timestamp=datetime.now(), + club=ClubType.DRIVER, + launch_angle_vertical=12.0, + ) + defaults.update(kwargs) + return Shot(**defaults) + + +class TestResolveLaunch: + def test_returns_none_without_vertical_launch_angle(self): + shot = _shot(launch_angle_vertical=None) + assert resolve_launch(shot) is None + + def test_uses_measured_spin_when_high_confidence(self): + shot = _shot(spin_rpm=2500, spin_confidence=0.85) + cond = resolve_launch(shot) + assert cond is not None + assert cond.spin_rpm == 2500 + assert cond.spin_source == "measured" + + def test_uses_club_typical_when_low_confidence(self): + shot = _shot(spin_rpm=1500, spin_confidence=0.3, club=ClubType.DRIVER) + cond = resolve_launch(shot) + assert cond is not None + assert cond.spin_rpm == CLUB_TYPICAL_SPIN_RPM[ClubType.DRIVER] + assert cond.spin_source == "club_typical" + + def test_uses_club_typical_when_spin_missing(self): + shot = _shot(spin_rpm=None, club=ClubType.IRON_7) + cond = resolve_launch(shot) + assert cond is not None + assert cond.spin_rpm == CLUB_TYPICAL_SPIN_RPM[ClubType.IRON_7] + assert cond.spin_source == "club_typical" + + def test_medium_confidence_still_falls_back(self): + # Medium confidence (~0.5) is below the high threshold — use typical. + shot = _shot(spin_rpm=3000, spin_confidence=0.5) + cond = resolve_launch(shot) + assert cond is not None + assert cond.spin_source == "club_typical" + + def test_defaults_horizontal_angle_to_zero(self): + shot = _shot(launch_angle_horizontal=None) + cond = resolve_launch(shot) + assert cond.launch_angle_h == 0.0 + + def test_defaults_spin_axis_to_zero(self): + shot = _shot(spin_axis_deg=None) + cond = resolve_launch(shot) + assert cond.spin_axis_deg == 0.0 + + +def _driver(spin_rpm=2700, launch=11.0, ball_speed=165.0, axis=0.0, la_h=0.0): + return LaunchConditions( + ball_speed_mph=ball_speed, + launch_angle_v=launch, + launch_angle_h=la_h, + spin_rpm=spin_rpm, + spin_axis_deg=axis, + spin_source="measured", + ) + + +class TestSimulate: + def test_driver_carry_in_expected_range(self): + # 165 mph ball speed / 11° / 2700 RPM is close to PGA Tour averages. + # TrackMan data: ~270–285 yards carry. + traj = simulate(_driver()) + assert 250 <= traj.carry_yards <= 300, ( + f"Driver carry {traj.carry_yards:.1f} yd outside plausible range" + ) + + def test_iron_carry_in_expected_range(self): + # 7-iron: 120 mph ball speed, 17° launch, 6500 RPM → ~160-180 yd + cond = LaunchConditions( + ball_speed_mph=120.0, + launch_angle_v=17.0, + launch_angle_h=0.0, + spin_rpm=6500, + spin_axis_deg=0.0, + spin_source="measured", + ) + traj = simulate(cond) + assert 140 <= traj.carry_yards <= 200, ( + f"7-iron carry {traj.carry_yards:.1f} yd outside plausible range" + ) + + def test_higher_launch_produces_higher_apex(self): + low = simulate(_driver(launch=8.0)) + high = simulate(_driver(launch=15.0)) + assert high.apex_yards > low.apex_yards + + def test_more_spin_produces_higher_apex(self): + low_spin = simulate(_driver(spin_rpm=1800)) + high_spin = simulate(_driver(spin_rpm=3500)) + assert high_spin.apex_yards > low_spin.apex_yards + + def test_fade_lands_right_of_target(self): + traj = simulate(_driver(axis=10.0)) # +axis = fade + assert traj.lateral_yards > 3.0 + + def test_draw_lands_left_of_target(self): + traj = simulate(_driver(axis=-10.0)) # -axis = draw + assert traj.lateral_yards < -3.0 + + def test_straight_shot_stays_near_center(self): + traj = simulate(_driver(axis=0.0, la_h=0.0)) + assert abs(traj.lateral_yards) < 1.0 + + def test_horizontal_launch_offsets_landing(self): + # +la_h should push ball right + traj = simulate(_driver(la_h=2.0)) + assert traj.lateral_yards > 1.0 + + def test_trajectory_ends_at_ground(self): + traj = simulate(_driver()) + assert traj.points[-1].z <= 0.01 + assert traj.points[-1].t == pytest.approx(traj.flight_time_s, rel=0.01) + + def test_spin_decays_over_flight(self): + traj = simulate(_driver(spin_rpm=3000)) + final_spin = traj.points[-1].spin_rpm + # 4%/s for ~6s flight → ~80% of initial + assert 2300 < final_spin < 2900 + + def test_flight_time_reasonable(self): + traj = simulate(_driver()) + # Drivers typically spend 5-8 seconds in the air. + assert 4.0 < traj.flight_time_s < 9.0 + + def test_landing_angle_is_positive_descent(self): + traj = simulate(_driver()) + # Ball descends on landing — angle below horizontal is positive. + assert 20.0 < traj.landing_angle_deg < 60.0 + + def test_zero_launch_angle_does_not_crash(self): + # Extreme input should still produce a terminated trajectory. + cond = LaunchConditions( + ball_speed_mph=100.0, + launch_angle_v=0.5, + launch_angle_h=0.0, + spin_rpm=3000, + spin_axis_deg=0.0, + spin_source="measured", + ) + traj = simulate(cond) + assert traj.carry_yards > 0 + assert traj.flight_time_s < 5.0 + + def test_total_distance_includes_rollout(self): + traj = simulate(_driver()) + assert traj.total_yards > traj.carry_yards From 7b31c1d4753afd2ed4f35a767cad85e91731a1db Mon Sep 17 00:00:00 2001 From: JedS Date: Tue, 14 Apr 2026 13:36:30 +0200 Subject: [PATCH 04/29] Update ballistics.py Update base drag and saturation coefficients for modern dimpled balls as measured by Kensrud & Smith (2018) --- src/openflight/ballistics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openflight/ballistics.py b/src/openflight/ballistics.py index ec6f18d2f..745bec534 100644 --- a/src/openflight/ballistics.py +++ b/src/openflight/ballistics.py @@ -44,9 +44,9 @@ # These are simple parametric forms consistent with Bearman & Harvey (1976) # and Kensrud & Smith (2018) for dimpled balls past the drag crisis # (Re ~ 5e4–2e5), which covers the full range of realistic golf shots. -CD_BASE = 0.24 +CD_BASE = 0.205 CD_SPIN_COEFF = 0.18 -CL_SATURATION = 0.30 +CL_SATURATION = 0.32 CL_HALF_SP = 0.15 # Exponential spin decay: ω(t) = ω₀·exp(-rate·t). From 9a099bd29cb6263c7dbd61dbf971b25d7b68c7d1 Mon Sep 17 00:00:00 2001 From: JedS Date: Tue, 14 Apr 2026 22:12:01 +0200 Subject: [PATCH 05/29] Share SPIN_CONFIDENCE_HIGH between launch_monitor and ballistics --- src/openflight/ballistics.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/openflight/ballistics.py b/src/openflight/ballistics.py index 745bec534..85a53ed2c 100644 --- a/src/openflight/ballistics.py +++ b/src/openflight/ballistics.py @@ -22,7 +22,7 @@ from dataclasses import dataclass from typing import Literal, Optional -from .launch_monitor import ClubType, Shot +from .launch_monitor import SPIN_CONFIDENCE_HIGH, ClubType, Shot MPH_TO_MPS = 0.44704 MPS_TO_MPH = 1.0 / MPH_TO_MPS @@ -65,11 +65,6 @@ # keeping payload size reasonable for UI/log consumers. SAMPLE_INTERVAL_S = 0.05 -# Must match the "high" threshold in Shot.spin_quality (launch_monitor.py). -# Below this we treat the radar measurement as unreliable and substitute -# club-typical spin rather than letting noise poison the trajectory. -SPIN_CONFIDENCE_HIGH = 0.7 - # Club-typical spin (RPM) from TrackMan PGA Tour averages. # Used as fallback when measured spin is missing or low-confidence. CLUB_TYPICAL_SPIN_RPM: dict[ClubType, float] = { From 0b061f4d74ab848653da3e1210b52a8cee6bb9f8 Mon Sep 17 00:00:00 2001 From: JedS Date: Sat, 25 Apr 2026 13:14:16 +0200 Subject: [PATCH 06/29] Use ballistics simulator for shot carry; table is now fallback only --- src/openflight/server.py | 62 ++++++++++++++++++++------------- tests/test_server.py | 75 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 24 deletions(-) diff --git a/src/openflight/server.py b/src/openflight/server.py index d5a54d314..6f20be0ba 100644 --- a/src/openflight/server.py +++ b/src/openflight/server.py @@ -20,6 +20,7 @@ from flask_cors import CORS from flask_socketio import SocketIO +from .ballistics import resolve_launch, simulate from .launch_monitor import ClubType, Shot from .ops243 import Direction, SpeedReading, set_show_raw_readings from .rolling_buffer.monitor import estimate_carry_with_spin, get_optimal_spin_for_ball_speed @@ -1737,32 +1738,45 @@ def on_shot_detected(shot: Shot): # rejected or missing axes fall back to conservative estimates. _ensure_user_facing_launch_angles(shot) - # Compute spin-adjusted carry using measured spin (if reliable) or club average + # Compute carry. Prefer the physics simulator (drag + Magnus, RK4) when a + # vertical launch angle is available; fall back to the table estimator only + # when the angle is missing (resolve_launch returns None). _MIN_RELIABLE_SPIN_CONF = 0.6 if shot.carry_spin_adjusted is None and shot.mode != "mock": - has_reliable_spin = ( - shot.spin_rpm - and shot.spin_rpm > 0 - and shot.spin_confidence is not None - and shot.spin_confidence >= _MIN_RELIABLE_SPIN_CONF - ) - spin_for_carry = ( - shot.spin_rpm - if has_reliable_spin - else get_optimal_spin_for_ball_speed(shot.ball_speed_mph, shot.club) - ) - shot.carry_spin_adjusted = estimate_carry_with_spin( - shot.ball_speed_mph, - spin_for_carry, - shot.club, - club_speed_mph=shot.club_speed_mph, - ) - logger.info( - "[SERVER] Spin-adjusted carry: %.0f yds (spin: %.0f rpm%s)", - shot.carry_spin_adjusted, - spin_for_carry, - "" if shot.spin_rpm and shot.spin_rpm > 0 else " avg", - ) + conditions = resolve_launch(shot) + if conditions is not None: + trajectory = simulate(conditions) + shot.carry_spin_adjusted = trajectory.carry_yards + logger.info( + "[SERVER] Ballistic carry: %.0f yds (spin: %.0f rpm, source: %s)", + shot.carry_spin_adjusted, + conditions.spin_rpm, + conditions.spin_source, + ) + else: + has_reliable_spin = ( + shot.spin_rpm + and shot.spin_rpm > 0 + and shot.spin_confidence is not None + and shot.spin_confidence >= _MIN_RELIABLE_SPIN_CONF + ) + spin_for_carry = ( + shot.spin_rpm + if has_reliable_spin + else get_optimal_spin_for_ball_speed(shot.ball_speed_mph, shot.club) + ) + shot.carry_spin_adjusted = estimate_carry_with_spin( + shot.ball_speed_mph, + spin_for_carry, + shot.club, + club_speed_mph=shot.club_speed_mph, + ) + logger.info( + "[SERVER] Table carry (no launch angle): %.0f yds (spin: %.0f rpm%s)", + shot.carry_spin_adjusted, + spin_for_carry, + "" if shot.spin_rpm and shot.spin_rpm > 0 else " avg", + ) if shot.spin_rejection_reason: logger.info( "[SERVER] Spin unavailable: %s (snr=%s, candidate=%s rpm)", diff --git a/tests/test_server.py b/tests/test_server.py index 74fd46546..286e05d3c 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1880,3 +1880,78 @@ def reset(self): assert shot.angle_source == "radar" assert shot.launch_angle_vertical == pytest.approx(18.7) assert shot.launch_angle_horizontal == pytest.approx(0.0) + + +class TestCarryComputation: + """Tests for the ballistic carry path in on_shot_detected.""" + + def _patch_environment(self, monkeypatch): + monkeypatch.setattr(server_module, "kld7_vertical", None) + monkeypatch.setattr(server_module, "kld7_horizontal", None) + monkeypatch.setattr(server_module, "camera_tracker", None) + monkeypatch.setattr(server_module, "camera_enabled", False) + monkeypatch.setattr(server_module, "monitor", None) + monkeypatch.setattr(server_module, "debug_mode", False) + monkeypatch.setattr(server_module, "get_session_logger", lambda: None) + monkeypatch.setattr(server_module.socketio, "emit", lambda *args, **kwargs: None) + + def test_carry_uses_ballistic_simulator_when_launch_angle_present(self, monkeypatch): + """A shot with a vertical launch angle should get carry from the physics sim.""" + self._patch_environment(monkeypatch) + + captured = {} + + from openflight import ballistics as ballistics_module + real_simulate = ballistics_module.simulate + + def spying_simulate(conditions, *args, **kwargs): + captured["conditions"] = conditions + return real_simulate(conditions, *args, **kwargs) + + monkeypatch.setattr(server_module, "simulate", spying_simulate) + + shot = Shot( + ball_speed_mph=165.0, + club_speed_mph=112.0, + timestamp=datetime.now(), + club=ClubType.DRIVER, + launch_angle_vertical=11.0, + launch_angle_confidence=0.8, + spin_rpm=2700, + spin_confidence=0.85, + angle_source="radar", + ) + + on_shot_detected(shot) + + assert "conditions" in captured, "simulate() should have been called" + assert captured["conditions"].spin_source == "measured" + assert shot.carry_spin_adjusted is not None + assert 250 < shot.carry_spin_adjusted < 300 + + def test_carry_falls_back_to_table_when_resolve_returns_none(self, monkeypatch): + """When resolve_launch returns None, the table path should compute carry.""" + self._patch_environment(monkeypatch) + + monkeypatch.setattr(server_module, "resolve_launch", lambda shot: None) + + def fail_simulate(*args, **kwargs): + raise AssertionError("simulate() must not be called when resolve_launch is None") + + monkeypatch.setattr(server_module, "simulate", fail_simulate) + + shot = Shot( + ball_speed_mph=150.0, + club_speed_mph=105.0, + timestamp=datetime.now(), + club=ClubType.DRIVER, + launch_angle_vertical=12.0, + spin_rpm=2700, + spin_confidence=0.85, + angle_source="radar", + ) + + on_shot_detected(shot) + + assert shot.carry_spin_adjusted is not None + assert shot.carry_spin_adjusted > 0 From 660dec6f1a784f822435a6f64898756b747d7419 Mon Sep 17 00:00:00 2001 From: JedS Date: Thu, 4 Jun 2026 19:03:28 +0200 Subject: [PATCH 07/29] Add ballistics validation + Cd/Cl coefficient sweep tooling scripts/analysis/validate_ballistics.py runs the model on TrackMan inputs (physics-only check) and on OpenFlight inputs (end-to-end check), comparing each prediction to TrackMan's normalized flat carry. Air density defaults to 1.184 kg/m^3 to match TM's 77F/sea-level normalization. Per-shot CSV, per-club + per-session stats, and a scatter plot per source. scripts/analysis/sweep_ballistic_coeffs.py fits the four ballistics.py aero constants (CD_BASE, CD_SPIN_COEFF, CL_SATURATION, CL_HALF_SP) via differential-evolution + Nelder-Mead refinement against TrackMan flat carry. Optional --loso flag does leave-one-session-out cross-validation so we can see whether the fit generalizes. Both scripts accept multiple --trackman / --comparison files with matching --session-label values; shots are concatenated, tagged with their origin, and broken out per-session in the reports and scatters (club -> color, session -> marker). --- scripts/analysis/sweep_ballistic_coeffs.py | 566 +++++++++++++++++ scripts/analysis/validate_ballistics.py | 686 +++++++++++++++++++++ 2 files changed, 1252 insertions(+) create mode 100644 scripts/analysis/sweep_ballistic_coeffs.py create mode 100644 scripts/analysis/validate_ballistics.py diff --git a/scripts/analysis/sweep_ballistic_coeffs.py b/scripts/analysis/sweep_ballistic_coeffs.py new file mode 100644 index 000000000..35514a5e1 --- /dev/null +++ b/scripts/analysis/sweep_ballistic_coeffs.py @@ -0,0 +1,566 @@ +"""Tune the four aerodynamic coefficients in +:mod:`openflight.ballistics` against TrackMan-measured carry. + +Parameters swept: + CD_BASE — drag coefficient at zero spin + CD_SPIN_COEFF — slope of Cd(Sp) (linear in spin parameter) + CL_SATURATION — Cl asymptote at high Sp + CL_HALF_SP — Sp at which Cl reaches CL_SATURATION/2 + +Method: scipy.optimize.differential_evolution (global) followed by +Nelder-Mead refinement. Loss is overall RMSE on the TrackMan-inputs +shots (model fed TM's own measurements vs TM's flat carry). + +Air density is set to 1.184 kg/m³ to match TrackMan's "Flat" +normalization (no wind, 0 ft alt, 77 °F). + +Usage (single session):: + + uv run python scripts/analysis/sweep_ballistic_coeffs.py \\ + --trackman session_logs/OpenFlight-Test.Normalized.csv \\ + --output-dir session_logs/sweep_20260506 + +Usage (multiple sessions, with leave-one-session-out CV):: + + uv run python scripts/analysis/sweep_ballistic_coeffs.py \\ + --trackman session_logs/tm_a.csv session_logs/tm_b.csv \\ + --session-label may06 jun02 \\ + --loso \\ + --output-dir session_logs/sweep_multi + +Multi-session shots are concatenated for the main fit. ``--loso`` then +refits on N-1 sessions per fold and scores on the held-out one — if the +held-out RMSE is much worse than the fit RMSE, the optimizer is +absorbing session-specific noise and the coefficients won't generalize. + +The script does not modify ballistics.py — it prints the optimal +coefficients and writes a stats/scatter report so the change can be +reviewed before being committed. +""" + +from __future__ import annotations + +import argparse +import csv +import math +import statistics +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Tuple + +import numpy as np +from scipy.optimize import differential_evolution, minimize + +_REPO_ROOT = Path(__file__).resolve().parents[2] +if str(_REPO_ROOT / "src") not in sys.path: + sys.path.insert(0, str(_REPO_ROOT / "src")) +if str(_REPO_ROOT / "scripts" / "analysis") not in sys.path: + sys.path.insert(0, str(_REPO_ROOT / "scripts" / "analysis")) + +import openflight.ballistics as bl # noqa: E402 +from openflight.ballistics import LaunchConditions, simulate # noqa: E402 + +# Reuse the TM CSV loader from the validation script so the data path is +# identical between the two tools. +from validate_ballistics import ( # noqa: E402 + TM_FLAT_AIR_DENSITY, + TMShot, + _default_session_label, + _normalize_club, + load_trackman, +) + + +# Bounds for the search. Wide enough to span published Cd/Cl ranges for +# dimpled golf balls in the post-drag-crisis regime; narrow enough that +# differential evolution converges in a few minutes. +PARAM_BOUNDS: List[Tuple[float, float]] = [ + (0.16, 0.28), # CD_BASE + (0.00, 0.40), # CD_SPIN_COEFF + (0.18, 0.42), # CL_SATURATION + (0.03, 0.35), # CL_HALF_SP +] +PARAM_NAMES = ["CD_BASE", "CD_SPIN_COEFF", "CL_SATURATION", "CL_HALF_SP"] +DEFAULT_COEFFS = ( + bl.CD_BASE, + bl.CD_SPIN_COEFF, + bl.CL_SATURATION, + bl.CL_HALF_SP, +) + + +@dataclass +class FitResult: + coeffs: Tuple[float, float, float, float] + rmse: float + preds: List[float] + + +def _build_conditions(s: TMShot) -> LaunchConditions: + return LaunchConditions( + ball_speed_mph=s.ball_speed_mph, + launch_angle_v=s.launch_v_deg, + launch_angle_h=s.launch_h_deg if s.launch_h_deg is not None else 0.0, + spin_rpm=s.spin_rpm, + spin_axis_deg=s.spin_axis_deg if s.spin_axis_deg is not None else 0.0, + spin_source="measured", + ) + + +def _filter_shots(shots: List[TMShot]) -> List[TMShot]: + """Keep only shots with the inputs the model needs + a measured carry.""" + out = [] + for s in shots: + if s.ball_speed_mph is None or s.launch_v_deg is None: + continue + if s.spin_rpm is None or s.carry_yards is None: + continue + out.append(s) + return out + + +def simulate_with_coeffs( + shots: List[TMShot], + coeffs: Tuple[float, float, float, float], +) -> List[float]: + """Monkey-patch the ballistics module constants, run simulate() for + every shot, restore the originals on exit. + + Relies on ``ballistics._cd`` and ``ballistics._cl`` resolving the + constants at call time from the module's global namespace. + """ + saved = (bl.CD_BASE, bl.CD_SPIN_COEFF, bl.CL_SATURATION, bl.CL_HALF_SP) + bl.CD_BASE, bl.CD_SPIN_COEFF, bl.CL_SATURATION, bl.CL_HALF_SP = coeffs + try: + results = [] + for s in shots: + traj = simulate(_build_conditions(s), air_density=TM_FLAT_AIR_DENSITY) + results.append(traj.carry_yards) + return results + finally: + bl.CD_BASE, bl.CD_SPIN_COEFF, bl.CL_SATURATION, bl.CL_HALF_SP = saved + + +def make_loss(shots: List[TMShot], measured: np.ndarray): + """Closure that the optimizer can call with a 4-vector.""" + def _loss(x: np.ndarray) -> float: + preds = simulate_with_coeffs(shots, tuple(x)) + return float(np.sqrt(np.mean((np.asarray(preds) - measured) ** 2))) + return _loss + + +def evaluate( + shots: List[TMShot], + measured: np.ndarray, + coeffs: Tuple[float, float, float, float], +) -> FitResult: + preds = simulate_with_coeffs(shots, coeffs) + rmse = float(np.sqrt(np.mean((np.asarray(preds) - measured) ** 2))) + return FitResult(coeffs=coeffs, rmse=rmse, preds=preds) + + +# --------------------------------------------------------------------------- +# Reporting +# --------------------------------------------------------------------------- + +def _stats(deltas: List[float]) -> Dict[str, float]: + if not deltas: + return {"n": 0, "mean": float("nan"), "stdev": float("nan"), + "rmse": float("nan"), "mae": float("nan"), "max_abs": float("nan")} + mean = statistics.fmean(deltas) + stdev = statistics.pstdev(deltas) if len(deltas) > 1 else 0.0 + rmse = math.sqrt(sum(d * d for d in deltas) / len(deltas)) + mae = sum(abs(d) for d in deltas) / len(deltas) + return {"n": len(deltas), "mean": mean, "stdev": stdev, + "rmse": rmse, "mae": mae, "max_abs": max(abs(d) for d in deltas)} + + +def format_table( + label: str, + shots: List[TMShot], + measured: List[float], + preds: List[float], +) -> str: + """Stats block grouped by club + (when multiple) by session.""" + by_club: Dict[str, List[float]] = {} + by_session: Dict[str, List[float]] = {} + for s, m, p in zip(shots, measured, preds): + delta = p - m + by_club.setdefault(_normalize_club(s.club_raw), []).append(delta) + by_session.setdefault(s.session or "default", []).append(delta) + + overall = _stats([p - m for p, m in zip(preds, measured)]) + lines = [f"=== {label} ==="] + lines.append( + f"OVERALL n={overall['n']:3d} bias={overall['mean']:+6.2f} yd " + f"rmse={overall['rmse']:5.2f} yd mae={overall['mae']:5.2f} yd " + f"max|d|={overall['max_abs']:5.2f} yd" + ) + lines.append("By club:") + lines.append(f" {'club':14s} {'n':>3s} {'bias':>8s} {'rmse':>6s} " + f"{'mae':>6s} {'max|d|':>7s}") + for club in sorted(by_club): + s = _stats(by_club[club]) + lines.append( + f" {club:14s} {s['n']:3d} {s['mean']:+7.2f} {s['rmse']:6.2f} " + f"{s['mae']:6.2f} {s['max_abs']:7.2f}" + ) + if len(by_session) > 1: + lines.append("By session:") + lines.append(f" {'session':24s} {'n':>3s} {'bias':>8s} {'rmse':>6s} " + f"{'mae':>6s} {'max|d|':>7s}") + for sess in sorted(by_session): + s = _stats(by_session[sess]) + sess_disp = sess if len(sess) <= 24 else sess[:21] + "..." + lines.append( + f" {sess_disp:24s} {s['n']:3d} {s['mean']:+7.2f} {s['rmse']:6.2f} " + f"{s['mae']:6.2f} {s['max_abs']:7.2f}" + ) + return "\n".join(lines) + + +def write_per_shot_csv( + shots: List[TMShot], + measured: List[float], + preds_default: List[float], + preds_fit: List[float], + out_path: Path, +) -> None: + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path, "w", newline="", encoding="utf-8") as fh: + w = csv.writer(fh) + w.writerow([ + "session", "shot_index", "club", + "ball_speed_mph", "launch_v_deg", "launch_h_deg", + "spin_rpm", "spin_axis_deg", + "measured_carry_yards", + "model_default_yards", "delta_default", + "model_fit_yards", "delta_fit", + ]) + for s, m, pd_, pf in zip(shots, measured, preds_default, preds_fit): + w.writerow([ + s.session or "default", + s.shot_index, _normalize_club(s.club_raw), + f"{s.ball_speed_mph:.2f}", f"{s.launch_v_deg:.2f}", + f"{(s.launch_h_deg or 0.0):.2f}", + f"{s.spin_rpm:.0f}", f"{(s.spin_axis_deg or 0.0):.2f}", + f"{m:.2f}", + f"{pd_:.2f}", f"{pd_ - m:+.2f}", + f"{pf:.2f}", f"{pf - m:+.2f}", + ]) + + +def write_scatter( + shots: List[TMShot], + measured: List[float], + preds_default: List[float], + preds_fit: List[float], + out_path: Path, + fit_coeffs: Tuple[float, float, float, float], +) -> None: + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + out_path.parent.mkdir(parents=True, exist_ok=True) + clubs = sorted({_normalize_club(s.club_raw) for s in shots}) + sessions = sorted({(s.session or "default") for s in shots}) + color_for = {c: plt.cm.tab10(i % 10) for i, c in enumerate(clubs)} + marker_cycle = ["o", "s", "^", "D", "v", "P", "X", "*", "<", ">"] + marker_for = {s: marker_cycle[i % len(marker_cycle)] for i, s in enumerate(sessions)} + single_session = len(sessions) == 1 + + fig, axes = plt.subplots(1, 2, figsize=(13, 6.5), sharex=True, sharey=True) + + for ax, preds, label in ( + (axes[0], preds_default, "default"), + (axes[1], preds_fit, "fit"), + ): + for club in clubs: + for sess in sessions: + xs, ys = [], [] + for s, m, p in zip(shots, measured, preds): + if _normalize_club(s.club_raw) != club: + continue + if (s.session or "default") != sess: + continue + xs.append(m) + ys.append(p) + if not xs: + continue + leg_label = ( + f"{club} (n={len(xs)})" if single_session + else f"{club}/{sess} (n={len(xs)})" + ) + ax.scatter(xs, ys, label=leg_label, + color=color_for[club], marker=marker_for[sess], + s=42, alpha=0.85, edgecolor="k", linewidth=0.5) + all_x = list(measured) + all_y = preds + lo = min(min(all_x), min(all_y)) * 0.95 + hi = max(max(all_x), max(all_y)) * 1.05 + ax.plot([lo, hi], [lo, hi], "k--", lw=1, label="1:1") + ax.fill_between([lo, hi], [lo - 5, hi - 5], [lo + 5, hi + 5], + color="grey", alpha=0.15, label="+/-5 yd") + deltas = np.asarray(preds) - np.asarray(measured) + rmse = float(np.sqrt(np.mean(deltas ** 2))) + bias = float(np.mean(deltas)) + ax.set_title(f"{label}: rmse={rmse:.2f} yd bias={bias:+.2f} yd") + ax.set_xlabel("Measured carry (TM Flat, yd)") + ax.set_ylabel("Model carry (yd)") + ax.set_xlim(lo, hi) + ax.set_ylim(lo, hi) + ax.set_aspect("equal") + ax.grid(True, alpha=0.3) + ax.legend(loc="best", fontsize=8) + + fig.suptitle( + f"Default vs fit (fit: CD_BASE={fit_coeffs[0]:.4f}, " + f"CD_SPIN_COEFF={fit_coeffs[1]:.4f}, " + f"CL_SATURATION={fit_coeffs[2]:.4f}, " + f"CL_HALF_SP={fit_coeffs[3]:.4f})", + fontsize=11, + ) + fig.tight_layout() + fig.savefig(out_path, dpi=140) + plt.close(fig) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main(argv=None) -> int: + try: + sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined] + except (AttributeError, ValueError): + pass + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--trackman", required=True, type=Path, nargs="+", + help="One or more TrackMan normalized CSVs. Shots " + "from all files are concatenated for the fit.") + parser.add_argument("--session-label", required=False, type=str, nargs="+", + help="Optional explicit session label per --trackman " + "file (default: filename stem with common " + "prefixes trimmed).") + parser.add_argument("--output-dir", required=True, type=Path) + parser.add_argument("--seed", type=int, default=42, + help="DE seed for reproducibility.") + parser.add_argument("--de-popsize", type=int, default=12, + help="Differential-evolution population size.") + parser.add_argument("--de-maxiter", type=int, default=40, + help="Differential-evolution max iterations.") + parser.add_argument("--de-tol", type=float, default=1e-3, + help="Differential-evolution tolerance.") + parser.add_argument("--loso", action="store_true", + help="Leave-one-session-out cross-validation: for " + "each session, refit on the others and report " + "the held-out RMSE. Only meaningful with " + "multiple --trackman files.") + args = parser.parse_args(argv) + + # Validate inputs. + for p in args.trackman: + if not p.exists(): + print(f"TrackMan CSV not found: {p}", file=sys.stderr) + return 2 + if args.session_label and len(args.session_label) != len(args.trackman): + print( + f"--session-label count ({len(args.session_label)}) must match " + f"--trackman count ({len(args.trackman)})", + file=sys.stderr, + ) + return 2 + + # Load all sessions, tagged with their labels. + labels = args.session_label or [_default_session_label(p) for p in args.trackman] + all_shots: List[TMShot] = [] + for path, label in zip(args.trackman, labels): + shots_in = load_trackman(path, session=label) + usable = _filter_shots(shots_in) + print(f"Loaded {len(shots_in)} TM shots from {path.name} [{label}] " + f"({len(usable)} usable)") + all_shots.extend(usable) + shots = all_shots + if not shots: + print("No usable shots loaded.", file=sys.stderr) + return 2 + measured = np.array([s.carry_yards for s in shots], dtype=float) + print(f"Total usable shots across {len(args.trackman)} session(s): {len(shots)}") + + # Baseline with current ballistics.py constants. + baseline = evaluate(shots, measured, DEFAULT_COEFFS) + print(f"Baseline RMSE: {baseline.rmse:.3f} yd " + f"(coeffs: {DEFAULT_COEFFS})") + + loss_fn = make_loss(shots, measured) + + # --- Global search: differential evolution --- + eval_counter = {"n": 0} + + def _logged_loss(x): + eval_counter["n"] += 1 + return loss_fn(x) + + print(f"\nDifferential evolution: popsize={args.de_popsize}, " + f"maxiter={args.de_maxiter}, seed={args.seed}") + t0 = time.time() + de_result = differential_evolution( + _logged_loss, + bounds=PARAM_BOUNDS, + seed=args.seed, + maxiter=args.de_maxiter, + popsize=args.de_popsize, + tol=args.de_tol, + polish=False, # we do our own Nelder-Mead refinement + updating="deferred", # vectorizable, but more importantly deterministic w/ seed + workers=1, + disp=False, + ) + de_elapsed = time.time() - t0 + print(f"DE converged in {eval_counter['n']} evals " + f"({de_elapsed:.1f}s). Best RMSE: {de_result.fun:.3f} yd") + print(f" -> {dict(zip(PARAM_NAMES, de_result.x))}") + + # --- Local refinement: Nelder-Mead from DE best --- + print("\nNelder-Mead refinement...") + t1 = time.time() + nm = minimize( + loss_fn, + x0=de_result.x, + method="Nelder-Mead", + bounds=PARAM_BOUNDS, + options={"xatol": 1e-5, "fatol": 1e-4, "maxiter": 400}, + ) + nm_elapsed = time.time() - t1 + print(f"NM finished in {nm.nit} iterations ({nm_elapsed:.1f}s). " + f"RMSE: {nm.fun:.3f} yd") + + # Pick whichever is better (NM should be ≤ DE). + if nm.fun <= de_result.fun: + best_x = tuple(float(v) for v in nm.x) + best_rmse = float(nm.fun) + else: + best_x = tuple(float(v) for v in de_result.x) + best_rmse = float(de_result.fun) + fit = evaluate(shots, measured, best_x) + assert abs(fit.rmse - best_rmse) < 1e-6, (fit.rmse, best_rmse) + + # --- Report --- + args.output_dir.mkdir(parents=True, exist_ok=True) + + table_default = format_table( + "Default coefficients", + shots, list(measured), baseline.preds, + ) + table_fit = format_table( + "Fit coefficients", + shots, list(measured), fit.preds, + ) + + fit_lines = [ + "Optimal coefficients (TM-inputs, RMSE objective):", + f" CD_BASE = {best_x[0]:.5f} (default {DEFAULT_COEFFS[0]:.5f})", + f" CD_SPIN_COEFF = {best_x[1]:.5f} (default {DEFAULT_COEFFS[1]:.5f})", + f" CL_SATURATION = {best_x[2]:.5f} (default {DEFAULT_COEFFS[2]:.5f})", + f" CL_HALF_SP = {best_x[3]:.5f} (default {DEFAULT_COEFFS[3]:.5f})", + "", + f"Baseline RMSE: {baseline.rmse:.3f} yd", + f"Fit RMSE: {fit.rmse:.3f} yd", + f"Improvement: {baseline.rmse - fit.rmse:+.3f} yd " + f"({100 * (baseline.rmse - fit.rmse) / baseline.rmse:+.1f}%)", + "", + table_default, + "", + table_fit, + ] + + # --- Leave-one-session-out cross-validation --- + # Only emit when --loso is requested AND there are multiple sessions. + # This is the honest generalization check: refit on N-1 sessions, score + # on the held-out one. If held-out RMSE is much worse than fit RMSE, + # the optimizer is finding session-specific quirks (overfit). + sessions_in_data = sorted({s.session or "default" for s in shots}) + if args.loso and len(sessions_in_data) > 1: + loso_lines = ["", "=== Leave-one-session-out cross-validation ==="] + loso_lines.append( + f"{'held-out':24s} {'n_test':>6s} {'fit_RMSE':>9s} " + f"{'test_RMSE':>10s} {'test_bias':>10s} fit_coeffs" + ) + for held_out in sessions_in_data: + train_shots = [s for s in shots if (s.session or "default") != held_out] + test_shots = [s for s in shots if (s.session or "default") == held_out] + if not train_shots or not test_shots: + continue + train_measured = np.array([s.carry_yards for s in train_shots]) + test_measured = np.array([s.carry_yards for s in test_shots]) + print(f"\n LOSO refit (held out: {held_out}, " + f"n_train={len(train_shots)}, n_test={len(test_shots)})...") + sub_loss = make_loss(train_shots, train_measured) + sub_de = differential_evolution( + sub_loss, + bounds=PARAM_BOUNDS, + seed=args.seed, + maxiter=args.de_maxiter, + popsize=args.de_popsize, + tol=args.de_tol, + polish=False, + updating="deferred", + workers=1, + disp=False, + ) + sub_nm = minimize( + sub_loss, + x0=sub_de.x, + method="Nelder-Mead", + bounds=PARAM_BOUNDS, + options={"xatol": 1e-5, "fatol": 1e-4, "maxiter": 400}, + ) + sub_best = tuple(float(v) for v in ( + sub_nm.x if sub_nm.fun <= sub_de.fun else sub_de.x + )) + sub_fit_rmse = float(min(sub_nm.fun, sub_de.fun)) + # Score on held-out session with the refit coeffs. + test_preds = simulate_with_coeffs(test_shots, sub_best) + test_deltas = np.asarray(test_preds) - test_measured + test_rmse = float(np.sqrt(np.mean(test_deltas ** 2))) + test_bias = float(np.mean(test_deltas)) + ho_disp = held_out if len(held_out) <= 24 else held_out[:21] + "..." + loso_lines.append( + f"{ho_disp:24s} {len(test_shots):>6d} {sub_fit_rmse:>9.3f} " + f"{test_rmse:>10.3f} {test_bias:>+10.3f} " + f"({sub_best[0]:.3f}, {sub_best[1]:.3f}, " + f"{sub_best[2]:.3f}, {sub_best[3]:.3f})" + ) + fit_lines.extend(loso_lines) + elif args.loso: + fit_lines.extend([ + "", + "(LOSO requested but only one session loaded — skipped.)", + ]) + out_stats = args.output_dir / "sweep_stats.txt" + out_stats.write_text("\n".join(fit_lines), encoding="utf-8") + print() + print("\n".join(fit_lines)) + + write_per_shot_csv( + shots, list(measured), baseline.preds, fit.preds, + args.output_dir / "sweep_per_shot.csv", + ) + write_scatter( + shots, list(measured), baseline.preds, fit.preds, + args.output_dir / "sweep_scatter.png", + best_x, + ) + print(f"\nWrote {out_stats}") + print(f"Wrote {args.output_dir / 'sweep_per_shot.csv'}") + print(f"Wrote {args.output_dir / 'sweep_scatter.png'}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/analysis/validate_ballistics.py b/scripts/analysis/validate_ballistics.py new file mode 100644 index 000000000..d10a68180 --- /dev/null +++ b/scripts/analysis/validate_ballistics.py @@ -0,0 +1,686 @@ +"""Validate the ballistic model against paired OpenFlight + TrackMan data. + +Two independent comparisons are produced: + +1. **TM-inputs** — feed TrackMan's measured (ball_speed, launch_v, + launch_h, spin_rpm, spin_axis) into :func:`openflight.ballistics.simulate` + and compare the model's ``carry_yards`` to TrackMan's ``Carry Flat - + Length``. This isolates the physics model from any sensor noise. + +2. **OF-inputs** — feed OpenFlight's measured values for the same shots + into the model and compare to TrackMan's carry. This is the + end-to-end check the user actually sees on screen (sensor + model + error combined). Spin handling matches production + :func:`openflight.ballistics.resolve_launch`: measured spin if + confidence >= SPIN_CONFIDENCE_HIGH, otherwise club-typical fallback. + OpenFlight has no spin-axis measurement, so spin_axis = 0 is assumed. + +TrackMan's "Flat" carry is normalized to no-wind, 0 ft altitude, 77 °F. +We pass ``air_density = 1.184 kg/m³`` (≈ 25 °C sea level) so the model +runs in the same atmosphere TrackMan reports for — otherwise the default +1.225 kg/m³ (15 °C ISA) would add a systematic ~2 yd model bias on +driver-distance shots. + +Usage (single session):: + + uv run python scripts/analysis/validate_ballistics.py \\ + --trackman session_logs/OpenFlight-Test.Normalized.csv \\ + --comparison session_logs/comparison_20260506.csv \\ + --output-dir session_logs/validation_20260506 + +Usage (multiple sessions — concatenates shots, tags each with a session +label, and emits per-session stats alongside the per-club stats):: + + uv run python scripts/analysis/validate_ballistics.py \\ + --trackman session_logs/tm_session_a.csv session_logs/tm_session_b.csv \\ + --comparison session_logs/comp_a.csv session_logs/comp_b.csv \\ + --session-label may06 jun02 \\ + --output-dir session_logs/validation_multi + +``--comparison`` (and ``--session-label``) must match the count of +``--trackman`` when provided. When ``--comparison`` is omitted the +OF-inputs run is skipped. +""" + +from __future__ import annotations + +import argparse +import csv +import json +import math +import statistics +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +# Make the in-repo package importable when running via `uv run python ...` +# from the repo root (uv usually arranges this, but we also support a +# plain `python scripts/analysis/validate_ballistics.py` invocation). +_REPO_ROOT = Path(__file__).resolve().parents[2] +if str(_REPO_ROOT / "src") not in sys.path: + sys.path.insert(0, str(_REPO_ROOT / "src")) + +from openflight.ballistics import ( # noqa: E402 + CLUB_TYPICAL_SPIN_RPM, + LaunchConditions, + simulate, +) +from openflight.launch_monitor import SPIN_CONFIDENCE_HIGH, ClubType # noqa: E402 + +# TrackMan "Flat" normalization: no wind, 0 ft altitude, 77 °F. +# ρ = P / (R_specific · T) with P = 101325 Pa, T = 298.15 K, R = 287.05 J/(kg·K) +# → 1.1839 kg/m³. We round to 1.184 for the input. +TM_FLAT_AIR_DENSITY = 1.184 + +# Club-name → ClubType map. The comparison CSV uses normalized names like +# "driver", "7-iron", "pw"; the raw TM CSV uses "7 Iron", "Driver", "PW". +_CLUB_MAP: Dict[str, ClubType] = { + "driver": ClubType.DRIVER, + "3-wood": ClubType.WOOD_3, + "5-wood": ClubType.WOOD_5, + "7-wood": ClubType.WOOD_7, + "3-hybrid": ClubType.HYBRID_3, + "5-hybrid": ClubType.HYBRID_5, + "7-hybrid": ClubType.HYBRID_7, + "9-hybrid": ClubType.HYBRID_9, + "2-iron": ClubType.IRON_2, + "3-iron": ClubType.IRON_3, + "4-iron": ClubType.IRON_4, + "5-iron": ClubType.IRON_5, + "6-iron": ClubType.IRON_6, + "7-iron": ClubType.IRON_7, + "8-iron": ClubType.IRON_8, + "9-iron": ClubType.IRON_9, + "pw": ClubType.PW, + "gw": ClubType.GW, + "sw": ClubType.SW, + "lw": ClubType.LW, +} + + +def _normalize_club(raw: Optional[str]) -> str: + """Lower-case, collapse "7 Iron" / "7-iron" / "iron 7" → "7-iron" etc.""" + if raw is None: + return "" + s = str(raw).strip().lower().replace("_", "-") + if not s: + return "" + # Strip trailing punctuation. + s = s.rstrip(".") + # "7 iron" → "7-iron", "iron 7" → "7-iron" + parts = s.replace("-", " ").split() + if len(parts) == 2: + a, b = parts + if a.isdigit() and b in ("iron", "i", "wood", "w", "hybrid", "h"): + kind = {"iron": "iron", "i": "iron", "wood": "wood", "w": "wood", + "hybrid": "hybrid", "h": "hybrid"}[b] + return f"{a}-{kind}" + if b.isdigit() and a in ("iron", "wood", "hybrid"): + return f"{b}-{a}" + aliases = {"drv": "driver", "1-wood": "driver", "pitching-wedge": "pw", + "sand-wedge": "sw", "gap-wedge": "gw", "lob-wedge": "lw"} + return aliases.get(s, s) + + +def _club_type(name: str) -> ClubType: + return _CLUB_MAP.get(_normalize_club(name), ClubType.UNKNOWN) + + +def _to_float(v) -> Optional[float]: + if v is None or v == "": + return None + try: + return float(str(v).replace(",", "")) + except (TypeError, ValueError): + return None + + +@dataclass +class TMShot: + """Slim TrackMan record carrying only what the ballistic model needs.""" + + shot_index: int # row index in the CSV (1-based, after header/units rows) + club_raw: str + ball_speed_mph: Optional[float] + launch_v_deg: Optional[float] + launch_h_deg: Optional[float] + spin_rpm: Optional[float] + spin_axis_deg: Optional[float] + carry_yards: Optional[float] + timestamp: str + session: str = "" # session label (typically derived from the source filename) + + +def _default_session_label(path: Path) -> str: + """Cheap session label: filename stem, with any "OpenFlight-Test." + or "comparison_" prefix trimmed so output is readable.""" + stem = path.stem + for prefix in ("OpenFlight-Test.", "comparison_", "session_"): + if stem.startswith(prefix): + stem = stem[len(prefix):] + break + return stem or path.name + + +def load_trackman(path: Path, session: Optional[str] = None) -> List[TMShot]: + """Read a TrackMan "Normalized" CSV. Handles ``sep=,`` preamble and + the units row directly under the header. Each emitted shot is tagged + with ``session`` (defaulting to the file stem) so multi-session + aggregations can keep their origin straight.""" + session_label = session if session is not None else _default_session_label(path) + shots: List[TMShot] = [] + with open(path, "r", encoding="utf-8-sig", newline="") as fh: + # Skip optional ``sep=,`` Excel preamble. + pos = fh.tell() + first = fh.readline().lstrip("").lstrip() + if not first.lower().startswith("sep="): + fh.seek(pos) + reader = csv.DictReader(fh) + if reader.fieldnames is None: + return shots + # Trackman exports sometimes include a units row ("[mph]", "[deg]") + # right under the header. Detect by looking for "[..]" cells. + idx = 0 + for row in reader: + if any( + isinstance(v, str) and v.strip().startswith("[") and v.strip().endswith("]") + for v in row.values() + ): + continue + idx += 1 + shots.append(TMShot( + shot_index=idx, + club_raw=row.get("Club", "") or "", + ball_speed_mph=_to_float(row.get("Ball Speed")), + launch_v_deg=_to_float(row.get("Launch Angle")), + launch_h_deg=_to_float(row.get("Launch Direction")), + spin_rpm=_to_float(row.get("Spin Rate")), + spin_axis_deg=_to_float(row.get("Spin Axis")), + carry_yards=_to_float(row.get("Carry Flat - Length")), + timestamp=row.get("Date", "") or "", + session=session_label, + )) + return shots + + +@dataclass +class ComparisonRow: + """One row from the paired OF/TM comparison CSV.""" + + timestamp_of: str + club_raw: str + match_quality: str + # OpenFlight fields + ball_speed_of: Optional[float] + launch_v_of: Optional[float] + launch_h_of: Optional[float] + spin_of: Optional[float] + carry_of: Optional[float] + # TrackMan fields (for ground truth and fallback spin) + ball_speed_tm: Optional[float] + launch_v_tm: Optional[float] + launch_h_tm: Optional[float] + spin_tm: Optional[float] + spin_axis_tm: Optional[float] # may stay None — comparison CSV usually omits axis + carry_tm: Optional[float] + session: str = "" + + +def load_comparison(path: Path, session: Optional[str] = None) -> List[ComparisonRow]: + session_label = session if session is not None else _default_session_label(path) + rows: List[ComparisonRow] = [] + with open(path, "r", encoding="utf-8-sig", newline="") as fh: + reader = csv.DictReader(fh) + for r in reader: + rows.append(ComparisonRow( + timestamp_of=r.get("timestamp_of", "") or "", + club_raw=r.get("club", "") or "", + match_quality=r.get("match_quality", "") or "", + ball_speed_of=_to_float(r.get("ball_speed_of")), + launch_v_of=_to_float(r.get("launch_v_of")), + launch_h_of=_to_float(r.get("launch_h_of")), + spin_of=_to_float(r.get("spin_of")), + carry_of=_to_float(r.get("carry_of")), + ball_speed_tm=_to_float(r.get("ball_speed_tm")), + launch_v_tm=_to_float(r.get("launch_v_tm")), + launch_h_tm=_to_float(r.get("launch_h_tm")), + spin_tm=_to_float(r.get("spin_tm")), + spin_axis_tm=_to_float(r.get("spin_axis_tm")), + carry_tm=_to_float(r.get("carry_tm")), + session=session_label, + )) + return rows + + +def _spin_axis_lookup(tm_shots: List[TMShot]) -> Dict[Tuple[str, float, float], float]: + """Build (club, ball_speed_rounded, launch_v_rounded) -> spin_axis_deg + so comparison rows (which may lack spin_axis) can borrow TM's axis.""" + out: Dict[Tuple[str, float, float], float] = {} + for s in tm_shots: + if s.spin_axis_deg is None or s.ball_speed_mph is None or s.launch_v_deg is None: + continue + club = _normalize_club(s.club_raw) + key = (club, round(s.ball_speed_mph, 1), round(s.launch_v_deg, 1)) + out.setdefault(key, s.spin_axis_deg) + return out + + +@dataclass +class ValidationRow: + """One row of the per-shot validation CSV.""" + + source: str # "tm" or "of" + shot_label: str + club: str + session: str + ball_speed_mph: float + launch_v_deg: float + launch_h_deg: float + spin_rpm: float + spin_axis_deg: float + spin_source: str # "measured" / "club_typical" / "tm_borrowed" + measured_carry_yards: float + model_carry_yards: float + delta_yards: float # model - measured + + +def _has_required_inputs(*vals) -> bool: + return all(v is not None and not (isinstance(v, float) and math.isnan(v)) for v in vals) + + +def validate_tm_inputs( + tm_shots: List[TMShot], + air_density: float = TM_FLAT_AIR_DENSITY, +) -> List[ValidationRow]: + """Run the model on each TrackMan shot and compare to TM's flat carry.""" + out: List[ValidationRow] = [] + for s in tm_shots: + if not _has_required_inputs( + s.ball_speed_mph, s.launch_v_deg, s.spin_rpm, s.carry_yards + ): + continue + spin_axis = s.spin_axis_deg if s.spin_axis_deg is not None else 0.0 + launch_h = s.launch_h_deg if s.launch_h_deg is not None else 0.0 + conditions = LaunchConditions( + ball_speed_mph=s.ball_speed_mph, + launch_angle_v=s.launch_v_deg, + launch_angle_h=launch_h, + spin_rpm=s.spin_rpm, + spin_axis_deg=spin_axis, + spin_source="measured", + ) + traj = simulate(conditions, air_density=air_density) + session_tag = s.session or "default" + out.append(ValidationRow( + source="tm", + shot_label=f"tm#{s.shot_index}@{session_tag}", + club=_normalize_club(s.club_raw), + session=session_tag, + ball_speed_mph=s.ball_speed_mph, + launch_v_deg=s.launch_v_deg, + launch_h_deg=launch_h, + spin_rpm=s.spin_rpm, + spin_axis_deg=spin_axis, + spin_source="measured", + measured_carry_yards=s.carry_yards, + model_carry_yards=traj.carry_yards, + delta_yards=traj.carry_yards - s.carry_yards, + )) + return out + + +def _resolve_of_spin( + row: ComparisonRow, + club: ClubType, +) -> Tuple[float, str]: + """Mirror production resolve_launch(): use OF spin only when confident, + else club-typical fallback. The comparison CSV doesn't carry spin + confidence, so we treat any present OF spin as measured (this is the + same fallback the production server would log).""" + if row.spin_of is not None and row.spin_of > 0: + return row.spin_of, "measured" + typical = CLUB_TYPICAL_SPIN_RPM.get(club, CLUB_TYPICAL_SPIN_RPM[ClubType.UNKNOWN]) + return typical, "club_typical" + + +def validate_of_inputs( + comp_rows: List[ComparisonRow], + spin_axis_lookup: Dict[Tuple[str, float, float], float], + air_density: float = TM_FLAT_AIR_DENSITY, +) -> List[ValidationRow]: + """Run the model on OpenFlight inputs and compare to TrackMan's + flat carry. Skips rows flagged as ``ball_speed_mismatch`` etc.""" + out: List[ValidationRow] = [] + for i, row in enumerate(comp_rows, start=1): + if row.match_quality != "good": + continue + if not _has_required_inputs( + row.ball_speed_of, row.launch_v_of, row.carry_tm + ): + continue + club_enum = _club_type(row.club_raw) + spin_rpm, spin_source = _resolve_of_spin(row, club_enum) + # OF doesn't measure spin axis. Borrow TM's axis when we can + # match the shot; otherwise assume pure backspin (axis = 0). + spin_axis = 0.0 + if row.ball_speed_tm is not None and row.launch_v_tm is not None: + key = ( + _normalize_club(row.club_raw), + round(row.ball_speed_tm, 1), + round(row.launch_v_tm, 1), + ) + if key in spin_axis_lookup: + spin_axis = spin_axis_lookup[key] + launch_h = row.launch_h_of if row.launch_h_of is not None else 0.0 + conditions = LaunchConditions( + ball_speed_mph=row.ball_speed_of, + launch_angle_v=row.launch_v_of, + launch_angle_h=launch_h, + spin_rpm=spin_rpm, + spin_axis_deg=spin_axis, + spin_source="measured" if spin_source == "measured" else "club_typical", + ) + traj = simulate(conditions, air_density=air_density) + session_tag = row.session or "default" + out.append(ValidationRow( + source="of", + shot_label=f"of#{i}@{session_tag}@{row.timestamp_of[:19]}", + club=_normalize_club(row.club_raw), + session=session_tag, + ball_speed_mph=row.ball_speed_of, + launch_v_deg=row.launch_v_of, + launch_h_deg=launch_h, + spin_rpm=spin_rpm, + spin_axis_deg=spin_axis, + spin_source=spin_source, + measured_carry_yards=row.carry_tm, + model_carry_yards=traj.carry_yards, + delta_yards=traj.carry_yards - row.carry_tm, + )) + return out + + +# --------------------------------------------------------------------------- +# Output +# --------------------------------------------------------------------------- + +def write_per_shot_csv(rows: List[ValidationRow], path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", newline="", encoding="utf-8") as fh: + w = csv.writer(fh) + w.writerow([ + "source", "session", "shot_label", "club", + "ball_speed_mph", "launch_v_deg", "launch_h_deg", + "spin_rpm", "spin_axis_deg", "spin_source", + "measured_carry_yards", "model_carry_yards", + "delta_yards", "abs_delta_yards", + ]) + for r in rows: + w.writerow([ + r.source, r.session, r.shot_label, r.club, + f"{r.ball_speed_mph:.2f}", f"{r.launch_v_deg:.2f}", f"{r.launch_h_deg:.2f}", + f"{r.spin_rpm:.0f}", f"{r.spin_axis_deg:.2f}", r.spin_source, + f"{r.measured_carry_yards:.2f}", f"{r.model_carry_yards:.2f}", + f"{r.delta_yards:.2f}", f"{abs(r.delta_yards):.2f}", + ]) + + +def _stats(deltas: List[float]) -> Dict[str, float]: + if not deltas: + return {"n": 0, "mean": float("nan"), "stdev": float("nan"), + "rmse": float("nan"), "mae": float("nan"), "max_abs": float("nan")} + mean = statistics.fmean(deltas) + stdev = statistics.pstdev(deltas) if len(deltas) > 1 else 0.0 + rmse = math.sqrt(sum(d * d for d in deltas) / len(deltas)) + mae = sum(abs(d) for d in deltas) / len(deltas) + return { + "n": len(deltas), + "mean": mean, + "stdev": stdev, + "rmse": rmse, + "mae": mae, + "max_abs": max(abs(d) for d in deltas), + } + + +def format_stats(label: str, rows: List[ValidationRow]) -> str: + """Return a multi-line stats block: overall, per-club, per-session.""" + lines = [f"=== {label} ==="] + overall = _stats([r.delta_yards for r in rows]) + lines.append( + f"OVERALL n={overall['n']:3d} bias={overall['mean']:+6.2f} yd " + f"rmse={overall['rmse']:5.2f} yd mae={overall['mae']:5.2f} yd " + f"max|d|={overall['max_abs']:5.2f} yd" + ) + + # Per-club table + clubs = sorted({r.club for r in rows}) + lines.append("-" * 80) + lines.append("By club:") + lines.append(f" {'club':10s} {'n':>3s} {'bias':>8s} {'rmse':>6s} " + f"{'mae':>6s} {'max|d|':>7s} {'measured_n':>4s} {'fallback_n':>4s}") + for club in clubs: + club_rows = [r for r in rows if r.club == club] + s = _stats([r.delta_yards for r in club_rows]) + measured_n = sum(1 for r in club_rows if r.spin_source == "measured") + fallback_n = sum(1 for r in club_rows if r.spin_source == "club_typical") + lines.append( + f" {club:10s} {s['n']:3d} {s['mean']:+7.2f} {s['rmse']:6.2f} " + f"{s['mae']:6.2f} {s['max_abs']:7.2f} {measured_n:>10d} {fallback_n:>10d}" + ) + + # Per-session table — only shown when there's more than one session. + sessions = sorted({r.session for r in rows}) + if len(sessions) > 1: + lines.append("By session:") + lines.append(f" {'session':24s} {'n':>3s} {'bias':>8s} {'rmse':>6s} " + f"{'mae':>6s} {'max|d|':>7s}") + for sess in sessions: + sess_rows = [r for r in rows if r.session == sess] + s = _stats([r.delta_yards for r in sess_rows]) + sess_disp = sess if len(sess) <= 24 else sess[:21] + "..." + lines.append( + f" {sess_disp:24s} {s['n']:3d} {s['mean']:+7.2f} {s['rmse']:6.2f} " + f"{s['mae']:6.2f} {s['max_abs']:7.2f}" + ) + lines.append("") + return "\n".join(lines) + + +def write_scatter( + rows: List[ValidationRow], + title: str, + out_path: Path, +) -> None: + """Predicted-vs-measured scatter with 1:1 line and ±5 yd band.""" + try: + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + except ImportError: + print(f"[warn] matplotlib not available — skipping {out_path.name}", + file=sys.stderr) + return + + if not rows: + return + out_path.parent.mkdir(parents=True, exist_ok=True) + + # Color by club, marker by session (so multi-session runs are legible). + clubs = sorted({r.club for r in rows}) + sessions = sorted({r.session for r in rows}) + color_for = {c: plt.cm.tab10(i % 10) for i, c in enumerate(clubs)} + # Reused matplotlib markers — cycle if there are many sessions. + marker_cycle = ["o", "s", "^", "D", "v", "P", "X", "*", "<", ">"] + marker_for = {s: marker_cycle[i % len(marker_cycle)] for i, s in enumerate(sessions)} + + fig, ax = plt.subplots(figsize=(7, 7)) + for club in clubs: + for sess in sessions: + cr = [r for r in rows if r.club == club and r.session == sess] + if not cr: + continue + label = ( + f"{club} (n={len(cr)})" if len(sessions) == 1 + else f"{club}/{sess} (n={len(cr)})" + ) + ax.scatter( + [r.measured_carry_yards for r in cr], + [r.model_carry_yards for r in cr], + label=label, + color=color_for[club], + marker=marker_for[sess], + s=42, alpha=0.85, edgecolor="k", linewidth=0.5, + ) + all_x = [r.measured_carry_yards for r in rows] + all_y = [r.model_carry_yards for r in rows] + lo = min(min(all_x), min(all_y)) * 0.95 + hi = max(max(all_x), max(all_y)) * 1.05 + ax.plot([lo, hi], [lo, hi], "k--", lw=1, label="1:1") + ax.fill_between([lo, hi], [lo - 5, hi - 5], [lo + 5, hi + 5], + color="grey", alpha=0.15, label="+/-5 yd") + + stats = _stats([r.delta_yards for r in rows]) + ax.set_title( + f"{title}\nn={stats['n']} bias={stats['mean']:+.2f} yd " + f"rmse={stats['rmse']:.2f} yd mae={stats['mae']:.2f} yd" + ) + ax.set_xlabel("Measured carry (TrackMan Flat, yd)") + ax.set_ylabel("Model carry (yd)") + ax.set_xlim(lo, hi) + ax.set_ylim(lo, hi) + ax.set_aspect("equal") + ax.grid(True, alpha=0.3) + ax.legend(loc="best", fontsize=8) + fig.tight_layout() + fig.savefig(out_path, dpi=140) + plt.close(fig) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main(argv: Optional[List[str]] = None) -> int: + # Allow non-ASCII (e.g. arrows, Greek letters) in printed stats on + # Windows consoles whose default encoding is cp1252. + try: + sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined] + except (AttributeError, ValueError): + pass + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--trackman", required=True, type=Path, nargs="+", + help="One or more TrackMan normalized CSVs (each " + "contains Spin Axis column). Multiple files " + "are concatenated; each shot is tagged with a " + "session label derived from the filename " + "(override with --session-label).") + parser.add_argument("--comparison", required=False, type=Path, nargs="+", + help="Paired OF/TM CSV(s) from compare_trackman.py " + "(used for OF-inputs validation). When passing " + "multiple, count must match --trackman and " + "files are paired by position so the spin-axis " + "lookup uses the matching session's TM data.") + parser.add_argument("--session-label", required=False, type=str, nargs="+", + help="Optional explicit labels (one per --trackman). " + "Default = filename stem with prefixes trimmed.") + parser.add_argument("--output-dir", required=True, type=Path, + help="Directory for per-shot CSVs, stats.txt, scatter PNGs.") + parser.add_argument("--air-density", type=float, default=TM_FLAT_AIR_DENSITY, + help=f"Air density (kg/m³) for simulate(). Default " + f"{TM_FLAT_AIR_DENSITY} matches TrackMan 'Flat' (77°F sea level).") + args = parser.parse_args(argv) + + # Resolve session labels. + if args.session_label and len(args.session_label) != len(args.trackman): + print( + f"--session-label count ({len(args.session_label)}) must match " + f"--trackman count ({len(args.trackman)})", + file=sys.stderr, + ) + return 2 + labels = args.session_label or [_default_session_label(p) for p in args.trackman] + + # Validate input files exist. + for p in args.trackman: + if not p.exists(): + print(f"TrackMan CSV not found: {p}", file=sys.stderr) + return 2 + if args.comparison: + if len(args.comparison) != len(args.trackman): + print( + f"--comparison count ({len(args.comparison)}) must match " + f"--trackman count ({len(args.trackman)})", + file=sys.stderr, + ) + return 2 + for p in args.comparison: + if not p.exists(): + print(f"Comparison CSV not found: {p}", file=sys.stderr) + return 2 + + # Load all TM shots and all comparison rows, tagged with session. + all_tm_shots: List[TMShot] = [] + per_session_tm: List[List[TMShot]] = [] # for per-session axis lookup + for path, label in zip(args.trackman, labels): + shots = load_trackman(path, session=label) + print(f"Loaded {len(shots)} TM shots from {path.name} [{label}]") + all_tm_shots.extend(shots) + per_session_tm.append(shots) + + # --- TM inputs --- + tm_rows = validate_tm_inputs(all_tm_shots, air_density=args.air_density) + write_per_shot_csv(tm_rows, args.output_dir / "validation_tm_inputs.csv") + write_scatter( + tm_rows, + f"TM-inputs validation (rho={args.air_density:.3f} kg/m³)", + args.output_dir / "scatter_tm_inputs.png", + ) + stats_text = [format_stats( + f"TM-inputs (model fed TrackMan measurements, rho={args.air_density:.3f})", + tm_rows, + )] + + # --- OF inputs --- + of_rows: List[ValidationRow] = [] + if args.comparison: + all_comp_rows: List[ComparisonRow] = [] + for comp_path, label, tm_subset in zip(args.comparison, labels, per_session_tm): + comp = load_comparison(comp_path, session=label) + print(f"Loaded {len(comp)} paired rows from {comp_path.name} [{label}]") + # Build axis lookup from this session's TM data — keeps the + # mapping unambiguous when different sessions share ball speeds. + axis_lookup = _spin_axis_lookup(tm_subset) + of_rows.extend(validate_of_inputs( + comp, axis_lookup, air_density=args.air_density, + )) + all_comp_rows.extend(comp) + write_per_shot_csv(of_rows, args.output_dir / "validation_of_inputs.csv") + write_scatter( + of_rows, + f"OF-inputs end-to-end (rho={args.air_density:.3f} kg/m³)", + args.output_dir / "scatter_of_inputs.png", + ) + stats_text.append(format_stats( + f"OF-inputs (OpenFlight measurements → model → TM carry, rho={args.air_density:.3f})", + of_rows, + )) + + out_stats = args.output_dir / "stats.txt" + out_stats.parent.mkdir(parents=True, exist_ok=True) + out_stats.write_text("\n".join(stats_text), encoding="utf-8") + + # Echo to stdout so the test runner immediately sees the result. + print() + print("\n".join(stats_text)) + print(f"\nWrote {out_stats}") + print(f"Wrote {args.output_dir / 'validation_tm_inputs.csv'}") + if of_rows: + print(f"Wrote {args.output_dir / 'validation_of_inputs.csv'}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 24124cbb42600dca76083ce00243031a7c043ebe Mon Sep 17 00:00:00 2001 From: JedS Date: Thu, 4 Jun 2026 19:33:34 +0200 Subject: [PATCH 08/29] Add --no-ballistics toggle to fall back to legacy table carry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit server.py: new --no-ballistics CLI flag (default off, i.e. ballistics on). When set, on_shot_detected skips resolve_launch/simulate entirely and routes every non-mock shot through the table-based estimator. The table-path log message now identifies which condition triggered the fallback (no launch angle vs ballistics disabled), and the chosen mode is announced at server startup. start-kiosk.sh: parses --no-ballistics, forwards it to the server command, and logs the mode alongside the existing camera/trigger lines. tests/test_server.py: new test_carry_skips_ballistic_when_ballistics_disabled that pins the contract — when the flag is off, resolve_launch and simulate must not run even with a valid launch angle present. --- scripts/start-kiosk.sh | 13 +++++++++++++ src/openflight/server.py | 35 ++++++++++++++++++++++++++++++----- tests/test_server.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/scripts/start-kiosk.sh b/scripts/start-kiosk.sh index 57f2a1025..4f77be4f7 100755 --- a/scripts/start-kiosk.sh +++ b/scripts/start-kiosk.sh @@ -42,6 +42,7 @@ EXPERIMENTAL_KLD7_VERTICAL_IMPACT_ENERGY="" EXPERIMENTAL_KLD7_HORIZONTAL_IMPACT_ENERGY="" EXPERIMENTAL_KLD7_HORIZONTAL_RETRY_IMPACT_ENERGY="" EXPERIMENTAL_KLD7_HORIZONTAL_ANGLE_LIMIT="" +NO_BALLISTICS=false # Buffer split presets (pre/post trigger segments out of 32 total) # At 20ksps: each segment = 6.4ms, total buffer = 204.8ms @@ -198,6 +199,10 @@ while [[ $# -gt 0 ]]; do EXPERIMENTAL_KLD7_HORIZONTAL_ANGLE_LIMIT="$2" shift 2 ;; + --no-ballistics) + NO_BALLISTICS=true + shift + ;; --port|-p) PORT="$2" shift 2 @@ -321,6 +326,10 @@ if [ "$NO_CAMERA" = true ]; then SERVER_CMD="$SERVER_CMD --no-camera" fi +if [ "$NO_BALLISTICS" = true ]; then + SERVER_CMD="$SERVER_CMD --no-ballistics" +fi + if [ -n "$TRIGGER" ]; then SERVER_CMD="$SERVER_CMD --trigger $TRIGGER" fi @@ -483,6 +492,10 @@ else log "Camera enabled (Hough + ByteTrack)" fi +if [ "$NO_BALLISTICS" = true ]; then + log "Ballistic carry model disabled (using legacy table)" +fi + $SERVER_CMD & SERVER_PID=$! diff --git a/src/openflight/server.py b/src/openflight/server.py index 89b1a4eb5..11516d017 100644 --- a/src/openflight/server.py +++ b/src/openflight/server.py @@ -69,6 +69,12 @@ experimental_kld7_radc_tuning: bool = False experimental_kld7_raw_radc_logging: bool = False +# Ballistic model toggle. When True (default), shot carry comes from the +# physics simulator whenever a vertical launch angle is available. When +# False, all carry computations go through the legacy table estimator — +# useful for A/B comparison or as a fallback while the model is tuned. +ballistics_enabled: bool = True + _DEFAULT_KLD7_RADC_TUNING = { "radc_speed_tolerance_mph": 10.0, "radc_centroid_floor_frac": 0.5, @@ -1771,12 +1777,13 @@ def on_shot_detected(shot: Shot): # rejected or missing axes fall back to conservative estimates. _ensure_user_facing_launch_angles(shot) - # Compute carry. Prefer the physics simulator (drag + Magnus, RK4) when a - # vertical launch angle is available; fall back to the table estimator only - # when the angle is missing (resolve_launch returns None). + # Compute carry. Prefer the physics simulator (drag + Magnus, RK4) when + # ballistics is enabled and a vertical launch angle is available; fall + # back to the table estimator otherwise (either ballistics disabled or + # angle missing → resolve_launch returns None). _MIN_RELIABLE_SPIN_CONF = 0.6 if shot.carry_spin_adjusted is None and shot.mode != "mock": - conditions = resolve_launch(shot) + conditions = resolve_launch(shot) if ballistics_enabled else None if conditions is not None: trajectory = simulate(conditions) shot.carry_spin_adjusted = trajectory.carry_yards @@ -1804,8 +1811,10 @@ def on_shot_detected(shot: Shot): shot.club, club_speed_mph=shot.club_speed_mph, ) + reason = "ballistics disabled" if not ballistics_enabled else "no launch angle" logger.info( - "[SERVER] Table carry (no launch angle): %.0f yds (spin: %.0f rpm%s)", + "[SERVER] Table carry (%s): %.0f yds (spin: %.0f rpm%s)", + reason, shot.carry_spin_adjusted, spin_for_carry, "" if shot.spin_rpm and shot.spin_rpm > 0 else " avg", @@ -2313,6 +2322,15 @@ def main(): "--log-dir", help="Directory for session logs (default: ~/openflight_sessions)" ) parser.add_argument("--no-logging", action="store_true", help="Disable session logging") + parser.add_argument( + "--no-ballistics", + action="store_true", + help=( + "Disable the physics-based carry simulator. All shots fall back to " + "the legacy table-based carry estimator. Default: ballistics enabled " + "whenever a vertical launch angle is available." + ), + ) parser.add_argument( "--trigger", choices=["polling", "threshold", "speed", "sound"], @@ -2469,8 +2487,10 @@ def main(): global experimental_kld7_radc_tuning global experimental_kld7_raw_radc_logging global active_kld7_radc_tuning + global ballistics_enabled experimental_kld7_raw_radc_logging = args.experimental_kld7_raw_radc_logging experimental_kld7_radc_tuning = args.experimental_kld7_radc_tuning + ballistics_enabled = not args.no_ballistics kld7_radc_tuning_kwargs = _kld7_radc_tuning_kwargs(args) active_kld7_radc_tuning = dict(kld7_radc_tuning_kwargs) @@ -2500,6 +2520,11 @@ def main(): init_session_logger(enabled=False) print("Session logging DISABLED") + if ballistics_enabled: + print("Ballistic carry model: ENABLED (simulator + drag/Magnus)") + else: + print("Ballistic carry model: DISABLED (table fallback for all shots)") + # Configure radar logging if requested if args.radar_log: logging.basicConfig( diff --git a/tests/test_server.py b/tests/test_server.py index 4d9b0aa93..5c85a11a0 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -2053,3 +2053,36 @@ def fail_simulate(*args, **kwargs): assert shot.carry_spin_adjusted is not None assert shot.carry_spin_adjusted > 0 + + def test_carry_skips_ballistic_when_ballistics_disabled(self, monkeypatch): + """When ballistics_enabled is False, the simulator must not run even + if a valid launch angle is present — carry falls through to the + table estimator. Mirrors what `--no-ballistics` does on the CLI.""" + self._patch_environment(monkeypatch) + monkeypatch.setattr(server_module, "ballistics_enabled", False) + + def fail_resolve(*args, **kwargs): + raise AssertionError("resolve_launch must not run when ballistics disabled") + + def fail_simulate(*args, **kwargs): + raise AssertionError("simulate() must not run when ballistics disabled") + + monkeypatch.setattr(server_module, "resolve_launch", fail_resolve) + monkeypatch.setattr(server_module, "simulate", fail_simulate) + + shot = Shot( + ball_speed_mph=165.0, + club_speed_mph=112.0, + timestamp=datetime.now(), + club=ClubType.DRIVER, + launch_angle_vertical=11.0, + launch_angle_confidence=0.8, + spin_rpm=2700, + spin_confidence=0.85, + angle_source="radar", + ) + + on_shot_detected(shot) + + assert shot.carry_spin_adjusted is not None + assert shot.carry_spin_adjusted > 0 From 2ff01c88063c5ba26c991dad76205a21cd79b5c5 Mon Sep 17 00:00:00 2001 From: John Pacino Date: Sun, 7 Jun 2026 09:30:07 -0400 Subject: [PATCH 09/29] fix(ops): refresh clock sync per sound trigger --- src/openflight/ops243.py | 11 +- src/openflight/rolling_buffer/trigger.py | 259 +++++++++++++++++++---- tests/test_ops243.py | 10 + tests/test_rolling_buffer.py | 99 ++++++++- 4 files changed, 336 insertions(+), 43 deletions(-) diff --git a/src/openflight/ops243.py b/src/openflight/ops243.py index d1b7d6a73..1c157ac92 100644 --- a/src/openflight/ops243.py +++ b/src/openflight/ops243.py @@ -272,6 +272,7 @@ def read_clock_sync( per_read_timeout: float = 0.2, max_sync_duration_s: float = 1.25, sample_interval_s: float = 0.01, + store: bool = True, ) -> dict: """Map the OPS internal clock to host epoch via repeated ``C?`` reads. @@ -296,9 +297,10 @@ def read_clock_sync( Sound-triggered captures use this mapping to convert the radar's internal ``trigger_time`` to host epoch only when - ``usable_for_trigger_timestamps`` is true. Returns a summary dict (also - stored on ``self.last_clock_sync``); never raises on a missing/garbled - reply. + ``usable_for_trigger_timestamps`` is true. Returns a summary dict. By + default it is also stored on ``self.last_clock_sync``; pass + ``store=False`` for diagnostics that should not affect the live timing + path. Never raises on a missing/garbled reply. """ if not self.serial or not self.serial.is_open: raise ConnectionError("Not connected to radar") @@ -409,7 +411,8 @@ def find_integer_rollover() -> Optional[tuple[dict, dict]]: "rollover_uncertainty_ms": rollover_uncertainty_ms, "reads": reads, } - self.last_clock_sync = summary + if store: + self.last_clock_sync = summary if usable_for_trigger_timestamps: logger.info( "[OPS] Clock sync: method=%s offset=%.3fs best_read_latency=%.1fms " diff --git a/src/openflight/rolling_buffer/trigger.py b/src/openflight/rolling_buffer/trigger.py index 980359127..eb5f8c568 100644 --- a/src/openflight/rolling_buffer/trigger.py +++ b/src/openflight/rolling_buffer/trigger.py @@ -780,6 +780,11 @@ class SoundTrigger(TriggerStrategy): instead, which uses Pi GPIO (lower threshold) + software S! trigger. """ + CLOCK_SYNC_SAMPLES = 36 + CLOCK_SYNC_MAX_ROLLOVER_UNCERTAINTY_MS = 40.0 + CLOCK_SYNC_MAX_TIMEOUT_READ_MS = 50.0 + CLOCK_SYNC_MAX_FALLBACK_AGE_S = 60.0 + def __init__( self, pre_trigger_segments: int = 12, @@ -796,6 +801,207 @@ def __init__( """ super().__init__(pre_trigger_segments=pre_trigger_segments) + @staticmethod + def _clock_sync_last_read_host_time(clock_sync: dict) -> Optional[float]: + """Return the host time of the last C? read in a clock-sync summary.""" + reads = clock_sync.get("reads") or [] + if not reads or not isinstance(reads[-1], dict): + return None + return reads[-1].get("host_after") or reads[-1].get("host_mid") + + @classmethod + def _clock_sync_age_s(cls, clock_sync: dict) -> Optional[float]: + """Return age in seconds for a clock-sync summary.""" + last_host_time = cls._clock_sync_last_read_host_time(clock_sync) + if last_host_time is None: + return None + try: + return time.time() - float(last_host_time) + except (TypeError, ValueError): + return None + + @classmethod + def _clock_sync_quality(cls, clock_sync: object) -> tuple[bool, str]: + """Return whether a clock sync is trustworthy enough for shot timing.""" + if not isinstance(clock_sync, dict): + return False, "missing" + + if not clock_sync.get("usable_for_trigger_timestamps"): + return False, f"unusable_method:{clock_sync.get('clock_sync_method', 'unknown')}" + + if clock_sync.get("best_offset_s") is None: + return False, "missing_best_offset" + + reads = clock_sync.get("reads") or [] + slow_invalid_reads = [ + read + for read in reads + if isinstance(read, dict) + and read.get("radar_clock_s") is None + and (read.get("read_latency_ms") or 0.0) >= cls.CLOCK_SYNC_MAX_TIMEOUT_READ_MS + ] + if slow_invalid_reads: + return False, f"timeout_reads:{len(slow_invalid_reads)}" + + method = clock_sync.get("clock_sync_method") + if method == "integer_rollover": + uncertainty = clock_sync.get("rollover_uncertainty_ms") + if uncertainty is None: + return False, "missing_rollover_uncertainty" + try: + if float(uncertainty) > cls.CLOCK_SYNC_MAX_ROLLOVER_UNCERTAINTY_MS: + return False, f"rollover_uncertainty:{float(uncertainty):.1f}ms" + except (TypeError, ValueError): + return False, "invalid_rollover_uncertainty" + return True, "valid_integer_rollover" + + if method == "fractional_clock": + return True, "valid_fractional_clock" + + return False, f"unsupported_method:{method or 'unknown'}" + + @classmethod + def _clock_sync_summary_for_log(cls, clock_sync: object) -> Optional[dict]: + """Return a compact JSONL-safe summary of a clock-sync candidate.""" + if not isinstance(clock_sync, dict): + return None + valid, reason = cls._clock_sync_quality(clock_sync) + return { + "valid": valid, + "reason": reason, + "source": clock_sync.get("source"), + "samples": clock_sync.get("samples"), + "valid_samples": clock_sync.get("valid_samples"), + "clock_sync_method": clock_sync.get("clock_sync_method"), + "best_offset_s": clock_sync.get("best_offset_s"), + "raw_best_offset_s": clock_sync.get("raw_best_offset_s"), + "best_read_latency_ms": clock_sync.get("best_read_latency_ms"), + "offset_spread_ms": clock_sync.get("offset_spread_ms"), + "rollover_uncertainty_ms": clock_sync.get("rollover_uncertainty_ms"), + "age_s": ( + round(cls._clock_sync_age_s(clock_sync), 3) + if cls._clock_sync_age_s(clock_sync) is not None + else None + ), + } + + def _select_clock_sync_for_capture( + self, + radar: "OPS243Radar", + capture: IQCapture, + ) -> Optional[dict]: + """Choose and apply the best OPS clock sync for this capture.""" + previous_sync = getattr(radar, "last_clock_sync", None) + previous_valid, previous_reason = self._clock_sync_quality(previous_sync) + previous_age_s = ( + self._clock_sync_age_s(previous_sync) if isinstance(previous_sync, dict) else None + ) + + fresh_sync = None + fresh_error = None + if hasattr(radar, "read_clock_sync"): + try: + fresh_sync = radar.read_clock_sync( + samples=self.CLOCK_SYNC_SAMPLES, + store=False, + ) + if isinstance(fresh_sync, dict): + fresh_sync["source"] = "per_shot" + except Exception as exc: # pylint: disable=broad-except + fresh_error = str(exc) + logger.warning("[TRIGGER] Per-shot OPS clock sync failed: %s", exc, exc_info=True) + + fresh_valid, fresh_reason = self._clock_sync_quality(fresh_sync) + + selected_sync = None + selected_source = "first_byte" + selected_reason = "no_valid_clock_sync" + + if fresh_valid and isinstance(fresh_sync, dict): + selected_sync = fresh_sync + selected_source = "fresh" + selected_reason = fresh_reason + radar.last_clock_sync = fresh_sync + elif ( + previous_valid + and isinstance(previous_sync, dict) + and previous_age_s is not None + and previous_age_s <= self.CLOCK_SYNC_MAX_FALLBACK_AGE_S + ): + selected_sync = previous_sync + selected_source = "previous" + selected_reason = ( + f"fresh_rejected:{fresh_reason};previous_age:{previous_age_s:.1f}s" + ) + elif previous_valid and previous_age_s is not None: + selected_reason = ( + f"fresh_rejected:{fresh_reason};previous_too_old:{previous_age_s:.1f}s" + ) + elif previous_reason != "missing": + selected_reason = f"fresh_rejected:{fresh_reason};previous_rejected:{previous_reason}" + elif fresh_error: + selected_reason = f"fresh_error:{fresh_error}" + else: + selected_reason = f"fresh_rejected:{fresh_reason}" + + selected_offset_s = None + selected_age_s = None + if isinstance(selected_sync, dict): + selected_offset_s = selected_sync.get("best_offset_s") + selected_age_s = self._clock_sync_age_s(selected_sync) + try: + capture.apply_trigger_timestamp_from_clock_sync(float(selected_offset_s)) + except (TypeError, ValueError): + logger.warning( + "[TRIGGER] Ignoring invalid selected OPS clock-sync offset: %r", + selected_offset_s, + ) + selected_sync = None + selected_source = "first_byte" + selected_reason = "selected_offset_invalid" + selected_offset_s = None + + previous_offset_s = ( + previous_sync.get("best_offset_s") if isinstance(previous_sync, dict) else None + ) + fresh_offset_s = fresh_sync.get("best_offset_s") if isinstance(fresh_sync, dict) else None + + selection_log = { + "selection": selected_source, + "selection_reason": selected_reason, + "selected_offset_s": selected_offset_s, + "selected_age_s": round(selected_age_s, 3) if selected_age_s is not None else None, + "fresh": self._clock_sync_summary_for_log(fresh_sync), + "previous": self._clock_sync_summary_for_log(previous_sync), + "fresh_error": fresh_error, + "fresh_delta_from_previous_ms": ( + round((fresh_offset_s - previous_offset_s) * 1000.0, 3) + if fresh_offset_s is not None and previous_offset_s is not None + else None + ), + } + + if selected_sync is not None: + logger.info( + "[TRIGGER] OPS clock sync selected: %s offset=%.6fs age=%sms " + "(fresh=%s, previous=%s, delta=%sms)", + selected_source, + selected_offset_s, + "n/a" if selected_age_s is None else f"{selected_age_s:.1f}", + fresh_reason, + previous_reason, + "n/a" + if selection_log["fresh_delta_from_previous_ms"] is None + else f"{selection_log['fresh_delta_from_previous_ms']:.1f}", + ) + else: + logger.info( + "[TRIGGER] OPS clock sync fallback to first-byte timing: %s", + selected_reason, + ) + + return selection_log + def wait_for_trigger( self, radar: "OPS243Radar", @@ -849,44 +1055,6 @@ def wait_for_trigger( if first_byte_timestamp is not None and capture.first_byte_timestamp is None: capture.first_byte_timestamp = float(first_byte_timestamp) - clock_sync = getattr(radar, "last_clock_sync", None) - clock_sync_usable = ( - bool(clock_sync.get("usable_for_trigger_timestamps")) - if isinstance(clock_sync, dict) - else False - ) - clock_offset_s = ( - clock_sync.get("best_offset_s") - if isinstance(clock_sync, dict) and clock_sync_usable - else None - ) - if clock_offset_s is not None: - try: - capture.apply_trigger_timestamp_from_clock_sync(float(clock_offset_s)) - except (TypeError, ValueError): - logger.warning( - "[TRIGGER] Ignoring invalid OPS clock-sync offset: %r", - clock_offset_s, - ) - elif isinstance(clock_sync, dict) and clock_sync.get("best_offset_s") is not None: - logger.info( - "[TRIGGER] Ignoring unusable OPS clock sync for trigger timestamp (method=%s)", - clock_sync.get("clock_sync_method", "unknown"), - ) - - if capture.first_byte_timestamp is not None and capture.trigger_timestamp is None: - capture.apply_trigger_timestamp_from_first_byte() - - if capture.trigger_timestamp is not None and capture.first_byte_timestamp is not None: - logger.info( - "[TRIGGER] Sound trigger wall time %.3f " - "(source=%s, first byte %.3f, post-trigger %.1fms)", - capture.trigger_timestamp, - capture.trigger_timestamp_source or "unknown", - capture.first_byte_timestamp, - capture.post_trigger_duration_ms, - ) - # Quick validation: does the capture contain any real swing data? # At a driving range, a nearby player's impact sound can trip the # trigger even though nothing was moving in front of our radar. @@ -909,6 +1077,21 @@ def wait_for_trigger( ) return None + self._select_clock_sync_for_capture(radar, capture) + + if capture.first_byte_timestamp is not None and capture.trigger_timestamp is None: + capture.apply_trigger_timestamp_from_first_byte() + + if capture.trigger_timestamp is not None and capture.first_byte_timestamp is not None: + logger.info( + "[TRIGGER] Sound trigger wall time %.3f " + "(source=%s, first byte %.3f, post-trigger %.1fms)", + capture.trigger_timestamp, + capture.trigger_timestamp_source or "unknown", + capture.first_byte_timestamp, + capture.post_trigger_duration_ms, + ) + logger.info( "[TRIGGER] Sound trigger accepted — peak %.1f mph, %d outbound readings", summary["valid_peak_outbound_mph"], diff --git a/tests/test_ops243.py b/tests/test_ops243.py index f7bec8895..934215f5f 100644 --- a/tests/test_ops243.py +++ b/tests/test_ops243.py @@ -286,6 +286,16 @@ def test_offset_spread_reported_for_multiple_reads(self): assert summary["offset_spread_ms"] is not None assert summary["offset_spread_ms"] >= 0.0 + def test_store_false_does_not_replace_last_clock_sync(self): + existing = {"best_offset_s": 42.0} + radar = self._radar(_FakeClockSerial(clock_value="137.429")) + radar.last_clock_sync = existing + + summary = radar.read_clock_sync(samples=2, per_read_timeout=0.05, store=False) + + assert summary["best_offset_s"] is not None + assert radar.last_clock_sync is existing + def test_no_response_is_handled(self): radar = self._radar(_FakeClockSerial(respond=False)) summary = radar.read_clock_sync(samples=2, per_read_timeout=0.01) diff --git a/tests/test_rolling_buffer.py b/tests/test_rolling_buffer.py index 1c438c994..2bb72f26e 100644 --- a/tests/test_rolling_buffer.py +++ b/tests/test_rolling_buffer.py @@ -1,6 +1,7 @@ """Tests for rolling_buffer module.""" import math +import time from datetime import datetime from unittest.mock import MagicMock @@ -592,9 +593,96 @@ def test_sound_trigger_prefers_ops_clock_sync_for_trigger_timestamp(self): radar = MagicMock() radar.wait_for_hardware_trigger.return_value = '{"sample_time": 0.0}' radar.last_hardware_trigger_first_byte_timestamp = 12345.678 - radar.last_clock_sync = { + radar.last_clock_sync = None + radar.read_clock_sync.return_value = { + "source": "per_shot", + "samples": 3, + "valid_samples": 3, + "best_offset_s": 12000.0, + "clock_sync_method": "integer_rollover", + "usable_for_trigger_timestamps": True, + "rollover_uncertainty_ms": 10.0, + "reads": [ + { + "host_after": time.time(), + "host_mid": time.time(), + "radar_clock_s": 100.0, + "read_latency_ms": 1.0, + } + ], + } + + capture = IQCapture( + sample_time=100.000, + trigger_time=100.068, + i_samples=[2048] * 4096, + q_samples=[2048] * 4096, + ) + processor = MagicMock() + processor.parse_capture.return_value = capture + processor.process_standard.return_value = SpeedTimeline( + readings=[ + SpeedReading( + speed_mph=100.0, + magnitude=1000.0, + timestamp_ms=68.0, + direction="outbound", + ) + ], + sample_rate_hz=937.5, + ) + + trigger = SoundTrigger(pre_trigger_segments=12) + result = trigger.wait_for_trigger(radar, processor, timeout=1.0) + + assert result is capture + assert result.trigger_timestamp == pytest.approx(12100.068) + assert result.trigger_timestamp_source == "ops_clock_sync" + assert radar.last_clock_sync is radar.read_clock_sync.return_value + radar.read_clock_sync.assert_called_once_with(samples=36, store=False) + + def test_sound_trigger_uses_recent_previous_sync_when_fresh_sync_is_bad(self): + """A bad per-shot C? read should not discard a recent valid sync.""" + from openflight.rolling_buffer.trigger import SoundTrigger + + now = time.time() + radar = MagicMock() + radar.wait_for_hardware_trigger.return_value = '{"sample_time": 0.0}' + radar.last_hardware_trigger_first_byte_timestamp = 12345.678 + previous_sync = { + "source": "startup", + "samples": 3, + "valid_samples": 3, "best_offset_s": 12000.0, + "clock_sync_method": "integer_rollover", "usable_for_trigger_timestamps": True, + "rollover_uncertainty_ms": 10.0, + "reads": [ + { + "host_after": now, + "host_mid": now, + "radar_clock_s": 100.0, + "read_latency_ms": 1.0, + } + ], + } + radar.last_clock_sync = previous_sync + radar.read_clock_sync.return_value = { + "source": "per_shot", + "samples": 4, + "valid_samples": 2, + "best_offset_s": 12100.0, + "clock_sync_method": "integer_rollover", + "usable_for_trigger_timestamps": True, + "rollover_uncertainty_ms": 900.0, + "reads": [ + { + "host_after": now, + "host_mid": now, + "radar_clock_s": None, + "read_latency_ms": 200.0, + } + ], } capture = IQCapture( @@ -623,6 +711,7 @@ def test_sound_trigger_prefers_ops_clock_sync_for_trigger_timestamp(self): assert result is capture assert result.trigger_timestamp == pytest.approx(12100.068) assert result.trigger_timestamp_source == "ops_clock_sync" + assert radar.last_clock_sync is previous_sync def test_sound_trigger_ignores_unusable_ops_clock_sync(self): """Whole-second-only clock sync should not override first-byte timing.""" @@ -636,6 +725,14 @@ def test_sound_trigger_ignores_unusable_ops_clock_sync(self): "usable_for_trigger_timestamps": False, "clock_sync_method": "integer_unusable_no_rollover", } + radar.read_clock_sync.return_value = { + "samples": 1, + "valid_samples": 0, + "best_offset_s": None, + "usable_for_trigger_timestamps": False, + "clock_sync_method": "no_valid_reads", + "reads": [], + } capture = IQCapture( sample_time=100.000, From 7fe50bbf5e1312c58126201053415b26fd107c4c Mon Sep 17 00:00:00 2001 From: Aadne S Djuve Date: Mon, 8 Jun 2026 14:46:51 +0200 Subject: [PATCH 10/29] fix: install kld7 driver by default so clean installs don't fail The K-LD7 angle radar failed on clean installs with "kld7 package not installed" because kld7 was an optional extra that no install path actually pulled in: setup.sh installs .[ui,analysis], `uv sync` and CI don't install extras, and only start-kiosk.sh's error hint mentioned .[ui,kld7]. Move kld7>=0.2.1 into base dependencies. It's a tiny pure-Python wheel whose only requirement (pyserial) is already a base dep, and the --kld7 runtime flag still gates actual hardware use, so it can't be forgotten by any install path again. The defensive find_spec("kld7") guards remain. - Remove the now-redundant kld7 optional extra - Update start-kiosk.sh hint to .[ui] - Update stale `pip install kld7` / `uv add kld7` messages in the kld7 driver and helper scripts to point at the project install - Add a regression test asserting kld7 is a base dependency --- pyproject.toml | 3 +-- scripts/analysis/calibrate_kld7_reflector.py | 3 ++- scripts/analysis/capture_kld7_radc.py | 2 +- scripts/hardware-test/test_kld7.py | 4 ++-- scripts/start-kiosk.sh | 2 +- src/openflight/kld7/tracker.py | 5 ++++- tests/test_project_metadata.py | 20 ++++++++++++++++++++ 7 files changed, 31 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 97630073d..73f8b9107 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "flask>=3.1.3", "flask-cors>=6.0.2", "flask-socketio>=5.6.1", + "kld7>=0.2.1", ] [project.optional-dependencies] @@ -52,8 +53,6 @@ camera = [ # "picamera2>=0.3.0; sys_platform == 'linux'", # "inference-sdk>=0.9.0", # Roboflow inference API ] -# K-LD7 radar support -kld7 = ["kld7>=0.2.1"] # Analysis tools for I/Q capture data analysis = [ "numpy>=1.20.0", diff --git a/scripts/analysis/calibrate_kld7_reflector.py b/scripts/analysis/calibrate_kld7_reflector.py index 79461cee5..0d41806e5 100644 --- a/scripts/analysis/calibrate_kld7_reflector.py +++ b/scripts/analysis/calibrate_kld7_reflector.py @@ -234,7 +234,8 @@ def main() -> int: try: from kld7 import FrameCode # type: ignore except ImportError: - print("[calibrate] kld7 package not installed (uv add kld7).", + print("[calibrate] kld7 package not installed. Reinstall the project: " + "uv pip install -e '.[ui]'", file=sys.stderr) return 2 diff --git a/scripts/analysis/capture_kld7_radc.py b/scripts/analysis/capture_kld7_radc.py index 56fe6c9d3..61459bc42 100755 --- a/scripts/analysis/capture_kld7_radc.py +++ b/scripts/analysis/capture_kld7_radc.py @@ -46,7 +46,7 @@ try: from kld7 import KLD7, FrameCode, KLD7Exception except ImportError: - print("kld7 package not installed. Run: pip install kld7") + print("kld7 package not installed. Reinstall the project: uv pip install -e '.[ui]'") sys.exit(1) # Add src to path for OPS243 import. This script lives in scripts/analysis. diff --git a/scripts/hardware-test/test_kld7.py b/scripts/hardware-test/test_kld7.py index 34970e9c7..aaf92f0a0 100755 --- a/scripts/hardware-test/test_kld7.py +++ b/scripts/hardware-test/test_kld7.py @@ -23,7 +23,7 @@ (tracked target) since the tracking filter may not lock on in time. Prerequisites: - pip install kld7 pyserial + uv pip install -e '.[ui]' # kld7 ships as a base dependency Usage: # Basic capture (auto-detect port, Ctrl+C to stop) @@ -56,7 +56,7 @@ try: from kld7 import KLD7, FrameCode, KLD7Exception except ImportError: - print("Error: kld7 package not installed. Run: pip install kld7") + print("Error: kld7 package not installed. Reinstall the project: uv pip install -e '.[ui]'") sys.exit(1) from serial.tools.list_ports import comports diff --git a/scripts/start-kiosk.sh b/scripts/start-kiosk.sh index 57f2a1025..e37fc336a 100755 --- a/scripts/start-kiosk.sh +++ b/scripts/start-kiosk.sh @@ -416,7 +416,7 @@ fi # Check if venv exists if [ ! -d ".venv" ]; then - error "Virtual environment not found. Run: uv venv && uv pip install -e '.[ui,kld7]'" + error "Virtual environment not found. Run: uv venv && uv pip install -e '.[ui]'" exit 1 fi diff --git a/src/openflight/kld7/tracker.py b/src/openflight/kld7/tracker.py index bec8a72dd..2e43554ee 100644 --- a/src/openflight/kld7/tracker.py +++ b/src/openflight/kld7/tracker.py @@ -196,7 +196,10 @@ def _init_ring_buffer(self): def connect(self) -> bool: """Connect to K-LD7 and configure for golf.""" if find_spec("kld7") is None: - logger.error("[KLD7] kld7 package not installed. Run: pip install kld7") + logger.error( + "[KLD7] kld7 package not installed. Reinstall the project: " + "uv pip install -e '.[ui]'" + ) return False port = self.port or _find_port() diff --git a/tests/test_project_metadata.py b/tests/test_project_metadata.py index ab0e4ccae..c5f36110c 100644 --- a/tests/test_project_metadata.py +++ b/tests/test_project_metadata.py @@ -1,5 +1,6 @@ """Tests for packaging metadata that affects setup/install behavior.""" +import re from pathlib import Path try: @@ -12,6 +13,25 @@ def _pyproject() -> dict: return tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) +def _requirement_name(dependency: str) -> str: + """Extract the distribution name from a PEP 508 dependency string.""" + return re.split(r"[<>=!~ \[;]", dependency, maxsplit=1)[0].strip() + + +def test_kld7_is_installed_by_default(): + """K-LD7 driver must be a base dependency so every install path includes it. + + The package is a tiny pure-Python wheel whose only requirement (pyserial) is + already a base dependency, and the --kld7 runtime flag gates actual hardware + use. Shipping it by default avoids the recurring "kld7 package not installed" + failure on clean installs, since setup.sh, `uv sync`, and CI do not pull + optional extras. + """ + dependencies = _pyproject()["project"]["dependencies"] + + assert any(_requirement_name(dep) == "kld7" for dep in dependencies) + + def test_camera_dependencies_are_not_installed_by_default(): """Camera tracking packages should not be part of the base install.""" dependencies = _pyproject()["project"]["dependencies"] From b7a851c00bdafb10464f81bbf37cbb84421dc6fb Mon Sep 17 00:00:00 2001 From: Pablo Arango Date: Tue, 9 Jun 2026 11:27:12 -0400 Subject: [PATCH 11/29] fix(kiosk): launch server via uv run so a moved venv self-heals The script sourced .venv/bin/activate, which bakes in an absolute VIRTUAL_ENV path at creation time. Moving the project directory left activate pointing at a stale path, so openflight-server resolved to a non-existent bin and startup failed with "command not found". Resolve the environment dynamically instead: require uv, run uv sync to reconcile against uv.lock, and launch with `uv run`. A moved, stale, or missing venv now repairs itself rather than failing cryptically. --- scripts/start-kiosk.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/start-kiosk.sh b/scripts/start-kiosk.sh index e37fc336a..e5a3ff65c 100755 --- a/scripts/start-kiosk.sh +++ b/scripts/start-kiosk.sh @@ -414,14 +414,13 @@ if [ "$DRY_RUN" = true ]; then exit 0 fi -# Check if venv exists -if [ ! -d ".venv" ]; then - error "Virtual environment not found. Run: uv venv && uv pip install -e '.[ui]'" +# Ensure the environment is in sync (uv recreates/repairs .venv as needed, +# so a moved project dir self-heals instead of failing with "command not found") +if ! command -v uv >/dev/null 2>&1; then + error "uv not found. Install it: https://docs.astral.sh/uv/" exit 1 fi - -# Activate venv -source .venv/bin/activate +uv sync --quiet configure_kld7_latency @@ -483,7 +482,7 @@ else log "Camera enabled (Hough + ByteTrack)" fi -$SERVER_CMD & +uv run $SERVER_CMD & SERVER_PID=$! # Wait for server to be ready From be045802ad85a8ab0a39c5d37fc4b553a564930c Mon Sep 17 00:00:00 2001 From: Pablo Arango Date: Tue, 9 Jun 2026 11:23:42 -0400 Subject: [PATCH 12/29] docs(ui): replace Vite template README with OpenFlight UI docs Document quick start, scripts, socket connection model, project layout, and troubleshooting in place of the default Vite boilerplate. --- ui/README.md | 142 +++++++++++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 61 deletions(-) diff --git a/ui/README.md b/ui/README.md index c987b941e..f6e89f241 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,73 +1,93 @@ -# React + TypeScript + Vite +# OpenFlight UI -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +The OpenFlight dashboard: a React + TypeScript + Vite app that connects to the +backend over `socket.io` and renders live shot data, session stats, camera ball +detection, and a screen-mounted display mode. -Currently, two official plugins are available: +This README covers frontend development. For the hardware, the radar pipeline, +and how the whole system fits together, see the [root README](../README.md). -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +## Quick start -## React Compiler +You need Node 20+ and a running OpenFlight backend. To start a backend without +hardware, run `scripts/start-kiosk.sh --mock` from the repo root (see the +[root README](../README.md#getting-started)). -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +```bash +npm install +npm run dev +``` + +The dev server runs on port `5173`. When served there, the UI assumes the +backend is at `http://localhost:8080`. Point it elsewhere with +`VITE_SOCKET_URL`: -## Expanding the ESLint configuration +```bash +VITE_SOCKET_URL="http://localhost:8081" npm run dev +``` -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +## Scripts -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... +| Script | Description | +| ---------------------- | -------------------------------------------- | +| `npm run dev` | Dev server with hot reload | +| `npm run build` | Type-check and build the production bundle | +| `npm run preview` | Serve the production build locally | +| `npm run lint` | ESLint | +| `npm run test` | Vitest unit tests | +| `npm run format` | Format `src/` with Prettier | +| `npm run format:check` | Check formatting without writing | - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, +## How the UI connects - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]); -``` +The app is entirely client-side. Everything flows through one socket connection. + +- **`utils/serverOrigin.ts`** resolves the backend origin: `VITE_SOCKET_URL` if + set, otherwise `http://localhost:8080` when running on the Vite dev port + (`5173`), otherwise the page's own origin (the production case, where the + backend serves the built UI). +- **`hooks/useSocket.ts`** owns the connection. It receives events like `shot`, + `session_state`, `camera_status`, `ball_detection`, and `trigger_status`, and + sends commands like `set_club`, `clear_session`, `simulate_shot`, and + `toggle_camera`. It's the source of truth for the event contract — read it + before assuming what the backend emits. +- **State** lives in `state/` (shot history and unit preferences) via React + context providers. +- **Shutdown** posts to `/api/shutdown` to stop the connected backend. + +**Display mode** lives at `/display`: a compact, fullscreen-friendly dashboard +for mounted screens and TVs. The [root README](../README.md#tv-display-mode) +covers casting it. + +**Launch Daddy** is a hidden mode toggled by a tap area in the header. When on, +new shots can fire an animated overlay. -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x'; -import reactDom from 'eslint-plugin-react-dom'; - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]); +## Project layout + +A few files do most of the work. This is illustrative, not exhaustive — +components carry co-located `.css` and `.test.tsx` files. + +```text +src/ + App.tsx # navigation, view selection, display routing + main.tsx # entry point + hooks/useSocket.ts # socket connection, events, backend commands + utils/serverOrigin.ts # backend origin resolution + state/ # shot history + unit preference context + components/ # CameraFeed, ShotDisplay, StatsView, DebugPanel, … + LaunchDaddy/ # the hidden overlay mode ``` + +## Troubleshooting + +**Socket won't connect.** Confirm the backend is running and reachable from the +browser. If it isn't on the default port, set `VITE_SOCKET_URL`. Connection logs +come from `hooks/useSocket.ts`. + +**Build fails.** `npm run build` surfaces TypeScript and bundling errors; `npm +run lint` catches the rest. + +--- + +Contributing guidelines (setup, code quality, PRs) live in +[CONTRIBUTING.md](../CONTRIBUTING.md). From ec4964ef50a96f7b77a09fb56dff7cd4111733e5 Mon Sep 17 00:00:00 2001 From: JedS Date: Tue, 9 Jun 2026 18:34:54 +0200 Subject: [PATCH 13/29] Default ballistics off; rename flag to --ballistics opt-in Flip ballistics_enabled default to False so all shots use the legacy table estimator unless the user opts in with --ballistics (CLI) or --ballistics (start-kiosk.sh). Keeps the simulator available for A/B work while Cd/Cl coefficients are still being validated against TM. Co-Authored-By: Claude Opus 4.7 --- scripts/start-kiosk.sh | 14 ++++++++------ src/openflight/server.py | 21 +++++++++++---------- tests/test_server.py | 3 ++- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/scripts/start-kiosk.sh b/scripts/start-kiosk.sh index 4f77be4f7..dbb0fe167 100755 --- a/scripts/start-kiosk.sh +++ b/scripts/start-kiosk.sh @@ -42,7 +42,7 @@ EXPERIMENTAL_KLD7_VERTICAL_IMPACT_ENERGY="" EXPERIMENTAL_KLD7_HORIZONTAL_IMPACT_ENERGY="" EXPERIMENTAL_KLD7_HORIZONTAL_RETRY_IMPACT_ENERGY="" EXPERIMENTAL_KLD7_HORIZONTAL_ANGLE_LIMIT="" -NO_BALLISTICS=false +BALLISTICS=false # Buffer split presets (pre/post trigger segments out of 32 total) # At 20ksps: each segment = 6.4ms, total buffer = 204.8ms @@ -199,8 +199,8 @@ while [[ $# -gt 0 ]]; do EXPERIMENTAL_KLD7_HORIZONTAL_ANGLE_LIMIT="$2" shift 2 ;; - --no-ballistics) - NO_BALLISTICS=true + --ballistics) + BALLISTICS=true shift ;; --port|-p) @@ -326,8 +326,8 @@ if [ "$NO_CAMERA" = true ]; then SERVER_CMD="$SERVER_CMD --no-camera" fi -if [ "$NO_BALLISTICS" = true ]; then - SERVER_CMD="$SERVER_CMD --no-ballistics" +if [ "$BALLISTICS" = true ]; then + SERVER_CMD="$SERVER_CMD --ballistics" fi if [ -n "$TRIGGER" ]; then @@ -492,7 +492,9 @@ else log "Camera enabled (Hough + ByteTrack)" fi -if [ "$NO_BALLISTICS" = true ]; then +if [ "$BALLISTICS" = true ]; then + log "Ballistic carry model enabled (simulator + drag/Magnus)" +else log "Ballistic carry model disabled (using legacy table)" fi diff --git a/src/openflight/server.py b/src/openflight/server.py index 11516d017..2a0d7947e 100644 --- a/src/openflight/server.py +++ b/src/openflight/server.py @@ -69,11 +69,11 @@ experimental_kld7_radc_tuning: bool = False experimental_kld7_raw_radc_logging: bool = False -# Ballistic model toggle. When True (default), shot carry comes from the -# physics simulator whenever a vertical launch angle is available. When -# False, all carry computations go through the legacy table estimator — -# useful for A/B comparison or as a fallback while the model is tuned. -ballistics_enabled: bool = True +# Ballistic model toggle. When True, shot carry comes from the physics +# simulator whenever a vertical launch angle is available. When False +# (default), all carry computations go through the legacy table estimator. +# The simulator is opt-in until coefficients are validated against TM. +ballistics_enabled: bool = False _DEFAULT_KLD7_RADC_TUNING = { "radc_speed_tolerance_mph": 10.0, @@ -2323,12 +2323,13 @@ def main(): ) parser.add_argument("--no-logging", action="store_true", help="Disable session logging") parser.add_argument( - "--no-ballistics", + "--ballistics", action="store_true", help=( - "Disable the physics-based carry simulator. All shots fall back to " - "the legacy table-based carry estimator. Default: ballistics enabled " - "whenever a vertical launch angle is available." + "Enable the physics-based carry simulator (drag + Magnus, RK4). " + "When set, shots with a vertical launch angle use the simulator " + "for carry; otherwise they fall back to the legacy table estimator. " + "Default: disabled (all shots use the table)." ), ) parser.add_argument( @@ -2490,7 +2491,7 @@ def main(): global ballistics_enabled experimental_kld7_raw_radc_logging = args.experimental_kld7_raw_radc_logging experimental_kld7_radc_tuning = args.experimental_kld7_radc_tuning - ballistics_enabled = not args.no_ballistics + ballistics_enabled = args.ballistics kld7_radc_tuning_kwargs = _kld7_radc_tuning_kwargs(args) active_kld7_radc_tuning = dict(kld7_radc_tuning_kwargs) diff --git a/tests/test_server.py b/tests/test_server.py index 5c85a11a0..0890093e9 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1996,6 +1996,7 @@ def _patch_environment(self, monkeypatch): def test_carry_uses_ballistic_simulator_when_launch_angle_present(self, monkeypatch): """A shot with a vertical launch angle should get carry from the physics sim.""" self._patch_environment(monkeypatch) + monkeypatch.setattr(server_module, "ballistics_enabled", True) captured = {} @@ -2057,7 +2058,7 @@ def fail_simulate(*args, **kwargs): def test_carry_skips_ballistic_when_ballistics_disabled(self, monkeypatch): """When ballistics_enabled is False, the simulator must not run even if a valid launch angle is present — carry falls through to the - table estimator. Mirrors what `--no-ballistics` does on the CLI.""" + table estimator. This is the default; `--ballistics` opts in.""" self._patch_environment(monkeypatch) monkeypatch.setattr(server_module, "ballistics_enabled", False) From d8f1bafded4b20532ccdf99e077c538feac51d79 Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Tue, 9 Jun 2026 11:20:38 -0700 Subject: [PATCH 14/29] [cr] add script to test direct interface with kld7 --- .../42128-1780857831/state/server-stopped | 1 + docs/kld7-troubleshooting.md | 21 + .../2026-06-09-kld7-prf-probing-design.md | 219 +++++++ scripts/hardware-test/probe_kld7_timing.py | 543 ++++++++++++++++++ tests/test_probe_kld7_timing.py | 189 ++++++ 5 files changed, 973 insertions(+) create mode 100644 .superpowers/brainstorm/42128-1780857831/state/server-stopped create mode 100644 docs/superpowers/specs/2026-06-09-kld7-prf-probing-design.md create mode 100644 scripts/hardware-test/probe_kld7_timing.py create mode 100644 tests/test_probe_kld7_timing.py diff --git a/.superpowers/brainstorm/42128-1780857831/state/server-stopped b/.superpowers/brainstorm/42128-1780857831/state/server-stopped new file mode 100644 index 000000000..a653e3700 --- /dev/null +++ b/.superpowers/brainstorm/42128-1780857831/state/server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1780860058366} diff --git a/docs/kld7-troubleshooting.md b/docs/kld7-troubleshooting.md index d85c7e9df..fd2160f7c 100644 --- a/docs/kld7-troubleshooting.md +++ b/docs/kld7-troubleshooting.md @@ -174,6 +174,27 @@ If `--kld7` or `--kld7-horizontal` is passed but the radar fails to connect afte ## RADC Streaming Issues +### Measuring real K-LD7 RADC cadence + +Use the guarded timing probe when launch-angle extraction is missing frames or +when one K-LD7 orientation appears slower than the other: + +```bash +uv run python scripts/hardware-test/probe_kld7_timing.py \ + --port /dev/kld7_vertical \ + --duration 10 \ + --frame-mask RADC,DONE \ + --output /tmp/kld7_vertical_timing.jsonl +``` + +At the production `RSPI=3` setting, expect roughly 34 RADC frames per second +with low `done_frame_gaps`. If cadence is much lower or gaps are high, +investigate USB scheduling, serial read duration, or requested packet volume +before changing launch-angle selection logic. + +Undocumented command probing is available only through `--unsafe-probe` and +requires `--output`. Do not use it in production sessions. + ### No RADC frames in buffer **Symptom:** K-LD7 connects but shots show `angle_source: estimated` instead of `radar`. diff --git a/docs/superpowers/specs/2026-06-09-kld7-prf-probing-design.md b/docs/superpowers/specs/2026-06-09-kld7-prf-probing-design.md new file mode 100644 index 000000000..46992587b --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-kld7-prf-probing-design.md @@ -0,0 +1,219 @@ +# K-LD7 Timing and PRF Probe + +**Date:** 2026-06-09 +**Status:** Approved +**Scope:** Build a guarded hardware exploration path for diagnosing unreliable K-LD7 launch-angle frames, measuring actual RADC frame cadence, and optionally probing explicitly listed undocumented serial commands. + +## Problem + +OpenFlight is not always getting enough reliable K-LD7 frames around impact to extract launch angle. The current production path streams RADC frames at 3 Mbaud and anchors angle extraction to the OPS243 impact timestamp, but a golf ball can cross the useful detection zone in only a few tens of milliseconds. More frames near impact would improve the odds of a clean launch-angle estimate. + +The hypothesis is that a higher PRF and slightly lower pulse width could improve frame capture. The K-LD7 public documentation, however, does not expose PRF or pulse width directly. The module is documented as an FSK Doppler radar. The exposed controls are maximum speed (`RSPI`), maximum range (`RRAI`), base frequency (`RBFR`), threshold/tracking settings, and data-frame request commands. That means the first question is whether the bottleneck is true RF acquisition cadence, serial readout/request strategy, or undocumented firmware capability. + +## Approach + +Create a standalone hardware-test probe that talks to the K-LD7 binary serial protocol directly, but defaults to non-destructive measurement. + +The exploration has two phases: + +1. **Safe measurement:** use documented commands only. Measure actual frame cadence, dropped `DONE` frame numbers, RADC payload read time, and serial errors under current and swept documented settings. +2. **Unsafe probing:** only with an explicit opt-in flag, send explicitly listed candidate undocumented command packets and record response codes plus before/after parameter diffs. + +This is not a broad brute-force scanner. The unsafe phase must only send command names and payloads deliberately supplied on the CLI or in a small allowlisted fixture. The goal is to discover whether a plausible engineering command exists, not to fuzz the firmware. + +## Evidence From Current Code And Docs + +- `src/openflight/kld7/tracker.py` configures `RRAI`, `RSPI`, `RBFR`, `DEDI`, `THOF`, `TRFT`, `MIAN`, `MAAN`, `MIRA`, `MARA`, `MISP`, `MASP`, and `VISU`. +- `src/openflight/kld7/serial_io.py` connects through the existing `kld7.KLD7` package at 3 Mbaud and patches packet reads for robust RADC streaming. +- The installed `kld7` package is a thin wrapper around binary packets: 4-byte command, 4-byte length, optional payload, then `RESP` plus data packets. +- `docs/K-LD7_Datasheet.pdf` lists documented commands: `INIT`, `GNFD`, `GRPS`, `SRPS`, `RFSE`, `GBYE`, and individual parameter setters including `RBFR`, `RSPI`, `RRAI`, `THOF`, `TRFT`, `VISU`, detection bounds, output routing, hold time, and micro-detection settings. +- The datasheet notes RADC is 3072 bytes and recommends the highest baud rate. It also states real-time readout is not possible if requested data readout time exceeds the typical frame duration. + +## Architecture + +**File:** `scripts/hardware-test/probe_kld7_timing.py` + +**Entry point:** `uv run python scripts/hardware-test/probe_kld7_timing.py` + +The script should not depend on the production `KLD7Tracker`. It should use pyserial directly or a small local protocol helper so we can observe raw packet timings without tracker-side buffering or selection logic. + +### Components + +**`KLD7Protocol`** + +Low-level serial protocol wrapper: + +- Opens the port at 115200 even parity. +- Sends `INIT` to negotiate the requested baud rate, usually 3 Mbaud. +- If `INIT` fails, optionally sends `GBYE` at 3 Mbaud and retries, matching the production recovery path for radars left streaming by a crashed process. +- Sends binary packets with `struct.pack("<4sI", cmd, length) + payload`. +- Reads exact packet headers and payloads with the same short-read tolerance used by `src/openflight/kld7/serial_io.py`. +- Exposes `send_command(cmd, payload=b"")`, `read_packet()`, `get_response()`, `read_params()`, `set_param()`, `request_frame(frame_mask)`, and `close()`. + +**`TimingRecorder`** + +Records one row per packet/frame: + +- Host monotonic timestamp at command send. +- First-byte arrival timestamp. +- Header complete timestamp. +- Payload complete timestamp. +- Packet code. +- Payload length. +- Response code, if applicable. +- `DONE` frame number, if requested. +- Short-read or timeout errors. + +**`ProbeResult`** + +Aggregates each run: + +- Effective RADC Hz. +- Effective `DONE` Hz. +- Mean/p50/p95 RADC payload read duration. +- Missing `DONE` frame numbers. +- Number of timeouts, short reads, invalid headers, and sensor-busy responses. +- Parameter snapshot before and after the run. + +## CLI + +```bash +uv run python scripts/hardware-test/probe_kld7_timing.py \ + --port /dev/kld7_vertical \ + --duration 10 \ + --frame-mask RADC,DONE +``` + +### Safe Flags + +- `--port PATH`: serial port or udev alias. Required unless auto-detection finds exactly one K-LD7. +- `--baud 3000000`: target post-`INIT` baud rate. +- `--duration SECONDS`: measurement duration for each configuration. +- `--frame-mask LIST`: comma-separated frame types. Default `RADC,DONE`. +- `--rspi-sweep`: run all documented `RSPI` values. +- `--rrai VALUE`: documented range code or meters. +- `--rbfr VALUE`: documented base frequency code. +- `--output PATH`: write JSONL packet log plus summary JSON. +- `--restore-params`: restore the initial `GRPS` parameter snapshot before exit. Default on. Restore should use individual documented parameter setters, not an opaque `SRPS` write that includes the software-version bytes. + +### Unsafe Flags + +- `--unsafe-probe`: enables undocumented command probing. +- `--probe-command CMD[:HEX_PAYLOAD]`: one explicitly listed 4-byte uppercase command packet to send. Repeatable. Hex payload must have an even number of characters. +- `--allow-factory-reset`: permits `RFSE`. Default refuses `RFSE` even in unsafe mode. +- `--no-restore-params`: leaves changed parameters in place, but only after printing a warning and requiring `--unsafe-probe`. + +Unsafe mode is unavailable unless `--output` is set, so every probe leaves an audit trail. + +## Safe Measurement Flow + +1. Resolve the serial port. +2. Connect at 115200, send `INIT`, switch to target baud. +3. Send `GRPS` and store the initial parameter snapshot. +4. Apply documented settings for the current run. +5. Re-read `GRPS` and record the active settings. +6. Loop until duration expires: + - Send `GNFD` with the requested frame mask. + - Read `RESP`. + - Read expected data packets, including `DONE` when requested. + - Record exact timing and packet sizes. +7. Summarize effective frame cadence and gaps. +8. Restore the initial parameter snapshot unless disabled. +9. Send `GBYE` and close the port. + +## Unsafe Probe Flow + +1. Run the safe connection and initial `GRPS` snapshot. +2. For each explicit `--probe-command`: + - Refuse known destructive commands unless separately allowed. + - Validate the command is exactly four ASCII uppercase bytes. + - Send the command with its exact payload. + - Read and record `RESP`. + - Drain and record any follow-up packets. + - Re-read `GRPS`. + - Emit a before/after parameter diff. +3. If the command returns `OK` or changes parameters, run a short `RADC,DONE` cadence sample. +4. Restore the original parameter snapshot before exit by default. + +## Safety Rules + +- No random command generation. +- No wildcard payload sweeps. +- No persistent production integration until a command is understood and reproducible. +- Refuse `RFSE` by default. +- Always snapshot parameters before writes. +- Restore parameters by default. +- Always send `GBYE` on exit. +- Treat any command that appears to alter RF timing as lab-only until RFbeam confirms regulatory implications. The K-LD7 has modular RF approvals; changing waveform timing or duty cycle outside documented settings could invalidate those assumptions. + +## Data Products + +**JSONL packet log:** one row per command/packet with timing and raw metadata. + +**Summary JSON:** one object per run: + +```json +{ + "port": "/dev/kld7_vertical", + "baud": 3000000, + "frame_mask": ["RADC", "DONE"], + "params_before": {"RSPI": 3, "RRAI": 0, "RBFR": 0}, + "params_active": {"RSPI": 3, "RRAI": 0, "RBFR": 0}, + "duration_s": 10.0, + "radc_frames": 340, + "done_frames": 340, + "effective_radc_hz": 34.0, + "done_frame_gaps": 0, + "read_duration_ms_p95": 12.4, + "errors": {} +} +``` + +## Interpretation + +The first decision point is whether the measured RADC stream is close to the documented `RSPI=3` cadence. + +- If RADC is near 34 Hz with low gaps, the launch-angle issue is probably frame selection, timing alignment, geometry, SNR, or target ambiguity rather than serial acquisition. +- If RADC is well below 34 Hz or has large `DONE` gaps, the next target is readout/request strategy, USB scheduling, or reducing requested packet volume. +- If a documented `RSPI` sweep changes effective cadence as expected, `RSPI=3` remains the highest documented cadence and true higher PRF is not exposed. +- If an undocumented command returns `OK` and changes cadence, it must stay experimental until validated against RF behavior, data quality, and module stability. + +## Testing + +Unit tests should cover protocol and summarization without hardware: + +- Packet builder creates correct command headers and payload lengths. +- Packet reader handles split headers and split payloads. +- `DONE` frame gap detection works across wrap-free monotonically increasing frame numbers. +- Summary statistics are correct for synthetic packet logs. +- Unsafe mode refuses undocumented probes unless `--unsafe-probe` is present. +- `RFSE` is refused unless `--allow-factory-reset` is present. +- Parameter restore runs in `finally` when initial parameters were captured. + +Hardware verification is manual and explicit: + +```bash +uv run python scripts/hardware-test/probe_kld7_timing.py \ + --port /dev/kld7_vertical \ + --duration 10 \ + --frame-mask RADC,DONE \ + --output /tmp/kld7_vertical_timing.jsonl +``` + +## Out Of Scope + +- Production use of undocumented commands. +- Automatic fuzzing of the K-LD7 firmware. +- Firmware extraction or binary reverse engineering. +- RF lab measurement of actual waveform timing. +- Changes to live launch-angle selection. +- UI changes. +- Replacing the `kld7` Python package in production. + +## Files Created Or Modified + +| File | Change | +|------|--------| +| `scripts/hardware-test/probe_kld7_timing.py` | New guarded timing/protocol probe | +| `tests/test_probe_kld7_timing.py` | New protocol, summary, and safety tests | +| `docs/kld7-troubleshooting.md` | Add a short section linking the probe and explaining interpretation | diff --git a/scripts/hardware-test/probe_kld7_timing.py b/scripts/hardware-test/probe_kld7_timing.py new file mode 100644 index 000000000..b8cd05ca0 --- /dev/null +++ b/scripts/hardware-test/probe_kld7_timing.py @@ -0,0 +1,543 @@ +#!/usr/bin/env python3 +"""Guarded K-LD7 timing and protocol probe.""" + +from __future__ import annotations + +import argparse +import json +import statistics +import struct +import sys +import time +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Iterable, Optional + +try: + import serial + from serial.tools.list_ports import comports +except ImportError: # pragma: no cover - operator environment issue + serial = None + comports = None + + +DEFAULT_BAUD = 3_000_000 +DEFAULT_START_BAUD = 115_200 +SUPPORTED_BAUD_RATES = [115_200, 460_800, 921_600, 2_000_000, 3_000_000] +MAX_PACKET_PAYLOAD_BYTES = 8192 +FRAME_CODES = { + "RADC": 0x01, + "RFFT": 0x02, + "PDAT": 0x04, + "TDAT": 0x08, + "DDAT": 0x10, + "DONE": 0x20, +} +DOCUMENTED_WRITE_COMMANDS = { + "RBFR", + "RSPI", + "RRAI", + "THOF", + "TRFT", + "VISU", + "MIRA", + "MARA", + "MIAN", + "MAAN", + "MISP", + "MASP", + "DEDI", + "RATH", + "ANTH", + "SPTH", + "DIG1", + "DIG2", + "DIG3", + "HOLD", + "MIDE", + "MIDS", +} +DESTRUCTIVE_COMMANDS = {"RFSE"} +PARAM_STRUCT_FORMAT = "<19s8B2b4Bb4BH2B" +PARAM_FIELDS = [ + "software_version", + "RBFR", + "RSPI", + "RRAI", + "THOF", + "TRFT", + "VISU", + "MIRA", + "MARA", + "MIAN", + "MAAN", + "MISP", + "MASP", + "DEDI", + "RATH", + "ANTH", + "SPTH", + "DIG1", + "DIG2", + "DIG3", + "HOLD", + "MIDE", + "MIDS", +] + + +@dataclass +class PacketRecord: + """One packet or packet-level error observed while probing.""" + + code: str + payload_bytes: int + command: Optional[str] = None + response_code: Optional[int] = None + send_monotonic: Optional[float] = None + first_byte_monotonic: Optional[float] = None + header_complete_monotonic: Optional[float] = None + complete_monotonic: Optional[float] = None + read_duration_ms: Optional[float] = None + done_frame: Optional[int] = None + error: Optional[str] = None + payload: bytes = field(default=b"", repr=False) + + +def build_packet(command: str, payload: bytes = b"") -> bytes: + """Build a K-LD7 binary command packet.""" + cmd = command.upper().encode("ascii") + if len(cmd) != 4: + raise ValueError("command must be exactly 4 ASCII characters") + return struct.pack("<4sI", cmd, len(payload)) + payload + + +def validate_probe_command(command: str, hex_payload: str) -> Optional[str]: + """Validate one explicit unsafe probe command specification.""" + try: + raw = command.encode("ascii") + except UnicodeEncodeError: + return "command must be ASCII" + if len(raw) != 4: + return "command must be exactly 4 ASCII characters" + if command != command.upper(): + return "command must be uppercase ASCII" + if len(hex_payload) % 2: + return "hex payload must have an even number of characters" + try: + bytes.fromhex(hex_payload) + except ValueError: + return "hex payload must be valid hexadecimal" + return None + + +def parse_frame_mask(value: str) -> int: + """Parse a comma-separated frame mask such as RADC,DONE.""" + mask = 0 + for raw_name in value.split(","): + name = raw_name.strip().upper() + if not name: + continue + if name not in FRAME_CODES: + raise argparse.ArgumentTypeError(f"unknown frame type {name!r}") + mask |= FRAME_CODES[name] + if mask == 0: + raise argparse.ArgumentTypeError("at least one frame type is required") + return mask + + +def _response_name(code: Optional[int]) -> Optional[str]: + names = { + 0: "OK", + 1: "UnknownCommand", + 2: "InvalidParameter", + 3: "InvalidRPSTVersion", + 4: "UARTError", + 5: "SensorBusy", + } + return names.get(code) + + +class KLD7Protocol: + """Small direct serial protocol wrapper for K-LD7 packets.""" + + def __init__(self, port_path: str, baud: int = DEFAULT_BAUD, timeout: float = 0.2): + if serial is None: + raise RuntimeError("pyserial is required for hardware probing") + self.port_path = port_path + self.baud = baud + self.port = serial.Serial( + port=port_path, + baudrate=DEFAULT_START_BAUD, + parity=serial.PARITY_EVEN, + stopbits=serial.STOPBITS_ONE, + timeout=timeout, + ) + self._init_device() + + def _init_device(self) -> None: + """Negotiate the requested baud rate, recovering once from a stuck 3M session.""" + try: + self._send_init() + except Exception: + self._send_gbye_at_3mbaud() + self._send_init() + + def _send_init(self) -> None: + if self.baud not in SUPPORTED_BAUD_RATES: + raise ValueError(f"unsupported baud rate: {self.baud}") + response = self.send_command( + "INIT", + SUPPORTED_BAUD_RATES.index(self.baud).to_bytes(4, "little", signed=True), + ) + if response.response_code != 0: + name = _response_name(response.response_code) or response.response_code + raise RuntimeError(f"INIT failed: {name}") + if self.baud != DEFAULT_START_BAUD: + self.port.baudrate = self.baud + + def _send_gbye_at_3mbaud(self) -> None: + try: + self.port.baudrate = DEFAULT_BAUD + self.port.reset_input_buffer() + self.port.write(build_packet("GBYE")) + self.port.flush() + time.sleep(0.3) + finally: + self.port.baudrate = DEFAULT_START_BAUD + + def _read_exact(self, n: int) -> tuple[bytes, Optional[float]]: + buf = b"" + first_byte_at = None + timeout = max(float(getattr(self.port, "timeout", 0.2) or 0.2), 0.2) + deadline = time.monotonic() + timeout + while len(buf) < n: + chunk = self.port.read(n - len(buf)) + if chunk: + if first_byte_at is None: + first_byte_at = time.monotonic() + buf += chunk + deadline = time.monotonic() + timeout + continue + if time.monotonic() >= deadline: + break + time.sleep(0.001) + return buf, first_byte_at + + def read_packet(self) -> PacketRecord: + started = time.monotonic() + header, first_byte_at = self._read_exact(8) + header_complete = time.monotonic() + if len(header) != 8: + return PacketRecord( + code="", + payload_bytes=0, + first_byte_monotonic=first_byte_at, + header_complete_monotonic=header_complete, + complete_monotonic=header_complete, + read_duration_ms=(header_complete - started) * 1000.0, + error=f"short header read: got {len(header)} of 8 bytes", + ) + raw_code, length = struct.unpack("<4sI", header) + code = raw_code.decode("ascii", errors="replace") + if length > MAX_PACKET_PAYLOAD_BYTES: + complete = time.monotonic() + return PacketRecord( + code=code, + payload_bytes=length, + first_byte_monotonic=first_byte_at, + header_complete_monotonic=header_complete, + complete_monotonic=complete, + read_duration_ms=(complete - (first_byte_at or started)) * 1000.0, + error=f"invalid payload length: {length} bytes", + ) + payload = b"" + payload_first = None + if length: + payload, payload_first = self._read_exact(length) + complete = time.monotonic() + error = None + if len(payload) != length: + error = f"short payload read: got {len(payload)} of {length} bytes" + done_frame = None + if code == "DONE" and len(payload) == 4: + done_frame = int.from_bytes(payload, "little", signed=False) + return PacketRecord( + code=code, + payload_bytes=length, + first_byte_monotonic=first_byte_at or payload_first, + header_complete_monotonic=header_complete, + complete_monotonic=complete, + read_duration_ms=(complete - (first_byte_at or started)) * 1000.0, + done_frame=done_frame, + error=error, + payload=payload, + ) + + def send_command(self, command: str, payload: bytes = b"") -> PacketRecord: + sent = time.monotonic() + self.port.reset_input_buffer() + self.port.write(build_packet(command, payload)) + self.port.flush() + packet = self.read_packet() + packet.command = command.upper() + packet.send_monotonic = sent + if packet.code == "RESP" and packet.payload: + packet.response_code = packet.payload[0] + return packet + + def request_frame(self, frame_mask: int) -> list[PacketRecord]: + records = [self.send_command("GNFD", int(frame_mask).to_bytes(4, "little", signed=True))] + expected = bin(frame_mask).count("1") + for _ in range(expected): + record = self.read_packet() + records.append(record) + if record.code == "DONE": + break + return records + + def read_params(self) -> tuple[dict, list[PacketRecord]]: + response = self.send_command("GRPS") + records = [response] + if response.response_code != 0: + return {}, records + packet = self.read_packet() + records.append(packet) + if packet.code != "RPST" or packet.error: + return {}, records + values = struct.unpack(PARAM_STRUCT_FORMAT, packet.payload) + params = {} + for name, value in zip(PARAM_FIELDS, values): + if name == "software_version": + params[name] = value.split(b"\x00", 1)[0].decode("ascii", errors="ignore") + else: + params[name] = int(value) + return params, records + + def set_param(self, command: str, value: int) -> PacketRecord: + if command not in DOCUMENTED_WRITE_COMMANDS: + raise ValueError(f"not a documented parameter command: {command}") + return self.send_command(command, int(value).to_bytes(4, "little", signed=True)) + + def restore_params(self, params: dict) -> list[PacketRecord]: + records = [] + for command in PARAM_FIELDS[1:]: + if command in params: + records.append(self.set_param(command, int(params[command]))) + return records + + def close(self) -> None: + try: + self.port.write(build_packet("GBYE")) + self.port.flush() + finally: + self.port.close() + + +def _percentile(values: list[float], percentile: float) -> Optional[float]: + if not values: + return None + values = sorted(values) + index = min(len(values) - 1, max(0, round((percentile / 100.0) * (len(values) - 1)))) + return values[index] + + +def summarize_packets(packets: list[PacketRecord], duration_s: float) -> dict: + """Summarize packet-level timing and frame continuity.""" + radc_packets = [p for p in packets if p.code == "RADC" and not p.error] + done_packets = [p for p in packets if p.code == "DONE" and not p.error] + done_frames = [p.done_frame for p in done_packets if p.done_frame is not None] + gaps = 0 + for previous, current in zip(done_frames, done_frames[1:]): + if current > previous + 1: + gaps += current - previous - 1 + read_durations = [p.read_duration_ms for p in radc_packets if p.read_duration_ms is not None] + errors: dict[str, int] = {} + for packet in packets: + if packet.error: + errors[packet.error] = errors.get(packet.error, 0) + 1 + return { + "duration_s": duration_s, + "radc_frames": len(radc_packets), + "done_frames": len(done_packets), + "effective_radc_hz": round(len(radc_packets) / duration_s, 3) if duration_s > 0 else 0.0, + "effective_done_hz": round(len(done_packets) / duration_s, 3) if duration_s > 0 else 0.0, + "done_frame_gaps": gaps, + "read_duration_ms_mean": statistics.mean(read_durations) if read_durations else None, + "read_duration_ms_p50": statistics.median(read_durations) if read_durations else None, + "read_duration_ms_p95": _percentile(read_durations, 95), + "errors": errors, + } + + +def parse_probe_command(command_spec: str) -> tuple[str, bytes]: + command, _, hex_payload = command_spec.partition(":") + return command, bytes.fromhex(hex_payload) + + +def find_kld7_port() -> Optional[str]: + """Return a K-LD7-like serial port only when exactly one candidate is present.""" + if comports is None: + return None + matches = [] + for port in comports(): + desc = (port.description or "").lower() + mfg = (port.manufacturer or "").lower() + if any(kw in desc for kw in ["ftdi", "cp210", "usb-serial", "uart"]) or any( + kw in mfg for kw in ["ftdi", "silicon labs"] + ): + matches.append(port.device) + return matches[0] if len(matches) == 1 else None + + +def measure(protocol: KLD7Protocol, frame_mask: int, duration_s: float) -> list[PacketRecord]: + """Request frames until the duration expires.""" + records = [] + deadline = time.monotonic() + duration_s + while time.monotonic() < deadline: + records.extend(protocol.request_frame(frame_mask)) + return records + + +def apply_documented_settings( + protocol: KLD7Protocol, args: argparse.Namespace +) -> list[PacketRecord]: + records = [] + if args.rrai is not None: + records.append(protocol.set_param("RRAI", args.rrai)) + if args.rbfr is not None: + records.append(protocol.set_param("RBFR", args.rbfr)) + return records + + +def run_unsafe_probes(protocol: KLD7Protocol, args: argparse.Namespace) -> list[PacketRecord]: + records = [] + for command_spec in args.probe_command: + command, payload = parse_probe_command(command_spec) + records.append(protocol.send_command(command, payload)) + _params, param_records = protocol.read_params() + records.extend(param_records) + return records + + +def write_jsonl(path: Path, records: Iterable[PacketRecord], summary: dict) -> None: + with path.open("w", encoding="utf-8") as handle: + for record in records: + data = asdict(record) + data.pop("payload", None) + handle.write(json.dumps({"type": "packet", **data}, sort_keys=True) + "\n") + handle.write(json.dumps({"type": "summary", **summary}, sort_keys=True) + "\n") + + +def _run_one_configuration( + args: argparse.Namespace, rspi: Optional[int] = None +) -> tuple[list, dict]: + port = args.port or find_kld7_port() + if not port: + raise RuntimeError( + "--port is required unless exactly one K-LD7-like serial port is present" + ) + frame_mask = parse_frame_mask(args.frame_mask) + protocol = KLD7Protocol(port, baud=args.baud) + records: list[PacketRecord] = [] + params_before: dict = {} + try: + params_before, param_records = protocol.read_params() + records.extend(param_records) + records.extend(apply_documented_settings(protocol, args)) + if rspi is not None: + records.append(protocol.set_param("RSPI", rspi)) + params_active, param_records = protocol.read_params() + records.extend(param_records) + if args.unsafe_probe: + records.extend(run_unsafe_probes(protocol, args)) + measurement_records = measure(protocol, frame_mask, args.duration) + records.extend(measurement_records) + summary = summarize_packets(measurement_records, args.duration) + summary.update( + { + "port": port, + "baud": args.baud, + "frame_mask": [name for name, value in FRAME_CODES.items() if frame_mask & value], + "params_before": params_before, + "params_active": params_active, + } + ) + if rspi is not None: + summary["rspi_sweep_value"] = rspi + return records, summary + finally: + if params_before and not args.no_restore_params: + records.extend(protocol.restore_params(params_before)) + protocol.close() + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Probe K-LD7 RADC timing and guarded commands.") + parser.add_argument("--port") + parser.add_argument("--baud", type=int, default=DEFAULT_BAUD, choices=SUPPORTED_BAUD_RATES) + parser.add_argument("--duration", type=float, default=10.0) + parser.add_argument("--frame-mask", default="RADC,DONE") + parser.add_argument("--rspi-sweep", action="store_true") + parser.add_argument("--rrai", type=int) + parser.add_argument("--rbfr", type=int) + parser.add_argument("--output", type=Path) + parser.add_argument("--unsafe-probe", action="store_true") + parser.add_argument("--probe-command", action="append", default=[]) + parser.add_argument("--allow-factory-reset", action="store_true") + parser.add_argument("--no-restore-params", action="store_true") + return parser + + +def validate_args(args: argparse.Namespace) -> list[str]: + errors = [] + if args.duration <= 0: + errors.append("--duration must be positive") + if args.unsafe_probe and not args.output: + errors.append("--unsafe-probe requires --output so probe activity is auditable") + if args.probe_command and not args.unsafe_probe: + errors.append("--probe-command requires --unsafe-probe") + if args.no_restore_params and not args.unsafe_probe: + errors.append("--no-restore-params requires --unsafe-probe") + try: + parse_frame_mask(args.frame_mask) + except argparse.ArgumentTypeError as exc: + errors.append(str(exc)) + for command_spec in args.probe_command: + command, _, hex_payload = command_spec.partition(":") + error = validate_probe_command(command, hex_payload) + if error: + errors.append(f"{command_spec}: {error}") + if command.upper() in DESTRUCTIVE_COMMANDS and not args.allow_factory_reset: + errors.append("RFSE is refused unless --allow-factory-reset is set") + return errors + + +def main(argv: Optional[list[str]] = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + errors = validate_args(args) + if errors: + for error in errors: + print(f"error: {error}", file=sys.stderr) + return 2 + all_records = [] + summaries = [] + try: + rspi_values: Iterable[Optional[int]] = range(4) if args.rspi_sweep else [None] + for rspi in rspi_values: + records, summary = _run_one_configuration(args, rspi) + all_records.extend(records) + summaries.append(summary) + except Exception as exc: # pylint: disable=broad-except + print(f"error: {exc}", file=sys.stderr) + return 1 + output = summaries[0] if len(summaries) == 1 else {"runs": summaries} + print(json.dumps(output, indent=2, sort_keys=True)) + if args.output: + write_jsonl(args.output, all_records, output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_probe_kld7_timing.py b/tests/test_probe_kld7_timing.py new file mode 100644 index 000000000..1128232af --- /dev/null +++ b/tests/test_probe_kld7_timing.py @@ -0,0 +1,189 @@ +"""Tests for the guarded K-LD7 timing probe.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "hardware-test" / "probe_kld7_timing.py" +spec = importlib.util.spec_from_file_location("probe_kld7_timing", SCRIPT) +probe = importlib.util.module_from_spec(spec) +assert spec.loader is not None +sys.modules[spec.name] = probe +spec.loader.exec_module(probe) + + +class FakeSerial: + """Small serial fake that can return deliberately split chunks.""" + + def __init__(self, chunks): + self.chunks = list(chunks) + self.writes = [] + self.timeout = 0.2 + self.baudrate = 115200 + self.closed = False + + def read(self, n): + if not self.chunks: + return b"" + chunk = self.chunks.pop(0) + if len(chunk) > n: + self.chunks.insert(0, chunk[n:]) + return chunk[:n] + return chunk + + def write(self, data): + self.writes.append(data) + return len(data) + + def flush(self): + return None + + def reset_input_buffer(self): + return None + + def close(self): + self.closed = True + + +def test_build_packet_uppercases_command_and_packs_length(): + packet = probe.build_packet("gnfd", (0x21).to_bytes(4, "little")) + + assert packet == b"GNFD\x04\x00\x00\x00!\x00\x00\x00" + + +def test_validate_command_rejects_non_four_byte_command(): + assert probe.validate_probe_command("ABC", "") == "command must be exactly 4 ASCII characters" + + +def test_validate_command_rejects_non_uppercase_command(): + assert probe.validate_probe_command("test", "") == "command must be uppercase ASCII" + + +def test_validate_command_rejects_odd_hex_payload(): + assert ( + probe.validate_probe_command("TST1", "abc") + == "hex payload must have an even number of characters" + ) + + +def test_read_packet_handles_split_header_and_payload(): + payload = b"\x00\x01\x02\x03" + header = b"DONE" + len(payload).to_bytes(4, "little") + fake = FakeSerial([header[:3], header[3:8], payload[:1], payload[1:]]) + protocol = probe.KLD7Protocol.__new__(probe.KLD7Protocol) + protocol.port = fake + + packet = protocol.read_packet() + + assert packet.code == "DONE" + assert packet.payload == payload + assert packet.payload_bytes == 4 + + +def test_summarize_measurements_counts_done_gaps(): + packets = [ + probe.PacketRecord(code="DONE", payload_bytes=4, complete_monotonic=1.0, done_frame=10), + probe.PacketRecord( + code="RADC", payload_bytes=3072, complete_monotonic=1.1, read_duration_ms=10.0 + ), + probe.PacketRecord(code="DONE", payload_bytes=4, complete_monotonic=2.0, done_frame=12), + probe.PacketRecord( + code="RADC", payload_bytes=3072, complete_monotonic=2.1, read_duration_ms=20.0 + ), + ] + + summary = probe.summarize_packets(packets, duration_s=2.0) + + assert summary["radc_frames"] == 2 + assert summary["done_frames"] == 2 + assert summary["done_frame_gaps"] == 1 + assert summary["effective_radc_hz"] == 1.0 + assert summary["read_duration_ms_p95"] == 20.0 + + +def test_unsafe_probe_requires_output(): + parser = probe.build_parser() + args = parser.parse_args(["--port", "/dev/null", "--unsafe-probe", "--probe-command", "TEST"]) + + assert probe.validate_args(args) == [ + "--unsafe-probe requires --output so probe activity is auditable" + ] + + +def test_rfse_requires_factory_reset_flag(): + parser = probe.build_parser() + args = parser.parse_args( + [ + "--port", + "/dev/null", + "--output", + "/tmp/probe.jsonl", + "--unsafe-probe", + "--probe-command", + "RFSE", + ] + ) + + assert probe.validate_args(args) == ["RFSE is refused unless --allow-factory-reset is set"] + + +def test_parse_frame_mask_combines_known_flags(): + assert probe.parse_frame_mask("RADC,DONE") == 0x21 + + +def test_probe_command_requires_unsafe_probe(): + parser = probe.build_parser() + args = parser.parse_args(["--port", "/dev/null", "--probe-command", "TEST"]) + + assert probe.validate_args(args) == ["--probe-command requires --unsafe-probe"] + + +def test_rfse_allowed_when_factory_reset_flag_is_present(): + parser = probe.build_parser() + args = parser.parse_args( + [ + "--port", + "/dev/null", + "--output", + "/tmp/probe.jsonl", + "--unsafe-probe", + "--allow-factory-reset", + "--probe-command", + "RFSE", + ] + ) + + assert probe.validate_args(args) == [] + + +def test_send_command_records_response_code_and_written_packet(): + payload = b"\x00" + header = b"RESP" + len(payload).to_bytes(4, "little") + fake = FakeSerial([header, payload]) + protocol = probe.KLD7Protocol.__new__(probe.KLD7Protocol) + protocol.port = fake + + response = protocol.send_command("TEST", b"\x01\x02") + + assert fake.writes == [b"TEST\x02\x00\x00\x00\x01\x02"] + assert response.command == "TEST" + assert response.response_code == 0 + + +def test_parse_probe_command_decodes_hex_payload(): + assert probe.parse_probe_command("TEST:0102ff") == ("TEST", b"\x01\x02\xff") + + +def test_write_jsonl_strips_payload_bytes(tmp_path): + output = tmp_path / "probe.jsonl" + records = [probe.PacketRecord(code="RADC", payload_bytes=3072, payload=b"\x00\x01")] + summary = {"radc_frames": 1} + + probe.write_jsonl(output, records, summary) + + lines = output.read_text(encoding="utf-8").splitlines() + assert '"payload"' not in lines[0] + assert '"type": "packet"' in lines[0] + assert '"type": "summary"' in lines[1] From ed4e8885d373cdb8c31435cc5913f59146e02bf0 Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Thu, 11 Jun 2026 10:00:49 -0700 Subject: [PATCH 15/29] [cr] some code cleanup --- src/openflight/kld7/radc.py | 174 +++++++++++++-------------------- src/openflight/kld7/tracker.py | 32 +++--- tests/test_kld7.py | 22 ----- 3 files changed, 79 insertions(+), 149 deletions(-) diff --git a/src/openflight/kld7/radc.py b/src/openflight/kld7/radc.py index 9bdb8190b..57da2babf 100644 --- a/src/openflight/kld7/radc.py +++ b/src/openflight/kld7/radc.py @@ -9,6 +9,7 @@ import logging import math from dataclasses import asdict, dataclass +from functools import lru_cache import numpy as np @@ -52,6 +53,8 @@ WAVELENGTH_M = 3e8 / 24.125e9 # ~12.43 mm ANTENNA_SPACING_M = 8.0e-3 # ~0.64λ, calibrated against PDAT reference data +MPH_TO_KMH = 1.609 + # Vertical rule-stack candidate selection (impact-relative, OPS-anchored). # Primary frames are far enough after impact to usually be ball-only. The # early context window can support a primary anchor when it is OPS-bin aligned, @@ -92,17 +95,35 @@ def parse_radc_payload(payload: bytes) -> dict[str, np.ndarray]: } +def _channels_from_frame(radc_raw): + """Return the channel dict for a frame's RADC payload. + + Frames carry either the raw 3072-byte payload (live path) or an + already-parsed channel dict (offline replay). + """ + return parse_radc_payload(radc_raw) if isinstance(radc_raw, bytes) else radc_raw + + def to_complex_iq(i_channel: np.ndarray, q_channel: np.ndarray) -> np.ndarray: """Convert uint16 I/Q arrays to complex float, removing DC offset. Uses per-channel mean removal instead of a fixed midpoint, since the K-LD7 ADC bias varies across channels and units. """ - i_float = i_channel.astype(np.float64) - np.mean(i_channel.astype(np.float64)) - q_float = q_channel.astype(np.float64) - np.mean(q_channel.astype(np.float64)) + i_float = i_channel.astype(np.float64) + i_float -= i_float.mean() + q_float = q_channel.astype(np.float64) + q_float -= q_float.mean() return i_float + 1j * q_float +@lru_cache(maxsize=4) +def _hann_window(n: int) -> np.ndarray: + # Cached per length: this runs twice per RADC frame at ~34 Hz, and the + # window never changes. Callers must not mutate the returned array. + return np.hanning(n) + + def compute_spectrum( iq: np.ndarray, fft_size: int = 2048, dc_mask_bins: int = DC_MASK_BINS ) -> np.ndarray: @@ -116,11 +137,7 @@ def compute_spectrum( Returns: Magnitude spectrum (linear scale), length = fft_size """ - windowed = iq * np.hanning(len(iq)) - padded = np.zeros(fft_size, dtype=np.complex128) - padded[: len(windowed)] = windowed - fft_result = np.fft.fft(padded) - magnitude = np.abs(fft_result) + magnitude = np.abs(np.fft.fft(iq * _hann_window(len(iq)), n=fft_size)) # Mask DC leakage at both ends of the spectrum if dc_mask_bins > 0: magnitude[:dc_mask_bins] = 0.0 @@ -132,10 +149,7 @@ def compute_fft_complex( iq: np.ndarray, fft_size: int = 2048, dc_mask_bins: int = DC_MASK_BINS ) -> np.ndarray: """Compute complex FFT output (not magnitude) for phase-based processing.""" - windowed = iq * np.hanning(len(iq)) - padded = np.zeros(fft_size, dtype=np.complex128) - padded[: len(windowed)] = windowed - result = np.fft.fft(padded) + result = np.fft.fft(iq * _hann_window(len(iq)), n=fft_size) if dc_mask_bins > 0: result[:dc_mask_bins] = 0.0 result[-dc_mask_bins:] = 0.0 @@ -345,7 +359,7 @@ def aliased_velocity_from_ball_speed_mph( max_speed_kmh: float = 100.0, ) -> float: """Map true ball speed to the K-LD7 aliased Doppler velocity in km/h.""" - ball_speed_kmh = ball_speed_mph * 1.609 + ball_speed_kmh = ball_speed_mph * MPH_TO_KMH unambiguous_range = max_speed_kmh * 2.0 aliased_kmh = ball_speed_kmh % unambiguous_range if aliased_kmh > max_speed_kmh: @@ -411,7 +425,7 @@ def ball_bin_range_from_speed( tolerance_mph: Search window around the expected velocity (±) """ aliased_kmh = aliased_velocity_from_ball_speed_mph(ball_speed_mph, max_speed_kmh) - tol_kmh = tolerance_mph * 1.609 + tol_kmh = tolerance_mph * MPH_TO_KMH lo_vel = aliased_kmh - tol_kmh hi_vel = aliased_kmh + tol_kmh @@ -604,9 +618,9 @@ def _find_peak_near_expected_bin( if sub_hi <= sub_lo: continue indices = np.arange(sub_lo, sub_hi, dtype=int) - mask = np.array( - [circular_bin_distance(idx, expected_bin, fft_size) <= tolerance for idx in indices] - ) + # Vectorized circular_bin_distance over the whole sub-band + direct = np.abs(indices - expected_bin) + mask = np.minimum(direct, fft_size - direct) <= tolerance if not mask.any(): continue near_indices = indices[mask] @@ -713,8 +727,6 @@ class _VerticalFrameCandidate: snr_linear: float angle_deg: float speed_mph: float - raw_angle_deg: float - geom_bearing_deg: float t_after_impact_s: float | None phase_coherence: float | None peak_width_bins: int @@ -1001,7 +1013,7 @@ def empty(reason: str, has_radc: bool, warnings: tuple[str, ...]) -> RADCFrameDi return empty("missing_radc", has_radc=False, warnings=("missing_radc",)) try: - channels = parse_radc_payload(radc_raw) if isinstance(radc_raw, bytes) else radc_raw + channels = _channels_from_frame(radc_raw) except ValueError: return empty("invalid_payload_size", has_radc=True, warnings=("invalid_payload",)) if not isinstance(channels, dict): @@ -1100,7 +1112,7 @@ def empty(reason: str, has_radc: bool, warnings: tuple[str, ...]) -> RADCFrameDi else None ) peak_velocity_kmh = bin_to_velocity_kmh(peak_bin, fft_size, max_speed_kmh) - peak_ball_speed_mph = (2.0 * max_speed_kmh + peak_velocity_kmh) / 1.609 + peak_ball_speed_mph = (2.0 * max_speed_kmh + peak_velocity_kmh) / MPH_TO_KMH speed_error_mph = ( peak_ball_speed_mph - ops243_ball_speed_mph if ops243_ball_speed_mph is not None else None ) @@ -1247,7 +1259,7 @@ def find_impact_frames( energies.append(0.0) continue try: - channels = parse_radc_payload(radc) if isinstance(radc, bytes) else radc + channels = _channels_from_frame(radc) iq = to_complex_iq(channels["f1a_i"], channels["f1a_q"]) except (KeyError, TypeError, ValueError): energies.append(0.0) @@ -1279,7 +1291,6 @@ def extract_launch_angle( frames: list[dict], fft_size: int = 2048, max_speed_kmh: float = 100.0, - cfar_threshold: float = 2.5, impact_energy_threshold: float = 3.0, angle_offset_deg: float = 0.0, ops243_ball_speed_mph: float | None = None, @@ -1303,7 +1314,8 @@ def extract_launch_angle( Pipeline: 1. Find impact frames (high-velocity energy spikes) 2. Group consecutive impacts into shot events - 3. For each shot, run band-limited CFAR in the ball velocity range + 3. For each shot, find the strongest peak in the ball velocity band + (median-SNR gated, OPS-anchored when ball speed is available) 4. Per-bin interferometric angle estimation on ball detections. The per-frame angle is the magnitude²-weighted centroid of the per-bin angles inside the spectral peak (bins whose magnitude @@ -1414,9 +1426,7 @@ def extract_launch_angle( ball_bands=ball_bands, ) if not impact_indices: - import logging - - logging.getLogger("openflight.kld7.radc").info( + logger.info( "[KLD7-RADC] No impact frames found (energy_threshold=%.1f, ball_bands=%s, %d frames)", impact_energy_threshold, ball_bands, @@ -1451,25 +1461,20 @@ def extract_launch_angle( peak_snrs = [] peak_speeds_mph = [] peak_bins: list[int] = [] - peak_times: list[float | None] = [] # flight time per frame (s), for geometry peak_coherences: list[float | None] = [] peak_widths: list[int] = [] peak_ops_anchor_weak: list[bool] = [] peak_frame_indices: list[int] = [] + # Time after impact per frame (s) — doubles as the flight time + # used by the geometry estimator. per_frame_t_after_impact: list[float | None] = [] for fi in sorted(frame_set): radc_raw = frames[fi].get("radc") if radc_raw is None: continue - frame_ts = frames[fi].get("timestamp") - flight_time_s = ( - float(frame_ts) - float(time_ref) - if frame_ts is not None and time_ref is not None - else None - ) try: - channels = parse_radc_payload(radc_raw) if isinstance(radc_raw, bytes) else radc_raw + channels = _channels_from_frame(radc_raw) except (KeyError, TypeError, ValueError): continue @@ -1539,47 +1544,23 @@ def extract_launch_angle( # Magnitude²-weighted centroid of the per-bin angles across # the spectral peak, rather than the raw angle at a single - # bin. Search a small neighborhood (`centroid_search_bins`) - # around the peak and include bins whose magnitude is at - # least `centroid_floor_frac` of the peak. For a range- - # spread target this integrates the angle estimate across - # all the energy in the peak; restricting to a neighborhood - # prevents random noise bins elsewhere in the band (which - # have similar magnitudes when there is no real ball signal) - # from contributing. This is the wideband monopulse - # formulation (Zhang et al., Sensors 2016). - if centroid_floor_frac < 1.0: - # Clip the centroid neighborhood to the sub-band that - # contains the peak. This prevents the neighborhood - # from spilling across the wrap into an unrelated - # spectral region when the ball band wraps around DC. - # Fall back to FFT bounds if peak_bin sits outside any - # listed sub-band (defensive — shouldn't happen). - sub_for_peak = next( - (sub for sub in ball_bands if sub[0] <= peak_bin < sub[1]), - (0, fft_size), - ) - sub_lo, sub_hi = sub_for_peak - lo_n = max(sub_lo, peak_bin - CENTROID_SEARCH_BINS) - hi_n = min(sub_hi, peak_bin + CENTROID_SEARCH_BINS + 1) - neigh = spec[lo_n:hi_n] - neigh_mask = neigh >= peak_val * centroid_floor_frac - if neigh_mask.any(): - neigh_indices = np.flatnonzero(neigh_mask) + lo_n - neigh_w = neigh[neigh_mask] ** 2 - neigh_w_sum = float(neigh_w.sum()) - if neigh_w_sum > 0: - centroid_angle = float( - np.sum(angles[neigh_indices] * neigh_w) / neigh_w_sum - ) - else: - centroid_angle = float(angles[peak_bin]) - else: - centroid_angle = float(angles[peak_bin]) - else: - # Disabled (frac=1.0) — fall back to the legacy - # single-peak-bin angle for exact backward compatibility. - centroid_angle = float(angles[peak_bin]) + # bin. For a range-spread target this integrates the angle + # estimate across all the energy in the peak; restricting to + # a neighborhood prevents random noise bins elsewhere in the + # band (which have similar magnitudes when there is no real + # ball signal) from contributing. This is the wideband + # monopulse formulation (Zhang et al., Sensors 2016). + # `peak_band` is the sub-band the peak was found in, so the + # neighborhood cannot spill across the DC wrap into an + # unrelated spectral region. + centroid_angle, peak_width = _centroid_angle_for_peak( + angles, + spec, + peak_bin, + peak_val, + peak_band, + centroid_floor_frac, + ) phase_coherence = _phase_coherence_for_peak( f1a_fft, f2a_fft, @@ -1589,19 +1570,9 @@ def extract_launch_angle( peak_band, coherence_bins=4, ) - peak_width = int( - _centroid_angle_for_peak( - angles, - spec, - peak_bin, - peak_val, - peak_band, - centroid_floor_frac, - )[1] - ) vel = bin_to_velocity_kmh(peak_bin, fft_size, max_speed_kmh) - frame_speed_mph = (200.0 + vel) / 1.609 + frame_speed_mph = (2.0 * max_speed_kmh + vel) / MPH_TO_KMH frame_ts = _optional_float(frames[fi].get("timestamp")) t_after_impact: float | None = None if frame_ts is not None and time_ref is not None: @@ -1611,7 +1582,6 @@ def extract_launch_angle( peak_snrs.append(snr) peak_bins.append(peak_bin) peak_speeds_mph.append(frame_speed_mph) - peak_times.append(flight_time_s) peak_coherences.append(phase_coherence) peak_widths.append(peak_width) peak_ops_anchor_weak.append(ops_anchor_weak) @@ -1624,7 +1594,6 @@ def extract_launch_angle( angs = np.array(peak_angles) snrs = np.array(peak_snrs) bins_arr = np.array(peak_bins, dtype=int) - times_arr = np.array([np.nan if t is None else t for t in peak_times], dtype=float) weak_ops_anchor_arr = np.array(peak_ops_anchor_weak, dtype=bool) frame_indices_arr = np.array(peak_frame_indices, dtype=int) t_after_arr = np.array( @@ -1640,8 +1609,6 @@ def extract_launch_angle( ], dtype=float, ) - raw_angs = angs.copy() - geom_arr = np.zeros_like(angs, dtype=float) used_rule_stack = False if orientation == "vertical" and time_ref is not None: candidates: list[_VerticalFrameCandidate] = [] @@ -1659,8 +1626,6 @@ def extract_launch_angle( snr_linear=float(snrs[i]), angle_deg=float(angle), speed_mph=float(peak_speeds_mph[i]), - raw_angle_deg=float(raw_angs[i]), - geom_bearing_deg=float(geom_arr[i]), t_after_impact_s=per_frame_t_after_impact[i], phase_coherence=peak_coherences[i], peak_width_bins=int(peak_widths[i]), @@ -1677,7 +1642,6 @@ def extract_launch_angle( clean_angs = angs[keep_mask] clean_snrs = snrs[keep_mask] clean_bins = bins_arr[keep_mask] - clean_times = times_arr[keep_mask] clean_weak_ops_anchor = weak_ops_anchor_arr[keep_mask] clean_frame_indices = frame_indices_arr[keep_mask] clean_t_after = t_after_arr[keep_mask] @@ -1710,7 +1674,6 @@ def extract_launch_angle( angs = angs[strong_mask] snrs = snrs[strong_mask] bins_arr = bins_arr[strong_mask] - times_arr = times_arr[strong_mask] weak_ops_anchor_arr = weak_ops_anchor_arr[strong_mask] frame_indices_arr = frame_indices_arr[strong_mask] t_after_arr = t_after_arr[strong_mask] @@ -1725,7 +1688,6 @@ def extract_launch_angle( clean_angs = angs clean_snrs = snrs clean_bins = bins_arr - clean_times = times_arr clean_weak_ops_anchor = weak_ops_anchor_arr clean_frame_indices = frame_indices_arr clean_t_after = t_after_arr @@ -1753,7 +1715,6 @@ def extract_launch_angle( clean_angs = angs[clean_mask] clean_snrs = snrs[clean_mask] clean_bins = bins_arr[clean_mask] - clean_times = times_arr[clean_mask] clean_weak_ops_anchor = weak_ops_anchor_arr[clean_mask] clean_frame_indices = frame_indices_arr[clean_mask] clean_t_after = t_after_arr[clean_mask] @@ -1819,27 +1780,27 @@ def extract_launch_angle( and distance_ft is not None ): per_frame_geom = [ - (float(clean_times[i]), float(clean_angs[i] + angle_offset_deg), float(w[i])) + (float(clean_t_after[i]), float(clean_angs[i] + angle_offset_deg), float(w[i])) for i in range(len(clean_angs)) - if not math.isnan(clean_times[i]) + if not math.isnan(clean_t_after[i]) ] strong_per_frame_geom = [ - (float(clean_times[i]), float(clean_angs[i] + angle_offset_deg), float(w[i])) + (float(clean_t_after[i]), float(clean_angs[i] + angle_offset_deg), float(w[i])) for i in range(len(clean_angs)) - if not math.isnan(clean_times[i]) and not bool(clean_weak_ops_anchor[i]) + if not math.isnan(clean_t_after[i]) and not bool(clean_weak_ops_anchor[i]) ] strong_single_frame_indices = [ i for i in range(len(clean_angs)) - if not math.isnan(clean_times[i]) + if not math.isnan(clean_t_after[i]) and not bool(clean_weak_ops_anchor[i]) - and 0.0 < float(clean_times[i]) <= GEOM_FLIGHT_T_MAX_S + and 0.0 < float(clean_t_after[i]) <= GEOM_FLIGHT_T_MAX_S ] single_frame_fallback_idx = ( min( strong_single_frame_indices, key=lambda i: ( - float(clean_times[i]), + float(clean_t_after[i]), ( math.inf if math.isnan(float(clean_bin_errors[i])) @@ -1887,7 +1848,7 @@ def extract_launch_angle( single_geom = ( fit_launch_angle_single_frame_geometric( ( - float(clean_times[single_frame_fallback_idx]), + float(clean_t_after[single_frame_fallback_idx]), float(clean_angs[single_frame_fallback_idx] + angle_offset_deg), float(w[single_frame_fallback_idx]), ), @@ -1905,7 +1866,6 @@ def extract_launch_angle( clean_angs = clean_angs[selected_single] clean_snrs = clean_snrs[selected_single] clean_bins = clean_bins[selected_single] - clean_times = clean_times[selected_single] clean_weak_ops_anchor = clean_weak_ops_anchor[selected_single] clean_frame_indices = clean_frame_indices[selected_single] clean_t_after = clean_t_after[selected_single] @@ -1919,7 +1879,7 @@ def extract_launch_angle( corrected_angle, geom_single_frame_resid, int(clean_frame_indices[0]), - float(clean_times[0]) * 1000.0, + float(clean_t_after[0]) * 1000.0, float(clean_angs[0] + angle_offset_deg), ) else: @@ -1934,7 +1894,7 @@ def extract_launch_angle( logger.info( "[RADC] Geometry estimator: <2 in-flight frames " "(flight times ms=%s); falling back to naive bearing average", - [None if math.isnan(t) else round(float(t) * 1000.0, 1) for t in clean_times], + [None if math.isnan(t) else round(float(t) * 1000.0, 1) for t in clean_t_after], ) # Hard physical bounds — reject obvious outliers before they diff --git a/src/openflight/kld7/tracker.py b/src/openflight/kld7/tracker.py index 2e43554ee..9fcb3a574 100644 --- a/src/openflight/kld7/tracker.py +++ b/src/openflight/kld7/tracker.py @@ -457,27 +457,19 @@ def _stream_loop(self): "[KLD7] Stream generator exited (frames=%d, %s)", frame_count, self.orientation ) - except KLD7Exception as e: - errors += 1 - logger.warning( - "[KLD7] Stream error %d/%d (%s): %s", errors, max_errors, self.orientation, e - ) - if errors < max_errors: - self._drain_after_stream_error() - continue - reconnects += 1 - if reconnects > max_reconnects: - break - if self._reconnect_after_stream_errors(errors): - errors = 0 - continue - break - except Exception as e: - if _is_recoverable_stream_error(e): + # KLD7Exception is always treated as recoverable; other + # exceptions only when they match known transient patterns. + if isinstance(e, KLD7Exception) or _is_recoverable_stream_error(e): errors += 1 + label = ( + "Stream error" + if isinstance(e, KLD7Exception) + else "Recoverable stream error" + ) logger.warning( - "[KLD7] Recoverable stream error %d/%d (%s): %s", + "[KLD7] %s %d/%d (%s): %s", + label, errors, max_errors, self.orientation, @@ -560,8 +552,8 @@ def _radc_frames_for_extraction( if shot_timestamp is None: return frames, frames_available, 0 - window_before_s = max(float(getattr(self, "buffer_seconds", 0.0) or 0.0), 0.0) - window_after_s = max(float(getattr(self, "shot_window_after_s", 0.0) or 0.0), 0.0) + window_before_s = max(float(self.buffer_seconds or 0.0), 0.0) + window_after_s = max(float(self.shot_window_after_s or 0.0), 0.0) start = shot_timestamp - window_before_s end = shot_timestamp + window_after_s diff --git a/tests/test_kld7.py b/tests/test_kld7.py index 865787567..d03591698 100644 --- a/tests/test_kld7.py +++ b/tests/test_kld7.py @@ -1049,8 +1049,6 @@ def test_vertical_rules_can_pair_strong_anchor_with_weak_rising_neighbor(self): snr_linear=10.0, angle_deg=7.0, speed_mph=117.0, - raw_angle_deg=7.0, - geom_bearing_deg=0.0, t_after_impact_s=0.038, phase_coherence=0.99, peak_width_bins=5, @@ -1062,8 +1060,6 @@ def test_vertical_rules_can_pair_strong_anchor_with_weak_rising_neighbor(self): snr_linear=VERTICAL_RULE_WEAK_ADJACENT_SNR_FLOOR + 0.1, angle_deg=11.0, speed_mph=117.0, - raw_angle_deg=11.0, - geom_bearing_deg=0.0, t_after_impact_s=0.071, phase_coherence=0.99, peak_width_bins=5, @@ -1088,8 +1084,6 @@ def test_vertical_rules_can_pair_primary_anchor_with_early_ops_matched_neighbor( snr_linear=6.0, angle_deg=2.0, speed_mph=117.0, - raw_angle_deg=2.0, - geom_bearing_deg=0.0, t_after_impact_s=0.012, phase_coherence=0.99, peak_width_bins=5, @@ -1101,8 +1095,6 @@ def test_vertical_rules_can_pair_primary_anchor_with_early_ops_matched_neighbor( snr_linear=10.0, angle_deg=7.0, speed_mph=117.0, - raw_angle_deg=7.0, - geom_bearing_deg=0.0, t_after_impact_s=0.038, phase_coherence=0.99, peak_width_bins=5, @@ -1126,8 +1118,6 @@ def test_vertical_rules_prefer_primary_pair_over_early_context_pair(self): snr_linear=6.0, angle_deg=2.0, speed_mph=117.0, - raw_angle_deg=2.0, - geom_bearing_deg=0.0, t_after_impact_s=0.012, phase_coherence=0.99, peak_width_bins=5, @@ -1139,8 +1129,6 @@ def test_vertical_rules_prefer_primary_pair_over_early_context_pair(self): snr_linear=10.0, angle_deg=7.0, speed_mph=117.0, - raw_angle_deg=7.0, - geom_bearing_deg=0.0, t_after_impact_s=0.038, phase_coherence=0.99, peak_width_bins=5, @@ -1152,8 +1140,6 @@ def test_vertical_rules_prefer_primary_pair_over_early_context_pair(self): snr_linear=8.0, angle_deg=11.0, speed_mph=117.0, - raw_angle_deg=11.0, - geom_bearing_deg=0.0, t_after_impact_s=0.071, phase_coherence=0.99, peak_width_bins=5, @@ -1177,8 +1163,6 @@ def test_vertical_rules_do_not_anchor_on_early_frame_alone(self): snr_linear=12.0, angle_deg=2.0, speed_mph=117.0, - raw_angle_deg=2.0, - geom_bearing_deg=0.0, t_after_impact_s=0.012, phase_coherence=0.99, peak_width_bins=5, @@ -1202,8 +1186,6 @@ def test_vertical_rules_require_strong_anchor_for_weak_adjacent_frames(self): snr_linear=VERTICAL_RULE_WEAK_ADJACENT_SNR_FLOOR + 0.1, angle_deg=5.0 + idx, speed_mph=117.0, - raw_angle_deg=5.0 + idx, - geom_bearing_deg=0.0, t_after_impact_s=0.040 + idx * 0.030, phase_coherence=0.99, peak_width_bins=5, @@ -1229,8 +1211,6 @@ def test_vertical_rules_reject_weak_neighbor_when_bearing_is_not_rising(self): snr_linear=10.0, angle_deg=7.0, speed_mph=117.0, - raw_angle_deg=7.0, - geom_bearing_deg=0.0, t_after_impact_s=0.038, phase_coherence=0.99, peak_width_bins=5, @@ -1242,8 +1222,6 @@ def test_vertical_rules_reject_weak_neighbor_when_bearing_is_not_rising(self): snr_linear=VERTICAL_RULE_WEAK_ADJACENT_SNR_FLOOR + 0.1, angle_deg=3.0, speed_mph=117.0, - raw_angle_deg=3.0, - geom_bearing_deg=0.0, t_after_impact_s=0.071, phase_coherence=0.99, peak_width_bins=5, From 39803e5f1f569e7df9535bb356b2b651c9a6b2a2 Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Thu, 11 Jun 2026 10:15:18 -0700 Subject: [PATCH 16/29] [cr] add kld7 dc-alias blind zone flag and sub-frame fringe instrumentation, cleanup old session logs --- .../plans/2026-06-09-kld7-prf-probing.md | 615 +++++++++++++++++ .../replay_kld7_geometry_candidates.py | 630 ++++++++++++++++++ .../compare_ball_speed.png | Bin 92461 -> 0 bytes .../compare_carry.png | Bin 86144 -> 0 bytes .../compare_club_speed.png | Bin 85972 -> 0 bytes .../compare_launch_h.png | Bin 76831 -> 0 bytes .../compare_launch_v.png | Bin 83940 -> 0 bytes .../compare_overview.png | Bin 90482 -> 0 bytes .../compare_spin.png | Bin 77727 -> 0 bytes .../compare_takeaways.png | Bin 210370 -> 0 bytes src/openflight/kld7/radc.py | 136 ++++ src/openflight/kld7/tracker.py | 2 + tests/test_kld7.py | 169 +++++ 13 files changed, 1552 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-09-kld7-prf-probing.md create mode 100644 scripts/analysis/replay_kld7_geometry_candidates.py delete mode 100644 session_logs/comparison_20260506_plots/compare_ball_speed.png delete mode 100644 session_logs/comparison_20260506_plots/compare_carry.png delete mode 100644 session_logs/comparison_20260506_plots/compare_club_speed.png delete mode 100644 session_logs/comparison_20260506_plots/compare_launch_h.png delete mode 100644 session_logs/comparison_20260506_plots/compare_launch_v.png delete mode 100644 session_logs/comparison_20260506_plots/compare_overview.png delete mode 100644 session_logs/comparison_20260506_plots/compare_spin.png delete mode 100644 session_logs/comparison_20260506_plots/compare_takeaways.png diff --git a/docs/superpowers/plans/2026-06-09-kld7-prf-probing.md b/docs/superpowers/plans/2026-06-09-kld7-prf-probing.md new file mode 100644 index 000000000..2b4da5075 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-kld7-prf-probing.md @@ -0,0 +1,615 @@ +# K-LD7 Timing and PRF Probe Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a guarded K-LD7 timing/protocol probe that measures real RADC frame cadence and optionally probes explicitly listed undocumented commands. + +**Architecture:** Add a standalone hardware-test script with a small serial protocol layer, pure summarization helpers, and CLI safety gates. Tests exercise packet construction, split-read handling, frame-gap summaries, and unsafe-mode validation without requiring hardware. + +**Tech Stack:** Python 3.11+, `uv run`, `pytest`, `pyserial`, standard-library `argparse`, `dataclasses`, `json`, `struct`, and `statistics`. + +--- + +## File Structure + +| File | Responsibility | +|------|----------------| +| `scripts/hardware-test/probe_kld7_timing.py` | Standalone K-LD7 protocol probe, CLI, measurement loop, unsafe command probing, JSONL/summary output | +| `tests/test_probe_kld7_timing.py` | Unit tests for protocol helpers, fake serial reads, summarization, CLI safety validation | +| `docs/kld7-troubleshooting.md` | Short operator-facing section explaining when and how to run the timing probe | + +No production tracker code changes are planned. + +## Task 1: Protocol Primitives + +**Files:** +- Create: `scripts/hardware-test/probe_kld7_timing.py` +- Test: `tests/test_probe_kld7_timing.py` + +- [ ] **Step 1: Write failing tests for packet construction and command validation** + +Add tests: + +```python +import importlib.util +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "hardware-test" / "probe_kld7_timing.py" +spec = importlib.util.spec_from_file_location("probe_kld7_timing", SCRIPT) +probe = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(probe) + + +def test_build_packet_uppercases_command_and_packs_length(): + packet = probe.build_packet("gnfd", (0x21).to_bytes(4, "little")) + + assert packet == b"GNFD\x04\x00\x00\x00!\x00\x00\x00" + + +def test_validate_command_rejects_non_four_byte_command(): + assert probe.validate_probe_command("ABC", "") == "command must be exactly 4 ASCII characters" + + +def test_validate_command_rejects_non_uppercase_command(): + assert probe.validate_probe_command("test", "") == "command must be uppercase ASCII" + + +def test_validate_command_rejects_odd_hex_payload(): + assert probe.validate_probe_command("TST1", "abc") == "hex payload must have an even number of characters" +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: + +```bash +uv run pytest tests/test_probe_kld7_timing.py -v +``` + +Expected: FAIL because `scripts/hardware-test/probe_kld7_timing.py` does not exist. + +- [ ] **Step 3: Implement packet helpers** + +Create `scripts/hardware-test/probe_kld7_timing.py` with: + +```python +#!/usr/bin/env python3 +"""Guarded K-LD7 timing and protocol probe.""" + +from __future__ import annotations + +import argparse +import json +import statistics +import struct +import sys +import time +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import BinaryIO, Iterable, Optional + +try: + import serial + from serial.tools.list_ports import comports +except ImportError: # pragma: no cover - exercised by operator environment + serial = None + comports = None + + +DEFAULT_BAUD = 3_000_000 +DEFAULT_START_BAUD = 115_200 +SUPPORTED_BAUD_RATES = [115_200, 460_800, 921_600, 2_000_000, 3_000_000] +FRAME_CODES = { + "RADC": 0x01, + "RFFT": 0x02, + "PDAT": 0x04, + "TDAT": 0x08, + "DDAT": 0x10, + "DONE": 0x20, +} +DOCUMENTED_WRITE_COMMANDS = { + "RBFR", "RSPI", "RRAI", "THOF", "TRFT", "VISU", "MIRA", "MARA", + "MIAN", "MAAN", "MISP", "MASP", "DEDI", "RATH", "ANTH", "SPTH", + "DIG1", "DIG2", "DIG3", "HOLD", "MIDE", "MIDS", +} +DESTRUCTIVE_COMMANDS = {"RFSE"} + + +def build_packet(command: str, payload: bytes = b"") -> bytes: + cmd = command.upper().encode("ascii") + if len(cmd) != 4: + raise ValueError("command must be exactly 4 ASCII characters") + return struct.pack("<4sI", cmd, len(payload)) + payload + + +def validate_probe_command(command: str, hex_payload: str) -> Optional[str]: + try: + raw = command.encode("ascii") + except UnicodeEncodeError: + return "command must be ASCII" + if len(raw) != 4: + return "command must be exactly 4 ASCII characters" + if command != command.upper(): + return "command must be uppercase ASCII" + if len(hex_payload) % 2: + return "hex payload must have an even number of characters" + try: + bytes.fromhex(hex_payload) + except ValueError: + return "hex payload must be valid hexadecimal" + return None +``` + +- [ ] **Step 4: Run tests to verify Task 1 passes** + +Run: + +```bash +uv run pytest tests/test_probe_kld7_timing.py -v +``` + +Expected: PASS for the four packet-helper tests. + +## Task 2: Packet Reading and Summary Logic + +**Files:** +- Modify: `scripts/hardware-test/probe_kld7_timing.py` +- Modify: `tests/test_probe_kld7_timing.py` + +- [ ] **Step 1: Write failing tests for split reads and `DONE` gap summaries** + +Append tests: + +```python +class FakeSerial: + def __init__(self, chunks): + self.chunks = list(chunks) + self.writes = [] + self.timeout = 0.2 + self.baudrate = 115200 + + def read(self, n): + if not self.chunks: + return b"" + chunk = self.chunks.pop(0) + if len(chunk) > n: + self.chunks.insert(0, chunk[n:]) + return chunk[:n] + return chunk + + def write(self, data): + self.writes.append(data) + return len(data) + + def flush(self): + return None + + def reset_input_buffer(self): + return None + + def close(self): + return None + + +def test_read_packet_handles_split_header_and_payload(): + payload = b"\x00\x01\x02\x03" + header = b"DONE" + len(payload).to_bytes(4, "little") + fake = FakeSerial([header[:3], header[3:8], payload[:1], payload[1:]]) + protocol = probe.KLD7Protocol.__new__(probe.KLD7Protocol) + protocol.port = fake + + packet = protocol.read_packet() + + assert packet.code == "DONE" + assert packet.payload == payload + assert packet.payload_bytes == 4 + + +def test_summarize_measurements_counts_done_gaps(): + packets = [ + probe.PacketRecord(code="DONE", payload_bytes=4, complete_monotonic=1.0, done_frame=10), + probe.PacketRecord(code="RADC", payload_bytes=3072, complete_monotonic=1.1, read_duration_ms=10.0), + probe.PacketRecord(code="DONE", payload_bytes=4, complete_monotonic=2.0, done_frame=12), + probe.PacketRecord(code="RADC", payload_bytes=3072, complete_monotonic=2.1, read_duration_ms=20.0), + ] + + summary = probe.summarize_packets(packets, duration_s=2.0) + + assert summary["radc_frames"] == 2 + assert summary["done_frames"] == 2 + assert summary["done_frame_gaps"] == 1 + assert summary["effective_radc_hz"] == 1.0 + assert summary["read_duration_ms_p95"] == 20.0 +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: + +```bash +uv run pytest tests/test_probe_kld7_timing.py -v +``` + +Expected: FAIL because `KLD7Protocol`, `PacketRecord`, and `summarize_packets` are not implemented. + +- [ ] **Step 3: Implement protocol records, exact reads, and summaries** + +Add to the script: + +```python +@dataclass +class PacketRecord: + code: str + payload_bytes: int + command: Optional[str] = None + response_code: Optional[int] = None + send_monotonic: Optional[float] = None + first_byte_monotonic: Optional[float] = None + header_complete_monotonic: Optional[float] = None + complete_monotonic: Optional[float] = None + read_duration_ms: Optional[float] = None + done_frame: Optional[int] = None + error: Optional[str] = None + payload: bytes = field(default=b"", repr=False) + + +class KLD7Protocol: + def __init__(self, port_path: str, baud: int = DEFAULT_BAUD, timeout: float = 0.2): + if serial is None: + raise RuntimeError("pyserial is required for hardware probing") + self.port_path = port_path + self.baud = baud + self.port = serial.Serial( + port=port_path, + baudrate=DEFAULT_START_BAUD, + parity=serial.PARITY_EVEN, + stopbits=1, + timeout=timeout, + ) + + def _read_exact(self, n: int) -> tuple[bytes, Optional[float]]: + buf = b"" + first_byte_at = None + deadline = time.monotonic() + max(float(getattr(self.port, "timeout", 0.2) or 0.2), 0.2) + while len(buf) < n: + chunk = self.port.read(n - len(buf)) + if chunk: + if first_byte_at is None: + first_byte_at = time.monotonic() + buf += chunk + continue + if time.monotonic() >= deadline: + break + time.sleep(0.001) + return buf, first_byte_at + + def read_packet(self) -> PacketRecord: + started = time.monotonic() + header, first_byte_at = self._read_exact(8) + header_complete = time.monotonic() + if len(header) != 8: + return PacketRecord( + code="", + payload_bytes=0, + first_byte_monotonic=first_byte_at, + header_complete_monotonic=header_complete, + complete_monotonic=header_complete, + error=f"short header read: got {len(header)} of 8 bytes", + ) + raw_code, length = struct.unpack("<4sI", header) + code = raw_code.decode("ascii", errors="replace") + payload = b"" + payload_first = None + if length: + payload, payload_first = self._read_exact(length) + complete = time.monotonic() + error = None + if len(payload) != length: + error = f"short payload read: got {len(payload)} of {length} bytes" + done_frame = None + if code == "DONE" and len(payload) == 4: + done_frame = int.from_bytes(payload, "little", signed=False) + return PacketRecord( + code=code, + payload_bytes=length, + first_byte_monotonic=first_byte_at or payload_first, + header_complete_monotonic=header_complete, + complete_monotonic=complete, + read_duration_ms=(complete - (first_byte_at or started)) * 1000.0, + done_frame=done_frame, + error=error, + payload=payload, + ) + + +def _percentile(values: list[float], percentile: float) -> Optional[float]: + if not values: + return None + values = sorted(values) + index = min(len(values) - 1, max(0, round((percentile / 100.0) * (len(values) - 1)))) + return values[index] + + +def summarize_packets(packets: list[PacketRecord], duration_s: float) -> dict: + radc_packets = [p for p in packets if p.code == "RADC" and not p.error] + done_packets = [p for p in packets if p.code == "DONE" and not p.error] + done_frames = [p.done_frame for p in done_packets if p.done_frame is not None] + gaps = 0 + for previous, current in zip(done_frames, done_frames[1:]): + if current > previous + 1: + gaps += current - previous - 1 + read_durations = [p.read_duration_ms for p in radc_packets if p.read_duration_ms is not None] + errors: dict[str, int] = {} + for packet in packets: + if packet.error: + errors[packet.error] = errors.get(packet.error, 0) + 1 + return { + "duration_s": duration_s, + "radc_frames": len(radc_packets), + "done_frames": len(done_packets), + "effective_radc_hz": round(len(radc_packets) / duration_s, 3) if duration_s > 0 else 0.0, + "effective_done_hz": round(len(done_packets) / duration_s, 3) if duration_s > 0 else 0.0, + "done_frame_gaps": gaps, + "read_duration_ms_mean": statistics.mean(read_durations) if read_durations else None, + "read_duration_ms_p50": statistics.median(read_durations) if read_durations else None, + "read_duration_ms_p95": _percentile(read_durations, 95), + "errors": errors, + } +``` + +- [ ] **Step 4: Run tests to verify Task 2 passes** + +Run: + +```bash +uv run pytest tests/test_probe_kld7_timing.py -v +``` + +Expected: PASS for packet construction, split reads, and summary tests. + +## Task 3: CLI Safety Gates and Measurement Loop + +**Files:** +- Modify: `scripts/hardware-test/probe_kld7_timing.py` +- Modify: `tests/test_probe_kld7_timing.py` + +- [ ] **Step 1: Write failing tests for CLI validation** + +Append tests: + +```python +def test_unsafe_probe_requires_output(): + parser = probe.build_parser() + args = parser.parse_args(["--port", "/dev/null", "--unsafe-probe", "--probe-command", "TEST"]) + + assert probe.validate_args(args) == ["--unsafe-probe requires --output so probe activity is auditable"] + + +def test_rfse_requires_factory_reset_flag(): + parser = probe.build_parser() + args = parser.parse_args([ + "--port", "/dev/null", + "--output", "/tmp/probe.jsonl", + "--unsafe-probe", + "--probe-command", "RFSE", + ]) + + assert probe.validate_args(args) == ["RFSE is refused unless --allow-factory-reset is set"] + + +def test_parse_frame_mask_combines_known_flags(): + assert probe.parse_frame_mask("RADC,DONE") == 0x21 +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: + +```bash +uv run pytest tests/test_probe_kld7_timing.py -v +``` + +Expected: FAIL because parser, arg validation, and frame-mask parsing are missing. + +- [ ] **Step 3: Implement parser, validation, and measurement flow** + +Add: + +```python +def parse_frame_mask(value: str) -> int: + mask = 0 + for raw_name in value.split(","): + name = raw_name.strip().upper() + if not name: + continue + if name not in FRAME_CODES: + raise argparse.ArgumentTypeError(f"unknown frame type {name!r}") + mask |= FRAME_CODES[name] + return mask + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Probe K-LD7 RADC timing and guarded commands.") + parser.add_argument("--port") + parser.add_argument("--baud", type=int, default=DEFAULT_BAUD, choices=SUPPORTED_BAUD_RATES) + parser.add_argument("--duration", type=float, default=10.0) + parser.add_argument("--frame-mask", default="RADC,DONE") + parser.add_argument("--rspi-sweep", action="store_true") + parser.add_argument("--rrai", type=int) + parser.add_argument("--rbfr", type=int) + parser.add_argument("--output", type=Path) + parser.add_argument("--unsafe-probe", action="store_true") + parser.add_argument("--probe-command", action="append", default=[]) + parser.add_argument("--allow-factory-reset", action="store_true") + parser.add_argument("--no-restore-params", action="store_true") + return parser + + +def validate_args(args: argparse.Namespace) -> list[str]: + errors = [] + if args.unsafe_probe and not args.output: + errors.append("--unsafe-probe requires --output so probe activity is auditable") + if args.probe_command and not args.unsafe_probe: + errors.append("--probe-command requires --unsafe-probe") + for command_spec in args.probe_command: + command, _, hex_payload = command_spec.partition(":") + error = validate_probe_command(command, hex_payload) + if error: + errors.append(f"{command_spec}: {error}") + if command.upper() in DESTRUCTIVE_COMMANDS and not args.allow_factory_reset: + errors.append("RFSE is refused unless --allow-factory-reset is set") + return errors +``` + +Extend `KLD7Protocol`: + +```python + def send_command(self, command: str, payload: bytes = b"") -> PacketRecord: + sent = time.monotonic() + self.port.reset_input_buffer() + self.port.write(build_packet(command, payload)) + self.port.flush() + packet = self.read_packet() + packet.command = command.upper() + packet.send_monotonic = sent + if packet.code == "RESP" and packet.payload: + packet.response_code = packet.payload[0] + return packet + + def request_frame(self, frame_mask: int) -> list[PacketRecord]: + records = [self.send_command("GNFD", int(frame_mask).to_bytes(4, "little", signed=True))] + expected = bin(frame_mask).count("1") + for _ in range(expected): + record = self.read_packet() + records.append(record) + if record.code == "DONE": + break + return records + + def close(self) -> None: + try: + self.port.write(build_packet("GBYE")) + self.port.flush() + finally: + self.port.close() +``` + +Add `measure()` and `main()`: + +```python +def measure(protocol: KLD7Protocol, frame_mask: int, duration_s: float) -> list[PacketRecord]: + records = [] + deadline = time.monotonic() + duration_s + while time.monotonic() < deadline: + records.extend(protocol.request_frame(frame_mask)) + return records + + +def write_jsonl(path: Path, records: Iterable[PacketRecord], summary: dict) -> None: + with path.open("w", encoding="utf-8") as handle: + for record in records: + data = asdict(record) + data.pop("payload", None) + handle.write(json.dumps({"type": "packet", **data}, sort_keys=True) + "\n") + handle.write(json.dumps({"type": "summary", **summary}, sort_keys=True) + "\n") + + +def main(argv: Optional[list[str]] = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + errors = validate_args(args) + if errors: + for error in errors: + print(f"error: {error}", file=sys.stderr) + return 2 + frame_mask = parse_frame_mask(args.frame_mask) + if not args.port: + print("error: --port is required for the first implementation", file=sys.stderr) + return 2 + protocol = KLD7Protocol(args.port, baud=args.baud) + records = [] + try: + records = measure(protocol, frame_mask, args.duration) + finally: + protocol.close() + summary = summarize_packets(records, args.duration) + print(json.dumps(summary, indent=2, sort_keys=True)) + if args.output: + write_jsonl(args.output, records, summary) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +``` + +- [ ] **Step 4: Run focused tests** + +Run: + +```bash +uv run pytest tests/test_probe_kld7_timing.py -v +``` + +Expected: PASS. + +## Task 4: Docs and Verification + +**Files:** +- Modify: `docs/kld7-troubleshooting.md` +- Test: `tests/test_probe_kld7_timing.py` + +- [ ] **Step 1: Add troubleshooting docs** + +Add a section after "RADC Streaming Issues": + +```markdown +### Measuring Real K-LD7 RADC Cadence + +Use the guarded timing probe when launch-angle extraction is missing frames or when one K-LD7 orientation appears slower than the other: + +```bash +uv run python scripts/hardware-test/probe_kld7_timing.py \ + --port /dev/kld7_vertical \ + --duration 10 \ + --frame-mask RADC,DONE \ + --output /tmp/kld7_vertical_timing.jsonl +``` + +At the production `RSPI=3` setting, expect roughly 34 RADC frames per second with low `done_frame_gaps`. If cadence is much lower or gaps are high, investigate USB scheduling, serial read duration, or requested packet volume before changing launch-angle selection logic. + +Undocumented command probing is available only through `--unsafe-probe` and requires `--output`. Do not use it in production sessions. +``` + +- [ ] **Step 2: Run unit tests** + +Run: + +```bash +uv run pytest tests/test_probe_kld7_timing.py -v +``` + +Expected: PASS. + +- [ ] **Step 3: Run lint/format checks for touched Python files** + +Run: + +```bash +uv run ruff check scripts/hardware-test/probe_kld7_timing.py tests/test_probe_kld7_timing.py +uv run ruff format --check scripts/hardware-test/probe_kld7_timing.py tests/test_probe_kld7_timing.py +``` + +Expected: PASS. + +- [ ] **Step 4: Run a no-hardware CLI validation check** + +Run: + +```bash +uv run python scripts/hardware-test/probe_kld7_timing.py --unsafe-probe --probe-command TEST +``` + +Expected: exit code `2` and stderr containing `--unsafe-probe requires --output`. + diff --git a/scripts/analysis/replay_kld7_geometry_candidates.py b/scripts/analysis/replay_kld7_geometry_candidates.py new file mode 100644 index 000000000..32467a6c9 --- /dev/null +++ b/scripts/analysis/replay_kld7_geometry_candidates.py @@ -0,0 +1,630 @@ +#!/usr/bin/env python3 +"""Replay vertical K-LD7 raw ADC candidates through geometry ranking. + +This analysis mode intentionally treats TrackMan as an evaluation label, not +as an input to candidate selection. It asks: if we enumerate physically +plausible RADC candidates near the OPS ball-speed bin and impact time, which +candidate or candidate pair would a geometry-only ranker choose? +""" + +from __future__ import annotations + +import argparse +import csv +import json +import math +import statistics +import sys +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any + +import numpy as np + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "src")) + +from replay_kld7_trackman import TrackmanTarget, load_buffers, load_targets # noqa: E402 + +from openflight.kld7.geometry import ( # noqa: E402 + fit_launch_angle_geometric, + fit_launch_angle_single_frame_geometric, +) +from openflight.kld7.radc import ( # noqa: E402 + ball_bin_range_from_speed, + circular_bin_distance, + compute_fft_complex, + expected_ball_bin_from_speed, + parse_radc_payload, + per_bin_angle_deg, + spectrum_from_channel_ffts, + to_complex_iq, +) + + +@dataclass(frozen=True) +class GeometryCandidate: + shot_number: int + club: str + frame_index: int + dt_ms: float + bin_index: int + expected_bin: int + bin_error: int + snr: float + snr_db: float + bearing_deg: float + single_launch_deg: float + single_resid_deg: float + band_rank: int + local_peak: bool + local_prominence: float + trackman_angle_deg: float + single_abs_error_deg: float + + +@dataclass(frozen=True) +class GeometryPick: + shot_number: int + club: str + trackman_angle_deg: float + estimator: str + launch_angle_deg: float | None + abs_error_deg: float | None + score: float | None + frame_count: int + frame_indices: str + dt_ms: str + bins: str + bin_errors: str + snrs: str + fit_rmse_deg: float | None + single_resid_deg: float | None + reason: str + + +@dataclass(frozen=True) +class ScoreWeights: + single_resid: float + pair_rmse: float + bin_error: float + time: float + band_rank: float + local_peak_penalty: float + snr_reward: float + short_span_penalty: float + + +def _to_float(value: Any) -> float | None: + if value in (None, ""): + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + +def _parse_window_ms(raw: str) -> tuple[float, float]: + parts = [float(part.strip()) for part in raw.split(",") if part.strip()] + if len(parts) != 2: + raise argparse.ArgumentTypeError("expected START,END") + if parts[0] > parts[1]: + raise argparse.ArgumentTypeError("window start must be <= end") + return parts[0], parts[1] + + +def _band_bins(bands: list[tuple[int, int]]) -> np.ndarray: + parts = [np.arange(lo, hi, dtype=np.int64) for lo, hi in bands if lo < hi] + if not parts: + return np.array([], dtype=np.int64) + return np.concatenate(parts) + + +def _local_peak(spec: np.ndarray, bin_index: int, radius: int = 2) -> tuple[bool, float]: + lo = max(0, bin_index - radius) + hi = min(len(spec), bin_index + radius + 1) + local_max = float(np.max(spec[lo:hi])) if hi > lo else 0.0 + value = float(spec[bin_index]) + if local_max <= 0.0: + return False, 0.0 + return value >= local_max, value / local_max + + +def enumerate_candidates( + target: TrackmanTarget, + frames: list[dict[str, Any]], + *, + shot_timestamp: float, + near_window_ms: tuple[float, float], + speed_tolerance_mph: float, + max_bin_error: int, + min_snr: float, + angle_offset_deg: float, + mount_deg: float, + distance_ft: float, + fft_size: int, +) -> list[GeometryCandidate]: + expected_bin = expected_ball_bin_from_speed(target.ball_speed_mph, fft_size=fft_size) + bands = ball_bin_range_from_speed( + target.ball_speed_mph, + speed_tolerance_mph, + fft_size=fft_size, + ) + band_bins = _band_bins(bands) + if band_bins.size == 0: + return [] + + candidates: list[GeometryCandidate] = [] + for frame_index, frame in enumerate(frames): + timestamp = _to_float(frame.get("timestamp")) + if timestamp is None: + continue + dt_ms = (timestamp - float(shot_timestamp)) * 1000.0 + if dt_ms < near_window_ms[0] or dt_ms > near_window_ms[1]: + continue + + radc_raw = frame.get("radc") + if radc_raw is None: + continue + try: + channels = parse_radc_payload(radc_raw) if isinstance(radc_raw, bytes) else radc_raw + except (KeyError, TypeError, ValueError): + continue + + f1a_iq = to_complex_iq(channels["f1a_i"], channels["f1a_q"]) + f2a_iq = to_complex_iq(channels["f2a_i"], channels["f2a_q"]) + f1b_iq = ( + to_complex_iq(channels["f1b_i"], channels["f1b_q"]) + if "f1b_i" in channels and "f1b_q" in channels + else None + ) + f1a_fft = compute_fft_complex(f1a_iq, fft_size=fft_size) + f2a_fft = compute_fft_complex(f2a_iq, fft_size=fft_size) + f1b_fft = compute_fft_complex(f1b_iq, fft_size=fft_size) if f1b_iq is not None else None + spec = spectrum_from_channel_ffts(f1a_fft, f2a_fft, f1b_fft, source="f1a") + positive = spec[spec > 0] + if positive.size == 0: + continue + noise_floor = float(np.median(positive)) + if noise_floor <= 0.0: + continue + + snrs = spec[band_bins] / noise_floor + order = np.argsort(-snrs) + band_rank_by_bin = {int(band_bins[idx]): rank + 1 for rank, idx in enumerate(order)} + angles = per_bin_angle_deg(f1a_fft, f2a_fft) + + for bin_index_raw, snr_raw in zip(band_bins, snrs, strict=True): + bin_index = int(bin_index_raw) + snr = float(snr_raw) + if snr < min_snr: + continue + bin_error = circular_bin_distance(bin_index, expected_bin, fft_size=fft_size) + if bin_error > max_bin_error: + continue + + bearing = float(angles[bin_index] + angle_offset_deg) + single = fit_launch_angle_single_frame_geometric( + (dt_ms / 1000.0, bearing, snr * snr), + target.ball_speed_mph, + distance_ft, + mount_deg, + ) + if single is None: + continue + launch, resid = single + is_local_peak, local_prominence = _local_peak(spec, bin_index) + candidates.append( + GeometryCandidate( + shot_number=target.shot_number, + club=target.club, + frame_index=frame_index, + dt_ms=dt_ms, + bin_index=bin_index, + expected_bin=expected_bin, + bin_error=bin_error, + snr=snr, + snr_db=10.0 * math.log10(snr) if snr > 0.0 else 0.0, + bearing_deg=bearing, + single_launch_deg=launch, + single_resid_deg=resid, + band_rank=band_rank_by_bin[bin_index], + local_peak=is_local_peak, + local_prominence=local_prominence, + trackman_angle_deg=target.trackman_angle_deg, + single_abs_error_deg=abs(launch - target.trackman_angle_deg), + ) + ) + return candidates + + +def _candidate_pre_score( + candidate: GeometryCandidate, + *, + target_time_ms: float, + weights: ScoreWeights, +) -> float: + time_penalty = abs(candidate.dt_ms - target_time_ms) / 50.0 + bin_penalty = candidate.bin_error / 25.0 + snr_reward = min(math.log2(max(candidate.snr, 1.0)), 5.0) / 5.0 + rank_penalty = min(candidate.band_rank - 1, 50) / 25.0 + local_penalty = 0.0 if candidate.local_peak else weights.local_peak_penalty + return ( + candidate.single_resid_deg * weights.single_resid + + bin_penalty * weights.bin_error + + time_penalty * weights.time + + rank_penalty * weights.band_rank + + local_penalty + - snr_reward * weights.snr_reward + ) + + +def rank_single( + candidates: list[GeometryCandidate], + *, + target_time_ms: float, + weights: ScoreWeights, + launch_min_deg: float | None, + launch_max_deg: float | None, +) -> GeometryPick | None: + filtered = [ + candidate + for candidate in candidates + if (launch_min_deg is None or candidate.single_launch_deg >= launch_min_deg) + and (launch_max_deg is None or candidate.single_launch_deg <= launch_max_deg) + ] + if not filtered: + return None + best = min( + filtered, + key=lambda cand: _candidate_pre_score(cand, target_time_ms=target_time_ms, weights=weights), + ) + score = _candidate_pre_score(best, target_time_ms=target_time_ms, weights=weights) + return GeometryPick( + shot_number=best.shot_number, + club=best.club, + trackman_angle_deg=best.trackman_angle_deg, + estimator="single", + launch_angle_deg=round(best.single_launch_deg, 3), + abs_error_deg=abs(best.single_launch_deg - best.trackman_angle_deg), + score=score, + frame_count=1, + frame_indices=str([best.frame_index]), + dt_ms=json.dumps([round(best.dt_ms, 1)]), + bins=str([best.bin_index]), + bin_errors=str([best.bin_error]), + snrs=json.dumps([round(best.snr, 3)]), + fit_rmse_deg=None, + single_resid_deg=best.single_resid_deg, + reason="ok", + ) + + +def _pair_score( + first: GeometryCandidate, + second: GeometryCandidate, + *, + fit_rmse: float, + target_time_ms: float, + weights: ScoreWeights, +) -> float: + avg_bin = (first.bin_error + second.bin_error) / 2.0 + avg_rank = (min(first.band_rank, 50) + min(second.band_rank, 50)) / 2.0 + min_snr = min(first.snr, second.snr) + local_penalty = (0.0 if first.local_peak else weights.local_peak_penalty) + ( + 0.0 if second.local_peak else weights.local_peak_penalty + ) + time_penalty = ( + abs(first.dt_ms - target_time_ms) + abs(second.dt_ms - target_time_ms) + ) / 100.0 + span_ms = abs(second.dt_ms - first.dt_ms) + span_penalty = weights.short_span_penalty if span_ms < 8.0 else 0.0 + snr_reward = min(math.log2(max(min_snr, 1.0)), 5.0) / 5.0 + return ( + fit_rmse * weights.pair_rmse + + (avg_bin / 25.0) * weights.bin_error + + (avg_rank / 25.0) * weights.band_rank + + time_penalty * weights.time + + span_penalty + + local_penalty + - snr_reward * weights.snr_reward + ) + + +def rank_pair( + candidates: list[GeometryCandidate], + *, + ball_speed_mph: float, + mount_deg: float, + distance_ft: float, + target_time_ms: float, + max_pairs_per_frame: int, + weights: ScoreWeights, + launch_min_deg: float | None, + launch_max_deg: float | None, +) -> tuple[GeometryPick | None, list[dict[str, Any]]]: + by_frame: dict[int, list[GeometryCandidate]] = {} + for candidate in candidates: + by_frame.setdefault(candidate.frame_index, []).append(candidate) + trimmed: list[GeometryCandidate] = [] + for frame_candidates in by_frame.values(): + trimmed.extend( + sorted( + frame_candidates, + key=lambda cand: _candidate_pre_score( + cand, + target_time_ms=target_time_ms, + weights=weights, + ), + )[:max_pairs_per_frame] + ) + + pair_rows: list[dict[str, Any]] = [] + best_pick: GeometryPick | None = None + best_score = math.inf + ordered = sorted(trimmed, key=lambda cand: (cand.dt_ms, cand.frame_index, cand.bin_index)) + for left_idx, first in enumerate(ordered): + for second in ordered[left_idx + 1 :]: + if second.frame_index == first.frame_index: + continue + if second.dt_ms <= first.dt_ms: + continue + fit = fit_launch_angle_geometric( + [ + (first.dt_ms / 1000.0, first.bearing_deg, first.snr * first.snr), + (second.dt_ms / 1000.0, second.bearing_deg, second.snr * second.snr), + ], + ball_speed_mph, + distance_ft, + mount_deg, + ) + if fit is None: + continue + launch, rmse, _ = fit + if launch_min_deg is not None and launch < launch_min_deg: + continue + if launch_max_deg is not None and launch > launch_max_deg: + continue + score = _pair_score( + first, + second, + fit_rmse=rmse, + target_time_ms=target_time_ms, + weights=weights, + ) + row = { + "shot_number": first.shot_number, + "club": first.club, + "trackman_angle_deg": first.trackman_angle_deg, + "launch_angle_deg": launch, + "abs_error_deg": abs(launch - first.trackman_angle_deg), + "score": score, + "fit_rmse_deg": rmse, + "frame_indices": [first.frame_index, second.frame_index], + "dt_ms": [first.dt_ms, second.dt_ms], + "bins": [first.bin_index, second.bin_index], + "bin_errors": [first.bin_error, second.bin_error], + "snrs": [first.snr, second.snr], + "bearings": [first.bearing_deg, second.bearing_deg], + "local_peaks": [first.local_peak, second.local_peak], + "band_ranks": [first.band_rank, second.band_rank], + } + pair_rows.append(row) + if score < best_score: + best_score = score + best_pick = GeometryPick( + shot_number=first.shot_number, + club=first.club, + trackman_angle_deg=first.trackman_angle_deg, + estimator="pair", + launch_angle_deg=round(launch, 3), + abs_error_deg=abs(launch - first.trackman_angle_deg), + score=score, + frame_count=2, + frame_indices=str(row["frame_indices"]), + dt_ms=json.dumps([round(v, 1) for v in row["dt_ms"]]), + bins=str(row["bins"]), + bin_errors=str(row["bin_errors"]), + snrs=json.dumps([round(v, 3) for v in row["snrs"]]), + fit_rmse_deg=rmse, + single_resid_deg=None, + reason="ok", + ) + return best_pick, sorted(pair_rows, key=lambda row: row["score"]) + + +def _empty_pick(target: TrackmanTarget, reason: str) -> GeometryPick: + return GeometryPick( + shot_number=target.shot_number, + club=target.club, + trackman_angle_deg=target.trackman_angle_deg, + estimator="none", + launch_angle_deg=None, + abs_error_deg=None, + score=None, + frame_count=0, + frame_indices="[]", + dt_ms="[]", + bins="[]", + bin_errors="[]", + snrs="[]", + fit_rmse_deg=None, + single_resid_deg=None, + reason=reason, + ) + + +def _write_csv(path: Path, rows: list[dict[str, Any]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + fieldnames = sorted({key for row in rows for key in row}) + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + +def _summary(rows: list[GeometryPick]) -> dict[str, Any]: + detected = [row for row in rows if row.abs_error_deg is not None] + errors = [float(row.abs_error_deg) for row in detected] + by_estimator: dict[str, int] = {} + by_reason: dict[str, int] = {} + for row in rows: + by_estimator[row.estimator] = by_estimator.get(row.estimator, 0) + 1 + by_reason[row.reason] = by_reason.get(row.reason, 0) + 1 + return { + "targets": len(rows), + "detected": len(detected), + "detection_rate": len(detected) / len(rows) if rows else 0.0, + "mae": statistics.fmean(errors) if errors else None, + "median_abs_error": statistics.median(errors) if errors else None, + "p90_abs_error": sorted(errors)[round(0.9 * (len(errors) - 1))] if errors else None, + "max_abs_error": max(errors) if errors else None, + "within_1_deg": sum(error <= 1.0 for error in errors), + "within_2_deg": sum(error <= 2.0 for error in errors), + "within_5_deg": sum(error <= 5.0 for error in errors), + "estimator_counts": dict(sorted(by_estimator.items())), + "reason_counts": dict(sorted(by_reason.items())), + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--openflight", required=True, type=Path) + parser.add_argument("--comparison", required=True, type=Path) + parser.add_argument("--summary-output", required=True, type=Path) + parser.add_argument("--rows-output", required=True, type=Path) + parser.add_argument("--candidates-output", required=True, type=Path) + parser.add_argument("--pairs-output", required=True, type=Path) + parser.add_argument("--detail-shot", type=int, default=None) + parser.add_argument("--near-window-ms", type=_parse_window_ms, default=(0.0, 150.0)) + parser.add_argument("--speed-tolerance", type=float, default=10.0) + parser.add_argument("--max-bin-error", type=int, default=25) + parser.add_argument("--min-snr", type=float, default=2.0) + parser.add_argument("--angle-offset", type=float, default=0.0) + parser.add_argument("--mount-deg", type=float, default=10.0) + parser.add_argument("--distance-ft", type=float, default=5.0) + parser.add_argument("--target-time-ms", type=float, default=55.0) + parser.add_argument("--fft-size", type=int, default=2048) + parser.add_argument("--max-pairs-per-frame", type=int, default=8) + parser.add_argument("--single-resid-weight", type=float, default=3.0) + parser.add_argument("--pair-rmse-weight", type=float, default=6.0) + parser.add_argument("--bin-error-weight", type=float, default=1.0) + parser.add_argument("--time-weight", type=float, default=1.0) + parser.add_argument("--band-rank-weight", type=float, default=1.0) + parser.add_argument("--local-peak-penalty", type=float, default=0.75) + parser.add_argument("--snr-reward-weight", type=float, default=1.0) + parser.add_argument("--short-span-penalty", type=float, default=1.0) + parser.add_argument("--launch-min-deg", type=float, default=None) + parser.add_argument("--launch-max-deg", type=float, default=None) + args = parser.parse_args() + weights = ScoreWeights( + single_resid=args.single_resid_weight, + pair_rmse=args.pair_rmse_weight, + bin_error=args.bin_error_weight, + time=args.time_weight, + band_rank=args.band_rank_weight, + local_peak_penalty=args.local_peak_penalty, + snr_reward=args.snr_reward_weight, + short_span_penalty=args.short_span_penalty, + ) + + targets = [ + target + for target in load_targets(args.comparison, axis="vertical") + if target.orientation == "vertical" + ] + buffers = load_buffers(args.openflight) + shot_timestamps: dict[int, float] = {} + with args.openflight.open("r", encoding="utf-8") as handle: + for line in handle: + entry = json.loads(line) + if entry.get("type") != "kld7_buffer" or entry.get("orientation") != "vertical": + continue + shot_number = int(entry.get("shot_number") or -1) + timestamp = _to_float(entry.get("shot_timestamp")) + if shot_number >= 1 and timestamp is not None: + shot_timestamps[shot_number] = timestamp + + rows: list[GeometryPick] = [] + candidate_rows: list[dict[str, Any]] = [] + pair_rows: list[dict[str, Any]] = [] + for target in targets: + buffer = buffers.get((target.shot_number, "vertical")) + if not buffer: + rows.append(_empty_pick(target, "missing_buffer")) + continue + + shot_timestamp = shot_timestamps.get(target.shot_number) + if shot_timestamp is None: + rows.append(_empty_pick(target, "missing_shot_timestamp")) + continue + + candidates = enumerate_candidates( + target, + buffer, + shot_timestamp=shot_timestamp, + near_window_ms=args.near_window_ms, + speed_tolerance_mph=args.speed_tolerance, + max_bin_error=args.max_bin_error, + min_snr=args.min_snr, + angle_offset_deg=args.angle_offset, + mount_deg=args.mount_deg, + distance_ft=args.distance_ft, + fft_size=args.fft_size, + ) + if args.detail_shot is None or target.shot_number == args.detail_shot: + candidate_rows.extend(asdict(candidate) for candidate in candidates) + + single_pick = rank_single( + candidates, + target_time_ms=args.target_time_ms, + weights=weights, + launch_min_deg=args.launch_min_deg, + launch_max_deg=args.launch_max_deg, + ) + pair_pick, pairs = rank_pair( + candidates, + ball_speed_mph=target.ball_speed_mph, + mount_deg=args.mount_deg, + distance_ft=args.distance_ft, + target_time_ms=args.target_time_ms, + max_pairs_per_frame=args.max_pairs_per_frame, + weights=weights, + launch_min_deg=args.launch_min_deg, + launch_max_deg=args.launch_max_deg, + ) + if args.detail_shot is None or target.shot_number == args.detail_shot: + pair_rows.extend(pairs[:200]) + + pick = pair_pick or single_pick + rows.append(pick if pick is not None else _empty_pick(target, "no_geometry_candidate")) + + _write_csv(args.rows_output, [asdict(row) for row in rows]) + _write_csv(args.candidates_output, candidate_rows) + _write_csv(args.pairs_output, pair_rows) + + payload = { + "params": { + "near_window_ms": list(args.near_window_ms), + "speed_tolerance_mph": args.speed_tolerance, + "max_bin_error": args.max_bin_error, + "min_snr": args.min_snr, + "angle_offset_deg": args.angle_offset, + "mount_deg": args.mount_deg, + "distance_ft": args.distance_ft, + "target_time_ms": args.target_time_ms, + "max_pairs_per_frame": args.max_pairs_per_frame, + "launch_min_deg": args.launch_min_deg, + "launch_max_deg": args.launch_max_deg, + "score_weights": asdict(weights), + }, + "summary": _summary(rows), + } + args.summary_output.parent.mkdir(parents=True, exist_ok=True) + args.summary_output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/session_logs/comparison_20260506_plots/compare_ball_speed.png b/session_logs/comparison_20260506_plots/compare_ball_speed.png deleted file mode 100644 index 48a59853d27f0075a051c1633a97a1e3a97dba09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92461 zcmd?RcR1Vs`!=jft?HuHQlmp{T3c)_N^MHjY&CYMJtAgxnKf(2UPa9yM0HVW@2xe0 zAP9mO&nx}@zRz+0@f^?d*K_}K=Quu}3dwuC#&w<7d7bC`ot}<5BRwlU1qB78#={2& z6clH|DJUpMY0rS)G|rNOz&{XA)hC{YuC|^&&)lID+Rr>+I=gy0J3Qy|hPrz=xVnf5 z-@7X;Ey!i>>G{$FA|e9&zds@D>TV~}YUb4qjzahHp{WN2#U)Sjf2XExIejTkQBY_+ zP&V>SU!9`yH5wZJxtXZLbHP6`+EX<(^zHN0d@K(x!dV1OR(HnXCgtxL^;Q|FEc=^2F#HKV?cb@o^K@HLP!=N6q+bJI1zfwJFW4?w9VZE=l;(t z!M^WD^XdP-qu>po0=N1QHcHA2wf^b`6AS|9JWYHpF{f^G$q*TTG&^Qk&;CZ(&zRxKC_`(Ca&JiDxS zy;g=s{GEo0g=8z2il!~i%Jp0Cd*ZksId9Ci)&=al6Mc48hRR?Qb#*}}fo6@K#YYFY zirqs(&x8@_9CgwpD+4x2v{{AwLdO`!E>}Iq8&~TvP@t4Gq^CdO6tr!5n}=uO+j+&+anrR0 zQsYRu1+HpFM|MrzsXy;sqLA5etzBC*`&ugvr>AMcSRW3(3*tumtr*XkvYx2u%|{~5 zgSI`H8MvdVW;kkReQIJYEfYRapuHZLxVw$0VE#OURXO#vnP+?vjmLEQUT2>>!1Q5N z*QOf1oQTs@Irx2Dh5hf)n36%up_0c_k_U&DFI%bWd^i6FPweC4DccdGU9Yz4aosgJ z$HAhTM~dXY5&i|N{n89MOFel2jQbaS2%bqqM zN*|Xd@jxKsQzzgfw&MlQzP(;QR_8O=&GoMVwZAbcEw@i;x?R(Ubw|Ew2x~N zb^?Py7pli_T*EC(aC!~uUn5;$VZr)=EqZz#Hy5M0{_g#Icbvn~?%D)=WvDcH^T}zN zbK7|Z1-4`BLnc;TqdIG4b>$Z5IuZ#)f*xOaG27qmRb@V=pNTRb6%YsYYdT;dn3tV^ zIeXC*%^7r9IXW=`Pmj7T(_dRXjt5h-UE?w}&cw!sOXB9{_A@RwzvgLn^joO5xvtNN zao0CwI*g9#PW=dSVWBhG2M^0ti<+FV%k;DTiS_*b@m@;Zz)Yi6r%1#6B_jOjV8;Tz z;UjnVDG`)Eey}vpZ@t1&NX0eG;Nj>h}j`SNl$} zwmB&Su{)LtZl!T1P2Az{INBgz^BQi-cP`4sbgT+GgcS zNH`69;?YFXQ<$vJx_J+ACJ2ua@oX74#iM-85G?zooe?XsXtrjagY;#>UxaycU5^4u zu+tku%9HHfeOzkn1v6{*--(1ent`?TK=@y#;}+-5v+f1w5)=N8`?`!lkgA+db|+lX z5pR!~FW3)%GVP7{nSG9)ydz;A(|%Y#kSh`$#umHMYP*HJxy)jv+utNJ!8#WZ}4T5bi9I4 zT&o$F6r~Q?mNv2QdR&0V%i@~`giKxzpU98ayX9g*0_)h`XGUO`n)N66M(0jWLhuTF zv!y-xt|a-xjc+a~lusa%V0JI0{3m_HBK1C}3D?ZHcM4^K6t=iuS*V+}&T*fGmqC_y zzPY}|zAwkzb&k!Q^n3z2E>n$}g_-^x@n(%rRuNh! zTJfvA0yp*4l=M+3yjwxUV2JR8Ado*?{1W*eR|yha(x$o;Hxjduu~PLpHc#5#Dngg` zZLO*3>6Z%__Kzi2*Dez>10^Q{c4f1ze#C&qEDT3 zZ85Yr;701B#p)gfhQ>9p;8!c#I6Mf|_LdRrQ)u+W8fqr<9$xR6)9B}H(*86Y50&N7 z~`1tsC$!jmwP94PR>ryCPt}vSO-ij^92e?QQ*kIUk&6xd)pd*1< z?^LM=6;NzVv`mEH#+p)Nc7KMqCJ+NQUeg-b}f6jCY&G;Eo*6QA!_VQU9 zfBb1}4L4}0>*eun4>w$vMryEWI|t%6{0q!!u;{~b^&j}abs=gAuN5PrAz||S&!4uY z^)7MF#Kplr^JnhZ44*}(mOHwctC;&< zf{qHsJej^X+hj7urQ%!2smL#&DNAatqN2WD+Gz%ZsA~&XcqBFLo4bP>>G9uPP1$T- z&tT$1<9A9cFp$}!=^nG97*#MW%o(5LuaHC9ZEgN?jnN~%tDeXESoA^;LmnnG=-3av zznC^PKi}#+QP)MEw)+Qsy^5URrUMAtJ{L_`xV&zdFg4i~YNtt_!pFRO=QiOoS>Kl= zY?11ABGPjBH_%bkTf2bcZKy+)ZC9$`1iSRhFBaxq#mzNkmSLd+<1~F#n;|v!eP5}W z4Gay1jQEW|ecVI~2V;|bEU5Y4S2rUhT&Lw`k3azb%}iDLwAL}n3uHG>Hh|zSCxPsQ z&SHmn($<@^5O}f>Z4iTvw=6tZdJf`64J%f`$Ow-Rj0hNtXAD1Dh zc95^gRm2oH!a7lQz5bSAukskr2?%HwN)K;+1G`y0*)7}4h+2mwoI4@py|q7a9X1m{ zh{_1q`c((*yuv1C8_A|z5lLY|alAJd6Xt?ryV~BRs@1HfpA;+{NYm(79l%VnHKU`P zX+5#Lh$sg^)Xs0+npDE@tMMwkCWsX7sHVtOlf^cW8`c6VOe!oZ>rfS23!OAgGg4uH zUoL#j!8Y^a*a&hv+z!Wkt2m)!mB>4==0ZU2@tyL+9~yJ;;30 zb3l7t!lcx=%M!jOK^zdqiZS{qrVTKZASju`7zAD9Y@MDj*dxq7tWTwzY)O`%b~7! zr=b{@h35R8X#gl98S=-b4pFjT{5)ziI^OLof_OFbWB#kXYvRMsa>2BgeFD~w%Cl+W ziV0ku9*^r3hTk+76WI1MnH?pPxMXC;6!vDrBDHe#0*xa=H6{3;%S{TW^kwN@M44qXxIgGzJPdopHGw}0YJpdtn^mi0b_G5&e*0fu^NMf-HxHo(7 zC`pix)qXQ+cZKcB5*Ya9lg z0h*F2KLIcpnh?`}`0&Tr^aqvvKLMTd`-Z#m9WCKcbv%aVlcY8X-Lm+4q8{Nduf01+ z=Tt{7?RdLdUf-4x=K0+o_qdrrl908GDs^qs%!hrymIT$@FIZ@@#UjUjQ%(A+E8NuI znks}QF(UwiU*Pyp9;NhH{Qax`?P^BNG+fyPB!Nd_$yNrcEM}>sdD$7F$j%+aV^$eJbmSg`+%QuU)AFN?3j6YwE z_h#R>2v$=T>=H_}_}M|mM2jjHyL1;68xZ;^pFC-R3PeR=l+?RM@4IB}40!z%Tc$z}XTs&$GWgwiTfk;xFvdNOooIP6Ec&P4wD z)y$1-$~{1+*Ek8Ei`uiQw$=WyR=6m<4`6DQ`Rs6$T4VbP`#$GeO7@Pon?iIuAV`b> zFp3W}q2qQfh)#F=dCj|nuW%F;17%>>O8Tr%1{1>Mz%mx5_HNQgUae~k+?kAh8XX_=4*yca4*(ML~OdA-}-(9}K#+C+C(oA9V?{4a~;eV3b z|AgU+;L8{>r{Pap(D6YRh#E6hY=j7A6lB$OWQ?iN8arQk;|hN=W1Z)+0dX90+Lphv zK*q8uzf+|c1j2P}lOKW77BjzabrUlK0IHVSGRiuCj(*Ejh<^I)wXT%KDb7AS9l~-^ zQ>sE|$kJ6KYxVpQ@tM>hKAA3blHrwvP@9t@e9#nDnGmx)I?`mSs@(Pr0OBdUf8fkf zmW)RM!=+@9wjb`2IhsH3$V`$FU3Qgf<1D_pcsuzH>&ZyV@q+MQl;aTp0JTBG;Z~=` zm3PAC-plH+xxTN|%UsWZI$Amm!&7^{qX-s7^TLlDoU*6Sw)g`416 zy09{exKJ%G+%V&~!gvg*E*6|H@shCddZoy5J?=>z=|5D&!^W2tNBy{=^t<%TQvrl7 znKQQUtD0{qgOgb##fA!WROu^e!AeIwSBeW8kxD3+smaFa{YDk#m{M=^X8+nWMfEo1 zwAs{F2OEV!N^3#_vn(jV#6oql55YnQlnei@FMa@JH})h>pJ%r_uds)lkZQII;k7TG zvr=vX_3Sh(mn%YHE;UG_4dsK$Phau~>=Z)PxA z@xd!2<{sJ~R%yUIePtrkf5DejOyyal)|>sXQhh~!iFZhV1FABo6|%xK2?$9A?7c*3 zdAJqL$5LvrUaXM0_x`WIFYwXkGLWW6=DQNwbwEV9-RoqK@;UJ6prLN4Ng}mJEd@&d*01`__S5^SSu@~7 zAdbyq=Z@=)4vjLIain!=;$-;XID4D zZ>W<+jj&`Bx1U{P>G#Q1D5am}|D0ep{sz6?*olw=giTwsRk(Jm3gX<3d@fhb;-yCq zAHF1IpRjpOg5gGPzu(#$jsibX+bo#pgN;wD)y? z@@L*tr~G4T_1+q}Xr0Q=%e!sz^WwihF#LzLb)>Jso;s~u@ZtGQS~z#Ab{4{zc4^&mh=y=j&moXrTk4!0gENTg?*)uTL0_NKk@P4rG zZ!hK-Y?6q0SM5~s%KLB5T_%zbI6Oxyg&z1QZ&5j#aSNyiteER*kuHw;r^xB9eJnE-<=0#@83s<%|Q%;0#+-QxK z{!5Dn+%s&sKmSK_1O_)kDy4@DLUDy2r>l}W*Goh#trUJ+;lU# z8PuLBkUUE=r7jO*t@qz|JWl~TCZwP+P0kvp&z${yP0W@mAQLd=GoZ@igoFP@%sxIv zp(iaJr0(r`N}=~&4xtnjN@IZMgMp;XClTel^@|5RQbG7pQ&S@cE2T)Od;H~7%^n~Y zodM6~4bZcxr+Bh3!}v-2yJ(G_uxQ0&-%^A8_oQ@5m&s8wJ4^zcV%4XX=RfGt02-6C z;rhPQNI49|c+3&N8dsJFuaLPV&@$+*4Lfsp7epISFQEhErvWv$N=BgYCi=?&@Mii#A-Ly`D>oDAXD7*OGSK*IAhR;eTqaTPay$$E)pPGCcb=$OQ8!nf7QVPJ8rJ za4qVqvqL}luQIYj1`$%Vpd2R0Nit9DbWk`tu{$Ui!AN`^?Wh&aJu0OU3nx ziuK47#O(1R{oK_tyR0t5RO#T2f?z#I#}c-Sm(LB3>8lsG&R|m1ZYuy@Y`DS0ZiG%R z*dDZ_u5~g`HUP*|?HS-%dHu#ya1WtnMAEDB$!EMFWksl0 z9quZI>i>LCsR9^;D$-O|kYp!F5G#}_VAs|F7F%>Kkn!*1%XEx;qpIkLF%OkIVhP1Ab_o^7&-}wFz#OGu) z@Z^XXUZ>b`y?(hMZL<9XD5KHQ(Sal3uqaA~gvE-ZzV-`I2?KQBiwiSX)JCat;!j7V z4`_rLL?o&rnG~phQYKc!`4JY!K!e7C+(%kNio0j@mRYqZ*sqypXgOMKUmr((b*>KW zpA_&vVx=4U{OrETQ@vX`H8YK$IevqJZnb_c+5;ayY~om}hNCJGSQB!~hOWHgeT-3y zVqMScKovA_7?QH}A>-$}7;~BM0J5H$q=`B+NP8=P#_0&8e4ohfEJei$))hR4RQ`x& zgj6Az%H~qv3V2Dtt~-p-p{8Z?i2{$Ua)s~jA40AT(-mY|!3r-Lvi_N$v0Mpw>-rRZ z(qs`*WFu$$`>M~UmAr{8JUoxde28g8qe1hQx!aG+>jX~61L5V73acb02SoYc#S13~ zyW>4dxBeKNwpc^jNa+=yin=?1(>H~H!ilweMkc7|ov?9FbgBiE@Juw z+;T;>ryD@0spC?u#DkxWO8B+HC6#8;gu$R*_Z>RQuIgM&j_Boalq-H*X{{hGh)kHR zjaJ_3Jizo+Y(XJ3ySri%qx=>5v(4DHl^7_N(4**cMB8lQuf1}An!@bQ@S-J=WyX>^+fFm2b$qEk@F5>cQu%IdR3o_3X7nhx;e)U}{UKRG) zck}s(?(n_8kCd6%ty&JZB0BH#^CHwPA3{LIX^|!VM59*Ee>p#SvWJE<;O;H1+Zhlq zcUT@?U?<+=zOp2HOotKSJgtVavvd2d6~gQBpI$6yyO3x&#Vh%YkDZ`P8owNvVa{J> z?$87o8+?a6zBTwTh?-t}ODiNsuiyTB25*URIYhKE_*2A$W^$&qTaN6-A&vZ?FKRrL z^wve*bt`!O2_=a*aA}Wx`qyRY)nkgkgi&gP%GiVF7No)WX95TAAF00Q+O+vvR%VDg zC}R|N6Ro3Q#PkHV0h&+sqE9R#%7d7t7jC^s<@G%8z8uwcDg%}u-#!@BdG!~^2iBq? z(rl8>FDrCUu|{DCJ$1djoUWt{uVGV{yaY=)sA|k-c(!ev1FglNHeKwa)y6P>z%THLQAZt%Wkg5f1=nqfVJlD(Hk6qE3!2eg9DV zoGoQ4A*!~$TVchyF>Cc|WJUWmEF!j4v&`2|Ba)vYI`3o zL4E(Ruv!Km`w1rB#$Bw$;a42@y8NKiuWrzN|EqBRz4HeVOFJAC?w- zLtL^)MkmT+=T{W%6=Dy2Wp;i>v01zhp`>d>Rs?1oyyQQf#I!^60?=XCSdPLnJa7$U zLim7>-l!-G>ET6ve~|NWvB-{m;_@cow;M4D#G5U6ByVlZ0iD0X4%=lReH)4Dm7m$?}NhEXGFc9lsj`e5wlf@shwtF?C76GTMuRpV~U#6#~&b=p*y&v zA)K)lgomGv+?Mr`SH{;;%8pwx)*H&y(Qx>3*;wqDF<%`89bTLw-$&1lS!F5RqG)-> zuLHB_zeyts5tJ%9mtG!dEr0AaxAGTiltemFV>|`nG0jTX_@B1WjMz7Mn&X;>H;b*ET*i$_pN`8F!XTEHkwdlD zEDAf;eft_A!wDLBK<5#T_c{~QsK!lCUp#Yu{yh!F*I-PvCmQr6L|tyFM{j%Lr>B@M zUtW-zixiVX`{284y1FxD<<07xe{DSh$ZK=3++tz?pqt>Hfoi)RE#h*6=Sk!>G55(H z&>H+*1_#K=&NL}KJ>7wG$9H;U=PQHAi>E$cU(u}}0|kM9e}8|~YNy1A`J9Hfc2~Vs zi(Y1UFx%UnQK|@2tWxCF}H8u_O#6W;4BnIY|_m-#7sEKs|9?$(En0$KDR}C8R zS`+CW-7+p?0NG|O2O=`JJRqC>k3<^QG691f+$qwup@M1xMRO(~JD~m+Ab`K@1UM?$ zAq}K-@d2NQIw;UyzGFfv!2-Y+1@8v+=b~d;qJT+borpKQvEoo{d>Qz>izIt9c!*n~M}KIrGC9E6Nz zn88Vjj%i9wh>Nujp94b=9?iAI^wd}&kMoIg!zhuJ9l*df>~JEs`;a{q3PlwYF6b^H z07u&78Y%&AhQb`2j5Qli-O)~E{;UWGb-@|AD~B^^rM85F?QzSt5Z99fFl9b5+1L#f zfX`Y8{hiB%exANA6lbzk!*b9bnYNz7`2%<>r=g*74OW^~{w)%;hzBZ>xbshq0juZ( zYn=%`b$W6O^wiKw6$^lA@s1e)!>9#=&we?jn=EWGSp_;G(gTwIL4@&jOxt&|j)lYI z2&1&!j9dT!q1)}`JUe7QI-6C((U&9W(2a<8ok$X~nkImDh}~E>{L{hApdzP)qfsw* z!0Vzb{8WRg9<9<7+HKA(VnM_oa6Io!4m=nOuZaV13YBZj^vvQLSb0C<`NO@e4Y=-P zZ8YR&qaJ=nW-HTt<-2ox>0Z#<9gx%%_;w9E68Y@bqPeag+Oc!G{Ss|vYPq|by` z5kMQR0g)-;Wc(B1+Pss5#)D2-cNGQ_K!25*fqt24L`v`PMAFmp_#aAx4dm)3>C23^ z)D-heglpjsDXv-hD|k>62;ycy`^9ApjB^%rY_!s5O7!UL55kNeHbXh=ImrAr4?|TB z&N`s2vtW3-O@K;Mp<`^WO&9Nuvaa5SjB00WPWCztA0Q64e+?Z#0)c=@8WjaO)mn7F z%!`@pGzC}x%j52SxOV`JIdvnzm_r0mS-cn%7D0&ZgUOgYfS3rk1N3sIQCGP@iN*Z# zWQUX9LzZQUqDNs{0gN1Sqw>QEe1@P)3jwgFN{~&;CEct*>7(M#{n}N~0__^Y>0eL^ z8}Z*+xxBFtD$+^8i9^7*xTIKmxM-zGc9jL~FC^2LuOoy{zQhCt8N;#+^?0vRv(;e6 zyA^rG{kNAWIY}Wd{ub09CXU}IGUn}^+1}>h<1IX{3WYaDUtFpOVbAmn#TaTP5XYh3 zsUx$HK-tW}_%A!_(9 zqxjnK=Xilk{CaQ~81XAXo3_z+(zf&INSW|;8Iw9E!eUqX?NpH=$6cWeV?E7mlLkud z3nl&Ln(QVeE^p4Uhw5A^8k|!q9@6v}2@uqLH$3Oa!Irph&}=*OnehQPO@Naq9DAyN zjwu%OgU{*}B>o9=9^6 zxHD+zvej)3)d*N76vQzdxEPFG>{7fE*>T~$=D{h73&qkw*h6_){>^#Wu+4QSv{&_;Lu^5v!LW($9spi@TO zX89wYStk&FuL4mRzs6<$?4|p@k5ZjD?j+6`j{z1KLy){&Q+B^ky}ekm)JCvy>p8#t z$*4_CgMrnTSo7MEokfkMU&+BZ2OoLD6;q%8Cb_La3bBI zd-5%dh3+;xRr-bH+j8o=BqWU=7uXg-?d;aloTJ za&k58*mMvUNxW}Fr=^{RTsv(6H<&RXSe^bCq(=K+eMnuh-yO~c) zx+@nq`X;ZZ+Nnvrsg2U%@Vt~C?>&^m$UWpXbe$_zQB@+%ONF;2M(+7{S~RX4Evo&U zH}$vpg&~bA_}hikOx6~kMkXa%SEtZYb3S;<6)tESI*QofK3+Jf$7YAh^7~+@_YhVd z&#gUJ!eo?@fNSoBm&NN0mG2@dJp40S*L%tt2Z#NS(=DsVpDN$L!N`y{d zoQfSvi(Pu=s0&YU!XT}CvY96I^nwi!!{9$EMOq_4H*~1j#7;Lu)%60;)wP&AO2%nU z90FZC2~+dg)6s<}AqtAOp6M+##;iRvVSNps>V~N87+()O*lI*(Ozp=m{^<=6PHj;= z`bKV$XCyDD>vf7hy!)g@1ci;;cfcyaibo0<_WYf@*z%6wTV{!L@#M4(9P*~-a*N=f zd8_ojxj=noaAcU;q06r1h;S|yzB^{eFgcvPbZfpYhCPxp0+Pi1$zWEQpj{m0S!toH z)%9nd1i5T|u-V3G!Sxb=jdj-WrSVsxcN@QO@`orC(uV>G%tGzS#Z`I3FRhBb$kw>j z9(83&#@pN7ouz>d2Uynjl2d7o>p3>6r_NEcdL7!YPlf9BJ_u1haT1ue(L91N1~YQw z?QV}&&NVCcx^b*lJ|I}`4~j97wHTq`1OjF%0gX34wAM7KCpm}-7EQ{!qN{H zS)23IqKIA1Uh^gra zTT?j~T|$b!$-2)!bOQK@fp%fAQs3_BwWyU?MmEXcfLiN9j)hrHk68BVFo8DNYXWgr zvvVJ=BktEoDGNavKR%78#wsPap(tf2}G!!+`CF0 zoe+Leco1c*R0Lp%OBLKzPkoT3U#aWm|IBF>U?yySM*h0wOqMHJ=(T5+%kZYDou&r4 zhlR9KTCp0=V)-(xo%w>vnK!(8#h3?{0MzzEC+8PDQnNtTv9Qy(zWD_CEv<4POBHAz z)f=3pf~F&{QoAvz^prZ%)BoN#+3dv`ho3t^7~d7Cczi2Bj3nOvWUVkbd)j8kW(L)- z22z1Xz$8$sHsFjqko1Fft>Y51WM)SB#)+T?-p;TsBh**Ph0N++Y+7E#IN*Q??elP% znH?RiLq>xK_R4_*pDz+SqRr#RZ&(=7f2@3CEn@eVV)%I4rE(^I*N!@!tKutaJdhBz zAA7j*+R~%#0bMechnqmTM&ksI%qXdUi+2?9jtePnTb2%7wSVv&Ir0}~*vxUl>>Snj z`_b$dR2k^zdn9+16Xm3E5z}dZX*-yQ2yj^rNZ?jKodZxSkQxAa9J>~w5EKG=*=qmY z3WiVPvkLfQfMg2CU(79zY)bLyT7Zu)#=nWMAyc&S=p2Ff~`cy}N zH2cN!95s%ieYQ2MT99E#NomG=7q7@0oIG+IruofJeUGNYu@7FzFv{xe3$!nGX;XjT z7>m&+SlMydiK-j*`7QU0vWkN}{kX2;nAibq>jbA@q>biUj(b2OnNllHPdT`OQO<6% zo#(szAz48=ki>b+d}#P?m8=Em6s3OA6mr3J_O@)}>+Imva+{)@N6Nerj}WemUv@+) zi%8oUf2=O)gYVa&rK)v}Le%hum^3dOn6Cn_%eODjxVy^KmQAzN!T>@Q6(ZF6xP(a` zoa?t!nPCd4GX=#GgTX+>154&l?YAhHPi0s;y!faiyq#5(d`uYDc<)1%w4|sKRib}J z_nDp&S8vg6Jc9M&UtJPlxb>>V!qi5oA*O?LQOV=FxgiOQYgrb#2Br-ex%*tMiz;pfWJc`f?Nou0P1I&W10kvVwr_oFvj6&5iS z&bI+~aH)Idq_8Hl1^a+h9?=d-{=ID84(}Td%DmjgmRoWv8{#v(bOhRnZP3m=gA>t0 z_ZOH1cH|`+e>tcd@4&B<0uQyWpxjSX&~)aX``&?6PELs{{Q;2hF4>^?1H>3LlKHBh zq%pE5On4D1o~8GBq!^Pw%^5^Le+F`f(t>8An=c{R?DKx8%FxNinakUV`A5q4x@7au z@}3yjuCO@B))u+k>-w$PW`*s(3jY{vLeVwcMHJ z6t83pZ__U_;alzmPEQC~8j0NzdRDX_F{sutym|h1iS|(4aoZaO5q~Rj&RWs5rtSJv zwufXbf|jYvF#+yJQ@gU)>rF3CO)m;34IYOGa>;`3BCP<1hwk9K6+18fZ6ql0qz8h6 zR~|Y<--x2@5Bj}g?%=Kp%T28>>AUXO!CDU#ZJ>yD5r#ckUOR{}<9PwT*?CxOTXTnI z5T|EKmBCvqm#&uMi_?M^-_8uZn%bmKs?g^xVK0q;Nu9iXz~R4iL#pka6#8LIcAqor z<9uv?U~yQ!P8cKs;4v#Vx2DPby#Z&zyl9?D;WH9;3u zm)dB+av(ofUhR+Q5DSTV6D79_K75zazGKFm0L_b|p*QBU-vK4HooQQ8oC$)4Z>eB5 zTA`Hx*0OE}pR3w~r-=rYC{cV-K)HgL7*>&wfdmAQqWsg}97=c@~NUU{IY-3T4>TJyuzB%1TSkEQAv-C0Kxb}sAK~*7lzE1cwI0#$P3E)6t%6K|uppfWK#sJL?!Qv(V&yr2 zao{BMUr~ki=V@dLTQt@mm6<%j8U`(d@Zpht!U5`Ut&<6t}RPq1x> zUHScrj%o5h1he-%3F^-wG(cEf*tznGxMSx-2!b8R7Q!gk3dglN13|Ml76W`>@9DAl zlW+TIo~4G|6K!k{s7MHB*&*WALA;~;OThRg2b#tNqYBHZoug<_EcE~`f_3}4L<=@< z#iwLuVJ4`3eKR0GKi_XyF9{af?6cBB1db@Mqij|XCjb-F5;Byw&Bt+&Abec|Bljl*y6hO;~`CQicb-I4oLZR*G^b zNk~ks0^a}VX%KOJ>J(=XSt71TbN1?mP?MPc+nOiy3a_Xr+J&G>ya=h@S4F%EtbcO# zCHcGlWGTet1c?Cqi4(90mLIA+{Q9W>i-tWc^}JA_JCHpgmtG#9d-X=3qNyKhRWw;0 z#3%aTiiNlF&ZWo7`?F!^-@|=0)5!wJ#C5~OtI(=b4L^WHKKPPGKK~)MvyZNLk=bBO zIFVIp)8IV`mi1H@gQ1jifFX7?k4c=gvvF-TE3f9pC?AZ{yF8W(IIjr6*(sH2M^+ z)0nd9P18RNHn{9qi!DR8&;l@IXjQp~ue+ zJZixI_{pv(GwI{UCzEJH{$3Wco%Ck4$OFA13g!!}(l4!*Imo|KdP}aW&YZof1iy0X zfP5iJFLuLe=`RP1zG`Beu5mCF5Cjf`I;RogQV?=}{P=NsoRDy)hI{R*i}W3b&X?cr zX_&oci`Ezy{RnU@`U}~M^2u?i#2XYvcIjES+z(U&e%_Cf6uMp`0Ar<-)^Qkk_{B{# zO)WHmwvmIZ#7M4>OU7JK&ZgW*iZtXY0_3~#N18uii;_>gdj9K6b+i8HKd)`#>ds1C zkFSB&|M{3Fbb1W5SF}?^e*m#FjBI-2W@2W}$@7@s3Ezu+!vT9LZErFze(7a;f5C^~ zxdh25D!z-p@(Ln|8=?P3`(|j{TxC%aZ>dpn9K)HRGP9AWX)wzgZg^HO1{4ooE&ZJ) zd*>+HwHxZm#z=D8{McovB$0vk-|yXHw7~kNpm>(Sz76_&qv--7G77=h1 z`EOh|FEaB#NR4y^TwYRY>eJci&v$wsuX!eRD(#`5T3V50)(?yow6E{>JUHrn8&K9b zZ_WSO1;Pt65DcfNfHJ~3{~q~_Tv=dNE>Uxki?`+FW$MfgwG;~wDr2@imLpav|fryxX>0sY*MW?AyF z^ls{nlw1%~6_2Y#p1Sei4t2T_L|9UC7|H;?#AOF$bCN;F0a9PoxP3wRe4zACGpF)a zD_bQ;@Wx^s-xTvz1GPBOcyOp#kAkD`@0eh~i)fs$c^PQq_)NFR=eY13kQ3m=%m$?v zvTPbH0Oq4WJm|ehIP|~r`RAIZKcx(Anu6kovAbz25|m?IAnm>YIwNagHS+IyAN=NH zE)dsrH*rjbDI_?So+}uw6OUQb(e@#}i2y)(l+5uy_ge?8Ag77CvLm3*`LT5fv_Z8X zQv*lZ#QTMPa7UVyUCn!B&%|$JgmB~A;D2UrX3ukQu{3AFU2pkyR$ngQXXD8<@j0benthA-@1!-T;6QZw11?F;h}ESp zGi-Nz1(w9|f_>^_a0OOpk8f2G;x5!mC!1t}qnyc;9r_EjCgtbwz@6SoRspd=Jd1FX zR-m`Feq3qO_B2frHu+HR=(?1P`AnuW0r&?~KhO?3XCiGfA923UX0 zfK7#dTYAj2@_9HM#;?x<=mO6%BQi7b6H@lHwhHXDaiAvI+PMxcGymP0{wClBTMsA! zntWqS!2aK9Zu0f>dePE{f0%Mu-rX&*P(H0<`SiH^r-jSW*A0O8MnMDM8QF4|iF&;F zo0`RZc5&Pf4CWe8oq4xrgNAFj_v+{~&@}x-w(?0n`}#_2q8W&YB!PJf$j2fCx+Fm* zi0r8Z5-GCPoN9qYW zwgECZ?-rm$`{9Dgw61fVaRCeaT)iN=tzoThVs+Te)r&y=$5NAuZci(KlSBhe z_&xx`aD6Hvr07MOkNfi=Kfm-<-ZTdw7bHJV;E{%g+n5HhfaWpS?)U%C)jS~w-Y@^_ z_7(%%JI`FJ=Es72^i+KjuK*kS0ZKPTp6nT|gPwFV&@1J30 z6>ldhfC=v-J4wm5_{=)^-k =J)w87iLX!B@+w2BqxU|0Yh6RgEgPw!)q!X&IVt8 zkB~LmRqwzkY=GrSh-n4{tINwM;-NO-QO+z7C0XpE&)nxDl{`uomIO^Wa~RrX3S!cz zi~Z)W2vYfMcPWTe6bT$u;a?xBcqVx1{+@b;mcP{5uybGIngr0Fl~@41yJ@KGzSkHu zncUiYCT7>|37rtP>H}(t^~Kd+A{It^@9Kd@bKU_BY+B#IfX&^J8P;`;?Svc7c9sIq zHt^r$$;!RS8E_q5r*6ojlGi6dt1AQ4Ec=0gai`#ZxA;6AbTJVdtu(@?EwGxjLQr#u z^=?pkjdV>ESGDuceDdyz?)HK2tUv*famW4us39v&5vU9@T@>`dAt>@sdw>n6d9Tmg zrL05JsZF7?{BHVJiEoaNy>V~iLOE70ICf|0=}-A4J~5PmB*q!Oj)hbJv1A9)hTqx? zY>tVgzTD~;;^7@+JNY8l3XrD)VbeEI(2$=h@eiha%B4tM6e+Drvl~=kwhH}jU z%GzIyCB>%I1)VB?XsZp+9Wrbj7 zl+RDO<3L>Zk`jTQrtZAbFBE$#TKcP8=5HpxjF(^OOxGF2u|2>_y9lWzx2TD8RJrc% zKp)}`Y$j4bpLk!Y5h&4-HHK=j9Y6Tk@_{bcyI+IH)LGCXN{<9ovgWq2uEPkR$DBk8 zLr?54By~v9Oc=Lvxhg#*+y18opx%Hf1{bSI(JaU2-F*7(EA*I3{3rvI#S@vzxhqdR~L)=kk@DYST8#1FNzn0Kav8snD1pBNz8f4Dh19!h@TCyG;67Zg za_x~0P#f2H3NW!lp*gu_2F{-CVz0=bYl8T0AO+OP$VSQ<+eGM|ldFw|jec(JDqHS> zXDzzXzaWDyph=ml1D-M3`XivRfPn{-q^z5P3_vQjj_Ximd0}ao$T4{Dfw!rBZ$WsN zcfR(mQ*C_zg{iTu%Ge?ZXHV%yplbd{O2xzzMYf0By+F<)c<1%M%>RA{$dX_{=puX- z*}wuGTXA2hi1s@0b&}n$lNFcTrx#l5;t!p188J&hOu;5RU&n*`gxh+Tj$R!ka*Ln3$WKD}E$a!5z`DeXx70>K@~?V9&j>mR z-4L9hED*?*apm3xRwxG`cYUTvFJ{nNP~((0`{aMQmq$KnfHWA)^jl1kgE384kTICj z6KT(-SnUkRkQYS!O)MoP5MT1IPw_uy4hD>%#g{7goX9dIkakw8UNzwspk~GTxv$aG zIeu?F<>pN^HQ%EhGH)khm0J7$9|rCc7`Xiu7V$>CU2@h@dYALT9auZxkgcm^X*#9K z4+(P(Nc?>J1dmtZ~V($Tt&@rVq;bZ zbHi*X!z!@ZoBWg$=Y0wQNYC%?{7myN;paiKU2Zh8|Uy$(2fNtG;E=p?a z3VAo>Q>Cc<(PXc`kN$nmS#kp1@%$W2?9muv@Azh_mHB&;jHlYmwNhtTVHp21B0jNx|Cz zJQIL~%7U@+6%~5G%ZQ-L9tS)sY;6MN)0zt&D=>fo*QCU!T%acnscCbbxLK_@BRL^G zA}Ieu5Yqk0*r(bSo67BVH{yIu>I{$_yI0EL0j`{)Z3Esj=8n12c{RWzh)xxrLxn;? z_^SdH%otF2*#Pw5&Mh4co*tqyMuveo1#!je~M=ve~K+yACR%`acz&lZ6mTI33(^@K((?xa0Rc%QK*U*$6f;VuxG0mKBa|D6836|5$zxuw+jMkv-e4@p#Cqgx z$cmh7vLQ=JAJBS<+MwzMN_gzzImq9)w|vQu`T-BydDdA=fVClohkM>{@)B#wj*lBYiYSU8p%|!0 zDAFK}bW68|A|fFn9U|Rjw{$4dNQ0CDf=GSW+&<@d&;R}3>lkO~IFD}DUTe)c@B6Bo z))RZRm`VH7;!?R7Gla=_t}BKNU>LB_Zt4yC&iY#Mior(0_v~gc5Vjp;=t%=$_ zgkMdtLA=vvgF0OmJe%q8Bxme8$w9(N$k`b-TL0YWs9AJSP|x@HRKAm3MlXnL9LhMm zq}dC|nsIm`coUbq;}PBaZD6HFpWgo4h6IUTVP#TCdJjEJx4_fOu-?&f!s)TqvJE;q zX9Ah@ohEyV+&7Z;19+6P+zB$MBHr1=ItBt+sqDETX+xKyr=H~z5E`;LQ`PzF_zBNP zs0Uk!SGp{g)?@>%-*X7zTCi?In!7_aol+Rs=22{Jo*taP3jB!`!Y}N)!(Jx=cV>$( z{C*>L8PYO_^)p*w+>Y4h01t}!c#A~dJtuYdXtzHpyf$$j!6%E4!JK#qbn2a6dLi!m zQ{v2&i5JFEaS|u*v5g1>4k%A1cB3W`L$M+?@hPbmPk^3~QquZfPjl%g7eQI0%A ziBB6EdbiYEa%OK64E zl4T#6cD=nNz!keDE9czpORD3YM|`YxyCdHW-QZ#{fSuiZ#}(QRP~mLZ)#8hyK5a-(ACde4jsg+OtTqVNbc&*f^TUVZlC|Y@Wy6&@AZbWVvK>wmn2RGiS_o* zmS$>CJEw?Q6!&gKCeduGG@aN~aevfM$UO988Ih4Wwl0G^-eO-tm3&0(2G=!QMh4$B zFT-hESYjAJE!5` z=`;OV*}WAy>npl=!g{U0ttQ_sX291@?({1fI|ZA#^|~m0?3Zl6drj2xn+F5kZ$+hm zoMR{NY`Sk1Ehv6{E&FJWuaJyF;)mOJMe1v&M%Yi zRz2^E>El#LYWSR@*fw{Zn&v}G_be1Q&z4gr=Rv*AX=OPXbt8yDECv_&JO%GUhzcdKjjjE}LIL=2tUe5~wd?Rm2>_VDC06C1eYrm^vt zR&4L^xbI~Rg`dO8>vzj$v5KD7>B6hTU){oOVgzV+H2LJmKOJ*Rr66f7#WBXK$Wa^W zrDYgtb+TO`fqa{ts{?Dl@dgiz0W_LmWBfBDmfLjsVS4n>7q8C!NqJ$ns5Qo}iPsUm zoBfUjnSvZyA?}PEh0n3BNMq5^9>DuyV^e=uwruIvk%7OBy`*;-h0C8RW zN0eR@E19$26>A8FHAL_^CYYi%|vucX( zy%&9#P!am)Sgqc!%V26=x>ht`e`nTopR0za=7TX;vKd8`C&Nr)9;Y)VNIOL2tHPOz z*l{#0li&p*=JBF)+d4j!rL+Y#Ari=;SRyKlxAFOJ@uguIkIk1v`*INQ@q z%DSdwQ6V$z?DqwLXH-@uM91*Iq5bd=^!dRu?%W>~G|DfTfJ}GcW9G6POMlywD8GVQ=57wHlT=v}WEBoQ=ly zV6T&}rY=_}>%Ln2&9#z6B-kwR>!}bXv&{mB1&lusF!<`EW3sMt&x1`|;~~pP0sF%Z9`|?vyoi4=`@n@Su=|NwGHV zlU(lG5Ciw;x&>DLydTyvGn**Q{(iGr>I^F%Pli|T&--U1aZ+`(jlYhlji2TemW&-H zDI7;K;``++_tm(ZxPLC18<7ja>zLyH`$2alO|a=-!s*j8{~{ftp@D&?yR;VF*(V>G zMVp31`JL+=Nv&LeBO;hP+=gRpTL}499_!hdo+u{!&!*&9E?f*hx0yHm#AR0L;9C*< zbx)o&7Ty~iJmH!qF{xZaTdrDL89M!Qg85p_+o$1bTq%!KRC62^);@d}wBL_kSKcaE zee;o?==Kd`>0ASjbNB8AN_ZaBkmI?Pnf|yo3C78hOjHSxJ>jW(SBDv@?vM7|*Iu?UBusK1^#Z)MbStMJ2@8pCkBsy_y_-;?wI!3(A5&{FF%1dGqt=I^%c$2NA zV$|D^1AbNd0jGxE(N({u?s81`Zmy;O1aIBQ?y5(}T%F##qP@t!wP8;)`sjh(wZTi4 z(iO@3gtLDaS$X%+Mog-lupq7hCa^YZA*xsUVG%PFX zVv++C8k}-%Di2i%P9!YX+RO9jhv-99c5*e0O0SP`P2#|91~ekLuz)|co)`Kfzl5W5 zj}VaEQuySCNgCm!E+C6dNI*(Q?-0f|h9ztF>oY1DGu;pV`Dh@De_da_RjDVFoPu0j zl^YT~Wy^vrT8pyFeX3c^LUURwjg3=g*M|Bj7ThcJ$u`H)(%hz9@O$v3=XP43=C*6s z#QI*5_RAw{!<1w!pt-)QVs;+SUd0VR4vG}?vn56EjwS>;n1lGW@s0xvZcJLI$8*bc z*Np6MDfl>CU08Xj>fqT6M@07$OyTHh$*AAAZ&;ka9?n@HG&{kXd)LOLVZ`e2@sFhf z_%sT~u#fBCj#deMmrY_Y-o>#pE@2er?v)81HCki(l?(D&hVysc^mYZ!*Y^xB(q3oc zles#*s3N_*LZ{rIiid2I6dz76Df}j}>?&F>J78!f_9IsbcJBTO-~cT;n4J*SiOIQ$X+l=c@$@@6<&TF0`pvm0QAnOp92!e+&PD&kc2$Oh_5eS%NW z_jOIID|dnGp!uvx-IkW$^@q-0_@#W&H0aCshkEB{zZ5<>rIhn~+-l<)RA5FIM1T5_ zs0TSV_o~Pv1YkET*^3+ zs;E)vH?AtX_O0iQ%?e03*Vg?ZU)bZOv>ex}rBaLMR19DEt6b?jpk4pqd-m#yg68)# zYUv~ID%0enMbf!a4yqiqp`$AT3h9Z-AU;WSFPQBCM3Li6$B zFs=mZryK1$6$ZYi0UA|Z@F^?KOPp4iW{*0`C7c)4x0d>_PVkj|@Ld;t9oU<@1lI%G zT&)jtgAEM&+{OPQ_d-SigvR*t*c}F(1GLJh>mU7e0C+Y0INrbP_bfusav#Wwa1V5o zKsS5XPYfKZpf2s8_D*0Pzhr84(B6|mv*?!P%fQlnbKOqCwoKQD`>sfxj>4N9la;QL z`<@uvX`~1@e$?U#yg>s|90idowgStykU}ul*__-kiTi|u5Hbvqc~84~g=1pG{6WXN zYT`kph$psiLO@YNWr@$4*ksj`_@c2j)y*&9Q8Z#78Ns3{Kyqy9ETN)uENPCQ9OYNZ zc?$;Nfqs~P^?>7KB!T%E&ZR47zvf3^Vm{~Y-3HDPM};kmA7GQNrF}%&(7^?gnTv;; zqsL6Tnw`P7{rvq4Km(R6IgHHKSXZxy%W_rQYzocO*=~IO=j8(&y=z3SB;q)i@F9W5 zzJ|4lfa@V#oo`Ke_h+625R&v%r1%{pChSzh%x?jL=1jJ=WAuNx4sYP&O7OVHwe@2M zL;xe$_iR~?PC7%fJ_0MXb-B=3HOybR&+Nvce(iWqsuZxB6@i>RrE8)1+^cPZ%VhG$ zmQ`HAkpfbJAju7g`F-t}QzT_i;M$*36r3`}^fQ^r;D1tAgNykQY#5_Pbp;eL4$Jar-ea zLlFl}LKvQMy|jkQneegcpvg8e_w4X2i+X$~Lgu60U>)bz+4ym*h2D-iSE!~Mz|q}# zQD0x*)_RI08NOuTBEpZY##?hfh2hRgN!UnV{c*P{*LF?(D`Dcr4==czY1E+iSViY7 zaG3oqhd=O%^#Ww&*_^xz+jt(qjwu{*>%fNW|5>Y)svBtbu<+mqI}`hJ)CdZ|wXs>|ifv^IX$ks#U*FwAXhAj6@6bq=aOa-l>=R;_vxm;8C+D_6aZjN7*uAn3 zy>_F~B-}*1j`_0sI?LjzD=$5kI69ppRcb=<7CW{nY=ty*~Rl#^?}YHL5Gd(2QWN*8K)scY!%-!~tSRRm ztuOjI&BwF&DFH0>bb~~W-i&%fd2-42y|ixslqK1nozZ_H{=q>u>Ct05(n@vG<1}%$ zQk=D`pwFG8@JdJIKPw7T+pAn$FsHE32r+K!EGAG28lWb!|&%@<=f;Awk*23$z56uyRj#ANih z(LO|~g-#o~D!CHm_G44S@7`prpto<>qrZC-xyhqAQ+dS`@vh>P=QE6jRGoLO_BDDk$b4{ZO`+r zPE!sGd*r5`i>39U3AQHO$G{vkUFQDMv|i{VNJXTRMQ!wAPXDC_=$in44AV%39~}Uz zsN;W}dQI7X!^@d+fNeFSY#H|1Ae#kQdheK|C|hn&n?cI*3We#2cOeJ;{Du9voqtbD zm2pw;RnWF&gxmL!$^supXk}KLiv3CjrYb{FelM^O*RbWLGgOA$JD?`^X}HEbcR)GU zDLI_gF-%2ac`kOGpUhG-1SJOBv`Uh^iyS^eED3!k>RH;J!7%I*Z4z)mT80(r*S-!X zJ6EnTS1haPoUK=^P`YqP?ZCUV?s6^1Hgc2L~lR%PXryhDDe65L0~CrC*A{Zfp-v4YT0 z9m|D$P!+BB2xJ9?KySUo9``Z+(S6z!`^a;t)N{{R@phr1$#0!zUA!#Aj(ETDbrfu^ z#>gM!)^kyqn3?rKYUKbgWftYwW&Futt6@YU+egQ?{Q8Qx_{us(hJ~l(FMF)NMb)Kn zMB`{TS-xgl7LXY={L|qlsTn!HfB&w@Hw4V%89NMEU+9Be*|FTiIX>iHQatZ-lWMW& z4gQ7MaAWoq&;&IC^zThrvi%mn;l}tQx8kfdg+#*r);Yx;oECL z1!f52nzhYxA~33Yz|C=S=vr=L&XG*DF{a!}+gzjD)E`EniT|MdmWbCqrq{wIh$CJH zK3>cC^Gl1dbF@_h(P()NRWo0C{xh~yNL4aILZ@xU)F3>$+2A>A3CvG6oiuKfasfsT8nc5+E@2gM^_)k*MHrP zV^&?_F`%5$r`$cgU31dZbMEN(HM!BF2agu}2t2pw5B=}M&%OvC; z-{h_dWYwSjof*HL)7aQ}vwP}fxOvkjvygzVKRVx#2Bg5aem4nOK^f{mh+X9?XU?6= zBe1FdYHrG2-$i&HCB{;;O{6kp! zpNip{(KjlRzhn;{9j|-ixl@0|yUSeRu769#U0za_ARCQ|j`{B91Mf+7Ovn@LHne7s zh!CgE%NU{;R8zYKtb+^drvQBCw^@B)B}HM08NSi$TYV~X@HD7P4BR}xHW6X zZqgjjciL7(_kFIWQ^7i0zap-?Qm2e9%I4RT}7etGFW5vC{as^HsvYiU|>*7xueeg@~k2=OXut96) z2i{~XJeQkQ)hD^agyhFjAZl^v%HBWIV`DP$Omtt`qe-;KY!IsuBvtJY zdN6JzV0?s_a{(!?X}wXSE4G%BRZNjM&B!E0M{5EGOL!L2AY*3iBv)A-d+rw?MU^Z8 z64>;Qh>PNbz4LGxqX^2_XOQy{f4eI7!XO?30<2hp+O?#@n0zdvN&1+)z}0V|QvEo& z@&F}p9mMfs29O-Kx9BFc&wTv+=|v7JS6s!OXaA1*GSO6|yUdI?q+Z^uR%Cf4Lv=XL zPry1ca^3dqC^%rLxRk8B#tu7J9L z;$+4kv~3ZuK{KMVv`O4Qjyb*$A@%A}1nWwg1r2@9kMy{SD3&VuYw;4m6Y4o7&P4DKt8_dc|e3J5bjG8jfbP>w}|5hthJ>04~~- z1T4g{Jm}U%)^Rg-ip$HKiLR4Fy>^0s7yYt?goMWSXU+^m+>CvIszsEvvyhTcz0)tN z$-6Pgr=bpJqOuHt z*GQT%B68B-BDGGh#+O0)L|&pu3;hZrCIba`>2WolnDwcC1Ch0lp=u;$3Dn0rhPPFg<`KnHyxiQMAncdQu}EY# zTS<`^jC*jc@{J%s9$}++EF4GntXKkhgz>vIS3FpFG0VJ3Rs4Uf_xUh4_M!8SfJlnM~l9fxcC<25nW{dvMlK!(&clqT+zrKN)R$t;rr$IL(Z3 z5#>T1HDHW-&7ViFruCQ;`|)zAz)dFz?tZ?<$5Q11`0eI3Fh6+&7)AfB5q4N(py6!{ z?MAgKJ`n*2&+X|f5!3h~t#rNw2?K(VO(LGjM(vEglOO6`JdNk)hN6(EHkw?v$4Q+1 z6suj(BV>_m_N+r=-9%bN2_v+4PjPY(0RZ(OtULQEn(U{fFq)pmB%7`>Q{P=2yY{a% z$S;XVkG?-qfh&8vr8aFjUxh zCO}`@4D7&ExXo5E$r^*2aCjW@k|JMK8LKdlke=A1P4Q8-Hu*jm)j5gqz>IJ|YoBXg z%u~s|0>p#<48)_u`Cm0{3Wu*9@Nw~$Lh{ANF|y%${D@LC8f$+>cI2W!Iw+;>b{;#p zzAAdR@fX2GwU{mA;SYbIh%E&7qXN}a8dS?uxJw&rd9klbaB&}(5`MJyI_l}N!!qBs z8%D3c@r?EuIuVgy`tyfVY?&POERM<3r#J5UOo9rx2pMS;8O2UX@7lLd7IyXt>3r_M z>NJLt|FMSKOm9Z3*VXE^a3Ry)N8_f-l}TRA*j&9&>xs^soaH1|M$m`;l8bci?&Fo~ z)xzgz{F~k%xzWF=?*HofDOdRZhtn;Rk0$BFl9Advy1F9(!^@|B5+OL@a|x9BFQaQf zWUcwdH&$)#x@w1?&M}8!DJ5vOZv<3rK<74BQ^^{%f!0N+P!6jrPcQfLQiE*$wbk1b z0nZIN8`D-N1A*3hkGQ@2HPE~e$8pPO?K6qx9oqWgxwrOk<(S!?Kf-!06~YHVH? ze5Zcyco|(?XL zuH<14RpVjDCSTYH{M-z>cp{}GE28bRLgn#{k4msXIoCy;<+H!((TKGl+RKC0A zHd-6(|9PYQX|N9G%tO8k8!K`_TxVoX$dN%$pC+e?vV)WhPBO|)TT!FrSf0Mtj5p8Gn^e-=?Z72QAemK8~;ZQMgV72B-TsosYiZG>JMBHB=3 z)8`n;rDWRBrN?sH`)x9UmKr!O98wc6H|-xY7)&v!%{I7|Uh#cWD7CSXFC#7ZBrfms z%wckM9EO~!!UMWJAMX>@W9g@x&m?0Bu54{>9b$)e>LISG^0<)R#{j-CDY7a#hoF*7 z+jsOwnYn_>)4&GLy;uJ)jMV#|Wkn|o#qj^-8~>V5a*7_W`hj<51k2Q+sf{es z{$oej9-1BV8K*@xt)}Dd&nI3; zP?Lr!XrPw+Ui@=@35WYy(H|b=D?~_A`zVQU-YWfg#`9@j6*Q8=Wefc>ls+Lj7Z|4x_zCl|7O`Az!5Ev%E{_3Zfhi>vv(KR zs}`&#mOIW-F%-c6?FUS*U2nSWu)rcDBgXhWlx&_9iAILmm>OdJI(gU06)N%3-9#_F zRodzOX2V8A8k#{T9}*H`fVMcFw2$T`gS-9wt0GtZg5p&bhleSgu7~W0l`n_r!BPi4 zA!~;_X(JQK?66@l!Vdj9;YT0qyt`ezcEzo-7xObyi_5*4RO*gsUHta4hTS5Z)h?n* zYb?UC4jm;ps_Rp<)O*Lc1XN!hP_AuX9U;K8p1(%Fz774>D89BwT=J?P^Y&36wtTYTEBBH$5*|$$n}QzN!84ztO}b| z)N7NIp<-AP_iLWsBZ-QYOvZebA?7yF>a(*>Bu*O5e#LjgWy_*H*`m}Qzu~{JRO%-)!<;Jh6KlZ0NYvxXRK->lsDnByQxoHMdzFX2dYF$hsj961Eqv9%v!=#V{YMS9_HT>QJ^d;M&m??xygKoNP$_JFH+ z&+Wpbzy&=e0jjo5lCNlv|3lo5cRKn!?d`s(ld&&)D}}LElv&eZbU zws8KS@m;ey;|NCuv5m}I_47q`s{n0J|9(H1@41V*6%kNNe59hZ5}w49UKYC70vr-} zOp75nKyhGrD$!DW%p?Y)xrc_o?&qzjTXc9qb9`%`S^nyYLe&<(2$lF_b}R4(d92^_ zGK~O=5~FD#u!yJ@lhrKUdCwBd6G!CP_uR)nigN^=DQU{oU9cT8cEuA4^obz=^EjWF zSQ{j*#whTzXG`VdPv-3aF-%3i8MZKv%Lvb!jBhCfBO`G}ih45je8Ry;g-Ox8siGY) z1E!{wqQI%T4j5tGn#5A77SGGf{J($9+pUb>;nKzqU0%t#4AQvMVh{WJ)|9PI4-JV_ z8khwPA{X@cshtW}+zy@Xyc_n97MwHbXo~Z;fmV4Nax43B<70K&>v7$2e7ik8fpy2| zU6Cd4yq*bo2%@BEBI3t|4EcDvCEFqAm!=EcI5XFO5j%EUW0X8Z51D z!l!ZsrZ8RovXfT@kAv>Tj`d~@Q(Xq;VitoJ$u0KUz862~<9bG-(?K1huatlF63Lq; z{fZI|C1}OZ-fr-{B}&Ee(&Rq36lq69=$-^#om{Uj4TmZ-OjQ#~z|@ zB6#Ij9(AUr`0szj1)lF{lE96$rQS#ng;CT(Q|KC6(1Mtv=i1Ctw&HKCDVGyKKxetz zW^E;3cAtBwO)4SMI6xc%<0=)&qR_7-SL>m06@omBh7dY?{(L?mmSL1TXaASd;N0~0 zub|PBlJX*YMi|1i{`~;Rs>lcELBm5_L!s+FXKIPFY~#^-4j72cd_w6)Q!D**7x{x5 zQK}O}dkV;J@r&|c-cx^Fi~k^09_t0rZ9$-+Kha-ZND3&NeO6g|!qLfza# z%#7&&{xdoiBT`d}97yMa4Rxs(*_Swj`WXJ8hQLfIvj#xSLruBKFibH`n=c`M%R}ey zR2-k%w{Pcw9{)s6S=1l@f$d@*O0!H8(=UfDsSgQgMJAZew>_5MNkiTDC@;7Fow}r) zYj7761`Xl#(@cdh2@pU$5IXWxC_d}{UX1o=3_hG_4*22VM73-PDJe_=G}OO!QqP9sDP2dkfRD23@hSmqYpdt!LJ2Xk>$G($!lojd{}XoyCJfJMX% zrN8+YT75E4Q!UTve#8R;6A*t7^*ha>A|eCXQ~{LpvN%-i{}8~sENBO&+vfcf-#$8_c9Y-KRaL_)qs2SQ zLikN!G`3-}?bnl8B~^T)KnP7bG9A zO5{@e*Z~~(K>CJ{AQPz3or1n}YqlBTUKRlTrLikwo~&~@)7jqX&y>eKvW5Gw(*HoB z^>uAY1ZtW%)HzQk{mQINbh*ba-cI;B02KLbVoq2(;QlsxNX2keKisd2lO`t6!ZEQb zEyst+a;pDXCh3a`GpUB0^_Z7xP8;i3|G7zEpxfgRzv;Hr{~dGVI49c!nb)Aqk%MN< zc|{J(CAI@R4AHQym6mRJPrt9wtfgTipK^eH6Lf!Rm^P}Et@Cb23suSS?nY-auJ_#u z6-gr8Prby4kJ!$!Eav5ZY&NzXB*h)OVl~qylI`mXt&fV#8?xrPimA(viw;{w4rV+z z;SEZ|E7*W>3GK@%O-^`K+p0Owh7?7UWYfw=N59UyL7}UkMqGA{m`>38w&?i;4Vk&z zfK(I0#i`qOVYQeM03Foc{SeYn%B-ZmK+>mKj7eax!}iN8`UtX+`*x;hNzfW1icWgb zL1v(JxOz)@J{E079Euq_Vu2z;E|A;}F)VE*bT(2ghWI>6C#Oj3*(W3{24`e)@hAEC zxHy7(9g(eTMHX=3C|Z*+{bCVu;-tW=*N^>FU}@_)*U@+ggq$3G%<%c>oB8W$qIg~HMBR2N zTs$y{Ji)YFVwC&R>W=hXYjt-3D*`tYVlHq%F_25fZBn%w7)dY;v#&#ZH_}qC5SIXA zR+wL>;u|G~og6ZG?J$3FcW&7`H2oiFZ<0HOYF0scxi745X*dpwH5Ut7%yWr26j#b( zIfKJc!|}-ZobQQ#2avr|h*GcQ&jS8cEFVkq5-ySD#4c01clYk=bY!2#i?*Xd<|Y9N z;j!!AZKf2nCxG}iNLBk+7mxY1k=akd*huGql#S6-q?_pZ^q}o2D=4Kr6belLIc^jL zI;i@`68TIb=c3NWTVSWv6*A885+Fq6ud$8}h~o5o&-W^4fIK>iv}qEIW!^y}&s~Dh z1A~=YQqDaB7j>+K;V22lw(Y=-?A8nV?1M_mL|S2dbbguH<~7tvjJH-q1^pbKDJaW*eyz4y)?AYmlwt0V~%zNXSJ5_s`RuLJj8A6hZ9C&r8 z7k9j$oHi7X$X@Q(HAugi#evqk>=V1o4^QF)hq{=_vW=8DXuvgM`k(~K!8yuO7AfWw zg`n*=+9=dt(IX|Ov-5j;dibWJS=|g2I#{1|O-EGcYe-QX* zM!-0e>^>Cwv$mQ_ZXj>0nHk41+>yV~?20nPe+&y`%5nnQ33O!7KdhuJT} zIu2OeVQjjPT5m4LLLCou+>a_3Q=kJ&Yoel}WV@-W*CUJhr`=AB-4VI`y7DXNw~;d5 z%shlpd<-s8)IL2q3r41GPJ;M>r@2iA@WAYMI$_#sqmCmADvt5t*5Z0nVH24Z6gsMq z3#NgQefLa_TsWqLvoOh=J!`X558I7JXf_6(?F}|A#WprE|C_#jAdaA?Z5>-~4e_sa zAY-mpM%qpE^6yL@CVrFMPR~&=kDzL|A;Pl_E^ z9(+vI{S6F=a%$s0;nxWzIU@v(npbhq8qax4#?sRpSH+-*(x02h?p72*ICRITi=-FJ z$E()}M>i3ufb>7GRov|oDL zP*v7izVys#AaxFbAxzbJ@tlM#sIoyMggk$~--I@1ifwFECPxq9xnxBKcTE#-Ha>n= zdHt8WWQ_4Z4q9{Cr_rRUzsUqU*^KuVc#n`r5NYF2bU6PsArvN8gma(4zm+xzpb=e;N^EQa^v-FZn77kVyRkOw@Tl?+Vd}D)?fJnZK{8M_|%R z$yl6oUx{4>g=dwmJE~a1^V)-sV438`C(yqGov5Mg=?W7xMR1;Z*YUk|HoG z!=K5Kmao(yY{bLq4zn6zQJe>2Z>aMxU zQTY7N>$0PSW9wLRemSkX*I<3}J>GIWJ-JpQLIKb^nsme)=9hnaVa6kpv2co2Vg0k% z7Ka$KVw3L5bDgy(7GU*|0KxFR@K^t%%cdmC+DevxAUw?>#N`s>dt!2OX@nazsY4^! zge-?}x0phS6sE|k+Y|)=1gvYx7$a1u{=$FT?;sJ{aK*A~m& zAD?A~{(FN>7^=WyzQ@+$rf0?En@@yxVe(LKZay#l_VT`HJ7ITd}pch}Rw&EF-dK z3wvzuja)ceXuZESgppOh_qOu~y#I;xOiU#ZcA28MnnD?+hD~YHeY{S$Cu!cm37v+D zkBA&g5e=u_MA#Xh8)7NO1xD49&d&7$5v4ZKiPtK+;wnn^gcM%+Ql>VaH`&!LjV1Vq zhSoQPPeq5=TM6YD+p-gPe;W~MK#^vOZY~d%VDU>vvMxP-(64w_XdEi`%qe2#x*OA` za3lb^zQCxihnsZg!R^SX?;qttMRyL10{6tGw-c8FtifBzy#{M24dnxwsIUbM07r#j zN$~a_cE++i|FH9oIUoEVJJvYudTZmllH#!g<5lk>97A(<%zbf4pbDSDH1*DxJZeZ~ zbjjvV8e0M%TUo8*hfrn3p77(RIlHZS)+G0`o3@V-W5v1vZXq~D5S0=`27i%V>vG9E zcO66h_3$*?(6(K`<0vDdV8k|FLEZrsd%!OC7k!~QEjtZ7*t)y)(QGTM5gasLQeb3q zI^WzVEm?T5`#gQo8tM~e1W!n4JueA*Izm`tt2ZL(+P-{o{xsDrc{dfjjM{MmqlAa4+HLwcF6jOU(k)_`VqQbQea$l<4T*U54Vp38w#1O{HxSkdF z@kX_6DDS`8rn>>@El}Vs3%Y00R+5imhS$~^D@^7Y9#Iow7paRE5=`VWX9lAH{#=N+ zO(Nkj(+0Eth9{=E_QSc8+fMRCUy2V@!_#ECOEaHIe8J5)Oo_u}RT~>%Uj;;JpC^`K zmQCLQMPU@fL$bCcd}`P@>Y5r=mNn$K zcsRapuL^4GHr530$t~ z8v2|)*`s$f&)|HYK8g*OwvxUxXR>?X_2T!t4IdWW>WT+Z=AYabA33U+rg3hw@|m6g zrsW>yD0++`KJDCt-8Zym%YysPoUhC?lj?rIk(qU*`JK&xT%SH_vWx1G&a0jHpLY#? zEgbM!b~MlJS>jknw4+K}$!LCpX|tX&GOksV4*79wwJQL>%3It!GHO$XIhLX!hIsIf<3Eq-^QFqa(c!tolRc*BVwz@W(OA=b ziA#4@X-UobIHWJMa*J&Ao@W=N*_c#uTu2_mqEBo^S7(g>6+?qpx+)ex5)~&L*Z%eK{zfhI zG#!YlZhG>_Ib&pQ&Zgz}+YMn2>2AHjN;bbOTf|I4(ov*VthJqLQW~3TG3)T6E%?*1 zSe3{1^Ui^^cJH;#p8Dm((U_Bxm)7<3gW4Mmmtk3<6zOP?q~9xIoAn+!V&DB)RYFn@ zbPU5Lf2!SG#q`(r2J1hz)3aI_db8)q5nYvwQeOE{!qxK&)yN9?jue>!vtMu4ZoHIq zCmn?^9}Byx%S_@>QRcG=*_IbZ@=5yV#s7RP6EeHHmCg0@R7KPyW0m;*x@mLrr<#&j zG_xK(*kh>L@C_Y`LUmNZue-vgEu~`ys!DzB&gm5|Jqt$Wr_IU-YQsW$r(I=4`nRRJ z4y7kfxyrcA=;t=)AOCagi|e-~sdbrJx7?$7KD=*iW~g=;t9Of@<{n(+E%%x2=T>M> z&KYS@n}J?0kM-zq=DtNy>9X0NYg0`dp^hk6OpZ2fyDJ=0f+u(_`f8hVrpnihf(Z=L zY)Q?$K0+RK@=l}=^X;!aB(_jA0^gFU_=WqE#||zo+N+DX6eZ5kK7Tzpkud#fi*Uiy z+vC?<-e`aH1AapSky;1(AWmUX?S@05sF)cd0^|f`)I|LkYah~}D@aiE>Zp-EPCm7Q zv`3ik-Ef$Fk6`DWYGH&+%8c(5ewM$CKnvAMl+D;x+to_3NSP5dJHVni#ovW-Hd$sV4<2Xvt_53Zxy_x`SKM=t7vF zQognR*QCzKCdM;iQl)WmX}1Nl5HzNeR8xi6O6!Y3()YQUa?ZuFI!~)2KZ!5=Tx@_o zJ!ztOMw@i^Uy0(*Gp!64o%S7PVp7Gy_49b+DsHOsKj&7LOl%hCj}B|VfdA^UP3azRhYgAlZrD=0DIzkR99bb zhD%q9Ke?K~=;Z>X6=f!MO4zUQ%g0deF#uu$ayJw07o>R^lYVY8pBKM2Oen=GrW6tX zT16ae^i1!cFU2{QWzlhr^s^Z2*iQ0)Qb8*$L4AT>9+qqA9l1Y}iFU;i0zdF7R0Mmj z`_g36l#>{N(uNXbjxNm3`!w*;p(=JR=H--`+$(SBzAoadjY=(x8T*2ivPLp4H+6(m zAO!o!X^jpD(8i#!9y3&G6q#Etw7xFfzmHRJH2+rU%j)BZgtO}%}EAv*`;n9DhU*y&!6grNrq2K0?*n0PjbT|LDM}-`(A9 ze1*al13>Beu(K0-&(WVxCtjlqK-Wq5OOF6`J#`Qb1h=vWg;N@6h4VjNe+fs05a!xe zp1O!k;?Pesm=2{vXp1%jRGr|rL(WYT_u>3Gq}&zG$$&M>%M+U35Gxt6aScpQ|GsZf zXG&gO_9(uuUpUJK^VG1FH-k_Nh4$3{@{_;2h-SW?g#*$fE9rjshTZ3~ zz{;0?s-8u?h+c~u6qH9+2mo4`M9RLD(0}uZt7v(#I6rpIo!CG?1ezjTF@xbxt9^@^ zWFaerY9UAN1aT*viwCh@I@3*G*V!;sum1msV)|DacrRqgL~WNwcZ1p!h(pl)`z-?2xJjDATC1$mm%cp-$s5p;3AVj&R2 zlCeUPh6DweF`>?Cx~m|gb+zcbp0n?uxZ0IXhfE)~0ez;r^tLiR`7Z>2WZ-+8oFx15 zJmV|f|C8Odqx0}3aR)q90)7={Huqa5PF$?)J?s8duV4_XOkFz%Z|9PJ)2ixzt_zZ} zkUxo6Ux>L*#ciIVIb2~8?i~j&T>}Dpkc+5EGJ<{H63`m}$|2*rT`k2n$y+%GI$qQo zk*Rb|&{UWL2A7J8J}w!xU-x~_)i#{SYFC~BWhf_UUN09-TZ?LGeS+3$0L{)?v8E^J zOWey#hcRmAr;mKZlM{=$;~J~4#lbP%t2u!&rU0deI*!B}KRB&LKUAX!6GHOJRB zVYY1eP=B2-t(coty@|qd#g0KNp5M`E9!plmTCQCY+mk96>s3xyW;Qbzu-tdfWDgRz ztNu)l42hga62>#qZM3~GZ4xANdo)sf@nb~wj*@n*dc3#X3~yjaV4x1!JAoawuc2_h zCqxG(ID|1w>RrwtP;tg^cwxr`TD{U4d;76=v(Ag{iaXU@wQlrzkd&*WUz$#XoDPdf z*;HEPyb`a{^Vghbmm%|PAfU>G8Ql{Z3oM*638~}ALtTHX2dslhM`#IvMmTo0WR4Vh zv_SWU5{$<8yRNuY_Ns%#juEw|zNyev6_zI#%OvC5X&ezDr@?P|-Y0$D9l+7}vwPTg zu0Oa^DZHP!R}{XJC{T`NF>mELq?fjg3%cKH_;TpgAJ*6@u+SwfXat5AlqluS(V1it z(k}-W(jWc0ALaJPn5Zf>w1>h?GP1F=H^L~OvTGJiJtgLMFSp{y4sZk@PN$S>dI|T~ z<}{Q(@J1HvjlG!5#w>3#(`QqD}aQwP^Vun zm#%GujkeQEhJs1oDy0#^76r(5X#{vbrVz4(^6rg0+gELx)k*a+#3INvQrt3{XG`)8 z7m#nrvcYU+t-KQVZV}V6Izs!7w*CaD#5dlrWb9N~o$^Ez7>&SqzZPV-&>Vf?4Bo}b zsbZ`U!+Ug#S(2btSsqDz)v%Nno_8bjtno#=8$Rs~_EMDK5+`Har*U9E+B{wPaQ_>e zZU#F~-ual9A=8R&_TCI$k>86)mOWc^=V?*$-Yt8$a~X{kMMh(f<|Vq%lIftVQn|<6 z70;ZvX5ckr*x2kW8?Xj0I;jPCM}LA3m< zODfTl$@4AQJ~kH;?v|8ABgNyJxon;O$JDMf7}VK1^DOj9tdV&Qf%Ie|XnyHj6p`g@ zhuTaZ+q?S#fsKPYfg2WO+u8P5XNL=lXpoHlKJdVm~}ErBPmu z@4TZ$3&70&1E=@+{7EU^L#QH@T|~j)u_L+eYPkC;o6*PjB&5kP-gBMi*qB+f{)NnT zgTZ6d$=50jh8qn@pS*SJuBN7gylU?n9|j!!j^MZu?xllxZymmVgzjO|MZ6Cg=X}WH zkvjj=8{tv!-!akqJ@F&i$maBsq zU-k0G({}agSw!Eh9s1wuWxm7n;>YyMn1rh}p;&13xb=E&F+HLCAym!h&m7y$&|P{J znn`oEI*bC~=(z`I%_H#4t@VCW+bY&GjwjAWt3In#4Z4su8obtUf=Ow26@bj}kx7BF zz!{_knPO)BsKup~^6&4jmqdALU>NOMKulvqW9bY~qs*^0)oVf1aA+|P47Ip%(n(Pkphf)-|eQepffJbD_}p9zlKAaP|jG%DC=$QcJkM})qL z{k;hgg(YhH=#@#*`Fw`LZ^K7+Aq+I!j?%O9IIHwOMJlN4Vp8ie-|_DR!oB;%bW7#e zdT=4aW!E4GBWjwuf9fapY^&)Z4#hGx4w08jS$#B}G@k4qU^km| zqtM$3@@TA*f2%Bime6nibCZns`N{@Px~oJVb5#$xxoC5E_Z!-1vt~hI+hc53d5))7 z#fp&7b_w%c)GXiLeT$deV);`pamtm z`xbHo;)^r)zF?^Ee1`Sp!q+-iaF1dGM_lah*-%}u=GV&K8uM({P`i9E2yfS+v(|o7 z$7dD8nYa{BFA2foYX*kfM!V~vIY=yZxzl&kPO=}dPKc;Ud2Af`B;H@74Cm&^otS_% zyMrs_ev}pZL~7m;bQKD-->2H~VmR`$x_V?O^X{<|1-|p5iA&5t*OCbaIYrkN$CP;T z%QCOFuc}?VH5a{Mi_3G!%oP>xeR_RO?EC3x#4}D|oOcU3{2qJ1#P^pI9KLYY!G^@< z9Sw+5l$CE!;fu)ldg0-|?IfhS0I90erGl1BpI5&9n^Wyj*>M@w5phd~jZQo($e6hm z?3gL(<{7w6i1DCufx9N#o}-z z2lDp~Pps^$J{4|@h`T02@$*}=sl&V6Is5FO@$<>T-G$=<%PBFt>KDKMc~R{kcM6Uj znO12X%11vm8>*OUENK3H)03w@e?+%zOInjMX5B`=^EiqV%Kg;8ao4wFsBzaCG366W zxO0{E#VpfF)Bd>vYqJ#510PzU|2Fyj*{QlbNP6#RjZt$sccNc|*;Ebgm?S ze{?nLAyeh|u8^0<`APBI>^<+fVLtt05eoE;-=EWfBneuVaOAhp!{dt%Pg+w2Y-uEn zg|GeiCKb`cg71)coSJ>jXK}#3M_nr8l+KYtx5t^mFF>@Jui;qyChJDhR!5TtRiSX` z?3*T~6A_bR@M6l7(4>T)Yc=)zG27O%u(js-Rm}ttW9xM3|Ptys|0*dy*z(qC&Y-8Ff8 zb6p0qpM1z9Sr*ezduh{1eC$>+C)+45DaW6<>7_F_N*u;UGrS*f%cJvZ8`-c|j&got z&Mn9;Y{P$0S!)| z-NLW!2^AA-n9CYhuMQj@{L(^0cb$o@BwI32ZSUu>@AOWn>d;^`17$vzv}XfXf#QL6P6Dke2!MT{Dh$rZy` z!%+!pm}|8&2XEQbtL5yCkm;$$utIsD?>|K5k2^Y5peAc=Lx;?NwIR(I0D!}i?Hy;f zE}^_4qy%9kBjC_J`QpT*OTlp@hoH>?MJwf7>i-s{{hyQ>fZP1$sA+w8rZ#zVi)QVE35}63k!-59ia^@E8#4jNbAk z5(36M+KE;)xP7=T(T_J`JJGXl-879j{w5qCW>9U#o<{`~aEsG4$l8(Dl%IZE41BX7x z?mWGORX=RrYEJlo@vdD6=gjG_$*`W;BZjhq2dXQcPV}4?9CMC#&Sap# z+Hjf7eh2>gP>1q3QG(;!LIE~zwLQ@;IkQ)Bw=+oK($Jy4*c)s-msDrbH|dU&+E}PE zrUp_flqH9e5Pk^^xO1C0zF6oP007@LPoBKq?li=&Z)i9KXOO&}-Y$T7k9U&E;XkMM z;_hfptNHo_>i!L6p`3?~4Xc~@6q(yQP%>J3gUO7shkhd-*qsqdG!b}Wdw(^Q?BJus z_+VWf07XTigh#5<`hrgCby|L@2epozjEsz#CQ7m(?Zn%N#mGd&);W23`7IEE;8}dw zwnTyzsO450yeA&vyj%g}b)4@0K`;IfVQ(20AW97| zq{K*rw19-Ds2C_RARW>SHFPUVNQ=~v3eqJpbewg&-~YMZbIyk|U-tgsHOxHoJnLTT zSL;T=3&Nux>Slh4AGs zptb2#yY>W^2nq|g(0qop<9HY+CfzDGqTq~*Lk%;ZK$BvIpFFC13=)41p$YfO{I{b9 z2W9{PyR);sQc0;{w@-lpt@YuPJ34N9^cdNTmtf!9$mr-K%=oVS-WUvJ3}7DTT5))E z+TfYN-be1|&JU0NU&q5P-FlKpx&ah9ejb6Cym&ftV<-*DiKoHfgeu4XAecERVGPF`bSlMk* zDBF7Y#&J2|Rwv1@5-=VqDJW`EzkGM~QT+k@M|aYsFm8TZW)xPl@{8SIWnV7JaxJ&{ z1&@2I9NGME!y!KQLwsUqIR(S?FK05yl-hvdz)LzmL>NIRV^hlOaDwR{Lb53>*dNEl z&OXKA1li%mpI@L|CWx~P^@zlG=%|jth78xPOE%rHKx`uPJu~2CH5L3!T2}{@I&gcM zQCdV=gg)HuBX(Kb1hn!m(Jt3ErigI^lQcsy+qB-I{bUVacC}iA}jovVB)_Ba$sZHRGXI;?i(>H|QNCWU?0 zUI79+Q=i5Bg>fv6hX_Fthu zwiIt!3kvao?iKtq%_03)H2&gV#Rd_~HeIoc8!5FP-|qH@JN@h87~}FGx|-4AXTwuG z^fi7W${_*{uauf%b~eqCiP7^7TEAkze8(Nq5E8ipud26PU3_hQ!gP8A<%@dRx0q7} z@8x(HdwI{j7IXZs&Mc$3ig0xPdWDFGyw{8d;Xls>hdJMm*c&-Er<}TtpBFDUG9Txo zqXR)L*3+99`i?taA9fmfRtp6bd?`pSPN5ubhK$NsNRUy1=&H=;?+m6rOVN{_9+9yd z6U&;jlhIjQWi_aKzE11cp+nTZ{&*kHu_pQjUycJ+%%^nyRmr}pyi}GG!6SCZ6aT>1 ze9|ze(JjeyOk7AN{f=QLwMY_AY4Y?6H`dwnJ(3XO_u^XOB%DTz#-fqzu!S#7M?!+1 z-kep^70s?wO}TI@#u|Oq`I}a@9ob1rN+>G2%<>}bK!Y@iBMwNZFNP~jwhC(JGbB>5 zNv+_-b%CD}hzb-1kWbqLZ&E~F5e4D0=33WNTi#kFO&u1E$BcD;ewN7^EpRlch4ee)b)5z0hpY|6v> zI$#d=FdpW2=XyfFpIr2U*=2#G$a)20fp~=;-yaO!AYsj_^o&k#?6(=CWbC}-_F=^T z?3M_CR?8AX_z2QB&Xbp+WOkskK1m|E5pqNv6y=CP+#?{FRJHSOZ%b6}cbWwkij|NR z6C=r)e+o9GDM;SrvjjFIb6}9tWt2rj&)n_`Nlg}QL}4Q>t&ntk>m9wB-DgI0sAA{4 z1HM5RLLZ?9R)U`6J8sU;^W*$xF;$lW&#?%Wq>_0ky9er71QYjv5O2=(u&MuBHSZFO zs!LZ3Y?dNIxb^pb1V46+7lR4fxf5qT0jIWO`E!BGnj-g{LB(9u@aEFMxSt~QFdbmw zaRGm<=9J5H@%CDa&m?d%nVrMLG$ohx932#%RKc^d4TNGBb{|i#Q7UhGhh+ZE0l-Xi zwPowNZ9B~-q4y*LI%{O_E_9bQ$0&S;$}5^(w>!1vjOEYcsj)#cd>mQr<|e;auPh)5`{F#vhuz52 zokuOeT`1-u#D5A7-i_k=dm*c8X8>HI2@v>&)IeQ>waf*&6C0Czo(V*Z9rRg!0!Gs5 z^*tRddcRygQ|&EkS;E8FR^lEZ?Nt)~0XhZ&tiZYCq5C@GsUtM6;pR8~1Xq5uRm?9> z6VG351-**wuqT{5AQ><(^_ZkRaQK3>`{}E4H2!7pIo9G=gjPV7pqh1+gV*wf=qf~3 zDva#)7vL0K^{;XeuXw9tiE=6lB^hR{`R}=Ig!DS)l(m!s*N~Yl^9j$5aAzjFTIjKk zIA3prN|*wIXalr<;#u#J=|!v-bIECd$v+~?+zIDaIjcKxL!q;JT&WJ%@`UuG3<)1q zR#B?{bVUDtxpA@Sw=R2Xl(oJYeE`SHJs?gSChTM-p~h9lj7 z;%?c&;3WIxRY0gm6mNdd4P9w$&(*^0+IN&gq#%coQ)X1B*t;5Wt!XwpG5*dl@}^6q zVO8rU|04B8lB$+Hq}~q9F2h+NE-e)FY}}5lU?+pEb)Lg2$Vgp&ICtMd|2Qb?oC3 zz68nKsBZ~6tqYYGK9kGGQ@b^_Go-ooJh1PfqiZx!LY-G>@HHTdTJ>=k9r;X(ElhCODc!y72cd1Wm?Vp$xZYc27#xhPML8J3nn+u8? z`Ug+(v)=Fr?iHg7EZ*za`{8Uzg$SYQLO>+c(1J1uX>bNDENbC0(PMe2K;V-rNX9b@ z4Z&y!%L^I7iIJRhi5_$4DKtl+?sUe?t1ko2u_JY(MQdy;QW3D>eSpNNPIO?9T2e)1 zJfYehIvZfT;70Bo#Y4}ktVk6HSsx&uz5@)V)wBxYq2Z3YIGQo@JrlO>F&m-xFi2{J zl{)!OV|PF!rUT`{-oWM@|y;_mCSf=p{7vV!Vf~0%_ole9xX_ z?17*;5JhM{+R7*#Xv;SKs$ZZAB3|e_j(dR;+a)IoLD0jq>! z{oX9Et0-nPtK(QBQGoAUhWj!)t>Dk@%zfuNod;V+dhEFYcxc`}2tW*4nX3-^Yd*o}&9b=1@Fb;-8GMnBps3w?T zvzU{Mu44@`?5f@cMo@6p;>!YTSbwi``Ln;;brr@Rxcy>a1Q0B^N1vxu|NXlIy`39u zcFY`v=(|Y}^$9`DhWd9ti_fvGkV%#aOkH2b+uQ*;GMSudF8?0-udtI0P%U!XQXx6h zt(*ee(~VG==I*B$&i!a-!Ys+QSDa~=+RC|hK(x4s0@w^bl8H!}hRp`5#MVeN4I4$?fH_KBwaq_1U?!T9c2 zf|sgUX6KO`nyAv)59vx-KMA>mJ4LhYR{H&2T|w)N<*|B2>ww_!NJ!v(Hiu6d zyb6uSyuyEDp?sYNBYgVqWjIn9W2Ooh*+HkFR@|}!Bhxrczx*z}FcapZb4U>jCA$EW zq0VsVP5`f)McG(`b8`t!`YB*Gcw9=OJab+{Ou3kLdWkr&1+Eh&1;aYU$2W`_b4U(| z2~Bl`iYY4Asn8;QEeh_L*(d*0x$sTG?Z03CZzSR76pSMEL49(WfWg6mK7!acKe)Yo z30d+ouaR;DPSUS4VpmCNMy_{!V7Q$Xr$2+EzNyH)2r6e!7H7VOb%&m*Zfj}S1h(Ek z=ba2|T8lPdN#s;>n5JvkC40l-EK9O1voqWId$Os$wihz-i=j)Agk2o z)RenBv@^^;KEV4-0hm|F|K5fw2ARmi-5hVz&{MDp6^_Ty&$y{VeqH-wcGQ7?9bx?=N zLjb}1f~(*h0yd?e@QPj1mAE0RNqGIJlG)sBDp9ahR9H~Mokl(323sfJoe%0q#1inY zhq!noQGLS5Y8*N|{NY^HQO$dG>=W;WOYBufGOLo`L`-qYw%AC zS{B|lj7al#_`w!(OwTw#99<<0?ZNkgT8rXE`5?!+6`JxC7~S-CR%>PYY?0BAz&)L24NWi8fEy&2jRQHyEA;Yt zINEY2%L?(V;n2t;EXiZ<+M^1%N@x2|KHwfsvtqGax!Yf+{6w#Y=RLTKTE|h)R>nMY z>kdRVPuY6H$2(*=y=WY0PNRRvOX4v#8d02niu6B(wi)bTbNVk4 zS%%CV>w#B8%^!35Y|XQ=v|&2Iv2d=Bdr`Lkn_oGTZZk#w_K#F`UO- z9j8G|(1GBfj~|b9Nv435Ni4ifYFpOfU|d>v5m6Q^p@{MIe*ok41i%(uAlnfG!HY`d zWwpI0A3eBvdmEufFpH0a=uR6r%{(CHwAsEfiwFSCO7oJ>^nkY@tr=y=zH~-ho1=qT z9z3QAo{!YkALP$L6;u_YUp;fs_LKP5kv}a5>SBgNg?q+4XmQ_*)-TsegGIc#|I_Rf zc`&M(yZGPYy)^#D!@k1K(1bI5DR(E;c*lACVs*f6cfzSp7X1$HVLba(jgvU3jKFw1+H*wq|UE%e9<KXo#t>qvmRr^;fCtx|~89gR09;)Ix%;8y3VkQCc>I5Sm5x7__ zV0F8w_8$L5%>y=A%fB;h7fikxa?eZ`fb_!x=x40-Hk^=dUt;5Z-$59x7edaaAe25M z)D{`A4vmJ%gSzU`8^b>U9jE>n#$Zxf938Yyr@*smH&Pg-iQGD^fh~0_)(+PT40}io zl!GK9X9EN2Uh5l!526<&)SO)JhxBTYUPSdod<;t2ebQPK*2I9n&U<#Dt8%-8a4f%m zA2KY9T)b#tIa70=`}efTPD9#yOd$mE89ewxGa?j_Z9|4G8!{=nhC5NONLw1)3f-ah zCs7GMU>{UT<}Dl`mQer*K@AXy_CotXtB(GB-_`~_*NM3N)I>jN=40XI7c~YG%qK?V z44e(R&PeIszkQf~SAVP6Ed|{g)kPMJd^b2PmyjsT(Gjg;K&KqYjf~=cfkIHt&Mve0 z^6+b=TAXlvWaM!$>KSKk1laW!IhZ~hnTH*AX>d&lCAKI*!P?L;y0l%>+q+`ZyK*C- z_X9hlVjW}tx;6@C5&u;HfKGcUr7iM4~GT64QCO7h61-rDx~xAVUo;^`>sQXLGezq zfYStcR@=a#A__E&QVbVAfBI^1JtR=@iY3K+Ut884Awy^bc&M=&uJU!H;OQOfYuEA_ z?tY0B=UK*%iXP975lQ~dslTkTL1boP8ll#SYaVTrHVU7N9F$#cOtlT1`7q)&ThKDx z!KBu|-UFjUIsoO9u)`swW$Y&igMWovYipOt{8Flc;k%jG@V)DgQNdKS;f&a2>FZG7 z@HEs`zbPdM6%}HX2II|kE$Q*}0yL0CnEG4iqW*{|LpRrbMat5cDhkvEd?`7d!gU@y z!AtNrN(^r-`%FXcp1B|aOWJ=|=oYBrpeIkP2_6gJ&wL=4Lkg1zp^7%F2?HWVGvcVi z@=hzYecU4xp2+jbjn+l?jPQ59Qf*i+M*W06jfGLJ+nnu8_}uOS$LKiA=G59@2f6wM zX}8|7`ANF|L746*KYINF_&Q(mC1@y0*`%J+xDlVLo*(GV(JOSePhM8rET!>%@Bkp9 z;$JUlo`9V^8N8q@q9~+{?@=RUe5%iM<#4uuk<*V&uiy_PIZx@ot7q7T?u1G6B&Xzr ziCiWC%`+}WKWw37693Kv=jax?aP9i_5@p-&F9lnHnKO&N5*hZ>>2UYmlYuWFcX>Kl z@v*NeE$!~F1wTo(g6UgmN4?u?{AZ|F^Fq02j{6zJollN75_W0oVFl=40d@mvgW~7O zK=gvQxz&QF=2WgG$mTl1W%&sllRY!4ik4XJOUuB+rp+`!4IbXnfC$9dgfKDsldw22 zL;B(D{9b+5qI=z!8yKiDPY?2cT1K5bbob6({OMeFwO7P=dEu(7fAY@I5~`w>51BWu z)fu7C@&$HHbpY+BUt@y<`&J@kVz6pHK+1>T)~kPiX9jy;&W;X& zW%_{2R!CuR=nzn~H=hOOE^eaJ&CrX2?YhxZkI_x?I!w=if<0&=Y#w)@8K-c(zW$=y zWcw|kw3;=^a*9AU^nZC8<(zx;LFc26v+E$Uagd7y-B@K!i~WxhBzo!iQP9yplJg`A zPWeH_{lcZwB83{qVbkS$<<5poD1){ueo*7CR3bvPGZ-OjeP=e_w;*?OMA#>F-jy07 z|4M8yLc%EtEpJ`%sb@aljxEWe?1Gk9C?)5sUNs=GzJKrSjdT@o071$gFH0UU4m#40 z5DNoOoGgd!K-HlL{bTb4v;xUkJt;IKMbA!+7Ulu6!_Nz~$3KC19!XaZEK9=`QuPtQ zef2vRLSu*jE)W)92e9B!^BWzbfJ*OcR$gHv52lRu&W`Dc;fw=oYaPNUx#h zJMcw>jGB>^wGPt2OgTX8Gz6g`lDZ9QB$vQ0VF^hJvkm~#II=D(3Mh<2NT3!Fi2d=# z8^>+tX5jG6`0G9s8N?mjZ88jc{ldT9(X~g+1Pbms+14In=HfNEB?wac2M)Bodpyf4 zw<%ekQiU|x0<*>~4q$>t;xg4bb2{>M7L|DX_{aDK2iI&7Sc(ujDrVjs$tG2)I7O_< zCunCg+3p1()6kUoCCK*q%x6VFDooJEUmAZRvC7_ik=?CS@dyGy-8h#Z5`!f$-?jqC zO6Tv~p+%tAlwBpYU12^%$|?xxk^USUDyFNkPde*t;u)*v=G!!BIxZJ3aQ}F-7xVP4 z>NpJEt)Ts4vI<~mMYLC`@-2XaoVFJDO!tPn{_^Q}=_mj;EMmkBwhyQeO$1DRCI9j*_-FSEIM289EWT0PB2?%FswzDIY1b3~ovmT;^LV0~<@`t6MrgKS6U+R+AGvnbHx)C;2X$fLD}pwpPV59v5ML zmU~HGL+fNKwLBr`M2?wkN*%Om(-l2@C8wEh<$ZWspp==wpH#ETp23_1D96EeoNL*Q>ACIt9RS&D&wq!*ja>Bc&RXim4j9`ov#bVISZz%XZZmX z7%6Ab>sW<#w8e(mtgNizu*ITAcjz{I@yPDg^9_+?FDgaZH~^ zJfn}4!H*azRQa4_1RiO>ZKgI}F6U0v2`jFT1Q8x|olk*!65vcXyf1g0UYvZD?3qj4 z4kAA42eIm;pM408*>_&N53?3W%MsJ*J_1%D*Y@Dr?Nhz`h{Az6`*NnVt~aZHY6J{3 z{q>Y79HljeIT%HahF7N3JVwEKPPgH{ET}^nT3vdwv{x z>+n`-xhJ|cJV~6fWo7jXFXazb4mRRbjAP7%o~XsdKs^RV^Zu-89!ZY9)KHBX)@~{5 zV+Da4<#qKg8^2nVBXF9&N=M>~^-)vurB1#io?8i-WgtcodZ_+QDFufood&Y`Ise>! z`WTz{8j8fch#cF3Ah(+Ov%PC^h@mBDd&8sM=UJfc9&*OyOEes-Li6;oZde4As?}nr^3f)d9J9T2&aJ;KLZGSfw5_XSv$yIS zY)p^U68Y^tuV(=|?JcA*m|Tslh>D9u*Z9OLY{&qzp%FY|GV*fwC#;)D`psyHCsWE5 zL|q!W7mrzWpR;b!a?-KN@H(H(#I{`6CZfW>QEFqlyqhkVg-7)cXRJk*_ItxXlk#XQ z{PGenPpivI=`A5?!(tjLMR#wn=rXDLTMg=Os}63QabktH3v0O7JvLt=?+<7+rV2~m z?+90tv%06UyVz+;(rlzb;IHYI;P}^0l16rk{xc-75`^A%03M{2^Nu^wSq~2iwN&-Q zp8?&rBmH*0^QD7Z%>=Vt!|RINzxW)1Z_-?IA1Igyftw4eVgK~-m)nNx;21vj^7_{=rxE07x7aAiF9iN60U zLTD~c5h2>1TwZ!7a2H?}`r3P%spYwI9r;W~{sN%GO@`CMr+D=nGzw`znzuw<EmhJpz+2X*g#Cg5k0*rmk5xb^#>&x6n! zfb6r1x(e^#4Jp

Nu2l^!Z3mN^HN;;RYeT2xHA})F!T}cMFd8GUyFv{XBj?w9ynE zBBC_^QFC$%2*Whd#_`r(8RrIuv}t?%^$Me`uhyB5qGQD|1W%JOMsq)4s}3V<@n?-vjFLK za=fDxb3}G@n6`lnp~QJ&C`guA{aMexeB>=}69m}kFVw;oE&bSr=5t_hx-siA8BTFp z2k2QpyUDPjLaV(bDNZa^*Nl1VD%c|Qy%c(XH;`V$N8QXTdBMHwDl-r1=u2+vRVs{p z=$1mENA_|Y9#pNfG1l0vGXgUY9j-6$P|J^G_nymA!B!So98i9p`1BA)RKYb{E8=Vs zd}&$Jk5ZnliDMV8zderAXKRLqm^B7UeC|6)6X9mLpSh8@Sa2lgo;G>}7u8bzK>t81xkJgN7Vrlek{IKWz^>C<}` zhd=TDNC)vpu$UsrIRn6dqXQ#w+ByFy1b|5u8KR0)Z8{%(CF2GH&0 zd@fI9&|8!N976P7spgG4v9gP8B2U;PFA4sH-sIma+(mU9@G)21W#&gY#8^O043QIR zO4y9`k|L!rgV>^We*yi)9H&)>UMd@-X)LnE3-wzdBCZ49jj-v zB(%d=aKg3Qc-E7ixgR(^#AF$0eUNIlItX0QtIYfHh~AE?R)0KWzXSNS!3n(vx{Q}n z+rA5auNMIn!yr%?y;7nPbfdz@W5%Bki!2w0Zbv{%w0}e*hgp~&2THnz;iu~xihY-JUA^-~Cz}QCE}fpGUGKvt zv95M25I?xIP|GbBUb%Qt1GVi9tHe~D8147bRlawzr=>XZ|8}NS@nJY$f+#ZzE5goKX#p3jgaqbZS8a^NFsDS-ldnkihdDv#0!8}3Mf z>P_t4XJ1_@qr9$9#u-DRp5`6gfBXvrTp%#3Ng1~Juz4Oue}Xg5Pu{z7Fe;XRXmPDi zn}1kK%e@e?Jzn?q>2q^)TS6-MthFuoYFT)2P>O`)MJ&$v@NoO~}SUmw+=FAVisy}v8PYIejdvzty?W~b!E;*|>4zE@s#0y?x zE4NQMfqtsQTaNq93CV)X)+sl=y}f4&7(nf!!`ceNt}deFkoI*uS*O5KQ&Tgl;?AR| zcg@e9vx+IQPyyH%AlUye+YE26>U5AU3ewIfx!f4v``<;LAZ`N5=dA!DV31z@b>)C~ z;>ND}eT)`?GO;=_T@<=2r{kXODSYivXpWE%!Loj(n)jzb*$FS-q_iHnI8bi2w}e3J z90jB%Y~Z!*0hcJ)CDXwW%5&vP$(?9PuEDHoR8i;?}5Ef5X*3svwUwZXBOF+W_ZEy-!tlEXikl~KH=8xfV9ceS!%Kj}Yk zE$HMWtL5e@-*-7#u3JHPc1nGSf!%Qo2K&9K2@4u{3N)b?aPa=Lp2%b^$ztjinPE~7 zhK969jeu?;-3ucAKsj{<7{`1-V9V3KAY(A1v|wLPUl*iH8OHsAfj$CBD1paG17$&n z$22@cgpVJKx`xpTT;LY^nqD=TrxbP|WZpv6+sBx*X8Tt3gQ|VummT66fV8y=waxWy zF3XcD!B;cCS@+1lV@T5S(aBewgYzbRNkD2YNuUHnK<^9n|bNyBNz-JXNL)3 z4lOM$=A*!f`)1!8!+{i5W7dl~8hO*Z5P3{ZqaOH?XUo(e&2?>SwuEorez@lv@XY`HlAjt|%*zCJ{Qq zEO3r6$+SRK8RDz%o?+G`T~G`Spw@ALO$T?JaCbVSPExu}19_#T4J)^1>{m+WU^T2$ z;mR~wPYvvFEdszO46;&LBv5q6r;}z%eSa5R|HL^(_)clrK-7RrSPrxSZ8cJ^C zA4Zlm&0`6yU|I#q1on3|HI`xvd1C7zkV=DzUtNXjL;)3sQa_pU&kuexd+86_wA>M# z!P_wHZrPCOsQgvYQ>tg?AkSRaL0)n3o+(11U%hjPaWGc#*Z+Kro0I%m7QGsG3E}U8 zBka_BZ+!?}UheJL{Jbl1)*56MN)?-qi7jED4uIcsvLaz{TkC{fv0@lKi`9%+SMWxW zYAbZ+DCv@5ko%g|`oLenMh5V!36KbOfq65B<_n^3qyhxSoshT;>RKl_DnEhvy_gt} zf?1)4LG4HRbFRbud>74!**|~~7>7kD7$GR=wAf1QJ25RQAnen*bvV+CA(==VysG)) z!n-iE8U=pe!X0~e==DyS&C&-Y0lb!(E@uvnS zuJ(N_zxxY-{F+^(%~9!q2Uo0?&?KRawKHSx2+Z@$!)R-$rN_S8!WcdLO%u9|jYbO| z`?cFOk7HWPx}QHr{j%76u1}fb$!lqoEM_CW1=#ESWI~#bPP~V*`ZF;~7bc6WqUWkO zJ;y~t^juet?a-3zq#gNusHmn~QY%cBLf!wB??e0PJ2e@zUs`n38`KXg-+U12IQQz+ zhb*@_-7O3U@J8s=Oln5$tlt$`tvxgK>ThLUqPWSfjI`c?~`sj2pd_a#`)H)Z|ZzT-kNp@SJv-* zXK+V~qG{l>Me%?I#Y`tp&einHpa_I#z3V{Mbh$@7+1z}@QhI*5LItE&^!EsOrUwC| zspBPh1=L1j)h>oGnzooPTG*78Ux%anX3dSXaz!n!)_M%N8tl2uM#AnnQQy3aIyLDq ztyj&=%!)Me>4!H!PZH%X?lD!qN%8YR`s-#nI+ zty;XZX*V9804+xS)^o(5LWiq~_C05Ezb5$c*Vh1`!9;vP*zp0ml@gX_3Yy%~em_aT z_gX=gu`;wFShR=;4bHdtAKfO#*4%#qL1YSC1CVPb`YS0w;4>6;nc~`RQ}%jb8K(xO zkY235lf2@}TYsHIa$?Tr3-HnTl?*3kbtu^0Ndy4xKI zlZT@|JDtX2`dSZ~fQ6W%e&ycUgAw%#nYE_i1)sSBxPnAFX_SZmiAv~bC%UF%-89^$ z@|It&3kat7ksh#a`)+4DzM7pO5T6{?9na602-nm1Zyx?1dU%Md-e?Q4b^t#$^QA9{ zIsU2ROZO!RmM=e-+5BR_XDWA=enIma=`|7wxLFDSEt_1b`K`X096uJIJB0E?XVp7w zW4F&~o1!}&-=3O{v)Ns+s z*~8J#|7_latPf1IDbKp5JZQ<*@1W$bmPT{`cndv)`<2dLO>A`q_qGC$@YS zUYTb;U*a{wTwY?&a^S_Q&i1BK{j=|0{(UGK?-JQO-d%&I&i}in=@18L1b(I?rv#us zfLD_ckLHoR*DP$(^AG@W__zrwc>WM$!-{ z>l_C|GZZb(?Tc|A4eP1ckKw%5J-7z_4T>+i^5>lbG6-mk3&C0OPttxk@M^%aJH;ox zV0^@@)WW7WB)o*Xyl;$DA**H>xN-8ssv;D|1*@e-%uTg8~P^%Z;aO0 z;Xb6my*nr7AAJDR>TCuECSYrR03khH{|$~tnekfP&OpNJ^c1~!U0f`!-xu~SZH|ie z_he)kl7liywp(REsQ%>q_hm3_JUq@{4_x8_-2;erlfd?_8IcS`lw5HUkOaICa7FWk zXr>QLXlr2M5&KSs6pS-LmYm%JzK`n&jXQo7eH6}9Zgo+T zBd)e#hyM|!&R2%K{-3vOg(OaCul1s%Pod3c-@o8dKI;g`ZApbJ=u}dEIKhn^u*8${ zSyle&-I>!~>$TD+@_SFJ(r0P^Fg&>B_uuF8)lI=F_(NKQHVQmPeQQKA3}&&w5L!b{ z$~qWm`ds)~bbmqvAKQ#%1#j|+vCPt0L8rOe1_~-aT%1DG z9T^6XXb#;RzoeWZaqJs1m~IK~!iW~Z0fV9g)`3gtfIVAL-s=;SKoLe=E!>jH_Ll8( zQ)vTH5_X`{_@BPu`SQn?r8eQ9Xt8_SXi5`F`BnhNMoscbJo|6oG2fM-6FB43o! z8KRpT;K9-8GM^ zvnBoB*B#^8|9ggg!zJO@esxjLZyx9)Ye?&doZ?7&N1YJZxn_`na-gbBXUi7voN#GZ zEBWouuFw4T4J61sNR4fU#Q<@r!NT`-#}CH8l5$hX?#loCmgec}@&em2&fXBH z3x)Q=UPiFfd+6?6?SamW#MwpevwS22$+e3Y35Av##evp0L%5B^H3pau>zzNt#r2g& z0jBz0Z%)b>!d}5Vl-=es-Tfn8BK%yo3_xBxpG1@((mL8&kad&Ya4Co5I}hA3Gr!QUwVTSb2bHmJ-2{q=I#^%gXcu8Lu=*T8|2J-;*s>)$5@_^D{ zAW;M>DN7qqc}5CG3pqkZGXeKimS>i$UBf^x%+?tXG@yykIGtss=yv<|>sM7|2FIT& zmx{0WV|;7OcN%9-u7c3QK=_p22q36VZpe_Md`od}9ygg!UXnRpAM}H=i$TsctV&l$ zKbDI;yPbaYj%tGf1FzR$7Fl}V4USaitjY@Qq<1eR|H@K?oQzyZPl+dFzqb%UM4*tL zYZc&kvGd$G@YXK)VYDnt#Fr?n28v+%!5i{^$W7vQ0b+d+J!){i&JRIq>JT=sB$Y*@ zC7^dsOf0|c^KBJ!^QkcI^W)(e@Y;dVdU1}?hfXn^>S&Ik2umzXjVJYln> z7`S%8)>oexsuCumxC8d1B^Vk@JpABFnI2fSd3Ft%qXMmVmcT1DJN*$=u$0?)d_VDN zID@-%xK0Vh|ipQdz0{0c3`Kar1xbR+}m^nCPMMA{PR<__5bM2Z*_>|~K zet8pApC~WE+Sh!Cb{}l*QY~(5ysb_BN$8+DfI}d;1T}IgM;r66?8SxUa)SWGM_Pdp zaNdQUb8FVE(n-pR*cd_MA@Jp^QUVhzRBn`$u+rCQT36qw5`7hij+4?#WLSn{UtdB( za%U-GaPvA?3``s%V^8VC)|ecIJyWwL4gnB(ri=D=&mSwTG;g`gC z(Q<6&uotK^`*Iux*MbUWb0_IsG%Nt z9w@VtUZOug7}qfM=29AvYdOtuWQMcVPgd(we?`yEUCEl94O_rSM@~HXe<(Zq?e>B+ zvfV0d6;8Bqbs|wSGUIAtmg3e^x&7HI@q^A(6 z*w_vcD~gM{)`zS|av=GJSG%`NApHgu_vWjMw~^(YqDq7LZ1!g(|1y`Juq`>!-7>76 z|J`Y+HfZ}0mJZ`8<4HA-4j3_vnHbq1q9;9W#b;;5C-Zu2l&^r5CO)4U9HDo*g44#u z(;N0;Qhc;l+Wi&*{1-A^1U%4%dl(+3YGO@M$DYET-&!}y5xw>K9iAR%u*72c@G&n9 zrs)6HWYPtHJV?L4X&@{WKztX4t}{{ZBX8aV4_S&;ziO4$OgW`fk8U@04{Y}?Z3tuatprVEjWNW?*Wy{pZSNYY5=Q0Bj=izUN2^%c^c5YrKPY1{Gah1Domzm zOnDe6HPG%p5+>N2&f zYmgBrpL`u7%Os%T1dTTv-242VrsvY&5>dK$TA>HqE7n;u;TNslr$l6=;~$4 zp9n~f!!)J_+7D5Op=*%3_bwHlLw`G@$Ub5R(KT?;dl#EdFM~C*?ac;stAqEPwoufJ z^mSAMxJ^K^HnDyZ0!~tO={M^Q!6-ZF)%*Ctm9-BM?~{q`$MH7Z!xcR)r6*EugA%Te zvxWV-$5pGtL+3)NZbF>=WPfSlh80fX)Xon7rO+L173VvjK7Hx{(;MGLwAKxaxgQu` zM<^QOa8$HG6R4khnV;SK+za>tnhqR0FN@H0K=41I!1--?u$09N4G$CEzyD2g%CL!~ zQHRw$)q4zH8+Wh?C`|k>9Xg7g4F^pc7+a4!`*fkaBh7t`F)iAGdIz2c_BCFe(Fw^U zTf37m@~}aP zv)W|=wgMVuD4#;<`x-6({kq3USZbKSJEb?KKYeA86gbm{gL9Y;SqK4yxQ0?K3_jz5 z^r>D!yF>6}h`+ZRu~6^;>4+#q4XYrF$X=HRks+X{k`vEB$pBc?Ct99K3j-w7q+{ftIfrIDvKCT+v z_kX-sTYra?4&0tN>tfQGeQCq<3OHgi=fV@7cP53D^5c$;`)A&=)A!oi>Yq6{{=D&c zd9hRaXo7j^wxU*NGwGv%1wQME->VS?N`3DMg58R0`kCK^ z@@*rnjnpoE{5ko(H!5qye&_TWT|#{`VS_Hu;kTAcPx2E@H?Qc3{G@^`-RA{AgNE9L zdy=&aldQdUlNRU`XajFNFAV5-y1b9AUF7-&8mf^IY{*^BMc>wl2-op)m)?B7)14BdGHuCcaZIw2W{CsiW9(m3f#Nldu+6yPJSo=CSib3!fFz;eRQ z$NqQW&z+Qvq>`-V|CbO5fC~@Ch{78Zmbm)V=UsUcdHds5I_5+`>?M7ZFHim_-<8Fh}f!f*@m0TE$8tTWslE#a`=M?#OcJ7LW z3I9zwkd3hn+qHV<&mCj_%$ueA6(q2e)M5x*1s7Y|qi6(8v{PVK!h+W%8}$T!(ty=i zKMO1$3gAb^;yD7ZS=eEtxuC=`Tp`t zpOt%gYhG+?K6dwVM~SJ_Q=(a4TAdEf)wb{#IwK)R8AW!QD3&+J#3?;pDZIu=dyT~7 z(LcX^`(`DR)H62@B);3;lbsFm7Jeus2TqWz>+BF(6|?2F1(>se#uRX(^Ou;xi(5r~ z1g`AT~e|Ec85QOSjs-{4xomG#ntGnrz zdLX7{jG@4KwPX(Yv{SPMJr6T{KS} z%SO5sHqS`ud1u{y|31zu=wk3cfxquo-deMz*@1$ro*gQiYLD+8Z6VQSdqsgtlvCwz zPy1?RAcI`8V5Ts@J%RdV1!&V1=pK~bj6-d zS7@J|0rb}{B{LXRjb*nEziH_5GLOIR*evw920yN4HfP|8uH9dmp9jO5t}7=1qnXXE zg1(&bq*3W>8cB{G$g9bS-bHI(!IP!#f7bG6Xm9%Q-Zbqp_JdEuk=}w%vk$O6aUp)y z6{$BMBL8$JghrFuH}ws`VpG75AYrb-**3%l&R&*0Mq+Y=ya3lX8n$>ZMHGpKlISw^ ztH3!kr*sjgiXxFpMnd!^N4zky**?mGiwiL0IQe`cYU-zm7rp>XI+Krz3on zo)(=+04DYD-_!ii!U3L@AAo)trH@fn;mjIn(Lo_D*e!&0jqc-jxtx-LJ~dKhcxtl= zYA4iwVrB{s0Tk-ssV$?=430o;C7_ii`2E;FaJAj|r!wTM)NrK>&>l_zK-Gn?A&9{; zfTW(BXrFC&?He1AC+84$6VrNZdhRDOh5+1<3K&@?t0j2p{=V&GMh02BhU_sTVi`Zi|Ksb_vJ+v+tJEmfZz&o`sHbK^QMrQ8yRBi+J5dvgYgf79yodWnK zZGM?rRs}`=;01NRH$vQ3@h{huE`FFi4>T~7aOCNKX~(Q2<@X@HvVw|S-PHna^roJcn;yy1ELUew5&Lt)uZNHZETf8sVuZoqov670$z6D?V_eDB*K$!CrDWfV1 zN!A6V8m%H>!?K52Ij(~MVI-^m1%AIM3*`6rQhOl*eKLHWyv0QGY&yY~+~EQs@dKm9 zg@wMv3Z1|+m4kB?e#QG~ra!ox=D)nGjA=Y6W_T+IQ$7F@U!}Euu&pLDNZv+(e9)K1Kj?XZ z8u)(~#`gSanQvg3aY7X;a-%n_`syiB^8BB7As0&0l-Zkf^OO57{0D2ZnoE3Gz6S*mt+c5o^7(Uex64l&Ra|C;iGvYj%w zeKO>?+1|M&R%0Ornhi4wxqcdk7cG=czcHYs4iV%A-|_JxTv$m%gOD=b?bA8FvIYqPtUi z_r^|;Dlc|Qj)SJlEj_Fj97Q2BJyVNOrIg=EqD`CJ24NgA|IK}ww|G*wU_gc!kG9<7 z;83BF>Gz}pB{Ge=9FZ~X-eHro2PiCz9?&>0fgRfdOe08Kq_<`0`m74FxtZwv_p=UF z2mv+;yVQ9r`ZM=ZKOWkkiWLHOdJ@=$PFOVA0W0mJK!;QR>P0&;~`~@V9{#<0b}NVB?sxG;Jhh;nUMgIT?i3|SYve_b}_5q^tf%1>jy)} zM*(A%TRbynGJMM`XF3*?z+GDzqV#RcNNt*y^4CnZ?N5H{BnwN1@ynccZ-fZ>p6?3w z<6&L%7HqETRMvB~G}KMf`*m}KK9*{iJik#$$kj7U-K+@~->uJmSV;U|$!nbFpP~;1 zD2Y(32-J9w^x-X}nGqCJVZUVO+-2VQF%C?)rE@6i!Y!TxAm!Ip_SJ;Z|XK%R!ty3RSiDJ^*@T~IHRB@!0y$RHnd8^*MIf3o7oV@WgBY#xgpenGq z!J^#`Ytt@hrdnyd0{&sYCI|~-Vbq0Z5;T%;)dTrU5VFm@_gN!KT`zEhygo>rFKka# zW|;OmB=t;gZ+47_ij@=-6VEIrpHdyH#Poz zi=WVyFpUxyR>DFW@72Q`iThR5vyPUgsO;=ktnL^rW@*n#V!W2~E0LzKz9KXznU0p}<{D0Ypr9(t)oWrf;mK@>qpRv21{W4alE_WS4F53s6Ji6>j$Z_(y4-^m zI|^6Zl?9VxEwi(k+YsIO{re9T>Va}avMy+y{Vct?_iGKFCastsp2|v#Vsa+ znVy;d^ua%lQ!%pZN}}l%8{d?kypY88IPQFVyC09%_SB$L|03+WhgQV zx-q+waqJ<ptodZ*o1kyMci}&6yzp72nP15(Zc@G3xDAgqxoYpdrfa4X z3px+!0(aD=`VP%MdvO0`qOX!?A9p)*3xi0Z3K*n7UD&xmOC+>PjF0;h7&5Z}batv4 z$0UQ6jP&Zs7lyJY4um>rrE9*Vzu%nwCC!g;%CQq&4Ko(OAutQT`%n*VLQ?i`OwELe zF;{_2c#d6skR$clb_0uHb3Ivr%G^``qV*V^2IA0Q#xFMbt;#0*so1VWArTSjpgB~6 z=x%$rc*vwM6TJI-F1NhPYXY#8=#xErqH7tp(R%~6Z`;IV9s@83=(aSn&hkESIOD6| z&(EZ-E5c>;1c|s;E;hX+RjNsw^Pmc4b#>Gud>Q;g?0GKhv7LTV!#&G>i(AV<>Q^Oe zTbNdS$mm6jzTSO4Xh;wN>R0}~UlYhQ2Z^6y;gLZ76uf~FVCCvv>-{!T$*piaUR)LM zXkI9^y0!J@EGF!}6v9-tq@@D_DWWe?Hy&D&`k}}L3Pjr!0-H|!^AUfYSArXGVu?fItIwu>j7DUHm%(kc(j$2KF0U; zvsm7&lT*)tJR$%%OEv91iCIrvdzS_ah(u1!f9jVMa+qX zuMb?`USOyv1L+Z|@sSxHcxG8{rl)35(4_RXE7M;Qt4%aI!Y&V2a7CJzG=LU(OPWi zW}sa|)GFi)fc#D?gA%}UhQ$XTtF8x-X`~Wl1P4?tLC~Mz!^{EmF>#I%QBzX`f}xh@ zhQ`@bSRQZP-Eo{CV}1+R&LeA3J!XJw0j6KaqD*eT@US~0DTldATv}R5UjB6Q<9U3W z^EInx4HO42tN%1{ctl69to5&$*;0_X|H`IK(hd`SqUe2O&eeyzAJCmzb#2RcZgcOc z>6p_67CHkm3>BRP3~#NM;~+0-eavNlunHIE`OuGGX7MEtB9MQr#C;lC+Erwik0tw% z>H8Uy%Y(F#b4tm=ke^?Qbw6HNI03pQW#AIzfTu?mRGA|qXYp~7?hFkvw?O}kTwS|+ zN^pd#hd?fmRJDu!`M2OA_5g{E_|m1;7PKD1$7EDEp|Fit5@`NDi0SYRdLa8!KX7qL zzWKCT8(Csmdh%m=_(16*Dk^H)ohFCeccTn2^D6WUtqO2g*Sia&a-kW&;232yF_^xE zV?4${E^JwH8f&gx<{MaSXo_V zDIRT9A?C*OZ*VY76nqr5eSZSP$raTaT190rV z^?S1}hziF!*!__E%v5$$+r^yRI_3q|BPx}arH8O09a50pQYtbx3JqXvM{c1&Z7bn| z%(CJFfd0|+Lmd`QLR`%A5oiDi-_oPY-LxnO?R19h9^h)Re&FUUNq$!9jU%~uPs zWRu}lT^|1!TzeoOHfg-};HQP2#x%f+hfFLM)*HqW#gBihTPnjztoThlPau}GRSca5|7?i2 zHdz_}#0&4JsWnF+9g0m8qp!O%OyUw~o=nhfk#2sBI^An@idR7Z17j?p10e_9`Eh#e zcy-ey(57|6yMEkV7-{JN@r@8hKY-<)*Kir;MDF(|3UHLBh(Wp)TAFfF69$mT%?N;0 z^1^_HI{-bQh}K5M>8^RVXPiWIiFzk3i`i zK%UX_aq{2{pr6tK=hYS}O>>|~%z;<43@}~7snL4@Xx`tzc(ntx^|{E08O&=kx3dpp zi9?s(hp03VAcrW@=lLCr65qa!08|k^8I*(b$Ur8tx^V8a^=}Qm5j@&Z(gNSXI1Scz z5LKqaGyJ`}z9oiF1tn+w9wLfCQfpe~BA%;Wet!PaJQC0&hZORYmnAUwX<80$oC-3l zGB!?OU_55LDSSrZG8p=GKBH{=N&&Kk`GbA&HunPK#-xgLh zJK_n;ivSvXLnNL8P$HTx>AO)89z&*uh_}85R;1mZN^o4QZET7`hei6PB)$Vg=4r29 z@tOu;mK&i$10SZBMZRCww1R(j40c-|y7e2?Xh@X8YL&Nq5mKS5z86&F5KH_yxazfh z(>3rs1?x~^l3ZDp#k@UOGDpPw4~Bdgm}wJOkPM{8!bXf_rLZj8UqFLh_?WQ@)TOcq zpIK|)yTqAct7(NXm+V_yXAe3vas!SSJ0X-+GESSyB3kPl5!-rC&d?&G4*f;a@r>#! z0Zz`IwNnHznBgCOf|bV1sI>`)&Hz(4AcZ_4^TBK(eo6J6v8rAc>fV67s^;dZ{-hfG zg+qO>+`<yEwQ^_ z;JFEGeY*l7HL56W7ztBAAaN}nvz_ikggaM*j^kqOWAJND@RX#o+y9X+->FN_ASa#j zY#40z)=>Kol-5xH!flIazay@ZFmnnfM0WeqGcoq$zt0LlbPaI|{m3VG=B)q8`!Ewi zyuqBS(V-9j*FaE*bm(Y&1So}Ub(k5E-CH*p&k&Q6_Vq_0mI-{~c^b!7Ysp2F@~t-2 z2n>5zE_3U?!$L0iAieQN4bK)K4BVa3lM6og5VbwtRR8JY$8Rtb=tfnWaWgiDIJ{WE zw(SdujvmAE$iT}xXn3^ewy?0Y-n;NE!rcE0_!Wo?pR$j>!Ujv?z7hmX?7zvn9BY(?h+7M7#WGq=y z`4DjPV$=6_$9Zc59DQfeY##IJpHI>_c#7oj3tnmt%4a%9=>Id~D)sT#iQ`cXRiZS+ zH8Ws9?=JLd;-+?b`^UN!>GV`t2PPuOQlf>oqL~4}NRGh4Ai>9d3|55ZP}B$?@lb9b zjCXL^smFWD^i4t6P=I|`eXZ?qYmKEF7pqiHhxrlkHTs0p*-785b*-@gw3L_+am?f9 zK;;@HCr9!7c2aT!9NT8_42I?s`X6ip2AWbnx)0DMA^8<2slZ zD~v#Weh(1JUFgd~8gy7v&Z|{_E^(Z&d1m0Lol{sr(yO!g`;5J$ zD8ZoGoeR0;q{zf%i29rb1zJujIzotJ<&>wRzHevl_&$wgO_u(pq0k95Ct^pU(5vut$&}pD{ zoEIL*ZfL#F^M@NAQj;}iUEe5e8T%^a?DnG&OmoAR6ej(CF4)35NQiC=rs=kT6ClrA zu$GdOknl$8c5oAB`Zfbz#m~XU`>TM9nod8AzRE}aVV8vRclh_`DHTaB7!R=FGlm2l z>YzY@3kd!IAz=976BpBQvbEyL?A&P zc~yrv9~=vbww(7ijUB4B(KLsJ0ttCh8Hv0qP>v%hMMev#_JE^$x4d1ibi^(S@rGbn zS1DfuspiU1mprc~g+24gd(~ej!1EsW#e?Y3d$&|Uei$#@@Aa~(>L!XeMsR^K_e3uB%3wM9?b&%YNxyl{5+6oJpeqs0p)^f<`dM(Kl7-pQ z+UF|)ie$uy_*ndG(;A`*RFKqv*rK<~YRqY)2eLnG(f`h`AHs5HER9_{Nk=Oi4}&f} zCvDKF0S;pBbnG~maN*vKUT))wg^Y%jW2_eg^6E}IV68!`(-2C3U72O6QcfKO+1r0Oaf>GdrfAqQ-vDCT)+&nX})W*Mf?5zc{l^v(40Ut z3riZgA7Z5v9tIjt3nDBa$~00J@js;RD7rU3^Ph1Kz;?>G0=iTO)2EM{y_vz|(Ckv_ zFVc;koc5vOhLMw4$Egq^lKwqO-EdgE)XQ_?l(H(-$$0GtLnYEsLD?iruvC2)Q7NtA zni_8^co&3c``btQm(IJHKIeyLIl$BOosAau>mx8wNW0NFyE}_I=yA`FY&uc($+tCw(Me zQRlG%%#-OR5hK}-=?J$r-X6}S-O}e%KI`S6`ng86Mh@5G?a?zdNJe2ZHspSx`D4w5 zKOcir3y|+M58J;yacR9G_ge|-zrl=jO28RNsh02WiDM>H**EjF%upEu3T!9Q5nY)J zM%t_Eljg147uG)bWD=03sWrNbW$J3hOY49g?581)uUXo2&JT`=)?3ecIu%=wB`to@ zH!m4vQB0`K9muHpB5*0}+=ZXc-h=uwkkN6T#%9jzbwv!#r;T9c8U~99R=gk74?RMj zm35oDcN@w6Q>Omx>@g+aFKiUi#C_T{z=@aH$H(OKQJX<&jxug+ffcN6+>djs@zzk9 zbS>_t&7mJNznV|(73K7`!UKsP$sC>kAWjzuQ{n0@*1+YPNAqy7O+!9RtGA zx0jQIawh$Ps$K`NDoeEIPoFaL237-m23UXO1UlbUwwrT1Lf!#Q?(F$QI8p_L6sa#@ zWLDbO58~Zfus!aoa#$VqE=M5@C;5K{X69?HV=aJMtQ!$1VI@unqBO<~5=^^FkN;|O zO=(p2RToOT8$gaoXqh%+L<_gY6>lbgg=O{0FSHiJB8SJ>>u-~_aqrZ8yXqtiVLOp+ zq<=t`v8?F-#VD@>D#)6?nHo@sGhV+g2aKhRR_szn!r>I@&zB#)Bt$2zm_kE+Wrc<$ zE4RY8Z+)P$$6}1d^8NXKm1c#&nZ6dC&d|$hYb&dRnzKK8EkcOZ_G%iIT}Jd}BDx;E za~`ak;kbT1e=y+ati6G40oW^9f;v?Rv9us-GuXNT{Lr;gt$gMYEPds=L3qwt9-(-_ z66@E?fkH7dSAj_NoN6(^Ij50{{^tA{#1BS?YWU(!cN2;P1MTYSlv}}r{blGQxMfK4 z+NDbRna@pyK4a8t;6XkRd^Pz}ao#w5cGt|+L`vv#vJ!`o-|#>4z(XI^YC1zkHp^0c zKko8kp@Bu*krq=Y#zB?UL)aEtc?wPYI%jo6OPq@pVqL>`E4_Iv+G_WCHu0K%{vwbh zQs(S+RC8b)i+j_Bxi$Xz%cWpVdM;vzWpD(dHR#pL9wiWthcw^_M?^%x1$a$JLBc7; zKmA7kCun`=Ag;=))S^F6IGFZcFDP!(!Oga1#10e|<*~G=64lr>5`2L5K7TM0RlcBx z(JG1A-BVrHvQ<5Agkm|im@(6ztvx7ZQd<2VHf1`?o;iE0pj|%-y3S(KCbi>sNSAeL5nFaDJwKGJcp458$j+?V#)9MWO0H;|W% zZq_b2o(5@c5|HUE)QWBqlJ962w#B4wutYB-Lg2^}yd_P=u`}ipA@9l1!S9!b-KKM;e zm!A3o^tz~YcStq9k!4LSvYCx7!;FG(x*(yyW*<7b3ZX#5@5s&F63@)m`Q(p(m(nCx zQ4@=SV8wVGi!H|}F6Pk`Y~wZg4Sha3Hm-$zMWt6TBWFW1^551#cbuHho+Lq*W(d}T z43Cd^u3(Nt{f*0V_}kgJkjLO~SN6cNnSw!&a027+Gnxrh*|&`B_xV+WgM)2B`NmPl zhf^2M+LQ?~LxVr3))^XTPJ;7=oA=efZW<#AR-zABe~(LKFWh%YDNZ|V=TKa-Sb}Jz zFET_qix%S>3eGqAv)c(Nz5aLn|7Z1V52jQ;QFmfaq&ijcBgxk0Hx2JZ|n?=#(#s8L_@e^|ipjeMbGKOn=d^T93lu* zLUsi5W}OF^!j25(cJ-n#6G$O90}DtcfzhT9Y5~3AU@QZeS}qX7zz%0NHWvh9$A6H! zru;|D1*_&)TwHw;kW{z zr+PQ^?~3X$UQjuup{7!jSQkjiBo zPz`qKbi~j8^(2^}S4Hd2{q2R3xsr!n{DI>eQRT-t|2i2xzaQ5S#XIj~7Ot!MJ@sao z=}+*^=vfKFZVliQSdNPqp4sRHvS!oCV-Csi-&PYhyaF;l0z1OUI&s|#qZ#D@f19;) z1;`tJ!~ob@$_*>i3xCngxgECn&?*RiZM&a9RP zFR$(Lwqo0;IVr~`rAfVAe2!MZX8+7tfr!iaCiCtw^3FYs(Fdzqsrd5t-~B$|D1@f$ z@L&s>KntrFa6KlZ|B5zP&v#9GqGYQ_7}AL$%ws;+W53UHZO{9tdfbxiZM2YE0So_E@1kN(l#Ky~+d zHUqI0+wDDg{q=(R69+(@<3q@M3pfwNgT?$>>?y4sFS^&fn;s-gCi8P{Pl)Cum~EWl zrRxx=y`vCXN_%eSY0m1$vsjZ7Hm!^Q(f)U&1{3LP}|CyO-rH@5C0!+l1ub z4}k7ji>o8ontip%toQKZKv4GNp&)`oB1`Y(ox^n0(g$+JWW8RNbrP0zqWl!}>G;U* zi9{k0f?^5~g63+0(wCv3wExDXQAe1?NLP@ zF}531Q&Y2`#ON$C?c%Us(nP6^IokXk!)ipvGfl?~XACk&eppKM58EvZW6y>MaAn-; zGbp}6SC9A7j!CkwiEO^eLGC`Y#XXM(LlWT7<%QW9Jn*zGJq0(ixU zqIW5<{&KUQxRHWS62Pd2Cd~>$Ji3t#cz!Zf8+@+^xX3WPMjmsaz zNgNWX^_F?n?SHvzg8)DDbRdLxyUd*tL<^Q-H$Em=g?F3W7@ zq1?+~w+KXwzL&?#J?m3F0XY~jnQC_Jm{YrukyYQJFLl>H0ybEDiWny{9UP-j9NZSr zD9cjSU#F0TCEs6G6;?pn_S3#Rfcb&7IMCO-a@-fYddyKaz#robWbv+vejMi z>{ZAwd0RO`x)?0jkUJ8!@$Hw($;{vl3`+7(-P!qa?JQwMeX)fv^3aONT3!H=I*DqW z*MGs>4mbU(ZSVWU6LmK;wDxJ#E1DI?e1DdtW-VKBAu#tYNFTQ$}LxKFL$8 z;!!i=Za4WT=^>bGODZVng_zumUv=nxFW7hs*cA0?m;-0b+t05D>^vm0nS3DA#!OK| z+`Z|9?IK7Vy_l22j@-HFw*Ku*nO`pJ={X^*0ymv@K zwZXut+m~%R8^8KgWrwtmv3Mxgk1Ek=?ivSuT3_kHTL5n=E{!B~&J+*!uWuQaLhcjX zP8B;3Tx9vR{rn1l48bLbyy7rQMJ@_OGbtf;w(>{Oes_!Wh%cKQ?(FPD_p}ALevMSv zDu59?q(w1Yy!>w%6r;Ef--S)0FKsxhn*LAO|~CT z-7RBE$WYa-j|@}yGyAh?$K5A-ATO#5IxMOtO2Q?=9K5SAu&Um8gAFrn355X;J(g7kz=90=HNFaBe?r`4MrB~0rtYt*07BfxjZ$|DZ_ompH-OM zkH#CC2*vyiqszVf%kjGXWjk4e)!M?s_w9o4!WW9K=q2Om1hYuqh25`X!pFGS0i#+% zOf0Rmk8&HS=djFj4FdXfGq1zxNK%!pzd)v``?+4UC~ebx%7}QKy1?2tEC$k7+9RGN zbuSUquVB#^6i1$S7ZEyB=(uJEtkg`G{XGdOscx`!=%vV&T*r8#bZbjzpr17Om6#lI zzq5WoDL|S``$kO)W^f`&&M;E@8Ru5iw1ft?I3BH-6Aj$VW)grsO5dg+LU~qez`aR_ zrWgrtMr;!k4J&cQTRH-06f^bS)`RiM`XE=$QNR*C%7JJDjByjtgM;K< zSO4Lk_ind8-m|O+QHHasvNVE{-X?PWD3PNlC0b(-aBl@jv8lE@BLWuhw*aaIluJoO z#QpHGbMfF7ULkNWn})N06u}V*u?QEEA?DkkNY@ch=Qp#& zQ}JuC-IT;@=BUY^FhLNJg{4IRX}wLpprCJX5(aRa1n2T*r*SLLp^Wrp2Wk!cBmfx_bWnguE0(Of@PvOn7+z`x*HrXQb6|Q`-j7T z6)yuzfXGLf_o4()Ixa3{r2nV@GmHKxRNJDD-`jYhCdvKcv37ELD&-%dlCO_?8ji_c z!S!?{`jV}|ZI2SyAuoKaKk7FP1UI8SI&QzLM&DJmzkzt}nBZ$8BQpR2`#}9eeCmBJ>>GB4V zM4%DB4IwKkdQ|kFB*A=SC2nU;`W<7rW-vjee$B0CGJEL|mLeiR2QDIgHBQ%=v$%v{Tg2;;f64y45{}dC z8!T2GP)g6RdEW6omvHMOe@GCfI_mf+QIDjbm$fyi?@8c2$BZgN``;$R7~r2W*p>%} zyNJ+n7#!c#dA8yS-j*mLG6UFGEmzP5;<(;>%LFD0EZ-H<|2rgCeKLY$#XMSDKD}-&EwP>uq#A$sFq++Nh(1Je)BM;V^k%^;v>F3biQ9Ka+_-oP& zf2qG61&$4>EDyomA$!4q06X+EBtAn+l#KZ7K~_*=8`hwF#1I~`U2H|>;eqKiP* z4a{QE17VAufZT)6(8?%^h~TWk^>sbG0p0Z+SP98Pln?QlGjCDAB{{!3Zt2gl0Wr6dp=(PTk5!5@Mkhd?s;WKq?hVOtWGQu1KWx3Qk@4lgh{o$B1 zR)FrOnWeG?&%CO>{7GZldU zsvTi%Q@9z1Z#02JldCM$OLX^PIOpx{Ef0$AJAUVrk?H^x(|TxL*^&0YXLz{a6b25O z-9RjJ9F&MRMOX;{eoevS2p%t9%Z&MNtOV{EUU!jqxmi-fKqu${@eML3V!3u3vm7Ka z!0HdMn}WK!dPe{~9i1%bX<@PT<+11t>!mn1%8o3Wu=ZD4P!mRkFc;Lxl#q z-bP@?<@~%39KjwqB8ia_Xf!#4Q@Jb&eoRp+0beRx_p0h%I=Y7NS}k)wO?=95ASXXKw%T|f9H>S2u< zGW&#}M-IpfL+r@pIkUH$pWE6%Vqb{4=}o)pFP$4QgV0qlQy5rjMvp28wHhFkIUR`+ z7%ul!*qUR!E*d1heEEW~Nr*0U`G#2BAXXm9mlH+ai-^iOH40YLEXgS;y}x$*-Rxij z$md^(r=G??UF3W%_fMD(_V#aS>cJ4F!}+do-q$t&h>^TucWi$*PNQ7^2s2t5`QFX; zLL5+Z38D#tqK{|e=KCz&Do|2LXbFOquGW4Fq`H<*q{5R~T-Tt! zocKo$B@4E>X%K_j6Jk+WEvOMsav*Ir)c%0C%>W|X{(Jv5f?2dD#?>I{OB5w^0=wEz zN3I}5WS&spka+aTgx`3+-H7sMwTQgL&`0pH?gQ#g7V_hSk;H?y)h9XpA+0t8>Jyes z-}4=C6ejq{651bZ1>@^kG)kmEpnPv-4UFR#%4U#t7Yw^nL9kdoICLG11xR(b0MBAP zP5`dgf9Ed$fHU-ayMwul{y`uUx&)XV|4JC#p2j&)1L&RFz;l#JuO4H>f1gRgWV$6H zN4q5SMd9EOvb=?$s|xw#Qz*yQeh4nUglrmFUT8>3`!VvH{f4jA(r=*59jtgtwLoJ~ zuL}ERXN@HrmdAm@f#oS3BV#jjSSCwmSstu+W`gKdC-j71J6={sMyC9TAkgDjuyp-Y zU_vL0oGp``EhpuO9+%er_VHcFUutm4`+VWxVJkW%^P0rBC#Rh zyZULSP1^?2k(n2+63$q)~;8g=pqSj8T;PAh9YN%Km|N z#C4i-A>eTK>@4WNa*(D8#k-jgA`#iyBW@3-G#hinDCL!9?j)}mG1A0|#dLG#T4FYLUkmH~x7Kq}M-7NujbcBd-3BTgg z1Ax(4VZWn-=D!P>gCX^3`v>S`P%_KHSxH1f_WO>OXe^Ewc>&ju#S(ggkW~;017G4| zJY|fXD7ZU&kA+CrTY_G-yu6^GurQOUSaz&cDM~^=Qx;Zm4U{2_tRJETn~C&T%mbVX zmLS>SARgp5B0l|{Y}1{P<&khSlnnib`0zd+3~h|%waozHn)cKLt#oA4=ITsk3l-m9 zKE>-x>?08QnDY~?r!c4RIk36~(Fg}Q=#@%zDA%H8c&P?LCBhgksDubB295NB+r{Bg zv+h)s03K(msTqCZ##byA+L8IS7h2V*IDe-J?9-*wzK}|7kUNA1L)XFF1Pg1@g26_Y zaSHm2X&jMh=ztzik$BIUECU6Ch0oxhI?rTsHpwEPoU4fn5shtp;mY7k&qFGbq*kwWuBN#cyFhL}ajFK`w8;_d#te*%l1PXG#o0ew7@z?it}D%HVsNty z=G)wd%lc*4w zSKjc~%YEa%52}j{!>5xC8FGcZe~N)p+f-g6@kNoZ+%u1Wh$C2vgu;{_txa!G<}>hc zJD~{3)c5DP1EBYTVgvSY?KF|2Lo*A15towN=~4|{ll3;K)`5uh3l%i8zLU41Vs;$jhTJp6vk z?L{awGi2;WCh+Jk0k3id^FTFBHG~=@`HO}9+~EXkKEUS=k*{3KXHloTGZd)p@ZK!1->9Sw_ttlMP3n* z*lPs22bhm55dZ@{1rWZAUU%BLDp2bvLAP_0{AA%DKtkx%D%bj9ikqs+9Z1E3Rtyvj zRto3i^g){nKSd!GNbkY02y`ZdexbmeFt8PW{svXGm`~j?VM@h0koTZEJ_H_pHrEzMgvyp4su@p{Z|uSd;(!K zy+rd(yG3g52WRx| z!|5!=;f{n`XetGYI*#?}KmY3LIG&wLdk3Nxn>s;Jt24~~4f=yW&o9)NXYQuRvKN|m z(ZISa4fI8{#=|?`wSM&XqY7^%EPMh^1jufIGH-3A;d1lDiU}BYoClLt{fB>d#DoIZ zGtv#s$Y4R+f3!-#xJ8yg${F7mLZVV@#4I}E3rk+csQPz1EXdHEM;#fo=>Kd1vl zGZken#CISLKP10OG?0V_HxI?Ix>yQr4mcz9z~=`;f#L0yKMw+o%ZP~0+`;kjCrs!N z`LYWFVjTT{|`z#lm{P&`Y*Uv6h0VWLsj4)Bv(L@mIE!5 z7L@XzHZ%~}1(u5p)D##Ab>RxSVj}HO;Vd)>Xc)TAU1aqIkOI^ditxgG46u3SCj<01 zktf+aBVX}9UH=B{suI#cfpr&9mn?Dt?*YJG=^PQm=|JT+%~VLMCMLN@hIE`1AxvPY z0EFl)pq;ooqofp0aMz}$p+Vu%C{APzS>^!i2?a}Q?#rPqNnaLDxCXkbsnt!*I~~Bw z&Vs(YBGhHdzx8+-xybwes%t$6A;BWzWs2HI0i?+O3^cEF-x|~?=s*JnW^VE*l+pqf zP{^c|R2PN9n-%f%nD;SM-rnzgRboN%B_s4lS7%7A)6?|)Qx=0|K7#yO4jQEuLqAqm z2^-E5(-qu0^4ktKyNzDeQxnf|P z&RqT%{-;e7YZVUkpIe!obV@BD^R{9`cR4vJjYVtH&tG||-j4v9l&u<>nBk|5Lj|IH zzP|Ds1JRq~;~}LMQ9G4(p^*wMiXl`(MRI%omzLz`jOAeda!N-pkqU&HVS=<#eeODp zR^6Z4(|^^hFHT8{nS=?ZKC9J{kUBOymilO$XY&eG(wlSK;RBRA+r%=0sqSSrxR1EC zNEwDZXwvNpcy_gm!zp52XJ>#+k1Z#H)VN?;>Q7F^8%wOKfBui@A(6vfKSqAVIP@&;Sg zN6FJL)STw7-Sk7*L5QnYFPu;ejy7qh$t3ByN0Lf{Y3LUhON%$%j!5`f{8xs_mKSU@ z#%2GhA&X{Oai6BbT#mASL&j?L2E%;AUmUvPwf)!d+#n8Mq8bQK+3tz zg~Fz>a+}=k1>KtVsD~AYHVZaHi_BAb#Q*o>Ed?>h(^8_R`ksi|=K zTJEnttdm||Uw}I@GCz5%c4N%`$kP17TCJyNSy^m#miu&6q?ShqA@!ySD6aAwYj4;@ zt+tnQnO*iPJB`|yY<2qXURJ7meb;Qec0ap&eAL-B`kR%B(5)uM!S!y1;36a5mHP72vO?KTwE6;A2txklCiv7g z5$!t>gvTjlvbV{8+?+9QPZS*ltcG%To0vr~HH2?DX>M&!x63Z0flFN7>`eFW zRCuy-cF|O>i_&zJ1LZd6eyXW$n;2|n4X>5@${6Atw=k0w%deyr>rMk05%jpQrspww zJ{m9#FW>5i%*b+$6+2K?m~Q1>W`lM*3n6GQpRhl|uPpLjg61MneH}rrm9bj^>gQ@? z8)h%d_2{cmPdMlm0O(?%r*8~>Nr;PsG#YErz*hvFfiq{_F}Z!pkNFDNUt8srPoEzFw+~CXH`U7#h9@pI z4}!!4u^4M1=hxCrQ@mHj1YY|~?AFrL&%S_FW=HBhFo1SE|MK zy3(--5!*=mkW&{jOD^%I=?;|ywmra4l}Z@#@>=?JGw=Q_F=H+dg%sI4Qoe&%X)1V* zPqn;w)I>ZuwUSnBsQVi8OQz-J5r=zuiL!M!lc zyoXS)&qq7~F#rYc!*W+;Al##&73nd;J3zBKd-GQ{my_C7oQ?zU#X;pGBiHg8*NDeI zp2nnW{!iLtD>WT>IdYf##=(%Ao|hLyAAxHW80km;YdkjzFn$N@{Hs6v0#9?H?Yy!D zV_{HF##zKkM8u`+>0z0op_NGH1)scvyJ?*NFU;~1;bQInUJvhC0>Jr_VA{OF$-`6L zy?B;m#j!faG{bKbUNw&a4kbC_;oCD_GL&T?V*6a=cx&W)6l^zGXdD1-(;Qng*yQF> zY1~PJAxshI#?lMygtgN#t_ZN^xKy@0oqo#gLXVw(?TNzc zEFBg4u#Fp;H)T0e5@%2V006L%MLLGYjWLFbLCrOvKR>Dt64`a-)BMjU_5ON}38U)t23BCm|0^KA_n33n*TO2})&+ zm-S)3AO{nN;l8e*if+15=E8pqSc2*i4n(#zi@1pSJHZP>ya8hu zMlyTwlArHZxR<>?4{cqYQ!Bk3V>NJ_LoX`CGUk+tfc>Cjd4z~E_}JH)HTi!TwvsrP zRd%vYm(X6Cl&~wb! zjQuosQf?+^Xts>vv$ssnm6L1qNGzbSOFvnnw+tAjJj^;KH@3B~vc7Se*PWrZ<>69e zt}O1zu;(_J=jPKy8r5z~u?uS#N5r$YsmrX^)?&|ZlMYUQsd0BeK4kxiV3GMg+8#C& zj)iz3~m>-cEuLz%Givq+sDKOxP+V z9kT8jTRgQ@UpvItpE6@Sa$jct$SUSg6{a~ZVtfSOkOIfU;`jr)qy&NHyhF3QI*8JDW`u@BJ) zUJm50n}@~ejcaG2op|WIyZr>I-Lb*&e*0_-!g-#_gQAMjk5$vM@{CU;V#3X;5G1i@CcU0l&2d@CGB>Qa{KIP zu%j)4_ETV}+gKtcmZ2q5ra^-%pSTCF@%|pk!87Rq`P@0Lyhx%MR+3z^(~B{(O2MUU@|^S9?T-$Qli1r6SlqEs=(3HhW02 zlwWO(JGB&cl!YW4&=tL4cIpKNUJ@qNj-1;~$C`WCp0^bIWbHh4SC7oAjKxbo>7U4} z{;z2=wS(0bS8y2=Qhc2VlR||fZ>B`QAA`EYWNf=M5+x)7w&y?6EVg|Hi0Cc8fQOt7 zhCJ4w69EVm5xV;7y4pMNB3Kl-?zMKdFY7adQbs@SjXcA;*{;-9=;*Gb7`iJTO}Eel zU`!f>%ob6Y#!`$h`-Hr?SvL6`Btwva_^%v%Bv7zx(C|If;tKT!Q7aQDWvPC72JBip zTR#CiVB&U_=s+M&qy_z#GAn&{Knqqul@a-D^T&XG%Dp&C<3_xa7m~{&fdm?J%sIP$ zpb^#`8c46kY!P>&_B36{(8Tz73#hz~(!fTDJ{8=SLt6f82xd)J$&H-_3y+a8W5^g^ zF9Bq+ukSoE&jBeXZwiF-mF%wrO40J|%Uh&`lLoE`+vISuyf|JqSO|_5L44{uZh)U( zYoN^^*aAyUCCucqJ%|9_z~uD}*jo+mdUk7QL}g~+b2#rj8MERv>%P_;&RUxJs3>AC zNjz8zVn12{dSsuhI(hMiE*QNS5sHv7DCX_H`}?DHl#*G*L12c`%cNFtbzlNA8_kt- zwJq2p353l?;&=VebC~G1$JtrIF=9RxngvpFbnHBjzXhrRTc|OLI~Oi#RLvhS70cs{ zXu{*k8~$3y71LxXy9x{?HIB*O|ARGPQ4AM+j1`G^58e3#0Dd39IO_7YL$X9D-5=i> zEZ4^$BoeMcRzS2`xplG{AjFNqv;h|2wee>sux7myWzSvWckG5Ht7BoLvM+>5t+T|p zIZn{!hRfeJT1b}Ivu+Y#Up81K{4GdTGL@MXYMq^ep_hBKd*jcqxn7kcCI}YsB#3~k z83n^pg?znFk9Pkyxvqg9J?zejSnd3>S_QEALW30yK71jgl@pKh&uzx9zXXjVYU#~ks+HyGz30u&!64eLmlD_{%9z6nLI$i7O zmf6kSyJz$cR*QUJ4$a_?Y@Ls!PdCeg=#qK#*TsKk>IeOt%vSi8`1Q_{e++$NbXYU> zBM19eUh0&H_YqP*9iN$5Q+kV-#)nz4fmD7Q5tAtmR){jqm(ng+Vot}VfY zhBg-)j&73~%Dp^!nF|I{9LbX5T;?EbU1^QnzlCh@B4$#0&Ev)jQ{N z#P!Hb%V@j|={Oi=#AQKEEe$H456_h9nl)dISuLOH`c9GaXMeE!=&!dPD=|#$n@|WQ z%$KJ-PCPkeS=pLIs^tpdI$%&A1agaGBuK= z46Y)N>(r#9d0_nargeQKEcfp%N+3)8_g`M&5PPz@ful_%z&!mM6k)Gj(Q#1aNRFRF z$AMPA+gHacuBJrb^)bNSVm9*5RziyXd7|3R+OG%x6F115{$595#3>W}ef(}?dyD+J zg|OsY6aPY0Ld5Y00b%H0iSX$D=T}aRgSzlK5C_=d!C?b^Xa?h|csf)8Olb07F*#?~`!79blpf>0;Z zf(lvvap^Mx-6GDS6Ck47a&bKSlaBcmm|ks_AyR>n8=ps5I*brMJLNqe?7S`Ne``(HXs3sy6N!a z4e$h_fbtOmaL?5nEa&yTzF+BEA$Pbp7BQ^Y)vqEBAiTq)bZv zQ!dkfS8{2R;sc!dW_s1Ucm;Ux8iU-u4NiLu3@rE@9o0)7%<@^nnl1y<&lvD=;sXMU zJxUs$Yzl2Qg>v9UtV>dVnbljrc4*F)d?C06y|iq=K9?6Ri#^(o-Y4(%vX4>#J~%^mXbD#sT;II7la~dC zz1CGhBsaFc4x_X-bV>Z~L0ugU!16Z#OnaPtB7azdb}%A5bb%qncIhf4QCY`2Op~R; zWm>m)ULLKMDQdV*o&S^D@uvyx!_lhse&7(${JAz2p0#1%qrJjVOG&i`;PrT_g zmtSdledT2r*zL?9FasEa`VY4uQfB-TX?FhYC$Hz91XR&3es-(6%Mx<^IyLPNffNyE zxJJ9LX&ufeSf_l~OpQsrYM)3)jQw-Gf+Z5*llsbPz`NJi0pTBPhx>dfgifxG$9nSB z@;R+&ULyeCWQr=GTCjLM4z%`~A(-BB&wxeEV{k?}v#hGODM))S_ObK+uKo530C>R{ ziKiNa8wS8Xb-4sOx`7F$&yN=e3LDPegix;8N@>=%zD$*NdZmmIoAm`X+ClyDhhqXW zaC5Bdf1=M%4D_ehEX&&s_%sHa1YgJ-x<6V2%|r%5gQMLJ9XZTQAUZ$RBs-BNT$+&4$6oEzW`HEKRxEM9;3R{i2P zd(Jl|{?v6onhj4U`AzShnB=!k@b#kSdA)I6`q<`c?(0DPBj*X;_#AT@-Abtb8MB}7 zZC!FH1XnNir|S=fYGt1uHjVQINpo8?|I=Pgn^w-Fhq30YAcTrui#0 zUdCgj^zt66DS-q6TUuiPwTvi;p|srbHzI@F4`5me+@kAVOJwz9O_QcNqS7Apvhl8q z+gaoKUj@8BUf(30u;uT~P-^};-`8F*AMC|axln+6#`_g@ohRm~XP;lB(|=8ve4OVs z_ve5r(O5VKG|LzL3Wn+L-igEV9ho$P*^UtiO^kp>P!sqQLX0inumHwT`EW{-VkBii z?~4q3LG9j_IbY@t2mx8=B6;4Mqcwk_n-C6lQqS)DSK+AWj2}7r#iwb$d&+J+b%DA^ zFMqmst~)M{l;vPw{*ld7B8i1Br6Tv*kycI`W-|+OVepRXN9j&>HtkSX4o`kw*}tVa z{-y|)!Xa~OSaZ3IwzktIjexwJ0(#Q*hspH`A5Z^t8}pF-6h0A>|Jk$iMfB$jlXpic z@BM(wUs`fkZRxfgornq9+;Q$<-N6zIbDdWTgybY%MYx3!Q!ousU4Fx?=!OtPEv!57 zN2ZT>sYjc93e^_t^bupoD1Yw2edi|R5#`?I$E5g><3m{Wjt)v$Nz*)ph2KsTJjiT>{zuYO(VuOrL`Q=KKD)7obrpvFFfE9gzVm$;a7(#~lvboQ6 z+t>9}((fVzaaS{+Lb~#U`D$3k2TS-ZhnPuTfpN56_|8$x6`ywT9ertiQN*WGxf1Wg zvXh8e|Ed(2cI3dgyOR=qv_Xv+*VtHM&pza`(UO@?h+$@xuTciL$Ni=$Ki41nc6$EC8*CG zY2yw`|5fO+?>r>43>YCNw5g?^IKLdzrwpSH*K8^3d#*v*mp`YIMIG+eaJ0{?X*|d7 z)!@XZI0Dhgav`RDV~9FQfp<&_P(O_7JdS?sJOo6Z(tU0;lG6;PJP$It-CYP;EcWXf zg0*d-52m*Ug7$>fa1-+!&nk{oXO`6Zc-?eq|BRUhkyy5Pdo<64#wjtTQ!JCb{|Jws zah?<+^URmCfn2xS;(_cEN@`gWFCodZ^+L626nA~6_c z?1l*1Kcvm+=_t=9@U0Y3$q!cDTOKqUsVLSWOKaU9uDftvnp@DPvS{ph z`S=tT`uv2pkzeP+Nhf5g_{+I=QzL@SWq-Gn=h0W!-Tlv^7lTs_ZfCz5aU$7GZ!aFa zFxea)ph}hX*|zRw6>Sx|P0MAQ0?#C${PDJ%@}7j?q*S`Sc*v9tu_u+Qa-Ioi;9$dM z6omGGSo=E6ISVgn0CxjI7P?r=kW8m!<{vQYd5vux%M&JJUfcmY@j##cBisKe?K*>+ zTDvtz1qAVUq$%A-qzH&~F`^WuNKJ@HSCk$iy$J{&jt3BsDjh+p2#6$fP&fjDgc?Dr z2q-lmy+p`e+wYri?%W@D?#%t;h%>}w@11w8r>tjr62E+c?%g@!@&NF7j-_Vwd3}j| z07?#ap*2{gfw{xNIG7RE`bUP>0&dpbo3}wgy*x(U0YbdLBs|P(=ZJ}Gw|F*Fg^qV;;0}YN30UXdot9b|w+%*q9`4#q}5~s$3UQI#) zdAdt~Po2EMul6FrY3Hq3`C|*Q5~L{Z;XPWfo5vkI=9az&cUlnc^y6j!nGa!g8i7;j zl`~5*GNG;MhVjZ}`348N$Mz4^>6M3(2#-u(%rxtcdO=0rNlYu$cqsW2nv-Gl-;`Ke z5Q+0ZKwY*x5r#uN>O8PxGxG7B0^LRgY}M%;zeNory2u)v6`X>@J1aGzKX6tz%ar^c4b?q^Hbe(jpO}pufo-Y3 zpAd_55mr%qhaZz#oa#vKD>Ny~mH}Wq4LvnLEog{f$%4RH@5K>4;Cj;GJAM)PM>3$T zGoWI-r^w^yI7N-#w_pf|71-h0qn4a9cYu0vVQL!eMep{0PQk$G z5!Cf4ep`qMKwcu789uk>{=;5szBDM(VeVq#=6li?4-I z5P4iKe{~Yz{CA+e*T^-)VAe>6{QGSPmd4+nfoz<%8BE6o*wEq(^SCb1z}cL#fGAip z(m&kf0>1jf3w?x&eGM69GMG1awN_!;|14h&^!>?T3jbIKLLTBj^9qd&R|TH;f>hm) z5QKgf$UG*FJ2$oom%g2iPySrv?!Ckj&1zTchMypp4woED$w^FtrYjZfTmuxCv>T%$ z#1{0pWz{wX*s8v}|LkLAP14*14=OUuF!;R*>V4Nt-)~PffHit3TLr$MiGV%e35T-{ z#;L)2X^OX*e`==Sm>FAEAp5CY<{hZI&PYtE>P!p}o2eJ>wSV5SWkPcB$WX#=@6o={ zQ((m9+`m5J`JkW6k1dut!jGQ5_Bo^*!f{+8I7`4kGF(<-+EwV^1g=NUB^vgFrsS6= zcg5ToGw46Q{-YanN+b4xx!98egAEAe`FyD-SJe=hA2I+))tzlHxV*7o289ZL0kM>& zLXR+*U}t)jiNW%D06#+x=IuE6=7uU5#F$_ts4EZ;9G#a^m93|PZJg~M8?Wq#PToU^ zg-ZF4{)gw`u+D=g8)>iGO7=udi3RA#&=U;~LsYks8Mf!mE;xT%AmpyKLgc>L2m(_# z+{y2I$4wvE1|d1mx_WD>bx}PpBS9%l0azh!FEtB{#NcECzi2R^^azRY+{D0)t+V@J z7bmeTzSB&mV`aLFkfK0vVWwHaP6MkOaM)721}6L2&}2#FUl1Q{Njl*JQu(iq6%k)y zRw!nCZ)0)PX)-TIvnv3s)_8@ZGr*bH!i+{9T!IZn(lvujw^2c#v?0vQcIPvxOpB{8 zc-*s)jmJoEcgWrshkDDA`VwqiHAK5Fkc#FlSRzf)2V@+2&(Lb0x0*q9EC;&kd0Pxk z`2et_AHIJ74G~HMVyu)NwiF<3tpL3c{|p!4)KOCA`TE$jn!tXiJ8>b3Cl!L@Wj)hwI~7zC5hM|Sxm*GZ0}0$nvUn-Y_ zwd&}V3ju`lbAde|+0%G~aY=;+$3^Tq)8sSHF8wwx47BFc-(`J_VN zs=Tp0AGFqU9Upvszx(>3MbN71mImJ4e1tp9OIw?@VSZi{4Slrmj?B7h4SNodXTyW{ zc9bN!9A)q0-ejGb0>j;g4VMhor2B-4#37=(MLC6dG@o8Q!x+fr4bQRNq0^wGykRV| zmRgyoL2v|t=Dt=YSiY!?Oo@%2o9AdZB zD9tHmfDnoBT%Dd7=yR+8J3K8snAKX_!y{e1ZNTgVc?}bS+}+g1N6f-C@_-N%S>10&}DyZ^}thKEt>&bO%8&m4~i_B6Rxj{XSBwa5w(+=)i^+V!HyC)2K7+k{t@O z^i;>U1T{N*NiElFP|QozY6YG*u*1G@Su4!XxPav3ku&A8bVIKM`R#e4k^OQTeiYJ+ zB^GlYRC{K;KKMvwr&?JUv>;I=0}0f9d1~3E$m(rFJsxlwOOy(XB83biBUfjkvj>hj zXZI&eO6oy1N`ty|5nLYO-V0Ec1BARGGYBE1cejA9_JD(Ci3q*`ZxzYW=uSI0L>q<^ zq17aO1XT*D{$Hp_>t9)FQg*7%Jp(EtdJ$$GevHanzhm$>12^8Jz%UG~<=_9>r02Hg zv$h*dfZ%Tri2XXgWP)yin$=v3bbxv#=G6$n*HWvyDUGnb)5-C0(+6$fw#QAmE-JYI zCFq?xkqpKGT9pNOhulGC`pU0(|HRlPjJb6pG>}va^!VQ3IZT7$vu0(zf!M%K_##oK zf&Q$>+??q-J?08wiO=dSqNPO;*G*p= zXo2T1pjFc3^@$#LCkUMfCjZ+RAwGEadY>sbEJC9BlKeMWP$JymI(}yZ{@A;Ag)dT6 z&ekLJB9q=Z1>~$uLn_)#QooATHk2^uh99s>NqE}_7A6wjdP6k)vdddY{i99}!ema} zh?Jycr&cFyT386^OjVHTv5`gfOM{C$%$wfmc80zsj5?14DB}fO!i20NMP!UxqY~M- zD})%67pRjhk*lLXfQhG0Z%yxqb?%Q4#qaKF(UtIIL9_GiqaHzk^Su!abk?fjK|`w> zY>!t2=fE1ewOu;)#r$h1J{eY(?ZG1t))28TaIIl-Ow zTGz4;ntO+{%1&f7{p29>$_;Fl3J(X{;%4zs9xDWbahDv7V-weF*N)rVx${6~+Jk;kPi8A5 z9iv^%_jHaw)+r#r$`SQUXBnINTR>^2u5UkhlL>a{IU71ppx>Bm9~o`8-qF9HO6FZv z(d&w%nSYEG)>-f;tFPuep*RogJ$xM=YemM)RW% zk-QN-wf7nBO)Y{7&5k<#Ec&!8`AvbhoEE*VDzKmVJBs)EyrSo!?%nN5U$0i;;^$tA zQI4(hM?EHH(?W{zvHWK zT4F}$vfS$`!sRD`0`RQ7I=*ygDJv(@%MqVsGIo0EGD98vgGez@Sxs$Au3DZTiTS`% z%MRGck!byT2_@|alS&gA{9F!{3pX@a@TMsy;``{qwLv{OX>QH>CgU4BFb0iZ)|Qwi5~9XJTqj=!JPG`{d_DCw%5wx1 zC+hCbS(j{eVPW>+$+K{KW=1IG=<}%Tjr&=aQjMJoCa`Fnfq_Tr^KqlNhHy^N*l{m< z#oer^V2$EGKfSqu&)a{NAgZt|RK%PSt=!Kh-?^4wR z+|LPc+~=NSQ1CDtXFufKA4JHro}8V!lUsMKIoY~8+U@DPeShEd(48qO`bNwkd?%w_S0|OlH_8NjnEA}y6)M3 zXouI?F)z`Ng?aHKJ8N;0VUK#Mf#YcddVu%72no^Z(V*^EgSY1Xc_lbBbXrR$&7)-Q zG5+k!So&(D(2m9kL9JBCd=`MrBI5DPCQt}$|$#A``Z-cK}9O-q~?&NXt&y!hqa z6ta6Q=H%xp$@ntM0vl`H42<`IVv*jm8VinP?9=W&m0;g1iY+CD-?-zk4Bd{Y)*}-S zaf-P>RuwJe$Wt*zVIHQ{*=SFMi8*vG_>?Fvn0R3Z+~6{_4Qe@UgUPG!pzx;F%RiEa z=H*9xqo2|key{=n{BQ!Xg{I%i;U@5=)e?J~Gi!I$_;gSTr*FtSCdO(|+ldYZraXvB zSnl3$4LC-HBpwO_#nnFL+r51t4NcMkv&bZK2NN+VI(o5xSVT9#M|IxYQTR_7R#Vxrd&5sj29|!p>N!hlfysq*9oedlZ|?Sg+oP$yn~OAYdO-%i7Pj!J`{s9NwNNu`ED|&C6 zlOc!E9~P`LxiatV$pjNy!);x*iws}_*5i##sflm?1DHwQ&q!d#{p`*$)dI3SS1*pC%= z^t8?iEcPxak;XC3oCAo!R+7Xz(uyJxr!l3L^$T<2RA|-}ftXLA7BukTx*`8PGBpC{+r1ce-v$h8;8G2@4x-})DckyOA z1c))bi85$?m*SDj;@TcZbW=}1fYq!4A1yWFhh@)hk$7;pLi=@DC0(VbsK7!C&+&$g zEX0~N2-`M%mz$4KK@Z_-pKSQKqLaG6uz3UkDB728$QviIL0K`Q4iKe~mAtOGube(OIo>Ntux{7gT>3&d zJ15%NPJ=H@Wv8(;6GJJ&O0Z>Hwup^fg0=S*0?DlELw481&tkDZ^1BlLa2PMH$74AS zS7Ipdj2QWaZ-`CEI`qEZi0QYMEv8I72U=J@tUF?bS94w?5Z^E-M;U{$2-tej@&^zzY&@zD_|+BgsVwLrYw1@H}LE* z&;RfP=iG4n~YtvVhz$|+?z5~U!A>i^vTan&&5Vx&calf7`P>4|8tB7S#s;5l9N zA-8o41~Z1xMxT^aC(R-gJimZ8xjrQ=qF_*j*VV(E)JYeK_b&L5Ftjgfh3{J`%-$44 zMsD0P{yiteyedM~!4RAk{)^!Ois7p5y6amc`YRlpD8aZP-$_wUc2GZ#sn6)({)blv z(=SY zG~$;;2z_TRQ>jy*G8#<{^V*ystFG3rOA|OQ=ZwM$5vZyNZ~9QqW5z(4<_b&jjtU$l zS$AjX5u*?%+EXJSwkpBd^Mo3Bw2gQ@t|p>jRDi)JYy%vA?|K7O9B&<+rEL7NKN$_P z-v=V{sL&;U zK?o2aNDB}Gcg6F*-?(GkzxT)eaWlp_5J`6Se)f9SnrqIv5~ii0be#SIJrxzzaTVp; zI#g5#pHWfK{5*UR{-yR8DF}W@dfd6^q3dGffx7QzO{IR{@F!Jto(or>zT2jxF%j18MN6*U!= z%58Z)@8reFL+{TF?X>)jw?5jy(O_uTub1_T?o^gqMMj38%nX)V)#&%tN^ z_xNJ0=j`&oPpGKSZ1R`?_t}%fa$1M~_X#`ANk*Fgee#m(04&J=Jg!jvzyFq#?+Oe{ zowrwK%0|mc7UfgkWa#vRTH#TSi|O+!*FAOUy_Jz#GTsdmdHA} z(Q%iirNGGW@UpQAhq^TUaB-QY%=R~Aw^~SEAU!#-(TU zOlaSYg=crixw+R{IU=iraNj>ig}e)n=x7WeEk?@jJ>GX7MQ2MXk(s!)*p(_z!Rg5j zvvVJyqIz;yn^O(j7_byB)cEn|&wG9=&4CK6bso@ioInK-C;#Qk z&9x|kvqj)`_EAUa&7Tj&ty)j=>0~MEM5!-%b_i7c+}&PVN_OvdJ%4AWJ@#l)+4>8% zz?(Bftk=GrDi;O-`XB^9jV3n#3;?q(w5{GkPCvg0CF6YJUV~!rq*Gr~3 zk4r(Bzw%_h6Cs9>hF~}LdpuJtMwrlbC!p{m6dE$pl zL`U=u&8$UiyI*frQtkj%vg9l!BITa5N_wxnXP7_NmU7xpy^NK1u^?S7zBsHOiLF*elD;cLPG-b)j)cdr>Igl#U4J`sHSdqSimwe|g8)~Spown1+4W>ObW)Vb=UD)I|E^nnv zjv;?IZFx;K=J9yerheVQB-D>20b!q2E7Y-dNmfJ`*T(M`Y{nWZ*t0<-BB82|&^Qj! zIvg{FZ&gv!l$M$e_iaH~cn7ZOuq{sq`nA-NBsdiPi1~EfP|5elwFw z1RvkO-o-%lmD(A_><$yyF6z{y@e@~9>Y)wacat)keVwxGPbTY{qBV@;$ zyAe*Yu;BBJD++S0JK|C$Jr}$mJa{m%#>8i^USw7${vD6;!Le4=2aq;XF%5ooL5BG{ z-`!^uY<=5fc&3f2oKs9Rd+>idMS8q5GBc5PUNQbHf7JIyx60Y9hmt6IHkZaHZ5>i% z_jg5pi7$_qwdj7dl&|V`X(G?2`fdGwF-aII(C_d_a>CS0n3d*RG&TBl^?tY&jU-Gy zhuHA#mcU&k5+E`7B_dpw!@|Seo0uVJ*T_WdA%ARcs&9L=kcCW@d0)mIq;0b6^IW%? zb`>W|d~5pq`=duX6UF%Kn%I2)oFWq7Pc`BK+8MX|9Xw6RJ{C!R%P(ZqO8fAkPT@_g zQeNH{L?1=1`{bqt*5(?O*(}%hZ~yJR#ZZHj@|cU7s=$oAJ}>e^H(f3`%Uuzdfig;y z33%6F^X=uS<%tbJmw_C0bj$kuz`%rNN~G`79cD8|kCYLMpvmIQfVKV`{ej-Hb&#}{ z{j>@V1ggls%X=gO(JGGMx)xYkzg~wLb?-sq0^|6N`U4s(Z(((fYCY`weJmRNeNg-t zAPUBYN#rzXAGgmv7G*Z-{U|vGa!M}=@9o`Vfn8Evs;|bS`c0rASNhC$CB-`?R2}!c zSpH_`#C+W!Qpy&wX@5z=cRi0(m)SGylkGGRXy*fGjBJTuRr^~b8B|{H{ddYaAce0H z7v1)wih!-W? zuG%%gZ*^(32ai3*PXv;dsH^_Y#9MfEcyf&JmC0XrNfNUk1GNtWMgj&daldPcQG+XdTK0JrW23<(xnxL1-kE;Qq8lt8~ovq!B{YjQ8d; zBRTz2h|lX}bc4}0 zFNvD;G(@{AlJLiG`x7To^0ZeIKKy!tF~Qk?d~(0eYiWWMc~L@mzDB5V>kAS#$F&|p z(|)RB0PC56CY#RpzuT76qsG*l4Y6_=M5#(oyg?6%&_w7~v)IAlN!bAg>9Y{+ZE^Sw z`g-sEZhq71>!WUTnQ8v3D$En=OZ7JccdK#je5H3Kx#EN@E{RfQlC|UpQ6^Yld~sH~ zL8*Cz^v1%F+S%LYP{ogKJxW1fyg9!6;p??6JJT8+=tJ=MHy17=k?-_T( zP2~(anEpT|q!JEEKS}w~ne<^GQz_EX0XaE0{AN|ar1HQq`uX21?^keUN(jBsznI&C zCY6q#wfEZXS8O7@-&_!DcQPq7C>Ac4t^Kgr^+iR<<)Gqxe>UyGXCW2Mux(90-|eVq zXbh%tNuOEkk!9FQsNF8{GVE=wQun7l&~CnxOZ93@yMs$TF~%{0;1G)3rL{;nLhlpG zXDp6do?s;8%7qAL_S4g+1nq58kLQ?FIXiDVRZYfa%U{<1tXdkhGoP!=ulpl;Q!O|Q z;*%4(|Lu%s^#}!}M6>p@FzTE{lnK@?3KK8U8|+ZB_T(D9Zw`W`Z@<2mC>Lyv9LZ8S z)03l~MAO*K@IuvyN}C+L?I>jb?-^o z49jn|$I@O`CZNo^xX!$sRqn4x1e-%dj?_rJhI5ZAZ-8wT?qJ(cVpwWUjV-$qe%7Ga zSPWtNdh8dV5|vNg5L~*_xjqjP#T;xb0ybic%^M`=WayH<=0h>^DiAGw(|w+GpwekP z=Uz#Tm*|TjIQ6sz(t#vdRi_))?J;^pp)%2?ZU))iUzO_a8rv5T&dKAYM+K`tcX3T; zPhW-7U9t4@)7LIsG>b<`_6$I|iur-u@%C80No-^^mu4*v;rR1c;6mN3pI{J_*5%yU z?mpKJ?k<(#Y&B-o3A~?->(p%oPRD*7$*@iyL_KH@q4WB2-nM$XW4FR_w4Z#;%Y05M zce_%wDSB3CtvmD&IX{5GrZ-*Q86y~kAy|7Qhr@k1?D9Y92ymCXEW;(~_w&Ohjs7*B zOJhUB#U}9!zRjGEN8aqu&6Nw;ie2`%7tP;Y`TcdNxC*0smeCjB@Py`Mo&nW@o)4CT z!)ewG@w1BpfG1aRJ!uPpg((ez`(zYxt@SMjbtvba(_WA4e%4|7SRUOUsI{}9E$v}wqrEV!gIe=^1Ef-Vgl>gR8yQTrsHdU)1 zuVKkzAY@N*-BL|u<&QD}JJnC3oclFXrANh`#{MXF@0Wi2R97Pa$@=itrS@Mck>$g6 z3k8J}#~)6XZ`uf4l=1&5v-K-9>+Z86$FkS27cDC6_0b~->uw}tl79GYE~>Ok@BD3H z3zexa)t0i#LS|;1u4BeHmL|_O@T3`^6hcxrT9V$Uka9m;L|#HpG%0dQLmF^Q#n!I-D@Lp%N9`ym}EuJYqM&OGn8W|LfM*DS{W zkW*#he5jjn7{_mNM#6U5y}H!ucKul@wV@B2H#wk&&KoZWneVO-XnNs`s{l`zs_cK#kZSmH%e8R$TPU03L7|%B*G=vCG|scM8hVuwo4+zHB|zt@@TMuCLlk~bR1w; z?}%^w_Cfjj>`!A=E@>XRG8HXDh89jfVW@qbj?}|5o%Al8y-$yvdDmX(LwlP(<1DWp z_jd<9;bjU~!+w?B+xq(B_4(-#CHnf;kcJO3Tym_r(;NXMtu82%Rcbiatw_Hxj)ckG zKoV^@mM7~FU(*Vo7G7>7 zITHyKtk_6euG1Sa9BzAgisM}%(MEc17L~4J+5-(w!L?X)qUsQjcgCtKy<7g%MD($e z>Zw=!P9j|bS19dHz&@EMKQdYC>7bKdpqr~9FY+wLb-j%%Ez8>+F(sz-jyPNGQu$68 z&3JW1kb#T7fcs{KrBS-&m+lm)=#XnhZHQ2uUV8;k=rB`Hj8P12Cnqybfl~^9X(@fAIJ3ZFW$5W-jQra?jXo<_4=je$oz|@iri6N z&2-FHD-tjrC^z4VQ0E8GVT=Ly;1bKL?`B;UqRnF8eY(qRD#_Y&)H=S#QUO}(r9_7k z72}*$e|qIMz>_Y-?Zlw(@s&ymoJI-PVs-6@i|nAY@Z2|G;Ww)lwM*z$XVu*&chR?d z4J6C0b|J7LKXhV-LYAopj^jbH2WWC1py4{fCq8Ao7`{d%Mi(M4)t|wM1=ShRD@^ z09Hcrmts*UE(Ud(8YTxLj&vYO}cdm#GP43+{R4-44S}-EQN308ZRXn`nD@m=ryA^|1G~dB1L~eeWupK-B>=1 zOd{63(`u*bSMOzUsqdp8|16dXbNVa!qA|`Pc(KG=%G4*2#2+j!f3~!>sX9#1stHWB zAJSO$;xx|m?yq(T2}-FTYMDMmS1fB9CShIn=+b*=RZJk7bpnK}_wo+JqdzjfUujeV zD$McsmuK=s`O!P=88!1cNkxipE>!w%*i!(TSzOetthVBJ==7X?-iA0P7T!5;VQ;dO zgnr6+Y``b~a}u{)RBTLz8>ElAUlRrTg^u0H63f5Sknxuq*ADUAW9A=F!V1vo!)EhG z33AWDF@tKJgwYgA=Dw&mtKSK#sr2xIV6Pc=-3%e>HE++mr$4~XaE<{ z&liRYZL2W8g=FZ^y#TWg{Gc4{ijGX=^A1Wr`1fEhddovO(SMJV)c-iw|DN2Y`gh^~ zJvsH{U-15WB6r{)1Nq+{{{Od7SnB^C|KD4&CsvmNx9;<4r9UA<|9YxVxM^>vaHb=^ z2jjoiHGzQuf8v;nPX)>bk!9>l-q~=jD{0(Wa3m)KIbYYa4dmCux6T0jN(5x~wx+Fm zph25NDlGk^e%m?y_vd-rq&!P2HZ-{%&k z>_WbLhy;+(H7ph#H>8KOWGp%v!-GGHi znj+YyH{~XXIZsq)5s!FiEKb&qY}5%syHBa~ca)UsJe#&>lb#TAtpQEukypMlY|iJV z+4$B`mCj`aQOmvW-@i$r1W~{JfM5IxB>IeCBQ(LY&guBs)xy$7`=iW)olEL;Zj(X# zBn#%N`s|#HK<%}7E&jY;Y*OXL+*x~X4-RR1o%oeOwj35=ECANF$mk&`B$Z#D9W{aT zH2Y;3oGR3`+rKof#1`>@@K@dA&-DxR+JG2Z z4q%iMSVS{!E&z!aSIQPfN(mGeo5K1T)iXo&5iDsb z>!IA)5+{h*6vmFTf~IqEAP0SR|I_-`s$<>%#R*pF4^{xFoa@Rp&sW&?T+)Qr6Y04y z_?L388+w2&6ZR-B+I*R1Vg+}7labG$(|v2@H)97>@!3mnm9f`S4oPbEXY754Hz1P` zE?!Oe_U{Y z^gxPuM@H`N;7bP<*>Y`>>;f^7v?nV?R~4?6i|SbItj`M`Wsg!5todfsot$tb_8x~2 z1$P6+^~fJ`8Y_3@`J5li(#y^E=4p{(X)2%lOmSiI*y@dkKdPOFQ{aImBmm0--QJ_(vr9~eoc8Dr0!Sz&?Htz7wRR)0w=j@>ucKX_c*IY#;a`WD2 z;SfwB{z8R2=quIQmKV&jJDJ|FuST4%TkDo8HmU-+aIwT(rwV9eS!iV@VQpy$u3ZcF6em4)F;!?ZhKaryu~?nl%&h*CDK{ z_%_bX`LGfw-8G=+SjVg>*i4f*R$f_&Vm$*HyqS~6Bv>f}{97X25M(6s!il~7G zXcm{RXd+UW?)85k2JUgDzmQg&-43?x@|2c)OtROQot>be3V-%-e9!`e^orc8+6s`G zD(iadem`g7kEsXkvxL(XcE0H$MOF#AS3rx~&c<5=euBNuSzLM|-#$qQbV#!zQ1|+@ zTH07lk0>^C6=#MEd#2VqtwVaN>rRxslKA+OJFdGVGfN|HulPxl&T0V9$4vKItlF#$ zbT%G+x!YEa<&Q(Ph6a%(N_Iyc<4@cr-$QuGRho>P@*rXIXx|DTuZEfw6s~L02r#*8#On0Twc7KP3oPj^;^&Ffn2rAZi(a137 z;tAYG$q4X%qw1_jT%@wAhRZ?UzQ8aiX&AugqQzN8K5cf! z!e=H{Fg^Q)stR(gQzRb~!(AbaSd3BQ}+;b(81UJl_P^#m$QCK(}Z_dQnZh zuw-sjJNE)9j}W$e%_kCN(2w;1iUMiC9(&rYwOrIajEX%{l2E_ae;0eg26}6rRNV_` zj0kkh+2)6M5xt;M_kbHfr(V2xhO(1PN_4#T@>@XYdVH-xZ;UUd$f5~zhK((4Y8!#| zA6N?SPYghUguT2?O?$AKyh=$$xpxzUsXOj?>*nzp6wMjcc{zRd>WUXi!-X=)C~GTB zy%yJ&-ssRAj^7gxHYQBOsf8JSH zOEm89`BfF!_N2C9@Om=)u+X=R*3Cl!i zo|;{esSWib?a%-!*T&Vt*&t~TomgiCfmau#aFp4Ckk)oJsBu1v%_@l`vrxjZQ~>RC~;=Bb{Nu<9yHUXS)O%hgDEm&zjJU+t$sepkfHh7)MZ#_vHe z=2S0_Y*vAyV7Bi|vBsP^b7tBOTqS5?KYkym+9x9!$63TZ2s2p2mB7ZaK~zG7$=|8( zlhXipQ`hIPO-+76vFGkOS&Wz4MLSyP&#GSgR8HyT?c(-y(?*FK<`ka@7cXzSSrSQS zTQrs==`pAEYGjM}p$V)2$2M-HLfYjQV#5g)ue$P%=gfdwex%xzls7{mjcR4x0bwui zeWPWNj#@YUxaykQui5_6#JT2sgCS(|=naboT;vYn=vjW{%?lgUB4aM=^M+c#Ey>s(Y?X_}tC4aSEJi@~@vqN3hn_L6590O0ktbbjHR02n z<+{FSE-|bpzLusL`bNxUIRsR?r;~&Wrru*IZ9%?}HY16naifmR6P5Q1yjwRtO(SiE z2o-MWFJ_r*@FjIL<|rEvjSs-u*VJhEhn`5N5eG*@C7$8Hg(tWMpTxfOUa=9=X>-K= zFHf^@ae`(GsZ#i&GPE=|;OYmPkH<$?}C*h+q)~) z(0o45u4!PJV*kM(N~{_nmeJ5LHpoXq|F9D?8`^KFQjrv-rA*ER!@%ogQt+ku6b>G# zPz8I!APv(F=jT{2FZ38O60-QI)Y$$a z&r#l*lA<~CpZona-u%VNIvZhL7` zs`8yA-rO9Cr#kpERzsdm?Jx8HB+0t?*vImzh3`;oZkg666wy&0kO!U zSwWrco3USqD;hV)(=fV`x*yzCuF=vS(K~H&txj`8r0kt)ufwug?8sVNP)FYD_2~Xv zE%$I`-EDHXg7&uX%F%d&zhzCE-~P4CY|;=RX!mc+ws{v zRa|SDJN23~l!;Z-^Dg7v)<;rw3opH;)ba(^mXy=lafP%O7Dc{QxyKq|RBXsnS?5RN zbQ9QbHc(&oysFo-3q-(UY(n{*&C?L&0+G^m2V?9tx+l6xkD;y0VjG!Gh#ze>UnqOj z9rogqZ>vFx>AZ70UXcXFENKDn;Uoq8TO z|EMn2%JyzF(kSeWbkQ?ipr@W9*ZS32o+~;Yj{QzDY{ru)Ry^Kj3Sr3n``8X^5lj8y4G>8_c~e+U?mR)dT}f6nSAlO$Hm_MMOmiI3`!?0aqU zs_t*3Y)C~4aoe(GG=BD1Ka~VI<*?|8p|4eWTFj*jhk5qg`D|x*+wABEd6jc&B0RdY z;~QHNd+ySzgmxiQhU!D(=BuXkz?^!rGA1A+j2yF+sX{Y=r*vi1hp?wm=84LB@*gbw z6aNTR%m8EIm68Q~XNTt$CXnQW3V!=mo7X5M_S9=d24428HYHE$d8y0Ul0llt4L$Tm zgOOmf@b&)o&clPwm~eYfOEDj7<9a;8VYpMhK54*>v^J2@{X!A%6Y69npHgILq}{x} zI4WZKLXD$)(9ZnRgJ*cEY%X?D)UqiaI(Yn?|MpD0^Ou3axrrfh2R9Qcs>cgJJDJ(0 z1ymkQ=6=X2w%|TDi+29GAUPz<(kjcct5@wSCJLp7Lb-pdEfX`}JOY(AMumhOtu=N*l4bv2sE|FI|T^`SrTzdSl~m5b34X52ETX4m=$ugNLhYME>65$XpCOJ;JY(@=l5G<_W?!EOg zz?zYmU!p#j?EBJCnldqhhVHg5%hLj?`mIB`4bP9ApSlFM#o;-r zCD@y^*c{>_=lk!Tg6O*_SP*-#c-2Pr7K0+kr+Vi1ybkOEJL^YoUg4^n%UG(#mz162 zQ2YsOi|LQ3c4_W6GK1dZ%9&&SvbU}(fLZ+VbNvYsf z(y`tdO!x^BqE5S~Mq7Rl(fqRwr(d4y^pBE(^v)8wnFV20>bTc7cjhNn2<3SPUMcD> z)a$B5<4c=lx`^6x)nIp6_A8F)rl7o8hnV23?xFk$8bEttl)H*?dthhn&PSDw7eB7E z)}&p`l4^7#T}7VS+Bc6kv|W`He}7HrOnj=meXXn=uYae6wPanx(A2INvH=TLUV@&ie!FIW~bj$jUZ{K z{ZZiOXTw;%LEhg$cpclkKwcWe@*I!K0|q!QHf{iANR8aU26w({U+1G{|2991R*Ykn z4V-urqTO=&q*>Iq*VTIJqx%tIg6|pg+B9lMi;upgNQgt(1_2iP;d5Fii{6N4;B%6k z>9;Oq>lHps?`LW}uby~|MUZor<+Z974nF_)5bc7@d^WIOkc(Vh?6+M+oB7%nWYo?b(;@ZJI!U> zcHiPwH|g=o=Z}@t0_viIVxWE_Gmv+cE>iS&wj#!gd1%>yO3nRq;dR*-k- ztzOtMvvYRq36e&V&b+AK>V*c^6;mdG)2-_ZD{v|nH&nWnqMgD}d)QE1)vGnE$BYI}hx_b7p{r1}e4N~}FXx53#{aj5Sw-9mXmbXK3$&iTBjFC|Z5G=jSYx?HpS z%nRs>R~dp;A0O~#^L)n`GlZuGSN$(vY0|vpnBRa2;+LmDQ9CW~jV!zu;mw?-$MKR* zzpmdIZ*jF}`{g)e5d)qD2RrAmxvJ409ZdAwJ^)d*Y1~#usE_-Ggj&#>P@3wEh*SAr*qqSgBz?gLKI`5N= zJsh}p?MGFXGGwAFn_2$+&3%1i(%(Swm9rz>rn*wix_QuI_^&D;_!H-@f8J1uc}7?w z@!V3mR=J++vW?JB)bitYR~aGO`|6%18L(6cB@QDr@% zMvi9M@}@Qs;o-l59NDd<@KSNHg=^W<>C?Z}f-hm^U-U??y-pJ9NJBhM9;Uh-+ce;l zwUdj!_17!H!91)f$@Cf zBFq^eb6+$e{WiTR`jsCqm~q42yxABmH0%MUsK+~Sdxp8Hhf>GT8>O`iyu@9A&mqn& zV9!T#D86111y_P@_2c`R!8CNzLxqM!XdAyncY3Y|x=-DD8}ON@0>$my9w&hBeg@ow zPq*~i-5&6N6O6zcjrQEwmJAF&Fy2!h;bRaG_;E8qHmDI-zt+7*q3i#d!y~#U*B^m5 zLP`egUci6Y&IPO|oS($%ejwh(|E_bv5FQyb1o;t{J#iXfNJIOxfh;*PJ=*LiFt@K2 zq%;x-$R`E|);}I|=DD3!;Am5|GZlR(6Ei6FEc^iYo+rwH_&d~;LuKs*Hkt>KJ*%97jp;WvBp)dDg8`CiZ0H8gW2vz5F8Icz`}+EL#<q&WFJ|zkrwa~ zdA6tqRDs9T&)oYrou-0<0(v7f<>&%4259b7pv`L@ngO#y43t7G-|e$$iDV;61BG4^ zWS*_cR27&YY;DTY0GU4(Y#csrAywpo@i+|m@aADJ8NB--R!A=|O6m|e*9hHVgAt|s z5-hJHQiexRV}>>)l?Ap1mv(TRA?LHFx4~-X2WHX5z0|o4P&(KAp(ROOe3wc->R};| zr4|_2p2r^A#34sQ1DHcu>nOJ6p@&S1r{>->T)f#Ez*gln9#`T-71EZwZ^i%m09C8T zg@nwva;iezkC5Z_s?p!<^&*6=6vU)>bsi07x1Z%b3gU(NSsqbdO%5wDrC7yvYN{u# zOkpF_FZl~}w3-3~;_qyN7Wrp$mi;zQwC2oQ%nWf*S94}__O^8F4@1s`9sDS=g8B<( z*z53R&_q9iz)`*K-&HYff{?iA2=fVd>?DSkhSj}euk`NT2v+U6jC)k{lJm3Pt0q?*bY2k6^7RBGP{H`NKnco-RYp1}Tq*J`I1L0j_!p1eZ;4 zI-fjAPfbsgpMw^D#`rD#>4}W)Th;~J!$~lV_x74jNei`5*OCrwDwM#*BO|-5)f5s$ z;^oN;p$MkKzF`Z6xbIo%ONtc0Yo#<1d`1_YH|LeslIi=Suyg`lHdcNrDr%V z2tcZU`0aFV+_pjF^TE!DV!1Nodl3}y|GTr{@aTOZAPU_*`C#H{UZ=7nhUekZcoiyt zVh{95XAo$-IJ(thd94jiU7vwtnB%+ekt0W(AC!YqeGj&NkxDeDvx3wA7y_uCJZ46_ z)7zn4)mOZS^bKnyI7PAFpd+vWnXrl?O^xlNK@nu;=Y@ebBq;1&E5GCoNhp9k;-DOd zzj>qXOL5v`K}WQ^_59c;ipvqEd8|SY9(P;gI(F_lJ2Ua@|9%>~-%S3hE0<=fM|8Bq zyD|{ot8vdyu-d~W@N6=KHWEn$neit@q2~7nQh_u6auycYK5JsKR|9N1;8`URpUi>3 zb9W{D??I{1nyK{kD&UfH1bu>H{U_FO2J2bk0L5rjnu7(U2%N)$a(_}Lb^*<~SCHXz`&8e}Uli095!gP?5)BY^5k@e@|spcD%}^Do(&8$9vMqHpO}%S5tF` z45qj;P^`y6zD<3AB&-4n#dvZ7Yz&-4iv32;hEfQ8*XQJEpP$@OqWsGoF;YQ>ThxE+ zvEf0DjEsyTQ1w)1u$7LZQ-KC~T9?1$*C!wkm}T8Hk~TIpx|1l|8iX9J_i-;6|L|)7 zYS23*YIn<(xCTas3dl)DSG9R$=@vxe&sY;5^=N(FR`Gi-+^sn#GUyu7pZdYqd?8U+ zR$uHFR3MMhox=i{hZ!gAAgQ>omuV*7d^;!`78cgl*AsEO9I?dOeY44q@c75Jy5Ea3 zWn>d1g{9Mm!u_)J+v&X@?|)sSXe{z9u`hL%dRbm;AAEa&chZ0|(&Je^K@zFh(8*kd zVYyZ1&(`36{{+SZldEBokyQ%M=#5~+NI+8Zk7j0+k{_Jb>$T|3W7f%O<#u_~7m=P6 zu8iXSkiW-!%IwpFJ4ET3q+5~1_uTA*@Ua)0j(l_&5rkPVkK2<6S_S?eXDZt?EquK} zy%pP4N|}Lb1$xmgMbgu4jh+l%H3Jx$BG#jWcPS<%@_I>T+Psm_`wzEX7{eqZt>SOS zfWfx2c8HrXo?r;z2~jAwZT-EQg}ikh$F+2R;ii&Zj}{sRFi8OEC(^GUpY0bzf&0SE zuMBP{6?+e?O_Z$l@kvkxN?UFCC!u9p#XzB(WK4MZ)lx%h zNtAf__S+Wa*D?i9U!b2}amAL=F8%L4i*XI?&-oDHE;_BpDmN+*MhcJ7c?fnixT@;< zf?@e&HQz#Q2qrWTQ~5TA)8%Q2rZAed2n|RDjE`{H z31@=_IBb1-(resFrF5w;^99Mwowflcl9nZ`k5eX6OUgz8mGEN^(Unl9=-NL%WvNJM z_s;uK=Q7K5+U`K1YVxC!{JcDAWOBw=TaVFmjE>4?1idT!QqeHq z0%b?}BwGG*?8^Z<8`yzny*8kriwEv{n*j9zRh~CLZVS2vTbS_)^IZJd=iIPiAmzQ1 zjs(Ct4B8NjwD;qFl75Nl==0+&vvEAt9x!e<2AG3lieC;Wt3CEiFbd%`W(E24zQmCd z!QF=sAAT*MjT&mPNU%#RQ{^x@aty3hS%IP30E*F+q1xd46m^N=Y2@NETc2#3(eE+L z`ZVn1tA|I^wf6VOlrac2*!YG({k8_VO zVrg9Mc>rAyz}PxiQx2CU7L9Ev(#yd8Qn5M38^d;hBKGY7wcNVxTH z*{wnqC<9`V>yLhZ{15nvtt|f;p(ak}a}oLn)#zZO5$V#Vb%` z$6#JChA%B(>e~^+2Ubu?YF4*d5YFW=NHY%b*KKAEZs}?w#i4+Kf=z1toy{c&iU^4QcZOf?`1{v)B};(2J;??vCRWSrPo4JXKw&C_Beq}p z5TplC%nv53qT>c+VdYw-4%0t7=lMuxqOzyN;?tkPVFd!8*D zBisL^=J(eZ|D9zhoaM<_nS1`f@{o1+& z8?H)W5(>Q|ylOZ{oh9D7*zqX4$&S*Yh}u0l=gmmHJ8}UpmFL;=WpD38?qnxi=4Mpb zOuPH>@AxL&(u%j;_S0h0#Lnn%n5ixUpYB-G-bU%MCW8bKLvx?(BnKR@#}z#?q)yL3 zN^618SqIIuD#^LFjZ-7}{h}w>Ya99a&<|TqAbi)MQXktn54pb_2G43&Iren?*sNti zrMxni2|!N;&wEtGDP1E%IGW;SDic!Hh`1PJ9EJ{d^{udI3M~~7%z}TiAeCi zDmjGy6qIF8e`HkTQN*71uf0CVLGx^|C;rH zaBPjgVivme*vOucDG{Z&An@}c3~s65?XhAij(~Gv2x!>T3ygVh`5#{$JK@9t>)#n0 zn+WDP2t`%lj%E;bM#U_48o+myfcXJ+qFRSeDe_WRFa8qns#%{6(qGV6nA6jkLE|s@ zB-?o60`&+?V>4f013_2^VGOMidsureE7sP_s&2ITxJ}DuV#?37E&um%;UvJIi!h<& z7U~Ls^FH(sJxg5*2R0+=Z(Ea-71kp|h>?2N9<2UpYa0uP(`Gj;ZD1(U+|9>+T_0m< zrH8DiY^o>YV3P5;cG$@<10sPh|I9u_9Qrw@bCzP%R@>A&%RFN6X!vo%Gh^eHY&8igH4A!L<1P6J5xaN!0RykYOW zC~H}{ai}bfAfDuwIvU^5c~;u8^4EuNxC-e`8ihnYo%07MP_nS3wjErGY2!)HuFLOp zmy?uqbMAhEQEjf`Td*Oc@FSQ3K~>77tiYx4_8Dzz+_e6_sx)dn6F#1de4&$aSO0gS z{7iV=M%lZNOEB{1=DueY3lkHyryY2ea&_F=Pf{i`C}2DxP?N3*(1Q)sWtF#mG5V># z3p|DoO`@+5U;>|#KtU_`bcoIWx*aQBuUHFD=$LX(wDZ5%iQ535TrPtZz^p%Pry^?CCt|@u<^++88 z8_28wZqK)phA#f!Pe*5sY)odKPF5hI^jCdE>lqR<-s)23s<4kG8G&)* zSObnyw0>6xY%V@Gqn46cSUFZ|JhoFfa3)1|BqCXZ#d?q8M43F+YFF@Z5`1Ox&pg0N zJhO#!87pstR>B%gcS$TvEO&i-XqO^s+!jn-MP>&3QjNkl|AWI$7lb`jiu+z-Ez*i~ zeZNdO8tSR@rFQ?xTd>g67C4bNe33U<+&qD;N=^a6#8%&AJ`PnIW@n6?-bq9rJr{ zgMzL%kTdJAl;Iq_e*3=IJYk!REk_xc`{hP;j9@6gYbY;WkUl5;OyM;RlD3_xYUkOs zbie~1zYVG7*Q4%q{b^l7%&WN4 zL`#F(FeAsm(j(r$@Khy;*wzYbAHN{_5qN+*e~xd}PsN!yQ(9t?QTN0%KODg3;st&x zN(<-=DZa>QqPj2DXZAemmOl&|#>MX_-C0$tVZW1=0ZHs7_bIof<5tWe3YV2x(<;9o zHHyAMw|Fn4CF1JUd(C;=hPo!#80Wkp^*nta-EvxqFQ;X=CJc|ek*M~UVnPQy@BBwvy$5OCiyUk z=Q#Scr1LGGaAD`tl7U=EbyI(P`ZU+aNohX#Mwu!# zQ&|S9_Y?yZrLTssF%g6jwo^NdKVhgkQp%ZqmalPM@r1vQl;Q44~`nl|M2pf{0#HSyUTCe;>ceb z8Y+S$ZON3gPI^T@=unrDS@Y(W-wWn$moX__QSJ|pE@ew5{f(P~;~kDz*D`;JeMAc7 znvT?Nc5|0-!b+cP9K0|6N!HcPXy}q*{t@eWZtmz@LX7zN z)N1pWnA=0@=GHHF0gMS=*J||Np1@qSdJCY#1Q_B)D9@)b02Gqd-8)>_|M6CBD*bdcSn z0S8|7c@~#_>to8k(biU0^PxU(_a<#DJLKOGlq*2>xbQ>R_V-9@GMeZbIYr#>Dit?U9=)T(#LP!#~<|K z^Lk*@=i1ZcuU3_EGw&2Sh`~_Z(k8eukT45+!F}5+$A{fk!gR9(~VH zvh*2(Npqrk@&JJs7m=8mkX%$ra$wTLu2K{?X8!xkyEX4i#Bnm*($#_dF74*~HbsRP zENKKO?%1V(t$onDnZr2V4PewPfRM=@pW5M?Y9D$4a7Yed`U~f-Yn8X7jE3Ss9|?cT z>k8{yQjOxfS8`>cNPArz@tmjj{LEUZ7pQZE!1EN z9oGX=aD*o^mPwQt1{Xe6wG*l`_1V5BHM} zEv8h%iCF=c$&UT;5%>%wm@`uoHm0F%uOhO)`$|wq=t(MbzcFfk`^YHHAp-eRv^6gY ztS$}@V~>0X^R)V}c@T>g!_hkk;v9o3l7Vs$t-vN?83(S=Ug({zVC=QWEdi8J2&1%S z{4DL=RH2i~jvGPbTK7M&o!*jcW>?*pIxsnfZ~xhFu3uiwQT@nTBa%;wkOnl!iE<4B zqza%%=>y+QnEB=$u1NdpBr@s%$ny}aoP%aaLP2!J7O?M%Q)#tf_mdS`Kmk6m7<1dA zQ1t1c3aXPc2{1yo2NGabT=0AT#@5GVU#{=RdlKYQghoE< z7@cGx>6F!=G=;dCw(Gk1*;A0KDF>|+v_f+;ud|CX-WYmk+=SN^M(3<@ zSc>9d<+x-J>M;nW#~ja6&pR;*50zHBRA~3i#8V+zht?lQme;@`-HUKY)N4}XM2Z7k zbUk(5e*2YJ^mjWn!q_(@A(pjJ7Rl$3DadtCi4M9=o8-|QNx9o5ou3i%0#h!@A3hj+ zj&cpZC?x3=pUqr<;@dvufu&3id30edKc+Dg8{|_h?fn$c^e@`?SVA8|Rm)@5g0@>F zC);e2)^i8%oybds9~e(jwsp?+BtPp(*Rv(bTqCvr0zbRxh-{WChR7yAywyAlGL%vV z*@y?oqhfbY;I}UnB(^V=q=m*|QI^*-?)c;*60Gx#Rn1z2Ad(63?z;9CJX#ob5hhlQ z_t=_FQF3z;4(C95ZdWG#zZby?vVHo!QEX3#ivAitfGT{qj3Ve!So$UlpOSM# zLwcU;Vhej!D;r=k;Vh8}g@=b)W8oODtZ)WjOz^@tGKaRX+_8HaQnjiR3e>qOiF3(7 z()NPnHB>(eVwB?ULPaE6ZeUOlc}Fwe4@I2NzDwggT{DG}1{0AON!4CQQ?kbL`_-V% z9^ua6Q=b31yA&_}h^Eot$@;-hWC3O$>2I5R{J4g+8#Y-pmf5Wz!Q7(lh=sU{XfAzu zZW?5jl1SbZ;%a)&ST4V4Bn9Dh<^bnJD1rQ0dhLeC&L8r8gxf&u+4T{U4=CiM=LgLF zpZWnFDwLMHQN$M+H7+|EE!Hx7Oh!xiwTWI}^B)&kziF3T5pSZ38V!-|ji6;Cth4+- z_XE$BYveMvCaT$tw|-~|BWonp@6_jvcd^F?^%Qq4u*4}%3gNtCsZ?alw-TDK6L*`8J*n*Z}usUd{o_lZH+B^6za5CGHz;8bOu#t@VO0q4$ zl0vjD{>W;22FPtLXif3uXt>TZZCKVj0m7@=P#g?QzxP~p#$~vUWXpU3uVe44vn&&w zz97-xKIZw1@2U%}d{}Ipj{DmE9vWcA9A9Ce*LOOz4jR6BTqPRj&glC`|G1#nz2{K$ zYI4`J)Z1gmNl-W2^OwsbsRP!Ga&PtETG;j&M5`Yp+1OJOv+s6)H%&_{@fROzne2F7 z)JPtF-RyhDo0uGRyU{<9)4@QB8qCQD(Q;_6B;rDhumVTwB}*oj<&VYPj^@4wO-{w_ zKGoDA`=TrDzWKbOI7L*akmXafM|#mY;aC<}yem#S+yI#24PoES=hhy7sg&FDq3jO# z=I+IM>8#wy+Q}w?Y`Rt!szhv8;u+F!Tj)uiA^bLePf6#^^vbOm>l$bkZ=zN)*2bf2sKWg? z&9PhgVh*ZiFSiV{8Q)sO#qgM9Nt9%v0ZVm)UQV*akV~_%E>=|GvN+Uz0TFJN$7d$F zUmvVg%1(Icu@3$~)1JJz{+NSfa{JfPUf`NSPYw3{1z%|ZhA0_LuJDuLZYojAh0e2) zA4o4l8yg$T&mPb|qFhN(VZ7;&As*A(e?f$jM@1?D8M=S!b`~6L8N+1~;s9V@ z`0@7Y0FmCm6raHnJj^UYlXBVyoB##DIBDA3+xNCo!JP%aNa*jpA}}%4kqj=eW11DV zsmM$SNVN&DEZBK09dTeHG4E^Y#lx;QMN1q+Ev*+>hce*%pOC|5^pUkWfLy z=0b*DFoL^w_aMRGA$U?PAU{tHikLa>1OfP!6OIw{DA3&W7l!N9$m$uZA$LQ5N&l4i zc0R9f=i}Ld&ued(fDgJ2QXb86>%^+~=^xpV5Zw~jedmtMJ-9;^hi3XZ5#TOj*`%r7 zcB=EI2Df`Co4Zj-)HgaU2e;Cl!md)*UXy^sJ?}4QGf!lGXh%M17}FEuz*UD^-n+o- z3JAQxw-U&A1lM$I%J3QvHEC#sgE*Z+3|!<#e9r@A5}Cv$2v1?-&YXR$R-224MH%jZ z*a|yxvaY@K=iUCoA%mLa$kS~k8F=wM>NLULl55;S!rS1bGDUm$%Xl1};O-T;jAInd z4So-#4+eOxEH2T9>Xoj>-M6gCVP$Xv9;y#0d2=LQ=B9>6(E`{%okD_^w>KV#c;tgT z39`dDr#Fx(vJEEYgvEVmvT%rG`E?U&zZw;A?TEsGllB*%q{dHZu^n%10G}5xRr%a`sQYm`eBPSuqom(Yk*6nz#(%`_*=r4&owL|Br27 z1E?Si#7C^s@dAe)0pX=PIj`4)R%@Da_mlwZ0xhzkbRC{d5Xra<(7VFL*G>$ld>o%t zpKk4z+il2w7e3@N%LeRU!90L;I&Q!30-`iK*FfuO-x4;y0x3}yp7~l8oq;rhrf@42 z!R=Bt35g$Ok$z`hNK`;VUn^)S!t`-q)ft?ex(UKC=bqmWYg|e+OCNnY!~(gOSwXZO zBw8OOa3V8}w~X8Aa4W!#*_3SV7QG@MkP>}*J?v8P7;eOFYHAZbSuLMd_EKYHCYZ#T z1)Bkypq)+<3C=V78btff-W_CSkHH&}PJHS;u_^7 z6^I`(HArvh_NyA$(#Qlv1Jc;I{R0CX_toaNP;eHT%7;nBIiRq(wjhZ-L~!hCiqZ~N zlTeT=UzG_vK9+y)oe;_$&`?zk+jfGdVh+X!hZYF4F@?@ceyyX_qM1qs+SEb_4yL~w z5xjUC5d3X&&7%Zl>;K17{hS}0m=fZ=*;(%UeVOm+=^n{w)FtO>+ZSn!Tf*!imrWQo zMawW|yD#;$6F+0neZP^eNb*wHrSr#Eg_F5X%MZE(eN~zc><{1fk%xBhF#_4(yQm6O z04`N~>F$0v)*nV@4Va(~)IuA{F!&ARpQ#Vj-s^ucs6+Xs&>dShU)frXnkn8~v3%-t z5BS=dvGlm3vjBj%T;tPK? zK$4+Kn2wUgBG78(r`-3zIb{lfuU$Sd0p0!I&-Xn~Bo-Uj{JHX`BjNpBtm$zMvwg@- zTiNpryo65RTDASpYr)LgWpeL?1~J+!M@8*uPm>XG4hz3xCkxNsy@EN~fj_4*j^Yy9 zA)sYohZFhoa95Za5FMnRkBcNaRZmmNu~Fl7vUCX21B?^tJk7(xt(5q*XCus%`ruKr zR}qW55H9i-e2MOmW0v=C^J< z+ISnpMCINXqY7i8QE+puNj16ER9pEAeD=JGi3{p3@~J0PPcM}8=*V_wD!QVku&M9< zzM5g66#+XLb}`vJ5$Ji13fpnks%j8aEKJCx%7iPk_P>Ap{HWY6SO9VBn*?4QA;Ro8 zKD>8U>zj?RiY)h_S9)e`-n>clhEF>vLn}5f4%+*O+@8B4I+1Vol;&l#9@r=DR166# zmj-v^yWPUiczEh+n^X4kM3!=*=*yXG(^~cLc|%2-kHES3&mK9$_yC-6op#X;5@!}c zx6}zI>12Er$TL)`9f7A|KOjX&=Sj*;>>_`y{dOeO(_1uJ{SmcAeAW$Dn{$4T@LAUA zjrVb8gBFT?iQ;u(sldL!h^r?vNwUJdhagDR0~zgeL7-*VA?l2tI=xti%iud$BYfCV zijW!7rYbi9`>%Xfh4uGk*Ou51c9VbF5><>8e{G2{tA15WO$}ozc3RY-Bqs`X*u7#! z%fP)QsWU@6{e}W5qUf{<6K~O`#eTc*ed)N4s_y-j3gud=pQp)9p^(3t1aZSCdvzql zx2`$>dILV@?N+dclC4hc!_co)<2dZ4lJSD=;^SMDs)fI9z3Pk3-0wx_WlCn&o7@nR zW-{gHlC$HtpuzFX^VkCn{cZNUlLU>Ax&DYH=S2_1FJBfgiOcxxZ=D@UlA=PO0 zdO8dXC5VB6MmxokeO_c@Tk?iq(x4ODb$lP`)A`{HhJ<+5ztZ7!Ys70#7NzSq>|O{l zExRaQBMbPbym^E8nQ{!J7TU<+AK%rE_~o>bL9bE2m#+0ar<-jZqw;`gxxG zg0M$599K=@bg~w4<>fq8tDUl;g847q&hDV5hbpSVasDAn@y18Tfc{MrU}crvIaDt% zEY*T`VcsL2$L6D;Tc(O_R@@l}YohT37|G{G(5aqxvC5rvr<*Tb78c(5($G-#CSdEO zilnO{TVLpUv6Qgz%2^JUuI8L42Z}-)u)l)d-lhD0Y-(SZlRcnr#reqaX3j~{`=A$u zhGc%?{r6iY4||BEws-D~DGf818n@k-B~hKZ05?oStUv_Mb43^9rKIk|AK9)I>wh7w zf?zQ%qphs$Jks(uP)DO984=g&`?yHHQKU93oP^AKd0l4g15!4h0#9iL^{kBfepY#*4J3Lt`8Wlc^WkGv`(wPhF7_AH z2Y&s+uuG$zCf8YaCXn`Csb%fX=HMs0(Xs{kl-H7Ka%!Pt9&so9M z_<9|%sE_-C7r$owwbVfV-t>%c}F{ zt^=5p+C6k$BVuU1t?|aO_oHFfV=t(8heg`UO@9n5kH*}tm~{W5mZ`5~W8>(yVkpbw z##E=g*Ih`VA-eE39;vG3jxJoBn5u{tQ-~q5b`v zqRuIwy1%MtIsCd+2ZK!c@F>Wm*DI$0L>e92+jRsfr9sOw<#o#&n3ldK@#k}&BiNcE z&$5}`0580Imk^^_k(eKAss3GA9>>A);ETm`fuLu{*|TX(6G_l zSJD)DS5i|8a=?crdvc^bSYe+75g<~YG@5lnUn)+i7p3t#tvY=jb$VSi!?RiHq#DjV zEu>Jp>J&5@Ry#V@KeTD~y>Wd}C586t@OogLCU<|`y*D1Q)${H<9|-+3Rjxkv+&E!% zyPv|!26H<5BXxi1Hvx~wmqulrS#>>%Myp2I9{tcY^jnu)wUGgFgCtv;%UZ$qzDmJp z%D2`G0h24jLs<}^d zBfi+^+UQP-ym$Z+#f6lVIY;cHOhWk`VIi}+6@Z7DK04iT?6vN0BINtnQl2 zDbkwU{5*ke>c;9O`Br(4SRUIN6lo9en(9nt`#GiLzft8)cE7x!$~!?}<7KX9XuLYf zoj9XG5t3Es&0oJ*OT~!g*TZe2CgOg$={B!!Xr_IT)Cy-_c(?G(Z&;qlVSSL(!e3W} zq15z#S>SrT@QYK~LdN#RalI8?0u3QSO97b=ItI&M441HcIqv&*d7Mqxn~0*$D2U-A zUzoW@v1Kng^O)t>AGkk{$L!pFRX%xdVKByx@&_f8TI*mK@3d3$)m|H!#xa{hGGT?j z$WQ(hxEV0Kw#+~}bJeL*!0hLjetl`uQQz*mmn$;gSBe>v;$Pa`Xo~ppWRjX?AScSg zb(*1{-@&vHb6@vK{VRtlJ$`7T9o?5i3o+q)+qx{c+?e^TsVn_AK%uqby1(JhY=^^? zI`wh&Z>@I0E3ZGJ${B$DAA7uZ=3P%uGHR;1jEH1gZwxvb{}k)Dp3QBF2rnNyoxRih z*wNq%sV4Um>q5-k{<1RD>$Ovs<+p6BIY;K_Ez15$V>kGrPK<7TGd&PF#V-FX%O$7;^HaD+l2Xzg}(Omt>gW2Jk z$Nofjbb1Z%j{B?CQTqhB^ZdIpR-C_`^1W`I^Z2~{<*mgZ*6zUh*D~w1YnFWaH|}C@ zD+CmM%HQS36-o&yG_>)2GOgLR?A_aM+UK!=Z>lB3EuJiYKb18WLfU!&6V(GfkwcVV zy%q%Mlv@83<6aDwX-s|2=@sU^3VoI)$V)kkEkN5-1nG_y(oc6Mc>yX0VZx-R(uj3( zmxPAmK|kflp%oE~L705X$Qw(#pWoa6TUj83(om7A10wD58J@Rt3ZMP)bnEt*oW#aV z_T6s3#bX;;=D9k%I^F$Ya!cL&g~R?VPtVq{{N`LEIbT$;-sYM>lo)RaQ&`@5TXK|` zS$D1-VS%aN(dVCEI1_)q4^eNkdIxonAhxzYZgFAJ(PmN;!LtMT}7`bDs78M9T| zH!1F4OL;2RywxEfg2L|HA%5911;YVBEjCd?i0G(6@bunm*zfTPz99_6U6qnvco{}` z(2s;P)h|JDVTAF<{P|`cVNe@4@J8EI#-fO{IcU(WpYp3VmxJWJ*yuLhHS#{&BG1nW zC9!80=BZ7e3T%4b{8zT^C^K1Ri8E73T+HWQp{E^tBloW+Bj$k-yU*R3;ZRJb*3-kG z0+X!1Q$$6eYR`MRi!yjGkcB`#YeH&VIj@Mu|762ZOcG(&mId&hPTYC$WB#;nT%biN!>D#noWbsP4Mz+o7J0w$p+ zjWh>vST}xd1K3DSPb;zVB(Udlb}E3Rn# zd!JETS=&)?XY-IBM0;LW*I!ugw{nVG7EyhMTKgi9=6!(MWAW#CSa34A|{qs`{P?D!HbHum1y2_l5>^l5(RK`Qfjx8iC4*PkWXb0uNy9xzcm%)LLUwIH~>irrR`0$ri5cRM1;VPTyu-+mAkUz zzs~7+1Ywgd%4SnUnxYwcw+bCZ^N!IHzvm)q*+0cfM}I*nT}kVL4a(Q7P_ly%suPlM z`1z7+cgelwsMlV3Vfh-smdnpMUAd$bL?7)|fj?#)9>2p{>fVtt zx7fC9PgUr}tAqZxZL92e0?x>;+v9CAsNWrQ}_TeBC5<(9SZ@P~7gXaV?`VNsXby-K9PnFs9rc zOr*#ROeO&tGZ$_P9CCv%JO_K%s2NVuO%^$JCQsW@fik?_JjO|MEW#x8p43?`P50J~eEiuPd+1vJ0?d0wi;1{E}Pys>p;BeWhqA(Uj z8KuKjtidQo6(f@wrui^z^CqP6sC!&5NvQHMbL+Q&#daK2uwQgscbw`l80CXaA6 zH}9XyVJwN{lXVF~O6wn?@XXcm0tHFeW1{RZ7^}*wJNTe`YW4MrQbIBsXg}Wl+R$@P zL88!3TX>!4EqBn4Cf6Z;tL%vjE`XYAO#~O}uj-J>96XZ|G`@XK*uyO=b=fF!hN9T9-1Q%XFIEF|EX zN*u<4T=f9oEz+?JdJ#j3UrcAD?Pvv8+r<}F6k>UP@+r+6<9KuvkN~!54jsGaP)&r@ z62*wQJxOq_B4g1)?v=pqWBHsG%+Vf_n3MNc3Jw)Q66MB561Zmb>PCfivxu%l`%9i@ zx$q#}BSNj;_)X907WTG!lH+qA{1rwAqN4W&r}e%5$=Aom1U{hZ+ z=zo40w0MG(fwH*W7DGNQcwd>l3@s}_rZLJtr|!G8FGe3wQ?<1OD>bMBPf%7X2`h|G z;>8Xdio_(tGN+pRe!E}GmO5z_@dMNeQ_cNMQpuc_wdWP5nOyU-&M-4fQmJ;`&P<8j z3NzKD=B5_DPFxjM11rxMHp;X`^PsAJqdxl2h27gXcoA5Npo1fPT{kZo8^Q};DRna` z?gcQH2i?03CVH=$nWvwn<@o`f9EMqWL(o{ID#3XR1;Z+v@Sqyz`e#{t5AF1kv|Jht zEMC7741)8DW3eK3JkHrt5HB=wZ21SkXI#QJ&tWg6)S4zZKLN$4=>|y(M3u0ggH`}y zxR(<30!;O`b(fGDH5sPX@>!YyHDOriRRYm?UStHm*D%}HSfuT00JXVQ#tk4K-M7ms zB!Qg1_K)u{oZ=z`?~Vu$xZwQ}>W)2-#F_|2Vq%?*yAaBtDkjBCi&7>rokIV?tM6P2Nx-|?cboI z&47eZgaoDGv`oRve8|La2ke~uLs+Ekk5voc?%#T<@$pDe#<`+p3+#-Rp&F)}XaI_7L8})zCgMg+##&}tF&|YM0S)uj<6%~Xs6A@^fA$#~Q zdB4A3O(Y&;6lD?hxZ9vyBX{rMvybv`4<5MBSO&;H1w;u@cmzY!xSbDMcW8@u%RYu zLT0X?*~k@L8kX~+Dq)vWyZ|TkyC!>pZczotfq8lGAHvo!H3o>*Z<)E`jxtsp1)!yC zO(d&gZ{PPf!bW32=}?GNxixT`qEPySeuXzwGQiw`{2>LbSJJ)Op{*bEIDJk=dR{r_ z!3|0>WhPOA#xAa)*UisT@e<}Mi!Gz|9{}vYrAUV&7l;}xzCi;*j3mgs(Csgwc#R6N zih>{C$c#dR(*j!idDE2x1Ro#l{M^4z6#*QACw)~J3GdIb-vU&}zB@EStb^A1VFc*+ zm0GmW!4Uv|-m1V0rZB;Mqsn)fz_@10hs%_w@l(CX@drwDQ($2&ftm@YLvSwg&+tR6 zlr&jm#hV4GRU@{2JwF8zJXtwPf7;5tlrg_xDLmSak zz4Y;Do;C~)-d-(eQPUw6Xo-1$Y(xpVmV#x+-G#PZ2c|3!IKnl7-cY(2W|t2-3{SGEDs72P)H;33)sK37_Sm#nGZ#+ z4^9xRcIk#Y`$CY7AU;t5{cu0AsqzZYffWgg;BYy>-m?ccWiOiC7rudFE8py-;CDxF zv=|Iv=16?(Itc7<++J=-qdxZCO)NgtaJ=nCOXTp} zAECjYMX~FkS?mR3)dH59EF7Zu+E0&}fRnQPsMa6N>L>48tkLRFw4THfwsYY?gtp*w zN=lfRD-RMcz-srXvb$$EbJ$}Wi*l`D>k3oK<5l>nO1ijfWUC;0><23w5Qn0fT?+hc z8VRG|rYZ$>VCCdl)lR-4$pQj31wQBHl%o;T18}mcesDl5Bv~F7XB@xC!_gw-gpPKw z*C33(XigB-nv1Ga~(15rKRxc+5v$U+jr=ht5hS8?0mhH981=ZEGa;s)x3 zHw$l>v3g9lea1XLk@{rtrA0rh>XRaGw<4b~L3*S}5((POCGqxA5W4BTd& zdl-^tdJTan@MobRvZ6-*UW1-M6Iyg44@UrSa)1I7r6pCxgkOz+@)2XsY<;#77^=C< z^!P$3o^0Za*^Wv$L1M1S)~HbPsEL{yNKToD+WmgYI871FV7}S(@U@lxAJh5EmBtM7 zw=$qSk%HO$`O?qhhuEEO-yoyka{lFodoTEs@B>BA+q0}jr5?Vxm%HG0p+7`ow)U-i z?i*(B#(j=uA~{- zZ3%a%HakiB>u5AIWByE)zwi-m_7H#;ZQ$Zv(k(3zS~`9?tKJK4Lak>hg|7g=_{Hfr z2K#&R(br|UtB08=_E1oyh%RiiUm#9$S91&}l4RC=O9Q91(_;4P94NHCAjFtU@@Kad zEr3B8GS;hqOHe@wJxlmoH4{*ZcuC>cOr-=_E@j6)m}WG*8pL4u%9!Rn6!VB=Gs8Kkn(r<}5Jbj6|iW}k>V)z`g+*dm!Eomwc7>4ag331(9 zjEY)sH^ciq7PD%*wvP1i4_Jc;?&oW~JtC??=MvgQ+H5DYVvc+WYYOfA6M>jPr*<)DRX$*FthNf{Y~ZlL&342wrq>Cs@-JN z>jG`(mt9p8PEQw0@T+Iuo<<~9@a14An-!cowXoe_fk>h9{XB~cJsX2(%hnz}$J95X+DZRQw>Id8V4C6#fYHv833tw0+n2^^@w3S17 z&P6F%jBH+E_E)wOpVJf~+un3qm2miQNO^fVc?&5tXT<^~a?wyA(;CT@3rb=3VITbx zStw(FKo`e~tLN5atRnj2vMAD@pWsVGGQ@g<_}VJBtY@;%e13B|)05;*chZE6v8V|L z<%wXl`TzlmPRGI%lfB+dxXqw$Jb>n*B*PT>9VNpz6EB#-hT1Z8WW8dGD*9}7lx}nE z@;)mu+#<_Jx62Q)Pud@(XIbddM1mtd(5g=D5vfV@gn6`eNqZVOM{yt;n-Aa~vSwH% zU+UO`;#wVW?#5-4-FF^aOQoNJ&@An17 zYDpU8m}eTUgQTaC!UN*3!In)3e9ew1E*!O^QO#8NfttNBsxGbg}t>)!Q zN8Pd4l2{sATibc{f1GGSiT^m!-Zl00S8C+iibvlRx5G>k(piwMXAC3>V^T9zaj;or zq?sdY{SPsvmL4>Vb~wtR#a=*E77HKlOddrn0U6D|cazl}JT<_rxokf2A(%)^{@K;M z$d}H7XkisvN(s39yO0!Dd<$NM?9$&a zW0d^QOE!;YHY8M~;&;r6nMM7*wOy>S5cHVvCwOfN?GvNxL25GS^K;NL^sD&4Y7}Zk zYqGp9rq1D&?O1Ah5Mo@VgZ~d&@Dp@s&>`SOK`FOk4=f7S2nV_ZygL%J=!+r<7XSMm zBv~~IKO?oHtdx|s-QimnePI#k0~634R>J!zaWl|q-FDxyM?3w_vOZ`>7%gO>7l0PI zW0_$2^MO@J7O6}O?K z@u;?f!es#S)3^pU0zpdXMD8PYTdn9Dg0bft@$+i( z)gIg5PXr=_Jw5ns$6g#IB~EfE{sx=og3Y=>DYU@@`R*azo3Ibc0)%wk;703V-_jyZ zbn8d?4d?&HyJ*NHf3DkJ;8)i%hMNNtf(|ROpYW;!rO5o$%#S9T->w^s?EO{T9U0e&>dZzC}@IXY{L3{JC=V!sk*8-l0FFZc{&H?Xtjt+lRzA9z6nP%_+PvsprDO zfz*AjYd_ROyLG`Hj1odD%YPn1;5INiZNx9am_~I!VrLO`iWyLLg&31dUE3uvhpQ{M zdFqrSkIaUewd8qamkfIyIzZPW`=7zJtb~2y@(h!12juJftl-Ov8ghis_19;tA;rxH z`?d{`!wquB&?Z|GT>{a6g#v|>&BM;=;T3b7{rncukPk3z(9%3){WQGlw?x0NQJ{s( zQoTWo%!zY)XBTVxPq1kjQ(4yB!WEv5)Y1>9b!C+d@+IO%b=)1m?}cGz-pRN#lHO-A zdvgO7U62$hxZ$J{#R2`U9YZHMo$|9(o$S^`mHHrRh$*jiUcBpltvpFj))B2e z?`Ox-+QTMM2Fc)`@I~IQd;Zz;E zRkv);Q0Z85+2~!In+BXz zsL?f$iXS@uAw{D8Jod~gaHo=M3Eh7}a2s}S!1 z8F6}$&f+-!bH}9at{n@IpWk-3hxhl&P@dIoy9v1-1=GVA$?0Ll&jVK32k62gkpg@d zjRN?llLr&cFBamb+uN(YE?tIRF**i%-B5V z`=TtLVAB2=)f=dxg({zBVN5N8;(gV`qXdwp^oI{s2oqsmy@5ksQQLfVT!qL)Tbvon z=*j9o%i50L9)ynwrN?kEX-YEus-CBQ&FP}b$gZd;wI6g%tK2#oa~H~*;l>gIX;@0& zWcO-)tbnSEL9|otb7+kE7$UWPFi1!ibsZSm5(RESOlrsFQD`a`26R{Mcr07OmnKQTy8p&MY zAhZaWc={^H$&QKXN+VUfGOsdLCB?DBH4wHlV|i0H08|i}_-8lo)Wje_aI==(FJ-Pc z%U&iFDOVyBCtQN0?(z{JBc5B~c*7M|lo=;OQfmz09+w-|H3?~njgTvJ@2eSYN$v%bku`)i zIeQl8psE5^*k0)K(M~?;S>vd_##sPJYzgTw3TU4>8>*l&+|sN26vSU31!tkT9awEA z_MO#Cgl=ab%yAhd>i3d9ZOP{5^E#0f;oFF=Ffno>5WB`5c~gO#1pX=OP+PP`jFghy96I9?;Nft9Jcs5w!P#6z zb$TxdoZ#tCzWMqdPYn>0Tv>XBVp_6vn>^1GjCO&ppE78Bn zbY9h04Q~3utth<>jDD`wa4q|jB2aj{f`$Z5bZ8S6TJ{*yeIFMB0*9NpS*Y>xzXaiH z@}T0iO*wv^^YdWqTQGq^a7W|d_j4S2B7t+T0zpLQJxGybcx;kD!-3VdnvQ<0N9NV~ z0zBZNhIHSZ1!(vIDg5&F>%@8*yumwaic`Qfl!45ezKU!X*4Q8~A+aM8w5+<>+ZUst zp3K@|KxxduYwFk~&s{MRq6MzW?nLp!2vF2sVgg?uu3Qpc&A9^h))4SxflPcX*Dd}ae~nLdR5O^|hdh=TVq zfwU2$T(mgv1BcC_5o2HBsIr_2#lE%s_`b$LSCkNV0#XfV((PRB;59C@TqB=LQW=+p zLDk{U(j&@aok&g42azD!o4muPj8S$>Ow1Ih9`CL96}9oPm^hn?CPKT~kI$?ugN?r% zD)o$lmtV4n)ljx32#Oy^`HFuPPwAq!fj@SDH4>h|dG9aJ^Ic{69rrDM{mxJ;Ihc$> zKt#l^xkWa^LL=-k;I2?k%q*r)poT0=JliME*0~Ocy~0}2TKwPWN_ZHH;8cTL zO-Y3-yWh_*mEM3{&HDf3YO0!82S`ZFUPOLMRHqKTM(NwPZ_PK4wGY9U_>=D209F^03}a4C!R8wjYs5UgiB27y3uN>aK%d+P zxj9xa$Y*YA!!i-YC?6tXQhK&~0nKfhZm&Gk6k%bYc7sG*kko82%LldpJFn7|>%(J6 z0?P|SJMW{pUOd}xFgjklqA?ubDv5viAJLF^-U)&a=ul5d>;eSAyO}LC451@^9b94b z#D1!0bk8ts^Si7XcP@W7F3zFxHl}|_A1KjfqZpw`zu@YaAP6e8uYbRhZ>?PkZ{aM2 zHqI;u7Hg>$8!K(>JN9vbKox!~d}=U494F1>>W57c=D-(~z&gx_&_H#$1rP}|4=B}` z4s+T-7K_Y{^J&e^(k?^rn9^xYT`>Oj`DH0|ZOB23=D*{sdV6%}GhKKkq=ttA+> z93i#r0O%Xa4Iug&s%noSe0rFZy`e>L-$3kv353f!5ndwpsb*HsGl!+@MescLh2P~x z=ui`RcnX!Uhd@aa1js*7p;(LMwZrSZph-spHV6hnABdpw5qzSDz^`ocVw%Y}meLax z(Td1`08pp&A!BBwwkoVXb48X;l)I6UG4@gsX_L`o4FI$`r0uDtD^OFKL))YnIFC+G zg>5)_E#L%$7$7UE+ve6iBKY4$x9n@Kh=yR3yyS#-00rJHY^VlLLF?)O!-1D|I$b+N zXd9t3g5`VrTxa9~2at19Z2Wq@4tdyC1b=%F z&+C#s1ITVaIMCr3|D-wQKo4hSaM>A0*%L^jUge~ZXD zPIw)%L+G4EBOvO4kt}fIA|dLOAOp%@4*tVSU;(^;?_tWX`k)9McP>(Z)qp4s{?Hx7 zw<3+n0yIHbfH&cLcnqoIfar3AuR!$Q&qJbwFP_U8ppxkYn>8Bf(TKQ!KlfLUrm`;Q&r$$`Wj{Cf^=fUPI+A~wh?0;`PsK!JxoQ#YiL0#B&- z@0)IXjE4$*3t$rJ18!rI^C@)sdT5UfOzzjqxbf641P0>Yx7b^7)m!v^$B_Jo`iI{( z_DHFs20cwumSE+x+)F^N#GC|p96&aOe>dr`@$Z+c5L_lPoS0WAkr(OtkUw}{{_oE` z@&6s+x`0$H?k|? z*4yBRNTz_oL^z8p&`%cdP%iNP`+lnM+suQXqKXSdS|MabJb;oqc=7N1CE)eOf20$z z0raIiAcqmb!T7+LTM*w5R$j^NzlX#{d|O6$7zHST^2Y>a$z~m3vzhP^0xGt!V`habuV9_@Z5(EF{%^-N|FOETgNC60I8jwTNBVV(O9d2}- zDcmHGvZR|0E?&WK#zkaPe4Ey%GHO~Cof1C z0DhhWK^w&kUcnr&G68wU5c!sOF}BbcQBo01Po;>6-SJ$C0yKSqOrU6O6$4Uqf5H%C ziTa?}+UXF`*T8K{E_$htf9^jt+TSQYh4vCBiNDV^UBI25u{kJ;wpW&JW%9#qo0tOA z#lj^NrixktgeTVREc+v*QZV0~&J!{;9fWIU58Cyaq*qANA>!~2mEBb)*=yWF{HHsH zik)~y|L22V{=M@zJqvEQMuNdRKL#!$6bNCb0k2LvJ|-s+^DBIYKqW|k!2FxzrGUGg zi8+EEvc@X>S!v)U(E&%nFcLALFoFsjQo4If5wBISHoLpb02Q)9@%rNOPcZkyM+$U* zHy%;qU1oy+a2@~^2EdwQ>?X3K8Y6btL{`AM6kp#?%T!2GE zI^^3Dldi81%PcLFlvIa^$@&f*J2yfz-BIvJd>g2+BKH`0TLiu*e9#$7I57qn;QGK0 z6&$-oAl`15Kz|E*GIkodkRAMw5#aEZ?S&L7#h`!bg}tm+D9KYHj;{h8E%3+78qe(CNdfWxLzuvF zAZ7~9%us@L|Lq+_N-v|M;qq6wmKLC-M4|2I+eC71sC55UKLXo>qVS?*r-0!%f%T;J zm{Ig7{#W;KnH2v6>oD9K0!*I@o=r_SIdYN9;GdRxQLfrF|yg0a)~q`W>bGPhD@kb&0=d8L@OAYcB;!eI*so zf8z$-(opbG4m#iwgBiNHJqYHVd*3hz95~a^O~`v9q=ojh^=A!KOI(vHwKY8KIM^9J zaO)u9-*1m>TI{yU6gXOnQR5KqYyz9p5h{iBKF}KDz2WSCU<>^tBq7J=MX%+1R!|9} z?YDwEOP}&oTcJ;#^bx9MuU-~J-sQU3~kRVjF88jwAkCg|!v zl0^bKtHXZiJodT8+>c3PwckbTR0MuHG@f%F)Z0zjp;_pVOrhaFsqzAHGEdw{l zMwO44cQ=%l$fKktg8K=<7vU}@E-Unh} zM@5E8peaF4iop-zXg7=U`-8sd-4HYqHk^D5p*nMuRoJvxqucXIpHW1-8LybkvuEu> zXwgXnwNv>VEwLMsQU!%zSiK!=5$XlI@H`}fmm<{@9Qw~x`g-BXs!HgBFy-hO7qq_U z=tFI{-(O(+(QT8YX*gcWMY>^=wmzsxqM&*D2Kt<@s>61G94w@T<^!r+^$vwe2T7fV zF$rntp~gSEfR=K=f(Ju)*DS4h6j@FKWihaP|EkfgHl@Cs*JIzI4OX0&cYEVUO%-9{J|Tq7IIa{a*VwD@c9nhDD1BO z9lgay8UO6TFzRmKcT&&OGJ&(v-tI_db4LdMOwz((q5*Mp})|4Z{mqMny{`+r~ZK@MTVD4!Q;p${1t z>peS!xyJuupBBSby8@Qpqqjxyt7A^^IkVcdC9L-Haof0$?IyINct;S-7QuXq+s)g2 z3Qr6MZ8?C>&=s<)@xUVJ`V41w0i&2%ue)qT0?swI1(2+9DrJcbA> zsF4@!brQg)b+Fam1t_mvz>O>|QL%~+3(u;*e8IRAI$O$rhQGaJ^zblkDR>oA`uLtLEF2q*YkQZho(F3LP?vL%HG87+H->`|wXmC7jl_k8QTzSni#_x-r<+dpTIv-3P0 z4)4$F{dzsu>)4BG9X+D}Ha074FswN34DV>iAW-$8UTMwK-(XIwXxK|fc??BME+!l# z3F(}d)1&DzULbFVV%hRnifdD`QA}O&$M~;9v`_5*uVjB+(?~@3uon|R3bx{eN*^^d z0&)E))6ikjr1=`N_el1vH$1YvTV|7bZs!QrdtJhF%+d-UzgQNLof10d6cdia1d#u@TPLW9*|uw|LMct z>fXGr2zSP_p3!5!>#8<(b!l-f!Xy6K5#(hkd^k9c4D09 z;q{naMrpuV`*;=(v+TUkeP~iL*79p`_fO&iZBdoz_}UnWv>zjg8T7M1ph(uVLI>{# z)=s$XMAH(y6=l6fk%gC{)QlREUOT}9M0Bp(;2$6`f)EN(dy*^OJ=0eF4zkH@ZWd%j zBsc%cRVZJ*>x#tPi#vqD=$VN6dwZzMNb{RJ|JWgRLqR|4Qr2|4$JtSKUf2VVKexoRnGPS+rvU%qj=XFzdA$Dxd2z>t zYiba)!vt{6HUf+S(?BwXdtM(9PzxX|350Hfs3=&c4L8g$50Z|K8L6Fm^xyexG5?tY zR*(2!SJZ?*GY^*V4xFFQz?BHjA`OAXQ@mTEz~K1%Y;dR52wdt$+5lk0^z+teej@yV z9btg5pGRid(~5Q95ybp&_v>+id8sOlinz!7dRIE*mj{YMM?c#Pzfbjjkl2$-y!U+j zK2(rMmxuO5M1`+2>)2Wg-15jTn1=eyGRZy#z|%V4Udu=+yu%f`4YVaK$WCsP_z$BnID z+8I3mr{F~%%IDqdF^T-sYA4pDv!qv7y6j)28sf{4u42wB)>Y%U;$7J2Hp#2@_Pl-V zeFYNz{-UYl;~O2+34%Ud=nL1golGk&qQMHWD7oY73lWc0uWl>%-y3LT)9 za8L!~O$^WSf4NnP`-G@BnQrpIP6`SO11k$R%kTh1bzZqFytMt0jN^bd(CtDVWAF%0 z`p1slk)8Q*z<_bDtn8_roE#-3rGNVVeh4fo3eixlFK8O_8FDWVJD2}NzuAb-jPuua zT%YU7MBG_>5$gr9tm`<2P;C{J<#IUl`0_ZtY^iNmLB3iy{Ca09Dlt*aXP>Ltl#e>M zcdHYwTMi6q8y_<2MJ)F4z#i7)ME4IZ-*cg~7RbvCG}}z|NO_YlBAB%NXQgAujzhML z3*3B~k&Y7V;{FZOqczyDDfGuSg)i@_u;imd3jy$)`m`2lFY)ALc+`CAzubk=({DUK z6_juCD(mjuyW)s*o5m3+D(va$u>^EEF%P7FLXj8G+Mh_0GCQF(48nXk$?E!kq8KBQ zgVlR#k~Vmob@_qK=s^B>-tI^qON8#&ep$itA9K62uU| zacZaF;4a_DNV|~`0~}&orF{5C4jnw$+<5?R!yQ{6-xb)cU$+k2hAw^w>1Mw8yy_tt zHbNxF-ajVQx8A;0yQZXb@}yPGy$D#vr(Zbcyg{9$C|3@)+}VjXN3fy&o9F-Bg1{t& zWK+(ZJ>F90E7>p7emI}(5craa9?Rb17S0=tR5CHjr?2&FE!I@Kk`aWo^3(_UzMu2B#i~L zt=sB-Sm_J1rmx!v@!6xJ`r4jZQtWj!jM5v})sG zEFOe%W3@xm6o3UAn|1t|Y z<(yP?!Wp16winCfRWe*xY5WGT$Y$@OGuI7x`Nfg#%Beyg>=3qkX^Xa$JxjdGUm4Z) z2F3apXdaBYzJh>q44woCHD5w1OrRurq_=F{>IS5(_N%wIcgxPo^ty|@%BB9MJN!aI zl2JQdY+Z1r4Jj?+c>;VcD=bW4y_)iIV9i=wgcm?>AXg4Ks7|)>DdRfPj*K zbWC5$Ree5yyQ-({OnNA=n?%ShblI&tw~|e9n{Bq^$E=lqwxvwxWW|f^nwAP0YFBTR zmkf6m8lx-x)!vA|_YpIDPF9`x(m>-OZq>j~ z#;RN_ae7tDKzT&&Uf&Br%NfoGw{G6l&6Q`Ser&A2(FfOeE!-ASv9a6Q3KQ;^ZQ|H# z6HE+_83M?k7;${JLc#(GCb_tzhtscL@4j+Scm3F%yLSaQJETX|BER?v+O5J?M@dP^ zy(Z;t>lZyVHBZ@>cF_K2>FV#kTV)QOIUa3VvbWB)ucxPFr?{5l>Ie5GCxm-LCs&#d zbyZ#|8tIxmWY6cLbE0ud%U#~ySYSGs?R!U|!Tze5VI#d2Q-f9QmgXVBA%4s1yhd;G08ZCtZu{M8|)S!4IA`0~F$T*V*k;mTU2 z<T2%fI~`Rcx4CtrN2O|Tn?6IJZ~G}PQ8U)c z4vYA*0n)pS1d|PPnNTCEyJFPTSW&;@EKu5se5WFhMH9u#=@hBYSL3%_m&{2Ud>5*c z<tY31&;HhH$UvPILO!zn29Otram z^?h7A@*c~4{G`-_ZqBM&?N_p&k+3)$Q8jqR-A~F`@X-4mC$5U$>WuYS#U<6y`M_LQ zEgwf6y?X|!(XEV6$)!6tGC!BrdNUsFz}(rhXNSDYk;{_=1BA+UN_>sODU)g!L1#>9 zvPfu7;F>)&MSXq!5DB|eapUnzo}MZ9z_>rifl1`rWQB3-npmIx6T$m^LgA|p zp*#0NnSIczX~RAf+juTCa30)e=;T&Aj%2eg=U`()8Y>9FZ&3U+!qVj*7#I(s`$@7l zdp|Aln_l5mfUT2l1Zi%9BUbgUyui9abR-|qLFjCXmZ)&|;6}e1;Ng2lRH7;u&%d_Z zY`=Pb`hIMh%&tMRlJ3JAu-IGWaxEr1M}WuQu;xSi;6X1TdXl#gmLBO_$S-uM+~{Q9 z;|u0t8Ko`X+Q@vwa7m+dY|UL%h8`9bZVQA4WnEHLMdx5-GKrh+f>r$H>p7p#Da~wv zIU)kfs)MDdACGWjhh^rZ=$|WgssLwawZAbel7ui>CBKbPT)SqSW=at$gwc;Whe?0} z#bBuMEpmP)0r=XkY?$9X@`IU>*?5 zyNAMLILm#j;oN0C>4y$__)(%fd%DT2SJ6B(bWYAhcoFsEd0m|*WR-^O?Ce=Pk6TzI zLhDs2L+=qO((^?{zO=`_A7}frJ`R9dtKHg&)vGz5js3J`YvMU6_p~#4>k^+bBm394 zd67)MwVqxZ%VPz9lP1}|GZHocNGl;c* zWy0iONQ@4{anFHx#Kp#bJ0dsrh%8+KRGC`7I1_cNCBheT&o}5VYRMMmK_@*^D_5e! zn8Ic&tBRk&H4aTiS6cF>DeA`(oCrwLNflIO^-&-+qj4a zvt|;}!tnw;Lsljz*vF4IO4V+|V#5Om5Zbb2?b@|TBJK7SLRspM%4P%{kh;^FH%v?*E{bNs@*3B! z6%d?6J?lOU|1EyP4qMExA1lW37)OQrMxc}{-`N);30bG%MySVSIP@ERT=uJu^KSU$ zPGalDuzPKa-Y&o*zpyBbx4Qkk`NI)_UC&FVphw~t6VoCEHC6jG-H~|X1yQ&-O@*f$ zt3n&P<|ZV}%*@K_RhVzy{o88~08kTJXtrt6zsl(piwP>H{Em==Jr(PAo;a=CNiwc_ zDpcfR2cP9tn{yJNYSa8Cib_0o=1eqszQG^94LrN2$D0YZlX@rvc!B)6O@#Fj8ExN| zctL`C*C9G@s$+p!u*?6{(yz@h=1dsyVuQ2rH9Vpyf{>1xHsI>OD0%%tPUPsr?vVEpWNh~ znk6u`9}LkrW5-4E<$78 zCXoj1{6xM$bLSBm@2)Go4P7sM_Ex)2u{u065xqfB0s_n>3^AXhJAjKNttsp8bNd$( zeBFZrc)vNsBqk>Q5;1pc87(Ughl=aJdi7p4JFTrU&1$8i zzkb#dJ`?k*ZRmurlJqYW9~#H; zCUZkom0d9?wK`>NWEkvt=I_t8xQ#g{C!?d`ARqsG*D1~k_1^j}u-gZ57d~^Vp-DRM zmZ-1#=Xb|wuonL2w>qogZ$8I`$VJK{ZPM-Tt+Kc566eFxGcq6*5Cl7)tJI&JE^5D) zX2yH^cp#T)F##LQv$S{e=uNy>D`)43eU+A*KQXUbmF;ud*>oHr zTZ8$gyosLrED+!4ox}b{e3Q?eJ7Cd_OvP;axlp&-fZKmU)J)s@?<7#C2H<+i=CZwn)B>%jD5)qG>9*3vB63OKAV- zyXq7~@en)3ez>>3O(hgp1q`NY6Z1sQ9mfSkPj}>=W$(5@+y@`Q4WD;m*6D-ZC(F1i zEG$5uxB$25?EG!+ED?NvT|H;w{W1>G6=zg0^n)#U)^wSU(g=rtwAn)gkQ4AXy{oBT z1*xcuWXC9@lZsuP^|PGAU?VfvF0Q=#5q*mty&OqIL__)vFkYKzLvJrNDgtQlO9{Ez z#d|zz8}(v+o;{inlH0^0TA6XxHqg#({Ld@-Dz>RAyK5#l(V+|C-!?7B zT#hv}4ny5&TqYRzIBX3SSf2}YHLr7wa}Rq=Z}HxG&X@6`4ft`W5RI}|mE(0y>PqMJ zR+t##4@Zx6_?%lgX(^Dyh;Fy+73zR7`h~1Jm-TP@TqN>J=QdI=R;>K=39{wkQD9a> z-3QGSc(jtwz!^;tP8-{i+qaoq{vp?n|8G{o(bBCxiCE;4_3$@n!B5a1bJ~Q6m>kY8 zaHpv9;9p5(nUFt?W|^U-%6+>omSSmE5wLdHdh$c)NAfNiq8_(*Z|}B3u&BtOMB%o$ zv4AaW)?QHHsoy3oJLlx~8c2gd#4v=VJ%g(2KH_@N8>zeW@)MY3(ljS*JSSXB{2&wP zSrax&U#>LI4yi+Xl_V%Am~I9HnS8NOVTVdwm_8jYPVVlW)xI&%0vGZj;s%_Th_eTM z8jYd#viE+RFOJOQ-KKE7V-G8n~LiHtsi*1zI7(^gFKf6%?&L*=|uU( zVR$4iLqQ>-muafZ*(5h3&`fNVLsO;z&k{`;45{>tl-3u(Vstwptzw292;Y=L*f#E??m*?i;+sLc|^yfn&lhna#MKL?%b(A~i zYJasK46ct0*0{BWi${scvDv0RJ`uh+$m;`EFwzSCS_LR3`RGTQtt^&ZzqEW*Ih|cy zxf#7xRz;)pR;o~G**J=(S>;rc*5K&U@qauwrdC($-7#3Y>e0<@AFRwz#!QtQ5|ADG zvx0H1daSylygYro7VDZd1Zd#pY#^;9?mQtcJ_5Q<>*0)5bt`VvY+^4dS)w`l3hmJ$ zpojbWH)PB~!=5^Fc@ag&&plS|xzT8JR#r&P{$HdtMjC6lWpB&WXIy; zi;RFR$d?DTe$AG+v7o6=dzl|v!y9fj{BG>-p83Q0kJJ)x(nPrEWtOdxV$ZmS+H?}} zO9yb<)xK9QrY(==yJD6c@4{dCi|?|T)$B(_>CVjo7&}6_RCe=ANu3Nbw7&yso$G%o z`F;f6c1jw(WZB#m+xTRz$)r>!w8{KxpR$k7sEMQSW(nYch~!WNEG^r=FXI}f-6{9^ zk@=S1Hp=6(i8B-J@kPF4$uBFXf<)~bB!rdjT+=;V?SI$c`Dsrf&Y*~hXE2=c^YRuq zfg}GBjX}22@^zWuEM{8{TNm=PIvqPW^RrV?z-{}vN7^U3y$@|3;W1Szp1gQd19KW! z&X0f(PNhIj_wbX6&wky=VLFPzV@pum1WWP*otDYVuM0)qL?QOY5=ztaIOU#$ z+LDJlzg&{i(a?y37X>-62f+1egOW*ud*Cx4X(I@oK%S=Uhg4gJ(4S5^XOJJ~v;Tmr zL1+$t7GlOvKG@U!>NX?wW(RLW^pBI~RX1;W@wcw}o>g5@Q4cizDFJCG)`&XoqNlXS zFpeGppV@>w6oPQIa36>I)Df#m`yo*|4P*jceyWtQn;4%=?^#;Ty(!a5XE6fhFeDs3 zavKM@(Q2qRc7t?h5HQSWSd*+k>kWWZ-R?5g$@KLWbBCtiKrNEd)~V7&50UIQjHK$w zsHn{x8*Ebdk)29R^?wez*Y@q(TV$Ws?Ef8>CP7#Xilb4>0*P_CD0t&}U9;7JO1rPT zCoZ+SK0evouT+NHlZg|86i56|%|9uRqQ`mz4O6cb^PRYlq-2cR%5iJ)R+}!Sacj)K z;{R~VTX5U9_jB`s69UfmdhyAF8%*EIXJ@Iu=&p1!J^`8v+GksrB{UCWVRB99LK|FR zN-F@S>x{8v3@A)82yH1)lmwdh;CDu4O%MYaw5;OZX-1jGT9 z?Fg(og`m4(1qs_$@%El0#E>MohHtaa$kK0 zD$$2A@&#_853;!CYy3rbUwQV4jYZI;G4v@nD}&O^$gz9OGybz(K`Mu9A?!;bM0wzy z>5o3wOm}@-L}?hryIaAjx`Gi4kM*I92Q1wH^M>_IFxK$iW@hxDbh{{3H@nwcGr+MipiThY|x@x&vQ$c(Uw#3y-R(XuG<;u3Bk@$d)uD zm)3e=^hkl>Cd+Fh#r<3<|I_$rL&BH=J{B?W4R<@fI|q$1vM?-)27hNPCy=va<93zP z7G3t1^GZo0shWMt%*z)`&gYz5Lj7>*Jy_;1pZFm9n8@YVyUfm=vj#pfF^@(g7r+i_ z4`kM`5rmg^jIxNK?N7<@>3=A+M10`hu$23(+O*B7v}(!tC)KTb(uvmn5@H4MoFi6G ztW{!_I#lywB|HxABq)>GawZ*)?#Bh!{`vFD&tLZNXUvoqD1BLhmsKmJ#26`-4VB>z z@w^jBoideE?3Q;N3+k)=_S$tmMU89SqzH1dB)h^#G>D>9o64q^ETU!#8`}1Cjby}i zl#5;}SIRfaxER?f75wD-NyeKpA01u_OLVSKyRYk6(P66WOmA^8p5R@2TJnka^$REC zy1Q3rZhUa{Ry&T?@CHi$^zOKoDt-+HtqBc(7|#ZVhP0T6?XuDfjZh%Aqz8qnET#VU z?%lrqV~V6~P(L=m6dIp!&&$Y6{rpGOtn*vW$so>=!+a%zSu1IW=5*^g<+s|e;~L@7 z=44NK=yRmjUc8sf9_6n+UIQa4fEN0qd zcNt`DPk@UILb|P)pcekR0L7~4Nd0-r;-rNRC@VqG`f{-;YMqx4pAZtQVV}kAp7ZD1cBV zQd~c^|9TMfpJ3jkc*~^8eGVm|l5ZqGmF&sdeC^Mh2^Oq4^gxtlQmqC@G9ATHg0?=^ z<$F$ou1VGOKX{Lm$0?6hpW+(Yu1tChK0CB4BAt35u)oQdQf+x~W&hc&*dC{4Rc*SC zCjWSHpH@g4!^6sb4s+(B_jEUv$ys9Il#(<3kC^8h%2uDsEAnMuzi#+Js$vv__uQWn zxdG25oA~8&MEDN$9_Jc8JP!5EUe_Iqs0X@QZT=d&uT-WA%l-7-|AIEz#p;vDA=~(R z$+jtOPC%0!el#nk_vGG#hb)eWj0UUFRJ_6i+q{H|vP@J?%C0*(@sGfc+1@*ps;%AY zlwf^rEdd#0JUXB0|968ab6q$5h9Y)eA4foKKGPLE4uk0x!Wrwo2#VV6d-3AMgxWBe zQfGUYMbxJA>9}bR>ig+9oHbF~CajiZWJzbtkMcw4>&?*^6&0ceqKoj{&u2UEj~%|i#z=|J-z$a z=%^!b%|Vd5*7t4csXwpE21tPy64f?R|9GH!J!~6kz<=7(<9ppo8E&d9>}91mGzU`k zY+|=qwcIPwTg2fm)W4$WNacvDl6%sK5JoE9%^O4)MHggm+FT;CB@y3E`y+%(eK|sl zs6R%2IvH~>Z{*}kiZrCrog`k2x!Do-wkEW1I+*Gt`$;QS`&rYD*I!gdAYLus$#rei z&!ESh)Xc$yuN$uB`ZpYCoq6p4V!+9Vr~W{x|9vrbMP6>xVxGi3{QRZ_^dQdwLF(u2 zgd4;EFpd-KDcYEk`uv19W%Ied!M>k!qf6ew?->tyA-{mYF}PKaTUjNiE#x74M5fp# zWgBNI>n7CJIlEn>6Q^9O8C+HDqW_yb^7+joO0<=V2W}|Bz7#S1<#c%|xiT^I=@k1u zTS}=GmRbEQ8~Er<6IdIBTNqs=hOhFbP*rQn)lyYx0rsa zuVkk6bD5%5zpw2;uXy6bjUAC>lwmsS?@UiIWH1`(lgQ~`zkbPN@1dwdt8}YhH-s%| zQNfQ*PR*Bn{rc4cS7v!>sRmpDdo1g=;m%z6lBX9>+*FG_!HI-^ftARjGyzB8d6|`} z9fSE~>abP=pK$r$>=*p9;z_OcsKrR@NKoGNe-(SwYkz^Ud&V;{<)ZbcUxGR$=rbP3 zgxcC#rNJui_Yj39FA8O%e(3n=u~}z$6k1l1PU@j}Z+gNu-_Vm7^@IDyz(L)PnYBOZHw8 z;2m@C+k04b@U;;;=5k(EHv62{4rf@X&mF9>@1K|JM|2`%`aEGe89mKwBG7E&H${o> z!Tp6U{Yk3-Zgg*vwk2q3$BXD!HAlF@0ag#TESZal$pHJP+_`hd84R(_?tTamn*b)+ z>@H)tLq#Q(?r^knI~9FO#`npm9vzlgo?|aU9)%#iih}bE#&)>kZqjFyN=xzzL z9xTfT*x;mQ=wyDpOZ~b^RJq|Lf|RddK%Y`BOHb9Z(SGl#UzHXWRg}>7F?9cco=c$kfxQd3iFL|8#I zbPZP>IdQ-b^0qZXiPZy4p}~upwVZwduumXV9=7IKe+A}&fo<7}T&}h+`y4wy!G7@4 zwoRA7R<_w4hu>YGew=OeVO~Rfq5i#GTXnH~6&b3NU2I~9cYRHDZY4&8ESnabCUDlW z+pTc<$3Q|ffK$$~@51Q-M_vO9gxHVIwHtwX#@_@=U=#=GP(Q)DeATMo15GOrS4xZy zt5;TCykaAH+RjP(QH2nfpuo$Xj0enB-HzvO_2v%bcTtG$n(39uTJjzcPYNUuWZ&-PNr-sH zp`}R17M3hF-<)sfI6@)aslY-^9~&cUO1>k&Y&LAeSAmKt{j1xIF&ll1OS&_$?ZX(0 z=$j{6j$UGe#^t5QyIv(89^PeoUAyQD^35b4<3YxdjHKlqMfy{VLwK zIWA6chegh>3(qzU`No`o6y2(;VQ4uN^3-rG8(&IuPs0)Y9A_zMgF`EC3BAy|ylrNm zm|mo5@4))u$BxG<4u0^~^td&fXZgkHt?QEX(;bD%13#}^xp7?)c7`lf6-4N|z;>Hd z&in>zEQYu~6FPnc8*P~fK(=hMp~-jGGD;yyR$uOraCUx1>Rw&5zP6taT3n~0l+WIO zgRrfU05MxEjKME7JbgNH+)-h()^N0qimF01se@!a=gUERdwZM4hY~cu`PZ*yrhl)Q zWril{-JF~Kl6y)G1%FPw91IiAUK1AJa6MggRf@^Qf}Fd$X0GRZ&kRfH#yYao1~zac ztvNET5V^hWVUo#Dk@33^zA|Pro}K?!7xX6Az3=PShDj3+acSOW2S|{#nVHymtxKPh zfT}$%6E=>+tvbLLQs;gJ6^egVrjxHtwr;HHzj!=LMNxlMw3bNsQp1F)p>Y%83 z^Jp#X@-BUcE%ZmmEq~xiZqu`8V}!bUhwoAG z4A1(tHmIy$PJI5vK`l?!LaOd}Uda9_Tyu3Id;89BHPBFgvJ!Z`8KzK5J27)7G3%(7S zoZlzX7d6Iqwt#;1^p&c!iW@on!!%6PqkVTLeJa*)&G@2EwIxs)g zR7ezcI*n$grp=CPIqxF?QWceU#pGj(B~#FRHrS67onlD*cj28MBVyKIq6+OfB;21^ z9|y?C_U1{>1Ab?RoYYTZ^%&$&7F|&gKR??%`|SwnA%D>SPkv+&l0h*N3B%X6m~u1| zbp%$U>*84v))_qByR+nJ;TEA|Bc{DNNd>1HRc%(MDF)@W-rs7ccM^!H$ewSPZ$hwS zh)*&j?jIEzHEO84x5%Ga^osX9=;58a6m~=FD7}f&xMHR+$NSl@@6y20I~lK+U;i>^ z_qh}c^R~WaTf0W{+V#v|N9T@JRMY&TFv)G&<#xJry-IOnKMxz+G#_h#{BZPw4rzo# z!mcZ&?WOg(ApBlbpzEkZ6|Wqlvsj~&6g7A8cy64V(#u)Btz0YPylxG|U#<$^V2}SG z{ZRL$UA4z~+sK&nA6J)O$yuvgG5wVzr49|xH7+&|k+WVfPnr*`B?@(btIkzp3?u4L9ZKzpiD z-%vBQQgoI9GL_cPHO!Q})@h|6n!{H2()6on+ycVGjjpZCg8JEDuI<&&VouxPpTIQ? z;I+#oQGx$;1Wm8Y1SM>58H)N@)g9oJ&KkRvPoei< zGEe1n5sBb*$?(XjHJvJ@wSs_VSTfwhrfqtT*8bEJyH%9a4aqbSi4UK?nhuuB7N{Hg^Jd>}@@p}Bxt=2%C&$U_ z|1RO~{an25$(%woNs}%3LbiJCWimY#d6(TCFWT5sK~Zx%(i)P+uX;QGlz*sut}msi zKTB2f*0v88Ruu=08blT3I$r5b-&NYfyS;j^+MY$x#&-LoDmC@uiW3r~6q0vo-8F~Y zKkMs#fglH1AzOb*k~X)t;k~z%_)Y=c{-hVqlNYW%(ZS>CBT(%p+N2z6>@TXT{A^9x zYT>vQk8-R1HK&wU|5Vvm{;5AXynnT+uwWg>5N8E5EjSXqZUI?or^@ z60P3dq5y%&p1-`xq7T|9E!bDbuROkbqUAL&H$zdH5}{03=@kQtU&W?H?vn+cF4Llw zD6&!3wK*`+8X7(^fxz_xLl}M z6m>@Bbcz3c>7PO!@0}{cEf=c=RT^B4ZcBClE*B#$EuL-G^ZMhTF%*3wA%i?_=x2jJ ztLo9CKmIh4$r$MS3Kb%UdKT}Y0e)vxSyRbaV9TR^FZU(hu%g95RIx)ysEYCG-%cEU zwv=*FR(a7YcEQU6`J}qIgIxe@_ zPPniGmAZy?8hQri&tjm1X*C~MOEQsUgFu8G*)6jEwrU~&=yE6}7~amY_g@xh9Mp`TBnA?PPb5bO7?hr}87 zt*GLfNs>5pTGJ7I=Fpwf&%=G89WNWcyklo&S_;cv=1RU%eWUnVFQ%XZ*^^G?W&ND% z<6?%kE?wqWI>Y|C?8&1^E{XMBU<3gzU@mDyOEpA4{%$1ioJeFuNf=}<1@1)VGFSTg z9Ki}sv2qBYs6Zqz-kcd-QbO;zV(Fg6(9(i@yi@G6IW0CeR%x&`+s>|Hv(6>t_+VN- zMeq%Ez#I_3B4I02kf!)=>!_)z$!5d%mA76{Qj!+MGTMKI?##US5&zLGCNDqVn$5D) zP*wTfE5v1xOxeB0A9ImYMt+)4}+SURSkC#&|`2PP1}wSnaZ4?Ec-kAy~b4^k4$t zhkh6c2g;0S$=_2uc={VB1TGuCS~9(a_BppKq8;vg0%QRCgBad@YR|*S%A`V*&Eqq! zbWAq9qtCutB!ztt4p=9(cHP=!2vy&9zYRHf43y~Wx3Br)3HQ6jZPs+$lZTaMz-92~ z#n-oD3KXJouV-r7acN1}el2};O-ruL|Do8MByg+^Qzn&%)VJ=~kx~qvz{$Y%L}Gjb zZ}JhF*i+@DACre%175}b@MT${diUe~%!A`T_tLv%%mC<|%(Qs4bm>xY#10aKK{)7~ zGb$yIRscOHdE;p^3d8*z3X^mu_3hB2JC~PJ;IJVw(UGwArDDhP1iZzASB_{Sc|VJ76X|v8;tn!cEF2WWwepaZqCd;Pk8k0R zVS4$5P#U(?tIxQk(d;iWD)3NLG9e0wyWc;q{}jH1_S;%3WNlFry<%I?_fIz^4YWmc zIvH+Gq)9%VjohsJSIfW06aQOzu!m2-kbm8R7HP5OiLsxX6yr@dvMcUWkl!Ol{02iG zDngV>Xv-5=>E+^O>g47p)lr%Bh^M74kWPM>w(QuQO_hwvcLW~M0Ok=1JW#?1bK|%h zlaZ^}J*Z02J!Fmw`Fqd2ie2HbcZbM+wIuq-0^6aMU}ybv=ZBm1dhr$4HnTbZ?HW@- zHMTUxKYi3cYtjx6XTa13THQsI$VN=9$+Lz;xsi3gBbg|w9*S+>40D%cPhTI2@BLMG zg@KxT$55$|ym5WW+1|KN&v24|!myLDrS>u6PS8W{s{N$+%@Z8WfJxg`RI z{#R5j_RK^<_u3DC)T4K2TGRCQV7n>FG@F7uFb0T9vvXIxaWPy})>f|cOu5n`RU-BN z?c(|ha+781Lq9``^D$2Q+)mU-}WcS%YW8fnZ z#4R*fwRq;#3GGDqW31M0u=l#fFP{F>Z)!hc~eDi+U0n#{j~#8_6b(@nMFL$3I@}c8q9N zo0gN?8UEDB_rQQmbU2q!ZpHtAC0iy1FDifGsq>9JQKh1+y}-#o@?H$^)%u6=ae}hL zgye1kU7M8s*P{UivhnI4z#wr>nNigE>4z2ZhKBGP30Zev*azh-tY!$3YJ$wh$b|5A z^WB^0nSoR8g;_hU}BMz0Ont;KBrH-+&jAjk9Nf9k&N zs;yert4%j9KHyR2I?sFH+j|d7>3)O=Xn_+K&iT5=MFF0Dr<(#?*(e`Gds68{y)>bdMWqR7-~Wd3Lf9cS@56g{ zdT50i$2@(I)0%6ME!xPnO5EGayLr>$cvB-cdR5gs{X~8f{!}#bOk<({qbCIyk1NIA zhctSTz|&~6JLpfy6g5+d3cR3-z*A2o{%h8(aV1Gx4E~5QZ+?71qT^EiG38PSs9P@L z@gEzQSfm=xJah293-3`AI#QQYH6K?m)MOi%4YbFX`nwI5TsoZyLB)PAD-)f|1;i0U z=l6tXvyD>mEOs?Sv~f`KH)tlKuu8?TiPt8wCFC63SwaMyM8+x?y=tD`PtScI}!50@l)9PUZSRYl%<_zDV zI1&Z0NU!!3(jS~6C9lpRIv2vRSYnhr=deT!6@6px>;!xXbp&Tdj#TTMB0zdA5b&T+ z78dxWUj*t&9(h<|s(k2vELz~OQdFzN>))07KONP+`MB^7vm?LESEr&r?Y9RM*@i7@ z_rIqoZjBp?#89fFtn5fiO77q@vU7MX2gk|n!_yGaSbc;1Gx<0qNsyLW2Cu-n~HVE$i%CzdZg=tSY zpXd89i7}shbwa~h6UUTS={JPESlKYeH46No%f{HCuP`Yd4fA=eRyM`-PE~SYw*T{_C zhl1l7riy`_zKD8|AO4{Kh}sI71q~k=sZ}JO;_bV40l~o*6(2}&XaV`g8ahsf!BXlr z31~nXldR$$BWkN3uDZ0)<{WnGj)A-}WkF z+`9XmG8)Ri4kFA-@T;=kQPOpN$N6Zc>) z=0&KscfnSytKWs2Ec6i0R^HH%4LC7*SVo9M4SUuSVL^{4YR*J@)*r{Xe4l>(&Wt|b z>rQ(?anXhe(!0FsN2O!%!%0Q!Y9sI@BzWf*N`BTA-P2y~OK^S+!c6h#-RkBCb{MKX ziqFDU_tTUfe-{6?^hp^Dp**Y%<*+Rx{MuB1r6|?#xS}V0)jR*YBoTBElDhtjd~iL&uQ8@(q&b8)V=Tbr5mDSeFa(WE_K8^DleKLyK5Y_s2g&C~CpO`}eg0XwZOB zFMJj3+AINY5;Cc@3g^bt76fZ%AjAXOhO@4`l*6KHBJ@9(#YWLJBULOD_9S$3DG&ip z>{GIZ4~SyeG5Y%gERb#ViJ7%Y&-tAx{49TD_)UXP`<5*wIZL)TeaqO8Qj{SKj}l21 z-O9;KdAg?OVXv;iT`?IMLl||4zlg}!@R`)G^#$?67@8&AntI=nW*?Lb8(s{0hXI)v z;g4N-`L_hl$>TqiWnRjTD1ii(ja0)sh^N!=q^2MvYj1|=_U+zR830&lEb+?P)sceh z_9&D|CwX^gx>ubUo}YP+OE`J#XDah@@@573$AI7xKCJbH$t|~V+?f@n1=p}fO6mQ+L&S9EMwrSb3qY(^Mh>nTbG7HyR zJ(>`W&LgF}w}1VP?c!Bol3HYyW=<_LHE;GXz}Rg(po)p4G?4&KG(D93b1^cDyyBo& zG&8Jg{E&ysQDOB@|3_#r6Yq5&4|C4#ekeVLR#h37NH`J9y?v|5$uBxjtT+fdmgzwD z(2B`56w7Y3sV_~-)(}MlAGamQpd?KAgO?OrKhtr=#CP3qIQ;fcg8XYW>BR^^ zroDv|Uc~%=@n1$)^5Aet+)xCgSs--7w7f*8#E^kY%v|L_0WL+u{}1VOr2}8^k%t?P zBVz*#$0rPM9Xk;x+JJZh~r9*^Ns(CV33 zdaX2A&XM>VeQ$Y*WO&2u;?HiquT1}{)l38BUhL57Fxk3in@IdAvraOKp;o3~6+tdG zH4gW2bK|+7!onu=B>$g!Q5mBNoMG+_;^;P!fpAmqEhWyrP&Th1Sz$%ST-^PQ;}7m> zp5;pSHWUtmGBuT~e=}#s;2C7H4KJ;+EU^%(fPY4GXi^qY!5X*TTI*ky zk=b_eB{?O-TKCrLnYV*ETOHaWSFeIFS{OcP%CXwMZ98L55xnmka2i zR7ecK!4-bY!U0(Lat#zf{a=-Yl6n#;#4i`|yxS?^3BLjxNpZzYMOp0D3VQ8s=zrca zQnUL_&CbG?Zg;t^oTWF=60A05%*TPt@@d5YihVBou8IEpvUFlv7KlJ6I!(-Nu5}m9 zXGtK=EXE{@(uqYLM<9r{a<+nYWK5rvd|0@0|3t1QbXZG(i;~J&6Tq^HE1yV z4H8Wawspa+m|xKG?+bnY^y^~$rFQ(a^`6H2V%>|1x`K<;&z?v;c^GmwZ7;XIY3j?F zNLEtMn_<9ymSFA~(A3Bm@e2ypab0OYBNkPqx(k7pfwF%-(=W{A6y?sm6pn?rfnJax zNWrtvNnSZB>7rEl6sCxmkEW$%_sVC5c1<>s3bu)*q&^Mg^&sa!TD<~_K+K6LzM~2KjDlNIt$G9S%n(du*J4{ zbK1X9LY9r8wzfay8wv2fN)%tFmw(cnWgCiJm`@QvMD_wPzT$c)RZ@f7XQq?n>51J? z*`0V_(r3C%{n}YO#&%@I6PaVZEvZYNyQ6$BqTCX*oHG{841cHxyb+XU4JXaBf+;Hk zqLXc}eA#O$jt(F@+(8v)J`SjEX2pmbD4K$i)VAimmzS4PMb!h5VlX7u%a?M=IFVq! zrMw&qb^2PI(|Yl(b?W zH_ULCa9Z^8zC+(bD8D7yBpgEC5v%v2a(|TyY4fri+|4Afl?g!=={k`YlD&w>;flZ_ zTzf3bRdNg+Oe*iFFz2L??;$CL|BYLJ#NM$O^y1}hSN<_Scdo6lyOgK{R{YPznJQP8 zcd(a_m&nb%P}_Xq)P9esub1;)VnYNObS9Sna~@TN#Rks+l>FLeSI?X|GdUHKY12|C zn`|O@bQ>>kc_;$0hTgS5wK)H4(J0r^f1_;v)ef|AR(5qdB34_zR%E?gps-OgBpCvd zk}aJdTao;+aQ;2?XrdRfnHoUfld;lI7|3K?k)J8-C^Z+>9F~2TssA7OT zbGohYj*3t7UFeFn3NQjf^89l!suuZn#C>9kA=X`xxKvQIwzak4Io=N}2-}(re z7zqcFi4%(Rwv%Lo!Y8;|hT=w;JmfYItp%*-xLk5trKa$zZ%*U!h45#pt!qwK;E8i} zn4}bP0vnzgNk^V1jTu!lI{51%Heoc->&|c^=I7f49i&m(DvGf zkY|>hQ%u7jp)g2;Q}P>$5&Qcj%0I{C`@mYOMhXPmUM~rSun-K8miSosW?kb?O<*@@ zG#V5Ig25YIMm&7mQ%}s+RS_(x8qPEL*3#&GKBCeOd#_~OD5`)nMQ!{|8cFnrX{VgN zES|pNmlXRP(c{3{?+c&;QFOk>K|W$Fa=hHy2JvHW?ux@Nm4?VX;{CUX}YJ1ddyvt+yrZUGK7}_fvXV8<+|Ua=m$brEYMHx zogN`=8N8QUZ*4uj@+!Uw`C>WI(>NM=g1q9|lsG*F3*L{`JzrLsvvWhFc7d3}uU_x|0_ za~#j}=W`y%{XOoA>$=YKI?vDN{eHb)_1iqBhIjcJNcm>3_IDmt~@SJc)}Rm%Bg1YdIf;L3Zv4$4o~JnO6)f^H{bCWrI7b_ z=UPQaw#B=r!C}*Der;DnTnZ#wF2BzeC!OHo*T0l_gP$jP(~2~^%}jkxEGsEdGck$v z=)Jy2Na)ywS6O?0EbFYPRsJdgS}W#b4pZ&`~$WcnM)xrGg7A3Fe7272l zB9H&l&cJ$?p;mLnSpT6?tpxWcuk)ra)c>Bae3ru=ecx{&phs7DPt91dI4j ztyRwOJr_b|!Eo{p@v$I+CgSfAaWu|y*9=OupTu&9{Z?s-Dw*#R+blTY9>p(fLaajC z!U|NH-K`tkUn9;gkpBDq;{m+!8xXQEyS9-trJ|vO`C)soA(jG99XDC#%>enbdX~u7 z3_#=9u>B3_+~M;+y=a+UL^HU5HSv(gep-V)mKE3wjH>hRd5}$<9G~d1#JE-j`!>P# z6!U{-PWZ~W)At3W^?suuEUKX)xQMnd?B1C*az+dR? zhOG-d$L`>Ta++!vLzp7fWFgjiVwnO6>J&W8(sgHjGVn5QMBya&dj+i2QWnKbPC6sN zR-RzJ!@P^;-b7v=u|;p<*O&Kyo<*MZOjc2WL7=u|x<@%zx6aTc|zVxlCuF zZ!(hHTsw0w{7p4*$d@yN@8R$B>|@cZLb;u(F_dv z$}b9yXd>4#Jk8%?8Jc?1`=_-MN1$#Th9n(#-nYHrgdqlaPoJ@|23X21T&NLtyR|F0 z-h`6*rmp{@7!kp9-QSw+oh>^+@P}~!F9te2-QeIXR$TbRdyVK8DU|;Qn4!M`9FWgo z*K3|QVdeyga_(>rAfHNUMdTc+UdhwNUseEvXB~?3A0js11@WOB3`_Q&niGPf@TT!l zlxJAsOQ(70#G&Vl_ul%=;CbLiRGQoeqQgTOjlDr-o-N=-JfUX~Ref>^*Q{?-ZE$Dl z)wdlBHdJ5!sBS7(q_OIO*Am1zfMB zXs+_SkqhA^LMZj8L41BCqx$8g%*|E2KmW!y7v~LIVvEk>0+(WR>9jJ8Ty>C_L>6(2 zCMq8|aHF$|k_4cFcub;@8u>XocZL9u#PB#+yy3HxPq~xDSA4SPPHOR!>SEPKB8Ayj z#vpbau@b)}i2&)|{Zm%`eo*m|`JU$+~#U|1P!Qkt!jpwn5Q zt{j$e{d&8MCkP)_OVr^ayK!a#;&d-WXq}njqL|(zqvr1;cHHT9D^oEb&RY#L<7bA$ znl&-saoV*r&AR-ku@ac&r^L2x2I9sF3>c8DjygkVAbpviN-Z!&%{LzBVG`*)> z4(*o{{16vlq1u=J(5_F)D8&!Cq*x>`9U^Tfup(ZsYmXM57_(nkuTb8`Ho92Pp5ANeIH`j_d_+@ zR;SJ{rm1=S^Q}gJ@_PDUw8#)W3xTO{vBW+)%)c+yZ?V#%MJn<@#d^t0guSWsR_+Yo z7iI%#=IFVkW=>-b=}IzucVY<(I7vj(2`A1tEh9d?yO9#Ve(FC?L4iA(cBhN$`~yQq z4WHcu8vSk`%}b7ZaGd9*Q^D2sav?v3HFQoqUAm&>17QqcwIQ-_6e?yz{R=n2S84Jk z@bHN1{%mjDDlF4-&s}t#9PgWU%zhf8JIy8bXsVUm))|fK7r5&seS00}8dj5Q3NTfx z24jI0lAh21exEMnrPi#)@<^r#>D2JCBM+0*JH<59&x>6CPH-Mw&#=2e9Om@=Pb#6wJ7G^hv&P#O}0^ApTF{4G|1VbzE~BL{b)PK zuT1qs)wsV7>!TUlZ}5ut%Cxw+pG$A!7$x9}CZMT?AZ6U}-z{KOi|zj_%_ zAI%+W@yslG(`(<|9w7GxSOhDWH4FP6l?GW+zQGSCmW&+%$q~&YBGexspJd+ifV_(9 zjcA7I>zTTOIhddj-W$YzcCc9I-?OLbKR(IcA(qAWcY3{#f#K^xQVEi@F!Y9&z-eD& z+Y`g=AK^{gHgDbsb#1(LHow9i1Z)@N=WoT1kT+H!bP4v^Nf%}-gnm>PyM%G(ItXfl zr9*Ew8uW(k+;~9}>ru@){d+k)@U7XCC%BY!mFmJ z=%EJ+-NC7$IJwVcMuGw9W$QBXcwb%;hI?oLlaC+7le>ZekvqXiIR4eGf~yZ#zAf#_ z@;@{%2^fYAvKh*xjz!$i*8UPE=nfwhqwM5ZTe=4irda7EB5visc6Ux;qy=UD&cXQYhmc9#+XtD3eYVmHj1n z2x-1HxRAm(n|b!_Y^gb#`SS>B`};Y`^)cMBlqfa3be>ltgvnw<%Hrw5o>XX+(}RmZ zps{2&z>7z7lk_jjsh8%O#*rt}QE3LbPNJN&JlF0+)KQd*)^CrrBM@=Fd4Z0 zfCFR=;UDz7&15OfnNV(5+>hbPm4|L}@_IcDFUw4*rPx=&(o8ERCT3&?ACqQP=duhC zZgu?q{rnOJmSsQ$#}e$;hZgP+2$jTgG09%<=+Rrf=fC&*3OYJE+GI7;tr+aC zX>xz#EIKE%n#ZnIe3_Ew{AtWc!BGG4(wn8N<8xu?x3HBGJ0Qha@Uo+fOAv5vle5Qtf)kpc7U4*xEM;=(adV9S>MEx9xX9%o}I$9L*c z|7QKiL3kEuTXOP6Un>-ZcGGL~qBv}8@6*ym`q9#9f(Mp~ve{C#ID$QLEsIEbq?HMu zRB|@^*X}#s5RQGuL-h$rQaHSQ;Q0c@K&-R9rH1yhMDg~r(qc`cdtrI|j}>Z|?Sy(} z!+mFK^`Mdj2{x*X7@zS~`-p+@+}jhE+~&GpgRheGmwT><$Mt(z3D^RbzQ*vp!Dcz{ z-2}i8f01JL9C*%{!M3ZVe6667rf*?fjwi35PRxq*9dPwsmgzI4A@h@1RWD*uzug9u zf?xka>F4LjhWR)PfM4BYa7~eTi=eavsUt(FOhxOq)Z4?yj@5#?Gz80`{i3FoX)i|; z5lF&YbLexeS#amcSc@Vw=oX_70jkt7dByrgIJ!^O)Q-8B%1*7D;IsYJUw3eWscc)A z2y^U%cd*941lE#(tZa**8wo8M`Ro0-4WE#T`iA>TIvJq)1aGz!S4Btp*e-TPbw9CL zw$v(|1(e@Cdt#rSCw6@JrGaHr$2r~~;5GF)-=C_c7Ex&~Giapj@t+DBA2RKs{0k85 zTC^gf2`mXS3gU+w|HucF9FkDJdtHQpW4$W2fTe52YIO~dtd$$cXI?LA;s>=_*)FoL zF53Dvu`>_DfRTM&u$Fv^zpUJOZ2!z;mt5J5zVOydh3ib~ z!hiN4{!ynI|v)P162hLQ#mJ3b;f2eKO;Gm zAZaXOW+7TA5()t#k7`oRh)!gRHW7t0t76{pQ$v($iu}b{$bsp?@8v zL4KVa{VQ%p2WGyxkaCTXys_ie*0BMS->bP;E>@@#m?Ixax*fT+Xua<_AB$g^ARCDd+r=Ni=4wHtxYTI*s(#}R zR&+-vF|ACWnIUvZpz-Gsd`uQXvb1)e@5h36IzeK})4g!Joec>KoB>)_Jo3*D{^r!A z?A9Srn$IKSWX`^RVkb6eV+v9tEGhknUJ~GR1Do~Ua!*ory8!nf#x#lL<;nm68+-?O zcM>~vA(w+R=_bV{6lsF2E^7qU3KnDwbpkbi-fDtiyf^gMfO(81r@I8y;dSL{Fb)rA z#ZXL@m%zv5_$mr?W%2z7Xp-uV-@t@TT)o2R$wI;V^#}wgt8FrM306a5vayGg;ZAze z0xCe2i>l@~iEt(<5wip{X67*cxfiE$*XP3B7iyviL+>7}J zN`RmL1J<%n;AVHOoPUk#??}it|9WZnOQa#9w>=8?31W;(Jb618u1oc1e3YkWQanhM zBvE$bd}~N2#Ku;nyKvg~w|gkcixCzRA_ovoLB>)xl4f}e6{T4N8jqO{o6I!VsVjsI zjZp?!a1<34Ll-JXd3k(Ehvp~EgsTJqs~aAaf43^S`)t zkKlH*t_9#~@gfCo>KqdE0NxqV$o(DdPXd>IS#QMumAE2~eUx_olEg3fP^8h}-P8%V zzm}xMW@cb@kH=F%r^U;a=JNA25S0BkQ&yQ!D(Tyv0E0C_HZ@DY#Tg9Ih0BVtbj4~+ zvM?Zad5ov(9HOSpk=g7aC2umv2>5mf2I~p=Lz0hI9f}QJz{hq8Q+qN5PhC4}Xvn&8 zVI{5A|;vGZx1 zI1vx}qxtWzZ|P`h2Zl|?Bma+H+vFE_?V$F&zw=e&^E(e8r|0?d&lso3lo$L3Wq2{? z4EnVvvxHG=%wmRmSChWdxy=CN0`o815*J{_ap`IdW{iYpBd~QV6j85n zKlXR4n2VI>-jU7qXR2v`2}^q81euK%yXKV+Eah(x%)d0j1;pOovF6?|iaOimL`D#6Q8xDx`&XGD_dTWJM>yK6!@+!tj~#YalX|g-qKe-f zQ#Y;Ox9v{F$WFG=P2ZMCr#_62KLlsA7bg|5_I?7I`YGY0D0wwwkDFI>tL8tE{7#;& zpYlU{=xg@Ii{nH^Wvj!=xtNQ0=3E2!d5@!y4c5 z4Tb#!Ofu@+1Nqt5))e;Xic9c5ET^Wfm}XkDe_UeWcNJoIi*&!Prd{zw)1a7f#c2b1Vh-veGBtSZ&<`fw75>n9v31?40H1h{y_?gY*{GIvPP-;I%b!MZr|vqFF~O_0GfgO)Y+W40t+981M$R)hSMSJSX%#$> zDLrUO^`S>-?kv!KD5}QOie9DM9E+vUU*d_Yd4ekc5XEIA47S|5x;@=0f24W06;nHu zdQOh2sTdS%{?a=4b|5^9r^m;~(S1iB z@LL;Gttz7P)X-x$WKp0(wzaQdKYrO_Y#>*W&uGOiem`4{=HFpSOU^4iqYArz`@+~6L;CT zRkHVBsDaugrQjWGY)k@?u7=061hJA>9{U4vUt>H!lxZvcX&;Qv9I#N;4uy*&7YunQ-iJl8~$y3lWPHFi{|lA z{4Jo)+lNl-Gky!EDv$WpW4DTl-K8il3GJcWysY$Yp_Dm_*zfF(Z;Am)&3pcVPV#2~ zFNPI04c*&qA|fK>e#Gssg26Hv{6nu6e*cXxd&}fm8d*}_q2C)q<=zc&=|vf6919M) zsCSKx=wdz?!6Fa!iujRuAc-*K+mSkJWr=fk55VsEkQgu$)()tvkua#=4O`>y7Xw4* zrp)-EBnB3bB2a(Nx4rYEqo@Cjgo4jd+M6tp#i%$3yFdA3VDO6|Ab>l;?XsAtMvCvN zhGZfeYA1@~I*>4Q$*^6d62m=+L^QEqVGQ%>m6bm6?L9qqVKD{N<0_Pg#IKQXpk`4x zcQ!K9AYCfifl#5wgOWoiWFXp1n-{sEX&}y$9O1ZB`jg+-M)r`(Q{LcDw{i@c&u$p9 z5sxS`Kf|i^purlx9Z0!<9~>GQ4T4cw_PwtA{0RY61gp#@wQw)njmM#m@LID!~tF> z5Jc2qGqF4(jag31GS|}3T7vdn8R>b#M{)ibr5!dH4<%DKvU3N1j*@Sei{_dlG!iKe zX<^JEFVELDa2>T%<|+=}jv-ehJ=eNx%jiFXS7&MRfppa&1W7!$XW%-5wU;@aH44q@ zga>BgECEC^?2c*tDf?<5P*2H%gjPg6(A0wJr~66_T3v9|2$KM(;@94t~f| z-26WfTp^^7QWS8;ZYS*~a=tXJ`@a%9XxrC-m`FMW!4x}Y+vKquPoh;ZTuCnvt`pz# zs_c7bXM^$#u{L}Cq-;{fO36lhPXOup7$hfmtD`&^sREoQcJb>Qfo|pW>W`0)l z(67fZ(I?S%5H#0G)2q345;92Dz4jF=&i~1~CtKtUN(6`|<3kV<18nK1@Mc_nJzhhZ ze)R3zSv}alzLX6>qx$)%w*r}q9nUbo>)xMs7Ibb>My2 zwY#)h_fs}Pzx~syPHxt0za-+X^oUwjRUE(Nn(gJSO}wG%8#?!N<%hSfteL23YO*ic z5<=#l_>{ZrGjGea>^dml_<#C{bB00pCwulG$=NiB{?Y^VqDqG4ww8sOaYSsO#-T1= zv1f6Z)DtJT81|;pdW5;EbcU4eSv&>rs_JUhPF$o@|M|VecXf%L2z9Rn%>)$Ap%o(Q zCRqtzlT@*Gz$tzLp>>L^e0bFU!uJ4)3P$(ge`C%53zQ7ym6JB^SXElesR2YWGP z8$o!)GGcg)Nrm!xY;0_IgwRma5$<$b9ndu^GS7nA?==D`CED|7ydkTt^=ze?#ceY25 zAOD0e=hMNWupU=t29cA5z3~i2ttAMYQoTR8Rb401k6+tV>43H_S@yXuO0rN9sysrx z>RRZv3nL~i8q%H|KYWc4X^O+1xak!d(Gwx9cm@%+IJq=Vg-LNYA&HBgIxI+SZrye( z1D_iZ)Juo{)8X8ou$IB2ZRG>ZQ;s35DD`D+SvI~9w9#gFKVHPUQZ2sLYD!1)R!b`@ zzhKYp>Bgga4s*hh8L+7y%7hUU29k%{nI95%-i$Q+BF5P=adAU=n5bpJb&B9_z^_dZ~ga~Yat9@G4auEu~W5!t%; z@?7EG)AODr-Od*Ut;$Kc7?J-X>%4TR49jX){=KOV z(-ibv4fkrYkR@lqu!En!WNuIueNXwPL_3L}a?$vNE>}q*hGb6?QsDx!nciQ0%=Hg5 z!8XfDv%?S#{4EWV9v%N~WW<1`l%sx2be+&_eq^*w7;TWJ@#2fI_L-T!q==EPIo>!J zCjznU2e|A|4m8d-^3w&t49%Ql*=pcCYgwOz-AoUT*(oZf3$=*+*RQ2+fud8_PP+_i zhAW7jc}E>?y-=Z1EnckdVMbkNY{>{t@WG(QMCR!!}; zi=|TG9KPfpThVJzG6|Q4?JsR~Dkp~CnS64iAEuU~jZp({CcyauUg+}bFhzy@$>eDg z(?RrlGp+DBfDG_EaZtsvOV!%dJ4L+Hy!!#D4| z1~d2S+N9Y&*>AqWE+=~LHCb{Avwfpg1L4;Bfoy_EC@Q??7YPnZJKSQ!a=tvnabWs4v77(8!H90@Rx~KMZ4`2@ z3=`jTSjKRBU0chA7o?(QGEFWU8JD`0y2C8uAYkhxv1 zg6dYPj?PYV6qPX7pa75~X%n*#JJ}sf>>)W-0 zPffO7ikY!Fl6d@8)6rKW&X-gzZ=?ZYxg04wS|a&wFU$IM4PX1oE#~ig%toOd+GSNP z0;HEGaL<^;^w|{*_|pEO;3g;};T_+bjU%I?PM_18_l?!(97#9QwObU!;PuH)$*FgG z>t2Do=t3-)?0Y%g#Ml0hzUNIIBa8VrxPSK8!n=;Rq@mscpf>-=Q~mY;!I}71faLRt zVH7S9e7CF*#kJO{tMT7%$ob~3cQ097z4EMfubhL5_UZ*P5P5E={Tnae-x}1J2cGwA zE;&PRL`1@DYnw_=o$9p7l_f+Metnho-x2*&FCB&z8s!lE`2w@w8-cVm&nIdeI_ec- zA-DnAqbf3VqgAS}^3IYSuX@Ek{V8X$<7j;gEzM^Nfk39OMyoPctRfR{;xB$pA=YcQ zE#kTy7Okw5v~PIgqy&BT;}E445ulH}oHao*qS4&W)-@}gHG3}SFsJT$QNMqGkx#IC zAa=a_+jjH&kT1%e7ce5TEmE!tlZ`03V6H%fCFq&vM$J@j1R7o*@no%3dv{+#`^51( zN~x)uSH>f?3yDgrHYTJA?h;mO+o4k&MoGFeHL)$pH$eNc`BUAcO9fi8 zw9!h946+%UmPH0e-~95tLqOp8KwtA+*-A!^=QqVIzYFrKeW;AQ`^9tLzO5-PPMLaw ztgz*D)H@N%gJ_!>p{CcLgD=Ft5ZR{|Da|?4;?NVAm1aYUDn1&?^6g@sqct0w_UYP~ zYl`pQ_l_FmhWPL_ybRhTa=c(-q{Oc0M28)0p8Fqvf3rwx@K0$-2>Z6rv9-6h&IGa~ z?#T@3_-S_FYOk0PU6zz0bBE2oZJ)y8(oQo9G-v5~9(-p$WlLwrR&4h@`km4d*87n= zLbmBYe$4-JT1xZp$^3lk-l`HBtItX?$;kpzmkw1%M3i>xq-&TddMKr)+>#UMY2`k5 z&VPD>rAIE#XRsG8=U?!DvaOp|1XI)t##gBvvl?oRGnkRmjOc%Hp?Pv7gKeu^T*|m$ zWtsYz+eF?(yS0$2>(P!uy%X`jZ*feGWOV(c1dZQ^CLo@+1dKtxk&%(+-g~*gmmoJx>$tNGbdD z>y~tIp8a$CsXrlizid$njZTNhuSb7g?MQuUS%~QoA5L*)4y1qfEcMu-9;fv;BXoM| z^SM6VyBnpB8%Iio_5b~HYGmJZzTFvM1Vzg^>t?JXh4=Q&jM$4Q@@)F-RIz?yrWgI5 z(rS*UL&-U^QyScAY7KTZDw=|_m)vGtoUR%p)8R+m-{HGo>>S(a_uKt^Fa^qgc7W{D zG(ne2_JSza*Q+MSt4fi4`tfzT+)SkG43Ex^R5pR}I`fwaFD~eRyVP-DwMME(0P9Tu z>a@XD^Xe-8tSC9YO?7TZdns3xXy1Ev|CXD1QVvmOT4F`NYn%zVvxA?t?S5o-2~@n2 zaHDukbp`8HQG(u9J?WE+vk;n*T{?jm@n#=g+|d-1bkfuRK86KlqSHMUgT86c2E52$;)=3VH&{ zxTlx8^!@Q6p@jll7LCzXl7ZGmc;o01F85kp{4_Ob*8K7Dui@6LGdW@W!O)Z;EoeWr zb!_0>YF%kz|QAp+$U0u2yEK=-SpN*nWtnFix`!3D?%0w zp2Y`ew@UIGD(ni}a?ESi#De-vYG7i9ra#R1@4vI*-@ift1hEmRQ2{H#QTiEiou02~ z$g3>MM{GNd{nDjN$-X&^46<`rbM4ptGoz&Xdd`S$yc$)y#Y{95VbDw;ghyy@)54gE zis22;D_*w=#di$RKHEsLuSeS3-;v&KUzLU0kxIcMI&d{wVxr{7P4|(E1|TBPw^)Fm zsAzboJ+t8%SzjKFHoO~gr$mbR6JgQwNA|n6NTF*e&JLh@mjjW}fR99wQ(s^B0e9q5 zekz8Ah2@hB%s|QX1zR{S^o1Vy>dqgkvAO+YKkYe|7(Lg_Cy{ev0ZdCKQKO zampq@8u0qJSG9NvY-)%y?>uSl4<8=LkNf9>{od-KNUy4)VMIX^>@I^hRK)1@;7*jeYG4_G zYi@ai1<%R5|9Vbx-7er$d~E&po6}e>2%y~Msq?lykee+eP0t3LLvFUW9)d6_Ib10! zEH3F)vrG`u4ZAyp<`q;)wy%tBTwN8QymYonn09n!`DUNh?4P@TkpNP$(KKm*ypJ35 ziNRT(z)mA8$R`MQJxL<2#BeU^Z~g(Ek6WB>otdJLxMXm^9LKgrKZ)WT$m-OCiS<9R z>)>_~-(^shEac6`ez(1pmE%#5UsS&N7-Yt8%O`;;C?VdFk;FSz%|aYSvZc;{QxPo3 z$naze90apOMG^8&$m{xI_>{+}qFpckkghTbNwP45*LS6b;Qv*iH zV?6f1;2f~Zr#`X5`Tn?Y8L!IyN60%QByN&VO9E#RVyrbD(P^tOdine9gDgMHv)oQ2 z9Po3cS^(~SKjWD_!_HsQs=|tZW5pD(DE)OqMPXlM}gYEm8F2;S_9|4;fI`4 zhln>81`AJ!a(+7bwMD(i6bLWrwwkvy8Qh?>)zGnrUC6fT&-OFBKt>yeY9!~QNNQfx z&8fjpgX3iJJDHA>e%5;l{s(vMLiFszKz-LgR}W8J)*sV|*!=-JxPY7&=R8_L5h3Pn zc3?pLK;!=M?dfanab2B3_|NFE`TuL$+XG7jEQqfm3;qmB-9*+D>Vk$1Cd9x^&$dF$ zp1r8*YyP-5V(|L&S|a;JvgMqtavf(zR#paN`wsHWg!hZE=jBMWvgdV|_(yA>IPs$; z&n+*~3reLXOM71_+fI$J5E4BLzF=<)ap_GVQv6$PP_37SVAfT{v2kNxZrhCwr6B-7 zMnP?A#-e8za_`>jYc8^QYMV%S;Zx@x_?=r7g*!ek-u_II^7c{sL>H1_e%|u;m+9y- zoJnYQ)*?(YZ)pg3z|!gaV~o^`SCn=HJ=!kcH0WgEO4$ZDEl65Ki_k7TF)VZ(~+&~*tA6JvLdVg zN(VbhSIYX1c7unt*aqVwXl+M3;qKqVA#O}Y*YKlKA-u0~kswwLj5NWM%ykYcxsnO- zXHRn!?#jmUc;fZqfF9u8DHhGN%Cb5_c?3lPoz3@Wv%RQ9ezK4+rfJJyW5~1+B5rN`;xPyhg36+9r#$B;Ec5;Bm(@pLny~p|@Hcrv$zBL=y;I0^Q{)2?^Ub zc#Ao-g(gFmr+MXDuuBity3pbEd*IF^9xn{qroad58s!~u7^rN8RW_TGwJd^)O$^$` zrtL_ubi;#KMgYON&xq|l@p*Z^fn$yR`QB6oUHFu)9n-fR; zwbHdR-VQMHy`OI5ygXyMCtGe}O@C`tPN0u;Qr*pf663O z|0#1=xqHfdz;Mh{Vy8!NM0%smTefKGn67`OIRs0F6C}19!_4Od|Rip%BxL)HIWL zDCO(Kk4Z?9rTf|Yfz&gDB?GAgBltGs>}ymc>Nl(^R}qS3Pv%cm)A9*!=R101>isBA zOBbiL3a2%HTXGIo3^Znud$Ygcp)acC5sWx8w$NgC5$XL0$1uAFbV z%%>dsX8Pn3_0kW)<)T-0)L*~&W__zY&a^9a?gRVas+*7vGqPkofv-r&_brMfe~Uhz z4*k})*$;U^a(+pV=*Q1HndVBl)@~&C%}>^k5c5CR%StbOO_=&{d=_%EoVHs3)Uqif z`C|rR?t_zxW;*kS&aMODU`N>TFQc*I%KdBJPwJ}Xph0NNe*KL-+3kxzZKt4qt}eO) z>zBsd^>P8G3iq=bDwZnShGg+aYBLrctEx0zH+zYhttTzUoNXp8P8{B8`=@B`F5i$n z^LI^um4Co!|Bd#!C%S4}7I#7+#p+h8-S=ezV>mgLD*;ZB@@sZ>fT3@9@ZiBLGL6mc zfnXqQ)^z2b5ShkEgWm3fn2KN_+f3a)pz5z1)6TUE?$|+pUrMgt*%c0#edvKf%*$y8 zHBHR*7IE)6c`rP?yAQ5bS!e?KFgAC1QDv&2a5XZBnYdKgPa^q6GuMyo*e5?AQu$ob z_EYHzUnaW1S#QF0)pisn(i}g^2P_~v?R)WJ=O`>d&Do2{-BmJBV@v)sI6*D~p98|u z1_A%PZW7v4vWvVB)OF_DUt zV_pZpyc~aLlEeTvg{&deE_Kt2sE158k{kdcpH7#!IxsJuhE6*Hex$n7nRRmKM<-4f zPFCLk)C71zVyhL!fX{y_7m~Se(9nrbtlZun@Q+|w3yiET({7b>VsDR@M{*@Os+f{K^d!c9-Qt* zsXSGWh4;q44`1Hgb}IOOAgfQYT+WVu^FM89^toAskA()LdJOd3g+EY3L$>h$sTABH zXP!Ug89>lo_yYs=|I4qunX&3W8f_`iDtRmKhF=Vi`gH78Pmy4y8(a6r`W+5p&`}HK z7O6616xp?Q+kVew`$bllu-s+x6k+icF_N7bcByUX%B#wqh;Zt&n@sASmObC>=`teK z)jYv|KBvkhYSdAkY70=`e~;s{f8KnW5HB)OHQU7rs-bg`DDYtw?W&>#lysBm^G8Z| zOQ+X8ho&QxXc0`{uOPhc+D#nA9pBLq!~&$SuZ%!7pq}UI67+Eht3u(r5RtP_nYJi} zBD}X?SCrt17X}_tgLu{&*G{6q7vA7?^XA^e+>E!6ump9OxibCnb)qfEIbZi8rUZPO z!y$r^C1Ij*Pfoo`9EZ*$sN(rwbW`P^yOgg03VW^Wkqx?xQ2brXvSUzdDW_PI4|G%4 z2M?bGXkur8(O4j2mlXe*H@lWEULet z9PI{CFlYjJr7rr^JQ#lqxZ~Q)X`qSyGMj0O+F|?WZN3s$VHflv;RsRBIjn@;V%L5j z7Gca94}Y}A_29QWzFL0YzDY)^3~-&Ak=Dg#eXI3Sjmm3J?`o1*tIE1V=Tn+x;}@2# zs&6JS=raK-5R3M_6D9@XkZoQ}p%Hwt+X70mbO?kGavw-c?OXsT(IIMVV&GHp8O98c#u zk2?p^V6_)3S(N--q6MY{S$e`XXrNzIl?8HN^H&hih-a{I28cy;t-R3osRdlkZ{E9F z#t4!ylRN9J&GBBnrcpo$A8ovKQ};kH3ln4JJ{gpi+%waV{aG9@c53Q|eofpU@OKOF z=5U4wN&#Q5Y6{*0r8RWviZyliW^6u2^u5bZcz)k+r;nvYXiNs0$FmSenl6cXbYQ6= zDs9Vxs5^>0TYV@JI@F@$KIoH;a7pWfL;q zoSEIux%*Ol3FII7-6)3dsRYBrtniV_M4rAKQ}^8+r@l<$E>z2z!Oc+)Ag%%>y^-Kj zmTj`2wv05e=zAa9{=_BX<{^;-zEpoXtmNd$oK-_5DpDaJFen(f0& zLWLh%T7ON;c(`N34DZ#q-y;scY6Pp=;-Y%S#WPN;ESp0oQ#^{2*oLu9^o&z$Q_Cl4 zaPzOBfOi{qJ~6W}F~yqiI=h(pCz^CNNteHAKY=lD2=%|6iqRn@EO%A?%kv=|HCS*f zfJ^jFb>>>ZWlZw)^w9#&(J%pVrrqqLEz4J;js@ga*wdUP(PM}Hxuzu*+ytRV`xjH* z!zYNL%empTHN8fJRD8sy`0uaHCw>;o&qQuUqbJJY?aJgk^2OgR)getxQ% z`g{XO?=1FB?@7ayvV&}D_`-dhv_SkA_P1Wf5 zj*Iuct$S7^?PZ*(2c@HQQAakeUA=PXPVUNXDdCm2bv7NP^_hYLG?D8cC?snmn6*dp zGG&8R_J=}h-2myMT&`nivf3?|Ort<=*jG(2(eyRzmzRAlNNKN}Dpe)duJC*1JpP9l zq&e{$n?_;+D}AkNv@B8mh_C9nf?MZ8(YJ!BZ0yjzQ=7#JaWC`4kNfiFhZrAUle~5Q z(4o=giM}4PXU{LEY^aqnt&EWD3cB}8`jx;u~Mb+w_%csUxzD(~Ft9h4(1 z*{XNe_N)`*?Gwk3AKJG$%whfl!&?(MSE*@a-5F^$O7K0jr;Bmumz#$UTyiWDB)@RoQ>r%+BLEUo8?4!pbi zHK3zkZe}v|xsBl#f2ZwvlYe|w`usSgzwmN|ZV!54;~^js)(vZiS0AzM_yDDf$%adX$kdTEU#6N;TEwt_s~r?@7^D=@(J%In6ildZEkzJEYq{(2j1^Q zeTteqorx=OFV~x2TfgyrlV);kW7^E-livbQDdW*4HVmuP-V{nc+`n=!Pm!MFA>~2` zKHc&I?|erKN%V0J7w@J69a$wW!Kak{?wFsmIb|$rK(A|NQGY5#zLNb4&X4Pq_MH{v z;^8S#1dd$nbDjN}Zf1&%xb?Su$u5edHBV(S8t!Ex5U5Hz{&_StTAymV#FLJd4qFS? zMPMp{wpA24&PgXAiA%ynP1D`(z4oqPN{Uqwqy84q zp`?>4%qhLQO}luaA8Sdj;Akf%5ZqnT;3y}YjHvB8fwFc9Hjot?8-dM&m=w&czdctQylRHd1z;Q7j&~GW=^P-J0!dCoPB=u8J%fzay|!FZW^LXZlkQ9#_dU zK12Q0v7W|PRZ-GV4qUE+wx02Ja6uT`D>Q{hxsgKpx$;jno-77om_E#wc`t*nBxoXx zhaz{|8DOX6PCBwDzdKRqrNCJWY{CBWfGu1M*E(dsbp{!0=qjW;`?I5ng0>6D&Cc(a zp=<0NhGpBP&Gy?)XJ6)$tz>sWdv>OZm=ehlJv(<&m2iK+#I$F7j&|HJ%BtY4YCNiF zIRe|WPs4XsG4VXUjgc`BudWsSs!if7hBEZfj^-uH7 zoC=j#wMyO7jnlZg@gXCozi3D}CSQtT-IO30!y3_gBQnkSqQGPM4$c!NgW>TkS#;9! zn}mr#<=R1Tf6vY2Ai77gAHLbgJSW4uA1eBP-q|J@us=(UZ%0#v%++{hqhWfdr5`p{ zSt)DWP^14*Bz-!r6djfeckhpQ$@bH~b#7mLt}&3QZGKgYxk=waY(smjPT%c=8);qB z2fL~y9RGYz*1Ww+harWoxT4m zoBAnNB+5o5Fovh5Qcg%08A*ZS-o+P3C0-S$-=RkG2o5xS!v-}!4H zHDT8a=Ht9Ssl_X=f2Q>?Yg=}&xX5cWy%Wm^Eq9e;Q!UJW-f|JN!bke1ZZTiVU}j5u zVR%h#F>|4AylPCK@WCrWrtf-ORfPIHrR#hOAr-NBu~*;huZ5_f4*!^c$+{%YydoN= zL*`cefWTHc^qP5qYPNOKQDE-c$>}ho%CfZKu2krXpf?8n z%C59kj+N8TS{xG`_8hff^vHOO`|a<+Sz|kNTrJBE zE2hZZ$@+0C&$jAjToc7^SFOr~Om1r0)wsu86F>g;mVMtC@NFBTH!QdJ?Ykr8pc!(_ z<#fD9BWR$#D=JQlXmEc>THEL1{Gi||*BwQ4JM_|Lmfp3YmFkPSq~1JB$1pLeT3yiIWBn`psAB8HOu^x;!lSE- z9Jk-Ue$2T}J=o>;AsT;9xt2uNZ>Jd_j_J)jzLb=`R`gjaEaJ_W1zRqx{gAKS#&ngX z8S~NdLOJKWIxDgJp%url+VwttfDu}8i_=;8!ZLwPLIv79Ho{~5C-|^aHK0!E{pi57 z4fPG(#yHK0@YJ3epvbA3nzvQM_IX@o-EOhF)F`}Sr=e#_Ux>czIbucu{y4{R`n5+i zjBc+DL%Kt=>gUEQ6~1d0d9GkfAN{BoS#aPfndxkkq!pCa))L87 zYdgpkP~~^eT0<$tF=@kw{y+81Z!(iGb+OFtKS))1jJQsl#E}46`i#1@brT#bURfuS zqT|q6#yr)r1Mm1gmeO{9PMi1^(${x)N8gA|@*C$_J^7)s9P^@cwH58OnlXw?!^;f{ zj$q>F9JCXb#IM}GJW(&w-Re_wh~~qeisp^Z*vh=G$edm0(>D1nny>ovRURq2-}l8W*E=_jCp^r#u-od>^Ha6cfsZ!FNFo2b2Bhn+ zzc%gMNZ0r7Z(fnJSM!+$Yu` zt!z>0u1y+siO{l!E<+5~oyoOtyI-S_FH2Oi87(xaibeyoC#$JdJ7 z0AYt;ejU1X>|w7Sb_&bv`>OdnSt=Y2*#kq{1|Kb1;^}}oeC5wu@<)D?>iSE2U%FGm zYaU+PO>68nCEEB*PJY`~pvA*y5!Y7)9pkM~ z4h?&gj*k1N$rFo*+OJw4MLrc`;!Qhdc)Xkm)a=bZtDUA^JUxfX%D#WYh!T$!bK>!C zMb!`cCo+I?gh7562K{n)!2zhT*nY>BtaC^D_gi`8BMiw{Sy)yR4_ zA&o~N-;vrGp5Js*A<A&?JNy$>VAb0;gS~E z8O8bEK6SpcI$6G4-dE-hdz!wXKJUnCD&7j{U0-{17TM?K&1Gysz1(V)+#^G$ogx^-Vk+dq=LU3BwFz530m zo00*cP0iLU5Q*wBm`4^%TB)W&;QAcvi*eX4a1V`?imyhe2Q1>{pSX_NkCA7yx_& zkC4yPL-Gmmz$5Kk_t*6cEYV)5hnt?*4S-!znM1{q{+SMfZq%6d-*(q zTqOmiymwBgv20os6X+}c<&t1As%hoKtNGMUantBi)IRdJ6ww|(EY?<~LO*W-1xNB8a9>}klXd*?E>3bo4z9>(`R+)Pps za`_BOhX35H5aqB`HQ~dr0P7b60(bppcOjkQEWD&Ei!2iE39NhU^bF>tGvL+9=h!?F{TK_4{iVwcjEDQu(9r*T wY+hO&Mb7S@gWr7~x|;u+|KI-H`Daw$B$J!Tm$_S&K-7o#AKjOzXyEa`0In4e?*IS* diff --git a/session_logs/comparison_20260506_plots/compare_club_speed.png b/session_logs/comparison_20260506_plots/compare_club_speed.png deleted file mode 100644 index 25a2d040c185f031fbd90b9891d65ae5ee7e53f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85972 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

rQ}IQ=p6aqTwg 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 z

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

ImKHvP#w&Wt7>hTR!z7v^l$jYPI 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 diff --git a/session_logs/comparison_20260506_plots/compare_launch_v.png b/session_logs/comparison_20260506_plots/compare_launch_v.png deleted file mode 100644 index 8a7e04e48ab95f40e6677ca4fa13faf2002bab19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83940 zcmeFZby(Bk|2K;0P^X9iqO?f~C=$}4fTF-?X%$D8^fnqSkPr!_yF)rgh=o$42MicU zGsYwbjIrmQ-{1E+&wuAS*SXGrhwD-pv3=sc-|yG^6?+TQ)?kKmKp7Yqm>=A~r^mp+ z^pb&rapV{i_)g;#B@ld)^;CQ8sqbpz>GRYb&Y<1Isfn95OsC86>Br|>H*Jk-0i-J2Lr=dPx`+@lQz7*42KvP9^6wg z@J(5nIO=OKH2iZdUYj`vrh0TqFQ-u8m{gq5u z`=%%V`+$LgRVhUEe?OXIP`UKq2SM&Z8K?jIKv_l^cI>|oGPw8zj{Nrl?9^#i#{WJL zIC=~U&gVZLafgq9tN1@39f$sJUMAeaz#tA?YTi(3P9j=ID-07~8(aBHjwgp4%fy6{ zH>OFeXp%QM`ogWt&&#F#_dLldT1gVeM?JEhv$Z3@iw3QS(vMg9mgipaz^P-27jN4< zhj5%1HUC)N-t_nD2^XrvP|;|MKSi~YntSsauGNpV7RoxqVHDjd9eCQkCv~9$p&fAqj9san~yT=Ne`XRoUt|4wrlq zF{|zJno`=C3PmW|jMu+d&1byBCTw(LU8=;S>Zi66)swQ;&O1RXHZFH@9;>c6eDu^v zf9^x)m63{7Dfij?>@nfd(RG!o!R6aT+)<0Pl$4b6g+i06yc&n0j^*J}$M?Ty!k|V zSZZaos@*bR7tX1GtUjS$YJnckd-Um9UmrX>MkQZ6)%;U!R^y*=;c@27Ob@x<-O`hn zA!t24J*VxJ(G?f^b+U(N4?(~ee4LP%tj~47%teT`{4!2};cA_Uzpg~zSWlCu?Qc{u zs^b00smvS_0%oux_qh)5Aw%O)+jM`M7cX8&c`X~DcBWs;gn;K^fOY*_VPT_xeneOx z%4eeFdN`zAoY;g6+;VH96!+{`b~#1e6JX#Y$i zc3ctZLQLm$sc(~V8GoFy;Lw{9ytO)kqo(1wZZ0h?i6-AS=qN2{p>E?!_~JUg%^4vH zAFZP*2<5Gl)Uu4;)d_THTD}k|_XbO|SgN#EvXmD^jdDXeY2bdle*67v%bOm5r`n`E z=DVKBNUQ$#F=N!d*fRa?HJUuDV7%H$5XH3ywxS*d%u6TVd69*~?Dgn=)>dir!TU?M zBW2T@7haQ63@4LYT-~$JIz#n@?i660Z$eKm?;1SAkmXeH8dwXTR7%&<<3;K+S`+`F z4j+Ja9{JoB9@?Ye9KEe;W0Mzo@%GQCJAaOmcQ^g<$oqdd7}L51mglZ!5WJ>!2z4)$Ku(97uI4oplc~VeU(Q)SCdU= zW4>GAHE%$D{)?HPtBMnwDTRfFDprnQ&mT(n`4P@$f;qENK|3I~Dll_OZGQjx6FFAr zO0plV%;lGr9TTA+vAmr3%CEH%uz!(E%p7rBB`l>_)VWwhEljiq1`gXktTU}~K*)M6&py!9bf;lwcoDPjW70S$+a1sw!gZKJ z@-8EHC~v?{n|_UhVV(KH{e_-Xg;LWRcbd|2LALz2SIj4NEH=BP@y}D!$>Sc;lxFQz z*;TGG%Vy+ot^7vU?NJ_|pBHU?O{?vu6Rusqj%@THr4i3BUf7Y_9G#4-77O|@k4yU(pIW4F*)vND#?(6D z5k!T@f;Q$Yf7)Wgk~vxFdP}-*jU#s304M@TeD5XRu_I-TrmluUxDgN&!s^?Yx1T&uwgM(1C3s zkTg)=A;f#kYOwpJzD|(%S!TW7Td40vj#Va9?ECZ=t z7|^;Z6@H?*khb&paHz;@rARD!uiPry{$?Fwb3nY!JxB{+LI1T<-Eqm+P~1ndB^DV7qXx3XQ6A?t`X!4|Z*bQASf* zDtfdz7R8+>X$vnIm7zxvU(wFf$>F?80A;!p*<(MDH%IaJ<<0KtP-Z&CwU}UqBG#nAMyqV233dhxwSm3A zaD_#J^U4|L*lWm(+fDL*CBiWcP9R&#fyN zY7yt@=yN=}q2Hg0D-H7#v z8-MAQa(e@#gqDdOt94GUSt+fXboYqK$;o+)Z}G2ESPf-S{QUKA&rA+NbG;M28pSQg zD5|tIs6R1&{Q#6y7kXzzxhApvK&2?||Gg_si6NJEv!`?=$1BStEo86^h;Kg)&-!i6g6aOCeY#>2BCu%#LPC(dfbcV7E${Y01gCD(yN5 z79D^tPY)@z%31=zbR229NWy}J`s~E{8*U!{_ATOTJ^4AL^4NVGxcNa}{)0?E$;wIw zqeIx+3U0puz|-Z=Dy%q}al)mtnP1 z`#=dUQds|W#ia9RBC%0`(P=#xR6xEeKrAu-Y`E|`%10OMEDzwSiHd(MnJH?UdWo>G1|yt5?9Y~8RrY$8vu<7(qbh%jTZsSeT%xF(`_z|1 zWcfx(r;(dje|9HSi1UZK+XlhWPu$JUzfm7V*|Q#i%E$m!LsbBlH!wuI4kuNjqvTfaB+kt~R2iXz8~D4W$IW1$de6lhH%^%iq zO>M!jHSh6xDTn5aD`Wn0p^0KvX+v+hR*XKLjH8biQ8QZvtZh8chRyLpdHsxjDANw#=Z3cXzLy#;An=x`$26#R_)Pud4TbvorgsDm_i=JH&L=sRfcqi(Ylg}hkHgc6k~sMgRq*NQ ztcYYz)TZ*N3J@ncg-fKBdkT)lI3Uhvflq~ zQ*SPQWTsN^JV`qHcXGoy2cdI$GH;vO_#l@JzX}hUs7@k^?xoIlOm;-f%jv1BL~=;F zcR9}HHpGOiiRx=qC;BL+{~1f4o>yRLhOaaJb~btXvglE=^xxa|{a4xRQ{{ZhD36xp zv@S|F=w_(p4!bliT~9PYd}Efbr?kJjXgz%(+AmX3>36WnbJ(A^_<2&LWB7V%(QfBz zGue{;Lci^}oQCe5$v{k+!eLPik#qr^s3s4c_6fRKWKgJQR2!xQziBt?byLK&`d5`a~n)4j6I{kLEj8twqH==Z~Gc&5m z^eR?*Cp*d)@=8J@ezLPS^X}a}S*V+kW2aoE|L2)2RW{unvhA(_0cKpQmO%Jt?7r~i zaj;}t!j@YNQd1Mn8M)^HYtc#2qGA#N+i&u#*CGgA`%&d74FtPN_Y;?KE+}SpaKqc)peBR?BO5 z40Pbt{&=(P@*MiHNl?cuAhMKa|M6L;)tix9?n-P={&G*3W_)|eZn@ZT7UIz%m|vg1 z?52NFRxI)YuL9Du;)wI$C%I?e-;^-CmnHbiiEoW1)u~_EiAeS_XkqoQ3}V z)SRU3?{$9q0tMgp);q;~TCHCEV3V@``Oax0GzC=YZHy9)bqC?vvTI}P;xOO!$)&aE zU5Xgd8lq&7^qcdDU;$|N=yz9%yri(rqz{547y%L0r=exfP9P?TSR}0_$DYxWQ zcf}vh6+QcJV7wcHGYQUUs$7K!%&OyN(5~@sJpt;mg*<5VT#6OZzn{*lKz_iVI?MR< zfP2*uTjn)-ZTXRybF~;CQC8-8^_6v%n&Z4~yU+eA5f9HC1k_i_%8mHTC)H|uh0B{_ zx2SUpX$P$42A_voopNhc4zaubd{SRlYq|=<;#tQ$$En+*`hfighgoKDrD){km8`Z5}i`X-cJUD zI+&=uJg!k#>-!V~mJ9Qt3dV6A=a6y}WFjuncIl2+y)f>(^i+)17&~;>zE$96PNk77 ziLJhm(nkW+W(`VA_SKkc1F_u9wRtGvhRL7p=qPl^2hg>>$bzux)L)a>DvTmwzD!r6 zSvF77II&hkI#sw)@XJCfra(fs;T}ZK(^5eAe!4%=EM9DYtO4+n`4=74(M$LWYYlcc zPz-x?h!6dlJcaFI?!%cReUcdQoYF3yhaURpL$bVB@9AB<$L;Dbx@oy}0iw6{DRBKa zzlizf9-VceSl|_8X*I5!3T0CjA)XpMi05D0m;D8TvaNUNB!G3HhgrLR`Z;PEzC;HS z=`~3py=8ojY>CYv$-}vY_zE!v^MU>Ha9OLk<{agV|Hoa;(Dunu=C4!c^G2%eKM;b9)qh1R9V`Z%H-5djvo>Qf z$`3~t447;z3ZNfwe_CeUN~jZZ5em)I-AuApBL$9D!bPX@PUhiU(4~(p2jo7132TFw z+z7~5@3wQzwnQJVKYQAj)2)`B(v<>RPAm#8=9IjV@tCi-E;{84xhbI$-BHDsAe-O3 z@wLE1VT6k>GvgGB$O3N@^iWBi>_GPJUxAvM0~*n#>*}UhmJr( zH@UEusL~S`1Ufs?E5FCp94>OkZum63-3I6B#|MTGgkuvh{PyW9b zVE+G?`G0k9lpCtt=QJb2!z<5RQGaY=$TN9mij?fy@?wEXA+=hGdiQ{CE^_VK4?tU| zxVeJ0do54id?DdA`Eh@g!uI1johw=&u4|2#)-N?X^2Yg?)jCyxF_cr%!JwA1Wip`H zZ!6Xx#SQB(Yg`s8*3Ew9JlXUq)#??1?o*450%yfy&mb(3OV@nLQV{c>5M?MDm5lZR z!{PGTQK>i=HTx341di2UEgfHTdw*oUDE|Gc^Y7Se+kiJ71;Ugo;Hs;4c6^KV^P_+c z^9xeWxVw#WCFUi&RDNSo8VA}LoUSlc+v}r&nC1*xsLXJ=L1F9!l2E!0eiH}Kj5#9- z%5b`{K0yP~kPx^vWL(wbgEt#0GA#92=y6Gwbjy?PrIc7UH=<}@MDz~k4J1$apxd?6 z6(>3((5sD|K>D%;0~e}Z`r4l(6eonMSkqtC%ycUq5lZr$5GTR?8DgxI`1I2JNuy&@cKrXst`xOCpe}b05mZ=Ojokx zZ1$p*N7*=6nE})n1=XgCP=@Zg{^;YBdemab<XN{H>W3oP+6&3K`*TZOu>I9_I{!}+WNsWl>2E#qEp zbpYmkuRAx_>48zOef##UHvmwkR~HBJqbC4f90v256B+aI);A=1)tO#zJMD_h>Rg7! z;j*a!I?Hera;z1dI_34ND_sc)43U5P)tW^|;!|@X{JmLxtzt^HsZ0V0e`{WKF@QzU}@Z~`g+bQNimI+H2XN&iLh1~WD%X84jOJWFBi z`#C9_t_$du%FcE3)*CkYc7M~DjudehIB~sYVbi2TOAEd#p9< zJtTnK6XY&{A`?l(yGts&cNid20sFqvuhjrMR|?3|)#2QS>~y7aLZCEc&~-acfMA=k z*97Rg7peIipEBY`=DU;8v5zw`JfGzZGYpyl}&v`b%+p-nz^>nPoA4MM1Bpc?JtD_*|KsY5#HltAiaPGxt&eLCCLdpV5#1FH` z{|$1yztk{z(_p&|+}{r*@(Q=e4nPgki?R-Odu5!0pfwUi!>Yw38P(yQHTmGplik_j0-iXYf2_RPwklH;oW- zE3UkpT$u4Hgl+H9@-1ir*Bv*$_JegMEx*nqv-!Fi7`hs1o1fwyj^aIlQvCH;_ssW> zhTRjSj!7eFV>VV}V!`Sn%K^<|k^B9}V1aw522j3$Pf^_$Wnh4x!LF;}c+ERCMC0}J zFIwtuds%ua-KO=h9G%6q!NJyEsEQ!qcyZV`vI+^PS(co6cbP+6m=?;W#!G4D7W-fP`l9N zytB^JdnJ%8XKrbi6vqgG-iz;QXTUD211v5Yt4FzixDIgY7u|UG>JHavMa!?h5x!K~ zzFc2$qk2rRo&>ihl0-eHAT;@U5Sr%WeYt2eV>5hNE0q1JdF2~ zDg$&S!OZ1F;QdVvRrcz}zkT<`bITnpBf#FfausEmI$GpYbZP+*wvC=oE$%OG8R`rx zl|JU4;6P-FA8K6GeY=`!otKjNNd>PiU_a>+BTI-sVzOKYlgZ#x7n{?R1}9V^_B7*R_}(^t8a+S+(*JFkmy zOLldP1nu{YF6{{h^KL)HY9GUC@ouSR2nO5@5s#hgI0A*@Gu5AFRJ%YsGR!~E?{Xz(qiT&9)`5W^_0DTx93#!^tx=~Cc@~O z2D3ZX6jHc$8XmSrZ_&%%|=;-ywO` zcaWDjEP@4VM4+14WHoFz^m(E48koEr2&>jnGS@Nfj>*;C95K1?7-&6C8l4`KlR=jVhd*jP+#(D=I!tE?fM2}Uxqdun&@d**H9GnDGNF1Z>l`M}NfA(IV z{k>Y19TYo;Y1rZ~OxKtj-P-L-feBd4Gq~McA-U{UcrNvGk>Ssu`}P3Uk<6t8P(&>p zP(skXaWDjUbZ!n9gU=IGf6DgO$^=@PZ~!oqFKD>)E=Nys{c5BE1oTht2l~bI?o70j zdsENyP;pg&RmC&4VM;S)6bYvr7P*XJ86Y{Z6G~vCkS$GXotEfV zdzcDL3?}Ul7aTJ-Oeq_Hcj#O*^_lQWGz<8_X6y^vf7L3M#BS~uYz8{nP6>q_Xk);r zup!|%+y(X|?K!H9t_7dt<|ba0_jUV%vA~vDR5_C?;hp0iODPWhIS*QovjDY{BICg*Dz->|D-Rm+>YX|A zHaO2&M>FFx^Q8y(@BcL@GWcBuI2%8O`3WDq7Y+|BTp0vUU>G3|C@)AcWUS1X9`KdT zbsp3-k!2~Lzn$uAJcR}YTb|v@uOc5<16Gj-z-QFGRbPoD>ens#UI}bP_>ZS*Dl)Nxr0MZ}vdz3+Na!sBE z_vrY}1y`Idj=i8egJ4p_30U`XCrUNojmn5c{(7;*`Ek1G$%U%H>o0*N7m;%bUU;G$ zv3g*<5}-g2+<>p}!=W3kF2X8Wy63-QZ- zK<&Qkty{O!=twfv2eJk_3c8&}Zl!fM>d?xqS_8gpHK*&+%mDCH;Pv|(%+IO~B>EEX zda+G%TcbF7T+^tHO>c2ZyS*b*0K-71|ZZQnL zlS&T2PK9aI2^{oF=eUVjV|uj7(ZaoMxp%iPqAd$rtJK7vLgy)ZmDVxx8;^$l0XzH( z?K=DGz05Ga#xW7?`&$QQ2b?#nA=)Y-%AS{s+Q)roB6pF2z)9m4DJsddi7vm(qA;j{ z1vq_PP>U{K%dYryy3_Wst>Y*JKzi;aO5m8yLsbug&$@oQEGYQxGxZn$M)Ug2j5M&a6D!n!tJbZ@?;R^_tK4i}nUMy@N!-uc*Is>-6KitA?P0 zZ-}?=0l6aIEbphIK!~aaw^tl$1+4J@tXq=xI_kt=<{zENSzydUZvlyC+OL_;E1&>= zo(q`-WP?TT7HikwhUnwZd{&qf3<75L3A8}-&P1Z)?h|h8B<}OmZO5N8mwFz$WOFG%ShKvB8$yvK-q=Xk5fz!f_X_9vdk_0L5)ozstpZvnci`SslG|& z67yH#vs9f5bEATI1 z$YF$?U>Egl*E*z5q$!7roKOutH|LdU_lU#n3*YBoqeO{~1 z2YwxId;?Hdyj9Fe3g%Kty%By(RsBN*l8pGyA3t)=FoGJ7cRnC=4KdJ zKlVEs@*Bf5Bz6my1u3RgHd^<$i!`GPrK*(TOV8$BLKS+(3-~Lw##wR}uzpBWdfbeM zt+K>FIk7o*K+?*c42l1#w{*LiC3j;%O>kFphDAyz!pB`Dw`k$D-neAohmLT;+z~nZ zoQ^Sj7oJ?WV-|M5H${9m^_-xzPhEIk>WbJ{`JT4EmS6ld>}}{1 z1H~_>)&Bz_u;6Q&3T8i%nyNXaoZPOxGz@>_@x91D$9)u`YjTx4F#e=I;#PcD2VsYO zws9TwhTloX{pn-u2Si<>25r~-M;&!ugj<>}08j?6UsUv~R{-{?vXjXj-Y+$E$Ie~b zH4%0|T^f0z*MMkFi;C2IXTMrBzKFEi=qqB*B_Qw(2F;`aGjFr@70|`JuWb#g!*<2u zSD*%ETIno!aU6tg(W6LjV$yvANopZkUHkF+E~Yf}XQbZkuKHKv{wqTK=NLyS@)`IduNkzeL8TdIeUV}VS|X6jlxZJ(b| zMNs3ca=IdLBtzNrOwMmP!x=;F$)pUOKmpG)MexR*2U=Rpzb%=)E9Qt{SYA~;@hna0 z*-QTe<#QIVkby^#RAo}PTyWm?!IM`3FSv{I@lq?G z!d(yt7I=xjitf_E~fLg&l2m{=lIH;{r)axk;N8!~Qw=VgTt z$&y?J45baV6V2(Pu(dQXWB^Siz~P+3su09o!s>;kO6FI2urBtZHkiViBu*Q(WzwxM z?YrdszMnU6z|bLZTJo&trTpV8RE>vrSKo=w?<4pd2Y%EB8oiYMsBbW-{cP{m1T1#8KA)%0oN%IG)w9+WqYbg`|ec zYB#>=-$qsiH!;HB?5+kzp54e*f5UjaE#!T$>_Kz;H1HaGWq)1&M!8^-v1j!wKFqB( zz_N*bTlPF6@}N^ptIRZp#2ALMNskm`ZCJgC_81zoq;VrA{)`x(;v@z$>{cQLE65Yc}nI$vV=3%T;S>=#I?};SJ40Qjn19GRxM_Dwka4be&6;n?5-l z5FZfhLRRJQ9^)uQ2r7LNyTSCtUH{{PPzPb)e{Nw+?!&~!bfAtRqu_1ouMxCgfP)-M zl5$RR{v?{9%9hgW0c@YP*Em>(x();ifN!)Ro#Tvm8-#uKS6AebyH!sR9ckKX1&|>BeOfdQ@(@P1Co2`RW zYb?-0gD~;X!OV=l7%+EqLSv6+i*s^R6p_xy-{~R=Sio5Y9YtV;fG=#SPryVu?)H0$ zr1MU?Kie(b&zLE9idX)0Hq)WAFf4WK0#cDy8#cxcY1lS=^|nKP38`nI5*(u1m|*ZK zuJYwN6pA=1#Af@`??J$@`;3TV*|w@nwFrQCUEb-M6NO@TO`H&ro12QjG?;vaFK0uy zT~2zhtB^Y*c@MVQw4se{o#tfiDDA(;C#$nQHop)1thtYwR*q{sTe9~Hc-mHqJbkMk z0~7N4P*2~2X&WD{t1$%TcJGiYQ3E2kFC0*?|2!l14fGfrfT2t;2NQ-X49{n@SW#Qe z*uJyTS}K*H8|J{lk|i9mU~#@6%;qze9AGKIa2Bq!s&I~Bsy(@fq@<^sI?tgMk82WY zyt$RjYB-e342hS6osT^ju~*~_JlJ~otw9N@DforseU7JdywnEoL;~K4DKbj$XGn_B`6dJ$aGBymDT_a(7Ca;yRxL0?P}Hd%TLnFW0C$A zAB)=zVUw?#d|WDEy)SFk?dQq!Ruqd!Z+NI6-)4CrRoLp#B}nGrv7rUn+?93C)BUza z*&H>z>p~BYG6>-76U6oo1O&?F9!8BxPrm!mr1=aH-8#Rw4-|I~Pjf&fh_*I)d?^sg z#IEaIyd7UJe60IiR?o@@9mP@SY(~U^G#pu&{a9&3D0Ggp&cwnbK}cie}(X306EvX06S!R z08|D{t>3}^ZW(tWIkz&jrP_)b`tc<~G?fBf77!?0f7LRmkslk!@%Wl}Dmvihz->kT2lZ@}#|I(~7etXDbQ{acmC_A0EjcQ+tuII^)I1Z7N=vNE$ zlw4Lz&1o z{E^;+!RIj?eR0OGtK=ywRW1~X<@?!P5)FmR*9lpM?1#sUp@Va_ap8&TJob#5@jvuJ zHLp0X-!62#W5+9adIPY9reNZlKx75Iib@q#m8{68A+o5&J&1UR-U6{m7yY1nUa@Q? z`l++~D=1neqpWwwk-iN(iI1AS_BvdZ5ra|KUK#V zThDPPr{@K28Pc$p5H-Lh+)%^qB|jung17-Dtn~~+5i#5QU5I|i<-bNdFk27%{gOHL zb|YtI2uqyT*^=4ns6^Rbwvk?I&wzr7Jj{=O`4NKM>C`%%uQ(S2oHiinUkq+<fIFxFdgVHu z0vVtaDImEvYf|y-r5Onf5||1d+RhA%4zTPh7#10<8I+li4gi4+v$-^&Kqs@J`*f@# zIij5kwC`AxzEAev2Y4_E#%@Qgy zD0gXCs_Ci@=|Sfv4^``v_z#akT@8egQORN<5cd}1^tIP+JpTM;pf!-@)XrZ{{+N`s z3`9xWaSwX%;?K;?zhn#-uso;HZHZNg4I3Sm?lc8Cg8W4}?-Ksl+}vCQ4{GI>4u}lM zgKXE|!9x9Z4?wf{0$cdS&5`GTrQ-?Qf1v|RrQ-+v2uX)Q0SpiiCuICV+68W=G=d-s z-@ZMt0jL)=)smj6Xc;ax#tby0d@yt?2nL{IclmiB^84UM3Usg3aU;z^b5o%6LbgCL z+f|JAkUQ!@@q4+v6-rE}6McZFn=b?gT@d1tU9`Ov<5<@4vy>i#n0stALi&*Pw}m$D zazGMM!hZ{{)lY}}(-y0k83fY*_c)IrdF9Zwuv>tn+NPUj(}1gXmeRBXq&)@TugO8@ zl^c~N<@=B((?E7HtU9%b5Kz-v?SJ6>c03J(%~FdP_b4U;%WAMW7R0|?CmLIvS*cc( zFL>a*pB^4K*jVk%s=XLOM8^Uq^)?Wk$R9!fnteR)UkdL`K=Al)>S9+P-oNigyq&85 zFM;&&NxL@5jAB5#B_lI>8yHitQP64(*!&6@6Wi1|`^lyT0)L@tO_8HjB#**a)r%Q* zod=E6uQ=raHt<}?pG5C*SM!PjTBOd3ac#A3e5|>p+65$tLThgWyYT|Z@~ndZRAa3T zXp-_k$@2W-9i?b&TKC|?^|HuxD-bbS9}l1q(*Oln~zGC5BHK})d`ekdYPPv>0$U)Y1?koLj zOX+S$^B5RjioMTxFVW$BTh%noU!?#pzD0K?~P^T z{J@)vKNp4omI`OVt}Fizm^2Vk9lg_WHgm;&)b40uQ3sfszBbFiqbjT76ms8Prro$4 z$J`}&gdsO*o>`A*W);3vY>?1jWcZ5Zhb_1OGS`cT)?mAbpVD5ZYXWnlK<<9=k-LaY zy%@UC4$^vxHGb<6HcU4;9$W6wb%aX3TeFdfj- z7f57`%9<|2{p{zgVRR8W`IjDirbm(DYc;z68+I32||=nhC(VP<#k$@%c%xSvioAT_%83jD=sMn|*@5SJH!e41Dgu z1u;r@w$e3zZtsz&QvQ3JK?M6UbeaMRFit}&VCeE`yBe>Q3VdFGw$wS#%WL%qcZ&YU zmH$aD2OTp4K{TK)r{?D5i~!Dg1vmv*>eCoi=Hxfr)?mGdx*rw~_Q~GTU3k&k*@_lXjwR?`EmATD;OoKq7eWGAJFm%|U-(c+9!lkAiP0zlr2k znglsPgHW8Wfd5PSQdCD(_46JbZ;al&Lqoi7IoNZgTedo=0TB8#nf`kw_%Vdf zGeDf)4fKQw<>cnl-L&Qk+h7)Pb3LVbv4_{~R4|Dz&M0@z^iqLB8F4PyowR z9hMDFh~|*&pwqX+7W$wEg#us1yL#{U9C)ZZNN}BD{c|DC%>lH2b-ELjTt$QM#R# zf#J-9+!eaXL^FE`ICnu-hG$hP{PASvi%D_!7SD8bu~<&EL7SQqKpLIC z$X8QSgHM!49UFSC^WZ_|b5P)vZ&^n819Q+S8Hg7X-Q>4T_Eyc?V~(|8nU=h@$=6D@ zp}X82%bRPvsv|-yinCXE4v#$rN0xaPRuIIT9xrT64=PJ*Kf>jl?uyh*x|2{>|N&|c{`EWt zWeZaOS~&e7C1pii%`PmC_}-Oct$?YF^kSGQ%L3Nbp8(MI#Y%ACykSN+z;e2-mGm6= z^_xySJAJZa70949t+WF<0u7K873klQx2#U9C%BD z^ZmPFUnfMN-lb8MsFQZD>8E)LTzr`K8}+YeP78sly>AVh(=N*kE*4B;`7!A52~;4$ zl|3pKe;M;Z++#f(*j_-Oeel`D%jyVN|Nr$IPb4^+n9oMOvDOv@OCYPy@9H^z?P`D! z9s^O#;`beR6JKSYsOp{GQ=sx3WB$*Y{d)GXFYw8Z%gcaZXbm{R1Q|K{ipco*FVMjN z`xA#KRgO`o)q!B;74QS#$l+_eYI$Uo|JV5Yw5OabIk*o0qvJYME3!s7UEiVi-_K3n zrlv{Bk9WZRfp!w;k~_V|<0|0~A6tS`%eVnrd!j$E&(QHGnFbalIQe`#_PHqUGvFn{ zPpD*`trk}JX5)%?lop$K(oU=Vs+~rzqZHCGlNDGI7D+7$vs}&i68eARc3nV2y1Hl$ zQd!Pmym4+Qm#yJnyzpyR_IE3)#f}FS0usb9SZ1`uu2jWxF zqzKkDq6U~RP{48K46MJkK*Z1dwjYnt z&W3L4(^`GoOi`}iqGFD|qT^~D0Eojx5ErPPoqbLK_My!54FI)`xWsSYCW7?NXW-Eq z3GW2BY6T?4ssW5?cwe+fO>!*zef|h~F)PCS^~+@(xJ4O@SYLX=3m{B9P*0sf=-wM# zu`07p!7;VmVhagSK*!X;)qZ|?x?S8SVJfxC0;W@ztHZXjx@A=Ad~42mX`H3uJnR%I?V1ZeZ5 znx4$pJPr>YJb2of0Gg73umK~XnLJqq@)nZ-j9Jqauuo$G>2~b#J>Go)o1fU?0H^Zi z(UF&$UwYm>LOFhm$~bgMOe7FRJyjR*Vh6x*XOL5+GiK*^DmX5APa+l?8ocQco&u70 z=5C9H`DyNw)9K)yC(!%rozD9^Yok2mpPa5E!1Cq-rU3WcQBh#S$|%2O+rveV(3d>> z5iXN3w=LfhPy~eBUbEMyxGRT;?Bs}tCA}pW>9)7Bu(yHpaXi^XumqH+e*heM=pKj? z)Pg~xo%yEft{S?)vo?Yr3yn#o_w!?|BW|wBh?u0MicuVS)^WlFN3P$BD88o*6!Q{5 z3_}7|1oKm#0eI)p>S%d6o*nkH55#?^KqTh-xe1z;?=-~t>~{k24^#t3Mk8J$ifd_} zaNGvO0Kp~!5j=NPnwd+(kBRlWBr33SRRW^A@x6CnU!Q2k98USH^XpoA!-r&_n%~`6 zKtEdl`0)bvcZs8DkoOtlW$=?Q-qNbGpQW$dc085YWY_HfDHFr0w;O!?+;uR-WLHj< zpFIS|y%%-k;&1j3KL7^6T3~^MqsxJPpcV*@PGkT{#0cb$cwrnU&*mb#$_7bK!21jw zN@SZwxMw?EAF8&hwtFvZo2Ba;t+-ck!e+PM+YSX@IVO;9PV+b__~9uC{;n!As{YNg zh-=+G`R}+``9oB%W#~Ps{-HMKI#FLBQ2hAh6~Xw?0U%y-%`yDX&Z+}(U3I@*>G>^l zA+f24G*{rp&aA6=m?&E7x9bkxcp@;E#|LyKTSzVA1nbVa-s$_f4_UwH<;94H#f|0! zeF7}V&^80Zx{}WKsLhi&U%m;WRk^8vW!euJ9iE_HS#2spGW%f1nbF^y;L$ExXXKq1;N8ygcjNPnjoV!W|s z1<*>(03t{(Uj*`_cTHhL|Bmk4dN3#FDMtensi)Bs;a;s^v2x&{`8gRtamJclqQ^p+ zdGQP}t2m$s3g14;@C#SAc`~`#zkH&}GnAXw36(=k;#czzlu+%KbzI7$F8e?W_1OmY z(#A(Cz}?kI5a{MVvXf)zK&=3%|t*zUmy5IM) z3ITfNb8=t-C?)g)PDlE5BmkOjREZa>GUu&lN6uY3tgI-uKRNOF8;@1ZH5~p<{9URI zj)z=F+Ef26Rjn;q%)$9h5z>(X-o~chDTHq6cyw6v@8t`ixRqPv?3x2Su6u(Xi|t_A zgJ`A;uy|Qi_ALr(g;PNHeg3dK@!G?L`g9O6J2v|I+ry`PvfW*gqJ|A;;Fq`o)HdoL zE-|qNov(`i4h50{5;Bz?f;++;4MFQKc!WT^JHcOJFw*S zev5EkHi>qKXAH>C@=H?^pe|rQ7Fy)it-k+7*p~-Vxwh}FloCz5L}iFZgi0i2tSE_P zh|HzPEOW?EsWh0%kSTMKd7h$CvQQ%PREfw`ge08nS?%w4zH`nWr$4^EKRau^>s`R=tQl#yTJAxR6Dqlk^E;5Kfa!=a~ggNLapz8rG5u0Pfk6l**?E3z-c#3?^5 zBSVAPlwX;d!Z-V~$a-o@dPYy@DsR4wPlL@^clmyp1@(aj)o`a6NO(_}F;vhG!jP3e zSwd8?OW$XZy>oXdC4|vqpNL_zm6y5n%Ig%W`y7s5+f3yX#Ov)b1dY>nW_9y?awW>L z_!@VHPTW3<#ECpZIN35+Jy7J?Z~5U(=%Z6lF4`APD&E=fX%h^y&@YAp211slWjQ^<(-UIV68Gdj@Aejl|JGan3zbp(#MGfuKCK0S^8)Fp(()b?LoYU zNPqqd5+FxTZ0b+L~o z7ll;JTowKJ7)_%K@GE_+qs6L{B-17CPrPxQ)2-IHdW5>1hf<`lva0HQm$=gaRp;?% za_o!uT-rA@-`aXg3a;iC`N>d-t3|H1yQnM{m17gwr@cXSM*jRGEytn7e9~l%f?xHI zL1L6PG+wS;q^+bFk$e14hm<3TvhyrV$H2N8NR%+LuzfityLIbJbl|#I^qDAfGY^L^ z2b>gWb@sn|)rdMUKg6-UY?6F|!Cay^!hAue=s5}EKFbKA6oCs)i=>n<~Uo0kMlu5vvkFVkF?4opT*R+*{;K7QoFVM{d4lZ zY2`JCt{Wbil(KG~k`C*aI8csetMC^A?)NnqwuHRn1zm{`SI zsAgslJ#iWn$u(^LJ=XAlX6760(Hn!QeR;7p4yr-`S=Q5B2QEA_-JP14oXx6rOSQ)| zEZ8!v;+#0kb65<19TR;YK!P`%(W*Y|UJ zm$Y!sDd{HXmxq;l#wK6)SX>j_aB-f;fk7eGb1H(vyea9}y6}>$p}3>v-R~zQmG>5S zHn_}Z8RoYL7T&dD*EL8Ges=sznTW9P+t8y2p|7}_=}O2%=ee)hLd$8*nj9A)S7cii zC_Q!GioF~-$Qd_J++BCRkBy0X+lQ#hSCY-#t&Pndp1c#_;2m=f+Xl2K)_DPX?;xAc)SNBc!$niwZLu0^ zmy(J=cnkCn4EH?kv4xqn%_)Fi?J$}D z^ZoeoX@0o@?*K{y;CDa8Q;od0Uwq_LP`@xAmmOjcaL@)0&m7`DalWr4VwjZ}BALC* zyzI@Bk%V85*_In!{B$k5;5O0nM~Rp!=Nx?6egs)Y=FeP$A3LTPIzhO23JwfIiE5Nt zY&9+@0&)*|o@sisRA9X_VsK(6(a*cClx1`uXJ#`%(uqqImus>0edHZj{`|(~x9R!( z$}@Hs;;6wV#4d%ETTRXOnsLXDOIT7e=3ZNOt;k!|5+%l=m%fsxXL`+mCm##Vbl(y7 zF(=LL^j+S7(lf=(>z~0drX`rwf%Zk5xw!>S<^IA)xY?r%K((_0u>JLmd}WmCE|qPN zI=)8c_X7<%KgT7}hdxR%1*>qqqb;G{$jY=)m7dfBq-OQZ>eJD6gB>^C*s}L+?;P23 zQk7bkf`h$sycAAy=5}EXxuf7&hoc?e+J~}+%$&J65b>WZ}8xklg3O#3CbT*Mx zBsUqwzV{)|!UbsUG0%=oNKW@rRyhrgF~MbdG?2X-HaQ$+s}t z*)tQVbyt3C9cxk}--oo!Qh|?CUf3JS-h94Hts|jo@COv!-#*yOKc&{Yc*7PU%u=UA zGS&*$PQZ`vlWU1BJX7!kz;>Hm+UXaj)teVp-f~e-wPqjm`rAb*IVA-ez$p+XWb~vJ zO@7GMa_J3C?|YuOIcoPSwWewpDsNrF!sKQ#bGK-9=H@NT!zsdl#`|gv+n*{%GbWsV zkFs%0`OC~$5ox!CFMb&jT;t4>Z`Ja)t}g`i#T7;7*1=PQ1tGlgJi%GujoRQ){rate zINlc(9qLH=Iynve`4n(&gJ+g#U#39lmHzHAVs`Iy(R{6zE#kIm`=i$ZS+JuNPaHL6 z!NEsc8@jGptObhEK@r9eKhoE#%jF~*eyJxwpxl8Y)-wl^hgNe*eqa!&+ zuHMHd^TOBvg1~cRQ`tP%+?m8`&6B&NWSief7JK~s((DD|?I>7XP3g>g@80{$Y z;Wop|-(G^2kWOQbd&?NA{tIC)!>AS=r}^)t$2pCfzc>}j@pJR~p|0zG)%(#&F)cEb z^~Skv%M+N(3gGff?nRO{O-OgmOF81D0z&fx<-I!YsFUCtNsjiR!##j>6?pp zIo%`J@P3ae>*>x&S0&=5rd6k&eI9N}wPMh;L#oJD>iQ=d)pFkRri*v!WDYZ^ABi6( zSWFuuyFt*|2v?3L&ynlslPfoIZQ!>`%iB_;=E+S|6La>GF1a!ieEEqrM?M~HR7vb( z+4hZ}*<0Nt0dIznGRk zC;hlt=()zRMaKIBJ(JyTYL)3tNLtLtBY|$~uBaWb29hL0ID~2@CB_{O!m)1xh4`cO zTA>0b9Sk!??7MlwS_txh z0WQIZ{=ITu^?T8xH~cqcKetVYt0!cU$VntL_R`M4yt~;2N3ouPLG+P3_m|{_aL?$A z_uaobbVCm1YnOp6?Yp|-9a}u*!XF$@rcj?Veimg9+97?iKgjOc(d1Uq>QwQUijE8B z4-&34IP!O-#c8dUEsFA%8vu@w3mS{`r3^B$SB%MC>$nxGK14fmRz3N?*yg1EPS%yf zyga_yafVs~9Y;3#9U~VUH|*CMM~zq;Zr*HCtgFRcreC8U?RjBtje1)-42o6_NjIHG zJKO3SwN5cF@dV}EoPFld5Gv^AxyZ(2jeeIgdh1ws z31e|T>DWv+v*!x5+rfqJoMVdxD`&6aX3TgMfludda5@|fa652$tx{Rahl1OpOcSL; znY)_l@oLq##xFM7eoS$SP|o1*O(E-N>XyjeBt$zg_UJEji4gK=8J@1HI~cJj;Bda~ zjiI{8m14Jd_pF?P;0n#8Zrz1#HoW5z}Rl@ zYFwwTdz(+}z>Q}!R}<>{$|dgtlkN8+JB{HZJFncU%8k=%wk;!k9V1&}S_HVbhxN+S z{X$%Vl?#+e2Lp3fR*k^_0m0w)d0K_`MV%upDMh|HXkldyWeeuqyC4Xem=sh;1 zpDs7zsK>;8M2n~n%ku7M<^A+D%UQp=xmlb4n#e&KES8kY<5%@WO`ghMUE2IeZ8M@6 zLZjH%^4&w<_`vzOdQr}s7`M;mDn8qU{tzo^}O-dO9q|K-Oe{Qy36s2*$a4@Y#L^Uen`Du?$1#+ zB9U$8?;&jbV0X|h zk>}cM&VHL33H!8<-BzXPF`Le#01rF&z>0)jCv$(fPYUi6Yg(0V6yvE|d23Yq+kvF{ zIoCo{^&daN>QjQHDpG60&l|IGSS>#hS={4nkn1U&kU;xsudJpfKAozfy2?HJvt?86 z9cd3io37H~nggo55~`9*C53-;9!^eT{XOx@UZC@9_2*t8$C@zCoYunkPRW>ti68 zr8w`OGdH-{M(Y$d=6q|n66)SreBYth-weS+o8DPiwV3IPKXi9)+2g$O+|yM{oR_@X zu~WZ)elBdZ@p7D^vs3cn+ZlQ<56({OM|$s+ue_DHn74ii373LKF&Y~;N`wu%HT}|F z=CdGj`(}>s^_5*&nJ-^t-IMe8EV7<+pYE*q%pvA5G$8$J+1}3_qI+)p7v$GP)!mDX zw9z#vchBtI;)xH*)oIVq&vr8YO0r#tS%k;bIeoUY03YR0gJqY!mtB!$FagRoecrm~ z!JN?Jmpk^pccz{D<;=s{f8dx?A1yOcDQ2r}58tVKfxWEjU-Tx8w9nS;E*!qbo<0&N zTWY#Xxb#hfxD5^7YwusrM;cGI7~D&|m(s*;T)ZmG+qSH3>UUk+hd(b@XD9%-8-2da zCcKBUGnwyUr&#x++cZDGt?-~aMrn;MU? zwug|-3N?OB?f?F%&~yBp{=%EBZDCENU)H8MwwV?>zFwj4SjE*AwtY2+nOIiY+Xk2$dbJXMJ~ zDn7McEZfJNCBb2^k+F^{ zznMLpJGricsnUmMD}y0IVf(xr?M`6PiQDY@CMqh0W4t0%bE zF7$mC4ObL9Awow;Ddmu{bv?72Rfv~T!An7JYI>b9XVS&?5)!5#V zbN0sU0@#r=;Ggqv3zzz>6+l^1+ls@PF!4yqwT3mTVA0~G#a29V`H6;F7w8z-8UY|{ z*`M-EXq%#BZBupo7kaXu)t7D-?LyO|KmPLp*;Y z-Q`jpp2+#lNeqw_7+P>2A?dxZk>|BA^UnL0sT6MJi{R}^@_S%N%mpXfkdTmYfQfI? zoPGVOztjM8U$1hC=oJy)KZ_OG&PCR3E_#*0PSbFx;`T@2ls}!6h=C=P^giJvto{!dmv_?(uY}Dwg+ifMFlL{hM1c?0Rh=Y(@V^4mK_m>EcwF$?bSrm)Xw;dt6 z2FUuCWiB0XW%Oi?Ue3m<@k}XX<(DyNP>NuU=}t&Y>>2~dw%>KspeP-2?r6=Y4D6)c ze=@bIj{}SR@@DOL*L8?5*zD~{-TVY7b&Zk7h?uxH6{!muAiy}6hDhY!rbD|tf9`9h zEJ+%BcfoO4-;L&)qjgM!LuuQh7Zy%lIRUW&@u+6Udi}0uyh6@k_TdP0ShzhkYuByI z0D!P4wg_M7=p)OnSrQykxL_4B1l;6S^pB(#Eox1yD+lW(JkoqZgv|s1NV1~SkhC3R z|9Yb>WhIsE{r`N&xQ0UjzA+OVN_j4rSnicXgZv1KZW9#BS&mTyt?; z+Bl@glz!t?$8@_pc&7Gu+&l6(6+?2j?a~=++v6g<)YS_O>>V+ydBDj z3H;XVGHPot>NK331E6otsEs^cQ%r=s!Px zrI&Yk@4hhr}7r zkdygmUM}6GN_v+VH0DU?nB%P3Z`1yTLDSt1i#OOvShYoUQ&R7biw24=kkxb@yd$TJ z_GREN`JF-*MoLcXhaKtaXhbbn4OSRZypp0Zw+ z2f~0v4z;fo?>r6QCkrl4$I%MVVPAD$WX8!rLlsvuKR4#x(WGM>b8;|CtL)+edr1Az`W`J=I)aBwn z-<*!+CTZUcv*&3P(by-rgZFh4X+5at3AMoU$7^mCGNw2O{I z$o&Ezc!v4UD;=Q{>o~dOPTWkY0|LU&crVNq#awu?TpL;AF4BJy049zr3iu0hegt76 zyAcryktC@Vy!bJ;XMg&Exn|48jT?nEJrIQ;yekFg1RJ}~@BTaFu%v{XSaJ>jQ07O% zj73>vm%+8c*?oeA3U}NR_I1tixj~beO}Jd#{&aLp`!9TyV<7KI@$rMR7H1HvWcJRw zLg7}68W}^Ip|7@k)k>=pOcK+lK8dCG|2Ubhm;8-Q$A3lW-CXN75t11rgybB&ni)A` zjwmUGw+^A(1)ID1?}eO^Gp`I0+wa|}kypF$)cA7zO2@+1*OMG|c{0tB(^lp`LVIb} z+FE#kDo@@lNJ&ZYYn5TV{PsZa3Ur0KAmHd1=R*&^4cWph!*}S7EKkUZhi%4o!g)y# zo6=Roll+(>S(UW4l*7+i?az+iUT|#1@$XjHzd$LNLG{%PgpM2)g1o%(uZrkb=kq7w zAt+lp{sildj7nGF%o3>!JE1qyyqiT=J#Xw!Q}bML8>8=G*-7uNrzXIQjE?D*I;G-d zc#FM%T}|!5i+jK<1rN2J4J?48PI_QmH_uELFpA*Tt%-+=($Z%z=D~`L51BT@C*WcK zdpG_GRnsg~@k)=)tIRNN&P(VI)c@Sv_`C81*K0!aAh}#gm#eOpYe|S#=0Y$SRPmKr1J~NdHZYk{NkWc4vOS_*d^9D*f>eci0$xMIL?LjW`W(Itlaoec3 z%w@Rj3RR5qjT9{vOR8@vTZWqdIN5uvz@YQh&2=`|^N1>uwA`3ekkM+in&RNtctX4l zgt1OsOuqMm7yF;VrnK*t6~c*CIb_xZ`phS72&zAaqu%_DF$)y^0$7MqYCB~UdJ_b9#5>(_-@Q^-9l6K$ zi{c3e53#MP921}E2UQ7J)zcv-u;tDMA}ejx232|2A1i12RVGS!V?cR4QMU>yMNoo3Bpwz$af$U4?66K*_CoD#~jF8f5g!-JJu5X((H2 z@wE0%X@(0MTI1hzYnrq@k?dhumIENu1>0piiGpL$!A<4kKF545#nyoblBH5BIqQx> zp|n?ivW7otfOtnhIK0g<2yB^ngZpAa!X&3%AlIzTd^+zwi=rVN*yd;jSn+yNT7uDD z@oR%&&=A`l;bv?E({U^B5bN4o>1hKUW85VdgKzCyEvSnn1v)>i)1np!OrZ{>G{*F-{1HvKaGK5jnQ3} zoC#!q*2eIXX(Q3|cdzVwUK=gZNK|fw)0Rd(zo_NC1LKSQa?3;Wvpoxi5XCYYkTDgu zu_Wu81Vzn)DC$}OE*f1Bz@4?|W@!PG*MOW)lZ+iJP0OCAKVEZ8VO&dV2*0MmvjHHU zd1O!yV>h-~>Op0@-HXPN9J^f#V!jc`qH0U+5J4rpW_`N$lu%LUXT^HPGDa4rn~KeY z30@dQ;PUDprQ-LFkv@op^i?$oJ%x?oOt+5xzb9k3_NTYEuu8&75VGwm-XiIooF%?K z8Y&Uen>Zi6%k?60<3yveyf~BL2#r-EPATFD2?Mt35kzFIP|F+{X9J?J#Dz)^o%VW@ z;h(IJf)g13Q^eu`5*DLz}9$;tDlv6cT5wUOQfjRH@D*lkgQ-Lm)H-RK5+Hl}+W5 zt+WrV)aD=tDmvN9DLp*YO5ZN2Sl%-qQQBWL(;K$*f!)sb;d;l&lF588i}i2Q`AqGt zUi~40OTJ6ax5D~8m$MeR$Rs!2ijoNwIIc~dm*ZrZ@`{`7$Yi+qDcCXAxzAR-=p zlVZ-d z2P>22>$7`kAAV@KNizoJ2eMwWg!C?8asi9UXZFK!8){wU{&R1F(&u(!dc)|1<8xK)17#IxS>c~_Ngqt|3cRe4OhZBe9dnIZL^h8M5}uk4&4ivSB5IDvunPioX)={v6eUV zY@;Jle##q573~kRWu9M>AYw@y|Iy95hWQnT)AF#U2_nKz=d9bE7DcN{{aNH?T9P71A97y5C+ZePQ79RGYq*qIk2qct}Qvos8Sp5jO- z$xM?Au)P+qvaQbB2};h=mPV#MVh=?>St=+f@D_1nd_#hdT8!|qunz+7MOK|FopI8V zsaqFgAvI-JRIHrSw`14ixi@UQHMcaXx0y1bRWB~tJ07`giQHhko7E1r2iB0o=U~@s zsJr@jMrUywTH7>tD#+fJw?ATco=k2O^?odJR?A3rgez2*MR8FKRefmygE}b}b}>F0 z=4DLM`X+SBqL^fplMtAB*op!>Fgjy|xchrtXQ!dA=cwD7G@br2pnpnzDyW}<`P@K# zys*Bq<8Z)+y)Otlz@XM`Q!n%0F~ufD{??}Kl$Q6Mhulw?Znluot7}PeIZNbP662`7 z`y@6H1?e@Vhg6BUV@l3HGSHLARmvw;!TZDM%A0lD%Z;R_&C!wjCp)12h`GXpV>8{v z;mXNWo$5KQLKR(_Wwp>QWq6cM%r>#;YKQCyZ8?!Rv7Bj<>1~<;z=fL8ybRh>L|l*xm36L`U+#Z1*}C9NJ8NRX;F2EN@3^Q^&EMd} z@3h=4kxuL&)Q<_I^~nG3h%S_p*2m$rM!<573RgS^0Avs}-zwntc)DneY>8n};>BsZ z;;d^ErP@k^GD{(&chWry=E5q#^pni=mSYybD}>VyEd5lq_w+)z>!qbHb_+4gbF)7* z)cQSii^te>t&>KI7J5WEi?_F!w}(15@LjAkj{oP!rV+Je`zRsl=qBE+JpExhtACS* z#6eRwoE~u(QG)kw+HY+R&c-qMpO66b6FyhhMGzL)_Jw2U)02bSQ3F%QNw#GfB3upo zANFsI+;fH@A_^;|(l3s>u?V;Nf*pC|!|#2Ept^M$oZA%B4IRt{yS0=MePhqblf~lX z-3)k%^TfyD$n!XygC*5%NPvN>2QP$8_Wr7TuP#LVLN%}Vhpdw%R*^#MO&6- zd}5&Tm@mxFLHEK2TxwawmcF=k)!r{GOquw3$eHU3^|Q~L7A5`BY*epe1LuZbI8>br{bz-c5*wJ{v}{Zz!4 zbxp$zd1qjqX*Qo<-OM~^0H9>})jBmN$K{LqDuDv%=}HB<xr-*Dw~z1^CUSA zs8QRfK6cV(FmjhgzP%J{d2H0;aW%B;Y+TDv*_}08PiBAD&ZShvJ^AeTA2idQpqaJK z|H$*=YC@LNzNo_77SjYA#ksH7vN?U@XdQ#{o!oXYJJK|FmJqjI8=A>)42KXx69dTs z|HobevY~`hduA#SmGNAo;>pIzPmiPH;^d%4n(cbp>Bx5pBqO2o=@Fnnvgi7Hklm89@9=T#1<9(n0FS+fHQvN1znIK4LB+ zDsE!8@d+%!!!bgp^VOW1*aSCKIZg-Ng;*dYQq!!yBs0|zBb8hbrT$g-{qu!r!N3nQ zNq6K%*7aBSZV1u2wCaDmZJt(@8tq0F5$*}!Gw<|y)dC>gE0(5t|U5`|MKfk&R!Hi7?Q%|z6>`PDI5ea_rJSrxkWw$l; z=z5;8-NvJ|tFuIqAA|5CVe@_HRHr3N%5@l1R51^$D7K*I=K_$^H(ig?kBseKg<_lW z4*XoZ$fU%Yx0o!W|El&ROg);&BR6<9)AAa#&TNgC@AJP&@zKs6X_=M5P%mrrn2W+u zRK6p4z6X@DU0%(O0&%*S!bXfvS7aVB+@1e=&TYiEVg9JH@)o{Vd-;lfasKPR3Fltq z9q3$cM5T9kRLfMyQ!$yFVzL5`G04lg&@OC5Z$;v(h4Xe&mwEj)_0(1N$!-o8f1}Q+ zD>;+gGSz+SH|IR-n|hl*T4+h;*Mf^bOlA?aWJ4gNxNW4KB(*XLb|p!hDE8|JG)W%w zyeVfGmOAT>*JM`wwhV7dR@q>;KO`N4Pfk;IeHbrOBQq-q-rgnD`r9pL=Vs5J!7Ot8 zzxc>L!?OEH{R7V_-J;D79g6*aQb%LYJIm$BzPRz<7ABJyf2ND~WN{b@>SOPtuGZ3id&k$n==Z%2OF%5evWjk7VfB!{+gSi-<5kO zM#@p}_-qfS(ngP|F9wBY4jtw?YuA5w?2lx(&s)?EpS=Wll2oF}m^jR#916JE@1?O( zA`8$=M`-yU)B&B*RN^~iaazC*MxHdEtc>App@u9jnj4=`H6Pwrt~NwSrCVm&XebEe7~UvTa;I2l~?RF4~;S>9^{oTJLzgeSb zd=#anxwY@5?rUa0*2qLf-HjC=7G=#8{+=2c>VMC#T7Bq-UO{(xUKhK5-oshla&Pan z&+M&!CzKsz*Lh6l-rDR1#<+IDGErx=y(w~5=(h0T3&$sR@RwDFS4cgXO@io~XoGjH z2xXbU0W@-*!#0})kgnxs_PK~WHVhi zBa_7-Kr{iL;{><#sBB?0fe19{O?S5ry;kBO1f%wGIK zS)TJ`G!Wp6ue8^xo!zG4hW%5=p;=S-ZG^+*`vM(AiqmSv{7E7L#YKrj{>L9YBj(dU zDoUcTl>)k8SXfP1K1dQ0SQJ>5HtzLi9>2I)S|aRZ)~M~}SKD@!>OV6L-KDQG(Jvvw z80xRN25E@);=38mS-&$iO&=vkFVZlo+3skT$e1p5`&zU7a7X-fZ&v^$>8DVNc8;N8 zu{BEUEL4xU8+n`W{`*9wGJPHawMS9mMqhz?I#2vrlIRdayQmSDlRdG1ChU%8VX)on z_vnj<`W;QY4&X@se26|T_aC+78$`UNm}~W51>-f!IdOFS9#Gs2LwVyGyWVsqL6}`I zG*j2{=`-dljZgMRUgZa9H)s5G7hlE-o)gFSF0pwbN6L`}>2W{(gxV7O2s z#h<~6c|#ZOX>*pbY`9ob2b5X1WCAB?STmg>o15mKyIEbDK?cm-E2CH+9*k{I9 z)DqvRFyBM#RA3>k{^H%{bP3+^F|P;v&IhpEx7h3rov3h>Irh5kKW(v4X^%c<(ACJS zmlZ?Eww#%$ACvO!&Tvy2`z$Nd$H!@zxdbU8zPnr%RuxY~h%hm&ugAPN!`Gq8&)xFu ze|nD`-(7bzacui;)3^DUY}=LfF(B9Y0n!1|`Bk*Q36i9AHXlL1vTY>8Q>8HqlA z4-Uz%WxP)YvT-L?DPAEA9&tzWSIFhZj>5s0zK|vw-LI$79N2yDZ8ef81*|f87(+=& zF?e8y2pjhWVy4_aiH||OL?Qm@Xq|Zr$#9L8h@K!xSoO#rE1ish9L@YfORf zwat5jgeudwWj&`6v~z>$#&WnrM%h*goDAnMH5W)!Vi?)Hke>WRxt4$88_FGl4~5u? z6U&I3@qgRL0cdw|%u@ZZ<{ ztsd;*&(9?}Vwf;W)@fK!&}ryaKOUw3E$W^7CG7aO?MA}3zE(i`JJgl9 zO!t}9zL|8361DhT(UFFrd*{aa@kz|KDVYT5|7EHMtNPIVJU&a1)Ze?M{q2JEVA4H< zWxY^0pHWv=AGW8QX+f}A8L91n9ldS&P0SO7F>@i!K2K5Z=2-V{{3HlGuUWUyI>LFZ zyNmRV#JNSdPzcz+F>DJ~=y{H;(EM_=Kci9-LEs8YMiy}J60zW@=jsi6o|$^|_u~T; zz%hHi>*t0n%#eIW43$~sQ;{9sBm>i+ImL*;_1hsUz&{jhu`s4k`nACz)3t!A)&>vN zd9}rU8we2#FOnW0=!t@Pu)XGlLMB52Ok4SC241l2&PR014yzba)tVm0pwLN7B_47f zsOQzE!p$d>`*M|3?PKkXZw#W{Q4h+t@kMdl5R$GjL^*01V=bwBkceyys5zkUxD1H z$;2u0sDJSo<#G0V+!>_!EbU$%(63?XxaijLZjVBX;a2PE z$khusFmV zkcHJEAVE&FCzzHIx-Ik>nI4WV#9dhPXa@<}IE6cNRYuYDlc-uy^9(crbj5k{S4=^P zPxnlsgk^zTXa)Lydu|RN_2UBFkt}y~hpPE|n|!MM zuk0h%JfdvQw2oGeHFRk+MJdB5-sy2nB_D3}vZ$ZKZH;x09-a=mqCk6M0YahhYm+Nv zWwTw==TZC%g$N^mwy3TFQvLAM5Oz~pQUuy|Al{pvPCBtri#AWvWq8APdjXcOWm#Iv zY7vK!>-eBbwm4ZL+({N6Q+XA=lw*zPPo|!KsJxU5LyT>`R#5tGUpd$9{`gOsT6{+jw-72io^;ihNi94T36^g)a>uG z#z$j<>*0zR#5}M+sUIt4OdON+{Aw|W(bc6Q_5z!~*7*jLr1TxC(dL9C(>Lzxg~_Wc zUGA>(V+(FxOxvqIQK#fYtQ9H`Pd`s0eo2?H?wFyBxCk{UZbr*3Io6+U9Bb!31|uS9 z?P51yt??5>Dv_eqrW>6H8@7L(;LlF}7&(sgS)#>T|qwjG58e^ zUoHf-7P7ijMk;$(-IUF-0z<2*(%9cm7dQj?4u}xWm3mr$Kk3*TG4JcNR|R%G4^HEQ z^(EXEc9{5h(e1dAyVRy9re9f|{$W%jHG@8EG%S~#M%tS~Pd5IMGNPrpp#|u2Ow)>1 zE|dno;({%^tPkil8J*gOj&Q7hEq1)fD7B+c!;@g}Xv2Vw5j=POXl0!GdLF99j*d1) zABD&yQOvHplhK`EUWhurLo^S%f$9;n!8WTuD9J9q}kSUn1YJRXnS@&3{*cr@|;HI zzalQ?!=|@n7XCPj>lrz!rkvx1suv~FNMR`Nm(6QvI~uL$ zi??fMSgcQL+EYnM2fPI2OmDLmW$FC9@KQ~+$op&|~@ zo`Nfi2Nf=~ngWKzaBIf;4@K{92H$bUNk)1qr=ANnG=iEly(YQbL?i*7#a{4OpXp;Ak0Ml4Um@<4t*?_T#_OI1=RD+e%w;I?#-vwKb~akNxq*%dRKXL7MWZxmPdy0{x;x93jrjC9DGxDi?yZ?n|w4{_y0u|PHW4>zRO__)xz0nf(y+fiqaIx?l6i>9NS?tGl| zYOOCsaoU>j7+RKvF&2O*DDBD7WPrO8^8PEG}ZjY`+4 z@9CLAd^bc_`}MDaQzeF&ypX8Q3NXyFSeyVE6bk#Vn_0^HsrK|jo%)tc3M1lwVtger z4123MUvkpdAtmJRBrw{>x|i|8aY!D8P@(QWh;*FqVSV4UmX?j1B@;+JQ0B=H$dKQ@b38EV0(k5=X=R z!T*U_B1s;kVqVqZffr$VgwYdG5{?f!_%t!jDV*!D9C`R4S=eP#Q^f^1QzG{gWILZ= z`Z0)ZL8*KWgE-zWGgXs$yF|oEfV#s)pqqI#hT98jR%WZqrJ$H0iMiJyHT3qy7>hTa^vqAn*h_Zp-`EqkoDqk#LB{OC~!nyV4uT}1>3gN6=>JN@(W z)NU!fWWa+-QDH1g2y`;K0Cv|=7XK4<*08I{c}SbA14KpnynE!+0FuJtLuYt?zNqV= znUSNK)m(E+4tPIvU8Br0H_|Y90^4qsoqC%*_TxfyYp#oF4Pt9rzLB@0Ucahp|kf>$Q{b1lJaHL{NaR{@~i; zcQ9I(@KVH~POu)^Ju2|_5;^Rs*ylPO6_{|+B#d1SN+CKAU~CrB^rS+F_TxKFbTA83E5E$WmwA408$fIcN!>M zj>e-x_v1s@((Fn4@ypv58ZO|kn{GdJ6idwvtCe^`%l>^n-S6_AWQGziFnf{;69M+j25|c_aaB4q|_`uDV!p zK6uLyMg3P8(y2?Ao=lGQ^dOuu$KdNapdEZCM86+tIn2z|gowt_Hc(Iw^lgwQdvRH9 zVeLNs@vyevZDtV`X7BCI_@!(0{`!ZFL(7+eC}d_P00xGyO3k8_w?@0fwtl@SVvq7k zCFo!W9TDG8M?}I&_Y7J34wI;vIS3*s!PeLcGYv+O@|KQp0O3QD`}dnvAsd7`{^sb- zCqDJLu617CowbhH!x@Rs>f7yVo9p%rT`1Shi(hgb7k!3t^uz(2(B%kHB)Z{tP}+Hx z?X$N70t(5bhPAdtVNNQPxHW9T2IlK_s9b2PCwdr;OE_mPNeSR>iYYOF)z;0`qO)^* zVX9hXwn|-j6~$-mI>Vq)5(h(|wqe-I3tqiC>;b#E!D(~!8UQk6h|HLb@$Q-iqfW?{ z&eF&wZopA0r-ktWX(`wl^l)^Ov1_CVk;}dl62ZQ?R!)7#Ejz_qqsCL+&nKxObn_O( zHUAo0!Jcl}r#XPoyCR5`o|`W4eOz}G=Gm?hH0Fiy-qJ__VAhlITLc_&sooy$Ye|j@ znqVDAKGGeD{-~x<289BBKRe7VqOnC&@!N9=-|@3^=~tm1^m(&_86(`DJewQ}Rd<4g zv~|6XfqK0S?h2pILGD0LD5ivm^q}{*15vpCWjUUh5dS+NUsjp@xV)@*i*Lj9y!UHu z=-z(IxS(g+o+9_pg9H`x+H-I4v7Zy!1W_+RsF9BowJlEmD#4S2ILL)qZ_gVj{=;}Z z-$-!rX}sUDYk}kVC7x}hT8r9q<_cu=9jl~<_efz%m!Pn4KsgIV&KBR43f4Kl^s2Xm zA>kosS7qWZDXTaIb8~XpfSfJq7SPGGMo*thL`^7`*h+=SQ*@}Ip@Gz##I(i$YJzp! z*6p%yj`B<3i4nRUb)@foUs9jYTbW|)B79u(@dy@Zlyn;Sct!TxtNKp)Qu3U1wO|x4 zW;lj$p)bZ^a)4k^B)l`e!yP+`h9ztOg}Cq29@t5s2NHI4#HaOPFiZNfY$N~nodPt$ zl0+B@uPnRnHpiB?tx~hZDt10oDV_T*T1p72zj636n#%I6f0a+E^fDcEQ1GWte~Q-P=`imA)m( zbsndhS`K&Nd1P@phS{Rh3?jHI?B@QqRoEc^<1! zqn%|GGr52@bIQ`vQr&1R9$BsSBD+|~JF7ud+TsA(Z(h%n1{o#Un0y+r7~ed6oAb-^ zQ#W>Ph0o6(h<22oek3&ORFDx{R(Vi8f~{*$qh(X7snBon!oRMO*a@z%=D475-2m_= zT#$jNM{q|W!uIR;>vI^#@bL5VA8|to;G;2!YQwcf0nd3JfQNg$cy?2Qg0WR9^YG;V z>kWwzmymc{d+$>Y5!_ziT%`A}JLI3u8w50{10VHCpL6U>9Y^la<3T=w}8xd zEfmd(khJ68w8a#c(A~d4vy1n;*gt|7zGS&)JSQ@mgU_J&u#Tv(0SVV*c z89hDSlxf)Edmoc>0h zcQ@l@#0#4vd4LytA%$k6JcFQ45)d-#o`U0{IsVWMKUEHTM@e~}Joau6l+ojdhD?*p zPHo%eAQ>nr^Wl>^^b$Hdy+Qbo&Jz$!gZ~>VJ^|lq5|RZ@seowl2~96Goc;1YDE6(& zzgC8oQ+EPx=ic|dpUc78a7h6+jcfpyt%M*{PP|Aowl2f1XJDjQS?PlB`tTFvY?jK^ zlav73Abx)f zL>)%w?)KeJ`CofLuuRs4UA56!5js9KZ@0A_c^izaOjkLox z2ZAh8Jv42qD!8=Mv`?g+DpyDyAlwZ3X z>|TQmv?C2KByeqaw;%81JjBcd=qp+4NEI&QkJ~|RSO0xB?V@SOpCEXk66iGHv*`jcTQ)ta%Bl-uCD_PE57cG%T-Q9qSSV0gO)cGq4fB;Vf^VlylR9E1q16< zh^iz!#~j?MeSj$uis(Od#(&vT z2d99cTnaD)(#bL?P?QUZirsXnMW)W~lTN={p3a*Bhb+%9Dwj;}Ae&>{igV0FFir%H z9Z>riba^&?3dfRcgW%e5XAQvTb&qWzTV{%Ye}VWuM=-)PJ0*p0O3+7R1UiQTBAWnt zgQ3)1J5jGY_PAXp07qR$zZF*O=vPR@NKzBqG3Z4Nn{MG;y6rEdagUAjs>bbiBp3L< z?5OTb48p&;m&e`8PZr1-hx)k zA||3gP7srX-M#zS7K=Z{sc7l9nThU4@*dCsC#txn+k3$iQ>|5*itsq%D8?+P_-BFq z4!@MQFDKF*GJ@XkFeuhSVquCU3=u?vrqwuq;Yr~=?NL{D@IIm3zO9~*P{4UyNDz7~ zxwFChw>V;Xz+mVsa_zvponw%60CF`xE9YBt$1EBF|jJ(X=Cs7xK-$Sq#%M?VsjO25RrA(Og$L- z4Fb4>pB)io{9iG{;cSaqlA-xZ8Mu-T0ou+EP?Icwwh%a;?vFe6W);EXyrG!VYrU+J=W`Sdwh;+#>G*AK@m?x=A~yKRBZ77a)CuIi|#bX zdFuiaLLZ$a5zE56GeM;nmYJ}lb`DIT3TyTIXX_Q!>{S#~d8`7`P#5C-vYsZwVci;1 zFvQ8p^PZ3^z3@pPd@`{I8CR!tcB>^XW&LA;Fl<5%Vt(eCozWMr_;X1$Gx}<^kjUw? z&@lR#6q&zINH5u^C7?QS4l@lRq*uqONcG;?2Vc)iBgj|MXSGyqJB#f{%YvRkg+cLA zsN?^Dmfr(+Wduc)bA$qU9#~k32S7*-gahsco$bYW9`|}5Hobo$_mO-vVsFHxyBJc$ zo3acprI^NuGkHO{(T>B(F!_3mkw+b75me`s`;gWhZ0$C8#SUGqP$MX zGe72rgl2GZ8OAY}P^de!5W+$>4Dt>XA31`S`e@Yj{wQ|98EsEYz9oWx>nO?+MFNC} zNS3*d6(aJpY~~7?iZ}|_rt*J~N}dFDeLU?U0_5}|1_%OjrJr7d4V3)xc@m6>dy4Wk z&_7W!o~3oz_W|{uLtD-?y@Pz2_}!7XdZ_xzdVn8F^PoO{;p2IN)u1>a1+o}?!=ZPu zjs<2>nBq^|fto71lMx1|GEB)C2DZ3+;3Zy;c^i!A#GApsMiR}-aC+X`Ej)NV{GqOu zk0&u>@HdVi^(PzKQ!DQcP7{RK-Vi#K9k(Y7UqVpYz<@akW3>UhPb{X>-t=Fluo_(U zI4O+5aLYwn7v>lgo`1(pc5-6pC06RzV*6feyz-G&K+Du_ zLEYR}hKI~4w`~8u4wHY-2V=78NqiB;9c-D!6^uYe<8V2FNC4|(@!5mIfqu@yuuUZQ zh1iM7f=@?i>M&X1_>R`xELcNQadwS0Af_2iehm5p*$=yMKWX;?EE3_Q;c`9Gt*b%d z(~(NFOI8o>Y;oy zZ2NoFl*31&1gBV`+H zLhoFXXWf>YF4#*fZKWRg$B!7YUT6_luZ&L|q&xX{Nz~V^&LnFa50BQ`A8~7zj!dFW~_>|n6<2B(=Y{EL9 z3qE+Rh@a~?RkYwf@|`Xzj@Wm;9k|CiEGXw`BucJbLoWWp#?)nB*U$nd^XO>FxW5W7 zsu>5eliXalvmKD95b~-Zv92~ku_)KwFwGq;#V1QZohbn%7}Kdc-w)VzM&Rfzz=F^t z4;XNMfDpz!s1OH@1In+KUfJimtH4@uT%q|H1)OB#6`^vkXVz5lhMp67Yp#mS* z?*eyV)m%Cxjl{Z-xY9G&pfNTBw?8ib=5bN0y`6#rfx^3l?ChuhzU-1~1e%1l=!Os; zUE)CzkIxV9Uq$V&N1;Nm7I|I}XA(Voz|A)a+zY-8{|j~`^hP-`g^w&Bu1ps04GnW# znX<(Xno)(#XeTi&AX3YPOBOPbt2pUvh&9hlKT~1}{QU*=PR)Tv1OOjQmy=@{6DX{a zPw?+s=KeRFD|7h)_p;-!XO_DEFT&nC9P7USAAcL^B$bMUtcFljRw7v~D?3?H*(;f) zfug!vB%_lG**mk4v=JeDq*V536GFeoOV@RO?)&q(kKgw^j{A@MI>LFr&)0Z9AM5$U z@aH<;2`kDm*3{+sO@=i%<}jq;3fs5@Pd^E9zr2Elio2|TybZysjSFd8(OHiKE!*o( zmLH{(%@A+bo>*MVA1-LN@syw<4<6e>w&UIyhTVlH zJ$Mh8GsJ9Rz;54Qv?vLr--pWXpOp$O;PuhT1?-MxZ5P+(8e^?!VJ3-VqWeJ{`}uky zs5ahT1BXqlmE^j_Q4ZPSZFPn?iwPk2Z(Pv|WPb}}{D7`HSADUP%$pdNxHZSN&F|0G zpOmt_CwgPab?fC(4!dN-pEPdQ9z0ky9`m48XKP>g6rRfTT&G#2viALh>-rgovH5q% z&2+uJe>8!x3PdshQJR-BLtG}QLP#L$IM|pDGV$qW{Ki5@obGnGKD^#tapGnWJ-Y}3 zaH%?%D)agM<@)biy%REw$;YwK)qv)GmeR<{PLqRcoW5iJnt<(MPdX#f- zd=pwG(VmV%PkR^2x=u1|N)dr1PiK}>K~h2mBbGSE>jVn)toO7_u-SRtf1BB^$EY40 z&+vjR-TS^||Mpt5byMBjTl{Qn87K}O)p+ofT<;=r>D}F*Z+AuKru=IH;bSp+^T$(q z0yl}85N7BS6#(w5Wy;3m>fULSmW6BeBz{n&dgi?W^Pm1?JH}3 zTqAsD`Mi`38Fuz`<0Gy`J+Ap<`6u&ouWr2|yh9FW*flPa<70b%*CXJbbp*#H3<(jm zVnwqVi7%%#^8u`yVl4*>By_)qu-Xxh5c^zL?trDqbGOgEJi9x+Y<6pWH(kovXJBqJ z&Zy3o?kHGtJZ7B!e4ou*5iDpey#Y9IE4_;qA_)Ent)aX3J=rJtB6gfZK~~amcZh?3 z+p)VimewPEj#`@@#}Kclv|J@f-(3wz;rTrCg6(X&Orf-N+^Y&_)d-pwbJiL=`qS^X z9@6Qh5>YYM%N1wXVkIoyPtr51DP=?a<%6!Hq^cvyb`ll_JT)ax<>s z7PG=avy6r*=Zr#SE^*B+EzYCd9B=fOa2k!v9%L0}ZI_n*-G5VNnstqhT4ej%oPPSc zsnH3#y*W|df3lq7NW6JvvUe_!W=9E73)k+Hsa)*eE~HjpymFb%+&8@46_&mUs!vd^-fHOv)$cJ!QT@V>Sy0m z%*7Y(r|{Kkz`ps^k2C*N+69@gG;s_wQD z84N1}j}Y8Y^idkKqKTLBEk3KnkW42W%N$o#<1qqc2U_cF&oa}W=+pS8j$`zD2j2hT z+dyPw^n84Za1y2vLCEqw<>(ol*!w8TX!BSzHbWZli$NShEhI)ruS;<(-}*fuqNl^p49&3f0@2>-enAN0r}|g>kZ&v;E!u?+uN}kD<1n2 z7ZxUeoRNx4g1|?oZu_sy8Fm3ormI!{eX`*>g9^VkOOVy{2X3ED=C1h{PS1Xi?c`B= z0jix4VgqQJZBH;hu6-ZcaBk08w{*o#Hj#^?Et_i#aQV{=mf*Vu6Q^1JtLwAZ>z&+# z_R7ELL}n3nr0uRM$$W;+-6-K)Jro|Ya8SSa$xBfai6jr5u+_8`sibaV5HvIwbq(#2 zye_Vlc%dzwS53rrr8F)pmHYF9S1vj)aIBl*)orED<=Jl`*0OwC=xlP!yTueW?Pz=v zs@S4)s7f3J4&;~+E<3jBoY*-xgEB_yq$heBXI%H^hjQrj$YweRnOSCTIPmWG>^;jq zpVBd;@C<-?vj5H)|BU(comqcgkPZQQjj3^ZvdgA1>p?5}gDzUVsen*+Hm>E% zJ51G%A3r|+VPFrcKc|Z+c-w_-V0d*tI&D9%3ym@1Z))r3Q8I6UN!kmxx3|km31g7e z!(EEkZ#Qlr*j&I}R<_>0z9IkFe*Yi1Lpp!3o?~UB7kHfaw<2l?nZ<^~PZHhu0Vv1U z9i$$8l~#>g&7_K-q&QksVhfpy7!07{*Fk;#>lu%}c*OXk3c zj-e4BIEC;R^czG%LSKxlH09mQCtuRcoLj{IvIyTl#c^6S9_6PwQ7z8?NKuxx41xA= zGa&lirlzJoKJ~h%zb#w4W(@}iV}J-0T4suEuReYq!2HM8NHs7rHFa!-;K>Y;&3gL! z*KT9DOA%N|D&WfkHu|befW6KoZ}EQ>=mnWX@gDr@)xv7%_}8PbkYJ&7dLqr;^ffgv zl2DhJi}|?nWLsj))QMNIlaC6pd-zGr{e)59*Yvhg{hQTk4k_vylIzm)7~)vv`My^J z%}A4Q>9SZPuq_q*mKGowL!_Z7eW{Dt@;TLq#F1P2_9TEjdewq=WoB^vcrmo>-o$z7 zqX&{mS_Xn#$%|ARD~TPMYOgzO*XYr*FE$IUQb)1py3UmpM?FKtwC26>a1$pjjp7-n z({j~kRSzbauV2JD+_JFLyDuun`%`+>c*f-Mou_jx7gJw8YRAJDxd+64zG@8FK(PBo zR`)8TcR{Ok%)-B1;mEz>30~ZF;B(FFJJH1w^iVc(?Yeb>t=}0b4hP~kBvRm)m6dl_ zHy=KHc&2MDMcZn(Jel=D>+_}+Qmw$~nO7~_WNN%W-QKs~`FY?f<6}vC&WF>N=FkVL zc*8gno?S@z+&RzScWmdaoUNT6y<1(1CzT_6xExInT(k4lXe{ulVwmx#RQAjdM&rHs z)Nc?RD+}LVR@=0CRtNs}-%shi zi;q+rf1Yr#6=_wvz(mzwR*eS{nZA`CVY(b+N_Nejn1>zd#pU#v|MPN)!C(ocI>B7(;S>7 zxP&-GFJ8Ljx~cM{D>x1ekXcGZ6}Se!_=3c62z~R$LeXFbgvAgV$XgOSN>&A}@?=W* zV4d`)SHL*3gloyiu=Ov~DuXTrlJM8qay&a|I>C>tneu+P{(^|E~TrY4&toXtZ(L=>ZU{7>}`##)% zIJ&xF&RFTd}7n_M>C&=XNtliF}e|bwADxZ z>qCzd+a||y{IhU24C)P?Urm4X8f=K+u)_E98`nB$x_!I$96T7$&t+z!x1@XQY_eU$ zQsVtY%~kj~4(ya8`I(n-F0&{iedFNIk&Xl<&!4Horu&N$B}+`?^V->QwPP8e4A_H zmq$;Y+{O^0vaH`jyg|RYd}@cn%8T9#uUL-!mCc}1>DUzU7HGBi3m$t193Pq(jlgXO z0li$;6`q4|%Q}}Q9U)|iH%O$8*Dc=jg0&gi;k#S`|52+Wb&U zF=?TW>+6TXrPjx0jHLUAuN2}){y0@8X;~sORm?!CzG&hw`|Z-j5btC+Gd0hkLr5(B zYpn4rZavlwulJspGW<}uCTEsMceUXMmlJ%cOq9(2gzO)u$1<$iRqBzAamZN!36T46 zzCYNDQL!_hf6wMfJaM{c^Xn{+t?L&3`lYO9DpY=Y zQ7h++w6s)6%IOFmYCc7i{v;)4AAH2sGt#7RaZi9|fQH9bS!pHH>TegXn(|cz<=Z8$ z>l}}W2;B0Kl~UU-`U5Jf&UQIKrOKQqH>ZhcZ+2O>Ny^ck45TpMP?|(9;06kAh+4>O z8c!!RgmvlxoM1(>x6<@CAN$fBP|mPGW}a{H?yXi6k$1M(+h)5imWcwjn7&J|GV#f2 zFhef8f`HGZgNyN*1XGxT;DI)w2GGoPix>jjjZ7_!6)TayVfz>3*0`k(HT_>hgI07k zt<~`?>TQ|wzj2`MQya80G1=Run_YuC0jeS2SArZW|T(F3NhsuEi_ zZ)v%@VQLB>)LWKR5FD+Ub&Dw#b$TEt?)i><`w$?d=}?X@;ZSQ!-QZnqK4_l4xUtsq z#btJxSlB=EUex(dJQW-n3)T=jWg|WaO^G>(w3k&?2cs~`;NperH?KXMcMid``wPSM zSX0;Rx-U~^nK$)px3lWrhmnG!d17C+Un)vJtzs_F{;i`(1;n0F6NwrJ9GE_*_Pw|;9^GWJ!d~~m+Kuka2wT2x|U?|4gv;N z>%1&^eu=v?pdOVS-`F(ni=;VGd7Q+?jrX>2zoOXeslFd9(ONq$Yh9(C*9{c5PgKu# zkeqt$G5a;dzpW#&uz9&G4=uZ7NIXX57ld2(_fWhZ-KjN5Z^&Ldc27spwE5vYgiJpF zu~B;KUfd6)AbIvNS2lF+1BvM|nNb{as__Jyz3TN46K)q>DJX^Y)-~=$;(;45wGk=R zF7GdpoaPPkr4w)X_(2Q!<;e~oseoUnJoi|wqb~z2*U|p@jhh2P>P$&c&?dN$^ z4%)FYynn&fYczFN#@4?i=WWsGxuo`S6|JgQuL|2ah7PsR<*0y7ob)wg(PFk6uRo~R z)6nOxS!QQcc;sOGQoYSH+Kaw)b5TLAdILqBZjC?)v-9$#%y$of$+|1;N0L{}<9tvN zMN8Z7O;EUxcFV8xM)xv`3Vg{PxhZWg3QFh3Y$%eN6ECT^UDUpy_ad?O*I2sNWZhnUe3(XC zNE#+xh82wBsPuFN?FE{()9<51dO|c$#z()QCRK;5Vya5_^r87WJ9h2LbL1$hxpJ}T zp$4yfMoLRfm);5MO0k7Eo2!c!L@vpqEfnz{Lx)C+IzAx)*in`bFC&MB%6F{Qz%)}jB zI0k4Gc+nd!q%nlv`LMeAl-lmKzPe2>c6LVOOSQJFkdh73ekm`lUdbG!tr)i!(6cV= z^RLm0@8+xDQsMqI@z-?kM*lS2%{u{fk8mPISET!}(}J}>uaAF<2)mPbBj``#gq1`w zTqX3N&IqcCqP_!r_g+=-{geqt*$SDeF81Ai4vetDo}74bul}1X~P;RbSr0Qs>1kOZFn%QDugjHMEuFkz3Vzx zqz1E2?^r9vH>bD>hrPw~qD6u=_S~3Vdf&|zS3_%MV-S9?j`{ii~2l*ZH zUAJ5y<_0KDUf(%2k8}>nDpTN(cn<>N@B>q6jOJ~@wG(9roZ=~!P>dnV-9G(Aos1DH z0Y*5QSqJ(HdJG*R?yUIn;K4S9E7faYuq;esVf7j$3akYEjXZk z9YjJ|my&a;XfJ8>QUD3&UBTm}g6#Dwdm0}8XwVu1IM>X}o-Qe*4SJTt3CkK_b_OIo z3c%PRkdgRVl7Zrl2bDmYF`&y>@TpEldX=0emOub!zeSZh03DHg4rK0qcax#6F+|z? zmTnEK@P6Ag{ONZ0@w3;x!ci4x*KcUf)-SH1HF2&}%E&oQ&iNA(6E zX-xa-2|(FZ;TD2thAWh3bFr!|oZX4n*ySYJbcciJA@8OcBqSt^+M7bH&^Dh62#~{3 za?7hhW0q*2(A?A5EPhpOigS@r~?n=F;X zb}W|1dn4pTZCK<;P6v+St6;F=aR4o;d*$VPz7&?{Td=>ao{*sd4DBC~0usQYir+4L6NS{spuI2;)6nyV> zB0ISO93#$@zW57nSRGhRP5k^y2nkjsPX+x2_s%XvUl%RXjbmrt#KiToy84up^Uzzm zN;5JbT?CtxwdsfK*^uy|Nos>1A^y_|wdF@i|HsRIbV6JB`xfvJqwF0jr4w?$J)NAy zRfWe@+diD?_8(#TeOU1B%v1i)48Jqj1HK!0K76ZpWoQtfdMdme2N1KQku{uyqXw{- z2}*pM!?#&*P3Uk#AhL=JXcSZ~ywY~;TW(N7mv54UjmQC@Wv+U*xW#z4pS*AN>j%l0 z;Zc6LAAC80B*Wnn*lS2@;|C}xWO55Kikt=KO3e_Jj~>M%{oX?35*BwCPlx=kH|RMz z`QXQ~+xPTU<01UDQvTImH)Nsg*pPVm>K-QQ(K|x;?Do;Vy{))5#tG>SV_>nI0M(m8 z7<6hH-AJM14juQ(ZGPAqA5KZd?uJ}uERE>SLWoMrIOyJFA^;NYSj|R%%9f7Nen3kz&zchME+1bdUJxAziwC{dH>PX zR6e9>SSF_!^CHn{?mja{NNf| z0oLWSWRqjP5hghGH@5d87rF#;60|!rny6fKnt|Q()(npgA?;uv0Pd^>M_Wwo#a7+X z7xag!#KuF&tkzNjer$dYhz$y~@n zrzfJA5too?CdgF(XA4|p5*@qAQ*{u3Ed2wXML~MO*5v!+m@S)*>$@G5gg2D$x%rwp z<7bCVlPh>#x_wHY+8rMEo3w>?$|^AN&KRT2Qb&z}{ zfXusrK1gljZNHF>F5i;aLD* zJ3B?&25p-Onlu+SB1@oX-dEVzi1?isCa$-~J}xAd%|X9;lnN7*n#gD!x}(wl*9gKL zX|gPMGrsZvAdo4yHnUTK0-RE3yX{evy(PYjew6|Ddsraf7|Qz5Y?{MajP-4^t8Yj@o@%hRIjg>2-=Z{yg@ z%f-oy__BqNk#vW|97Dokgw7Ife*P>RVs=Rr!a_gZd^-86J#u~B!D=0A2aN;QB73if9<|*F z3$%|^ejs6zu_#h%si2C=M`>pK9+mqJP1^C$6sQ1U;DziQCv6Hafy!PFe(%7Dh=}I4 z(~Ok$=$qok)7hNMmpj2z<8zL~>~@;4jlqj%E{=4&mW`EFt#SCA*LN*a2`GVr>IYh$ zj+COlsTWOm=WzDB3)moI?Kr?mx2{K(P>83Q(6MP~5mj#_ZAi@Rp!T%)rWiuc+=}9O z^5)m;QX#D^9N(UQW7=7yL@}qMnd~4qI~7!dRe}<;!Al8`Zw0Pj0;z%pB2h<9jmZl; zt3xh!37F6I)_mv*VpzMB_@b>8^-QAubx}GD`GP<6%_MLP0TlUl2F4Q%EhI@GKdC^&z-GN13V5pC zKs6oYq0PU=J0$>6vYOVw4 z^kUmQN~p%h-hTc1^;)P^hC1)%5YGkD>3@})%b%H?76d*WQ+! z9HUiuRMarak^(D;%`$}5&v9tetp8dfsdFSMbNkoVW}(+}dgotsxgu<+UYU4VS~^Hu zrLur2K&2Usl)o>-dCw^0W-O&oA?Y1tH9FAa>T{n}RFD+>sqeABGjSa}EJh8&v1;sW zjdmDRdO9~YsUp8Q@$!PDBHh>ND^pZiH;aEiX8hr+Xv<*1Tu*2R{|s+p-u-l+(2d(7 z3~X8XR^WHjg5pBoe?1m{{d(g~l)f~NKF<%hy_!(E68d`#D!g}=YE1M;x>EUp?mOb{ z{a-wp#u!XpmB8BEHMM+&$F4m~a$WSHo91lgQsI~v%*Mt>ik|`euL(AVn2%y;8QeX7 zf-M~cPFvWcD+a0!1E0ee@Ij$zP+{NGa`So6K7d^Eq?NmtE-@a3c&74w2_vmOF0|3q*KCDnehwHDKcwgLedol*W`w5A zem-H`2*Qyc1fBRymjtEb{=Vq=*S%TvQdNb_T@P56|DLNo?wUN@CWYA3!okCALIG#h%4bH8gs`#EV_)t04ArFhnr*A@m`XP>P-nE! zj*yE1Js=?niq7eI$t0W%w6iwoN3^;|(W^TF{^4 zjfW$>@0y%sM|RU&jfD<0nvmTE*WD8cc8Es^YwkAWppQev!&+X>MSdv-VHhF zn{AvHCWVCiS*$v)sCA8X_7lD$heH-7)TaiLA-YK64X#O6P z=SDlDmXws_hv>C2R3hTa%2om){i$9ab9Hqr)^s9#$tuV63conHeB@2BA=Haaq_@Sy z*NIXsD%#XWEeJa&`}atLn637R%YXc>V@HqX5+hXk7VlNsGeXjysZes?N=Cg<{I!fZ zz}jSv_GwXG-ApmNh^wiJ8f50xv{A~FzwWJ z8TWm7e_y}%l|1k*k( zQxGpcafd8@vLs)G&g}yrLmd*skTc!$+JP-EpB} zytgAFl87&H*2RFi3w9jzm-#!N_hS!3=J6nH3U93<@f?ootL(I9hmY6xDQ`&nksZC2 z(E9WwwR#m_DSXl`0_BodCLalw6g0X&r1m|_TzW?G5d44uc?#z2aMup zhS>_f7LLB0yyJBD+Uh3oJ!Vz07!0oI24dJ$6D*#Ua8`KMO-!gAje zLBEZ-Cep5$Zg=o8R&3lJ0OjRw=y3~(I0MPqWc~yQ@O2`xH|fG&HYx+|c9Ty#Jqm}c z#nO7n!(v`*5^gy7wBe*vOxQICoG&LUpxbtE)Bnrhg_l@TNWtmA@O+DJMn=s95(Wji zj6-UaQ5#@H#IT&8yZOF!&q=8e zljm+BA1wCnROHw6*xF0Gs~Q-%N=jIb=eolBU*As|bIW@l)g0Azb0u$~wL8HM!H;!< zX1%?=3unGl9MRg3Dw>vS@~t5?^ir`^MROV@ZWV4ObbDriIB)Uvcqw?p-}kFjD|BRM zBKiJFc}Buus9ANT`j3Y{DJGBWG(0j@Dzy7_u7gQ^Z`!*g9?gN3&6Q=$)ccgjs_}v| z1G_KV;F~?jsH|VbP&NwkG=->;S;inqNWwG4Z=cvGonGIx=v%O($%nKfdzVm;AW>I@ zV%!TF#0v`-0CfA#(6MMX4>-DR;mK7d-?bDlCQz(1x=Cwtr6p}Chbt|H2{oZ+N zCv^%cD4~-w@}e@BtXpv$Wt$zZ5)$3PiltYVOT-nEj2>QwjYD9ri9sG`Xp>G&e z1jW^FJa(44%yASJ73o3TqLd7$#uf&l8h&|q6-axUkY>@Gq{R(4yap(O*-y)_T7SE0 zpZv^OBYhPNaR%a zyn{H7X$yD!X$Xw0vW#Et(Jey72dUAvp+H88kq0yaP2DcKjLy@9=mV%$hJhsQLv>VM z8tiw*SAFRrFT$%D?`dM1 zM=?vC0k3dw@Izi+9`sV%0Z(e7gkTOjv*S>ZR%7Sd4Be9j%X@m9Cj(cRAFgcrx0PKl z^YW5-fxl&-+`}TvtyV@{_q=z=hr3stH86oAm^M|XH!EwiNOzdG77>T}QfGYYeF zesR#2Eo8$=&;bsj@8aT;HH@Ij0y1OZ{z^2YeSOV;GZQCjZUZ#-`!VoXlGUK^oDKSd z=!oL4&UQ3@RG*JmqXIa9rQ77TUz}m9&>mvqdS{6?cm?^!)Z|3r*YQQ9lNBzpRPPzA zRj1a?VT-8g*yR=AtQN)-dI@#k$kOf=|8~+BU?(Jv?=>)5<=7Ko?e(0?WTF)D3LNT6 zdT0Pxf@?%O4yUE2hBi9BJitUv$LHl4ELpMyqNRH1Ags$vESh2%hCWLEY~xjXSZPXk z?qs6zqgfUg6(u>buqRA+@lz@scLYQ!A)7O5eIF`0#d0EJVw@W*`KttMmNHCk38S`c}-eVADeXsm9nJuu?gr z3p^hgV_Jk*xdzlOF4DLzyVuV>k5!^S;0ZG&>dZwt=gWXaj3i{!cL0An^6>;K<)HUv zJ33dQa)bn$^fG7FsFU1~@~nRH-q5;|XE1bqG|w@WP13P3QI|>z5*3>6?7U=TW|oflAm^r3{lrU5SI^eNDy=cW^?5ejfy);3^9rgTSY`(2*iMp9xT^!a;k-b>XTGj-hL4M7OxF#!g#iJ zm!D?X{%=gMSEh&RUa`Z6>^lWi+)F+`{M7`-O4j~d(hd|BZ4O+uYy|3dEvr=*THMD->@8>fC0yWzi-5YH1N!)QJ z-=LPQ%JASfXN^I+K@Q!ZgF{UGwf_v$No&bA!(Sqf-I=$|qb?$jmsA(}`nNc%F_w8ln0oy?NgOJvL891Ml3g@{CO zuDRS2#v4m}{nYIHw{JfPcnnlA?_JE2CFiBLXxw*a4CvG1zIWoM8xU+Ypxn`Lw)t-< zWTm;D+-s(^`Xl1H>md@!LUTx? zhaa^&7s^Vb`%q_0+}yIWR`*=ON#8k6QBBJ^{M>vW_PJDNZN6Q}eC|J0eI)(pF&`(U zirpjH<8~Pj>})vGoAq5+Y+XR<)P4NgCL;%e zVGOED4+P0viJ&ZG^Y3ubAKlT*5XVo^OoicUVSkg?G-IT|=h7EKoOAAQa%d+~=3RXu zr7Cdv4H-k-+_f=Oyfkl^8&`>*+T}2f)#pVF{-ZGvFAZG%B;MX9Anh_KayQ~T_E{hQu z<~Y0n(ck+4RE?Y2j}0};x!%-hok9bi3WL<_Cn-wk5!7}?0H~n0(>>*Rk$PwF;(r4s zG0S6qv{LN$hvlA@{d}82goWUX4mvZcNN+_ijyB1*e2bVO#K|3x7SMQ>91Ux%;51LR z+>xoKrtTz0_eoZwgyIO2TC*I7=6-E;qVXB1Gc%(3KpFwp)@dEm8Dp|MuzzvP0 zxK}=tRqG9lb-MRf)JG2TAr%QEsG()kCb5qg&3J@KjX^?6V@THU<$--aFY`QoAMW=m z)k%=;Nm=AlDoAJdU0()7~a@E)v!275WkvN-I0;^2>u}Y@cmfXA7xe`z1Z| zUo-5lJ@+=zW#H*(UjP%e5X>@I^tJ$k5O{-&N{8S%uzU&sA}B^>29nbs>1Mg!guc>v zRQs16M{Qv^IsV%iUjI|59Vwvq=9eb6`!sp08eW*|UOK%C2^iz`YGg0<#N@0U#2wM^ zh&&B;N+(QeP{3S4t|d!$oF7WvA$GVVykWOld7P8oW!9j%jf&>QhEXlqGBq_jsYlCN zG#t+ue^Y+V<|j#mqhZbKwwo>R_*OVg+BM_<5YLF+zjISlFfw^W_h)lwQP*7=fZdmc zN*ZZYe05DGUIl3%AUwAlF7zJlFjlQ%KB?^*U!ZE7)=*QkVi|2S`DOR@=)#XUfg2M2P>^k^OEikD4nW)ND&eo;F! zdgo!r9bEImTJPPgP)#U_G_Vx=u2zUI8O>F~jp5=RT2#lbovF^+;SiIH2n8kE9WeZZ zNK~mU!)}*Wy+<( zn#AdGzSV%-O@2=j=H4WjtN){MGI^_slL|HOKiH1Sb>?6Mp~Zo1ZJ**svO;QZE%O;L z?+J(@8U*ZI5O}9?uc8jYP@uRUd$CP|Vpft~J%{5VGTil59JGe*A8q$+Vu-&|$Imac zIZ*ieA&C>F#sO;%n+-6Zdy@P2yZNmBD5dMJ5$e#e(l@Jp>E_uEQGe=hb(+d`mSF9> z{V3)p^%;bp*6>OV&y^QwAjlAPv7T9saM{u)#vrujhIKu7>r8Zr&Y*yxk~Q43DeZj? zdwhvqO`Dv6`^ykvD&qE5&%`@2f>+0dp|Pe%gJ%b+Tghssem$b!CKS z>i{CSa$J+5v64J3s77QUlG?fJT>W1$X*~a8zG8S}MFN&aWQL+mjsJnmh`fXnn+&;r zwIOFS2bObf*sixbSwie(Y2RUlsY%xXnV*{dmHgrbMinL@0fkHq*;Us6D#paQ6B2=T zAGCT!qZqN040uJL-=BqgbzSlkD*HU;n8VGc_P4mq$P)?0jyQSHAd`-=VeZA~pN#3~ zN{J*O%IK)b%(Y1~OI-)py?i*QLFm{hD~GV+O@`8ej<6m3*xb1{Q4rXu=FShwlY*P zD(2Vt%i7wa{7VSqNCOUB+~Om`0slcF+yGBlLNB8(9=_>Wn;V#lPhYb6w+OXz%d&#g zu!b3M*8=GK)3(z4*h=U#Xtl`B93m%ShSdaQsXJ7kh>%%f2n zAaIJ8ClQ+i`Z*?w0wk^p;8`4U^#ai}m+vohih!VK6?|1hY1aKHFur^p3S+vKv#$6^94*h}=Lo`Ifg@TWQVHCUvRBK`D?e6?eJ);&K?+T(wg0Rc;RO zAcAn+664%galMD3A)visi+{v}w+XF^D;olaGdjKsWN=jo$*|-I22~HQ%iIp@G%P}b z5UZ@Nd^7N1ti>GAoUQ-3uU=}&Ree`gLBopF5*^oJq!{Uw)QiH|uIcHMusz8g_g}KI z<_d=)(~IFT@l|)KM4E(y=&di2OXTXBk1(r2;17#Gx9T>SHaz;&kZs!#SQwUK0Cr1k zCUH?h(lY@ARr;FmkFLqH#BV+}BV=(vx$h)l1gZK>3riZLz0Y19%U6E(Vjq{-mi)l0 z=$XtCUR}#B{iFc1*H|iD4N+eDSiT6^i%zPeUFBOP66a{(KMxZ5iP)_zW=c45(PQt?$w+#3LvZG;#S5|jno;3=3#9KJpzBm`|+;HjkkvqJ?(n;m<= zyVDnRbl9=-U zGsL$*194^f=oY-CfJdA^RO1Pa&iKi$>CH8=yBG3EJ=-|L$RY3$vGDk++pX)nAD|1MXZcLpsvGk50;n)cKiR33A zJgArj#Dwo>wuaKAGj{%3 zAXGpv2p6%m2oX>kxx%$<1relB(`0;ikw}xqx?4tvj~f|<^FR6qc6BoA2sK?jJeK}w zRG(gvap>Oeqmn6iZY^3{d{g3d2u=iMQ$2^b4~{!Iip(F!U9@;+`sMnZw)02v>^kB? z>HGzl>fYtms8cdp)HxDdM+By@0^hIoQ={tZ7l;|&ZRhlnqcOwQsA6DwxUi&e=zUFI zZ*^4bWGPzp*GQvu-hdFivolT&A6U$sES61sly;4;Z=3)AW$Vw!s`qv~qq&`a%XsQ! z+@6R7Vo!u`MJ~;8biFRKNq85lr2VBa#c=lx-~LlTsC3(NEFed$061D~2T}YWS8ZSZ z&-p47V(ZuEKd0-&_apRK=k_*Jh?@aFVr5GSgzN?uyuDENY$vbOnexQMp8C)HVj?1a z^-~?&aPXHv_2pz46&riRnRnh&&)1gw_dCoVyh1is$nQh{omm$AZO@QL zTgj~x7ssNMPRJ(d+yS7uKJt-X{s@!I#_>S^+?g4o*Nb5IVrpI6%9s9Fi65^0GHZEs z1dC=qk}NfeyKZ))P}rIlI9%hGIDyn2hbKX-H7fWXD|+!=-xbR_mFw1}FgDm&(O=Hb z$9KYEla}ikM@DOF9{`9(#O*n6knZ13q5xZuVRRVVnm*np<{AS-!{Q~?+iB(xA1vO! zT`zO%e72q|movV9-gfd)mfPqDZ!?cBSNG{()T>LUx~*LNA$gpzvZYQgU#Y8|{8(uQ z!&{sZmv6cz>Ur?+#b`1iYvCurYu#g-s2@wU(>FwdR52-ltUp-g>JS2l(h!lM0CK3R z7_0wk4C^6ABb@l(1j38wz;OXT|hE^Q3<{UdE;JR#U)Qc@^%Z%@)* zlQRGQcfT&m$gfqcS~7Mo`13DH;_ScPXKZy{?7WKa4|JA3lxj?%g3|t*lEpbWTyDoj z%^$uS`JP?AD?2Fq3%Vc#U3E;UHj$8-E4B@F2cT(vl_xx(eTDVLT_(7~0YgpHdtA?U z!+9hd$v``WHhAAk(Qj#^e`GuqZL!fZ=KA{q^)LDp7iUL8b-+gB>C2dG-|%kYq;aXx zK{kos#4aIbj+tnmKYzlBGI3Mx!$^Kz3()k#-=j@p;JE-Qcg)C7o{Gwr&XKM$00;UMf(54_E_eqY;X|L-yHR(7Zn{j1BuHdtE;7~X;t^at{SoDuds z$K5Zbt;RvHir<(a_K)XF%5DYT$WaT`ct|MMuUl7f+RDPh#o6Tn20+P4Jib;_!gu6B z?pLLG9Q*$Ovj5ICHh+0yFVPFBY(3=T;o;$lRoB-EN`EG{2`!>Obe^7uW}JU&_Dfu7 zn!f|f{Wk!E#(6hv-mI@YS*7cR5Yl@N{tOdV|3z37bp9h78{mUS+ZU18>ei0PI1Y|G zZ*0+~oxGO+ApI0YGSg5$%Od;h`LkyerHXj|6~<3xrKJsVUJIKqrg>^err0H~bB5FO zs_f;un#IxAZWAs%;7fgXTs>U=F)8Z+wb_3Cog-`{*qEKSBhbh5d7||o>JXs9Bb8>~ zHwC)j*Mb}DE3w<7cG+zxEEy(N=Y{O_Kb!)8t19l15SIDD@GW(0Q)1w-z+SOWJQ66} zD7k81;GG8#EI>jQEo38Z3}C9>|hjakWRAH8hW>a00r{{P-uhg(Sp>GK-}#J;DC%@xT5A6xU@eW<=o(_ zI=B6dV9FBz$@kKr8fZ8LIcs{!2%$*apULI zldXMyM0m*eONL_9nF}!pFVp@euv)9|sn}Swe)HQp2$2rk(@?x!;gQ}2y+S{WZ2dE~ z^<8HFc!1+yr{>`e95}Hn0ei1tgQt}UyqK^;n|e| z)`y%v|KrFpgq4Olh~-tbQ-8mil8E&TY85d#xqYw-Jyh@>>S>*&;&}<|IIDkr_>bOD z!pr4bi!RiL(WJS>OWDClZvdI8uj%%;)5yA(lFEawQb_(^mK#8K0LpmPlm9KUpnn}S zVqj$Q5fdCbEMatsIK8i&j`&uI44O2J?6^0%%F;tpSVc%;h?)zI&kL_ zVtn7%{EY+5otoEH9m<%)Pfy#nfQ{Z68n0DJ)b`NKg!CLwtUu#EDEyf2tow9M5-g@sjdzJBqI3JA4luk{rTZDqM`-(Cj^6Ed7kk7ukY+6Rw%TmtSk(t!+JcN?ELbeJRRKs*ZZ33 zIG({Oz;kmiv*o@v6mu4u)j_M@Q&F&#gx|U~isSm=tC|+RKMKI+;~obi=PZo>G^n(^ zI6Y!bnW#`6Le6+IXHR5^`1oV=L#>kXKcPergTSO7zYI2^Yv+ZSyCz-x{y{%B3a zau91K!Vn2P8`Y?>TwAFAMw5G0A+ry5|35^4zV9C*pxtkWNZC!g&W^t&z;T1JLvHCB z@4Vop%0Mn{VGGNqEv~ONtfCx7M&cj|-h(=A++JG!C=5r;K>%NlQKiW+k zvN5N#i`sYn`3+whwS}eC2T*jSdd^@FlGyB`?)zk*Ks1KtE~Jsg4D*J(`7)Y8@T*%zKU=1g^Igo8x zS$UdTm`8}-lntv^sR2*SMX$tIL!J3=cQW~149hvCh?a|#1!A+D0h{22L9ak&t3+dj zIFwllAP@b07pN?QVB>F7vsdObcM+x*q_3t}@VpuHkwZ0jcdG~w9ip*|90(vuP z<&uXt2ZZW@uK}af%o2DSG-sN9-R;>=O9Phw{{X{te@P{PB^V_+4+(oCQ4;$OABPxd z3`U!-fS98oF%MA?Jbcj*8s)l9G6GJ&1y^{XH0x?Pe z*9vRLNYD=U%i-IMDztWh4J0vzZ_e!m`AOoc$LsUWDGcpZEy>yxTTMqpABqfq#~ug7hW<37nIvoRD>e*8bphmD8hWGO++R4#q@D-(xq z=+@)8#&(DuACcKhkMJ+!!=x0Ai12Qzy7fB8syyLz-JIy0ftmHXh#NXyaWS#i2xs>K zAqUrh*hu*QV9{%IgL^KAqt=>JI~o|>DF+FU0_MgO&3pT}B8S?$Ce4n-bb*UijlKbdh)DYDX);QP%H!D{Q zAmr6dYUib^P5xFV?ua9plyHp=*XB9pB7f}$|6}IaeiggdOK-jUm{f*3mF=AV&p5Bv z)z-E^lNIx!j)7)C@~Hv0J?c{(t||B5p^QIYuA_e@AgaP z{E-)im2Mc>!h>I?W=^kgcBV}pg3^Y#45-t41*0I+tS0&Q>~lgm3~q5gf1^MjX+AB7 zLzI$<@ShAhEZfR1ty7eFHwbA*=3 zhl9zGXa-oFRm0nFE^usYO4z-a+`I6Z^ys_RTnb}Ml-Ft;Q;ag}mnwiI8jbN#7 z-@eIs4d~rM*L?AUgHc*u{n91twyr+4fSvBMBv;}!68|C3A`&)*bjIyte|gDs4!eY9 z{=|Qt$YF`KBv_nNS4koA|MZESn#$Ile|^is6Q2ZPOxR4kWy3)bkfoPm}gt(|~>O zyGg?Rt-O2&c!$bBO02*?wU_#n4Y6HoUlmCcc~`v`uf_D!#H(cFjIOH=j&>Dy?Dr`& zfvz2um6dx??)+tT@ZrV(h#xbSJvY~%{>wg(70LAfbY0+M#GX|HT|ehb!j=C1hFA|^ z>m!{+M}B2=HKkl#Rh8@gq^2egy=|x8K?iEtm(7L24J;Z&g|g$~g{<^B4c62WJnw@q z{%N#!0ZY)_Ch%P0RuSniD%gebrzj0UUxa?Pg}YeVUCBp5FffUgU6;&Iw2K4mH}P=sq4s#OGFPLU}kT}IZep&oYQ0L z-BvNFrry8b_An>I`3!93ZN!{3_Yz`1tA>zgC1S~adfG})&)S}q-mV5Ah)%mQB|-9- zU%q_VfU`#ynSHmA$$athWgEf2e_BO;e()lDMpfpsP|rc!uY~Ij*K{<-FD(QU{O6^% z8yq0TTuwZKyw>PPiNlTvy8gUWmaP_L4$4LLl@n};aOz&ZCG3SZ3WlS(e^B{fK~fL- zl`eS9SC^>@3u)_SV-(LnrY~4gG4unciNxHG|3A%iZ3Yyza{d`Ll^bF(I}&36gjLv$ z#sKkzg|HK_P;Ug(dzOBewKvb+c3xsjVEo$SJBbMjm5of6rBon!BTTBoLYGJ0mYI`* zTCmkLL0mybO5pze=6jF)7V564#J}zU!MBkaZ@<2NseN^I1kmw?JM?I@)d*F^sax<^ z=(^O^Z8hXa0#pvpHK^Y6g3{?@pu%NEg?CjYUSp!-R8d$Q43%^v!eb1Q(&6?+-ry!h za%fT*wFJy+lvW=Bd4=DVtC zo0`&>Z#|d z1d3%|q;PON!|=T<*kaHk4Wgh3?HnyqC8q%1LImrCjYx$fbVT@0=D-~VSEA9q%7gP@ zrScgymC)o%B-4EibY{nAG>tCAHgt2voc%-%%<>v(1a1U~kaw$Qb$T*sAq`SmXF>wL zdsqhd&D`I?Ds{(06q2!iL=kfiLUN7%zt-LZ9P9q=AD0x7l~p1$vy^PINy?sOMrH4r zJyN9Xls!sTA=z7Ybs34w2t_u@UjOq=-Q#(_&-42qzvFiu-{X7S_l@hiKI{EH&)4}H zdraHCw}D^5!&dTf#qyosUrjb0otz4het&OO+Ixp&Zgo67J2(P&FqB(HL;@%KbQfST zJVqWD`!Bo;@EKUe(2t<7vGm~SzJd%e3Y7qlg8i4B9^94(V7qjNnPol*d(fyK2CpAR z?A9uDfB%v%J;d_jsQ}X`7_6|sMiHESS$$mhI}f;fzoGZI1t>^%W+vixS=0XjBmBq@ z-~T*G@IDN+lk-GanLh!x`2qgFT3T996i=9f`{3`-;H3aAinj?UEdb|4L~_6jt;z-c zCI5VfbVx*4a4Z+(;c4-Nthfq7Jm9zGEC2p6mLgo%uW~3M!okrIS(@xAtORLhe$Q92 zBnlwXb&Wwkp?ORd&L#58f^w2gR5WoLcF*ty_p0o_6fLq1rQd=bI4&^wAK2@Ie0|}= z%_O15?t#`8DJ)_0`Y`E)4A<#{9#d(W?UrL(dsE1~f!YZb^?C7=8jc52*}gEy-}iXlY? z5fNR~v9$?~9fiiW-?0S(UqAP|z%bKgJ0c7y2>IY6i-mO!mXNqUL|sRoCQ!4*f#o#t zWctS83!c~$SH9$-*Jx$3)+^KpF%Eh3WojYbq5m+Fq$}`(QWc@mJ=Y-J3)f}uc6^vZ z(t*LeN`a9Q@@gnX0vnUg1?J0)m((3#Xemf!rcOM^1v~U&u>;vCj~|0+RgSIJ|yOAVNA7Qhe(_p8_t)o5^uB zZaB&24n6+#Ru!;+%-}}gdjcTXTW`JtFC+*!Vq;ZoN(Q|DHxm*vxLmckbe=aJ?ld(v zngjU-KuD1R3WX8NtMVC@;9i>q#}-xzK`j;4${o1P))9u z-&!`gpLZ_0U`3pRT)2~-G(O-6X}RUucl+rint1F&^F0ZPCtUyn=}gay{tq)g@q&WJj1IQZPrtd7>+#O;x7fW~=x3qkJ^eD9%HsZqrSAq`$ZQ`>JV(Cczue999 z$0hMOQ`lqTm;M&SrLNS5@5|wut&F<(!do!k78^l+*Oj51e95*2Qk@1;I*^H(>0Wd( zn(>9jvEMo@0*f%1!zlW>ELsmofePVXFRszURb*uVz96+L#IBXNTX`3-+OD>)yQ7UU z^b>R?-9U8)@a_t5Rf?!)2eGg_BL9{Ot)}neS+C%c;F!RZeNA)g1#cXIA58YP3oHZj8 z(=1a*sUDZd2yKuT0o9(t3Y^~B-CkU)UAtzF+FdzZ+ZEkh?b_KDS=ra(C1`p!bJ-g0P|mWcyRGh51o7+>WIqpT~liOeA9o4Zh= z_mOLlNH^w*)f}0Wl#3+UvH~dWn5fliZ_W3Yk~rwdYjXQGJ1i}Lu%dps&l^i8VUNs7 z`|{NTqkp;rm|b95Ad7C9!NGar?~kn;*m9AGPR0(rd13bi^|<)H@kR>X1DKN+scPHI z0o+&GHrOX29aA}lLU&*&<@}yK134$TxYJ`57XHf}fc}mqt_z|tY&!33t?NK8@%D~D z&3ef829)emkyp9$!=M7s26%H9tT}&y4rB`5*WlUuqj?veWh@Ew(JDnyN*aKZp+m^+ zaeq8Be6*Epy93Xfor&pg2!5^Yf2p~AC0sx~`FU*}o(th$Ud}3+uw=S)L>|HU{_R`K zq*H(SQ)msvt@Z-#v81@zZgKfz2t87=z(#>XNT{z34^dlZEYGmBKOj6>oA>XA)~Q$q ziqO`EG{2Y1U@o%^JkZyBI~h$B54r8AwwBgL#YwyaK&F13C4~5an~oY9*ur z!$mtoU>uVHVD%|Jb>u%$9I61+q=Hpizxu(gJ3!O7k}!iXqvG?n&_QAE+{eRSx*u0R zq5^(NV0<}k`C|(#&TO@_saD8;JU=MAOK2g;%XnIessz+=V5os(r1JbUjgv>AnR4|` z`2eQa;R>nAj%GC7`?ObIUX)imCU7ZSB`UXW;l>;Jxxqa#jV`tC{UbA+RhNFCLJwr0 zz`<)wuRYKHJ+up8%*{+iaB}gYzvwSHJ|1WfTqAIksimCaE4YhkB{9GiJT)FuKaJbE zO>IHJ9`Y!(^Xp?Aw2ds@A)j{YhhggW_2qm*Il9#vp=+-KUB^F+UEJ78>6aE+a+Xr7 zgpsJfjjqE{DZxr_>?$AhZAgOrEq_=ZbeEkLVqfUK?Ot4P?ZOyd9wkiy9 zzrEM6{O=#pVwNlI;^&akzxixNd&%+jA#R=W`yHVEv{bBc6)f-uIivM{F-L+g<&qwr z57|yIr-s?x+hKi4tbTwuP%}L`YJ5^+1_sWk(l$F_qSYOIu$u7rb!4c!U$NpHVjv~& zM;XrB4yCZZ=pbkdT9|VgJZY>lseWK8f~@Eun_ey$5)=5+z}~6V1;_Wi>dZ=JU9(`A zxoPK5R7?PB;2(~M52Id1aMPj&c1jLuAryDpU@HM)f&?_{+Nkirdo6qnz{O{ZRHkZ; zvE}N}WFgW)%Av-xgy277!d_v;y?4Y)rDyh+GD&Yn22etdWgj2F8aciH+&}r&oh)T@>q^4k=V~-`4Jh@tbQ1c|OF-id z^2nApwv#Z8In;ZA{?Y%`tijhI!7u&b7w!V&H-s=E`;r_64N+aNzN^Me)b0-sa@xok z3OvD{W+1eEfAB@ZA!VFSq7o03@+pugse^wwar&(U*A{-BJ;ioVQk>@p zvIrb3b3{|T3Y1Ol`v;D}3IveTUk3~T(5(j!Cj-}|^vj(VU7zkB1r(f@aqf{60>?NN z+D*v8P=T=MPMCYP&g_2y_`NA$v`J=x*Am*FW_qB`R0jKP7)Jl~9%k?!8gN^}H%uf& zVSpf%?k%+c^YgF03Cpi=u{rby5%U1p4E@O!&eNCvYVxq?pjAB34t7;=Q++}uqv;XU zb3!q;_#5khx5DFqPw4=h85Olqib{gK3%~$;*i?SM5NLw)sIfsEBb71|*UlR>1lEQ?YIV`(xGbv#Hl=rWZU3WCvkEc?m+JjlVip ztN?O z$5A}|x928Dk((L7VeH@@$Ya`u`gB5)zrN1+h*f$GL02pnSfX7;Gh~2iZvpt*v=_$( zZ-p1}w)Xoy3XXSY)koTlH@mm2o`9$_ELO^l8`$BWp`&z$VJosWnd;4F(%lE^=w<%@ zwUxfFp*3x7dxUcxh|4H`Z*v>&t{jjZM)XPU9|r?DNq^-XWUET=wzKO%b{yn|$#2Am zGeD7$wHZJJy0Y3ZJp9zY*jPyx`p4nn2%3Ef6|a@^-W&Lm7#%(kzh@%#FQ=9FH;k|r z$o!?u_yJ@M4hm8M7|-eLAu6C{K>mZ({_W{YqK5RrkN$h>h_!&5VVFc=6u~tIwHPvn zntcPtFHo)Sy%<{b=koWsNf&xx;a&!i%h!kGNwYopIfNkYEzpJ#5enEVH?HnuDFIpkN%H=0F#oGK z&bBPXJtMs72ii5mo1s>)uI0=S2pt(2$co66As#G@z9!C1P$LLSmP$Vj#%Qmj|9lFb1I|5_dI){8mH-(#0zQyE+y2YX}ytTx?bc~Eh zuR}vhgl;~tX}&TWp9a_3$CV6E{`Np|7O(I|M>}bbE+^w@*=Ss%Oo!oWYHKDn_1SuT zm6Tvn4~tihr&&4QD4r~P7_1SIsPCw6{ETs+C@LdO=DUdN=W7#>9;#~u68(G}o#nad zQi0JciJ3Te?yTmm5}Th|8kttppQ&pmf`ZkYo^t(kzsNCAm^EG=geU6?);H(Alzx;+ zB-Wi&QMh!ed$;=b!^-GrK5v0?Ibxv%r3(GZ@89`)oM$=YZl!J0VRju#GuslsKMK`a z&(;4)PSvB=lm9#+KR>&-N_VcVj(VDLpyVm1m;r^y>ifiGLr49AVu#bGvmAE4HwE<^ z=UEG?T8Ftr>?G!bf^=5Z`Ic&8cO$mD3OD81*;k4*s)$2Zw;Q~LcUGf>DBUmu=H{*5 zgEz&ytCO?Ei7}hC5lM5a?HP6h%E7^rtOBc4_z%<2MM#wm(&_@ z?!OWLk}=c13Qw*9H2(+v7*bAHRV-eT-Ci}h8?p1!Fl!kTE4;8V{oZZiEw%N4o?}S= z_KYdVwPFQ8-wK+_3$JpiZtB`~V}`qGw+)6=r!kG5dD;f~R6pf%ADVr-|JBubkyy` zd!igi8M&9c5WqGsVCWpp0p_=h72A;D6dH5f8sbl%+lc*wRmEeZdaS2LYY~XNpO-x+ zPdEOW$(j-0QN2g!sd7_)wak%1+$qy#cR5e^wHbW6KGSzp zKRI-Mn_R>(-Kn1NtjqpIBf{yOP%oXy=p|*+*s3GwPmZQIpR7DmnjA^zetD{wJF|^R z%3hpyVQhUUqWwETV9&SFS_TM3%{hGZcnWg51$G;8A80BV$f>9@kUC9RDif7k!1qLQ zv0KbcVIR*|dUsr_yk9$=5iF$mWxLTkXKhxS4_ivk{z`1k$!wQ;HTlz2mztC91Z{}} zW;Yv08>f0+YDs^jQ)soGK{ZO1`l%7RGn0A?UC^6)8D>{lgDa z!crNDE^$DY*#45}ORgXwLvrWNwr>xK;9v+io}*?1n{?mRw8=R`Cx2g5uqQJR4S7+{ImHuAv>9 zKBeq~D2!&$IcSadBl-div241?@$GF&bRh%rBM_lddwZ`T%V$`CSNE#UDb0WE*jAc* ze-!GMqJjr)e@W5ZHw6V~&Y&wEgvxC^N>XkvAx0XpL&&`a0NS&ifVsiTpF%Q~qzKYN zPm&;}5^^_%G41JPr5=zVh)nOJJyC`KElg6MWn+AP@WV ztIb#4`?mFalXpNoz8qIN!HBe+z&rmkbd>vT3e3sM^uBdNEBf{`6 z3j>eHLn+{)sHm)aeM(E_=%o1ptkkd+6%zI{X9_1rpbNufPWb_Nz@(=;6FE%#pfuXk=XDbJ!rsX1}<)GEMy33z1ma3 z!p)^`_x2D1RbZfA>&oJ5zV9USv56_i``VRBeLlw^4F|mQ|8yuoSIDpV=o`L$Ken$C zpE3G*-{y{32%L#N12sta=r31XJFD&ksgPcWoXl|oNId`fy|?|dS0sGdT|BJz3=2*t zZH$wntFwT+H#J&1O;`(&3LwG8k~+e34|@=qa@2%^lLjg23>OZA>vc^+2E%V>@+^pP zV*bmN+7Z1E4x?*XGuXk(w!PN__L8qko&HNK1*#HYKB0)8l?hl`z^gpim&9NDZ`Xv@ zFH9+KL85g#yGEZj|Iv_Z6ta~>KkoiIgAm-29bez0rbBo4p_nO7#NQ)b91S=KthOV~6celEjLv1{6}YpJ!AXWi9`5H*cOn53ue=kQvwm2Q_lcJqro%+S1)7yP z+#34QEzo=dv)^g}LLD?vNxTgh)n_DtEiDyHXi1Fh7BQ>xP0zs-ELTQEuYt5F@_wOhX`tB!X!~2W!7!3qCS>WNXKaeM*0%_~H zQRL;xnOo{lMi`L$x8Z@r`|Dh89NGW0YLW2ep}M!n{VXIQ8|3EkAyPgo3Sq_wias0& zpu$jN^sx(kVnXppWmH#{>zj!Co}VPn1r#v;s1F`YQZr63}`wEFs(^~lCVP!Tom8{ zB<@^1brQ2E>*VB=bNvt+p`e+=9?2Has8c!yYHAKYRzqR4bu{y`Ml*>`0n4c5ir!hc5x;p7 zK3)QBB@)p@IHNe-KqXns%i;_e-_P&CCz2-M86^|q3^>Kg%M(E^X`pMCp0;!ZCa5_) z6>@|YD61L*7@*HhM+poEcpR-|Rf4=oa_^wQ>pv!g90$Kvb|vHqkV~eTya8b_FqDzh z(8VSE!}mS8D^>tE*|%88L{_#AQ#feasTxKr^4Z`QLd}5gQB<6##01#;D_Fjq#k4_u zIQyKR<-0R|dKG_<_IRBL$tES&e=vnF+wwGUzu10NCG$aSmT|JzN-V(rCWa&cjhato zpzBS7tb>uK3IE!v!UjK>85a1$a#W==eK@vHdfeCv?6Ucb^!WZ#6T;u%83H4Q{&CF3 zFG#)*8H*OChx9=eyl-lYI0p2WVNo;(2`H*`5*fOH%c?UFdqT!E*{c{)zT33!!H4Sc93$Y>Oz5NTxd?;bPI8 zi`rjt-aCMk3&vok5+ znAR@`5ZlZiLny8ePAPSl*l%&(Z*~M8K5YiPGst(ZLahpwc8Med`egL-m5^2WA=ugI z1&V0IVpSw{g#i$PU0VzOf@HAb@FY_YqkNt{MiH+)i;pW@_IL}!{WpRC=RN5!MR+T? z%>Cji5os{hEq7b_gM>g-857$#>GD#@m4>mY^l!&uc0z<_rf6n8VgAafE~aq ze}O_uVZ{_JO!ymB$K%J4XV=347aBL>BR52Vl7T|~=MDC1e&NcMeoN#s^^EU=M?fy^iC-gO5OI+s?QBDhD^ndzdPMTL28!`>4Jy9}JbmqxQUyrj#Hz$+X`I zP1mf_e_7&4e+1tiW%M3&2$+m}#syUjUO&osv%2{H-bi0&oUa-hANt1Pc4U zXGWNwo^~*PzCO*J1!ha$lLFZF0F37Dt|#vzV3&f9E)Rf1WYzl_T%hbUbVJ1jhkA&c z>2Lh` zz(km8AX6^ftfyIRT>@4d$Kd)KVi@)4y8t-|878b0ID&y+Id~X?wr1+n5>%1tpH~Cj zhU|JGdt@t#sY{4Mm)Grp8`xE`4Pzgz>3PW1E5bKD>t@~T@pWz0j}=FpfN|^RD+#QM zosH?ez}IV^0Sm8L#`QA?*51aKd2dtkGr%ynvc1)10tFQ^ID`Gg83I@P$Z`WuxnD-H zXRwOG6h4!3Uc`T5`KFAV+%(|!nfaK?B$8@m+KuLPC;|!X?Fk7qP_+3J2zX#Z)Eylx z?6xFt8i*f;^Yo#tRH#OH>=Dmn*h|4tLK;}Kdp`UyAG8GJI4pLS;|78EEpy8*!6&+e z+=iQE=rxbP5(t;B%@5z1$#0%%&9(sXs?8AjR22mfBM3*%-4a^3LbHsMmZhDYyL&^3DOu}t#r@Ax1%Do(7{VdN+cck8} z)4!@uwJ2t`B?bw9JE|X0y^Z|d!P;@zTpX1*1*Yw}`7^*9RS~qzvV7NlA6I)+Z#IL{|fms68T9&`D#t<{&g!j?YI&a<8Qd8;RP2CGfO_k!%gdCMleoQ5fvjkf^mQu8M($%5*itZ9ag~L$Z=@fLyZDV|_>H`@QF=XgbNx4c0rL}xX!HCtA)S~W)WsAhDVHyNdfdwW zPJv9o?Ji1k$89)*gZ;29zX1FtVOHJ0gIUB4aog4yTf2KhkV-&kNITC!e`5VJ2p6rx zj}U4DO~;luHlCivE8!eC#~QU?2fWW28I9Z|2sOMZ-u|+OZhezQGoTU zn8e|@EYxGD$;YdVY-)$Weq=@T@G$MX+mZRZJ^Wt_kcHk)}IjL)@9GNAmxA(c-TK1oQd)L z3abR~`0`CyGF~S<)J|6JcCNmSTymfYuvyt<=t6!zFb`Uuyl)8QkX6Ju7?QOn4R9G& z>-p?%4qGQ4BbTN(Y4+Cwh^Kp2aO$>>dIH|yl9pCR>Ix4(9QJcVaT*^Ig^AJXB-@cSwPp}P zBmeN+lG$?*!$~b3kCl9)$_o3+c+;$P$(JUsh5BU~uNr0d(XpGsl{L^utP~|iMylCv z0-X)wU6JBkadOpXch#pHI%Mk+2i{?4()qjTrlGWmUPs$9^1-9_4)|(?#htLSNo$Yg zkwfZB>c{?EkNKL5Xy^T!=I^R(ouT)lu{#PYE7F-2-FkN{zJk&$2(XvU?nk|XYG9ud z!ta}hdFTIWvXGU|H#XNB)`LD?>T&krVPIeDiwNp!&b`}W5zwDH9yW!c^gWgwKN zb!Ar1Ynzc)H*CbLNPU?Y_oT!bTK>4iuy7sfluS9`o3exZg_>wliF@|k2<`f~%W{oJ^u2!rIpWf5w} z`aJht`!P$3psdeQWOxG@;oTs& z)5TB`){K}Im&uRy#4xskGI%861u6R4RXZ)i0(=*NnDTb{^1+y~cgWe9psEIVJ|VfY z=AqVyCw?`z_beS7!d3i$!+<$z5XDlMe^<|qt`LLni1Z4v4c-7T0-*ly7J=CKdGS4V z9kZWFV36|Jzz&=6w$ft^?t7RI6Q6~)ZgmE?6^OWfDT3qj56U`(I>BPs!z1c85H75$ z&fivJLmVpD1oI6%7iHt8OZjPh>?-r1YfvYI;sS5V% z1Y6pP>htCb7{##C($eRjdy0uI27JOqS7P+rgszUQZMYf0TDkgRgy|iU#{$FS2Si<( zhLArp8II{vtfmYi2Cu)I0;W2sn@m<4^E>Bay>1LK1@;&SUGPd4dMeP^NEr2uwE1&5 zIuEV$z%z+z9?NoMF}L2V#rO+eAbe%M3YnKMu~tPT4S8w=G@wKmai-Vq(xk+x6E@%!2T@Da(L6URI#v}pTq10AO8@<>QhU4keu+%LhtBwgiXA+FZnZfQ7 zV$6E+kz5ohMoA&Sv_kweB0_cS@UtVufTeAygpeM5@cFMe2OpJ_`ppssNpyU@rK%^*eQ+b)IUlGSS zqC*F|U9qk?*s2!MO?{GzYC;jhPoV<)6dx5;uSMv%T;&a7)fw(gke;s;kPSbo;-cJS+TkB9E#eW^?sqBU7=&dp%csXi@1KCTn5|JVx914R8i?WMF_woCLVNJ ziGucw6N}B0VDPgHB!bVnAlIMX=wEpYTbH^m*n3w3ATtDVO^+Q1pq9FWoo#5zNuyNu zJXQsdB@eJpXjE+n;^9`a2rL^OVvrxtI*(ir+E~2?ak~eKa+@Ot2F~!zM}TJ0`X|tt zx(9&HU<+iXFM;5OytD&I$AbICq;zk2xBBh!pjFAGnY$Ly0wn6Yd{PwXUN5m2z~$7= z{iPJg`wk?ua<(u&+fHc1V18A@ssa5-SC302Zk#Ed6%mwu$XfRIE8swI+Mj>M3OL{o z^TOXRBj1$2sB{v?L7?;J2PB7KVfphf|F?hAA%&n*l(klMumueFVNi$S9NDjJ+EgEo zLnZ^jz*dsjA_h)Rl>gP zT)bYhyD2^}4vuo7X+)4&9xSG^)P% z2%f=PD#+h74oPlsF(rr zi~Qt%A=b|UU1S@&=#ePw+zIfQPuDPN)<7{hC-zfk3v#JTPVjld8R}X00QT969w1`{ zO+Nu`F^7g0p)F;^EOw^3KG4@vu;o^*=pZT>Of)t2LZJ`tp$#ORT;T6bEsi8G{A^C& zelkHI?Ld?RJx$ER%VZ5O`qKW@dozsIS+&S=7kB^-MTOv6Oye>EmU-0De^a=1^d!=J?o4TFElL+dG_sMrx`Uo#gySi)6@ zi>&B*By*WIchQ~#^jWd--P_nKEGjE-*VOQL>+q4WQS)2Hz|t}CX~_o)whg311V1v) z@EAWg6kNyyA(Gq>8lvlK2DWUk)mIhIX9_Pv=Ju1s_trLX@h88VbhV*P zSme$0kxAnp72nx$RM~@nkfwYW_fs-S^uP7Rq8v@fpYg?`x7}@)=nnwP{ zibk;I7FR2P;%Ro*$Rti7QCJh|lB}Y0a@k5$c6TRa z(OIENd14cbWg53RL5NR9<_9Z-orOH+r5LOXCcbYQsr)R_0}v`?Esj}=aohz=re#O` zzR;?`?o#CGtH?+CbNsVPXf_V}D+!JS=$LLRKx_;$)Pap%Q6w{;J!S_+u5|`^G1HWe zwAXd@H9y%#X<36o@CtR`Rt#n1BBe8}t((Z^ zWo5K!D+DsyyUV-~g5p8S1I7>{*qDpTd}QJ0Sxd(KYxUT^egED!-+Jb#BhMGT8?lAh zChlb?4u&{RoVBo4f-r?4}S}rsd!(_<$E7SF!NNDl>HOj!|}U)TeImmcA7zZ z(z5dQ(bNT=%V;L*Q8y4%8`USqqFqw<(YCngNJFgegEG9ACjM3|6nFa$n>-2X^BG@P zthOdg5p;0shbEp*M6EsT+<7X)N-?mN~w(=pH zIZyQv*|$P1scUJl*srD)(_;k2SYDy9L9`!k!b}6jLri3R9w3o>ft?p<-kq6-g-=O* z``67K!h*9qh90Zb9Gc5wcAk|TiZimZl$TR)QE~W7%v?Ef{ahWFj?4`$Eu9n5Cm!Eo zxi#WvV5Zaer7$x0O2iG@H*aPvVm=;X;_FbV`f*7%tI)@j>Xi{RRi<2RLP@(m)pX&) z1414v{RQ`yF-xyZA=HTe;-~f6r!oToq1D|nCzOd>NL2m5^6f* zxXZ4Ud{}DdyAsKq<%&i!43~_OL}5AataMwFN@(0lq_ou6*Jqfl--@T0B&~4-8=L#g zhZl(*-S8ipy=xLuXZDSGuGS%^Aklf>|7>-T?P+hex~+)mk?mNIH&m?h3_9n?nEcbI z&VCF98z7ZiO}k4OS`Qr}i5pxSJSMZBlHapxX^tXiRSSsTdC+)Il!4MXw9PYiop1gs zx2~$>@t$Tc<|ZRqvP}o21i}1gEp}kX%+O@)O#VQ^Sb2P|L8HpAVF;*0)GG&rgHqWo z#)MDGo{@ONIlP?`yQrejG9jJNv}8A6U2^Yu`0@Ltr@h1;c->(s4Qoih_+kATZ-{m^ z09qutVD<5VIqR-MvRLA)%Z>R3ZNi*1x^HgDcTYpNGM{}DKJ^2U;8)+7WS^ZQD63QC zB)74Q3+$C9H9H%Yjh4HxC;r?ubne#d3uln7+s)H0;SyP>Jy-yF*eRY#K}&X`be;>6 z>%HiD74l9!G`WhIvP=?lkd!?wsQZNN)-8958e)*XXotFR&3=6L`TAnNW7CH0v zE^*tGe6#x1k43KxYs4z&fyI@bdwL#Plq1dHI;H09)0w{o`QCj{!^XEJ2o-o*LA^WT z3}Fj8-ELzObzWn9;JBf;0#w&0yWX&PjI060rvE~BO24DLB4?$IV<(Um!avc&HY=SEC8!h(EV6rhUh#lYRW zT>~xNdN0`FlMR`IKccgbPiLC^z4kW$Lj-j~s967*4`+;1WGeHy&Z&CPAJ*5@LmmG# z%zCI&Xt3s)Dcbc%?r3Xkf3d#|cO{>_fw=Qj*H7-z7ocu81${*eM2Z@UDb2`uMzz^L zJ!hCiNF#V2wW=I#S6?-`f&6d#)Wj$TY7v_2_OCv!rM5sEb=`bhyg=UQ8mN=0o0h<7 zMzbLr+~+;DloL+o>6N`1w}hMj=|UXPvosQtZ?20^ZofS3lLz{NSj6w|gT!cc`-G)g zc)}^hz?1r>V71xDXYj2kM(^|TZ^JY|%IedMcE})4lNNK6DrC9H7vEVNwE|@Adp=3U zBk+sbIzgt}Q|~AUa6Ih{WyMN^jhv&6Jb-_`k#zr5WoK2Nt z8i0U7hHn<}$%Cxumvial26RAQeR8fg&c;6YzW!$DLQdQGSW zpDBrt-)=fBAK3*Kz!kY$yP(LyfBE&D(Ccy$dO3Gq#4eJsl7`uxom`UVl+hw;oLB-=G{Sjjn96FuI?R;L5&ZN< zC1ch(iahn`G$Lt&yS(fqAC+!E>Q3qlO>e1F2MKjb{citQUbrXXL7vZ>5F%??L~xn) zc?y+)*#i})ga=*?N$iE6p53PTF#IsLEMqk|O116XHy`Em<{4*cx2r*iZ1Ax<_lFL3 zbj|pkFOyzh9aCE+IVY~pONumW&wCj=1bw>{EY@A*9~g8C|A)8%K~L8wx67Fe2F(7W zJFe{?3uXxN$(;{E~6N z&l?W^Sk>QvYD4G$L|Q|zp!{<@a|WcNf`4A$EePv zpT1LTNJ4?;D?0|GIt~skG13_BcQ580-Om=WR4e>?ShyLdD{A=w;D6$J5{WvD-K%Zi#nm|-&q>RW1;BQ35~xL9ZE+Ea`wWG)-p7vlV~>|S zCpdXdP$eJoyA_9p+CW|EdJq&-A1fTpsC;&FvSvW%UOR;%+_9fQr!4zP+ohsL2J~0X9#4Kf=mq;nQzU(PFVM?4mQd{S z{=3Efm_+l4C&gT>>|ImAw80z?jWJtW)nCjA#Q?bN1a9>4(P$IWyl-RcCQbEA@(R+g zTtG=GK;DDg{yY=3Mqutocy|3~X)ZMFD|Za!EvQz>Bh#oyzg^4;0^*8kX6}_l&!6v% z)Jku>_0PT~p~^==VZ|H=gcE&gac|E_P$SNDTORHy7O)dtw*>EV)o%Qhn4>q^V}iVH zB>^My$#DEcC!AI`w6wOrU4D?oA&E;FXJfVHP>a(+M+CBV< zGL;nk-rUX17r>hJp2Wj$2LQ-yf4E*h+qk_3^(aSZfv^qis0=l|IiR0oEIvx1YUK`={`2II$`SVHw9%rMDFyuZSn9Y{g>%Hr7GB ziiK0Z(%Wtt#>Lx|vuhOFx5xB$Zuoi+YxHwVD#3i$EwwHz#MIoQS!NSNBSf0IlMR(8 z5(1fVINyN(=&@r&GbJ*744g*2tftC*!qu0+(CU>FV0`zZO?iP@{C2O$^8J%G zcuV5rwwR@$96=?kMyk06V)6mfyZH_Q{*2YWrn2kc(9l~vJ+k%c7rmP_3QZNQf_)a39rPiHO zX1mS!7e=jvNx^A``k42{_y*3xldN2feeb5_SL{PQ_2l$g`4)#FyQmcK<<$s1-4sSm z#xb!kz#k+K=Oajo>nw;k9;j_8bkyC7Jojso4|6N*T)pK8fsH?Pq#&@t2&$5nPO29B z^W!Lp#r}w@HGXqd&5uxRAS7CSb$<|*N10D2L56)N(CMTf$v!18CW<7_VX-swxpc+@ z{HOvwq&;QNC`jd&L`ix0ANRMM7|M`OHEEKT3Y!Qweca=FEnrEtm+A`)9T-kcY#vBb zJr;cNDtW^An6U9_{Nt<4Bd6tCkB2V@CXPa-pjddCS@M#mj_Sh)JKvy1BCaZz?+y2z zw$zx;mpV6lL}^n-t398An<55o<49pLT7q=t+IQuADq?3 zjlmtEDJ23n+co#A9^_VUOoa#aL$lU9@KEvQWV;x2KUH3Cd*8PU$Z05s9M5C0GtL{Z z_|y=(P1*c*j+~Oy=C=0A3~&`mH?9+(x+qvEvI)!o75E*3?N8>k8CPcHWa?hQmObOs zuyb|h_AM#scH}#U8iv$o_#g#__+GapF0GS_IoOY-KKDPuAny^+c@Iv~{{Mu1J@Ehh zGqW>Rl+^G4&MZ$`3x&|{AF1Jhi(|EGU4xm&Y)pqxz9Vmz>N8DanO MvPv?!QaAknKg-}_IZ4j>e)s;gef*UbWof7xsmaL5XdcSR zsFIOU(vXoI$va93jteTE;x4)>ops9C>saDHZIOs4S6!N$Vc!NTl0tCO*vy_vPu zEgrs`Ja@QQO&uI;?1g!GE&uo5;IXzd;jPwnY=fg5vys!bCnG!Kfc$r8^d*Z6*&#Br zhcZ&?EV*fcz0I$gux>BbHErAO4gg$jcN z`s<#SiK-#_Z{ps+5c#o3axttViH+U0e>v0g!}4XDs~Af#U%ltpE&Gw77DxRU^M~uA z?YH0q{Cg-bhH`Al{;$80-8ll^_P@t#>5l*P$N%3yE%~`KMdj+_$B%Qhb6*;3V@T%3 zeY5Q4$;{?`8IApf`x6`~xWhZ~kHdNJMEUQxGnEZL)Wk0EGfkI(|x7u~?NZ z6?nKSRp=fflk-HrhurDo=eV+BrQZ!!Q?XcRq^W1yO*d)R`q5Zcy*_l?sO|)uhyd!! zn#;-cy;_ErsdL!M13P+w9qor-KhClb`;r|;PQbN7ReweJ`QEBMT5M|>O&Us)|MvI5 zFiHM``CwkEyFs~IemQZi8Q+<0B52b>K0n%Uw<4{R4L*xH;(C4Hf|2%4iSiR5gC6?J}ibc zr*GsG6B~(QM?MLI53Is#*)1*dB1?JQi@vntHuPggbsw`o@RT&5G&AP;Y2>4tP8Y1M4n%#Zu4{?>zVg!?+`BSF2%dk zXZQYo;MJp88q7CeBCfY9nMT4ICL@zvxMB+b|1wzNSvumrT#jF!sC_qNTYrhccF1jW z;m>5S1pYwt(=+^Vv38&NQKmq4>@iyBr9tDuQMdGqXYQ?E>A~m^wF%?6_Ezgz;*)06 zu@!cQj#01+{F3iY)6kC;IF#TfGbM2P?|18pdA7hF97;XqsVtN2oAT|yA2iR~MTXc8 z6aG6l(R29IOXv({*QwJC5o)|F`jr!G z5+hm9eDyt6e;1Gr_DUy5;_e^6DBaJ9w%M?_5dzKR?N<3zhUW~ zTCK#q80?y*t;y}lVCT%)tF8KHek8Si7b@KmcWB8Yl|*lTgZ2775k(>6s4y2{8sk7a z@2(>4`TLesW>C!GtZ+lGVjN#Qu3h56{_bXOy97z}Iimv+Q#`{cGWAJ%xOk(IGe~%W zqjYEH`x0Rc9qe9tus233S*9ru?o!Epsel_TFD-}V_@}^g>yOEZ__V-}b{e7K@d7o; zyCFg072$)0*2{SGlJBTey&atVyNWllW5@PS&WX;IN{^YiKGfo(yIj9MwBE=bQ$*<6 zKZh3?BxRUm%Wn_umf}mAGewrm)-zbKDxM~D1M|6FyYtEKmkIO9F(sA`r26&pX>>3L z_pU-W;Y-lB3Z-67+tt6ncW!^P&G^cJ)0-DcR!w}QS!`!a@Z37sTdvPnA|w0x$(4+Z z^@c)>xPHE`f&ZE7Wt+JE`i;nQ+&V@5CoTyKG_R?|KPa9yiE<3?oLuDcUl^B zHEs&AC1I6geF%R{V%~a=x#NrLp{UspLGfuc?@kIVL@pf(*IDXpZM{5Nu5ED3en!3! z^Q_iybgnagWXy4)XB9ag8kstRD}(P?yzUIm?qCCj(&O5@%r|!B%#P)ob+avhl0JFN zV-%{|;w7hnX9jL=C0cN%m-}?B9h*g*@jsUG56syVBBw&Rb^FqvWXBCv?r!3!`l8_E zXdBqiMmf7(5_UM6A+b9bpDQ}*Nf==wtQQPf*<+VR%iVujM!WnyRv5U|nrxr2zamiz zUv_lj9Si=F%eXhszK&D3f{<%5y~n5~)*#9_>R=gL3g2qPKL1tk@GR#gwPZ!dAvmP7 zH?G2Jqy(>*XYr@&k}fx|en}o-=DW4uJ~5P2ixPiG?BtzsGQxo{%faTw9~jgA->_nr z`!uiyBi}`wt->4+_x0{j4&%ZPxW`sKhZn z97Xi(5=(Kt2EuG~Mo_jB){Lz>o+5E5oaavylu<9^LkSzaW0h-0dTaggC~NoQ&-4yPpUsS!m+KI=p?Q(emq2_2Kdd zq30%iD824)|F~;^7zgD>32V|G?fobo$}4BHxuM7E{X#|heIFqoj_;pFGQg+#kzr7sp|mpQgP=Pkojh7EixIia{SV z<``hLewU3JhE5-6;AzP@fKo=N3%odMIb6iI;FxXF&a9=4Go*uB8u&ydWnVeb*hVMD zZ6Q^%Lz>M6l%d*WTyF2(V4MXz5Md=quGD|ZN>8v(KErk2WwF|7xy7&(YIoKpU}IRw(MgwmR>?=6W_D22ITPv zKQx@gIq3Y%3hhp`CqXAM(e_$MO)i-hJ}JCaO~}L?-&Zu-j}$a7^Qm|+h9>Px-glc@ zJ(t)@>ow>3JQ3GHI_R`Lb`agt(&FHqX%g+4)Zs%(tvFxeN?ffUnfOTy$7FGe$b~uN=2${z0J7XhROqcK&DPSntonM`(ym&Y64g*>lTdP?5bQ zg0PTKXKGhDQ~hWzhGC`1RV1(KCB|jwHp2K2!&+q2NDbstoDRAUV_Pa&- z0>m@cmGIbJDO$Evc^hNUA1@pH-mL3@N^4=)!O*;1=Y29YI!1l?qo5Hq}=-ZFwuU|7Pv= zrfXrdVv>LQV*+i&&Jve3@}p_Bs-wak?(g!nG+PIcH60_c|FH4V#g08B zBm3kL(M?A7`r0P!h|<|{|6u#S*6FK@wW+#=n2Qo3!#KYa7ycf2K~p*0Jlp!+M1n-j zb=Qv(_pGT%uB)>e$cnvK=-L@~|5@P$VF%L!W5(|~Mgh$M^qg1lU+bJ`2`h%*85p$; z9Z8HI_9Bde8Vd}J_6hhAk$OU>9@&J$LeGu*{bugKWnM-l(H``{I!mG9NQtApQR1FM z^i+JXc#gvN+uCv4{W=1S-}NSgkUeSTbvTMeOnh%Kw<=BZiV+uEGko9k90#Va~B2J4(hp8gT zSqC2TmQO|VmTz>ypVVsTy|MIO+7UWXZIZ=x3)GEzq%$(kX|pDb5_CCWcCve zRb#p5%+=S)=1Lz$d#p8DVAsQcuj$Ws>ph6%m0G{?=6mjC6@kh}dDFXMO$`kkQ|gDv zUjKO0z6n3xbieUFc+-%*R@i$ae1geir5a<$!G!+XkoY*9GR$+mRmi*1xK<-=kN8ku z@N|OV-TCfc(UC2;nG~omGO->rY5Q?kQ<>Np19M|T%d1beVut8DDB3AUZ z1LvPzagBaoy?x|Avo&mCll4oTitXv=OM#ePqx@GpdjPbuI5&?^H;0?JS|$Y4KH;P< zwD|Kky7%7t2i}T?)%D&ufe@5+oQ6OnDR4t=oA^jqLXu7~&++b(krY1Wu=J}78Rveb zC9L~WrrC5k{&b+y8h%)8H@&#tE>Q?ebHo7JxjSi}Fe0tqm=K25L4XOTTqnjKC|&7m zZy$xU*Xo=$`P*89(||woMes#eb(_^Sh>wdjx9+O^PB2s%EOEp|__BZAs>MpSnR(7R zu0pdbX)>!M1n3wGnfJ9sJ*Ls3jH);W(jhC42gjv(04xfNH&A!j%7mf>Y-#mT!Xg}_w(`R#Zke>@0UIb?K2a(3?r>`H=kMm~&6}4n zqnvG28F_RH~EUlHf;OU7x{v7zdof`h4_JD5cYd7$N}b;ZNk* z%GcjZT)USSmeAQpq4i}?8k-V|BgGuQv0wz6M%W|b{s+xxpIbsNcwHZc3&LVroI&RE z17Q3!PLg?VnlY^eG3#QGY^F9lpnad?X%4^$%k*ukDZ)6?PN4C{v%_P+P3%Jp)2~Wj>a>^McHl=GBdiGKrSGP^BdU!>2f1?{;Y*^{F)o?Yq=$V50p*gLR`?zPy zrw~yk0>9_NER0fflGf=(J61RrJQD-7B|gzDollqb#?a{-r8V@OQr*R9m^;zz ztq&Ll{|=#)$o%&08(#e5FVZOONFl6NnQZo`;4nSGo9nukQJ9oskARO}c$wZ{hYYWQ?dPAXmlHgt;C0!%M)raKeo)GDJlu?B$ys^Ys zyCiAt>cA4g?)|v|Aj~m#CyvMt)=BUz>~t2{PKqYdlaWn9d4e)?BR{WaV8(8_oIuI; znr0SslH++#dY%k@c4JKGods6&!?GB4BN{=%(`Gi-R5Ud4k6xfXRpK5wYeaKKoWbO1 zXI|TH+8~}CCTHzZX3bVU?jRwVa9L8IGl}g=ug#4}(L2o5{T~grwFi8H9~X@ZrVZ}_ z46CWo%v|4(6>}K1lO_bQ*=iVntMdecf#W{C$6_@mh%a<|e^!=}&aigljjC~0T>!y1 z!&R(qm)QlES@>5m2EF{tZES9LZMppz9ow6c8E-0Po6sf3(d)x61|#3-)~=Y61cg2r zK4dXF%G%mHfi_5}vh{i|r2lzpvky8dH{U(<%Y~jFyC=3i?yC|WV^ebskBDN8 zZ~rkVi>ZdsTUhjdGfk9D*F*N=fQ0czHI1G4M~WKfGPHB+Xv$AnZqw4Y2)-;q@%!lJ zy*m6@geWgv-ZI8O7>@#4P$oWrx}zhB-=l;!SycjChH)FhA?`zq0BLSc}%#ODN- z_noICV)wqYsGpuw(#(t%S*yW*be{|qIvi@mNO?`qno{H1h<&@5)t%6%)Ow~Ij4CAp zL0(tpas`ikG@XkWe|B5oMbkHv=y{z)zxddX0{AjF&bjDgnNzCf+WO5pEP<$#f}NRu z%^fSZfNy2epR2s0&E2;j@IYdTIzT4!XThD0ViMq3a*`GnRVkzp%q;fYDs+j(NTH62 z>5j5>yqc1j;NNXr+JMGW9)~#wPo-g?5qN8wR=V`>Rwa!i@r)!xg24(C^#XT-xzgDp zYN2|+nd2**vGjVH)0u~_91ygN#?EMPaBgQT3(}~am6z_Ai|qDf@L2lIIqGo#OaB;i zlq^Zl{Ng3t_zrbXxAfO4T&eQ9sHxYn)|fBv&DEuA%+R5Vt(iY36UX9R<_Y@i4)6V* zToiG#2s28u&lo)s*&=doVLrv^T9eQbf@C)#-OJrBZL!~0ll8^gRHOH^pq@QRd*2l2 zgrL$Ha)ci@0F8NQiO3UG-sBpCPQm=fO)l@4-BS81Jl(N6`LDdEL2y~|+L;c|TIPGz z!%^6(Z4xyNYER?-NQU+;0yAI?M-s1LMVXNOOiLCPJ?>3~=oZCGd7gky%dQ69sX>&? z@-x>T6;0Dzyxp?KSgutxUEL-@@+b`KV!-FGzSP*zc2k)1l6l;mr0~5A)Zu|lrN@ug zkS!| z^WFV%U)r)S*B_TGh44gu03uyVDzqHxyD08aw5Qvzm;w@z8?a_tkDxXJqAh`-;e{Xe z1Mca_=Zx3Gf>Lj_Pp_r-**L20YZ#9BpmeMzk#BI;YOF%6Ip3v2ii!ere}n+k_Qfxw z2(`%PVh4@C#@I)E16 zxxFZszAQ>An{!thLv!oo9rfVt1KoKKKG5I6%GJqwsx9fU>4W@*nB?^~k@lfNYr|jt zm7ry6R1zRYlN6As(eYFsrCfiXn!F-7Dj?W2(J}Risau z>6vb#I*NWX8c1b65Inh0+*v1|)Rc|u`^Xp|^|d^0I+4W8SaL6u`)YokI;V7gP32-v z^YB0|r)Eah{SW7g!=f3}buyoWM0oY_{a2e<+m>FGX1(TKQeQ3OmaO!I>EH43hivN~ zbUM@Wv)N|#Dm;o?qs4~AHvX(qM^}ymj`%pBHa4@lJyeA_j-!rH& zyYcZ>Tb4}MDJe#NnQ{tZY&`K;fFP5fM4MAW^i^S#$f*KAQy7gR9pUue>`?;f;Wn9> zMzQ_r404Fdm?bIf)X8?Fm3FWY^ypzvO*k3DcV=JFj3q0vIHRe88x4NUiO9}sh?({P z)N4LO#q<(|c^`1o~ZsUz!c$pgpAxk72NzMkgJnD193 zVQrTBM~6zNEFW(zk1zA{>+UXN1(b6)@rK=}IT_&VK8X{&0!*(_;qf$vZx+?H6DOXl z?|fyMlp!ZOB~SOjT&_Y_;?9W7}kQx`x9YuyM4VoT{e<-*7Y2V}i4C3^HS zyykR)^_Y0+WRS?Pw2`>^i8;i0$j%iEh_A~-18(OM6BFxvNI`b?P-N*$%dK`q3M?+> zngWd|J53{?h0L)Xp2aqUZo(szR}136BOxxkJq(7;N$+!pOJ?A-(#EQ-wF8dFaIRZ|Ndxm z6ge;dJ^tUP|2q%=H=dK%S9rO(TT2|~zZn0qEF75$YmXFc4k#sxo(p6Yv!+0O8u1SYvqR=wWM}?Y288e|$D4q1CAxMCecpOBuc!%2)JVwD5ac<&HlG6DXjkF7(4x@*BXd@$NUNm9$VK~a9MQ0C0$K^*LjRSNCEDkhv<`K zKtIAqYE4`KzQqAZD#M($2h=Thz!FREuLd;~w1SjiIawd9B)9v?rHP&0-L;Bk2E|)j zeA{B6V-s};ylBK6wUSE#kzd%b`xx1dKUmM#66usgJH@OA^KLZTZOSMYwt;*$n%|+^ zKLe=hk6CimOGB?6WA8-SOi4dl7AvSPh1B$@Ctwnfj5RF+hCwYx{CLz}d9cabA5Z~^ zs;}Ig@SZzm3qN8`$F7)nHCVI@rG;7KZ*NjY*~DpTPqcg&YKQAMtCNYllXBCk@p||G z{I%Aq(zNpmt~37K;}7D}W}GQTN$U;>Qi?u;KsS9G7SM>-MR}9>%+b5^?4>|dGNs?0 z#1mf)pp5fCvVFb>3nAAtdQ|HhK0Zje$r9V%v)aLjo=*(t>U8i@6rR7vaPxh=55@5e z>#+(v)1H2WG)wkJ!bMSo5TIv?H7^EF zU<;7TgQ%n`H4e(mvSU0hl)U^?33HevXz?Sr@J@6tsDRg zcTp#kv(f|L?Vw}7{;XfkG1<_*dS>?wcXVPU80%JZDLy~>8(n9kX4k$88!QbK7S6_? z#b-QzH1TNZJBEV&S(m6W3p@X%(^7u_Ai%twa@cb2!*>CHM~U>Ilx~pvK^spve^|~; zRa|H9OmMLHZbRsVL(tml`O+)Kc|O_?ssIum0dx(4?A=#Se?HWB@uev;Xs2&y?x2lZuew^ z879!BNlwyMN{O88VPpG1>S4Z!5)A69?XYcqF`shi&Mrt^i>*RqdEm{okGkQw!&94- znyOelH#!SI+Z362L$}zYrsPQR5l-WuQI|=Y@Et8SRNg(qZ(bTLh7d0(c>bFjiI*$^ z^0cSNG-Xf)J7WpzNrv9<;h7=h@t18s=H2w>%A>(&=USEkDOAf;rB317ZH-UqWO00J zjwcXfeh|RWsR6*+#XQ_i^66)E-bVfYz)*1Rd+nE!+!d%h*jT;wsx#%_9MAzjB zXd1ypc~Y>%fhn9u)1c*8qfL;ob)nDdOFa(7g9YUIlyufZCAK};{9b)2EVbJp`i55z zLs+4?B~4=p;O?{32h#SbPh%&OStF@+z|qQ`?@k`V=$Gm}Q+oT(rNj;~lJL9Ts~w(k z?GeI`Y0U#TVYRB?$+)3q<5--=5N9r?@Aa;GSHeT5&if3;dthd~Eb^)JxrUO#dD_MK z#I$XKSu%+~WZdQ0=GcD2v*e{3jWCYhy5vG&GWCZU@Ka-B3D!{agCTdirgT zjodVOC`|}0GpM~`1TArWR|k!IoGWnwZD8m2;Y*jr84^?`Sq0Bhx5eL}O%V-??v1$b zDn-GjQ)qR`34UcoIDA!JKC>)pZRM3|;@$E*bW`msQS`J4{Xj$GRO44z*ao>k#!A*jwJhi;nBhX} z7|!*%^SINT6i3zBG`>ANM6YkOFhbgyjVU!sv`bk=(lpbjG-lHI!q%qH#MO7@P_CR> z`09(+rS#*&i^0h+~C7G z?iw$}*|w-$u5J^v9Y`kG%g-WFo`Uk-h4kFap>lWUFoA`A;=UWWs<{?>yEts=G#EXb zV5x_5b0eXYLZ}v1cKc#3D^Cj*v77s_Dv=nqHqg>4lTu;Uk6p#)MTA}COR*|?;e=Eqbp~g`d^UUu2bMl0b z_*w=7m&?s-5bG(*Lt-3=<$t(2Sutv1R2J(BfTGOhhhv{MK3^DHP4FW^#m83==5;y? zs_RE9m#^rj3%_$avfiCuuWMaSzg2Mu@W$jIEg9L0#O|fm8MN*rV2EeFNdrdLDOhZW3Fu<`v?THc`ZuB*$v=~^G-S>Bn3*EPU0ALNpK_i4tgHOzXBt zrcd?un%K_NSF1gc;YG6KUGHCthfDj;vy}x+Jgqk9__20+N8?;a{#h&ixz*rMv;~q? z$-+67BLv-YeXW`I9k*5i=ntWre|O42)hIH;b>CeWD#YqK!>OVQGkNi%Xzk@Ue&aGE zQRs?lV75`cA9wq_!TT-!Q}ZIMVZz@3YWckm zGReOcuMg0E{KJ!wxLHL~3q_;L0h|~c>^^L!ftZxaHmry0jBn{3Ed!B0$;@v0yxVlS4b_1#1MKNgGq)J~01qOI_o%&Y1j`}IpQ&kdP9~gL@h6uQR-o?Egicnb= zPl2Hwnv+j{{{;)q*_)MO|M@YWDd!GWy{D|(6-RjxV{qUSn1J{~fzi4n6sHB=3tC62 zbv%XId)MlsHM2DRU{fjLlfvA?y@jE3Y`KeXsg6&iSbE+BTkdREPKgO zy(M&R;s}+;bI+-Q(<+KmG``!Ls@$stHN>WVI^%~LCt;@75;-c_z z4rMuyy0XL9>mT5We0!Xsq<_AIM$7MH=$Mr6lV<47wJS1YXLww=RA|s~_(H})h+40$HZhoq9HuZpe&3Z>az zukEzqx&G)y%pjXo{N#%;+X5zy zpxLjO_-8Cw`B7y_*sJpq%Bv?mC%Xss7J`_XSfo|=n41Mxhwa;o`ECfl2rRi{9^iY#=d{zpHHMYpJgTquIHY&8gcL@cvvN5&kO$g`X-D@KaxyhAu(Vv&!I_%Hs zeAFm1my}+5Riqp0ED#dnC$iQ|&d9?t>^R@v8jIt1cpv8iPv~el7{29{X2|<@r6Id4 zsj89~%YT6}S>-GLKfRlH&-l5_B-^W#hXV6cb|?h?gd4=SyydsuRT+bJQ<3!Wo3il> zfr82zMP(@|^;mI-Ib|f&sGlVEBw-a|1DV^C!nzmo@?Ds{}q?2*z;l@=`l{t0~yb1d-bzPPl6#R^FDaZHqGKV zW?113vEh4~9U7v&m^Jmu$@GjXsQ42s;=`D-wI~@^gbrbwEaUj&n}Q-|V}H)nqaIBk z3eDc|qGsIgH!k|AezS>OvhzN2LRBB!rxQ!n_{i+lF%A-qZDxz$X*}TiAJc;2dUg*7 z&hhU^9r~CY?behpqxCo(0$@D~kxo#KOV9B-58KLyEdkCWWG$})fhjhrq$ne09P1+1 z$ST6;lx7eu)>a&v4M`%#HD>qdpz>_al`MZwN&bcOB{sh9Yqr&NKm6J598YX70gx~v zjqRV(comu;kI|W*Q~5-8R!n&Z4J>l4Qh%4wJU=Y{TZye~^tGN#p#$F29$ zZ2{+W64Muq&0*wwi#ICU;Zaadf9M5zWXg#+=H_fGmdBB6j}6wfP`DdUe>Q#)oVQ)s z>GMi|rp+OEQ&m|uIN!X#q|vK)U`~Rx6KpPp)!a!k7fn!r#@Cyibur6t`+hNzIW+ z_6&Xj92q|l&NeV>Fi#vE~c)``C@B)0q^SiiG=${hVZE zN0&iCu_?DTbe*Q}pJR)B;zljISFUy4UTRuFI`y!wYu4whvXqRuZ#ZdxMb@vZ-&>t3 z4bf85Ze*!pSBbGF{P{7A zMrt9)G|BF9%k{*G=utkw`4E->lQ`Gk&-^T4Emg!P?|aKx*z1)-+#~J8%LAY?oN*E- zuhi&+?ESEPnN`t*AJSp&G-%?P7&j!Jh&n@UN|iX~h1Q&*(+2V$1T1?Ih)Qj_24 zNFp}Lv*i!K&G=(L9xYA~jas|V^nGjb&#mXbqx|;nLbm-mMkWgKjbYPi>NNdyf|YAY zIi{U)Sv#Lir@f!7pKl_h`sminmWTVUSBjTEFuoeobSSRO)~2{2+3) zU~p(P<#@V@LBn~dtq7kHTY0C_beuvwk0mXk|CCzn*_A)}=34TB7>7Wr2UA)*3S!hD zpSymQP+fBP)I5;RuSFly6k)2L!Yt-C09+2Y6%X76lm8L*v{zLsG^?6U)U4sUqpcTs zaQeHw!UmJ2oN5{4zFSIPr8w2*v_36}QPMImx{1x@hVW>7N!JKIOqupxgK}*7QfzFf z0M(nIZ#om4HnH;GsI}e7sm){6Q#$XYGSUeQPib}5VKrjqYmcWoe(Dm085ga&4yIF) zE4LW~lT#nPpy5q8mfQoM|8^ABB?<21Ry2tL&m7VH;x}qj4@dBa4jN3(p=h%6PO3bU z)Q^~q_3t9yQ&1FW_dzQXH_T5`R_1@(dWPf*gDUy4kq%0;4=IfEf2dSRQp6y-9p2wp zgSB7AERsuS_F+usyDh2t<4Gq9G4~zwwGJuKHKtB~#_S^`d2>`*>T=he^wo}vg?$Iw z6v-r4F7+(6303YOfpEEmhvPD1=~E1d+tg8te{UWYLV zpNr{^1EKjPyK^4BxDPOCSmQ*R9rpR}08EX3pianGZ_baDdusM>BcL?#EN7HuGj_~5 z>sC%KbSj^6RjX80`MQRxObSrfs7k+L=<2M&@#=j`86sbG*%oc~>5iDp+pANmN=oi% zw>tg|c#gK4fINF~*>|7It&xKE>n>(ds0NM88Geu4XeENNll#Bul!{yr_uQ0e_cBm+ zy?Kc|>VvWydirjt35}4H&9zM3LZ{G?rth4**okK$?|O6n&UNv9e9#}TmVuI(kL;|N zbL(7HGfL?wi}K4?Va@^^jnD1vehR@l9@F?|&OPt1_SQSW+5ZhLd7PURq|Y2D$A$_J0W6%SQP5^*Jb}GG zGZb=MP{+asFn}qje+D%0@hvDcy4F1@DtIx_t-dYU3&Yrgv37!G?G9*|WiSxo3fdpx zRbR$@D3AW2ImAevA*kYxgv-M$H@(~p%Spq<_T}(SuE3?&km;u=$Xbko&sppB;N{CC z0{BTT^(^QzEtsX?g&C@J!^#S0gy#cGNG3pX<%N6T)(*sk2}qbMoUI%KO77@y2E#?q zwMOJtEFw39j)9!W#->s$oSg ziA(RDUIYbi)v9du!EzNj?M73`jmag@;PLywz-gU%e9;w9OeX^`3bi3>azCi#h2?GC zr)8NY#I^>EM)4!1MvqF-_`j~d?Dn^b4zg}C+8jLDF&o31#i? zX^)`&y=03&Irx1_8pf$i!%Fc*9H=fcm`Y?Eg&w7DgEi|(`Roi|$o(su!W!6d%mbRs z>Fpi%R&(W$Qk?`pwTTlP=+XzY;@eM`!QHp3uTGv!Xz$FYlke}-cOID*go%kuochO+ zf?66z#PL6|aT@~(a~Zq=hp`)AeXQR zyN0052JJ`#3HEnJqAwmkYiv!1u$4HUi(eZpGrNYw)yqLy=`kNE$#1sjR7&tjTT>bH zBgnXC z-~uHRkWs%ViE;zivWr$lmn}xEARmuR>6Alrq-@K^@Fq@z2oSs@x;M)cEj!2e;elDu zmcc}mJ`8%<^?ySLZHn5AOIpLCHxMP-L!|N%GNrH%8WcV#Jt%x_2n2hYJ>ltE*&wEQ zyuQ9r$#Udv%Y~*IkN`5mKY#w5&hK=mq)ztZ`H4#o9a21&B_3S*a_~YEPyBlKs z9@{ohOSkOn9}(sTvW@f^?bGPqCqwIEFxzGC^gd3;fr8}PNZ_Pp6z$n-`X63RAmNe} z?k#BSpM(~Q(+p0p7PL`Of5F_}WK>Zaed7LzoMRM8d;ibl|I;*Ri`l6 zdlMP)`_5+zAfI@R2X|O!o4>e2YEzoFFJ2WjRvPiz(mId90-5e2BbN zu-s+!c{*5TQqhl*L)}pS?}(?Rd{3>%Al>++&}zhDcVnKy_5k@%38p)UzbyA4@l4d~ z7wb?nhNP%2y@18rmQw~;0bqFVbt_F$7+?R^Btw0gm%R~h%sStM6jm253 z;i67Z4nvIpz^-0|7MlY(S#40Y){4O3B ?R$>DdK`Pb-cz5^OF_gqnW8}@R86Cv@ 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 diff --git a/session_logs/comparison_20260506_plots/compare_spin.png b/session_logs/comparison_20260506_plots/compare_spin.png deleted file mode 100644 index f078288fa30d20473d68e1ce9fde31725f814812..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77727 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 diff --git a/session_logs/comparison_20260506_plots/compare_takeaways.png b/session_logs/comparison_20260506_plots/compare_takeaways.png deleted file mode 100644 index 8bdbf1d5df77f90a4cf160814bd3dab5fb9bd01b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 210370 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

hp0;Z+1Jjui7cEc#2N+oIzJlxOJ2s}i#MzgB zR$WykFZnN1IE>u%seBLm9ue|NENa0k;uQT%>qR z>T+?$sZ(t$>RG5+o}+Ci!OC)>Snul49YJ>&Ron+`<3uRD&Yu&oXpd!W00u>9*zc>+ zX?T8dk!~E&0*c^GK?&f#ox6LTc3f<%q}_>l2zeKvRt64$wly8fQV`R!0==^5pA)div7e;P?in51psDe(SKkZvx zi0$HGTmBQ-Dym zz4K8Y!+Pc!IqJtcKWPAvs(&`2Xzzb&Ls4)3Uz<`u(Ixxy=gFWDMZb214Ooq)fbuU| z7w8l5;Lg3hlf4CLEmkynz-^baeE^xw(&9 zIGW)0LD%sa_?%YY0!P$8!6m3)94e_*VUe7bMZdJOx3>q1LZ6xeAI9(yCiSAX;gtt= znwpyL8yDFI1_nxIBVGs&iU7Y)FbX9cvC*Z9#dS@Q1b~P;`rOKZSZ- zx&%r(*qmnUG$4b;zC9EMey+-?Vs|t=mwSlKhO+Jh!&RjQ_tPve3V_RJmOJT7te2yp zygCb%&xNnGga~+D@*ii1iN1c_4=6)4rC$(aSER(m+2uPt`T+=HEa}S&gkHI#4(S&_ zXMHG>$giYSEhK7bh7?tR1ZefEW%yk`rstw)%*lkE4 z5vwe4p-ToexrT!yABPk-D|zjfK?SwnqIN$haCJR9Wbpg^8Rj`vY7E4-q-C6(oarlY zC~U_a$L4sjJBq0oPZ90+9p$6_K5&+W&2bthqz!?BK~YeNnhxfoZc4x=gl7T%^y1o@ z1nAqTpb+rQ+YC@`s~NJC1Ow%1z%x;5adT4|w0y+3YtP7M=s<~IB-$;kCJReTQ$U5+ zL?8hq3Q!tN?N|E9%*Kk6CoD;YKt=jpFc(W&o&rFj!CMr@bpJ&oCYBKoP)RS#R9}Hv z)pV}O+bpEkd5>yqralgM4@m>iaKyr$62VLDor!X(K6C&C3%3&iR;tu*mY0@jkeXq| z_P>7Iy464qIH=J&^{!F64ek=Xv7GGtwVk@Zt!vRJ$*lDPK~bp`8Z{@+911VWLJ{1lhJX0 zdawqXP7SlhnZkza*4_8HvA~FEqM^M)bz|$~ZpdZw8qwEv85?1s- z@4zG@=^uBz1As3YC2y=j0zeD)z2qCm!4h_9w2@b~A*3Vf+7sp$2mppL&gbb`RQ$t>$eH;R zF87EJR2&E5xE@&M@O)31+umjw&7rBMw6p0CgxT7pI;jLUi7O&?@UVnWYyuUfK>exW z;8!W0VlZR(gT7#LxH@3gK?1RRkJy=26I27J+xr|{{yb=XFppZK9rIxxn^(m=#H$%BHRBK=} zZuC}(pvbWsxG9?Ue1Cqp0`(dM#j9zBq9A}AIzGtLD9r*z?wTPg2I&vaG(+pHt)mc3 zSZ_{RI&MSU5BB!L&}M)uvI+Qg?Yg}dSpPnJnbVGL%+X7O&U@ucG*Z#Zx>fI-B(42# zBe((r0~J{oVwONHGTQ=a2}|FDdFflG z)_=C%O>N6rNgE&GyZIHdwJ};4)o9mF48=MjZ5CF-jkLbP10s%bS}>t0123X8fU!=u z&<5nupqk6^f5&I0WpA6Wtvzq?kQ{{V&bO{j*E%VN2T1K(hhT5(H1#GuzZY9>gV_XH zg5|=9XRH}xcGeS_ubqjB$e+UjD4@)4n`PgPO&#KEwG})Mz>fW)#ugZd;^z)yM?WS?Ofam#lz-IQbj1Tw+O#H_V|9G1Jx%M&5vLhGs@M*MT#qqQL4=m_tL-a^ z8$t7%mey7Vdipn(k9sVFJ0fD!?i;aUr zIw;>(DAcXL$I)eEZDBz6uLlYPG+Yx>KKn1gzd+Otv7Mf)Ryg<_pb09#sYE0s=!8RV z6EM<4bHU%)gf7AKr=+YLsSS|maJ5-FNFF z`6R%{Q#RVhu^HHGpzO7Y&jcyWDM(@g?pg@&ywkx4G7mIGjGBtWKpFU-i&Wq!q3Bg< z(eK~JSJ&1u!JgY3zX6SV#}gQhL4O)UJ;Xq)o+0du$$-@fj7a2+;fYaAw-@{LMWRAN z?rx2jil4;^9e>h)VlMQrcmXFVhT!e{_f#^|N=m^2rT2mJfhl+tLIohz)*(0_&UU zE0oowrN0@(f}S8=qDr~aTpZ@|J19r%G++Qc?PL+GvkS!R8i)+SJs*A}X?qEb*FkU^ z>1}>H=4p9)jpOE+FbJVcZbNpl+w&&i0s|6TE6n(-xu zH9QUzl+vB3d<)sMjL{|8Yk*!y1(rr_0ojzaMI zzvKJADF�|8EAc;LHAF0syc7zaLu29}F3?1VInY0+4PgVLKS&U$w8cU2CL8{YbEd zG8ax(7DrKP)z~ax0+)sm*#tgOU0vOFUEp}|@$gPO{rB;)w59n!_eKl72A*NY{@|Wy za9XzrM@%4b({?8Q`s+vC70t?~5tK%t^X;n5KiwX6od5UcNfSq*g8tb$)d3wo+f`dg zNT{Yz>cj5`Ik1Ni7d|1O7_bNjeN__N&)RhpNHKm#kKk7`OY1Ga0pUgG^km zdn2>}JUOj_pw0{i3}%X>z*ls11HZuWuY~>3uSD5TH}-|yVxPZLj7@afE}`Mr*g4gN z9ZXFAjG0E#%RMOl1h7ikz&}+zTptT6=?nEhpdg9}qrO{(cNB z)|Z!;F$F4=M5EL7!STn=T&dC|I^#ciS7G$lisgl1hCzqp{OjT|<jDmy=v*ZQ2q>VVPO>G0`!AL)g-j2ry7BEW+gIT^I4{MjP z_f*9hcVefNO<~!Z*l_<=U}I>V1p#0B=mh#nysU)q`#+2J$J&%4X{yc}Q&qohefY`ns_vTvz*XJ#OIp~E0lAlLbP=riyWlK{5b zz{{X^V@u{GJW-Smp77HJIXOpJJym5F$F)5s>&R76W`7J-2Da?o>tHsfXJnLQ*?IM< zle-DZqSr`^L_)?Jm1+T_aPx~onIH^m+FW&D=;Y>*v7aTpui^TpZ#g9SUs4| zPomJ+2MS=Nld_~3``Xkz(ABlCA>g7PB%Qn1w0Ib&@Ja_t>;CJcE^t&^7i)j z{sne_Bta&}fBg7lVOd@}S~QJD$-pDNYW(h z#a8U4oqM+?@1T{np?rvJg8Sh#et2tkhI0lR+d2|6p1>{t7Cz4kOkCrJ@f|=ri#c;d zQV+wM4P)Ccc|@5uJ}P~P8_AU`&ryLONFNc3$+<%Z*MJ)7_`)qxhliJM@YBF-Cp>{evFELpQF*J)|Oowysil47l^o0?^1TEO;&oE^-`+)O`xxE_g2G0b+E5~nr%y$Z7%hU1*AZ!bh)%GLfFbMSv@!?%y z2K=+Hu$+A7V)gz-xqb1>R$;b%z=P~=4&wdZQB7gIM2DYw!WaxsIt-Vjq1K+kI zp&(T1{r>z3Foi<-xPe$qv)Sig9!z6=#>V_&sa5R&>>JPhjc)XYY>NJSlx1t#+50R^ zyc0qGd*-W6jz%$+7BP_ygfIz(i^FTR#w zY}4((V>)`--Hr?VfV3MD*fgGQ_^^NQ_e9+m(=0cAa%Xd#DQE|@AeyZ&Fp2qAe>?O>o8M;z3um4f4J{IoQye6dGdch$ zAFnHwt1Bry$nmeQEK?A+bQ&ewCx4k%Ni#rrZTGL&dm>dWHQ*tFv{FI-(`T+p5Alo{ z^G=LCa>!o*dxhKBch$6(zmIyW!V?O)O0?#OcI!A?%Z$*p@|hW2 zmmw*S4k39i$z;+}Mb`zN^^gPJcuHD*-miMmLLL+5Y0ExNPG;~#rCApRZSMO+n*63{ zSg2aYa?wKHfmC%GH$4bWLh%-BngR;gbiibs=I=0sfdOnB!^aWr%eqBLY%&o~wd)nT z<mZ$G$@7==+>eQ?3FzPU+l?6<#3)8}N2SW~IgDsF*dUK_ye#{|`;{qu<9^z) zlE^^qw_hzuLAnT$$o>F4O>aS#8^@+#)r+DvMqlp`DQ$l0tuM~N#>hB6%Dg$Bd>-SQ z3mYkv+H(cYY(m?uG@9s#q-0~yLZ7&UhYQE-yN^NY+|M7EOcLc($l7>5h%y=q zeP65qbbyp@UpRkVcB`b<0RGw9ZgzwjEYO)Sym_&rSF^u)p`+Y~c@~F6w*3zWeHzP& zYLQcN!a@xhy*sB9727-0GW%SXVDwKWoBXRKf(2aZ=u_at6_L?Ba)Z#Z5sRVC|?Gx}z~ zSOn=IhL<4|rRZVNH8DJtY0hoi8OJ@mgR-FeSnas}n#SuwrTV!nL7h<-$O=9&%zd-0%#&{^z>Za&Uxwuo#j1MvmEEpG-zZ= zUCu`J7GF3OOxU^Nl8E{F)19;8&##C>w94K%8b_Zj5p~a=UKr3oK`BY2A9hsZij7m| zI@VoAug0kYGEoz0)lzGlWFG<7P<>xa1C^?0D7%%-S|@zh9rAiX>yNq?Gt4_ODz=lI z1-6E^8>1ncxhhYv>F5GOf-}B@FlqNFz37D#z4cGp+l^NZg}SE%Or6P&Thz0;>w?!h z*REZ=mIaoL4u65sTdh!SYshCkhsJ3>((~t4G>SGV%a|b#yane)k8~1D?%p66E~8U2 zR$Cr18ahzcTBsLXqN>H+%%l&BS=oxEJI82RimZxMP_{4I=jt1LC~$}!)Ad362^Ma~ z7V?;J)bjMqVW<*G$A&R`FIJ{BCNr*!ymnSo&~?cqbUX~8uz9tB{d zsJ}g1zdkN_L+3#1z(yqK!lp#~keJu{UeF_s*QZEei>is3N9Z&tn|8CfD_>@-(d+P^ zVjfHt`m>93gq&u2dUj#T0$+dN8S9wjpjFwqLu#R2WOl;?*!BmGqRbyDOaJ#c@$HBI ze{kYCc-G8MX~uqdlj4ScnrrZ?jIe(l4QhltjkHzT{xZ3-~Moz+`60!E%`Cui5DTi7-yq8pBG; z@51NiSb`|e)r^w7x~#qKuD+V^VzJSJSQLe}94}dXZg5j$5hf1IG!&{q)TRkx!6zY& z-#j2B7JQOz zdiY}$q_%4_D)Ndl|L{^jbpuG#VL(No-D?Ldkay6dOqmxIO3v?nn}G?d!j+ucqr)+0 zvT}!NQ(WH=Nj2kC18;r?}P5dq0ne`~BEMTpdrpL>Fi=H6E;3 zZttga*c@`NH6()eKr?;+{#6^DoGPXa6;vRVM8H)ZaI`0;b+=|@KLI^eV z=Tuif9=||Tqgx1xcyaQrbmp;K0k_q?!p89IprDzRFP=w%GdeW}yGWGt@W=ROer|3* zt5NBCSu0>azJ?W~D2v3UNMV=_eqzp)Yk!CDdzy9C%<#CMtJ9VkHt=enbrCzCGL%~N zC6s`NzV8d3psCiSJqVF98!AvuaJ4^uv$Lh-8TW3t=jz)%fa7l7DKk!AVlP_M&;+-h z@J$QLbcmSbF4vM}uH4##b(*|*`%SC9xj6(>r$iROrOTpiyeLHKhQovx7vG);Ue@gx zAHo|a={fc&j*he{`;!vu?|7nC@r8_va+M{XK5Wq-ixtL8A5*_w>&C6*B;9$mVW3oz z7x`nKlO-WB^GFq_dlqa1-s# zCS|L8CMGmj%vVXiFE7PfSRTR6294kBG?Y2QjBZQB)0EpF8(5Eo%-g7qH{lT-rP4Or63|NK)9UNV*)Nv zy~20%&$zY$UxT9)X|3d-87m~*ia+qS$h~h*ZR(cK*SAzpl;7E&{v(?*9%56QXdI5M zR3k4y6SJ$9RTvNhKdSC|6;x-_>Y$C58!fai6su#Pt zkfWMKAA{=;Z2S!3oVJHz^25M`k!|AP?w^d3H zBpB6fFoYZ<;W_K;CX1ec8k8XXM&yJ}z?4t9rpncY z+EEAIQw)skO`kI9)!65ZES1W+$qPoYiPcH9ZqXElqIu?fxied?b|zcr3SoPK~sVe%AdJgM4M(}jzy1xRAE zrM-t(aU+(~DTwFs1MF7S$I|V~cSzE!KvbQOpy|Sb+YTPNqf=H;gQEX4(#b?ukx}f) zWHZ&BC1a@7OkTNQ9dkZddrCFl7=^(L$Xh47bqSEBjay)#M-J={m4vsZvu+ z0KdT06{M6dfSG-{;hl|<;hNcBoVn#jKkS+eVJursGOabY)_yUx#XJ!?JCxd6@b};n zGN(MVw7z%rh%0iNM&Vij2ChKkNxW?Q3#sv85U#G3vWiP~(Wq?Sz@{CKpb=329{01; z+qXQraJIrvcRfY|LJj~pvNyDKhNJh8R~AEWx|#+%kONO1?KRpAF0_7+Cnd`khlG?z z%O$XDvB`1R91kOuypl9j1tGvvIlLOjm`3cL>QClu=GjYu_5H4B&6hl7?YZ~XNO$ITLTl{%wK3`6kryu2og zz4XEn47zDI0Wu^Sm;9yJUnp{B4A>beiR5{?;8bondAyy`T#uyp+WDFPBLBp=e&X%< zxD?Uag7-zX{sxz#>R05KT^tu&d6Q^6vq%6m+xUw_u1P3u>wM4SFA_PuprCH`Ibfuj z&E{fp=Bq)?)CyUG+IT1D0(O-694`5%8@%Lrzr+uNC3GgR2#3)+c{R57vm24r%_v76 z%k3Z*$bMfj)dQx=;WqGUeB}21u&^@gBgV7YEY=7Qeyqa%)fn_l`CKzjhJo}Z;Syee znrc#ylw5#83iT8ak1^6Aa=sJgUBhjDR8LYnSt&pEY1g~~e~Un>6e`Dj+k!QVtS(lU z+IJuuCLA}emfo1nBt0I^)zM-TIdL!YnALS%Tu43O?atzmk-0@;ZvJRuhyBcEq=%y( z70!J8cF%U}k)Mkj6Uh&vN1TB^ve1lG2y5vOW{As0>^0SVDmeV5nmJlwVBAC_u%q3~ zRgId(KHiq`CgIAuAy!qU+Rz>p)z~Z$I~#{r#Os3Iys zKVgEt7WwqH#;A$amTvc^jzvq_1!TaRZ|6Wu0^{<-wWqwUn!R9lrCtz8ULw-}-x z@2D^zpc*$?*OoMC{~^IgQn=Al&oaWQF8vP*Ju9?UJdk3vTts}L&Y5_4V8C6ALW_jN zwht0g@ZN-dh(1 zzkG6o!{=Fh%hC`RtA=d7;5y@m;mw&ouk#ajnTGceNNH) z8w6zB!OZdT1}-NNboOq$Zv$^B!q??)W4)h39;FVJ8g(5+ogT>9TJ#IKAR~y`>2*`{ z3STvd1yO%Vb+OqvXm|v!vXIeDZ0yv~(0kRG@`k3dpZCA3cX2N6f5Q{mI=6xz3d#?I zhwVtj^R&7_pD3oG=&e6->NLG01mFpt)%)QGA+P-@-Wt0r!TMXSOf-HxbD^{2evDD9 za!(&|g*TgjV20|}vy0)7zMhqhiTu@JJ7=_Ye#0I>_SUX!0J(90?;t&C?rqoW;_~vS zZ;u-ETKOE(tGOB>n;(5KmW_-QePEBRbjL46`316V{cax$A4D&vWDXdJEnIK{xMD1* zdY#xGj^i3k(ydP~(-PYQ%Lk2_@HBBjITLuYFMvZ5xSpI8M}}gDdx_owfRMTE3&*tq2?+XJ(5DmcBz`l(`yHL#u;T{hV&3$+tptOsvh z%AE=g%pSr@RdPG?ovdzcueNGDh7@Z5nF46_|1$-kp@2#OIR23W5Q-h?5PkjdIHkDQ z-n@In@C^n6N8KlM!TM0=#~2PWFbDbfF4hH7Lr$F0U<6K8`+ z)S0znW7wn$17qf#dR9vrl;t|uN=oOFXE42YK8Bx5r zs;KkFcmMSdgZDqONYp#O*A^AKhN#3VgFbOQD=zwR{6`yR{JqrshUTEorFcsyE`}I* z5Wk0^KKbA9^&Tk2`&V{-o;Pvj z>O6(YsIj7aIVx$^kyB&l=t(#JS_AyZQ3RY}fwG!u`r`7%bvavJ+}Ow~4=#;GJ2sTS z5)Dh{eT!|M;}e*;hPVT`%7IvNUwBJ zO}U7F0{hcJm69xGsY?WR*!*qWA1D308Npc!165$X^PUe-U~~cQ!2WXY*9-9J&>R0^ zSkT_Oc{2$L0veQ(9mq0JYB-?iOaZcvu-e)e9%pNrARW#UxPsXaKKd5F6XZkw+&bR^ zAN@nHQvVZ3u)1bu66@;fOg6`gQ5@Nkk-+V6U}dLKXyqWSDsg+v4KlY#tD;~E26 zs9TjsuG{I?&`%rUh4!;_Z(Z99h)^iv-(-!2yl|k;?gyf)Y#`<^_)y^WW*KzUGTSA| zv-RA^XzEDmh90ZNdSX@C6)Kh@H}j5W^ZWcl4K&HirBRu zXAKR@3t&5SzqfNwDw2*%Ne@qvWhU@$(BTWN&8;mZd#``BH%c(Q_La3EbWkl`BgOji z+y$j=xj6hOr<|=w1AymI909XsrV$Fqcd;D73TIY@^jW#l&0Nef0tUs` zEO}B8ma*5SijO8)Yt^A$9W|oAZj{t<>Rd`zuAJ>OUMc>^!S7>K>ezCxZNdjDR*A*P#pvNWpal@2r|X|N4temiK3{sMibGyAH3;DMK9 z#4OAt?sd0bhNk6{(YuEUs=w^i(jQY3N$cFE8qkat*wg&j)UUj zNC16K+L9Q8uQU}M`3e{Yi4tjPP2xkO2UgOn3H)N(m4fe6z{{uTSp*Yx)zy_`-|3r$ z1A06fCHr@y?%cly7urWQo)UlsNzl>*21aW( zGaX1Mh2lN74u|Pfx%!z^m`Z$>B?a=Jt_}xFi4xd^%t=5r)a?L-jMUa_Mm-O!XF(ZVHc@vh;Z zpNo&U^u_ZIruxM}z zaQC}4)_=Y`P-CeDXjvU7#f70{WE#B2>y<92x#_giE_dqMAec3vxvuHmW!CLh47#H? za#aUYICF7oc0dAZtu;D7OQdmT->qZB(&St%b$FZIeu^7)|37s>?yD>ogi}qA<&sCS z>i8Qyjh8cDxs(A<^N|q%;F|~%f>KTt&M>!pN1&Eio0EY{{6oq;U?UzU8ks2uk zk!e^!Wbl9l@PH`&fEJW?DF;R71BE9lEGvVmU;v~84KX7Ik*cby*kr?$o#AAKfd`_zs3-~W<_4?gpDY<4|4nl5!#Jur+!{$}zvK}05u+$Gk>LH~ z0l!wyi`Gr+#aWSxd0=2*iU7FsO#^xZK5OjRiq-pT$C`i-o*G5MOnOQS&P)T4JX22YKo)!Y zi3`ue_xhGHAXTsy780T`ibYx8v2visYKm;ASFU!y5u-i#77EdA@D%+lmyjkxC)Xb` z8=MMo)9)}q=S`Du^t_-J`UNzh;C`l<2m{3~TUT}-0iYi^2T%Ma zI3Pd*F@8+qcrN8wQNeaIMQZLVQTW1skz{ioqgoe^b5y~#MaM)J*Nj*Hae^c zrFw#ELjkv|-O?s&M)DLzS!%A)iMS#LY8q1(0j^wSZL{rbxJt4!l=P&yvNV+{G0}c& z3=U+>t}1i5*DTLN8x9DtpSp7jE0K3ih3* ztewIjW%kpL7*+CkRG5{xWD588K9gmQcIeRD>m>dF=rr1(NDO6&TKP6|u5YDK6N^NwNv z5rB>=CrfL@a-5M6Z!UT5!y@U0l^U#sWG(3h=7sRd!M?Q@QDccDIy< zre@Bx6Y{d&{=A2{2C?hiv%2)(Sot66%zTwdA-3mL7NUv#(cvz8!%2_3^Zd(r?DnpS zz7~_e&~%^&CM^Zi4WcWB+9Mi|n99jt9)%?d1NjSmNIRFpe?#T70@MU1oED3&Q#(@_ zs>tF7D}(gMgxg{fn{|r@b?-WN8eN0jkzjC$Z@aE;*+S#R;oP1+qe2<|5{G+yz3YM4 zF_g-r<AC(PGCG2}B?vF;?#Jn>(v3-NmU>RGz$QnwjR zorR^vVkz9aE@^I#7xM{jB&OmbS2FU>G zaQS@LPL>kLL9av8I*J-{m zJ|;cAG`U>BvP@j-t73{Ps{NVFP5T0ZINmRiiJpJ+r_(Kge$U~pmA9jtA?LcjKl|o8 zgK#e%wwTv%0Bl5WOTdPFTO^;v_Iu}4E~V)bWpno7y4cAMuq!qKJUTfAKngb=K}lC& z#+0^gv{Xh0JiSGC&VdKQny-oM>iZM{!wl~DGo!2REPmTugLb6T`s4`zwIk&LJa1bl z{$tTl2494xhCSSf#b*7THrv{22U}UhMi@cV@G9QG`IDzfg++Jgw=)6bkv{g|62s`7 z-fTH>=P^bngK3S>O=dLf`AHEg9yj`lczIl&^DCvJ0swb9eE(VZnbIaMM^S4r_D*3F zlbSl;)#$-A;Yyrhw5PEd-c0|}^o8dh(^q$PEr1g?KD)Y>PdAqolf+=lEgaUZAnMQJ znM&n`*h!(k>YtQ1BZ+#98`m};Yq$aM)*OI45y&<52sQja$f4u2jzcbv>>8PF^T>~2SXrH?o6=Z+mw*>Af1Yd8NQO01`Ks?;JP z^y`Vhy13p`CxU6ewy7wZE}*k5*G|OZ#a{zlsV8oZyEQjwcaDA*u8>=He1mFbqUFPE zp~q(bGnW4yy~sq3Vhn|+!WkJ@o&hL=_wg~G3?fV_xWKfC1K43;_7u~R4^%=+%EP+LRkRKl+7f~<2nF70{GqNr98{~=m3PZYn+V+znAMp= zBY@AUKfx@h@o{`OZ*UT`|2oWHz9pi^g$%X;h?St}ZQa`f<&|{*b&$8| z!O&0;wO^V`MYaVv_MS!=G^xNf!9XhgHf!bQ2bu<8Q21=oKrAU6pDnL*kqfY%$v$)b zH8#)82OeHuQc`v>sf^6G3NHwDZ)EPVTPmf~R$%^y!r7NR{R8C~DN{3#t6kpP3EB#D`M3wG?v5xnJkEU67TUMYndP^{b?+ETB$xz;ouv#$Jux?b+W|lgOgDy z06C=$q;lfODb}SoRJw3%@-L18-Qhv|v}2?swlO9inKri~cs6H#{l=qbUj6_-VOFcTY^uW%>Y6B{j$r7IpM;Q+8VwrHNuHDi;b? zpH~-OX1yCg_k#38E-nhLua*F|CP4aG>II�+#&+6*J<~=ZOFoO0C`c$5(-t%465m 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 diff --git a/src/openflight/kld7/radc.py b/src/openflight/kld7/radc.py index 57da2babf..a626907ed 100644 --- a/src/openflight/kld7/radc.py +++ b/src/openflight/kld7/radc.py @@ -49,6 +49,23 @@ DEFAULT_BALL_ALIASED_MIN_KMH = -39.0 DEFAULT_BALL_ALIASED_MAX_KMH = -7.0 +# DC alias blind zone: ball speeds whose Doppler alias lands within this +# margin of 0 km/h sit in the DC clutter region (and partially under the +# DC mask), where angle extraction is unreliable. At the ±100 km/h wrap, +# 124 mph aliases exactly onto DC; ±11 km/h covers the empirically bad +# ~118-131 mph band (sub-frame STFT findings, 2026-06-09: 26% two-ray +# pass rate vs ~70% for clear speeds). Affects 4i/5i/3h ball speeds. +DC_ALIAS_BLIND_ZONE_KMH = 11.0 +# Confidence ceiling for shots measured inside the blind zone — same +# ceiling as legacy-naive-suspect results, since both are clutter-prone. +DC_BLIND_ZONE_CONFIDENCE_MAX = 0.35 + +# Sub-frame STFT instrumentation: 64-sample windows at 75% overlap give +# 13 sub-frames per 256-sample RADC frame (~7.2 ms each, ~1.8 ms apart) — +# short enough to resolve ground-bounce fringe oscillation within a frame. +SUBFRAME_WINDOW_SAMPLES = 64 +SUBFRAME_STEP_SAMPLES = 16 + # K-LD7 antenna parameters (24 GHz) WAVELENGTH_M = 3e8 / 24.125e9 # ~12.43 mm ANTENNA_SPACING_M = 8.0e-3 # ~0.64λ, calibrated against PDAT reference data @@ -377,6 +394,21 @@ def expected_ball_bin_from_speed( return _velocity_to_bin(aliased_kmh, fft_size, max_speed_kmh) +def is_dc_alias_blind_zone( + ball_speed_mph: float, + margin_kmh: float = DC_ALIAS_BLIND_ZONE_KMH, + max_speed_kmh: float = 100.0, +) -> bool: + """Return True when a ball speed's Doppler alias lands near DC. + + These shots' spectra sit in the DC clutter region where the ball + return is masked or blended with clutter — angle output should be + treated as low-confidence regardless of its apparent SNR. + """ + aliased_kmh = aliased_velocity_from_ball_speed_mph(ball_speed_mph, max_speed_kmh) + return abs(aliased_kmh) <= margin_kmh + + def circular_bin_distance(a: int, b: int, fft_size: int = 2048) -> int: """Shortest distance between FFT bins, accounting for circular wrap.""" distance = abs(int(a) - int(b)) @@ -719,6 +751,90 @@ def _phase_coherence_for_peak( return float(np.clip(coherence, 0.0, 1.0)) +def subframe_fringe_metrics( + f1a_iq: np.ndarray, + f2a_iq: np.ndarray, + peak_bin: int, + fft_size: int = 2048, + search_bins: int = CENTROID_SEARCH_BINS, +) -> dict | None: + """Sub-frame STFT fringe metrics for one RADC frame. + + Slices the frame into short overlapping windows and tracks the + interferometric elevation and F1A/F2A magnitude balance at the peak + across them. Indoor ground-bounce multipath sweeps several fringe + cycles within one ~29 ms frame, which shows up as coherent + oscillation in both metrics — flat metrics indicate a clean + single-ray return. Diagnostic only: logged per shot so the fringe + hypothesis can be checked against this rig's sessions offline. + + Returns None when the frame is too short or no sub-frame has usable + signal at the peak. + """ + n = len(f1a_iq) + if n < SUBFRAME_WINDOW_SAMPLES: + return None + lo = max(0, peak_bin - search_bins) + hi = min(fft_size, peak_bin + search_bins + 1) + if hi <= lo: + return None + + balances: list[float] = [] + elevations: list[float] = [] + for start in range(0, n - SUBFRAME_WINDOW_SAMPLES + 1, SUBFRAME_STEP_SAMPLES): + window = slice(start, start + SUBFRAME_WINDOW_SAMPLES) + f1 = compute_fft_complex(f1a_iq[window], fft_size=fft_size) + f2 = compute_fft_complex(f2a_iq[window], fft_size=fft_size) + # The sub-frame peak wanders within the frame peak's neighborhood + # as the fringe modulates — track it rather than reading a fixed bin. + local = np.abs(f1[lo:hi]) + np.abs(f2[lo:hi]) + sub_bin = lo + int(np.argmax(local)) + mag1 = float(np.abs(f1[sub_bin])) + mag2 = float(np.abs(f2[sub_bin])) + if mag1 <= 0 or mag2 <= 0: + continue + balances.append(mag2 / mag1) + phase_diff = float(np.angle(f1[sub_bin] * np.conj(f2[sub_bin]))) + sin_theta = phase_diff * WAVELENGTH_M / (2 * np.pi * ANTENNA_SPACING_M) + elevations.append(float(np.degrees(np.arcsin(np.clip(sin_theta, -1.0, 1.0))))) + + if not balances: + return None + elev = np.array(elevations) + bal = np.array(balances) + return { + "subframe_count": len(balances), + "elev_p2p_deg": round(float(elev.max() - elev.min()), 2), + "elev_std_deg": round(float(elev.std()), 2), + "balance_min": round(float(bal.min()), 3), + "balance_max": round(float(bal.max()), 3), + } + + +def _fringe_metrics_for_selected_frames( + frames: list[dict], + frame_indices, + peak_bins, + fft_size: int, +) -> list[dict]: + """Compute fringe metrics for the frames a shot result selected.""" + out: list[dict] = [] + for fi, peak_bin in zip(frame_indices, peak_bins): + radc_raw = frames[int(fi)].get("radc") + if radc_raw is None: + continue + try: + channels = _channels_from_frame(radc_raw) + f1a_iq = to_complex_iq(channels["f1a_i"], channels["f1a_q"]) + f2a_iq = to_complex_iq(channels["f2a_i"], channels["f2a_q"]) + except (KeyError, TypeError, ValueError): + continue + metrics = subframe_fringe_metrics(f1a_iq, f2a_iq, int(peak_bin), fft_size=fft_size) + if metrics is not None: + out.append({"frame_index": int(fi), **metrics}) + return out + + @dataclass(frozen=True) class _VerticalFrameCandidate: frame_index: int @@ -1417,6 +1533,18 @@ def extract_launch_angle( ball_bands = default_ball_bin_ranges(fft_size, max_speed_kmh) ops_expected_bin = None + dc_blind_zone = ops243_ball_speed_mph is not None and is_dc_alias_blind_zone( + ops243_ball_speed_mph, max_speed_kmh=max_speed_kmh + ) + if dc_blind_zone: + logger.warning( + "[RADC] Ball speed %.1f mph aliases within ±%.0f km/h of DC — " + "angle extraction is degraded in this band (confidence capped at %.2f)", + ops243_ball_speed_mph, + DC_ALIAS_BLIND_ZONE_KMH, + DC_BLIND_ZONE_CONFIDENCE_MAX, + ) + min_velocity_bin = 150 # skip low-velocity body/clutter impact_indices = find_impact_frames( frames, @@ -1985,6 +2113,9 @@ def extract_launch_angle( elif selection_path == "legacy_naive_suspect": confidence = min(confidence, VERTICAL_LEGACY_NAIVE_CONFIDENCE_MAX) + if dc_blind_zone: + confidence = min(confidence, DC_BLIND_ZONE_CONFIDENCE_MAX) + selected_t_ms = [ None if math.isnan(float(t)) else round(float(t) * 1000.0, 1) for t in clean_t_after ] @@ -1992,6 +2123,9 @@ def extract_launch_angle( None if math.isnan(float(bin_error)) else int(round(float(bin_error))) for bin_error in clean_bin_errors ] + fringe_metrics = _fringe_metrics_for_selected_frames( + frames, clean_frame_indices, clean_bins, fft_size + ) results.append( { @@ -2014,6 +2148,8 @@ def extract_launch_angle( "selected_t_ms": selected_t_ms, "selected_bin_errors": selected_bin_errors, "weak_adjacent_frame_used": bool(np.any(clean_weak_ops_anchor)), + "dc_blind_zone": dc_blind_zone, + "fringe_metrics": fringe_metrics, "ball_speed_mph": round(avg_speed_mph, 1), "confidence": confidence, "detection_count": len(peak_angles), diff --git a/src/openflight/kld7/tracker.py b/src/openflight/kld7/tracker.py index 9fcb3a574..2bd6af263 100644 --- a/src/openflight/kld7/tracker.py +++ b/src/openflight/kld7/tracker.py @@ -25,6 +25,8 @@ "geom_fit_rmse_deg", "geom_single_frame_resid_deg", "weak_adjacent_frame_used", + "dc_blind_zone", + "fringe_metrics", "raw_angle_deg", "angle_offset_deg", "spectrum_source", diff --git a/tests/test_kld7.py b/tests/test_kld7.py index d03591698..aba7f0c14 100644 --- a/tests/test_kld7.py +++ b/tests/test_kld7.py @@ -1680,3 +1680,172 @@ def fake_extract_launch_angle(frames, **kwargs): "distance_ft": 5.5, } ] + + +def _make_iq_pair_with_fringe( + velocity_kmh, + angle_deg, + wobble_deg=0.0, + wobble_cycles=2, + am_depth=0.0, + amplitude=1000.0, + n=256, +): + """Synthesize complex F1A/F2A I/Q for one frame. + + The F2A steering phase tracks ``angle_deg`` plus an optional sinusoidal + wobble (simulating a ground-bounce fringe sweeping the blended angle), + and F2A amplitude is optionally modulated by ``am_depth`` at the same + rate (simulating the channel-balance oscillation of a fringe). + """ + from openflight.kld7.radc import ANTENNA_SPACING_M, WAVELENGTH_M + + max_speed_kmh = 100.0 + if velocity_kmh >= 0: + norm_freq = velocity_kmh / (2 * max_speed_kmh) + else: + norm_freq = 1.0 + velocity_kmh / (2 * max_speed_kmh) + + t = np.arange(n) + carrier = 2 * np.pi * norm_freq * t + fringe = 2 * np.pi * wobble_cycles * t / n + inst_angle_rad = np.radians(angle_deg + wobble_deg * np.sin(fringe)) + steering = 2 * np.pi * ANTENNA_SPACING_M * np.sin(inst_angle_rad) / WAVELENGTH_M + + f1a = amplitude * np.exp(1j * carrier) + f2a = amplitude * (1.0 + am_depth * np.sin(fringe)) * np.exp(1j * (carrier + steering)) + return f1a, f2a + + +class TestDCAliasBlindZone: + """Ball speeds that alias onto DC are buried in the DC clutter mask + (K-LD7 ±100 km/h wrap: ~118-131 mph lands within ~11 km/h of bin 0).""" + + def test_blind_zone_band(self): + from openflight.kld7.radc import is_dc_alias_blind_zone + + # 124 mph = 199.5 km/h aliases to -0.5 km/h — dead center of DC + assert is_dc_alias_blind_zone(124.0) + # Edges of the empirical 118-131 mph band + assert is_dc_alias_blind_zone(119.0) # aliases to -8.5 km/h + assert is_dc_alias_blind_zone(130.0) # aliases to +9.2 km/h + # Clear speeds on either side of the band + assert not is_dc_alias_blind_zone(110.0) # -23.0 km/h + assert not is_dc_alias_blind_zone(140.0) # +25.3 km/h + # Typical iron / minimum pipeline speeds are nowhere near DC + assert not is_dc_alias_blind_zone(72.0) + assert not is_dc_alias_blind_zone(35.0) + + def _frames_with_tone(self, aliased_kmh, amplitude=6000): + synth = TestRADCAngleExtraction() + impact_ts = time.time() + quiet = synth._make_quiet_radc_payload() + frames = [{"timestamp": impact_ts - (6 - i) * 0.056, "radc": quiet} for i in range(6)] + tone = synth._make_radc_payload_with_tone(aliased_kmh, angle_deg=5.0, amplitude=amplitude) + frames.append({"timestamp": impact_ts + 0.056, "radc": tone}) + return frames + + def test_extract_flags_blind_zone_and_caps_confidence(self): + from openflight.kld7.radc import extract_launch_angle + + # 119 mph aliases to -8.5 km/h: detectable (outside the 8-bin DC + # mask) but inside the blind-zone margin where clutter dominates. + ball_speed_mph = 119.0 + aliased_kmh = (ball_speed_mph * 1.609) % 200.0 - 200.0 + + results = extract_launch_angle( + self._frames_with_tone(aliased_kmh), + ops243_ball_speed_mph=ball_speed_mph, + ) + + assert results + assert results[0]["dc_blind_zone"] is True + assert results[0]["confidence"] <= 0.35 + + def test_extract_clear_speed_not_flagged(self): + from openflight.kld7.radc import extract_launch_angle + + ball_speed_mph = 72.0 + aliased_kmh = (ball_speed_mph * 1.609) % 200.0 - 200.0 + + results = extract_launch_angle( + self._frames_with_tone(aliased_kmh), + ops243_ball_speed_mph=ball_speed_mph, + ) + + assert results + assert results[0]["dc_blind_zone"] is False + + +class TestSubframeFringeMetrics: + """Per-frame sub-frame STFT metrics exposing ground-bounce fringes: + elevation ripple and F1A/F2A balance excursion across 64-sample windows.""" + + def _peak_bin(self, velocity_kmh, fft_size=2048): + from openflight.kld7.radc import _velocity_to_bin + + return _velocity_to_bin(velocity_kmh, fft_size) + + def test_constant_angle_has_low_ripple(self): + from openflight.kld7.radc import subframe_fringe_metrics + + f1a, f2a = _make_iq_pair_with_fringe(-84.0, angle_deg=8.0) + metrics = subframe_fringe_metrics(f1a, f2a, self._peak_bin(-84.0)) + + assert metrics is not None + assert metrics["subframe_count"] == 13 + assert metrics["elev_p2p_deg"] < 1.0 + assert metrics["balance_max"] - metrics["balance_min"] < 0.1 + + def test_fringe_wobble_detected(self): + from openflight.kld7.radc import subframe_fringe_metrics + + f1a, f2a = _make_iq_pair_with_fringe( + -84.0, angle_deg=8.0, wobble_deg=6.0, wobble_cycles=2, am_depth=0.4 + ) + metrics = subframe_fringe_metrics(f1a, f2a, self._peak_bin(-84.0)) + + assert metrics is not None + assert metrics["elev_p2p_deg"] > 4.0 + assert metrics["balance_max"] - metrics["balance_min"] > 0.3 + + def test_zero_signal_returns_none(self): + from openflight.kld7.radc import subframe_fringe_metrics + + zeros = np.zeros(256, dtype=np.complex128) + assert subframe_fringe_metrics(zeros, zeros, 1186) is None + + def test_extract_attaches_fringe_metrics_for_selected_frames(self): + from openflight.kld7.radc import extract_launch_angle + + synth = TestRADCAngleExtraction() + impact_ts = time.time() + quiet = synth._make_quiet_radc_payload() + frames = [{"timestamp": impact_ts - (6 - i) * 0.056, "radc": quiet} for i in range(6)] + tone = synth._make_radc_payload_with_tone(-84.0, angle_deg=5.0, amplitude=6000) + frames.append({"timestamp": impact_ts + 0.056, "radc": tone}) + + results = extract_launch_angle(frames, ops243_ball_speed_mph=72.0) + + assert results + fringe = results[0]["fringe_metrics"] + assert isinstance(fringe, list) and fringe + entry = fringe[0] + assert entry["frame_index"] in results[0]["selected_frame_indices"] + for key in ("subframe_count", "elev_p2p_deg", "balance_min", "balance_max"): + assert key in entry + + +class TestRADCDiagnosticsPassthrough: + def test_selection_diagnostics_include_blind_zone_and_fringe(self): + from openflight.kld7.tracker import _radc_selection_diagnostics + + best = { + "estimator": "naive", + "dc_blind_zone": True, + "fringe_metrics": [{"frame_index": 6, "elev_p2p_deg": 7.2}], + } + diagnostics = _radc_selection_diagnostics(best, relaxed_retry=False) + + assert diagnostics["dc_blind_zone"] is True + assert diagnostics["fringe_metrics"] == [{"frame_index": 6, "elev_p2p_deg": 7.2}] From 4c265d86f5fd4f614ded898e5b2e08a858e1bf2c Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Thu, 11 Jun 2026 11:56:38 -0700 Subject: [PATCH 17/29] [cr] fresh pass at spin dedection --- scripts/analysis/replay_spin_dechirp.py | 383 +++++++++++++++++++++ src/openflight/rolling_buffer/processor.py | 215 ++++++++++-- tests/test_rolling_buffer.py | 135 +++++++- 3 files changed, 699 insertions(+), 34 deletions(-) create mode 100644 scripts/analysis/replay_spin_dechirp.py diff --git a/scripts/analysis/replay_spin_dechirp.py b/scripts/analysis/replay_spin_dechirp.py new file mode 100644 index 000000000..3130ab387 --- /dev/null +++ b/scripts/analysis/replay_spin_dechirp.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 +"""Replay raw rolling-buffer captures through a dechirped Doppler-sideband +spin estimator and score it against TrackMan truth alongside the production +envelope detector. + +Offline only — no production behavior changes. Scaffold for the "dechirped +coherent sideband spectrum" approach (TrackMan-style, single window): + +1. Track the ball's Doppler carrier f_d(t) with a short-window STFT. + The ball decelerates ~3-4 kHz/s, smearing the carrier (and every + sideband) ~200 Hz over a 60 ms window — fatal for resolving sideband + spacings of 42-167 Hz (2500-10000 RPM) in a plain FFT. +2. Mix the raw I/Q by the conjugate chirp so the carrier becomes a + stationary tone at 0 Hz and spin sidebands sit at exactly +/- m*f_mod. +3. Search a harmonic comb over candidate modulation frequencies, scoring + symmetric sideband-pair support in the dechirped spectrum. +4. Disambiguate the harmonic number: the seam's 2-fold symmetry + modulates at 2x spin "more often than not" (FlightScope US9868044), + while a logo/asymmetry modulates at 1x. Both interpretations are + emitted; odd-harmonic support at 1.5x the comb fundamental indicates + the true fundamental is half the observed spacing. + +Usage: + uv run --no-sync python scripts/analysis/replay_spin_dechirp.py \ + --openflight session_logs/session_20260511_120001_range.jsonl \ + --comparison session_logs/comparison_test2.csv \ + --output session_logs/spin_dechirp_replay_test2.csv +""" + +from __future__ import annotations + +import argparse +import csv +import statistics +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Optional + +import numpy as np + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT / "src")) +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import compare_trackman as ct # noqa: E402 pylint: disable=wrong-import-position +from experiment_spin_windows import ( # noqa: E402 + _load_session_entries, + _load_trackman_by_shot, + _to_int, +) + +from openflight.rolling_buffer.processor import RollingBufferProcessor # noqa: E402 +from openflight.rolling_buffer.types import IQCapture # noqa: E402 + +SAMPLE_RATE = 30000 +WAVELENGTH_M = 0.01243 +MPS_TO_MPH = 2.23694 + +# Carrier tracking +STFT_WINDOW = 256 # ~8.5 ms — 117 Hz resolution, enough to track the carrier +STFT_STEP = 64 # ~2.1 ms hop +TRACK_TOLERANCE_HZ = 900 # search window around expected ball Doppler +TRACK_MIN_SNR = 3.0 # STFT peak vs frame median to count as "carrier present" + +# Sideband search (after dechirp, carrier at 0 Hz) +SIDEBAND_FFT_SIZE = 1 << 17 # zero-padded for fine comb sampling +MOD_MIN_HZ = 33.0 # 2000 RPM at 1x +MOD_MAX_HZ = 400.0 # 12000 RPM at 2x seam modulation +COMB_STEP_HZ = 0.5 +COMB_HARMONICS = (1.0, 2.0, 3.0) +COMB_HARMONIC_WEIGHTS = (1.0, 0.6, 0.35) +ODD_HARMONIC_RATIO = 0.35 # support at 1.5x fundamental => spacing is really f/2 +MIN_USABLE_MS = 25.0 +SPIN_PLAUSIBLE_RPM = (1500.0, 13000.0) + + +@dataclass +class CarrierTrack: + """Quadratic fit of the ball Doppler carrier over its usable window.""" + + poly: np.ndarray # np.polyfit coeffs of f_d(t) in Hz vs seconds + start_sample: int + end_sample: int + + @property + def usable_ms(self) -> float: + return (self.end_sample - self.start_sample) / SAMPLE_RATE * 1000 + + +def track_carrier( + iq: np.ndarray, + ball_speed_mph: float, + onset_sample: int, +) -> Optional[CarrierTrack]: + """Fit f_d(t) from STFT peaks near the expected ball Doppler. + + Also determines the usable window: the contiguous run of STFT frames + (starting at ball onset) whose carrier peak stays above TRACK_MIN_SNR. + """ + expected_hz = 2 * (ball_speed_mph / MPS_TO_MPH) / WAVELENGTH_M + window = np.hanning(STFT_WINDOW) + freqs = np.fft.fftfreq(STFT_WINDOW, d=1 / SAMPLE_RATE) + band = (np.abs(freqs - expected_hz) <= TRACK_TOLERANCE_HZ) & (freqs > 0) + if not band.any(): + return None + + times, peaks, mags = [], [], [] + end_of_track = onset_sample + for start in range(onset_sample, len(iq) - STFT_WINDOW, STFT_STEP): + seg = iq[start : start + STFT_WINDOW] * window + spec = np.abs(np.fft.fft(seg)) + floor = float(np.median(spec[spec > 0])) or 1.0 + in_band = spec[band] + peak_idx = int(np.argmax(in_band)) + snr = float(in_band[peak_idx]) / floor + if snr < TRACK_MIN_SNR: + # Carrier lost (net impact / out of range): stop at the first + # loss after we have accumulated some track. + if len(times) >= 5: + break + continue + times.append((start + STFT_WINDOW / 2) / SAMPLE_RATE) + peaks.append(float(freqs[band][peak_idx])) + mags.append(float(in_band[peak_idx])) + end_of_track = start + STFT_WINDOW + + if len(times) < 5: + return None + t = np.array(times) + f = np.array(peaks) + w = np.sqrt(np.array(mags)) + order = 2 if len(times) >= 8 else 1 + poly = np.polyfit(t, f, order, w=w) + return CarrierTrack(poly=poly, start_sample=onset_sample, end_sample=end_of_track) + + +def dechirp(iq: np.ndarray, track: CarrierTrack) -> np.ndarray: + """Mix the usable window by the conjugate carrier chirp (carrier -> 0 Hz).""" + segment = iq[track.start_sample : track.end_sample] + t = (track.start_sample + np.arange(len(segment))) / SAMPLE_RATE + phase = 2 * np.pi * np.polyval(np.polyint(track.poly), t) + return segment * np.exp(-1j * phase) + + +@dataclass +class SidebandEstimate: + mod_freq_hz: float # fundamental sideband spacing found by the comb + comb_score: float # harmonic-comb support (vs noise floor) + odd_support: float # support at 1.5x fundamental (harmonic-number evidence) + spin_rpm_1x: float # spin if the modulating feature is 1x (logo) + spin_rpm_2x: float # spin if the modulation is the 2x seam + best_rpm: float # disambiguated pick + harmonic_choice: str # "1x" | "2x" + + +def sideband_spin(baseband: np.ndarray) -> Optional[SidebandEstimate]: + """Find the spin modulation frequency as symmetric sidebands around 0 Hz.""" + windowed = baseband * np.hanning(len(baseband)) + spectrum = np.abs(np.fft.fft(windowed, SIDEBAND_FFT_SIZE)) + + df = SAMPLE_RATE / SIDEBAND_FFT_SIZE + half_cell_bins = max(1, int(round(SAMPLE_RATE / len(baseband) / 2 / df))) + # Local floor window: wide enough to estimate the carrier-skirt level + # at that offset, excluding the sideband cell itself. + floor_bins = 8 * half_cell_bins + + def side_snr(spectrum_half: np.ndarray, k: int) -> float: + cell = spectrum_half[k - half_cell_bins : k + half_cell_bins + 1] + lo = max(1, k - floor_bins) + hi = min(len(spectrum_half) - 1, k + floor_bins) + ring = np.concatenate( + [ + spectrum_half[lo : k - half_cell_bins], + spectrum_half[k + half_cell_bins + 1 : hi], + ] + ) + ring = ring[ring > 0] + if cell.size == 0 or ring.size < 4: + return 0.0 + # The dechirped carrier's skirt slopes steeply: normalize each + # sideband by its own neighborhood, not a global floor, or the + # lowest candidate frequency always wins. + return float(cell.max()) / float(np.median(ring)) + + upper_half = spectrum[: SIDEBAND_FFT_SIZE // 2] + lower_half = spectrum[SIDEBAND_FFT_SIZE // 2 :][::-1] # mirrored negative freqs + + def mag_at(freq_hz: float) -> tuple[float, float]: + # local-floor SNR within half a natural-resolution cell of +/-freq_hz + k = int(round(freq_hz / df)) + if k - floor_bins < 1 or k + floor_bins >= SIDEBAND_FFT_SIZE // 2: + return 0.0, 0.0 + # lower_half[j] = spectrum[N-1-j] = bin at -(j+1)*df, so -k*df is j=k-1 + return side_snr(upper_half, k), side_snr(lower_half, k - 1) + + candidates = np.arange(MOD_MIN_HZ, MOD_MAX_HZ, COMB_STEP_HZ) + scores = np.zeros(len(candidates)) + for i, f0 in enumerate(candidates): + score = 0.0 + for harmonic, weight in zip(COMB_HARMONICS, COMB_HARMONIC_WEIGHTS): + upper, lower = mag_at(f0 * harmonic) + # symmetric-pair support: both sidebands must be present + score += weight * min(upper, lower) + scores[i] = score + + best = int(np.argmax(scores)) + f_mod = float(candidates[best]) + comb_score = float(scores[best]) + if comb_score <= 0: + return None + + # Harmonic-number disambiguation: if there is sideband support at + # 1.5x f_mod (an odd multiple of f_mod/2), the true fundamental is + # f_mod/2 and f_mod was its second harmonic. + upper, lower = mag_at(1.5 * f_mod) + odd_support = min(upper, lower) + + spin_1x = f_mod * 60.0 + spin_2x = f_mod / 2.0 * 60.0 + if odd_support >= ODD_HARMONIC_RATIO * comb_score: + choice, best_rpm = "2x", spin_2x + else: + # Default to 1x when the comb fundamental itself is plausible + # spin; prefer 2x when 1x would be implausibly high. + if spin_1x > SPIN_PLAUSIBLE_RPM[1] and SPIN_PLAUSIBLE_RPM[0] <= spin_2x: + choice, best_rpm = "2x", spin_2x + else: + choice, best_rpm = "1x", spin_1x + + return SidebandEstimate( + mod_freq_hz=f_mod, + comb_score=comb_score, + odd_support=float(odd_support), + spin_rpm_1x=spin_1x, + spin_rpm_2x=spin_2x, + best_rpm=best_rpm, + harmonic_choice=choice, + ) + + +def replay_shot( + processor: RollingBufferProcessor, + capture: IQCapture, +) -> tuple[Optional[Any], Optional[SidebandEstimate], dict[str, Any]]: + """Run production processing and the dechirped estimator on one capture.""" + processed = processor.process_capture(capture) + diag: dict[str, Any] = {} + if not processed: + return None, None, diag + + i_data = np.array(capture.i_samples, dtype=np.float64) + q_data = np.array(capture.q_samples, dtype=np.float64) + iq = (i_data - i_data.mean()) + 1j * (q_data - q_data.mean()) + + onset = max(0, int(processed.ball_timestamp_ms * SAMPLE_RATE / 1000)) + track = track_carrier(iq, processed.ball_speed_mph, onset) + if track is None: + diag["dechirp_skip"] = "no_carrier_track" + return processed, None, diag + + diag["usable_ms"] = round(track.usable_ms, 1) + diag["chirp_hz_per_s"] = round(float(track.poly[-2]) if len(track.poly) >= 2 else 0.0) + if track.usable_ms < MIN_USABLE_MS: + diag["dechirp_skip"] = f"usable_window_{track.usable_ms:.0f}ms" + return processed, None, diag + + estimate = sideband_spin(dechirp(iq, track)) + return processed, estimate, diag + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--openflight", type=Path, required=True, help="Session JSONL") + parser.add_argument("--comparison", type=Path, required=True, help="TrackMan CSV") + parser.add_argument("--output", type=Path, required=True, help="Per-shot output CSV") + args = parser.parse_args() + + shots, captures = _load_session_entries(args.openflight) + trackman_by_shot = _load_trackman_by_shot(args.comparison) + processor = RollingBufferProcessor(sample_rate=SAMPLE_RATE) + + rows: list[dict[str, Any]] = [] + for shot_entry, capture_entry in zip(shots, captures): + shot_data = shot_entry.get("data", shot_entry) + shot_number = _to_int(shot_data.get("shot_number")) + truth = trackman_by_shot.get(shot_number or -1, {}) + if truth.get("match_quality") != "good" or truth.get("spin_tm") is None: + continue + + capture = IQCapture( + sample_time=capture_entry.get("sample_time", 0), + trigger_time=capture_entry.get("trigger_time", 0), + i_samples=capture_entry["i_samples"], + q_samples=capture_entry["q_samples"], + ) + processed, estimate, diag = replay_shot(processor, capture) + if not processed: + continue + + spin_tm = truth["spin_tm"] + baseline = processed.spin + row: dict[str, Any] = { + "shot_number": shot_number, + "club": ct.normalize_club(shot_data.get("club")), + "spin_tm": spin_tm, + "ball_speed_of": round(processed.ball_speed_mph, 1), + "baseline_rpm": baseline.spin_rpm if baseline and baseline.spin_rpm else None, + "baseline_quality": baseline.quality if baseline else None, + "baseline_snr": baseline.snr if baseline else None, + "usable_ms": diag.get("usable_ms"), + "chirp_hz_per_s": diag.get("chirp_hz_per_s"), + "dechirp_skip": diag.get("dechirp_skip"), + } + if estimate: + row.update( + { + "dechirp_rpm": round(estimate.best_rpm), + "dechirp_choice": estimate.harmonic_choice, + "dechirp_rpm_1x": round(estimate.spin_rpm_1x), + "dechirp_rpm_2x": round(estimate.spin_rpm_2x), + "dechirp_score": round(estimate.comb_score, 2), + "dechirp_odd_support": round(estimate.odd_support, 2), + } + ) + rows.append(row) + + if not rows: + print("No paired shots with TrackMan spin truth found.") + return + + fieldnames = sorted({key for row in rows for key in row}) + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + _print_summary(rows) + print(f"\nPer-shot rows written to {args.output}") + + +def _summarize(label: str, pairs: list[tuple[float, float]], total: int) -> None: + if not pairs: + print(f"{label:28s} coverage 0/{total}") + return + errors = [abs(measured - truth) for measured, truth in pairs] + pct = [abs(m - t) / t * 100 for m, t in pairs if t > 0] + within10 = sum(1 for p in pct if p <= 10) + print( + f"{label:28s} coverage {len(pairs)}/{total} " + f"MAE {statistics.mean(errors):6.0f} rpm " + f"median {statistics.median(errors):6.0f} rpm " + f"within10% {within10}/{len(pairs)}" + ) + + +def _print_summary(rows: list[dict[str, Any]]) -> None: + total = len(rows) + baseline_pairs = [ + (row["baseline_rpm"], row["spin_tm"]) for row in rows if row.get("baseline_rpm") + ] + dechirp_pairs = [(row["dechirp_rpm"], row["spin_tm"]) for row in rows if row.get("dechirp_rpm")] + # Oracle: the better of the 1x/2x interpretations per shot — the + # ceiling a perfect harmonic-disambiguation rule could reach. + oracle_pairs = [] + for row in rows: + if row.get("dechirp_rpm_1x"): + truth = row["spin_tm"] + best = min( + (row["dechirp_rpm_1x"], row["dechirp_rpm_2x"]), + key=lambda rpm: abs(rpm - truth), + ) + oracle_pairs.append((best, truth)) + + print(f"\n=== Spin replay summary ({total} TrackMan-paired shots) ===") + _summarize("production envelope", baseline_pairs, total) + _summarize("dechirped sidebands", dechirp_pairs, total) + _summarize("dechirp + harmonic oracle", oracle_pairs, total) + + +if __name__ == "__main__": + main() diff --git a/src/openflight/rolling_buffer/processor.py b/src/openflight/rolling_buffer/processor.py index 9240904a2..75de10463 100644 --- a/src/openflight/rolling_buffer/processor.py +++ b/src/openflight/rolling_buffer/processor.py @@ -89,17 +89,37 @@ class RollingBufferProcessor: # Rail-rejection guards. The envelope FFT has two pathological # regions where the peak picker hunts for noise rather than a real # seam tone: - # - The lowest few bins of the valid seam range are dominated by - # residual DC leakage from the envelope subtraction (the Hann - # main lobe is ~2 bins wide but the leakage shoulder extends - # several bins further). On real captures this produces a - # pile-up at ~2637-3076 RPM. Zero the lowest N bins so they - # cannot win the argmax. + # - Slow envelope drift (range falloff, residual DC) leaks into + # the lowest bins of the seam range. The polynomial detrend + # removes the drift itself; a 1-bin guard absorbs what little + # leakage remains. (This guard was previously 5 bins, which + # zeroed 33-51.3 Hz = 1980-3080 RPM — typical driver backspin — + # and produced a rail-artifact pile-up at ~3296 RPM instead.) # - The highest 1-2 bins are the bandpass shoulder of the # prefilter. Even a moderate noise spike there reads as # ~12000 RPM. Reject the pick when the peak lands there # and SNR isn't strong enough to override. - SPIN_DC_LEAKAGE_BINS = 5 # Zero this many low bins of valid range + SPIN_DC_LEAKAGE_BINS = 1 # Zero this many low bins of valid range + # Picks at or below this RPM are reported but capped to low quality: + # red envelope noise produces sustained narrowband mimics in this + # band that a single ~136 ms capture cannot distinguish from a real + # seam tone (see the low-band cap in detect_spin). + SPIN_LOW_BAND_SUSPECT_MAX_RPM = 3100.0 + # Polynomial order for envelope detrending before the spin FFT. + # Order 3 follows the smooth range-falloff decay across the window + # but cannot track a >=33 Hz seam tone (>=2 cycles in any window + # long enough to pass SPIN_MIN_SAMPLES), so real spin is preserved. + SPIN_DETREND_POLY_ORDER = 3 + # Ball-signal-end detection: the analysis window must stop when the + # ball signal dies (net impact indoors), not at the end of the + # capture — the amplitude cliff plus dead air corrupts the envelope + # FFT. Signal is "lost" when the smoothed envelope stays below + # SPIN_SIGNAL_LOSS_THRESHOLD x (early-window median) for + # SPIN_SIGNAL_LOSS_HOLD_SAMPLES. + SPIN_SIGNAL_LOSS_SMOOTH_SAMPLES = 90 # ~3 ms moving average + SPIN_SIGNAL_LOSS_REF_SAMPLES = 450 # first ~15 ms sets reference level + SPIN_SIGNAL_LOSS_THRESHOLD = 0.15 # fraction of reference level + SPIN_SIGNAL_LOSS_HOLD_SAMPLES = 150 # ~5 ms sustained loss SPIN_UPPER_RAIL_BINS = 2 # Top N bins of valid range = "upper rail" SPIN_PRIOR_MIN_RELATIVE_MAG = 0.40 # Candidate must be this strong to displace argmax SPIN_PRIOR_MAX_RELATIVE_ERROR = 0.55 # Candidate must be within this fraction of expected @@ -416,6 +436,116 @@ def process_overlapping(self, capture: IQCapture) -> SpeedTimeline: """ return self._process_capture(capture, self.STEP_SIZE_OVERLAP) + def _local_noise_floor( + self, valid_mag: np.ndarray, peak_idx: int, window_samples: int + ) -> float: + """Noise level around the peak, excluding the peak's main lobe. + + Takes the *max* of the two sides' medians: a genuine narrow tone + has both sides at the true noise floor, while a peak riding the + top of a red-noise slope has at least one side nearly as high as + the peak itself. A pooled median would let the rolled-off side + mask the slope. + + The exclusion zone scales with the Hann main-lobe width in + zero-padded bins (2 x FFT_SIZE / window_samples per side), so a + short analysis window's wider tone skirt is not counted as noise. + """ + # A real Hann tone reaches its first null 2 x FFT/N padded bins + # from the peak; beyond that only -31 dB sidelobes remain. The + # neighborhood is the immediate shoulder just past the null — a + # red-noise bump's tell-tale is that this shoulder stays near + # peak level, and a wider window would dilute it with far bins + # that have already rolled off. + exclude = int(np.ceil(2 * self.SPIN_ENVELOPE_FFT_SIZE / max(window_samples, 1))) + half_width = 3 * exclude + lo = max(0, peak_idx - half_width) + hi = min(len(valid_mag), peak_idx + half_width + 1) + side_floors = [] + for side in ( + valid_mag[lo : max(lo, peak_idx - exclude)], + valid_mag[min(hi, peak_idx + exclude + 1) : hi], + ): + live = side[side > 0] + # A side needs a few live bins to give a meaningful median + # (the leakage guard zeroes bins near the low band edge). + if live.size >= 3: + side_floors.append(float(np.median(live))) + return max(side_floors) if side_floors else 0.0 + + def _spin_peak_is_persistent( + self, ball_envelope: np.ndarray, peak_freq_hz: float + ) -> bool: + """Whether the picked seam tone is present in both halves of the window. + + A real seam tone persists for the whole ball flight; an envelope-noise + fluctuation concentrates its energy in part of the window. Demoting + non-persistent picks is the single-window analogue of tracking + sideband traces across successive spectra. + """ + half = len(ball_envelope) // 2 + if half < self.SPIN_MIN_SAMPLES // 2: + return True # window too short to split meaningfully + for segment in (ball_envelope[:half], ball_envelope[half:]): + seg = segment - np.mean(segment) + windowed = seg * np.hanning(len(seg)) + magnitude = np.abs(np.fft.fft(windowed, self.SPIN_ENVELOPE_FFT_SIZE)) + freqs = np.fft.fftfreq(self.SPIN_ENVELOPE_FFT_SIZE, d=1 / self.SAMPLE_RATE) + half_fft = self.SPIN_ENVELOPE_FFT_SIZE // 2 + magnitude, freqs = magnitude[1:half_fft], freqs[1:half_fft] + valid = (freqs >= self.SPIN_MIN_SEAM_HZ) & (freqs <= self.SPIN_MAX_SEAM_HZ) + valid_mag, valid_freqs = magnitude[valid], freqs[valid] + if not np.any(valid_mag > 0): + return False + floor = float(np.median(valid_mag[valid_mag > 0])) + # Tolerance: the half-window's natural resolution (±2 bins) + tol_hz = 2.0 * self.SAMPLE_RATE / len(seg) + near = np.abs(valid_freqs - peak_freq_hz) <= tol_hz + if not near.any() or floor <= 0: + return False + near_max = float(valid_mag[near].max()) + # The pick must be above the floor AND (near-)dominant in + # this half. A broad noise bump keeps energy near the pick + # but its dominant frequency wanders across the bump; a real + # tone stays pinned at the same frequency in every half. + if near_max < 2.5 * floor or near_max < 0.7 * float(valid_mag.max()): + return False + return True + + def _ball_signal_end_sample(self, envelope: np.ndarray, start_sample: int) -> int: + """Return the absolute sample index where the ball signal ends. + + Detects sustained loss of the bandpassed ball tone (net impact + indoors) by comparing the smoothed envelope against the level at + the start of the ball window. Returns len(envelope) when no loss + is found, so outdoor shots keep the full capture. + """ + tail = envelope[start_sample:] + if len(tail) < self.SPIN_SIGNAL_LOSS_REF_SAMPLES: + return len(envelope) + + kernel = np.ones(self.SPIN_SIGNAL_LOSS_SMOOTH_SAMPLES) + kernel /= self.SPIN_SIGNAL_LOSS_SMOOTH_SAMPLES + smoothed = np.convolve(tail, kernel, mode="same") + reference = float(np.median(smoothed[: self.SPIN_SIGNAL_LOSS_REF_SAMPLES])) + if reference <= 0: + return len(envelope) + + below = smoothed < reference * self.SPIN_SIGNAL_LOSS_THRESHOLD + hold = self.SPIN_SIGNAL_LOSS_HOLD_SAMPLES + if len(below) < hold: + return len(envelope) + sustained = np.convolve(below.astype(float), np.ones(hold), mode="valid") >= hold + if not sustained.any(): + return len(envelope) + loss_offset = int(np.argmax(sustained)) + logger.info( + "[PROCESSOR] Ball signal lost at %.1fms (window trimmed from %.1fms)", + (start_sample + loss_offset) / self.SAMPLE_RATE * 1000, + len(envelope) / self.SAMPLE_RATE * 1000, + ) + return start_sample + loss_offset + def detect_spin( self, capture: IQCapture, @@ -468,10 +598,11 @@ def detect_spin( # Amplitude envelope envelope = np.abs(filtered) - # Trim to ball-present window (from ball onset to end of capture) + # Trim to ball-present window: from ball onset to where the ball + # signal dies (net impact indoors), falling back to capture end. start_sample = max(0, int(ball_timestamp_ms * self.SAMPLE_RATE / 1000)) spin_window_start_sample = start_sample - spin_window_end_sample = len(envelope) + spin_window_end_sample = self._ball_signal_end_sample(envelope, start_sample) ball_envelope = envelope[spin_window_start_sample:spin_window_end_sample] # Trim filter transients from both ends. sosfiltfilt's internal @@ -512,12 +643,17 @@ def detect_spin( weak_modulation = modulation_depth < 0.01 # Remove DC and apply Hann window - ball_envelope -= envelope_mean + ball_envelope = ball_envelope - envelope_mean if envelope_std < 1e-6: return SpinResult.no_spin_detected( "Envelope variation too low", modulation_depth=modulation_depth, ) + # Detrend slow envelope drift (range falloff) so it cannot leak + # into the low end of the seam band and shadow real driver spin. + x = np.arange(len(ball_envelope), dtype=np.float64) + trend = np.polyval(np.polyfit(x, ball_envelope, self.SPIN_DETREND_POLY_ORDER), x) + ball_envelope = ball_envelope - trend windowed = ball_envelope * np.hanning(len(ball_envelope)) # --- Primary: FFT on envelope --- @@ -563,8 +699,17 @@ def detect_spin( at_lower_rail = peak_idx < leakage + self.SPIN_UPPER_RAIL_BINS at_upper_rail = peak_idx >= n_valid - self.SPIN_UPPER_RAIL_BINS - # SNR: peak vs median noise floor in valid range + # SNR: peak vs the stricter of two noise floors. The global + # median catches white noise; the local median catches red + # envelope noise, which forms broad low-frequency bumps whose + # peaks tower over the global median while sitting barely above + # their own neighborhood. A real seam tone is a narrow line + # (Hann main lobe) above its local floor; a noise-bump peak is + # not. Without the local floor, toneless red noise reads as + # high-confidence ~2000-3300 RPM spin. noise_floor = np.median(valid_mag[valid_mag > 0]) if np.any(valid_mag > 0) else 1.0 + local_floor = self._local_noise_floor(valid_mag, peak_idx, len(windowed)) + noise_floor = max(noise_floor, local_floor) fft_snr = peak_mag / noise_floor if noise_floor > 0 else 0 spin_candidates = self._build_spin_candidates( valid_mag, @@ -627,23 +772,27 @@ def detect_spin( candidates=spin_candidates, ) - # Lower-rail picks survive the leakage zeroing only if energy - # leaks just past the guard. Treat them as suspect: require - # modulation depth to clearly exceed the weak-modulation - # threshold (i.e., a real seam tone rather than envelope wander). + # Lower-rail picks sit where residual envelope drift leaks past + # the detrend. Treat them as suspect: require modulation depth + # to clearly exceed the weak-modulation threshold AND medium + # SNR (a real low-spin driver tone is strong; noise picks in + # this zone hover just above the report floor). if at_lower_rail and ( - modulation_depth is None or modulation_depth < 0.012 + modulation_depth is None + or modulation_depth < 0.012 + or fft_snr < self.SPIN_SNR_MEDIUM ): logger.warning( "[PROCESSOR] Spin rejected: lower-rail peak at %.0f RPM " - "(mod %.4f, envelope-DC leakage)", + "(mod %.4f, SNR %.1f, envelope-drift leakage suspected)", spin_rpm, modulation_depth if modulation_depth is not None else float("nan"), + fft_snr, ) return SpinResult.no_spin_detected( f"Lower-rail peak at {spin_rpm:.0f} RPM " - f"(mod {modulation_depth or 0:.4f}, " - f"envelope-DC leakage suspected)", + f"(mod {modulation_depth or 0:.4f}, SNR {fft_snr:.1f}, " + f"envelope-drift leakage suspected)", snr=fft_snr, modulation_depth=modulation_depth, peak_freq_hz=peak_freq, @@ -815,6 +964,18 @@ def detect_spin( quality = "low" confidence = 0.3 + # A seam tone must persist across the whole window — a pick whose + # energy lives in only half the window is an envelope-noise + # fluctuation, however sharp its spectral peak looks. + if not self._spin_peak_is_persistent(ball_envelope, peak_freq): + logger.info( + "[PROCESSOR] Spin demoted: %.0f RPM peak not persistent " + "across both window halves", + spin_rpm, + ) + confidence = min(confidence, 0.3) + quality = "low" + # Weak modulation caps confidence — the envelope FFT peak may be # noise rather than real seam modulation. if weak_modulation: @@ -822,11 +983,17 @@ def detect_spin( if quality == "high": quality = "medium" - # Low-edge picks can be real low-spin driver candidates, but - # real Trackman comparison sessions also show 3300-3500 RPM - # rail artifacts on irons/wedges. Keep the candidate visible for - # analysis, but never treat it as a reliable spin measurement. - if at_lower_rail: + # Low-band picks (<= ~3100 RPM / ~52 Hz) can be real driver spin, + # and the detrended FFT now reports their correct value — but at + # a ~136 ms window this band has an irreducible noise-mimic + # problem: lowpassed envelope noise produces sustained narrowband + # components here that are physically indistinguishable from a + # seam tone within a single capture (and real TrackMan sessions + # showed exactly such artifacts on irons/wedges). Report the + # value, but never as reliable. Lifting this cap requires an + # estimator with more signal energy (dechirped Doppler sidebands) + # or a longer observation window. + if at_lower_rail or spin_rpm <= self.SPIN_LOW_BAND_SUSPECT_MAX_RPM: confidence = min(confidence, 0.5) if quality in ("high", "medium"): quality = "low" diff --git a/tests/test_rolling_buffer.py b/tests/test_rolling_buffer.py index 2bb72f26e..417d761ab 100644 --- a/tests/test_rolling_buffer.py +++ b/tests/test_rolling_buffer.py @@ -2356,14 +2356,17 @@ def test_lower_rail_peak_rejected_with_toneless_envelope(self): ) if result.spin_rpm > 0: accepted.append((seed, result)) - # No accepted detection may sit at the bottom 5 bins of the seam - # search range (≈ 33-50 Hz, ≈ 2000-3000 RPM) — that whole region - # is dominated by envelope-DC leakage on real data. + # With the envelope detrend, 2000-3100 RPM is measurable for real + # tones — but toneless noise must never produce a *reliable* + # result there. Rail-zone picks additionally require medium SNR, + # which envelope noise does not reach. for seed, r in accepted: - assert r.spin_rpm > 3100, ( - f"seed={seed}: lower rail not rejected, got " - f"{r.spin_rpm} RPM (quality={r.quality}, snr={r.snr})" - ) + if r.spin_rpm <= 3100: + assert not r.is_reliable and r.confidence <= 0.5, ( + f"seed={seed}: noise accepted as reliable low spin, got " + f"{r.spin_rpm} RPM (quality={r.quality}, snr={r.snr}, " + f"confidence={r.confidence})" + ) def test_upper_rail_peak_rejected_with_toneless_envelope(self): """A toneless envelope must not be reported as ~12000 RPM @@ -2476,11 +2479,16 @@ def test_real_seam_modulation_still_passes(self): assert result.at_upper_rail is False, "Interior peak should not flag at_upper_rail" def test_lower_rail_candidate_confidence_is_capped(self): - """Lower-rail candidates may be visible but must not be reliable.""" + """Lower-rail candidates may be visible but must not be reliable. + + The rail zone is the lowest 3 live bins of the seam range + (~33-44 Hz, <=~2640 RPM) now that the detrend replaced the wide + leakage guard; 2300 RPM sits inside it. + """ processor = RollingBufferProcessor() i, q = self._amplitude_modulated_iq( base_speed_mph=160.0, - mod_freq_hz=3000.0 / 60.0, + mod_freq_hz=2300.0 / 60.0, modulation_depth=0.03, ) capture = IQCapture( @@ -2497,6 +2505,7 @@ def test_lower_rail_candidate_confidence_is_capped(self): ) assert result.spin_rpm > 0 + assert abs(result.spin_rpm - 2300) < 250 assert result.at_lower_rail is True assert result.confidence <= 0.5 assert result.is_reliable is False @@ -2615,7 +2624,8 @@ def test_phase_confirmation_accepts_matching_witness(self): assert witness is not None assert witness["confirmed"] is True - assert witness["method"] == "phase_residual" + # Either phase witness may win the ranking; both are valid confirmations. + assert witness["method"] in ("phase_residual", "instant_frequency") assert witness["rpm"] == pytest.approx(7031.25) assert witness["snr"] >= processor.SPIN_PHASE_SNR_MIN assert witness["agreement_pct"] <= processor.SPIN_PHASE_AGREEMENT_PCT @@ -2710,3 +2720,108 @@ def test_disconnect_stops_capture_thread(self): assert monitor._running is False, ( "disconnect() must call stop() so the capture thread shuts down" ) + + +class TestSpinWindowTrimming: + """The spin window must end where ball signal ends (net impact), not at + the end of the capture — the amplitude cliff plus dead air corrupts the + envelope FFT.""" + + def _make_capture_with_signal_loss( + self, + base_speed_mph=120, + spin_rpm=6000, + signal_end_ms=55.0, + modulation_depth=0.03, + sample_rate=30000, + num_samples=4096, + ): + """Ball tone with seam modulation that dies mid-capture (net impact).""" + rng = np.random.default_rng(7) + wavelength = 0.01243 + doppler_hz = 2 * (base_speed_mph / 2.23694) / wavelength + seam_hz = spin_rpm / 60.0 + + t = np.arange(num_samples) / sample_rate + phase = 2 * np.pi * doppler_hz * t + amplitude = 200 * (1.0 + modulation_depth * np.sin(2 * np.pi * seam_hz * t)) + i_signal = amplitude * np.cos(phase) + q_signal = amplitude * np.sin(phase) + + end_sample = min(int(signal_end_ms * sample_rate / 1000), num_samples) + i_signal[end_sample:] = rng.normal(0, 2, num_samples - end_sample) + q_signal[end_sample:] = rng.normal(0, 2, num_samples - end_sample) + + return IQCapture( + sample_time=0.0, + trigger_time=0.068, + i_samples=(i_signal + 2048).astype(int).clip(0, 4095).tolist(), + q_samples=(q_signal + 2048).astype(int).clip(0, 4095).tolist(), + ) + + def test_spin_correct_when_ball_signal_dies_mid_capture(self): + capture = self._make_capture_with_signal_loss(spin_rpm=6000, signal_end_ms=55.0) + processor = RollingBufferProcessor() + result = processor.detect_spin(capture, ball_speed_mph=120, ball_timestamp_ms=5.0) + assert result.spin_rpm > 0, f"Should detect spin, got: {result.rejection_reason}" + assert abs(result.spin_rpm - 6000) < 300, ( + f"Expected ~6000 RPM, got {result.spin_rpm} (quality={result.quality})" + ) + + def test_full_length_signal_is_not_truncated(self): + """A ball tone spanning the whole capture must keep its full window + (seam_cycles reflects the full ~126 ms analysis window).""" + capture = self._make_capture_with_signal_loss(spin_rpm=6000, signal_end_ms=10000.0) + processor = RollingBufferProcessor() + result = processor.detect_spin(capture, ball_speed_mph=120, ball_timestamp_ms=5.0) + assert result.spin_rpm > 0 + assert abs(result.spin_rpm - 6000) < 300 + # 6000 RPM = 100 Hz; >= 11 cycles requires >= ~110 ms of window + assert result.seam_cycles >= 11, ( + f"Window appears wrongly truncated: {result.seam_cycles} cycles" + ) + + +class TestSpinDriverDeadZone: + """Typical driver backspin (2000-3000 RPM) must be measurable: the old + DC-leakage guard zeroed 33-51.3 Hz (1980-3080 RPM) of the envelope FFT.""" + + def _tone_capture(self, spin_rpm, modulation_depth=0.03, decay=None): + sample_rate, num_samples = 30000, 4096 + wavelength = 0.01243 + doppler_hz = 2 * (160 / 2.23694) / wavelength + t = np.arange(num_samples) / sample_rate + phase = 2 * np.pi * doppler_hz * t + amplitude = np.full(num_samples, 200.0) + if spin_rpm: + amplitude *= 1.0 + modulation_depth * np.sin(2 * np.pi * (spin_rpm / 60.0) * t) + if decay is not None: + amplitude *= np.linspace(1.0, decay, num_samples) + return IQCapture( + sample_time=0.0, + trigger_time=0.068, + i_samples=(amplitude * np.cos(phase) + 2048).astype(int).clip(0, 4095).tolist(), + q_samples=(amplitude * np.sin(phase) + 2048).astype(int).clip(0, 4095).tolist(), + ) + + def test_low_driver_spin_measurable(self): + """2400 RPM (40 Hz) sits inside the old zeroed band.""" + processor = RollingBufferProcessor() + result = processor.detect_spin( + self._tone_capture(2400), ball_speed_mph=160, ball_timestamp_ms=5.0 + ) + assert result.spin_rpm > 0, f"Should detect spin, got: {result.rejection_reason}" + assert abs(result.spin_rpm - 2400) < 250, f"Expected ~2400 RPM, got {result.spin_rpm}" + + def test_range_falloff_decay_does_not_fake_driver_spin(self): + """A smoothly decaying envelope with no seam modulation (ball flying + away, no spin signal) must not produce a confident driver-band spin.""" + processor = RollingBufferProcessor() + result = processor.detect_spin( + self._tone_capture(spin_rpm=0, decay=0.4), + ball_speed_mph=160, + ball_timestamp_ms=5.0, + ) + assert result.spin_rpm == 0 or result.quality == "low", ( + f"Decay ramp faked spin: {result.spin_rpm} RPM quality={result.quality}" + ) From ed6f37a2622597e67e6a8b21e45bf15d980a915e Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Thu, 11 Jun 2026 13:02:46 -0700 Subject: [PATCH 18/29] [cr] updates to the interactive setup script --- README.md | 14 +- docs/kld7-troubleshooting.md | 8 +- docs/raspberry-pi-setup.md | 209 +++++++++++++++----------- docs/trackman-test-process.md | 3 +- scripts/setup/setup.sh | 201 ++++++++++++++++++------- scripts/setup/setup_kld7_devices.sh | 221 ++++++++++++++++++++++++++++ 6 files changed, 511 insertions(+), 145 deletions(-) create mode 100755 scripts/setup/setup_kld7_devices.sh diff --git a/README.md b/README.md index 91e69bd95..5888547ee 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,19 @@ Follow the **[Sound Trigger Wiring Guide](docs/sound-trigger-wiring.md)** to con ### 3. Set up the Pi -Follow the **[Raspberry Pi Setup Guide](docs/raspberry-pi-setup.md)** for OS installation, software setup, radar configuration, and auto-start. +Flash Raspberry Pi OS (64-bit), plug in the radars, then run the interactive setup: + +```bash +git clone https://github.com/jewbetcha/openflight.git +cd openflight +./scripts/setup/setup.sh +``` + +The script installs everything and walks you through the one-time hardware +configuration (radar flash setup, K-LD7 device naming, auto-start) with +prompts — no manual config editing needed. It's safe to re-run any time. +See the **[Raspberry Pi Setup Guide](docs/raspberry-pi-setup.md)** for +details and troubleshooting. ### 4. Hit balls diff --git a/docs/kld7-troubleshooting.md b/docs/kld7-troubleshooting.md index fd2160f7c..19977fb59 100644 --- a/docs/kld7-troubleshooting.md +++ b/docs/kld7-troubleshooting.md @@ -158,9 +158,13 @@ Other possibilities: **Cause:** The K-LD7 uses an FTDI USB-to-serial adapter which shows up as `/dev/ttyUSB*`. If multiple USB-serial devices are connected, auto-detection may pick the wrong one. -**Fix:** Specify the port explicitly or set up udev rules: +**Fix:** Run the device naming wizard so each radar gets a fixed name, or +specify the port explicitly: ```bash -# Explicit port +# Recommended: map the radars to /dev/kld7_vertical / /dev/kld7_horizontal +./scripts/setup/setup_kld7_devices.sh + +# Or: explicit port scripts/start-kiosk.sh --kld7 --kld7-port /dev/ttyUSB0 # Find available ports diff --git a/docs/raspberry-pi-setup.md b/docs/raspberry-pi-setup.md index b6e87e9c1..c3f90c118 100644 --- a/docs/raspberry-pi-setup.md +++ b/docs/raspberry-pi-setup.md @@ -17,79 +17,110 @@ Make sure you have all the hardware. See the **[Parts List](PARTS.md)** for what **Optional:** - K-LD7 + FTDI adapter (×2) — for launch angle and club path (see [Parts List](PARTS.md)) -## Initial Setup +## Setup ### 1. Install Raspberry Pi OS Use Raspberry Pi Imager to flash **Raspberry Pi OS (64-bit)** to your SD card. -### 2. Clone and Install +### 2. Run the setup script + +Plug in the OPS243-A (and the K-LD7 adapters if you have them), then: ```bash cd ~ git clone https://github.com/jewbetcha/openflight.git cd openflight - -# Run the setup script (handles everything) ./scripts/setup/setup.sh ``` -The setup script will: -- Create a Python virtual environment -- Install all Python dependencies -- Install Node.js dependencies -- Build the UI -- Run tests to verify installation +The script installs everything, then walks you through the one-time hardware +configuration with prompts: + +1. **Dependencies** — Python venv, packages, UI build, test run +2. **OPS243-A radar** — saves rolling buffer mode to the radar's flash + (you'll be asked to unplug/replug the radar once) +3. **K-LD7 radars** (if you have them) — identifies each radar by plugging + them in one at a time, so OpenFlight always knows which is which +4. **Auto-start on boot** — optional systemd service +5. **Desktop shortcut** — optional -Or manually: +Every step can be skipped and the script is **safe to re-run** any time — +it picks up where you left off. + +### 3. Start hitting balls ```bash -curl -LsSf https://astral.sh/uv/install.sh | sh -python -m venv .venv --system-site-packages -source .venv/bin/activate -uv pip install -e ".[ui]" -cd ui && npm install && npm run build && cd .. +./scripts/start-kiosk.sh # Default: rolling buffer + sound trigger +./scripts/start-kiosk.sh --kld7 # With K-LD7 angle radars +./scripts/start-kiosk.sh --mock # Mock mode (no hardware) ``` -## Radar Setup (One-Time) +Then open `http://localhost:8080` or use the touchscreen. + +--- -The OPS243-A needs a one-time configuration to enable rolling buffer mode with hardware sound triggering. This saves settings to flash memory so it boots in the correct mode every time. +## What the Script Configures (Reference) -> **Why?** The OPS243-A has a firmware bug where the HOST_INT pin mode switches unexpectedly when entering rolling buffer mode at runtime. Saving to flash and power cycling bypasses this. Confirmed by OmniPreSense engineering. +You don't need this section unless something went wrong or you prefer to do +things by hand. -### 1. Configure and Save +### OPS243-A Rolling Buffer Mode + +The OPS243-A needs a one-time configuration to enable rolling buffer mode with +hardware sound triggering, saved to flash so it boots correctly every time. + +> **Why?** The OPS243-A has a firmware bug where the HOST_INT pin mode switches +> unexpectedly when entering rolling buffer mode at runtime. Saving to flash and +> power cycling bypasses this. Confirmed by OmniPreSense engineering. + +

+Manual steps ```bash +# 1. Configure and save to flash uv run python scripts/hardware-test/test_rolling_buffer_persist.py --setup + +# 2. Power cycle: unplug the radar's USB cable, wait 3 seconds, plug back in + +# 3. Verify — make a sound near the SEN-14262, you should see I/Q trigger data +uv run python scripts/hardware-test/test_rolling_buffer_persist.py --test ``` -### 2. Power Cycle +
-Unplug the radar's USB cable, wait 3 seconds, plug it back in. +### K-LD7 Device Names -### 3. Verify +USB serial adapters can swap between `/dev/ttyUSB0` and `/dev/ttyUSB1` after a +reboot, so OpenFlight needs fixed names (`/dev/kld7_vertical` and +`/dev/kld7_horizontal`) to tell the two radars apart. The wizard handles this — +you just plug each radar in when asked: ```bash -uv run python scripts/hardware-test/test_rolling_buffer_persist.py --test +./scripts/setup/setup_kld7_devices.sh # run / redo the mapping +./scripts/setup/setup_kld7_devices.sh --show # check the current mapping ``` -Make a sound near the SEN-14262 — you should see trigger data with I/Q samples. +It also installs the FTDI low-latency rule (the K-LD7 RADC stream runs at +3 Mbaud and needs `latency_timer=1ms` instead of the Linux default 16ms). +On startup, the server logs should show both radars at `1ms`: -## K-LD7 Angle Radar Setup - -Each K-LD7 connects via a 3.3V FTDI USB-to-serial adapter and appears as `/dev/ttyUSB*`. +```text +[KLD7:vertical] USB serial latency_timer=1ms ... +[KLD7:horizontal] USB serial latency_timer=1ms ... +``` -### Stable Device Names (udev rules) +
+Manual steps (what the wizard does) -USB serial devices can swap between `/dev/ttyUSB0` and `/dev/ttyUSB1` after a reboot depending on enumeration order. To assign fixed names based on each FTDI adapter's unique serial number: +Find each adapter's serial number: ```bash -# Find the serial numbers for each adapter udevadm info -a /dev/ttyUSB0 | grep '{serial}' | head -1 udevadm info -a /dev/ttyUSB1 | grep '{serial}' | head -1 ``` -Create a udev rule with the serial numbers: +Create a udev rule with the serial numbers (replace `FTXXXXXX`/`FTYYYYYY`): ```bash sudo tee /etc/udev/rules.d/99-kld7.rules << 'EOF' @@ -100,30 +131,57 @@ EOF sudo udevadm control --reload-rules && sudo udevadm trigger ``` -Replace `FTXXXXXX` and `FTYYYYYY` with the actual serial numbers. Now the radars are always at `/dev/kld7_vertical` and `/dev/kld7_horizontal` regardless of plug order. +Then install the latency rule: + +```bash +sudo scripts/setup/setup_kld7_latency.sh +``` + +Use `--dry-run` to preview the rule, or `--all-ftdi` if the `/dev/kld7_*` +names aren't set up yet. + +
-### FTDI Low-Latency Mode +### Auto-Start on Boot -The K-LD7 RADC stream runs at 3 Mbaud, so the FTDI USB adapters should use a -`latency_timer` of `1ms` instead of the Linux default `16ms`. Install the -OpenFlight udev rule once after the stable `/dev/kld7_*` symlinks are working: +The setup script installs and enables a systemd service configured for your +username and install path. + +
+Manual steps and service management ```bash -sudo scripts/setup/setup_kld7_latency.sh +# Install (adjust User= and paths in the file if your username isn't the default) +sudo cp ~/openflight/scripts/setup/openflight.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable openflight +sudo systemctl start openflight ``` -The script writes `/etc/udev/rules.d/99-openflight-kld7-latency.rules`, applies -the value to currently connected K-LD7 adapters, and reloads udev so it persists -after reboot or replug. Use `--dry-run` to preview the exact rule, or -`--all-ftdi` if the K-LD7 adapters do not have stable symlinks yet. +Management: -On startup, confirm the server logs show both radars at `1ms`: +```bash +sudo systemctl status openflight --no-pager # Check status +journalctl -u openflight -f # View logs +sudo systemctl stop openflight # Stop +sudo systemctl restart openflight # Restart +sudo systemctl disable openflight # Disable auto-start +``` -```text -[KLD7:vertical] USB serial latency_timer=1ms ... -[KLD7:horizontal] USB serial latency_timer=1ms ... +To modify the service: + +```bash +sudo nano /etc/systemd/system/openflight.service +sudo systemctl daemon-reload +sudo systemctl restart openflight ``` +
+ +--- + +## K-LD7 Physical Setup + ### Mounting - **Vertical unit** — measures launch angle. Mount with the antenna plane vertical, aimed at the hitting area. @@ -153,14 +211,9 @@ See [K-LD7 Troubleshooting](kld7-troubleshooting.md) for more details. ### Kiosk Mode (Fullscreen — Recommended) ```bash -# Default: rolling buffer + sound trigger -./scripts/start-kiosk.sh - -# With K-LD7 launch-angle geometry defaults -./scripts/start-kiosk.sh --kld7-geometry - -# Mock mode (no hardware needed) -./scripts/start-kiosk.sh --mock +./scripts/start-kiosk.sh # Default: rolling buffer + sound trigger +./scripts/start-kiosk.sh --kld7-geometry # With K-LD7 launch-angle geometry defaults +./scripts/start-kiosk.sh --mock # Mock mode (no hardware needed) ``` ### Manual Start @@ -178,34 +231,6 @@ Then open `http://localhost:8080`. DISPLAY=:0 ./scripts/start-kiosk.sh ``` -## Auto-Start on Boot - -### Enable the Service - -```bash -sudo cp ~/openflight/scripts/setup/openflight.service /etc/systemd/system/ -sudo systemctl daemon-reload -sudo systemctl enable openflight -sudo systemctl start openflight -``` - -### Service Management - -```bash -sudo systemctl status openflight --no-pager # Check status -journalctl -u openflight -f # View logs -sudo systemctl stop openflight # Stop -sudo systemctl restart openflight # Restart -sudo systemctl disable openflight # Disable auto-start -``` - -To modify the service: -```bash -sudo nano /etc/systemd/system/openflight.service -sudo systemctl daemon-reload -sudo systemctl restart openflight -``` - ## Observability (Grafana Cloud) OpenFlight can ship session logs to Grafana Cloud for long-term analysis. @@ -233,14 +258,17 @@ See the [Sound Trigger Wiring Guide — Troubleshooting](sound-trigger-wiring.md ### K-LD7 Not Connecting ```bash -# Check USB devices -ls /dev/ttyUSB* /dev/kld7_* +# Check the device mapping +./scripts/setup/setup_kld7_devices.sh --show # Test standalone uv run python scripts/hardware-test/test_kld7.py ``` -Look for `[KLD7] Connected on /dev/ttyUSB...` in the server logs. See [K-LD7 Troubleshooting](kld7-troubleshooting.md) for "Wrong length reply" and other connection issues. +If the mapping is missing or points at the wrong radar, re-run the wizard: +`./scripts/setup/setup_kld7_devices.sh`. Look for `[KLD7] Connected on +/dev/ttyUSB...` in the server logs. See [K-LD7 Troubleshooting](kld7-troubleshooting.md) +for "Wrong length reply" and other connection issues. ### Service Won't Start @@ -286,11 +314,20 @@ openflight-server --mock # Mock mode openflight-server --web-port 3000 # Custom port ``` +### Setup + +```bash +./scripts/setup/setup.sh # Full interactive setup (re-run safe) +./scripts/setup/setup.sh --deps-only # Dependencies only +./scripts/setup/setup_kld7_devices.sh # K-LD7 device naming wizard +./scripts/setup/setup_kld7_devices.sh --show # Show current K-LD7 mapping +``` + ### Testing ```bash uv run python scripts/hardware-test/test_rolling_buffer_persist.py --test # Sound trigger uv run python scripts/hardware-test/test_sound_trigger_hardware.py # Direct trigger test uv run python scripts/hardware-test/test_kld7.py # K-LD7 standalone -uv run pytest tests/ -v # Full test suite +uv run pytest tests/ -v # Full test suite ``` diff --git a/docs/trackman-test-process.md b/docs/trackman-test-process.md index d8a67b872..502856cf9 100644 --- a/docs/trackman-test-process.md +++ b/docs/trackman-test-process.md @@ -40,7 +40,8 @@ For every test pass, preserve enough raw data and diagnostics to answer: spin rail filtering. 3. Confirm OPS243 rolling buffer mode is active and sound trigger re-arms after each shot. -4. Confirm K-LD7 orientation and udev symlinks: +4. Confirm K-LD7 orientation and udev symlinks + (`./scripts/setup/setup_kld7_devices.sh --show`): - horizontal: `/dev/kld7_horizontal` - vertical: `/dev/kld7_vertical` 5. Confirm both K-LD7 FTDI adapters are in low-latency mode. Run this once on diff --git a/scripts/setup/setup.sh b/scripts/setup/setup.sh index bdac4b619..b097df82f 100755 --- a/scripts/setup/setup.sh +++ b/scripts/setup/setup.sh @@ -1,8 +1,20 @@ #!/bin/bash # # OpenFlight Setup Script -# Installs all Python and Node.js dependencies for first-time setup # +# Installs all dependencies, then (on a Raspberry Pi) walks through the +# one-time hardware configuration interactively: +# - OPS243-A rolling buffer flash config +# - K-LD7 device naming + FTDI low-latency rules +# - Auto-start on boot (systemd service) +# - Desktop shortcut +# +# Usage: +# ./scripts/setup/setup.sh # full interactive setup +# ./scripts/setup/setup.sh --deps-only # install dependencies, skip hardware +# ./scripts/setup/setup.sh --non-interactive # no prompts (deps only) +# +# Safe to re-run at any time. set -e @@ -16,20 +28,46 @@ YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color -log() { - echo -e "${GREEN}[OpenFlight]${NC} $1" -} +log() { echo -e "${GREEN}[OpenFlight]${NC} $1"; } +warn() { echo -e "${YELLOW}[OpenFlight]${NC} $1"; } +error() { echo -e "${RED}[OpenFlight]${NC} $1"; } +info() { echo -e "${BLUE}[OpenFlight]${NC} $1"; } -warn() { - echo -e "${YELLOW}[OpenFlight]${NC} $1" -} +INTERACTIVE=true +DEPS_ONLY=false -error() { - echo -e "${RED}[OpenFlight]${NC} $1" -} +while [[ $# -gt 0 ]]; do + case "$1" in + --non-interactive) + INTERACTIVE=false + DEPS_ONLY=true + shift + ;; + --deps-only) + DEPS_ONLY=true + shift + ;; + --help|-h) + awk 'NR>1 && !/^#/{exit} NR>1{sub(/^# ?/,""); print}' "$0" + exit 0 + ;; + *) + error "Unknown option: $1 (try --help)" + exit 1 + ;; + esac +done -info() { - echo -e "${BLUE}[OpenFlight]${NC} $1" +# Ask a yes/no question. Returns 0 for yes. Usage: confirm "Question?" [Y|N] +confirm() { + local question="$1" + local default="${2:-N}" + local prompt suffix answer + if [ "$default" == "Y" ]; then suffix="[Y/n]"; else suffix="[y/N]"; fi + prompt="$(echo -e "${BLUE}[OpenFlight]${NC} ${question} ${suffix} ")" + read -r -p "$prompt" answer + answer="${answer:-$default}" + [[ "$answer" =~ ^[Yy]$ ]] } cd "$PROJECT_DIR" @@ -58,12 +96,16 @@ else warn "Unknown platform: $OSTYPE" fi +# ────────────────────────────────────────────────────────────────────── +# Phase 1: Dependencies +# ────────────────────────────────────────────────────────────────────── + # Check for Python 3.9+ log "Checking Python version..." if command -v python3 &> /dev/null; then PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') - PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1) - PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2) + PYTHON_MAJOR=$(echo "$PYTHON_VERSION" | cut -d. -f1) + PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2) if [ "$PYTHON_MAJOR" -ge 3 ] && [ "$PYTHON_MINOR" -ge 9 ]; then log "Python $PYTHON_VERSION found ✓" @@ -102,15 +144,12 @@ if ! command -v uv &> /dev/null; then log "Installing uv (fast Python package manager)..." curl -LsSf https://astral.sh/uv/install.sh | sh # Source the new path - export PATH="$HOME/.cargo/bin:$PATH" + export PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH" fi # Create virtual environment -log "Creating Python virtual environment..." -if [ "$PLATFORM" == "pi" ]; then - python3 -m venv .venv - log "Created venv" -else +if [ ! -d .venv ]; then + log "Creating Python virtual environment..." python3 -m venv .venv log "Created venv" fi @@ -123,14 +162,12 @@ log "Activated virtual environment" log "Installing Python dependencies..." if command -v uv &> /dev/null; then uv pip install -e ".[ui,analysis]" - - # Camera dependencies are disabled for the radar-only production path. - # If camera support returns, re-enable the optional camera extra in - # pyproject.toml and restore installation here. else pip install -e ".[ui,analysis]" - # Camera dependencies are disabled for the radar-only production path. fi +# Camera dependencies are disabled for the radar-only production path. +# If camera support returns, re-enable the optional camera extra in +# pyproject.toml and restore installation here. log "Python dependencies installed ✓" # Install dev dependencies @@ -154,9 +191,7 @@ log "UI built ✓" cd .. # Make scripts executable -log "Making scripts executable..." -chmod +x scripts/*.sh -chmod +x scripts/setup/*.sh +chmod +x scripts/*.sh scripts/setup/*.sh # Run tests to verify installation log "Running tests to verify installation..." @@ -166,38 +201,94 @@ else warn "Some tests failed - installation may be incomplete" fi +# ────────────────────────────────────────────────────────────────────── +# Phase 2: Hardware configuration (Raspberry Pi, interactive) +# ────────────────────────────────────────────────────────────────────── + +if [ "$PLATFORM" == "pi" ] && [ "$DEPS_ONLY" == "false" ] && [ "$INTERACTIVE" == "true" ]; then + echo "" + echo -e "${GREEN}=== Hardware Setup ===${NC}" + echo "" + info "Dependencies are installed. The next steps configure your hardware." + info "You can skip any step and re-run this script later." + + # --- OPS243-A rolling buffer flash config --- + echo "" + if confirm "Configure the OPS243-A radar now? (it must be plugged in)" "Y"; then + log "Saving rolling buffer mode to the radar's flash memory..." + if python scripts/hardware-test/test_rolling_buffer_persist.py --setup; then + echo "" + info "Now power cycle the radar: unplug its USB cable, wait 3 seconds," + info "and plug it back in. (This works around a firmware bug — one time only.)" + read -r -p "Press Enter after plugging it back in... " _ + sleep 2 + log "Verifying (make a sharp sound near the sound detector when asked)..." + if python scripts/hardware-test/test_rolling_buffer_persist.py --test; then + log "OPS243-A configured ✓" + else + warn "Verification failed. See docs/raspberry-pi-setup.md → Radar Setup." + fi + else + warn "Radar configuration failed — is the OPS243-A plugged in?" + warn "You can re-run this script, or see docs/raspberry-pi-setup.md." + fi + else + info "Skipped. Run later with:" + info " uv run python scripts/hardware-test/test_rolling_buffer_persist.py --setup" + fi + + # --- K-LD7 device naming + latency --- + echo "" + if confirm "Do you have K-LD7 angle radars to set up?" "N"; then + "$SCRIPT_DIR/setup_kld7_devices.sh" + else + info "Skipped. Run later with: ./scripts/setup/setup_kld7_devices.sh" + fi + + # --- Auto-start service --- + echo "" + if confirm "Start OpenFlight automatically on boot?" "N"; then + log "Installing systemd service for user '$USER'..." + sed -e "s|^User=.*|User=$USER|" \ + -e "s|/home/coleman/openflight|$PROJECT_DIR|g" \ + "$SCRIPT_DIR/openflight.service" | sudo tee /etc/systemd/system/openflight.service > /dev/null + sudo systemctl daemon-reload + sudo systemctl enable openflight + log "Service installed and enabled ✓ (starts on next boot)" + info "Manage it with: sudo systemctl {start|stop|status} openflight" + else + info "Skipped. See docs/raspberry-pi-setup.md → Auto-Start on Boot." + fi + + # --- Desktop shortcut --- + echo "" + if confirm "Add an OpenFlight shortcut to the desktop?" "N"; then + mkdir -p "$HOME/Desktop" + sed -e "s|/home/coleman/openflight|$PROJECT_DIR|g" \ + "$SCRIPT_DIR/OpenFlight.desktop" > "$HOME/Desktop/OpenFlight.desktop" + chmod +x "$HOME/Desktop/OpenFlight.desktop" + log "Desktop shortcut added ✓" + fi +elif [ "$PLATFORM" == "pi" ]; then + info "Skipping hardware setup ($([ "$INTERACTIVE" == "false" ] && echo "non-interactive" || echo "--deps-only"))." + info "Run ./scripts/setup/setup.sh again without flags to configure hardware." +fi + +# ────────────────────────────────────────────────────────────────────── +# Done +# ────────────────────────────────────────────────────────────────────── + echo "" echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ Setup Complete! 🎉 ║${NC}" echo -e "${GREEN}╚═══════════════════════════════════════════╝${NC}" echo "" -log "To activate the virtual environment:" -echo " source .venv/bin/activate" +log "Start OpenFlight:" +echo " ./scripts/start-kiosk.sh # Default: rolling buffer + sound trigger" +echo " ./scripts/start-kiosk.sh --kld7 # With K-LD7 angle radars" +echo " ./scripts/start-kiosk.sh --mock # Mock mode (no hardware)" echo "" -if [ "$PLATFORM" == "pi" ]; then - log "IMPORTANT: Configure the radar for rolling buffer mode (one-time):" - echo " uv run python scripts/hardware-test/test_rolling_buffer_persist.py --setup" - echo " # Then power cycle the radar (unplug USB, wait 3s, replug)" - echo " uv run python scripts/hardware-test/test_rolling_buffer_persist.py --test" - echo "" -fi -log "To start the server:" -echo " ./scripts/start-kiosk.sh # Default: rolling buffer + sound trigger" -echo " ./scripts/start-kiosk.sh --mock # Mock mode (no radar)" +log "Then open http://localhost:8080 (or use the touchscreen)." echo "" -if [ "$PLATFORM" == "pi" ]; then - log "To set up auto-start on boot:" - echo " sudo cp scripts/openflight.service /etc/systemd/system/" - echo " sudo systemctl daemon-reload" - echo " sudo systemctl enable openflight" - echo "" - log "To set up log shipping to Grafana Cloud:" - echo " sudo scripts/setup/setup_alloy.sh" - echo "" - log "To add desktop shortcut:" - echo " cp scripts/OpenFlight.desktop ~/Desktop/" - echo " chmod +x ~/Desktop/OpenFlight.desktop" - echo "" -fi -log "For more info, see docs/raspberry-pi-setup.md" +log "For details and troubleshooting, see docs/raspberry-pi-setup.md" echo "" diff --git a/scripts/setup/setup_kld7_devices.sh b/scripts/setup/setup_kld7_devices.sh new file mode 100755 index 000000000..c9f609ee1 --- /dev/null +++ b/scripts/setup/setup_kld7_devices.sh @@ -0,0 +1,221 @@ +#!/bin/bash +# +# Interactive K-LD7 USB adapter naming wizard. +# +# Identifies each K-LD7's FTDI adapter by plug-in order — no serial numbers +# to look up, no udev rules to edit by hand. Writes a udev rule so the +# radars always appear at /dev/kld7_vertical and /dev/kld7_horizontal no +# matter which USB port they use or what order they enumerate in, then +# installs the FTDI low-latency rule. +# +# Usage: +# scripts/setup/setup_kld7_devices.sh # interactive wizard +# scripts/setup/setup_kld7_devices.sh --show # show current mapping +# +# Re-run any time to redo the mapping (e.g. after replacing an adapter). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RULE_FILE="/etc/udev/rules.d/99-kld7.rules" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { echo -e "${GREEN}[K-LD7 Setup]${NC} $1"; } +warn() { echo -e "${YELLOW}[K-LD7 Setup]${NC} $1"; } +err() { echo -e "${RED}[K-LD7 Setup]${NC} $1"; } +ask() { echo -e "${BLUE}[K-LD7 Setup]${NC} $1"; } + +show_mapping() { + echo "" + if [ -e /dev/kld7_vertical ] || [ -e /dev/kld7_horizontal ]; then + log "Current K-LD7 device names:" + for name in kld7_vertical kld7_horizontal; do + if [ -e "/dev/$name" ]; then + echo " /dev/$name -> $(readlink -f "/dev/$name")" + else + echo " /dev/$name -> (radar not connected or not mapped)" + fi + done + else + warn "No /dev/kld7_* device names found." + fi + if [ -f "$RULE_FILE" ]; then + echo "" + log "Installed rule ($RULE_FILE):" + sed 's/^/ /' "$RULE_FILE" + fi +} + +if [ "${1:-}" == "--show" ]; then + show_mapping + exit 0 +fi + +if ! command -v udevadm &> /dev/null; then + err "udevadm not found — this wizard only runs on Linux (Raspberry Pi)." + exit 1 +fi + +list_ttyusb() { + ls /dev/ttyUSB* 2>/dev/null || true +} + +# Wait for exactly one new /dev/ttyUSB* device to appear vs a snapshot. +# Prints the new device path. +wait_for_new_device() { + local before="$1" + local waited=0 + while [ "$waited" -lt 90 ]; do + sleep 1 + waited=$((waited + 1)) + local now new + now="$(list_ttyusb)" + new="$(comm -13 <(echo "$before" | sort) <(echo "$now" | sort) | head -1)" + if [ -n "$new" ]; then + # Give udev a moment to finish setting the device up + sleep 2 + echo "$new" + return 0 + fi + done + return 1 +} + +device_serial() { + udevadm info -q property -n "$1" 2>/dev/null | sed -n 's/^ID_SERIAL_SHORT=//p' +} + +device_usb_path() { + udevadm info -q property -n "$1" 2>/dev/null | sed -n 's/^ID_PATH=//p' +} + +identify_adapter() { + # $1 = orientation label. Asks the user to plug that radar in and + # prints "serial|usb_path" for the adapter that appears. + local label="$1" + local before + before="$(list_ttyusb)" + echo "" >&2 + ask "Plug in the ${label} K-LD7's USB cable now (waiting up to 90s)..." >&2 + local dev + if ! dev="$(wait_for_new_device "$before")"; then + err "No new USB serial device appeared. Check the cable and re-run the wizard." >&2 + exit 1 + fi + log "Found ${label} adapter at $dev" >&2 + echo "$(device_serial "$dev")|$(device_usb_path "$dev")" +} + +echo "" +echo -e "${GREEN}=== K-LD7 Device Naming Wizard ===${NC}" +echo "" +echo "This identifies which USB adapter belongs to which radar so OpenFlight" +echo "always knows which K-LD7 is which. You'll unplug both adapters, then" +echo "plug them back in one at a time when asked." +echo "" + +if [ -f "$RULE_FILE" ]; then + show_mapping + echo "" + read -r -p "A mapping already exists. Redo it? [y/N] " redo + if [[ ! "$redo" =~ ^[Yy]$ ]]; then + log "Keeping the existing mapping." + exit 0 + fi +fi + +read -r -p "How many K-LD7 radars do you have? [2/1] " count +count="${count:-2}" +if [[ "$count" != "1" && "$count" != "2" ]]; then + err "Please answer 1 or 2." + exit 1 +fi + +echo "" +ask "Unplug BOTH K-LD7 USB cables from the Pi (leave the OPS243 connected)." +read -r -p "Press Enter when both are unplugged... " _ + +VERT_INFO="$(identify_adapter "VERTICAL (launch angle — mounted upright)")" +VERT_SERIAL="${VERT_INFO%%|*}" +VERT_PATH="${VERT_INFO##*|}" + +HORIZ_SERIAL="" +HORIZ_PATH="" +if [ "$count" == "2" ]; then + HORIZ_INFO="$(identify_adapter "HORIZONTAL (club path — mounted flat)")" + HORIZ_SERIAL="${HORIZ_INFO%%|*}" + HORIZ_PATH="${HORIZ_INFO##*|}" +fi + +# Build the rule. Prefer the adapter's unique serial number; fall back to +# the physical USB port path for cheap clones with no serial (in that case +# the cables must stay in the same USB ports). +RULE_LINES=() +USED_PATH_FALLBACK=false + +rule_for() { + local serial="$1" path="$2" name="$3" + if [ -n "$serial" ]; then + echo "SUBSYSTEM==\"tty\", ATTRS{serial}==\"$serial\", SYMLINK+=\"$name\"" + else + USED_PATH_FALLBACK=true + echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"$path\", SYMLINK+=\"$name\"" + fi +} + +if [ "$count" == "2" ] && [ -n "$VERT_SERIAL" ] && [ "$VERT_SERIAL" == "$HORIZ_SERIAL" ]; then + warn "Both adapters report the same serial number — falling back to USB port position." + VERT_SERIAL="" + HORIZ_SERIAL="" +fi + +RULE_LINES+=("$(rule_for "$VERT_SERIAL" "$VERT_PATH" "kld7_vertical")") +if [ "$count" == "2" ]; then + RULE_LINES+=("$(rule_for "$HORIZ_SERIAL" "$HORIZ_PATH" "kld7_horizontal")") +fi + +echo "" +log "Writing $RULE_FILE (you may be asked for your password):" +printf ' %s\n' "${RULE_LINES[@]}" +printf '%s\n' "${RULE_LINES[@]}" | sudo tee "$RULE_FILE" > /dev/null +sudo udevadm control --reload-rules +sudo udevadm trigger +sleep 2 + +echo "" +ok=true +if [ ! -e /dev/kld7_vertical ]; then + ok=false +fi +if [ "$count" == "2" ] && [ ! -e /dev/kld7_horizontal ]; then + ok=false +fi + +if [ "$ok" == "true" ]; then + log "Device names verified ✓" + show_mapping +else + err "Device names did not appear. Re-run the wizard, or see the manual" + err "steps in docs/raspberry-pi-setup.md (K-LD7 Angle Radar Setup)." + exit 1 +fi + +if [ "$USED_PATH_FALLBACK" == "true" ]; then + echo "" + warn "Your adapters have no unique serial number, so the mapping is tied" + warn "to the physical USB ports. Keep each cable in the port it's in now." +fi + +# FTDI low-latency rule (the 3 Mbaud RADC stream needs latency_timer=1ms) +echo "" +log "Installing the FTDI low-latency rule..." +sudo "$SCRIPT_DIR/setup_kld7_latency.sh" + +echo "" +log "K-LD7 setup complete. The radars are now at:" +echo " /dev/kld7_vertical$([ "$count" == "2" ] && echo " and /dev/kld7_horizontal")" From f995346ad80911c0d2b612684fb435cd2283ceee Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Thu, 11 Jun 2026 13:18:08 -0700 Subject: [PATCH 19/29] [cr] update outdated readme --- README.md | 58 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5888547ee..6026255a2 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,12 @@ OpenFlight is an open-source golf launch monitor that uses Doppler radar to meas ### What It Measures -- **Ball Speed**: 30-220 mph range with ±0.5% accuracy (OPS243-A) +- **Ball Speed**: 35-200 mph range with ±0.5% accuracy (OPS243-A) - **Club Speed**: Detected from pre-impact readings (OPS243-A) - **Smash Factor**: Ball speed / club speed ratio - **Launch Angle**: Vertical launch measured by K-LD7 angle radar - **Club Path**: Horizontal aim direction measured by second K-LD7 -- **Spin Rate**: Via rolling buffer I/Q analysis (~50-60% detection rate) +- **Spin Rate**: Via rolling buffer I/Q analysis (the hardest radar measurement — see [Limitations](#limitations)) - **Carry Distance**: Computed from ball speed, launch angle, and spin ### Hardware at a Glance @@ -33,7 +33,7 @@ OpenFlight is an open-source golf launch monitor that uses Doppler radar to meas | OPS243-A Radar | Ball speed, club speed, spin | $249 | | Raspberry Pi 5 | Runs everything | $60 | | 7" Touchscreen | Shows shot data | $46 | -| SparkFun SEN-14262 | Sound trigger for spin detection | $18 | +| SparkFun SEN-14262 | Impact sound trigger for shot capture | $18 | | K-LD7 (×2) + FTDI adapters | Launch angle + club path | $140 | | Power supply + accessories | | $27 | | **Total** | | **~$540** | @@ -98,9 +98,10 @@ This is browser/tab casting only. OpenFlight does not include native Cast SDK su ``` ┌─────────────┐ USB/Serial ┌─────────────┐ Callback ┌─────────────┐ WebSocket ┌─────────────┐ -│ OPS243-A │ ───────────▶ │ Launch │ ──────────▶ │ Flask │ ──────────▶ │ React │ -│ Radar │ Speed data │ Monitor │ on_shot() │ Server │ "shot" │ UI │ -└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ +│ OPS243-A │ ───────────▶ │ Rolling │ ──────────▶ │ Flask │ ──────────▶ │ React │ +│ Radar │ I/Q buffer │ Buffer │ on_shot() │ Server │ "shot" │ UI │ +└─────────────┘ │ Monitor │ └─────────────┘ └─────────────┘ + └─────────────┘ ▲ ┌─────────────┐ USB/Serial │ │ K-LD7 (×2) │ ──────────────────── angle data ──────────────┘ @@ -135,32 +136,41 @@ The K-LD7 modules are positioned near the OPS243-A, one mounted vertically (laun ### Radar Settings for Golf -| Setting | Value | Why | -| ----------- | -------- | --------------------------- | -| Sample Rate | 20 kHz | Supports up to ~139 mph | -| Buffer Size | 512 | Faster updates (~10-15 Hz) | -| Min Speed | 10 mph | Filter slow movements | -| Direction | Outbound | Ball moving away from radar | -| Power | Max (0) | Best detection range | +| Setting | Value | Why | +| -------------- | ---------------------- | -------------------------------------------- | +| Mode | Rolling buffer | Raw I/Q capture for spin + precise speeds | +| Sample Rate | 30 ksps | Supports up to ~208 mph ball speed | +| Capture | 4096 I/Q samples | ~136 ms around impact | +| Trigger | Sound (SEN-14262) | ~10 µs hardware latency via HOST_INT | +| Min Ball Speed | 35 mph | Filter club waggle and slow movements | +| DC Mask | ~15 mph exclusion zone | Reject body movement and environmental noise | + +These are applied automatically — the one-time flash configuration is handled +by the setup script. ### Python API ```python -from openflight import LaunchMonitor +from openflight.rolling_buffer import RollingBufferMonitor + +monitor = RollingBufferMonitor() # auto-detects the OPS243-A +monitor.connect() +monitor.start() -with LaunchMonitor() as monitor: - print("Swing when ready...") - shot = monitor.wait_for_shot(timeout=60) +print("Swing when ready...") +shot = monitor.wait_for_shot(timeout=60) +if shot: + print(f"Ball Speed: {shot.ball_speed_mph:.1f} mph") + print(f"Est. Carry: {shot.estimated_carry_yards:.0f} yards") - if shot: - print(f"Ball Speed: {shot.ball_speed_mph:.1f} mph") - print(f"Est. Carry: {shot.estimated_carry_yards:.0f} yards") +monitor.stop() +monitor.disconnect() ``` ## Limitations - **Cosine error**: If ball doesn't travel directly toward/away from radar, measured speed will be slightly lower than actual -- **Spin detection**: ~50-60% reliable — depends on signal quality and trigger timing +- **Spin detection**: The hardest radar measurement, especially indoors — the usable signal window ends when the ball hits the net, and short windows can't resolve low spin (commercial radar units have the same constraint and fall back to estimated spin indoors). Low driver-band readings (≤~3100 RPM) are reported at reduced confidence. When spin isn't measured, carry falls back to club-typical spin values. Improving this is an active focus. - **K-LD7 speed aliasing**: The K-LD7 max speed is 62 mph, so it's used only for angle/distance, not speed ### Ball Markings @@ -195,7 +205,9 @@ openflight/ │ ├── server.py # Flask server, K-LD7 correlation, carry │ ├── session_logger.py # JSONL session logging │ ├── kld7/ # K-LD7 angle radar -│ │ ├── tracker.py # Ring buffer, ball/club detection +│ │ ├── radc.py # FFT, phase interferometry, angle extraction +│ │ ├── tracker.py # Ring buffer, shot correlation +│ │ ├── geometry.py # Launch-angle trajectory fitting │ │ └── types.py # Data types │ └── rolling_buffer/ # Spin rate detection │ ├── monitor.py # Rolling buffer monitor @@ -214,7 +226,7 @@ Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Areas of interest: -- **Better spin detection**: Increase reliability beyond 50-60% +- **Better spin detection**: A dechirped Doppler-sideband estimator is in development (`scripts/analysis/replay_spin_dechirp.py`) — help validating it against launch-monitor truth data is especially welcome - **K-LD7 signal processing**: Improve ball detection from sparse radar frames - **Mobile app**: Bluetooth connection to phone From 989d2828c88b32958cb415fa889254e5f64a3517 Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Thu, 11 Jun 2026 13:23:45 -0700 Subject: [PATCH 20/29] [cr] docs for spin replay analysis --- README.md | 1 + docs/spin-dechirp-replay.md | 141 ++++++++++++++++++++++++ scripts/analysis/replay_spin_dechirp.py | 3 + 3 files changed, 145 insertions(+) create mode 100644 docs/spin-dechirp-replay.md diff --git a/README.md b/README.md index 6026255a2..eef6e4e4f 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,7 @@ uv run pytest tests/ -v - **[Sound Trigger Wiring](docs/sound-trigger-wiring.md)** — How to wire the sound trigger - **[Raspberry Pi Setup](docs/raspberry-pi-setup.md)** — Full setup guide - **[Rolling Buffer & Spin Detection](docs/rolling_buffer_spin_detection.md)** — Spin measurement details +- **[Dechirped-Sideband Spin Replay](docs/spin-dechirp-replay.md)** — Next-gen spin estimator test bench - **[K-LD7 Ball Detection Theory](docs/kld7-ball-detection-theory.md)** — How angle detection works - **[K-LD7 Session Review](docs/kld7-session-review.md)** — Offline review workflow for session JSONL files - **[Observability & Log Shipping](docs/observability.md)** — Ship logs to Grafana Cloud diff --git a/docs/spin-dechirp-replay.md b/docs/spin-dechirp-replay.md new file mode 100644 index 000000000..0fc27cd55 --- /dev/null +++ b/docs/spin-dechirp-replay.md @@ -0,0 +1,141 @@ +# Dechirped-Sideband Spin Replay + +Reference for `scripts/analysis/replay_spin_dechirp.py` — the offline test +bench for the next-generation spin estimator. Offline only; it never touches +production behavior. Tune here against truth data first, then port to +`rolling_buffer/processor.py` once the numbers prove out. + +## Why this exists + +A spinning golf ball's rotating asymmetries (seam, logo, dimples) modulate the +radar return, creating **sidebands** flanking the ball's Doppler tone, spaced +at exact multiples of the modulation frequency. Read the spacing → read the +spin. This is how commercial radar units (TrackMan US8845442, FlightScope +US9868044) measure spin. + +The production detector instead FFTs the bandpassed **amplitude envelope**, +which only captures the weak AM component (1-5% modulation depth) and discards +the phase modulation carrying most of the signal energy. + +The trap blocking the sideband approach directly: **ball deceleration**. Drag +at driver speeds chirps the Doppler carrier ~3-4 kHz/s, so over a 60 ms +capture the carrier — and every sideband — sweeps ~200 Hz. Sideband spacings +are 40-270 Hz (2500-8000 RPM), so a plain FFT smears the whole pattern into +one blurred lump. The chirp must be removed first. + +## Pipeline + +1. **Carrier tracking** (`track_carrier`) — short overlapping STFTs (256 + samples ≈ 8.5 ms, 2 ms hop) find the ball Doppler peak near the + OPS243-expected frequency in each frame; a quadratic `f_d(t)` is fitted + through the peaks. SNR collapse marks the end of the usable window (net + impact), which the script reports as `usable_ms`. +2. **Dechirp** (`dechirp`) — multiply the raw I/Q by + `exp(-j·2π·∫f_d(t)dt)` (the conjugate chirp). The decelerating carrier + becomes a stationary tone at 0 Hz; sidebands land at exactly ±m·f_mod, + coherent over the whole window. +3. **Harmonic comb search** (`sideband_spin`) — one large zero-padded FFT, + then a comb over candidate modulation frequencies (33-400 Hz, 0.5 Hz + steps). Each candidate scores `min(upper, lower)` sideband support at + ±1f/±2f/±3f — real spin sidebands are symmetric around the carrier, + noise usually isn't. Each sideband is normalized by its **local** + neighborhood floor, not a global one: the dechirped carrier's skirt + slopes steeply, and with a global floor the lowest candidate frequency + always wins (this was the first-run failure mode). +4. **Harmonic-number disambiguation** — the comb finds the sideband + *spacing*; spin is that spacing (1× logo/asymmetry modulation) or half + of it (the seam's 2-fold symmetry modulates at 2×, the common case per + FlightScope's patent). Sideband support at 1.5× the found spacing — an + odd harmonic — proves the true fundamental is half the spacing. Both + interpretations are emitted (`dechirp_rpm_1x` / `dechirp_rpm_2x`) plus + the pick (`dechirp_rpm`, `dechirp_choice`). +5. **Scoring** — pairs captures with launch-monitor truth and prints + coverage / MAE / median / within-10% for three estimators: production + envelope, dechirped sidebands, and a **harmonic oracle** (the better of + the 1×/2× interpretations per shot — the ceiling a perfect + disambiguation rule could reach). + +## Requirements + +1. **Software** — a normal OpenFlight install (`./scripts/setup/setup.sh + --deps-only`). Runs on any machine, no radar hardware needed. +2. **A session JSONL with shots** — produced automatically by any normal + session. The needed `shot_detected` and `rolling_buffer_capture` (raw + 4096-sample I/Q) entries are logged by default; no special flags. +3. **Spin truth** — hit a session with OpenFlight running alongside a + reference monitor (TrackMan etc.) that exports per-shot spin to CSV, + then pair the two: + + ```bash + uv run python scripts/analysis/compare_trackman.py \ + --openflight session_logs/session_YYYYMMDD_*.jsonl \ + --trackman ~/Downloads/TrackMan_export.csv \ + --output session_logs/comparison_mysession.csv + ``` + + Pairing is by club + chronological order with a ball-speed tolerance, so + the two systems don't need identical shot counts. Hand-rolled CSVs from + other monitors work if they have `shot_number_of`, `match_quality`, + `spin_tm`, and `ball_speed_tm` columns. + +## Running + +```bash +uv run --no-sync python scripts/analysis/replay_spin_dechirp.py \ + --openflight session_logs/session_YYYYMMDD_HHMMSS.jsonl \ + --comparison session_logs/comparison_mysession.csv \ + --output session_logs/spin_dechirp_replay.csv +``` + +Prints the three-row summary and writes a per-shot CSV (both spin +interpretations, comb score, usable window, chirp rate, baseline result). +Under a minute for a ~60-shot session. + +Notes: + +- Only shots with `match_quality == "good"` and a truth spin value are + scored; unmatched warm-up shots are dropped automatically. +- The estimator is research-grade: trust the summary table and + high-comb-score shots, not per-shot `dechirp_rpm` as a final answer. + +## Baseline results (2026-06-11, session_20260511 / comparison_test2, 61 shots) + +| estimator | coverage | result | +|---|---|---| +| production envelope | 12/61 | median error ~3200 rpm | +| dechirp, comb score ≥ 5 | 11/61 | **~1.0% median error** (0.1-1.5% per shot) | +| dechirp + harmonic oracle | 61/61 | 18/61 within 10% | + +Interpretation: the spin information is present in our raw captures and +dechirping recovers it at launch-monitor-grade precision when the comb locks. +The two failure buckets are both visible in the per-shot CSV: + +1. **Harmonic mis-choice** — errors of exactly 2× or ½× where the oracle + column shows the right answer was available. Improving the + disambiguation rule is the highest-value next step. +2. **Low-score noise locks** — shots where no real sideband pattern exists + and the comb picks noise. Needs a comb-score confidence gate before any + value is reported. + +## Next steps (in value order) + +1. Strengthen 1×/2× disambiguation (use odd-harmonic evidence more + aggressively; consider club-band plausibility only as a tiebreaker). +2. Add a comb-score gate and report "not measurable" below it. +3. Validate on a held-out session (different day/rig if possible) before + porting anything to production. More truth-paired sessions from diverse + rigs/nets/balls are the most valuable contribution here. +4. Long term: this estimator is what could lift the production low-band + confidence cap (`SPIN_LOW_BAND_SUSPECT_MAX_RPM` in + `rolling_buffer/processor.py`) — the envelope path cannot distinguish + real ≤3100 RPM spin from red envelope noise in a single 136 ms capture, + but coherent sidebands carry far more evidence. + +## Related + +- `docs/rolling_buffer_spin_detection.md` — production envelope detector +- `scripts/analysis/experiment_spin_windows.py` — earlier window experiments + (shares the JSONL/CSV loaders this script imports) +- TrackMan US8845442 / EP1698380, FlightScope US9868044, Liu 2017 + ("A Micro-Doppler Modulation of Spin Projectile on CW Radar") — the + sideband physics and harmonic-ambiguity math diff --git a/scripts/analysis/replay_spin_dechirp.py b/scripts/analysis/replay_spin_dechirp.py index 3130ab387..189d82f4b 100644 --- a/scripts/analysis/replay_spin_dechirp.py +++ b/scripts/analysis/replay_spin_dechirp.py @@ -25,6 +25,9 @@ --openflight session_logs/session_20260511_120001_range.jsonl \ --comparison session_logs/comparison_test2.csv \ --output session_logs/spin_dechirp_replay_test2.csv + +Full reference (requirements, pipeline, baseline results, next steps): +docs/spin-dechirp-replay.md """ from __future__ import annotations From f9ec7c1ef690ba41ec2d7254bb23e84433b79efb Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Thu, 11 Jun 2026 13:44:44 -0700 Subject: [PATCH 21/29] [cr] give unique UUID for each session --- src/openflight/session_logger.py | 17 ++++++++++++++++ tests/test_session_logger.py | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/openflight/session_logger.py b/src/openflight/session_logger.py index 585642064..f8f0ca855 100644 --- a/src/openflight/session_logger.py +++ b/src/openflight/session_logger.py @@ -7,14 +7,21 @@ import json import logging +import uuid from dataclasses import asdict, dataclass from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional +from . import __version__ from .kld7.radc import RADC_PAYLOAD_BYTES from .ops243 import SpeedReading +# Version of the session JSONL format itself. Bump on breaking changes to +# entry structure; additive changes (new fields, new entry types) do not +# require a bump. Consumed by offline analysis and (eventually) cloud sync. +SESSION_FORMAT_VERSION = 1 + @dataclass class SessionMetadata: @@ -29,6 +36,13 @@ class SessionMetadata: config: Dict[str, Any] mode: str # "rolling-buffer" or "mock" trigger_type: Optional[str] # For rolling-buffer mode: "polling", "threshold", etc. + # Globally unique session identity for cloud sync dedupe. The + # timestamp-based session_id stays for filenames and display; this + # UUID travels inside the data so renamed/copied session files keep + # their identity (see docs/cloud-sync-design.md). + session_uuid: str = "" + format_version: int = 1 + app_version: str = "" class SessionLogger: @@ -146,6 +160,9 @@ def start_session( config=config or {}, mode=mode, trigger_type=trigger_type, + session_uuid=str(uuid.uuid4()), + format_version=SESSION_FORMAT_VERSION, + app_version=__version__, ) self._write_entry("session_start", asdict(metadata)) diff --git a/tests/test_session_logger.py b/tests/test_session_logger.py index 8ad87b88f..1ec574daf 100644 --- a/tests/test_session_logger.py +++ b/tests/test_session_logger.py @@ -500,3 +500,36 @@ def test_clock_sync_disabled_skips_write(self, tmp_path): logger = SessionLogger(log_dir=tmp_path, enabled=False) logger.log_clock_sync(device="ops243", port="x", summary=self._summary()) assert logger.session_path is None + + +class TestSessionIdentity: + """session_start must carry a globally unique ID and format version so + cloud sync can dedupe sessions by content, not filename.""" + + def _start_entry(self, tmp_path): + logger = SessionLogger(log_dir=tmp_path, enabled=True) + logger.start_session(mode="rolling-buffer", trigger_type="sound") + logger.end_session() + session_file = next(tmp_path.glob("session_*.jsonl")) + with session_file.open() as handle: + first = json.loads(handle.readline()) + return first + + def test_session_start_has_uuid_and_format_version(self, tmp_path): + import uuid + + import openflight + + entry = self._start_entry(tmp_path) + assert entry["type"] == "session_start" + # Valid UUID4, distinct from the timestamp-based session_id + parsed = uuid.UUID(entry["session_uuid"]) + assert parsed.version == 4 + assert entry["session_uuid"] != entry["session_id"] + assert entry["format_version"] == 1 + assert entry["app_version"] == openflight.__version__ + + def test_session_uuid_is_unique_per_session(self, tmp_path): + first = self._start_entry(tmp_path / "a") + second = self._start_entry(tmp_path / "b") + assert first["session_uuid"] != second["session_uuid"] From 30cc49ac8493f4655ee8db65f4048fc2a4168569 Mon Sep 17 00:00:00 2001 From: John Pacino Date: Fri, 12 Jun 2026 20:14:35 -0400 Subject: [PATCH 22/29] fix(ops): don't truncate buffer dump when trigger fires near timeout wait_for_hardware_trigger bounded TOTAL elapsed time, so a sound trigger firing in the last ~5s of the 30s wait window had its ~46KB rolling-buffer dump cut off mid-stream at the deadline ("Incomplete capture (missing: Q)"), silently losing the shot. At a 30s re-arm cadence that's roughly 1 in 6 random impacts. Once the first byte arrives the trigger has fired and the dump is in flight: extend the deadline to first_byte + dump_grace (8s, vs the ~4-5s a dump actually takes). The existing completeness and inactivity exits still return as soon as the Q array closes. Co-Authored-By: Claude Fable 5 --- src/openflight/ops243.py | 14 +++++-- tests/test_ops243.py | 87 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/src/openflight/ops243.py b/src/openflight/ops243.py index 1c157ac92..ae5a26839 100644 --- a/src/openflight/ops243.py +++ b/src/openflight/ops243.py @@ -1051,7 +1051,7 @@ def trigger_capture(self, timeout: float = 10.0) -> str: return full_response - def wait_for_hardware_trigger(self, timeout: float = 30.0) -> str: + def wait_for_hardware_trigger(self, timeout: float = 30.0, dump_grace: float = 8.0) -> str: """ Wait for hardware trigger to fire and read the buffer dump. @@ -1060,7 +1060,11 @@ def wait_for_hardware_trigger(self, timeout: float = 30.0) -> str: (HOST_INT). Used with SoundTrigger (SparkFun SEN-14262). Args: - timeout: Maximum time to wait for trigger data + timeout: Maximum time to wait for the trigger to fire + dump_grace: Extra time allowed for the dump to finish once the + first byte has arrived. The ~46KB rolling-buffer dump takes + ~4-5s; a trigger firing near the end of the timeout window + must not have its dump cut off by the original deadline. Returns: Raw response string containing JSON lines, or empty string on timeout @@ -1073,11 +1077,12 @@ def wait_for_hardware_trigger(self, timeout: float = 30.0) -> str: response_lines = [] start_time = time.time() + deadline = start_time + timeout last_data_time = None bytes_received = 0 self.last_hardware_trigger_first_byte_timestamp = None - while (time.time() - start_time) < timeout: + while time.time() < deadline: if self.serial.in_waiting: first_byte_timestamp = time.time() if last_data_time is None else None chunk = self.serial.read(self.serial.in_waiting) @@ -1086,6 +1091,9 @@ def wait_for_hardware_trigger(self, timeout: float = 30.0) -> str: if first_byte_timestamp is not None: last_data_time = first_byte_timestamp self.last_hardware_trigger_first_byte_timestamp = last_data_time + # The trigger fired — the dump is now in flight. Extend + # the deadline so a late trigger gets its full dump. + deadline = max(deadline, last_data_time + dump_grace) logger.debug( "[OPS] Hardware trigger: first byte after %.1fs", last_data_time - start_time, diff --git a/tests/test_ops243.py b/tests/test_ops243.py index 934215f5f..fb9d076a8 100644 --- a/tests/test_ops243.py +++ b/tests/test_ops243.py @@ -1,5 +1,7 @@ """Tests for OPS243 radar driver.""" +import time + import pytest from openflight.ops243 import Direction, OPS243Radar, SpeedReading @@ -340,3 +342,88 @@ def test_raises_when_not_connected(self): radar.serial = None with pytest.raises(ConnectionError): radar.read_clock_sync(samples=1) + + +class _ScheduledSerial: + """Serial stand-in that releases bytes on a wall-clock schedule. + + Models the rolling-buffer dump: nothing arrives until the hardware + trigger fires, then ~46KB streams over several seconds. Schedule is + a list of (seconds_after_reset, bytes) pairs. + """ + + def __init__(self, schedule): + self.is_open = True + self._schedule = sorted(schedule, key=lambda item: item[0]) + self._t0 = time.time() + self._consumed = 0 + + def reset_input_buffer(self): + self._t0 = time.time() + self._consumed = 0 + + def _released(self): + elapsed = time.time() - self._t0 + return b"".join(data for t, data in self._schedule if t <= elapsed) + + @property + def in_waiting(self): + return len(self._released()) - self._consumed + + def read(self, n): + released = self._released() + chunk = released[self._consumed : self._consumed + n] + self._consumed += len(chunk) + return chunk + + +class TestWaitForHardwareTrigger: + """Tests for the hardware-trigger read loop (sound trigger path).""" + + # A complete dump ends with a closed Q array — the loop's completeness + # check requires '"Q"' followed by ']}'. + _DUMP = [ + b'{"sample_time":946.077}\r\n{"trigger_time":946.145}\r\n', + b'{"I":[2168,2187,2155,2154]}\r\n', + b'{"Q":[2048,2050,2047,2049]}', + ] + + def _radar(self, serial_obj): + radar = OPS243Radar.__new__(OPS243Radar) + radar.serial = serial_obj + radar.last_hardware_trigger_first_byte_timestamp = None + return radar + + def test_trigger_near_timeout_still_reads_full_dump(self): + """A trigger firing just before the timeout must not truncate the dump. + + Regression test: the wait loop bounded TOTAL elapsed time, so a + trigger at 26s of a 30s window had its ~4.5s dump cut off at 30.0s + ("Incomplete capture (missing: Q)"). Scaled down: trigger at 0.3s + of a 0.4s window, dump completes at 0.7s. + """ + schedule = [ + (0.30, self._DUMP[0]), + (0.50, self._DUMP[1]), + (0.70, self._DUMP[2]), + ] + radar = self._radar(_ScheduledSerial(schedule)) + response = radar.wait_for_hardware_trigger(timeout=0.4) + assert response == b"".join(self._DUMP).decode("ascii") + + def test_no_trigger_returns_empty_after_timeout(self): + """With no data at all, the wait still times out promptly.""" + radar = self._radar(_ScheduledSerial([])) + start = time.time() + response = radar.wait_for_hardware_trigger(timeout=0.3) + assert response == "" + assert time.time() - start < 1.5 + + def test_complete_dump_returns_before_grace_expires(self): + """A dump that finishes early returns immediately on completeness.""" + schedule = [(0.05, b"".join(self._DUMP))] + radar = self._radar(_ScheduledSerial(schedule)) + start = time.time() + response = radar.wait_for_hardware_trigger(timeout=5.0) + assert response == b"".join(self._DUMP).decode("ascii") + assert time.time() - start < 1.0 From 2862a399a112037628c28181a9ed1add5ed7d9e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:13:10 +0000 Subject: [PATCH 23/29] Bump @types/node from 25.6.0 to 25.9.3 in /ui Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.0 to 25.9.3. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.9.3 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- ui/package-lock.json | 16 ++++++++-------- ui/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 302a13687..9023f87d4 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", - "@types/node": "^25.6.0", + "@types/node": "^25.9.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", @@ -1418,13 +1418,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/react": { @@ -3411,9 +3411,9 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, diff --git a/ui/package.json b/ui/package.json index 783d7ec60..e722638e9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", - "@types/node": "^25.6.0", + "@types/node": "^25.9.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", From f5008cefd0c31c4ae3375f0d5f7c5abe9f9884c2 Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Sun, 14 Jun 2026 17:28:57 -0700 Subject: [PATCH 24/29] [cr] first pass at cloud uploader service --- README.md | 19 ++ docs/cloud-sync-design.md | 178 ++++++++++++++++ docs/cloud-sync.md | 144 +++++++++++++ docs/openflight-cloud-uploader-spec.md | 272 +++++++++++++++++++++++++ pyproject.toml | 1 + scripts/setup/openflight-cloud.service | 12 ++ scripts/setup/openflight-cloud.timer | 13 ++ scripts/setup/setup.sh | 46 +++++ src/openflight/cloud/__init__.py | 0 src/openflight/cloud/cli.py | 91 +++++++++ src/openflight/cloud/client.py | 247 ++++++++++++++++++++++ src/openflight/cloud/commands.py | 239 ++++++++++++++++++++++ src/openflight/cloud/config.py | 65 ++++++ src/openflight/cloud/filtering.py | 163 +++++++++++++++ src/openflight/cloud/spool.py | 164 +++++++++++++++ src/openflight/cloud/trigger.py | 50 +++++ src/openflight/server.py | 21 ++ tests/test_cloud_cli.py | 58 ++++++ tests/test_cloud_client.py | 166 +++++++++++++++ tests/test_cloud_commands.py | 216 ++++++++++++++++++++ tests/test_cloud_config.py | 84 ++++++++ tests/test_cloud_filtering.py | 141 +++++++++++++ tests/test_cloud_spool.py | 127 ++++++++++++ tests/test_cloud_trigger.py | 49 +++++ 24 files changed, 2566 insertions(+) create mode 100644 docs/cloud-sync-design.md create mode 100644 docs/cloud-sync.md create mode 100644 docs/openflight-cloud-uploader-spec.md create mode 100644 scripts/setup/openflight-cloud.service create mode 100644 scripts/setup/openflight-cloud.timer create mode 100644 src/openflight/cloud/__init__.py create mode 100644 src/openflight/cloud/cli.py create mode 100644 src/openflight/cloud/client.py create mode 100644 src/openflight/cloud/commands.py create mode 100644 src/openflight/cloud/config.py create mode 100644 src/openflight/cloud/filtering.py create mode 100644 src/openflight/cloud/spool.py create mode 100644 src/openflight/cloud/trigger.py create mode 100644 tests/test_cloud_cli.py create mode 100644 tests/test_cloud_client.py create mode 100644 tests/test_cloud_commands.py create mode 100644 tests/test_cloud_config.py create mode 100644 tests/test_cloud_filtering.py create mode 100644 tests/test_cloud_spool.py create mode 100644 tests/test_cloud_trigger.py diff --git a/README.md b/README.md index eef6e4e4f..f34972299 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,24 @@ scripts/start-kiosk.sh --mock Then open http://localhost:8080 or use the touchscreen. +### 5. Sync to the cloud (optional) + +OpenFlight can push your sessions to the **FlightWeb** cloud so you can review +shots from any device. It's opt-in, and **raw radar data never leaves your +Pi** — only shot results and session metadata are uploaded (verify with +`openflight-cloud push --dry-run`). + +`setup.sh` offers to enable this and link your Pi. To do it by hand: + +```bash +openflight-cloud link # pair this Pi (enter a short code in your browser) +openflight-cloud status # linked? queued? parked? +``` + +Once linked, sessions sync automatically (on session end and via a ~10-minute +timer that heals wifi outages). See the **[Cloud Sync Guide](docs/cloud-sync.md)** +for details. + ### TV Display Mode OpenFlight also serves a fullscreen-friendly browser display for tablets, TV browsers, or a Chrome tab cast to Chromecast. @@ -241,6 +259,7 @@ uv run pytest tests/ -v - **[Parts List](docs/PARTS.md)** — What to buy - **[Sound Trigger Wiring](docs/sound-trigger-wiring.md)** — How to wire the sound trigger - **[Raspberry Pi Setup](docs/raspberry-pi-setup.md)** — Full setup guide +- **[Cloud Sync](docs/cloud-sync.md)** — Push filtered sessions to FlightWeb - **[Rolling Buffer & Spin Detection](docs/rolling_buffer_spin_detection.md)** — Spin measurement details - **[Dechirped-Sideband Spin Replay](docs/spin-dechirp-replay.md)** — Next-gen spin estimator test bench - **[K-LD7 Ball Detection Theory](docs/kld7-ball-detection-theory.md)** — How angle detection works diff --git a/docs/cloud-sync-design.md b/docs/cloud-sync-design.md new file mode 100644 index 000000000..b23ad4f47 --- /dev/null +++ b/docs/cloud-sync-design.md @@ -0,0 +1,178 @@ +# OpenFlight Cloud Sync — Client-Side Design (Proposal) + +Status: **draft / not implemented**. Design for the two pieces of the planned +cloud service that live in this (public) repo: the ingest API contract the Pi +speaks, and the uploader that speaks it. The service itself lives in a +separate private repo; this document is the interface between them. + +Design priorities, in order: + +1. **The contract is forever.** Fielded Pis update rarely. The wire format + must tolerate old clients indefinitely and evolve additively. +2. **Never upload raw ADC/I-Q.** Filtered session summaries only — keeps + storage costs sane, uploads fast on bad wifi, and is the + privacy-friendly default. +3. **No user babysitting.** Sessions upload themselves when connectivity + exists; "no wifi at the range" is a non-event, not a manual step. + +--- + +## 1. Ingest API contract (v1) + +All endpoints under `https:///v1/`, TLS only. Authentication is +an opaque per-device bearer token (`of_device_...` prefix) — revocable from +the web app, scoped to one device. + +### Device linking (how a Pi gets its token) + +Simplified RFC 8628 device-code flow — the user never copies a long token +onto the Pi: + +``` +Pi Cloud +── ───── +POST /v1/device-link/start + {device_name, client_version} ─────▶ + ◀───── {link_code: "ABCD-1234", + poll_token, interval_s, expires_s} + + (Pi prints: "Go to cloud.openflight.example/link + and enter code ABCD-1234") + +POST /v1/device-link/poll + {poll_token} (repeat) ─────▶ + ◀───── {status: "pending"} + ◀───── {status: "linked", + device_token, device_id} +``` + +The user enters the code on the website while signed in. Token is stored at +`~/.config/openflight/cloud.json`, mode `0600`, never logged. + +### Session upload + +``` +PUT /v1/sessions/{session_id} + Authorization: Bearer + Content-Type: application/x-ndjson + Content-Encoding: gzip + + +``` + +- **`session_id` is the `session_uuid` embedded in the `session_start` + entry** (a UUID4 written at session creation since format_version 1 / + app 0.2.0 — survives file renames and copies). For older sessions that + predate the field, the uploader falls back to a deterministic UUIDv5 of + `(device_id, session filename)`. Either way the auto-push and the manual + push script can both submit the same session and the server dedupes for + free; PUT semantics make retries safe. +- The client prepends one manifest line to the body: + + ```json + {"type": "upload_manifest", "format_version": 1, "client_version": "0.2.0", + "device_id": "...", "filtered": true, "kept_entry_types": ["session_start", ...]} + ``` + +**Responses:** + +| code | meaning | client behavior | +|---|---|---| +| 201 | accepted (`{session_id, shot_count}`) | mark pushed | +| 200 | duplicate — already stored | mark pushed | +| 401 | token invalid/revoked | stop, flag "needs re-link" | +| 402 | quota/entitlement exceeded | park, retry daily | +| 413 | body too large (cap ~20 MB gzipped) | log error, park file | +| 422 | unparseable (`{reason}`) | log error, park file | +| 429 | rate limited (`Retry-After`) | back off | +| 5xx | server trouble | retry with backoff | + +`GET /v1/health` → 200, used by the uploader to short-circuit when offline. + +### Evolution rules + +- The server must **accept and store unknown entry types** (skip parsing, + don't reject) — old and new clients coexist for years. +- Additive changes only within `/v1`; breaking changes get `/v2` and `/v1` + keeps working. + +--- + +## 2. Client-side filtering (the raw-ADC strip) + +Filtering uses an **allowlist**, not a blocklist — a future heavy entry type +added to the session logger can never leak to the cloud by accident: + +``` +keep: session_start, session_end, shot_detected, trigger_event, session_error +drop: rolling_buffer_capture, iq_blocks, iq_reading, reading_accepted, + and anything not on the keep list +guard: any kept line > 32 KB is dropped and counted (belt and suspenders) +``` + +`shot_detected` carries everything the insights product needs (speeds, spin ++ quality/confidence, angles, carry, K-LD7 diagnostics). `trigger_event` and +`session_error` are small and power reliability insights. Raw I/Q and RADC +stay on the Pi where they belong — they remain available locally for the +offline analysis workflows. + +A typical filtered session is **tens of KB gzipped** vs tens of MB raw. + +--- + +## 3. The uploader (spool-and-retry) + +Lives in the public repo. Three entry points, one mechanism: + +``` +openflight-cloud link # device-link flow (one time) +openflight-cloud push [--dry-run] # filter + upload anything unpushed +openflight-cloud status # linked? queued? parked? last error? +``` + +**Mechanism:** + +- The session directory itself is the queue. A session counts as "pushed" + when a sidecar marker (`.jsonl.pushed`) exists — originals are + never moved or modified, state survives crashes, and no database is + involved. +- A **systemd timer** (every ~10 min) runs `push`; the server process also + fires a non-blocking `push` on session end. The timer makes wifi outages + self-healing; the hook makes the happy path fast. Neither can ever delay + shot processing. +- Per-file attempt counter (in the sidecar); after ~20 failures the file is + parked (`.parked`) and reported by `status` instead of retried forever. +- `--dry-run` prints exactly which entry lines would upload — the privacy + answer to "what are you sending?" + +**Config** (`~/.config/openflight/cloud.json`): + +```json +{"endpoint": "https://cloud.openflight.example", + "device_token": "of_device_...", "device_id": "...", "enabled": true} +``` + +Uploading is **opt-in**: nothing leaves the Pi until the user runs +`openflight-cloud link`. Later, the interactive `setup.sh` can offer linking +as an optional step. + +--- + +## 4. Server-side checklist (private repo, for reference) + +Not designed here, but the contract above implies: the four endpoints; +dedupe on `session_id`; entitlement check at ingest (quota → 402); +blob → object storage; parse kept entries → per-shot rows in Postgres; +device management UI (list/revoke); link-code UI. + +## Open questions + +- Tier gating: does the free tier get full history or last-N sessions? + (Affects only server; contract unchanged.) +- Should `iq_reading` summaries (SNR stats, no raw data) join the allowlist + later for radar-health insights? Cheap to add — allowlist makes it an + explicit decision. +- AGPL hygiene: the uploader (public repo) is AGPL like everything here; + the private service must not import code from this repo unless that code + is dual-licensed or contributor-cleared. Keep the boundary at the wire + contract. diff --git a/docs/cloud-sync.md b/docs/cloud-sync.md new file mode 100644 index 000000000..688dd935d --- /dev/null +++ b/docs/cloud-sync.md @@ -0,0 +1,144 @@ +# Cloud Sync + +OpenFlight can push your session logs to the **FlightWeb** cloud so you can +review shots from any device. It's an opt-in `openflight-cloud` CLI that runs +on the Pi. + +> **Privacy promise — raw radar data never leaves your Pi.** The uploader +> applies an allowlist *before* upload: only shot results and session metadata +> are sent. Raw I/Q captures, rolling-buffer dumps, and per-reading detections +> stay local. The server stores exactly what the Pi sends — so the filter is +> enforced here, on the device. Run `openflight-cloud push --dry-run` any time +> to see precisely what would be uploaded. + +## Quick start + +```bash +openflight-cloud link # one-time: pair this Pi with your account +openflight-cloud status # linked? queued? parked? last error? +openflight-cloud push --dry-run # show exactly which entries would upload +openflight-cloud push # filter + upload anything not yet pushed +``` + +`scripts/setup/setup.sh` offers to enable cloud sync and link the Pi for you +(on a Raspberry Pi). Everything below can also be done by hand. + +## Linking a device + +You never copy a long token onto the Pi. Linking uses a short, screen-readable +code (RFC 8628 device flow): + +1. Run `openflight-cloud link`. It prints something like: + + ``` + Go to https://flightweb.fly.dev/link and enter code: ABCD-2345 + + (waiting up to 900s; sign in and enter the code) + ``` + +2. Open that URL in any browser, sign in, and enter the code. +3. The Pi detects the pairing, saves its device token, and enables uploads: + + ``` + Linked! device_id=… . Uploads are now enabled. + ``` + +The code expires after ~15 minutes. If it expires or you mistype, just re-run +`openflight-cloud link`. + +To name the device (defaults to the Pi's hostname): + +```bash +openflight-cloud link --device-name "garage pi" +``` + +## How uploads happen + +Two triggers, one mechanism — both are safe to run at any time and never block +shot processing: + +- **systemd timer** (`openflight-cloud.timer`, ~every 10 min) runs `push`. This + is the safety net that heals wifi outages. +- **On session end**, the server fires a non-blocking `push` for the fast happy + path. If it fails (offline, etc.), the timer picks it up later. + +**The session directory is the queue.** No database is involved — state lives +in sidecar files next to each `session_*.jsonl`, so it survives reboots and +crashes: + +| Sidecar | Meaning | +|---|---| +| `.jsonl.pushed` | Successfully uploaded (won't be sent again). | +| `.jsonl.parked` | Given up on — see `reason`; reported by `status`. | +| `.jsonl.state` | In-flight retry counter / quota cooldown. | + +Originals are never moved or modified. Uploads are idempotent: re-sending a +session that's already stored is safe and dedupes server-side. + +### What gets retried, what gets parked + +| Situation | Behavior | +|---|---| +| Network down / server 5xx | Retried on the next timer tick (exponential per-file backoff); parked after ~20 failures. | +| Rate limited (429) | Backs off for the server-provided interval, then retries. | +| Quota exceeded (402) | Deferred ~24h, then retried automatically. | +| Token rejected (401) | All uploads stop and `status` flags **needs re-link** — run `openflight-cloud link` again. | +| Rejected as malformed/too large (413/422) | Parked (this indicates a client bug and shouldn't happen with correct filtering). | + +## Config + +Stored at `~/.config/openflight/cloud.json`, mode `0600` (the `device_token` is +a bearer credential — keep it secret): + +```json +{ + "endpoint": "https://flightweb.fly.dev", + "device_token": "of_device_…", + "device_id": "…", + "enabled": true +} +``` + +- Written by `link`; read by `push` / `status`. +- Set `"enabled": false` (or delete the file) to turn the uploader into a no-op + without unlinking. +- `endpoint` is configurable in case the production domain moves. + +## systemd units + +Installed by `setup.sh`, or by hand: + +```bash +sudo cp scripts/setup/openflight-cloud.{service,timer} /etc/systemd/system/ +# edit User= and the paths if your install isn't /home/coleman/openflight +sudo systemctl daemon-reload +sudo systemctl enable --now openflight-cloud.timer +``` + +Inspect it: + +```bash +systemctl status openflight-cloud.timer # next run time +journalctl -u openflight-cloud.service # upload logs +``` + +The timer is harmless before you link — uploads stay off until a device token +exists. + +## Troubleshooting + +- **`status` says "not linked"** — run `openflight-cloud link`. +- **"needs re-link" / 401** — the token was revoked or rotated. Re-run + `openflight-cloud link`; the new token replaces the old one in the config. +- **A session is parked** — `status` shows the reason and last error. Parked + sessions are skipped on future runs. To retry one, delete its + `.jsonl.parked` (and `.jsonl.state`) sidecar. +- **Nothing uploads** — confirm `enabled: true` in the config and that the Pi + is online (`openflight-cloud status` reports reachability). +- **"What is it sending?"** — `openflight-cloud push --dry-run` lists every + entry type and count that would be uploaded, and what's dropped. + +## See also + +- [`docs/openflight-cloud-uploader-spec.md`](openflight-cloud-uploader-spec.md) + — the wire contract this client implements (endpoints, status codes, caps). diff --git a/docs/openflight-cloud-uploader-spec.md b/docs/openflight-cloud-uploader-spec.md new file mode 100644 index 000000000..f261ce8e7 --- /dev/null +++ b/docs/openflight-cloud-uploader-spec.md @@ -0,0 +1,272 @@ +# `openflight-cloud` Uploader — Implementation Spec + +**Audience:** an agent building the uploader in the public **openflight** repo +(AGPL-3.0). **Author:** the FlightWeb server team. This describes the wire +contract **as the server actually implements it today** (verified against the +deployed code, not the original draft). It supersedes the speculative parts of +`openflight/docs/cloud-sync-design.md`; where they differ, this wins. + +> Drop this file into the openflight repo (e.g. `docs/openflight-cloud-uploader-spec.md`). +> Nothing here requires importing FlightWeb code — the wire contract *is* the +> boundary between the two repos. + +--- + +## 0. What you're building + +A small CLI, `openflight-cloud`, that lives on the Pi and pushes filtered +session logs to the FlightWeb cloud. Three subcommands, one spool-and-retry +mechanism: + +``` +openflight-cloud link # one-time device pairing +openflight-cloud push [--dry-run] # filter + upload anything unpushed +openflight-cloud status # linked? queued? parked? last error? +``` + +**The single most important rule:** the server stores the device-uploaded blob +**verbatim** — it does *not* re-filter raw radar data out of a device upload. +Therefore **the uploader MUST apply the allowlist filter (§4) before upload.** +The product promise "raw radar data never leaves your Pi" is enforced *here*, +in this client. (The server does filter *manual web uploads* as defense in +depth, but that path is irrelevant to the Pi.) + +--- + +## 1. Endpoint + transport + +- Base URL: configurable; current deployment **`https://flightweb.fly.dev`**. + Store it in config — the production domain may move (openflight vs. flightweb + is undecided). All paths below are under `/v1`. +- TLS only. Bearer auth for session upload; link endpoints are unauthenticated. +- Request/response bodies are JSON except the session-upload body, which is + gzipped NDJSON. +- The server returns `Retry-After` (seconds) on every `429`. + +--- + +## 2. Device linking (RFC 8628-style) + +The user never copies a long token onto the Pi. Flow: + +### 2a. `POST /v1/device-link/start` + +Unauthenticated. Rate limited **per IP: 10 requests / 15 min** (→ `429`). + +Request: +```json +{ "device_name": "garage pi", "client_version": "0.3.0" } +``` +- `device_name`: **required, 1–64 chars** (trimmed). Missing/blank/too-long → `422`. +- `client_version`: optional string. + +Response `200`: +```json +{ "link_code": "ABCD-2345", "poll_token": "", "interval_s": 5, "expires_s": 900 } +``` +- `link_code` format: `^[ABCDEFGHJKLMNPQRSTUVWXYZ]{4}-[2-9]{4}$` (no I/O/0/1 — + built to be read off a screen). Print it for the user: + `Go to /link and enter code ABCD-2345`. +- `poll_token`: high-entropy opaque string; **persist it** for polling. Treat as + a secret (don't log). +- `interval_s` (5): minimum seconds between polls — **honor it**. +- `expires_s` (900): code/poll lifetime (~15 min). + +### 2b. User action (out of band) + +The user opens `/link` in a browser, signs in, and enters the code. + +### 2c. `POST /v1/device-link/poll` + +Rate limited **per poll_token: 30 / 60s** (→ `429`). Poll no faster than +`interval_s`. + +Request: +```json +{ "poll_token": "" } +``` + +Responses (all `200` unless noted): +| Body | Meaning | Client action | +|---|---|---| +| `{"status":"pending"}` | not entered yet | keep polling at `interval_s` | +| `{"status":"expired"}` | code lifetime passed | stop; tell user to re-run `link` | +| `{"status":"linked","device_token":"of_device_…","device_id":""}` | paired | **save token + id, stop polling** | +| `404 {"reason":"unknown_poll_token"}` | unknown OR already consumed | stop; re-run `link` | +| `429` + `Retry-After` | polling too fast | back off | + +**The `linked` response is returned exactly once.** The first successful poll +after the user enters the code issues the token; any subsequent poll with the +same `poll_token` gets `404`. So persist `device_token` + `device_id` +**atomically on first receipt** — if you crash between receiving and saving, +the user must re-link. + +- `device_token` format: `^of_device_[0-9A-Za-z]{32}$`. The server stores only a + SHA-256 hash; this plaintext is shown **once**. + +--- + +## 3. Session upload + +### `PUT /v1/sessions/{session_id}` + +``` +PUT /v1/sessions/1f0e9c2a-7b3d-4e5f-8a9b-0c1d2e3f4a5b +Authorization: Bearer of_device_<…> +Content-Type: application/x-ndjson +Content-Encoding: gzip + + +``` + +Rate limited **per device: 120 / hour** (→ `429`). A full backlog flush stays +well under this; if you somehow hit it, honor `Retry-After`. + +**`{session_id}`** must be a lowercase UUID (server lowercases anyway, but send +lowercase). It is: +- the **`session_uuid`** from the `session_start` entry (UUID4, present since + openflight 0.2.0 / format_version 1) — **preferred**; or +- for older sessions lacking it, a deterministic **UUIDv5 of + `(device_id, session_filename)`** so the same file always maps to the same id + (free dedupe + safe retries). + +If the body contains a `session_start` with a `session_uuid`, it **must equal** +the URL id (case-insensitive) or you get `422 session_uuid_mismatch`. Simplest: +always use the embedded `session_uuid` as the URL id when present. + +**Body:** gzipped NDJSON, filtered per §4, with a manifest first line (§4). +PUT is idempotent — re-uploading the same session is safe and dedupes. + +### Responses → client behavior + +| Code | Body | Meaning | Client action | +|---|---|---|---| +| `201` | `{session_id, shot_count}` | accepted, stored | mark `.pushed` | +| `200` | `{session_id, shot_count}` | duplicate (already stored) | mark `.pushed` | +| `401` | `{reason}` | bad/revoked/missing token (`missing_or_malformed_token` \| `invalid_or_revoked_token`) | stop all uploads; flag "needs re-link" | +| `402` | `{reason:"quota_exceeded"}` | over entitlement | park; retry daily | +| `413` | `{reason}` | too large (`body_too_large` gz>20 MB, or `inflated_too_large` >64 MB) | log + park (should never happen if §4 filtering works) | +| `422` | `{reason}` | unparseable (`invalid_session_id`, `session_uuid_mismatch`, `invalid_gzip`, `no_valid_jsonl`) | log + park; this is a client bug — surface it | +| `429` | `{reason:"rate_limited"}` + `Retry-After` | rate limited | back off `Retry-After` seconds | +| `5xx` | — | server trouble | retry with exponential backoff | + +`200` and `201` are both success — treat identically (mark pushed). Don't +distinguish them for retry purposes. + +### `GET /v1/health` + +`200 {"status":"ok"}`, unauthenticated, no side effects. Use it to +short-circuit `push` when offline (cheap connectivity probe before doing work). + +--- + +## 4. Client-side filtering (the raw-ADC strip) — REQUIRED + +Build the upload body by transforming the session `.jsonl`: + +1. **Prepend one manifest line** as the first line: + ```json + {"type":"upload_manifest","format_version":1,"client_version":"0.3.0", + "device_id":"","filtered":true,"kept_entry_types":[...]} + ``` +2. **Keep only allowlisted entry types** (filter by each line's `type`): + ``` + KEEP: session_start, session_end, shot_detected, trigger_event, session_error + DROP: rolling_buffer_capture, iq_blocks, iq_reading, reading_accepted, + ops_clock_sync, shot_camera, config_change, and ANYTHING not on the keep list + ``` + Use an **allowlist, not a blocklist** — a future heavy entry type the session + logger gains must never leak by default. +3. **Drop any kept line > 32 KB** (and count it) — belt-and-suspenders guard + matching the server's per-line cap. +4. **gzip** the result. Keep it under **20 MB gzipped / 64 MB inflated** (a + filtered session is normally tens of KB, so this is just a safety check — + if a session somehow exceeds it, park the file and report it rather than + uploading raw). + +`--dry-run` should print exactly which entry lines *would* upload (the privacy +answer to "what are you sending?"). The server matches this allowlist exactly, +so anything you keep here is what gets stored. + +> Why this is load-bearing: the server stores the device blob **as received**. +> Whatever you upload is what lives in the cloud. Filtering is not optional. + +--- + +## 5. The uploader mechanism (spool-and-retry) + +- **The session directory is the queue.** A session counts as pushed when a + sidecar marker `".jsonl.pushed"` exists. Never move or modify + originals — state survives crashes, no database needed. +- **Triggers:** a **systemd timer** (~every 10 min) runs `push` (heals wifi + outages); the server process also fires a non-blocking `push` on session end + (fast happy path). Neither may ever block/delay shot processing. +- **Per-file attempt counter** in the sidecar; after ~20 failures, **park** the + file (`".jsonl.parked"`) and report via `status` instead of + retrying forever. +- **Terminal vs. retryable** (see §3 table): `401` stops everything and flags + re-link; `402` parks for daily retry; `413`/`422` park (client bug — these + shouldn't happen with correct filtering); `429`/`5xx` back off and retry. +- Push is **opt-in**: nothing leaves the Pi until the user runs + `openflight-cloud link`. + +--- + +## 6. Config — `~/.config/openflight/cloud.json`, mode `0600` + +```json +{ "endpoint": "https://flightweb.fly.dev", + "device_token": "of_device_…", + "device_id": "…", + "enabled": true } +``` +- `0600`, never logged. `device_token` is a bearer credential. +- `enabled:false` (or file absent) → uploader is a no-op. +- Written by `link` on success; read by `push`/`status`. + +--- + +## 7. Evolution rules (so old Pis keep working forever) + +- The server **accepts and stores unknown entry types** in the blob (it skips + parsing them, doesn't reject) — but you should still filter to the allowlist + so raw data doesn't leak. +- The server **accepts unknown fields** on known entry types. +- `/v1` is append-only and tolerates old clients indefinitely. Breaking changes + would ship as `/v2`; `/v1` keeps working. So a fielded Pi that never updates + keeps uploading forever. +- Send a real `client_version` in `device-link/start` and the manifest — it's + stored per-device and helps the server team support old clients. + +--- + +## 8. Reference values (verified against deployed server) + +| Thing | Value | +|---|---| +| Link code regex | `^[ABCDEFGHJKLMNPQRSTUVWXYZ]{4}-[2-9]{4}$` | +| Device token regex | `^of_device_[0-9A-Za-z]{32}$` | +| Poll interval / code TTL | 5 s / 900 s | +| Rate: link-start | 10 / 15 min per IP | +| Rate: poll | 30 / 60 s per poll_token | +| Rate: upload | 120 / hour per device | +| Caps | 20 MB gzipped, 64 MB inflated, 32 KB per line | +| Allowlist (keep) | session_start, session_end, shot_detected, trigger_event, session_error (+ upload_manifest) | +| Health | `GET /v1/health` → `200 {"status":"ok"}` | + +--- + +## 9. Suggested build order + +1. `link` against `/v1/device-link/{start,poll}` → write config. Verify a real + device row appears (the user will see it on the FlightWeb Devices page). +2. Filtering + manifest (§4) with `--dry-run` first — get the body exactly right + before uploading anything. +3. `push` for a single session → confirm `201`, then re-run → confirm `200` + duplicate. +4. Spool/sidecar/park mechanics + systemd timer. +5. `status` reporting. + +**Exit test (shared with the server team):** a real Pi runs `link`, the user +enters the code, a session uploads, and it appears on FlightWeb. That closes +the Phase 1 loop. diff --git a/pyproject.toml b/pyproject.toml index 73f8b9107..c4a5906ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ analysis = [ [project.scripts] openflight-server = "openflight.server:main" +openflight-cloud = "openflight.cloud.cli:main" [project.urls] Homepage = "https://github.com/jewbetcha/openflight" diff --git a/scripts/setup/openflight-cloud.service b/scripts/setup/openflight-cloud.service new file mode 100644 index 000000000..987f75a2f --- /dev/null +++ b/scripts/setup/openflight-cloud.service @@ -0,0 +1,12 @@ +[Unit] +Description=OpenFlight cloud uploader (push filtered sessions to FlightWeb) +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +User=coleman +WorkingDirectory=/home/coleman/openflight +# Uses the venv's console script; falls back to module form if not on PATH. +ExecStart=/home/coleman/openflight/.venv/bin/openflight-cloud push +# Opt-in: this no-ops until the user runs `openflight-cloud link`. diff --git a/scripts/setup/openflight-cloud.timer b/scripts/setup/openflight-cloud.timer new file mode 100644 index 000000000..6d27d415c --- /dev/null +++ b/scripts/setup/openflight-cloud.timer @@ -0,0 +1,13 @@ +[Unit] +Description=Run the OpenFlight cloud uploader every ~10 minutes (heals wifi outages) + +[Timer] +# Fire shortly after boot, then every 10 minutes. RandomizedDelaySec spreads +# load so a fleet of Pis doesn't hit the server in lockstep. +OnBootSec=2min +OnUnitActiveSec=10min +RandomizedDelaySec=60 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/scripts/setup/setup.sh b/scripts/setup/setup.sh index b097df82f..88821355d 100755 --- a/scripts/setup/setup.sh +++ b/scripts/setup/setup.sh @@ -269,6 +269,46 @@ if [ "$PLATFORM" == "pi" ] && [ "$DEPS_ONLY" == "false" ] && [ "$INTERACTIVE" == chmod +x "$HOME/Desktop/OpenFlight.desktop" log "Desktop shortcut added ✓" fi + + # --- Cloud sync (opt-in) --- + echo "" + info "Cloud sync pushes *filtered* session logs (shots, not raw radar) to" + info "FlightWeb so you can review them from anywhere. It is opt-in: nothing" + info "leaves this Pi until you link it to your account." + echo "" + if confirm "Enable cloud sync for this Pi?" "N"; then + log "Installing the cloud uploader timer (runs every ~10 min)..." + for unit in openflight-cloud.service openflight-cloud.timer; do + sed -e "s|^User=.*|User=$USER|" \ + -e "s|/home/coleman/openflight|$PROJECT_DIR|g" \ + "$SCRIPT_DIR/$unit" | sudo tee "/etc/systemd/system/$unit" > /dev/null + done + sudo systemctl daemon-reload + sudo systemctl enable --now openflight-cloud.timer + log "Cloud uploader timer installed and enabled ✓" + + echo "" + if confirm "Link this Pi to your FlightWeb account now?" "Y"; then + log "Starting device linking..." + echo "" + # `openflight-cloud` is on PATH via the activated venv. The command + # prints a short code; enter it in your browser when prompted. + if openflight-cloud link; then + log "Pi linked ✓ — sessions will sync automatically from now on." + else + warn "Linking did not complete. Re-run any time with:" + warn " openflight-cloud link" + fi + else + info "Skipped. Link later (the timer keeps running, but uploads stay" + info "off until you do) with:" + info " openflight-cloud link" + fi + info "Check sync state any time with: openflight-cloud status" + else + info "Skipped. Enable later by re-running this script, or see" + info " docs/cloud-sync.md" + fi elif [ "$PLATFORM" == "pi" ]; then info "Skipping hardware setup ($([ "$INTERACTIVE" == "false" ] && echo "non-interactive" || echo "--deps-only"))." info "Run ./scripts/setup/setup.sh again without flags to configure hardware." @@ -290,5 +330,11 @@ echo " ./scripts/start-kiosk.sh --mock # Mock mode (no hardware)" echo "" log "Then open http://localhost:8080 (or use the touchscreen)." echo "" +log "Cloud sync (optional):" +echo " openflight-cloud link # pair this Pi with FlightWeb" +echo " openflight-cloud status # linked? queued? parked?" +echo " openflight-cloud push --dry-run # see exactly what would upload" +echo "" log "For details and troubleshooting, see docs/raspberry-pi-setup.md" +log "For cloud sync details, see docs/cloud-sync.md" echo "" diff --git a/src/openflight/cloud/__init__.py b/src/openflight/cloud/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/openflight/cloud/cli.py b/src/openflight/cloud/cli.py new file mode 100644 index 000000000..39f3bdd1c --- /dev/null +++ b/src/openflight/cloud/cli.py @@ -0,0 +1,91 @@ +"""``openflight-cloud`` command-line entry point. + +openflight-cloud link # one-time device pairing +openflight-cloud push [--dry-run] # filter + upload anything unpushed +openflight-cloud status # linked? queued? parked? last error? +""" + +import argparse +from pathlib import Path +from typing import List, Optional + +from ..session_logger import SessionLogger +from . import commands +from .client import CloudClient +from .config import CONFIG_PATH, CloudConfig, load_config + +DEFAULT_LOG_DIR = SessionLogger.DEFAULT_LOG_DIR + + +def _build_parser() -> argparse.ArgumentParser: + # Shared options usable either before or after the subcommand. + common = argparse.ArgumentParser(add_help=False) + common.add_argument( + "--config", + type=Path, + default=CONFIG_PATH, + help=f"Path to cloud config (default: {CONFIG_PATH}).", + ) + common.add_argument( + "--log-dir", + type=Path, + default=DEFAULT_LOG_DIR, + help=f"Session log directory (default: {DEFAULT_LOG_DIR}).", + ) + + parser = argparse.ArgumentParser( + prog="openflight-cloud", + description="Push filtered OpenFlight session logs to the FlightWeb cloud.", + parents=[common], + ) + sub = parser.add_subparsers(dest="command") + + link = sub.add_parser( + "link", parents=[common], help="Pair this device with a FlightWeb account." + ) + link.add_argument("--device-name", default=None, help="Device label (default: hostname).") + + push = sub.add_parser("push", parents=[common], help="Upload any unpushed sessions.") + push.add_argument( + "--dry-run", + action="store_true", + help="Show exactly which entries would upload; send nothing.", + ) + + sub.add_parser("status", parents=[common], help="Show link state, queue, and parked sessions.") + return parser + + +def _client(config: CloudConfig) -> CloudClient: + """Build a CloudClient from persisted config.""" + return CloudClient(config.endpoint, token=config.device_token or None) + + +def main(argv: Optional[List[str]] = None) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + + if not args.command: + parser.print_help() + return 1 + + config = load_config(args.config) or CloudConfig() + + if args.command == "link": + ok = commands.cmd_link(config, args.config, _client(config), device_name=args.device_name) + return 0 if ok else 1 + + if args.command == "push": + summary = commands.cmd_push(config, args.log_dir, _client(config), dry_run=args.dry_run) + return 1 if summary.get("needs_relink") else 0 + + if args.command == "status": + commands.cmd_status(config, args.log_dir, _client(config)) + return 0 + + parser.print_help() + return 1 + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/src/openflight/cloud/client.py b/src/openflight/cloud/client.py new file mode 100644 index 000000000..9be3aef39 --- /dev/null +++ b/src/openflight/cloud/client.py @@ -0,0 +1,247 @@ +"""HTTP client for the FlightWeb cloud wire contract. + +Uses the standard library (``urllib``) — no third-party HTTP dependency on the +Pi. The low-level transport is injectable (``request_fn``) so tests run without +the network. + +The contract lives entirely in this module; nothing here imports FlightWeb +code. See docs/openflight-cloud-uploader-spec.md. +""" + +import json +import urllib.error +import urllib.request +from collections import namedtuple +from dataclasses import dataclass +from typing import Callable, Dict, Optional + +API_PREFIX = "/v1" +DEFAULT_TIMEOUT = 30 + +HttpResponse = namedtuple("HttpResponse", ["status", "headers", "body"]) + + +class CloudNetworkError(Exception): + """Raised when the request never reached the server (DNS, TLS, offline).""" + + +class RateLimited(Exception): + """Raised on a 429 for link endpoints; carries Retry-After seconds.""" + + def __init__(self, retry_after: Optional[int]): + super().__init__(f"rate limited, retry after {retry_after}s") + self.retry_after = retry_after + + +class LinkError(Exception): + """Raised when a link request is rejected (e.g. 422 invalid device name).""" + + def __init__(self, message: str, status: int, reason: Optional[str] = None): + super().__init__(message) + self.status = status + self.reason = reason + + +@dataclass +class LinkStart: + """Response from ``device-link/start``: code to show + token to poll.""" + + link_code: str + poll_token: str + interval_s: int + expires_s: int + + +@dataclass +class LinkPoll: + """Response from ``device-link/poll``: pending/expired/linked/unknown.""" + + status: str + device_token: Optional[str] = None + device_id: Optional[str] = None + + +@dataclass +class UploadResult: + """Outcome of an upload, with the action the spool layer should take.""" + + status_code: int + # one of: success, relink, quota, park, rate_limited, retry + action: str + reason: Optional[str] = None + retry_after: Optional[int] = None + session_id: Optional[str] = None + shot_count: Optional[int] = None + + +def _header(headers: Dict[str, str], name: str) -> Optional[str]: + """Case-insensitive header lookup.""" + lowered = {k.lower(): v for k, v in (headers or {}).items()} + return lowered.get(name.lower()) + + +def _retry_after(headers: Dict[str, str]) -> Optional[int]: + value = _header(headers, "Retry-After") + if value is None: + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + +def urllib_request( + method: str, + url: str, + data: Optional[bytes] = None, + headers: Optional[Dict[str, str]] = None, + timeout: int = DEFAULT_TIMEOUT, +) -> HttpResponse: + """Default transport. Returns HttpResponse for any HTTP status (including + 4xx/5xx); raises CloudNetworkError only when the server was unreachable.""" + req = urllib.request.Request(url, data=data, headers=headers or {}, method=method) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + return HttpResponse( + status=resp.status, + headers=dict(resp.headers.items()), + body=resp.read(), + ) + except urllib.error.HTTPError as exc: + # HTTP error responses are valid contract responses, not failures. + return HttpResponse( + status=exc.code, + headers=dict(exc.headers.items()) if exc.headers else {}, + body=exc.read(), + ) + except urllib.error.URLError as exc: + raise CloudNetworkError(str(exc.reason)) from exc + except (TimeoutError, OSError) as exc: + raise CloudNetworkError(str(exc)) from exc + + +class CloudClient: + """Thin client over the ``/v1`` wire contract.""" + + def __init__( + self, + endpoint: str, + token: Optional[str] = None, + request_fn: Callable[..., HttpResponse] = urllib_request, + timeout: int = DEFAULT_TIMEOUT, + ): + self.endpoint = endpoint.rstrip("/") + self.token = token + self._request = request_fn + self.timeout = timeout + + def _url(self, path: str) -> str: + return f"{self.endpoint}{API_PREFIX}{path}" + + def _json_request( + self, method: str, path: str, payload: Optional[dict] = None, headers: Optional[dict] = None + ) -> HttpResponse: + body = json.dumps(payload).encode("utf-8") if payload is not None else None + hdrs = {"Accept": "application/json"} + if body is not None: + hdrs["Content-Type"] = "application/json" + if headers: + hdrs.update(headers) + return self._request(method, self._url(path), data=body, headers=hdrs, timeout=self.timeout) + + @staticmethod + def _body_json(resp: HttpResponse) -> dict: + if not resp.body: + return {} + try: + parsed = json.loads(resp.body.decode("utf-8")) + return parsed if isinstance(parsed, dict) else {} + except (json.JSONDecodeError, ValueError): + return {} + + def health(self) -> bool: + """Cheap connectivity probe; True iff the server reports healthy.""" + try: + resp = self._request( + "GET", self._url("/health"), data=None, headers=None, timeout=self.timeout + ) + except CloudNetworkError: + return False + return resp.status == 200 and self._body_json(resp).get("status") == "ok" + + def device_link_start(self, device_name: str, client_version: str) -> LinkStart: + """Begin pairing; returns a link code to show the user and a poll token.""" + resp = self._json_request( + "POST", + "/device-link/start", + {"device_name": device_name, "client_version": client_version}, + ) + if resp.status == 429: + raise RateLimited(_retry_after(resp.headers)) + body = self._body_json(resp) + if resp.status != 200: + raise LinkError(f"link start failed ({resp.status})", resp.status, body.get("reason")) + return LinkStart( + link_code=body["link_code"], + poll_token=body["poll_token"], + interval_s=int(body.get("interval_s", 5)), + expires_s=int(body.get("expires_s", 900)), + ) + + def device_link_poll(self, poll_token: str) -> LinkPoll: + """Poll for pairing completion; on ``linked`` returns the device token.""" + resp = self._json_request("POST", "/device-link/poll", {"poll_token": poll_token}) + if resp.status == 429: + raise RateLimited(_retry_after(resp.headers)) + if resp.status == 404: + # Unknown OR already-consumed poll token. + return LinkPoll(status="unknown") + body = self._body_json(resp) + if resp.status != 200: + raise LinkError(f"link poll failed ({resp.status})", resp.status, body.get("reason")) + return LinkPoll( + status=body.get("status", "pending"), + device_token=body.get("device_token"), + device_id=body.get("device_id"), + ) + + def upload_session(self, session_id: str, gzipped_body: bytes) -> UploadResult: + """PUT a filtered, gzipped session. Maps status -> spool action.""" + headers = { + "Content-Type": "application/x-ndjson", + "Content-Encoding": "gzip", + } + if self.token: + headers["Authorization"] = f"Bearer {self.token}" + resp = self._request( + "PUT", + self._url(f"/sessions/{session_id}"), + data=gzipped_body, + headers=headers, + timeout=self.timeout, + ) + body = self._body_json(resp) + reason = body.get("reason") + + if resp.status in (200, 201): + return UploadResult( + status_code=resp.status, + action="success", + session_id=body.get("session_id"), + shot_count=body.get("shot_count"), + ) + if resp.status == 401: + return UploadResult(resp.status, action="relink", reason=reason) + if resp.status == 402: + return UploadResult(resp.status, action="quota", reason=reason) + if resp.status in (413, 422): + return UploadResult(resp.status, action="park", reason=reason) + if resp.status == 429: + return UploadResult( + resp.status, + action="rate_limited", + reason=reason, + retry_after=_retry_after(resp.headers), + ) + # 5xx and anything unexpected: back off and retry. + return UploadResult(resp.status, action="retry", reason=reason) diff --git a/src/openflight/cloud/commands.py b/src/openflight/cloud/commands.py new file mode 100644 index 000000000..ad7768ce8 --- /dev/null +++ b/src/openflight/cloud/commands.py @@ -0,0 +1,239 @@ +"""Command orchestration for the openflight-cloud CLI. + +Each function takes its dependencies explicitly (config, client, output sink, +sleep) so the logic is testable without the network or real timers. +""" + +import socket +import time +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional + +from . import filtering, spool +from .client import CloudNetworkError, RateLimited +from .config import CloudConfig, save_config + +OutFn = Callable[[str], None] + + +def cmd_link( + config: CloudConfig, + config_path: Path, + client, + device_name: Optional[str] = None, + sleep: Callable[[float], None] = time.sleep, + now_fn: Callable[[], float] = time.monotonic, + out: OutFn = print, +) -> bool: + """One-time device pairing (RFC 8628-style). Returns True on success.""" + device_name = (device_name or socket.gethostname() or "openflight pi").strip()[:64] + try: + start = client.device_link_start(device_name, filtering.CLIENT_VERSION) + except RateLimited as exc: + out(f"Rate limited starting link; try again in {exc.retry_after}s.") + return False + + link_url = f"{config.endpoint.rstrip('/')}/link" + out("") + out(f" Go to {link_url} and enter code: {start.link_code}") + out("") + out(f" (waiting up to {start.expires_s}s; sign in and enter the code)") + + deadline = now_fn() + start.expires_s + interval = max(1, start.interval_s) + while now_fn() < deadline: + sleep(interval) + try: + poll = client.device_link_poll(start.poll_token) + except RateLimited as exc: + sleep(exc.retry_after or interval) + continue + + if poll.status == "pending": + continue + if poll.status == "linked": + # Persist token + id atomically on first receipt — the linked + # response is returned exactly once. + config.device_token = poll.device_token + config.device_id = poll.device_id + config.enabled = True + save_config(config, config_path) + out(f"Linked! device_id={poll.device_id}. Uploads are now enabled.") + return True + # expired or unknown (consumed/invalid token) + out(f"Link {poll.status}. Re-run `openflight-cloud link`.") + return False + + out("Link timed out. Re-run `openflight-cloud link`.") + return False + + +def _describe_dry_run(filename: str, session_id: str, result: filtering.FilterResult, out: OutFn): + out(f"\n{filename} -> session {session_id}") + if not result.kept_lines: + out(" (nothing to upload — no allowlisted entries)") + for entry_type in sorted(result.kept_type_counts): + out(f" keep {result.kept_type_counts[entry_type]:>5} x {entry_type}") + if result.dropped_oversize: + out(f" drop {result.dropped_oversize:>5} oversize line(s) (>32 KB)") + + +def cmd_push( + config: CloudConfig, + log_dir: Path, + client, + dry_run: bool = False, + out: OutFn = print, +) -> Dict[str, Any]: + """Filter and upload anything unpushed. Returns a summary dict.""" + summary: Dict[str, Any] = { + "uploaded": 0, + "parked": 0, + "deferred": 0, + "failed": 0, + "offline": False, + "needs_relink": False, + "dry_run": dry_run, + } + + if not dry_run and not config.is_active(): + out("Uploader inactive (not linked or disabled). Run `openflight-cloud link`.") + summary["skipped"] = "inactive" + return summary + + # Cheap connectivity probe so we don't churn while offline. + if not dry_run and not client.health(): + out("Cloud unreachable; will retry later.") + summary["offline"] = True + return summary + + pending = spool.pending_sessions(log_dir) + if not pending: + out("Nothing to upload.") + return summary + + for path in pending: + if not dry_run and spool.in_cooldown(path): + summary["deferred"] += 1 + continue + + lines = path.read_text(errors="replace").splitlines() + session_id = filtering.resolve_session_id(lines, config.device_id, path.name) + result = filtering.filter_session_lines(lines, config.device_id) + + if dry_run: + _describe_dry_run(path.name, session_id, result, out) + continue + + try: + body = filtering.build_upload_body(result) + except filtering.BodyTooLargeError as exc: + spool.mark_parked( + path, + reason="body_too_large", + attempts=spool.read_attempts(path), + last_error=str(exc), + ) + summary["parked"] += 1 + out(f"{path.name}: too large after filtering — parked ({exc}).") + continue + + try: + upload = client.upload_session(session_id, body) + except CloudNetworkError as exc: + out(f"{path.name}: network error ({exc}); will retry later.") + summary["offline"] = True + break + + action = upload.action + if action == "success": + spool.mark_pushed(path, session_id, upload.shot_count) + summary["uploaded"] += 1 + out(f"{path.name}: uploaded ({upload.status_code}).") + elif action == "relink": + summary["needs_relink"] = True + out("Device token rejected. Stopping uploads — re-run `openflight-cloud link`.") + break + elif action == "quota": + spool.record_cooldown(path, "quota_exceeded", spool.QUOTA_COOLDOWN_S) + summary["deferred"] += 1 + out(f"{path.name}: quota exceeded — deferring ~24h.") + elif action == "park": + spool.mark_parked( + path, + reason=upload.reason or "client_error", + attempts=spool.read_attempts(path), + last_error=str(upload.status_code), + ) + summary["parked"] += 1 + out(f"{path.name}: rejected ({upload.status_code} {upload.reason}) — parked.") + elif action == "rate_limited": + out(f"Rate limited; backing off {upload.retry_after}s. Will retry later.") + summary["rate_limited"] = upload.retry_after + break + else: # retry (5xx / unexpected) + attempts = spool.record_failure( + path, f"{upload.status_code} {upload.reason or ''}".strip() + ) + summary["failed"] += 1 + if spool.is_parked(path): + summary["parked"] += 1 + out(f"{path.name}: failed {attempts}x — parked.") + else: + out(f"{path.name}: server error ({upload.status_code}); attempt {attempts}.") + + return summary + + +def cmd_status( + config: CloudConfig, + log_dir: Path, + client=None, + out: OutFn = print, +) -> Dict[str, Any]: + """Report link state, queue counts, and parked sessions.""" + out(f"Endpoint: {config.endpoint}") + if config.is_linked(): + out(f"Linked: yes (device_id={config.device_id})") + out(f"Enabled: {'yes' if config.enabled else 'no (uploads paused)'}") + else: + out("Linked: no — this device is not linked. Run `openflight-cloud link` to pair it.") + + online = None + if client is not None and config.is_active(): + online = client.health() + out(f"Reachable: {'yes' if online else 'no (cloud unreachable)'}") + + counts = spool.summarize(log_dir) + out( + f"Sessions: {counts['total']} total | {counts['pushed']} pushed | " + f"{counts['pending']} pending | {counts['parked']} parked" + ) + + parked = _parked_details(log_dir) + if parked: + out("Parked:") + for name, info in parked: + out(f" {name}: {info.get('reason')} (last_error={info.get('last_error')})") + + return { + "counts": counts, + "parked": [name for name, _ in parked], + "linked": config.is_linked(), + "online": online, + } + + +def _parked_details(log_dir: Path) -> List: + import json + + details = [] + for path in spool.session_files(log_dir): + if spool.is_parked(path): + marker = path.with_name(path.name + spool.PARKED_SUFFIX) + try: + info = json.loads(marker.read_text()) + except (json.JSONDecodeError, ValueError, OSError): + info = {} + details.append((path.name, info)) + return details diff --git a/src/openflight/cloud/config.py b/src/openflight/cloud/config.py new file mode 100644 index 000000000..0708f518c --- /dev/null +++ b/src/openflight/cloud/config.py @@ -0,0 +1,65 @@ +"""Persistent config for the openflight-cloud uploader. + +Stored at ``~/.config/openflight/cloud.json`` with mode ``0600``. The +``device_token`` is a bearer credential and must never be logged. The file is +written by ``link`` on success and read by ``push``/``status``. When the file +is absent (or ``enabled`` is false) the uploader is a no-op. +""" + +import json +import os +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Optional + +# Current deployment. The production domain is undecided (openflight vs. +# flightweb), so it lives in config and may move. +DEFAULT_ENDPOINT = "https://flightweb.fly.dev" + +CONFIG_PATH = Path.home() / ".config" / "openflight" / "cloud.json" + + +@dataclass +class CloudConfig: + """Uploader configuration persisted to disk.""" + + endpoint: str = DEFAULT_ENDPOINT + device_token: str = "" + device_id: str = "" + enabled: bool = True + + def is_linked(self) -> bool: + """True when a device token and id are both present.""" + return bool(self.device_token and self.device_id) + + def is_active(self) -> bool: + """True when the uploader should actually push (linked and enabled).""" + return self.enabled and self.is_linked() + + +def load_config(path: Path = CONFIG_PATH) -> Optional[CloudConfig]: + """Load config from ``path``; return None if the file is absent.""" + path = Path(path) + if not path.exists(): + return None + data = json.loads(path.read_text()) + return CloudConfig( + endpoint=data.get("endpoint", DEFAULT_ENDPOINT), + device_token=data.get("device_token", ""), + device_id=data.get("device_id", ""), + enabled=data.get("enabled", True), + ) + + +def save_config(config: CloudConfig, path: Path = CONFIG_PATH) -> None: + """Write config to ``path`` with mode 0600, creating parent dirs.""" + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + # Create with restrictive permissions from the start so the bearer token is + # never briefly world-readable. + fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(fd, "w", encoding="utf-8") as handle: + json.dump(asdict(config), handle, indent=2) + handle.write("\n") + # Re-assert mode in case the file already existed with looser permissions. + os.chmod(path, 0o600) diff --git a/src/openflight/cloud/filtering.py b/src/openflight/cloud/filtering.py new file mode 100644 index 000000000..b37dbe2e5 --- /dev/null +++ b/src/openflight/cloud/filtering.py @@ -0,0 +1,163 @@ +"""Client-side filtering — the raw-ADC strip. + +This is the load-bearing privacy boundary. The FlightWeb server stores a +device upload **verbatim**; it does not re-filter raw radar data out of a +device upload. So the product promise "raw radar data never leaves your Pi" +is enforced *here*, by applying an allowlist before upload. + +Use an allowlist, not a blocklist — any future heavy entry type the session +logger gains must never leak by default. +""" + +import gzip +import json +import uuid +from dataclasses import dataclass, field +from typing import Any, Dict, Iterable, List, Optional + +from .. import __version__ + +CLIENT_VERSION = __version__ + +# Allowlisted entry types — only these are uploaded. ``error`` and +# ``session_error`` are both kept: the session logger currently emits ``error`` +# (see session_logger.py), while the server spec names ``session_error``; +# keeping both is privacy-safe (error entries carry only error strings/context, +# never raw ADC) and future-proofs a rename. +KEEP_ENTRY_TYPES = frozenset( + { + "session_start", + "session_end", + "shot_detected", + "trigger_event", + "session_error", + "error", + } +) + +MANIFEST_TYPE = "upload_manifest" +MANIFEST_FORMAT_VERSION = 1 + +# Per-line cap mirroring the server's per-line guard. Belt-and-suspenders. +MAX_LINE_BYTES = 32 * 1024 +# Body caps mirroring the server. A filtered session is normally tens of KB, +# so these are safety checks, not normal operating limits. +MAX_GZIP_BYTES = 20 * 1024 * 1024 +MAX_INFLATED_BYTES = 64 * 1024 * 1024 + +# Fixed namespace for deterministic UUIDv5 of (device_id, session_filename), +# used for older sessions that predate the embedded session_uuid. A stable +# namespace makes the same file always map to the same id (dedupe + safe retry). +SESSION_NAMESPACE = uuid.UUID("8d8ac610-566d-4ef0-9c22-186b2a5ed793") + + +class BodyTooLargeError(Exception): + """Raised when a filtered body exceeds the gzip/inflated caps.""" + + +@dataclass +class FilterResult: + """Outcome of filtering one session file.""" + + manifest: Dict[str, Any] + kept_lines: List[str] + dropped_oversize: int = 0 + kept_type_counts: Dict[str, int] = field(default_factory=dict) + + +def _iter_entries(lines: Iterable[str]): + """Yield (raw_line, parsed_dict) for parseable JSON lines, skipping junk.""" + for raw in lines: + stripped = raw.strip() + if not stripped: + continue + try: + parsed = json.loads(stripped) + except (json.JSONDecodeError, ValueError): + continue + if isinstance(parsed, dict): + yield stripped, parsed + + +def resolve_session_id(lines: Iterable[str], device_id: str, filename: str) -> str: + """Resolve the upload session id. + + Prefer the ``session_uuid`` embedded in the ``session_start`` entry + (present since openflight 0.2.0). For older sessions lacking it, fall back + to a deterministic UUIDv5 of ``(device_id, filename)`` so the same file + always maps to the same id. Always lowercase. + """ + for _, entry in _iter_entries(lines): + if entry.get("type") == "session_start": + session_uuid = entry.get("session_uuid") + if session_uuid: + return str(session_uuid).lower() + break + return str(uuid.uuid5(SESSION_NAMESPACE, f"{device_id}:{filename}")) + + +def filter_session_lines( + lines: Iterable[str], device_id: str, client_version: str = CLIENT_VERSION +) -> FilterResult: + """Filter raw session lines to the allowlist and build the manifest. + + Drops non-allowlisted types, drops any kept line over ``MAX_LINE_BYTES`` + (counting them), and skips blank/unparseable lines. + """ + kept_lines: List[str] = [] + kept_type_counts: Dict[str, int] = {} + dropped_oversize = 0 + + for raw, entry in _iter_entries(lines): + entry_type = entry.get("type") + if entry_type not in KEEP_ENTRY_TYPES: + continue + if len(raw.encode("utf-8")) > MAX_LINE_BYTES: + dropped_oversize += 1 + continue + kept_lines.append(raw) + kept_type_counts[entry_type] = kept_type_counts.get(entry_type, 0) + 1 + + manifest = { + "type": MANIFEST_TYPE, + "format_version": MANIFEST_FORMAT_VERSION, + "client_version": client_version, + "device_id": device_id, + "filtered": True, + "kept_entry_types": sorted(kept_type_counts), + } + return FilterResult( + manifest=manifest, + kept_lines=kept_lines, + dropped_oversize=dropped_oversize, + kept_type_counts=kept_type_counts, + ) + + +def build_upload_body( + result: FilterResult, + max_gzip_bytes: Optional[int] = None, + max_inflated_bytes: Optional[int] = None, +) -> bytes: + """Build the gzipped NDJSON upload body (manifest first), enforcing caps. + + Raises BodyTooLargeError if the body would exceed either cap — the caller + should park the session and report it rather than upload raw. + """ + # Resolve at call time so tests (and config) can adjust the module caps. + max_gzip_bytes = MAX_GZIP_BYTES if max_gzip_bytes is None else max_gzip_bytes + max_inflated_bytes = MAX_INFLATED_BYTES if max_inflated_bytes is None else max_inflated_bytes + out_lines = [json.dumps(result.manifest)] + out_lines.extend(result.kept_lines) + ndjson = ("\n".join(out_lines) + "\n").encode("utf-8") + + if len(ndjson) > max_inflated_bytes: + raise BodyTooLargeError( + f"inflated body {len(ndjson)} bytes exceeds cap {max_inflated_bytes}" + ) + + # mtime=0 keeps the gzip output deterministic (stable retries/dedupe). + body = gzip.compress(ndjson, mtime=0) + if len(body) > max_gzip_bytes: + raise BodyTooLargeError(f"gzip body {len(body)} bytes exceeds cap {max_gzip_bytes}") + return body diff --git a/src/openflight/cloud/spool.py b/src/openflight/cloud/spool.py new file mode 100644 index 000000000..f8336aa1a --- /dev/null +++ b/src/openflight/cloud/spool.py @@ -0,0 +1,164 @@ +"""Spool-and-retry mechanics — the session directory *is* the queue. + +State lives in sidecar files next to each ``session_*.jsonl`` so it survives +crashes with no database: + +- ``.jsonl.pushed`` — present once accepted by the server (terminal). +- ``.jsonl.parked`` — present once given up on (terminal). +- ``.jsonl.state`` — JSON attempt counter + last error for in-flight + retries; removed on success. + +Originals are never moved or modified. +""" + +import json +import time +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional + +PUSHED_SUFFIX = ".pushed" +PARKED_SUFFIX = ".parked" +STATE_SUFFIX = ".state" + +SESSION_GLOB = "session_*.jsonl" + +# After this many failures, park the file and report via ``status`` instead of +# retrying forever. +MAX_ATTEMPTS = 20 + +# How long to defer a session after a quota (402) rejection — retry daily +# rather than every timer tick. +QUOTA_COOLDOWN_S = 24 * 60 * 60 + + +def _sidecar(path: Path, suffix: str) -> Path: + # Append (not replace) so "session_x.jsonl" -> "session_x.jsonl.pushed". + return path.with_name(path.name + suffix) + + +def _now() -> str: + return datetime.now().isoformat() + + +def session_files(log_dir: Path) -> List[Path]: + """All session JSONL files in ``log_dir`` (sorted); empty if dir missing.""" + log_dir = Path(log_dir) + if not log_dir.is_dir(): + return [] + return sorted(log_dir.glob(SESSION_GLOB)) + + +def is_pushed(path: Path) -> bool: + """True if a ``.pushed`` marker exists for this session.""" + return _sidecar(path, PUSHED_SUFFIX).exists() + + +def is_parked(path: Path) -> bool: + """True if a ``.parked`` marker exists for this session.""" + return _sidecar(path, PARKED_SUFFIX).exists() + + +def pending_sessions(log_dir: Path) -> List[Path]: + """Session files that are neither pushed nor parked.""" + return [p for p in session_files(log_dir) if not is_pushed(p) and not is_parked(p)] + + +def read_attempts(path: Path) -> int: + """Number of recorded failed attempts for this session (0 if none).""" + state_path = _sidecar(path, STATE_SUFFIX) + if not state_path.exists(): + return 0 + try: + return int(json.loads(state_path.read_text()).get("attempts", 0)) + except (json.JSONDecodeError, ValueError): + return 0 + + +def _write_json(path: Path, data: Dict[str, Any]) -> None: + path.write_text(json.dumps(data, indent=2) + "\n") + + +def record_failure(path: Path, error: str) -> int: + """Increment the attempt counter, store the error, and park if maxed out. + + Returns the new attempt count. + """ + attempts = read_attempts(path) + 1 + _write_json( + _sidecar(path, STATE_SUFFIX), + {"attempts": attempts, "last_error": error, "last_attempt_at": _now()}, + ) + if attempts >= MAX_ATTEMPTS: + mark_parked(path, reason="max_attempts", attempts=attempts, last_error=error) + return attempts + + +def record_cooldown(path: Path, reason: str, seconds: float, now: Optional[float] = None) -> None: + """Defer retries for this session until ``seconds`` from now (e.g. quota). + + Does not increment the failure counter or park — quota is not a client bug. + """ + now = time.time() if now is None else now + state_path = _sidecar(path, STATE_SUFFIX) + state: Dict[str, Any] = {} + if state_path.exists(): + try: + state = json.loads(state_path.read_text()) + except (json.JSONDecodeError, ValueError): + state = {} + state.update( + {"cooldown_until": now + seconds, "cooldown_reason": reason, "last_attempt_at": _now()} + ) + _write_json(state_path, state) + + +def in_cooldown(path: Path, now: Optional[float] = None) -> bool: + """True if this session is deferred (cooldown not yet elapsed).""" + now = time.time() if now is None else now + state_path = _sidecar(path, STATE_SUFFIX) + if not state_path.exists(): + return False + try: + until = json.loads(state_path.read_text()).get("cooldown_until") + except (json.JSONDecodeError, ValueError): + return False + return until is not None and now < until + + +def mark_pushed(path: Path, session_id: str, shot_count: Optional[int]) -> None: + """Mark a session as successfully uploaded and clear retry state.""" + _write_json( + _sidecar(path, PUSHED_SUFFIX), + {"session_id": session_id, "shot_count": shot_count, "pushed_at": _now()}, + ) + state_path = _sidecar(path, STATE_SUFFIX) + if state_path.exists(): + state_path.unlink() + + +def mark_parked(path: Path, reason: str, attempts: int, last_error: Optional[str]) -> None: + """Mark a session as parked (given up on); reported via ``status``.""" + _write_json( + _sidecar(path, PARKED_SUFFIX), + { + "reason": reason, + "attempts": attempts, + "last_error": last_error, + "parked_at": _now(), + }, + ) + + +def summarize(log_dir: Path) -> Dict[str, int]: + """Count pushed / parked / pending sessions for ``status``.""" + files = session_files(log_dir) + pushed = sum(1 for p in files if is_pushed(p)) + parked = sum(1 for p in files if is_parked(p)) + pending = sum(1 for p in files if not is_pushed(p) and not is_parked(p)) + return { + "total": len(files), + "pushed": pushed, + "parked": parked, + "pending": pending, + } diff --git a/src/openflight/cloud/trigger.py b/src/openflight/cloud/trigger.py new file mode 100644 index 000000000..18b13e828 --- /dev/null +++ b/src/openflight/cloud/trigger.py @@ -0,0 +1,50 @@ +"""Fire-and-forget push trigger for the server's session-end happy path. + +Spawns ``openflight-cloud push`` as a detached subprocess so an upload never +blocks or delays shot processing. The systemd timer remains the safety net that +heals wifi outages; this is just the fast path. +""" + +import subprocess +import sys +from pathlib import Path +from typing import Callable + +from .config import CONFIG_PATH, CloudConfig + + +def fire_push_async( + config: CloudConfig, + log_dir: Path, + config_path: Path = CONFIG_PATH, + popen_fn: Callable[..., object] = subprocess.Popen, +) -> bool: + """Spawn a detached ``push`` if the uploader is active. Never raises. + + Returns True if a push was spawned, False otherwise (inactive or spawn + failed). The caller is on the shot/session path, so all errors are + swallowed. + """ + if not config.is_active(): + return False + + cmd = [ + sys.executable, + "-m", + "openflight.cloud.cli", + "--config", + str(config_path), + "--log-dir", + str(log_dir), + "push", + ] + try: + popen_fn( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + ) + return True + except (OSError, ValueError): + return False diff --git a/src/openflight/server.py b/src/openflight/server.py index 2a0d7947e..1afd5255f 100644 --- a/src/openflight/server.py +++ b/src/openflight/server.py @@ -2037,6 +2037,26 @@ def on_trigger_diagnostic(data: dict): monitor.start(shot_callback=on_shot_detected, live_callback=on_live_reading) +def _fire_cloud_push(session_logger): + """Best-effort, non-blocking cloud push on session end. + + Fully guarded: the uploader is opt-in and must never delay or break the + shot/session path. The systemd timer is the safety net if this no-ops. + """ + try: + from .cloud.config import load_config + from .cloud.trigger import fire_push_async + + config = load_config() + if config is None or not config.is_active(): + return + log_dir = getattr(session_logger, "log_dir", None) + if log_dir is not None: + fire_push_async(config, log_dir=log_dir) + except Exception: # pylint: disable=broad-exception-caught + pass + + def stop_monitor(): """Stop the launch monitor.""" global monitor # pylint: disable=global-statement @@ -2045,6 +2065,7 @@ def stop_monitor(): session_logger = get_session_logger() if session_logger: session_logger.end_session() + _fire_cloud_push(session_logger) if monitor: monitor.stop() diff --git a/tests/test_cloud_cli.py b/tests/test_cloud_cli.py new file mode 100644 index 000000000..836fc1018 --- /dev/null +++ b/tests/test_cloud_cli.py @@ -0,0 +1,58 @@ +"""Tests for the openflight-cloud CLI argument wiring.""" + +import pytest + +from openflight.cloud import cli + + +class TestArgParsing: + def test_requires_subcommand(self, capsys): + rc = cli.main([]) + assert rc != 0 + + def test_unknown_subcommand_errors(self): + with pytest.raises(SystemExit): + cli.main(["frobnicate"]) + + +class TestDispatch: + def test_status_dispatches(self, monkeypatch, tmp_path): + called = {} + + def fake_status(config, log_dir, client=None, out=print): + called["log_dir"] = log_dir + out("status-ran") + return {} + + monkeypatch.setattr(cli.commands, "cmd_status", fake_status) + rc = cli.main(["status", "--log-dir", str(tmp_path), "--config", str(tmp_path / "c.json")]) + assert rc == 0 + assert called["log_dir"] == tmp_path + + def test_push_passes_dry_run_flag(self, monkeypatch, tmp_path): + captured = {} + + def fake_push(config, log_dir, client, dry_run=False, out=print): + captured["dry_run"] = dry_run + return {"needs_relink": False} + + monkeypatch.setattr(cli.commands, "cmd_push", fake_push) + cli.main(["push", "--dry-run", "--log-dir", str(tmp_path), "--config", str(tmp_path / "c.json")]) + assert captured["dry_run"] is True + + def test_push_returns_nonzero_when_relink_needed(self, monkeypatch, tmp_path): + monkeypatch.setattr( + cli.commands, "cmd_push", lambda *a, **k: {"needs_relink": True} + ) + rc = cli.main(["push", "--log-dir", str(tmp_path), "--config", str(tmp_path / "c.json")]) + assert rc != 0 + + def test_link_dispatches(self, monkeypatch, tmp_path): + monkeypatch.setattr(cli.commands, "cmd_link", lambda *a, **k: True) + rc = cli.main(["link", "--config", str(tmp_path / "c.json")]) + assert rc == 0 + + def test_link_returns_nonzero_on_failure(self, monkeypatch, tmp_path): + monkeypatch.setattr(cli.commands, "cmd_link", lambda *a, **k: False) + rc = cli.main(["link", "--config", str(tmp_path / "c.json")]) + assert rc != 0 diff --git a/tests/test_cloud_client.py b/tests/test_cloud_client.py new file mode 100644 index 000000000..d1c25c0dd --- /dev/null +++ b/tests/test_cloud_client.py @@ -0,0 +1,166 @@ +"""Tests for the openflight-cloud HTTP client (wire contract).""" + +import json + +import pytest + +from openflight.cloud import client as cl + + +class FakeTransport: + """Records requests and returns queued responses.""" + + def __init__(self, responses): + self._responses = list(responses) + self.calls = [] + + def __call__(self, method, url, data=None, headers=None, timeout=30): + self.calls.append( + {"method": method, "url": url, "data": data, "headers": headers or {}} + ) + return self._responses.pop(0) + + +def _resp(status, body=None, headers=None): + raw = json.dumps(body).encode() if body is not None else b"" + return cl.HttpResponse(status=status, headers=headers or {}, body=raw) + + +class TestHealth: + def test_ok_returns_true(self): + client = cl.CloudClient("https://e.test", request_fn=FakeTransport([_resp(200, {"status": "ok"})])) + assert client.health() is True + + def test_non_ok_returns_false(self): + client = cl.CloudClient("https://e.test", request_fn=FakeTransport([_resp(503)])) + assert client.health() is False + + def test_network_error_returns_false(self): + def boom(*a, **k): + raise cl.CloudNetworkError("offline") + + client = cl.CloudClient("https://e.test", request_fn=boom) + assert client.health() is False + + def test_uses_v1_health_path(self): + t = FakeTransport([_resp(200, {"status": "ok"})]) + cl.CloudClient("https://e.test/", request_fn=t).health() + assert t.calls[0]["url"] == "https://e.test/v1/health" + assert t.calls[0]["method"] == "GET" + + +class TestDeviceLinkStart: + def test_parses_link_start_response(self): + body = { + "link_code": "ABCD-2345", + "poll_token": "secret-token", + "interval_s": 5, + "expires_s": 900, + } + t = FakeTransport([_resp(200, body)]) + client = cl.CloudClient("https://e.test", request_fn=t) + result = client.device_link_start("garage pi", "0.2.0") + assert result.link_code == "ABCD-2345" + assert result.poll_token == "secret-token" + assert result.interval_s == 5 + assert result.expires_s == 900 + + def test_sends_device_name_and_version(self): + t = FakeTransport([_resp(200, {"link_code": "A", "poll_token": "p", "interval_s": 5, "expires_s": 900})]) + cl.CloudClient("https://e.test", request_fn=t).device_link_start("garage pi", "9.9.9") + sent = json.loads(t.calls[0]["data"].decode()) + assert sent == {"device_name": "garage pi", "client_version": "9.9.9"} + assert t.calls[0]["url"] == "https://e.test/v1/device-link/start" + + def test_422_raises_link_error(self): + t = FakeTransport([_resp(422, {"reason": "invalid_device_name"})]) + with pytest.raises(cl.LinkError): + cl.CloudClient("https://e.test", request_fn=t).device_link_start("", "0.2.0") + + def test_429_raises_rate_limited_with_retry_after(self): + t = FakeTransport([_resp(429, {"reason": "rate_limited"}, headers={"Retry-After": "42"})]) + with pytest.raises(cl.RateLimited) as exc: + cl.CloudClient("https://e.test", request_fn=t).device_link_start("pi", "0.2.0") + assert exc.value.retry_after == 42 + + +class TestDeviceLinkPoll: + def test_pending(self): + t = FakeTransport([_resp(200, {"status": "pending"})]) + result = cl.CloudClient("https://e.test", request_fn=t).device_link_poll("tok") + assert result.status == "pending" + + def test_linked_returns_token_and_id(self): + body = {"status": "linked", "device_token": "of_device_x", "device_id": "uuid-1"} + t = FakeTransport([_resp(200, body)]) + result = cl.CloudClient("https://e.test", request_fn=t).device_link_poll("tok") + assert result.status == "linked" + assert result.device_token == "of_device_x" + assert result.device_id == "uuid-1" + + def test_404_maps_to_unknown(self): + t = FakeTransport([_resp(404, {"reason": "unknown_poll_token"})]) + result = cl.CloudClient("https://e.test", request_fn=t).device_link_poll("tok") + assert result.status == "unknown" + + def test_429_raises_rate_limited(self): + t = FakeTransport([_resp(429, {}, headers={"Retry-After": "5"})]) + with pytest.raises(cl.RateLimited): + cl.CloudClient("https://e.test", request_fn=t).device_link_poll("tok") + + +class TestUploadSession: + def _client(self, responses): + return cl.CloudClient( + "https://e.test", token="of_device_tok", request_fn=FakeTransport(responses) + ) + + def test_201_is_success(self): + r = self._client([_resp(201, {"session_id": "s1", "shot_count": 7})]).upload_session("s1", b"gz") + assert r.action == "success" + assert r.shot_count == 7 + + def test_200_is_success(self): + r = self._client([_resp(200, {"session_id": "s1", "shot_count": 7})]).upload_session("s1", b"gz") + assert r.action == "success" + + def test_401_needs_relink(self): + r = self._client([_resp(401, {"reason": "invalid_or_revoked_token"})]).upload_session("s1", b"gz") + assert r.action == "relink" + + def test_402_quota(self): + r = self._client([_resp(402, {"reason": "quota_exceeded"})]).upload_session("s1", b"gz") + assert r.action == "quota" + + def test_413_parks(self): + r = self._client([_resp(413, {"reason": "body_too_large"})]).upload_session("s1", b"gz") + assert r.action == "park" + + def test_422_parks(self): + r = self._client([_resp(422, {"reason": "invalid_gzip"})]).upload_session("s1", b"gz") + assert r.action == "park" + assert r.reason == "invalid_gzip" + + def test_429_retry_with_retry_after(self): + r = self._client( + [_resp(429, {"reason": "rate_limited"}, headers={"Retry-After": "30"})] + ).upload_session("s1", b"gz") + assert r.action == "rate_limited" + assert r.retry_after == 30 + + def test_5xx_retry(self): + r = self._client([_resp(503)]).upload_session("s1", b"gz") + assert r.action == "retry" + + def test_sends_bearer_auth_and_gzip_headers(self): + t = FakeTransport([_resp(201, {"session_id": "s1", "shot_count": 1})]) + cl.CloudClient("https://e.test", token="of_device_tok", request_fn=t).upload_session( + "1f0e9c2a-7b3d-4e5f-8a9b-0c1d2e3f4a5b", b"gzbytes" + ) + call = t.calls[0] + assert call["method"] == "PUT" + assert call["url"] == "https://e.test/v1/sessions/1f0e9c2a-7b3d-4e5f-8a9b-0c1d2e3f4a5b" + assert call["headers"]["Authorization"] == "Bearer of_device_tok" + assert call["headers"]["Content-Type"] == "application/x-ndjson" + assert call["headers"]["Content-Encoding"] == "gzip" + assert call["data"] == b"gzbytes" diff --git a/tests/test_cloud_commands.py b/tests/test_cloud_commands.py new file mode 100644 index 000000000..e77e29b92 --- /dev/null +++ b/tests/test_cloud_commands.py @@ -0,0 +1,216 @@ +"""Tests for openflight-cloud command orchestration (link/push/status).""" + +import json + +import pytest + +from openflight.cloud import commands, spool +from openflight.cloud.client import LinkPoll, LinkStart, UploadResult +from openflight.cloud.config import CloudConfig + + +class FakeClient: + def __init__(self, *, healthy=True, link_start=None, polls=None, uploads=None): + self._healthy = healthy + self._link_start = link_start + self._polls = list(polls or []) + self._uploads = list(uploads or []) + self.uploaded = [] + + def health(self): + return self._healthy + + def device_link_start(self, device_name, client_version): + return self._link_start + + def device_link_poll(self, poll_token): + return self._polls.pop(0) + + def upload_session(self, session_id, body): + self.uploaded.append(session_id) + return self._uploads.pop(0) + + +def _write_session(tmp_path, name, *entries): + path = tmp_path / name + path.write_text("\n".join(json.dumps(e) for e in entries) + "\n") + return path + + +def _linked_config(): + return CloudConfig( + endpoint="https://e.test", + device_token="of_device_tok", + device_id="dev-1", + enabled=True, + ) + + +class TestPush: + def test_offline_short_circuits(self, tmp_path): + _write_session(tmp_path, "session_a.jsonl", {"type": "session_start"}) + client = FakeClient(healthy=False) + out = [] + result = commands.cmd_push(_linked_config(), tmp_path, client, out=out.append) + assert result["offline"] is True + assert client.uploaded == [] + + def test_inactive_config_is_noop(self, tmp_path): + _write_session(tmp_path, "session_a.jsonl", {"type": "session_start"}) + config = CloudConfig(enabled=False) + client = FakeClient() + result = commands.cmd_push(config, tmp_path, client, out=lambda _m: None) + assert result["skipped"] == "inactive" + assert client.uploaded == [] + + def test_success_marks_pushed(self, tmp_path): + path = _write_session( + tmp_path, + "session_a.jsonl", + {"type": "session_start", "session_uuid": "1f0e9c2a-7b3d-4e5f-8a9b-0c1d2e3f4a5b"}, + {"type": "shot_detected", "ball_speed_mph": 90}, + ) + client = FakeClient( + uploads=[UploadResult(201, action="success", session_id="x", shot_count=1)] + ) + result = commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) + assert client.uploaded == ["1f0e9c2a-7b3d-4e5f-8a9b-0c1d2e3f4a5b"] + assert spool.is_pushed(path) + assert result["uploaded"] == 1 + + def test_dry_run_does_not_upload_or_mark(self, tmp_path): + path = _write_session( + tmp_path, + "session_a.jsonl", + {"type": "session_start", "session_uuid": "u"}, + {"type": "shot_detected", "ball_speed_mph": 90}, + {"type": "rolling_buffer_capture", "i_samples": [1, 2, 3]}, + ) + client = FakeClient() + out = [] + commands.cmd_push(_linked_config(), tmp_path, client, dry_run=True, out=out.append) + assert client.uploaded == [] + assert not spool.is_pushed(path) + printed = "\n".join(out) + # The privacy answer: shows kept types, hides dropped raw data. + assert "shot_detected" in printed + assert "session_start" in printed + assert "rolling_buffer_capture" not in printed + + def test_relink_stops_and_flags(self, tmp_path): + _write_session(tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"}) + _write_session(tmp_path, "session_b.jsonl", {"type": "session_start", "session_uuid": "b"}) + client = FakeClient( + uploads=[UploadResult(401, action="relink", reason="invalid_or_revoked_token")] + ) + result = commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) + assert result["needs_relink"] is True + # Stops after the first 401 — does not attempt the second session. + assert len(client.uploaded) == 1 + + def test_park_on_422(self, tmp_path): + path = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"}) + client = FakeClient(uploads=[UploadResult(422, action="park", reason="invalid_gzip")]) + commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) + assert spool.is_parked(path) + + def test_quota_sets_cooldown_not_park(self, tmp_path): + path = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"}) + client = FakeClient(uploads=[UploadResult(402, action="quota", reason="quota_exceeded")]) + commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) + assert not spool.is_parked(path) + assert spool.in_cooldown(path) + + def test_5xx_records_failure_and_leaves_pending(self, tmp_path): + path = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"}) + client = FakeClient(uploads=[UploadResult(503, action="retry")]) + commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) + assert spool.read_attempts(path) == 1 + assert not spool.is_pushed(path) + assert not spool.is_parked(path) + + def test_oversize_body_parks(self, tmp_path, monkeypatch): + path = _write_session(tmp_path, "session_a.jsonl", {"type": "shot_detected", "ball_speed_mph": 90}) + from openflight.cloud import filtering + + monkeypatch.setattr(filtering, "MAX_GZIP_BYTES", 1) + client = FakeClient(uploads=[UploadResult(201, action="success")]) + commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) + assert client.uploaded == [] + assert spool.is_parked(path) + + def test_skips_sessions_in_cooldown(self, tmp_path): + path = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"}) + spool.record_cooldown(path, "quota_exceeded", seconds=spool.QUOTA_COOLDOWN_S) + client = FakeClient(uploads=[UploadResult(201, action="success")]) + result = commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) + assert client.uploaded == [] + assert result["deferred"] == 1 + + +class TestLink: + def test_links_and_saves_config(self, tmp_path): + config_path = tmp_path / "cloud.json" + client = FakeClient( + link_start=LinkStart("ABCD-2345", "poll-tok", 5, 900), + polls=[ + LinkPoll("pending"), + LinkPoll("linked", device_token="of_device_new", device_id="dev-99"), + ], + ) + out = [] + ok = commands.cmd_link( + CloudConfig(endpoint="https://e.test"), + config_path, + client, + device_name="garage pi", + sleep=lambda _s: None, + out=out.append, + ) + assert ok is True + saved = json.loads(config_path.read_text()) + assert saved["device_token"] == "of_device_new" + assert saved["device_id"] == "dev-99" + assert saved["enabled"] is True + assert "ABCD-2345" in "\n".join(out) + + def test_expired_returns_false(self, tmp_path): + client = FakeClient( + link_start=LinkStart("ABCD-2345", "poll-tok", 5, 900), + polls=[LinkPoll("expired")], + ) + ok = commands.cmd_link( + CloudConfig(endpoint="https://e.test"), + tmp_path / "cloud.json", + client, + device_name="pi", + sleep=lambda _s: None, + out=lambda _m: None, + ) + assert ok is False + assert not (tmp_path / "cloud.json").exists() + + +class TestStatus: + def test_reports_unlinked(self, tmp_path): + out = [] + commands.cmd_status(CloudConfig(), tmp_path, out=out.append) + assert "not linked" in "\n".join(out).lower() + + def test_reports_reachability_when_client_given(self, tmp_path): + out = [] + client = FakeClient(healthy=False) + result = commands.cmd_status(_linked_config(), tmp_path, client=client, out=out.append) + assert result["online"] is False + assert "unreachable" in "\n".join(out).lower() + + def test_reports_counts_and_parked(self, tmp_path): + a = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start"}) + b = _write_session(tmp_path, "session_b.jsonl", {"type": "session_start"}) + spool.mark_pushed(a, "id-a", 2) + spool.mark_parked(b, reason="invalid_gzip", attempts=3, last_error="422") + out = [] + commands.cmd_status(_linked_config(), tmp_path, out=out.append) + text = "\n".join(out) + assert "dev-1" in text + assert "invalid_gzip" in text diff --git a/tests/test_cloud_config.py b/tests/test_cloud_config.py new file mode 100644 index 000000000..7f637f138 --- /dev/null +++ b/tests/test_cloud_config.py @@ -0,0 +1,84 @@ +"""Tests for the openflight-cloud config module.""" + +import json +import stat + +import pytest + +from openflight.cloud import config as cfg + + +class TestLoadConfig: + def test_returns_none_when_file_absent(self, tmp_path): + assert cfg.load_config(tmp_path / "cloud.json") is None + + def test_loads_all_fields(self, tmp_path): + path = tmp_path / "cloud.json" + path.write_text( + json.dumps( + { + "endpoint": "https://example.test", + "device_token": "of_device_" + "a" * 32, + "device_id": "abc-123", + "enabled": True, + } + ) + ) + loaded = cfg.load_config(path) + assert loaded.endpoint == "https://example.test" + assert loaded.device_token == "of_device_" + "a" * 32 + assert loaded.device_id == "abc-123" + assert loaded.enabled is True + + def test_defaults_endpoint_and_enabled_when_missing(self, tmp_path): + path = tmp_path / "cloud.json" + path.write_text(json.dumps({"device_token": "t", "device_id": "i"})) + loaded = cfg.load_config(path) + assert loaded.endpoint == cfg.DEFAULT_ENDPOINT + assert loaded.enabled is True + + +class TestSaveConfig: + def test_writes_file_with_0600_permissions(self, tmp_path): + path = tmp_path / "nested" / "cloud.json" + config = cfg.CloudConfig(device_token="tok", device_id="id") + cfg.save_config(config, path) + + assert path.exists() + mode = stat.S_IMODE(path.stat().st_mode) + assert mode == 0o600 + + def test_round_trips_through_load(self, tmp_path): + path = tmp_path / "cloud.json" + config = cfg.CloudConfig( + endpoint="https://e.test", + device_token="of_device_" + "b" * 32, + device_id="dev-9", + enabled=False, + ) + cfg.save_config(config, path) + loaded = cfg.load_config(path) + assert loaded == config + + +class TestIsLinked: + def test_true_when_token_and_id_present(self): + config = cfg.CloudConfig(device_token="t", device_id="i") + assert config.is_linked() + + def test_false_when_token_missing(self): + assert not cfg.CloudConfig(device_id="i").is_linked() + + def test_false_when_id_missing(self): + assert not cfg.CloudConfig(device_token="t").is_linked() + + +class TestIsActive: + def test_active_requires_enabled_and_linked(self): + assert cfg.CloudConfig(device_token="t", device_id="i", enabled=True).is_active() + + def test_inactive_when_disabled(self): + assert not cfg.CloudConfig(device_token="t", device_id="i", enabled=False).is_active() + + def test_inactive_when_not_linked(self): + assert not cfg.CloudConfig(enabled=True).is_active() diff --git a/tests/test_cloud_filtering.py b/tests/test_cloud_filtering.py new file mode 100644 index 000000000..c0ee719ed --- /dev/null +++ b/tests/test_cloud_filtering.py @@ -0,0 +1,141 @@ +"""Tests for the openflight-cloud client-side filtering (raw-ADC strip).""" + +import gzip +import json +import uuid + +import pytest + +from openflight.cloud import filtering as flt + + +def _line(entry_type, **fields): + return json.dumps({"ts": "2026-06-14T00:00:00", "type": entry_type, **fields}) + + +class TestResolveSessionId: + def test_uses_embedded_session_uuid_lowercased(self): + u = "1F0E9C2A-7B3D-4E5F-8A9B-0C1D2E3F4A5B" + lines = [_line("session_start", session_uuid=u)] + assert flt.resolve_session_id(lines, "dev-1", "session_x.jsonl") == u.lower() + + def test_uuid5_fallback_when_no_session_uuid(self): + lines = [_line("session_start")] + result = flt.resolve_session_id(lines, "dev-1", "session_x.jsonl") + expected = str(uuid.uuid5(flt.SESSION_NAMESPACE, "dev-1:session_x.jsonl")) + assert result == expected + + def test_uuid5_fallback_is_deterministic(self): + a = flt.resolve_session_id([], "dev-1", "session_x.jsonl") + b = flt.resolve_session_id([], "dev-1", "session_x.jsonl") + assert a == b + + def test_uuid5_fallback_differs_by_device(self): + a = flt.resolve_session_id([], "dev-1", "session_x.jsonl") + b = flt.resolve_session_id([], "dev-2", "session_x.jsonl") + assert a != b + + +class TestFilterSessionLines: + def test_keeps_only_allowlisted_types(self): + lines = [ + _line("session_start", session_uuid="u"), + _line("rolling_buffer_capture", i_samples=[1, 2, 3]), + _line("shot_detected", ball_speed_mph=100), + _line("iq_blocks", blocks=[]), + _line("trigger_event", accepted=True), + _line("session_end"), + ] + result = flt.filter_session_lines(lines, device_id="dev-1") + kept_types = [json.loads(line)["type"] for line in result.kept_lines] + assert kept_types == [ + "session_start", + "shot_detected", + "trigger_event", + "session_end", + ] + + def test_keeps_both_error_and_session_error(self): + lines = [ + _line("error", error="boom"), + _line("session_error", error="boom2"), + ] + result = flt.filter_session_lines(lines, device_id="dev-1") + kept_types = [json.loads(line)["type"] for line in result.kept_lines] + assert kept_types == ["error", "session_error"] + + def test_drops_unknown_future_type_by_default(self): + lines = [_line("some_future_heavy_type", data="x" * 10)] + result = flt.filter_session_lines(lines, device_id="dev-1") + assert result.kept_lines == [] + + def test_drops_kept_line_over_32kb_and_counts_it(self): + big = _line("shot_detected", note="x" * (33 * 1024)) + small = _line("shot_detected", ball_speed_mph=90) + result = flt.filter_session_lines([big, small], device_id="dev-1") + assert result.dropped_oversize == 1 + assert len(result.kept_lines) == 1 + + def test_ignores_blank_and_unparseable_lines(self): + lines = ["", " ", "not json", _line("shot_detected", ball_speed_mph=90)] + result = flt.filter_session_lines(lines, device_id="dev-1") + assert len(result.kept_lines) == 1 + + def test_manifest_has_expected_shape(self): + lines = [ + _line("session_start", session_uuid="u"), + _line("shot_detected", ball_speed_mph=90), + ] + result = flt.filter_session_lines(lines, device_id="dev-7", client_version="9.9.9") + m = result.manifest + assert m["type"] == "upload_manifest" + assert m["format_version"] == 1 + assert m["client_version"] == "9.9.9" + assert m["device_id"] == "dev-7" + assert m["filtered"] is True + assert set(m["kept_entry_types"]) == {"session_start", "shot_detected"} + + def test_kept_entry_types_are_sorted_and_unique(self): + lines = [ + _line("shot_detected"), + _line("shot_detected"), + _line("session_start"), + ] + result = flt.filter_session_lines(lines, device_id="d") + assert result.manifest["kept_entry_types"] == ["session_start", "shot_detected"] + + +class TestBuildUploadBody: + def test_body_is_gzip_with_manifest_first(self): + lines = [ + _line("session_start", session_uuid="u"), + _line("shot_detected", ball_speed_mph=90), + ] + result = flt.filter_session_lines(lines, device_id="dev-1") + body = flt.build_upload_body(result) + + ndjson = gzip.decompress(body).decode("utf-8") + out_lines = ndjson.strip().split("\n") + first = json.loads(out_lines[0]) + assert first["type"] == "upload_manifest" + assert json.loads(out_lines[1])["type"] == "session_start" + assert json.loads(out_lines[2])["type"] == "shot_detected" + + def test_raises_when_gzip_exceeds_cap(self): + result = flt.FilterResult( + manifest={"type": "upload_manifest"}, + kept_lines=[_line("shot_detected", ball_speed_mph=90)], + dropped_oversize=0, + ) + # 1-byte caps make any real body too large. + with pytest.raises(flt.BodyTooLargeError): + flt.build_upload_body(result, max_gzip_bytes=1, max_inflated_bytes=1_000_000) + + def test_raises_when_inflated_exceeds_cap(self): + result = flt.FilterResult( + manifest={"type": "upload_manifest"}, + kept_lines=[_line("shot_detected", ball_speed_mph=90)], + dropped_oversize=0, + ) + with pytest.raises(flt.BodyTooLargeError): + flt.build_upload_body(result, max_gzip_bytes=1_000_000, max_inflated_bytes=1) diff --git a/tests/test_cloud_spool.py b/tests/test_cloud_spool.py new file mode 100644 index 000000000..61b4cd9e7 --- /dev/null +++ b/tests/test_cloud_spool.py @@ -0,0 +1,127 @@ +"""Tests for the openflight-cloud spool-and-retry sidecar mechanics.""" + +import json + +import pytest + +from openflight.cloud import spool + + +def _session(tmp_path, name="session_20260614_120000_range.jsonl"): + path = tmp_path / name + path.write_text('{"type":"session_start"}\n') + return path + + +class TestDiscovery: + def test_session_files_finds_only_session_jsonl(self, tmp_path): + _session(tmp_path, "session_a.jsonl") + _session(tmp_path, "session_b.jsonl") + (tmp_path / "radar_raw_x.log").write_text("noise") + (tmp_path / "other.jsonl").write_text("{}") + found = {p.name for p in spool.session_files(tmp_path)} + assert found == {"session_a.jsonl", "session_b.jsonl"} + + def test_session_files_empty_when_dir_missing(self, tmp_path): + assert spool.session_files(tmp_path / "nope") == [] + + def test_pending_excludes_pushed_and_parked(self, tmp_path): + a = _session(tmp_path, "session_a.jsonl") + b = _session(tmp_path, "session_b.jsonl") + c = _session(tmp_path, "session_c.jsonl") + spool.mark_pushed(b, session_id="id-b", shot_count=3) + spool.mark_parked(c, reason="quota_exceeded", attempts=20, last_error="402") + pending = {p.name for p in spool.pending_sessions(tmp_path)} + assert pending == {"session_a.jsonl"} + assert a # referenced + + +class TestPushedMarker: + def test_mark_pushed_creates_sidecar(self, tmp_path): + path = _session(tmp_path) + spool.mark_pushed(path, session_id="abc", shot_count=5) + marker = tmp_path / (path.name + ".pushed") + assert marker.exists() + data = json.loads(marker.read_text()) + assert data["session_id"] == "abc" + assert data["shot_count"] == 5 + assert spool.is_pushed(path) + + def test_mark_pushed_clears_attempt_state(self, tmp_path): + path = _session(tmp_path) + spool.record_failure(path, "5xx") + spool.mark_pushed(path, session_id="abc", shot_count=1) + assert spool.read_attempts(path) == 0 + assert not (tmp_path / (path.name + ".state")).exists() + + +class TestParkedMarker: + def test_mark_parked_creates_sidecar(self, tmp_path): + path = _session(tmp_path) + spool.mark_parked(path, reason="invalid_session_id", attempts=1, last_error="422") + marker = tmp_path / (path.name + ".parked") + assert marker.exists() + data = json.loads(marker.read_text()) + assert data["reason"] == "invalid_session_id" + assert data["attempts"] == 1 + assert data["last_error"] == "422" + assert spool.is_parked(path) + + +class TestAttemptCounter: + def test_attempts_start_at_zero(self, tmp_path): + path = _session(tmp_path) + assert spool.read_attempts(path) == 0 + + def test_record_failure_increments_and_returns_count(self, tmp_path): + path = _session(tmp_path) + assert spool.record_failure(path, "5xx") == 1 + assert spool.record_failure(path, "5xx") == 2 + assert spool.read_attempts(path) == 2 + + def test_record_failure_stores_last_error(self, tmp_path): + path = _session(tmp_path) + spool.record_failure(path, "boom") + state = json.loads((tmp_path / (path.name + ".state")).read_text()) + assert state["last_error"] == "boom" + + def test_record_failure_parks_after_max_attempts(self, tmp_path): + path = _session(tmp_path) + for _ in range(spool.MAX_ATTEMPTS - 1): + spool.record_failure(path, "5xx") + assert not spool.is_parked(path) + spool.record_failure(path, "5xx") + assert spool.is_parked(path) + assert spool.read_attempts(path) == spool.MAX_ATTEMPTS + + +class TestCooldown: + def test_no_cooldown_by_default(self, tmp_path): + path = _session(tmp_path) + assert spool.in_cooldown(path, now=1000.0) is False + + def test_record_cooldown_blocks_until_elapsed(self, tmp_path): + path = _session(tmp_path) + spool.record_cooldown(path, "quota_exceeded", seconds=100, now=1000.0) + assert spool.in_cooldown(path, now=1050.0) is True + assert spool.in_cooldown(path, now=1101.0) is False + + def test_cooldown_does_not_park(self, tmp_path): + path = _session(tmp_path) + spool.record_cooldown(path, "quota_exceeded", seconds=100, now=1000.0) + assert not spool.is_parked(path) + + +class TestStatusSummary: + def test_summarizes_counts(self, tmp_path): + a = _session(tmp_path, "session_a.jsonl") + b = _session(tmp_path, "session_b.jsonl") + c = _session(tmp_path, "session_c.jsonl") + spool.mark_pushed(a, session_id="a", shot_count=1) + spool.mark_parked(b, reason="r", attempts=20, last_error="402") + assert c + summary = spool.summarize(tmp_path) + assert summary["pushed"] == 1 + assert summary["parked"] == 1 + assert summary["pending"] == 1 + assert summary["total"] == 3 diff --git a/tests/test_cloud_trigger.py b/tests/test_cloud_trigger.py new file mode 100644 index 000000000..f0c6db413 --- /dev/null +++ b/tests/test_cloud_trigger.py @@ -0,0 +1,49 @@ +"""Tests for the non-blocking session-end push trigger.""" + +import pytest + +from openflight.cloud import trigger +from openflight.cloud.config import CloudConfig + + +class TestFirePushAsync: + def test_fires_when_active(self, tmp_path): + calls = [] + config = CloudConfig(device_token="t", device_id="i", enabled=True) + ok = trigger.fire_push_async( + config, log_dir=tmp_path, popen_fn=lambda cmd, **k: calls.append(cmd) + ) + assert ok is True + assert len(calls) == 1 + assert "push" in calls[0] + + def test_skips_when_inactive(self, tmp_path): + calls = [] + config = CloudConfig(enabled=False) + ok = trigger.fire_push_async( + config, log_dir=tmp_path, popen_fn=lambda cmd, **k: calls.append(cmd) + ) + assert ok is False + assert calls == [] + + def test_swallows_spawn_errors(self, tmp_path): + def boom(cmd, **k): + raise OSError("no exec") + + config = CloudConfig(device_token="t", device_id="i", enabled=True) + # Must never raise into the caller (server shot path). + assert trigger.fire_push_async(config, log_dir=tmp_path, popen_fn=boom) is False + + def test_passes_config_and_log_dir(self, tmp_path): + calls = [] + config = CloudConfig(device_token="t", device_id="i", enabled=True) + cfg_path = tmp_path / "cloud.json" + trigger.fire_push_async( + config, + log_dir=tmp_path, + config_path=cfg_path, + popen_fn=lambda cmd, **k: calls.append(cmd), + ) + cmd = calls[0] + assert str(cfg_path) in cmd + assert str(tmp_path) in cmd From 5efa95b7ec24798a86f7335ab7508a35542f4837 Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Sun, 14 Jun 2026 18:52:39 -0700 Subject: [PATCH 25/29] [cr] update sync docs --- docs/cloud-sync.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/cloud-sync.md b/docs/cloud-sync.md index 688dd935d..7390e2a68 100644 --- a/docs/cloud-sync.md +++ b/docs/cloud-sync.md @@ -23,6 +23,16 @@ openflight-cloud push # filter + upload anything not yet pushed `scripts/setup/setup.sh` offers to enable cloud sync and link the Pi for you (on a Raspberry Pi). Everything below can also be done by hand. +> **Upgrading an existing install?** The `openflight-cloud` command is created +> at install time. If you added cloud sync by pulling new code into a venv that +> predates it, reinstall so the console script gets wired up: +> +> ```bash +> uv pip install -e . +> ``` +> +> Until then you can run it as a module: `python -m openflight.cloud.cli link`. + ## Linking a device You never copy a long token onto the Pi. Linking uses a short, screen-readable From fd9760e2b5a10893efb87452bf5d8097b8e9df52 Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Mon, 15 Jun 2026 12:58:19 -0700 Subject: [PATCH 26/29] [cr] fix uploads of big files with raw ADC --- src/openflight/cloud/commands.py | 7 ++- src/openflight/cloud/filtering.py | 65 ++++++++++++++------- tests/test_cloud_filtering.py | 96 +++++++++++++++++++++++-------- 3 files changed, 120 insertions(+), 48 deletions(-) diff --git a/src/openflight/cloud/commands.py b/src/openflight/cloud/commands.py index ad7768ce8..f8bb35afc 100644 --- a/src/openflight/cloud/commands.py +++ b/src/openflight/cloud/commands.py @@ -117,9 +117,10 @@ def cmd_push( summary["deferred"] += 1 continue - lines = path.read_text(errors="replace").splitlines() - session_id = filtering.resolve_session_id(lines, config.device_id, path.name) - result = filtering.filter_session_lines(lines, config.device_id) + # Stream the file: a raw-ADC session can be hundreds of MB, but we only + # keep the (tiny) shot lines. Loading it whole would risk OOM on a Pi. + result = filtering.filter_session_file(path, config.device_id) + session_id = result.session_id if dry_run: _describe_dry_run(path.name, session_id, result, out) diff --git a/src/openflight/cloud/filtering.py b/src/openflight/cloud/filtering.py index b37dbe2e5..a3a67ca83 100644 --- a/src/openflight/cloud/filtering.py +++ b/src/openflight/cloud/filtering.py @@ -13,6 +13,7 @@ import json import uuid from dataclasses import dataclass, field +from pathlib import Path from typing import Any, Dict, Iterable, List, Optional from .. import __version__ @@ -63,6 +64,7 @@ class FilterResult: kept_lines: List[str] dropped_oversize: int = 0 kept_type_counts: Dict[str, int] = field(default_factory=dict) + session_id: str = "" def _iter_entries(lines: Iterable[str]): @@ -79,37 +81,27 @@ def _iter_entries(lines: Iterable[str]): yield stripped, parsed -def resolve_session_id(lines: Iterable[str], device_id: str, filename: str) -> str: - """Resolve the upload session id. - - Prefer the ``session_uuid`` embedded in the ``session_start`` entry - (present since openflight 0.2.0). For older sessions lacking it, fall back - to a deterministic UUIDv5 of ``(device_id, filename)`` so the same file - always maps to the same id. Always lowercase. - """ - for _, entry in _iter_entries(lines): - if entry.get("type") == "session_start": - session_uuid = entry.get("session_uuid") - if session_uuid: - return str(session_uuid).lower() - break - return str(uuid.uuid5(SESSION_NAMESPACE, f"{device_id}:{filename}")) - - -def filter_session_lines( - lines: Iterable[str], device_id: str, client_version: str = CLIENT_VERSION +def _filter_entries( + lines: Iterable[str], device_id: str, filename: str, client_version: str ) -> FilterResult: - """Filter raw session lines to the allowlist and build the manifest. + """Single-pass core: consume ``lines`` once, applying the allowlist. - Drops non-allowlisted types, drops any kept line over ``MAX_LINE_BYTES`` - (counting them), and skips blank/unparseable lines. + Works on any line iterable — including an open file object, which streams + line by line so a huge raw-ADC session is never held in memory at once. + Captures the ``session_uuid`` from ``session_start`` during the same pass + so we don't need a second read to resolve the upload id. """ kept_lines: List[str] = [] kept_type_counts: Dict[str, int] = {} dropped_oversize = 0 + embedded_uuid = "" for raw, entry in _iter_entries(lines): entry_type = entry.get("type") + if entry_type == "session_start" and not embedded_uuid: + session_uuid = entry.get("session_uuid") + if session_uuid: + embedded_uuid = str(session_uuid).lower() if entry_type not in KEEP_ENTRY_TYPES: continue if len(raw.encode("utf-8")) > MAX_LINE_BYTES: @@ -118,6 +110,7 @@ def filter_session_lines( kept_lines.append(raw) kept_type_counts[entry_type] = kept_type_counts.get(entry_type, 0) + 1 + session_id = embedded_uuid or str(uuid.uuid5(SESSION_NAMESPACE, f"{device_id}:{filename}")) manifest = { "type": MANIFEST_TYPE, "format_version": MANIFEST_FORMAT_VERSION, @@ -131,9 +124,37 @@ def filter_session_lines( kept_lines=kept_lines, dropped_oversize=dropped_oversize, kept_type_counts=kept_type_counts, + session_id=session_id, ) +def filter_session_lines( + lines: Iterable[str], + device_id: str, + client_version: str = CLIENT_VERSION, + filename: str = "", +) -> FilterResult: + """Filter raw session lines to the allowlist and build the manifest. + + Drops non-allowlisted types, drops any kept line over ``MAX_LINE_BYTES`` + (counting them), and skips blank/unparseable lines. ``filename`` is only + used for the UUIDv5 session-id fallback when no ``session_uuid`` is present. + """ + return _filter_entries(lines, device_id, filename, client_version) + + +def filter_session_file(path, device_id: str, client_version: str = CLIENT_VERSION) -> FilterResult: + """Stream-filter a session file by path without loading it into memory. + + The raw ADC (rolling_buffer_capture / kld7_buffer / iq_blocks) is dropped as + each line is read, so peak memory stays near a single line regardless of how + large the raw-ADC file is. This is the production path for ``push``. + """ + path = Path(path) + with path.open(encoding="utf-8", errors="replace") as handle: + return _filter_entries(handle, device_id, path.name, client_version) + + def build_upload_body( result: FilterResult, max_gzip_bytes: Optional[int] = None, diff --git a/tests/test_cloud_filtering.py b/tests/test_cloud_filtering.py index c0ee719ed..40c722e80 100644 --- a/tests/test_cloud_filtering.py +++ b/tests/test_cloud_filtering.py @@ -13,29 +13,6 @@ def _line(entry_type, **fields): return json.dumps({"ts": "2026-06-14T00:00:00", "type": entry_type, **fields}) -class TestResolveSessionId: - def test_uses_embedded_session_uuid_lowercased(self): - u = "1F0E9C2A-7B3D-4E5F-8A9B-0C1D2E3F4A5B" - lines = [_line("session_start", session_uuid=u)] - assert flt.resolve_session_id(lines, "dev-1", "session_x.jsonl") == u.lower() - - def test_uuid5_fallback_when_no_session_uuid(self): - lines = [_line("session_start")] - result = flt.resolve_session_id(lines, "dev-1", "session_x.jsonl") - expected = str(uuid.uuid5(flt.SESSION_NAMESPACE, "dev-1:session_x.jsonl")) - assert result == expected - - def test_uuid5_fallback_is_deterministic(self): - a = flt.resolve_session_id([], "dev-1", "session_x.jsonl") - b = flt.resolve_session_id([], "dev-1", "session_x.jsonl") - assert a == b - - def test_uuid5_fallback_differs_by_device(self): - a = flt.resolve_session_id([], "dev-1", "session_x.jsonl") - b = flt.resolve_session_id([], "dev-2", "session_x.jsonl") - assert a != b - - class TestFilterSessionLines: def test_keeps_only_allowlisted_types(self): lines = [ @@ -105,6 +82,79 @@ def test_kept_entry_types_are_sorted_and_unique(self): assert result.manifest["kept_entry_types"] == ["session_start", "shot_detected"] +class TestFilterSessionFile: + """Streaming filter that reads a file path without materializing it.""" + + def _write(self, path, *entries): + path.write_text("\n".join(json.dumps(e) for e in entries) + "\n") + + def test_keeps_shots_and_strips_raw_adc(self, tmp_path): + path = tmp_path / "session_x.jsonl" + self._write( + path, + {"type": "session_start", "session_uuid": "1F0E9C2A-7B3D-4E5F-8A9B-0C1D2E3F4A5B"}, + {"type": "rolling_buffer_capture", "i_samples": [1, 2, 3], "q_samples": [4, 5]}, + {"type": "shot_detected", "ball_speed_mph": 142}, + {"type": "kld7_buffer", "frames": [{"radc_b64": "AAAA"}]}, + {"type": "shot_detected", "ball_speed_mph": 99}, + {"type": "session_end"}, + ) + result = flt.filter_session_file(path, device_id="dev-1") + kept_types = [json.loads(line)["type"] for line in result.kept_lines] + assert kept_types == ["session_start", "shot_detected", "shot_detected", "session_end"] + assert result.kept_type_counts["shot_detected"] == 2 + + def test_resolves_session_id_from_embedded_uuid(self, tmp_path): + path = tmp_path / "session_x.jsonl" + self._write( + path, + {"type": "session_start", "session_uuid": "1F0E9C2A-7B3D-4E5F-8A9B-0C1D2E3F4A5B"}, + {"type": "shot_detected", "ball_speed_mph": 90}, + ) + result = flt.filter_session_file(path, device_id="dev-1") + assert result.session_id == "1f0e9c2a-7b3d-4e5f-8a9b-0c1d2e3f4a5b" + + def test_session_id_uuid5_fallback_uses_filename(self, tmp_path): + path = tmp_path / "session_x.jsonl" + self._write(path, {"type": "session_start"}, {"type": "shot_detected"}) + result = flt.filter_session_file(path, device_id="dev-1") + expected = str(uuid.uuid5(flt.SESSION_NAMESPACE, "dev-1:session_x.jsonl")) + assert result.session_id == expected + + def test_does_not_load_whole_file_into_memory(self, tmp_path): + """Regression: filtering a large raw-ADC file must use bounded memory. + + The old path read the entire file via read_text().splitlines(), peaking + at ~2x the file size — enough to OOM-kill the push on a Pi, dropping the + whole upload ("0 shots uploaded"). Streaming keeps peak ~one line. + """ + import tracemalloc + + path = tmp_path / "session_big.jsonl" + # ~20 MB of raw ADC (100 lines x ~200 KB), plus a handful of shots. + big = {"type": "rolling_buffer_capture", "i_samples": list(range(25000))} + with path.open("w") as f: + f.write(json.dumps({"type": "session_start", "session_uuid": "u"}) + "\n") + for i in range(100): + f.write(json.dumps(big) + "\n") + if i % 25 == 0: + f.write(json.dumps({"type": "shot_detected", "ball_speed_mph": 90}) + "\n") + f.write(json.dumps({"type": "session_end"}) + "\n") + + file_size = path.stat().st_size + assert file_size > 15 * 1024 * 1024 # sanity: the file really is large + + tracemalloc.start() + result = flt.filter_session_file(path, device_id="d") + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + assert result.kept_type_counts["shot_detected"] == 4 + # Streaming peaks at roughly one line; the file is >15 MB. A generous + # 8 MB ceiling cleanly fails the old whole-file-in-memory approach. + assert peak < 8 * 1024 * 1024, f"peak {peak / 1024 / 1024:.1f} MB — file not streamed" + + class TestBuildUploadBody: def test_body_is_gzip_with_manifest_first(self): lines = [ From 46f022e0998182610c7baff75b688dcdccfd5ecc Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Mon, 15 Jun 2026 13:09:51 -0700 Subject: [PATCH 27/29] [cr] add retry to uploader --- docs/cloud-sync.md | 17 ++++-- src/openflight/cloud/cli.py | 23 +++++++- src/openflight/cloud/commands.py | 78 +++++++++++++++++++++++---- src/openflight/cloud/spool.py | 20 +++++++ tests/test_cloud_cli.py | 55 +++++++++++++++++-- tests/test_cloud_client.py | 24 ++++++--- tests/test_cloud_commands.py | 92 ++++++++++++++++++++++++++++++-- tests/test_cloud_spool.py | 32 +++++++++++ 8 files changed, 307 insertions(+), 34 deletions(-) diff --git a/docs/cloud-sync.md b/docs/cloud-sync.md index 7390e2a68..3c1debece 100644 --- a/docs/cloud-sync.md +++ b/docs/cloud-sync.md @@ -18,6 +18,7 @@ openflight-cloud link # one-time: pair this Pi with your account openflight-cloud status # linked? queued? parked? last error? openflight-cloud push --dry-run # show exactly which entries would upload openflight-cloud push # filter + upload anything not yet pushed +openflight-cloud push --retry # also re-attempt parked/failed sessions ``` `scripts/setup/setup.sh` offers to enable cloud sync and link the Pi for you @@ -141,10 +142,18 @@ exists. - **"needs re-link" / 401** — the token was revoked or rotated. Re-run `openflight-cloud link`; the new token replaces the old one in the config. - **A session is parked** — `status` shows the reason and last error. Parked - sessions are skipped on future runs. To retry one, delete its - `.jsonl.parked` (and `.jsonl.state`) sidecar. -- **Nothing uploads** — confirm `enabled: true` in the config and that the Pi - is online (`openflight-cloud status` reports reachability). + sessions are skipped on future runs. Re-attempt all parked/deferred sessions + with `openflight-cloud push --retry`. +- **A session uploaded with 0 shots (or you want to re-send a stored one)** — + `status` flags 0-shot uploads by name. Force a re-upload of a specific + session (even one already marked pushed) with + `openflight-cloud push --retry ` — `` is the + filename or any substring of it. Re-uploads are idempotent (the server + dedupes), so this is always safe. +- **Nothing uploads / "Nothing to upload"** — everything is already pushed. + Confirm `enabled: true` and that the Pi is online (`status` reports + reachability). To re-send a session that was already pushed, use + `push --retry ` as above. - **"What is it sending?"** — `openflight-cloud push --dry-run` lists every entry type and count that would be uploaded, and what's dropped. diff --git a/src/openflight/cloud/cli.py b/src/openflight/cloud/cli.py index 39f3bdd1c..d098048b3 100644 --- a/src/openflight/cloud/cli.py +++ b/src/openflight/cloud/cli.py @@ -51,6 +51,18 @@ def _build_parser() -> argparse.ArgumentParser: action="store_true", help="Show exactly which entries would upload; send nothing.", ) + push.add_argument( + "--retry", + nargs="?", + const="__ALL__", + default=None, + metavar="SESSION", + help=( + "Re-upload sessions that were parked or failed. Give a session " + "filename (or substring) to force re-upload a specific one even if " + "it was already sent." + ), + ) sub.add_parser("status", parents=[common], help="Show link state, queue, and parked sessions.") return parser @@ -76,7 +88,16 @@ def main(argv: Optional[List[str]] = None) -> int: return 0 if ok else 1 if args.command == "push": - summary = commands.cmd_push(config, args.log_dir, _client(config), dry_run=args.dry_run) + retry = args.retry is not None + session = None if args.retry == "__ALL__" else args.retry + summary = commands.cmd_push( + config, + args.log_dir, + _client(config), + dry_run=args.dry_run, + retry=retry, + session=session, + ) return 1 if summary.get("needs_relink") else 0 if args.command == "status": diff --git a/src/openflight/cloud/commands.py b/src/openflight/cloud/commands.py index f8bb35afc..aacd3e6da 100644 --- a/src/openflight/cloud/commands.py +++ b/src/openflight/cloud/commands.py @@ -78,14 +78,48 @@ def _describe_dry_run(filename: str, session_id: str, result: filtering.FilterRe out(f" drop {result.dropped_oversize:>5} oversize line(s) (>32 KB)") +def _apply_retry(log_dir: Path, session: Optional[str], out: OutFn) -> int: + """Clear markers so failed/parked (or named) sessions upload again. + + Bulk retry (no ``session``) un-parks and clears cooldowns but leaves + already-pushed sessions alone. A named ``session`` (filename substring) + force-clears even ``.pushed`` so a session the server already stored — e.g. + one that landed with 0 shots — can be re-sent. Returns how many matched. + """ + targeted = bool(session) + matched = 0 + for path in spool.session_files(log_dir): + if targeted: + if session not in path.name: + continue + elif not (spool.is_parked(path) or spool.in_cooldown(path)): + continue + cleared = spool.clear_markers(path, include_pushed=targeted) + matched += 1 + if cleared: + out(f"{path.name}: reset {', '.join(cleared)} for retry.") + if matched == 0: + if targeted: + out(f"No session matching '{session}'. Run `openflight-cloud status` to list them.") + else: + out("No parked or deferred sessions to retry.") + return matched + + def cmd_push( config: CloudConfig, log_dir: Path, client, dry_run: bool = False, + retry: bool = False, + session: Optional[str] = None, out: OutFn = print, ) -> Dict[str, Any]: - """Filter and upload anything unpushed. Returns a summary dict.""" + """Filter and upload anything unpushed. Returns a summary dict. + + With ``retry``, first reset markers so previously parked/failed sessions + (or a specific ``session`` by filename substring) are eligible again. + """ summary: Dict[str, Any] = { "uploaded": 0, "parked": 0, @@ -101,6 +135,9 @@ def cmd_push( summary["skipped"] = "inactive" return summary + if retry: + _apply_retry(log_dir, session, out) + # Cheap connectivity probe so we don't churn while offline. if not dry_run and not client.health(): out("Cloud unreachable; will retry later.") @@ -217,24 +254,43 @@ def cmd_status( for name, info in parked: out(f" {name}: {info.get('reason')} (last_error={info.get('last_error')})") + zero_shot = _zero_shot_uploads(log_dir) + if zero_shot: + out("Uploaded with 0 shots (re-upload with `openflight-cloud push --retry `):") + for name in zero_shot: + out(f" {name}") + return { "counts": counts, "parked": [name for name, _ in parked], + "zero_shot": zero_shot, "linked": config.is_linked(), "online": online, } -def _parked_details(log_dir: Path) -> List: +def _read_marker(path: Path, suffix: str) -> dict: import json - details = [] + marker = path.with_name(path.name + suffix) + try: + return json.loads(marker.read_text()) + except (json.JSONDecodeError, ValueError, OSError): + return {} + + +def _parked_details(log_dir: Path) -> List: + return [ + (path.name, _read_marker(path, spool.PARKED_SUFFIX)) + for path in spool.session_files(log_dir) + if spool.is_parked(path) + ] + + +def _zero_shot_uploads(log_dir: Path) -> List[str]: + """Pushed sessions the server stored with no shots — likely worth re-sending.""" + names = [] for path in spool.session_files(log_dir): - if spool.is_parked(path): - marker = path.with_name(path.name + spool.PARKED_SUFFIX) - try: - info = json.loads(marker.read_text()) - except (json.JSONDecodeError, ValueError, OSError): - info = {} - details.append((path.name, info)) - return details + if spool.is_pushed(path) and not _read_marker(path, spool.PUSHED_SUFFIX).get("shot_count"): + names.append(path.name) + return names diff --git a/src/openflight/cloud/spool.py b/src/openflight/cloud/spool.py index f8336aa1a..e6a82f62a 100644 --- a/src/openflight/cloud/spool.py +++ b/src/openflight/cloud/spool.py @@ -150,6 +150,26 @@ def mark_parked(path: Path, reason: str, attempts: int, last_error: Optional[str ) +def clear_markers(path: Path, include_pushed: bool = False) -> List[str]: + """Remove terminal/retry markers so a session becomes pending again. + + Clears ``.parked`` and ``.state`` (un-park + reset attempts/cooldown). With + ``include_pushed`` also clears ``.pushed`` to force re-upload of a session + the server already stored (idempotent server-side). Returns the suffixes + actually removed. + """ + suffixes = [PARKED_SUFFIX, STATE_SUFFIX] + if include_pushed: + suffixes.append(PUSHED_SUFFIX) + cleared: List[str] = [] + for suffix in suffixes: + marker = _sidecar(path, suffix) + if marker.exists(): + marker.unlink() + cleared.append(suffix) + return cleared + + def summarize(log_dir: Path) -> Dict[str, int]: """Count pushed / parked / pending sessions for ``status``.""" files = session_files(log_dir) diff --git a/tests/test_cloud_cli.py b/tests/test_cloud_cli.py index 836fc1018..1786edfd1 100644 --- a/tests/test_cloud_cli.py +++ b/tests/test_cloud_cli.py @@ -32,18 +32,63 @@ def fake_status(config, log_dir, client=None, out=print): def test_push_passes_dry_run_flag(self, monkeypatch, tmp_path): captured = {} - def fake_push(config, log_dir, client, dry_run=False, out=print): + def fake_push(config, log_dir, client, dry_run=False, retry=False, session=None, out=print): captured["dry_run"] = dry_run return {"needs_relink": False} monkeypatch.setattr(cli.commands, "cmd_push", fake_push) - cli.main(["push", "--dry-run", "--log-dir", str(tmp_path), "--config", str(tmp_path / "c.json")]) + cli.main( + ["push", "--dry-run", "--log-dir", str(tmp_path), "--config", str(tmp_path / "c.json")] + ) assert captured["dry_run"] is True - def test_push_returns_nonzero_when_relink_needed(self, monkeypatch, tmp_path): - monkeypatch.setattr( - cli.commands, "cmd_push", lambda *a, **k: {"needs_relink": True} + def test_push_retry_all(self, monkeypatch, tmp_path): + captured = {} + + def fake_push(config, log_dir, client, dry_run=False, retry=False, session=None, out=print): + captured.update(retry=retry, session=session) + return {"needs_relink": False} + + monkeypatch.setattr(cli.commands, "cmd_push", fake_push) + cli.main( + ["push", "--retry", "--log-dir", str(tmp_path), "--config", str(tmp_path / "c.json")] + ) + assert captured == {"retry": True, "session": None} + + def test_push_retry_named_session(self, monkeypatch, tmp_path): + captured = {} + + def fake_push(config, log_dir, client, dry_run=False, retry=False, session=None, out=print): + captured.update(retry=retry, session=session) + return {"needs_relink": False} + + monkeypatch.setattr(cli.commands, "cmd_push", fake_push) + cli.main( + [ + "push", + "--retry", + "session_20260527", + "--log-dir", + str(tmp_path), + "--config", + str(tmp_path / "c.json"), + ] ) + assert captured == {"retry": True, "session": "session_20260527"} + + def test_push_no_retry_by_default(self, monkeypatch, tmp_path): + captured = {} + + def fake_push(config, log_dir, client, dry_run=False, retry=False, session=None, out=print): + captured.update(retry=retry, session=session) + return {"needs_relink": False} + + monkeypatch.setattr(cli.commands, "cmd_push", fake_push) + cli.main(["push", "--log-dir", str(tmp_path), "--config", str(tmp_path / "c.json")]) + assert captured == {"retry": False, "session": None} + + def test_push_returns_nonzero_when_relink_needed(self, monkeypatch, tmp_path): + monkeypatch.setattr(cli.commands, "cmd_push", lambda *a, **k: {"needs_relink": True}) rc = cli.main(["push", "--log-dir", str(tmp_path), "--config", str(tmp_path / "c.json")]) assert rc != 0 diff --git a/tests/test_cloud_client.py b/tests/test_cloud_client.py index d1c25c0dd..03ece5dde 100644 --- a/tests/test_cloud_client.py +++ b/tests/test_cloud_client.py @@ -15,9 +15,7 @@ def __init__(self, responses): self.calls = [] def __call__(self, method, url, data=None, headers=None, timeout=30): - self.calls.append( - {"method": method, "url": url, "data": data, "headers": headers or {}} - ) + self.calls.append({"method": method, "url": url, "data": data, "headers": headers or {}}) return self._responses.pop(0) @@ -28,7 +26,9 @@ def _resp(status, body=None, headers=None): class TestHealth: def test_ok_returns_true(self): - client = cl.CloudClient("https://e.test", request_fn=FakeTransport([_resp(200, {"status": "ok"})])) + client = cl.CloudClient( + "https://e.test", request_fn=FakeTransport([_resp(200, {"status": "ok"})]) + ) assert client.health() is True def test_non_ok_returns_false(self): @@ -66,7 +66,9 @@ def test_parses_link_start_response(self): assert result.expires_s == 900 def test_sends_device_name_and_version(self): - t = FakeTransport([_resp(200, {"link_code": "A", "poll_token": "p", "interval_s": 5, "expires_s": 900})]) + t = FakeTransport( + [_resp(200, {"link_code": "A", "poll_token": "p", "interval_s": 5, "expires_s": 900})] + ) cl.CloudClient("https://e.test", request_fn=t).device_link_start("garage pi", "9.9.9") sent = json.loads(t.calls[0]["data"].decode()) assert sent == {"device_name": "garage pi", "client_version": "9.9.9"} @@ -116,16 +118,22 @@ def _client(self, responses): ) def test_201_is_success(self): - r = self._client([_resp(201, {"session_id": "s1", "shot_count": 7})]).upload_session("s1", b"gz") + r = self._client([_resp(201, {"session_id": "s1", "shot_count": 7})]).upload_session( + "s1", b"gz" + ) assert r.action == "success" assert r.shot_count == 7 def test_200_is_success(self): - r = self._client([_resp(200, {"session_id": "s1", "shot_count": 7})]).upload_session("s1", b"gz") + r = self._client([_resp(200, {"session_id": "s1", "shot_count": 7})]).upload_session( + "s1", b"gz" + ) assert r.action == "success" def test_401_needs_relink(self): - r = self._client([_resp(401, {"reason": "invalid_or_revoked_token"})]).upload_session("s1", b"gz") + r = self._client([_resp(401, {"reason": "invalid_or_revoked_token"})]).upload_session( + "s1", b"gz" + ) assert r.action == "relink" def test_402_quota(self): diff --git a/tests/test_cloud_commands.py b/tests/test_cloud_commands.py index e77e29b92..e4beb4356 100644 --- a/tests/test_cloud_commands.py +++ b/tests/test_cloud_commands.py @@ -109,20 +109,26 @@ def test_relink_stops_and_flags(self, tmp_path): assert len(client.uploaded) == 1 def test_park_on_422(self, tmp_path): - path = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"}) + path = _write_session( + tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"} + ) client = FakeClient(uploads=[UploadResult(422, action="park", reason="invalid_gzip")]) commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) assert spool.is_parked(path) def test_quota_sets_cooldown_not_park(self, tmp_path): - path = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"}) + path = _write_session( + tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"} + ) client = FakeClient(uploads=[UploadResult(402, action="quota", reason="quota_exceeded")]) commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) assert not spool.is_parked(path) assert spool.in_cooldown(path) def test_5xx_records_failure_and_leaves_pending(self, tmp_path): - path = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"}) + path = _write_session( + tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"} + ) client = FakeClient(uploads=[UploadResult(503, action="retry")]) commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) assert spool.read_attempts(path) == 1 @@ -130,7 +136,9 @@ def test_5xx_records_failure_and_leaves_pending(self, tmp_path): assert not spool.is_parked(path) def test_oversize_body_parks(self, tmp_path, monkeypatch): - path = _write_session(tmp_path, "session_a.jsonl", {"type": "shot_detected", "ball_speed_mph": 90}) + path = _write_session( + tmp_path, "session_a.jsonl", {"type": "shot_detected", "ball_speed_mph": 90} + ) from openflight.cloud import filtering monkeypatch.setattr(filtering, "MAX_GZIP_BYTES", 1) @@ -140,7 +148,9 @@ def test_oversize_body_parks(self, tmp_path, monkeypatch): assert spool.is_parked(path) def test_skips_sessions_in_cooldown(self, tmp_path): - path = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"}) + path = _write_session( + tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"} + ) spool.record_cooldown(path, "quota_exceeded", seconds=spool.QUOTA_COOLDOWN_S) client = FakeClient(uploads=[UploadResult(201, action="success")]) result = commands.cmd_push(_linked_config(), tmp_path, client, out=lambda _m: None) @@ -148,6 +158,65 @@ def test_skips_sessions_in_cooldown(self, tmp_path): assert result["deferred"] == 1 +class TestPushRetry: + def test_retry_all_reuploads_parked_session(self, tmp_path): + path = _write_session( + tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"} + ) + spool.mark_parked(path, reason="max_attempts", attempts=20, last_error="503") + client = FakeClient(uploads=[UploadResult(201, action="success", shot_count=1)]) + result = commands.cmd_push( + _linked_config(), tmp_path, client, retry=True, out=lambda _m: None + ) + assert client.uploaded == ["a"] + assert spool.is_pushed(path) + assert result["uploaded"] == 1 + + def test_retry_all_reuploads_cooled_down_session(self, tmp_path): + path = _write_session( + tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"} + ) + spool.record_cooldown(path, "quota_exceeded", seconds=spool.QUOTA_COOLDOWN_S) + client = FakeClient(uploads=[UploadResult(201, action="success", shot_count=1)]) + commands.cmd_push(_linked_config(), tmp_path, client, retry=True, out=lambda _m: None) + assert client.uploaded == ["a"] + + def test_retry_all_leaves_pushed_sessions_alone(self, tmp_path): + path = _write_session( + tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"} + ) + spool.mark_pushed(path, "a", 1) + client = FakeClient(uploads=[]) + commands.cmd_push(_linked_config(), tmp_path, client, retry=True, out=lambda _m: None) + assert client.uploaded == [] + assert spool.is_pushed(path) + + def test_retry_named_session_force_reuploads_pushed(self, tmp_path): + path = _write_session( + tmp_path, "session_20260527_x.jsonl", {"type": "session_start", "session_uuid": "a"} + ) + spool.mark_pushed(path, "a", 0) # previously "uploaded" with 0 shots + client = FakeClient(uploads=[UploadResult(201, action="success", shot_count=5)]) + commands.cmd_push( + _linked_config(), tmp_path, client, retry=True, session="20260527", out=lambda _m: None + ) + assert client.uploaded == ["a"] + assert spool.is_pushed(path) + + def test_retry_named_no_match_reports_and_uploads_nothing(self, tmp_path): + path = _write_session( + tmp_path, "session_a.jsonl", {"type": "session_start", "session_uuid": "a"} + ) + spool.mark_pushed(path, "a", 1) + client = FakeClient(uploads=[]) + out = [] + commands.cmd_push( + _linked_config(), tmp_path, client, retry=True, session="nope", out=out.append + ) + assert client.uploaded == [] + assert "no" in "\n".join(out).lower() + + class TestLink: def test_links_and_saves_config(self, tmp_path): config_path = tmp_path / "cloud.json" @@ -204,6 +273,19 @@ def test_reports_reachability_when_client_given(self, tmp_path): assert result["online"] is False assert "unreachable" in "\n".join(out).lower() + def test_flags_zero_shot_uploads_with_retry_hint(self, tmp_path): + a = _write_session(tmp_path, "session_zero.jsonl", {"type": "session_start"}) + b = _write_session(tmp_path, "session_ok.jsonl", {"type": "session_start"}) + spool.mark_pushed(a, "id-a", 0) + spool.mark_pushed(b, "id-b", 12) + out = [] + result = commands.cmd_status(_linked_config(), tmp_path, out=out.append) + text = "\n".join(out) + assert "session_zero.jsonl" in text + assert "--retry" in text + assert "session_ok.jsonl" not in text # healthy uploads aren't nagged + assert result["zero_shot"] == ["session_zero.jsonl"] + def test_reports_counts_and_parked(self, tmp_path): a = _write_session(tmp_path, "session_a.jsonl", {"type": "session_start"}) b = _write_session(tmp_path, "session_b.jsonl", {"type": "session_start"}) diff --git a/tests/test_cloud_spool.py b/tests/test_cloud_spool.py index 61b4cd9e7..ba3497498 100644 --- a/tests/test_cloud_spool.py +++ b/tests/test_cloud_spool.py @@ -112,6 +112,38 @@ def test_cooldown_does_not_park(self, tmp_path): assert not spool.is_parked(path) +class TestClearMarkers: + def test_clears_parked_and_state_keeps_pushed_by_default(self, tmp_path): + path = _session(tmp_path) + spool.mark_pushed(path, "id", 1) + spool.mark_parked(path, reason="r", attempts=2, last_error="e") + spool.record_failure(path, "e") # writes .state + cleared = spool.clear_markers(path) + assert spool.PARKED_SUFFIX in cleared + assert spool.STATE_SUFFIX in cleared + assert spool.PUSHED_SUFFIX not in cleared + assert spool.is_pushed(path) + assert not spool.is_parked(path) + + def test_include_pushed_clears_everything(self, tmp_path): + path = _session(tmp_path) + spool.mark_pushed(path, "id", 1) + cleared = spool.clear_markers(path, include_pushed=True) + assert spool.PUSHED_SUFFIX in cleared + assert not spool.is_pushed(path) + + def test_returns_empty_when_no_markers(self, tmp_path): + path = _session(tmp_path) + assert spool.clear_markers(path) == [] + + def test_after_clear_session_is_pending_again(self, tmp_path): + a = _session(tmp_path, "session_a.jsonl") + spool.mark_parked(a, reason="r", attempts=20, last_error="e") + assert a not in spool.pending_sessions(tmp_path) + spool.clear_markers(a) + assert a in spool.pending_sessions(tmp_path) + + class TestStatusSummary: def test_summarizes_counts(self, tmp_path): a = _session(tmp_path, "session_a.jsonl") From 658a0f349ced6f3e92c4fe8e0896837c3deb9b33 Mon Sep 17 00:00:00 2001 From: John Pacino Date: Tue, 16 Jun 2026 00:19:23 -0400 Subject: [PATCH 28/29] feat: ball-speed cosine correction + opt-in calculated spin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two opt-in launch-monitor accuracy features, both off by default. --ball-speed-cosine-correction: the OPS243 reads the radial component of a ball departing at the launch angle; correct_ball_speed() divides out cos(launch − ball elevation from radar) using the measured VLA + radar geometry (--kld7-ball-distance, new --kld7-radar-height-inches). Works with any launch-angle source. --calculated-spin: the 24 GHz return carries no usable spin line, so when the vertical launch angle was radar/camera-measured, spin_rpm is rewritten with the kinematic estimate 170·v·sin(LA)^1.2 (spin_estimate.py); the displaced radar value is kept in spin_rpm_measured. Runs after the cosine correction (the model is calibrated on true ball speed). Adds spin_source / ball_speed_raw_mph provenance to the shot payload and the kiosk flag passthroughs. Tests: test_speed_correction, test_spin_estimate, and TestApplyCalculatedSpin (radar/camera gate, model range, rewrite). Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/start-kiosk.sh | 18 +++++ src/openflight/launch_monitor.py | 7 ++ src/openflight/server.py | 111 ++++++++++++++++++++++++++++- src/openflight/speed_correction.py | 76 ++++++++++++++++++++ src/openflight/spin_estimate.py | 71 ++++++++++++++++++ tests/test_server.py | 47 ++++++++++++ tests/test_speed_correction.py | 55 ++++++++++++++ tests/test_spin_estimate.py | 50 +++++++++++++ 8 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 src/openflight/speed_correction.py create mode 100644 src/openflight/spin_estimate.py create mode 100644 tests/test_speed_correction.py create mode 100644 tests/test_spin_estimate.py diff --git a/scripts/start-kiosk.sh b/scripts/start-kiosk.sh index a0b8ca1de..b9d35b326 100755 --- a/scripts/start-kiosk.sh +++ b/scripts/start-kiosk.sh @@ -43,6 +43,8 @@ EXPERIMENTAL_KLD7_HORIZONTAL_IMPACT_ENERGY="" EXPERIMENTAL_KLD7_HORIZONTAL_RETRY_IMPACT_ENERGY="" EXPERIMENTAL_KLD7_HORIZONTAL_ANGLE_LIMIT="" BALLISTICS=false +CALCULATED_SPIN=false +BALL_SPEED_COSINE=false # Buffer split presets (pre/post trigger segments out of 32 total) # At 20ksps: each segment = 6.4ms, total buffer = 204.8ms @@ -203,6 +205,14 @@ while [[ $# -gt 0 ]]; do BALLISTICS=true shift ;; + --calculated-spin) + CALCULATED_SPIN=true + shift + ;; + --ball-speed-cosine-correction) + BALL_SPEED_COSINE=true + shift + ;; --port|-p) PORT="$2" shift 2 @@ -330,6 +340,14 @@ if [ "$BALLISTICS" = true ]; then SERVER_CMD="$SERVER_CMD --ballistics" fi +if [ "$CALCULATED_SPIN" = true ]; then + SERVER_CMD="$SERVER_CMD --calculated-spin" +fi + +if [ "$BALL_SPEED_COSINE" = true ]; then + SERVER_CMD="$SERVER_CMD --ball-speed-cosine-correction" +fi + if [ -n "$TRIGGER" ]; then SERVER_CMD="$SERVER_CMD --trigger $TRIGGER" fi diff --git a/src/openflight/launch_monitor.py b/src/openflight/launch_monitor.py index 1654aef27..4630ae08b 100644 --- a/src/openflight/launch_monitor.py +++ b/src/openflight/launch_monitor.py @@ -247,6 +247,9 @@ class Shot: impact_timestamp: Optional[float] = None impact_timestamp_kld7: Optional[float] = None club_speed_mph: Optional[float] = None + # Raw OPS radial ball speed, kept when the cosine correction rewrites + # ball_speed_mph (radar bin anchoring must keep using the radial value) + ball_speed_raw_mph: Optional[float] = None peak_magnitude: Optional[float] = None readings: List[SpeedReading] = field(default_factory=list) club: ClubType = ClubType.DRIVER @@ -259,6 +262,10 @@ class Shot: launch_angle_horizontal_source: Optional[str] = None spin_rpm: Optional[float] = None spin_confidence: Optional[float] = None + # Raw radar-measured spin, kept when --calculated-spin rewrites + # spin_rpm with the kinematic estimate (for offline scoring) + spin_rpm_measured: Optional[float] = None + spin_source: Optional[str] = None # "measured", "calculated", or None spin_result_quality: Optional[str] = None spin_snr: Optional[float] = None spin_modulation_depth: Optional[float] = None diff --git a/src/openflight/server.py b/src/openflight/server.py index 1afd5255f..a532ae98d 100644 --- a/src/openflight/server.py +++ b/src/openflight/server.py @@ -21,10 +21,12 @@ from flask_socketio import SocketIO from .ballistics import resolve_launch, simulate -from .launch_monitor import ClubType, Shot +from .launch_monitor import SPIN_CONFIDENCE_HIGH, ClubType, Shot from .ops243 import Direction, SpeedReading, set_show_raw_readings from .rolling_buffer.monitor import estimate_carry_with_spin, get_optimal_spin_for_ball_speed from .session_logger import get_session_logger, init_session_logger, log_session_error +from .speed_correction import correct_ball_speed +from .spin_estimate import calculated_spin_rpm # Configure logging logger = logging.getLogger(__name__) @@ -761,10 +763,19 @@ def _session_start_config() -> dict: return config +ball_speed_correction_enabled = False +ball_speed_correction_distance_ft = 5.5 +ball_speed_correction_ball_above_radar_ft = -4.0 / 12.0 +calculated_spin_enabled = False + + def shot_to_dict(shot: Shot) -> dict: """Convert Shot to JSON-serializable dict.""" return { "ball_speed_mph": round(shot.ball_speed_mph, 1), + "ball_speed_raw_mph": ( + round(shot.ball_speed_raw_mph, 1) if shot.ball_speed_raw_mph else None + ), "club_speed_mph": round(shot.club_speed_mph, 1) if shot.club_speed_mph else None, "smash_factor": round(shot.smash_factor, 2) if shot.smash_factor else None, "estimated_carry_yards": round(shot.estimated_carry_yards), @@ -789,6 +800,10 @@ def shot_to_dict(shot: Shot) -> dict: "spin_axis_deg": shot.spin_axis_deg, # Spin data from rolling buffer mode "spin_rpm": round(shot.spin_rpm) if shot.spin_rpm else None, + "spin_rpm_measured": ( + round(shot.spin_rpm_measured) if shot.spin_rpm_measured else None + ), + "spin_source": shot.spin_source, "spin_confidence": round(shot.spin_confidence, 2) if shot.spin_confidence else None, "spin_quality": shot.spin_quality, "spin_snr": round(shot.spin_snr, 2) if shot.spin_snr is not None else None, @@ -1485,6 +1500,38 @@ def handle_shutdown(): threading.Thread(target=_shutdown_process_after_delay, daemon=True).start() +def _apply_calculated_spin(shot: Shot) -> bool: + """Replace radar-measured spin with the kinematic estimate. + + The 24 GHz OPS return carries no usable spin line (see + spin_estimate.py), so when the vertical launch angle was actually + measured (radar/camera, not the club-table estimate), spin_rpm is + rewritten with 170*v*sin(LA)^1.2. The displaced measured value is + kept in spin_rpm_measured for offline scoring. Returns True when + the shot was rewritten. + """ + if shot.launch_angle_vertical is None: + return False + if shot.launch_angle_vertical_source not in ("radar", "camera"): + return False + spin_calc = calculated_spin_rpm(shot.ball_speed_mph, shot.launch_angle_vertical) + if spin_calc is None: + return False + shot.spin_rpm_measured = shot.spin_rpm + shot.spin_rpm = spin_calc + shot.spin_confidence = SPIN_CONFIDENCE_HIGH + shot.spin_source = "calculated" + shot.spin_rejection_reason = None + logger.info( + "[SERVER] Calculated spin: %.0f rpm (v=%.1f mph, LA=%.1f deg, measured was %s)", + spin_calc, + shot.ball_speed_mph, + shot.launch_angle_vertical, + "%.0f rpm" % shot.spin_rpm_measured if shot.spin_rpm_measured else "none", + ) + return True + + def on_shot_detected(shot: Shot): """Callback when a shot is detected - emit to all clients.""" global ball_detected, ball_detection_confidence # pylint: disable=global-statement @@ -1777,6 +1824,30 @@ def on_shot_detected(shot: Shot): # rejected or missing axes fall back to conservative estimates. _ensure_user_facing_launch_angles(shot) + # Ball-speed cosine correction: the OPS reads the radial component of + # a ball departing at the launch angle. Applied AFTER the K-LD7 (which + # must anchor to the radial speed) and BEFORE carry/ballistics. + if ball_speed_correction_enabled and shot.launch_angle_vertical is not None: + raw_speed = shot.ball_speed_mph + shot.ball_speed_raw_mph = raw_speed + shot.ball_speed_mph = correct_ball_speed( + raw_speed, + shot.launch_angle_vertical, + ball_speed_correction_distance_ft, + ball_speed_correction_ball_above_radar_ft, + ) + logger.info( + "[SERVER] Ball speed cosine correction: %.1f -> %.1f mph (LA %.1f)", + raw_speed, + shot.ball_speed_mph, + shot.launch_angle_vertical, + ) + + # Calculated spin runs AFTER the cosine correction (the model is + # calibrated on true ball speed) and BEFORE carry/ballistics. + if calculated_spin_enabled: + _apply_calculated_spin(shot) + # Compute carry. Prefer the physics simulator (drag + Magnus, RK4) when # ballistics is enabled and a vertical launch angle is available; fall # back to the table estimator otherwise (either ballistics disabled or @@ -2392,6 +2463,26 @@ def main(): default=0.0, help="K-LD7 vertical angle offset in degrees (default: 0.0)", ) + parser.add_argument( + "--ball-speed-cosine-correction", + action="store_true", + help=( + "Correct OPS radial ball speed to true ball speed using the launch " + "angle and radar geometry (validated vs TrackMan; see " + "src/openflight/speed_correction.py)" + ), + ) + parser.add_argument( + "--calculated-spin", + action="store_true", + help=( + "Replace radar-measured spin with the kinematic estimate " + "(170*v*sin(LA)^1.2) when the launch angle was measured. The 24 GHz " + "OPS return carries no usable spin line (see " + "src/openflight/spin_estimate.py); the measured value is kept in " + "spin_rpm_measured for offline scoring" + ), + ) parser.add_argument( "--kld7-vertical-estimator", choices=("geometry", "naive"), @@ -2416,6 +2507,16 @@ def main(): "(weak lever; default: 5.5)" ), ) + parser.add_argument( + "--kld7-radar-height-inches", + dest="kld7_radar_height_inches", + type=float, + default=4.0, + help=( + "K-LD7 radar height above the ball in inches, used by the ball-speed " + "cosine correction geometry (default: 4.0)" + ), + ) parser.add_argument( "--kld7-horizontal", action="store_true", @@ -2512,6 +2613,14 @@ def main(): global ballistics_enabled experimental_kld7_raw_radc_logging = args.experimental_kld7_raw_radc_logging experimental_kld7_radc_tuning = args.experimental_kld7_radc_tuning + global ball_speed_correction_enabled + global ball_speed_correction_distance_ft + global ball_speed_correction_ball_above_radar_ft + ball_speed_correction_enabled = args.ball_speed_cosine_correction + ball_speed_correction_distance_ft = args.kld7_ball_distance + ball_speed_correction_ball_above_radar_ft = -args.kld7_radar_height_inches / 12.0 + global calculated_spin_enabled + calculated_spin_enabled = args.calculated_spin ballistics_enabled = args.ballistics kld7_radc_tuning_kwargs = _kld7_radc_tuning_kwargs(args) active_kld7_radc_tuning = dict(kld7_radc_tuning_kwargs) diff --git a/src/openflight/speed_correction.py b/src/openflight/speed_correction.py new file mode 100644 index 000000000..73f399c10 --- /dev/null +++ b/src/openflight/speed_correction.py @@ -0,0 +1,76 @@ +"""Ball-speed cosine correction. + +The OPS243 measures RADIAL speed — the component of the ball's velocity +along the radar's line of sight. The ball departs upward at the launch +angle while the radar sits low behind the tee, so the radial reading is +compressed by cos(angle between velocity and LOS). This is the dominant +cause of the long-observed ~2-2.5 mph OPS-below-TrackMan ball speed gap. + +Model (zero free parameters): the OPS mode-based extraction reads near +the MAXIMUM of the radial-speed profile — radial speed rises early in +flight as the LOS aligns with the velocity vector, then falls with drag, +so the profile has a peak. The correction divides the measured speed by +that predicted peak fraction. + +Validated offline against TrackMan ball speeds (2026-06): +- 2026-06-08 bay, 128 shots: bias -2.15 -> +0.33 mph, |err| 2.83 -> 1.48 +- 2026-05-30 holdout, 26 shots: bias -2.26 -> +0.30, |err| 2.35 -> 1.31 +- Coleman cross-rig outdoor, 62 shots: bias -2.09 -> +0.65, median 0.58 +- Production configuration (OUR launch angles — 100 two-ray + 28 + club-fallback — instead of TrackMan's): bias +0.32, |err| 1.52, + median 0.67. The LA dependence couples speed accuracy to launch-angle + accuracy, but LA errors are zero-mean post-calibration, so the + coupling adds ~0.1 mph of scatter and no bias. + +Known caveat: high-launch wedges on one outdoor rig overcorrected (~+3); +under observation. Club speed does NOT need this correction — the club +head's delivery is nearly parallel to the LOS (error ~0.1-0.2 mph). +""" + +from __future__ import annotations + +import math + +MPH_TO_FTS = 1.4666667 +DRAG_MPH_PER_MS = 0.027 # iron-speed drag deceleration of the ball + + +def radial_speed_factor( + launch_angle_deg: float, + ball_speed_mph: float, + ball_distance_ft: float, + ball_above_radar_ft: float, + window_ms: float = 70.0, +) -> float: + """Predicted (OPS radial reading) / (true ball speed), in (0, 1]. + + Maximum of the radial-speed profile over the capture window: + radial(t) = v(t) * cos(launch - elevation_of_ball_from_radar(t)). + """ + if ball_speed_mph <= 0: + return 1.0 + la = math.radians(launch_angle_deg) + v_fts = ball_speed_mph * MPH_TO_FTS + best = 0.0 + t_ms = 0.0 + while t_ms <= window_ms: + v_frac = max(1.0 - DRAG_MPH_PER_MS * t_ms / ball_speed_mph, 0.0) + t = t_ms / 1000.0 + x = ball_distance_ft + v_fts * math.cos(la) * t + y = ball_above_radar_ft + v_fts * math.sin(la) * t + best = max(best, v_frac * math.cos(la - math.atan2(y, x))) + t_ms += 2.0 + return min(max(best, 0.5), 1.0) + + +def correct_ball_speed( + measured_mph: float, + launch_angle_deg: float, + ball_distance_ft: float, + ball_above_radar_ft: float, +) -> float: + """True ball speed from the OPS radial measurement.""" + factor = radial_speed_factor( + launch_angle_deg, measured_mph, ball_distance_ft, ball_above_radar_ft + ) + return measured_mph / factor diff --git a/src/openflight/spin_estimate.py b/src/openflight/spin_estimate.py new file mode 100644 index 000000000..bdb4819b1 --- /dev/null +++ b/src/openflight/spin_estimate.py @@ -0,0 +1,71 @@ +"""Calculated spin from measured ball speed and launch angle. + +The OPS243 return carries no usable spin line: golf-ball dimples +(~0.3 mm) are Rayleigh-smooth at 24 GHz (λ = 12.4 mm) and the specular +point does not rotate with the ball, so rotation barely modulates the +echo within our ~65 ms ball dwell. Offline analysis of the 2026-06-08 +TrackMan session (131 paired shots, PW-3h) found no spectral line at +the TrackMan spin frequency on any club after dechirp and carrier +removal — and the production envelope estimator's output had ~zero +within-club correlation with TrackMan spin (r ≈ +0.19). + +What does work is impact kinematics. Spin loft tracks launch angle and +the friction impulse gives spin ∝ v·sin(spin loft), yielding a single +global formula with no club input: + + spin_rpm = 170 · ball_speed_mph · sin(LA)^1.2 + +Validation against the 2026-06-08 TrackMan truth set: +- TrackMan inputs (physics ceiling): 10.5% median error across the + bag — better than an oracle per-club median table (12.0%) +- Leave-one-club-out blind: 11.3% median, 97/131 within 25%; the + fitted (coefficient, exponent) were stable across all folds +- Simulated with production launch-angle noise (2.5° MAE two-ray): + ~21% median expected live + +Caveats: calibrated on one player/session (range balls). Attack-angle +style shifts spin loft at a fixed launch angle, so cross-player error +is likely a few points worse. Accuracy degrades at low launch angles +(cot(LA) error amplification) — the same clubs where measured launch +angle is weakest. +""" + +import math +from typing import Optional + +# Fitted on the 2026-06-08 TrackMan session (131 shots, PW-3h); +# leave-one-club-out stable at (170, 1.2) across all folds. +SPIN_COEFF_RPM_PER_MPH = 170.0 +SPIN_LA_EXPONENT = 1.2 + +# Outside these bounds the kinematic model is extrapolating into +# regimes it was never calibrated on (top-spinned thins, pop-ups). +MIN_LAUNCH_ANGLE_DEG = 2.0 +MAX_LAUNCH_ANGLE_DEG = 60.0 + +# Physical ceiling — beyond fresh-groove lob wedge territory. +MAX_SPIN_RPM = 13000.0 + + +def calculated_spin_rpm( + ball_speed_mph: float, + launch_angle_deg: float, +) -> Optional[float]: + """Kinematic spin estimate from ball speed and vertical launch angle. + + Use the true (cosine-corrected) ball speed when available — the + model was calibrated against TrackMan ball speed. + + Returns None when inputs are missing or outside the calibrated + range; callers should fall back to club-typical spin. + """ + if ball_speed_mph is None or launch_angle_deg is None: + return None + if ball_speed_mph <= 0: + return None + if not MIN_LAUNCH_ANGLE_DEG <= launch_angle_deg <= MAX_LAUNCH_ANGLE_DEG: + return None + + sin_la = math.sin(math.radians(launch_angle_deg)) + spin = SPIN_COEFF_RPM_PER_MPH * ball_speed_mph * sin_la**SPIN_LA_EXPONENT + return float(min(spin, MAX_SPIN_RPM)) diff --git a/tests/test_server.py b/tests/test_server.py index 0890093e9..3e94acb3e 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -2087,3 +2087,50 @@ def fail_simulate(*args, **kwargs): assert shot.carry_spin_adjusted is not None assert shot.carry_spin_adjusted > 0 + + +class TestApplyCalculatedSpin: + """Tests for the --calculated-spin shot rewrite.""" + + def _shot(self, la=18.0, la_source="radar", ball_speed=115.0, spin=6800.0): + return Shot( + ball_speed_mph=ball_speed, + timestamp=datetime.now(), + club=ClubType.IRON_7, + launch_angle_vertical=la, + launch_angle_vertical_source=la_source, + spin_rpm=spin, + spin_confidence=0.3, + spin_rejection_reason="SNR too low", + ) + + def test_rewrites_spin_when_launch_angle_measured(self): + shot = self._shot() + assert server_module._apply_calculated_spin(shot) is True + # 170 * 115 * sin(18deg)^1.2 ~= 4800 rpm + assert 4500 < shot.spin_rpm < 5100 + assert shot.spin_rpm_measured == 6800.0 + assert shot.spin_source == "calculated" + assert shot.spin_confidence == pytest.approx(0.7) + assert shot.spin_rejection_reason is None + + def test_untouched_when_launch_angle_estimated(self): + shot = self._shot(la_source="estimated") + assert server_module._apply_calculated_spin(shot) is False + assert shot.spin_rpm == 6800.0 + assert shot.spin_source is None + + def test_untouched_when_no_launch_angle(self): + shot = self._shot(la=None) + assert server_module._apply_calculated_spin(shot) is False + assert shot.spin_rpm == 6800.0 + + def test_untouched_when_launch_angle_outside_model_range(self): + shot = self._shot(la=1.0) + assert server_module._apply_calculated_spin(shot) is False + assert shot.spin_rpm == 6800.0 + + def test_camera_launch_angle_accepted(self): + shot = self._shot(la_source="camera") + assert server_module._apply_calculated_spin(shot) is True + assert shot.spin_source == "calculated" diff --git a/tests/test_speed_correction.py b/tests/test_speed_correction.py new file mode 100644 index 000000000..e339b5262 --- /dev/null +++ b/tests/test_speed_correction.py @@ -0,0 +1,55 @@ +"""Tests for the ball-speed cosine correction.""" + +import pytest + +from openflight.speed_correction import correct_ball_speed, radial_speed_factor + +D_FT = 5.0 +H_FT = -4.0 / 12.0 + + +class TestRadialSpeedFactor: + def test_factor_below_one_for_lofted_launch(self): + # A ball departing upward always reads slow on a low radar + f = radial_speed_factor(19.0, 110.0, D_FT, H_FT) + assert 0.95 < f < 1.0 + + def test_typical_iron_compression_matches_observed_bias(self): + # The validated datasets showed ~2.1-2.6 mph of compression at + # iron speeds (~2-2.5% of ball speed) + f = radial_speed_factor(19.0, 108.0, D_FT, H_FT) + compression_mph = 108.0 * (1.0 - f) + assert 1.5 < compression_mph < 3.5 + + def test_higher_launch_compresses_more(self): + f_wedge = radial_speed_factor(30.0, 90.0, D_FT, H_FT) + f_iron = radial_speed_factor(17.0, 110.0, D_FT, H_FT) + f_driver = radial_speed_factor(11.0, 150.0, D_FT, H_FT) + assert f_wedge < f_iron < f_driver + + def test_farther_tee_compresses_more(self): + # LOS flattens with distance while the velocity stays pitched up + assert radial_speed_factor(18.0, 110.0, 6.5, H_FT) < radial_speed_factor( + 18.0, 110.0, 5.0, H_FT + ) + + def test_zero_launch_is_nearly_uncorrected(self): + f = radial_speed_factor(0.0, 110.0, D_FT, H_FT) + assert f > 0.995 + + def test_degenerate_inputs_clamp(self): + assert radial_speed_factor(19.0, 0.0, D_FT, H_FT) == 1.0 + assert 0.5 <= radial_speed_factor(44.0, 60.0, D_FT, H_FT) <= 1.0 + + +class TestCorrectBallSpeed: + def test_correction_raises_speed(self): + corrected = correct_ball_speed(108.0, 19.0, D_FT, H_FT) + assert corrected > 108.0 + assert corrected == pytest.approx(110.3, abs=0.8) + + def test_roundtrip_consistency(self): + # Correcting then re-deriving the radial reading lands back + true_speed = correct_ball_speed(108.0, 19.0, D_FT, H_FT) + radial = true_speed * radial_speed_factor(19.0, true_speed, D_FT, H_FT) + assert radial == pytest.approx(108.0, abs=0.15) diff --git a/tests/test_spin_estimate.py b/tests/test_spin_estimate.py new file mode 100644 index 000000000..d46898cac --- /dev/null +++ b/tests/test_spin_estimate.py @@ -0,0 +1,50 @@ +"""Tests for the kinematic calculated-spin model.""" + +import math + +import pytest + +from openflight.spin_estimate import ( + MAX_SPIN_RPM, + SPIN_COEFF_RPM_PER_MPH, + SPIN_LA_EXPONENT, + calculated_spin_rpm, +) + + +class TestCalculatedSpin: + def test_seven_iron_regime(self): + """115 mph at 17.8 deg (TrackMan 7i medians) lands in 7i spin range.""" + spin = calculated_spin_rpm(115.0, 17.8) + assert spin == pytest.approx( + SPIN_COEFF_RPM_PER_MPH * 115.0 * math.sin(math.radians(17.8)) ** SPIN_LA_EXPONENT + ) + assert 3500 <= spin <= 5500 + + def test_wedge_spins_more_than_long_iron(self): + """At realistic speed/LA pairs, wedges out-spin long irons.""" + wedge = calculated_spin_rpm(92.0, 28.0) + long_iron = calculated_spin_rpm(130.0, 12.0) + assert wedge > long_iron + + def test_monotonic_in_launch_angle(self): + spins = [calculated_spin_rpm(110.0, la) for la in (10, 15, 20, 25, 30)] + assert spins == sorted(spins) + + def test_monotonic_in_ball_speed(self): + spins = [calculated_spin_rpm(v, 20.0) for v in (80, 100, 120, 140)] + assert spins == sorted(spins) + + def test_capped_at_physical_maximum(self): + assert calculated_spin_rpm(200.0, 55.0) == MAX_SPIN_RPM + + def test_none_outside_calibrated_launch_range(self): + assert calculated_spin_rpm(110.0, 1.0) is None + assert calculated_spin_rpm(110.0, -5.0) is None + assert calculated_spin_rpm(110.0, 65.0) is None + + def test_none_for_missing_or_invalid_inputs(self): + assert calculated_spin_rpm(None, 20.0) is None + assert calculated_spin_rpm(110.0, None) is None + assert calculated_spin_rpm(0.0, 20.0) is None + assert calculated_spin_rpm(-10.0, 20.0) is None From 51d62067500d6640cdcd8dae20af05b39375507d Mon Sep 17 00:00:00 2001 From: John Pacino Date: Tue, 16 Jun 2026 00:19:23 -0400 Subject: [PATCH 29/29] feat(ui): show spin provenance (estimated/radar) on the shot display The Spin Rate card shows an "estimated" subtext when spin came from the calculated model (spin_source === 'calculated') and "radar" when measured, mirroring the H. Launch card's source subtext. Adds spin_source to the Shot type (and the DisplayMode test mock). Co-Authored-By: Claude Opus 4.8 (1M context) --- ui/src/components/DisplayMode.test.tsx | 1 + ui/src/components/ShotDisplay.tsx | 7 +++++++ ui/src/types/shot.ts | 1 + 3 files changed, 9 insertions(+) diff --git a/ui/src/components/DisplayMode.test.tsx b/ui/src/components/DisplayMode.test.tsx index cab8891ec..3edbe1522 100644 --- a/ui/src/components/DisplayMode.test.tsx +++ b/ui/src/components/DisplayMode.test.tsx @@ -32,6 +32,7 @@ const shot: Shot = { spin_rpm: 2450, spin_confidence: 0.8, spin_quality: 'high', + spin_source: 'calculated', carry_spin_adjusted: 261, }; diff --git a/ui/src/components/ShotDisplay.tsx b/ui/src/components/ShotDisplay.tsx index d43f889f9..b08e4ed2d 100644 --- a/ui/src/components/ShotDisplay.tsx +++ b/ui/src/components/ShotDisplay.tsx @@ -234,6 +234,13 @@ export function ShotDisplay({ shot, animate = false }: ShotDisplayProps) { value={hasSpin ? formatSpinRpm(shot.spin_rpm!) : '—'} unit={hasSpin ? 'rpm' : undefined} label="Spin Rate" + subtext={ + hasSpin && shot.spin_source + ? shot.spin_source === 'calculated' + ? 'estimated' + : 'radar' + : undefined + } variant="spin" confidence={hasSpin ? shot.spin_quality : null} /> diff --git a/ui/src/types/shot.ts b/ui/src/types/shot.ts index adf575712..bb0deec61 100644 --- a/ui/src/types/shot.ts +++ b/ui/src/types/shot.ts @@ -19,6 +19,7 @@ export interface Shot { spin_rpm: number | null; spin_confidence: number | null; spin_quality: 'high' | 'medium' | 'low' | null; + spin_source: 'measured' | 'calculated' | null; carry_spin_adjusted: number | null; }