Coverage for backend/django/core/auxiliary/models/PropertyInfo.py: 88%
161 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-12-18 04:00 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-12-18 04:00 +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 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.
54 key = models.CharField(max_length=64)
55 displayName = models.CharField(max_length=64)
56 index = models.IntegerField(default=0)
57 created_at = models.DateTimeField(auto_now_add=True)
58 values: models.QuerySet["PropertyValue"]
60 objects = AccessControlManager()
62 class Meta:
63 ordering = ['created_at']
65 @classmethod
66 def create(cls, indexes:dict[str,list[IndexedItem]]={}, **fields) -> tuple["PropertyInfo", list[PropertyValue]]:
68 value = fields.pop("value")
69 property_info = PropertyInfo(**fields)
70 # Create a property value object with this value
71 if indexes == {}: 71 ↛ 74line 71 didn't jump to line 74 because the condition on line 71 was always true
72 property_value = PropertyValue(value=value, property=property_info, flowsheet=fields.get("flowsheet"))
74 return property_info, [property_value]
77 @classmethod
78 def create_save(cls, **fields) -> "PropertyInfo":
79 # Create and save a new property info object
80 property_info, property_values = cls.create(**fields)
81 property_info.save()
82 for property_value in property_values:
83 property_value.save()
84 return property_info
87 def get_value_bulk(self, indexes: list | None = None) -> Any:
88 """
89 Get the value of the property at the specified indexes,
90 or the first value if no indexes are specified.
92 This method should be used in bulk operations (where
93 all the related property values are prefetched).
94 """
95 property_value = get_value_object(self, indexes)
96 if property_value is None:
97 raise ValueError(f"No property values found with indexes={indexes}")
98 return property_value.value
101 def set_value_bulk(self, value: Any, indexes: list | None = None) -> PropertyValue:
102 """
103 Set the value of the property at the given indexes,
104 or the first value if no indexes are specified.
106 This method should be used in bulk operations (where
107 all the related property values are prefetched).
108 """
109 property_value = get_value_object(self, indexes)
110 property_value.value = value
111 return property_value
114 def get_value_object(self, indexes: list[str] | None = None) -> PropertyValue:
115 """
116 Get the property value object at the specified indexes,
117 or the first value if no indexes are specified.
119 This method should not be used in bulk operations.
120 """
121 if indexes is None:
122 property_value = self.values.first()
123 else:
124 annotated_values = self.values.annotate(
125 total_indexes=Count("indexedItems"),
126 matching_indexes=Count("indexedItems", filter=Q(indexedItems__key__in=indexes))
127 )
128 property_value = annotated_values.get(total_indexes=len(indexes), matching_indexes=len(indexes))
129 return property_value
132 def get_value(self, indexes: list | None = None) -> Any:
133 """
134 Get the value of the property at the specified indexes,
135 or the first value if no indexes are specified.
137 This method should not be used in bulk operations.
138 """
139 value_object = self.get_value_object(indexes=indexes)
140 if value_object is None: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 return None
142 return value_object.value
145 def set_value(self, value: Any, indexes=None) -> None:
146 """
147 Set the value of the property at the given indexes,
148 or the first value if no indexes are specified.
150 This method should not be used in bulk operations.
151 """
152 property_value = self.get_value_object(indexes=indexes)
153 property_value.value = value
154 property_value.save()
157 def get_indexes(self, index_set: str) -> list[IndexedItem]:
158 """
159 Get all the indexed items of the specified type for this property.
160 """
161 property_values = self.values.all()
162 return IndexedItem.objects.filter(propertyValues__in=property_values, type=index_set)
164 def has_value(self) -> bool:
165 property_value = self.get_value_object()
166 if property_value is None: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 return False
168 return property_value.has_value()
170 def has_value_bulk(self) -> bool:
171 property_value = get_value_object(self)
172 if property_value is None:
173 return False
174 return property_value.has_value()
176 def get_cutoff_and_property_values(self, config: ObjectType):
177 first_index = config.indexSets[0] # get the outermost level e.g: "outlet 1, outlet 2"
178 first_indexed_items = IndexedItem.objects.filter(owner=self.set.simulationObject, type=first_index)
180 property_values = []
181 for indexed_item in first_indexed_items:
182 property_values.extend(indexed_item.propertyValues.all())
184 count = first_indexed_items.count()
186 if not count: 186 ↛ 187line 186 didn't jump to line 187 because the condition on line 186 was never true
187 return None, None
189 cutoff = len(property_values) - 1 - (len(property_values) / count) # get the index where it starts to be disabled
191 return cutoff, property_values
194 def isSpecified(self) -> bool:
195 """
196 Checks if all the required property values have been specified.
197 This includes values that are enabled, control set points, and recycle guesses.
198 Both guesses and set points are needed.
199 """
200 value: PropertyValue
201 for value in self.values.all():
202 if (
203 (
204 value.enabled
205 or value.is_control_set_point()
206 or self.is_recycle_var()
207 )
208 and not value.has_value()
209 ):
210 return False
211 return True
214 def enable(self, condition: bool = True) -> list[PropertyValue]:
215 """Enables or disables property values based on configuration rules."""
216 object_type = self.set.simulationObject.objectType
217 config = configuration.get(object_type)
219 list_prop_val = []
221 if self.key not in config.properties: 221 ↛ 222line 221 didn't jump to line 222 because the condition on line 221 was never true
222 propertySetGroup_key = None; # default to none, we are assuming the property doesn't exist.
223 else:
224 propertySetGroup_key = config.properties[self.key].propertySetGroup
225 propertySetGroup_config = config.propertySetGroups.get(propertySetGroup_key, None)
227 if propertySetGroup_config is not None and propertySetGroup_config.type == "exceptLast" and condition == True:
228 # in ExceptLast properties, you don't enable all of them, you enable
229 # all of them except the last one
230 cutoff, property_values = self.get_cutoff_and_property_values(config)
231 if property_values:
232 for i in range(len(property_values)):
233 if i <= cutoff:
234 # if the property is before the cutoff, it should be enabled
235 property_values[i].enabled = condition
236 else:
237 property_values[i].enabled = False
238 list_prop_val.append(property_values[i])
239 else:
240 # this must be a custom property or a machine learning property.
241 values = list(self.values.all())
242 for value in values:
243 value.enabled = condition
244 list_prop_val.append(value)
245 return list_prop_val
247 def is_recycle_var(self) -> bool:
248 return hasattr(self, "recycleConnection")
250 def add_control(self, prop: "PropertyValue") -> ControlValue:
251 return ControlValue.create(setPoint=self.values.first(), manipulated=prop.values.first(), flowsheet=self.flowsheet)
253 def unit_conversion(self, new_unit: str) -> None:
254 """
255 Perform a unit conversion on the value field of the PropertyInfo instance
256 """
258 # update both the unit and the value
259 original_unit = self.unit
260 self.unit = new_unit
261 # original_value = self.get_value()
262 original_value = self.values.all()
263 for value in list(original_value):
264 # we only want to update the units of properties that can't be manually edited.
265 # this is a design choice, so users don't enter 100, switch from Pa to kPa, and it gets converted to 0.1 kPa
266 if not value.is_enabled():
267 if value.value != None: 267 ↛ 263line 267 didn't jump to line 263 because the condition on line 267 was always true
268 float_value = float(value.value)
269 new_value = convert_value(float_value, original_unit, new_unit)
270 value.value = new_value
271 value.save()
273 def get_schema(self) -> PropertyType | None:
274 #get the type of the unit operation
275 if self.set.simulationObject == None: 275 ↛ 276line 275 didn't jump to line 276 because the condition on line 275 was never true
276 return None
277 else:
278 return self.set.simulationObject.schema.properties.get(self.key, None)
280 def is_custom_property(self) -> bool:
281 return self.get_schema() is None
283def check_is_except_last(property_info: PropertyInfo) -> bool:
284 """
285 Check if the property is of type "exceptLast"
286 """
287 object_type = property_info.set.simulationObject.objectType
288 config = configuration.get(object_type)
289 for schema in config.propertySetGroups.values(): 289 ↛ 292line 289 didn't jump to line 292 because the loop on line 289 didn't complete
290 if schema.type == "exceptLast": 290 ↛ 289line 290 didn't jump to line 289 because the condition on line 290 was always true
291 return True
292 return False