Merge remote-tracking branch 'origin/v0.6.0' into v0.7.0

This commit is contained in:
Thomas Krijnen
2021-08-15 20:40:55 +02:00
92 changed files with 2659 additions and 724 deletions
@@ -1,31 +0,0 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"cost_item": None, "products": []}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
quantity_names = set()
for quantity in self.settings["cost_item"].CostQuantities or []:
if quantity.Name:
quantity_names.add(quantity.Name)
for product in self.settings["products"]:
ifcopenshell.api.run(
"control.assign_control",
self.file,
related_object=product,
relating_control=self.settings["cost_item"],
)
for name in quantity_names:
ifcopenshell.api.run(
"cost.assign_cost_item_product_quantities",
self.file,
cost_item=self.settings["cost_item"],
prop_name=name
)
@@ -4,25 +4,28 @@ import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"cost_item": None, "prop_name": ""}
self.settings = {"cost_item": None, "products": [], "prop_name": ""}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
self.quantities = set(self.settings["cost_item"].CostQuantities or [])
for control in self.settings["cost_item"].Controls or []:
for related_object in control.RelatedObjects:
self.add_quantity_from_related_object(related_object)
for product in self.settings["products"]:
ifcopenshell.api.run(
"control.assign_control",
self.file,
related_object=product,
relating_control=self.settings["cost_item"],
)
self.add_quantity_from_related_object(product)
self.settings["cost_item"].CostQuantities = list(self.quantities)
def add_quantity_from_related_object(self, element):
if element.is_a("IfcTypeObject"):
for definition in element.HasPropertySets or []:
self.add_quantity_from_qto(definition)
else:
for relationship in element.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByProperties"):
self.add_quantity_from_qto(relationship.RelatingPropertyDefinition)
if not element.is_a("IfcObject"):
return
for relationship in element.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByProperties"):
self.add_quantity_from_qto(relationship.RelatingPropertyDefinition)
def add_quantity_from_qto(self, qto):
if not qto.is_a("IfcElementQuantity"):
@@ -51,21 +51,43 @@ class Data:
del data["OwnerHistory"]
del data["CostValues"]
data["IsNestedBy"] = []
data["Controls"] = []
data["Controls"] = {}
for rel in cost_item.IsNestedBy:
[data["IsNestedBy"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcCostItem")]
parametric_quantities = []
for rel in cost_item.Controls:
[data["Controls"].append(o.id()) for o in rel.RelatedObjects or []]
for related_object in rel.RelatedObjects or []:
quantities = cls.get_object_quantities(cost_item, related_object)
data["Controls"][related_object.id()] = quantities
parametric_quantities.extend(quantities)
cls.cost_items[cost_item.id()] = data
cls.load_cost_item_quantities(cost_item, data)
cls.load_cost_item_quantities(cost_item, data, parametric_quantities)
cls.load_cost_item_values(cost_item, data)
cls.is_loaded = True
@classmethod
def load_cost_item_quantities(cls, cost_item, data):
def get_object_quantities(cls, cost_item, element):
if not element.is_a("IfcObject"):
return []
results = []
for relationship in element.IsDefinedBy:
if not relationship.is_a("IfcRelDefinesByProperties"):
continue
qto = relationship.RelatingPropertyDefinition
if not qto.is_a("IfcElementQuantity"):
continue
for prop in qto.Quantities:
if prop in cost_item.CostQuantities or []:
results.append(prop.id())
return results
@classmethod
def load_cost_item_quantities(cls, cost_item, data, parametric_quantities):
data["CostQuantities"] = []
data["TotalCostQuantity"] = cls.get_total_quantity(cost_item)
for quantity in cost_item.CostQuantities or []:
if quantity.id() in parametric_quantities:
continue
quantity_data = quantity.get_info()
del quantity_data["Unit"]
cls.physical_quantities[quantity.id()] = quantity_data
@@ -98,7 +120,11 @@ class Data:
def load_cost_item_value(cls, cost_item_data, cost_item, cost_value):
value_data = cost_value.get_info()
del value_data["AppliedValue"]
del value_data["UnitBasis"]
if value_data["UnitBasis"]:
data = cost_value.UnitBasis.get_info()
data["ValueComponent"] = data["ValueComponent"].wrappedValue
data["UnitComponent"] = data["UnitComponent"].id()
value_data["UnitBasis"] = data
if value_data["ApplicableDate"]:
value_data["ApplicableDate"] = ifcopenshell.util.date.ifc2datetime(value_data["ApplicableDate"])
if value_data["FixedUntilDate"]:
@@ -1,3 +1,8 @@
import ifcopenshell
import ifcopenshell.util.unit
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, **settings):
self.file = file
@@ -10,4 +15,19 @@ class Usecase:
if name == "AppliedValue" and value is not None:
# TODO: support all applied value select types
value = self.file.createIfcMonetaryMeasure(value)
elif name == "UnitBasis":
self.remove_existing_unit_basis()
if value:
value_component = self.file.create_entity(
ifcopenshell.util.unit.get_unit_measure_type(value["UnitComponent"].UnitType),
value["ValueComponent"],
)
value = self.file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"])
setattr(self.settings["cost_value"], name, value)
def remove_existing_unit_basis(self):
if (
self.settings["cost_value"].UnitBasis
and len(self.file.get_inverse(self.settings["cost_value"].UnitBasis)) == 1
):
ifcopenshell.util.element.remove_deep(self.file, self.settings["cost_value"].UnitBasis)
@@ -9,7 +9,7 @@ class Usecase:
layers = list(self.settings["layer_set"].MaterialLayers or [])
layer = self.file.create_entity("IfcMaterialLayer", **{
"Material": self.settings["material"],
"LayerThickness": 0.
"LayerThickness": 1.
})
layers.append(layer)
self.settings["layer_set"].MaterialLayers = layers
@@ -11,6 +11,7 @@ class Usecase():
self.settings[key] = value
def execute(self):
# TODO: don't also edit the profile def in this usecase
for name, value in self.settings["attributes"].items():
setattr(self.settings["profile"], name, value)
self.settings["profile"].Material = self.settings["material"]
@@ -7,3 +7,4 @@ class Usecase:
def execute(self):
self.file.remove(self.settings["profile"])
# TODO: deep purge
@@ -0,0 +1,10 @@
class Usecase():
def __init__(self, file, **settings):
self.file = file
self.settings = {"profile": None, "attributes": {}}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["profile"], name, value)
@@ -0,0 +1,10 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"profile": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
self.file.remove(self.settings["profile"])
# TODO: deep purge
@@ -11,12 +11,15 @@ class Usecase:
def execute(self):
self.added_elements = set()
self.whitelisted_inverse_attributes = {}
if self.settings["element"].is_a("IfcTypeProduct"):
return self.append_type_product()
elif self.settings["element"].is_a("IfcMaterial"):
return self.append_material()
elif self.settings["element"].is_a("IfcCostSchedule"):
return self.append_cost_schedule()
elif self.settings["element"].is_a("IfcProfileDef"):
return self.append_profile_def()
def is_already_appended(self):
try:
@@ -36,6 +39,12 @@ class Usecase:
self.whitelisted_inverse_attributes = {"IfcCostSchedule": ["Controls"], "IfcCostItem": ["IsNestedBy"]}
return self.add_element(self.settings["element"])
def append_profile_def(self):
if [e for e in self.file.by_type("IfcProfileDef") if e.ProfileName == self.settings["element"].ProfileName]:
return
self.whitelisted_inverse_attributes = {"IfcProfileDef": ["HasProperties"]}
return self.add_element(self.settings["element"])
def append_type_product(self):
if self.is_already_appended():
return
@@ -64,3 +64,15 @@ class Usecase:
"Material": self.settings["product"],
}
)
elif self.settings["product"].is_a("IfcProfileDef"):
for definition in self.settings["product"].HasProperties or []:
if definition.Name == self.settings["name"]:
return definition
return self.file.create_entity(
"IfcProfileProperties",
**{
"Name": self.settings["name"],
"ProfileDefinition": self.settings["product"],
}
)
@@ -26,6 +26,8 @@ class Data:
cls.add_type_product_psets(product, product_id)
elif product.is_a("IfcMaterialDefinition"):
cls.add_material_psets(product, product_id)
elif product.is_a("IfcProfileDef"):
cls.add_profile_psets(product, product_id)
else:
cls.add_product_psets(product, product_id)
@@ -44,6 +46,13 @@ class Data:
for pset in product.HasProperties:
cls.add_pset(pset, product_id)
@classmethod
def add_profile_psets(cls, product, product_id):
if not product.HasProperties:
return
for pset in product.HasProperties:
cls.add_pset(pset, product_id)
@classmethod
def add_product_psets(cls, product, product_id):
if not hasattr(product, "IsDefinedBy") or not product.IsDefinedBy:
@@ -59,7 +68,7 @@ class Data:
@classmethod
def add_pset(cls, pset, product_id):
data = pset.get_info()
if not pset.is_a("IfcMaterialProperties"):
if not pset.is_a("IfcMaterialProperties") and not pset.is_a("IfcProfileProperties"):
del data["OwnerHistory"]
del data["HasProperties"]
if hasattr(pset, "HasProperties"):
@@ -0,0 +1,9 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"unit_type": "LENGTHUNIT", "name": "METRE"}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
return self.file.create_entity("IfcSIUnit", UnitType=self.settings["unit_type"], Name=self.settings["name"])
@@ -24,6 +24,9 @@ class Data:
return
for unit in unit_assignment[0].Units:
cls.unit_assignment.append(unit.id())
for unit in (
cls.file.by_type("IfcDerivedUnit") + cls.file.by_type("IfcNamedUnit") + cls.file.by_type("IfcMonetaryUnit")
):
cls.load_unit(unit)
cls.is_loaded = True
@@ -0,0 +1,17 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"units": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
unit_assignment = self.file.by_type("IfcUnitAssignment")
if not unit_assignment:
return
unit_assignment = unit_assignment[0]
units = set(unit_assignment.Units or [])
units = units - set(self.settings["units"])
if units:
unit_assignment.Units = list(units)
return unit_assignment
+10 -2
View File
@@ -359,19 +359,27 @@ class file(object):
return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)]
return [entity_instance(e, self) for e in self.wrapped_data.by_type_excl_subtypes(type)]
def traverse(self, inst, max_levels=None):
def traverse(self, inst, max_levels=None, breadth_first=False):
"""Get a list of all referenced instances for a particular instance including itself
:param inst: The entity instance to get all sub instances
:type inst: ifcopenshell.entity_instance.entity_instance
:param max_levels: How far deep to recursively fetch sub instances. None or -1 means infinite.
:type max_levels: None|int
:param breadth_first: Whether to use breadth-first search, the default is depth-first.
:type max_levels: bool
:returns: A list of ifcopenshell.entity_instance.entity_instance objects
:rtype: list
"""
if max_levels is None:
max_levels = -1
return [entity_instance(e, self) for e in self.wrapped_data.traverse(inst.wrapped_data, max_levels)]
if breadth_first:
fn = self.wrapped_data.traverse_breadth_first
else:
fn = self.wrapped_data.traverse
return [entity_instance(e, self) for e in fn(inst.wrapped_data, max_levels)]
def get_inverse(self, inst):
"""Return a list of entities that reference this entity
@@ -104,7 +104,7 @@ def has_element_reference(value, element):
def remove_deep(ifc_file, element):
# @todo maybe some sort of try-finally mechanism.
ifc_file.batch()
subgraph = list(ifc_file.traverse(element))
subgraph = list(ifc_file.traverse(element, breadth_first=True))
subgraph_set = set(subgraph)
for ref in subgraph[::-1]:
if ref.id() and len(set(ifc_file.get_inverse(ref)) - subgraph_set) == 0:
@@ -25,7 +25,7 @@ unit_names = [
"CANDELA",
"COULOMB",
"CUBIC_METRE",
"DEGREE CELSIUS",
"DEGREE_CELSIUS",
"FARAD",
"GRAM",
"GRAY",
@@ -43,7 +43,7 @@ unit_names = [
"SECOND",
"SIEMENS",
"SIEVERT",
"SQUARE METRE",
"SQUARE_METRE",
"METRE",
"STERADIAN",
"TESLA",
@@ -52,7 +52,6 @@ unit_names = [
"WEBER",
]
si_dimensions = {
"METRE": (1, 0, 0, 0, 0, 0, 0),
"SQUARE_METRE": (2, 0, 0, 0, 0, 0, 0),
@@ -87,6 +86,72 @@ si_dimensions = {
"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",
}
# Are you good at physics? Want to help fill these in? :)
named_dimensions = {
# "ABSORBEDDOSEUNIT": (0, 0, 0, 0, 0, 0, 0),
"AMOUNTOFSUBSTANCEUNIT": (0, 0, 0, 0, 0, 1, 0),
"AREAUNIT": (2, 0, 0, 0, 0, 0, 0),
# "DOSEEQUIVALENTUNIT": (0, 0, 0, 0, 0, 0, 0),
# "ELECTRICCAPACITANCEUNIT": (0, 0, 0, 0, 0, 0, 0),
# "ELECTRICCHARGEUNIT": (0, 0, 0, 0, 0, 0, 0),
# "ELECTRICCONDUCTANCEUNIT": (0, 0, 0, 0, 0, 0, 0),
"ELECTRICCURRENTUNIT": (0, 0, 0, 1, 0, 0, 0),
# "ELECTRICRESISTANCEUNIT": (0, 0, 0, 0, 0, 0, 0),
# "ELECTRICVOLTAGEUNIT": (0, 0, 0, 0, 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": (0, 0, 0, 0, 0, 0, 0),
# "INDUCTANCEUNIT": (0, 0, 0, 0, 0, 0, 0),
"LENGTHUNIT": (1, 0, 0, 0, 0, 0, 0),
# "LUMINOUSFLUXUNIT": (0, 0, 0, 0, 0, 0, 0),
"LUMINOUSINTENSITYUNIT": (0, 0, 0, 0, 0, 0, 1),
# "MAGNETICFLUXDENSITYUNIT": (0, 0, 0, 0, 0, 0, 0),
# "MAGNETICFLUXUNIT": (0, 0, 0, 0, 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, 0, 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),
}
si_conversions = {
"inch": 0.0254,
"foot": 0.3048,
@@ -169,7 +234,7 @@ def get_prefix_multiplier(text):
def get_unit_name(text):
text = text.upper().replace("METER", "METRE")
for name in unit_names:
if name in text:
if name.replace("_", " ") in text:
return name
@@ -177,6 +242,10 @@ 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_property_unit(prop, ifc_file):
unit = getattr(prop, "Unit", None)
if unit:
@@ -197,6 +266,10 @@ def get_property_unit(prop, ifc_file):
return units[0]
def get_unit_measure_type(unit_type):
return "Ifc" + unit_type[0:-4].lower().capitalize() + "Measure"
def get_symbol_quantity_class(symbol):
# Dumb, but everybody gets it, unlike regex golf
if not symbol:
@@ -259,19 +332,17 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit):
return value
"""Returns a unit scale factor to convert to and from IFC project length units and SI meters
Example::
ifc_project_length * unit_scale = si_meters
si_meters / unit_scale = ifc_project_length
:returns: The scale factor
:rtype: float
"""
def calculate_unit_scale(file):
"""Returns a unit scale factor to convert to and from IFC project length units and SI meters
Example::
ifc_project_length * unit_scale = si_meters
si_meters / unit_scale = ifc_project_length
:returns: The scale factor
:rtype: float
"""
units = file.by_type("IfcUnitAssignment")[0]
unit_scale = 1
for unit in units.Units:
@@ -34,7 +34,9 @@ class json_logger:
self.instance = instance
def log(self, level, message, *args, **kwargs):
self.statements.append(log_entry_type(level, message % args, kwargs.get("instance"))._asdict())
self.statements.append(
log_entry_type(level, message % args, kwargs.get("instance"))._asdict()
)
def __getattr__(self, level):
return functools.partial(self.log, level, instance=self.instance)
@@ -81,7 +83,9 @@ def assert_valid(attr, val, schema):
if isinstance(attr_type, simple_type):
invalid = type(val) != simple_type_python_mapping[attr_type.declared_type()]
elif isinstance(attr_type, (entity_type, type_declaration)):
invalid = not isinstance(val, ifcopenshell.entity_instance) or not val.is_a(attr_type.name())
invalid = not isinstance(val, ifcopenshell.entity_instance) or not val.is_a(
attr_type.name()
)
elif isinstance(attr_type, select_type):
val_to_use = val
if isinstance(schema.declaration_by_name(val.is_a()), enumeration_type):
@@ -90,13 +94,19 @@ def assert_valid(attr, val, schema):
else:
invalid = True
if not invalid:
invalid = not any(try_valid(x, val_to_use, schema) for x in attr_type.select_list())
invalid = not any(
try_valid(x, val_to_use, schema) for x in attr_type.select_list()
)
elif isinstance(attr_type, enumeration_type):
invalid = val not in attr_type.enumeration_items()
elif isinstance(attr_type, aggregation_type):
b1, b2 = attr_type.bound1(), attr_type.bound2()
ty = attr_type.type_of_element()
invalid = len(val) < b1 or (b2 != -1 and len(val) > b2) or not all(assert_valid(ty, v, schema) for v in val)
invalid = (
len(val) < b1
or (b2 != -1 and len(val) > b2)
or not all(assert_valid(ty, v, schema) for v in val)
)
else:
raise NotImplementedError("Not impl %s %s" % (type(attr_type), attr_type))
@@ -135,6 +145,7 @@ def validate(f, logger):
logger.set_instance(inst)
entity = schema.declaration_by_name(inst.is_a())
attrs = entity.all_attributes()
if entity.is_abstract():
e = "Entity %s is abstract" % entity.name()
@@ -143,20 +154,38 @@ def validate(f, logger):
else:
logger.error("In %s\n%s", inst, e)
for attr, val, is_derived in zip(entity.all_attributes(), inst, entity.derived()):
has_invalid_value = False
for i in range(len(attrs)):
try:
inst[i]
pass
except:
if hasattr(logger, "set_instance"):
logger.error("Invalid attribute value for %s.%s", entity, attrs[i])
else:
logger.error(
"In %s\nInvalid attribute value for %s.%s",
inst,
entity,
attrs[i],
)
has_invalid_value = True
if val is None and not (is_derived or attr.optional()):
logger.error("Attribute %s.%s not optional", entity, attr)
if not has_invalid_value:
for attr, val, is_derived in zip(attrs, inst, entity.derived()):
if val is not None:
attr_type = attr.type_of_attribute()
try:
assert_valid(attr, val, schema)
except ValidationError as e:
if hasattr(logger, "set_instance"):
logger.error(str(e))
else:
logger.error("In %s\n%s", inst, e)
if val is None and not (is_derived or attr.optional()):
logger.error("Attribute %s.%s not optional", entity, attr)
if val is not None:
attr_type = attr.type_of_attribute()
try:
assert_valid(attr, val, schema)
except ValidationError as e:
if hasattr(logger, "set_instance"):
logger.error(str(e))
else:
logger.error("In %s\n%s", inst, e)
for attr in entity.all_inverse_attributes():
val = getattr(inst, attr.name())