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.
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 +539 -384
  18. qnty/expressions/types.py +70 -0
  19. qnty/problems/__init__.py +145 -0
  20. qnty/problems/composition.py +1031 -0
  21. qnty/problems/problem.py +695 -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} +754 -432
  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.0.dist-info}/METADATA +1 -1
  54. qnty-0.1.0.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.0.dist-info}/WHEEL +0 -0
@@ -1,1251 +0,0 @@
1
- """
2
- Core EngineeringProblem class for engineering problem solving.
3
-
4
- This module provides the main EngineeringProblem class that coordinates
5
- all aspects of engineering problem definition, solving, and analysis.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from collections.abc import Callable
11
- from typing import TYPE_CHECKING, Any
12
-
13
- from qnty.equations import Equation, EquationSystem
14
-
15
- if TYPE_CHECKING:
16
- from qnty.expressions import BinaryOperation, Constant, VariableReference
17
- from qnty.utils.logging import get_logger
18
- from qnty.solving.order import Order
19
- from qnty.problem.reconstruction import EquationReconstructor
20
- from qnty.problem.metaclass import ProblemMeta
21
- from qnty.solving.solvers import SolverManager
22
- from qnty.quantities import Quantity as Qty
23
- from qnty.quantities import TypeSafeVariable as Variable
24
- from qnty.generated.units import DimensionlessUnits
25
-
26
- # Constants
27
- MAX_ITERATIONS_DEFAULT = 100
28
- TOLERANCE_DEFAULT = 1e-10
29
- MATHEMATICAL_OPERATORS = ['+', '-', '*', '/', ' / ', ' * ', ' + ', ' - ']
30
- COMMON_COMPOSITE_VARIABLES = ['P', 'c', 'S', 'E', 'W', 'Y']
31
-
32
-
33
- # Custom Exceptions
34
- class VariableNotFoundError(KeyError):
35
- """Raised when trying to access a variable that doesn't exist."""
36
- pass
37
-
38
-
39
- class EquationValidationError(ValueError):
40
- """Raised when an equation fails validation."""
41
- pass
42
-
43
-
44
- class SolverError(RuntimeError):
45
- """Raised when the solving process fails."""
46
- pass
47
-
48
-
49
- class Problem(metaclass=ProblemMeta):
50
- """
51
- Main container class for engineering problems.
52
-
53
- This class coordinates all aspects of engineering problem definition, solving, and analysis.
54
- It supports both programmatic problem construction and class-level inheritance patterns
55
- for defining domain-specific engineering problems.
56
-
57
- Key Features:
58
- - Automatic dependency graph construction and topological solving order
59
- - Dual solving approach: SymPy symbolic solving with numerical fallback
60
- - Sub-problem composition with automatic variable namespacing
61
- - Comprehensive validation and error handling
62
- - Professional report generation capabilities
63
-
64
- Usage Patterns:
65
- 1. Inheritance Pattern (Recommended for domain problems):
66
- class MyProblem(EngineeringProblem):
67
- x = Variable("x", Qty(5.0, length))
68
- y = Variable("y", Qty(0.0, length), is_known=False)
69
- eq = y.equals(x * 2)
70
-
71
- 2. Programmatic Pattern (For dynamic problems):
72
- problem = EngineeringProblem("Dynamic Problem")
73
- problem.add_variables(x, y)
74
- problem.add_equation(y.equals(x * 2))
75
-
76
- 3. Composition Pattern (For reusable sub-problems):
77
- class ComposedProblem(EngineeringProblem):
78
- sub1 = create_sub_problem()
79
- sub2 = create_sub_problem()
80
- # Equations can reference sub1.variable, sub2.variable
81
-
82
- Attributes:
83
- name (str): Human-readable name for the problem
84
- description (str): Detailed description of the problem
85
- variables (dict[str, Variable]): All variables in the problem
86
- equations (list[Equation]): All equations in the problem
87
- is_solved (bool): Whether the problem has been successfully solved
88
- solution (dict[str, Variable]): Solved variable values
89
- sub_problems (dict[str, EngineeringProblem]): Integrated sub-problems
90
- """
91
-
92
- def __init__(self, name: str | None = None, description: str = ""):
93
- # Handle subclass mode (class-level name/description) vs explicit name
94
- self.name = name or getattr(self.__class__, 'name', self.__class__.__name__)
95
- self.description = description or getattr(self.__class__, 'description', "")
96
-
97
- # Core storage
98
- self.variables: dict[str, Variable] = {}
99
- self.equations: list[Equation] = []
100
-
101
- # Internal systems
102
- self.equation_system = EquationSystem()
103
- self.dependency_graph = Order()
104
-
105
- # Solving state
106
- self.is_solved = False
107
- self.solution: dict[str, Variable] = {}
108
- self.solving_history: list[dict[str, Any]] = []
109
-
110
- # Performance optimization caches
111
- self._known_variables_cache: dict[str, Variable] | None = None
112
- self._unknown_variables_cache: dict[str, Variable] | None = None
113
- self._cache_dirty = True
114
-
115
- # Validation and warning system
116
- self.warnings: list[dict[str, Any]] = []
117
- self.validation_checks: list[Callable] = []
118
-
119
- self.logger = get_logger()
120
- self.solver_manager = SolverManager(self.logger)
121
-
122
- # Sub-problem composition support
123
- self.sub_problems: dict[str, Problem] = {}
124
- self.variable_aliases: dict[str, str] = {} # Maps alias -> original variable symbol
125
-
126
- # Initialize equation reconstructor
127
- self.equation_reconstructor = EquationReconstructor(self)
128
-
129
- # Auto-populate from class-level variables and equations (subclass pattern)
130
- self._extract_from_class_variables()
131
-
132
- def _extract_from_class_variables(self):
133
- """Extract variables, equations, and sub-problems from class-level definitions."""
134
- self._extract_sub_problems()
135
- self._extract_direct_variables()
136
- self._recreate_validation_checks()
137
- self._create_composite_equations()
138
- self._extract_equations()
139
-
140
- def _extract_sub_problems(self):
141
- """Extract and integrate sub-problems from class-level definitions."""
142
- if hasattr(self.__class__, '_original_sub_problems'):
143
- original_sub_problems = getattr(self.__class__, '_original_sub_problems', {})
144
- for attr_name, sub_problem in original_sub_problems.items():
145
- self._integrate_sub_problem(sub_problem, attr_name)
146
-
147
- def _extract_direct_variables(self):
148
- """Extract direct variables from class-level definitions."""
149
- processed_symbols = set()
150
-
151
- # Single pass through class attributes to collect variables
152
- for attr_name, attr_value in self._get_class_attributes():
153
- if isinstance(attr_value, Variable):
154
- # Set symbol based on attribute name (T_bar, P, etc.)
155
- attr_value.symbol = attr_name
156
-
157
- # Skip if we've already processed this symbol
158
- if attr_value.symbol in processed_symbols:
159
- continue
160
- processed_symbols.add(attr_value.symbol)
161
-
162
- # Clone variable to avoid shared state between instances
163
- cloned_var = self._clone_variable(attr_value)
164
- self.add_variable(cloned_var)
165
- # Set the same cloned variable object as instance attribute
166
- # Use super() to bypass our custom __setattr__ during initialization
167
- super().__setattr__(attr_name, cloned_var)
168
-
169
- def _extract_equations(self):
170
- """Extract and process equations from class-level definitions."""
171
- equations_to_process = self._collect_class_equations()
172
-
173
- for attr_name, equation in equations_to_process:
174
- try:
175
- if self._process_equation(attr_name, equation):
176
- setattr(self, attr_name, equation)
177
- except Exception as e:
178
- # Log but continue - some equations might fail during class definition
179
- self.logger.warning(f"Failed to process equation {attr_name}: {e}")
180
- # Still set the original equation as attribute
181
- setattr(self, attr_name, equation)
182
-
183
- def _get_class_attributes(self) -> list[tuple[str, Any]]:
184
- """Get all non-private class attributes efficiently."""
185
- return [(attr_name, getattr(self.__class__, attr_name))
186
- for attr_name in dir(self.__class__)
187
- if not attr_name.startswith('_')]
188
-
189
- def _collect_class_equations(self) -> list[tuple[str, Equation]]:
190
- """Collect all equation objects from class attributes."""
191
- equations_to_process = []
192
- for attr_name, attr_value in self._get_class_attributes():
193
- if isinstance(attr_value, Equation):
194
- equations_to_process.append((attr_name, attr_value))
195
- return equations_to_process
196
-
197
- def _process_equation(self, attr_name: str, equation: Equation) -> bool:
198
- """Process a single equation and add it to the problem if valid."""
199
- return self._process_equation_impl(attr_name, equation)
200
-
201
- def _process_equation_impl(self, attr_name: str, equation: Equation) -> bool:
202
- """
203
- Process a single equation and determine if it should be added.
204
- Returns True if the equation was successfully processed.
205
- """
206
- # First, update variable references to use symbols instead of names
207
- updated_equation = self._update_equation_variable_references(equation)
208
-
209
- # Check if this equation contains delayed expressions
210
- try:
211
- has_delayed = self.equation_reconstructor.contains_delayed_expressions(updated_equation)
212
- if has_delayed:
213
- return self._handle_delayed_equation(attr_name, updated_equation)
214
- except Exception as e:
215
- self.logger.debug(f"Error checking delayed expressions for {attr_name}: {e}")
216
-
217
- # Check if this equation has invalid self-references
218
- try:
219
- has_self_ref = self._has_invalid_self_references(updated_equation)
220
- if has_self_ref:
221
- self.logger.debug(f"Skipping invalid self-referencing equation {attr_name}: {updated_equation}")
222
- return False
223
- except Exception as e:
224
- self.logger.debug(f"Error checking self-references for {attr_name}: {e}")
225
-
226
- # Check if equation references non-existent variables
227
- try:
228
- has_missing = self._equation_has_missing_variables(updated_equation)
229
- if has_missing:
230
- return self._handle_equation_with_missing_variables(attr_name, updated_equation)
231
- except Exception as e:
232
- self.logger.debug(f"Error checking missing variables for {attr_name}: {e}")
233
-
234
- # Process valid equation
235
- self.add_equation(updated_equation)
236
- return True
237
-
238
- def _handle_delayed_equation(self, attr_name: str, equation: Equation) -> bool:
239
- """Handle equations with delayed expressions."""
240
- resolved_equation = self.equation_reconstructor.resolve_delayed_equation(equation)
241
- if resolved_equation:
242
- self.add_equation(resolved_equation)
243
- setattr(self, attr_name, resolved_equation)
244
- return True
245
- else:
246
- self.logger.debug(f"Skipping unresolvable delayed equation {attr_name}: {equation}")
247
- return False
248
-
249
- def _handle_equation_with_missing_variables(self, attr_name: str, equation: Equation) -> bool:
250
- """Handle equations that reference missing variables."""
251
- # Handle conditional equations more carefully
252
- if self._is_conditional_equation(equation):
253
- return self._handle_conditional_equation(attr_name, equation)
254
-
255
- # Only attempt reconstruction for simple mathematical expressions from composition
256
- if self.equation_reconstructor.should_attempt_reconstruction(equation):
257
- return self._attempt_equation_reconstruction(attr_name, equation)
258
- else:
259
- # Skip other problematic equations
260
- self.logger.debug(f"Skipping equation with missing variables {attr_name}: {equation}")
261
- return False
262
-
263
- def _handle_conditional_equation(self, attr_name: str, equation: Equation) -> bool:
264
- """Handle conditional equations with missing variables."""
265
- missing_vars = equation.get_all_variables() - set(self.variables.keys())
266
-
267
-
268
- # Skip conditional equations from sub-problems in composed systems
269
- if self.sub_problems and self._is_conditional_equation_from_subproblem(equation, attr_name):
270
- self.logger.debug(f"Skipping conditional equation {attr_name} from sub-problem in composed system")
271
- return False
272
-
273
- # Check for composite expressions that might be reconstructable
274
- unresolvable_vars = [var for var in missing_vars
275
- if any(op in var for op in MATHEMATICAL_OPERATORS)]
276
-
277
- if self.sub_problems and unresolvable_vars:
278
- # Before skipping, try to reconstruct conditional equations with composite expressions
279
- self.logger.debug(f"Attempting to reconstruct conditional equation {attr_name} with composite variables: {unresolvable_vars}")
280
- reconstructed_equation = self.equation_reconstructor.reconstruct_composite_expressions_generically(equation)
281
- if reconstructed_equation:
282
- self.logger.debug(f"Successfully reconstructed conditional equation {attr_name}: {reconstructed_equation}")
283
- self.add_equation(reconstructed_equation)
284
- setattr(self, attr_name, reconstructed_equation)
285
- return True
286
- else:
287
- self.logger.debug(f"Failed to reconstruct conditional equation {attr_name}, trying simple substitution")
288
- # Try simple substitution for basic arithmetic expressions
289
- reconstructed_equation = self._try_simple_substitution(equation, missing_vars)
290
- if reconstructed_equation:
291
- self.add_equation(reconstructed_equation)
292
- return True
293
- else:
294
- self.add_equation(equation)
295
- return True
296
- else:
297
- # Try to add the conditional equation even with missing simple variables
298
- self.add_equation(equation)
299
- return True
300
-
301
- def _try_simple_substitution(self, _equation: Equation, _missing_vars: set[str]) -> Equation | None:
302
- """
303
- Try simple substitution for basic arithmetic expressions in conditional equations.
304
-
305
- The real issue is that nested expressions in conditionals aren't being handled properly.
306
- For now, just return None and let the equation be added as-is.
307
- """
308
- return None
309
-
310
- def _fix_variable_references(self, equation: Equation) -> Equation:
311
- """
312
- Fix VariableReferences in equation expressions to point to Variables in problem.variables.
313
-
314
- This resolves issues where expression trees contain VariableReferences pointing to
315
- proxy Variables from class creation time instead of the actual Variables in the problem.
316
- """
317
- try:
318
- # Fix the RHS expression
319
- fixed_rhs = self._fix_expression_variables(equation.rhs)
320
-
321
- # Create new equation with fixed RHS (LHS should already be correct)
322
- return Equation(equation.name, equation.lhs, fixed_rhs)
323
-
324
- except Exception as e:
325
- self.logger.debug(f"Error fixing variable references in equation {equation.name}: {e}")
326
- return equation # Return original if fixing fails
327
-
328
- def _fix_expression_variables(self, expr):
329
- """
330
- Recursively fix VariableReferences in an expression tree to point to correct Variables.
331
- """
332
-
333
- if isinstance(expr, VariableReference):
334
- # Check if this VariableReference points to the wrong Variable
335
- symbol = getattr(expr, 'symbol', None)
336
- if symbol and symbol in self.variables:
337
- correct_var = self.variables[symbol]
338
- if expr.variable is not correct_var:
339
- # Create new VariableReference pointing to correct Variable
340
- return VariableReference(correct_var)
341
- return expr
342
-
343
- elif isinstance(expr, BinaryOperation):
344
- # Recursively fix left and right operands
345
- fixed_left = self._fix_expression_variables(expr.left)
346
- fixed_right = self._fix_expression_variables(expr.right)
347
- return BinaryOperation(expr.operator, fixed_left, fixed_right)
348
-
349
- elif hasattr(expr, 'operand'):
350
- # Recursively fix operand
351
- fixed_operand = self._fix_expression_variables(expr.operand)
352
- return type(expr)(expr.operator, fixed_operand)
353
-
354
- elif hasattr(expr, 'function_name'):
355
- # Recursively fix left and right operands
356
- fixed_left = self._fix_expression_variables(expr.left)
357
- fixed_right = self._fix_expression_variables(expr.right)
358
- return type(expr)(expr.function_name, fixed_left, fixed_right)
359
-
360
- elif isinstance(expr, Constant):
361
- return expr
362
-
363
- else:
364
- # Unknown expression type, return as-is
365
- return expr
366
-
367
- def _attempt_equation_reconstruction(self, attr_name: str, equation: Equation) -> bool:
368
- """Attempt to reconstruct equations with composite expressions."""
369
- missing_vars = equation.get_all_variables() - set(self.variables.keys())
370
- self.logger.debug(f"Attempting to reconstruct equation {attr_name} with missing variables: {missing_vars}")
371
-
372
- reconstructed_equation = self.equation_reconstructor.reconstruct_composite_expressions_generically(equation)
373
- if reconstructed_equation:
374
- self.logger.debug(f"Successfully reconstructed {attr_name}: {reconstructed_equation}")
375
- self.add_equation(reconstructed_equation)
376
- setattr(self, attr_name, reconstructed_equation)
377
- return True
378
- else:
379
- self.logger.debug(f"Failed to reconstruct equation {attr_name}: {equation}")
380
- return False
381
-
382
- def _integrate_sub_problem(self, sub_problem: Problem, namespace: str) -> None:
383
- """
384
- Integrate a sub-problem by flattening its variables with namespace prefixes.
385
- Creates a simple dotted access pattern: self.header.P becomes self.header_P
386
- """
387
- self.sub_problems[namespace] = sub_problem
388
-
389
- # Get proxy configurations if available
390
- proxy_configs = getattr(self.__class__, '_proxy_configurations', {}).get(namespace, {})
391
-
392
- # Create a namespace object for dotted access (self.header.P)
393
- namespace_obj = type('SubProblemNamespace', (), {})()
394
-
395
- # Add all sub-problem variables with namespace prefixes
396
- for var_symbol, var in sub_problem.variables.items():
397
- namespaced_var = self._create_namespaced_variable(var, var_symbol, namespace, proxy_configs)
398
- self.add_variable(namespaced_var)
399
-
400
- # Set both namespaced access (self.header_P) and dotted access (self.header.P)
401
- if namespaced_var.symbol is not None:
402
- super().__setattr__(namespaced_var.symbol, namespaced_var)
403
- setattr(namespace_obj, var_symbol, namespaced_var)
404
-
405
- # Set the namespace object for dotted access
406
- super().__setattr__(namespace, namespace_obj)
407
-
408
- # Also add all sub-problem equations (they'll be namespaced automatically)
409
- for equation in sub_problem.equations:
410
- try:
411
- # Skip conditional equations for variables that are overridden to known values in composition
412
- if self._should_skip_subproblem_equation(equation, namespace):
413
- continue
414
-
415
- namespaced_equation = self._namespace_equation(equation, namespace)
416
- if namespaced_equation:
417
- self.add_equation(namespaced_equation)
418
- except Exception as e:
419
- self.logger.debug(f"Failed to namespace equation from {namespace}: {e}")
420
-
421
- def _create_namespaced_variable(self, var: Variable, var_symbol: str, namespace: str, proxy_configs: dict) -> Variable:
422
- """Create a namespaced variable with proper configuration."""
423
- namespaced_symbol = f"{namespace}_{var_symbol}"
424
- namespaced_var = self._clone_variable(var)
425
- namespaced_var.symbol = namespaced_symbol
426
- namespaced_var.name = f"{var.name} ({namespace.title()})"
427
-
428
- # Apply proxy configuration if available
429
- if var_symbol in proxy_configs:
430
- config = proxy_configs[var_symbol]
431
- namespaced_var.quantity = config['quantity']
432
- namespaced_var.is_known = config['is_known']
433
-
434
- return namespaced_var
435
-
436
- def _namespace_equation(self, equation: Equation, namespace: str) -> Equation | None:
437
- """
438
- Create a namespaced version of an equation by prefixing all variable references.
439
- """
440
- try:
441
- # Get all variable symbols in the equation
442
- variables_in_eq = equation.get_all_variables()
443
-
444
- # Create mapping from original symbols to namespaced symbols
445
- symbol_mapping = {}
446
- for var_symbol in variables_in_eq:
447
- namespaced_symbol = f"{namespace}_{var_symbol}"
448
- if namespaced_symbol in self.variables:
449
- symbol_mapping[var_symbol] = namespaced_symbol
450
-
451
- if not symbol_mapping:
452
- return None
453
-
454
- # Create new equation with namespaced references
455
- # For LHS, we need a Variable object to call .equals()
456
- # For RHS, we need proper expression structure
457
- namespaced_lhs = self._namespace_expression_for_lhs(equation.lhs, symbol_mapping)
458
- namespaced_rhs = self._namespace_expression(equation.rhs, symbol_mapping)
459
-
460
- if namespaced_lhs and namespaced_rhs:
461
- equals_method = getattr(namespaced_lhs, 'equals', None)
462
- if equals_method:
463
- return equals_method(namespaced_rhs)
464
-
465
- return None
466
-
467
- except Exception:
468
- return None
469
-
470
- def _namespace_expression(self, expr, symbol_mapping):
471
- """
472
- Create a namespaced version of an expression by replacing variable references.
473
- """
474
-
475
- # Handle variable references
476
- if isinstance(expr, VariableReference):
477
- return self._namespace_variable_reference(expr, symbol_mapping)
478
- elif hasattr(expr, 'symbol') and expr.symbol in symbol_mapping:
479
- return self._namespace_variable_object(expr, symbol_mapping)
480
-
481
- # Handle operations
482
- elif isinstance(expr, BinaryOperation):
483
- return self._namespace_binary_operation(expr, symbol_mapping)
484
- elif hasattr(expr, 'operand'):
485
- return self._namespace_unary_operation(expr, symbol_mapping)
486
- elif hasattr(expr, 'function_name'):
487
- return self._namespace_binary_function(expr, symbol_mapping)
488
- elif isinstance(expr, Constant):
489
- return expr
490
- else:
491
- return expr
492
-
493
- def _namespace_variable_reference(self, expr, symbol_mapping):
494
- """Namespace a VariableReference object."""
495
- if expr.symbol in symbol_mapping:
496
- namespaced_symbol = symbol_mapping[expr.symbol]
497
- if namespaced_symbol in self.variables:
498
- return VariableReference(self.variables[namespaced_symbol])
499
- return expr
500
-
501
- def _namespace_variable_object(self, expr, symbol_mapping):
502
- """Namespace a Variable object."""
503
- namespaced_symbol = symbol_mapping[expr.symbol]
504
- if namespaced_symbol in self.variables:
505
- # Return VariableReference for use in expressions, not the Variable itself
506
- return VariableReference(self.variables[namespaced_symbol])
507
- return expr
508
-
509
- def _namespace_binary_operation(self, expr, symbol_mapping):
510
- """Namespace a BinaryOperation."""
511
- namespaced_left = self._namespace_expression(expr.left, symbol_mapping)
512
- namespaced_right = self._namespace_expression(expr.right, symbol_mapping)
513
- return BinaryOperation(expr.operator, namespaced_left, namespaced_right)
514
-
515
- def _namespace_unary_operation(self, expr, symbol_mapping):
516
- """Namespace a UnaryFunction."""
517
- namespaced_operand = self._namespace_expression(expr.operand, symbol_mapping)
518
- return type(expr)(expr.operator, namespaced_operand)
519
-
520
- def _namespace_binary_function(self, expr, symbol_mapping):
521
- """Namespace a BinaryFunction."""
522
- namespaced_left = self._namespace_expression(expr.left, symbol_mapping)
523
- namespaced_right = self._namespace_expression(expr.right, symbol_mapping)
524
- return type(expr)(expr.function_name, namespaced_left, namespaced_right)
525
-
526
- def _namespace_expression_for_lhs(self, expr, symbol_mapping):
527
- """
528
- Create a namespaced version of an expression for LHS, returning Variable objects.
529
- """
530
-
531
- if isinstance(expr, VariableReference):
532
- symbol = getattr(expr, 'symbol', None)
533
- if symbol and symbol in symbol_mapping:
534
- namespaced_symbol = symbol_mapping[symbol]
535
- if namespaced_symbol in self.variables:
536
- return self.variables[namespaced_symbol]
537
- # If we can't find a mapping, return None since VariableReference doesn't have .equals()
538
- return None
539
- elif hasattr(expr, 'symbol') and expr.symbol in symbol_mapping:
540
- # This is a Variable object
541
- namespaced_symbol = symbol_mapping[expr.symbol]
542
- if namespaced_symbol in self.variables:
543
- return self.variables[namespaced_symbol]
544
- return expr
545
- else:
546
- return expr
547
-
548
- def _clone_variable(self, variable: Variable) -> Variable:
549
- """Create a copy of a variable to avoid shared state without corrupting global units."""
550
- # Create a new Variable with the same properties but avoid deepcopy
551
- # which can corrupt global unit objects
552
- cloned = Variable(
553
- name=variable.name,
554
- expected_dimension=variable.expected_dimension,
555
- is_known=variable.is_known
556
- )
557
- # Set attributes that are not part of constructor
558
- cloned.symbol = variable.symbol
559
- cloned.quantity = variable.quantity # Keep reference to same quantity - units must not be copied
560
-
561
- # Ensure the cloned variable has fresh validation checks
562
- if hasattr(variable, 'validation_checks'):
563
- try:
564
- setattr(cloned, 'validation_checks', [])
565
- except (AttributeError, TypeError):
566
- # validation_checks might be read-only or not settable
567
- pass
568
- return cloned
569
-
570
- def _recreate_validation_checks(self):
571
- """Collect and integrate validation checks from class-level Check objects."""
572
- # Clear existing checks
573
- self.validation_checks = []
574
-
575
- # Collect Check objects from metaclass
576
- class_checks = getattr(self.__class__, '_class_checks', {})
577
-
578
- for check in class_checks.values():
579
- # Create a validation function from the Check object
580
- def make_check_function(check_obj):
581
- def check_function(problem_instance):
582
- return check_obj.evaluate(problem_instance.variables)
583
- return check_function
584
-
585
- self.validation_checks.append(make_check_function(check))
586
-
587
- def _create_composite_equations(self):
588
- """
589
- Create composite equations for common patterns in sub-problems.
590
- This handles equations like P = min(header.P, branch.P) automatically.
591
- """
592
- if not self.sub_problems:
593
- return
594
-
595
- # Common composite patterns to auto-generate
596
- for var_name in COMMON_COMPOSITE_VARIABLES:
597
- # Check if this variable exists in multiple sub-problems
598
- sub_problem_vars = []
599
- for namespace in self.sub_problems:
600
- namespaced_symbol = f"{namespace}_{var_name}"
601
- if namespaced_symbol in self.variables:
602
- sub_problem_vars.append(self.variables[namespaced_symbol])
603
-
604
- # If we have the variable in multiple sub-problems and no direct variable exists
605
- if len(sub_problem_vars) >= 2 and var_name in self.variables:
606
- # Check if a composite equation already exists
607
- equation_attr_name = f"{var_name}_eqn"
608
- if hasattr(self.__class__, equation_attr_name):
609
- # Skip auto-creation since explicit equation exists
610
- continue
611
-
612
- # Auto-create composite equation
613
- try:
614
- from qnty.expressions import min_expr
615
- composite_var = self.variables[var_name]
616
- if not composite_var.is_known: # Only for unknown variables
617
- composite_expr = min_expr(*sub_problem_vars)
618
- equals_method = getattr(composite_var, 'equals', None)
619
- if equals_method:
620
- composite_eq = equals_method(composite_expr)
621
- self.add_equation(composite_eq)
622
- setattr(self, f"{var_name}_eqn", composite_eq)
623
- except Exception as e:
624
- self.logger.debug(f"Failed to create composite equation for {var_name}: {e}")
625
-
626
- def _get_equation_lhs_symbol(self, equation: Equation) -> str | None:
627
- """Safely extract the symbol from equation's left-hand side."""
628
- return getattr(equation.lhs, 'symbol', None)
629
-
630
- def _is_conditional_equation(self, equation: Equation) -> bool:
631
- """Check if an equation is a conditional equation."""
632
- return 'cond(' in str(equation)
633
-
634
- def _equation_has_missing_variables(self, equation: Equation) -> bool:
635
- """Check if an equation references variables that don't exist in this problem."""
636
- try:
637
- all_vars = equation.get_all_variables()
638
- missing_vars = [var for var in all_vars if var not in self.variables]
639
- return len(missing_vars) > 0
640
- except Exception:
641
- return False
642
-
643
- def _has_invalid_self_references(self, equation: Equation) -> bool:
644
- """
645
- Check if an equation has invalid self-references.
646
- This catches malformed equations like 'c = max(c, c)' from class definition.
647
- """
648
- try:
649
- # Get LHS variable - check if it has symbol attribute
650
- lhs_symbol = self._get_equation_lhs_symbol(equation)
651
- if lhs_symbol is None:
652
- return False
653
-
654
- # Get all variables referenced in RHS
655
- rhs_vars = equation.rhs.get_variables() if hasattr(equation.rhs, 'get_variables') else set()
656
-
657
- # Check if the LHS variable appears multiple times in RHS (indicating self-reference)
658
- # This is a heuristic - a proper implementation would parse the expression tree
659
- equation_str = str(equation)
660
- if lhs_symbol in rhs_vars:
661
- # For conditional equations, self-references are often valid (as fallback values)
662
- if 'cond(' in equation_str:
663
- return False # Allow self-references in conditional equations
664
-
665
- # Count occurrences of the variable in the equation string
666
- count = equation_str.count(lhs_symbol)
667
- if count > 2: # LHS + multiple RHS occurrences
668
- return True
669
-
670
- return False
671
-
672
- except Exception:
673
- return False
674
-
675
-
676
- def _is_conditional_equation_from_subproblem(self, equation: Equation, _equation_name: str) -> bool:
677
- """
678
- Check if a conditional equation comes from a sub-problem and should be skipped in composed systems.
679
-
680
- In composed systems, if a sub-problem's conditional variable is already set to a known value,
681
- we don't want to include the conditional equation that would override that known value.
682
- """
683
- try:
684
- # Check if this is a conditional equation with a known LHS variable
685
- lhs_symbol = self._get_equation_lhs_symbol(equation)
686
- if lhs_symbol is not None:
687
-
688
- # Check if the LHS variable already exists and is known
689
- if lhs_symbol in self.variables:
690
- var = self.variables[lhs_symbol]
691
- # If the variable is already known (set explicitly in composition),
692
- # and this conditional equation references missing variables, skip it
693
- missing_vars = equation.get_all_variables() - set(self.variables.keys())
694
- if missing_vars and var.is_known:
695
- # This is a sub-problem's conditional equation for a variable that's already known
696
- # No point including it since the value is already determined
697
- return True
698
-
699
- return False
700
-
701
- except Exception:
702
- return False
703
-
704
- def _should_skip_subproblem_equation(self, equation: Equation, namespace: str) -> bool:
705
- """
706
- Check if an equation from a sub-problem should be skipped during integration.
707
-
708
- Skip conditional equations for variables that are set to known values in the composed problem.
709
- """
710
- try:
711
- # Check if this is a conditional equation
712
- if not self._is_conditional_equation(equation):
713
- return False
714
-
715
- # Check if the LHS variable would be set to a known value in composition
716
- original_symbol = self._get_equation_lhs_symbol(equation)
717
- if original_symbol is not None:
718
- namespaced_symbol = f"{namespace}_{original_symbol}"
719
-
720
- # Check if this namespaced variable exists and is already known
721
- if namespaced_symbol in self.variables:
722
- var = self.variables[namespaced_symbol]
723
- if var.is_known:
724
- # The variable is already set to a known value in composition,
725
- # so skip the conditional equation that would override it
726
- self.logger.debug(f"Skipping conditional equation for {namespaced_symbol} (already known: {var.quantity})")
727
- return True
728
-
729
- return False
730
-
731
- except Exception:
732
- return False
733
-
734
- # =============================================================================
735
- # VARIABLE MANAGEMENT METHODS
736
- # =============================================================================
737
-
738
- def add_variable(self, variable: Variable) -> None:
739
- """
740
- Add a variable to the problem.
741
-
742
- The variable will be available for use in equations and can be accessed
743
- via both dictionary notation (problem['symbol']) and attribute notation
744
- (problem.symbol).
745
-
746
- Args:
747
- variable: Variable object to add to the problem
748
-
749
- Note:
750
- If a variable with the same symbol already exists, it will be replaced
751
- and a warning will be logged.
752
- """
753
- if variable.symbol in self.variables:
754
- self.logger.warning(f"Variable {variable.symbol} already exists. Replacing.")
755
-
756
- if variable.symbol is not None:
757
- self.variables[variable.symbol] = variable
758
- # Set parent problem reference for dependency invalidation
759
- try:
760
- setattr(variable, '_parent_problem', self)
761
- except (AttributeError, TypeError):
762
- # _parent_problem might not be settable
763
- pass
764
- # Also set as instance attribute for dot notation access
765
- if variable.symbol is not None:
766
- setattr(self, variable.symbol, variable)
767
- self.is_solved = False
768
- self._invalidate_caches()
769
-
770
- def add_variables(self, *variables: Variable) -> None:
771
- """Add multiple variables to the problem."""
772
- for var in variables:
773
- self.add_variable(var)
774
-
775
- def get_variable(self, symbol: str) -> Variable:
776
- """Get a variable by its symbol."""
777
- if symbol not in self.variables:
778
- raise VariableNotFoundError(f"Variable '{symbol}' not found in problem '{self.name}'.")
779
- return self.variables[symbol]
780
-
781
- def get_known_variables(self) -> dict[str, Variable]:
782
- """Get all known variables."""
783
- if self._cache_dirty or self._known_variables_cache is None:
784
- self._update_variable_caches()
785
- return self._known_variables_cache.copy() if self._known_variables_cache else {}
786
-
787
- def get_unknown_variables(self) -> dict[str, Variable]:
788
- """Get all unknown variables."""
789
- if self._cache_dirty or self._unknown_variables_cache is None:
790
- self._update_variable_caches()
791
- return self._unknown_variables_cache.copy() if self._unknown_variables_cache else {}
792
-
793
- def get_known_symbols(self) -> set[str]:
794
- """Get symbols of all known variables."""
795
- return {symbol for symbol, var in self.variables.items() if var.is_known}
796
-
797
- def get_unknown_symbols(self) -> set[str]:
798
- """Get symbols of all unknown variables."""
799
- return {symbol for symbol, var in self.variables.items() if not var.is_known}
800
-
801
- def get_known_variable_symbols(self) -> set[str]:
802
- """Alias for get_known_symbols for compatibility."""
803
- return self.get_known_symbols()
804
-
805
- def get_unknown_variable_symbols(self) -> set[str]:
806
- """Alias for get_unknown_symbols for compatibility."""
807
- return self.get_unknown_symbols()
808
-
809
- # Properties for compatibility
810
- @property
811
- def known_variables(self) -> dict[str, Variable]:
812
- """Get all variables marked as known."""
813
- return self.get_known_variables()
814
-
815
- @property
816
- def unknown_variables(self) -> dict[str, Variable]:
817
- """Get all variables marked as unknown."""
818
- return self.get_unknown_variables()
819
-
820
- def mark_unknown(self, *symbols: str) -> Problem:
821
- """Mark variables as unknown (to be solved for)."""
822
- for symbol in symbols:
823
- if symbol in self.variables:
824
- self.variables[symbol].mark_unknown()
825
- else:
826
- raise VariableNotFoundError(f"Variable '{symbol}' not found in problem '{self.name}'")
827
- self.is_solved = False
828
- self._invalidate_caches()
829
- return self
830
-
831
- def mark_known(self, **symbol_values: Qty) -> Problem:
832
- """Mark variables as known and set their values."""
833
- for symbol, quantity in symbol_values.items():
834
- if symbol in self.variables:
835
- self.variables[symbol].mark_known(quantity)
836
- else:
837
- raise VariableNotFoundError(f"Variable '{symbol}' not found in problem '{self.name}'")
838
- self.is_solved = False
839
- self._invalidate_caches()
840
- return self
841
-
842
- def invalidate_dependents(self, changed_variable_symbol: str) -> None:
843
- """
844
- Mark all variables that depend on the changed variable as unknown.
845
- This ensures they get recalculated when the problem is re-solved.
846
-
847
- Args:
848
- changed_variable_symbol: Symbol of the variable whose value changed
849
- """
850
- if not hasattr(self, 'dependency_graph') or not self.dependency_graph:
851
- # If dependency graph hasn't been built yet, we can't invalidate
852
- return
853
-
854
- # Get all variables that depend on the changed variable
855
- dependent_vars = self.dependency_graph.graph.get(changed_variable_symbol, [])
856
-
857
- # Mark each dependent variable as unknown
858
- for dependent_symbol in dependent_vars:
859
- if dependent_symbol in self.variables:
860
- var = self.variables[dependent_symbol]
861
- # Only mark as unknown if it was previously solved (known)
862
- if var.is_known:
863
- var.mark_unknown()
864
- # Recursively invalidate variables that depend on this one
865
- self.invalidate_dependents(dependent_symbol)
866
-
867
- # Mark problem as needing re-solving
868
- self.is_solved = False
869
- self._invalidate_caches()
870
-
871
- # =============================================================================
872
- # EQUATION MANAGEMENT METHODS
873
- # =============================================================================
874
-
875
- def add_equation(self, equation: Equation) -> None:
876
- """
877
- Add an equation to the problem.
878
-
879
- The equation will be validated to ensure all referenced variables exist.
880
- Missing variables that look like simple identifiers will be auto-created
881
- as unknown placeholders.
882
-
883
- Args:
884
- equation: Equation object to add to the problem
885
-
886
- Raises:
887
- EquationValidationError: If the equation is invalid or cannot be processed
888
-
889
- Note:
890
- Adding an equation resets the problem to unsolved state.
891
- """
892
- if equation is None:
893
- raise EquationValidationError("Cannot add None equation to problem")
894
-
895
- # Fix VariableReferences in equation to point to correct Variables
896
- equation = self._fix_variable_references(equation)
897
-
898
- # Validate that all variables in the equation exist
899
- try:
900
- equation_vars = equation.get_all_variables()
901
- except Exception as e:
902
- raise EquationValidationError(f"Failed to extract variables from equation: {e}") from e
903
-
904
- missing_vars = [var for var in equation_vars if var not in self.variables]
905
-
906
- if missing_vars:
907
- self._handle_missing_variables(missing_vars)
908
-
909
- # Check again for remaining missing variables
910
- equation_vars = equation.get_all_variables()
911
- remaining_missing = [var for var in equation_vars if var not in self.variables]
912
- if remaining_missing:
913
- self.logger.warning(f"Equation references missing variables: {remaining_missing}")
914
-
915
- self.equations.append(equation)
916
- self.equation_system.add_equation(equation)
917
- self.is_solved = False
918
-
919
- def _handle_missing_variables(self, missing_vars: list[str]) -> None:
920
- """Handle missing variables by creating placeholders for simple symbols."""
921
- for missing_var in missing_vars:
922
- if self._is_simple_variable_symbol(missing_var):
923
- self._create_placeholder_variable(missing_var)
924
-
925
- def _is_simple_variable_symbol(self, symbol: str) -> bool:
926
- """Check if a symbol looks like a simple variable identifier."""
927
- return (symbol.isidentifier() and
928
- not any(char in symbol for char in ['(', ')', '+', '-', '*', '/', ' ']))
929
-
930
- def _create_placeholder_variable(self, symbol: str) -> None:
931
- """Create a placeholder variable for a missing symbol."""
932
-
933
- placeholder_var = Variable(
934
- name=f"Auto-created: {symbol}",
935
- expected_dimension=DimensionlessUnits.dimensionless.dimension,
936
- is_known=False
937
- )
938
- placeholder_var.symbol = symbol
939
- placeholder_var.quantity = Qty(0.0, DimensionlessUnits.dimensionless)
940
- self.add_variable(placeholder_var)
941
- self.logger.debug(f"Auto-created placeholder variable: {symbol}")
942
-
943
- def add_equations(self, *equations: Equation) -> Problem:
944
- """Add multiple equations to the problem."""
945
- for eq in equations:
946
- self.add_equation(eq)
947
- return self
948
-
949
- # =============================================================================
950
- # PROBLEM SOLVING METHODS
951
- # =============================================================================
952
-
953
- def solve(self, max_iterations: int = MAX_ITERATIONS_DEFAULT, tolerance: float = TOLERANCE_DEFAULT) -> dict[str, Variable]:
954
- """
955
- Solve the engineering problem by finding values for all unknown variables.
956
-
957
- This method orchestrates the complete solving process:
958
- 1. Builds dependency graph from equations
959
- 2. Determines optimal solving order using topological sorting
960
- 3. Solves equations iteratively using symbolic/numerical methods
961
- 4. Verifies solution against all equations
962
- 5. Updates variable states and synchronizes instance attributes
963
-
964
- Args:
965
- max_iterations: Maximum number of solving iterations (default: 100)
966
- tolerance: Numerical tolerance for convergence (default: 1e-10)
967
-
968
- Returns:
969
- dict mapping variable symbols to solved Variable objects
970
-
971
- Raises:
972
- SolverError: If solving fails or times out
973
-
974
- Example:
975
- >>> problem = MyEngineeringProblem()
976
- >>> solution = problem.solve()
977
- >>> print(f"Force = {solution['F'].quantity}")
978
- """
979
- self.logger.info(f"Solving problem: {self.name}")
980
-
981
- try:
982
- # Clear previous solution
983
- self.solution = {}
984
- self.is_solved = False
985
- self.solving_history = []
986
-
987
- # Build dependency graph
988
- self._build_dependency_graph()
989
-
990
- # Use solver manager to solve the system
991
- solve_result = self.solver_manager.solve(
992
- self.equations,
993
- self.variables,
994
- self.dependency_graph,
995
- max_iterations,
996
- tolerance
997
- )
998
-
999
- if solve_result.success:
1000
- # Update variables with the result
1001
- self.variables = solve_result.variables
1002
- self.solving_history.extend(solve_result.steps)
1003
-
1004
- # Sync solved values back to instance attributes
1005
- self._sync_variables_to_instance_attributes()
1006
-
1007
- # Verify solution
1008
- self.solution = self.variables
1009
- verification_passed = self.verify_solution()
1010
-
1011
- # Mark as solved based on solver result and verification
1012
- if verification_passed:
1013
- self.is_solved = True
1014
- self.logger.info("Solution verified successfully")
1015
- return self.solution
1016
- else:
1017
- self.logger.warning("Solution verification failed")
1018
- return self.solution
1019
- else:
1020
- raise SolverError(f"Solving failed: {solve_result.message}")
1021
-
1022
- except SolverError:
1023
- raise
1024
- except Exception as e:
1025
- self.logger.error(f"Solving failed: {e}")
1026
- raise SolverError(f"Unexpected error during solving: {e}") from e
1027
-
1028
- def _build_dependency_graph(self):
1029
- """Build the dependency graph for solving order determination."""
1030
- # Reset the dependency graph
1031
- self.dependency_graph = Order()
1032
-
1033
- # Get known variables
1034
- known_vars = self.get_known_symbols()
1035
-
1036
- # Add dependencies from equations
1037
- for equation in self.equations:
1038
- self.dependency_graph.add_equation(equation, known_vars)
1039
-
1040
- def _sync_variables_to_instance_attributes(self):
1041
- """
1042
- Sync variable objects to instance attributes after solving.
1043
- This ensures that self.P refers to the same Variable object that's in self.variables.
1044
- Variables maintain their original dimensional types (e.g., AreaVariable, PressureVariable).
1045
- """
1046
- for var_symbol, var in self.variables.items():
1047
- # Update instance attribute if it exists
1048
- if hasattr(self, var_symbol):
1049
- # Variables preserve their dimensional types during solving
1050
- setattr(self, var_symbol, var)
1051
-
1052
- # Also update sub-problem namespace objects
1053
- for namespace, sub_problem in self.sub_problems.items():
1054
- if hasattr(self, namespace):
1055
- namespace_obj = getattr(self, namespace)
1056
- for var_symbol in sub_problem.variables:
1057
- namespaced_symbol = f"{namespace}_{var_symbol}"
1058
- if namespaced_symbol in self.variables and hasattr(namespace_obj, var_symbol):
1059
- setattr(namespace_obj, var_symbol, self.variables[namespaced_symbol])
1060
-
1061
- def verify_solution(self, tolerance: float = 1e-10) -> bool:
1062
- """Verify that all equations are satisfied."""
1063
- if not self.equations:
1064
- return True
1065
-
1066
- try:
1067
- for equation in self.equations:
1068
- if not equation.check_residual(self.variables, tolerance):
1069
- self.logger.debug(f"Equation verification failed: {equation}")
1070
- return False
1071
- return True
1072
- except Exception as e:
1073
- self.logger.debug(f"Solution verification error: {e}")
1074
- return False
1075
-
1076
- # =============================================================================
1077
- # SYSTEM ANALYSIS METHODS
1078
- # =============================================================================
1079
-
1080
- def analyze_system(self) -> dict[str, Any]:
1081
- """Analyze the equation system for solvability, cycles, etc."""
1082
- try:
1083
- self._build_dependency_graph()
1084
- known_vars = self.get_known_symbols()
1085
- analysis = self.dependency_graph.analyze_system(known_vars)
1086
-
1087
- # Add some additional info
1088
- analysis['total_equations'] = len(self.equations)
1089
- analysis['is_determined'] = len(self.get_unknown_variables()) <= len(self.equations)
1090
-
1091
- return analysis
1092
- except Exception as e:
1093
- self.logger.debug(f"Dependency analysis failed: {e}")
1094
- # Return basic analysis on failure
1095
- return {
1096
- 'total_variables': len(self.variables),
1097
- 'known_variables': len(self.get_known_variables()),
1098
- 'unknown_variables': len(self.get_unknown_variables()),
1099
- 'total_equations': len(self.equations),
1100
- 'is_determined': len(self.get_unknown_variables()) <= len(self.equations),
1101
- 'has_cycles': False,
1102
- 'solving_order': [],
1103
- 'can_solve_completely': False,
1104
- 'unsolvable_variables': []
1105
- }
1106
-
1107
- def reset_solution(self):
1108
- """Reset the problem to unsolved state."""
1109
- self.is_solved = False
1110
- self.solution = {}
1111
- self.solving_history = []
1112
-
1113
- # Reset unknown variables to unknown state
1114
- for var in self.variables.values():
1115
- if not var.is_known:
1116
- var.is_known = False
1117
-
1118
- def _invalidate_caches(self) -> None:
1119
- """Invalidate performance caches when variables change."""
1120
- self._cache_dirty = True
1121
-
1122
- def _update_variable_caches(self) -> None:
1123
- """Update the variable caches for performance."""
1124
- if not self._cache_dirty:
1125
- return
1126
-
1127
- self._known_variables_cache = {symbol: var for symbol, var in self.variables.items() if var.is_known}
1128
- self._unknown_variables_cache = {symbol: var for symbol, var in self.variables.items() if not var.is_known}
1129
- self._cache_dirty = False
1130
-
1131
- # =============================================================================
1132
- # VALIDATION AND WARNING SYSTEM
1133
- # =============================================================================
1134
-
1135
- def add_validation_check(self, check_function: Callable) -> None:
1136
- """Add a validation check function."""
1137
- self.validation_checks.append(check_function)
1138
-
1139
- def validate(self) -> list[dict[str, Any]]:
1140
- """Run all validation checks and return any warnings."""
1141
- validation_warnings = []
1142
-
1143
- for check in self.validation_checks:
1144
- try:
1145
- result = check(self)
1146
- if result:
1147
- validation_warnings.append(result)
1148
- except Exception as e:
1149
- self.logger.debug(f"Validation check failed: {e}")
1150
-
1151
- return validation_warnings
1152
-
1153
- def get_warnings(self) -> list[dict[str, Any]]:
1154
- """Get all warnings from the problem."""
1155
- warnings = self.warnings.copy()
1156
- warnings.extend(self.validate())
1157
- return warnings
1158
-
1159
- # =============================================================================
1160
- # UTILITY METHODS
1161
- # =============================================================================
1162
-
1163
- def copy(self) -> Problem:
1164
- """Create a copy of this problem."""
1165
- from copy import deepcopy
1166
- return deepcopy(self)
1167
-
1168
- def __str__(self) -> str:
1169
- """String representation of the problem."""
1170
- status = "SOLVED" if self.is_solved else "UNSOLVED"
1171
- return f"EngineeringProblem('{self.name}', vars={len(self.variables)}, eqs={len(self.equations)}, {status})"
1172
-
1173
- def __repr__(self) -> str:
1174
- """Detailed representation of the problem."""
1175
- return self.__str__()
1176
-
1177
- # =============================================================================
1178
- # SPECIAL METHODS FOR ATTRIBUTE ACCESS
1179
- # =============================================================================
1180
-
1181
- def __setattr__(self, name: str, value: Any) -> None:
1182
- """Custom attribute setting to maintain variable synchronization."""
1183
- # During initialization, use normal attribute setting
1184
- if not hasattr(self, 'variables') or name.startswith('_'):
1185
- super().__setattr__(name, value)
1186
- return
1187
-
1188
- # If setting a variable that exists in our variables dict, update both
1189
- if isinstance(value, Variable) and name in self.variables:
1190
- self.variables[name] = value
1191
-
1192
- super().__setattr__(name, value)
1193
-
1194
- def __getitem__(self, key: str) -> Variable:
1195
- """Allow dict-like access to variables."""
1196
- return self.get_variable(key)
1197
-
1198
- def __setitem__(self, key: str, value: Variable) -> None:
1199
- """Allow dict-like assignment of variables."""
1200
- # Update the symbol to match the key if they differ
1201
- if value.symbol != key:
1202
- value.symbol = key
1203
- self.add_variable(value)
1204
-
1205
- def _update_equation_variable_references(self, equation: Equation) -> Equation:
1206
- """Update VariableReference objects in equation to use symbols instead of names."""
1207
- from qnty.expressions import VariableReference
1208
-
1209
- # Update LHS if it's a VariableReference
1210
- updated_lhs = equation.lhs
1211
- if isinstance(equation.lhs, VariableReference):
1212
- # Find the variable by name and update to use symbol
1213
- var_name = equation.lhs.variable.name
1214
- matching_var = None
1215
- for var in self.variables.values():
1216
- if var.name == var_name:
1217
- matching_var = var
1218
- break
1219
- if matching_var and matching_var.symbol:
1220
- updated_lhs = VariableReference(matching_var)
1221
-
1222
- # Update RHS by recursively updating expressions
1223
- updated_rhs = self._update_expression_variable_references(equation.rhs)
1224
-
1225
- # Create new equation with updated references
1226
- return Equation(equation.name, updated_lhs, updated_rhs)
1227
-
1228
- def _update_expression_variable_references(self, expr):
1229
- """Recursively update VariableReference objects in expression tree."""
1230
- from qnty.expressions import VariableReference, BinaryOperation, Constant
1231
-
1232
- if isinstance(expr, VariableReference):
1233
- # Find the variable by name and update to use symbol
1234
- var_name = expr.variable.name
1235
- matching_var = None
1236
- for var in self.variables.values():
1237
- if var.name == var_name:
1238
- matching_var = var
1239
- break
1240
- if matching_var and matching_var.symbol:
1241
- return VariableReference(matching_var)
1242
- return expr
1243
- elif isinstance(expr, BinaryOperation):
1244
- updated_left = self._update_expression_variable_references(expr.left)
1245
- updated_right = self._update_expression_variable_references(expr.right)
1246
- return BinaryOperation(expr.operator, updated_left, updated_right)
1247
- elif isinstance(expr, Constant):
1248
- return expr
1249
- else:
1250
- # Return unknown expression types as-is
1251
- return expr