Coverage for backend/django/core/auxiliary/models/PropertyValue.py: 91%
87 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-26 20:57 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-26 20:57 +0000
1from django.db import models
2from core.auxiliary.models.IndexedItem import IndexedItem
3from core.managers import AccessControlManager
4from core.auxiliary.models.ControlValue import ControlValue
5from typing import TYPE_CHECKING
6if TYPE_CHECKING:
7 from core.auxiliary.models.PropertyInfo import PropertyInfo
8 from core.auxiliary.models.PropertySet import PropertySet
9 from core.auxiliary.models.ControlValue import ControlValue
11class PropertyValue(models.Model):
12 flowsheet = models.ForeignKey("Flowsheet", on_delete=models.CASCADE, related_name="propertyValues")
13 value = models.JSONField(null=True, blank=True)
14 displayValue = models.JSONField(null=True, blank=True)
15 enabled = models.BooleanField(default=True)
16 formula = models.CharField(max_length=2048, null=True, blank=True, default=None)
17 property: "PropertyInfo" = models.ForeignKey("PropertyInfo", on_delete=models.CASCADE, related_name="values", null=True)
18 indexedItems = models.ManyToManyField(IndexedItem, related_name="propertyValues")
19 created_at = models.DateTimeField(auto_now_add=True)
21 objects = AccessControlManager()
22 controlManipulated: ControlValue | None
23 controlSetPoint: ControlValue | None
24 # Bulk update doesn't work with access control manager so we
25 # sometimes need to just use the normal manager.
26 _unsafe_objects = models.Manager()
28 class Meta:
29 ordering = ['created_at']
31 def enable(self, state = True) -> None:
32 self.enabled = state
33 self.save()
35 def get_index(self, index_set: str) -> list[IndexedItem]:
36 """
37 Get the indexed item of the specified type for this property.
38 """
39 return self.indexedItems.get(type=index_set)
41 def has_value(self) -> bool:
42 return self.value not in [None, ""]
44 def get_indexed_items(self) -> list[IndexedItem]:
45 """
46 Gets the list of indexed items for this property value, in order
47 Note that this is kinda expensive if you're doing it for everything,
48 as it looks up the unit op type every time.
49 But it's helpful for testing.
50 """
51 if self.property.is_custom_property():
52 return [] # no special indexes on custom properties as they don't exist in the schema
53 indexes = list(self.indexedItems.all())
54 property_key = self.property.key
55 properties = self.property.set.simulationObject.schema.properties.get(property_key, None)
56 index_set_order = self.property.set.simulationObject.schema.properties[property_key].indexSets
57 return sort_indexes(index_set_order, indexes)
59 def get_indexes(self) -> list[str]:
60 """
61 Gets the list of indexes for this property value, in order. e.g "outlet_1
62, Note that this is kinda expensive if you're doing it for everything,
63 as it looks up the unit op type every time.
64 But it's helpful for testing.
65 """
66 return [index.key for index in self.get_indexed_items()]
68 def get_simulation_object(self):
69 return self.property.set.simulationObject
71 def get_index_names(self) -> list[str]:
72 return [index.displayName for index in self.get_indexed_items()]
74 def is_control_set_point(self) -> bool:
75 return hasattr(self, "controlSetPoint")
77 def is_control_manipulated(self) -> bool:
78 return hasattr(self, "controlManipulated")
80 def is_externally_controlled(self) -> bool:
81 """
82 Returns true if the property is controlled or controlling an external property
83 """
84 if not self.is_control_manipulated() and not self.is_control_set_point():
85 return False
86 if self.is_control_manipulated():
87 # set point is from a different property set
88 return self.controlManipulated.setPoint.property.set != self.property.set
89 if self.is_control_set_point(): 89 ↛ 92line 89 didn't jump to line 92 because the condition on line 89 was always true
90 # manipulated property is from a different property set
91 return self.controlSetPoint.manipulated.property.set != self.property.set
92 return False
94 def is_enabled(self) -> bool:
95 """
96 Returns true if the property is to be enabled in simulation: either a statevar or a controlled property
97 """
98 return (self.enabled and not self.is_control_manipulated() ) or self.is_control_set_point()
101 def add_control(self, prop: "PropertyValue") -> ControlValue:
102 # This property is the set point controlling the manipulated property
103 return ControlValue.create(setPoint=self, manipulated=prop, flowsheet=prop.flowsheet)
106 def auto_replace(self):
107 """
108 Automatically create control relationships as autocomplete option:
109 e.g.
110 - Mass Flow -> Molar Flow
111 - Vapour Fraction -> Temperature
113 Logic:
114 - Non-enabled property (self) becomes controlSetPoint
115 - Enabled property becomes manipulated
116 - No properties are enabled/disabled here
117 """
118 property_info: PropertyInfo = self.property
119 property_set: PropertySet = property_info.set
120 simulation_object = property_set.simulationObject
122 control_mapping = {}
124 if simulation_object.objectType == "stream": 124 ↛ 141line 124 didn't jump to line 141 because the condition on line 124 was always true
125 control_mapping.update({
126 "flow_mass": "flow_mol",
127 "vapor_frac": "temperature",
128 "enth_mol": "temperature",
129 "enth_mass": "temperature",
130 "entr_mol": "pressure",
131 "entr_mass": "pressure",
132 # "total_energy_flow": "",
133 # "flow_vol": "",
134 # "mole_frac_phase_comp": "",
135 # "efficiency_isentropic": "", // are unitops still streams??
136 # "deltaP": "work_mechanical",
137 # "ratioP": "",
138 # "work_mechanical": "",
139 })
141 setpoint_key = property_info.key
142 manipulated_key = control_mapping.get(setpoint_key)
143 if not manipulated_key:
144 return
146 # find enabled property to manipulate
147 manipulated_pvs = [
148 pv for prop_info in property_set.containedProperties.all()
149 if prop_info.key == manipulated_key
150 for pv in prop_info.values.all()
151 if pv.enabled # only check the actual enabled flag
152 ]
154 if not manipulated_pvs: 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true
155 return
157 manipulated_pv = manipulated_pvs[0]
159 if ControlValue.objects.filter(manipulated=manipulated_pv).exists(): 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true
160 return
162 # create the control relationship
163 ControlValue.create(
164 manipulated=manipulated_pv,
165 setPoint=self,
166 flowsheet=self.flowsheet
167 )
169PropertyValueIntermediate = PropertyValue.indexedItems.through
172def sort_indexes(index_set_order : list[str], indexes : list[IndexedItem]) -> list[IndexedItem]:
173 """
174 Sorts the list of indexed items based on the order defined in the config file.
175 index_set_order: list[str], a list of index set types, the order in which the indexes should be sorted. e.g ["splitter_fraction","compound","phase"]
176 indexes: list[IndexedItem], a list of indexed items to be sorted. e.g a list of indexed items attached to a PropertyValue
177 """
178 item = [index.type for index in indexes]
179 reordered_indexes = [None] * len(indexes)
181 for i in range(len(item)):
182 if item[i] != index_set_order[i]:
183 # Find the correct position for the current item in object_properties
184 correct_position = index_set_order.index(item[i])
185 # Place the current index in the correct position
186 reordered_indexes[correct_position] = indexes[i]
187 else:
188 # If the item is already in the correct position, keep it as is
189 reordered_indexes[i] = indexes[i]
191 # Update the original indexes list with the reordered indexes
192 for i in range(len(indexes)):
193 indexes[i] = reordered_indexes[i]
194 return indexes