diff --git a/src/speckleifc/property_extraction.py b/src/speckleifc/property_extraction.py index 8be9c9a..81737ce 100644 --- a/src/speckleifc/property_extraction.py +++ b/src/speckleifc/property_extraction.py @@ -3,7 +3,7 @@ from typing import Any from ifcopenshell.entity_instance import entity_instance from ifcopenshell.util.element import get_type -from speckleifc.quantity_extraction import get_quantities +from speckleifc.qtos_only import get_quantities def extract_properties(element: entity_instance) -> dict[str, object]: diff --git a/src/speckleifc/qtos_only.py b/src/speckleifc/qtos_only.py new file mode 100644 index 0000000..b20b012 --- /dev/null +++ b/src/speckleifc/qtos_only.py @@ -0,0 +1,112 @@ +from typing import Any + +from ifcopenshell.entity_instance import entity_instance +from ifcopenshell.util.unit import get_full_unit_name, get_project_unit + + +def _format_unit_name(unit_name: str) -> str: + """ + Convert IFC unit names to user-friendly format. + """ + if not unit_name: + return "" + + # Convert underscore-separated words to space-separated and title case + return unit_name.replace("_", " ").title() + + +def _get_unit_info(element: entity_instance, quantity) -> dict[str, str]: + """Get unit information for a quantity.""" + try: + if hasattr(quantity, 'Unit') and quantity.Unit: + # Quantity has its own unit + try: + unit_name = get_full_unit_name(quantity.Unit) + formatted_unit_name = _format_unit_name(unit_name) + return {"units": formatted_unit_name} + except: + return {"units": str(quantity.Unit)} + else: + # Fall back to project unit based on quantity type + unit_mapping = { + "IfcQuantityLength": "LENGTHUNIT", + "IfcQuantityArea": "AREAUNIT", + "IfcQuantityVolume": "VOLUMEUNIT", + "IfcQuantityCount": None, # Count quantities typically have no units + "IfcQuantityWeight": "MASSUNIT", + "IfcQuantityTime": "TIMEUNIT" + } + + quantity_type = quantity.is_a() + unit_type = unit_mapping.get(quantity_type) + if not unit_type: + return {} + + # Get the project unit for this unit type (with built-in caching) + project_unit = get_project_unit(element.file, unit_type, use_cache=True) + if not project_unit: + return {} + + # Get unit name + unit_name = get_full_unit_name(project_unit) + + # Format the unit name to be user-friendly + formatted_unit_name = _format_unit_name(unit_name) + + return {"units": formatted_unit_name} + except Exception: + # If anything fails, return empty dict to maintain robustness + return {} + + +def _get_quantities(quantities: list[entity_instance], element: entity_instance) -> dict[str, Any]: + """Extract quantity values from IfcPhysicalQuantity entities.""" + results = {} + for quantity in quantities or []: + quantity_name = quantity.Name + if quantity.is_a("IfcPhysicalSimpleQuantity"): + # Get the quantity value (3rd attribute for simple quantities) + value = getattr(quantity, quantity.attribute_name(3)) + unit_info = _get_unit_info(element, quantity) + + if unit_info: + # Create structured quantity object with units + results[quantity_name] = { + "name": quantity_name, + "value": value, + **unit_info, + } + else: + # No unit info available, keep as simple value with name + results[quantity_name] = {"name": quantity_name, "value": value} + + elif quantity.is_a("IfcPhysicalComplexQuantity"): + # Handle complex quantities + data = {k: v for k, v in quantity.get_info().items() if v is not None and k != "Name"} + data["properties"] = _get_quantities(quantity.HasQuantities, element) + del data["HasQuantities"] + results[quantity_name] = data + return results + + +def get_quantities(element: entity_instance) -> dict[str, object]: + """ + Extract quantity takeoffs (QTOs) from an IFC element with unit information. + """ + qtos = {} + + # Handle elements with IsDefinedBy relationship + if hasattr(element, "IsDefinedBy") and element.IsDefinedBy: + for relationship in element.IsDefinedBy: + if relationship.is_a("IfcRelDefinesByProperties"): + definition = relationship.RelatingPropertyDefinition + if definition.is_a("IfcElementQuantity"): + try: + quantities_data = _get_quantities(definition.Quantities, element) + quantities_data["id"] = definition.id() + qtos[definition.Name] = quantities_data + except (KeyError, AttributeError): + # If entity access fails, skip this quantity set + continue + + return qtos \ No newline at end of file diff --git a/src/speckleifc/quantity_extraction.py b/src/speckleifc/quantity_extraction.py index 4e6c8dd..e583055 100644 --- a/src/speckleifc/quantity_extraction.py +++ b/src/speckleifc/quantity_extraction.py @@ -4,66 +4,6 @@ from ifcopenshell.entity_instance import entity_instance from ifcopenshell.util.element import get_psets from ifcopenshell.util.unit import get_full_unit_name, get_project_unit -# Global cache for project units per IFC file -_file_project_units_cache: dict[int, dict[str, Any]] = {} - -# Cache for unit information by field name per file -_quantity_field_units_cache: dict[int, dict[str, dict[str, str]]] = {} - - -def _get_cached_project_unit(element: entity_instance, unit_type: str): - """ - Get project unit with caching per file. - """ - file_id = id(element.file) # Use file object ID as cache key - - # Initialize cache for this file if needed - if file_id not in _file_project_units_cache: - _file_project_units_cache[file_id] = {} - - file_cache = _file_project_units_cache[file_id] - - # Check if we already cached this unit type for this file - if unit_type in file_cache: - return file_cache[unit_type] - - # Not cached - get project unit and cache it - try: - project_unit = get_project_unit(element.file, unit_type) - file_cache[unit_type] = project_unit - return project_unit - except Exception: - # Cache None for failed lookups to avoid repeated failures - file_cache[unit_type] = None - return None - - -def _get_cached_field_unit_info(element: entity_instance, qty_entity) -> dict[str, str]: - """ - Get unit info for quantity field with caching by field name. - """ - file_id = id(element.file) - field_name = qty_entity.Name - - # Handle empty field names with fallback to direct computation - if not field_name: - return _get_unit_info(element, qty_entity.is_a()) - - # Initialize file cache if needed - if file_id not in _quantity_field_units_cache: - _quantity_field_units_cache[file_id] = {} - - field_cache = _quantity_field_units_cache[file_id] - - # Check if we already cached this field name for this file - if field_name in field_cache: - return field_cache[field_name] - - # Not cached - compute unit info and cache it by field name - unit_info = _get_unit_info(element, qty_entity.is_a()) - field_cache[field_name] = unit_info - return unit_info - def _format_unit_name(unit_name: str) -> str: """ @@ -95,8 +35,8 @@ def _get_unit_info(element: entity_instance, quantity_type: str) -> dict[str, st if not unit_type: return {} - # Get the project unit for this unit type (cached) - project_unit = _get_cached_project_unit(element, unit_type) + # Get the project unit for this unit type (with built-in caching) + project_unit = get_project_unit(element.file, unit_type, use_cache=True) if not project_unit: return {} @@ -117,7 +57,7 @@ def get_quantities(element: entity_instance) -> dict[str, object]: Extract quantity takeoffs (QTOs) from an IFC element with unit information. """ # Get basic quantities using existing utility - quantities = get_psets(element, qtos_only=True) + quantities = get_psets(element, qtos_only=True, should_inherit=False) if not quantities: return {} @@ -148,7 +88,7 @@ def get_quantities(element: entity_instance) -> dict[str, object]: # Get the IFC quantity entity for unit information qty_entity = quantity_entities[qty_name] - unit_info = _get_cached_field_unit_info(element, qty_entity) + unit_info = _get_unit_info(element, qty_entity.is_a()) if unit_info: # Create structured quantity object with units