qnty 0.0.9__py3-none-any.whl → 0.1.1__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 +540 -384
  18. qnty/expressions/types.py +70 -0
  19. qnty/problems/__init__.py +145 -0
  20. qnty/problems/composition.py +1101 -0
  21. qnty/problems/problem.py +737 -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} +829 -444
  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.1.dist-info}/METADATA +1 -1
  54. qnty-0.1.1.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.1.dist-info}/WHEEL +0 -0
@@ -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 qnty.equations.equation import Equation
8
- from qnty.quantities import TypeSafeVariable as Variable
9
- from qnty.solving.order import Order
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
- variables: dict[str, Variable]
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
- pass
52
-
50
+ ...
51
+
53
52
  @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:
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
- pass
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(f"Solved {variable} = {result}")
85
-
86
- def _get_known_variables(self, variables: dict[str, Variable]) -> set[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, Variable]) -> set[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 qnty.equations.equation import Equation
4
- from qnty.expressions import VariableReference
5
- from qnty.quantities import TypeSafeVariable as Variable
6
- from qnty.solving.order import Order
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 doesn't have cycles and has at least one unknown.
27
+ Can handle any system that has at least one unknown and a dependency graph.
31
28
  """
32
- if not unknowns:
33
- return False
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
- variables=variables,
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 = -1 # Initialize to handle case where loop doesn't run
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
- remaining_unknowns = [v for v in self._get_unknown_variables(working_vars)
81
- if v not in known_vars]
82
- for var_symbol in remaining_unknowns:
83
- for eq in equations:
84
- if eq.can_solve_for(var_symbol, known_vars):
85
- solvable.append(var_symbol)
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
- # Try to break conditional cycles
90
- remaining_unknowns = [v for v in self._get_unknown_variables(working_vars)
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
- equation = dependency_graph.get_equation_for_variable(var_symbol, known_vars)
123
- if equation is None:
124
- # Try any equation that can solve it
125
- for eq in equations:
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
- variables=working_vars,
180
- steps=self.steps,
181
- success=success,
182
- message=message,
183
- method="IterativeSolver",
184
- iterations=iteration + 1
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