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
@@ -0,0 +1,1012 @@
|
|
1
|
+
"""
|
2
|
+
Refactored Unified Variable System
|
3
|
+
=================================
|
4
|
+
|
5
|
+
This module provides a streamlined variable system with improved architecture:
|
6
|
+
|
7
|
+
Key Refactoring Improvements:
|
8
|
+
- **Encapsulated Global State**: CacheManager replaces global variables
|
9
|
+
- **Simplified Mixins**: Reduced from 6 complex mixins to 5 focused ones
|
10
|
+
- **Eliminated Code Duplication**: ArithmeticDispatcher centralizes operations
|
11
|
+
- **Simplified Constructor Logic**: ConstructorProcessor handles complex initialization
|
12
|
+
- **Better Separation of Concerns**: Each component has a single responsibility
|
13
|
+
- **Maintained Backward Compatibility**: All existing APIs work unchanged
|
14
|
+
|
15
|
+
Architecture:
|
16
|
+
- UnifiedVariable: Main class with focused mixins
|
17
|
+
- QuantityManagementMixin: Core quantity storage and state
|
18
|
+
- FlexibleConstructorMixin: Simplified argument processing
|
19
|
+
- UnifiedArithmeticMixin: User-controllable arithmetic operations
|
20
|
+
- ExpressionMixin: Equation and comparison operations
|
21
|
+
- SetterCompatibilityMixin: Backward compatibility with setter system
|
22
|
+
- ErrorHandlerMixin: Consistent error handling
|
23
|
+
"""
|
24
|
+
|
25
|
+
from __future__ import annotations
|
26
|
+
|
27
|
+
from typing import TYPE_CHECKING, Any, Self
|
28
|
+
|
29
|
+
from qnty.utils.error_handling import ErrorHandlerMixin
|
30
|
+
|
31
|
+
if TYPE_CHECKING:
|
32
|
+
from ..dimensions import DimensionSignature
|
33
|
+
from ..equations import Equation
|
34
|
+
from ..expressions import Expression
|
35
|
+
from .base_qnty import Quantity, TypeSafeSetter
|
36
|
+
|
37
|
+
|
38
|
+
class QuantityManagementMixin:
|
39
|
+
"""Handles core quantity storage and state management."""
|
40
|
+
|
41
|
+
def __init__(self):
|
42
|
+
self._quantity: Quantity | None = None
|
43
|
+
self._is_known: bool = False
|
44
|
+
self._name: str = ""
|
45
|
+
self._symbol: str | None = None
|
46
|
+
|
47
|
+
@property
|
48
|
+
def quantity(self) -> Quantity | None:
|
49
|
+
"""Get the underlying quantity."""
|
50
|
+
return self._quantity
|
51
|
+
|
52
|
+
@quantity.setter
|
53
|
+
def quantity(self, value: Quantity | None) -> None:
|
54
|
+
"""Set the underlying quantity."""
|
55
|
+
self._quantity = value
|
56
|
+
if value is not None:
|
57
|
+
self._is_known = True
|
58
|
+
|
59
|
+
@property
|
60
|
+
def is_known(self) -> bool:
|
61
|
+
"""Check if variable has a known value."""
|
62
|
+
return self._is_known and self._quantity is not None
|
63
|
+
|
64
|
+
@is_known.setter
|
65
|
+
def is_known(self, value: bool) -> None:
|
66
|
+
"""Set known status for backward compatibility."""
|
67
|
+
self._is_known = value
|
68
|
+
|
69
|
+
def mark_known(self) -> Self:
|
70
|
+
"""Mark variable as having a known value."""
|
71
|
+
self._is_known = True
|
72
|
+
return self
|
73
|
+
|
74
|
+
def mark_unknown(self) -> Self:
|
75
|
+
"""Mark variable as unknown."""
|
76
|
+
self._is_known = False
|
77
|
+
return self
|
78
|
+
|
79
|
+
def update(self, value: Any = None, unit: Any = None, quantity: Quantity | None = None, is_known: bool | None = None) -> Self:
|
80
|
+
"""Flexible update method for variable properties."""
|
81
|
+
if quantity is not None:
|
82
|
+
self.quantity = quantity
|
83
|
+
elif value is not None and unit is not None:
|
84
|
+
# Create new quantity from value and unit using existing setter system
|
85
|
+
if hasattr(self, "_create_quantity_from_value_unit"):
|
86
|
+
self.quantity = self._create_quantity_from_value_unit(value, unit) # type: ignore[misc]
|
87
|
+
|
88
|
+
if is_known is not None:
|
89
|
+
self._is_known = is_known
|
90
|
+
|
91
|
+
return self
|
92
|
+
|
93
|
+
|
94
|
+
class FlexibleConstructorMixin:
|
95
|
+
"""Handles flexible variable initialization patterns maintaining backward compatibility."""
|
96
|
+
|
97
|
+
def __init__(self):
|
98
|
+
# Only set instance attributes if not already set by class definition
|
99
|
+
# This preserves class attributes from generated quantities
|
100
|
+
if not hasattr(self, "_setter_class"):
|
101
|
+
self._setter_class: type | None = None
|
102
|
+
if not hasattr(self, "_unit_mappings"):
|
103
|
+
self._unit_mappings: dict[str, str] = {}
|
104
|
+
|
105
|
+
def _initialize_from_args(self, *args, **kwargs) -> None:
|
106
|
+
"""Process constructor arguments with full backward compatibility."""
|
107
|
+
is_dimensionless = self.__class__.__name__ == "Dimensionless"
|
108
|
+
|
109
|
+
if len(args) == 1 and isinstance(args[0], str):
|
110
|
+
# Pattern: Variable("name") - may have is_known in kwargs
|
111
|
+
self._name = args[0]
|
112
|
+
self._is_known = kwargs.get("is_known", False)
|
113
|
+
|
114
|
+
elif len(args) == 2:
|
115
|
+
if is_dimensionless:
|
116
|
+
# Dimensionless pattern: Variable(value, "name")
|
117
|
+
value, name = args
|
118
|
+
self._name = name
|
119
|
+
self._is_known = True
|
120
|
+
# For dimensionless, create quantity directly
|
121
|
+
from ..units import DimensionlessUnits
|
122
|
+
from .base_qnty import Quantity
|
123
|
+
|
124
|
+
self.quantity = Quantity(value, DimensionlessUnits.dimensionless)
|
125
|
+
elif isinstance(args[1], str):
|
126
|
+
# Pattern: Variable(value, unit) - needs name from kwargs or auto-generated
|
127
|
+
value, unit = args
|
128
|
+
self._initialize_with_value_unit(value, unit)
|
129
|
+
elif isinstance(args[1], bool):
|
130
|
+
# Pattern: Variable(name, is_known)
|
131
|
+
name, is_known = args
|
132
|
+
self._name = name
|
133
|
+
self._is_known = is_known
|
134
|
+
|
135
|
+
elif len(args) == 3:
|
136
|
+
# Pattern: Variable(value, unit, name) - for dimensional quantities
|
137
|
+
value, unit, name = args
|
138
|
+
self._initialize_with_value_unit_name(value, unit, name)
|
139
|
+
|
140
|
+
elif len(args) == 4:
|
141
|
+
# Pattern: Variable(value, unit, name, is_known)
|
142
|
+
value, unit, name, is_known = args
|
143
|
+
self._initialize_with_value_unit_name(value, unit, name)
|
144
|
+
self._is_known = is_known
|
145
|
+
|
146
|
+
# Handle keyword arguments
|
147
|
+
if "name" in kwargs:
|
148
|
+
self._name = kwargs["name"]
|
149
|
+
if "is_known" in kwargs:
|
150
|
+
self._is_known = kwargs["is_known"]
|
151
|
+
if "quantity" in kwargs:
|
152
|
+
self.quantity = kwargs["quantity"]
|
153
|
+
|
154
|
+
# Set default name if not provided
|
155
|
+
if not hasattr(self, "_name") or not self._name:
|
156
|
+
self._name = f"var_{id(self)}"
|
157
|
+
|
158
|
+
def _initialize_with_value_unit(self, value: Any, unit: Any) -> None:
|
159
|
+
"""Initialize with value and unit."""
|
160
|
+
if hasattr(self, "_create_quantity_from_value_unit"):
|
161
|
+
self.quantity = self._create_quantity_from_value_unit(value, unit)
|
162
|
+
self._is_known = True
|
163
|
+
|
164
|
+
def _initialize_with_value_unit_name(self, value: Any, unit: Any, name: str) -> None:
|
165
|
+
"""Initialize with value, unit, and name."""
|
166
|
+
self._name = name
|
167
|
+
self._initialize_with_value_unit(value, unit)
|
168
|
+
|
169
|
+
def _create_quantity_from_value_unit(self, value: Any, unit: Any) -> Quantity | None:
|
170
|
+
"""Create quantity from value and unit using setter system."""
|
171
|
+
if hasattr(self, "_setter_class") and self._setter_class is not None:
|
172
|
+
# Use the setter system like TypedQuantity does
|
173
|
+
setter = self._setter_class(self, value)
|
174
|
+
unit_prop = self._find_unit_property(setter, str(unit))
|
175
|
+
if unit_prop:
|
176
|
+
getattr(setter, unit_prop)
|
177
|
+
# Debug: check what self.quantity returns
|
178
|
+
result = self.quantity
|
179
|
+
# Ensure we return the quantity if it was successfully set
|
180
|
+
return result
|
181
|
+
|
182
|
+
# Fallback to direct quantity creation
|
183
|
+
from ..units import DimensionlessUnits
|
184
|
+
from .base_qnty import Quantity
|
185
|
+
|
186
|
+
# Try to find the unit in the registry or use dimensionless fallback
|
187
|
+
try:
|
188
|
+
from ..units.registry import registry
|
189
|
+
|
190
|
+
# Use a simple lookup approach since get_unit method may not exist
|
191
|
+
if hasattr(registry, "units") and str(unit) in registry.units:
|
192
|
+
unit_constant = registry.units[str(unit)] # type: ignore[assignment]
|
193
|
+
return Quantity(value, unit_constant) # type: ignore[arg-type,assignment]
|
194
|
+
except Exception:
|
195
|
+
pass
|
196
|
+
|
197
|
+
# Final fallback to dimensionless
|
198
|
+
return Quantity(value, DimensionlessUnits.dimensionless)
|
199
|
+
|
200
|
+
def _find_unit_property(self, setter: TypeSafeSetter, unit: str) -> str | None:
|
201
|
+
"""Find unit property with simple lookup."""
|
202
|
+
if not hasattr(self, "_setter_class") or self._setter_class is None:
|
203
|
+
return None
|
204
|
+
|
205
|
+
# Direct property lookup
|
206
|
+
if hasattr(setter, unit):
|
207
|
+
return unit
|
208
|
+
|
209
|
+
return None
|
210
|
+
|
211
|
+
|
212
|
+
class UnifiedArithmeticMixin:
|
213
|
+
"""Provides unified arithmetic operations with user-controllable return types."""
|
214
|
+
|
215
|
+
def __init__(self):
|
216
|
+
self._arithmetic_mode: str = "expression" # 'quantity', 'expression', 'auto'
|
217
|
+
|
218
|
+
def set_arithmetic_mode(self, mode: str) -> Self:
|
219
|
+
"""Set arithmetic return type preference."""
|
220
|
+
if mode not in ("quantity", "expression", "auto"):
|
221
|
+
raise ValueError(f"Invalid arithmetic mode: {mode}")
|
222
|
+
self._arithmetic_mode = mode
|
223
|
+
return self
|
224
|
+
|
225
|
+
def __add__(self, other) -> Any:
|
226
|
+
"""Unified addition with mode-based dispatch."""
|
227
|
+
return self._unified_add(self, other, self._arithmetic_mode)
|
228
|
+
|
229
|
+
def __radd__(self, other) -> Any:
|
230
|
+
"""Reverse addition."""
|
231
|
+
return self._unified_add(other, self, self._arithmetic_mode)
|
232
|
+
|
233
|
+
def __sub__(self, other) -> Any:
|
234
|
+
"""Unified subtraction with mode-based dispatch."""
|
235
|
+
return self._unified_subtract(self, other, self._arithmetic_mode)
|
236
|
+
|
237
|
+
def __rsub__(self, other) -> Any:
|
238
|
+
"""Reverse subtraction."""
|
239
|
+
return self._unified_subtract(other, self, self._arithmetic_mode)
|
240
|
+
|
241
|
+
def __mul__(self, other) -> Any:
|
242
|
+
"""Unified multiplication with mode-based dispatch."""
|
243
|
+
# Ultra-fast path for scalar multiplication when in quantity mode only
|
244
|
+
if (type(other) in (int, float) and getattr(self, 'quantity', None) is not None and
|
245
|
+
self._arithmetic_mode == "quantity"):
|
246
|
+
from .base_qnty import Quantity
|
247
|
+
return Quantity(self.quantity.value * other, self.quantity.unit) # type: ignore[attr-defined]
|
248
|
+
return self._unified_multiply(self, other, self._arithmetic_mode)
|
249
|
+
|
250
|
+
def __rmul__(self, other) -> Any:
|
251
|
+
"""Reverse multiplication."""
|
252
|
+
# Ultra-fast path for scalar multiplication when in quantity mode only
|
253
|
+
if (type(other) in (int, float) and getattr(self, 'quantity', None) is not None and
|
254
|
+
self._arithmetic_mode == "quantity"):
|
255
|
+
from .base_qnty import Quantity
|
256
|
+
return Quantity(other * self.quantity.value, self.quantity.unit) # type: ignore[attr-defined]
|
257
|
+
return self._unified_multiply(other, self, self._arithmetic_mode)
|
258
|
+
|
259
|
+
def __truediv__(self, other) -> Any:
|
260
|
+
"""Unified division with mode-based dispatch."""
|
261
|
+
# Ultra-fast path for scalar division when in quantity mode only
|
262
|
+
if (type(other) in (int, float) and getattr(self, 'quantity', None) is not None and
|
263
|
+
self._arithmetic_mode == "quantity"):
|
264
|
+
from .base_qnty import Quantity
|
265
|
+
return Quantity(self.quantity.value / other, self.quantity.unit) # type: ignore[attr-defined]
|
266
|
+
return self._unified_divide(self, other, self._arithmetic_mode)
|
267
|
+
|
268
|
+
def __rtruediv__(self, other) -> Any:
|
269
|
+
"""Reverse division."""
|
270
|
+
return self._unified_divide(other, self, self._arithmetic_mode)
|
271
|
+
|
272
|
+
def __pow__(self, other) -> Any:
|
273
|
+
"""Unified exponentiation with mode-based dispatch."""
|
274
|
+
return self._unified_power(self, other, self._arithmetic_mode)
|
275
|
+
|
276
|
+
def __rpow__(self, other) -> Any:
|
277
|
+
"""Reverse exponentiation."""
|
278
|
+
return self._unified_power(other, self, self._arithmetic_mode)
|
279
|
+
|
280
|
+
@staticmethod
|
281
|
+
def _should_return_quantity(left: Any, right: Any) -> bool:
|
282
|
+
"""Optimized check for quantity path eligibility."""
|
283
|
+
# Ultra-fast type checks using type() comparison
|
284
|
+
left_type = type(left)
|
285
|
+
right_type = type(right)
|
286
|
+
|
287
|
+
# Fast path: both primitives
|
288
|
+
if left_type in (int, float) and right_type in (int, float):
|
289
|
+
return True
|
290
|
+
|
291
|
+
# Fast path: one primitive, one variable
|
292
|
+
if left_type in (int, float):
|
293
|
+
# Use getattr for safe access, defaulting to False for missing attributes
|
294
|
+
return getattr(right, "is_known", False) and getattr(right, "quantity", None) is not None
|
295
|
+
if right_type in (int, float):
|
296
|
+
return getattr(left, "is_known", False) and getattr(left, "quantity", None) is not None
|
297
|
+
|
298
|
+
# Both complex objects - optimized checks
|
299
|
+
return (getattr(left, "is_known", False) and getattr(left, "quantity", None) is not None and
|
300
|
+
getattr(right, "is_known", False) and getattr(right, "quantity", None) is not None)
|
301
|
+
|
302
|
+
def _unified_add(self, left: Any, right: Any, return_type: str = "auto") -> Any:
|
303
|
+
"""Unified addition with controllable return type."""
|
304
|
+
if return_type == "quantity" or (return_type == "auto" and self._should_return_quantity(left, right)):
|
305
|
+
return self._quantity_add(left, right)
|
306
|
+
else:
|
307
|
+
return self._expression_add(left, right)
|
308
|
+
|
309
|
+
def _unified_subtract(self, left: Any, right: Any, return_type: str = "auto") -> Any:
|
310
|
+
"""Unified subtraction with controllable return type."""
|
311
|
+
if return_type == "quantity" or (return_type == "auto" and self._should_return_quantity(left, right)):
|
312
|
+
return self._quantity_subtract(left, right)
|
313
|
+
else:
|
314
|
+
return self._expression_subtract(left, right)
|
315
|
+
|
316
|
+
def _unified_multiply(self, left: Any, right: Any, return_type: str = "auto") -> Any:
|
317
|
+
"""Unified multiplication with controllable return type."""
|
318
|
+
if return_type == "quantity" or (return_type == "auto" and self._should_return_quantity(left, right)):
|
319
|
+
return self._quantity_multiply(left, right)
|
320
|
+
else:
|
321
|
+
return self._expression_multiply(left, right)
|
322
|
+
|
323
|
+
def _unified_divide(self, left: Any, right: Any, return_type: str = "auto") -> Any:
|
324
|
+
"""Unified division with controllable return type."""
|
325
|
+
if return_type == "quantity" or (return_type == "auto" and self._should_return_quantity(left, right)):
|
326
|
+
return self._quantity_divide(left, right)
|
327
|
+
else:
|
328
|
+
return self._expression_divide(left, right)
|
329
|
+
|
330
|
+
def _unified_power(self, left: Any, right: Any, return_type: str = "auto") -> Any:
|
331
|
+
"""Unified exponentiation with controllable return type."""
|
332
|
+
if return_type == "quantity" or (return_type == "auto" and self._should_return_quantity(left, right)):
|
333
|
+
return self._quantity_power(left, right)
|
334
|
+
else:
|
335
|
+
return self._expression_power(left, right)
|
336
|
+
|
337
|
+
# Cache for dimensionless quantities to reduce allocation
|
338
|
+
_DIMENSIONLESS_ZERO = None
|
339
|
+
_DIMENSIONLESS_ONE = None
|
340
|
+
_DIMENSIONLESS_CACHE = {} # Cache for common dimensionless values
|
341
|
+
|
342
|
+
@classmethod
|
343
|
+
def _get_dimensionless_quantity(cls, value: float):
|
344
|
+
"""Get cached dimensionless quantity for common values."""
|
345
|
+
# Initialize cache if needed
|
346
|
+
if cls._DIMENSIONLESS_ZERO is None:
|
347
|
+
from ..units import DimensionlessUnits
|
348
|
+
from .base_qnty import Quantity
|
349
|
+
|
350
|
+
cls._DIMENSIONLESS_ZERO = Quantity(0.0, DimensionlessUnits.dimensionless)
|
351
|
+
cls._DIMENSIONLESS_ONE = Quantity(1.0, DimensionlessUnits.dimensionless)
|
352
|
+
|
353
|
+
# Fast return for common values
|
354
|
+
if value == 0.0:
|
355
|
+
return cls._DIMENSIONLESS_ZERO
|
356
|
+
elif value == 1.0:
|
357
|
+
return cls._DIMENSIONLESS_ONE
|
358
|
+
elif value in cls._DIMENSIONLESS_CACHE:
|
359
|
+
return cls._DIMENSIONLESS_CACHE[value]
|
360
|
+
else:
|
361
|
+
# Create new quantity and cache if it's a common value
|
362
|
+
from ..units import DimensionlessUnits
|
363
|
+
from .base_qnty import Quantity
|
364
|
+
|
365
|
+
qty = Quantity(value, DimensionlessUnits.dimensionless)
|
366
|
+
|
367
|
+
# Cache common integer values
|
368
|
+
if isinstance(value, int) and -10 <= value <= 10:
|
369
|
+
cls._DIMENSIONLESS_CACHE[value] = qty
|
370
|
+
# Cache common decimal values
|
371
|
+
elif value in (-1.0, 0.5, 2.0, -0.5):
|
372
|
+
cls._DIMENSIONLESS_CACHE[value] = qty
|
373
|
+
|
374
|
+
return qty
|
375
|
+
|
376
|
+
@classmethod
|
377
|
+
def _quantity_add(cls, left: Any, right: Any):
|
378
|
+
"""Fast path addition returning Quantity with optimized quantity creation."""
|
379
|
+
from ..expressions import BinaryOperation
|
380
|
+
from .base_qnty import ArithmeticOperations
|
381
|
+
|
382
|
+
# Fast path for numeric types
|
383
|
+
left_type = type(left)
|
384
|
+
right_type = type(right)
|
385
|
+
|
386
|
+
if left_type in (int, float):
|
387
|
+
left_qty = cls._get_dimensionless_quantity(float(left))
|
388
|
+
else:
|
389
|
+
left_qty = left.quantity if hasattr(left, "quantity") else left
|
390
|
+
|
391
|
+
if right_type in (int, float):
|
392
|
+
right_qty = cls._get_dimensionless_quantity(float(right))
|
393
|
+
else:
|
394
|
+
right_qty = right.quantity if hasattr(right, "quantity") else right
|
395
|
+
|
396
|
+
# If either operand has no quantity, fall back to expression mode
|
397
|
+
if left_qty is None or right_qty is None:
|
398
|
+
return BinaryOperation("+", left, right)
|
399
|
+
|
400
|
+
# Delegate to ArithmeticOperations for the actual computation
|
401
|
+
return ArithmeticOperations.add(left_qty, right_qty)
|
402
|
+
|
403
|
+
@staticmethod
|
404
|
+
def _expression_add(left: Any, right: Any):
|
405
|
+
"""Flexible path addition returning Expression."""
|
406
|
+
from ..expressions import BinaryOperation, wrap_operand
|
407
|
+
|
408
|
+
return BinaryOperation("+", wrap_operand(left), wrap_operand(right))
|
409
|
+
|
410
|
+
@classmethod
|
411
|
+
def _quantity_subtract(cls, left: Any, right: Any):
|
412
|
+
"""Fast path subtraction returning Quantity with optimized quantity creation."""
|
413
|
+
from .base_qnty import ArithmeticOperations
|
414
|
+
|
415
|
+
# Fast path for numeric types using cached dimensionless quantities
|
416
|
+
left_type = type(left)
|
417
|
+
right_type = type(right)
|
418
|
+
|
419
|
+
if left_type in (int, float):
|
420
|
+
left_qty = cls._get_dimensionless_quantity(float(left))
|
421
|
+
else:
|
422
|
+
left_qty = left.quantity if hasattr(left, "quantity") else left
|
423
|
+
|
424
|
+
if right_type in (int, float):
|
425
|
+
right_qty = cls._get_dimensionless_quantity(float(right))
|
426
|
+
else:
|
427
|
+
right_qty = right.quantity if hasattr(right, "quantity") else right
|
428
|
+
|
429
|
+
# Delegate to ArithmeticOperations for the actual computation
|
430
|
+
return ArithmeticOperations.subtract(left_qty, right_qty)
|
431
|
+
|
432
|
+
@staticmethod
|
433
|
+
def _expression_subtract(left: Any, right: Any):
|
434
|
+
"""Flexible path subtraction returning Expression."""
|
435
|
+
from ..expressions import BinaryOperation, wrap_operand
|
436
|
+
|
437
|
+
return BinaryOperation("-", wrap_operand(left), wrap_operand(right))
|
438
|
+
|
439
|
+
@classmethod
|
440
|
+
def _quantity_multiply(cls, left: Any, right: Any):
|
441
|
+
"""Fast path multiplication returning Quantity with optimized quantity creation."""
|
442
|
+
from .base_qnty import ArithmeticOperations
|
443
|
+
|
444
|
+
# Fast path for numeric types using cached dimensionless quantities
|
445
|
+
left_type = type(left)
|
446
|
+
right_type = type(right)
|
447
|
+
|
448
|
+
if left_type in (int, float):
|
449
|
+
left_qty = cls._get_dimensionless_quantity(float(left))
|
450
|
+
else:
|
451
|
+
left_qty = left.quantity if hasattr(left, "quantity") else left
|
452
|
+
|
453
|
+
if right_type in (int, float):
|
454
|
+
right_qty = cls._get_dimensionless_quantity(float(right))
|
455
|
+
else:
|
456
|
+
right_qty = right.quantity if hasattr(right, "quantity") else right
|
457
|
+
|
458
|
+
# Delegate to ArithmeticOperations for the actual computation
|
459
|
+
return ArithmeticOperations.multiply(left_qty, right_qty)
|
460
|
+
|
461
|
+
@staticmethod
|
462
|
+
def _expression_multiply(left: Any, right: Any):
|
463
|
+
"""Optimized flexible path multiplication returning Expression."""
|
464
|
+
from ..expressions import BinaryOperation, wrap_operand
|
465
|
+
|
466
|
+
# Fast path for scalar operations - avoid double wrap_operand calls
|
467
|
+
left_type = type(left)
|
468
|
+
right_type = type(right)
|
469
|
+
|
470
|
+
if left_type in (int, float) and right_type in (int, float):
|
471
|
+
# Both scalars - very rare but handle efficiently
|
472
|
+
from ..expressions.nodes import Constant, _get_dimensionless_quantity
|
473
|
+
left_const = Constant(_get_dimensionless_quantity(float(left)))
|
474
|
+
right_const = Constant(_get_dimensionless_quantity(float(right)))
|
475
|
+
return BinaryOperation("*", left_const, right_const)
|
476
|
+
elif left_type in (int, float):
|
477
|
+
# Left scalar, right variable - common case
|
478
|
+
from ..expressions.nodes import Constant, _get_dimensionless_quantity
|
479
|
+
left_const = Constant(_get_dimensionless_quantity(float(left)))
|
480
|
+
return BinaryOperation("*", left_const, wrap_operand(right))
|
481
|
+
elif right_type in (int, float):
|
482
|
+
# Left variable, right scalar - very common case
|
483
|
+
from ..expressions.nodes import Constant, _get_dimensionless_quantity
|
484
|
+
right_const = Constant(_get_dimensionless_quantity(float(right)))
|
485
|
+
return BinaryOperation("*", wrap_operand(left), right_const)
|
486
|
+
else:
|
487
|
+
# General case - both need wrapping
|
488
|
+
return BinaryOperation("*", wrap_operand(left), wrap_operand(right))
|
489
|
+
|
490
|
+
@classmethod
|
491
|
+
def _quantity_divide(cls, left: Any, right: Any):
|
492
|
+
"""Fast path division returning Quantity with optimized quantity creation."""
|
493
|
+
from .base_qnty import ArithmeticOperations
|
494
|
+
|
495
|
+
# Fast path for numeric types using cached dimensionless quantities
|
496
|
+
left_type = type(left)
|
497
|
+
right_type = type(right)
|
498
|
+
|
499
|
+
if left_type in (int, float):
|
500
|
+
left_qty = cls._get_dimensionless_quantity(float(left))
|
501
|
+
else:
|
502
|
+
left_qty = left.quantity if hasattr(left, "quantity") else left
|
503
|
+
|
504
|
+
if right_type in (int, float):
|
505
|
+
right_qty = cls._get_dimensionless_quantity(float(right))
|
506
|
+
else:
|
507
|
+
right_qty = right.quantity if hasattr(right, "quantity") else right
|
508
|
+
|
509
|
+
# Delegate to ArithmeticOperations for the actual computation
|
510
|
+
return ArithmeticOperations.divide(left_qty, right_qty)
|
511
|
+
|
512
|
+
@staticmethod
|
513
|
+
def _expression_divide(left: Any, right: Any):
|
514
|
+
"""Optimized flexible path division returning Expression."""
|
515
|
+
from ..expressions import BinaryOperation, wrap_operand
|
516
|
+
|
517
|
+
# Fast path for scalar operations - avoid double wrap_operand calls
|
518
|
+
left_type = type(left)
|
519
|
+
right_type = type(right)
|
520
|
+
|
521
|
+
if right_type in (int, float):
|
522
|
+
# Most common case: variable / scalar
|
523
|
+
from ..expressions.nodes import Constant, _get_dimensionless_quantity
|
524
|
+
right_const = Constant(_get_dimensionless_quantity(float(right)))
|
525
|
+
return BinaryOperation("/", wrap_operand(left), right_const)
|
526
|
+
elif left_type in (int, float):
|
527
|
+
# Less common: scalar / variable
|
528
|
+
from ..expressions.nodes import Constant, _get_dimensionless_quantity
|
529
|
+
left_const = Constant(_get_dimensionless_quantity(float(left)))
|
530
|
+
return BinaryOperation("/", left_const, wrap_operand(right))
|
531
|
+
else:
|
532
|
+
# General case - both need wrapping
|
533
|
+
return BinaryOperation("/", wrap_operand(left), wrap_operand(right))
|
534
|
+
|
535
|
+
@classmethod
|
536
|
+
def _quantity_power(cls, left: Any, right: Any):
|
537
|
+
"""Fast path exponentiation returning Quantity with optimized handling."""
|
538
|
+
left_type = type(left)
|
539
|
+
right_type = type(right)
|
540
|
+
|
541
|
+
# Get quantities efficiently
|
542
|
+
if left_type in (int, float):
|
543
|
+
left_qty = cls._get_dimensionless_quantity(float(left))
|
544
|
+
else:
|
545
|
+
left_qty = left.quantity if hasattr(left, "quantity") else left
|
546
|
+
|
547
|
+
# For exponentiation, right operand is usually a scalar
|
548
|
+
if right_type in (int, float):
|
549
|
+
return left_qty**right
|
550
|
+
else:
|
551
|
+
right_qty = right.quantity if hasattr(right, "quantity") else right
|
552
|
+
return left_qty**right_qty
|
553
|
+
|
554
|
+
@staticmethod
|
555
|
+
def _expression_power(left: Any, right: Any):
|
556
|
+
"""Flexible path exponentiation returning Expression."""
|
557
|
+
from ..expressions import BinaryOperation, wrap_operand
|
558
|
+
|
559
|
+
return BinaryOperation("**", wrap_operand(left), wrap_operand(right))
|
560
|
+
|
561
|
+
|
562
|
+
class ExpressionMixin:
|
563
|
+
"""Provides expression and equation creation capabilities."""
|
564
|
+
|
565
|
+
def __init__(self):
|
566
|
+
pass # Properties will be handled by UnifiedVariable
|
567
|
+
|
568
|
+
def equals(self, other) -> Equation:
|
569
|
+
"""Create an equation: self = other."""
|
570
|
+
from ..equations import Equation
|
571
|
+
from ..expressions import wrap_operand
|
572
|
+
|
573
|
+
equation_name = f"{getattr(self, 'name', 'var')}_eq"
|
574
|
+
return Equation(name=equation_name, lhs=wrap_operand(self), rhs=wrap_operand(other)) # type: ignore[arg-type]
|
575
|
+
|
576
|
+
def lt(self, other) -> Expression:
|
577
|
+
"""Create less-than comparison expression."""
|
578
|
+
from ..expressions import BinaryOperation, wrap_operand
|
579
|
+
|
580
|
+
return BinaryOperation("<", wrap_operand(self), wrap_operand(other)) # type: ignore[arg-type]
|
581
|
+
|
582
|
+
def leq(self, other) -> Expression:
|
583
|
+
"""Create less-than-or-equal comparison expression."""
|
584
|
+
from ..expressions import BinaryOperation, wrap_operand
|
585
|
+
|
586
|
+
return BinaryOperation("<=", wrap_operand(self), wrap_operand(other)) # type: ignore[arg-type]
|
587
|
+
|
588
|
+
def geq(self, other) -> Expression:
|
589
|
+
"""Create greater-than-or-equal comparison expression."""
|
590
|
+
from ..expressions import BinaryOperation, wrap_operand
|
591
|
+
|
592
|
+
return BinaryOperation(">=", wrap_operand(self), wrap_operand(other)) # type: ignore[arg-type]
|
593
|
+
|
594
|
+
def gt(self, other) -> Expression:
|
595
|
+
"""Create greater-than comparison expression."""
|
596
|
+
from ..expressions import BinaryOperation, wrap_operand
|
597
|
+
|
598
|
+
return BinaryOperation(">", wrap_operand(self), wrap_operand(other)) # type: ignore[arg-type]
|
599
|
+
|
600
|
+
# Python comparison operators for convenience
|
601
|
+
def __lt__(self, other) -> Expression:
|
602
|
+
return self.lt(other)
|
603
|
+
|
604
|
+
def __le__(self, other) -> Expression:
|
605
|
+
return self.leq(other)
|
606
|
+
|
607
|
+
def __ge__(self, other) -> Expression:
|
608
|
+
return self.geq(other)
|
609
|
+
|
610
|
+
def __gt__(self, other) -> Expression:
|
611
|
+
return self.gt(other)
|
612
|
+
|
613
|
+
def eq(self, other) -> Expression:
|
614
|
+
"""Create equality comparison expression."""
|
615
|
+
from ..expressions import BinaryOperation, wrap_operand
|
616
|
+
|
617
|
+
return BinaryOperation("==", wrap_operand(self), wrap_operand(other)) # type: ignore[arg-type]
|
618
|
+
|
619
|
+
def ne(self, other) -> Expression:
|
620
|
+
"""Create inequality comparison expression."""
|
621
|
+
from ..expressions import BinaryOperation, wrap_operand
|
622
|
+
|
623
|
+
return BinaryOperation("!=", wrap_operand(self), wrap_operand(other)) # type: ignore[arg-type]
|
624
|
+
|
625
|
+
def get_variables(self) -> set[str]:
|
626
|
+
"""Get variable names used in this variable (for equation system compatibility)."""
|
627
|
+
return {getattr(self, 'symbol', None) or getattr(self, 'name', 'var')}
|
628
|
+
|
629
|
+
def evaluate(self, variable_values: dict[str, FieldQnty]) -> Quantity:
|
630
|
+
"""Evaluate this variable in the context of a variable dictionary."""
|
631
|
+
# If this variable has a quantity, return it
|
632
|
+
if self.quantity is not None:
|
633
|
+
return self.quantity
|
634
|
+
|
635
|
+
# Try to find this variable in the provided values
|
636
|
+
var_name = getattr(self, 'symbol', None) or getattr(self, 'name', 'var')
|
637
|
+
if var_name in variable_values:
|
638
|
+
var = variable_values[var_name]
|
639
|
+
if var.quantity is not None:
|
640
|
+
return var.quantity
|
641
|
+
|
642
|
+
# If no quantity available, raise error
|
643
|
+
raise ValueError(f"Cannot evaluate variable '{var_name}' without value. Available variables: {list(variable_values.keys())}")
|
644
|
+
|
645
|
+
def solve_from(self, expression) -> Self:
|
646
|
+
"""
|
647
|
+
Solve this variable from an expression.
|
648
|
+
|
649
|
+
If the expression can be evaluated to a concrete value, assigns that value.
|
650
|
+
Otherwise, falls back to symbolic equation solving.
|
651
|
+
|
652
|
+
Args:
|
653
|
+
expression: The expression to solve from (can be a value, quantity, or expression)
|
654
|
+
|
655
|
+
Returns:
|
656
|
+
Self for method chaining
|
657
|
+
"""
|
658
|
+
from ..expressions import Expression
|
659
|
+
|
660
|
+
# Handle different types of input with optimized type checking
|
661
|
+
|
662
|
+
if hasattr(expression, "quantity") and expression.quantity is not None:
|
663
|
+
# Direct quantity/variable input - avoid repeated attribute access
|
664
|
+
qty = expression.quantity
|
665
|
+
self.quantity = qty
|
666
|
+
self._is_known = True
|
667
|
+
elif isinstance(expression, Expression):
|
668
|
+
# Expression input - try to evaluate it first
|
669
|
+
try:
|
670
|
+
# Check if the expression can auto-evaluate
|
671
|
+
can_eval, variables = expression._can_auto_evaluate()
|
672
|
+
if can_eval:
|
673
|
+
# Expression evaluates to a concrete value - use the evaluate method
|
674
|
+
evaluated_quantity = expression.evaluate(variables)
|
675
|
+
if evaluated_quantity is not None:
|
676
|
+
self.quantity = evaluated_quantity
|
677
|
+
self._is_known = True
|
678
|
+
else:
|
679
|
+
# Fall back to symbolic solving
|
680
|
+
self._symbolic_solve_from(expression)
|
681
|
+
else:
|
682
|
+
# Fall back to symbolic solving
|
683
|
+
self._symbolic_solve_from(expression)
|
684
|
+
except Exception:
|
685
|
+
# If evaluation fails, try symbolic solving
|
686
|
+
self._symbolic_solve_from(expression)
|
687
|
+
else:
|
688
|
+
# Direct value input (int, float)
|
689
|
+
try:
|
690
|
+
# Try to create a quantity with the same dimension as this variable
|
691
|
+
if hasattr(self, "_dimension") and getattr(self, '_dimension', None):
|
692
|
+
# Use cached dimensionless quantity for better performance
|
693
|
+
self.quantity = getattr(self, '_get_dimensionless_quantity', lambda _x: None)(float(expression)) # type: ignore[misc]
|
694
|
+
self._is_known = True
|
695
|
+
except Exception:
|
696
|
+
pass
|
697
|
+
|
698
|
+
return self
|
699
|
+
|
700
|
+
def solve(self) -> Self:
|
701
|
+
"""
|
702
|
+
Solve this variable by finding equations in the calling scope.
|
703
|
+
|
704
|
+
This method automatically discovers equations from the calling scope
|
705
|
+
and attempts to solve for this variable. If no stored equations are found,
|
706
|
+
it falls back to looking for direct expressions that can be used.
|
707
|
+
|
708
|
+
Returns:
|
709
|
+
Self for method chaining
|
710
|
+
|
711
|
+
Raises:
|
712
|
+
ValueError: If no equations found or equation cannot be solved
|
713
|
+
"""
|
714
|
+
from ..equations import Equation
|
715
|
+
from ..expressions import ScopeDiscoveryService
|
716
|
+
|
717
|
+
# Find all variables in the calling scope
|
718
|
+
all_variables = ScopeDiscoveryService.find_variables_in_scope()
|
719
|
+
|
720
|
+
var_name = getattr(self, 'symbol', None) or getattr(self, 'name', 'var')
|
721
|
+
|
722
|
+
# Strategy 1: Look for stored equations on variables
|
723
|
+
equations_found = []
|
724
|
+
for var in all_variables.values():
|
725
|
+
if hasattr(var, "_equations"):
|
726
|
+
equations_found.extend(var._equations)
|
727
|
+
|
728
|
+
# Try to solve using stored equations
|
729
|
+
for equation in equations_found:
|
730
|
+
try:
|
731
|
+
if hasattr(equation, "solve_for"):
|
732
|
+
solved_value = equation.solve_for(var_name, all_variables)
|
733
|
+
if solved_value is not None and hasattr(solved_value, "quantity"):
|
734
|
+
self.quantity = solved_value.quantity
|
735
|
+
self._is_known = True
|
736
|
+
return self
|
737
|
+
except Exception:
|
738
|
+
continue
|
739
|
+
|
740
|
+
# Strategy 2: Look for Equation objects in scope (created with .equals())
|
741
|
+
frame = ScopeDiscoveryService._find_user_frame(getattr(ScopeDiscoveryService, '_get_current_frame', lambda: None)()) # type: ignore[misc]
|
742
|
+
if frame:
|
743
|
+
for obj in list(frame.f_locals.values()) + list(frame.f_globals.values()):
|
744
|
+
if isinstance(obj, Equation):
|
745
|
+
try:
|
746
|
+
solved_value = obj.solve_for(var_name, all_variables)
|
747
|
+
if solved_value is not None and hasattr(solved_value, "quantity"):
|
748
|
+
self.quantity = solved_value.quantity
|
749
|
+
self._is_known = True
|
750
|
+
return self
|
751
|
+
except Exception:
|
752
|
+
continue
|
753
|
+
|
754
|
+
raise ValueError(f"No equations found in scope to solve for '{var_name}'")
|
755
|
+
|
756
|
+
def _symbolic_solve_from(self, expression):
|
757
|
+
"""Fallback symbolic solving method."""
|
758
|
+
try:
|
759
|
+
equation = self.equals(expression)
|
760
|
+
solved_value = equation.solve_for(getattr(self, 'symbol', None) or getattr(self, 'name', 'var'), {})
|
761
|
+
if solved_value is not None and hasattr(solved_value, "quantity"):
|
762
|
+
self.quantity = solved_value.quantity
|
763
|
+
self._is_known = True
|
764
|
+
except Exception:
|
765
|
+
# If all else fails, silently continue
|
766
|
+
pass
|
767
|
+
|
768
|
+
|
769
|
+
class SetterCompatibilityMixin:
|
770
|
+
"""Provides backward compatibility with existing setter system."""
|
771
|
+
|
772
|
+
def __init__(self):
|
773
|
+
# Only set instance attributes if not already set by class definition
|
774
|
+
# This preserves class attributes from generated quantities
|
775
|
+
if not hasattr(self, "_setter_class"):
|
776
|
+
self._setter_class: type | None = None
|
777
|
+
|
778
|
+
def set(self, value: float) -> TypeSafeSetter:
|
779
|
+
"""Create setter object for fluent API compatibility."""
|
780
|
+
if hasattr(self, "_setter_class") and self._setter_class:
|
781
|
+
return self._setter_class(self, value) # Correct parameter order
|
782
|
+
else:
|
783
|
+
# Fallback to generic setter
|
784
|
+
from .base_qnty import TypeSafeSetter
|
785
|
+
|
786
|
+
return TypeSafeSetter(self, value) # Correct parameter order
|
787
|
+
|
788
|
+
|
789
|
+
class UnitConverter:
|
790
|
+
"""Base class for unit conversion operations."""
|
791
|
+
|
792
|
+
def __init__(self, variable: FieldQnty):
|
793
|
+
self.variable = variable
|
794
|
+
|
795
|
+
def _get_unit_constant(self, unit_str: str):
|
796
|
+
"""Get unit constant from unit string."""
|
797
|
+
# Try to get from field_units module directly using the variable type
|
798
|
+
try:
|
799
|
+
from ..units import field_units
|
800
|
+
# Get the units class for this variable type
|
801
|
+
var_type = self.variable.__class__.__name__
|
802
|
+
if hasattr(field_units, f"{var_type}Units"):
|
803
|
+
units_class = getattr(field_units, f"{var_type}Units")
|
804
|
+
# Try direct lookup first
|
805
|
+
if hasattr(units_class, unit_str):
|
806
|
+
return getattr(units_class, unit_str)
|
807
|
+
except ImportError:
|
808
|
+
pass
|
809
|
+
|
810
|
+
# Fallback to registry lookup
|
811
|
+
try:
|
812
|
+
from ..units.registry import registry
|
813
|
+
if hasattr(registry, 'units') and unit_str in registry.units:
|
814
|
+
return registry.units[unit_str]
|
815
|
+
except Exception:
|
816
|
+
pass
|
817
|
+
|
818
|
+
raise ValueError(f"Unknown unit: {unit_str}")
|
819
|
+
|
820
|
+
def _convert_quantity(self, to_unit_constant, modify_original: bool = True):
|
821
|
+
"""Convert the variable's quantity to the specified unit."""
|
822
|
+
if self.variable.quantity is None:
|
823
|
+
raise ValueError(f"Cannot convert {self.variable.name}: no value set")
|
824
|
+
|
825
|
+
from_unit = self.variable.quantity.unit
|
826
|
+
from_value = self.variable.quantity.value
|
827
|
+
|
828
|
+
# Convert value
|
829
|
+
if from_unit.name == to_unit_constant.name:
|
830
|
+
new_value = from_value
|
831
|
+
else:
|
832
|
+
# Use the registry for conversion
|
833
|
+
try:
|
834
|
+
from ..units.registry import registry
|
835
|
+
new_value = registry.convert(from_value, from_unit, to_unit_constant)
|
836
|
+
except Exception:
|
837
|
+
# Fallback to SI conversion
|
838
|
+
si_value = from_value * from_unit.si_factor
|
839
|
+
new_value = si_value / to_unit_constant.si_factor
|
840
|
+
|
841
|
+
# Create new quantity
|
842
|
+
from .base_qnty import Quantity
|
843
|
+
new_quantity = Quantity(new_value, to_unit_constant)
|
844
|
+
|
845
|
+
if modify_original:
|
846
|
+
self.variable.quantity = new_quantity
|
847
|
+
return self.variable
|
848
|
+
else:
|
849
|
+
# Return a new variable with the converted value
|
850
|
+
new_var = self.variable.__class__(new_value, to_unit_constant.name, f"{self.variable.name}_converted")
|
851
|
+
return new_var
|
852
|
+
|
853
|
+
|
854
|
+
class ToUnitConverter(UnitConverter):
|
855
|
+
"""Handles L.to_unit.unit() conversions that modify the original variable."""
|
856
|
+
|
857
|
+
def __call__(self, unit_str: str):
|
858
|
+
"""Convert to specified unit using string notation."""
|
859
|
+
unit_constant = self._get_unit_constant(unit_str)
|
860
|
+
return self._convert_quantity(unit_constant, modify_original=True)
|
861
|
+
|
862
|
+
def __getattr__(self, unit_name: str):
|
863
|
+
"""Enable L.to_unit.mm() syntax."""
|
864
|
+
def converter():
|
865
|
+
unit_constant = self._get_unit_constant(unit_name)
|
866
|
+
return self._convert_quantity(unit_constant, modify_original=True)
|
867
|
+
return converter
|
868
|
+
|
869
|
+
|
870
|
+
class AsUnitConverter(UnitConverter):
|
871
|
+
"""Handles L.as_unit.unit() conversions that return a new representation."""
|
872
|
+
|
873
|
+
def __call__(self, unit_str: str):
|
874
|
+
"""Convert to specified unit using string notation, returning new variable."""
|
875
|
+
unit_constant = self._get_unit_constant(unit_str)
|
876
|
+
return self._convert_quantity(unit_constant, modify_original=False)
|
877
|
+
|
878
|
+
def __getattr__(self, unit_name: str):
|
879
|
+
"""Enable L.as_unit.mm() syntax."""
|
880
|
+
def converter():
|
881
|
+
unit_constant = self._get_unit_constant(unit_name)
|
882
|
+
return self._convert_quantity(unit_constant, modify_original=False)
|
883
|
+
return converter
|
884
|
+
|
885
|
+
|
886
|
+
class FieldQnty(QuantityManagementMixin, FlexibleConstructorMixin, UnifiedArithmeticMixin, ExpressionMixin, SetterCompatibilityMixin, ErrorHandlerMixin):
|
887
|
+
"""
|
888
|
+
Unified variable class that replaces the 4-level inheritance chain.
|
889
|
+
|
890
|
+
This combines all capabilities through focused mixins instead of inheritance:
|
891
|
+
- QuantityManagementMixin: Core quantity storage and state management
|
892
|
+
- FlexibleConstructorMixin: Backward-compatible initialization patterns
|
893
|
+
- UnifiedArithmeticMixin: User-controllable arithmetic operations
|
894
|
+
- ExpressionMixin: Expression and equation creation capabilities
|
895
|
+
- SetterCompatibilityMixin: Backward compatibility with setter system
|
896
|
+
- ErrorHandlerMixin: Consistent error handling
|
897
|
+
"""
|
898
|
+
|
899
|
+
# Class attributes that subclasses should define
|
900
|
+
_dimension: DimensionSignature | None = None
|
901
|
+
_setter_class: type | None = None
|
902
|
+
_default_unit_property: str | None = None
|
903
|
+
_unit_mappings: dict[str, str] = {}
|
904
|
+
|
905
|
+
def __init__(self, *args, **kwargs):
|
906
|
+
"""Initialize unified variable with simplified setup."""
|
907
|
+
# Initialize core components
|
908
|
+
self._initialize_core_attributes()
|
909
|
+
self._initialize_mixins()
|
910
|
+
|
911
|
+
# Process constructor arguments
|
912
|
+
self._initialize_from_args(*args, **kwargs)
|
913
|
+
|
914
|
+
def _initialize_core_attributes(self) -> None:
|
915
|
+
"""Initialize core variable attributes."""
|
916
|
+
self.validation_checks = []
|
917
|
+
self._parent_problem = None
|
918
|
+
self._equations = []
|
919
|
+
if not hasattr(self, "_symbol"):
|
920
|
+
self._symbol = None
|
921
|
+
|
922
|
+
def _initialize_mixins(self) -> None:
|
923
|
+
"""Initialize all mixin components."""
|
924
|
+
QuantityManagementMixin.__init__(self)
|
925
|
+
FlexibleConstructorMixin.__init__(self)
|
926
|
+
UnifiedArithmeticMixin.__init__(self)
|
927
|
+
ExpressionMixin.__init__(self)
|
928
|
+
SetterCompatibilityMixin.__init__(self)
|
929
|
+
ErrorHandlerMixin.__init__(self)
|
930
|
+
|
931
|
+
# Simplified property management
|
932
|
+
@property
|
933
|
+
def name(self) -> str:
|
934
|
+
return getattr(self, "_name", "")
|
935
|
+
|
936
|
+
@name.setter
|
937
|
+
def name(self, value: str) -> None:
|
938
|
+
self._name = value
|
939
|
+
|
940
|
+
@property
|
941
|
+
def expected_dimension(self) -> DimensionSignature | None:
|
942
|
+
return self._dimension
|
943
|
+
|
944
|
+
@expected_dimension.setter
|
945
|
+
def expected_dimension(self, value: DimensionSignature | None) -> None:
|
946
|
+
self._dimension = value
|
947
|
+
|
948
|
+
@property
|
949
|
+
def symbol(self) -> str | None:
|
950
|
+
return getattr(self, "_symbol", None) or self.name
|
951
|
+
|
952
|
+
@symbol.setter
|
953
|
+
def symbol(self, value: str | None) -> None:
|
954
|
+
self._symbol = value
|
955
|
+
|
956
|
+
@property
|
957
|
+
def to_unit(self) -> ToUnitConverter:
|
958
|
+
"""Get unit converter that modifies the original variable."""
|
959
|
+
# Try to get the specific converter class for this variable type
|
960
|
+
try:
|
961
|
+
from . import field_converters
|
962
|
+
class_name = self.__class__.__name__
|
963
|
+
converter_class_name = f"ToUnit{class_name}Converter"
|
964
|
+
converter_class = getattr(field_converters, converter_class_name, None)
|
965
|
+
if converter_class:
|
966
|
+
return converter_class(self)
|
967
|
+
except ImportError:
|
968
|
+
pass
|
969
|
+
# Fallback to generic converter
|
970
|
+
return ToUnitConverter(self)
|
971
|
+
|
972
|
+
@property
|
973
|
+
def as_unit(self) -> AsUnitConverter:
|
974
|
+
"""Get unit converter that returns a new variable representation."""
|
975
|
+
# Try to get the specific converter class for this variable type
|
976
|
+
try:
|
977
|
+
from . import field_converters
|
978
|
+
class_name = self.__class__.__name__
|
979
|
+
converter_class_name = f"AsUnit{class_name}Converter"
|
980
|
+
converter_class = getattr(field_converters, converter_class_name, None)
|
981
|
+
if converter_class:
|
982
|
+
return converter_class(self)
|
983
|
+
except ImportError:
|
984
|
+
pass
|
985
|
+
# Fallback to generic converter
|
986
|
+
return AsUnitConverter(self)
|
987
|
+
|
988
|
+
def __str__(self) -> str:
|
989
|
+
status = f"{self._quantity}" if (self.is_known and self._quantity) else "(unset)"
|
990
|
+
return f"{self.name} = {status}"
|
991
|
+
|
992
|
+
def __repr__(self) -> str:
|
993
|
+
return f"{self.__class__.__name__}('{self.name}', known={self.is_known})"
|
994
|
+
|
995
|
+
|
996
|
+
def create_domain_variable_class(
|
997
|
+
dimension: DimensionSignature, setter_class: type | None = None, default_unit_property: str | None = None, unit_mappings: dict[str, str] | None = None
|
998
|
+
) -> type[FieldQnty]:
|
999
|
+
"""
|
1000
|
+
Factory function to create domain-specific variable classes.
|
1001
|
+
|
1002
|
+
This replaces the need for hand-coded inheritance chains by generating
|
1003
|
+
the necessary class with proper dimension and setter configuration.
|
1004
|
+
"""
|
1005
|
+
|
1006
|
+
class DomainVariable(FieldQnty):
|
1007
|
+
_dimension = dimension
|
1008
|
+
_setter_class = setter_class
|
1009
|
+
_default_unit_property = default_unit_property
|
1010
|
+
_unit_mappings = unit_mappings or {}
|
1011
|
+
|
1012
|
+
return DomainVariable
|