qnty 0.0.7__py3-none-any.whl → 0.0.9__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 (76) hide show
  1. qnty/__init__.py +140 -58
  2. qnty/_backup/problem_original.py +1251 -0
  3. qnty/_backup/quantity.py +63 -0
  4. qnty/codegen/cli.py +125 -0
  5. qnty/codegen/generators/data/unit_data.json +8807 -0
  6. qnty/codegen/generators/data_processor.py +345 -0
  7. qnty/codegen/generators/dimensions_gen.py +434 -0
  8. qnty/codegen/generators/doc_generator.py +141 -0
  9. qnty/codegen/generators/out/dimension_mapping.json +974 -0
  10. qnty/codegen/generators/out/dimension_metadata.json +123 -0
  11. qnty/codegen/generators/out/units_metadata.json +223 -0
  12. qnty/codegen/generators/quantities_gen.py +159 -0
  13. qnty/codegen/generators/setters_gen.py +178 -0
  14. qnty/codegen/generators/stubs_gen.py +167 -0
  15. qnty/codegen/generators/units_gen.py +295 -0
  16. qnty/codegen/generators/utils/__init__.py +0 -0
  17. qnty/equations/__init__.py +4 -0
  18. qnty/equations/equation.py +257 -0
  19. qnty/equations/system.py +127 -0
  20. qnty/expressions/__init__.py +61 -0
  21. qnty/expressions/cache.py +94 -0
  22. qnty/expressions/functions.py +96 -0
  23. qnty/expressions/nodes.py +546 -0
  24. qnty/generated/__init__.py +0 -0
  25. qnty/generated/dimensions.py +514 -0
  26. qnty/generated/quantities.py +6003 -0
  27. qnty/generated/quantities.pyi +4192 -0
  28. qnty/generated/setters.py +12210 -0
  29. qnty/generated/units.py +9798 -0
  30. qnty/problem/__init__.py +91 -0
  31. qnty/problem/base.py +142 -0
  32. qnty/problem/composition.py +385 -0
  33. qnty/problem/composition_mixin.py +382 -0
  34. qnty/problem/equations.py +413 -0
  35. qnty/problem/metaclass.py +302 -0
  36. qnty/problem/reconstruction.py +1016 -0
  37. qnty/problem/solving.py +180 -0
  38. qnty/problem/validation.py +64 -0
  39. qnty/problem/variables.py +239 -0
  40. qnty/quantities/__init__.py +6 -0
  41. qnty/quantities/expression_quantity.py +314 -0
  42. qnty/quantities/quantity.py +428 -0
  43. qnty/quantities/typed_quantity.py +215 -0
  44. qnty/solving/__init__.py +0 -0
  45. qnty/solving/manager.py +90 -0
  46. qnty/solving/order.py +355 -0
  47. qnty/solving/solvers/__init__.py +20 -0
  48. qnty/solving/solvers/base.py +92 -0
  49. qnty/solving/solvers/iterative.py +185 -0
  50. qnty/solving/solvers/simultaneous.py +547 -0
  51. qnty/units/__init__.py +0 -0
  52. qnty/{prefixes.py → units/prefixes.py} +54 -33
  53. qnty/{unit.py → units/registry.py} +73 -32
  54. qnty/utils/__init__.py +0 -0
  55. qnty/utils/logging.py +40 -0
  56. qnty/validation/__init__.py +0 -0
  57. qnty/validation/registry.py +0 -0
  58. qnty/validation/rules.py +167 -0
  59. qnty-0.0.9.dist-info/METADATA +199 -0
  60. qnty-0.0.9.dist-info/RECORD +63 -0
  61. qnty/dimension.py +0 -186
  62. qnty/equation.py +0 -216
  63. qnty/expression.py +0 -492
  64. qnty/unit_types/base.py +0 -47
  65. qnty/units.py +0 -8113
  66. qnty/variable.py +0 -263
  67. qnty/variable_types/base.py +0 -58
  68. qnty/variable_types/expression_variable.py +0 -68
  69. qnty/variable_types/typed_variable.py +0 -87
  70. qnty/variables.py +0 -2298
  71. qnty/variables.pyi +0 -6148
  72. qnty-0.0.7.dist-info/METADATA +0 -355
  73. qnty-0.0.7.dist-info/RECORD +0 -19
  74. /qnty/{unit_types → codegen}/__init__.py +0 -0
  75. /qnty/{variable_types → codegen/generators}/__init__.py +0 -0
  76. {qnty-0.0.7.dist-info → qnty-0.0.9.dist-info}/WHEEL +0 -0
