simple-ifc/CLAUDE.md

28 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code when working in this repository of IFC building model files.

Overview

This repository contains IFC (Industry Foundation Classes) building models. IFC is an open standard (ISO 16739) for sharing building and infrastructure data. The repository uses standard tooling for:

  • Querying and editing models with ifcquery / ifcedit / ifcmcp
  • Validation against schema rules and IDS specifications
  • Continuous integration — models are validated automatically on every commit
  • Branch-based workflows — feature branches for model changes, ifcmerge for conflict resolution

Preferred Tooling

ifcmcp (preferred for interactive sessions)

The ifcmcp MCP server holds the IFC model in memory across tool calls — no file I/O between operations. It is always the first choice for multi-step queries and edits.

Setup (add to .mcp.json):

{
  "mcpServers": {
    "ifc": {
      "type": "stdio",
      "command": "python3",
      "args": ["-m", "ifcmcp"]
    }
  }
}

Or via CLI: claude mcp add --transport stdio ifc -- python3 -m ifcmcp

ifcquery / ifcedit (CLI — useful for scripting)

python3 -m ifcquery <file> <subcommand> [args]
python3 -m ifcedit <subcommand> [args]

Each invocation reads/writes from disk. Use for scripting or when MCP is not available.


MCP Tool Reference

Session tools

  • ifc_load(path) — Open an IFC file into memory. Must be called before any other tool.
  • ifc_save(path="") — Write model to disk. Empty path overwrites the original file.
  • ifc_reset() — Clear in-memory model without saving.

Query tools (read-only)

  • ifc_summary() — Schema version, entity counts, project name/description.
  • ifc_tree() — Full spatial hierarchy: Project → Site → Building → Storeys → Spaces → Elements.
  • ifc_info(element_id) — Deep inspection of any entity by step ID: attributes, property sets, resolved 4×4 placement matrix, type, material, container.
  • ifc_select(query) — Filter elements by IFC class (e.g. "IfcWall", "IfcWindow"). Returns [{id, type, name}].
  • ifc_relations(element_id, traverse="") — All relationships for an element. Use traverse="up" to walk the hierarchy up to IfcProject.
  • ifc_clash(element_id, clearance=0, tolerance=0.002, scope="storey") — Geometric clash detection within the same storey (or wider scope).
  • ifc_validate(express_rules=False) — Schema and constraint validation. Pass express_rules=True for full EXPRESS rule checking (slower). Returns {"valid": bool, "issues": [...]}.
  • ifc_schedule(max_depth=None) — Work schedules with nested task trees and start/finish dates.
  • ifc_cost(max_depth=None) — Cost schedules with nested cost item trees.
  • ifc_schema(entity_type) — IFC class documentation for the loaded model's schema version.

Edit discovery tools

  • ifc_list(module="") — List all API modules, or functions within a module (e.g. "sequence", "root", "geometry").
  • ifc_docs(function_path) — Full parameter documentation for an API function (e.g. "root.remove_product", "sequence.add_task").

Edit execution

  • ifc_edit(function_path, params="{}") — Execute an ifcopenshell.api mutation. params is a JSON string. Returns {"ok": true, "result": ...} or {"ok": false, "error": "..."}. Does not auto-save.
  • ifc_quantify(rule, selector="") — Run quantity take-off. Writes IfcElementQuantity psets to the in-memory model. Does not auto-save. Rules: "IFC4QtoBaseQuantities" or "IFC4X3QtoBaseQuantities".

Parameter coercion in ifc_edit

Type JSON value Python result
entity_instance "42" Entity resolved from model by step ID
list[entity_instance] "5,6,7" List of resolved entities
dict '{"key": "val"}' Parsed JSON object
bool "true" True
Optional[X] "none" None

Standard Workflow

ifc_load("model.ifc")
ifc_summary()                          # understand what's in the model
ifc_tree()                             # see spatial hierarchy
ifc_validate(express_rules=True)       # check model health before editing

# Query elements of interest
ifc_select("IfcWall")
ifc_info(<id>)
ifc_relations(<id>)

