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
@@ -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