Skip to content

Runner API

Core simulation execution functions.

simulate

idfkit.simulation.runner.simulate(model, weather, *, output_dir=None, energyplus=None, expand_objects=True, annual=False, design_day=False, output_prefix='eplus', output_suffix='C', readvars=False, timeout=3600.0, extra_args=None, cache=None, fs=None, on_progress=None)

Run an EnergyPlus simulation.

Creates an isolated run directory, writes the model, and executes EnergyPlus as a subprocess. The caller's model is not mutated.

When expand_objects is True (the default) and the model contains GroundHeatTransfer:Slab:* or GroundHeatTransfer:Basement:* objects, the Slab and/or Basement ground heat-transfer preprocessors are run automatically before simulation. This is equivalent to calling run_slab_preprocessor or run_basement_preprocessor individually, but happens transparently.

Parameters:

Name Type Description Default
model IDFDocument

The EnergyPlus model to simulate.

required
weather str | Path

Path to the weather file (.epw).

required
output_dir str | Path | None

Directory for output files (default: auto temp dir).

None
energyplus EnergyPlusConfig | None

Pre-configured EnergyPlus installation. If None, uses find_energyplus for auto-discovery.

None
expand_objects bool

Run ExpandObjects before simulation. When True, also runs the Slab and Basement ground heat-transfer preprocessors if the model contains the corresponding objects.

True
annual bool

Run annual simulation (-a flag).

False
design_day bool

Run design-day-only simulation (-D flag).

False
output_prefix str

Prefix for output files (default "eplus").

'eplus'
output_suffix Literal['C', 'L', 'D']

Output file naming suffix: "C" for combined table files (default), "L" for legacy separate table files, or "D" for timestamped separate files.

'C'
readvars bool

Run ReadVarsESO after simulation (-r flag).

False
timeout float

Maximum runtime in seconds (default 3600).

3600.0
extra_args list[str] | None

Additional command-line arguments.

None
cache SimulationCache | None

Optional simulation cache for content-hash lookups.

None
fs FileSystem | None

Optional file system backend for storing results on remote storage (e.g., S3). When provided, output_dir is required and specifies the remote destination path. EnergyPlus runs locally in a temp directory; results are then uploaded to output_dir via fs after execution.

Note

The fs parameter handles output storage only. The weather file must be a local path — remote weather files are not automatically downloaded. For cloud workflows, download weather files first using WeatherDownloader or pre-stage them locally before calling simulate().

None
on_progress Callable[[SimulationProgress], Any] | Literal['tqdm'] | None

Optional callback invoked with a SimulationProgress event each time EnergyPlus emits a progress line (warmup iterations, simulation day changes, post-processing steps, etc.). Pass "tqdm" to use a built-in tqdm progress bar (requires pip install idfkit[progress]).

None

Returns:

Type Description
SimulationResult

SimulationResult with paths to output files.

Raises:

Type Description
SimulationError

On timeout, OS error, or missing weather file.

ExpandObjectsError

If a preprocessing step (ExpandObjects, Slab, or Basement) fails during automatic preprocessing.

EnergyPlusNotFoundError

If EnergyPlus cannot be found.

