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
qnty/solving/__init__.py
ADDED
File without changes
|
qnty/solving/manager.py
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from qnty.solving.order import Order
|
4
|
+
|
5
|
+
from ..equations import Equation
|
6
|
+
from ..quantities import FieldQnty
|
7
|
+
from .solvers.base import BaseSolver, SolveResult
|
8
|
+
from .solvers.iterative import IterativeSolver
|
9
|
+
from .solvers.simultaneous import SimultaneousEquationSolver
|
10
|
+
|
11
|
+
|
12
|
+
class SolverManager:
|
13
|
+
"""
|
14
|
+
Manages multiple solvers and selects the best one for a given problem.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self, logger: logging.Logger | None = None):
|
18
|
+
self.logger = logger
|
19
|
+
self.solvers = [
|
20
|
+
SimultaneousEquationSolver(logger), # Try simultaneous first for cyclic systems
|
21
|
+
IterativeSolver(logger), # Fall back to iterative
|
22
|
+
]
|
23
|
+
|
24
|
+
def solve(self, equations: list[Equation], variables: dict[str, FieldQnty], dependency_graph: Order | None = None, max_iterations: int = 100, tolerance: float = 1e-10) -> SolveResult:
|
25
|
+
"""
|
26
|
+
Solve the system using the best available solver.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
equations: List of equations to solve
|
30
|
+
variables: Dictionary of all variables (known and unknown)
|
31
|
+
dependency_graph: Optional dependency graph
|
32
|
+
max_iterations: Maximum number of iterations
|
33
|
+
tolerance: Convergence tolerance
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
SolveResult containing the solution
|
37
|
+
"""
|
38
|
+
unknowns = {s for s, v in variables.items() if not v.is_known}
|
39
|
+
|
40
|
+
if not unknowns:
|
41
|
+
return SolveResult(variables=variables, steps=[], success=True, message="No unknowns to solve", method="NoSolver")
|
42
|
+
|
43
|
+
# Get system analysis if we have a dependency graph
|
44
|
+
analysis = None
|
45
|
+
if dependency_graph:
|
46
|
+
known_vars = {s for s, v in variables.items() if v.is_known}
|
47
|
+
analysis = dependency_graph.analyze_system(known_vars)
|
48
|
+
|
49
|
+
# Try each solver in order of preference
|
50
|
+
for solver in self.solvers:
|
51
|
+
if solver.can_handle(equations, unknowns, dependency_graph, analysis):
|
52
|
+
result = self._try_solver(solver, equations, variables, dependency_graph, max_iterations, tolerance)
|
53
|
+
if result.success:
|
54
|
+
return result
|
55
|
+
|
56
|
+
# No solver could handle the problem
|
57
|
+
return SolveResult(variables=variables, steps=[], success=False, message="No solver could handle this problem", method="NoSolver")
|
58
|
+
|
59
|
+
def add_solver(self, solver: BaseSolver):
|
60
|
+
"""Add a custom solver to the manager."""
|
61
|
+
self.solvers.insert(0, solver) # Add to beginning for highest priority
|
62
|
+
|
63
|
+
def get_available_solvers(self) -> list[str]:
|
64
|
+
"""Get list of available solver names."""
|
65
|
+
return [solver.__class__.__name__ for solver in self.solvers]
|
66
|
+
|
67
|
+
def _try_solver(self, solver: BaseSolver, equations: list[Equation], variables: dict[str, FieldQnty], dependency_graph: Order | None, max_iterations: int, tolerance: float) -> SolveResult:
|
68
|
+
"""
|
69
|
+
Try a specific solver and log results appropriately.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
solver: The solver to try
|
73
|
+
equations: List of equations to solve
|
74
|
+
variables: Dictionary of variables
|
75
|
+
dependency_graph: Optional dependency graph
|
76
|
+
max_iterations: Maximum iterations
|
77
|
+
tolerance: Convergence tolerance
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
SolveResult from the attempted solver
|
81
|
+
"""
|
82
|
+
solver_name = solver.__class__.__name__
|
83
|
+
|
84
|
+
if self.logger:
|
85
|
+
self.logger.debug(f"Using {solver_name} for solving")
|
86
|
+
|
87
|
+
result = solver.solve(equations, variables, dependency_graph, max_iterations, tolerance)
|
88
|
+
|
89
|
+
if not result.success and self.logger:
|
90
|
+
# Use debug level for expected fallback from SimultaneousEquationSolver
|
91
|
+
if solver_name == "SimultaneousEquationSolver":
|
92
|
+
self.logger.debug(f"{solver_name} failed: {result.message}")
|
93
|
+
else:
|
94
|
+
self.logger.warning(f"{solver_name} failed: {result.message}")
|
95
|
+
|
96
|
+
return result
|
qnty/solving/order.py
ADDED
@@ -0,0 +1,403 @@
|
|
1
|
+
from collections import defaultdict, deque
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from ..equations import Equation
|
5
|
+
|
6
|
+
|
7
|
+
class Order:
|
8
|
+
"""
|
9
|
+
Manages dependencies between variables in a system of equations.
|
10
|
+
Uses topological sorting to determine the correct solving order.
|
11
|
+
"""
|
12
|
+
|
13
|
+
def __init__(self):
|
14
|
+
# Graph structure: dependency_source -> [dependent_variables]
|
15
|
+
self.graph = defaultdict(list)
|
16
|
+
# Count of dependencies for each variable
|
17
|
+
self.in_degree = defaultdict(int)
|
18
|
+
# All variables in the system
|
19
|
+
self.variables = set()
|
20
|
+
# Equations that can solve for each variable
|
21
|
+
self.solvers = defaultdict(list) # variable -> [equations that can solve it]
|
22
|
+
|
23
|
+
def add_equation(self, equation: Equation, known_vars: set[str]):
|
24
|
+
"""Add an equation to the dependency graph."""
|
25
|
+
eq_vars = equation.get_all_variables()
|
26
|
+
unknown_vars = equation.get_unknown_variables(known_vars)
|
27
|
+
|
28
|
+
# Update variables set
|
29
|
+
self.variables.update(eq_vars)
|
30
|
+
|
31
|
+
# Analyze equation structure to determine dependencies and solvers
|
32
|
+
lhs_vars = self._extract_variables_from_side(equation.lhs)
|
33
|
+
rhs_vars = self._extract_variables_from_side(equation.rhs)
|
34
|
+
|
35
|
+
# Handle different equation patterns
|
36
|
+
self._process_equation_dependencies(equation, lhs_vars, rhs_vars, unknown_vars, eq_vars, known_vars)
|
37
|
+
|
38
|
+
def add_dependency(self, dependency_source: str, dependent_variable: str):
|
39
|
+
"""
|
40
|
+
Add a dependency: dependent_variable depends on dependency_source.
|
41
|
+
This means dependency_source must be solved before dependent_variable.
|
42
|
+
"""
|
43
|
+
if dependent_variable != dependency_source: # Avoid self-dependencies
|
44
|
+
# Add to graph
|
45
|
+
if dependent_variable not in self.graph[dependency_source]:
|
46
|
+
self.graph[dependency_source].append(dependent_variable)
|
47
|
+
self.in_degree[dependent_variable] += 1
|
48
|
+
|
49
|
+
# Ensure both variables are tracked
|
50
|
+
self.variables.add(dependency_source)
|
51
|
+
self.variables.add(dependent_variable)
|
52
|
+
|
53
|
+
def remove_dependency(self, dependency_source: str, dependent_variable: str):
|
54
|
+
"""Remove a dependency between variables."""
|
55
|
+
if dependent_variable in self.graph[dependency_source]:
|
56
|
+
self.graph[dependency_source].remove(dependent_variable)
|
57
|
+
self.in_degree[dependent_variable] -= 1
|
58
|
+
|
59
|
+
def get_solving_order(self, known_vars: set[str]) -> list[str]:
|
60
|
+
"""
|
61
|
+
Get the order in which variables should be solved using topological sort.
|
62
|
+
Returns list of variables in solving order.
|
63
|
+
"""
|
64
|
+
# Create a copy of in_degree for this computation
|
65
|
+
temp_in_degree = self.in_degree.copy()
|
66
|
+
temp_graph = defaultdict(list)
|
67
|
+
|
68
|
+
# Initialize temp_graph with copies, ensuring all variables have entries
|
69
|
+
for var in self.variables:
|
70
|
+
temp_graph[var] = self.graph[var].copy() if var in self.graph else []
|
71
|
+
|
72
|
+
# Initialize queue with variables that have no dependencies (already known)
|
73
|
+
queue = deque()
|
74
|
+
|
75
|
+
# Add known variables to queue first
|
76
|
+
for var in known_vars:
|
77
|
+
if var in self.variables:
|
78
|
+
queue.append(var)
|
79
|
+
|
80
|
+
# Add variables with no remaining dependencies AND have solver equations
|
81
|
+
for var in self.variables:
|
82
|
+
if var not in known_vars and temp_in_degree[var] == 0 and var in self.solvers:
|
83
|
+
queue.append(var)
|
84
|
+
|
85
|
+
solving_order = []
|
86
|
+
|
87
|
+
while queue:
|
88
|
+
current_var = queue.popleft()
|
89
|
+
solving_order.append(current_var)
|
90
|
+
|
91
|
+
# Remove this variable's influence on dependent variables
|
92
|
+
if current_var in temp_graph:
|
93
|
+
for dependent_var in temp_graph[current_var]:
|
94
|
+
temp_in_degree[dependent_var] -= 1
|
95
|
+
|
96
|
+
# If dependent variable has no more dependencies AND has solvers, add it to queue
|
97
|
+
if temp_in_degree[dependent_var] == 0 and dependent_var in self.solvers:
|
98
|
+
queue.append(dependent_var)
|
99
|
+
|
100
|
+
# Filter out known variables from the result, as they don't need solving
|
101
|
+
result = [var for var in solving_order if var not in known_vars]
|
102
|
+
|
103
|
+
return result
|
104
|
+
|
105
|
+
def detect_cycles(self) -> list[list[str]]:
|
106
|
+
"""
|
107
|
+
Detect cycles in the dependency graph.
|
108
|
+
Returns list of cycles (each cycle is a list of variables).
|
109
|
+
"""
|
110
|
+
WHITE, GRAY, BLACK = 0, 1, 2
|
111
|
+
color = defaultdict(int)
|
112
|
+
cycles = []
|
113
|
+
current_path = []
|
114
|
+
|
115
|
+
def dfs_visit(node: str) -> bool:
|
116
|
+
"""DFS visit with cycle detection. Returns True if cycle found."""
|
117
|
+
if color[node] == GRAY:
|
118
|
+
# Found a back edge - cycle detected
|
119
|
+
cycle_start = current_path.index(node)
|
120
|
+
cycle = current_path[cycle_start:] + [node]
|
121
|
+
cycles.append(cycle)
|
122
|
+
return True
|
123
|
+
|
124
|
+
if color[node] == BLACK:
|
125
|
+
return False
|
126
|
+
|
127
|
+
# Mark as being processed
|
128
|
+
color[node] = GRAY
|
129
|
+
current_path.append(node)
|
130
|
+
|
131
|
+
# Visit neighbors
|
132
|
+
for neighbor in self.graph[node]:
|
133
|
+
if dfs_visit(neighbor):
|
134
|
+
return True
|
135
|
+
|
136
|
+
# Mark as completely processed
|
137
|
+
color[node] = BLACK
|
138
|
+
current_path.pop()
|
139
|
+
return False
|
140
|
+
|
141
|
+
# Check all variables
|
142
|
+
for var in self.variables:
|
143
|
+
if color[var] == WHITE:
|
144
|
+
dfs_visit(var)
|
145
|
+
|
146
|
+
return cycles
|
147
|
+
|
148
|
+
def can_solve_system(self, known_vars: set[str]) -> tuple[bool, list[str]]:
|
149
|
+
"""
|
150
|
+
Check if the system can be completely solved given known variables.
|
151
|
+
Returns (can_solve, unsolvable_variables).
|
152
|
+
"""
|
153
|
+
all_unknown = self.variables - known_vars
|
154
|
+
|
155
|
+
# Find variables with no solver equations
|
156
|
+
truly_unsolvable = self._find_truly_unsolvable_variables(all_unknown)
|
157
|
+
|
158
|
+
# Check equation-to-variable ratio
|
159
|
+
variables_with_solvers = all_unknown - set(truly_unsolvable)
|
160
|
+
unique_equations = self._get_unique_equations(variables_with_solvers)
|
161
|
+
|
162
|
+
# Simple heuristic: need at least as many equations as unknowns
|
163
|
+
can_solve_completely = len(unique_equations) >= len(variables_with_solvers) and len(truly_unsolvable) == 0
|
164
|
+
|
165
|
+
if can_solve_completely:
|
166
|
+
return True, []
|
167
|
+
|
168
|
+
# Find all unsolvable variables
|
169
|
+
solving_order = self.get_solving_order(known_vars)
|
170
|
+
solvable = set(solving_order)
|
171
|
+
conditional_unsolvable = all_unknown - solvable
|
172
|
+
unsolvable = list(set(truly_unsolvable) | conditional_unsolvable)
|
173
|
+
|
174
|
+
return False, unsolvable
|
175
|
+
|
176
|
+
def get_solvable_variables(self, known_vars: set[str]) -> list[str]:
|
177
|
+
"""Get variables that can be solved in the next iteration."""
|
178
|
+
solvable = []
|
179
|
+
|
180
|
+
for var in self.variables:
|
181
|
+
if var not in known_vars:
|
182
|
+
# Check if all dependencies of this variable are known
|
183
|
+
dependencies_known = True
|
184
|
+
for dep_source in self.graph:
|
185
|
+
if var in self.graph[dep_source] and dep_source not in known_vars:
|
186
|
+
dependencies_known = False
|
187
|
+
break
|
188
|
+
|
189
|
+
if dependencies_known and var in self.solvers:
|
190
|
+
solvable.append(var)
|
191
|
+
|
192
|
+
return solvable
|
193
|
+
|
194
|
+
def get_equation_for_variable(self, var: str, known_vars: set[str]) -> Equation | None:
|
195
|
+
"""Get an equation that can solve for the given variable."""
|
196
|
+
if var not in self.solvers:
|
197
|
+
return None
|
198
|
+
|
199
|
+
# Find the first equation that can solve for this variable
|
200
|
+
for equation in self.solvers[var]:
|
201
|
+
if equation.can_solve_for(var, known_vars):
|
202
|
+
return equation
|
203
|
+
|
204
|
+
return None
|
205
|
+
|
206
|
+
def get_strongly_connected_components(self) -> list[set[str]]:
|
207
|
+
"""
|
208
|
+
Find strongly connected components in the dependency graph.
|
209
|
+
Variables in the same SCC must be solved simultaneously.
|
210
|
+
"""
|
211
|
+
# Tarjan's algorithm for finding SCCs
|
212
|
+
index_counter = [0]
|
213
|
+
stack = []
|
214
|
+
lowlinks = {}
|
215
|
+
index = {}
|
216
|
+
on_stack = {}
|
217
|
+
components = []
|
218
|
+
|
219
|
+
def strongconnect(node: str):
|
220
|
+
index[node] = index_counter[0]
|
221
|
+
lowlinks[node] = index_counter[0]
|
222
|
+
index_counter[0] += 1
|
223
|
+
stack.append(node)
|
224
|
+
on_stack[node] = True
|
225
|
+
|
226
|
+
for neighbor in self.graph[node]:
|
227
|
+
if neighbor not in index:
|
228
|
+
strongconnect(neighbor)
|
229
|
+
lowlinks[node] = min(lowlinks[node], lowlinks[neighbor])
|
230
|
+
elif on_stack[neighbor]:
|
231
|
+
lowlinks[node] = min(lowlinks[node], index[neighbor])
|
232
|
+
|
233
|
+
if lowlinks[node] == index[node]:
|
234
|
+
component = set()
|
235
|
+
while True:
|
236
|
+
w = stack.pop()
|
237
|
+
on_stack[w] = False
|
238
|
+
component.add(w)
|
239
|
+
if w == node:
|
240
|
+
break
|
241
|
+
components.append(component)
|
242
|
+
|
243
|
+
for node in self.variables:
|
244
|
+
if node not in index:
|
245
|
+
strongconnect(node)
|
246
|
+
|
247
|
+
# Filter out single-node components (unless they have self-loops)
|
248
|
+
significant_components = []
|
249
|
+
for component in components:
|
250
|
+
if len(component) > 1:
|
251
|
+
significant_components.append(component)
|
252
|
+
elif len(component) == 1:
|
253
|
+
node = next(iter(component))
|
254
|
+
if node in self.graph[node]: # Self-loop
|
255
|
+
significant_components.append(component)
|
256
|
+
|
257
|
+
return significant_components
|
258
|
+
|
259
|
+
def analyze_system(self, known_vars: set[str]) -> dict[str, Any]:
|
260
|
+
"""
|
261
|
+
Perform comprehensive analysis of the equation system.
|
262
|
+
Returns analysis results including cycles, SCCs, solvability, etc.
|
263
|
+
"""
|
264
|
+
analysis = {}
|
265
|
+
|
266
|
+
# Basic info
|
267
|
+
analysis["total_variables"] = len(self.variables)
|
268
|
+
analysis["known_variables"] = len(known_vars)
|
269
|
+
analysis["unknown_variables"] = len(self.variables - known_vars)
|
270
|
+
|
271
|
+
# Solving order
|
272
|
+
analysis["solving_order"] = self.get_solving_order(known_vars)
|
273
|
+
|
274
|
+
# Solvability
|
275
|
+
can_solve, unsolvable = self.can_solve_system(known_vars)
|
276
|
+
analysis["can_solve_completely"] = can_solve
|
277
|
+
analysis["unsolvable_variables"] = unsolvable
|
278
|
+
|
279
|
+
# Cycles and SCCs
|
280
|
+
analysis["cycles"] = self.detect_cycles()
|
281
|
+
analysis["strongly_connected_components"] = self.get_strongly_connected_components()
|
282
|
+
analysis["has_cycles"] = len(analysis["cycles"]) > 0
|
283
|
+
|
284
|
+
# Next solvable variables
|
285
|
+
analysis["immediately_solvable"] = self.get_solvable_variables(known_vars)
|
286
|
+
|
287
|
+
return analysis
|
288
|
+
|
289
|
+
def _extract_variables_from_side(self, side: Any) -> set[str]:
|
290
|
+
"""
|
291
|
+
Extract variables from either left or right side of an equation.
|
292
|
+
|
293
|
+
Args:
|
294
|
+
side: The equation side (Variable or Expression)
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
Set of variable names found in this side
|
298
|
+
"""
|
299
|
+
# Check if it's a Variable with a symbol attribute
|
300
|
+
if hasattr(side, "symbol") and hasattr(side, "name"):
|
301
|
+
return {str(side.symbol) if side.symbol else str(side.name)}
|
302
|
+
# Check if it's an Expression with get_variables method
|
303
|
+
elif hasattr(side, "get_variables") and callable(side.get_variables):
|
304
|
+
return side.get_variables() # type: ignore[return-value]
|
305
|
+
else:
|
306
|
+
return set()
|
307
|
+
|
308
|
+
def _process_equation_dependencies(self, equation: Equation, lhs_vars: set[str], rhs_vars: set[str], unknown_vars: set[str], eq_vars: set[str], known_vars: set[str]):
|
309
|
+
"""
|
310
|
+
Process dependencies and solvers for an equation based on its structure.
|
311
|
+
|
312
|
+
Args:
|
313
|
+
equation: The equation to process
|
314
|
+
lhs_vars: Variables on left-hand side
|
315
|
+
rhs_vars: Variables on right-hand side
|
316
|
+
unknown_vars: Unknown variables in the equation
|
317
|
+
eq_vars: All variables in the equation
|
318
|
+
known_vars: Set of known variables
|
319
|
+
"""
|
320
|
+
# If LHS is a single variable, it depends on all variables in RHS
|
321
|
+
if len(lhs_vars) == 1:
|
322
|
+
lhs_var = next(iter(lhs_vars))
|
323
|
+
if lhs_var in unknown_vars:
|
324
|
+
self.solvers[lhs_var].append(equation)
|
325
|
+
# Add dependencies: LHS variable depends on all RHS variables
|
326
|
+
for rhs_var in rhs_vars:
|
327
|
+
if rhs_var != lhs_var:
|
328
|
+
self.add_dependency(rhs_var, lhs_var)
|
329
|
+
|
330
|
+
# If RHS is a single variable, it depends on all variables in LHS
|
331
|
+
elif len(rhs_vars) == 1:
|
332
|
+
rhs_var = next(iter(rhs_vars))
|
333
|
+
if rhs_var in unknown_vars:
|
334
|
+
self.solvers[rhs_var].append(equation)
|
335
|
+
# Add dependencies: RHS variable depends on all LHS variables
|
336
|
+
for lhs_var in lhs_vars:
|
337
|
+
if lhs_var != rhs_var:
|
338
|
+
self.add_dependency(lhs_var, rhs_var)
|
339
|
+
|
340
|
+
# For more complex cases, use can_solve_for check
|
341
|
+
else:
|
342
|
+
for unknown_var in unknown_vars:
|
343
|
+
if equation.can_solve_for(unknown_var, known_vars):
|
344
|
+
self.solvers[unknown_var].append(equation)
|
345
|
+
# Add dependencies: unknown_var depends on all other variables in equation
|
346
|
+
for other_var in eq_vars:
|
347
|
+
if other_var != unknown_var:
|
348
|
+
self.add_dependency(other_var, unknown_var)
|
349
|
+
|
350
|
+
def _find_truly_unsolvable_variables(self, all_unknown: set[str]) -> list[str]:
|
351
|
+
"""
|
352
|
+
Find variables that have no solver equations.
|
353
|
+
|
354
|
+
Args:
|
355
|
+
all_unknown: Set of all unknown variables
|
356
|
+
|
357
|
+
Returns:
|
358
|
+
List of variables with no solver equations
|
359
|
+
"""
|
360
|
+
truly_unsolvable = []
|
361
|
+
for var in all_unknown:
|
362
|
+
if var not in self.solvers or len(self.solvers[var]) == 0:
|
363
|
+
truly_unsolvable.append(var)
|
364
|
+
return truly_unsolvable
|
365
|
+
|
366
|
+
def _get_unique_equations(self, variables_with_solvers: set[str]) -> set[Equation]:
|
367
|
+
"""
|
368
|
+
Get unique equations that can solve variables.
|
369
|
+
|
370
|
+
Args:
|
371
|
+
variables_with_solvers: Variables that have solver equations
|
372
|
+
|
373
|
+
Returns:
|
374
|
+
Set of unique equations
|
375
|
+
"""
|
376
|
+
unique_equations = set()
|
377
|
+
for var in variables_with_solvers:
|
378
|
+
if var in self.solvers and self.solvers[var]:
|
379
|
+
unique_equations.add(self.solvers[var][0])
|
380
|
+
return unique_equations
|
381
|
+
|
382
|
+
def visualize_dependencies(self) -> str:
|
383
|
+
"""Create a text representation of the dependency graph."""
|
384
|
+
lines = ["Dependency Graph:"]
|
385
|
+
lines.append("=" * 20)
|
386
|
+
|
387
|
+
for source_var in sorted(self.graph.keys()):
|
388
|
+
if self.graph[source_var]:
|
389
|
+
dependents = ", ".join(sorted(self.graph[source_var]))
|
390
|
+
lines.append(f"{source_var} -> [{dependents}]")
|
391
|
+
|
392
|
+
lines.append("")
|
393
|
+
lines.append("In-degrees:")
|
394
|
+
for var in sorted(self.variables):
|
395
|
+
lines.append(f"{var}: {self.in_degree[var]}")
|
396
|
+
|
397
|
+
return "\n".join(lines)
|
398
|
+
|
399
|
+
def __str__(self) -> str:
|
400
|
+
return f"DependencyGraph(variables={len(self.variables)}, equations={len(self.solvers)})"
|
401
|
+
|
402
|
+
def __repr__(self) -> str:
|
403
|
+
return self.__str__()
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""
|
2
|
+
Solver package for the Optinova engineering problem library.
|
3
|
+
|
4
|
+
This package contains different solver implementations for solving systems
|
5
|
+
of engineering equations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from ..manager import SolverManager
|
9
|
+
from .base import BaseSolver, SolveError, SolveResult
|
10
|
+
from .iterative import IterativeSolver
|
11
|
+
from .simultaneous import SimultaneousEquationSolver
|
12
|
+
|
13
|
+
__all__ = ["BaseSolver", "SolveResult", "SolveError", "IterativeSolver", "SimultaneousEquationSolver", "SolverManager"]
|
@@ -0,0 +1,82 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from abc import ABC, abstractmethod
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
from ...equations import Equation
|
9
|
+
from ...quantities import FieldQnty
|
10
|
+
from ..order import Order
|
11
|
+
|
12
|
+
|
13
|
+
class SolveError(Exception):
|
14
|
+
"""Exception raised when solving fails."""
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class SolveResult:
|
19
|
+
"""Result of a solve operation."""
|
20
|
+
|
21
|
+
variables: dict[str, FieldQnty]
|
22
|
+
steps: list[dict[str, Any]] = field(default_factory=list)
|
23
|
+
success: bool = True
|
24
|
+
message: str = ""
|
25
|
+
method: str = ""
|
26
|
+
iterations: int = 0
|
27
|
+
|
28
|
+
|
29
|
+
class BaseSolver(ABC):
|
30
|
+
"""Base class for all equation solvers."""
|
31
|
+
|
32
|
+
def __init__(self, logger: logging.Logger | None = None):
|
33
|
+
self.logger = logger
|
34
|
+
self.steps: list[dict[str, Any]] = []
|
35
|
+
|
36
|
+
@abstractmethod
|
37
|
+
def can_handle(self, equations: list[Equation], unknowns: set[str], dependency_graph: Order | None = None, analysis: dict[str, Any] | None = None) -> bool:
|
38
|
+
"""
|
39
|
+
Check if this solver can handle the given problem.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
equations: List of equations to solve
|
43
|
+
unknowns: Set of unknown variable symbols
|
44
|
+
dependency_graph: Optional dependency graph for analysis
|
45
|
+
analysis: Optional system analysis results
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
True if this solver can handle the problem
|
49
|
+
"""
|
50
|
+
...
|
51
|
+
|
52
|
+
@abstractmethod
|
53
|
+
def solve(self, equations: list[Equation], variables: dict[str, FieldQnty], dependency_graph: Order | None = None, max_iterations: int = 100, tolerance: float = 1e-10) -> SolveResult:
|
54
|
+
"""
|
55
|
+
Solve the system of equations.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
equations: List of equations to solve
|
59
|
+
variables: Dictionary of all variables (known and unknown)
|
60
|
+
dependency_graph: Optional dependency graph
|
61
|
+
max_iterations: Maximum number of iterations
|
62
|
+
tolerance: Convergence tolerance
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
SolveResult containing the solution
|
66
|
+
"""
|
67
|
+
...
|
68
|
+
|
69
|
+
def _log_step(self, iteration: int, variable: str, equation: str, result: str, method: str | None = None):
|
70
|
+
"""Log a solving step."""
|
71
|
+
step = {"iteration": iteration, "variable": variable, "equation": equation, "result": result, "method": method or self.__class__.__name__}
|
72
|
+
self.steps.append(step)
|
73
|
+
if self.logger:
|
74
|
+
self.logger.debug("Solved %s = %s", variable, result)
|
75
|
+
|
76
|
+
def _get_known_variables(self, variables: dict[str, FieldQnty]) -> set[str]:
|
77
|
+
"""Get symbols of known variables."""
|
78
|
+
return {s for s, v in variables.items() if v.is_known}
|
79
|
+
|
80
|
+
def _get_unknown_variables(self, variables: dict[str, FieldQnty]) -> set[str]:
|
81
|
+
"""Get symbols of unknown variables."""
|
82
|
+
return {s for s, v in variables.items() if not v.is_known}
|