Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ifc-spf

Read and write IFC files with the standard library. Instances, property sets, quantities, units, the spatial chain — and edits written back out — no geometry kernel, no compiled dependency, no schema download.

from ifcspf import Model, Index

model = Model.open("building.ifc")
index = Index(model)

for door in model.of_type("IfcDoor"):            # subtypes included
    print(model.attr(door, "Name"),              # attributes by name, not position
          index.flat(door),                      # {"Pset_DoorCommon.FireExit": True, …}
          index.spatial(door)["storey"])         # where it is

Extracted from two tools that had each written it once already — a quantity takeoff and a building-code checker — because the interesting part of both was never the parser.

What it does

layer what you get
spf ISO 10303-21 tokeniser and parser: strings and their \X2\ escapes, references, enumerations, typed values, complex instances, comments, gzip, the header
schema attributes addressed by name (attr(door, "OverallWidth")), subtype expansion (IfcWall finds IfcWallStandardCase), IFC4 / IFC2X3 differences — all as data tables
units the project unit assignment resolved to SI factors, including conversion-based units (feet, inches), plus output conversion to mm, ft2, cy, …
psets property sets, quantity sets and the spatial chain, converted to SI, with type-object inheritance and per-property unit overrides
write instances back to ISO 10303-21: semantic round trip, byte-idempotent output, \X2\ string encoding, IFC GlobalId compression

The three things it gets right that hand-rolled readers usually don't

Type-level property sets are inherited. A door's fire rating often lives on its IfcDoorType, not the occurrence. Index collects type sets first and lets instance sets override them — the inheritance IFC intends — and marks which is which (Value.from_type).

Per-property units override the project unit. IfcPropertySingleValue and every IfcQuantity* may carry their own Unit. A file with millimetre lengths and one property in metres is not exotic; it is a Tuesday.

Every measure comes out in SI, and says what it was. Value carries the converted number, the raw number, the measure type (IFCLENGTHMEASURE) and the kind (length), so a consumer converts once, deliberately, at its own edge.

value = index.get(door, "Pset_PlancheckDoor", "ClearWidth")
value.value      # 0.88          — metres, whatever the file used
value.raw        # 880.0         — as written
value.measure    # 'IFCLENGTHMEASURE'
value.kind       # 'length'
value.from_type  # False

Install

pip install ifc-spf

Python 3.11+. No dependencies.

API

Model.open(path)  /  Model.from_text(text)      # .ifc and .ifc.gz
model.of_type("IfcWall", subtypes=True) -> [Entity]
model.attr(entity, "Name") / model.ref_attr(entity, "RelatingStructure")
model.get(ref) / model.resolve_all(refs) / model.referencing(entity, of_type=None)
model.schema        # 'IFC4' | 'IFC2X3' | …
model.scale         # UnitScale(length=0.001, area=1.0, …)
model.type_counts()

index = Index(model, inherit_type=True)
index.values(entity)   # {set name: {property name: Value}}
index.flat(entity)     # {"Pset.Name": value}
index.get(entity, "Pset_DoorCommon", "FireExit")   # one Value, case-insensitive
index.find(entity, "FireExit")                     # by name, any set
index.spatial(entity)  # {"space": …, "storey": …, "building": …, "site": …}
index.storey_elevation(entity)                     # metres

from ifcspf import loads, load, unwrap, convert, expand_types
convert(0.88, "mm")    # 880.0   — SI base out to a declared unit

# editing — by name, refusing to guess
model.set_attr(wall, "Name", "Tường trục A")   # unknown names raise KeyError
new = model.add("IfcWall", GlobalId=new_guid(), Name="W-09")
model.remove(entity)          # raises DanglingReferenceError if referenced
model.remove(entity, force=True)               # …strips the references too
model.save("edited.ifc")      # or model.dumps() for the text

The written file is semantically identical to what was parsed (same instances, same values) and writing is byte-idempotent, but layout is not preserved: comments and whitespace go, numbers are respelt canonically (1.0E31000.0), non-ASCII strings come out as \X2\ runs. Diff an edited file against a previous write, not against the original export.

What it deliberately does not do

  • No geometry. No swept solids, no BRep, no placement maths, no clash detection. Property-level tooling — takeoffs, code checks, audits, exports — does not need it, and pretending otherwise is how a reader becomes a kernel.
  • No schema validation. It reads what the file states about itself, and it writes what the instances say — an edit that violates the EXPRESS schema will be written faithfully, not corrected. The one guard is referential: remove() will not silently orphan references.
  • No layout preservation on write. Output is canonical, not a patch of the original text (see above).

If you need geometry or validation, use ifcopenshell — it is excellent, and this package is not trying to replace it. This is for the large class of jobs where pulling in a compiled IFC toolkit is the heaviest thing in the project.

Used by

  • qto — quantity takeoff with pluggable cost classification
  • plancheck — building code as machine-readable rules

MIT licensed.

About

Read IFC-SPF (STEP) files with the Python standard library — instances, property sets, quantities, units. No geometry kernel, no compiled dependency.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages