Rendered from research/appendix_c_determinism_investigation.md.
Appendix C — Architecture Reproducibility Investigation
Author: Osama Ata
Date: 2026-08-30
Status: Resolved. Fix landed upstream in bim-guard commit
3659bcf5
("fix: key the geometry shape cache on the entity id, not the object
address").
This investigation is promoted from a working .txt note to a citable
appendix because it is representative of the standard this repository holds
its own validation pipeline to: a correctness bug was found through a
reproducibility check (the same input produced different outputs across runs),
root-caused with a falsifiable measurement rather than a guess, fixed, and the
fix verified by re-running to a stable, independently-justifiable answer. The
original investigation notes follow unedited below.
Symptom
The architectural analysis returned a different number of findings for the same model on identical consecutive runs:
wbdg_office_arc 1304, 1307, 1304, 1304 (one server, use_cache=false)
Clinic_Architectural 2917 on one boot, 2847 on another
Both models parsed the same element count and loaded the same 51 rules in every
run, so the input was identical. Every rule observed appearing and disappearing
targeted IfcSpace on a numeric property: CODE 9.5.1.2 (bathroom ceiling height,
= 1950 mm) and CODE 3.8.3.2 (accessible corridor width, >= 1100 mm).
Ruled out
- Rule ordering.
RuleService.list_by_themereads rows in insertion order from the table, and the offline client stores them in a list. Stable within a process. - Space enumeration order. ifcopenshell returns
by_type("IfcSpace")in a stable order; three consecutive opens ofwbdg_office_arcgave 99 spaces in an identical sequence (same firstGlobalId, same tuple hash). - Hash randomisation.
PYTHONHASHSEEDvaries per process, but the counts varied inside a single process, so it cannot be the cause. - Warm-up or caching. Runs 3 and 4 reverted to the earlier value, so it is not a state that builds up.
Root cause
app/modules/ifc_reader/ifc_geometry.py, IFCGeometryExtractor._get_shape
cached tessellated shapes under id(element) — the CPython address of the
ifcopenshell entity_instance wrapper.
ifcopenshell creates a NEW wrapper object on every entity access and frees it
when the caller drops the reference, so those addresses are recycled. Measured
directly on wbdg_office_arc: fetching the model's 99 spaces, releasing them,
and fetching them again, 32 addresses were reused and 31 of those now pointed
at a DIFFERENT space.
address 139739738185728: was space 04Sq57eS5FffNezn2ilgmZ,
now space 06njXbG3HC4RydTXssDqX8
The extractor is reused across rules while each rule re-fetches its target
elements, which is exactly the pattern that triggers reuse. A cache hit
therefore returned another element's geometry, or a cached None, chosen
effectively at random by the allocator.
That explains every part of the symptom: only geometry-derived properties were affected (a space's Height and Width come from the bounding box, not from a Pset); the variance appeared within one process; and it did not accumulate.
This was not only a reproducibility bug. It silently attributed one element's measurements to another.
Fix
Key the cache on the entity's own STEP instance id (element.id()), which is
stable for the life of the model. An object that is not an ifcopenshell entity
is measured without being cached rather than keyed on something unstable.
Landed in bim-guard at
app/modules/ifc_reader/ifc_geometry.py:237
(commit 3659bcf5), with the reasoning preserved in the code comment at
:235-237.
Verification
wbdg_office_arc, six consecutive runs after the fix: 1304 findings every time,
identical rule-by-rule breakdown. Clinic_Architectural: 2847 twice on one boot,
and 2847 again after a server restart. AC20-FZK-Haus: 77, matching its earlier
value.
The deterministic answer is the LOWER count, and it is the correct one. Every
space in wbdg_office_arc measures at least 2500 mm tall (median 2500 mm, max
9757 mm), so none can violate the 1950 mm bathroom-ceiling rule. The three
CODE 9.5.1.2 findings that came and went were false violations produced by
measuring a space with some other element's shape. The bug was inventing
findings, not hiding them.
Unit tests: 29 failed / 762 passed both before and after — unchanged. The 29
failures are the pre-existing missing-static-asset ones documented in
test-results.md finding 7.