Build your first model¶
By the end of this you will have built an EnergyPlus model in JavaScript from nothing, seen 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. You need Node 20 or newer and nothing else — no EnergyPlus installation, no build step, no TypeScript configuration.
You do not need to know EnergyPlus. Where the model needs a number, this page gives you one.
Step 1: Set up a project¶
mkdir first-model
cd first-model
npm init -y
npm pkg set type=module
npm install @idfkit/core @idfkit/schemas
While the packages are unpublished
idfkit-js is a prototype and nothing is on npm yet. Until it is, build the tarballs from a checkout and install those instead of the last line above:
git clone https://github.com/idfkit/idfkit-js.git
(cd idfkit-js && npm install && npx tsc --build \
&& npm pack -w @idfkit/schemas -w @idfkit/core --pack-destination ..)
npm install ./idfkit-schemas-0.0.0.tgz ./idfkit-core-0.0.0.tgz
Everything after this point is identical either way.
Step 2: Create an empty model¶
Every model is bound to one EnergyPlus version, because field order genuinely differs between releases. So the first thing to get is a schema.
Create build-model.mjs:
import { IDFDocument } from '@idfkit/core';
import { schemas } from '@idfkit/core/node';
const schema = await schemas().load('26.1.0');
const doc = new IDFDocument(schema);
doc.add('Version', null, { version_identifier: '26.1' });
console.log('objects:', doc.size);
Run it:
Two things to notice. schemas() returns the bundle that ships inside
@idfkit/schemas, holding all 17 EnergyPlus versions; loading one is the only
step in this whole tutorial that is asynchronous. And Version takes null
where the other objects will take a name, because it has no name field at all.
Step 3: Add a zone¶
A zone is a volume of air EnergyPlus solves for. Add one, just above the
console.log:
add returns the object, and its fields are ordinary properties. Add this at the
end of the file and run it again:
Those field names are the epJSON names, used exactly as the schema spells them:
ceiling_height, not ceilingHeight. If you are using an editor with
TypeScript support, it can complete all of them — that comes later, in
Static types generated from the
schema.
Step 4: Add a wall¶
A wall needs a construction, and a construction needs a material. Add all three, after the zone:
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 is a live array you push to:
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 }
);
That is a 5 m by 2.7 m wall, listed anticlockwise from the top left as EnergyPlus expects.
Step 5: Check the model hangs together¶
Three of those objects refer to each other by name: the wall names its
construction and its zone, the construction names its material. Replace the
console.log lines at the end with:
An empty array means every name that is referred to actually exists. Change
'Exterior Wall' to 'Exteriar Wall' in the wall and run it again to see what a
broken reference looks like, then change it back.
Step 6: Rename the zone¶
This is the part worth slowing down for. Add to the end of the file:
console.log('wall points at:', wall.zone_name);
zone.name = 'Open Plan';
console.log('after rename: ', wall.zone_name);
console.log(
'referencing Open Plan:',
doc.references.referencingObjects('Open Plan').map((o) => o.name)
);
The wall's zone_name changed and you never touched it. Assigning to .name
went through a real property setter, and that setter moved the edge in the
document's reference graph, rewriting 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. Why it works this way is
Why accessors and not a Proxy.
Step 7: Write it out¶
Add these two lines at the top of the file, next to the other imports:
and this at the very bottom:
Run it once more, then look at what you made:
Zone,
Open Plan, !- Name
, !- Direction of Relative North
, !- X Origin
, !- Y Origin
, !- Z Origin
, !- Type
1, !- Multiplier
2.7; !- Ceiling Height
Your zone is called Open Plan, and so is the wall's Zone Name further down
the file.
Notice the empty fields you never set. IDF has no field names — the !-
comments are comments, and a value's meaning comes entirely from how many commas
precede it. Those blanks are holding positions open. It 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 8: Read it back¶
New file, read-model.mjs:
import { loadIdf } from '@idfkit/core/node';
const doc = await loadIdf('office.idf');
console.log('version:', doc.version);
console.log('objects:', doc.size);
for (const zone of doc.all('Zone')) {
console.log(`${zone.name}: ceiling ${zone.ceiling_height} m`);
}
const wall = doc.require('BuildingSurface:Detailed', 'North Wall');
console.log('vertices:', wall.extensible.length);
console.log('first vertex:', wall.extensible[0]);
version: 26.1.0
objects: 5
Open Plan: ceiling 2.7 m
vertices: 4
first vertex: {
vertex_x_coordinate: 0,
vertex_y_coordinate: 0,
vertex_z_coordinate: 2.7
}
loadIdf read Version, 26.1; out of the file and resolved it to the bundled
26.1.0 schema on its own. Collections are iterable, require throws if the
object is missing where get would return undefined, and the vertices came
back as the same array of objects you pushed.
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 five objects.
That is the whole object model. Everything else is more object types.
Where to go next¶
- Turn on the generated static types so a misspelled field name becomes a compile error instead of a surprise.
- Run a simulation with the model you just built.
- Browse the how-to guides for the task you actually came here to do.