Coverage for backend/django/core/auxiliary/models/PropertyInfo.py: 96%

173 statements  

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

1from django.db import models 

2from django.db.models import Q, Count 

3 

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 

12 

13from core.managers import AccessControlManager, AllFlowsheetStatesManager 

14from core.auxiliary.models.PropertyValue import PropertyValue 

15from core.auxiliary.models.FlowsheetHistoryModel import FlowsheetHistoryModel 

16from typing import TYPE_CHECKING 

17 

18if TYPE_CHECKING: 

19 from core.auxiliary.models.PropertyValue import PropertyValue 

20 from core.auxiliary.models.RecycleData import RecycleProperty 

21 from core.auxiliary.models.PropertySet import PropertySet 

22 

23 

24class HistoricalValue(models.Model): 

25 flowsheet_state = models.ForeignKey( 

26 "FlowsheetState", on_delete=models.CASCADE, related_name="HistoricalValues" 

27 ) 

28 value = models.FloatField() 

29 property = models.ForeignKey( 

30 "PropertyInfo", on_delete=models.CASCADE, related_name="history", null=True 

31 ) 

32 

33 created_at = models.DateTimeField(auto_now_add=True) 

34 objects = AccessControlManager() 

35 all_states = AllFlowsheetStatesManager() 

36 

37 class Meta: 

38 ordering = ["created_at"] 

39 

40 

41class ProcessPathProperty(models.Model): 

42 flowsheet_state = models.ForeignKey( 

43 "FlowsheetState", on_delete=models.CASCADE, related_name="ProcessPathProperties" 

44 ) 

45 value = models.FloatField(null=True, blank=True) 

46 property = models.ForeignKey( 

47 "PropertyInfo", 

48 on_delete=models.CASCADE, 

49 related_name="ProcessPathProperties", 

50 null=True, 

51 ) 

52 path = models.ForeignKey( 

53 "ProcessPath", 

54 on_delete=models.CASCADE, 

55 related_name="ProcessPathProperties", 

56 null=True, 

57 ) 

58 

59 created_at = models.DateTimeField(auto_now_add=True) 

60 objects = AccessControlManager() 

61 all_states = AllFlowsheetStatesManager() 

62 

63 class Meta: 

64 ordering = ["created_at"] 

65 

66 

67class PropertyInfo(FlowsheetHistoryModel, models.Model): 

68 flowsheet_state = models.ForeignKey( 

69 "FlowsheetState", on_delete=models.CASCADE, related_name="propertyInfos" 

70 ) 

71 set = models.ForeignKey( 

72 "PropertySet", 

73 on_delete=models.CASCADE, 

74 related_name="ContainedProperties", 

75 null=True, 

76 ) 

77 type = models.CharField(choices=DisplayType.choices, default=DisplayType.numeric) 

78 unitType = models.CharField( 

79 choices=UnitOfMeasure.choices, default=UnitOfMeasure.none 

80 ) 

81 unit = models.CharField(max_length=32, blank=True) # selected unit 

82 key = models.CharField(max_length=64) 

83 displayName = models.CharField(max_length=64) 

84 index = models.IntegerField(default=0) 

85 managed = models.BooleanField(default=False) 

86 managed_source = models.CharField(max_length=64, blank=True) 

87 can_edit = models.BooleanField(default=True) 

88 can_edit_formula = models.BooleanField(default=True) 

89 can_delete = models.BooleanField(default=True) 

90 formula_incomplete = models.BooleanField(default=False) 

91 formula_incomplete_reason = models.TextField(blank=True) 

92 created_at = models.DateTimeField(auto_now_add=True) 

93 values: models.QuerySet["PropertyValue"] 

94 recycleConnection: "RecycleProperty" 

95 set: "PropertySet" 

96 

97 objects = AccessControlManager() 

98 all_states = AllFlowsheetStatesManager() 

99 

100 class Meta: 

101 ordering = ["created_at"] 

102 

103 @classmethod 

104 def create( 

105 cls, indexes: dict[str, list[IndexedItem]] = {}, **fields 

106 ) -> tuple["PropertyInfo", list[PropertyValue]]: 

