First Commit
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user