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
@@ -7,24 +7,18 @@ Mathematical equations for qnty variables with solving capabilities.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import logging
10
11
  from typing import cast
11
12
 
13
+ from ..constants import SOLVER_DEFAULT_TOLERANCE
12
14
  from ..expressions import Expression, VariableReference
13
- from ..quantities.quantity import TypeSafeVariable
14
-
15
- # Global optimization flags and cache
16
- _SCOPE_DISCOVERY_ENABLED = False # Disabled by default due to high overhead
17
- _VARIABLE_TYPE_CACHE = {} # Cache for hasattr checks
15
+ from ..quantities import FieldQnty
16
+ from ..utils.scope_discovery import ScopeDiscoveryService
18
17
 
18
+ _logger = logging.getLogger(__name__)
19
19
 
20
- def _is_typesafe_variable(obj) -> bool:
21
- """Optimized type check with caching for hasattr calls."""
22
- obj_type = type(obj)
23
- if obj_type not in _VARIABLE_TYPE_CACHE:
24
- _VARIABLE_TYPE_CACHE[obj_type] = (
25
- hasattr(obj, 'symbol') and hasattr(obj, 'name') and hasattr(obj, 'quantity')
26
- )
27
- return _VARIABLE_TYPE_CACHE[obj_type]
20
+ # Global optimization flags
21
+ _SCOPE_DISCOVERY_ENABLED = False # Disabled by default due to high overhead
28
22
 
29
23
 
30
24
  class Equation:
@@ -32,21 +26,22 @@ class Equation:
32
26
  Represents a mathematical equation with left-hand side equal to right-hand side.
33
27
  Optimized with __slots__ for memory efficiency.
34
28
  """
35
- __slots__ = ('name', 'lhs', 'rhs', '_variables')
36
-
37
- def __init__(self, name: str, lhs: TypeSafeVariable | Expression, rhs: Expression):
29
+
30
+ __slots__ = ("name", "lhs", "rhs", "_variables")
31
+
32
+ def __init__(self, name: str, lhs: FieldQnty | Expression, rhs: Expression):
38
33
  self.name = name
39
-
34
+
40
35
  # Convert Variable to VariableReference if needed - use isinstance for performance
41
- if isinstance(lhs, TypeSafeVariable):
36
+ if isinstance(lhs, FieldQnty):
42
37
  self.lhs = VariableReference(lhs)
43
38
  else:
44
39
  # It's already an Expression
45
40
  self.lhs = cast(Expression, lhs)
46
-
41
+
47
42
  self.rhs = rhs
48
43
  self._variables: set[str] | None = None # Lazy initialization for better performance
49
-
44
+
50
45
  def get_all_variables(self) -> set[str]:
51
46
  """Get all variable names used in this equation."""
52
47
  if self._variables is None:
@@ -55,71 +50,67 @@ class Equation:
55
50
  rhs_vars = self.rhs.get_variables()
56
51
  self._variables = lhs_vars | rhs_vars
57
52
  return self._variables
58
-
53
+
59
54
  @property
60
55
  def variables(self) -> set[str]:
61
56
  """Get all variable names used in this equation (cached property)."""
62
57
  return self.get_all_variables()
63
-
58
+
64
59
  def get_unknown_variables(self, known_vars: set[str]) -> set[str]:
65
60
  """Get variables that are unknown (not in known_vars set)."""
66
61
  return self.variables - known_vars
67
-
62
+
68
63
  def get_known_variables(self, known_vars: set[str]) -> set[str]:
69
64
  """Get variables that are known (in known_vars set)."""
70
65
  return self.variables & known_vars
71
-
66
+
72
67
  def can_solve_for(self, target_var: str, known_vars: set[str]) -> bool:
73
68
  """Check if this equation can solve for target_var given known_vars."""
74
69
  if target_var not in self.variables:
75
70
  return False
71
+
76
72
  # Direct assignment case: lhs is the variable
77
73
  if isinstance(self.lhs, VariableReference) and self.lhs.name == target_var:
78
74
  rhs_vars = self.rhs.get_variables()
79
75
  return rhs_vars.issubset(known_vars)
76
+
77
+ # General case: can solve if target_var is the only unknown
80
78
  unknown_vars = self.get_unknown_variables(known_vars)
81
- # Can solve if target_var is the only unknown
82
79
  return unknown_vars == {target_var}
83
-
84
- def solve_for(self, target_var: str, variable_values: dict[str, TypeSafeVariable]) -> TypeSafeVariable:
80
+
81
+ def solve_for(self, target_var: str, variable_values: dict[str, FieldQnty]) -> FieldQnty:
85
82
  """
