Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

Go microservice implementing the **EN ISO 13790** annual heating energy demand calculation pipeline derived from [tabula-calculator.xlsx](https://episcope.eu/welcome/) *(Accessed on: 26.06.26)*. The calculation method has been documented in [TABULA CommonCalculationMethod](https://episcope.eu/fileadmin/tabula/public/docs/report/TABULA_CommonCalculationMethod.pdf) *(Accessed on: 26.06.2026)*. The tool covers all European building typologies across 20 countries defined by **TABULA & EPISCOPE (IEE Projects)**.

The results have been validated against the Excel Workbook output. So far **19/20 countries at 100% accuracy in total 2,091 / 2,147 buildings validated.** See the [validation report](docs/validation.md).
Results are validated against the workbook's own output: **19 of 20 countries at 100% accuracy, 2,091 of 2,147 buildings passing.** See the [validation report](docs/validation.md).

---

Expand All @@ -14,7 +14,7 @@ The results have been validated against the Excel Workbook output. So far **19/2
| Dependency | Version |
| ---------- | ------- |
| Go | 1.26+ |
| PostgreSQL | >= 15 |
| PostgreSQL | 15 to 17 |

---

Expand All @@ -38,7 +38,7 @@ Architecture documentation (arc42): under development, not yet published.

![test coverage](https://codecov.io/github/THD-Spatial-AI/ignis/graphs/icicle.svg?token=CTUZED1ELJ)

The top section in the graphic above, represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
In the graphic above, the top section is the whole project, the sections below it are folders, and the smallest are individual files. Slice size is the number of statements and slice colour is the coverage.

> [!NOTE]
> **AI usage disclaimer:** Some tests in this repository were written with the assistance of AI coding tools, then reviewed and validated by a maintainer before merging.
Expand Down
4 changes: 2 additions & 2 deletions cmd/validate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type TestResult struct {
ErrorMessage string
}

const tolerancePercent = 2.5 // 2% tolerance for heating demand cross-country variations
const tolerancePercent = 2.5 // percent, allowed deviation from the TABULA reference q_h_nd

var cfg = config.LoadConfig()

Expand Down Expand Up @@ -210,7 +210,7 @@ func runPipelineTest(pool *pgxpool.Pool, tableName string, rowID int) TestResult
result.PercentError = 100.0
}

// Determine if test passed (allow 2% tolerance for cross-country variations)
// Determine if test passed
result.Passed = result.PercentError <= tolerancePercent

return result
Expand Down
20 changes: 10 additions & 10 deletions data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ This directory contains the TABULA Webtool workbook used to seed the heat demand
| Field | Detail |
|-------|--------|
| **File** | `tabula-calculator.xlsx` |
| **Source** | TABULA Webtool — [building-typology.eu](https://webtool.building-typology.eu/) |
| **Source** | TABULA Webtool, [building-typology.eu](https://webtool.building-typology.eu/) |
| **Author** | Institut Wohnen und Umwelt (IWU), Darmstadt, Germany |
| **Project** | Intelligent Energy Europe, IEE/09/739/SI2.558245 |
| **License** | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |

The workbook contains per-country building typology data (U-values, areas, infiltration rates, climate parameters) and reference heating demand values (`q_h_nd`) used to validate the calculation pipeline within ±2.5 %.
The workbook contains per-country building typology data (U-values, areas, infiltration rates, climate parameters) and reference heating demand values (`q_h_nd`) used to validate the calculation pipeline within ±2.5%.

**Citation:**

Expand All @@ -22,27 +22,27 @@ This file is distributed under the same CC BY 4.0 terms as the original TABULA d

## tabula-calculator-lite.xlsx

A trimmed derivative of `tabula-calculator.xlsx`, created for this project and published alongside it on GitHub. `build_db` only ever reads the `Calc.Set.Building` sheet (see `internal/db/table_constructor.go`) — every other sheet in the full TABULA Webtool workbook (country-specific system/demo calculations, charts, auxiliary climate tables, etc.) is dead weight for this tool's purposes, and made up the majority of the original file's size.
A trimmed derivative of `tabula-calculator.xlsx`, created for this project and published alongside it on GitHub. `build_db` reads only the `Calc.Set.Building` sheet (see `internal/db/table_constructor.go`). Every other sheet in the full TABULA Webtool workbook (country-specific system and demo calculations, charts, auxiliary climate tables) is unused here and made up most of the original file's size.

Sheets kept:
- **`Info`** — the original workbook's own attribution/info sheet, kept as-is so provenance travels with the file itself, not just this README.
- **`Calc.Set.Building`** — the only sheet `build_db` actually reads.
- **`BlankSheet`** (hidden) — a template artifact from the original workbook; harmless, left in rather than risking broken internal references by removing it.
- **`Info`**: the original workbook's attribution sheet, kept so provenance travels with the file rather than only in this README.
- **`Calc.Set.Building`**: the only sheet `build_db` reads.
- **`BlankSheet`** (hidden): a template artefact from the original workbook, left in place because removing it risks breaking internal references.

Result: **11 MB**, down from the original **28 MB** — everything else (roughly two-thirds of the file, including a single sheet, `Calc.Set.System`, that alone exceeded the size of the sheet actually used) was unused by this tool.
The result is **11 MB**, down from **28 MB**. The removed sheets were roughly two-thirds of the file, including `Calc.Set.System`, which alone was larger than the sheet in use.

This is a derivative of TABULA Webtool data and remains under the same **CC BY 4.0** license as the original — see attribution above and in [`ATTRIBUTIONS.md`](../ATTRIBUTIONS.md). It is what `environment/ignis-db.dockerfile` bakes into the `ignis-build-db` image (see that file's comments): the data is static source-of-truth reference data, read once to seed Postgres and never modified independently afterward, so shipping it inside the image — rather than mounting it in at runtime — makes a given image tag fully reproducible.
This is a derivative of TABULA Webtool data and remains under the same **CC BY 4.0** licence as the original; see the attribution above and in [`ATTRIBUTIONS.md`](../ATTRIBUTIONS.md). `environment/ignis-db.dockerfile` bakes it into the `ignis-build-db` image. The data is static reference data, read once to seed Postgres and never modified afterwards, so shipping it inside the image rather than mounting it at runtime makes a given image tag reproducible.

## Usage

The `build_db` binary reads whichever `.xlsx` it finds in this directory (`filepath.Glob("data/*.xlsx")`, first match alphabetically — currently `tabula-calculator-lite.xlsx`, since both files satisfy the same `Calc.Set.Building`-only requirement):
The `build_db` binary reads whichever `.xlsx` it finds in this directory (`filepath.Glob("data/*.xlsx")`, first match alphabetically, currently `tabula-calculator-lite.xlsx`, since both files satisfy the same `Calc.Set.Building` requirement):

```bash
go build -o bin/build_db cmd/build_db/main.go
./bin/build_db
```

Inside Docker (`environment/ignis-db.dockerfile`), only `tabula-calculator-lite.xlsx` is present in the image at all, so there's no ambiguity there.
Inside Docker (`environment/ignis-db.dockerfile`) only `tabula-calculator-lite.xlsx` is present, so there is no ambiguity.

!!! warning "Destructive operation"
Running `build_db` drops and recreates all TABULA country tables. Do not run against a database that holds production data without a backup.
33 changes: 20 additions & 13 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
---
audience: developer
---

# API reference

The interactive reference lives in its own standalone page, [`openapi/index.html`](openapi/index.html), so it can be opened directly without running `mkdocs serve`. It renders [`openapi/openapi.yaml`](openapi/openapi.yaml); download that file to generate a client or import it into Postman.

## Authentication

ignis carries no credential and asks for none. Every endpoint is reachable by anything that can open a connection to it, so who that is gets decided at the network layer. Deploy it only on a network whose access you control, behind a platform that has already authenticated the end user.
ignis carries no credential and asks for none. Every endpoint is reachable by anything that can open a connection to it, so reachability is decided at the network layer. Deploy it only on a network whose access you control, behind a platform that has already authenticated the end user.

!!! warning "Do not publish ignis on a public interface"
There is nothing in ignis to stop an unauthenticated caller. Keep it on an internal network, or behind something that authenticates for it. A user-facing login in front of your own system (EnerPlanET uses Keycloak) authenticates the user to *that system*, not to ignis; that system's backend then calls ignis on the user's behalf.
Nothing in ignis stops an unauthenticated caller. Keep it on an internal network, or behind something that authenticates for it. A user-facing login in front of a calling system (EnerPlanET uses Keycloak) authenticates the user to *that system*, not to ignis; that system's backend then calls ignis on the user's behalf.

!!! note "Base URL"
Depends on which environment you started. `environment/http` publishes the app directly, so the base URL is `http://localhost:8088`. `environment/https` puts Caddy in front of it for TLS, giving `https://localhost`. In a deployment it is whatever host ignis is published on. Every path below is the same either way.
The base URL depends on the environment in use. `environment/http` publishes the app directly, giving `http://localhost:8088`. `environment/https` puts Caddy in front of it for TLS, giving `https://localhost`. In a deployment it is whatever host ignis is published on. Every path below is the same either way.

## Call sequence

Expand All @@ -20,25 +24,28 @@ ignis carries no credential and asks for none. Every endpoint is reachable by an
| 2 | `GET /api/v1/data/{code}` | Every physical input behind that archetype (roughly 200 fields): envelope areas, U-values, climate data, solar gains. `GET /api/v1/fields` gives a plain-language description of each field. |
| 3 | `POST /api/v1/calculate/{code}` | Annual heating demand `q_h_nd` in kWh/(m²·a), from the ISO 13790 pipeline. |

Step 2 is optional. Call it when you need to inspect or report the inputs; `calculate` uses them either way.
Step 2 is optional, for inspecting or reporting the inputs. `calculate` uses them either way.

If you have a construction year rather than a period code, send `year=` in place of `period=` at step 1 and ignis resolves it. Exactly one of the two is required. `year` must be a non-negative integer no later than the current calendar year. A year before the earliest or after the latest band defined for that country and type resolves to the oldest or newest period. `GET /api/v1/periods/{country}` lists a country's bands as `{ period, year_from, year_to }`, oldest first, with `year_from` 0 and `year_to` 9999 marking the open-ended bands. Step 2's response also carries the resolved variant's own band, at `tabula_data.BasicParameters.BuildingAppearance.Year1_Building` / `Year2_Building`.
A construction year can replace the period code: send `year=` instead of `period=` at step 1 and ignis resolves it. Exactly one of the two is required. `year` must be a non-negative integer no later than the current calendar year. A year before the earliest or after the latest band defined for that country and type resolves to the oldest or newest period. `GET /api/v1/periods/{country}` lists a country's bands as `{ period, year_from, year_to }`, oldest first, with `year_from` 0 and `year_to` 9999 marking the open-ended bands. Step 2's response also carries the resolved variant's own band, at `tabula_data.BasicParameters.BuildingAppearance.Year1_Building` / `Year2_Building`.

## Overriding archetype inputs

`calculate` accepts an optional JSON body. Every field is independent and optional: send only what you want to change, and every other input still comes from the archetype's TABULA defaults.
`calculate` accepts an optional JSON body. Every field is independent and optional: send only the fields that change, and every other input comes from the archetype's TABULA defaults.

| You want to change | Field(s) |
| To change | Field(s) |
|---|---|
| The building's actual floor area | `A_ref` |
| How cold or mild the local climate is | `HeatingDays`, `Theta_e`, `theta_i` |
| How much sun the site gets | `I_Sol_South`, `I_Sol_East`, `I_Sol_West`, `I_Sol_North`, `I_Sol_Hor` |
| Extra heat loss at wall, roof and window junctions | `delta_U_ThermalBridging_Original`, `delta_U_ThermalBridging_Refurbished` |
| The building's actual room height or storey count | `h_room`, `n_Storey` |
| The building's actual room height | `h_room` |
| How airtight the building is, or how much it is ventilated | `n_air_infiltration`, `n_air_use` |
| How much heat the building's structure can store | `c_m` |

See `CalculateRequest` in the reference below for exact types and validation rules; `HeatingDays` and the solar-irradiance fields must not be negative.
`CalculateRequest` in the reference below carries the exact types and validation rules. `HeatingDays` and the solar-irradiance fields must not be negative.

!!! warning "`n_Storey` is accepted but has no effect"
`n_Storey` reaches the result only through the envelope-area estimation path, which applies when a variant's `Code_TypeIntake_EnvelopeArea` is `Estimation`. No shipped TABULA variant carries that code. Its other consumers are the envelope plausibility ratios, and those end in `Check_EnvArea_ExactToEstim`, which nothing reads. The value is validated and then changes no result. `h_room` is a real lever and is unaffected by this.

!!! example "Overriding a single field"
```bash
Expand All @@ -52,8 +59,8 @@ The body also accepts an optional `surfaces` list, one entry per physical elemen

- Each entry takes an `id`, a `type` (`wall`, `window`, `roof`, `floor`, `door`), an `area` in m², a `u_value` in W/(m²·K), and, for windows, an `azimuth` and a `tilt`.
- ignis merges the entries in each category into the single area and area-weighted U-value the calculation needs.
- A category you do not list keeps the archetype's default for that category.
- You own the accuracy of the geometry you send. ignis uses the areas, U-values and orientations as given and does not check them against the archetype.
- A category that is not listed keeps the archetype's default.
- The caller owns the accuracy of the geometry. ignis uses the areas, U-values and orientations as given and does not check them against the archetype.
- `azimuth` is degrees clockwise from North (0 North, 90 East, 180 South, 270 West). For windows it also selects which direction's solar irradiance the window counts toward, rounded to the nearest of North, East, South or West. A window with no `azimuth` is assumed to face South.
- `tilt` is degrees from horizontal: 0 is a flat skylight, 90 a vertical window. A window within 5 degrees of horizontal counts toward horizontal irradiance rather than a compass direction. city2tabula measures tilt the other way round (0 for a vertical wall, 90 for a flat roof), so convert its values with `ignis_tilt = 90 - c2t_tilt` before sending them.

Expand All @@ -78,9 +85,9 @@ The body also accepts an optional `surfaces` list, one entry per physical elemen

**Step 1:** Start the stack, from `environment/http/`: `docker compose -f docker-compose.prod.yml up -d`. On a first run, load the TABULA data once: `docker compose -f docker-compose.prod.yml --profile seed run --rm build-db`.

**Step 2:** Serve `docs/openapi/` on `http://localhost:8000` (`python -m http.server 8000` from that directory works), since `ALLOWED_ORIGINS` allows that origin already. Opening the file directly (`file://`) works for reading the reference, but **Try it out** needs an allowed origin.
**Step 2:** Serve `docs/openapi/` on `http://localhost:8000` (`python -m http.server 8000` from that directory), which `ALLOWED_ORIGINS` already allows. Opening the file directly (`file://`) is enough for reading the reference, but **Try it out** needs an allowed origin.

**Step 3:** Pick `http://localhost:8088` from the **Servers** dropdown, then expand an endpoint, click **Try it out**, fill in the parameters, and **Execute**.

!!! info "Using the HTTPS environment instead"
`environment/https` serves the same API on `https://localhost`. Select that server in the dropdown, and on the quickstart file open `https://localhost` in a tab once and accept the certificate warning first: browser JavaScript cannot click through it the way a manual page load can.
`environment/https` serves the same API on `https://localhost`. Select that server in the dropdown. On the quickstart path, open `https://localhost` in a tab once and accept the certificate warning first: browser JavaScript cannot click through it the way a manual page load can.
Loading
Loading