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]
app.py(web layer, thin): declares 163 routes, parses/validates the request body with Pydantic v1 models, calls onePtxApimethod, and maps errors to HTTP status codes. It contains no business logic.PtxApi(orchestrator,controllers.py): framework-agnostic. It loads the project record, rebuilds aProjectConfig, resolves the assumption book into typed params, calls the right engine, persists results, and returns plain dicts. This is the “do the thing” layer.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.pyclosed-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 bycore/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 raisesHTTPException(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 underbackend/data/results/<project_id>/. backend/db/repository.pydefines the store interfaces; everySQLite*store inbackend/db/implements one (sites.pydeclares 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/(+ apytestinbackend/tests/). Orchestration →PtxApi. New HTTP surface → a route inapp.pythat just parses + delegates. - Changing a response shape is a three-place change (core producer →
api.tstype →client.ts/room). Keep them in step. - Preserve the
ValueError → 422convention so the UI keeps getting readable errors.
Related topics
From the app’s documentation, docs/04-backend/backend-architecture.md · Hyprisma 1.0.5