107 

108 value = fields.pop("value") 

109 property_info = PropertyInfo(**fields) 

110 # Create a property value object with this value 

111 if indexes == {}: 111 ↛ 118line 111 didn't jump to line 118 because the condition on line 111 was always true

112 property_value = PropertyValue( 

113 value=value, 

114 property=property_info, 

115 flowsheet_state=fields.get("flowsheet_state"), 

116 ) 

117 

118 return property_info, [property_value] 

119 

120 @classmethod 

121 def create_save(cls, **fields) -> "PropertyInfo": 

122 # Create and save a new property info object 

123 property_info, property_values = cls.create(**fields) 

124 property_info.save() 

125 for property_value in property_values: 

126 property_value.save() 

127 return property_info 

128 

129 def get_value_bulk(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. 

133 

134 This method should be used in bulk operations (where 

135 all the related property values are prefetched). 

136 """ 

137 property_value = get_value_object(self, indexes) 

138 if property_value is None: 

139 raise ValueError(f"No property values found with indexes={indexes}") 

140 return property_value.value 

141 

142 def set_value_bulk(self, value: Any, indexes: list | None = None) -> PropertyValue: 

143 """ 

144 Set the value of the property at the given indexes, 

145 or the first value if no indexes are specified. 

146 

147 This method should be used in bulk operations (where 

148 all the related property values are prefetched). 

149 """ 

150 property_value = get_value_object(self, indexes) 

151 property_value.value = value 

152 return property_value 

153 

154 def get_value_object(self, indexes: list[str] | None = None) -> PropertyValue: 

155 """ 

156 Get the property value object at the specified indexes, 

157 or the first value if no indexes are specified. 

158 

159 This method should not be used in bulk operations. 

160 """ 

161 if indexes is None: 

162 property_value = self.values.first() 

163 else: 

164 annotated_values = self.values.annotate( 

165 total_indexes=Count("indexedItems"), 

166 matching_indexes=Count( 

167 "indexedItems", filter=Q(indexedItems__key__in=indexes) 

168 ), 

169 ) 

170 property_value = annotated_values.get( 

171 total_indexes=len(indexes), matching_indexes=len(indexes) 

172 ) 

173 return property_value 

174 

175 def get_value(self, indexes: list | None = None) -> Any: 

176 """ 

177 Get the value of the property at the specified indexes, 

178 or the first value if no indexes are specified. 

179 

180 This method should not be used in bulk operations. 

181 """ 

182 value_object = self.get_value_object(indexes=indexes) 

183 if value_object is None: 

184 return None 

185 return value_object.value 

186 

187 def set_value(self, value: Any, indexes=None) -> None: 

188 """ 

189 Set the value of the property at the given indexes, 

190 or the first value if no indexes are specified. 

191 

192 This method should not be used in bulk operations. 

193 """ 

194 property_value = self.get_value_object(indexes=indexes) 

195 property_value.value = value 

196 property_value.save() 

197 

198 def get_indexes(self, index_set: str) -> list[IndexedItem]: 

199 """ 

200 Get all the indexed items of the specified type for this property. 

201 """ 

202 property_values = self.values.all() 

203 return IndexedItem.objects.filter( 

204 propertyValues__in=property_values, type=index_set 

205 ) 

206 

207 def has_value(self) -> bool: 

208 property_value = self.get_value_object() 

209 if property_value is None: 

210 return False 

211 return property_value.has_value() 

212 

213 def has_value_bulk(self) -> bool: 

214 property_value = get_value_object(self) 

215 if property_value is None: 

216 return False 

217 return property_value.has_value() 

218 

219 def get_cutoff_and_property_values(self, config: ObjectType): 

220 first_index = config.indexSets[ 

221 0 

222 ] # get the outermost level e.g: "outlet 1, outlet 2" 

223 first_indexed_items = IndexedItem.objects.filter( 

224 owner=self.set.simulationObject, type=first_index 

225 ) 

226 

227 property_values = [] 

228 for indexed_item in first_indexed_items: 

229 property_values.extend(indexed_item.propertyValues.all()) 

230 

231 count = first_indexed_items.count() 

232 

233 if not count: 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true

234 return None, None 

235 

236 cutoff = ( 

237 len(property_values) - 1 - (len(property_values) / count) 

238 ) # get the index where it starts to be disabled 

239 

240 return cutoff, property_values 

241 

242 def isSpecified(self) -> bool: 

243 """ 

244 Checks if all the required property values have been specified. 

245 This includes values that are enabled, control set points, and recycle guesses. 

246 Both guesses and set points are needed. 

247 """ 

248 value: PropertyValue 

249 for value in self.values.all(): 

250 if ( 

251 value.enabled or value.is_control_set_point() or self.is_recycle_var() 

252 ) and not value.has_value(): 

253 return False 

254 return True 

255 

256 def enable(self, condition: bool = True) -> list[PropertyValue]: 

257 """Enables or disables property values based on configuration rules.""" 

258 object_type = self.set.simulationObject.objectType 

259 config = configuration.get(object_type) 

260 

261 list_prop_val = [] 

262 

263 if self.key not in config.properties: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true

264 propertySetGroup_key = ( 

265 None # default to none, we are assuming the property doesn't exist. 

266 ) 

267 else: 

268 propertySetGroup_key = config.properties[self.key].propertySetGroup 

269 propertySetGroup_config = config.propertySetGroups.get( 

270 propertySetGroup_key, None 

271 ) 

272 

273 if ( 

274 propertySetGroup_config is not None 

275 and propertySetGroup_config.type == "exceptLast" 

276 and condition == True 

277 ): 

278 # in ExceptLast properties, you don't enable all of them, you enable 

279 # all of them except the last one 

280 cutoff, property_values = self.get_cutoff_and_property_values(config) 

281 if property_values: 

282 for i in range(len(property_values)): 

283 if i <= cutoff: 

284 # if the property is before the cutoff, it should be enabled 

285 property_values[i].enabled = condition 

286 else: 

287 property_values[i].enabled = False 

288 list_prop_val.append(property_values[i]) 

289 else: 

290 # this must be a custom property or a machine learning property. 

291 values = list(self.values.all()) 

292 for value in values: 

293 value.enabled = condition 

294 list_prop_val.append(value) 

295 return list_prop_val 

296 

297 def is_recycle_var(self) -> bool: 

298 return hasattr(self, "recycleConnection") 

299 

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

301 return ControlValue.create( 

302 setPoint=self.values.first(), 

303 manipulated=prop.values.first(), 

304 flowsheet_state=self.flowsheet_state, 

305 ) 

306 

307 def unit_conversion(self, new_unit: str) -> None: 

308 """ 

309 Perform a unit conversion on the value field of the PropertyInfo instance 

310 """ 

311 

312 # update both the unit and the value 

313 original_unit = self.unit 

314 self.unit = new_unit 

315 # original_value = self.get_value() 

316 original_value = self.values.all() 

317 for value in list(original_value): 

318 # we only want to update the units of properties that can't be manually edited. 

319 # this is a design choice, so users don't enter 100, switch from Pa to kPa, and it gets converted to 0.1 kPa 

320 if not value.is_enabled(): 

321 if value.value != None: 

322 float_value = float(value.value) 

323 new_value = convert_value(float_value, original_unit, new_unit) 

324 value.value = new_value 

325 value.save() 

326 

327 def get_schema(self) -> PropertyType | None: 

328 # get the type of the unit operation 

329 if self.set.simulationObject == None: 

330 return None 

331 else: 

332 return self.set.simulationObject.schema.properties.get(self.key, None) 

333 

334 def is_custom_property(self) -> bool: 

335 return self.get_schema() is None 

336 

337 

338def check_is_except_last(property_info: PropertyInfo) -> bool: 

339 """ 

340 Check if the property is of type "exceptLast" 

341 """ 

342 object_type = property_info.set.simulationObject.objectType 

343 config = configuration.get(object_type) 

344 for schema in config.propertySetGroups.values(): 

345 if schema.type == "exceptLast": 

346 return True 

347 return False