# 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 . from math import pi import numpy as np import pytest import ifcopenshell.api.context import ifcopenshell.api.georeference import ifcopenshell.api.pset import ifcopenshell.api.root import ifcopenshell.api.unit import ifcopenshell.util.element import ifcopenshell.util.geolocation import ifcopenshell.util.unit as subject import test.bootstrap from ifcopenshell.util.shape_builder import ShapeBuilder class TestMmToM: def test_converts_a_positive_value(self): assert subject.mm_to_m(150) == 0.15 def test_returns_zero_for_zero(self): assert subject.mm_to_m(0) == 0.0 def test_passes_through_negative_values(self): assert subject.mm_to_m(-25) == -0.025 class TestCacheUnits(test.bootstrap.IFC4): def test_run(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) assert self.file.units == {} subject.cache_units(self.file) assert self.file.units == {"LENGTHUNIT": length, "AREAUNIT": area} class TestClearUnitCache(test.bootstrap.IFC4): def test_run(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) subject.cache_units(self.file) subject.clear_unit_cache(self.file) assert self.file.units == {} class TestGetProjectUnit(test.bootstrap.IFC4): def test_run(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) assert subject.get_project_unit(self.file, "LENGTHUNIT") == length assert subject.get_project_unit(self.file, "AREAUNIT") == area def test_using_a_cache(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) assert self.file.units == {} assert subject.get_project_unit(self.file, "LENGTHUNIT", use_cache=True) == length assert self.file.units == {"LENGTHUNIT": length, "AREAUNIT": area} ifcopenshell.api.unit.assign_unit(self.file, units=[length2]) assert subject.get_project_unit(self.file, "LENGTHUNIT", use_cache=True) == length subject.clear_unit_cache(self.file) assert subject.get_project_unit(self.file, "LENGTHUNIT", use_cache=True) == length2 assert self.file.units == {"LENGTHUNIT": length2, "AREAUNIT": area} class TestGetPropertyUnit(test.bootstrap.IFC4): def test_no_unit(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") prop = self.file.createIfcQuantityLength(Name="Foo", LengthValue=42.0) assert subject.get_property_unit(prop, self.file) is None def test_simple_quantity(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) prop = self.file.createIfcQuantityLength(Name="Foo", LengthValue=42.0) assert subject.get_property_unit(prop, self.file) == length prop.Unit = length2 assert subject.get_property_unit(prop, self.file) == length2 def test_single_value(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) prop = self.file.createIfcPropertySingleValue(Name="Foo", NominalValue=self.file.createIfcLengthMeasure(42.0)) assert subject.get_property_unit(prop, self.file) == length prop.Unit = length2 assert subject.get_property_unit(prop, self.file) == length2 def test_enumerated_value(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) prop = self.file.createIfcPropertyEnumeratedValue( Name="Foo", EnumerationValues=[self.file.createIfcLengthMeasure(42.0)] ) assert subject.get_property_unit(prop, self.file) == length prop.EnumerationValues = [] prop.EnumerationReference = self.file.createIfcPropertyEnumeration( "Foo", [self.file.createIfcAreaMeasure(42.0)] ) assert subject.get_property_unit(prop, self.file) == area prop.EnumerationReference = self.file.createIfcPropertyEnumeration( "Foo", [self.file.createIfcAreaMeasure(42.0)], Unit=length2 ) assert subject.get_property_unit(prop, self.file) == length2 prop.EnumerationValues = [self.file.createIfcAreaMeasure(42.0)] assert subject.get_property_unit(prop, self.file) == length2 prop.EnumerationReference.Unit = None assert subject.get_property_unit(prop, self.file) == area def test_list_value(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) prop = self.file.createIfcPropertyListValue(Name="Foo", ListValues=[self.file.createIfcLengthMeasure(42.0)]) assert subject.get_property_unit(prop, self.file) == length prop.Unit = length2 assert subject.get_property_unit(prop, self.file) == length2 prop.Unit = None prop.ListValues = [] assert subject.get_property_unit(prop, self.file) is None def test_bounded_value(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) prop = self.file.createIfcPropertyBoundedValue(Name="Foo") assert subject.get_property_unit(prop, self.file) is None prop.UpperBoundValue = self.file.createIfcLengthMeasure(42.0) assert subject.get_property_unit(prop, self.file) == length prop.UpperBoundValue = None prop.LowerBoundValue = self.file.createIfcLengthMeasure(42.0) assert subject.get_property_unit(prop, self.file) == length prop.LowerBoundValue = None prop.SetPointValue = self.file.createIfcLengthMeasure(42.0) assert subject.get_property_unit(prop, self.file) == length prop.Unit = length2 assert subject.get_property_unit(prop, self.file) == length2 class TestConvert(test.bootstrap.IFC4): def test_run(self): assert subject.convert(1, None, "METRE", None, "METRE") == 1 assert subject.convert(1, None, "METRE", "MILLI", "METRE") == 1000 assert subject.convert(1000, "MILLI", "METRE", None, "METRE") == 1 assert subject.convert(1, None, "SQUARE_METRE", None, "SQUARE_METRE") == 1 assert subject.convert(1, None, "SQUARE_METRE", "MILLI", "SQUARE_METRE") == 1000000 assert subject.convert(1, None, "CUBIC_METRE", "MILLI", "CUBIC_METRE") == 1000000000 class TestCalculateUnitScale(test.bootstrap.IFC4): def test_prefix_and_conversion_based_units_are_considered(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") length = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="foot") length.ConversionFactor.UnitComponent.Prefix = "MILLI" ifcopenshell.api.unit.assign_unit(self.file, units=[length]) assert subject.calculate_unit_scale(self.file) == 0.3048 * 0.001 angle = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="degree") angle.ConversionFactor.UnitComponent.Prefix = "MILLI" ifcopenshell.api.unit.assign_unit(self.file, units=[angle]) assert subject.calculate_unit_scale(self.file, "PLANEANGLEUNIT") == pi / 180 * 0.001 class TestFormatLength(test.bootstrap.IFC4): def test_run(self): assert subject.format_length(1, 1, decimal_places=0, unit_system="metric") == "1" assert subject.format_length(1, 1, decimal_places=2, unit_system="metric") == "1.00" assert subject.format_length(3, 5, decimal_places=2, unit_system="metric") == "5.00" assert subject.format_length(3.123, 0.01, decimal_places=2, unit_system="metric") == "3.12" assert subject.format_length(3, 1, unit_system="imperial", input_unit="foot") == "3'" assert subject.format_length(3.5, 1, unit_system="imperial", input_unit="foot") == "3' - 6\"" assert subject.format_length(3.123, 1, unit_system="imperial", input_unit="foot") == "3' - 1\"" assert subject.format_length(3.123, 2, unit_system="imperial", input_unit="foot") == "3' - 1 1/2\"" assert subject.format_length(3.123, 4, unit_system="imperial", input_unit="foot") == "3' - 1 1/2\"" assert subject.format_length(3.123, 32, unit_system="imperial", input_unit="foot") == "3' - 1 15/32\"" assert subject.format_length(24, 1, unit_system="imperial", input_unit="inch") == "2'" assert subject.format_length(25.23, 1, unit_system="imperial", input_unit="inch") == "2' - 1\"" assert subject.format_length(25.23, 4, unit_system="imperial", input_unit="inch") == "2' - 1 1/4\"" assert subject.format_length(3, 1, unit_system="imperial", input_unit="foot", output_unit="inch") == '36"' assert subject.format_length(3.5, 1, unit_system="imperial", input_unit="foot", output_unit="inch") == '42"' assert subject.format_length(3.123, 1, unit_system="imperial", input_unit="foot", output_unit="inch") == '37"' assert ( subject.format_length(3.123, 2, unit_system="imperial", input_unit="foot", output_unit="inch") == '37 1/2"' ) assert ( subject.format_length(3.123, 4, unit_system="imperial", input_unit="foot", output_unit="inch") == '37 1/2"' ) assert ( subject.format_length(3.123, 32, unit_system="imperial", input_unit="foot", output_unit="inch") == '37 15/32"' ) assert subject.format_length(24, 1, unit_system="imperial", input_unit="inch", output_unit="inch") == '24"' assert subject.format_length(25.23, 1, unit_system="imperial", input_unit="inch", output_unit="inch") == '25"' assert ( subject.format_length(25.23, 4, unit_system="imperial", input_unit="inch", output_unit="inch") == '25 1/4"' ) class TestIsAttrType(test.bootstrap.IFC4): def test_run(self): schema = ifcopenshell.schema_by_name("IFC4") declaration = schema.declaration_by_name("IfcPropertySingleValue").as_entity() assert declaration nominal_value = declaration.attribute_by_index(2).type_of_attribute() assert subject.is_attr_type(nominal_value, "IfcValue") assert subject.is_attr_type(nominal_value, "IfcLengthMeasure") assert not subject.is_attr_type(nominal_value, "IfcLengthMeasure", include_select_types=False) class TestConvertFileLengthUnits(test.bootstrap.IFC2X3): def test_run(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) output = subject.convert_file_length_units(self.file, target_units="METER") # there was some renumbering bug in the rocksdb rewrite this statement is to test for that assert max(i.id() for i in output) == len(output.wrapped_data.entity_names()) + 1 assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" def test_precision_conversion(self): # Regression test for #6127: IfcGeometricRepresentationContext.Precision # is typed IfcReal but interpreted in the project length unit, so it must # be scaled along with the length measures. ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) context = ifcopenshell.api.context.add_context(self.file, context_type="Model") context.Precision = 0.01 # Subcontexts derive Precision from the parent and must be left alone. ifcopenshell.api.context.add_context( self.file, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=context ) output = subject.convert_file_length_units(self.file, target_units="METER") new_context = output.by_type("IfcGeometricRepresentationContext", include_subtypes=False)[0] assert new_context.Precision == pytest.approx(0.00001) def test_attribute_conversion(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") builder = ShapeBuilder(self.file) rectangle = builder.rectangle((100, 100)) extrusion = builder.extrude(rectangle, 1000) # IfcLengthMeasure entities. product = ifcopenshell.api.root.create_entity(self.file, "IfcWall") pset = ifcopenshell.api.pset.add_pset(self.file, product, "TestPset") # Consider weird case when same entity is reused. length_measure = self.file.create_entity("IfcLengthMeasure", 50.0) enum_property = self.file.create_entity( "IfcPropertyEnumeratedValue", Name="Enum", # Not entirely sure if there are real life cases when mixed typed entities used in the list # but just to be safe. EnumerationValues=[ length_measure, self.file.create_entity("IfcLabel", "TEXT"), self.file.create_entity("IfcLengthMeasure", 250.0), length_measure, ], ) ifcopenshell.api.pset.edit_pset( self.file, pset, properties={ "Length": self.file.create_entity("IfcLengthMeasure", 25.0), "Enum": enum_property, }, ) ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) output = subject.convert_file_length_units(self.file, target_units="METER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" extrusion = output.by_type("IfcExtrudedAreaSolid")[0] # Simple float attribute. assert extrusion.Depth == 1 # List of floats in IFC2X3 and list of lists of floats in IFC4+. rectangle = extrusion.SweptArea.OuterCurve expected_points = [(0.0, 0.0), (0.1, 0.0), (0.1, 0.1), (0.0, 0.1)] if self.file.schema == "IFC2X3": # IfcPolyline. points = [p.Coordinates for p in rectangle.Points] expected_points += expected_points[:1] else: # IfcIndexedPolyCurve. points = rectangle.Points.CoordList assert np.allclose(points, expected_points) # IfcLengthMeasure entities. product = output.by_type("IfcWall")[0] pset_data = ifcopenshell.util.element.get_pset(product, "TestPset") assert pset_data["Length"] == 0.025 assert pset_data["Enum"] == [0.05, "TEXT", 0.25, 0.05] def test_converting_map_conversion_if_there_is_no_map_unit(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") ifcopenshell.api.context.add_context(self.file, "Model") ifcopenshell.api.georeference.add_georeferencing(self.file) ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Eastings": 10000}) ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) output = subject.convert_file_length_units(self.file, target_units="METER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" if self.file.schema == "IFC2X3": parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) assert parameters assert parameters.e == 10 else: assert output.by_type("IfcMapConversion")[0].Eastings == 10 def test_preserving_enh_if_there_is_a_map_unit(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") meter = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") ifcopenshell.api.context.add_context(self.file, "Model") ifcopenshell.api.georeference.add_georeferencing(self.file) map_unit = subject.get_full_unit_name(meter) if self.file.schema == "IFC2X3" else meter ifcopenshell.api.georeference.edit_georeferencing( self.file, projected_crs={"MapUnit": map_unit}, coordinate_operation={"Eastings": 10, "Scale": 0.001}, ) ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) output = subject.convert_file_length_units(self.file, target_units="METER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" if self.file.schema == "IFC2X3": parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) assert parameters assert parameters.e == 10 assert parameters.n == 0 assert parameters.scale == 1 crs = ifcopenshell.util.element.get_pset(output.by_type("IfcProject")[0], name="ePSet_ProjectedCRS") assert crs["MapUnit"] == "METRE" else: map_conversion = output.by_type("IfcMapConversion")[0] assert map_conversion.Eastings == 10 assert map_conversion.Northings == 0 assert map_conversion.Scale == 1 assert subject.get_full_unit_name(output.by_type("IfcProjectedCRS")[0].MapUnit) == "METRE" def test_preserving_enh_if_there_is_a_map_unit_which_is_also_the_project_default(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") meter = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") ifcopenshell.api.context.add_context(self.file, "Model") ifcopenshell.api.georeference.add_georeferencing(self.file) map_unit = subject.get_full_unit_name(meter) if self.file.schema == "IFC2X3" else meter ifcopenshell.api.georeference.edit_georeferencing( self.file, projected_crs={"MapUnit": map_unit}, coordinate_operation={"Eastings": 10, "Scale": 1}, ) ifcopenshell.api.unit.assign_unit(self.file, units=[meter]) output = subject.convert_file_length_units(self.file, target_units="MILLIMETER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "MILLIMETRE" if self.file.schema == "IFC2X3": parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) assert parameters assert parameters.e == 10 assert parameters.n == 0 assert parameters.scale == 1 crs = ifcopenshell.util.element.get_pset(output.by_type("IfcProject")[0], name="ePSet_ProjectedCRS") assert crs["MapUnit"] == "METRE" else: map_conversion = output.by_type("IfcMapConversion")[0] assert map_conversion.Eastings == 10 assert map_conversion.Northings == 0 assert map_conversion.Scale == 0.001 assert subject.get_full_unit_name(output.by_type("IfcProjectedCRS")[0].MapUnit) == "METRE" unit_assignment = subject.get_unit_assignment(output) assert unit_assignment assert len(unit_assignment.Units) == 1 class TestConvertFileLengthUnitsIFC4(test.bootstrap.IFC4, TestConvertFileLengthUnits): pass class TestConvertFileLengthUnitsIFC4X3(test.bootstrap.IFC4X3, TestConvertFileLengthUnits): pass