All method pages

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

  1. 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.

  2. Persist the sizing. The room calls patchConfig → PATCH /projects/:id/config. PtxApi.patch_config normalises the sizing fields and writes the updated config_json back to the projects row. Sizing lives in the project config, not the assumption book (see data model).

  3. Trigger the solve. The room calls solveProject → POST /projects/:id/solve. The route is thin: it parses the request and calls PtxApi.solve(project_id).

  4. Rebuild the world (orchestrator). solve loads the project record, config_from_record rebuilds a ProjectConfig, 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.

  5. Run the engine. workspace.recompute calls 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 frame resolved by _mode_b_frame. The engine returns a { result, financials } bundle. _finalize_greedy down-samples any hourly arrays into day-envelopes so the response never carries 8,760 raw rows.
  6. Respond. PtxApi returns a plain dict (SolveBundle); the route serialises it to JSON. The shape is mirrored exactly by the api.ts TypeScript type — the backend never sends a field the frontend hasn’t declared.

  7. 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/ raises ValueError for 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, the api.ts type, and the room consumer. Run tsc to catch every break.
  • Keep expensive work out of the route: parse + delegate in app.py, orchestrate in PtxApi, calculate in core/.

From the app’s documentation, docs/01-architecture/request-lifecycle.md · Hyprisma 1.0.5