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.
Files changed (92) hide show
  1. qnty/__init__.py +2 -3
  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 +1 -1
  12. qnty/equations/equation.py +118 -155
  13. qnty/equations/system.py +68 -65
  14. qnty/expressions/__init__.py +25 -46
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +46 -68
  17. qnty/expressions/nodes.py +539 -384
  18. qnty/expressions/types.py +70 -0
  19. qnty/problems/__init__.py +145 -0
  20. qnty/problems/composition.py +1031 -0
  21. qnty/problems/problem.py +695 -0
  22. qnty/problems/rules.py +145 -0
  23. qnty/problems/solving.py +1216 -0
  24. qnty/problems/validation.py +127 -0
  25. qnty/quantities/__init__.py +28 -5
  26. qnty/quantities/base_qnty.py +677 -0
  27. qnty/quantities/field_converters.py +24004 -0
  28. qnty/quantities/field_qnty.py +1012 -0
  29. qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
  30. qnty/{generated/quantities.py → quantities/field_vars.py} +754 -432
  31. qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
  32. qnty/solving/manager.py +50 -44
  33. qnty/solving/order.py +181 -133
  34. qnty/solving/solvers/__init__.py +2 -9
  35. qnty/solving/solvers/base.py +27 -37
  36. qnty/solving/solvers/iterative.py +115 -135
  37. qnty/solving/solvers/simultaneous.py +93 -165
  38. qnty/units/__init__.py +1 -0
  39. qnty/{generated/units.py → units/field_units.py} +1700 -991
  40. qnty/units/field_units.pyi +2461 -0
  41. qnty/units/prefixes.py +58 -105
  42. qnty/units/registry.py +76 -89
  43. qnty/utils/__init__.py +16 -0
  44. qnty/utils/caching/__init__.py +23 -0
  45. qnty/utils/caching/manager.py +401 -0
  46. qnty/utils/error_handling/__init__.py +66 -0
  47. qnty/utils/error_handling/context.py +39 -0
  48. qnty/utils/error_handling/exceptions.py +96 -0
  49. qnty/utils/error_handling/handlers.py +171 -0
  50. qnty/utils/logging.py +4 -4
  51. qnty/utils/protocols.py +164 -0
  52. qnty/utils/scope_discovery.py +420 -0
  53. {qnty-0.0.9.dist-info → qnty-0.1.0.dist-info}/METADATA +1 -1
  54. qnty-0.1.0.dist-info/RECORD +60 -0
  55. qnty/_backup/problem_original.py +0 -1251
  56. qnty/_backup/quantity.py +0 -63
  57. qnty/codegen/cli.py +0 -125
  58. qnty/codegen/generators/data/unit_data.json +0 -8807
  59. qnty/codegen/generators/data_processor.py +0 -345
  60. qnty/codegen/generators/dimensions_gen.py +0 -434
  61. qnty/codegen/generators/doc_generator.py +0 -141
  62. qnty/codegen/generators/out/dimension_mapping.json +0 -974
  63. qnty/codegen/generators/out/dimension_metadata.json +0 -123
  64. qnty/codegen/generators/out/units_metadata.json +0 -223
  65. qnty/codegen/generators/quantities_gen.py +0 -159
  66. qnty/codegen/generators/setters_gen.py +0 -178
  67. qnty/codegen/generators/stubs_gen.py +0 -167
  68. qnty/codegen/generators/units_gen.py +0 -295
  69. qnty/expressions/cache.py +0 -94
  70. qnty/generated/dimensions.py +0 -514
  71. qnty/problem/__init__.py +0 -91
  72. qnty/problem/base.py +0 -142
  73. qnty/problem/composition.py +0 -385
  74. qnty/problem/composition_mixin.py +0 -382
  75. qnty/problem/equations.py +0 -413
  76. qnty/problem/metaclass.py +0 -302
  77. qnty/problem/reconstruction.py +0 -1016
  78. qnty/problem/solving.py +0 -180
  79. qnty/problem/validation.py +0 -64
  80. qnty/problem/variables.py +0 -239
  81. qnty/quantities/expression_quantity.py +0 -314
  82. qnty/quantities/quantity.py +0 -428
  83. qnty/quantities/typed_quantity.py +0 -215
  84. qnty/validation/__init__.py +0 -0
  85. qnty/validation/registry.py +0 -0
  86. qnty/validation/rules.py +0 -167
  87. qnty-0.0.9.dist-info/RECORD +0 -63
  88. /qnty/{codegen → extensions}/__init__.py +0 -0
  89. /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
  90. /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
  91. /qnty/{generated → extensions/reporting}/__init__.py +0 -0
  92. {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), # Fall back to iterative
21
+ IterativeSolver(logger), # Fall back to iterative
21
22
  ]
22
-
23
- def solve(self, equations: list[Equation], variables: dict[str, Variable],
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
- variables=variables,
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
- if self.logger:
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
- else:
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
- variables=variables,
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 qnty.equations.equation import Equation
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
- # For equations of the form "var = expression", var depends on all variables in expression
33
- # Check if LHS is a Variable (has symbol) or Expression (has get_variables)
34
- if hasattr(equation.lhs, 'symbol'):
35
- # Type guard: this is a Variable
36
- lhs_vars = {equation.lhs.symbol} # type: ignore[attr-defined]
37
- elif hasattr(equation.lhs, 'get_variables'):
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
- # Variables with no solver equations are truly unsolvable
198
- truly_unsolvable = []
199
- for var in all_unknown:
200
- if var not in self.solvers or len(self.solvers[var]) == 0:
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 = {self.solvers[var][0] for var in variables_with_solvers if var in self.solvers and self.solvers[var]}
206
-
207
- # For now, use simple heuristic: need at least as many equations as unknowns
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 not can_solve_completely:
211
- # If we can't solve completely, check what would be left unsolvable
212
- solving_order = self.get_solving_order(known_vars)
213
- solvable = set(solving_order)
214
- conditional_unsolvable = all_unknown - solvable
215
- unsolvable = list(set(truly_unsolvable) | conditional_unsolvable)
216
- else:
217
- unsolvable = []
218
-
219
- return can_solve_completely, unsolvable
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['total_variables'] = len(self.variables)
313
- analysis['known_variables'] = len(known_vars)
314
- analysis['unknown_variables'] = len(self.variables - known_vars)
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['solving_order'] = self.get_solving_order(known_vars)
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['can_solve_completely'] = can_solve
322
- analysis['unsolvable_variables'] = unsolvable
323
-
276
+ analysis["can_solve_completely"] = can_solve
277
+ analysis["unsolvable_variables"] = unsolvable
278
+
324
279
  # Cycles and SCCs
325
- analysis['cycles'] = self.detect_cycles()
326
- analysis['strongly_connected_components'] = self.get_strongly_connected_components()
327
- analysis['has_cycles'] = len(analysis['cycles']) > 0
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['immediately_solvable'] = self.get_solvable_variables(known_vars)
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__()
@@ -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"]