Coverage for backend/django/core/auxiliary/serializers/PropertyValueSerializer.py: 84%

147 statements  

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

1import logging 

2from typing import List 

3 

4from diagnostics.serializers import FindingSerializer 

5from drf_spectacular.utils import extend_schema_field 

6from rest_framework import serializers 

7 

8from core.auxiliary.models.PropertyInfo import PropertyInfo, check_is_except_last 

9from core.auxiliary.models.PropertySet import PropertySet 

10from core.auxiliary.models.PropertyValue import PropertyValue 

11from flowsheetInternals.unitops.config.config_base import configuration 

12 

13from ..viewsets.compound_conversions import ( 

14 check_fully_defined, 

15 convert_to_molar_fractions, 

16 convert_to_raw_values, 

17 stream_has_build_state_inputs, 

18) 

19 

20class PropertyValueSerializer(serializers.ModelSerializer): 

21 controlManipulated = serializers.IntegerField( 

22 source='controlManipulated.id', read_only=True) 

23 controlManipulatedName = serializers.CharField( 

24 source='controlManipulated.setPoint.property.displayName', read_only=True) 

25 controlManipulatedId = serializers.IntegerField( 

26 source='controlManipulated.setPoint.property.set.simulationObject.id', read_only=True) 

27 controlSetPoint = serializers.IntegerField( 

28 source='controlSetPoint.id', read_only=True) 

29 controlSetPointName = serializers.CharField( 

30 source='controlSetPoint.manipulated.property.displayName', read_only=True) 

31 controlSetPointId = serializers.IntegerField( 

32 source='controlSetPoint.manipulated.property.set.simulationObject.id', read_only=True) 

33 controlManipulatedObject = serializers.CharField( 

34 source='controlManipulated.setPoint.property.set.simulationObject.componentName', read_only=True) 

35 controlSetPointObject = serializers.CharField( 

36 source='controlSetPoint.manipulated.property.set.simulationObject.componentName', read_only=True) 

37 indexedSets = serializers.SerializerMethodField() 

38 indexedSetNames = serializers.SerializerMethodField() 

39 diagnosticFindings = serializers.SerializerMethodField() 

40 

41 @extend_schema_field(serializers.ListField(child=serializers.CharField())) 

42 def get_indexedSets(self, instance: PropertyValue) -> list: 

43 return instance.get_indexes() 

44 

45 @extend_schema_field(serializers.ListField(child=serializers.CharField())) 

46 def get_indexedSetNames(self, instance: PropertyValue) -> list: 

47 return instance.get_index_names() 

48 

49 @extend_schema_field(FindingSerializer(many=True)) 

50 def get_diagnosticFindings(self, instance: PropertyValue) -> list: 

51 """ 

52 Deterministic diagnostics findings for this single property value. 

53 

54 These findings are attached during updates so the frontend can show rule 

55 feedback without making an extra /api/diagnostics/... request. 

56 """ 

57 return getattr(instance, "_diagnostic_findings", []) 

58 

59 class Meta: 

60 model = PropertyValue 

61 fields = "__all__" 

62 read_only_fields = ['id', 'controlManipulated', 'controlSetPoint'] 

63 

64 def update(self, instance: PropertyValue, validated_data: dict) -> None: 

65 if not instance.property.set.has_simulation_object: 65 ↛ 67line 65 didn't jump to line 67 because the condition on line 65 was never true

66 # eg. pinch property set 

67 return super().update(instance, validated_data) 

68 self.handle_update(instance, validated_data) 

69 self._attach_diagnostics_findings(instance, validated_data) 

70 return instance 

71 

72 def handle_save(self, instance, validated_data) -> None: 

73 value = validated_data["value"] 

74 displayValue = validated_data.get("displayValue", value) 

75 instance.value = value 

76 instance.displayValue = displayValue 

77 instance.save() 

78 

79 def handle_update(self, instance, validated_data) -> None: 

80 from idaes_factory.endpoints import ( 

81 BuildStateSolveError, 

82 build_state_request, 

83 ) 

84 

85 if "formula" in validated_data: 

86 instance.formula = validated_data["formula"] 

87 instance.save() 

88 return 

89 

90 property_info: PropertyInfo = instance.property 

91 property_set: PropertySet = property_info.set 

92 simulation_object = property_set.simulationObject 

93 def handle_save(): return self.handle_save(instance, validated_data) 

94 

95 object_type = simulation_object.objectType 

96 config = configuration.get(object_type) 

97 

98 if simulation_object.objectType != "stream": 

99 handle_save() 

100 if check_is_except_last(property_info): 

101 # The last property should be calculated so that all of the last index set sums to 1. 

102 

103 # get the first index set 

104 # The last items in the first index set will be calculated. 

105 first_index_type = property_info.get_schema().indexSets[0] 

106 

107 # Get the all the indices for this PropertyValue 

108 indexed_items = list(instance.indexedItems.all()) 

109 indexed_item_ids = [index.id for index in indexed_items if index.type != first_index_type] 

110 # Get all the other property values that have the same indices 

111 filtered_properties = instance.property.values 

112 for index in indexed_item_ids: 

113 filtered_properties = filtered_properties.filter( 

114 indexedItems__id=index) 

115 other_property_values: List[PropertyValue] = list(filtered_properties.all()) 

116 

117 # Split off the last one, that will be calculated 

118 calculated_property_value = other_property_values[-1] 

119 known_property_values = other_property_values[:-1] 

