Skip to content

Thermal recipe v2 (sun-only sun matrix); land the stranded #40 - #42

Merged
kastnerp merged 2 commits into
mainfrom
claude/thermal-recipe-v2
Oct 1, 2026
Merged

kastnerp merged 2 commits into
mainfrom
claude/thermal-recipe-v2

Conversation

@kastnerp

Copy link
Copy Markdown
Member

What

Two commits:

  1. feat(recipes): see the campaign one recipe at a time; the thermal phases are stages #40, cherry-picked onto main. PR feat(recipes): see the campaign one recipe at a time; the thermal phases are stages #40 (recipe views, thermal stages, docs/thermal.md, scripts/admit_thermal.py) was merged into feat(recipes): the campaign names what a worker that declares no recipes may take #39's stacked branch claude/recipe-fence seven seconds after feat(recipes): the campaign names what a worker that declares no recipes may take #39 was squash-merged to main, so it never reached main or production (/healthz reports 0.23.0 @ 2b2492b7). The cherry-pick applies cleanly: feat(recipes): the campaign names what a worker that declares no recipes may take #39's squash commit has the same tree as 697715f.
  2. The thermal recipe is surf-1008/rad6R0P2-fft-v2; v1 is withdrawn. Version 0.24.1.

Why v2

Eddy3D PR #960 adds -d to the thermal sun matrix (gendaymtx -5 0.533 -m 4 -O1 -d). Without it, every sun also carried the whole Perez sky, so the direct term re-counted about 2.6 % of each sensor's diffuse sky. That is about 1.1 % of total irradiance on open ground and about 30 % of "direct" on a north wall. irradiance.npy and t_surface.npy change. By the contract in docs/thermal.md, that makes a new recipe version, never an edit.

Changes

  • docs/thermal.md
    • The recipe is v2 in the table and in the spec.
    • A new Sky row names the three gendaymtx matrices.
    • The telemetry and manifest include radiance.gendaymtx: {sky, sun, direct_sky}, as the Eddy3D v2 node records them.
    • A new Versions section says why v1 is withdrawn and that there was nothing to retire.
  • scripts/admit_thermal.py
    • Posts v2.
    • WITHDRAWN names v1.
    • --post refuses while a case of a withdrawn version is still pending or leased, so an old-build node never archives v1 while v2 is being admitted.
  • Tests
    • THERMAL is now v2 in test_node_releases.py and test_recipe_views.py.
    • New: a lease never crosses thermal versions, in either direction.
    • New: withdrawn_in_flight counts only pending and leased cases.
    • New: main() refuses with a leased v1 case and proceeds with a done one; the done case is the control.
    • With the guard disabled, the refusal test goes red.
  • DOMAIN.md: a recipe's version is part of its name.
  • CHANGELOG: v2 is under 0.24.1; feat(recipes): see the campaign one recipe at a time; the thermal phases are stages #40's own entry stays as 0.24.0.

What production held (read-only query, 2026-09-29)

  • No case of any surf-* recipe in any state, and no thermal events.
  • No worker has ever declared a thermal recipe. Every fleet build predates the Eddy3D thermal runner (5f6dc824, 2026-09-27).
  • So v1 needs no retire-or-re-admit decision. The v2 pilot is admitted fresh.

Before merging

Tests

uv run --with pytest --with httpx python -m pytest tests/ --ignore=tests/test_db_postgres.py -q: 826 passed, 0 skipped. After the final CHANGELOG amend, the version tests and the touched files were re-run: 69 passed.

🤖 Generated with Claude Code