# Discover available edits
ifc_list("sequence")
ifc_docs("sequence.add_task")

# Edit (multiple calls, all in-memory)
ifc_edit("...", '{"param": "value"}')

# Verify, then save
ifc_validate()
ifc_save()

IFC Concepts

Spatial Hierarchy

IfcProject
  └─ IfcSite
       └─ IfcBuilding
            ├─ IfcBuildingStorey "Ground Floor"
            │    ├─ IfcSpace "kitchen"  (aggregated via IfcRelAggregates)
            │    │    ├─ IfcWindow  (contained in space via IfcRelContainedInSpatialStructure)
            │    │    └─ IfcDoor
            │    ├─ IfcWall  (contained directly in storey via IfcRelContainedInSpatialStructure)
            │    │    └─ IfcOpeningElement  (via IfcRelVoidsElement)
            │    │         └─ IfcWindow  (via IfcRelFillsElement)
            │    ├─ IfcElementAssembly  (optional grouping of walls)
            │    │    └─ IfcWall  (parts, via IfcRelAggregates)
            │    ├─ IfcSlab, IfcFooting, ...
            └─ IfcBuildingStorey "First Floor"
                 └─ ...

Important notes on containment:

  • Elements are contained in a spatial element (project, site, building, storey, or space) via IfcRelContainedInSpatialStructure — windows and doors are not necessarily in a space; they may be contained directly in a storey or building
  • Walls may appear directly in a storey (contained), or as parts of an IfcElementAssembly (aggregated) — both patterns are valid
  • Windows/doors appear in two places simultaneously: spatially contained in a spatial element, and structurally filling an opening in a wall (via IfcRelFillsElement). Both relationships must be maintained when editing.

Key Relationships

Relationship Meaning
IfcRelAggregates Whole → parts (project → site, assembly → walls)
IfcRelContainedInSpatialStructure Element belongs to a spatial container
IfcRelVoidsElement Opening cuts through a wall/slab
IfcRelFillsElement Window/door fills an opening
IfcRelSpaceBoundary Element bounds a space (thermal/acoustic boundary)
IfcRelDefinesByType Element instance → type
IfcRelDefinesByProperties Element → property set
IfcRelAssignsToProcess Element → construction task
IfcRelSequence Task A finishes before task B starts

Common Editing Recipes

Deleting a window or door

Windows and doors have multiple relationships that must all be cleaned up:

  1. Find the opening: ifc_relations(<window_id>) → note filled_void → IfcOpeningElement ID
  2. Find space boundaries: ifc_relations(<window_id>) → note any IfcRelSpaceBoundary IDs referencing this element
  3. Delete the window: ifc_edit("root.remove_product", '{"product": "<window_id>"}')
  4. Delete the orphaned opening: ifc_edit("root.remove_product", '{"product": "<opening_id>"}')

root.remove_product is a smart delete — removes the entity plus all its relationships (geometry, placement, properties, materials, containment, type assignments, space boundaries). The IfcOpeningElement must be removed separately.

Verify no orphaned openings remain:

ifc_relations(<wall_id>)     # → check children.openings for unfilled IfcOpeningElements

Moving a window or door

ifc_edit("geometry.edit_object_placement", '{"product": "<window_id>", "matrix": [[...],[...],[...],[0,0,0,1]]}')
ifc_edit("geometry.edit_object_placement", '{"product": "<opening_id>", "matrix": [[...],[...],[...],[0,0,0,1]]}')

Always move the IfcOpeningElement to the same position as the window/door. Also check IfcRelSpaceBoundary entities — their geometry may need updating if the window moves between spaces or changes boundary.

Changing element types

ifc_select("IfcWindowType")            # list available types
ifc_edit("type.assign_type", '{"related_objects": "<window_id>", "relating_type": "<type_id>"}')

Type assignment also remaps geometry representations from the type to the occurrence.

Editing attributes on any entity

attribute.edit_attributes works on any entity, not just rooted products — useful for geometry entities, placements, task times, etc.:

ifc_edit("attribute.edit_attributes", '{"product": "<id>", "attributes": "{\"Depth\": 3.0}"}')