120 total_ = sum([float(prop_val.value) for prop_val in known_property_values if prop_val.pk != instance.pk and prop_val.value != None]) + float(instance.value) 

121 calculated_property_value.value = 1 - total_ 

122 calculated_property_value.save() 

123 return 

124 

125 property_set.ContainedProperties.all().prefetch_related("values") 

126 

127 revert_values: dict[PropertyValue, float] = {} 

128 mole_frac_comp = property_set.get_property("mole_frac_comp") 

129 

130 def get_revert_values(): 

131 for prop in mole_frac_comp.values.all(): 

132 revert_values[prop] = prop.value 

133 

134 update_is_empty = validated_data["value"] in [None, ""] 

135 if property_info.key == "mole_frac_comp": 

136 fully_defined = check_fully_defined( 

137 property_set, check_none_empty=True) 

138 

139 if fully_defined: 

140 if update_is_empty: 140 ↛ 142line 140 didn't jump to line 142 because the condition on line 140 was never true

141 # we are becoming undefined, set all the properties to "raw" values 

142 convert_to_raw_values(property_set) 

143 handle_save() 

144 else: 

145 handle_save() 

146 mole_frac_comp.refresh_from_db() 

147 if check_fully_defined(property_set, check_fraction_sum=True): 147 ↛ 151line 147 didn't jump to line 151 because the condition on line 147 was always true

148 # previously had a value, staying as fully defined 

149 convert_to_molar_fractions(property_set) 

150 else: 

151 convert_to_raw_values(property_set) 

152 handle_save() 

153 else: 

154 # not fully defined 

155 handle_save() 

156 mole_frac_comp.refresh_from_db() 

157 if ( 

158 not update_is_empty 

159 and check_fully_defined(property_set, [mole_frac_comp]) 

160 ): 

161 # this could make us fully defined 

162 if check_fully_defined(property_set, check_fraction_sum=True): 

163 get_revert_values() 

164 convert_to_molar_fractions(property_set) 

165 

166 else: 

167 fully_defined = check_fully_defined( 

168 property_set, check_none_empty=True) 

169 if update_is_empty: 169 ↛ 170line 169 didn't jump to line 170 because the condition on line 169 was never true

170 if fully_defined: 

171 # we are becoming undefined, set all the properties to "raw" values 

172 convert_to_raw_values(property_set) 

173 handle_save() 

174 else: 

175 handle_save() 

176 if ( 

177 not fully_defined 

178 and check_fully_defined(property_set, check_fraction_sum=True) 

179 ): 

180 # we are now fully defined 

181 get_revert_values() 

182 convert_to_molar_fractions(property_set) 

183 

184 simulation_object.refresh_from_db() 

185 if ( 

186 not property_set.disable_all # ie. outlet stream or recycle stream 

187 and stream_has_build_state_inputs(property_set) 

188 and all([not value.is_externally_controlled() 

189 for prop in PropertyInfo.objects.filter(set__simulationObject=simulation_object) 

190 for value in prop.values.all()]) 

191 ): 

192 # Streams can be built once composition is complete and one valid 

193 # state-variable pair is present; they do not need every enabled 

194 # stream property specified up front. 

195 request_user = self.context["request"].user 

196 try: 

197 build_state_request( 

198 simulation_object, 

199 request_user, 

200 rollback_values={ 

201 prop.id: value 

202 for prop, value in revert_values.items() 

203 } or None, 

204 ) 

205 except BuildStateSolveError as e: 

206 # revert values if any 

207 value_objs = [] 

208 for prop, value in revert_values.items(): 

209 value_objs.append(prop) 

210 prop.value = value 

211 

212 PropertyValue.objects.bulk_update(value_objs, ["value"]) 

213 raise e 

214 

215 def _attach_diagnostics_findings(self, instance: PropertyValue, validated_data: dict) -> None: 

216 """ 

217 Attach diagnostics rule findings to the instance for response serialization. 

218 

219 This lets the frontend get rule results in the same request that persists 

220 the updated property value (avoids a second request right after blur). 

221 """ 

222 logger = logging.getLogger(__name__) 

223 

224 # Formula-only updates don't have a meaningful numeric value to validate. 

225 if "formula" in validated_data and "value" not in validated_data: 

226 instance._diagnostic_findings = [] 

227 return 

228 

229 raw_value = getattr(instance, "value", None) 

230 try: 

231 numeric_value = float(raw_value) 

232 except (TypeError, ValueError): 

233 instance._diagnostic_findings = [] 

234 return 

235 

236 try: 

237 from diagnostics.rules.engine import build_rule_context, evaluate_rules 

238 except Exception: 

239 logger.error("Failed to import diagnostics rules engine", exc_info=True) 

240 instance._diagnostic_findings = [] 

241 return 

242 

243 try: 

244 property_info: PropertyInfo = instance.property 

245 property_set: PropertySet = property_info.set 

246 simulation_object = property_set.simulationObject 

247 

248 ctx = build_rule_context(simulation_object, property_info, numeric_value) 

249 instance._diagnostic_findings = [f.to_dict() for f in evaluate_rules(ctx)] 

250 except Exception: 

251 # Diagnostics should never fail the primary update path, but we log for debugging. 

252 logger.error( 

253 "Diagnostics rule evaluation failed for property %s (value=%s)", 

254 getattr(instance, "id", "?"), 

255 raw_value, 

256 exc_info=True, 

257 ) 

258 instance._diagnostic_findings = []