Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 05805b9249a60b9da31b9a2d5890c26c79c36fa8d42b3a33d47f735c9afce841
1"""Right-handed rigid geometry in millimetres, with explicit object-to-world maps.23These immutable host contracts import without Warp. Rotations are row-major4matrices acting on column vectors; an acquisition origin is its first pixel5centre. Small pose transfers belong at the optimisation control boundary.6"""78from __future__ import annotations910import math11from dataclasses import dataclass12from typing import cast1314from dpt.contracts import ContractError, finite_tuple1516type Vector3 = tuple[float, float, float]17type Matrix3 = tuple[float, float, float, float, float, float, float, float, float]18IDENTITY: Matrix3 = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)192021def vector3(value: Vector3, name: str) -> Vector3:22return cast(Vector3, finite_tuple(value, name, minimum=None, length=3))232425def dot(a: Vector3, b: Vector3) -> float:26return sum(x * y for x, y in zip(a, b, strict=True))272829def cross(a: Vector3, b: Vector3) -> Vector3:30return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0])313233def matvec(matrix: Matrix3, vector: Vector3) -> Vector3:34return cast(35Vector3, tuple(sum(matrix[3 * i + j] * vector[j] for j in range(3)) for i in range(3))36)373839def transpose(matrix: Matrix3) -> Matrix3:40return cast(Matrix3, tuple(matrix[3 * j + i] for i in range(3) for j in range(3)))414243def matmul(left: Matrix3, right: Matrix3) -> Matrix3:44return cast(45Matrix3,46tuple(47sum(left[3 * i + k] * right[3 * k + j] for k in range(3))48for i in range(3)49for j in range(3)50),51)525354def rotation_matrix(value: Matrix3, name: str = "rotation") -> Matrix3:55value = cast(Matrix3, finite_tuple(value, name, minimum=None, length=9))56product = matmul(transpose(value), value)57if max(abs(a - b) for a, b in zip(product, IDENTITY, strict=True)) > 1e-10:58raise ContractError(f"{name} must be orthonormal to 1e-10")59columns = [(value[i], value[i + 3], value[i + 6]) for i in range(3)]60if dot(cross(columns[0], columns[1]), columns[2]) < 0.0:61raise ContractError(f"{name} must be right-handed")62return cast(Matrix3, tuple(float(x) for x in value))636465# region book:geometry-rigid-transform66@dataclass(frozen=True, slots=True)67class RigidTransform:68"""T_WO maps object points into world coordinates; translation is in mm."""6970rotation: Matrix3 = IDENTITY71translation_mm: Vector3 = (0.0, 0.0, 0.0)7273def __post_init__(self) -> None:74object.__setattr__(self, "rotation", rotation_matrix(self.rotation))75object.__setattr__(self, "translation_mm", vector3(self.translation_mm, "translation"))7677def point(self, point_mm: Vector3) -> Vector3:78rotated = matvec(self.rotation, point_mm)79return cast(Vector3, tuple(rotated[i] + self.translation_mm[i] for i in range(3)))8081def direction(self, direction: Vector3) -> Vector3:82return matvec(self.rotation, direction)8384def inverse(self) -> RigidTransform:85inverse_rotation = transpose(self.rotation)86translation = matvec(inverse_rotation, self.translation_mm)87return RigidTransform(inverse_rotation, cast(Vector3, tuple(-x for x in translation)))8889def compose(self, right: RigidTransform) -> RigidTransform:90"""Return self @ right; the right-hand map is applied first."""91return RigidTransform(92matmul(self.rotation, right.rotation), self.point(right.translation_mm)93)9495def packed(self) -> tuple[float, ...]:96"""Twelve float64 device values: row-major rotation, then translation."""97return (*self.rotation, *self.translation_mm)9899100# endregion book:geometry-rigid-transform101102103@dataclass(frozen=True, slots=True)104class DetectorGeometry:105"""Finite source-to-pixel segments; u increases columns and v increases rows."""106107source_mm: Vector3108origin_mm: Vector3109u: Vector3110v: Vector3111spacing_mm: tuple[float, float]112shape: tuple[int, int]113114def __post_init__(self) -> None:115for name in ("source_mm", "origin_mm", "u", "v"):116object.__setattr__(self, name, vector3(getattr(self, name), name))117if abs(dot(self.u, self.u) - 1.0) > 1e-10 or abs(dot(self.v, self.v) - 1.0) > 1e-10:118raise ContractError("detector basis vectors must have unit length")119if abs(dot(self.u, self.v)) > 1e-10:120raise ContractError("detector basis vectors must be perpendicular")121spacing = finite_tuple(self.spacing_mm, "pixel spacing", positive=True, length=2)122if len(self.shape) != 2 or any(type(x) is not int or x <= 0 for x in self.shape):123raise ContractError("detector shape is (height, width), both positive integers")124object.__setattr__(self, "shape", tuple(self.shape))125object.__setattr__(self, "spacing_mm", spacing)126offset: Vector3 = cast(127Vector3, tuple(self.source_mm[i] - self.origin_mm[i] for i in range(3))128)129if abs(dot(offset, cross(self.u, self.v))) <= 1e-12:130raise ContractError("source must lie outside the detector plane")131if self.pixels > 2**31 - 1:132raise ContractError("detector exceeds signed 32-bit indexing")133134@property135def pixels(self) -> int:136return self.shape[0] * self.shape[1]137138def pixel_centre(self, row: int, column: int) -> Vector3:139if type(row) is not int or type(column) is not int:140raise ContractError("pixel indices must be integers")141if not 0 <= row < self.shape[0] or not 0 <= column < self.shape[1]:142raise ContractError("pixel index outside detector")143return cast(144Vector3,145tuple(146self.origin_mm[i]147+ column * self.spacing_mm[0] * self.u[i]148+ row * self.spacing_mm[1] * self.v[i]149for i in range(3)150),151)152153154def _skew(vector: Vector3) -> Matrix3:155x, y, z = vector156return (0.0, -z, y, z, 0.0, -x, -y, x, 0.0)157158159def _coefficients(theta2: float) -> tuple[float, float, float, float, float]:160# Series avoid cancellation in both the exponential and its derivative.161if not math.isfinite(theta2):162raise ContractError("rotation norm exceeds the finite geometry range")163if theta2 < 1e-2:164a = 1 - theta2 / 6 + theta2**2 / 120 - theta2**3 / 5040165b = 0.5 - theta2 / 24 + theta2**2 / 720 - theta2**3 / 40320166c = 1 / 6 - theta2 / 120 + theta2**2 / 5040 - theta2**3 / 362880167db = -1 / 12 + theta2 / 180 - theta2**2 / 6720 + theta2**3 / 453600168dc = -1 / 60 + theta2 / 1260 - theta2**2 / 60480 + theta2**3 / 4989600169return a, b, c, db, dc170theta = math.sqrt(theta2)171sin, cos = math.sin(theta), math.cos(theta)172return (173sin / theta,174(1 - cos) / theta2,175(theta - sin) / (theta2 * theta),176(theta * sin - 2 * (1 - cos)) / theta2**2,177(3 * sin - theta * (2 + cos)) / (theta2**2 * theta),178)179180181# region book:geometry-right-se3-update182def compose_pose(anchor: RigidTransform, increment: tuple[float, ...]) -> RigidTransform:183"""Apply anchor @ exp(xi^); xi=(tx,ty,tz,rx,ry,rz), mm and radians.184185Translation is the Lie algebra coordinate, not a separately added world186displacement. A solver keeps anchor fixed until it discards curvature history.187"""188increment = finite_tuple(increment, "pose increment", minimum=None, length=6)189translation: Vector3 = (increment[0], increment[1], increment[2])190rotation: Vector3 = (increment[3], increment[4], increment[5])191omega = _skew(rotation)192omega2 = matmul(omega, omega)193a, b, c, _, _ = _coefficients(dot(rotation, rotation))194matrix: Matrix3 = cast(195Matrix3, tuple(IDENTITY[i] + a * omega[i] + b * omega2[i] for i in range(9))196)197velocity: Matrix3 = cast(198Matrix3, tuple(IDENTITY[i] + b * omega[i] + c * omega2[i] for i in range(9))199)200return anchor.compose(RigidTransform(matrix, matvec(velocity, translation)))201202203# endregion book:geometry-right-se3-update204205206def right_jacobian_se3(increment: tuple[float, ...]) -> tuple[tuple[float, ...], ...]:207"""Map fixed-anchor chart perturbations to local right perturbations.208209exp(xi+d)^ ~= exp(xi^) exp((J_r(xi)d)^). This is an analytic210derivative of Rodrigues' formula, including translation/rotation coupling.211"""212transform = compose_pose(RigidTransform(), increment)213rho: Vector3 = (increment[0], increment[1], increment[2])214rotation: Vector3 = (increment[3], increment[4], increment[5])215omega = _skew(rotation)216omega2 = matmul(omega, omega)217_, b, c, db, dc = _coefficients(dot(rotation, rotation))218rt = transpose(transform.rotation)219velocity: Matrix3 = cast(220Matrix3, tuple(IDENTITY[i] + b * omega[i] + c * omega2[i] for i in range(9))221)222translation_block = matmul(rt, velocity)223rotation_block = tuple(IDENTITY[i] - b * omega[i] + c * omega2[i] for i in range(9))224coupling: list[Vector3] = []225for axis in range(3):226basis: Vector3 = cast(Vector3, tuple(float(j == axis) for j in range(3)))227derivative = _skew(basis)228left, right = matmul(derivative, omega), matmul(omega, derivative)229dv: Matrix3 = cast(230Matrix3,231tuple(232db * rotation[axis] * omega[i]233+ b * derivative[i]234+ dc * rotation[axis] * omega2[i]235+ c * (left[i] + right[i])236for i in range(9)237),238)239coupling.append(matvec(rt, matvec(dv, rho)))240return tuple(241tuple(242translation_block[3 * i + j]243if i < 3 and j < 3244else coupling[j - 3][i]245if i < 3246else rotation_block[3 * (i - 3) + (j - 3)]247if j >= 3248else 0.0249for j in range(6)250)251for i in range(6)252)253254255def chart_gradient(256increment: tuple[float, ...], right_gradient: tuple[float, ...]257) -> tuple[float, ...]:258right_gradient = finite_tuple(right_gradient, "right tangent gradient", minimum=None, length=6)259jacobian = right_jacobian_se3(increment)260return tuple(sum(jacobian[i][j] * right_gradient[i] for i in range(6)) for j in range(6))261