Note the nested JSON: params is a JSON string and attributes within it is also a JSON string.

Resizing element geometry

attribute.edit_attributes on IfcCartesianPointList2D.CoordList fails with a type error (expects AGGREGATE OF AGGREGATE OF DOUBLE, gets list). To resize a slab/extrusion, remove and recreate the representation instead:

# 1. Unassign and remove old representation
ifc_edit("geometry.unassign_representation", '{"product": "<element_id>", "representation": "<rep_id>"}')
ifc_edit("geometry.remove_representation", '{"representation": "<rep_id>"}')

# 2. Create new representation with correct dimensions
ifc_edit("geometry.add_slab_representation", '{"context": "11", "depth": 0.8, "polyline": "[[0,0],[0.6,0],[0.6,0.5],[0,0.5]]"}')

# 3. Assign new representation
ifc_edit("geometry.assign_representation", '{"product": "<element_id>", "representation": "<new_rep_id>"}')

Note: the polyline parameter must be passed as a JSON string (not a bare array).

Tracing geometry chains

Walk entity references to find extrusion depths, clipping planes, etc.:

ifc_info(<wall_id>)                    # → Representation → IfcProductDefinitionShape id
ifc_info(<pds_id>)                     # → Representations → IfcShapeRepresentation id (Body)
ifc_info(<body_rep_id>)                # → Items → IfcExtrudedAreaSolid or IfcBooleanClippingResult

Batch parallel ifc_info calls to speed up traversal across many elements.


Quantity Take-Off (QTO)

Run ifc_quantify to compute quantities. Results are written as IfcElementQuantity property sets on each element.

ifc_quantify("IFC4QtoBaseQuantities")              # IFC4 models
ifc_quantify("IFC4X3QtoBaseQuantities")            # IFC4x3 models
ifc_quantify("IFC4QtoBaseQuantities", "IfcWall")   # restrict to element type
ifc_save()                                          # persist to disk

Weight availability by element type (IFC4QtoBaseQuantities):

Element Weight?
IfcWall, IfcSlab, IfcFooting, IfcBeam, IfcColumn ✓ GrossWeight, NetWeight
IfcWindow, IfcDoor ✗ Area/dimensions only
IfcRoof ✗ GrossArea only
IfcCovering ✗ GrossArea/Width only
IfcPipeSegment ✗ Length only

Weight is computed only for structural/volumetric elements with known material density.


Construction Scheduling

Schedule structure

IfcWorkPlan
  └─ IfcWorkSchedule
       ├─ IfcTask "P1: Foundations"  (summary)
       │    ├─ IfcTask "P1.1: Install Ground Beams"  (leaf, has IfcTaskTime)
       │    └─ IfcTask "P1.2: Pour Floor Slab"       (leaf, has IfcTaskTime)
       ├─ IfcTask "P2: Structure"    (summary, has IfcTaskTime)
       │    └─ IfcTask "P2.1: Erect Walls"           (leaf, has IfcTaskTime)
       └─ ...

Physical elements are linked to tasks via IfcRelAssignsToProcess. Task ordering is via IfcRelSequence (FINISH_START, START_START, etc.).

Creating a schedule (MCP recipe)

# Work plan and schedule
ifc_edit("sequence.add_work_plan", '{"name": "Construction Plan", "predefined_type": "ACTUAL"}')
ifc_edit("sequence.add_work_schedule", '{"name": "Construction Schedule", "predefined_type": "PLANNED", "work_plan": "<plan_id>"}')

# Phase (summary) tasks
ifc_edit("sequence.add_task", '{"work_schedule": "<sched_id>", "name": "Foundations", "identification": "P1", "predefined_type": "CONSTRUCTION"}')

# Leaf tasks
ifc_edit("sequence.add_task", '{"parent_task": "<phase_id>", "name": "Install Ground Beams", "identification": "P1.1", "predefined_type": "CONSTRUCTION"}')

# Assign physical elements to tasks
ifc_edit("sequence.assign_process", '{"relating_process": "<task_id>", "related_object": "<element_id>"}')

