qnty 0.0.7__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 (76) 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/equations/equation.py +257 -0
  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/expressions/nodes.py +546 -0
  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/equation.py +0 -216
  63. qnty/expression.py +0 -492
  64. qnty/unit_types/base.py +0 -47
  65. qnty/units.py +0 -8113
  66. qnty/variable.py +0 -263
  67. qnty/variable_types/base.py +0 -58
  68. qnty/variable_types/expression_variable.py +0 -68
  69. qnty/variable_types/typed_variable.py +0 -87
  70. qnty/variables.py +0 -2298
  71. qnty/variables.pyi +0 -6148
  72. qnty-0.0.7.dist-info/METADATA +0 -355
  73. qnty-0.0.7.dist-info/RECORD +0 -19
  74. /qnty/{unit_types → codegen}/__init__.py +0 -0
  75. /qnty/{variable_types → codegen/generators}/__init__.py +0 -0
  76. {qnty-0.0.7.dist-info → qnty-0.0.9.dist-info}/WHEEL +0 -0
@@ -0,0 +1,257 @@
1
+ """
2
+ Equation System
3
+ ===============
4
+
5
+ Mathematical equations for qnty variables with solving capabilities.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import cast
11
+
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]
28
+
29
+
30
+ class Equation:
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')
36
+
37
+ def __init__(self, name: str, lhs: TypeSafeVariable | Expression, rhs: Expression):
38
+ self.name = name
39
+
40
+ # Convert Variable to VariableReference if needed - use isinstance for performance
41
+ if isinstance(lhs, TypeSafeVariable):
42
+ self.lhs = VariableReference(lhs)
43
+ else:
44
+ # It's already an Expression
45
+ self.lhs = cast(Expression, lhs)
46
+
47
+ self.rhs = rhs
48
+ self._variables: set[str] | None = None # Lazy initialization for better performance
49
+
50
+ def get_all_variables(self) -> set[str]:
51
+ """Get all variable names used in this equation."""
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()
63
+
64
+ def get_unknown_variables(self, known_vars: set[str]) -> set[str]:
65
+ """Get variables that are unknown (not in known_vars set)."""
66
+ return self.variables - known_vars
67
+
68
+ def get_known_variables(self, known_vars: set[str]) -> set[str]:
69
+ """Get variables that are known (in known_vars set)."""
70
+ return self.variables & known_vars
71
+
72
+ def can_solve_for(self, target_var: str, known_vars: set[str]) -> bool:
73
+ """Check if this equation can solve for target_var given known_vars."""
74
+ if target_var not in self.variables:
75
+ return False
76
+ # Direct assignment case: lhs is the variable
77
+ if isinstance(self.lhs, VariableReference) and self.lhs.name == target_var:
78
+ rhs_vars = self.rhs.get_variables()
79
+ return rhs_vars.issubset(known_vars)
80
+ unknown_vars = self.get_unknown_variables(known_vars)
81
+ # Can solve if target_var is the only unknown
82
+ return unknown_vars == {target_var}
83
+
84
+ def solve_for(self, target_var: str, variable_values: dict[str, TypeSafeVariable]) -> TypeSafeVariable:
85
+ """
86
+ Solve the equation for target_var.
87
+ Returns the target variable with updated quantity.
88
+ """
89
+ if target_var not in self.variables:
90
+ raise ValueError(f"Variable '{target_var}' not found in equation")
91
+
92
+ # Handle direct assignment: target = expression
93
+ if isinstance(self.lhs, VariableReference) and self.lhs.name == target_var:
94
+ # Direct assignment: target_var = rhs
95
+ result_qty = self.rhs.evaluate(variable_values)
96
+
97
+ # Update existing variable object to preserve references
98
+ var_obj = variable_values.get(target_var)
99
+ if var_obj is not None:
100
+ # Convert result to the target variable's original unit if it had one
101
+ if var_obj.quantity is not None and var_obj.quantity.unit is not None:
102
+ # Convert to the target variable's defined unit - be more specific about exceptions
103
+ try:
104
+ result_qty = result_qty.to(var_obj.quantity.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
108
+ pass
109
+
110
+ var_obj.quantity = result_qty
111
+ var_obj.is_known = True
112
+ return var_obj
113
+
114
+ # Create new variable if not found - this shouldn't happen in normal usage
115
+ raise ValueError(f"Variable '{target_var}' not found in variable_values")
116
+
117
+ # For more complex equations, we would need algebraic manipulation
118
+ # Currently focusing on direct assignment which covers most engineering cases
119
+ raise NotImplementedError(f"Cannot solve for {target_var} in equation {self}. "
120
+ f"Only direct assignment equations (var = expression) are supported.")
121
+
122
+ def check_residual(self, variable_values: dict[str, TypeSafeVariable], tolerance: float = 1e-10) -> bool:
123
+ """
124
+ Check if equation is satisfied by evaluating residual (LHS - RHS).
125
+ Returns True if |residual| < tolerance, accounting for units.
126
+ """
127
+ try:
128
+ # Both lhs and rhs should be Expressions after __init__ conversion
129
+ lhs_value = self.lhs.evaluate(variable_values)
130
+ rhs_value = self.rhs.evaluate(variable_values)
131
+
132
+ # Check dimensional compatibility
133
+ if lhs_value._dimension_sig != rhs_value._dimension_sig:
134
+ return False
135
+
136
+ # Convert to same units for comparison
137
+ rhs_converted = rhs_value.to(lhs_value.unit)
138
+ residual = abs(lhs_value.value - rhs_converted.value)
139
+
140
+ return residual < tolerance
141
+ except (ValueError, TypeError, AttributeError, KeyError):
142
+ # Handle specific expected errors during evaluation/conversion
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
147
+
148
+ def _discover_variables_from_scope(self) -> dict[str, TypeSafeVariable]:
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
+
156
+ import inspect
157
+
158
+ # Get the frame that called this method (skip through __str__ calls)
159
+ frame = inspect.currentframe()
160
+ try:
161
+ # Skip frames until we find one outside the equation system
162
+ depth = 0
163
+ max_depth = 8 # Reduced from 10 for performance
164
+ while frame and depth < max_depth and (
165
+ frame.f_code.co_filename.endswith(('equation.py', 'expression.py')) or
166
+ frame.f_code.co_name in ['__str__', '__repr__']
167
+ ):
168
+ frame = frame.f_back
169
+ depth += 1
170
+
171
+ if not frame:
172
+ return {}
173
+
174
+ # Only get local variables first (faster than combining both)
175
+ local_vars = frame.f_locals
176
+ required_vars = self.variables
177
+ discovered = {}
178
+
179
+ # First pass: check locals only (most common case)
180
+ for var_name in required_vars:
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
+ ):
186
+ discovered[var_name] = obj
187
+ break
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
+
202
+ return discovered
203
+
204
+ finally:
205
+ del frame
206
+
207
+ def _can_auto_solve(self) -> tuple[bool, str, dict[str, TypeSafeVariable]]:
208
+ """Check if equation can be auto-solved from scope."""
209
+ try:
210
+ discovered = self._discover_variables_from_scope()
211
+
212
+ # Check if this is a simple assignment equation (one unknown)
213
+ unknowns = []
214
+ knowns = []
215
+
216
+ for var_name in self.variables:
217
+ if var_name in discovered:
218
+ var = discovered[var_name]
219
+ if hasattr(var, 'is_known') and not var.is_known:
220
+ unknowns.append(var_name)
221
+ elif hasattr(var, 'quantity') and var.quantity is not None:
222
+ knowns.append(var_name)
223
+ else:
224
+ unknowns.append(var_name) # Assume unknown if no quantity
225
+ else:
226
+ return False, "", {} # Missing variable
227
+
228
+ # Can only auto-solve if there's exactly one unknown
229
+ if len(unknowns) == 1:
230
+ return True, unknowns[0], discovered
231
+
232
+ return False, "", {}
233
+
234
+ except Exception:
235
+ return False, "", {}
236
+
237
+ def _try_auto_solve(self) -> bool:
238
+ """Try to automatically solve the equation if possible."""
239
+ try:
240
+ can_solve, target_var, variables = self._can_auto_solve()
241
+ if can_solve:
242
+ self.solve_for(target_var, variables)
243
+ return True
244
+ return False
245
+ except Exception:
246
+ return False
247
+
248
+ def __str__(self) -> str:
249
+ # Try to auto-solve if possible before displaying
250
+ self._try_auto_solve()
251
+ return f"{self.lhs} = {self.rhs}"
252
+
253
+ def __repr__(self) -> str:
254
+ return f"Equation(name='{self.name}', lhs={self.lhs!r}, rhs={self.rhs!r})"
255
+
256
+
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