86
83
  Solve the equation for target_var.
87
84
  Returns the target variable with updated quantity.
88
85
  """
89
86
  if target_var not in self.variables:
90
87
  raise ValueError(f"Variable '{target_var}' not found in equation")
91
-
92
- # Handle direct assignment: target = expression
93
- if isinstance(self.lhs, VariableReference) and self.lhs.name == target_var:
94
- # Direct assignment: target_var = rhs
95
- result_qty = self.rhs.evaluate(variable_values)
96
-
97
- # Update existing variable object to preserve references
98
- var_obj = variable_values.get(target_var)
99
- if var_obj is not None:
100
- # Convert result to the target variable's original unit if it had one
101
- if var_obj.quantity is not None and var_obj.quantity.unit is not None:
102
- # Convert to the target variable's defined unit - be more specific about exceptions
103
- try:
104
- result_qty = result_qty.to(var_obj.quantity.unit)
105
- except (ValueError, TypeError, AttributeError):
106
- # Log specific conversion issues but continue with calculated unit
107
- # This preserves the original behavior while being more explicit
108
- pass
109
-
110
- var_obj.quantity = result_qty
111
- var_obj.is_known = True
112
- return var_obj
113
-
114
- # Create new variable if not found - this shouldn't happen in normal usage
88
+
89
+ # Only handle direct assignment: target = expression
90
+ if not (isinstance(self.lhs, VariableReference) and self.lhs.name == target_var):
91
+ raise NotImplementedError(f"Cannot solve for {target_var} in equation {self}. Only direct assignment equations (var = expression) are supported.")
92
+
93
+ # Direct assignment: target_var = rhs
94
+ result_qty = self.rhs.evaluate(variable_values)
95
+
96
+ # Get the variable object to update
97
+ var_obj = variable_values.get(target_var)
98
+ if var_obj is None:
115
99
  raise ValueError(f"Variable '{target_var}' not found in variable_values")
116
-
117
- # For more complex equations, we would need algebraic manipulation
118
- # Currently focusing on direct assignment which covers most engineering cases
119
- raise NotImplementedError(f"Cannot solve for {target_var} in equation {self}. "
120
- f"Only direct assignment equations (var = expression) are supported.")
121
-
122
- def check_residual(self, variable_values: dict[str, TypeSafeVariable], tolerance: float = 1e-10) -> bool:
100
+
101
+ # Convert result to the target variable's original unit if it had one
102
+ if var_obj.quantity is not None and var_obj.quantity.unit is not None:
103
+ try:
104
+ result_qty = result_qty.to(var_obj.quantity.unit)
105
+ except (ValueError, TypeError, AttributeError) as e:
106
+ _logger.debug(f"Unit conversion failed for {target_var}: {e}. Using calculated unit.")
107
+
108
+ # Update the variable and return it
109
+ var_obj.quantity = result_qty
110
+ var_obj.is_known = True
111
+ return var_obj
112
+
113
+ def check_residual(self, variable_values: dict[str, FieldQnty], tolerance: float = SOLVER_DEFAULT_TOLERANCE) -> bool:
123
114
  """
124
115
  Check if equation is satisfied by evaluating residual (LHS - RHS).
125
116
  Returns True if |residual| < tolerance, accounting for units.
@@ -128,112 +119,86 @@ class Equation:
128
119
  # Both lhs and rhs should be Expressions after __init__ conversion
129
120
  lhs_value = self.lhs.evaluate(variable_values)
130
121
  rhs_value = self.rhs.evaluate(variable_values)
131
-
132
- # Check dimensional compatibility
133
- if lhs_value._dimension_sig != rhs_value._dimension_sig:
122
+
123
+ # Check dimensional compatibility using public API
124
+ if not self._are_dimensionally_compatible(lhs_value, rhs_value):
134
125
  return False
135
-
126
+
136
127
  # Convert to same units for comparison
137
128
  rhs_converted = rhs_value.to(lhs_value.unit)
138
129
  residual = abs(lhs_value.value - rhs_converted.value)
139
-
130
+
140
131
  return residual < tolerance
