From f46be80193a153d1311ffad31d52ac47206cb1ae Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Mon, 30 Mar 2026 07:28:01 +0100 Subject: [PATCH] Add api.structural.assign_product, assign_to_building, and api.geometry.add_topology_representation assign_product creates IfcRelAssignsToProduct linking a structural member to a physical building element. assign_to_building creates IfcRelServicesBuildings linking a structural analysis model to a building. add_topology_representation creates IfcTopologyRepresentation for structural elements, inferring the representation type from the item class. Generated with the assistance of an AI coding tool. --- .../ifcopenshell/api/geometry/__init__.py | 2 + .../geometry/add_topology_representation.py | 98 +++++++++++++++++++ .../ifcopenshell/api/structural/__init__.py | 4 + .../api/structural/assign_product.py | 64 ++++++++++++ .../api/structural/assign_to_building.py | 64 ++++++++++++ .../test_add_topology_representation.py | 82 ++++++++++++++++ .../api/structural/test_assign_product.py | 56 +++++++++++ .../api/structural/test_assign_to_building.py | 62 ++++++++++++ 8 files changed, 432 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/geometry/add_topology_representation.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/structural/assign_product.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/structural/assign_to_building.py create mode 100644 src/ifcopenshell-python/test/api/geometry/test_add_topology_representation.py create mode 100644 src/ifcopenshell-python/test/api/structural/test_assign_product.py create mode 100644 src/ifcopenshell-python/test/api/structural/test_assign_to_building.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py index a8a5ca609b..d845f4dc83 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py @@ -25,6 +25,7 @@ geometry extrusions). from .. import wrap_usecases from .add_axis_representation import add_axis_representation +from .add_topology_representation import add_topology_representation from .add_boolean import add_boolean from .clip_solid import clip_solid from .clip_solid_bounded import clip_solid_bounded @@ -63,6 +64,7 @@ wrap_usecases(__path__, __name__) __all__ = [ "add_axis_representation", + "add_topology_representation", "add_boolean", "clip_solid", "clip_solid_bounded", diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_topology_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_topology_representation.py new file mode 100644 index 0000000000..14c4fa9dcc --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_topology_representation.py @@ -0,0 +1,98 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2026 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 . +# This file was generated with the assistance of an AI coding tool. + +from typing import Optional + +import ifcopenshell + + +_ITEM_TYPE_TO_REP_TYPE = { + "IfcVertex": "Vertex", + "IfcVertexPoint": "Vertex", + "IfcEdge": "Edge", + "IfcOrientedEdge": "Edge", + "IfcEdgeCurve": "Edge", + "IfcEdgeLoop": "Edge", + "IfcPath": "Edge", + "IfcFace": "Face", + "IfcFaceSurface": "Face", + "IfcAdvancedFace": "Face", + "IfcClosedShell": "Face", + "IfcOpenShell": "Face", + "IfcConnectedFaceSet": "Face", +} + + +def add_topology_representation( + file: ifcopenshell.file, + context: ifcopenshell.entity_instance, + item: ifcopenshell.entity_instance, + representation_identifier: Optional[str] = None, + representation_type: Optional[str] = None, +) -> ifcopenshell.entity_instance: + """Adds an IfcTopologyRepresentation for a structural element + + Structural analysis elements (IfcStructuralSurfaceMember, + IfcStructuralCurveMember) use topology representations rather than solid + geometry. This is analogous to :func:`add_axis_representation` and + :func:`add_profile_representation` but produces an + IfcTopologyRepresentation instead of an IfcShapeRepresentation. + + The representation type ("Face", "Edge", "Vertex") is inferred from the + item's IFC class if not provided explicitly. + + :param context: The IfcGeometricRepresentationContext for the + representation, typically a Reference context. + :param item: The IfcTopologicalRepresentationItem (e.g. IfcFaceSurface, + IfcEdge) to include in the representation. + :param representation_identifier: The RepresentationIdentifier string. + Defaults to the context's ContextIdentifier. + :param representation_type: The RepresentationType string ("Face", + "Edge", "Vertex"). Inferred from item class if not given. + :return: The newly created IfcTopologyRepresentation entity. + + Example: + + .. code:: python + + context = ifcopenshell.util.representation.get_context( + model, "Model", "Reference", "GRAPH_VIEW") + face = model.createIfcFaceSurface(bounds, surface, True) + rep = ifcopenshell.api.geometry.add_topology_representation( + model, context=context, item=face) + ifcopenshell.api.geometry.assign_representation( + model, product=member, representation=rep) + """ + if representation_identifier is None: + representation_identifier = context.ContextIdentifier + + if representation_type is None: + for ifc_class, rep_type in _ITEM_TYPE_TO_REP_TYPE.items(): + if item.is_a(ifc_class): + representation_type = rep_type + break + else: + representation_type = "Undefined" + + return file.createIfcTopologyRepresentation( + context, + representation_identifier, + representation_type, + [item], + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/structural/__init__.py index 2baa262432..8a21e73602 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/__init__.py @@ -30,7 +30,9 @@ from .add_structural_load import add_structural_load from .add_structural_load_case import add_structural_load_case from .add_structural_load_group import add_structural_load_group from .add_structural_member_connection import add_structural_member_connection +from .assign_product import assign_product from .assign_structural_analysis_model import assign_structural_analysis_model +from .assign_to_building import assign_to_building from .edit_structural_analysis_model import edit_structural_analysis_model from .edit_structural_boundary_condition import edit_structural_boundary_condition from .edit_structural_connection_cs import edit_structural_connection_cs @@ -57,7 +59,9 @@ __all__ = [ "add_structural_load_case", "add_structural_load_group", "add_structural_member_connection", + "assign_product", "assign_structural_analysis_model", + "assign_to_building", "edit_structural_analysis_model", "edit_structural_boundary_condition", "edit_structural_connection_cs", diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/assign_product.py b/src/ifcopenshell-python/ifcopenshell/api/structural/assign_product.py new file mode 100644 index 0000000000..a30672facd --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/assign_product.py @@ -0,0 +1,64 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2026 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 . +# This file was generated with the assistance of an AI coding tool. + +import ifcopenshell +import ifcopenshell.api.root + + +def assign_product( + file: ifcopenshell.file, + relating_product: ifcopenshell.entity_instance, + related_object: ifcopenshell.entity_instance, +) -> ifcopenshell.entity_instance: + """Links an object to a product via IfcRelAssignsToProduct + + Typically used to associate a physical building element with a structural + analysis member (IfcStructuralSurfaceMember, IfcStructuralCurveMember) so + that analysis results can be traced back to the physical model. + + :param relating_product: The IfcProduct that the object is assigned to, + typically an IfcStructuralMember. + :param related_object: The IfcObjectDefinition being assigned, typically + a physical building element such as an IfcWall or IfcSlab. + :return: The IfcRelAssignsToProduct relationship. + + Example: + + .. code:: python + + wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall") + member = ifcopenshell.api.root.create_entity( + model, ifc_class="IfcStructuralSurfaceMember") + ifcopenshell.api.structural.assign_product(model, + relating_product=member, related_object=wall) + """ + for rel in relating_product.ReferencedBy or []: + if not rel.is_a("IfcRelAssignsToProduct"): + continue + if related_object in rel.RelatedObjects: + return rel + related_objects = list(rel.RelatedObjects) + related_objects.append(related_object) + rel.RelatedObjects = related_objects + return rel + + rel = ifcopenshell.api.root.create_entity(file, ifc_class="IfcRelAssignsToProduct") + rel.RelatingProduct = relating_product + rel.RelatedObjects = [related_object] + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/assign_to_building.py b/src/ifcopenshell-python/ifcopenshell/api/structural/assign_to_building.py new file mode 100644 index 0000000000..7e70f20272 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/assign_to_building.py @@ -0,0 +1,64 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2026 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 . +# This file was generated with the assistance of an AI coding tool. + +import ifcopenshell +import ifcopenshell.api.owner +import ifcopenshell.guid + + +def assign_to_building( + file: ifcopenshell.file, + structural_analysis_model: ifcopenshell.entity_instance, + building: ifcopenshell.entity_instance, +) -> ifcopenshell.entity_instance: + """Associates a structural analysis model with a building via IfcRelServicesBuildings + + The existing :func:`assign_structural_analysis_model` handles + IfcRelAssignsToGroup (linking structural members to the analysis model). + This function handles the separate model-to-building relationship, which + records which building the structural analysis model serves. + + :param structural_analysis_model: The IfcStructuralAnalysisModel to + associate with the building. + :param building: The IfcBuilding (or other IfcSpatialStructureElement) + that the structural analysis model serves. + :return: The IfcRelServicesBuildings relationship. + + Example: + + .. code:: python + + building = ifcopenshell.util.selector.filter_elements(model, "IfcBuilding")[0] + model_ = ifcopenshell.api.structural.add_structural_analysis_model(model) + ifcopenshell.api.structural.assign_to_building(model, + structural_analysis_model=model_, building=building) + """ + for rel in structural_analysis_model.ServicesBuildings or []: + if building in rel.RelatedBuildings: + return rel + rel.RelatedBuildings = list(rel.RelatedBuildings) + [building] + return rel + + return file.create_entity( + "IfcRelServicesBuildings", + ifcopenshell.guid.new(), + OwnerHistory=ifcopenshell.api.owner.create_owner_history(file), + RelatingSystem=structural_analysis_model, + RelatedBuildings=[building], + ) diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_topology_representation.py b/src/ifcopenshell-python/test/api/geometry/test_add_topology_representation.py new file mode 100644 index 0000000000..5b89f50338 --- /dev/null +++ b/src/ifcopenshell-python/test/api/geometry/test_add_topology_representation.py @@ -0,0 +1,82 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2026 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 . +# This file was generated with the assistance of an AI coding tool. + +import ifcopenshell.api.context +import ifcopenshell.api.geometry +import ifcopenshell.api.root +import test.bootstrap + + +class TestAddTopologyRepresentation(test.bootstrap.IFC4): + def setup_context(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + model = ifcopenshell.api.context.add_context(self.file, context_type="Model") + return ifcopenshell.api.context.add_context( + self.file, + context_type="Model", + context_identifier="Reference", + target_view="GRAPH_VIEW", + parent=model, + ) + + def test_creates_topology_representation(self): + context = self.setup_context() + face = self.file.create_entity("IfcFaceSurface") + rep = ifcopenshell.api.geometry.add_topology_representation(self.file, context=context, item=face) + assert rep.is_a("IfcTopologyRepresentation") + assert rep.ContextOfItems == context + assert face in rep.Items + + def test_infers_face_representation_type(self): + context = self.setup_context() + face = self.file.create_entity("IfcFaceSurface") + rep = ifcopenshell.api.geometry.add_topology_representation(self.file, context=context, item=face) + assert rep.RepresentationType == "Face" + + def test_infers_edge_representation_type(self): + context = self.setup_context() + edge = self.file.create_entity("IfcEdge") + rep = ifcopenshell.api.geometry.add_topology_representation(self.file, context=context, item=edge) + assert rep.RepresentationType == "Edge" + + def test_defaults_representation_identifier_to_context_identifier(self): + context = self.setup_context() + face = self.file.create_entity("IfcFaceSurface") + rep = ifcopenshell.api.geometry.add_topology_representation(self.file, context=context, item=face) + assert rep.RepresentationIdentifier == context.ContextIdentifier + + def test_custom_representation_identifier(self): + context = self.setup_context() + face = self.file.create_entity("IfcFaceSurface") + rep = ifcopenshell.api.geometry.add_topology_representation( + self.file, context=context, item=face, representation_identifier="Body" + ) + assert rep.RepresentationIdentifier == "Body" + + def test_custom_representation_type_overrides_inferred(self): + context = self.setup_context() + face = self.file.create_entity("IfcFaceSurface") + rep = ifcopenshell.api.geometry.add_topology_representation( + self.file, context=context, item=face, representation_type="Undefined" + ) + assert rep.RepresentationType == "Undefined" + + +class TestAddTopologyRepresentationIFC2X3(test.bootstrap.IFC2X3, TestAddTopologyRepresentation): + pass diff --git a/src/ifcopenshell-python/test/api/structural/test_assign_product.py b/src/ifcopenshell-python/test/api/structural/test_assign_product.py new file mode 100644 index 0000000000..3e2f1c2b4a --- /dev/null +++ b/src/ifcopenshell-python/test/api/structural/test_assign_product.py @@ -0,0 +1,56 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2026 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 . +# This file was generated with the assistance of an AI coding tool. + +import ifcopenshell.api.root +import ifcopenshell.api.structural +import test.bootstrap + + +class TestAssignProduct(test.bootstrap.IFC4): + def test_creating_a_new_relationship(self): + member = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcStructuralSurfaceMember") + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + rel = ifcopenshell.api.structural.assign_product(self.file, relating_product=member, related_object=wall) + assert rel.is_a("IfcRelAssignsToProduct") + assert rel.RelatingProduct == member + assert wall in rel.RelatedObjects + + def test_adding_a_second_object_to_an_existing_relationship(self): + member = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcStructuralSurfaceMember") + wall1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + rel1 = ifcopenshell.api.structural.assign_product(self.file, relating_product=member, related_object=wall1) + rel2 = ifcopenshell.api.structural.assign_product(self.file, relating_product=member, related_object=wall2) + assert rel1 == rel2 + assert len(self.file.by_type("IfcRelAssignsToProduct")) == 1 + assert wall1 in rel1.RelatedObjects + assert wall2 in rel1.RelatedObjects + + def test_does_not_duplicate_an_existing_assignment(self): + member = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcStructuralSurfaceMember") + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.structural.assign_product(self.file, relating_product=member, related_object=wall) + ifcopenshell.api.structural.assign_product(self.file, relating_product=member, related_object=wall) + assert len(self.file.by_type("IfcRelAssignsToProduct")) == 1 + rels = self.file.by_type("IfcRelAssignsToProduct") + assert len(rels[0].RelatedObjects) == 1 + + +class TestAssignProductIFC2X3(test.bootstrap.IFC2X3, TestAssignProduct): + pass diff --git a/src/ifcopenshell-python/test/api/structural/test_assign_to_building.py b/src/ifcopenshell-python/test/api/structural/test_assign_to_building.py new file mode 100644 index 0000000000..671ca1b476 --- /dev/null +++ b/src/ifcopenshell-python/test/api/structural/test_assign_to_building.py @@ -0,0 +1,62 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2026 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 . +# This file was generated with the assistance of an AI coding tool. + +import ifcopenshell.api.root +import ifcopenshell.api.structural +import test.bootstrap + + +class TestAssignToBuilding(test.bootstrap.IFC4): + def test_creating_a_new_relationship(self): + model = ifcopenshell.api.structural.add_structural_analysis_model(self.file) + building = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuilding") + rel = ifcopenshell.api.structural.assign_to_building( + self.file, structural_analysis_model=model, building=building + ) + assert rel.is_a("IfcRelServicesBuildings") + assert rel.RelatingSystem == model + assert building in rel.RelatedBuildings + + def test_adding_a_second_building_to_an_existing_relationship(self): + model = ifcopenshell.api.structural.add_structural_analysis_model(self.file) + building1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuilding") + building2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuilding") + rel1 = ifcopenshell.api.structural.assign_to_building( + self.file, structural_analysis_model=model, building=building1 + ) + rel2 = ifcopenshell.api.structural.assign_to_building( + self.file, structural_analysis_model=model, building=building2 + ) + assert rel1 == rel2 + assert len(self.file.by_type("IfcRelServicesBuildings")) == 1 + assert building1 in rel1.RelatedBuildings + assert building2 in rel1.RelatedBuildings + + def test_does_not_duplicate_an_existing_assignment(self): + model = ifcopenshell.api.structural.add_structural_analysis_model(self.file) + building = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuilding") + ifcopenshell.api.structural.assign_to_building(self.file, structural_analysis_model=model, building=building) + ifcopenshell.api.structural.assign_to_building(self.file, structural_analysis_model=model, building=building) + assert len(self.file.by_type("IfcRelServicesBuildings")) == 1 + rels = self.file.by_type("IfcRelServicesBuildings") + assert len(rels[0].RelatedBuildings) == 1 + + +class TestAssignToBuildingIFC2X3(test.bootstrap.IFC2X3, TestAssignToBuilding): + pass