diff --git a/examples/dynamics/lennard_jones_potential_equilibrium_separation.py b/examples/dynamics/lennard_jones_potential_equilibrium_separation.py index 3ab4cc299..7c2625f6a 100644 --- a/examples/dynamics/lennard_jones_potential_equilibrium_separation.py +++ b/examples/dynamics/lennard_jones_potential_equilibrium_separation.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from sympy import solve, symbols +from sympy import solve, symbols, Expr from symplyphysics import ( print_expression, vector_magnitude, @@ -8,7 +8,6 @@ units, Quantity, ) -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.fields.scalar_field import ScalarField from symplyphysics.core.points.cartesian_point import CartesianPoint from symplyphysics.laws.dynamics.fields import ( @@ -33,7 +32,7 @@ } -def potential_energy_function(point: CartesianPoint) -> ScalarValue: +def potential_energy_function(point: CartesianPoint) -> Expr: return A / point.x**12 - B / point.x**6 diff --git a/symplyphysics/core/approx.py b/symplyphysics/core/approx.py index b6c3ae190..e14517536 100644 --- a/symplyphysics/core/approx.py +++ b/symplyphysics/core/approx.py @@ -8,11 +8,11 @@ * `assert_equal_vectors` asserts that two quantity vectors are equal. """ -from typing import Optional +from typing import Optional, SupportsFloat from pytest import approx from sympy import N, re, im from sympy.physics.units import Dimension -from symplyphysics.core.dimensions import assert_equivalent_dimension, ScalarValue +from symplyphysics.core.dimensions import assert_equivalent_dimension from symplyphysics.core.symbols.quantities import Quantity from symplyphysics.core.vectors.vectors import QuantityVector @@ -44,7 +44,7 @@ def approx_equal_numbers( def approx_equal_quantities( lhs: Quantity, - rhs: ScalarValue, + rhs: SupportsFloat, *, relative_tolerance: Optional[float] = None, absolute_tolerance: Optional[float] = None, @@ -80,8 +80,8 @@ def approx_equal_quantities( # Combined with assert for better test output def assert_equal( - lhs: ScalarValue, - rhs: ScalarValue, + lhs: SupportsFloat, + rhs: SupportsFloat, *, relative_tolerance: Optional[float] = None, absolute_tolerance: Optional[float] = None, diff --git a/symplyphysics/core/convert.py b/symplyphysics/core/convert.py index 17a13aab4..1308a631f 100644 --- a/symplyphysics/core/convert.py +++ b/symplyphysics/core/convert.py @@ -1,11 +1,8 @@ -from typing import Any -from sympy import Expr, sympify, S -from sympy.physics import units +from typing import Any, SupportsFloat +from sympy import Expr, S, sympify from sympy.physics.units import Quantity as SymQuantity -from sympy.physics.units.systems.si import dimsys_SI -from sympy.physics.units.definitions import dimension_definitions -from .dimensions import assert_equivalent_dimension +from .dimensions import assert_equivalent_dimension, dimension_to_si_unit from .symbols.quantities import Quantity @@ -13,41 +10,31 @@ def convert_to(value: Expr, target_unit: Expr) -> Expr: """ Convert ``value`` to its scale factor with ``value`` unit represented as ``target_unit``. """ - value_quantity = value if isinstance(value, Quantity) else Quantity(value) - target_quantity = target_unit if isinstance(target_unit, Quantity) else Quantity(target_unit) - assert_equivalent_dimension(value_quantity, value_quantity.dimension.name, "convert_to", - target_quantity.dimension) - return sympify(value_quantity.scale_factor) * (1 / sympify(target_quantity.scale_factor)) + if not isinstance(value, SymQuantity): + value = Quantity(value) + if not isinstance(target_unit, SymQuantity): + target_unit = Quantity(target_unit) + + assert_equivalent_dimension(value, value.dimension.name, "convert_to", target_unit.dimension) + return sympify(value.scale_factor) * (1 / sympify(target_unit.scale_factor)) def convert_to_float(value: Expr) -> float: return float(convert_to(value, S.One)) -_si_conversions: dict[units.Dimension, Expr] = { - dimension_definitions.angle: S.One, - dimension_definitions.length: units.meter, - dimension_definitions.mass: units.kilogram, - dimension_definitions.time: units.second, - dimension_definitions.current: units.ampere, - dimension_definitions.temperature: units.kelvin, - dimension_definitions.amount_of_substance: units.mole, - dimension_definitions.luminous_intensity: units.candela, -} - +def convert_to_si(value: SupportsFloat) -> Expr: + if not isinstance(value, SymQuantity): + value = Quantity(value) -def convert_to_si(value: Expr | float) -> Expr: - quantity = value if isinstance(value, Quantity) else Quantity(value) - dependencies = dimsys_SI.get_dimensional_dependencies(quantity.dimension) - unit = S.One - for dimension, power in dependencies.items(): - unit *= _si_conversions[dimension]**power - return convert_to(quantity, unit) + unit = dimension_to_si_unit(value.dimension) + return convert_to(value, unit) def evaluate_quantity(quantity: Expr, **kwargs: Any) -> Quantity: - if not isinstance(quantity, Quantity): + if not isinstance(quantity, SymQuantity): quantity = Quantity(quantity) + scale_factor_ = quantity.scale_factor.evalf(**kwargs) dimension = quantity.dimension return Quantity(scale_factor_, dimension=dimension) diff --git a/symplyphysics/core/dimensions.py b/symplyphysics/core/dimensions.py deleted file mode 100644 index c58af23d6..000000000 --- a/symplyphysics/core/dimensions.py +++ /dev/null @@ -1,443 +0,0 @@ -from __future__ import annotations - -from typing import Any, Callable, TypeAlias, Iterable -from sympy import Abs, Expr, S, Derivative, Function as SymFunction, Min, Max, sympify, Add, Mul, Pow -from sympy.functions.elementary.miscellaneous import MinMaxBase -from sympy.physics.units import Dimension, Quantity as SymQuantity -from sympy.physics.units.prefixes import Prefix -from sympy.physics.units.systems.si import dimsys_SI - -from .errors import UnitsError - -ScalarValue: TypeAlias = Expr | float - - -class AnyDimension(Dimension): # type: ignore[misc] - # pylint: disable-next=signature-differs - def __new__(cls) -> AnyDimension: - return super().__new__(cls, "any_dimension") # type: ignore[no-any-return] - - def _eval_nseries(self, _x: Any, _n: Any, _logx: Any, _cdir: Any) -> Any: - pass - - -any_dimension = AnyDimension() - - -def _is_any_dimension(factor: Expr) -> bool: - return factor in (S.Zero, S.Infinity, S.NegativeInfinity, S.NaN) - - -def _is_number(value: Any) -> bool: - try: - complex(value) - except (TypeError, ValueError): - return False - - return True - - -# pylint: disable-next=too-many-statements -def collect_quantity_factor_and_dimension(expr: Expr) -> tuple[Expr, Dimension]: - """ - Returns tuple with scale factor expression and dimension expression. Designed to be - used during the instantiation of the `symplyphysics.Quantity` class. - - Raises: - ValueError: If the dimensions of the sub-expressions don't match. - sympy.SympifyError: If ``expr`` cannot be converted to a value used by `sympy`. - """ - - def _collect_quantity(expr: SymQuantity) -> tuple[Expr, Dimension]: - return (expr.scale_factor, expr.dimension) - - def _collect_prefix(expr: Prefix) -> tuple[Expr, Dimension]: - return (expr.scale_factor, dimensionless) - - def _elementwise_wrapper( - inner: Callable[[Expr, Dimension, Expr], tuple[Expr, Dimension]] - ) -> Callable[[Expr], tuple[Expr, Dimension]]: - - def outer(expr: Expr) -> tuple[Expr, Dimension]: - factor, dim = collect_quantity_factor_and_dimension(expr.args[0]) - for arg in expr.args[1:]: - factor, dim = inner(factor, dim, arg) - return (factor, dim) - - return outer - - @_elementwise_wrapper - def _collect_mul(factor: Expr, dim: Dimension, arg: Expr) -> tuple[Expr, Dimension]: - arg_factor, arg_dim = collect_quantity_factor_and_dimension(arg) - - factor *= arg_factor - - if _is_any_dimension(factor): - return (factor, dimensionless) - - return (factor, dim * arg_dim) - - def _collect_pow(expr: Pow) -> tuple[Expr, Dimension]: - (base_factor, base_dim) = collect_quantity_factor_and_dimension(expr.base) - (exp_factor, exp_dim) = collect_quantity_factor_and_dimension(expr.exp) - - if _is_any_dimension(exp_factor) or dimsys_SI.is_dimensionless(exp_dim): - return (base_factor**exp_factor, base_dim**exp_factor) - - raise ValueError(f"Dimension of '{expr.exp}' is {exp_dim}, but it should be dimensionless") - - @_elementwise_wrapper - def _collect_add(factor: Expr, dim: Dimension, arg: Expr) -> tuple[Expr, Dimension]: - arg_factor, arg_dim = collect_quantity_factor_and_dimension(arg) - - if _is_any_dimension(factor): - dim = arg_dim - elif _is_any_dimension(arg_factor): - arg_dim = dim - - if not dimsys_SI.equivalent_dims(dim, arg_dim): - raise ValueError(f"Dimension of '{arg}' is {arg_dim}, but it should be {dim}") - - return (factor + arg_factor, dim) - - def _collect_abs(expr: Abs) -> tuple[Expr, Dimension]: - arg_factor, arg_dim = collect_quantity_factor_and_dimension(expr.args[0]) - return (Abs(arg_factor), arg_dim) - - def _collect_min_max(expr: MinMaxBase) -> tuple[Expr, Dimension]: - cls = type(expr) - - def collect(factor: Expr, dim: Dimension, arg: Expr) -> tuple[Expr, Dimension]: - arg_factor, arg_dim = collect_quantity_factor_and_dimension(arg) - - if _is_any_dimension(factor): - dim = arg_dim - elif _is_any_dimension(arg_factor): - arg_dim = dim - - if not dimsys_SI.equivalent_dims(dim, arg_dim): - raise ValueError(f"Dimension of '{arg}' is {arg_dim}, but it should be {dim}") - - return (cls(factor, arg_factor), dim) - - return _elementwise_wrapper(collect)(expr) - - def _collect_function(expr: SymFunction) -> tuple[Expr, Dimension]: - factors: list[Expr] = [] - - for arg in expr.args: - (arg_factor, arg_dim) = collect_quantity_factor_and_dimension(arg) - - # only functions with dimensionless arguments are supported - if _is_any_dimension(arg_factor) or dimsys_SI.is_dimensionless(arg_dim): - factors.append(arg_factor) - continue - - raise ValueError(f"Dimension of '{arg}' is {arg_dim}, but it should be dimensionless") - - factor = expr.func(*(f for f in factors)) - return (factor, dimensionless) - - def _unsupported_derivative(expr: Derivative) -> tuple[Expr, Dimension]: - raise ValueError(f"'{expr}' should not contain unevaluated Derivative") - - def _collect_default(expr: Expr) -> tuple[Expr, Dimension]: - if not _is_number(expr): - raise ValueError(f"'{expr}' should be an expression made of numbers or quantities.") - - return expr, dimensionless - - expr = sympify(expr) - - cases: dict[type, Callable[[Expr], tuple[Expr, Dimension]]] = { - SymQuantity: _collect_quantity, - Prefix: _collect_prefix, - Mul: _collect_mul, - Pow: _collect_pow, - Add: _collect_add, - Abs: _collect_abs, - MinMaxBase: _collect_min_max, - Derivative: _unsupported_derivative, - SymFunction: _collect_function, - } - - for type_, collector in cases.items(): - if isinstance(expr, type_): - return collector(expr) - - return _collect_default(expr) - - -# pylint: disable-next=too-many-statements -def collect_expression_and_dimension(expr: Expr) -> tuple[Expr, Dimension]: - """ - Returns the simplified representation and the dimension of the given expression. Unlike - `collect_quantity_factor_and_dimension` it supports derivatives, symbols, and functions with - dimensionful arguments. - - Raises: - ValueError: if a sub-expression is dimensionful instead of dimensionless. - UnitsError: if the dimensions of sub-expressions don't match. - """ - - from .symbols.quantities import Quantity # pylint: disable=import-outside-toplevel - - def _split_numeric_and_symbolic( - expr: Expr,) -> tuple[list[Expr], list[SymQuantity], list[tuple[Expr, Dimension]]]: - nums: list[Expr] = [] - qtys: list[SymQuantity] = [] - syms: list[tuple[Expr, Dimension]] = [] - - for arg in expr.args: - if isinstance(arg, SymQuantity): - qtys.append(arg) - elif _is_number(arg): - nums.append(arg) - else: - syms.append(collect_expression_and_dimension(arg)) - - return nums, qtys, syms - - def _collect_mul(expr: Mul) -> tuple[Expr, Dimension]: - nums, qtys, syms = _split_numeric_and_symbolic(expr) - - qty_factor = S.One - for num in nums: - qty_factor *= num - - qty_dim = dimensionless - for qty in qtys: - factor = qty.scale_factor - qty_factor *= factor - - if not _is_any_dimension(factor): - qty_dim *= qty.dimension - - if _is_any_dimension(qty_factor): - return qty_factor, dimensionless - - if dimsys_SI.is_dimensionless(qty_dim): - expr_ = qty_factor - else: - expr_ = Quantity(qty_factor, dimension=qty_dim) - - dim = qty_dim - - for sym_expr, sym_dim in syms: - expr_ *= sym_expr - dim *= sym_dim - - return expr_, dim - - def _collect_pow(expr: Pow) -> tuple[Expr, Dimension]: - exp_expr, exp_dim = collect_expression_and_dimension(expr.exp) - - if not _is_any_dimension(exp_expr) and not dimsys_SI.is_dimensionless(exp_dim): - raise ValueError( - f"Dimension of '{expr.exp}' is {exp_dim}, but it should be dimensionless") - - base_expr, base_dim = collect_expression_and_dimension(expr.base) - - expr_ = base_expr**exp_expr - dim = base_dim**exp_expr - - return expr_, dim - - def _collect_unique_dimension( - nums: Iterable[Expr], - qtys: Iterable[SymQuantity], - syms: Iterable[tuple[Expr, Dimension]], - ) -> Dimension: - dim = None - - if not all(_is_any_dimension(num) for num in nums): - dim = dimensionless - - for qty in qtys: - if dim is None: - dim = qty.dimension - continue - - if _is_any_dimension(qty.scale_factor): - continue - - if not dimsys_SI.equivalent_dims(dim, qty.dimension): - raise UnitsError(f"The dimension of {qty} is {qty.dimension}, expected {dim}") - - for sym_expr, sym_dim in syms: - if dim is None: - dim = sym_dim - continue - - if _is_any_dimension(sym_expr): - continue - - if not dimsys_SI.equivalent_dims(dim, sym_dim): - raise UnitsError(f"The dimension of '{sym_expr}' is {sym_dim}, expected {dim}") - - # edge case when both `qtys` and `syms` are empty and all `nums` are of any dimension - if dim is None: - dim = dimensionless - - return dim - - def _collect_add(expr: Add) -> tuple[Expr, Dimension]: - nums, qtys, syms = _split_numeric_and_symbolic(expr) - dim = _collect_unique_dimension(nums, qtys, syms) - - qty_sum = sum(nums, start=S.Zero) + sum((qty.scale_factor for qty in qtys), start=S.Zero) - - if not dimsys_SI.is_dimensionless(dim): - qty_sum = Quantity(qty_sum, dimension=dim) - - sym_sum = sum((sym_expr for (sym_expr, _) in syms), start=S.Zero) - - return qty_sum + sym_sum, dim - - def _collect_abs(expr: Abs) -> tuple[Expr, Dimension]: - expr_, dim = collect_expression_and_dimension(expr.args[0]) - return Abs(expr_), dim - - def _collect_min_max(expr: MinMaxBase) -> tuple[Expr, Dimension]: - cls = type(expr) - - nums, qtys, syms = _split_numeric_and_symbolic(expr) - dim = _collect_unique_dimension(nums, qtys, syms) - - if dimsys_SI.is_dimensionless(dim): - expr_ = cls( - *nums, - *(qty.scale_factor for qty in qtys), - *(sym_expr for (sym_expr, _) in syms), - ) - return expr_, dim - - minmax_num = cls(*nums) - minmax_qty_factor = cls(minmax_num, *(qty.scale_factor for qty in qtys)) - minmax_qty = Quantity(minmax_qty_factor, dimension=dim) - - expr_ = cls(minmax_qty, *(sym_expr for (sym_expr, _) in syms)) - - return expr_, dim - - def _collect_function(expr: SymFunction) -> tuple[Expr, Dimension]: - func = expr.func - dim = getattr(func, "dimension", dimensionless) - - factors = [] - for arg in expr.args: - arg_factor, _ = collect_expression_and_dimension(arg) - factors.append(arg_factor) - - expr_ = func(*factors) - return expr_, dim - - def _collect_derivative(expr: Derivative) -> tuple[Expr, Dimension]: - func, *args = expr.args - _, dim = collect_expression_and_dimension(func.func) - - expr_ = func - for arg, n in args: - arg_expr, arg_dim = collect_expression_and_dimension(arg) - dim /= arg_dim**n - expr_ = expr_.diff((arg_expr, n)) - return expr_, dim - - expr = sympify(expr) - - # early return that works for `sympy.Quantity`, `SymbolNew`, `SymbolIndexedNew`, and `FunctionNew` - if hasattr(expr, "dimension"): - return expr, getattr(expr, "dimension") - - cases: dict[type, Callable[[Any], tuple[Expr, Dimension]]] = { - Mul: _collect_mul, - Pow: _collect_pow, - Add: _collect_add, - Abs: _collect_abs, - Min: _collect_min_max, - Max: _collect_min_max, - Derivative: _collect_derivative, - SymFunction: _collect_function, - } - - for type_, collector in cases.items(): - if isinstance(expr, type_): - return collector(expr) - - return (expr, dimensionless) - - -def assert_equivalent_dimension( - arg: SymQuantity | ScalarValue | Dimension, - param_name: str, - func_name: str, - expected_unit: SymQuantity | Dimension, -) -> None: - """ - Asserts if the dimension of the argument matches the provided unit. - - Args: - arg: Number, quantity, expression made of numbers and quantities, or dimension. - param_name: Name of the parameter of the calling function. - func_name: Name of the calling function. - expected_unit: Expression or dimension which `arg` is compared to. - - Raises: - TypeError: If `arg` is a number, but `expected_unit` is not dimensionless. - UnitsError: If the dimensions don't match otherwise, or when the scale factor of `arg` is not a number. - """ - - if not isinstance(expected_unit, Dimension): - expected_scale_factor, expected_unit = collect_quantity_factor_and_dimension(expected_unit) - - if _is_any_dimension(expected_scale_factor) or isinstance(expected_unit, AnyDimension): - return - - # HACK: this allows to treat angle type as dimensionless - expected_unit = expected_unit.subs("angle", S.One) - - if not isinstance(arg, Dimension): - (scale_factor, arg) = collect_quantity_factor_and_dimension(arg) - - if not _is_number(scale_factor): - # NOTE: this should probably raise `ValueError` or `TypeError` - raise UnitsError(f"Argument '{param_name}' to function '{func_name}' should " - f"not contain free symbols: '{scale_factor}'") - - if _is_any_dimension(scale_factor) or isinstance(arg, AnyDimension): - return - - # HACK: this allows to treat angle type as dimensionless - arg = arg.subs("angle", S.One) - - if dimsys_SI.is_dimensionless(arg) and not dimsys_SI.is_dimensionless(expected_unit): - # NOTE: this should probably be `UnitsError` - raise TypeError(f"Argument '{param_name}' to function '{func_name}'" - f" is Number but '{expected_unit}' is not dimensionless") - - if not dimsys_SI.equivalent_dims(arg, expected_unit): - raise UnitsError(f"Argument '{param_name}' to function '{func_name}' must " - f"be in units equivalent to '{expected_unit.name}', got {arg.name}") - - -dimensionless = Dimension(S.One) - - -def print_dimension(dimension: Dimension) -> str: - return "dimensionless" if dimsys_SI.is_dimensionless(dimension) else str(dimension.name) - - -__all__ = [ - # re-exports - "Dimension", - - # locals - "ScalarValue", - "AnyDimension", - "any_dimension", - "collect_quantity_factor_and_dimension", - "collect_expression_and_dimension", - "assert_equivalent_dimension", - "dimensionless", - "print_dimension", -] diff --git a/symplyphysics/core/dimensions/__init__.py b/symplyphysics/core/dimensions/__init__.py new file mode 100644 index 000000000..1e68e2c3f --- /dev/null +++ b/symplyphysics/core/dimensions/__init__.py @@ -0,0 +1,28 @@ +from sympy.physics.units import Dimension +from sympy.physics.units.systems.si import dimsys_SI + +from .dimensions import any_dimension, assert_equivalent_dimension, dimension_to_si_unit, print_dimension +from .collect_quantity import collect_quantity_factor_and_dimension +from .collect_expression import collect_expression_and_dimension +from .miscellaneous import dimensionless + +__all__ = [ + # re-exports + "Dimension", + "dimsys_SI", + + # .dimensions + "any_dimension", + "assert_equivalent_dimension", + "print_dimension", + "dimension_to_si_unit", + + # .collect_quantity + "collect_quantity_factor_and_dimension", + + # .collect_expression + "collect_expression_and_dimension", + + # .miscellaneous + "dimensionless", +] diff --git a/symplyphysics/core/dimensions/collect_expression.py b/symplyphysics/core/dimensions/collect_expression.py new file mode 100644 index 000000000..bd4323612 --- /dev/null +++ b/symplyphysics/core/dimensions/collect_expression.py @@ -0,0 +1,219 @@ +from typing import Iterable, Callable, Any +from sympy import Expr, S, Mul, Add, Pow, Abs, Min, Max, Derivative, Function as SymFunction, sympify +from sympy.functions.elementary.miscellaneous import MinMaxBase +from sympy.physics.units import Dimension, Quantity as SymQuantity +from sympy.physics.units.systems.si import dimsys_SI + +from ..errors import UnitsError +from ..symbols.quantities import Quantity +from .miscellaneous import is_number, is_any_dimension, dimensionless + + +def _split_numeric_and_symbolic( + expr: Expr,) -> tuple[list[Expr], list[SymQuantity], list[tuple[Expr, Dimension]]]: + nums: list[Expr] = [] + qtys: list[SymQuantity] = [] + syms: list[tuple[Expr, Dimension]] = [] + + for arg in expr.args: + if isinstance(arg, SymQuantity): + qtys.append(arg) + elif is_number(arg): + nums.append(arg) + else: + syms.append(collect_expression_and_dimension(arg)) + + return nums, qtys, syms + + +def _collect_mul(expr: Mul) -> tuple[Expr, Dimension]: + nums, qtys, syms = _split_numeric_and_symbolic(expr) + + qty_factor = S.One + for num in nums: + qty_factor *= num + + qty_dim = dimensionless + for qty in qtys: + factor = qty.scale_factor + qty_factor *= factor + + if not is_any_dimension(factor): + qty_dim *= qty.dimension + + if is_any_dimension(qty_factor): + return qty_factor, dimensionless + + if dimsys_SI.is_dimensionless(qty_dim): + expr_ = qty_factor + else: + expr_ = Quantity(qty_factor, dimension=qty_dim) + + dim = qty_dim + + for sym_expr, sym_dim in syms: + expr_ *= sym_expr + dim *= sym_dim + + return expr_, dim + + +def _collect_pow(expr: Pow) -> tuple[Expr, Dimension]: + exp_expr, exp_dim = collect_expression_and_dimension(expr.exp) + + if not is_any_dimension(exp_expr) and not dimsys_SI.is_dimensionless(exp_dim): + raise ValueError(f"Dimension of '{expr.exp}' is {exp_dim}, but it should be dimensionless") + + base_expr, base_dim = collect_expression_and_dimension(expr.base) + + expr_ = base_expr**exp_expr + dim = base_dim**exp_expr + + return expr_, dim + + +def _collect_unique_dimension( + nums: Iterable[Expr], + qtys: Iterable[SymQuantity], + syms: Iterable[tuple[Expr, Dimension]], +) -> Dimension: + dim = None + + if not all(is_any_dimension(num) for num in nums): + dim = dimensionless + + for qty in qtys: + if dim is None: + dim = qty.dimension + continue + + if is_any_dimension(qty.scale_factor): + continue + + if not dimsys_SI.equivalent_dims(dim, qty.dimension): + raise UnitsError(f"The dimension of {qty} is {qty.dimension}, expected {dim}") + + for sym_expr, sym_dim in syms: + if dim is None: + dim = sym_dim + continue + + if is_any_dimension(sym_expr): + continue + + if not dimsys_SI.equivalent_dims(dim, sym_dim): + raise UnitsError(f"The dimension of '{sym_expr}' is {sym_dim}, expected {dim}") + + # edge case when both `qtys` and `syms` are empty and all `nums` are of any dimension + if dim is None: + dim = dimensionless + + return dim + + +def _collect_add(expr: Add) -> tuple[Expr, Dimension]: + nums, qtys, syms = _split_numeric_and_symbolic(expr) + dim = _collect_unique_dimension(nums, qtys, syms) + + qty_sum = sum(nums, start=S.Zero) + sum((qty.scale_factor for qty in qtys), start=S.Zero) + + if not dimsys_SI.is_dimensionless(dim): + qty_sum = Quantity(qty_sum, dimension=dim) + + sym_sum = sum((sym_expr for (sym_expr, _) in syms), start=S.Zero) + + return qty_sum + sym_sum, dim + + +def _collect_abs(expr: Abs) -> tuple[Expr, Dimension]: + expr_, dim = collect_expression_and_dimension(expr.args[0]) + return Abs(expr_), dim + + +def _collect_min_max(expr: MinMaxBase) -> tuple[Expr, Dimension]: + cls = type(expr) + + nums, qtys, syms = _split_numeric_and_symbolic(expr) + dim = _collect_unique_dimension(nums, qtys, syms) + + if dimsys_SI.is_dimensionless(dim): + expr_ = cls( + *nums, + *(qty.scale_factor for qty in qtys), + *(sym_expr for (sym_expr, _) in syms), + ) + return expr_, dim + + minmax_num = cls(*nums) + minmax_qty_factor = cls(minmax_num, *(qty.scale_factor for qty in qtys)) + minmax_qty = Quantity(minmax_qty_factor, dimension=dim) + + expr_ = cls(minmax_qty, *(sym_expr for (sym_expr, _) in syms)) + + return expr_, dim + + +def _collect_function(expr: SymFunction) -> tuple[Expr, Dimension]: + func = expr.func + dim = getattr(func, "dimension", dimensionless) + + factors = [] + for arg in expr.args: + arg_factor, _ = collect_expression_and_dimension(arg) + factors.append(arg_factor) + + expr_ = func(*factors) + return expr_, dim + + +def _collect_derivative(expr: Derivative) -> tuple[Expr, Dimension]: + func, *args = expr.args + _, dim = collect_expression_and_dimension(func.func) + + expr_ = func + for arg, n in args: + arg_expr, arg_dim = collect_expression_and_dimension(arg) + dim /= arg_dim**n + expr_ = expr_.diff((arg_expr, n)) + return expr_, dim + + +_cases: dict[type, Callable[[Any], tuple[Expr, Dimension]]] = { + Mul: _collect_mul, + Pow: _collect_pow, + Add: _collect_add, + Abs: _collect_abs, + Min: _collect_min_max, + Max: _collect_min_max, + Derivative: _collect_derivative, + SymFunction: _collect_function, +} + + +def collect_expression_and_dimension(expr: Any) -> tuple[Expr, Dimension]: + """ + Returns the simplified representation and the dimension of the given expression. Unlike + `collect_quantity_factor_and_dimension` it supports derivatives, symbols, and functions with + dimensionful arguments. + + Raises: + ValueError: if a sub-expression is dimensionful instead of dimensionless. + UnitsError: if the dimensions of sub-expressions don't match. + """ + + expr = sympify(expr) + + # early return that works for `sympy.Quantity`, `SymbolNew`, `SymbolIndexedNew`, and `FunctionNew` + if hasattr(expr, "dimension"): + return expr, getattr(expr, "dimension") + + for type_, collector in _cases.items(): + if isinstance(expr, type_): + return collector(expr) + + return (expr, dimensionless) + + +__all__ = [ + "collect_expression_and_dimension", +] diff --git a/symplyphysics/core/dimensions/collect_quantity.py b/symplyphysics/core/dimensions/collect_quantity.py new file mode 100644 index 000000000..0d2c4c7f2 --- /dev/null +++ b/symplyphysics/core/dimensions/collect_quantity.py @@ -0,0 +1,153 @@ +from typing import Callable, SupportsFloat +from sympy import Expr, Pow, Derivative, Abs, Mul, Add, Function as SymFunction, sympify +from sympy.functions.elementary.miscellaneous import MinMaxBase +from sympy.physics.units import Quantity as SymQuantity, Dimension +from sympy.physics.units.prefixes import Prefix +from sympy.physics.units.systems.si import dimsys_SI + +from .miscellaneous import is_any_dimension, is_number, dimensionless + + +def _collect_quantity(expr: SymQuantity) -> tuple[Expr, Dimension]: + return (expr.scale_factor, expr.dimension) + + +def _collect_prefix(expr: Prefix) -> tuple[Expr, Dimension]: + return (expr.scale_factor, dimensionless) + + +def _elementwise_wrapper( + inner: Callable[[Expr, Dimension, Expr], tuple[Expr, Dimension]] +) -> Callable[[Expr], tuple[Expr, Dimension]]: + + def outer(expr: Expr) -> tuple[Expr, Dimension]: + factor, dim = collect_quantity_factor_and_dimension(expr.args[0]) + for arg in expr.args[1:]: + factor, dim = inner(factor, dim, arg) + return (factor, dim) + + return outer + + +@_elementwise_wrapper +def _collect_mul(factor: Expr, dim: Dimension, arg: Expr) -> tuple[Expr, Dimension]: + arg_factor, arg_dim = collect_quantity_factor_and_dimension(arg) + + factor *= arg_factor + + if is_any_dimension(factor): + return (factor, dimensionless) + + return (factor, dim * arg_dim) + + +def _collect_pow(expr: Pow) -> tuple[Expr, Dimension]: + (base_factor, base_dim) = collect_quantity_factor_and_dimension(expr.base) + (exp_factor, exp_dim) = collect_quantity_factor_and_dimension(expr.exp) + + if is_any_dimension(exp_factor) or dimsys_SI.is_dimensionless(exp_dim): + return (base_factor**exp_factor, base_dim**exp_factor) + + raise ValueError(f"Dimension of '{expr.exp}' is {exp_dim}, but it should be dimensionless") + + +@_elementwise_wrapper +def _collect_add(factor: Expr, dim: Dimension, arg: Expr) -> tuple[Expr, Dimension]: + arg_factor, arg_dim = collect_quantity_factor_and_dimension(arg) + + if is_any_dimension(factor): + dim = arg_dim + elif is_any_dimension(arg_factor): + arg_dim = dim + + if not dimsys_SI.equivalent_dims(dim, arg_dim): + raise ValueError(f"Dimension of '{arg}' is {arg_dim}, but it should be {dim}") + + return (factor + arg_factor, dim) + + +def _collect_abs(expr: Abs) -> tuple[Expr, Dimension]: + arg_factor, arg_dim = collect_quantity_factor_and_dimension(expr.args[0]) + return (Abs(arg_factor), arg_dim) + + +def _collect_min_max(expr: MinMaxBase) -> tuple[Expr, Dimension]: + cls = type(expr) + + def collect(factor: Expr, dim: Dimension, arg: Expr) -> tuple[Expr, Dimension]: + arg_factor, arg_dim = collect_quantity_factor_and_dimension(arg) + + if is_any_dimension(factor): + dim = arg_dim + elif is_any_dimension(arg_factor): + arg_dim = dim + + if not dimsys_SI.equivalent_dims(dim, arg_dim): + raise ValueError(f"Dimension of '{arg}' is {arg_dim}, but it should be {dim}") + + return (cls(factor, arg_factor), dim) + + return _elementwise_wrapper(collect)(expr) + + +def _collect_function(expr: SymFunction) -> tuple[Expr, Dimension]: + factors: list[Expr] = [] + + for arg in expr.args: + (arg_factor, arg_dim) = collect_quantity_factor_and_dimension(arg) + + # only functions with dimensionless arguments are supported + if is_any_dimension(arg_factor) or dimsys_SI.is_dimensionless(arg_dim): + factors.append(arg_factor) + continue + + raise ValueError(f"Dimension of '{arg}' is {arg_dim}, but it should be dimensionless") + + factor = expr.func(*(f for f in factors)) + return (factor, dimensionless) + + +def _unsupported_derivative(expr: Derivative) -> tuple[Expr, Dimension]: + raise ValueError(f"'{expr}' should not contain unevaluated Derivative") + + +def _collect_default(expr: Expr) -> tuple[Expr, Dimension]: + if not is_number(expr): + raise ValueError(f"'{expr}' should be an expression made of numbers or quantities.") + + return expr, dimensionless + + +_cases: dict[type, Callable[[Expr], tuple[Expr, Dimension]]] = { + SymQuantity: _collect_quantity, + Prefix: _collect_prefix, + Mul: _collect_mul, + Pow: _collect_pow, + Add: _collect_add, + Abs: _collect_abs, + MinMaxBase: _collect_min_max, + Derivative: _unsupported_derivative, + SymFunction: _collect_function, +} + + +def collect_quantity_factor_and_dimension(expr: SupportsFloat) -> tuple[Expr, Dimension]: + """ + Returns tuple with scale factor expression and dimension expression. Designed to be + used during the instantiation of the `symplyphysics.Quantity` class. + + Raises: + ValueError: If the dimensions of the sub-expressions don't match. + sympy.SympifyError: If ``expr`` cannot be converted to a value used by `sympy`. + """ + + expr = sympify(expr) + + for type_, collector in _cases.items(): + if isinstance(expr, type_): + return collector(expr) + + return _collect_default(expr) + + +__all__ = ["collect_quantity_factor_and_dimension"] diff --git a/symplyphysics/core/dimensions/dimensions.py b/symplyphysics/core/dimensions/dimensions.py new file mode 100644 index 000000000..d5182b247 --- /dev/null +++ b/symplyphysics/core/dimensions/dimensions.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from typing import Any, SupportsFloat +from sympy import Expr, S +from sympy.physics import units +from sympy.physics.units import Dimension, Quantity as SymQuantity +from sympy.physics.units.systems.si import dimsys_SI + +from ..errors import UnitsError +from .collect_quantity import collect_quantity_factor_and_dimension +from .miscellaneous import is_any_dimension, is_number + + +class AnyDimension(Dimension): # type: ignore[misc] + # pylint: disable-next=signature-differs + def __new__(cls) -> AnyDimension: + return super().__new__(cls, "any_dimension") # type: ignore[no-any-return] + + def _eval_nseries(self, _x: Any, _n: Any, _logx: Any, _cdir: Any) -> Any: + pass + + +any_dimension = AnyDimension() + + +def assert_equivalent_dimension( + arg: SupportsFloat | Dimension, + param_name: str, + func_name: str, + expected_unit: SymQuantity | Dimension, +) -> None: + """ + Asserts if the dimension of the argument matches the provided unit. + + Args: + arg: Number, quantity, expression made of numbers and quantities, or dimension. + param_name: Name of the parameter of the calling function. + func_name: Name of the calling function. + expected_unit: Expression or dimension which `arg` is compared to. + + Raises: + TypeError: If `arg` is a number, but `expected_unit` is not dimensionless. + UnitsError: If the dimensions don't match otherwise, or when the scale factor of `arg` is not a number. + """ + + if not isinstance(expected_unit, Dimension): + expected_scale_factor, expected_unit = collect_quantity_factor_and_dimension(expected_unit) + + if is_any_dimension(expected_scale_factor) or isinstance(expected_unit, AnyDimension): + return + + # HACK: this allows to treat angle type as dimensionless + expected_unit = expected_unit.subs("angle", S.One) + + if not isinstance(arg, Dimension): + (scale_factor, arg) = collect_quantity_factor_and_dimension(arg) + + if not is_number(scale_factor): + # NOTE: this should probably raise `ValueError` or `TypeError` + raise UnitsError(f"Argument '{param_name}' to function '{func_name}' should " + f"not contain free symbols: '{scale_factor}'") + + if is_any_dimension(scale_factor) or isinstance(arg, AnyDimension): + return + + # HACK: this allows to treat angle type as dimensionless + arg = arg.subs("angle", S.One) + + if dimsys_SI.is_dimensionless(arg) and not dimsys_SI.is_dimensionless(expected_unit): + # NOTE: this should probably be `UnitsError` + raise TypeError(f"Argument '{param_name}' to function '{func_name}'" + f" is Number but '{expected_unit}' is not dimensionless") + + if not dimsys_SI.equivalent_dims(arg, expected_unit): + raise UnitsError(f"Argument '{param_name}' to function '{func_name}' must " + f"be in units equivalent to '{expected_unit.name}', got {arg.name}") + + +def print_dimension(dimension: Dimension) -> str: + """Returns the prettified name of ``dimension``.""" + + return "dimensionless" if dimsys_SI.is_dimensionless(dimension) else str(dimension.name) + + +_si_conversions = { + units.length: units.meter, + units.mass: units.kilogram, + units.time: units.second, + units.current: units.ampere, + units.temperature: units.kelvin, + units.amount_of_substance: units.mole, + units.luminous_intensity: units.candela, +} + + +def dimension_to_si_unit(dimension: Dimension) -> Expr: + """Converts ``dimension`` to the corresponding SI unit.""" + + si_unit = S.One + + dependencies = dimsys_SI.get_dimensional_dependencies(dimension) + for dim, n in dependencies.items(): + si_unit *= _si_conversions.get(dim, S.One)**n + + return si_unit + + +__all__ = [ + "AnyDimension", + "any_dimension", + "assert_equivalent_dimension", + "print_dimension", + "dimension_to_si_unit", +] diff --git a/symplyphysics/core/dimensions/miscellaneous.py b/symplyphysics/core/dimensions/miscellaneous.py new file mode 100644 index 000000000..ec3cfe33d --- /dev/null +++ b/symplyphysics/core/dimensions/miscellaneous.py @@ -0,0 +1,33 @@ +from typing import Any +from sympy import Expr, S +from sympy.physics.units import Dimension + + +def is_any_dimension(factor: Expr) -> bool: + """ + Checks if ``factor`` is `0`, `±Inf`, or `NaN`, which can have any dimension due to their + absorbing nature. + """ + + return factor in (S.Zero, S.Infinity, S.NegativeInfinity, S.NaN) + + +def is_number(value: Any) -> bool: + """Checks if ``value`` is a (complex) number.""" + + try: + complex(value) + except (TypeError, ValueError): + return False + + return True + + +dimensionless = Dimension(S.One) +"""Alias for `Dimension(1)`.""" + +__all__ = [ + "is_any_dimension", + "is_number", + "dimensionless", +] diff --git a/symplyphysics/core/experimental/vectors/__init__.py b/symplyphysics/core/experimental/vectors/__init__.py index 7c5714756..b1cb72a40 100644 --- a/symplyphysics/core/experimental/vectors/__init__.py +++ b/symplyphysics/core/experimental/vectors/__init__.py @@ -380,7 +380,7 @@ def filter_scales(mapping: dict[VectorExpr, Expr]) -> dict[VectorExpr, Expr]: mapping = filter_scales(collect_scales(flatten_additions(self.args))) - scaled_addends = [vector * scale for vector, scale in mapping.items()] + scaled_addends: list[VectorExpr] = [vector * scale for vector, scale in mapping.items()] match len(scaled_addends): case 0: diff --git a/symplyphysics/core/fields/analysis.py b/symplyphysics/core/fields/analysis.py index 9343d92c2..f8235caaa 100644 --- a/symplyphysics/core/fields/analysis.py +++ b/symplyphysics/core/fields/analysis.py @@ -1,7 +1,6 @@ -from typing import Sequence +from typing import Sequence, Any from sympy import Expr, integrate, simplify from symplyphysics import Vector, dot_vectors, vector_magnitude, vector_unit -from ..dimensions import ScalarValue from ..fields.operators import curl_operator, divergence_operator from ..fields.vector_field import VectorField from ..geometry.elements import parametrized_curve_element, parametrized_curve_element_magnitude, volume_element_magnitude @@ -10,8 +9,11 @@ # trajectory should be array with projections to coordinates, eg [3 * cos(parameter), 3 * sin(parameter)] -def circulation_along_curve(field: VectorField, trajectory: Sequence[Expr], - parameter_limits: ParameterLimits) -> ScalarValue: +def circulation_along_curve( + field: VectorField, + trajectory: Sequence[Any], + parameter_limits: ParameterLimits, +) -> Expr: (parameter, parameter_from, parameter_to) = parameter_limits field_applied = field.apply(trajectory) curve_element_vector = parametrized_curve_element(Vector(trajectory, field.coordinate_system), @@ -23,8 +25,12 @@ def circulation_along_curve(field: VectorField, trajectory: Sequence[Expr], # calculate circulation along curve using surface that has this curve as a boundary # surface should be array with projections to coordinates, eg [parameter1 * cos(parameter2), parameter1 * sin(parameter2)] -def circulation_along_surface_boundary(field: VectorField, surface: Sequence[Expr], - parameter_and_limits1: ParameterLimits, parameter_and_limits2: ParameterLimits) -> ScalarValue: +def circulation_along_surface_boundary( + field: VectorField, + surface: Sequence[Any], + parameter_and_limits1: ParameterLimits, + parameter_and_limits2: ParameterLimits, +) -> Expr: # circulation over surface is flux of curl of the field field_rotor_vector_field = curl_operator(field) return flux_across_surface(field_rotor_vector_field, surface, parameter_and_limits1, @@ -33,8 +39,11 @@ def circulation_along_surface_boundary(field: VectorField, surface: Sequence[Exp # trajectory should be array with projections to coordinates, eg [3 * cos(parameter), 3 * sin(parameter)] # trajectory and field should be 2-dimensional, on XY plane -def flux_across_curve(field: VectorField, trajectory: Sequence[Expr], - parameter_limits: ParameterLimits) -> ScalarValue: +def flux_across_curve( + field: VectorField, + trajectory: Sequence[Any], + parameter_limits: ParameterLimits, +) -> Expr: if len(trajectory) > 2: raise ValueError(f"Trajectory should have at most 2 components, got {len(trajectory)}") (parameter, parameter_from, parameter_to) = parameter_limits @@ -51,8 +60,12 @@ def flux_across_curve(field: VectorField, trajectory: Sequence[Expr], # trajectory should be array with projections to coordinates, eg [3 * cos(parameter), 3 * sin(parameter)] -def flux_across_surface(field: VectorField, surface: Sequence[Expr], - parameter_and_limits1: ParameterLimits, parameter_and_limits2: ParameterLimits) -> ScalarValue: +def flux_across_surface( + field: VectorField, + surface: Sequence[Any], + parameter_and_limits1: ParameterLimits, + parameter_and_limits2: ParameterLimits, +) -> Expr: (parameter1, parameter1_from, parameter1_to) = parameter_and_limits1 (parameter2, parameter2_from, parameter2_to) = parameter_and_limits2 # calculate SurfaceIntegral integrand, which is Dot(Field, dS) @@ -66,8 +79,12 @@ def flux_across_surface(field: VectorField, surface: Sequence[Expr], # flux across some curve, that is a surface boundary is double integral of divergence of the field -def flux_across_surface_boundary(field: VectorField, surface: Sequence[Expr], - parameter_and_limits1: ParameterLimits, parameter_and_limits2: ParameterLimits) -> ScalarValue: +def flux_across_surface_boundary( + field: VectorField, + surface: Sequence[Any], + parameter_and_limits1: ParameterLimits, + parameter_and_limits2: ParameterLimits, +) -> Expr: (parameter1, parameter1_from, parameter1_to) = parameter_and_limits1 (parameter2, parameter2_from, parameter2_to) = parameter_and_limits2 field_divergence = divergence_operator(field) @@ -83,9 +100,12 @@ def flux_across_surface_boundary(field: VectorField, surface: Sequence[Expr], # over volume. # Parametrized volumes are not supported. We define volume by the integral limits. # Integration starts from the last limit, ie z_limits -def flux_across_volume_boundary(field: VectorField, x_limits: tuple[ScalarValue, ScalarValue], - y_limits: tuple[ScalarValue, ScalarValue], z_limits: tuple[ScalarValue, - ScalarValue]) -> ScalarValue: +def flux_across_volume_boundary( + field: VectorField, + x_limits: tuple[Any, Any], + y_limits: tuple[Any, Any], + z_limits: tuple[Any, Any], +) -> Expr: (x_from, x_to) = x_limits (y_from, y_to) = y_limits (z_from, z_to) = z_limits diff --git a/symplyphysics/core/fields/operators.py b/symplyphysics/core/fields/operators.py index 3e4addcdc..9ff5bba18 100644 --- a/symplyphysics/core/fields/operators.py +++ b/symplyphysics/core/fields/operators.py @@ -1,6 +1,5 @@ -from sympy import diff, sin, tan +from sympy import diff, sin, tan, Expr, S -from ..dimensions import ScalarValue from ..fields.vector_field import VectorField from ..fields.scalar_field import ScalarField from ..coordinate_systems.coordinate_systems import CoordinateSystem @@ -42,10 +41,10 @@ def gradient_operator(field: ScalarField) -> Vector: raise ValueError(f"Unsupported coordinate system: {field.coordinate_system}") -def divergence_operator(field: VectorField) -> ScalarValue: +def divergence_operator(field: VectorField) -> Expr: field_space = field.apply_to_basis() # extend missing components with zeroes - field_components = list(field_space.components) + [0] * (3 - len(field_space.components)) + field_components = list(field_space.components) + [S.Zero] * (3 - len(field_space.components)) if field.coordinate_system.coord_system_type == CoordinateSystem.System.CARTESIAN: x = field_space.coordinate_system.coord_system.base_scalars()[0] y = field_space.coordinate_system.coord_system.base_scalars()[1] diff --git a/symplyphysics/core/fields/parameters.py b/symplyphysics/core/fields/parameters.py index d3b570f59..94f00f102 100644 --- a/symplyphysics/core/fields/parameters.py +++ b/symplyphysics/core/fields/parameters.py @@ -1,6 +1,5 @@ -from typing import TypeAlias +from typing import TypeAlias, Any from sympy import Expr -from ..dimensions import ScalarValue ParameterType: TypeAlias = Expr -ParameterLimits: TypeAlias = tuple[ParameterType, ScalarValue, ScalarValue] +ParameterLimits: TypeAlias = tuple[ParameterType, Any, Any] diff --git a/symplyphysics/core/fields/scalar_field.py b/symplyphysics/core/fields/scalar_field.py index fb018b464..582076b2e 100644 --- a/symplyphysics/core/fields/scalar_field.py +++ b/symplyphysics/core/fields/scalar_field.py @@ -1,6 +1,6 @@ from __future__ import annotations from functools import partial -from typing import Callable, Sequence, TypeAlias, TypeVar +from typing import Callable, Sequence, TypeAlias, TypeVar, Any from sympy import Expr, sympify from sympy.vector import express @@ -9,16 +9,14 @@ from ..points.sphere_point import SpherePoint from ..points.cylinder_point import CylinderPoint from ..coordinate_systems.coordinate_systems import CoordinateSystem -from ...core.dimensions import ScalarValue T = TypeVar("T", bound="Point") -FieldFunction: TypeAlias = Callable[[T], ScalarValue] | ScalarValue +FieldFunction: TypeAlias = Callable[[T], Expr] | Any -def _subs_with_point(expr: ScalarValue, coordinate_system: CoordinateSystem, - point_: Point) -> ScalarValue: +def _subs_with_point(expr: Any, coordinate_system: CoordinateSystem, point_: Point) -> Expr: base_scalars = coordinate_system.coord_system.base_scalars() - # convert ScalarValue to Expr + # convert Any to Expr expression = sympify(expr) for i, scalar in enumerate(base_scalars): expression = expression.subs(scalar, point_.coordinate(i)) @@ -43,27 +41,27 @@ def __init__( point_function: FieldFunction = 0, # type: ignore[type-arg] coordinate_system: CoordinateSystem = CoordinateSystem(CoordinateSystem.System.CARTESIAN) ) -> None: + if not callable(point_function): + point_function = sympify(point_function) + self._point_function = point_function self._coordinate_system = coordinate_system - def __call__(self, point_: Point) -> ScalarValue: + def __call__(self, point_: Point) -> Expr: if not callable(self._point_function): return self._point_function # Point with general Point type is not checked against coordinate system. # It's up to user to make sure that field function works with general Point type. - if isinstance( - point_, CartesianPoint - ) and self._coordinate_system.coord_system_type != CoordinateSystem.System.CARTESIAN: + if (isinstance(point_, CartesianPoint) and + self._coordinate_system.coord_system_type != CoordinateSystem.System.CARTESIAN): raise ValueError( f"Unsupported coordinate system for CartesianPoint: {self._coordinate_system}") - if isinstance( - point_, SpherePoint - ) and self._coordinate_system.coord_system_type != CoordinateSystem.System.SPHERICAL: + if (isinstance(point_, SpherePoint) and + self._coordinate_system.coord_system_type != CoordinateSystem.System.SPHERICAL): raise ValueError( f"Unsupported coordinate system for SpherePoint: {self._coordinate_system}") - if isinstance( - point_, CylinderPoint - ) and self._coordinate_system.coord_system_type != CoordinateSystem.System.CYLINDRICAL: + if (isinstance(point_, CylinderPoint) and + self._coordinate_system.coord_system_type != CoordinateSystem.System.CYLINDRICAL): raise ValueError( f"Unsupported coordinate system for CylinderPoint: {self._coordinate_system}") return self._point_function(point_) @@ -84,7 +82,7 @@ def field_function(self) -> FieldFunction: # type: ignore[type-arg] # Can contain value instead of SymPy Vector, eg 0.5, but it should be sympified. @staticmethod def from_expression( - expr: Expr | float, + expr: Any, coordinate_system: CoordinateSystem = CoordinateSystem(CoordinateSystem.System.CARTESIAN) ) -> ScalarField: point_function = partial(_subs_with_point, expr, coordinate_system) @@ -93,7 +91,7 @@ def from_expression( # Applies field to a trajectory / surface / volume - calls field function with each element of the trajectory as parameter. # trajectory_ - list of expressions that correspond to a function in some space, eg [param, param] for a linear function y = x # return - value that depends on trajectory parameters. - def apply(self, trajectory_: Sequence[Expr | float]) -> ScalarValue: + def apply(self, trajectory_: Sequence[Any]) -> Expr: trajectory_as_point = Point(*trajectory_) if self._coordinate_system.coord_system_type == CoordinateSystem.System.CARTESIAN: trajectory_as_point = CartesianPoint(*trajectory_) @@ -106,7 +104,7 @@ def apply(self, trajectory_: Sequence[Expr | float]) -> ScalarValue: # Convert coordinate system to space and apply field. # Applying field to entire space is necessary for SymPy field operators like Curl. # return - value that depends on basis parameters. - def apply_to_basis(self) -> ScalarValue: + def apply_to_basis(self) -> Expr: return self.apply(self.basis) # Converts ScalarField to SymPy expression diff --git a/symplyphysics/core/fields/vector_field.py b/symplyphysics/core/fields/vector_field.py index 6bc3bba39..e7d41f75b 100644 --- a/symplyphysics/core/fields/vector_field.py +++ b/symplyphysics/core/fields/vector_field.py @@ -1,6 +1,6 @@ from __future__ import annotations from functools import partial -from typing import Callable, Sequence, TypeAlias, TypeVar +from typing import Callable, Sequence, TypeAlias, TypeVar, Any from sympy import Expr, sympify from sympy.vector import Vector as SymVector @@ -10,14 +10,16 @@ from ..points.cylinder_point import CylinderPoint from ..coordinate_systems.coordinate_systems import CoordinateSystem from ..vectors.vectors import Vector -from ...core.dimensions import ScalarValue T = TypeVar("T", bound="Point") -FieldFunction: TypeAlias = Callable[[T], Sequence[ScalarValue]] | Sequence[ScalarValue] +FieldFunction: TypeAlias = Callable[[T], Sequence[Expr]] | Sequence[Any] -def _subs_with_point(expr: Sequence[ScalarValue], coordinate_system: CoordinateSystem, - point_: Point) -> Sequence[Expr]: +def _subs_with_point( + expr: Sequence[Any], + coordinate_system: CoordinateSystem, + point_: Point, +) -> Sequence[Expr]: base_scalars = coordinate_system.coord_system.base_scalars() result: list[Expr] = [] for e in expr: @@ -43,6 +45,9 @@ def __init__( point_function: FieldFunction, # type: ignore[type-arg] coordinate_system: CoordinateSystem = CoordinateSystem(CoordinateSystem.System.CARTESIAN) ) -> None: + if not callable(point_function): + point_function = list(map(sympify, point_function)) + self._point_function = point_function self._coordinate_system = coordinate_system @@ -90,15 +95,17 @@ def from_vector(vector_: Vector) -> VectorField: # Constructs new VectorField from SymPy expression. # Can contain value instead of SymPy Vector, eg 0.5, but it should be sympified. @staticmethod - def from_sympy_vector(sympy_vector_: SymVector, - coordinate_system: CoordinateSystem) -> VectorField: + def from_sympy_vector( + sympy_vector_: SymVector, + coordinate_system: CoordinateSystem, + ) -> VectorField: field_vector = Vector.from_sympy_vector(sympy_vector_, coordinate_system) return VectorField.from_vector(field_vector) # Applies field to a trajectory / surface / volume - calls field functions with each element of the trajectory as parameter. # trajectory_ - list of expressions that correspond to a function in some space, eg [param, param] for a linear function y = x # return - vector parametrized by trajectory parameters. - def apply(self, trajectory_: Sequence[Expr | float]) -> Vector: + def apply(self, trajectory_: Sequence[Any]) -> Vector: trajectory_as_point = Point(*trajectory_) if self._coordinate_system.coord_system_type == CoordinateSystem.System.CARTESIAN: trajectory_as_point = CartesianPoint(*trajectory_) diff --git a/symplyphysics/core/geometry/elements.py b/symplyphysics/core/geometry/elements.py index e115af848..475a34866 100644 --- a/symplyphysics/core/geometry/elements.py +++ b/symplyphysics/core/geometry/elements.py @@ -1,6 +1,5 @@ -from sympy import Expr, diff, sin +from sympy import Expr, diff, sin, S from symplyphysics import CoordinateSystem, vector_magnitude -from ..dimensions import ScalarValue from ..vectors.vectors import Vector @@ -11,14 +10,14 @@ def parametrized_curve_element(trajectory: Vector, parameter: Expr) -> Vector: return Vector.from_sympy_vector(trajectory_element_sympy_vector, trajectory.coordinate_system) -def parametrized_curve_element_magnitude(trajectory: Vector, parameter: Expr) -> ScalarValue: +def parametrized_curve_element_magnitude(trajectory: Vector, parameter: Expr) -> Expr: return vector_magnitude(parametrized_curve_element(trajectory, parameter)) # For non parametrized volumes -def volume_element_magnitude(coordinate_system: CoordinateSystem) -> ScalarValue: +def volume_element_magnitude(coordinate_system: CoordinateSystem) -> Expr: if coordinate_system.coord_system_type == CoordinateSystem.System.CARTESIAN: - return 1 + return S.One if coordinate_system.coord_system_type == CoordinateSystem.System.CYLINDRICAL: r = coordinate_system.coord_system.base_scalars()[0] return r diff --git a/symplyphysics/core/operations/symbolic.py b/symplyphysics/core/operations/symbolic.py index 7bf078e9d..c5e913044 100644 --- a/symplyphysics/core/operations/symbolic.py +++ b/symplyphysics/core/operations/symbolic.py @@ -108,7 +108,7 @@ class ExactDifferential(Symbolic): # pylint: disable=too-many-ancestors #. `Wikipedia `__. """ - + class InexactDifferential(Symbolic): # pylint: disable=too-many-ancestors """ A differential is said to be **inexact** (or **imperfect**) when its integral is diff --git a/symplyphysics/core/points/cartesian_point.py b/symplyphysics/core/points/cartesian_point.py index 5485ec21c..6c8012270 100644 --- a/symplyphysics/core/points/cartesian_point.py +++ b/symplyphysics/core/points/cartesian_point.py @@ -1,31 +1,33 @@ -from .point import Coordinate, Point +from typing import Any +from sympy import Expr +from .point import Point # This class represents point in 3d cartesian space (rectangular coordinates). class CartesianPoint(Point): # Length of a rectangle @property - def x(self) -> Coordinate: + def x(self) -> Expr: return self.coordinate(0) @x.setter - def x(self, value_: Coordinate) -> None: + def x(self, value_: Any) -> None: self.set_coordinate(0, value_) # Width of a rectangle @property - def y(self) -> Coordinate: + def y(self) -> Expr: return self.coordinate(1) @y.setter - def y(self, value_: Coordinate) -> None: + def y(self, value_: Any) -> None: self.set_coordinate(1, value_) # Height of a rectangle @property - def z(self) -> Coordinate: + def z(self) -> Expr: return self.coordinate(2) @z.setter - def z(self, value_: Coordinate) -> None: + def z(self, value_: Any) -> None: self.set_coordinate(2, value_) diff --git a/symplyphysics/core/points/cylinder_point.py b/symplyphysics/core/points/cylinder_point.py index 7e0608f3b..0db480c3d 100644 --- a/symplyphysics/core/points/cylinder_point.py +++ b/symplyphysics/core/points/cylinder_point.py @@ -1,53 +1,55 @@ -from .point import Coordinate, Point +from typing import Any +from sympy import Expr +from .point import Point # This class represents point in 3d cylindrical space. class CylinderPoint(Point): @property - def radius(self) -> Coordinate: + def radius(self) -> Expr: return self.coordinate(0) @radius.setter - def radius(self, value_: Coordinate) -> None: + def radius(self, value_: Any) -> None: self.set_coordinate(0, value_) @property - def azimuthal_angle(self) -> Coordinate: + def azimuthal_angle(self) -> Expr: return self.coordinate(1) @azimuthal_angle.setter - def azimuthal_angle(self, value_: Coordinate) -> None: + def azimuthal_angle(self, value_: Any) -> None: self.set_coordinate(1, value_) @property - def height(self) -> Coordinate: + def height(self) -> Expr: return self.coordinate(2) @height.setter - def height(self, value_: Coordinate) -> None: + def height(self, value_: Any) -> None: self.set_coordinate(2, value_) @property - def r(self) -> Coordinate: + def r(self) -> Expr: return self.radius @r.setter - def r(self, value_: Coordinate) -> None: + def r(self, value_: Any) -> None: self.radius = value_ @property - def theta(self) -> Coordinate: + def theta(self) -> Expr: return self.azimuthal_angle @theta.setter - def theta(self, value_: Coordinate) -> None: + def theta(self, value_: Any) -> None: self.azimuthal_angle = value_ @property - def z(self) -> Coordinate: + def z(self) -> Expr: return self.height @z.setter - def z(self, value_: Coordinate) -> None: + def z(self, value_: Any) -> None: self.height = value_ diff --git a/symplyphysics/core/points/point.py b/symplyphysics/core/points/point.py index 64b3ff4e5..926a0a89a 100644 --- a/symplyphysics/core/points/point.py +++ b/symplyphysics/core/points/point.py @@ -1,7 +1,5 @@ -from typing import Iterable, TypeAlias -from sympy import Expr - -Coordinate: TypeAlias = Expr | float +from typing import Iterable, Any +from sympy import Expr, sympify, S # This class not only represents point in space, but any trajectory in any-dimensional space. @@ -9,23 +7,23 @@ # represents a plane in 3D-space, where C is SympPy CoordSys3D. class Point: # may contain not number but sympy expression, eg C.x - _coordinates: list[Coordinate] = [] + _coordinates: list[Expr] = [] - def __init__(self, *coordinates: Coordinate) -> None: - self._coordinates = list(coordinates) + def __init__(self, *coordinates: Any) -> None: + self._coordinates = list(map(sympify, coordinates)) @property - def coordinates(self) -> Iterable[Coordinate]: + def coordinates(self) -> Iterable[Expr]: return iter(self._coordinates) - def coordinate(self, index: int) -> Coordinate: + def coordinate(self, index: int) -> Expr: if len(self._coordinates) <= index: return 0 return self._coordinates[index] - def set_coordinate(self, index: int, value: Coordinate) -> None: + def set_coordinate(self, index: int, value: Any) -> None: if value is None: - value = 0 + value = S.Zero if len(self._coordinates) <= index: - self._coordinates.extend([0] * (index + 1 - len(self._coordinates))) - self._coordinates[index] = value + self._coordinates.extend([S.Zero] * (index + 1 - len(self._coordinates))) + self._coordinates[index] = sympify(value) diff --git a/symplyphysics/core/points/sphere_point.py b/symplyphysics/core/points/sphere_point.py index ee1f84caf..898b2d370 100644 --- a/symplyphysics/core/points/sphere_point.py +++ b/symplyphysics/core/points/sphere_point.py @@ -1,53 +1,55 @@ -from .point import Coordinate, Point +from typing import Any +from sympy import Expr +from .point import Point # This class represents point in 3d spherical space. class SpherePoint(Point): @property - def radius(self) -> Coordinate: + def radius(self) -> Expr: return self.coordinate(0) @radius.setter - def radius(self, value_: Coordinate) -> None: + def radius(self, value_: Any) -> None: self.set_coordinate(0, value_) @property - def azimuthal_angle(self) -> Coordinate: + def azimuthal_angle(self) -> Expr: return self.coordinate(1) @azimuthal_angle.setter - def azimuthal_angle(self, value_: Coordinate) -> None: + def azimuthal_angle(self, value_: Any) -> None: self.set_coordinate(1, value_) @property - def polar_angle(self) -> Coordinate: + def polar_angle(self) -> Expr: return self.coordinate(2) @polar_angle.setter - def polar_angle(self, value_: Coordinate) -> None: + def polar_angle(self, value_: Any) -> None: self.set_coordinate(2, value_) @property - def r(self) -> Coordinate: + def r(self) -> Expr: return self.radius @r.setter - def r(self, value_: Coordinate) -> None: + def r(self, value_: Any) -> None: self.radius = value_ @property - def theta(self) -> Coordinate: + def theta(self) -> Expr: return self.azimuthal_angle @theta.setter - def theta(self, value_: Coordinate) -> None: + def theta(self, value_: Any) -> None: self.azimuthal_angle = value_ @property - def phi(self) -> Coordinate: + def phi(self) -> Expr: return self.polar_angle @phi.setter - def phi(self, value_: Coordinate) -> None: + def phi(self, value_: Any) -> None: self.polar_angle = value_ diff --git a/symplyphysics/core/quantity_decorator.py b/symplyphysics/core/quantity_decorator.py index 019d05ec9..5f6e8febf 100644 --- a/symplyphysics/core/quantity_decorator.py +++ b/symplyphysics/core/quantity_decorator.py @@ -1,13 +1,13 @@ import functools import inspect -from typing import Any, Callable, Sequence, TypeAlias +from typing import Any, Callable, Sequence, TypeAlias, SupportsFloat from sympy.physics.units import Quantity as SymQuantity, Dimension from .symbols.symbols import DimensionSymbol, Function, Symbol, IndexedSymbol from .operations.symbolic import Symbolic -from .dimensions import assert_equivalent_dimension, ScalarValue +from .dimensions import assert_equivalent_dimension -_ValueType: TypeAlias = ScalarValue | SymQuantity | DimensionSymbol | Symbolic +_ValueType: TypeAlias = SupportsFloat | DimensionSymbol | Symbolic _UnitType: TypeAlias = Dimension | Symbol | Function | IndexedSymbol | Symbolic @@ -18,7 +18,7 @@ def _assert_expected_unit( param_name: str, function_name: str, ) -> None: - components: list[ScalarValue | SymQuantity | Dimension] = [] + components: list[SupportsFloat | Dimension] = [] indexed = isinstance(value, Sequence) values = list(value) if isinstance(value, Sequence) else list([value]) for item in values: diff --git a/symplyphysics/core/symbols/quantities.py b/symplyphysics/core/symbols/quantities.py index c49fd2c89..b5ba3d990 100644 --- a/symplyphysics/core/symbols/quantities.py +++ b/symplyphysics/core/symbols/quantities.py @@ -1,21 +1,23 @@ from __future__ import annotations from functools import partial -from typing import Any, Optional, Sequence +from typing import Any, Optional, Sequence, SupportsFloat from sympy import S, Expr, sympify, Abs from sympy.physics.units import Dimension, Quantity as SymQuantity from sympy.physics.units.systems.si import SI from sympy.multipledispatch import dispatch +from sympy.printing.printer import Printer from .symbols import DimensionSymbol, next_name -from ..dimensions import collect_quantity_factor_and_dimension +from ..dimensions.collect_quantity import collect_quantity_factor_and_dimension +from ..dimensions.dimensions import dimension_to_si_unit # to avoid cyclic import class Quantity(DimensionSymbol, SymQuantity): # type: ignore[misc] # pylint: disable=too-many-ancestors # pylint: disable-next=signature-differs def __new__(cls, - _expr: Expr | float = S.One, + _expr: SupportsFloat = S.One, *, display_symbol: Optional[str] = None, display_latex: Optional[str] = None, @@ -30,14 +32,15 @@ def __new__(cls, return obj # type: ignore[no-any-return] def __init__(self, - expr: Expr | float = S.One, + expr: SupportsFloat = S.One, *, display_symbol: Optional[str] = None, display_latex: Optional[str] = None, dimension: Optional[Dimension] = None) -> None: (scale, dimension_) = collect_quantity_factor_and_dimension(expr) try: - _ = complex(scale) # if this fails, then ``scale`` contains a symbolic sub-expression + # if this fails (but it shouldn't), then ``scale`` contains a symbolic sub-expression + _ = complex(scale) except Exception as e: raise ValueError( f"Argument '{expr}' to function 'Quantity()' should " @@ -67,6 +70,27 @@ def _eval_is_positive(self) -> bool: def _eval_Abs(self) -> Quantity: return self.__class__(Abs(self.scale_factor), dimension=self.dimension) + def _sympystr(self, p: Printer) -> str: + if "QTY" not in self.display_name: + return self.display_name + + si_unit = dimension_to_si_unit(self.dimension) + + si_value = self.convert_to(si_unit) / si_unit + + qty: SymQuantity + + for qty in si_value.atoms(SymQuantity): + si_value = si_value.subs(qty, 1) + si_value = si_value.n(3) + + for qty in si_unit.atoms(SymQuantity): + abbrev = qty.abbrev + if abbrev: + si_unit = si_unit.subs(qty, abbrev) + + return str(p.doprint(si_value * si_unit)) + # Allows for some SymPy comparisons, eg Piecewise function @dispatch(Quantity, Quantity) # type: ignore[misc] @@ -74,12 +98,20 @@ def _eval_is_ge(lhs: Quantity, rhs: Quantity) -> bool: return scale_factor(lhs) >= scale_factor(rhs) -def subs_list(input_: Sequence[Expr | float], subs_: dict[Expr, Quantity]) -> Sequence[Quantity]: +def subs_list( + input_: Sequence[SupportsFloat], + subs_: dict[Expr, SymQuantity], +) -> Sequence[Quantity]: return [Quantity(sympify(c).subs(subs_)) for c in input_] -def scale_factor(quantity_: Quantity | float) -> float: - if isinstance(quantity_, Quantity): +def scale_factor(quantity_: SupportsFloat) -> float: + """ + Extracts the scale factor converted to `float` from the input if it is a quantity. Otherwise + simply calls `float` on the input. + """ + + if isinstance(quantity_, SymQuantity): return float(quantity_.scale_factor) - return quantity_ + return float(quantity_) diff --git a/symplyphysics/core/symbols/symbols.py b/symplyphysics/core/symbols/symbols.py index b3a5e7a74..9e5333769 100644 --- a/symplyphysics/core/symbols/symbols.py +++ b/symplyphysics/core/symbols/symbols.py @@ -125,7 +125,7 @@ def __init__(cls, cls.arguments = arguments super().__init__(display_name, dimension, display_latex=display_latex) - def __repr__(cls) -> str: + def __repr__(cls) -> str: # pylint: disable=invalid-repr-returned return cls.display_name diff --git a/symplyphysics/core/vectors/arithmetics.py b/symplyphysics/core/vectors/arithmetics.py index 4b02f8887..878bd2a88 100644 --- a/symplyphysics/core/vectors/arithmetics.py +++ b/symplyphysics/core/vectors/arithmetics.py @@ -1,27 +1,27 @@ from functools import reduce from operator import add -from typing import Optional, Sequence +from typing import Optional, Sequence, Any from sympy import S, Expr, cos, sin, sqrt, sympify, diff, integrate from .vectors import Vector from ..expr_comparisons import expr_equals from ..coordinate_systems.coordinate_systems import CoordinateSystem -from ..dimensions import ScalarValue # Add zeroes so that both vectors have the same length. # Use 'max_size' to increase or trim vector size. Vectors are aligned to the # larger (more dimensions) vector size if not set. def _extend_two_vectors( - vector_left: Vector, - vector_right: Vector, - max_size: Optional[int] = None) -> tuple[Sequence[ScalarValue], Sequence[ScalarValue]]: + vector_left: Vector, + vector_right: Vector, + max_size: Optional[int] = None, +) -> tuple[Sequence[Expr], Sequence[Expr]]: max_size = max(len(vector_left.components), len( vector_right.components)) if max_size is None else max_size list_left_extended = list( - vector_left.components) + [0] * (max_size - len(vector_left.components)) + vector_left.components) + [S.Zero] * (max_size - len(vector_left.components)) list_right_extended = list( - vector_right.components) + [0] * (max_size - len(vector_right.components)) + vector_right.components) + [S.Zero] * (max_size - len(vector_right.components)) return (list_left_extended, list_right_extended) @@ -88,7 +88,7 @@ def subtract_cartesian_vectors(*vectors: Vector) -> Vector: # Change Vector magnitude (length) # Scalar multiplication changes the magnitude of the vector and does not change it's direction. -def scale_vector(scalar_value: ScalarValue, vector: Vector) -> Vector: +def scale_vector(scalar_value: Any, vector: Vector) -> Vector: if vector.coordinate_system.coord_system_type == CoordinateSystem.System.CARTESIAN: vector_components = [scalar_value * e for e in vector.components] return Vector(vector_components, vector.coordinate_system) @@ -110,8 +110,10 @@ def scale_vector(scalar_value: ScalarValue, vector: Vector) -> Vector: # Multiply elements of two lists respectively and sum the results -def _multiply_lists_and_sum(list_left: Sequence[ScalarValue], - list_right: Sequence[ScalarValue]) -> Expr: +def _multiply_lists_and_sum( + list_left: Sequence[Any], + list_right: Sequence[Any], +) -> Expr: return sympify(reduce(add, map(lambda lr: lr[0] * lr[1], zip(list_left, list_right)), 0)) @@ -232,7 +234,7 @@ def diff_cartesian_vector( def integrate_cartesian_vector( vector_: Vector, - *args: Expr | tuple[Expr, Expr, Expr], + *args: Expr | tuple[Expr, Any, Any], ) -> Vector: if vector_.coordinate_system.coord_system_type != CoordinateSystem.System.CARTESIAN: raise ValueError( diff --git a/symplyphysics/core/vectors/vectors.py b/symplyphysics/core/vectors/vectors.py index 7a7884f6a..70bba2938 100644 --- a/symplyphysics/core/vectors/vectors.py +++ b/symplyphysics/core/vectors/vectors.py @@ -6,7 +6,7 @@ from sympy.physics.units import Dimension from sympy.physics.units.definitions.dimension_definitions import angle as angle_type -from ..dimensions import assert_equivalent_dimension, dimensionless, ScalarValue +from ..dimensions import assert_equivalent_dimension, dimensionless from ..symbols.quantities import Quantity, subs_list from ..symbols.id_generator import next_id from ..symbols.symbols import DimensionSymbol @@ -26,22 +26,22 @@ class Vector: #NOTE: 4 and higher dimensional vectors are not supported cause of using CoordSys3D # to allow rebasing vector coordinate system. _coordinate_system: CoordinateSystem - _components: list[ScalarValue] + _components: list[Expr] def __init__( self, - components: Sequence[ScalarValue], + components: Sequence[Any], coordinate_system: CoordinateSystem = CoordinateSystem(CoordinateSystem.System.CARTESIAN) ) -> None: self._coordinate_system = coordinate_system - self._components = list(components) + self._components = list(map(sympify, components)) @property def coordinate_system(self) -> CoordinateSystem: return self._coordinate_system @property - def components(self) -> Sequence[ScalarValue]: + def components(self) -> Sequence[Expr]: return self._components # Converts SymPy Vector to Vector @@ -111,7 +111,7 @@ class QuantityVector(DimensionSymbol): _inner_vector: Vector def __init__(self, - components: Sequence[Quantity | ScalarValue], + components: Sequence[Quantity | Any], coordinate_system: CoordinateSystem = CoordinateSystem(CoordinateSystem.System.CARTESIAN), *, dimension: Optional[Dimension] = None) -> None: diff --git a/symplyphysics/docs/symbols_role.py b/symplyphysics/docs/symbols_role.py index 5d4b830f8..c8d406594 100644 --- a/symplyphysics/docs/symbols_role.py +++ b/symplyphysics/docs/symbols_role.py @@ -23,7 +23,7 @@ if isinstance(_sub_obj, Symbol): _symbols_by_module[_attr].add(_sub_attr) if _unincluded: - _s_unincluded = ", ".join(_unincluded) + _s_unincluded = ", ".join(_unincluded) # pylint: disable=invalid-name raise ValueError(f"Include {_s_unincluded} in .__all__.") @@ -35,12 +35,11 @@ def process_string(doc: str, path: Path) -> str: index_from, index_to = match.span() name = match.group(1) - found = False + directory = "" for directory, collection in _symbols_by_module.items(): if name in collection: - found = True break - if not found: + if not directory: raise ValueError(f"Unknown symbol '{name}' in '{path}'.") part_before = doc[last_index_to:index_from] diff --git a/symplyphysics/laws/dynamics/vector/instantaneous_power_is_force_dot_velocity.py b/symplyphysics/laws/dynamics/vector/instantaneous_power_is_force_dot_velocity.py index ac0f9b840..c31a44f4a 100644 --- a/symplyphysics/laws/dynamics/vector/instantaneous_power_is_force_dot_velocity.py +++ b/symplyphysics/laws/dynamics/vector/instantaneous_power_is_force_dot_velocity.py @@ -1,7 +1,7 @@ +from sympy import Expr from symplyphysics import units, Quantity, Vector, validate_input, validate_output from symplyphysics.core.vectors.arithmetics import dot_vectors from symplyphysics.core.vectors.vectors import QuantityVector -from symplyphysics.core.dimensions import ScalarValue # Description ## The power due to a force is the rate at which that force does work on an object. @@ -13,7 +13,7 @@ ## dot(a, b) is the dot product between vectors a and b -def power_law(force_: Vector, velocity_: Vector) -> ScalarValue: +def power_law(force_: Vector, velocity_: Vector) -> Expr: return dot_vectors(force_, velocity_) diff --git a/symplyphysics/laws/fields/circulation_is_integral_along_curve.py b/symplyphysics/laws/fields/circulation_is_integral_along_curve.py index f11cdf2d9..a9996ad73 100644 --- a/symplyphysics/laws/fields/circulation_is_integral_along_curve.py +++ b/symplyphysics/laws/fields/circulation_is_integral_along_curve.py @@ -1,7 +1,6 @@ -from typing import Sequence -from sympy import (Expr, Symbol as SymSymbol) +from typing import Sequence, Any +from sympy import Expr, Symbol as SymSymbol from symplyphysics import Quantity -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.fields.analysis import circulation_along_curve from symplyphysics.core.fields.vector_field import VectorField @@ -36,14 +35,21 @@ parameter = SymSymbol("parameter") -def circulation_law(field: VectorField, trajectory: Sequence[Expr], parameter_from: ScalarValue, - parameter_to: ScalarValue) -> ScalarValue: +def circulation_law( + field: VectorField, + trajectory: Sequence[Any], + parameter_from: Any, + parameter_to: Any, +) -> Expr: return circulation_along_curve(field, trajectory, (parameter, parameter_from, parameter_to)) # trajectory should be array with projections to coordinates, eg [3 * cos(parameter), 3 * sin(parameter)] -def calculate_circulation(field: VectorField, trajectory: Sequence[Expr], - parameter_limits: tuple[ScalarValue, ScalarValue]) -> Quantity: +def calculate_circulation( + field: VectorField, + trajectory: Sequence[Expr], + parameter_limits: tuple[Any, Any], +) -> Quantity: (parameter_from, parameter_to) = parameter_limits result_expr = circulation_law(field, trajectory, parameter_from, parameter_to) return Quantity(result_expr) diff --git a/symplyphysics/laws/fields/circulation_is_integral_of_curl_over_surface.py b/symplyphysics/laws/fields/circulation_is_integral_of_curl_over_surface.py index 39cd2c365..ad675af15 100644 --- a/symplyphysics/laws/fields/circulation_is_integral_of_curl_over_surface.py +++ b/symplyphysics/laws/fields/circulation_is_integral_of_curl_over_surface.py @@ -1,7 +1,6 @@ -from typing import Sequence +from typing import Sequence, Any from sympy import (Expr, Symbol as SymSymbol) from symplyphysics import Quantity -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.fields.analysis import circulation_along_surface_boundary from symplyphysics.core.fields.vector_field import VectorField @@ -41,17 +40,23 @@ parameter2 = SymSymbol("parameter2") -def circulation_law(field: VectorField, trajectory: Sequence[Expr], - parameter1_limits: tuple[ScalarValue, ScalarValue], parameter2_limits: tuple[ScalarValue, - ScalarValue]) -> ScalarValue: +def circulation_law( + field: VectorField, + trajectory: Sequence[Any], + parameter1_limits: tuple[Any, Any], + parameter2_limits: tuple[Any, Any], +) -> Expr: return circulation_along_surface_boundary(field, trajectory, (parameter1, parameter1_limits[0], parameter1_limits[1]), (parameter2, parameter2_limits[0], parameter2_limits[1])) # surface should be array with projections to coordinates, eg [parameter1 * cos(parameter2), parameter1 * sin(parameter2)] -def calculate_circulation(field: VectorField, surface: Sequence[Expr], - parameter1_limits: tuple[ScalarValue, ScalarValue], parameter2_limits: tuple[ScalarValue, - ScalarValue]) -> Quantity: +def calculate_circulation( + field: VectorField, + surface: Sequence[Any], + parameter1_limits: tuple[Any, Any], + parameter2_limits: tuple[Any, Any], +) -> Quantity: result_expr = circulation_law(field, surface, parameter1_limits, parameter2_limits) return Quantity(result_expr) diff --git a/symplyphysics/laws/fields/flux_is_integral_across_curve.py b/symplyphysics/laws/fields/flux_is_integral_across_curve.py index b0e52b9c0..5402239bd 100644 --- a/symplyphysics/laws/fields/flux_is_integral_across_curve.py +++ b/symplyphysics/laws/fields/flux_is_integral_across_curve.py @@ -1,7 +1,6 @@ -from typing import Sequence +from typing import Sequence, Any from sympy import (Expr, Symbol as SymSymbol) from symplyphysics import Quantity -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.fields.analysis import flux_across_curve from symplyphysics.core.fields.vector_field import VectorField @@ -36,15 +35,22 @@ parameter = SymSymbol("parameter") -def flux_law(field: VectorField, trajectory: Sequence[Expr], parameter_from: ScalarValue, - parameter_to: ScalarValue) -> ScalarValue: +def flux_law( + field: VectorField, + trajectory: Sequence[Any], + parameter_from: Any, + parameter_to: Any, +) -> Expr: return flux_across_curve(field, trajectory, (parameter, parameter_from, parameter_to)) # trajectory should be array with projections to coordinates, eg [3 * cos(parameter), 3 * sin(parameter)] # trajectory and field should be 2-dimensional, on XY plane -def calculate_flux(field: VectorField, trajectory: Sequence[Expr], - parameter_limits: tuple[ScalarValue, ScalarValue]) -> Quantity: +def calculate_flux( + field: VectorField, + trajectory: Sequence[Expr], + parameter_limits: tuple[Any, Any], +) -> Quantity: (parameter_from, parameter_to) = parameter_limits result_expr = flux_law(field, trajectory, parameter_from, parameter_to) return Quantity(result_expr) diff --git a/symplyphysics/laws/fields/flux_is_integral_across_surface.py b/symplyphysics/laws/fields/flux_is_integral_across_surface.py index 8ef478d83..b9d3aa9a0 100644 --- a/symplyphysics/laws/fields/flux_is_integral_across_surface.py +++ b/symplyphysics/laws/fields/flux_is_integral_across_surface.py @@ -1,7 +1,6 @@ -from typing import Sequence +from typing import Sequence, Any from sympy import (Expr, Symbol as SymSymbol) from symplyphysics import Quantity -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.fields.analysis import flux_across_surface from symplyphysics.core.fields.vector_field import VectorField @@ -37,16 +36,23 @@ parameter2 = SymSymbol("parameter2") -def flux_law(field: VectorField, trajectory: Sequence[Expr], parameter1_limits: tuple[ScalarValue, - ScalarValue], parameter2_limits: tuple[ScalarValue, ScalarValue]) -> ScalarValue: +def flux_law( + field: VectorField, + trajectory: Sequence[Any], + parameter1_limits: tuple[Any, Any], + parameter2_limits: tuple[Any, Any], +) -> Expr: return flux_across_surface(field, trajectory, (parameter1, parameter1_limits[0], parameter1_limits[1]), (parameter2, parameter2_limits[0], parameter2_limits[1])) # surface should be array with projections to coordinates, eg [3 * cos(parameter), 3 * sin(parameter)] -def calculate_flux(field: VectorField, surface: Sequence[Expr], - parameter1_limits: tuple[ScalarValue, ScalarValue], parameter2_limits: tuple[ScalarValue, - ScalarValue]) -> Quantity: +def calculate_flux( + field: VectorField, + surface: Sequence[Any], + parameter1_limits: tuple[Any, Any], + parameter2_limits: tuple[Any, Any], +) -> Quantity: result_expr = flux_law(field, surface, parameter1_limits, parameter2_limits) return Quantity(result_expr) diff --git a/symplyphysics/laws/geometry/vector/dot_product_is_proportional_to_cosine_between_vectors.py b/symplyphysics/laws/geometry/vector/dot_product_is_proportional_to_cosine_between_vectors.py index d438eb7c0..03f63f710 100644 --- a/symplyphysics/laws/geometry/vector/dot_product_is_proportional_to_cosine_between_vectors.py +++ b/symplyphysics/laws/geometry/vector/dot_product_is_proportional_to_cosine_between_vectors.py @@ -1,3 +1,4 @@ +from sympy import Expr from symplyphysics import ( Quantity, QuantityVector, @@ -5,7 +6,6 @@ dot_vectors, vector_magnitude, ) -from symplyphysics.core.dimensions import ScalarValue # Description: ## Dot product is scalar binary operation defined as the product of the norm of the vectors @@ -18,7 +18,7 @@ ## norm(v) - norm, or length, of vector v -def cosine_between_vectors_law(vector_left_: Vector, vector_right_: Vector) -> ScalarValue: +def cosine_between_vectors_law(vector_left_: Vector, vector_right_: Vector) -> Expr: dot_product_ = dot_vectors(vector_left_, vector_right_) vector_left_norm_ = vector_magnitude(vector_left_) vector_right_norm_ = vector_magnitude(vector_right_) diff --git a/symplyphysics/laws/gravity/gravity_force_from_mass_and_distance.py b/symplyphysics/laws/gravity/gravity_force_from_mass_and_distance.py index bbbc11159..bd628be9f 100644 --- a/symplyphysics/laws/gravity/gravity_force_from_mass_and_distance.py +++ b/symplyphysics/laws/gravity/gravity_force_from_mass_and_distance.py @@ -15,7 +15,7 @@ #. `Physics LibreTexts. Newton's Law of Universal Gravitation (13.2.1) `__. """ -from sympy import Eq, solve, sqrt +from sympy import Eq, solve, sqrt, Expr from symplyphysics import ( Quantity, validate_input, @@ -25,7 +25,6 @@ symbols, quantities, ) -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.points.cartesian_point import CartesianPoint from symplyphysics.core.expr_comparisons import expr_equals from symplyphysics.core.fields.scalar_field import ScalarField @@ -72,7 +71,7 @@ }) -def potential_field_function(point: CartesianPoint) -> ScalarValue: +def potential_field_function(point: CartesianPoint) -> Expr: return _potential.subs( distance_between_mass_centers, sqrt(point.x**2 + point.y**2 + point.z**2), diff --git a/symplyphysics/laws/kinematics/vector/center_of_mass_for_system_of_particles.py b/symplyphysics/laws/kinematics/vector/center_of_mass_for_system_of_particles.py index 70f9c1836..9714c108a 100644 --- a/symplyphysics/laws/kinematics/vector/center_of_mass_for_system_of_particles.py +++ b/symplyphysics/laws/kinematics/vector/center_of_mass_for_system_of_particles.py @@ -11,7 +11,7 @@ """ from typing import Sequence -from sympy import S +from sympy import S, Expr from symplyphysics import ( units, validate_input, @@ -22,11 +22,10 @@ add_cartesian_vectors, scale_vector, ) -from symplyphysics.core.dimensions import ScalarValue def center_of_mass_law( - masses_: Sequence[ScalarValue], + masses_: Sequence[Expr], position_vectors_: Sequence[Vector], ) -> Vector: r""" diff --git a/test/core/dimensions/collect_dimension_test.py b/test/core/dimensions/collect_dimension_test.py index a12cc69f1..9594d8fdb 100644 --- a/test/core/dimensions/collect_dimension_test.py +++ b/test/core/dimensions/collect_dimension_test.py @@ -49,14 +49,14 @@ def test_pow() -> None: assert dimsys_SI.equivalent_dims(dim, units.current**n) expr = symbols.current**(2 * n) - collect_expression_and_dimension(expr)[1] + _ = collect_expression_and_dimension(expr)[1] expr = (symbols.acceleration * symbols.mass)**(2 * symbols.time * symbols.temporal_frequency) - collect_expression_and_dimension(expr)[1] + _ = collect_expression_and_dimension(expr)[1] bad_expr = symbols.mass**symbols.amount_of_substance with raises(ValueError): - collect_expression_and_dimension(bad_expr) + _ = collect_expression_and_dimension(bad_expr) def test_add() -> None: @@ -70,7 +70,7 @@ def test_add() -> None: expr = first_mass + second_mass + symbols.time with raises(ValueError): - collect_expression_and_dimension(expr)[1] + _ = collect_expression_and_dimension(expr)[1] def test_abs() -> None: @@ -85,7 +85,7 @@ def test_abs() -> None: # the error propagates from the argument expr = abs(symbols.time + symbols.length) with raises(ValueError): - collect_expression_and_dimension(expr)[1] + _ = collect_expression_and_dimension(expr)[1] def test_min() -> None: @@ -99,7 +99,7 @@ def test_min() -> None: expr = Min(first_mass, second_mass, symbols.time) with raises(ValueError): - collect_expression_and_dimension(expr)[1] + _ = collect_expression_and_dimension(expr)[1] def test_derivative() -> None: diff --git a/test/core/experimental/vectors/test_vector.py b/test/core/experimental/vectors/test_vector.py index 45c19f86e..01f1e9d82 100644 --- a/test/core/experimental/vectors/test_vector.py +++ b/test/core/experimental/vectors/test_vector.py @@ -35,6 +35,7 @@ def test_init() -> None: VectorSymbol(name, dim, norm=symbols.force, display_latex=latex) one_newton_force = VectorSymbol(name, dim, norm=1 * units.newton, display_latex=latex) + assert one_newton_force.norm is not None # to satisfy mypy assert_equal(one_newton_force.norm, 1 * units.newton) assert not one_newton_force.is_zero diff --git a/test/core/fields/analysis_test.py b/test/core/fields/analysis_test.py index 66ba85324..412d9f395 100644 --- a/test/core/fields/analysis_test.py +++ b/test/core/fields/analysis_test.py @@ -1,9 +1,8 @@ from collections import namedtuple from typing import Sequence from pytest import fixture, mark, raises -from sympy import Expr, cos, pi, sin, sqrt, Symbol as SymSymbol, sympify +from sympy import Expr, cos, pi, sin, sqrt, Symbol as SymSymbol, sympify, S from symplyphysics.core.coordinate_systems.coordinate_systems import CoordinateSystem -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.fields.analysis import circulation_along_curve, circulation_along_surface_boundary, flux_across_curve, flux_across_surface, flux_across_surface_boundary, flux_across_volume_boundary from symplyphysics.core.fields.vector_field import VectorField from symplyphysics.core.points.cartesian_point import CartesianPoint @@ -227,8 +226,8 @@ def test_basic_flux_across_volume_boundary(test_args: Args) -> None: def test_basic_flux_across_sphere_boundary() -> None: B = CoordinateSystem(CoordinateSystem.System.CYLINDRICAL) - def field_function(p: CylinderPoint) -> Sequence[ScalarValue]: - return [p.radius**3, 0, 0] + def field_function(p: CylinderPoint) -> Sequence[Expr]: + return [p.radius**3, S.Zero, S.Zero] field = VectorField(field_function, B) result = flux_across_volume_boundary(field, (1, 2), (0, 2 * pi), (0, 5)) diff --git a/test/core/fields/operators_test.py b/test/core/fields/operators_test.py index 142329a36..e504e3787 100644 --- a/test/core/fields/operators_test.py +++ b/test/core/fields/operators_test.py @@ -1,9 +1,8 @@ from collections import namedtuple from typing import Sequence from pytest import fixture -from sympy import Expr, cos, exp, sin, Symbol as SymSymbol, sqrt +from sympy import Expr, cos, exp, sin, Symbol as SymSymbol, sqrt, S from sympy.vector import VectorZero -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.expr_comparisons import expr_equals from symplyphysics.core.coordinate_systems.coordinate_systems import CoordinateSystem from symplyphysics.core.fields.vector_field import VectorField @@ -40,7 +39,7 @@ def test_basic_gradient() -> None: def test_cylindrical_gradient() -> None: C1 = CoordinateSystem(CoordinateSystem.System.CYLINDRICAL) - def field_function(p: CylinderPoint) -> ScalarValue: + def field_function(p: CylinderPoint) -> Expr: return p.r**2 + p.r * p.z * sin(p.theta) - p.z**2 cylindrical_field = ScalarField(field_function, C1) @@ -61,7 +60,7 @@ def field_function(p: CylinderPoint) -> ScalarValue: def test_spherical_gradient() -> None: C1 = CoordinateSystem(CoordinateSystem.System.SPHERICAL) - def field_function(p: SpherePoint) -> ScalarValue: + def field_function(p: SpherePoint) -> Expr: return -p.r**2 * cos(2 * p.phi + p.theta) spherical_field = ScalarField(field_function, C1) @@ -97,8 +96,12 @@ def test_cylindrical_divergence(test_args: Args) -> None: # verify that in cylindrical coordinates result is same C1 = CoordinateSystem(CoordinateSystem.System.CYLINDRICAL) - def field_function(p: CylinderPoint) -> Sequence[ScalarValue]: - return [p.radius * 2 / 3, 0, p.height * 2 / 3] + def field_function(p: CylinderPoint) -> Sequence[Expr]: + return [ + p.radius * 2 / 3, + S.Zero, + p.height * 2 / 3, + ] cylindrical_field = VectorField(field_function, C1) result = divergence_operator(cylindrical_field) @@ -108,10 +111,11 @@ def field_function(p: CylinderPoint) -> Sequence[ScalarValue]: def test_spherical_divergence() -> None: C1 = CoordinateSystem(CoordinateSystem.System.SPHERICAL) - def field_function(p: SpherePoint) -> Sequence[ScalarValue]: + def field_function(p: SpherePoint) -> Sequence[Expr]: return [ 1 / p.radius**2 * cos(p.polar_angle), - cos(p.polar_angle), p.radius * sin(p.polar_angle) * cos(p.azimuthal_angle) + cos(p.polar_angle), + p.radius * sin(p.polar_angle) * cos(p.azimuthal_angle), ] field = VectorField(field_function, C1) @@ -137,10 +141,11 @@ def test_basic_curl(test_args: Args) -> None: def test_cylindrical_curl() -> None: C1 = CoordinateSystem(CoordinateSystem.System.CYLINDRICAL) - def field_function(p: CylinderPoint) -> Sequence[ScalarValue]: + def field_function(p: CylinderPoint) -> Sequence[Expr]: return [ - p.radius * sin(p.azimuthal_angle), p.radius**2 * p.height, - p.height * cos(p.azimuthal_angle) + p.radius * sin(p.azimuthal_angle), + p.radius**2 * p.height, + p.height * cos(p.azimuthal_angle), ] field = VectorField(field_function, C1) @@ -157,10 +162,11 @@ def field_function(p: CylinderPoint) -> Sequence[ScalarValue]: def test_spherical_curl() -> None: C1 = CoordinateSystem(CoordinateSystem.System.SPHERICAL) - def field_function(p: SpherePoint) -> Sequence[ScalarValue]: + def field_function(p: SpherePoint) -> Sequence[Expr]: return [ 1 / p.radius**2 * cos(p.polar_angle), - cos(p.polar_angle), p.radius * sin(p.polar_angle) * cos(p.azimuthal_angle) + cos(p.polar_angle), + p.radius * sin(p.polar_angle) * cos(p.azimuthal_angle), ] field = VectorField(field_function, C1) diff --git a/test/core/fields/scalar_field_test.py b/test/core/fields/scalar_field_test.py index 321da1d65..4b81f5bb6 100644 --- a/test/core/fields/scalar_field_test.py +++ b/test/core/fields/scalar_field_test.py @@ -1,9 +1,8 @@ from collections import namedtuple from pytest import fixture, raises -from sympy import atan, cos, pi, sin, sqrt, symbols, simplify, sympify +from sympy import atan, cos, pi, sin, sqrt, symbols, simplify, sympify, Expr from sympy.vector import express from symplyphysics.core.test_decorators import unsupported_usage -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.points.cylinder_point import CylinderPoint from symplyphysics.core.points.sphere_point import SpherePoint from symplyphysics.core.coordinate_systems.coordinate_systems import CoordinateSystem, coordinates_rotate, coordinates_transform @@ -25,7 +24,7 @@ def test_args_fixture() -> Args: def test_basic_field() -> None: - def field_function(p: CartesianPoint) -> ScalarValue: + def field_function(p: CartesianPoint) -> Expr: return p.z * p.y field = ScalarField(field_function) diff --git a/test/core/fields/vector_field_test.py b/test/core/fields/vector_field_test.py index d4fc99712..0a1c68bf7 100644 --- a/test/core/fields/vector_field_test.py +++ b/test/core/fields/vector_field_test.py @@ -1,10 +1,9 @@ from collections import namedtuple -from typing import Sequence +from typing import Sequence, Any from pytest import fixture, raises from sympy import Expr, atan, cos, sin, sqrt, symbols from sympy.vector import express from symplyphysics.core.test_decorators import unsupported_usage -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.points.cylinder_point import CylinderPoint from symplyphysics.core.points.cartesian_point import CartesianPoint from symplyphysics.core.points.point import Point @@ -12,7 +11,7 @@ from symplyphysics.core.fields.vector_field import VectorField -def _assert_point(field_: VectorField, point_: Point, expected_: Sequence[Expr | float]) -> None: +def _assert_point(field_: VectorField, point_: Point, expected_: Sequence[Any]) -> None: value = field_(point_) for idx, v in enumerate(value.components): assert v == expected_[idx] @@ -32,7 +31,7 @@ def test_args_fixture() -> Args: def test_basic_field() -> None: - def field_function(p: CartesianPoint) -> Sequence[ScalarValue]: + def field_function(p: CartesianPoint) -> Sequence[Expr]: return [p.y, p.x] field = VectorField(field_function) @@ -176,7 +175,7 @@ def test_basic_apply_to_basis(test_args: Args) -> None: def test_custom_names_apply_to_basis() -> None: - def field_function(p: CylinderPoint) -> Sequence[ScalarValue]: + def field_function(p: CylinderPoint) -> Sequence[Expr]: return [p.radius, p.azimuthal_angle, p.height] C1 = CoordinateSystem(CoordinateSystem.System.CYLINDRICAL) diff --git a/test/core/points/point_test.py b/test/core/points/point_test.py index 5d2a0cc60..c8e4e46cd 100644 --- a/test/core/points/point_test.py +++ b/test/core/points/point_test.py @@ -1,4 +1,5 @@ -from sympy import Expr, symbols +from typing import Any +from sympy import symbols from sympy.vector import CoordSys3D from symplyphysics.core.points.cartesian_point import CartesianPoint from symplyphysics.core.points.cylinder_point import CylinderPoint @@ -6,7 +7,7 @@ from symplyphysics.core.points.point import Point -def _assert_point(point_: Point, expected_: list[Expr | float]) -> None: +def _assert_point(point_: Point, expected_: list[Any]) -> None: for idx, c in enumerate(point_.coordinates): assert c == expected_[idx] diff --git a/test/core/vectors/arithmetics_test.py b/test/core/vectors/arithmetics_test.py index 5aa53bd9b..419682a7e 100644 --- a/test/core/vectors/arithmetics_test.py +++ b/test/core/vectors/arithmetics_test.py @@ -1,9 +1,9 @@ +from typing import Any from collections import namedtuple from pytest import fixture, raises from sympy import atan2, cos, pi, sin, sqrt, Rational, nan from symplyphysics import (SI, Quantity, dimensionless, units, QuantityVector, Vector, CoordinateSystem, coordinates_transform) -from symplyphysics.core.dimensions import ScalarValue from symplyphysics.core.vectors.arithmetics import (add_cartesian_vectors, cross_cartesian_vectors, dot_vectors, equal_vectors, scale_vector, vector_magnitude, vector_unit, project_vector, reject_cartesian_vector) @@ -560,7 +560,7 @@ def test_basic_quantity_unit_vector() -> None: def test_cartesian_vector_projection() -> None: w = Vector([1, 2, -1]) - def check(v: Vector, correct: list[ScalarValue]) -> None: + def check(v: Vector, correct: list[Any]) -> None: derived = project_vector(v, w) assert vector_magnitude(cross_cartesian_vectors(derived, w)) == 0 assert derived.components == correct @@ -577,7 +577,7 @@ def check(v: Vector, correct: list[ScalarValue]) -> None: def test_cartesian_vector_rejection() -> None: w = Vector([1, 2, -1]) - def check(v: Vector, correct: list[ScalarValue]) -> None: + def check(v: Vector, correct: list[Any]) -> None: derived = reject_cartesian_vector(v, w) assert dot_vectors(derived, w) == 0 assert derived.components == correct