Coverage for backend/django/core/auxiliary/models/PropertyValue.py: 93%

89 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-22 05:22 +0000

1from django.db import models 

2from core.auxiliary.models.IndexedItem import IndexedItem 

3from core.managers import AccessControlManager, AllFlowsheetStatesManager 

4from core.auxiliary.models.ControlValue import ControlValue 

5from core.auxiliary.models.FlowsheetHistoryModel import FlowsheetHistoryModel 

6from typing import TYPE_CHECKING 

7 

8if TYPE_CHECKING: 

9 from core.auxiliary.models.PropertyInfo import PropertyInfo 

10 from core.auxiliary.models.PropertySet import PropertySet 

11 from core.auxiliary.models.ControlValue import ControlValue 

12 

13 

14class PropertyValue(FlowsheetHistoryModel, models.Model): 

15 flowsheet_state = models.ForeignKey( 

16 "FlowsheetState", on_delete=models.CASCADE, related_name="propertyValues" 

17 ) 

18 value = models.JSONField(null=True, blank=True) 

19 displayValue = models.JSONField(null=True, blank=True) 

20 enabled = models.BooleanField(default=True) 

21 formula = models.CharField(max_length=2048, null=True, blank=True, default=None) 

22 property: models.ForeignKey["PropertyInfo"] = models.ForeignKey( 

23 "PropertyInfo", on_delete=models.CASCADE, related_name="values", null=True 

24 ) 

25 indexedItems = models.ManyToManyField(IndexedItem, related_name="propertyValues") 

26 created_at = models.DateTimeField(auto_now_add=True) 

27 tag = models.CharField( 

28 max_length=255, null=True, blank=True, default=None 

29 ) # tag can be used to sync the digital model with the rest of the sytem. 

30 objects = AccessControlManager() 

31 all_states = AllFlowsheetStatesManager() 

32 controlManipulated: ControlValue | None 

33 controlSetPoint: ControlValue | None 

34 # Bulk update doesn't work with access control manager so we 

35 # sometimes need to just use the normal manager. 

36 _unsafe_objects = models.Manager() 

37 

38 class Meta: 

39 ordering = ["created_at"] 

40 

41 def enable(self, state=True) -> None: 

42 self.enabled = state 

43 self.save() 

44 

45 def get_index(self, index_set: str) -> list[IndexedItem]: 

46 """ 

47 Get the indexed item of the specified type for this property. 

48 """ 

49 return self.indexedItems.get(type=index_set) 

50 

51 def has_value(self) -> bool: 

52 return self.value not in [None, ""] 

53 

54 def get_indexed_items(self) -> list[IndexedItem]: 

55 """ 

56 Gets the list of indexed items for this property value, in order 

57 Note that this is kinda expensive if you're doing it for everything, 

58 as it looks up the unit op type every time. 

59 But it's helpful for testing. 

60 """ 

61 if self.property.is_custom_property(): 

62 return [] # no special indexes on custom properties as they don't exist in the schema 

63 indexes = list(self.indexedItems.all()) 

64 property_key = self.property.key 

65 index_set_order = self.property.set.simulationObject.schema.properties[ 

66 property_key 

67 ].indexSets 

68 return sort_indexes(index_set_order, indexes) 

69 

70 def get_indexes(self) -> list[str]: 

71 """ 

72 Gets the list of indexes for this property value, in order. e.g "outlet_1 

73 , Note that this is kinda expensive if you're doing it for everything, 

74 as it looks up the unit op type every time. 

75 But it's helpful for testing. 

76 """ 

77 return [index.key for index in self.get_indexed_items()] 

78 

79 def get_simulation_object(self): 

80 return self.property.set.simulationObject 

81 

82 def get_index_names(self) -> list[str]: 

83 return [index.displayName for index in self.get_indexed_items()] 

84 

85 def is_control_set_point(self) -> bool: 

86 return hasattr(self, "controlSetPoint") 

87 

88 def is_control_manipulated(self) -> bool: 

89 return hasattr(self, "controlManipulated") 

90 

91 def is_externally_controlled(self) -> bool: 

92 """ 

93 Returns true if the property is controlled or controlling an external property 

94 """ 

95 if not self.is_control_manipulated() and not self.is_control_set_point(): 

96 return False 

97 if self.is_control_manipulated(): 97 ↛ 99line 97 didn't jump to line 99 because the condition on line 97 was never true

