Version migration¶
idfkit.migration is a thin orchestration layer over EnergyPlus's IDFVersionUpdater transition binaries. Use it to forward-migrate an IDF (or IDFDocument) from an older EnergyPlus version to a newer one. Backward migration is not supported (EnergyPlus's binaries are one-way).
When to use¶
- You have an old IDF and want to simulate it against a newer EnergyPlus installation.
- You're maintaining a fleet of models and want them all on the same version.
- You need to surface what transition steps changed during migration.
Quick start¶
from idfkit import load_idf
from idfkit.migration import migrate
from idfkit.simulation import find_energyplus
doc = load_idf("legacy_v22.idf") # version (22, 1, 0)
config = find_energyplus() # e.g. (25, 2, 0)
report = migrate(doc, target_version=config.version, energyplus=config)
new_doc = report.migrated_model # fresh IDFDocument at (25, 2, 0)
The same path is wired into simulate(..., auto_migrate=True) — see simulation-execution.md. Use the explicit migrate(...) form when you want to inspect or persist the migrated model independently of running a simulation.
Core API¶
from idfkit.migration import (
migrate, # sync entrypoint
async_migrate, # asyncio entrypoint
MigrationReport,
MigrationStep,
MigrationDiff,
MigrationProgress,
Migrator, # protocol (sync)
AsyncMigrator, # protocol (async)
SubprocessMigrator, # default backend
AsyncSubprocessMigrator,
plan_migration_chain, # planning without execution
document_diff, # structural diff between two docs
)
migrate always takes a parsed IDFDocument and returns a MigrationReport whose migrated_model attribute is the new document. The input is never mutated.
Top-level signature¶
migrate(
model: IDFDocument,
target_version: tuple[int, int, int] | str,
*,
energyplus: EnergyPlusConfig | None = None, # default: auto-discover
migrator: Migrator | None = None, # plug a custom backend
on_progress: Callable[[MigrationProgress], None] | None = None,
work_dir: str | Path | None = None, # default: a fresh tempdir
keep_work_dir: bool = False,
) -> MigrationReport
async_migrate mirrors this signature.
The migration chain¶
EnergyPlus ships one Transition-VX-to-VY binary per version step. idfkit plans a sequence of binaries to walk from source → target. Inspect the plan without executing:
from idfkit.migration import plan_migration_chain
chain = plan_migration_chain(
source=(22, 1, 0),
target=(25, 2, 0),
)
for step in chain:
print(step.from_version, "->", step.to_version, step.binary)
A four-step chain (22.1 → 22.2 → 23.1 → 23.2 → 24.1 → 24.2 → 25.1 → 25.2) is normal — each step is a separate binary invocation.
What MigrationReport contains¶
report = migrate(doc, target_version=(25, 2, 0))
report.migrated_model # IDFDocument
report.source_version # (22, 1, 0)
report.target_version # (25, 2, 0)
report.steps # list[MigrationStep]
report.diff # MigrationDiff (structural changes)
report.summary() # human-readable rollup
for step in report.steps:
print(step.from_version, "->", step.to_version)
print(step.stdout)
print(step.stderr)
print(step.audit_text) # contents of the transition audit file
print(step.runtime_seconds)
MigrationDiff surfaces what changed structurally: added_object_types and removed_object_types (tuples of type names), object_count_delta (per-type count changes), and field_changes (per-type FieldDeltas recording added/removed fields). Its is_empty property is True when nothing changed.
Inspecting changes¶
diff = report.diff
print(f"Added object types: {diff.added_object_types}") # tuple[str, ...]
print(f"Removed object types: {diff.removed_object_types}") # tuple[str, ...]
print(f"Object count changes: {diff.object_count_delta}") # {type: signed delta}
# Per-type schema-level field renames (FieldDelta has .added / .removed)
for obj_type, delta in diff.field_changes.items():
print(obj_type, "added fields:", delta.added, "removed fields:", delta.removed)
Progress events¶
def on_progress(event: MigrationProgress) -> None:
print(event.step_index, event.total_steps, event.from_version, "->", event.to_version, event.phase)
migrate(doc, target_version=(25, 2, 0), on_progress=on_progress)
Async¶
import asyncio
from idfkit.migration import async_migrate
async def main():
report = await async_migrate(doc, target_version=(25, 2, 0))
return report.migrated_model
new_doc = asyncio.run(main())
Plugging a custom backend¶
from pathlib import Path
from idfkit.migration import MigrationStepResult
class MyMigrator:
def migrate_step(
self,
idf_text: str,
from_version: tuple[int, int, int],
to_version: tuple[int, int, int],
*,
work_dir: Path,
) -> (
MigrationStepResult
): ... # invoke a binary, return MigrationStepResult(idf_text=..., stdout=..., stderr=..., audit_text=...)
migrate(doc, target_version=(25, 2, 0), migrator=MyMigrator())
The default SubprocessMigrator shells out to the binaries shipped with the installed EnergyPlus. Custom backends are useful for containerised or remote migration services.
Diffing two arbitrary documents¶
document_diff is exposed independently — useful for change reports outside migration:
from idfkit.migration import document_diff
diff = document_diff(old_doc, new_doc)
if diff.is_empty:
print("No structural changes")
else:
print("Added types:", diff.added_object_types)
print("Removed types:", diff.removed_object_types)
print("Count deltas:", diff.object_count_delta)
Common mistakes¶
assuming migration is reversible
keep the original and migrate forward
running migrate without energyplus when the installed version is older
install a newer EnergyPlus, or migrate using one that has the binaries
assuming simulate(auto_migrate=True) persists the migrated model
call migrate explicitly and persist if you need it later
Related¶
- simulation-execution.md —
auto_migrate=Truefor transparent migration. - parsing-idf-epjson.md — explicit version override at load time.
- CLI:
idfkit migrate <input.idf> <target_version>migrates from the shell. - API docs: py.idfkit.com/api/migration/