- Put the model's
.ttlfile in themodels/directory - Create a
.mdfile with the same base name as the.ttlfile in theexamples/directory:- This
.mdfile should have at least the following 3 empty sections, in this order: ## Downloads## Queries## Model Components- there should also be a "header" section at the top of the file which allows executable code to be embedded in the markdown file (see below)
- This
- Add this
.mdfile to thetoc:inmyst.ymlunder the appropriate section (Example Buildings or Example Systems) - If you want to add queries to the model, add them in
queries.toml. The section name should have the same base name as the.ttland.mdfile.
For a model called mybuilding.ttl, the following files would be created:
models/mybuilding.ttlexamples/mybuilding.md
The examples/mybuilding.md file would look like this:
---
jupytext:
formats: md:myst
text_representation:
extension: .md
format_name: myst
kernelspec:
display_name: open223-models
language: python
name: open223-models
---
# My Example Building
<description of the building>
## Downloads
## Queries
## Model Components
The queries.toml file might look like this:
[[mybuilding]]
description="Zone/room temperature sensors"
query="""
SELECT ?location ?sensor WHERE {
?sensor rdf:type/rdfs:subClassOf* s223:Sensor .
?sensor s223:observes ?property .
?property qudt:hasQuantityKind quantitykind:Temperature .
?sensor s223:hasObservationLocation ?location
}
"""./build_examples.sh contains the whole pipeline. Everything
it does is logged to build-examples.log (CI uploads that as the
example-build-log artifact), and on failure it prints the error-related lines
and the tail of the log. The stages, in order:
- Resolve ontology dependencies:
uv run ontoenv initthenontoenv updateto populate.ontoenv/with the ontologies undermodels/andontologies/. - Compile models and regenerate pages:
make -j 2(feel free to increase 2 for more parallelism) - Install the Jupyter kernel:
make install-kernelregisters theopen223-modelskernel used in the jupyter book to run the code samples - Build and execute the book:
uv run jupyter book build --html --execute --execute-parallel 1 --strict, writes the site to_build/html/. Retried up to three times, because fetching 223P's imports is flaky in CI — see When it breaks.
CI then runs tools/stage-site-assets.sh, which copies CNAME and the model
files into the built site (/, /models/, /compiled/, /withimports/), checks
the copies landed, and deploys to gh-pages.
Compiling (make compile-models), once per models/<name>.ttl:
| Output | Command | Purpose |
|---|---|---|
models/compiled/<name>.ttl |
tools/compile.py -r |
SHACL inference + validation against 223P |
models/withimports/<name>.ttl |
tools/compile.py -i |
the compiled model with all imports inlined |
Both rules are prefixed with - in the Makefile, so a model that fails SHACL
validation does not stop the site build. Use make validate-model MODEL=<name>
to see those violations and get a nonzero exit.
Regenerating pages (make update-examples), once per examples/<name>.md.
Each script rewrites one section of the page in place (via
tools/markdown_utils.py's upsert_section), so the prose you write around them
is preserved:
| Script | What it writes |
|---|---|
tools/make_model_formats.py |
models/<name>.jsonld next to the Turtle source |
tools/generate-queries.py |
the ## Queries table, from queries.toml |
tools/make_count_table.py |
the ## Model Components counts, from the model |
tools/make-notebook.py |
the ## Load and Validate Model code cell, with the model URL baked in |
tools/mark-out-of-date.py |
adds or removes the "not updated since the last 223P revision" warning |
Two other pieces are not part of make: tools/model-base-url.sh (below) and
tools/copy-validation-output.mjs, a MyST plugin registered in myst.yml that
turns the validation cell's stdout into a copyable text block.
Generated and gitignored: models/withimports/, _build/, .ontoenv/,
.model-base-url, build-examples.log.
Generated but committed: models/compiled/*.ttl, models/*.jsonld, and the
generated sections of examples/*.md. Regenerate them rather than editing them by
hand.
ValueError: Graph does not contain an ontology declaration: the model the page loaded has noa owl:Ontologysubject. Check the URL in the failing cell: it may be pointing at a published copy that predates your fix (see below).- A page fails the book build:
--strictfails on cell exceptions, not on a model reportingModel is valid: False. The traceback is inbuild-examples.log. - A page did not pick up your change:
makeonly regeneratesexamples/<name>.mdwhenmodels/<name>.ttl, one of thetools/scripts, or.model-base-urlis newer. Force it withtouch models/<name>.ttl. OntologyImportsNotFound: ... http://www.w3.org/ns/shacl#(or.../2004/02/skos/core): a transient fetch failure, not a bad model. Every notebook resolves 223P's imports from scratch, so one build askswww.w3.orgfor the same file once per model, and Cloudflare throttles datacenter egress — CI loses a few pages this way while the same build passes from a laptop.build_examples.shretries the book build three times; MyST does not cache a notebook whose execution raised, so a retry re-runs only the failed pages.--execute-paralleldoes not work: jupyter-book 2.1.6 ignores the value. A local build starts 15 kernels at once (MyST's default) and a CI build starts 4, never the 1build_examples.shasks for. So do not give the notebooks anything that tolerates only one writer, such as a shared persistent OntoEnv.- OntoEnv errors after an upgrade: environments written by ontoenv 0.5 cannot
be read by 0.6.
rm -rf .ontoenvand rebuild. - Everything is stale or wrong:
make cleanremoves the compiled models and.ontoenv, then rebuild.
To validate a local model directly against 223P without rebuilding the site:
make validate-model MODEL=mybuildingTo regenerate, execute, and build only that model's page:
make model-page MODEL=mybuildingThe single-page build first validates the local models/mybuilding.ttl and
exits nonzero if that validation fails. The rendered page is written under
_build/html/. Executable-cell errors and exceptions also fail book builds; a
model reporting that it does not validate does not.
Each example page ends in a Model.from_file("...") cell. That URL is written
into examples/*.md when the page is generated, by tools/model-base-url.sh:
| Build | Model URL |
|---|---|
any local build (./build_examples.sh, make model-page, make update-examples) |
file:// this checkout, so you validate the models you are editing |
CI (main, a branch, or a pull request) |
the exact commit under test on raw.githubusercontent.com |
No build reads https://models.open223.info, main included. That copy is
whatever the last successful deploy left behind, so validating against it
deadlocks: a commit that fixes a broken model is checked against the old broken
file, fails, and never deploys — leaving the next build the same stale file to
fail on. pnnl-bdg1-2.ttl was stuck there, its owl:Ontology declaration
restored in #75 but never published.
Pinning the commit rather than the branch also keeps a built page reproducible:
its URL still resolves to the bytes that page was validated against after main
has moved on. The published URL is the one a reader actually wants, so the
generated cell names it in a comment just above the call:
# load the model into the BuildingMOTIF instance. This page pins the exact copy
# it was built and validated against, so it stays reproducible as the site moves
# on. To run this against the current published model, use the permanent URL:
#
# model = Model.from_file("https://models.open223.info/nrel-example.ttl")
#
model = Model.from_file("https://raw.githubusercontent.com/open223/models.open223.info/<sha>/models/nrel-example.ttl")Export OPEN223_MODEL_BASE_URL to override it, for instance to reproduce the
published site locally:
OPEN223_MODEL_BASE_URL=https://models.open223.info ./build_examples.shWhile a local build runs, examples/*.md point at your checkout and carry a
"Preview build" warning admonition; so do branch and pull request builds, whose
pages validate a copy that is not going to be published. A main build is
building exactly what is about to be published, so it carries no such note.
Only the published URL belongs in the repository, so three things keep the others out:
./build_examples.shrestores the published URL when it exits, however it exits. A finished local build leaves no URL churn ingit status.make install-hooks(once per clone) points git at.githooks/, whosepre-commithook rejects a commit that stages a non-published URL.make check-model-urlsfails the CI build if one was committed anyway.
To fix the pages by hand at any point, run make publish-urls: it regenerates
them with https://models.open223.info, keeping real content changes such as
query tables and component counts. (git checkout -- examples/ also works, but
throws those away too.)
Nothing rewrites these files on merge: CI regenerates the pages in its own
workspace and publishes only the built HTML, so a committed file:// URL stays
in the repository until someone regenerates it.