141
- except (ValueError, TypeError, AttributeError, KeyError):
142
- # Handle specific expected errors during evaluation/conversion
132
+ except (ValueError, TypeError, AttributeError, KeyError) as e:
133
+ _logger.debug(f"Expected error in residual check for equation '{self.name}': {e}")
143
134
  return False
144
135
  except Exception as e:
145
- # Re-raise unexpected errors to avoid masking bugs
146
- raise RuntimeError(f"Unexpected error in residual check for equation '{self.name}': {e}") from e
147
-
148
- def _discover_variables_from_scope(self) -> dict[str, TypeSafeVariable]:
136
+ error_msg = f"Unexpected error in residual check for equation '{self.name}': {e}"
137
+ _logger.error(error_msg)
138
+ raise RuntimeError(error_msg) from e
139
+
140
+ def _discover_variables_from_scope(self) -> dict[str, FieldQnty]:
149
141
  """
150
- Automatically discover variables from the calling scope.
151
- Now optimized with caching and conditional execution.
142
+ Automatically discover variables from the calling scope using centralized service.
152
143
  """
153
144
  if not _SCOPE_DISCOVERY_ENABLED:
154
145
  return {}
155
-
156
- import inspect
157
-
158
- # Get the frame that called this method (skip through __str__ calls)
159
- frame = inspect.currentframe()
160
- try:
161
- # Skip frames until we find one outside the equation system
162
- depth = 0
163
- max_depth = 8 # Reduced from 10 for performance
164
- while frame and depth < max_depth and (
165
- frame.f_code.co_filename.endswith(('equation.py', 'expression.py')) or
166
- frame.f_code.co_name in ['__str__', '__repr__']
167
- ):
168
- frame = frame.f_back
169
- depth += 1
170
-
171
- if not frame:
172
- return {}
173
-
174
- # Only get local variables first (faster than combining both)
175
- local_vars = frame.f_locals
176
- required_vars = self.variables
177
- discovered = {}
178
-
179
- # First pass: check locals only (most common case)
180
- for var_name in required_vars:
181
- for obj in local_vars.values():
182
- if _is_typesafe_variable(obj) and (
183
- (hasattr(obj, 'symbol') and obj.symbol == var_name) or
184
- (hasattr(obj, 'name') and obj.name == var_name)
185
- ):
186
- discovered[var_name] = obj
187
- break
188
-
189
- # Second pass: check globals only if needed
190
- if len(discovered) < len(required_vars):
191
- global_vars = frame.f_globals
192
- remaining_vars = required_vars - discovered.keys()
193
- for var_name in remaining_vars:
194
- for obj in global_vars.values():
195
- if _is_typesafe_variable(obj) and (
196
- (hasattr(obj, 'symbol') and obj.symbol == var_name) or
197
- (hasattr(obj, 'name') and obj.name == var_name)
198
- ):
199
- discovered[var_name] = obj
200
- break
201
-
202
- return discovered
203
-
204
- finally:
205
- del frame
206
-
207
- def _can_auto_solve(self) -> tuple[bool, str, dict[str, TypeSafeVariable]]:
208
- """Check if equation can be auto-solved from scope."""
146
+
147
+ # Use centralized scope discovery service
148
+ return ScopeDiscoveryService.discover_variables(self.variables, enable_caching=True)
149
+
150
+ def _are_dimensionally_compatible(self, lhs_value, rhs_value) -> bool:
151
+ """Check if two quantities are dimensionally compatible."""
209
152
  try:
210
- discovered = self._discover_variables_from_scope()
211
-
212
- # Check if this is a simple assignment equation (one unknown)
213
- unknowns = []
214
- knowns = []
215
-
216
- for var_name in self.variables:
217
- if var_name in discovered:
218
- var = discovered[var_name]
219
- if hasattr(var, 'is_known') and not var.is_known:
220
- unknowns.append(var_name)
221
- elif hasattr(var, 'quantity') and var.quantity is not None:
222
- knowns.append(var_name)
223
- else:
224
- unknowns.append(var_name) # Assume unknown if no quantity
153
+ # Try to convert - if successful, they're compatible
154
+ rhs_value.to(lhs_value.unit)
155
+ return True
156
+ except (ValueError, TypeError, AttributeError):
157
+ return False
158
+
159
+ def _analyze_variable_states(self, discovered: dict[str, FieldQnty]) -> dict[str, str]:
160
+ """Analyze which variables are known vs unknown."""
161
+ states = {}
162
+ for var_name in self.variables:
163
+ if var_name not in discovered:
164
+ states[var_name] = "missing"
165
+ else:
166
+ var = discovered[var_name]
167
+ if hasattr(var, "is_known") and not var.is_known:
168
+ states[var_name] = "unknown"
169
+ elif hasattr(var, "quantity") and var.quantity is not None:
170
+ states[var_name] = "known"
225
171
  else:
226
- return False, "", {} # Missing variable
227
-
228
- # Can only auto-solve if there's exactly one unknown
229
- if len(unknowns) == 1:
230
- return True, unknowns[0], discovered
231
-
172
+ states[var_name] = "unknown"
173
+ return states
174
+
175
+ def _determine_solvability(self, states: dict[str, str], discovered: dict[str, FieldQnty]) -> tuple[bool, str, dict[str, FieldQnty]]:
176
+ """Determine if equation can be solved based on variable states."""
177
+ if "missing" in states.values():
232
178
  return False, "", {}
233
-
234
- except Exception:
179
+
180
+ unknowns = [name for name, state in states.items() if state == "unknown"]
181
+
182
+ # Can only auto-solve if there's exactly one unknown
183
+ if len(unknowns) == 1:
184
+ return True, unknowns[0], discovered
185
+
186
+ return False, "", {}
187
+
188
+ def _can_auto_solve(self) -> tuple[bool, str, dict[str, FieldQnty]]:
189
+ """Check if equation can be auto-solved from scope using centralized service."""
190
+ try:
191
+ discovered = self._discover_variables_from_scope()
192
+ if not discovered:
193
+ return False, "", {}
194
+
195
+ variable_states = self._analyze_variable_states(discovered)
196
+ return self._determine_solvability(variable_states, discovered)
197
+
198
+ except (AttributeError, KeyError, ValueError, TypeError) as e:
199
+ _logger.debug(f"Cannot auto-solve equation '{self.name}': {e}")
235
200
  return False, "", {}
236
-
201
+
237
202
  def _try_auto_solve(self) -> bool:
238
203
  """Try to automatically solve the equation if possible."""
239
204
  try:
@@ -242,16 +207,14 @@ class Equation:
242
207
  self.solve_for(target_var, variables)
243
208
  return True
244
209
  return False
245
- except Exception:
210
+ except (AttributeError, KeyError, ValueError, TypeError) as e:
211
+ _logger.debug(f"Auto-solve failed for equation '{self.name}': {e}")
246
212
  return False
247
-
213
+
248
214
  def __str__(self) -> str:
249
215
  # Try to auto-solve if possible before displaying
250
216
  self._try_auto_solve()
251
217
  return f"{self.lhs} = {self.rhs}"
252
-
218
+
253
219
  def __repr__(self) -> str:
254
220
  return f"Equation(name='{self.name}', lhs={self.lhs!r}, rhs={self.rhs!r})"
255
-
256
-
257
-
qnty/equations/system.py CHANGED
@@ -1,4 +1,4 @@
1
- from ..quantities.quantity import TypeSafeVariable
1
+ from ..quantities import FieldQnty
2
2
  from .equation import Equation
3
3
 
4
4
 
@@ -7,121 +7,124 @@ class EquationSystem:
7
7
  System of equations that can be solved together.
8
8
  Optimized with __slots__ for memory efficiency.
