Two IFC model versions in, a human-readable changelog out.
Which walls moved and by how much. Which properties changed. Quantity deltas by class — and, crucially, what caused them. What was added and deleted. As markdown for a pull request, HTML for a CI artifact, JSON for a script, or BCF 2.1 for a coordination tool.
Pure standard library. No geometry kernel, no server, no wheels to install.
ifcdiff old.ifc new.ifc## Headline
- IfcSlab NetVolume is up +6.2% (+1.50 m³): +1.50 from 1 edited element.
- IfcWall NetVolume is up +1.4% (+0.24 m³): +2.40 from new elements,
-2.40 from removed elements, +0.24 from 1 edited element.
## Moved (2)
| Element | Class | Location | Change |
| ------------ | ------- | ------------------------- | --------------------------------------- |
| Wall — core C| IfcWall | Site / Building / Level 1 | moved 3,500 mm (Z−3,500 mm) rotated 90° |
| Wall — east | IfcWall | Site / Building / Level 1 | moved 250 mm (X+250 mm) |
IfcOpenShell ships a tool with the same name. pip install ifcdiff gets
you that one — a mature,
GlobalId-based comparator with a real geometry kernel behind it. If you need
boundary-representation equivalence ("is this extrusion the same solid as that
mesh?"), use IfcOpenShell's, not this one.
This project exists because that tool answers a different question. It emits
diff.json: a machine-readable list of which GlobalIds changed. What it does
not do is tell a project engineer why the concrete volume went up 8% this
issue — and that is the question someone asks on every project.
IfcOpenShell ifcdiff |
this ifcdiff |
|
|---|---|---|
| Install | ifcopenshell (~100 MB, native geometry kernel) |
stdlib only, zero dependencies |
| Geometry comparison | resolves breps; tolerance-aware | representation fingerprint only (see limits) |
| Output | JSON | markdown, HTML, JSON, BCF 2.1 |
| Quantity roll-up | — | by class / material / storey / type, decomposed by cause |
| Movement magnitude | changed / unchanged | distance in mm, per-axis, rotation in degrees |
| GUID instability | assumes GlobalIds are stable | falls back to name/tag/position matching |
| CI gate | — | --fail-on 'qty:NetVolume>5%', exit code 1 |
They are complements. Use IfcOpenShell's when geometry is the question; use this one when the changelog is.
The PyPI distribution here is ifcdiff-cli to avoid the collision. The
command is still ifcdiff.
pip install ifcdiff-cliOr just vendor it — it is stdlib-only, so git clone and python -m ifcdiff
works with no install at all.
# markdown changelog to stdout
ifcdiff old.ifc new.ifc
# several reports at once; the format comes from the extension
ifcdiff old.ifc new.ifc -o report.md -o report.html -o report.json -o issues.bcfzip
# why did the concrete move? group the roll-up by material
ifcdiff old.ifc new.ifc --by material
# fail the build when structural quantities drift or elements vanish
ifcdiff old.ifc new.ifc -q --fail-on 'qty:NetVolume>5%' --fail-on 'deleted>0'Exit codes: 0 clean, 1 a gate tripped, 2 bad input or usage.
| Kind | Meaning |
|---|---|
added / deleted |
present in only one model |
moved |
world placement changed — distance in mm, per-axis delta, rotation in degrees |
geometry |
the representation subgraph changed shape (placement excluded) |
quantity |
an IfcElementQuantity value changed, with percentage |
property |
any IfcPropertySet value added, removed or changed |
material |
material or layer thickness changed |
type |
a different IfcTypeObject is assigned |
container |
re-hosted to another storey or spatial element |
attribute |
Name, Description, ObjectType, Tag or PredefinedType |
classification |
IfcClassificationReference changed |
reclassified |
the IFC entity itself changed, e.g. IfcWall → IfcSlab |
The headline is the point of the tool:
IfcWall NetVolume is up +1.4% (+0.24 m³): +2.40 from new elements,
-2.40 from removed elements, +0.24 from 1 edited element.
The three causes always sum exactly to the delta, so the number is auditable —
you can see whether a jump came from new elements, deleted ones, or the same
elements growing. Group with --by class|material|storey|type.
All values are normalised to SI (m, m², m³) from each file's own units, so a millimetre export and a metre export of the same building compare as identical.
Some authoring tools regenerate GUIDs on export, which naively reads as "every
element was deleted and re-added". --match controls the fallback:
guid— GlobalId only, like IfcOpenShell's.strict(default) — then class + name + tag + storey.loose— then world position, then shape fingerprint.
Fallback pairings only apply when they are unambiguously 1:1, and every change found that way is flagged with ⚠︎ in the report so you can distrust it.
ifcdiff old.ifc new.ifc -o issues.bcfzip # one topic per element
ifcdiff old.ifc new.ifc -o issues.bcfzip --bcf-group kind # "42 walls moved"BCF 2.1 with a viewpoint per topic: the element pre-selected and an orthogonal
camera aimed at its world position. Topic GUIDs are UUIDv5-derived and the
timestamp defaults to the new file's mtime (SOURCE_DATE_EPOCH honoured), so
the archive is byte-for-byte reproducible — re-running in CI does not produce a
new set of topics.
- run: |
pip install ifcdiff-cli
ifcdiff baseline.ifc "$MODEL" -o report.md -o issues.bcfzip \
--fail-on 'deleted>0' --fail-on 'qty:NetVolume>5%'Gate metrics: any change kind, plus changes, modified, unchanged,
moved_mm (largest single movement) and qty:<Name> (absolute, or percent
with a trailing %).
from ifcdiff import load, compare, Options, render_markdown, write_bcf
result = compare(load("old.ifc"), load("new.ifc"), Options(match="loose"))
print(result.counts) # {'added': 1, 'moved': 2, ...}
for change in result.by_kind("moved"):
print(change.name, change.detail["distance_mm"])
volume = result.rollup_for("NetVolume")
print(volume.pct, volume.from_added, volume.from_changed)
print(render_markdown(result))
write_bcf(result, "issues.bcfzip")This is an honest list, not a roadmap.
- No boundary-representation resolution. Geometry comparison is a
fingerprint of the representation subgraph. If one file describes a wall as
an extrusion and the other as a mesh, that is reported as a shape change even
when the two resolve to the same solid. IfcOpenShell's
ifcdiffhandles this; this one deliberately does not. - No implicit geometry evaluation. Openings, clipping and boolean results
are not subtracted. Quantities come from
IfcElementQuantityas authored — if the exporter did not write base quantities, there is nothing to roll up. IfcGridPlacementis not resolved (it needs the grid), so such elements are treated as world-origin and only their shape fingerprint speaks.- Speed. Roughly 2 MB/s of IFC on one core; an 8 MB pair diffs in ~3.5 s.
Large brep-heavy models are dominated by the shape fingerprint — pass
--no-geometryto skip it when you only care about data changes. - Type-borne property sets are folded into each occurrence by default and
marked
(type); disable with--no-inherited-psets.
make test # 157 tests, stdlib only
make demo # diff the bundled example models
make examples # regenerate examples/*.ifc and the sample reportstests/ifcgen.py is a small IFC4 writer used to build fixtures. The tests run
against genuinely valid IFC-SPF — units, placement chains, property sets, base
quantities and material layer sets — rather than mocks, and examples/old.ifc
and examples/new.ifc come out of it and open in any viewer.
- IfcOpenShell
ifcdiff— the established CLI; geometry kernel, JSON output. - Speckle — version diffing in a hosted viewer; excellent if your workflow already pushes to a Speckle server.
- ThatOpen / web-ifc and viewers built on it — in-browser visual comparison, colour-coded in 3D.
- Solibri Model Comparison — commercial, ruleset-driven, GUI.
None of them is a zero-dependency CLI that prints a changelog and sets an exit code. That is the gap this fills.
MIT