npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@idfkit/engine

v0.4.0

Published

Run building energy simulations in the browser via WebAssembly. Framework-agnostic loader, worker and output parsers.

Downloads

674

Readme

@idfkit/engine

Run building energy simulations in the browser, via WebAssembly. No server, no install, no upload — the model never leaves the page.

This package is the loader, worker and output parsers. It carries no engine binary of its own; pair it with @idfkit/engine-assets, whose package version is the EnergyPlus version it bundles.

npm install @idfkit/engine @idfkit/[email protected]

Quickstart

Assets have to be reachable over HTTP at runtime. The reliable way is to copy them somewhere your app serves, and point the loader at that path:

// package.json
{
  "scripts": {
    "prebuild": "idfkit-engine-assets public/energyplus",
    "predev": "idfkit-engine-assets public/energyplus"
  }
}
import { createEnergyPlus } from '@idfkit/engine';

const ep = await createEnergyPlus({
  assetBaseUrl: '/energyplus',
  onConsole: (line) => console.log(line),
  onProgress: ({ phase, progress }) => console.log(phase, progress),
});

const result = await ep.run({ idf, epw });

if (result.success) {
  // `variables` is a Map keyed by the output file's variable id.
  console.log(`${result.eso?.variables.size ?? 0} output variables`);
} else {
  console.error(result.fatalError);
  for (const entry of result.err?.entries ?? []) console.error(entry.message);
}

ep.dispose();

Add public/energyplus/ to .gitignore — it is generated, and it is ~50 MB.

Choosing an EnergyPlus version

The asset package's version is the EnergyPlus version:

npm install @idfkit/[email protected]   # EnergyPlus version 26.1.0 software
npm install @idfkit/[email protected]   # EnergyPlus version 25.2.0 software

Rebuilds of the same upstream release use the PATCH slot: 26.1.1, 26.1.2. A prerelease suffix like 26.1.0-build.2 does not work here -- npm sorts a prerelease BELOW its release and excludes it from ordinary ranges, so @idfkit/[email protected] and ^26.1.0 would both skip it and users would never receive the rebuild. Reserve prereleases for builds nobody should adopt automatically.

To offer several versions at runtime, skip the copy step and point assetBaseUrl at a CDN per version:

const ep = await createEnergyPlus({
  assetBaseUrl: `https://cdn.jsdelivr.net/npm/@idfkit/engine-assets@${version}/assets`,
  // The binary is named after its version, so this must move with it. Each
  // asset package exports the right value as `wasmFile`.
  wasmFile: `energyplus.js-${version}.wasm`,
  // The worker ships in @idfkit/engine, a different package, so it cannot be
  // derived from assetBaseUrl here.
  workerUrl: 'https://cdn.jsdelivr.net/npm/@idfkit/engine/dist/engine.worker.js',
});

That path is cross-origin, which brings the caveats in the next section.

Serving requirements

| Requirement | Why | | --- | --- | | Content-Type: application/wasm on .wasm | Streaming compilation rejects other types | | Long-lived immutable caching | The binary is ~28 MB and content-addressed by version | | worker-src blob: in your CSP | Only if loading the worker cross-origin — see below | | script-src 'unsafe-eval' in your CSP | The Emscripten glue is fetched as text and run via new Function. 'wasm-unsafe-eval' alone is not sufficient. |

Browser floor. Engine binaries built with WebAssembly exception handling — every asset package published after idfkit-engine#11 — need Chrome/Edge 95, Firefox 100, or Safari and iOS Safari 15.2. The engine uses exceptions to report its own fatal errors, so this is not optional on those builds. The loader itself has no such requirement, and an older asset package does not either.

Cross-origin loading. A worker script cannot itself be cross-origin, so when workerUrl is on another origin the loader wraps it in a same-origin Blob that importScripts() the real one. That needs worker-src blob: in your CSP, and the remote origin must send permissive CORS headers. Serving from your own origin avoids all of this, which is why the copy step is the documented default.

Threads. The bundled build is single-threaded, so it does not require cross-origin isolation (COOP/COEP). If you swap in a threaded build, you will need both headers and SharedArrayBuffer.

API

createEnergyPlus(options): Promise<EnergyPlusEngine>

Loads the runtime. Downloads and compiles ~28 MB, so call it once and reuse the result across runs.

| Option | Default | Notes | | --- | --- | --- | | assetBaseUrl | required | Where the WASM/IDD/datasets live. No default exists — this package ships no binary. | | workerUrl | ${assetBaseUrl}/engine.worker.js | The copy step puts it there. Override only if the worker is served elsewhere. | | wasmFile | energyplus.js-26.1.wasm | Must match your asset package version. | | loaderFile | energyplus.js | Emscripten glue filename. | | loadTimeoutMs | 30000 | Raise on slow connections. | | onConsole | — | Every stdout/stderr line. | | onProgress | — | Coarse phase + percentage. |

