Skip to content

A simulation-ready IDF and an EnergyPlus runner (design note)

Status: Done (D-130; built 2026-10-11, not merged). Approved 2026-10-11 (the owner) · Date: 2026-10-11 · Decisions: D-016, D-023, D-038, D-064, D-111, D-112, D-124, D-126; new: D-130, ADR-019 · Branch: feature/idf-out (worktree D:\BEMGen\.claude\worktrees\idf-export-simulation-ea3c59, from main 3e46108) · Target version: 1.3.0 (the owner's call; see "Open for the owner")

This is one stage run as D-111 sets out: this note first; one test-first build on the branch; a review of the diff by a second agent; the headless Rhino check once; scripts/verify.ps1 before the merge; one decision-log entry and an ADR, because the stage supersedes part of D-112 and refines D-016. No component GUID changes. D-064 waives backward compatibility.

Numbering. D-128 and D-129 are taken by the kept, unmerged branches feature/program-popup-window and feature/program-popup-web (the owner, 2026-10-11: keep them as they are). This stage takes D-130 so that no number is used twice whichever branch merges first. ADR-016 exists only in the archive tag archive/archetype-data, so the next free ADR is ADR-019.

Stop line. The build ends with the branch complete: verified, reviewed, smoke-checked, its documentation synchronised. It does not merge, tag, push, export the public docs, or publish a release.

Decisions at a glance

Settled with the owner on 2026-10-11:

  1. Ground model: indoor minus 2 °C (Part B.4). The owner asked for the F-factor method if BEMGen has what it needs, otherwise this rule. The weather file's ground temperatures are there, but the F-factor is not: the envelope preset holds layers, and an F-factor depends on the slab-edge configuration (ASHRAE 90.1's tables), not on the layers alone. So the F-factor method waits for a sourced F-factor (D-023).
  2. Shading of unmodelled storeys is made at converter level (Part G), by both Convert2IDF and Convert2BEM.
  3. The GLOBAL.md rule is worded in the build, revised as needed (Part E).
  4. Unmodelled storeys cast shadows, a research rule change. Confirmed.
  5. The reference case and the metrics of Experiment outputs. Confirmed.
  6. EnergyPlus runs in opt-in tests and a C# check tool, never in the pipeline. Confirmed.
  7. Every non-convex outdoor surface is split into convex pieces. Confirmed.
  8. EnergyPlus 26.2, the Weather input, the research outputs, version 1.3.0 with no tag, F1 left as a finding, D-130 and ADR-019. Confirmed.

Problem

Convert2IDF writes a file that holds "the minimum to run once weather and outputs are chosen" (D-112, ADR-010), and EnergyPlus is never run. The next step on the roadmap is the first EnergyPlus run. The owner wants the exported IDF ready to simulate with the settings a study needs, and a way to run EnergyPlus end to end from the repository.

What a first run showed (2026-10-11, before this note)

