Metadata-Version: 2.4
Name: ifc-spf
Version: 0.2.0
Summary: Read IFC-SPF (STEP) files with the standard library — instances, property sets, quantities and units, no geometry kernel.
Author: Nguyen Thu Thuy
License: MIT
Project-URL: Homepage, https://github.com/sophie-nguyenthuthuy/ifc-spf
Project-URL: Issues, https://github.com/sophie-nguyenthuthuy/ifc-spf/issues
Keywords: ifc,bim,step,iso-10303,buildingsmart,aec
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: File Formats
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# 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.

```python
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.

```python
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

```bash
pip install ifc-spf
```

Python 3.11+. No dependencies.

## API

```python
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.0E3` → `1000.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](https://github.com/sophie-nguyenthuthuy/qto) — quantity takeoff with
  pluggable cost classification
- [plancheck](https://github.com/sophie-nguyenthuthuy/plancheck) — building code
  as machine-readable rules

MIT licensed.
