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.
Files changed (74) hide show
  1. qnty/__init__.py +140 -59
  2. qnty/constants/__init__.py +10 -0
  3. qnty/constants/numerical.py +18 -0
  4. qnty/constants/solvers.py +6 -0
  5. qnty/constants/tests.py +6 -0
  6. qnty/dimensions/__init__.py +23 -0
  7. qnty/dimensions/base.py +97 -0
  8. qnty/dimensions/field_dims.py +126 -0
  9. qnty/dimensions/field_dims.pyi +128 -0
  10. qnty/dimensions/signature.py +111 -0
  11. qnty/equations/__init__.py +4 -0
  12. qnty/equations/equation.py +220 -0
  13. qnty/equations/system.py +130 -0
  14. qnty/expressions/__init__.py +40 -0
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +74 -0
  17. qnty/expressions/nodes.py +701 -0
  18. qnty/expressions/types.py +70 -0
  19. qnty/extensions/plotting/__init__.py +0 -0
  20. qnty/extensions/reporting/__init__.py +0 -0
  21. qnty/problems/__init__.py +145 -0
  22. qnty/problems/composition.py +1031 -0
  23. qnty/problems/problem.py +695 -0
  24. qnty/problems/rules.py +145 -0
  25. qnty/problems/solving.py +1216 -0
  26. qnty/problems/validation.py +127 -0
  27. qnty/quantities/__init__.py +29 -0
  28. qnty/quantities/base_qnty.py +677 -0
  29. qnty/quantities/field_converters.py +24004 -0
  30. qnty/quantities/field_qnty.py +1012 -0
  31. qnty/quantities/field_setter.py +12320 -0
  32. qnty/quantities/field_vars.py +6325 -0
  33. qnty/quantities/field_vars.pyi +4191 -0
  34. qnty/solving/__init__.py +0 -0
  35. qnty/solving/manager.py +96 -0
  36. qnty/solving/order.py +403 -0
  37. qnty/solving/solvers/__init__.py +13 -0
  38. qnty/solving/solvers/base.py +82 -0
  39. qnty/solving/solvers/iterative.py +165 -0
  40. qnty/solving/solvers/simultaneous.py +475 -0
  41. qnty/units/__init__.py +1 -0
  42. qnty/units/field_units.py +10507 -0
  43. qnty/units/field_units.pyi +2461 -0
  44. qnty/units/prefixes.py +203 -0
  45. qnty/{unit.py → units/registry.py} +89 -61
  46. qnty/utils/__init__.py +16 -0
  47. qnty/utils/caching/__init__.py +23 -0
  48. qnty/utils/caching/manager.py +401 -0
  49. qnty/utils/error_handling/__init__.py +66 -0
  50. qnty/utils/error_handling/context.py +39 -0
  51. qnty/utils/error_handling/exceptions.py +96 -0
  52. qnty/utils/error_handling/handlers.py +171 -0
  53. qnty/utils/logging.py +40 -0
  54. qnty/utils/protocols.py +164 -0
  55. qnty/utils/scope_discovery.py +420 -0
  56. qnty-0.1.0.dist-info/METADATA +199 -0
  57. qnty-0.1.0.dist-info/RECORD +60 -0
  58. qnty/dimension.py +0 -186
  59. qnty/equation.py +0 -297
  60. qnty/expression.py +0 -553
  61. qnty/prefixes.py +0 -229
  62. qnty/unit_types/base.py +0 -47
  63. qnty/units.py +0 -8113
  64. qnty/variable.py +0 -300
  65. qnty/variable_types/base.py +0 -58
  66. qnty/variable_types/expression_variable.py +0 -106
  67. qnty/variable_types/typed_variable.py +0 -87
  68. qnty/variables.py +0 -2298
  69. qnty/variables.pyi +0 -6148
  70. qnty-0.0.8.dist-info/METADATA +0 -355
  71. qnty-0.0.8.dist-info/RECORD +0 -19
  72. /qnty/{unit_types → extensions}/__init__.py +0 -0
  73. /qnty/{variable_types → extensions/integration}/__init__.py +0 -0
  74. {qnty-0.0.8.dist-info → qnty-0.1.0.dist-info}/WHEEL +0 -0
File without changes
@@ -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}