Skip to content

EnergyPlus runner

Status: Provisional (ADR-019 and D-130, written by the stage's controller; realisation pending the owner's reading) · Since: version 1.3.0 · Code: Lod.Simulation (src/Lod.Simulation/), tests tests/Lod.Simulation.Tests/, tool scripts/eplus-check/ · Design note: 2026-10-11-idf-out.md, Part E · Definitions: Experiment outputs

Purpose

Lod.Simulation runs an installed EnergyPlus on a written IDF and a weather file, reads what the run reports, and computes the experiment metrics. A run is not a pipeline step (D-130): it takes files, never an IGeneratedBuilding, and changes no pipeline object. The pipeline still ends at the converters.

  • The library is netstandard2.0, so that the deferred Grasshopper run component can use it. It depends on Lod.Core only (Result, Diagnostic, DiagnosticCodes) and never on RhinoCommon, Grasshopper, or Lod.Export.
  • It reads EnergyPlus's text files with the base class library only: no SQLite library and no native code.
  • It never writes to the console (GLOBAL.md quality rule 7) and never writes outside the run's output folder.
  • Plain dotnet test needs no EnergyPlus. Tests that run EnergyPlus are opt-in (see Tests).

Interface

Type Role
EnergyPlusInstallation.Locate(folder) Checks that the folder holds energyplus.exe and Energy+.idd, and reads the version from the IDD's first line (!IDD_Version 26.2.0) and the build from its second (!IDD_BUILD 4bd7a1f26f). No process is started.
EnergyPlusRunRequest(IdfPath, WeatherPath, OutputFolder, Timeout) One run. The output folder must be missing or empty; the timeout positive.
IProcessRunner, SystemProcessRunner Runs a process to its end, its timeout, or cancellation (CancellationToken), and stops it in the last two cases. Injected into the runner, so that tests use a fake (AGENTS.md, dependency injection). The default starts the program without a shell or window and discards its console output.
EnergyPlusRunner(installationFolder, processes).Run(request, token) Runs energyplus.exe -w <epw> -d <output folder> <idf> with full paths and the output folder as working folder, and returns Result<EnergyPlusRun>: the request, the installation, the exit code, the wall-clock time, the parsed eplusout.err, and the IDF's Version. Completed is exit code 0 and "EnergyPlus Completed Successfully".
SimulationResultReader.Read(run, conditions) Reads the raw outputs of a completed run into a SimulationResult. The caller gives the RunConditions (processor, logical cores, concurrency; RunConditions.Detect(concurrency) reads this machine).
SiteRecord.Read(idf, weather), SiteRecord.Create(bytes, bytes, name) The site record of experiment outputs §7 from the IDF header, and the site ID.
ExperimentMetrics The metrics of experiment outputs §2 to §5 and §9 as pure functions. Compute(variant, reference) takes the reference's whole result, or only its ReferenceLoads (hourly heating, cooling, and total, ReferenceLoads.Of(result)), the one part of a reference the metrics use, so that a reference read back from stored hourly series gives the same metrics (D-153).
ExpectedWarnings, RunVerdict.Of(run, expected) The warnings a BEMGen IDF may give, each with a pattern and a reason, and optionally a context line, a bound on a number of a context line, and a count limit per run or per key (D-187), and the verdict on a run: it passes when it completed with no fatal, no severe, only expected warnings, and no limit exceeded. RunVerdict.LimitedCounts gives the limited entries' counts, which the batch runner writes into site_status.message (expected warnings: OutdoorWetBulbIteration 1).
ResearchMeters.EndUses, ResearchMeters.Fuels (D-132) The end uses by fuel (EndUse, Fuel, meter) and the fuels with their facility meters: six end-use meters whose sums per fuel are the four facility meters, because BEMGen writes no fans and no exterior equipment.
EndUseClosure.Check(result, tolerances) (D-132) Checks every hour that the end uses of each fuel sum to its facility meter, and all end uses to the total load, within ToleranceSettings.LoadEquals; returns the largest residual, its hour, and the failing hours per fuel and for the total.
Batch.BatchInputs (D-132) Expand(inputs): files and folders into IDF files (a folder stands for its files with the extension .idf, in ordinal name order, so not *.idfx, which Windows' *.idf pattern also matches; a path given twice once); SiteNames(files): one name per file, the first free of -2, -3, ... for a name already taken. Shared by eplus-check and the batch runner.
Batch.SiteZoning (D-132) The zoning line of the site record split into simplifier, aggregator, and the two heat-transfer options; SiteZoning.Reference is the reference site's line, tied by a test to what IdfSiteRecord writes.
Batch.BatchPlan.Create(sites) (D-132) Groups sites into test cases by building, envelope, and weather SHA-256 from their site records, finds each test case's reference by its zoning line, and puts references in phase 1 and variants in phase 2; a site without a reference, an incomplete record, an unresolved or mismatched weather file, a duplicate, or an ambiguous reference is not run (NotRunReason). WithFailedReferences(names) drops the variants of the references that failed.
SiteRecord.RecordedWeatherFileName(idf) (D-132) The weather file name the IDF header records, so that a batch can look the file up in a folder.
Batch.BatchPlan.CreateForTestCases(testCases) (D-133) Plans the test cases a test suite declares, so that membership and references come from the suite's manifest and not from the IDF headers. Each site is still checked (record, weather file, site ID); site names must be unique across test cases. A test case whose reference is refused (it is not among the sites, cannot run, or has another zoning line than SiteZoning.Reference) runs no site (NoReferenceSite). References are phase 1 and variants phase 2, as in Create.
Batch.BatchTestCaseInput(TestCaseId, ReferenceName, Sites) (D-133) One test case of a suite: its ID, the name of its reference site, and its sites (BatchSiteInput), the reference among them.
BatchSiteInput.ExpectedSiteId (D-133) The site ID the suite recorded when it wrote the IDF, or null outside a suite. A site whose IDF and weather bytes give another ID is not run.
NotRunReason.ModifiedSinceWritten (D-133) The not-run reason for that case: the site's IDF or weather file is not the one its test suite wrote.

A run that did not complete is still a successful Result, with the warning EnergyPlusRunFailed, so that its error report can be read; SimulationResultReader.Read then refuses it with the error EnergyPlusRunFailed.

Parsers

Parser File What it reads
EnergyPlusErrorReport.Parse eplusout.err Each ** Warning **, ** Severe **, and ** Fatal ** with its continuation lines (** ~~~ **) and its line number; the entries of the recurring error summary, marked IsRecurringSummary; the summary counts and Completed from the last line; the elapsed time (Elapsed Time=00hr 00min 9.95sec); the version and build from the first line. Any text parses.
EsoReader.Parse, EsoReader.Read eplusout.eso, eplusout.mtr The data dictionary to End of Data Dictionary: report codes 1 to 6 are the environment and timestamp layouts; every other line is code,count,[key,]name [units] !frequency, and only !Hourly lines with one value are kept (a meter has no key). Then the records: a code-1 line opens an environment, a code-2 line is an hourly timestamp, and other codes are values. Only the run-period environment counts: an environment whose timestamps carry a design-day type (SummerDesignDay, WinterDesignDay) is a sizing period and is skipped, and warm-up days are never reported. No run period, or more than one, is an error. Keys and names are compared ignoring case, as EnergyPlus upper-cases keys. Hours are counted on the code-2 timestamps that span the whole hour (start minute 0, end minute 60), so the timestamps a timestep-frequency variable adds do not count as hours. Each hour's month, day, and hour are kept as EsoFile.Timestamps (D-132); a timestamp line whose month, day, or hour does not parse is a format error.
EnergyPlusZoneReport.Parse eplusout.eio The Zone Information lines, with the columns located by the names of the ! <Zone Information> header line: name, zone multiplier, zone list multiplier (the effective multiplier is their product), floor area, volume, and whether the zone is part of the total floor area. The file prints areas and volumes with two decimals.
EnergyPlusSurfaceReport.Parse eplusout.eio The surface details that Output:Surfaces:List, Details writes: each HeatTransfer Surface line with the zone of the Zone Surfaces line before it, and its class and ExtBoundCondition, the columns located by the names of the ! <HeatTransfer Surface> header line. Shading Surfaces and its Shading Surface lines are not heat-transfer surfaces. The ground surfaces are those on Ground or FCGround, EnergyPlus 26.2's name for GroundFCfactorMethod (experiment outputs §9). A file without the header is the error EnergyPlusOutputMissing, which names the output to request; a surface before any Zone Surfaces line is a format error.
TabularReport.ParseBuildingArea eplustbl.csv The Building Area table of the Annual Building Utility Performance Summary: Total Building Area and Net Conditioned Building Area.
TabularReport.ParseUnmetHours eplustbl.csv The unmet hours (D-153): the Facility row of the System Summary's Time Setpoint Not Met table, its columns During Heating [hr] and During Cooling [hr] located by their headers. These count every hour of the run period, occupied or not, in which a zone misses its setpoint by more than the Setpoint Not Met Criteria tolerance, each hour once for the facility; the Comfort and Setpoint Not Met Summary of the utility performance summary has only the occupied hours, which depend on each zone's people. A file without the table, the columns, or the row is a format error.

A malformed file is the error EnergyPlusOutputFormat, whose message names the file and the line.

The simulation result

SimulationResult holds the raw outputs of experiment outputs §8:

Member Source Notes
Heating, Cooling eplusout.mtr: Heating:DistrictHeatingWater, Cooling:DistrictCooling HourlySeries: 8760 hourly loads in W (the meter's J / 3600); AnnualKilowattHours is Σ W × 1 h / 1000.
Total the sum of Electricity:Facility, NaturalGas:Facility, DistrictHeatingWater:Facility, DistrictCooling:Facility hour by hour
Meters every meter of experiment outputs §1 (ResearchMeters.All) A meter that is not written is zero every hour; MetersWritten lists those that are. A meter not in J is an error.
Hours eplusout.mtr the 8760 HourStamps (month, day, hour ending 1 to 24) of the run period's hourly timestamps (D-132); EsoFile.Timestamps keeps them for any .eso or .mtr
Zones eplusout.eio and eplusout.eso (Zone Mean Air Temperature) name, multiplier, floor area, volume, conditioned, and 8760 temperatures in °C. A zone is conditioned when the IDF has a ZoneControl:Thermostat for it (a zone list is expanded); names are compared ignoring case. A zone without a temperature series is an error.
GroundHeatTransfer, GroundSurfaces eplusout.eio (surface details) and eplusout.eso (Surface Inside Face Conduction Heat Transfer Energy, keyed by surface) the ground heat transfer of experiment outputs §9: per hour, Σ over the ground surfaces of their zone's multiplier × the surface's J / 3600, in W, with EnergyPlus's sign (positive toward the zone). The surfaces and zones come from the .eio, never from the IDF. A building whose .eio lists no ground surface has a zero series; a ground surface without the variable is the error EnergyPlusOutputMissing naming it, one in units other than J or of a zone with no Zone Information line a format error.
FloorArea eplustbl.csv total and net conditioned
UnmetHours eplustbl.csv the facility's hours with the heating or cooling setpoint not met (Heating, Cooling), a sanity check (experiment outputs §8)
Runtime eplusout.err and the runner the EnergyPlus elapsed time, the wall-clock time, the EnergyPlus version and build, and the RunConditions. Comparable is true only for a concurrency of 1 (experiment outputs §6).
Site the IDF header and the two files' bytes the site record
ErrorReport eplusout.err

Every series has exactly 8760 values; a file whose run period has another number of hours, or a series of another length, is the error HourlySeriesLength. A missing file is the error EnergyPlusOutputMissing.

Site record

Convert2IDF writes the identifying lines into the IDF header, and SiteRecord reads them back. The header is the comment lines before the first IDF object, and the prefixes are exact:

! BEMGen Convert2IDF, code version <version>
! Site building: <typology, typed parameters, seed>
! Site zoning: <simplifier and options, aggregator, heat-transfer options>
! Site weather: <file> sha256 <hex>        (or: ! Site weather: none)
! Site envelope: <envelope preset name>
  • Site ID: the SHA-256, in lower-case hexadecimal, of the IDF file's bytes followed directly by the weather file's bytes, as they are on disk. Any change to either file gives another ID; the same two files always give the same ID.
  • CodeVersion comes from the header's first line, which Convert2IDF always writes.
  • A missing line, the code version included, is the warning SiteRecordIncomplete, which names the lines.
  • A weather file whose name (ignoring case) or SHA-256 differs from the header's is the warning SiteWeatherMismatch. none means the IDF was written without a weather file and is not a mismatch.

Metrics

ExperimentMetrics implements experiment outputs §2 to §5 and §9 as pure functions over series of any length (a SimulationResult gives 8760), and Compute(variant, reference) gathers them into SiteMetrics for heating, cooling, and total, with the site's own ground heat transfer.

Function Experiment outputs Definition
Annual §2 annual sum, kWh; EUI per conditioned and per total floor area, kWh/m²
Percentile §3 Hyndman and Fan type 7: position (n − 1)·p on the sorted values, interpolated linearly; checked against NumPy's np.quantile
Peaks §3 maximum, P99, P95, P90 of all hours, zero hours included, each in W and per both floor areas
Thresholds, HoursAbove §3 the reference site's P99, P95, P90 of the total load, and the hours of a site strictly above each; the thresholds are kept with the counts
CvRmse §4 √(Σ (yᵢ − rᵢ)² / n) / r̄, a fraction, denominator n
QuantileNrmse §4 √(Σₚ (Qᵧ(p) − Qᵣ(p))² / 101) / r̄ over QuantileGrid, p = k / 100 for k = 0 … 100
KolmogorovSmirnov §4 maxₓ |Fᵧ(x) − Fᵣ(x)| at every value of either series, ties allowed
Temperature §5 area-weighted mean and population standard deviation per hour, w = floor area × multiplier, over all zones and over conditioned zones
GroundHeatTransfer §9 the annual net Σ G, gain Σ G over G > 0, and loss Σ −G over G < 0, kWh; zero for a zero series; not compared with the reference

A metric without a value is a MetricValue with Value null and a MetricFlag, never 0 or infinity:

Flag When
ZeroReferenceMean r̄ = 0 for CV(RMSE) or the quantile NRMSE (for example no cooling in a cold climate)
NoConditionedZone the temperature over conditioned zones of a building without one
ZeroFloorArea an EUI or a normalised peak over a zero floor area (a building without conditioned floor area), or temperature weights that sum to zero

Two all-zero series have a Kolmogorov–Smirnov D of 0, and a reference against itself has zero comparison metrics. Series of different lengths, empty series, values that are not finite, and a probability outside [0, 1] are programming errors and throw.

Diagnostics

All codes are in DiagnosticCodes (Lod.Core):

Code Severity When
EnergyPlusNotFound error the folder, energyplus.exe, or Energy+.idd is missing, or the IDD's first line names no version
SimulationInputMissing error the IDF or weather file does not exist
OutputFolderNotEmpty error the output folder exists and is not empty; nothing runs
InvalidParameter error the timeout is not positive
EnergyPlusTimeout error EnergyPlus ran past the timeout and was stopped
EnergyPlusCancelled error the caller cancelled, before or during the run
EnergyPlusStartFailed error the process could not start
EnergyPlusOutputMissing error eplusout.err after a run, or a file or zone series the result needs, is missing
EnergyPlusVersionMismatch warning the IDF's Version differs from the installation's major and minor version, or the IDF has none
EnergyPlusRunFailed warning on a run, error on reading its result a non-zero exit code, or no successful completion in eplusout.err
EnergyPlusOutputFormat error a file has an unexpected layout, or a meter is not in J
HourlySeriesLength error a run period or series is not 8760 hours long
SiteRecordIncomplete warning the IDF header lacks a site record line
SiteWeatherMismatch warning the weather file is not the one the header records

The check tool

scripts/eplus-check/ runs EnergyPlus on many IDFs in parallel and judges each run with RunVerdict against ExpectedWarnings.

A warning that EnergyPlus prints only in its recurring error summary is checked like any other; a summary entry that repeats a warning printed earlier is checked once (review of the stage, 2026-10-11).

SystemProcessRunner stops only the process it started when a timeout passes or the caller cancels, not its child processes, because netstandard2.0 cannot stop a process tree. EnergyPlusRunner passes neither -x nor -r, so EnergyPlus starts no helper program. Its usage, the expected warnings with their reasons, and how to read its summary are in its scripts/eplus-check/README.md.

The batch runner

scripts/IdfBatchRunner/ (D-132) runs EnergyPlus on a batch of IDFs with the types above: it plans the batch from the site records, runs the references and then the variants of the references that succeeded, judges each run (RunVerdict), reads it (SimulationResultReader), checks its closure (EndUseClosure), computes its metrics against its reference (ExperimentMetrics.Compute), and writes one folder per run: batch.json, a central DuckDB database with every site's metrics and status, one Parquet file per succeeded site with its hourly series, written by the worker that ran it, and each site's eplusout.err (its whole EnergyPlus folder with --keep-raw or when it did not succeed; D-153). A resumed run compares its remaining test sites with a reference written before the interruption through the reference's ReferenceLoads, read back from its hourly file. The DuckDB library is referenced by the tool only; Lod.Simulation stays BCL-only. It also runs a test suite written by scripts/TestSuiteGenerator (D-133, --suite): the suite's suite.json gives the test cases, their references, and their weather files (BatchPlan.CreateForTestCases), and the suite's tables are copied into the central database. Usage, the run folder, the central database, and the hourly files are in its scripts/IdfBatchRunner/README.md; the suites are written by the test-suite generator (scripts/TestSuiteGenerator/README.md).

Tests

tests/Lod.Simulation.Tests (net8.0, run by scripts/verify.ps1):

  • Always: the parsers on short fixture texts written for the tests in EnergyPlus's layouts; the installation check on a temporary folder; the runner with a fake IProcessRunner (arguments, timeout, cancellation, missing installation, IDF, or weather file, a failed run, a non-empty output folder, the version warning); the default process runner on Windows' own cmd.exe and PING.EXE; the result reader on a scripted run that writes a synthetic year of outputs; the site record; the verdict; and the metrics on small series worked by hand, the percentiles and the quantile NRMSE also against NumPy 2.5.3 values written into the tests as literals, with every zero guard. Since D-132 also the end-use catalogue and the closure check (a balanced year, one end use off its fuel meter at one hour, a residual within the load tolerance, missing meters), the timestamps, the batch plan (test cases, references, every not-run reason, the variants of a failed reference, independence of the input order), the zoning line, and the input expansion.
  • Opt-in: [EnergyPlusFact] and [EnergyPlusTheory] are xunit attributes that set Skip unless the environment variable BEMGEN_ENERGYPLUS names a valid installation folder. The end-to-end set (tests/Lod.Simulation.Tests/EndToEnd/) generates buildings as the export tests do, converts each site with Convert2Idf and the installation's own WeatherData/USA_IL_Chicago-OHare.Intl.AP.725300_TMY3.epw (so every IDF has Site:Location and the site record), and runs it into a folder outside the repository: BEMGEN_SIM_OUT when set, otherwise a new folder under %TEMP%\bemgen-simulation-e2e\<date-time>. A folder inside the repository is refused. At most half the logical cores run EnergyPlus at a time across all tests, and a site is run once per test run and shared by the tests that use it. Each test gathers the problems of every site and reports them together.
  • Every run: it completed with no severe or fatal error and only the warnings of ExpectedWarnings.Default, judged by RunVerdict (a version mismatch is not expected), and the runner and the reader give no warning; the zones of the .eio equal the building's in name, multiplier, floor area, volume, and thermostat (conditioned); SimulationResultReader gives a complete result with 8760 hours in every series; the end uses (lighting, electric and gas equipment, hot water, heating, cooling) sum to the total load every hour within ToleranceSettings.LoadEquals; the Building Area table's total and conditioned floor area equal the building's Σ floor area × multiplier over all zones and over conditioned zones; the site record names the Chicago file with its SHA-256 and the envelope. The .eio and the table print two decimals, so the building's values are rounded to two decimals before the comparison with ToleranceSettings.AreaEquals.
  • Across the configurations of one building: one building line, a different zoning line per configuration, and the hourly internal-load meters (InteriorLights:Electricity, InteriorEquipment:Electricity, InteriorEquipment:NaturalGas, WaterSystems:DistrictHeatingWater) equal to the reference's every hour within LoadEquals (GLOBAL.md scientific rule 1 through EnergyPlus).
  • Sites: the canonical linear plan through every simplifier and floor aggregator with both heat-transfer settings (56 sites); the floor-aggregator set below (24 more sites, the linear plan's 8 being shared); the overhanging stair-bay bar (Conditioned Merge with Join Pieces, the upper storey moved by (5.3, −2.1) m, both settings); and the Medium Office program JSON on the office plate through every simplifier and aggregator (28 sites, the default heat-transfer options, internal loads compared with No Simplification, Stack). 110 sites in all.
  • The floor-aggregator set (the owner): the canonical linear plan (1, 2, 1), the office plate (1, 3, 1), the enclosed court (1, 3, 1), and the stepped band's three storeys, each under No Simplification through each floor aggregator with both settings, against the reference (No Simplification, Stack, both options on). Beyond the checks above it asserts that Σ floor area × multiplier and Σ volume × multiplier equal the reference's (the .eio being tied to the building zone by zone), that EnergyPlus's Total Building Area equals the reference's, and that the IDF has shading surfaces exactly when the building has unmodelled storeys under Stacked Floor Zone Multiplier (StackedFloorZoneMultiplier.UnmodelledStoreys) and none under the other aggregators. It asserts no bound on heating or cooling differences, which are research results. It writes reports/floor-aggregators-<plan>.csv (every metric of experiment outputs §2 to §5 from ExperimentMetrics.Compute, each site against the reference; the hourly temperatures summarised as their annual means) and a text table of the key numbers beside it, and prints the table's path in the test output.
  • Ground heat transfer (constructions and ground note, Part B): GroundHeatTransferEndToEndTests also needs BEMGEN_WEATHER_3B, the path of a climate zone 3B weather file ([EnergyPlusWeatherFact]). It converts the packet's medium office F-factor set on the two-storey linear plan with that file, runs it, and checks a clean run, the .eio's ground surfaces equal to the IDF's and to those the reader summed, and January's ground heat transfer negative; it writes ground-results.txt with the warnings and the annual and monthly values.
  • Last run (2026-10-11, EnergyPlus 26.2.0 build 4bd7a1f26f, 28 logical cores, 14 runs at a time): the 110 sites passed every check; the test project passed 139 of 139 tests in 103 s of wall time.
$env:BEMGEN_ENERGYPLUS = "D:\EnergyPlusV26-2-0"
dotnet test tests/Lod.Simulation.Tests -c Release
Remove-Item Env:BEMGEN_ENERGYPLUS