Skip to content

Tech stack#

What these two knowledge graphs are built with — and, just as importantly, which parts are not decided yet. Anything below marked open is genuinely open; nothing here should be read as a commitment that has not been made.

Settled#

Concern Choice Notes
Data model RDF both graphs
Serialisation Turtle for the OEKG, RDF/XML for the MHP draft the MHP draft's format is an artefact of its export tool, not a decision
Triple store Apache Jena Fuseki the OEKG is served from a Fuseki dataset
Query protocol SPARQL see the endpoint
OEKG schema Open Energy Ontology (OEO)
MHP schema MHPO imports from and aligns to OEO
Graph manipulation rdflib also how the platform writes to the store
SHACL validation pyshacl works; but see the warning below about what it validates
Documentation mkdocs + mkdocs-material this site

The repositories#

Four repositories are in play, and knowing which owns what saves a lot of confusion:

Repository Owns
oekg (this one) both graphs' data, shapes and documentation
ontology the Open Energy Ontology
municipal-heat-planning-ontology MHPO
municipal-heat-planning-pdf-processing the RAG pipeline over published heat plans
oeplatform the Open Energy Platform — consumer and writer of the OEKG, via factsheets

Note the direction of that last row: oeplatform writes the OEKG over SPARQL. It does not read files from this repository. See how the OEKG is populated.

Versions#

OEO release vendored in the archive v2.8.0 (oekg/archive/madbkr_ba/oekg_rework/oeo-full.owl)
OEO release the live graph targets tracks the current OEO; not pinned here

The vendored copy exists to keep the archived thesis self-contained. It is not the version to work from — take OEO releases from the ontology repository.

Dependencies and environment#

Managed with uv. pyproject.toml declares the dependency groups, uv.lock pins the exact resolved versions, and .python-version pins the interpreter — which uv downloads itself, so no system Python is required.

File Role
pyproject.toml dependency groups; package = false (this repo is not an importable package)
uv.lock committed — the exact resolved set, so CI installs what you have locally
.python-version the interpreter uv fetches (3.13); requires-python is >=3.11

The commands a contributor runs live in CONTRIBUTING.md, deliberately in one place rather than copied here — two copies of setup instructions diverge within months. That page also lists the traps, including the fact that the dev server does not serve at /.

There are three groups: docs (the documentation build), schema (LinkML, which generates the SHACL shapes) and graph (pyshacl and rdflib). schema and graph are separate because linkml accounts for 88 of the 95 packages they resolve to between them, and a job that only validates has no reason to install a generator.

Pull-request checks run uv lock --check, which fails if uv.lock is out of step with pyproject.toml, then install every group and build the documentation with --strict. The deploy workflow installs with --locked, which fails the same way.

--frozen does not assert anything — corrected 2026-08-07

This page and CONTRIBUTING.md both used to state that CI enforced the lockfile with --frozen. That was wrong, and the guarantee it described did not exist: --frozen means "sync without updating the lockfile", so it accepts a stale lock and exits 0. Verified by running uv sync --group docs --frozen against a pyproject.toml carrying a dependency group absent from uv.lock — it passed. The flags that actually assert are --locked and uv lock --check, and both are now in use. Compounding it, the deploy workflow does not run on pull requests at all, so nothing checked anything before merge; that is what checks.yml is for.

This diverges from the rest of the Open Energy Family

Other OEP repositories use a plain requirements.txt with pip. This one deliberately does not. The trade was made knowingly: a single source of truth plus a real lockfile was judged worth more than byte-for-byte consistency with a 15-line workflow. If you maintain other family repos, this is the one place this repo will surprise you.

The archived thesis scripts are still not reproducible

oekg/archive/madbkr_ba/scripts/ was written against unrecorded versions of rdflib and owlready2, needs a Java toolchain for sync_reasoner(), and one script raises TypeError on every invocation. It is closed work; the archive makes no reproducibility claim. The graph group now locks an rdflib — but it locks it for new work, not for these scripts, and it does not lock owlready2 or provide Java. Nothing here resurrects them.

What is still open#

Model-design questions rather than repository-structure ones, worked as a separate effort. The first of them has since been decided and is kept here, marked as such, so that anyone who read the old text sees what changed rather than finding the section quietly gone.

The model source of truth#

