qnty 0.0.7__py3-none-any.whl → 0.0.9__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 +140 -58
- qnty/_backup/problem_original.py +1251 -0
- qnty/_backup/quantity.py +63 -0
- qnty/codegen/cli.py +125 -0
- qnty/codegen/generators/data/unit_data.json +8807 -0
- qnty/codegen/generators/data_processor.py +345 -0
- qnty/codegen/generators/dimensions_gen.py +434 -0
- qnty/codegen/generators/doc_generator.py +141 -0
- qnty/codegen/generators/out/dimension_mapping.json +974 -0
- qnty/codegen/generators/out/dimension_metadata.json +123 -0
- qnty/codegen/generators/out/units_metadata.json +223 -0
- qnty/codegen/generators/quantities_gen.py +159 -0
- qnty/codegen/generators/setters_gen.py +178 -0
- qnty/codegen/generators/stubs_gen.py +167 -0
- qnty/codegen/generators/units_gen.py +295 -0
- qnty/codegen/generators/utils/__init__.py +0 -0
- qnty/equations/__init__.py +4 -0
- qnty/equations/equation.py +257 -0
- qnty/equations/system.py +127 -0
- qnty/expressions/__init__.py +61 -0
- qnty/expressions/cache.py +94 -0
- qnty/expressions/functions.py +96 -0
- qnty/expressions/nodes.py +546 -0
- qnty/generated/__init__.py +0 -0
- qnty/generated/dimensions.py +514 -0
- qnty/generated/quantities.py +6003 -0
- qnty/generated/quantities.pyi +4192 -0
- qnty/generated/setters.py +12210 -0
- qnty/generated/units.py +9798 -0
- qnty/problem/__init__.py +91 -0
- qnty/problem/base.py +142 -0
- qnty/problem/composition.py +385 -0
- qnty/problem/composition_mixin.py +382 -0
- qnty/problem/equations.py +413 -0
- qnty/problem/metaclass.py +302 -0
- qnty/problem/reconstruction.py +1016 -0
- qnty/problem/solving.py +180 -0
- qnty/problem/validation.py +64 -0
- qnty/problem/variables.py +239 -0
- qnty/quantities/__init__.py +6 -0
- qnty/quantities/expression_quantity.py +314 -0
- qnty/quantities/quantity.py +428 -0
- qnty/quantities/typed_quantity.py +215 -0
- qnty/solving/__init__.py +0 -0
- qnty/solving/manager.py +90 -0
- qnty/solving/order.py +355 -0
- qnty/solving/solvers/__init__.py +20 -0
- qnty/solving/solvers/base.py +92 -0
- qnty/solving/solvers/iterative.py +185 -0
- qnty/solving/solvers/simultaneous.py +547 -0
- qnty/units/__init__.py +0 -0
- qnty/{prefixes.py → units/prefixes.py} +54 -33
- qnty/{unit.py → units/registry.py} +73 -32
- qnty/utils/__init__.py +0 -0
- qnty/utils/logging.py +40 -0
- qnty/validation/__init__.py +0 -0
- qnty/validation/registry.py +0 -0
- qnty/validation/rules.py +167 -0
- qnty-0.0.9.dist-info/METADATA +199 -0
- qnty-0.0.9.dist-info/RECORD +63 -0
- qnty/dimension.py +0 -186
- qnty/equation.py +0 -216
- qnty/expression.py +0 -492
- qnty/unit_types/base.py +0 -47
- qnty/units.py +0 -8113
- qnty/variable.py +0 -263
- qnty/variable_types/base.py +0 -58
- qnty/variable_types/expression_variable.py +0 -68
- qnty/variable_types/typed_variable.py +0 -87
- qnty/variables.py +0 -2298
- qnty/variables.pyi +0 -6148
- qnty-0.0.7.dist-info/METADATA +0 -355
- qnty-0.0.7.dist-info/RECORD +0 -19
- /qnty/{unit_types → codegen}/__init__.py +0 -0
- /qnty/{variable_types → codegen/generators}/__init__.py +0 -0
- {qnty-0.0.7.dist-info → qnty-0.0.9.dist-info}/WHEEL +0 -0
@@ -0,0 +1,413 @@
|
|
1
|
+
"""
|
2
|
+
Equation processing pipeline for Problem class.
|
3
|
+
|
4
|
+
This module contains all equation-related operations including adding equations,
|
5
|
+
processing equation validation, handling missing variables, and equation reconstruction.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from typing import TYPE_CHECKING, Any
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from qnty.equations import Equation
|
14
|
+
from qnty.quantities import TypeSafeVariable as Variable
|
15
|
+
|
16
|
+
from qnty.equations import Equation
|
17
|
+
|
18
|
+
# Constants for equation processing
|
19
|
+
MATHEMATICAL_OPERATORS = ['+', '-', '*', '/', ' / ', ' * ', ' + ', ' - ']
|
20
|
+
COMMON_COMPOSITE_VARIABLES = ['P', 'c', 'S', 'E', 'W', 'Y']
|
21
|
+
|
22
|
+
|
23
|
+
# Custom Exceptions
|
24
|
+
class EquationValidationError(ValueError):
|
25
|
+
"""Raised when an equation fails validation."""
|
26
|
+
pass
|
27
|
+
|
28
|
+
|
29
|
+
class EquationsMixin:
|
30
|
+
"""Mixin class providing equation management functionality."""
|
31
|
+
|
32
|
+
# These attributes/methods will be provided by other mixins in the final Problem class
|
33
|
+
variables: dict[str, Variable]
|
34
|
+
equations: list[Equation]
|
35
|
+
equation_system: Any
|
36
|
+
sub_problems: dict[str, Any]
|
37
|
+
logger: Any
|
38
|
+
equation_reconstructor: Any
|
39
|
+
|
40
|
+
def _create_placeholder_variable(self, _symbol: str) -> None:
|
41
|
+
"""Will be provided by VariablesMixin."""
|
42
|
+
...
|
43
|
+
|
44
|
+
def add_equation(self, equation: Equation) -> None:
|
45
|
+
"""
|
46
|
+
Add an equation to the problem.
|
47
|
+
|
48
|
+
The equation will be validated to ensure all referenced variables exist.
|
49
|
+
Missing variables that look like simple identifiers will be auto-created
|
50
|
+
as unknown placeholders.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
equation: Equation object to add to the problem
|
54
|
+
|
55
|
+
Raises:
|
56
|
+
EquationValidationError: If the equation is invalid or cannot be processed
|
57
|
+
|
58
|
+
Note:
|
59
|
+
Adding an equation resets the problem to unsolved state.
|
60
|
+
"""
|
61
|
+
if equation is None:
|
62
|
+
raise EquationValidationError("Cannot add None equation to problem")
|
63
|
+
|
64
|
+
# Fix VariableReferences in equation to point to correct Variables
|
65
|
+
equation = self._fix_variable_references(equation)
|
66
|
+
|
67
|
+
# Validate that all variables in the equation exist
|
68
|
+
try:
|
69
|
+
equation_vars = equation.get_all_variables()
|
70
|
+
except Exception as e:
|
71
|
+
raise EquationValidationError(f"Failed to extract variables from equation: {e}") from e
|
72
|
+
|
73
|
+
missing_vars = [var for var in equation_vars if var not in self.variables]
|
74
|
+
|
75
|
+
if missing_vars:
|
76
|
+
self._handle_missing_variables(missing_vars)
|
77
|
+
|
78
|
+
# Check again for remaining missing variables
|
79
|
+
equation_vars = equation.get_all_variables()
|
80
|
+
remaining_missing = [var for var in equation_vars if var not in self.variables]
|
81
|
+
if remaining_missing:
|
82
|
+
self.logger.warning(f"Equation references missing variables: {remaining_missing}")
|
83
|
+
|
84
|
+
self.equations.append(equation)
|
85
|
+
self.equation_system.add_equation(equation)
|
86
|
+
self.is_solved = False
|
87
|
+
|
88
|
+
def add_equations(self, *equations: Equation):
|
89
|
+
"""Add multiple equations to the problem."""
|
90
|
+
for eq in equations:
|
91
|
+
self.add_equation(eq)
|
92
|
+
return self
|
93
|
+
|
94
|
+
def _handle_missing_variables(self, missing_vars: list[str]) -> None:
|
95
|
+
"""Handle missing variables by creating placeholders for simple symbols."""
|
96
|
+
for missing_var in missing_vars:
|
97
|
+
if self._is_simple_variable_symbol(missing_var):
|
98
|
+
self._create_placeholder_variable(missing_var)
|
99
|
+
|
100
|
+
def _is_simple_variable_symbol(self, symbol: str) -> bool:
|
101
|
+
"""Check if a symbol looks like a simple variable identifier."""
|
102
|
+
return (symbol.isidentifier() and
|
103
|
+
not any(char in symbol for char in ['(', ')', '+', '-', '*', '/', ' ']))
|
104
|
+
|
105
|
+
def _process_equation(self, attr_name: str, equation: Equation) -> bool:
|
106
|
+
"""Process a single equation and add it to the problem if valid."""
|
107
|
+
return self._process_equation_impl(attr_name, equation)
|
108
|
+
|
109
|
+
def _process_equation_impl(self, attr_name: str, equation: Equation) -> bool:
|
110
|
+
"""
|
111
|
+
Process a single equation and determine if it should be added.
|
112
|
+
Returns True if the equation was successfully processed.
|
113
|
+
"""
|
114
|
+
# First, update variable references to use symbols instead of names
|
115
|
+
updated_equation = self._update_equation_variable_references(equation)
|
116
|
+
|
117
|
+
# Check if this equation contains delayed expressions
|
118
|
+
try:
|
119
|
+
has_delayed = self.equation_reconstructor.contains_delayed_expressions(updated_equation)
|
120
|
+
if has_delayed:
|
121
|
+
return self._handle_delayed_equation(attr_name, updated_equation)
|
122
|
+
except Exception as e:
|
123
|
+
self.logger.debug(f"Error checking delayed expressions for {attr_name}: {e}")
|
124
|
+
|
125
|
+
# Check if this equation has invalid self-references
|
126
|
+
try:
|
127
|
+
has_self_ref = self._has_invalid_self_references(updated_equation)
|
128
|
+
if has_self_ref:
|
129
|
+
self.logger.debug(f"Skipping invalid self-referencing equation {attr_name}: {updated_equation}")
|
130
|
+
return False
|
131
|
+
except Exception as e:
|
132
|
+
self.logger.debug(f"Error checking self-references for {attr_name}: {e}")
|
133
|
+
|
134
|
+
# Check if equation references non-existent variables
|
135
|
+
try:
|
136
|
+
has_missing = self._equation_has_missing_variables(updated_equation)
|
137
|
+
if has_missing:
|
138
|
+
return self._handle_equation_with_missing_variables(attr_name, updated_equation)
|
139
|
+
except Exception as e:
|
140
|
+
self.logger.debug(f"Error checking missing variables for {attr_name}: {e}")
|
141
|
+
|
142
|
+
# Process valid equation
|
143
|
+
self.add_equation(updated_equation)
|
144
|
+
return True
|
145
|
+
|
146
|
+
def _handle_delayed_equation(self, attr_name: str, equation: Equation) -> bool:
|
147
|
+
"""Handle equations with delayed expressions."""
|
148
|
+
resolved_equation = self.equation_reconstructor.resolve_delayed_equation(equation)
|
149
|
+
if resolved_equation:
|
150
|
+
self.add_equation(resolved_equation)
|
151
|
+
setattr(self, attr_name, resolved_equation)
|
152
|
+
return True
|
153
|
+
else:
|
154
|
+
self.logger.debug(f"Skipping unresolvable delayed equation {attr_name}: {equation}")
|
155
|
+
return False
|
156
|
+
|
157
|
+
def _handle_equation_with_missing_variables(self, attr_name: str, equation: Equation) -> bool:
|
158
|
+
"""Handle equations that reference missing variables."""
|
159
|
+
# Handle conditional equations more carefully
|
160
|
+
if self._is_conditional_equation(equation):
|
161
|
+
return self._handle_conditional_equation(attr_name, equation)
|
162
|
+
|
163
|
+
# Only attempt reconstruction for simple mathematical expressions from composition
|
164
|
+
if self.equation_reconstructor.should_attempt_reconstruction(equation):
|
165
|
+
return self._attempt_equation_reconstruction(attr_name, equation)
|
166
|
+
else:
|
167
|
+
# Skip other problematic equations
|
168
|
+
self.logger.debug(f"Skipping equation with missing variables {attr_name}: {equation}")
|
169
|
+
return False
|
170
|
+
|
171
|
+
def _handle_conditional_equation(self, attr_name: str, equation: Equation) -> bool:
|
172
|
+
"""Handle conditional equations with missing variables."""
|
173
|
+
missing_vars = equation.get_all_variables() - set(self.variables.keys())
|
174
|
+
|
175
|
+
# Skip conditional equations from sub-problems in composed systems
|
176
|
+
if self.sub_problems and self._is_conditional_equation_from_subproblem(equation, attr_name):
|
177
|
+
self.logger.debug(f"Skipping conditional equation {attr_name} from sub-problem in composed system")
|
178
|
+
return False
|
179
|
+
|
180
|
+
# Check for composite expressions that might be reconstructable
|
181
|
+
unresolvable_vars = [var for var in missing_vars
|
182
|
+
if any(op in var for op in MATHEMATICAL_OPERATORS)]
|
183
|
+
|
184
|
+
if self.sub_problems and unresolvable_vars:
|
185
|
+
# Before skipping, try to reconstruct conditional equations with composite expressions
|
186
|
+
self.logger.debug(f"Attempting to reconstruct conditional equation {attr_name} with composite variables: {unresolvable_vars}")
|
187
|
+
reconstructed_equation = self.equation_reconstructor.reconstruct_composite_expressions_generically(equation)
|
188
|
+
if reconstructed_equation:
|
189
|
+
self.logger.debug(f"Successfully reconstructed conditional equation {attr_name}: {reconstructed_equation}")
|
190
|
+
self.add_equation(reconstructed_equation)
|
191
|
+
setattr(self, attr_name, reconstructed_equation)
|
192
|
+
return True
|
193
|
+
else:
|
194
|
+
self.logger.debug(f"Failed to reconstruct conditional equation {attr_name}, trying simple substitution")
|
195
|
+
# Try simple substitution for basic arithmetic expressions
|
196
|
+
reconstructed_equation = self._try_simple_substitution(equation, missing_vars)
|
197
|
+
if reconstructed_equation:
|
198
|
+
self.add_equation(reconstructed_equation)
|
199
|
+
return True
|
200
|
+
else:
|
201
|
+
self.add_equation(equation)
|
202
|
+
return True
|
203
|
+
else:
|
204
|
+
# Try to add the conditional equation even with missing simple variables
|
205
|
+
self.add_equation(equation)
|
206
|
+
return True
|
207
|
+
|
208
|
+
def _try_simple_substitution(self, _equation: Equation, _missing_vars: set[str]) -> Equation | None:
|
209
|
+
"""
|
210
|
+
Try simple substitution for basic arithmetic expressions in conditional equations.
|
211
|
+
|
212
|
+
The real issue is that nested expressions in conditionals aren't being handled properly.
|
213
|
+
For now, just return None and let the equation be added as-is.
|
214
|
+
"""
|
215
|
+
return None
|
216
|
+
|
217
|
+
def _attempt_equation_reconstruction(self, attr_name: str, equation: Equation) -> bool:
|
218
|
+
"""Attempt to reconstruct equations with composite expressions."""
|
219
|
+
missing_vars = equation.get_all_variables() - set(self.variables.keys())
|
220
|
+
self.logger.debug(f"Attempting to reconstruct equation {attr_name} with missing variables: {missing_vars}")
|
221
|
+
|
222
|
+
reconstructed_equation = self.equation_reconstructor.reconstruct_composite_expressions_generically(equation)
|
223
|
+
if reconstructed_equation:
|
224
|
+
self.logger.debug(f"Successfully reconstructed {attr_name}: {reconstructed_equation}")
|
225
|
+
self.add_equation(reconstructed_equation)
|
226
|
+
setattr(self, attr_name, reconstructed_equation)
|
227
|
+
return True
|
228
|
+
else:
|
229
|
+
self.logger.debug(f"Failed to reconstruct equation {attr_name}: {equation}")
|
230
|
+
return False
|
231
|
+
|
232
|
+
def _get_equation_lhs_symbol(self, equation: Equation) -> str | None:
|
233
|
+
"""Safely extract the symbol from equation's left-hand side."""
|
234
|
+
return getattr(equation.lhs, 'symbol', None)
|
235
|
+
|
236
|
+
def _is_conditional_equation(self, equation: Equation) -> bool:
|
237
|
+
"""Check if an equation is a conditional equation."""
|
238
|
+
return 'cond(' in str(equation)
|
239
|
+
|
240
|
+
def _equation_has_missing_variables(self, equation: Equation) -> bool:
|
241
|
+
"""Check if an equation references variables that don't exist in this problem."""
|
242
|
+
try:
|
243
|
+
all_vars = equation.get_all_variables()
|
244
|
+
missing_vars = [var for var in all_vars if var not in self.variables]
|
245
|
+
return len(missing_vars) > 0
|
246
|
+
except Exception:
|
247
|
+
return False
|
248
|
+
|
249
|
+
def _has_invalid_self_references(self, equation: Equation) -> bool:
|
250
|
+
"""
|
251
|
+
Check if an equation has invalid self-references.
|
252
|
+
This catches malformed equations like 'c = max(c, c)' from class definition.
|
253
|
+
"""
|
254
|
+
try:
|
255
|
+
# Get LHS variable - check if it has symbol attribute
|
256
|
+
lhs_symbol = self._get_equation_lhs_symbol(equation)
|
257
|
+
if lhs_symbol is None:
|
258
|
+
return False
|
259
|
+
|
260
|
+
# Get all variables referenced in RHS
|
261
|
+
rhs_vars = equation.rhs.get_variables() if hasattr(equation.rhs, 'get_variables') else set()
|
262
|
+
|
263
|
+
# Check if the LHS variable appears multiple times in RHS (indicating self-reference)
|
264
|
+
# This is a heuristic - a proper implementation would parse the expression tree
|
265
|
+
equation_str = str(equation)
|
266
|
+
if lhs_symbol in rhs_vars:
|
267
|
+
# For conditional equations, self-references are often valid (as fallback values)
|
268
|
+
if 'cond(' in equation_str:
|
269
|
+
return False # Allow self-references in conditional equations
|
270
|
+
|
271
|
+
# Count occurrences of the variable in the equation string
|
272
|
+
count = equation_str.count(lhs_symbol)
|
273
|
+
if count > 2: # LHS + multiple RHS occurrences
|
274
|
+
return True
|
275
|
+
|
276
|
+
return False
|
277
|
+
|
278
|
+
except Exception:
|
279
|
+
return False
|
280
|
+
|
281
|
+
def _is_conditional_equation_from_subproblem(self, equation: Equation, _equation_name: str) -> bool:
|
282
|
+
"""
|
283
|
+
Check if a conditional equation comes from a sub-problem and should be skipped in composed systems.
|
284
|
+
|
285
|
+
In composed systems, if a sub-problem's conditional variable is already set to a known value,
|
286
|
+
we don't want to include the conditional equation that would override that known value.
|
287
|
+
"""
|
288
|
+
try:
|
289
|
+
# Check if this is a conditional equation with a known LHS variable
|
290
|
+
lhs_symbol = self._get_equation_lhs_symbol(equation)
|
291
|
+
if lhs_symbol is not None:
|
292
|
+
|
293
|
+
# Check if the LHS variable already exists and is known
|
294
|
+
if lhs_symbol in self.variables:
|
295
|
+
var = self.variables[lhs_symbol]
|
296
|
+
# If the variable is already known (set explicitly in composition),
|
297
|
+
# and this conditional equation references missing variables, skip it
|
298
|
+
missing_vars = equation.get_all_variables() - set(self.variables.keys())
|
299
|
+
if missing_vars and var.is_known:
|
300
|
+
# This is a sub-problem's conditional equation for a variable that's already known
|
301
|
+
# No point including it since the value is already determined
|
302
|
+
return True
|
303
|
+
|
304
|
+
return False
|
305
|
+
|
306
|
+
except Exception:
|
307
|
+
return False
|
308
|
+
|
309
|
+
def _fix_variable_references(self, equation: Equation) -> Equation:
|
310
|
+
"""
|
311
|
+
Fix VariableReferences in equation expressions to point to Variables in problem.variables.
|
312
|
+
|
313
|
+
This resolves issues where expression trees contain VariableReferences pointing to
|
314
|
+
proxy Variables from class creation time instead of the actual Variables in the problem.
|
315
|
+
"""
|
316
|
+
try:
|
317
|
+
# Fix the RHS expression
|
318
|
+
fixed_rhs = self._fix_expression_variables(equation.rhs)
|
319
|
+
|
320
|
+
# Create new equation with fixed RHS (LHS should already be correct)
|
321
|
+
return Equation(equation.name, equation.lhs, fixed_rhs)
|
322
|
+
|
323
|
+
except Exception as e:
|
324
|
+
self.logger.debug(f"Error fixing variable references in equation {equation.name}: {e}")
|
325
|
+
return equation # Return original if fixing fails
|
326
|
+
|
327
|
+
def _fix_expression_variables(self, expr):
|
328
|
+
"""
|
329
|
+
Recursively fix VariableReferences in an expression tree to point to correct Variables.
|
330
|
+
"""
|
331
|
+
from qnty.expressions import BinaryOperation, Constant, VariableReference
|
332
|
+
|
333
|
+
if isinstance(expr, VariableReference):
|
334
|
+
# Check if this VariableReference points to the wrong Variable
|
335
|
+
symbol = getattr(expr, 'symbol', None)
|
336
|
+
if symbol and symbol in self.variables:
|
337
|
+
correct_var = self.variables[symbol]
|
338
|
+
if expr.variable is not correct_var:
|
339
|
+
# Create new VariableReference pointing to correct Variable
|
340
|
+
return VariableReference(correct_var)
|
341
|
+
return expr
|
342
|
+
|
343
|
+
elif isinstance(expr, BinaryOperation):
|
344
|
+
# Recursively fix left and right operands
|
345
|
+
fixed_left = self._fix_expression_variables(expr.left)
|
346
|
+
fixed_right = self._fix_expression_variables(expr.right)
|
347
|
+
return BinaryOperation(expr.operator, fixed_left, fixed_right)
|
348
|
+
|
349
|
+
elif hasattr(expr, 'operand'):
|
350
|
+
# Recursively fix operand
|
351
|
+
fixed_operand = self._fix_expression_variables(expr.operand)
|
352
|
+
return type(expr)(expr.operator, fixed_operand)
|
353
|
+
|
354
|
+
elif hasattr(expr, 'function_name'):
|
355
|
+
# Recursively fix left and right operands
|
356
|
+
fixed_left = self._fix_expression_variables(expr.left)
|
357
|
+
fixed_right = self._fix_expression_variables(expr.right)
|
358
|
+
return type(expr)(expr.function_name, fixed_left, fixed_right)
|
359
|
+
|
360
|
+
elif isinstance(expr, Constant):
|
361
|
+
return expr
|
362
|
+
|
363
|
+
else:
|
364
|
+
# Unknown expression type, return as-is
|
365
|
+
return expr
|
366
|
+
|
367
|
+
def _update_equation_variable_references(self, equation: Equation) -> Equation:
|
368
|
+
"""Update VariableReference objects in equation to use symbols instead of names."""
|
369
|
+
from qnty.expressions import VariableReference
|
370
|
+
|
371
|
+
# Update LHS if it's a VariableReference
|
372
|
+
updated_lhs = equation.lhs
|
373
|
+
if isinstance(equation.lhs, VariableReference):
|
374
|
+
# Find the variable by name and update to use symbol
|
375
|
+
var_name = equation.lhs.variable.name
|
376
|
+
matching_var = None
|
377
|
+
for var in self.variables.values():
|
378
|
+
if var.name == var_name:
|
379
|
+
matching_var = var
|
380
|
+
break
|
381
|
+
if matching_var and matching_var.symbol:
|
382
|
+
updated_lhs = VariableReference(matching_var)
|
383
|
+
|
384
|
+
# Update RHS by recursively updating expressions
|
385
|
+
updated_rhs = self._update_expression_variable_references(equation.rhs)
|
386
|
+
|
387
|
+
# Create new equation with updated references
|
388
|
+
return Equation(equation.name, updated_lhs, updated_rhs)
|
389
|
+
|
390
|
+
def _update_expression_variable_references(self, expr):
|
391
|
+
"""Recursively update VariableReference objects in expression tree."""
|
392
|
+
from qnty.expressions import BinaryOperation, Constant, VariableReference
|
393
|
+
|
394
|
+
if isinstance(expr, VariableReference):
|
395
|
+
# Find the variable by name and update to use symbol
|
396
|
+
var_name = expr.variable.name
|
397
|
+
matching_var = None
|
398
|
+
for var in self.variables.values():
|
399
|
+
if var.name == var_name:
|
400
|
+
matching_var = var
|
401
|
+
break
|
402
|
+
if matching_var and matching_var.symbol:
|
403
|
+
return VariableReference(matching_var)
|
404
|
+
return expr
|
405
|
+
elif isinstance(expr, BinaryOperation):
|
406
|
+
updated_left = self._update_expression_variable_references(expr.left)
|
407
|
+
updated_right = self._update_expression_variable_references(expr.right)
|
408
|
+
return BinaryOperation(expr.operator, updated_left, updated_right)
|
409
|
+
elif isinstance(expr, Constant):
|
410
|
+
return expr
|
411
|
+
else:
|
412
|
+
# Return unknown expression types as-is
|
413
|
+
return expr
|