qnty 0.0.9__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qnty/__init__.py +2 -3
- qnty/constants/__init__.py +10 -0
- qnty/constants/numerical.py +18 -0
- qnty/constants/solvers.py +6 -0
- qnty/constants/tests.py +6 -0
- qnty/dimensions/__init__.py +23 -0
- qnty/dimensions/base.py +97 -0
- qnty/dimensions/field_dims.py +126 -0
- qnty/dimensions/field_dims.pyi +128 -0
- qnty/dimensions/signature.py +111 -0
- qnty/equations/__init__.py +1 -1
- qnty/equations/equation.py +118 -155
- qnty/equations/system.py +68 -65
- qnty/expressions/__init__.py +25 -46
- qnty/expressions/formatter.py +188 -0
- qnty/expressions/functions.py +46 -68
- qnty/expressions/nodes.py +539 -384
- qnty/expressions/types.py +70 -0
- qnty/problems/__init__.py +145 -0
- qnty/problems/composition.py +1031 -0
- qnty/problems/problem.py +695 -0
- qnty/problems/rules.py +145 -0
- qnty/problems/solving.py +1216 -0
- qnty/problems/validation.py +127 -0
- qnty/quantities/__init__.py +28 -5
- qnty/quantities/base_qnty.py +677 -0
- qnty/quantities/field_converters.py +24004 -0
- qnty/quantities/field_qnty.py +1012 -0
- qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
- qnty/{generated/quantities.py → quantities/field_vars.py} +754 -432
- qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
- qnty/solving/manager.py +50 -44
- qnty/solving/order.py +181 -133
- qnty/solving/solvers/__init__.py +2 -9
- qnty/solving/solvers/base.py +27 -37
- qnty/solving/solvers/iterative.py +115 -135
- qnty/solving/solvers/simultaneous.py +93 -165
- qnty/units/__init__.py +1 -0
- qnty/{generated/units.py → units/field_units.py} +1700 -991
- qnty/units/field_units.pyi +2461 -0
- qnty/units/prefixes.py +58 -105
- qnty/units/registry.py +76 -89
- qnty/utils/__init__.py +16 -0
- qnty/utils/caching/__init__.py +23 -0
- qnty/utils/caching/manager.py +401 -0
- qnty/utils/error_handling/__init__.py +66 -0
- qnty/utils/error_handling/context.py +39 -0
- qnty/utils/error_handling/exceptions.py +96 -0
- qnty/utils/error_handling/handlers.py +171 -0
- qnty/utils/logging.py +4 -4
- qnty/utils/protocols.py +164 -0
- qnty/utils/scope_discovery.py +420 -0
- {qnty-0.0.9.dist-info → qnty-0.1.0.dist-info}/METADATA +1 -1
- qnty-0.1.0.dist-info/RECORD +60 -0
- qnty/_backup/problem_original.py +0 -1251
- qnty/_backup/quantity.py +0 -63
- qnty/codegen/cli.py +0 -125
- qnty/codegen/generators/data/unit_data.json +0 -8807
- qnty/codegen/generators/data_processor.py +0 -345
- qnty/codegen/generators/dimensions_gen.py +0 -434
- qnty/codegen/generators/doc_generator.py +0 -141
- qnty/codegen/generators/out/dimension_mapping.json +0 -974
- qnty/codegen/generators/out/dimension_metadata.json +0 -123
- qnty/codegen/generators/out/units_metadata.json +0 -223
- qnty/codegen/generators/quantities_gen.py +0 -159
- qnty/codegen/generators/setters_gen.py +0 -178
- qnty/codegen/generators/stubs_gen.py +0 -167
- qnty/codegen/generators/units_gen.py +0 -295
- qnty/expressions/cache.py +0 -94
- qnty/generated/dimensions.py +0 -514
- qnty/problem/__init__.py +0 -91
- qnty/problem/base.py +0 -142
- qnty/problem/composition.py +0 -385
- qnty/problem/composition_mixin.py +0 -382
- qnty/problem/equations.py +0 -413
- qnty/problem/metaclass.py +0 -302
- qnty/problem/reconstruction.py +0 -1016
- qnty/problem/solving.py +0 -180
- qnty/problem/validation.py +0 -64
- qnty/problem/variables.py +0 -239
- qnty/quantities/expression_quantity.py +0 -314
- qnty/quantities/quantity.py +0 -428
- qnty/quantities/typed_quantity.py +0 -215
- qnty/validation/__init__.py +0 -0
- qnty/validation/registry.py +0 -0
- qnty/validation/rules.py +0 -167
- qnty-0.0.9.dist-info/RECORD +0 -63
- /qnty/{codegen → extensions}/__init__.py +0 -0
- /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
- /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
- /qnty/{generated → extensions/reporting}/__init__.py +0 -0
- {qnty-0.0.9.dist-info → qnty-0.1.0.dist-info}/WHEEL +0 -0
qnty/equations/equation.py
CHANGED
@@ -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
|
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
|
-
|
21
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
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,
|
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,
|
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
|
-
#
|
93
|
-
if isinstance(self.lhs, VariableReference) and self.lhs.name == target_var:
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
#
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
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
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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,
|
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:
|
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
|
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
|
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
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
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})"
|
qnty/expressions/__init__.py
CHANGED
@@ -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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
18
|
+
"Expression",
|
19
|
+
"VariableReference",
|
20
|
+
"Constant",
|
21
|
+
"BinaryOperation",
|
22
|
+
"UnaryFunction",
|
23
|
+
"ConditionalExpression",
|
46
24
|
# Helper functions
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
37
|
+
"wrap_operand",
|
38
|
+
# Scope discovery
|
39
|
+
"ScopeDiscoveryService",
|
40
|
+
]
|