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
« 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
10from diagnostics.serializers import FindingSerializer
12from ..viewsets.compound_conversions import (
13 check_fully_defined,
14 convert_to_molar_fractions,
15 convert_to_raw_values,
16)
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()
39 @extend_schema_field(serializers.ListField(child=serializers.CharField()))
40 def get_indexedSets(self, instance: PropertyValue) -> list:
41 return instance.get_indexes()
43 @extend_schema_field(serializers.ListField(child=serializers.CharField()))
44 def get_indexedSetNames(self, instance: PropertyValue) -> list:
45 return instance.get_index_names()
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.
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", [])
57 class Meta:
58 model = PropertyValue
59 fields = "__all__"
60 read_only_fields = ['id', 'controlManipulated', 'controlSetPoint']
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
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()
77 def handle_update(self, instance, validated_data) -> None:
78 from idaes_factory.endpoints import build_state_request, BuildStateSolveError
80 if "formula" in validated_data:
81 instance.formula = validated_data["formula"]
82 instance.save()
83 return
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)
90 object_type = simulation_object.objectType
91 config = configuration.get(object_type)
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.
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]
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())
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
120 property_set.ContainedProperties.all().prefetch_related("values")
122 revert_values: dict[PropertyValue, float] = {}
123 mole_frac_comp = property_set.get_property("mole_frac_comp")
125 def get_revert_values():
126 for prop in mole_frac_comp.values.all():
127 revert_values[prop] = prop.value
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)
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)
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)
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
199 PropertyValue.objects.bulk_update(value_objs, ["value"])
200 raise e
202 def _attach_diagnostics_findings(self, instance: PropertyValue, validated_data: dict) -> None:
203 """
204 Attach diagnostics rule findings to the instance for response serialization.
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__)
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
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
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
230 try:
231 property_info: PropertyInfo = instance.property
232 property_set: PropertySet = property_info.set
233 simulation_object = property_set.simulationObject
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 = []