qnty 0.0.9__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qnty/__init__.py +2 -3
- qnty/constants/__init__.py +10 -0
- qnty/constants/numerical.py +18 -0
- qnty/constants/solvers.py +6 -0
- qnty/constants/tests.py +6 -0
- qnty/dimensions/__init__.py +23 -0
- qnty/dimensions/base.py +97 -0
- qnty/dimensions/field_dims.py +126 -0
- qnty/dimensions/field_dims.pyi +128 -0
- qnty/dimensions/signature.py +111 -0
- qnty/equations/__init__.py +1 -1
- qnty/equations/equation.py +118 -155
- qnty/equations/system.py +68 -65
- qnty/expressions/__init__.py +25 -46
- qnty/expressions/formatter.py +188 -0
- qnty/expressions/functions.py +46 -68
- qnty/expressions/nodes.py +539 -384
- qnty/expressions/types.py +70 -0
- qnty/problems/__init__.py +145 -0
- qnty/problems/composition.py +1031 -0
- qnty/problems/problem.py +695 -0
- qnty/problems/rules.py +145 -0
- qnty/problems/solving.py +1216 -0
- qnty/problems/validation.py +127 -0
- qnty/quantities/__init__.py +28 -5
- qnty/quantities/base_qnty.py +677 -0
- qnty/quantities/field_converters.py +24004 -0
- qnty/quantities/field_qnty.py +1012 -0
- qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
- qnty/{generated/quantities.py → quantities/field_vars.py} +754 -432
- qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
- qnty/solving/manager.py +50 -44
- qnty/solving/order.py +181 -133
- qnty/solving/solvers/__init__.py +2 -9
- qnty/solving/solvers/base.py +27 -37
- qnty/solving/solvers/iterative.py +115 -135
- qnty/solving/solvers/simultaneous.py +93 -165
- qnty/units/__init__.py +1 -0
- qnty/{generated/units.py → units/field_units.py} +1700 -991
- qnty/units/field_units.pyi +2461 -0
- qnty/units/prefixes.py +58 -105
- qnty/units/registry.py +76 -89
- qnty/utils/__init__.py +16 -0
- qnty/utils/caching/__init__.py +23 -0
- qnty/utils/caching/manager.py +401 -0
- qnty/utils/error_handling/__init__.py +66 -0
- qnty/utils/error_handling/context.py +39 -0
- qnty/utils/error_handling/exceptions.py +96 -0
- qnty/utils/error_handling/handlers.py +171 -0
- qnty/utils/logging.py +4 -4
- qnty/utils/protocols.py +164 -0
- qnty/utils/scope_discovery.py +420 -0
- {qnty-0.0.9.dist-info → qnty-0.1.0.dist-info}/METADATA +1 -1
- qnty-0.1.0.dist-info/RECORD +60 -0
- qnty/_backup/problem_original.py +0 -1251
- qnty/_backup/quantity.py +0 -63
- qnty/codegen/cli.py +0 -125
- qnty/codegen/generators/data/unit_data.json +0 -8807
- qnty/codegen/generators/data_processor.py +0 -345
- qnty/codegen/generators/dimensions_gen.py +0 -434
- qnty/codegen/generators/doc_generator.py +0 -141
- qnty/codegen/generators/out/dimension_mapping.json +0 -974
- qnty/codegen/generators/out/dimension_metadata.json +0 -123
- qnty/codegen/generators/out/units_metadata.json +0 -223
- qnty/codegen/generators/quantities_gen.py +0 -159
- qnty/codegen/generators/setters_gen.py +0 -178
- qnty/codegen/generators/stubs_gen.py +0 -167
- qnty/codegen/generators/units_gen.py +0 -295
- qnty/expressions/cache.py +0 -94
- qnty/generated/dimensions.py +0 -514
- qnty/problem/__init__.py +0 -91
- qnty/problem/base.py +0 -142
- qnty/problem/composition.py +0 -385
- qnty/problem/composition_mixin.py +0 -382
- qnty/problem/equations.py +0 -413
- qnty/problem/metaclass.py +0 -302
- qnty/problem/reconstruction.py +0 -1016
- qnty/problem/solving.py +0 -180
- qnty/problem/validation.py +0 -64
- qnty/problem/variables.py +0 -239
- qnty/quantities/expression_quantity.py +0 -314
- qnty/quantities/quantity.py +0 -428
- qnty/quantities/typed_quantity.py +0 -215
- qnty/validation/__init__.py +0 -0
- qnty/validation/registry.py +0 -0
- qnty/validation/rules.py +0 -167
- qnty-0.0.9.dist-info/RECORD +0 -63
- /qnty/{codegen → extensions}/__init__.py +0 -0
- /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
- /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
- /qnty/{generated → extensions/reporting}/__init__.py +0 -0
- {qnty-0.0.9.dist-info → qnty-0.1.0.dist-info}/WHEEL +0 -0
qnty/solving/manager.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
import logging
|
1
2
|
|
2
|
-
from qnty.equations.equation import Equation
|
3
|
-
from qnty.quantities import TypeSafeVariable as Variable
|
4
3
|
from qnty.solving.order import Order
|
5
4
|
|
5
|
+
from ..equations import Equation
|
6
|
+
from ..quantities import FieldQnty
|
6
7
|
from .solvers.base import BaseSolver, SolveResult
|
7
8
|
from .solvers.iterative import IterativeSolver
|
8
9
|
from .solvers.simultaneous import SimultaneousEquationSolver
|
@@ -12,79 +13,84 @@ class SolverManager:
|
|
12
13
|
"""
|
13
14
|
Manages multiple solvers and selects the best one for a given problem.
|
14
15
|
"""
|
15
|
-
|
16
|
-
def __init__(self, logger=None):
|
16
|
+
|
17
|
+
def __init__(self, logger: logging.Logger | None = None):
|
17
18
|
self.logger = logger
|
18
19
|
self.solvers = [
|
19
20
|
SimultaneousEquationSolver(logger), # Try simultaneous first for cyclic systems
|
20
|
-
IterativeSolver(logger),
|
21
|
+
IterativeSolver(logger), # Fall back to iterative
|
21
22
|
]
|
22
|
-
|
23
|
-
def solve(self, equations: list[Equation], variables: dict[str,
|
24
|
-
dependency_graph: Order | None = None,
|
25
|
-
max_iterations: int = 100, tolerance: float = 1e-10) -> SolveResult:
|
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:
|
26
25
|
"""
|
27
26
|
Solve the system using the best available solver.
|
28
|
-
|
27
|
+
|
29
28
|
Args:
|
30
29
|
equations: List of equations to solve
|
31
30
|
variables: Dictionary of all variables (known and unknown)
|
32
31
|
dependency_graph: Optional dependency graph
|
33
32
|
max_iterations: Maximum number of iterations
|
34
33
|
tolerance: Convergence tolerance
|
35
|
-
|
34
|
+
|
36
35
|
Returns:
|
37
36
|
SolveResult containing the solution
|
38
37
|
"""
|
39
38
|
unknowns = {s for s, v in variables.items() if not v.is_known}
|
40
|
-
|
39
|
+
|
41
40
|
if not unknowns:
|
42
|
-
return SolveResult(
|
43
|
-
|
44
|
-
steps=[],
|
45
|
-
success=True,
|
46
|
-
message="No unknowns to solve",
|
47
|
-
method="NoSolver"
|
48
|
-
)
|
49
|
-
|
41
|
+
return SolveResult(variables=variables, steps=[], success=True, message="No unknowns to solve", method="NoSolver")
|
42
|
+
|
50
43
|
# Get system analysis if we have a dependency graph
|
51
44
|
analysis = None
|
52
45
|
if dependency_graph:
|
53
46
|
known_vars = {s for s, v in variables.items() if v.is_known}
|
54
47
|
analysis = dependency_graph.analyze_system(known_vars)
|
55
|
-
|
48
|
+
|
56
49
|
# Try each solver in order of preference
|
57
50
|
for solver in self.solvers:
|
58
51
|
if solver.can_handle(equations, unknowns, dependency_graph, analysis):
|
59
|
-
|
60
|
-
self.logger.debug(f"Using {solver.__class__.__name__} for solving")
|
61
|
-
|
62
|
-
result = solver.solve(equations, variables, dependency_graph,
|
63
|
-
max_iterations, tolerance)
|
64
|
-
|
52
|
+
result = self._try_solver(solver, equations, variables, dependency_graph, max_iterations, tolerance)
|
65
53
|
if result.success:
|
66
54
|
return result
|
67
|
-
|
68
|
-
if self.logger:
|
69
|
-
# Use debug level for expected fallback from SimultaneousEquationSolver
|
70
|
-
if solver.__class__.__name__ == "SimultaneousEquationSolver":
|
71
|
-
self.logger.debug(f"{solver.__class__.__name__} failed: {result.message}")
|
72
|
-
else:
|
73
|
-
self.logger.warning(f"{solver.__class__.__name__} failed: {result.message}")
|
74
|
-
|
55
|
+
|
75
56
|
# No solver could handle the problem
|
76
|
-
return SolveResult(
|
77
|
-
|
78
|
-
steps=[],
|
79
|
-
success=False,
|
80
|
-
message="No solver could handle this problem",
|
81
|
-
method="NoSolver"
|
82
|
-
)
|
83
|
-
|
57
|
+
return SolveResult(variables=variables, steps=[], success=False, message="No solver could handle this problem", method="NoSolver")
|
58
|
+
|
84
59
|
def add_solver(self, solver: BaseSolver):
|
85
60
|
"""Add a custom solver to the manager."""
|
86
61
|
self.solvers.insert(0, solver) # Add to beginning for highest priority
|
87
|
-
|
62
|
+
|
88
63
|
def get_available_solvers(self) -> list[str]:
|
89
64
|
"""Get list of available solver names."""
|
90
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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
from collections import defaultdict, deque
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
-
from
|
4
|
+
from ..equations import Equation
|
5
5
|
|
6
6
|
|
7
7
|
class Order:
|
@@ -9,7 +9,7 @@ class Order:
|
|
9
9
|
Manages dependencies between variables in a system of equations.
|
10
10
|
Uses topological sorting to determine the correct solving order.
|
11
11
|
"""
|
12
|
-
|
12
|
+
|
13
13
|
def __init__(self):
|
14
14
|
# Graph structure: dependency_source -> [dependent_variables]
|
15
15
|
self.graph = defaultdict(list)
|
@@ -19,64 +19,22 @@ class Order:
|
|
19
19
|
self.variables = set()
|
20
20
|
# Equations that can solve for each variable
|
21
21
|
self.solvers = defaultdict(list) # variable -> [equations that can solve it]
|
22
|
-
|
22
|
+
|
23
23
|
def add_equation(self, equation: Equation, known_vars: set[str]):
|
24
24
|
"""Add an equation to the dependency graph."""
|
25
25
|
eq_vars = equation.get_all_variables()
|
26
26
|
unknown_vars = equation.get_unknown_variables(known_vars)
|
27
|
-
|
27
|
+
|
28
28
|
# Update variables set
|
29
29
|
self.variables.update(eq_vars)
|
30
|
-
|
30
|
+
|
31
31
|
# Analyze equation structure to determine dependencies and solvers
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
lhs_vars = equation.lhs.get_variables()
|
39
|
-
else:
|
40
|
-
lhs_vars = set()
|
41
|
-
rhs_vars = equation.rhs.get_variables() if hasattr(equation.rhs, 'get_variables') else set()
|
42
|
-
|
43
|
-
# If LHS is a single variable, it depends on all variables in RHS
|
44
|
-
if len(lhs_vars) == 1:
|
45
|
-
lhs_var = next(iter(lhs_vars))
|
46
|
-
|
47
|
-
# Only add as solver for the LHS variable (if it's unknown)
|
48
|
-
if lhs_var in unknown_vars:
|
49
|
-
self.solvers[lhs_var].append(equation)
|
50
|
-
|
51
|
-
# Add dependencies: LHS variable depends on all RHS variables
|
52
|
-
for rhs_var in rhs_vars:
|
53
|
-
if rhs_var != lhs_var:
|
54
|
-
self.add_dependency(rhs_var, lhs_var)
|
55
|
-
|
56
|
-
# If RHS is a single variable, it depends on all variables in LHS
|
57
|
-
elif len(rhs_vars) == 1:
|
58
|
-
rhs_var = next(iter(rhs_vars))
|
59
|
-
|
60
|
-
# Only add as solver for the RHS variable (if it's unknown)
|
61
|
-
if rhs_var in unknown_vars:
|
62
|
-
self.solvers[rhs_var].append(equation)
|
63
|
-
|
64
|
-
# Add dependencies: RHS variable depends on all LHS variables
|
65
|
-
for lhs_var in lhs_vars:
|
66
|
-
if lhs_var != rhs_var:
|
67
|
-
self.add_dependency(lhs_var, rhs_var)
|
68
|
-
|
69
|
-
# For more complex cases, use can_solve_for check
|
70
|
-
else:
|
71
|
-
for unknown_var in unknown_vars:
|
72
|
-
if equation.can_solve_for(unknown_var, known_vars):
|
73
|
-
self.solvers[unknown_var].append(equation)
|
74
|
-
|
75
|
-
# Add dependencies: unknown_var depends on all other variables in equation
|
76
|
-
for other_var in eq_vars:
|
77
|
-
if other_var != unknown_var:
|
78
|
-
self.add_dependency(other_var, unknown_var)
|
79
|
-
|
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
|
+
|
80
38
|
def add_dependency(self, dependency_source: str, dependent_variable: str):
|
81
39
|
"""
|
82
40
|
Add a dependency: dependent_variable depends on dependency_source.
|
@@ -87,11 +45,11 @@ class Order:
|
|
87
45
|
if dependent_variable not in self.graph[dependency_source]:
|
88
46
|
self.graph[dependency_source].append(dependent_variable)
|
89
47
|
self.in_degree[dependent_variable] += 1
|
90
|
-
|
48
|
+
|
91
49
|
# Ensure both variables are tracked
|
92
50
|
self.variables.add(dependency_source)
|
93
51
|
self.variables.add(dependent_variable)
|
94
|
-
|
52
|
+
|
95
53
|
def remove_dependency(self, dependency_source: str, dependent_variable: str):
|
96
54
|
"""Remove a dependency between variables."""
|
97
55
|
if dependent_variable in self.graph[dependency_source]:
|
@@ -106,44 +64,44 @@ class Order:
|
|
106
64
|
# Create a copy of in_degree for this computation
|
107
65
|
temp_in_degree = self.in_degree.copy()
|
108
66
|
temp_graph = defaultdict(list)
|
109
|
-
|
67
|
+
|
110
68
|
# Initialize temp_graph with copies, ensuring all variables have entries
|
111
69
|
for var in self.variables:
|
112
70
|
temp_graph[var] = self.graph[var].copy() if var in self.graph else []
|
113
|
-
|
71
|
+
|
114
72
|
# Initialize queue with variables that have no dependencies (already known)
|
115
73
|
queue = deque()
|
116
|
-
|
74
|
+
|
117
75
|
# Add known variables to queue first
|
118
76
|
for var in known_vars:
|
119
77
|
if var in self.variables:
|
120
78
|
queue.append(var)
|
121
|
-
|
79
|
+
|
122
80
|
# Add variables with no remaining dependencies AND have solver equations
|
123
81
|
for var in self.variables:
|
124
82
|
if var not in known_vars and temp_in_degree[var] == 0 and var in self.solvers:
|
125
83
|
queue.append(var)
|
126
|
-
|
84
|
+
|
127
85
|
solving_order = []
|
128
|
-
|
86
|
+
|
129
87
|
while queue:
|
130
88
|
current_var = queue.popleft()
|
131
89
|
solving_order.append(current_var)
|
132
|
-
|
90
|
+
|
133
91
|
# Remove this variable's influence on dependent variables
|
134
92
|
if current_var in temp_graph:
|
135
93
|
for dependent_var in temp_graph[current_var]:
|
136
94
|
temp_in_degree[dependent_var] -= 1
|
137
|
-
|
95
|
+
|
138
96
|
# If dependent variable has no more dependencies AND has solvers, add it to queue
|
139
97
|
if temp_in_degree[dependent_var] == 0 and dependent_var in self.solvers:
|
140
98
|
queue.append(dependent_var)
|
141
|
-
|
99
|
+
|
142
100
|
# Filter out known variables from the result, as they don't need solving
|
143
101
|
result = [var for var in solving_order if var not in known_vars]
|
144
|
-
|
102
|
+
|
145
103
|
return result
|
146
|
-
|
104
|
+
|
147
105
|
def detect_cycles(self) -> list[list[str]]:
|
148
106
|
"""
|
149
107
|
Detect cycles in the dependency graph.
|
@@ -153,7 +111,7 @@ class Order:
|
|
153
111
|
color = defaultdict(int)
|
154
112
|
cycles = []
|
155
113
|
current_path = []
|
156
|
-
|
114
|
+
|
157
115
|
def dfs_visit(node: str) -> bool:
|
158
116
|
"""DFS visit with cycle detection. Returns True if cycle found."""
|
159
117
|
if color[node] == GRAY:
|
@@ -162,66 +120,63 @@ class Order:
|
|
162
120
|
cycle = current_path[cycle_start:] + [node]
|
163
121
|
cycles.append(cycle)
|
164
122
|
return True
|
165
|
-
|
123
|
+
|
166
124
|
if color[node] == BLACK:
|
167
125
|
return False
|
168
|
-
|
126
|
+
|
169
127
|
# Mark as being processed
|
170
128
|
color[node] = GRAY
|
171
129
|
current_path.append(node)
|
172
|
-
|
130
|
+
|
173
131
|
# Visit neighbors
|
174
132
|
for neighbor in self.graph[node]:
|
175
133
|
if dfs_visit(neighbor):
|
176
134
|
return True
|
177
|
-
|
135
|
+
|
178
136
|
# Mark as completely processed
|
179
137
|
color[node] = BLACK
|
180
138
|
current_path.pop()
|
181
139
|
return False
|
182
|
-
|
140
|
+
|
183
141
|
# Check all variables
|
184
142
|
for var in self.variables:
|
185
143
|
if color[var] == WHITE:
|
186
144
|
dfs_visit(var)
|
187
|
-
|
145
|
+
|
188
146
|
return cycles
|
189
|
-
|
147
|
+
|
190
148
|
def can_solve_system(self, known_vars: set[str]) -> tuple[bool, list[str]]:
|
191
149
|
"""
|
192
150
|
Check if the system can be completely solved given known variables.
|
193
151
|
Returns (can_solve, unsolvable_variables).
|
194
152
|
"""
|
195
153
|
all_unknown = self.variables - known_vars
|
196
|
-
|
197
|
-
#
|
198
|
-
truly_unsolvable =
|
199
|
-
|
200
|
-
|
201
|
-
truly_unsolvable.append(var)
|
202
|
-
|
203
|
-
# Check if we have enough equations for unknowns
|
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
|
204
159
|
variables_with_solvers = all_unknown - set(truly_unsolvable)
|
205
|
-
unique_equations =
|
206
|
-
|
207
|
-
#
|
160
|
+
unique_equations = self._get_unique_equations(variables_with_solvers)
|
161
|
+
|
162
|
+
# Simple heuristic: need at least as many equations as unknowns
|
208
163
|
can_solve_completely = len(unique_equations) >= len(variables_with_solvers) and len(truly_unsolvable) == 0
|
209
|
-
|
210
|
-
if
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
return
|
220
|
-
|
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
|
+
|
221
176
|
def get_solvable_variables(self, known_vars: set[str]) -> list[str]:
|
222
177
|
"""Get variables that can be solved in the next iteration."""
|
223
178
|
solvable = []
|
224
|
-
|
179
|
+
|
225
180
|
for var in self.variables:
|
226
181
|
if var not in known_vars:
|
227
182
|
# Check if all dependencies of this variable are known
|
@@ -230,24 +185,24 @@ class Order:
|
|
230
185
|
if var in self.graph[dep_source] and dep_source not in known_vars:
|
231
186
|
dependencies_known = False
|
232
187
|
break
|
233
|
-
|
188
|
+
|
234
189
|
if dependencies_known and var in self.solvers:
|
235
190
|
solvable.append(var)
|
236
|
-
|
191
|
+
|
237
192
|
return solvable
|
238
|
-
|
193
|
+
|
239
194
|
def get_equation_for_variable(self, var: str, known_vars: set[str]) -> Equation | None:
|
240
195
|
"""Get an equation that can solve for the given variable."""
|
241
196
|
if var not in self.solvers:
|
242
197
|
return None
|
243
|
-
|
198
|
+
|
244
199
|
# Find the first equation that can solve for this variable
|
245
200
|
for equation in self.solvers[var]:
|
246
201
|
if equation.can_solve_for(var, known_vars):
|
247
202
|
return equation
|
248
|
-
|
203
|
+
|
249
204
|
return None
|
250
|
-
|
205
|
+
|
251
206
|
def get_strongly_connected_components(self) -> list[set[str]]:
|
252
207
|
"""
|
253
208
|
Find strongly connected components in the dependency graph.
|
@@ -260,21 +215,21 @@ class Order:
|
|
260
215
|
index = {}
|
261
216
|
on_stack = {}
|
262
217
|
components = []
|
263
|
-
|
218
|
+
|
264
219
|
def strongconnect(node: str):
|
265
220
|
index[node] = index_counter[0]
|
266
221
|
lowlinks[node] = index_counter[0]
|
267
222
|
index_counter[0] += 1
|
268
223
|
stack.append(node)
|
269
224
|
on_stack[node] = True
|
270
|
-
|
225
|
+
|
271
226
|
for neighbor in self.graph[node]:
|
272
227
|
if neighbor not in index:
|
273
228
|
strongconnect(neighbor)
|
274
229
|
lowlinks[node] = min(lowlinks[node], lowlinks[neighbor])
|
275
230
|
elif on_stack[neighbor]:
|
276
231
|
lowlinks[node] = min(lowlinks[node], index[neighbor])
|
277
|
-
|
232
|
+
|
278
233
|
if lowlinks[node] == index[node]:
|
279
234
|
component = set()
|
280
235
|
while True:
|
@@ -284,11 +239,11 @@ class Order:
|
|
284
239
|
if w == node:
|
285
240
|
break
|
286
241
|
components.append(component)
|
287
|
-
|
242
|
+
|
288
243
|
for node in self.variables:
|
289
244
|
if node not in index:
|
290
245
|
strongconnect(node)
|
291
|
-
|
246
|
+
|
292
247
|
# Filter out single-node components (unless they have self-loops)
|
293
248
|
significant_components = []
|
294
249
|
for component in components:
|
@@ -298,58 +253,151 @@ class Order:
|
|
298
253
|
node = next(iter(component))
|
299
254
|
if node in self.graph[node]: # Self-loop
|
300
255
|
significant_components.append(component)
|
301
|
-
|
256
|
+
|
302
257
|
return significant_components
|
303
|
-
|
258
|
+
|
304
259
|
def analyze_system(self, known_vars: set[str]) -> dict[str, Any]:
|
305
260
|
"""
|
306
261
|
Perform comprehensive analysis of the equation system.
|
307
262
|
Returns analysis results including cycles, SCCs, solvability, etc.
|
308
263
|
"""
|
309
264
|
analysis = {}
|
310
|
-
|
265
|
+
|
311
266
|
# Basic info
|
312
|
-
analysis[
|
313
|
-
analysis[
|
314
|
-
analysis[
|
315
|
-
|
267
|
+
analysis["total_variables"] = len(self.variables)
|
268
|
+
analysis["known_variables"] = len(known_vars)
|
269
|
+
analysis["unknown_variables"] = len(self.variables - known_vars)
|
270
|
+
|
316
271
|
# Solving order
|
317
|
-
analysis[
|
318
|
-
|
272
|
+
analysis["solving_order"] = self.get_solving_order(known_vars)
|
273
|
+
|
319
274
|
# Solvability
|
320
275
|
can_solve, unsolvable = self.can_solve_system(known_vars)
|
321
|
-
analysis[
|
322
|
-
analysis[
|
323
|
-
|
276
|
+
analysis["can_solve_completely"] = can_solve
|
277
|
+
analysis["unsolvable_variables"] = unsolvable
|
278
|
+
|
324
279
|
# Cycles and SCCs
|
325
|
-
analysis[
|
326
|
-
analysis[
|
327
|
-
analysis[
|
328
|
-
|
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
|
+
|
329
284
|
# Next solvable variables
|
330
|
-
analysis[
|
331
|
-
|
285
|
+
analysis["immediately_solvable"] = self.get_solvable_variables(known_vars)
|
286
|
+
|
332
287
|
return analysis
|
333
|
-
|
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
|
+
|
334
382
|
def visualize_dependencies(self) -> str:
|
335
383
|
"""Create a text representation of the dependency graph."""
|
336
384
|
lines = ["Dependency Graph:"]
|
337
385
|
lines.append("=" * 20)
|
338
|
-
|
386
|
+
|
339
387
|
for source_var in sorted(self.graph.keys()):
|
340
388
|
if self.graph[source_var]:
|
341
389
|
dependents = ", ".join(sorted(self.graph[source_var]))
|
342
390
|
lines.append(f"{source_var} -> [{dependents}]")
|
343
|
-
|
391
|
+
|
344
392
|
lines.append("")
|
345
393
|
lines.append("In-degrees:")
|
346
394
|
for var in sorted(self.variables):
|
347
395
|
lines.append(f"{var}: {self.in_degree[var]}")
|
348
|
-
|
396
|
+
|
349
397
|
return "\n".join(lines)
|
350
|
-
|
398
|
+
|
351
399
|
def __str__(self) -> str:
|
352
400
|
return f"DependencyGraph(variables={len(self.variables)}, equations={len(self.solvers)})"
|
353
|
-
|
401
|
+
|
354
402
|
def __repr__(self) -> str:
|
355
403
|
return self.__str__()
|
qnty/solving/solvers/__init__.py
CHANGED
@@ -5,16 +5,9 @@ This package contains different solver implementations for solving systems
|
|
5
5
|
of engineering equations.
|
6
6
|
"""
|
7
7
|
|
8
|
+
from ..manager import SolverManager
|
8
9
|
from .base import BaseSolver, SolveError, SolveResult
|
9
10
|
from .iterative import IterativeSolver
|
10
|
-
from ..manager import SolverManager
|
11
11
|
from .simultaneous import SimultaneousEquationSolver
|
12
12
|
|
13
|
-
__all__ = [
|
14
|
-
'BaseSolver',
|
15
|
-
'SolveResult',
|
16
|
-
'SolveError',
|
17
|
-
'IterativeSolver',
|
18
|
-
'SimultaneousEquationSolver',
|
19
|
-
'SolverManager'
|
20
|
-
]
|
13
|
+
__all__ = ["BaseSolver", "SolveResult", "SolveError", "IterativeSolver", "SimultaneousEquationSolver", "SolverManager"]
|