# Sequence relationships between phases/tasks
ifc_edit("sequence.assign_sequence", '{"relating_process": "<pred_id>", "related_process": "<succ_id>", "sequence_type": "FINISH_START"}')

# Add IfcTaskTime to ALL tasks (leaf AND summary — required for cascade to propagate)
ifc_edit("sequence.add_task_time", '{"task": "<task_id>"}')   # → returns IfcTaskTime id

# Set durations and start dates (use attribute.edit_attributes — see bug note below)
ifc_edit("attribute.edit_attributes", '{"product": "<task_time_id>", "attributes": "{\"ScheduleStart\": \"2026-03-02T09:00:00\", \"ScheduleDuration\": \"P5D\"}"}')

# Cascade dates forward through sequence relationships
ifc_edit("sequence.cascade_schedule", '{"task": "<first_task_id>"}')

# Verify
ifc_schedule()

Work calendar (MonFri)

ifc_edit("sequence.add_work_calendar", '{"name": "Mon-Fri Work Week"}')
ifc_edit("sequence.add_work_time", '{"work_calendar": "<cal_id>", "time_type": "WorkingTimes"}')
ifc_edit("sequence.assign_recurrence_pattern", '{"parent": "<work_time_id>", "recurrence_type": "WEEKLY"}')
ifc_edit("sequence.edit_recurrence_pattern", '{"recurrence_pattern": "<pat_id>", "attributes": "{\"WeekdayComponent\": [1,2,3,4,5]}"}')

WeekdayComponent uses integers 17 where 1=Monday, 7=Sunday.

cascade_schedule limitations

  • Every task in the chain (summary AND leaf) must have an IfcTaskTime — tasks without one have null start/finish and block propagation to successors
  • Cascade propagates through IfcRelSequence relationships only, not into subtasks of a summary task. After cascade sets a summary task's start, its leaf subtasks must have their starts set manually (or connected via additional leaf-to-leaf sequences)
  • Summary task finish = start + duration; without a duration, finish equals start, propagating wrong dates to successors
  • cascade_schedule does not automatically respect a work calendar; weekend-skipping must be applied manually when setting task dates

Known bug: sequence.edit_task_time silently fails for datetime fields

sequence.edit_task_time returns {"ok": true} but does not persist ScheduleStart or ScheduleFinish changes. Use attribute.edit_attributes on the IfcTaskTime entity instead:

# WRONG — silently does nothing for datetime fields:
ifc_edit("sequence.edit_task_time", '{"task_time": "123", "attributes": "{\"ScheduleStart\": \"2026-03-09T09:00:00\"}"}')

# CORRECT:
ifc_edit("attribute.edit_attributes", '{"product": "123", "attributes": "{\"ScheduleStart\": \"2026-03-09T09:00:00\"}"}')

sequence.edit_task_time works correctly for ScheduleDuration — only datetime fields are affected.


Bill of Quantities (BoQ)

Concepts

IFC separates quantities (how much) from pricing (how much it costs). They are independent layers:

  • Quantities live on elements as IfcElementQuantity property sets (written by ifc_quantify). These must exist before building a cost schedule.
  • Pricing lives in a separate IfcCostScheduleIfcCostItem tree. Unit rates are stored on IfcCostValue entities attached to cost items, not on elements.
  • At BoQ generation time, a tool multiplies unit rate × quantity to compute line-item totals.
  • The same element can appear in multiple cost schedules with different rates; rates can be updated without touching geometry.

Cost schedule structure

IfcCostSchedule  "Bill of Quantities"  (predefined_type: COSTPLAN or PRICEDBILLOFQUANTITIES)
  └─ IfcCostItem  "A - Substructure"   (section/summary — has IfcCostValue with Category="*")
       ├─ IfcCostItem  "A.1 - Ground Beams"  (leaf — has IfcCostValue with unit rate)
       │    linked to IfcFooting elements via IfcRelAssignsToControl
       └─ IfcCostItem  "A.2 - Floor Slab"
            linked to IfcSlab elements via IfcRelAssignsToControl