98 # set point is from a different property set 

99 return self.controlManipulated.setPoint.property.set != self.property.set 

100 if self.is_control_set_point(): 100 ↛ 103line 100 didn't jump to line 103 because the condition on line 100 was always true

101 # manipulated property is from a different property set 

102 return self.controlSetPoint.manipulated.property.set != self.property.set 

103 return False 

104 

105 def is_enabled(self) -> bool: 

106 """ 

107 Returns true if the property is to be enabled in simulation: either a statevar or a controlled property 

108 """ 

109 return ( 

110 self.enabled and not self.is_control_manipulated() 

111 ) or self.is_control_set_point() 

112 

113 def add_control(self, prop: "PropertyValue") -> ControlValue: 

114 # This property is the set point controlling the manipulated property 

115 return ControlValue.create( 

116 setPoint=self, 

117 manipulated=prop, 

118 flowsheet_state=prop.flowsheet_state, 

119 ) 

120 

121 def auto_replace(self): 

122 """ 

123 Automatically create control relationships as autocomplete option: 

124 e.g. 

125 - Mass Flow -> Molar Flow 

126 - Vapour Fraction -> Temperature 

127 

128 Logic: 

129 - Non-enabled property (self) becomes controlSetPoint 

130 - Enabled property becomes manipulated 

131 - No properties are enabled/disabled here 

132 """ 

133 property_info: PropertyInfo = self.property 

134 property_set: PropertySet = property_info.set 

135 simulation_object = property_set.simulationObject 

136 

137 control_mapping = {} 

138 

139 if simulation_object.objectType == "stream": 139 ↛ 158line 139 didn't jump to line 158 because the condition on line 139 was always true

140 control_mapping.update( 

141 { 

142 "flow_mass": "flow_mol", 

143 "vapor_frac": "temperature", 

144 "enth_mol": "temperature", 

145 "enth_mass": "temperature", 

146 "entr_mol": "pressure", 

147 "entr_mass": "pressure", 

148 # "total_energy_flow": "", 

149 # "flow_vol": "", 

150 # "mole_frac_phase_comp": "", 

151 # "efficiency_isentropic": "", // are unitops still streams?? 

152 # "deltaP": "work_mechanical", 

153 # "ratioP": "", 

154 # "work_mechanical": "", 

155 } 

156 ) 

157 

158 setpoint_key = property_info.key 

159 manipulated_key = control_mapping.get(setpoint_key) 

160 if not manipulated_key: 

161 return 

162 

163 # find enabled property to manipulate 

164 manipulated_pvs = [ 

165 pv 

166 for prop_info in property_set.containedProperties.all() 

167 if prop_info.key == manipulated_key 

168 for pv in prop_info.values.all() 

169 if pv.enabled # only check the actual enabled flag 

170 ] 

171 

172 if not manipulated_pvs: 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true

173 return 

174 

175 manipulated_pv = manipulated_pvs[0] 

176 

177 if ControlValue.objects.filter(manipulated=manipulated_pv).exists(): 

178 return 

179 

180 # create the control relationship 

181 ControlValue.create( 

182 manipulated=manipulated_pv, 

183 setPoint=self, 

184 flowsheet_state=self.flowsheet_state, 

185 ) 

186 

187 

188PropertyValueIntermediate = PropertyValue.indexedItems.through 

189 

190 

191def sort_indexes( 

192 index_set_order: list[str], indexes: list[IndexedItem] 

193) -> list[IndexedItem]: 

194 """ 

195 Sorts the list of indexed items based on the order defined in the config file. 

196 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"] 

197 indexes: list[IndexedItem], a list of indexed items to be sorted. e.g a list of indexed items attached to a PropertyValue 

198 """ 

199 item = [index.type for index in indexes] 

200 reordered_indexes = [None] * len(indexes) 

201 

202 for i in range(len(item)): 

203 if item[i] != index_set_order[i]: 

204 # Find the correct position for the current item in object_properties 

205 correct_position = index_set_order.index(item[i]) 

206 # Place the current index in the correct position 

207 reordered_indexes[correct_position] = indexes[i] 

208 else: 

209 # If the item is already in the correct position, keep it as is 

210 reordered_indexes[i] = indexes[i] 

211 

212 # Update the original indexes list with the reordered indexes 

213 for i in range(len(indexes)): 

214 indexes[i] = reordered_indexes[i] 

215 return indexes