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

147 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-02-11 21:43 +0000

1from core.auxiliary.models.PropertyValue import PropertyValue 

2from rest_framework import serializers 

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

4from core.auxiliary.models.PropertySet import PropertySet 

5from flowsheetInternals.unitops.config.config_base import configuration 

6from typing import List 

7from drf_spectacular.utils import extend_schema_field 

8import logging 

9 

10from diagnostics.serializers import FindingSerializer 

11 

12from ..viewsets.compound_conversions import ( 

13 check_fully_defined, 

14 convert_to_molar_fractions, 

15 convert_to_raw_values, 

16) 

17 

18class PropertyValueSerializer(serializers.ModelSerializer): 

19 controlManipulated = serializers.IntegerField( 

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

21 controlManipulatedName = serializers.CharField( 

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

23 controlManipulatedId = serializers.IntegerField( 

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

25 controlSetPoint = serializers.IntegerField( 

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

27 controlSetPointName = serializers.CharField( 

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

29 controlSetPointId = serializers.IntegerField( 

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

31 controlManipulatedObject = serializers.CharField( 

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

33 controlSetPointObject = serializers.CharField( 

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

35 indexedSets = serializers.SerializerMethodField() 

36 indexedSetNames = serializers.SerializerMethodField() 

37 diagnosticFindings = serializers.SerializerMethodField() 

38 

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

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

41 return instance.get_indexes() 

42 

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

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

45 return instance.get_index_names() 

46 

47 @extend_schema_field(FindingSerializer(many=True)) 

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

49 """ 

50 Deterministic diagnostics findings for this single property value. 

51 

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

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

54 """ 

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

56 

57 class Meta: 

58 model = PropertyValue 

59 fields = "__all__" 

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

61 

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

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

64 # eg. pinch property set 

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

66 self.handle_update(instance, validated_data) 

67 self._attach_diagnostics_findings(instance, validated_data) 

68 return instance 

69 

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

71 value = validated_data["value"] 

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

73 instance.value = value 

74 instance.displayValue = displayValue 

75 instance.save() 

76 

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

78 from idaes_factory.endpoints import build_state_request, BuildStateSolveError 

79 

80 if "formula" in validated_data: 

81 instance.formula = validated_data["formula"] 

82 instance.save() 

83 return 

84 

85 property_info: PropertyInfo = instance.property 

86 property_set: PropertySet = property_info.set 

87 simulation_object = property_set.simulationObject 

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

89 

90 object_type = simulation_object.objectType 

91 config = configuration.get(object_type) 

92 

93 if simulation_object.objectType != "stream": 

94 handle_save() 

95 if check_is_except_last(property_info): 

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

97 

98 # get the first index set 

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

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

101 

102 # Get the all the indices for this PropertyValue 

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

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

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

106 filtered_properties = instance.property.values 

107 for index in indexed_item_ids: 

108 filtered_properties = filtered_properties.filter( 

109 indexedItems__id=index) 

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

111 

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

113 calculated_property_value = other_property_values[-1] 

114 known_property_values = other_property_values[:-1] 

115 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) 

116 calculated_property_value.value = 1 - total_ 

117 calculated_property_value.save() 

118 return 

119 

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

121 

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

123 mole_frac_comp = property_set.get_property("mole_frac_comp") 

124 

125 def get_revert_values(): 

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

127 revert_values[prop] = prop.value 

128 

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

130 if property_info.key == "mole_frac_comp": 

131 fully_defined = check_fully_defined( 

132 property_set, check_none_empty=True) 

133 

134 if fully_defined: 

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

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

137 convert_to_raw_values(property_set) 

138 handle_save() 

139 else: 

140 handle_save() 

141 mole_frac_comp.refresh_from_db() 

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

143 # previously had a value, staying as fully defined 

144 convert_to_molar_fractions(property_set) 

145 else: 

146 convert_to_raw_values(property_set) 

147 handle_save() 

148 else: 

149 # not fully defined 

150 handle_save() 

151 mole_frac_comp.refresh_from_db() 

152 if ( 

153 not update_is_empty 

154 and check_fully_defined(property_set, [mole_frac_comp]) 

155 ): 

156 # this could make us fully defined 

157 if check_fully_defined(property_set, check_fraction_sum=True): 

158 get_revert_values() 

159 convert_to_molar_fractions(property_set) 

160 

161 else: 

162 fully_defined = check_fully_defined( 

163 property_set, check_none_empty=True) 

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

165 if fully_defined: 

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

167 convert_to_raw_values(property_set) 

168 handle_save() 

169 else: 

170 handle_save() 

171 else: 

172 handle_save() 

173 if ( 

174 not fully_defined 

175 and check_fully_defined(property_set, check_fraction_sum=True) 

176 ): 

177 # we are now fully defined 

178 get_revert_values() 

179 convert_to_molar_fractions(property_set) 

180 

181 simulation_object.refresh_from_db() 

182 if ( 

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

184 and check_fully_defined(property_set, check_fraction_sum=True) 

185 and all([not value.is_externally_controlled() 

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

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

188 ): 

189 # if stream and all properties are specified, solve the stream 

190 try: 

191 build_state_request(simulation_object) 

192 except BuildStateSolveError as e: 

193 # revert values if any 

194 value_objs = [] 

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

196 value_objs.append(prop) 

197 prop.value = value 

198 

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

200 raise e 

201 

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

203 """ 

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

205 

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

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

208 """ 

209 logger = logging.getLogger(__name__) 

210 

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

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

213 instance._diagnostic_findings = [] 

214 return 

215 

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

217 try: 

218 numeric_value = float(raw_value) 

219 except (TypeError, ValueError): 

220 instance._diagnostic_findings = [] 

221 return 

222 

223 try: 

224 from diagnostics.rules.engine import build_rule_context, evaluate_rules 

225 except Exception: 

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

227 instance._diagnostic_findings = [] 

228 return 

229 

230 try: 

231 property_info: PropertyInfo = instance.property 

232 property_set: PropertySet = property_info.set 

233 simulation_object = property_set.simulationObject 

234 

235 ctx = build_rule_context(simulation_object, property_info, numeric_value) 

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

237 except Exception: 

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

239 logger.error( 

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

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

242 raw_value, 

243 exc_info=True, 

244 ) 

245 instance._diagnostic_findings = [] 

246