Elements are linked to cost items via assign_cost_item_quantity, which also creates the IfcRelAssignsToControl relationship automatically.

Quantity names by element type

Element Useful quantity names
IfcWall, IfcSlab, IfcFooting NetVolume, GrossVolume
IfcWindow, IfcDoor Area
IfcRoof, IfcCovering GrossArea, NetArea
IfcPipeSegment Length

MCP recipe — creating a BoQ from scratch

# 1. Ensure quantities are computed first
ifc_quantify("IFC4QtoBaseQuantities")

# 2. Create the cost schedule
ifc_edit("cost.add_cost_schedule", '{"name": "Bill of Quantities", "predefined_type": "COSTPLAN"}')
# → returns IfcCostSchedule id (e.g. 100)

# 3. Create section (summary) items at top level
ifc_edit("cost.add_cost_item", '{"cost_schedule": "100"}')
# → returns IfcCostItem id (e.g. 101)
ifc_edit("cost.edit_cost_item", '{"cost_item": "101", "attributes": "{\"Name\": \"Substructure\", \"Identification\": \"A\"}"}')

# 4. Create leaf items under a section
ifc_edit("cost.add_cost_item", '{"cost_item": "101"}')
# → returns id (e.g. 102)
ifc_edit("cost.edit_cost_item", '{"cost_item": "102", "attributes": "{\"Name\": \"Ground Beams\", \"Identification\": \"A.1\"}"}')

# 5. Link elements parametrically — quantities update automatically if geometry changes
ifc_edit("cost.assign_cost_item_quantity", '{"cost_item": "102", "products": "10,11,12,13", "prop_name": "NetVolume"}')

# 6. Add a unit rate to the leaf item
ifc_edit("cost.add_cost_value", '{"parent": "102"}')
# → returns IfcCostValue id (e.g. 200)
ifc_edit("cost.edit_cost_value", '{"cost_value": "200", "attributes": "{\"AppliedValue\": 350.0}"}')

# 7. Add a sum value to section items (Category="*" marks it as a subtotal)
#    This is required for PDF export — compute total = sum(quantity × rate) for each section
ifc_edit("cost.add_cost_value", '{"parent": "101"}')
# → returns id (e.g. 201)
ifc_edit("cost.edit_cost_value", '{"cost_value": "201", "attributes": "{\"Category\": \"*\", \"AppliedValue\": 2500.0}"}')

# 8. Verify and save
ifc_cost()
ifc_validate()
ifc_save()

Key API notes

  • cost.add_cost_item accepts no name parameter — set name afterwards with cost.edit_cost_item
  • cost.edit_cost_item and cost.edit_cost_value require attributes as a JSON string (not a dict):
    '{"attributes": "{\"Name\": \"Ground Beams\"}"}'   # correct — double-encoded
    '{"attributes": {"Name": "Ground Beams"}}'          # WRONG — will error
    
  • assign_cost_item_quantity with prop_name="" counts elements instead of summing a named quantity
  • AppliedValue accepts a plain float (stored as IfcMonetaryMeasure)

Section items MUST have Category="*" cost values

Section/summary items with no quantities and no Category="*" cost value have ItemIsASum=False in the export. This causes the PDF exporter to try parsing their empty Quantity field as a float and crash. Every section item must have an IfcCostValue with Category="*" and AppliedValue set to the pre-computed section total.

Exporting to PDF

The ifc5d PDF writer produces the best-looking output. The format flag requires uppercase:

# PDF — invoke directly (not wired into the CLI format switch)
python3 -c "
import ifcopenshell
from ifc5d.ifc5Dspreadsheet import Ifc5DPdfWriter
f = ifcopenshell.open('model.ifc')
cs = next(iter(f.by_type('IfcCostSchedule')))
Ifc5DPdfWriter(file=f, output='boq.pdf', options={}, cost_schedule=cs,
               force_schedule_type='PRICEDBILLOFQUANTITIES').write()
"

# XLSX (via CLI — uppercase format required)
python3 -m ifc5d.ifc5Dspreadsheet -f XLSX model.ifc ./output_dir/

