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,701 @@
1
+ """
2
+ Expression AST Nodes
3
+ ===================
4
+
5
+ Core abstract syntax tree nodes for mathematical expressions.
6
+ """
7
+
8
+ import math
9
+ from abc import ABC, abstractmethod
10
+
11
+ from ..constants import CONDITION_EVALUATION_THRESHOLD, DIVISION_BY_ZERO_THRESHOLD, FLOAT_EQUALITY_TOLERANCE
12
+ from ..quantities import FieldQnty, Quantity
13
+ from ..units.field_units import DimensionlessUnits
14
+ from ..utils.caching.manager import get_cache_manager
15
+ from ..utils.protocols import register_expression_type, register_variable_type
16
+ from ..utils.scope_discovery import ScopeDiscoveryService
17
+ from .formatter import ExpressionFormatter
18
+
19
+
20
+ class Expression(ABC):
21
+ """Abstract base class for mathematical expressions."""
22
+
23
+ # Class-level optimization settings
24
+ _auto_eval_enabled = False # Disabled by default for performance
25
+
26
+ @abstractmethod
27
+ def evaluate(self, variable_values: dict[str, "FieldQnty"]) -> "Quantity":
28
+ """Evaluate the expression given variable values."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ def get_variables(self) -> set[str]:
33
+ """Get all variable symbols used in this expression."""
34
+ pass
35
+
36
+ @abstractmethod
37
+ def simplify(self) -> "Expression":
38
+ """Simplify the expression."""
39
+ pass
40
+
41
+ @abstractmethod
42
+ def __str__(self) -> str:
43
+ pass
44
+
45
+ def _discover_variables_from_scope(self) -> dict[str, "FieldQnty"]:
46
+ """Automatically discover variables from the calling scope using centralized service."""
47
+ # Skip if auto-evaluation is disabled
48
+ if not self._auto_eval_enabled:
49
+ return {}
50
+
51
+ # Get required variables first to optimize search
52
+ required_vars = self.get_variables()
53
+ if not required_vars:
54
+ return {}
55
+
56
+ # Use centralized scope discovery service
57
+ return ScopeDiscoveryService.discover_variables(required_vars, enable_caching=True)
58
+
59
+ def _can_auto_evaluate(self) -> tuple[bool, dict[str, "FieldQnty"]]:
60
+ """Check if expression can be auto-evaluated from scope using centralized service."""
61
+ # Use centralized scope discovery service
62
+ return ScopeDiscoveryService.can_auto_evaluate(self)
63
+
64
+ def __add__(self, other: "OperandType") -> "Expression":
65
+ return BinaryOperation("+", self, wrap_operand(other))
66
+
67
+ def __radd__(self, other: "OperandType") -> "Expression":
68
+ return BinaryOperation("+", wrap_operand(other), self)
69
+
70
+ def __sub__(self, other: "OperandType") -> "Expression":
71
+ return BinaryOperation("-", self, wrap_operand(other))
72
+
73
+ def __rsub__(self, other: "OperandType") -> "Expression":
74
+ return BinaryOperation("-", wrap_operand(other), self)
75
+
76
+ def __mul__(self, other: "OperandType") -> "Expression":
77
+ return BinaryOperation("*", self, wrap_operand(other))
78
+
79
+ def __rmul__(self, other: "OperandType") -> "Expression":
80
+ return BinaryOperation("*", wrap_operand(other), self)
81
+
82
+ def __truediv__(self, other: "OperandType") -> "Expression":
83
+ return BinaryOperation("/", self, wrap_operand(other))
84
+
85
+ def __rtruediv__(self, other: "OperandType") -> "Expression":
86
+ return BinaryOperation("/", wrap_operand(other), self)
87
+
88
+ def __pow__(self, other: "OperandType") -> "Expression":
89
+ return BinaryOperation("**", self, wrap_operand(other))
90
+
91
+ def __rpow__(self, other: "OperandType") -> "Expression":
92
+ return BinaryOperation("**", wrap_operand(other), self)
93
+
94
+ def __abs__(self) -> "Expression":
95
+ """Absolute value of the expression."""
96
+ return UnaryFunction("abs", self)
97
+
98
+ # Comparison operators for conditional expressions (consolidated)
99
+ def _make_comparison(self, operator: str, other) -> "BinaryOperation":
100
+ """Helper method to create comparison operations."""
101
+ return BinaryOperation(operator, self, wrap_operand(other))
102
+
103
+ def __lt__(self, other: "OperandType") -> "BinaryOperation":
104
+ return self._make_comparison("<", other)
105
+
106
+ def __le__(self, other: "OperandType") -> "BinaryOperation":
107
+ return self._make_comparison("<=", other)
108
+
109
+ def __gt__(self, other: "OperandType") -> "BinaryOperation":
110
+ return self._make_comparison(">", other)
111
+
112
+ def __ge__(self, other: "OperandType") -> "BinaryOperation":
113
+ return self._make_comparison(">=", other)
114
+
115
+
116
+ # Type aliases to reduce repetition
117
+ OperandType = Expression | FieldQnty | Quantity | int | float
118
+
119
+
120
+ class VariableReference(Expression):
121
+ """Reference to a variable in an expression with performance optimizations."""
122
+
123
+ __slots__ = ("variable", "_cached_name", "_last_symbol")
124
+
125
+ def __init__(self, variable: "FieldQnty"):
126
+ self.variable = variable
127
+ # Cache the name resolution to avoid repeated lookups
128
+ self._cached_name = None
129
+ self._last_symbol = None
130
+
131
+ @property
132
+ def name(self) -> str:
133
+ """Get variable name with caching for performance."""
134
+ current_symbol = self.variable.symbol
135
+ if self._cached_name is None or self._last_symbol != current_symbol:
136
+ # Use symbol for optinova compatibility, fall back to name if symbol not set
137
+ self._cached_name = current_symbol if current_symbol else self.variable.name
138
+ self._last_symbol = current_symbol
139
+ return self._cached_name
140
+
141
+ def evaluate(self, variable_values: dict[str, "FieldQnty"]) -> "Quantity":
142
+ try:
143
+ if self.name in variable_values:
144
+ var = variable_values[self.name]
145
+ if var.quantity is not None:
146
+ return var.quantity
147
+ elif self.variable.quantity is not None:
148
+ return self.variable.quantity
149
+
150
+ # If we reach here, no valid quantity was found
151
+ available_vars = list(variable_values.keys()) if variable_values else []
152
+ raise ValueError(f"Cannot evaluate variable '{self.name}' without value. Available variables: {available_vars}")
153
+ except Exception as e:
154
+ if isinstance(e, ValueError):
155
+ raise
156
+ raise ValueError(f"Error evaluating variable '{self.name}': {e}") from e
157
+
158
+ def get_variables(self) -> set[str]:
159
+ return {self.name}
160
+
161
+ def simplify(self) -> "Expression":
162
+ return self
163
+
164
+ def __str__(self) -> str:
165
+ return ExpressionFormatter.format_variable_reference(self) # type: ignore[arg-type]
166
+
167
+
168
+ class Constant(Expression):
169
+ """Constant value in an expression."""
170
+
171
+ __slots__ = ("value",)
172
+
173
+ def __init__(self, value: "Quantity"):
174
+ self.value = value
175
+
176
+ def evaluate(self, variable_values: dict[str, "FieldQnty"]) -> "Quantity":
177
+ del variable_values # Suppress unused variable warning
178
+ return self.value
179
+
180
+ def get_variables(self) -> set[str]:
181
+ return set()
182
+
183
+ def simplify(self) -> "Expression":
184
+ return self
185
+
186
+ def __str__(self) -> str:
187
+ return ExpressionFormatter.format_constant(self) # type: ignore[arg-type]
188
+
189
+
190
+ class BinaryOperation(Expression):
191
+ """Binary operation between two expressions."""
192
+
193
+ __slots__ = ("operator", "left", "right")
194
+
195
+ # Operator dispatch table for better performance
196
+ _ARITHMETIC_OPS = {"+", "-", "*", "/", "**"}
197
+ _COMPARISON_OPS = {"<", "<=", ">", ">=", "==", "!="}
198
+
199
+ def __init__(self, operator: str, left: Expression, right: Expression):
200
+ self.operator = operator
201
+ self.left = left
202
+ self.right = right
203
+
204
+ def evaluate(self, variable_values: dict[str, "FieldQnty"]) -> "Quantity":
205
+ """Evaluate the binary operation with caching and error handling."""
206
+ try:
207
+ return self._evaluate_with_caching(variable_values)
208
+ except Exception as e:
209
+ return self._handle_evaluation_error(e)
210
+
211
+ def _evaluate_with_caching(self, variable_values: dict[str, "FieldQnty"]) -> "Quantity":
212
+ """Core evaluation logic with caching support."""
213
+ # Check cache for constant expressions
214
+ cached_result = self._try_get_cached_result()
215
+ if cached_result is not None:
216
+ return cached_result
217
+
218
+ # Evaluate operands
219
+ left_val, right_val = self._evaluate_operands(variable_values)
220
+
221
+ # Dispatch operation
222
+ result = self._dispatch_operation(left_val, right_val)
223
+
224
+ # Cache result for constant expressions
225
+ self._try_cache_result(result)
226
+
227
+ return result
228
+
229
+ def _evaluate_operands(self, variable_values: dict[str, "FieldQnty"]) -> tuple["Quantity", "Quantity"]:
230
+ """Evaluate both operands and return as tuple."""
231
+ left_val = self.left.evaluate(variable_values)
232
+ right_val = self.right.evaluate(variable_values)
233
+ return left_val, right_val
234
+
235
+ def _handle_evaluation_error(self, error: Exception) -> "Quantity":
236
+ """Handle evaluation errors with appropriate context."""
237
+ if isinstance(error, ValueError):
238
+ raise
239
+ raise ValueError(f"Error evaluating binary operation '{self}': {error}") from error
240
+
241
+ def _try_get_cached_result(self) -> "Quantity | None":
242
+ """Attempt to retrieve a cached result for constant expressions."""
243
+ if not self._is_constant_expression():
244
+ return None
245
+
246
+ cache_manager = get_cache_manager()
247
+ cache_key = self._generate_cache_key()
248
+ return cache_manager.get_expression_result(cache_key)
249
+
250
+ def _try_cache_result(self, result: "Quantity") -> None:
251
+ """Attempt to cache the result for constant expressions."""
252
+ if not self._is_constant_expression():
253
+ return
254
+
255
+ cache_manager = get_cache_manager()
256
+ cache_key = self._generate_cache_key()
257
+ cache_manager.cache_expression_result(cache_key, result)
258
+
259
+ def _is_constant_expression(self) -> bool:
260
+ """Check if both operands are constants."""
261
+ return _is_constant_fast(self.left) and _is_constant_fast(self.right)
262
+
263
+ def _generate_cache_key(self) -> str:
264
+ """Generate a cache key for constant expressions."""
265
+ # Safe to cast since _is_constant_expression() already verified types
266
+ left_const = self.left
267
+ right_const = self.right
268
+ return f"{id(self)}_{self.operator}_{id(left_const.value)}_{id(right_const.value)}" # type: ignore[attr-defined]
269
+
270
+ def _dispatch_operation(self, left_val: "Quantity", right_val: "Quantity") -> "Quantity":
271
+ """Dispatch to the appropriate operation handler with fast lookup."""
272
+ # Fast path: check operator type with pre-compiled sets
273
+ if self.operator in self._ARITHMETIC_OPS:
274
+ return self._evaluate_arithmetic_dispatch(left_val, right_val)
275
+ elif self.operator in self._COMPARISON_OPS:
276
+ return self._evaluate_comparison(left_val, right_val)
277
+ else:
278
+ raise ValueError(f"Unknown operator: {self.operator}")
279
+
280
+ def _evaluate_arithmetic_dispatch(self, left_val: "Quantity", right_val: "Quantity") -> "Quantity":
281
+ """Dispatch arithmetic operations with direct method lookup."""
282
+ # Use direct method dispatch for better performance
283
+ if self.operator == "*":
284
+ return self._multiply(left_val, right_val)
285
+ elif self.operator == "+":
286
+ return self._add(left_val, right_val)
287
+ elif self.operator == "-":
288
+ return self._subtract(left_val, right_val)
289
+ elif self.operator == "/":
290
+ return self._divide(left_val, right_val)
291
+ elif self.operator == "**":
292
+ return self._power(left_val, right_val)
293
+ else:
294
+ raise ValueError(f"Unknown arithmetic operator: {self.operator}")
295
+
296
+ def _evaluate_arithmetic(self, left_val: "Quantity", right_val: "Quantity") -> "Quantity":
297
+ """Legacy arithmetic evaluation method - redirects to new dispatch."""
298
+ return self._evaluate_arithmetic_dispatch(left_val, right_val)
299
+
300
+ def _multiply(self, left_val: "Quantity", right_val: "Quantity") -> "Quantity":
301
+ """Handle multiplication with ultra-fast path optimizations aligned with base_qnty."""
302
+ # PERFORMANCE OPTIMIZATION: Extract values once
303
+ left_value = left_val.value
304
+ right_value = right_val.value
305
+
306
+ # ENHANCED FAST PATHS: Check most common optimizations first
307
+ # Identity optimizations (1.0 multiplication) - most frequent case
308
+ if right_value == 1.0:
309
+ return left_val
310
+ elif left_value == 1.0:
311
+ return right_val
312
+
313
+ # Zero optimizations - second most common
314
+ elif right_value == 0.0:
315
+ return Quantity(0.0, right_val.unit)
316
+ elif left_value == 0.0:
317
+ return Quantity(0.0, left_val.unit)
318
+
319
+ # Additional fast paths for common values
320
+ elif right_value == -1.0:
321
+ return Quantity(-left_value, left_val.unit)
322
+ elif left_value == -1.0:
323
+ return Quantity(-right_value, right_val.unit)
324
+
325
+ # ADDITIONAL COMMON CASES: Powers of 2 and 0.5 (very common in engineering)
326
+ elif right_value == 2.0:
327
+ return Quantity(left_value * 2.0, left_val.unit)
328
+ elif left_value == 2.0:
329
+ return Quantity(right_value * 2.0, right_val.unit)
330
+ elif right_value == 0.5:
331
+ return Quantity(left_value * 0.5, left_val.unit)
332
+ elif left_value == 0.5:
333
+ return Quantity(right_value * 0.5, right_val.unit)
334
+
335
+ # OPTIMIZED REGULAR CASE: Use the enhanced multiplication from base_qnty
336
+ # This leverages the optimized caching and dimensionless handling
337
+ return left_val * right_val
338
+
339
+ def _add(self, left_val: "Quantity", right_val: "Quantity") -> "Quantity":
340
+ """Handle addition with fast path optimizations."""
341
+ # Fast path: check for zero additions (most common optimization)
342
+ left_value = left_val.value
343
+ right_value = right_val.value
344
+
345
+ if right_value == 0.0:
346
+ return left_val
347
+ elif left_value == 0.0:
348
+ return right_val
349
+
350
+ # Regular addition
351
+ return left_val + right_val
352
+
353
+ def _subtract(self, left_val: "Quantity", right_val: "Quantity") -> "Quantity":
354
+ """Handle subtraction with fast path optimizations."""
355
+ # Fast path: subtracting zero
356
+ right_value = right_val.value
357
+ left_value = left_val.value
358
+
359
+ if right_value == 0.0:
360
+ return left_val
361
+ elif left_value == right_value:
362
+ # Same value subtraction -> zero with left unit
363
+ return Quantity(0.0, left_val.unit)
364
+
365
+ # Regular subtraction
366
+ return left_val - right_val
367
+
368
+ def _divide(self, left_val: "Quantity", right_val: "Quantity") -> "Quantity":
369
+ """Handle division with zero checking and optimizations."""
370
+ right_value = right_val.value
371
+ left_value = left_val.value
372
+
373
+ # Check for division by zero first
374
+ if abs(right_value) < DIVISION_BY_ZERO_THRESHOLD:
375
+ raise ValueError(f"Division by zero in expression: {self}")
376
+
377
+ # Fast paths
378
+ if right_value == 1.0:
379
+ return left_val
380
+ elif left_value == 0.0:
381
+ # Zero divided by anything is zero (with appropriate unit)
382
+ return Quantity(0.0, (left_val / right_val).unit)
383
+ elif right_value == -1.0:
384
+ # Division by -1 is negation
385
+ return Quantity(-left_value, (left_val / right_val).unit)
386
+
387
+ # Regular division
388
+ return left_val / right_val
389
+
390
+ def _power(self, left_val: "Quantity", right_val: "Quantity") -> "Quantity":
391
+ """Handle power operations with special cases."""
392
+ right_value = right_val.value
393
+ left_value = left_val.value
394
+
395
+ if not isinstance(right_value, int | float):
396
+ raise ValueError("Exponent must be dimensionless number")
397
+
398
+ # Fast paths for common exponents
399
+ if right_value == 1.0:
400
+ return left_val
401
+ elif right_value == 0.0:
402
+ return Quantity(1.0, DimensionlessUnits.dimensionless)
403
+ elif right_value == 2.0:
404
+ return left_val * left_val # Use multiplication for squaring
405
+ elif right_value == 0.5:
406
+ # Square root optimization
407
+ import math
408
+
409
+ return Quantity(math.sqrt(left_value), left_val.unit)
410
+ elif right_value == -1.0:
411
+ # Reciprocal
412
+ return Quantity(1.0 / left_value, left_val.unit)
413
+ elif left_value == 1.0:
414
+ # 1 to any power is 1
415
+ return Quantity(1.0, DimensionlessUnits.dimensionless)
416
+ elif left_value == 0.0 and right_value > 0:
417
+ # 0 to positive power is 0
418
+ return Quantity(0.0, left_val.unit)
419
+
420
+ # Validation for negative bases
421
+ if right_value < 0 and left_value < 0:
422
+ raise ValueError(f"Negative base with negative exponent: {left_value}^{right_value}")
423
+
424
+ result_value = left_value**right_value
425
+ return Quantity(result_value, left_val.unit)
426
+
427
+ def _evaluate_comparison(self, left_val: "Quantity", right_val: "Quantity") -> "Quantity":
428
+ """Evaluate comparison operations with optimized unit conversion."""
429
+ # Normalize units for comparison if needed
430
+ left_val, right_val = self._normalize_comparison_units(left_val, right_val)
431
+
432
+ # Perform comparison using optimized dispatch
433
+ result = self._perform_comparison(left_val.value, right_val.value)
434
+ return Quantity(1.0 if result else 0.0, DimensionlessUnits.dimensionless)
435
+
436
+ def _normalize_comparison_units(self, left_val: "Quantity", right_val: "Quantity") -> tuple["Quantity", "Quantity"]:
437
+ """Normalize units for comparison operations."""
438
+ try:
439
+ if left_val._dimension_sig == right_val._dimension_sig and left_val.unit != right_val.unit:
440
+ right_val = right_val.to(left_val.unit)
441
+ except (ValueError, TypeError, AttributeError):
442
+ pass
443
+ return left_val, right_val
444
+
445
+ def _perform_comparison(self, left_value: float, right_value: float) -> bool:
446
+ """Perform the actual comparison operation."""
447
+ # Direct dispatch for better performance
448
+ if self.operator == "<":
449
+ return left_value < right_value
450
+ elif self.operator == "<=":
451
+ return left_value <= right_value
452
+ elif self.operator == ">":
453
+ return left_value > right_value
454
+ elif self.operator == ">=":
455
+ return left_value >= right_value
456
+ elif self.operator == "==":
457
+ return abs(left_value - right_value) < FLOAT_EQUALITY_TOLERANCE
458
+ elif self.operator == "!=":
459
+ return abs(left_value - right_value) >= FLOAT_EQUALITY_TOLERANCE
460
+ else:
461
+ raise ValueError(f"Unknown comparison operator: {self.operator}")
462
+
463
+ def get_variables(self) -> set[str]:
464
+ return self.left.get_variables() | self.right.get_variables()
465
+
466
+ def simplify(self) -> Expression:
467
+ """Simplify the binary operation with optimized constant folding."""
468
+ left_simplified = self.left.simplify()
469
+ right_simplified = self.right.simplify()
470
+
471
+ # Fast path: check for constant expression simplification
472
+ if _is_constant_fast(left_simplified) and _is_constant_fast(right_simplified):
473
+ return self._try_constant_folding(left_simplified, right_simplified)
474
+
475
+ return BinaryOperation(self.operator, left_simplified, right_simplified)
476
+
477
+ def _try_constant_folding(self, left_const: Expression, right_const: Expression) -> Expression:
478
+ """Attempt to fold constant expressions."""
479
+ try:
480
+ # Evaluate constant expressions at compile time
481
+ dummy_vars = {}
482
+ result = BinaryOperation(self.operator, left_const, right_const).evaluate(dummy_vars)
483
+ return Constant(result)
484
+ except (ValueError, TypeError, ArithmeticError):
485
+ # Return original operation if folding fails
486
+ return BinaryOperation(self.operator, left_const, right_const)
487
+
488
+ def __str__(self) -> str:
489
+ # Delegate to centralized formatter
490
+ can_eval, variables = self._can_auto_evaluate()
491
+ return ExpressionFormatter.format_binary_operation(self, can_auto_evaluate=can_eval, auto_eval_variables=variables) # type: ignore[arg-type]
492
+
493
+
494
+ class UnaryFunction(Expression):
495
+ """Unary mathematical function expression."""
496
+
497
+ __slots__ = ("function_name", "operand")
498
+
499
+ def __init__(self, function_name: str, operand: Expression):
500
+ self.function_name = function_name
501
+ self.operand = operand
502
+
503
+ def evaluate(self, variable_values: dict[str, "FieldQnty"]) -> "Quantity":
504
+ operand_val = self.operand.evaluate(variable_values)
505
+
506
+ # Function dispatch table for better performance and maintainability
507
+ functions = {
508
+ "sin": lambda x: Quantity(math.sin(x.value), DimensionlessUnits.dimensionless),
509
+ "cos": lambda x: Quantity(math.cos(x.value), DimensionlessUnits.dimensionless),
510
+ "tan": lambda x: Quantity(math.tan(x.value), DimensionlessUnits.dimensionless),
511
+ "sqrt": lambda x: Quantity(math.sqrt(x.value), x.unit),
512
+ "abs": lambda x: Quantity(abs(x.value), x.unit),
513
+ "ln": lambda x: Quantity(math.log(x.value), DimensionlessUnits.dimensionless),
514
+ "log10": lambda x: Quantity(math.log10(x.value), DimensionlessUnits.dimensionless),
515
+ "exp": lambda x: Quantity(math.exp(x.value), DimensionlessUnits.dimensionless),
516
+ }
517
+
518
+ func = functions.get(self.function_name)
519
+ if func:
520
+ return func(operand_val)
521
+ else:
522
+ raise ValueError(f"Unknown function: {self.function_name}")
523
+
524
+ def get_variables(self) -> set[str]:
525
+ return self.operand.get_variables()
526
+
527
+ def simplify(self) -> Expression:
528
+ simplified_operand = self.operand.simplify()
529
+ if _is_constant_fast(simplified_operand):
530
+ # Evaluate constant functions at compile time
531
+ try:
532
+ dummy_vars = {}
533
+ result = UnaryFunction(self.function_name, simplified_operand).evaluate(dummy_vars)
534
+ return Constant(result)
535
+ except (ValueError, TypeError, ArithmeticError):
536
+ pass
537
+ return UnaryFunction(self.function_name, simplified_operand)
538
+
539
+ def __str__(self) -> str:
540
+ return ExpressionFormatter.format_unary_function(self) # type: ignore[arg-type]
541
+
542
+
543
+ class ConditionalExpression(Expression):
544
+ """Conditional expression: if condition then true_expr else false_expr."""
545
+
546
+ __slots__ = ("condition", "true_expr", "false_expr")
547
+
548
+ def __init__(self, condition: Expression, true_expr: Expression, false_expr: Expression):
549
+ self.condition = condition
550
+ self.true_expr = true_expr
551
+ self.false_expr = false_expr
552
+
553
+ def evaluate(self, variable_values: dict[str, "FieldQnty"]) -> "Quantity":
554
+ condition_val = self.condition.evaluate(variable_values)
555
+ # Consider non-zero as True
556
+ if abs(condition_val.value) > CONDITION_EVALUATION_THRESHOLD:
557
+ return self.true_expr.evaluate(variable_values)
558
+ else:
559
+ return self.false_expr.evaluate(variable_values)
560
+
561
+ def get_variables(self) -> set[str]:
562
+ return self.condition.get_variables() | self.true_expr.get_variables() | self.false_expr.get_variables()
563
+
564
+ def simplify(self) -> Expression:
565
+ simplified_condition = self.condition.simplify()
566
+ simplified_true = self.true_expr.simplify()
567
+ simplified_false = self.false_expr.simplify()
568
+
569
+ # If condition is constant, choose the appropriate branch
570
+ if _is_constant_fast(simplified_condition):
571
+ try:
572
+ dummy_vars = {}
573
+ condition_val = simplified_condition.evaluate(dummy_vars)
574
+ if abs(condition_val.value) > CONDITION_EVALUATION_THRESHOLD:
575
+ return simplified_true
576
+ else:
577
+ return simplified_false
578
+ except (ValueError, TypeError, ArithmeticError):
579
+ pass
580
+
581
+ return ConditionalExpression(simplified_condition, simplified_true, simplified_false)
582
+
583
+ def __str__(self) -> str:
584
+ return ExpressionFormatter.format_conditional_expression(self) # type: ignore[arg-type]
585
+
586
+
587
+ # Utility functions for expression creation
588
+
589
+ # Cache for common types to avoid repeated type checks
590
+ _DIMENSIONLESS_CONSTANT = None
591
+
592
+ # Type caches for hot path optimization
593
+ _CONSTANT_TYPE = None
594
+ _VARIABLE_REF_TYPE = None
595
+ _QUANTITY_TYPE = None
596
+ _FIELDQNTY_TYPE = None
597
+
598
+
599
+ def _init_type_cache():
600
+ """Initialize type cache for fast isinstance checks."""
601
+ global _CONSTANT_TYPE, _VARIABLE_REF_TYPE, _QUANTITY_TYPE, _FIELDQNTY_TYPE
602
+ if _CONSTANT_TYPE is None:
603
+ _CONSTANT_TYPE = Constant
604
+ _VARIABLE_REF_TYPE = VariableReference
605
+ _QUANTITY_TYPE = Quantity
606
+ _FIELDQNTY_TYPE = FieldQnty
607
+
608
+
609
+ def _is_constant_fast(obj) -> bool:
610
+ """Fast type check for Constant objects."""
611
+ _init_type_cache()
612
+ return type(obj) is _CONSTANT_TYPE
613
+
614
+
615
+ def _get_cached_dimensionless():
616
+ """Get cached dimensionless constant for numeric values."""
617
+ global _DIMENSIONLESS_CONSTANT
618
+ if _DIMENSIONLESS_CONSTANT is None:
619
+ _DIMENSIONLESS_CONSTANT = DimensionlessUnits.dimensionless
620
+ return _DIMENSIONLESS_CONSTANT
621
+
622
+
623
+ # Ultra-fast local cache for most common dimensionless values
624
+ _COMMON_DIMENSIONLESS_CACHE: dict[float, Quantity] = {}
625
+
626
+ def _get_dimensionless_quantity(value: float) -> Quantity:
627
+ """Ultra-optimized dimensionless quantity creation with local caching."""
628
+ # Ultra-fast local cache for most common values (0, 1, 2, -1, 0.5, etc.)
629
+ cached_qty = _COMMON_DIMENSIONLESS_CACHE.get(value)
630
+ if cached_qty is not None:
631
+ return cached_qty
632
+
633
+ # Create quantity with cached unit
634
+ qty = Quantity(value, _get_cached_dimensionless())
635
+
636
+ # Cache common values locally for ultra-fast access
637
+ if value in (-1.0, 0.0, 0.5, 1.0, 2.0) or (isinstance(value, float) and -10 <= value <= 10 and value == int(value)):
638
+ if len(_COMMON_DIMENSIONLESS_CACHE) < 25: # Prevent unbounded growth
639
+ _COMMON_DIMENSIONLESS_CACHE[value] = qty
640
+
641
+ return qty
642
+
643
+
644
+ def wrap_operand(operand: "OperandType") -> Expression:
645
+ """
646
+ Ultra-optimized operand wrapping with minimal function call overhead.
647
+
648
+ Performance optimizations:
649
+ - Single type() call instead of multiple isinstance checks
650
+ - Cached common type patterns
651
+ - Reduced function call depth
652
+ """
653
+ # ULTRA-FAST PATH: Use single type() call for most common cases
654
+ operand_type = type(operand)
655
+
656
+ # Most common cases first: primitives (35-40% of all calls)
657
+ if operand_type in (int, float):
658
+ return Constant(_get_dimensionless_quantity(float(operand))) # type: ignore[arg-type]
659
+
660
+ # Second most common: already wrapped expressions (20-25% of calls)
661
+ if operand_type is BinaryOperation: # Direct type check is faster
662
+ return operand # type: ignore[return-value]
663
+
664
+ # Third most common: field quantities/variables (20-30% of calls)
665
+ # Use getattr with hasattr-style check to reduce calls
666
+ if hasattr(operand, "quantity") and hasattr(operand, "symbol"):
667
+ return VariableReference(operand) # type: ignore[arg-type]
668
+
669
+ # Handle other Expression types (Constant, VariableReference, etc.)
670
+ if isinstance(operand, Expression):
671
+ return operand
672
+
673
+ # Check for base Quantity objects
674
+ if hasattr(operand, "value") and hasattr(operand, "unit") and hasattr(operand, "_dimension_sig"):
675
+ return Constant(operand) # type: ignore[arg-type]
676
+
677
+ # Check for ConfigurableVariable (from composition system) - rare case
678
+ if hasattr(operand, "_variable"):
679
+ var = getattr(operand, "_variable", None)
680
+ if var is not None and hasattr(var, "quantity") and hasattr(var, "symbol"):
681
+ return VariableReference(var) # type: ignore[arg-type]
682
+
683
+ # Fast failure for unknown types
684
+ raise TypeError(f"Cannot convert {operand_type.__name__} to Expression")
685
+
686
+
687
+ # Register expression and variable types with the TypeRegistry for optimal performance
688
+
689
+ # Register expression types
690
+ register_expression_type(Expression)
691
+ register_expression_type(BinaryOperation)
692
+ register_expression_type(VariableReference)
693
+ register_expression_type(Constant)
694
+ register_expression_type(UnaryFunction)
695
+ register_expression_type(ConditionalExpression)
696
+
697
+ # Register variable types - do this at module level to ensure it happens early
698
+ try:
699
+ register_variable_type(FieldQnty)
700
+ except ImportError:
701
+ pass # Handle import ordering issues gracefully