@@ -0,0 +1,90 @@
1
+
2
+ from qnty.equations.equation import Equation
3
+ from qnty.quantities import TypeSafeVariable as Variable
4
+ from qnty.solving.order import Order
5
+
6
+ from .solvers.base import BaseSolver, SolveResult
7
+ from .solvers.iterative import IterativeSolver
8
+ from .solvers.simultaneous import SimultaneousEquationSolver
9
+
10
+
11
+ class SolverManager:
12
+ """
13
+ Manages multiple solvers and selects the best one for a given problem.
14
+ """
15
+
16
+ def __init__(self, logger=None):
17
+ self.logger = logger
18
+ self.solvers = [
19
+ SimultaneousEquationSolver(logger), # Try simultaneous first for cyclic systems
20
+ IterativeSolver(logger), # Fall back to iterative
21
+ ]
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:
26
+ """
27
+ Solve the system using the best available solver.
28
+
29
+ Args:
30
+ equations: List of equations to solve
31
+ variables: Dictionary of all variables (known and unknown)
32
+ dependency_graph: Optional dependency graph
33
+ max_iterations: Maximum number of iterations
34
+ tolerance: Convergence tolerance
35
+
36
+ Returns:
37
+ SolveResult containing the solution
38
+ """
39
+ unknowns = {s for s, v in variables.items() if not v.is_known}
40
+
41
+ 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
+
50
+ # Get system analysis if we have a dependency graph
51
+ analysis = None
52
+ if dependency_graph:
53
+ known_vars = {s for s, v in variables.items() if v.is_known}
54
+ analysis = dependency_graph.analyze_system(known_vars)
55
+
56
+ # Try each solver in order of preference
57
+ for solver in self.solvers:
58
+ 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
+
65
+ if result.success:
66
+ 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
+
75
+ # 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
+
84
+ def add_solver(self, solver: BaseSolver):
85
+ """Add a custom solver to the manager."""
86
+ self.solvers.insert(0, solver) # Add to beginning for highest priority
87
+
88
+ def get_available_solvers(self) -> list[str]:
89
+ """Get list of available solver names."""
90
+ return [solver.__class__.__name__ for solver in self.solvers]
qnty/solving/order.py ADDED
@@ -0,0 +1,355 @@
1
+ from collections import defaultdict, deque
2
+ from typing import Any
3
+
4
+ from qnty.equations.equation 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
+ # 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
+
80
+ def add_dependency(self, dependency_source: str, dependent_variable: str):
81
+ """
82
+ Add a dependency: dependent_variable depends on dependency_source.
83
+ This means dependency_source must be solved before dependent_variable.
84
+ """
85
+ if dependent_variable != dependency_source: # Avoid self-dependencies
86
+ # Add to graph
87
+ if dependent_variable not in self.graph[dependency_source]:
88
+ self.graph[dependency_source].append(dependent_variable)
89
+ self.in_degree[dependent_variable] += 1
90
+
91
+ # Ensure both variables are tracked
92
+ self.variables.add(dependency_source)
93
+ self.variables.add(dependent_variable)
94
+
95
+ def remove_dependency(self, dependency_source: str, dependent_variable: str):
96
+ """Remove a dependency between variables."""
97
+ if dependent_variable in self.graph[dependency_source]:
98
+ self.graph[dependency_source].remove(dependent_variable)
99
+ self.in_degree[dependent_variable] -= 1
100
+
101
+ def get_solving_order(self, known_vars: set[str]) -> list[str]:
102
+ """
103
+ Get the order in which variables should be solved using topological sort.
104
+ Returns list of variables in solving order.
105
+ """
106
+ # Create a copy of in_degree for this computation
107
+ temp_in_degree = self.in_degree.copy()
108
+ temp_graph = defaultdict(list)
109
+
110
+ # Initialize temp_graph with copies, ensuring all variables have entries
111
+ for var in self.variables:
112
+ temp_graph[var] = self.graph[var].copy() if var in self.graph else []
113
+
114
+ # Initialize queue with variables that have no dependencies (already known)
115
+ queue = deque()
116
+
117
+ # Add known variables to queue first
118
+ for var in known_vars:
119
+ if var in self.variables:
120
+ queue.append(var)
121
+
122
+ # Add variables with no remaining dependencies AND have solver equations
123
+ for var in self.variables:
124
+ if var not in known_vars and temp_in_degree[var] == 0 and var in self.solvers:
125
+ queue.append(var)
126
+
127
+ solving_order = []
128
+
129
+ while queue:
130
+ current_var = queue.popleft()
131
+ solving_order.append(current_var)
132
+
133
+ # Remove this variable's influence on dependent variables
134
+ if current_var in temp_graph:
135
+ for dependent_var in temp_graph[current_var]:
136
+ temp_in_degree[dependent_var] -= 1
137
+
138
+ # If dependent variable has no more dependencies AND has solvers, add it to queue
139
+ if temp_in_degree[dependent_var] == 0 and dependent_var in self.solvers:
140
+ queue.append(dependent_var)
141
+
142
+ # Filter out known variables from the result, as they don't need solving
143
+ result = [var for var in solving_order if var not in known_vars]
144
+
145
+ return result
146
+
147
+ def detect_cycles(self) -> list[list[str]]:
148
+ """
149
+ Detect cycles in the dependency graph.
150
+ Returns list of cycles (each cycle is a list of variables).
151
+ """
152
+ WHITE, GRAY, BLACK = 0, 1, 2
153
+ color = defaultdict(int)
154
+ cycles = []
155
+ current_path = []
156
+
157
+ def dfs_visit(node: str) -> bool:
158
+ """DFS visit with cycle detection. Returns True if cycle found."""
159
+ if color[node] == GRAY:
160
+ # Found a back edge - cycle detected
161
+ cycle_start = current_path.index(node)
162
+ cycle = current_path[cycle_start:] + [node]
163
+ cycles.append(cycle)
164
+ return True
165
+
166
+ if color[node] == BLACK:
167
+ return False
168
+
169
+ # Mark as being processed
170
+ color[node] = GRAY
171
+ current_path.append(node)
172
+
173
+ # Visit neighbors
174
+ for neighbor in self.graph[node]:
175
+ if dfs_visit(neighbor):
176
+ return True
177
+
178
+ # Mark as completely processed
179
+ color[node] = BLACK
180
+ current_path.pop()
181
+ return False
182
+
183
+ # Check all variables
184
+ for var in self.variables:
185
+ if color[var] == WHITE:
186
+ dfs_visit(var)
187
+
188
+ return cycles
189
+
190
+ def can_solve_system(self, known_vars: set[str]) -> tuple[bool, list[str]]:
191
+ """
192
+ Check if the system can be completely solved given known variables.
193
+ Returns (can_solve, unsolvable_variables).
194
+ """
195
+ 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
204
+ 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
208
+ 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
+
221
+ def get_solvable_variables(self, known_vars: set[str]) -> list[str]:
222
+ """Get variables that can be solved in the next iteration."""
223
+ solvable = []
224
+
225
+ for var in self.variables:
226
+ if var not in known_vars:
227
+ # Check if all dependencies of this variable are known
228
+ dependencies_known = True
229
+ for dep_source in self.graph:
230
+ if var in self.graph[dep_source] and dep_source not in known_vars:
231
+ dependencies_known = False
232
+ break
233
+
234
+ if dependencies_known and var in self.solvers:
235
+ solvable.append(var)
236
+
237
+ return solvable
238
+
239
+ def get_equation_for_variable(self, var: str, known_vars: set[str]) -> Equation | None:
240
+ """Get an equation that can solve for the given variable."""
241
+ if var not in self.solvers:
242
+ return None
243
+
244
+ # Find the first equation that can solve for this variable
245
+ for equation in self.solvers[var]:
246
+ if equation.can_solve_for(var, known_vars):
247
+ return equation
248
+
249
+ return None
250
+
251
+ def get_strongly_connected_components(self) -> list[set[str]]:
252
+ """
253
+ Find strongly connected components in the dependency graph.
254
+ Variables in the same SCC must be solved simultaneously.
255
+ """
256
+ # Tarjan's algorithm for finding SCCs
257
+ index_counter = [0]
258
+ stack = []
259
+ lowlinks = {}
260
+ index = {}
261
+ on_stack = {}
262
+ components = []
263
+
264
+ def strongconnect(node: str):
265
+ index[node] = index_counter[0]
266
+ lowlinks[node] = index_counter[0]
267
+ index_counter[0] += 1
268
+ stack.append(node)
269
+ on_stack[node] = True
270
+
271
+ for neighbor in self.graph[node]:
272
+ if neighbor not in index:
273
+ strongconnect(neighbor)
274
+ lowlinks[node] = min(lowlinks[node], lowlinks[neighbor])
275
+ elif on_stack[neighbor]:
276
+ lowlinks[node] = min(lowlinks[node], index[neighbor])
277
+
278
+ if lowlinks[node] == index[node]:
279
+ component = set()
280
+ while True:
281
+ w = stack.pop()
282
+ on_stack[w] = False
283
+ component.add(w)
284
+ if w == node:
285
+ break
286
+ components.append(component)
287
+
288
+ for node in self.variables:
289
+ if node not in index:
290
+ strongconnect(node)
291
+
292
+ # Filter out single-node components (unless they have self-loops)
293
+ significant_components = []
294
+ for component in components:
295
+ if len(component) > 1:
296
+ significant_components.append(component)
297
+ elif len(component) == 1:
298
+ node = next(iter(component))
299
+ if node in self.graph[node]: # Self-loop
300
+ significant_components.append(component)
301
+
302
+ return significant_components
303
+
304
+ def analyze_system(self, known_vars: set[str]) -> dict[str, Any]:
305
+ """
306
+ Perform comprehensive analysis of the equation system.
307
+ Returns analysis results including cycles, SCCs, solvability, etc.
308
+ """
309
+ analysis = {}
310
+
311
+ # 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
+
316
+ # Solving order
317
+ analysis['solving_order'] = self.get_solving_order(known_vars)
318
+
319
+ # Solvability
320
+ can_solve, unsolvable = self.can_solve_system(known_vars)
321
+ analysis['can_solve_completely'] = can_solve
322
+ analysis['unsolvable_variables'] = unsolvable
323
+
324
+ # 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
+
329
+ # Next solvable variables
330
+ analysis['immediately_solvable'] = self.get_solvable_variables(known_vars)
331
+
332
+ return analysis
333
+
334
+ def visualize_dependencies(self) -> str:
335
+ """Create a text representation of the dependency graph."""
336
+ lines = ["Dependency Graph:"]
337
+ lines.append("=" * 20)
338
+
339
+ for source_var in sorted(self.graph.keys()):
340
+ if self.graph[source_var]:
341
+ dependents = ", ".join(sorted(self.graph[source_var]))
342
+ lines.append(f"{source_var} -> [{dependents}]")
343
+
344
+ lines.append("")
345
+ lines.append("In-degrees:")
346
+ for var in sorted(self.variables):
347
+ lines.append(f"{var}: {self.in_degree[var]}")
348
+
349
+ return "\n".join(lines)
350
+
351
+ def __str__(self) -> str:
352
+ return f"DependencyGraph(variables={len(self.variables)}, equations={len(self.solvers)})"
353
+
354
+ def __repr__(self) -> str:
355
+ return self.__str__()
@@ -0,0 +1,20 @@
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 .base import BaseSolver, SolveError, SolveResult
9
+ from .iterative import IterativeSolver
10
+ from ..manager import SolverManager
11
+ from .simultaneous import SimultaneousEquationSolver
12
+
13
+ __all__ = [
14
+ 'BaseSolver',
15
+ 'SolveResult',
16
+ 'SolveError',
17
+ 'IterativeSolver',
18
+ 'SimultaneousEquationSolver',
19
+ 'SolverManager'
20
+ ]
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from qnty.equations.equation import Equation
8
+ from qnty.quantities import TypeSafeVariable as Variable
9
+ from qnty.solving.order import Order
10
+
11
+
12
+ class SolveError(Exception):
13
+ """Exception raised when solving fails."""
14
+ pass
15
+
16
+
17
+ @dataclass
18
+ class SolveResult:
19
+ """Result of a solve operation."""
20
+ variables: dict[str, Variable]
21
+ steps: list[dict[str, Any]] = field(default_factory=list)
22
+ success: bool = True
23
+ message: str = ""
24
+ method: str = ""
25
+ iterations: int = 0
26
+
27
+
28
+ class BaseSolver(ABC):
29
+ """Base class for all equation solvers."""
30
+
31
+ def __init__(self, logger=None):
32
+ self.logger = logger
33
+ self.steps = []
34
+
35
+ @abstractmethod
36
+ def can_handle(self, equations: list[Equation], unknowns: set[str],
37
+ dependency_graph: Order | None = None,
38
+ analysis: dict[str, Any] | None = None) -> bool:
39
+ """
40
+ Check if this solver can handle the given problem.
41
+
42
+ Args:
43
+ equations: List of equations to solve
44
+ unknowns: Set of unknown variable symbols
45
+ dependency_graph: Optional dependency graph for analysis
46
+ analysis: Optional system analysis results
47
+
48
+ Returns:
49
+ True if this solver can handle the problem
50
+ """
51
+ pass
52
+
53
+ @abstractmethod
54
+ def solve(self, equations: list[Equation], variables: dict[str, Variable],
55
+ dependency_graph: Order | None = None,
56
+ max_iterations: int = 100, tolerance: float = 1e-10) -> SolveResult:
57
+ """
58
+ Solve the system of equations.
59
+
60
+ Args:
61
+ equations: List of equations to solve
62
+ variables: Dictionary of all variables (known and unknown)
63
+ dependency_graph: Optional dependency graph
64
+ max_iterations: Maximum number of iterations
65
+ tolerance: Convergence tolerance
66
+
67
+ Returns:
68
+ SolveResult containing the solution
69
+ """
70
+ pass
71
+
72
+ def _log_step(self, iteration: int, variable: str, equation: str,
73
+ result: str, method: str | None = None):
74
+ """Log a solving step."""
75
+ step = {
76
+ 'iteration': iteration,
77
+ 'variable': variable,
78
+ 'equation': equation,
79
+ 'result': result,
80
+ 'method': method or self.__class__.__name__
81
+ }
82
+ self.steps.append(step)
83
+ if self.logger:
84
+ self.logger.debug(f"Solved {variable} = {result}")
85
+
86
+ def _get_known_variables(self, variables: dict[str, Variable]) -> set[str]:
87
+ """Get symbols of known variables."""
88
+ return {s for s, v in variables.items() if v.is_known}
89
+
90
+ def _get_unknown_variables(self, variables: dict[str, Variable]) -> set[str]:
91
+ """Get symbols of unknown variables."""
92
+ return {s for s, v in variables.items() if not v.is_known}