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,188 @@
1
+ """
2
+ Expression String Formatting System
3
+ ===================================
4
+
5
+ Centralized formatting logic for mathematical expressions with proper
6
+ operator precedence handling and readable output generation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from .types import BinaryOperationProtocol, ConditionalExpressionProtocol, ConstantProtocol, ExpressionProtocol, UnaryFunctionProtocol, VariableReferenceProtocol
14
+
15
+
16
+ class ExpressionFormatter:
17
+ """
18
+ Handles string representation of expressions with proper precedence rules.
19
+
20
+ Centralizes complex formatting logic that was previously scattered across
21
+ expression classes, making it easier to maintain and extend.
22
+ """
23
+
24
+ # Operator precedence levels (higher number = higher precedence)
25
+ OPERATOR_PRECEDENCE = {
26
+ # Comparison operators (lowest precedence)
27
+ "<": 0,
28
+ "<=": 0,
29
+ ">": 0,
30
+ ">=": 0,
31
+ "==": 0,
32
+ "!=": 0,
33
+ # Arithmetic operators
34
+ "+": 1,
35
+ "-": 1, # Addition/subtraction
36
+ "*": 2,
37
+ "/": 2, # Multiplication/division
38
+ "**": 3, # Exponentiation (highest precedence)
39
+ }
40
+
41
+ # Right-associative operators (most are left-associative by default)
42
+ RIGHT_ASSOCIATIVE = {"**"}
43
+
44
+ # Left-associative operators that need special parenthesization
45
+ LEFT_ASSOCIATIVE_SPECIAL = {"-", "/"}
46
+
47
+ @staticmethod
48
+ def format_binary_operation(binary_op: BinaryOperationProtocol, can_auto_evaluate: bool = False, auto_eval_variables: dict[str, Any] | None = None) -> str:
49
+ """
50
+ Format binary operation with proper parenthesization.
51
+
52
+ Args:
53
+ binary_op: The binary operation to format
54
+ can_auto_evaluate: Whether auto-evaluation should be attempted
55
+ auto_eval_variables: Variables for auto-evaluation
56
+
57
+ Returns:
58
+ Formatted string representation
59
+ """
60
+ # Try auto-evaluation first if requested
61
+ if can_auto_evaluate and auto_eval_variables:
62
+ evaluation_result = ExpressionFormatter._try_evaluate(binary_op, auto_eval_variables)
63
+ if evaluation_result is not None:
64
+ return str(evaluation_result)
65
+
66
+ # Get string representations of operands
67
+ left_str = str(binary_op.left)
68
+ right_str = str(binary_op.right)
69
+
70
+ # Apply parenthesization rules
71
+ left_str = ExpressionFormatter._maybe_parenthesize_left(binary_op.left, binary_op.operator, left_str)
72
+ right_str = ExpressionFormatter._maybe_parenthesize_right(binary_op.right, binary_op.operator, right_str)
73
+
74
+ return f"{left_str} {binary_op.operator} {right_str}"
75
+
76
+ @staticmethod
77
+ def _maybe_parenthesize_left(left_expr: ExpressionProtocol, operator: str, left_str: str) -> str:
78
+ """Add parentheses to left operand if needed for precedence."""
79
+ if not ExpressionFormatter._is_binary_operation(left_expr):
80
+ return left_str
81
+
82
+ left_precedence = ExpressionFormatter._get_expression_precedence(left_expr)
83
+ current_precedence = ExpressionFormatter.get_operator_precedence(operator)
84
+
85
+ # Left side needs parentheses only if its precedence is strictly lower
86
+ if left_precedence < current_precedence:
87
+ return f"({left_str})"
88
+
89
+ return left_str
90
+
91
+ @staticmethod
92
+ def _maybe_parenthesize_right(right_expr: ExpressionProtocol, operator: str, right_str: str) -> str:
93
+ """Add parentheses to right operand if needed for precedence and associativity."""
94
+ if not ExpressionFormatter._is_binary_operation(right_expr):
95
+ return right_str
96
+
97
+ right_precedence = ExpressionFormatter._get_expression_precedence(right_expr)
98
+ current_precedence = ExpressionFormatter.get_operator_precedence(operator)
99
+
100
+ # Right side needs parentheses if:
101
+ # 1. Its precedence is strictly lower, OR
102
+ # 2. Same precedence AND current operator is left-associative (-, /)
103
+ needs_parentheses = right_precedence < current_precedence or (right_precedence == current_precedence and operator in ExpressionFormatter.LEFT_ASSOCIATIVE_SPECIAL)
104
+
105
+ if needs_parentheses:
106
+ return f"({right_str})"
107
+
108
+ return right_str
109
+
110
+ @staticmethod
111
+ def format_unary_function(func: UnaryFunctionProtocol) -> str:
112
+ """Format unary function call."""
113
+ return f"{func.function_name}({func.operand})"
114
+
115
+ @staticmethod
116
+ def format_conditional_expression(cond_expr: ConditionalExpressionProtocol) -> str:
117
+ """Format conditional expression in if-then-else form."""
118
+ return f"if({cond_expr.condition}, {cond_expr.true_expr}, {cond_expr.false_expr})"
119
+
120
+ @staticmethod
121
+ def format_variable_reference(var_ref: VariableReferenceProtocol) -> str:
122
+ """Format variable reference (just the name)."""
123
+ return var_ref.name
124
+
125
+ @staticmethod
126
+ def format_constant(constant: ConstantProtocol) -> str:
127
+ """Format constant value."""
128
+ return str(constant.value)
129
+
130
+ @staticmethod
131
+ def is_operator_right_associative(operator: str) -> bool:
132
+ """Check if operator is right-associative."""
133
+ return operator in ExpressionFormatter.RIGHT_ASSOCIATIVE
134
+
135
+ @staticmethod
136
+ def get_operator_precedence(operator: str) -> int:
137
+ """Get precedence level for operator."""
138
+ return ExpressionFormatter.OPERATOR_PRECEDENCE.get(operator, 0)
139
+
140
+ @staticmethod
141
+ def _try_evaluate(expression: ExpressionProtocol, variables: dict[str, Any]) -> Any | None:
142
+ """
143
+ Safely attempt to evaluate an expression.
144
+
145
+ Args:
146
+ expression: Expression to evaluate
147
+ variables: Variable context for evaluation
148
+
149
+ Returns:
150
+ Evaluation result or None if evaluation failed
151
+ """
152
+ try:
153
+ return expression.evaluate(variables)
154
+ except (KeyError, ValueError, TypeError, ZeroDivisionError, AttributeError):
155
+ # Catch specific exceptions that are expected during evaluation
156
+ return None
157
+
158
+ @staticmethod
159
+ def _is_binary_operation(expr: ExpressionProtocol) -> bool:
160
+ """Check if expression is a binary operation using duck typing."""
161
+ return hasattr(expr, "operator") and hasattr(expr, "left") and hasattr(expr, "right")
162
+
163
+ @staticmethod
164
+ def _get_expression_precedence(expr: ExpressionProtocol) -> int:
165
+ """Get precedence of an expression if it's a binary operation."""
166
+ if ExpressionFormatter._is_binary_operation(expr):
167
+ # We know it's a binary operation from the check above, so it has an operator
168
+ binary_op = expr # type: ignore[assignment]
169
+ return ExpressionFormatter.get_operator_precedence(binary_op.operator) # type: ignore[attr-defined]
170
+ return 0
171
+
172
+ @staticmethod
173
+ def format_expression_with_auto_eval(expr: ExpressionProtocol) -> str:
174
+ """
175
+ Format expression, attempting auto-evaluation if possible.
176
+
177
+ This is a convenience method that handles the common pattern of
178
+ trying to auto-evaluate before falling back to symbolic representation.
179
+ """
180
+ # Check if auto-evaluation is possible for binary operations
181
+ if ExpressionFormatter._is_binary_operation(expr) and hasattr(expr, "_can_auto_evaluate"):
182
+ # We know it's a binary operation with auto-evaluation capability
183
+ binary_op = expr # type: ignore[assignment]
184
+ can_eval, variables = binary_op._can_auto_evaluate() # type: ignore[attr-defined]
185
+ return ExpressionFormatter.format_binary_operation(binary_op, can_auto_evaluate=can_eval, auto_eval_variables=variables) # type: ignore[arg-type]
186
+ else:
187
+ # For other expression types, just use their standard string representation
188
+ return str(expr)
@@ -0,0 +1,74 @@
1
+ """
2
+ Expression Helper Functions
3
+ ==========================
4
+
5
+ Convenience functions for creating mathematical expressions.
6
+ """
7
+
8
+ from ..quantities import FieldQnty, Quantity
9
+ from .nodes import BinaryOperation, ConditionalExpression, Expression, UnaryFunction, wrap_operand
10
+
11
+ # Type aliases for better maintainability
12
+ ExpressionOperand = Expression | FieldQnty | Quantity | int | float
13
+ ConditionalOperand = Expression | BinaryOperation
14
+
15
+
16
+ def _create_unary_function(name: str, docstring: str):
17
+ """Factory function for creating unary mathematical functions."""
18
+
19
+ def func(expr: ExpressionOperand) -> UnaryFunction:
20
+ return UnaryFunction(name, wrap_operand(expr))
21
+
22
+ func.__name__ = name
23
+ func.__doc__ = docstring
24
+ return func
25
+
26
+
27
+ def _create_comparison_expr(expressions: tuple[ExpressionOperand, ...], comparator: str) -> Expression:
28
+ """Create min/max expression using specified comparator."""
29
+ if len(expressions) < 2:
30
+ raise ValueError(f"{comparator}_expr requires at least 2 arguments")
31
+
32
+ wrapped_expressions = [wrap_operand(expr) for expr in expressions]
33
+ result = wrapped_expressions[0]
34
+
35
+ for expr in wrapped_expressions[1:]:
36
+ if comparator == "min":
37
+ # min(a, b) = if(a < b, a, b)
38
+ result = cond_expr(result < expr, result, expr)
39
+ else: # max
40
+ # max(a, b) = if(a > b, a, b)
41
+ result = cond_expr(result > expr, result, expr)
42
+
43
+ return result
44
+
45
+
46
+ # Mathematical functions (generated via factory)
47
+ sin = _create_unary_function("sin", "Sine function.")
48
+ cos = _create_unary_function("cos", "Cosine function.")
49
+ tan = _create_unary_function("tan", "Tangent function.")
50
+ sqrt = _create_unary_function("sqrt", "Square root function.")
51
+ abs_expr = _create_unary_function("abs", "Absolute value function.")
52
+ ln = _create_unary_function("ln", "Natural logarithm function.")
53
+ log10 = _create_unary_function("log10", "Base-10 logarithm function.")
54
+ exp = _create_unary_function("exp", "Exponential function.")
55
+
56
+
57
+ def cond_expr(
58
+ condition: ConditionalOperand,
59
+ true_expr: ExpressionOperand,
60
+ false_expr: ExpressionOperand,
61
+ ) -> ConditionalExpression:
62
+ """Conditional expression: if condition then true_expr else false_expr."""
63
+ wrapped_condition = condition if isinstance(condition, Expression) else condition
64
+ return ConditionalExpression(wrapped_condition, wrap_operand(true_expr), wrap_operand(false_expr))
65
+
66
+
67
+ def min_expr(*expressions: ExpressionOperand) -> Expression:
68
+ """Minimum of multiple expressions."""
69
+ return _create_comparison_expr(expressions, "min")
70
+
71
+
72
+ def max_expr(*expressions: ExpressionOperand) -> Expression:
73
+ """Maximum of multiple expressions."""
74
+ return _create_comparison_expr(expressions, "max")