Skip to content

Common tasks

Quick recipes for the operations you'll use every day, in both languages where both have them and with the difference stated where they do not. Each section is self-contained — jump to the one you need. If you're brand new to idfkit, work through Build your first model first, then come back here.

Load a Model

from idfkit import load_idf

# Load an existing IDF file
model = load_idf("building.idf")
print(f"Loaded {len(model)} objects")

# For migration-only tolerant loading of legacy/noisy files:
model = load_idf("legacy_building.idf", strict_parsing=False)
print(f"Tolerant load parsed {len(model)} objects")
import { loadIdf } from '@idfkit/core/node';
import type { TypeMap } from '@idfkit/types-v26-1';

// Load an existing IDF file
const doc = await loadIdf<TypeMap>('building.idf');
console.log(`Loaded ${doc.size} objects`);

// For migration-only tolerant loading of legacy or noisy files:
const legacy = await loadIdf<TypeMap>('legacy_building.idf', { strict: false });
console.log(`Tolerant load parsed ${legacy.size} objects`);

load_idf() uses strict parsing by default (strict_parsing=True) and raises IDFParseError for malformed objects. Use strict_parsing=False only as a migration/compatibility fallback for legacy or noisy files.

Query Objects

Access objects with O(1) dictionary lookups:

# Get all zones
for zone in model["Zone"]:
    print(f"Zone: {zone.name}")

# Get a specific zone by name
office = model["Zone"]["Office"]
print(f"Origin: ({office.x_origin}, {office.y_origin}, {office.z_origin})")
// Every zone
for (const zone of doc.all('Zone')) {
  console.log(`Zone: ${zone.name}`);
}

// One zone by name
const office = doc.require('Zone', 'Office');
console.log(`Origin: (${office.x_origin}, ${office.y_origin}, ${office.z_origin})`);

Modify Fields

Change field values with attribute assignment:

# Update a field
office.x_origin = 10.0

# See what references this zone
for obj in model.get_referencing("Office"):
    print(f"  {obj.obj_type}: {obj.name}")
// Update a field
office.x_origin = 10.0;

// See what references this zone
for (const obj of doc.references.referencingObjects('Office')) {
  console.log(`  ${obj.typeName}: ${obj.name}`);
}

Discover Available Fields

Not sure what fields an object type has? Use describe() to see all available fields:

# See all fields for a Zone
print(model.describe("Zone"))
# === Zone ===
# ...
# Fields (9):
#   direction_of_relative_north (number) [deg] default=0
#   x_origin (number) [m] default=0
#   ...

# See required fields for a Material
desc = model.describe("Material")
print(f"Required: {desc.required_fields}")
# Required: ['roughness', 'thickness', 'conductivity', 'density', 'specific_heat']
import { describeObjectType } from '@idfkit/core';

// Every field a Zone has
const zone = describeObjectType(doc.schema, 'Zone');
for (const field of zone.fields) {
  console.log(`  ${field.name} (${field.fieldType}) ${field.units ?? ''}`);
}

// The fields a Material cannot do without
const material = describeObjectType(doc.schema, 'Material');
console.log(
  'Required:',
  material.fields.filter((field) => field.required).map((field) => field.name)
);

In REPL/Jupyter, use tab completion to explore object fields:

>>> zone = model["Zone"]["Office"]
>>> zone.<TAB>
x_origin, y_origin, z_origin, multiplier, ...

Both libraries catch a misspelled field name. They catch it at different moments, and the difference is the one thing worth knowing about the two type systems: Python validates when the object is built, TypeScript when the file is compiled.

model.add("Zone", "Office", x_orgin=0)  # Raises: unknown field 'x_orgin'

# Disable validation for bulk operations where performance matters
model.add("Zone", "Office", x_origin=0, validate=False)
// @ts-expect-error x_orgin is a typo for x_origin
doc.add('Zone', 'Office', { x_orgin: 0 }); // caught by the compiler, before it runs

// Field names are checked against the schema for the version in the type map, so
// there is nothing to switch off and nothing to pay at run time.
doc.add('Zone', 'Office', { x_origin: 0 });

IDE Support

idfkit ships type stubs for all 858 EnergyPlus object types — your IDE will autocomplete field names, show inline documentation, and catch typos. See Type-Safe Development for details.

Create a New Model

from idfkit import new_document

# Create a baseline model for EnergyPlus 24.1
model = new_document(version=(24, 1, 0))

# Update pre-seeded singleton objects
building = model["Building"].first()
if building is not None:
    building.name = "My Building"
    building.north_axis = 0
    building.terrain = "City"

geometry_rules = model["GlobalGeometryRules"].first()
if geometry_rules is not None:
    geometry_rules.starting_vertex_position = "UpperLeftCorner"
    geometry_rules.vertex_entry_direction = "Counterclockwise"
    geometry_rules.coordinate_system = "Relative"

# Add additional named objects
model.add("Zone", "Office", x_origin=0, y_origin=0, z_origin=0)

# Add an additional unnamed singleton object
model.add("Timestep", number_of_timesteps_per_hour=4)
import { IdfDocument } from '@idfkit/core';
import { schemas } from '@idfkit/core/node';
import type { TypeMap } from '@idfkit/types-v26-1';

// A new model against one EnergyPlus release
const schema = await schemas().load('26.1.0');
const doc = new IdfDocument<TypeMap>(schema);

// Nothing is pre-seeded: add the singletons you want
doc.add('Version', null, { version_identifier: '26.1' });
doc.add('Building', 'My Building', { north_axis: 0, terrain: 'City' });
doc.add('GlobalGeometryRules', null, {
  starting_vertex_position: 'UpperLeftCorner',
  vertex_entry_direction: 'Counterclockwise',
  coordinate_system: 'Relative',
});