9
9
  """
10
- __slots__ = ('equations', 'variables', '_known_cache', '_unknown_cache')
11
-
10
+
11
+ __slots__ = ("equations", "variables", "_known_cache", "_unknown_cache")
12
+
12
13
  def __init__(self, equations: list[Equation] | None = None):
13
14
  self.equations = equations or []
14
- self.variables = {} # Dict[str, TypeSafeVariable]
15
+ self.variables: dict[str, FieldQnty] = {} # Dict[str, FieldQnty]
15
16
  self._known_cache: set[str] | None = None # Cache for known variables
16
17
  self._unknown_cache: set[str] | None = None # Cache for unknown variables
17
-
18
+
18
19
  def add_equation(self, equation: Equation):
19
20
  """Add an equation to the system."""
20
21
  self.equations.append(equation)
21
22
  self._invalidate_caches()
22
-
23
- def add_variable(self, variable: TypeSafeVariable):
23
+
24
+ def add_variable(self, variable: FieldQnty):
24
25
  """Add a variable to the system."""
25
26
  self.variables[variable.name] = variable
26
27
  self._invalidate_caches()
27
-
28
+
28
29
  def _invalidate_caches(self):
29
30
  """Invalidate cached known/unknown variable sets."""
30
31
  self._known_cache = None
31
32
  self._unknown_cache = None
32
-
33
+
34
+ def _is_variable_known(self, var: FieldQnty) -> bool:
35
+ """Determine if a variable should be considered known."""
36
+ return var.is_known and var.quantity is not None
37
+
33
38
  def get_known_variables(self) -> set[str]:
34
39
  """Get names of all known variables (cached)."""
35
40
  if self._known_cache is None:
36
- self._known_cache = {name for name, var in self.variables.items() if var.is_known and var.quantity is not None}
41
+ self._known_cache = {name for name, var in self.variables.items() if self._is_variable_known(var)}
37
42
  return self._known_cache
38
-
43
+
39
44
  def get_unknown_variables(self) -> set[str]:
40
45
  """Get names of all unknown variables (cached)."""
41
46
  if self._unknown_cache is None:
42
- self._unknown_cache = {name for name, var in self.variables.items() if not var.is_known or var.quantity is None}
47
+ self._unknown_cache = {name for name, var in self.variables.items() if not self._is_variable_known(var)}
43
48
  return self._unknown_cache
44
-
49
+
50
+ def _find_solvable_equation_variable_pair(self, known_vars: set[str], unknown_vars: set[str]) -> tuple[Equation, str] | None:
51
+ """Find the first equation-variable pair that can be solved."""
52
+ for equation in self.equations:
53
+ for unknown_var in unknown_vars:
54
+ if equation.can_solve_for(unknown_var, known_vars):
55
+ return equation, unknown_var
56
+ return None
57
+
45
58
  def can_solve_any(self) -> bool:
46
59
  """Check if any equation can be solved with current known variables."""
47
60
  known_vars = self.get_known_variables()
48
61
  unknown_vars = self.get_unknown_variables()
49
-
50
- for equation in self.equations:
51
- for unknown_var in unknown_vars:
52
- if equation.can_solve_for(unknown_var, known_vars):
53
- return True
54
- return False
55
-
62
+ return self._find_solvable_equation_variable_pair(known_vars, unknown_vars) is not None
63
+
56
64
  def solve_step(self) -> bool:
57
65
  """Solve one step - find and solve one equation. Returns True if progress made."""
58
66
  known_vars = self.get_known_variables()
59
67
  unknown_vars = self.get_unknown_variables()
60
-
61
- # Find an equation that can be solved
62
- for equation in self.equations:
63
- for unknown_var in unknown_vars:
64
- if equation.can_solve_for(unknown_var, known_vars):
65
- # Solve for this variable
66
- equation.solve_for(unknown_var, self.variables)
67
- # Invalidate caches since variable states have changed
68
- self._invalidate_caches()
69
- return True # Progress made
70
-
71
- return False # No progress possible
72
-
68
+
69
+ pair = self._find_solvable_equation_variable_pair(known_vars, unknown_vars)
70
+ if pair is None:
71
+ return False
72
+
73
+ equation, unknown_var = pair
74
+ equation.solve_for(unknown_var, self.variables)
75
+ self._invalidate_caches()
76
+ return True
77
+
73
78
  def solve(self, max_iterations: int = 100) -> bool:
74
79
  """Solve the system iteratively. Returns True if fully solved."""
80
+ if max_iterations <= 0:
81
+ raise ValueError("max_iterations must be positive")
82
+
75
83
  for _ in range(max_iterations):
76
84
  if not self.can_solve_any():
77
85
  break
78
86
  if not self.solve_step():
79
87
  break
80
-
88
+
81
89
  # Check if all variables are known
82
90
  unknown_vars = self.get_unknown_variables()
83
91
  return len(unknown_vars) == 0
84
-
92
+
93
+ def _get_next_solvable_variable(self, simulated_known: set[str]) -> str | None:
94
+ """Find the next variable that can be solved given current known variables."""
95
+ unknown_vars = {name for name in self.variables.keys() if name not in simulated_known}
96
+ if not unknown_vars:
97
+ return None
98
+
99
+ for equation in self.equations:
100
+ for unknown_var in unknown_vars:
101
+ if equation.can_solve_for(unknown_var, simulated_known):
102
+ return unknown_var
103
+ return None
104
+
85
105
  def get_solving_order(self) -> list[str]:
86
106
  """