kastnerp and others added 2 commits September 29, 2026 11:19
…ses are stages (#40)

* feat(recipes): see the campaign one recipe at a time; the thermal phases are stages

Every recipe until now was a CFD wind recipe, so the broker pooled them: one
set of counts, one ETA, one dataset. The Radiance surface-temperature recipe
is a different training set with a different throughput -- an hour a case
where CFD takes days. Pooled, its throughput projected an ETA for wind cases
nothing had touched, and every thermal case ranked at the bottom of the
run-time distribution.

- /v1/status, /v1/cases and /v1/dataset take ?recipe=. Status always lists
  every recipe's states (by_recipe); the dataset answers per recipe from the
  same cached pass; a case's percentiles rank it among its own recipe.
- The dashboard gets one recipe selector that scopes all of them, hidden
  while the campaign holds one recipe.
- scene / trace / surface are stages; "trace a/b chunks" counts chunks
  FINISHED, like dirs, and drives the ETA.
- A thermal case's page shows a Surface temperatures card instead of Mesh &
  Solve, no pedestrian-wind viewer, and a map of the sensed core.
- docs/thermal.md is the contract a node running the thermal recipe follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(thermal): the arrays are sensor-major, so a node can write them chunk by chunk

Hour-major [8760, n] would mean holding a whole case in memory (~8 GB at
300k sensors, temperature plus irradiance) to transpose it before writing,
which a lab PC does not have. Sensor-major rows are appended as each chunk
of sensors leaves Radiance and the admittance solve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(thermal): the contract as the node implements it

The Eddy3D thermal runner is written (branch claude/thermal-recipe), and a
few things in the contract changed on contact:

- the admittance solve runs on each chunk as it leaves Radiance, so a chunk
  counts once traced AND solved and there is no separate `surface` phase or
  `surface_seconds` (the broker still parses `surface a/b chunks`);
- the material table, worldcover-v1, with the rule that a surface's Radiance
  reflectance is 1 - its assembly's absorptivity;
- crowns reflect 0.15 and transmit 0.45 per face, about 0.2 across two;
- each sensor's sky view comes from its chunk's own direct-only sky trace;
- the archive carries spec.json and geometry/<case>.json, and sensors.npy's
  `material` is the WorldCover class on the ground, 200 roof, 201 facade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(thermal): admit the thermal pilot beside the wind campaign's sites

scripts/admit_thermal.py puts a surface-temperature case beside wind sites,
spread across LCZs and then across cities, deterministic so a re-run
proposes the same sites. Each case's weather is chosen HERE and written into
its spec, so every attempt on every machine uses the same file: the nearest
station whose dataset is exactly TMYx -- the whole period of record, never
one of its windows -- as `E3D climate-index find` ranks them. It looks 300
stations out: around one site the nearest 25 were all EnergyPlus TMY files.

A dry run by default; --post posts in batches of 25 with a pause, because a
bulk write against production runs under the lock the fleet's heartbeats
wait on. It refuses to post while workers that declare no recipes would be
handed thermal cases. --any-state samples unfinished wind sites: a dry run
against production found 2 finished of 5,000, so the agreed pilot of 50
finished sites cannot be drawn yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ffc217a)
… withdrawn

Eddy3D PR #960 adds -d to the thermal sun matrix. Without it every sun also
carried the whole Perez sky, and the direct term re-counted about 2.6 % of each
sensor's diffuse sky: ~1.1 % of total irradiance on open ground, ~30 % of
"direct" on a north wall. irradiance.npy and t_surface.npy change, so by the
contract in docs/thermal.md it is a new recipe version, never an edit.

- docs/thermal.md: recipe v2 in the table and the spec, a Sky row naming the
  three gendaymtx matrices, radiance.gendaymtx (sky, sun, direct_sky flags) in
  the telemetry and the manifest as a v2 node records them, and a Versions
  section: why v1 is withdrawn, and that it never needed retiring.
- scripts/admit_thermal.py posts v2. WITHDRAWN names v1, and --post refuses
  while a case of a withdrawn version is still pending or leased, so an
  old-build node never archives v1 beside v2 being admitted.
- Tests: the literal v2; lease never crosses thermal versions in either
  direction; withdrawn_in_flight counts only pending/leased; main() refuses
  with a leased v1 case and proceeds with a done one (the control). With the
  guard disabled, the refusal test goes red.
- DOMAIN.md: a recipe's version is part of its name.

Production on 2026-09-29 held no v1 case in any state (cases table, events)
and no worker had ever declared it: every fleet build predates the thermal
runner (Eddy3D 5f6dc824, 2026-09-27).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kastnerp
kastnerp merged commit 3a32340 into main Oct 1, 2026
6 checks passed
kastnerp added a commit that referenced this pull request Oct 1, 2026
…lds; 0.25.0

Only the version lines conflicted. pyproject.toml and uv.lock are 0.25.0, a
feature on top of main's 0.24.1, and the CHANGELOG entry for the fields moves
from [0.24.0] to [0.25.0] above main's 0.24.1 and 0.24.0. app.py, db.py,
DOMAIN.md and dashboard.html merged on their own: a thermal case still hides
the pedestrian-wind section, and the field viewer still reads the broker first.
Full suite on the merged tree: 840 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kastnerp added a commit that referenced this pull request Oct 1, 2026
…direction (#41)

case_fields table; PUT /v1/cases/{id}/fields/{direction}?lease_id= from the node as each direction is solved (lease-gated, 413 past the size limit, 422 for a blob that is not a field); GET /v1/cases/{id}/fields and /fields/{direction}; the dashboard's field viewer reads the broker first, .wfld stays the fallback. Merged with main after #42 (version 0.25.0); 840 tests green on the merged tree.

Eddy3D #962 (already on dev) is the node half: until this deploys, a dev node's field upload gets 404 and its cases complete without fields in the database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kastnerp
kastnerp deleted the claude/thermal-recipe-v2 branch October 4, 2026 18:44
kastnerp added a commit that referenced this pull request Oct 6, 2026
…direction (#41)

case_fields table; PUT /v1/cases/{id}/fields/{direction}?lease_id= from the node as each direction is solved (lease-gated, 413 past the size limit, 422 for a blob that is not a field); GET /v1/cases/{id}/fields and /fields/{direction}; the dashboard's field viewer reads the broker first, .wfld stays the fallback. Merged with main after #42 (version 0.25.0); 840 tests green on the merged tree.

Eddy3D #962 (already on dev) is the node half: until this deploys, a dev node's field upload gets 404 and its cases complete without fields in the database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant