~/satyajit

debim, read and measured: a YAML building language, a lossy IFC importer, and a fidelity score an empty frame passes

mdjsonmcp

2026-11-09 · 16 min · 3d · cad · bim · mcp · agents · evals

Why read this

Solidtop 85%

Reads debim's IFC importer and benchmark code, measures tokens and loss on public samples, and shows its 85.5% fidelity score sits below an empty frame's.

  • Original analysis
  • Runs on a laptop CPU
  • Concrete numbers to act on

3D & spatialMITPractitioner teardown

How this was scored
Is it new?
1 of 3: An incremental tweak
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
1 of 3: Relevant for months
Does it affect many?
1 of 3: A specialist community
Only here?
2 of 3: A teardown or measurement few others did

Score 58 of 100, ranked 473 of 701 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

On 6 November 2026 an X account posted a short video under the line "your BIM model is too big for AI, so someone turned buildings into code" (post). The project is debim, an open-source "Building-as-Code engine" by Prida Takon. The post makes three claims: a large BIM file cannot fit in an agent's context window, debim represents walls, columns, beams, dimensions and materials as compact structured text instead, and the developer reports "85.5% median visual fidelity across 255 real building models". To its credit it also says "some information can still be lost".

That last sentence is the interesting one, and nobody in the thread quantifies it. So I did the part I could do on a CPU in a sandbox: read the code, take four public IFC files, count their tokens with a real tokenizer, measure what a box-per-element representation costs and loses, and rebuild the two benchmark metrics to see what they reward.

Labels, as everywhere on this site: reported is the project's own claim (README, its research note, the post); measured is something I ran or read off a file; reasoned is my arithmetic or inference on the other two.

PRIDA-TAKON/debim@4ffd1f3 · snapshot 2026-11-10
tracked files
570
license
MIT
branch
main
tests
325 files
source
5.4 MB
commit date
2026-10-29
source by language
Python4.4 MB(294)HTML819.3 kB(7)JavaScript164.7 kB(4)Jupyter Notebook36.6 kB(5)CSS24.1 kB(2)Dockerfile1.2 kB(2)Ruby0.7 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-11-10 at 4ffd1f3 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

What debim is

Read at commit 4ffd1f3 (29 October 2026). pyproject.toml says version 1.2.0 (the README badge still says v1.1.0), Python 3.11 or newer, MIT licence. The core depends on pydantic, PyYAML, NumPy, ezdxf, Shapely and SciPy; IFC goes through the optional ifcopenshell extra, the MCP server through the optional mcp extra. The repository holds 570 tracked files.

A .debim file is YAML with a fixed top level: schema: "IFC4-Minimal", a project block with units, a site box, storeys with elevation and height, a structural grid (axes_x, axes_y as named offsets in metres), a material list, and elements. Elements are placed on the grid, not in coordinates. From the repo's own house.debim:

grids:
  axes_x: { "1": 0.00, "2": 4.00, "3": 8.00 }
  axes_y: { "A": 0.00, "B": 4.00, "C": 8.00 }
 
elements:
  - class: IfcColumn
    tag: "C1"
    material: "CONC_240"
    profile: { shape: BOX, width: 0.25, depth: 0.25 }
    placement: { grid: ["1", "A"], base_storey: "L1", top_storey: "L1" }
 
  - class: IfcWall
    tag: "W-EXT-01"
    height: 3.00
    placement: { from_grid: ["1", "A"], to_grid: ["2", "A"], storey: "L1" }
    layers:
      - { name: "plaster", material: "PLASTER_CEMENT", thickness: 0.015, function: "finish" }
      - { name: "AAC block", material: "AAC_BLOCK", thickness: 0.075, function: "structure" }
      - { name: "plaster", material: "PLASTER_CEMENT", thickness: 0.015, function: "finish" }

(The original has Thai layer names; I translated them and folded the layers onto one line each.) A wall is two grid points, a height and a layer stack. A slab is a boundary of grid points, a thickness and openings. That is the right shape for a structural engineer's first pass at a reinforced-concrete house, which is what most of the examples are: two Thai houses, a townhouse, a Farnsworth House transcription and a Soumaya Museum sketch.

Around that sits a lot of tooling, read from src/debim/ and the README:

The MCP design is the part I agree with most. The README's own motivation is that "Asking an LLM to guess building dimensions directly in text leads to fatal hallucinations", and the answer it chose is to let the model write a declarative file and have deterministic code do the arithmetic. Every tool takes the manifest as a string and returns computed numbers. That is how an agent should touch quantities.

Two directions, one headline

The post's headline is about the other direction. "Your BIM model is too big for AI" describes an existing IFC from Revit or Archicad, and the fix it implies is to convert that file into debim text and give the text to the agent. That is the importer, and the importer is where "some information can still be lost" lives. Authoring a new building in YAML loses nothing, because there was nothing before it.

So the questions are: how big is an IFC in tokens, how big is the converted text, and what did the conversion keep?

What I ran, and what I could not

The site owner authorised running the cloned repository. The sandbox's permission layer still refused both attempts, pip install -e of the clone and python -m debim import, so I did not run debim's own code. Everything below is one of three things:

  1. Measured with standard libraries: IfcOpenShell 0.9.0.post1 from PyPI to parse and mesh the IFC files, tiktoken's o200k_base to count tokens, Python 3.13, on a shared 4-vCPU box.
  2. Read from debim's source at commit 4ffd1f3, with file and function names so you can check.
  3. Re-implemented by me from that source where a piece was small enough to rebuild exactly: the visual-fidelity renderer and comparison (about 60 lines), and the material classifier (about 30 lines). Where my rebuild could differ from the original, I say how.

The inputs are four public IFC files that debim itself ships in tests/fixtures/: buildingSMART's Building-Architecture.ifc, Building-Structural.ifc and Building-Hvac.ifc (IFC4, written by a SketchUp IFC exporter), and the long-standing Duplex_A_20110907.ifc sample (IFC2X3, from Revit). They are small on purpose: they are the files its own tests run on, and the Duplex is the closest thing the field has to a standard small house.

How many tokens a building is

Measured. Each IFC file tokenised whole, as text, with o200k_base:

The Duplex, a two-unit house with 218 physical elements, is 2,380,763 bytes and 1,029,326 tokens. It does not fit a 1M-token window, never mind a 200k one. Most of that is geometry: of its 38,898 STEP entities, 25,514 are points, directions, placements, loops, faces and other representation entities. The three buildingSMART samples are a sharper case of the same thing. Building-Architecture.ifc has 13 physical elements and 69,273 tokens; one of its lines, a triangulated face set, is 53,736 characters long.

Two corrections to the project's own token arithmetic, both measured:

The green bars are my stand-in for the converted side. I wrote one YAML record per physical element in the shape of debim's catch-all record (class, tag, source, position, width, depth, height, layer), with my own script, because I could not run the importer. For the Duplex that is 72,397 bytes and 27,580 tokens: 37 times fewer than the IFC. debim's real output will differ. Walls are shorter than boxes (two grid names, a height, a thickness), furniture is about the same, and every distinct wall end adds a grid line. But the order of magnitude is the point, and it is real: a building as one short record per element fits comfortably in any current window.

For scale, debim's hand-written examples, measured the same way: house.debim is 1,045 tokens, examples/farnsworth_house/project.debim is 2,801, and the larger examples/thai_l_house/project.debim is 17,853.

What a record per element keeps

A record per element is small because it drops things. Here is what import_ifc_to_manifest() writes, read class by class.

Walls (IfcWall) become a straight segment between two grid points, with a thickness and a height. The length and thickness come from a rectangular extrusion profile if there is one, else from Revit property sets, else from defaults: 3.0 m long, 2.8 m tall, 0.15 m thick. A wall whose profile is not a rectangle (an L in plan, a curve, a wall with a sloped top) is written as a straight rectangular wall. Every endpoint becomes a grid line, and a new endpoint within 5 cm of an existing one is snapped onto it (get_or_create_grid_x, tolerance 0.05), so wall ends can move by up to 5 cm. Doors and windows are attached as children of their host wall.

Beams (IfcBeam) always become profile: BoxProfile(shape="BOX", ...), with the flange width as the width and the section depth as the depth. A steel wide-flange beam is written as a solid rectangle. When the beam's material is classified as concrete, the importer also writes reinforcement the IFC never had: main_top="2-DB16", main_bottom="2-DB16", stirrups="RB6 @ 0.15m". That is a default for a small Thai RC beam, and the QTO will count its kilograms.

Storeys keep their names and elevations, and every storey's height is set to 3.5 m.

Furniture becomes a bounding box with a position and rotation; with bake_assets=True it also exports a GLB mesh beside the YAML (the README's "10% baked GLB assets"). It is off by default.

Everything else, every IfcElement without its own path (IfcBuildingElementProxy, IfcDiscreteAccessory, IfcChimney and so on), goes through the catch-all at the end of the function: an IfcCustomElement with the bounding box's centre and its width, depth and height in the element's own axes.

Materials are reduced to a category by keyword (resolve_material): a name containing TIMBER, WOOD, LUMBER or PLYWOOD is timber; STEEL, METAL, WIDE, W- or a steel section pattern is steel; CONC is concrete; anything else is masonry, priced against MAT-BRICK-01. I re-implemented that rule and ran it over the Duplex (measured): 160 of its 218 elements land in masonry, including all 61 furnishings, 24 windows and 14 doors (they have no material, so the rule reads their names), and the 7 concrete footings, which have no material either.

Dropped entirely: property sets (the importer reads a few to find dimensions and writes none; the Duplex has 2,093 property sets holding 12,030 values, measured), quantity sets, classification references, the element's GlobalId (the tag is its Name, or a prefix and the first 8 characters of the id), type objects, and any relationship other than containment and door-or-window-in-wall.

How much the box costs depends on the element. Measured on the Duplex: each element meshed in world coordinates, its volume by the divergence theorem, divided by its bounding box's volume.

Walls, slabs, coverings and footings are boxes already, and the importer handles them well enough. The beams are the problem. The Duplex has eight, all wide-flange steel and all square to the world axes: six W410X60, which fill 10.7% of their box, and two W310X60, which fill 12.6%. A take-off on the solid box counts about 9.3 times the steel for the first and 7.9 times for the second (reasoned, 1 / 0.107 and 1 / 0.126). Windows and doors are mostly frame and air, but they are hosted openings, so their box is closer to what an estimator wants. For a tool whose stated origin is an accurate bill of quantities, the beam path is the one I would fix first: an IShape profile with flange and web thickness would cost a few more tokens and give the right kilograms.

What "retention" counts

The README reports "100.0% Median Retention Rate" and an 85.0% mean over 407 real models. The research note in docs/research/ gives the mean as 83.0%, and mean storage compression as 91.6% against the README's 88.4%; the two documents disagree with each other (read).

Here is the metric, read from tools/kaggle/debim_kaggle_stress_test.py and tools/compare_ifc.py: import the IFC, write the YAML, compile the YAML back to IFC, count IfcElement instances (minus openings) in both files, and report min(100, recompiled / original). That number is a count. A W310X60 that comes back as a solid box counts as retained. So does a sofa that comes back as a box, and a proxy that comes back as an IfcBuildingElementProxy box. The catch-all guarantees every element produces a record, so on any file the importer and compiler do not crash on, retention is near 100% by construction (reasoned). The cap at 100 hides the other direction: a compiler that adds elements (rebar, sleeves, generated proxies) would make up for ones it lost. What the 100% median says is that the pipeline runs end to end on most files and keeps one thing per thing. That is worth something, and it is not fidelity.

What "visual fidelity" scores

The 85.5% figure in the post is the median, over 255 models, of a per-model "macro average visual match" (reported; the README links a Kaggle notebook I could not run from here). The code behind it is in the repo, read from tools/render_visual_regression.py:

  1. Pick up to 20 elements per class, 50 per model, deduplicated by type.
  2. Mesh the original element with IfcOpenShell, and find the matching debim element by its tag.
  3. Render each mesh alone, in matplotlib, at a fixed 30-degree elevation and 45-degree azimuth, 300 by 300 pixels, on a dark slate background. Before rendering, centre each mesh on its own bounding box and scale it so its largest side spans 1.6 of a 2-unit frame.
  4. Compare the two pictures pixel by pixel. A pixel differs if its mean RGB difference exceeds 25. The score is 1 minus the share of differing pixels; 85% or more is a "PASS".
  5. A missing match scores 0. Average per class, then across classes.

Step 3 makes the score blind to position and to size. Step 4 counts the background. Together they mean an element that lights a small share of the frame agrees with almost anything on the rest. The README calls 85% "the international standard benchmark"; I know of no such standard for this metric, and none is cited.

I rebuilt steps 3 and 4 exactly, in matplotlib with the same colours, view, normalisation, threshold and resize, and ran them on 50 elements, one per element type, from the four samples (measured). For each I compared the real element against several things that are not it. The toy below shows the mechanism in 2D; the table under it is the real run.

The median element lights 7.8% of the frame. Against its own bounding box it scores 96.8%, and 48 of 50 pass. Against itself made 3 times bigger and moved 40 m it scores 100%, every time, because of the normalisation. Against a blank frame, an element that is not there at all, it scores a median 92.2%, and 42 of 50 pass. Only a unit cube, which fills most of the frame, drops the score to 68.0%.

So an 85.5% median is below what an empty picture gets (reasoned from the two numbers, with one caveat). The caveat: in debim's benchmark a missing element scores 0, not the blank-frame score, so its 85.5% is a mix of matched elements scoring high and unmatched ones scoring nothing, averaged across classes. My blank-frame number is a baseline for the metric, not a reconstruction of their run. It does say what the metric can see: almost nothing about shape and nothing about placement.

It also explains the README's table (reasoned). Its best classes are cable trays at 99.1%, beams at 95.0% and bracing members at 94.2%: long, thin things that light the fewest pixels. In my run the Duplex's W310X60 beam lights 1.1% of the frame, and a blank frame scores 98.9% against it. The beams score highest under a metric that cannot tell an I-section from a rectangle, on the class where the importer writes a rectangle.

The same script computes a 3D bounding-box IoU between the original and the debim element, which does see position and size. The notebook reports it as a separate line; the headline uses the pixel score.

What I would trust it with

None of this makes debim useless. It makes the importer a lossy summary with an honest-sounding name, and the benchmarks measure that it runs, not how lossy it is. What I would and would not do with it, reasoned from the above:

Authoring a new building, early. A structural grid, storeys, columns, beams, walls and slabs in a few thousand tokens of YAML, compiled to IFC4 and taken off deterministically, is a sensible format for an agent to write and for a human to review in a diff. The house example is 1,045 tokens. The QTO and costing tools are the right pattern: the model edits text, code does arithmetic. Check its take-off against a hand calculation on one small model before you trust a bill of quantities from it, and set the reinforcement yourself.

Asking questions about an existing model. Do not convert. Give the agent IfcOpenShell query tools, or an MCP server that wraps them, and let it read the classes, property sets and quantities it needs. Those are exactly what the importer drops. A record-per-element summary is still a useful first message (the 27,580-token Duplex fits easily), as long as everyone knows it is boxes.

Round-tripping. Not today. A model that goes IFC to debim to IFC comes back with steel beams as solid boxes, generic storey heights, no property sets and new ids. If a downstream tool keys on GlobalId, it will not find its elements.

Reading the benchmarks. Treat "retention" as "did not crash and kept the count", and "visual fidelity" as a number an empty frame passes. If the project wants a fidelity figure that means something, the pieces are already in its own script: report the bounding-box IoU per class, add volume and surface-area ratios of the meshes, and render both meshes in the original's frame instead of each in its own.

Where it sits

Text-first, agent-facing building and geometry tools are having a moment. On this site, NVIDIA's usd-brep puts a NURBS kernel under a USD schema with its own MCP surface, Steve's desktop CAD gives an agent a Python feature API over OpenCascade, and KOLC+ puts Gaussian-splat scans beside BIM in a closed cloud. debim's bet is the simplest of these: no kernel at all, just a grid, boxes, layers and a compiler, which is why it fits in a context window and why it cannot hold a wide-flange beam.

Reproduce it

The measurements need only PyPI packages, not debim:

python3 -m venv venv
./venv/bin/pip install ifcopenshell tiktoken pyyaml numpy matplotlib pillow
git clone --depth 1 https://github.com/PRIDA-TAKON/debim
ls debim/tests/fixtures/*.ifc   # the four samples used here (plus Duplex_MEP)

The token count is the whole file as text through tiktoken.get_encoding("o200k_base"). The fill ratio is, per element:

# measure.py (excerpt): mesh volume over bounding-box volume
st = ifcopenshell.geom.settings(); st.set("use-world-coords", True)
sh = ifcopenshell.geom.create_shape(st, elem)
v = np.array(sh.geometry.verts).reshape(-1, 3)
f = np.array(sh.geometry.faces).reshape(-1, 3)
a, b, c = v[f[:, 0]], v[f[:, 1]], v[f[:, 2]]
vol = abs(np.einsum("ij,ij->i", a, np.cross(b, c)).sum() / 6)
fill = vol / np.prod(v.max(0) - v.min(0))

For the pixel score, copy render_mesh_to_image() and compare_images() from tools/render_visual_regression.py, which is what I rebuilt, and compare an element's render against np.full((300, 300, 3), (15, 23, 42)), the empty slate frame. If you can run the repo itself, debim import on the Duplex followed by a token count of the YAML would replace my stand-in with the real number; I would like to know it.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "debim, read and measured: a YAML building language, a lossy IFC importer, and a fidelity score an empty frame passes", ai.satyajit.si, November 2026.

bibtex
@misc{ghana2026debim,
  author = {Satyajit Ghana},
  title  = {debim, read and measured: a YAML building language, a lossy IFC importer, and a fidelity score an empty frame passes},
  url    = {https://ai.satyajit.si/articles/debim},
  year   = {2026}
}
share