ifcpatch: add FixDoubleTypedPredefinedType recipe for OJT001 cleanup

Existing IFC files (Revit exports, or Bonsai models typed before a
type's PredefinedType was set) commonly carry an explicit
PredefinedType/ObjectType on occurrences whose type already has a
concrete PredefinedType, violating buildingSMART rule OJT001. PR #8829
stops this happening going forward at the authoring layer; this recipe
is the one-click cleanup for files that already have the problem.

The "is the type concrete" check mirrors
ifcopenshell.api.type.assign_type and
ifcopenshell.api.attribute.edit_attributes exactly, including the
USERDEFINED nuance (only counts as concrete if the type also carries a
custom description to fall back on), so behaviour stays consistent
between authoring and cleanup. Bonsai's IFC Patch UI discovers recipes
by globbing ifcpatch/recipes/*.py, so no Bonsai UI changes are needed.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Petru Conduraru
2026-07-21 15:38:12 +03:00
parent e52e5e2e58
commit 01bd6cb897
2 changed files with 288 additions and 0 deletions
@@ -0,0 +1,101 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcPatch.
#
# IfcPatch 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.
#
# IfcPatch 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 IfcPatch. If not, see <http://www.gnu.org/licenses/>.
# This file was generated with the assistance of an AI coding tool.
from logging import Logger
from typing import Union
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.selector
import ifcpatch
class Patcher(ifcpatch.BasePatcher):
def __init__(self, file: ifcopenshell.file, logger: Union[Logger, None] = None, query: str = ""):
"""Clear an occurrence's PredefinedType/ObjectType when its type is already concrete
IFC forbids "double typing": if an IfcTypeObject's own PredefinedType
is a concrete value (i.e. not NOTDEFINED), the PredefinedType and
ObjectType of its typed occurrences must be left empty, since the
type already carries that information (buildingSMART rule OJT001).
Legacy files, or files typed before their type's PredefinedType was
set, commonly carry a stray PredefinedType (often NOTDEFINED) or
ObjectType on the occurrence anyway. This recipe finds every
occurrence typed by a type with a concrete PredefinedType and clears
the occurrence's own PredefinedType and ObjectType attributes,
without touching untyped occurrences or occurrences of a type that is
itself NOTDEFINED (as those may still carry meaningful data of their
own).
The "is the type concrete" check mirrors
ifcopenshell.api.type.assign_type and
ifcopenshell.api.attribute.edit_attributes: a type whose
PredefinedType is USERDEFINED only counts as concrete if it also has
a custom type name (e.g. ElementType) to fall back on. This keeps the
recipe non-destructive: it will never remove the only description an
occurrence has.
:param query: A query to select the subset of IFC elements to
restrict the patch to, optional. If not provided, patch will be
applied to all typed occurrences in the model. See
ifcopenshell.util.selector for query syntax.
Example:
.. code:: python
# Fix all double-typed occurrences in the model.
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixDoubleTypedPredefinedType"})
# Only fix double-typed walls.
ifcpatch.execute({
"input": "input.ifc",
"file": model,
"recipe": "FixDoubleTypedPredefinedType",
"arguments": ["IfcWall"],
})
"""
super().__init__(file, logger)
self.query = query
def patch(self):
occurrences = None
if self.query:
occurrences = ifcopenshell.util.selector.filter_elements(self.file, self.query)
cleared = 0
for rel in self.file.by_type("IfcRelDefinesByType"):
predefined_type = ifcopenshell.util.element.get_predefined_type(rel.RelatingType)
if predefined_type is None or predefined_type == "NOTDEFINED":
continue
for occurrence in rel.RelatedObjects:
if occurrences is not None and occurrence not in occurrences:
continue
has_predefined_type = hasattr(occurrence, "PredefinedType")
if not occurrence.ObjectType and (not has_predefined_type or not occurrence.PredefinedType):
continue # Already clean.
occurrence.ObjectType = None
if has_predefined_type:
occurrence.PredefinedType = None
cleared += 1
self.logger.info(f"Cleared PredefinedType/ObjectType on {cleared} double-typed occurrence(s).")
@@ -0,0 +1,187 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2026 Petru Conduraru <petru@bimvoice.com>
#
# 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 <http://www.gnu.org/licenses/>.
# This file was generated with the assistance of an AI coding tool.
import ifcopenshell.api.root
import ifcopenshell.api.type
import ifcpatch
import test.bootstrap
class TestFixDoubleTypedPredefinedType(test.bootstrap.IFC4):
def test_clears_occurrence_under_concrete_type(self):
wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1")
wall_type.PredefinedType = "MOVABLE"
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
wall.PredefinedType = "NOTDEFINED"
wall.ObjectType = "Some legacy value"
ifcopenshell.api.type.assign_type(
self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False
)
# assign_type would already clear these; force them back to simulate
# a legacy file where the occurrence was typed before this cleanup
# existed.
wall.PredefinedType = "NOTDEFINED"
wall.ObjectType = "Some legacy value"
output = ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []})
assert wall.PredefinedType is None
assert wall.ObjectType is None
assert output == self.file
def test_leaves_occurrence_under_notdefined_type_untouched(self):
wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1")
wall_type.PredefinedType = "NOTDEFINED"
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
wall.PredefinedType = "MOVABLE"
wall.ObjectType = "Legitimate own value"
ifcopenshell.api.type.assign_type(
self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False
)
wall.PredefinedType = "MOVABLE"
wall.ObjectType = "Legitimate own value"
ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []})
assert wall.PredefinedType == "MOVABLE"
assert wall.ObjectType == "Legitimate own value"
def test_leaves_untyped_element_untouched(self):
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
wall.PredefinedType = "NOTDEFINED"
wall.ObjectType = "Untyped value"
ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []})
assert wall.PredefinedType == "NOTDEFINED"
assert wall.ObjectType == "Untyped value"
def test_userdefined_type_with_custom_description_clears_occurrence(self):
# Mirrors ifcopenshell.api.type.assign_type: a USERDEFINED type only
# counts as "concrete" if it also has a custom type name to fall
# back on (here IfcWallType.ElementType).
wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1")
wall_type.PredefinedType = "USERDEFINED"
wall_type.ElementType = "MyCustomWall"
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
wall.PredefinedType = "NOTDEFINED"
wall.ObjectType = "Some legacy value"
ifcopenshell.api.type.assign_type(
self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False
)
wall.PredefinedType = "NOTDEFINED"
wall.ObjectType = "Some legacy value"
ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []})
assert wall.PredefinedType is None
assert wall.ObjectType is None
def test_userdefined_type_without_custom_description_untouched(self):
# A USERDEFINED type with no ElementType/ObjectType of its own
# carries no real type information, so assign_type leaves the
# occurrence alone. This recipe mirrors that.
wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1")
wall_type.PredefinedType = "USERDEFINED"
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
wall.PredefinedType = "NOTDEFINED"
wall.ObjectType = "Kept because type has no description"
ifcopenshell.api.type.assign_type(
self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False
)
wall.PredefinedType = "NOTDEFINED"
wall.ObjectType = "Kept because type has no description"
ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []})
assert wall.PredefinedType == "NOTDEFINED"
assert wall.ObjectType == "Kept because type has no description"
def test_already_clean_file_is_a_no_op(self):
wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1")
wall_type.PredefinedType = "MOVABLE"
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
ifcopenshell.api.type.assign_type(
self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False
)
assert wall.PredefinedType is None
assert wall.ObjectType is None
before = self.file.to_string()
ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []})
assert self.file.to_string() == before
def test_query_restricts_which_occurrences_are_touched(self):
wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1")
wall_type.PredefinedType = "MOVABLE"
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
wall.PredefinedType = "NOTDEFINED"
column_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcColumnType", name="CT1")
column_type.PredefinedType = "COLUMN"
column = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcColumn")
column.PredefinedType = "NOTDEFINED"
ifcopenshell.api.type.assign_type(
self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False
)
ifcopenshell.api.type.assign_type(
self.file, related_objects=[column], relating_type=column_type, should_map_representations=False
)
wall.PredefinedType = "NOTDEFINED"
column.PredefinedType = "NOTDEFINED"
ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": ["IfcWall"]})
assert wall.PredefinedType is None
assert column.PredefinedType == "NOTDEFINED"
class TestFixDoubleTypedPredefinedTypeIFC2X3(test.bootstrap.IFC2X3):
def test_ifc2x3_occurrence_without_predefined_type_attribute_is_guarded(self):
# IFC2X3 IfcWallStandardCase has no PredefinedType attribute at all;
# only ObjectType may be cleared there.
wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1")
wall_type.PredefinedType = "STANDARD"
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallStandardCase")
wall.ObjectType = "Some legacy value"
assert not hasattr(wall, "PredefinedType")
ifcopenshell.api.type.assign_type(
self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False
)
wall.ObjectType = "Some legacy value"
ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []})
assert wall.ObjectType is None
def test_ifc2x3_userdefined_type_without_elementtype_untouched(self):
wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1")
wall_type.PredefinedType = "USERDEFINED"
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallStandardCase")
wall.ObjectType = "Kept because type has no description"
ifcopenshell.api.type.assign_type(
self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False
)
wall.ObjectType = "Kept because type has no description"
ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []})
assert wall.ObjectType == "Kept because type has no description"