Build your first model¶
By the end of this you will have built an EnergyPlus model from nothing, watched the reference graph rewrite itself when you rename a zone, written the model to an IDF file, and read it back.
It takes about fifteen minutes, and the code is the same lesson in both languages. Every step below carries a Python tab and a TypeScript tab that do the same thing and print the same output, so pick your language once and follow one column down the page.
Before you start¶
You need one of the two libraries and nothing else: no EnergyPlus, no build step, no schema files to fetch. Both carry every supported EnergyPlus schema, 8.9.0 through 26.1.0, so nothing is downloaded while you work.
Python installs from PyPI with pip install idfkit, and How to install
idfkit covers the extras and the supported
interpreters. The TypeScript packages are not yet published under the shared
idfkit install name; until they are, work from a checkout of the idfkit-js
repository on Node 20 or newer.
Keep the code in one file as you go, build_model.py or build-model.ts, and
re-run it after each step. You do not need to know EnergyPlus: where the model
needs a number, this page gives you one.
Step 1: Create an empty model¶
Every model is bound to one EnergyPlus version, because field order genuinely
differs between releases. Pinning it is therefore the first thing you do, and
it is the one step where the two libraries ask for different work. Python
resolves the schema from the version you name and seeds the singleton objects
a model cannot do without (Version, Building, SimulationControl, and
GlobalGeometryRules). The TypeScript core cannot assume a filesystem, because
the same build has to run in a browser, a worker, and an edge runtime, so you
load the schema yourself, await it, hand it to the constructor, and add
Version by hand. Version takes null where every other object takes a
name, because it has no name field at all.
That string is the version in the form both libraries agree on. The underlying
value differs: Python's doc.version is the tuple (26, 1, 0), which sorts and
compares without a helper, and TypeScript's is the string '26.1.0', which is
what its schema keys already are. version_string() renders the Python tuple
into the shared form, and anything crossing between the two languages uses the
string.
Step 2: Add a zone¶
A zone is a volume of air EnergyPlus solves for. add takes the object type,
the name, and the fields, and hands back the object it created; its fields are
ordinary properties from there on.
Those field names are the epJSON names, spelled exactly as the schema spells
them: ceiling_height in both languages, not ceilingHeight, and not the
IDD's Ceiling Height. An editor can
complete them, from the shipped stubs in Python and from an opt-in type package
in TypeScript, which is Static types generated from the
schema.
Step 3: Add a wall, its construction, and its material¶
Three objects, each naming the next: the wall names a construction, the construction names a material. Add them in that order.
doc.add(
"Material",
"Brick 100mm",
roughness="MediumRough",
thickness=0.1,
conductivity=0.89,
density=1920,
specific_heat=790,
)
doc.add("Construction", "Exterior Wall", outside_layer="Brick 100mm")
wall = doc.add(
"BuildingSurface:Detailed",
"North Wall",
surface_type="Wall",
construction_name="Exterior Wall",
zone_name="Open Office",
outside_boundary_condition="Outdoors",
sun_exposure="SunExposed",
wind_exposure="WindExposed",
)
doc.add('Material', 'Brick 100mm', {
roughness: 'MediumRough',
thickness: 0.1,
conductivity: 0.89,
density: 1920,
specific_heat: 790,
});
doc.add('Construction', 'Exterior Wall', {
outside_layer: 'Brick 100mm',
});
const wall = doc.add('BuildingSurface:Detailed', 'North Wall', {
surface_type: 'Wall',
construction_name: 'Exterior Wall',
zone_name: 'Open Office',
outside_boundary_condition: 'Outdoors',
sun_exposure: 'SunExposed',
wind_exposure: 'WindExposed',
});
The wall has no shape yet. Its corners live in an extensible group, a section
of the object that repeats, which both libraries expose as a live list you add
rows to. Python reaches it under the name of the group, wall.vertices;
TypeScript reaches every group under one property, wall.extensible.
wall.vertices.extend([
{"vertex_x_coordinate": 0, "vertex_y_coordinate": 0, "vertex_z_coordinate": 2.7},
{"vertex_x_coordinate": 0, "vertex_y_coordinate": 0, "vertex_z_coordinate": 0},
{"vertex_x_coordinate": 5, "vertex_y_coordinate": 0, "vertex_z_coordinate": 0},
{"vertex_x_coordinate": 5, "vertex_y_coordinate": 0, "vertex_z_coordinate": 2.7},
])
print(len(wall.vertices))
wall.extensible.push(
{ vertex_x_coordinate: 0, vertex_y_coordinate: 0, vertex_z_coordinate: 2.7 },
{ vertex_x_coordinate: 0, vertex_y_coordinate: 0, vertex_z_coordinate: 0 },
{ vertex_x_coordinate: 5, vertex_y_coordinate: 0, vertex_z_coordinate: 0 },
{ vertex_x_coordinate: 5, vertex_y_coordinate: 0, vertex_z_coordinate: 2.7 }
);
console.log(wall.extensible.length);
That is a 5 m by 2.7 m wall, listed anticlockwise from the top left, which is
the order GlobalGeometryRules declares and EnergyPlus expects.
Step 4: Check that the model hangs together¶
Validation reads the model against the schema and reports what it finds, one record per finding. Nothing is raised: you get a result and decide what to do with it.
No errors means every required field is present and every name the model refers
to actually exists. Change Exterior Wall to Exteriar Wall on the wall and
run it again: both libraries report one error, coded E009, for a reference to
an object that does not exist. Then change it back. The codes are identical in
both languages and the human-readable messages are not, so match on the code.
Step 5: Rename the zone and watch the references follow¶
This is the part worth slowing down for. The wall names the zone, so renaming the zone would normally mean finding and fixing that name everywhere it appears. Instead, ask the document to do the rename.
The wall's zone_name changed and you never touched it. Both libraries keep a
live reference graph, so the rename moved an edge in that graph and rewrote
every field anywhere in the model that pointed at the old name. There is no
update() to call and no index to rebuild, so there is nothing to forget.
The two spellings differ where each language's receiver differs. Python hangs
the query on the document, doc.get_referencing(name), and returns a set;
TypeScript exposes the graph itself as doc.references and hangs the query
there, doc.references.referencingObjects(name), returning an array. Why
TypeScript can do this with real properties rather than a Proxy is Accessors,
not proxies.
Step 6: Write it to an IDF file¶
Writing is where the two libraries part on shape rather than on vocabulary.
Python's file API is synchronous, so save_idf returns when the file is on
disk. The TypeScript core is synchronous and pure so that it can run in a
browser, which pushes everything touching a disk into @idfkit/core/node, where
it is awaited: a synchronous core with async
edges. The verb is the same on both
sides.
Open office.idf in a text editor. The zone is there under its new name, and so
is the wall's Zone Name field further down. Notice the empty fields you never
set: IDF has no field names, the !- markers are comments, and a value's
meaning comes entirely from how many commas precede it. Those blanks hold
positions open, which matters most on the wall, where dropping one would shift
every vertex coordinate into the wrong slot. The hazards of a positional
format is the long version.
Step 7: Read it back¶
Parsing detects the version from the file and resolves the matching schema on its own, so reading takes no more setup than writing did.
from idfkit import load_idf
reread = load_idf("office.idf")
print(version_string(reread.version))
for z in reread["Zone"]:
print(f"{z.name}: ceiling {z.ceiling_height} m")
wall_again = reread["BuildingSurface:Detailed"]["North Wall"]
print(len(wall_again.vertices))
print(wall_again.vertices[0].vertex_z_coordinate)
import { loadIdf } from '@idfkit/core/node';
import type { TypeMap } from '@idfkit/types-v26-1';
const reread = await loadIdf<TypeMap>('office.idf');
console.log(reread.version);
for (const z of reread.all('Zone')) {
console.log(`${z.name}: ceiling ${z.ceiling_height} m`);
}
const wallAgain = reread.require('BuildingSurface:Detailed', 'North Wall');
console.log(wallAgain.extensible.length);
console.log(wallAgain.extensible[0].vertex_z_coordinate);
The vertices came back as the same rows you added. Collections are iterable in
both languages, and both offer a lookup that returns nothing on a miss and one
that fails on a miss. The spellings follow each language's habits: Python reads
a collection with the subscript operator, doc["Zone"]["Open Plan"], which
raises KeyError when the name is absent, while TypeScript needs a method and
names it for what it does, doc.require('Zone', 'Open Plan'). Where you would
rather have the absent value than an error, both spell it get.
What you built¶
You created a model, connected four objects by name, renamed one and watched the others follow, wrote real IDF, and parsed it back into the same objects. That is the whole object model. Everything else is more object types.
It is also the whole shared vocabulary. The names you used are the ones the two libraries agreed on, and where they differ, they differ because the languages do: a subscript against a method, a set against an array, a tuple against a string, a synchronous call against an awaited one. The naming map records every pair and the reason for each difference.
Where to go next¶
- How-to guides for the task you actually came here to do.
- Static types generated from the schema, so a misspelled field name becomes an editor error rather than a surprise at simulation time.
- Run the model you just built. In Python that is How to run a simulation, which drives a local EnergyPlus installation; in TypeScript it is How to run a simulation in the browser, which runs EnergyPlus compiled to WebAssembly.
- Simulate an office block, the next tutorial, which builds a real two-storey block instead of one wall and takes it all the way to a heating load. It needs a local EnergyPlus, so it is Python only.
- What each language has, before you plan work around a capability one of them does not carry yet.