engine.run({ idf, epw?, files?, outputs? }): Promise<EngineRunResult>

Runs one simulation. HVACTemplate:* objects are expanded automatically first.

Resolves even when the engine fails. A model that does not converge is data, not an exception — check result.success and read result.err. A failed HVACTemplate expansion and a cancelled run also resolve. It rejects only when the run could not be attempted at all (worker died, runtime never loaded, engine disposed).

Returns exitCode, success, cancelled, err, eso, mtr, rdd, mdd, sql, html, csv, expand, omittedOutputs, consoleOutput, fatalError and crash.

crash is set only when a C++ exception unwound out of the engine instead of being handled inside it — { type, message }, e.g. EnergyPlus::FatalError. That is a defect in the engine binary, not in the model: report it with the asset package version rather than asking the user to fix their input.

Runs are serialised; a second run() while one is in flight is rejected.

Reading back less: outputs

Every output the model wrote is read back and, for eso and mtr, parsed. For an annual timestep model that is tens of megabytes copied out of the WASM heap, decoded, turned into Maps and structured-cloned — wasted on an app that reads its results from the SQLite output.

const result = await ep.run({ idf, epw, outputs: { eso: false, mtr: false } });
const db = await parseSQL(result.sql!);

Keys are eso, mtr, rdd, mdd, csv, html and sql; each defaults to true, and naming one does not switch off the others. Prefer this to making the model suppress the files with OutputControl:Files — that edits the user's document to save work in the parser.

eplusout.err is always read: success and fatalError come from it. Whatever you deselect is absent from the result and named in result.omittedOutputs, so "not read" stays distinguishable from "the model never wrote one".

Models that read a file: files

Schedule:File, Table:Lookup, Chiller:Electric:ASHRAE205 and friends name a file the engine has to be able to open. Pass its contents in files, keyed by exactly the path the model names:

// Schedule:File,
//   Occupancy, Fraction,
//   occupancy.csv,         !- File Name
//   1, 1, 8760, Comma, No, 60;

await ep.run({ idf, files: { 'occupancy.csv': csvText } });

Values may be a string or a Uint8Array — ASHRAE 205 representations are commonly CBOR and WINDOW data files are binary.

The rules, all of them:

  • Keys are relative to the model, and subdirectories work: a model naming data/profile.csv needs the key data/profile.csv.
  • Keys are case-sensitive, because the simulation filesystem is. Occupancy.CSV will not open occupancy.csv.
  • A leading /, a .., a backslash, or a name the engine owns (input.idf, weather.epw, Energy+.idd, anything under output/ or datasets/) is rejected — run() rejects, since that is a mistake in your code.
  • A model naming a file that is not in files fails before the engine starts, resolving with success: false and a fatalError naming the object, the field and the path. That is a modelling problem, so it resolves like any other failed run.
  • Staged files last exactly one run. The next run() on the same engine starts from a clean filesystem.

To find out what a model needs before running it — to prompt for an upload, say — use detectExternalFileReferences(idf), exported from the package root.

ExternalInterface:* FMU objects are not covered: they need a co-simulation partner process, not a file.

engine.cancel() / engine.dispose()

cancel() is best-effort and usually will not stop a run in progress. callMain is synchronous inside the worker and blocks its event loop, so a cancel message posted after the engine starts is not even dispatched until the run has finished. In practice it only lands in the window before the engine starts — after HVACTemplate expansion, for a model that has templates.

To stop a running simulation, dispose() and create a new engine. That costs a full ~28 MB reload, so reserve it for a user who explicitly wants out.

When cancellation does land, run() resolves with cancelled: true rather than rejecting.

Reading eplusout.sql

The SQLite output carries the tabular summary reports that have no ESO/MTR equivalent. It needs sql.js, an optional peer dependency:

npm install sql.js
import { parseSQL, configureSqlJs } from '@idfkit/engine/sql';

// Default fetches the sql.js runtime from sql.js.org. Override it if your CSP
// blocks that origin, or if you need to work offline.
configureSqlJs({ locateFile: (f) => `/sql-js/${f}` });

const db = await parseSQL(result.sql!);

EnergyPlus writes ReportData — the table holding every reported value — with no index on the variable it belongs to, so an unindexed getTimeSeries() scans the whole run. parseSQL therefore builds that index while loading. On a 40 MB annual file (1.8 M rows) that costs ~0.9 s once and takes five series from ~490 ms to ~130 ms; a database opened only for the tabular reports can skip it:

const db = await parseSQL(result.sql!, { indexTimeSeries: false });
renderSummary(db.getTabularData('AnnualBuildingUtilityPerformanceSummary'));
queueMicrotask(() => db.ensureTimeSeriesIndex()); // later, off the critical path

Meters and variables share one dictionary here: getVariables() returns both, isMeter tells them apart, and getTimeSeries(id) reads either. getEnvironments() gives each period's index (what a data point's timestamp.environmentIndex refers to) and environmentType; its latitude/longitude are 0, because the SQLite output stores no site location — read those from result.eso?.environments.

A query naming a column the database does not have throws rather than answering empty. That can only be a defect in this package, and a silent empty result is indistinguishable from a model that reported nothing — which is how #14 survived a release. A table that is simply absent is still tolerated.

What is and is not supported

The browser build is not the full desktop engine. Verified against the shipped binary:

Not available

| Feature | Why | | --- | --- | | PythonPlugin:* / Python EMS API | Compiled with LINK_WITH_PYTHON=OFF. Use EMS/Erl instead — it is fully supported. | | ShadowCalculationPixelCounting | The one GPU path in EnergyPlus (OpenGL via Penumbra). Compiled with OPENGL_REQUIRED=OFF. Degrades gracefully: warns and reverts to PolygonClipping. | | GHX borehole field design | Needs PYTHON_CLI. Modelling a pre-sized GHX is fine. | | Basement, Slab, EPMacro, IDFVersionUpdater | Separate Fortran binaries; BUILD_FORTRAN=OFF. Pre-run natively. | | ExternalInterface:* (FMU / BCVTB) | Needs sockets and a co-simulation partner process. | | Multithreading | Single-threaded by design — which is why no COOP/COEP headers are needed. |

Available: HVACTemplate:* (via the bundled ExpandObjects), EMS/Erl, SQLite output, Foundation:Kiva, AirflowNetwork, conduction finite difference and PCM.

Memory ceiling: 1 GB (starts at 256 MB and grows). Large models — many zones, sub-hourly output, long run periods — can exhaust it. Request fewer output variables or shorten the run period before suspecting the model.

Full detail, including agent-facing guidance, is in .agents/skills/using-idfkit-engine/references/capability-support.md.

Validation

Every release is verified to reproduce the native EnergyPlus 26.1.0 build across annual simulations, on a corpus chosen to stress distinct physics:

| Case | Stresses | Data points | | --- | --- | --- | | envelope-freefloat | Conduction and infiltration, no HVAC to damp errors | 1,242,704 | | solar-fenestration | Solar position, transmission, angular dependence | 1,523,712 | | thermal-mass-pcm | Phase-change material via conduction finite difference | 387,552 | | hvac-airloop-plant | Air loop + plant, component sizing, controller iteration | 639,480 |

Each reported variable is reduced to a fingerprint — count, sum, min, max and an order-sensitive checksum — and compared against the native result. Aggregates (sum, checksum) must agree to 1e-9 relative; order statistics (min, max) to 1e-6, because a single controller iterating once more at one timestep relocates an extremum. Variable and data-point counts must match exactly.

The worst divergence measured across the corpus is 3.5e-9, on the maximum of one VAV outlet humidity ratio — whose own sum and checksum agree to better than 1e-9. Everything else is tighter.

Reproduce it yourself:

npm run differential:reference   # needs a native EnergyPlus install
npm run test:differential        # needs only a browser

On ASHRAE Standard 140 / BESTEST. This suite is not Standard 140. Those case files are copyrighted by ASHRAE and are redistributed by nobody — they are absent from both the EnergyPlus source tree and its installer — so this corpus uses EnergyPlus's own BSD-licensed test files instead.

The claim above is deliberately narrow and entirely about this package: the WebAssembly build is numerically indistinguishable from the native build. The Standard 140 validation of the simulation engine itself is NREL's, performed and published by them for EnergyPlus. What this test establishes is that compiling to WebAssembly does not disturb it.

Attribution

This package runs EnergyPlus version 26.1.0 software, compiled to WebAssembly. EnergyPlus is developed by NREL and the U.S. Department of Energy and is distributed under its own licence — see LICENSE-EnergyPlus.txt and NOTICE in @idfkit/engine-assets.

This project is not affiliated with, endorsed by, or sponsored by NREL, the U.S. Department of Energy, or any EnergyPlus copyright holder.

The loader, worker and parsers in this package are MIT licensed.