qnty 0.0.8__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 +140 -59
- 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 +4 -0
- qnty/equations/equation.py +220 -0
- qnty/equations/system.py +130 -0
- qnty/expressions/__init__.py +40 -0
- qnty/expressions/formatter.py +188 -0
- qnty/expressions/functions.py +74 -0
- qnty/expressions/nodes.py +701 -0
- qnty/expressions/types.py +70 -0
- qnty/extensions/plotting/__init__.py +0 -0
- qnty/extensions/reporting/__init__.py +0 -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 +29 -0
- qnty/quantities/base_qnty.py +677 -0
- qnty/quantities/field_converters.py +24004 -0
- qnty/quantities/field_qnty.py +1012 -0
- qnty/quantities/field_setter.py +12320 -0
- qnty/quantities/field_vars.py +6325 -0
- qnty/quantities/field_vars.pyi +4191 -0
- qnty/solving/__init__.py +0 -0
- qnty/solving/manager.py +96 -0
- qnty/solving/order.py +403 -0
- qnty/solving/solvers/__init__.py +13 -0
- qnty/solving/solvers/base.py +82 -0
- qnty/solving/solvers/iterative.py +165 -0
- qnty/solving/solvers/simultaneous.py +475 -0
- qnty/units/__init__.py +1 -0
- qnty/units/field_units.py +10507 -0
- qnty/units/field_units.pyi +2461 -0
- qnty/units/prefixes.py +203 -0
- qnty/{unit.py → units/registry.py} +89 -61
- 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 +40 -0
- qnty/utils/protocols.py +164 -0
- qnty/utils/scope_discovery.py +420 -0
- qnty-0.1.0.dist-info/METADATA +199 -0
- qnty-0.1.0.dist-info/RECORD +60 -0
- qnty/dimension.py +0 -186
- qnty/equation.py +0 -297
- qnty/expression.py +0 -553
- qnty/prefixes.py +0 -229
- qnty/unit_types/base.py +0 -47
- qnty/units.py +0 -8113
- qnty/variable.py +0 -300
- qnty/variable_types/base.py +0 -58
- qnty/variable_types/expression_variable.py +0 -106
- qnty/variable_types/typed_variable.py +0 -87
- qnty/variables.py +0 -2298
- qnty/variables.pyi +0 -6148
- qnty-0.0.8.dist-info/METADATA +0 -355
- qnty-0.0.8.dist-info/RECORD +0 -19
- /qnty/{unit_types → extensions}/__init__.py +0 -0
- /qnty/{variable_types → extensions/integration}/__init__.py +0 -0
- {qnty-0.0.8.dist-info → qnty-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
"""
|
2
|
+
Dimension Signatures
|
3
|
+
====================
|
4
|
+
|
5
|
+
Immutable dimension signatures for ultra-fast dimensional analysis using prime number encoding.
|
6
|
+
|
7
|
+
This file contains the core DimensionSignature class that provides zero-cost dimensional
|
8
|
+
compatibility checking through compile-time type system integration.
|
9
|
+
"""
|
10
|
+
|
11
|
+
from dataclasses import dataclass
|
12
|
+
from typing import ClassVar, final
|
13
|
+
|
14
|
+
from .base import BaseDimension
|
15
|
+
|
16
|
+
|
17
|
+
@final
|
18
|
+
@dataclass(frozen=True, slots=True)
|
19
|
+
class DimensionSignature:
|
20
|
+
"""Immutable dimension signature for zero-cost dimensional analysis."""
|
21
|
+
|
22
|
+
# Store as bit pattern for ultra-fast comparison
|
23
|
+
_signature: int | float = 1
|
24
|
+
|
25
|
+
# Instance cache for interning common dimensions
|
26
|
+
_INSTANCE_CACHE: ClassVar[dict[int | float, "DimensionSignature"]] = {}
|
27
|
+
|
28
|
+
# Maximum cache size to prevent memory issues
|
29
|
+
_MAX_CACHE_SIZE: ClassVar[int] = 100
|
30
|
+
|
31
|
+
def __new__(cls, signature: int | float = 1):
|
32
|
+
"""Optimized constructor with instance interning and validation."""
|
33
|
+
# Input validation
|
34
|
+
if not isinstance(signature, int | float):
|
35
|
+
raise TypeError(f"Signature must be int or float, got {type(signature)}")
|
36
|
+
if signature <= 0:
|
37
|
+
raise ValueError(f"Signature must be positive, got {signature}")
|
38
|
+
|
39
|
+
if signature in cls._INSTANCE_CACHE:
|
40
|
+
return cls._INSTANCE_CACHE[signature]
|
41
|
+
|
42
|
+
instance = object.__new__(cls)
|
43
|
+
|
44
|
+
# Cache common signatures with size limit
|
45
|
+
if len(cls._INSTANCE_CACHE) < cls._MAX_CACHE_SIZE:
|
46
|
+
cls._INSTANCE_CACHE[signature] = instance
|
47
|
+
|
48
|
+
return instance
|
49
|
+
|
50
|
+
@classmethod
|
51
|
+
def create(cls, length: int = 0, mass: int = 0, time: int = 0, current: int = 0, temp: int = 0, amount: int = 0, luminosity: int = 0):
|
52
|
+
"""Create dimension from exponents with efficient computation."""
|
53
|
+
# Fast path for dimensionless
|
54
|
+
if not any([length, mass, time, current, temp, amount, luminosity]):
|
55
|
+
return cls(1)
|
56
|
+
|
57
|
+
# Compute signature using tuple of (base, exponent) pairs for efficiency
|
58
|
+
signature = 1.0
|
59
|
+
dimensions = [
|
60
|
+
(BaseDimension.LENGTH, length),
|
61
|
+
(BaseDimension.MASS, mass),
|
62
|
+
(BaseDimension.TIME, time),
|
63
|
+
(BaseDimension.CURRENT, current),
|
64
|
+
(BaseDimension.TEMPERATURE, temp),
|
65
|
+
(BaseDimension.AMOUNT, amount),
|
66
|
+
(BaseDimension.LUMINOSITY, luminosity),
|
67
|
+
]
|
68
|
+
|
69
|
+
for base, exponent in dimensions:
|
70
|
+
if exponent != 0:
|
71
|
+
signature *= base**exponent
|
72
|
+
|
73
|
+
return cls(signature)
|
74
|
+
|
75
|
+
def __mul__(self, other: "DimensionSignature") -> "DimensionSignature":
|
76
|
+
"""Multiply dimensions."""
|
77
|
+
if not isinstance(other, DimensionSignature):
|
78
|
+
raise TypeError(f"Cannot multiply DimensionSignature with {type(other)}")
|
79
|
+
return DimensionSignature(self._signature * other._signature)
|
80
|
+
|
81
|
+
def __truediv__(self, other: "DimensionSignature") -> "DimensionSignature":
|
82
|
+
"""Divide dimensions."""
|
83
|
+
if not isinstance(other, DimensionSignature):
|
84
|
+
raise TypeError(f"Cannot divide DimensionSignature by {type(other)}")
|
85
|
+
return DimensionSignature(self._signature / other._signature)
|
86
|
+
|
87
|
+
def __pow__(self, power: int | float) -> "DimensionSignature":
|
88
|
+
"""Raise dimension to a power."""
|
89
|
+
if not isinstance(power, int | float):
|
90
|
+
raise TypeError(f"Power must be int or float, got {type(power)}")
|
91
|
+
if power == 1:
|
92
|
+
return self
|
93
|
+
if power == 0:
|
94
|
+
return DimensionSignature(1)
|
95
|
+
return DimensionSignature(self._signature**power)
|
96
|
+
|
97
|
+
def is_compatible(self, other: "DimensionSignature") -> bool:
|
98
|
+
"""Check dimensional compatibility."""
|
99
|
+
if not isinstance(other, DimensionSignature):
|
100
|
+
return False
|
101
|
+
return self._signature == other._signature
|
102
|
+
|
103
|
+
def __eq__(self, other: object) -> bool:
|
104
|
+
"""Check equality."""
|
105
|
+
if self is other:
|
106
|
+
return True
|
107
|
+
return isinstance(other, DimensionSignature) and self._signature == other._signature
|
108
|
+
|
109
|
+
def __hash__(self) -> int:
|
110
|
+
"""Hash based on signature."""
|
111
|
+
return hash(self._signature)
|
@@ -0,0 +1,220 @@
|
|
1
|
+
"""
|
2
|
+
Equation System
|
3
|
+
===============
|
4
|
+
|
5
|
+
Mathematical equations for qnty variables with solving capabilities.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
import logging
|
11
|
+
from typing import cast
|
12
|
+
|
13
|
+
from ..constants import SOLVER_DEFAULT_TOLERANCE
|
14
|
+
from ..expressions import Expression, VariableReference
|
15
|
+
from ..quantities import FieldQnty
|
16
|
+
from ..utils.scope_discovery import ScopeDiscoveryService
|
17
|
+
|
18
|
+
_logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
# Global optimization flags
|
21
|
+
_SCOPE_DISCOVERY_ENABLED = False # Disabled by default due to high overhead
|
22
|
+
|
23
|
+
|
24
|
+
class Equation:
|
25
|
+
"""
|
26
|
+
Represents a mathematical equation with left-hand side equal to right-hand side.
|
27
|
+
Optimized with __slots__ for memory efficiency.
|
28
|
+
"""
|
29
|
+
|
30
|
+
__slots__ = ("name", "lhs", "rhs", "_variables")
|
31
|
+
|
32
|
+
def __init__(self, name: str, lhs: FieldQnty | Expression, rhs: Expression):
|
33
|
+
self.name = name
|
34
|
+
|
35
|
+
# Convert Variable to VariableReference if needed - use isinstance for performance
|
36
|
+
if isinstance(lhs, FieldQnty):
|
37
|
+
self.lhs = VariableReference(lhs)
|
38
|
+
else:
|
39
|
+
# It's already an Expression
|
40
|
+
self.lhs = cast(Expression, lhs)
|
41
|
+
|
42
|
+
self.rhs = rhs
|
43
|
+
self._variables: set[str] | None = None # Lazy initialization for better performance
|
44
|
+
|
45
|
+
def get_all_variables(self) -> set[str]:
|
46
|
+
"""Get all variable names used in this equation."""
|
47
|
+
if self._variables is None:
|
48
|
+
# Both lhs and rhs should be Expressions after __init__ conversion
|
49
|
+
lhs_vars = self.lhs.get_variables()
|
50
|
+
rhs_vars = self.rhs.get_variables()
|
51
|
+
self._variables = lhs_vars | rhs_vars
|
52
|
+
return self._variables
|
53
|
+
|
54
|
+
@property
|
55
|
+
def variables(self) -> set[str]:
|
56
|
+
"""Get all variable names used in this equation (cached property)."""
|
57
|
+
return self.get_all_variables()
|
58
|
+
|
59
|
+
def get_unknown_variables(self, known_vars: set[str]) -> set[str]:
|
60
|
+
"""Get variables that are unknown (not in known_vars set)."""
|
61
|
+
return self.variables - known_vars
|
62
|
+
|
63
|
+
def get_known_variables(self, known_vars: set[str]) -> set[str]:
|
64
|
+
"""Get variables that are known (in known_vars set)."""
|
65
|
+
return self.variables & known_vars
|
66
|
+
|
67
|
+
def can_solve_for(self, target_var: str, known_vars: set[str]) -> bool:
|
68
|
+
"""Check if this equation can solve for target_var given known_vars."""
|
69
|
+
if target_var not in self.variables:
|
70
|
+
return False
|
71
|
+
|
72
|
+
# Direct assignment case: lhs is the variable
|
73
|
+
if isinstance(self.lhs, VariableReference) and self.lhs.name == target_var:
|
74
|
+
rhs_vars = self.rhs.get_variables()
|
75
|
+
return rhs_vars.issubset(known_vars)
|
76
|
+
|
77
|
+
# General case: can solve if target_var is the only unknown
|
78
|
+
unknown_vars = self.get_unknown_variables(known_vars)
|
79
|
+
return unknown_vars == {target_var}
|
80
|
+
|
81
|
+
def solve_for(self, target_var: str, variable_values: dict[str, FieldQnty]) -> FieldQnty:
|
82
|
+
"""
|
83
|
+
Solve the equation for target_var.
|
84
|
+
Returns the target variable with updated quantity.
|
85
|
+
"""
|
86
|
+
if target_var not in self.variables:
|
87
|
+
raise ValueError(f"Variable '{target_var}' not found in equation")
|
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:
|
99
|
+
raise ValueError(f"Variable '{target_var}' not found in variable_values")
|
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:
|
114
|
+
"""
|
115
|
+
Check if equation is satisfied by evaluating residual (LHS - RHS).
|
116
|
+
Returns True if |residual| < tolerance, accounting for units.
|
117
|
+
"""
|
118
|
+
try:
|
119
|
+
# Both lhs and rhs should be Expressions after __init__ conversion
|
120
|
+
lhs_value = self.lhs.evaluate(variable_values)
|
121
|
+
rhs_value = self.rhs.evaluate(variable_values)
|
122
|
+
|
123
|
+
# Check dimensional compatibility using public API
|
124
|
+
if not self._are_dimensionally_compatible(lhs_value, rhs_value):
|
125
|
+
return False
|
126
|
+
|
127
|
+
# Convert to same units for comparison
|
128
|
+
rhs_converted = rhs_value.to(lhs_value.unit)
|
129
|
+
residual = abs(lhs_value.value - rhs_converted.value)
|
130
|
+
|
131
|
+
return residual < tolerance
|
132
|
+
except (ValueError, TypeError, AttributeError, KeyError) as e:
|
133
|
+
_logger.debug(f"Expected error in residual check for equation '{self.name}': {e}")
|
134
|
+
return False
|
135
|
+
except Exception as e:
|
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]:
|
141
|
+
"""
|
142
|
+
Automatically discover variables from the calling scope using centralized service.
|
143
|
+
"""
|
144
|
+
if not _SCOPE_DISCOVERY_ENABLED:
|
145
|
+
return {}
|
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."""
|
152
|
+
try:
|
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"
|
171
|
+
else:
|
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():
|
178
|
+
return False, "", {}
|
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}")
|
200
|
+
return False, "", {}
|
201
|
+
|
202
|
+
def _try_auto_solve(self) -> bool:
|
203
|
+
"""Try to automatically solve the equation if possible."""
|
204
|
+
try:
|
205
|
+
can_solve, target_var, variables = self._can_auto_solve()
|
206
|
+
if can_solve:
|
207
|
+
self.solve_for(target_var, variables)
|
208
|
+
return True
|
209
|
+
return False
|
210
|
+
except (AttributeError, KeyError, ValueError, TypeError) as e:
|
211
|
+
_logger.debug(f"Auto-solve failed for equation '{self.name}': {e}")
|
212
|
+
return False
|
213
|
+
|
214
|
+
def __str__(self) -> str:
|
215
|
+
# Try to auto-solve if possible before displaying
|
216
|
+
self._try_auto_solve()
|
217
|
+
return f"{self.lhs} = {self.rhs}"
|
218
|
+
|
219
|
+
def __repr__(self) -> str:
|
220
|
+
return f"Equation(name='{self.name}', lhs={self.lhs!r}, rhs={self.rhs!r})"
|
qnty/equations/system.py
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
from ..quantities import FieldQnty
|
2
|
+
from .equation import Equation
|
3
|
+
|
4
|
+
|
5
|
+
class EquationSystem:
|
6
|
+
"""
|
7
|
+
System of equations that can be solved together.
|
8
|
+
Optimized with __slots__ for memory efficiency.
|
9
|
+
"""
|
10
|
+
|
11
|
+
__slots__ = ("equations", "variables", "_known_cache", "_unknown_cache")
|
12
|
+
|
13
|
+
def __init__(self, equations: list[Equation] | None = None):
|
14
|
+
self.equations = equations or []
|
15
|
+
self.variables: dict[str, FieldQnty] = {} # Dict[str, FieldQnty]
|
16
|
+
self._known_cache: set[str] | None = None # Cache for known variables
|
17
|
+
self._unknown_cache: set[str] | None = None # Cache for unknown variables
|
18
|
+
|
19
|
+
def add_equation(self, equation: Equation):
|
20
|
+
"""Add an equation to the system."""
|
21
|
+
self.equations.append(equation)
|
22
|
+
self._invalidate_caches()
|
23
|
+
|
24
|
+
def add_variable(self, variable: FieldQnty):
|
25
|
+
"""Add a variable to the system."""
|
26
|
+
self.variables[variable.name] = variable
|
27
|
+
self._invalidate_caches()
|
28
|
+
|
29
|
+
def _invalidate_caches(self):
|
30
|
+
"""Invalidate cached known/unknown variable sets."""
|
31
|
+
self._known_cache = None
|
32
|
+
self._unknown_cache = None
|
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
|
+
|
38
|
+
def get_known_variables(self) -> set[str]:
|
39
|
+
"""Get names of all known variables (cached)."""
|
40
|
+
if self._known_cache is None:
|
41
|
+
self._known_cache = {name for name, var in self.variables.items() if self._is_variable_known(var)}
|
42
|
+
return self._known_cache
|
43
|
+
|
44
|
+
def get_unknown_variables(self) -> set[str]:
|
45
|
+
"""Get names of all unknown variables (cached)."""
|
46
|
+
if self._unknown_cache is None:
|
47
|
+
self._unknown_cache = {name for name, var in self.variables.items() if not self._is_variable_known(var)}
|
48
|
+
return self._unknown_cache
|
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
|
+
|
58
|
+
def can_solve_any(self) -> bool:
|
59
|
+
"""Check if any equation can be solved with current known variables."""
|
60
|
+
known_vars = self.get_known_variables()
|
61
|
+
unknown_vars = self.get_unknown_variables()
|
62
|
+
return self._find_solvable_equation_variable_pair(known_vars, unknown_vars) is not None
|
63
|
+
|
64
|
+
def solve_step(self) -> bool:
|
65
|
+
"""Solve one step - find and solve one equation. Returns True if progress made."""
|
66
|
+
known_vars = self.get_known_variables()
|
67
|
+
unknown_vars = self.get_unknown_variables()
|
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
|
+
|
78
|
+
def solve(self, max_iterations: int = 100) -> bool:
|
79
|
+
"""Solve the system iteratively. Returns True if fully solved."""
|
80
|
+
if max_iterations <= 0:
|
81
|
+
raise ValueError("max_iterations must be positive")
|
82
|
+
|
83
|
+
for _ in range(max_iterations):
|
84
|
+
if not self.can_solve_any():
|
85
|
+
break
|
86
|
+
if not self.solve_step():
|
87
|
+
break
|
88
|
+
|
89
|
+
# Check if all variables are known
|
90
|
+
unknown_vars = self.get_unknown_variables()
|
91
|
+
return len(unknown_vars) == 0
|
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
|
+
|
105
|
+
def get_solving_order(self) -> list[str]:
|
106
|
+
"""
|
107
|
+
Get the order in which variables can be solved.
|
108
|
+
Optimized to avoid creating full system copies.
|
109
|
+
"""
|
110
|
+
order = []
|
111
|
+
simulated_known = self.get_known_variables().copy()
|
112
|
+
|
113
|
+
max_iterations = len(self.variables) # Prevent infinite loops
|
114
|
+
for _ in range(max_iterations):
|
115
|
+
next_var = self._get_next_solvable_variable(simulated_known)
|
116
|
+
if next_var is None:
|
117
|
+
break
|
118
|
+
|
119
|
+
order.append(next_var)
|
120
|
+
simulated_known.add(next_var)
|
121
|
+
|
122
|
+
return order
|
123
|
+
|
124
|
+
def __str__(self) -> str:
|
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
|
+
|
129
|
+
def __repr__(self) -> str:
|
130
|
+
return f"EquationSystem(equations={self.equations!r})"
|
@@ -0,0 +1,40 @@
|
|
1
|
+
"""
|
2
|
+
Expression System Package
|
3
|
+
=========================
|
4
|
+
|
5
|
+
Mathematical expressions for building equation trees with qnty variables.
|
6
|
+
"""
|
7
|
+
|
8
|
+
# Core AST classes
|
9
|
+
# Helper functions
|
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
|
14
|
+
|
15
|
+
# Define public API
|
16
|
+
__all__ = [
|
17
|
+
# Core AST classes
|
18
|
+
"Expression",
|
19
|
+
"VariableReference",
|
20
|
+
"Constant",
|
21
|
+
"BinaryOperation",
|
22
|
+
"UnaryFunction",
|
23
|
+
"ConditionalExpression",
|
24
|
+
# Helper functions
|
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",
|
36
|
+
# Utilities
|
37
|
+
"wrap_operand",
|
38
|
+
# Scope discovery
|
39
|
+
"ScopeDiscoveryService",
|
40
|
+
]
|