Source code in src/idfkit/simulation/runner.py
def simulate(
    model: IDFDocument,
    weather: str | Path,
    *,
    output_dir: str | Path | None = None,
    energyplus: EnergyPlusConfig | None = None,
    expand_objects: bool = True,
    annual: bool = False,
    design_day: bool = False,
    output_prefix: str = "eplus",
    output_suffix: Literal["C", "L", "D"] = "C",
    readvars: bool = False,
    timeout: float = 3600.0,
    extra_args: list[str] | None = None,
    cache: SimulationCache | None = None,
    fs: FileSystem | None = None,
    on_progress: Callable[[SimulationProgress], Any] | Literal["tqdm"] | None = None,
) -> SimulationResult:
    """Run an EnergyPlus simulation.

    Creates an isolated run directory, writes the model, and executes
    EnergyPlus as a subprocess. The caller's model is not mutated.

    When *expand_objects* is ``True`` (the default) and the model contains
    ``GroundHeatTransfer:Slab:*`` or ``GroundHeatTransfer:Basement:*``
    objects, the Slab and/or Basement ground heat-transfer preprocessors
    are run automatically before simulation.  This is equivalent to
    calling [run_slab_preprocessor][idfkit.simulation.expand.run_slab_preprocessor] or
    [run_basement_preprocessor][idfkit.simulation.expand.run_basement_preprocessor]
    individually, but happens transparently.

    Args:
        model: The EnergyPlus model to simulate.
        weather: Path to the weather file (.epw).
        output_dir: Directory for output files (default: auto temp dir).
        energyplus: Pre-configured EnergyPlus installation. If None,
            uses [find_energyplus][idfkit.simulation.config.find_energyplus] for auto-discovery.
        expand_objects: Run ExpandObjects before simulation.  When
            ``True``, also runs the Slab and Basement ground heat-transfer
            preprocessors if the model contains the corresponding objects.
        annual: Run annual simulation (``-a`` flag).
        design_day: Run design-day-only simulation (``-D`` flag).
        output_prefix: Prefix for output files (default "eplus").
        output_suffix: Output file naming suffix: ``"C"`` for combined table
            files (default), ``"L"`` for legacy separate table files, or
            ``"D"`` for timestamped separate files.
        readvars: Run ReadVarsESO after simulation (``-r`` flag).
        timeout: Maximum runtime in seconds (default 3600).
        extra_args: Additional command-line arguments.
        cache: Optional simulation cache for content-hash lookups.
        fs: Optional file system backend for storing results on remote
            storage (e.g., S3). When provided, ``output_dir`` is required
            and specifies the remote destination path. EnergyPlus runs
            locally in a temp directory; results are then uploaded to
            ``output_dir`` via *fs* after execution.

            !!! note
                The ``fs`` parameter handles **output storage only**.
                The ``weather`` file must be a local path — remote weather
                files are not automatically downloaded. For cloud workflows,
                download weather files first using [WeatherDownloader][idfkit.weather.WeatherDownloader]
                or pre-stage them locally before calling ``simulate()``.
        on_progress: Optional callback invoked with a
            [SimulationProgress][idfkit.simulation.progress.SimulationProgress] event
            each time EnergyPlus emits a progress line (warmup iterations,
            simulation day changes, post-processing steps, etc.).  Pass
            ``"tqdm"`` to use a built-in tqdm progress bar (requires
            ``pip install idfkit[progress]``).

    Returns:
        SimulationResult with paths to output files.

    Raises:
        SimulationError: On timeout, OS error, or missing weather file.
        ExpandObjectsError: If a preprocessing step (ExpandObjects, Slab,
            or Basement) fails during automatic preprocessing.
        EnergyPlusNotFoundError: If EnergyPlus cannot be found.
    """
    if fs is not None and output_dir is None:
        msg = "output_dir is required when using a file system backend"
        raise ValueError(msg)

    progress_cb, progress_cleanup = resolve_on_progress(on_progress)

    try:
        config = resolve_config(energyplus)
        logger.debug("EnergyPlus: %s (version %d.%d.%d)", config.executable, *config.version)
        weather_path = Path(weather).resolve()

        if not weather_path.is_file():
            msg = f"Weather file not found: {weather_path}"
            raise SimulationError(msg)

        logger.info("Starting simulation with weather %s", weather_path.name)

        cache_key: CacheKey | None = None
        if cache is not None:
            cache_key = cache.compute_key(
                model,
                weather_path,
                expand_objects=expand_objects,
                annual=annual,
                design_day=design_day,
                output_suffix=output_suffix,
                readvars=readvars,
                extra_args=extra_args,
            )
            cached = cache.get(cache_key)
            if cached is not None:
                logger.debug("Cache hit for key %s", cache_key.hex_digest[:12])
                return cached
            logger.debug("Cache miss for key %s", cache_key.hex_digest[:12])

        # Copy model to avoid mutation
        sim_model = model.copy()
        ensure_sql_output(sim_model)

        # Auto-preprocess ground heat-transfer objects when needed.
        sim_model, ep_expand = maybe_preprocess(model, sim_model, config, weather_path, expand_objects)

        # When using a remote fs, always run locally in a temp dir
        local_output_dir = None if fs is not None else output_dir
        run_dir = prepare_run_directory(local_output_dir, weather_path)
        idf_path = run_dir / "model.idf"

        from ..writers import write_idf

        write_idf(sim_model, idf_path)

        cmd = build_command(
            config=config,
            idf_path=idf_path,
            weather_path=run_dir / weather_path.name,
            output_dir=run_dir,
            output_prefix=output_prefix,
            output_suffix=output_suffix,
            expand_objects=ep_expand,
            annual=annual,
            design_day=design_day,
            readvars=readvars,
            extra_args=extra_args,
        )
        logger.debug("Command: %s", " ".join(cmd))

        start = time.monotonic()

        if progress_cb is not None:
            stdout, stderr, returncode = _run_with_progress(cmd, run_dir, timeout, start, progress_cb)
        else:
            stdout, stderr, returncode = _run_simple(cmd, run_dir, timeout, start)
    finally:
        if progress_cleanup is not None:
            progress_cleanup()

    elapsed = time.monotonic() - start

    if returncode != 0:
        logger.warning("Simulation exited with code %d in %.1fs", returncode, elapsed)
    else:
        logger.info("Simulation completed successfully in %.1fs", elapsed)

    if fs is not None:
        remote_dir = Path(str(output_dir))
        upload_results(run_dir, remote_dir, fs)
        result = SimulationResult(
            run_dir=remote_dir,
            success=returncode == 0,
            exit_code=returncode,
            stdout=stdout,
            stderr=stderr,
            runtime_seconds=elapsed,
            output_prefix=output_prefix,
            fs=fs,
        )
    else:
        result = SimulationResult(
            run_dir=run_dir,
            success=returncode == 0,
            exit_code=returncode,
            stdout=stdout,
            stderr=stderr,
            runtime_seconds=elapsed,
            output_prefix=output_prefix,
        )
    if cache is not None and cache_key is not None and result.success:
        cache.put(cache_key, result)
    return result

