Skip to content

IFC CLI interface#7660

Draft
brunopostle wants to merge 25 commits intov0.8.0from
ifc_cli
Draft

IFC CLI interface#7660
brunopostle wants to merge 25 commits intov0.8.0from
ifc_cli

Conversation

@brunopostle
Copy link
Contributor

@brunopostle brunopostle commented Feb 10, 2026

I'm putting this on a secret branch in an attempt to avoid being slop-shamed, I would slop-shame me. All code here is generated by an LLM.

This branch adds two new command-line tools: src/ifcquery and src/ifcedit, they can be installed separately using something like pip install -e src/ifcquery/ and pip install -e src/ifcedit/. More information about these tools is in their respective README files, here are summaries:

ifcquery: A CLI tool for querying and inspecting IFC building models. All output is structured JSON (or human-readable text), making it easy to pipe into other tools or scripts.

ifcedit: A CLI wrapper that exposes all 350+ ifcopenshell.api modifier functions as shell commands. Functions are auto-discovered at runtime via introspection.

Actually ifcedit is really cool, and does a lot for such a small tool, but this is not the reason for creating this. What these tools provide is just enough discoverable functionality for an AI agent (I'm using Claude Code) to query and manipulate IFC files in the same way that the same agent can be used to work with source code.

To start you first need to have both tools working and in your PATH, which might be simple or hard depending on your system, then create a folder, add an IFC file to the folder, and start up your coding agent.

You need a prompt to get going, something like this (untested, but it will probably work):

Hi there! you now have the ability to work with IFC files, these are BIM models that describe entire construction projects such as buildings. To do this you have been provided with two tools: ifcquery for inspecting and querying IFC files and ifcedit for manipulating them. These tools are all you need for this task, there is definitely and absolutely no need to ever open the IFC file directly or construct ifcopenshell python code on the fly to access it.

Start by running the tools to discover their functionality, they both have a --help parameter that tells you what commands are available. You have been provided with a test IFC file, so try some searches, such as finding a storey and counting windows on that storey; then try some manipulation, add a pset parameter, check it worked, delete it, check that worked, or do some other tests to make sure you have a good outline of the system capabilities.

Native IFC files are Git friendly so feel free to create a repository, commit the IFC file, roll-back etc.. as necessary. However note that IFC branches don't merge by default and need some help from a tool called ifcmerge, this needs to be configured as a mergetool in Git - note that ifcmerge by default prefers the $REMOTE branch and will rewrite some step IDs in the $LOCAL branch, this is perfect when you are merging from main or the origin, but if you want to merge a throwaway branch into main you need to reverse the order of $REMOTE and $LOCAL - but you know more about Git than I do, so you'll figure this out when you need it.

After learning about the tools, create or update your CLAUDE.md file with everything you have learnt. Congratulations, you are now an Architect, feel free to enjoy obscure coffee devices, long-hours, and black turtle-necked sweaters!

Provides four subcommands for querying IFC models with JSON output:
- summary: schema, entity counts, project info
- tree: spatial hierarchy (Project > Site > Building > Storey > elements)
- info: deep element inspection (attributes, psets, type, material, container)
- select: filter elements using selector syntax
Runtime introspection auto-discovers all API functions. Subcommands:
list (modules/functions), docs (parameter documentation), run (execute
with type-coerced CLI arguments). Output is JSON to stdout, supports
--dry-run validation and -o alternate output path.
New subcommand: ifcquery <file> relations <element_id>
Returns all relationships for an element organised by category:
hierarchy (parent, container, aggregate, nest), children (contained,
parts, components, openings), type relationships, groups, systems,
zones, material, referenced structures, and connections/ports.
Empty categories are omitted from output.

Optional --traverse up flag walks the hierarchy from the element
up to IfcProject, returning the chain as a list.
Uses ifcopenshell.geom.tree API directly to check an element for
intersections and clearance violations against sibling elements
(--scope storey) or the entire model (--scope all).
@aothms
Copy link
Member

aothms commented Feb 10, 2026

This is so cool, this completely eliminates the problems with this idiosyncratic context-less serialization and I see you have things like ifcclash to figure out geometric relationships; which is huge to make sense of the model, explicit points carry so little semantics otherwise. Maybe the only thing missing is something like render to image for something multi-modal. Here's something for pyvista that I have on my disk, maybe you can adapt it into a subcommand.

Is it ok to forward this to others or do you prefer to keep it quiet for some time?

import os
import glob
import numpy as np
import ifcopenshell
import ifcopenshell.geom
import pyvista as pv

IFC_FOLDER = "."

def add_shape_to_plotter(shape, plotter: pv.Plotter):
    geom = shape.geometry

    verts = np.array(geom.verts, dtype=float).reshape(-1, 3)
    if verts.size == 0:
        return

    faces = np.array(geom.faces, dtype=int).reshape(-1, 3)

    material_ids = np.array(geom.material_ids, dtype=int)
    for midx, mat in enumerate(geom.materials):
        tri_mask = material_ids == midx
        if not np.any(tri_mask):
            continue

        sub_faces = faces[tri_mask]
        faces_pv = np.hstack(
            [np.full((sub_faces.shape[0], 1), 3, dtype=int), sub_faces]
        ).ravel()

        mesh = pv.PolyData(verts, faces_pv)

        diffuse = np.array(mat.diffuse.components)
        diffuse = np.clip(diffuse, 0.0, 1.0)
        color = tuple((diffuse * 255).astype(np.uint8))

        transparency = mat.transparency if mat.transparency == mat.transparency else 0.
        opacity = float(np.clip(1.0 - transparency, 0.0, 1.0))

        plotter.add_mesh(
            mesh,
            color=color,
            opacity=opacity,
            show_edges=False,
        )

    edges = np.array(geom.edges, dtype=int).reshape(-1, 2)
    if edges.size > 0:
        lines_pv = np.hstack(
            [np.full((edges.shape[0], 1), 2, dtype=int), edges]
        ).ravel()

        line_mesh = pv.PolyData(verts, lines=lines_pv)

        plotter.add_mesh(
            line_mesh,
            line_width=1,
            color="black",
        )


def ifc_to_pyvista_screenshot(ifc_path: str, screenshot_path: str):
    print(f"Processing: {ifc_path}")

    settings = ifcopenshell.geom.settings(USE_WORLD_COORDS=True)
    ifc_file = ifcopenshell.open(ifc_path)

    iterator = ifcopenshell.geom.iterator(settings, ifc_file, exclude=('IfcOpeningElement',))
    if not iterator.initialize():
        print(f"  No geometry found in {ifc_path}")
        return

    plotter = pv.Plotter(off_screen=True)
    plotter.background_color = "white"

    while True:
        shape = iterator.get()
        add_shape_to_plotter(shape, plotter)

        if not iterator.next():
            break

    os.makedirs(os.path.dirname(screenshot_path), exist_ok=True)
    print(f"  Saving screenshot to: {screenshot_path}")
    plotter.show(screenshot=screenshot_path, auto_close=True)


def main():
    render_dir = os.path.join(IFC_FOLDER, "img")
    os.makedirs(render_dir, exist_ok=True)

    ifc_files = glob.glob(os.path.join(IFC_FOLDER, "*.ifc"))
    if not ifc_files:
        print(f"No IFC files found in {IFC_FOLDER}")
        return

    for ifc_path in ifc_files:
        base = os.path.splitext(os.path.basename(ifc_path))[0]
        out_png = os.path.join(render_dir, f"{base}.png")
        ifc_to_pyvista_screenshot(ifc_path, out_png)


if __name__ == "__main__":
    main()

@brunopostle
Copy link
Contributor Author

@aothms of course you can share it :) I'm just putting it in a technical forum so I don't have to answer non-technical questions.

The viewer looks fine, you should commit it to IfcOpenShell! I'm just using the revert button in Bonsai to view model changes as they happen.

It urgently needs porting to an MCP server, the CLI interface works fine, but every command involves reloading the entire IFC file. An MCP server would load the file once and be an order of magnitude faster.

@brunopostle
Copy link
Contributor Author

@aothms The prompt should also include something about using ifctester and ifcopenshell.validate for test driven development.

The ifcclash functionality appears to work, but it wouldn't surprise me if there were bugs here. I have another CLI tool that queries a topologic model in the IFC (my Homemaker generated buildings contain a full topologic CellComplex serialisation) - so you can give the bot an understanding of place and spatial relationships, but this requires a Homemaker model, and doesn't update when you move a wall.

@brunopostle
Copy link
Contributor Author

To be clear, this does actually work. I have taken a model, deleted windows using a vague location specification, moved remaining windows, changed their type, and raised the height of a storey - all without any major problems using just a chat interface - the bot needed to be told that editing windows involves editing their openings, but after that it had no problem.

@aothms
Copy link
Member

aothms commented Feb 10, 2026

Maybe it's time that we think about unifying this API (maybe not to the depth of homemaker functionality) but the idea of a cellcomplex / compositesolid kind of topology that guides the creation of IFC elements. Because that is probably the right level of abstraction dealing with programmatic creation of IFC (for the space bounding elements at least).

(We also have another more declarative/scenegraph-style geometry+ifc creation module hopefully soon to be merged, I think that would be quite complementary)

Model Context Protocol server implementation for AI agent access to IFC
projects
@brunopostle
Copy link
Contributor Author

@aothms Ok, I added an MCP server implementation: src/ifcmcp.

Seems to work exactly the same but much faster with big models.

@brunopostle
Copy link
Contributor Author

I think we should be alarmed.

I dislike the hype and bubble of AI, I hate that it depends on someone else's data centre running proprietary software consuming vast resources. I accept the entire TESCREAL criticism of the industry, and this critique is more valid and concerning than ever, but the one criticism that is no longer true is that 'it doesn't work'.

With coordinators like gas town now a thing, this is very doable:

Imagine a swarm of bots working on the same building project, working through a snagging list, and adding to it whenever they find a problem, generating IDS regression tests as they go.

One bot is making sure that every material and construction has thermal properties, others are making sure the spatial boundaries and structural model are coordinated with the architecture, some or all are doing clash detection as they go.

A pricing bot is finding prices on the internet, assigning them and building a bill of materials. The thermal model is checked through openstudio, with the results feeding back into the snagging queue. The structural model is checked for stability and loading, because all the elements in the model have density and mass, and all IsStructural elements have associated structural elements that line up and have equivalent specification.

Other bots are checking all doors in firewalls have the correct fire specification. The schedule is checked for consistency, for tasks taking place in the same zone simultaneously, for elements that are not in tasks, with any problems raised in the snagging queue.

Bots are checking drawings, ensuring that all elements are visible and labelled somewhere, checking that dimensions actually point to something, that section cuts are called out on plans, that labels are not clashing.

All this work is on the same IFC model, in parallel, using git branches, with the results merged back as they are checked and approved, lots of this merging can happen without human approval.

Other bots find ways to reduce costs by swapping out materials and structure. A human designer can interfere with the model, move walls, change the design or whatever, and the bots will clean up the mess afterwards.

