"""Structured dataclasses to replace raw numpy arrays.
Provides type-safe dataclasses that improve code readability and maintain
semantic meaning while supporting arithmetic operations and numpy integration.
"""
import math
from dataclasses import dataclass
from typing import Self, Union, overload
import numpy as np
[docs]
@dataclass(frozen=True)
class Point2D:
"""A 2D point with validation."""
x: float
y: float
def __array__(self, dtype: np.dtype | None = None, copy: bool | None = None) -> np.ndarray:
"""Enable numpy array operations."""
arr = np.array([self.x, self.y])
if dtype is not None:
arr = arr.astype(dtype, copy=False if copy is None else copy)
return arr
[docs]
def to_array(self) -> np.ndarray:
"""Convert to numpy array."""
return np.array([self.x, self.y])
[docs]
@classmethod
def from_array(cls, arr: np.ndarray) -> "Point2D":
"""Create Point2D from numpy array."""
if arr.shape != (2,):
raise ValueError(f"Expected array shape (2,), got {arr.shape}")
return cls(x=float(arr[0]), y=float(arr[1]))
[docs]
def isclose(self, other: "Point2D", rtol: float = 1e-9, atol: float = 1e-12) -> bool:
"""Compare with tolerance."""
return bool(
np.isclose(self.x, other.x, rtol=rtol, atol=atol) and np.isclose(self.y, other.y, rtol=rtol, atol=atol)
)
[docs]
def assert_close(self, other: "Point2D", rtol: float = 1e-9, atol: float = 1e-12, msg: str = "") -> None:
"""Assert close with custom error message."""
if not self.isclose(other, rtol=rtol, atol=atol):
error_msg = f"{self} != {other} (rtol={rtol}, atol={atol})"
if msg:
error_msg = f"{msg}: {error_msg}"
raise AssertionError(error_msg)
def __sub__(self, other: "Point2D | float") -> "Point2D":
"""Subtract point or scalar from point."""
if isinstance(other, Point2D):
return Point2D(self.x - other.x, self.y - other.y)
if isinstance(other, (int, float)):
return Point2D(self.x - other, self.y - other)
return NotImplemented
def __add__(self, other: "Point2D | float") -> "Point2D":
"""Add two points/vectors or add scalar to point."""
if isinstance(other, Point2D):
return Point2D(self.x + other.x, self.y + other.y)
if isinstance(other, (int, float)):
return Point2D(self.x + other, self.y + other)
return NotImplemented
def __mul__(self, scalar: float) -> "Point2D":
"""Multiply point by a scalar."""
return Point2D(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar: float) -> "Point2D":
"""Multiply point by a scalar (reverse order)."""
return self.__mul__(scalar)
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs: object, **kwargs: object) -> "Point2D | None":
"""Handle numpy universal functions to maintain Point2D type."""
if ufunc == np.multiply and method == "__call__" and len(inputs) == 2:
# Handle scalar * point multiplication
if isinstance(inputs[0], (int, float, np.number)) and isinstance(inputs[1], Point2D):
return inputs[1] * float(inputs[0]) # point * scalar
if isinstance(inputs[0], Point2D) and isinstance(inputs[1], (int, float, np.number)):
return inputs[0] * float(inputs[1]) # point * scalar
# For other operations, defer to numpy
return NotImplemented
[docs]
def serialize(self) -> dict:
"""Serialize to dictionary representation."""
return {"x": float(self.x), "y": float(self.y)}
[docs]
@classmethod
def deserialize(cls, data: dict) -> "Point2D":
"""Deserialize from dictionary representation."""
return cls(data["x"], data["y"])
[docs]
@dataclass(frozen=True)
class Point3D:
"""A 3D point with validation."""
x: float
y: float
z: float
def __array__(self, dtype: np.dtype | None = None, copy: bool | None = None) -> np.ndarray:
"""Enable numpy array operations."""
arr = np.array([self.x, self.y, self.z])
if dtype is not None:
arr = arr.astype(dtype, copy=False if copy is None else copy)
return arr
[docs]
def to_array(self) -> np.ndarray:
"""Convert to numpy array."""
return np.array([self.x, self.y, self.z])
[docs]
def to_homogeneous(self) -> np.ndarray:
"""Convert to homogeneous coordinates [x, y, z, 1]."""
return np.array([self.x, self.y, self.z, 1.0])
[docs]
@classmethod
def from_array(cls, arr: np.ndarray) -> "Point3D":
"""Create Point3D from numpy array."""
if arr.shape != (3,):
raise ValueError(f"Expected array shape (3,), got {arr.shape}")
return cls(x=float(arr[0]), y=float(arr[1]), z=float(arr[2]))
[docs]
def distance_to(self, other: "Point3D") -> float:
"""Calculate Euclidean distance to another point."""
return np.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2)
[docs]
def isclose(self, other: "Point3D", rtol: float = 1e-9, atol: float = 1e-12) -> bool:
"""Compare with tolerance."""
return bool(
np.isclose(self.x, other.x, rtol=rtol, atol=atol)
and np.isclose(self.y, other.y, rtol=rtol, atol=atol)
and np.isclose(self.z, other.z, rtol=rtol, atol=atol)
)
[docs]
def assert_close(self, other: "Point3D", rtol: float = 1e-9, atol: float = 1e-12, msg: str = "") -> None:
"""Assert close with custom error message."""
if not self.isclose(other, rtol=rtol, atol=atol):
error_msg = f"{self} != {other} (rtol={rtol}, atol={atol})"
if msg:
error_msg = f"{msg}: {error_msg}"
raise AssertionError(error_msg)
@overload
def __sub__(self, other: Union["Point3D", "Position3D"]) -> "Vector3D": ...
@overload
def __sub__(self, other: Union["Vector3D", "Direction3D"]) -> "Point3D": ...
@overload
def __sub__(self, other: float) -> "Point3D": ...
def __sub__(self, other):
"""Subtract point, position, vector, or scalar from point."""
if isinstance(other, (Point3D, Position3D)):
return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z)
if isinstance(other, (Vector3D, Direction3D)):
return Point3D(self.x - other.x, self.y - other.y, self.z - other.z)
if isinstance(other, (int, float)):
return Point3D(self.x - other, self.y - other, self.z - other)
return NotImplemented
def __add__(self, other: "Vector3D | float") -> "Point3D":
"""Add a vector or scalar to a point to get a new point."""
if isinstance(other, Vector3D):
return Point3D(self.x + other.x, self.y + other.y, self.z + other.z)
if isinstance(other, (int, float)):
return Point3D(self.x + other, self.y + other, self.z + other)
return NotImplemented
def __mul__(self, scalar: float) -> "Point3D":
"""Multiply point by a scalar."""
return Point3D(self.x * scalar, self.y * scalar, self.z * scalar)
def __rmul__(self, scalar: float) -> "Point3D":
"""Multiply point by a scalar (reverse order)."""
return self.__mul__(scalar)
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs: object, **kwargs: object) -> "Point3D | None":
"""Handle numpy universal functions to maintain Point3D type."""
if ufunc == np.multiply and method == "__call__" and len(inputs) == 2:
# Handle scalar * point multiplication
if isinstance(inputs[0], (int, float, np.number)) and isinstance(inputs[1], Point3D):
return inputs[1] * float(inputs[0]) # point * scalar
if isinstance(inputs[0], Point3D) and isinstance(inputs[1], (int, float, np.number)):
return inputs[0] * float(inputs[1]) # point * scalar
# For other operations, defer to numpy
return NotImplemented
[docs]
def to_position3d(self) -> "Position3D":
"""Convert to Position3D (homogeneous coordinates with w=1)."""
return Position3D(x=self.x, y=self.y, z=self.z)
[docs]
@dataclass(frozen=True)
class Vector3D:
"""A 3D vector with validation and common operations."""
x: float
y: float
z: float
def __array__(self, dtype: np.dtype | None = None, copy: bool | None = None) -> np.ndarray:
"""Enable numpy array operations."""
arr = np.array([self.x, self.y, self.z])
if dtype is not None:
arr = arr.astype(dtype, copy=False if copy is None else copy)
return arr
[docs]
def to_array(self) -> np.ndarray:
"""Convert to numpy array for compatibility."""
return np.array([self.x, self.y, self.z])
[docs]
def to_homogeneous(self) -> np.ndarray:
"""Convert to homogeneous coordinates [x, y, z, 0]."""
return np.array([self.x, self.y, self.z, 0.0])
[docs]
@classmethod
def from_array(cls, arr: np.ndarray) -> "Vector3D":
"""Create Vector3D from numpy array."""
if arr.shape != (3,):
raise ValueError(f"Expected array shape (3,), got {arr.shape}")
return cls(x=float(arr[0]), y=float(arr[1]), z=float(arr[2]))
[docs]
def magnitude(self) -> float:
"""Calculate vector magnitude."""
return np.sqrt(self.x**2 + self.y**2 + self.z**2)
[docs]
def normalize(self) -> "Vector3D":
"""Return normalized vector."""
mag = self.magnitude()
if mag == 0:
raise ValueError("Cannot normalize zero vector")
return Vector3D(self.x / mag, self.y / mag, self.z / mag)
@overload
def dot(self, other: "Direction3D") -> float: ...
@overload
def dot(self, other: "Vector3D") -> float: ...
[docs]
def dot(self, other) -> float:
"""Calculate dot product with another vector."""
return self.x * other.x + self.y * other.y + self.z * other.z
[docs]
def cross(self, other: "Vector3D") -> "Vector3D":
"""Calculate cross product with another vector."""
return Vector3D(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x,
)
[docs]
def to_direction3d(self) -> "Direction3D":
"""Convert to Direction3D."""
return Direction3D(self.x, self.y, self.z)
[docs]
def isclose(self, other: "Vector3D", rtol: float = 1e-9, atol: float = 1e-12) -> bool:
"""Compare with tolerance."""
return bool(
np.isclose(self.x, other.x, rtol=rtol, atol=atol)
and np.isclose(self.y, other.y, rtol=rtol, atol=atol)
and np.isclose(self.z, other.z, rtol=rtol, atol=atol)
)
[docs]
def assert_close(self, other: "Vector3D", rtol: float = 1e-9, atol: float = 1e-12, msg: str = "") -> None:
"""Assert close with custom error message."""
if not self.isclose(other, rtol=rtol, atol=atol):
error_msg = f"{self} != {other} (rtol={rtol}, atol={atol})"
if msg:
error_msg = f"{msg}: {error_msg}"
raise AssertionError(error_msg)
def __rmatmul__(self, other: np.ndarray) -> "Vector3D":
"""Enable matrix multiplication: matrix @ vector."""
if isinstance(other, np.ndarray):
result = other @ np.array(self)
return Vector3D.from_array(result)
return NotImplemented
def __add__(self, other: "Vector3D | float") -> "Vector3D":
"""Add two vectors or add scalar to vector."""
if isinstance(other, Vector3D):
return Vector3D(self.x + other.x, self.y + other.y, self.z + other.z)
if isinstance(other, (int, float)):
return Vector3D(self.x + other, self.y + other, self.z + other)
return NotImplemented
def __sub__(self, other: "Vector3D | float") -> "Vector3D":
"""Subtract vector or scalar from vector."""
if isinstance(other, Vector3D):
return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z)
if isinstance(other, (int, float)):
return Vector3D(self.x - other, self.y - other, self.z - other)
return NotImplemented
def __mul__(self, scalar: float) -> "Vector3D":
"""Multiply vector by a scalar."""
return Vector3D(self.x * scalar, self.y * scalar, self.z * scalar)
def __rmul__(self, scalar: float) -> "Vector3D":
"""Multiply vector by a scalar (reverse order)."""
return self.__mul__(scalar)
def __truediv__(self, scalar: float) -> "Vector3D":
"""Divide vector by a scalar."""
if scalar == 0:
raise ZeroDivisionError("Cannot divide vector by zero")
return Vector3D(self.x / scalar, self.y / scalar, self.z / scalar)
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs: object, **kwargs: object) -> "Vector3D | None":
"""Handle numpy universal functions to maintain Vector3D type."""
if ufunc == np.multiply and method == "__call__":
# Handle scalar * vector multiplication
if len(inputs) == 2:
if isinstance(inputs[0], (int, float, np.number)) and isinstance(inputs[1], Vector3D):
return inputs[1] * float(inputs[0]) # vector * scalar
if isinstance(inputs[0], Vector3D) and isinstance(inputs[1], (int, float, np.number)):
return inputs[0] * float(inputs[1]) # vector * scalar
elif (
ufunc == np.matmul
and method == "__call__"
and len(inputs) == 2
and isinstance(inputs[0], np.ndarray)
and isinstance(inputs[1], Vector3D)
):
# Handle matrix @ vector multiplication
# Call the __rmatmul__ method directly
return inputs[1].__rmatmul__(inputs[0])
# For other operations, defer to numpy
return NotImplemented
[docs]
def serialize(self) -> dict:
"""Serialize to dictionary representation."""
return {"x": float(self.x), "y": float(self.y), "z": float(self.z)}
[docs]
@classmethod
def deserialize(cls, data: dict) -> "Vector3D":
"""Deserialize from dictionary representation."""
return cls(data["x"], data["y"], data["z"])
[docs]
@dataclass(frozen=True)
class Position3D:
"""A 3D position that can be converted to homogeneous coordinates [x,y,z,1]."""
x: float
y: float
z: float
def __array__(self, dtype: np.dtype | None = None, copy: bool | None = None) -> np.ndarray:
"""Enable numpy array operations."""
arr = np.array([self.x, self.y, self.z, 1.0])
if dtype is not None:
arr = arr.astype(dtype, copy=False if copy is None else copy)
return arr
[docs]
def to_array(self) -> np.ndarray:
"""Convert to homogeneous 4D array [x,y,z,1]."""
return np.array([self.x, self.y, self.z, 1.0])
[docs]
@classmethod
def from_array(cls, arr: np.ndarray) -> "Position3D":
"""Create from 4D homogeneous array [x,y,z,1] or 3D array [x,y,z]."""
if arr.shape == (4,):
w = float(arr[3])
# w == 1 (point) or w == 0 (direction) → take xyz directly; otherwise de-homogenize.
if not math.isclose(w, 1.0) and not math.isclose(w, 0.0):
return cls(arr[0] / w, arr[1] / w, arr[2] / w)
return cls(arr[0], arr[1], arr[2])
if arr.shape == (3,):
return cls(arr[0], arr[1], arr[2])
raise ValueError(f"Expected array shape (3,) or (4,), got {arr.shape}")
[docs]
def to_point3d(self) -> Point3D:
"""Convert to Point3D."""
return Point3D(self.x, self.y, self.z)
[docs]
def isclose(self, other: "Position3D", rtol: float = 1e-9, atol: float = 1e-12) -> bool:
"""Compare with tolerance."""
return bool(
np.isclose(self.x, other.x, rtol=rtol, atol=atol)
and np.isclose(self.y, other.y, rtol=rtol, atol=atol)
and np.isclose(self.z, other.z, rtol=rtol, atol=atol)
)
[docs]
def assert_close(self, other: "Position3D", rtol: float = 1e-9, atol: float = 1e-12, msg: str = "") -> None:
"""Assert close with custom error message."""
if not self.isclose(other, rtol=rtol, atol=atol):
error_msg = f"{self} != {other} (rtol={rtol}, atol={atol})"
if msg:
error_msg = f"{msg}: {error_msg}"
raise AssertionError(error_msg)
def __matmul__(self, other: np.ndarray) -> "Position3D":
"""Enable matrix multiplication: position @ matrix."""
if isinstance(other, np.ndarray):
result = np.array(self) @ other
return Position3D.from_array(result)
return NotImplemented
def __rmatmul__(self, other: np.ndarray) -> "Position3D":
"""Enable matrix multiplication: matrix @ position (most common case)."""
if isinstance(other, np.ndarray):
result = other @ np.array(self)
return Position3D.from_array(result)
return NotImplemented
@overload
def __sub__(self, other: Union["Position3D", Point3D]) -> Vector3D: ...
@overload
def __sub__(self, other: Union[Vector3D, "Direction3D"]) -> "Position3D": ...
@overload
def __sub__(self, other: float) -> "Position3D": ...
def __sub__(self, other):
"""Subtract position, point, vector, direction, or scalar from position."""
if isinstance(other, (Position3D, Point3D)):
return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z)
if isinstance(other, (Vector3D, Direction3D)):
return Position3D(self.x - other.x, self.y - other.y, self.z - other.z)
if isinstance(other, (int, float)):
return Position3D(self.x - other, self.y - other, self.z - other)
return NotImplemented
def __add__(self, other: "Vector3D | Direction3D | Point3D | float") -> "Position3D":
"""Add a vector, direction, point, or scalar to a position to get a new position."""
if isinstance(other, (Vector3D, Direction3D, Point3D)):
return Position3D(self.x + other.x, self.y + other.y, self.z + other.z)
if isinstance(other, (int, float)):
return Position3D(self.x + other, self.y + other, self.z + other)
return NotImplemented
def __mul__(self, scalar: float) -> "Position3D":
"""Multiply position by a scalar."""
return Position3D(self.x * scalar, self.y * scalar, self.z * scalar)
def __rmul__(self, scalar: float) -> "Position3D":
"""Multiply position by a scalar (reverse order)."""
return self.__mul__(scalar)
def __radd__(self, other: "Vector3D | Direction3D | Point3D | float") -> "Position3D":
"""Add position to a scalar (reverse order)."""
return self.__add__(other)
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs: object, **kwargs: object) -> "Position3D | None":
"""Handle numpy universal functions to maintain Position3D type."""
if ufunc == np.multiply and method == "__call__":
# Handle scalar * position multiplication
if len(inputs) == 2:
if isinstance(inputs[0], (int, float, np.number)) and isinstance(inputs[1], Position3D):
return inputs[1] * float(inputs[0]) # position * scalar
if isinstance(inputs[0], Position3D) and isinstance(inputs[1], (int, float, np.number)):
return inputs[0] * float(inputs[1]) # position * scalar
elif (
ufunc == np.matmul
and method == "__call__"
and len(inputs) == 2
and isinstance(inputs[0], np.ndarray)
and isinstance(inputs[1], Position3D)
):
# Handle matrix @ position multiplication
# Call the __rmatmul__ method directly
return inputs[1].__rmatmul__(inputs[0])
# For other operations, defer to numpy
return NotImplemented
[docs]
@classmethod
def from_point3d(cls, point: Point3D) -> "Position3D":
"""Create Position3D from Point3D."""
return cls(point.x, point.y, point.z)
[docs]
def distance_to(self, other: "Position3D") -> float:
"""Calculate Euclidean distance to another position."""
return np.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2)
[docs]
def serialize(self) -> dict:
"""Serialize to dictionary representation."""
return {"x": float(self.x), "y": float(self.y), "z": float(self.z)}
[docs]
@classmethod
def deserialize(cls, data: dict) -> "Position3D":
"""Deserialize from dictionary representation."""
return cls(data["x"], data["y"], data["z"])
[docs]
@dataclass(frozen=True)
class Direction3D:
"""A 3D direction vector that converts to homogeneous [x,y,z,0]."""
x: float
y: float
z: float
def __array__(self, dtype: np.dtype | None = None, copy: bool | None = None) -> np.ndarray:
"""Enable numpy array operations."""
arr = np.array([self.x, self.y, self.z, 0.0])
if dtype is not None:
arr = arr.astype(dtype, copy=False if copy is None else copy)
return arr
[docs]
def to_array(self) -> np.ndarray:
"""Convert to homogeneous 4D array [x,y,z,0]."""
return np.array([self.x, self.y, self.z, 0.0])
[docs]
@classmethod
def from_array(cls, arr: np.ndarray) -> "Direction3D":
"""Create from 4D homogeneous array [x,y,z,0] or 3D array [x,y,z]."""
if arr.shape in {(4,), (3,)}:
return cls(arr[0], arr[1], arr[2])
raise ValueError(f"Expected array shape (3,) or (4,), got {arr.shape}")
[docs]
def magnitude(self) -> float:
"""Calculate vector magnitude."""
return np.sqrt(self.x**2 + self.y**2 + self.z**2)
[docs]
def normalize(self) -> "Direction3D":
"""Return normalized direction vector."""
mag = self.magnitude()
if mag == 0:
raise ValueError("Cannot normalize zero vector")
return Direction3D(self.x / mag, self.y / mag, self.z / mag)
@overload
def dot(self, other: "Direction3D") -> float: ...
@overload
def dot(self, other: Vector3D) -> float: ...
[docs]
def dot(self, other) -> float:
"""Calculate dot product with another direction."""
return self.x * other.x + self.y * other.y + self.z * other.z
[docs]
def cross(self, other: "Direction3D") -> "Direction3D":
"""Calculate cross product with another direction."""
return Direction3D(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x,
)
[docs]
def to_vector3d(self) -> Vector3D:
"""Convert to Vector3D."""
return Vector3D(self.x, self.y, self.z)
[docs]
def isclose(self, other: "Direction3D", rtol: float = 1e-9, atol: float = 1e-12) -> bool:
"""Compare with tolerance."""
return bool(
np.isclose(self.x, other.x, rtol=rtol, atol=atol)
and np.isclose(self.y, other.y, rtol=rtol, atol=atol)
and np.isclose(self.z, other.z, rtol=rtol, atol=atol)
)
[docs]
def assert_close(self, other: "Direction3D", rtol: float = 1e-9, atol: float = 1e-12, msg: str = "") -> None:
"""Assert close with custom error message."""
if not self.isclose(other, rtol=rtol, atol=atol):
error_msg = f"{self} != {other} (rtol={rtol}, atol={atol})"
if msg:
error_msg = f"{msg}: {error_msg}"
raise AssertionError(error_msg)
def __rmatmul__(self, other: np.ndarray) -> "Direction3D":
"""Enable matrix multiplication: matrix @ direction."""
if isinstance(other, np.ndarray):
result = other @ np.array(self)
return Direction3D.from_array(result)
return NotImplemented
def __mul__(self, scalar: float) -> "Direction3D":
"""Multiply direction by a scalar."""
return Direction3D(self.x * scalar, self.y * scalar, self.z * scalar)
def __rmul__(self, scalar: float) -> "Direction3D":
"""Multiply direction by a scalar (reverse order)."""
return self.__mul__(scalar)
def __truediv__(self, scalar: float) -> "Direction3D":
"""Divide direction by a scalar."""
if scalar == 0:
raise ZeroDivisionError("Cannot divide direction by zero")
return Direction3D(self.x / scalar, self.y / scalar, self.z / scalar)
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs: object, **kwargs: object) -> "Direction3D | None":
"""Handle numpy universal functions to maintain Direction3D type."""
if ufunc == np.multiply and method == "__call__":
# Handle scalar * direction multiplication
if len(inputs) == 2:
if isinstance(inputs[0], (int, float, np.number)) and isinstance(inputs[1], Direction3D):
return inputs[1] * float(inputs[0]) # direction * scalar
if isinstance(inputs[0], Direction3D) and isinstance(inputs[1], (int, float, np.number)):
return inputs[0] * float(inputs[1]) # direction * scalar
elif (
ufunc == np.matmul
and method == "__call__"
and len(inputs) == 2
and isinstance(inputs[0], np.ndarray)
and isinstance(inputs[1], Direction3D)
):
# Handle matrix @ direction multiplication
# Call the __rmatmul__ method directly
return inputs[1].__rmatmul__(inputs[0])
# For other operations, defer to numpy
return NotImplemented
def __add__(self, other: "Direction3D | float") -> "Direction3D":
"""Add two directions or add scalar to direction."""
if isinstance(other, Direction3D):
return Direction3D(self.x + other.x, self.y + other.y, self.z + other.z)
if isinstance(other, (int, float)):
return Direction3D(self.x + other, self.y + other, self.z + other)
return NotImplemented
def __sub__(self, other: "Direction3D | float") -> "Direction3D":
"""Subtract direction or scalar from direction."""
if isinstance(other, Direction3D):
return Direction3D(self.x - other.x, self.y - other.y, self.z - other.z)
if isinstance(other, (int, float)):
return Direction3D(self.x - other, self.y - other, self.z - other)
return NotImplemented
[docs]
def serialize(self) -> dict:
"""Serialize to dictionary representation."""
return {"x": float(self.x), "y": float(self.y), "z": float(self.z)}
[docs]
@classmethod
def deserialize(cls, data: dict) -> "Direction3D":
"""Deserialize from dictionary representation."""
return cls(data["x"], data["y"], data["z"])
[docs]
@dataclass(frozen=True)
class Ray:
"""A ray defined by origin point and direction vector."""
origin: Point3D
direction: Direction3D
[docs]
def point_at(self, t: float) -> Point3D:
"""Get point along ray at parameter t."""
return Point3D(
self.origin.x + t * self.direction.x,
self.origin.y + t * self.direction.y,
self.origin.z + t * self.direction.z,
)
[docs]
@classmethod
def from_two_points(cls, p1: Point3D, p2: Point3D) -> "Ray":
"""Create ray from two points."""
direction_vec = Direction3D(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
return cls(origin=p1, direction=direction_vec.normalize())
[docs]
@dataclass(frozen=True)
class IntersectionResult:
"""Result of ray-surface intersection calculation."""
intersects: bool
point: Point3D | None = None
distance: float | None = None
surface_normal: Direction3D | None = None
[docs]
@classmethod
def no_intersection(cls) -> "IntersectionResult":
"""Create result indicating no intersection."""
return cls(intersects=False)
[docs]
@classmethod
def intersection_at(
cls, point: Point3D, distance: float, normal: Direction3D | None = None
) -> "IntersectionResult":
"""Create result indicating intersection at given point."""
return cls(intersects=True, point=point, distance=distance, surface_normal=normal)
[docs]
class RotationMatrix(np.ndarray):
"""A 3x3 rotation matrix that validates its mathematical properties."""
def __new__(cls, input_array: np.ndarray | list, validate_handedness: bool = True) -> Self:
"""Create a new rotation matrix from input array or list with validation.
Args:
input_array: 3x3 array or nested list representing the rotation matrix
validate_handedness: If True, enforce right-handed coordinate system (det=+1)
If False, allow left-handed systems (det=-1) for legacy compatibility
"""
# Convert input to numpy array (handles both arrays and lists)
obj = np.asarray(input_array, dtype=np.float64).view(cls)
if obj.shape != (3, 3):
raise ValueError(f"RotationMatrix must be 3x3, got shape {obj.shape}")
# Validate it's a proper rotation matrix
cls._validate(obj, validate_handedness=validate_handedness)
return obj
@staticmethod
def _validate(matrix: np.ndarray, validate_handedness: bool = True) -> None:
"""Validate that matrix is a proper rotation matrix.
Args:
matrix: The matrix to validate
validate_handedness: If True, enforce right-handed coordinate system
"""
# Check if matrix is orthonormal (R^T * R = I)
should_be_identity = matrix.T @ matrix
if not np.allclose(should_be_identity, np.eye(3), atol=1e-6):
raise ValueError("Matrix is not orthonormal")
# Check determinant is +1 (right-handed) or -1 (left-handed)
det = np.linalg.det(matrix)
if validate_handedness:
if not np.isclose(det, 1.0, atol=1e-6):
raise ValueError(f"Matrix determinant is {det:.6f}, expected +1.0 for right-handed rotation")
# Allow both +1 and -1 determinants (right-handed and left-handed)
elif not (np.isclose(det, 1.0, atol=1e-6) or np.isclose(det, -1.0, atol=1e-6)):
raise ValueError(f"Matrix determinant is {det:.6f}, expected ±1.0 for rotation matrix")
[docs]
@classmethod
def identity(cls) -> "RotationMatrix":
"""Create an identity rotation matrix."""
return cls(np.eye(3))
[docs]
@classmethod
def deserialize(cls, data: dict) -> "RotationMatrix":
"""Create RotationMatrix from serialized data with automatic handedness detection.
Tries strict right-handed validation first, falls back to allowing left-handed
matrices for legacy compatibility.
Args:
data: Matrix data (list or array)
"""
try:
# Try with strict right-handed validation first
return cls(data, validate_handedness=True)
except ValueError:
# Fall back to allowing left-handed matrices for legacy data
return cls(data, validate_handedness=False)
[docs]
@dataclass(frozen=True)
class ScreenGeometry:
"""Physical screen dimensions and orientation for visualization.
Defines a screen centered at the origin on the specified plane.
"""
width: float # full screen width in mm
height: float # full screen height in mm
plane: str # "xz", "xy", or "yz"
def __post_init__(self) -> None:
"""Validate plane parameter."""
valid_planes = ("xz", "xy", "yz")
if self.plane not in valid_planes:
raise ValueError(f"plane must be one of {valid_planes}, got '{self.plane}'")