qnty 0.0.9__py3-none-any.whl → 0.1.1__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.
- qnty/__init__.py +2 -3
- qnty/constants/__init__.py +10 -0
- qnty/constants/numerical.py +18 -0
- qnty/constants/solvers.py +6 -0
- qnty/constants/tests.py +6 -0
- qnty/dimensions/__init__.py +23 -0
- qnty/dimensions/base.py +97 -0
- qnty/dimensions/field_dims.py +126 -0
- qnty/dimensions/field_dims.pyi +128 -0
- qnty/dimensions/signature.py +111 -0
- qnty/equations/__init__.py +1 -1
- qnty/equations/equation.py +118 -155
- qnty/equations/system.py +68 -65
- qnty/expressions/__init__.py +25 -46
- qnty/expressions/formatter.py +188 -0
- qnty/expressions/functions.py +46 -68
- qnty/expressions/nodes.py +540 -384
- qnty/expressions/types.py +70 -0
- qnty/problems/__init__.py +145 -0
- qnty/problems/composition.py +1101 -0
- qnty/problems/problem.py +737 -0
- qnty/problems/rules.py +145 -0
- qnty/problems/solving.py +1216 -0
- qnty/problems/validation.py +127 -0
- qnty/quantities/__init__.py +28 -5
- qnty/quantities/base_qnty.py +677 -0
- qnty/quantities/field_converters.py +24004 -0
- qnty/quantities/field_qnty.py +1012 -0
- qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
- qnty/{generated/quantities.py → quantities/field_vars.py} +829 -444
- qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
- qnty/solving/manager.py +50 -44
- qnty/solving/order.py +181 -133
- qnty/solving/solvers/__init__.py +2 -9
- qnty/solving/solvers/base.py +27 -37
- qnty/solving/solvers/iterative.py +115 -135
- qnty/solving/solvers/simultaneous.py +93 -165
- qnty/units/__init__.py +1 -0
- qnty/{generated/units.py → units/field_units.py} +1700 -991
- qnty/units/field_units.pyi +2461 -0
- qnty/units/prefixes.py +58 -105
- qnty/units/registry.py +76 -89
- qnty/utils/__init__.py +16 -0
- qnty/utils/caching/__init__.py +23 -0
- qnty/utils/caching/manager.py +401 -0
- qnty/utils/error_handling/__init__.py +66 -0
- qnty/utils/error_handling/context.py +39 -0
- qnty/utils/error_handling/exceptions.py +96 -0
- qnty/utils/error_handling/handlers.py +171 -0
- qnty/utils/logging.py +4 -4
- qnty/utils/protocols.py +164 -0
- qnty/utils/scope_discovery.py +420 -0
- {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/METADATA +1 -1
- qnty-0.1.1.dist-info/RECORD +60 -0
- qnty/_backup/problem_original.py +0 -1251
- qnty/_backup/quantity.py +0 -63
- qnty/codegen/cli.py +0 -125
- qnty/codegen/generators/data/unit_data.json +0 -8807
- qnty/codegen/generators/data_processor.py +0 -345
- qnty/codegen/generators/dimensions_gen.py +0 -434
- qnty/codegen/generators/doc_generator.py +0 -141
- qnty/codegen/generators/out/dimension_mapping.json +0 -974
- qnty/codegen/generators/out/dimension_metadata.json +0 -123
- qnty/codegen/generators/out/units_metadata.json +0 -223
- qnty/codegen/generators/quantities_gen.py +0 -159
- qnty/codegen/generators/setters_gen.py +0 -178
- qnty/codegen/generators/stubs_gen.py +0 -167
- qnty/codegen/generators/units_gen.py +0 -295
- qnty/expressions/cache.py +0 -94
- qnty/generated/dimensions.py +0 -514
- qnty/problem/__init__.py +0 -91
- qnty/problem/base.py +0 -142
- qnty/problem/composition.py +0 -385
- qnty/problem/composition_mixin.py +0 -382
- qnty/problem/equations.py +0 -413
- qnty/problem/metaclass.py +0 -302
- qnty/problem/reconstruction.py +0 -1016
- qnty/problem/solving.py +0 -180
- qnty/problem/validation.py +0 -64
- qnty/problem/variables.py +0 -239
- qnty/quantities/expression_quantity.py +0 -314
- qnty/quantities/quantity.py +0 -428
- qnty/quantities/typed_quantity.py +0 -215
- qnty/validation/__init__.py +0 -0
- qnty/validation/registry.py +0 -0
- qnty/validation/rules.py +0 -167
- qnty-0.0.9.dist-info/RECORD +0 -63
- /qnty/{codegen → extensions}/__init__.py +0 -0
- /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
- /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
- /qnty/{generated → extensions/reporting}/__init__.py +0 -0
- {qnty-0.0.9.dist-info → qnty-0.1.1.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)
|
qnty/expressions/functions.py
CHANGED
@@ -5,92 +5,70 @@ Expression Helper Functions
|
|
5
5
|
Convenience functions for creating mathematical expressions.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from
|
8
|
+
from ..quantities import FieldQnty, Quantity
|
9
|
+
from .nodes import BinaryOperation, ConditionalExpression, Expression, UnaryFunction, wrap_operand
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
49
|
-
"""Base-10 logarithm function."""
|
50
|
-
return UnaryFunction('log10', Expression._wrap_operand(expr))
|
43
|
+
return result
|
51
44
|
|
52
45
|
|
53
|
-
|
54
|
-
|
55
|
-
|
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(
|
59
|
-
|
60
|
-
|
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
|
-
|
63
|
-
|
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:
|
67
|
+
def min_expr(*expressions: ExpressionOperand) -> Expression:
|
70
68
|
"""Minimum of multiple expressions."""
|
71
|
-
|
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:
|
72
|
+
def max_expr(*expressions: ExpressionOperand) -> Expression:
|
85
73
|
"""Maximum of multiple expressions."""
|
86
|
-
|
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")
|