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/problems/solving.py
ADDED
@@ -0,0 +1,1216 @@
|
|
1
|
+
"""
|
2
|
+
Problem solving system with equation reconstruction capabilities.
|
3
|
+
|
4
|
+
This module provides the complete solving system including:
|
5
|
+
- High-level solve orchestration (formerly solving.py)
|
6
|
+
- Equation reconstruction for malformed expressions (formerly reconstruction.py)
|
7
|
+
- Expression parsing and rebuilding (formerly expression_parser.py)
|
8
|
+
- Namespace mapping for variables (formerly namespace_mapper.py)
|
9
|
+
- Composite expression rebuilding (formerly composite_expression_rebuilder.py)
|
10
|
+
- Delayed expression resolution (formerly delayed_expression_resolver.py)
|
11
|
+
|
12
|
+
All consolidated into a focused solving system.
|
13
|
+
"""
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
|
17
|
+
import re
|
18
|
+
from logging import Logger
|
19
|
+
from typing import Any
|
20
|
+
|
21
|
+
from ..equations import Equation
|
22
|
+
from ..expressions import BinaryOperation, ConditionalExpression, Constant, UnaryFunction, VariableReference, cos, sin
|
23
|
+
from ..quantities import FieldQnty
|
24
|
+
|
25
|
+
# Type aliases for better readability
|
26
|
+
VariableDict = dict[str, FieldQnty]
|
27
|
+
ReconstructionResult = Equation | None
|
28
|
+
NamespaceMapping = dict[str, str]
|
29
|
+
|
30
|
+
# Constants for pattern matching
|
31
|
+
CONDITIONAL_PATTERNS: set[str] = {"cond("}
|
32
|
+
FUNCTION_PATTERNS: set[str] = {"sin(", "cos(", "tan(", "log(", "exp(", "sqrt"}
|
33
|
+
MATH_OPERATORS: set[str] = {"(", ")", "+", "-", "*", "/"}
|
34
|
+
EXCLUDED_FUNCTION_NAMES: set[str] = {"sin", "cos", "max", "min", "exp", "log", "sqrt", "tan"}
|
35
|
+
|
36
|
+
# Compiled regex patterns for performance
|
37
|
+
VARIABLE_PATTERN_DETAILED = re.compile(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\b")
|
38
|
+
VARIABLE_PATTERN = re.compile(r"\b[A-Za-z][A-Za-z0-9_]*\b")
|
39
|
+
|
40
|
+
# Tuple of types for isinstance() checks
|
41
|
+
VALID_EXPRESSION_TYPES = (VariableReference, FieldQnty, int, float, BinaryOperation, ConditionalExpression, Constant, UnaryFunction)
|
42
|
+
|
43
|
+
|
44
|
+
# ========== CUSTOM EXCEPTIONS ==========
|
45
|
+
|
46
|
+
|
47
|
+
class SolverError(RuntimeError):
|
48
|
+
"""Raised when the solving process fails."""
|
49
|
+
|
50
|
+
pass
|
51
|
+
|
52
|
+
|
53
|
+
class EquationReconstructionError(Exception):
|
54
|
+
"""Base exception for equation reconstruction errors."""
|
55
|
+
|
56
|
+
pass
|
57
|
+
|
58
|
+
|
59
|
+
class MalformedExpressionError(EquationReconstructionError):
|
60
|
+
"""Raised when expressions are malformed and cannot be reconstructed."""
|
61
|
+
|
62
|
+
pass
|
63
|
+
|
64
|
+
|
65
|
+
class NamespaceMappingError(EquationReconstructionError):
|
66
|
+
"""Raised when namespace mapping fails."""
|
67
|
+
|
68
|
+
pass
|
69
|
+
|
70
|
+
|
71
|
+
class PatternReconstructionError(EquationReconstructionError):
|
72
|
+
"""Raised when mathematical pattern reconstruction fails."""
|
73
|
+
|
74
|
+
pass
|
75
|
+
|
76
|
+
|
77
|
+
# ========== EXPRESSION PARSER ==========
|
78
|
+
|
79
|
+
|
80
|
+
class ExpressionParser:
|
81
|
+
"""
|
82
|
+
Focused class for parsing and rebuilding mathematical expressions.
|
83
|
+
|
84
|
+
Handles conversion from string patterns to Expression objects using
|
85
|
+
safe evaluation techniques and proper namespace management.
|
86
|
+
"""
|
87
|
+
|
88
|
+
def __init__(self, variables: VariableDict, logger: Logger):
|
89
|
+
"""
|
90
|
+
Initialize the expression parser.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
variables: Dictionary of available variables
|
94
|
+
logger: Logger for debugging
|
95
|
+
"""
|
96
|
+
self.variables = variables
|
97
|
+
self.logger = logger
|
98
|
+
|
99
|
+
def parse_composite_expression_pattern(self, composite_symbol: str) -> Any | None:
|
100
|
+
"""
|
101
|
+
Parse a composite expression pattern and reconstruct it using available variables.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
composite_symbol: The composite expression string to parse
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
Reconstructed expression if successful, None otherwise
|
108
|
+
|
109
|
+
Examples:
|
110
|
+
"D - T * 2" -> header_D - header_T * 2
|
111
|
+
"(P - S) / E" -> (header_P - header_S) / header_E
|
112
|
+
"""
|
113
|
+
if not composite_symbol or not isinstance(composite_symbol, str):
|
114
|
+
return None
|
115
|
+
|
116
|
+
try:
|
117
|
+
# Extract variable names from the composite expression
|
118
|
+
var_matches = VARIABLE_PATTERN_DETAILED.findall(composite_symbol)
|
119
|
+
if not var_matches:
|
120
|
+
return None
|
121
|
+
|
122
|
+
# Find the namespace that contains most of these variables
|
123
|
+
best_namespace = self._find_best_namespace_for_variables(var_matches)
|
124
|
+
if not best_namespace:
|
125
|
+
return None
|
126
|
+
|
127
|
+
# Create substitution mapping
|
128
|
+
substitution_map = {}
|
129
|
+
for var_name in var_matches:
|
130
|
+
if var_name not in EXCLUDED_FUNCTION_NAMES:
|
131
|
+
namespaced_name = f"{best_namespace}_{var_name}"
|
132
|
+
if namespaced_name in self.variables:
|
133
|
+
substitution_map[var_name] = namespaced_name
|
134
|
+
|
135
|
+
if not substitution_map:
|
136
|
+
return None
|
137
|
+
|
138
|
+
# Substitute variables in the expression string
|
139
|
+
substituted_expr = composite_symbol
|
140
|
+
for original, namespaced in substitution_map.items():
|
141
|
+
# Use word boundary regex for precise replacement
|
142
|
+
pattern = r"\b" + re.escape(original) + r"\b"
|
143
|
+
substituted_expr = re.sub(pattern, namespaced, substituted_expr)
|
144
|
+
|
145
|
+
# Try to build the expression from the substituted string
|
146
|
+
return self._build_expression_from_string(substituted_expr)
|
147
|
+
|
148
|
+
except Exception as e:
|
149
|
+
self.logger.debug(f"Failed to parse composite expression '{composite_symbol}': {e}")
|
150
|
+
return None
|
151
|
+
|
152
|
+
def _find_best_namespace_for_variables(self, var_names: list[str]) -> str | None:
|
153
|
+
"""Find the namespace that contains the most variables from the list."""
|
154
|
+
namespace_scores = {}
|
155
|
+
|
156
|
+
# Score each namespace based on how many variables it contains
|
157
|
+
for var_name in self.variables:
|
158
|
+
if "_" in var_name:
|
159
|
+
namespace = var_name.split("_")[0]
|
160
|
+
base_var = "_".join(var_name.split("_")[1:])
|
161
|
+
|
162
|
+
if base_var in var_names:
|
163
|
+
namespace_scores[namespace] = namespace_scores.get(namespace, 0) + 1
|
164
|
+
|
165
|
+
if not namespace_scores:
|
166
|
+
return None
|
167
|
+
|
168
|
+
# Return the namespace with the highest score
|
169
|
+
return max(namespace_scores.items(), key=lambda x: x[1])[0]
|
170
|
+
|
171
|
+
def _build_expression_from_string(self, expr_string: str) -> Any | None:
|
172
|
+
"""
|
173
|
+
Build an expression from a string by safely evaluating with variable substitution.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
expr_string: The expression string to evaluate
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
Built expression object if successful, None otherwise
|
180
|
+
"""
|
181
|
+
try:
|
182
|
+
# Create a safe evaluation context with only our variables
|
183
|
+
eval_context = {}
|
184
|
+
|
185
|
+
# Add variables to context
|
186
|
+
for var_symbol, var_obj in self.variables.items():
|
187
|
+
eval_context[var_symbol] = var_obj
|
188
|
+
|
189
|
+
# Add safe mathematical functions
|
190
|
+
eval_context.update(
|
191
|
+
{
|
192
|
+
"sin": sin,
|
193
|
+
"cos": cos,
|
194
|
+
"abs": abs,
|
195
|
+
"min": min,
|
196
|
+
"max": max,
|
197
|
+
"__builtins__": {}, # Disable built-ins for security
|
198
|
+
}
|
199
|
+
)
|
200
|
+
|
201
|
+
# Safely evaluate the expression
|
202
|
+
result = eval(expr_string, eval_context, {})
|
203
|
+
return result
|
204
|
+
|
205
|
+
except Exception as e:
|
206
|
+
self.logger.debug(f"Failed to build expression from string '{expr_string}': {e}")
|
207
|
+
return None
|
208
|
+
|
209
|
+
def parse_malformed_variable_pattern(self, malformed_symbol: str) -> Any | None:
|
210
|
+
"""
|
211
|
+
Parse a malformed variable pattern from proxy evaluation.
|
212
|
+
|
213
|
+
Args:
|
214
|
+
malformed_symbol: The malformed variable symbol to parse
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
Reconstructed expression if successful, None otherwise
|
218
|
+
"""
|
219
|
+
if not malformed_symbol or not isinstance(malformed_symbol, str):
|
220
|
+
return None
|
221
|
+
|
222
|
+
try:
|
223
|
+
# Check if this looks like a mathematical expression with embedded values
|
224
|
+
if any(char in malformed_symbol for char in MATH_OPERATORS):
|
225
|
+
# Try to parse as a composite expression
|
226
|
+
return self.parse_composite_expression_pattern(malformed_symbol)
|
227
|
+
|
228
|
+
# Check for specific malformed patterns
|
229
|
+
if "." in malformed_symbol and "(" in malformed_symbol:
|
230
|
+
# Looks like a method call result - try to extract the base pattern
|
231
|
+
base_pattern = self._extract_base_pattern_from_malformed(malformed_symbol)
|
232
|
+
if base_pattern:
|
233
|
+
return self.parse_composite_expression_pattern(base_pattern)
|
234
|
+
|
235
|
+
return None
|
236
|
+
|
237
|
+
except Exception as e:
|
238
|
+
self.logger.debug(f"Failed to parse malformed variable '{malformed_symbol}': {e}")
|
239
|
+
return None
|
240
|
+
|
241
|
+
def _extract_base_pattern_from_malformed(self, malformed_symbol: str) -> str | None:
|
242
|
+
"""Extract the base mathematical pattern from a malformed symbol."""
|
243
|
+
if not malformed_symbol or not isinstance(malformed_symbol, str):
|
244
|
+
return None
|
245
|
+
|
246
|
+
try:
|
247
|
+
# Remove numeric values that look like results
|
248
|
+
cleaned = re.sub(r"\d+\.\d+", "VAR", malformed_symbol)
|
249
|
+
|
250
|
+
# Remove method calls like .value, .quantity
|
251
|
+
cleaned = re.sub(r"\.(?:value|quantity|magnitude)\b", "", cleaned)
|
252
|
+
|
253
|
+
# Return pattern if it contains mathematical operators
|
254
|
+
return cleaned if any(char in cleaned for char in MATH_OPERATORS) else None
|
255
|
+
|
256
|
+
except (AttributeError, re.error):
|
257
|
+
return None
|
258
|
+
|
259
|
+
def parse_and_rebuild_expression(self, expr: Any, missing_vars: list[str]) -> Any | None:
|
260
|
+
"""
|
261
|
+
Parse composite expressions and rebuild them using existing variables.
|
262
|
+
|
263
|
+
Args:
|
264
|
+
expr: The expression to parse and rebuild
|
265
|
+
missing_vars: List of missing variable names
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
Rebuilt expression if successful, None otherwise
|
269
|
+
"""
|
270
|
+
if not missing_vars:
|
271
|
+
return expr
|
272
|
+
|
273
|
+
try:
|
274
|
+
# For each missing variable, try to reconstruct it
|
275
|
+
reconstructed_components = {}
|
276
|
+
|
277
|
+
for missing_var in missing_vars:
|
278
|
+
if any(char in missing_var for char in MATH_OPERATORS):
|
279
|
+
# This is a composite expression
|
280
|
+
rebuilt = self.parse_composite_expression_pattern(missing_var)
|
281
|
+
if rebuilt:
|
282
|
+
reconstructed_components[missing_var] = rebuilt
|
283
|
+
|
284
|
+
if not reconstructed_components:
|
285
|
+
return None
|
286
|
+
|
287
|
+
# Try to substitute the reconstructed components back into the original expression
|
288
|
+
return self._substitute_in_expression(expr, reconstructed_components)
|
289
|
+
|
290
|
+
except Exception as e:
|
291
|
+
self.logger.debug(f"Failed to parse and rebuild expression: {e}")
|
292
|
+
return None
|
293
|
+
|
294
|
+
def _substitute_in_expression(self, _expr: Any, substitutions: dict[str, Any]) -> Any | None:
|
295
|
+
"""
|
296
|
+
Substitute reconstructed components back into an expression tree.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
_expr: The original expression (unused in current implementation)
|
300
|
+
substitutions: Map of missing variables to their reconstructed versions
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
Expression with substitutions applied
|
304
|
+
"""
|
305
|
+
if not substitutions:
|
306
|
+
return None
|
307
|
+
|
308
|
+
# Return the first successful substitution
|
309
|
+
# TODO: Implement full expression tree traversal for complex substitutions
|
310
|
+
return next(iter(substitutions.values()))
|
311
|
+
|
312
|
+
|
313
|
+
# ========== NAMESPACE MAPPER ==========
|
314
|
+
|
315
|
+
|
316
|
+
class NamespaceMapper:
|
317
|
+
"""
|
318
|
+
Focused class for handling variable namespace mapping operations.
|
319
|
+
|
320
|
+
Provides efficient mapping of base variable names to their namespaced
|
321
|
+
counterparts with caching for performance optimization.
|
322
|
+
"""
|
323
|
+
|
324
|
+
def __init__(self, variables: VariableDict, logger: Logger):
|
325
|
+
"""
|
326
|
+
Initialize the namespace mapper.
|
327
|
+
|
328
|
+
Args:
|
329
|
+
variables: Dictionary of available variables
|
330
|
+
logger: Logger for debugging
|
331
|
+
"""
|
332
|
+
self.variables = variables
|
333
|
+
self.logger = logger
|
334
|
+
|
335
|
+
# Performance optimization caches
|
336
|
+
self._namespace_cache: dict[str, set[str]] = {}
|
337
|
+
self._variable_mapping_cache: dict[frozenset, NamespaceMapping] = {}
|
338
|
+
self._all_variable_names: set[str] | None = None
|
339
|
+
|
340
|
+
def extract_base_variables_from_composites(self, missing_vars: list[str]) -> set[str]:
|
341
|
+
"""
|
342
|
+
Extract base variable names from composite expressions.
|
343
|
+
|
344
|
+
Args:
|
345
|
+
missing_vars: List of missing variable expressions
|
346
|
+
|
347
|
+
Returns:
|
348
|
+
Set of base variable names found in the expressions
|
349
|
+
"""
|
350
|
+
base_vars = set()
|
351
|
+
|
352
|
+
for missing_var in missing_vars:
|
353
|
+
if not isinstance(missing_var, str):
|
354
|
+
continue
|
355
|
+
|
356
|
+
# Extract variable names using regex
|
357
|
+
matches = VARIABLE_PATTERN.findall(missing_var)
|
358
|
+
|
359
|
+
for match in matches:
|
360
|
+
# Skip function names
|
361
|
+
if match not in EXCLUDED_FUNCTION_NAMES:
|
362
|
+
base_vars.add(match)
|
363
|
+
|
364
|
+
return base_vars
|
365
|
+
|
366
|
+
def find_namespace_mappings(self, base_vars: set[str]) -> NamespaceMapping:
|
367
|
+
"""
|
368
|
+
Find namespace mappings for a set of base variables.
|
369
|
+
|
370
|
+
Args:
|
371
|
+
base_vars: Set of base variable names
|
372
|
+
|
373
|
+
Returns:
|
374
|
+
Dictionary mapping base variables to their namespaced versions
|
375
|
+
"""
|
376
|
+
# Use cache for performance
|
377
|
+
cache_key = frozenset(base_vars)
|
378
|
+
if cache_key in self._variable_mapping_cache:
|
379
|
+
return self._variable_mapping_cache[cache_key]
|
380
|
+
|
381
|
+
mappings = {}
|
382
|
+
|
383
|
+
# Build all variable names cache if needed
|
384
|
+
if self._all_variable_names is None:
|
385
|
+
self._all_variable_names = set(self.variables.keys())
|
386
|
+
|
387
|
+
# For each base variable, find its namespaced version
|
388
|
+
for base_var in base_vars:
|
389
|
+
namespaced_var = self._find_namespaced_variable(base_var)
|
390
|
+
if namespaced_var:
|
391
|
+
mappings[base_var] = namespaced_var
|
392
|
+
|
393
|
+
# Cache the result
|
394
|
+
self._variable_mapping_cache[cache_key] = mappings
|
395
|
+
return mappings
|
396
|
+
|
397
|
+
def _find_namespaced_variable(self, base_var: str) -> str | None:
|
398
|
+
"""
|
399
|
+
Find the namespaced version of a base variable.
|
400
|
+
|
401
|
+
Args:
|
402
|
+
base_var: Base variable name
|
403
|
+
|
404
|
+
Returns:
|
405
|
+
Namespaced variable name if found, None otherwise
|
406
|
+
"""
|
407
|
+
if not base_var or not isinstance(base_var, str):
|
408
|
+
return None
|
409
|
+
|
410
|
+
# Build all variable names cache if needed
|
411
|
+
if self._all_variable_names is None:
|
412
|
+
self._all_variable_names = set(self.variables.keys())
|
413
|
+
|
414
|
+
# Direct match first (most efficient)
|
415
|
+
if base_var in self._all_variable_names:
|
416
|
+
return base_var
|
417
|
+
|
418
|
+
# Look for namespaced versions
|
419
|
+
suffix = f"_{base_var}"
|
420
|
+
candidates = [name for name in self._all_variable_names if name.endswith(suffix)]
|
421
|
+
|
422
|
+
# Return shortest match (least nested namespace)
|
423
|
+
return min(candidates, key=len) if candidates else None
|
424
|
+
|
425
|
+
def get_namespaces_for_variable(self, base_var: str) -> set[str]:
|
426
|
+
"""
|
427
|
+
Get all namespaces that contain a particular base variable.
|
428
|
+
|
429
|
+
Args:
|
430
|
+
base_var: Base variable name
|
431
|
+
|
432
|
+
Returns:
|
433
|
+
Set of namespace prefixes that contain the variable
|
434
|
+
"""
|
435
|
+
# Use cache for performance
|
436
|
+
if base_var in self._namespace_cache:
|
437
|
+
return self._namespace_cache[base_var]
|
438
|
+
|
439
|
+
namespaces = set()
|
440
|
+
|
441
|
+
for var_name in self.variables.keys():
|
442
|
+
if var_name.endswith(f"_{base_var}") and "_" in var_name:
|
443
|
+
# Extract namespace (everything before the last underscore + base_var)
|
444
|
+
parts = var_name.split("_")
|
445
|
+
if len(parts) >= 2 and parts[-1] == base_var:
|
446
|
+
namespace = "_".join(parts[:-1])
|
447
|
+
namespaces.add(namespace)
|
448
|
+
|
449
|
+
# Cache the result
|
450
|
+
self._namespace_cache[base_var] = namespaces
|
451
|
+
return namespaces
|
452
|
+
|
453
|
+
def clear_caches(self) -> None:
|
454
|
+
"""Clear all internal caches."""
|
455
|
+
self._namespace_cache.clear()
|
456
|
+
self._variable_mapping_cache.clear()
|
457
|
+
self._all_variable_names = None
|
458
|
+
|
459
|
+
|
460
|
+
# ========== COMPOSITE EXPRESSION REBUILDER ==========
|
461
|
+
|
462
|
+
|
463
|
+
class CompositeExpressionRebuilder:
|
464
|
+
"""
|
465
|
+
Focused class for rebuilding composite expressions from malformed patterns.
|
466
|
+
|
467
|
+
Handles reconstruction of expressions that were malformed during proxy
|
468
|
+
evaluation and provides methods to recover the original mathematical structure.
|
469
|
+
"""
|
470
|
+
|
471
|
+
def __init__(self, variables: VariableDict, logger: Logger):
|
472
|
+
"""
|
473
|
+
Initialize the composite expression rebuilder.
|
474
|
+
|
475
|
+
Args:
|
476
|
+
variables: Dictionary of available variables
|
477
|
+
logger: Logger for debugging
|
478
|
+
"""
|
479
|
+
self.variables = variables
|
480
|
+
self.logger = logger
|
481
|
+
|
482
|
+
def identify_malformed_variables(self, missing_vars: list[str]) -> list[str]:
|
483
|
+
"""
|
484
|
+
Identify which missing variables are malformed from proxy evaluation.
|
485
|
+
|
486
|
+
Args:
|
487
|
+
missing_vars: List of missing variable names
|
488
|
+
|
489
|
+
Returns:
|
490
|
+
List of variables that appear to be malformed
|
491
|
+
"""
|
492
|
+
malformed = []
|
493
|
+
|
494
|
+
for var in missing_vars:
|
495
|
+
if self._is_malformed_variable(var):
|
496
|
+
malformed.append(var)
|
497
|
+
|
498
|
+
return malformed
|
499
|
+
|
500
|
+
def _is_malformed_variable(self, var_name: str) -> bool:
|
501
|
+
"""
|
502
|
+
Check if a variable name appears to be malformed from proxy evaluation.
|
503
|
+
|
504
|
+
Args:
|
505
|
+
var_name: Variable name to check
|
506
|
+
|
507
|
+
Returns:
|
508
|
+
True if the variable appears malformed
|
509
|
+
"""
|
510
|
+
if not isinstance(var_name, str) or not var_name:
|
511
|
+
return False
|
512
|
+
|
513
|
+
# Check for numeric values embedded in expressions
|
514
|
+
if re.search(r"\d+\.\d+", var_name):
|
515
|
+
return True
|
516
|
+
|
517
|
+
# Check for method calls in variable names
|
518
|
+
if ".value" in var_name or ".quantity" in var_name:
|
519
|
+
return True
|
520
|
+
|
521
|
+
# Check for unbalanced parentheses
|
522
|
+
if var_name.count("(") != var_name.count(")"):
|
523
|
+
return True
|
524
|
+
|
525
|
+
# Check for very long names with multiple operations
|
526
|
+
if len(var_name) > 50 and any(op in var_name for op in MATH_OPERATORS):
|
527
|
+
return True
|
528
|
+
|
529
|
+
return False
|
530
|
+
|
531
|
+
def reconstruct_malformed_proxy_expression(self, equation: Equation, _malformed_vars: list[str]) -> Any | None:
|
532
|
+
"""
|
533
|
+
Generically reconstruct expressions that were malformed due to proxy evaluation.
|
534
|
+
|
535
|
+
Args:
|
536
|
+
equation: The equation containing malformed expressions
|
537
|
+
_malformed_vars: List of malformed variable names (kept for signature compatibility)
|
538
|
+
|
539
|
+
Returns:
|
540
|
+
Reconstructed expression if successful, None otherwise
|
541
|
+
"""
|
542
|
+
try:
|
543
|
+
# Try to extract a meaningful pattern from the equation's RHS
|
544
|
+
rhs_str = str(equation.rhs)
|
545
|
+
|
546
|
+
# Look for recognizable mathematical patterns
|
547
|
+
pattern = self._extract_mathematical_pattern(rhs_str)
|
548
|
+
if pattern:
|
549
|
+
# Try to reconstruct the pattern with proper variable references
|
550
|
+
return self._reconstruct_pattern(pattern)
|
551
|
+
|
552
|
+
# If direct pattern extraction fails, try alternative approaches
|
553
|
+
return self._attempt_fallback_reconstruction(equation)
|
554
|
+
|
555
|
+
except Exception as e:
|
556
|
+
self.logger.debug(f"Failed to reconstruct malformed proxy expression: {e}")
|
557
|
+
return None
|
558
|
+
|
559
|
+
def _extract_mathematical_pattern(self, expression_str: str) -> str | None:
|
560
|
+
"""
|
561
|
+
Extract a mathematical pattern from an expression string.
|
562
|
+
|
563
|
+
Args:
|
564
|
+
expression_str: String representation of the expression
|
565
|
+
|
566
|
+
Returns:
|
567
|
+
Cleaned mathematical pattern if extractable, None otherwise
|
568
|
+
"""
|
569
|
+
try:
|
570
|
+
# Remove common method calls and numeric results
|
571
|
+
cleaned = expression_str
|
572
|
+
|
573
|
+
# Remove .value, .quantity, etc.
|
574
|
+
cleaned = re.sub(r"\.(?:value|quantity|magnitude)\b", "", cleaned)
|
575
|
+
|
576
|
+
# Replace numeric constants with placeholders
|
577
|
+
cleaned = re.sub(r"\d+\.\d+", "NUM", cleaned)
|
578
|
+
|
579
|
+
# If we still have mathematical operators, this might be reconstructable
|
580
|
+
if any(op in cleaned for op in MATH_OPERATORS):
|
581
|
+
return cleaned
|
582
|
+
|
583
|
+
return None
|
584
|
+
|
585
|
+
except Exception:
|
586
|
+
return None
|
587
|
+
|
588
|
+
def _reconstruct_pattern(self, pattern: str) -> Any | None:
|
589
|
+
"""
|
590
|
+
Reconstruct a mathematical pattern using available variables.
|
591
|
+
|
592
|
+
Args:
|
593
|
+
pattern: The mathematical pattern to reconstruct
|
594
|
+
|
595
|
+
Returns:
|
596
|
+
Reconstructed expression if successful, None otherwise
|
597
|
+
"""
|
598
|
+
try:
|
599
|
+
# Extract variable names from the pattern
|
600
|
+
var_matches = VARIABLE_PATTERN.findall(pattern)
|
601
|
+
|
602
|
+
# Try to map these to existing variables
|
603
|
+
var_mapping = {}
|
604
|
+
for var_name in var_matches:
|
605
|
+
if var_name not in EXCLUDED_FUNCTION_NAMES and var_name != "NUM":
|
606
|
+
# Look for a matching variable in our namespace
|
607
|
+
matching_var = self._find_matching_variable(var_name)
|
608
|
+
if matching_var:
|
609
|
+
var_mapping[var_name] = matching_var
|
610
|
+
|
611
|
+
if not var_mapping:
|
612
|
+
return None
|
613
|
+
|
614
|
+
# Substitute the variables back into the pattern
|
615
|
+
reconstructed_pattern = pattern
|
616
|
+
for original, replacement in var_mapping.items():
|
617
|
+
reconstructed_pattern = re.sub(r"\b" + re.escape(original) + r"\b", replacement, reconstructed_pattern)
|
618
|
+
|
619
|
+
# Replace NUM placeholders with appropriate constants
|
620
|
+
reconstructed_pattern = re.sub(r"\bNUM\b", "1.0", reconstructed_pattern)
|
621
|
+
|
622
|
+
# Try to evaluate the reconstructed pattern
|
623
|
+
return self._safe_evaluate_pattern(reconstructed_pattern)
|
624
|
+
|
625
|
+
except Exception as e:
|
626
|
+
self.logger.debug(f"Failed to reconstruct pattern '{pattern}': {e}")
|
627
|
+
return None
|
628
|
+
|
629
|
+
def _find_matching_variable(self, var_name: str) -> str | None:
|
630
|
+
"""Find a variable in our namespace that matches the given name."""
|
631
|
+
# Direct match first
|
632
|
+
if var_name in self.variables:
|
633
|
+
return var_name
|
634
|
+
|
635
|
+
# Look for namespaced versions
|
636
|
+
candidates = [v for v in self.variables.keys() if v.endswith(f"_{var_name}")]
|
637
|
+
|
638
|
+
if candidates:
|
639
|
+
# Return the shortest match (least nested)
|
640
|
+
return min(candidates, key=len)
|
641
|
+
|
642
|
+
return None
|
643
|
+
|
644
|
+
def _safe_evaluate_pattern(self, pattern: str) -> Any | None:
|
645
|
+
"""Safely evaluate a reconstructed pattern."""
|
646
|
+
try:
|
647
|
+
# Create evaluation context with our variables
|
648
|
+
eval_context: dict[str, Any] = dict(self.variables)
|
649
|
+
eval_context["__builtins__"] = {} # Security
|
650
|
+
|
651
|
+
return eval(pattern, eval_context, {})
|
652
|
+
|
653
|
+
except Exception:
|
654
|
+
return None
|
655
|
+
|
656
|
+
def _attempt_fallback_reconstruction(self, equation: Equation) -> Any | None:
|
657
|
+
"""Attempt fallback reconstruction methods."""
|
658
|
+
try:
|
659
|
+
# Try to get variables from the LHS and create a simple reconstruction
|
660
|
+
if isinstance(equation.lhs, FieldQnty | VariableReference):
|
661
|
+
symbol = getattr(equation.lhs, "symbol", None)
|
662
|
+
if isinstance(symbol, str) and symbol in self.variables:
|
663
|
+
lhs_var = self.variables[symbol]
|
664
|
+
|
665
|
+
# Look for similar variables that might be used in a simple expression
|
666
|
+
namespace = self._extract_namespace_from_variable(symbol)
|
667
|
+
if namespace:
|
668
|
+
return self._create_simple_reconstruction(namespace, lhs_var)
|
669
|
+
|
670
|
+
return None
|
671
|
+
|
672
|
+
except (AttributeError, TypeError):
|
673
|
+
return None
|
674
|
+
|
675
|
+
def _extract_namespace_from_variable(self, var_symbol: str) -> str | None:
|
676
|
+
"""Extract namespace from a variable symbol."""
|
677
|
+
if "_" in var_symbol:
|
678
|
+
return var_symbol.split("_")[0]
|
679
|
+
return None
|
680
|
+
|
681
|
+
def _create_simple_reconstruction(self, namespace: str, target_var: FieldQnty) -> Any | None:
|
682
|
+
"""Create a simple reconstruction based on namespace variables."""
|
683
|
+
try:
|
684
|
+
# Find other variables in the same namespace
|
685
|
+
namespace_vars = [v for name, v in self.variables.items() if name.startswith(f"{namespace}_") and v != target_var]
|
686
|
+
|
687
|
+
if namespace_vars:
|
688
|
+
# Return the first other variable as a simple reconstruction
|
689
|
+
# This is a fallback - in practice, more sophisticated logic would be needed
|
690
|
+
return namespace_vars[0]
|
691
|
+
|
692
|
+
return None
|
693
|
+
|
694
|
+
except Exception:
|
695
|
+
return None
|
696
|
+
|
697
|
+
|
698
|
+
# ========== DELAYED EXPRESSION RESOLVER ==========
|
699
|
+
|
700
|
+
|
701
|
+
class DelayedExpressionResolver:
|
702
|
+
"""
|
703
|
+
Focused class for resolving delayed expressions and equations.
|
704
|
+
|
705
|
+
Handles components that have deferred evaluation needs and provides
|
706
|
+
safe resolution with proper type checking and context management.
|
707
|
+
"""
|
708
|
+
|
709
|
+
def __init__(self, variables: VariableDict, logger: Logger):
|
710
|
+
"""
|
711
|
+
Initialize the delayed expression resolver.
|
712
|
+
|
713
|
+
Args:
|
714
|
+
variables: Dictionary of available variables
|
715
|
+
logger: Logger for debugging
|
716
|
+
"""
|
717
|
+
self.variables = variables
|
718
|
+
self.logger = logger
|
719
|
+
|
720
|
+
def contains_delayed_expressions(self, equation: Equation) -> bool:
|
721
|
+
"""
|
722
|
+
Check if an equation contains delayed expressions that need resolution.
|
723
|
+
|
724
|
+
Args:
|
725
|
+
equation: The equation to check for delayed expressions
|
726
|
+
|
727
|
+
Returns:
|
728
|
+
True if equation contains delayed expressions
|
729
|
+
"""
|
730
|
+
try:
|
731
|
+
# Check for common delayed expression patterns
|
732
|
+
equation_str = str(equation)
|
733
|
+
|
734
|
+
# Look for delayed expression markers
|
735
|
+
delayed_markers = ["DelayedExpression", "DelayedVariable", "DelayedFunction", "resolve(", "delayed_", "proxy_"]
|
736
|
+
|
737
|
+
return any(marker in equation_str for marker in delayed_markers)
|
738
|
+
|
739
|
+
except Exception:
|
740
|
+
return False
|
741
|
+
|
742
|
+
def resolve_delayed_equation(self, equation: Equation) -> ReconstructionResult:
|
743
|
+
"""
|
744
|
+
Resolve a delayed equation by evaluating its delayed expressions.
|
745
|
+
|
746
|
+
Args:
|
747
|
+
equation: The equation with delayed expressions to resolve
|
748
|
+
|
749
|
+
Returns:
|
750
|
+
Resolved equation if successful, None otherwise
|
751
|
+
"""
|
752
|
+
try:
|
753
|
+
# Try to resolve the RHS if it has delayed components
|
754
|
+
resolved_rhs = self._resolve_delayed_expression(equation.rhs)
|
755
|
+
|
756
|
+
if resolved_rhs is None:
|
757
|
+
return None
|
758
|
+
|
759
|
+
# Create new equation with resolved RHS
|
760
|
+
if isinstance(equation.lhs, FieldQnty):
|
761
|
+
return equation.lhs.equals(resolved_rhs)
|
762
|
+
|
763
|
+
return None
|
764
|
+
|
765
|
+
except Exception as e:
|
766
|
+
self.logger.debug(f"Failed to resolve delayed equation: {e}")
|
767
|
+
return None
|
768
|
+
|
769
|
+
def _resolve_delayed_expression(self, expr: Any) -> Any | None:
|
770
|
+
"""
|
771
|
+
Resolve a delayed expression by calling its resolve method if available.
|
772
|
+
|
773
|
+
Args:
|
774
|
+
expr: Expression that might be delayed
|
775
|
+
|
776
|
+
Returns:
|
777
|
+
Resolved expression if successful, None otherwise
|
778
|
+
"""
|
779
|
+
try:
|
780
|
+
# Check if this is a valid expression type that doesn't need resolution
|
781
|
+
if isinstance(expr, VALID_EXPRESSION_TYPES):
|
782
|
+
# Check if this expression has a resolve method
|
783
|
+
if hasattr(expr, "resolve") and callable(getattr(expr, "resolve", None)):
|
784
|
+
context = self.variables.copy()
|
785
|
+
return getattr(expr, "resolve")(context)
|
786
|
+
return expr
|
787
|
+
|
788
|
+
# Try to recursively resolve components for binary operations
|
789
|
+
if isinstance(expr, BinaryOperation):
|
790
|
+
resolved_left = self._resolve_delayed_expression(expr.left)
|
791
|
+
resolved_right = self._resolve_delayed_expression(expr.right)
|
792
|
+
|
793
|
+
if resolved_left is not None and resolved_right is not None:
|
794
|
+
return BinaryOperation(expr.operator, resolved_left, resolved_right)
|
795
|
+
|
796
|
+
return None
|
797
|
+
|
798
|
+
except (AttributeError, TypeError) as e:
|
799
|
+
self.logger.debug(f"Failed to resolve delayed expression: {e}")
|
800
|
+
return None
|
801
|
+
|
802
|
+
|
803
|
+
# ========== MAIN EQUATION RECONSTRUCTOR ==========
|
804
|
+
|
805
|
+
|
806
|
+
class EquationReconstructor:
|
807
|
+
"""
|
808
|
+
Handles reconstruction of equations with composite expressions.
|
809
|
+
|
810
|
+
This refactored class provides equation reconstruction capabilities by
|
811
|
+
coordinating focused component classes for parsing, namespace mapping,
|
812
|
+
delayed resolution, and composite expression rebuilding.
|
813
|
+
|
814
|
+
Key Features:
|
815
|
+
- Delegates to focused component classes for specific responsibilities
|
816
|
+
- Generic composite expression reconstruction
|
817
|
+
- Malformed equation recovery from proxy operations
|
818
|
+
- Namespace variable mapping and resolution
|
819
|
+
- Mathematical pattern parsing and rebuilding
|
820
|
+
- Performance optimization through focused caching
|
821
|
+
|
822
|
+
Example Usage:
|
823
|
+
reconstructor = EquationReconstructor(problem)
|
824
|
+
fixed_equation = reconstructor.fix_malformed_equation(broken_equation)
|
825
|
+
"""
|
826
|
+
|
827
|
+
def __init__(self, problem: Any) -> None:
|
828
|
+
"""
|
829
|
+
Initialize the equation reconstructor.
|
830
|
+
|
831
|
+
Args:
|
832
|
+
problem: The EngineeringProblem instance containing variables and logger
|
833
|
+
|
834
|
+
Raises:
|
835
|
+
ValueError: If problem doesn't have required attributes
|
836
|
+
"""
|
837
|
+
# Type-safe attribute access
|
838
|
+
try:
|
839
|
+
self.variables: VariableDict = problem.variables
|
840
|
+
self.logger: Logger = problem.logger
|
841
|
+
except AttributeError as e:
|
842
|
+
raise ValueError(f"Problem must have 'variables' and 'logger' attributes: {e}") from e
|
843
|
+
|
844
|
+
if not isinstance(self.variables, dict):
|
845
|
+
raise ValueError("Problem.variables must be a dictionary")
|
846
|
+
if not isinstance(self.logger, Logger):
|
847
|
+
raise ValueError("Problem.logger must be a Logger instance")
|
848
|
+
|
849
|
+
self.problem = problem
|
850
|
+
|
851
|
+
# Initialize focused component classes
|
852
|
+
self.expression_parser = ExpressionParser(self.variables, self.logger)
|
853
|
+
self.namespace_mapper = NamespaceMapper(self.variables, self.logger)
|
854
|
+
self.delayed_resolver = DelayedExpressionResolver(self.variables, self.logger)
|
855
|
+
self.composite_rebuilder = CompositeExpressionRebuilder(self.variables, self.logger)
|
856
|
+
|
857
|
+
def fix_malformed_equation(self, equation: Equation) -> ReconstructionResult:
|
858
|
+
"""
|
859
|
+
Generic method to fix equations that were malformed during class definition.
|
860
|
+
|
861
|
+
Specifically handles composite expressions like '(D - (T - c) * 2.0)' that should
|
862
|
+
reference namespaced variables like 'branch_D', 'branch_T', 'branch_c'.
|
863
|
+
|
864
|
+
Args:
|
865
|
+
equation: The malformed equation to fix
|
866
|
+
|
867
|
+
Returns:
|
868
|
+
Fixed equation if reconstruction succeeds, None otherwise
|
869
|
+
|
870
|
+
Raises:
|
871
|
+
EquationReconstructionError: If equation reconstruction fails with detailed error
|
872
|
+
"""
|
873
|
+
if equation is None:
|
874
|
+
return None
|
875
|
+
|
876
|
+
try:
|
877
|
+
# Get all variables referenced in the equation
|
878
|
+
all_vars = equation.get_all_variables()
|
879
|
+
missing_vars = [var for var in all_vars if var not in self.variables]
|
880
|
+
|
881
|
+
if not missing_vars:
|
882
|
+
return equation # Nothing to fix
|
883
|
+
|
884
|
+
self.logger.debug(f"Found missing variables in equation: {missing_vars}")
|
885
|
+
|
886
|
+
# Attempt to reconstruct equations with composite variables using generic approach
|
887
|
+
fixed_equation = self._reconstruct_composite_expressions(equation, missing_vars)
|
888
|
+
|
889
|
+
if fixed_equation:
|
890
|
+
self.logger.debug(f"Successfully reconstructed equation: {fixed_equation}")
|
891
|
+
return fixed_equation
|
892
|
+
else:
|
893
|
+
self.logger.debug("Failed to reconstruct equation")
|
894
|
+
return None
|
895
|
+
|
896
|
+
except Exception as e:
|
897
|
+
self.logger.debug(f"Error in fix_malformed_equation: {e}")
|
898
|
+
return None
|
899
|
+
|
900
|
+
def _reconstruct_composite_expressions(self, equation: Equation, missing_vars: list[str]) -> ReconstructionResult:
|
901
|
+
"""
|
902
|
+
Generic reconstruction of equations with composite expressions.
|
903
|
+
|
904
|
+
Delegates to NamespaceMapper for variable mapping and ExpressionParser
|
905
|
+
for substitution operations.
|
906
|
+
|
907
|
+
Args:
|
908
|
+
equation: The equation to reconstruct
|
909
|
+
missing_vars: List of missing variable names
|
910
|
+
|
911
|
+
Returns:
|
912
|
+
Reconstructed equation if successful, None otherwise
|
913
|
+
|
914
|
+
Raises:
|
915
|
+
NamespaceMappingError: If namespace mapping fails
|
916
|
+
"""
|
917
|
+
if not missing_vars:
|
918
|
+
return None
|
919
|
+
|
920
|
+
try:
|
921
|
+
# Extract variable symbols from composite expressions
|
922
|
+
composite_vars = self.namespace_mapper.extract_base_variables_from_composites(missing_vars)
|
923
|
+
|
924
|
+
if not composite_vars:
|
925
|
+
self.logger.debug("No composite variables found to extract")
|
926
|
+
return None
|
927
|
+
|
928
|
+
# Find which namespaces contain these variables
|
929
|
+
namespace_mappings = self.namespace_mapper.find_namespace_mappings(composite_vars)
|
930
|
+
|
931
|
+
if not namespace_mappings:
|
932
|
+
self.logger.debug("No namespace mappings found")
|
933
|
+
return None
|
934
|
+
|
935
|
+
# Reconstruct the equation by substituting composite expressions
|
936
|
+
return self._substitute_composite_expressions(equation, missing_vars, namespace_mappings)
|
937
|
+
|
938
|
+
except Exception as e:
|
939
|
+
self.logger.debug(f"Error in _reconstruct_composite_expressions: {e}")
|
940
|
+
return None
|
941
|
+
|
942
|
+
def _substitute_composite_expressions(self, equation: Equation, missing_vars: list[str], namespace_mappings: dict[str, str]) -> ReconstructionResult:
|
943
|
+
"""
|
944
|
+
Substitute composite expressions with properly namespaced variables.
|
945
|
+
|
946
|
+
Args:
|
947
|
+
equation: The equation to substitute expressions in
|
948
|
+
missing_vars: List of missing variable names
|
949
|
+
namespace_mappings: Mapping from base variables to namespaced variables
|
950
|
+
|
951
|
+
Returns:
|
952
|
+
Reconstructed equation if successful, None otherwise
|
953
|
+
"""
|
954
|
+
if not missing_vars or not namespace_mappings:
|
955
|
+
return None
|
956
|
+
|
957
|
+
try:
|
958
|
+
# Get the equation string representation for debugging
|
959
|
+
eq_str = str(equation)
|
960
|
+
self.logger.debug(f"Substituting expressions in equation: {eq_str}")
|
961
|
+
|
962
|
+
# For each missing composite expression, try to rebuild it
|
963
|
+
for missing_var in missing_vars:
|
964
|
+
if missing_var in eq_str:
|
965
|
+
reconstructed_expr = self._reconstruct_expression_from_mapping(missing_var, namespace_mappings)
|
966
|
+
if reconstructed_expr:
|
967
|
+
# Replace the original equation's RHS or LHS
|
968
|
+
lhs_var = self._get_lhs_variable(equation)
|
969
|
+
if lhs_var:
|
970
|
+
return lhs_var.equals(reconstructed_expr)
|
971
|
+
|
972
|
+
return None
|
973
|
+
|
974
|
+
except Exception as e:
|
975
|
+
self.logger.debug(f"Error in _substitute_composite_expressions: {e}")
|
976
|
+
return None
|
977
|
+
|
978
|
+
def _reconstruct_expression_from_mapping(self, composite_expr: str, namespace_mappings: dict[str, str]) -> Any | None:
|
979
|
+
"""
|
980
|
+
Reconstruct a composite expression using the namespace mappings.
|
981
|
+
|
982
|
+
Args:
|
983
|
+
composite_expr: The composite expression string to reconstruct
|
984
|
+
namespace_mappings: Mapping from base variables to namespaced variables
|
985
|
+
|
986
|
+
Returns:
|
987
|
+
Reconstructed expression if successful, None otherwise
|
988
|
+
"""
|
989
|
+
if not composite_expr or not namespace_mappings:
|
990
|
+
return None
|
991
|
+
|
992
|
+
try:
|
993
|
+
# Create a substitution pattern for the expression
|
994
|
+
substituted_expr = composite_expr
|
995
|
+
|
996
|
+
# Replace base variable names with their namespaced counterparts
|
997
|
+
for base_var, namespaced_var in namespace_mappings.items():
|
998
|
+
if namespaced_var in self.variables:
|
999
|
+
# Use word boundary regex to avoid partial matches
|
1000
|
+
pattern = r"\b" + re.escape(base_var) + r"\b"
|
1001
|
+
substituted_expr = re.sub(pattern, namespaced_var, substituted_expr)
|
1002
|
+
|
1003
|
+
# Try to evaluate the substituted expression using the expression parser
|
1004
|
+
return self.expression_parser.parse_composite_expression_pattern(substituted_expr)
|
1005
|
+
|
1006
|
+
except Exception as e:
|
1007
|
+
self.logger.debug(f"Error in expression reconstruction: {e}")
|
1008
|
+
return None
|
1009
|
+
|
1010
|
+
def _get_lhs_variable(self, equation: Equation) -> FieldQnty | None:
|
1011
|
+
"""
|
1012
|
+
Safely extract the left-hand side variable from an equation.
|
1013
|
+
|
1014
|
+
Args:
|
1015
|
+
equation: The equation to extract LHS from
|
1016
|
+
|
1017
|
+
Returns:
|
1018
|
+
The LHS variable if valid, None otherwise
|
1019
|
+
"""
|
1020
|
+
try:
|
1021
|
+
# Check if lhs is a VariableReference
|
1022
|
+
if isinstance(equation.lhs, VariableReference):
|
1023
|
+
var_name = equation.lhs.name
|
1024
|
+
if var_name in self.variables:
|
1025
|
+
return self.variables[var_name]
|
1026
|
+
# Check if lhs is a FieldQnty with symbol attribute
|
1027
|
+
elif isinstance(equation.lhs, FieldQnty):
|
1028
|
+
symbol = getattr(equation.lhs, "symbol", None)
|
1029
|
+
if isinstance(symbol, str) and symbol in self.variables:
|
1030
|
+
return self.variables[symbol]
|
1031
|
+
except (AttributeError, TypeError):
|
1032
|
+
pass
|
1033
|
+
|
1034
|
+
return None
|
1035
|
+
|
1036
|
+
def contains_delayed_expressions(self, equation: Equation) -> bool:
|
1037
|
+
"""
|
1038
|
+
Check if an equation contains delayed expressions that need resolution.
|
1039
|
+
|
1040
|
+
Args:
|
1041
|
+
equation: The equation to check for delayed expressions
|
1042
|
+
|
1043
|
+
Returns:
|
1044
|
+
True if equation contains delayed expressions
|
1045
|
+
"""
|
1046
|
+
return self.delayed_resolver.contains_delayed_expressions(equation)
|
1047
|
+
|
1048
|
+
def resolve_delayed_equation(self, equation: Equation) -> ReconstructionResult:
|
1049
|
+
"""
|
1050
|
+
Resolve a delayed equation by evaluating its delayed expressions.
|
1051
|
+
|
1052
|
+
Args:
|
1053
|
+
equation: The equation with delayed expressions to resolve
|
1054
|
+
|
1055
|
+
Returns:
|
1056
|
+
Resolved equation if successful, None otherwise
|
1057
|
+
"""
|
1058
|
+
return self.delayed_resolver.resolve_delayed_equation(equation)
|
1059
|
+
|
1060
|
+
def should_attempt_reconstruction(self, equation: Equation) -> bool:
|
1061
|
+
"""
|
1062
|
+
Determine if we should attempt to reconstruct this equation.
|
1063
|
+
|
1064
|
+
Only attempt reconstruction for simple mathematical expressions,
|
1065
|
+
not complex structures like conditionals.
|
1066
|
+
|
1067
|
+
Args:
|
1068
|
+
equation: The equation to evaluate for reconstruction
|
1069
|
+
|
1070
|
+
Returns:
|
1071
|
+
True if reconstruction should be attempted
|
1072
|
+
"""
|
1073
|
+
if equation is None:
|
1074
|
+
return False
|
1075
|
+
|
1076
|
+
try:
|
1077
|
+
equation_str = str(equation)
|
1078
|
+
|
1079
|
+
# Skip conditional equations using constant set
|
1080
|
+
if any(pattern in equation_str for pattern in CONDITIONAL_PATTERNS):
|
1081
|
+
return False
|
1082
|
+
|
1083
|
+
# Skip equations with complex function calls
|
1084
|
+
if any(func in equation_str for func in FUNCTION_PATTERNS):
|
1085
|
+
# These might be complex - only attempt if they're in the problematic patterns
|
1086
|
+
self.logger.debug(f"Equation contains complex functions: {equation_str}")
|
1087
|
+
|
1088
|
+
# Only attempt if the missing variables look like mathematical expressions
|
1089
|
+
all_vars = equation.get_all_variables()
|
1090
|
+
missing_vars = [var for var in all_vars if var not in self.variables]
|
1091
|
+
|
1092
|
+
for missing_var in missing_vars:
|
1093
|
+
# Check if this looks like a mathematical expression we can handle
|
1094
|
+
if any(char in missing_var for char in MATH_OPERATORS):
|
1095
|
+
return True
|
1096
|
+
|
1097
|
+
return False
|
1098
|
+
|
1099
|
+
except Exception as e:
|
1100
|
+
self.logger.debug(f"Error in should_attempt_reconstruction: {e}")
|
1101
|
+
return False
|
1102
|
+
|
1103
|
+
def reconstruct_composite_expressions_generically(self, equation: Equation) -> ReconstructionResult:
|
1104
|
+
"""
|
1105
|
+
Generically reconstruct equations with composite expressions by parsing the
|
1106
|
+
composite symbols and rebuilding them from existing variables.
|
1107
|
+
|
1108
|
+
Enhanced to handle malformed expressions from proxy evaluation.
|
1109
|
+
|
1110
|
+
Args:
|
1111
|
+
equation: The equation to reconstruct
|
1112
|
+
|
1113
|
+
Returns:
|
1114
|
+
Reconstructed equation if successful, None otherwise
|
1115
|
+
|
1116
|
+
Raises:
|
1117
|
+
MalformedExpressionError: If expressions are too malformed to reconstruct
|
1118
|
+
"""
|
1119
|
+
if equation is None:
|
1120
|
+
return None
|
1121
|
+
|
1122
|
+
try:
|
1123
|
+
all_vars = equation.get_all_variables()
|
1124
|
+
missing_vars = [var for var in all_vars if var not in self.variables]
|
1125
|
+
|
1126
|
+
if not missing_vars:
|
1127
|
+
return equation
|
1128
|
+
|
1129
|
+
# Get the LHS variable with proper validation
|
1130
|
+
lhs_var = self._get_lhs_variable(equation)
|
1131
|
+
if lhs_var is None:
|
1132
|
+
return None
|
1133
|
+
|
1134
|
+
# Check for malformed expressions that contain evaluated numeric values
|
1135
|
+
malformed_vars = self.composite_rebuilder.identify_malformed_variables(missing_vars)
|
1136
|
+
|
1137
|
+
if malformed_vars:
|
1138
|
+
# This is a malformed expression from proxy evaluation
|
1139
|
+
reconstructed_rhs = self.composite_rebuilder.reconstruct_malformed_proxy_expression(equation, malformed_vars)
|
1140
|
+
if reconstructed_rhs:
|
1141
|
+
return lhs_var.equals(reconstructed_rhs)
|
1142
|
+
return None
|
1143
|
+
|
1144
|
+
# Reconstruct the RHS by parsing and rebuilding composite expressions
|
1145
|
+
reconstructed_rhs = self.expression_parser.parse_and_rebuild_expression(equation.rhs, missing_vars)
|
1146
|
+
|
1147
|
+
if reconstructed_rhs:
|
1148
|
+
return lhs_var.equals(reconstructed_rhs)
|
1149
|
+
|
1150
|
+
return None
|
1151
|
+
|
1152
|
+
except Exception as e:
|
1153
|
+
self.logger.debug(f"Reconstruction failed: {e}")
|
1154
|
+
return None
|
1155
|
+
|
1156
|
+
def _clear_caches(self) -> None:
|
1157
|
+
"""
|
1158
|
+
Clear all internal caches. Should be called when variables change.
|
1159
|
+
|
1160
|
+
This method provides a way to reset cached data when the problem
|
1161
|
+
state changes, ensuring cache consistency.
|
1162
|
+
"""
|
1163
|
+
self.namespace_mapper.clear_caches()
|
1164
|
+
|
1165
|
+
# Delegation methods for public API compatibility
|
1166
|
+
def parse_composite_expression_pattern(self, composite_symbol: str) -> Any | None:
|
1167
|
+
"""
|
1168
|
+
Parse a composite expression pattern using the expression parser.
|
1169
|
+
|
1170
|
+
Args:
|
1171
|
+
composite_symbol: The composite expression string to parse
|
1172
|
+
|
1173
|
+
Returns:
|
1174
|
+
Reconstructed expression if successful, None otherwise
|
1175
|
+
"""
|
1176
|
+
return self.expression_parser.parse_composite_expression_pattern(composite_symbol)
|
1177
|
+
|
1178
|
+
def parse_malformed_variable_pattern(self, malformed_symbol: str) -> Any | None:
|
1179
|
+
"""
|
1180
|
+
Parse a malformed variable pattern using the expression parser.
|
1181
|
+
|
1182
|
+
Args:
|
1183
|
+
malformed_symbol: The malformed variable symbol to parse
|
1184
|
+
|
1185
|
+
Returns:
|
1186
|
+
Reconstructed expression if successful, None otherwise
|
1187
|
+
"""
|
1188
|
+
return self.expression_parser.parse_malformed_variable_pattern(malformed_symbol)
|
1189
|
+
|
1190
|
+
def parse_and_rebuild_expression(self, expr: Any, missing_vars: list[str]) -> Any | None:
|
1191
|
+
"""
|
1192
|
+
Parse composite expressions and rebuild them using existing variables.
|
1193
|
+
|
1194
|
+
Args:
|
1195
|
+
expr: The expression to parse and rebuild
|
1196
|
+
missing_vars: List of missing variable names
|
1197
|
+
|
1198
|
+
Returns:
|
1199
|
+
Rebuilt expression if successful, None otherwise
|
1200
|
+
"""
|
1201
|
+
return self.expression_parser.parse_and_rebuild_expression(expr, missing_vars)
|
1202
|
+
|
1203
|
+
|
1204
|
+
# Export all relevant classes
|
1205
|
+
__all__ = [
|
1206
|
+
"EquationReconstructor",
|
1207
|
+
"ExpressionParser",
|
1208
|
+
"NamespaceMapper",
|
1209
|
+
"CompositeExpressionRebuilder",
|
1210
|
+
"DelayedExpressionResolver",
|
1211
|
+
"SolverError",
|
1212
|
+
"EquationReconstructionError",
|
1213
|
+
"MalformedExpressionError",
|
1214
|
+
"NamespaceMappingError",
|
1215
|
+
"PatternReconstructionError",
|
1216
|
+
]
|