MethodArchitecture
verified checked against the code · what the statuses mean
Request lifecycle — a solve from click to chart
In simple terms
This page follows one recalculation all the way through the app: you change a size in the cockpit, and a moment later the levelized cost, the flow diagram, and the charts update. Understanding this one path teaches you the whole data seam — every other action follows the same shape.
The worked example: editing a capacity
You drag the electrolyser slider in the Prefeasibility cockpit from 500 to 600 MW. Here is what happens, in order.
sequenceDiagram
participant U as You (cockpit slider)
participant R as Room (React)
participant C as client.ts / api.ts
participant A as app.py (FastAPI)
participant P as PtxApi (controllers)
participant E as core/ engine
participant D as SQLite + Parquet
U->>R: drag slider → new value
R->>R: debounce (~250 ms)
R->>C: patchConfig(projectId, {ely_mw: 600})
C->>A: PATCH /projects/:id/config
A->>P: patch_config(...)
P->>D: persist config_json
P-->>A: ok
R->>C: solveProject(projectId)
C->>A: POST /projects/:id/solve
A->>P: solve(project_id)
P->>D: load project record
P->>P: config_from_record + resolve book → typed params
P->>E: run_prefeasibility_auto (or greedy in Mode B)
E-->>P: result + financials bundle
P-->>A: SolveBundle (plain dict)
A-->>C: JSON
C-->>R: typed SolveBundle
R->>R: tween numbers, re-light bottleneck, spring bars
Step by step
-
Edit + debounce (frontend). The slider updates local React state immediately (the input feels instant). A debounce (~250 ms) prevents a solve on every pixel of drag — only the settled value is sent.
-
Persist the sizing. The room calls
patchConfig→PATCH /projects/:id/config.PtxApi.patch_confignormalises the sizing fields and writes the updatedconfig_jsonback to theprojectsrow. Sizing lives in the project config, not the assumption book (see data model). -
Trigger the solve. The room calls
solveProject→POST /projects/:id/solve. The route is thin: it parses the request and callsPtxApi.solve(project_id). -
Rebuild the world (orchestrator).
solveloads the project record,config_from_recordrebuilds aProjectConfig, and the assumption book is resolved into typed params (parameters/book.py). Site CFs are applied. This is where “book-canonical” becomes real — every number the engine will read comes from the book. -
Run the engine.
workspace.recomputecalls the right engine:- Mode A (annual):
run_prefeasibility_auto— closed-form ceilings, the binding one is production. - Mode B (timeseries): the greedy hourly simulation, fed an 8,760-hour
frameresolved by_mode_b_frame. The engine returns a{ result, financials }bundle._finalize_greedydown-samples any hourly arrays into day-envelopes so the response never carries 8,760 raw rows.
- Mode A (annual):
-
Respond.
PtxApireturns a plain dict (SolveBundle); the route serialises it to JSON. The shape is mirrored exactly by theapi.tsTypeScript type — the backend never sends a field the frontend hasn’t declared. -
Recalculation-as-hero (frontend). The room receives the typed bundle and animates the change: numbers tween to their new values, the binding stage re-lights in the flow diagram, and bars/flows spring to new widths (150–300 ms). This is the app’s signature moment — the instrument reacting.
The two-layer contract (why some changes don’t re-solve)
The app separates SOLVE (expensive — re-run the engine) from POST-PROCESS
(cheap — re-derive from an existing result). Changing a capacity is a SOLVE.
Changing a financial-only assumption like WACC re-derives financials without
re-running the physics. The { result, financials } bundle reflects this split.
See System architecture.
Validation & errors along the path
- Frontend: light client-side checks; numeric inputs use
parseDecimal(locale-safe) so,and.both work. - Backend (strict): Pydantic v1 models validate the request body;
core/raisesValueErrorfor domain problems (bad parameter range, unknown derivative kind). The route maps that to HTTP 422 with a specific message, which the room surfaces in its error panel. A missing project is a 404.
How other actions map onto this same shape
- Right-size (
POST /right-size) runs the optimiser and returns updated inputs — same seam, heavier engine call. - Feasibility solve is the same shape but asynchronous: the POST returns a job id and the room polls until done — see the feasibility solve flow.
- Snapshot / export read stored inputs + results and regenerate artefacts on demand.
How to modify this safely
- Adding data to the response is a three-place change: the
core/producer, theapi.tstype, and the room consumer. Runtscto catch every break. - Keep expensive work out of the route: parse + delegate in
app.py, orchestrate inPtxApi, calculate incore/.
Related topics
From the app’s documentation, docs/01-architecture/request-lifecycle.md · Hyprisma 1.0.5