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/solvers/base.py
CHANGED
@@ -1,23 +1,24 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import logging
|
3
4
|
from abc import ABC, abstractmethod
|
4
5
|
from dataclasses import dataclass, field
|
5
6
|
from typing import Any
|
6
7
|
|
7
|
-
from
|
8
|
-
from
|
9
|
-
from
|
8
|
+
from ...equations import Equation
|
9
|
+
from ...quantities import FieldQnty
|
10
|
+
from ..order import Order
|
10
11
|
|
11
12
|
|
12
13
|
class SolveError(Exception):
|
13
14
|
"""Exception raised when solving fails."""
|
14
|
-
pass
|
15
15
|
|
16
16
|
|
17
17
|
@dataclass
|
18
18
|
class SolveResult:
|
19
19
|
"""Result of a solve operation."""
|
20
|
-
|
20
|
+
|
21
|
+
variables: dict[str, FieldQnty]
|
21
22
|
steps: list[dict[str, Any]] = field(default_factory=list)
|
22
23
|
success: bool = True
|
23
24
|
message: str = ""
|
@@ -27,66 +28,55 @@ class SolveResult:
|
|
27
28
|
|
28
29
|
class BaseSolver(ABC):
|
29
30
|
"""Base class for all equation solvers."""
|
30
|
-
|
31
|
-
def __init__(self, logger=None):
|
31
|
+
|
32
|
+
def __init__(self, logger: logging.Logger | None = None):
|
32
33
|
self.logger = logger
|
33
|
-
self.steps = []
|
34
|
-
|
34
|
+
self.steps: list[dict[str, Any]] = []
|
35
|
+
|
35
36
|
@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:
|
37
|
+
def can_handle(self, equations: list[Equation], unknowns: set[str], dependency_graph: Order | None = None, analysis: dict[str, Any] | None = None) -> bool:
|
39
38
|
"""
|
40
39
|
Check if this solver can handle the given problem.
|
41
|
-
|
40
|
+
|
42
41
|
Args:
|
43
42
|
equations: List of equations to solve
|
44
43
|
unknowns: Set of unknown variable symbols
|
45
44
|
dependency_graph: Optional dependency graph for analysis
|
46
45
|
analysis: Optional system analysis results
|
47
|
-
|
46
|
+
|
48
47
|
Returns:
|
49
48
|
True if this solver can handle the problem
|
50
49
|
"""
|
51
|
-
|
52
|
-
|
50
|
+
...
|
51
|
+
|
53
52
|
@abstractmethod
|
54
|
-
def solve(self, equations: list[Equation], variables: dict[str,
|
55
|
-
dependency_graph: Order | None = None,
|
56
|
-
max_iterations: int = 100, tolerance: float = 1e-10) -> SolveResult:
|
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:
|
57
54
|
"""
|
58
55
|
Solve the system of equations.
|
59
|
-
|
56
|
+
|
60
57
|
Args:
|
61
58
|
equations: List of equations to solve
|
62
59
|
variables: Dictionary of all variables (known and unknown)
|
63
60
|
dependency_graph: Optional dependency graph
|
64
61
|
max_iterations: Maximum number of iterations
|
65
62
|
tolerance: Convergence tolerance
|
66
|
-
|
63
|
+
|
67
64
|
Returns:
|
68
65
|
SolveResult containing the solution
|
69
66
|
"""
|
70
|
-
|
71
|
-
|
72
|
-
def _log_step(self, iteration: int, variable: str, equation: str,
|
73
|
-
result: str, method: str | None = None):
|
67
|
+
...
|
68
|
+
|
69
|
+
def _log_step(self, iteration: int, variable: str, equation: str, result: str, method: str | None = None):
|
74
70
|
"""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
|
-
}
|
71
|
+
step = {"iteration": iteration, "variable": variable, "equation": equation, "result": result, "method": method or self.__class__.__name__}
|
82
72
|
self.steps.append(step)
|
83
73
|
if self.logger:
|
84
|
-
self.logger.debug(
|
85
|
-
|
86
|
-
def _get_known_variables(self, variables: dict[str,
|
74
|
+
self.logger.debug("Solved %s = %s", variable, result)
|
75
|
+
|
76
|
+
def _get_known_variables(self, variables: dict[str, FieldQnty]) -> set[str]:
|
87
77
|
"""Get symbols of known variables."""
|
88
78
|
return {s for s, v in variables.items() if v.is_known}
|
89
|
-
|
90
|
-
def _get_unknown_variables(self, variables: dict[str,
|
79
|
+
|
80
|
+
def _get_unknown_variables(self, variables: dict[str, FieldQnty]) -> set[str]:
|
91
81
|
"""Get symbols of unknown variables."""
|
92
82
|
return {s for s, v in variables.items() if not v.is_known}
|
@@ -1,185 +1,165 @@
|
|
1
1
|
from typing import Any
|
2
2
|
|
3
|
-
from
|
4
|
-
from
|
5
|
-
from
|
6
|
-
from
|
7
|
-
|
3
|
+
from ...equations import Equation
|
4
|
+
from ...expressions import ConditionalExpression, VariableReference
|
5
|
+
from ...quantities.field_qnty import FieldQnty
|
6
|
+
from ..order import Order
|
8
7
|
from .base import BaseSolver, SolveResult
|
9
8
|
|
10
9
|
|
11
10
|
class IterativeSolver(BaseSolver):
|
12
11
|
"""
|
13
12
|
Iterative solver that follows dependency order like solving engineering problems by hand.
|
14
|
-
|
13
|
+
|
15
14
|
This solver works by:
|
16
15
|
1. Using dependency graph to determine the correct solving order
|
17
16
|
2. Solving variables one by one in dependency order (just like manual solving)
|
18
17
|
3. Preserving units throughout with Pint integration
|
19
18
|
4. Verifying each solution with residual checking
|
20
19
|
5. Repeating until all unknowns are solved
|
21
|
-
|
20
|
+
|
22
21
|
This approach mirrors how engineers solve problems by hand: solve what you can
|
23
22
|
with what you know, then use those results to solve the next level of dependencies.
|
24
23
|
"""
|
25
|
-
|
26
|
-
def can_handle(self, equations: list[Equation], unknowns: set[str],
|
27
|
-
dependency_graph: Order | None = None,
|
28
|
-
analysis: dict[str, Any] | None = None) -> bool:
|
24
|
+
|
25
|
+
def can_handle(self, equations: list[Equation], unknowns: set[str], dependency_graph: Order | None = None, analysis: dict[str, Any] | None = None) -> bool:
|
29
26
|
"""
|
30
|
-
Can handle any system that
|
27
|
+
Can handle any system that has at least one unknown and a dependency graph.
|
31
28
|
"""
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# The IterativeSolver can now handle cycles through iterative convergence
|
36
|
-
# if analysis and analysis.get('has_cycles', False):
|
37
|
-
# return False
|
38
|
-
|
39
|
-
# If we have a dependency graph, we can try to solve
|
40
|
-
if dependency_graph:
|
41
|
-
return True
|
42
|
-
|
43
|
-
# As a fallback, we can try to handle any system
|
44
|
-
return len(unknowns) > 0
|
45
|
-
|
46
|
-
def solve(self, equations: list[Equation], variables: dict[str, Variable],
|
47
|
-
dependency_graph: Order | None = None,
|
48
|
-
max_iterations: int = 100, tolerance: float = 1e-10) -> SolveResult:
|
29
|
+
return bool(unknowns and dependency_graph)
|
30
|
+
|
31
|
+
def solve(self, equations: list[Equation], variables: dict[str, FieldQnty], dependency_graph: Order | None = None, max_iterations: int = 100, tolerance: float = 1e-10) -> SolveResult:
|
49
32
|
"""
|
50
33
|
Solve the system iteratively using dependency graph.
|
51
34
|
"""
|
52
35
|
self.steps = []
|
53
|
-
|
36
|
+
|
54
37
|
if not dependency_graph:
|
55
|
-
return SolveResult(
|
56
|
-
|
57
|
-
steps=self.steps,
|
58
|
-
success=False,
|
59
|
-
message="Dependency graph required for iterative solving",
|
60
|
-
method="IterativeSolver"
|
61
|
-
)
|
62
|
-
|
38
|
+
return SolveResult(variables=variables, steps=self.steps, success=False, message="Dependency graph required for iterative solving", method="IterativeSolver")
|
39
|
+
|
63
40
|
# Make a copy of variables to work with
|
64
41
|
working_vars = dict(variables.items())
|
65
42
|
known_vars = self._get_known_variables(working_vars)
|
66
|
-
|
43
|
+
|
67
44
|
if self.logger:
|
68
45
|
self.logger.debug(f"Starting iterative solve with {len(known_vars)} known variables")
|
69
|
-
|
46
|
+
|
70
47
|
# Iterative solving
|
71
|
-
iteration =
|
48
|
+
iteration = 0
|
72
49
|
for iteration in range(max_iterations):
|
73
50
|
iteration_start = len(known_vars)
|
74
|
-
|
51
|
+
|
75
52
|
# Get variables that can be solved in this iteration
|
76
53
|
solvable = dependency_graph.get_solvable_variables(known_vars)
|
77
|
-
|
54
|
+
|
78
55
|
# Fallback: attempt direct equations for remaining unknowns
|
79
56
|
if not solvable:
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
break
|
87
|
-
|
57
|
+
solvable = self._find_directly_solvable_variables(equations, working_vars, known_vars)
|
58
|
+
|
59
|
+
# Try to break conditional cycles if still no solvable variables
|
60
|
+
if not solvable:
|
61
|
+
solvable = self._solve_conditional_cycles(equations, working_vars, known_vars)
|
62
|
+
|
88
63
|
if not solvable:
|
89
|
-
#
|
90
|
-
|
91
|
-
if v not in known_vars]
|
92
|
-
if remaining_unknowns:
|
93
|
-
# Look for conditional equations that can be evaluated
|
94
|
-
for var_symbol in remaining_unknowns:
|
95
|
-
for eq in equations:
|
96
|
-
# Check if LHS is a VariableReference with matching name
|
97
|
-
if (isinstance(eq.lhs, VariableReference) and
|
98
|
-
eq.lhs.name == var_symbol and
|
99
|
-
'ConditionalExpression' in str(type(eq.rhs))):
|
100
|
-
# This is a conditional equation - try to solve it
|
101
|
-
try:
|
102
|
-
solved_var = eq.solve_for(var_symbol, working_vars)
|
103
|
-
working_vars[var_symbol] = solved_var
|
104
|
-
known_vars.add(var_symbol)
|
105
|
-
solvable = [var_symbol] # Mark as solved this iteration
|
106
|
-
if self.logger:
|
107
|
-
self.logger.debug(f"Solved conditional cycle: {var_symbol} = {solved_var.quantity}")
|
108
|
-
break
|
109
|
-
except Exception:
|
110
|
-
continue
|
111
|
-
if solvable:
|
112
|
-
break
|
113
|
-
|
114
|
-
if not solvable:
|
115
|
-
break # No more variables can be solved
|
116
|
-
|
64
|
+
break # No more variables can be solved
|
65
|
+
|
117
66
|
if self.logger:
|
118
67
|
self.logger.debug(f"Iteration {iteration + 1} solvable: {solvable}")
|
119
|
-
|
68
|
+
|
120
69
|
# Solve for each solvable variable
|
121
70
|
for var_symbol in solvable:
|
122
|
-
|
123
|
-
if
|
124
|
-
|
125
|
-
|
126
|
-
if eq.can_solve_for(var_symbol, known_vars):
|
127
|
-
equation = eq
|
128
|
-
break
|
129
|
-
|
130
|
-
if equation is None:
|
131
|
-
continue
|
132
|
-
|
133
|
-
try:
|
134
|
-
solved_var = equation.solve_for(var_symbol, working_vars)
|
135
|
-
working_vars[var_symbol] = solved_var
|
136
|
-
known_vars.add(var_symbol)
|
137
|
-
|
138
|
-
# Verify solution by checking residual (like checking work by hand)
|
139
|
-
if equation.check_residual(working_vars, tolerance):
|
140
|
-
if self.logger:
|
141
|
-
self.logger.debug(f"Solution verified for {var_symbol}")
|
142
|
-
else:
|
143
|
-
if self.logger:
|
144
|
-
self.logger.warning(f"Residual check failed for {var_symbol}")
|
145
|
-
|
146
|
-
self._log_step(
|
147
|
-
iteration + 1,
|
148
|
-
var_symbol,
|
149
|
-
str(equation),
|
150
|
-
str(solved_var.quantity),
|
151
|
-
'iterative'
|
152
|
-
)
|
153
|
-
|
154
|
-
except Exception as e:
|
155
|
-
if self.logger:
|
156
|
-
self.logger.error(f"Failed to solve for {var_symbol}: {e}")
|
157
|
-
return SolveResult(
|
158
|
-
variables=working_vars,
|
159
|
-
steps=self.steps,
|
160
|
-
success=False,
|
161
|
-
message=f"Failed to solve for {var_symbol}: {e}",
|
162
|
-
method="IterativeSolver",
|
163
|
-
iterations=iteration + 1
|
164
|
-
)
|
165
|
-
|
71
|
+
result = self._solve_single_variable(var_symbol, equations, working_vars, known_vars, dependency_graph, iteration, tolerance)
|
72
|
+
if not result:
|
73
|
+
return SolveResult(variables=working_vars, steps=self.steps, success=False, message=f"Failed to solve for {var_symbol}", method="IterativeSolver", iterations=iteration + 1)
|
74
|
+
|
166
75
|
# Check for progress
|
167
76
|
if len(known_vars) == iteration_start:
|
168
77
|
if self.logger:
|
169
78
|
self.logger.warning("No progress made, stopping early")
|
170
79
|
break
|
171
|
-
|
80
|
+
|
172
81
|
# Check if we solved all unknowns
|
173
82
|
remaining_unknowns = self._get_unknown_variables(working_vars)
|
174
83
|
success = len(remaining_unknowns) == 0
|
175
|
-
|
84
|
+
|
176
85
|
message = "All variables solved" if success else f"Could not solve: {remaining_unknowns}"
|
177
|
-
|
178
|
-
return SolveResult(
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
86
|
+
|
87
|
+
return SolveResult(variables=working_vars, steps=self.steps, success=success, message=message, method="IterativeSolver", iterations=iteration + 1)
|
88
|
+
|
89
|
+
def _find_directly_solvable_variables(self, equations: list[Equation], working_vars: dict[str, FieldQnty], known_vars: set[str]) -> list[str]:
|
90
|
+
"""Find variables that can be directly solved from equations."""
|
91
|
+
solvable = []
|
92
|
+
remaining_unknowns = [v for v in self._get_unknown_variables(working_vars) if v not in known_vars]
|
93
|
+
|
94
|
+
for var_symbol in remaining_unknowns:
|
95
|
+
for eq in equations:
|
96
|
+
if eq.can_solve_for(var_symbol, known_vars):
|
97
|
+
solvable.append(var_symbol)
|
98
|
+
break
|
99
|
+
|
100
|
+
return solvable
|
101
|
+
|
102
|
+
def _solve_conditional_cycles(self, equations: list[Equation], working_vars: dict[str, FieldQnty], known_vars: set[str]) -> list[str]:
|
103
|
+
"""Attempt to solve conditional cycles in the equation system."""
|
104
|
+
remaining_unknowns = [v for v in self._get_unknown_variables(working_vars) if v not in known_vars]
|
105
|
+
|
106
|
+
for var_symbol in remaining_unknowns:
|
107
|
+
for eq in equations:
|
108
|
+
# Check if this is a conditional equation that can be solved
|
109
|
+
if self._is_conditional_equation(eq, var_symbol):
|
110
|
+
try:
|
111
|
+
solved_var = eq.solve_for(var_symbol, working_vars)
|
112
|
+
working_vars[var_symbol] = solved_var
|
113
|
+
known_vars.add(var_symbol)
|
114
|
+
|
115
|
+
if self.logger:
|
116
|
+
self.logger.debug(f"Solved conditional cycle: {var_symbol} = {solved_var.quantity}")
|
117
|
+
|
118
|
+
return [var_symbol] # Return immediately after solving one
|
119
|
+
except Exception:
|
120
|
+
continue
|
121
|
+
|
122
|
+
return []
|
123
|
+
|
124
|
+
def _is_conditional_equation(self, equation: Equation, var_symbol: str) -> bool:
|
125
|
+
"""Check if equation is a conditional equation for the given variable."""
|
126
|
+
return isinstance(equation.lhs, VariableReference) and equation.lhs.name == var_symbol and isinstance(equation.rhs, ConditionalExpression)
|
127
|
+
|
128
|
+
def _solve_single_variable(
|
129
|
+
self, var_symbol: str, equations: list[Equation], working_vars: dict[str, FieldQnty], known_vars: set[str], dependency_graph: Order, iteration: int, tolerance: float
|
130
|
+
) -> bool:
|
131
|
+
"""Solve for a single variable and update the system state."""
|
132
|
+
# Find equation that can solve for this variable
|
133
|
+
equation = dependency_graph.get_equation_for_variable(var_symbol, known_vars)
|
134
|
+
|
135
|
+
if equation is None:
|
136
|
+
# Try any equation that can solve it
|
137
|
+
for eq in equations:
|
138
|
+
if eq.can_solve_for(var_symbol, known_vars):
|
139
|
+
equation = eq
|
140
|
+
break
|
141
|
+
|
142
|
+
if equation is None:
|
143
|
+
return True # Skip this variable, not a failure
|
144
|
+
|
145
|
+
try:
|
146
|
+
solved_var = equation.solve_for(var_symbol, working_vars)
|
147
|
+
working_vars[var_symbol] = solved_var
|
148
|
+
known_vars.add(var_symbol)
|
149
|
+
|
150
|
+
# Verify solution by checking residual
|
151
|
+
if equation.check_residual(working_vars, tolerance):
|
152
|
+
if self.logger:
|
153
|
+
self.logger.debug(f"Solution verified for {var_symbol}")
|
154
|
+
else:
|
155
|
+
if self.logger:
|
156
|
+
self.logger.warning(f"Residual check failed for {var_symbol}")
|
157
|
+
|
158
|
+
self._log_step(iteration + 1, var_symbol, str(equation), str(solved_var.quantity), "iterative")
|
159
|
+
|
160
|
+
return True
|
161
|
+
|
162
|
+
except Exception as e:
|
163
|
+
if self.logger:
|
164
|
+
self.logger.error(f"Failed to solve for {var_symbol}: {e}")
|
165
|
+
return False
|