Source code for httk.core.vectors.numeric
"""
The *numeric* presentation of vectors: plain numpy numbers for callers who just want floats.
Where the backend/view family (see :mod:`httk.core.vectors.vector_view`) lets the same tensor be
seen as the exact :class:`~httk.core.vectors.fracvector.FracVector`, an exact
:class:`~httk.core.vectors.surdvector.SurdVector`, a nested ``tuple``, or a
:class:`numpy.ndarray`, the **numeric** concept is a convenience presentation one level above that:
it is for users who do not care which representation carries the numbers and simply want plain
numpy floats to compute with.
The numeric presentation is **numpy-backed**, so a caller always knows the concrete type it gets:
:func:`to_numeric` returns a base-class ``float64`` :class:`numpy.ndarray` for a tensor and a plain
:class:`float` for a scalar — never a view subclass, never a 0-d array. :data:`NumericVector` is the
generic name for what comes out.
numpy is an optional dependency of *httk-core* (the ``httk-core[numpy]`` extra). :func:`to_numeric`
therefore **requires numpy** and raises :class:`ImportError` when it is not installed. The scalar
helper :func:`to_numeric_scalar` is the exception: converting a single value to a ``float`` needs no
numpy, so it works unconditionally and never raises for a missing numpy.
Use the numeric helpers when you just want numpy numbers; reach for a specific view
(:class:`~httk.core.vectors.vector_numpy_view.VectorNumpyView`,
:class:`~httk.core.vectors.vector_native_view.VectorNativeView`) when you need control over the exact
container type, dtype, or leaf codec.
"""
import fractions
from typing import TYPE_CHECKING, Any
from httk.core.views import unwrap
from .exactmath import any_to_fraction
from .fracvector import FracVector
from .surdvector import SurdScalar, SurdVector
from .vector_backend import VectorBackend
from .vector_like import VectorLike
from .vector_view import VectorView
if TYPE_CHECKING:
import numpy
# PEP 695 type aliases are lazily evaluated, so the numpy.ndarray reference (numpy imported only
# under TYPE_CHECKING) is safe here: the alias value is not evaluated at runtime.
[docs]
type NumericVector = float | numpy.ndarray
_NUMPY_REQUIRED_MESSAGE = "the numeric presentation requires numpy; install the httk-core[numpy] extra"
[docs]
def numpy_available() -> bool:
"""
Return whether the optional numpy dependency is available for the numeric helpers.
This reads the vectors package's ``_numpy_available`` flag freshly on each call (the flag set
when :mod:`httk.core.vectors` conditionally imports/registers the numpy backend), so tests may
monkeypatch ``httk.core.vectors._numpy_available`` to exercise the numpy-absent path.
"""
from httk.core import vectors
return bool(vectors._numpy_available)
def _require_numpy() -> None:
"""Raise :class:`ImportError` (naming the ``httk-core[numpy]`` extra) if numpy is unavailable."""
if not numpy_available():
raise ImportError(_NUMPY_REQUIRED_MESSAGE)
[docs]
def to_numeric_scalar(obj: Any) -> float:
"""
Convert a single scalar value to a plain :class:`float`, deterministically.
A :class:`~httk.core.vectors.surdvector.SurdScalar` (or scalar
:class:`~httk.core.vectors.surdvector.SurdVector`) and a scalar
:class:`~httk.core.vectors.fracvector.FracVector` render through their own exact
``to_float()``; a :class:`~fractions.Fraction`, ``int``, ``float``, or numeric ``str`` render via
:func:`~httk.core.vectors.exactmath.any_to_fraction`. A non-scalar shape raises
:class:`TypeError`.
Unlike :func:`to_numeric`, this needs **no numpy**: a plain ``float`` conversion has no numpy
dependency, so it works unconditionally and never raises for a missing numpy.
"""
if isinstance(obj, SurdScalar):
return obj.to_float()
if isinstance(obj, SurdVector):
if obj.dim != ():
raise TypeError(f"to_numeric_scalar: expected a scalar, got a SurdVector of shape {obj.dim}")
return obj._as_scalar().to_float()
if isinstance(obj, FracVector):
if obj.dim != ():
raise TypeError(f"to_numeric_scalar: expected a scalar, got a FracVector of shape {obj.dim}")
return obj.to_float()
if isinstance(obj, (bool, int, float, str, fractions.Fraction)):
return float(any_to_fraction(obj))
raise TypeError(f"to_numeric_scalar: cannot convert {type(obj).__name__} to a scalar float")
[docs]
def to_numeric(obj: VectorLike) -> NumericVector:
"""
Present ``obj`` as plain numpy numbers: a :class:`numpy.ndarray` for a tensor, a ``float`` for a
scalar.
A tensor becomes a base-class ``float64`` :class:`numpy.ndarray` (never a view subclass) via
:class:`~httk.core.vectors.vector_numpy_view.VectorNumpyView`; a **scalar** input (shape ``()``)
returns a plain :class:`float` via :func:`to_numeric_scalar` (never a 0-d array).
The numeric presentation is numpy-backed, so this **always requires numpy**: it raises
:class:`ImportError` (naming the ``httk-core[numpy]`` extra) when numpy is not installed,
uniformly, so the contract is predictable regardless of the input shape. Use
:func:`to_numeric_scalar` directly for a single float without a numpy requirement.
"""
_require_numpy()
import numpy
from .vector_numpy_view import VectorNumpyView
# Bare Python scalars are not wrapped by the vector family; convert them directly.
if isinstance(obj, (bool, int, float, str, fractions.Fraction)):
return to_numeric_scalar(obj)
# Everything else is family-representable: inspect its shape through a backend. A view built
# through the low-level constructor path can be backend-less, so fall through to create.
if isinstance(obj, VectorView) and getattr(obj, "_backend", None) is not None:
backend = obj._backend
elif isinstance(obj, VectorBackend):
backend = obj
else:
backend = VectorBackend.create(obj)
if backend.dim == ():
return to_numeric_scalar(unwrap(backend))
# numpy.asarray drops the view subclass, yielding a plain base-class float64 ndarray.
return numpy.asarray(VectorNumpyView(backend))