EnergyPlus 26.2.0 (D:\EnergyPlusV26-2-0, build 4bd7a1f26f) is installed on the development machine with five TMY3 weather files. All 1073 IDFs the export tests write (BEMGEN_IDF_OUT) were run as they are with the Chicago O'Hare TMY3 file:

  • All 1073 completed (exit code 0). The same 1073 pass scripts/idd-check against the 26.2.0 IDD with 0 problems.
  • Warnings, by kind:
  • Version: in IDF="25.2" not the same as expected="26.2": every file.
  • Output:Meter: invalid Key Name="NATURALGAS:FACILITY": 1015 files, every building without gas equipment.
  • Entered Zone Floor Area(s) differ more than 5% from the sum of the Space Floor Area(s): 491 files, every Single Zone Building and Single Zone per Floor Type building. This is by design (ADR-010: a zone spanning storeys states its floor area). The run confirms that EnergyPlus uses the stated area. For example, the linear plan under Perimeter Core and Single Zone Building states 1728 m², EnergyPlus reports 1728 m², and lighting is 4.667 W/m² × 1728 m² = 8064 W. This settles the first of ADR-010's items "to verify on the first EnergyPlus run".
  • ... Flow Rate per Exterior Surface Area, but Exterior Surface Area = 0. 0 Infiltration will result.: 12 occurrences in 4 files, the core zones of Perimeter Core under the two area bases of infiltration. BEMGen's own design flow for those zones is 0 too (ZoneInfiltration.DesignFlow), so the result is right, but the warning is noise.
  • One severe error, 3 files: L1/CONDITIONED-1/F14 is a casting surface and is non-convex. Shadowing values may be inaccurate. This is the stair-bay bar under Conditioned Merge with Join Pieces and the upper storey moved so that it overhangs. The exposed floor of the overhang is non-convex and shades the roof below. ADR-010's "FullExterior does not need convex zones" holds for zones, not for casting surfaces.
  • The window construction named like its glazing material (ADR-010's second item) runs without a message.

So the files already run. "Simulation-ready" means the settings D-112 left open, a clean error file, and the runner.

The floor aggregators against the reference (2026-10-11, before this note)

The owner asked for one case per floor aggregator, with the results inspected to confirm the aggregators are implemented correctly.

Setup. The IDFs of four plans (the canonical linear plan with multipliers 1, 2, 1; the office plate 1, 3, 1; the enclosed court 1, 3, 1; the stepped band's three storeys) were taken under No Simplification through each of the four floor aggregators, with both heat-transfer settings. The research outputs of Part C were added by hand, and the files were run in EnergyPlus 26.2 with the Chicago TMY3 file. The metrics of Experiment outputs were computed against the reference (No Simplification, Stack, both options on) by a throwaway script, not committed: the build implements them in Lod.Simulation and repeats this inspection there. All 32 runs completed with no severe error.

Conservation holds through EnergyPlus. In every plan, under every aggregator and both settings:

  • the zones' floor area and volume times multiplier, as EnergyPlus reads them, equal the reference's (1728, 6300, 1404, and 8640 m²);
  • the hourly internal loads (lighting, equipment, hot water) equal the reference's at every hour, to within 6e-11 W.

Annual differences from the reference, heat-transfer options on (heating / cooling / total):

Plan Stacked Floor Zone Multiplier Single Zone per Floor Type Single Zone Building
Linear +0.5 % / +0.6 % / +0.3 % +25.7 % / +5.4 % / +8.3 % +26.1 % / +4.8 % / +8.2 %
Office plate +0.3 % / +0.4 % / +0.2 % −1.8 % / −0.7 % / −0.6 % −2.0 % / −1.3 % / −0.9 %
Stepped band +0.1 % / +0.8 % / +0.3 % +8.4 % / +0.4 % / +2.6 % +9.0 % / −0.1 % / +2.6 %
Enclosed court −2.9 % / +4.7 % / +0.9 % +8.7 % / +3.0 % / +3.4 % +9.2 % / +2.3 % / +3.3 %

Stack with both options off differs from the reference by −7.2 % heating (linear) and −9.6 % (court). Interzone heat transfer matters, which supports the owner's choice of reference.

Reading. Each difference has a designed cause, so the four aggregators behave as ADR-009 and ADR-013 specify:

  1. The single-zone aggregators condition every zone they merge. The linear plan's unconditioned stair (288 m²) is conditioned under Single Zone per Floor Type and Single Zone Building: 1440 m² conditioned becomes 1728 m². Heating rises by about a quarter, while the EUI per conditioned area moves less (25.8 → 27.0 kWh/m²). This is the rule the research brief §8 gives for Z3 across a floor, applied across storeys. The office plate, all conditioned, shows the plain geometry effect of one zone: within 2 %. It is why the experiment outputs report EUI per conditioned and per total area.
  2. Under Stacked Floor Zone Multiplier every slab between storeys is adiabatic, whatever EnableFloorHeatTransfer says (ADR-013 option a, D-075). The stepped band has no multiplier (every storey differs, so each is split off as × 1, D-046). Its Stacked Floor Zone Multiplier file differs from Stack only in its 28 floor and ceiling pieces between storeys (and its provenance line): Adiabatic in place of paired. That gives +0.8 % cooling with the options on, and results identical to Stack with them off. This is as designed, though not obvious when every multiplier is 1 (finding F1 below).
  3. Unmodelled storeys cast no shadow. Under Stacked Floor Zone Multiplier the court's storeys L1 and L3 are represented by L2 × 3 and not modelled, so the court is empty at their heights. In annual window-transmitted solar (Enclosure Windows Total Transmitted Solar Radiation Energy):
  4. L0 receives 44.4 MWh in place of 39.5;
  5. L2 receives 45.9 MWh in place of the 42.6 MWh mean of L1 to L3;
  6. in total the building receives 229.4 MWh in place of 214.6 MWh (+6.9 %).

This gives the +4.7 % cooling and −2.9 % heating. ADR-009 names this effect as part of the geometry effect under study and claims no engine behaviour. The run measures it: it is small where nothing shades (linear, office plate) and large in a court (finding F2 below).

Findings for the owner (not changed by this stage). They are geometry or research rules (GLOBAL.md scientific rule 7):

  • F1. Stacked Floor Zone Multiplier could pair the slabs between two modelled × 1 storeys that are adjacent in the assembly, as Stack does, so that the method differs from Stack only where it multiplies.
  • F2. The storeys a multiplied storey stands for could cast shadows: shading surfaces (Shading:Building:Detailed) at their walls, written by the converters, would restore the court's self-shading without modelling the zones.

A trial of F2 (2026-10-11, by hand, not committed). The 44 outdoor walls of the court's representative storey L2 were copied 3 m down and 3 m up, to where L1 and L3 stand, as 88 opaque Shading:Building:Detailed surfaces. The results against Stack (both options on):

Case Window solar Heating Cooling
Stack (reference) 214.6 MWh 206.7 MWh 276.3 MWh
Stacked Floor Zone Multiplier, empty gaps +6.9 % −2.9 % +4.7 %
the same with the 88 shading surfaces +0.1 % +0.1 % +0.9 %

L0's and L2's window solar return exactly to their Stack values (39.5 and 42.7 MWh). The 0.9 % cooling left over is the multiplier itself and the adiabatic slabs (F1).

The owner on F1 and F2 (2026-10-11): F1 stays a finding. F2 is taken up in this stage: unmodelled storeys cast shadows (Part G).

The owner's answers (2026-10-11)

  1. EnergyPlus 26.2 replaces 25.2 as the target.
  2. Weather by an EPW path input. The writer reads the file's header into the IDF and records the file. The weather file itself is passed to EnergyPlus at run time, as always.
  3. A research set of outputs: hourly facility meters, hourly ideal-loads heating and cooling energy per zone, hourly zone mean air temperature, the annual summary tables with unmet hours, and SQLite output.
  4. Revise the rules to allow (1) a C# runner that starts the installed EnergyPlus so that the stage can test end to end, and (2) later, reuse of the same code for a Grasshopper component. Part (2) is deferred, but the runner is written in C# so that it can be reused.
  5. Experiment outputs. The owner's specification "Experiment Output Variables" defines what is needed from the simulations, or aggregated after them. An integrated version goes into the docs: Experiment outputs.
  6. On that page (2026-10-11):
  7. the reference case is No Simplification + Stack with both heat-transfer options on;
  8. one case per floor aggregator, inspected (above);
  9. EUI per conditioned and per total floor area;
  10. the total load as the sum of the four facility meters (gas equipment included);
  11. hours above the reference's percentiles;
  12. both load-duration-curve measures;
  13. the building temperature over all zones and over conditioned zones;
  14. the metrics in Lod.Simulation.

Part A: EnergyPlus 26.2

  1. IdfDefaults.EnergyPlusVersion becomes 26.2. Every class the writer uses is compared field by field with the 26.2.0 IDD (D:\EnergyPlusV26-2-0\Energy+.idd), as S6 did for 25.2. The IDD check already passes, so only renamed defaults or new required fields can change the text. Every difference is logged.
  2. scripts/idd-check/README.md and convert2idf.md name the 26.2.0 IDD (installation, release assets, or idd/Energy+.idd.in at v26.2.0).
  3. The IDF header no longer says "BEMGen does not run it. Add a weather file and outputs before simulating (D-112)". It says the file targets EnergyPlus 26.2 and names the weather file it was written for, or says that none was given.

Part B: weather and site

  1. EpwHeader (Lod.Export.Idf, a plain parser over text, so tests need no file): it reads the LOCATION line (city, state or province, country, source, WMO number, latitude, longitude, time zone, elevation) and the GROUND TEMPERATURES line (depths and their twelve monthly values). It also reads the DATA PERIODS line to check that the data are hourly (one record per hour). Anything malformed is the error InvalidWeatherFile, which names the line and the field. Only the eight header lines are read, never the hourly records.
  2. IdfOptions.Weather (IdfWeather?: the header, the file name, and the SHA-256 of the file's bytes, computed by Convert2Idf when it reads the path). With a weather file, the IDF holds Site:Location from the LOCATION line, named <city> <state> <country>, and the header records the file name, its checksum, and the location (GLOBAL.md scientific rule 2). Without one, there is no Site:Location, EnergyPlus takes the location from the weather file it runs with, and the info NoWeatherFile says so. Provenance gains the line WEATHER:.
  3. The run period stays the building's calendar (ADR-018): a non-leap year and its weekday, with the weather file's holidays and daylight saving off, so that the hourly schedules stay on their days. EnergyPlus reads the weather file's hours in order whatever years it records, as TMY files mix years. No sizing: ideal loads have no capacity limit (D-112), so SimulationControl keeps sizing off and no design days are written. A future sizing run would read the .ddy beside the .epw.
  4. The ground model: a choice for the owner (decision 1). The owner judged ISO 13370:2017 more than needed (2026-10-11) and asked for the established ways. EnergyPlus offers four, two of which suit this study:
Method How it works in EnergyPlus Inputs Same at every level of detail?
F-factor (recommended) Construction:FfactorGroundFloor per ground surface (F-factor, area, exposed perimeter) with the boundary GroundFCfactorMethod. EnergyPlus builds an equivalent construction whose conductance is F × exposed perimeter, against Site:GroundTemperature:FCfactorMethod: the weather file's monthly ground temperatures at 0.5 m, written explicitly from the .epw header. This is the method of ASHRAE 90.1 Appendix G and of the DOE prototypes: the ASHRAE901_*_STD2019 example files shipped with EnergyPlus 26.2 use Construction:FfactorGroundFloor. an F-factor per envelope (W/m·K); each surface's exposed perimeter The total, yes: Σ F·P is F times the footprint's exposed perimeter, which every configuration keeps. The split between zones follows each zone's exposed perimeter: perimeter zones lose more, a core zone nothing. That is a real geometry effect, which the study is meant to measure.
Weather-derived Site:GroundTemperature:BuildingSurface, by the slab preprocessor GroundHeatTransfer:Slab (EnergyPlus's auxiliary Slab program) simulates an equivalent square slab of the footprint's area-to-perimeter ratio for a weather file and gives twelve monthly temperatures under the building. The envelope's ground-floor construction is kept. the footprint's area and perimeter; soil and slab properties; a preprocessor run per footprint and weather yes, one set per building
"Indoor minus 2 °C" The EnergyPlus documentation's rule of thumb for large slabs: the ground under a conditioned building is about 2 °C below the mean indoor temperature, for example 18 °C under 20 °C. an indoor temperature yes, but not weather-derived, so it ignores the climate (research brief §13)
Fixed 18 °C (today) What EnergyPlus assumes without input none yes, not weather-derived
Foundation:Kiva (for comparison) A two-dimensional numerical ground model per foundation slab and soil details no: per surface, and costly

A trial (2026-10-11, by hand, not committed). The linear plan's ground floors were rewritten as F-factor floors with F = 1.264 W/m·K (ASHRAE 90.1's unheated, uninsulated slab, F-0.73, as a placeholder), each piece's exposed perimeter measured on the footprint, and the default 0.5 m weather temperatures, then run with the Chicago weather file: - The ground pieces sum to the footprint's area and perimeter (432 m², 84 m) in every configuration. - The Perimeter Core core floor, with no exposed perimeter, is accepted without a warning. - Against the fixed 18 °C, heating rises by 10 to 11 % and cooling by about 5 % (the 0.5 m temperatures run from −3 °C to 23 °C in Chicago).

The owner's decision (2026-10-11): indoor minus 2 °C. The F-factor method was preferred if BEMGen has what it needs. It has the weather file's 0.5 m temperatures, but not an F-factor. An F-factor depends on how the slab edge is insulated (ASHRAE 90.1's tables) and cannot be derived from the envelope's layers alone, so the method waits for sourced envelope values (D-023). The rule as built: - The writer writes Site:GroundTemperature:BuildingSurface with twelve monthly values T_g,m = T_in,m − 2 °C. The envelope's ground-floor construction is kept, so both converters describe the ground floor alike. - T_in,m is the mean, over the hours of month m, of the building's hourly setpoint, weighted by floor area times multiplier over the conditioned zones. A zone's hourly setpoint is the midpoint of its heating and cooling setpoints where it has both, and the one it has otherwise. - The value is the same at every level of detail. Merges weight setpoints by floor area over the conditioned sources (ADR-005, ADR-018), and a merge that conditions an unconditioned zone gives it the merged setpoint. So the area-weighted mean over conditioned zones is the same before and after any merge. This is exact when every conditioned zone has the same sides (both, heating only, or cooling only). A merge of zones with different sides is reported in the build log if a test meets one. - A building without a conditioned zone uses T_in = 20 °C, and so 18 °C, the value used before, with the info GroundTemperatureDefault. - The values and the rule are recorded in the header and in provenance. The tests check the twelve values, their invariance across every simplifier and aggregator of the linear plan, and the case without a conditioned zone.

Part C: the research outputs

The outputs follow Experiment outputs: the owner's experiment output specification (2026-10-11), integrated with BEMGen and EnergyPlus 26.2. That page fixes what each case must yield, and from which EnergyPlus output each quantity comes. The first run showed that Heating:EnergyTransfer and Cooling:EnergyTransfer hold only the zones' sensible heat. The ideal loads' total heating and cooling go to Heating:DistrictHeatingWater and Cooling:DistrictCooling. DistrictHeatingWater:Facility also holds hot water (WaterSystems:DistrictHeatingWater), so heating comes from the end-use meter. They are written after the HVAC objects, in this order:

  1. Output:VariableDictionary,IDF; (so a reader can see what is available) and Output:SQLite,SimpleAndTabular;.
  2. OutputControl:Table:Style,CommaAndHTML; and Output:Table:SummaryReports,AllSummary;. AllSummary holds the Building Area table (total and net conditioned area, the normaliser of the experiment outputs; confirmed in a 26.2 run), the zone summary, and the hours a setpoint is not met.
  3. Output:Variable with key * at Hourly:
  4. Zone Mean Air Temperature, a raw output;
  5. as diagnostics, when some zone is conditioned: Zone Ideal Loads Zone Sensible Heating Energy, Zone Ideal Loads Zone Sensible Cooling Energy, Zone Ideal Loads Zone Total Heating Energy, and Zone Ideal Loads Zone Total Cooling Energy.
  6. Output:Meter at Hourly (was monthly), only for what the building uses:
  7. Heating:DistrictHeatingWater, Cooling:DistrictCooling, Heating:EnergyTransfer, Cooling:EnergyTransfer, DistrictHeatingWater:Facility, and DistrictCooling:Facility, with any conditioned zone;
  8. Electricity:Facility, with any lights or electric equipment;
  9. InteriorLights:Electricity, with lights;
  10. InteriorEquipment:Electricity, with electric equipment;
  11. NaturalGas:Facility and InteriorEquipment:NaturalGas, with gas equipment;
  12. WaterSystems:DistrictHeatingWater and DistrictHeatingWater:Facility, with hot water.

This removes warning 2. The meter names were confirmed in a 26.2 run's .mtd.

The frequency, variables, and meters live in IdfDefaults as one named set (IdfOutputs.Research), so that a later selectable set needs no new mechanism. The set is not a component input in this stage. The IDF header gains the identifying lines of the case record (experiment outputs §7): the building ID, the zoning configuration ID, and the weather file and its checksum. The runner reads them back.

Part D: a clean error file

  1. Infiltration where the basis has no area. A zone whose infiltration basis is per exterior surface area or per exterior wall area (D-124) and which has none of that area gets no ZoneInfiltration:DesignFlowRate object. Its design flow is 0 in BEMGen and in EnergyPlus alike, so nothing is lost. The info InfiltrationNotWritten names the zones, and the infiltration checks count one object per zone with a non-zero design flow. This removes warning 4.
  2. Non-convex outdoor surfaces are split into convex pieces (the owner, 2026-10-11: option b).
  3. Why. EnergyPlus projects every shading surface onto every receiving surface and clips the polygons, which is reliable only when the shading surface is convex. A non-convex caster is a severe error ("shadowing values may be inaccurate"), and a non-convex receiver a warning.
  4. What. Every surface written Outdoors that is not convex is split deterministically into convex pieces with the same total area, reported with the info SurfaceSplit, as floors with holes are split now: roofs (L, U, court, and hub footprints), exposed floors, any wall that is not convex, and the shading surfaces of Part G. Surfaces facing the ground, adiabatic surfaces, and surfaces between zones take no part in shading and stay whole. Windows are rectangles and are not affected.
  5. The cost. More surfaces for every non-rectangular footprint, and a longer shading calculation. Most IDF snapshots change in their roof surfaces. The areas, and with them every invariant and the infiltration, are unchanged.
  6. Zone floor area (warning 3) stays. It is the documented behaviour of zones spanning storeys and confirms the stated area is used. The runner lists it as an expected warning (Part E), with the reason.

Part E: the EnergyPlus runner (C#)

Rule change. D-016 keeps simulation out of the pipeline, and D-112 keeps EnergyPlus out of the tests. D-130 refines D-016 and supersedes D-112's "not run":

  • The pipeline still ends at the converters. A run is not a pipeline step: it takes a written IDF file and a weather file, never an IGeneratedBuilding, and changes no pipeline object.
  • A new library, Lod.Simulation, runs an installed EnergyPlus and reads what the run reports.
  • Tests may run EnergyPlus only when an installation is named (opt-in, below). Plain dotnet test still needs no EnergyPlus.

GLOBAL.md gains an architecture rule to that effect (proposed text: "7. Simulation is not a pipeline step. An EnergyPlus run, through Lod.Simulation, takes a written IDF and a weather file and changes no pipeline object; plain dotnet test never requires EnergyPlus."). AGENTS.md's boundary table gains a row.

  1. Lod.Simulation (netstandard2.0, so the deferred Grasshopper component can use it). It depends on Lod.Core (for Result and Diagnostic) and must not depend on RhinoCommon, Grasshopper, or Lod.Export.
  2. EnergyPlusInstallation.Locate(folder): checks energyplus.exe and Energy+.idd, and reads the version from the IDD's first line (!IDD_Version 26.2.0) without starting a process.
  3. EnergyPlusRunRequest(IdfPath, WeatherPath, OutputFolder, Timeout).
  4. IProcessRunner, injected (AGENTS.md: dependency injection for external services), with the default SystemProcessRunner. It takes a CancellationToken, so that a component can cancel later.
  5. EnergyPlusRunner.Run(request, token) → Result<EnergyPlusRun>: exit code, elapsed time, output folder, and the parsed reports. It never writes to the console (GLOBAL.md quality rule 7). A missing installation, IDF, or weather file, a timeout, or an output folder that exists and is not empty is a structured error.
  6. EnergyPlusErrorReport.Parse(text): each warning, severe, and fatal with its continuation lines, the summary counts, and whether the run completed. EnergyPlusZoneReport.Parse(eio): the Zone Information lines (name, multiplier, floor area, volume), so a test can compare EnergyPlus's zones with the building's.
  7. A version mismatch between the IDF and the installation is the warning EnergyPlusVersionMismatch.
  8. Raw outputs (experiment outputs §8), read from text files with the BCL only (no SQLite library, so the deferred component loads no native code):

    • EsoReader reads eplusout.mtr and eplusout.eso (data dictionary, then hourly records) into named hourly series of 8760 values;
    • EnergyPlusZoneReport reads eplusout.eio;
    • TabularReport reads the Building Area table of eplustbl.csv;
    • the runtime comes from eplusout.err.

    Together they make one SimulationResult: the hourly heating, cooling, and total load, the end-use meters, zone temperatures with zone areas and multipliers, total and conditioned floor area, the runtime and its conditions, and the case record. A meter that is not written reads as zero every hour, and a series that is not 8760 hours long is an error. - Metrics (experiment outputs §2 to §5; the owner placed them here): ExperimentMetrics, pure functions over SimulationResult, each with its zero guard tested: - the annual sums and EUI per conditioned and per total floor area; - the peaks and type 7 percentiles; - the hours above the reference's thresholds; - CV(RMSE), quantile NRMSE, and Kolmogorov–Smirnov; - the area-weighted temperature mean and spread, over all zones and over conditioned zones.

    The fixtures are small series with values worked by hand. The percentiles are also checked against NumPy's type 7 results, written into the test as literals. 2. The check tool scripts/eplus-check/ is a C# console project (net8.0, in BEMGen.sln so that the verify gate builds it) over Lod.Simulation. It replaces nothing. - dotnet run --project scripts/eplus-check -c Release -- --energyplus <folder> --weather <file.epw> <file.idf | folder>... runs every file, in parallel, into a results folder outside the repository. - It prints one line per file. It exits with 1 on any fatal, any severe, a run that did not complete, or a warning that is not on its list of expected warnings. That list starts with the zone floor area of zones spanning storeys (Part D.3), and each entry carries its reason. - Its README records the last run, as idd-check's does. 3. Tests tests/Lod.Simulation.Tests (net8.0, in verify.ps1 like the others): - Always: the parsers on short fixture texts written for the tests, the installation check on a temporary folder, and the runner with a fake IProcessRunner (arguments, timeout, cancellation, a missing file, a failed run). - End to end, opt-in. [EnergyPlusFact] (an xunit FactAttribute that sets Skip when BEMGEN_ENERGYPLUS does not name an installation; no new package) generates buildings, converts them with the installation's own Chicago weather file, runs them, and asserts: - the run completed, with 0 severe errors and only expected warnings; - the zones of the .eio equal the building's zones in name, multiplier, floor area, and volume; - the .sql and the tables were written; - the SimulationResult is complete (8760 hours in each series), and its end uses sum to its total load hour by hour; - the conditioned floor area and the multiplied zone areas equal the building's; - the hourly lighting and equipment meters are equal across every zoning configuration of one building, which checks GLOBAL.md scientific rule 1 through EnergyPlus. - The floor-aggregator set (the owner): for each of the four plans of the inspection above, the reference and No Simplification through each floor aggregator with both settings. These tests assert the conservation through EnergyPlus (multiplied areas and volumes, and hourly internal loads equal to the reference's at every hour, within the central load tolerance). They also write a metrics report (§2 to §5 of the experiment outputs) to a folder outside the repository for inspection. They assert no bound on heating or cooling differences: those are research results, not invariants (AGENTS.md rule 12). - Further coverage: the canonical linear plan through every simplifier and aggregator with both settings; the overhanging stair-bay bar of the severe error; and the Medium Office program JSON. Hourly results are not compared between levels of detail in this stage: that is the research itself, outside the repository (D-016).

Part F: the component

Convert2IDF gains one appended input, Weather (Wthr, text, item, optional): the fully qualified path of an .epw file. Without it the IDF has no Site:Location and the info NoWeatherFile appears. An unreadable or malformed file is an error, and no IDF is output. The appended-input guard of D-126 keeps saved definitions opening without Grasshopper's IO dialog. The GUID is unchanged. The headless Rhino spec convert-idf.py gains a run with the Chicago weather file and one without; the examples are rebuilt at the release, not in this stage. The Grasshopper run component is deferred (the owner).

Part G: unmodelled storeys cast shadows

The owner's decision (2026-10-11). Self-shading is enabled for the floor-multiplier approach. This revises the reading of finding F2 above: the storeys a multiplied storey stands for are not modelled as zones, but they cast shadows. It supersedes in part ADR-009's acceptance of the empty gaps as part of the geometry effect under study, and the research brief §11 (V4) changes with it.

  1. What casts. Under Stacked Floor Zone Multiplier, every storey of the assembled stack that is represented but not modelled (StackedFloorZoneMultiplier.UnmodelledStoreys, which the preview already draws grey) contributes its outdoor walls, at their true elevations, as opaque shading surfaces. Windows are part of the opaque surface: sun entering a window of an unmodelled storey does not reach another zone.
  2. An unmodelled storey never has an exposed floor or a roof, because such storeys are split off as × 1 (D-046). So walls are enough, and the shading surfaces of a group are exactly the faces that the empty gaps leave absent.
  3. The other aggregators model every storey and add no shading surface.
  4. Where it is made: at converter level (the owner, 2026-10-11). Both Convert2IDF and Convert2BEM write the shading; the building does not carry it, so no pipeline type changes.
  5. Shared rule. Both converters call one shared rule in Lod.Core.Conversion, as they share ConstructionRoles and HeatTransferOptions.Exported. UnmodelledStoreyShading.Of(building) assembles the stack from the building's floor entries (IGeneratedBuilding.Sources, Storeys.Assemble). It takes every storey of the stack that no zone of the building covers in height, and returns that storey's outdoor walls at their true elevation, with the IDs S{k}/<wall ID>.
  6. No special cases. The rule names no aggregator and reads no provenance text. Under Stack, Single Zone per Floor Type, and Single Zone Building every storey is covered, so it returns nothing. Under Stacked Floor Zone Multiplier it returns the unmodelled storeys.
  7. Outputs. Convert2IDF writes them as Shading:Building:Detailed. Convert2BEM gains an appended output, Shading (surfaces), for ClimateStudio. Both record the count and area in provenance.
  8. Checks.
  9. The shared rule's tests check that the shading area per unmodelled storey equals that storey's outdoor wall area in the assembly, and that no shading appears under the other aggregators. Validation does not change, because shading is not part of the building.
  10. The shading surfaces take no part in any zone invariant.
  11. The IDF parse-back checks that every shading surface is planar, simple, and convex after Part D.2.
  12. Evidence. The F2 trial above: the court's window solar returns to within 0.1 % of Stack, heating to +0.1 %, and cooling to +0.9 %. The end-to-end floor-aggregator set repeats the comparison with the shading the code writes.
  13. Records. D-130 records the rule change. ADR-013 gains a section, and ADR-009 is marked superseded in part. The research brief §11 V4 and the experiment outputs are updated in the same change (GLOBAL.md scientific rule 7).

Breaking changes (D-064)

  • Every IDF says 26.2. Meters are hourly and written only when used. New output objects are added.
  • Zones without the area their infiltration basis needs lose their zero infiltration object.
  • Every non-convex outdoor surface is split into convex pieces (Part D.2).
  • The ground temperatures are the building's monthly indoor setpoint minus 2 °C, not 18 °C.
  • Conversions of Stacked Floor Zone Multiplier buildings write shading surfaces for the unmodelled storeys: in the IDF, and in a new Shading output of Convert2BEM (Part G).
  • Every IDF snapshot changes in its version, header, and output lines. Those with non-rectangular roofs change in their roof surfaces, and the Stacked Floor Zone Multiplier snapshot gains shading surfaces. Load and schedule lines do not change. The minimal snapshot is reviewed line by line, and each changed geometry line is explained in the build log.

Tests first

The build writes the tests of each part before its code:

  • the EpwHeader cases (valid; a missing or short LOCATION; a non-numeric latitude; latitude and longitude out of range; data that are not hourly);
  • Site:Location present and absent;
  • the meters by use (a building without gas has no gas meter; one with hot water has the water meter);
  • the outputs in order;
  • infiltration not written and its invariant;
  • the convex split of outdoor surfaces (the same area, outward-facing, every piece convex);
  • the monthly ground temperatures (the values, their invariance across simplifiers and aggregators, and a building without a conditioned zone);
  • the shading surfaces of unmodelled storeys (count, area per storey equal to its outdoor wall area, IDs, none under the other aggregators) and their output on both converters;
  • the runner's parsers and fakes;
  • the end-to-end set.

scripts/idd-check runs on every file the tests write, against the 26.2.0 IDD, and scripts/eplus-check runs on all of them with the Chicago weather file.

Documentation

  • Experiment outputs (new, from the owner's specification), linked from the research brief §14.
  • ADR-019 (simulation-ready IDF and the runner). It amends ADR-010's scope, settles ADR-010's items "to verify on the first EnergyPlus run", and supersedes D-112 in part.
  • D-130, which records the rule changes: the runner (refining D-016, superseding D-112 in part), the shading of unmodelled storeys (superseding ADR-009 in part), and the ground model.
  • ADR-013 gains a section on the shading surfaces, and ADR-009 is marked superseded in part. The research brief §11 V4 and §8 Z5 describe the shading. envelope-presets.md records that the F-factor method waits for a sourced F-factor.
  • convert2idf.md: version, weather, outputs, infiltration, splitting, the new diagnostics, the IDD check record, and the first-run record.
  • New docs/architecture/energyplus-runner.md.
  • READMEs for scripts/eplus-check/ and scripts/idd-check/.
  • AGENTS.md: the boundary table, the commands, the test projects, and the project state.
  • GLOBAL.md: the new rule.
  • The roadmap progress log, the README in a line, and the smoke-test checklist.
  • The stage closes with sync-docs.

Open for the owner

Nothing blocks the build: see Decisions at a glance.

Build log

The build ran on 2026-10-11, unsupervised after the owner's go-ahead.

Parts. Three parts were built in parallel worktrees from ad3fe17 by separate agents, each test-first, each passing scripts/verify.ps1 on its own branch. They were then rebased one after another onto feature/idf-out, and the verify gate was run after each:

  • runner: Lod.Simulation, its tests, and scripts/eplus-check; eight commits.
  • shading: the shared rule UnmodelledStoreyShading, the convex split IdfConvex, the IDF shading objects, and Convert2BEM's Shading output; four commits.
  • writer: EnergyPlus 26.2, EpwHeader, Site:Location and the weather record, the ground rule, the research outputs, the case record, infiltration, and the Weather input; nine commits.

The end-to-end set followed on a fourth branch.

Findings and deviations.

  1. The ground rule is invariant only for uniform setpoints (writer). The rule as settled (Part B.4) gives the same monthly values at every level of detail when every conditioned program has the same setpoints, which is the case for the example presets (21/24 °C), and the invariance test passes under every simplifier and aggregator. Two cases differ:
  2. Uneven setpoints under Perimeter Core. Each perimeter zone takes the setpoint of its conditioned sources but is weighted by its whole floor area, which now includes part of the unconditioned stair. With dwellings at 20/26 °C and a corridor at 16/28 °C, January gives 20.880 °C against 20.889 °C in the reference. A test records this case.
  3. Merges of zones with different sides, a heating-only corridor with dual-setpoint dwellings: 20.889 °C against 20.444 °C.

The note's claim of exact invariance (written before the build) held for one merged zone, not for several zones that each take in unconditioned area. A fix would weight each zone by the floor area of its conditioned sources rather than its whole area. That changes the settled rule, so it is left to the owner. Both cases are written up in convert2idf.md. 2. Minimum warm-up days (writer). The writer writes 6 and called it "the EnergyPlus default", but the 26.2 IDD default is 1. The value is kept, so results do not change, and the description is corrected. Changing it is the owner's call. 3. Every other class matches the 26.2 IDD in field names, order, minimum fields, and keys, the six new classes included (writer). 4. The case record's zoning line leaves out the aggregator's Multipliers (writer). The floor counts are in the building line, so every zoning of one building shares one building line. 5. The convex split (shading). From a reflex vertex, the polygon is cut along the extension of one of its edges to the first boundary point; the shortest valid cut wins, and ties go to ring order. Each cut removes a reflex vertex and adds none, so there are at most r + 1 pieces, and rectilinear shapes give rectangles. Original vertices are kept exactly. A cut through a window is skipped, and each window is written on the piece that contains it; if every cut crosses a window, the error is WindowBlocksConvexSplit. Walls are rectangles in every test building, so no wall is split, and the window rule is covered by unit tests on synthetic shapes. With the split disabled, 429 of 1243 export tests failed the new convexity check, L1/Conditioned-1/F14 among them. 6. No snapshot changed in its roofs (shading). The snapshot plans have no non-convex outdoor surface, so the note's expectation that most snapshots would change in their roofs did not hold. All six IDF snapshots changed in their header, version, ground, and output lines, and the Stacked Floor Zone Multiplier snapshot gained eight shading objects (the L1 walls, 252 m²). No load, schedule, or zone geometry line changed. 7. Shading names and IDs (shading). Shading names share the heat-transfer surfaces' name group (IdfNameKind.Surface), as the IDD puts both in AllShadingAndHTSurfNames. The IDs are S{k}/ plus the floor's own wall ID, for example S1/US1/W2. 8. ADR-010 said "non-convex surfaces stay as they are" (shading). ADR-019 supersedes it. 9. Line ends in the runner's tests (controller). Six parser tests built fixture edits from " " inside raw string literals and failed when the source was checked out with CRLF line ends, which a rebase does under core.autocrlf. The fixtures are now made LF on declaration and pass with either checkout (cac6e3e). The parsers themselves strip , as EnergyPlus writes CRLF on Windows. 10. The .eio file prints areas and volumes with two decimals (runner). The end-to-end comparison rounds the building's values to two decimals before applying ToleranceSettings.AreaEquals. No tolerance was widened. 11. Diagnostic codes (runner). Thirteen runner codes and the writer's and shading's codes were added to DiagnosticCodes; merging them needed only conflict resolution, with no renaming.

Checks so far. On the combined branch (afd75ff), scripts/verify.ps1 passed with 0 warnings: 629 core, 518 generator, 160 program JSON, 1379 export, 2726 integration, and 131 simulation tests (one opt-in end-to-end test skipped). The parts' own checks:

  • IDD check against 26.2: 1074 files (writer) and 1079 files (shading), 0 problems.
  • EnergyPlus 26.2 with the Chicago file: eight files (writer) and 58 files (shading, with extra warnings on). Every run had 0 severe errors, and there were no convexity messages, no version or gas-meter warnings, and no zero-area infiltration warnings. The only warning left is the expected zone floor area of zones spanning storeys.

End-to-end set (fourth part, e3836b8, 5c566bc). There are 110 EnergyPlus cases, opt-in through BEMGEN_ENERGYPLUS:

  • the canonical linear plan through every simplifier and aggregator with both heat-transfer settings (56);
  • the floor-aggregator set of the four plans (24 more);
  • the overhanging stair-bay bar with both settings (2);
  • the Medium Office program JSON on the office plate through every simplifier and aggregator (28).

Every case completed with 0 severe and 0 fatal errors and only the expected warning, and every check held:

  • the .eio zones equal the building's in name, multiplier, area, volume, and conditioning;
  • 8760 hours in every series;
  • the end uses sum to the total load every hour within LoadEquals;
  • the tables' total and conditioned areas equal the building's;
  • the case record matches;
  • the hourly internal loads equal the reference's every hour;
  • shading surfaces appear exactly under Stacked Floor Zone Multiplier with unmodelled storeys.

The .eio and the tables print two decimals, so each run is compared zone by zone with its own building rounded to two decimals; no tolerance was widened. After the review fixes, the set ran again: 144 of 144 tests in 88 s, 14 EnergyPlus processes at a time.

Floor aggregators against the reference, repeated with the code's own outputs and shading. Each cell gives heating / cooling / total, in % against No Simplification + Stack with both options on:

Plan Stacked Floor Zone Multiplier, options on Single Zone per Floor Type, on Single Zone Building
Linear +0.52 / +0.32 / +0.24 +26.07 / +5.57 / +8.25 +26.28 / +5.07 / +8.12
Office plate +0.32 / +0.28 / +0.17 −1.75 / −0.70 / −0.57 −2.10 / −1.18 / −0.83
Enclosed court +0.09 / +0.58 / +0.24 +8.72 / +3.09 / +3.39 +9.03 / +2.55 / +3.28
Stepped band +0.15 / +0.33 / +0.16 +8.50 / +0.31 / +2.41 +8.86 / −0.07 / +2.38

With the shading of unmodelled storeys, the court under the multiplier moves from −2.9 % / +4.7 % (heating / cooling, the inspection before the build) to +0.09 % / +0.58 %. The full metrics of experiment outputs §2 to §5 are written by the tests as reports outside the repository. These are research results, and no bound is asserted.

Whole-matrix checks.

  • scripts/eplus-check (no version allowance): the 1080 IDFs the export tests write, 1080 passed. Its only warning is the expected zone floor area, in 491 files.
  • scripts/idd-check against the 26.2.0 IDD: the same 1080 files, 0 problems.

Review by a second agent (D-111), of 3e46108..26644d3. The code was found sound in its metrics, meters, parsers, convex split, shading, ground rule, infiltration, architecture boundaries, and appended Grasshopper parameters. The reviewer also ran a real case through the reader and the metrics: 115 eplus-check runs, all passed. Its findings, and what was done:

  1. Blocker: the end-to-end set was not yet on the branch. It was merged from the fourth part (above).
  2. Should-fix: a warning printed only in EnergyPlus's recurring error summary was never checked. Summary entries are now checked unless they repeat a warning printed earlier (68d5247, three tests).
  3. The zoning line carried the multiplier's Storeys, so it changed from building to building. Storeys is now left out like Multipliers (1f27b43, a test across floor counts; the one snapshot line changed).
  4. The case record did not read the code version. It now reads the header's first line (13b6643).
  5. The weather-mismatch warning still named ground temperatures. Fixed (13b6643).
  6. Only the started process is stopped on a timeout, not its children. Documented as a limit that the runner's options never meet (c1a691b).
  7. Timestep timestamps would have counted as hours. Hours are now counted only on timestamps spanning the hour (1089070, a test).
  8. Weather files were decoded as UTF-8 only. A file that is not valid UTF-8 is now read as ISO-8859-1 (898fb42, a test with an accented city in both encodings).

Headless Rhino check on 26644d3. All 16 specs ran to the end with every Validate True, no exception, no missing component, and no failed check. The weather scenarios of convert-idf behaved as specified, Convert2BEM's Shading held surfaces only under the multiplier, all 20 example definitions saved with 1.2.0 reopened and solved, and the spec's 35 IDFs passed the 26.2 IDD check (smoke-test checklist, "Result (simulation-ready IDF)").

Final checks on 5c566bc, before the version commit 5939ac2. scripts/verify.ps1 passed with 0 warnings: 629 core, 518 generator, 160 program JSON, 1382 export, 2726 integration, and 136 simulation tests, with the 5 opt-in EnergyPlus tests skipped. With BEMGEN_ENERGYPLUS set, the simulation tests passed 144 of 144.

Open for the owner.

  • The ground rule's invariance under uneven setpoints (item 1).
  • The minimum warm-up days, 6 against the IDD default of 1 (item 2).
  • The F-factor method, once a sourced F-factor exists.
  • F1.
  • The Grasshopper run component (deferred).
  • The merge, the tag, the push, the docs export, and the release (stop line).

Open for a person (D-058). The Weather input and the Shading output on the canvas and in the viewport, a 1.2.0 definition in the editor, and the toolbar look.

The owner's answers to the build (2026-10-11):

  1. Item 1, the ground rule. No change. The mapping from zone setpoints to ground temperature is fixed, and that is all that is wanted. The values may differ between zonings, and characterising each zoning method and its resulting setpoints in every respect is the point of the study. The two cases stay as characterisation tests.
  2. Item 2, warm-up days. They stay 6.
  3. Set aside for now: the F-factor method, F1, and the run component.
  4. Tag: v1.3.0 is set on the stage's last commit, with no merge and no push; the owner reviews the branch by hand.

Fresh-clone gate (2026-10-11). A clean clone of feature/idf-out at e84ab84 passed scripts/verify.ps1 with 0 warnings: 629 core, 518 generator, 160 program JSON, 1382 export, 2726 integration, and 136 simulation tests (5 opt-in skipped). The annotated tag v1.3.0 is on the next commit, which records this, and is local only.