simple-ifc/CLAUDE.md

635 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`):
```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)
```bash
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:
```python
# 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 `IfcCostSchedule``IfcCostItem` 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**:
```bash
# 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:
```bash
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`:
```bash
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:
```xml
<!-- 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:
```bash
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.ifc``IfcBuildingElementProxyType` 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:
```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