qnty 0.0.8__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. qnty/__init__.py +140 -59
  2. qnty/constants/__init__.py +10 -0
  3. qnty/constants/numerical.py +18 -0
  4. qnty/constants/solvers.py +6 -0
  5. qnty/constants/tests.py +6 -0
  6. qnty/dimensions/__init__.py +23 -0
  7. qnty/dimensions/base.py +97 -0
  8. qnty/dimensions/field_dims.py +126 -0
  9. qnty/dimensions/field_dims.pyi +128 -0
  10. qnty/dimensions/signature.py +111 -0
  11. qnty/equations/__init__.py +4 -0
  12. qnty/equations/equation.py +220 -0
  13. qnty/equations/system.py +130 -0
  14. qnty/expressions/__init__.py +40 -0
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +74 -0
  17. qnty/expressions/nodes.py +701 -0
  18. qnty/expressions/types.py +70 -0
  19. qnty/extensions/plotting/__init__.py +0 -0
  20. qnty/extensions/reporting/__init__.py +0 -0
  21. qnty/problems/__init__.py +145 -0
  22. qnty/problems/composition.py +1031 -0
  23. qnty/problems/problem.py +695 -0
  24. qnty/problems/rules.py +145 -0
  25. qnty/problems/solving.py +1216 -0
  26. qnty/problems/validation.py +127 -0
  27. qnty/quantities/__init__.py +29 -0
  28. qnty/quantities/base_qnty.py +677 -0
  29. qnty/quantities/field_converters.py +24004 -0
  30. qnty/quantities/field_qnty.py +1012 -0
  31. qnty/quantities/field_setter.py +12320 -0
  32. qnty/quantities/field_vars.py +6325 -0
  33. qnty/quantities/field_vars.pyi +4191 -0
  34. qnty/solving/__init__.py +0 -0
  35. qnty/solving/manager.py +96 -0
  36. qnty/solving/order.py +403 -0
  37. qnty/solving/solvers/__init__.py +13 -0
  38. qnty/solving/solvers/base.py +82 -0
  39. qnty/solving/solvers/iterative.py +165 -0
  40. qnty/solving/solvers/simultaneous.py +475 -0
  41. qnty/units/__init__.py +1 -0
  42. qnty/units/field_units.py +10507 -0
  43. qnty/units/field_units.pyi +2461 -0
  44. qnty/units/prefixes.py +203 -0
  45. qnty/{unit.py → units/registry.py} +89 -61
  46. qnty/utils/__init__.py +16 -0
  47. qnty/utils/caching/__init__.py +23 -0
  48. qnty/utils/caching/manager.py +401 -0
  49. qnty/utils/error_handling/__init__.py +66 -0
  50. qnty/utils/error_handling/context.py +39 -0
  51. qnty/utils/error_handling/exceptions.py +96 -0
  52. qnty/utils/error_handling/handlers.py +171 -0
  53. qnty/utils/logging.py +40 -0
  54. qnty/utils/protocols.py +164 -0
  55. qnty/utils/scope_discovery.py +420 -0
  56. qnty-0.1.0.dist-info/METADATA +199 -0
  57. qnty-0.1.0.dist-info/RECORD +60 -0
  58. qnty/dimension.py +0 -186
  59. qnty/equation.py +0 -297
  60. qnty/expression.py +0 -553
  61. qnty/prefixes.py +0 -229
  62. qnty/unit_types/base.py +0 -47
  63. qnty/units.py +0 -8113
  64. qnty/variable.py +0 -300
  65. qnty/variable_types/base.py +0 -58
  66. qnty/variable_types/expression_variable.py +0 -106
  67. qnty/variable_types/typed_variable.py +0 -87
  68. qnty/variables.py +0 -2298
  69. qnty/variables.pyi +0 -6148
  70. qnty-0.0.8.dist-info/METADATA +0 -355
  71. qnty-0.0.8.dist-info/RECORD +0 -19
  72. /qnty/{unit_types → extensions}/__init__.py +0 -0
  73. /qnty/{variable_types → extensions/integration}/__init__.py +0 -0
  74. {qnty-0.0.8.dist-info → qnty-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,111 @@
1
+ """
2
+ Dimension Signatures
3
+ ====================
4
+
5
+ Immutable dimension signatures for ultra-fast dimensional analysis using prime number encoding.
6
+
7
+ This file contains the core DimensionSignature class that provides zero-cost dimensional
8
+ compatibility checking through compile-time type system integration.
9
+ """
10
+
11
+ from dataclasses import dataclass
12
+ from typing import ClassVar, final
13
+
14
+ from .base import BaseDimension
15
+
16
+
17
+ @final
18
+ @dataclass(frozen=True, slots=True)
19
+ class DimensionSignature:
20
+ """Immutable dimension signature for zero-cost dimensional analysis."""
21
+
22
+ # Store as bit pattern for ultra-fast comparison
23
+ _signature: int | float = 1
24
+
25
+ # Instance cache for interning common dimensions
26
+ _INSTANCE_CACHE: ClassVar[dict[int | float, "DimensionSignature"]] = {}
27
+
28
+ # Maximum cache size to prevent memory issues
29
+ _MAX_CACHE_SIZE: ClassVar[int] = 100
30
+
31
+ def __new__(cls, signature: int | float = 1):
32
+ """Optimized constructor with instance interning and validation."""
33
+ # Input validation
34
+ if not isinstance(signature, int | float):
35
+ raise TypeError(f"Signature must be int or float, got {type(signature)}")
36
+ if signature <= 0:
37
+ raise ValueError(f"Signature must be positive, got {signature}")
38
+
39
+ if signature in cls._INSTANCE_CACHE:
40
+ return cls._INSTANCE_CACHE[signature]
41
+
42
+ instance = object.__new__(cls)
43
+
44
+ # Cache common signatures with size limit
45
+ if len(cls._INSTANCE_CACHE) < cls._MAX_CACHE_SIZE:
46
+ cls._INSTANCE_CACHE[signature] = instance
47
+
48
+ return instance
49
+
50
+ @classmethod
51
+ def create(cls, length: int = 0, mass: int = 0, time: int = 0, current: int = 0, temp: int = 0, amount: int = 0, luminosity: int = 0):
52
+ """Create dimension from exponents with efficient computation."""
53
+ # Fast path for dimensionless
54
+ if not any([length, mass, time, current, temp, amount, luminosity]):
55
+ return cls(1)
56
+
57
+ # Compute signature using tuple of (base, exponent) pairs for efficiency
58
+ signature = 1.0
59
+ dimensions = [
60
+ (BaseDimension.LENGTH, length),
61
+ (BaseDimension.MASS, mass),
62
+ (BaseDimension.TIME, time),
63
+ (BaseDimension.CURRENT, current),
64
+ (BaseDimension.TEMPERATURE, temp),
65
+ (BaseDimension.AMOUNT, amount),
66
+ (BaseDimension.LUMINOSITY, luminosity),
67
+ ]
68
+
69
+ for base, exponent in dimensions:
70
+ if exponent != 0:
71
+ signature *= base**exponent
72
+
73
+ return cls(signature)
74
+
75
+ def __mul__(self, other: "DimensionSignature") -> "DimensionSignature":
76
+ """Multiply dimensions."""
77
+ if not isinstance(other, DimensionSignature):
78
+ raise TypeError(f"Cannot multiply DimensionSignature with {type(other)}")
79
+ return DimensionSignature(self._signature * other._signature)
80
+
81
+ def __truediv__(self, other: "DimensionSignature") -> "DimensionSignature":
82
+ """Divide dimensions."""
83
+ if not isinstance(other, DimensionSignature):
84
+ raise TypeError(f"Cannot divide DimensionSignature by {type(other)}")
85
+ return DimensionSignature(self._signature / other._signature)
86
+
87
+ def __pow__(self, power: int | float) -> "DimensionSignature":
88
+ """Raise dimension to a power."""
89
+ if not isinstance(power, int | float):
90
+ raise TypeError(f"Power must be int or float, got {type(power)}")
91
+ if power == 1:
92
+ return self
93
+ if power == 0:
94
+ return DimensionSignature(1)
95
+ return DimensionSignature(self._signature**power)
96
+
97
+ def is_compatible(self, other: "DimensionSignature") -> bool:
98
+ """Check dimensional compatibility."""
99
+ if not isinstance(other, DimensionSignature):
100
+ return False
101
+ return self._signature == other._signature
102
+
103
+ def __eq__(self, other: object) -> bool:
104
+ """Check equality."""
105
+ if self is other:
106
+ return True
107
+ return isinstance(other, DimensionSignature) and self._signature == other._signature
108
+
109
+ def __hash__(self) -> int:
110
+ """Hash based on signature."""
111
+ return hash(self._signature)
@@ -0,0 +1,4 @@
1
+ from .equation import Equation
2
+ from .system import EquationSystem
3
+
4
+ __all__ = ["Equation", "EquationSystem"]
@@ -0,0 +1,220 @@
1
+ """
2
+ Equation System
3
+ ===============
4
+
5
+ Mathematical equations for qnty variables with solving capabilities.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import cast
12
+
13
+ from ..constants import SOLVER_DEFAULT_TOLERANCE
14
+ from ..expressions import Expression, VariableReference
15
+ from ..quantities import FieldQnty
16
+ from ..utils.scope_discovery import ScopeDiscoveryService
17
+
18
+ _logger = logging.getLogger(__name__)
19
+
20
+ # Global optimization flags
21
+ _SCOPE_DISCOVERY_ENABLED = False # Disabled by default due to high overhead
22
+
23
+
24
+ class Equation:
25
+ """
26
+ Represents a mathematical equation with left-hand side equal to right-hand side.
27
+ Optimized with __slots__ for memory efficiency.
28
+ """
29
+
30
+ __slots__ = ("name", "lhs", "rhs", "_variables")
31
+
32
+ def __init__(self, name: str, lhs: FieldQnty | Expression, rhs: Expression):
33
+ self.name = name
34
+
35
+ # Convert Variable to VariableReference if needed - use isinstance for performance
36
+ if isinstance(lhs, FieldQnty):
37
+ self.lhs = VariableReference(lhs)
38
+ else:
39
+ # It's already an Expression
40
+ self.lhs = cast(Expression, lhs)
41
+
42
+ self.rhs = rhs
43
+ self._variables: set[str] | None = None # Lazy initialization for better performance
44
+
45
+ def get_all_variables(self) -> set[str]:
46
+ """Get all variable names used in this equation."""
47
+ if self._variables is None:
48
+ # Both lhs and rhs should be Expressions after __init__ conversion
49
+ lhs_vars = self.lhs.get_variables()
50
+ rhs_vars = self.rhs.get_variables()
51
+ self._variables = lhs_vars | rhs_vars
52
+ return self._variables
53
+
54
+ @property
55
+ def variables(self) -> set[str]:
56
+ """Get all variable names used in this equation (cached property)."""
57
+ return self.get_all_variables()
58
+
59
+ def get_unknown_variables(self, known_vars: set[str]) -> set[str]:
60
+ """Get variables that are unknown (not in known_vars set)."""
61
+ return self.variables - known_vars
62
+
63
+ def get_known_variables(self, known_vars: set[str]) -> set[str]:
64
+ """Get variables that are known (in known_vars set)."""
65
+ return self.variables & known_vars
66
+
67
+ def can_solve_for(self, target_var: str, known_vars: set[str]) -> bool:
68
+ """Check if this equation can solve for target_var given known_vars."""
69
+ if target_var not in self.variables:
70
+ return False
71
+
72
+ # Direct assignment case: lhs is the variable
73
+ if isinstance(self.lhs, VariableReference) and self.lhs.name == target_var:
74
+ rhs_vars = self.rhs.get_variables()
75
+ return rhs_vars.issubset(known_vars)
76
+
77
+ # General case: can solve if target_var is the only unknown
78
+ unknown_vars = self.get_unknown_variables(known_vars)
79
+ return unknown_vars == {target_var}
80
+
81
+ def solve_for(self, target_var: str, variable_values: dict[str, FieldQnty]) -> FieldQnty:
82
+ """
83
+ Solve the equation for target_var.
84
+ Returns the target variable with updated quantity.
85
+ """
86
+ if target_var not in self.variables:
87
+ raise ValueError(f"Variable '{target_var}' not found in equation")
88
+
89
+ # Only handle direct assignment: target = expression
90
+ if not (isinstance(self.lhs, VariableReference) and self.lhs.name == target_var):
91
+ raise NotImplementedError(f"Cannot solve for {target_var} in equation {self}. Only direct assignment equations (var = expression) are supported.")
92
+
93
+ # Direct assignment: target_var = rhs
94
+ result_qty = self.rhs.evaluate(variable_values)
95
+
96
+ # Get the variable object to update
97
+ var_obj = variable_values.get(target_var)
98
+ if var_obj is None:
99
+ raise ValueError(f"Variable '{target_var}' not found in variable_values")
100
+
101
+ # Convert result to the target variable's original unit if it had one
102
+ if var_obj.quantity is not None and var_obj.quantity.unit is not None:
103
+ try:
104
+ result_qty = result_qty.to(var_obj.quantity.unit)
105
+ except (ValueError, TypeError, AttributeError) as e:
106
+ _logger.debug(f"Unit conversion failed for {target_var}: {e}. Using calculated unit.")
107
+
108
+ # Update the variable and return it
109
+ var_obj.quantity = result_qty
110
+ var_obj.is_known = True
111
+ return var_obj
112
+
113
+ def check_residual(self, variable_values: dict[str, FieldQnty], tolerance: float = SOLVER_DEFAULT_TOLERANCE) -> bool:
114
+ """
115
+ Check if equation is satisfied by evaluating residual (LHS - RHS).
116
+ Returns True if |residual| < tolerance, accounting for units.
117
+ """
118
+ try:
119
+ # Both lhs and rhs should be Expressions after __init__ conversion
120
+ lhs_value = self.lhs.evaluate(variable_values)
121
+ rhs_value = self.rhs.evaluate(variable_values)
122
+
123
+ # Check dimensional compatibility using public API
124
+ if not self._are_dimensionally_compatible(lhs_value, rhs_value):
125
+ return False
126
+
127
+ # Convert to same units for comparison
128
+ rhs_converted = rhs_value.to(lhs_value.unit)
129
+ residual = abs(lhs_value.value - rhs_converted.value)
130
+
131
+ return residual < tolerance
132
+ except (ValueError, TypeError, AttributeError, KeyError) as e:
133
+ _logger.debug(f"Expected error in residual check for equation '{self.name}': {e}")
134
+ return False
135
+ except Exception as e:
136
+ error_msg = f"Unexpected error in residual check for equation '{self.name}': {e}"
137
+ _logger.error(error_msg)
138
+ raise RuntimeError(error_msg) from e
139
+
140
+ def _discover_variables_from_scope(self) -> dict[str, FieldQnty]:
141
+ """
142
+ Automatically discover variables from the calling scope using centralized service.
143
+ """
144
+ if not _SCOPE_DISCOVERY_ENABLED:
145
+ return {}
146
+
147
+ # Use centralized scope discovery service
148
+ return ScopeDiscoveryService.discover_variables(self.variables, enable_caching=True)
149
+
150
+ def _are_dimensionally_compatible(self, lhs_value, rhs_value) -> bool:
151
+ """Check if two quantities are dimensionally compatible."""
152
+ try:
153
+ # Try to convert - if successful, they're compatible
154
+ rhs_value.to(lhs_value.unit)
155
+ return True
156
+ except (ValueError, TypeError, AttributeError):
157
+ return False
158
+
159
+ def _analyze_variable_states(self, discovered: dict[str, FieldQnty]) -> dict[str, str]:
160
+ """Analyze which variables are known vs unknown."""
161
+ states = {}
162
+ for var_name in self.variables:
163
+ if var_name not in discovered:
164
+ states[var_name] = "missing"
165
+ else:
166
+ var = discovered[var_name]
167
+ if hasattr(var, "is_known") and not var.is_known:
168
+ states[var_name] = "unknown"
169
+ elif hasattr(var, "quantity") and var.quantity is not None:
170
+ states[var_name] = "known"
171
+ else:
172
+ states[var_name] = "unknown"
173
+ return states
174
+
175
+ def _determine_solvability(self, states: dict[str, str], discovered: dict[str, FieldQnty]) -> tuple[bool, str, dict[str, FieldQnty]]:
176
+ """Determine if equation can be solved based on variable states."""
177
+ if "missing" in states.values():
178
+ return False, "", {}
179
+
180
+ unknowns = [name for name, state in states.items() if state == "unknown"]
181
+
182
+ # Can only auto-solve if there's exactly one unknown
183
+ if len(unknowns) == 1:
184
+ return True, unknowns[0], discovered
185
+
186
+ return False, "", {}
187
+
188
+ def _can_auto_solve(self) -> tuple[bool, str, dict[str, FieldQnty]]:
189
+ """Check if equation can be auto-solved from scope using centralized service."""
190
+ try:
191
+ discovered = self._discover_variables_from_scope()
192
+ if not discovered:
193
+ return False, "", {}
194
+
195
+ variable_states = self._analyze_variable_states(discovered)
196
+ return self._determine_solvability(variable_states, discovered)
197
+
198
+ except (AttributeError, KeyError, ValueError, TypeError) as e:
199
+ _logger.debug(f"Cannot auto-solve equation '{self.name}': {e}")
200
+ return False, "", {}
201
+
202
+ def _try_auto_solve(self) -> bool:
203
+ """Try to automatically solve the equation if possible."""
204
+ try:
205
+ can_solve, target_var, variables = self._can_auto_solve()
206
+ if can_solve:
207
+ self.solve_for(target_var, variables)
208
+ return True
209
+ return False
210
+ except (AttributeError, KeyError, ValueError, TypeError) as e:
211
+ _logger.debug(f"Auto-solve failed for equation '{self.name}': {e}")
212
+ return False
213
+
214
+ def __str__(self) -> str:
215
+ # Try to auto-solve if possible before displaying
216
+ self._try_auto_solve()
217
+ return f"{self.lhs} = {self.rhs}"
218
+
219
+ def __repr__(self) -> str:
220
+ return f"Equation(name='{self.name}', lhs={self.lhs!r}, rhs={self.rhs!r})"
@@ -0,0 +1,130 @@
1
+ from ..quantities import FieldQnty
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
+
11
+ __slots__ = ("equations", "variables", "_known_cache", "_unknown_cache")
12
+
13
+ def __init__(self, equations: list[Equation] | None = None):
14
+ self.equations = equations or []
15
+ self.variables: dict[str, FieldQnty] = {} # Dict[str, FieldQnty]
16
+ self._known_cache: set[str] | None = None # Cache for known variables
17
+ self._unknown_cache: set[str] | None = None # Cache for unknown variables
18
+
19
+ def add_equation(self, equation: Equation):
20
+ """Add an equation to the system."""
21
+ self.equations.append(equation)
22
+ self._invalidate_caches()
23
+
24
+ def add_variable(self, variable: FieldQnty):
25
+ """Add a variable to the system."""
26
+ self.variables[variable.name] = variable
27
+ self._invalidate_caches()
28
+
29
+ def _invalidate_caches(self):
30
+ """Invalidate cached known/unknown variable sets."""
31
+ self._known_cache = None
32
+ self._unknown_cache = None
33
+
34
+ def _is_variable_known(self, var: FieldQnty) -> bool:
35
+ """Determine if a variable should be considered known."""
36
+ return var.is_known and var.quantity is not None
37
+
38
+ def get_known_variables(self) -> set[str]:
39
+ """Get names of all known variables (cached)."""
40
+ if self._known_cache is None:
41
+ self._known_cache = {name for name, var in self.variables.items() if self._is_variable_known(var)}
42
+ return self._known_cache
43
+
44
+ def get_unknown_variables(self) -> set[str]:
45
+ """Get names of all unknown variables (cached)."""
46
+ if self._unknown_cache is None:
47
+ self._unknown_cache = {name for name, var in self.variables.items() if not self._is_variable_known(var)}
48
+ return self._unknown_cache
49
+
50
+ def _find_solvable_equation_variable_pair(self, known_vars: set[str], unknown_vars: set[str]) -> tuple[Equation, str] | None:
51
+ """Find the first equation-variable pair that can be solved."""
52
+ for equation in self.equations:
53
+ for unknown_var in unknown_vars:
54
+ if equation.can_solve_for(unknown_var, known_vars):
55
+ return equation, unknown_var
56
+ return None
57
+
58
+ def can_solve_any(self) -> bool:
59
+ """Check if any equation can be solved with current known variables."""
60
+ known_vars = self.get_known_variables()
61
+ unknown_vars = self.get_unknown_variables()
62
+ return self._find_solvable_equation_variable_pair(known_vars, unknown_vars) is not None
63
+
64
+ def solve_step(self) -> bool:
65
+ """Solve one step - find and solve one equation. Returns True if progress made."""
66
+ known_vars = self.get_known_variables()
67
+ unknown_vars = self.get_unknown_variables()
68
+
69
+ pair = self._find_solvable_equation_variable_pair(known_vars, unknown_vars)
70
+ if pair is None:
71
+ return False
72
+
73
+ equation, unknown_var = pair
74
+ equation.solve_for(unknown_var, self.variables)
75
+ self._invalidate_caches()
76
+ return True
77
+
78
+ def solve(self, max_iterations: int = 100) -> bool:
79
+ """Solve the system iteratively. Returns True if fully solved."""
80
+ if max_iterations <= 0:
81
+ raise ValueError("max_iterations must be positive")
82
+
83
+ for _ in range(max_iterations):
84
+ if not self.can_solve_any():
85
+ break
86
+ if not self.solve_step():
87
+ break
88
+
89
+ # Check if all variables are known
90
+ unknown_vars = self.get_unknown_variables()
91
+ return len(unknown_vars) == 0
92
+
93
+ def _get_next_solvable_variable(self, simulated_known: set[str]) -> str | None:
94
+ """Find the next variable that can be solved given current known variables."""
95
+ unknown_vars = {name for name in self.variables.keys() if name not in simulated_known}
96
+ if not unknown_vars:
97
+ return None
98
+
99
+ for equation in self.equations:
100
+ for unknown_var in unknown_vars:
101
+ if equation.can_solve_for(unknown_var, simulated_known):
102
+ return unknown_var
103
+ return None
104
+
105
+ def get_solving_order(self) -> list[str]:
106
+ """
107
+ Get the order in which variables can be solved.
108
+ Optimized to avoid creating full system copies.
109
+ """
110
+ order = []
111
+ simulated_known = self.get_known_variables().copy()
112
+
113
+ max_iterations = len(self.variables) # Prevent infinite loops
114
+ for _ in range(max_iterations):
115
+ next_var = self._get_next_solvable_variable(simulated_known)
116
+ if next_var is None:
117
+ break
118
+
119
+ order.append(next_var)
120
+ simulated_known.add(next_var)
121
+
122
+ return order
123
+
124
+ def __str__(self) -> str:
125
+ known_count = len(self.get_known_variables())
126
+ total_vars = len(self.variables)
127
+ return f"EquationSystem({len(self.equations)} equations, {known_count}/{total_vars} variables known)"
128
+
129
+ def __repr__(self) -> str:
130
+ return f"EquationSystem(equations={self.equations!r})"
@@ -0,0 +1,40 @@
1
+ """
2
+ Expression System Package
3
+ =========================
4
+
5
+ Mathematical expressions for building equation trees with qnty variables.
6
+ """
7
+
8
+ # Core AST classes
9
+ # Helper functions
10
+ # Scope discovery service
11
+ from ..utils.scope_discovery import ScopeDiscoveryService
12
+ from .functions import abs_expr, cond_expr, cos, exp, ln, log10, max_expr, min_expr, sin, sqrt, tan
13
+ from .nodes import BinaryOperation, ConditionalExpression, Constant, Expression, UnaryFunction, VariableReference, wrap_operand
14
+
15
+ # Define public API
16
+ __all__ = [
17
+ # Core AST classes
18
+ "Expression",
19
+ "VariableReference",
20
+ "Constant",
21
+ "BinaryOperation",
22
+ "UnaryFunction",
23
+ "ConditionalExpression",
24
+ # Helper functions
25
+ "sin",
26
+ "cos",
27
+ "tan",
28
+ "sqrt",
29
+ "abs_expr",
30
+ "ln",
31
+ "log10",
32
+ "exp",
33
+ "cond_expr",
34
+ "min_expr",
35
+ "max_expr",
36
+ # Utilities
37
+ "wrap_operand",
38
+ # Scope discovery
39
+ "ScopeDiscoveryService",
40
+ ]