23 KiB
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,
ifcmergefor 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. Usetraverse="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. Passexpress_rules=Truefor 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 anifcopenshell.apimutation.paramsis a JSON string. Returns{"ok": true, "result": ...}or{"ok": false, "error": "..."}. Does not auto-save.ifc_quantify(rule, selector="")— Run quantity take-off. WritesIfcElementQuantitypsets 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:
- Find the opening:
ifc_relations(<window_id>)→ notefilled_void→ IfcOpeningElement ID - Find space boundaries:
ifc_relations(<window_id>)→ note anyIfcRelSpaceBoundaryIDs referencing this element - Delete the window:
ifc_edit("root.remove_product", '{"product": "<window_id>"}') - 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.
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": "BASELINE", "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 (Mon–Fri)
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 1–7 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
IfcRelSequencerelationships 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_scheduledoes 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
IfcElementQuantityproperty sets (written byifc_quantify). These must exist before building a cost schedule. - Pricing lives in a separate
IfcCostSchedule→IfcCostItemtree. Unit rates are stored onIfcCostValueentities attached to cost items, not on elements. - At BoQ generation time, a tool multiplies
unit rate × quantityto 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_itemaccepts nonameparameter — set name afterwards withcost.edit_cost_itemcost.edit_cost_itemandcost.edit_cost_valuerequireattributesas a JSON string (not a dict):'{"attributes": "{\"Name\": \"Ground Beams\"}"}' # correct — double-encoded '{"attributes": {"Name": "Ground Beams"}}' # WRONG — will errorassign_cost_item_quantitywithprop_name=""counts elements instead of summing a named quantityAppliedValueaccepts a plain float (stored asIfcMonetaryMeasure)
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
-fflag is case-sensitive:XLSX,CSV,ODS(lowercase silently fails withNameError: writer) - PDF is not in the CLI format switch — must be called directly from Python (see above)
- XLSX/CSV
TotalPricecolumn 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 × RateSubtotalfor leaf items; for section items it readsTotalPricefrom theCategory="*"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.
ifcopenshell.validate
Lower-level programmatic validation:
python3 -c "import ifcopenshell; import ifcopenshell.validate; f = ifcopenshell.open('<file>'); ifcopenshell.validate.validate(f)"
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
In .gitattributes:
*.ifc merge=ifcmerge
In .git/config or ~/.gitconfig:
[merge "ifcmerge"]
name = IFC merge driver
driver = ifcmerge %O %A %B %P
ifcmerge behaviour
- By default ifcmerge prefers the
$REMOTEbranch and rewrites step IDs in$LOCALto avoid conflicts. This is the correct behaviour when merging frommain/origininto your local branch (i.e. agit pullorgit 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:
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.driver = ifcmerge %O %B %A %P
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_validateand IDS checks → merge via ifcmerge on approval - Tag releases for milestone model snapshots
Tips
- Always validate before saving — run
ifc_validate()after edits and beforeifc_save() ifc_infoon non-product entities — works on any step ID including geometry, task times, placements; useful for tracing chains- Batch parallel tool calls — independent
ifc_infoandifc_editcalls can be issued in parallel for speed ifc_editdoes not auto-save — always callifc_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, orifc_info - Space boundaries — when adding, moving, or deleting windows and doors, check for
IfcRelSpaceBoundaryrelationships that may also need updating