find_energyplus

idfkit.simulation.config.find_energyplus(*, version=None, path=None)

Find an EnergyPlus installation.

Discovery order
  1. Explicit path argument.
  2. ENERGYPLUS_DIR environment variable.
  3. energyplus on PATH (via shutil.which).
  4. Platform-specific default directories (newest version first).

Parameters:

Name Type Description Default
version tuple[int, int, int] | str | None

Optional version filter. Accepts (major, minor, patch) tuple or a string like "24.1.0" or "24.1".

None
path str | Path | None

Explicit path to EnergyPlus install directory or executable.

None

Returns:

Type Description
EnergyPlusConfig

Validated EnergyPlusConfig.

Raises:

Type Description
EnergyPlusNotFoundError

If no matching installation is found.

Source code in src/idfkit/simulation/config.py
def find_energyplus(
    *,
    version: tuple[int, int, int] | str | None = None,
    path: str | Path | None = None,
) -> EnergyPlusConfig:
    """Find an EnergyPlus installation.

    Discovery order:
        1. Explicit *path* argument.
        2. ``ENERGYPLUS_DIR`` environment variable.
        3. ``energyplus`` on ``PATH`` (via [shutil.which][]).
        4. Platform-specific default directories (newest version first).

    Args:
        version: Optional version filter. Accepts ``(major, minor, patch)``
            tuple or a string like ``"24.1.0"`` or ``"24.1"``.
        path: Explicit path to EnergyPlus install directory or executable.

    Returns:
        Validated EnergyPlusConfig.

    Raises:
        EnergyPlusNotFoundError: If no matching installation is found.
    """
    target_version = _normalize_version(version) if version is not None else None
    searched: list[str] = []

    # 1. Explicit path
    if path is not None:
        p = Path(path).resolve()
        searched.append(str(p))
        config = EnergyPlusConfig.from_path(p)
        if target_version is not None and config.version != target_version:
            raise EnergyPlusNotFoundError(searched)
        return config

    # 2-4. Try candidates from env var, PATH, and platform dirs
    for candidate in _discovery_candidates():
        searched.append(str(candidate))
        logger.debug("Trying candidate %s", candidate)
        result = _try_candidate(candidate, target_version)
        if result is not None:
            logger.info("Found EnergyPlus %d.%d.%d at %s", *result.version, result.install_dir)
            return result

    raise EnergyPlusNotFoundError(searched)

