qnty 0.0.8__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 (74) hide show
  1. qnty/__init__.py +140 -59
  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 +4 -0
  12. qnty/equations/equation.py +220 -0
  13. qnty/equations/system.py +130 -0
  14. qnty/expressions/__init__.py +40 -0
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +74 -0
  17. qnty/expressions/nodes.py +701 -0
  18. qnty/expressions/types.py +70 -0
  19. qnty/extensions/plotting/__init__.py +0 -0
  20. qnty/extensions/reporting/__init__.py +0 -0
  21. qnty/problems/__init__.py +145 -0
  22. qnty/problems/composition.py +1031 -0
  23. qnty/problems/problem.py +695 -0
  24. qnty/problems/rules.py +145 -0
  25. qnty/problems/solving.py +1216 -0
  26. qnty/problems/validation.py +127 -0
  27. qnty/quantities/__init__.py +29 -0
  28. qnty/quantities/base_qnty.py +677 -0
  29. qnty/quantities/field_converters.py +24004 -0
  30. qnty/quantities/field_qnty.py +1012 -0
  31. qnty/quantities/field_setter.py +12320 -0
  32. qnty/quantities/field_vars.py +6325 -0
  33. qnty/quantities/field_vars.pyi +4191 -0
  34. qnty/solving/__init__.py +0 -0
  35. qnty/solving/manager.py +96 -0
  36. qnty/solving/order.py +403 -0
  37. qnty/solving/solvers/__init__.py +13 -0
  38. qnty/solving/solvers/base.py +82 -0
  39. qnty/solving/solvers/iterative.py +165 -0
  40. qnty/solving/solvers/simultaneous.py +475 -0
  41. qnty/units/__init__.py +1 -0
  42. qnty/units/field_units.py +10507 -0
  43. qnty/units/field_units.pyi +2461 -0
  44. qnty/units/prefixes.py +203 -0
  45. qnty/{unit.py → units/registry.py} +89 -61
  46. qnty/utils/__init__.py +16 -0
  47. qnty/utils/caching/__init__.py +23 -0
  48. qnty/utils/caching/manager.py +401 -0
  49. qnty/utils/error_handling/__init__.py +66 -0
  50. qnty/utils/error_handling/context.py +39 -0
  51. qnty/utils/error_handling/exceptions.py +96 -0
  52. qnty/utils/error_handling/handlers.py +171 -0
  53. qnty/utils/logging.py +40 -0
  54. qnty/utils/protocols.py +164 -0
  55. qnty/utils/scope_discovery.py +420 -0
  56. qnty-0.1.0.dist-info/METADATA +199 -0
  57. qnty-0.1.0.dist-info/RECORD +60 -0
  58. qnty/dimension.py +0 -186
  59. qnty/equation.py +0 -297
  60. qnty/expression.py +0 -553
  61. qnty/prefixes.py +0 -229
  62. qnty/unit_types/base.py +0 -47
  63. qnty/units.py +0 -8113
  64. qnty/variable.py +0 -300
  65. qnty/variable_types/base.py +0 -58
  66. qnty/variable_types/expression_variable.py +0 -106
  67. qnty/variable_types/typed_variable.py +0 -87
  68. qnty/variables.py +0 -2298
  69. qnty/variables.pyi +0 -6148
  70. qnty-0.0.8.dist-info/METADATA +0 -355
  71. qnty-0.0.8.dist-info/RECORD +0 -19
  72. /qnty/{unit_types → extensions}/__init__.py +0 -0
  73. /qnty/{variable_types → extensions/integration}/__init__.py +0 -0
  74. {qnty-0.0.8.dist-info → qnty-0.1.0.dist-info}/WHEEL +0 -0