87
107
  Get the order in which variables can be solved.
88
108
  Optimized to avoid creating full system copies.
89
109
  """
90
110
  order = []
91
- # Track known variables without modifying the original
92
111
  simulated_known = self.get_known_variables().copy()
93
-
94
- # Continue until no more variables can be solved
112
+
95
113
  max_iterations = len(self.variables) # Prevent infinite loops
96
- iterations = 0
97
-
98
- while iterations < max_iterations:
99
- unknown_vars = {name for name in self.variables.keys() if name not in simulated_known}
100
- if not unknown_vars:
114
+ for _ in range(max_iterations):
115
+ next_var = self._get_next_solvable_variable(simulated_known)
116
+ if next_var is None:
101
117
  break
102
-
103
- found_solvable = False
104
- # Find next solvable variable
105
- for equation in self.equations:
106
- for unknown_var in unknown_vars:
107
- if equation.can_solve_for(unknown_var, simulated_known):
108
- order.append(unknown_var)
109
- # Simulate marking as known for next iteration
110
- simulated_known.add(unknown_var)
111
- found_solvable = True
112
- break
113
- if found_solvable:
114
- break
115
-
116
- if not found_solvable:
117
- break # No more progress possible
118
-
119
- iterations += 1
120
-
118
+
119
+ order.append(next_var)
120
+ simulated_known.add(next_var)
121
+
121
122
  return order
122
-
123
+
123
124
  def __str__(self) -> str:
124
- return f"EquationSystem({len(self.equations)} equations, {len(self.variables)} variables)"
125
-
125
+ known_count = len(self.get_known_variables())
126
+ total_vars = len(self.variables)
127
+ return f"EquationSystem({len(self.equations)} equations, {known_count}/{total_vars} variables known)"
128
+
126
129
  def __repr__(self) -> str:
127
130
  return f"EquationSystem(equations={self.equations!r})"
@@ -6,56 +6,35 @@ Mathematical expressions for building equation trees with qnty variables.
6
6
  """
7
7
 
8
8
  # Core AST classes
9
- from .nodes import (
10
- Expression,
11
- VariableReference,
12
- Constant,
13
- BinaryOperation,
14
- UnaryFunction,
15
- ConditionalExpression
16
- )
17
-
18
9
  # Helper functions
19
- from .functions import (
20
- sin,
21
- cos,
22
- tan,
23
- sqrt,
24
- abs_expr,
25
- ln,
26
- log10,
27
- exp,
28
- cond_expr,
29
- min_expr,
30
- max_expr
31
- )
32
-
33
- # Cache utilities
34
- from .cache import wrap_operand
10
+ # Scope discovery service
11
+ from ..utils.scope_discovery import ScopeDiscoveryService
12
+ from .functions import abs_expr, cond_expr, cos, exp, ln, log10, max_expr, min_expr, sin, sqrt, tan
13
+ from .nodes import BinaryOperation, ConditionalExpression, Constant, Expression, UnaryFunction, VariableReference, wrap_operand
35
14
 
36
15
  # Define public API
37
16
  __all__ = [
38
17
  # Core AST classes
39
- 'Expression',
40
- 'VariableReference',
41
- 'Constant',
42
- 'BinaryOperation',
43
- 'UnaryFunction',
44
- 'ConditionalExpression',
45
-
18
+ "Expression",
19
+ "VariableReference",
20
+ "Constant",
21
+ "BinaryOperation",
22
+ "UnaryFunction",
23
+ "ConditionalExpression",
46
24
  # Helper functions
47
- 'sin',
48
- 'cos',
49
- 'tan',
50
- 'sqrt',
51
- 'abs_expr',
52
- 'ln',
53
- 'log10',
54
- 'exp',
55
- 'cond_expr',
56
- 'min_expr',
57
- 'max_expr',
58
-
25
+ "sin",
26
+ "cos",
27
+ "tan",
28
+ "sqrt",
29
+ "abs_expr",
30
+ "ln",
31
+ "log10",
32
+ "exp",
33
+ "cond_expr",
34
+ "min_expr",
35
+ "max_expr",
59
36
  # Utilities
60
- 'wrap_operand'
61
- ]
37
+ "wrap_operand",
38
+ # Scope discovery
39
+ "ScopeDiscoveryService",
40
+ ]