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
qnty/problem/reconstruction.py
DELETED
@@ -1,1016 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Equation reconstruction system for handling composite expressions.
|
3
|
-
|
4
|
-
This module provides the advanced equation reconstruction capabilities
|
5
|
-
that allow EngineeringProblem to automatically fix malformed equations
|
6
|
-
created during composition and proxy operations.
|
7
|
-
"""
|
8
|
-
|
9
|
-
import re
|
10
|
-
from logging import Logger
|
11
|
-
from re import Pattern
|
12
|
-
from typing import Any
|
13
|
-
|
14
|
-
from qnty.equations.equation import Equation
|
15
|
-
from qnty.expressions import BinaryOperation, Constant, UnaryFunction, VariableReference, cos, sin
|
16
|
-
from qnty.quantities.expression_quantity import ExpressionQuantity as Variable
|
17
|
-
from qnty.quantities.quantity import Quantity as Qty
|
18
|
-
|
19
|
-
# No BinaryFunction in qnty - operations are handled by BinaryOperation or specific functions
|
20
|
-
|
21
|
-
|
22
|
-
# Type aliases for better readability
|
23
|
-
VariableDict = dict[str, Variable]
|
24
|
-
NamespaceMapping = dict[str, str]
|
25
|
-
ReconstructionResult = Equation | None
|
26
|
-
|
27
|
-
# Type alias for valid expression types that can be used with Variable.equals()
|
28
|
-
# This represents all types that the equals() method accepts as its expression parameter
|
29
|
-
ValidExpressionType = (
|
30
|
-
VariableReference | BinaryOperation | UnaryFunction | Constant |
|
31
|
-
Variable | Qty | int | float
|
32
|
-
)
|
33
|
-
|
34
|
-
# Tuple of types for isinstance() checks - extracted from ValidExpressionType
|
35
|
-
# Note: isinstance() requires a tuple of types, not a Union type, so we maintain both:
|
36
|
-
# - ValidExpressionType: for type annotations and documentation
|
37
|
-
# - VALID_EXPRESSION_TYPES: for runtime isinstance() checks
|
38
|
-
VALID_EXPRESSION_TYPES = (
|
39
|
-
VariableReference, BinaryOperation, UnaryFunction, Constant,
|
40
|
-
Variable, Qty, int, float
|
41
|
-
)
|
42
|
-
|
43
|
-
# Constants for better maintainability and performance
|
44
|
-
EXCLUDED_FUNCTION_NAMES: set[str] = {'sin', 'cos', 'max', 'min', 'exp', 'log', 'sqrt', 'tan'}
|
45
|
-
MATH_OPERATORS: set[str] = {'(', ')', '+', '-', '*', '/'}
|
46
|
-
CONDITIONAL_PATTERNS: set[str] = {'cond('}
|
47
|
-
FUNCTION_PATTERNS: set[str] = {'sin(', 'cos(', 'tan(', 'log(', 'exp('}
|
48
|
-
|
49
|
-
# Compiled regex patterns for performance
|
50
|
-
VARIABLE_PATTERN: Pattern[str] = re.compile(r'\b[A-Za-z][A-Za-z0-9_]*\b')
|
51
|
-
VARIABLE_PATTERN_DETAILED: Pattern[str] = re.compile(r'\b([a-zA-Z_][a-zA-Z0-9_]*)\b')
|
52
|
-
|
53
|
-
|
54
|
-
# Custom exceptions for better error handling
|
55
|
-
class EquationReconstructionError(Exception):
|
56
|
-
"""Base exception for equation reconstruction errors."""
|
57
|
-
pass
|
58
|
-
|
59
|
-
|
60
|
-
class MalformedExpressionError(EquationReconstructionError):
|
61
|
-
"""Raised when expressions are malformed and cannot be reconstructed."""
|
62
|
-
pass
|
63
|
-
|
64
|
-
|
65
|
-
class NamespaceMappingError(EquationReconstructionError):
|
66
|
-
"""Raised when namespace mapping fails."""
|
67
|
-
pass
|
68
|
-
|
69
|
-
|
70
|
-
class PatternReconstructionError(EquationReconstructionError):
|
71
|
-
"""Raised when mathematical pattern reconstruction fails."""
|
72
|
-
pass
|
73
|
-
|
74
|
-
|
75
|
-
class EquationReconstructor:
|
76
|
-
"""
|
77
|
-
Handles reconstruction of equations with composite expressions.
|
78
|
-
|
79
|
-
This class provides advanced equation reconstruction capabilities that allow
|
80
|
-
EngineeringProblem to automatically fix malformed equations created during
|
81
|
-
composition and proxy operations. It uses pattern matching, namespace mapping,
|
82
|
-
and expression parsing to reconstruct valid equations from composite symbols.
|
83
|
-
|
84
|
-
Key Features:
|
85
|
-
- Generic composite expression reconstruction
|
86
|
-
- Malformed equation recovery from proxy operations
|
87
|
-
- Namespace variable mapping and resolution
|
88
|
-
- Mathematical pattern parsing and rebuilding
|
89
|
-
- Performance optimization through caching
|
90
|
-
|
91
|
-
Example Usage:
|
92
|
-
reconstructor = EquationReconstructor(problem)
|
93
|
-
fixed_equation = reconstructor.fix_malformed_equation(broken_equation)
|
94
|
-
"""
|
95
|
-
|
96
|
-
def __init__(self, problem: Any) -> None:
|
97
|
-
"""
|
98
|
-
Initialize the equation reconstructor.
|
99
|
-
|
100
|
-
Args:
|
101
|
-
problem: The EngineeringProblem instance containing variables and logger
|
102
|
-
|
103
|
-
Raises:
|
104
|
-
ValueError: If problem doesn't have required attributes
|
105
|
-
"""
|
106
|
-
if not hasattr(problem, 'variables') or not hasattr(problem, 'logger'):
|
107
|
-
raise ValueError("Problem must have 'variables' and 'logger' attributes")
|
108
|
-
|
109
|
-
self.problem: Any = problem
|
110
|
-
self.variables: VariableDict = problem.variables
|
111
|
-
self.logger: Logger = problem.logger
|
112
|
-
|
113
|
-
# Performance optimization: cache compiled patterns and mappings
|
114
|
-
# Performance optimization: cache compiled patterns and mappings
|
115
|
-
self._namespace_cache: dict[str, set[str]] = {}
|
116
|
-
self._variable_mapping_cache: dict[frozenset, NamespaceMapping] = {}
|
117
|
-
|
118
|
-
# Cache commonly accessed data for performance
|
119
|
-
self._all_variable_names: set[str] | None = None
|
120
|
-
|
121
|
-
def _is_valid_expression_type(self, obj: Any) -> bool:
|
122
|
-
"""
|
123
|
-
Check if an object is a valid expression type for use with Variable.equals().
|
124
|
-
|
125
|
-
Args:
|
126
|
-
obj: The object to check
|
127
|
-
|
128
|
-
Returns:
|
129
|
-
True if the object is a valid expression type
|
130
|
-
"""
|
131
|
-
return isinstance(obj, VALID_EXPRESSION_TYPES)
|
132
|
-
|
133
|
-
def fix_malformed_equation(self, equation: Equation) -> ReconstructionResult:
|
134
|
-
"""
|
135
|
-
Generic method to fix equations that were malformed during class definition.
|
136
|
-
|
137
|
-
Specifically handles composite expressions like '(D - (T - c) * 2.0)' that should
|
138
|
-
reference namespaced variables like 'branch_D', 'branch_T', 'branch_c'.
|
139
|
-
|
140
|
-
Args:
|
141
|
-
equation: The malformed equation to fix
|
142
|
-
|
143
|
-
Returns:
|
144
|
-
Fixed equation if reconstruction succeeds, None otherwise
|
145
|
-
|
146
|
-
Raises:
|
147
|
-
EquationReconstructionError: If equation reconstruction fails with detailed error
|
148
|
-
"""
|
149
|
-
if equation is None:
|
150
|
-
return None
|
151
|
-
|
152
|
-
try:
|
153
|
-
# Get all variables referenced in the equation
|
154
|
-
all_vars = equation.get_all_variables()
|
155
|
-
missing_vars = [var for var in all_vars if var not in self.variables]
|
156
|
-
|
157
|
-
if not missing_vars:
|
158
|
-
return equation # Nothing to fix
|
159
|
-
|
160
|
-
self.logger.debug(f"Found missing variables in equation: {missing_vars}")
|
161
|
-
|
162
|
-
# Attempt to reconstruct equations with composite variables using generic approach
|
163
|
-
fixed_equation = self._reconstruct_composite_expressions(equation, missing_vars)
|
164
|
-
|
165
|
-
if fixed_equation:
|
166
|
-
self.logger.debug(f"Successfully reconstructed equation: {fixed_equation}")
|
167
|
-
return fixed_equation
|
168
|
-
else:
|
169
|
-
self.logger.debug("Failed to reconstruct equation")
|
170
|
-
return None
|
171
|
-
|
172
|
-
except Exception as e:
|
173
|
-
self.logger.debug(f"Error in fix_malformed_equation: {e}")
|
174
|
-
return None
|
175
|
-
|
176
|
-
def _reconstruct_composite_expressions(self, equation: Equation, missing_vars: list[str]) -> ReconstructionResult:
|
177
|
-
"""
|
178
|
-
Generic reconstruction of equations with composite expressions.
|
179
|
-
|
180
|
-
Handles cases where expressions like '(D - (T - c) * 2.0)' need to be
|
181
|
-
mapped to proper namespaced variables by analyzing the structure and
|
182
|
-
finding the best matching variables in available namespaces.
|
183
|
-
|
184
|
-
Args:
|
185
|
-
equation: The equation to reconstruct
|
186
|
-
missing_vars: List of missing variable names
|
187
|
-
|
188
|
-
Returns:
|
189
|
-
Reconstructed equation if successful, None otherwise
|
190
|
-
|
191
|
-
Raises:
|
192
|
-
NamespaceMappingError: If namespace mapping fails
|
193
|
-
"""
|
194
|
-
if not missing_vars:
|
195
|
-
return None
|
196
|
-
|
197
|
-
try:
|
198
|
-
# Extract variable symbols from composite expressions
|
199
|
-
composite_vars = self._extract_base_variables_from_composites(missing_vars)
|
200
|
-
|
201
|
-
if not composite_vars:
|
202
|
-
self.logger.debug("No composite variables found to extract")
|
203
|
-
return None
|
204
|
-
|
205
|
-
# Find which namespaces contain these variables
|
206
|
-
namespace_mappings = self._find_namespace_mappings(composite_vars)
|
207
|
-
|
208
|
-
if not namespace_mappings:
|
209
|
-
self.logger.debug("No namespace mappings found")
|
210
|
-
return None
|
211
|
-
|
212
|
-
# Reconstruct the equation by substituting composite expressions
|
213
|
-
return self._substitute_composite_expressions(equation, missing_vars, namespace_mappings)
|
214
|
-
|
215
|
-
except Exception as e:
|
216
|
-
self.logger.debug(f"Error in _reconstruct_composite_expressions: {e}")
|
217
|
-
return None
|
218
|
-
|
219
|
-
def _extract_base_variables_from_composites(self, missing_vars: list[str]) -> set[str]:
|
220
|
-
"""
|
221
|
-
Extract base variable symbols from composite expressions.
|
222
|
-
|
223
|
-
Args:
|
224
|
-
missing_vars: List of missing variable names from composite expressions
|
225
|
-
|
226
|
-
Returns:
|
227
|
-
Set of base variable symbols found in the expressions
|
228
|
-
|
229
|
-
Example:
|
230
|
-
'(D - (T - c) * 2.0)' -> {'D', 'T', 'c'}
|
231
|
-
"""
|
232
|
-
if not missing_vars:
|
233
|
-
return set()
|
234
|
-
|
235
|
-
base_vars: set[str] = set()
|
236
|
-
|
237
|
-
for missing_var in missing_vars:
|
238
|
-
# Use compiled regex for better performance
|
239
|
-
matches = VARIABLE_PATTERN.findall(missing_var)
|
240
|
-
|
241
|
-
for match in matches:
|
242
|
-
# Filter out obvious non-variable terms using constant set
|
243
|
-
if match not in EXCLUDED_FUNCTION_NAMES:
|
244
|
-
base_vars.add(match)
|
245
|
-
|
246
|
-
return base_vars
|
247
|
-
|
248
|
-
def _find_namespace_mappings(self, base_vars: set[str]) -> NamespaceMapping:
|
249
|
-
"""
|
250
|
-
Find which namespace each base variable should map to.
|
251
|
-
|
252
|
-
Args:
|
253
|
-
base_vars: Set of base variable symbols to map
|
254
|
-
|
255
|
-
Returns:
|
256
|
-
Mapping from base variable names to namespaced variable names
|
257
|
-
|
258
|
-
Example:
|
259
|
-
{'D': 'branch_D', 'T': 'header_T', 'c': 'branch_c'}
|
260
|
-
|
261
|
-
Raises:
|
262
|
-
NamespaceMappingError: If mapping fails for critical variables
|
263
|
-
"""
|
264
|
-
if not base_vars:
|
265
|
-
return {}
|
266
|
-
|
267
|
-
# Use cache key for performance optimization
|
268
|
-
cache_key = frozenset(base_vars)
|
269
|
-
if cache_key in self._variable_mapping_cache:
|
270
|
-
return self._variable_mapping_cache[cache_key]
|
271
|
-
|
272
|
-
mappings: NamespaceMapping = {}
|
273
|
-
|
274
|
-
# For each base variable, find the best namespace match
|
275
|
-
for base_var in base_vars:
|
276
|
-
candidates = self._find_namespace_candidates(base_var)
|
277
|
-
|
278
|
-
# Use heuristics to pick the best candidate
|
279
|
-
if len(candidates) == 1:
|
280
|
-
mappings[base_var] = candidates[0]
|
281
|
-
elif len(candidates) > 1:
|
282
|
-
# If multiple candidates, use context clues or pick first namespace alphabetically
|
283
|
-
best_candidate = sorted(candidates)[0]
|
284
|
-
mappings[base_var] = best_candidate
|
285
|
-
self.logger.debug(f"Multiple candidates for '{base_var}': {candidates}, chose '{best_candidate}'")
|
286
|
-
else:
|
287
|
-
self.logger.debug(f"No candidates found for base variable: {base_var}")
|
288
|
-
|
289
|
-
# Cache the result for performance
|
290
|
-
self._variable_mapping_cache[cache_key] = mappings
|
291
|
-
return mappings
|
292
|
-
|
293
|
-
def _find_namespace_candidates(self, base_var: str) -> list[str]:
|
294
|
-
"""
|
295
|
-
Find all possible namespace candidates for a base variable.
|
296
|
-
|
297
|
-
Args:
|
298
|
-
base_var: Base variable name to find candidates for
|
299
|
-
|
300
|
-
Returns:
|
301
|
-
List of candidate namespaced variable names
|
302
|
-
"""
|
303
|
-
candidates = []
|
304
|
-
|
305
|
-
# Cache variable names for performance
|
306
|
-
if self._all_variable_names is None:
|
307
|
-
self._all_variable_names = set(self.variables.keys())
|
308
|
-
|
309
|
-
# Look for exact matches in namespaced variables
|
310
|
-
for var_name in self._all_variable_names:
|
311
|
-
if '_' in var_name:
|
312
|
-
_, var_part = var_name.split('_', 1) # namespace not needed here
|
313
|
-
if var_part == base_var:
|
314
|
-
candidates.append(var_name)
|
315
|
-
|
316
|
-
return candidates
|
317
|
-
|
318
|
-
def _clear_caches(self) -> None:
|
319
|
-
"""
|
320
|
-
Clear all internal caches. Should be called when variables change.
|
321
|
-
|
322
|
-
This method provides a way to reset cached data when the problem
|
323
|
-
state changes, ensuring cache consistency.
|
324
|
-
"""
|
325
|
-
self._namespace_cache.clear()
|
326
|
-
self._variable_mapping_cache.clear()
|
327
|
-
self._all_variable_names = None
|
328
|
-
|
329
|
-
def _substitute_composite_expressions(self, equation: Equation, missing_vars: list[str], namespace_mappings: NamespaceMapping) -> ReconstructionResult:
|
330
|
-
"""
|
331
|
-
Substitute composite expressions with properly namespaced variables.
|
332
|
-
|
333
|
-
Args:
|
334
|
-
equation: The equation to substitute expressions in
|
335
|
-
missing_vars: List of missing variable names
|
336
|
-
namespace_mappings: Mapping from base variables to namespaced variables
|
337
|
-
|
338
|
-
Returns:
|
339
|
-
Reconstructed equation if successful, None otherwise
|
340
|
-
"""
|
341
|
-
if not missing_vars or not namespace_mappings:
|
342
|
-
return None
|
343
|
-
|
344
|
-
try:
|
345
|
-
# Get the equation string representation for debugging
|
346
|
-
eq_str = str(equation)
|
347
|
-
self.logger.debug(f"Substituting expressions in equation: {eq_str}")
|
348
|
-
|
349
|
-
# For each missing composite expression, try to rebuild it
|
350
|
-
for missing_var in missing_vars:
|
351
|
-
if missing_var in eq_str:
|
352
|
-
reconstructed_expr = self._reconstruct_expression_from_mapping(missing_var, namespace_mappings)
|
353
|
-
if reconstructed_expr:
|
354
|
-
# Replace the original equation's RHS or LHS
|
355
|
-
if isinstance(equation.lhs, VariableReference):
|
356
|
-
var_name = equation.lhs.name
|
357
|
-
if var_name and var_name in self.variables:
|
358
|
-
lhs_var = self.variables[var_name]
|
359
|
-
return lhs_var.equals(reconstructed_expr)
|
360
|
-
elif hasattr(equation.lhs, 'symbol'):
|
361
|
-
symbol = getattr(equation.lhs, 'symbol', None)
|
362
|
-
if symbol and symbol in self.variables:
|
363
|
-
lhs_var = self.variables[symbol]
|
364
|
-
return lhs_var.equals(reconstructed_expr)
|
365
|
-
|
366
|
-
return None
|
367
|
-
|
368
|
-
except Exception as e:
|
369
|
-
self.logger.debug(f"Error in _substitute_composite_expressions: {e}")
|
370
|
-
return None
|
371
|
-
|
372
|
-
def _reconstruct_expression_from_mapping(self, composite_expr: str, namespace_mappings: NamespaceMapping) -> Any | None:
|
373
|
-
"""
|
374
|
-
Reconstruct a composite expression using the namespace mappings.
|
375
|
-
|
376
|
-
This method now uses generic parsing instead of hardcoded patterns.
|
377
|
-
|
378
|
-
Args:
|
379
|
-
composite_expr: The composite expression string to reconstruct
|
380
|
-
namespace_mappings: Mapping from base variables to namespaced variables
|
381
|
-
|
382
|
-
Returns:
|
383
|
-
Reconstructed expression if successful, None otherwise
|
384
|
-
"""
|
385
|
-
if not composite_expr or not namespace_mappings:
|
386
|
-
return None
|
387
|
-
|
388
|
-
try:
|
389
|
-
# Create a substitution pattern for the expression
|
390
|
-
substituted_expr = composite_expr
|
391
|
-
|
392
|
-
# Replace base variable names with their namespaced counterparts
|
393
|
-
for base_var, namespaced_var in namespace_mappings.items():
|
394
|
-
if namespaced_var in self.variables:
|
395
|
-
# Use word boundary regex to avoid partial matches
|
396
|
-
import re
|
397
|
-
pattern = r'\b' + re.escape(base_var) + r'\b'
|
398
|
-
substituted_expr = re.sub(pattern, namespaced_var, substituted_expr)
|
399
|
-
|
400
|
-
# Try to evaluate the substituted expression using our generic parser
|
401
|
-
return self.parse_composite_expression_pattern(substituted_expr)
|
402
|
-
|
403
|
-
except Exception as e:
|
404
|
-
self.logger.debug(f"Error in generic expression reconstruction: {e}")
|
405
|
-
return None
|
406
|
-
|
407
|
-
def _matches_common_pattern(self, expression: str) -> bool:
|
408
|
-
"""
|
409
|
-
Check if expression matches patterns that can be reconstructed.
|
410
|
-
|
411
|
-
Args:
|
412
|
-
expression: The expression string to check
|
413
|
-
|
414
|
-
Returns:
|
415
|
-
True if expression contains mathematical operators and variables
|
416
|
-
"""
|
417
|
-
# Generic check - any expression with mathematical operators and variable names
|
418
|
-
return (any(char in expression for char in '+-*/()') and
|
419
|
-
any(char.isalpha() for char in expression))
|
420
|
-
|
421
|
-
def contains_delayed_expressions(self, equation: Equation) -> bool:
|
422
|
-
"""
|
423
|
-
Check if an equation contains delayed expressions that need resolution.
|
424
|
-
|
425
|
-
Args:
|
426
|
-
equation: The equation to check for delayed expressions
|
427
|
-
|
428
|
-
Returns:
|
429
|
-
True if equation contains delayed expressions
|
430
|
-
"""
|
431
|
-
if equation is None:
|
432
|
-
return False
|
433
|
-
|
434
|
-
try:
|
435
|
-
# Check if the RHS contains delayed expressions
|
436
|
-
return self._expression_has_delayed_components(equation.rhs)
|
437
|
-
except Exception as e:
|
438
|
-
self.logger.debug(f"Error checking delayed expressions: {e}")
|
439
|
-
return False
|
440
|
-
|
441
|
-
def _expression_has_delayed_components(self, expr: Any) -> bool:
|
442
|
-
"""
|
443
|
-
Recursively check if an expression contains delayed components.
|
444
|
-
|
445
|
-
Args:
|
446
|
-
expr: The expression to check
|
447
|
-
|
448
|
-
Returns:
|
449
|
-
True if expression contains delayed components
|
450
|
-
"""
|
451
|
-
if expr is None:
|
452
|
-
return False
|
453
|
-
|
454
|
-
if hasattr(expr, 'resolve'):
|
455
|
-
# This is a delayed component
|
456
|
-
return True
|
457
|
-
|
458
|
-
# Check if it's an equation with delayed RHS
|
459
|
-
if hasattr(expr, 'rhs') and hasattr(expr.rhs, 'resolve'):
|
460
|
-
return True
|
461
|
-
|
462
|
-
# For expressions with operands, check recursively
|
463
|
-
if hasattr(expr, 'left') and hasattr(expr, 'right'):
|
464
|
-
return (self._expression_has_delayed_components(expr.left) or
|
465
|
-
self._expression_has_delayed_components(expr.right))
|
466
|
-
|
467
|
-
if hasattr(expr, 'operand'):
|
468
|
-
return self._expression_has_delayed_components(expr.operand)
|
469
|
-
|
470
|
-
if hasattr(expr, 'args'):
|
471
|
-
return any(self._expression_has_delayed_components(arg) for arg in expr.args)
|
472
|
-
|
473
|
-
return False
|
474
|
-
|
475
|
-
def resolve_delayed_equation(self, equation: Equation) -> ReconstructionResult:
|
476
|
-
"""
|
477
|
-
Resolve a delayed equation by evaluating its delayed expressions.
|
478
|
-
|
479
|
-
Args:
|
480
|
-
equation: The equation with delayed expressions to resolve
|
481
|
-
|
482
|
-
Returns:
|
483
|
-
Resolved equation if successful, None otherwise
|
484
|
-
"""
|
485
|
-
if equation is None:
|
486
|
-
return None
|
487
|
-
|
488
|
-
try:
|
489
|
-
# Create context with all current variables
|
490
|
-
context = self.variables.copy()
|
491
|
-
|
492
|
-
# If the RHS is delayed, resolve it
|
493
|
-
if hasattr(equation.rhs, 'resolve'):
|
494
|
-
resolve_method = getattr(equation.rhs, 'resolve', None)
|
495
|
-
if callable(resolve_method):
|
496
|
-
resolved_rhs = resolve_method(context)
|
497
|
-
if resolved_rhs:
|
498
|
-
# Get the left-hand side variable
|
499
|
-
lhs_var = None
|
500
|
-
if isinstance(equation.lhs, VariableReference):
|
501
|
-
var_name = equation.lhs.name
|
502
|
-
if var_name in context:
|
503
|
-
lhs_var = context[var_name]
|
504
|
-
elif hasattr(equation.lhs, 'symbol'):
|
505
|
-
symbol = getattr(equation.lhs, 'symbol', None)
|
506
|
-
if isinstance(symbol, str) and symbol in context:
|
507
|
-
lhs_var = context[symbol]
|
508
|
-
|
509
|
-
if lhs_var:
|
510
|
-
# Type check resolved_rhs for safety
|
511
|
-
if self._is_valid_expression_type(resolved_rhs):
|
512
|
-
# Safe to use - type checker knows this is valid
|
513
|
-
typed_rhs: ValidExpressionType = resolved_rhs # type: ignore[assignment]
|
514
|
-
return lhs_var.equals(typed_rhs)
|
515
|
-
else:
|
516
|
-
self.logger.debug(f"Resolved RHS has invalid type: {type(resolved_rhs)}")
|
517
|
-
return None
|
518
|
-
|
519
|
-
return None
|
520
|
-
|
521
|
-
except Exception as e:
|
522
|
-
self.logger.debug(f"Error resolving delayed equation: {e}")
|
523
|
-
return None
|
524
|
-
|
525
|
-
def should_attempt_reconstruction(self, equation: Equation) -> bool:
|
526
|
-
"""
|
527
|
-
Determine if we should attempt to reconstruct this equation.
|
528
|
-
|
529
|
-
Only attempt reconstruction for simple mathematical expressions,
|
530
|
-
not complex structures like conditionals.
|
531
|
-
|
532
|
-
Args:
|
533
|
-
equation: The equation to evaluate for reconstruction
|
534
|
-
|
535
|
-
Returns:
|
536
|
-
True if reconstruction should be attempted
|
537
|
-
"""
|
538
|
-
if equation is None:
|
539
|
-
return False
|
540
|
-
|
541
|
-
try:
|
542
|
-
equation_str = str(equation)
|
543
|
-
|
544
|
-
# Skip conditional equations using constant set
|
545
|
-
if any(pattern in equation_str for pattern in CONDITIONAL_PATTERNS):
|
546
|
-
return False
|
547
|
-
|
548
|
-
# Skip equations with complex function calls
|
549
|
-
if any(func in equation_str for func in FUNCTION_PATTERNS):
|
550
|
-
# These might be complex - only attempt if they're in the problematic patterns
|
551
|
-
self.logger.debug(f"Equation contains complex functions: {equation_str}")
|
552
|
-
pass
|
553
|
-
|
554
|
-
# Only attempt if the missing variables look like mathematical expressions
|
555
|
-
all_vars = equation.get_all_variables()
|
556
|
-
missing_vars = [var for var in all_vars if var not in self.variables]
|
557
|
-
|
558
|
-
for missing_var in missing_vars:
|
559
|
-
# Check if this looks like a mathematical expression we can handle
|
560
|
-
if any(char in missing_var for char in MATH_OPERATORS):
|
561
|
-
return True
|
562
|
-
|
563
|
-
return False
|
564
|
-
|
565
|
-
except Exception as e:
|
566
|
-
self.logger.debug(f"Error in should_attempt_reconstruction: {e}")
|
567
|
-
return False
|
568
|
-
|
569
|
-
def reconstruct_composite_expressions_generically(self, equation: Equation) -> ReconstructionResult:
|
570
|
-
"""
|
571
|
-
Generically reconstruct equations with composite expressions by parsing the
|
572
|
-
composite symbols and rebuilding them from existing variables.
|
573
|
-
|
574
|
-
Enhanced to handle malformed expressions from proxy evaluation.
|
575
|
-
|
576
|
-
Args:
|
577
|
-
equation: The equation to reconstruct
|
578
|
-
|
579
|
-
Returns:
|
580
|
-
Reconstructed equation if successful, None otherwise
|
581
|
-
|
582
|
-
Raises:
|
583
|
-
MalformedExpressionError: If expressions are too malformed to reconstruct
|
584
|
-
"""
|
585
|
-
if equation is None:
|
586
|
-
return None
|
587
|
-
|
588
|
-
try:
|
589
|
-
all_vars = equation.get_all_variables()
|
590
|
-
missing_vars = [var for var in all_vars if var not in self.variables]
|
591
|
-
|
592
|
-
if not missing_vars:
|
593
|
-
return equation
|
594
|
-
|
595
|
-
# Get the LHS variable with proper validation
|
596
|
-
lhs_var = self._get_lhs_variable(equation)
|
597
|
-
if lhs_var is None:
|
598
|
-
return None
|
599
|
-
|
600
|
-
# Check for malformed expressions that contain evaluated numeric values
|
601
|
-
malformed_vars = self._identify_malformed_variables(missing_vars)
|
602
|
-
|
603
|
-
if malformed_vars:
|
604
|
-
# This is a malformed expression from proxy evaluation
|
605
|
-
reconstructed_rhs = self._reconstruct_malformed_proxy_expression(equation, malformed_vars)
|
606
|
-
if reconstructed_rhs:
|
607
|
-
return lhs_var.equals(reconstructed_rhs)
|
608
|
-
return None
|
609
|
-
|
610
|
-
# Reconstruct the RHS by parsing and rebuilding composite expressions
|
611
|
-
reconstructed_rhs = self.parse_and_rebuild_expression(equation.rhs, missing_vars)
|
612
|
-
|
613
|
-
if reconstructed_rhs:
|
614
|
-
return lhs_var.equals(reconstructed_rhs)
|
615
|
-
|
616
|
-
return None
|
617
|
-
|
618
|
-
except Exception as e:
|
619
|
-
self.logger.debug(f"Reconstruction failed: {e}")
|
620
|
-
return None
|
621
|
-
|
622
|
-
def _get_lhs_variable(self, equation: Equation) -> Variable | None:
|
623
|
-
"""
|
624
|
-
Safely extract the left-hand side variable from an equation.
|
625
|
-
|
626
|
-
Args:
|
627
|
-
equation: The equation to extract LHS from
|
628
|
-
|
629
|
-
Returns:
|
630
|
-
The LHS variable if valid, None otherwise
|
631
|
-
"""
|
632
|
-
# Check if lhs is a VariableReference
|
633
|
-
if isinstance(equation.lhs, VariableReference):
|
634
|
-
var_name = equation.lhs.name
|
635
|
-
if var_name in self.variables:
|
636
|
-
return self.variables[var_name]
|
637
|
-
# Check if lhs is a Variable with symbol attribute
|
638
|
-
elif hasattr(equation.lhs, 'symbol'):
|
639
|
-
symbol = getattr(equation.lhs, 'symbol', None)
|
640
|
-
if isinstance(symbol, str) and symbol in self.variables:
|
641
|
-
return self.variables[symbol]
|
642
|
-
|
643
|
-
return None
|
644
|
-
|
645
|
-
def _identify_malformed_variables(self, missing_vars: list[str]) -> list[str]:
|
646
|
-
"""
|
647
|
-
Identify variables that are malformed due to proxy evaluation.
|
648
|
-
|
649
|
-
Args:
|
650
|
-
missing_vars: List of missing variable names
|
651
|
-
|
652
|
-
Returns:
|
653
|
-
List of malformed variable names
|
654
|
-
"""
|
655
|
-
# Look for missing variables that have composite patterns (parentheses and operators)
|
656
|
-
return [var for var in missing_vars
|
657
|
-
if ('(' in var and ')' in var and
|
658
|
-
any(op in var for op in MATH_OPERATORS))]
|
659
|
-
|
660
|
-
def _reconstruct_malformed_proxy_expression(self, equation: Equation, malformed_vars: list[str]) -> Any | None: # noqa: ARG002
|
661
|
-
"""
|
662
|
-
Generically reconstruct expressions that were malformed due to proxy evaluation.
|
663
|
-
|
664
|
-
Args:
|
665
|
-
equation: The equation containing malformed expressions
|
666
|
-
malformed_vars: List of malformed variable names (kept for signature compatibility)
|
667
|
-
|
668
|
-
Returns:
|
669
|
-
Reconstructed expression if successful, None otherwise
|
670
|
-
|
671
|
-
Note:
|
672
|
-
Malformed variables look like: "(var1 - (var2 - var3) * 2.0) = 1.315 in"
|
673
|
-
We extract the mathematical pattern and rebuild it symbolically using existing variables.
|
674
|
-
The malformed_vars parameter is kept for potential future use and API consistency.
|
675
|
-
"""
|
676
|
-
eq_str = str(equation)
|
677
|
-
self.logger.debug(f"Reconstructing malformed equation: {eq_str}")
|
678
|
-
|
679
|
-
try:
|
680
|
-
# Extract the RHS expression from the equation
|
681
|
-
if hasattr(equation, 'rhs'):
|
682
|
-
rhs_expr = equation.rhs
|
683
|
-
return self._rebuild_expression_from_malformed(rhs_expr)
|
684
|
-
except Exception as e:
|
685
|
-
self.logger.debug(f"Failed to reconstruct malformed expression: {e}")
|
686
|
-
|
687
|
-
return None
|
688
|
-
|
689
|
-
def _rebuild_expression_from_malformed(self, expr: Any) -> Any | None:
|
690
|
-
"""
|
691
|
-
Recursively rebuild an expression that contains malformed variable references.
|
692
|
-
"""
|
693
|
-
if isinstance(expr, VariableReference):
|
694
|
-
# Check if this is a malformed variable reference
|
695
|
-
var_symbol = expr.name
|
696
|
-
if ' = ' in var_symbol:
|
697
|
-
# This is malformed - try to extract the original pattern
|
698
|
-
return self.parse_malformed_variable_pattern(var_symbol)
|
699
|
-
elif var_symbol in self.variables:
|
700
|
-
return expr
|
701
|
-
elif (any(op in var_symbol for op in ['+', '-', '*', '/']) and
|
702
|
-
any(char.isalpha() for char in var_symbol) and var_symbol.count('_') >= 1):
|
703
|
-
# This is a composite expression pattern - try to parse and rebuild it
|
704
|
-
return self.parse_composite_expression_pattern(var_symbol)
|
705
|
-
else:
|
706
|
-
return None
|
707
|
-
|
708
|
-
elif hasattr(expr, 'symbol') and isinstance(getattr(expr, 'symbol', None), str):
|
709
|
-
# This might be a malformed Variable object (not VariableReference)
|
710
|
-
var_symbol = expr.symbol
|
711
|
-
if ' = ' in var_symbol:
|
712
|
-
# This is malformed - try to extract the original pattern
|
713
|
-
return self.parse_malformed_variable_pattern(var_symbol)
|
714
|
-
elif var_symbol in self.variables:
|
715
|
-
return self.variables[var_symbol]
|
716
|
-
else:
|
717
|
-
return None
|
718
|
-
|
719
|
-
elif isinstance(expr, BinaryOperation):
|
720
|
-
# Recursively rebuild operands
|
721
|
-
left_rebuilt = self._rebuild_expression_from_malformed(expr.left)
|
722
|
-
right_rebuilt = self._rebuild_expression_from_malformed(expr.right)
|
723
|
-
|
724
|
-
if left_rebuilt and right_rebuilt:
|
725
|
-
return BinaryOperation(expr.operator, left_rebuilt, right_rebuilt)
|
726
|
-
|
727
|
-
elif isinstance(expr, UnaryFunction):
|
728
|
-
operand_rebuilt = self._rebuild_expression_from_malformed(expr.operand)
|
729
|
-
if operand_rebuilt:
|
730
|
-
if expr.function_name == 'sin':
|
731
|
-
return sin(operand_rebuilt)
|
732
|
-
elif expr.function_name == 'cos':
|
733
|
-
return cos(operand_rebuilt)
|
734
|
-
else:
|
735
|
-
return UnaryFunction(expr.function_name, operand_rebuilt)
|
736
|
-
|
737
|
-
elif isinstance(expr, Constant):
|
738
|
-
return expr
|
739
|
-
|
740
|
-
return None
|
741
|
-
|
742
|
-
def parse_composite_expression_pattern(self, composite_symbol: str) -> Any | None:
|
743
|
-
"""
|
744
|
-
Parse a composite expression pattern and reconstruct it using available variables.
|
745
|
-
|
746
|
-
Args:
|
747
|
-
composite_symbol: The composite expression string to parse
|
748
|
-
|
749
|
-
Returns:
|
750
|
-
Reconstructed expression if successful, None otherwise
|
751
|
-
|
752
|
-
Examples:
|
753
|
-
- "(branch_D - (branch_T_n - branch_c) * 2.0)" -> branch_D - 2.0 * (branch_T_n - branch_c)
|
754
|
-
- "(header_T - header_c) * 2.5" -> (header_T - header_c) * 2.5
|
755
|
-
- "d_2 * 2.0" -> d_2 * 2.0
|
756
|
-
- "S_r / header_S" -> S_r / header_S
|
757
|
-
"""
|
758
|
-
if not composite_symbol:
|
759
|
-
return None
|
760
|
-
|
761
|
-
pattern = composite_symbol
|
762
|
-
|
763
|
-
# Handle simple patterns first (variable op constant/variable)
|
764
|
-
simple_result = self._handle_simple_composite_patterns(pattern)
|
765
|
-
if simple_result:
|
766
|
-
return simple_result
|
767
|
-
|
768
|
-
# Remove outer parentheses if the entire pattern is wrapped
|
769
|
-
pattern = self._remove_outer_parentheses(pattern)
|
770
|
-
|
771
|
-
# Extract variable names that exist in our system using compiled regex
|
772
|
-
potential_vars = VARIABLE_PATTERN_DETAILED.findall(pattern)
|
773
|
-
existing_vars = [var for var in potential_vars if var in self.variables]
|
774
|
-
|
775
|
-
if len(existing_vars) < 1:
|
776
|
-
self.logger.debug(f"No existing variables found in pattern: {pattern}")
|
777
|
-
return None
|
778
|
-
|
779
|
-
# Try to rebuild the mathematical pattern
|
780
|
-
return self._rebuild_mathematical_pattern(pattern, existing_vars)
|
781
|
-
|
782
|
-
def _handle_simple_composite_patterns(self, pattern: str) -> Any | None:
|
783
|
-
"""
|
784
|
-
Handle simple composite patterns like 'var * const' or 'var1 / var2'.
|
785
|
-
|
786
|
-
Args:
|
787
|
-
pattern: The pattern string to handle
|
788
|
-
|
789
|
-
Returns:
|
790
|
-
Reconstructed expression if successful, None otherwise
|
791
|
-
"""
|
792
|
-
pattern = pattern.strip()
|
793
|
-
|
794
|
-
# Handle patterns like "d_2 * 2.0"
|
795
|
-
if ' * ' in pattern:
|
796
|
-
parts = pattern.split(' * ', 1)
|
797
|
-
if len(parts) == 2:
|
798
|
-
left_part, right_part = parts
|
799
|
-
left_part = left_part.strip()
|
800
|
-
right_part = right_part.strip()
|
801
|
-
|
802
|
-
# Check if left is variable and right is number
|
803
|
-
if left_part in self.variables:
|
804
|
-
try:
|
805
|
-
right_value = float(right_part)
|
806
|
-
left_var_ref = VariableReference(self.variables[left_part])
|
807
|
-
return left_var_ref * right_value
|
808
|
-
except ValueError:
|
809
|
-
# Right part is not a number, check if it's a variable
|
810
|
-
if right_part in self.variables:
|
811
|
-
left_var_ref = VariableReference(self.variables[left_part])
|
812
|
-
right_var_ref = VariableReference(self.variables[right_part])
|
813
|
-
return left_var_ref * right_var_ref
|
814
|
-
|
815
|
-
# Handle patterns like "S_r / header_S"
|
816
|
-
elif ' / ' in pattern:
|
817
|
-
parts = pattern.split(' / ', 1)
|
818
|
-
if len(parts) == 2:
|
819
|
-
left_part, right_part = parts
|
820
|
-
left_part = left_part.strip()
|
821
|
-
right_part = right_part.strip()
|
822
|
-
|
823
|
-
# Check if both are variables
|
824
|
-
if left_part in self.variables and right_part in self.variables:
|
825
|
-
left_var_ref = VariableReference(self.variables[left_part])
|
826
|
-
right_var_ref = VariableReference(self.variables[right_part])
|
827
|
-
return left_var_ref / right_var_ref
|
828
|
-
|
829
|
-
# Check if left is variable and right is number
|
830
|
-
elif left_part in self.variables:
|
831
|
-
try:
|
832
|
-
right_value = float(right_part)
|
833
|
-
left_var_ref = VariableReference(self.variables[left_part])
|
834
|
-
return left_var_ref / right_value
|
835
|
-
except ValueError:
|
836
|
-
pass
|
837
|
-
|
838
|
-
# Handle patterns like "var + const", "var - const"
|
839
|
-
elif ' + ' in pattern or ' - ' in pattern:
|
840
|
-
# Find the operator
|
841
|
-
if ' + ' in pattern:
|
842
|
-
operator = '+'
|
843
|
-
parts = pattern.split(' + ', 1)
|
844
|
-
else:
|
845
|
-
operator = '-'
|
846
|
-
parts = pattern.split(' - ', 1)
|
847
|
-
|
848
|
-
if len(parts) == 2:
|
849
|
-
left_part, right_part = parts
|
850
|
-
left_part = left_part.strip()
|
851
|
-
right_part = right_part.strip()
|
852
|
-
|
853
|
-
if left_part in self.variables:
|
854
|
-
left_var_ref = VariableReference(self.variables[left_part])
|
855
|
-
|
856
|
-
# Try as number first
|
857
|
-
try:
|
858
|
-
right_value = float(right_part)
|
859
|
-
if operator == '+':
|
860
|
-
return left_var_ref + right_value
|
861
|
-
else:
|
862
|
-
return left_var_ref - right_value
|
863
|
-
except ValueError:
|
864
|
-
# Try as variable
|
865
|
-
if right_part in self.variables:
|
866
|
-
right_var_ref = VariableReference(self.variables[right_part])
|
867
|
-
if operator == '+':
|
868
|
-
return left_var_ref + right_var_ref
|
869
|
-
else:
|
870
|
-
return left_var_ref - right_var_ref
|
871
|
-
|
872
|
-
return None
|
873
|
-
|
874
|
-
def _remove_outer_parentheses(self, pattern: str) -> str:
|
875
|
-
"""
|
876
|
-
Remove outer parentheses if they wrap the entire expression.
|
877
|
-
|
878
|
-
Args:
|
879
|
-
pattern: The pattern string to process
|
880
|
-
|
881
|
-
Returns:
|
882
|
-
Pattern with outer parentheses removed if appropriate
|
883
|
-
"""
|
884
|
-
if not pattern.startswith('(') or not pattern.endswith(')'):
|
885
|
-
return pattern
|
886
|
-
|
887
|
-
# Count parentheses to make sure we're removing the outermost pair
|
888
|
-
paren_count = 0
|
889
|
-
for char in pattern[1:-1]:
|
890
|
-
if char == '(':
|
891
|
-
paren_count += 1
|
892
|
-
elif char == ')':
|
893
|
-
paren_count -= 1
|
894
|
-
if paren_count < 0:
|
895
|
-
return pattern # Don't remove - they don't wrap everything
|
896
|
-
|
897
|
-
# We made it through without breaking, so the outer parens wrap everything
|
898
|
-
return pattern[1:-1]
|
899
|
-
|
900
|
-
def parse_malformed_variable_pattern(self, malformed_symbol: str) -> Any | None:
|
901
|
-
"""
|
902
|
-
Parse a malformed variable symbol and reconstruct it using available variables.
|
903
|
-
|
904
|
-
Args:
|
905
|
-
malformed_symbol: The malformed variable symbol to parse
|
906
|
-
|
907
|
-
Returns:
|
908
|
-
Reconstructed expression if successful, None otherwise
|
909
|
-
|
910
|
-
Examples:
|
911
|
-
- "(var1 - (var2 - var3) * 2.0) = 1.315 in" -> var1 - 2.0 * (var2 - var3)
|
912
|
-
- "(var1 + var2) = 0.397 in" -> var1 + var2
|
913
|
-
"""
|
914
|
-
if not malformed_symbol or ' = ' not in malformed_symbol:
|
915
|
-
return None
|
916
|
-
|
917
|
-
pattern = malformed_symbol.split(' = ')[0].strip()
|
918
|
-
|
919
|
-
# Remove outer parentheses if present
|
920
|
-
pattern = self._remove_outer_parentheses(pattern)
|
921
|
-
|
922
|
-
# Extract variable names that exist in our system using compiled regex
|
923
|
-
potential_vars = VARIABLE_PATTERN_DETAILED.findall(pattern)
|
924
|
-
existing_vars = [var for var in potential_vars if var in self.variables]
|
925
|
-
|
926
|
-
if len(existing_vars) < 2:
|
927
|
-
self.logger.debug(f"Insufficient variables in malformed pattern: {pattern}")
|
928
|
-
return None
|
929
|
-
|
930
|
-
# Try to rebuild common mathematical patterns
|
931
|
-
return self._rebuild_mathematical_pattern(pattern, existing_vars)
|
932
|
-
|
933
|
-
def _rebuild_mathematical_pattern(self, pattern: str, existing_vars: list[str]) -> Any | None:
|
934
|
-
"""
|
935
|
-
Rebuild mathematical expressions from string patterns using existing variables.
|
936
|
-
|
937
|
-
Args:
|
938
|
-
pattern: The mathematical pattern string to rebuild
|
939
|
-
existing_vars: List of existing variable names to use
|
940
|
-
|
941
|
-
Returns:
|
942
|
-
Reconstructed expression if successful, None otherwise
|
943
|
-
|
944
|
-
Note:
|
945
|
-
Uses eval() in a controlled namespace with only VariableReference objects
|
946
|
-
to ensure we get Expression objects instead of evaluated Variables.
|
947
|
-
"""
|
948
|
-
if not pattern or not existing_vars:
|
949
|
-
return None
|
950
|
-
|
951
|
-
try:
|
952
|
-
# Build secure namespace with VariableReference objects
|
953
|
-
namespace: dict[str, Any] = {'__builtins__': {}}
|
954
|
-
|
955
|
-
# Add VariableReference objects to namespace for secure evaluation
|
956
|
-
for var_name in existing_vars:
|
957
|
-
if var_name in self.variables:
|
958
|
-
# Create VariableReference to get Expression objects instead of raw values
|
959
|
-
var_ref = VariableReference(self.variables[var_name])
|
960
|
-
namespace[var_name] = var_ref
|
961
|
-
else:
|
962
|
-
self.logger.debug(f"Variable '{var_name}' not found in available variables")
|
963
|
-
|
964
|
-
# Evaluate the pattern to create the expression
|
965
|
-
self.logger.debug(f"Rebuilding pattern: '{pattern}' with vars: {existing_vars}")
|
966
|
-
result = eval(pattern, namespace)
|
967
|
-
self.logger.debug(f"Rebuild result: {result} (type: {type(result)})")
|
968
|
-
|
969
|
-
return result
|
970
|
-
|
971
|
-
except Exception as e:
|
972
|
-
self.logger.debug(f"Failed to rebuild pattern '{pattern}': {e}")
|
973
|
-
return None
|
974
|
-
|
975
|
-
def parse_and_rebuild_expression(self, expr: Any, missing_vars: list[str]) -> Any | None:
|
976
|
-
"""
|
977
|
-
Parse composite expressions and rebuild them using existing variables.
|
978
|
-
|
979
|
-
Args:
|
980
|
-
expr: The expression to parse and rebuild
|
981
|
-
missing_vars: List of missing variable names
|
982
|
-
|
983
|
-
Returns:
|
984
|
-
Rebuilt expression if successful, None otherwise
|
985
|
-
"""
|
986
|
-
if expr is None:
|
987
|
-
return None
|
988
|
-
|
989
|
-
if isinstance(expr, VariableReference):
|
990
|
-
if expr.name in missing_vars:
|
991
|
-
# This is a composite expression - try to parse and rebuild it
|
992
|
-
return self.parse_composite_expression_pattern(expr.name)
|
993
|
-
return expr
|
994
|
-
|
995
|
-
elif isinstance(expr, BinaryOperation):
|
996
|
-
# Recursively rebuild operands
|
997
|
-
left_rebuilt = self.parse_and_rebuild_expression(expr.left, missing_vars)
|
998
|
-
right_rebuilt = self.parse_and_rebuild_expression(expr.right, missing_vars)
|
999
|
-
|
1000
|
-
if left_rebuilt and right_rebuilt:
|
1001
|
-
return BinaryOperation(expr.operator, left_rebuilt, right_rebuilt)
|
1002
|
-
|
1003
|
-
elif isinstance(expr, UnaryFunction):
|
1004
|
-
# Recursively rebuild operand
|
1005
|
-
operand_rebuilt = self.parse_and_rebuild_expression(expr.operand, missing_vars)
|
1006
|
-
|
1007
|
-
if operand_rebuilt:
|
1008
|
-
return UnaryFunction(expr.function_name, operand_rebuilt)
|
1009
|
-
|
1010
|
-
# Note: There's no BinaryFunction class - binary functions are handled by
|
1011
|
-
# specific function calls like min_expr, max_expr or BinaryOperation
|
1012
|
-
|
1013
|
-
elif isinstance(expr, Constant):
|
1014
|
-
return expr
|
1015
|
-
|
1016
|
-
return None
|