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.
- qnty/__init__.py +140 -59
- 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 +4 -0
- qnty/equations/equation.py +220 -0
- qnty/equations/system.py +130 -0
- qnty/expressions/__init__.py +40 -0
- qnty/expressions/formatter.py +188 -0
- qnty/expressions/functions.py +74 -0
- qnty/expressions/nodes.py +701 -0
- qnty/expressions/types.py +70 -0
- qnty/extensions/plotting/__init__.py +0 -0
- qnty/extensions/reporting/__init__.py +0 -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 +29 -0
- qnty/quantities/base_qnty.py +677 -0
- qnty/quantities/field_converters.py +24004 -0
- qnty/quantities/field_qnty.py +1012 -0
- qnty/quantities/field_setter.py +12320 -0
- qnty/quantities/field_vars.py +6325 -0
- qnty/quantities/field_vars.pyi +4191 -0
- qnty/solving/__init__.py +0 -0
- qnty/solving/manager.py +96 -0
- qnty/solving/order.py +403 -0
- qnty/solving/solvers/__init__.py +13 -0
- qnty/solving/solvers/base.py +82 -0
- qnty/solving/solvers/iterative.py +165 -0
- qnty/solving/solvers/simultaneous.py +475 -0
- qnty/units/__init__.py +1 -0
- qnty/units/field_units.py +10507 -0
- qnty/units/field_units.pyi +2461 -0
- qnty/units/prefixes.py +203 -0
- qnty/{unit.py → units/registry.py} +89 -61
- 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 +40 -0
- qnty/utils/protocols.py +164 -0
- qnty/utils/scope_discovery.py +420 -0
- qnty-0.1.0.dist-info/METADATA +199 -0
- qnty-0.1.0.dist-info/RECORD +60 -0
- qnty/dimension.py +0 -186
- qnty/equation.py +0 -297
- qnty/expression.py +0 -553
- qnty/prefixes.py +0 -229
- qnty/unit_types/base.py +0 -47
- qnty/units.py +0 -8113
- qnty/variable.py +0 -300
- qnty/variable_types/base.py +0 -58
- qnty/variable_types/expression_variable.py +0 -106
- qnty/variable_types/typed_variable.py +0 -87
- qnty/variables.py +0 -2298
- qnty/variables.pyi +0 -6148
- qnty-0.0.8.dist-info/METADATA +0 -355
- qnty-0.0.8.dist-info/RECORD +0 -19
- /qnty/{unit_types → extensions}/__init__.py +0 -0
- /qnty/{variable_types → extensions/integration}/__init__.py +0 -0
- {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
|