qnty 0.0.8__py3-none-any.whl → 0.0.9__py3-none-any.whl

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