Coverage for backend/django/core/auxiliary/models/PropertyInfo.py: 92%
160 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-11 21:43 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-11 21:43 +0000
1from django.db import models
2from django.db.models import Q, Count
4from core.auxiliary.models.IndexedItem import IndexedItem
5from core.auxiliary.enums.uiEnums import DisplayType
6from core.auxiliary.enums.unitsOfMeasure import UnitOfMeasure
7from idaes_factory.unit_conversion import convert_value
8from idaes_factory.queryset_lookup import get_value_object
9from common.config_types import *
10from core.auxiliary.models.ControlValue import ControlValue
11from flowsheetInternals.unitops.config.config_base import configuration
13from core.managers import AccessControlManager
14from core.auxiliary.models.PropertyValue import PropertyValue
15from typing import TYPE_CHECKING
16if TYPE_CHECKING:
17 from core.auxiliary.models.PropertyValue import PropertyValue
19class HistoricalValue(models.Model):
20 flowsheet = models.ForeignKey("Flowsheet", on_delete=models.CASCADE, related_name="HistoricalValues")
21 value = models.FloatField()
22 property = models.ForeignKey("PropertyInfo", on_delete=models.CASCADE, related_name="history", null=True)
24 created_at = models.DateTimeField(auto_now_add=True)
25 objects = AccessControlManager()
29 class Meta:
30 ordering = ['created_at']
33class ProcessPathProperty(models.Model):
34 flowsheet = models.ForeignKey("Flowsheet", on_delete=models.CASCADE, related_name="ProcessPathProperties")
35 value = models.FloatField(null=True, blank=True)
36 property = models.ForeignKey("PropertyInfo", on_delete=models.CASCADE, related_name="ProcessPathProperties", null=True)
37 path = models.ForeignKey("ProcessPath", on_delete=models.CASCADE, related_name="ProcessPathProperties", null=True)
39 created_at = models.DateTimeField(auto_now_add=True)
40 objects = AccessControlManager()
43 class Meta:
44 ordering = ['created_at']
47class PropertyInfo(models.Model):
48 flowsheet = models.ForeignKey("Flowsheet", on_delete=models.CASCADE, related_name="propertyInfos")
49 set = models.ForeignKey("PropertySet", on_delete=models.CASCADE, related_name="ContainedProperties", null=True)
50 type = models.CharField(choices=DisplayType.choices , default=DisplayType.numeric)
51 unitType = models.CharField(choices=UnitOfMeasure.choices , default=UnitOfMeasure.none)
52 unit = models.CharField(max_length=32, blank=True) # selected unit
53 key = models.CharField(max_length=64)
54 displayName = models.CharField(max_length=64)
55 index = models.IntegerField(default=0)
56 created_at = models.DateTimeField(auto_now_add=True)
57 values: models.QuerySet["PropertyValue"]
59 objects = AccessControlManager()
61 class Meta:
62 ordering = ['created_at']
64 @classmethod
65 def create(cls, indexes:dict[str,list[IndexedItem]]={}, **fields) -> tuple["PropertyInfo", list[PropertyValue]]:
67 value = fields.pop("value")
68 property_info = PropertyInfo(**fields)
69 # Create a property value object with this value
70 if indexes == {}: 70 ↛ 73line 70 didn't jump to line 73 because the condition on line 70 was always true
71 property_value = PropertyValue(value=value, property=property_info, flowsheet=fields.get("flowsheet"))
73 return property_info, [property_value]
76 @classmethod
77 def create_save(cls, **fields) -> "PropertyInfo":
78 # Create and save a new property info object
79 property_info, property_values = cls.create(**fields)
80 property_info.save()
81 for property_value in property_values:
82 property_value.save()
83 return property_info
86 def get_value_bulk(self, indexes: list | None = None) -> Any:
87 """
88 Get the value of the property at the specified indexes,
89 or the first value if no indexes are specified.
91 This method should be used in bulk operations (where
92 all the related property values are prefetched).
93 """
94 property_value = get_value_object(self, indexes)
95 if property_value is None:
96 raise ValueError(f"No property values found with indexes={indexes}")
97 return property_value.value
100 def set_value_bulk(self, value: Any, indexes: list | None = None) -> PropertyValue:
101 """
102 Set the value of the property at the given indexes,
103 or the first value if no indexes are specified.
105 This method should be used in bulk operations (where
106 all the related property values are prefetched).
107 """
108 property_value = get_value_object(self, indexes)
109 property_value.value = value
110 return property_value
113 def get_value_object(self, indexes: list[str] | None = None) -> PropertyValue:
114 """
115 Get the property value object at the specified indexes,
116 or the first value if no indexes are specified.
118 This method should not be used in bulk operations.
119 """
120 if indexes is None:
121 property_value = self.values.first()
122 else:
123 annotated_values = self.values.annotate(
124 total_indexes=Count("indexedItems"),
125 matching_indexes=Count("indexedItems", filter=Q(indexedItems__key__in=indexes))
126 )
127 property_value = annotated_values.get(total_indexes=len(indexes), matching_indexes=len(indexes))
128 return property_value
131 def get_value(self, indexes: list | None = None) -> Any:
132 """
133 Get the value of the property at the specified indexes,
134 or the first value if no indexes are specified.
136 This method should not be used in bulk operations.
137 """
138 value_object = self.get_value_object(indexes=indexes)
139 if value_object is None: 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 return None
141 return value_object.value
144 def set_value(self, value: Any, indexes=None) -> None:
145 """
146 Set the value of the property at the given indexes,
147 or the first value if no indexes are specified.
149 This method should not be used in bulk operations.
150 """
151 property_value = self.get_value_object(indexes=indexes)
152 property_value.value = value
153 property_value.save()
156 def get_indexes(self, index_set: str) -> list[IndexedItem]:
157 """
158 Get all the indexed items of the specified type for this property.
159 """
160 property_values = self.values.all()
161 return IndexedItem.objects.filter(propertyValues__in=property_values, type=index_set)
163 def has_value(self) -> bool:
164 property_value = self.get_value_object()
165 if property_value is None: 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 return False
167 return property_value.has_value()
169 def has_value_bulk(self) -> bool:
170 property_value = get_value_object(self)
171 if property_value is None:
172 return False
173 return property_value.has_value()
175 def get_cutoff_and_property_values(self, config: ObjectType):
176 first_index = config.indexSets[0] # get the outermost level e.g: "outlet 1, outlet 2"
177 first_indexed_items = IndexedItem.objects.filter(owner=self.set.simulationObject, type=first_index)
179 property_values = []
180 for indexed_item in first_indexed_items:
181 property_values.extend(indexed_item.propertyValues.all())
183 count = first_indexed_items.count()
185 if not count: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true
186 return None, None
188 cutoff = len(property_values) - 1 - (len(property_values) / count) # get the index where it starts to be disabled
190 return cutoff, property_values
193 def isSpecified(self) -> bool:
194 """
195 Checks if all the required property values have been specified.
196 This includes values that are enabled, control set points, and recycle guesses.
197 Both guesses and set points are needed.
198 """
199 value: PropertyValue
200 for value in self.values.all():
201 if (
202 (
203 value.enabled
204 or value.is_control_set_point()
205 or self.is_recycle_var()
206 )
207 and not value.has_value()
208 ):
209 return False
210 return True
213 def enable(self, condition: bool = True) -> list[PropertyValue]:
214 """Enables or disables property values based on configuration rules."""
215 object_type = self.set.simulationObject.objectType
216 config = configuration.get(object_type)
218 list_prop_val = []
220 if self.key not in config.properties:
221 propertySetGroup_key = None; # default to none, we are assuming the property doesn't exist.
222 else:
223 propertySetGroup_key = config.properties[self.key].propertySetGroup
224 propertySetGroup_config = config.propertySetGroups.get(propertySetGroup_key, None)
226 if propertySetGroup_config is not None and propertySetGroup_config.type == "exceptLast" and condition == True:
227 # in ExceptLast properties, you don't enable all of them, you enable
228 # all of them except the last one
229 cutoff, property_values = self.get_cutoff_and_property_values(config)
230 if property_values:
231 for i in range(len(property_values)):
232 if i <= cutoff:
233 # if the property is before the cutoff, it should be enabled
234 property_values[i].enabled = condition
235 else:
236 property_values[i].enabled = False
237 list_prop_val.append(property_values[i])
238 else:
239 # this must be a custom property or a machine learning property.
240 values = list(self.values.all())
241 for value in values:
242 value.enabled = condition
243 list_prop_val.append(value)
244 return list_prop_val
246 def is_recycle_var(self) -> bool:
247 return hasattr(self, "recycleConnection")
249 def add_control(self, prop: "PropertyValue") -> ControlValue:
250 return ControlValue.create(setPoint=self.values.first(), manipulated=prop.values.first(), flowsheet=self.flowsheet)
252 def unit_conversion(self, new_unit: str) -> None:
253 """
254 Perform a unit conversion on the value field of the PropertyInfo instance
255 """
257 # update both the unit and the value
258 original_unit = self.unit
259 self.unit = new_unit
260 # original_value = self.get_value()
261 original_value = self.values.all()
262 for value in list(original_value):
263 # we only want to update the units of properties that can't be manually edited.
264 # this is a design choice, so users don't enter 100, switch from Pa to kPa, and it gets converted to 0.1 kPa
265 if not value.is_enabled():
266 if value.value != None: 266 ↛ 262line 266 didn't jump to line 262 because the condition on line 266 was always true
267 float_value = float(value.value)
268 new_value = convert_value(float_value, original_unit, new_unit)
269 value.value = new_value
270 value.save()
272 def get_schema(self) -> PropertyType | None:
273 #get the type of the unit operation
274 if self.set.simulationObject == None:
275 return None
276 else:
277 return self.set.simulationObject.schema.properties.get(self.key, None)
279 def is_custom_property(self) -> bool:
280 return self.get_schema() is None
282def check_is_except_last(property_info: PropertyInfo) -> bool:
283 """
284 Check if the property is of type "exceptLast"
285 """
286 object_type = property_info.set.simulationObject.objectType
287 config = configuration.get(object_type)
288 for schema in config.propertySetGroups.values():
289 if schema.type == "exceptLast":
290 return True
291 return False