qnty 0.0.9__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qnty/__init__.py +2 -3
- qnty/constants/__init__.py +10 -0
- qnty/constants/numerical.py +18 -0
- qnty/constants/solvers.py +6 -0
- qnty/constants/tests.py +6 -0
- qnty/dimensions/__init__.py +23 -0
- qnty/dimensions/base.py +97 -0
- qnty/dimensions/field_dims.py +126 -0
- qnty/dimensions/field_dims.pyi +128 -0
- qnty/dimensions/signature.py +111 -0
- qnty/equations/__init__.py +1 -1
- qnty/equations/equation.py +118 -155
- qnty/equations/system.py +68 -65
- qnty/expressions/__init__.py +25 -46
- qnty/expressions/formatter.py +188 -0
- qnty/expressions/functions.py +46 -68
- qnty/expressions/nodes.py +539 -384
- qnty/expressions/types.py +70 -0
- qnty/problems/__init__.py +145 -0
- qnty/problems/composition.py +1031 -0
- qnty/problems/problem.py +695 -0
- qnty/problems/rules.py +145 -0
- qnty/problems/solving.py +1216 -0
- qnty/problems/validation.py +127 -0
- qnty/quantities/__init__.py +28 -5
- qnty/quantities/base_qnty.py +677 -0
- qnty/quantities/field_converters.py +24004 -0
- qnty/quantities/field_qnty.py +1012 -0
- qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
- qnty/{generated/quantities.py → quantities/field_vars.py} +754 -432
- qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
- qnty/solving/manager.py +50 -44
- qnty/solving/order.py +181 -133
- qnty/solving/solvers/__init__.py +2 -9
- qnty/solving/solvers/base.py +27 -37
- qnty/solving/solvers/iterative.py +115 -135
- qnty/solving/solvers/simultaneous.py +93 -165
- qnty/units/__init__.py +1 -0
- qnty/{generated/units.py → units/field_units.py} +1700 -991
- qnty/units/field_units.pyi +2461 -0
- qnty/units/prefixes.py +58 -105
- qnty/units/registry.py +76 -89
- qnty/utils/__init__.py +16 -0
- qnty/utils/caching/__init__.py +23 -0
- qnty/utils/caching/manager.py +401 -0
- qnty/utils/error_handling/__init__.py +66 -0
- qnty/utils/error_handling/context.py +39 -0
- qnty/utils/error_handling/exceptions.py +96 -0
- qnty/utils/error_handling/handlers.py +171 -0
- qnty/utils/logging.py +4 -4
- qnty/utils/protocols.py +164 -0
- qnty/utils/scope_discovery.py +420 -0
- {qnty-0.0.9.dist-info → qnty-0.1.0.dist-info}/METADATA +1 -1
- qnty-0.1.0.dist-info/RECORD +60 -0
- qnty/_backup/problem_original.py +0 -1251
- qnty/_backup/quantity.py +0 -63
- qnty/codegen/cli.py +0 -125
- qnty/codegen/generators/data/unit_data.json +0 -8807
- qnty/codegen/generators/data_processor.py +0 -345
- qnty/codegen/generators/dimensions_gen.py +0 -434
- qnty/codegen/generators/doc_generator.py +0 -141
- qnty/codegen/generators/out/dimension_mapping.json +0 -974
- qnty/codegen/generators/out/dimension_metadata.json +0 -123
- qnty/codegen/generators/out/units_metadata.json +0 -223
- qnty/codegen/generators/quantities_gen.py +0 -159
- qnty/codegen/generators/setters_gen.py +0 -178
- qnty/codegen/generators/stubs_gen.py +0 -167
- qnty/codegen/generators/units_gen.py +0 -295
- qnty/expressions/cache.py +0 -94
- qnty/generated/dimensions.py +0 -514
- qnty/problem/__init__.py +0 -91
- qnty/problem/base.py +0 -142
- qnty/problem/composition.py +0 -385
- qnty/problem/composition_mixin.py +0 -382
- qnty/problem/equations.py +0 -413
- qnty/problem/metaclass.py +0 -302
- qnty/problem/reconstruction.py +0 -1016
- qnty/problem/solving.py +0 -180
- qnty/problem/validation.py +0 -64
- qnty/problem/variables.py +0 -239
- qnty/quantities/expression_quantity.py +0 -314
- qnty/quantities/quantity.py +0 -428
- qnty/quantities/typed_quantity.py +0 -215
- qnty/validation/__init__.py +0 -0
- qnty/validation/registry.py +0 -0
- qnty/validation/rules.py +0 -167
- qnty-0.0.9.dist-info/RECORD +0 -63
- /qnty/{codegen → extensions}/__init__.py +0 -0
- /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
- /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
- /qnty/{generated → extensions/reporting}/__init__.py +0 -0
- {qnty-0.0.9.dist-info → qnty-0.1.0.dist-info}/WHEEL +0 -0
@@ -1,382 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Sub-problem composition functionality for Problem class.
|
3
|
-
|
4
|
-
This module contains all the sub-problem integration logic,
|
5
|
-
namespace handling, and composite equation creation.
|
6
|
-
"""
|
7
|
-
|
8
|
-
from __future__ import annotations
|
9
|
-
|
10
|
-
from typing import TYPE_CHECKING, Any
|
11
|
-
|
12
|
-
if TYPE_CHECKING:
|
13
|
-
from qnty.quantities import TypeSafeVariable as Variable
|
14
|
-
|
15
|
-
# Constants for composition
|
16
|
-
MATHEMATICAL_OPERATORS = ['+', '-', '*', '/', ' / ', ' * ', ' + ', ' - ']
|
17
|
-
COMMON_COMPOSITE_VARIABLES = ['P', 'c', 'S', 'E', 'W', 'Y']
|
18
|
-
|
19
|
-
|
20
|
-
class CompositionMixin:
|
21
|
-
"""Mixin class providing sub-problem composition functionality."""
|
22
|
-
|
23
|
-
# These attributes/methods will be provided by other mixins in the final Problem class
|
24
|
-
variables: dict[str, Variable]
|
25
|
-
sub_problems: dict[str, Any]
|
26
|
-
logger: Any
|
27
|
-
|
28
|
-
def add_variable(self, _variable: Variable) -> None:
|
29
|
-
"""Will be provided by VariablesMixin."""
|
30
|
-
...
|
31
|
-
|
32
|
-
def add_equation(self, _equation: Any) -> None:
|
33
|
-
"""Will be provided by EquationsMixin."""
|
34
|
-
...
|
35
|
-
|
36
|
-
def _clone_variable(self, _variable: Variable) -> Variable:
|
37
|
-
"""Will be provided by VariablesMixin."""
|
38
|
-
...
|
39
|
-
|
40
|
-
def _process_equation(self, _attr_name: str, _equation: Any) -> bool:
|
41
|
-
"""Will be provided by EquationsMixin."""
|
42
|
-
...
|
43
|
-
|
44
|
-
def _recreate_validation_checks(self) -> None:
|
45
|
-
"""Will be provided by ValidationMixin."""
|
46
|
-
...
|
47
|
-
|
48
|
-
def _is_conditional_equation(self, _equation: Any) -> bool:
|
49
|
-
"""Will be provided by EquationsMixin."""
|
50
|
-
...
|
51
|
-
|
52
|
-
def _get_equation_lhs_symbol(self, _equation: Any) -> str | None:
|
53
|
-
"""Will be provided by EquationsMixin."""
|
54
|
-
...
|
55
|
-
|
56
|
-
def _extract_from_class_variables(self):
|
57
|
-
"""Extract variables, equations, and sub-problems from class-level definitions."""
|
58
|
-
self._extract_sub_problems()
|
59
|
-
self._extract_direct_variables()
|
60
|
-
self._recreate_validation_checks() # type: ignore[attr-defined]
|
61
|
-
self._create_composite_equations()
|
62
|
-
self._extract_equations()
|
63
|
-
|
64
|
-
def _extract_sub_problems(self):
|
65
|
-
"""Extract and integrate sub-problems from class-level definitions."""
|
66
|
-
if hasattr(self.__class__, '_original_sub_problems'):
|
67
|
-
original_sub_problems = getattr(self.__class__, '_original_sub_problems', {})
|
68
|
-
for attr_name, sub_problem in original_sub_problems.items():
|
69
|
-
self._integrate_sub_problem(sub_problem, attr_name)
|
70
|
-
|
71
|
-
def _extract_direct_variables(self):
|
72
|
-
"""Extract direct variables from class-level definitions."""
|
73
|
-
from qnty.quantities import TypeSafeVariable as Variable
|
74
|
-
|
75
|
-
processed_symbols = set()
|
76
|
-
|
77
|
-
# Single pass through class attributes to collect variables
|
78
|
-
for attr_name, attr_value in self._get_class_attributes():
|
79
|
-
if isinstance(attr_value, Variable):
|
80
|
-
# Set symbol based on attribute name (T_bar, P, etc.)
|
81
|
-
attr_value.symbol = attr_name
|
82
|
-
|
83
|
-
# Skip if we've already processed this symbol
|
84
|
-
if attr_value.symbol in processed_symbols:
|
85
|
-
continue
|
86
|
-
processed_symbols.add(attr_value.symbol)
|
87
|
-
|
88
|
-
# Clone variable to avoid shared state between instances
|
89
|
-
cloned_var = self._clone_variable(attr_value)
|
90
|
-
self.add_variable(cloned_var)
|
91
|
-
# Set the same cloned variable object as instance attribute
|
92
|
-
# Use super() to bypass our custom __setattr__ during initialization
|
93
|
-
super().__setattr__(attr_name, cloned_var)
|
94
|
-
|
95
|
-
def _extract_equations(self):
|
96
|
-
"""Extract and process equations from class-level definitions."""
|
97
|
-
|
98
|
-
equations_to_process = self._collect_class_equations()
|
99
|
-
|
100
|
-
for attr_name, equation in equations_to_process:
|
101
|
-
try:
|
102
|
-
if self._process_equation(attr_name, equation):
|
103
|
-
setattr(self, attr_name, equation)
|
104
|
-
except Exception as e:
|
105
|
-
# Log but continue - some equations might fail during class definition
|
106
|
-
self.logger.warning(f"Failed to process equation {attr_name}: {e}")
|
107
|
-
# Still set the original equation as attribute
|
108
|
-
setattr(self, attr_name, equation)
|
109
|
-
|
110
|
-
def _get_class_attributes(self) -> list[tuple[str, Any]]:
|
111
|
-
"""Get all non-private class attributes efficiently."""
|
112
|
-
return [(attr_name, getattr(self.__class__, attr_name))
|
113
|
-
for attr_name in dir(self.__class__)
|
114
|
-
if not attr_name.startswith('_')]
|
115
|
-
|
116
|
-
def _collect_class_equations(self) -> list[tuple[str, Any]]:
|
117
|
-
"""Collect all equation objects from class attributes."""
|
118
|
-
from qnty.equations import Equation
|
119
|
-
|
120
|
-
equations_to_process = []
|
121
|
-
for attr_name, attr_value in self._get_class_attributes():
|
122
|
-
if isinstance(attr_value, Equation):
|
123
|
-
equations_to_process.append((attr_name, attr_value))
|
124
|
-
return equations_to_process
|
125
|
-
|
126
|
-
def _integrate_sub_problem(self, sub_problem, namespace: str) -> None:
|
127
|
-
"""
|
128
|
-
Integrate a sub-problem by flattening its variables with namespace prefixes.
|
129
|
-
Creates a simple dotted access pattern: self.header.P becomes self.header_P
|
130
|
-
"""
|
131
|
-
self.sub_problems[namespace] = sub_problem
|
132
|
-
|
133
|
-
# Get proxy configurations if available
|
134
|
-
proxy_configs = getattr(self.__class__, '_proxy_configurations', {}).get(namespace, {})
|
135
|
-
|
136
|
-
# Create a namespace object for dotted access (self.header.P)
|
137
|
-
namespace_obj = type('SubProblemNamespace', (), {})()
|
138
|
-
|
139
|
-
# Add all sub-problem variables with namespace prefixes
|
140
|
-
for var_symbol, var in sub_problem.variables.items():
|
141
|
-
namespaced_var = self._create_namespaced_variable(var, var_symbol, namespace, proxy_configs)
|
142
|
-
self.add_variable(namespaced_var)
|
143
|
-
|
144
|
-
# Set both namespaced access (self.header_P) and dotted access (self.header.P)
|
145
|
-
if namespaced_var.symbol is not None:
|
146
|
-
super().__setattr__(namespaced_var.symbol, namespaced_var)
|
147
|
-
setattr(namespace_obj, var_symbol, namespaced_var)
|
148
|
-
|
149
|
-
# Set the namespace object for dotted access
|
150
|
-
super().__setattr__(namespace, namespace_obj)
|
151
|
-
|
152
|
-
# Also add all sub-problem equations (they'll be namespaced automatically)
|
153
|
-
for equation in sub_problem.equations:
|
154
|
-
try:
|
155
|
-
# Skip conditional equations for variables that are overridden to known values in composition
|
156
|
-
if self._should_skip_subproblem_equation(equation, namespace):
|
157
|
-
continue
|
158
|
-
|
159
|
-
namespaced_equation = self._namespace_equation(equation, namespace)
|
160
|
-
if namespaced_equation:
|
161
|
-
self.add_equation(namespaced_equation)
|
162
|
-
except Exception as e:
|
163
|
-
self.logger.debug(f"Failed to namespace equation from {namespace}: {e}")
|
164
|
-
|
165
|
-
def _create_namespaced_variable(self, var: Variable, var_symbol: str, namespace: str, proxy_configs: dict) -> Variable:
|
166
|
-
"""Create a namespaced variable with proper configuration."""
|
167
|
-
namespaced_symbol = f"{namespace}_{var_symbol}"
|
168
|
-
namespaced_var = self._clone_variable(var)
|
169
|
-
namespaced_var.symbol = namespaced_symbol
|
170
|
-
namespaced_var.name = f"{var.name} ({namespace.title()})"
|
171
|
-
|
172
|
-
# Apply proxy configuration if available
|
173
|
-
if var_symbol in proxy_configs:
|
174
|
-
config = proxy_configs[var_symbol]
|
175
|
-
namespaced_var.quantity = config['quantity']
|
176
|
-
namespaced_var.is_known = config['is_known']
|
177
|
-
|
178
|
-
return namespaced_var
|
179
|
-
|
180
|
-
def _namespace_equation(self, equation, namespace: str):
|
181
|
-
"""
|
182
|
-
Create a namespaced version of an equation by prefixing all variable references.
|
183
|
-
"""
|
184
|
-
try:
|
185
|
-
# Get all variable symbols in the equation
|
186
|
-
variables_in_eq = equation.get_all_variables()
|
187
|
-
|
188
|
-
# Create mapping from original symbols to namespaced symbols
|
189
|
-
symbol_mapping = {}
|
190
|
-
for var_symbol in variables_in_eq:
|
191
|
-
namespaced_symbol = f"{namespace}_{var_symbol}"
|
192
|
-
if namespaced_symbol in self.variables:
|
193
|
-
symbol_mapping[var_symbol] = namespaced_symbol
|
194
|
-
|
195
|
-
if not symbol_mapping:
|
196
|
-
return None
|
197
|
-
|
198
|
-
# Create new equation with namespaced references
|
199
|
-
# For LHS, we need a Variable object to call .equals()
|
200
|
-
# For RHS, we need proper expression structure
|
201
|
-
namespaced_lhs = self._namespace_expression_for_lhs(equation.lhs, symbol_mapping)
|
202
|
-
namespaced_rhs = self._namespace_expression(equation.rhs, symbol_mapping)
|
203
|
-
|
204
|
-
if namespaced_lhs and namespaced_rhs:
|
205
|
-
equals_method = getattr(namespaced_lhs, 'equals', None)
|
206
|
-
if equals_method:
|
207
|
-
return equals_method(namespaced_rhs)
|
208
|
-
|
209
|
-
return None
|
210
|
-
|
211
|
-
except Exception:
|
212
|
-
return None
|
213
|
-
|
214
|
-
def _namespace_expression(self, expr, symbol_mapping):
|
215
|
-
"""
|
216
|
-
Create a namespaced version of an expression by replacing variable references.
|
217
|
-
"""
|
218
|
-
from qnty.expressions import BinaryOperation, ConditionalExpression, Constant, VariableReference
|
219
|
-
|
220
|
-
# Handle variable references
|
221
|
-
if isinstance(expr, VariableReference):
|
222
|
-
return self._namespace_variable_reference(expr, symbol_mapping)
|
223
|
-
elif hasattr(expr, 'symbol') and expr.symbol in symbol_mapping:
|
224
|
-
return self._namespace_variable_object(expr, symbol_mapping)
|
225
|
-
|
226
|
-
# Handle operations
|
227
|
-
elif isinstance(expr, BinaryOperation):
|
228
|
-
return self._namespace_binary_operation(expr, symbol_mapping)
|
229
|
-
elif isinstance(expr, ConditionalExpression):
|
230
|
-
return self._namespace_conditional_expression(expr, symbol_mapping)
|
231
|
-
elif hasattr(expr, 'operand'):
|
232
|
-
return self._namespace_unary_operation(expr, symbol_mapping)
|
233
|
-
elif hasattr(expr, 'function_name'):
|
234
|
-
return self._namespace_binary_function(expr, symbol_mapping)
|
235
|
-
elif isinstance(expr, Constant):
|
236
|
-
return expr
|
237
|
-
else:
|
238
|
-
return expr
|
239
|
-
|
240
|
-
def _namespace_variable_reference(self, expr, symbol_mapping):
|
241
|
-
"""Namespace a VariableReference object."""
|
242
|
-
from qnty.expressions import VariableReference
|
243
|
-
|
244
|
-
# VariableReference uses the 'name' property which returns the symbol if available
|
245
|
-
symbol = expr.name
|
246
|
-
if symbol in symbol_mapping:
|
247
|
-
namespaced_symbol = symbol_mapping[symbol]
|
248
|
-
if namespaced_symbol in self.variables:
|
249
|
-
return VariableReference(self.variables[namespaced_symbol])
|
250
|
-
return expr
|
251
|
-
|
252
|
-
def _namespace_variable_object(self, expr, symbol_mapping):
|
253
|
-
"""Namespace a Variable object."""
|
254
|
-
from qnty.expressions import VariableReference
|
255
|
-
|
256
|
-
namespaced_symbol = symbol_mapping[expr.symbol]
|
257
|
-
if namespaced_symbol in self.variables:
|
258
|
-
# Return VariableReference for use in expressions, not the Variable itself
|
259
|
-
return VariableReference(self.variables[namespaced_symbol])
|
260
|
-
return expr
|
261
|
-
|
262
|
-
def _namespace_binary_operation(self, expr, symbol_mapping):
|
263
|
-
"""Namespace a BinaryOperation."""
|
264
|
-
from qnty.expressions import BinaryOperation
|
265
|
-
|
266
|
-
namespaced_left = self._namespace_expression(expr.left, symbol_mapping)
|
267
|
-
namespaced_right = self._namespace_expression(expr.right, symbol_mapping)
|
268
|
-
return BinaryOperation(expr.operator, namespaced_left, namespaced_right)
|
269
|
-
|
270
|
-
def _namespace_unary_operation(self, expr, symbol_mapping):
|
271
|
-
"""Namespace a UnaryFunction."""
|
272
|
-
namespaced_operand = self._namespace_expression(expr.operand, symbol_mapping)
|
273
|
-
return type(expr)(expr.operator, namespaced_operand)
|
274
|
-
|
275
|
-
def _namespace_binary_function(self, expr, symbol_mapping):
|
276
|
-
"""Namespace a BinaryFunction."""
|
277
|
-
namespaced_left = self._namespace_expression(expr.left, symbol_mapping)
|
278
|
-
namespaced_right = self._namespace_expression(expr.right, symbol_mapping)
|
279
|
-
return type(expr)(expr.function_name, namespaced_left, namespaced_right)
|
280
|
-
|
281
|
-
def _namespace_conditional_expression(self, expr, symbol_mapping):
|
282
|
-
"""Namespace a ConditionalExpression."""
|
283
|
-
from qnty.expressions import ConditionalExpression
|
284
|
-
|
285
|
-
namespaced_condition = self._namespace_expression(expr.condition, symbol_mapping)
|
286
|
-
namespaced_true_expr = self._namespace_expression(expr.true_expr, symbol_mapping)
|
287
|
-
namespaced_false_expr = self._namespace_expression(expr.false_expr, symbol_mapping)
|
288
|
-
|
289
|
-
return ConditionalExpression(namespaced_condition, namespaced_true_expr, namespaced_false_expr)
|
290
|
-
|
291
|
-
def _namespace_expression_for_lhs(self, expr, symbol_mapping):
|
292
|
-
"""
|
293
|
-
Create a namespaced version of an expression for LHS, returning Variable objects.
|
294
|
-
"""
|
295
|
-
from qnty.expressions import VariableReference
|
296
|
-
|
297
|
-
if isinstance(expr, VariableReference):
|
298
|
-
# VariableReference uses the 'name' property which returns the symbol if available
|
299
|
-
symbol = expr.name
|
300
|
-
if symbol and symbol in symbol_mapping:
|
301
|
-
namespaced_symbol = symbol_mapping[symbol]
|
302
|
-
if namespaced_symbol in self.variables:
|
303
|
-
return self.variables[namespaced_symbol]
|
304
|
-
# If we can't find a mapping, return None since VariableReference doesn't have .equals()
|
305
|
-
return None
|
306
|
-
elif hasattr(expr, 'symbol') and expr.symbol in symbol_mapping:
|
307
|
-
# This is a Variable object
|
308
|
-
namespaced_symbol = symbol_mapping[expr.symbol]
|
309
|
-
if namespaced_symbol in self.variables:
|
310
|
-
return self.variables[namespaced_symbol]
|
311
|
-
return expr
|
312
|
-
else:
|
313
|
-
return expr
|
314
|
-
|
315
|
-
def _should_skip_subproblem_equation(self, equation, namespace: str) -> bool:
|
316
|
-
"""
|
317
|
-
Check if an equation from a sub-problem should be skipped during integration.
|
318
|
-
|
319
|
-
Skip conditional equations for variables that are set to known values in the composed problem.
|
320
|
-
"""
|
321
|
-
try:
|
322
|
-
# Check if this is a conditional equation
|
323
|
-
if not self._is_conditional_equation(equation):
|
324
|
-
return False
|
325
|
-
|
326
|
-
# Check if the LHS variable would be set to a known value in composition
|
327
|
-
original_symbol = self._get_equation_lhs_symbol(equation)
|
328
|
-
if original_symbol is not None:
|
329
|
-
namespaced_symbol = f"{namespace}_{original_symbol}"
|
330
|
-
|
331
|
-
# Check if this namespaced variable exists and is already known
|
332
|
-
if namespaced_symbol in self.variables:
|
333
|
-
var = self.variables[namespaced_symbol]
|
334
|
-
if var.is_known:
|
335
|
-
# The variable is already set to a known value in composition,
|
336
|
-
# so skip the conditional equation that would override it
|
337
|
-
self.logger.debug(f"Skipping conditional equation for {namespaced_symbol} (already known: {var.quantity})")
|
338
|
-
return True
|
339
|
-
|
340
|
-
return False
|
341
|
-
|
342
|
-
except Exception:
|
343
|
-
return False
|
344
|
-
|
345
|
-
def _create_composite_equations(self):
|
346
|
-
"""
|
347
|
-
Create composite equations for common patterns in sub-problems.
|
348
|
-
This handles equations like P = min(header.P, branch.P) automatically.
|
349
|
-
"""
|
350
|
-
if not self.sub_problems:
|
351
|
-
return
|
352
|
-
|
353
|
-
# Common composite patterns to auto-generate
|
354
|
-
for var_name in COMMON_COMPOSITE_VARIABLES:
|
355
|
-
# Check if this variable exists in multiple sub-problems
|
356
|
-
sub_problem_vars = []
|
357
|
-
for namespace in self.sub_problems:
|
358
|
-
namespaced_symbol = f"{namespace}_{var_name}"
|
359
|
-
if namespaced_symbol in self.variables:
|
360
|
-
sub_problem_vars.append(self.variables[namespaced_symbol])
|
361
|
-
|
362
|
-
# If we have the variable in multiple sub-problems and no direct variable exists
|
363
|
-
if len(sub_problem_vars) >= 2 and var_name in self.variables:
|
364
|
-
# Check if a composite equation already exists
|
365
|
-
equation_attr_name = f"{var_name}_eqn"
|
366
|
-
if hasattr(self.__class__, equation_attr_name):
|
367
|
-
# Skip auto-creation since explicit equation exists
|
368
|
-
continue
|
369
|
-
|
370
|
-
# Auto-create composite equation
|
371
|
-
try:
|
372
|
-
from qnty.expressions import min_expr
|
373
|
-
composite_var = self.variables[var_name]
|
374
|
-
if not composite_var.is_known: # Only for unknown variables
|
375
|
-
composite_expr = min_expr(*sub_problem_vars)
|
376
|
-
equals_method = getattr(composite_var, 'equals', None)
|
377
|
-
if equals_method:
|
378
|
-
composite_eq = equals_method(composite_expr)
|
379
|
-
self.add_equation(composite_eq)
|
380
|
-
setattr(self, f"{var_name}_eqn", composite_eq)
|
381
|
-
except Exception as e:
|
382
|
-
self.logger.debug(f"Failed to create composite equation for {var_name}: {e}")
|