Coverage for backend/core/auxiliary/models/PropertyInfo.py: 88%
157 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +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
17class HistoricalValue(models.Model):
18 flowsheet = models.ForeignKey("Flowsheet", on_delete=models.CASCADE, related_name="HistoricalValues")
19 value = models.FloatField()
20 property = models.ForeignKey("PropertyInfo", on_delete=models.CASCADE, related_name="history", null=True)
22 created_at = models.DateTimeField(auto_now_add=True)
23 objects = AccessControlManager()
27 class Meta:
28 ordering = ['created_at']
31class ProcessPathProperty(models.Model):
32 flowsheet = models.ForeignKey("Flowsheet", on_delete=models.CASCADE, related_name="ProcessPathProperties")
33 value = models.FloatField(null=True, blank=True)
34 property = models.ForeignKey("PropertyInfo", on_delete=models.CASCADE, related_name="ProcessPathProperties", null=True)
35 path = models.ForeignKey("ProcessPath", on_delete=models.CASCADE, related_name="ProcessPathProperties", null=True)
37 created_at = models.DateTimeField(auto_now_add=True)
38 objects = AccessControlManager()
41 class Meta:
42 ordering = ['created_at']
45class PropertyInfo(models.Model):
46 flowsheet = models.ForeignKey("Flowsheet", on_delete=models.CASCADE, related_name="propertyInfos")
47 set = models.ForeignKey("PropertySet", on_delete=models.CASCADE, related_name="ContainedProperties", null=True)
48 type = models.CharField(choices=DisplayType.choices , default=DisplayType.numeric)
49 unitType = models.CharField(choices=UnitOfMeasure.choices , default=UnitOfMeasure.none)
50 unit = models.CharField(max_length=32, blank=True) # selected unit
51 expression = models.OneToOneField("Expression", on_delete=models.SET_NULL, related_name="property", null=True) # this should probably be refactored to a one-to-one relationship, and could be moved to the expression model.
52 key = models.CharField(max_length=64)
53 displayName = models.CharField(max_length=64)
54 index = models.IntegerField(default=0)
55 created_at = models.DateTimeField(auto_now_add=True)
57 objects = AccessControlManager()
59 class Meta:
60 ordering = ['created_at']
62 @classmethod
63 def create(cls, indexes:dict[str,list[IndexedItem]]={}, **fields) -> tuple["PropertyInfo", list[PropertyValue]]:
65 value = fields.pop("value")
66 property_info = PropertyInfo(**fields)
67 # Create a property value object with this value
68 if indexes == {}: 68 ↛ 71line 68 didn't jump to line 71 because the condition on line 68 was always true
69 property_value = PropertyValue(value=value, property=property_info, flowsheet=fields.get("flowsheet"))
71 return property_info, [property_value]
74 @classmethod
75 def create_save(cls, **fields) -> "PropertyInfo":
76 # Create and save a new property info object
77 property_info, property_values = cls.create(**fields)
78 property_info.save()
79 for property_value in property_values:
80 property_value.save()
81 return property_info
84 def get_value_bulk(self, indexes: list | None = None) -> Any:
85 """
86 Get the value of the property at the specified indexes,
87 or the first value if no indexes are specified.
89 This method should be used in bulk operations (where
90 all the related property values are prefetched).
91 """
92 property_value = get_value_object(self, indexes)
93 if property_value is None:
94 raise ValueError(f"No property values found with indexes={indexes}")
95 return property_value.value
98 def set_value_bulk(self, value: Any, indexes: list | None = None) -> PropertyValue:
99 """
100 Set the value of the property at the given indexes,
101 or the first value if no indexes are specified.
103 This method should be used in bulk operations (where
104 all the related property values are prefetched).
105 """
106 property_value = get_value_object(self, indexes)
107 property_value.value = value
108 return property_value
111 def get_value_object(self, indexes: list[str] | None = None) -> PropertyValue:
112 """
113 Get the property value object at the specified indexes,
114 or the first value if no indexes are specified.
116 This method should not be used in bulk operations.
117 """
118 if indexes is None:
119 property_value = self.values.first()
120 else:
121 annotated_values = self.values.annotate(
122 total_indexes=Count("indexedItems"),
123 matching_indexes=Count("indexedItems", filter=Q(indexedItems__key__in=indexes))
124 )
125 property_value = annotated_values.get(total_indexes=len(indexes), matching_indexes=len(indexes))
126 return property_value
129 def get_value(self, indexes: list | None = None) -> Any:
130 """
131 Get the value of the property at the specified indexes,
132 or the first value if no indexes are specified.
134 This method should not be used in bulk operations.
135 """
136 value_object = self.get_value_object(indexes=indexes)
137 if value_object is None: 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true
138 return None
139 return value_object.value
142 def set_value(self, value: Any, indexes=None) -> None:
143 """
144 Set the value of the property at the given indexes,
145 or the first value if no indexes are specified.
147 This method should not be used in bulk operations.
148 """
149 property_value = self.get_value_object(indexes=indexes)
150 property_value.value = value
151 property_value.save()
154 def get_indexes(self, index_set: str) -> list[IndexedItem]:
155 """
156 Get all the indexed items of the specified type for this property.
157 """
158 property_values = self.values.all()
159 return IndexedItem.objects.filter(propertyValues__in=property_values, type=index_set)
161 def has_value(self) -> bool:
162 property_value = self.get_value_object()
163 if property_value is None: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 return False
165 return property_value.has_value()
167 def has_value_bulk(self) -> bool:
168 property_value = get_value_object(self)
169 if property_value is None:
170 return False
171 return property_value.has_value()
173 def get_cutoff_and_property_values(self, config: ObjectType):
174 first_index = config.indexSets[0] # get the outermost level e.g: "outlet 1, outlet 2"
175 first_indexed_items = IndexedItem.objects.filter(owner=self.set.simulationObject, type=first_index)
177 property_values = []
178 for indexed_item in first_indexed_items:
179 property_values.extend(indexed_item.propertyValues.all())
181 count = first_indexed_items.count()
183 if not count: 183 ↛ 184line 183 didn't jump to line 184 because the condition on line 183 was never true
184 return None, None
186 cutoff = len(property_values) - 1 - (len(property_values) / count) # get the index where it starts to be disabled
188 return cutoff, property_values
191 def isSpecified(self) -> bool:
192 value: PropertyValue
193 for value in self.values.all():
194 if (
195 (
196 (value.enabled and not value.is_control_manipulated() )
197 or value.is_control_set_point()
198 or self.is_recycle_var()
199 )
200 and not value.has_value()
201 ):
202 return False
203 return True
206 def enable(self, condition: bool = True) -> list[PropertyValue]:
207 """Enables or disables property values based on configuration rules."""
208 object_type = self.set.simulationObject.objectType
209 config = configuration.get(object_type)
211 list_prop_val = []
213 if self.key not in config.properties: 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true
214 propertySetGroup_key = None; # default to none, we are assuming the property doesn't exist.
215 else:
216 propertySetGroup_key = config.properties[self.key].propertySetGroup
217 propertySetGroup_config = config.propertySetGroups.get(propertySetGroup_key, None)
219 if propertySetGroup_config is not None and propertySetGroup_config.type == "exceptLast" and condition == True:
220 # in ExceptLast properties, you don't enable all of them, you enable
221 # all of them except the last one
222 cutoff, property_values = self.get_cutoff_and_property_values(config)
223 if property_values:
224 for i in range(len(property_values)):
225 if i <= cutoff:
226 # if the property is before the cutoff, it should be enabled
227 property_values[i].enabled = condition
228 else:
229 property_values[i].enabled = False
230 list_prop_val.append(property_values[i])
231 else:
232 # this must be a custom property or a machine learning property.
233 values = list(self.values.all())
234 for value in values:
235 value.enabled = condition
236 list_prop_val.append(value)
237 return list_prop_val
239 def is_recycle_var(self) -> bool:
240 return hasattr(self, "recycleConnection")
242 def add_control(self, prop: "PropertyValue") -> ControlValue:
243 return ControlValue.create(setPoint=self.values.first(), manipulated=prop.values.first(), flowsheet=self.flowsheet)
245 def unit_conversion(self, new_unit: str) -> None:
246 """
247 Perform a unit conversion on the value field of the PropertyInfo instance
248 """
250 # update both the unit and the value
251 original_unit = self.unit
252 self.unit = new_unit
253 # original_value = self.get_value()
254 original_value = self.values.all()
255 for value in list(original_value):
256 # we only want to update the units of properties that can't be manually edited.
257 # this is a design choice, so users don't enter 100, switch from Pa to kPa, and it gets converted to 0.1 kPa
258 if not value.is_enabled():
259 if value.value != None: 259 ↛ 255line 259 didn't jump to line 255 because the condition on line 259 was always true
260 float_value = float(value.value)
261 new_value = convert_value(float_value, original_unit, new_unit)
262 value.value = new_value
263 value.save()
265 def get_schema(self) -> PropertyType | None:
266 #get the type of the unit operation
267 if self.set.simulationObject == None: 267 ↛ 268line 267 didn't jump to line 268 because the condition on line 267 was never true
268 return None
269 else:
270 return self.set.simulationObject.schema.properties.get(self.key, None)
272def check_is_except_last(property_info: PropertyInfo) -> bool:
273 """
274 Check if the property is of type "exceptLast"
275 """
276 object_type = property_info.set.simulationObject.objectType
277 config = configuration.get(object_type)
278 for schema in config.propertySetGroups.values(): 278 ↛ 281line 278 didn't jump to line 281 because the loop on line 278 didn't complete
279 if schema.type == "exceptLast": 279 ↛ 278line 279 didn't jump to line 278 because the condition on line 279 was always true
280 return True
281 return False