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
qnty/expression.py DELETED
@@ -1,553 +0,0 @@
1
- """
2
- Expression System
3
- =================
4
-
5
- Mathematical expressions for building equation trees with qnty variables.
6
- """
7
-
8
- import math
9
- from abc import ABC, abstractmethod
10
- from typing import Union, cast
11
-
12
- from .units import DimensionlessUnits
13
-
14
- # if TYPE_CHECKING:
15
- from .variable import FastQuantity, TypeSafeVariable
16
-
17
-
18
- def wrap_operand(operand: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
19
- """
20
- Wrap non-Expression operands in appropriate Expression subclasses.
21
-
22
- This function handles type conversion without circular imports by using
23
- duck typing and delayed imports where necessary.
24
- """
25
- # Type guard for Expression types
26
- if hasattr(operand, 'evaluate') and hasattr(operand, 'get_variables'):
27
- # Already an Expression
28
- return cast('Expression', operand)
29
- elif hasattr(operand, 'name') and hasattr(operand, 'quantity') and hasattr(operand, 'is_known'):
30
- # TypeSafeVariable-like object
31
- return VariableReference(cast('TypeSafeVariable', operand))
32
- elif hasattr(operand, 'value') and hasattr(operand, 'unit') and hasattr(operand, '_dimension_sig'):
33
- # FastQuantity-like object
34
- return Constant(cast('FastQuantity', operand))
35
- elif isinstance(operand, int | float):
36
- # Numeric value - create dimensionless quantity
37
-
38
- return Constant(FastQuantity(float(operand), DimensionlessUnits.dimensionless))
39
- else:
40
- raise TypeError(f"Cannot convert {type(operand)} to Expression")
41
-
42
-
43
- class Expression(ABC):
44
- """Abstract base class for mathematical expressions."""
45
-
46
- @abstractmethod
47
- def evaluate(self, variable_values: dict[str, 'TypeSafeVariable']) -> 'FastQuantity':
48
- """Evaluate the expression given variable values."""
49
- pass
50
-
51
- @abstractmethod
52
- def get_variables(self) -> set[str]:
53
- """Get all variable symbols used in this expression."""
54
- pass
55
-
56
- @abstractmethod
57
- def simplify(self) -> 'Expression':
58
- """Simplify the expression."""
59
- pass
60
-
61
- @abstractmethod
62
- def __str__(self) -> str:
63
- pass
64
-
65
- def _discover_variables_from_scope(self) -> dict[str, 'TypeSafeVariable']:
66
- """Automatically discover variables from the calling scope."""
67
- import inspect
68
-
69
- # Get the frame that called this method (skip through __str__ calls)
70
- frame = inspect.currentframe()
71
- try:
72
- # Skip frames until we find one outside the expression system
73
- while frame and (
74
- frame.f_code.co_filename.endswith('expression.py') or
75
- frame.f_code.co_name in ['__str__', '__repr__']
76
- ):
77
- frame = frame.f_back
78
-
79
- if not frame:
80
- return {}
81
-
82
- # Combine local and global variables
83
- all_vars = {**frame.f_globals, **frame.f_locals}
84
-
85
- # Find TypeSafeVariable objects that match our required variables
86
- required_vars = self.get_variables()
87
- discovered = {}
88
-
89
- for var_name in required_vars:
90
- for name, obj in all_vars.items():
91
- # Check if this is a TypeSafeVariable with matching symbol/name
92
- if hasattr(obj, 'symbol') and obj.symbol == var_name:
93
- discovered[var_name] = obj
94
- break
95
- elif hasattr(obj, 'name') and obj.name == var_name:
96
- discovered[var_name] = obj
97
- break
98
- # Check if this is an Expression that can be evaluated to get the variable
99
- elif hasattr(obj, 'get_variables') and name == var_name:
100
- # This is an expression named after our variable - try to evaluate it
101
- try:
102
- if hasattr(obj, '_can_auto_evaluate'):
103
- can_eval, expr_vars = obj._can_auto_evaluate()
104
- if can_eval:
105
- result = obj.evaluate(expr_vars)
106
- # Create a temporary variable to hold the result
107
- from .variables import Length # Import here to avoid circular import
108
- temp_var = Length(result.value, result.unit.symbol, f"temp_{var_name}")
109
- temp_var.symbol = var_name
110
- discovered[var_name] = temp_var
111
- break
112
- except Exception:
113
- pass
114
-
115
- return discovered
116
-
117
- finally:
118
- del frame
119
-
120
- def _can_auto_evaluate(self) -> tuple[bool, dict[str, 'TypeSafeVariable']]:
121
- """Check if expression can be auto-evaluated from scope."""
122
- try:
123
- discovered = self._discover_variables_from_scope()
124
- required_vars = self.get_variables()
125
-
126
- # Check if all required variables are available and have values
127
- for var_name in required_vars:
128
- if var_name not in discovered:
129
- return False, {}
130
- var = discovered[var_name]
131
- if not hasattr(var, 'quantity') or var.quantity is None:
132
- return False, {}
133
-
134
- return True, discovered
135
-
136
- except Exception:
137
- return False, {}
138
-
139
- def __add__(self, other: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
140
- return BinaryOperation('+', self, wrap_operand(other))
141
-
142
- def __radd__(self, other: Union['TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
143
- return BinaryOperation('+', wrap_operand(other), self)
144
-
145
- def __sub__(self, other: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
146
- return BinaryOperation('-', self, wrap_operand(other))
147
-
148
- def __rsub__(self, other: Union['TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
149
- return BinaryOperation('-', wrap_operand(other), self)
150
-
151
- def __mul__(self, other: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
152
- return BinaryOperation('*', self, wrap_operand(other))
153
-
154
- def __rmul__(self, other: Union['TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
155
- return BinaryOperation('*', wrap_operand(other), self)
156
-
157
- def __truediv__(self, other: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
158
- return BinaryOperation('/', self, wrap_operand(other))
159
-
160
- def __rtruediv__(self, other: Union['TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
161
- return BinaryOperation('/', wrap_operand(other), self)
162
-
163
- def __pow__(self, other: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
164
- return BinaryOperation('**', self, wrap_operand(other))
165
-
166
- def __rpow__(self, other: Union['TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
167
- return BinaryOperation('**', wrap_operand(other), self)
168
-
169
- # Comparison operators for conditional expressions
170
- def __lt__(self, other: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'BinaryOperation':
171
- return BinaryOperation('<', self, self._wrap_operand(other))
172
-
173
- def __le__(self, other: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'BinaryOperation':
174
- return BinaryOperation('<=', self, self._wrap_operand(other))
175
-
176
- def __gt__(self, other: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'BinaryOperation':
177
- return BinaryOperation('>', self, self._wrap_operand(other))
178
-
179
- def __ge__(self, other: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'BinaryOperation':
180
- return BinaryOperation('>=', self, self._wrap_operand(other))
181
-
182
- @staticmethod
183
- def _wrap_operand(operand: Union['Expression', 'TypeSafeVariable', 'FastQuantity', int, float]) -> 'Expression':
184
- """Wrap non-Expression operands in appropriate Expression subclasses."""
185
- return wrap_operand(operand)
186
-
187
-
188
- class VariableReference(Expression):
189
- """Reference to a variable in an expression with performance optimizations."""
190
-
191
- def __init__(self, variable: 'TypeSafeVariable'):
192
- self.variable = variable
193
- # Cache the name resolution to avoid repeated lookups
194
- self._cached_name = None
195
- self._last_symbol = None
196
-
197
- @property
198
- def name(self) -> str:
199
- """Get variable name with caching for performance."""
200
- current_symbol = self.variable.symbol
201
- if self._cached_name is None or self._last_symbol != current_symbol:
202
- # Use symbol for optinova compatibility, fall back to name if symbol not set
203
- self._cached_name = current_symbol if current_symbol else self.variable.name
204
- self._last_symbol = current_symbol
205
- return self._cached_name
206
-
207
- def evaluate(self, variable_values: dict[str, 'TypeSafeVariable']) -> 'FastQuantity':
208
- try:
209
- if self.name in variable_values:
210
- var = variable_values[self.name]
211
- if var.quantity is not None:
212
- return var.quantity
213
- elif self.variable.quantity is not None:
214
- return self.variable.quantity
215
-
216
- # If we reach here, no valid quantity was found
217
- available_vars = list(variable_values.keys()) if variable_values else []
218
- raise ValueError(
219
- f"Cannot evaluate variable '{self.name}' without value. "
220
- f"Available variables: {available_vars}"
221
- )
222
- except Exception as e:
223
- if isinstance(e, ValueError):
224
- raise
225
- raise ValueError(f"Error evaluating variable '{self.name}': {e}") from e
226
-
227
- def get_variables(self) -> set[str]:
228
- return {self.name}
229
-
230
- def simplify(self) -> 'Expression':
231
- return self
232
-
233
- def __str__(self) -> str:
234
- return self.name
235
-
236
-
237
- class Constant(Expression):
238
- """Constant value in an expression."""
239
-
240
- def __init__(self, value: 'FastQuantity'):
241
- self.value = value
242
-
243
- def evaluate(self, variable_values: dict[str, 'TypeSafeVariable']) -> 'FastQuantity':
244
- del variable_values # Suppress unused variable warning
245
- return self.value
246
-
247
- def get_variables(self) -> set[str]:
248
- return set()
249
-
250
- def simplify(self) -> 'Expression':
251
- return self
252
-
253
- def __str__(self) -> str:
254
- return str(self.value.value)
255
-
256
-
257
- class BinaryOperation(Expression):
258
- """Binary operation between two expressions."""
259
-
260
- def __init__(self, operator: str, left: Expression, right: Expression):
261
- self.operator = operator
262
- self.left = left
263
- self.right = right
264
-
265
- def evaluate(self, variable_values: dict[str, 'TypeSafeVariable']) -> 'FastQuantity':
266
- try:
267
- left_val = self.left.evaluate(variable_values)
268
- right_val = self.right.evaluate(variable_values)
269
-
270
- if self.operator == '+':
271
- return left_val + right_val
272
- elif self.operator == '-':
273
- return left_val - right_val
274
- elif self.operator == '*':
275
- return left_val * right_val
276
- elif self.operator == '/':
277
- # Check for division by zero
278
- if abs(right_val.value) < 1e-15:
279
- raise ValueError(f"Division by zero in expression: {self}")
280
- return left_val / right_val
281
- elif self.operator == '**':
282
- # For power, right side should be dimensionless
283
- if isinstance(right_val.value, int | float):
284
- if right_val.value < 0 and left_val.value < 0:
285
- raise ValueError(f"Negative base with negative exponent: {left_val.value}^{right_val.value}")
286
- result_value = left_val.value ** right_val.value
287
- # For power operations, we need to handle units carefully
288
- # This is a simplified implementation
289
- return FastQuantity(result_value, left_val.unit)
290
- else:
291
- raise ValueError("Exponent must be dimensionless number")
292
- elif self.operator in ['<', '<=', '>', '>=', '==', '!=']:
293
- # Comparison operations - return dimensionless 1.0 or 0.0
294
- # Convert to same units for comparison if possible
295
- try:
296
- if left_val._dimension_sig == right_val._dimension_sig and left_val.unit != right_val.unit:
297
- right_val = right_val.to(left_val.unit)
298
- except (ValueError, TypeError, AttributeError):
299
- pass
300
-
301
- result = False # Initialize result
302
- if self.operator == '<':
303
- result = left_val.value < right_val.value
304
- elif self.operator == '<=':
305
- result = left_val.value <= right_val.value
306
- elif self.operator == '>':
307
- result = left_val.value > right_val.value
308
- elif self.operator == '>=':
309
- result = left_val.value >= right_val.value
310
- elif self.operator == '==':
311
- result = abs(left_val.value - right_val.value) < 1e-10
312
- elif self.operator == '!=':
313
- result = abs(left_val.value - right_val.value) >= 1e-10
314
-
315
- return FastQuantity(1.0 if result else 0.0, DimensionlessUnits.dimensionless)
316
- else:
317
- raise ValueError(f"Unknown operator: {self.operator}")
318
- except Exception as e:
319
- if isinstance(e, ValueError):
320
- raise
321
- raise ValueError(f"Error evaluating binary operation '{self}': {e}") from e
322
-
323
- def get_variables(self) -> set[str]:
324
- return self.left.get_variables() | self.right.get_variables()
325
-
326
- def simplify(self) -> Expression:
327
- left_simplified = self.left.simplify()
328
- right_simplified = self.right.simplify()
329
-
330
- # Basic simplification rules
331
- if isinstance(left_simplified, Constant) and isinstance(right_simplified, Constant):
332
- # Evaluate constant expressions
333
- dummy_vars = {}
334
- try:
335
- result = BinaryOperation(self.operator, left_simplified, right_simplified).evaluate(dummy_vars)
336
- return Constant(result)
337
- except (ValueError, TypeError, ArithmeticError):
338
- pass
339
-
340
- return BinaryOperation(self.operator, left_simplified, right_simplified)
341
-
342
- def __str__(self) -> str:
343
- # Try to auto-evaluate if all variables are available
344
- can_eval, variables = self._can_auto_evaluate()
345
- if can_eval:
346
- try:
347
- result = self.evaluate(variables)
348
- return str(result)
349
- except Exception:
350
- pass # Fall back to symbolic representation
351
-
352
- # Handle operator precedence for cleaner string representation
353
- precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '**': 3, '<': 0, '<=': 0, '>': 0, '>=': 0, '==': 0, '!=': 0}
354
- left_str = str(self.left)
355
- right_str = str(self.right)
356
-
357
- # Add parentheses for left side when precedence is strictly lower
358
- if isinstance(self.left, BinaryOperation) and precedence.get(self.left.operator, 0) < precedence.get(self.operator, 0):
359
- left_str = f"({left_str})"
360
-
361
- # CRITICAL FIX: For right side, add parentheses when:
362
- # 1. Precedence is strictly lower, OR
363
- # 2. Precedence is equal AND operation is left-associative (-, /)
364
- if isinstance(self.right, BinaryOperation):
365
- right_prec = precedence.get(self.right.operator, 0)
366
- curr_prec = precedence.get(self.operator, 0)
367
-
368
- # Need parentheses if:
369
- # - Right has lower precedence, OR
370
- # - Same precedence and current operator is left-associative (- or /)
371
- if (right_prec < curr_prec or
372
- (right_prec == curr_prec and self.operator in ['-', '/'])):
373
- right_str = f"({right_str})"
374
-
375
- return f"{left_str} {self.operator} {right_str}"
376
-
377
-
378
-
379
- class UnaryFunction(Expression):
380
- """Unary mathematical function expression."""
381
-
382
- def __init__(self, function_name: str, operand: Expression):
383
- self.function_name = function_name
384
- self.operand = operand
385
-
386
- def evaluate(self, variable_values: dict[str, 'TypeSafeVariable']) -> 'FastQuantity':
387
-
388
- operand_val = self.operand.evaluate(variable_values)
389
-
390
- if self.function_name == 'sin':
391
- # Assume input is in radians, result is dimensionless
392
- result_value = math.sin(operand_val.value)
393
- return FastQuantity(result_value, DimensionlessUnits.dimensionless)
394
- elif self.function_name == 'cos':
395
- result_value = math.cos(operand_val.value)
396
- return FastQuantity(result_value, DimensionlessUnits.dimensionless)
397
- elif self.function_name == 'tan':
398
- result_value = math.tan(operand_val.value)
399
- return FastQuantity(result_value, DimensionlessUnits.dimensionless)
400
- elif self.function_name == 'sqrt':
401
- # For sqrt, we need to handle units carefully
402
- result_value = math.sqrt(operand_val.value)
403
- # This is simplified - proper unit handling would need dimensional analysis
404
- return FastQuantity(result_value, operand_val.unit)
405
- elif self.function_name == 'abs':
406
- return FastQuantity(abs(operand_val.value), operand_val.unit)
407
- elif self.function_name == 'ln':
408
- # Natural log - input should be dimensionless
409
- result_value = math.log(operand_val.value)
410
- return FastQuantity(result_value, DimensionlessUnits.dimensionless)
411
- elif self.function_name == 'log10':
412
- result_value = math.log10(operand_val.value)
413
- return FastQuantity(result_value, DimensionlessUnits.dimensionless)
414
- elif self.function_name == 'exp':
415
- # Exponential - input should be dimensionless
416
- result_value = math.exp(operand_val.value)
417
- return FastQuantity(result_value, DimensionlessUnits.dimensionless)
418
- else:
419
- raise ValueError(f"Unknown function: {self.function_name}")
420
-
421
- def get_variables(self) -> set[str]:
422
- return self.operand.get_variables()
423
-
424
- def simplify(self) -> Expression:
425
- simplified_operand = self.operand.simplify()
426
- if isinstance(simplified_operand, Constant):
427
- # Evaluate constant functions at compile time
428
- try:
429
- dummy_vars = {}
430
- result = UnaryFunction(self.function_name, simplified_operand).evaluate(dummy_vars)
431
- return Constant(result)
432
- except (ValueError, TypeError, ArithmeticError):
433
- pass
434
- return UnaryFunction(self.function_name, simplified_operand)
435
-
436
- def __str__(self) -> str:
437
- return f"{self.function_name}({self.operand})"
438
-
439
-
440
- class ConditionalExpression(Expression):
441
- """Conditional expression: if condition then true_expr else false_expr."""
442
-
443
- def __init__(self, condition: Expression, true_expr: Expression, false_expr: Expression):
444
- self.condition = condition
445
- self.true_expr = true_expr
446
- self.false_expr = false_expr
447
-
448
- def evaluate(self, variable_values: dict[str, 'TypeSafeVariable']) -> 'FastQuantity':
449
- condition_val = self.condition.evaluate(variable_values)
450
- # Consider non-zero as True
451
- if abs(condition_val.value) > 1e-10:
452
- return self.true_expr.evaluate(variable_values)
453
- else:
454
- return self.false_expr.evaluate(variable_values)
455
-
456
- def get_variables(self) -> set[str]:
457
- return (self.condition.get_variables() |
458
- self.true_expr.get_variables() |
459
- self.false_expr.get_variables())
460
-
461
- def simplify(self) -> Expression:
462
- simplified_condition = self.condition.simplify()
463
- simplified_true = self.true_expr.simplify()
464
- simplified_false = self.false_expr.simplify()
465
-
466
- # If condition is constant, choose the appropriate branch
467
- if isinstance(simplified_condition, Constant):
468
- try:
469
- dummy_vars = {}
470
- condition_val = simplified_condition.evaluate(dummy_vars)
471
- if abs(condition_val.value) > 1e-10:
472
- return simplified_true
473
- else:
474
- return simplified_false
475
- except (ValueError, TypeError, ArithmeticError):
476
- pass
477
-
478
- return ConditionalExpression(simplified_condition, simplified_true, simplified_false)
479
-
480
- def __str__(self) -> str:
481
- return f"if({self.condition}, {self.true_expr}, {self.false_expr})"
482
-
483
-
484
- # Convenience functions for mathematical operations
485
- def sin(expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> UnaryFunction:
486
- """Sine function."""
487
- return UnaryFunction('sin', Expression._wrap_operand(expr))
488
-
489
- def cos(expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> UnaryFunction:
490
- """Cosine function."""
491
- return UnaryFunction('cos', Expression._wrap_operand(expr))
492
-
493
- def tan(expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> UnaryFunction:
494
- """Tangent function."""
495
- return UnaryFunction('tan', Expression._wrap_operand(expr))
496
-
497
- def sqrt(expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> UnaryFunction:
498
- """Square root function."""
499
- return UnaryFunction('sqrt', Expression._wrap_operand(expr))
500
-
501
- def abs_expr(expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> UnaryFunction:
502
- """Absolute value function."""
503
- return UnaryFunction('abs', Expression._wrap_operand(expr))
504
-
505
- def ln(expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> UnaryFunction:
506
- """Natural logarithm function."""
507
- return UnaryFunction('ln', Expression._wrap_operand(expr))
508
-
509
- def log10(expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> UnaryFunction:
510
- """Base-10 logarithm function."""
511
- return UnaryFunction('log10', Expression._wrap_operand(expr))
512
-
513
- def exp(expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> UnaryFunction:
514
- """Exponential function."""
515
- return UnaryFunction('exp', Expression._wrap_operand(expr))
516
-
517
- def cond_expr(condition: Union[Expression, 'BinaryOperation'],
518
- true_expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float],
519
- false_expr: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> ConditionalExpression:
520
- """Conditional expression: if condition then true_expr else false_expr."""
521
- return ConditionalExpression(
522
- condition if isinstance(condition, Expression) else condition,
523
- Expression._wrap_operand(true_expr),
524
- Expression._wrap_operand(false_expr)
525
- )
526
-
527
- def min_expr(*expressions: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> Expression:
528
- """Minimum of multiple expressions."""
529
- if len(expressions) < 2:
530
- raise ValueError("min_expr requires at least 2 arguments")
531
-
532
- wrapped_expressions = [Expression._wrap_operand(expr) for expr in expressions]
533
- result = wrapped_expressions[0]
534
-
535
- for expr in wrapped_expressions[1:]:
536
- # min(a, b) = if(a < b, a, b)
537
- result = cond_expr(result < expr, result, expr)
538
-
539
- return result
540
-
541
- def max_expr(*expressions: Union[Expression, 'TypeSafeVariable', 'FastQuantity', int, float]) -> Expression:
542
- """Maximum of multiple expressions."""
543
- if len(expressions) < 2:
544
- raise ValueError("max_expr requires at least 2 arguments")
545
-
546
- wrapped_expressions = [Expression._wrap_operand(expr) for expr in expressions]
547
- result = wrapped_expressions[0]
548
-
549
- for expr in wrapped_expressions[1:]:
550
- # max(a, b) = if(a > b, a, b)
551
- result = cond_expr(result > expr, result, expr)
552
-
553
- return result