// Named objects
doc.add('Zone', 'Office', { x_origin: 0, y_origin: 0, z_origin: 0 });

// And unnamed singletons
doc.add('Timestep', null, { number_of_timesteps_per_hour: 4 });

Write Output

from idfkit import write_idf, save_idf, save_epjson

# Write to IDF format
save_idf(model, "output.idf")

# Or write to epJSON format
save_epjson(model, "output.epJSON")

# Get the text back instead
idf_string = write_idf(model)
import { writeIdf } from '@idfkit/core';
import { saveEpJson, saveIdf } from '@idfkit/core/node';

// Write IDF
await saveIdf(doc, 'output.idf');

// Or epJSON
await saveEpJson(doc, 'output.epJSON');

// Or take the text and decide where it goes yourself
const idfText = writeIdf(doc);

Run a Simulation

from idfkit.simulation import simulate

result = simulate(
    model,
    weather="weather.epw",
    design_day=True,  # Fast design-day run
)

print(f"Success: {result.success}")
print(f"Runtime: {result.runtime_seconds:.1f}s")

# Check for errors
if result.errors.has_fatal:
    for err in result.errors.fatal:
        print(f"Error: {err.message}")

Python only, permanently

Running a locally installed EnergyPlus and reading its results belongs to Python alone. JavaScript is not waiting on a port and will not gain a counterpart: this is a permanent boundary, so no issue tracks it, and moving the capability out of that state takes a constitutional amendment rather than a ledger edit.

Requires an EnergyPlus installation on the machine and a subprocess to drive it, then reads the eplusout files that run leaves on disk. Neither the installation nor the subprocess is available in a browser, which is the runtime the JavaScript library targets. JavaScript reaches EnergyPlus by the other mechanism instead: see browser-simulation, which is not a workaround for this entry but a different capability.

The full entry, including the vocabulary this capability owns, is on the capability parity page.

Query Results

# Get time-series data from SQLite output
ts = result.sql.get_timeseries(
    variable_name="Zone Mean Air Temperature",
    key_value="Office",
)
print(f"Variable: {ts.variable_name}")
print(f"Temperature range: {min(ts.values):.1f}°C to {max(ts.values):.1f}°C")

# Filter by environment if needed
ts_sizing = result.sql.get_timeseries(
    variable_name="Zone Mean Air Temperature",
    key_value="Office",
    environment="sizing",  # Design day data only
)

# Get tabular data
tables = result.sql.get_tabular_data(report_name="AnnualBuildingUtilityPerformanceSummary")

Python only, permanently

Running a locally installed EnergyPlus and reading its results belongs to Python alone. JavaScript is not waiting on a port and will not gain a counterpart: this is a permanent boundary, so no issue tracks it, and moving the capability out of that state takes a constitutional amendment rather than a ledger edit.

Requires an EnergyPlus installation on the machine and a subprocess to drive it, then reads the eplusout files that run leaves on disk. Neither the installation nor the subprocess is available in a browser, which is the runtime the JavaScript library targets. JavaScript reaches EnergyPlus by the other mechanism instead: see browser-simulation, which is not a workaround for this entry but a different capability.

The full entry, including the vocabulary this capability owns, is on the capability parity page.

Find Weather Stations

from idfkit.weather import StationIndex, geocode

# Load the station index (instant, no network needed)
index = StationIndex.load()

# Search by name
results = index.search("chicago ohare")
print(results[0].station.display_name)

# Find nearest station to an address
results = index.nearest(*geocode("Willis Tower, Chicago, IL"))
station = results[0].station
print(f"{station.display_name}: {results[0].distance_km:.0f} km away")
import { geocode, loadStationIndex } from '@idfkit/weather';

// The index is a file you serve; nothing is bundled into the library
const index = await loadStationIndex('/stations.json.gz');

// Search by name
const [match] = index.search('chicago ohare');
console.log(match?.station.displayName);

// Nearest to an address
const [latitude, longitude] = await geocode('Willis Tower, Chicago, IL');
const [nearest] = index.nearest(latitude, longitude);
console.log(`${nearest?.station.displayName}: ${nearest?.distanceKm.toFixed(0)} km away`);

Apply Design Days

from idfkit.weather import DesignDayManager

# Parse a DDY file and apply design days to your model
ddm = DesignDayManager("weather.ddy")
added = ddm.apply_to_model(
    model,
    heating="99.6%",
    cooling="1%",
    update_location=True,
)
print(f"Added {len(added)} design days")

Not in JavaScript yet

Design days and ASHRAE sizing conditions is available in Python and is not in JavaScript today. A temporary gap, not a boundary. The port is tracked in idfkit-js#20.

The full entry, including the vocabulary this capability owns, is on the capability parity page.

Lossless Round-Trip

Pass preserve_formatting=True to build a Concrete Syntax Tree (CST) so that write_idf and save_idf reproduce the original formatting, comments, and whitespace for unmodified objects:

from idfkit import load_idf, save_idf

# Build a CST to preserve original formatting
model = load_idf("building.idf", preserve_formatting=True)

# Modify a zone ceiling height
model["Zone"]["Office"].ceiling_height = 3.5

# Unmodified objects keep their original formatting, comments,
# and whitespace; only the changed object is re-serialised.
save_idf(model, "building_updated.idf")

Not in JavaScript yet

Formatting-preserving round-trip is available in Python and is not in JavaScript today. A temporary gap, not a boundary. The port is tracked in idfkit-js#12.

The full entry, including the vocabulary this capability owns, is on the capability parity page.

Next Steps