All method pages

MethodBackend

partial some parts checked against the code, others not yet · what the statuses mean

Backend architecture

In simple terms

The backend is the brain: it does the physics and economics, and answers the web page’s questions. It’s written in Python because that’s where the scientific libraries and the optimisation solver live. It has three layers: a thin web API that receives requests, an orchestrator that decides what to do, and the engines that do the actual calculation.

Why it’s layered this way

Keeping the maths (core/) independent of the web framework means the engines can be tested directly, reused from scripts, and one day served through a different transport — without touching the physics. The web layer stays boring on purpose.

Where it lives

  • Web layer: backend/api/app.py (FastAPI).
  • Orchestrator: backend/api/controllers.py (PtxApi).
  • Engines & domain logic: backend/core/.
  • Storage: backend/db/ (SQLite) + backend/data/ (Parquet).

The three layers

flowchart TB
  HTTP[HTTP request] --> App[app.py — FastAPI route]
  App --> Ctrl[PtxApi method — controllers.py]
  Ctrl --> Core[core/ engine or service]
  Core --> Store[(SQLite + Parquet)]
  Core --> Ctrl --> App --> Resp[JSON response]
  1. app.py (web layer, thin): declares 163 routes, parses/validates the request body with Pydantic v1 models, calls one PtxApi method, and maps errors to HTTP status codes. It contains no business logic.
  2. PtxApi (orchestrator, controllers.py): framework-agnostic. It loads the project record, rebuilds a ProjectConfig, resolves the assumption book into typed params, calls the right engine, persists results, and returns plain dicts. This is the “do the thing” layer.
  3. core/ (engines & domain): the physics and economics. Never imports FastAPI.

The engines (the staged spine)

Each stage adds one physics layer (see System architecture):

  • Prefeasibility · Mode A — core/run.py closed-form: each component reports a yearly ceiling; the binding (smallest) one is production.
  • Prefeasibility · Mode B — core/greedy.py: an 8,760-hour simulation with fixed priority rules and no foresight.
  • Feasibility — core/feasibility.py: builds a PyPSA network and solves the whole year as a linear program with HiGHS (optimal dispatch + operating constraints), persisted by core/dispatch_store.py.

Routing between engines is run_prefeasibility_auto (NH₃-only vs multi) and run_greedy_from_config / run_feasibility_from_config. Financials (core/ financials/) turn any engine result into CAPEX/OPEX and levelized costs.

Book-canonical parameters

Every technical/financial number lives in a versioned assumption book. core/parameters/book.py turns the book into typed Technical/Financial params that thread through both engines, every component, and the financials — so editing any parameter changes the result, and nothing is hard-coded in the engine.

Request validation & error handling

  • Validation: Pydantic v1 request models at the route boundary, plus explicit domain checks in core/ (e.g. parameter ranges, unknown derivative kinds).
  • Error mapping (verified pattern): domain code raises ValueError; the route catches it and raises HTTPException(status_code=422, detail=<message>). So a bad input yields a 422 with a specific, human message (e.g. “unknown derivative field(s) …”) rather than a 500. Not-found yields 404.

Asynchronous work

Long solves (feasibility, right-size landscape, storage sweep) run as background thread-jobs. The route returns immediately with a job status; the frontend polls a status route until done, then reads the persisted result. Every such job — and every request that waits on the LP — takes its turn in ONE solve queue (core/solve_queue.py): at most PTX_SOLVE_WORKERS run at once, the others are reported queued with their place until a worker is free (the API reference, “The solve queue”). The feasibility solve persists an hourly Parquet frame + a JSON sidecar once (SOLVE), and every chart/table is derived on read from that frame (POST-PROCESS) — the expensive optimisation runs once.

Storage & the repository seam

  • SQLite (via stdlib sqlite3) holds projects, per-project assumption values, snapshots, the two version axes (book + sizing), DAB versions, and the Site library. Parquet (PyArrow) holds hourly result timeseries under backend/data/results/<project_id>/.
  • backend/db/repository.py defines the store interfaces; every SQLite* store in backend/db/ implements one (sites.py declares its own). This is the seam kept open for a future MySQL/multi-user move — see the operations/security notes.

Security posture (honest)

Sign-in and roles are enforced (since 2026-11-03): the session cookie becomes a principal (backend/db/workspace_context.py), every route maps to one action of the role matrix (backend/core/access.py), PtxApi refuses to act with no principal and answers 404 for another workspace’s project, and every store method takes workspace_id as a required keyword (test_enforcement.py checks each one). Passwords are stored as salted scrypt hashes in the database; there are no secrets in code or .env. The details are on the API overview and Operations & security pages.

How to modify this safely

  • New behaviour → core/ (+ a pytest in backend/tests/). Orchestration → PtxApi. New HTTP surface → a route in app.py that just parses + delegates.
  • Changing a response shape is a three-place change (core producer → api.ts type → client.ts/room). Keep them in step.
  • Preserve the ValueError → 422 convention so the UI keeps getting readable errors.

From the app’s documentation, docs/04-backend/backend-architecture.md · Hyprisma 1.0.5