qnty/dimension.py DELETED
@@ -1,186 +0,0 @@
1
- """
2
- Dimension System
3
- ================
4
-
5
- Compile-time dimensional analysis using type system for ultra-fast operations.
6
- """
7
-
8
- from dataclasses import dataclass
9
- from enum import IntEnum
10
- from typing import final
11
-
12
-
13
- class BaseDimension(IntEnum):
14
- """Base dimensions as prime numbers for efficient bit operations."""
15
- LENGTH = 2
16
- MASS = 3
17
- TIME = 5
18
- CURRENT = 7
19
- TEMPERATURE = 11
20
- AMOUNT = 13
21
- LUMINOSITY = 17
22
- DIMENSIONLESS = 1 # Must be 1 to act as multiplicative identity
23
-
24
-
25
- @final
26
- @dataclass(frozen=True)
27
- class DimensionSignature:
28
- """Immutable dimension signature for zero-cost dimensional analysis."""
29
-
30
- # Store as bit pattern for ultra-fast comparison
31
- _signature: int | float = 1
32
-
33
- @classmethod
34
- def create(cls, length=0, mass=0, time=0, current=0, temp=0, amount=0, luminosity=0):
35
- """Create dimension from exponents."""
36
- signature = 1
37
- if length != 0:
38
- signature *= BaseDimension.LENGTH ** length
39
- if mass != 0:
40
- signature *= BaseDimension.MASS ** mass
41
- if time != 0:
42
- signature *= BaseDimension.TIME ** time
43
- if current != 0:
44
- signature *= BaseDimension.CURRENT ** current
45
- if temp != 0:
46
- signature *= BaseDimension.TEMPERATURE ** temp
47
- if amount != 0:
48
- signature *= BaseDimension.AMOUNT ** amount
49
- if luminosity != 0:
50
- signature *= BaseDimension.LUMINOSITY ** luminosity
51
-
52
- return cls(signature)
53
-
54
- def __mul__(self, other):
55
- return DimensionSignature(self._signature * other._signature)
56
-
57
- def __truediv__(self, other):
58
- return DimensionSignature(self._signature / other._signature)
59
-
60
- def __pow__(self, power):
61
- return DimensionSignature(self._signature ** power)
62
-
63
- def is_compatible(self, other):
64
- """Ultra-fast dimensional compatibility check."""
65
- return self._signature == other._signature
66
-
67
- def __eq__(self, other):
68
- """Fast equality check for dimensions."""
69
- return isinstance(other, DimensionSignature) and self._signature == other._signature
70
-
71
- def __hash__(self):
72
- """Enable dimensions as dictionary keys."""
73
- return hash(self._signature)
74
-
75
-
76
- # Pre-defined dimension constants (alphabetically ordered)
77
- ABSORBED_DOSE = DimensionSignature.create(length=2, time=-2) # L^2 T^-2
78
- ACCELERATION = DimensionSignature.create(length=1, time=-2) # L T^-2
79
- ACTIVATION_ENERGY = DimensionSignature.create(amount=-1, length=2, time=-2) # N^-1 L^2 T^-2
80
- AMOUNT = DimensionSignature.create(amount=1) # N
81
- AMOUNT_OF_SUBSTANCE = DimensionSignature.create(amount=1) # N
82
- ANGLE_PLANE = DimensionSignature(BaseDimension.DIMENSIONLESS) # Dimensionless
83
- ANGLE_SOLID = DimensionSignature(BaseDimension.DIMENSIONLESS) # Dimensionless
84
- ANGULAR_ACCELERATION = DimensionSignature.create(time=-2) # T^-2
85
- ANGULAR_MOMENTUM = DimensionSignature.create(length=2, mass=1, time=-1) # L^2 M T^-1
86
- AREA = DimensionSignature.create(length=2) # L^2
87
- AREA_PER_UNIT_VOLUME = DimensionSignature.create(length=-1) # L^-1
88
- ATOMIC_WEIGHT = DimensionSignature.create(amount=-1, mass=1) # N^-1 M
89
- CONCENTRATION = DimensionSignature.create(length=-3, mass=1) # L^-3 M
90
- CURRENT = DimensionSignature.create(current=1) # A
91
- DIMENSIONLESS = DimensionSignature(BaseDimension.DIMENSIONLESS) # Dimensionless
92
- DYNAMIC_FLUIDITY = DimensionSignature.create(length=1, mass=-1, time=1) # L M^-1 T
93
- ELECTRICAL_CONDUCTANCE = DimensionSignature.create(current=2, length=-2, mass=-1, time=3) # A^2 L^-2 M^-1 T^3
94
- ELECTRICAL_PERMITTIVITY = DimensionSignature.create(current=2, length=-3, mass=-1, time=4) # A^2 L^-3 M^-1 T^4
95
- ELECTRICAL_RESISTIVITY = DimensionSignature.create(current=-2, length=3, mass=1, time=-3) # A^-2 L^3 M T^-3
96
- ELECTRIC_CAPACITANCE = DimensionSignature.create(current=2, length=-2, mass=-1, time=4) # A^2 L^-2 M^-1 T^4
97
- ELECTRIC_CHARGE = DimensionSignature.create(amount=-1, current=1, time=1) # N^-1 A T
98
- ELECTRIC_CURRENT_INTENSITY = DimensionSignature.create(current=1) # A
99
- ELECTRIC_DIPOLE_MOMENT = DimensionSignature.create(current=1, length=1, time=1) # A L T
100
- ELECTRIC_FIELD_STRENGTH = DimensionSignature.create(current=-1, length=1, mass=1, time=-3) # A^-1 L M T^-3
101
- ELECTRIC_INDUCTANCE = DimensionSignature.create(current=-2, length=2, mass=1, time=-2) # A^-2 L^2 M T^-2
102
- ELECTRIC_POTENTIAL = DimensionSignature.create(current=-1, length=2, mass=1, time=-3) # A^-1 L^2 M T^-3
103
- ELECTRIC_RESISTANCE = DimensionSignature.create(current=-2, length=2, mass=1, time=-3) # A^-2 L^2 M T^-3
104
- ENERGY_FLUX = DimensionSignature.create(mass=1, time=-3) # M T^-3
105
- ENERGY_HEAT_WORK = DimensionSignature.create(length=2, mass=1, time=-2) # L^2 M T^-2
106
- ENERGY_PER_UNIT_AREA = DimensionSignature.create(mass=1, time=-2) # M T^-2
107
- FORCE = DimensionSignature.create(length=1, mass=1, time=-2) # L M T^-2
108
- FORCE_BODY = DimensionSignature.create(length=-2, mass=1, time=-2) # L^-2 M T^-2
109
- FORCE_PER_UNIT_MASS = DimensionSignature.create(length=1, time=-2) # L T^-2
110
- FREQUENCY_VOLTAGE_RATIO = DimensionSignature.create(current=1, length=-2, mass=-1, time=3) # A L^-2 M^-1 T^3
111
- FUEL_CONSUMPTION = DimensionSignature.create(length=-2) # L^-2
112
- HEAT_OF_COMBUSTION = DimensionSignature.create(length=2, time=-2) # L^2 T^-2
113
- HEAT_OF_FUSION = DimensionSignature.create(length=2, time=-2) # L^2 T^-2
114
- HEAT_OF_VAPORIZATION = DimensionSignature.create(length=2, time=-2) # L^2 T^-2
115
- HEAT_TRANSFER_COEFFICIENT = DimensionSignature.create(mass=1, temp=-1, time=-3) # M Θ^-1 T^-3
116
- ILLUMINANCE = DimensionSignature.create(length=-2, luminosity=1) # L^-2 J
117
- KINETIC_ENERGY_OF_TURBULENCE = DimensionSignature.create(length=2, time=-2) # L^2 T^-2
118
- LENGTH = DimensionSignature.create(length=1) # L
119
- LINEAR_MASS_DENSITY = DimensionSignature.create(length=-1, mass=1) # L^-1 M
120
- LINEAR_MOMENTUM = DimensionSignature.create(length=1, mass=1, time=-1) # L M T^-1
121
- LUMINANCE_SELF = DimensionSignature.create(length=-2, luminosity=1) # L^-2 J
122
- LUMINOSITY = DimensionSignature.create(luminosity=1) # J
123
- LUMINOUS_FLUX = DimensionSignature.create(luminosity=1) # J
124
- LUMINOUS_INTENSITY = DimensionSignature.create(luminosity=1) # J
125
- MAGNETIC_FIELD = DimensionSignature.create(current=1, length=-1) # A L^-1
126
- MAGNETIC_FLUX = DimensionSignature.create(current=-1, length=2, mass=1, time=-2) # A^-1 L^2 M T^-2
127
- MAGNETIC_INDUCTION_FIELD_STRENGTH = DimensionSignature.create(current=-1, mass=1, time=-2) # A^-1 M T^-2
128
- MAGNETIC_MOMENT = DimensionSignature.create(current=1, length=2) # A L^2
129
- MAGNETIC_PERMEABILITY = DimensionSignature.create(current=-2, length=2, mass=1, time=-2) # A^-2 L^2 M T^-2
130
- MAGNETOMOTIVE_FORCE = DimensionSignature.create(current=1) # A
131
- MASS = DimensionSignature.create(mass=1) # M
132
- MASS_DENSITY = DimensionSignature.create(length=-3, mass=1) # L^-3 M
133
- MASS_FLOW_RATE = DimensionSignature.create(mass=1, time=-1) # M T^-1
134
- MASS_FLUX = DimensionSignature.create(length=-2, mass=1, time=-1) # L^-2 M T^-1
135
- MASS_FRACTION_OF_I = DimensionSignature(BaseDimension.DIMENSIONLESS) # Dimensionless
136
- MASS_TRANSFER_COEFFICIENT = DimensionSignature.create(length=-2, mass=1, time=-1) # L^-2 M T^-1
137
- MOLALITY_OF_SOLUTE_I = DimensionSignature.create(amount=1, mass=-1) # N M^-1
138
- MOLARITY_OF_I = DimensionSignature.create(amount=1, length=-3) # N L^-3
139
- MOLAR_CONCENTRATION_BY_MASS = DimensionSignature.create(amount=1) # N
140
- MOLAR_FLOW_RATE = DimensionSignature.create(amount=1, time=-1) # N T^-1
141
- MOLAR_FLUX = DimensionSignature.create(amount=1, length=-2, time=-1) # N L^-2 T^-1
142
- MOLAR_HEAT_CAPACITY = DimensionSignature.create(amount=-1, length=2, temp=-1, time=-2) # N^-1 L^2 Θ^-1 T^-2
143
- MOLE_FRACTION_OF_I = DimensionSignature(BaseDimension.DIMENSIONLESS) # Dimensionless
144
- MOMENTUM_FLOW_RATE = DimensionSignature.create(length=1, mass=1, time=-2) # L M T^-2
145
- MOMENTUM_FLUX = DimensionSignature.create(length=-1, mass=1, time=-2) # L^-1 M T^-2
146
- MOMENT_OF_INERTIA = DimensionSignature.create(length=2, mass=1) # L^2 M
147
- NORMALITY_OF_SOLUTION = DimensionSignature.create(amount=1, length=-3) # N L^-3
148
- PARTICLE_DENSITY = DimensionSignature.create(length=-3) # L^-3
149
- PERCENT = DimensionSignature(BaseDimension.DIMENSIONLESS) # Dimensionless
150
- PERMEABILITY = DimensionSignature.create(length=2) # L^2
151
- PHOTON_EMISSION_RATE = DimensionSignature.create(length=-2, time=-1) # L^-2 T^-1
152
- POWER_PER_UNIT_MASS = DimensionSignature.create(length=2, time=-3) # L^2 T^-3
153
- POWER_PER_UNIT_VOLUME = DimensionSignature.create(length=-1, mass=1, time=-3) # L^-1 M T^-3
154
- POWER_THERMAL_DUTY = DimensionSignature.create(length=2, mass=1, time=-3) # L^2 M T^-3
155
- PRESSURE = DimensionSignature.create(length=-1, mass=1, time=-2) # L^-1 M T^-2
156
- RADIATION_DOSE_EQUIVALENT = DimensionSignature.create(length=2, time=-2) # L^2 T^-2
157
- RADIATION_EXPOSURE = DimensionSignature.create(current=1, mass=-1, time=1) # A M^-1 T
158
- RADIOACTIVITY = DimensionSignature.create(time=-1) # T^-1
159
- SECOND_MOMENT_OF_AREA = DimensionSignature.create(length=4) # L^4
160
- SECOND_RADIATION_CONSTANT_PLANCK = DimensionSignature.create(length=1, temp=1) # L Θ
161
- SPECIFIC_ENTHALPY = DimensionSignature.create(length=2, time=-2) # L^2 T^-2
162
- SPECIFIC_GRAVITY = DimensionSignature(BaseDimension.DIMENSIONLESS) # Dimensionless
163
- SPECIFIC_HEAT_CAPACITY_CONSTANT_PRESSURE = DimensionSignature.create(length=2, mass=1, temp=-1, time=-2) # L^2 M Θ^-1 T^-2
164
- SPECIFIC_LENGTH = DimensionSignature.create(length=1, mass=-1) # L M^-1
165
- SPECIFIC_SURFACE = DimensionSignature.create(length=2, mass=-1) # L^2 M^-1
166
- SPECIFIC_VOLUME = DimensionSignature.create(length=3, mass=-1) # L^3 M^-1
167
- STRESS = DimensionSignature.create(length=-1, mass=1, time=-2) # L^-1 M T^-2
168
- SURFACE_MASS_DENSITY = DimensionSignature.create(length=-2, mass=1) # L^-2 M
169
- SURFACE_TENSION = DimensionSignature.create(mass=1, time=-2) # M T^-2
170
- TEMPERATURE = DimensionSignature.create(temp=1) # Θ
171
- THERMAL_CONDUCTIVITY = DimensionSignature.create(length=1, mass=1, temp=1, time=-3) # L M Θ T^-3
172
- TIME = DimensionSignature.create(time=1) # T
173
- TORQUE = DimensionSignature.create(length=2, mass=1, time=-2) # L^2 M T^-2
174
- TURBULENCE_ENERGY_DISSIPATION_RATE = DimensionSignature.create(length=2, time=-3) # L^2 T^-3
175
- VELOCITY_ANGULAR = DimensionSignature.create(time=-1) # T^-1
176
- VELOCITY_LINEAR = DimensionSignature.create(length=1, time=-1) # L T^-1
177
- VISCOSITY_DYNAMIC = DimensionSignature.create(length=-1, mass=1, time=-1) # L^-1 M T^-1
178
- VISCOSITY_KINEMATIC = DimensionSignature.create(length=2, time=-1) # L^2 T^-1
179
- VOLUME = DimensionSignature.create(length=3) # L^3
180
- VOLUMETRIC_CALORIFIC_HEATING_VALUE = DimensionSignature.create(length=-1, mass=1, time=-2) # L^-1 M T^-2
181
- VOLUMETRIC_COEFFICIENT_OF_EXPANSION = DimensionSignature.create(length=-3, mass=1, temp=-1) # L^-3 M Θ^-1
182
- VOLUMETRIC_FLOW_RATE = DimensionSignature.create(length=3, time=-1) # L^3 T^-1
183
- VOLUMETRIC_FLUX = DimensionSignature.create(length=1, time=-1) # L T^-1
184
- VOLUMETRIC_MASS_FLOW_RATE = DimensionSignature.create(length=-3, mass=1, time=-1) # L^-3 M T^-1
185
- VOLUME_FRACTION_OF_I = DimensionSignature(BaseDimension.DIMENSIONLESS) # Dimensionless
186
- WAVENUMBER = DimensionSignature.create(length=-1) # L^-1
qnty/equation.py DELETED
@@ -1,297 +0,0 @@
1
- """
2
- Equation System
3
- ===============
4
-
5
- Mathematical equations for qnty variables with solving capabilities.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from typing import cast
11
-
12
- from .expression import Expression, VariableReference
13
- from .variable import TypeSafeVariable
14
-
15
-
16
- class Equation:
17
- """Represents a mathematical equation with left-hand side equal to right-hand side."""
18
-
19
- def __init__(self, name: str, lhs: TypeSafeVariable | Expression, rhs: Expression):
20
- self.name = name
21
-
22
- # Convert Variable to VariableReference if needed
23
- # Use duck typing to avoid circular import
24
- if hasattr(lhs, 'name') and hasattr(lhs, 'quantity') and hasattr(lhs, 'is_known'):
25
- # It's a TypeSafeVariable-like object
26
- self.lhs = VariableReference(cast('TypeSafeVariable', lhs))
27
- else:
28
- # It's already an Expression
29
- self.lhs = cast(Expression, lhs)
30
-
31
- self.rhs = rhs
32
- self.variables = self.get_all_variables()
33
-
34
- def get_all_variables(self) -> set[str]:
35
- """Get all variable names used in this equation."""
36
- # Both lhs and rhs should be Expressions after __init__ conversion
37
- lhs_vars = self.lhs.get_variables()
38
- rhs_vars = self.rhs.get_variables()
39
- return lhs_vars | rhs_vars
40
-
41
- def get_unknown_variables(self, known_vars: set[str]) -> set[str]:
42
- """Get variables that are unknown (not in known_vars set)."""
43
- return self.variables - known_vars
44
-
45
- def get_known_variables(self, known_vars: set[str]) -> set[str]:
46
- """Get variables that are known (in known_vars set)."""
47
- return self.variables & known_vars
48
-
49
- def can_solve_for(self, target_var: str, known_vars: set[str]) -> bool:
50
- """Check if this equation can solve for target_var given known_vars."""
51
- if target_var not in self.variables:
52
- return False
53
- # Direct assignment case: lhs is the variable
54
- if isinstance(self.lhs, VariableReference) and self.lhs.name == target_var:
55
- rhs_vars = self.rhs.get_variables()
56
- return rhs_vars.issubset(known_vars)
57
- unknown_vars = self.get_unknown_variables(known_vars)
58
- # Can solve if target_var is the only unknown
59
- return unknown_vars == {target_var}
60
-
61
- def solve_for(self, target_var: str, variable_values: dict[str, TypeSafeVariable]) -> TypeSafeVariable:
62
- """
63
- Solve the equation for target_var.
64
- Returns the target variable with updated quantity.
65
- """
66
- if target_var not in self.variables:
67
- raise ValueError(f"Variable '{target_var}' not found in equation")
68
-
69
- # Handle direct assignment: target = expression
70
- if isinstance(self.lhs, VariableReference) and self.lhs.name == target_var:
71
- # Direct assignment: target_var = rhs
72
- result_qty = self.rhs.evaluate(variable_values)
73
-
74
- # Update existing variable object to preserve references
75
- var_obj = variable_values.get(target_var)
76
- if var_obj is not None:
77
- # Convert result to the target variable's original unit if it had one
78
- if var_obj.quantity is not None and var_obj.quantity.unit is not None:
79
- # Convert to the target variable's defined unit
80
- try:
81
- result_qty = result_qty.to(var_obj.quantity.unit)
82
- except Exception:
83
- # If conversion fails, keep the calculated unit
84
- pass
85
-
86
- var_obj.quantity = result_qty
87
- var_obj.is_known = True
88
- return var_obj
89
-
90
- # Create new variable if not found - this shouldn't happen in normal usage
91
- raise ValueError(f"Variable '{target_var}' not found in variable_values")
92
-
93
- # For more complex equations, we would need algebraic manipulation
94
- # Currently focusing on direct assignment which covers most engineering cases
95
- raise NotImplementedError(f"Cannot solve for {target_var} in equation {self}. "
96
- f"Only direct assignment equations (var = expression) are supported.")
97
-
98
- def check_residual(self, variable_values: dict[str, TypeSafeVariable], tolerance: float = 1e-10) -> bool:
99
- """
100
- Check if equation is satisfied by evaluating residual (LHS - RHS).
101
- Returns True if |residual| < tolerance, accounting for units.
102
- """
103
- try:
104
- # Both lhs and rhs should be Expressions after __init__ conversion
105
- lhs_value = self.lhs.evaluate(variable_values)
106
- rhs_value = self.rhs.evaluate(variable_values)
107
-
108
- # Check dimensional compatibility
109
- if lhs_value._dimension_sig != rhs_value._dimension_sig:
110
- return False
111
-
112
- # Convert to same units for comparison
113
- rhs_converted = rhs_value.to(lhs_value.unit)
114
- residual = abs(lhs_value.value - rhs_converted.value)
115
-
116
- return residual < tolerance
117
- except Exception:
118
- return False
119
-
120
- def _discover_variables_from_scope(self) -> dict[str, TypeSafeVariable]:
121
- """Automatically discover variables from the calling scope."""
122
- import inspect
123
-
124
- # Get the frame that called this method (skip through __str__ calls)
125
- frame = inspect.currentframe()
126
- try:
127
- # Skip frames until we find one outside the equation system
128
- while frame and (
129
- frame.f_code.co_filename.endswith(('equation.py', 'expression.py')) or
130
- frame.f_code.co_name in ['__str__', '__repr__']
131
- ):
132
- frame = frame.f_back
133
-
134
- if not frame:
135
- return {}
136
-
137
- # Combine local and global variables
138
- all_vars = {**frame.f_globals, **frame.f_locals}
139
-
140
- # Find TypeSafeVariable objects that match our required variables
141
- required_vars = self.variables
142
- discovered = {}
143
-
144
- for var_name in required_vars:
145
- for name, obj in all_vars.items():
146
- if hasattr(obj, 'symbol') and obj.symbol == var_name:
147
- discovered[var_name] = obj
148
- break
149
- elif hasattr(obj, 'name') and obj.name == var_name:
150
- discovered[var_name] = obj
151
- break
152
-
153
- return discovered
154
-
155
- finally:
156
- del frame
157
-
158
- def _can_auto_solve(self) -> tuple[bool, str, dict[str, TypeSafeVariable]]:
159
- """Check if equation can be auto-solved from scope."""
160
- try:
161
- discovered = self._discover_variables_from_scope()
162
-
163
- # Check if this is a simple assignment equation (one unknown)
164
- unknowns = []
165
- knowns = []
166
-
167
- for var_name in self.variables:
168
- if var_name in discovered:
169
- var = discovered[var_name]
170
- if hasattr(var, 'is_known') and not var.is_known:
171
- unknowns.append(var_name)
172
- elif hasattr(var, 'quantity') and var.quantity is not None:
173
- knowns.append(var_name)
174
- else:
175
- unknowns.append(var_name) # Assume unknown if no quantity
176
- else:
177
- return False, "", {} # Missing variable
178
-
179
- # Can only auto-solve if there's exactly one unknown
180
- if len(unknowns) == 1:
181
- return True, unknowns[0], discovered
182
-
183
- return False, "", {}
184
-
185
- except Exception:
186
- return False, "", {}
187
-
188
- def _try_auto_solve(self) -> bool:
189
- """Try to automatically solve the equation if possible."""
190
- try:
191
- can_solve, target_var, variables = self._can_auto_solve()
192
- if can_solve:
193
- self.solve_for(target_var, variables)
194
- return True
195
- return False
196
- except Exception:
197
- return False
198
-
199
- def __str__(self) -> str:
200
- # Try to auto-solve if possible before displaying
201
- self._try_auto_solve()
202
- return f"{self.lhs} = {self.rhs}"
203
-
204
- def __repr__(self) -> str:
205
- return f"Equation(name='{self.name}', lhs={self.lhs!r}, rhs={self.rhs!r})"
206
-
207
-
208
- class EquationSystem:
209
- """System of equations that can be solved together."""
210
-
211
- def __init__(self, equations: list[Equation] | None = None):
212
- self.equations = equations or []
213
- self.variables = {} # Dict[str, TypeSafeVariable]
214
-
215
- def add_equation(self, equation: Equation):
216
- """Add an equation to the system."""
217
- self.equations.append(equation)
218
-
219
- def add_variable(self, variable: TypeSafeVariable):
220
- """Add a variable to the system."""
221
- self.variables[variable.name] = variable
222
-
223
- def get_known_variables(self) -> set[str]:
224
- """Get names of all known variables."""
225
- return {name for name, var in self.variables.items() if var.is_known and var.quantity is not None}
226
-
227
- def get_unknown_variables(self) -> set[str]:
228
- """Get names of all unknown variables."""
229
- return {name for name, var in self.variables.items() if not var.is_known or var.quantity is None}
230
-
231
- def can_solve_any(self) -> bool:
232
- """Check if any equation can be solved with current known variables."""
233
- known_vars = self.get_known_variables()
234
- unknown_vars = self.get_unknown_variables()
235
-
236
- for equation in self.equations:
237
- for unknown_var in unknown_vars:
238
- if equation.can_solve_for(unknown_var, known_vars):
239
- return True
240
- return False
241
-
242
- def solve_step(self) -> bool:
243
- """Solve one step - find and solve one equation. Returns True if progress made."""
244
- known_vars = self.get_known_variables()
245
- unknown_vars = self.get_unknown_variables()
246
-
247
- # Find an equation that can be solved
248
- for equation in self.equations:
249
- for unknown_var in unknown_vars:
250
- if equation.can_solve_for(unknown_var, known_vars):
251
- # Solve for this variable
252
- equation.solve_for(unknown_var, self.variables)
253
- return True # Progress made
254
-
255
- return False # No progress possible
256
-
257
- def solve(self, max_iterations: int = 100) -> bool:
258
- """Solve the system iteratively. Returns True if fully solved."""
259
- for _ in range(max_iterations):
260
- if not self.can_solve_any():
261
- break
262
- if not self.solve_step():
263
- break
264
-
265
- # Check if all variables are known
266
- unknown_vars = self.get_unknown_variables()
267
- return len(unknown_vars) == 0
268
-
269
- def get_solving_order(self) -> list[str]:
270
- """Get the order in which variables can be solved."""
271
- order = []
272
- temp_system = EquationSystem(self.equations.copy())
273
- temp_system.variables = self.variables.copy()
274
-
275
- while temp_system.can_solve_any():
276
- known_vars = temp_system.get_known_variables()
277
- unknown_vars = temp_system.get_unknown_variables()
278
-
279
- # Find next solvable variable
280
- for equation in temp_system.equations:
281
- for unknown_var in unknown_vars:
282
- if equation.can_solve_for(unknown_var, known_vars):
283
- order.append(unknown_var)
284
- # Mark as known for next iteration
285
- temp_system.variables[unknown_var].is_known = True
286
- break
287
- else:
288
- continue
289
- break
290
-
291
- return order
292
-
293
- def __str__(self) -> str:
294
- return f"EquationSystem({len(self.equations)} equations, {len(self.variables)} variables)"
295
-
296
- def __repr__(self) -> str:
297
- return f"EquationSystem(equations={self.equations!r})"