diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b2bd338ee7..ec2d62b411 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -74,6 +74,9 @@ jobs:
- name: Build ifcopenshell
run: |
+ echo $Python3_ROOT_DIR
+ echo ${{ env.pythonLocation }}
+
mkdir build && cd build
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
@@ -83,7 +86,9 @@ jobs:
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
- -DPython3_ROOT_DIR=$Python3_ROOT_DIR \
+ -DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
+ -DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
+ -DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
-DCOLLADA_SUPPORT=Off \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
-DGLTF_SUPPORT=On \
diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py
index 33ee3d9af3..cc20b88ac2 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py
@@ -16,9 +16,12 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
-import ifcopenshell
-from math import pi
from fractions import Fraction
+from math import pi
+from typing import Tuple, Iterable, Any
+
+import ifcopenshell
+import ifcopenshell.api
prefixes = {
"EXA": 1e18,
@@ -624,67 +627,75 @@ def format_length(
return f"{rounded_val:.{decimal_places}f}"
-def get_base_type_name(
- content_type: ifcopenshell.ifcopenshell_wrapper.named_type | ifcopenshell.ifcopenshell_wrapper.type_declaration) -> ifcopenshell.ifcopenshell_wrapper.type_declaration | None:
+def is_attr_type(
+ content_type: ifcopenshell.ifcopenshell_wrapper.named_type | ifcopenshell.ifcopenshell_wrapper.type_declaration,
+ ifc_unit_type_name: str) -> ifcopenshell.ifcopenshell_wrapper.type_declaration | None:
cur_decl = content_type
while hasattr(cur_decl, "declared_type") is True:
cur_decl = cur_decl.declared_type()
if hasattr(cur_decl, "name") is False:
continue
- if cur_decl.name() == "IfcLengthMeasure":
+ if cur_decl.name() == ifc_unit_type_name:
return cur_decl
if isinstance(cur_decl, ifcopenshell.ifcopenshell_wrapper.aggregation_type):
res = cur_decl.type_of_element()
cur_decl = res.declared_type()
- if hasattr(cur_decl, "name") and cur_decl.name() == "IfcLengthMeasure":
+ if hasattr(cur_decl, "name") and cur_decl.name() == ifc_unit_type_name:
return cur_decl
while hasattr(cur_decl, "declared_type") is True:
cur_decl = cur_decl.declared_type()
if hasattr(cur_decl, "name") is False:
continue
- if cur_decl.name() == "IfcLengthMeasure":
+ if cur_decl.name() == ifc_unit_type_name:
return cur_decl
return None
+def iter_element_and_attributes_per_type(ifc_file: ifcopenshell.file, attr_type_name: str) -> Iterable[
+ Tuple[ifcopenshell.entity_instance, ifcopenshell.ifcopenshell_wrapper.attribute, Any, str]]:
+ schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema)
+
+ for element in ifc_file:
+ entity = schema.declaration_by_name(element.is_a())
+ attrs = entity.all_attributes()
+ for i, (attr, val, is_derived) in enumerate(zip(attrs, list(element), entity.derived())):
+ if is_derived:
+ continue
+
+ # Get all methods and attributes of the element
+ attr_type = attr.type_of_attribute()
+ base_type = is_attr_type(attr_type, attr_type_name)
+ if base_type is None:
+ continue
+
+ if val is None:
+ continue
+
+ yield element, attr, val
+
+
def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str) -> ifcopenshell.file:
"""Converts all units in an IFC file to the specified target units. Returns a new file."""
prefix = "MILLI" if target_units == "MILLIMETERS" else None
- file_patched = ifcopenshell.api.run("project.create_file", version=ifc_file.schema)
# Copy all elements from the original file to the patched file
- for el in ifc_file:
- file_patched.add(el)
+ file_patched = ifcopenshell.file.from_string(ifc_file.wrapped_data.to_string())
unit_assignment = ifcopenshell.util.unit.get_unit_assignment(file_patched)
old_length = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == "LENGTHUNIT"][0]
new_length = ifcopenshell.api.run("unit.add_si_unit", file_patched, unit_type="LENGTHUNIT", prefix=prefix)
- schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema)
# Traverse all elements and their nested attributes in the file and convert them
- for element in file_patched:
- entity = schema.declaration_by_name(element.is_a())
- attrs = entity.all_attributes()
- for i, (attr, val, is_derived) in enumerate(zip(attrs, list(element), entity.derived())):
- if is_derived:
- continue
- # Get all methods and attributes of the element
- attr_type = attr.type_of_attribute()
- base_type = get_base_type_name(attr_type)
- if base_type is None:
- continue
- if val is None:
- continue
- if isinstance(val, tuple):
- new_el = [ifcopenshell.util.unit.convert_unit(v, old_length, new_length) for v in val]
- setattr(element, attr.name(), tuple(new_el))
- else:
- new_el = ifcopenshell.util.unit.convert_unit(val, old_length, new_length)
- # set the new value
- setattr(element, attr.name(), new_el)
+ for element, attr, val in iter_element_and_attributes_per_type(file_patched, "IfcLengthMeasure"):
+ if isinstance(val, tuple):
+ new_value = [ifcopenshell.util.unit.convert_unit(v, old_length, new_length) for v in val]
+ setattr(element, attr.name(), tuple(new_value))
+ else:
+ new_value = ifcopenshell.util.unit.convert_unit(val, old_length, new_length)
+ setattr(element, attr.name(), new_value)
file_patched.remove(old_length)
unit_assignment.Units = tuple([new_length, *unit_assignment.Units])
diff --git a/src/ifcopenshell-python/test/fixtures/units/beam-standard-case.ifc b/src/ifcopenshell-python/test/fixtures/units/beam-standard-case.ifc
new file mode 100644
index 0000000000..0f506b45a4
--- /dev/null
+++ b/src/ifcopenshell-python/test/fixtures/units/beam-standard-case.ifc
@@ -0,0 +1,426 @@
+ISO-10303-21;
+HEADER;
+FILE_DESCRIPTION((''),'2;1');
+FILE_NAME('','2019-03-20T15:56:44',(''),(''),'BuildingSmart IfcKit by Constructivity','IfcDoc 12.0.0.0','');
+FILE_SCHEMA(('IFC4x1'));
+ENDSEC;
+
+DATA;
+
+#1= IFCBEAMTYPE('0juf4qyggSstrxA20Qwnsj',$,'IPE220','Beam type',$,$,$,$,$,.BEAM.);
+
+#2= IFCRELDECLARES('3P3zL0KYv4C9D9h3OX$dey',$,$,$,#5,(#1,#6));
+
+#3= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20Q49sj',$,$,$,(#1),#7);
+#7= IFCMATERIALPROFILESET($,$,(#8),$);
+#8= IFCMATERIALPROFILE('IPE220',$,#9,#10,$,$);
+#9= IFCMATERIAL('S275J2',$,'Steel');
+#10= IFCISHAPEPROFILEDEF(.AREA.,'IPE220',$,110.,220.,5.9,9.2,12.,$,$);
+
+#4= IFCRELDEFINESBYTYPE('2aq$Crcs_xJvd69lbm2bMM',$,'beam typing',$,(#11,#12,#13,#14,#15,#16,#17,#18,#19),#1);
+
+#5= IFCPROJECT('32DJhIf6esIeAOIlD4Xw2m',#20,'Test model for beam cardinal points',$,$,$,$,(#21),#22);
+#20= IFCOWNERHISTORY(#24,#25,$,.NOTDEFINED.,$,$,$,1320688800);
+#21= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-05,#26,$);
+#22= IFCUNITASSIGNMENT((#63,#64,#65,#66,#67,#68,#69,#70));
+#24= IFCPERSONANDORGANIZATION(#71,#72,$);
+#25= IFCAPPLICATION(#72,'Unknown','SDS/2 Version 6.300 on NT','Unknown');
+#26= IFCAXIS2PLACEMENT3D(#73,#74,#75);
+#27= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#76));
+#28= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#78));
+#29= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#79));
+#30= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#81));
+#31= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#82));
+#32= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#84));
+#33= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#85));
+#34= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#87));
+#35= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#88));
+#36= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#90));
+#37= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#91));
+#38= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#93));
+#39= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#94));
+#40= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#96));
+#41= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#97));
+#42= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#99));
+#43= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#100));
+#44= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#102));
+#45= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#103));
+#46= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#105));
+#47= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#106));
+#48= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#108));
+#49= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#109));
+#50= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#111));
+#51= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#112));
+#52= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#114));
+#53= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#115));
+#54= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#117));
+#55= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#118));
+#56= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#120));
+#57= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#121));
+#58= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#123));
+#59= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#124));
+#60= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#126));
+#61= IFCSHAPEREPRESENTATION(#21,'Body','SweptSolid',(#127));
+#62= IFCSHAPEREPRESENTATION(#21,'Axis','Curve3D',(#129));
+#63= IFCSIUNIT($,.LENGTHUNIT.,.MILLI.,.METRE.);
+#64= IFCSIUNIT($,.PLANEANGLEUNIT.,$,.RADIAN.);
+#65= IFCSIUNIT($,.MASSUNIT.,.KILO.,.GRAM.);
+#66= IFCSIUNIT($,.TIMEUNIT.,$,.SECOND.);
+#67= IFCSIUNIT($,.AREAUNIT.,$,.SQUARE_METRE.);
+#68= IFCSIUNIT($,.PRESSUREUNIT.,$,.PASCAL.);
+#69= IFCSIUNIT($,.FORCEUNIT.,$,.NEWTON.);
+#70= IFCSIUNIT($,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
+#71= IFCPERSON($,'Liebich',$,$,$,$,$,$);
+#72= IFCORGANIZATION($,'buildingSMART',$,$,$);
+#73= IFCCARTESIANPOINT((0.,0.,0.));
+#74= IFCDIRECTION((0.,0.,1.));
+#75= IFCDIRECTION((1.,0.,0.));
+#76= IFCEXTRUDEDAREASOLID(#10,#130,#131,2000.);
+#77= IFCPRODUCTDEFINITIONSHAPE($,$,(#28,#27));
+#78= IFCPOLYLINE((#132,#133));
+#79= IFCEXTRUDEDAREASOLID(#10,#134,#135,2000.);
+#80= IFCPRODUCTDEFINITIONSHAPE($,$,(#30,#29));
+#81= IFCPOLYLINE((#136,#137));
+#82= IFCEXTRUDEDAREASOLID(#10,#138,#139,2000.);
+#83= IFCPRODUCTDEFINITIONSHAPE($,$,(#32,#31));
+#84= IFCPOLYLINE((#140,#141));
+#85= IFCEXTRUDEDAREASOLID(#10,#142,#143,2000.);
+#86= IFCPRODUCTDEFINITIONSHAPE($,$,(#34,#33));
+#87= IFCPOLYLINE((#144,#145));
+#88= IFCEXTRUDEDAREASOLID(#10,#146,#147,2000.);
+#89= IFCPRODUCTDEFINITIONSHAPE($,$,(#36,#35));
+#90= IFCPOLYLINE((#148,#149));
+#91= IFCEXTRUDEDAREASOLID(#10,#150,#151,2000.);
+#92= IFCPRODUCTDEFINITIONSHAPE($,$,(#38,#37));
+#93= IFCPOLYLINE((#152,#153));
+#94= IFCEXTRUDEDAREASOLID(#10,#154,#155,2000.);
+#95= IFCPRODUCTDEFINITIONSHAPE($,$,(#40,#39));
+#96= IFCPOLYLINE((#156,#157));
+#97= IFCEXTRUDEDAREASOLID(#10,#158,#159,2000.);
+#98= IFCPRODUCTDEFINITIONSHAPE($,$,(#42,#41));
+#99= IFCPOLYLINE((#160,#161));
+#100= IFCEXTRUDEDAREASOLID(#10,#162,#163,2000.);
+#101= IFCPRODUCTDEFINITIONSHAPE($,$,(#44,#43));
+#102= IFCPOLYLINE((#164,#165));
+#103= IFCEXTRUDEDAREASOLID(#166,#167,#168,3000.);
+#104= IFCPRODUCTDEFINITIONSHAPE($,$,(#46,#45));
+#105= IFCPOLYLINE((#170,#171));
+#106= IFCEXTRUDEDAREASOLID(#166,#172,#173,3000.);
+#107= IFCPRODUCTDEFINITIONSHAPE($,$,(#48,#47));
+#108= IFCPOLYLINE((#175,#176));
+#109= IFCEXTRUDEDAREASOLID(#166,#177,#178,3000.);
+#110= IFCPRODUCTDEFINITIONSHAPE($,$,(#50,#49));
+#111= IFCPOLYLINE((#180,#181));
+#112= IFCEXTRUDEDAREASOLID(#166,#182,#183,3000.);
+#113= IFCPRODUCTDEFINITIONSHAPE($,$,(#52,#51));
+#114= IFCPOLYLINE((#185,#186));
+#115= IFCEXTRUDEDAREASOLID(#166,#187,#188,3000.);
+#116= IFCPRODUCTDEFINITIONSHAPE($,$,(#54,#53));
+#117= IFCPOLYLINE((#190,#191));
+#118= IFCEXTRUDEDAREASOLID(#166,#192,#193,3000.);
+#119= IFCPRODUCTDEFINITIONSHAPE($,$,(#56,#55));
+#120= IFCPOLYLINE((#195,#196));
+#121= IFCEXTRUDEDAREASOLID(#166,#197,#198,3000.);
+#122= IFCPRODUCTDEFINITIONSHAPE($,$,(#58,#57));
+#123= IFCPOLYLINE((#200,#201));
+#124= IFCEXTRUDEDAREASOLID(#166,#202,#203,3000.);
+#125= IFCPRODUCTDEFINITIONSHAPE($,$,(#60,#59));
+#126= IFCPOLYLINE((#205,#206));
+#127= IFCEXTRUDEDAREASOLID(#166,#207,#208,3000.);
+#128= IFCPRODUCTDEFINITIONSHAPE($,$,(#62,#61));
+#129= IFCPOLYLINE((#210,#211));
+#130= IFCAXIS2PLACEMENT3D(#212,$,$);
+#131= IFCDIRECTION((0.,0.,1.));
+#132= IFCCARTESIANPOINT((0.,0.,0.));
+#133= IFCCARTESIANPOINT((0.,0.,2000.));
+#134= IFCAXIS2PLACEMENT3D(#213,$,$);
+#135= IFCDIRECTION((0.,0.,1.));
+#136= IFCCARTESIANPOINT((0.,0.,0.));
+#137= IFCCARTESIANPOINT((0.,0.,2000.));
+#138= IFCAXIS2PLACEMENT3D(#214,$,$);
+#139= IFCDIRECTION((0.,0.,1.));
+#140= IFCCARTESIANPOINT((0.,0.,0.));
+#141= IFCCARTESIANPOINT((0.,0.,2000.));
+#142= IFCAXIS2PLACEMENT3D(#215,$,$);
+#143= IFCDIRECTION((0.,0.,1.));
+#144= IFCCARTESIANPOINT((0.,0.,0.));
+#145= IFCCARTESIANPOINT((0.,0.,2000.));
+#146= IFCAXIS2PLACEMENT3D(#216,$,$);
+#147= IFCDIRECTION((0.,0.,1.));
+#148= IFCCARTESIANPOINT((0.,0.,0.));
+#149= IFCCARTESIANPOINT((0.,0.,2000.));
+#150= IFCAXIS2PLACEMENT3D(#217,$,$);
+#151= IFCDIRECTION((0.,0.,1.));
+#152= IFCCARTESIANPOINT((0.,0.,0.));
+#153= IFCCARTESIANPOINT((0.,0.,2000.));
+#154= IFCAXIS2PLACEMENT3D(#218,$,$);
+#155= IFCDIRECTION((0.,0.,1.));
+#156= IFCCARTESIANPOINT((0.,0.,0.));
+#157= IFCCARTESIANPOINT((0.,0.,2000.));
+#158= IFCAXIS2PLACEMENT3D(#219,$,$);
+#159= IFCDIRECTION((0.,0.,1.));
+#160= IFCCARTESIANPOINT((0.,0.,0.));
+#161= IFCCARTESIANPOINT((0.,0.,2000.));
+#162= IFCAXIS2PLACEMENT3D(#220,$,$);
+#163= IFCDIRECTION((0.,0.,1.));
+#164= IFCCARTESIANPOINT((0.,0.,0.));
+#165= IFCCARTESIANPOINT((0.,0.,2000.));
+#166= IFCTSHAPEPROFILEDEF(.AREA.,'1/2IPE300',$,150.,150.,7.1,10.7,15.,$,$,$,$);
+#167= IFCAXIS2PLACEMENT3D(#221,$,$);
+#168= IFCDIRECTION((0.,0.,1.));
+#170= IFCCARTESIANPOINT((0.,0.,0.));
+#171= IFCCARTESIANPOINT((0.,0.,3000.));
+#172= IFCAXIS2PLACEMENT3D(#222,$,$);
+#173= IFCDIRECTION((0.,0.,1.));
+#175= IFCCARTESIANPOINT((0.,0.,0.));
+#176= IFCCARTESIANPOINT((0.,0.,3000.));
+#177= IFCAXIS2PLACEMENT3D(#223,$,$);
+#178= IFCDIRECTION((0.,0.,1.));
+#180= IFCCARTESIANPOINT((0.,0.,0.));
+#181= IFCCARTESIANPOINT((0.,0.,3000.));
+#182= IFCAXIS2PLACEMENT3D(#224,$,$);
+#183= IFCDIRECTION((0.,0.,1.));
+#185= IFCCARTESIANPOINT((0.,0.,0.));
+#186= IFCCARTESIANPOINT((0.,0.,3000.));
+#187= IFCAXIS2PLACEMENT3D(#225,$,$);
+#188= IFCDIRECTION((0.,0.,1.));
+#190= IFCCARTESIANPOINT((0.,0.,0.));
+#191= IFCCARTESIANPOINT((0.,0.,3000.));
+#192= IFCAXIS2PLACEMENT3D(#226,$,$);
+#193= IFCDIRECTION((0.,0.,1.));
+#195= IFCCARTESIANPOINT((0.,0.,0.));
+#196= IFCCARTESIANPOINT((0.,0.,3000.));
+#197= IFCAXIS2PLACEMENT3D(#227,$,$);
+#198= IFCDIRECTION((0.,0.,1.));
+#200= IFCCARTESIANPOINT((0.,0.,0.));
+#201= IFCCARTESIANPOINT((0.,0.,3000.));
+#202= IFCAXIS2PLACEMENT3D(#228,$,$);
+#203= IFCDIRECTION((0.,0.,1.));
+#205= IFCCARTESIANPOINT((0.,0.,0.));
+#206= IFCCARTESIANPOINT((0.,0.,3000.));
+#207= IFCAXIS2PLACEMENT3D(#229,$,$);
+#208= IFCDIRECTION((0.,0.,1.));
+#210= IFCCARTESIANPOINT((0.,0.,0.));
+#211= IFCCARTESIANPOINT((0.,0.,3000.));
+#212= IFCCARTESIANPOINT((-55.,110.,0.));
+#213= IFCCARTESIANPOINT((0.,110.,0.));
+#214= IFCCARTESIANPOINT((55.,110.,0.));
+#215= IFCCARTESIANPOINT((-55.,0.,0.));
+#216= IFCCARTESIANPOINT((0.,0.,0.));
+#217= IFCCARTESIANPOINT((55.,0.,0.));
+#218= IFCCARTESIANPOINT((-55.,-110.,0.));
+#219= IFCCARTESIANPOINT((0.,-110.,0.));
+#220= IFCCARTESIANPOINT((55.,-110.,0.));
+#221= IFCCARTESIANPOINT((-75.,75.,0.));
+#222= IFCCARTESIANPOINT((0.,75.,0.));
+#223= IFCCARTESIANPOINT((75.,75.,0.));
+#224= IFCCARTESIANPOINT((-75.,0.,0.));
+#225= IFCCARTESIANPOINT((0.,0.,0.));
+#226= IFCCARTESIANPOINT((75.,0.,0.));
+#227= IFCCARTESIANPOINT((-75.,-75.,0.));
+#228= IFCCARTESIANPOINT((0.,-75.,0.));
+#229= IFCCARTESIANPOINT((75.,-75.,0.));
+
+#6= IFCBEAMTYPE('0juf4qyggSstrxA20Qdisj',$,'1/2IPE300','Beam type',$,$,$,$,$,.BEAM.);
+
+#11= IFCBEAMSTANDARDCASE('0juf4qyggSI8rxA20Qwnsj',$,'A-1','IPE220','Beam',#232,#77,'A-1',$);
+#232= IFCLOCALPLACEMENT(#235,#236);
+#235= IFCLOCALPLACEMENT(#237,#26);
+#236= IFCAXIS2PLACEMENT3D(#256,#257,#258);
+#237= IFCLOCALPLACEMENT($,#26);
+#239= IFCLOCALPLACEMENT(#235,#260);
+#240= IFCLOCALPLACEMENT(#235,#261);
+#241= IFCLOCALPLACEMENT(#235,#262);
+#242= IFCLOCALPLACEMENT(#235,#263);
+#243= IFCLOCALPLACEMENT(#235,#264);
+#244= IFCLOCALPLACEMENT(#235,#265);
+#245= IFCLOCALPLACEMENT(#235,#266);
+#246= IFCLOCALPLACEMENT(#235,#267);
+#247= IFCLOCALPLACEMENT(#235,#268);
+#248= IFCLOCALPLACEMENT(#235,#269);
+#249= IFCLOCALPLACEMENT(#235,#270);
+#250= IFCLOCALPLACEMENT(#235,#271);
+#251= IFCLOCALPLACEMENT(#235,#272);
+#252= IFCLOCALPLACEMENT(#235,#273);
+#253= IFCLOCALPLACEMENT(#235,#274);
+#254= IFCLOCALPLACEMENT(#235,#275);
+#255= IFCLOCALPLACEMENT(#235,#276);
+#256= IFCCARTESIANPOINT((0.,0.,0.));
+#257= IFCDIRECTION((1.,0.,0.));
+#258= IFCDIRECTION((0.,1.,0.));
+#260= IFCAXIS2PLACEMENT3D(#277,#278,#279);
+#261= IFCAXIS2PLACEMENT3D(#280,#281,#282);
+#262= IFCAXIS2PLACEMENT3D(#283,#284,#285);
+#263= IFCAXIS2PLACEMENT3D(#286,#287,#288);
+#264= IFCAXIS2PLACEMENT3D(#289,#290,#291);
+#265= IFCAXIS2PLACEMENT3D(#292,#293,#294);
+#266= IFCAXIS2PLACEMENT3D(#295,#296,#297);
+#267= IFCAXIS2PLACEMENT3D(#298,#299,#300);
+#268= IFCAXIS2PLACEMENT3D(#301,#302,#303);
+#269= IFCAXIS2PLACEMENT3D(#304,#305,#306);
+#270= IFCAXIS2PLACEMENT3D(#307,#308,#309);
+#271= IFCAXIS2PLACEMENT3D(#310,#311,#312);
+#272= IFCAXIS2PLACEMENT3D(#313,#314,#315);
+#273= IFCAXIS2PLACEMENT3D(#316,#317,#318);
+#274= IFCAXIS2PLACEMENT3D(#319,#320,#321);
+#275= IFCAXIS2PLACEMENT3D(#322,#323,#324);
+#276= IFCAXIS2PLACEMENT3D(#325,#326,#327);
+#277= IFCCARTESIANPOINT((0.,1500.,0.));
+#278= IFCDIRECTION((1.,0.,0.));
+#279= IFCDIRECTION((0.,1.,0.));
+#280= IFCCARTESIANPOINT((0.,3000.,0.));
+#281= IFCDIRECTION((1.,0.,0.));
+#282= IFCDIRECTION((0.,1.,0.));
+#283= IFCCARTESIANPOINT((0.,4500.,0.));
+#284= IFCDIRECTION((1.,0.,0.));
+#285= IFCDIRECTION((0.,1.,0.));
+#286= IFCCARTESIANPOINT((0.,6000.,0.));
+#287= IFCDIRECTION((1.,0.,0.));
+#288= IFCDIRECTION((0.,1.,0.));
+#289= IFCCARTESIANPOINT((0.,7500.,0.));
+#290= IFCDIRECTION((1.,0.,0.));
+#291= IFCDIRECTION((0.,1.,0.));
+#292= IFCCARTESIANPOINT((0.,9000.,0.));
+#293= IFCDIRECTION((1.,0.,0.));
+#294= IFCDIRECTION((0.,1.,0.));
+#295= IFCCARTESIANPOINT((0.,10500.,0.));
+#296= IFCDIRECTION((1.,0.,0.));
+#297= IFCDIRECTION((0.,1.,0.));
+#298= IFCCARTESIANPOINT((0.,12000.,0.));
+#299= IFCDIRECTION((1.,0.,0.));
+#300= IFCDIRECTION((0.,1.,0.));
+#301= IFCCARTESIANPOINT((0.,0.,1500.));
+#302= IFCDIRECTION((0.98,0.081,0.182));
+#303= IFCDIRECTION((-0.0001,0.9138,-0.40616));
+#304= IFCCARTESIANPOINT((0.,1500.,1500.));
+#305= IFCDIRECTION((0.98,0.081,0.182));
+#306= IFCDIRECTION((-0.0001,0.9138,-0.40616));
+#307= IFCCARTESIANPOINT((0.,3000.,1500.));
+#308= IFCDIRECTION((0.98,0.081,0.182));
+#309= IFCDIRECTION((-0.0001,0.9138,-0.40616));
+#310= IFCCARTESIANPOINT((0.,4500.,1500.));
+#311= IFCDIRECTION((0.98,0.081,0.182));
+#312= IFCDIRECTION((-0.0001,0.9138,-0.40616));
+#313= IFCCARTESIANPOINT((0.,6000.,1500.));
+#314= IFCDIRECTION((0.98,0.081,0.182));
+#315= IFCDIRECTION((-0.0001,0.9138,-0.40616));
+#316= IFCCARTESIANPOINT((0.,7500.,1500.));
+#317= IFCDIRECTION((0.98,0.081,0.182));
+#318= IFCDIRECTION((-0.0001,0.9138,-0.40616));
+#319= IFCCARTESIANPOINT((0.,9000.,1500.));
+#320= IFCDIRECTION((0.98,0.081,0.182));
+#321= IFCDIRECTION((-0.0001,0.9138,-0.40616));
+#322= IFCCARTESIANPOINT((0.,10500.,1500.));
+#323= IFCDIRECTION((0.98,0.081,0.182));
+#324= IFCDIRECTION((-0.0001,0.9138,-0.40616));
+#325= IFCCARTESIANPOINT((0.,12000.,1500.));
+#326= IFCDIRECTION((0.98,0.081,0.182));
+#327= IFCDIRECTION((-0.0001,0.9138,-0.40616));
+
+#12= IFCBEAMSTANDARDCASE('0juf4qyggSI8rxA20sznsj',$,'A-2','IPE220','Beam',#239,#80,'A-2',$);
+
+#13= IFCBEAMSTANDARDCASE('0juf4qyggSI8s4A20sznsj',$,'A-3','IPE220','Beam',#240,#83,'A-3',$);
+
+#14= IFCBEAMSTANDARDCASE('0juf4qyggSI8s4A20sznw6',$,'A-4','IPE220','Beam',#241,#86,'A-4',$);
+
+#15= IFCBEAMSTANDARDCASE('0juf4qyggSI8rxA20Qwnab',$,'A-5','IPE220','Beam',#242,#89,'A-5',$);
+
+#16= IFCBEAMSTANDARDCASE('0juf4qyggSI8rxA20Qwng1',$,'A-6','IPE220','Beam',#243,#92,'A-6',$);
+
+#17= IFCBEAMSTANDARDCASE('0juf4qyggSI8rxA20Qwn3s',$,'A-7','IPE220','Beam',#244,#95,'A-7',$);
+
+#18= IFCBEAMSTANDARDCASE('0juf4qyggSI8rxA20Qwnlq',$,'A-8','IPE220','Beam',#245,#98,'A-8',$);
+
+#19= IFCBEAMSTANDARDCASE('0juf4qyggSI8s4A20sznrt',$,'A-9','IPE220','Beam',#246,#101,'A-9',$);
+
+#23= IFCRELAGGREGATES('0ZdtoJM$VsHBqD_feRfxAg',$,$,$,#5,(#259));
+
+#169= IFCBEAMSTANDARDCASE('3QbcAsYoB7Hvx$4VHzijYi',$,'B-1','1/2IPE300','Beam',#247,#104,'B-1',$);
+
+#174= IFCBEAMSTANDARDCASE('3Qb5gsYoB7Hvx$4VHzijYi',$,'B-2','1/2IPE300','Beam',#248,#107,'B-2',$);
+
+#179= IFCBEAMSTANDARDCASE('3QbcAsYsg7Hvx$4VHzijYi',$,'B-3','1/2IPE300','Beam',#249,#110,'B-3',$);
+
+#184= IFCBEAMSTANDARDCASE('3QbcAsYsg7Hvx$4VHzijGT',$,'B-4','1/2IPE300','Beam',#250,#113,'B-4',$);
+
+#189= IFCBEAMSTANDARDCASE('3QbcAsYsg7Hvx$4VHzijdF',$,'B-5','1/2IPE300','Beam',#251,#116,'B-5',$);
+
+#194= IFCBEAMSTANDARDCASE('3QbcAsYsg7Hvx$4VHzij3V',$,'B-6','1/2IPE300','Beam',#252,#119,'B-6',$);
+
+#199= IFCBEAMSTANDARDCASE('3QbcAsYsg7Hvx$4VHzij71',$,'B-7','1/2IPE300','Beam',#253,#122,'B-7',$);
+
+#204= IFCBEAMSTANDARDCASE('3QbcAsYsg7Hvx$4VHzijvb',$,'B-8','1/2IPE300','Beam',#254,#125,'B-8',$);
+
+#209= IFCBEAMSTANDARDCASE('3QbcAsYsg7Hvx$4VHzijp1',$,'B-9','1/2IPE300','Beam',#255,#128,'B-9',$);
+
+#230= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20Q2fsj',$,$,$,(#6),#345);
+#345= IFCMATERIALPROFILESET($,$,(#346),$);
+#346= IFCMATERIALPROFILE('1/2IPE300',$,#347,#166,$,$);
+#347= IFCMATERIAL('S275J2',$,'Steel');
+
+#231= IFCRELDEFINESBYTYPE('2aq$Crcs_xJvN69lbm2bMM',$,'beam typing',$,(#169,#174,#179,#184,#189,#194,#199,#204,#209),#6);
+
+#233= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QfZsj',$,$,$,(#11),#348);
+#348= IFCMATERIALPROFILESETUSAGE(#7,1,$);
+
+#234= IFCRELCONTAINEDINSPATIALSTRUCTURE('2aq$Crcs_xJvtg9lbm2bMM',$,'Physical model',$,(#11,#12,#13,#14,#15,#16,#17,#18,#19,#169,#174,#179,#184,#189,#194,#199,#204,#209),#238);
+
+#238= IFCBUILDING('0Xwup04AK2G8Mt0WNZVy_Z',$,'Building',$,$,#235,$,$,.ELEMENT.,$,$,$);
+
+#259= IFCSITE('10cTefjFQoJexBQrSqFcWZ',$,'Site',$,$,#237,$,$,.ELEMENT.,$,$,$,$,$);
+
+#328= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QgAsj',$,$,$,(#12),#350);
+#350= IFCMATERIALPROFILESETUSAGE(#7,2,$);
+
+#329= IFCRELASSOCIATESMATERIAL('3x0gFSPAr5puQ5WI22xYOm',$,$,$,(#13),#351);
+#351= IFCMATERIALPROFILESETUSAGE(#7,3,$);
+
+#330= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QOHsj',$,$,$,(#14),#352);
+#352= IFCMATERIALPROFILESETUSAGE(#7,4,$);
+
+#331= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QRLsj',$,$,$,(#15),#353);
+#353= IFCMATERIALPROFILESETUSAGE(#7,5,$);
+
+#332= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QMmsj',$,$,$,(#16),#354);
+#354= IFCMATERIALPROFILESETUSAGE(#7,6,$);
+
+#333= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20Qh4sj',$,$,$,(#17),#355);
+#355= IFCMATERIALPROFILESETUSAGE(#7,7,$);
+
+#334= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QwKsj',$,$,$,(#18),#356);
+#356= IFCMATERIALPROFILESETUSAGE(#7,8,$);
+
+#335= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QJksj',$,$,$,(#19),#357);
+#357= IFCMATERIALPROFILESETUSAGE(#7,9,$);
+
+#336= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QbFsj',$,$,$,(#169),#358);
+#358= IFCMATERIALPROFILESETUSAGE(#345,1,$);
+
+#337= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20Qqlsj',$,$,$,(#174),#359);
+#359= IFCMATERIALPROFILESETUSAGE(#345,2,$);
+
+#338= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20Q7isj',$,$,$,(#179),#360);
+#360= IFCMATERIALPROFILESETUSAGE(#345,3,$);
+
+#339= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20Q4nsj',$,$,$,(#184),#361);
+#361= IFCMATERIALPROFILESETUSAGE(#345,4,$);
+
+#340= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20Q41sj',$,$,$,(#189),#362);
+#362= IFCMATERIALPROFILESETUSAGE(#345,5,$);
+
+#341= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QURsj',$,$,$,(#194),#363);
+#363= IFCMATERIALPROFILESETUSAGE(#345,6,$);
+
+#342= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20Qw9sj',$,$,$,(#199),#364);
+#364= IFCMATERIALPROFILESETUSAGE(#345,7,$);
+
+#343= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20QOcsj',$,$,$,(#204),#365);
+#365= IFCMATERIALPROFILESETUSAGE(#345,8,$);
+
+#344= IFCRELASSOCIATESMATERIAL('0juf4qyggSstrxA20Qw6sj',$,$,$,(#209),#366);
+#366= IFCMATERIALPROFILESETUSAGE(#345,9,$);
+
+#349= IFCRELAGGREGATES('1FUTCayKJbIQqDNnZzXu07',$,$,$,#259,(#238));
+ENDSEC;
+
+END-ISO-10303-21;
diff --git a/src/ifcopenshell-python/test/util/test_unit_conversion.py b/src/ifcopenshell-python/test/util/test_unit_conversion.py
new file mode 100644
index 0000000000..af84677647
--- /dev/null
+++ b/src/ifcopenshell-python/test/util/test_unit_conversion.py
@@ -0,0 +1,49 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2023 Dion Moult
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+import pathlib
+
+import pytest
+
+import ifcopenshell.util.unit
+
+UNITS_FIXTURE_DIR = pathlib.Path(__file__).parent.parent / "fixtures" / "units"
+
+
+@pytest.mark.parametrize("ifc_file", UNITS_FIXTURE_DIR.glob("*.ifc"))
+def test_file_units_length_convert(ifc_file):
+ f = ifcopenshell.open(ifc_file)
+ project_unit = ifcopenshell.util.unit.get_project_unit(f, "LENGTHUNIT")
+ if project_unit.Prefix == "MILLI":
+ target_units = "METERS"
+ scale = 0.001
+ else:
+ target_units = "MILLIMETERS"
+ scale = 1000
+
+ new_f = ifcopenshell.util.unit.convert_file_length_units(f, target_units)
+
+ for element, attr, val in ifcopenshell.util.unit.iter_element_and_attributes_per_type(new_f, "IfcLengthMeasure"):
+ elem_id = element.id()
+ original_element = f.by_id(elem_id)
+ original_val = getattr(original_element, attr.name())
+ if isinstance(original_val, tuple):
+ # assert element is equal to original element times scale
+ assert val == tuple([v * scale for v in original_val])
+ else:
+ # assert element is equal to original element times scale
+ assert val == original_val * scale