Decided (2026-08-07), and no longer "under consideration". LinkML is the authoring layer: one schema definition generates the SHACL shapes that validate the data. LinkML never mints domain terms — every class and slot points at an IRI owned by the municipal heat planning ontology (MHPO) or by OEO — and OWL generation stays off, so it never competes with MHPO as a source of terms.

The toolchain for this is now installed and locked, in the schema and graph groups above. The schema itself does not exist yet, so nothing in this repository generates shapes today — gen-shacl works but has nothing to point at. Authoring it is the next step, and it is being worked as a separate effort.

What validates what#

⚠️ No SHACL file in this repository validates any live graph. Every one of them was authored against a dump:

  • oekg/shapes/ — the most developed shapes available, written against the thesis-reworked graph
  • oekg/eval/oekg_shacl.txt — evaluation shapes for the same era
  • the archive's own copies — thesis provenance

So pyshacl "working" does not mean there is a validation pipeline. There is not one yet.

Namespace migration#

The project has moved from http://openenergy-platform.org/… to https://openenergyplatform.org/…. The live graph has been updated. Some files here have not — notably oekg/shapes/ and the competency-question SPARQL in oekg/eval/.

This matters practically: a query or shapes file copied from those locations may silently return nothing against the live graph, because a zero-result SPARQL query is indistinguishable from a correct query about absent data. Files under oekg/legacy/ and oekg/archive/ use the old form correctly — they are dated artefacts and should not be changed.

Where the heat-planning graph will be hosted#

Undecided. Either a second dataset in the Fuseki store that already serves the OEKG, or a separate instance. Access, backup and governance ride on the answer.

The graph / table boundary#

Undecided. Which extracted heat-plan data belongs in the graph as triples, and which is better served as a table on the Open Energy Platform. See Workflow.

The modelling and build workflow — planned, not built#

This diagram describes an intention, not reality

None of the pipeline below exists today. It is drawn to make the open decisions visible and to give the shapes-first work a starting point to argue with — not to record an agreed design. The boxes marked undecided are the actual open questions listed above.

Do not treat this as the agreed pipeline. When a decision is made, the marker in this diagram should be replaced by the answer.

flowchart LR
    SRC["Model source of truth<br/>SHACL-first or LinkML<br/>UNDECIDED"]
    GEN["Generated artifacts<br/>what exactly: UNDECIDED"]
    SHAPES["SHACL shapes"]
    DATA["Graph data"]
    VAL["Validation<br/>pyshacl"]
    LOAD["Load into Fuseki"]

    SRC --> GEN
    SRC --> SHAPES
    SHAPES --> VAL
    DATA --> VAL
    VAL -- passes --> LOAD
    VAL -- fails --> SRC

The shape of it is not controversial — author a model, generate from it, validate data against it, load what passes. What is undecided is what the leftmost box actually is, and that determines everything downstream.

Documentation and CI#

Site generator mkdocs 1.x with mkdocs-material
Diagrams mermaid, via Material's pymdownx.superfences custom fence
Build strictness strict: true plus an explicit validation: block
Hosting GitHub Pages

The mkdocs~=1.6 pin is deliberate — do not relax it to allow 2.0

The Material for MkDocs team warns that MkDocs 2.0 introduces backward-incompatible changes to the framework Material is built on: the plugin system is removed (all plugins stop working), the theming system is rewritten (all overrides break), no migration path exists, the contribution model is closed, and it is currently unlicensed — unsuitable for production use. Material itself is not deprecated; it is actively maintained and Production/Stable.

mkdocs~=1.6 resolves to >=1.6, ==1.*, which permits 1.9 but blocks 2.0 — verified. Keep it that way until the situation upstream resolves. See the Material team's analysis.

Why the validation: block exists#

strict: true alone is not enough. mkdocs 1.6 does not check #anchors by default, so a strict build passes happily while cross-page anchor links rot. These pages carry several such links, so the config adds:

validation:
  anchors: warn
  unrecognized_links: warn
  absolute_links: warn

With strict: true, a warning becomes a build failure. This was verified with a negative control — an anchor was deliberately broken and the build aborted with exit 1 — so the passing build is a real result and not a disabled check.

Documentation is the only thing this repository has CI for. There is no test suite and no validation job; adding SHACL validation to CI is an obvious future step, but it depends on the model source of truth above being decided first.