System Architecture¶
Architecture of the Nature for Cooling Rapid Assessment Tool. Companion to methodology/, which explains what the numbers mean.
1. Overview¶
flowchart LR
subgraph data["Methodology as data"]
Config["config/*.yaml<br/>typologies · weights · factors<br/>country defaults · citations"]
end
subgraph backend["backend/ (Python)"]
Engine["nature_cooling.engine<br/>pure, deterministic scoring"]
API["nature_cooling.api<br/>FastAPI (stateless)"]
Report["nature_cooling.report<br/>PDF / XLSX builders"]
end
subgraph frontend["frontend/ (React + TS)"]
Wizard["6-step questionnaire wizard"]
Dash["results dashboard + A/B/C comparison"]
Method["methodology browser"]
end
Config --> Engine
Engine --> API
API --> Wizard
API --> Dash
API --> Method
API --> Report
Three hard boundaries:
- Config → Engine. All methodology values (typology performance, weights, factors, defaults, citations) are YAML under
config/, schema-validated at load. Changing the methodology never requires changing code. - Engine → API. The engine is a pure function
run_assessment(AssessmentInput, MethodologyConfig) → AssessmentResult— no I/O, no network, no randomness, no global state. The API is a thin stateless wrapper. - API → Frontend. The frontend owns UX only. It never computes scores; even previews come from the API. One source of truth for every number.
2. Backend¶
2.1 Package layout¶
backend/
├── pyproject.toml # distribution: criterra-nature-cooling
├── src/nature_cooling/
│ ├── __init__.py # __version__ (engine semver)
│ ├── engine/
│ │ ├── models.py # Pydantic v2: AssessmentInput / AssessmentResult
│ │ ├── config.py # YAML loader + schema validation + version stamp
│ │ ├── scoring/ # one module per formula family
│ │ │ ├── heat_exposure.py # heat exposure (data-rich / data-poor paths)
│ │ │ ├── vulnerability.py
│ │ │ ├── heat_priority.py
│ │ │ ├── suitability.py # suitability score + hard suitability flags
│ │ │ ├── adjustment.py # derived site factors
│ │ │ ├── cooling.py # score + °C range clipped to envelope
│ │ │ ├── energy_ghg.py
│ │ │ ├── costs.py # capex, savings, payback, derived feasibility
│ │ │ ├── co_benefits.py
│ │ │ ├── equity.py
│ │ │ └── final_score.py
│ │ ├── confidence.py # branched per-block confidence
│ │ ├── recommendation.py # deterministic template composer
│ │ └── runner.py # orchestration: run_assessment()
│ ├── cli.py # `nature-cooling serve` console script
│ ├── api/
│ │ ├── main.py # FastAPI app factory
│ │ ├── schemas.py # API-layer models + storage document schema
│ │ ├── storage.py # local-first project store (platformdirs, atomic JSON)
│ │ ├── validation.py # /validate: errors, warnings, confidence preview + hint
│ │ ├── webapp.py # embedded frontend at `/`, SPA fallback
│ │ └── routes/ # assessments, methodology, meta, projects, reports
│ └── report/
│ ├── catalog.py # module-level English string catalog
│ ├── content.py # stored assessments → display rows, verbatim (single + comparison)
│ ├── pdf.py # 2-page PDF + comparison PDF, brand TTFs embedded
│ ├── xlsx.py # workbooks: per-assessment and comparison
│ └── fonts/ # static TTF builds + OFL notices
└── tests/
├── scoring/ # unit tests per module (100% engine coverage target)
├── scenarios/ # golden cases: input JSON → hand-verified expected output
├── api/ # contract tests per endpoint (100% api coverage target)
└── report/ # extracted-text/structure + byte-determinism tests
2.2 Engine contract¶
- Deterministic: same
AssessmentInput+ same config version → byte-identicalAssessmentResult. - Versioned: every result records
engine_version(semver) andmethodology_version(date-stamped config version). Comparisons across differing methodology versions warn. - Missing data: required fields fail validation before scoring; optional fields fall back (qualitative →
unknown= 50; quantitative →Nonepropagates tonot_applicable, never a silent zero). Every applied default is itemised inassumptions_applied. - Suitability gates: disqualifying site conditions produce
suitability_flagsin the result; the UI and report must render them prominently.
2.3 API surface (v1)¶
| Endpoint | Purpose |
|---|---|
POST /api/assessments/evaluate |
Validate input, run engine, return full result (stateless) |
POST /api/assessments/validate |
Dry-run validation for inline form feedback: errors + warnings, per-block confidence preview, highest-value missing field hint |
GET /api/typologies |
Typology library incl. suitability conditions, citations, each entry's one-line curation reason (served verbatim from the published curation records in docs/assets/, which travel inside the wheel — curation provenance stays out of config/ so rewording a reason can never force a methodology version bump), and the bibliography: the full reference behind every citation key with its DOI/URL, parsed from the same BIBLIOGRAPHY.md the citation check reads, so a key like jacobs2020 always renders as the work it names |
GET /api/methodology |
Formulas, weights, factors, version — powers the methodology browser |
GET /api/meta |
Engine version, methodology version, license |
GET /api/projects |
Project summaries, most recently updated first |
POST /api/projects |
Create a project (name + site description) |
GET /api/projects/{id} |
Full project incl. assessments and methodology_update_available flags |
PATCH /api/projects/{id} |
Update name / site description |
DELETE /api/projects/{id} |
Delete a project |
POST /api/projects/{id}/assessments |
Create a draft assessment (auto-save target) |
GET /api/projects/{id}/assessments/{aid} |
One stored assessment |
PATCH /api/projects/{id}/assessments/{aid} |
Update label / draft input (input frozen once evaluated) |
DELETE /api/projects/{id}/assessments/{aid} |
Delete an assessment |
POST /api/projects/{id}/assessments/{aid}/evaluate |
Explicitly run the engine and persist the result; refuses to recompute a stored result |
POST /api/projects/{id}/assessments/{aid}/duplicate |
Comparison draft: carries the site description, blanks intervention + cost/energy groups |
GET /api/projects/{id}/assessments/{aid}/report.pdf |
The 2-page PDF report of a stored result; 404 unknown ids, 409 for a draft |
GET /api/projects/{id}/assessments/{aid}/report.xlsx |
The XLSX workbook (Inputs, Results, Assumptions & Warnings) of a stored result; same refusals |
GET /api/projects/{id}/report/comparison.pdf |
Comparison report over 2–4 stored, evaluated assessments (?assessments= ids, caller's order = column order); 409 if any is a draft |
GET /api/projects/{id}/report/comparison.xlsx |
The comparison workbook (Comparison, Site context, Scenario detail); same refusals |
Persistence in v1 is local-first: one JSON document per project (schema_version, identity, timestamps, site description, assessments[] each holding its full input and full versioned result) under the platformdirs user-data directory, owned by a thin storage layer behind the API; multi-user storage is v2. Stored results are never recomputed — a newer methodology version is surfaced as available, and re-running is an explicit user action creating a new assessment.
3. Frontend¶
React + TypeScript (Vite). Structure mirrors the user journey:
- Questionnaire wizard — the 6 input groups as steps (project → site → climate → vulnerability → intervention → cost/energy); inline validation via
/validate; every field with the qualitative fallback and "unknown" affordances the methodology requires. Every intervention card opens one per-entry detail dialog (a native modal<dialog>): identity, the inherited evidence class with every citation inline — the finding, the full reference, and its DOI/URL as a link — the curation reason, each suitability condition compared against the described site with its numbers stated, and — where a verified photograph matches the site's climate zone — the example image, folded in as a section rather than a second dialog. Absent content renders nothing: no placeholder, no "not available". - Results dashboard — score cards (Heat Priority Index, NbS Cooling Opportunity Score), the six output blocks, branched confidence badges, suitability flags, assumptions list, recommendation.
- Comparison view — same site, interventions A/B/C side by side, with user-editable scenario labels and a PDF/XLSX comparison export (2–4 options, in the on-screen order). Options assessed at different scales are flagged as not like for like — on screen and in the export — rather than silently tabulated; the export highlights the best value per criterion and states the facts in a short narrative, but never ranks the options or names a winner.
- Methodology browser — renders
/api/methodology+ citations; every score in the UI links to its formula and sources.
Visual identity: the criterra.eu design tokens (paper #eaebe2, ink #16231c, brand green #2e6a4e; Newsreader / Hanken Grotesk / IBM Plex Mono, self-hosted). Design north star: "a scientific instrument, not a lifestyle app" — whitespace, one accent colour, score cards, no rainbow dashboards. WCAG AA.
4. Configuration (config/)¶
| File | Content |
|---|---|
nbs_typologies.yaml |
18 cooling archetypes and the 121 typologies inheriting them: an archetype carries every performance value and suitability condition, and every archetype carries a sources: array (DOI + finding); a typology carries identity, family and availability, and no performance value of its own |
availability.yaml |
Which entries are offered for a given scale, land use and site conditions — gating is configuration, never code, and feeds no score |
weights.yaml |
All aggregation weights (single global set) |
adjustment_factors.yaml |
Condition → factor tables + derivation rules |
input_mapping.yaml |
Qualitative → numeric mappings |
country_defaults.yaml |
Emission factors, energy prices, currency (each cited: IEA/Ember, national sources) |
recommendation_templates.yaml |
Deterministic recommendation fragments |
climate_classification.yaml |
Köppen–Geiger class → climate zone, for the map picker's autofill. A methodology value, not data: the classification is cited to beck2023, the mapping onto the tool's six zones is the methodology's own judgement |
derived_scores.yaml |
Derived sub-indicators, confidence blocks and thresholds |
Each file has a top-level version: (date-stamped), and all of them must agree — a mismatch fails config load and CI. CI validates schemas and refuses uncited performance values.
4.1 Bundled datasets (data/geo/)¶
Three published geographic datasets ship inside the wheel, staged alongside config/ and the bibliography, so that the map-based site picker and its place search work with no network access at all: Natural Earth admin-0 boundaries (public domain, at 1:50m for the country lookup and 1:110m for the basemap the browser draws), Natural Earth populated places (public domain, all 7,342 — the offline place-search index), and the Köppen–Geiger present-day classification at 0.1° (Beck et al. 2023, CC BY 4.0). All are derived from their published sources by tools/build_datasets.py, which records each source checksum; all ship with their full licence text and a statement of what was changed, in data/geo/. The runtime formats are JSON and zlib, so the package needs no geospatial dependency and installs with one command.
5. Quality and CI/CD¶
- Tooling:
ruff(lint + format),mypy --stricton the engine,pytest+ coverage; frontend:eslint,tsc,vitest. - Tests: unit tests per scoring module; ~20 golden scenarios with hand-verified outputs as regression armor; config schema tests; API contract tests; determinism test (repeat runs byte-identical).
- CI (GitHub Actions): every push/PR → lint, type-check, tests, coverage gate, plus the packaged-wheel smoke check (build frontend + wheel together, install, assert
/serves the app shell and/api/metaanswers), a container-image build, and a strict documentation-site build; pushes tomainredeploy the docs site to GitHub Pages. Methodology version changes require a matching Methodology Report update (checked in review). - Releases: a
vX.Y.Ztag runs the full gates, then builds the wheel, pushes the container image to GHCR, redeploys the docs site, and creates a GitHub release with the wheel attached. - Conventional Commits; one coherent change set per merge to
main.
6. Deployment model¶
- v1 local-first: the wheel embeds the production frontend build and the cited methodology configuration;
pip install "criterra-nature-cooling[serve]"+nature-cooling servestarts API and web app on one origin (no CORS middleware). Projects live under theplatformdirsuser-data path. - Hosting: one container image built from the wheel (python-slim, non-root) with a minimal
compose.yamlmounting a named volume at the data path; reverse proxy, TLS, and multi-user machinery remain the host's concern and v2's scope. - Runtime configuration (v2.2): exactly one setting exists — the map-imagery tile source,
NATURE_COOLING_TILE_URL+NATURE_COOLING_TILE_ATTRIBUTION(or the equivalentnature-cooling serve --tile-url/--tile-attributionflags). Unset, the application makes no third-party request of any kind; a deployment operator who sets both gives that deployment's browsers real imagery, requested browser-direct and credited on the map. HOSTING.md is the operator's page.