qnty 0.0.9__py3-none-any.whl → 0.1.1__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.
Files changed (92) hide show
  1. qnty/__init__.py +2 -3
  2. qnty/constants/__init__.py +10 -0
  3. qnty/constants/numerical.py +18 -0
  4. qnty/constants/solvers.py +6 -0
  5. qnty/constants/tests.py +6 -0
  6. qnty/dimensions/__init__.py +23 -0
  7. qnty/dimensions/base.py +97 -0
  8. qnty/dimensions/field_dims.py +126 -0
  9. qnty/dimensions/field_dims.pyi +128 -0
  10. qnty/dimensions/signature.py +111 -0
  11. qnty/equations/__init__.py +1 -1
  12. qnty/equations/equation.py +118 -155
  13. qnty/equations/system.py +68 -65
  14. qnty/expressions/__init__.py +25 -46
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +46 -68
  17. qnty/expressions/nodes.py +540 -384
  18. qnty/expressions/types.py +70 -0
  19. qnty/problems/__init__.py +145 -0
  20. qnty/problems/composition.py +1101 -0
  21. qnty/problems/problem.py +737 -0
  22. qnty/problems/rules.py +145 -0
  23. qnty/problems/solving.py +1216 -0
  24. qnty/problems/validation.py +127 -0
  25. qnty/quantities/__init__.py +28 -5
  26. qnty/quantities/base_qnty.py +677 -0
  27. qnty/quantities/field_converters.py +24004 -0
  28. qnty/quantities/field_qnty.py +1012 -0
  29. qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
  30. qnty/{generated/quantities.py → quantities/field_vars.py} +829 -444
  31. qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
  32. qnty/solving/manager.py +50 -44
  33. qnty/solving/order.py +181 -133
  34. qnty/solving/solvers/__init__.py +2 -9
  35. qnty/solving/solvers/base.py +27 -37
  36. qnty/solving/solvers/iterative.py +115 -135
  37. qnty/solving/solvers/simultaneous.py +93 -165
  38. qnty/units/__init__.py +1 -0
  39. qnty/{generated/units.py → units/field_units.py} +1700 -991
  40. qnty/units/field_units.pyi +2461 -0
  41. qnty/units/prefixes.py +58 -105
  42. qnty/units/registry.py +76 -89
  43. qnty/utils/__init__.py +16 -0
  44. qnty/utils/caching/__init__.py +23 -0
  45. qnty/utils/caching/manager.py +401 -0
  46. qnty/utils/error_handling/__init__.py +66 -0
  47. qnty/utils/error_handling/context.py +39 -0
  48. qnty/utils/error_handling/exceptions.py +96 -0
  49. qnty/utils/error_handling/handlers.py +171 -0
  50. qnty/utils/logging.py +4 -4
  51. qnty/utils/protocols.py +164 -0
  52. qnty/utils/scope_discovery.py +420 -0
  53. {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/METADATA +1 -1
  54. qnty-0.1.1.dist-info/RECORD +60 -0
  55. qnty/_backup/problem_original.py +0 -1251
  56. qnty/_backup/quantity.py +0 -63
  57. qnty/codegen/cli.py +0 -125
  58. qnty/codegen/generators/data/unit_data.json +0 -8807
  59. qnty/codegen/generators/data_processor.py +0 -345
  60. qnty/codegen/generators/dimensions_gen.py +0 -434
  61. qnty/codegen/generators/doc_generator.py +0 -141
  62. qnty/codegen/generators/out/dimension_mapping.json +0 -974
  63. qnty/codegen/generators/out/dimension_metadata.json +0 -123
  64. qnty/codegen/generators/out/units_metadata.json +0 -223
  65. qnty/codegen/generators/quantities_gen.py +0 -159
  66. qnty/codegen/generators/setters_gen.py +0 -178
  67. qnty/codegen/generators/stubs_gen.py +0 -167
  68. qnty/codegen/generators/units_gen.py +0 -295
  69. qnty/expressions/cache.py +0 -94
  70. qnty/generated/dimensions.py +0 -514
  71. qnty/problem/__init__.py +0 -91
  72. qnty/problem/base.py +0 -142
  73. qnty/problem/composition.py +0 -385
  74. qnty/problem/composition_mixin.py +0 -382
  75. qnty/problem/equations.py +0 -413
  76. qnty/problem/metaclass.py +0 -302
  77. qnty/problem/reconstruction.py +0 -1016
  78. qnty/problem/solving.py +0 -180
  79. qnty/problem/validation.py +0 -64
  80. qnty/problem/variables.py +0 -239
  81. qnty/quantities/expression_quantity.py +0 -314
  82. qnty/quantities/quantity.py +0 -428
  83. qnty/quantities/typed_quantity.py +0 -215
  84. qnty/validation/__init__.py +0 -0
  85. qnty/validation/registry.py +0 -0
  86. qnty/validation/rules.py +0 -167
  87. qnty-0.0.9.dist-info/RECORD +0 -63
  88. /qnty/{codegen → extensions}/__init__.py +0 -0
  89. /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
  90. /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
  91. /qnty/{generated → extensions/reporting}/__init__.py +0 -0
  92. {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/WHEEL +0 -0
@@ -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
+ ]