From aa053bd52c87af28a7b30a6fa4c60948f7a6d976 Mon Sep 17 00:00:00 2001 From: Ghesselink Date: Mon, 4 May 2026 15:33:34 +0000 Subject: [PATCH] unblock voxel schema loading, add test for express --- .../ifcopenshell/express/schema_class.py | 17 +++- .../test/test_express_aggregate_bounds.py | 80 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/ifcopenshell-python/test/test_express_aggregate_bounds.py diff --git a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py index 4dd5e9d450..9547e7047e 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py +++ b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py @@ -401,7 +401,13 @@ class SchemaClass(codegen.Base): if isinstance(type, nodes.AggregationType): aggr_type = type.aggregate_type - make_bound = lambda b: -1 if b == "?" else int(b) + def make_bound(b): + # `?` and non-literal bounds (attribute references, arithmetic expressions) collapse to -1. + # + try: + return int(b) + except (TypeError, ValueError): + return -1 bound1, bound2 = map(make_bound, (type.bounds.lower, type.bounds.upper)) decl_type = get_declared_type(type.type, emitted_names) return x.aggregation_type(aggr_type, bound1, bound2, decl_type) @@ -528,7 +534,14 @@ class SchemaClass(codegen.Base): inv_attrs = [] for attr in type.inverse: if attr.bounds: - make_bound = lambda b: -1 if b == "?" else int(b) + def make_bound(b): + # `?` and non-literal bounds (attribute references, arithmetic + # expressions) collapse to -1 (unbounded) — the C++ runtime has + # no third state for "dynamic cardinality". + try: + return int(b) + except (TypeError, ValueError): + return -1 bound1, bound2 = map(make_bound, (attr.bounds.lower, attr.bounds.upper)) else: bound1, bound2 = -1, -1 diff --git a/src/ifcopenshell-python/test/test_express_aggregate_bounds.py b/src/ifcopenshell-python/test/test_express_aggregate_bounds.py new file mode 100644 index 0000000000..030c301a78 --- /dev/null +++ b/src/ifcopenshell-python/test/test_express_aggregate_bounds.py @@ -0,0 +1,80 @@ +import os +import sys +import tempfile +import unittest + +import ifcopenshell.express + +sys.path.insert(0, os.path.dirname(ifcopenshell.express.__file__)) + + +def _parse(schema_text): + with tempfile.NamedTemporaryFile(mode="w", suffix=".exp", delete=False) as f: + f.write(schema_text) + path = f.name + try: + return ifcopenshell.express.parse(path) + finally: + os.unlink(path) + cache = path + ".cache.dat" + if os.path.exists(cache): + os.unlink(cache) + + +class TestAggregateBounds(unittest.TestCase): + def test_literal_bounds_preserved(self): + """After loading [1;3] -> (1, 3)?""" + s = _parse( + "SCHEMA t; ENTITY E; v : ARRAY [1:3] OF REAL; END_ENTITY; END_SCHEMA;" + ) + agg = ( + next(d for d in s.schema.declarations() if d.name() == "E") + .attributes()[0] + .type_of_attribute() + .as_aggregation_type() + ) + self.assertEqual((agg.bound1(), agg.bound2()), (1, 3)) + s.disown() + + def test_unbounded_marker(self): + """ [0:?] -> (0, -1)?""" + s = _parse( + "SCHEMA t; ENTITY E; v : LIST [0:?] OF REAL; END_ENTITY; END_SCHEMA;" + ) + agg = ( + next(d for d in s.schema.declarations() if d.name() == "E") + .attributes()[0] + .type_of_attribute() + .as_aggregation_type() + ) + # import pdb; pdb.set_trace() + self.assertEqual((agg.bound1(), agg.bound2()), (0, -1)) + s.disown() + + def test_voxel_grid_with_dynamic_bound_loads(self): + """ + Array that is an expression : [1:dim_x*dim_y*dim_z] + Parsing must not crash, Bbund must be (1, -1) + """ + s = _parse( + """ + SCHEMA t; + TYPE IfcBoolean = BOOLEAN; END_TYPE; + + ENTITY IfcVoxelHolder; + NumberOfVoxelsX : INTEGER; + NumberOfVoxelsY : INTEGER; + NumberOfVoxelsZ : INTEGER; + Voxels : ARRAY [1:NumberOfVoxelsX*NumberOfVoxelsY*NumberOfVoxelsZ] OF IfcBoolean; + END_ENTITY; + END_SCHEMA; + """ + ) + holder = next(d for d in s.schema.declarations() if d.name() == "IfcVoxelHolder") + voxels = holder.attributes()[-1].type_of_attribute().as_aggregation_type() + self.assertEqual((voxels.bound1(), voxels.bound2()), (1, -1)) + s.disown() + + +if __name__ == "__main__": + unittest.main()