# CSV
python3 -m ifc5d.ifc5Dspreadsheet -f CSV model.ifc ./output_dir/

Requires typst for PDF: pip install typst

Known ifc5d limitations:

  • The -f flag is case-sensitive: XLSX, CSV, ODS (lowercase silently fails with NameError: writer)
  • PDF is not in the CLI format switch — must be called directly from Python (see above)
  • XLSX/CSV TotalPrice column is always 0 for unit-rate items; totals are only computed as spreadsheet formulas in the ODS writer (which has a column-name bug), and at render time in the typst PDF template
  • The typst PDF template computes Total = Quantity × RateSubtotal for leaf items; for section items it reads TotalPrice from the Category="*" cost value

Validation and CI/CD

Manual validation

ifc_validate()                         # schema validation (fast)
ifc_validate(express_rules=True)       # full EXPRESS rules (slower, more thorough)

Or via CLI:

python3 -m ifcquery <file> validate
python3 -m ifcquery <file> validate --rules

IDS validation

IDS (Information Delivery Specification) files define requirements that models must satisfy — required property sets, classification codes, material assignments, etc. Validate with ifctester:

python3 -m ifctester <ids_file> <ifc_file>

IDS checks are typically run in CI on every commit or pull request.

Reading ifctester output:

  • [PASS] (0/0) — no elements matched the applicability filter; check trivially passes (minOccurs="0")
  • [PASS] (5/5) — 5 elements matched and all 5 passed the requirement
  • [FAIL] (0/5) — 5 elements matched but none passed; the failure lines list each failing element

IDS structure: each specification has two parts:

  • <applicability> — filter selecting which elements the check applies to (AND logic across multiple facets)
  • <requirements> — what those elements must have (property name, value, pset name)

Fixing IDS failures — workflow:

  1. Read the IDS file to understand exactly what property name, pset name, and value are required
  2. Use ifc_info(<element_id>) to inspect the failing element's current property sets
  3. Note whether multiple elements share the same IfcPropertySet entity (same id) — editing it once fixes all of them
  4. Use pset.edit_pset to add missing properties:
ifc_edit("pset.edit_pset", '{"pset": "<pset_id>", "properties": "{\"FireRating\": \"30\"}", "should_purge": "false"}')

Use should_purge: false to add/update properties without removing existing ones.

Critical: IDS property names are exact string matches. A property named Thermal Transmitance (space, single-t) is entirely different from the IFC standard ThermalTransmittance. Always copy property names character-for-character from the IDS <baseName> element — do not assume they match standard IFC pset property names.

Shared psets: in IFC, multiple elements (instances and their type) often share the same IfcPropertySet entity via IfcRelDefinesByProperties. Check the "id" field in ifc_info property set output — if 4 walls and a WallType all show the same pset id, one pset.edit_pset call fixes all of them simultaneously.

Known ifctester quirk with boolean applicability filters: ifctester may flag elements even when their boolean property value does not match the applicability filter (e.g., an IsExternal=True wall being matched by an IsExternal=False filter). Treat the reported failing elements as authoritative — add whatever the requirement asks for, regardless of whether the applicability logic seems correct.

<simpleValue>?</simpleValue> is NOT a wildcard in ifctester — it matches the literal string "?". To check that an attribute has any non-empty value, use an xs:restriction pattern instead:

<!-- WRONG — matches only the literal string "?" -->
<attribute>
  <name><simpleValue>Name</simpleValue></name>
  <value><simpleValue>?</simpleValue></value>
</attribute>

<!-- CORRECT — matches any non-empty string -->
<attribute>
  <name><simpleValue>Name</simpleValue></name>
  <value>
    <xs:restriction base="xs:string">
      <xs:pattern value=".+"/>
    </xs:restriction>
  </value>
</attribute>

This applies to <attribute> requirements. The xmlns:xs="http://www.w3.org/2001/XMLSchema" namespace must be declared on the root <ids> element (it is in the standard IDS template).

ifcopenshell.validate

Lower-level programmatic validation:

python3 -c "import ifcopenshell; import ifcopenshell.validate; f = ifcopenshell.open('<file>'); ifcopenshell.validate.validate(f)"

Bonsai Asset Libraries

Bonsai ships IFC4 asset libraries at:

/usr/lib/python3.14/site-packages/bonsai/bim/data/libraries/

Key libraries:

  • IFC4 Furniture Library.ifc — 150 IfcFurnitureType entries (beds, chairs, sofas, tables, desks, kitchen units, etc.)
  • IFC4 Landscape Library.ifc — 66 IfcGeographicElementType entries (Apple, Round Tree, Conical Tree Small (5m), Shrub, etc.)
  • IFC4 Entourage Library.ifcIfcBuildingElementProxyType people figures

Appending a library type

Use ifc_edit("project.append_asset", ...) via MCP if available. At the time of writing, the MCP coercion layer had no handler for ifcopenshell.file-typed parameters, so the library argument could not be passed through ifc_edit — this may have since been fixed in ifcedit/ifcmcp. If MCP does not work, fall back to Python:

import ifcopenshell, ifcopenshell.api.project, ifcopenshell.api.type

LIB = '/usr/lib/python3.14/site-packages/bonsai/bim/data/libraries/IFC4 Landscape Library.ifc'
f   = ifcopenshell.open('model.ifc')
lib = ifcopenshell.open(LIB)

# Find the type in the library
apple_type_in_lib = next(t for t in lib.by_type('IfcGeographicElementType') if t.Name == 'Apple')

# Copy into the model — returns the new type entity in f
apple = ifcopenshell.api.project.append_asset(f, library=lib, element=apple_type_in_lib)

# Assign to an occurrence
tree = f.by_id(<tree_element_id>)
ifcopenshell.api.type.assign_type(f, related_objects=[tree], relating_type=apple)

f.write('model.ifc')

append_asset copies all dependent geometry, materials, and styles. The occurrence keeps its own name, placement, and properties while getting the type's geometry.


Git Workflow

IFC files use STEP Physical File format — plain text, one entity per line — which is natively git-friendly. Commits, diffs, rollbacks, and blame all work as expected.

However, IFC branches do not merge correctly with standard git because STEP IDs are file-scoped integers that conflict across branches. Use ifcmerge as the merge driver:

Configuring ifcmerge

.gitattributes is already committed to this repo with *.ifc merge=ifcmerge.

Add the driver to .git/config or ~/.gitconfig:

[merge "ifcmerge"]
    name = IFC merge driver
    driver = /home/bruno/src/ifcmerge/ifcmerge %O %A %B %P

ifcmerge behaviour

  • By default ifcmerge prefers the $REMOTE branch and rewrites step IDs in $LOCAL to avoid conflicts. This is the correct behaviour when merging from main/origin into your local branch (i.e. a git pull or git merge main).
  • When merging a feature branch into main (i.e. you want $LOCAL=main to be the authoritative base), reverse the driver arguments so that main's step IDs are preserved:
    driver = ifcmerge %O %B %A %P
    
    The easiest approach is to merge in the other direction (merge main into the feature branch first to resolve conflicts there, then fast-forward main), which keeps the default driver order correct.

Typical branch workflow

  • main — validated, CI-passing models only
  • Feature branches for each change (new elements, schedule additions, geometry edits, etc.)
  • Open a pull request → CI runs ifc_validate and IDS checks → merge via ifcmerge on approval
  • Tag releases for milestone model snapshots

Tips

  • Always validate before saving — run ifc_validate() after edits and before ifc_save()
  • ifc_info on non-product entities — works on any step ID including geometry, task times, placements; useful for tracing chains
  • Batch parallel tool calls — independent ifc_info and ifc_edit calls can be issued in parallel for speed
  • ifc_edit does not auto-save — always call ifc_save() explicitly when done with a set of edits
  • ISO 8601 durations — task durations use P5D (5 days), P1W (1 week), PT8H (8 hours)
  • Step IDs are file-specific — never hard-code step IDs; always query first with ifc_select, ifc_tree, or ifc_info
  • Space boundaries — when adding, moving, or deleting windows and doors, check for IfcRelSpaceBoundary relationships that may also need updating