qnty 0.0.9__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 (92) hide show
  1. qnty/__init__.py +2 -3
  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 +1 -1
  12. qnty/equations/equation.py +118 -155
  13. qnty/equations/system.py +68 -65
  14. qnty/expressions/__init__.py +25 -46
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +46 -68
  17. qnty/expressions/nodes.py +539 -384
  18. qnty/expressions/types.py +70 -0
  19. qnty/problems/__init__.py +145 -0
  20. qnty/problems/composition.py +1031 -0
  21. qnty/problems/problem.py +695 -0
  22. qnty/problems/rules.py +145 -0
  23. qnty/problems/solving.py +1216 -0
  24. qnty/problems/validation.py +127 -0
  25. qnty/quantities/__init__.py +28 -5
  26. qnty/quantities/base_qnty.py +677 -0
  27. qnty/quantities/field_converters.py +24004 -0
  28. qnty/quantities/field_qnty.py +1012 -0
  29. qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
  30. qnty/{generated/quantities.py → quantities/field_vars.py} +754 -432
  31. qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
  32. qnty/solving/manager.py +50 -44
  33. qnty/solving/order.py +181 -133
  34. qnty/solving/solvers/__init__.py +2 -9
  35. qnty/solving/solvers/base.py +27 -37
  36. qnty/solving/solvers/iterative.py +115 -135
  37. qnty/solving/solvers/simultaneous.py +93 -165
  38. qnty/units/__init__.py +1 -0
  39. qnty/{generated/units.py → units/field_units.py} +1700 -991
  40. qnty/units/field_units.pyi +2461 -0
  41. qnty/units/prefixes.py +58 -105
  42. qnty/units/registry.py +76 -89
  43. qnty/utils/__init__.py +16 -0
  44. qnty/utils/caching/__init__.py +23 -0
  45. qnty/utils/caching/manager.py +401 -0
  46. qnty/utils/error_handling/__init__.py +66 -0
  47. qnty/utils/error_handling/context.py +39 -0
  48. qnty/utils/error_handling/exceptions.py +96 -0
  49. qnty/utils/error_handling/handlers.py +171 -0
  50. qnty/utils/logging.py +4 -4
  51. qnty/utils/protocols.py +164 -0
  52. qnty/utils/scope_discovery.py +420 -0
  53. {qnty-0.0.9.dist-info → qnty-0.1.0.dist-info}/METADATA +1 -1
  54. qnty-0.1.0.dist-info/RECORD +60 -0
  55. qnty/_backup/problem_original.py +0 -1251
  56. qnty/_backup/quantity.py +0 -63
  57. qnty/codegen/cli.py +0 -125
  58. qnty/codegen/generators/data/unit_data.json +0 -8807
  59. qnty/codegen/generators/data_processor.py +0 -345
  60. qnty/codegen/generators/dimensions_gen.py +0 -434
  61. qnty/codegen/generators/doc_generator.py +0 -141
  62. qnty/codegen/generators/out/dimension_mapping.json +0 -974
  63. qnty/codegen/generators/out/dimension_metadata.json +0 -123
  64. qnty/codegen/generators/out/units_metadata.json +0 -223
  65. qnty/codegen/generators/quantities_gen.py +0 -159
  66. qnty/codegen/generators/setters_gen.py +0 -178
  67. qnty/codegen/generators/stubs_gen.py +0 -167
  68. qnty/codegen/generators/units_gen.py +0 -295
  69. qnty/expressions/cache.py +0 -94
  70. qnty/generated/dimensions.py +0 -514
  71. qnty/problem/__init__.py +0 -91
  72. qnty/problem/base.py +0 -142
  73. qnty/problem/composition.py +0 -385
  74. qnty/problem/composition_mixin.py +0 -382
  75. qnty/problem/equations.py +0 -413
  76. qnty/problem/metaclass.py +0 -302
  77. qnty/problem/reconstruction.py +0 -1016
  78. qnty/problem/solving.py +0 -180
  79. qnty/problem/validation.py +0 -64
  80. qnty/problem/variables.py +0 -239
  81. qnty/quantities/expression_quantity.py +0 -314
  82. qnty/quantities/quantity.py +0 -428
  83. qnty/quantities/typed_quantity.py +0 -215
  84. qnty/validation/__init__.py +0 -0
  85. qnty/validation/registry.py +0 -0
  86. qnty/validation/rules.py +0 -167
  87. qnty-0.0.9.dist-info/RECORD +0 -63
  88. /qnty/{codegen → extensions}/__init__.py +0 -0
  89. /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
  90. /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
  91. /qnty/{generated → extensions/reporting}/__init__.py +0 -0
  92. {qnty-0.0.9.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)
@@ -5,92 +5,70 @@ Expression Helper Functions
5
5
  Convenience functions for creating mathematical expressions.
6
6
  """
7
7
 
8
- from typing import TYPE_CHECKING, Union
8
+ from ..quantities import FieldQnty, Quantity
9
+ from .nodes import BinaryOperation, ConditionalExpression, Expression, UnaryFunction, wrap_operand
9
10
 
10
- if TYPE_CHECKING:
11
- from ..quantities.quantity import Quantity, TypeSafeVariable
12
- from .nodes import BinaryOperation, Expression
11
+ # Type aliases for better maintainability
12
+ ExpressionOperand = Expression | FieldQnty | Quantity | int | float
13
+ ConditionalOperand = Expression | BinaryOperation
13
14
 
14
- from .nodes import ConditionalExpression, Expression, UnaryFunction
15
15
 
16
+ def _create_unary_function(name: str, docstring: str):
17
+ """Factory function for creating unary mathematical functions."""
16
18
 
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))
19
+ def func(expr: ExpressionOperand) -> UnaryFunction:
20
+ return UnaryFunction(name, wrap_operand(expr))
21
21
 
22
+ func.__name__ = name
23
+ func.__doc__ = docstring
24
+ return func
22
25
 
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
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")
42
31
 
43
- def ln(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
44
- """Natural logarithm function."""
45
- return UnaryFunction('ln', Expression._wrap_operand(expr))
32
+ wrapped_expressions = [wrap_operand(expr) for expr in expressions]
33
+ result = wrapped_expressions[0]
46
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)
47
42
 
48
- def log10(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
49
- """Base-10 logarithm function."""
50
- return UnaryFunction('log10', Expression._wrap_operand(expr))
43
+ return result
51
44
 
52
45
 
53
- def exp(expr: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> UnaryFunction:
54
- """Exponential function."""
55
- return UnaryFunction('exp', Expression._wrap_operand(expr))
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.")
56
55
 
57
56
 
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:
57
+ def cond_expr(
58
+ condition: ConditionalOperand,
59
+ true_expr: ExpressionOperand,
60
+ false_expr: ExpressionOperand,
61
+ ) -> ConditionalExpression:
61
62
  """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
- )
63
+ wrapped_condition = condition if isinstance(condition, Expression) else condition
64
+ return ConditionalExpression(wrapped_condition, wrap_operand(true_expr), wrap_operand(false_expr))
67
65
 
68
66
 
69
- def min_expr(*expressions: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> 'Expression':
67
+ def min_expr(*expressions: ExpressionOperand) -> Expression:
70
68
  """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
69
+ return _create_comparison_expr(expressions, "min")
82
70
 
83
71
 
84
- def max_expr(*expressions: Union['Expression', 'TypeSafeVariable', 'Quantity', int, float]) -> 'Expression':
72
+ def max_expr(*expressions: ExpressionOperand) -> Expression:
85
73
  """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
74
+ return _create_comparison_expr(expressions, "max")