First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
@@ -0,0 +1,44 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
"""Utility functions for extracting IFC data
Data in IFC files is represented using relationships between IFC entities. To
extract data like "what properties does this wall have" involves looping
through these relationships which can be tedious.
This module makes it easy to get commonly requested data from IFC
relationships, such as properties of a wall, what elements are connected to
pipes, dates from work schedules, filtering maintainable elements, and more.
The most commonly used utilities to help you get started are:
- See :mod:`ifcopenshell.util.element` which contains a lot of useful functions
for getting most common relationships on elements.
- See :func:`ifcopenshell.util.element.get_psets` to get all properties of an
entity, like a wall.
- See :func:`ifcopenshell.util.element.get_type` to get the corresponding type
object (e.g. the wall type definition) of a single occurrence (e.g. an
individual wall).
- See :func:`ifcopenshell.util.placement.get_local_placement` to get the XYZ
placement point of a single object.
- See :func:`ifcopenshell.util.unit.calculate_unit_scale` to convert between SI
units and project units.
- See :mod:`ifcopenshell.util.shape` to calculate quantities from processed
geometry.
"""
@@ -0,0 +1,111 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.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/>.
import math
import ifcopenshell
import ifcopenshell.util.unit
def add_linear_placement_fallback_position(file: ifcopenshell.file) -> ifcopenshell.file:
import ifcopenshell.api.alignment
patched_file = ifcopenshell.file.from_string(file.wrapped_data.to_string())
linear_placements = patched_file.by_type("IfcLinearPlacement")
for lp in linear_placements:
ifcopenshell.api.alignment.update_fallback_position(patched_file, lp)
return patched_file
def create_alignment_geometry(file: ifcopenshell.file) -> ifcopenshell.file:
import ifcopenshell.api.alignment
patched_file = ifcopenshell.file.from_string(file.wrapped_data.to_string())
alignments = patched_file.by_type("IfcAlignment")
for alignment in alignments:
ifcopenshell.api.alignment.create_representation(patched_file, alignment)
return patched_file
def append_zero_length_segments(file: ifcopenshell.file) -> ifcopenshell.file:
"""Appends zero length segments to all alignment layouts and layout geometry, if missing."""
import ifcopenshell.api.alignment
patched_file = ifcopenshell.file.from_string(file.wrapped_data.to_string())
alignments = patched_file.by_type("IfcAlignment")
for alignment in alignments:
layouts = ifcopenshell.api.alignment.get_alignment_layouts(alignment)
for layout in layouts:
ifcopenshell.api.alignment.add_zero_length_segment(patched_file, layout, include_referent=False)
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
if curve:
ifcopenshell.api.alignment.add_zero_length_segment(patched_file, curve)
return patched_file
def station_as_string(file: ifcopenshell.file, sta: float):
"""
Returns a stringized version of a station. Example 100.0 is 1+00.00 as a stationing string.
If the project units are SI-based, the string is in the format xxx+yyy.zzz
If the project units are Emperial-based, the string is in the format xx+yy.zz
:param station: the station to be stringized
:return: stringized station
"""
unit_type = ifcopenshell.util.unit.get_project_unit(file, "LENGTHUNIT")
if unit_type.is_a("IfcConversionBasedUnit"):
station = ifcopenshell.util.unit.convert(
sta, from_unit=unit_type.Name, from_prefix=None, to_unit="foot", to_prefix=None
)
plus_seperator = 2
precision = 2
else:
station = ifcopenshell.util.unit.convert(
sta, from_unit=unit_type.Name, from_prefix=unit_type.Prefix, to_unit="meter", to_prefix=None
)
plus_seperator = 3
precision = 3
value = math.fabs(station)
shifter = math.pow(10.0, plus_seperator)
v1 = math.floor(value / shifter)
v2 = value - v1 * shifter
# Check to make sure that v2 is not basically the same as shifter
# If station = 69500.00000, we sometimes get 694+100.00 instead of 695+00.00
if math.isclose(v2 - shifter, 0.0, abs_tol=5.0 * math.pow(10.0, -(precision + 1))):
v2 = 0.0
v1 += 1
v1 = -1 * v1 if station < 0 else v1
station_string = "{:d}+{:0{}.{}f}".format(v1, v2, plus_seperator + precision + 1, precision)
# special case when v1 is 0 and station is negative, the string above doesn't get the leading
# negative sign. this snippet fixes that
if v1 == 0 and station < 0:
station_string = "-" + station_string
return station_string
@@ -0,0 +1,81 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
from typing import Literal, Union
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
PrimitiveType = Literal["entity", "string", "float", "integer", "boolean", "enum", "binary"]
ComplexPrimitiveType = Literal["list", "array", "set"]
PrimitiveTypeOutput = Union[
PrimitiveType,
tuple[ComplexPrimitiveType, "PrimitiveTypeOutput"],
tuple[Literal["select"], tuple["PrimitiveTypeOutput", ...]],
None,
]
def get_primitive_type(
attribute_or_data_type: Union[ifcopenshell_wrapper.attribute, ifcopenshell_wrapper.parameter_type],
) -> PrimitiveTypeOutput:
if isinstance(attribute_or_data_type, ifcopenshell_wrapper.attribute):
data_type = str(attribute_or_data_type.type_of_attribute())
else:
data_type = str(attribute_or_data_type)
if data_type.find("<type") == 0:
return get_primitive_type(data_type[data_type[1:].find("<") + 1 :])
elif data_type.find("<list") == 0:
return ("list", get_primitive_type(data_type[data_type[1:].find("<") + 1 :]))
elif data_type.find("<array") == 0:
return ("array", get_primitive_type(data_type[data_type[1:].find("<") + 1 :]))
elif data_type.find("<set") == 0:
return ("set", get_primitive_type(data_type[data_type[1:].find("<") + 1 :]))
elif data_type.find("<select") == 0:
select_definition = data_type[data_type.find("(") + 1 : data_type.find(")")].split("|")
select_types = [get_primitive_type(d.strip()) for d in select_definition]
return ("select", tuple(select_types))
elif "<entity" in data_type:
return "entity"
elif "<string>" in data_type:
return "string"
elif "<real>" in data_type:
return "float"
elif "<number>" in data_type or "<integer>" in data_type:
return "integer"
elif "<boolean>" in data_type:
return "boolean"
elif "<logical>" in data_type or "<enumeration" in data_type:
return "enum"
elif "<binary" in data_type:
return "binary"
def get_enum_items(attribute: ifcopenshell_wrapper.attribute) -> tuple[str, ...]:
named_type = attribute.type_of_attribute().as_named_type()
assert named_type
enumeration = named_type.declared_type().as_enumeration_type()
assert enumeration
return enumeration.enumeration_items()
def get_select_items(attribute: ifcopenshell_wrapper.attribute) -> tuple[ifcopenshell_wrapper.declaration, ...]:
named_type = attribute.type_of_attribute().as_named_type()
assert named_type
select_type = named_type.declared_type().as_select_type()
assert select_type
return select_type.select_list()
@@ -0,0 +1,251 @@
{
"IfcPerformanceHistory": {
"Identification": "LifeCyclePhase"
},
"IfcProcedure": {
"Identification": "ProcedureID",
"LongDescription": "ProcedureType",
"PredefinedType": "UserDefinedProcedureType"
},
"IfcTask": {
"Identification": "TaskId",
"LongDescription": "Status",
"Status": "WorkMethod",
"WorkMethod": "IsMilestone",
"IsMilestone": "Priority"
},
"IfcWorkPlan": {
"Identification": "Identifier",
"PredefinedType": "WorkControlType"
},
"IfcWorkSchedule": {
"Identification": "Identifier",
"PredefinedType": "WorkControlType"
},
"IfcSpace": {
"PredefinedType": "InteriorOrExteriorSpace"
},
"IfcTransportElement": {
"PredefinedType": "OperationType"
},
"IfcBuildingElementProxy": {
"PredefinedType": "CompositionType"
},
"IfcRamp": {
"PredefinedType": "ShapeType"
},
"IfcRelCoversSpaces": {
"RelatingSpace": "RelatedSpace"
},
"IfcRoof": {
"PredefinedType": "ShapeType"
},
"IfcStair": {
"PredefinedType": "ShapeType"
},
"IfcAsset": {
"Identification": "AssetID"
},
"IfcInventory": {
"PredefinedType": "InventoryType"
},
"IfcActionRequest": {
"Identification": "RequestID"
},
"IfcCostSchedule": {
"Identification": "SubmittedBy",
"PredefinedType": "PreparedBy",
"Status": "SubmittedOn",
"SubmittedOn": "Status",
"UpdateDate": "TargetUsers"
},
"IfcPermit": {
"Identification": "PermitID"
},
"IfcProjectOrder": {
"Identification": "ID"
},
"IfcConstructionEquipmentResource": {
"Identification": "ResourceIdentifier",
"LongDescription": "ResourceGroup",
"Usage": "ResourceConsumption",
"BaseCosts": "BaseQuantity"
},
"IfcConstructionMaterialResource": {
"Identification": "ResourceIdentifier",
"LongDescription": "ResourceGroup",
"Usage": "ResourceConsumption",
"BaseCosts": "BaseQuantity",
"BaseQuantity": "Suppliers",
"PredefinedType": "UsageRatio"
},
"IfcConstructionProductResource": {
"Identification": "ResourceIdentifier",
"LongDescription": "ResourceGroup",
"Usage": "ResourceConsumption",
"BaseCosts": "BaseQuantity"
},
"IfcCrewResource": {
"Identification": "ResourceIdentifier",
"LongDescription": "ResourceGroup",
"Usage": "ResourceConsumption",
"BaseCosts": "BaseQuantity"
},
"IfcLaborResource": {
"Identification": "ResourceIdentifier",
"LongDescription": "ResourceGroup",
"Usage": "ResourceConsumption",
"BaseCosts": "BaseQuantity",
"BaseQuantity": "SkillSet"
},
"IfcSubContractResource": {
"Identification": "ResourceIdentifier",
"LongDescription": "ResourceGroup",
"Usage": "ResourceConsumption",
"BaseCosts": "BaseQuantity",
"BaseQuantity": "SubContractor",
"PredefinedType": "JobDescription"
},
"IfcStructuralLinearAction": {
"ProjectedOrTrue": "CausedBy",
"PredefinedType": "ProjectedOrTrue"
},
"IfcStructuralPlanarAction": {
"ProjectedOrTrue": "CausedBy",
"PredefinedType": "ProjectedOrTrue"
},
"IfcReinforcingBar": {
"PredefinedType": "BarRole"
},
"IfcOrganization": {
"Identification": "Id"
},
"IfcPerson": {
"Identification": "Id"
},
"IfcApproval": {
"Identifier": "Description",
"Name": "ApprovalDateTime",
"Description": "ApprovalStatus",
"TimeOfApproval": "ApprovalLevel",
"Status": "ApprovalQualifier",
"Level": "Name",
"Qualifier": "Identifier"
},
"IfcApprovalRelationship": {
"Name": "RelatedApproval",
"Description": "RelatingApproval",
"RelatingApproval": "Description",
"RelatedApprovals": "Name"
},
"IfcObjective": {
"LogicalAggregator": "ResultValues"
},
"IfcCostValue": {
"Category": "CostType"
},
"IfcCurrencyRelationship": {
"Name": "RelatingMonetaryUnit",
"Description": "RelatedMonetaryUnit",
"RelatingMonetaryUnit": "ExchangeRate",
"RelatedMonetaryUnit": "RateDateTime",
"ExchangeRate": "RateSource"
},
"IfcClassificationReference": {
"Identification": "ItemReference"
},
"IfcDocumentInformation": {
"Identification": "DocumentId",
"Location": "DocumentReferences"
},
"IfcDocumentInformationRelationship": {
"Name": "RelatingDocument",
"Description": "RelatedDocuments",
"RelatingDocument": "RelationshipType"
},
"IfcDocumentReference": {
"Identification": "ItemReference"
},
"IfcLibraryInformation": {
"Location": "LibraryReference"
},
"IfcLibraryReference": {
"Identification": "ItemReference"
},
"IfcMaterialProperties": {
"Properties": "ExtendedProperties"
},
"IfcBlobTexture": {
"Mode": "TextureType",
"Parameter": "RasterFormat",
"RasterFormat": "RasterCode"
},
"IfcExternallyDefinedHatchStyle": {
"Identification": "ItemReference"
},
"IfcExternallyDefinedSurfaceStyle": {
"Identification": "ItemReference"
},
"IfcExternallyDefinedTextFont": {
"Identification": "ItemReference"
},
"IfcImageTexture": {
"Mode": "TextureType",
"Parameter": "UrlReference"
},
"IfcPixelTexture": {
"Mode": "TextureType",
"Parameter": "Width",
"Width": "Height",
"Height": "ColourComponents",
"ColourComponents": "Pixel"
},
"IfcTextureCoordinateGenerator": {
"Maps": "Mode",
"Mode": "Parameter"
},
"IfcTextureMap": {
"Maps": "TextureMaps"
},
"IfcAsymmetricIShapeProfileDef": {
"BottomFlangeWidth": "OverallWidth",
"BottomFlangeThickness": "FlangeThickness",
"BottomFlangeFilletRadius": "FilletRadius",
"BottomFlangeEdgeRadius": "CentreOfGravityInY"
},
"IfcProfileProperties": {
"Name": "ProfileName",
"Description": "ProfileDefinition"
},
"IfcPropertyDependencyRelationship": {
"Name": "DependingProperty",
"Description": "DependantProperty",
"DependingProperty": "Name",
"DependantProperty": "Description"
},
"IfcBoundaryEdgeCondition": {
"TranslationalStiffnessByLengthX": "LinearStiffnessByLengthX",
"TranslationalStiffnessByLengthY": "LinearStiffnessByLengthY",
"TranslationalStiffnessByLengthZ": "LinearStiffnessByLengthZ"
},
"IfcBoundaryFaceCondition": {
"TranslationalStiffnessByAreaX": "LinearStiffnessByAreaX",
"TranslationalStiffnessByAreaY": "LinearStiffnessByAreaY",
"TranslationalStiffnessByAreaZ": "LinearStiffnessByAreaZ"
},
"IfcBoundaryNodeCondition": {
"TranslationalStiffnessX": "LinearStiffnessX",
"TranslationalStiffnessY": "LinearStiffnessY",
"TranslationalStiffnessZ": "LinearStiffnessZ"
},
"IfcBoundaryNodeConditionWarping": {
"TranslationalStiffnessX": "LinearStiffnessX",
"TranslationalStiffnessY": "LinearStiffnessY",
"TranslationalStiffnessZ": "LinearStiffnessZ"
},
"IfcStructuralLoadTemperature": {
"DeltaTConstant": "DeltaT_Constant",
"DeltaTY": "DeltaT_Y",
"DeltaTZ": "DeltaT_Z"
}
}
@@ -0,0 +1,30 @@
{
"IfcClassification": {
"Specification": "Location"
},
"IfcPropertyBoundedValue": {
"Specification": "Description"
},
"IfcPropertyEnumeratedValue": {
"Specification": "Description"
},
"IfcPropertyListValue": {
"Specification": "Description"
},
"IfcPropertyReferenceValue": {
"Specification": "Description"
},
"IfcPropertyTableValue": {
"Specification": "Description"
},
"IfcPropertySingleValue": {
"Specification": "Description"
},
"IfcComplexProperty": {
"Specification": "Description"
},
"IfcWorkTime": {
"StartDate": "Start",
"FinishDate": "Finish"
}
}
@@ -0,0 +1,105 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
import json
import os
from typing import Union
import ifcopenshell
import ifcopenshell.util.classification
import ifcopenshell.util.element
import ifcopenshell.util.system
cwd = os.path.dirname(os.path.realpath(__file__))
ifc4_to_brick_map = {}
with open(os.path.join(cwd, "ifc4_to_brick.json")) as f:
ifc4_to_brick_map = json.load(f)
def get_brick_type(element: ifcopenshell.entity_instance) -> Union[str, None]:
references = ifcopenshell.util.classification.get_references(element)
for reference in references:
system = ifcopenshell.util.classification.get_classification(reference)
if system.Name == "Brick":
return reference.Location
result = None
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
if predefined_type:
result = ifc4_to_brick_map.get(f"{element.is_a()}.{predefined_type}", None)
if not result:
result = ifc4_to_brick_map.get(element.is_a(), None)
if not result:
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
ifc_type_class = element_type.is_a().replace("Type", "")
result = ifc4_to_brick_map.get(f"{ifc_type_class}.{predefined_type}", None)
if not result:
result = ifc4_to_brick_map.get(ifc_type_class, None)
if result:
if result.startswith("http"):
return result
return f"https://brickschema.org/schema/Brick#{result}"
# Generic fallback
if element.is_a("IfcDistributionElement"):
return f"https://brickschema.org/schema/Brick#Equipment"
elif element.is_a("IfcSpatialElement") or element.is_a("IfcSpatialStructureElement"):
return f"https://brickschema.org/schema/Brick#Location"
elif element.is_a("IfcSystem"):
return f"https://brickschema.org/schema/Brick#System"
def get_element_feeds(element: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
current_element = element
processed_elements = set()
downstream_equipment: set[ifcopenshell.entity_instance] = set()
# A queue is a list of branches. A branch is a list of elements in
# sequence, each one connecting to another element. An element in a
# branch may have a child queue. The queue and child queues are
# acyclic.
def extend_branch(element, branch, predecessor=None):
processed_elements.add(element)
branch_element = {"element": element, "children": [], "predecessor": predecessor}
branch.append(branch_element)
connected = {
e
for e in ifcopenshell.util.system.get_connected_to(element, flow_direction="SOURCE")
if e not in processed_elements
}
connected.update(
[
e
for e in ifcopenshell.util.system.get_connected_from(element, flow_direction="SOURCE")
if e not in processed_elements
]
)
for connected_element in connected:
if connected_element.is_a("IfcFlowFitting") or connected_element.is_a("IfcFlowSegment"):
branch_element["children"].append(extend_branch(connected_element, [], element))
else:
downstream_equipment.add(connected_element)
return branch
extended_branch = extend_branch(current_element, [])
return downstream_equipment
@@ -0,0 +1,9 @@
{
"IfcElectricDistributionPoint": "IfcElectricDistributionBoard"
, "IfcAnnotationCurveOccurrence": "IfcStyledItem"
, "IfcAnnotationFillAreaOccurrence": "IfcStyledItem"
, "IfcAnnotationSurfaceOccurrence": "IfcStyledItem"
, "IfcAnnotationSymbolOccurrence": "IfcStyledItem"
, "IfcAnnotationTextOccurrence": "IfcStyledItem"
, "IfcExtendedMaterialProperties": "IfcMaterialProperties"
}
@@ -0,0 +1,213 @@
{
"IfcComplexPropertyTemplate": "",
"IfcProjectLibrary": "",
"IfcPropertySetTemplate": "",
"IfcRelAssignsToGroupByFactor": "",
"IfcRelDeclares": "",
"IfcRelDefinesByObject": "",
"IfcRelDefinesByTemplate": "",
"IfcSimplePropertyTemplate": "",
"IfcEvent": "",
"IfcEventType": "",
"IfcProcedureType": "",
"IfcTaskType": "",
"IfcWorkCalendar": "",
"IfcCivilElement": "",
"IfcCivilElementType": "",
"IfcElementAssemblyType": "",
"IfcExternalSpatialElement": "IfcSpace",
"IfcGeographicElement": "",
"IfcGeographicElementType": "",
"IfcOpeningStandardCase": "",
"IfcRelInterferesElements": "",
"IfcRelSpaceBoundary1stLevel": "",
"IfcRelSpaceBoundary2ndLevel": "",
"IfcSpatialZone": "",
"IfcSpatialZoneType": "",
"IfcBeamStandardCase": "",
"IfcBuildingSystem": "",
"IfcChimney": "",
"IfcChimneyType": "",
"IfcColumnStandardCase": "",
"IfcDoorStandardCase": "",
"IfcDoorType": "",
"IfcMemberStandardCase": "",
"IfcPlateStandardCase": "",
"IfcRampType": "",
"IfcRoofType": "",
"IfcShadingDevice": "",
"IfcShadingDeviceType": "",
"IfcSlabElementedCase": "",
"IfcSlabStandardCase": "",
"IfcStairType": "",
"IfcWallElementedCase": "",
"IfcWindowStandardCase": "",
"IfcWindowType": "",
"IfcDistributionCircuit": "",
"IfcDistributionSystem": "",
"IfcBuildingElementPartType": "",
"IfcFurniture": "",
"IfcSystemFurnitureElement": "",
"IfcActuator": "",
"IfcAlarm": "",
"IfcController": "",
"IfcFlowInstrument": "",
"IfcSensor": "",
"IfcUnitaryControlElement": "",
"IfcUnitaryControlElementType": "",
"IfcConstructionEquipmentResourceType": "",
"IfcConstructionMaterialResourceType": "",
"IfcConstructionProductResourceType": "",
"IfcCrewResourceType": "",
"IfcLaborResourceType": "",
"IfcSubContractResourceType": "",
"IfcAudioVisualAppliance": "",
"IfcAudioVisualApplianceType": "",
"IfcCableCarrierFitting": "",
"IfcCableCarrierSegment": "",
"IfcCableFitting": "",
"IfcCableFittingType": "",
"IfcCableSegment": "",
"IfcCommunicationsAppliance": "",
"IfcCommunicationsApplianceType": "",
"IfcElectricAppliance": "",
"IfcElectricDistributionBoard": "",
"IfcElectricDistributionBoardType": "",
"IfcElectricFlowStorageDevice": "",
"IfcElectricGenerator": "",
"IfcElectricMotor": "",
"IfcElectricTimeControl": "",
"IfcJunctionBox": "",
"IfcLamp": "",
"IfcLightFixture": "",
"IfcMotorConnection": "",
"IfcOutlet": "",
"IfcProtectiveDevice": "",
"IfcProtectiveDeviceTrippingUnit": "",
"IfcProtectiveDeviceTrippingUnitType": "",
"IfcSolarDevice": "",
"IfcSolarDeviceType": "",
"IfcSwitchingDevice": "",
"IfcTransformer": "",
"IfcAirTerminal": "",
"IfcAirTerminalBox": "",
"IfcAirToAirHeatRecovery": "",
"IfcBoiler": "",
"IfcBurner": "",
"IfcBurnerType": "",
"IfcChiller": "",
"IfcCoil": "",
"IfcCompressor": "",
"IfcCondenser": "",
"IfcCooledBeam": "",
"IfcCoolingTower": "",
"IfcDamper": "",
"IfcDuctFitting": "",
"IfcDuctSegment": "",
"IfcDuctSilencer": "",
"IfcEngine": "",
"IfcEngineType": "",
"IfcEvaporativeCooler": "",
"IfcEvaporator": "",
"IfcFan": "",
"IfcFilter": "",
"IfcFlowMeter": "",
"IfcHeatExchanger": "",
"IfcHumidifier": "",
"IfcMedicalDevice": "",
"IfcMedicalDeviceType": "",
"IfcPipeFitting": "",
"IfcPipeSegment": "",
"IfcPump": "",
"IfcSpaceHeater": "",
"IfcTank": "",
"IfcTubeBundle": "",
"IfcUnitaryEquipment": "",
"IfcValve": "",
"IfcVibrationIsolator": "",
"IfcFireSuppressionTerminal": "",
"IfcInterceptor": "",
"IfcInterceptorType": "",
"IfcSanitaryTerminal": "",
"IfcStackTerminal": "",
"IfcWasteTerminal": "",
"IfcStructuralCurveAction": "",
"IfcStructuralCurveReaction": "",
"IfcStructuralLoadCase": "",
"IfcStructuralSurfaceAction": "",
"IfcStructuralSurfaceReaction": "",
"IfcFootingType": "",
"IfcPileType": "",
"IfcReinforcingBarType": "",
"IfcReinforcingMeshType": "",
"IfcSurfaceFeature": "",
"IfcTendonAnchorType": "",
"IfcTendonType": "",
"IfcVoidingFeature": "",
"IfcResourceApprovalRelationship": "",
"IfcAppliedValue": "",
"IfcReference": "",
"IfcResourceConstraintRelationship": "",
"IfcEventTime": "",
"IfcLagTime": "",
"IfcRecurrencePattern": "",
"IfcResourceTime": "",
"IfcTaskTime": "",
"IfcTaskTimeRecurring": "",
"IfcTimePeriod": "",
"IfcWorkTime": "",
"IfcExternalReferenceRelationship": "",
"IfcConnectionVolumeGeometry": "",
"IfcAdvancedBrep": "",
"IfcAdvancedBrepWithVoids": "",
"IfcCartesianPointList3D": "",
"IfcExtrudedAreaSolidTapered": "",
"IfcFixedReferenceSweptAreaSolid": "",
"IfcRevolvedAreaSolidTapered": "",
"IfcSweptDiskSolidPolygonal": "",
"IfcTriangulatedFaceSet": "",
"IfcBoundaryCurve": "",
"IfcBSplineCurveWithKnots": "",
"IfcBSplineSurfaceWithKnots": "",
"IfcCompositeCurveOnSurface": "",
"IfcCurveBoundedSurface": "",
"IfcCylindricalSurface": "",
"IfcOuterBoundaryCurve": "",
"IfcPcurve": "",
"IfcRationalBSplineCurveWithKnots": "",
"IfcRationalBSplineSurfaceWithKnots": "",
"IfcReparametrisedCompositeCurveSegment": "",
"IfcMaterialConstituent": "",
"IfcMaterialConstituentSet": "",
"IfcMaterialLayerWithOffsets": "",
"IfcMaterialProfile": "",
"IfcMaterialProfileSet": "",
"IfcMaterialProfileSetUsage": "",
"IfcMaterialProfileSetUsageTapering": "",
"IfcMaterialProfileWithOffsets": "",
"IfcMaterialRelationship": "",
"IfcConversionBasedUnitWithOffset": "",
"IfcVector": "",
"IfcColourRgbList": "",
"IfcIndexedColourMap": "",
"IfcIndexedTriangleTextureMap": "",
"IfcTextureVertexList": "",
"IfcMirroredProfileDef": "",
"IfcTable": "",
"IfcMapConversion": "",
"IfcProjectedCRS": "",
"IfcStructuralLoadConfiguration": "",
"IfcSurfaceReinforcementArea": "",
"IfcAdvancedFace": "",
"IfcTableColumn": "",
"IfcCartesianPointList2D": "",
"IfcIndexedPolyCurve": "",
"IfcIndexedPolygonalFace": "",
"IfcIndexedPolygonalFaceWithVoids": "",
"IfcPolygonalFaceSet": "",
"IfcIntersectionCurve": "",
"IfcSeamCurve": "",
"IfcSphericalSurface": "",
"IfcSurfaceCurve": "",
"IfcToroidalSurface": ""
}
@@ -0,0 +1,99 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.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/>.
from typing import Optional
import ifcopenshell.util.element
def get_references(element: ifcopenshell.entity_instance, should_inherit=True) -> set[ifcopenshell.entity_instance]:
"""Gets classification references associated with the element
:param should_inherit: If true, classification references are inherited
from the type. Classifications can be overriden per system.
:return: A set of IfcClassificationReference
"""
results = set()
if not element.is_a("IfcRoot"):
if (references := getattr(element, "HasExternalReferences", None)) is not None or (
references := getattr(element, "HasExternalReference", None)
) is not None:
return {r.RelatingReference for r in references}
if should_inherit and element.is_a("IfcObject"):
element_type = ifcopenshell.util.element.get_type(element)
if element_type and element_type != element:
results = get_references(element_type)
occurrence_results = {
r.RelatingClassification
for r in getattr(element, "HasAssociations", [])
if r.is_a("IfcRelAssociatesClassification")
}
if results:
type_references_per_system = {}
occurrence_references_per_system = {}
for result in results:
type_references_per_system.setdefault(get_classification(result), []).append(result)
for result in occurrence_results:
occurrence_references_per_system.setdefault(get_classification(result), []).append(result)
type_references_per_system.update(occurrence_references_per_system)
results = set()
for values in type_references_per_system.values():
[results.add(v) for v in values]
return results
return occurrence_results
def get_classification(reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Get the IfcClassification that a classification reference belongs to
:param reference: An IfcClassificationReference
:return: IfcClassification
"""
if reference.is_a("IfcClassification"):
return reference
return get_classification(reference.ReferencedSource) if reference.ReferencedSource is not None else None
def get_inherited_references(reference: Optional[ifcopenshell.entity_instance]) -> list[ifcopenshell.entity_instance]:
results = []
while True:
if not reference or reference.is_a("IfcClassification"):
break
results.append(reference)
reference = reference.ReferencedSource
return results
def get_classification_data(file: ifcopenshell.file) -> Optional[tuple[list[dict], str]]:
if not file or not file.by_type("IfcClassification"):
return [], ""
classification = file.by_type("IfcClassification")[0]
classification_name = classification.Name
def process_references(reference):
data = reference.get_info()
del data["ReferencedSource"]
data["referenced_source"] = reference.ReferencedSource.id() if reference.ReferencedSource else None
data["has_references"] = bool(reference.HasReferences)
data["references"] = (
[process_references(ref) for ref in reference.HasReferences] if reference.HasReferences else []
)
return data
classification_data = [process_references(reference) for reference in classification.HasReferences]
return classification_data, classification_name
@@ -0,0 +1,111 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult, Yassine Oualid <dion@thinkmoult.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/>.
#
#
from typing import Union
import ifcopenshell
def get_constraints(product: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
"""
Retrieves the constraints assigned to the `product`.
:param product: The IFC element.
:return: List of assigned constraints.
"""
constraints = []
for rel in product.HasAssociations or []:
if rel.is_a("IfcRelAssociatesConstraint"):
constraints.append(rel.RelatingConstraint)
return constraints
def get_constrained_elements(constraint: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
"""
Retrieves the elements constrained by a `constraint`.
:param product: The IFC element.
:return: Set of elements constrained by a `constrant`.
"""
elements = set()
for rel in constraint.file.get_inverse(constraint):
if rel.is_a("IfcRelAssociatesConstraint"):
elements.update(rel.RelatedObjects)
return elements
def get_metrics(constraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
"""
Retrieves the list of nested constraints for a IfcObjective `constraint`.
:param product: IfcObjective constraint.
:return: List of nested constraints.
"""
metrics = []
for metric in constraint.BenchmarkValues or []:
metrics.append(metric)
return metrics
def get_metric_reference(metric: ifcopenshell.entity_instance, is_deep=True):
def get_reference_Attribute(ref, path):
if ref:
if is_deep:
if not path:
path = ref.AttributeIdentifier
else:
path += ".{}".format(ref.AttributeIdentifier) if ref.AttributeIdentifier else ""
return get_reference_Attribute(ref.InnerReference, path)
else:
return ref.AttributeIdentifier
return path
reference = metric.ReferencePath
return get_reference_Attribute(reference, "")
def get_metric_constraints(
resource: ifcopenshell.entity_instance, attribute
) -> Union[list[ifcopenshell.entity_instance], None]:
metrics = []
for constraint in get_constraints(resource) or []:
for metric in get_metrics(constraint) or []:
if bool(
get_metric_reference(metric, is_deep=False) == attribute
or get_metric_reference(metric, is_deep=True) == attribute
):
metrics.append(metric)
if metrics:
return metrics
return None
def is_hard_constraint(metric: ifcopenshell.entity_instance) -> bool:
return metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO"
def is_attribute_locked(product: ifcopenshell.entity_instance, attribute) -> bool:
is_locked = False
metrics = get_metric_constraints(product, attribute)
for metric in metrics or []:
if is_hard_constraint(metric):
is_locked = True
return is_locked
@@ -0,0 +1,436 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
from collections.abc import Generator
from typing import Any, Literal, Optional, Union
import lark
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.util.attribute
import ifcopenshell.util.element
from ifcopenshell.util.doc import get_predefined_type_doc
from ifcopenshell.util.element import get_psets
from ifcopenshell.util.unit import get_unit_symbol
arithmetic_operator_symbols = {"ADD": "+", "DIVIDE": "/", "MULTIPLY": "*", "SUBTRACT": "-"}
symbol_arithmetic_operators = {"+": "ADD", "/": "DIVIDE", "*": "MULTIPLY", "-": "SUBTRACT"}
FILTER_BY_TYPE = Literal["PRODUCT", "RESOURCE", "PROCESS"]
def get_primitive_applied_value(applied_value: Union[ifcopenshell.entity_instance, float, None]) -> float:
if not applied_value:
return 0.0
elif isinstance(applied_value, float):
return applied_value
elif hasattr(applied_value, "wrappedValue") and isinstance(applied_value.wrappedValue, float):
return applied_value.wrappedValue
elif applied_value.is_a("IfcMeasureWithUnit"):
return applied_value.ValueComponent
assert False, f"Applied value {applied_value} not implemented"
def get_total_quantity(root_element: ifcopenshell.entity_instance) -> Union[float, None]:
# 3 IfcPhysicalQuantity Value
if root_element.is_a("IfcCostItem"):
# Different output for no quantities and zero quantites
# as they have different meaning in IFC.
quantities = root_element.CostQuantities
if not quantities:
return None
return sum([q[3] for q in quantities])
elif root_element.is_a("IfcConstructionResource"):
quantity = root_element.BaseQuantity
return quantity[3] if quantity else 1.0
def calculate_applied_value(
root_element: ifcopenshell.entity_instance,
cost_value: ifcopenshell.entity_instance,
category_filter: Optional[str] = None,
) -> float:
if cost_value.ArithmeticOperator and cost_value.Components:
component_values = []
for component in cost_value.Components:
component_values.append(calculate_applied_value(root_element, component, category_filter))
if cost_value.ArithmeticOperator == "ADD":
return sum(component_values)
result = component_values.pop(0)
if cost_value.ArithmeticOperator == "DIVIDE":
for value in component_values:
try:
result /= value
except ZeroDivisionError:
pass
elif cost_value.ArithmeticOperator == "MULTIPLY":
for value in component_values:
result *= value
elif cost_value.ArithmeticOperator == "SUBTRACT":
for value in component_values:
result -= value
return result
if cost_value.Category is None:
return get_primitive_applied_value(cost_value.AppliedValue)
elif cost_value.Category == "*":
if root_element.IsNestedBy:
return sum_child_root_elements(root_element)
else:
return get_primitive_applied_value(cost_value.AppliedValue)
elif cost_value.Category:
if root_element.IsNestedBy:
return sum_child_root_elements(root_element, category_filter=cost_value.Category)
else:
return get_primitive_applied_value(cost_value.AppliedValue)
return 0.0
def sum_child_root_elements(root_element: ifcopenshell.entity_instance, category_filter: Optional[str] = None) -> float:
result = 0.0
for rel in root_element.IsNestedBy:
for child_root_element in rel.RelatedObjects:
if get_assigned_rate_cost_item(child_root_element):
new_child_root_element = get_assigned_rate_cost_item(child_root_element)
else:
new_child_root_element = child_root_element
if root_element.is_a("IfcCostItem"):
values = new_child_root_element.CostValues
elif root_element.is_a("IfcConstructionResource"):
values = child_root_element.BaseCosts
for child_cost_value in values or []:
if category_filter and child_cost_value.Category != category_filter:
continue
child_applied_value = calculate_applied_value(new_child_root_element, child_cost_value)
child_quantity = get_total_quantity(child_root_element)
child_quantity = 1.0 if child_quantity is None else child_quantity
if child_cost_value.UnitBasis:
value_component = child_cost_value.UnitBasis.ValueComponent.wrappedValue
result += child_quantity / value_component * child_applied_value
else:
result += child_quantity * child_applied_value
return result
def serialise_cost_value(cost_value: ifcopenshell.entity_instance) -> str:
result = _serialise_cost_value(cost_value)
if result and result[0] == "(" and result[-1] == ")":
return result[1:-1]
return result
def get_assigned_rate_cost_item(cost_item: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
# same as in tool. Maybe just create one?
for assignment in cost_item.HasAssignments:
if assignment.RelatingControl.is_a() == "IfcCostItem":
return assignment.RelatingControl
def _serialise_cost_value(cost_value: ifcopenshell.entity_instance) -> str:
value = ""
if cost_value.ArithmeticOperator and cost_value.Components:
operator = arithmetic_operator_symbols[cost_value.ArithmeticOperator]
serialised_components = []
for component in cost_value.Components:
serialised_components.append(_serialise_cost_value(component))
value = operator.join(serialised_components)
elif cost_value.AppliedValue is not None:
value = serialise_applied_value(cost_value.AppliedValue)
category = ""
if cost_value.Category == "*":
category = "SUM"
elif cost_value.Category:
category = cost_value.Category
if not category and not value:
value = "0"
if category:
return f"{category}({value})"
elif cost_value.Components:
return f"({value})"
return value
def serialise_applied_value(applied_value: ifcopenshell.entity_instance) -> str:
if applied_value.is_a("IfcMonetaryMeasure"):
return str(applied_value.wrappedValue)
return "?"
def unserialise_cost_value(formula: str, cost_value: ifcopenshell.entity_instance) -> dict[str, Any]:
unserialiser = CostValueUnserialiser()
result = unserialiser.parse(formula)
def map_element_to_result(element: ifcopenshell.entity_instance, result: dict):
result["ifc"] = element
for i, component in enumerate(result.get("Components", [])):
if element.Components and i < len(element.Components):
map_element_to_result(element.Components[i], result["Components"][i])
map_element_to_result(cost_value, result)
return result
def get_cost_items_for_product(product: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
"""
Returns a list of cost items related to the given product.
:param product: An object of class IfcProduct representing a product.
:return: A list of IfcCostItem objects representing the cost items related to the product.
"""
cost_items = []
for assignment in product.HasAssignments:
if assignment.is_a("IfcRelAssignsToControl") and assignment.RelatingControl.is_a("IfcCostItem"):
cost_items.append(assignment.RelatingControl)
return cost_items
def get_root_cost_items(cost_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [
related_object
for rel in cost_schedule.Controls or []
for related_object in rel.RelatedObjects
if related_object.is_a("IfcCostItem")
]
def get_all_nested_cost_items(
cost_item: ifcopenshell.entity_instance,
) -> Generator[ifcopenshell.entity_instance]:
for cost_item in get_nested_cost_items(cost_item):
yield cost_item
yield from get_all_nested_cost_items(cost_item)
def get_nested_cost_items(cost_item: ifcopenshell.entity_instance, is_deep=False) -> list[ifcopenshell.entity_instance]:
if is_deep:
return list(get_all_nested_cost_items(cost_item))
else:
return ifcopenshell.util.element.get_components(cost_item)
def get_schedule_cost_items(
cost_schedule: ifcopenshell.entity_instance,
) -> Generator[ifcopenshell.entity_instance]:
"""Get all cost schedule cost items, including the nested ones."""
for cost_item in get_root_cost_items(cost_schedule):
yield cost_item
yield from get_all_nested_cost_items(cost_item)
def get_cost_assignments_by_type(
cost_item: ifcopenshell.entity_instance, filter_by_type: Optional[FILTER_BY_TYPE] = None
) -> list[ifcopenshell.entity_instance]:
if filter_by_type is not None:
if filter_by_type == "PRODUCT":
filter_by_type = "IfcElement"
elif filter_by_type == "RESOURCE":
filter_by_type = "IfcResource"
elif filter_by_type == "PROCESS":
filter_by_type = "IfcProcess"
return [
related_object
for r in cost_item.Controls or []
for related_object in r.RelatedObjects
if not filter_by_type or related_object.is_a(filter_by_type)
]
def get_cost_item_assignments(
cost_item: ifcopenshell.entity_instance, filter_by_type: Optional[FILTER_BY_TYPE] = None, is_deep: bool = False
) -> list[ifcopenshell.entity_instance]:
if not is_deep:
return get_cost_assignments_by_type(cost_item, filter_by_type)
else:
current_assignments = get_cost_assignments_by_type(cost_item, filter_by_type)
nested_assignments = [
product
for nested_cost_item in get_all_nested_cost_items(cost_item)
for product in get_cost_assignments_by_type(nested_cost_item, filter_by_type)
]
return current_assignments + nested_assignments
def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, str]]:
results = []
for cost_value in cost_item.CostValues or []:
label = "{0:.2f}".format(calculate_applied_value(cost_item, cost_value))
label += " = {}".format(serialise_cost_value(cost_value))
unit_data = {"value_component": None, "unit_component": None, "unit_symbol": ""}
if cost_value.UnitBasis:
data = cost_value.UnitBasis.get_info()
unit_data["value_component"] = data["ValueComponent"].wrappedValue
unit_data["unit_component"] = data["UnitComponent"].id()
unit_data["unit_symbol"] = get_unit_symbol(cost_value.UnitBasis.UnitComponent)
results.append(
{
"id": cost_value.id(),
"label": label,
"name": cost_value.Name,
"category": cost_value.Category,
"applied_value": (
get_primitive_applied_value(cost_value.AppliedValue) if cost_value.AppliedValue else None
),
"unit_data": unit_data,
}
)
return results
def get_cost_schedule_types(file: ifcopenshell.file) -> list[dict[str, str]]:
schema = ifcopenshell_wrapper.schema_by_name(file.schema_identifier)
results: list[dict[str, str]] = []
declaration = schema.declaration_by_name("IfcCostSchedule").as_entity()
assert declaration
version = file.schema_identifier
for attribute in declaration.attributes():
if attribute.name() == "PredefinedType":
for enumeration in ifcopenshell.util.attribute.get_enum_items(attribute):
results.append(
{
"name": enumeration,
"description": get_predefined_type_doc(version, "IfcCostSchedule", enumeration),
}
)
break
return results
def get_product_quantity_names(elements: list[ifcopenshell.entity_instance]) -> list[str]:
names = set()
for element in elements or []:
potential_names = set()
qtos = get_psets(element, qtos_only=True)
for qset, quantities in qtos.items():
potential_names.update(quantities.keys())
names = names.intersection(potential_names) if names else potential_names
return [n for n in names if n != "id"]
def get_cost_schedule(cost_item: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Returns the cost schedule of a cost item."""
for rel in cost_item.HasAssignments or []:
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcCostSchedule"):
return rel.RelatingControl
for rel in cost_item.Nests or []:
return get_cost_schedule(rel.RelatingObject)
def get_cost_rate(
file: ifcopenshell_wrapper.file, cost_item: ifcopenshell.entity_instance
) -> Optional[ifcopenshell.entity_instance]:
"""Returns the cost rate of a cost item."""
# There is no direct relationship between a cost item and a cost rate in IFC, so we need to infer it, based on the assumption that cost_rate.CostValues == cost_item.CostValues.
if get_cost_schedule(cost_item).PredefinedType == "SCHEDULEOFRATES":
return None # Cost item is already a cost rate
if cost_item.CostValues:
potential_rates = file.get_inverse(cost_item.CostValues[0])
for potential_rate in potential_rates:
schedule = get_cost_schedule(potential_rate)
if schedule.PredefinedType == "SCHEDULEOFRATES":
return potential_rate
return None
class CostValueUnserialiser:
def parse(self, formula: str):
l = lark.Lark("""start: formula
formula: operand (operator operand)*
operand: value | category "(" formula ")"
value: NUMBER?
category: WORD?
operator: add | divide | multiply | subtract
add: "+"
divide: "/"
multiply: "*"
subtract: "-"
// Embed common.lark for packaging
DIGIT: "0".."9"
HEXDIGIT: "a".."f"|"A".."F"|DIGIT
INT: DIGIT+
SIGNED_INT: ["+"|"-"] INT
DECIMAL: INT "." INT? | "." INT
_EXP: ("e"|"E") SIGNED_INT
FLOAT: INT _EXP | DECIMAL _EXP?
SIGNED_FLOAT: ["+"|"-"] FLOAT
NUMBER: FLOAT | INT
SIGNED_NUMBER: ["+"|"-"] NUMBER
_STRING_INNER: /.*?/
_STRING_ESC_INNER: _STRING_INNER /(?<!\\\\)(\\\\\\\\)*?/
ESCAPED_STRING : "\\"" _STRING_ESC_INNER "\\""
LCASE_LETTER: "a".."z"
UCASE_LETTER: "A".."Z"
LETTER: UCASE_LETTER | LCASE_LETTER
WORD: LETTER+
CNAME: ("_"|LETTER) ("_"|LETTER|DIGIT)*
WS_INLINE: (" "|/\\t/)+
WS: /[ \\t\\f\\r\\n]/+
CR : /\\r/
LF : /\\n/
NEWLINE: (CR? LF)+
%ignore WS // Disregard spaces in text
""")
start = l.parse(formula)
return self.get_formula(start.children[0])
def get_formula(self, formula):
if len(formula.children) == 1:
return self.get_operand(formula.children[0])
results = {"Components": []}
for child in formula.children:
if child.data == "operand":
results["Components"].append(self.get_operand(child))
elif child.data == "operator":
results["ArithmeticOperator"] = self.get_operator(child)
return results
def get_operand(self, operand):
child = operand.children[0]
if child.data == "value":
value = self.get_value(child)
return {"AppliedValue": float(value) if value else None}
elif child.data == "category":
data = {}
category = self.get_category(child)
if category:
if category.lower() == "sum":
category = "*"
data["Category"] = category
formula = self.get_formula(operand.children[1])
if formula.get("Components"):
data["Components"] = formula["Components"]
data["ArithmeticOperator"] = formula["ArithmeticOperator"]
else:
data["AppliedValue"] = formula["AppliedValue"]
return data
def get_value(self, value):
if value.children:
return value.children[0].value
def get_category(self, category):
if category.children:
return category.children[0].value
def get_operator(self, operator):
return operator.children[0].data.upper()
@@ -0,0 +1,102 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>, @Andrej730
#
# 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/>.
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Union
import numpy as np
import ifcopenshell
from ifcopenshell.util.shape_builder import ShapeBuilder
@dataclass
class Clipping:
location: tuple[float, float, float]
normal: tuple[float, float, float]
type: str = "IfcBooleanClippingResult"
operand_type: str = "IfcHalfSpaceSolid"
@classmethod
def parse(
cls, raw_data: Union[ifcopenshell.entity_instance, Clipping, dict[str, Any]]
) -> Union[ifcopenshell.entity_instance, Clipping]:
"""Parse various formats into a clipping object
`raw_data` can be either:
- IfcBooleanResult IFC entity
- `Clipping` instance
- dictionary to define `Clipping` - either `location` and `normal`
or a `matrix` where XY plane is the clipping boundary and +Z is removed.
`matrix` method will be soon to be deprecated completely.
"""
if isinstance(raw_data, ifcopenshell.entity_instance):
if not raw_data.is_a("IfcBooleanResult"):
raise Exception(f"Provided clipping of unexpected IFC class: {raw_data}")
return raw_data
elif isinstance(raw_data, Clipping):
return raw_data
elif isinstance(raw_data, dict):
if "matrix" in raw_data:
raw_data = raw_data.copy()
matrix = np.array(raw_data["matrix"])[:3]
raw_data["normal"] = matrix[:, 2].tolist()
raw_data["location"] = matrix[:, 3].tolist()
del raw_data["matrix"]
clipping_data = cls(**raw_data)
if clipping_data.type != "IfcBooleanClippingResult":
raise Exception(f'Provided clipping with unexpected result type "{clipping_data.type}"')
if clipping_data.operand_type != "IfcHalfSpaceSolid":
raise Exception(f'Provided clipping with unexpected operand type "{clipping_data.operand_type}"')
return clipping_data
raise Exception(f"Unexpected clipping type provided: {raw_data}")
def apply(
self, ifc_file: Union[ifcopenshell.file, None], first_operand: ifcopenshell.entity_instance, unit_scale: float
) -> ifcopenshell.entity_instance:
"""Applies the clipping data as an IfcBooleanClippingResult to an operand
:param ifc_file: The model to create the entities in
:param first_operand: The representation item to apply the boolean clipping to.
:param unit_scale: The unit scale value to convert from the Clipping's SI units to project units
:return: An IfcBooleanClippingResult which uses an IfcHalfSpaceSolid to clip the first operand
"""
if not ifc_file:
ifc_file = first_operand.file
builder = ShapeBuilder(ifc_file)
normal = np.array(self.normal)
if np.allclose(normal, np.array([0.0, 0.0, 1.0]), atol=1e-2) or np.allclose(
normal, np.array([0.0, 0.0, -1.0]), atol=1e-2
):
arbitrary_vector = np.array([0.0, 1.0, 0.0])
else:
arbitrary_vector = np.array([0.0, 0.0, 1.0])
x_axis = np.cross(normal, arbitrary_vector)
x_axis /= np.linalg.norm(x_axis)
placement = builder.create_axis2_placement_3d([i / unit_scale for i in self.location], self.normal, x_axis)
plane = ifc_file.create_entity("IfcPlane", placement)
second_operand = ifc_file.createIfcHalfSpaceSolid(plane, False)
return ifc_file.createIfcBooleanClippingResult("DIFFERENCE", first_operand, second_operand)
@@ -0,0 +1,270 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
import datetime
from re import findall
from typing import Any, Literal, Union, overload
import isodate
from dateutil import parser
import ifcopenshell
def timedelta2duration(timedelta):
components = {
"days": getattr(timedelta, "days", 0),
"hours": 0,
"minutes": 0,
"seconds": getattr(timedelta, "seconds", 0),
}
if components["seconds"]:
components["hours"], components["minutes"], components["seconds"] = [
int(i) for i in str(datetime.timedelta(seconds=components["seconds"])).split(":")
]
return isodate.Duration(**components)
def ifc2datetime(element: Union[str, int, ifcopenshell.entity_instance]):
if isinstance(element, str):
if "P" in element[0:2]: # IfcDuration
duration = parse_duration(element)
if isinstance(duration, datetime.timedelta):
return timedelta2duration(duration)
return duration
elif len(element) > 3 and element[2] == ":": # IfcTime
return datetime.time.fromisoformat(element)
elif ":" in element: # IfcDateTime
return datetime.datetime.fromisoformat(element)
else: # IfcDate
return datetime.date.fromisoformat(element)
elif isinstance(element, int): # IfcTimeStamp
return datetime.datetime.fromtimestamp(element)
elif isinstance(element, ifcopenshell.entity_instance):
if element.is_a("IfcDateAndTime"):
return datetime.datetime(
element.DateComponent.YearComponent,
element.DateComponent.MonthComponent,
element.DateComponent.DayComponent,
element.TimeComponent.HourComponent,
element.TimeComponent.MinuteComponent,
int(element.TimeComponent.SecondComponent),
# TODO: implement TimeComponent timezone
)
elif element.is_a("IfcCalendarDate"):
return datetime.date(
element.YearComponent,
element.MonthComponent,
element.DayComponent,
)
def readable_ifc_duration(duration: str) -> str:
"""Convert ISO duration to more readable string format.
Examples:
- "P2Y3M1W4DT5H45M30S" -> "2 Y 3 M 1 W 4 D 5 h 45 m 30 s"
- "P2Y3MT30S" -> "2 Y 3 M 30 s"
- "PT2500H" -> "2500 h" (hours are not converted to days)
"""
# NOTE: we don't use isodate.parseduration as it's going to
# represent "PT2500H" as "12w 6d 4h", though user may want
# intentionally to use just hours.
if "T" in duration:
period_duration, time_duration = duration.split("T")
period_duration = period_duration[1:]
else:
period_duration = duration[1:]
time_duration = ""
result: list[str] = []
for designator in ("Y", "M", "W", "D"):
if designator in period_duration:
value, period_duration = period_duration.split(designator)
if float(value):
result.append(f"{value}{designator}")
if time_duration:
for designator in ("H", "M", "S"):
if designator in time_duration:
value, time_duration = time_duration.split(designator)
if float(value):
result.append(f"{value}{designator.lower()}")
return " ".join(result)
@overload
def datetime2ifc(dt: None, ifc_type: Any) -> None: ...
@overload
def datetime2ifc(
dt: Union[datetime.date, str, None],
ifc_type: Literal[
"IfcDuration",
"IfcTimeStamp",
"IfcDateTime",
"IfcDate",
"IfcTime",
"IfcCalendarDate",
"IfcLocalTime",
],
) -> Union[int, str, dict[str, Any], None]: ...
def datetime2ifc(
dt: Union[datetime.date, str, None],
ifc_type: Literal[
"IfcDuration",
"IfcTimeStamp",
"IfcDateTime",
"IfcDate",
"IfcTime",
"IfcCalendarDate",
"IfcLocalTime",
],
) -> Union[int, str, dict[str, Any], None]:
if isinstance(dt, str):
if ifc_type == "IfcDuration":
return dt
try:
dt = datetime.datetime.fromisoformat(dt)
except:
dt = datetime.time.fromisoformat(dt)
elif dt is None:
return
if ifc_type == "IfcDuration":
return isodate.duration_isoformat(dt)
elif ifc_type == "IfcTimeStamp":
return int(dt.timestamp())
elif ifc_type == "IfcDateTime":
if isinstance(dt, datetime.datetime):
return dt.isoformat()
elif isinstance(dt, datetime.date):
return datetime.datetime.combine(dt, datetime.datetime.min.time()).isoformat()
elif ifc_type == "IfcDate":
if isinstance(dt, datetime.datetime):
return dt.date().isoformat()
elif isinstance(dt, datetime.date):
return dt.isoformat()
elif ifc_type == "IfcTime":
if isinstance(dt, datetime.datetime):
return dt.time().isoformat()
elif isinstance(dt, datetime.time):
return dt.isoformat()
elif ifc_type == "IfcCalendarDate":
return {
"DayComponent": dt.day,
"MonthComponent": dt.month,
"YearComponent": dt.year,
}
elif ifc_type == "IfcLocalTime":
# TODO implement timezones
return {
"HourComponent": dt.hour,
"MinuteComponent": dt.minute,
"SecondComponent": dt.second,
}
raise TypeError(f"Unsupported ifc_type for conversion from datetime.datetime = {ifc_type}, value = {dt}")
def string_to_date(string):
if not string:
return None
try:
return parser.isoparse(string)
except:
try:
return parser.parse(string, dayfirst=True, fuzzy=True)
except:
return None
def string_to_duration(duration_string):
# TODO support years, months, weeks aswell
days = 0
hours = 0
minutes = 0
seconds = 0
match = findall(r"(\d+\.?\d*)d", duration_string)
if match:
days = float(match[0])
match = findall(r"(\d+\.?\d*)h", duration_string)
if match:
hours = float(match[0])
match = findall(r"(\d+\.?\d*)m", duration_string)
if match:
minutes = float(match[0])
match = findall(r"(\d+\.?\d*)s", duration_string)
if match:
seconds = float(match[0])
return isodate.duration_isoformat(datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds))
def parse_duration(value: Union[str, None]) -> Union[datetime.timedelta, None]:
if not value:
return None
if isinstance(value, str):
if "P" in value:
try:
return isodate.parse_duration(value)
except:
print("Error parsing ISO string duration")
return None
else:
try:
final_string = "P"
value_upper = value.upper()
for char in value_upper:
if char.isdigit():
final_string += char
elif char == "D":
final_string += "D"
if "H" in value_upper or "S" in value_upper or "MIN" in value_upper:
final_string += "T"
elif char == "W":
final_string += "W"
elif char == "M":
final_string += "M"
elif char == "Y":
final_string += "Y"
elif char == "H":
final_string = (
final_string[:1] + "T" + final_string[1:] if "T" not in final_string else final_string
)
final_string += "H"
elif char == "M":
if "MIN" in value_upper and "T" not in final_string:
final_string = final_string[:1] + "T" + final_string[1:]
final_string += "M"
elif char == "S":
final_string = (
final_string[:1] + "T" + final_string[1:] if "T" not in final_string else final_string
)
final_string += "S"
return isodate.parse_duration(final_string)
except:
print("error fuzzy parsing duration")
return None
def canonicalise_time(time: Union[datetime.datetime, None]) -> str:
if not time:
return "-"
return time.strftime("%d/%m/%y")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,424 @@
{
"IfcBeam": [
"IfcBeamType"
],
"IfcBuilding": [
"IfcSpaceType"
],
"IfcBuildingElement": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcBuildingElementComponent": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcBuildingElementPart": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcBuildingElementProxy": [
"IfcBuildingElementProxyType"
],
"IfcBuildingStorey": [
"IfcSpaceType"
],
"IfcChamferEdgeFeature": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcColumn": [
"IfcColumnType"
],
"IfcCovering": [
"IfcCoveringType"
],
"IfcCurtainWall": [
"IfcCurtainWallType"
],
"IfcDiscreteAccessory": [
"IfcDiscreteAccessoryType",
"IfcVibrationIsolatorType"
],
"IfcDistributionChamberElement": [
"IfcDistributionChamberElementType"
],
"IfcDistributionControlElement": [
"IfcActuatorType",
"IfcAlarmType",
"IfcControllerType",
"IfcFlowInstrumentType",
"IfcSensorType"
],
"IfcDistributionElement": [
"IfcDistributionElementType"
],
"IfcDistributionFlowElement": [
"IfcDistributionChamberElementType"
],
"IfcDoor": [
"IfcDoorStyle"
],
"IfcEdgeFeature": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcElectricDistributionPoint": [
"IfcAirTerminalBoxType",
"IfcDamperType",
"IfcElectricTimeControlType",
"IfcFlowMeterType",
"IfcProtectiveDeviceType",
"IfcSwitchingDeviceType",
"IfcValveType"
],
"IfcElectricalElement": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcElement": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcElementAssembly": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcElementComponent": [
"IfcDiscreteAccessoryType",
"IfcFastenerType"
],
"IfcEnergyConversionDevice": [
"IfcAirToAirHeatRecoveryType",
"IfcBoilerType",
"IfcChillerType",
"IfcCoilType",
"IfcCondenserType",
"IfcCooledBeamType",
"IfcCoolingTowerType",
"IfcElectricGeneratorType",
"IfcElectricMotorType",
"IfcEvaporativeCoolerType",
"IfcEvaporatorType",
"IfcHeatExchangerType",
"IfcHumidifierType",
"IfcMotorConnectionType",
"IfcSpaceHeaterType",
"IfcTransformerType",
"IfcTubeBundleType",
"IfcUnitaryEquipmentType"
],
"IfcEquipmentElement": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcFastener": [
"IfcFastenerType",
"IfcMechanicalFastenerType"
],
"IfcFeatureElement": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcFeatureElementAddition": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcFeatureElementSubtraction": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcFlowController": [
"IfcAirTerminalBoxType",
"IfcDamperType",
"IfcElectricTimeControlType",
"IfcFlowMeterType",
"IfcProtectiveDeviceType",
"IfcSwitchingDeviceType",
"IfcValveType"
],
"IfcFlowFitting": [
"IfcCableCarrierFittingType",
"IfcDuctFittingType",
"IfcJunctionBoxType",
"IfcPipeFittingType"
],
"IfcFlowMovingDevice": [
"IfcCompressorType",
"IfcFanType",
"IfcPumpType"
],
"IfcFlowSegment": [
"IfcCableCarrierSegmentType",
"IfcCableSegmentType",
"IfcDuctSegmentType",
"IfcPipeSegmentType"
],
"IfcFlowStorageDevice": [
"IfcElectricFlowStorageDeviceType",
"IfcTankType"
],
"IfcFlowTerminal": [
"IfcAirTerminalType",
"IfcElectricApplianceType",
"IfcElectricHeaterType",
"IfcFireSuppressionTerminalType",
"IfcGasTerminalType",
"IfcLampType",
"IfcLightFixtureType",
"IfcOutletType",
"IfcSanitaryTerminalType",
"IfcStackTerminalType",
"IfcWasteTerminalType"
],
"IfcFlowTreatmentDevice": [
"IfcDuctSilencerType",
"IfcFilterType"
],
"IfcFooting": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcFurnishingElement": [
"IfcFurnishingElementType",
"IfcFurnitureType",
"IfcSystemFurnitureElementType"
],
"IfcMechanicalFastener": [
"IfcMechanicalFastenerType"
],
"IfcMember": [
"IfcMemberType"
],
"IfcOpeningElement": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcPile": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcPlate": [
"IfcPlateType"
],
"IfcProjectionElement": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcRailing": [
"IfcRailingType"
],
"IfcRamp": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcRampFlight": [
"IfcRampFlightType"
],
"IfcReinforcingBar": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcReinforcingElement": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcReinforcingMesh": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcRoof": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcRoundedEdgeFeature": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcSite": [
"IfcSpaceType"
],
"IfcSlab": [
"IfcSlabType"
],
"IfcSpace": [
"IfcSpaceType"
],
"IfcSpatialStructureElement": [
"IfcSpaceType"
],
"IfcStair": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcStairFlight": [
"IfcStairFlightType"
],
"IfcTendon": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcTendonAnchor": [
"IfcBeamType",
"IfcBuildingElementProxyType",
"IfcColumnType",
"IfcCoveringType",
"IfcCurtainWallType",
"IfcMemberType",
"IfcPlateType",
"IfcRailingType",
"IfcRampFlightType",
"IfcSlabType",
"IfcStairFlightType",
"IfcWallType"
],
"IfcTransportElement": [
"IfcTransportElementType"
],
"IfcVirtualElement": [
"IfcDistributionElementType",
"IfcFurnishingElementType",
"IfcTransportElementType"
],
"IfcWall": [
"IfcWallType"
],
"IfcWallStandardCase": [
"IfcWallType"
],
"IfcWindow": [
"IfcWindowStyle"
]
}
@@ -0,0 +1,384 @@
{
"IfcActuator": [
"IfcActuatorType"
],
"IfcAirTerminal": [
"IfcAirTerminalType"
],
"IfcAirTerminalBox": [
"IfcAirTerminalBoxType"
],
"IfcAirToAirHeatRecovery": [
"IfcAirToAirHeatRecoveryType"
],
"IfcAlarm": [
"IfcAlarmType"
],
"IfcAudioVisualAppliance": [
"IfcAudioVisualApplianceType"
],
"IfcBeam": [
"IfcBeamType"
],
"IfcBeamStandardCase": [
"IfcBeamType"
],
"IfcBoiler": [
"IfcBoilerType"
],
"IfcBuildingElementPart": [
"IfcBuildingElementPartType"
],
"IfcBuildingElementProxy": [
"IfcBuildingElementProxyType"
],
"IfcBurner": [
"IfcBurnerType"
],
"IfcCableCarrierFitting": [
"IfcCableCarrierFittingType"
],
"IfcCableCarrierSegment": [
"IfcCableCarrierSegmentType"
],
"IfcCableFitting": [
"IfcCableFittingType"
],
"IfcCableSegment": [
"IfcCableSegmentType"
],
"IfcChiller": [
"IfcChillerType"
],
"IfcChimney": [
"IfcChimneyType"
],
"IfcCivilElement": [
"IfcCivilElementType"
],
"IfcCoil": [
"IfcCoilType"
],
"IfcColumn": [
"IfcColumnType"
],
"IfcColumnStandardCase": [
"IfcColumnType"
],
"IfcCommunicationsAppliance": [
"IfcCommunicationsApplianceType"
],
"IfcCompressor": [
"IfcCompressorType"
],
"IfcCondenser": [
"IfcCondenserType"
],
"IfcController": [
"IfcControllerType"
],
"IfcCooledBeam": [
"IfcCooledBeamType"
],
"IfcCoolingTower": [
"IfcCoolingTowerType"
],
"IfcCovering": [
"IfcCoveringType"
],
"IfcCurtainWall": [
"IfcCurtainWallType"
],
"IfcDamper": [
"IfcDamperType"
],
"IfcDiscreteAccessory": [
"IfcDiscreteAccessoryType"
],
"IfcDistributionChamberElement": [
"IfcDistributionChamberElementType"
],
"IfcDistributionControlElement": [
"IfcDistributionElementType"
],
"IfcDistributionElement": [
"IfcDistributionElementType"
],
"IfcDistributionFlowElement": [
"IfcDistributionElementType"
],
"IfcDoor": [
"IfcDoorType",
"IfcDoorStyle"
],
"IfcDoorStandardCase": [
"IfcDoorType",
"IfcDoorStyle"
],
"IfcDuctFitting": [
"IfcDuctFittingType"
],
"IfcDuctSegment": [
"IfcDuctSegmentType"
],
"IfcDuctSilencer": [
"IfcDuctSilencerType"
],
"IfcElectricAppliance": [
"IfcElectricApplianceType"
],
"IfcElectricDistributionBoard": [
"IfcElectricDistributionBoardType"
],
"IfcElectricFlowStorageDevice": [
"IfcElectricFlowStorageDeviceType"
],
"IfcElectricGenerator": [
"IfcElectricGeneratorType"
],
"IfcElectricMotor": [
"IfcElectricMotorType"
],
"IfcElectricTimeControl": [
"IfcElectricTimeControlType"
],
"IfcElementAssembly": [
"IfcElementAssemblyType"
],
"IfcEnergyConversionDevice": [
"IfcDistributionElementType"
],
"IfcEngine": [
"IfcEngineType"
],
"IfcEvaporativeCooler": [
"IfcEvaporativeCoolerType"
],
"IfcEvaporator": [
"IfcEvaporatorType"
],
"IfcFan": [
"IfcFanType"
],
"IfcFastener": [
"IfcFastenerType"
],
"IfcFilter": [
"IfcFilterType"
],
"IfcFireSuppressionTerminal": [
"IfcFireSuppressionTerminalType"
],
"IfcFlowController": [
"IfcDistributionElementType"
],
"IfcFlowFitting": [
"IfcDistributionElementType"
],
"IfcFlowInstrument": [
"IfcFlowInstrumentType"
],
"IfcFlowMeter": [
"IfcFlowMeterType"
],
"IfcFlowMovingDevice": [
"IfcDistributionElementType"
],
"IfcFlowSegment": [
"IfcDistributionElementType"
],
"IfcFlowStorageDevice": [
"IfcDistributionElementType"
],
"IfcFlowTerminal": [
"IfcDistributionElementType"
],
"IfcFlowTreatmentDevice": [
"IfcDistributionElementType"
],
"IfcFooting": [
"IfcFootingType"
],
"IfcFurnishingElement": [
"IfcFurnishingElementType"
],
"IfcFurniture": [
"IfcFurnitureType"
],
"IfcGeographicElement": [
"IfcGeographicElementType"
],
"IfcHeatExchanger": [
"IfcHeatExchangerType"
],
"IfcHumidifier": [
"IfcHumidifierType"
],
"IfcInterceptor": [
"IfcInterceptorType"
],
"IfcJunctionBox": [
"IfcJunctionBoxType"
],
"IfcLamp": [
"IfcLampType"
],
"IfcLightFixture": [
"IfcLightFixtureType"
],
"IfcMechanicalFastener": [
"IfcMechanicalFastenerType"
],
"IfcMedicalDevice": [
"IfcMedicalDeviceType"
],
"IfcMember": [
"IfcMemberType"
],
"IfcMemberStandardCase": [
"IfcMemberType"
],
"IfcMotorConnection": [
"IfcMotorConnectionType"
],
"IfcOutlet": [
"IfcOutletType"
],
"IfcPile": [
"IfcPileType"
],
"IfcPipeFitting": [
"IfcPipeFittingType"
],
"IfcPipeSegment": [
"IfcPipeSegmentType"
],
"IfcPlate": [
"IfcPlateType"
],
"IfcPlateStandardCase": [
"IfcPlateType"
],
"IfcProtectiveDevice": [
"IfcProtectiveDeviceType"
],
"IfcProtectiveDeviceTrippingUnit": [
"IfcProtectiveDeviceTrippingUnitType"
],
"IfcPump": [
"IfcPumpType"
],
"IfcRailing": [
"IfcRailingType"
],
"IfcRamp": [
"IfcRampType"
],
"IfcRampFlight": [
"IfcRampFlightType"
],
"IfcReinforcingBar": [
"IfcReinforcingBarType"
],
"IfcReinforcingMesh": [
"IfcReinforcingMeshType"
],
"IfcRoof": [
"IfcRoofType"
],
"IfcSanitaryTerminal": [
"IfcSanitaryTerminalType"
],
"IfcSensor": [
"IfcSensorType"
],
"IfcShadingDevice": [
"IfcShadingDeviceType"
],
"IfcSlab": [
"IfcSlabType"
],
"IfcSlabElementedCase": [
"IfcSlabType"
],
"IfcSlabStandardCase": [
"IfcSlabType"
],
"IfcSolarDevice": [
"IfcSolarDeviceType"
],
"IfcSpace": [
"IfcSpaceType"
],
"IfcSpaceHeater": [
"IfcSpaceHeaterType"
],
"IfcSpatialZone": [
"IfcSpatialZoneType"
],
"IfcStackTerminal": [
"IfcStackTerminalType"
],
"IfcStair": [
"IfcStairType"
],
"IfcStairFlight": [
"IfcStairFlightType"
],
"IfcSwitchingDevice": [
"IfcSwitchingDeviceType"
],
"IfcSystemFurnitureElement": [
"IfcSystemFurnitureElementType"
],
"IfcTank": [
"IfcTankType"
],
"IfcTendon": [
"IfcTendonType"
],
"IfcTendonAnchor": [
"IfcTendonAnchorType"
],
"IfcTransformer": [
"IfcTransformerType"
],
"IfcTransportElement": [
"IfcTransportElementType"
],
"IfcTubeBundle": [
"IfcTubeBundleType"
],
"IfcUnitaryControlElement": [
"IfcUnitaryControlElementType"
],
"IfcUnitaryEquipment": [
"IfcUnitaryEquipmentType"
],
"IfcValve": [
"IfcValveType"
],
"IfcVibrationIsolator": [
"IfcVibrationIsolatorType"
],
"IfcWall": [
"IfcWallType"
],
"IfcWallElementedCase": [
"IfcWallType"
],
"IfcWallStandardCase": [
"IfcWallType"
],
"IfcWasteTerminal": [
"IfcWasteTerminalType"
],
"IfcWindow": [
"IfcWindowType",
"IfcWindowStyle"
],
"IfcWindowStandardCase": [
"IfcWindowType",
"IfcWindowStyle"
]
}
@@ -0,0 +1,428 @@
{
"IfcActuator": [
"IfcActuatorType"
],
"IfcAirTerminal": [
"IfcAirTerminalType"
],
"IfcAirTerminalBox": [
"IfcAirTerminalBoxType"
],
"IfcAirToAirHeatRecovery": [
"IfcAirToAirHeatRecoveryType"
],
"IfcAlarm": [
"IfcAlarmType"
],
"IfcAudioVisualAppliance": [
"IfcAudioVisualApplianceType"
],
"IfcBeam": [
"IfcBeamType"
],
"IfcBearing": [
"IfcBearingType"
],
"IfcBoiler": [
"IfcBoilerType"
],
"IfcBuildingElementPart": [
"IfcBuildingElementPartType"
],
"IfcBuildingElementProxy": [
"IfcBuildingElementProxyType"
],
"IfcBuiltElement": [
"IfcBuiltElementType"
],
"IfcBurner": [
"IfcBurnerType"
],
"IfcCableCarrierFitting": [
"IfcCableCarrierFittingType"
],
"IfcCableCarrierSegment": [
"IfcCableCarrierSegmentType"
],
"IfcCableFitting": [
"IfcCableFittingType"
],
"IfcCableSegment": [
"IfcCableSegmentType"
],
"IfcCaissonFoundation": [
"IfcCaissonFoundationType"
],
"IfcChiller": [
"IfcChillerType"
],
"IfcChimney": [
"IfcChimneyType"
],
"IfcCivilElement": [
"IfcCivilElementType"
],
"IfcCoil": [
"IfcCoilType"
],
"IfcColumn": [
"IfcColumnType"
],
"IfcCommunicationsAppliance": [
"IfcCommunicationsApplianceType"
],
"IfcCompressor": [
"IfcCompressorType"
],
"IfcCondenser": [
"IfcCondenserType"
],
"IfcController": [
"IfcControllerType"
],
"IfcConveyorSegment": [
"IfcConveyorSegmentType"
],
"IfcCooledBeam": [
"IfcCooledBeamType"
],
"IfcCoolingTower": [
"IfcCoolingTowerType"
],
"IfcCourse": [
"IfcCourseType"
],
"IfcCovering": [
"IfcCoveringType"
],
"IfcCurtainWall": [
"IfcCurtainWallType"
],
"IfcDamper": [
"IfcDamperType"
],
"IfcDeepFoundation": [
"IfcDeepFoundationType"
],
"IfcDiscreteAccessory": [
"IfcDiscreteAccessoryType"
],
"IfcDistributionBoard": [
"IfcDistributionBoardType"
],
"IfcDistributionChamberElement": [
"IfcDistributionChamberElementType"
],
"IfcDistributionControlElement": [
"IfcDistributionElementType"
],
"IfcDistributionElement": [
"IfcDistributionElementType"
],
"IfcDistributionFlowElement": [
"IfcDistributionElementType"
],
"IfcDoor": [
"IfcDoorType"
],
"IfcDuctFitting": [
"IfcDuctFittingType"
],
"IfcDuctSegment": [
"IfcDuctSegmentType"
],
"IfcDuctSilencer": [
"IfcDuctSilencerType"
],
"IfcEarthworksElement": [
"IfcBuiltElementType"
],
"IfcEarthworksFill": [
"IfcBuiltElementType"
],
"IfcElectricAppliance": [
"IfcElectricApplianceType"
],
"IfcElectricDistributionBoard": [
"IfcElectricDistributionBoardType"
],
"IfcElectricFlowStorageDevice": [
"IfcElectricFlowStorageDeviceType"
],
"IfcElectricFlowTreatmentDevice": [
"IfcElectricFlowTreatmentDeviceType"
],
"IfcElectricGenerator": [
"IfcElectricGeneratorType"
],
"IfcElectricMotor": [
"IfcElectricMotorType"
],
"IfcElectricTimeControl": [
"IfcElectricTimeControlType"
],
"IfcElementAssembly": [
"IfcElementAssemblyType"
],
"IfcEnergyConversionDevice": [
"IfcDistributionElementType"
],
"IfcEngine": [
"IfcEngineType"
],
"IfcEvaporativeCooler": [
"IfcEvaporativeCoolerType"
],
"IfcEvaporator": [
"IfcEvaporatorType"
],
"IfcFan": [
"IfcFanType"
],
"IfcFastener": [
"IfcFastenerType"
],
"IfcFilter": [
"IfcFilterType"
],
"IfcFireSuppressionTerminal": [
"IfcFireSuppressionTerminalType"
],
"IfcFlowController": [
"IfcDistributionElementType"
],
"IfcFlowFitting": [
"IfcDistributionElementType"
],
"IfcFlowInstrument": [
"IfcFlowInstrumentType"
],
"IfcFlowMeter": [
"IfcFlowMeterType"
],
"IfcFlowMovingDevice": [
"IfcDistributionElementType"
],
"IfcFlowSegment": [
"IfcDistributionElementType"
],
"IfcFlowStorageDevice": [
"IfcDistributionElementType"
],
"IfcFlowTerminal": [
"IfcDistributionElementType"
],
"IfcFlowTreatmentDevice": [
"IfcDistributionElementType"
],
"IfcFooting": [
"IfcFootingType"
],
"IfcFurnishingElement": [
"IfcFurnishingElementType"
],
"IfcFurniture": [
"IfcFurnitureType"
],
"IfcGeographicElement": [
"IfcGeographicElementType"
],
"IfcHeatExchanger": [
"IfcHeatExchangerType"
],
"IfcHumidifier": [
"IfcHumidifierType"
],
"IfcImpactProtectionDevice": [
"IfcImpactProtectionDeviceType"
],
"IfcInterceptor": [
"IfcInterceptorType"
],
"IfcJunctionBox": [
"IfcJunctionBoxType"
],
"IfcKerb": [
"IfcKerbType"
],
"IfcLamp": [
"IfcLampType"
],
"IfcLightFixture": [
"IfcLightFixtureType"
],
"IfcLiquidTerminal": [
"IfcLiquidTerminalType"
],
"IfcMechanicalFastener": [
"IfcMechanicalFastenerType"
],
"IfcMedicalDevice": [
"IfcMedicalDeviceType"
],
"IfcMember": [
"IfcMemberType"
],
"IfcMobileTelecommunicationsAppliance": [
"IfcMobileTelecommunicationsApplianceType"
],
"IfcMooringDevice": [
"IfcMooringDeviceType"
],
"IfcMotorConnection": [
"IfcMotorConnectionType"
],
"IfcNavigationElement": [
"IfcNavigationElementType"
],
"IfcOutlet": [
"IfcOutletType"
],
"IfcPavement": [
"IfcPavementType"
],
"IfcPile": [
"IfcPileType"
],
"IfcPipeFitting": [
"IfcPipeFittingType"
],
"IfcPipeSegment": [
"IfcPipeSegmentType"
],
"IfcPlate": [
"IfcPlateType"
],
"IfcProtectiveDevice": [
"IfcProtectiveDeviceType"
],
"IfcProtectiveDeviceTrippingUnit": [
"IfcProtectiveDeviceTrippingUnitType"
],
"IfcPump": [
"IfcPumpType"
],
"IfcRail": [
"IfcRailType"
],
"IfcRailing": [
"IfcRailingType"
],
"IfcRamp": [
"IfcRampType"
],
"IfcRampFlight": [
"IfcRampFlightType"
],
"IfcReinforcedSoil": [
"IfcBuiltElementType"
],
"IfcReinforcingBar": [
"IfcReinforcingBarType"
],
"IfcReinforcingMesh": [
"IfcReinforcingMeshType"
],
"IfcRoof": [
"IfcRoofType"
],
"IfcSanitaryTerminal": [
"IfcSanitaryTerminalType"
],
"IfcSensor": [
"IfcSensorType"
],
"IfcShadingDevice": [
"IfcShadingDeviceType"
],
"IfcSign": [
"IfcSignType"
],
"IfcSignal": [
"IfcSignalType"
],
"IfcSlab": [
"IfcSlabType"
],
"IfcSolarDevice": [
"IfcSolarDeviceType"
],
"IfcSpace": [
"IfcSpaceType"
],
"IfcSpaceHeater": [
"IfcSpaceHeaterType"
],
"IfcSpatialZone": [
"IfcSpatialZoneType"
],
"IfcStackTerminal": [
"IfcStackTerminalType"
],
"IfcStair": [
"IfcStairType"
],
"IfcStairFlight": [
"IfcStairFlightType"
],
"IfcSwitchingDevice": [
"IfcSwitchingDeviceType"
],
"IfcSystemFurnitureElement": [
"IfcSystemFurnitureElementType"
],
"IfcTank": [
"IfcTankType"
],
"IfcTendon": [
"IfcTendonType"
],
"IfcTendonAnchor": [
"IfcTendonAnchorType"
],
"IfcTendonConduit": [
"IfcTendonConduitType"
],
"IfcTrackElement": [
"IfcTrackElementType"
],
"IfcTransformer": [
"IfcTransformerType"
],
"IfcTransportElement": [
"IfcTransportElementType"
],
"IfcTubeBundle": [
"IfcTubeBundleType"
],
"IfcUnitaryControlElement": [
"IfcUnitaryControlElementType"
],
"IfcUnitaryEquipment": [
"IfcUnitaryEquipmentType"
],
"IfcValve": [
"IfcValveType"
],
"IfcVehicle": [
"IfcVehicleType"
],
"IfcVibrationDamper": [
"IfcVibrationDamperType"
],
"IfcVibrationIsolator": [
"IfcVibrationIsolatorType"
],
"IfcWall": [
"IfcWallType"
],
"IfcWallStandardCase": [
"IfcWallType"
],
"IfcWasteTerminal": [
"IfcWasteTerminalType"
],
"IfcWindow": [
"IfcWindowType"
]
}
@@ -0,0 +1,116 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
import zipfile
from typing import IO, TypedDict, Union
from typing_extensions import NotRequired
class HeaderMetadata(TypedDict):
name: NotRequired[str]
# FILE_DESCRIPTION, not the description from FILE_NAME.
description: NotRequired[str]
implementation_level: NotRequired[str]
time_stamp: NotRequired[str]
schema_name: NotRequired[str]
class IfcHeaderExtractor:
"""An utility class for extracting header information from IFC files.
This class provides functionality to extract key metadata from the header section of
IFC files without recreating the entire file as `ifcopenshell.file`.
For optimization, extractor will search only for the first 50 lines
of the IFC file for metadata.
Supported formats: .ifc, .ifczip.
Metadata available by the extraction is presented in the example below.
Example:
.. code:: python
from ifcopenshell.util.file import IfcHeaderExtractor
extractor = IfcHeaderExtractor("path/to/your/file.ifc")
# Get dictionary of the extracted metadata.
header_info = extractor.extract()
# Print the extracted information
# ViewDefinition[DesignTransferView]
print("File Description:", header_info.get("description"))
# 2;1
print("Implementation Level:", header_info.get("implementation_level"))
# file.ifc
print("File Name:", header_info.get("name"))
# 2024-06-25T15:48:10+05:00
print("Time Stamp:", header_info.get("time_stamp"))
# IFC4X3_ADD2
print("Schema Name:", header_info.get("schema_name"))
"""
def __init__(self, filepath: str):
self.filepath = filepath
def extract(self) -> HeaderMetadata:
extension = self.filepath.split(".")[-1]
if extension.lower() == "ifc":
with open(self.filepath) as ifc_file:
return self.extract_ifc_spf(ifc_file)
elif extension.lower() == "ifczip":
return self.extract_ifc_zip()
elif extension.lower() == "ifcsqlite":
return {} # TODO
raise ValueError(f"Unsupported file extension: '{extension}'.")
def extract_ifc_spf(self, ifc_file: Union[IO[bytes], IO[str]]) -> HeaderMetadata:
# https://www.steptools.com/stds/step/IS_final_p21e3.html#clause-8
data = HeaderMetadata()
max_lines_to_parse = 50
for _ in range(max_lines_to_parse):
line = next(ifc_file)
if isinstance(line, bytes):
line = line.decode("utf-8")
if line.startswith("FILE_DESCRIPTION"):
for i, part in enumerate(line.split("'")):
if i == 1:
data["description"] = part
elif i == 3:
data["implementation_level"] = part
elif line.startswith("FILE_NAME"):
for i, part in enumerate(line.split("'")):
if i == 1:
data["name"] = part
elif i == 3:
data["time_stamp"] = part
elif line.startswith("FILE_SCHEMA"):
data["schema_name"] = line.split("'")[1]
break
return data
def extract_ifc_zip(self) -> HeaderMetadata:
archive = zipfile.ZipFile(self.filepath, "r")
return self.extract_ifc_spf(archive.open(archive.filelist[0]))
@@ -0,0 +1,186 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
from typing import Literal
from typing_extensions import assert_never
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.attribute
# COBie actually uses an exclusion list, but this inclusion list is equivalent.
cobie_type_classes = [
"IfcDoorStyle",
"IfcBuildingElementProxyType",
"IfcChimneyType",
"IfcCoveringType",
"IfcDoorType",
"IfcFootingType",
"IfcPileType",
"IfcRoofType",
"IfcShadingDeviceType",
"IfcWindowType",
"IfcDistributionControlElementType",
"IfcDistributionChamberElementType",
"IfcEnergyConversionDeviceType",
"IfcFlowControllerType",
"IfcFlowMovingDeviceType",
"IfcFlowStorageDeviceType",
"IfcFlowTerminalType",
"IfcFlowTreatmentDeviceType",
"IfcElementAssemblyType",
"IfcBuildingElementPartType",
"IfcDiscreteAccessoryType",
"IfcMechanicalFastenerType",
"IfcReinforcingElementType",
"IfcVibrationIsolatorType",
"IfcFurnishingElementType",
"IfcGeographicElementType",
"IfcTransportElementType",
"IfcSpatialZoneType",
"IfcWindowStyle",
]
cobie_component_classes = [
"IfcBuildingElementProxy",
"IfcChimney",
"IfcCovering",
"IfcDoor",
"IfcShadingDevice",
"IfcWindow",
"IfcDistributionControlElement",
"IfcDistributionChamberElement",
"IfcEnergyConversionDevice",
"IfcFlowController",
"IfcFlowMovingDevice",
"IfcFlowStorageDevice",
"IfcFlowTerminal",
"IfcFlowTreatmentDevice",
"IfcDiscreteAccessory",
"IfcTendon",
"IfcTendonAnchor",
"IfcVibrationIsolator",
"IfcFurnishingElement",
"IfcGeographicElement",
"IfcTransportElement",
]
fmhem_classes_ifc4 = [
"IfcDoorType",
"IfcWindowType",
"IfcShadingDeviceType",
"IfcDistributionControlElementType",
"IfcEnergyConversionDeviceType",
"IfcFlowControllerType",
"IfcFlowMovingDeviceType",
"IfcFlowStorageDeviceType",
"IfcFlowTerminalType",
"IfcFlowTreatmentDeviceType",
"IfcFurnishingElementType",
"IfcTransportElementType",
]
fmhem_classes_ifc2x3 = [
"IfcDoorStyle",
"IfcWindowStyle",
"IfcShadingDeviceType",
"IfcDistributionControlElementType",
"IfcEnergyConversionDeviceType",
"IfcFlowControllerType",
"IfcFlowMovingDeviceType",
"IfcFlowStorageDeviceType",
"IfcFlowTerminalType",
"IfcFlowTreatmentDeviceType",
"IfcFurnishingElementType",
"IfcTransportElementType",
]
fmhem_excluded_classes = [
"IfcCooledBeamType",
"IfcBurnerType",
"IfcCoilType",
"IfcLampType",
]
def get_cobie_types(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]:
elements = []
for ifc_class in cobie_type_classes:
try:
elements += ifc_file.by_type(ifc_class)
except:
pass
return elements
def get_cobie_components(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]:
elements = []
for ifc_class in cobie_component_classes:
try:
elements += ifc_file.by_type(ifc_class)
except:
pass
return elements
def get_fmhem_types(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]:
elements = []
if ifc_file.schema == "IFC2X3":
fmhem_classes = fmhem_classes_ifc2x3
else:
fmhem_classes = fmhem_classes_ifc4
for ifc_class in fmhem_classes:
try:
elements += [e for e in ifc_file.by_type(ifc_class) if e.is_a() not in fmhem_excluded_classes]
except:
pass
return elements
def get_fmhem_classes(schema: Literal["IFC4", "IFC2X3"] = "IFC4") -> dict[str, list[str]]:
results = {}
def get_fmhem_class(declaration: W.entity) -> None:
if declaration.name() in fmhem_excluded_classes:
pass
elif declaration.is_abstract():
pass
else:
types = []
for attribute in declaration.all_attributes():
if attribute.name() == "PredefinedType":
types = list(ifcopenshell.util.attribute.get_enum_items(attribute))
if "NOTDEFINED" in types:
types.remove("NOTDEFINED")
results[declaration.name()] = types
for subtype in declaration.subtypes():
get_fmhem_class(subtype)
if schema == "IFC4":
classes = fmhem_classes_ifc4
elif schema == "IFC2X3":
classes = fmhem_classes_ifc2x3
else:
assert_never(schema)
schema_ = ifcopenshell.schema_by_name(schema)
for ifc_class in classes:
declaration = schema_.declaration_by_name(ifc_class)
get_fmhem_class(declaration)
return results
@@ -0,0 +1,425 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 @Andrej730
#
# 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/>.
RUN_FROM_DEV_REPO = False
import glob
import sys
from itertools import chain
from pathlib import Path
from typing import cast
from lxml import etree
import ifcopenshell.api.project
import ifcopenshell.api.unit
import ifcopenshell.guid
import ifcopenshell.ifcopenshell_wrapper as W
if not RUN_FROM_DEV_REPO:
import shutil
import zipfile
BASE_MODULE_PATH = Path(__file__).parent
if not RUN_FROM_DEV_REPO:
IFC4x3_HTML_ZIP_LOCATION = BASE_MODULE_PATH / "annex-a-psd.zip"
IFC4x3_OUTPUT_PATH = BASE_MODULE_PATH / "schema/Pset_IFC4X3.ifc"
else:
IFC4x3_PSD_LOCATION = BASE_MODULE_PATH / "../output/psd"
try:
IFC4x3_OUTPUT_PATH = sys.argv[1]
except:
IFC4x3_OUTPUT_PATH = BASE_MODULE_PATH / "../output/Pset_IFC4X3.ifc"
IFC2x3_HTML_ZIP_LOCATION = BASE_MODULE_PATH / "IFC2x3_TC1_HTML_distribution-pset_errata.zip"
IFC2x3_OUTPUT_PATH = BASE_MODULE_PATH / "schema/Pset_IFC2X3.ifc"
PROPERTY_TYPES_DICT = {
"TypePropertySingleValue": ("P_SINGLEVALUE", "type"),
# in IFC2X3 weirdly xmls have TypeSimpleProperty
# which is actually more P_REFERENCEVALUE value, not P_SINGLEVALUE
# because it utilizes IfcTimeSeries
"TypeSimpleProperty": ("P_REFERENCEVALUE", "type"),
"TypePropertyListValue": ("P_LISTVALUE", "type"),
"TypePropertyBoundedValue": ("P_BOUNDEDVALUE", "type"),
"TypePropertyReferenceValue": ("P_REFERENCEVALUE", "reftype"),
"TypePropertyEnumeratedValue": ("P_ENUMERATEDVALUE", ""),
"TypePropertyTableValue": ("P_TABLEVALUE", "type"),
"TypeComplexProperty": ("P_COMPLEX", ""),
}
class PsetTemplatesGenerator:
def parse_ifc4x3_data(self):
print("Starting parsing data for IFC4X3...")
if not RUN_FROM_DEV_REPO:
if not IFC4x3_HTML_ZIP_LOCATION.is_file():
raise Exception(
f'ISO release for Ifc4.3.2.0 expected to be located in "{IFC4x3_HTML_ZIP_LOCATION.resolve()}"\n'
"For generating ifc pset library please either setup docs as described above \n"
"or change IFC4x3_HTML_ZIP_LOCATION in the script accordingly.\n"
"You can download docs from the repository: \n"
"https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/annex-a-psd.zip"
)
# unzip the data
pset_data_location = BASE_MODULE_PATH / "temp/annex-a-psd"
with zipfile.ZipFile(IFC4x3_HTML_ZIP_LOCATION, "r") as fi_zip:
fi_zip.extractall(pset_data_location)
else:
if not IFC4x3_PSD_LOCATION.is_dir():
raise Exception(
f'Psets xmls files expected to be in folder "{IFC4x3_PSD_LOCATION.resolve()}\\"\n'
"For generating ifc pset library please either setup docs as described above \n"
"or change IFC4x3_PSD_LOCATION in the script accordingly."
)
pset_data_location = IFC4x3_PSD_LOCATION
pset_data_glob = f"{pset_data_location}/*.xml"
self.parse_psets_data("IFC4X3", pset_data_glob, "IFC4X3 Property Set Templates", str(IFC4x3_OUTPUT_PATH))
if not RUN_FROM_DEV_REPO:
shutil.rmtree(pset_data_location)
def parse_ifc2x3_data(self):
print("Starting parsing data for IFC2X3...")
if not IFC2x3_HTML_ZIP_LOCATION.is_file():
raise Exception(
f'ISO release for IFC2x3 TC1 expected to be located in "{IFC2x3_HTML_ZIP_LOCATION.resolve()}"\n'
"For doc extraction please either setup docs as described above \n"
"or change IFC2x3_HTML_LOCATION in the script accordingly.\n"
"You can download docs from the url: \n"
"https://standards.buildingsmart.org/IFC/RELEASE/IFC2x3/TC1/IFC2x3_TC1_HTML_distribution-pset_errata.zip"
)
# unzip the data
pset_data_location = BASE_MODULE_PATH / "temp/ifc2x3_html"
with zipfile.ZipFile(IFC2x3_HTML_ZIP_LOCATION, "r") as fi_zip:
fi_zip.extractall(pset_data_location)
pset_data_glob = f"{pset_data_location}/**/pset*.xml"
# we're using IFC4X3 since IfcPropertySetTemplate and IfcRelDeclares
# are not available in IFC2X3
self.parse_psets_data("IFC4X3", pset_data_glob, "IFC2X3 Property Set Templates", str(IFC2x3_OUTPUT_PATH))
shutil.rmtree(pset_data_location)
def ifc_entity(self, entity_name, **kwargs):
entity = self.ifc_file.create_entity(entity_name, **kwargs)
return entity
def parse_psets_data(self, schema_name, pset_data_glob, project_name, ifc_output_path):
schema_name = schema_name.upper()
self.ifc_file = ifcopenshell.api.project.create_file(version=schema_name)
self.units = dict()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(self.ifc_file.schema_identifier)
derived_unit_enum = schema.declaration_by_name("IfcDerivedUnitEnum").as_enumeration_type()
assert derived_unit_enum
self.ifc_derived_unit_enum = derived_unit_enum.enumeration_items()
ifc_unit_enum = schema.declaration_by_name("IfcUnitEnum").as_enumeration_type()
assert ifc_unit_enum
self.ifc_unit_enum = ifc_unit_enum.enumeration_items()
value_select = schema.declaration_by_name("IfcValue").as_select_type()
assert value_select
select_types = cast(tuple[W.select_type, ...], value_select.select_list())
self.ifc_value_types = [t.name() for t in chain(*[select_type.select_list() for select_type in select_types])]
project = self.ifc_entity("IfcProject", Name=project_name, GlobalId=ifcopenshell.guid.new())
psets_list = []
if schema_name == "IFC2X3":
rel = None
else:
rel = self.ifc_entity(
"IfcRelDeclares",
RelatedDefinitions=psets_list,
RelatingContext=project,
GlobalId=ifcopenshell.guid.new(),
)
# in IFC4 there is also
# IFCLIBRARYREFERENCE after each (sometimes multiple of them if there are several languages involved)
# 1) enumeration item (also with IFCRELASSOCIATESLIBRARY)
# 2) enumeration
# 3) property set (also with IFCRELASSOCIATESLIBRARY)
# 4) property definition (also with IFCRELASSOCIATESLIBRARY)
# but in ifc4x3 there is no data for those library references
# TODO: need to add it to .ifc for IFC4X3 too
# if https://github.com/buildingSMART/IFC4.3.x-development/issues/587 is resolved
# iterate through all xmls files
for pset_path in glob.iglob(pset_data_glob, recursive=True):
pset_path = Path(pset_path)
with open(pset_path, "r", encoding="utf-8") as fi:
root_xml = etree.fromstring(fi.read())
pset_name = root_xml.find("Name").text
# pset / qset
pset_type = True if pset_name.split("_")[0] == "Pset" else False
# TODO: if guids provided in xml should use them instead
pset_guid = ifcopenshell.guid.new()
applicable_entities = [i.text for i in root_xml.find("ApplicableClasses").findall("ClassName")]
pset = self.ifc_entity(
"IfcPropertySetTemplate",
GlobalId=pset_guid,
TemplateType=root_xml.get("templatetype"),
Name=pset_name,
Description=root_xml.find("Definition").text,
ApplicableEntity=",".join(applicable_entities).strip(),
)
if project_name.startswith("IFC2X3"):
pset.TemplateType = self.get_pset_template_type_ifc2x3(pset)
else:
pset.TemplateType = root_xml.get("templatetype")
# NOTE: there is also Applicability tag
# but it's seems always empty in ifc4 and ifc4x3
# in ifc2x3 it's present but contains just applicability string description
# e.g. "IfcElectricMotorType, IfcFlowMovingDeviceType entities." for "Pset_ElectricMotorTypeCommon"
pdef_entities = []
for pdef in root_xml.find("PropertyDefs" if pset_type else "QtoDefs").getchildren():
pset_property = self.get_property_from_pdef(pset_name, pset_type, pdef)
if not pset_property:
continue
pdef_entities.append(pset_property)
pset.HasPropertyTemplates = pdef_entities
psets_list.append(pset)
print(f"{len(psets_list)} psets parsed.")
if rel:
rel.RelatedDefinitions = psets_list
self.ifc_file.write(ifc_output_path)
def get_property_from_pdef(self, pset_name, pset_type, pdef):
pset_prop_guid = ifcopenshell.guid.new()
pset_property_name = pdef.find("Name").text
# figuring pset property types and related values
if not pset_type:
property_type_tag = None
else:
try:
assert len(pdef.find("PropertyType").getchildren()) == 1, "Not implemented case"
except AssertionError:
warning_message = (
f"WARNING: met not handles case with multiple property types "
f"- {pset_name}/{pset_property_name}."
"This property may have incorrect data parsed."
)
print(warning_message)
property_type_tag = pdef.find("PropertyType").getchildren()[0].tag
if property_type_tag not in PROPERTY_TYPES_DICT:
warning_message = (
f"WARNING: not implemented property type "
f"{property_type_tag} ({pset_name}/{pset_property_name}). "
"This property will be skipped. Need to rework the code to support that type of property."
)
print(warning_message)
return
# NOTE: there is also ValueDef tag
# which could have two subtags - MinValue and MaxValue
# but it's seems to be present only in ifc2x3 and everywhere values are either "", "0", "?"
# so just ignoring it
# create pset property
if not pset_type or property_type_tag != "TypeComplexProperty":
pset_property = self.ifc_entity("IfcSimplePropertyTemplate")
# tested on IFC4_ADD2.ifc - all props are READWRITE by default
pset_property.AccessState = "READWRITE"
else:
pset_property = self.ifc_entity("IfcComplexPropertyTemplate")
complex_prop_xml = pdef.find("PropertyType/TypeComplexProperty")
child_properties = [
self.get_property_from_pdef(pset_name, pset_type, child_pdef)
for child_pdef in complex_prop_xml.findall("PropertyDef")
]
pset_property.UsageName = complex_prop_xml.get("name")
pset_property.HasPropertyTemplates = child_properties
pset_property.GlobalId = pset_prop_guid
pset_property.Description = pdef.find("Definition").text
pset_property.Name = pset_property_name
self.add_prop_type_params_to_prop(pset_type, pdef, pset_property, property_type_tag)
return pset_property
def get_unit(self, unit_type):
# to define USERDEFINED unit type we'd need more info
# like UserDefinedType name and elements for IfcDerivedUnit
# which is not provided in .xmls
if unit_type != "USERDEFINED":
return None
if unit_type in self.units:
return self.units[unit_type]
unit_entity = None
if unit_type in self.ifc_derived_unit_enum:
# TODO: define derived units if there will be someday api like `ifcopenshell.api.unit.add_derived_unit(...)`
# since creating IfcDerivedUnit is more complex and requiring settting up elements it consists of
# and related IfcNamedUnits
# unit_entity = ifcopenshell.api.unit.add_derived_unit(self.ifc_file, unit_type=unit_type)
pass
elif unit_type in self.ifc_unit_enum:
unit_entity = ifcopenshell.api.unit.add_si_unit(self.ifc_file, unit_type=unit_type)
elif unit_type == "IFCMONETARYUNIT":
self.ifc_entity("IFCMONETARYUNIT", Currency="")
else:
print(f"WARNING. Wasn't able to find units {unit_type} in schema.")
self.units[unit_type] = unit_entity
return unit_entity
def add_prop_type_params_to_prop(self, pset_type, pdef, pset_property, property_type_tag):
if not pset_type:
property_type = pdef.find("QtoType").text
else:
property_type, property_type_parse = PROPERTY_TYPES_DICT[property_type_tag]
pset_property.TemplateType = property_type
if not pset_type or property_type == "P_COMPLEX":
# qsets and complex props don't have measure types
return
property_type_path = f"PropertyType/{property_type_tag}"
property_type_xml = pdef.find(property_type_path)
# usually it's only for P_TABLEVALUE
expression_xml = property_type_xml.find("Expression")
if expression_xml is not None and expression_xml.text != "-":
pset_property.Expression = expression_xml.text
# figure measure type for property sets
if property_type == "P_ENUMERATEDVALUE":
# always create new enumeration for the new properties
# even if the same enumeration was already used before
# example of reoccuring enumeration in .ifc for ifc4 - PEnum_ElementStatus
enum_items = [i.text for i in property_type_xml.findall(f"EnumList/EnumItem")]
enum_items = [self.ifc_entity("IfcLabel", wrappedValue=i) for i in enum_items]
prop_enumeration = self.ifc_entity(
"IfcPropertyEnumeration",
Name=property_type_xml.find("EnumList").get("name"),
EnumerationValues=enum_items,
)
pset_property.Enumerators = prop_enumeration
pset_property.PrimaryMeasureType = "IfcLabel"
elif property_type == "P_TABLEVALUE":
pset_property.PrimaryMeasureType = property_type_xml.find("DefiningValue/DataType").get("type")
pset_property.SecondaryMeasureType = property_type_xml.find("DefinedValue/DataType").get("type")
else:
if property_type == "P_REFERENCEVALUE":
if property_type_tag == "TypeSimpleProperty":
type_xml = property_type_xml.find("DataType")
primary_measure_type = type_xml.get(property_type_parse)
unit_type = property_type_xml.find("UnitType")
secondary_measure_type = (
unit_type.get(property_type_parse).strip() if unit_type is not None else None
)
if primary_measure_type != "IfcTimeSeries":
unit = self.get_unit(secondary_measure_type)
secondary_measure_type = primary_measure_type
primary_measure_type = "IfcTimeSeries"
pset_property.PrimaryUnit = unit
pset_property.PrimaryMeasureType = primary_measure_type
pset_property.SecondaryMeasureType = secondary_measure_type
else:
pset_property.PrimaryMeasureType = property_type_xml.get(property_type_parse)
# TODO: ifc4add2 seems to have some secondary measure types
# need to add it in ifc4x3 too
# if https://github.com/buildingSMART/IFC4.3.x-development/issues/586 is resolved
else:
if property_type == "P_LISTVALUE":
property_type_node = "ListValue"
if property_type in ("P_SINGLEVALUE", "P_BOUNDEDVALUE"):
property_type_node = "DataType"
# only used only in ifc2x3, omitted in ifc4 and ifc4x3
unit_type_xml = property_type_xml.find("UnitType")
if unit_type_xml is not None:
unit_type = unit_type_xml.get(property_type_parse).strip()
unit_entity = self.get_unit(unit_type)
pset_property.PrimaryUnit = unit_entity
type_xml = property_type_xml.find(property_type_node)
if property_type_node == "ListValue":
# for ListValue data type is contained in a single <DataType>
primary_measure_type = type_xml.find("DataType").get(property_type_parse)
else:
primary_measure_type = type_xml.get(property_type_parse)
primary_measure_type = primary_measure_type.strip()
if property_type == "P_SINGLEVALUE" and primary_measure_type not in self.ifc_value_types:
print(
f"Error assinging {primary_measure_type} as PrimaryMeasureType - it's not IfcValue, {property_type_tag}"
)
pset_property.PrimaryMeasureType = primary_measure_type
def get_pset_template_type_ifc2x3(self, pset_template: ifcopenshell.entity_instance) -> str:
def declaration_is_a(declaration: ifcopenshell_wrapper.declaration, ifc_class: str) -> bool:
if declaration.name() == ifc_class:
return True
super_type = declaration.supertype()
if not super_type:
return False
return declaration_is_a(super_type, ifc_class)
name = pset_template.Name
applicability = pset_template.ApplicableEntity
schema = ifcopenshell.schema_by_name("IFC2X3")
if "PHistory" in name:
return "PSET_PERFORMANCEDRIVEN"
applicable_types = applicability.replace(", ", ",").split(",")
for applicable_type in applicable_types:
if not applicable_type:
continue
parts = applicable_type.split("/")
assert 3 > len(parts) > 0
if parts[0].isupper(): # IFC2X3 thing
applicable_type = parts[1]
else:
applicable_type = parts[0]
applicable_type = applicable_type.strip()
declaration = schema.declaration_by_name(applicable_type)
if declaration_is_a(declaration, "IfcTypeObject"):
return "PSET_TYPEDRIVENOVERRIDE"
# ifc4x3+
elif declaration_is_a(declaration, "IfcProfileDef"):
return "PSET_PROFILEDRIVEN"
elif declaration_is_a(declaration, "IfcMaterialDefinition"):
return "PSET_MATERIALDRIVEN"
return "PSET_OCCURRENCEDRIVEN"
if __name__ == "__main__":
templates_generator = PsetTemplatesGenerator()
templates_generator.parse_ifc4x3_data()
if not RUN_FROM_DEV_REPO:
templates_generator.parse_ifc2x3_data()
@@ -0,0 +1,691 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
import math
from decimal import ROUND_HALF_UP, Decimal
from typing import Any, NamedTuple, Optional, Union
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.placement
MatrixType = ifcopenshell.util.placement.MatrixType
class HelmertTransformation(NamedTuple):
e: float
n: float
h: float
xaa: float
xao: float
scale: float
factor_x: float
factor_y: float
factor_z: float
def dms2dd(degrees: int, minutes: int, seconds: int, us: int = 0) -> float:
"""Convert degrees, minutes, and (micro)seconds to decimal degrees
All components must be either positive or negative.
:param degrees: The degrees component
:param minutes: The minutes component
:param seconds: The seconds component
:param us: The microseconds component
:return: The angle in decimal degrees.
"""
all_positive_or_zero = degrees >= 0 and minutes >= 0 and seconds >= 0 and us >= 0
all_negative_or_zero = degrees <= 0 and minutes <= 0 and seconds <= 0 and us <= 0
assert all_positive_or_zero or all_negative_or_zero
return degrees + minutes / 60.0 + seconds / 3600.0 + us / 3600000000.0
def dd2dms(dd: float, use_us: bool = False) -> Union[tuple[int, int, int, int], tuple[int, int, float]]:
"""Convert decimal degrees to degrees, minutes, and (micro)seconds format
:param dd: The decimal degrees
:param use_us: True if to include microseconds and false otherwise. Defaults to false.
:return: The angle in a tuple of either 3 or 4 values,
4 values: integer number of degrees, integer number of minutes, integer number of seconds and integer number of microseconds
3 values: integer number of degrees, integer number of minutes, and a float number for seconds
:note: the tuple follows the format of IfcCompoundPlaneAngleMeasure. Namely all of its components are either positive or negative.
"""
dd_decimal = Decimal(str(dd))
degrees = int(dd_decimal)
degrees_decimal = Decimal(degrees)
fractional_part = dd_decimal - degrees_decimal
minutes_decimal = fractional_part * Decimal(60)
minutes = int(minutes_decimal)
minutes_decimal_int = Decimal(minutes)
seconds_decimal = (minutes_decimal - minutes_decimal_int) * Decimal(60)
if use_us:
seconds = int(seconds_decimal)
seconds_decimal_int = Decimal(seconds)
microseconds_decimal = (seconds_decimal - seconds_decimal_int) * Decimal(1000000)
microseconds = int(microseconds_decimal.quantize(Decimal(1), rounding=ROUND_HALF_UP))
return (degrees, minutes, seconds, microseconds)
else:
seconds_float = float(seconds_decimal)
return (degrees, minutes, seconds_float)
def xyz2enh(
x: float,
y: float,
z: float,
eastings: float = 0.0,
northings: float = 0.0,
orthogonal_height: float = 0.0,
x_axis_abscissa: float = 1.0,
x_axis_ordinate: float = 0.0,
scale: float = 1.0,
factor_x: float = 1.0,
factor_y: float = 1.0,
factor_z: float = 1.0,
) -> tuple[float, float, float]:
"""Manually convert local XYZ coordinates to map eastings, northings, and height
This function is for advanced users as it allows you to specify your own
helmert transformation parameters (i.e. those typically stored in
IfcMapConversion). This manual approach is useful for tests or in case your
are setting your helmert transformations in non-standard locations, or if
you are applying your own temporary false origin (such as when federating
models for digital twins of large cities).
For most scenarios you should use :func:`auto_xyz2enh` instead.
:param x: The X local engineering coordinate.
:param y: The Y local engineering coordinate.
:param z: The Z local engineering coordinate.
:param eastings: The eastings offset to apply.
:param northings: The northings offset to apply.
:param orthogonal_height: The orthogonal height offset to apply.
:param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:param scale: The unit scale such that local ordinate * scale = map
ordinate. E.g. if your project is in millimeters but your CRS is in
meters, your scale should be 0.001.
:param factor_x: The combined scale factor for the X value to convert from
local coordinates to map coordinates. Your surveyor will typically know
this number and approximate it as a constant on a small site. Typically
factor_x and factor_y will be identical, and factor_z will be 1.
:param factor_y: Same but for the Y value.
:param factor_z: Same but for the Z value.
:return: A tuple of three ordinates representing the easting, northing and height.
"""
theta = math.atan2(x_axis_ordinate, x_axis_abscissa)
eastings = (scale * factor_x * math.cos(theta) * x) - (scale * factor_y * math.sin(theta) * y) + eastings
northings = (scale * factor_x * math.sin(theta) * x) + (scale * factor_y * math.cos(theta) * y) + northings
height = (scale * factor_z * z) + orthogonal_height
return (eastings, northings, height)
def auto_xyz2enh(
ifc_file: ifcopenshell.file, x: float, y: float, z: float, should_return_in_map_units: bool = True
) -> tuple[float, float, float]:
"""Convert from local XYZ coordinates to global map coordinate eastings, northings, and heights
The necessary georeferencing map conversion is automatically detected from
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the coordinates are returned unchanged.
For IFC2X3, the map conversion is detected from the IfcProject's
ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC":
https://www.buildingsmart.org/standards/bsi-standards/standards-library/
:param ifc_file: The IFC file
:param x: The X local engineering coordinate provided in project length units.
:param y: The Y local engineering coordinate provided in project length units.
:param z: The Z local engineering coordinate provided in project length units.
:param should_return_in_map_units: If true, the result is given in map units.
If false, the result will be converted back into project units.
:return: The global map coordinate eastings, northings, and height.
"""
parameters = get_helmert_transformation_parameters(ifc_file)
if not parameters:
return x, y, z
wcs = get_wcs(ifc_file)
if wcs is not None:
x, y, z = (np.linalg.inv(wcs) @ np.array((x, y, z, 1)))[:3]
enh = xyz2enh(x, y, z, *parameters)
if should_return_in_map_units:
return enh
return enh[0] / parameters.scale, enh[1] / parameters.scale, enh[2] / parameters.scale
def auto_enh2xyz(
ifc_file: ifcopenshell.file, easting: float, northing: float, height: float, is_specified_in_map_units: bool = True
) -> tuple[float, float, float]:
"""Convert from global map coordinate eastings, northings, and heights to local XYZ coordinates
The necessary georeferencing map conversion is automatically detected from
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the Z coordinate is returned unchanged.
For IFC2X3, the map conversion is detected from the IfcProject's
ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC":
https://www.buildingsmart.org/standards/bsi-standards/standards-library/
:param ifc_file: The IFC file
:param easting: The global easting map coordinate provided in map units.
:param northing: The global northing map coordinate provided in map units.
:param height: The global height map coordinate provided in map units.
:param is_specified_in_map_units: True if the input eastings, northing, and height are in map units.
:return: The local engineering XYZ coordinates in project length units.
"""
parameters = get_helmert_transformation_parameters(ifc_file)
if not parameters:
return easting, northing, height
if not is_specified_in_map_units:
easting *= parameters.scale
northing *= parameters.scale
height *= parameters.scale
xyz = enh2xyz(easting, northing, height, *parameters)
wcs = get_wcs(ifc_file)
if wcs is not None:
xyz = tuple((wcs @ np.array((*xyz, 1)))[:3])
return xyz
def get_helmert_transformation_parameters(ifc_file: ifcopenshell.file) -> Optional[HelmertTransformation]:
"""Retrieves the parameters of a helmert transformation that represents a
coordinate operation
This coordinate operation is typically what is used to convert between
local engineering coordinates and map coordinates.
:param ifc_file: The IFC model, typically containing an
IfcCoordinateOperation such as an IfcMapConversion.
:return: The parameters of the transformation.
"""
if ifc_file.schema == "IFC2X3":
project = ifc_file.by_type("IfcProject")[0]
conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")
if not conversion:
return
e = conversion.get("Eastings", None) or 0
n = conversion.get("Northings", None) or 0
h = conversion.get("OrthogonalHeight", None) or 0
xaa = conversion.get("XAxisAbscissa", None) or 0
xao = conversion.get("XAxisOrdinate", None) or 0
scale = conversion.get("Scale", None) or 1
factor_x = factor_y = factor_z = 1
else:
conversion = ifc_file.by_type("IfcCoordinateOperation")
if not conversion:
return
conversion = conversion[0]
if conversion.is_a("IfcMapConversion"):
e = conversion.Eastings or 0
n = conversion.Northings or 0
h = conversion.OrthogonalHeight or 0
xaa = conversion.XAxisAbscissa or 0
xao = conversion.XAxisOrdinate or 0
scale = conversion.Scale or 1
if conversion.is_a() == "IfcMapConversionScaled":
factor_x = conversion.FactorX
factor_y = conversion.FactorY
factor_z = conversion.FactorZ
else:
factor_x = factor_y = factor_z = 1
elif conversion.is_a() == "IfcRigidOperation":
e = conversion.FirstCoordinate.wrappedValue
n = conversion.SecondCoordinate.wrappedValue
h = conversion.Height or 0
xaa = 1.0
xao = 0.0
scale = factor_x = factor_y = factor_z = 1
if not xaa and not xao:
xaa = 1.0
xao = 0.0
return HelmertTransformation(e, n, h, xaa, xao, scale, factor_x, factor_y, factor_z)
def get_crs(ifc_file: ifcopenshell.file) -> dict[str, Any]:
"""Get CRS information from an IFC file."""
if ifc_file.schema == "IFC2X3":
return ifcopenshell.util.element.get_pset(ifc_file.by_type("IfcProject")[0], "ePSet_ProjectedCRS")
for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if operation := context.HasCoordinateOperation:
return operation[0].TargetCRS.get_info()
def auto_z2e(ifc_file: ifcopenshell.file, z: float, should_return_in_map_units: bool = True) -> float:
"""Convert a Z coordinate to an elevation using model georeferencing data
The necessary georeferencing map conversion is automatically detected from
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the Z coordinate is returned unchanged.
For IFC2X3, the map conversion is detected from the IfcProject's
ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC":
https://www.buildingsmart.org/standards/bsi-standards/standards-library/
:param ifc_file: The IFC file
:param z: The Z local engineering coordinate provided in project length units.
:return: The elevation in project length units.
"""
parameters = get_helmert_transformation_parameters(ifc_file)
if not parameters:
return z
e = z2e(z, parameters.h, parameters.scale, parameters.factor_z)
if should_return_in_map_units:
return e
return e / parameters.scale
def z2e(z: float, orthogonal_height: float = 0.0, scale: float = 1.0, factor_z: float = 1.0) -> float:
"""Manually convert a Z coordinate to a map elevation
This function is for advanced users as it allows you to specify your own
orthogonal height offset and transformation parameters.
For most scenarios you should use :func:`auto_z2e` instead.
:param z: The Z local engineering coordinate provided in project length units.
:param orthogonal_height: The orthogonal height offset to apply.
:param scale: The unit scale such that local ordinate * scale = map
ordinate. E.g. if your project is in millimeters but your CRS is in
meters, your scale should be 0.001.
:param factor_x: The combined scale factor for the Z value to convert from
local coordinates to map coordinates. Your surveyor will typically know
this number and approximate it as a constant on a small site. This is
typically just 1.0, as average combined scale factors usually only
affect the XY axes.
:return: The elevation in map units.
"""
return (scale * factor_z * z) + orthogonal_height
def enh2xyz(
e: float,
n: float,
h: float,
eastings: float = 0.0,
northings: float = 0.0,
orthogonal_height: float = 0,
x_axis_abscissa: float = 1.0,
x_axis_ordinate: float = 0.0,
scale: float = 1.0,
factor_x: float = 1.0,
factor_y: float = 1.0,
factor_z: float = 1.0,
) -> tuple[float, float, float]:
"""Manually convert map eastings, northings, and height to local XYZ coordinates
This function is for advanced users as it allows you to specify your own
helmert transformation parameters (i.e. those typically stored in
IfcMapConversion). This manual approach is useful for tests or in case your
are setting your helmert transformations in non-standard locations, or if
you are applying your own temporary false origin (such as when federating
models for digital twins of large cities).
For most scenarios you should use :func:`auto_enh2xyz` instead.
:param e: The global easting map coordinate.
:param n: The global northing map coordinate.
:param h: The global height map coordinate.
:param eastings: The eastings offset to apply.
:param northings: The northings offset to apply.
:param orthogonal_height: The orthogonal height offset to apply.
:param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:param scale: The unit scale such that local ordinate * scale = map
ordinate. E.g. if your project is in millimeters but your CRS is in
meters, your scale should be 0.001.
:param factor_x: The combined scale factor for the X value to convert from
local coordinates to map coordinates. Your surveyor will typically know
this number and approximate it as a constant on a small site. Typically
factor_x and factor_y will be identical, and factor_z will be 1.
:param factor_y: Same but for the Y value.
:param factor_z: Same but for the Z value.
:return: A tuple of three ordinates representing XYZ.
"""
theta = math.atan2(x_axis_ordinate, x_axis_abscissa)
sint = math.sin(theta)
cost = math.cos(theta)
x = (((e - eastings) * cost) + ((n - northings) * sint)) / (scale * factor_x)
y = (((eastings - e) * sint) + ((n - northings) * cost)) / (scale * factor_y)
z = ((h - orthogonal_height) / scale) / factor_z
return (x, y, z)
def local2global(
matrix: MatrixType,
eastings: float = 0.0,
northings: float = 0.0,
orthogonal_height: float = 0.0,
x_axis_abscissa: float = 1.0,
x_axis_ordinate: float = 0.0,
scale: float = 1.0,
factor_x: float = 1.0,
factor_y: float = 1.0,
factor_z: float = 1.0,
) -> MatrixType:
"""Manually convert a 4x4 matrix from local to global coordinates
This function is for advanced users as it allows you to specify your own
helmert transformation parameters (i.e. those typically stored in
IfcMapConversion). This manual approach is useful for tests or in case your
are setting your helmert transformations in non-standard locations, or if
you are applying your own temporary false origin (such as when federating
models for digital twins of large cities).
For most scenarios you should use :func:`auto_local2global` instead.
:param matrix: A 4x4 numpy matrix representing local coordinates.
:param eastings: The eastings offset to apply.
:param northings: The northings offset to apply.
:param orthogonal_height: The orthogonal height offset to apply.
:param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:param scale: The combined scale factor to convert from local coordinates
to map coordinates.
:return: A numpy 4x4 array matrix representing global coordinates.
"""
theta = math.atan2(x_axis_ordinate, x_axis_abscissa)
scale_and_factor_matrix = np.array(
[
[scale * factor_x, 0, 0, 0],
[0, scale * factor_y, 0, 0],
[0, 0, scale * factor_z, 0],
[0, 0, 0, 1],
]
)
rotation_matrix = np.array(
[
[math.cos(theta), -math.sin(theta), 0, 0],
[math.sin(theta), math.cos(theta), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
]
)
result = rotation_matrix @ scale_and_factor_matrix @ matrix
result[:3, 0] /= np.linalg.norm(result[:3, 0])
result[:3, 1] /= np.linalg.norm(result[:3, 1])
result[:3, 2] /= np.linalg.norm(result[:3, 2])
result[0, 3] += eastings
result[1, 3] += northings
result[2, 3] += orthogonal_height
return result
def auto_local2global(
ifc_file: ifcopenshell.file, matrix: MatrixType, should_return_in_map_units: bool = True
) -> MatrixType:
"""Convert a local matrix to a global map matrix
The necessary georeferencing map conversion is automatically detected from
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the matrix is returned unchanged.
:param ifc_file: The IFC file
:param matrix: A 4x4 numpy matrix representing local coordinates.
:param should_return_in_map_units: If true, the result is given in map units.
If false, the result will be converted back into project units.
:return: A numpy 4x4 array matrix representing global coordinates.
"""
parameters = get_helmert_transformation_parameters(ifc_file)
if not parameters:
return matrix.copy()
wcs = get_wcs(ifc_file)
if wcs is not None:
matrix = np.linalg.inv(wcs) @ matrix
result = local2global(matrix, *parameters)
if should_return_in_map_units:
return result
result[:3, 3] /= parameters.scale
return result
def global2local(
matrix: MatrixType,
eastings: float = 0.0,
northings: float = 0.0,
orthogonal_height: float = 0.0,
x_axis_abscissa: float = 1.0,
x_axis_ordinate: float = 0.0,
scale: float = 1.0,
factor_x: float = 1.0,
factor_y: float = 1.0,
factor_z: float = 1.0,
) -> MatrixType:
"""Manually convert a 4x4 matrix from global to local coordinates
This function is for advanced users as it allows you to specify your own
helmert transformation parameters (i.e. those typically stored in
IfcMapConversion). This manual approach is useful for tests or in case your
are setting your helmert transformations in non-standard locations, or if
you are applying your own temporary false origin (such as when federating
models for digital twins of large cities).
:param matrix: A 4x4 numpy matrix representing global coordinates.
:param eastings: The eastings offset to apply.
:param northings: The northings offset to apply.
:param orthogonal_height: The orthogonal height offset to apply.
:param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:param scale: The combined scale factor to convert from local coordinates
to map coordinates.
:return: A numpy 4x4 array matrix representing local coordinates.
"""
theta = math.atan2(x_axis_ordinate, x_axis_abscissa)
scale_and_factor_matrix = np.array(
[
[scale * factor_x, 0, 0, 0],
[0, scale * factor_y, 0, 0],
[0, 0, scale * factor_z, 0],
[0, 0, 0, 1],
]
)
rotation_matrix = np.array(
[
[math.cos(theta), -math.sin(theta), 0, 0],
[math.sin(theta), math.cos(theta), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
]
)
result = matrix.copy()
result[0, 3] -= eastings
result[1, 3] -= northings
result[2, 3] -= orthogonal_height
result = np.linalg.inv(scale_and_factor_matrix) @ np.linalg.inv(rotation_matrix) @ result
result[:3, 0] /= np.linalg.norm(result[:3, 0])
result[:3, 1] /= np.linalg.norm(result[:3, 1])
result[:3, 2] /= np.linalg.norm(result[:3, 2])
return result
def auto_global2local(
ifc_file: ifcopenshell.file, matrix: MatrixType, is_specified_in_map_units: bool = True
) -> MatrixType:
"""Convert a global map matrix to a local matrix
The necessary georeferencing map conversion is automatically detected from
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the matrix is returned unchanged.
:param ifc_file: The IFC file
:param matrix: A 4x4 numpy matrix representing local coordinates.
:param should_return_in_map_units: If true, the result is given in map units.
If false, the result will be converted back into project units.
:param is_specified_in_map_units: True if the input matrix is in map units.
:return: A numpy 4x4 array matrix representing global coordinates.
"""
parameters = get_helmert_transformation_parameters(ifc_file)
if not parameters:
return matrix.copy()
if not is_specified_in_map_units:
matrix = matrix.copy()
matrix[:3, 3] *= parameters.scale
result = global2local(matrix, *parameters)
wcs = get_wcs(ifc_file)
if wcs is not None:
return wcs @ result
return result
def xaxis2angle(x: float, y: float) -> float:
"""Converts X axis abscissa and ordinates to an angle in decimal degrees
The X axis abscissa and ordinate is how IFC stores grid north.
This X axis vector indicates "where is project east, if grid north is up
the page?". See the diagram on the IfcGeometricRepresentationContext
documentation for clarification.
The angle indicates "how do I rotate project east to get to grid east?".
Alternatively: "how do I rotate project north to get to grid north?".
Positive angles are anticlockwise.
:param x: The X axis abscissa
:param y: The X axis ordinate
:return: The equivalent angle in decimal degrees from the X axis
"""
return math.degrees(math.atan2(y, x)) * -1
def yaxis2angle(x: float, y: float) -> float:
"""Converts Y axis abscissa and ordinates to an angle in decimal degrees
The Y axis abscissa and ordinate is how IFC stores true north.
This Y axis vector indicates "where is true north, if project north is up
the page?". See the diagram on the IfcGeometricRepresentationContext
documentation for clarification.
The angle indicates "how do I rotate project north to get to true north?".
Positive angles are anticlockwise.
:param x: The Y axis abscissa
:param y: The Y axis ordinate
:return: The equivalent angle in decimal degrees from the Y axis
"""
angle = math.degrees(math.atan2(y, x)) - 90
if angle < -180:
angle += 360
elif angle > 180:
angle -= 360
return angle
def get_grid_north(ifc_file: ifcopenshell.file) -> float:
"""Get an angle pointing to map grid north
Anticlockwise is positive.
The necessary georeferencing map conversion is automatically detected from
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the Z coordinate is returned unchanged.
For IFC2X3, the map conversion is detected from the IfcProject's
ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC":
https://www.buildingsmart.org/standards/bsi-standards/standards-library/
:param ifc_file: The IFC file
:return: An angle to grid north in decimal degrees
"""
parameters = get_helmert_transformation_parameters(ifc_file)
if not parameters:
return 0
return xaxis2angle(parameters.xaa, parameters.xao)
def get_true_north(ifc_file: ifcopenshell.file) -> float:
"""Get an angle pointing to global true north
Anticlockwise is positive.
Always remember that true north is not a constant! (Unless you are working
in polar coordinates) This true north is only a reference value useful for
things like solar analysis on small sites (<1km). If you're after the north
that your surveyor is using, you're probably after :func:`get_grid_north`
instead.
:param ifc_file: The IFC file
:return: An angle to true north in decimal degrees
"""
try:
for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.TrueNorth:
return yaxis2angle(*context.TrueNorth.DirectionRatios[0:2])
except:
return 0
return 0
def angle2xaxis(angle: float) -> tuple[float, float]:
"""Converts an angle into an X axis abscissa and ordinate
The inverse of :func:`xaxis2angle`.
:param angle: The angle in decimal degrees where anticlockwise is positive.
:return: A tuple of X axis abscissa and ordinate
"""
angle_rad = math.radians(angle)
x = math.cos(angle_rad)
y = -math.sin(angle_rad)
return x, y
def angle2yaxis(angle: float) -> tuple[float, float]:
"""Converts an angle into an Y axis abscissa and ordinate
The inverse of :func:`yaxis2angle`.
:param angle: The angle in decimal degrees where anticlockwise is positive.
:return: A tuple of Y axis abscissa and ordinate
"""
angle_rad = math.radians(angle)
x = -math.sin(angle_rad)
y = math.cos(angle_rad)
return x, y
def get_wcs(ifc_file: ifcopenshell.file) -> Optional[MatrixType]:
"""Gets the WCS (prioritising 3D contexts) as a matrix
:param: The IFC file
:return: A 4x4 matrix in project units
"""
wcs = None
for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
wcs = context.WorldCoordinateSystem
if context.ContextType == "Model":
break
if wcs:
return ifcopenshell.util.placement.get_axis2placement(wcs)
@@ -0,0 +1,35 @@
{
"IfcAudioVisualAppliance.CAMERA": "Camera",
"IfcUnitaryEquipment.AIRHANDLER": "AHU",
"IfcBoiler": "Boiler",
"IfcUnitaryEquipment.AIRCONDITIONINGUNIT": "CRAC",
"IfcChiller": "Chiller",
"IfcCompressor": "Compressor",
"IfcCondenser": "Condenser",
"IfcCoolingTower": "Cooling_Tower",
"IfcDamper": "Damper",
"IfcFan": "Fan",
"IfcFilter": "Filter",
"IfcHeatExchanger": "Heat_Exchanger",
"IfcHumidifier": "Humidifier",
"IfcPump": "Pump",
"IfcAirTerminalBox": "TerminalUnit",
"IfcAirTerminalBox.CONSTANTFLOW": "CAV",
"IfcAirTerminalBox.VARIABLEFLOWPRESSUREDEPENDANT": "VAV",
"IfcAirTerminalBox.VARIABLEFLOWPRESSUREINDEPENDANT": "VAV",
"IfcValve": "Valve",
"IfcUnitaryControlElement.THERMOSTAT": "Thermostat",
"IfcUnitaryControlElement.WEATHERSTATION": "Weather_Station",
"IfcLightFixture": "Luminaire",
"IfcFlowMeter": "Meter",
"IfcFlowMeter.ENERGYMETER": "Electric_Meter",
"IfcFlowMeter.GASMETER": "Gas_Meter",
"IfcFlowMeter.WATERMETER": "Water_Meter",
"IfcElectricMotor": "Motor",
"IfcSolarDevice.SOLARPANEL": "PV_Panel",
"IfcBuilding": "https://w3id.org/rec#Building",
"IfcBuildingStorey": "https://w3id.org/rec#Level",
"IfcSpace": "https://w3id.org/rec#Room",
"IfcSpatialZone": "https://w3id.org/rec#Zone",
"IfcSpatialZone.THERMAL": "https://w3id.org/rec#HVACZone"
}
@@ -0,0 +1,226 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 @Andrej730
#
# 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/>.
try:
from server import (
R,
get_resource_path,
process_markdown,
resource_documentation_builder,
)
except ModuleNotFoundError as e:
print(
"ERROR. Failed to import `server.py`.\n"
"Make sure you run this script from `/code` folder of https://github.com/buildingSMART/IFC4.3.x-development\n"
)
raise e
import itertools
import json
import operator
from collections import Counter
from typing import Any, Union
from bs4 import BeautifulSoup
import ifcopenshell
# Hacky modified functions from server.py to make parser work
def get_definition_from_md(resource: str, mdc: str) -> str:
# Only match up to the first h2
lines = []
for line in mdc.split("\n"):
if line.startswith("## "):
break
if line.startswith("### "):
words = line.split(" ")
line = " ".join((words[0], "", *words[1:]))
lines.append(line)
mdc = "\n".join(lines)
mdc_splitted = mdc.split("\n\n")
return mdc_splitted[1] if len(mdc_splitted) > 1 else ""
def get_type_values(resource: str, mdc: str) -> dict[str, Any]:
values = R.type_values.get(resource)
if not values:
return
has_description = values[0] == values[0].upper()
if has_description:
soup = BeautifulSoup(process_markdown(resource, mdc), features="lxml")
described_values = []
for value in values:
description = None
for h in soup.findAll("h3"):
if h.text != value:
continue
description = BeautifulSoup(features="lxml")
for sibling in h.find_next_siblings():
if sibling.name == "h3":
break
description.append(sibling)
description = str(description)
described_values.append({"name": value, "description": description})
values = described_values
return {"number": 0, "has_description": has_description, "schema_values": values}
def get_attributes_keep_md(resource, builder):
attrs = builder.attributes
supertype_counts = Counter()
supertype_counts.update([a[0] for a in attrs])
attrs = [a[1:] for a in attrs]
supertype_counts = list(supertype_counts.items())[::-1]
insertion_points = [0] + list(itertools.accumulate(map(operator.itemgetter(1), supertype_counts[::-1])))[:-1]
group_data = supertype_counts[::-1]
groups = []
for i, attr in enumerate(attrs):
if i in insertion_points:
name, total_attributes = group_data[insertion_points.index(i)]
group = {
"name": name,
"attributes": [],
"is_inherited": insertion_points[-1] != i,
"total_attributes": total_attributes,
}
groups.append(group)
attribute = {
"number": attr[0],
"name": attr[1],
"type": attr[2][0] if isinstance(attr[2], list) else attr[2],
"formal": attr[2][1] if isinstance(attr[2], list) else None,
# @nb we're not really talking about markdown anymore
# since the new attribute parser operates on a converted
# dom, but it appears to work nonetheless.
"description": attr[3],
"is_inverse": not attr[0],
}
if attribute["name"] == "PredefinedType" and not attribute["description"]:
description = "A list of types to further identify the object. Some property sets may be specifically applicable to one of these types."
if "Type" not in group["name"]:
description += "\n> NOTE If the object has an associated IfcTypeObject with a _PredefinedType_, then this attribute shall not be used."
attribute["description"] = description
group["attributes"].append(attribute)
total_inherited_attributes = sum([g["total_attributes"] for g in groups if g["is_inherited"]])
inherited_groups_with_attributes = [g for g in groups if g["is_inherited"] and g["total_attributes"]]
if inherited_groups_with_attributes:
inherited_groups_with_attributes[-1]["is_last_inherited_group"] = True
return {
"number": None,
"groups": groups,
"total_inherited_attributes": total_inherited_attributes,
}
# -------------------------
# Temporary fix for https://github.com/buildingSMART/IFC4.3.x-development/issues/754.
_original_get_resource_path = get_resource_path
def get_resource_path(resource: str, abort_on_error=False) -> Union[str, None]:
md = _original_get_resource_path(resource, abort_on_error)
if md and resource == "IfcURIReference":
md = md.replace("IfcMeasureResource", "IfcExternalReferenceResource")
return md
def get_description_json(resource: str) -> str:
md = get_resource_path(resource, abort_on_error=False)
if not md:
raise Exception(f"No resource path found for '{resource}'.")
mdc = open(md, "r", encoding="utf-8").read()
description = get_definition_from_md(resource, mdc)
return description
def get_attributes_json(resource):
builder = resource_documentation_builder(resource)
attrs = get_attributes_keep_md(resource, builder)
if not attrs["groups"]:
return []
attrs = [a for a in attrs["groups"] if a["name"] == resource]
if not attrs:
return []
return attrs[0]["attributes"]
def get_predefined_type_values_json(resource):
md = get_resource_path(resource, abort_on_error=False)
if not md:
raise Exception(f"No resource path found for '{resource}'.")
mdc = open(md, "r", encoding="utf-8").read()
return get_type_values(resource, mdc)["schema_values"]
def save_entities_data(entities: list[str]) -> None:
entities_description = dict()
for entity in entities:
entity_data = dict()
try:
entity_data["description"] = get_description_json(entity)
except Exception as e:
md = get_resource_path(entity, abort_on_error=False)
if md:
print(f"server returned markdown file path ('{md}') but there was some other parsing error, see below.")
raise e
print(
f"WARNING. Cannot find resource path for `{entity}` in DEV DOCUMENTATION even though it's present in ifcopenshell schema. It will be skipped."
)
continue
attrs = get_attributes_json(entity)
attributes_data = dict()
predefined_types_data = dict()
for a in attrs:
if a["name"] == "PredefinedType":
predefined_type_name_enum = a["type"].split()[-1]
predef_values = get_predefined_type_values_json(predefined_type_name_enum)
for v in predef_values:
description = v["description"]
description = description.strip() if description else ""
if description:
description = BeautifulSoup(description, features="lxml").find("p").text
predefined_types_data[v["name"]] = description
continue
description = a["description"]
description = description.strip() if description else ""
if description:
description = BeautifulSoup(description, features="lxml").find("p").text
attributes_data[a["name"]] = description
entity_data["attributes"] = attributes_data
entity_data["predefined_types"] = predefined_types_data
entities_description[entity] = entity_data
with open("entities_description.json", "w") as fo:
json.dump(entities_description, fo, sort_keys=True, indent=4)
if __name__ == "__main__":
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name("IFC4X3_ADD2")
entities: list[str] = [e.name() for e in schema.declarations()]
save_entities_data(entities)
@@ -0,0 +1,343 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.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/>.
from __future__ import annotations
try:
from lark import Lark, Transformer
from lark.exceptions import UnexpectedCharacters, UnexpectedEOF, UnexpectedToken
LARK_AVAILABLE = True
except ImportError:
LARK_AVAILABLE = False
import re
from typing import Union
if LARK_AVAILABLE:
mvd_grammar = r"""
start: entry+
entry: "ViewDefinition" "[" simple_value_list "]" -> view_definition
| "Comment" "[" simple_value_list "]" -> comment
| "ExchangeRequirement" "[" other_keyword "]" -> exchangerequirement
| "Option" "[" other_keyword "]" -> option
| GENERIC_KEYWORD "[" dynamic_option_word "]" -> dynamic_option
GENERIC_KEYWORD: /[A-Za-z0-9_]+/
simple_value_list: value ("," value)*
value_list_set: value_set (";" value_set)*
value_set: set_name ":" simple_value_list
set_name: /[A-Za-z0-9_]+/
value: /[A-Za-z0-9 _\.-]+/
other_keyword: /[^\[\]]+/
dynamic_option_word: /[^\[\]]+/
%import common.WS
%ignore WS
"""
parser = Lark(mvd_grammar, parser="lalr")
class DescriptionTransform(Transformer):
def __init__(self):
self.view_definitions = []
self.keywords = set()
self.comments = ""
self.exchange_requirements = ""
self.options = ""
self._dynamic = {}
def view_definition(self, args):
self.keywords.add("view_definitions")
self.view_definitions.extend(args[0])
def store_text_attribute(self, args, keyword):
self.keywords.add(keyword)
setattr(self, keyword, " ".join(" ".join(str(child) for child in args[0].children).split()))
def comment(self, args):
self.keywords.add("comments")
self.comments = args[0] if len(args[0]) > 1 else args[0][0]
def exchangerequirement(self, args):
self.store_text_attribute(args, "exchange_requirements")
def option(self, args):
if v := parse_semicolon_separated_kv(" ".join(" ".join(str(child) for child in args[0].children).split())):
setattr(self, "options", v)
else:
self.store_text_attribute(args, "options")
def dynamic_option(self, args):
try:
original_keyword = str(args[0])
key = original_keyword.lower()
raw_text = args[1].children[0].value
parsed_value = parse_semicolon_separated_kv(raw_text)
self._dynamic[key] = (parsed_value, original_keyword)
self.keywords.add(key)
setattr(self, key, parsed_value)
except Exception:
setattr(self, key, None)
def simple_value_list(self, args):
return [str(arg) for arg in args]
def value_list_set(self, args):
return args
def value_set(self, args):
return [str(args[0])] + args[1]
def value(self, args):
return str(args[0])
def set_name(self, args):
return str(args[0])
def parse_mvd(description):
text = " ".join(description)
parsed_description = DescriptionTransform()
try:
if not text:
parsed_description.view_definitions = None
return parsed_description
parse_tree = parser.parse(text)
parsed_description.transform(parse_tree)
except (UnexpectedCharacters, UnexpectedEOF, UnexpectedToken):
parsed_description.view_definitions = None
return parsed_description
def parse_semicolon_separated_kv(text: str) -> dict[str, str | list[str]] | None:
if not re.search(r"\w+\s*:\s*[^:]+", text):
return None
result = {}
try:
pairs = text.split(";")
for pair in pairs:
if ":" in pair:
key, value = pair.split(":", 1)
key = key.strip()
values = [v.strip() for v in value.split(",")]
result[key] = values[0] if len(values) == 1 else values
return result
except Exception:
return None
else:
def parse_mvd(description):
return None
class MvdInfo:
def __init__(self, header):
self._header = header
self._parsed = None
def _ensure_parsed(self):
if not LARK_AVAILABLE:
return
if self._parsed is None:
self._parsed = parse_mvd(self._header.file_description.description)
@property
def description(self) -> list[str]:
return self._header.file_description.description
@description.setter
def description(self, new_description: list[str]):
self._header.file_description.description = tuple(new_description)
self._parsed = None
@property
def view_definitions(self):
self._ensure_parsed()
if not self._parsed or self._parsed.view_definitions is None:
return None #
vd = self._parsed.view_definitions
vd_list = vd if isinstance(vd, list) else [vd] if vd else []
return AutoCommitList(
vd_list,
callback=lambda val: (self._update_keyword("ViewDefinition", val), setattr(self, "_parsed", None)),
formatter=lambda lst: ",".join(str(i) for i in lst),
)
@view_definitions.setter
def view_definitions(self, new_value: Union[str, list[str]]):
if isinstance(new_value, list):
value = ", ".join(new_value)
else:
value = str(new_value)
self._update_keyword("ViewDefinition", value)
@property
def comments(self):
self._ensure_parsed()
comments = self._parsed.comments
comment_list = comments if isinstance(comments, list) else [comments] if comments else []
return AutoCommitList(
comment_list,
callback=lambda val: self._update_keyword("Comment", val),
formatter=lambda lst: ", ".join(str(i) for i in lst),
)
@comments.setter
def comments(self, new_value: Union[str, list[str]]):
if isinstance(new_value, list):
value = ", ".join(new_value)
else:
value = str(new_value)
self._update_keyword("Comment", value)
@property
def exchange_requirements(self):
self._ensure_parsed()
return self._parsed.exchange_requirements if self._parsed else None
@exchange_requirements.setter
def exchange_requirements(self, new_value: str):
self._update_keyword("ExchangeRequirement", new_value)
@property
def options(self):
self._ensure_parsed()
if isinstance(self._parsed.options, dict):
return DictionaryHandler(self._parsed.options, self, "Option")
return self._parsed.options if self._parsed else None
@options.setter
def options(self, new_value: str):
self._update_keyword("Option", new_value)
@property
def keywords(self):
self._ensure_parsed()
return self._parsed.keywords if self._parsed else set()
def _update_keyword(self, keyword: str, new_value: str):
updated = False
new_line = f"{keyword} [{new_value}]"
lines = []
for line in self.description:
if line.strip().startswith(f"{keyword} ["):
lines.append(new_line)
updated = True
else:
lines.append(line)
if not updated:
lines.append(new_line)
self.description = lines
def __getattr__(self, name):
self._ensure_parsed()
if hasattr(self._parsed, "_dynamic"):
name_lc = name.lower()
if name_lc in self._parsed._dynamic:
value, original_keyword = self._parsed._dynamic[name_lc]
return DictionaryHandler(value, self, original_keyword)
raise AttributeError(f"'MvdInfo' object has no attribute '{name}'")
def __dir__(self):
base = super().__dir__()
if self._parsed and hasattr(self._parsed, "_dynamic"):
return base + [kw for _, kw in self._parsed._dynamic.values()]
return base
class DictionaryHandler(dict):
def __init__(self, initial_data, mvdinfo, keyword):
super().__init__()
self._mvdinfo = mvdinfo
self._keyword = keyword
for k, v in initial_data.items():
if isinstance(v, list):
super().__setitem__(k, AutoCommitList(v, self._commit))
else:
super().__setitem__(k, v)
def _commit(self):
new_value = "; ".join(f"{k}: {', '.join(v) if isinstance(v, list) else v}" for k, v in self.items())
self._mvdinfo._update_keyword(self._keyword, new_value)
def __setitem__(self, key, value):
if isinstance(value, list):
value = AutoCommitList(value, self._commit)
super().__setitem__(key, value)
self._commit()
def __delitem__(self, key):
super().__delitem__(key)
self._commit()
class AutoCommitList(list):
"ensures keyword attributes are written back to ifcopenshell.file.header"
def __init__(self, iterable, callback, formatter=None):
super().__init__(iterable)
self._callback = callback
self._formatter = formatter
def _commit(self):
if self._formatter:
self._callback(self._formatter(self))
else:
self._callback()
def append(self, item):
super().append(item)
self._commit()
def extend(self, iterable):
super().extend(iterable)
self._commit()
def insert(self, index, item):
super().insert(index, item)
self._commit()
def remove(self, item):
super().remove(item)
self._commit()
def pop(self, index=-1):
item = super().pop(index)
self._commit()
return item
def clear(self):
super().clear()
self._commit()
def __setitem__(self, index, value):
super().__setitem__(index, value)
self._commit()
def __delitem__(self, index):
super().__delitem__(index)
self._commit()
@@ -0,0 +1,237 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
from collections.abc import Iterable
from typing import Literal, Optional
import numpy as np
import numpy.typing as npt
import ifcopenshell
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixType:
"""Converts a location, X, and Z axis vector to a 4x4 transformation matrix
IFC uses a right-handed coordinate system, so it is not necessary to
provide the Y axis.
:param o: The origin (i.e. location) of the matrix
:param z: The +Z vector / axis of the matrix
:param x: The +X vector / axis of the matrix
:return: A 4x4 numpy matrix
"""
x = x / np.linalg.norm(x)
z = z / np.linalg.norm(z)
y = np.cross(z, x)
y = y / np.linalg.norm(y)
r = np.eye(4)
r[:-1, :-1] = x, y, z
r[-1, :-1] = o
return r.T
def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
"""Parses an IfcAxis2Placement (2D or 3D) to a 4x4 transformation matrix
Note that this function only parses a single placement axis. If you want to
get the placement of an element instead, element placements often are made
out of multiple placement axes or other alternative placement methods. You
should use ``get_local_placement`` instead.
:param placement: The IfcLocalPlacement enitity
:return: A 4x4 numpy matrix
"""
ifc_class = placement.is_a()
if ifc_class in ("IfcAxis2Placement3D", "IfcAxis2PlacementLinear"):
z = np.array(placement.Axis.DirectionRatios if placement.Axis else (0, 0, 1))
x = np.array(placement.RefDirection.DirectionRatios if placement.RefDirection else (1, 0, 0))
location = placement.Location
if coordinates := getattr(location, "Coordinates", None):
o = coordinates
else:
import ifcopenshell.geom
settings = ifcopenshell.geom.settings()
settings.set("convert-back-units", True)
shape = ifcopenshell.geom.create_shape(settings, placement)
return np.array(shape.matrix).reshape((4, 4), order="F")
elif ifc_class == "IfcAxis2Placement2D":
z = np.array((0, 0, 1))
if placement.RefDirection:
x = np.array(placement.RefDirection.DirectionRatios)
x.resize(3)
else:
x = np.array((1, 0, 0))
o = (*placement.Location.Coordinates, 0.0)
elif ifc_class == "IfcAxis1Placement":
axis = placement.Axis
z = np.array(axis.DirectionRatios if axis else (0, 0, 1))
x = np.array((1, 0, 0))
o = placement.Location.Coordinates
return a2p(o, z, x)
def get_local_placement(placement: Optional[ifcopenshell.entity_instance] = None) -> MatrixType:
"""Parse a local placement into a 4x4 transformation matrix
This is typically used to find the location and rotation of an element. The
transformation matrix takes the form of:
.. code::
[ [ x_x, y_x, z_x, x ]
[ x_y, y_y, z_y, y ]
[ x_z, y_z, z_z, z ]
[ 0.0, 0.0, 0.0, 1.0 ] ]
Example:
.. code:: python
placement = file.by_type("IfcBeam")[0].ObjectPlacement
matrix = ifcopenshell.util.placement.get_local_placement(placement)
:param placement: The IfcLocalPlacement entity
:return: A 4x4 numpy matrix
"""
if placement is None:
return np.eye(4)
if (rel_to := placement.PlacementRelTo) is None:
parent = np.eye(4)
else:
parent = get_local_placement(rel_to)
return np.dot(parent, get_axis2placement(placement.RelativePlacement))
def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) -> MatrixType:
"""Parses an IfcCartesianTransformationOperator into a 4x4 transformation matrix
Note that in general you will not need to call this directly. See
``get_mappeditem_transformation`` instead.
:param item: The IfcCartesianTransformationOperator entity
:return: A 4x4 numpy transformation matrix
"""
origin = np.array(inst.LocalOrigin.Coordinates)
axis1 = np.array((1.0, 0.0, 0.0))
axis2 = np.array((0.0, 1.0, 0.0))
axis3 = np.array((0.0, 0.0, 1.0))
if inst.Axis1:
axis1[0:3] = inst.Axis1.DirectionRatios
if inst.Axis2:
axis2[0:3] = inst.Axis2.DirectionRatios
if inst.Axis3:
axis3[0:3] = inst.Axis3.DirectionRatios
m4 = a2p(origin, axis3, axis1)
# Negate axis2 (introduce mirroring) when supplied axis2
# is opposite of constructed axis2, but remains orthogonal
if m4.T[1][0:3].dot(axis2) < 0.0:
m4.T[1] *= -1.0
scale1 = scale2 = scale3 = 1.0
if inst.Scale:
scale1 = inst.Scale
if inst.is_a("IfcCartesianTransformationOperator3DnonUniform"):
scale2 = inst.Scale2 if inst.Scale2 is not None else scale1
scale3 = inst.Scale3 if inst.Scale3 is not None else scale1
else:
scale2 = scale3 = scale1
m4.T[0] *= scale1
m4.T[1] *= scale2
m4.T[2] *= scale3
return m4
def get_mappeditem_transformation(item: ifcopenshell.entity_instance) -> MatrixType:
"""Parse an IfcMappedItem into a 4x4 transformation matrix
Mapped items take a representation with an origin and transform them with a
cartesian transformation operation. This function returns the final
transformation matrix.
:param item: The IfcMappedItem entity
:return: A 4x4 numpy transformation matrix
"""
m4 = get_axis2placement(item.MappingSource.MappingOrigin)
# TODO 2d
if item.MappingTarget.is_a("IfcCartesianTransformationOperator3D"):
return get_cartesiantransformationoperator3d(item.MappingTarget) @ m4
def get_storey_elevation(storey: ifcopenshell.entity_instance) -> float:
"""Get the Z elevation in project units of a buildling storey
Building storeys store elevation in two possible locations: the Z value of
its placement, or as a fallback the ``Elevation`` attribute.
:param storey: The IfcBuildingStorey entity
:return: The elevation in project units
"""
if storey.ObjectPlacement:
matrix = get_local_placement(storey.ObjectPlacement)
return matrix[2][3]
return getattr(storey, "Elevation", 0.0) or 0.0
def rotation(angle: float, axis: Literal["X", "Y", "Z"], is_degrees=True) -> MatrixType:
"""Create a 4x4 numpy matrix representing an euler rotation
:param angle: The angle of rotation
:param axis: The axis to rotate around, either X, Y, or Z.
:param is_degrees: Whether or not the angle is specified in degrees or
radians. Defaults to true (i.e. degrees).
:return: A 4x4 numpy rotation matrix
"""
theta = np.radians(angle) if is_degrees else angle
cos, sin = np.cos(theta), np.sin(theta)
# fmt: off
if axis == "X":
return np.array([
[1, 0, 0, 0],
[0, cos, -sin, 0],
[0, sin, cos, 0],
[0, 0, 0, 1]
])
elif axis == "Y":
return np.array([
[cos, 0, sin, 0],
[0, 1, 0, 0],
[-sin, 0, cos, 0],
[0, 0, 0, 1]
])
elif axis == "Z":
return np.array([
[cos, -sin, 0, 0],
[sin, cos, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
])
# fmt: on
@@ -0,0 +1,35 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
from timeit import default_timer as timer
class Profiler:
"""
A python context manager timing utility, useful for measure functions performances
"""
def __init__(self, task):
self.task = task
def __enter__(self):
self.start = timer()
def __exit__(self, *args):
print(f"{self.task} {timer() - self.start:.6f} s")
@@ -0,0 +1,250 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
import pathlib
import re
from functools import lru_cache
from typing import Literal, NamedTuple, Optional, Union
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.schema
import ifcopenshell.util.type
from ifcopenshell.entity_instance import entity_instance
templates: dict[ifcopenshell.util.schema.IFC_SCHEMA, "PsetQto"] = {}
def get_template(schema_identiier: str) -> "PsetQto":
"""
:param schema_identiier: As in ``file.schema_identifier``, not ``file.schema``.
"""
global templates
schema = ifcopenshell.util.schema.get_fallback_schema(schema_identiier)
if schema not in templates:
templates[schema] = PsetQto(schema)
return templates[schema]
class PsetQto:
# fmt: off
templates_path: dict[ifcopenshell.util.schema.IFC_SCHEMA, str] = {
"IFC2X3": "Pset_IFC2X3.ifc",
"IFC4": "Pset_IFC4_ADD2.ifc",
"IFC4X3": "Pset_IFC4X3.ifc"
}
# fmt: on
templates: list[ifcopenshell.file]
def __init__(
self,
schema: ifcopenshell.util.schema.IFC_SCHEMA,
templates: Optional[list[ifcopenshell.file]] = None,
) -> None:
self.schema = ifcopenshell.schema_by_name(schema)
if not templates:
folder_path = pathlib.Path(__file__).parent.absolute()
path = str(folder_path.joinpath("schema", self.templates_path[schema]))
ifc_file: ifcopenshell.file = ifcopenshell.open(path)
templates = [ifc_file]
# See bug 3583. We backport this change from IFC4X3 because it just makes sense.
# Users aren't forced to use it.
if schema == "IFC4":
for element in templates[0].by_type("IfcPropertySetTemplate"):
if element.TemplateType == "QTO_OCCURRENCEDRIVEN":
element.TemplateType = "QTO_TYPEDRIVENOVERRIDE"
self.templates = templates
@lru_cache
def get_applicable(
self,
ifc_class="",
predefined_type="",
pset_only=False,
qto_only=False,
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
) -> list[entity_instance]:
"""Get applicable property set templates."""
any_class = not ifc_class
entity = None
if not any_class:
entity = self.schema.declaration_by_name(ifc_class).as_entity()
assert entity
result = []
for template in self.templates:
for prop_set in template.by_type("IfcPropertySetTemplate"):
if pset_only:
if prop_set.TemplateType and prop_set.TemplateType.startswith("QTO_"):
continue
if qto_only:
if prop_set.TemplateType and prop_set.TemplateType.startswith("PSET_"):
continue
if any_class or (
entity
and self.is_applicable(
entity, prop_set.ApplicableEntity or "IfcRoot", predefined_type, prop_set.TemplateType, schema
)
):
result.append(prop_set)
return result
@lru_cache
def get_applicable_names(
self,
ifc_class: str,
predefined_type: str = "",
pset_only: bool = False,
qto_only: bool = False,
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
) -> list[str]:
"""Return names instead of objects for other use eg. enum"""
return [
prop_set.Name for prop_set in self.get_applicable(ifc_class, predefined_type, pset_only, qto_only, schema)
]
def is_applicable(
self,
entity: W.entity,
applicables: str,
predefined_type: str = "",
template_type: str = "NOTDEFINED",
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
) -> bool:
"""
applicables can have multiple possible patterns :
.. code-block:: text
IfcBoilerType (IfcClass)
IfcBoilerType/STEAM (IfcClass/PREDEFINEDTYPE)
IfcBoilerType[PerformanceHistory] (IfcClass[PerformanceHistory])
IfcBoilerType/STEAM[PerformanceHistory] (IfcClass/PREDEFINEDTYPE[PerformanceHistory])
"""
for applicable in applicables.split(","):
match = re.match(r"(\w+)(\[\w+\])*/*(\w+)*(\[\w+\])*", applicable)
if not match:
continue
# Uncomment if usage found
# applicable_perf_history = match.group(2) or match.group(4)
matched_type = match.group(3)
if matched_type and not predefined_type:
continue
# Case insensitive to handle things like material categories
elif matched_type and predefined_type.lower() != match.group(3).lower():
continue
applicable_class = match.group(1)
if ifcopenshell.util.schema.is_a(entity, applicable_class):
return True
# There is an implementer agreement that if the template type is
# type based, the type need not be explicitly mentioned
# https://github.com/buildingSMART/IFC4.3.x-development/issues/22
# This will be fixed in IFC4.3
template_type = template_type or ""
if "TYPE" in template_type and ifcopenshell.util.schema.is_a(entity, "IfcTypeObject"):
types = ifcopenshell.util.type.get_applicable_types(applicable_class, schema)
if not types:
# Abstract classes will not have an "applicable type" but
# the implementer agreement still applies to them.
occurrence_class = None
try:
occurrence_class = self.schema.declaration_by_name(applicable_class + "Type")
except:
try:
occurrence_class = self.schema.declaration_by_name("IfcType" + applicable_class[3:])
except:
pass
if occurrence_class:
types = [occurrence_class.name()]
for ifc_type in types:
if ifcopenshell.util.schema.is_a(entity, ifc_type):
return True
return False
@lru_cache
def get_by_name(self, name: str) -> Optional[entity_instance]:
for template in self.templates:
for prop_set in template.by_type("IfcPropertySetTemplate"):
if prop_set.Name == name:
return prop_set
return None
def is_templated(self, name: str) -> bool:
return bool(self.get_by_name(name))
def get_pset_template_type(pset_template: entity_instance) -> Literal["PSET", "QTO", None]:
"""Get the type of the pset template.
If type is mixed or not defined, return None."""
# Try to identify whether it's pset or qto from the template type.
template_type = pset_template.TemplateType
if template_type:
if template_type.startswith("PSET_"):
return "PSET"
elif template_type.startswith("QTO_"):
return "QTO"
# Can also be 'NOTDEFINED'.
pset_types = set()
for prop in pset_template.HasPropertyTemplates:
prop_template_type = prop.TemplateType
if prop_template_type:
if prop_template_type.startswith("P_"):
pset_types.add("PSET")
else: # All other values are Q_.
pset_types.add("QTO")
pset_type = next(iter(pset_types)) if len(pset_types) == 1 else None
return pset_type
class ApplicableEntity(NamedTuple):
value: str
ifc_class: str
predefined_type: Union[str, None]
performance_history: bool
def parse_applicable_entity(applicable_entity: str) -> list[ApplicableEntity]:
"""Parse ApplicableEntity string query to tuples.
:param applicable_entity: IfcPropertySetTemplate.ApplicableEntity query.
:return: List of ApplicableEntity tuples.
"""
items: list[ApplicableEntity] = []
for item in applicable_entity.split(","):
value = item
item, predefined_type = parts if len(parts := item.split("/")) > 1 else (item, None)
ifc_class, performance_history = (parts[0], True) if len(parts := item.split("[")) > 1 else (item, False)
items.append(ApplicableEntity(value, ifc_class, predefined_type, performance_history))
return items
def convert_applicable_entities_to_query(applicable_entities: list[ApplicableEntity]) -> str:
"""Get query supported by :func:`ifcopenshell.util.selector.filter_elements`."""
parts: list[str] = []
for entity in applicable_entities:
# NOTE: selector currently doesn't support checking if element has performance history.
part = entity.ifc_class
if entity.predefined_type:
part += f', PredefinedType="{entity.predefined_type}"'
parts.append(part)
return " + ".join(parts)
@@ -0,0 +1,523 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
from collections.abc import Generator, Sequence
from typing import Literal, Optional, TypedDict, Union
import numpy as np
import numpy.typing as npt
import ifcopenshell
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
CONTEXT_TYPE = Literal["Model", "Plan", "NotDefined"]
REPRESENTATION_IDENTIFIER = Literal[
"CoG",
"Box",
"Annotation",
"Axis",
"FootPrint",
"Profile",
"Surface",
"Reference",
"Body",
"Body-Fallback",
"Clearance",
"Lighting",
]
TARGET_VIEW = Literal[
"ELEVATION_VIEW",
"GRAPH_VIEW",
"MODEL_VIEW",
"PLAN_VIEW",
"REFLECTED_PLAN_VIEW",
"SECTION_VIEW",
"SKETCH_VIEW",
"USERDEFINED",
"NOTDEFINED",
]
def get_context(
ifc_file: ifcopenshell.file,
context: CONTEXT_TYPE,
subcontext: Optional[REPRESENTATION_IDENTIFIER] = None,
target_view: Optional[TARGET_VIEW] = None,
) -> Union[ifcopenshell.entity_instance, None]:
"""Get IfcGeometricRepresentationSubContext by the provided context type, identifier, and target view.
:param context: ContextType.
:param subcontext: A ContextIdentifier string, or any if left blank.
:param target_view: A TargetView string, or any if left blank.
"""
if subcontext or target_view:
elements = ifc_file.by_type("IfcGeometricRepresentationSubContext")
else:
elements = ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
for element in elements:
if context and element.ContextType != context:
continue
if subcontext and getattr(element, "ContextIdentifier") != subcontext:
continue
if target_view and getattr(element, "TargetView") != target_view:
continue
return element
def is_representation_of_context(
representation: ifcopenshell.entity_instance,
context: Union[ifcopenshell.entity_instance, CONTEXT_TYPE],
subcontext: Optional[REPRESENTATION_IDENTIFIER] = None,
target_view: Optional[TARGET_VIEW] = None,
) -> bool:
"""Check if representation has specified context or context type, identifier, and target view.
:param representation: IfcShapeRepresentation.
:param context: Either a specific IfcGeometricRepresentationContext or a ContextType.
:param subcontext: A ContextIdentifier string, or any if left blank.
:param target_view: A TargetView string, or any if left blank.
"""
if isinstance(context, ifcopenshell.entity_instance):
return representation.ContextOfItems == context
if target_view is not None:
return (
representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext")
and representation.ContextOfItems.TargetView == target_view
and representation.ContextOfItems.ContextIdentifier == subcontext
and representation.ContextOfItems.ContextType == context
)
elif subcontext is not None:
return (
representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext")
and representation.ContextOfItems.ContextIdentifier == subcontext
and representation.ContextOfItems.ContextType == context
)
return representation.ContextOfItems.ContextType == context
def get_representations_iter(
element: ifcopenshell.entity_instance,
) -> Generator[ifcopenshell.entity_instance, None, None]:
"""Get an iterator with element's IfcShapeRepresentations.
:param element: An IfcProduct or IfcTypeProduct
"""
if element.is_a("IfcProduct") and (rep := element.Representation):
for r in rep.Representations:
yield r
elif element.is_a("IfcTypeProduct") and (maps := element.RepresentationMaps):
for r in maps:
yield r.MappedRepresentation
def get_representation(
element: ifcopenshell.entity_instance,
context: Union[ifcopenshell.entity_instance, CONTEXT_TYPE],
subcontext: Optional[REPRESENTATION_IDENTIFIER] = None,
target_view: Optional[TARGET_VIEW] = None,
) -> Union[ifcopenshell.entity_instance, None]:
"""Gets a IfcShapeRepresentation filtered by the context type, identifier, and target view
:param element: An IfcProduct or IfcTypeProduct
:param context: Either a specific IfcGeometricRepresentationContext or a ContextType
:param subcontext: A ContextIdentifier string, or any if left blank.
:param target_view: A TargetView string, or any if left blank.
:return: The first IfcShapeRepresentation matching the criteria.
"""
for r in get_representations_iter(element):
if is_representation_of_context(r, context, subcontext, target_view):
return r
def guess_type(items: Sequence[ifcopenshell.entity_instance]) -> Union[str, None]:
"""Guesses the appropriate RepresentationType attribute based on a list of items
:param items: A list of IfcRepresentationItem, typically in an IfcShapeRepresentation
:return: The appropriate RepresentationType value, or None if no valid value
"""
if all([True if i.is_a("IfcMappedItem") else False for i in items]):
return "MappedRepresentation"
elif all([True if i.is_a("IfcPoint") or i.is_a("IfcCartesianPointList") else False for i in items]):
return "Point"
elif all([True if i.is_a("IfcCartesianPointList3d") else False for i in items]):
return "PointCloud"
elif all([True if i.is_a("IfcCurve") and i.Dim == 2 else False for i in items]):
return "Curve2D"
elif all([True if i.is_a("IfcCurve") and i.Dim == 3 else False for i in items]):
return "Curve3D"
elif all([True if i.is_a("IfcCurve") else False for i in items]):
return "Curve"
elif all([True if i.is_a("IfcSegment") else False for i in items]):
return "Segment"
elif all([True if i.is_a("IfcSurface") and i.Dim == 2 else False for i in items]):
return "Surface2D"
elif all([True if i.is_a("IfcSurface") and i.Dim == 3 else False for i in items]):
return "Surface3D"
elif all([True if i.is_a("IfcSurface") else False for i in items]):
return "Surface"
elif all([True if i.is_a("IfcSectionedSurface") else False for i in items]):
return "SectionedSurface"
elif all([True if i.is_a("IfcAnnotationFillArea") else False for i in items]):
return "FillArea"
elif all([True if i.is_a("IfcTextLiteral") else False for i in items]):
return "Text"
elif all([True if i.is_a("IfcBSplineSurface") else False for i in items]):
return "AdvancedSurface"
elif all(
[
(
True
if i.is_a("IfcGeometricSet") or i.is_a("IfcPoint") or i.is_a("IfcCurve") or i.is_a("IfcSurface")
else False
)
for i in items
]
):
return "GeometricSet"
elif all(
[
(
True
if i.is_a("IfcGeometricCurveSet")
or (i.is_a("IfcGeometricSet") and all([e.is_a("IfcSurface") for e in i.Elements]))
or i.is_a("IfcPoint")
or i.is_a("IfcCurve")
else False
)
for i in items
]
):
return "GeometricCurveSet"
elif all(
[
(
True
if i.is_a("IfcPoint")
or i.is_a("IfcCurve")
or i.is_a("IfcGeometricCurveSet")
or i.is_a("IfcAnnotationFillArea")
or i.is_a("IfcTextLiteral")
else False
)
for i in items
]
):
return "Annotation2D"
elif all([True if i.is_a("IfcTessellatedItem") else False for i in items]):
return "Tessellation"
elif all(
[
(
True
if i.is_a("IfcTessellatedItem")
or i.is_a("IfcShellBasedSurfaceModel")
or i.is_a("IfcFaceBasedSurfaceModel")
else False
)
for i in items
]
):
return "SurfaceModel"
elif all(
[True if i.is_a() == "IfcExtrudedAreaSolid" or i.is_a() == "IfcRevolvedAreaSolid" else False for i in items]
):
return "SweptSolid"
elif all([True if i.is_a("IfcSolidModel") else False for i in items]):
return "SolidModel"
elif all(
[
(
True
if i.is_a("IfcTessellatedItem")
or i.is_a("IfcShellBasedSurfaceModel")
or i.is_a("IfcFaceBasedSurfaceModel")
or i.is_a("IfcSolidModel")
else False
)
for i in items
]
):
return "SurfaceOrSolidModel"
elif all(
[
(
True
if i.is_a("IfcSweptAreaSolid") or i.is_a("IfcSweptDiskSolid") or i.is_a("IfcSectionedSolidHorizontal")
else False
)
for i in items
]
):
return "AdvancedSweptSolid"
elif all([True if i.is_a("IfcCsgSolid") or i.is_a("IfcBooleanClippingResult") else False for i in items]):
return "Clipping"
elif all(
[
True if i.is_a("IfcBooleanResult") or i.is_a("IfcCsgPrimitive3d") or i.is_a("IfcCsgSolid") else False
for i in items
]
):
return "CSG"
elif all([True if i.is_a("IfcFacetedBrep") else False for i in items]):
return "Brep"
elif all([True if i.is_a("IfcManifoldSolidBrep") else False for i in items]):
return "AdvancedBrep"
elif all([True if i.is_a("IfcBoundingBox") else False for i in items]):
return "BoundingBox"
elif all([True if i.is_a("IfcSectionedSpine") else False for i in items]):
return "SectionedSpine"
elif all([True if i.is_a("IfcLightSource") else False for i in items]):
return "LightSource"
elif all([True if i.is_a("IfcVertex") else False for i in items]):
return "Vertex"
elif all([True if i.is_a("IfcEdge") else False for i in items]):
return "Edge"
elif all([True if i.is_a("IfcPath") else False for i in items]):
return "Path"
elif all([True if i.is_a("IfcFace") else False for i in items]):
return "Face"
elif all([True if i.is_a("IfcOpenShell") else False for i in items]):
return "Shell"
def resolve_representation(representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Resolve possibly mapped representation.
:param representation: IfcRepresentation
:return: Representation resolved from mappings
"""
# Tekla 2023 has missing items and mapped representation, though it's invalid IFC.
if (
len(representation.Items or []) == 1
and representation.Items[0].is_a("IfcMappedItem")
and (mapped_rep := representation.Items[0].MappingSource.MappedRepresentation)
):
return resolve_representation(mapped_rep)
return representation
class ResolvedItemDict(TypedDict):
matrix: npt.NDArray[np.float64]
item: ifcopenshell.entity_instance
def resolve_items(
representation: ifcopenshell.entity_instance, matrix: Optional[npt.NDArray[np.float64]] = None
) -> list[ResolvedItemDict]:
if matrix is None:
matrix = np.eye(4)
results: list[ResolvedItemDict] = []
for item in representation.Items or []: # Be forgiving of invalid IFCs because Revit :(
if item.is_a("IfcMappedItem"):
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(item)
if not np.allclose(rep_matrix, np.eye(4)):
rep_matrix = rep_matrix @ matrix.copy()
results.extend(resolve_items(item.MappingSource.MappedRepresentation, rep_matrix))
else:
results.append(ResolvedItemDict(matrix=matrix.copy(), item=item))
return results
def resolve_base_items(
representation: ifcopenshell.entity_instance,
) -> Generator[ifcopenshell.entity_instance, None, None]:
"""Resolve representation to it's base items resolving mapped items and boolean results to it's operands."""
queue: list[ifcopenshell.entity_instance] = list(representation.Items)
while queue:
item = queue.pop()
if item.is_a("IfcMappedItem"):
yield from resolve_base_items(item.MappingSource.MappedRepresentation)
elif item.is_a("IfcBooleanResult"):
queue.append(item.FirstOperand)
queue.append(item.SecondOperand)
else:
yield item
def get_prioritised_contexts(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]:
"""Gets a list of contexts ordered from high priority to low priority
Models can contain multiple geometric contexts. When visualising models,
you may want to prioritise visualising certain contexts over others,
determined by the context type, identifier, target view, and target scale.
The default prioritises 3D, then 2D. It then prioritises subcontexts, then
contexts. It then prioritises bodies, then others. It also prioritises
model views, then plan views, then others.
:param ifc_file: The model containing contexts
:return: A list of IfcGeometricRepresentationContext (or SubContext) from
high priority to low priority.
"""
# Annotation ContextType is to accommodate broken Revit files
# See https://github.com/Autodesk/revit-ifc/issues/187
type_priority = ["Model", "Plan", "Annotation"]
identifier_priority = [
"Body",
"Body-FallBack",
"Facetation",
"FootPrint",
"Profile",
"Surface",
"Reference",
"Axis",
"Clearance",
"Box",
"Lighting",
"Annotation",
"CoG",
]
target_view_priority = [
"MODEL_VIEW",
"PLAN_VIEW",
"REFLECTED_PLAN_VIEW",
"ELEVATION_VIEW",
"SECTION_VIEW",
"GRAPH_VIEW",
"SKETCH_VIEW",
"USERDEFINED",
"NOTDEFINED",
]
def sort_context(context):
priority = []
if context.ContextType in type_priority:
priority.append(len(type_priority) - type_priority.index(context.ContextType))
else:
priority.append(0)
if context.ContextIdentifier in identifier_priority:
priority.append(len(identifier_priority) - identifier_priority.index(context.ContextIdentifier))
else:
priority.append(0)
if getattr(context, "TargetView", None) in target_view_priority:
priority.append(len(target_view_priority) - target_view_priority.index(context.TargetView))
else:
priority.append(0)
priority.append(getattr(context, "TargetScale", None) or 0) # Big then small
return tuple(priority)
return sorted(ifc_file.by_type("IfcGeometricRepresentationContext"), key=sort_context, reverse=True)
def get_part_of_product(
element: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
"""Gets the product definition or representation map of an element
This is typically used for setting shape aspects. Note that this will
return None for IFC2X3 element types.
:param element: An IfcProduct or IfcTypeProduct
:param context: A IfcGeometricRepresentationContext
:return: IfcProductRepresentationSelect
"""
if element.is_a("IfcProduct"):
return element.Representation
elif element.is_a("IfcTypeProduct") and element.file.schema != "IFX2X3":
if maps := [r for r in element.RepresentationMaps if r.MappedRepresentation.ContextOfItems == context]:
return maps[0]
def get_item_shape_aspect(
representation: ifcopenshell.entity_instance, item: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
"""Gets the shape aspect relating to an item
:param representation: The IfcShapeRepresentation that the item is part of
:param item: The IfcRepresentationItem you want to get the shape aspect of
:return: IfcShapeAspect, or None if none exists
"""
for inverse in item.file.get_inverse(item):
if (
inverse.is_a("IfcShapeRepresentation")
and inverse.ContextOfItems == representation.ContextOfItems
and (of_shape_aspect := inverse.OfShapeAspect)
):
return of_shape_aspect[0]
def get_material_style(
material: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance, ifc_class: str = "IfcSurfaceStyle"
) -> Union[ifcopenshell.entity_instance, None]:
"""Get a presentation style associated with a material
:param material: the IfcMaterial
:param context: IfcGeometricRepresentationContext that the style belongs to
:param ifc_class: The class name of the type of style you need, typically
IfcSurfaceStyle for 3D styling.
:return: IfcPresentationStyle
"""
if definition_representation := material.HasRepresentation:
for styled_rep in definition_representation[0].Representations:
if styled_rep.ContextOfItems == context:
for item in styled_rep.Items:
for style in item.Styles:
if style.is_a(ifc_class):
return style
def get_reference_line(wall: ifcopenshell.entity_instance, fallback_length: float = 1.0) -> list[npt.NDArray]:
"""Fetch the reference axis that goes in the +X direction
A base line will then be offset from this reference line based on the
material usage. From that base line, the layer thicknesses will offset
again, and be extruded to form the body representation.
:param wall: ifcopenshell.entity_instance
:param fallback_length: If there is no reference axis, assume it starts at
the object placement (i.e. 0.0, 0.0) and extends for this fallback
length along the +X axis.
:return: A list of two 2D coordinates representing the start and end of the
axis. The axis always goes in the +X direction.
"""
if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"):
for item in ifcopenshell.util.representation.resolve_representation(axis).Items:
if item.is_a("IfcPolyline"):
points = [p[0] for p in item.Points]
elif item.is_a("IfcIndexedPolyCurve"):
points = item.Points.CoordList
else:
continue
if points[0][0] < points[1][0]: # An axis always goes in the +X direction
return [np.array(points[0]), np.array(points[1])]
return [np.array(points[1]), np.array(points[0])]
elif extrusions := ifcopenshell.util.shape.get_base_extrusions(wall):
for extrusion in extrusions:
profile = extrusion.SweptArea
curve = getattr(profile, "OuterCurve", None)
if not curve:
continue
elif curve.is_a("IfcPolyline"):
x = [p[0][0] for p in curve.Points]
elif curve.is_a("IfcIndexedPolyCurve"):
x = [p[0] for p in curve.Points.CoordList]
else:
continue
return [np.array((min(x), 0.0)), np.array((max(x), 0.0))]
return [np.array((0.0, 0.0)), np.array((fallback_length, 0.0))]
@@ -0,0 +1,171 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
from typing import Any, Union
import ifcopenshell.util.cost
import ifcopenshell.util.date
import ifcopenshell.util.element
PRODUCTIVITY_PSET_DATA = Union[dict[str, Any], None]
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcConstructionResource.htm#Table-7.3.3.7.1.3.H
RESOURCES_TO_QUANTITIES: dict[str, tuple[str, ...]] = {
"IfcCrewResource": ("IfcQuantityTime",),
"IfcLaborResource": ("IfcQuantityTime",),
"IfcSubContractResource": ("IfcQuantityTime",),
"IfcConstructionEquipmentResource": ("IfcQuantityTime",),
"IfcConstructionMaterialResource": (
"IfcQuantityVolume",
"IfcQuantityArea",
"IfcQuantityLength",
"IfcQuantityWeight",
),
"IfcConstructionProductResource": ("IfcQuantityCount",),
}
def get_productivity(resource: ifcopenshell.entity_instance, should_inherit: bool = True) -> PRODUCTIVITY_PSET_DATA:
productivity = ifcopenshell.util.element.get_psets(resource).get("EPset_Productivity", None)
if should_inherit and not productivity:
# Note: This is not part of the Schema - but it makes sense to inherit from parent
productivity = get_parent_productivity(resource)
return productivity
def get_parent_productivity(resource: ifcopenshell.entity_instance) -> PRODUCTIVITY_PSET_DATA:
if not resource.Nests:
return
else:
parent_resource = resource.Nests[0].RelatingObject
productivity = ifcopenshell.util.element.get_psets(parent_resource).get("EPset_Productivity", None)
return productivity
def get_unit_consumed(productivity: PRODUCTIVITY_PSET_DATA) -> Union[Any, None]:
duration = productivity.get("BaseQuantityConsumed", None)
if not duration:
return
return ifcopenshell.util.date.ifc2datetime(duration)
def get_quantity_produced(productivity: PRODUCTIVITY_PSET_DATA) -> float:
if not productivity:
return 0.0
return productivity.get("BaseQuantityProducedValue", 0.0)
def get_quantity_produced_name(productivity: PRODUCTIVITY_PSET_DATA):
if not productivity:
return ""
return productivity.get("BaseQuantityProducedName", "")
def get_total_quantity_produced(resource: ifcopenshell.entity_instance, quantity_name_in_process: str) -> float:
def get_product_quantity(product: ifcopenshell.entity_instance, quantity_name: str):
psets = ifcopenshell.util.element.get_psets(product)
for pset in psets.values():
for name, value in pset.items():
if name == quantity_name:
return float(value)
total = 0.0
products = get_parametric_resource_products(resource)
if quantity_name_in_process == "Count":
total = len(products)
else:
for product in products:
total += get_product_quantity(product, quantity_name_in_process) or 0
return total
def get_parametric_resource_products(resource: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
products = []
for rel in resource.HasAssignments or []:
if not rel.is_a("IfcRelAssignsToProcess"):
continue
for rel2 in rel.RelatingProcess.HasAssignments or []:
if not rel2.is_a("IfcRelAssignsToProduct"):
continue
products.append(rel2.RelatingProduct)
return products
def get_task_assignments(resource: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
for rel in resource.HasAssignments or []:
if not rel.is_a("IfcRelAssignsToProcess"):
continue
return rel.RelatingProcess
def get_resource_required_work(resource: ifcopenshell.entity_instance) -> Union[str, None]:
productivity = get_productivity(resource)
if productivity:
quantity_produced = get_quantity_produced(productivity)
time_consumed = get_unit_consumed(productivity)
quantity_name_in_process = get_quantity_produced_name(productivity)
total_quantity_to_produce = get_total_quantity_produced(resource, quantity_name_in_process)
if not time_consumed or not quantity_produced or not total_quantity_to_produce:
return
iso_string = ""
if "T" in productivity.get("BaseQuantityConsumed", ""):
seconds = (time_consumed.days * 24 * 60 * 60) + time_consumed.seconds
productivity_ratio = seconds / quantity_produced
required_work = total_quantity_to_produce * productivity_ratio
iso_string = f"PT{required_work / 60 / 60}H"
else:
days = time_consumed.days + (time_consumed.seconds / (24 * 60 * 60))
productivity_ratio = days / quantity_produced
required_work = total_quantity_to_produce * productivity_ratio
iso_string = f"P{required_work}D"
return iso_string
def get_nested_resources(resource: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [object for rel in resource.IsNestedBy or [] for object in rel.RelatedObjects]
def get_cost(resource: ifcopenshell.entity_instance) -> tuple[Union[float, None], Union[str, None]]:
"""Get cost data for IfcConstructionResource.
:return: a tuple of cost and unit.
"""
cost, unit = None, None
if base_costs := resource.BaseCosts:
costs = [ifcopenshell.util.cost.calculate_applied_value(resource, cost_value) for cost_value in base_costs]
cost = sum(costs)
unit_basis = next((unit_basis for cost_value in base_costs if (unit_basis := cost_value.UnitBasis)), None)
if unit_basis and (unit_component := unit_basis.UnitComponent).is_a("IfcConversionBasedUnit"):
unit = unit_component.Name
return cost, unit
def get_quantity(resource: ifcopenshell.entity_instance) -> float:
if resource.Usage and resource.Usage.ScheduleWork:
duration = ifcopenshell.util.date.ifc2datetime(resource.Usage.ScheduleWork)
return duration.total_seconds() / 3600
# TODO: is it safe to assume None quantity should be treated as 1.0? See #910 in ifc4x3dev.
quantity = resource.BaseQuantity
return 1.0 if quantity is None else quantity[3]
def get_parent_cost(resource: ifcopenshell.entity_instance) -> Union[tuple[Union[float, None], Union[str, None]], None]:
if not (nests := resource.Nests):
return
else:
cost = get_cost(nests[0].RelatingObject)
return cost
@@ -0,0 +1,562 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
import json
import os
import time
from typing import Any, Literal, Union
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.util.attribute
# This is highly experimental and incomplete, however, it may work for simple datasets.
cwd = os.path.dirname(os.path.realpath(__file__))
IFC_SCHEMA = Literal["IFC2X3", "IFC4", "IFC4X3"]
def get_fallback_schema(version: str) -> IFC_SCHEMA:
"""Fallback to the schema version we do have docs and mapping for.
Needed to support IFC versions like 4X3_RC1, 4X1 etc.
:param version: Typically a string from ``ifcopenshell.file.schema_identifier``, e.g. IFC4X3_ADD2
"""
if version.startswith("IFC4X3"):
version = "IFC4X3"
elif version.startswith("IFC4"):
version = "IFC4"
elif version.startswith("IFC2X3"):
version = "IFC2X3"
else:
assert False, f"Unexpected schema version: {version}."
return version
def get_declaration(element: ifcopenshell.entity_instance):
"""Get the schema declaration of an actively used entity instance
IFC models are made out of instances (e.g. with a STEP ID) of entities
(e.g. IfcWall). Those entities are defined through a **Schema
Declaration**.
**Schema Declaration** objects can be used to query information about the
IFC schema itself, such as data types, enumeration values, and inheritance.
:param element: Any instance, typically from a loaded or created IFC model
Example:
.. code:: python
wall = model.createIfcWall()
declaration = ifcopenshell.util.schema.get_declaration(wall)
print(declaration.name()) # IfcWall
print(declaration.is_abstract()) # False
print(declaration.supertype().name()) # IfcBuildingElement
"""
return element.wrapped_data.declaration().as_entity()
def is_a(declaration: ifcopenshell.ifcopenshell_wrapper.declaration, ifc_class: str) -> bool:
"""Checks if a schema declaration is a class
:param declaration: The declaration from the schema.
:param ifc_class: A case insensitive IFC class name (e.g. IfcRoot)
:return: True is the declaration is of that class
Example:
.. code:: python
wall = model.createIfcWall()
declaration = ifcopenshell.util.schema.get_declaration(wall)
ifcopenshell.util.schema.is_a(declaration, "IfcRoot") # True
"""
return declaration._is(ifc_class)
def get_supertypes(
declaration: ifcopenshell.ifcopenshell_wrapper.entity,
) -> list[ifcopenshell.ifcopenshell_wrapper.entity]:
"""Gets a list of supertype declarations
:param declaration: The declaration from the schema, as an entity.
:return: A list of supertypes in order from parent to grandparent.
Example:
.. code:: python
wall = model.createIfcWall()
results = ifcopenshell.util.schema.get_supertypes(wall.wrapped_data.declaration().as_entity())
# [<entity IfcBuildingElement>, <entity IfcElement>, ..., <entity IfcRoot>]
"""
results = []
while True:
if not (declaration := declaration.supertype()):
break
results.append(declaration)
return results
def get_subtypes(
declaration: ifcopenshell.ifcopenshell_wrapper.entity,
) -> list[ifcopenshell.ifcopenshell_wrapper.entity]:
"""Get a flat list of subtype declarations, recursively.
Abstract classes are skipped.
Inconsistently, the declaration itself is also added to this list. This
should be fixed exclude the declaration itself.
:param declaration: The declaration from the schema, as an entity.
:return: A list of subtypes in order from child to grandchild.
.. code:: python
schema = ifcopenshell.schema_by_name("IFC4")
declaration = schema.declaration_by_name("IfcFlowSegment")
print(ifcopenshell.util.schema.get_subtypes(declaration))
[<entity IfcFlowSegment>, <entity IfcCableCarrierSegment>, ..., <entity IfcPipeSegment>]
"""
def get_classes(decl: ifcopenshell_wrapper.entity) -> list[ifcopenshell_wrapper.entity]:
results: list[ifcopenshell_wrapper.entity] = []
if not decl.is_abstract():
results.append(decl)
for subtype in decl.subtypes():
results.extend(get_classes(subtype))
return results
return get_classes(declaration)
def reassign_class(
ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance, new_class: str
) -> ifcopenshell.entity_instance:
"""
Attempts to change the class (entity name) of `element` to `new_class` by
removing element and recreating a similar instance of type `new_class`
with the same id.
In certain cases it may affect the structure of inversely related instances:
- Multiple occurrences of reassigned instance within the same aggregate
(such as start and end-point of polyline)
- Occurrences of reassigned instance within an ordered aggregate
(such as IfcRelNests)
It's unlikely that this affects real-world usage of this function.
:raises ValueError: If ``new_class`` does not exist in the provided file schema.
"""
if element.is_a() == new_class:
return element
if not ifc_file:
ifc_file = element.file
schema = ifcopenshell_wrapper.schema_by_name(ifc_file.schema_identifier)
try:
declaration = schema.declaration_by_name(new_class)
except RuntimeError:
raise ValueError(
f"Class of {element} could not be changed to {new_class} as the class does not exist in schema {ifc_file.schema_identifier}."
)
info = element.get_info()
new_attributes = {}
for attribute in declaration.all_attributes():
name = attribute.name()
old_attribute = info.get(name, None)
if old_attribute:
if ifcopenshell.util.attribute.get_primitive_type(attribute) == "enum":
if old_attribute in ifcopenshell.util.attribute.get_enum_items(attribute):
new_attributes[name] = old_attribute
else:
new_attributes[name] = old_attribute
inverse_pairs = ifc_file.get_inverse(element, allow_duplicate=True, with_attribute_indices=True)
ifc_file.remove(element)
try:
new_element = ifc_file.create_entity(new_class, id=info["id"], **new_attributes)
except:
print(f"Class of {element} could not be changed to {new_class}")
old_class = info.pop("type")
return ifc_file.create_entity(old_class, **info)
for inverse_pair in inverse_pairs:
inverse, index = inverse_pair
if inverse[index] is None:
inverse[index] = new_element
elif isinstance(inverse[index], tuple):
item = list(inverse[index])
item.append(new_element)
inverse[index] = item
return new_element
class BatchReassignClass:
def __init__(self, file: ifcopenshell.file):
self.file = file
self.purge()
def reassign(self, element: ifcopenshell.entity_instance, new_class: str) -> ifcopenshell.entity_instance:
try:
new_element = self.file.create_entity(new_class)
except:
print(f"Class of {element} could not be changed to {new_class}")
return element
new_attributes = [new_element.attribute_name(i) for i, attribute in enumerate(new_element)]
for i, attribute in enumerate(element):
try:
new_element[new_attributes.index(element.attribute_name(i))] = attribute
except:
continue
for inverse_pair in self.file.get_inverse(element, allow_duplicate=True, with_attribute_indices=True):
inverse, index = inverse_pair
self.replacements.setdefault(inverse, {}).setdefault(index, {})[element] = new_element
self.to_delete.add(element)
return new_element
def unbatch(self):
for inverse, replacements in self.replacements.items():
for index, element_map in replacements.items():
value = inverse[index]
new = inverse.walk(lambda x: True, lambda v: element_map.get(v, v), value)
if value != new:
inverse[index] = new
for element in self.to_delete:
self.file.remove(element)
self.purge()
def purge(self) -> None:
# mapping {inverse: {attribute_index: {old_element: new_element} } }
self.replacements: dict[
ifcopenshell.entity_instance, dict[int, dict[ifcopenshell.entity_instance, ifcopenshell.entity_instance]]
] = {}
self.to_delete: set[ifcopenshell.entity_instance] = set()
class Migrator:
migrated_ids: dict[int, int]
attribute_overrides: dict[int, dict[int, str]]
def __init__(self):
self.migrated_ids = {}
self.attribute_overrides = {}
self.class_4_to_2x3 = json.load(open(os.path.join(cwd, "class_4_to_2x3.json"), "r"))
self.class_2x3_to_4 = json.load(open(os.path.join(cwd, "class_2x3_to_4.json"), "r"))
# IFC classes, and their IFC attributes mapping
self.attributes_mapping = {
("IFC4", "IFC2X3"): json.load(open(os.path.join(cwd, "attribute_4_to_2x3.json"), "r")),
("IFC4X3", "IFC4"): json.load(open(os.path.join(cwd, "attribute_4x3_to_4.json"), "r")),
}
self.default_values = {
"ChangeAction": "NOCHANGE",
"CompositionType": "ELEMENT",
"CrossSectionArea": 1,
"DataValue": 0,
"DefinedValues": [0],
"DefiningValues": [0],
"DestabilizingLoad": False,
"Edition": "",
"EndParam": 1.0,
"EnumerationValues": [0],
"GeodeticDatum": "",
"Intent": "",
"IsHeading": False,
"ListValues": [0],
"LongitudinalBarCrossSectionArea": 1,
"LongitudinalBarNominalDiameter": 1,
"LongitudinalBarSpacing": 1,
"Name": "",
"NominalDiameter": 1,
"PredefinedType": "NOTDEFINED",
"RowCells": [0],
"SequenceType": "NOTDEFINED",
"Source": "",
"StartParam": 0.0,
"TransverseBarCrossSectionArea": 1,
"TransverseBarNominalDiameter": 1,
"TransverseBarSpacing": 1,
# Manual additions from experience
"InteriorOrExteriorSpace": "NOTDEFINED",
"AssemblyPlace": "NOTDEFINED", # See bug https://github.com/Autodesk/revit-ifc/issues/395
}
self.default_entities = {
"CurrentValue": None,
"DepreciatedValue": None,
"Jurisdiction": None,
"OriginalValue": None,
"Owner": None,
"OwnerHistory": None,
"Position": None,
"PropertyReference": None,
"RepresentationContexts": None,
"ResponsiblePerson": None,
"ResponsiblePersons": None,
"Rows": None,
"TotalReplacementCost": None,
"UnitsInContext": None,
"User": None,
}
def preprocess(self, old_file: ifcopenshell.file, new_file: ifcopenshell.file) -> None:
new_file.assign_header_from(old_file)
to_delete = set()
if old_file.schema == "IFC2X3" and new_file.schema == "IFC4":
# IfcCalendarDate is deprecated in IFC4
for element in old_file.by_type("IfcCalendarDate"):
for inverse, attribute_index in old_file.get_inverse(
element, allow_duplicate=True, with_attribute_indices=True
):
self.attribute_overrides.setdefault(inverse.id(), {})[
attribute_index
] = f"{element[2]}-{element[1]}-{element[0]}"
to_delete.add(element)
if old_file.schema == "IFC4" and new_file.schema == "IFC4X3":
# IfcPresentationStyleAssignment is deprecated
for assignment in old_file.by_type("IfcPresentationStyleAssignment"):
for styled_item in old_file.get_inverse(assignment):
if not styled_item.is_a("IfcStyledItem"):
continue
styled_item.Styles = [s for s in styled_item.Styles if s.is_a("IfcPresentationStyle")] + list(
assignment.Styles
)
to_delete.add(assignment)
for element in to_delete:
old_file.remove(element)
def migrate(
self, element: ifcopenshell.entity_instance, new_file: ifcopenshell.file
) -> ifcopenshell.entity_instance:
if element.id() == 0:
ifc_class = element.is_a()
if ifc_class == "IfcCountMeasure" and new_file.schema == "IFC4X3":
value = element.wrappedValue
if isinstance(value, float):
ifc_class = "IfcNumericMeasure"
return new_file.create_entity(ifc_class, element.wrappedValue)
try:
return new_file.by_id(self.migrated_ids[element.id()])
except:
pass
# print("Migrating", element)
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(new_file.schema_identifier)
new_element = self.migrate_class(element, new_file)
# print("Migrated class from {} to {}".format(element, new_element))
new_element_schema = schema.declaration_by_name(new_element.is_a())
if not hasattr(new_element_schema, "all_attributes"):
return element # The element has no attributes, so migration is done
new_element = self.migrate_attributes(element, new_file, new_element, new_element_schema)
self.migrated_ids[element.id()] = new_element.id()
return new_element
def migrate_class(
self, element: ifcopenshell.entity_instance, new_file: ifcopenshell.file
) -> ifcopenshell.entity_instance:
ifc_class = element.is_a()
if ifc_class == "IfcQuantityCount" and new_file.schema == "IFC4X3":
# 3 IfcPhysicalSimpleQuantity Value
value = element[3]
if isinstance(value, float):
ifc_class = "IfcQuantityNumber"
try:
new_element = new_file.create_entity(ifc_class)
except:
# The element does not exist in this schema
# Complex migration is not yet supported (e.g. polygonal face set to faceted brep)
if new_file.schema == "IFC2X3":
new_element = new_file.create_entity(self.class_4_to_2x3[ifc_class])
elif new_file.schema == "IFC4":
new_element = new_file.create_entity(self.class_2x3_to_4[ifc_class])
return new_element
def migrate_attributes(
self,
element: ifcopenshell.entity_instance,
new_file: ifcopenshell.file,
new_element: ifcopenshell.entity_instance,
new_element_schema: ifcopenshell_wrapper.declaration,
) -> ifcopenshell.entity_instance:
for attribute_index, value in self.attribute_overrides.get(element.id(), {}).items():
new_element[attribute_index] = value
for i, attribute in enumerate(new_element_schema.all_attributes()):
if new_element_schema.derived()[i]:
continue
self.migrate_attribute(attribute, element, new_file, new_element, new_element_schema)
return new_element
def find_equivalent_attribute(
self,
new_element: ifcopenshell.entity_instance,
attribute: ifcopenshell_wrapper.attribute,
element: ifcopenshell.entity_instance,
attributes_mapping: dict[str, dict[str, str]],
reverse_mapping: bool = False,
) -> Union[Any, None]:
# print("Searching for an equivalent", element, new_element, attribute.name())
ifc_class = new_element.is_a()
attr_name = attribute.name()
try:
if reverse_mapping:
equivalent_map = attributes_mapping[ifc_class]
equivalent = list(equivalent_map.keys())[list(equivalent_map.values()).index(attr_name)]
else:
equivalent = attributes_mapping[ifc_class][attr_name]
if hasattr(element, equivalent):
# print("Equivalent found", equivalent)
return getattr(element, equivalent)
else:
return
except Exception as e:
if (
ifc_class == "IfcQuantityNumber"
and attr_name == "NumberValue"
and new_element.file.schema == "IFC4X3"
and element.is_a("IfcQuantityCount")
):
# 3 IfcPhysicalSimpleQuantity Value
return element[3]
print(
"Unable to find equivalent attribute of {} to migrate from {} to {}".format(
attr_name, element, new_element
)
)
raise e
def migrate_attribute(
self,
attribute: ifcopenshell_wrapper.attribute,
element: ifcopenshell.entity_instance,
new_file: ifcopenshell.file,
new_element: ifcopenshell.entity_instance,
new_element_schema: ifcopenshell_wrapper.declaration,
) -> None:
# NOTE: `attribute` is an attribute in new file schema
# print("Migrating attribute", element, new_element, attribute.name())
old_file = element.wrapped_data.file
if hasattr(element, attribute.name()):
value = getattr(element, attribute.name())
# print("Attribute names matched", value)
elif new_file.schema == "IFC2X3" and old_file.schema == "IFC4":
# IFC4 to IFC2X3: We know the IFC2X3 attribute name, but not its IFC4 equivalent
try:
value = self.find_equivalent_attribute(
new_element, attribute, element, self.attributes_mapping[("IFC4", "IFC2X3")], reverse_mapping=True
)
except: # We tried our best
return
elif new_file.schema == "IFC4" and old_file.schema == "IFC2X3":
# IFC2X3 to IFC4: We know the IFC4 attribute name, but not its IFC2X3 equivalent
try:
value = self.find_equivalent_attribute(
new_element, attribute, element, self.attributes_mapping[("IFC4", "IFC2X3")]
)
except: # We tried our best
return
elif new_file.schema == "IFC4X3" and old_file.schema == "IFC4":
try:
value = self.find_equivalent_attribute(
new_element, attribute, element, self.attributes_mapping[("IFC4X3", "IFC4")]
)
except: # We tried our best
return
elif new_file.schema == "IFC4" and old_file.schema == "IFC4X3":
try:
value = self.find_equivalent_attribute(
new_element, attribute, element, self.attributes_mapping[("IFC4X3", "IFC4")], reverse_mapping=True
)
except: # We tried our best
return
try:
value
except UnboundLocalError:
print(
f"Couldn't match attribute {attribute.name()} by name to migrate from {element} "
f"to {new_element} and there is no special mapping to handle migration "
f"from {old_file.schema} -> {new_file.schema}"
)
return
# print("Continuing migration of {} to migrate from {} to {}".format(attribute.name(), element, new_element))
if value is None and not attribute.optional():
value = self.generate_default_value(attribute, new_file)
if value is None:
print("Failed to generate default value for {} on {}".format(attribute.name(), element))
elif isinstance(value, ifcopenshell.entity_instance):
value = self.migrate(value, new_file)
elif isinstance(value, (list, tuple)):
if value and isinstance(value[0], ifcopenshell.entity_instance):
new_value = []
for item in value:
new_value.append(self.migrate(item, new_file))
value = new_value
if value is not None:
setattr(new_element, attribute.name(), value)
def generate_default_value(self, attribute: ifcopenshell_wrapper.attribute, new_file: ifcopenshell.file) -> Any:
if attribute.name() in self.default_values:
return self.default_values[attribute.name()]
elif attribute.name() == "OwnerHistory":
self.default_entities[attribute.name()] = new_file.create_entity(
"IfcOwnerHistory",
**{
"OwningUser": new_file.create_entity(
"IfcPersonAndOrganization",
**{
"ThePerson": new_file.create_entity("IfcPerson"),
"TheOrganization": new_file.create_entity(
"IfcOrganization", **{"Name": "IfcOpenShell Migrator"}
),
},
),
"OwningApplication": new_file.create_entity(
"IfcApplication",
**{
"ApplicationDeveloper": new_file.create_entity(
"IfcOrganization", **{"Name": "IfcOpenShell Migrator"}
),
"Version": "Works for me",
"ApplicationFullName": "IfcOpenShell Migrator",
"ApplicationIdentifier": "IfcOpenShell Migrator",
},
),
"ChangeAction": "NOCHANGE",
"CreationDate": int(time.time()),
},
)
return self.default_entities.get(attribute.name(), None)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,319 @@
{
"Pset_ActionRequest": "IfcFacilitiesMgmtDomain",
"Pset_ActorCommon": "IfcKernel",
"Pset_ActuatorTypeCommon": "IfcBuildingControlsDomain",
"Pset_ActuatorTypeElectricActuator": "IfcBuildingControlsDomain",
"Pset_ActuatorTypeHydraulicActuator": "IfcBuildingControlsDomain",
"Pset_ActuatorTypeLinearActuation": "IfcBuildingControlsDomain",
"Pset_ActuatorTypePneumaticActuator": "IfcBuildingControlsDomain",
"Pset_ActuatorTypeRotationalActuation": "IfcBuildingControlsDomain",
"Pset_AirSideSystemInformation": "IfcSharedBldgServiceElements",
"Pset_AirTerminalBoxPHistory": "IfcHvacDomain",
"Pset_AirTerminalBoxTypeCommon": "IfcHvacDomain",
"Pset_AirTerminalPHistory": "IfcHvacDomain",
"Pset_AirTerminalTypeCommon": "IfcHvacDomain",
"Pset_AirTerminalTypeRectangular": "IfcHvacDomain",
"Pset_AirTerminalTypeRound": "IfcHvacDomain",
"Pset_AirTerminalTypeSlot": "IfcHvacDomain",
"Pset_AirTerminalTypeSquare": "IfcHvacDomain",
"Pset_AirToAirHeatRecoveryPHist": "IfcHvacDomain",
"Pset_AirToAirHeatRecoveryTypeCommon": "IfcHvacDomain",
"Pset_AnalogInput": "IfcBuildingControlsDomain",
"Pset_AnalogOutput": "IfcBuildingControlsDomain",
"Pset_Asset": "IfcSharedFacilitiesElements",
"Pset_BeamCommon": "IfcSharedBldgElements",
"Pset_BinaryInput": "IfcBuildingControlsDomain",
"Pset_BinaryOutput": "IfcBuildingControlsDomain",
"Pset_BoilerPHistory": "IfcHvacDomain",
"Pset_BoilerTypeCommon": "IfcHvacDomain",
"Pset_BoilerTypeSteam": "IfcHvacDomain",
"Pset_BuildingCommon": "IfcProductExtension",
"Pset_BuildingElementProxyCommon": "IfcProductExtension",
"Pset_BuildingStoreyCommon": "IfcProductExtension",
"Pset_BuildingUse": "IfcProductExtension",
"Pset_BuildingUseAdjacent": "IfcProductExtension",
"Pset_BuildingWaterStorage": "IfcProductExtension",
"Pset_CableCarrierSegmentTypeCableLadderSegment": "IfcElectricalDomain",
"Pset_CableCarrierSegmentTypeCableTraySegment": "IfcElectricalDomain",
"Pset_CableCarrierSegmentTypeCableTrunkingSegment": "IfcElectricalDomain",
"Pset_CableCarrierSegmentTypeConduitSegment": "IfcElectricalDomain",
"Pset_CableSegmentTypeCableSegment": "IfcElectricalDomain",
"Pset_CableSegmentTypeConductorSegment": "IfcElectricalDomain",
"Pset_ChillerPHistory": "IfcHvacDomain",
"Pset_ChillerTypeCommon": "IfcHvacDomain",
"Pset_CoilPHistory": "IfcHvacDomain",
"Pset_CoilTypeCommon": "IfcHvacDomain",
"Pset_CoilTypeHydronic": "IfcHvacDomain",
"Pset_ColumnCommon": "IfcSharedBldgElements",
"Pset_CompressorPHistory": "IfcHvacDomain",
"Pset_CompressorTypeCommon": "IfcHvacDomain",
"Pset_ConcreteElementGeneral": "IfcStructuralElementsDomain",
"Pset_ConcreteElementQuantityGeneral": "IfcStructuralElementsDomain",
"Pset_ConcreteElementSurfaceFinishQuantityGeneral": "IfcStructuralElementsDomain",
"Pset_CondenserPHistory": "IfcHvacDomain",
"Pset_CondenserTypeCommon": "IfcHvacDomain",
"Pset_ControllerTypeCommon": "IfcBuildingControlsDomain",
"Pset_ControllerTypeProportional": "IfcBuildingControlsDomain",
"Pset_ControllerTypeTwoPosition": "IfcBuildingControlsDomain",
"Pset_CooledBeamPHistory": "IfcHvacDomain",
"Pset_CooledBeamPHistoryActive": "IfcHvacDomain",
"Pset_CooledBeamTypeActive": "IfcHvacDomain",
"Pset_CooledBeamTypeCommon": "IfcHvacDomain",
"Pset_CoolingTowerPHistory": "IfcHvacDomain",
"Pset_CoolingTowerTypeCommon": "IfcHvacDomain",
"Pset_CoveringCeiling": "IfcProductExtension",
"Pset_CoveringCommon": "IfcProductExtension",
"Pset_CoveringFlooring": "IfcProductExtension",
"Pset_CurtainWallCommon": "IfcSharedBldgElements",
"Pset_DamperPHistory": "IfcHvacDomain",
"Pset_DamperTypeCommon": "IfcHvacDomain",
"Pset_DamperTypeControlDamper": "IfcHvacDomain",
"Pset_DamperTypeFireDamper": "IfcHvacDomain",
"Pset_DamperTypeFireSmokeDamper": "IfcHvacDomain",
"Pset_DamperTypeSmokeDamper": "IfcHvacDomain",
"Pset_DesignPoint": "IfcPlumbingFireProtectionDomain",
"Pset_DiscreteAccessoryAnchorBolt": "IfcSharedComponentElements",
"Pset_DiscreteAccessoryColumnShoe": "IfcSharedComponentElements",
"Pset_DiscreteAccessoryCornerFixingPlate": "IfcSharedComponentElements",
"Pset_DiscreteAccessoryDiagonalTrussConnector": "IfcSharedComponentElements",
"Pset_DiscreteAccessoryEdgeFixingPlate": "IfcSharedComponentElements",
"Pset_DiscreteAccessoryFixingSocket": "IfcSharedComponentElements",
"Pset_DiscreteAccessoryLadderTrussConnector": "IfcSharedComponentElements",
"Pset_DiscreteAccessoryStandardFixingPlate": "IfcSharedComponentElements",
"Pset_DiscreteAccessoryWireLoop": "IfcSharedComponentElements",
"Pset_DistributionChamberElementTypeFormedDuct": "IfcSharedBldgServiceElements",
"Pset_DistributionChamberElementTypeInspectionChamber": "IfcSharedBldgServiceElements",
"Pset_DistributionChamberElementTypeInspectionPit": "IfcSharedBldgServiceElements",
"Pset_DistributionChamberElementTypeManhole": "IfcSharedBldgServiceElements",
"Pset_DistributionChamberElementTypeMeterChamber": "IfcSharedBldgServiceElements",
"Pset_DistributionChamberElementTypeSump": "IfcSharedBldgServiceElements",
"Pset_DistributionChamberElementTypeTrench": "IfcSharedBldgServiceElements",
"Pset_DistributionChamberElementTypeValveChamber": "IfcSharedBldgServiceElements",
"Pset_DistributionFlowElementCommon": "IfcSharedBldgServiceElements",
"Pset_DistributionPortDuct": "IfcSharedBldgServiceElements",
"Pset_DistributionPortPipe": "IfcSharedBldgServiceElements",
"Pset_DoorCommon": "IfcSharedBldgElements",
"Pset_DoorWindowGlazingType": "IfcSharedBldgElements",
"Pset_DoorWindowShadingType": "IfcSharedBldgElements",
"Pset_DrainageCatchment": "IfcPlumbingFireProtectionDomain",
"Pset_DrainageCulvert": "IfcPlumbingFireProtectionDomain",
"Pset_DrainageOutfall": "IfcPlumbingFireProtectionDomain",
"Pset_DrainageReserve": "IfcPlumbingFireProtectionDomain",
"Pset_Draughting": "IfcProductExtension",
"Pset_DuctConnection": "IfcHvacDomain",
"Pset_DuctDesignCriteria": "IfcHvacDomain",
"Pset_DuctFittingPHistory": "IfcHvacDomain",
"Pset_DuctFittingTypeCommon": "IfcHvacDomain",
"Pset_DuctSegmentPHistory": "IfcHvacDomain",
"Pset_DuctSegmentTypeCommon": "IfcHvacDomain",
"Pset_DuctSilencerPHistory": "IfcHvacDomain",
"Pset_DuctSilencerTypeCommon": "IfcHvacDomain",
"Pset_ElectricDistributionPointCommon": "IfcElectricalDomain",
"Pset_ElectricGeneratorTypeCommon": "IfcElectricalDomain",
"Pset_ElectricHeaterTypeElectricalCableHeater": "IfcElectricalDomain",
"Pset_ElectricHeaterTypeElectricalMatHeater": "IfcElectricalDomain",
"Pset_ElectricHeaterTypeElectricalPointHeater": "IfcElectricalDomain",
"Pset_ElectricMotorTypeCommon": "IfcElectricalDomain",
"Pset_ElectricalCircuit": "IfcElectricalDomain",
"Pset_ElectricalDeviceCommon": "IfcElectricalDomain",
"Pset_ElementShading": "IfcProductExtension",
"Pset_EnergyConsumptionPHistoryElectricity": "IfcHvacDomain",
"Pset_EnergyConsumptionPHistoryFuel": "IfcHvacDomain",
"Pset_EnergyConsumptionPHistorySteam": "IfcHvacDomain",
"Pset_EnergyConversionDeviceCoil": "IfcSharedBldgServiceElements",
"Pset_EnergyConversionDeviceSpaceHeaterPanel": "IfcSharedBldgServiceElements",
"Pset_EnergyConversionDeviceSpaceHeaterSectional": "IfcSharedBldgServiceElements",
"Pset_EvaporativeCoolerPHistory": "IfcHvacDomain",
"Pset_EvaporativeCoolerTypeCommon": "IfcHvacDomain",
"Pset_EvaporatorPHistory": "IfcHvacDomain",
"Pset_EvaporatorTypeCommon": "IfcHvacDomain",
"Pset_FanPHistory": "IfcHvacDomain",
"Pset_FanTypeCommon": "IfcHvacDomain",
"Pset_FanTypeSmokeControl": "IfcHvacDomain",
"Pset_FilterPHistory": "IfcHvacDomain",
"Pset_FilterTypeAirParticleFilter": "IfcHvacDomain",
"Pset_FilterTypeCommon": "IfcHvacDomain",
"Pset_FireRatingProperties": "IfcSharedBldgServiceElements",
"Pset_FireSuppressionTerminalTypeBreechingInlet": "IfcPlumbingFireProtectionDomain",
"Pset_FireSuppressionTerminalTypeFireHydrant": "IfcPlumbingFireProtectionDomain",
"Pset_FireSuppressionTerminalTypeHoseReel": "IfcPlumbingFireProtectionDomain",
"Pset_FireSuppressionTerminalTypeSprinkler": "IfcPlumbingFireProtectionDomain",
"Pset_FlowControllerDamper": "IfcSharedBldgServiceElements",
"Pset_FlowControllerFlowMeter": "IfcSharedBldgServiceElements",
"Pset_FlowFittingDuctFitting": "IfcSharedBldgServiceElements",
"Pset_FlowFittingPipeFitting": "IfcSharedBldgServiceElements",
"Pset_FlowInstrumentTypePressureGauge": "IfcBuildingControlsDomain",
"Pset_FlowInstrumentTypeThermometer": "IfcBuildingControlsDomain",
"Pset_FlowMeterTypeCommon": "IfcHvacDomain",
"Pset_FlowMeterTypeEnergyMeter": "IfcHvacDomain",
"Pset_FlowMeterTypeGasMeter": "IfcHvacDomain",
"Pset_FlowMeterTypeOilMeter": "IfcHvacDomain",
"Pset_FlowMeterTypeWaterMeter": "IfcHvacDomain",
"Pset_FlowMovingDeviceCompressor": "IfcSharedBldgServiceElements",
"Pset_FlowMovingDeviceFan": "IfcSharedBldgServiceElements",
"Pset_FlowMovingDeviceFanCentrifugal": "IfcSharedBldgServiceElements",
"Pset_FlowMovingDevicePump": "IfcSharedBldgServiceElements",
"Pset_FlowSegmentDuctSegment": "IfcSharedBldgServiceElements",
"Pset_FlowSegmentPipeSegment": "IfcSharedBldgServiceElements",
"Pset_FlowStorageDeviceTank": "IfcSharedBldgServiceElements",
"Pset_FlowTerminalAirTerminal": "IfcSharedBldgServiceElements",
"Pset_FurnitureTypeChair": "IfcSharedFacilitiesElements",
"Pset_FurnitureTypeCommon": "IfcSharedFacilitiesElements",
"Pset_FurnitureTypeDesk": "IfcSharedFacilitiesElements",
"Pset_FurnitureTypeFileCabinet": "IfcSharedFacilitiesElements",
"Pset_FurnitureTypeTable": "IfcSharedFacilitiesElements",
"Pset_GasTerminalPHistory": "IfcHvacDomain",
"Pset_GasTerminalTypeCommon": "IfcHvacDomain",
"Pset_GasTerminalTypeGasAppliance": "IfcHvacDomain",
"Pset_GasTerminalTypeGasBurner": "IfcHvacDomain",
"Pset_HeatExchangerTypeCommon": "IfcHvacDomain",
"Pset_HeatExchangerTypePlate": "IfcHvacDomain",
"Pset_HumidifierPHistory": "IfcHvacDomain",
"Pset_HumidifierTypeCommon": "IfcHvacDomain",
"Pset_LampTypeCommon": "IfcElectricalDomain",
"Pset_LightFixtureTypeCommon": "IfcElectricalDomain",
"Pset_LightFixtureTypeExitSign": "IfcElectricalDomain",
"Pset_LightFixtureTypeThermal": "IfcElectricalDomain",
"Pset_ManufacturerOccurrence": "IfcSharedFacilitiesElements",
"Pset_ManufacturerTypeInformation": "IfcSharedFacilitiesElements",
"Pset_MemberCommon": "IfcSharedBldgElements",
"Pset_MultiStateInput": "IfcBuildingControlsDomain",
"Pset_MultiStateOutput": "IfcBuildingControlsDomain",
"Pset_OpeningElementCommon": "IfcProductExtension",
"Pset_OutletTypeCommon": "IfcElectricalDomain",
"Pset_OutsideDesignCriteria": "IfcSharedBldgServiceElements",
"Pset_PackingInstructions": "IfcFacilitiesMgmtDomain",
"Pset_Permit": "IfcFacilitiesMgmtDomain",
"Pset_PipeConnection": "IfcHvacDomain",
"Pset_PipeConnectionFlanged": "IfcHvacDomain",
"Pset_PipeFittingPHistory": "IfcHvacDomain",
"Pset_PipeFittingTypeCommon": "IfcHvacDomain",
"Pset_PipeSegmentPHistory": "IfcHvacDomain",
"Pset_PipeSegmentTypeCommon": "IfcHvacDomain",
"Pset_PipeSegmentTypeGutter": "IfcHvacDomain",
"Pset_PlateCommon": "IfcSharedBldgElements",
"Pset_PrecastConcreteElementGeneral": "IfcStructuralElementsDomain",
"Pset_ProductRequirements": "IfcKernel",
"Pset_ProjectCommon": "IfcKernel",
"Pset_ProjectOrderChangeOrder": "IfcSharedMgmtElements",
"Pset_ProjectOrderMaintenanceWorkOrder": "IfcSharedMgmtElements",
"Pset_ProjectOrderMoveOrder": "IfcSharedMgmtElements",
"Pset_ProjectOrderPurchaseOrder": "IfcSharedMgmtElements",
"Pset_ProjectOrderWorkOrder": "IfcSharedMgmtElements",
"Pset_ProjectionElementShadingDevicePHistory": "IfcHvacDomain",
"Pset_PropertyAgreement": "IfcSharedFacilitiesElements",
"Pset_ProtectiveDeviceTypeCircuitBreaker": "IfcElectricalDomain",
"Pset_ProtectiveDeviceTypeCommon": "IfcElectricalDomain",
"Pset_ProtectiveDeviceTypeEarthFailureDevice": "IfcElectricalDomain",
"Pset_ProtectiveDeviceTypeFuseDisconnector": "IfcElectricalDomain",
"Pset_ProtectiveDeviceTypeResidualCurrentCircuitBreaker": "IfcElectricalDomain",
"Pset_ProtectiveDeviceTypeResidualCurrentSwitch": "IfcElectricalDomain",
"Pset_ProtectiveDeviceTypeVaristor": "IfcElectricalDomain",
"Pset_PumpPHistory": "IfcHvacDomain",
"Pset_PumpTypeCommon": "IfcHvacDomain",
"Pset_QuantityTakeOff": "IfcProductExtension",
"Pset_RailingCommon": "IfcSharedBldgElements",
"Pset_RampCommon": "IfcSharedBldgElements",
"Pset_RampFlightCommon": "IfcSharedBldgElements",
"Pset_ReinforcementBarCountOfIndependentFooting": "IfcStructuralElementsDomain",
"Pset_ReinforcementBarPitchOfBeam": "IfcStructuralElementsDomain",
"Pset_ReinforcementBarPitchOfColumn": "IfcStructuralElementsDomain",
"Pset_ReinforcementBarPitchOfContinuousFooting": "IfcStructuralElementsDomain",
"Pset_ReinforcementBarPitchOfSlab": "IfcStructuralElementsDomain",
"Pset_ReinforcementBarPitchOfWall": "IfcStructuralElementsDomain",
"Pset_ReinforcingBarBendingsBECCommon": "IfcStructuralElementsDomain",
"Pset_ReinforcingBarBendingsBS8666Common": "IfcStructuralElementsDomain",
"Pset_ReinforcingBarBendingsDIN135610Common": "IfcStructuralElementsDomain",
"Pset_ReinforcingBarBendingsISOCD3766Common": "IfcStructuralElementsDomain",
"Pset_Reliability": "IfcSharedFacilitiesElements",
"Pset_Risk": "IfcSharedFacilitiesElements",
"Pset_RoofCommon": "IfcSharedBldgElements",
"Pset_SanitaryTerminalTypeBath": "IfcPlumbingFireProtectionDomain",
"Pset_SanitaryTerminalTypeBidet": "IfcPlumbingFireProtectionDomain",
"Pset_SanitaryTerminalTypeCistern": "IfcPlumbingFireProtectionDomain",
"Pset_SanitaryTerminalTypeSanitaryFountain": "IfcPlumbingFireProtectionDomain",
"Pset_SanitaryTerminalTypeShower": "IfcPlumbingFireProtectionDomain",
"Pset_SanitaryTerminalTypeSink": "IfcPlumbingFireProtectionDomain",
"Pset_SanitaryTerminalTypeToiletPan": "IfcPlumbingFireProtectionDomain",
"Pset_SanitaryTerminalTypeUrinal": "IfcPlumbingFireProtectionDomain",
"Pset_SanitaryTerminalTypeWCSeat": "IfcPlumbingFireProtectionDomain",
"Pset_SanitaryTerminalTypeWashHandBasin": "IfcPlumbingFireProtectionDomain",
"Pset_SensorTypeCO2Sensor": "IfcBuildingControlsDomain",
"Pset_SensorTypeFireSensor": "IfcBuildingControlsDomain",
"Pset_SensorTypeGasSensor": "IfcBuildingControlsDomain",
"Pset_SensorTypeHeatSensor": "IfcBuildingControlsDomain",
"Pset_SensorTypeHumiditySensor": "IfcBuildingControlsDomain",
"Pset_SensorTypeLightSensor": "IfcBuildingControlsDomain",
"Pset_SensorTypeMovementSensor": "IfcBuildingControlsDomain",
"Pset_SensorTypePressureSensor": "IfcBuildingControlsDomain",
"Pset_SensorTypeSmokeSensor": "IfcBuildingControlsDomain",
"Pset_SensorTypeSoundSensor": "IfcBuildingControlsDomain",
"Pset_SensorTypeTemperatureSensor": "IfcBuildingControlsDomain",
"Pset_SiteCommon": "IfcProductExtension",
"Pset_SlabCommon": "IfcSharedBldgElements",
"Pset_SpaceCommon": "IfcProductExtension",
"Pset_SpaceFireSafetyRequirements": "IfcProductExtension",
"Pset_SpaceHeaterPHistoryCommon": "IfcHvacDomain",
"Pset_SpaceHeaterTypeCommon": "IfcHvacDomain",
"Pset_SpaceHeaterTypeHydronic": "IfcHvacDomain",
"Pset_SpaceLightingRequirements": "IfcProductExtension",
"Pset_SpaceOccupancyRequirements": "IfcProductExtension",
"Pset_SpaceParking": "IfcProductExtension",
"Pset_SpaceParkingAisle": "IfcProductExtension",
"Pset_SpaceProgramCommon": "IfcArchitectureDomain",
"Pset_SpaceThermalDesign": "IfcSharedBldgServiceElements",
"Pset_SpaceThermalPHistory": "IfcHvacDomain",
"Pset_SpaceThermalRequirements": "IfcProductExtension",
"Pset_StairCommon": "IfcSharedBldgElements",
"Pset_StairFlightCommon": "IfcSharedBldgElements",
"Pset_SwitchingDeviceTypeCommon": "IfcElectricalDomain",
"Pset_SwitchingDeviceTypeContactor": "IfcElectricalDomain",
"Pset_SwitchingDeviceTypeEmergencyStop": "IfcElectricalDomain",
"Pset_SwitchingDeviceTypeStarter": "IfcElectricalDomain",
"Pset_SwitchingDeviceTypeSwitchDisconnector": "IfcElectricalDomain",
"Pset_SwitchingDeviceTypeToggleSwitch": "IfcElectricalDomain",
"Pset_SystemFurnitureElementTypeCommon": "IfcSharedFacilitiesElements",
"Pset_SystemFurnitureElementTypePanel": "IfcSharedFacilitiesElements",
"Pset_SystemFurnitureElementTypeWorkSurface": "IfcSharedFacilitiesElements",
"Pset_TankTypeCommon": "IfcHvacDomain",
"Pset_TankTypeExpansion": "IfcHvacDomain",
"Pset_TankTypePreformed": "IfcHvacDomain",
"Pset_TankTypePressureVessel": "IfcHvacDomain",
"Pset_TankTypeSectional": "IfcHvacDomain",
"Pset_ThermalLoadAggregate": "IfcSharedBldgServiceElements",
"Pset_ThermalLoadDesignCriteria": "IfcSharedBldgServiceElements",
"Pset_TransformerTypeCommon": "IfcElectricalDomain",
"Pset_TransportElementCommon": "IfcProductExtension",
"Pset_TransportElementElevator": "IfcProductExtension",
"Pset_TubeBundleTypeCommon": "IfcHvacDomain",
"Pset_TubeBundleTypeFinned": "IfcHvacDomain",
"Pset_UnitaryEquipmentTypeAirConditioningUnit": "IfcHvacDomain",
"Pset_UnitaryEquipmentTypeAirHandler": "IfcHvacDomain",
"Pset_UtilityConsumption": "IfcSharedBldgServiceElements",
"Pset_ValvePHistory": "IfcHvacDomain",
"Pset_ValveTypeAirRelease": "IfcHvacDomain",
"Pset_ValveTypeCommon": "IfcHvacDomain",
"Pset_ValveTypeDrawOffCock": "IfcHvacDomain",
"Pset_ValveTypeFaucet": "IfcHvacDomain",
"Pset_ValveTypeFlushing": "IfcHvacDomain",
"Pset_ValveTypeGasTap": "IfcHvacDomain",
"Pset_ValveTypeIsolating": "IfcHvacDomain",
"Pset_ValveTypeMixing": "IfcHvacDomain",
"Pset_ValveTypePressureReducing": "IfcHvacDomain",
"Pset_ValveTypePressureRelief": "IfcHvacDomain",
"Pset_VibrationIsolatorTypeCommon": "IfcHvacDomain",
"Pset_WallCommon": "IfcSharedBldgElements",
"Pset_Warranty": "IfcSharedFacilitiesElements",
"Pset_WasteTerminalTypeFloorTrap": "IfcPlumbingFireProtectionDomain",
"Pset_WasteTerminalTypeFloorWaste": "IfcPlumbingFireProtectionDomain",
"Pset_WasteTerminalTypeGreaseInterceptor": "IfcPlumbingFireProtectionDomain",
"Pset_WasteTerminalTypeGullySump": "IfcPlumbingFireProtectionDomain",
"Pset_WasteTerminalTypeGullyTrap": "IfcPlumbingFireProtectionDomain",
"Pset_WasteTerminalTypeOilInterceptor": "IfcPlumbingFireProtectionDomain",
"Pset_WasteTerminalTypePetrolInterceptor": "IfcPlumbingFireProtectionDomain",
"Pset_WasteTerminalTypeRoofDrain": "IfcPlumbingFireProtectionDomain",
"Pset_WasteTerminalTypeWasteDisposalUnit": "IfcPlumbingFireProtectionDomain",
"Pset_WasteTerminalTypeWasteTrap": "IfcPlumbingFireProtectionDomain",
"Pset_WindowCommon": "IfcSharedBldgElements",
"Pset_ZoneCommon": "IfcProductExtension"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,515 @@
{
"pset_actionrequest": "ifcsharedmgmtelements",
"pset_actorcommon": "ifckernel",
"pset_actuatorphistory": "ifcbuildingcontrolsdomain",
"pset_actuatortypecommon": "ifcbuildingcontrolsdomain",
"pset_actuatortypeelectricactuator": "ifcbuildingcontrolsdomain",
"pset_actuatortypehydraulicactuator": "ifcbuildingcontrolsdomain",
"pset_actuatortypelinearactuation": "ifcbuildingcontrolsdomain",
"pset_actuatortypepneumaticactuator": "ifcbuildingcontrolsdomain",
"pset_actuatortyperotationalactuation": "ifcbuildingcontrolsdomain",
"pset_airsidesysteminformation": "ifcsharedbldgserviceelements",
"pset_airterminalboxphistory": "ifchvacdomain",
"pset_airterminalboxtypecommon": "ifchvacdomain",
"pset_airterminaloccurrence": "ifchvacdomain",
"pset_airterminalphistory": "ifchvacdomain",
"pset_airterminaltypecommon": "ifchvacdomain",
"pset_airtoairheatrecoveryphistory": "ifchvacdomain",
"pset_airtoairheatrecoverytypecommon": "ifchvacdomain",
"pset_alarmphistory": "ifcbuildingcontrolsdomain",
"pset_alarmtypecommon": "ifcbuildingcontrolsdomain",
"pset_annotationcontourline": "ifcproductextension",
"pset_annotationlineofsight": "ifcproductextension",
"pset_annotationsurveyarea": "ifcproductextension",
"pset_asset": "ifcsharedfacilitieselements",
"pset_audiovisualappliancephistory": "ifcelectricaldomain",
"pset_audiovisualappliancetypeamplifier": "ifcelectricaldomain",
"pset_audiovisualappliancetypecamera": "ifcelectricaldomain",
"pset_audiovisualappliancetypecommon": "ifcelectricaldomain",
"pset_audiovisualappliancetypedisplay": "ifcelectricaldomain",
"pset_audiovisualappliancetypeplayer": "ifcelectricaldomain",
"pset_audiovisualappliancetypeprojector": "ifcelectricaldomain",
"pset_audiovisualappliancetypereceiver": "ifcelectricaldomain",
"pset_audiovisualappliancetypespeaker": "ifcelectricaldomain",
"pset_audiovisualappliancetypetuner": "ifcelectricaldomain",
"pset_beamcommon": "ifcsharedbldgelements",
"pset_boilerphistory": "ifchvacdomain",
"pset_boilertypecommon": "ifchvacdomain",
"pset_boilertypesteam": "ifchvacdomain",
"pset_boilertypewater": "ifchvacdomain",
"pset_buildingcommon": "ifcproductextension",
"pset_buildingelementproxycommon": "ifcsharedbldgelements",
"pset_buildingelementproxyprovisionforvoid": "ifcsharedbldgelements",
"pset_buildingstoreycommon": "ifcproductextension",
"pset_buildingsystemcommon": "ifcsharedbldgelements",
"pset_buildinguse": "ifcproductextension",
"pset_buildinguseadjacent": "ifcproductextension",
"pset_burnertypecommon": "ifchvacdomain",
"pset_cablecarrierfittingtypecommon": "ifcelectricaldomain",
"pset_cablecarriersegmenttypecableladdersegment": "ifcelectricaldomain",
"pset_cablecarriersegmenttypecabletraysegment": "ifcelectricaldomain",
"pset_cablecarriersegmenttypecabletrunkingsegment": "ifcelectricaldomain",
"pset_cablecarriersegmenttypecommon": "ifcelectricaldomain",
"pset_cablecarriersegmenttypeconduitsegment": "ifcelectricaldomain",
"pset_cablefittingtypecommon": "ifcelectricaldomain",
"pset_cablesegmentoccurrence": "ifcelectricaldomain",
"pset_cablesegmenttypebusbarsegment": "ifcelectricaldomain",
"pset_cablesegmenttypecablesegment": "ifcelectricaldomain",
"pset_cablesegmenttypecommon": "ifcelectricaldomain",
"pset_cablesegmenttypeconductorsegment": "ifcelectricaldomain",
"pset_cablesegmenttypecoresegment": "ifcelectricaldomain",
"pset_chillerphistory": "ifchvacdomain",
"pset_chillertypecommon": "ifchvacdomain",
"pset_chimneycommon": "ifcsharedbldgelements",
"pset_civilelementcommon": "ifcproductextension",
"pset_coiloccurrence": "ifchvacdomain",
"pset_coilphistory": "ifchvacdomain",
"pset_coiltypecommon": "ifchvacdomain",
"pset_coiltypehydronic": "ifchvacdomain",
"pset_columncommon": "ifcsharedbldgelements",
"pset_communicationsappliancephistory": "ifcelectricaldomain",
"pset_communicationsappliancetypecommon": "ifcelectricaldomain",
"pset_compressorphistory": "ifchvacdomain",
"pset_compressortypecommon": "ifchvacdomain",
"pset_concreteelementgeneral": "ifcstructuralelementsdomain",
"pset_condenserphistory": "ifchvacdomain",
"pset_condensertypecommon": "ifchvacdomain",
"pset_condition": "ifcsharedfacilitieselements",
"pset_constructionresource": "ifcconstructionmgmtdomain",
"pset_controllerphistory": "ifcbuildingcontrolsdomain",
"pset_controllertypecommon": "ifcbuildingcontrolsdomain",
"pset_controllertypefloating": "ifcbuildingcontrolsdomain",
"pset_controllertypemultiposition": "ifcbuildingcontrolsdomain",
"pset_controllertypeprogrammable": "ifcbuildingcontrolsdomain",
"pset_controllertypeproportional": "ifcbuildingcontrolsdomain",
"pset_controllertypetwoposition": "ifcbuildingcontrolsdomain",
"pset_cooledbeamphistory": "ifchvacdomain",
"pset_cooledbeamphistoryactive": "ifchvacdomain",
"pset_cooledbeamtypeactive": "ifchvacdomain",
"pset_cooledbeamtypecommon": "ifchvacdomain",
"pset_coolingtowerphistory": "ifchvacdomain",
"pset_coolingtowertypecommon": "ifchvacdomain",
"pset_coveringceiling": "ifcsharedbldgelements",
"pset_coveringcommon": "ifcsharedbldgelements",
"pset_coveringflooring": "ifcsharedbldgelements",
"pset_curtainwallcommon": "ifcsharedbldgelements",
"pset_damperoccurrence": "ifchvacdomain",
"pset_damperphistory": "ifchvacdomain",
"pset_dampertypecommon": "ifchvacdomain",
"pset_dampertypecontroldamper": "ifchvacdomain",
"pset_dampertypefiredamper": "ifchvacdomain",
"pset_dampertypefiresmokedamper": "ifchvacdomain",
"pset_dampertypesmokedamper": "ifchvacdomain",
"pset_discreteaccessorycolumnshoe": "ifcsharedcomponentelements",
"pset_discreteaccessorycornerfixingplate": "ifcsharedcomponentelements",
"pset_discreteaccessorydiagonaltrussconnector": "ifcsharedcomponentelements",
"pset_discreteaccessoryedgefixingplate": "ifcsharedcomponentelements",
"pset_discreteaccessoryfixingsocket": "ifcsharedcomponentelements",
"pset_discreteaccessoryladdertrussconnector": "ifcsharedcomponentelements",
"pset_discreteaccessorystandardfixingplate": "ifcsharedcomponentelements",
"pset_discreteaccessorywireloop": "ifcsharedcomponentelements",
"pset_distributionchamberelementcommon": "ifcsharedbldgserviceelements",
"pset_distributionchamberelementtypeformedduct": "ifcsharedbldgserviceelements",
"pset_distributionchamberelementtypeinspectionchamber": "ifcsharedbldgserviceelements",
"pset_distributionchamberelementtypeinspectionpit": "ifcsharedbldgserviceelements",
"pset_distributionchamberelementtypemanhole": "ifcsharedbldgserviceelements",
"pset_distributionchamberelementtypemeterchamber": "ifcsharedbldgserviceelements",
"pset_distributionchamberelementtypesump": "ifcsharedbldgserviceelements",
"pset_distributionchamberelementtypetrench": "ifcsharedbldgserviceelements",
"pset_distributionchamberelementtypevalvechamber": "ifcsharedbldgserviceelements",
"pset_distributionportcommon": "ifcsharedbldgserviceelements",
"pset_distributionportphistorycable": "ifcsharedbldgserviceelements",
"pset_distributionportphistoryduct": "ifcsharedbldgserviceelements",
"pset_distributionportphistorypipe": "ifcsharedbldgserviceelements",
"pset_distributionporttypecable": "ifcsharedbldgserviceelements",
"pset_distributionporttypeduct": "ifcsharedbldgserviceelements",
"pset_distributionporttypepipe": "ifcsharedbldgserviceelements",
"pset_distributionsystemcommon": "ifcsharedbldgserviceelements",
"pset_distributionsystemtypeelectrical": "ifcsharedbldgserviceelements",
"pset_distributionsystemtypeventilation": "ifcsharedbldgserviceelements",
"pset_doorcommon": "ifcsharedbldgelements",
"pset_doorwindowglazingtype": "ifcsharedbldgelements",
"pset_ductfittingoccurrence": "ifchvacdomain",
"pset_ductfittingphistory": "ifchvacdomain",
"pset_ductfittingtypecommon": "ifchvacdomain",
"pset_ductsegmentoccurrence": "ifchvacdomain",
"pset_ductsegmentphistory": "ifchvacdomain",
"pset_ductsegmenttypecommon": "ifchvacdomain",
"pset_ductsilencerphistory": "ifchvacdomain",
"pset_ductsilencertypecommon": "ifchvacdomain",
"pset_electricaldevicecommon": "ifcelectricaldomain",
"pset_electricappliancephistory": "ifcelectricaldomain",
"pset_electricappliancetypecommon": "ifcelectricaldomain",
"pset_electricappliancetypedishwasher": "ifcelectricaldomain",
"pset_electricappliancetypeelectriccooker": "ifcelectricaldomain",
"pset_electricdistributionboardoccurrence": "ifcelectricaldomain",
"pset_electricdistributionboardtypecommon": "ifcelectricaldomain",
"pset_electricflowstoragedevicephistory": "ifcelectricaldomain",
"pset_electricflowstoragedevicetypecommon": "ifcelectricaldomain",
"pset_electricgeneratortypecommon": "ifcelectricaldomain",
"pset_electricmotortypecommon": "ifcelectricaldomain",
"pset_electrictimecontroltypecommon": "ifcelectricaldomain",
"pset_elementassemblycommon": "ifcproductextension",
"pset_elementcomponentcommon": "ifcsharedcomponentelements",
"pset_enginetypecommon": "ifchvacdomain",
"pset_environmentalimpactindicators": "ifcproductextension",
"pset_environmentalimpactvalues": "ifcproductextension",
"pset_evaporativecoolerphistory": "ifchvacdomain",
"pset_evaporativecoolertypecommon": "ifchvacdomain",
"pset_evaporatorphistory": "ifchvacdomain",
"pset_evaporatortypecommon": "ifchvacdomain",
"pset_fancentrifugal": "ifchvacdomain",
"pset_fanoccurrence": "ifchvacdomain",
"pset_fanphistory": "ifchvacdomain",
"pset_fantypecommon": "ifchvacdomain",
"pset_fastenerweld": "ifcsharedcomponentelements",
"pset_filterphistory": "ifchvacdomain",
"pset_filtertypeairparticlefilter": "ifchvacdomain",
"pset_filtertypecommon": "ifchvacdomain",
"pset_filtertypecompressedairfilter": "ifchvacdomain",
"pset_filtertypewaterfilter": "ifchvacdomain",
"pset_firesuppressionterminaltypebreechinginlet": "ifcplumbingfireprotectiondomain",
"pset_firesuppressionterminaltypecommon": "ifcplumbingfireprotectiondomain",
"pset_firesuppressionterminaltypefirehydrant": "ifcplumbingfireprotectiondomain",
"pset_firesuppressionterminaltypehosereel": "ifcplumbingfireprotectiondomain",
"pset_firesuppressionterminaltypesprinkler": "ifcplumbingfireprotectiondomain",
"pset_flowinstrumentphistory": "ifcbuildingcontrolsdomain",
"pset_flowinstrumenttypecommon": "ifcbuildingcontrolsdomain",
"pset_flowinstrumenttypepressuregauge": "ifcbuildingcontrolsdomain",
"pset_flowinstrumenttypethermometer": "ifcbuildingcontrolsdomain",
"pset_flowmeteroccurrence": "ifchvacdomain",
"pset_flowmetertypecommon": "ifchvacdomain",
"pset_flowmetertypeenergymeter": "ifchvacdomain",
"pset_flowmetertypegasmeter": "ifchvacdomain",
"pset_flowmetertypeoilmeter": "ifchvacdomain",
"pset_flowmetertypewatermeter": "ifchvacdomain",
"pset_footingcommon": "ifcstructuralelementsdomain",
"pset_furnituretypechair": "ifcsharedfacilitieselements",
"pset_furnituretypecommon": "ifcsharedfacilitieselements",
"pset_furnituretypedesk": "ifcsharedfacilitieselements",
"pset_furnituretypefilecabinet": "ifcsharedfacilitieselements",
"pset_furnituretypetable": "ifcsharedfacilitieselements",
"pset_heatexchangertypecommon": "ifchvacdomain",
"pset_heatexchangertypeplate": "ifchvacdomain",
"pset_humidifierphistory": "ifchvacdomain",
"pset_humidifiertypecommon": "ifchvacdomain",
"pset_interceptortypecommon": "ifcplumbingfireprotectiondomain",
"pset_junctionboxtypecommon": "ifcelectricaldomain",
"pset_lamptypecommon": "ifcelectricaldomain",
"pset_landregistration": "ifcproductextension",
"pset_lightfixturetypecommon": "ifcelectricaldomain",
"pset_lightfixturetypesecuritylighting": "ifcelectricaldomain",
"pset_manufactureroccurrence": "ifcsharedfacilitieselements",
"pset_manufacturertypeinformation": "ifcsharedfacilitieselements",
"pset_materialcombustion": "ifcmaterialresource",
"pset_materialcommon": "ifcmaterialresource",
"pset_materialconcrete": "ifcmaterialresource",
"pset_materialenergy": "ifcmaterialresource",
"pset_materialfuel": "ifcmaterialresource",
"pset_materialhygroscopic": "ifcmaterialresource",
"pset_materialmechanical": "ifcmaterialresource",
"pset_materialoptical": "ifcmaterialresource",
"pset_materialsteel": "ifcmaterialresource",
"pset_materialthermal": "ifcmaterialresource",
"pset_materialwater": "ifcmaterialresource",
"pset_materialwood": "ifcmaterialresource",
"pset_materialwoodbasedbeam": "ifcmaterialresource",
"pset_materialwoodbasedpanel": "ifcmaterialresource",
"pset_mechanicalfasteneranchorbolt": "ifcsharedcomponentelements",
"pset_mechanicalfastenerbolt": "ifcsharedcomponentelements",
"pset_mechanicalfastenercommon": "ifcsharedcomponentelements",
"pset_medicaldevicetypecommon": "ifchvacdomain",
"pset_membercommon": "ifcsharedbldgelements",
"pset_motorconnectiontypecommon": "ifcelectricaldomain",
"pset_openingelementcommon": "ifcproductextension",
"pset_outlettypecommon": "ifcelectricaldomain",
"pset_outsidedesigncriteria": "ifcsharedbldgserviceelements",
"pset_packinginstructions": "ifcsharedmgmtelements",
"pset_permit": "ifcsharedmgmtelements",
"pset_pilecommon": "ifcstructuralelementsdomain",
"pset_pipeconnectionflanged": "ifchvacdomain",
"pset_pipefittingoccurrence": "ifchvacdomain",
"pset_pipefittingphistory": "ifchvacdomain",
"pset_pipefittingtypebend": "ifchvacdomain",
"pset_pipefittingtypecommon": "ifchvacdomain",
"pset_pipefittingtypejunction": "ifchvacdomain",
"pset_pipesegmentoccurrence": "ifchvacdomain",
"pset_pipesegmentphistory": "ifchvacdomain",
"pset_pipesegmenttypecommon": "ifchvacdomain",
"pset_pipesegmenttypeculvert": "ifchvacdomain",
"pset_pipesegmenttypegutter": "ifchvacdomain",
"pset_platecommon": "ifcsharedbldgelements",
"pset_precastconcreteelementfabrication": "ifcstructuralelementsdomain",
"pset_precastconcreteelementgeneral": "ifcstructuralelementsdomain",
"pset_precastslab": "ifcstructuralelementsdomain",
"pset_profilearbitrarydoublet": "ifcprofileresource",
"pset_profilearbitraryhollowcore": "ifcprofileresource",
"pset_profilemechanical": "ifcprofileresource",
"pset_projectorderchangeorder": "ifcsharedmgmtelements",
"pset_projectordermaintenanceworkorder": "ifcsharedmgmtelements",
"pset_projectordermoveorder": "ifcsharedmgmtelements",
"pset_projectorderpurchaseorder": "ifcsharedmgmtelements",
"pset_projectorderworkorder": "ifcsharedmgmtelements",
"pset_propertyagreement": "ifcsharedfacilitieselements",
"pset_protectivedevicebreakeruniti2tcurve": "ifcelectricaldomain",
"pset_protectivedevicebreakeruniti2tfusecurve": "ifcelectricaldomain",
"pset_protectivedevicebreakerunitipicurve": "ifcelectricaldomain",
"pset_protectivedevicebreakerunittypemcb": "ifcelectricaldomain",
"pset_protectivedevicebreakerunittypemotorprotection": "ifcelectricaldomain",
"pset_protectivedeviceoccurrence": "ifcelectricaldomain",
"pset_protectivedevicetrippingcurve": "ifcelectricaldomain",
"pset_protectivedevicetrippingfunctiongcurve": "ifcelectricaldomain",
"pset_protectivedevicetrippingfunctionicurve": "ifcelectricaldomain",
"pset_protectivedevicetrippingfunctionlcurve": "ifcelectricaldomain",
"pset_protectivedevicetrippingfunctionscurve": "ifcelectricaldomain",
"pset_protectivedevicetrippingunitcurrentadjustment": "ifcelectricaldomain",
"pset_protectivedevicetrippingunittimeadjustment": "ifcelectricaldomain",
"pset_protectivedevicetrippingunittypecommon": "ifcelectricaldomain",
"pset_protectivedevicetrippingunittypeelectromagnetic": "ifcelectricaldomain",
"pset_protectivedevicetrippingunittypeelectronic": "ifcelectricaldomain",
"pset_protectivedevicetrippingunittyperesidualcurrent": "ifcelectricaldomain",
"pset_protectivedevicetrippingunittypethermal": "ifcelectricaldomain",
"pset_protectivedevicetypecircuitbreaker": "ifcelectricaldomain",
"pset_protectivedevicetypecommon": "ifcelectricaldomain",
"pset_protectivedevicetypeearthleakagecircuitbreaker": "ifcelectricaldomain",
"pset_protectivedevicetypefusedisconnector": "ifcelectricaldomain",
"pset_protectivedevicetyperesidualcurrentcircuitbreaker": "ifcelectricaldomain",
"pset_protectivedevicetyperesidualcurrentswitch": "ifcelectricaldomain",
"pset_protectivedevicetypevaristor": "ifcelectricaldomain",
"pset_pumpoccurrence": "ifchvacdomain",
"pset_pumpphistory": "ifchvacdomain",
"pset_pumptypecommon": "ifchvacdomain",
"pset_railingcommon": "ifcsharedbldgelements",
"pset_rampcommon": "ifcsharedbldgelements",
"pset_rampflightcommon": "ifcsharedbldgelements",
"pset_reinforcementbarcountofindependentfooting": "ifcstructuralelementsdomain",
"pset_reinforcementbarpitchofbeam": "ifcstructuralelementsdomain",
"pset_reinforcementbarpitchofcolumn": "ifcstructuralelementsdomain",
"pset_reinforcementbarpitchofcontinuousfooting": "ifcstructuralelementsdomain",
"pset_reinforcementbarpitchofslab": "ifcstructuralelementsdomain",
"pset_reinforcementbarpitchofwall": "ifcstructuralelementsdomain",
"pset_reinforcingbarcommon": "ifcstructuralelementsdomain",
"pset_reinforcingmeshcommon": "ifcstructuralelementsdomain",
"pset_risk": "ifcsharedfacilitieselements",
"pset_roofcommon": "ifcsharedbldgelements",
"pset_sanitaryterminaltypebath": "ifcplumbingfireprotectiondomain",
"pset_sanitaryterminaltypebidet": "ifcplumbingfireprotectiondomain",
"pset_sanitaryterminaltypecistern": "ifcplumbingfireprotectiondomain",
"pset_sanitaryterminaltypecommon": "ifcplumbingfireprotectiondomain",
"pset_sanitaryterminaltypesanitaryfountain": "ifcplumbingfireprotectiondomain",
"pset_sanitaryterminaltypeshower": "ifcplumbingfireprotectiondomain",
"pset_sanitaryterminaltypesink": "ifcplumbingfireprotectiondomain",
"pset_sanitaryterminaltypetoiletpan": "ifcplumbingfireprotectiondomain",
"pset_sanitaryterminaltypeurinal": "ifcplumbingfireprotectiondomain",
"pset_sanitaryterminaltypewashhandbasin": "ifcplumbingfireprotectiondomain",
"pset_sensorphistory": "ifcbuildingcontrolsdomain",
"pset_sensortypeco2sensor": "ifcbuildingcontrolsdomain",
"pset_sensortypecommon": "ifcbuildingcontrolsdomain",
"pset_sensortypeconductancesensor": "ifcbuildingcontrolsdomain",
"pset_sensortypecontactsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypefiresensor": "ifcbuildingcontrolsdomain",
"pset_sensortypeflowsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypefrostsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypegassensor": "ifcbuildingcontrolsdomain",
"pset_sensortypeheatsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypehumiditysensor": "ifcbuildingcontrolsdomain",
"pset_sensortypeidentifiersensor": "ifcbuildingcontrolsdomain",
"pset_sensortypeionconcentrationsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypelevelsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypelightsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypemoisturesensor": "ifcbuildingcontrolsdomain",
"pset_sensortypemovementsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypephsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypepressuresensor": "ifcbuildingcontrolsdomain",
"pset_sensortyperadiationsensor": "ifcbuildingcontrolsdomain",
"pset_sensortyperadioactivitysensor": "ifcbuildingcontrolsdomain",
"pset_sensortypesmokesensor": "ifcbuildingcontrolsdomain",
"pset_sensortypesoundsensor": "ifcbuildingcontrolsdomain",
"pset_sensortypetemperaturesensor": "ifcbuildingcontrolsdomain",
"pset_sensortypewindsensor": "ifcbuildingcontrolsdomain",
"pset_servicelife": "ifcsharedfacilitieselements",
"pset_servicelifefactors": "ifcsharedfacilitieselements",
"pset_shadingdevicecommon": "ifcsharedbldgelements",
"pset_shadingdevicephistory": "ifchvacdomain",
"pset_sitecommon": "ifcproductextension",
"pset_slabcommon": "ifcsharedbldgelements",
"pset_solardevicetypecommon": "ifcelectricaldomain",
"pset_soundattenuation": "ifcsharedbldgserviceelements",
"pset_soundgeneration": "ifcsharedbldgserviceelements",
"pset_spacecommon": "ifcproductextension",
"pset_spacecoveringrequirements": "ifcproductextension",
"pset_spacefiresafetyrequirements": "ifcproductextension",
"pset_spaceheaterphistory": "ifchvacdomain",
"pset_spaceheatertypecommon": "ifchvacdomain",
"pset_spaceheatertypeconvector": "ifchvacdomain",
"pset_spaceheatertyperadiator": "ifchvacdomain",
"pset_spacelightingrequirements": "ifcproductextension",
"pset_spaceoccupancyrequirements": "ifcproductextension",
"pset_spaceparking": "ifcproductextension",
"pset_spacethermaldesign": "ifcsharedbldgserviceelements",
"pset_spacethermalload": "ifcsharedbldgserviceelements",
"pset_spacethermalloadphistory": "ifcsharedbldgserviceelements",
"pset_spacethermalphistory": "ifchvacdomain",
"pset_spacethermalrequirements": "ifcproductextension",
"pset_spatialzonecommon": "ifcproductextension",
"pset_stackterminaltypecommon": "ifcplumbingfireprotectiondomain",
"pset_staircommon": "ifcsharedbldgelements",
"pset_stairflightcommon": "ifcsharedbldgelements",
"pset_structuralsurfacemembervaryingthickness": "ifcstructuralanalysisdomain",
"pset_switchingdevicetypecommon": "ifcelectricaldomain",
"pset_switchingdevicetypecontactor": "ifcelectricaldomain",
"pset_switchingdevicetypedimmerswitch": "ifcelectricaldomain",
"pset_switchingdevicetypeemergencystop": "ifcelectricaldomain",
"pset_switchingdevicetypekeypad": "ifcelectricaldomain",
"pset_switchingdevicetypemomentaryswitch": "ifcelectricaldomain",
"pset_switchingdevicetypephistory": "ifcelectricaldomain",
"pset_switchingdevicetypeselectorswitch": "ifcelectricaldomain",
"pset_switchingdevicetypestarter": "ifcelectricaldomain",
"pset_switchingdevicetypeswitchdisconnector": "ifcelectricaldomain",
"pset_switchingdevicetypetoggleswitch": "ifcelectricaldomain",
"pset_systemfurnitureelementtypecommon": "ifcsharedfacilitieselements",
"pset_systemfurnitureelementtypepanel": "ifcsharedfacilitieselements",
"pset_systemfurnitureelementtypeworksurface": "ifcsharedfacilitieselements",
"pset_tankoccurrence": "ifchvacdomain",
"pset_tankphistory": "ifchvacdomain",
"pset_tanktypecommon": "ifchvacdomain",
"pset_tanktypeexpansion": "ifchvacdomain",
"pset_tanktypepreformed": "ifchvacdomain",
"pset_tanktypepressurevessel": "ifchvacdomain",
"pset_tanktypesectional": "ifchvacdomain",
"pset_tendonanchorcommon": "ifcstructuralelementsdomain",
"pset_tendoncommon": "ifcstructuralelementsdomain",
"pset_thermalloadaggregate": "ifcsharedbldgserviceelements",
"pset_thermalloaddesigncriteria": "ifcsharedbldgserviceelements",
"pset_transformertypecommon": "ifcelectricaldomain",
"pset_transportelementcommon": "ifcproductextension",
"pset_transportelementelevator": "ifcproductextension",
"pset_tubebundletypecommon": "ifchvacdomain",
"pset_tubebundletypefinned": "ifchvacdomain",
"pset_unitarycontrolelementphistory": "ifcbuildingcontrolsdomain",
"pset_unitarycontrolelementtypecommon": "ifcbuildingcontrolsdomain",
"pset_unitarycontrolelementtypeindicatorpanel": "ifcbuildingcontrolsdomain",
"pset_unitarycontrolelementtypethermostat": "ifcbuildingcontrolsdomain",
"pset_unitaryequipmenttypeairconditioningunit": "ifchvacdomain",
"pset_unitaryequipmenttypeairhandler": "ifchvacdomain",
"pset_unitaryequipmenttypecommon": "ifchvacdomain",
"pset_utilityconsumptionphistory": "ifcsharedbldgserviceelements",
"pset_valvephistory": "ifchvacdomain",
"pset_valvetypeairrelease": "ifchvacdomain",
"pset_valvetypecommon": "ifchvacdomain",
"pset_valvetypedrawoffcock": "ifchvacdomain",
"pset_valvetypefaucet": "ifchvacdomain",
"pset_valvetypeflushing": "ifchvacdomain",
"pset_valvetypegastap": "ifchvacdomain",
"pset_valvetypeisolating": "ifchvacdomain",
"pset_valvetypemixing": "ifchvacdomain",
"pset_valvetypepressurereducing": "ifchvacdomain",
"pset_valvetypepressurerelief": "ifchvacdomain",
"pset_vibrationisolatortypecommon": "ifchvacdomain",
"pset_wallcommon": "ifcsharedbldgelements",
"pset_warranty": "ifcsharedfacilitieselements",
"pset_wasteterminaltypecommon": "ifcplumbingfireprotectiondomain",
"pset_wasteterminaltypefloortrap": "ifcplumbingfireprotectiondomain",
"pset_wasteterminaltypefloorwaste": "ifcplumbingfireprotectiondomain",
"pset_wasteterminaltypegullysump": "ifcplumbingfireprotectiondomain",
"pset_wasteterminaltypegullytrap": "ifcplumbingfireprotectiondomain",
"pset_wasteterminaltyperoofdrain": "ifcplumbingfireprotectiondomain",
"pset_wasteterminaltypewastedisposalunit": "ifcplumbingfireprotectiondomain",
"pset_wasteterminaltypewastetrap": "ifcplumbingfireprotectiondomain",
"pset_windowcommon": "ifcsharedbldgelements",
"pset_workcontrolcommon": "ifcprocessextension",
"pset_zonecommon": "ifcproductextension",
"qto_actuatorbasequantities": "ifcbuildingcontrolsdomain",
"qto_airterminalbasequantities": "ifchvacdomain",
"qto_airterminalboxtypebasequantities": "ifchvacdomain",
"qto_airtoairheatrecoverybasequantities": "ifchvacdomain",
"qto_alarmbasequantities": "ifcbuildingcontrolsdomain",
"qto_audiovisualappliancebasequantities": "ifcelectricaldomain",
"qto_beambasequantities": "ifcsharedbldgelements",
"qto_boilerbasequantities": "ifchvacdomain",
"qto_buildingbasequantities": "ifcproductextension",
"qto_buildingelementproxyquantities": "ifcsharedbldgelements",
"qto_buildingstoreybasequantities": "ifcproductextension",
"qto_burnerbasequantities": "ifchvacdomain",
"qto_cablecarrierfittingbasequantities": "ifcelectricaldomain",
"qto_cablecarriersegmentbasequantities": "ifcelectricaldomain",
"qto_cablefittingbasequantities": "ifcelectricaldomain",
"qto_cablesegmentbasequantities": "ifcelectricaldomain",
"qto_chillerbasequantities": "ifchvacdomain",
"qto_chimneybasequantities": "ifcsharedbldgelements",
"qto_coilbasequantities": "ifchvacdomain",
"qto_columnbasequantities": "ifcsharedbldgelements",
"qto_communicationsappliancebasequantities": "ifcelectricaldomain",
"qto_compressorbasequantities": "ifchvacdomain",
"qto_condenserbasequantities": "ifchvacdomain",
"qto_constructionequipmentresourcebasequantities": "ifcconstructionmgmtdomain",
"qto_constructionmaterialresourcebasequantities": "ifcconstructionmgmtdomain",
"qto_controllerbasequantities": "ifcbuildingcontrolsdomain",
"qto_cooledbeambasequantities": "ifchvacdomain",
"qto_coolingtowerbasequantities": "ifchvacdomain",
"qto_coveringbasequantities": "ifcsharedbldgelements",
"qto_curtainwallquantities": "ifcsharedbldgelements",
"qto_damperbasequantities": "ifchvacdomain",
"qto_distributionchamberelementbasequantities": "ifcsharedbldgserviceelements",
"qto_doorbasequantities": "ifcsharedbldgelements",
"qto_ductfittingbasequantities": "ifchvacdomain",
"qto_ductsegmentbasequantities": "ifchvacdomain",
"qto_ductsilencerbasequantities": "ifchvacdomain",
"qto_electricappliancebasequantities": "ifcelectricaldomain",
"qto_electricdistributionboardbasequantities": "ifcelectricaldomain",
"qto_electricflowstoragedevicebasequantities": "ifcelectricaldomain",
"qto_electricgeneratorbasequantities": "ifcelectricaldomain",
"qto_electricmotorbasequantities": "ifcelectricaldomain",
"qto_electrictimecontrolbasequantities": "ifcelectricaldomain",
"qto_evaporativecoolerbasequantities": "ifchvacdomain",
"qto_evaporatorbasequantities": "ifchvacdomain",
"qto_fanbasequantities": "ifchvacdomain",
"qto_filterbasequantities": "ifchvacdomain",
"qto_firesuppressionterminalbasequantities": "ifcplumbingfireprotectiondomain",
"qto_flowinstrumentbasequantities": "ifcbuildingcontrolsdomain",
"qto_flowmeterbasequantities": "ifchvacdomain",
"qto_footingbasequantities": "ifcstructuralelementsdomain",
"qto_heatexchangerbasequantities": "ifchvacdomain",
"qto_humidifierbasequantities": "ifchvacdomain",
"qto_interceptorbasequantities": "ifcplumbingfireprotectiondomain",
"qto_junctionboxbasequantities": "ifcelectricaldomain",
"qto_laborresourcebasequantities": "ifcconstructionmgmtdomain",
"qto_lampbasequantities": "ifcelectricaldomain",
"qto_lightfixturebasequantities": "ifcelectricaldomain",
"qto_memberbasequantities": "ifcsharedbldgelements",
"qto_motorconnectionbasequantities": "ifcelectricaldomain",
"qto_openingelementbasequantities": "ifcproductextension",
"qto_outletbasequantities": "ifcelectricaldomain",
"qto_pilebasequantities": "ifcstructuralelementsdomain",
"qto_pipefittingbasequantities": "ifchvacdomain",
"qto_pipesegmentbasequantities": "ifchvacdomain",
"qto_platebasequantities": "ifcsharedbldgelements",
"qto_projectionelementbasequantities": "ifcproductextension",
"qto_protectivedevicebasequantities": "ifcelectricaldomain",
"qto_protectivedevicetrippingunitbasequantities": "ifcelectricaldomain",
"qto_pumpbasequantities": "ifchvacdomain",
"qto_railingbasequantities": "ifcsharedbldgelements",
"qto_rampflightbasequantities": "ifcsharedbldgelements",
"qto_reinforcingelementbasequantities": "ifcstructuralelementsdomain",
"qto_roofbasequantities": "ifcsharedbldgelements",
"qto_sanitaryterminalbasequantities": "ifcplumbingfireprotectiondomain",
"qto_sensorbasequantities": "ifcbuildingcontrolsdomain",
"qto_sitebasequantities": "ifcproductextension",
"qto_slabbasequantities": "ifcsharedbldgelements",
"qto_solardevicebasequantities": "ifcelectricaldomain",
"qto_spacebasequantities": "ifcproductextension",
"qto_spaceheaterbasequantities": "ifchvacdomain",
"qto_stackterminalbasequantities": "ifcplumbingfireprotectiondomain",
"qto_stairflightbasequantities": "ifcsharedbldgelements",
"qto_switchingdevicebasequantities": "ifcelectricaldomain",
"qto_tankbasequantities": "ifchvacdomain",
"qto_transformerbasequantities": "ifcelectricaldomain",
"qto_tubebundlebasequantities": "ifchvacdomain",
"qto_unitarycontrolelementbasequantities": "ifcbuildingcontrolsdomain",
"qto_unitaryequipmentbasequantities": "ifchvacdomain",
"qto_valvebasequantities": "ifchvacdomain",
"qto_vibrationisolatorbasequantities": "ifchvacdomain",
"qto_wallbasequantities": "ifcsharedbldgelements",
"qto_wasteterminalbasequantities": "ifcplumbingfireprotectiondomain",
"qto_windowbasequantities": "ifcsharedbldgelements"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,212 @@
{
"IfcActuator": [
{
"name": "Electric Strike",
"predefined_type": "NOTDEFINED"
}
],
"IfcAirTerminal": [
{
"name": "Commercial Kitchen Hood"
}
],
"IfcAirTerminalType": [
{
"name": "Commercial Kitchen Hood"
}
],
"IfcAirTerminalBox": [
{
"name": "VAV Box"
}
],
"IfcCableSegment": [
{
"name": "Lighting Rod"
}
],
"IfcCovering": [
{
"name": "Flashing"
},
{
"name": "Capping"
},
{
"name": "Screed",
"predefined_type": "FLOORING"
},
{
"name": "Screed",
"predefined_type": "TOPPING"
}
],
"IfcElectricDistributionBoard": [
{
"name": "Circuit Breaker Panel",
"predefined_type": "DISTRIBUTIONBOARD"
},
{
"name": "Electrical Panel",
"predefined_type": "DISTRIBUTIONBOARD"
}
],
"IfcElectricDistributionBoardType": [
{
"name": "Circuit Breaker Panel",
"predefined_type": "DISTRIBUTIONBOARD"
},
{
"name": "Electrical Panel",
"predefined_type": "DISTRIBUTIONBOARD"
}
],
"IfcFireSuppressionTerminal": [
{
"name": "Fire Extinguisher"
}
],
"IfcFireSuppressionTerminalType": [
{
"name": "Fire Extinguisher"
}
],
"IfcFurniture": [
{
"name": "Casework"
},
{
"name": "Millwork"
},
{
"name": "Signage"
}
],
"IfcFurnitureType": [
{
"name": "Casework"
},
{
"name": "Millwork"
},
{
"name": "Signage"
}
],
"IfcGeographicElement": [
{
"name": "Trees"
},
{
"name": "Plants"
},
{
"name": "Shrubs"
}
],
"IfcGeographicElementType": [
{
"name": "Trees"
},
{
"name": "Plants"
},
{
"name": "Shrubs"
}
],
"IfcPlate": [
{
"name": "Glazing"
},
{
"name": "Glass"
},
{
"name": "Pane"
}
],
"IfcSensor": [
{
"name": "Card Reader"
},
{
"name": "Fob Reader"
}
],
"IfcSlab": [
{
"name": "Hob"
}
],
"IfcSwitchingDevice": [
{
"name": "Reed Switch"
},
{
"name": "Electric Isolating Switch"
}
],
"IfcUnitaryEquipment": [
{
"name": "Fan Coil Unit"
},
{
"name": "Roof Top Unit (RTU)",
"predefined_type": "ROOFTOPUNIT"
},
{
"name": "Furnace"
},
{
"name": "Central air conditioner (split or package)",
"predefined_type": "AIRCONDITIONINGUNIT"
},
{
"name": "Mini-split system",
"predefined_type": "SPLITSYSTEM"
}
],
"IfcUnitaryEquipmentType": [
{
"name": "Fan Coil Unit"
},
{
"name": "Roof Top Unit (RTU)",
"predefined_type": "ROOFTOPUNIT"
},
{
"name": "Furnace"
},
{
"name": "Central air conditioner (split or package)",
"predefined_type": "AIRCONDITIONINGUNIT"
},
{
"name": "Mini-split system",
"predefined_type": "SPLITSYSTEM"
}
],
"IfcWall": [
{
"name": "Glazing"
},
{
"name": "Glass"
},
{
"name": "Pane"
}
],
"IfcWindow": [
{
"name": "Glazing"
},
{
"name": "Glass"
},
{
"name": "Pane"
}
]
}
@@ -0,0 +1,274 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.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/>.
"""Script to ensure ifcopenshell_wrapper.pyi and ifcopenshell_wrapper.py work in sync.
Things we do check:
- all symbols from the wrapper present in the stub and vice versa
- functions and methods signatures
- read-only and settable properties, staticmethods
- class hierarchy
"""
import ast
import difflib
from pathlib import Path
from typing import Union
from typing_extensions import assert_never
def format_diff(lines: list[str]) -> None:
RED = "\033[91m"
GREEN = "\033[92m"
CYAN = "\033[96m"
RESET = "\033[0m"
for line in lines:
if line.startswith("+") and not line.startswith("+++"):
print(f"{GREEN}{line}{RESET}")
elif line.startswith("-") and not line.startswith("---"):
print(f"{RED}{line}{RESET}")
elif line.startswith("@@"):
print(f"{CYAN}{line}{RESET}")
else:
print(line)
SubnameType = Union[str, tuple[str, str]]
def get_function_node_name(node: ast.FunctionDef) -> Union[SubnameType, None]:
"""
:return: Function node name as ``SubnameType`` or ``None``, if function wasn't processed and can be skipped.
"""
node_name = node.name
is_init = node_name == "__init__"
if node_name.startswith("_") and node_name not in ("_is",) and not is_init:
return None
arg_nodes = node.args.args
defaults = [None] * (len(arg_nodes) - len(node.args.defaults)) + node.args.defaults
args: list[str] = []
for arg, default in zip(arg_nodes, defaults):
if default is None:
args.append(arg.arg)
else:
args.append(f"{arg.arg}={ast.unparse(default)}")
if arg := node.args.vararg:
args.append(f"*{arg.arg}")
if arg := node.args.kwarg:
args.append(f"**{arg.arg}")
# Skip non-informative constructors.
if is_init and args == ["self"]:
return None
node_name = f"def {node.name}"
node_name = f"{node_name}({', '.join(args)}): ..."
decorators = tuple(f"@{d.id}" for d in node.decorator_list if isinstance(d, ast.Name))
if len(decorators) == 1:
return decorators + (node_name,)
return node_name
def get_names_tree_lines(tree: ast.Module) -> list[str]:
# Get class tree.
names_tree: dict[str, set[SubnameType]] = {}
for node in tree.body:
subnames: set[SubnameType] = set()
node_name = None
if isinstance(node, ast.ClassDef):
# Skip `object_` as it's just a reference to `object`,
# which is implied by default.
bases = [b.id for b in node.bases if isinstance(b, ast.Name) and b.id not in ("_object", "object")]
bases_str = f"({', '.join(bases)})" if bases else ""
node_name = f"class {node.name}{bases_str}:"
for subnode in node.body:
subname = None
if isinstance(subnode, ast.AnnAssign):
target = subnode.target
assert isinstance(target, ast.Name)
subname = target.id
elif isinstance(subnode, ast.FunctionDef):
subname = get_function_node_name(subnode)
elif isinstance(subnode, ast.Assign):
targets = subnode.targets
if not len(targets) == 1 or not isinstance(target := targets[0], ast.Name):
continue
subname_ = target.id
if subname_.startswith(("_", "thisown")):
continue
value = subnode.value
if not isinstance(value, ast.Call):
subname = subname_
else:
# Catching wrappers like:
# - `matrix = property(matrix_getter)`
# - `matrix = property(matrix_getter, matrix_setter)`
# - `operation_str = staticmethod(operation_str)`
func = value.func
if not isinstance(func, ast.Name) or ((func_id := func.id) not in ("property", "staticmethod")):
continue
args = [arg.id for arg in value.args if isinstance(arg, ast.Name)]
len_args = len(args)
if len_args in (1, 2):
def find_method_by_name(name: str) -> Union[str, None]:
function_def = f"def {name}("
return next(
(
func_
for func_ in subnames
if isinstance(func_, str) and func_.startswith(function_def)
),
None,
)
# Use `set` for cases like `description = property(description, description)`.
wrapped_function = None
for arg in set(args):
assert (wrapped_function := find_method_by_name(arg))
subnames.remove(wrapped_function)
# TODO: sort it out in wrapper.py
# There's one annoying case in Element.product
# when property is overriding existing function, without using it.
# We should probably just exclude that function from the wrapper.
overridden_name = find_method_by_name(subname_)
if overridden_name:
subnames.remove(overridden_name)
if len_args == 2:
# Has both getter and setter, can be defined as a simple attribute.
subname = subname_
elif len_args == 1:
if func_id == "property":
# Has just getter, read-only, need to define it using a wrapper.
subname = (f"@{func_id}", f"def {subname_}(self): ...")
elif func_id == "staticmethod":
assert wrapped_function is not None
subname = (f"@{func_id}", f"def {subname_}({wrapped_function.split('(')[1]}")
else:
assert_never(func_id)
else:
assert_never(len_args)
else:
attr_args = [
arg
for arg in value.args
if isinstance(arg, ast.Attribute)
and isinstance(arg.value, ast.Name)
and arg.value.id == "_ifcopenshell_wrapper"
]
assert len(attr_args) == 2
subname = subname_
if subname is not None:
subnames.add(subname)
if not subnames:
node_name += " ..."
elif isinstance(node, ast.FunctionDef):
if node.name.startswith("_"):
continue
node_name = get_function_node_name(node)
assert isinstance(node_name, str)
elif isinstance(node, ast.Assign):
targets = node.targets
if not len(targets) == 1 or not isinstance(target := targets[0], ast.Name):
continue
node_name = target.id
elif isinstance(node, ast.AnnAssign):
target = node.target
assert isinstance(target, ast.Name)
node_name = target.id
if node_name is not None:
names_tree[node_name] = subnames
# Convert names tree to lines.
lines: list[str] = []
indent = " " * 4
def subname_sort(subname: SubnameType) -> str:
if isinstance(subname, str):
if subname.startswith("def "):
return subname.split("(")[0].removeprefix("def ")
return subname
return subname_sort(subname[1])
for name, subnames in sorted(names_tree.items(), key=lambda x: x[0]):
lines.append(name)
for subname in sorted(subnames, key=subname_sort):
subitems = (subname,) if isinstance(subname, str) else subname
for item in subitems:
lines.append(f"{indent}{item}")
return lines
def main() -> None:
package = Path(__file__).parent.parent.parent
stub_path = package / "ifcopenshell_wrapper.pyi"
wrapper_path = package / "ifcopenshell_wrapper.py"
# Parse files
stub_tree = ast.parse(stub_path.read_text())
wrapper_tree = ast.parse(wrapper_path.read_text())
# Extract class names
stub_classes = get_names_tree_lines(stub_tree)
wrapper_classes = get_names_tree_lines(wrapper_tree)
# Use difflib to create a unified diff of class names
diff = difflib.unified_diff(
stub_classes,
wrapper_classes,
fromfile="stub.pyi classes",
tofile="wrapper.py classes",
lineterm="",
n=10,
)
diff = list(diff)
format_diff(diff)
diff_no_header = diff[2:]
added = len([l for l in diff_no_header if l.startswith("+")])
removed = len([l for l in diff_no_header if l.startswith("-")])
if added or removed:
print(f"Added lines: {added}")
print(f"Removed lines: {removed}")
raise Exception("Found discrepancies between stub and wrapper.")
else:
print(f"All good, no discrepancies between stub and wrapper. 🎉🎉")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,465 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
import datetime
from collections.abc import Generator
from functools import cache
from math import floor
from typing import Literal, Optional, Union
import ifcopenshell.util.date
import ifcopenshell.util.element
DURATION_TYPE = Literal["ELAPSEDTIME", "WORKTIME", "NOTDEFINED"]
RECURRENCE_TYPE = Literal[
"BY_DAY_COUNT",
"BY_WEEKDAY_COUNT",
"DAILY",
"MONTHLY_BY_DAY_OF_MONTH",
"MONTHLY_BY_POSITION",
"WEEKLY",
"YEARLY_BY_DAY_OF_MONTH",
"YEARLY_BY_POSITION",
]
def derive_date(
task: ifcopenshell.entity_instance,
attribute_name: str,
date=None,
is_earliest: bool = False,
is_latest: bool = False,
):
"""
:param task: IfcTask.
"""
if task.TaskTime:
current_date = (
ifcopenshell.util.date.ifc2datetime(getattr(task.TaskTime, attribute_name))
if getattr(task.TaskTime, attribute_name)
else ""
)
if current_date:
return current_date
for subtask in get_all_nested_tasks(task):
current_date = derive_date(
subtask,
attribute_name,
date=date,
is_earliest=is_earliest,
is_latest=is_latest,
)
if is_earliest:
if current_date and (date is None or current_date < date):
date = current_date
if is_latest:
if current_date and (date is None or current_date > date):
date = current_date
return date
def derive_calendar(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
calendar = get_calendar(task)
if calendar:
return calendar
for rel in task.Nests or []:
return derive_calendar(rel.RelatingObject)
def get_calendar(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
calendar = [
rel.RelatingControl
for rel in task.HasAssignments or []
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkCalendar")
]
if calendar:
return calendar[0]
def count_working_days(start, finish, calendar: ifcopenshell.entity_instance) -> int:
result = 0
if start == finish:
return 0
current_date = datetime.date(start.year, start.month, start.day)
finish_date = datetime.date(finish.year, finish.month, finish.day)
while current_date <= finish_date:
if calendar and calendar.WorkingTimes and is_working_day(current_date, calendar):
result += 1
elif not calendar or not is_calendar_applicable(current_date, calendar):
result += 1
current_date += datetime.timedelta(days=1)
return result
def get_start_or_finish_date(
start,
duration,
duration_type: DURATION_TYPE,
calendar: ifcopenshell.entity_instance,
date_type: Literal["START", "FINISH"] = "FINISH",
):
if not duration.days:
# Typically a milestone will have zero duration, so the start == finish
return start
# We minus 1 because the start day itself is counted as a day
months = int(getattr(duration, "months", 0))
years = int(getattr(duration, "years", 0))
total_duration = duration.days + months * 30 + years * 12 * 30
duration = datetime.timedelta(days=total_duration - 1)
if date_type == "START":
duration = -duration
result = offset_date(start, duration, duration_type, calendar)
if date_type == "START":
return datetime.datetime.combine(result, datetime.time(9))
return datetime.datetime.combine(result, datetime.time(17))
def offset_date(start, duration, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance):
current_date = start
months = getattr(duration, "months", 0)
years = getattr(duration, "years", 0)
abs_duration = abs(duration.days + months * 30 + years * 12 * 30)
date_offset = datetime.timedelta(days=1 if duration.days > 0 else -1)
while abs_duration > 0:
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(current_date, calendar):
abs_duration -= 1
elif is_working_day(current_date, calendar):
abs_duration -= 1
current_date += date_offset
if duration.days > 0:
current_date = get_soonest_working_day(current_date, duration_type, calendar)
else:
current_date = get_recent_working_day(current_date, duration_type, calendar)
return current_date
def get_soonest_working_day(start, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance):
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(start, calendar):
return start
while not is_working_day(start, calendar):
if not is_calendar_applicable(start, calendar):
break
start += datetime.timedelta(days=1)
return start
def get_recent_working_day(start, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance):
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(start, calendar):
return start
while not is_working_day(start, calendar):
if not is_calendar_applicable(start, calendar):
break
start -= datetime.timedelta(days=1)
return start
@cache
def is_working_day(day, calendar: ifcopenshell.entity_instance) -> bool:
is_working_day = False
for work_time in calendar.WorkingTimes or []:
if is_work_time_applicable_to_day(work_time, day):
is_working_day = True
break
if not is_working_day:
return is_working_day
for work_time in calendar.ExceptionTimes or []:
if is_work_time_applicable_to_day(work_time, day):
is_working_day = False
break
return is_working_day
@cache
def is_calendar_applicable(day, calendar: ifcopenshell.entity_instance) -> bool:
if not calendar or not calendar.WorkingTimes:
return False
is_applicable = False
for work_time in calendar.WorkingTimes or []:
if is_day_in_work_time(day, work_time):
is_applicable = True
break
return is_applicable
def is_day_in_work_time(day, work_time: ifcopenshell.entity_instance) -> bool:
is_day_in_work_time = True
if isinstance(day, datetime.datetime):
day = datetime.date(day.year, day.month, day.day)
# 4 IfcWorktime Start
if start := work_time[4]:
start = ifcopenshell.util.date.ifc2datetime(start)
if day > start:
is_day_in_work_time = True
else:
is_day_in_work_time = False
# 5 IfcWorktime Finish
if finish := work_time[5]:
finish = ifcopenshell.util.date.ifc2datetime(finish)
if day < finish:
is_day_in_work_time = True
else:
is_day_in_work_time = False
return is_day_in_work_time
def is_work_time_applicable_to_day(work_time: ifcopenshell.entity_instance, day) -> bool:
if not is_day_in_work_time(day, work_time):
return False
if not work_time.RecurrencePattern:
return True
if isinstance(day, datetime.datetime):
day = datetime.date(day.year, day.month, day.day)
recurrence = work_time.RecurrencePattern
recurrence_type: RECURRENCE_TYPE = recurrence.RecurrenceType
if recurrence_type == "DAILY":
if not recurrence.Interval and not recurrence.Occurrences:
return True
# 4 IfcWorktime Start
if not work_time[4]:
return False
return False # TODO
elif recurrence_type == "WEEKLY":
if not recurrence.Interval and not recurrence.Occurrences:
return (day.weekday() + 1) in recurrence.WeekdayComponent
# 4 IfcWorktime Start
if not work_time[4]:
return False
return False # TODO
elif recurrence_type == "MONTHLY_BY_DAY_OF_MONTH":
if not recurrence.Interval and not recurrence.Occurrences:
return day.day in recurrence.DayComponent
return False # TODO
elif recurrence_type == "MONTHLY_BY_POSITION":
if not recurrence.Interval and not recurrence.Occurrences:
return (day.weekday() + 1) in recurrence.WeekdayComponent and floor(day.day / 7) + 1 == recurrence[
"Position"
]
return False # TODO
elif recurrence_type == "YEARLY_BY_DAY_OF_MONTH":
if not recurrence.Interval and not recurrence.Occurrences:
return day.month in recurrence.MonthComponent and day.day in recurrence.DayComponent
return False # TODO
elif recurrence_type == "YEARLY_BY_POSITION":
if not recurrence.Interval and not recurrence.Occurrences:
return (
day.month in recurrence.MonthComponent
and (day.weekday() + 1) in recurrence.WeekdayComponent
and floor(day.day / 7) + 1 == recurrence.Position
)
return False # TODO
def get_task_work_schedule(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
parent_task = get_parent_task(task)
if parent_task:
return get_task_work_schedule(parent_task) or get_task_work_schedule(task)
else:
for rel in task.HasAssignments:
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkSchedule"):
return rel.RelatingControl
return None
def get_nested_tasks(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [obj for obj in ifcopenshell.util.element.get_components(task) if obj.is_a("IfcTask")]
def get_parent_task(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
nests = task.Nests
if nests and (obj := nests[0].RelatingObject).is_a("IfcTask"):
return obj
def get_all_nested_tasks(task: ifcopenshell.entity_instance) -> Generator[ifcopenshell.entity_instance]:
for nested_task in get_nested_tasks(task):
yield nested_task
yield from get_all_nested_tasks(nested_task)
def get_work_schedule_tasks(work_schedule: ifcopenshell.entity_instance) -> Generator[ifcopenshell.entity_instance]:
"""Get all work schedule tasks, including the nested ones."""
for root_task in get_root_tasks(work_schedule):
yield root_task
yield from get_all_nested_tasks(root_task)
def get_root_tasks(work_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [obj for rel in work_schedule.Controls for obj in rel.RelatedObjects if obj.is_a("IfcTask")]
def guess_date_range(work_schedule: ifcopenshell.entity_instance):
earliest = None
latest = None
root_tasks = get_root_tasks(work_schedule)
tasks_with_assignements = []
for task in root_tasks:
if has_task_outputs(task) or has_task_inputs(task):
tasks_with_assignements.append(task)
for sub_task in get_all_nested_tasks(task):
if has_task_outputs(sub_task) or has_task_inputs(sub_task):
tasks_with_assignements.append(sub_task)
for task in tasks_with_assignements:
derived_start = derive_date(task, "ScheduleStart", is_earliest=True)
derived_finish = derive_date(task, "ScheduleFinish", is_latest=True)
if derived_start and (not earliest or derived_start < earliest):
earliest = derived_start
if derived_finish and (not latest or derived_finish > latest):
latest = derived_finish
return earliest, latest
def get_task_outputs(
task: ifcopenshell.entity_instance, is_recursive: bool = False
) -> set[ifcopenshell.entity_instance]:
if is_recursive:
return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_outputs(subtask)}
return {rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")}
def get_task_inputs(
task: ifcopenshell.entity_instance, is_recursive: bool = False
) -> set[ifcopenshell.entity_instance]:
if is_recursive:
return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_inputs(subtask)}
return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcProduct")}
def get_task_resources(
task: ifcopenshell.entity_instance, is_recursive: bool = False
) -> set[ifcopenshell.entity_instance]:
if is_recursive:
return {r for subtask in [task] + list(get_all_nested_tasks(task)) for r in get_task_resources(subtask)}
return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcResource")}
def has_task_outputs(task: ifcopenshell.entity_instance) -> bool:
return len(get_task_outputs(task)) > 0
def has_task_inputs(task: ifcopenshell.entity_instance) -> bool:
return len(get_task_inputs(task)) > 0
def get_tasks_for_product(
product: ifcopenshell.entity_instance, schedule: Optional[ifcopenshell.entity_instance] = None
) -> tuple[list[ifcopenshell.entity_instance], list[ifcopenshell.entity_instance]]:
"""
Get all tasks assigned to or referenced by the given product.
:param product: An object that is assigned tasks or references tasks.
:param schedule: An optional string representing the schedule name to filter tasks by.
:return: A tuple of two lists:
- The first list contains all tasks assigned to the product.
- The second list contains all tasks referenced by the product that are part of the given schedule.
"""
inputs = [
assignement.RelatingProcess
for assignement in product.HasAssignments
if assignement.is_a("IfcRelAssignsToProcess") and assignement.RelatingProcess.is_a("IfcTask")
]
outputs = [
obj
for ref in product.ReferencedBy
if ref.is_a("IfcRelAssignsToProduct")
for obj in ref.RelatedObjects
if obj.is_a("IfcTask")
]
if schedule:
inputs = [task for task in inputs if get_task_work_schedule(task).id() == schedule.id()]
outputs = [task for task in outputs if get_task_work_schedule(task).id() == schedule.id()]
return inputs, outputs
def get_sequence_assignment(task: ifcopenshell.entity_instance, sequence="successor"):
if sequence == "successor":
relationship_attr = "IsPredecessorTo"
elif sequence == "predecessor":
relationship_attr = "IsSuccessorFrom"
else:
return []
relationship = getattr(task, relationship_attr, None)
if relationship:
return relationship
for rel in task.Nests or []:
result = get_sequence_assignment(rel.RelatingObject, sequence)
if result:
return result
return []
def get_related_products(
relating_product: Optional[ifcopenshell.entity_instance] = None,
related_object: Optional[ifcopenshell.entity_instance] = None,
) -> set[ifcopenshell.entity_instance]:
"""Gets the related products being output by a task
:param relating_product: One of the products already output by the task.
:param related_object: The IfcTask that you want to get all the related
products for.
:return: A set of IfcProducts output by the IfcTask.
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's create a construction task. Note that the predefined type is
# important to distinguish types of tasks.
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Build wall", identification="A", predefined_type="CONSTRUCTION")
# Let's say we have a wall somewhere.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# Let's construct that wall!
ifcopenshell.api.sequence.assign_product(relating_product=wall, related_object=task)
# This will give us a set with that wall in it.
products = ifcopenshell.util.sequence.get_related_products(related_object=task)
"""
assert relating_product or related_object, "Either relating_product or related_object must be provided."
products = set()
if not related_object and relating_product:
for reference in relating_product.ReferencedBy:
if reference.is_a("IfcRelAssignsToProduct"):
related_object = reference.RelatedObjects[0]
if related_object:
assignments = related_object.HasAssignments
for assignment in assignments:
if assignment.is_a("IfcRelAssignsToProduct"):
products.add(assignment.RelatingProduct.id())
return products
@@ -0,0 +1,754 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.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/>.
from __future__ import annotations
from math import cos, radians
from typing import TYPE_CHECKING, Literal, Optional, Union
import numpy as np
import numpy.typing as npt
import shapely
import shapely.ops
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
if TYPE_CHECKING:
import ifcopenshell.ifcopenshell_wrapper as W
from ifcopenshell.geom import ShapeElementType
from ifcopenshell.util.shape_builder import VectorType
AXIS_LITERAL = Literal["X", "Y", "Z"]
VECTOR_3D = tuple[float, float, float]
# Used only for typing, but reused by `shape.py` users.
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
tol = 1e-6
# NOTE: See IfcGeomRepresentation.h for W.Triangulation buffer types.
# NOTE: For functions that return a single scalar ensure to use .item() to
# return the Python float instead of numpy float
# as it's less intrusive (doesn't promote numpy arrays on interactions),
# doesn't fail saving to IFC
# and precise enough anyway (internally Python floats are doubles).
def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
"""Checks whether a value is equivalent to X given a tolerance
:param value: Input value
:param x: The value to compare to
:param tolerance: The tolerance to use. Defaults to 1e-6.
:return: True or false
"""
if tolerance is None:
tolerance = tol
return abs(x - value) < tolerance
def get_volume(geometry: W.Triangulation) -> float:
"""Calculates the total internal volume of a geometry
Volumes of non-manifold geometry will be unpredictable.
:param geometry: Geometry output calculated by IfcOpenShell
:return: The volume in m3
"""
# https://stackoverflow.com/questions/1406029/how-to-calculate-the-volume-of-a-3d-mesh-object-the-surface-of-which-is-made-up
def signed_triangle_volume(p1, p2, p3):
v321 = p3[0] * p2[1] * p1[2]
v231 = p2[0] * p3[1] * p1[2]
v312 = p3[0] * p1[1] * p2[2]
v132 = p1[0] * p3[1] * p2[2]
v213 = p2[0] * p1[1] * p3[2]
v123 = p1[0] * p2[1] * p3[2]
return (1.0 / 6.0) * (-v321 + v231 + v312 - v132 - v213 + v123)
# Can't optimize it using buffers - performance seems to get only worse.
verts = geometry.verts
faces = geometry.faces
grouped_verts = [[verts[i], verts[i + 1], verts[i + 2]] for i in range(0, len(verts), 3)]
volumes = [
signed_triangle_volume(grouped_verts[faces[i]], grouped_verts[faces[i + 1]], grouped_verts[faces[i + 2]])
for i in range(0, len(faces), 3)
]
return abs(sum(volumes))
def get_x(geometry: W.Triangulation) -> float:
"""Calculates the X length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:return: The X dimension
"""
verts_flat = get_vertices(geometry).ravel()
return (np.max(verts_flat[0::3]) - np.min(verts_flat[0::3])).item()
def get_y(geometry: W.Triangulation) -> float:
"""Calculates the Y length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:return: The Y dimension
"""
verts_flat = get_vertices(geometry).ravel()
return (np.max(verts_flat[1::3]) - np.min(verts_flat[1::3])).item()
def get_z(geometry: W.Triangulation) -> float:
"""Calculates the Z length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:return: The Z dimension
"""
verts_flat = get_vertices(geometry).ravel()
return (np.max(verts_flat[2::3]) - np.min(verts_flat[2::3])).item()
def get_max_xy(geometry: W.Triangulation) -> float:
"""Gets the maximum X or Y length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:return: The maximum possible value out of the X and Y dimension
"""
return max(get_x(geometry), get_y(geometry))
def get_max_xyz(geometry: W.Triangulation) -> float:
"""Gets the maximum X, Y, or Z length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:return: The maximum possible value out of the X, Y, and Z dimension
"""
return max(get_x(geometry), get_y(geometry), get_z(geometry))
def get_min_xyz(geometry: W.Triangulation) -> float:
"""Gets the minimum X, Y, or Z length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:return: The minimum possible value out of the X, Y, and Z dimension
"""
return min(get_x(geometry), get_y(geometry), get_z(geometry))
def get_shape_matrix(shape: ShapeElementType) -> MatrixType:
"""Formats the transformation matrix of a shape as a 4x4 numpy array
:param shape: Shape output calculated by IfcOpenShell
:return: A 4x4 numpy array representing the transformation matrix
"""
return np.frombuffer(shape.transformation_buffer, "d").reshape((4, 4), order="F")
def get_bbox_centroid(geometry: W.Triangulation) -> tuple[float, float, float]:
"""Calculates the bounding box centroid of the geometry
The centroid is in local coordinates relative to the object's placement.
:param geometry: Geometry output calculated by IfcOpenShell
:return: A tuple representing the XYZ centroid
"""
vertices_array = get_vertices(geometry)
return (np.min(vertices_array, axis=0) + np.max(vertices_array, axis=0)) / 2
def get_vert_centroid(geometry: W.Triangulation) -> tuple[float, float, float]:
"""Calculates the average vertex centroid of the geometry
The centroid is in local coordinates relative to the object's placement.
:param geometry: Geometry output calculated by IfcOpenShell
:return: A tuple representing the XYZ centroid
"""
return np.mean(get_vertices(geometry), axis=0)
def get_element_bbox_centroid(
element: ifcopenshell.entity_instance, geometry: W.Triangulation
) -> npt.NDArray[np.float64]:
"""Calculates the element's bounding box centroid
The centroid is in global coordinates. Note that if you have the shape, it
is more efficient to use :func:`get_shape_bbox_centroid`.
:param element: The element occurrence
:param geometry: Geometry output calculated by IfcOpenShell
:return: A tuple representing the XYZ centroid
"""
centroid = get_bbox_centroid(geometry)
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
return np.array(centroid)
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
return (mat @ np.array([*centroid, 1.0]))[0:3]
def get_shape_bbox_centroid(shape: ShapeElementType, geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Calculates the shape's bounding box centroid
The centroid is in global coordinates. Note that if you do not have the
shape, you can use :func:`get_element_bbox_centroid`.
:param shape: Shape output calculated by IfcOpenShell
:param geometry: Geometry output calculated by IfcOpenShell
:return: A tuple representing the XYZ centroid
"""
centroid = get_bbox_centroid(geometry)
return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3]
def get_vertices(geometry: W.Triangulation, is_2d: bool = False) -> npt.NDArray[np.float64]:
"""Get all the vertices as a numpy array
Vertices are in local coordinates.
:param geometry: Geometry output calculated by IfcOpenShell
:param is_2d: Set to True to to get XY coordinates only.
:return: A numpy array listing all the vertices and their coordinates.
Array shape: (n, 3), where n - number of vertices.
"""
if is_2d:
return np.frombuffer(geometry.verts_buffer, "d").reshape(-1, 3)[:, :2]
return np.frombuffer(geometry.verts_buffer, "d").reshape(-1, 3)
def get_edges(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get all the edges as a numpy array
Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...]
Note that although geometry always holds triangulated faces, edges will
represent the original tessellation or BRep's faces, which may be quads or
ngons.
:param geometry: Geometry output calculated by IfcOpenShell
:return: A numpy array listing all the edges.
Array shape: (n, 2), where n - number of edges.
"""
return np.frombuffer(geometry.edges_buffer, dtype="i").reshape(-1, 2)
def get_faces(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get all the faces as a numpy array
Faces are always triangulated. If the shape is a BRep and you want to get
the original untriangulated output, refer to :func:`get_edges`.
Results are a nested numpy array e.g. [[f1v1, f1v2, f1v3], [f2v1, f2v2, f2v3], ...]
:param geometry: Geometry output calculated by IfcOpenShell
:return: A numpy array listing all the faces.
Array shape: (n, 3), where n - number of faces.
"""
return np.frombuffer(geometry.faces_buffer, dtype="i").reshape(-1, 3)
def get_material_colors(geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Get material colors as a numpy array.
:return: A numpy array listing RGBA color for each shape's material.
Array shape: (1, 4).
"""
# colors_buffer comes from geometry.materials and doesn't account
# for colors that can be set by some other way (e.g. IfcIndexedColourMap).
return np.frombuffer(geometry.colors_buffer, dtype="d").reshape(-1, 4)
def get_normals(geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Get vertex normals as a numpy array.
See geometry settings documentation for settings that affect normals.
:return: A numpy array listing normal for each shape vertex.
Array shape: (1, 3).
"""
return np.frombuffer(geometry.normals_buffer, dtype="d").reshape(-1, 3)
def get_shape_material_styles(geometry: W.Triangulation) -> tuple[W.style, ...]:
"""Get list of material styles."""
return geometry.materials
def get_faces_material_style_ids(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get material styles ids for the geometry faces.
Return a list of corresponding indices of styles from get_shape_material_styles for each face.
If face has no style assigned, index -1 is used.
"""
return np.frombuffer(geometry.material_ids_buffer, dtype="i")
def get_faces_representation_item_ids(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get representation item ids for the geometry faces."""
return np.frombuffer(geometry.item_ids_buffer, dtype="i")
def get_edges_representation_item_ids(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get representation item ids for the geometry edges.
Can be useful for geometry without faces and in general is more universal
since it's possible that geometry will have elements with and without faces.
"""
return np.frombuffer(geometry.edges_item_ids_buffer, dtype="i")
def get_shape_vertices(shape: ShapeElementType, geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Get the shape's vertices as a numpy array
Vertices are in global coordinates. If you do not have the shape, you can
use :func:`get_element_vertices`.
Results are a nested numpy array e.g. [[v1x, v1y, v1z], [v2x, v2y, v2z], ...]
:param shape: Shape output calculated by IfcOpenShell
:param geometry: Geometry output calculated by IfcOpenShell
:return: A numpy array listing all the vertices. Each vertex is a numpy array with XYZ coordinates.
Array shape: (n, 3), where n - number of vertices.
"""
verts = get_vertices(geometry)
mat = get_shape_matrix(shape)
return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1)
def get_element_vertices(element: ifcopenshell.entity_instance, geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Get the element's vertices as a numpy array
Vertices are in global coordinates. Note that if you have the shape, it is
more efficient to use :func:`get_shape_vertices`.
Results are a nested numpy array e.g. [[v1x, v1y, v1z], [v2x, v2y, v2z], ...]
:param element: The element occurrence
:param geometry: Geometry output calculated by IfcOpenShell
:return: A numpy array listing all the vertices. Each vertex is a numpy array with XYZ coordinates.
"""
verts = get_vertices(geometry)
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
return verts
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1)
def get_bottom_elevation(geometry: W.Triangulation) -> float:
"""Gets the lowest local Z ordinate of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:return: The Z value
"""
verts_flat = get_vertices(geometry).ravel()
return np.min(verts_flat[2::3]).item()
def get_top_elevation(geometry: W.Triangulation) -> float:
"""Gets the highest local Z ordinate of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:return: The Z value
"""
verts_flat = get_vertices(geometry).ravel()
return np.max(verts_flat[2::3]).item()
def get_shape_bottom_elevation(shape: ShapeElementType, geometry: W.Triangulation) -> float:
"""Gets the lowest global Z ordinate of the shape
If you do not have the shape, you can use :func:`get_element_bottom_elevation`
instead.
:param shape: Shape output calculated by IfcOpenShell
:param geometry: Geometry output calculated by IfcOpenShell
:return: The Z value
"""
return min([v[2] for v in get_shape_vertices(shape, geometry)])
def get_shape_top_elevation(shape: ShapeElementType, geometry: W.Triangulation) -> float:
"""Gets the highest global Z ordinate of the shape
If you do not have the shape, you can use :func:`get_element_top_elevation`
instead.
:param shape: Shape output calculated by IfcOpenShell
:param geometry: Geometry output calculated by IfcOpenShell
:return: The Z value
"""
return max([v[2] for v in get_shape_vertices(shape, geometry)])
def get_element_bottom_elevation(element: ifcopenshell.entity_instance, geometry: W.Triangulation) -> float:
"""Gets the lowest global Z ordinate of the element
Note that if you have the shape, it is more efficient to use
:func:`get_shape_bottom_elevation`.
:param element: The element occurrence
:param geometry: Geometry output calculated by IfcOpenShell
:return: The Z value
"""
return min([v[2] for v in get_element_vertices(element, geometry)])
def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry: W.Triangulation) -> float:
"""Gets the highest global Z ordinate of the element
Note that if you have the shape, it is more efficient to use
:func:`get_shape_top_elevation`.
:param element: The element occurrence
:param geometry: Geometry output calculated by IfcOpenShell
:return: The Z value
"""
return max([v[2] for v in get_element_vertices(element, geometry)])
def get_bbox(vertices: npt.NDArray[np.float64]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""Gets the bounding box of vertices
:param vertices: An iterable of vertices
:return: The bounding box value represented as a tuple of two numpy arrays.
The first holds the bottom left corner and the second holds the top
right. E.g. (np.array([minx, miny, minz]), np.array([maxx, maxy,
maxz]))
"""
return (np.min(vertices, axis=0), np.max(vertices, axis=0))
def get_area_vf(vertices: npt.NDArray[np.float64], faces: npt.NDArray[np.int32]) -> float:
"""Calculates the surface area given a list of vertices and triangulated faces
:param vertices: A list of 3D vertices, such as returned from get_vertices.
:param faces: A list of faces, such as returned from get_faces.
:return: The surface area.
"""
# Calculate the triangle normal vectors
v1 = vertices[faces[:, 1]] - vertices[faces[:, 0]]
v2 = vertices[faces[:, 2]] - vertices[faces[:, 0]]
triangle_normals = np.cross(v1, v2)
# Normalize the normal vectors to get their length (i.e., triangle area)
triangle_areas = np.linalg.norm(triangle_normals, axis=1) / 2
# Sum up the areas to get the total area of the mesh
mesh_area = np.sum(triangle_areas)
return mesh_area.item()
def get_area(geometry: W.Triangulation) -> float:
"""Calculates the surface area of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:return: The surface area.
"""
vertices = get_vertices(geometry)
faces = get_faces(geometry)
return get_area_vf(vertices, faces)
def get_side_area(
geometry: W.Triangulation,
axis: AXIS_LITERAL = "Y",
direction: Optional[VectorType] = None,
angle: float = 90.0,
) -> float:
"""Calculates the total surface area of surfaces that are visible from the specified axis
This is typically useful for calculating elevational areas. For example,
you might want to calculate the side area of a wall (i.e. only one side,
not both).
Surfaces do not need to be exactly perpendicular in the direction of the
specified axis. A surface is counted so long as it is visible from that
axis.
Note that this calculates the actual area, not the projected 2D area. If
you want the projected area, use :func:`get_footprint_area`.
:param geometry: Geometry output calculated by IfcOpenShell
:param axis: Either X, Y, or Z. Defaults to Y, which is used for standard
walls.
:param angle: Accept angle difference between face and axis, in degrees.
E.g. default angle 90 will find all faces with angle < 90 degrees.
:return: The surface area.
"""
if direction is None:
direction = {"X": (1.0, 0.0, 0.0), "Y": (0.0, 1.0, 0.0), "Z": (0.0, 0.0, 1.0)}[axis]
vertices = get_vertices(geometry)
faces = get_faces(geometry)
# Calculate the triangle normal vectors
v1 = vertices[faces[:, 1]] - vertices[faces[:, 0]]
v2 = vertices[faces[:, 2]] - vertices[faces[:, 0]]
triangle_normals = np.cross(v1, v2)
# Normalize the normal vectors
triangle_normals = triangle_normals / np.linalg.norm(triangle_normals, axis=1)[:, np.newaxis]
direction = np.array(direction) / np.linalg.norm(direction)
# Find the faces with a normal vector pointing in the desired +Y normal direction
# normal_tol < 0 is pointing away, = 0 is perpendicular, and > 0 is pointing towards.
normal_tol = 0.01 # For angle 90 it's close to perpendicular, but with a fuzz for numerical tolerance
acceptable_dot = cos(radians(angle)) + normal_tol
dot_products = np.dot(triangle_normals, direction)
filtered_face_indices = np.where(dot_products > acceptable_dot)[0]
filtered_faces = faces[filtered_face_indices]
return get_area_vf(vertices, filtered_faces)
def get_max_side_area(geometry: W.Triangulation) -> float:
"""Returns the maximum X, Y, or Z side area
See :func:`get_side_area` for how side area is calculated.
:param geometry: Geometry output calculated by IfcOpenShell
:return: The maximum surface area from either the X, Y, or Z axis.
"""
return max(get_side_area(geometry, axis="X"), get_side_area(geometry, axis="Y"), get_side_area(geometry, axis="Z"))
def get_top_area(geometry: W.Triangulation) -> float:
return get_side_area(geometry, axis="Z", angle=45)
def get_footprint_area(
geometry: W.Triangulation,
axis: AXIS_LITERAL = "Z",
direction: Optional[VECTOR_3D] = None,
) -> float:
"""Calculates the total footprint (i.e. projected) surface area visible from along an axis
This is typically useful for calculating footprint areas. For example, you
might want to calculate the top-down footprint area of a slab, ignoring
slopes in the slab.
Surfaces do not need to be exactly perpendicular in the direction of the
specified axis. A surface is counted so long as it is visible from that
axis.
Note that this calculates the 2D projected area, not the actual surface
area. If you want the actual area, use :func:`get_side_area`.
:param geometry: Geometry output calculated by IfcOpenShell
:param axis: Either X, Y, or Z. Defaults to Z.
:param direction: An XYZ iterable (e.g. (0., 0., 1.)). If a direction
vector is specified, this overrides the axis argument.
:return: The surface area.
"""
if direction is None:
direction = {"X": (1.0, 0.0, 0.0), "Y": (0.0, 1.0, 0.0), "Z": (0.0, 0.0, 1.0)}[axis]
vertices = get_vertices(geometry)
faces = get_faces(geometry)
# Calculate the triangle normal vectors
v1 = vertices[faces[:, 1]] - vertices[faces[:, 0]]
v2 = vertices[faces[:, 2]] - vertices[faces[:, 0]]
triangle_normals = np.cross(v1, v2)
# Normalize the normal vectors
triangle_normals = triangle_normals / np.linalg.norm(triangle_normals, axis=1)[:, np.newaxis]
direction = np.array(direction) / np.linalg.norm(direction)
# Find the faces with a normal vector pointing in the desired direction using dot product
# normal_tol < 0 is pointing away, = 0 is perpendicular, and > 0 is pointing towards.
normal_tol = 0.01 # Close to perpendicular, but with a fuzz for numerical tolerance
dot_products = np.dot(triangle_normals, direction)
filtered_face_indices = np.where(dot_products > normal_tol)[0]
filtered_faces = faces[filtered_face_indices]
# Flatten vertices along the direction
vertices = vertices.copy() # Buffers are read-only.
for idx in range(len(vertices)):
vertices[idx] = vertices[idx] - np.dot(vertices[idx], direction) * direction
# Now flatten 3D vertices into 2D polygons which can be unioned to find a footprint.
# Create an orthonormal basis using the direction
d = np.array(direction) / np.linalg.norm(direction)
# Find a vector not parallel to d
a = np.array(d)
if not np.isclose(a[2], 1.0, atol=0.01): # If d is not along the Z-axis
a[2] += 0.01 # Small perturbation to make it not parallel
else:
a = np.array([1, 0, 0])
# First basis vector
b = np.cross(d, a)
b /= np.linalg.norm(b)
# Second basis vector
c = np.cross(d, b)
# Project the flattened vertices onto the basis to get 2D coordinates
vertices_2d = np.array([[np.dot(v, b), np.dot(v, c)] for v in vertices])
polygons = [shapely.Polygon(vertices_2d[face]) for face in filtered_faces]
unioned_polygon = shapely.ops.unary_union(polygons)
return unioned_polygon.area
def get_outer_surface_area(geometry: W.Triangulation) -> float:
"""Calculates the outer surface area (i.e. all sides except for top and bottom)
This is typically useful for calculating painted areas of beams which
exclude the end faces (at the minimum and maximum local Z).
:param geometry: Geometry output calculated by IfcOpenShell
:return: The surface area.
"""
vertices = get_vertices(geometry)
faces = get_faces(geometry)
# Calculate the triangle normal vectors
v1 = vertices[faces[:, 1]] - vertices[faces[:, 0]]
v2 = vertices[faces[:, 2]] - vertices[faces[:, 0]]
triangle_normals = np.cross(v1, v2)
# Normalize the normal vectors
triangle_normals = triangle_normals / np.linalg.norm(triangle_normals, axis=1)[:, np.newaxis]
# Find the faces with a normal vector that isn't +Z or -Z
filtered_face_indices = np.where(abs(triangle_normals[:, 2]) < tol)[0]
filtered_faces = faces[filtered_face_indices]
return get_area_vf(vertices, filtered_faces)
def get_footprint_perimeter(geometry: W.Triangulation) -> float:
"""Calculates the footprint perimeter of the geometry
All faces with a negative Z normal are considered and the distance of all
perimeter edges are totaled.
:param geometry: Geometry output calculated by IfcOpenShell
:return: The perimeter length
"""
vertices = get_vertices(geometry)
faces = get_faces(geometry)
# Calculate the triangle normal vectors
v1 = vertices[faces[:, 1]] - vertices[faces[:, 0]]
v2 = vertices[faces[:, 2]] - vertices[faces[:, 0]]
triangle_normals = np.cross(v1, v2)
# Normalize the normal vectors
triangle_normals = triangle_normals / np.linalg.norm(triangle_normals, axis=1)[:, np.newaxis]
# Find the faces with a normal vector pointing in the negative Z direction
negative_z_face_indices = np.where(triangle_normals[:, 2] < -tol)[0]
negative_z_faces = faces[negative_z_face_indices]
# Initialize the set of counted edges and the perimeter
all_edges = set()
shared_edges = set()
perimeter = 0
# Loop through each face
for face in negative_z_faces:
# Loop through each edge of the face
for i in range(3):
# Get the indices of the two vertices that define the edge
edge = (face[i], face[(i + 1) % 3])
# Keep track of shared edges. Perimeter edges are unshared.
if (edge[1], edge[0]) in all_edges or (edge[0], edge[1]) in all_edges:
shared_edges.add((edge[0], edge[1]))
shared_edges.add((edge[1], edge[0]))
else:
all_edges.add(edge)
return np.sum([np.linalg.norm(vertices[e[0]] - vertices[e[1]]) for e in (all_edges - shared_edges)]).item()
def get_profiles(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
"""Gets all 2D profiles used in the definition of a parametric shape
Profiles may be retrieved either from material profile sets or from swept
solid extrusions. This is useful for later doing 2D take-off from profiles.
:param element: The element occurrence
:return: A list of profiles
"""
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if material and material.is_a("IfcMaterialProfileSet"):
return [mp.Profile for mp in material.MaterialProfiles]
return [e.SweptArea for e in get_extrusions(element)]
def get_extrusions(element: ifcopenshell.entity_instance) -> Union[list[ifcopenshell.entity_instance], None]:
"""Gets all extruded area solids used to define an element's model body geometry
:param element: The element occurrence
:return: A list of extrusion representation items or `None` if element has no representation.
"""
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
representation = ifcopenshell.util.representation.resolve_representation(representation)
extrusions = []
for item in representation.Items:
while True:
if item.is_a("IfcExtrudedAreaSolid"):
extrusions.append(item)
break
elif item.is_a("IfcBooleanResult"):
item = item.FirstOperand
else:
break
return extrusions
def get_base_extrusions(element: ifcopenshell.entity_instance) -> Union[list[ifcopenshell.entity_instance], None]:
"""Gets all base extrusions used to define an element's model body geometry
A base extrusion is assumed to be an extrusion prior to all boolean
results.
:param element: The element occurrence
:return: A list of extrusion representation items or `None` if element has no representation.
"""
if not (rep := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")):
return
extrusions = []
for item in ifcopenshell.util.representation.resolve_representation(rep).Items:
while item.is_a("IfcBooleanResult"):
item = item.FirstOperand
if item.is_a("IfcExtrudedAreaSolid"):
extrusions.append(item)
return extrusions
def get_total_edge_length(geometry: W.Triangulation) -> float:
"""Calculates the total length of edges in a given geometry.
:param geometry: Geometry output calculated by IfcOpenShell
:return: The total length of all edges in the geometry.
"""
vertices = get_vertices(geometry)
vertices = vertices[get_edges(geometry)]
return np.linalg.norm(vertices[:, 1] - vertices[:, 0], axis=1).sum().item()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,155 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.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/>.
from typing import Literal, Optional, Union
import ifcopenshell.util.system
group_types: dict[str, tuple[str, ...]] = {
"IfcZone": ("IfcZone", "IfcSpace", "IfcSpatialZone"),
"IfcBuiltSystem": (
"IfcBuiltElement",
"IfcFurnishingElement",
"IfcElementAssembly",
"IfcTransportElement",
),
"IfcBuildingSystem": (
"IfcBuildingElement",
"IfcFurnishingElement",
"IfcElementAssembly",
"IfcTransportElement",
),
"IfcDistributionSystem": ("IfcDistributionElement",),
"IfcStructuralAnalysisModel": ("IfcStructuralMember", "IfcStructuralConnection"),
"IfcSystem": ("IfcProduct",),
"IfcGroup": ("IfcObjectDefinition",),
}
# Subclasses.
group_types["IfcDistributionCircuit"] = group_types["IfcDistributionSystem"]
# Replaced by IfcDistributionCircuit in IFC4, though it wasn't limited to IfcDistributionElements:
# "Usage of IfcElectricalCircuit is as for the supertype IfcSystem".
group_types["IfcElectricalCircuit"] = group_types["IfcSystem"]
FLOW_DIRECTION = Literal["SINK", "SOURCE", "SOURCEANDSINK", "NOTEDEFINED"]
def is_assignable(product: ifcopenshell.entity_instance, system: ifcopenshell.entity_instance) -> bool:
for assignable in group_types.get(system.is_a(), ()):
if product.is_a(assignable):
return True
return False
def get_system_elements(system: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
results = []
for rel in system.IsGroupedBy:
results.extend(rel.RelatedObjects)
return results
def get_element_systems(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
results = []
for rel in element.HasAssignments:
if not rel.is_a("IfcRelAssignsToGroup"):
continue
group = rel.RelatingGroup
if not group.is_a("IfcSystem") or group.is_a() in ("IfcStructuralAnalysisModel", "IfcZone"):
continue
results.append(group)
return results
def get_element_zones(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
results = []
for rel in element.HasAssignments:
if not rel.is_a("IfcRelAssignsToGroup"):
continue
group = rel.RelatingGroup
if not group.is_a("IfcZone"):
continue
results.append(group)
return results
def get_ports(
element: ifcopenshell.entity_instance, flow_direction: Optional[FLOW_DIRECTION] = None
) -> list[ifcopenshell.entity_instance]:
results = []
for rel in getattr(element, "IsNestedBy", []) or []:
for port in rel.RelatedObjects:
if not port.is_a("IfcDistributionPort"):
continue
if flow_direction and port.FlowDirection != flow_direction:
continue
results.append(port)
# IFC2X3 only, deprecated in IFC4
for rel in getattr(element, "HasPorts", []) or []:
port = rel.RelatingPort
if flow_direction and port.FlowDirection != flow_direction:
continue
results.append(port)
return results
def get_connected_port(port: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
for rel in port.ConnectedTo:
return rel.RelatedPort
for rel in port.ConnectedFrom:
return rel.RelatingPort
def get_port_element(port: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
if hasattr(port, "Nests"):
for rel in port.Nests:
return rel.RelatingObject
# IFC2X3 only, deprecated in IFC4
elif hasattr(port, "ContainedIn"):
for rel in port.ContainedIn:
return rel.RelatedElement
def get_connected_to(
element: ifcopenshell.entity_instance, flow_direction: Optional[FLOW_DIRECTION] = None
) -> list[ifcopenshell.entity_instance]:
results = []
for port in ifcopenshell.util.system.get_ports(element, flow_direction=flow_direction):
for rel in port.ConnectedTo:
for other_port in [rel.RelatedPort, rel.RelatingPort]:
if other_port == port:
continue
other_element = get_port_element(other_port)
if other_element:
results.append(other_element)
return results
def get_connected_from(
element: ifcopenshell.entity_instance, flow_direction: Optional[FLOW_DIRECTION] = None
) -> list[ifcopenshell.entity_instance]:
results = []
for port in ifcopenshell.util.system.get_ports(element, flow_direction=flow_direction):
for rel in port.ConnectedFrom:
for other_port in [rel.RelatedPort, rel.RelatingPort]:
if other_port == port:
continue
other_element = get_port_element(other_port)
if other_element:
results.append(other_element)
return results
@@ -0,0 +1,81 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021, 2023 Dion Moult <dion@thinkmoult.com>, @Andrej730
#
# 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/>.
import json
import os
import ifcopenshell.util.schema
cwd = os.path.dirname(os.path.realpath(__file__))
entity_to_type_map: dict[ifcopenshell.util.schema.IFC_SCHEMA, dict[str, list[str]]] = {}
type_to_entity_map: dict[ifcopenshell.util.schema.IFC_SCHEMA, dict[str, list[str]]] = {}
mapped_schemas = {
"IFC2X3": "entity_to_type_map_2x3.json",
"IFC4": "entity_to_type_map_4.json",
"IFC4X3": "entity_to_type_map_4x3.json",
}
for schema in mapped_schemas:
# load entity maps from json
schema_path = os.path.join(cwd, mapped_schemas[schema])
with open(schema_path) as f:
entity_to_type_map[schema] = json.load(f)
# create type_to_entity map
type_to_entity_map[schema] = {}
for element, element_types in entity_to_type_map[schema].items():
for element_type in element_types:
type_to_entity_map[schema].setdefault(element_type, []).append(element)
if schema == "IFC2X3":
# Prioritize IfcBuildingElementProxyType if it's available as it seems to be the most generic type.
# Otherwise classes that don't have a special type in IFC2X3 (e.g. IfcBuildingElementPart, IfcRoof)
# have IfcBeamType as their first matching type, which can be confusing.
for occurrence_type, element_types in entity_to_type_map[schema].items():
if "IfcBuildingElementProxyType" in element_types:
element_types.sort(key=lambda x: x == "IfcBuildingElementProxyType", reverse=True)
# There is no official mapping for IFC2X3 but this method gets us something that looks correct
#
# NOTE: currently `type_to_entity_map` in IFC2X3 doesn't completely match `entity_to_type_map`,
# e.g. `get_applicabl_types(IfcRoof)` returns `[IfcBuildingElementProxyType, IfcBeamType, ...]`
# but `get_applicable_entities(IfcBuildingElementProxyType)` returns `[IfcBuildingElementProxy`].
for element_type, elements in type_to_entity_map[schema].items():
# need to take both Type (4 symbols) and Style (5 symbols) into account
guessed_element = element_type[:-5] if element_type.endswith("Style") else element_type[:-4]
if guessed_element in elements:
type_to_entity_map[schema][element_type] = [e for e in elements if guessed_element in e]
def get_applicable_types(ifc_class: str, schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4") -> list[str]:
"""Get applicable types IFC classes for the occurrence IFC class.
E.g. "IfcWindow" -> ["IfcWindowType"].
"""
schema = ifcopenshell.util.schema.get_fallback_schema(schema.upper())
return entity_to_type_map[schema].get(ifc_class, [])
def get_applicable_entities(ifc_type_class: str, schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4") -> list[str]:
"""Get applicable occurrence IFC classes for the type IFC class.
E.g. "IfcWindowType" -> ["IfcWindow"].
"""
schema = ifcopenshell.util.schema.get_fallback_schema(schema.upper())
return type_to_entity_map[schema].get(ifc_type_class, [])
@@ -0,0 +1,935 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.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/>.
from collections.abc import Generator
from fractions import Fraction
from math import pi
from typing import Literal, Optional, Union
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
prefixes = {
"EXA": 1e18,
"PETA": 1e15,
"TERA": 1e12,
"GIGA": 1e9,
"MEGA": 1e6,
"KILO": 1e3,
"HECTO": 1e2,
"DECA": 1e1,
"DECI": 1e-1,
"CENTI": 1e-2,
"MILLI": 1e-3,
"MICRO": 1e-6,
"NANO": 1e-9,
"PICO": 1e-12,
"FEMTO": 1e-15,
"ATTO": 1e-18,
}
unit_names = [
"AMPERE",
"BECQUEREL",
"CANDELA",
"COULOMB",
"CUBIC_METRE",
"DEGREE_CELSIUS",
"FARAD",
"GRAM",
"GRAY",
"HENRY",
"HERTZ",
"JOULE",
"KELVIN",
"LUMEN",
"LUX",
"MOLE",
"NEWTON",
"OHM",
"PASCAL",
"RADIAN",
"SECOND",
"SIEMENS",
"SIEVERT",
"SQUARE_METRE",
"METRE",
"STERADIAN",
"TESLA",
"VOLT",
"WATT",
"WEBER",
]
si_dimensions = {
"METRE": (1, 0, 0, 0, 0, 0, 0),
"SQUARE_METRE": (2, 0, 0, 0, 0, 0, 0),
"CUBIC_METRE": (3, 0, 0, 0, 0, 0, 0),
"GRAM": (0, 1, 0, 0, 0, 0, 0),
"SECOND": (0, 0, 1, 0, 0, 0, 0),
"AMPERE": (0, 0, 0, 1, 0, 0, 0),
"KELVIN": (0, 0, 0, 0, 1, 0, 0),
"MOLE": (0, 0, 0, 0, 0, 1, 0),
"CANDELA": (0, 0, 0, 0, 0, 0, 1),
"RADIAN": (0, 0, 0, 0, 0, 0, 0),
"STERADIAN": (0, 0, 0, 0, 0, 0, 0),
"HERTZ": (0, 0, -1, 0, 0, 0, 0),
"NEWTON": (1, 1, -2, 0, 0, 0, 0),
"PASCAL": (-1, 1, -2, 0, 0, 0, 0),
"JOULE": (2, 1, -2, 0, 0, 0, 0),
"WATT": (2, 1, -3, 0, 0, 0, 0),
"COULOMB": (0, 0, 1, 1, 0, 0, 0),
"VOLT": (2, 1, -3, -1, 0, 0, 0),
"FARAD": (-2, -1, 4, 2, 0, 0, 0),
"OHM": (2, 1, -3, -2, 0, 0, 0),
"SIEMENS": (-2, -1, 3, 2, 0, 0, 0),
"WEBER": (2, 1, -2, -1, 0, 0, 0),
"TESLA": (0, 1, -2, -1, 0, 0, 0),
"HENRY": (2, 1, -2, -2, 0, 0, 0),
"DEGREE_CELSIUS": (0, 0, 0, 0, 1, 0, 0),
"LUMEN": (0, 0, 0, 0, 0, 0, 1),
"LUX": (-2, 0, 0, 0, 0, 0, 1),
"BECQUEREL": (0, 0, -1, 0, 0, 0, 0),
"GRAY": (2, 0, -2, 0, 0, 0, 0),
"SIEVERT": (2, 0, -2, 0, 0, 0, 0),
"OTHERWISE": (0, 0, 0, 0, 0, 0, 0),
}
# See https://github.com/buildingSMART/IFC4.3.x-development/issues/72
si_type_names = {
"ABSORBEDDOSEUNIT": "GRAY",
"AMOUNTOFSUBSTANCEUNIT": "MOLE",
"AREAUNIT": "SQUARE_METRE",
"DOSEEQUIVALENTUNIT": "SIEVERT",
"ELECTRICCAPACITANCEUNIT": "FARAD",
"ELECTRICCHARGEUNIT": "COULOMB",
"ELECTRICCONDUCTANCEUNIT": "SIEMENS",
"ELECTRICCURRENTUNIT": "AMPERE",
"ELECTRICRESISTANCEUNIT": "OHM",
"ELECTRICVOLTAGEUNIT": "VOLT",
"ENERGYUNIT": "JOULE",
"FORCEUNIT": "NEWTON",
"FREQUENCYUNIT": "HERTZ",
"ILLUMINANCEUNIT": "LUX",
"INDUCTANCEUNIT": "HENRY",
"LENGTHUNIT": "METRE",
"LUMINOUSFLUXUNIT": "LUMEN",
"LUMINOUSINTENSITYUNIT": "CANDELA",
"MAGNETICFLUXDENSITYUNIT": "TESLA",
"MAGNETICFLUXUNIT": "WEBER",
"MASSUNIT": "GRAM",
"PLANEANGLEUNIT": "RADIAN",
"POWERUNIT": "WATT",
"PRESSUREUNIT": "PASCAL",
"RADIOACTIVITYUNIT": "BECQUEREL",
"SOLIDANGLEUNIT": "STERADIAN",
"THERMODYNAMICTEMPERATUREUNIT": "KELVIN", # Or, DEGREE_CELSIUS, but this is a quirk of IFC
"TIMEUNIT": "SECOND",
"VOLUMEUNIT": "CUBIC_METRE",
"USERDEFINED": "METRE",
}
# See IfcDimensionalExponents:
# (Length, Mass, Time, ElectricCurrent, ThermodynamicTemperature, AmountOfSubstance, LuminousIntensity)
named_dimensions = {
"ABSORBEDDOSEUNIT": (2, 0, -2, 0, 0, 0, 0),
"AMOUNTOFSUBSTANCEUNIT": (0, 0, 0, 0, 0, 1, 0),
"AREAUNIT": (2, 0, 0, 0, 0, 0, 0),
"DOSEEQUIVALENTUNIT": (2, 0, -2, 0, 0, 0, 0),
"ELECTRICCAPACITANCEUNIT": (-2, -1, 4, 2, 0, 0, 0),
"ELECTRICCHARGEUNIT": (0, 0, 1, 1, 0, 0, 0),
"ELECTRICCONDUCTANCEUNIT": (-2, -1, 3, 2, 0, 0, 0),
"ELECTRICCURRENTUNIT": (0, 0, 0, 1, 0, 0, 0),
"ELECTRICRESISTANCEUNIT": (2, 1, -3, -2, 0, 0, 0),
"ELECTRICVOLTAGEUNIT": (2, 1, -3, -1, 0, 0, 0),
"ENERGYUNIT": (2, 1, -2, 0, 0, 0, 0),
"FORCEUNIT": (1, 1, -2, 0, 0, 0, 0),
"FREQUENCYUNIT": (0, 0, -1, 0, 0, 0, 0),
"ILLUMINANCEUNIT": (-2, 0, 0, 0, 0, 1, 1),
"INDUCTANCEUNIT": (2, 1, -2, -2, 0, 0, 0),
"LENGTHUNIT": (1, 0, 0, 0, 0, 0, 0),
"LUMINOUSFLUXUNIT": (0, 0, 0, 0, 0, 1, 1),
"LUMINOUSINTENSITYUNIT": (0, 0, 0, 0, 0, 0, 1),
"MAGNETICFLUXDENSITYUNIT": (0, 1, -2, -1, 0, 0, 0),
"MAGNETICFLUXUNIT": (2, 1, -2, -1, 0, 0, 0),
"MASSUNIT": (0, 1, 0, 0, 0, 0, 0),
"PLANEANGLEUNIT": (0, 0, 0, 0, 0, 0, 0),
"POWERUNIT": (2, 1, -3, 0, 0, 0, 0),
"PRESSUREUNIT": (-1, 1, -2, 0, 0, 0, 0),
"RADIOACTIVITYUNIT": (0, 0, -1, 0, 0, 0, 0),
"SOLIDANGLEUNIT": (0, 0, 0, 0, 0, 0, 0),
"THERMODYNAMICTEMPERATUREUNIT": (0, 0, 0, 0, 1, 0, 0),
"TIMEUNIT": (0, 0, 1, 0, 0, 0, 0),
"VOLUMEUNIT": (3, 0, 0, 0, 0, 0, 0),
"USERDEFINED": (0, 0, 0, 0, 0, 0, 0),
}
si_conversions = {
"thou": 0.0000254,
"inch": 0.0254,
"foot": 0.3048,
"yard": 0.914,
"mile": 1609,
"square thou": 6.4516e-10,
"square inch": 0.0006452,
"square foot": 0.09290304,
"square yard": 0.83612736,
"acre": 4046.86,
"square mile": 2588881,
"cubic thou": 1.6387064e-14,
"cubic inch": 0.00001639,
"cubic foot": 0.02831684671168849,
"cubic yard": 0.7636,
"cubic mile": 4165509529,
"litre": 0.001,
"fluid ounce UK": 0.0000284130625,
"fluid ounce US": 0.00002957353,
"pint UK": 0.000568,
"pint US": 0.000473,
"gallon UK": 0.004546,
"gallon US": 0.003785,
"degree": pi / 180,
"ounce": 0.02835,
"pound": 0.454,
"ton UK": 1016.0469088,
"ton US": 907.18474,
"tonne": 1000.0,
"lbf": 4.4482216153,
"kip": 4448.2216153,
"psi": 6894.7572932,
"ksi": 6894757.2932,
"minute": 60,
"hour": 3600,
"day": 86400,
"btu": 1055.056,
"fahrenheit": 1.8,
}
si_offsets = {
"fahrenheit": -459.67,
}
imperial_types = {
"thou": "LENGTHUNIT",
"inch": "LENGTHUNIT",
"foot": "LENGTHUNIT",
"yard": "LENGTHUNIT",
"mile": "LENGTHUNIT",
"square thou": "AREAUNIT",
"square inch": "AREAUNIT",
"square foot": "AREAUNIT",
"square yard": "AREAUNIT",
"acre": "AREAUNIT",
"square mile": "AREAUNIT",
"cubic thou": "VOLUMEUNIT",
"cubic inch": "VOLUMEUNIT",
"cubic foot": "VOLUMEUNIT",
"cubic yard": "VOLUMEUNIT",
"cubic mile": "VOLUMEUNIT",
"litre": "VOLUMEUNIT",
"fluid ounce UK": "VOLUMEUNIT",
"fluid ounce US": "VOLUMEUNIT",
"pint UK": "VOLUMEUNIT",
"pint US": "VOLUMEUNIT",
"gallon UK": "VOLUMEUNIT",
"gallon US": "VOLUMEUNIT",
"degree": "PLANEANGLEUNIT",
"ounce": "MASSUNIT",
"pound": "MASSUNIT",
"ton UK": "MASSUNIT",
"ton US": "MASSUNIT",
"tonne": "MASSUNIT",
"lbf": "FORCEUNIT",
"kip": "FORCEUNIT",
"psi": "PRESSUREUNIT",
"ksi": "PRESSUREUNIT",
"minute": "TIMEUNIT",
"hour": "TIMEUNIT",
"day": "TIMEUNIT",
"btu": "ENERGYUNIT",
"fahrenheit": "THERMODYNAMICTEMPERATUREUNIT",
}
prefix_symbols = {
"EXA": "E",
"PETA": "P",
"TERA": "T",
"GIGA": "G",
"MEGA": "M",
"KILO": "k",
"HECTO": "h",
"DECA": "da",
"DECI": "d",
"CENTI": "c",
"MILLI": "m",
"MICRO": "μ",
"NANO": "n",
"PICO": "p",
"FEMTO": "f",
"ATTO": "a",
}
unit_symbols = {
# si units
"CUBIC_METRE": "m3",
"GRAM": "g",
"SECOND": "s",
"SQUARE_METRE": "m2",
"METRE": "m",
"NEWTON": "N",
"PASCAL": "Pa",
# conversion based units
"pound-force": "lbf",
"pound-force per square inch": "psi",
"thou": "th",
"inch": "in",
"foot": "ft",
"yard": "yd",
"mile": "mi",
"square thou": "th2",
"square inch": "in2",
"square foot": "ft2",
"square yard": "yd2",
"acre": "ac",
"square mile": "mi2",
"cubic thou": "th3",
"cubic inch": "in3",
"cubic foot": "ft3",
"cubic yard": "yd3",
"cubic mile": "mi3",
"litre": "L",
"fluid ounce UK": "fl oz",
"fluid ounce US": "fl oz",
"pint UK": "pt",
"pint US": "pt",
"gallon UK": "gal",
"gallon US": "gal",
"degree": "°",
"ounce": "oz",
"pound": "lb",
"ton UK": "ton",
"ton US": "ton",
"tonne": "t",
"lbf": "lbf",
"kip": "kip",
"psi": "psi",
"ksi": "ksi",
"minute": "min",
"hour": "hr",
"day": "day",
"btu": "btu",
"fahrenheit": "°F",
}
QUANTITY_CLASS = Literal[
"IfcQuantityCount",
"IfcQuantityNumber",
"IfcQuantityLength",
"IfcQuantityArea",
"IfcQuantityVolume",
"IfcQuantityWeight",
"IfcQuantityTime",
"IfcQuantityCount",
]
MEASURE_CLASS = Literal[
"IfcNumericMeasure",
"IfcLengthMeasure",
"IfcAreaMeasure",
"IfcVolumeMeasure",
"IfcMassMeasure",
]
def get_prefix(text):
if text:
for prefix in prefixes.keys():
if prefix in text.upper():
return prefix
def get_prefix_multiplier(text):
if not text:
return 1
prefix = get_prefix(text)
if prefix:
return prefixes[prefix]
return 1
def get_unit_name(text: str) -> Union[str, None]:
"""Get unit name from str, if unit is in SI."""
text = text.upper().replace("METER", "METRE")
for name in unit_names:
if name.replace("_", " ") in text:
return name
def get_unit_name_universal(text: str) -> Union[str, None]:
"""Get unit name from str, supports both SI and imperial system.
Can be used to provide units for `convert()`"""
text = text.upper().replace("METER", "METRE")
for name in unit_names:
if name.replace("_", " ") in text:
return name
for name in imperial_types:
if name.upper() in text:
return name
def get_full_unit_name(unit: ifcopenshell.entity_instance) -> str:
prefix = getattr(unit, "Prefix", None) or ""
return prefix + unit.Name.upper()
def get_si_dimensions(name):
return si_dimensions.get(name, si_dimensions["OTHERWISE"])
def get_named_dimensions(name):
return named_dimensions.get(name, (0, 0, 0, 0, 0, 0, 0))
def get_unit_assignment(ifc_file: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
return ifc_file.by_type("IfcProject")[0].UnitsInContext
def cache_units(ifc_file: ifcopenshell.file) -> None:
"""Cache the default units for performance
Repetitively fetching project units (such as for determining the unit of a
property) can be costly. This enables a cache to make it faster. If the
project units change, you can update the cache by rerunning this function.
:param ifc_file: The IFC file.
"""
ifc_file.units = {}
if assignment := get_unit_assignment(ifc_file):
ifc_file.units = {u.UnitType: u for u in assignment.Units if getattr(u, "UnitType", None)}
def clear_unit_cache(ifc_file: ifcopenshell.file) -> None:
"""Clears the unit cache of the project
:param ifc_file: The IFC file.
"""
ifc_file.units = {}
def get_project_unit(
ifc_file: ifcopenshell.file, unit_type: str, use_cache: bool = False
) -> Union[ifcopenshell.entity_instance, None]:
"""Get the default project unit of a particular unit type
:param ifc_file: The IFC file.
:param unit_type: The type of unit, taken from the list of IFC unit types,
such as "LENGTHUNIT".
:return: The IFC unit entity, or nothing if there is no default project
unit defined.
"""
if use_cache and not ifc_file.units:
cache_units(ifc_file)
if units := ifc_file.units:
return units.get(unit_type, None)
if unit_assignment := get_unit_assignment(ifc_file):
for unit in unit_assignment.Units or []:
if getattr(unit, "UnitType", None) == unit_type:
return unit
def get_property_unit(
prop: ifcopenshell.entity_instance, ifc_file: Union[ifcopenshell.file, None], use_cache: bool = False
) -> Union[ifcopenshell.entity_instance, None]:
"""Gets the unit definition of a property or quantity
Properties and quantities in psets and qtos can be associated with a unit.
This unit may be defined at the property itself explicitly, or if not
specified, fallback to the project default.
:param prop: The IfcProperty instance. You can fetch this via the instance
ID if doing :func:`ifcopenshell.util.element.get_psets` with
``verbose=True``.
:param ifc_file: The IFC file being used. This is necessary to check
default project units.
:return: The IFC unit entity, or nothing if there is no default project
unit defined.
"""
if unit := getattr(prop, "Unit", None):
return unit
value = None
measure_class = None
if prop.is_a("IfcPhysicalSimpleQuantity"):
entity = prop.wrapped_data.declaration().as_entity()
measure_class = entity.attribute_by_index(3).type_of_attribute().declared_type().name()
elif prop.is_a("IfcPropertySingleValue"):
measure_class = prop.NominalValue.is_a()
elif prop.is_a("IfcPropertyEnumeratedValue"):
if prop.EnumerationReference:
if unit := prop.EnumerationReference.Unit:
return unit
if value := next(iter(prop.EnumerationReference.EnumerationValues or ()), None):
measure_class = value.is_a()
if value := next(iter(prop.EnumerationValues or ()), None):
measure_class = value.is_a()
elif prop.is_a("IfcPropertyListValue"):
if value := next(iter(prop.ListValues or ()), None):
measure_class = value.is_a()
elif prop.is_a("IfcPropertyBoundedValue"):
if value := (prop.UpperBoundValue or prop.LowerBoundValue or prop.SetPointValue):
measure_class = value.is_a()
if measure_class and (unit_type := get_measure_unit_type(measure_class)):
if not ifc_file:
ifc_file = prop.file
return get_project_unit(ifc_file, unit_type, use_cache=use_cache)
def get_property_table_unit(
prop: ifcopenshell.entity_instance, ifc_file: Union[ifcopenshell.file, None], use_cache: bool = False
) -> dict[str, Union[ifcopenshell.entity_instance, None]]:
"""
Gets the unit definition of a property table
Properties and quantities in psets and qtos can be associated with a unit.
This unit may be defined at the property itself explicitly, or if not
specified, fallback to the project default.
:param prop: The property instance. You can fetch this via the instance ID
if doing :func:`ifcopenshell.util.element.get_psets` with
``verbose=True``.
:param ifc_file: The IFC file being used. This is necessary to check
default project units.
:return: A dictionary containing IFC unit entity by keyword.
If a unit-entity is missing,
the value associated to the key is `null`.
"""
if not ifc_file:
ifc_file = prop.file
defining_unit = None
if unit := prop.DefiningUnit:
defining_unit = unit
elif value := next(iter(prop.DefiningValues or ()), None):
if unit_type := get_measure_unit_type(value.is_a()):
defining_unit = get_project_unit(ifc_file, unit_type, use_cache=use_cache)
defined_unit = None
if unit := prop.DefinedUnit:
defined_unit = unit
elif value := next(iter(prop.DefinedValues or ()), None):
if unit_type := get_measure_unit_type(value.is_a()):
defined_unit = get_project_unit(ifc_file, unit_type, use_cache=use_cache)
return {
"DefiningUnit": defining_unit,
"DefinedUnit": defined_unit,
}
def get_unit_measure_class(unit_type: str) -> MEASURE_CLASS:
"""Get the IFC measure class for a unit type.
IFC has specific classes used to measure different units. An example of an
IFC measure class is ``IfcLengthMeasure``. An example of the correlating
unit type (i.e. the IfcUnitEnum) is ``LENGTHUNIT``.
The inverse function of this is :func:`get_measure_unit_type`
:param unit_type: A string chosen from IfcUnitEnum, such as LENGTHUNIT
"""
if unit_type == "USERDEFINED":
# See https://github.com/buildingSMART/IFC4.3.x-development/issues/71
return "IfcNumericMeasure"
return "Ifc" + unit_type[0:-4].lower().capitalize() + "Measure"
def get_measure_unit_type(measure_class: MEASURE_CLASS) -> str:
"""Get the unit type of an IFC measure class
IFC has different unit types which can be associated with units (e.g. SI
units, imperial units, derived units, etc). An example of a unit type (i.e.
an IfcUnitEnum) is ``LENGTHUNIT``. An example of the correlating measure
class used to store length data is ``IfcLengthMeasure``.
The inverse fucntion of this is :func:`get_unit_measure_class`
:param measure_class: The measure class, such as ``IfcLengthMeasure``. If
you have an ``IfcPropertySingleValue``, you can get this using
``prop.NominalValue.is_a()``.
:return: The unit type, as an uppercase value of IfcUnitEnum.
"""
if measure_class == "IfcNumericMeasure":
# See https://github.com/buildingSMART/IFC4.3.x-development/issues/71
return "USERDEFINED"
for text in ("Ifc", "Measure", "Non", "Positive", "Negative"):
measure_class = measure_class.replace(text, "")
return measure_class.upper() + "UNIT"
def get_symbol_measure_class(symbol: Optional[str] = None) -> MEASURE_CLASS:
# Dumb, but everybody gets it, unlike regex golf
if not symbol:
return "IfcNumericMeasure"
symbol = symbol.lower()
if symbol in ["km", "m", "cm", "mm", "ly", "lf", "lin", "yd", "ft", "in"]:
return "IfcLengthMeasure"
elif symbol in ["km2", "m2", "cm2", "mm2", "sqy", "sqft", "sqin"]:
return "IfcAreaMeasure"
elif symbol in ["km3", "m3", "cm3", "mm3", "cy", "cft", "cin"]:
return "IfcVolumeMeasure"
elif symbol in ["kg", "g", "mt", "kt", "t"]:
return "IfcMassMeasure"
elif symbol in ["day", "d", "hour", "hr", "h", "minute", "min", "m", "second", "sec", "s"]:
return "IfcTimeMeasure"
return "IfcNumericMeasure"
def get_symbol_quantity_class(symbol: Optional[str] = None) -> QUANTITY_CLASS:
# Dumb, but everybody gets it, unlike regex golf
if not symbol:
return "IfcQuantityCount"
symbol = symbol.lower()
if symbol in ["km", "m", "cm", "mm", "ly", "lf", "lin", "yd", "ft", "in"]:
return "IfcQuantityLength"
elif symbol in ["km2", "m2", "cm2", "mm2", "sqy", "sqft", "sqin"]:
return "IfcQuantityArea"
elif symbol in ["km3", "m3", "cm3", "mm3", "cy", "cft", "cin"]:
return "IfcQuantityVolume"
elif symbol in ["kg", "g", "mt", "kt", "t"]:
return "IfcQuantityWeight"
elif symbol in ["day", "d", "hour", "hr", "h", "minute", "min", "m", "second", "sec", "s"]:
return "IfcQuantityTime"
return "IfcQuantityCount"
def get_unit_symbol(unit: ifcopenshell.entity_instance) -> str:
symbol: str = ""
if unit.is_a("IfcSIUnit"):
symbol += prefix_symbols.get(unit.Prefix, "")
symbol += unit_symbols.get(unit.Name.replace("METER", "METRE"), "?")
if unit.is_a("IfcContextDependentUnit") and unit.UnitType == "USERDEFINED":
symbol = unit.Name
return symbol
def convert_unit(value: float, from_unit: ifcopenshell.entity_instance, to_unit: ifcopenshell.entity_instance) -> float:
"""Convert from one unit to another unit
:param value: The numeric value you want to convert
:param from_unit: The IfcNamedUnit to confirm from.
:param to_unit: The IfcNamedUnit to confirm from.
:return: The converted value.
"""
return convert(
value, getattr(from_unit, "Prefix", None), from_unit.Name, getattr(to_unit, "Prefix", None), to_unit.Name
)
def convert(value: float, from_prefix: Optional[str], from_unit: str, to_prefix: Optional[str], to_unit: str) -> float:
"""Converts between length, area, and volume units
In this case, you manually specify the names and (optionally) prefixes to
convert to and from. In case you want to automatically convert to units
already available as IFC entities, consider using convert_unit() instead.
:param value: The numeric value you want to convert
:param from_prefix: A prefix from IfcSIPrefix. Can be None
:param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
:param to_prefix: A prefix from IfcSIPrefix. Can be None
:param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
:return: The converted value.
"""
if from_unit.lower() in si_conversions:
value *= si_conversions[from_unit.lower()]
elif from_prefix:
value *= get_prefix_multiplier(from_prefix)
if "SQUARE" in from_unit:
value *= get_prefix_multiplier(from_prefix)
elif "CUBIC" in from_unit:
value *= get_prefix_multiplier(from_prefix)
value *= get_prefix_multiplier(from_prefix)
if to_unit.lower() in si_conversions:
return value * (1 / si_conversions[to_unit.lower()])
elif to_prefix:
value *= 1 / get_prefix_multiplier(to_prefix)
if "SQUARE" in from_unit:
value *= 1 / get_prefix_multiplier(to_prefix)
elif "CUBIC" in from_unit:
value *= 1 / get_prefix_multiplier(to_prefix)
value *= 1 / get_prefix_multiplier(to_prefix)
return value
def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUNIT") -> float:
"""Returns a unit scale factor to convert to and from IFC project units and SI units.
Example:
.. code:: python
ifc_project_length * unit_scale = si_meters
si_meters / unit_scale = ifc_project_length
:param ifc_file: The IFC file.
:param unit_type: The type of SI unit, defaults to "LENGTHUNIT"
:returns: The scale factor
"""
if (
type(ifc_file) is ifcopenshell.file
and unit_type
not in ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema_identifier)
.declaration_by_name("IfcUnitEnum")
.enumeration_items()
):
raise ValueError(f"Unit type {unit_type!r} does not name a valid type")
# Currently we assume that all ifc projects must have IfcProject.
if not (projects := ifc_file.by_type("IfcProject")) or not (units := projects[0].UnitsInContext):
return 1
unit_scale = 1
unit: ifcopenshell.entity_instance
for unit in units.Units:
if getattr(unit, "UnitType", ...) != unit_type:
continue
while unit.is_a("IfcConversionBasedUnit"):
conversion_factor = unit.ConversionFactor
unit_scale *= conversion_factor.ValueComponent.wrappedValue
unit = conversion_factor.UnitComponent
if unit.is_a("IfcSIUnit"):
unit_scale *= get_prefix_multiplier(unit.Prefix)
return unit_scale
def format_length(
value: float,
precision: float,
decimal_places: int = 2,
suppress_zero_inches=True,
unit_system: Literal["metric", "imperial"] = "imperial",
input_unit: Literal["foot", "inch"] = "foot",
output_unit: Literal["foot", "inch"] = "foot",
) -> str:
"""Formats a length for readability and imperial formatting
:param value: The value in meters if metric, or either decimal feet or
inches if imperial depending on input_unit.
:param precision: How precise the format should be. I.e. round to nearest.
For imperial, it is 1/Nth. E.g. 12 means to the nearest 1/12th of an
inch.
:param decimal_places: How many decimal places to display. Defaults to 2.
:param suppress_zero_inches: If imperial, whether or not to supress the
inches if the inches is zero.
:param unit_system: Choose whether your value is "metric" or "imperial"
:param input_unit: If imperial, specify whether your value is "foot" or
"inch".
:param output_unit: If imperial, specify whether your value is "foot" to
format as both feet and inches, or "inch" if only inches should be
shown.
:returns: The formatted string, such as 1' - 5 1/2".
"""
if unit_system == "imperial":
if input_unit == "foot":
feet = int(value)
inches = (value - feet) * 12
elif input_unit == "inch":
inches = value % 12
feet = int(round((value - inches) / 12))
# Round to the nearest 1/N
nearest = round(inches * precision)
# Create a fraction based on the rounded value and the precision
frac = Fraction(nearest, precision)
# If fraction is a whole number, format it accordingly
if frac.denominator == 1:
if suppress_zero_inches and frac.numerator == 0:
if output_unit == "foot":
return f"{feet}'"
return f'{feet * 12}"'
if output_unit == "foot":
return f"{feet}' - {frac.numerator}\""
return f'{(feet * 12) + frac.numerator}"'
if frac.numerator > frac.denominator:
remainder = frac.numerator % frac.denominator
whole = int((frac.numerator - remainder) / frac.denominator)
if output_unit == "foot":
return f"{feet}' - {whole} {remainder}/{frac.denominator}\""
return f'{(feet * 12) + whole} {remainder}/{frac.denominator}"'
# When we have a proper fraction (numerator < denominator), show "0 frac"
if output_unit == "foot":
return f"{feet}' - 0 {frac.numerator}/{frac.denominator}\""
return f'{feet * 12} {frac.numerator}/{frac.denominator}"'
elif unit_system == "metric":
rounded_val = round(value / precision) * precision
return f"{rounded_val:.{decimal_places}f}"
def is_attr_type(
content_type: ifcopenshell_wrapper.parameter_type,
ifc_unit_type_name: str,
include_select_types: bool = True,
) -> Union[ifcopenshell_wrapper.type_declaration, None]:
cur_decl = content_type
if hasattr(cur_decl, "name") and cur_decl.name() == ifc_unit_type_name:
return cur_decl
if include_select_types:
if hasattr(cur_decl, "select_list"):
for select_item in cur_decl.select_list():
if is_attr_type(select_item, ifc_unit_type_name):
return select_item
if hasattr(cur_decl, "declared_type"):
return is_attr_type(cur_decl.declared_type(), ifc_unit_type_name, include_select_types)
if isinstance(cur_decl, ifcopenshell_wrapper.aggregation_type):
# support aggregate of aggregates, as in IfcCartesianPointList3D.CoordList
def get_declared_type_from_aggregate(cur_decl):
cur_decl = cur_decl.type_of_element()
if not isinstance(cur_decl, ifcopenshell_wrapper.aggregation_type):
return cur_decl.declared_type()
return get_declared_type_from_aggregate(cur_decl)
cur_decl = get_declared_type_from_aggregate(cur_decl)
return is_attr_type(cur_decl, ifc_unit_type_name, include_select_types)
return None
FloatOrSequenceOfFloats = Union[float, tuple["FloatOrSequenceOfFloats", ...]]
def iter_element_and_attributes_per_type(ifc_file: ifcopenshell.file, attr_type_name: str) -> Generator[
tuple[
ifcopenshell.entity_instance,
ifcopenshell_wrapper.attribute,
Union[FloatOrSequenceOfFloats, ifcopenshell.entity_instance],
],
None,
None,
]:
schema = ifcopenshell_wrapper.schema_by_name(ifc_file.schema_identifier)
for element in ifc_file:
entity = schema.declaration_by_name(element.is_a()).as_entity()
assert entity
attrs = entity.all_attributes()
attrs_derived = entity.derived()
for attr, val, is_derived in zip(attrs, list(element), attrs_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
if isinstance(val, ifcopenshell.entity_instance) and not val.is_a(attr_type_name):
continue
elif isinstance(val, tuple):
if not val:
continue
val_ = val[0]
# If it's a tuple of entities, just yield the entities we need to edit.
if isinstance(val_, ifcopenshell.entity_instance):
for val_ in val:
if not val_.is_a(attr_type_name):
continue
yield element, attr, val_
continue
yield element, attr, val
def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "METER") -> ifcopenshell.file:
"""Converts all units in an IFC file to the specified target units. Returns a new file."""
import ifcopenshell.api.georeference
import ifcopenshell.api.unit
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
prefix = get_prefix(target_units)
si_unit = get_unit_name(target_units)
# Copy all elements from the original file to the patched file
file_patched = ifcopenshell.file.from_string(ifc_file.wrapped_data.to_string())
old_length = get_project_unit(file_patched, "LENGTHUNIT")
if si_unit:
new_length = ifcopenshell.api.unit.add_si_unit(file_patched, unit_type="LENGTHUNIT", prefix=prefix)
else:
target_units = target_units.lower()
if imperial_types.get(target_units) != "LENGTHUNIT":
raise Exception(
f'Couldn\'t identify target units "{target_units}". '
'The method supports singular unit names like "CENTIMETER", "METER", "FOOT", etc.'
)
new_length = ifcopenshell.api.unit.add_conversion_based_unit(file_patched, name=target_units)
# support tuple of tuples, as in IfcCartesianPointList3D.CoordList
def convert_value(value: FloatOrSequenceOfFloats) -> FloatOrSequenceOfFloats:
if not isinstance(value, tuple):
return convert_unit(value, old_length, new_length)
return tuple(convert_value(v) for v in value)
# Traverse all elements and their nested attributes in the file and convert them
for element, attr, val in iter_element_and_attributes_per_type(file_patched, "IfcLengthMeasure"):
# NOTE: There is no risk of editing same entities twice as they're all recreated
# after file is reloaded as `file_patched`.
if isinstance(val, ifcopenshell.entity_instance):
val.wrappedValue = convert_value(val.wrappedValue)
else:
new_value = convert_value(val)
setattr(element, attr.name(), new_value)
has_map_unit = False
if (
ifc_file.schema == "IFC2X3"
and (crs := ifcopenshell.util.element.get_pset(ifc_file.by_type("IfcProject")[0], name="ePSet_ProjectedCRS"))
and crs.get("MapUnit")
) or (ifc_file.schema != "IFC2X3" and (crs := ifc_file.by_type("IfcProjectedCRS")) and crs[0].MapUnit):
has_map_unit = True
if has_map_unit:
parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(ifc_file)
ifcopenshell.api.georeference.edit_georeferencing(
file_patched,
coordinate_operation={
"Eastings": parameters.e,
"Northings": parameters.n,
"OrthogonalHeight": parameters.h,
"Scale": parameters.scale / convert_value(1),
},
)
unit_assignment = get_unit_assignment(file_patched)
unit_assignment.Units = [new_length, *(u for u in unit_assignment.Units if u.UnitType != new_length.UnitType)]
if not file_patched.get_total_inverses(old_length):
ifcopenshell.util.element.remove_deep2(file_patched, old_length)
return file_patched