diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..d0b7719 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,23 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + + apt_packages: + - libcairo2-dev + - libfreetype6-dev + - libffi-dev + - libjpeg-dev + - libpng-dev + - libz-dev + +mkdocs: + configuration: mkdocs.yml + +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . \ No newline at end of file diff --git a/docs/docs/physics/compute.md b/docs/docs/physics/compute.md index b75bc54..7b1d1eb 100644 --- a/docs/docs/physics/compute.md +++ b/docs/docs/physics/compute.md @@ -186,13 +186,28 @@ print(mag_v) ## Stochastic Physics (`phaethon.random`) -Generates highly-optimized stochastic tensors that are instantly bounded by physical dimensions. +Generates highly-optimized stochastic tensors that are instantly bounded by physical dimensions. Phaethon utilizes an isolated `RandomState` engine under the hood, ensuring that stochastic physics simulations remain completely reproducible without contaminating the global NumPy environment. !!! warning "Axiom Bounds & Stochastic Generation" Because `phaethon.random` functions automatically instantiate new `BaseUnit` tensors under the hood, generating stochastic values that violate the target unit's physical boundaries (e.g., generating negative `Kelvin` or `Mass`) will immediately trigger an `AxiomViolationError` under the `default` axiom strictness level. To prevent unexpected crashes in your simulations, ensure your distribution parameters (`low`, `loc`, `scale`) remain within physically valid ranges, or temporarily adjust the axiom's strictness level using [`phaethon.using()`](config.md) or [`phaethon.config()`](config.md). +### phaethon.random.seed + +Reseeds the isolated physics random number generator. Crucial for ensuring absolute reproducibility in stochastic physical models, thermodynamic simulations, or machine learning cross-validations. + +**Arguments:** + +
+
+ seed + + int | None +
+
An integer to initialize the internal BitGenerator. If None, fresh entropy is drawn from the OS.
+
+ ### phaethon.random.uniform / normal Draws samples from continuous distributions (Uniform or Gaussian) and injects physical DNA. @@ -231,6 +246,9 @@ Draws samples from continuous distributions (Uniform or Gaussian) and injects ph ```python import phaethon as ptn +# Ensure reproducibility +ptn.random.seed(42) + # Uniform: Generate a 3x3 matrix of random pressures (10.5 to 20.5 Pascal) pressures = ptn.random.uniform(low=10.5, high=20.5, size=(3, 3), unit='Pa') @@ -300,15 +318,79 @@ Draws samples from an exponential distribution. Ideal for simulating the time be
The physical dimension to attach (typically 's' or u.Second).
+### phaethon.random.randint / choice + +Draws from discrete probability distributions. `randint` generates uniformly distributed integers, while `choice` generates a random sample from a predefined 1-D array of allowed physical states. + +**Arguments:** + +
+
+ low, high (randint) / a (choice) + + int | ArrayLike +
+
Integer bounds, or the 1-D array of allowed magnitudes to sample from.
+
+ +
+
+ size + + Any +
+
Output array shape.
+
+ +
+
+ unit + + str | type[BaseUnit] +
+
The physical dimension to attach.
+
+ +**Example Usage:** + +```python +import phaethon as ptn +import phaethon.units as u + +# Generate quantized discrete energy levels (-1, 0, or 1 eV) +energy_levels = ptn.random.randint(-1, 2, size=5, unit=u.Electronvolt) + +# Monte Carlo: Select a random speed from allowed discrete states +allowed_speeds = [300.0, 400.0, 500.0] +particles = ptn.random.choice(allowed_speeds, size=10, unit=u.MeterPerSecond) +``` + +### phaethon.random.shuffle / permutation + +Modifies sequence order. `shuffle` modifies a physical tensor sequence in-place along the first axis. `permutation` randomly permutes a tensor and returns a completely new copy (out-of-place). + +**Arguments:** + +
+
+ x + + BaseUnit | int +
+
The physical tensor to shuffle/permute. If an integer is passed to permutation, it returns a dimensionless permuted range.
+
+ **Example Usage:** ```python import phaethon as ptn import phaethon.units as u -# Simulating time between particle arrivals (average 1.5 seconds) -arrival_times = ptn.random.exponential(scale=1.5, size=(3,), unit=u.Second) +velocity_array = ptn.array([10.0, 20.0, 30.0], unit=u.MeterPerSecond) + +# Mutates the array directly in memory +ptn.random.shuffle(velocity_array) -print(arrival_times.dimension) -# Output: 'time' +# Creates a new array with randomized elements +new_tensor = ptn.random.permutation(velocity_array) ``` \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..9ea8871 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +mkdocs-material[imaging]>=9.0.0 +mkdocstrings[python]>=0.24.0 +mkdocs-minify-plugin>=0.7.0 +mkdocs-git-revision-date-localized-plugin>=1.2.0 \ No newline at end of file diff --git a/src/phaethon/_typing.py b/src/phaethon/_typing.py new file mode 100644 index 0000000..693660a --- /dev/null +++ b/src/phaethon/_typing.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from typing import Any, TYPE_CHECKING, TypeAlias, Iterable, TypeVar, Callable, Literal, Protocol, Mapping, TypedDict +from phaethon.core.compat import HAS_POLARS, HAS_PANDAS, HAS_NUMPY, HAS_TORCH + +if TYPE_CHECKING: + if HAS_POLARS: import polars as pl + if HAS_PANDAS: import pandas as pd + if HAS_NUMPY: import numpy as np + if HAS_TORCH: import torch + from .core.base import BaseUnit + + DataFrameLike: TypeAlias = pd.DataFrame | pl.DataFrame + NumericLike: TypeAlias = int | float | str | np.ndarray | Iterable[Any] + UnitLike: TypeAlias = str | type[BaseUnit] + + ConvertibleInput: TypeAlias = NumericLike | 'BaseUnit' | Iterable['BaseUnit'] + ColumnTarget: TypeAlias = str | Any + Extractable: TypeAlias = BaseUnit | torch.Tensor | np.ndarray | float | int | str | list[Any] | tuple[Any, ...] | None + UnwrappedArray: TypeAlias = np.ndarray | float | int | None + + ContextDict: TypeAlias = dict[str, NumericLike | BaseUnit] + AliasRegistry: TypeAlias = dict[str, str | list[str]] + + ImputeMethod: TypeAlias = Literal['mean', 'median', 'mode', 'ffill', 'bfill'] | str | float + InterpolationMethod = Literal[ + "linear", "nearest", "time", "index", "values", "pad", "zero", "slinear","quadratic", + "cubic", "spline", "barycentric", "polynomial", "krogh","piecewise_polynomial", + "pchip", "akima", "cubicspline" + ] + ErrorAction: TypeAlias = Literal['raise', 'coerce', 'clip'] + StrictnessLevel = Literal["default", "strict", "strict_warn", "loose_warn", "ignore"] + NumDtype: TypeAlias = Literal["float64", "float32", "float16", "int64", "int32"] + + if HAS_TORCH: + from .core.pinns.tensor import PTensor + TensorLikeDict: TypeAlias = dict[str, PTensor | torch.Tensor] + TensorLikeTuple: TypeAlias = tuple[PTensor | torch.Tensor, ...] + GradTarget: TypeAlias = bool | list[str] + + class _ParquetConfig(TypedDict, total=False): + engine: Literal["pyarrow", "fastparquet", "auto"] + compression: Literal["snappy", "gzip", "brotli", "lz4", "zstd"] + index: bool + partition_cols: list[str] + + class _HDF5Config(TypedDict, total=False): + compression: Literal["gzip", "lzf", "szip"] + compression_opts: int + chunks: bool | tuple[int, ...] + + DatasetInput: TypeAlias = Mapping[str, Any] | Iterable[Any] | Any + DatasetStateDict: TypeAlias = dict[str, dict[str, Any]] + + class _ResponsiveTableConfig(TypedDict, total=False): + max_col_width: int + float_format: str + justify: Literal["left", "right", "center"] + +else: + DataFrameLike: TypeAlias = Any + NumericLike: TypeAlias = Any + UnitLike: TypeAlias = Any + ConvertibleInput: TypeAlias = Any + ColumnTarget: TypeAlias = Any + Extractable: TypeAlias = Any + UnwrappedArray: TypeAlias = Any + ContextDict: TypeAlias = Any + AliasRegistry: TypeAlias = Any + ImputeMethod: TypeAlias = Any + InterpolationMethod: TypeAlias = Any + ErrorAction: TypeAlias = Any + StrictnessLevel: TypeAlias = Any + NumDtype: TypeAlias = Any + TensorLikeDict: TypeAlias = Any + TensorLikeTuple: TypeAlias = Any + GradTarget: TypeAlias = Any + +_Signature: TypeAlias = frozenset[tuple[str, int]] +_DataFrameT = TypeVar("_DataFrameT", bound=DataFrameLike) +_NumericT = TypeVar("_NumericT", bound=NumericLike) +_UnitT = TypeVar("_UnitT", bound='BaseUnit') +_UnitT_co = TypeVar("_UnitT_co", bound='BaseUnit', covariant=True) +_UnitClassT = TypeVar("_UnitClassT", bound=type) +_CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) +_ReturnT = TypeVar("_ReturnT") +_KeyT = TypeVar('_KeyT') + +class SupportsPredict(Protocol): + def fit(self, X: Any, y: Any = None, **kwargs: Any) -> Any: ... + def predict(self, X: Any) -> Any: ... + +class SupportsTransform(Protocol): + def fit(self, X: Any, y: Any = None, **kwargs: Any) -> Any: ... + def transform(self, X: Any) -> Any: ... + +class SupportsInverseTransform(SupportsTransform, Protocol): + def inverse_transform(self, X: Any) -> Any: ... + +_EstimatorT = TypeVar("_EstimatorT", bound=SupportsPredict) +_TransformerT = TypeVar("_TransformerT", bound=SupportsTransform) +_InvTransformerT = TypeVar("_InvTransformerT", bound=SupportsInverseTransform) \ No newline at end of file diff --git a/src/phaethon/core/axioms.py b/src/phaethon/core/axioms.py index a60781f..0b6b1fb 100644 --- a/src/phaethon/core/axioms.py +++ b/src/phaethon/core/axioms.py @@ -17,7 +17,7 @@ class SupportsContextEvaluation(Protocol): def __call__(self, context: ContextDict) -> NumericLike: ... -from .compat import _UnitClassT, _CallableT, NumericLike, ContextDict +from .._typing import _UnitClassT, _CallableT, NumericLike, ContextDict from ..exceptions import AxiomViolationError, DimensionMismatchError try: diff --git a/src/phaethon/core/base.py b/src/phaethon/core/base.py index c2eee84..a58de82 100644 --- a/src/phaethon/core/base.py +++ b/src/phaethon/core/base.py @@ -8,7 +8,7 @@ from .registry import ureg from .config import get_config, using from ..exceptions import DimensionMismatchError, PhysicalAlgebraError -from .compat import _UnitT, ContextDict, NumericLike, UnitLike +from .._typing import _UnitT, ContextDict, NumericLike, UnitLike if TYPE_CHECKING: import numpy as np diff --git a/src/phaethon/core/base.pyi b/src/phaethon/core/base.pyi new file mode 100644 index 0000000..4050976 --- /dev/null +++ b/src/phaethon/core/base.pyi @@ -0,0 +1,118 @@ +import numpy as np +import numpy.typing as npt +from typing import Any, overload +from .._typing import ContextDict, NumericLike, UnitLike, _UnitT + +class _DecomposeDispatcher: + def __call__(self) -> str: ... + def __get__(self, instance: Any, owner: type) -> '_DecomposeDispatcher': ... + +class _PhaethonUnitMeta(type): + dimension: str | None + symbol: str | None + base_multiplier: float + base_offset: float + is_anonymous: bool + __semantic__: str | None + _signature: frozenset[Any] + + def __mul__(cls, other: Any) -> type['BaseUnit']: ... + def __rmul__(cls, other: Any) -> type['BaseUnit']: ... + def __truediv__(cls, other: Any) -> type['BaseUnit']: ... + def __rtruediv__(cls, other: Any) -> type['BaseUnit']: ... + def __pow__(cls, power: int | float) -> type['BaseUnit']: ... + def __matmul__(cls, other: type['BaseUnit']) -> type['BaseUnit']: ... + def __rmatmul__(cls, other: type['BaseUnit']) -> type['BaseUnit']: ... + + def __lt__(cls, other: Any) -> bool: ... + def __le__(cls, other: Any) -> bool: ... + def __gt__(cls, other: Any) -> bool: ... + def __ge__(cls, other: Any) -> bool: ... + +class BaseUnit(metaclass=_PhaethonUnitMeta): + """ + The foundational core class for all units in Phaethon. + """ + dimension: str | None + symbol: str | None + aliases: list[str] | None + base_multiplier: float + base_offset: float + is_anonymous: bool + __semantic__: str | None + context: ContextDict + + def __init__(self, value: NumericLike, context: ContextDict | None = ...) -> None: ... + + @property + def mag(self) -> float | npt.NDArray[np.float64]: ... + + @property + def si(self) -> 'BaseUnit': ... + + @property + def shape(self) -> tuple[int, ...]: ... + + @property + def ndim(self) -> int: ... + + @property + def T(self) -> 'BaseUnit': ... + + def __getitem__(self, key: Any) -> 'BaseUnit': ... + + def __add__(self, other: Any) -> 'BaseUnit': ... + def __sub__(self, other: Any) -> 'BaseUnit': ... + def __mul__(self, other: Any) -> 'BaseUnit': ... + def __truediv__(self, other: Any) -> 'BaseUnit': ... + def __floordiv__(self, other: Any) -> 'BaseUnit': ... + def __mod__(self, other: Any) -> 'BaseUnit': ... + + def __radd__(self, other: Any) -> 'BaseUnit': ... + def __rsub__(self, other: Any) -> 'BaseUnit': ... + def __rmul__(self, other: Any) -> 'BaseUnit': ... + def __rtruediv__(self, other: Any) -> 'BaseUnit': ... + + def __pow__(self, power: int | float) -> 'BaseUnit': ... + def __invert__(self) -> 'BaseUnit': ... + def __neg__(self) -> 'BaseUnit': ... + def __pos__(self) -> 'BaseUnit': ... + def __abs__(self) -> 'BaseUnit': ... + def __round__(self, ndigits: int | None = ...) -> 'BaseUnit': ... + + def __matmul__(self, other: Any) -> 'BaseUnit': ... + def __rmatmul__(self, other: Any) -> 'BaseUnit': ... + + def sum(self, axis: int | tuple[int, ...] | None = ..., keepdims: bool = ..., **kwargs: Any) -> 'BaseUnit': ... + def mean(self, axis: int | tuple[int, ...] | None = ..., keepdims: bool = ..., **kwargs: Any) -> 'BaseUnit': ... + def max(self, axis: int | tuple[int, ...] | None = ..., keepdims: bool = ..., **kwargs: Any) -> 'BaseUnit': ... + def min(self, axis: int | tuple[int, ...] | None = ..., keepdims: bool = ..., **kwargs: Any) -> 'BaseUnit': ... + + def reshape(self, shape: tuple[int, ...] | int, *args: Any, order: str = ...) -> 'BaseUnit': ... + def flatten(self, order: str = ...) -> 'BaseUnit': ... + + @overload + def to(self, unit: type[_UnitT], context: ContextDict | None = ...) -> _UnitT: ... + @overload + def to(self, unit: str, context: ContextDict | None = ...) -> 'BaseUnit': ... + def to(self, unit: UnitLike, context: ContextDict | None = ...) -> 'BaseUnit' | _UnitT: ... + + def format(self, prec: int = ..., sigfigs: int | None = ..., scinote: bool = ..., delim: bool | str = ..., tag: bool = ...) -> str: ... + + decompose: _DecomposeDispatcher + + def __eq__(self, other: object) -> bool | npt.NDArray[np.bool_]: ... + def __ne__(self, other: object) -> bool | npt.NDArray[np.bool_]: ... + def __lt__(self, other: Any) -> bool | npt.NDArray[np.bool_]: ... + def __le__(self, other: Any) -> bool | npt.NDArray[np.bool_]: ... + def __gt__(self, other: Any) -> bool | npt.NDArray[np.bool_]: ... + def __ge__(self, other: Any) -> bool | npt.NDArray[np.bool_]: ... + + def __float__(self) -> float: ... + def __int__(self) -> int: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + + def __array__(self, dtype: Any = ...) -> npt.NDArray[Any]: ... + def __array_ufunc__(self, ufunc: Any, method: str, *inputs: Any, **kwargs: Any) -> Any: ... + def __array_function__(self, func: Any, types: Any, args: Any, kwargs: Any) -> Any: ... \ No newline at end of file diff --git a/src/phaethon/core/compat.py b/src/phaethon/core/compat.py index 4162b48..d1e0967 100644 --- a/src/phaethon/core/compat.py +++ b/src/phaethon/core/compat.py @@ -4,8 +4,7 @@ import importlib.metadata import warnings from packaging.version import parse -from typing import Any, TYPE_CHECKING, TypeAlias, Iterable, TypeVar, Callable, Literal, Protocol, Mapping - +from typing import Any def _check_dep(module_name: str, package_name: str | None = None) -> tuple[bool, str | None]: if package_name is None: @@ -61,112 +60,6 @@ def require_h5py(feature_name: str = "HDF5 I/O") -> None: if not HAS_H5PY: raise ImportError(f"{feature_name} requires h5py. Install via: pip install 'phaethon[io]' or h5py>=3.0.0") - -# ========================================================================= -# THE TYPE REGISTRY & CONFIGURATIONS -# ========================================================================= -if TYPE_CHECKING: - from typing import TypedDict, Unpack - if HAS_POLARS: import polars as pl - if HAS_PANDAS: import pandas as pd - if HAS_NUMPY: import numpy as np - if HAS_TORCH: import torch - from .base import BaseUnit - - DataFrameLike: TypeAlias = pd.DataFrame | pl.DataFrame - NumericLike: TypeAlias = int | float | str | np.ndarray | Iterable[Any] - UnitLike: TypeAlias = str | type[BaseUnit] - - ConvertibleInput: TypeAlias = NumericLike | 'BaseUnit' | Iterable['BaseUnit'] - ColumnTarget: TypeAlias = str | Any - Extractable: TypeAlias = BaseUnit | torch.Tensor | np.ndarray | float | int | str | list[Any] | tuple[Any, ...] | None - UnwrappedArray: TypeAlias = np.ndarray | float | int | None - - ContextDict: TypeAlias = dict[str, NumericLike | BaseUnit] - AliasRegistry: TypeAlias = dict[str, str | list[str]] - - ImputeMethod: TypeAlias = Literal['mean', 'median', 'mode', 'ffill', 'bfill'] | str | float - InterpolationMethod = Literal[ - "linear", "nearest", "time", "index", "values", "pad", "zero", "slinear","quadratic", - "cubic", "spline", "barycentric", "polynomial", "krogh","piecewise_polynomial", - "pchip", "akima", "cubicspline" - ] - ErrorAction: TypeAlias = Literal['raise', 'coerce', 'clip'] - StrictnessLevel = Literal["default", "strict", "strict_warn", "loose_warn", "ignore"] - NumDtype: TypeAlias = Literal["float64", "float32", "float16", "int64", "int32"] - - if HAS_TORCH: - from .pinns.tensor import PTensor - TensorLikeDict: TypeAlias = dict[str, PTensor | torch.Tensor] - TensorLikeTuple: TypeAlias = tuple[PTensor | torch.Tensor, ...] - GradTarget: TypeAlias = bool | list[str] - - class _ParquetConfig(TypedDict, total=False): - engine: Literal["pyarrow", "fastparquet", "auto"] - compression: Literal["snappy", "gzip", "brotli", "lz4", "zstd"] - index: bool - partition_cols: list[str] - - class _HDF5Config(TypedDict, total=False): - compression: Literal["gzip", "lzf", "szip"] - compression_opts: int - chunks: bool | tuple[int, ...] - - DatasetInput: TypeAlias = Mapping[str, Any] | Iterable[Any] | Any - DatasetStateDict: TypeAlias = dict[str, dict[str, Any]] - - class _ResponsiveTableConfig(TypedDict, total=False): - max_col_width: int - float_format: str - justify: Literal["left", "right", "center"] - -else: - DataFrameLike: TypeAlias = Any - NumericLike: TypeAlias = Any - UnitLike: TypeAlias = Any - ConvertibleInput: TypeAlias = Any - ColumnTarget: TypeAlias = Any - Extractable: TypeAlias = Any - UnwrappedArray: TypeAlias = Any - ContextDict: TypeAlias = Any - AliasRegistry: TypeAlias = Any - ImputeMethod: TypeAlias = Any - InterpolationMethod: TypeAlias = Any - ErrorAction: TypeAlias = Any - StrictnessLevel: TypeAlias = Any - NumDtype: TypeAlias = Any - TensorLikeDict: TypeAlias = Any - TensorLikeTuple: TypeAlias = Any - GradTarget: TypeAlias = Any - -_Signature: TypeAlias = frozenset[tuple[str, int]] -_DataFrameT = TypeVar("_DataFrameT", bound=DataFrameLike) -_NumericT = TypeVar("_NumericT", bound=NumericLike) -_UnitT = TypeVar("_UnitT", bound='BaseUnit') -_UnitT_co = TypeVar("_UnitT_co", bound='BaseUnit', covariant=True) -_UnitClassT = TypeVar("_UnitClassT", bound=type) -_CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) -_ReturnT = TypeVar("_ReturnT") -_KeyT = TypeVar('_KeyT') - -class SupportsPredict(Protocol): - def fit(self, X: Any, y: Any = None, **kwargs: Any) -> Any: ... - def predict(self, X: Any) -> Any: ... - -class SupportsTransform(Protocol): - def fit(self, X: Any, y: Any = None, **kwargs: Any) -> Any: ... - def transform(self, X: Any) -> Any: ... - -class SupportsInverseTransform(SupportsTransform, Protocol): - def inverse_transform(self, X: Any) -> Any: ... - -_EstimatorT = TypeVar("_EstimatorT", bound=SupportsPredict) -_TransformerT = TypeVar("_TransformerT", bound=SupportsTransform) -_InvTransformerT = TypeVar("_InvTransformerT", bound=SupportsInverseTransform) - -# ========================================================================= -# COMPATIBILITY HELPERS -# ========================================================================= def is_pandas_df(df: Any) -> bool: return HAS_PANDAS and df.__class__.__module__.startswith('pandas') diff --git a/src/phaethon/core/config.py b/src/phaethon/core/config.py index 91490a0..49b6956 100644 --- a/src/phaethon/core/config.py +++ b/src/phaethon/core/config.py @@ -4,7 +4,7 @@ from contextvars import ContextVar from contextlib import contextmanager from typing import Any, Generator -from .compat import ContextDict, AliasRegistry, ErrorAction, StrictnessLevel +from .._typing import ContextDict, AliasRegistry, ErrorAction, StrictnessLevel _GLB = "__phaethon_global__" _CTX = "__phaethon_context__" diff --git a/src/phaethon/core/dataset.py b/src/phaethon/core/dataset.py index b8a04ad..1c4f52d 100644 --- a/src/phaethon/core/dataset.py +++ b/src/phaethon/core/dataset.py @@ -12,7 +12,7 @@ import inspect from typing import Any, Mapping, Iterator, TYPE_CHECKING, overload -from .compat import HAS_NUMPY, HAS_TORCH +from .._typing import HAS_NUMPY, HAS_TORCH from .base import BaseUnit, _find_existing_class from .registry import ureg @@ -20,7 +20,7 @@ import numpy as np if TYPE_CHECKING: - from .compat import DatasetInput, TensorLikeDict + from .._typing import DatasetInput, TensorLikeDict if HAS_TORCH: import torch from .pinns.tensor import PTensor diff --git a/src/phaethon/core/fluent.py b/src/phaethon/core/fluent.py index 7c0e8b8..f710e0d 100644 --- a/src/phaethon/core/fluent.py +++ b/src/phaethon/core/fluent.py @@ -1,11 +1,11 @@ from __future__ import annotations from math import log10, floor -from typing import Literal, Any, overload, Generic +from typing import Literal, Any, overload, Generic, TYPE_CHECKING from ..exceptions import ConversionError, AmbiguousUnitError, UnitNotFoundError from .registry import UnitRegistry, ureg -from .compat import UnitLike, ConvertibleInput, _ReturnT, ContextDict, NumericLike, NumDtype, TYPE_CHECKING +from .._typing import UnitLike, ConvertibleInput, _ReturnT, ContextDict, NumericLike, NumDtype try: from .base import BaseUnit diff --git a/src/phaethon/core/io.py b/src/phaethon/core/io.py index 8de7d93..185ac51 100644 --- a/src/phaethon/core/io.py +++ b/src/phaethon/core/io.py @@ -19,7 +19,7 @@ ) if TYPE_CHECKING: - from .compat import DataFrameLike, _ParquetConfig, _HDF5Config + from .._typing import DataFrameLike, _ParquetConfig, _HDF5Config from typing import Unpack from .dataset import Dataset diff --git a/src/phaethon/core/linalg.py b/src/phaethon/core/linalg.py index aa112ad..23a48a2 100644 --- a/src/phaethon/core/linalg.py +++ b/src/phaethon/core/linalg.py @@ -3,9 +3,10 @@ Physics-aware wrappers around numpy.linalg operations. """ from __future__ import annotations -from typing import Any, overload +from typing import Any -from .compat import HAS_NUMPY, _UnitT +from .compat import HAS_NUMPY +from .._typing import _UnitT from .base import BaseUnit if HAS_NUMPY: diff --git a/src/phaethon/core/ml/estimator.py b/src/phaethon/core/ml/estimator.py index b175f0e..8c7492d 100644 --- a/src/phaethon/core/ml/estimator.py +++ b/src/phaethon/core/ml/estimator.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: import numpy as np from typing_extensions import Self - from ..compat import _EstimatorT, _UnitT, DataFrameLike, NumericLike, ConvertibleInput + from ..._typing import _EstimatorT, _UnitT, DataFrameLike, NumericLike, ConvertibleInput else: _EstimatorT = TypeVar("_EstimatorT") _UnitT = TypeVar("_UnitT") diff --git a/src/phaethon/core/ml/metrics.py b/src/phaethon/core/ml/metrics.py index bcc0e16..51bc4e2 100644 --- a/src/phaethon/core/ml/metrics.py +++ b/src/phaethon/core/ml/metrics.py @@ -3,7 +3,7 @@ """ from __future__ import annotations from typing import Any, Literal, overload -from ..compat import NumericLike, _UnitT +from ..._typing import NumericLike, _UnitT from ...exceptions import PhysicalAlgebraError from ..base import BaseUnit diff --git a/src/phaethon/core/ml/preprocessing.py b/src/phaethon/core/ml/preprocessing.py index 7b9ca2b..5b9b3d6 100644 --- a/src/phaethon/core/ml/preprocessing.py +++ b/src/phaethon/core/ml/preprocessing.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: import numpy as np from typing_extensions import Self - from ..compat import _InvTransformerT, DataFrameLike, NumericLike + from ..._typing import _InvTransformerT, DataFrameLike, NumericLike else: _InvTransformerT = TypeVar("_InvTransformerT") Self = Any diff --git a/src/phaethon/core/ml/selection.py b/src/phaethon/core/ml/selection.py index cfce3eb..1311746 100644 --- a/src/phaethon/core/ml/selection.py +++ b/src/phaethon/core/ml/selection.py @@ -5,7 +5,8 @@ from typing import Any from ..base import BaseUnit -from ..compat import HAS_NUMPY, HAS_SKLEARN, DataFrameLike +from ..compat import HAS_NUMPY, HAS_SKLEARN +from ..._typing import DataFrameLike if HAS_SKLEARN: import numpy as np diff --git a/src/phaethon/core/pinns/layers.py b/src/phaethon/core/pinns/layers.py index 89a317d..838c1a8 100644 --- a/src/phaethon/core/pinns/layers.py +++ b/src/phaethon/core/pinns/layers.py @@ -11,7 +11,7 @@ import torch import torch.nn as nn from .ops import fft, ifft - from ..compat import _UnitT_co + from ..._typing import _UnitT_co _BaseModule = nn.Module elif HAS_TORCH: import torch diff --git a/src/phaethon/core/pinns/ops.py b/src/phaethon/core/pinns/ops.py index 2173c25..a7c9d77 100644 --- a/src/phaethon/core/pinns/ops.py +++ b/src/phaethon/core/pinns/ops.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: import torch import torch.fft as tfft - from ..compat import _UnitT_co + from ..._typing import _UnitT_co elif HAS_TORCH: import torch import torch.fft as tfft diff --git a/src/phaethon/core/pinns/tensor.py b/src/phaethon/core/pinns/tensor.py index 1d41680..02fd5b8 100644 --- a/src/phaethon/core/pinns/tensor.py +++ b/src/phaethon/core/pinns/tensor.py @@ -4,7 +4,8 @@ from __future__ import annotations from typing import Any, Callable, TYPE_CHECKING, Generic, overload -from ..compat import HAS_TORCH, _UnitT_co, NumericLike, _UnitT +from ..compat import HAS_TORCH +from ..._typing import _UnitT_co, NumericLike, _UnitT from ..registry import ureg from ..base import BaseUnit from ...exceptions import DimensionMismatchError diff --git a/src/phaethon/core/plotting.py b/src/phaethon/core/plotting.py index efae4e7..7afc74d 100644 --- a/src/phaethon/core/plotting.py +++ b/src/phaethon/core/plotting.py @@ -8,7 +8,8 @@ from typing import Any, TYPE_CHECKING, overload, Mapping import numpy as np -from .compat import HAS_NUMPY, _KeyT, Extractable, UnwrappedArray +from .compat import HAS_NUMPY +from .._typing import _KeyT, Extractable, UnwrappedArray if TYPE_CHECKING: from .base import BaseUnit @@ -17,7 +18,8 @@ from collections.abc import Mapping from typing import Any, TYPE_CHECKING, overload -from .compat import HAS_NUMPY, _KeyT, Extractable, UnwrappedArray +from .compat import HAS_NUMPY +from .._typing import _KeyT, Extractable, UnwrappedArray if TYPE_CHECKING: from .base import BaseUnit diff --git a/src/phaethon/core/random.py b/src/phaethon/core/random.py index 0f76c86..97af4d4 100644 --- a/src/phaethon/core/random.py +++ b/src/phaethon/core/random.py @@ -3,121 +3,212 @@ Generates stochastic tensors strictly bounded by physical dimensions. """ from __future__ import annotations -from typing import Any, overload +from typing import Any -from .compat import UnitLike, HAS_NUMPY, _UnitT +from .compat import HAS_NUMPY from .registry import ureg from .base import BaseUnit if HAS_NUMPY: import numpy as np -def _resolve_unit(unit: UnitLike) -> type[BaseUnit]: +def _resolve_unit(unit: Any) -> type[BaseUnit]: if isinstance(unit, str): return ureg().get_unit_class(unit) if isinstance(unit, type) and issubclass(unit, BaseUnit): return unit - raise TypeError(f"The 'unit' argument must be a string alias or a BaseUnit class.") + raise TypeError("The 'unit' argument must be a string alias or a BaseUnit class.") -# ========================================================================= -# ptn.random.uniform() -# ========================================================================= -@overload -def uniform(low: float = ..., high: float = ..., size: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... -@overload -def uniform(low: float = ..., high: float = ..., size: Any = ..., unit: str = ...) -> BaseUnit: ... - -def uniform(low: float = 0.0, high: float = 1.0, size: Any = None, unit: UnitLike | None = None) -> BaseUnit: - """ - Draws samples from a uniform distribution and injects physical DNA. - - Args: - low: Lower boundary of the output interval. - high: Upper boundary of the output interval. - size: Output shape (e.g., (2, 3)). - unit: The physical dimension to attach (Class or alias string). - - Returns: - A BaseUnit tensor containing uniformly distributed physical values. - """ - if not HAS_NUMPY: raise ImportError("NumPy is required.") - if unit is None: raise ValueError("A physical unit must be specified.") - - raw_arr = np.random.uniform(low, high, size) - UnitClass = _resolve_unit(unit) - return UnitClass(raw_arr) +class RandomState: + def seed(self, seed: int | None = None) -> None: + """ + Reseeds the isolated physics random number generator. + + Crucial for ensuring absolute reproducibility in stochastic physical models, + thermodynamic simulations, or machine learning cross-validations. + + Args: + seed: An integer to initialize the internal BitGenerator. + If None, fresh, unpredictable entropy will be pulled from the OS. + """ + if not HAS_NUMPY: raise ImportError("NumPy is required.") + self._rng = np.random.default_rng(seed) -# ========================================================================= -# ptn.random.normal() -# ========================================================================= -@overload -def normal(loc: float = ..., scale: float = ..., size: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... -@overload -def normal(loc: float = ..., scale: float = ..., size: Any = ..., unit: str = ...) -> BaseUnit: ... + def uniform(self, low=0.0, high=1.0, size=None, unit=None) -> BaseUnit: + """ + Draws samples from a uniform distribution and injects physical DNA. + + Args: + low: Lower boundary of the output interval. + high: Upper boundary of the output interval. + size: Output shape (e.g., (2, 3)). + unit: The physical dimension to attach (Class or alias string). + + Returns: + A BaseUnit tensor containing uniformly distributed physical values. + """ + if not HAS_NUMPY: raise ImportError("NumPy is required.") + if unit is None: raise ValueError("A physical unit must be specified.") + + raw_arr = self._rng.uniform(low, high, size) + UnitClass = _resolve_unit(unit) + return UnitClass(raw_arr) -def normal(loc: float = 0.0, scale: float = 1.0, size: Any = None, unit: UnitLike | None = None) -> BaseUnit: - """ - Draws random samples from a normal (Gaussian) distribution. - - Args: - loc: Mean ("centre") of the distribution. - scale: Standard deviation (spread or "width"). - size: Output shape. - unit: The physical dimension to attach. - """ - if not HAS_NUMPY: raise ImportError("NumPy is required.") - if unit is None: raise ValueError("A physical unit must be specified.") - - raw_arr = np.random.normal(loc, scale, size) - UnitClass = _resolve_unit(unit) - return UnitClass(raw_arr) + def normal(self, loc=0.0, scale=1.0, size=None, unit=None) -> BaseUnit: + """ + Draws random samples from a normal (Gaussian) distribution. + + Args: + loc: Mean ("centre") of the distribution. + scale: Standard deviation (spread or "width"). + size: Output shape. + unit: The physical dimension to attach. + """ + if not HAS_NUMPY: raise ImportError("NumPy is required.") + if unit is None: raise ValueError("A physical unit must be specified.") + + raw_arr = self._rng.normal(loc, scale, size) + UnitClass = _resolve_unit(unit) + return UnitClass(raw_arr) -@overload -def poisson(lam: float = ..., size: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... -@overload -def poisson(lam: float = ..., size: Any = ..., unit: str = ...) -> BaseUnit: ... + def poisson(self, lam=1.0, size=None, unit=None) -> BaseUnit: + """ + Draws samples from a Poisson distribution. + + Extremely useful in Phaethon for modeling discrete physical events over + a continuous interval, such as radioactive decays (u.Becquerel) or + photon strikes (u.Photon). + + Args: + lam: Expected number of events occurring in a fixed-time interval. + size: Output shape. + unit: The physical dimension to attach. + """ + if not HAS_NUMPY: raise ImportError("NumPy is required.") + if unit is None: raise ValueError("A physical unit must be specified.") + + raw_arr = self._rng.poisson(lam, size) + UnitClass = _resolve_unit(unit) + return UnitClass(raw_arr) -def poisson(lam: float = 1.0, size: Any = None, unit: UnitLike | None = None) -> BaseUnit: - """ - Draws samples from a Poisson distribution. + def exponential(self, scale=1.0, size=None, unit=None) -> BaseUnit: + """ + Draws samples from an exponential distribution. + + Ideal for simulating the time between independent physics events, + such as the decay time of radioactive isotopes or thermodynamic + relaxation times. + + Args: + scale: The scale parameter, β = 1/λ. Must be non-negative. + size: Output shape. + unit: The physical dimension to attach (typically u.Second). + """ + if not HAS_NUMPY: raise ImportError("NumPy is required.") + if unit is None: raise ValueError("A physical unit must be specified.") + + raw_arr = self._rng.exponential(scale, size) + UnitClass = _resolve_unit(unit) + return UnitClass(raw_arr) - Extremely useful in Phaethon for modeling discrete physical events over - a continuous interval, such as radioactive decays (u.Becquerel) or - photon strikes (u.Photon). + def randint(self, low: int, high: int | None = None, size=None, unit=None) -> BaseUnit: + """ + Draws random integers from a discrete uniform distribution. + + Crucial for quantum mechanics, statistical grids, or any physical domain + where magnitudes are strictly quantized. + + Args: + low: Lowest (signed) integer to be drawn from the distribution. + high: One above the largest (signed) integer to be drawn. + size: Output shape. + unit: The physical dimension to attach. + + Returns: + A BaseUnit tensor containing discrete, uniformly distributed integers. + """ + if not HAS_NUMPY: raise ImportError("NumPy is required.") + if unit is None: raise ValueError("A physical unit must be specified.") + + raw_arr = self._rng.integers(low, high, size=size) + UnitClass = _resolve_unit(unit) + return UnitClass(raw_arr) + + def choice(self, a, size=None, replace=True, p=None, unit=None) -> BaseUnit: + """ + Generates a random sample from a given 1-D array of physical states. + + Allows physical entities to randomly collapse into a predefined set of + allowed states. Ideal for simulating quantum state measurements or + drawing specific velocity vectors in a Monte Carlo gas simulation. + + Args: + a: A 1-D array-like of allowed magnitudes. + size: Output shape. + replace: Whether the sample is with or without replacement. + p: The probabilities associated with each entry in 'a'. + unit: The physical dimension to attach. + + Returns: + A BaseUnit tensor representing the collapsed random states. + """ + if not HAS_NUMPY: raise ImportError("NumPy is required.") + if unit is None: raise ValueError("A physical unit must be specified.") + + raw_arr = self._rng.choice(a, size=size, replace=replace, p=p) + UnitClass = _resolve_unit(unit) + return UnitClass(raw_arr) - Args: - lam: Expected number of events occurring in a fixed-time interval. - size: Output shape. - unit: The physical dimension to attach. - """ - if not HAS_NUMPY: raise ImportError("NumPy is required.") - if unit is None: raise ValueError("A physical unit must be specified.") - - raw_arr = np.random.poisson(lam, size) - UnitClass = _resolve_unit(unit) - return UnitClass(raw_arr) + def shuffle(self, x: BaseUnit) -> None: + """ + Modifies a physical tensor sequence in-place by shuffling its contents. + + Randomizes the distribution of physical magnitudes along the first axis + without destroying or altering the underlying dimensional DNA. + Vital for dataset splitting or cross-validation in `phaethon.ml`. + + Args: + x: The Phaethon BaseUnit array to be shuffled in-place. + """ + if not HAS_NUMPY: raise ImportError("NumPy is required.") + if not isinstance(x, BaseUnit): + raise TypeError("The input to shuffle must be a Phaethon BaseUnit.") + + self._rng.shuffle(x._value) -@overload -def exponential(scale: float = ..., size: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... -@overload -def exponential(scale: float = ..., size: Any = ..., unit: str = ...) -> BaseUnit: ... + def permutation(self, x: BaseUnit | int) -> BaseUnit | Any: + """ + Randomly permutes a physical tensor, returning a completely NEW copy. + + Unlike `shuffle`, this method does not mutate the original tensor. + If an integer is passed, it returns a permuted range of dimensionless integers. + + Args: + x: A Phaethon BaseUnit array, or an integer to define a range. + + Returns: + A new BaseUnit tensor with randomly permuted elements along the first axis. + """ + if not HAS_NUMPY: raise ImportError("NumPy is required.") + + if isinstance(x, int): + from .units.scalar import Dimensionless + return Dimensionless(self._rng.permutation(x)) + + if not isinstance(x, BaseUnit): + raise TypeError("The input must be an integer or a Phaethon BaseUnit.") + + raw_perm = self._rng.permutation(x.mag) + return x.__class__(raw_perm, context=x.context) -def exponential(scale: float = 1.0, size: Any = None, unit: UnitLike | None = None) -> BaseUnit: - """ - Draws samples from an exponential distribution. - - Ideal for simulating the time between independent physics events, - such as the decay time of radioactive isotopes or thermodynamic - relaxation times. - - Args: - scale: The scale parameter, \u03b2 = 1/\u03bb. Must be non-negative. - size: Output shape. - unit: The physical dimension to attach (typically u.Second). - """ - if not HAS_NUMPY: raise ImportError("NumPy is required.") - if unit is None: raise ValueError("A physical unit must be specified.") - - raw_arr = np.random.exponential(scale, size) - UnitClass = _resolve_unit(unit) - return UnitClass(raw_arr) \ No newline at end of file +_rand = RandomState() + +seed = _rand.seed +uniform = _rand.uniform +normal = _rand.normal +poisson = _rand.poisson +exponential = _rand.exponential +randint = _rand.randint +choice = _rand.choice +shuffle = _rand.shuffle +permutation = _rand.permutation \ No newline at end of file diff --git a/src/phaethon/core/random.pyi b/src/phaethon/core/random.pyi new file mode 100644 index 0000000..f120b9a --- /dev/null +++ b/src/phaethon/core/random.pyi @@ -0,0 +1,59 @@ +import numpy as np +import numpy.typing as npt +from typing import Any, overload +from .._typing import _UnitT +from .base import BaseUnit + +class RandomState: + def __init__(self, seed: int | None = ...) -> None: ... + + def seed(self, seed: int | None = ...) -> None: ... + + @overload + def uniform(self, low: float = ..., high: float = ..., size: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... + @overload + def uniform(self, low: float = ..., high: float = ..., size: Any = ..., unit: str = ...) -> BaseUnit: ... + + @overload + def normal(self, loc: float = ..., scale: float = ..., size: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... + @overload + def normal(self, loc: float = ..., scale: float = ..., size: Any = ..., unit: str = ...) -> BaseUnit: ... + + @overload + def poisson(self, lam: float = ..., size: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... + @overload + def poisson(self, lam: float = ..., size: Any = ..., unit: str = ...) -> BaseUnit: ... + + @overload + def exponential(self, scale: float = ..., size: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... + @overload + def exponential(self, scale: float = ..., size: Any = ..., unit: str = ...) -> BaseUnit: ... + + @overload + def randint(self, low: int, high: int | None = ..., size: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... + @overload + def randint(self, low: int, high: int | None = ..., size: Any = ..., unit: str = ...) -> BaseUnit: ... + + @overload + def choice(self, a: Any, size: Any = ..., replace: bool = ..., p: Any = ..., unit: type[_UnitT] = ...) -> _UnitT: ... + @overload + def choice(self, a: Any, size: Any = ..., replace: bool = ..., p: Any = ..., unit: str = ...) -> BaseUnit: ... + + def shuffle(self, x: BaseUnit) -> None: ... + + @overload + def permutation(self, x: int) -> BaseUnit: ... + @overload + def permutation(self, x: _UnitT) -> _UnitT: ... + +_rand: RandomState + +seed = _rand.seed +uniform = _rand.uniform +normal = _rand.normal +poisson = _rand.poisson +exponential = _rand.exponential +randint = _rand.randint +choice = _rand.choice +shuffle = _rand.shuffle +permutation = _rand.permutation \ No newline at end of file diff --git a/src/phaethon/core/registry.py b/src/phaethon/core/registry.py index 076d948..bab089a 100644 --- a/src/phaethon/core/registry.py +++ b/src/phaethon/core/registry.py @@ -3,7 +3,7 @@ import math from typing import TYPE_CHECKING, overload, Literal from ..exceptions import UnitNotFoundError, DimensionMismatchError, AmbiguousUnitError -from .compat import UnitLike, _Signature, _UnitT +from .._typing import UnitLike, _Signature if TYPE_CHECKING: from .base import BaseUnit diff --git a/src/phaethon/core/schema.py b/src/phaethon/core/schema.py index 2ed9293..4f7a28f 100644 --- a/src/phaethon/core/schema.py +++ b/src/phaethon/core/schema.py @@ -8,11 +8,10 @@ from .registry import ureg from .dataset import Dataset -from .compat import ( - DataFrameLike, is_pandas_df, is_polars_df, _DataFrameT, ErrorAction, InterpolationMethod, - UnitLike, ColumnTarget, AliasRegistry, ContextDict, ImputeMethod, StrictnessLevel, - NumericLike, _UnitT_co, GradTarget, TensorLikeTuple, HAS_RAPIDFUZZ, - require_torch +from .compat import is_pandas_df, is_polars_df, HAS_RAPIDFUZZ, require_torch +from .._typing import ( + DataFrameLike, _DataFrameT, ErrorAction, InterpolationMethod, UnitLike, ColumnTarget, AliasRegistry, + ImputeMethod, StrictnessLevel, NumericLike, _UnitT_co, GradTarget, TensorLikeTuple, ContextDict ) if TYPE_CHECKING: diff --git a/src/phaethon/core/semantics.py b/src/phaethon/core/semantics.py index 734299b..84116e1 100644 --- a/src/phaethon/core/semantics.py +++ b/src/phaethon/core/semantics.py @@ -7,7 +7,7 @@ if TYPE_CHECKING: from .base import BaseUnit - from .compat import ContextDict, NumericLike + from .._typing import ContextDict, NumericLike from ..exceptions import DimensionMismatchError from .compat import HAS_RAPIDFUZZ diff --git a/src/phaethon/core/tensor.py b/src/phaethon/core/tensor.py index 5d334d1..5c99c31 100644 --- a/src/phaethon/core/tensor.py +++ b/src/phaethon/core/tensor.py @@ -5,7 +5,8 @@ from __future__ import annotations from typing import Any, overload -from .compat import UnitLike, HAS_NUMPY, _UnitT +from .compat import HAS_NUMPY +from .._typing import UnitLike, _UnitT from .registry import ureg from .base import BaseUnit diff --git a/src/phaethon/core/units/currency.py b/src/phaethon/core/units/currency.py index 6fb44b3..00ad750 100644 --- a/src/phaethon/core/units/currency.py +++ b/src/phaethon/core/units/currency.py @@ -12,7 +12,7 @@ from ..base import BaseUnit from .. import axioms as _axiom from ..axioms import CtxProxy -from ..compat import ContextDict +from ..._typing import ContextDict def fx_rate(symbol: str, default: float) -> CtxProxy: """ diff --git a/src/phaethon/core/vmath.py b/src/phaethon/core/vmath.py index 3f6bdb9..116bdc5 100644 --- a/src/phaethon/core/vmath.py +++ b/src/phaethon/core/vmath.py @@ -11,7 +11,7 @@ import builtins from typing import Any -from .compat import NumericLike, _NumericT +from .._typing import NumericLike, _NumericT try: import numpy as np diff --git a/src/phaethon/linalg.py b/src/phaethon/linalg.py index 465ca8b..21e2e14 100644 --- a/src/phaethon/linalg.py +++ b/src/phaethon/linalg.py @@ -1,6 +1,7 @@ """ -Phaethon Linear Algebra Module. -Physics-aware wrappers around numpy.linalg operations. +Dimensional Linear Algebra Module. +Provides strict, physics-aware matrix operations preserving +dimensional integrity and isotropic scaling. """ from .core.linalg import inv, det, solve, norm diff --git a/src/phaethon/random.py b/src/phaethon/random.py index 7726c9d..55e2dee 100644 --- a/src/phaethon/random.py +++ b/src/phaethon/random.py @@ -2,6 +2,11 @@ Random Physics Module. Generates stochastic tensors strictly bounded by physical dimensions. """ -from .core.random import uniform, normal, poisson, exponential +from .core.random import ( + seed, uniform, normal, poisson, exponential, randint, choice, shuffle, permutation +) -__all__ = ["uniform", "normal", "poisson", "exponential"] \ No newline at end of file +__all__ = [ + "seed", "uniform", "normal", "poisson", "exponential", + "randint", "choice", "shuffle", "permutation" +] \ No newline at end of file diff --git a/tests/test_physics/test_compute.py b/tests/test_physics/test_compute.py index 9398240..5310678 100644 --- a/tests/test_physics/test_compute.py +++ b/tests/test_physics/test_compute.py @@ -6,7 +6,7 @@ import phaethon as ptn import phaethon.units as u -from phaethon.exceptions import AxiomViolationError, DimensionMismatchError +from phaethon.exceptions import AxiomViolationError def test_native_scientific_compute(): @@ -17,79 +17,118 @@ def test_native_scientific_compute(): raw_matrix = [[1.0, 2.0], [3.0, 4.0]] arr_f32 = ptn.array(raw_matrix, u.Meter, dtype=np.float32, ndmin=3, order="F") - assert arr_f32.shape == (1, 2, 2) - assert arr_f32.mag.dtype == np.float32 - assert arr_f32.mag.flags["F_CONTIGUOUS"] is True - assert arr_f32.dimension == "length" + assert arr_f32.shape == (1, 2, 2), "Array reshaping (ndmin) failed." + assert arr_f32.mag.dtype == np.float32, "Dtype casting failed." + assert arr_f32.mag.flags["F_CONTIGUOUS"] is True, "Fortran memory order was not preserved." + assert arr_f32.dimension == "length", "Dimension resolution failed." massive_data = np.arange(1000, dtype=np.float64) arr_zero_copy = ptn.asarray(massive_data, u.Joule) - assert np.shares_memory(massive_data, arr_zero_copy.mag) - assert arr_zero_copy.dimension == "energy" + assert np.shares_memory(massive_data, arr_zero_copy.mag), "asarray must not copy memory." + assert arr_zero_copy.dimension == "energy", "Dimension resolution failed." masked_raw = ma.masked_array([10.0, -999.0, 30.0], mask=[0, 1, 0]) safe_temp = ptn.asanyarray(masked_raw, u.Kelvin) - assert isinstance(safe_temp.mag, ma.MaskedArray) - assert safe_temp.mag.mask[1] == True + assert isinstance(safe_temp.mag, ma.MaskedArray), "Masked array identity lost during initialization." + assert safe_temp.mag.mask[1] == True, "Masked values were corrupted." A_mat = ptn.array([[4.0, 7.0], [2.0, 6.0]], u.Meter) A_inv = ptn.linalg.inv(A_mat) - assert A_inv.dimension == "linear_attenuation" + assert A_inv.dimension == "linear_attenuation", "Inverse matrix dimension must be 1/L." + identity = A_mat @ A_inv - assert identity.dimension == "dimensionless" - assert np.allclose(identity.mag, np.eye(2)) + assert identity.dimension == "dimensionless", "Matrix multiplied by its inverse must be dimensionless." + assert np.allclose(identity.mag, np.eye(2)), "Inverse calculation yielded incorrect magnitudes." A_3x3 = ptn.array([[1, 2, 3], [0, 1, 4], [5, 6, 0]], u.Meter) det_vol = ptn.linalg.det(A_3x3) - assert det_vol.dimension == "volume" - assert np.isclose(det_vol.mag, 1.0) + assert det_vol.dimension == "volume", "Determinant of 3x3 Length matrix must yield Volume." + assert np.isclose(det_vol.mag, 1.0), "Determinant magnitude calculation failed." M_mass = ptn.array([[10.0, 2.0], [3.0, 5.0]], u.Kilogram) F_force = ptn.array([50.0, 25.0], u.Newton) accel_x = ptn.linalg.solve(M_mass, F_force) - assert accel_x.dimension == "acceleration" - assert isinstance(accel_x, u.MeterPerSecondSquared) + assert accel_x.dimension == "acceleration", "Solving M*a = F must yield Acceleration." + assert isinstance(accel_x, u.MeterPerSecondSquared), "Incorrect base unit resolution for acceleration." F_mat = ptn.array([[50.0, 10.0], [10.0, 25.0]], u.Newton) F_vec = ptn.array([100.0, 50.0], u.Newton) ratio_x = ptn.linalg.solve(F_mat, F_vec) - assert ratio_x.dimension == "dimensionless" + assert ratio_x.dimension == "dimensionless", "Solving Force/Force must yield dimensionless scalar." vec_v = ptn.array([3.0, 4.0], u.MeterPerSecond) mag_v = ptn.linalg.norm(vec_v) - assert mag_v.dimension == "speed" - assert np.isclose(mag_v.mag, 5.0) + assert mag_v.dimension == "speed", "Vector norm must preserve the original dimension." + assert np.isclose(mag_v.mag, 5.0), "L2 Norm calculation failed." mat_norm = ptn.linalg.norm(M_mass, ord="fro", keepdims=True) - assert mat_norm.dimension == "mass" - assert mat_norm.shape == (1, 1) + assert mat_norm.dimension == "mass", "Frobenius norm must preserve the original dimension." + assert mat_norm.shape == (1, 1), "keepdims argument was ignored in linalg.norm." + + ptn.random.seed(42) + rep_1 = ptn.random.uniform(0, 100, size=5, unit=u.Volt) + ptn.random.seed(42) + rep_2 = ptn.random.uniform(0, 100, size=5, unit=u.Volt) + assert np.allclose(rep_1.mag, rep_2.mag), "Isolated RNG seed failed to produce deterministic results." + assert rep_1.dimension == "electric_potential", "Unit injection failed in random.uniform." p_uni = ptn.random.uniform(low=10.5, high=20.5, size=(5, 5), unit=u.Pascal) - assert p_uni.shape == (5, 5) - assert p_uni.dimension == "pressure" - assert np.all((p_uni.mag >= 10.5) & (p_uni.mag <= 20.5)) + assert p_uni.shape == (5, 5), "Random tensor shape mismatch." + assert p_uni.dimension == "pressure", "Dimension mapping failed." + assert np.all((p_uni.mag >= 10.5) & (p_uni.mag <= 20.5)), "Uniform distribution bounds violated." mass_norm = ptn.random.normal(loc=100.0, scale=2.5, size=10, unit="kg") - assert mass_norm.dimension == "mass" - assert mass_norm.shape == (10,) - - bq_decay = ptn.random.poisson(lam=50.0, size=(100,), unit=u.Becquerel) - assert bq_decay.dimension == "radioactivity" - assert bq_decay.mag.dtype in (np.int32, np.int64) + assert mass_norm.dimension == "mass", "String alias mapping failed in random.normal." + assert mass_norm.shape == (10,), "Output shape mismatch in normal distribution." t_half = ptn.random.exponential(scale=1.5, size=(3, 3, 3), unit=u.Second) - assert t_half.dimension == "time" - assert t_half.ndim == 3 + assert t_half.dimension == "time", "Dimension mapping failed." + assert np.all(t_half.mag >= 0), "Exponential distribution generated impossible negative values." + + bq_decay = ptn.random.poisson(lam=50.0, size=(100,), unit=u.Becquerel) + assert bq_decay.dimension == "radioactivity", "Dimension mapping failed." + assert np.issubdtype(bq_decay.mag.dtype, np.integer), "Poisson events must be discrete integers." + + spin_states = ptn.random.randint(-1, 2, size=(50,), unit=u.Dimensionless) + assert np.issubdtype(spin_states.mag.dtype, np.integer), "Randint failed to produce integers." + assert np.all((spin_states.mag >= -1) & (spin_states.mag < 2)), "Randint boundary violation." + + allowed_velocities = [300.0, 400.0, 500.0] + gas_particles = ptn.random.choice(allowed_velocities, size=100, unit=u.MeterPerSecond) + assert gas_particles.dimension == "speed", "Choice dimension mapping failed." + assert np.all(np.isin(gas_particles.mag, allowed_velocities)), "Choice generated illegal states." + + ptn.random.seed(99) + ordered_energy = ptn.array(np.arange(100.0), u.Joule) + ordered_copy = ordered_energy.mag.copy() + + ptn.random.shuffle(ordered_energy) + assert not np.array_equal(ordered_energy.mag, ordered_copy), "Shuffle failed to randomize array." + assert np.array_equal(np.sort(ordered_energy.mag), ordered_copy), "Shuffle destroyed array elements." + assert ordered_energy.dimension == "energy", "Shuffle stripped physical dimensions." + + original_force = ptn.array([10.0, 20.0, 30.0], u.Newton) + permuted_force = ptn.random.permutation(original_force) + assert np.array_equal(original_force.mag, [10.0, 20.0, 30.0]), "Permutation mutated original tensor." + assert np.array_equal(np.sort(permuted_force.mag), [10.0, 20.0, 30.0]), "Permutation lost elements." + assert permuted_force.dimension == "force", "Permutation stripped physical dimensions." + + idx_range = ptn.random.permutation(10) + assert idx_range.dimension == "dimensionless", "Integer permutation must return Dimensionless unit." + assert set(idx_range.mag) == set(range(10)), "Integer permutation failed to generate valid range." with pytest.raises(np.linalg.LinAlgError, match="square"): ptn.linalg.inv(ptn.array([[1, 2, 3], [4, 5, 6]], u.Meter)) + with pytest.raises(np.linalg.LinAlgError, match="at least two-dimensional"): + ptn.linalg.inv(ptn.array([1, 2, 3], u.Meter)) + mat_db = ptn.array([[30, 30], [30, 30]], u.Decibel) with pytest.raises(AxiomViolationError, match="You cannot exponentiate"): ptn.linalg.det(mat_db) with pytest.raises(ValueError, match="physical unit must be specified"): ptn.random.uniform(size=5) - - with pytest.raises(np.linalg.LinAlgError, match="at least two-dimensional"): - ptn.linalg.inv(ptn.array([1, 2, 3], u.Meter)) \ No newline at end of file + + with pytest.raises(TypeError, match="must be a Phaethon BaseUnit"): + ptn.random.shuffle([1, 2, 3]) \ No newline at end of file