EnergyPlusConfig

idfkit.simulation.config.EnergyPlusConfig dataclass

Validated EnergyPlus installation configuration.

Attributes:

Name Type Description
executable Path

Path to the energyplus executable.

version tuple[int, int, int]

Parsed version as (major, minor, patch).

install_dir Path

Root installation directory.

idd_path Path

Path to the Energy+.idd file.

Source code in src/idfkit/simulation/config.py
@dataclass(frozen=True, slots=True)
class EnergyPlusConfig:
    """Validated EnergyPlus installation configuration.

    Attributes:
        executable: Path to the energyplus executable.
        version: Parsed version as (major, minor, patch).
        install_dir: Root installation directory.
        idd_path: Path to the Energy+.idd file.
    """

    executable: Path
    version: tuple[int, int, int]
    install_dir: Path
    idd_path: Path

    @property
    def weather_dir(self) -> Path | None:
        """Path to the bundled WeatherData directory, if present."""
        d = self.install_dir / "WeatherData"
        return d if d.is_dir() else None

    @property
    def schema_path(self) -> Path | None:
        """Path to Energy+.schema.epJSON, if present."""
        p = self.install_dir / "Energy+.schema.epJSON"
        return p if p.is_file() else None

    @property
    def expand_objects_exe(self) -> Path | None:
        """Path to ExpandObjects executable, if present."""
        name = "ExpandObjects.exe" if platform.system() == "Windows" else "ExpandObjects"
        p = self.install_dir / name
        return p if p.is_file() else None

    @property
    def _preprocess_dir(self) -> Path:
        """Path to the PreProcess/GrndTempCalc directory."""
        return self.install_dir / "PreProcess" / "GrndTempCalc"

    @property
    def slab_exe(self) -> Path | None:
        """Path to the Slab ground heat-transfer preprocessor, if present."""
        name = "Slab.exe" if platform.system() == "Windows" else "Slab"
        p = self._preprocess_dir / name
        return p if p.is_file() else None

    @property
    def slab_idd(self) -> Path | None:
        """Path to SlabGHT.idd, if present."""
        p = self._preprocess_dir / "SlabGHT.idd"
        return p if p.is_file() else None

    @property
    def basement_exe(self) -> Path | None:
        """Path to the Basement ground heat-transfer preprocessor, if present."""
        name = "Basement.exe" if platform.system() == "Windows" else "Basement"
        p = self._preprocess_dir / name
        return p if p.is_file() else None

    @property
    def basement_idd(self) -> Path | None:
        """Path to BasementGHT.idd, if present."""
        p = self._preprocess_dir / "BasementGHT.idd"
        return p if p.is_file() else None

    @classmethod
    def from_path(cls, path: str | Path) -> EnergyPlusConfig:
        """Create config from an explicit installation path.

        The path can point to either the installation directory or the
        energyplus executable directly.

        Args:
            path: Path to EnergyPlus install directory or executable.

        Returns:
            Validated EnergyPlusConfig.

        Raises:
            EnergyPlusNotFoundError: If the path is not a valid installation.
        """
        path = Path(path).resolve()

        # If path is an executable, derive install dir
        if path.is_file():
            install_dir = path.parent
            exe = path
        else:
            install_dir = path
            exe_name = "energyplus.exe" if platform.system() == "Windows" else "energyplus"
            exe = install_dir / exe_name

        if not exe.is_file():
            raise EnergyPlusNotFoundError([str(install_dir)])

        idd = install_dir / "Energy+.idd"
        if not idd.is_file():
            raise EnergyPlusNotFoundError([str(install_dir)])

        version = _extract_version(install_dir)
        if version is None:
            version = _extract_version_from_idd(idd)
        if version is None:
            raise EnergyPlusNotFoundError([str(install_dir)])

        return cls(
            executable=exe,
            version=version,
            install_dir=install_dir,
            idd_path=idd,
        )

version instance-attribute

executable instance-attribute

install_dir instance-attribute

idd_path instance-attribute