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,165 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from ...equations import Equation
|
4
|
+
from ...expressions import ConditionalExpression, VariableReference
|
5
|
+
from ...quantities.field_qnty import FieldQnty
|
6
|
+
from ..order import Order
|
7
|
+
from .base import BaseSolver, SolveResult
|
8
|
+
|
9
|
+
|
10
|
+
class IterativeSolver(BaseSolver):
|
11
|
+
"""
|
12
|
+
Iterative solver that follows dependency order like solving engineering problems by hand.
|
13
|
+
|
14
|
+
This solver works by:
|
15
|
+
1. Using dependency graph to determine the correct solving order
|
16
|
+
2. Solving variables one by one in dependency order (just like manual solving)
|
17
|
+
3. Preserving units throughout with Pint integration
|
18
|
+
4. Verifying each solution with residual checking
|
19
|
+
5. Repeating until all unknowns are solved
|
20
|
+
|
21
|
+
This approach mirrors how engineers solve problems by hand: solve what you can
|
22
|
+
with what you know, then use those results to solve the next level of dependencies.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def can_handle(self, equations: list[Equation], unknowns: set[str], dependency_graph: Order | None = None, analysis: dict[str, Any] | None = None) -> bool:
|
26
|
+
"""
|
27
|
+
Can handle any system that has at least one unknown and a dependency graph.
|
28
|
+
"""
|
29
|
+
return bool(unknowns and dependency_graph)
|
30
|
+
|
31
|
+
def solve(self, equations: list[Equation], variables: dict[str, FieldQnty], dependency_graph: Order | None = None, max_iterations: int = 100, tolerance: float = 1e-10) -> SolveResult:
|
32
|
+
"""
|
33
|
+
Solve the system iteratively using dependency graph.
|
34
|
+
"""
|
35
|
+
self.steps = []
|
36
|
+
|
37
|
+
if not dependency_graph:
|
38
|
+
return SolveResult(variables=variables, steps=self.steps, success=False, message="Dependency graph required for iterative solving", method="IterativeSolver")
|
39
|
+
|
40
|
+
# Make a copy of variables to work with
|
41
|
+
working_vars = dict(variables.items())
|
42
|
+
known_vars = self._get_known_variables(working_vars)
|
43
|
+
|
44
|
+
if self.logger:
|
45
|
+
self.logger.debug(f"Starting iterative solve with {len(known_vars)} known variables")
|
46
|
+
|
47
|
+
# Iterative solving
|
48
|
+
iteration = 0
|
49
|
+
for iteration in range(max_iterations):
|
50
|
+
iteration_start = len(known_vars)
|
51
|
+
|
52
|
+
# Get variables that can be solved in this iteration
|
53
|
+
solvable = dependency_graph.get_solvable_variables(known_vars)
|
54
|
+
|
55
|
+
# Fallback: attempt direct equations for remaining unknowns
|
56
|
+
if not solvable:
|
57
|
+
solvable = self._find_directly_solvable_variables(equations, working_vars, known_vars)
|
58
|
+
|
59
|
+
# Try to break conditional cycles if still no solvable variables
|
60
|
+
if not solvable:
|
61
|
+
solvable = self._solve_conditional_cycles(equations, working_vars, known_vars)
|
62
|
+
|
63
|
+
if not solvable:
|
64
|
+
break # No more variables can be solved
|
65
|
+
|
66
|
+
if self.logger:
|
67
|
+
self.logger.debug(f"Iteration {iteration + 1} solvable: {solvable}")
|
68
|
+
|
69
|
+
# Solve for each solvable variable
|
70
|
+
for var_symbol in solvable:
|
71
|
+
result = self._solve_single_variable(var_symbol, equations, working_vars, known_vars, dependency_graph, iteration, tolerance)
|
72
|
+
if not result:
|
73
|
+
return SolveResult(variables=working_vars, steps=self.steps, success=False, message=f"Failed to solve for {var_symbol}", method="IterativeSolver", iterations=iteration + 1)
|
74
|
+
|
75
|
+
# Check for progress
|
76
|
+
if len(known_vars) == iteration_start:
|
77
|
+
if self.logger:
|
78
|
+
self.logger.warning("No progress made, stopping early")
|
79
|
+
break
|
80
|
+
|
81
|
+
# Check if we solved all unknowns
|
82
|
+
remaining_unknowns = self._get_unknown_variables(working_vars)
|
83
|
+
success = len(remaining_unknowns) == 0
|
84
|
+
|
85
|
+
message = "All variables solved" if success else f"Could not solve: {remaining_unknowns}"
|
86
|
+
|
87
|
+
return SolveResult(variables=working_vars, steps=self.steps, success=success, message=message, method="IterativeSolver", iterations=iteration + 1)
|
88
|
+
|
89
|
+
def _find_directly_solvable_variables(self, equations: list[Equation], working_vars: dict[str, FieldQnty], known_vars: set[str]) -> list[str]:
|
90
|
+
"""Find variables that can be directly solved from equations."""
|
91
|
+
solvable = []
|
92
|
+
remaining_unknowns = [v for v in self._get_unknown_variables(working_vars) if v not in known_vars]
|
93
|
+
|
94
|
+
for var_symbol in remaining_unknowns:
|
95
|
+
for eq in equations:
|
96
|
+
if eq.can_solve_for(var_symbol, known_vars):
|
97
|
+
solvable.append(var_symbol)
|
98
|
+
break
|
99
|
+
|
100
|
+
return solvable
|
101
|
+
|
102
|
+
def _solve_conditional_cycles(self, equations: list[Equation], working_vars: dict[str, FieldQnty], known_vars: set[str]) -> list[str]:
|
103
|
+
"""Attempt to solve conditional cycles in the equation system."""
|
104
|
+
remaining_unknowns = [v for v in self._get_unknown_variables(working_vars) if v not in known_vars]
|
105
|
+
|
106
|
+
for var_symbol in remaining_unknowns:
|
107
|
+
for eq in equations:
|
108
|
+
# Check if this is a conditional equation that can be solved
|
109
|
+
if self._is_conditional_equation(eq, var_symbol):
|
110
|
+
try:
|
111
|
+
solved_var = eq.solve_for(var_symbol, working_vars)
|
112
|
+
working_vars[var_symbol] = solved_var
|
113
|
+
known_vars.add(var_symbol)
|
114
|
+
|
115
|
+
if self.logger:
|
116
|
+
self.logger.debug(f"Solved conditional cycle: {var_symbol} = {solved_var.quantity}")
|
117
|
+
|
118
|
+
return [var_symbol] # Return immediately after solving one
|
119
|
+
except Exception:
|
120
|
+
continue
|
121
|
+
|
122
|
+
return []
|
123
|
+
|
124
|
+
def _is_conditional_equation(self, equation: Equation, var_symbol: str) -> bool:
|
125
|
+
"""Check if equation is a conditional equation for the given variable."""
|
126
|
+
return isinstance(equation.lhs, VariableReference) and equation.lhs.name == var_symbol and isinstance(equation.rhs, ConditionalExpression)
|
127
|
+
|
128
|
+
def _solve_single_variable(
|
129
|
+
self, var_symbol: str, equations: list[Equation], working_vars: dict[str, FieldQnty], known_vars: set[str], dependency_graph: Order, iteration: int, tolerance: float
|
130
|
+
) -> bool:
|
131
|
+
"""Solve for a single variable and update the system state."""
|
132
|
+
# Find equation that can solve for this variable
|
133
|
+
equation = dependency_graph.get_equation_for_variable(var_symbol, known_vars)
|
134
|
+
|
135
|
+
if equation is None:
|
136
|
+
# Try any equation that can solve it
|
137
|
+
for eq in equations:
|
138
|
+
if eq.can_solve_for(var_symbol, known_vars):
|
139
|
+
equation = eq
|
140
|
+
break
|
141
|
+
|
142
|
+
if equation is None:
|
143
|
+
return True # Skip this variable, not a failure
|
144
|
+
|
145
|
+
try:
|
146
|
+
solved_var = equation.solve_for(var_symbol, working_vars)
|
147
|
+
working_vars[var_symbol] = solved_var
|
148
|
+
known_vars.add(var_symbol)
|
149
|
+
|
150
|
+
# Verify solution by checking residual
|
151
|
+
if equation.check_residual(working_vars, tolerance):
|
152
|
+
if self.logger:
|
153
|
+
self.logger.debug(f"Solution verified for {var_symbol}")
|
154
|
+
else:
|
155
|
+
if self.logger:
|
156
|
+
self.logger.warning(f"Residual check failed for {var_symbol}")
|
157
|
+
|
158
|
+
self._log_step(iteration + 1, var_symbol, str(equation), str(solved_var.quantity), "iterative")
|
159
|
+
|
160
|
+
return True
|
161
|
+
|
162
|
+
except Exception as e:
|
163
|
+
if self.logger:
|
164
|
+
self.logger.error(f"Failed to solve for {var_symbol}: {e}")
|
165
|
+
return False
|
@@ -0,0 +1,475 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
try:
|
6
|
+
from scipy.linalg import solve as scipy_solve # type: ignore[import-untyped]
|
7
|
+
|
8
|
+
HAS_SCIPY = True
|
9
|
+
except ImportError:
|
10
|
+
HAS_SCIPY = False
|
11
|
+
scipy_solve = None
|
12
|
+
|
13
|
+
from qnty.solving.order import Order
|
14
|
+
|
15
|
+
from ...equations import Equation
|
16
|
+
from ...quantities import Quantity
|
17
|
+
from ...quantities.field_qnty import FieldQnty
|
18
|
+
from .base import BaseSolver, SolveResult
|
19
|
+
|
20
|
+
|
21
|
+
class SimultaneousEquationSolver(BaseSolver):
|
22
|
+
"""
|
23
|
+
Solver for n×n simultaneous linear equation systems using matrix operations.
|
24
|
+
|
25
|
+
This solver handles systems where equations are mutually dependent (forming cycles
|
26
|
+
in the dependency graph), requiring simultaneous solution rather than sequential
|
27
|
+
solution of individual equations.
|
28
|
+
|
29
|
+
Algorithm:
|
30
|
+
1. Validate system requirements (square, n≥2, has cycles)
|
31
|
+
2. Extract coefficient matrix A and constant vector b from equations
|
32
|
+
3. Check numerical stability (condition number)
|
33
|
+
4. Solve matrix system Ax = b using robust linear algebra
|
34
|
+
5. Update variables with solutions and verify residuals
|
35
|
+
|
36
|
+
Supports any size square system (n equations, n unknowns) that is:
|
37
|
+
- Linearly independent (non-singular matrix)
|
38
|
+
- Well-conditioned (numerically stable)
|
39
|
+
- Composed of linear equations with units preserved throughout
|
40
|
+
|
41
|
+
Examples:
|
42
|
+
2×2 system: x + y = 3, 2x - y = 0 → x=1, y=2
|
43
|
+
3×3 system: Complex engineering systems with interdependent variables
|
44
|
+
"""
|
45
|
+
|
46
|
+
# Constants for numerical stability and validation
|
47
|
+
MIN_SYSTEM_SIZE = 2
|
48
|
+
MAX_CONDITION_NUMBER = 1e12
|
49
|
+
DEFAULT_TOLERANCE = 1e-10
|
50
|
+
|
51
|
+
# Performance optimization constants
|
52
|
+
LARGE_SYSTEM_THRESHOLD = 100 # Switch to optimized algorithms for n > 100
|
53
|
+
SPARSE_THRESHOLD = 0.1 # Use sparse matrices if density < 10%
|
54
|
+
|
55
|
+
def can_handle(self, equations: list[Equation], unknowns: set[str], dependency_graph: Order | None = None, analysis: dict[str, Any] | None = None) -> bool:
|
56
|
+
"""
|
57
|
+
Determine if this solver can handle the given system.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
equations: List of equations to solve
|
61
|
+
unknowns: Set of unknown variable symbols
|
62
|
+
dependency_graph: Optional dependency graph (unused)
|
63
|
+
analysis: Optional system analysis containing cycle information
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
True if this solver can handle the system:
|
67
|
+
- Square system (n equations = n unknowns)
|
68
|
+
- Minimum size (n ≥ 2)
|
69
|
+
- Contains cycles (indicating simultaneous equations needed)
|
70
|
+
"""
|
71
|
+
# dependency_graph parameter unused but required for interface compatibility
|
72
|
+
_ = dependency_graph
|
73
|
+
|
74
|
+
system_size = len(equations)
|
75
|
+
num_unknowns = len(unknowns)
|
76
|
+
|
77
|
+
# Validate square system with minimum size
|
78
|
+
if system_size != num_unknowns or system_size < self.MIN_SYSTEM_SIZE:
|
79
|
+
return False
|
80
|
+
|
81
|
+
# Only handle systems with cycles (mutual dependencies)
|
82
|
+
if analysis is None:
|
83
|
+
return False
|
84
|
+
has_cycles = analysis.get("has_cycles", False)
|
85
|
+
return bool(has_cycles)
|
86
|
+
|
87
|
+
def solve(self, equations: list[Equation], variables: dict[str, FieldQnty], dependency_graph: Order | None = None, max_iterations: int = 100, tolerance: float = DEFAULT_TOLERANCE) -> SolveResult:
|
88
|
+
"""
|
89
|
+
Solve the n×n simultaneous system using matrix operations.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
equations: List of n linear equations to solve simultaneously
|
93
|
+
variables: Dictionary of all variables (known and unknown)
|
94
|
+
dependency_graph: Optional dependency graph
|
95
|
+
(unused for simultaneous systems)
|
96
|
+
max_iterations: Maximum iterations
|
97
|
+
(unused for direct matrix solving)
|
98
|
+
tolerance: Numerical tolerance for residual checking
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
SolveResult containing the solution or error information
|
102
|
+
"""
|
103
|
+
# Mark unused parameters to satisfy linter
|
104
|
+
_ = dependency_graph, max_iterations
|
105
|
+
|
106
|
+
self.steps = []
|
107
|
+
|
108
|
+
# Step 1: Validate system requirements
|
109
|
+
validation_result = self._validate_system(equations, variables)
|
110
|
+
if not validation_result.success:
|
111
|
+
return validation_result
|
112
|
+
|
113
|
+
unknown_variable_names = list(self._get_unknown_variables(variables))
|
114
|
+
working_variables = dict(variables)
|
115
|
+
|
116
|
+
# Step 2: Extract and solve matrix system
|
117
|
+
try:
|
118
|
+
solution_vector = self._solve_matrix_system(equations, unknown_variable_names, working_variables)
|
119
|
+
if solution_vector is None:
|
120
|
+
return SolveResult(variables=working_variables, steps=self.steps, success=False, message="Failed to solve matrix system", method="SimultaneousEquationSolver")
|
121
|
+
|
122
|
+
# Step 3: Update variables with solutions
|
123
|
+
self._apply_solution_to_variables(unknown_variable_names, solution_vector, working_variables)
|
124
|
+
|
125
|
+
# Step 4: Verify solution quality
|
126
|
+
verification_result = self._verify_solution_quality(equations, working_variables, tolerance)
|
127
|
+
|
128
|
+
return SolveResult(
|
129
|
+
variables=working_variables, steps=self.steps, success=verification_result.success, message=verification_result.message, method="SimultaneousEquationSolver", iterations=1
|
130
|
+
)
|
131
|
+
|
132
|
+
except Exception as general_error:
|
133
|
+
return SolveResult(variables=working_variables, steps=self.steps, success=False, message=f"Simultaneous solving failed: {general_error}", method="SimultaneousEquationSolver")
|
134
|
+
|
135
|
+
def _validate_system(self, equations: list[Equation], variables: dict[str, FieldQnty]) -> SolveResult:
|
136
|
+
"""
|
137
|
+
Validate that the system meets requirements for simultaneous solving.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
SolveResult with success=True if valid, or error result if invalid
|
141
|
+
"""
|
142
|
+
unknown_variable_names = list(self._get_unknown_variables(variables))
|
143
|
+
num_unknowns = len(unknown_variable_names)
|
144
|
+
num_equations = len(equations)
|
145
|
+
|
146
|
+
# Validate square system with minimum size
|
147
|
+
if num_unknowns != num_equations or num_unknowns < self.MIN_SYSTEM_SIZE:
|
148
|
+
return SolveResult(
|
149
|
+
variables=variables,
|
150
|
+
steps=self.steps,
|
151
|
+
success=False,
|
152
|
+
message=f"Simultaneous solver requires n×n system (got {num_equations} equations, {num_unknowns} unknowns)",
|
153
|
+
method="SimultaneousEquationSolver",
|
154
|
+
)
|
155
|
+
|
156
|
+
if self.logger:
|
157
|
+
self.logger.debug(f"Attempting {num_unknowns}×{num_unknowns} simultaneous solution for {unknown_variable_names}")
|
158
|
+
|
159
|
+
return SolveResult(variables=variables, steps=self.steps, success=True, message="System validation passed", method="SimultaneousEquationSolver")
|
160
|
+
|
161
|
+
def _solve_matrix_system(self, equations: list[Equation], unknown_variables: list[str], working_variables: dict[str, FieldQnty]) -> np.ndarray | None:
|
162
|
+
"""
|
163
|
+
Extract coefficient matrix and solve the linear system.
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
Solution vector if successful, None if failed
|
167
|
+
"""
|
168
|
+
# Extract coefficient matrix A and constant vector b
|
169
|
+
coefficient_matrix, constant_vector = self._extract_matrix_system(equations, unknown_variables, working_variables)
|
170
|
+
|
171
|
+
if coefficient_matrix is None or constant_vector is None:
|
172
|
+
if self.logger:
|
173
|
+
self.logger.error("Could not extract linear coefficients from equations")
|
174
|
+
return None
|
175
|
+
|
176
|
+
# Check numerical stability via condition number
|
177
|
+
condition_number = np.linalg.cond(coefficient_matrix)
|
178
|
+
if condition_number > self.MAX_CONDITION_NUMBER:
|
179
|
+
if self.logger:
|
180
|
+
# Use debug level for expected fallback scenarios (systems with conditionals)
|
181
|
+
if np.isinf(condition_number):
|
182
|
+
self.logger.debug("Matrix is singular (cond=inf), falling back to iterative solver")
|
183
|
+
else:
|
184
|
+
self.logger.debug(f"System is ill-conditioned (cond={condition_number:.2e}), falling back to iterative solver")
|
185
|
+
return None
|
186
|
+
|
187
|
+
try:
|
188
|
+
# Choose optimal solving algorithm based on system size
|
189
|
+
system_size = coefficient_matrix.shape[0]
|
190
|
+
solution_vector = self._solve_optimized_system(coefficient_matrix, constant_vector, system_size)
|
191
|
+
return solution_vector
|
192
|
+
|
193
|
+
except np.linalg.LinAlgError as linear_algebra_error:
|
194
|
+
if self.logger:
|
195
|
+
self.logger.error(f"Linear algebra error: {linear_algebra_error}")
|
196
|
+
return None
|
197
|
+
|
198
|
+
def _solve_optimized_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray, system_size: int) -> np.ndarray:
|
199
|
+
"""
|
200
|
+
Solve the matrix system using optimized algorithms based on system characteristics.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
coefficient_matrix: The coefficient matrix A
|
204
|
+
constant_vector: The constant vector b
|
205
|
+
system_size: Size of the system (n for n×n)
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
Solution vector for the system Ax = b
|
209
|
+
"""
|
210
|
+
if system_size <= self.LARGE_SYSTEM_THRESHOLD:
|
211
|
+
# Use standard NumPy solver for small-medium systems
|
212
|
+
return np.linalg.solve(coefficient_matrix, constant_vector)
|
213
|
+
else:
|
214
|
+
# For large systems, use more efficient algorithms
|
215
|
+
if self.logger:
|
216
|
+
self.logger.debug(f"Using optimized algorithms for large system (n={system_size})")
|
217
|
+
|
218
|
+
# Check matrix density to decide between dense/sparse algorithms
|
219
|
+
density = np.count_nonzero(coefficient_matrix) / coefficient_matrix.size
|
220
|
+
|
221
|
+
if density < self.SPARSE_THRESHOLD:
|
222
|
+
# Use sparse matrix algorithms
|
223
|
+
if self.logger:
|
224
|
+
self.logger.debug(f"Matrix density {density:.3f} < {self.SPARSE_THRESHOLD}, using sparse algorithms")
|
225
|
+
return self._solve_sparse_system(coefficient_matrix, constant_vector)
|
226
|
+
else:
|
227
|
+
# Use optimized dense algorithms
|
228
|
+
if self.logger:
|
229
|
+
self.logger.debug(f"Matrix density {density:.3f} >= {self.SPARSE_THRESHOLD}, using optimized dense algorithms")
|
230
|
+
return self._solve_large_dense_system(coefficient_matrix, constant_vector)
|
231
|
+
|
232
|
+
def _solve_sparse_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray) -> np.ndarray:
|
233
|
+
"""
|
234
|
+
Solve sparse matrix system. Currently falls back to dense solver.
|
235
|
+
"""
|
236
|
+
return np.linalg.solve(coefficient_matrix, constant_vector)
|
237
|
+
|
238
|
+
def _solve_large_dense_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray) -> np.ndarray:
|
239
|
+
"""
|
240
|
+
Solve large dense matrix system using optimized algorithms.
|
241
|
+
"""
|
242
|
+
if HAS_SCIPY and scipy_solve is not None:
|
243
|
+
return scipy_solve(coefficient_matrix, constant_vector, assume_a="gen")
|
244
|
+
return np.linalg.solve(coefficient_matrix, constant_vector)
|
245
|
+
|
246
|
+
def _apply_solution_to_variables(self, unknown_variables: list[str], solution_vector: np.ndarray, working_variables: dict[str, FieldQnty]):
|
247
|
+
"""
|
248
|
+
Apply solution values to variables and record solving steps.
|
249
|
+
"""
|
250
|
+
for i, variable_name in enumerate(unknown_variables):
|
251
|
+
solution_value = float(solution_vector[i])
|
252
|
+
self._update_variable_with_solution(variable_name, solution_value, working_variables)
|
253
|
+
|
254
|
+
def _verify_solution_quality(self, equations: list[Equation], working_variables: dict[str, FieldQnty], tolerance: float) -> SolveResult:
|
255
|
+
"""
|
256
|
+
Verify solution quality by checking equation residuals.
|
257
|
+
|
258
|
+
Returns:
|
259
|
+
SolveResult indicating whether solution meets quality requirements
|
260
|
+
"""
|
261
|
+
max_residual = 0.0
|
262
|
+
for equation in equations:
|
263
|
+
if equation.check_residual(working_variables, tolerance):
|
264
|
+
if self.logger:
|
265
|
+
self.logger.debug(f"Equation {equation.name} verified")
|
266
|
+
else:
|
267
|
+
residual = self._calculate_equation_residual(equation, working_variables)
|
268
|
+
max_residual = max(max_residual, abs(residual))
|
269
|
+
if self.logger:
|
270
|
+
self.logger.warning(f"Equation {equation.name} residual: {residual}")
|
271
|
+
|
272
|
+
is_successful = max_residual < tolerance
|
273
|
+
success_message = "Simultaneous system solved successfully" if is_successful else f"Large residuals detected (max={max_residual:.2e})"
|
274
|
+
|
275
|
+
if self.logger and is_successful:
|
276
|
+
num_unknowns = len([v for v in working_variables.values() if not v.is_known])
|
277
|
+
variable_solutions = {var: f"{working_variables[var].quantity}" for var in working_variables if not working_variables[var].is_known}
|
278
|
+
self.logger.debug(f"Solved {num_unknowns}×{num_unknowns} system: {variable_solutions}")
|
279
|
+
|
280
|
+
return SolveResult(variables=working_variables, steps=[], success=is_successful, message=success_message, method="SimultaneousEquationSolver")
|
281
|
+
|
282
|
+
def _extract_matrix_system(self, equations: list[Equation], unknown_variables: list[str], variables: dict[str, FieldQnty]) -> tuple[np.ndarray | None, np.ndarray | None]:
|
283
|
+
"""
|
284
|
+
Extract coefficient matrix A and constant vector b from the system of equations.
|
285
|
+
|
286
|
+
Args:
|
287
|
+
equations: List of linear equations to extract coefficients from
|
288
|
+
unknown_variables: List of unknown variable symbols (determines column order)
|
289
|
+
variables: Dictionary of all variables for evaluation
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
Tuple of (coefficient_matrix, constant_vector) for system Ax = b
|
293
|
+
Returns (None, None) if extraction fails
|
294
|
+
|
295
|
+
Algorithm:
|
296
|
+
For each equation, extract coefficients by numerical differentiation:
|
297
|
+
1. Test each unknown variable with value 1, others with 0
|
298
|
+
2. Calculate residual to determine coefficient
|
299
|
+
3. Build coefficient matrix row by row
|
300
|
+
"""
|
301
|
+
try:
|
302
|
+
num_equations = len(equations)
|
303
|
+
|
304
|
+
# Use consistent float64 precision for numerical stability
|
305
|
+
dtype = np.float64
|
306
|
+
|
307
|
+
coefficient_matrix = np.zeros((num_equations, num_equations), dtype=dtype)
|
308
|
+
constant_vector = np.zeros(num_equations, dtype=dtype)
|
309
|
+
|
310
|
+
# Process equations in batches for large systems to reduce memory pressure
|
311
|
+
for equation_index, equation in enumerate(equations):
|
312
|
+
coefficient_list = self._extract_linear_coefficients_vector(equation, unknown_variables, variables)
|
313
|
+
if coefficient_list is None:
|
314
|
+
return None, None
|
315
|
+
|
316
|
+
# coefficient_list contains [a1, a2, ..., an, constant]
|
317
|
+
# where equation is a1*x1 + a2*x2 + ... + an*xn = constant
|
318
|
+
coefficient_matrix[equation_index, :] = coefficient_list[:-1] # Coefficients
|
319
|
+
constant_vector[equation_index] = coefficient_list[-1] # Constant term
|
320
|
+
|
321
|
+
return coefficient_matrix, constant_vector
|
322
|
+
|
323
|
+
except Exception:
|
324
|
+
return None, None
|
325
|
+
|
326
|
+
def _extract_linear_coefficients_vector(self, equation: Equation, unknown_variables: list[str], variables: dict[str, FieldQnty]) -> list[float] | None:
|
327
|
+
"""
|
328
|
+
Extract linear coefficients from equation using numerical differentiation.
|
329
|
+
|
330
|
+
Args:
|
331
|
+
equation: The equation to extract coefficients from
|
332
|
+
unknown_variables: List of unknown variable symbols
|
333
|
+
variables: Dictionary of all variables
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
List [a1, a2, ..., an, c] for equation a1*x1 + a2*x2 + ... + an*xn = c
|
337
|
+
Returns None if extraction fails
|
338
|
+
|
339
|
+
Algorithm:
|
340
|
+
Uses finite difference approximation:
|
341
|
+
1. Set all unknowns to 0, calculate residual → gives constant term
|
342
|
+
2. Set one unknown to 1, others to 0 → gives coefficient for that variable
|
343
|
+
3. Repeat for all unknowns to build coefficient vector
|
344
|
+
"""
|
345
|
+
try:
|
346
|
+
num_unknowns = len(unknown_variables)
|
347
|
+
coefficients = []
|
348
|
+
|
349
|
+
# Memory optimization: Pre-allocate arrays for large systems
|
350
|
+
residual_test_cases: list[float] = []
|
351
|
+
|
352
|
+
# Reuse test variables dictionary to reduce object creation overhead
|
353
|
+
test_vars = variables.copy()
|
354
|
+
|
355
|
+
# Test case for each unknown variable (finite difference)
|
356
|
+
for variable_index in range(num_unknowns):
|
357
|
+
self._set_test_variables(test_vars, unknown_variables, variable_index)
|
358
|
+
residual = self._calculate_equation_residual(equation, test_vars)
|
359
|
+
residual_test_cases.append(residual)
|
360
|
+
|
361
|
+
# Test case with all unknowns = 0 (baseline)
|
362
|
+
self._set_test_variables(test_vars, unknown_variables, -1) # -1 means set all to 0
|
363
|
+
baseline_residual = self._calculate_equation_residual(equation, test_vars)
|
364
|
+
|
365
|
+
# Extract coefficients: for equation sum(ai*xi) - c = 0
|
366
|
+
# When xi=1, xj=0 (j≠i): ai - c = residual_i → ai = residual_i + c
|
367
|
+
# When all xi=0: -c = baseline_residual → c = -baseline_residual
|
368
|
+
|
369
|
+
constant_term = -baseline_residual
|
370
|
+
for residual in residual_test_cases:
|
371
|
+
coefficient = residual + constant_term
|
372
|
+
coefficients.append(coefficient)
|
373
|
+
|
374
|
+
coefficients.append(constant_term) # Add constant term
|
375
|
+
return coefficients
|
376
|
+
|
377
|
+
except Exception:
|
378
|
+
return None
|
379
|
+
|
380
|
+
def _calculate_equation_residual(self, equation: Equation, test_variables: dict[str, FieldQnty]) -> float:
|
381
|
+
"""
|
382
|
+
Calculate equation residual (LHS - RHS) with proper unit handling.
|
383
|
+
|
384
|
+
Args:
|
385
|
+
equation: The equation to evaluate
|
386
|
+
test_variables: Dictionary of variables with test values
|
387
|
+
|
388
|
+
Returns:
|
389
|
+
Numerical residual value (dimensionless)
|
390
|
+
Returns infinity if evaluation fails
|
391
|
+
"""
|
392
|
+
try:
|
393
|
+
left_hand_side = equation.lhs.evaluate(test_variables)
|
394
|
+
right_hand_side = equation.rhs.evaluate(test_variables)
|
395
|
+
|
396
|
+
# Calculate residual and extract numerical value
|
397
|
+
residual = left_hand_side - right_hand_side
|
398
|
+
return self._extract_numerical_value(residual)
|
399
|
+
|
400
|
+
except Exception:
|
401
|
+
# Fallback for cases where evaluation fails
|
402
|
+
return float("inf")
|
403
|
+
|
404
|
+
def _update_variable_with_solution(self, variable_symbol: str, solution_value: float, variables: dict[str, FieldQnty]):
|
405
|
+
"""
|
406
|
+
Update a variable with its solved value and record the solving step.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
variable_symbol: Symbol of the variable to update
|
410
|
+
solution_value: Numerical solution value
|
411
|
+
variables: Dictionary of variables to update
|
412
|
+
"""
|
413
|
+
original_variable = variables[variable_symbol]
|
414
|
+
if original_variable.quantity is None:
|
415
|
+
raise ValueError(f"Variable {variable_symbol} has no quantity")
|
416
|
+
result_unit = original_variable.quantity.unit
|
417
|
+
solution_quantity = Quantity(solution_value, result_unit)
|
418
|
+
|
419
|
+
# Preserve the original variable name and create solved variable
|
420
|
+
original_name = original_variable.name
|
421
|
+
solved_variable = FieldQnty(name=original_name, expected_dimension=solution_quantity.dimension, is_known=True)
|
422
|
+
solved_variable.quantity = solution_quantity
|
423
|
+
solved_variable.symbol = variable_symbol
|
424
|
+
variables[variable_symbol] = solved_variable
|
425
|
+
|
426
|
+
# Record solving step for tracking
|
427
|
+
self._log_step(
|
428
|
+
1, # iteration number
|
429
|
+
variable_symbol,
|
430
|
+
"simultaneous_system",
|
431
|
+
str(solution_quantity),
|
432
|
+
"simultaneous",
|
433
|
+
)
|
434
|
+
|
435
|
+
def _set_test_variables(self, test_vars: dict[str, FieldQnty], unknown_variables: list[str], active_index: int):
|
436
|
+
"""
|
437
|
+
Set test variables for coefficient extraction.
|
438
|
+
|
439
|
+
Args:
|
440
|
+
test_vars: Dictionary of test variables to modify
|
441
|
+
unknown_variables: List of unknown variable names
|
442
|
+
active_index: Index of variable to set to 1.0, others set to 0.0. If -1, all set to 0.0
|
443
|
+
"""
|
444
|
+
for unknown_index, var_name in enumerate(unknown_variables):
|
445
|
+
test_value = 1.0 if unknown_index == active_index else 0.0
|
446
|
+
original_var = test_vars[var_name]
|
447
|
+
if original_var.quantity is None:
|
448
|
+
raise ValueError(f"Variable {var_name} has no quantity")
|
449
|
+
test_var = FieldQnty(name=f"test_{var_name}", expected_dimension=original_var.quantity.dimension, is_known=True)
|
450
|
+
test_var.quantity = Quantity(test_value, original_var.quantity.unit)
|
451
|
+
test_var.symbol = var_name
|
452
|
+
test_vars[var_name] = test_var
|
453
|
+
|
454
|
+
def _extract_numerical_value(self, value: Any) -> float:
|
455
|
+
"""
|
456
|
+
Extract numerical value from various quantity types.
|
457
|
+
|
458
|
+
Args:
|
459
|
+
value: Value that may be a Quantity, float, int, or other numeric type
|
460
|
+
|
461
|
+
Returns:
|
462
|
+
Float representation of the value
|
463
|
+
"""
|
464
|
+
# Check for Quantity type first (most common case)
|
465
|
+
if isinstance(value, Quantity):
|
466
|
+
return float(value.value)
|
467
|
+
# Handle primitive numeric types
|
468
|
+
elif isinstance(value, int | float):
|
469
|
+
return float(value)
|
470
|
+
# Handle objects with .value attribute as last resort
|
471
|
+
elif hasattr(value, "value"):
|
472
|
+
return float(value.value)
|
473
|
+
else:
|
474
|
+
# Last resort: try direct conversion
|
475
|
+
return float(value)
|
qnty/units/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
from .field_units import *
|