Core Tutorial¶
This interactive notebook walks you through the core features of idfkit, a fast and modern Python toolkit for parsing, creating, and manipulating EnergyPlus IDF and epJSON files. It is organized into three parts:
| Part | What you will learn |
|---|---|
| Basic | Creating documents, adding objects, reading/writing files |
| Advanced | Reference tracking, validation, format conversion, schedule management |
| Expert | 3D geometry, schema introspection, coordinate transforms, reference graph analysis |
For Weather and Simulation features, see:
import idfkit
print(f"idfkit version: {idfkit.__version__}")
idfkit version: 0.1.0
Part 1 — Basic Usage¶
This section covers the everyday operations that every idfkit user needs to know: creating a new EnergyPlus document from scratch, adding objects, reading their fields, and writing the result to IDF or epJSON files.
1.1 Creating a new document¶
Use new_document() to create an empty EnergyPlus model.
You can specify the target EnergyPlus version as a tuple.
from idfkit import new_document
model = new_document(version=(24, 1, 0))
print(model)
print(f"Version : {model.version}")
print(f"Objects : {len(model)}")
IDFDocument(version=24.1.0, objects=0) Version : (24, 1, 0) Objects : 0
1.2 Adding objects¶
Every EnergyPlus object has a type (e.g. "Zone") and a name.
Field data can be passed as a dictionary, as keyword arguments, or both.
# Add a Building object
model.add(
"Building",
"My Office Building",
{
"north_axis": 0,
"terrain": "City",
"loads_convergence_tolerance_value": 0.04,
"temperature_convergence_tolerance_value": 0.4,
"solar_distribution": "FullInteriorAndExterior",
"maximum_number_of_warmup_days": 25,
},
)
# Add zones using keyword arguments
model.add("Zone", "Office_Zone", x_origin=0.0, y_origin=0.0, z_origin=0.0)
model.add("Zone", "Corridor_Zone", x_origin=10.0, y_origin=0.0, z_origin=0.0)
print(model)
IDFDocument(version=24.1.0, objects=3) Building: 1 objects Zone: 2 objects
1.3 Accessing collections and objects¶
Objects are organized into collections by type. You can access them in two ways:
- Bracket notation:
model["Zone"]— uses the exact EnergyPlus type name. - Attribute notation:
model.zones— uses a Python-friendly alias.
# Bracket notation
zones = model["Zone"]
print(f"Number of zones: {len(zones)}")
# Attribute notation (shorthand)
zones_alt = model.zones
print(f"Same collection? {zones is zones_alt}")
# Get a specific object by name
office = model["Zone"]["Office_Zone"]
print(f"Zone name: {office.name}")
print(f"Zone type: {office.obj_type}")
Number of zones: 2 Same collection? True Zone name: Office_Zone Zone type: Zone
1.4 Reading and writing object fields¶
Fields are accessed as Python attributes using snake_case names.
# Read field values
print(f"X origin: {office.x_origin}")
print(f"Y origin: {office.y_origin}")
# Modify a field
office.x_origin = 5.0
print(f"Updated X origin: {office.x_origin}")
# Access via index (0 = name, 1+ = fields)
print(f"Name via index: {office[0]}")
X origin: 0.0 Y origin: 0.0 Updated X origin: 5.0 Name via index: Office_Zone
1.5 Iterating over objects¶
Collections are iterable. You can also iterate over the entire document.
# Iterate over a collection
for zone in model["Zone"]:
print(f" Zone: {zone.name} at ({zone.x_origin}, {zone.y_origin}, {zone.z_origin})")
# Iterate over all objects in the document
print(f"\nAll objects ({len(model)}):")
for obj in model.all_objects:
print(f" {obj.obj_type}: {obj.name}")
Zone: Office_Zone at (5.0, 0.0, 0.0) Zone: Corridor_Zone at (10.0, 0.0, 0.0) All objects (3): Building: My Office Building Zone: Office_Zone Zone: Corridor_Zone
1.6 Writing to IDF and epJSON formats¶
Call write_idf() or write_epjson() with no file path to get a string,
or pass a path to write directly to disk.
from idfkit import write_epjson, write_idf
# Get IDF as a string
idf_string = write_idf(model)
print("=== IDF output (first 600 chars) ===")
print(idf_string[:600])
print("...")
=== IDF output (first 600 chars) === !-Generator archetypal !-Option SortedOrder Version, 24.1; !- Version Identifier Building, My Office Building, !- Name 0, !- North Axis City, !- Terrain 0.04, !- Loads Convergence Tolerance Value 0.4, !- Temperature Convergence Tolerance Value FullInteriorAndExterior, !- Solar Distribution 25, !- Maximum Number Of Warmup Days ; !- Minimum Number Of Warmup Days Zone, Office_Zone, !- Name ...
# Get epJSON as a string
epjson_string = write_epjson(model)
print("=== epJSON output (first 600 chars) ===")
print(epjson_string[:600])
print("...")
=== epJSON output (first 600 chars) ===
{
"Version": {
"Version 1": {
"version_identifier": "24.1"
}
},
"Building": {
"My Office Building": {
"north_axis": 0,
"terrain": "City",
"loads_convergence_tolerance_value": 0.04,
"temperature_convergence_tolerance_value": 0.4,
"solar_distribution": "FullInteriorAndExterior",
"maximum_number_of_warmup_days": 25
}
},
"Zone": {
"Office_Zone": {
"x_origin": 5.0,
"y_origin": 0.0,
"z_origin": 0.0
},
"Corridor_Zone": {
"x_origin": 10.0,
"y_origin": 0.0,
"z_origin": 0.0
}
}
}
...
1.7 Loading an existing IDF file¶
Use load_idf() to parse an existing .idf file. Here we write our model
to a temporary file and then reload it to demonstrate the round-trip.
import tempfile
from pathlib import Path
from idfkit import load_idf
# Write to a temp file, then reload
with tempfile.NamedTemporaryFile(mode="w", suffix=".idf", delete=False) as f:
f.write(idf_string)
temp_path = Path(f.name)
reloaded = load_idf(str(temp_path))
print(reloaded)
print(f"Zones after reload: {len(reloaded['Zone'])}")
temp_path.unlink() # clean up
IDFDocument(version=24.1.0, objects=3) Building: 1 objects Zone: 2 objects Zones after reload: 2
1.8 Removing and copying objects¶
# Add a new zone, then remove it
temp_zone = model.add("Zone", "Temporary_Zone", x_origin=20.0, y_origin=0.0, z_origin=0.0)
print(f"Added zone: {temp_zone.name}")
print(f"Total zones: {len(model['Zone'])}")
# Remove it
model.removeidfobject(temp_zone)
print(f"After removal: {len(model['Zone'])} zones")
Added zone: Temporary_Zone Total zones: 3 After removal: 2 zones
Part 2 — Advanced Usage¶
This section covers reference tracking, document validation, format conversion, filtering, and schedule management.
2.1 Building a realistic model¶
Let's build a more complete model so we can demonstrate reference tracking and validation. We will add materials, constructions, surfaces, schedules, and internal loads.
model = new_document(version=(24, 1, 0))
# Building
model.add("Building", "Demo Building", north_axis=0, terrain="Suburbs")
# Global geometry rules
model.add(
"GlobalGeometryRules",
"",
starting_vertex_position="UpperLeftCorner",
vertex_entry_direction="Counterclockwise",
coordinate_system="Relative",
)
# Zone
model.add("Zone", "Office", x_origin=0, y_origin=0, z_origin=0, multiplier=1, type=1)
# Material
model.add(
"Material",
"Concrete_200mm",
roughness="MediumRough",
thickness=0.2,
conductivity=1.73,
density=2243,
specific_heat=837,
thermal_absorptance=0.9,
solar_absorptance=0.65,
visible_absorptance=0.65,
)
model.add(
"Material",
"Insulation_50mm",
roughness="MediumSmooth",
thickness=0.05,
conductivity=0.04,
density=32,
specific_heat=830,
)
# Construction referencing the materials
model.add(
"Construction",
"Exterior_Wall",
outside_layer="Concrete_200mm",
layer_2="Insulation_50mm",
)
model.add("Construction", "Floor_Slab", outside_layer="Concrete_200mm")
model.add("Construction", "Flat_Roof", outside_layer="Concrete_200mm")
# Surfaces (simple 5m x 5m x 3m box)
model.add(
"BuildingSurface:Detailed",
"Wall_South",
surface_type="Wall",
construction_name="Exterior_Wall",
zone_name="Office",
outside_boundary_condition="Outdoors",
sun_exposure="SunExposed",
wind_exposure="WindExposed",
number_of_vertices=4,
vertex_1_x_coordinate=0,
vertex_1_y_coordinate=0,
vertex_1_z_coordinate=3,
vertex_2_x_coordinate=0,
vertex_2_y_coordinate=0,
vertex_2_z_coordinate=0,
vertex_3_x_coordinate=5,
vertex_3_y_coordinate=0,
vertex_3_z_coordinate=0,
vertex_4_x_coordinate=5,
vertex_4_y_coordinate=0,
vertex_4_z_coordinate=3,
)
model.add(
"BuildingSurface:Detailed",
"Wall_East",
surface_type="Wall",
construction_name="Exterior_Wall",
zone_name="Office",
outside_boundary_condition="Outdoors",
sun_exposure="SunExposed",
wind_exposure="WindExposed",
number_of_vertices=4,
vertex_1_x_coordinate=5,
vertex_1_y_coordinate=0,
vertex_1_z_coordinate=3,
vertex_2_x_coordinate=5,
vertex_2_y_coordinate=0,
vertex_2_z_coordinate=0,
vertex_3_x_coordinate=5,
vertex_3_y_coordinate=5,
vertex_3_z_coordinate=0,
vertex_4_x_coordinate=5,
vertex_4_y_coordinate=5,
vertex_4_z_coordinate=3,
)
model.add(
"BuildingSurface:Detailed",
"Wall_North",
surface_type="Wall",
construction_name="Exterior_Wall",
zone_name="Office",
outside_boundary_condition="Outdoors",
sun_exposure="SunExposed",
wind_exposure="WindExposed",
number_of_vertices=4,
vertex_1_x_coordinate=5,
vertex_1_y_coordinate=5,
vertex_1_z_coordinate=3,
vertex_2_x_coordinate=5,
vertex_2_y_coordinate=5,
vertex_2_z_coordinate=0,
vertex_3_x_coordinate=0,
vertex_3_y_coordinate=5,
vertex_3_z_coordinate=0,
vertex_4_x_coordinate=0,
vertex_4_y_coordinate=5,
vertex_4_z_coordinate=3,
)
model.add(
"BuildingSurface:Detailed",
"Wall_West",
surface_type="Wall",
construction_name="Exterior_Wall",
zone_name="Office",
outside_boundary_condition="Outdoors",
sun_exposure="SunExposed",
wind_exposure="WindExposed",
number_of_vertices=4,
vertex_1_x_coordinate=0,
vertex_1_y_coordinate=5,
vertex_1_z_coordinate=3,
vertex_2_x_coordinate=0,
vertex_2_y_coordinate=5,
vertex_2_z_coordinate=0,
vertex_3_x_coordinate=0,
vertex_3_y_coordinate=0,
vertex_3_z_coordinate=0,
vertex_4_x_coordinate=0,
vertex_4_y_coordinate=0,
vertex_4_z_coordinate=3,
)
model.add(
"BuildingSurface:Detailed",
"Floor",
surface_type="Floor",
construction_name="Floor_Slab",
zone_name="Office",
outside_boundary_condition="Ground",
number_of_vertices=4,
vertex_1_x_coordinate=0,
vertex_1_y_coordinate=0,
vertex_1_z_coordinate=0,
vertex_2_x_coordinate=0,
vertex_2_y_coordinate=5,
vertex_2_z_coordinate=0,
vertex_3_x_coordinate=5,
vertex_3_y_coordinate=5,
vertex_3_z_coordinate=0,
vertex_4_x_coordinate=5,
vertex_4_y_coordinate=0,
vertex_4_z_coordinate=0,
)
model.add(
"BuildingSurface:Detailed",
"Roof",
surface_type="Roof",
construction_name="Flat_Roof",
zone_name="Office",
outside_boundary_condition="Outdoors",
sun_exposure="SunExposed",
wind_exposure="WindExposed",
number_of_vertices=4,
vertex_1_x_coordinate=0,
vertex_1_y_coordinate=0,
vertex_1_z_coordinate=3,
vertex_2_x_coordinate=5,
vertex_2_y_coordinate=0,
vertex_2_z_coordinate=3,
vertex_3_x_coordinate=5,
vertex_3_y_coordinate=5,
vertex_3_z_coordinate=3,
vertex_4_x_coordinate=0,
vertex_4_y_coordinate=5,
vertex_4_z_coordinate=3,
)
# Schedules
model.add("ScheduleTypeLimits", "Fraction", lower_limit_value=0, upper_limit_value=1, numeric_type="Continuous")
model.add(
"ScheduleTypeLimits", "Activity_Level", lower_limit_value=0, upper_limit_value=1000, numeric_type="Continuous"
)
model.add("Schedule:Constant", "Always_On", schedule_type_limits_name="Fraction", hourly_value=1.0)
model.add("Schedule:Constant", "Activity_Schedule", schedule_type_limits_name="Activity_Level", hourly_value=120.0)
# People (references Zone and Schedule)
model.add(
"People",
"Office_People",
zone_or_zonelist_or_space_or_spacelist_name="Office",
number_of_people_schedule_name="Always_On",
number_of_people_calculation_method="People",
number_of_people=10,
activity_level_schedule_name="Activity_Schedule",
)
# Lights
model.add(
"Lights",
"Office_Lights",
zone_or_zonelist_or_space_or_spacelist_name="Office",
schedule_name="Always_On",
design_level_calculation_method="Watts/Area",
watts_per_floor_area=10.0,
)
print(model)
2.2 Reference tracking¶
idfkit automatically builds a reference graph as objects are added. This lets you instantly find every object that refers to a given name.
# What objects reference the "Office" zone?
refs = model.get_referencing("Office")
print(f"Objects referencing 'Office' ({len(refs)}):")
for obj in sorted(refs, key=lambda o: o.obj_type):
print(f" {obj.obj_type}: {obj.name}")
Objects referencing 'Office' (8): BuildingSurface:Detailed: Wall_North BuildingSurface:Detailed: Floor BuildingSurface:Detailed: Wall_East BuildingSurface:Detailed: Wall_West BuildingSurface:Detailed: Roof BuildingSurface:Detailed: Wall_South Lights: Office_Lights People: Office_People
# What objects reference the "Exterior_Wall" construction?
wall_refs = model.get_referencing("Exterior_Wall")
print(f"Surfaces using 'Exterior_Wall' construction ({len(wall_refs)}):")
for obj in wall_refs:
print(f" {obj.name}")
Surfaces using 'Exterior_Wall' construction (4): Wall_North Wall_East Wall_West Wall_South
# Reverse query: what does the People object reference?
people_obj = model["People"]["Office_People"]
referenced_names = model.get_references(people_obj)
print(f"'Office_People' references: {referenced_names}")
'Office_People' references: {'ALWAYS_ON', 'OFFICE'}
2.3 Renaming with automatic reference updates¶
When you rename an object, idfkit updates every field across the document that pointed to the old name. There are two ways to rename:
- Direct assignment — set
.nameon the object itself. model.rename()— look up the object by type and old name, then rename it.
Both approaches trigger the same update pipeline: the collection index, the reference graph, and every referencing field are kept in sync automatically.
# Demonstrate rename on a separate model so the main model stays clean
rename_demo = new_document()
rename_demo.add("ScheduleTypeLimits", "Fraction", lower_limit_value=0, upper_limit_value=1, numeric_type="Continuous")
rename_demo.add(
"ScheduleTypeLimits", "Activity_Level", lower_limit_value=0, upper_limit_value=1000, numeric_type="Continuous"
)
rename_demo.add("Schedule:Constant", "Always_On", schedule_type_limits_name="Fraction", hourly_value=1.0)
rename_demo.add(
"Schedule:Constant", "Activity_Schedule", schedule_type_limits_name="Activity_Level", hourly_value=120.0
)
rename_demo.add("Zone", "Kitchen")
rename_demo.add(
"People",
"Kitchen_Staff",
zone_or_zonelist_or_space_or_spacelist_name="Kitchen",
number_of_people_schedule_name="Always_On",
number_of_people_calculation_method="People",
number_of_people=3,
activity_level_schedule_name="Activity_Schedule",
)
rename_demo.add(
"Lights",
"Kitchen_Lights",
zone_or_zonelist_or_space_or_spacelist_name="Kitchen",
schedule_name="Always_On",
design_level_calculation_method="Watts/Area",
watts_per_floor_area=12.0,
)
people = rename_demo["People"]["Kitchen_Staff"]
lights = rename_demo["Lights"]["Kitchen_Lights"]
# --- Method 1: Direct .name assignment ---
zone = rename_demo["Zone"]["Kitchen"]
print("Before rename:")
print(f" People zone ref: {people.zone_or_zonelist_or_space_or_spacelist_name}")
print(f" Lights zone ref: {lights.zone_or_zonelist_or_space_or_spacelist_name}")
zone.name = "Staff_Kitchen"
print("\nAfter zone.name = 'Staff_Kitchen':")
print(f" People zone ref: {people.zone_or_zonelist_or_space_or_spacelist_name}")
print(f" Lights zone ref: {lights.zone_or_zonelist_or_space_or_spacelist_name}")
print(f" Lookup by new name: {'Staff_Kitchen' in rename_demo['Zone']}")
print(f" Lookup by old name: {'Kitchen' in rename_demo['Zone']}")
# --- Method 2: model.rename() (equivalent result) ---
rename_demo.rename("Zone", "Staff_Kitchen", "Main_Kitchen")
print("\nAfter model.rename('Zone', 'Staff_Kitchen', 'Main_Kitchen'):")
print(f" People zone ref: {people.zone_or_zonelist_or_space_or_spacelist_name}")
print(f" Lights zone ref: {lights.zone_or_zonelist_or_space_or_spacelist_name}")
2.4 Filtering collections¶
Use the filter() method on a collection to select objects matching a predicate,
or use getsurfaces() for built-in surface type filtering.
# Filter walls only
walls = model["BuildingSurface:Detailed"].filter(lambda s: getattr(s, "surface_type", "") == "Wall")
print(f"Walls: {[w.name for w in walls]}")
# Built-in surface type filter
roofs = model.getsurfaces("roof")
print(f"Roofs: {[r.name for r in roofs]}")
# Get all surfaces belonging to a zone via reference graph
zone_surfaces = model.get_zone_surfaces("Office")
print(f"All objects referencing 'Office': {len(zone_surfaces)}")
Walls: ['Wall_South', 'Wall_East', 'Wall_North', 'Wall_West'] Roofs: ['Roof'] All objects referencing 'Office': 8
2.5 Schedule management¶
Schedules are central to EnergyPlus models. idfkit provides cached lookup and can detect which schedules are actually used in the model.
# Look up a schedule by name
sched = model.get_schedule("Always_On")
print(f"Schedule: {sched.name} (type: {sched.obj_type})")
# Add an unused schedule
model.add("Schedule:Constant", "Unused_Schedule", schedule_type_limits_name="Fraction", hourly_value=0.0)
# Detect which schedules are actually referenced
used = model.get_used_schedules()
print(f"Used schedules: {used}")
all_schedules = set(model.schedules_dict.keys())
unused = all_schedules - used
print(f"Unused schedules: {unused}")
Schedule: Always_On (type: Schedule:Constant)
Used schedules: {'ALWAYS_ON'}
Unused schedules: {'UNUSED_SCHEDULE'}
2.6 Validation¶
validate_document() checks the model against the EpJSON schema:
required fields, field types, numeric ranges, and reference integrity.
from idfkit import validate_document
result = validate_document(model)
print(result)
print(f"\nValid: {result.is_valid}")
print(f"Total issues: {result.total_issues}")
print(f"Errors: {len(result.errors)}, Warnings: {len(result.warnings)}, Info: {len(result.info)}")
# Show first few issues if any exist
for issue in result.errors[:5]:
print(issue)
for issue in result.warnings[:5]:
print(issue)
[ERROR] People:'Office_People'.activity_level_schedule_name: Required field 'activity_level_schedule_name' is missing
# Validate only specific object types
zone_result = validate_document(model, object_types=["Zone"])
print(f"Zone validation: {zone_result.total_issues} issues")
Zone validation: 0 issues
2.7 Format conversion¶
idfkit can write the same document to both IDF (text) and epJSON (JSON) formats. Conversion helpers are also available for file-to-file transforms.
# Compare the two formats for the same Zone object
idf_out = write_idf(model)
epjson_out = write_epjson(model)
print(f"IDF size : {len(idf_out):,} characters")
print(f"epJSON size: {len(epjson_out):,} characters")
IDF size : 10,712 characters epJSON size: 6,183 characters
2.8 Deep copy for variant analysis¶
Use model.copy() to create an independent clone. Changes to the copy
will not affect the original.
# Create a variant with thicker insulation
variant = model.copy()
insulation = variant["Material"]["Insulation_50mm"]
insulation.thickness = 0.10 # 100mm instead of 50mm
# Original is unchanged
original_thickness = model["Material"]["Insulation_50mm"].thickness
variant_thickness = variant["Material"]["Insulation_50mm"].thickness
print(f"Original thickness: {original_thickness} m")
print(f"Variant thickness : {variant_thickness} m")
Original thickness: 0.05 m Variant thickness : 0.1 m
Part 3 — Expert Usage¶
This section covers 3D geometry operations, schema introspection, coordinate transforms, and reference graph analysis.
3.1 3D geometry — vectors and polygons¶
The Vector3D and Polygon3D classes provide basic 3D math without
any external geometry dependency.
from idfkit import Polygon3D, Vector3D
# Vector arithmetic
v1 = Vector3D(1.0, 0.0, 0.0)
v2 = Vector3D(0.0, 1.0, 0.0)
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 x v2 = {v1.cross(v2)}")
print(f"v1 . v2 = {v1.dot(v2)}")
print(f"|v1 + v2| = {(v1 + v2).length():.4f}")
print(f"normalize = {(v1 + v2).normalize()}")
print(f"rotate 90° = {v1.rotate_z(90)}")
v1 + v2 = Vector3D(x=1.0, y=1.0, z=0.0) v1 x v2 = Vector3D(x=0.0, y=0.0, z=1.0) v1 . v2 = 0.0 |v1 + v2| = 1.4142 normalize = Vector3D(x=0.7071067811865475, y=0.7071067811865475, z=0.0) rotate 90° = Vector3D(x=6.123233995736766e-17, y=1.0, z=0.0)
# Create a polygon from tuples
floor_polygon = Polygon3D.from_tuples([
(0, 0, 0),
(10, 0, 0),
(10, 8, 0),
(0, 8, 0),
])
print(f"Area : {floor_polygon.area:.1f} m²")
print(f"Centroid : {floor_polygon.centroid}")
print(f"Normal : {floor_polygon.normal}")
print(f"Horizontal?: {floor_polygon.is_horizontal}")
print(f"Vertical? : {floor_polygon.is_vertical}")
Area : 80.0 m² Centroid : Vector3D(x=5.0, y=4.0, z=0.0) Normal : Vector3D(x=0.0, y=0.0, z=1.0) Horizontal?: True Vertical? : False
3.2 Extracting surface geometry from the model¶
Use get_surface_coords() to read vertex data from any surface object
and calculate_surface_area() to compute its area.
from idfkit.geometry import (
calculate_surface_area,
calculate_zone_floor_area,
calculate_zone_volume,
get_surface_coords,
)
for surface in model["BuildingSurface:Detailed"]:
area = calculate_surface_area(surface)
coords = get_surface_coords(surface)
stype = surface.surface_type
print(f" {surface.name:12s} type={stype:5s} area={area:6.1f} m² vertices={coords.num_vertices}")
Wall_South type=Wall area= 15.0 m² vertices=4 Wall_East type=Wall area= 15.0 m² vertices=4 Wall_North type=Wall area= 15.0 m² vertices=4 Wall_West type=Wall area= 15.0 m² vertices=4 Floor type=Floor area= 25.0 m² vertices=4 Roof type=Roof area= 25.0 m² vertices=4
# Zone-level geometry aggregates
floor_area = calculate_zone_floor_area(model, "Office")
volume = calculate_zone_volume(model, "Office")
print(f"Office zone floor area: {floor_area:.1f} m²")
print(f"Office zone volume : {volume:.1f} m³")
Office zone floor area: 25.0 m² Office zone volume : 75.0 m³
3.3 Polygon transforms¶
Polygons can be translated and rotated.
wall_south = model["BuildingSurface:Detailed"]["Wall_South"]
poly = get_surface_coords(wall_south)
print(f"Original centroid: {poly.centroid}")
# Translate by (10, 20, 0)
shifted = poly.translate(Vector3D(10, 20, 0))
print(f"Shifted centroid : {shifted.centroid}")
# Rotate 45° around the centroid
rotated = poly.rotate_z(45)
print(f"Rotated centroid : {rotated.centroid}")
print("Rotated vertices :")
for v in rotated.vertices:
print(f" ({v.x:.2f}, {v.y:.2f}, {v.z:.2f})")
Original centroid: Vector3D(x=2.5, y=0.0, z=1.5) Shifted centroid : Vector3D(x=12.5, y=20.0, z=1.5) Rotated centroid : Vector3D(x=2.5, y=0.0, z=1.5) Rotated vertices : (0.73, -1.77, 3.00) (0.73, -1.77, 0.00) (4.27, 1.77, 0.00) (4.27, 1.77, 3.00)
3.4 Schema introspection¶
The EpJSONSchema class lets you programmatically discover what fields
an object type supports, which ones are required, what their types and
defaults are, and which fields are references to other objects.
schema = model.schema
# How many object types does EnergyPlus 24.1 define?
print(f"Total object types in schema: {len(schema)}")
print(f"First 10: {schema.object_types[:10]}")
Total object types in schema: 847
First 10: ['Version', 'SimulationControl', 'PerformancePrecisionTradeoffs', 'Building', 'ShadowCalculation', 'SurfaceConvectionAlgorithm:Inside', 'SurfaceConvectionAlgorithm:Outside', 'HeatBalanceAlgorithm', 'HeatBalanceSettings:ConductionFiniteDifference', 'ZoneAirHeatBalanceAlgorithm']
# Inspect the Zone object type
obj_type = "Zone"
field_names = schema.get_field_names(obj_type)
required = schema.get_required_fields(obj_type)
memo = schema.get_object_memo(obj_type)
print(f"--- {obj_type} ---")
print(f"Memo: {memo}")
print(f"Fields ({len(field_names)}):")
for fname in field_names:
ftype = schema.get_field_type(obj_type, fname)
default = schema.get_field_default(obj_type, fname)
is_ref = schema.is_reference_field(obj_type, fname)
req = "*" if fname in required else " "
print(f" {req} {fname:40s} type={ftype!s:8s} default={default!s:12s} ref={is_ref}")
--- Zone ---
Memo: Defines a thermal zone of the building. Every zone contains one or more Spaces. Space is an optional input. If a Zone has no Space(s) specified in input then a default Space named <Zone Name> will be created. If some surfaces in a Zone are assigned to a space and some are not, then a default Space named <Zone Name>-Remainder will be created. Input references to Space Names must have a matching Space object (default space names may not be referenced except in output variable keys).
Fields (12):
direction_of_relative_north type=number default=0.0 ref=False
x_origin type=number default=0.0 ref=False
y_origin type=number default=0.0 ref=False
z_origin type=number default=0.0 ref=False
type type=integer default=1 ref=False
multiplier type=integer default=1 ref=False
ceiling_height type=number default=Autocalculate ref=False
volume type=number default=Autocalculate ref=False
floor_area type=number default=Autocalculate ref=False
zone_inside_convection_algorithm type=string default=None ref=False
zone_outside_convection_algorithm type=string default=None ref=False
part_of_total_floor_area type=string default=Yes ref=False
# Check extensible types (e.g., Construction has a variable number of layers)
print(f"Construction extensible? {schema.is_extensible('Construction')}")
print(f"Construction extensible size: {schema.get_extensible_size('Construction')}")
print(f"Zone extensible? {schema.is_extensible('Zone')}")
Construction extensible? False Construction extensible size: None Zone extensible? False
3.5 Reference graph statistics and dangling reference detection¶
The ReferenceGraph powering get_referencing() and get_references()
can also report statistics and detect broken references.
# Reference graph stats
stats = model.references.stats()
for key, value in stats.items():
print(f" {key}: {value}")
total_references: 22 objects_with_references: 13 names_referenced: 8 object_lists: 0
# Introduce a dangling reference on purpose
bad_model = model.copy()
bad_model.add(
"People",
"Ghost_People",
zone_or_zonelist_or_space_or_spacelist_name="NonExistent_Zone",
number_of_people_schedule_name="NonExistent_Schedule",
number_of_people_calculation_method="People",
number_of_people=5,
activity_level_schedule_name="NonExistent_Activity",
validate=False, # Skip validation on add so we can demonstrate it below
)
# Validate to find the dangling references
bad_result = validate_document(bad_model)
print(f"Valid? {bad_result.is_valid}")
for err in bad_result.errors:
if "non-existent" in err.message.lower() or "reference" in err.message.lower():
print(f" {err}")
3.6 Object field metadata via schema¶
You can retrieve schema-level metadata for individual fields of an object, which is useful for building editors or automated reporting.
# What object lists does the People "zone_or_zonelist_or_space_or_spacelist_name" field accept?
obj_list = schema.get_field_object_list("People", "zone_or_zonelist_or_space_or_spacelist_name")
print(f"People zone field accepts: {obj_list}")
# What object types provide names for the first list?
if obj_list:
providers = schema.get_types_providing_reference(obj_list[0])
print(f"Types providing '{obj_list[0]}': {providers[:10]}")
People zone field accepts: ['SpaceAndSpaceListNames', 'ZoneAndZoneListNames'] Types providing 'SpaceAndSpaceListNames': ['Space', 'SpaceList']
3.7 Working with to_dict() for data analysis¶
Convert objects or collections to plain dictionaries and lists for integration with pandas, JSON APIs, or any other tooling.
# Single object to dict
zone_dict = model["Zone"]["Office"].to_dict()
print("Zone as dict:")
for k, v in zone_dict.items():
print(f" {k}: {v}")
# Entire collection to list of dicts
materials_list = model["Material"].to_dict()
print(f"\nMaterials ({len(materials_list)} items):")
for m in materials_list:
print(f" {m['name']} — thickness={m.get('thickness')} m")
Zone as dict:
name: Office x_origin: 0 y_origin: 0 z_origin: 0 multiplier: 1 type: 1 Materials (2 items): Concrete_200mm — thickness=0.2 m Insulation_50mm — thickness=0.05 m
3.8 Available schema versions¶
The SchemaManager can detect which EnergyPlus schema versions are
available — either bundled with idfkit or from local EnergyPlus installations.
from idfkit import get_schema_manager
manager = get_schema_manager()
versions = manager.get_available_versions()
print(f"Available schema versions: {versions}")
Available schema versions: [(8, 9, 0), (9, 0, 1), (9, 1, 0), (9, 2, 0), (9, 3, 0), (9, 4, 0), (9, 5, 0), (9, 6, 0), (22, 1, 0), (22, 2, 0), (23, 1, 0), (23, 2, 0), (24, 1, 0), (24, 2, 0), (25, 1, 0), (25, 2, 0)]
Summary¶
| Level | Feature | Key API |
|---|---|---|
| Basic | Create documents | new_document() |
| Basic | Add / remove objects | model.add(), model.removeidfobject() |
| Basic | Access fields | obj.field_name, obj["field"] |
| Basic | Read / write files | load_idf(), write_idf(), write_epjson() |
| Advanced | Reference tracking | model.get_referencing(), model.get_references() |
| Advanced | Rename with updates | obj.name = ..., model.rename() |
| Advanced | Validation | validate_document() |
| Advanced | Schedule management | model.get_schedule(), model.get_used_schedules() |
| Advanced | Deep copy | model.copy() |
| Expert | 3D geometry | Vector3D, Polygon3D |
| Expert | Surface area / volume | calculate_surface_area(), calculate_zone_volume() |
| Expert | Schema introspection | schema.get_field_names(), schema.get_field_type() |
| Expert | Reference graph | model.references.stats(), dangling ref detection |
Next Steps¶
- Weather Guide — Weather station search and design days
- Simulation Guide — Running EnergyPlus simulations