|
23 | 23 | import ifcopenshell.util.element |
24 | 24 | import ifcopenshell.util.pset |
25 | 25 |
|
| 26 | +#: A dict value containing any of these keys describes an ``IfcPropertyBoundedValue``. |
| 27 | +BOUNDED_VALUE_KEYS = ("LowerBoundValue", "UpperBoundValue", "SetPointValue") |
26 | 28 | # Sentinel distinguishing "no Unit dict was passed at all" from "a Unit dict was passed |
27 | 29 | # with Unit explicitly set to None" (i.e. explicitly clear an existing override). |
28 | 30 | _NO_UNIT = object() |
@@ -77,6 +79,11 @@ def edit_pset( |
77 | 79 | become IfcBoolean, and integers will become IfcInteger. If more |
78 | 80 | control is desired, you may explicitly specify IFC data objects |
79 | 81 | directly. Note that provided `properties` might be mutated in the process. |
| 82 | + An ``IfcPropertyBoundedValue`` may be added or edited by providing a |
| 83 | + dictionary containing any of the keys ``LowerBoundValue``, |
| 84 | + ``UpperBoundValue``, or ``SetPointValue`` (at least one is required). |
| 85 | + Values may be plain Python types (cast using the property set |
| 86 | + template, if available) or explicit IFC data objects. |
80 | 87 | :param pset_template: If a property set template is provided, this will |
81 | 88 | be used to determine data types. If no user-defined template is |
82 | 89 | provided, the built-in buildingSMART templates will be loaded. |
@@ -154,6 +161,14 @@ def edit_pset( |
154 | 161 | # Editing existing properties will retain their current data types |
155 | 162 | # if possible. So this will still be a length measure. |
156 | 163 | ifcopenshell.api.pset.edit_pset(model, pset=pset, properties={"ExplicitLength": 12.3}) |
| 164 | +
|
| 165 | + # Bounded values (IfcPropertyBoundedValue) are described using a |
| 166 | + # dictionary with LowerBoundValue / UpperBoundValue / SetPointValue keys. |
| 167 | + # Any subset of these keys may be provided, and existing values are |
| 168 | + # retained unless a new dictionary is passed. |
| 169 | + pset = ifcopenshell.api.pset.add_pset(model, product=wall_type, name="Pset_ElectricalDeviceCommon") |
| 170 | + ifcopenshell.api.pset.edit_pset(model, pset=pset, |
| 171 | + properties={"RatedCurrent": {"LowerBoundValue": 1.0, "UpperBoundValue": 2.0}}) |
157 | 172 | """ |
158 | 173 | usecase = Usecase() |
159 | 174 | usecase.file = file |
@@ -226,6 +241,10 @@ def update_existing_properties(self) -> list[ifcopenshell.entity_instance]: |
226 | 241 | prop = self.update_existing_prop_enum(prop) |
227 | 242 | if prop: |
228 | 243 | existing_props.append(prop) |
| 244 | + elif prop.is_a("IfcPropertyBoundedValue"): |
| 245 | + prop = self.update_existing_prop_bounded_value(prop) |
| 246 | + if prop: |
| 247 | + existing_props.append(prop) |
229 | 248 | elif prop.is_a("IfcPropertySingleValue"): |
230 | 249 | prop = self.update_existing_prop_single_value(prop) |
231 | 250 | if prop: |
@@ -298,6 +317,56 @@ def update_existing_prop_enum( |
298 | 317 | del self.settings["properties"][prop.Name] |
299 | 318 | return prop |
300 | 319 |
|
| 320 | + def update_existing_prop_bounded_value( |
| 321 | + self, prop: ifcopenshell.entity_instance |
| 322 | + ) -> Union[ifcopenshell.entity_instance, None]: |
| 323 | + """ |
| 324 | + NOTE: Assumes the prop exists |
| 325 | + """ |
| 326 | + value = self.settings["properties"][prop.Name] |
| 327 | + |
| 328 | + if value is None: |
| 329 | + if self._try_purge(prop): |
| 330 | + return |
| 331 | + for ifc_attribute in BOUNDED_VALUE_KEYS: |
| 332 | + setattr(prop, ifc_attribute, None) |
| 333 | + elif isinstance(value, dict) and any(k in value for k in BOUNDED_VALUE_KEYS): |
| 334 | + primary_measure_type = None |
| 335 | + for ifc_attribute in BOUNDED_VALUE_KEYS: |
| 336 | + if existing_value := getattr(prop, ifc_attribute): |
| 337 | + primary_measure_type = existing_value.is_a() |
| 338 | + break |
| 339 | + |
| 340 | + for ifc_attribute in BOUNDED_VALUE_KEYS: |
| 341 | + if ifc_attribute not in value: |
| 342 | + continue |
| 343 | + new_value = value[ifc_attribute] |
| 344 | + if new_value is None: |
| 345 | + setattr(prop, ifc_attribute, None) |
| 346 | + continue |
| 347 | + if not isinstance(new_value, ifcopenshell.entity_instance): |
| 348 | + measure_type = primary_measure_type or self.get_primary_measure_type(prop.Name) |
| 349 | + assert measure_type, f"Couldn't find primary measure type for the prop value: '{prop.Name}'." |
| 350 | + new_value = self.file.create_entity( |
| 351 | + measure_type, self.cast_value_to_primary_measure_type(new_value, measure_type) |
| 352 | + ) |
| 353 | + setattr(prop, ifc_attribute, new_value) |
| 354 | + |
| 355 | + if all(getattr(prop, k) is None for k in BOUNDED_VALUE_KEYS): |
| 356 | + if self._try_purge(prop): |
| 357 | + return |
| 358 | + |
| 359 | + if unit := value.get("Unit"): |
| 360 | + prop.Unit = unit |
| 361 | + else: |
| 362 | + raise ValueError( |
| 363 | + f'Value "{value}" is not a valid value for bounded property {prop.Name}. ' |
| 364 | + f"Expected None or a dict with any of {BOUNDED_VALUE_KEYS} keys." |
| 365 | + ) |
| 366 | + |
| 367 | + del self.settings["properties"][prop.Name] |
| 368 | + return prop |
| 369 | + |
301 | 370 | def update_existing_prop_single_value( |
302 | 371 | self, prop: ifcopenshell.entity_instance |
303 | 372 | ) -> Union[ifcopenshell.entity_instance, None]: |
@@ -370,6 +439,9 @@ def add_new_properties(self) -> list[ifcopenshell.entity_instance]: |
370 | 439 | for name, value in self.settings["properties"].items(): |
371 | 440 | if value is None and self.settings["should_purge"]: |
372 | 441 | continue |
| 442 | + if isinstance(value, dict) and any(k in value for k in BOUNDED_VALUE_KEYS): |
| 443 | + properties.append(self.create_bounded_value_prop(name, value)) |
| 444 | + continue |
373 | 445 | unit, value = self.unpack_unit_value(value) |
374 | 446 |
|
375 | 447 | if isinstance(value, ifcopenshell.entity_instance): |
@@ -445,6 +517,34 @@ def add_new_properties(self) -> list[ifcopenshell.entity_instance]: |
445 | 517 | properties.append(self.file.create_entity("IfcPropertySingleValue", **args)) |
446 | 518 | return properties |
447 | 519 |
|
| 520 | + def create_bounded_value_prop(self, name: str, value: dict[str, Any]) -> ifcopenshell.entity_instance: |
| 521 | + """Creates a new IfcPropertyBoundedValue from a dict of LowerBoundValue / |
| 522 | + UpperBoundValue / SetPointValue (and optionally Unit). |
| 523 | + """ |
| 524 | + primary_measure_type = None |
| 525 | + for ifc_attribute in BOUNDED_VALUE_KEYS: |
| 526 | + existing_value = value.get(ifc_attribute) |
| 527 | + if isinstance(existing_value, ifcopenshell.entity_instance): |
| 528 | + primary_measure_type = existing_value.is_a() |
| 529 | + break |
| 530 | + |
| 531 | + kwargs: dict[str, Any] = {"Name": name} |
| 532 | + for ifc_attribute in BOUNDED_VALUE_KEYS: |
| 533 | + if (bound_value := value.get(ifc_attribute)) is None: |
| 534 | + continue |
| 535 | + if not isinstance(bound_value, ifcopenshell.entity_instance): |
| 536 | + measure_type = primary_measure_type or self.get_primary_measure_type(name) |
| 537 | + assert measure_type, f"Couldn't find primary measure type for the prop value: '{name}'." |
| 538 | + bound_value = self.file.create_entity( |
| 539 | + measure_type, self.cast_value_to_primary_measure_type(bound_value, measure_type) |
| 540 | + ) |
| 541 | + kwargs[ifc_attribute] = bound_value |
| 542 | + |
| 543 | + if unit := value.get("Unit"): |
| 544 | + kwargs["Unit"] = unit |
| 545 | + |
| 546 | + return self.file.create_entity("IfcPropertyBoundedValue", **kwargs) |
| 547 | + |
448 | 548 | def assign_new_properties(self, props: list[ifcopenshell.entity_instance]) -> None: |
449 | 549 | if hasattr(self.settings["pset"], "HasProperties"): |
450 | 550 | self.settings["pset"].HasProperties = props |
|
0 commit comments