qnty 0.0.8__py3-none-any.whl → 0.0.9__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 -58
- qnty/_backup/problem_original.py +1251 -0
- qnty/_backup/quantity.py +63 -0
- qnty/codegen/cli.py +125 -0
- qnty/codegen/generators/data/unit_data.json +8807 -0
- qnty/codegen/generators/data_processor.py +345 -0
- qnty/codegen/generators/dimensions_gen.py +434 -0
- qnty/codegen/generators/doc_generator.py +141 -0
- qnty/codegen/generators/out/dimension_mapping.json +974 -0
- qnty/codegen/generators/out/dimension_metadata.json +123 -0
- qnty/codegen/generators/out/units_metadata.json +223 -0
- qnty/codegen/generators/quantities_gen.py +159 -0
- qnty/codegen/generators/setters_gen.py +178 -0
- qnty/codegen/generators/stubs_gen.py +167 -0
- qnty/codegen/generators/units_gen.py +295 -0
- qnty/codegen/generators/utils/__init__.py +0 -0
- qnty/equations/__init__.py +4 -0
- qnty/{equation.py → equations/equation.py} +78 -118
- qnty/equations/system.py +127 -0
- qnty/expressions/__init__.py +61 -0
- qnty/expressions/cache.py +94 -0
- qnty/expressions/functions.py +96 -0
- qnty/{expression.py → expressions/nodes.py} +209 -216
- qnty/generated/__init__.py +0 -0
- qnty/generated/dimensions.py +514 -0
- qnty/generated/quantities.py +6003 -0
- qnty/generated/quantities.pyi +4192 -0
- qnty/generated/setters.py +12210 -0
- qnty/generated/units.py +9798 -0
- qnty/problem/__init__.py +91 -0
- qnty/problem/base.py +142 -0
- qnty/problem/composition.py +385 -0
- qnty/problem/composition_mixin.py +382 -0
- qnty/problem/equations.py +413 -0
- qnty/problem/metaclass.py +302 -0
- qnty/problem/reconstruction.py +1016 -0
- qnty/problem/solving.py +180 -0
- qnty/problem/validation.py +64 -0
- qnty/problem/variables.py +239 -0
- qnty/quantities/__init__.py +6 -0
- qnty/quantities/expression_quantity.py +314 -0
- qnty/quantities/quantity.py +428 -0
- qnty/quantities/typed_quantity.py +215 -0
- qnty/solving/__init__.py +0 -0
- qnty/solving/manager.py +90 -0
- qnty/solving/order.py +355 -0
- qnty/solving/solvers/__init__.py +20 -0
- qnty/solving/solvers/base.py +92 -0
- qnty/solving/solvers/iterative.py +185 -0
- qnty/solving/solvers/simultaneous.py +547 -0
- qnty/units/__init__.py +0 -0
- qnty/{prefixes.py → units/prefixes.py} +54 -33
- qnty/{unit.py → units/registry.py} +73 -32
- qnty/utils/__init__.py +0 -0
- qnty/utils/logging.py +40 -0
- qnty/validation/__init__.py +0 -0
- qnty/validation/registry.py +0 -0
- qnty/validation/rules.py +167 -0
- qnty-0.0.9.dist-info/METADATA +199 -0
- qnty-0.0.9.dist-info/RECORD +63 -0
- qnty/dimension.py +0 -186
- 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 → codegen}/__init__.py +0 -0
- /qnty/{variable_types → codegen/generators}/__init__.py +0 -0
- {qnty-0.0.8.dist-info → qnty-0.0.9.dist-info}/WHEEL +0 -0
@@ -9,34 +9,57 @@ from __future__ import annotations
|
|
9
9
|
|
10
10
|
from typing import cast
|
11
11
|
|
12
|
-
from
|
13
|
-
from .
|
12
|
+
from ..expressions import Expression, VariableReference
|
13
|
+
from ..quantities.quantity import TypeSafeVariable
|
14
|
+
|
15
|
+
# Global optimization flags and cache
|
16
|
+
_SCOPE_DISCOVERY_ENABLED = False # Disabled by default due to high overhead
|
17
|
+
_VARIABLE_TYPE_CACHE = {} # Cache for hasattr checks
|
18
|
+
|
19
|
+
|
20
|
+
def _is_typesafe_variable(obj) -> bool:
|
21
|
+
"""Optimized type check with caching for hasattr calls."""
|
22
|
+
obj_type = type(obj)
|
23
|
+
if obj_type not in _VARIABLE_TYPE_CACHE:
|
24
|
+
_VARIABLE_TYPE_CACHE[obj_type] = (
|
25
|
+
hasattr(obj, 'symbol') and hasattr(obj, 'name') and hasattr(obj, 'quantity')
|
26
|
+
)
|
27
|
+
return _VARIABLE_TYPE_CACHE[obj_type]
|
14
28
|
|
15
29
|
|
16
30
|
class Equation:
|
17
|
-
"""
|
31
|
+
"""
|
32
|
+
Represents a mathematical equation with left-hand side equal to right-hand side.
|
33
|
+
Optimized with __slots__ for memory efficiency.
|
34
|
+
"""
|
35
|
+
__slots__ = ('name', 'lhs', 'rhs', '_variables')
|
18
36
|
|
19
37
|
def __init__(self, name: str, lhs: TypeSafeVariable | Expression, rhs: Expression):
|
20
38
|
self.name = name
|
21
39
|
|
22
|
-
# Convert Variable to VariableReference if needed
|
23
|
-
|
24
|
-
|
25
|
-
# It's a TypeSafeVariable-like object
|
26
|
-
self.lhs = VariableReference(cast('TypeSafeVariable', lhs))
|
40
|
+
# Convert Variable to VariableReference if needed - use isinstance for performance
|
41
|
+
if isinstance(lhs, TypeSafeVariable):
|
42
|
+
self.lhs = VariableReference(lhs)
|
27
43
|
else:
|
28
44
|
# It's already an Expression
|
29
45
|
self.lhs = cast(Expression, lhs)
|
30
46
|
|
31
47
|
self.rhs = rhs
|
32
|
-
self.
|
48
|
+
self._variables: set[str] | None = None # Lazy initialization for better performance
|
33
49
|
|
34
50
|
def get_all_variables(self) -> set[str]:
|
35
51
|
"""Get all variable names used in this equation."""
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
52
|
+
if self._variables is None:
|
53
|
+
# Both lhs and rhs should be Expressions after __init__ conversion
|
54
|
+
lhs_vars = self.lhs.get_variables()
|
55
|
+
rhs_vars = self.rhs.get_variables()
|
56
|
+
self._variables = lhs_vars | rhs_vars
|
57
|
+
return self._variables
|
58
|
+
|
59
|
+
@property
|
60
|
+
def variables(self) -> set[str]:
|
61
|
+
"""Get all variable names used in this equation (cached property)."""
|
62
|
+
return self.get_all_variables()
|
40
63
|
|
41
64
|
def get_unknown_variables(self, known_vars: set[str]) -> set[str]:
|
42
65
|
"""Get variables that are unknown (not in known_vars set)."""
|
@@ -76,11 +99,12 @@ class Equation:
|
|
76
99
|
if var_obj is not None:
|
77
100
|
# Convert result to the target variable's original unit if it had one
|
78
101
|
if var_obj.quantity is not None and var_obj.quantity.unit is not None:
|
79
|
-
# Convert to the target variable's defined unit
|
102
|
+
# Convert to the target variable's defined unit - be more specific about exceptions
|
80
103
|
try:
|
81
104
|
result_qty = result_qty.to(var_obj.quantity.unit)
|
82
|
-
except
|
83
|
-
#
|
105
|
+
except (ValueError, TypeError, AttributeError):
|
106
|
+
# Log specific conversion issues but continue with calculated unit
|
107
|
+
# This preserves the original behavior while being more explicit
|
84
108
|
pass
|
85
109
|
|
86
110
|
var_obj.quantity = result_qty
|
@@ -114,42 +138,67 @@ class Equation:
|
|
114
138
|
residual = abs(lhs_value.value - rhs_converted.value)
|
115
139
|
|
116
140
|
return residual < tolerance
|
117
|
-
except
|
141
|
+
except (ValueError, TypeError, AttributeError, KeyError):
|
142
|
+
# Handle specific expected errors during evaluation/conversion
|
118
143
|
return False
|
144
|
+
except Exception as e:
|
145
|
+
# Re-raise unexpected errors to avoid masking bugs
|
146
|
+
raise RuntimeError(f"Unexpected error in residual check for equation '{self.name}': {e}") from e
|
119
147
|
|
120
148
|
def _discover_variables_from_scope(self) -> dict[str, TypeSafeVariable]:
|
121
|
-
"""
|
149
|
+
"""
|
150
|
+
Automatically discover variables from the calling scope.
|
151
|
+
Now optimized with caching and conditional execution.
|
152
|
+
"""
|
153
|
+
if not _SCOPE_DISCOVERY_ENABLED:
|
154
|
+
return {}
|
155
|
+
|
122
156
|
import inspect
|
123
157
|
|
124
158
|
# Get the frame that called this method (skip through __str__ calls)
|
125
159
|
frame = inspect.currentframe()
|
126
160
|
try:
|
127
161
|
# Skip frames until we find one outside the equation system
|
128
|
-
|
162
|
+
depth = 0
|
163
|
+
max_depth = 8 # Reduced from 10 for performance
|
164
|
+
while frame and depth < max_depth and (
|
129
165
|
frame.f_code.co_filename.endswith(('equation.py', 'expression.py')) or
|
130
166
|
frame.f_code.co_name in ['__str__', '__repr__']
|
131
167
|
):
|
132
168
|
frame = frame.f_back
|
169
|
+
depth += 1
|
133
170
|
|
134
171
|
if not frame:
|
135
172
|
return {}
|
136
173
|
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
# Find TypeSafeVariable objects that match our required variables
|
174
|
+
# Only get local variables first (faster than combining both)
|
175
|
+
local_vars = frame.f_locals
|
141
176
|
required_vars = self.variables
|
142
177
|
discovered = {}
|
143
178
|
|
179
|
+
# First pass: check locals only (most common case)
|
144
180
|
for var_name in required_vars:
|
145
|
-
for
|
146
|
-
if
|
147
|
-
|
148
|
-
|
149
|
-
|
181
|
+
for obj in local_vars.values():
|
182
|
+
if _is_typesafe_variable(obj) and (
|
183
|
+
(hasattr(obj, 'symbol') and obj.symbol == var_name) or
|
184
|
+
(hasattr(obj, 'name') and obj.name == var_name)
|
185
|
+
):
|
150
186
|
discovered[var_name] = obj
|
151
187
|
break
|
152
188
|
|
189
|
+
# Second pass: check globals only if needed
|
190
|
+
if len(discovered) < len(required_vars):
|
191
|
+
global_vars = frame.f_globals
|
192
|
+
remaining_vars = required_vars - discovered.keys()
|
193
|
+
for var_name in remaining_vars:
|
194
|
+
for obj in global_vars.values():
|
195
|
+
if _is_typesafe_variable(obj) and (
|
196
|
+
(hasattr(obj, 'symbol') and obj.symbol == var_name) or
|
197
|
+
(hasattr(obj, 'name') and obj.name == var_name)
|
198
|
+
):
|
199
|
+
discovered[var_name] = obj
|
200
|
+
break
|
201
|
+
|
153
202
|
return discovered
|
154
203
|
|
155
204
|
finally:
|
@@ -205,93 +254,4 @@ class Equation:
|
|
205
254
|
return f"Equation(name='{self.name}', lhs={self.lhs!r}, rhs={self.rhs!r})"
|
206
255
|
|
207
256
|
|
208
|
-
|
209
|
-
"""System of equations that can be solved together."""
|
210
|
-
|
211
|
-
def __init__(self, equations: list[Equation] | None = None):
|
212
|
-
self.equations = equations or []
|
213
|
-
self.variables = {} # Dict[str, TypeSafeVariable]
|
214
|
-
|
215
|
-
def add_equation(self, equation: Equation):
|
216
|
-
"""Add an equation to the system."""
|
217
|
-
self.equations.append(equation)
|
218
|
-
|
219
|
-
def add_variable(self, variable: TypeSafeVariable):
|
220
|
-
"""Add a variable to the system."""
|
221
|
-
self.variables[variable.name] = variable
|
222
|
-
|
223
|
-
def get_known_variables(self) -> set[str]:
|
224
|
-
"""Get names of all known variables."""
|
225
|
-
return {name for name, var in self.variables.items() if var.is_known and var.quantity is not None}
|
226
|
-
|
227
|
-
def get_unknown_variables(self) -> set[str]:
|
228
|
-
"""Get names of all unknown variables."""
|
229
|
-
return {name for name, var in self.variables.items() if not var.is_known or var.quantity is None}
|
230
|
-
|
231
|
-
def can_solve_any(self) -> bool:
|
232
|
-
"""Check if any equation can be solved with current known variables."""
|
233
|
-
known_vars = self.get_known_variables()
|
234
|
-
unknown_vars = self.get_unknown_variables()
|
235
|
-
|
236
|
-
for equation in self.equations:
|
237
|
-
for unknown_var in unknown_vars:
|
238
|
-
if equation.can_solve_for(unknown_var, known_vars):
|
239
|
-
return True
|
240
|
-
return False
|
241
|
-
|
242
|
-
def solve_step(self) -> bool:
|
243
|
-
"""Solve one step - find and solve one equation. Returns True if progress made."""
|
244
|
-
known_vars = self.get_known_variables()
|
245
|
-
unknown_vars = self.get_unknown_variables()
|
246
|
-
|
247
|
-
# Find an equation that can be solved
|
248
|
-
for equation in self.equations:
|
249
|
-
for unknown_var in unknown_vars:
|
250
|
-
if equation.can_solve_for(unknown_var, known_vars):
|
251
|
-
# Solve for this variable
|
252
|
-
equation.solve_for(unknown_var, self.variables)
|
253
|
-
return True # Progress made
|
254
|
-
|
255
|
-
return False # No progress possible
|
256
|
-
|
257
|
-
def solve(self, max_iterations: int = 100) -> bool:
|
258
|
-
"""Solve the system iteratively. Returns True if fully solved."""
|
259
|
-
for _ in range(max_iterations):
|
260
|
-
if not self.can_solve_any():
|
261
|
-
break
|
262
|
-
if not self.solve_step():
|
263
|
-
break
|
264
|
-
|
265
|
-
# Check if all variables are known
|
266
|
-
unknown_vars = self.get_unknown_variables()
|
267
|
-
return len(unknown_vars) == 0
|
268
|
-
|
269
|
-
def get_solving_order(self) -> list[str]:
|
270
|
-
"""Get the order in which variables can be solved."""
|
271
|
-
order = []
|
272
|
-
temp_system = EquationSystem(self.equations.copy())
|
273
|
-
temp_system.variables = self.variables.copy()
|
274
|
-
|
275
|
-
while temp_system.can_solve_any():
|
276
|
-
known_vars = temp_system.get_known_variables()
|
277
|
-
unknown_vars = temp_system.get_unknown_variables()
|
278
|
-
|
279
|
-
# Find next solvable variable
|
280
|
-
for equation in temp_system.equations:
|
281
|
-
for unknown_var in unknown_vars:
|
282
|
-
if equation.can_solve_for(unknown_var, known_vars):
|
283
|
-
order.append(unknown_var)
|
284
|
-
# Mark as known for next iteration
|
285
|
-
temp_system.variables[unknown_var].is_known = True
|
286
|
-
break
|
287
|
-
else:
|
288
|
-
continue
|
289
|
-
break
|
290
|
-
|
291
|
-
return order
|
292
|
-
|
293
|
-
def __str__(self) -> str:
|
294
|
-
return f"EquationSystem({len(self.equations)} equations, {len(self.variables)} variables)"
|
295
|
-
|
296
|
-
def __repr__(self) -> str:
|
297
|
-
return f"EquationSystem(equations={self.equations!r})"
|
257
|
+
|
qnty/equations/system.py
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
from ..quantities.quantity import TypeSafeVariable
|
2
|
+
from .equation import Equation
|
3
|
+
|
4
|
+
|
5
|
+
class EquationSystem:
|
6
|
+
"""
|
7
|
+
System of equations that can be solved together.
|
8
|
+
Optimized with __slots__ for memory efficiency.
|
9
|
+
"""
|
10
|
+
__slots__ = ('equations', 'variables', '_known_cache', '_unknown_cache')
|
11
|
+
|
12
|
+
def __init__(self, equations: list[Equation] | None = None):
|
13
|
+
self.equations = equations or []
|
14
|
+
self.variables = {} # Dict[str, TypeSafeVariable]
|
15
|
+
self._known_cache: set[str] | None = None # Cache for known variables
|
16
|
+
self._unknown_cache: set[str] | None = None # Cache for unknown variables
|
17
|
+
|
18
|
+
def add_equation(self, equation: Equation):
|
19
|
+
"""Add an equation to the system."""
|
20
|
+
self.equations.append(equation)
|
21
|
+
self._invalidate_caches()
|
22
|
+
|
23
|
+
def add_variable(self, variable: TypeSafeVariable):
|
24
|
+
"""Add a variable to the system."""
|
25
|
+
self.variables[variable.name] = variable
|
26
|
+
self._invalidate_caches()
|
27
|
+
|
28
|
+
def _invalidate_caches(self):
|
29
|
+
"""Invalidate cached known/unknown variable sets."""
|
30
|
+
self._known_cache = None
|
31
|
+
self._unknown_cache = None
|
32
|
+
|
33
|
+
def get_known_variables(self) -> set[str]:
|
34
|
+
"""Get names of all known variables (cached)."""
|
35
|
+
if self._known_cache is None:
|
36
|
+
self._known_cache = {name for name, var in self.variables.items() if var.is_known and var.quantity is not None}
|
37
|
+
return self._known_cache
|
38
|
+
|
39
|
+
def get_unknown_variables(self) -> set[str]:
|
40
|
+
"""Get names of all unknown variables (cached)."""
|
41
|
+
if self._unknown_cache is None:
|
42
|
+
self._unknown_cache = {name for name, var in self.variables.items() if not var.is_known or var.quantity is None}
|
43
|
+
return self._unknown_cache
|
44
|
+
|
45
|
+
def can_solve_any(self) -> bool:
|
46
|
+
"""Check if any equation can be solved with current known variables."""
|
47
|
+
known_vars = self.get_known_variables()
|
48
|
+
unknown_vars = self.get_unknown_variables()
|
49
|
+
|
50
|
+
for equation in self.equations:
|
51
|
+
for unknown_var in unknown_vars:
|
52
|
+
if equation.can_solve_for(unknown_var, known_vars):
|
53
|
+
return True
|
54
|
+
return False
|
55
|
+
|
56
|
+
def solve_step(self) -> bool:
|
57
|
+
"""Solve one step - find and solve one equation. Returns True if progress made."""
|
58
|
+
known_vars = self.get_known_variables()
|
59
|
+
unknown_vars = self.get_unknown_variables()
|
60
|
+
|
61
|
+
# Find an equation that can be solved
|
62
|
+
for equation in self.equations:
|
63
|
+
for unknown_var in unknown_vars:
|
64
|
+
if equation.can_solve_for(unknown_var, known_vars):
|
65
|
+
# Solve for this variable
|
66
|
+
equation.solve_for(unknown_var, self.variables)
|
67
|
+
# Invalidate caches since variable states have changed
|
68
|
+
self._invalidate_caches()
|
69
|
+
return True # Progress made
|
70
|
+
|
71
|
+
return False # No progress possible
|
72
|
+
|
73
|
+
def solve(self, max_iterations: int = 100) -> bool:
|
74
|
+
"""Solve the system iteratively. Returns True if fully solved."""
|
75
|
+
for _ in range(max_iterations):
|
76
|
+
if not self.can_solve_any():
|
77
|
+
break
|
78
|
+
if not self.solve_step():
|
79
|
+
break
|
80
|
+
|
81
|
+
# Check if all variables are known
|
82
|
+
unknown_vars = self.get_unknown_variables()
|
83
|
+
return len(unknown_vars) == 0
|
84
|
+
|
85
|
+
def get_solving_order(self) -> list[str]:
|
86
|
+
"""
|
87
|
+
Get the order in which variables can be solved.
|
88
|
+
Optimized to avoid creating full system copies.
|
89
|
+
"""
|
90
|
+
order = []
|
91
|
+
# Track known variables without modifying the original
|
92
|
+
simulated_known = self.get_known_variables().copy()
|
93
|
+
|
94
|
+
# Continue until no more variables can be solved
|
95
|
+
max_iterations = len(self.variables) # Prevent infinite loops
|
96
|
+
iterations = 0
|
97
|
+
|
98
|
+
while iterations < max_iterations:
|
99
|
+
unknown_vars = {name for name in self.variables.keys() if name not in simulated_known}
|
100
|
+
if not unknown_vars:
|
101
|
+
break
|
102
|
+
|
103
|
+
found_solvable = False
|
104
|
+
# Find next solvable variable
|
105
|
+
for equation in self.equations:
|
106
|
+
for unknown_var in unknown_vars:
|
107
|
+
if equation.can_solve_for(unknown_var, simulated_known):
|
108
|
+
order.append(unknown_var)
|
109
|
+
# Simulate marking as known for next iteration
|
110
|
+
simulated_known.add(unknown_var)
|
111
|
+
found_solvable = True
|
112
|
+
break
|
113
|
+
if found_solvable:
|
114
|
+
break
|
115
|
+
|
116
|
+
if not found_solvable:
|
117
|
+
break # No more progress possible
|
118
|
+
|
119
|
+
iterations += 1
|
120
|
+
|
121
|
+
return order
|
122
|
+
|
123
|
+
def __str__(self) -> str:
|
124
|
+
return f"EquationSystem({len(self.equations)} equations, {len(self.variables)} variables)"
|
125
|
+
|
126
|
+
def __repr__(self) -> str:
|
127
|
+
return f"EquationSystem(equations={self.equations!r})"
|
@@ -0,0 +1,61 @@
|
|
1
|
+
"""
|
2
|
+
Expression System Package
|
3
|
+
=========================
|
4
|
+
|
5
|
+
Mathematical expressions for building equation trees with qnty variables.
|
6
|
+
"""
|
7
|
+
|
8
|
+
# Core AST classes
|
9
|
+
from .nodes import (
|
10
|
+
Expression,
|
11
|
+
VariableReference,
|
12
|
+
Constant,
|
13
|
+
BinaryOperation,
|
14
|
+
UnaryFunction,
|
15
|
+
ConditionalExpression
|
16
|
+
)
|
17
|
+
|
18
|
+
# Helper functions
|
19
|
+
from .functions import (
|
20
|
+
sin,
|
21
|
+
cos,
|
22
|
+
tan,
|
23
|
+
sqrt,
|
24
|
+
abs_expr,
|
25
|
+
ln,
|
26
|
+
log10,
|
27
|
+
exp,
|
28
|
+
cond_expr,
|
29
|
+
min_expr,
|
30
|
+
max_expr
|
31
|
+
)
|
32
|
+
|
33
|
+
# Cache utilities
|
34
|
+
from .cache import wrap_operand
|
35
|
+
|
36
|
+
# Define public API
|
37
|
+
__all__ = [
|
38
|
+
# Core AST classes
|
39
|
+
'Expression',
|
40
|
+
'VariableReference',
|
41
|
+
'Constant',
|
42
|
+
'BinaryOperation',
|
43
|
+
'UnaryFunction',
|
44
|
+
'ConditionalExpression',
|
45
|
+
|
46
|
+
# Helper functions
|
47
|
+
'sin',
|
48
|
+
'cos',
|
49
|
+
'tan',
|
50
|
+
'sqrt',
|
51
|
+
'abs_expr',
|
52
|
+
'ln',
|
53
|
+
'log10',
|
54
|
+
'exp',
|
55
|
+
'cond_expr',
|
56
|
+
'min_expr',
|
57
|
+
'max_expr',
|
58
|
+
|
59
|
+
# Utilities
|
60
|
+
'wrap_operand'
|
61
|
+
]
|
@@ -0,0 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Expression Caching System
|
3
|
+
========================
|
4
|
+
|
5
|
+
Caching infrastructure for optimized expression evaluation and type checking.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import TYPE_CHECKING, Union
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from ..quantities.quantity import Quantity, TypeSafeVariable
|
12
|
+
from .nodes import Expression
|
13
|
+
|
14
|
+
# Import here to avoid circular imports - delayed imports
|
15
|
+
from ..generated.units import DimensionlessUnits
|
16
|
+
from ..quantities.quantity import Quantity, TypeSafeVariable
|
17
|
+
|
18
|
+
# Cache for common types to avoid repeated type checks
|
19
|
+
_NUMERIC_TYPES = (int, float)
|
20
|
+
_DIMENSIONLESS_CONSTANT = None
|
21
|
+
_CACHED_DIMENSIONLESS_QUANTITIES = {} # Cache for common numeric values
|
22
|
+
_MAX_CACHE_SIZE = 50 # Limit cache size to prevent memory bloat
|
23
|
+
_TYPE_CHECK_CACHE = {} # Cache for expensive isinstance checks
|
24
|
+
|
25
|
+
# Expression evaluation cache for repeated operations
|
26
|
+
_EXPRESSION_RESULT_CACHE = {}
|
27
|
+
_MAX_EXPRESSION_CACHE_SIZE = 200
|
28
|
+
|
29
|
+
|
30
|
+
def _get_cached_dimensionless():
|
31
|
+
"""Get cached dimensionless constant for numeric values."""
|
32
|
+
global _DIMENSIONLESS_CONSTANT
|
33
|
+
if _DIMENSIONLESS_CONSTANT is None:
|
34
|
+
_DIMENSIONLESS_CONSTANT = DimensionlessUnits.dimensionless
|
35
|
+
return _DIMENSIONLESS_CONSTANT
|
36
|
+
|
37
|
+
|
38
|
+
def _get_dimensionless_quantity(value: float) -> 'Quantity':
|
39
|
+
"""Get cached dimensionless quantity for common numeric values."""
|
40
|
+
if value in _CACHED_DIMENSIONLESS_QUANTITIES:
|
41
|
+
return _CACHED_DIMENSIONLESS_QUANTITIES[value]
|
42
|
+
|
43
|
+
# Cache common values with size limit
|
44
|
+
if len(_CACHED_DIMENSIONLESS_QUANTITIES) < _MAX_CACHE_SIZE and -10 <= value <= 10:
|
45
|
+
qty = Quantity(value, _get_cached_dimensionless())
|
46
|
+
_CACHED_DIMENSIONLESS_QUANTITIES[value] = qty
|
47
|
+
return qty
|
48
|
+
|
49
|
+
# Don't cache uncommon values
|
50
|
+
return Quantity(value, _get_cached_dimensionless())
|
51
|
+
|
52
|
+
|
53
|
+
def _is_numeric_type(obj) -> bool:
|
54
|
+
"""Cached type check for numeric types."""
|
55
|
+
obj_type = type(obj)
|
56
|
+
if obj_type not in _TYPE_CHECK_CACHE:
|
57
|
+
_TYPE_CHECK_CACHE[obj_type] = obj_type in _NUMERIC_TYPES
|
58
|
+
return _TYPE_CHECK_CACHE[obj_type]
|
59
|
+
|
60
|
+
|
61
|
+
def wrap_operand(operand: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> 'Expression':
|
62
|
+
"""
|
63
|
+
Optimized operand wrapping with cached type checks.
|
64
|
+
|
65
|
+
This function uses cached type checks for maximum performance.
|
66
|
+
"""
|
67
|
+
# Import Expression classes to avoid circular imports
|
68
|
+
from .nodes import Constant, Expression, VariableReference
|
69
|
+
|
70
|
+
# Fast path: check most common cases first using cached type check
|
71
|
+
if _is_numeric_type(operand):
|
72
|
+
# operand is guaranteed to be int or float at this point
|
73
|
+
return Constant(_get_dimensionless_quantity(float(operand))) # type: ignore[arg-type]
|
74
|
+
|
75
|
+
# Check if already an Expression (using isinstance for speed)
|
76
|
+
if isinstance(operand, Expression):
|
77
|
+
return operand
|
78
|
+
|
79
|
+
# Check for FastQuantity
|
80
|
+
if isinstance(operand, Quantity):
|
81
|
+
return Constant(operand)
|
82
|
+
|
83
|
+
# Check for TypeSafeVariable
|
84
|
+
if isinstance(operand, TypeSafeVariable):
|
85
|
+
return VariableReference(operand)
|
86
|
+
|
87
|
+
# Check for ConfigurableVariable (from composition system)
|
88
|
+
if hasattr(operand, '_variable'):
|
89
|
+
var = getattr(operand, '_variable', None)
|
90
|
+
if isinstance(var, TypeSafeVariable):
|
91
|
+
return VariableReference(var)
|
92
|
+
|
93
|
+
# No duck typing - fail fast for unknown types
|
94
|
+
raise TypeError(f"Cannot convert {type(operand)} to Expression")
|
@@ -0,0 +1,96 @@
|
|
1
|
+
"""
|
2
|
+
Expression Helper Functions
|
3
|
+
==========================
|
4
|
+
|
5
|
+
Convenience functions for creating mathematical expressions.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import TYPE_CHECKING, Union
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from ..quantities.quantity import Quantity, TypeSafeVariable
|
12
|
+
from .nodes import BinaryOperation, Expression
|
13
|
+
|
14
|
+
from .nodes import ConditionalExpression, Expression, UnaryFunction
|
15
|
+
|
16
|
+
|
17
|
+
# Convenience functions for mathematical operations
|
18
|
+
def sin(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
|
19
|
+
"""Sine function."""
|
20
|
+
return UnaryFunction('sin', Expression._wrap_operand(expr))
|
21
|
+
|
22
|
+
|
23
|
+
def cos(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
|
24
|
+
"""Cosine function."""
|
25
|
+
return UnaryFunction('cos', Expression._wrap_operand(expr))
|
26
|
+
|
27
|
+
|
28
|
+
def tan(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
|
29
|
+
"""Tangent function."""
|
30
|
+
return UnaryFunction('tan', Expression._wrap_operand(expr))
|
31
|
+
|
32
|
+
|
33
|
+
def sqrt(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
|
34
|
+
"""Square root function."""
|
35
|
+
return UnaryFunction('sqrt', Expression._wrap_operand(expr))
|
36
|
+
|
37
|
+
|
38
|
+
def abs_expr(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
|
39
|
+
"""Absolute value function."""
|
40
|
+
return UnaryFunction('abs', Expression._wrap_operand(expr))
|
41
|
+
|
42
|
+
|
43
|
+
def ln(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
|
44
|
+
"""Natural logarithm function."""
|
45
|
+
return UnaryFunction('ln', Expression._wrap_operand(expr))
|
46
|
+
|
47
|
+
|
48
|
+
def log10(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
|
49
|
+
"""Base-10 logarithm function."""
|
50
|
+
return UnaryFunction('log10', Expression._wrap_operand(expr))
|
51
|
+
|
52
|
+
|
53
|
+
def exp(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
|
54
|
+
"""Exponential function."""
|
55
|
+
return UnaryFunction('exp', Expression._wrap_operand(expr))
|
56
|
+
|
57
|
+
|
58
|
+
def cond_expr(condition: Union['Expression', 'BinaryOperation'],
|
59
|
+
true_expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float],
|
60
|
+
false_expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> ConditionalExpression:
|
61
|
+
"""Conditional expression: if condition then true_expr else false_expr."""
|
62
|
+
return ConditionalExpression(
|
63
|
+
condition if isinstance(condition, Expression) else condition,
|
64
|
+
Expression._wrap_operand(true_expr),
|
65
|
+
Expression._wrap_operand(false_expr)
|
66
|
+
)
|
67
|
+
|
68
|
+
|
69
|
+
def min_expr(*expressions: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> 'Expression':
|
70
|
+
"""Minimum of multiple expressions."""
|
71
|
+
if len(expressions) < 2:
|
72
|
+
raise ValueError("min_expr requires at least 2 arguments")
|
73
|
+
|
74
|
+
wrapped_expressions = [Expression._wrap_operand(expr) for expr in expressions]
|
75
|
+
result = wrapped_expressions[0]
|
76
|
+
|
77
|
+
for expr in wrapped_expressions[1:]:
|
78
|
+
# min(a, b) = if(a < b, a, b)
|
79
|
+
result = cond_expr(result < expr, result, expr)
|
80
|
+
|
81
|
+
return result
|
82
|
+
|
83
|
+
|
84
|
+
def max_expr(*expressions: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> 'Expression':
|
85
|
+
"""Maximum of multiple expressions."""
|
86
|
+
if len(expressions) < 2:
|
87
|
+
raise ValueError("max_expr requires at least 2 arguments")
|
88
|
+
|
89
|
+
wrapped_expressions = [Expression._wrap_operand(expr) for expr in expressions]
|
90
|
+
result = wrapped_expressions[0]
|
91
|
+
|
92
|
+
for expr in wrapped_expressions[1:]:
|
93
|
+
# max(a, b) = if(a > b, a, b)
|
94
|
+
result = cond_expr(result > expr, result, expr)
|
95
|
+
|
96
|
+
return result
|