See #6570. Now it's possible to specify the formula in cost item quantity assignment

This commit is contained in:
Massimo Fabbro
2025-12-26 18:53:00 +01:00
committed by Massimo Fabbro
parent 340d4fb82a
commit 2c2d0f2434
@@ -16,6 +16,9 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.cost
import ifcopenshell.api.control
import ast
from typing import Any from typing import Any
import ifcopenshell.api.control import ifcopenshell.api.control
@@ -27,6 +30,8 @@ def assign_cost_item_quantity(
cost_item: ifcopenshell.entity_instance, cost_item: ifcopenshell.entity_instance,
products: list[ifcopenshell.entity_instance], products: list[ifcopenshell.entity_instance],
prop_name: str = "", prop_name: str = "",
formula: str = "",
ifc_class: str = "IfcQuantityLength",
) -> None: ) -> None:
"""Adds a cost item quantity that is parametrically connected to a product """Adds a cost item quantity that is parametrically connected to a product
@@ -57,6 +62,12 @@ def assign_cost_item_quantity(
:param prop_name: The name of the quantity. If this is not specified, :param prop_name: The name of the quantity. If this is not specified,
then it is assumed that there is no calculated quantity, and the then it is assumed that there is no calculated quantity, and the
number of objects are counted instead. number of objects are counted instead.
:param formula: The string that contains the formula
:param ifc_class: The quantity class of the calculated value if the formula is
specified. Can be ['IfcQuantityCount', 'IfcQuantityNumber',
'IfcQuantityLength', 'IfcQuantityArea', 'IfcQuantityVolume',
'IfcQuantityWeight', 'IfcQuantityTime']. Check
ifcopenshell.util.unit.QUANTITY_CLASS for more info.
:return: None :return: None
Example: Example:
@@ -84,6 +95,18 @@ def assign_cost_item_quantity(
# item. # item.
ifcopenshell.api.cost.assign_cost_item_quantity(model, ifcopenshell.api.cost.assign_cost_item_quantity(model,
cost_item=item, products=[slab], prop_name="NetVolume") cost_item=item, products=[slab], prop_name="NetVolume")
# Now let's use the formula in order to calculate the quantity value.
# For example, let's say that a IfcWall has the reinfocement volume ratio
# stored in the Pset_ConcreteElementGeneral.ReinforcementVolumeRatio
# and of course it has also the gross volume stored in the
# Qto_WallBaseQuantities.GrossVolume. So we can add an IfcQuantity that stores the
# reinforcement volume calculated with reinfocement volume ratio * gross volume.
ifcopenshell.api.cost.assign_cost_item_quantity(model,
cost_item=item, products=[wall],
formula="Pset_ConcreteElementGeneral.ReinforcementVolumeRatio * NetVolume"
ifc_class="IfcQuantityVolume")
""" """
usecase = Usecase() usecase = Usecase()
usecase.file = file usecase.file = file
@@ -91,6 +114,8 @@ def assign_cost_item_quantity(
"cost_item": cost_item, "cost_item": cost_item,
"products": products or [], "products": products or [],
"prop_name": prop_name, "prop_name": prop_name,
"formula": formula,
"ifc_class" : ifc_class
} }
return usecase.execute() return usecase.execute()
@@ -106,6 +131,42 @@ class Usecase:
if product.is_a("IfcSpatialElement"): if product.is_a("IfcSpatialElement"):
continue continue
self.assign_cost_control(related_object=product, cost_item=self.settings["cost_item"]) self.assign_cost_control(related_object=product, cost_item=self.settings["cost_item"])
if self.settings["formula"]:
# not so much elegant, but simple and useful because variables can't has the dot in the name
# and Psets have the dot
separator = "0"
variables = self.extract_variables(self.settings["formula"])
variables_modified = {v.replace(".", separator) : None for v in variables}
for v in variables_modified:
if separator in v:
variables_modified[v] = self.get_value_from_pset(product, v, separator)
else:
variables_modified[v] = self.get_value_from_qset(product, v)
formula_modified = self.settings["formula"].replace(".", separator)
if any(v is None for v in variables_modified.values()):
for k, v in variables_modified.items():
if v == None:
print(f"Property {k.replace(separator, '.')} not found")
raise ValueError("Formula contains variables that are not found in properties. Please check the output")
result = eval(formula_modified, {}, variables_modified)
new_quantity = None
self.quantities = set(self.settings["cost_item"].CostQuantities or [])
for quantity in self.quantities:
if quantity.Formula == self.settings["formula"]:
new_quantity = quantity
self.settings["ifc_class"] = quantity.is_a()
continue
if new_quantity is None:
new_quantity = self.file.create_entity(self.settings["ifc_class"], Name="Unnamed")
new_quantity.Formula = self.settings["formula"]
self.quantities.add(new_quantity)
new_quantity[3] = result
continue
if self.settings["prop_name"]: if self.settings["prop_name"]:
if ( if (
self.settings["cost_item"].CostQuantities self.settings["cost_item"].CostQuantities
@@ -113,7 +174,7 @@ class Usecase:
): ):
continue continue
self.add_quantity_from_related_object(product) self.add_quantity_from_related_object(product)
if self.settings["prop_name"]: if self.settings["prop_name"] or self.settings["formula"]:
self.settings["cost_item"].CostQuantities = list(self.quantities) self.settings["cost_item"].CostQuantities = list(self.quantities)
else: else:
self.update_cost_item_count() self.update_cost_item_count()
@@ -158,3 +219,58 @@ class Usecase:
if not obj.is_a("IfcConstructionResource"): if not obj.is_a("IfcConstructionResource"):
count += 1 count += 1
quantity[3] = count quantity[3] = count
def extract_variables(self, expr: str):
tree = ast.parse(expr, mode="eval")
extractor = VariableExtractor()
extractor.visit(tree)
return extractor.variables
def get_value_from_pset(
self,
product:ifcopenshell.entity_instance,
v: str,
separator: str,
) -> float:
for relationship in product.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByProperties"):
pset = relationship.RelatingPropertyDefinition
if pset.Name == v.split(separator)[0]:
for prop in pset.HasProperties:
if prop.Name.lower() == v.split(separator)[1].lower():
return prop[2].wrappedValue
def get_value_from_qset(
self,
product:ifcopenshell.entity_instance,
v: str,
) -> float:
for relationship in product.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByProperties"):
qto = relationship.RelatingPropertyDefinition
if qto.is_a("IfcElementQuantity"):
for prop in qto.Quantities:
if prop.is_a("IfcPhysicalSimpleQuantity") and prop.Name.lower() == v.lower():
return prop[3]
class VariableExtractor(ast.NodeVisitor):
def __init__(self):
self.variables = set()
def visit_Name(self, node):
# simple variables wothout dots
self.variables.add(node.id)
def visit_Attribute(self, node):
# for variables with dots (chains)
parts = []
current = node
while isinstance(current, ast.Attribute):
parts.append(current.attr)
current = current.value
if isinstance(current, ast.Name):
parts.append(current.id)
full_name = ".".join(reversed(parts))
self.variables.add(full_name)