Coverage for backend/django/core/auxiliary/serializers/PropertyValueSerializer.py: 89%
146 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-12 01:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-12 01:47 +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 if (
172 not fully_defined
173 and check_fully_defined(property_set, check_fraction_sum=True)
174 ):
175 # we are now fully defined
176 get_revert_values()
177 convert_to_molar_fractions(property_set)
179 simulation_object.refresh_from_db()
180 if (
181 not property_set.disable_all # ie. outlet stream or recycle stream
182 and check_fully_defined(property_set, check_fraction_sum=True)
183 and all([not value.is_externally_controlled()
184 for prop in PropertyInfo.objects.filter(set__simulationObject=simulation_object)
185 for value in prop.values.all()])
186 ):
187 # if stream and all properties are specified, solve the stream
188 try:
189 build_state_request(simulation_object)
190 except BuildStateSolveError as e:
191 # revert values if any
192 value_objs = []
193 for prop, value in revert_values.items():
194 value_objs.append(prop)
195 prop.value = value
197 PropertyValue.objects.bulk_update(value_objs, ["value"])
198 raise e
200 def _attach_diagnostics_findings(self, instance: PropertyValue, validated_data: dict) -> None:
201 """
202 Attach diagnostics rule findings to the instance for response serialization.
204 This lets the frontend get rule results in the same request that persists
205 the updated property value (avoids a second request right after blur).
206 """
207 logger = logging.getLogger(__name__)
209 # Formula-only updates don't have a meaningful numeric value to validate.
210 if "formula" in validated_data and "value" not in validated_data:
211 instance._diagnostic_findings = []
212 return
214 raw_value = getattr(instance, "value", None)
215 try:
216 numeric_value = float(raw_value)
217 except (TypeError, ValueError):
218 instance._diagnostic_findings = []
219 return
221 try:
222 from diagnostics.rules.engine import build_rule_context, evaluate_rules
223 except Exception:
224 logger.error("Failed to import diagnostics rules engine", exc_info=True)
225 instance._diagnostic_findings = []
226 return
228 try:
229 property_info: PropertyInfo = instance.property
230 property_set: PropertySet = property_info.set
231 simulation_object = property_set.simulationObject
233 ctx = build_rule_context(simulation_object, property_info, numeric_value)
234 instance._diagnostic_findings = [f.to_dict() for f in evaluate_rules(ctx)]
235 except Exception:
236 # Diagnostics should never fail the primary update path, but we log for debugging.
237 logger.error(
238 "Diagnostics rule evaluation failed for property %s (value=%s)",
239 getattr(instance, "id", "?"),
240 raw_value,
241 exc_info=True,
242 )
243 instance._diagnostic_findings = []