This all sounds utopian, but it also sounds like hell (give me a month and a million and I'll have it all stood up and running).

@aothms
Copy link
Member

aothms commented Feb 12, 2026

I'm not all pessimistic of this. I recognise this in my own work as well, it's much easier now to explore more options / algorithms / approaches to a single problem. I imagine the same will happen with design variants and iteration. Problem shifts to understanding the problem, working out details will become more and more automated. Ultimately freeing up time to build a better more sustainable world. Probably I'm naive. But we can do our best to change the world for the better? Make sure our things work on open weights or self-hosted models etc.

@theoryshaw theoryshaw marked this pull request as draft February 17, 2026 12:16
@theoryshaw
Copy link
Member

Converted to draft, since you said 'don't merge'.

@brunopostle
Copy link
Contributor Author

@theoryshaw I've removed the 'do not merge' notice because this doesn't mess with any existing code, so can't break anything. I'd like a bit more testing of ifcquery in particular before merging.

@brunopostle
Copy link
Contributor Author

Just uploading the screenshots I posted to the matrix group. First this is my test building:

Screenshot From 2026-02-10 08-00-28

This is it after I asked it to delete the two middle windows from the first floor living room, move the remaining windows closer together, make them 'fancy' and raise the first floor storey height by 2m:

Screenshot From 2026-02-10 07-56-13

This is it after asking it to raise the second floor storey height by 2m, note that the gable walls have complex clipping, this is with the MCP interface:

image (2)

@aothms
Copy link
Member

aothms commented Feb 20, 2026

I have taken your work and trying to vibe a web-interface around it. It still breaks down at some trivial things, but I think your approach to this already works great (it's still a bit magical for me tbh).

Question for now is: are you ok with hosting this under ai.ifcopenshell.org once finished?

afbeelding

@aothms
Copy link
Member

aothms commented Feb 20, 2026

It's really amazing times we live in

afbeelding

before:

afbeelding

after:

afbeelding

I will submit this as a PR to your branch @brunopostle for you to review.

@aothms
Copy link
Member

aothms commented Feb 20, 2026

Also from scratch works quite well indeed.

afbeelding afbeelding

@brunopostle
Copy link
Contributor Author

@aothms the interesting thing is that when the bot gets it wrong, you just tell it what it did wrong, it sorts it out, and remembers for next time.

Guidelines for external contributors using AI coding tools,
covering licensing, AI disclosure requirements, PR scope,
commit style, code formatting, and testing expectations.

Generated with the assistance of an AI coding tool.
@brunopostle
Copy link
Contributor Author

Question for now is: are you ok with hosting this under ai.ifcopenshell.org once finished?

Yes of course, this is all intended to be merged into the release if the quality is good enough.

I notice that your pull-request removes some of the "This file was generated with the assistance of an AI coding tool." comments. I've cherry-picked the AGENTS.md file from the v0.8.0 branch, this should fix this sort of thing in the future.

brunopostle and others added 2 commits February 20, 2026 22:22
Actually create the ifcquery, ifcedit and ifcmcp executables on pip
install instead of relying on `python3 -m ifcquery` style usage
@aothms
Copy link
Member

aothms commented Feb 21, 2026

I notice that your pull-request removes some of the "This file was generated with the assistance of an AI coding tool." comments.

Added them back.

Also renamed git mv chat/ ifcchat

@aothms
Copy link
Member

aothms commented Feb 21, 2026

@aothms the interesting thing is that when the bot gets it wrong, you just tell it what it did wrong, it sorts it out, and remembers for next time.

I find it also interesting that it has the habit to check things. After deleting it did another query for windows and a info call on a deleted id to test.

But at the same time there is also a huge variance in the answers even in these simple cases. For example, when asking for the windows by level sometimes it starts from the window query walking upwards, sometimes it goes from the tree downwards. In the former case you don't get levels any levels with 0 windows. On the one hand this is of course due to the randomness/temperature, but maybe my question is also simply underconstrained and is therefore more susceptible to this kind of variance.

These are probably also things that can make it into the system prompt over time:

  • Don't forget to assign representations to elements when created
  • When deleting windows or doors ask whether the openings should be deleted as well
  • Where possible start from the ifc_tree to get a complete model breakdown

@brunopostle
Copy link
Contributor Author

* When deleting windows or doors ask whether the openings should be deleted as well

It definitely needs a reminder that doors and windows are often associated with openings, boundaries and structural elements that have closely related geometry.

@brunopostle
Copy link
Contributor Author

@aothms Here's a tool doing a similar thing with topologic:

https://github.com/Adventurous-Systems/topologicpy_MCP

…cmcp

ifcquery: validate [--rules], schedule [--depth N], cost [--depth N],
schema <EntityType>. ifcedit: quantify list/run subcommands using ifc5d
QTO rules. schedule and cost support max_depth to limit tree expansion,
replacing truncated levels with {truncated, count}. ifcmcp gains matching
session methods, @server.tool() decorators, and OpenAI tool schemas.

Generated with the assistance of an AI coding tool.
Adds ifc_validate, ifc_schedule, ifc_cost, ifc_schema, and ifc_quantify
to the hardcoded tool list in app.js to match the extended ifcmcp session.

Generated with the assistance of an AI coding tool.
Generated with the assistance of an AI coding tool.
@brunopostle
Copy link
Contributor Author

So, I added some more functionality: validation, 4d and 5d stuff.

Here's a test sequence:

❯ we are going to test new features in the mcp ifc interface, load the _test_simple.ifc model
 
Loaded _test_simple.ifc — IFC4 schema, 3320 entities. What features would you like to test?

❯ calculate quantities for the elements and types in the model

Quantification complete — 40 elements were quantified using IFC4QtoBaseQuantities.
The IfcElementQuantity psets have been written to the in-memory model.

The model contains walls, windows, slabs, footings, coverings, pipe segments, roofs, and
spaces. Want me to save the result to disk, or inspect the quantities on specific elements first?

❯ show the quantities for walls

Here are the Qto_WallBaseQuantities for all 4 walls:

  Wall ID: #111
  Length (m): 6.685
  Height (m): 4.385
  Width (m): 0.330
  Gross Side Area (m²): 25.15
  Net Side Area (m²): 18.61
  Gross Volume (m³): 7.69
  Net Volume (m³): 5.53
  Net Weight (kg): 12,976
  ────────────────────────────────────────
  Wall ID: #1316
  Length (m): 6.500
  Height (m): 4.385
  Width (m): 0.330
  Gross Side Area (m²): 24.48
  Net Side Area (m²): 17.93
  Gross Volume (m³): 7.48
  Net Volume (m³): 5.32
  Net Weight (kg): 12,475
  ────────────────────────────────────────
  Wall ID: #1664
  Length (m): 4.447
  Height (m): 3.000
  Width (m): 0.330
  Gross Side Area (m²): 14.23
  Net Side Area (m²): 10.97
  Gross Volume (m³): 4.11
  Net Volume (m³): 2.89
  Net Weight (kg): 6,788
  ────────────────────────────────────────
  Wall ID: #2173
  Length (m): 6.011
  Height (m): 3.000
  Width (m): 0.330
  Gross Side Area (m²): 18.79
  Net Side Area (m²): 13.89
  Gross Volume (m³): 5.58
  Net Volume (m³): 3.76
  Net Weight (kg): 8,809

  A few observations:
  - Two walls (#111, #1316) are taller (~4.38m) — likely on a storey with a sloped roof
  - Two walls (#1664, #2173) are 3.0m — standard floor-to-ceiling height
  - Net side area is noticeably smaller than gross due to window openings (the model has 11 windows)
  - All walls are 330mm wide and of type "exterior"

❯ do all elements have weights? can you tell me how much this building weighs? 

Here's the full picture:

  Elements WITH weight (NetWeight from QTO):

  ┌─────────┬───────┬──────────────┬────────────────┐
  │  Type   │ Count │   Element    │ NetWeight (kg) │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Wall    │ #111  │ exterior     │ 12,975.6       │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Wall    │ #1316 │ exterior     │ 12,474.9       │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Wall    │ #1664 │ exterior     │ 6,788.2        │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Wall    │ #2173 │ exterior     │ 8,808.8        │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Slab    │ #3125 │ living-floor │ 15,865.1       │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Footing │ #2530 │ ground beam  │ 1,464.4        │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Footing │ #2584 │ ground beam  │ 949.2          │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Footing │ #2637 │ ground beam  │ 1,423.8        │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Footing │ #2690 │ ground beam  │ 1,291.8        │
  ├─────────┼───────┼──────────────┼────────────────┤
  │ Total   │       │              │ 62,041.8 kg    │
  └─────────┴───────┴──────────────┴────────────────┘

  Elements WITHOUT weight:

  ┌────────────────┬───────┬───────────────────────────────┬──────────────────────────┐
  │      Type      │ Count │           QTO pset            │           Has            │
  ├────────────────┼───────┼───────────────────────────────┼──────────────────────────┤
  │ IfcWindow      │ 11    │ Qto_WindowBaseQuantities      │ Area, Height, Width only │
  ├────────────────┼───────┼───────────────────────────────┼──────────────────────────┤
  │ IfcCovering    │ 5     │ Qto_CoveringBaseQuantities    │ GrossArea, Width only    │
  ├────────────────┼───────┼───────────────────────────────┼──────────────────────────┤
  │ IfcRoof        │ 2     │ Qto_RoofBaseQuantities        │ GrossArea only           │
  ├────────────────┼───────┼───────────────────────────────┼──────────────────────────┤
  │ IfcPipeSegment │ 2     │ Qto_PipeSegmentBaseQuantities │ Length only              │
  └────────────────┴───────┴───────────────────────────────┴──────────────────────────┘

Summary: The IFC4 QtoBaseQuantities rule includes weight fields only for
structural/volumetric elements (walls, slabs, footings, beams, columns).
Roofs, windows, coverings, and pipes get area/length quantities but no
weight — the standard QTO schema simply doesn't define weight for those
types.

The partial structural weight is ~62 tonnes. To get a full building weight you'd
need to add material densities manually for the missing element types (roof
tiles, glazing, gutters, floor covering).

❯ we want to add scheduling information to this model, so we need a schedule,
work plans etc.. group elements into tasks, remember that foundations go first,
windows are inserted into walls after the walls are constructed, roofs need
walls below etc..

Let me first explore the sequence API to understand what's available, then plan the schedule structure.

Good — I have all the sequence API I need. Here's the plan:

  Schedule structure:
  - IfcWorkPlan → Construction Plan
    - IfcWorkSchedule → Construction Schedule
        - P1: Foundations → ground beams → floor slab (slab depends on footings)
      - P2: Structure → walls (after P1)
      - P3: Roof → roofs (after P2)
      - P4: Openings → windows (after P2, walls must exist)
      - P5: Finishes → eaves coverings + gutters (after P3), floor covering (after P1)

With FINISH_START sequences between phases. Let me build it:
Now create the 5 phase tasks under the schedule:
Phase tasks created (P1=3944, P2=3946, P3=3947, P4=3948, P5=3949). Now create the leaf tasks:
Good, I'll continue — now assigning all elements to their tasks and wiring up the sequence dependencies in parallel:
Continuing with windows, finishes, and sequence dependencies:
Now wiring up all the FINISH_START sequence dependencies:
All done. Now let me verify the schedule looks correct:
The schedule is complete. Let me save it:

Saved. Here's the complete schedule structure:

  Construction Plan (IfcWorkPlan)
  └── Construction Schedule (IfcWorkSchedule)
      ├── P1: Foundations
      │   ├── P1.1: Install Ground Beams  ← 4 footings
      │   └── P1.2: Pour Floor Slab       ← 1 slab  [after P1.1]
      ├── P2: Structure                               [after P1]
      │   └── P2.1: Erect Walls           ← 4 walls
      ├── P3: Roof                                    [after P2]
      │   └── P3.1: Install Roof Structure ← 2 roofs
      ├── P4: Openings                                [after P2]
      │   └── P4.1: Install Windows       ← 11 windows
      └── P5: Finishes                                [after P3]
          ├── P5.1: Install Eaves & Gutters ← 4 coverings + 2 pipe segments
          └── P5.2: Install Floor Covering  ← 1 floor covering

  Sequence logic:
  - P1.1 → P1.2 — slab poured after ground beams are set
  - P1 → P2 — walls only after foundations complete
  - P2 → P3 — roof needs walls to stand on
  - P2 → P4 — windows inserted into walls after walls are up (parallel with roof)
  - P3 → P5 — eaves, gutters and floor finishes after roof is on

  Dates are null — would you like to add durations and start dates to enable Gantt/cascade scheduling?

❯ yes, add durations and cascade the schedule

Now I'll add task times to all leaf tasks in parallel, then set durations and start:
Now set durations on all tasks and a start date on the first task, all in parallel:
The attributes value needs to be a JSON string. Retrying:
Now cascade from the first leaf task through the entire schedule:
Now verify the results:
The cascade propagated within P1 but not across phases — the phase summary tasks need task times
too for the sequence relationships to fire. Let me add them:
Now cascade again from the start:
The cascade isn't crossing phase boundaries because the summary tasks have no finish dates to hand
off. Let me set P1's start/finish explicitly from its subtasks, then cascade from there:
The phase summary tasks have no durations so cascade uses start=finish and all phases collapsed to
the same date. I need to set durations on the phase summaries so cascade can propagate finish dates
correctly, then manually set the remaining leaf task starts:
Now re-cascade from P1, then set the leaf task starts for the later phases:
All dates are correct. Saving:

The schedule is fully cascaded and saved. Here's the final Gantt:                                                                                   
   
  Task                       Duration   Start        Finish                                                                                           
  ─────────────────────────────────────────────────────────
  P1: Foundations                        02 Mar       09 Mar                                                                                          
    P1.1  Install Ground Beams   5d      02 Mar ──►  06 Mar                                                                                           
    P1.2  Pour Floor Slab        3d      07 Mar ──►  09 Mar
  P2: Structure                          10 Mar       16 Mar  (after P1)
    P2.1  Erect Walls            7d      10 Mar ──►  16 Mar
  P3: Roof                               17 Mar       21 Mar  (after P2)
    P3.1  Install Roof Structure 5d      17 Mar ──►  21 Mar
  P4: Openings                           17 Mar       19 Mar  (after P2, parallel with P3)
    P4.1  Install Windows        3d      17 Mar ──►  19 Mar
  P5: Finishes                           22 Mar       23 Mar  (after P3)
    P5.1  Install Eaves & Gutters 2d     22 Mar ──►  23 Mar
    P5.2  Install Floor Covering  2d     22 Mar ──►  23 Mar
  ─────────────────────────────────────────────────────────
  Total project duration: ~16 working days (2–23 March 2026)

  Windows and roof are installed in parallel after the walls go up, shaving a few days off the total schedule.

LOL, it thinks we work seven days a week. Here's the IFC displayed using ifc4d-gantt:

image
❯ check the model for validation errors

Model is valid — no issues found.                                                                                                                   
                                                                                                                                                      
❯ try it using the extra rules

Still clean — passes both schema validation and the full EXPRESS rules check. The schedule entities
we added are all well-formed.

❯ I'm looking at the gantt chart, we don't work weekends!

Good catch. Let me create a Mon–Fri work calendar, then recalculate all dates skipping weekends:                                                    
                                                                                                                                                      
  Corrected dates (working days only):                                                                                                                
  - P1.2 slab: starts Mon 09 Mar (not Sat 07)
  - P2.1 walls: starts Thu 12 Mar, finishes Fri 20 Mar (7 working days, skipping Mar 14–15 weekend)                                                   
  - P3/P4: start Mon 23 Mar (after walls finish Fri 20, skip weekend)
  - P5: starts Mon 30 Mar (after roof finishes Fri 27, skip weekend) 

Calendar created. Now update all task dates to skip weekends:
The schedule is still showing old dates — let me verify the task times were actually updated:
The edits returned ok:true but the dates aren't updating. The sequence.edit_task_time API may
have a type coercion issue with datetime updates. Let me try attribute.edit_attributes directly on
the IfcTaskTime entities:
attribute.edit_attributes works. Now update all remaining task times in parallel:
All dates are correct. Saving:

Updated schedule with weekends excluded:                                                                                                            
   
  Task                        Duration  Start        Finish       Day                                                                                 
  ────────────────────────────────────────────────────────────────────                                                                              
  P1: Foundations                        02 Mar       11 Mar                                                                                          
    P1.1  Install Ground Beams  5d       Mon 02 Mar ──► Fri 06 Mar                                                                                    
    P1.2  Pour Floor Slab       3d       Mon 09 Mar ──► Wed 11 Mar
  P2: Structure                          Thu 12 Mar   Fri 20 Mar  (after P1)
    P2.1  Erect Walls           7d       Thu 12 Mar ──► Fri 20 Mar
  P3: Roof                               Mon 23 Mar   Fri 27 Mar  (after P2)
    P3.1  Install Roof Structure 5d      Mon 23 Mar ──► Fri 27 Mar
  P4: Openings                           Mon 23 Mar   Wed 25 Mar  (after P2, parallel)
    P4.1  Install Windows       3d       Mon 23 Mar ──► Wed 25 Mar
  P5: Finishes                           Mon 30 Mar   Tue 31 Mar  (after P3)
    P5.1  Install Eaves & Gutters 2d     Mon 30 Mar ──► Tue 31 Mar
    P5.2  Install Floor Covering  2d     Mon 30 Mar ──► Tue 31 Mar
  ────────────────────────────────────────────────────────────────────
  Total: 22 working days  (02 Mar – 31 Mar 2026)

Also noted a bug worth reporting: sequence.edit_task_time silently returns ok:true
but doesn't persist datetime changes — attribute.edit_attributes is the reliable workaround.

@brunopostle
Copy link
Contributor Author

Some more tests, this time adding pricing information and generating a Bill of Quantities. Notice that I didn't really give it and useful instructions, but it got there in the end regardless;

❯ we want to create a bill of quantities, but are not quite sure how this is done in IFC.
Are all the elements given monetary values? or is there separate pricing information
that is applied when the BoQ is generated?

Let me check what's currently in the model to ground the explanation.

Good context. Here's how BoQ works in IFC:

  ---
  Bill of Quantities in IFC — Two Separate Concerns

IFC separates quantities from pricing. They are independent layers:

1. Quantities (already in this model)

The model already has quantities computed — you can see IfcElementQuantity
(40 instances), IfcQuantityLength (86), IfcQuantityArea (46), IfcQuantityVolume (25),
IfcQuantityWeight (18). These are property sets attached to elements describing
how much of something exists — wall net volume, slab gross area, window width, etc.

These come from ifc_quantify() and live on each element as IfcElementQuantity psets.

2. Pricing — IfcCostSchedule / IfcCostItem

Monetary values are not stored on elements. Instead, a separate cost schedule is built:

  IfcCostSchedule  (e.g. "Bill of Quantities")
    └─ IfcCostItem  "Concrete Walls"
         ├─ IfcCostValue  { AppliedValue: 120.00/m³, UnitBasis: m³ }
         └─ linked to elements via IfcRelAssignsToControl

The cost item references the elements (or their quantities), and the unit rate
(IfcCostValue) is applied at BoQ generation time — not stored on the element itself.
This means:

  - The same element can appear in multiple cost schedules with different rates
  - Rates can be updated without touching geometry or quantities
  - The model stays clean — no pricing leaks into the physical model

  3. How they connect

  At BoQ generation, a tool multiplies:

  Cost = IfcCostValue.AppliedValue (unit rate)
       × quantity extracted from the element's IfcElementQuantity

  The link between cost item and elements is via IfcRelAssignsToControl.

  Current state of this model

  - Quantities: present (already computed)
  - Cost schedule: none — ifc_cost() returned empty

So the next step would be to build an IfcCostSchedule with IfcCostItem entries, assign
unit rates, and link them to elements. Want to go ahead and create one?

❯ yes, go ahead and create a cost schedule 

Let me first understand what elements and quantities are in the model.
Now let me check what quantities are available on representative elements.
Now let me check quantities on coverings and pipe segments, and discover the cost API.
Good picture of the model. Now I'll build the cost schedule. I'll organize it into sections
with leaf items carrying parametric quantities and unit rates.

Step 1 — Create the cost schedule
Step 2 — Create section (summary) items

Names need to be set via edit_cost_item after creation. Let me create all sections in parallel.
Let me continue building — creating all remaining sections in parallel.

Step 3 — Create leaf items under each section (all in parallel)

Now create the remaining sub-items and name all leaves in parallel.
Name the last 4 items and then assign all quantities in parallel.

Step 4 — Assign parametric quantities to all leaf items
Step 5 — Add unit rate cost values to all leaf items in parallel
Step 6 — Set unit rates on all cost values in parallel

All rates set. Now validate and read back the schedule to verify before saving.
Valid and looking correct. Saving now.
Done. Here's a summary of the cost schedule that was created:

Bill of Quantities (COSTPLAN)
  ┌─────┬────────────────────────┬──────────────────┬────────────────┬───────────┐
  │ ID  │          Item          │ Linked Elements  │   Qty Basis    │ Unit Rate │
  ├─────┼────────────────────────┼──────────────────┼────────────────┼───────────┤
  │ A.1 │ Ground Beams           │ 4 IfcFooting     │ NetVolume (m³) │ €350/m³   │
  ├─────┼────────────────────────┼──────────────────┼────────────────┼───────────┤
  │ A.2 │ Floor Slab             │ 1 IfcSlab        │ NetVolume (m³) │ €250/m³   │
  ├─────┼────────────────────────┼──────────────────┼────────────────┼───────────┤
  │ B.1 │ Exterior Walls         │ 4 IfcWall        │ NetVolume (m³) │ €300/m³   │
  ├─────┼────────────────────────┼──────────────────┼────────────────┼───────────┤
  │ C.1 │ Exterior Windows       │ 11 IfcWindow     │ Area (m²)      │ €450/m²   │
  ├─────┼────────────────────────┼──────────────────┼────────────────┼───────────┤
  │ D.1 │ Pitched Roof Structure │ 2 IfcRoof        │ GrossArea (m²) │ €90/m²    │
  ├─────┼────────────────────────┼──────────────────┼────────────────┼───────────┤
  │ D.2 │ Eaves Tiles            │ 2 IfcCovering    │ GrossArea (m²) │ €50/m²    │
  ├─────┼────────────────────────┼──────────────────┼────────────────┼───────────┤
  │ D.3 │ Eaves Corona           │ 2 IfcCovering    │ GrossArea (m²) │ €70/m²    │
  ├─────┼────────────────────────┼──────────────────┼────────────────┼───────────┤
  │ D.4 │ Eaves Gutters          │ 2 IfcPipeSegment │ Length (m)     │ €40/m     │
  ├─────┼────────────────────────┼──────────────────┼────────────────┼───────────┤
  │ E.1 │ Floor Covering         │ 1 IfcCovering    │ GrossArea (m²) │ €55/m²    │
  └─────┴────────────────────────┴──────────────────┴────────────────┴───────────┘

The quantities are parametrically linked to the elements — if geometry changes and
quantities are re-computed with ifc_quantify, the cost totals will update automatically.
The unit rates are placeholder values; you'll want to adjust them to match your actual
pricing data.

❯ commit this

Committed.

❯ The tool to generate a BoQ is something like this (formats are CSV, ODS or XLSX):
❯
❯    python -m ifc5d.ifc5Dspreadsheet                              
❯      usage: python -m ifc5d.ifc5Dspreadsheet [-h] [-l LOG] [-f FORMAT] input output                                                                  
❯      python -m ifc5d.ifc5Dspreadsheet: error: the following arguments are required: input, output

There's a bug in ifc5Dspreadsheet.py. Let me look at it.
The format comparison is case-sensitive — it expects uppercase. The -f xlsx wasn't
matched, leaving writer unset.

Generated — the tool named the file after the schedule itself: Bill of Quantities.xlsx (6.7K).
The format flag requires uppercase: CSV, ODS, or XLSX.

❯ It doesn't look that good

The rates are being read but RateSubtotal and TotalPrice are both 0. Let me look at how
the tool computes those columns.
The tool calls calculate_applied_value to read the rate — let me check what's actually
stored in a cost value entity.
The calculate_applied_value works fine — the rate is 350. Let me look more carefully at
how the XLSX writer computes TotalPrice.
The XLSX writer is quite bare — no formula for TotalPrice. The PDF writer is more polished
and specifically designed for PRICEDBILLOFQUANTITIES. Let me check if it's usable.

PDF not available. Let me check what edit_cost_value_formula does — it might be the
right tool to get TotalPrice computed properly.
The PDF writer needs the typst Python package. Let me check if we can install it.

The tool only handles CSV/ODS/XLSX in the format switch. Let me check and use the PDF
writer directly.
The PDF writer isn't wired into the CLI. I'll call it directly, but first need to check what
options it needs and whether our schedule's predefined_type should change to
PRICEDBILLOFQUANTITIES.
The PDF writer uses a typst template with the CSV data. Let me invoke it directly.

Let me capture the intermediate CSV the PDF writer generates to find what's causing
the invalid float error.
The section rows have an empty Quantity field. Let me find all float() calls in the typst
template to pinpoint the one without an empty-string guard.
Found it. The typst template does float(row.at("Quantity")) on all rows where
ItemIsASum == "False" — including our section headers, which have empty Quantity.
The fix is to make section items proper sum items by adding a Category="*" cost value
with pre-computed totals. This also fixes the PDF so section totals display correctly.

Let me compute the totals and add sum values to all five sections in parallel.
Now set Category="*" and the computed section totals on all five in parallel.
Now validate, generate the PDF, and save.
The PDF writer is reading the file from disk — I need to save first.
PDF generated (149K). Let me commit the IFC changes too.
boq.pdf is ready at /home/bruno/src/simple-ifc/boq.pdf. The fix was that the section
items (A, B, C, D, E) needed a IfcCostValue with Category="*" so the tool recognises
them as sum rows rather than leaf items. Without that, the typst template tried to
parse their empty Quantity field as a float and crashed.

❯ update the CLAUDE.md file with everything you have learnt about creating BoQs

Done. The new Bill of Quantities section in CLAUDE.md covers:                                                                                       
                                                                                                                                                      
  - The conceptual separation of quantities vs. pricing in IFC                                                                                        
  - The full cost schedule structure with IFC entities                                                                                                
  - Quantity names by element type                                                                                                                    
  - A complete MCP recipe from ifc_quantify through to ifc_save                                                                                       
  - The double-encoded JSON gotcha for edit_cost_item/edit_cost_value
  - Why section items must have a Category="*" cost value (and what breaks without it)                                                                
  - How to export to PDF, XLSX, and CSV with the known ifc5d bugs documented

Here's the generated BoQ:

out0 out1 out2

I've put this example project with the CLAUDE.md findings here: https://github.com/brunopostle/simple-ifc

@brunopostle
Copy link
Contributor Author

The https://github.com/brunopostle/simple-ifc project has CI tests that are run on github:

❯ This project is failing some IDS tests in the IDS/ folder

Two failing checks. Let me read the IDS files to understand exactly what's required.

Two failures:
  1. (4.5) — walls missing FireRating = 30 in Pset_WallCommon
  2. (4.6) — walls missing a property literally named Thermal Transmitance (note the space and single-t — a non-standard name in this IDS)

The walls already have ThermalTransmittance (standard IFC name) but the IDS check is
for a differently-spelled property. Let me check the pset IDs on all 4 walls and the WallType,
then add both missing properties.

All 4 walls and the WallType share the same Pset_WallCommon entity (id=133), so one edit
fixes all five. I need to add FireRating = "30" and Thermal Transmitance (the IDS spelling)
while keeping the existing properties. I'll use should_purge=false to only add/update without
removing anything.

Now verify the pset looks right and re-run the IDS checks.

All 4 checks passing. Commit it.

All IDS checks now pass. What was fixed:

  - Pset_WallCommon id=133 — shared by all 4 walls and the IfcWallType, so one edit fixed all 5 failures at once.
  - FireRating = "30" added — required by check (4.5) for load-bearing walls.
  - Thermal Transmitance (with space, single-t) added — check (4.6) uses a non-standard property name that differs from the IFC standard
  ThermalTransmittance. The model already had the correctly-spelled property; the IDS just checked for a differently-named one.

❯ update CLAUDE.md with what you learned about IDS

Done. The new IDS section covers:

  - How to read ifctester output ([PASS] (0/0) vs [FAIL] (0/5))
  - IDS structure: applicability filter + requirements
  - The fix workflow: read IDS → inspect pset → edit
  - The should_purge: false flag for safe property addition
  - The exact-string-match gotcha for property names (the Thermal Transmitance lesson)
  - Shared IfcPropertySet entities — one edit fixes multiple elements
  - The ifctester boolean filter quirk

❯ We want some more IDS continuous integration tests for this project, create some
❯ based on issues you found generating schedules, quantities and BoQs

Let me look at the model's current state to design tests that are grounded in real issues
from the session, then verify they all pass before committing.

Good, all the properties I'm checking for exist. Now writing the IDS file.
Now run it to verify all checks pass against the current model.
The scheduling name checks are unexpectedly failing. Let me see the error details.
The ? wildcard is being treated as a literal string match — ifctester expects "?" not
"anyvalue". I need to use an xs:restriction pattern instead.
All 13 checks passing. Commit it.

Done. IDS/CI_project_checks.ids has 13 specifications across three groups:

QTO (5 checks) — verifies ifc_quantify has been run and saved before committing.
If someone adds new elements, quantities won't exist and BoQ cost item links will
silently produce zero totals.

Scheduling (3 checks) — verifies walls, windows, and footings all have names. Element
names appear verbatim in the BoQ Quantities column (e.g. "exterior - NetVolume")
and in construction schedule task descriptions — unnamed elements make both
outputs unreadable.

BoQ properties (5 checks) — verifies the properties that underpin BoQ unit rates:
  - Load-bearing walls have FireRating (fire protection cost basis)
  - Load-bearing walls have material assigned (needed for NetWeight quantity)
  - External walls, windows, and roofs all have ThermalTransmittance (insulation spec / glazing cost basis)

One lesson captured in MEMORY.md: <simpleValue>?</simpleValue> is treated as
a literal string "?" by ifctester, not a wildcard. Use <xs:restriction base="xs:string">
<xs:pattern value=".+"/></xs:restriction> instead to check that an attribute has any non-empty value.

❯ update CLAUDE.md with the IDS `?` wildcard gotcha

Done — both CLAUDE.md and the memory file now document that <simpleValue>?</simpleValue>
is a literal match in ifctester, with the correct xs:pattern alternative.

From the Github CI report:

image

@brunopostle
Copy link
Contributor Author

brunopostle commented Feb 27, 2026

More tests: "There are bug reports for this project on github": brunopostle/simple-ifc#1

[edit: to be clear, snagging issues can be created on github for this IFC project and the bot tries to fix them]

This now works:

  ifcedit run model.ifc project.append_asset --library library.ifc --element 5711

Generated with the assistance of an AI coding tool.
Renders IFC model geometry to a PNG image using pyvista (off_screen).

ifcquery CLI:
  ifcquery <file> render [-o out.png] [--selector QUERY]
                         [--element ID[,ID...]] [--view iso|top|south|north|east|west]

ifcmcp MCP tool:
  ifc_render(selector, element_ids, view) -> list[ImageContent]
  Returns base64-encoded PNG as an MCP ImageContent block so the agent
  can inspect the model geometry inline.

Generated with the assistance of an AI coding tool.
@brunopostle
Copy link
Contributor Author

@aothms, I added a render tool to ifcquery, so you can now do things like:

ifcquery _test_simple.ifc render
_test_simple

..or use a selector query and/or highlight particular elements:

ifcquery _test_simple.ifc render  --element 111,1664,1316
_test_simple2

This all works with the ifcmcp interface, so claude is busy looking at renders to see what to do:

  The highlighted render shows the wall (dark) and the door (dark element in the center-left of the wall), with the tree blob visible in white/ghost.
  The door appears to be on the LEFT portion of the south wall (the west section), which is where I intended it to be. Looking at the highlighted
  render:

  - The south wall is visible (dark, center-left of image)
  - The door geometry appears on the LEFT SIDE of the wall (slightly left of center of the wall)
  - The tree is visible as a large white ghost blob on the right

  So the door IS on the left (west) portion of the south wall, which is correct.

  The opening successfully voids the wall geometry. Looking back at the south elevation, I'm noticing the door might be harder to spot than
  expected—it's positioned in that left-center area of the wall, which could make it blend in with the overall composition or appear smaller than
  anticipated in the 2D view.

  Let me re-render the full south elevation to see all elements clearly. The issue might be that the wall's boolean operation needs to be properly
  evaluated by the renderer, or the door's orientation is making it less visible depending on how the local axes are aligned.

Guard the face array reshape so elements whose triangulation is not
divisible by 3 are silently dropped. Also wrap each _add_shape() call
in a try/except so a broken shape never aborts the whole render.

Generated with the assistance of an AI coding tool.
New ifcquery modules list geometric representation contexts (with step
IDs, context type, identifier, target view, parent) and material sets
(IfcMaterial, layer sets, constituent sets, profile sets). Exposed as
CLI subcommands and MCP tools.

Generated with the assistance of an AI coding tool.
JSON parses whole numbers as int, but ifcopenshell's C++ binding
requires Python floats for AGGREGATE OF DOUBLE attributes such as
DirectionRatios and Coordinates. Convert list elements to float when
the list already contains at least one float, leaving pure-integer
lists (face indices etc.) unchanged.

Generated with the assistance of an AI coding tool.
Generated with the assistance of an AI coding tool.
Generated with the assistance of an AI coding tool.
Exposes ShapeBuilder geometry methods via the MCP server: discovery
(list), documentation (docs), and execution (shape) with entity ID
coercion and numpy serialisation.

Generated with the assistance of an AI coding tool.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants