From 02a2f43e44659316811e9ce1677ef022ffcf4a36 Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Fri, 2 Oct 2026 09:39:07 -0500 Subject: [PATCH 1/4] docs, comments: describe downstream consumers by capability Reword docs, docstrings and comments that pointed at a specific downstream consumer so they describe the extension seams by what they do: the generic ext_force hook, the route_cost_fn seam, and the second layer of the nav-stats design that an extension uses to add its own domain-specific metrics. The base scorecard, selection and eval are described as domain-neutral. The scope note in the PyPI roadmap and the .cvcnav home question in the C++ port roadmap are simplified the same way. The material palette is now documented as positional with the public cvc::nav material_id enum and kNumMaterials, and its scorecard reference names cvc::nav::nav_scorecard. The scorecard reader's docstrings and comments (scorecard, selection, test_scorecard) name the C++ producer as cvc::nav::nav_scorecard::to_json(), whose keys match NavScorecard.to_dict field for field. material_raster: the provenance string now matches the C++ cvc::nav::material_raster::to_json value byte for byte. The rest of the document is not byte-identical, because the two writers format floats (bounds, mu/risk) differently; the comment says so. test_scorecard: the partial-reader test uses a generic extension sub-object as its ignored key and is renamed accordingly; its assertions are unchanged. test_scorecard_json_keys also changes the literal in its absent-key guard from "rf" to "ext". to_dict emits neither key, so the guard is equally weak before and after; asserting the exact base key set is left as a follow-up. grl-snam-weights: PROVENANCE.md and the recipe description say "geometry only". PROVENANCE.md ships under share/, so cvc_revision is bumped to 2 as the recipe requires; the weights are unchanged. --- .github/workflows/publish-weights-cvc.yml | 8 +++---- README.md | 2 +- .../grl-snam-weights/payload/PROVENANCE.md | 4 ++-- cvcpkg/recipes/grl-snam-weights/recipe.yaml | 4 ++-- docs/CVCNAV_CPP_PORT_ROADMAP.md | 2 +- docs/NAV_STATS.md | 16 ++++++++------ docs/VEHICLE_REFINEMENTS.md | 2 +- docs/cvc-nav-and-grl-snam-guide.md | 5 ++--- docs/pypi-publishing-roadmap.md | 5 ++--- grl_snam/__init__.py | 4 ++-- grl_snam/material_palette.py | 11 +++++----- grl_snam/nav.py | 2 +- grl_snam/planner.py | 2 +- grl_snam/scenario.py | 4 ++-- grl_snam/scorecard.py | 22 +++++++++---------- grl_snam/sdf_nav.py | 6 ++--- grl_snam/selection.py | 9 ++++---- grl_snam/selftest.py | 4 ++-- grl_snam/tools/austin.py | 2 +- grl_snam/tools/material_raster.py | 11 ++++++---- grl_snam/tools/scorecard_eval.py | 8 +++---- tests/test_ext_force.py | 5 +++-- tests/test_nav_ext_force.py | 2 +- tests/test_nav_native_drive.py | 2 +- tests/test_scorecard.py | 17 +++++++------- 25 files changed, 83 insertions(+), 76 deletions(-) diff --git a/.github/workflows/publish-weights-cvc.yml b/.github/workflows/publish-weights-cvc.yml index e7f9d96..723c4c0 100644 --- a/.github/workflows/publish-weights-cvc.yml +++ b/.github/workflows/publish-weights-cvc.yml @@ -1,15 +1,15 @@ name: publish-weights-cvc # Publish the grl-snam-weights data package (base GRL-SNAM nav coefficients — the -# research tier, NO RF-comm forces) to the PUBLIC `cvc` org on cvcpkg.org, using +# geometry-only research tier) to the PUBLIC `cvc` org on cvcpkg.org, using # this repo's CVCPKG_TOKEN (the cvc-org publisher) — the same token that ships the # grl-snam-cpXXX columns. Kept separate from publish-cvcpkg.yml (the interpreter # matrix) because the weights are a single noarch data package with no py column. # # Org packages belong in their own repo's recipes, never in libcvc-deps: this -# recipe lives here, in cvcpkg/recipes/grl-snam-weights. The DBG comm-aware weights -# are the SEPARATE private cvc-dbg-weights on the utdbg org — the research/ -# development seam is the org boundary; this workflow only ever targets --org cvc. +# recipe lives here, in cvcpkg/recipes/grl-snam-weights. Weights that a downstream +# application trains with its own extra force terms are published by that project +# to its own org; this workflow only ever targets --org cvc. # # workflow_dispatch: dry_run=true (default) packs only; dry_run=false publishes. on: diff --git a/README.md b/README.md index 534b6e7..006f57d 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ GRL-SNAM (not the other way round) and keep their dataset-specific code and mode of this general library. Such an extension reuses exactly the pieces shown above — the differentiable surrogate, the coefficient network, and the `pycvc`/`pycvc_gl` scene bindings — and versions its own additions in its own repository. This library stays -general; the extensions stay separate and private. +general; the extensions stay separate. ## Repository Structure diff --git a/cvcpkg/recipes/grl-snam-weights/payload/PROVENANCE.md b/cvcpkg/recipes/grl-snam-weights/payload/PROVENANCE.md index de6dd9b..ef24dcd 100644 --- a/cvcpkg/recipes/grl-snam-weights/payload/PROVENANCE.md +++ b/cvcpkg/recipes/grl-snam-weights/payload/PROVENANCE.md @@ -1,6 +1,6 @@ # grl-snam-weights — base GRL-SNAM nav coefficient provenance -Base GRL-SNAM navigation coefficients (no RF-comm forces) for the differentiable +Base GRL-SNAM navigation coefficients (geometry only) for the differentiable SDF navigator's `CoefMLP` — predicts `(alpha, beta, gamma)`. - `coef_sdf.cvcnav` — native `CVNV` blob for the C++ `cvc::nav` forward. @@ -9,7 +9,7 @@ SDF navigator's `CoefMLP` — predicts `(alpha, beta, gamma)`. ## How it was trained (v1.0.0) - **Pipeline:** `grl-snam train ` (self-supervised SDF-coefficient - training — geometry only, NO comm-force term) → `grl_snam.tools.coef_export` + training — geometry only, NO external-force term) → `grl_snam.tools.coef_export` (checkpoint → `CVNV`). - **Data:** the `austin_south` navigation SDF (`nav_sdf.npz`, 1024×1024 φ field over the public Austin geometry). diff --git a/cvcpkg/recipes/grl-snam-weights/recipe.yaml b/cvcpkg/recipes/grl-snam-weights/recipe.yaml index f4d26b5..a590518 100644 --- a/cvcpkg/recipes/grl-snam-weights/recipe.yaml +++ b/cvcpkg/recipes/grl-snam-weights/recipe.yaml @@ -6,7 +6,7 @@ recipe: # BUMP cvc_revision on ANY change to the staged bytes (a new checkpoint/blob, or # an edited PROVENANCE.md that ships under share/) — a same-revision re-push # silently no-ops against the already-published artifact. - cvc_revision: 1 + cvc_revision: 2 kind: data maintainer: "cvcpkg group" maintainer_email: "info@cvcpkg.org" @@ -14,7 +14,7 @@ recipe: license: MIT tags: [data, grl-snam, weights, nav] description: >- - Pretrained base GRL-SNAM navigation coefficients (no RF-comm forces): the + Pretrained base GRL-SNAM navigation coefficients (geometry only): the self-supervised SDF CoefMLP that predicts (alpha, beta, gamma) for the differentiable SDF navigator. Ships BOTH the native CVNV blob (coef_sdf.cvcnav, for the C++ cvc::nav forward) and the torch checkpoint diff --git a/docs/CVCNAV_CPP_PORT_ROADMAP.md b/docs/CVCNAV_CPP_PORT_ROADMAP.md index 3d9dfd6..21e71c7 100644 --- a/docs/CVCNAV_CPP_PORT_ROADMAP.md +++ b/docs/CVCNAV_CPP_PORT_ROADMAP.md @@ -361,7 +361,7 @@ def drive_enabled(): - **Deployment belief mode:** does a C++ renderer/game-engine host need clustered/private belief, or is **shared (M=1)** the whole deployment path? Shared is the thousands-of-agents target; if only shared, `map_id` is all-zeros and the M>1 COW/rebuild cost is untested-in-anger. Confirm before P6 sizing. - **`nsub` at deploy** (`meta.get("nsub",1)`): if always 1, the golden and gtests can pin `nsub=1`; if >1, the multi-substep transcendental accumulation must be in the golden. - **Whole-drive tolerance/horizon sign-off:** the proposed short-horizon `1e-3` normalized, `<0.5%` flip budget are from the existing `Squad(batched_drive)` 5e-3 tier and need empirical confirmation across all six stories and multiple seeds in P3/P6 — this is the single number that gates P6, and it is the top fidelity risk. -- **Canonical `.cvcnav` home + provenance policy:** where the blessed weights live (libcvc test-data vs pycvc-published vs per-deployment bundle) (kept out of public repos where required); and whether the provenance trailer is required for an audit trail. +- **Canonical `.cvcnav` home + provenance policy:** where the blessed weights live (libcvc test-data vs pycvc-published vs per-deployment bundle); and whether the provenance trailer is required for an audit trail. Files to create are listed in §2; the two files to edit are `/home/joe/src/cvc/wt-libcvc-nav/src/cvc/CMakeLists.txt` (add headers ~L88, sources ~L171 next to `nav/grid_nav.cpp`) and `/home/joe/src/cvc/wt-libcvc-nav/bindings/pycvc/pycvc_nav.i` (append the marshalling), plus the new grl-snam `coef_export.py`, `nav_native.py` additions, and the parity tests under `/home/joe/src/cvc/wt-grl-snam-nav/tests/`. --- diff --git a/docs/NAV_STATS.md b/docs/NAV_STATS.md index b10d199..d7b1a15 100644 --- a/docs/NAV_STATS.md +++ b/docs/NAV_STATS.md @@ -1,9 +1,10 @@ # Navigation statistics & the base-policy scorecard -grl-snam is the **base (RF-free)** owner of the two-layer nav-stats design: per-vehicle + per-episode -navigation telemetry and a corpus scorecard that ranks base-policy checkpoints. It is field-mirrored -by the C++ `cvc::nav::nav_stats` (libcvc `docs/NAV_STATS.md`) — the same schema and the same -hand-computed numbers — and the RF/comms layer (grl_snam_dbg) extends it, joined by vehicle index. +grl-snam is the **base (domain-neutral)** owner of the two-layer nav-stats design: per-vehicle + +per-episode navigation telemetry and a corpus scorecard that ranks base-policy checkpoints. It is +field-mirrored by the C++ `cvc::nav::nav_stats` (libcvc `docs/NAV_STATS.md`) — the same schema and +the same hand-computed numbers. The second layer is an extension seam: a downstream layer that needs +domain-specific metrics keeps them in its own record, joined to this one by vehicle index. ## The schema (`grl_snam.metrics`) @@ -36,14 +37,15 @@ All three collect the **same** base row, so a checkpoint scores identically whic `EpisodeStats.from_nav_stats(vehicles, straights_m=, arrival_times_s=, veh_contacts=, min_sep_m=)` reduces a list of per-vehicle `NavStats` (plus the fields `NavStats` doesn't carry) into one episode record; `aggregate_nav(episodes, checkpoint)` reduces a corpus into a single `NavScorecard` — the -RF-free fitness row (arrival, economy, safety, material). Mirrors C++ `cvc::nav::nav_scorecard`. +domain-neutral fitness row (arrival, economy, safety, material). Mirrors C++ +`cvc::nav::nav_scorecard`. ## Material & terrain-risk buckets The scorecard's material dimension is **discrete**, mirroring C++ `veh_nav_stats`: each vehicle's `NavStats` carries `time_over_material_s[13]` and `dist_over_material_m[13]`, indexed by the shared 13-class palette (`grl_snam.material_palette`, positional with `cvc::nav::kNumMaterials` and the -`cvc::dbg` `MATERIAL_TABLE`). `aggregate_nav` reduces the time buckets into +`cvc::nav` `material_id` enum). `aggregate_nav` reduces the time buckets into `NavScorecard.material_time_share[13]`, and `terrain_risk_share(...)` sums the traversable terrain-risk classes (foliage/soil/water/rock) into the single **"time in terrain-risk areas"** number the eval CLI prints as `risk-time%`. @@ -122,7 +124,7 @@ the floor keeps coverage everywhere. Magnitude scales with `--curriculum-eps`/bi ## Parity `tests/test_scorecard.py` pins the reducer to the SAME hand-computed corpus as libcvc's -`nav_stats_test.cpp` and cvcdbg's `tests/nav_stats_test.cpp` (the shared-schema contract); +`nav_stats_test.cpp` (the shared-schema contract); `tests/test_swarm.py` checks the `Swarm` collector against N serial `SdfNavigator`s to float32; `tests/test_material_buckets.py` hand-computes the material buckets (the time/dist-per-id contract mirrored from `nav_stats_test.cpp`), the id-raster sampler, and the byte-identical no-material default; diff --git a/docs/VEHICLE_REFINEMENTS.md b/docs/VEHICLE_REFINEMENTS.md index a8fc700..9c879bf 100644 --- a/docs/VEHICLE_REFINEMENTS.md +++ b/docs/VEHICLE_REFINEMENTS.md @@ -433,7 +433,7 @@ binding. That is deliberate: a green tick from an absent feature reads as coverage this repo does not have. Before pushing, run the linters CI runs — they are not installable here -(`black`/`ruff` are absent from `dbg-deps` and from the cvcpkg catalog for this +(`black`/`ruff` are absent from the local deps prefix and from the cvcpkg catalog for this platform tuple), so use a throwaway venv: ```bash diff --git a/docs/cvc-nav-and-grl-snam-guide.md b/docs/cvc-nav-and-grl-snam-guide.md index 100fa6c..731e3aa 100644 --- a/docs/cvc-nav-and-grl-snam-guide.md +++ b/docs/cvc-nav-and-grl-snam-guide.md @@ -736,9 +736,8 @@ cvcpkg install grl-snam-weights --prefix ./nav-env > on the free-space rollout surrogate currently *regresses* versus the hand-tuned > `(1,3,4)` basin. Use this package to exercise the pipeline and as a starting > checkpoint; keep the seed unless a longer run beats it on your own reach eval. -> The **RF-comm-aware** DBG policy is a *separate, private* package -> (`cvc-dbg-weights` on the `utdbg` org) — the research/development seam is kept at -> the org boundary, never mixed into this public package. +> A policy that a downstream application trains with its own extra force terms is +> published by that project, never mixed into this package. --- diff --git a/docs/pypi-publishing-roadmap.md b/docs/pypi-publishing-roadmap.md index ccb73ed..5ff486b 100644 --- a/docs/pypi-publishing-roadmap.md +++ b/docs/pypi-publishing-roadmap.md @@ -4,9 +4,8 @@ current Python packaging standards, so it installs with a plain `pip install grl-snam`. -> **Scope.** This covers the public `grl-snam` package only. Any sensitive -> downstream extensions are **not** publicly distributed; they are out of scope -> here and must never be published. Once `grl-snam` is on PyPI, downstreams can +> **Scope.** This covers the public `grl-snam` package only; downstream +> extensions are out of scope here. Once `grl-snam` is on PyPI, downstreams can > depend on it by version instead of a GitHub URL. --- diff --git a/grl_snam/__init__.py b/grl_snam/__init__.py index 5cbbc90..93851eb 100644 --- a/grl_snam/__init__.py +++ b/grl_snam/__init__.py @@ -4,8 +4,8 @@ (`train_coef_energy.py`, `surrogate_robust.py`, `eval_coef_energy.py`, `src/utils/`, `scripts/`, `experiments/`). The flat layout is preserved for backward compatibility with the original research code; this package -exposes a stable, importable API for downstream consumers (e.g. a -private downstream extension). +exposes a stable, importable API for downstream consumers (e.g. an +application-specific extension layer). Typical use:: diff --git a/grl_snam/material_palette.py b/grl_snam/material_palette.py index 628f3f4..80aea9c 100644 --- a/grl_snam/material_palette.py +++ b/grl_snam/material_palette.py @@ -3,20 +3,21 @@ This is the SINGLE source of the palette shared across the stack: it mirrors ``cvc::nav::kNumMaterials`` (libcvc ``inc/cvc/nav/nav_stats.h``) and is positional -with the ``cvc::dbg`` ``MATERIAL_TABLE`` (cvcdbg ``src/cvc/dbg/channel.cpp``), so a -material id means the same class in the C++ collector, the DBG RF layer, and here. -Keep the ORDER byte-for-byte with that table — the ids are the wire format. +with the ``cvc::nav`` ``material_id`` enum (libcvc ``inc/cvc/nav/material_raster.h``), +so a material id means the same class in the C++ collector, in any downstream table +keyed by these ids, and here. Keep the ORDER byte-for-byte with that enum — the ids +are the wire format. The discrete buckets (``time_over_material_s`` / ``dist_over_material_m``) are the parity field with the C++ scorecard. "Time in terrain-risk areas" is DERIVED from them — :func:`terrain_risk_share` sums the traversable outdoor classes that carry mobility risk — rather than stored as a separate schema field, so the scorecard -stays field-for-field with ``cvc::dbg::nav_scorecard``. +stays field-for-field with ``cvc::nav::nav_scorecard``. """ from __future__ import annotations -# Positional — index == material id. MUST match cvc::dbg MATERIAL_TABLE order. +# Positional — index == material id. MUST match the cvc::nav material_id enum order. MATERIALS = ( "reinforced_concrete", # 0 "brick", # 1 diff --git a/grl_snam/nav.py b/grl_snam/nav.py index 18ceb67..07990b1 100644 --- a/grl_snam/nav.py +++ b/grl_snam/nav.py @@ -92,7 +92,7 @@ def __init__( # ``o[B,2] -> [B,2]`` returning an extra acceleration (rollout/normalized # frame) summed alongside F_bar/F_goal each substep. Unlike carrot_bias_fn # (which nudges the pure-pursuit carrot), this enters the force law itself - # — the seam the DBG comm force uses as a genuine force term. ``None`` + # — the seam an application-specific force uses as a genuine force term. ``None`` # (default) is additively inert and bit-for-bit unchanged. self.ext_force_fn = None # Optional material-aware runtime (grl_snam.material.MaterialRuntime). diff --git a/grl_snam/planner.py b/grl_snam/planner.py index 940dd45..aead1a9 100644 --- a/grl_snam/planner.py +++ b/grl_snam/planner.py @@ -97,7 +97,7 @@ def astar(occ: np.ndarray, start, goal, cost: np.ndarray | None = None): parameter existed — the golden traces stay valid. The surcharge is domain-agnostic on purpose: it is "this cell is expensive", - not "this cell is dangerous/jammed/steep". Whoever builds the raster owns + not "this cell is dangerous/slow/steep". Whoever builds the raster owns the meaning. Keep values modest — a surcharge much larger than the map's diameter makes the heuristic wildly inadmissible and A* degenerates toward Dijkstra, exploring the whole grid for a route it was always going to take. diff --git a/grl_snam/scenario.py b/grl_snam/scenario.py index a87e8b8..c3cda3d 100644 --- a/grl_snam/scenario.py +++ b/grl_snam/scenario.py @@ -444,8 +444,8 @@ def _route_cost(self) -> np.ndarray | None: (the documented downstream seam — never clobbered), composed additively with the material cost raster when a MaterialGrid is attached. - route_cost_fn is the seam a downstream package (e.g. an RF-aware - planner) plugs a cost surface into; base GRL-SNAM never interprets it. + route_cost_fn is the seam a downstream package (e.g. an application- + specific planner) plugs a cost surface into; base GRL-SNAM never interprets it. """ cost = self.route_cost_fn() if self.route_cost_fn is not None else None if self._material is not None: diff --git a/grl_snam/scorecard.py b/grl_snam/scorecard.py index aad3f5e..9cc687d 100644 --- a/grl_snam/scorecard.py +++ b/grl_snam/scorecard.py @@ -1,10 +1,10 @@ """Base navigation SCORECARD — aggregate a corpus of episodes into one fitness row. This is the Python twin of the C++ ``cvc::nav::nav_scorecard`` (transfix/libcvc -``inc/cvc/nav/nav_stats.h``): the RF-free nav-fitness row grl-snam uses to rank its -OWN base-policy checkpoints over a scene corpus. The DBG campaign layers an -``rf_scorecard`` on top; the base half here is shared, so a grl-snam training run and -a cvcdbg ``dbg_arrival_check3 --episodes`` run report identical base numbers (the +``inc/cvc/nav/nav_stats.h``): the domain-neutral nav-fitness row grl-snam uses to rank +its OWN base-policy checkpoints over a scene corpus. A downstream extension can layer its +own domain-specific scorecard on top; the base half here is shared, so a grl-snam training +run and a C++ ``cvc::nav`` collector run report identical base numbers (the aggregation logic + JSON keys are field-for-field the same, and ``tests/test_scorecard`` pins them to the same hand-computed values the C++ ``nav_stats_test`` uses). @@ -20,7 +20,7 @@ import math from dataclasses import dataclass, field -from .material_palette import NUM_MATERIALS # 13 == cvc::dbg kNumMaterials (channel.cpp table) +from .material_palette import NUM_MATERIALS # 13 == cvc::nav::kNumMaterials @dataclass @@ -220,15 +220,15 @@ def to_json(self) -> str: @classmethod def from_dict(cls, d: dict) -> NavScorecard: """Parse a scorecard dict back into a ``NavScorecard`` — the inverse of :meth:`to_dict`, and - the reader for the C++ ``cvc::nav::scorecard_json`` output (the keys are field-for-field the - same, so a ``dbg_arrival_check3 --episodes`` / ``scorecard_json`` row loads directly). This is + the reader for the C++ ``cvc::nav::nav_scorecard::to_json()`` output (the keys are + field-for-field the same, so a recorded C++ scorecard row loads directly). This is the plumbing a selection/loss stage reads a recorded corpus fitness from; nothing consumes it yet (Track 4 Phase 1+ wires it into training). Tolerant of a partial producer: any missing key keeps the dataclass default, so an older or - newer writer (or one that omits zero fields) still loads. An ``rf`` sub-object, if present - (the DBG ``scorecard_json`` embeds one), is ignored here — the base reader is RF-free; the DBG - side reads the RF scorecard separately. + newer writer (or one that omits zero fields) still loads. An extension sub-object, if + present (a downstream writer may embed its own record), is ignored here — the base reader + is domain-neutral; the extension reads its own sub-record separately. """ cov = d.get("mean_coverage") or {} return cls( @@ -274,7 +274,7 @@ def from_dict(cls, d: dict) -> NavScorecard: @classmethod def from_json(cls, s: str) -> NavScorecard: - """Parse a scorecard JSON string (``to_json`` / C++ ``scorecard_json``) into a + """Parse a scorecard JSON string (``to_json`` / C++ ``nav_scorecard::to_json()``) into a ``NavScorecard``. See :meth:`from_dict`.""" return cls.from_dict(json.loads(s)) diff --git a/grl_snam/sdf_nav.py b/grl_snam/sdf_nav.py index cce0d4a..280e2c7 100644 --- a/grl_snam/sdf_nav.py +++ b/grl_snam/sdf_nav.py @@ -424,7 +424,7 @@ def sdf_rollout( returning an extra force (accel units) at the current positions each substep, summed into the acceleration alongside ``F_bar``/``F_goal``/``F_mat`` — the physics-agnostic Python twin of ``cvc::nav``'s ``ext_force`` port. It carries - NO RF/comms vocabulary; a private consumer (DBG's comm force) supplies it. + NO domain vocabulary; the caller (e.g. an application-specific force) supplies it. ``None`` (the default) is additively inert and bit-for-bit unchanged.""" hdt = dt / nsub minclr = torch.full((o.shape[0],), 9.9, device=o.device) @@ -440,7 +440,7 @@ def sdf_rollout( a = F_bar + F_goal + F_mat - ga.unsqueeze(-1) * v else: a = F_bar + F_goal - ga.unsqueeze(-1) * v - if ext_force_fn is not None: # generic external force (e.g. DBG comm force) + if ext_force_fn is not None: # generic external force (caller-supplied) a = a + ext_force_fn(o) v = v + hdt * a sp = v.norm(dim=-1, keepdim=True) @@ -647,7 +647,7 @@ def bicycle_rollout( a_max_e, a_lat_e = a_max * _mu, a_lat_max * _mu F_goal = -be.unsqueeze(-1) * (o - goal) - # Generic external force (e.g. DBG comm force). Evaluated once and, like + # Generic external force (caller-supplied). Evaluated once and, like # F_mat, joins BOTH couplings — the longitudinal projection AND the # steering bias below. None (default) is additively inert. F_ext = ext_force_fn(o) if ext_force_fn is not None else None diff --git a/grl_snam/selection.py b/grl_snam/selection.py index c217ed5..0ddd1bd 100644 --- a/grl_snam/selection.py +++ b/grl_snam/selection.py @@ -4,7 +4,7 @@ This is deliberately separate from :mod:`grl_snam.scorecard` (which stays a pure schema + reducer): selection reads finished ``NavScorecard`` rows and never serializes back, so it can never perturb the C++<->Python parity surface (the composite is a free function, NOT a scorecard field). It also stays -RF-free — the DBG campaign composes an RF fitness on top of ``composite_fitness`` in ``grl_snam_dbg``; +domain-neutral — a downstream extension can compose its own fitness on top of ``composite_fitness``; nothing here touches the loss or the rollout (that is Phase 2). Typical use — rank recorded corpus rows (the C++/native collector fills the formation/coverage/grip @@ -109,9 +109,10 @@ def select_best( def _main(argv: list[str] | None = None) -> int: """`python -m grl_snam.selection card1.json card2.json [--weights '{"w_form_arrival":1.0}']` — - load recorded scorecard rows (cvc::nav ``scorecard_json`` / ``NavScorecard.to_json``) and print - them best-first by composite fitness. This is the offline SELECTION entry point; the recorded - (``from_json``) row is where the formation/coverage/grip fields are actually non-zero.""" + load recorded scorecard rows (C++ ``cvc::nav::nav_scorecard::to_json()`` / + ``NavScorecard.to_json``) and print them best-first by composite fitness. This is the offline + SELECTION entry point; the recorded (``from_json``) row is where the formation/coverage/grip + fields are actually non-zero.""" import argparse ap = argparse.ArgumentParser( diff --git a/grl_snam/selftest.py b/grl_snam/selftest.py index fcae4e2..b886c6a 100644 --- a/grl_snam/selftest.py +++ b/grl_snam/selftest.py @@ -13,8 +13,8 @@ 4. SHAPES: (o, v, min_clear) come back as (B,2), (B,2), (B,). This is the correctness counterpart to the visual grl_snam_lab demo. It needs -torch (a declared GRL-SNAM dependency); the applied full-gym end-to-end demo lives -in a separate, private downstream project. +torch (a declared GRL-SNAM dependency); applied end-to-end demos live in +downstream projects. Run: ``grl-snam-selftest`` (installed console script) or ``python -m grl_snam.selftest``. """ diff --git a/grl_snam/tools/austin.py b/grl_snam/tools/austin.py index 5313c9c..89e8b76 100644 --- a/grl_snam/tools/austin.py +++ b/grl_snam/tools/austin.py @@ -15,7 +15,7 @@ The bundle path is always supplied at runtime and never defaulted: the DATA is OpenStreetMap (ODbL) and SRTM (US public domain), so renders are publishable, -but the bundle itself is a local artifact of a private project and its path +but the bundle itself is a local artifact and its path does not belong in this repo. """ diff --git a/grl_snam/tools/material_raster.py b/grl_snam/tools/material_raster.py index 5947607..497a15e 100644 --- a/grl_snam/tools/material_raster.py +++ b/grl_snam/tools/material_raster.py @@ -15,9 +15,9 @@ learned model — reproducible and reviewable; tune :func:`classify` + the palette's ``GRIP_MU`` / ``TERRAIN_RISK`` for other biomes. -NOTE on the palette: it is an RF/building-material table (``reinforced_concrete`` etc. carry -penetration-loss dB) plus a few natural-terrain classes, with NO asphalt/road class. ``reinforced_ -concrete`` means building walls, not pavement; buildings are obstacles the convoy never drives +NOTE on the palette: it is a building-material table (``reinforced_concrete``, ``brick``, +``glass`` etc.) plus a few natural-terrain classes, with NO asphalt/road class. ``reinforced_ +concrete`` means building walls, not pavement; buildings are obstacles a vehicle never drives (tagged from the scene's building metadata), so this GROUND classifier only ever emits the drivable classes ``open_air`` / ``foliage`` / ``soil`` / ``water`` / ``rock``. """ @@ -103,7 +103,10 @@ def to_material_json(material_id: np.ndarray, bounds) -> dict: ids = sorted(int(v) for v in np.unique(material_id)) return { "schema": "cvc-scene-material/1", - "provenance": "scene land cover (grl-snam material-raster: masks if present, else satellite); ids = cvc::dbg MATERIAL_TABLE", + # This provenance value matches the C++ cvc::nav::material_raster::to_json byte for byte + # (libcvc src/cvc/nav/material_raster.cpp). The documents as a whole are not + # byte-identical: the two writers format floats (bounds, mu/risk) differently. + "provenance": "scene land cover (cvc::nav material_raster: masks if present, else satellite); ids = the 13-class material palette", "rows": rows, "cols": cols, "bounds": _bounds_dict(bounds), diff --git a/grl_snam/tools/scorecard_eval.py b/grl_snam/tools/scorecard_eval.py index cdc9325..744e2c7 100644 --- a/grl_snam/tools/scorecard_eval.py +++ b/grl_snam/tools/scorecard_eval.py @@ -1,13 +1,13 @@ -"""Base-policy scorecard eval — rank a CoefMLP checkpoint by its RF-free navigation +"""Base-policy scorecard eval — rank a CoefMLP checkpoint by its domain-neutral navigation fitness over a scene corpus. Runs the vectorized :class:`~grl_snam.swarm.Swarm` with its opt-in base nav_stats collector across a corpus of city scenes, reduces each episode to an :class:`~grl_snam.scorecard.EpisodeStats`, and aggregates them into one :class:`~grl_snam.scorecard.NavScorecard` — the single fitness row grl-snam uses to -rank its own base-policy checkpoints (arrival, economy, safety, material), RF-free. -This is the base half of the two-layer nav-stats design's training bridge (the DBG -campaign layers an ``rf_scorecard`` on top); the scorecard field-mirrors the C++ +rank its own base-policy checkpoints (arrival, economy, safety, material). +This is the base half of the two-layer nav-stats design's training bridge (a downstream +extension can layer its own scorecard on top); the scorecard field-mirrors the C++ ``cvc::nav::nav_scorecard`` and the native ``sim_world`` collector, so a checkpoint scores identically whichever path evaluates it. diff --git a/tests/test_ext_force.py b/tests/test_ext_force.py index 9a5fcf7..3f68a48 100644 --- a/tests/test_ext_force.py +++ b/tests/test_ext_force.py @@ -1,8 +1,9 @@ """ext_force_fn parity — the generic external-force channel in sdf_rollout and bicycle_rollout (the Python twin of cvc::nav's ext_force port). A ``None`` hook (the default) and a zero force are byte-identical to the plain rollout; a live -force bends the trace. This is what carries the DBG comm force once it moves off -the carrot bias: F_comm/F_jam sum with F_bar/F_goal in the integrator. +force bends the trace. This is what carries an application-specific force as a +genuine force term rather than a carrot bias: F_ext sums with F_bar/F_goal in the +integrator. """ from __future__ import annotations diff --git a/tests/test_nav_ext_force.py b/tests/test_nav_ext_force.py index 45b6ceb..79e8773 100644 --- a/tests/test_nav_ext_force.py +++ b/tests/test_nav_ext_force.py @@ -3,7 +3,7 @@ #66 added the generic ext_force_fn hook to sdf_rollout / bicycle_rollout; this pins that SdfNavigator threads it through step() correctly: a None or zero-force hook leaves every trace bit-for-bit unchanged (the byte-identical-off contract -the DBG comm-force cutover relies on), and a non-zero hook actually bends the +an external-force caller relies on), and a non-zero hook actually bends the path. """ diff --git a/tests/test_nav_native_drive.py b/tests/test_nav_native_drive.py index 85673a2..31b6eea 100644 --- a/tests/test_nav_native_drive.py +++ b/tests/test_nav_native_drive.py @@ -1,6 +1,6 @@ """SdfNavigator native drive dispatch (``GRL_SNAM_NAV_DRIVE=native``). -The per-agent navigator the DBG convoy drives (Squad -> Scenario -> +The per-agent navigator a Squad convoy drives (Squad -> Scenario -> ``SdfNavigator``) can optionally run the torch-free C++ fused drive (``nav_native.drive_step``) instead of the torch coef-net + bicycle rollout, keeping the carrot FSM in Python. This is the SdfNavigator assembly of the diff --git a/tests/test_scorecard.py b/tests/test_scorecard.py index d8c13eb..14ee963 100644 --- a/tests/test_scorecard.py +++ b/tests/test_scorecard.py @@ -76,7 +76,7 @@ def test_scorecard_json_keys(): "material_time_share", ): assert k in d - assert "rf" not in d # base-only shape (grl-snam-non-DBG) + assert "ext" not in d # base-only shape (no extension sub-object) def test_navstats_accumulates_turn_and_fuel(): @@ -248,13 +248,14 @@ def test_scorecard_json_has_track1_keys(): } -# --- Track 4 Phase 0: the scorecard reader (inverse of to_dict/to_json; loads C++ scorecard_json) --- +# --- Track 4 Phase 0: the scorecard reader (inverse of to_dict/to_json; loads C++ nav_scorecard) --- def test_scorecard_reader_round_trips(): # A fully-populated scorecard (all Track-1 field families non-default) survives to_dict->from_dict # and to_json->from_json unchanged. Since to_dict's keys are field-for-field the C++ - # cvc::nav::scorecard_json keys, this is also the guarantee that a recorded C++ scorecard row loads. + # cvc::nav::nav_scorecard::to_json() keys, this is also the guarantee that a recorded C++ + # scorecard row loads. e0 = EpisodeStats( success=True, makespan_s=20, @@ -292,11 +293,11 @@ def test_scorecard_reader_round_trips(): assert NavScorecard.from_json(sc.to_json()).to_dict() == d # JSON round-trip too -def test_scorecard_reader_tolerates_partial_and_ignores_rf(): - # A partial producer (missing keys) loads with dataclass defaults; an embedded "rf" sub-object - # (the DBG scorecard_json shape) is ignored by the base reader. +def test_scorecard_reader_tolerates_partial_and_ignores_extension(): + # A partial producer (missing keys) loads with dataclass defaults; an embedded extension + # sub-object (a downstream writer's own record) is ignored by the base reader. sc = NavScorecard.from_dict( - {"checkpoint": "x", "success_rate": 0.5, "rf": {"outage_rate": 0.9}} + {"checkpoint": "x", "success_rate": 0.5, "ext": {"extra_metric": 0.9}} ) assert sc.checkpoint == "x" assert math.isclose(sc.success_rate, 0.5) @@ -305,4 +306,4 @@ def test_scorecard_reader_tolerates_partial_and_ignores_rf(): assert math.isclose(sc.mean_coverage.phantom_frac, 0.0) # missing nested -> default # missing list field -> the length-NUM_MATERIALS zero default, NOT [] (so share[m] never IndexErrors) assert sc.material_time_share == [0.0] * NUM_MATERIALS - assert not hasattr(sc, "rf") # rf sub-object dropped, not smuggled onto the base row + assert not hasattr(sc, "ext") # extension sub-object dropped, not smuggled onto the base row From b4044879234f982e30cc76ba450b31c202b08ae9 Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Fri, 2 Oct 2026 09:39:31 -0500 Subject: [PATCH 2/4] docs: drop workstation-local paths from the roadmaps Replace absolute paths into one developer's checkouts (worktrees and a sibling clone) with repo-relative names: libcvc paths are given relative to the libcvc repository root, GRL-SNAM paths relative to this repo, and the material-fork cross-check says to point GRL_SNAM_MATERIAL_FORK at a clone rather than at a specific directory. --- docs/CVCNAV_CPP_PORT_ROADMAP.md | 10 +++++----- docs/CVCNAV_CUDA_ASSESSMENT.md | 6 +++--- docs/CVCNAV_MATERIAL_PORT_ROADMAP.md | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/CVCNAV_CPP_PORT_ROADMAP.md b/docs/CVCNAV_CPP_PORT_ROADMAP.md index 21e71c7..f0c0fc9 100644 --- a/docs/CVCNAV_CPP_PORT_ROADMAP.md +++ b/docs/CVCNAV_CPP_PORT_ROADMAP.md @@ -62,7 +62,7 @@ Boundary is drawn **exactly at the bilinear sample**. Everything upstream stays Namespace `cvc::nav`, raw-pointer SoA cores, `int num_threads` on every batch, all new TUs compiled under the existing discipline (**no `-ffast-math`, no `-ffp-contract=fast`**, float32 interior to track torch f32). **Name collision to avoid:** `cvc::nav::sdf_field` already exists in `grid_nav.h` (the *built* field). The sampler stack is named **`field_stack`**. -Files (all under `/home/joe/src/cvc/wt-libcvc-nav/`): +Files (all under the libcvc repository root): ``` inc/cvc/nav/detail/parallel.h NEW (extract parallel_for from grid_nav.cpp) @@ -270,14 +270,14 @@ arch descriptor (hashed): pack("bytes` walks the **live** `nn.Module` (activations discovered from `model.net`, `bias` buffer present), plus `serialize_checkpoint(pt_path)` that rebuilds a `CoefMLP` from a saved state_dict. `write_coef_mlp(model, path)`; CLI `python -m grl_snam.coef_export coef_sdf.pt coef_mlp.cvcnav`. Wire `write_coef_mlp` into `grl_snam/tools/train.py` and `grl_snam/tools/pipeline.py` so every checkpoint ships its deployable twin. +- **Exporter** `grl_snam/coef_export.py` (this repo): `serialize_coef_mlp(model)->bytes` walks the **live** `nn.Module` (activations discovered from `model.net`, `bias` buffer present), plus `serialize_checkpoint(pt_path)` that rebuilds a `CoefMLP` from a saved state_dict. `write_coef_mlp(model, path)`; CLI `python -m grl_snam.coef_export coef_sdf.pt coef_mlp.cvcnav`. Wire `write_coef_mlp` into `grl_snam/tools/train.py` and `grl_snam/tools/pipeline.py` so every checkpoint ships its deployable twin. - **In-memory path is the parity substrate:** the same `bytes` feed torch (its source) and `pycvc.NavCoefMLP(blob)` — no file round-trip in the tolerance test. --- ## 5. Python-green seam + migration order -**pycvc surface** (append to `/home/joe/src/cvc/wt-libcvc-nav/bindings/pycvc/pycvc_nav.i`, following the existing writable-borrow discipline — validate exact dtype/shape/contig, never coerce-copy an in-place buffer, `Py_BEGIN_ALLOW_THREADS` across compute): +**pycvc surface** (append to libcvc's `bindings/pycvc/pycvc_nav.i`, following the existing writable-borrow discipline — validate exact dtype/shape/contig, never coerce-copy an in-place buffer, `Py_BEGIN_ALLOW_THREADS` across compute): - Fine-grained (parity + optional acceleration): `nav_sdf_sample(field[M,3,H,W]f32, o[N,2]f32, plane[N]i32|None, mnx..S) -> (phi[N], nrm[N,2])`; `NavCoefMLP(bytes)` / `nav_coef_mlp_load(path)` with `.forward(feat[N,5])->(N,3)`; `nav_drive_step(field, plane|None, , mlp, veh scalars, integ, reach_tol, mnx..S, num_threads) -> None`. - Object surface (pure-C++ twin + full delegation): opaque `nav_sim_world_create(...)`, `_step`, `_snapshot(->dict of fresh numpy)`, `_retarget`, `_add_obstacle`, `_free`. @@ -355,7 +355,7 @@ def drive_enabled(): - **FSM source of truth:** port `swarm.py._plan_carrot` (ring-buffer form), **not** `nav.py._plan_carrot` (list-pop form). They compute `moved` against different history references; `swarm.py` is the vectorized reference the tests pin, and its integer state is exact. - **`allow_reverse`:** `bicycle_rollout`'s own default is `False`, but the deployment path (`SdfNavigator.VEHICLE_DEFAULTS`, inherited by `Swarm.veh`) is `True`. C++ `veh_params` default must be `true` (§2.3), and the golden must exercise the reverse branch. - **Weight bias:** keep it **raw** in the file (D2), not pre-folded (D5) — `log(expm1)` is a load-time constant folded once in f32, and a raw mirror of the state_dict is auditable. D1's fixed-4742-float blob is rejected in favor of D2's generic+arch_hash layout. -- **CMake gtest wiring:** each new `*_test` needs **all five** `nav_test` entries in `/home/joe/src/cvc/wt-libcvc-nav/src/cvc/tests/CMakeLists.txt` — `add_executable` (L100), the `TEST_TARGETS`/list membership (L168), `target_link_libraries` (L320), `target_compile_features(... cxx_std_17)` (L850), `gtest_discover_tests` (L1014) — or the target is silently omitted (the "four-entry" trap in MEMORY). +- **CMake gtest wiring:** each new `*_test` needs **all five** `nav_test` entries in libcvc's `src/cvc/tests/CMakeLists.txt` — `add_executable` (L100), the `TEST_TARGETS`/list membership (L168), `target_link_libraries` (L320), `target_compile_features(... cxx_std_17)` (L850), `gtest_discover_tests` (L1014) — or the target is silently omitted (the "four-entry" trap in MEMORY). **Open questions needing a decision (do not block P0–P5):** - **Deployment belief mode:** does a C++ renderer/game-engine host need clustered/private belief, or is **shared (M=1)** the whole deployment path? Shared is the thousands-of-agents target; if only shared, `map_id` is all-zeros and the M>1 COW/rebuild cost is untested-in-anger. Confirm before P6 sizing. @@ -363,7 +363,7 @@ def drive_enabled(): - **Whole-drive tolerance/horizon sign-off:** the proposed short-horizon `1e-3` normalized, `<0.5%` flip budget are from the existing `Squad(batched_drive)` 5e-3 tier and need empirical confirmation across all six stories and multiple seeds in P3/P6 — this is the single number that gates P6, and it is the top fidelity risk. - **Canonical `.cvcnav` home + provenance policy:** where the blessed weights live (libcvc test-data vs pycvc-published vs per-deployment bundle); and whether the provenance trailer is required for an audit trail. -Files to create are listed in §2; the two files to edit are `/home/joe/src/cvc/wt-libcvc-nav/src/cvc/CMakeLists.txt` (add headers ~L88, sources ~L171 next to `nav/grid_nav.cpp`) and `/home/joe/src/cvc/wt-libcvc-nav/bindings/pycvc/pycvc_nav.i` (append the marshalling), plus the new grl-snam `coef_export.py`, `nav_native.py` additions, and the parity tests under `/home/joe/src/cvc/wt-grl-snam-nav/tests/`. +Files to create are listed in §2; the two files to edit are libcvc's `src/cvc/CMakeLists.txt` (add headers ~L88, sources ~L171 next to `nav/grid_nav.cpp`) and `bindings/pycvc/pycvc_nav.i` (append the marshalling), plus the new grl-snam `coef_export.py`, `nav_native.py` additions, and the parity tests under this repo's `tests/`. --- ## 10. Decisions on the §9 open questions diff --git a/docs/CVCNAV_CUDA_ASSESSMENT.md b/docs/CVCNAV_CUDA_ASSESSMENT.md index 8278227..bfae43e 100644 --- a/docs/CVCNAV_CUDA_ASSESSMENT.md +++ b/docs/CVCNAV_CUDA_ASSESSMENT.md @@ -12,12 +12,12 @@ I have verified every load-bearing claim against the actual code. Synthesis foll ## Verified ground truth (I read the code, not just the analyses) -- **CMake CUDA wiring** (`wt-libcvc-nav/CMakeLists.txt`): `CVC_ENABLE_CUDA` default ON, auto-disables without nvcc (L5, L25, L80-81); arch list `50 60 70 75 80` set **before** `enable_language(CUDA)` (L45, L75, L78 — the documented sm_52 fix); `+90` when nvcc≥11.8, `+100 120` when ≥12.8 (L63-67) — **local nvcc 12.0 compiles 50/60/70/75/80/90**; `CUDA_RESOLVE_DEVICE_SYMBOLS ON` so device-link emits SASS and **PTX is stripped — the arch list is the entire compat surface** (L54-57, SetupCUDA.cmake L29). `CVC_USING_CUDA` is a PUBLIC define (`src/cvc/CMakeLists.txt` L815); `voxels_kernels.cu` is appended to `SOURCE_FILES` only under `if(CVC_ENABLE_CUDA)` (L611-612) — the exact slot a nav `.cu` drops into. +- **CMake CUDA wiring** (libcvc `CMakeLists.txt`): `CVC_ENABLE_CUDA` default ON, auto-disables without nvcc (L5, L25, L80-81); arch list `50 60 70 75 80` set **before** `enable_language(CUDA)` (L45, L75, L78 — the documented sm_52 fix); `+90` when nvcc≥11.8, `+100 120` when ≥12.8 (L63-67) — **local nvcc 12.0 compiles 50/60/70/75/80/90**; `CUDA_RESOLVE_DEVICE_SYMBOLS ON` so device-link emits SASS and **PTX is stripped — the arch list is the entire compat surface** (L54-57, SetupCUDA.cmake L29). `CVC_USING_CUDA` is a PUBLIC define (`src/cvc/CMakeLists.txt` L815); `voxels_kernels.cu` is appended to `SOURCE_FILES` only under `if(CVC_ENABLE_CUDA)` (L611-612) — the exact slot a nav `.cu` drops into. - **The `--use_fast_math` bug is REAL** (`CMake/SetupCUDA.cmake` L88-96): it is attached **target-wide** to `cvc` for `$` × `$`, not per-source. Any future nav parity `.cu` compiled into `cvc` in Release inherits `ftz=1`, `prec-div=0`, `prec-sqrt=0`, and fast-intrinsic transcendentals — **silently breaking the float-equivalence contract**. This is a hard prerequisite to fix, confirmed by inspection (A3/A4 flagged it; it checks out). - **`sense_batch` structure** (`src/cvc/nav/grid_nav.cpp`): `parallel_for(planes.K, …)` (L593) — threads **across planes**, so shared M=1 ⇒ K=1 ⇒ one worker. The per-cell fold is accumulate-then-clamp reading the clamped prior value (L561-569, "Model C: running delta is f32"), i.e. **order-dependent**; the ray-march reads only `truth` (L497-551), i.e. **order-free**. The split the analyses propose is valid against the actual code. - **PERFORMANCE.md**: batched EDT 0.440 ms/agent vs 9.285 ms single-core = GPU ≈ 21 CPU cores (not a win vs 32); A* GPU 18.4 ms vs CPU 2.84 ms = **6.5× worse**; D2H 0.301 + compute 0.440 = 0.741 ms **loses to 16-core CPU 0.580 ms** ("a GPU EDT with a host consumer never wins at any agent count"); the "~60 agents" crossover is EDT-only/device-resident/private-ish, **not** the shared deployment crossover. - **Recipe** (`cvcpkg/recipes/libcvc-cuda/recipe.yaml`): `provides:[libcvc]`, `requires_capabilities:[cuda]`, `CVC_ENABLE_CUDA=ON`, linux+windows only (no macOS — no CUDA on Apple), self-hosted CUDA runner, `cvc_revision: 2`. Peers exist: `cvcgl-cuda`, `pycvc-cp311-cuda`, `pycvc-gl-cp311-cuda`. **No new recipe needed.** -- **Roadmap fidelity boundary** (`wt-grl-snam-nav/docs/CVCNAV_CPP_PORT_ROADMAP.md`): drawn **exactly at the bilinear sample** — belief→to_occupancy→EDT→field is **BIT**; sample→MLP→rollout is **FLOAT-EQUIV**. Rule 1: only BIT tiers may be a transparent Python default; torch stays the reference/golden forever. §10.1 ships all three belief modes (shared M=1 / clustered / private M=N); §10.2 deploy `nsub=1`, goldens pinned nsub=1. Flags: `GRL_SNAM_NAV_DRIVE` (drive, default `torch`) separate from `GRL_SNAM_NAV_BACKEND` (bit-kernels, default `native`). +- **Roadmap fidelity boundary** (`docs/CVCNAV_CPP_PORT_ROADMAP.md`): drawn **exactly at the bilinear sample** — belief→to_occupancy→EDT→field is **BIT**; sample→MLP→rollout is **FLOAT-EQUIV**. Rule 1: only BIT tiers may be a transparent Python default; torch stays the reference/golden forever. §10.1 ships all three belief modes (shared M=1 / clustered / private M=N); §10.2 deploy `nsub=1`, goldens pinned nsub=1. Flags: `GRL_SNAM_NAV_DRIVE` (drive, default `torch`) separate from `GRL_SNAM_NAV_BACKEND` (bit-kernels, default `native`). ## (1) Per-component GPU-fit table @@ -81,4 +81,4 @@ Speedups are device-resident on sm_80-class; "vs current" = vs today's single-th **The single decision variable:** after Step 1, does any *named* deployment's real N-at-frame-rate on *its own* hardware exceed the agent-parallel CPU ceiling? The GPU crushes today's *broken* single-threaded sense but only ties-to-modestly-beats a *fixed* CPU sense at N~1k; it pulls decisively ahead only as N outgrows core-count saturation. Measure that number before writing a single `.cu`. -Relevant files: `/home/joe/src/cvc/wt-libcvc-nav/CMake/SetupCUDA.cmake` (L88-96, the fast-math fix), `/home/joe/src/cvc/wt-libcvc-nav/CMakeLists.txt` (L42-78, arch list), `/home/joe/src/cvc/wt-libcvc-nav/src/cvc/CMakeLists.txt` (L611-612, L815, wiring slot), `/home/joe/src/cvc/wt-libcvc-nav/src/cvc/nav/grid_nav.cpp` (L497-601, sense split target), `/home/joe/src/cvc/wt-libcvc-nav/cvcpkg/recipes/libcvc-cuda/recipe.yaml`, `/home/joe/src/cvc/wt-grl-snam-nav/docs/CVCNAV_CPP_PORT_ROADMAP.md` (§1 fidelity boundary, §10 belief modes/nsub), `/home/joe/src/cvc/wt-grl-snam-nav/docs/PERFORMANCE.md` (L59-79, CUDA verdict). \ No newline at end of file +Relevant files: libcvc `CMake/SetupCUDA.cmake` (L88-96, the fast-math fix), `CMakeLists.txt` (L42-78, arch list), `src/cvc/CMakeLists.txt` (L611-612, L815, wiring slot), `src/cvc/nav/grid_nav.cpp` (L497-601, sense split target), `cvcpkg/recipes/libcvc-cuda/recipe.yaml`; this repo's `docs/CVCNAV_CPP_PORT_ROADMAP.md` (§1 fidelity boundary, §10 belief modes/nsub), `docs/PERFORMANCE.md` (L59-79, CUDA verdict). \ No newline at end of file diff --git a/docs/CVCNAV_MATERIAL_PORT_ROADMAP.md b/docs/CVCNAV_MATERIAL_PORT_ROADMAP.md index 410ad8a..07ec4bc 100644 --- a/docs/CVCNAV_MATERIAL_PORT_ROADMAP.md +++ b/docs/CVCNAV_MATERIAL_PORT_ROADMAP.md @@ -19,8 +19,8 @@ and hyperparameter. None of them are in this repo. They resolve as `$GRL_SNAM_MATERIAL_FORK/full_code/train_material.py` — a checkout of `github.com/SetasAditya/material-aware-grl-snam`, which `tests/test_material_fork_xcheck.py:22-24` skips against when the env var is -unset. A clone exists at `/home/joe/src/cvc/material-aware-grl-snam`. Set -`GRL_SNAM_MATERIAL_FORK` to it before touching any parity claim; the bit-identity +unset. Clone it and set +`GRL_SNAM_MATERIAL_FORK` to the clone before touching any parity claim; the bit-identity cross-check is otherwise silently green. **Companion C++-side doc:** libcvc `docs/NAV_MATERIAL.md` describes the same From 2b5417a272b4197167b2fd035bc42f4324d21299 Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Fri, 2 Oct 2026 09:41:54 -0500 Subject: [PATCH 3/4] docs: regenerate the cvc::nav guide and quickstart PDFs Re-render docs/cvc-nav-and-grl-snam-guide.pdf from the updated Markdown with the same python-markdown + WeasyPrint 70.0 pipeline and stylesheet that produced the original (re-rendering the previous Markdown with it reproduces the committed PDF pixel for pixel). The only text change is the reworded weights note in section 8. Both PDFs also had their cross-links to each other's Markdown written as file:// URIs into a local checkout, because WeasyPrint resolved the relative links against the render directory. Both are now rendered with the GitHub docs/ directory as the base URL, so those links point at the copies on GitHub. The quickstart Markdown is unchanged and its PDF renders identically page for page; only its link targets differ. --- docs/cvc-nav-and-grl-snam-guide.pdf | Bin 178193 -> 177855 bytes docs/cvc-nav-and-grl-snam-quickstart.pdf | Bin 61648 -> 61654 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/cvc-nav-and-grl-snam-guide.pdf b/docs/cvc-nav-and-grl-snam-guide.pdf index bd55eab98024b942bbc83b1e155999b6973796ea..9da24ea4da6855568f0ee83eba5ddbd2051867f2 100644 GIT binary patch delta 21852 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 delta 22154 zcmZ6yWmFz*u(gQ=2pZfWL4!kZ5AJTk-QAr=65QS0-QC^Y-Q67?WO&b+`PR(*@73#; zs$Ey@>h3&;i@t*MsRMV(fn?!eO?1xz7Y1B_TvW1_ZiE{m?R&yR(hT|nFX#J_%Qt6aBtvp@6qlt8jU zoU)zQ%eBcA>4GqkrV@BdQr!4}t`5)LioUEU225!nZD(mBp9SY}yINZPhQ@=mjHJ>A zI}c%sY1`=J8@>MD31v^&=U)QDh#kHt3dLd+y_K@^r({c*kt+&jjeqBItK-_a8M(Wn z%v1H9E!Gi4Z>T4JLRU(fE${|t{{dXE+u)+%XvYFlq78Jj+&L^r3}E~%I)kwdrlTek zqf0LArEaPu?lL6_|0tr$;1X(NWg#K~cIvrDAN)b#=|SP~`njCk=7RZe`p`w+@R(5W zdiw`fs8~y2gK$H_aYMsV_0c%lDd|`nV3L=<(ewrM)^7@hRhteSj&9aJ&;zCQ>wTH{ zWxwetdhaFu{r0bpay)BBT6vEWjSwA1cUaJe^|g%SugP2?Zq*63QiN~stDYb4F$RO` zA;{^ly06s3L{6UIJuenjJRf^w=@Qf)f3@A-{}$VuX$+dJ9 zi@?*W&fCEz@C>AOyw4my{z`K{JbbAg+Dm<6jDyj6w*q}*k1q1lB7T6gtut&*zQ9(h zGgRUB6?f%Nq6=REE2MKAMQT}aLVMQ`P**p_4U zG8|k5npK}g)qiJN$S<4x1yt&sTW+kBHNH5h1Ahs^R)NU!Sc#idksrTAnmD1%Pm4Q( zDtSAxS+C@RDR?#xCFHhbqs<-{60REH#OQ0;(Nm<&s_`P`q@YqGieB!i*b=SC?KLXr zR&~!t`qdrq!Z^Q>t#kk$G#DXnV*|B$Ww&*23p^tmbyrC=EC4 zTJO<;NMb`8N9Ql}hEK4vXMFO(wGPp8Z8$SV<;BpYI5uUE6;lA-cOyGN*Djsj(8%8t znG3<&fJ@`X8|8Y&d1Hf$kEZ)R%kcUZwbK2c)#PcWiyNqemYYusZNFBpRBz#?s2If6 z(aLnfv&qA-%`CB~b8GQ&dpG;Zd@>5=f(GWUMBQwqAQa+b2-erBMQJxt#Bnf_`&VEI z-6fv`1RM_U3n+iCl?TWxn--45-0AB>k*gTF0YsFc?TT~<35}a=XsngL1eZhZOIJ>e zKcWyr76)IJ*s78(bG8c6XBGb;T2Ro*f7H6PoDhZwP&-R0NuPpLfaF8&qi~s#3SCzPZ zKygQT`+TH=i?;JAYNOsr+^rhT2B4k9QSHr^nif8zn?;>^ll>CrwTM>G@3Ede zYuKlx3amC-uz9e;uyx1a)?J&e-`XwwJ(}vipJI(%9ma>DK!1Et3dJKTs#DWHkyjn@ z^Ot15A(LB2Xk;(bpp7RsRihkV8JvQ@MDFeWDy`=$fs4;lo=}1QrNnJL!gW800)VJ& zH$lfK9C+nD-m>Ss*N`)<`W53a)6JvyUc!%aJ{7a-EeapSxQ1$Td{4#<~D@!$-%V#MSjZ5b(K+V~HuwXleN>H~o zzOZStvXy+gqQQbjQ~chGG({)H7q1z#y+11v%vGUpU_=9L52^QEqL>hU9`GkwT(qzB z=UNu#TQcNA7w3pP3TRuF{T`lq*)FbU&e5HcJMUZNKMSUaaVtn>5Crot&}od7%LO}(K5 zrv2ZC5eD(YB1IYszf8SH<$<)xE09`VlsyN>NI%Qata3Hf$p{Y*Di#+vN>8uxX|0V0 zw-!II(DKDvY3hjt7-%9v`SOBf)9b~R&ve;i-B4dWCjaT23y6^ZHcnWX3>#Kmkk&U^ zG4^G^YWImX1kNlnbaTHA>Ywz6NyyR{YOvC?AZ2G(tE6_-Oqju8dw}I<_{8}0i?xB( zVM+F40YV2D%}a;T#4wNG%?!Tl?Ftu-Il}_=g*I*zxg>-lpId}~uT3}4S6afd!WrZ1 z0#PkW54Rn5Yd3|@mIo2roo9cnH9qGofS481xpLS**u$7e|_IJ?yn$AzBC^hS8EQP zu}m65j`>69cLgQ5u4l%&*NncAt4W0%K`lwK8cV>p$~s&uvyDPx25Cxrl*?k~i;VjU z$4=a~VK!-(LUm^b!YIBo1FYA39_w zSL^<@phEMDCTKheB{aa3w=ghG>}@V*$R{7P_*B<$t@Ub}d-S#ic6wlzv;3~LBC$itO}b5w zVA70G691T+=oCBahK&`WlWD6scYwK=rBly*@%&gsYeX-L(0H04SDw19s}!eBC0wfc zp8&}5jxr`q+9MQ*`$q3(8Q;%93233z;@_u^1NMD6KQb>f_OVAX0*p9vZ!L4UXh7f& z>B>okE)RYAG!Ml*G8}rCRm9B`JXQ?Uu>{i<8naS+(M9bTKJw1XhTOYoa61eed`Y@( zO5b4UN~UR%8gSw({D|cWz9V445noYNhY_P$FYso6FXIys3}XxprnL^snTgN&%~Gj> z0#qg?zYv6w&a z;ss@`hQ#&m8E`>_ zG%W~$l#Kgvzui`}+l=CM=nvzxe3iAmgL6`9f4N1I6YT0zb+(fo-P_BFt072>TgbS5 zi^uzPH1is(-cA|FEgI-t3h$AI2Te{c0XsYn|67i2QY43;Qx+VGG?=iCY@68Kn);qSFRZ#c5K*mOyChRV$$#Z7^brKWF+p#kGwnay}zK?bNtS}uP?mZU_ziCGMiZ+`INts}RF6AykaTARf zVliblfN&GAQ%(cGGqTjUf{R+;oRA_atZxcOw2Jlmcc!(4XG;p)NmiNjJhoaO#b88}9&NS{&WQeE>3BQJ!#qKL77D5eL)+#3aP;jmgpUmmpYP>%j^suHKUdKVU;)80O z8E@2}Jy~Fzan62hET!M0iQeW~orf2r|F;oV>sHG@NL1{pCGO;zIYMh;59D|?=})XH z5F!Y5-!)Vy1#iDJrIM$-FoaTceY_O(Xo$Rkz7sY|hC7Rk4G62NC$ z3#1uzT8%m7!-k?qE+%?UE^(p|bA&kmN-BlfE$XR-D|}Tno+# z!MmK8QwtsqY4een$XW+}3qfy|NL>%E1-_9OTn|nRp#+{-UJqUc!TLQ>rvY3Pu$hA~ zsGV*y*=i^sd67&V5K>RgMix(fbMpm2{VttvgO@J{@uf*;-xs$N6c{cwgrr=jQ7G(l z_M~O6-hRaq6yCK6ixV%e|bN6_sWiSq!gzmKI=)9y~5D#be-?Qy{udwRZ&35Q_*XO zvd{-NFUNN;_6L|@Y&g~E;DM;cKRf<|s!WUrmZZ9gG9O#1(k#H7ez0PwDu zd)-e?kSh5}t6to7zo`d^3ceEQ(h&-sM)@qiohoPYXJdZ5LkzY*+L3|j z^DNEkr}MN&t=zSSdY4ipU%zX}1o!XFiw_LFTu;!<%p3g2qx;}nyTNnkD%jd~cwA1j zOsl4LrJR`?C@$ekZt##k>HT%sf6EqMWj)L$zM*L{SfV%X!ecI>rYeUC|3?c>v2_5R zTt_=*4s5Ly4SMnaw)2As9DhbULw8S&61&^LF#yYlbVAQoYa5ENgoI(2!x7$#*B&qi z^5`;|L}wgL-9DK!J*0JNKK`}Y{ReK2TeZC9FV4%Nulj$*uO7RieYJ>Lq7iq71Cb(b z*2W!Kx>m>R%x-!d4pz5qVo%8rZjE~G*tVOZA6skn9sG1`4%%eX_5-zTQg40JOP|BO znxQu(6Odrbx<8g!iX~<0$L_N?p*%H4wS9&-pcB5}ko?WiT9~^nBf_h~n-F7rBQ>Cg?s6 zZIb)W&86Odc_a}8l0H9R@`2s5T|^~o6(?Rvv;VOyv&n33X4BK*`cu%=F8{!>4Vi=j z(E8HCB5L8qVz+(C&L$$&4SOs$x@IngeABjv7ofHsNG*79=Q#VVut0^)I2N)VR2`sv zMcg8@rAUERBk1dtLOVjmdXq$xV=`Ll7|zXO9?np3oNTt$#H#h}o96W3 z<>~+%3Y<&R(YW+)6ruG$I*HTLBHLT-K>1`Yb}bcj4nJE#LT(`wp40JVMdx*#7gh?H zES;oPEcHL+$rtNPKWnmQf)Ba=m#(JCb4QMh!#)rgazHTRNfgBvv5!QZ%El*#7%ak3 z!}l8B8ZKZAtte;Z;kUYvgGG?8KAB^{ey?oKthjd#I}S>S%m7Hu?2JDQo^KfL(Y zxxFi@sy#|E3wz z(glX?%?0^joaE>F_xFIsI93#FfChyKXJ|I*VC&iFP;+V=)*B?b~gegJGdjcj9dMtbpazi<-*;Eu6$0?vqi`{y2_Q~b|v z^SJf`J*$&04H+X}Ll0pkOZh!&*rNZ1vXcImZK^;SYuNAiB4+>R+!+L8zySKH3X;Q<{xw*4e@ZUMyd&hc(iM7u{&_!mlY;E6PFE z1OK3P%#z5ur4=vqY0xqkFtzjG$J4gI2#zSe{>J!=PMF|{q)>DjT<5-JfTXeqU-J~y z_%zdxxkAn2nOJvChpL7?y(PKmdYX^F@SOdZR_2K+ZXZXI%@wAI8|Ka^an;NFs<`>Q z!qC=9*2+-o*Whkdffd_9PmQduTaw7`X32GamN!1gNxw4j7zWw~;EcZsfArAA!ZFBm zG7gz7dR%O|+mbG(64bM}Bsa0klx?NrjKmBBOT8vvEm&?mYZ1kQNTxLO1AkG^E#I=+ zm8;}R#)d<0XuQIXurtA|)xuj75zU9RW^^*8GGZ(UzY2Sh{Ol1;FV5<}2{V{jW{dr= zx?kOYjbwH-2Xw>essEFAcNO~Xx%g5ai%uLy{Z9wo1Ub^LD%$bSxsl)@&Qf@;}zsYfwtSzTr$zU;JV z97FD?ZM8oXliPaIJ#|U|Ne=j_uyeULwo|-<^dR?ZLm*RyQXZ$!xkjd19b_MmVvJVi z@ss2q??Z0Q!&0pT!p$FEZhiXOQO4=U<2!EX4fJTA#F-5q>&IMp?>qh5SMbw~C zO8T|M>jOvH0jG*y+hM`+CmzPSH7ldssJRK|{ARe+cCD zbKRbIeh6BzbMj3?2LJ4QdklOLk<$M)e5lj*D(*b|hpB(+{_9q$kBLt;Mpx$P0;jCA zmXzbwgms3d?Hy3#K~)Ss(~-*iGVG3}h`J8-M70G5Ff>3sIjsLBN8%%Dn_s`gl1fQf zazglg2}#!y+?K4fWw0Vc?d|(J%$(U|r>?hcNW&@Rj%qMWm$2Yf;2~$I6~4EKC(}6> zgep@iON>=Y86#y3N{Z&4Sw!-w&fATC$aT*^BBdKwZ2!ytV`As)s?PiQ<;N4yne0Ri zK5+}&oF`5u0w2BHsf~-t-UpNCgKOSpt#40H=BRH0jjE@&mu=m6<;YJ#I@+D9=gBx? zd=i3m{;O!+D8=h5h|d__Xo%;LORB9rJK4%E9$lRnF{_iHho`|)7X!AZhndlH6N(XT z0yZUAuY`cHm|z974J)3S9buYUXz|xhn@-@hcXabrz`(9r`N2EGI8%x>Iet@C?!h9%y4UP3f3AWqw#^!orpGp~+H~a@ZE} zX!;ITXj3bY*&+3{U4;_Dv8Vg{stTjrES0=t77K$cr3Na|seUgaM5nQaoQq9SDp5es zYMFU+_KzM}g<%oO-kK$wCu9o&O^tj_{6?8DQ^RwK=hI5y{eJS}X*}vqpLCo~LM;HK zUPtkfpsl(cs2#uP7mN$$q z=Tc`Hw@wu;{e8mJMRo=CTb6}BsC1TvMKdBK->8Y;-nCE|#m$Xk)!rM_Myejiu}1&4 zXI*Y_?lyf)p&4J$R$w4v<2#bFcUmsg-*H+3_gbfKhojnpVHOt_B%!|u0H@4JE5y$D z$M$;Q7?<3i(Ze3O$3K^=!-bx{kFAm&%$Xl;*vdGX=!7*KJQ|Jf@@M~Bun~rTxfF4+ zRABZwc@mlZNvxV>Mia1U3FQ^irx=Zo(EQwEM$}-Dej>$JktPSf+ED$`9@d(b40VIi zQ*7+gL-|H;a3bJnGtxB&ydiop9Hb=|>K>l$h-DK*a9|>~+4Yvkcy?0CJy`*V7s&@k z2}W>|_N8Vz2N@XeQJFjQ=wE~F;8rTswA?G39xw+&f3yU@k~9zzh)_o!DC5b8BVyw* zl=#9cudTk`G&o%vr)>9Q(zgL?I&6HfnD&E z5wQ5b{7GS6&~Mf$j~%;GO#_8;iwaZMv3F)blS~x+tD_3mm-l*o&B?H|qYg5talUpvs;JMI~7YI7O{0V85 z$b>`oVsqYQAo~O|%!mVjb@?vV=odG|)G0?5XGL;m9L!v{!vPpe3*MgPrn~;gl+J#g zlLosx5k>0~GnabiEKRi~wW#`*EF*^rmQN+rEba!YZQeYvAY);oF4&Az_nBbAU8i*k z&fo@G{RSv+kdk?{>5OhU35dB z!4HAvJvR}YQ0h=;kHR4Vy5pRQUE}91I}U^&&XLAqTyr_!geed{r(cPwBQxn!qa~eQ zqDjaVmxBg8gLk~nI2+4_!sNXVCeCd{=wP>|(qo((aUbUIH*{KbWCC9NbTGby) z)sWImC@Vch4<}i?I8a_1=CrrO?7JPybor^VR*|g$D#PS4hqrp50-H>)uwo({+moao zV_XslzZ^Ugz6-o=Bf&5$vAJ&JlP{(g6?%Sa7_SJ?NC^J4K%+?{B@!Q#!uffp^oM|f zzPeQL&yB5O?_!~&Auk!=-4?h#t0ZHL-tht17F8A|O?QNT*pXAwOsRH7ZF?@`#9|i- zv>^?wbREg{Nh^A5VU# zuW!;KQB;k9a9U#BUCS)(`#-CtBJIv!Sp&01?I~j`N%Q~;WXgodGT6ek-(Y#&bLHIdiV2)>i5`G8fl(@$LclG z416HyZ=xr8V6SfgZk;$4osnF3X?D?zYzQL85=Ih+y93-WMm^7dH6^RG+eco#Tib3x z`Dn6D_=r~-9~W9S>rGztzDE7vT6=2X2o9+W?`S)lzaDT|UwFF^PCQTdLVF*HXw=f} z-6IvPbYXlvV{>sQCPVN3o=i^cBJf7#_Q2he4(d_fg`YU$hf$ zioV>7K~M2lXGC?bX{rSeid8(g@aOwqY}vSe5!PFE^)e@_P>He)3Lsv>9DM;nvL_$> zUeLF)leMUo(Bgob(_0NN!cZgsoZ-Sme5cgf@HZxUo-;vQwtu;^h?C#a+c`u8U}?x1 zAu<_XJ^qpd*jCr~Dp-H;U!(C@E|SSKYv25OZKQHvlu%ct$OvNy3_H>{5yTU*rZpzF zWN6yznPYb|p591>7-TAjD{8JXw8igph7Ukio!o~Ejvm(UZ=~YBeKr`mhpp+yF3De9 zziZ&M)n=Ysx1zW?Yj{tWK7ONRTi4vMyqr8xj`Z3FxLxYn8$kHFW?EwpT``!PxZ<}R zh=e_yIO^zsehf05PWTwccvRO%{hP_xS*i5SOvR=epiD0w_ zEmkrt^NLpP;4N_t*8PhIBd8m-J?~NO$!{Dg3lWY#&Egj(-w7|yXV1IN8i zII*=MbNp^S)0_(y`{!(~%+kAg@s$nqZl1Oti7qVm0b0Xe8vnKw>ECs(xGL1nx+3OJ zNv|UYv0RtXe#lMgH8xBbr1N!BFBx-Euw4N({EsmF-@}cc>xA)61Nh^tg1TM=M}G-m zyZ2~tNBq+J8FJh~>T5GUOXuQqe-ku}vp*hEU9WiT4aLmt^I-P~^WLl77fpXZo;?v) zpCCpJxy|d{GC*ubaot?A@5w z{)j;L!lxVGC>kpWR1~O5Yil(;7?n zVnn&CezPk~IB(>68lN5AZDUG&gYcj$eb$>FDUHMd52m3=m5hj`?Q1W9#vrk>_eo+R z+_ppgODLC4NwKVwPW>IvO`8uZSBNFtN^QOC})EH#cBSk&8qBTDv--IUKkNHTlajX?wQGYK6+)NBjH zR|xaHcvTJ5{)icbe1`_=xMMi8kh3)kSwTK_DNs8P2Rg*`JP(Og1&(kln-7mh0lE zrsZuG{-(#WSw(?%dc#S>`$Q(mu9Cin_BLw`|FLQp?j+L;V2}f=f~9Lmcu51ivoFB~ z>zD{!WXsGxw}eJkSi1VvY~5oCO|v_p%5r1J7#ND~!g>#-Y2O^`rGHx>_c>~F!F!e{ zF*oyaCoWQik9ttm$^3~ZxM7;9Od2lc(`zPj28lCq$cfnXg)}1JDF%{Aw7n1@GS)VH zce&kbA8#efBIN)gDcw>q@?)BsWUQ@p6O9XKcSU^4NMZeBV4D!M3{?i%KeOrU9YkE; zijhRAP`Pw`O$mu^8L5w7%y)HKh+Dnc>>(%HWkmdwp+`dulbopFP5-<}`quPfKODsv zqSXCPP1U|GcOg~l&#RB^M_#*#*=B9lqKM424-h?LssdzC!BQ};x_UZv4%PE3ddt%A z2+dK>f3PG`<0RIy@0v2I@{L3#4B*bjHWsaq;T@S zdoS~r00CcS-4h~pE(gA5N<-K*jK(HF-ksHLw7uvC6O@vs*@2_UTM(NL4!Iuhq*8s| z?wBvZ_^Rn5klzQf>3sUUYMiOWl!FzEGwh79nzawv_7P{u%hM@{Im}L-EI&&JWiYG6 z#8IJ|uZq#6$et6uHr;@ZSUHR*ML@GEc78WQ=m01vyslkWlzr`+*=NqN;iYHqCA728 z%c7f(3uoX6IyguaXSb2URIjmifC&%~kD%yv^N3tEO0u*x%lTECJ=GJ+=!){D`zrDD zYhPM3IJRH^WJER#bJaKvWFPN(&Temrb8Jr-#{PrIR9>1JkKCL0ATCzrj}*5=q*bdk z-4al05)jMo)0VSxf!F@Pwr$nl8i<^}79i~{g^T*30lc* z^2hq*t9rHU)X;PMtcri{U-3_3qf5A>;y~ zbX$pV8bWii6$xQsT_Xhkn?lj`8-U#&^v;l>`aH@E!fb3lFv&KK2!gguwY!*9-dXe5 zf{ju5RDXVZRv&0uJuoj?fW0_ChxgHna%Hy?iL@!a16>&1mVJoo*D zMib4EHI*(s$;`WIZ;oI@XV&;5wHRlwVpJ7Z^sD7#xl`FGccV<9?O*M9>}C3?Rv$44 z>q+DpRgGkRkW(XSYNC~w4uKeFx}vYlIA(U3ml>Uu?R@=w1@RHnx=Ne*BP>&{-ln?r zfT^y1sg?N#*Svj=eB%hTf(`^ih6@$kt@xxu~iZYpfHxpjO|oZPihe-#J|kVNIN z>4HAy%hCFEu@&6^{1oWxU-)JN+d(7k4P+NcN;`EqXiL&oBD+q|6wh%~){CMEF@Qzt zr9keDDW!>MnEm58Xw`%h_5NS7R$OfuXspJ#D)S-90l-(F(?Fk}2|g0C`{gOQA&Y27 z$$F`bw-Jzf@t)`tg#IM5?#w*C{o?d)=LuWzXinZnL|-g38RL_is_3O9lb7JG?+}qr zF{oV%ftZF|jJ3ik^$m&u99tP`n^W5t2ieM%aSr(m=0ucNo`;?`!{SBB{2=h> z5zI~z5dd)*0Vx{wsY(1WSkJCV!$n9J-_LK6DqLvteju8ib-wDX7t7**HE6k7j&oBv zv0o)}m`wTEui?hqBhihy}T9 zcIqSijTu9CPU&yy0T`3Yug}#eoxzxwO#b8Y1VTZENi8>Mrd^*EQySwx#emY1-{;15 z72+)exqqGg1XjpMj@ZH=GcnapXJmdQI-2N_&>*FZzx0o&*Ukf(5EUcjEa+_3NbS8z zUZ&pXnK=jc+lD?^B|e)`+Gz$oc?j zi$>K?qB5wL{JAGn>OS<2bJ|A6YW7$#+(+Kd-ll13f%iI)u3G$0pXd6OIC#3eYO*lI zq~~{p1Mdfwb#e60h4~HW-9Cpm!5`m}x8H?t@UDSlF3aP;?lsfFcDZT{`$>6=}-%6F@Nu@aJ;D1`(tLis7013WIoAt zKFWEQ81#c0XIAs$QDe+uUY+%~L&5x+eI3%3rW^+mx#hJbJ++ez#Obt>DLsAA?2-5y z>z3i}XDd2g@PAs-Gp4BzSlSCk!;=yDEW-b!+2K=~dF&0vk8c3Nd(oVmvgBFE(zr8R zz%c5`4fwlY{hgcv0=c{*)nMDE2U9a{JS(a$j&+|Q0Nd!w-R1RffJP@6U)!J4WY@cG zJ=D5t-pp~-i=j6kr(Li;CILNy)uwhpE6he{rUfiFy|IU$eYs$qEh8mkfO64vxbwI$ zC1s#Ln5>c=;}W2Ci(Ox2-55NV1x>S!rQ1G^pc+D&S*1OW#`m`AsKh41FTb&Z={ULK zw*^6SC$s0T!$taIO(UAJ(QpW?clnl?=7;C45*#&a`Dm4VdNLDX%`D(!=84MB5o(s* zdE6=R0}q41Cto)#JX_+51&$Bf$xlfSjG(G?}|1+E3BV5rHuj2m#m<`|Kvc(&}kx5 z-qW!EyB<`VdSr9EvJ@`X8=beZ)l6rVUy(v0SA`dpyZ+5}UVm>0H}W#j84~30Od|R* zKC|Pi-JtMkAKZV|TvIkEZI4KNHHDTo7%l$2UfPl`yCP;lrVF$U1$9XjbXmZd1vT!w zhQ?+Ntt|Dx$Wxlzq0BV>RQ_)LLku!r$jdJAX`I25%)m!j5Fv=%dxgM_rzC3n8Tz@P zf@O1A1fC+Z*8j0rB=}&Xya{#dnVoZdPBQdRaVtLMz4`9*@}Bd!cHJocu0nIi_?`V{ zbzpp29l-y19Xd7~<&B)h5S{ypnE#U`|0~M>Pm&OhYiY%qAR`eVqJ8{+C=q77F5>Or zAPr`M-s$pl_+vO7!W3Y%OCJJ1Mwf$O7oulVrfJ=<#j5Le(fIzm4Le~f)8nA8-i3}0 z#CULqJ>xRFo+(q?`mg`$_g~<&b}jQ=&b4egLSBZ*l$=IYZ(REP6x2(LQ@U0(cV<|W zYM<>?;^}bPbg2-O|G0Q|?KNaI$aWriBLcPDb=J2r_$BlX>ih{vmU}`?GtQzlzLA_5 zwKjVGQamCmU|BJGc3~4JJ{DSBp8O#bF`K5TGT+BDd;PrUE#@QG!U@!ba%i1sXqu^9 zdHnpfsfM$Ti{G{Pi?hbIz}Kd`QUI@@%2{`Xau~~q9iY%z{lc!gvXN3WX3t~L$;2e3 z*7ct|>Umt4zP8r&9MS)2kA9sJY-I|iQ09RB>~)i>ZIzypUNVRBxJyjZ5o35Sy($Rv zAx~Hiyf(O$rx8N!fP0=YX{ySGGv~uc$KXY!ktBjt<5PQI%cV%5O3CN4q)u#8rID`V zOv@sb?Zqg=nUtf4UejL>88ktpKA!Lxew?a*w@OghxT{Yun|WOtNT+S1=Em!cq5h08 z&}A91DnjeTU9ztG9K-U&sE#xyuMiNkB@i*^&R*Sd9E=hO{IW!fFSDpPOq+xz6)bJE zFIR8ynmCX{+*yQtMY$thsk)Obw;CYg|Fp!GoLT#S)VXSsD8_2B@tjm!4M^)J!=_2K z77uE}iC(nRU3LqS4HfcAI>*smt!ctDN`UITCIfj(qNX+oY*@x&Rt18ko;B%Qf3fP6 z#>p-bXUIwed!>XLYfZ_;y?a)X1p*#!{M*ZRO2n*F-{5C-XU$sYhB!xuFOg>5H6&x{19XP0%&={wcDP#u!cirm zUz;qyOPk+>!k)rCN#+b7* zq4X;Jc0a@Q?8p&Qz@!a~Vco5!o~k;$4r`qsGb{62s&)oC^!}sxTFliMH zFrZ}xI^#fVkZjEu8LIrt9dTjc!AhQcn1dbtevF)VP(ee$`0dj0OOcZ`Xw|6i5iram z;uQZT7?!XgzVW0Sb|uzQZ;iFG1smDDR|o!e(9`C3Hf^Sc4|fkr01DmKucIh6g3te^ z^ci}p@fdn+8nFhTCswQD8%ogp{_jUh=DNb>bz(1%2p?AXN;Y>G#9llRq64(zCrC)z z<`}g4`^#?rt_e#yXeaA~M#KAk+{DNPPGt)#Tve9A;STFj6zttnHe-K!RY;w#|+&#^lL8fC(F<-e6P!iKLa&GqY9SCDaE8~IEcFcS~mVAS4%Mhq%G}I6E**}M3DLV zk^hKDA-&?Ph$cA&>mV{mb%enfK6utM)5Y`5H*yAZw`}Yw5`uN;OvVWki@nti`NB{f z*~0$Z-^%)IA9nYA7gD8k%$>`-+NS?<>T@-VY&LCXr;KFsI)#!so1}!bXy%&qtUK9S zK6BX``epK=2pUNrNKZ`XMc6G{W=^PLdZu?qwAMnI%nx%AF2J0u|4Z%k-Rk_)C2#yS z%+;}1E&*v2LrqxlN4*$dJRgpwjVMc>_=~1g-V&TF+t7bU)w|dC7L7)EM@X&^6-RYB zNTJ}Z(g^u^PK?oX9JTS4UYucPLnHyC^1=}KUvjp&^iqLU#P3Lc_4Dzx%^Vsy9-aT( zHt&^2I>0D0S5=UtJoXC^oUOe8TJ_Uas_ArvS)~@J0oH0g7gt?wb1dgE4OC@U3qRf) zgiZM11+d<0g;=@N1{)AbeaQUfaoAIejW67_i6@?-<*5p zrYV{wo&~&dbLv4hkZ=BJ+wxs}Kk#>_>1>DiVVOJRgqF*Tu_4)brPUvKp}=$gRFcuU zwogJ~=No0lYp!uAKt1G{G{X{_#^$!T$IgOYfy}Jbb1*6IB`R99eViZI-I*UVToXIPU)Dw zeZ29O#Yz_X*#_ewdwa9E;~obe^m#t!u{VA<8FsNEREsNZ-WE(gxJWI8J7Rt8{@Va6 zcL|)Y)@?5CFz&Z~WJp6N*?*04&z}g+%{ufOrYmL3qgK-tWxIfqRt#aWw*ICn8;`Hl ze=HixLnXcD3U5QfhP1w%JY!7RO4Q9kSlU>7l#}tVLZf~yvU-IBm;Lh4r4<7j>9w9I z*;A?A3d!0xvPYWpe(%mt7Ws6tSGZ+u`W%pTxo|d>p#k}i9!o}3Q{cbXnt73lE;oT@ z-G}#*WZQxp$)6kOgYy=h%#kdD(l_4s%O7WJT=5J*H;-+vJXU*a9^Z!F6>PG7;-eeB zUmXX`x>uR7kskJ{mr5`En~uB@#sgiR#7&te##VSL9+q^TEzjh*yIsMbmrjYuo&c_5 z9;O=0Nz96AiI(*V^ZP}{!-0q%_}{ilkv=)wRCoOFC&(F`5QcsUdD?cPMnf?4kf}1B zQ)=P9$iD$cXJM%4TT|g>O``&bz@!sLmmB_0Oc%bxW3UAp=jd5@ss@-Q6GsGqmqD(d-tPcOhY^=+x zoO@oG{xq&xGf|DW2O*lQx*X&`+A$82Mw>7qUO!9Y64}=OltlaQrKjNUurIcn=o)#| zE~PKVkr26ueLB%mseHShokE&HBd9fM>wm_Rqx}O(^xrd;7r8)LxSP;%i)c zSrSGUMXI1tw=3&{OEMV$)Z#_SP_7t{{0cZ?Ik|6r^88TR7*HHHw*8O2{9$kWU#jVJ z&R2GxGtMQ(se=D=d=K9>$tDiK+5ITx_j%O4)_+c(;ADUL2)4p*zkl##5n}CEYHm0@6LI8vVqbZ3f23gGH3q2avA>`e;3X2B|`0+@=d-Cx0%6ipOY^Y<_qqv z@4U2UrI>j*oiMR)v(iGLDu?qWp_?vbXNCv0_sNsPlEfSDT?+sDvXuI_lLqu~+Y$Dc zJfh>Q$$n*)3Dtp8+A-5h$$}XJyl+MzS)x_#K?!$)kDKY~m;l0gN8&L(clmTEu9%c) zw3yqxode?2;P4j%6W<$+VTUEd*t!?zcR`PT5|jp+vg(J#Mw>%kKVn-eCj~}~eDpi2 z(5^JWe_)cDAI93l>Jq+z-!g4Y{+xc)A+kS;(%m#Y4n>pY zkAkj+3G0$d)Ay+(BC*^GKZEMM=0N@YaTMXL=tk@d3R^Tg%10ePrK}Pa_Hb&VBH)A) zCNN!I&4dIqjODaCN!c1{^WrwzUa;IP6>J2HnGfDXmA9#D|4aUI14x+ARz9CD>vC6^ zBO)*KS;f9s%bR*S3tfV7Uv$2oNMjKs0wii}z4yK3GsC&gQwY#Y*P@6+nb#yYaF*WO zW?8dRKnxQlQKQmJ#u`V_T>ON%hsX%~(X+_)V9>IfAM;RYQoNK*?#qTZ>q<%Cy4Lr_ zri%lfS1rDoy`EJ&6yvh`yv{fKhYUnx10eCSK~!aLq}%Sd%FI5`u~Tl{&yUq~+8=l_ z8?}zXs~Uh)hgH^w5AP2+DZVxFf<*Lda!#&f@tJ~gW~+75^|J~UqMyMH&GEvjIk7LD zot=Pe7+9Xff)-hO7DIhXQsk#G0(NDm(mCl$WI~xF zk1Hl=JmsSYYe2T5=l-{_Zj9akFhY&am()bX_%CVg2x`n2?*A1$6~pQUBrUVKN0Q|k z>M<4EVo=U;>7%pWK? zhrtX;nsHFeI21&VVL`l10geSVGlPj*B0sQTfX`6^i$F*&fR-hWNYDeQbFM)h3pm`M z0TG=OiE{wt&QAbr14u@E5)!imC@w%h%moEUlK^BXEPtv1St_t62V|*1k{pnwaWp{` z@(cw)Ru~pjML__LBT0}nc|cYg+y)Q0%E3kl$jYHFNfB5If&em@jW&R+B~1a7>&$QUbV`j9rRa1Xk3bRd%8XH}p8WRO!2lFnRsV&Io6 zLLQRC9e?-a02C()zL!=l=oxxBM_7UzBI6J?4#5+dR74XK{2{`R-Q+uQ4s4bF;-ayj_1nl9PUKs{fRfYSv{{UzK%TKz?a ziw>921Pt~U6)ph^#E32?)S1x1gbpUav;n5|Fn>pb=k?9J0a>0rS!4ISXo^{V5wK*w z{ot3IW>(GVHuJy-KNnZHL!WQUW#bFs$?y?Rn#(e&=VfwTEQ*UoaebLwm2<#MuIgzy zd(4J$wu68C^Iy>GG}pN>Ih4$$5^N?+N9+;Ll8d^TTo%jZy!ZpXBI;Ycl(i{THiMx1 zUVl7UmSs}RmNj*e`?^?{rMWSXVc$h~gqPL)VpamhWs_7_*Y%<)<_&7_*w>#|z`bg2 zlSw^q7R99LormBqb;5EJz)Rm9Q@%S3zJHptPAgNnRo9c?@7)5&vBG)DrSqA0x5Od#YRu+pn09@CzY69-PpMT7Y z_sNH{y0~nJ=4Ss?bCbf7i!4mlCFAn8o=-uw`l@Uex8PV)E-J`$@u2{kfLUD>O;yi( z1=U)W^0m|z$-HhzPdt|ILHhB{tN>()+%myGRD-0K#Z@(H>UmYnljZHQDT$zRFU~Nj zsemz$(}##w?}23ymqmGA{Yk}nQh&ZL7q>yM$K{L!aGBi9|F|h;)p=D;0blQDn8{Kr zgmZym=Vj3lht6lk#j=;nLO~gnYPk$Npga{ZB(eb9p8QTC+zL#WH+%H~0tms8R_X|{ z9@AVtToz4oTi+}of#7n2`QMN@CzE2asLExp*jSyV+^rY`c<&?8U<8Ob=YK`jT%O;| z!oy-gR$zHkH6)4AAyEb&i{I zK|G6|c1I*tiWsAHLcx+nQ&Yffbj*Cx)El;OFHLjRbx_dP>im{4S$_*PTq7X%3ZN!k zb_lfAZ#=9wx1NuUyw3 zH`U~KSiVJ*UQHjjigMRnEKlLPz2dKsro0T^78V6rqjM<|xrZ>7eC=D5NQL$$QE2cA z;U2P0q;m*EDO%s03S|-O?$qIABv!*QDNSmTTOfy%Tz`<;>p|ka>A-QgAv0Yew?c!- z9F9p@0rT}JDhd78OCVjT$4Pal6Z5`QeOg8$N)rUgZra??kaQBg%j3UPex}oFp@%^np`R)nKek>o#{d6m_^znWfR1((m3f?TAeT%ZQn?hav^BP z$Ql8a;duIa@#xTFHFMYwiI7RtLuFFra(_MKIAx*vWOP;|N6)LAhO|hYa?!`0txcGd zhg<=NSF>qmbK2Rn9J1>ANYLHE`IPD&7HD?V{d4H-?Nm+RRPo@ZO?gX6-=}KFIiCgXePLWOwb{Q(c-V*1%JE@ zIADyVV>LPA6~C{e5wAF;@d&7tqvb(~r>Ah^vT3fDU`I$k(|U0+xVWjN<$mPtbLw zg$RJXepw0#9i1`MN~Y4Bs$qZ0u78HgSJ_NwE}DzfA?pA$?l+g#S~QDdzC5q~OfRbD z@@AY?^ZRTkmr>v?!C@IP0vEq{-7?R}0P zVA~cJVB5BC8ewE%TzW=z)Sk+;J4idcYJz*T)o7ytTcMAd#(e&W6E5)-s?dWM)dF2? zh#6iLD7)b4T0Vu-r(N1S;wgK3S+b2?V-|LeS=cp>h#Yb1G-8sq4e%H-EU4tlcbTtIgtrC~X`dZI9><&KBkSmNb!0c1@>P-iLR- zgZEQLyT~A!$8NogOxRXShJUDM1^Z#Wjm+XUazy-y6No#s+@+rk^a~rt?5enC|9(Um z(Uvi*&)HX(^#@ipRI~i*-;Y;=-+fGWJ-DZu=_QWaK}rRr+KuD?V}E|bn2m2kp+(|| zW42qY%R8*gvo-5tc@K6u-@)$PWzn#&;+`^#d&*z2c0HfPP30EJyAO2juxXbfI%VI7 z?PFp_T*hbZ_HjGZ-A9JwP>(WDX=;8Y4;dZpYwTv=aCLOF5StproS-xmBZQGVC z;UxUFtti1TvGJ4~I8Bc7Yu8f9T0rBn~4$m5un z0xt5AuVrE{B_cIEKTYhHZ&J70hp6FasU!Tz73(_fwmq}x$bZYWmu)?2gl%~&w^Vd% zygUqbn4%lUv$rTJdnu|0Ig|i8)nlsvjW$oF77OlGv6>p)Xd(pBx zX#L%vO`w6-Zhzte{=Hg-AxE4c-WuQZj=vjioYC0?A8Al%MA1ciHX*wEa5{nc(+O)Y zf5gf9Eu?HOQoBCw^-TzV!l4!xj^wuy>)q$_w?@u)N8Y1Tl49{_MS5fTRs6lyGu^SX zCXcZ<4{^TlW1{|?hnT(XvtgOe*A|R)>TvAd)tMvS-hYYIkdYd^bg9g}TO>$1);VxMr*0SQJ2B!X`G0}^XZX%9%W7~K5|$);>X0_?*^ z?vqL%Taje?BNA-mhy+{j{<649PbTI0*IzR_B*D(YA&G;iZoKocg{s&^b)Oj9PbkJ> zEygxF!++K(`+9N%?vlr)?3q9Bz}oW;->}UC4;_(jhd$!Q>>0V;&dMlnG+X+V=*#EK@Pk%i; z?8fV-Tf_FIOB z7S(lAFZ?aN+>460|NPY1 zgMVjn!7CB|op*Y{osQzXYzpYaQ@?_e4W2G19(Cb}2EI(~2hWS^AG`qisy9t}^>^gW zzVHJTJOAcsFsja z_UP%f9+!^?zbvK@SXk{xFQ-5lq0D29c7J_6D`^$VFS7&4+xq*L-~Lowvq9Tx@Qw{R z8w#6`n4|<+jhF@3g%~l%hEhuAf3$p0-%zl*zo#qxGy;)hei6=yj<5|4=Oa43Mx(Iq z&lvi80f!pp{yvT{qSwx;7jg(08l%GaQ)c;Q$5@6V9?^^F!&ot*mGU7WLan-2LVt|P z^y^NFt~n;%*0n!Rh)zE$+b$upY(%<_$Pp;;46_t6ANh|93|k<6PV`FtkVJ6AgvkSbKH^utwbaFA&URBfLzzK+&tvr z{jSkbn)>bAv!9=T|MjbCTrR?q%735gdA+Nx>jsP(v>K~7H5N{oA;u&b=uueItQ8i` zI|@rx{R%7SI~YeRtmx49&=fzf(n<;kp@4M&t?4)273=q|w6qcB{arZ9&X+J8}+@M7c9 zN~4Zfnm6CRI(v53)!6?s&nEBX+4fB9i#au^HRO>Ey*)SflRCE8enejd3O1h{n|C|p zpZXo!h|bsfh6Lt5j&1GKqT|^>YE`D;=u=0irz?%L&pZ42wx5@t&?o*joSN{%M)$@Y zkM3>N+}CjxIY6Hp0uRVUiGS9!b-f=q%JYLtc^t!g#OPbQ9uct7Pg^jQzD_h$F5jp0 z_B5s6Bb;4bHz(`>V}C2x zdUr!*4!FH7N4vj~5nFRS)%Sjy89UtuATi*8OV6OBbcLIwYi@QYi+`tf9lB_}F1Aq$ zt;(#5t%s+tU8z-(t3JOk_-Q7fM_4;%AbgYHSThj5S#X>gc>UtnpT7HbBS;TAe3$1p z`IhUauiugDcc96BJAj&w>(^a7KvjobzrL62@1Ne6i2&B#o@CTJm~D4IqHjzDpC;7G zr;h0DtQbY8=yFNFqkmT)(R&BO;GkTr)}k$_m_#4xqepZ;VA$iNN3?l1?D5hgS_~X| z7foNQ8*V3;yH0hVy6Dr?rJtu=jZr6Qj5%Ioe*E^?>!;6lqVzpl)8jvl`=Soi+{=L; zp5>SDqniesXZs!=m7yKV@xF-59MNYn_vfm|yE^;ves$!{=pf3$sY4EKkD8vi{Bu}> zx~h88B9J^`5$O4k-@SeLY-g7Rv;r?;alzz{$#-N-p0s8nyK5$48PoeW0udHz{O4Ec zHl%OPNLqjzPoygggc z*S~C*W{)5AQP?L>{vVK+4y~8baRL*Usk8!Ze>Ofo3UhRFWnpa!c-l46IZxF=7{%fD z42vwvRS*|Y5mZ1>5R^?pL5JAsS<#C9$&v8(Y5s6$uFi z1rE^NK0XR_baG{3Z3=kW)mmF~+qM#ZKfeNx_GC!nPB7E) zjP2Y!bsWcb>b5E$D2bMNB9T5M<;MU1>;j}jNv164q)ASvG_knsE`aY_ETJ$$2`5?+ zK|;ZaBqmZs6GjgXkFhjUB1{w(u zIfq8Vjetf*cobqMBA5U{5Wx)$mPAMj_27e&f^bcQmY^ggB9vGOZv&m7K8h#^L@IDb zL_})*Ly0t!5%^z87z`?ul29)RxND#@(M*FJ63ruc8<;MI`iO)y)I>27atebu2_-nI zVB!(@uM{y1oK>0_e*wl-NQ?%5L8UP;Nfp6b(ePit{1RL}+@&NqPbO&<{F;p`GSINB z7qD{p7ksrZ;a;{W4vvd?UK4e2uvBoI)XB7%I7P&c3O>y1X_kWs%YpJ)G6MmIyh^Lu z7F^9>k9t{Nfr6^W3X% zQ}1p*oMzQ+I`+ottf;)a@P_kjI_^^>sqkq8`zEcTq3i9X_i6c1-)8y5dno2*l}>L! zL$eZVD#D2RMr$UyRRP}T$-Orzr(TsOGw(n1Z1fo>R@Qw!ghqB1Op1DRn^b5|Jk}l1 z?8`_-%*}maf9R$xW?lu}j$j$U`6BOkS!rJZ2pfxeQfK$6H}cT&#kxJALmK8sKp?;o z>i9dUX5O$Y{;txp*L4+1A6A?jH{>=gQ@d-kVw_IBBp+KTnOAj|dy}-7rFHq>C8K%{ zxOniU;3epe9z_I|u#t>wbSFNr{~$$lF#0B6nx^UaiB4U|t%=}rKCG>Hn&qFt4Oo}~&{0Q%^N47G z8^myo5?m->gj$7wlrZvzz}Nv+FYO)@ic3wFf67O7R^-7Y2|it%;6wIq>-w&G83dE8 zzMT*KQ85dSe>?U71;N{kQ}6QZ@O?0x7QG=;l~Hf3OMNE(uPPIhx>2R>BBoPjfg;q0H^*7FI4} z?9OzL`sBnViNqy|ouB^McrX@d^7YN#@dinclb4f6N!Q;mb89Ox6{fFcCn9l@4vfMB0Rjv(ZPx8bxm)$)>4dM zeoS1rfvr2Y+3j-Uc1V=hk>l2(F)TL>pV_RM9KU!$fXZFW>}GodurR%gIgA<5F+2}Bz!2D@&SdGwgXu&10W$X_h#&_@1Y;x!2)rDXlqql2(6}=KyEY8j6p%dM+D%ad1AaR z8HA`480oA_W&7eKhwFwZz7FjhWhlpZ88X1iNXi&$oEJE%`$m7UjE_|vHXU&?kVq^V zt!oC{7f{(9iDk6Am?L~pf(i;>y9^?IQxY&ugb*$PMZa_$ufY~?e~q>RVOIoP6`%5K z1g8ZqC9>vq4iawoVk^CO4O+(uz_7sVY#`h%&rZC$e06$$blx@nb5Sb#DoQ;vtMAPw zV&Sz)Ci^TY^faHEF^*HV7CD7nuq0`?Wn}AJ{YIg+E z{vJ|7ONW$-c1Vd$+ej(sSCO)RazEgt6OLR~XqdJjSB%&Ee}lCX%h9r>(h7wZjDdo2 zj9BnGmLD51m*ROa@86Rp+9iJu+U4$OzrOx-{ps|@ zX*NvD1V6mJ_eEZ;Kz-Rpu&sr=T1C6SSVt}5d(aLoOSD7x4%)G)AMN;i(avb+ot5pG z+&>F-fR9Fif2igiHt(o;V|>(Zv10rUtrebn97kNC)g{=<@ECt18|;X)Nhx$1@G`sWg*H=duhezv{A3J$^ z=%}MUKJs2Z@_s(@?R?~4#mDWF{GPOI*hkmsvm+g2fBcjmbUC@&YQMhLC>dkMvT9B8 z^$Oh?C!@92HCD#>gY=-6j^rCW&tN0og|u*}%YUPBaPVIi zLFycnZ11Fh4K$uXD-o@zwHoC{ClV16wMSV0U#9tF=37jLh%DB3=&_+oR%eQJk*|Z=|@N zK?5}Ty^`h`02j3Q<4J3S4(Ng&=<|1`-uP{Z V{zv;Gual_oAP6=JB_%~qMhY;*8kqn9 delta 2709 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 Date: Fri, 2 Oct 2026 10:01:45 -0500 Subject: [PATCH 4/4] grl-snam-weights: record the training SDF by a scene-relative path Both payloads carried the training meta's sdf_npz as an absolute path into the trainer's checkout: tools/train.py stores os.path.abspath(sdf_npz) in the checkpoint meta, and the .cvcnav provenance trailer is json.dumps of the same meta. Set it to austin_south/nav_sdf.npz in the coef_sdf.pt meta and rewrite the .cvcnav trailer from that meta with coef_export.write_coef_mlp. The weights are unchanged. Every tensor in the checkpoint is equal and its storage records are byte-identical; the .cvcnav blob is byte-identical up to the meta trailer (same header, arch_hash and weight floats). cvc_revision 2 already covers the byte change, and PROVENANCE.md notes it. --- .../grl-snam-weights/payload/PROVENANCE.md | 3 +++ .../grl-snam-weights/payload/coef_sdf.cvcnav | Bin 19376 -> 19323 bytes .../grl-snam-weights/payload/coef_sdf.pt | Bin 22686 -> 22622 bytes 3 files changed, 3 insertions(+) diff --git a/cvcpkg/recipes/grl-snam-weights/payload/PROVENANCE.md b/cvcpkg/recipes/grl-snam-weights/payload/PROVENANCE.md index ef24dcd..1323b68 100644 --- a/cvcpkg/recipes/grl-snam-weights/payload/PROVENANCE.md +++ b/cvcpkg/recipes/grl-snam-weights/payload/PROVENANCE.md @@ -18,5 +18,8 @@ SDF navigator's `CoefMLP` — predicts `(alpha, beta, gamma)`. - **Character:** an initial, deliberately short proof-of-pipeline run — enough that the coefficients have moved and the weights are usable, NOT a full training campaign. Regenerate from a longer run for production accuracy. +- **Metadata:** the checkpoint's `meta` and the `.cvcnav` provenance trailer + name the training SDF as `austin_south/nav_sdf.npz` (scene-relative). Since + revision 2; the weights are the same as in revision 1. Bump `cvc_revision` in `recipe.yaml` whenever these bytes change. diff --git a/cvcpkg/recipes/grl-snam-weights/payload/coef_sdf.cvcnav b/cvcpkg/recipes/grl-snam-weights/payload/coef_sdf.cvcnav index 0db7586bb100fb62f7b6a3cfe2a4b3932588a833..04ed363defa84021c5a0b511f60b84dac4f3f92c 100644 GIT binary patch delta 42 xcmdlmo$>cH#tjKxjN*$Ey%?nuON&b~^Wuy1OG`5J^AgMAi&N6{@(QYyY5`v(5D@?X delta 73 zcmex8jd85=)XZ3KCOvi<49H UQj7JAGZKqZL2@vG$%)=l0QXKC3IG5A diff --git a/cvcpkg/recipes/grl-snam-weights/payload/coef_sdf.pt b/cvcpkg/recipes/grl-snam-weights/payload/coef_sdf.pt index e53e1cbc14285624be675f410634e24c2e8c911d..4b2226448a36d479f55810ee1dd5d5834b7dfe73 100644 GIT binary patch delta 261 zcmbQYk@4OJ#tn@uED{V143l@UC~~pvK6x{Vg@GZ7W%3198F>i?35MkS)U^2Glr(+4 zwEUvn#FF^3)S}|d{5&@&k;w|IqMIF9r})bl7@L_I8yH&}8kt*~n;BY|8k(60c(Zey zs&tE={2@ew6K?Qi!O#H4O_P&CU6}=087Akk%1k~Ms>L{I^2bnnMzP6;VK$6&Cg%Z3 zfyt|Z6)7c@}9Vd4|b>tn!;!r})bl8XA~c z7+9K{8<|^}8Wl3whzEGHv4dDP5tHpAq}U+B0Qclq2mk;8