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.
Files changed (74) hide show
  1. qnty/__init__.py +140 -58
  2. qnty/_backup/problem_original.py +1251 -0
  3. qnty/_backup/quantity.py +63 -0
  4. qnty/codegen/cli.py +125 -0
  5. qnty/codegen/generators/data/unit_data.json +8807 -0
  6. qnty/codegen/generators/data_processor.py +345 -0
  7. qnty/codegen/generators/dimensions_gen.py +434 -0
  8. qnty/codegen/generators/doc_generator.py +141 -0
  9. qnty/codegen/generators/out/dimension_mapping.json +974 -0
  10. qnty/codegen/generators/out/dimension_metadata.json +123 -0
  11. qnty/codegen/generators/out/units_metadata.json +223 -0
  12. qnty/codegen/generators/quantities_gen.py +159 -0
  13. qnty/codegen/generators/setters_gen.py +178 -0
  14. qnty/codegen/generators/stubs_gen.py +167 -0
  15. qnty/codegen/generators/units_gen.py +295 -0
  16. qnty/codegen/generators/utils/__init__.py +0 -0
  17. qnty/equations/__init__.py +4 -0
  18. qnty/{equation.py → equations/equation.py} +78 -118
  19. qnty/equations/system.py +127 -0
  20. qnty/expressions/__init__.py +61 -0
  21. qnty/expressions/cache.py +94 -0
  22. qnty/expressions/functions.py +96 -0
  23. qnty/{expression.py → expressions/nodes.py} +209 -216
  24. qnty/generated/__init__.py +0 -0
  25. qnty/generated/dimensions.py +514 -0
  26. qnty/generated/quantities.py +6003 -0
  27. qnty/generated/quantities.pyi +4192 -0
  28. qnty/generated/setters.py +12210 -0
  29. qnty/generated/units.py +9798 -0
  30. qnty/problem/__init__.py +91 -0
  31. qnty/problem/base.py +142 -0
  32. qnty/problem/composition.py +385 -0
  33. qnty/problem/composition_mixin.py +382 -0
  34. qnty/problem/equations.py +413 -0
  35. qnty/problem/metaclass.py +302 -0
  36. qnty/problem/reconstruction.py +1016 -0
  37. qnty/problem/solving.py +180 -0
  38. qnty/problem/validation.py +64 -0
  39. qnty/problem/variables.py +239 -0
  40. qnty/quantities/__init__.py +6 -0
  41. qnty/quantities/expression_quantity.py +314 -0
  42. qnty/quantities/quantity.py +428 -0
  43. qnty/quantities/typed_quantity.py +215 -0
  44. qnty/solving/__init__.py +0 -0
  45. qnty/solving/manager.py +90 -0
  46. qnty/solving/order.py +355 -0
  47. qnty/solving/solvers/__init__.py +20 -0
  48. qnty/solving/solvers/base.py +92 -0
  49. qnty/solving/solvers/iterative.py +185 -0
  50. qnty/solving/solvers/simultaneous.py +547 -0
  51. qnty/units/__init__.py +0 -0
  52. qnty/{prefixes.py → units/prefixes.py} +54 -33
  53. qnty/{unit.py → units/registry.py} +73 -32
  54. qnty/utils/__init__.py +0 -0
  55. qnty/utils/logging.py +40 -0
  56. qnty/validation/__init__.py +0 -0
  57. qnty/validation/registry.py +0 -0
  58. qnty/validation/rules.py +167 -0
  59. qnty-0.0.9.dist-info/METADATA +199 -0
  60. qnty-0.0.9.dist-info/RECORD +63 -0
  61. qnty/dimension.py +0 -186
  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 → codegen}/__init__.py +0 -0
  73. /qnty/{variable_types → codegen/generators}/__init__.py +0 -0
  74. {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 .expression import Expression, VariableReference
13
- from .variable import TypeSafeVariable
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
- """Represents a mathematical equation with left-hand side equal to right-hand side."""
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
- # Use duck typing to avoid circular import
24
- if hasattr(lhs, 'name') and hasattr(lhs, 'quantity') and hasattr(lhs, 'is_known'):
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.variables = self.get_all_variables()
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
- # Both lhs and rhs should be Expressions after __init__ conversion
37
- lhs_vars = self.lhs.get_variables()
38
- rhs_vars = self.rhs.get_variables()
39
- return lhs_vars | rhs_vars
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 Exception:
83
- # If conversion fails, keep the calculated unit
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 Exception:
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
- """Automatically discover variables from the calling scope."""
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
- while frame and (
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
- # Combine local and global variables
138
- all_vars = {**frame.f_globals, **frame.f_locals}
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 name, obj in all_vars.items():
146
- if hasattr(obj, 'symbol') and obj.symbol == var_name:
147
- discovered[var_name] = obj
148
- break
149
- elif hasattr(obj, 'name') and obj.name == var_name:
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
- class EquationSystem:
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
+
@@ -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