mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
validate.py: flag instantiated measures with no corresponding unit
An instantiated measure (e.g. IfcLengthMeasure occupying an IfcValue-typed attribute such as IfcPropertySingleValue.NominalValue) is only meaningful if there is a project unit, an explicit per-instance unit override, or an IfcMeasureWithUnit wrapper to interpret it against. validate() previously never checked this. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -61,6 +61,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union
|
||||
import ifcopenshell
|
||||
import ifcopenshell.express.rule_executor
|
||||
import ifcopenshell.ifcopenshell_wrapper
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ifcopenshell.simple_spf
|
||||
@@ -231,6 +232,88 @@ def get_select_members(schema: schema_definition, ty: select_type) -> set[str]:
|
||||
return v
|
||||
|
||||
|
||||
measure_unit_type_cache: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
def get_measure_unit_types(schema: schema_definition) -> dict[str, str]:
|
||||
"""Map instantiable measure type names to the unit type a project unit needs to be defined for.
|
||||
|
||||
Some measure types (e.g. ratios, counts, descriptive or monetary measures) have no
|
||||
corresponding entry in IfcUnitEnum / IfcDerivedUnitEnum, so they are excluded: their
|
||||
value is meaningful without any project unit being defined.
|
||||
|
||||
:return: A mapping of measure type name (e.g. "IfcLengthMeasure") to unit type
|
||||
(e.g. "LENGTHUNIT").
|
||||
"""
|
||||
cache_key = schema.name()
|
||||
if (from_cache := measure_unit_type_cache.get(cache_key)) is not None:
|
||||
return from_cache
|
||||
|
||||
valid_unit_types: set[str] = set()
|
||||
for enum_name in ("IfcUnitEnum", "IfcDerivedUnitEnum"):
|
||||
try:
|
||||
decl = schema.declaration_by_name(enum_name)
|
||||
except RuntimeError:
|
||||
continue
|
||||
valid_unit_types.update(decl.enumeration_items())
|
||||
valid_unit_types.discard("USERDEFINED")
|
||||
|
||||
mapping: dict[str, str] = {}
|
||||
for decl in schema.declarations():
|
||||
if isinstance(decl, type_declaration) and decl.name().endswith("Measure"):
|
||||
unit_type = ifcopenshell.util.unit.get_measure_unit_type(decl.name())
|
||||
if unit_type in valid_unit_types:
|
||||
mapping[decl.name()] = unit_type
|
||||
|
||||
measure_unit_type_cache[cache_key] = mapping
|
||||
return mapping
|
||||
|
||||
|
||||
def get_project_units_by_type(f: ifcopenshell.file) -> dict[str, ifcopenshell.entity_instance]:
|
||||
"""Get the project's units, keyed by their unit type (e.g. "LENGTHUNIT")."""
|
||||
projects = f.by_type("IfcProject")
|
||||
if not projects or not (assignment := projects[0].UnitsInContext):
|
||||
return {}
|
||||
return {unit_type: u for u in assignment.Units or [] if (unit_type := getattr(u, "UnitType", None))}
|
||||
|
||||
|
||||
# Attributes that, of a measure occurrence's containing instance, may carry an explicit unit
|
||||
# that supersedes the need for a project unit.
|
||||
measure_unit_override_attr = {
|
||||
("IfcPropertySingleValue", "NominalValue"): "Unit",
|
||||
("IfcPropertyListValue", "ListValues"): "Unit",
|
||||
("IfcPropertyBoundedValue", "UpperBoundValue"): "Unit",
|
||||
("IfcPropertyBoundedValue", "LowerBoundValue"): "Unit",
|
||||
("IfcPropertyBoundedValue", "SetPointValue"): "Unit",
|
||||
("IfcPropertyEnumeration", "EnumerationValues"): "Unit",
|
||||
("IfcPropertyTableValue", "DefiningValues"): "DefiningUnit",
|
||||
("IfcPropertyTableValue", "DefinedValues"): "DefinedUnit",
|
||||
("IfcAppliedValue", "AppliedValue"): "UnitBasis",
|
||||
}
|
||||
|
||||
|
||||
def has_explicit_measure_unit(inst: ifcopenshell.entity_instance, entity_name: str, attr_name: str) -> bool:
|
||||
"""Check whether an instantiated measure occurrence is exempt from needing a project unit."""
|
||||
if entity_name == "IfcMeasureWithUnit" and attr_name == "ValueComponent":
|
||||
# The sibling UnitComponent attribute is the unit for this exact value.
|
||||
return True
|
||||
if entity_name == "IfcPropertyEnumeratedValue" and attr_name == "EnumerationValues":
|
||||
ref = inst.EnumerationReference
|
||||
return bool(ref is not None and ref.Unit is not None)
|
||||
if override_attr := measure_unit_override_attr.get((entity_name, attr_name)):
|
||||
return getattr(inst, override_attr, None) is not None
|
||||
return False
|
||||
|
||||
|
||||
def iter_measure_instances(val: Any) -> Iterator[ifcopenshell.entity_instance]:
|
||||
"""Recursively yield the instantiated measure values (e.g. IfcLengthMeasure) within `val`."""
|
||||
if isinstance(val, tuple):
|
||||
for v in val:
|
||||
yield from iter_measure_instances(v)
|
||||
elif isinstance(val, ifcopenshell.entity_instance):
|
||||
yield val
|
||||
|
||||
|
||||
def assert_valid(
|
||||
attr_type: attribute_types,
|
||||
val: Any,
|
||||
@@ -471,6 +554,8 @@ def validate(f: Union[ifcopenshell.file, str], logger: Union[Logger, json_logger
|
||||
|
||||
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(f.schema_identifier)
|
||||
used_guids: dict[str, ifcopenshell.entity_instance] = dict()
|
||||
measure_unit_types = get_measure_unit_types(schema)
|
||||
project_units = get_project_units_by_type(f)
|
||||
|
||||
for inst in f:
|
||||
if isinstance(logger, json_logger):
|
||||
@@ -575,6 +660,27 @@ def validate(f: Union[ifcopenshell.file, str], logger: Union[Logger, json_logger
|
||||
annotate_inst_attr_pos(inst, i),
|
||||
e,
|
||||
)
|
||||
else:
|
||||
for measure_inst in iter_measure_instances(val):
|
||||
unit_type = measure_unit_types.get(measure_inst.is_a())
|
||||
if not unit_type or unit_type in project_units:
|
||||
continue
|
||||
if has_explicit_measure_unit(inst, entity.name(), attr.name()):
|
||||
continue
|
||||
if isinstance(logger, json_logger):
|
||||
logger.set_state("attribute", f"{entity.name()}.{attr.name()}")
|
||||
logger.error("Instantiated measure %s has no corresponding unit", measure_inst)
|
||||
else:
|
||||
logger.error(
|
||||
"For instance:\n %s\n %s\nWith attribute:\n %s\n"
|
||||
"Instantiated measure:\n %s\nHas no project unit (%s), is not "
|
||||
"wrapped in IfcMeasureWithUnit, and has no explicit property unit\n",
|
||||
inst,
|
||||
annotate_inst_attr_pos(inst, i),
|
||||
attr,
|
||||
measure_inst,
|
||||
unit_type,
|
||||
)
|
||||
|
||||
for attr in entity.all_inverse_attributes():
|
||||
try:
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
|
||||
FILE_NAME('test.ifc','2024-08-30T12:34:53+05:00',('author','author email'),('organization','organization email'),'IfcOpenShell','Bonsai','Nobody');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSINGLEVALUE('Length',$,IFCLENGTHMEASURE(5.),$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
|
||||
FILE_NAME('test.ifc','2024-08-30T12:34:53+05:00',('author','author email'),('organization','organization email'),'IfcOpenShell','Bonsai','Nobody');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSINGLEVALUE('Ratio',$,IFCRATIOMEASURE(0.5),$);
|
||||
#2=IFCPROPERTYSINGLEVALUE('Count',$,IFCCOUNTMEASURE(3.),$);
|
||||
#3=IFCPROPERTYSINGLEVALUE('Money',$,IFCMONETARYMEASURE(3.),$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
|
||||
FILE_NAME('test.ifc','2024-08-30T12:34:53+05:00',('author','author email'),('organization','organization email'),'IfcOpenShell','Bonsai','Nobody');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#2=IFCPROPERTYSINGLEVALUE('Length',$,IFCLENGTHMEASURE(5.),#1);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
|
||||
FILE_NAME('test.ifc','2024-08-30T12:34:53+05:00',('author','author email'),('organization','organization email'),'IfcOpenShell','Bonsai','Nobody');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROJECT('1iBd2lkrX61vZxF9AnXLa0',$,'My Project',$,$,$,$,(#3),#2);
|
||||
#2=IFCUNITASSIGNMENT((#4));
|
||||
#3=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#5,$);
|
||||
#4=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#5=IFCAXIS2PLACEMENT3D(#6,$,$);
|
||||
#6=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#7=IFCPROPERTYSINGLEVALUE('Length',$,IFCLENGTHMEASURE(5.),$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
|
||||
FILE_NAME('test.ifc','2024-08-30T12:34:53+05:00',('author','author email'),('organization','organization email'),'IfcOpenShell','Bonsai','Nobody');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#2=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(5.),#1);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
Reference in New Issue
Block a user