Transform¶
Coordinate systems and the transforms between them.
A transform names the two coordinate systems it maps between, so it can say from where, to where and not only by how much. That is what lets the viewer refuse a transform built against a look-alike pair of spaces, and what makes a pyramid's per-level maps composable without a downsampling assumption.
cellier.transform ¶
Coordinate systems, and the transforms between them.
A transform here names the two coordinate systems it maps between, rather than carrying only a matrix. That is what lets the viewer refuse one built against a look-alike pair of spaces, what makes a pyramid's per-level maps compose without a downsampling assumption, and what a region needs in order to be pulled back through anything at all.
Built alongside an earlier, unnamed transform layer under this same path -- the two were deliberately independent, neither importing from the other (D1) -- and took the name over when that layer was retired in Phase 8 of the integration.
AxisRef
module-attribute
¶
AxisRef = str | UUID4
A reference to an axis, either by name or by id.
A str resolves through :meth:CoordinateSystem.axis_by_name, which
raises when the name is ambiguous within its system. A UUID4
resolves through :meth:CoordinateSystem.index_of and is never
ambiguous.
AxisSampling
module-attribute
¶
AxisSampling = Literal['discrete', 'continuous']
Whether an axis's coordinates are sample indices or measured positions.
"discrete" means the coordinates along this axis are sample indices,
so the grid is the integers: a voxel axis, or a points column that holds an
acquisition frame number. "continuous" means they are measured
positions that may fall anywhere.
The distinction decides how a position between samples is resolved, and the
two answers are genuinely different operations rather than one being an
approximation of the other. See :class:Axis for why that matters.
AxisType
module-attribute
¶
AxisType = Literal['array', 'space', 'time', 'channel', 'coordinate', 'displacement']
The closed set of axis types defined by OME-NGFF RFC-5.
CoordinateSystemType
module-attribute
¶
CoordinateSystemType = Annotated[Union[CoordinateSystem, DataCoordinateSystem, VisualCoordinateSystem, WorldCoordinateSystem, RenderedCoordinateSystem], Field(discriminator='coordinate_system_type')]
Discriminated union over every coordinate system kind (D8).
TransformType
module-attribute
¶
TransformType = Annotated[Union[AffineTransform, ByDimensionTransform, NonUniformAxisTransform], Field(discriminator='transform_type')]
Discriminated union over every concrete transform kind.
This, not BaseTransform, is what a pydantic model field should be
annotated with. BaseTransform is abstract: a field typed with it
loses the concrete subclass's validator and serializer, and a round-trip
through JSON raises PydanticSerializationError on the wrapped
transformnd object. The transform_type discriminator each concrete
class carries is what lets pydantic pick the right one back out again.
Use BaseTransform for ordinary function annotations, where it says
"anything satisfying the contract" and no serialization is involved.
AffineTransform
pydantic-model
¶
Bases: BaseTransform
An affine transform between two coordinate systems.
The matrix is (D_out + 1, D_in + 1); non-square is normal, not an
edge case. It is stored at float64 (D20): narrowing to float32 is
the renderer's job at upload, and chained affines across several
coordinate systems are exactly where float32 error accumulates.
Composition is :meth:then. There is no __matmul__ and no
compose: transformnd.Affine.__matmul__ applies its right
operand first, the wrapped affine is reachable through
.transform, and having a.transform @ b.transform and a
cellier-level composition mean opposite things in the same file is a
trap worth closing with a TypeError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transform_type
|
Literal['affine']
|
Discriminator field. |
required |
transform
|
Affine
|
The wrapped affine. A matrix-like is accepted and coerced. |
required |
broadcast_axes
|
frozenset[UUID4]
|
Ids of output axes the input has no extent along -- a dataset
broadcast over a channel axis, say. A zero matrix row is
indistinguishable from "constant zero", and the two differ for
extents: a broadcast dataset occupies all of that axis in the
output space, not the point |
required |
Config:
frozen:Truearbitrary_types_allowed:True
Fields:
-
name(str | None) -
input_coordinate_system(UUID4) -
output_coordinate_system(UUID4) -
id(UUID4) -
transform_type(Literal['affine']) -
transform(Affine) -
broadcast_axes(frozenset[UUID4])
Validators:
-
_coerce_affine→transform
output_ndim
property
¶
output_ndim: int
Number of output dimensions, read from the wrapped transform.
validate_against ¶
validate_against(input_coordinate_system: CoordinateSystem, output_coordinate_system: CoordinateSystem) -> None
Check this transform against the two systems it claims to join.
The four checks of D9: both ids match, and both axis counts match the wrapped transform's dimensionality. A future registry calls this on insertion; defining it now means the registry inherits a decided rule rather than inventing one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_coordinate_system
|
CoordinateSystem
|
The system this transform should map from. |
required |
output_coordinate_system
|
CoordinateSystem
|
The system this transform should map to. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any of the four checks fails. |
inverse ¶
inverse() -> AffineTransform | None
Return the inverse transform, or None.
Three cases (D24): exact for a non-singular square matrix; the
pseudo-inverse of the linear block for a full-column-rank
embedding, which is an exact left inverse; None
otherwise. A dimension-reducing transform is not silently handed
a right inverse, which would return a different point than the
caller put in.
The coordinate system ids are swapped. That is the whole
point, and it is where transformnd.Spaced.invert() goes
wrong: it inverts the inner transform but returns the original
space order, so the result claims a direction it does not
perform.
broadcast_axes is empty on the result: those name axes of the
forward transform's output system, which the inverse maps from,
not to.
Returns:
| Type | Description |
|---|---|
AffineTransform or None
|
The inverse, or |
map_coordinates ¶
Map points from the input system to the output system.
Accepts (N, D_in) or (D_in,) and returns matching rank.
Homogeneous (N, D_in + 1) input is rejected (D12). The
result is always a distinct array, even for an identity affine,
where transformnd would return the input object (D11).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinates
|
ndarray
|
Points in the input coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Points in the output coordinate system. |
imap_coordinates ¶
Map points from the output system back to the input system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinates
|
ndarray
|
Points in the output coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Points in the input coordinate system. |
Raises:
| Type | Description |
|---|---|
NonInvertibleTransformError
|
If this transform has no left inverse (D24 case 3). |
map_direction ¶
Map displacement vectors forward by A.
Needs no inverse, so it works on a transform whose
:meth:inverse is None. Nothing is normalized (D28).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
direction
|
ndarray
|
Vectors in the input coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Vectors in the output coordinate system. |
imap_direction ¶
Map displacement vectors back by A+.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
direction
|
ndarray
|
Vectors in the output coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Vectors in the input coordinate system. |
Raises:
| Type | Description |
|---|---|
NonInvertibleTransformError
|
If this transform has no left inverse. |
map_normal ¶
Map plane normals forward by (A+)^T (covariant, D27).
A normal is not a displacement: it transforms by the inverse
transpose, and using A is correct only for a rigid transform.
Nothing is normalized (D28).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
normal
|
ndarray
|
Normals in the input coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Normals in the output coordinate system. |
Raises:
| Type | Description |
|---|---|
NonInvertibleTransformError
|
If this transform has no left inverse. |
DegenerateNormalError
|
If a normal maps to the zero vector (D32). |
imap_normal ¶
Map plane normals back by A^T (covariant, D27).
Needs no inverse, so it works on a transform whose
:meth:inverse is None. Not injective: a normal tilted
along an axis this transform has no extent in comes back with
that tilt discarded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
normal
|
ndarray
|
Normals in the output coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Normals in the input coordinate system. |
Raises:
| Type | Description |
|---|---|
DegenerateNormalError
|
If a normal maps to the zero vector (D32). |
map_plane ¶
Map a plane from the input system to the output system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plane
|
Plane
|
A plane in the input coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
Plane
|
The mapped plane, in the output coordinate system. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the plane is not in this transform's input system. |
NonInvertibleTransformError
|
If this transform has no left inverse. |
DegenerateNormalError
|
If the normal maps to the zero vector (D32). |
imap_plane ¶
Map a plane from the output system back to the input system.
Needs no inverse. Not injective (see :meth:imap_normal), so
map_plane(imap_plane(p)) == p does not hold in general.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plane
|
Plane
|
A plane in the output coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
Plane
|
The pulled-back plane, in the input coordinate system. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the plane is not in this transform's output system. |
DegenerateNormalError
|
If the normal maps to the zero vector (D32). |
map_bounding_box ¶
map_bounding_box(box: AxisAlignedBoundingBox, output_coordinate_system: CoordinateSystem) -> AxisAlignedBoundingBox
Map a box forward, conservatively.
The result is the smallest axis-aligned box enclosing the
image, which is a superset unless the linear block is diagonal or
a signed permutation. Every axis in broadcast_axes comes back
(-inf, +inf) (D31).
The output coordinate system is required because
broadcast_axes holds axis ids and the arithmetic needs
their indices, which only the system can supply.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
box
|
AxisAlignedBoundingBox
|
A box in the input coordinate system. |
required |
output_coordinate_system
|
CoordinateSystem
|
This transform's output system, used to resolve
|
required |
Returns:
| Type | Description |
|---|---|
AxisAlignedBoundingBox
|
The enclosing box, in the output coordinate system. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the box is not in this transform's input system, or the given system is not this transform's output system. |
imap_bounding_box ¶
imap_bounding_box(box: AxisAlignedBoundingBox) -> AxisAlignedBoundingBox
Map a box back, conservatively.
Needs no broadcast handling (D31): the pseudo-inverse has an all-zero column for a broadcast axis, so that axis's extent is dropped whether it is finite or infinite.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
box
|
AxisAlignedBoundingBox
|
A box in the output coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
AxisAlignedBoundingBox
|
The enclosing box, in the input coordinate system. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the box is not in this transform's output system. |
NonInvertibleTransformError
|
If this transform has no left inverse. |
map_region ¶
map_region(region: ConvexRegion) -> ConvexRegion
Map a convex region forward.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
region
|
ConvexRegion
|
A region in the input coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
ConvexRegion
|
The mapped region, in the output coordinate system. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the region is not in this transform's input system. |
NonInvertibleTransformError
|
If this transform has no left inverse. |
imap_region ¶
imap_region(region: ConvexRegion, output_coordinate_system: CoordinateSystem) -> ConvexRegion
Map a convex region back into the input coordinate system.
This is the operation the slicer runs, and it needs nothing but
A^T -- exact, cheap, and available on a transform whose
:meth:inverse is None (D39).
The result is a region, never a collapsed bounding box (D41). Reducing to a box at the world level costs about 6x the voxels for a 45-degree slab, and the datastore wants both: the box for chunk selection and the constraints for the per-voxel mask. Callers that want the box ask for it themselves.
Constraints on a broadcast axis are dropped before A^T is
applied (D8). A broadcast axis is a free variable -- the source
exists at every position on it -- so such a constraint can
always be satisfied by moving along that axis. This is exactly
Fourier-Motzkin elimination of the broadcast axes specialised to
the axis-aligned case: the single +b / -b pair combines
to 0 <= 2 * half_thickness, which is trivially true. The
general pairwise combination is deferred until a consumer exists
that tilts an oblique plane through a broadcast axis and
bounds that same axis; nothing shipping does.
Without this step, selecting C = 2 against a source declared
broadcast over C pulls back through an all-zero row to
0 <= -2 and the region comes back empty -- the visual
disappears the moment the channel slider leaves zero.
A constraint on an axis this transform has no extent along and
which is not broadcast still comes back with a zero normal,
which is vacuous or infeasible rather than an error. Call
:meth:ConvexRegion.simplify to drop the vacuous ones.
map_region is deliberately not given the symmetric
treatment: the asymmetry is D31's, and broadcast_axes
affects the forward direction as unboundedness rather than as a
dropped constraint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
region
|
ConvexRegion
|
A region in the output coordinate system. |
required |
output_coordinate_system
|
CoordinateSystem
|
This transform's output system, used to resolve
|
required |
Returns:
| Type | Description |
|---|---|
ConvexRegion
|
The pulled-back region, in the input coordinate system. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the region is not in this transform's output system, or the given system is not this transform's output system. |
then ¶
then(other: AffineTransform, intermediate_coordinate_system: CoordinateSystem, output_coordinate_system: CoordinateSystem) -> AffineTransform
Return the transform that applies self, then other.
Both coordinate systems are required on every call, broadcast or
not. They are not decoration: broadcast_axes holds axis
ids, and propagating them through other needs the
intermediate system to resolve those ids to matrix columns and
the output system to name the resulting axes. Requiring them
unconditionally keeps one rule rather than two.
An output axis of the result is broadcast if other declares
it so, or if its row has a non-zero coefficient on any axis that
self broadcasts over -- anything downstream of an unbounded
axis is itself unbounded.
There is no | operator and no __matmul__.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
AffineTransform
|
The transform to apply second. |
required |
intermediate_coordinate_system
|
CoordinateSystem
|
The system both transforms meet in: this transform's output
and |
required |
output_coordinate_system
|
CoordinateSystem
|
|
required |
Returns:
| Type | Description |
|---|---|
AffineTransform
|
A transform from this one's input to |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the coordinate systems do not line up. A mismatch raises even when the ranks happen to agree (D19). |
from_axis_map
classmethod
¶
from_axis_map(input_coordinate_system: CoordinateSystem, output_coordinate_system: CoordinateSystem, axis_map: Mapping[AxisRef, AxisRef], scale: Mapping[AxisRef, float] | None = None, translation: Mapping[AxisRef, float] | None = None, broadcast_output_axes: Sequence[AxisRef] = (), constant_output_axes: Mapping[AxisRef, float] | None = None, name: str | None = None) -> Self
Build a transform by stating which axis corresponds to which.
This is the primary constructor, and axis_map is always
required. There is no positional default and no name-matching
default: a positional default is exactly wrong when the two
systems have equal rank but different order, and it fails
silently. Stating the correspondence costs one dict literal
and puts it where a reader can check it against the two systems.
scale and translation are keyed by input axis, so
every per-axis quantity in the call uses one key space. Omitted
axes get scale 1 and translation 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_coordinate_system
|
CoordinateSystem
|
The system to map from. The object, not the id: building the matrix needs its axes (D23). |
required |
output_coordinate_system
|
CoordinateSystem
|
The system to map to. |
required |
axis_map
|
Mapping[AxisRef, AxisRef]
|
|
required |
scale
|
Mapping[AxisRef, float] or None
|
Per-input-axis scale factors. Default 1. |
None
|
translation
|
Mapping[AxisRef, float] or None
|
Per-input-axis translations. Default 0. |
None
|
broadcast_output_axes
|
Sequence[AxisRef]
|
Output axes the input has no extent along. These get a zero
row and are recorded in |
()
|
constant_output_axes
|
Mapping[AxisRef, float] or None
|
Output axes the input has no extent along that sit at a
fixed value -- a slice index. These get a zero row and that
value in the translation column, and are not recorded in
|
None
|
name
|
str or None
|
Optional name for the transform. |
None
|
Returns:
| Type | Description |
|---|---|
AffineTransform
|
A transform of shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an input axis is unmapped, an output axis is neither
mapped nor declared, an output axis is claimed twice, or a
mapped pair disagrees on |
from_matrix
classmethod
¶
from_matrix(matrix: ndarray, input_coordinate_system: CoordinateSystem, output_coordinate_system: CoordinateSystem, broadcast_output_axes: Sequence[AxisRef] = (), name: str | None = None) -> Self
Build a transform from a matrix, the escape hatch.
Both coordinate systems are still required (D18): a transform
without both endpoints is meaningless in this model, and there is
no identity(ndim) that invents or omits them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
ndarray
|
The |
required |
input_coordinate_system
|
CoordinateSystem
|
The system to map from. |
required |
output_coordinate_system
|
CoordinateSystem
|
The system to map to. |
required |
broadcast_output_axes
|
Sequence[AxisRef]
|
Output axes the input has no extent along (D25). |
()
|
name
|
str or None
|
Optional name for the transform. |
None
|
Returns:
| Type | Description |
|---|---|
AffineTransform
|
The transform. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the matrix shape disagrees with the two systems' ranks. |
input_domain ¶
broadcast_output_axes ¶
broadcast_output_axes() -> frozenset[UUID4]
Return :attr:broadcast_axes, the recorded broadcast output axes.
Returns:
| Type | Description |
|---|---|
frozenset[UUID4]
|
Output axis ids. |
axis_correspondence ¶
Read {input axis: output axis} off the matrix.
Returns:
| Type | Description |
|---|---|
dict[int, int]
|
Input axis index to output axis index, for every input axis that reaches an output axis. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any input axis feeds more than one output axis, or any output
axis is fed by more than one input axis -- a shear or a rotation,
which the axis-aligned slicing path cannot express. See
|
restrict ¶
restrict(fixed: Mapping[AxisRef | int, float], input_coordinate_system: CoordinateSystem | None = None) -> AffineTransform
Pin input axes to fixed values, folding them into the translation.
Always succeeds: for an affine transform this is ordinary matrix
algebra. Each fixed axis's column is multiplied by its value and
added to the translation, then dropped from the linear block --
the input-side mirror of constant_output_axes.
broadcast_axes carries through unchanged: it names output
axes, which restricting the input does not touch.
The result is an intermediate, not a registrable transform. Its
input_coordinate_system is kept as provenance, but the
restricted domain is a strict subset of that system's axes, so the
rank no longer matches and :meth:validate_against will reject it.
That rejection is deliberate and loud; the intended use is to call
:meth:to_affine on the result and upload the matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fixed
|
Mapping[AxisRef | int, float]
|
|
required |
input_coordinate_system
|
CoordinateSystem or None
|
Needed only to resolve named axes. |
None
|
Returns:
| Type | Description |
|---|---|
AffineTransform
|
A transform over the remaining input axes, in their original relative order. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an axis is named twice, is out of range, or a name is given with no coordinate system to resolve it against. |
to_affine ¶
to_affine() -> AffineTransform
Return self: an affine transform already is one.
Returns:
| Type | Description |
|---|---|
AffineTransform
|
This transform. |
__eq__ ¶
Compare by endpoints, matrix and broadcast axes, not by id.
Two independently constructed but identical transforms compare
equal, which is why id is excluded. broadcast_axes is
included: two transforms with the same matrix but different
broadcast axes give different bounding boxes, so they are not
the same value.
Axis
pydantic-model
¶
Bases: BaseModel
A single axis of a coordinate system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Human-readable name, e.g. |
required |
axis_type
|
AxisType
|
The RFC-5 axis type. Required: there is no default, because a
channel or time axis silently typed as |
required |
unit
|
str or None
|
Free-form unit string. RFC-5 recommends UDUNITS-2, but nothing here validates or interprets it. |
required |
sampling
|
AxisSampling
|
Whether this axis's coordinates are sample indices
( What it decides. A position landing between two samples is resolved by snapping to the nearest sample on a discrete axis and by containment in a window on a continuous one. Those disagree by up to half a sampling interval: a snap flips at the midpoint between two samples, a window flips when the position reaches the sample. With an image (always discrete) and a points or graph store sharing one world axis, that difference is directly visible as the markers lagging the image by half a frame. Only a data axis is consulted. Discreteness is a property of how one store samples the world, not of the world itself: the same world time axis is sampled at thirteen irregular frames by one store and continuously by another, and each needs its own answer. This is the same reasoning that put the coordinate table on the transform rather than on the world. Unlike |
required |
id
|
UUID4
|
Unique identifier. Auto-generated. |
required |
Config:
frozen:True
Fields:
-
name(str) -
axis_type(AxisType) -
unit(str | None) -
sampling(AxisSampling) -
id(UUID4)
BaseTransform
pydantic-model
¶
Bases: BaseModel, ABC
A transform between two coordinate systems.
Wraps a :class:transformnd.base.Transform rather than
reimplementing one. transformnd.Spaced is deliberately not used:
it carries no axis metadata, does not serialize, and its invert()
returns the original space order while performing the inverted
transform. Owning the source/target pairing here is what avoids
inheriting that bug.
Transforms are frozen (Q13). Dimensionality is not stored: it
lives on the wrapped transform and is read through, so the model
cannot disagree with itself (D9). The cross-check against the
coordinate systems' axis counts cannot happen here, because the model
holds ids rather than systems; it is factored into
:meth:validate_against.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str or None
|
Optional human-readable name. RFC-5 gives transformations one. |
required |
input_coordinate_system
|
UUID4
|
The id of the coordinate system this transform maps from. |
required |
output_coordinate_system
|
UUID4
|
The id of the coordinate system this transform maps to. |
required |
transform
|
Transform
|
The wrapped |
required |
id
|
UUID4
|
Unique identifier. Auto-generated. Not |
required |
Fields:
-
name(str | None) -
input_coordinate_system(UUID4) -
output_coordinate_system(UUID4) -
transform(Transform) -
id(UUID4)
output_ndim
property
¶
output_ndim: int
Number of output dimensions, read from the wrapped transform.
validate_against ¶
validate_against(input_coordinate_system: CoordinateSystem, output_coordinate_system: CoordinateSystem) -> None
Check this transform against the two systems it claims to join.
The four checks of D9: both ids match, and both axis counts match the wrapped transform's dimensionality. A future registry calls this on insertion; defining it now means the registry inherits a decided rule rather than inventing one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_coordinate_system
|
CoordinateSystem
|
The system this transform should map from. |
required |
output_coordinate_system
|
CoordinateSystem
|
The system this transform should map to. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any of the four checks fails. |
map_coordinates
abstractmethod
¶
Map points from the input system to the output system.
imap_coordinates
abstractmethod
¶
Map points from the output system back to the input system.
map_direction
abstractmethod
¶
Map displacement vectors forward (contravariant).
imap_direction
abstractmethod
¶
Map displacement vectors back (contravariant).
map_normal
abstractmethod
¶
Map plane normals forward (covariant).
imap_normal
abstractmethod
¶
Map plane normals back (covariant).
map_bounding_box
abstractmethod
¶
map_bounding_box(box: AxisAlignedBoundingBox, output_coordinate_system: CoordinateSystem) -> AxisAlignedBoundingBox
Map an axis-aligned box forward, conservatively.
imap_bounding_box
abstractmethod
¶
imap_bounding_box(box: AxisAlignedBoundingBox) -> AxisAlignedBoundingBox
Map an axis-aligned box back, conservatively.
map_region
abstractmethod
¶
map_region(region: ConvexRegion) -> ConvexRegion
Map a convex region forward.
imap_region
abstractmethod
¶
imap_region(region: ConvexRegion, output_coordinate_system: CoordinateSystem) -> ConvexRegion
Map a convex region back, dropping broadcast constraints (D8).
input_domain
abstractmethod
¶
Return {input axis: (low, high)} for every bounded axis.
The span of input coordinates this transform can actually map. An axis absent from the result is unbounded -- an affine transform has no intrinsic domain at all, so its result is empty.
What it is for. A caller that rounds a position to a whole
sample needs to know which samples exist, or rounding can leave the
domain: the last sample's cell ends half a unit past its centre, and
round-half-up sends that boundary upward to a sample that is not
there. Structural, like :meth:axis_correspondence, and available
without a matrix -- a non-affine block answers from its own table.
Returns:
| Type | Description |
|---|---|
dict[int, tuple[float, float]]
|
Input axis index to |
axis_correspondence
abstractmethod
¶
Return {input axis: output axis} for every axis that reaches one.
Answers "which output axis does each input axis become", which the render layer needs in order to line a data axis up with the world axis a slider moves.
The correspondence is not stored -- keeping it beside the transform that already encodes it would be a second source of truth (D23) -- so each transform reads it back from whatever it does hold. An affine reads it off its matrix; a block container reads it off its block declarations, structurally, with no matrix involved, which is what lets a non-affine transform answer at all.
Returns:
| Type | Description |
|---|---|
dict[int, int]
|
Input axis index to output axis index. An input axis that reaches no output axis is absent. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any input axis feeds more than one output axis, or any output axis is fed by more than one input axis. That is a shear or a rotation, which the axis-aligned slicing path cannot express. |
broadcast_output_axes ¶
broadcast_output_axes() -> frozenset[UUID4]
Return the ids of the output axes the input has no extent along.
A dataset broadcast over an axis occupies all of it, not the point
its zero matrix row maps to, so a caller measuring where the data is
-- a world bounding box, say -- must skip these axes rather than read
a mapped coordinate off them. Structural, like
:meth:axis_correspondence: a block container answers from its
blocks, with no matrix involved.
Returns:
| Type | Description |
|---|---|
frozenset[UUID4]
|
Output axis ids. Empty unless a subclass records broadcasts. |
restrict
abstractmethod
¶
restrict(fixed: Mapping[AxisRef | int, float], input_coordinate_system: CoordinateSystem | None = None) -> BaseTransform
Pin some input axes to fixed values, dropping them from the domain.
The question the render layer actually needs answered is not "is this transform affine" but "with every collapsed axis pinned to this request's value, is what remains affine". That is strictly weaker, because evaluating any transform at a fixed input produces a constant, and folding a constant into a translation is something affine algebra already does. So a non-uniform axis that is sliced rather than displayed costs nothing at the GPU boundary.
The values in fixed are exact and already resolved.
restrict must not round or clamp them: that already happened one
layer up, in round_world_to_voxel, and doing it twice risks the
two disagreeing about which plane a position selects.
This is the input-side, post-hoc mirror of what
AffineTransform.from_axis_map(..., constant_output_axes=...)
does output-side at construction time. Named restrict rather
than slice because slice is claimed, hard, by the request
pipeline, which rounds and clamps -- precisely what this must not
do.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fixed
|
Mapping[AxisRef | int, float]
|
|
required |
input_coordinate_system
|
CoordinateSystem or None
|
The system to resolve named axes against. Required only when fixed has non-integer keys: a transform stores its endpoints as ids and cannot resolve a name on its own. |
None
|
Returns:
| Type | Description |
|---|---|
BaseTransform
|
A transform over the remaining (free) input axes. |
Raises:
| Type | Description |
|---|---|
NonAffineTransformError
|
If some part of the transform straddles the fixed/free split and is not affine, so there is no closed form for "fix one input, what is left as a function of the others". |
to_affine
abstractmethod
¶
to_affine() -> AffineTransform | None
Return this transform as an affine, or None if it is not one.
None is an answer, not a failure: callers that need a matrix
pair this with :meth:restrict and raise
NonAffineTransformError themselves, with a message naming the
axis they cannot express.
Returns:
| Type | Description |
|---|---|
AffineTransform or None
|
|
inverse
abstractmethod
¶
inverse() -> BaseTransform | None
Return the inverse transform, or None if there is none.
ByDimensionTransform
pydantic-model
¶
Bases: BaseTransform
A transform whose axes are handled by independent blocks.
Blocks partition both the input and the output axes, so every operation decomposes per block. That is what keeps a non-uniform axis from infecting the rest: a question confined to the affine axes is answered by the affine block, and the irregular one is never consulted.
This class routes its own blocks rather than delegating to
transformnd.ByDimension.apply. That method allocates its output
with empty_like(coords) -- an array shaped and typed like its
input -- so it is wrong for every non-square block, which includes
this class's motivating case. The expanding direction raises
IndexError; the contracting direction, which is the pull-back the
whole slicing path runs on, returns an input-shaped array with an
uninitialised trailing column and does not raise. The wrapped
ByDimension is still the object in the transform field -- it is
the right RFC-5 object and it reports the dimensionality
validate_against checks -- but it never executes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
blocks
|
tuple[TransformBlock, ...]
|
The blocks, whose input and output axis sets must each partition the container's axes exactly. |
required |
Config:
frozen:Truearbitrary_types_allowed:True
Fields:
-
name(str | None) -
input_coordinate_system(UUID4) -
output_coordinate_system(UUID4) -
id(UUID4) -
transform_type(Literal['by_dimension']) -
blocks(tuple[TransformBlock, ...]) -
transform(ByDimension)
Validators:
-
_derive_transform
output_ndim
property
¶
output_ndim: int
Number of output dimensions, read from the wrapped transform.
validate_against ¶
validate_against(input_coordinate_system: CoordinateSystem, output_coordinate_system: CoordinateSystem) -> None
Check this transform against the two systems it claims to join.
The four checks of D9: both ids match, and both axis counts match the wrapped transform's dimensionality. A future registry calls this on insertion; defining it now means the registry inherits a decided rule rather than inventing one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_coordinate_system
|
CoordinateSystem
|
The system this transform should map from. |
required |
output_coordinate_system
|
CoordinateSystem
|
The system this transform should map to. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any of the four checks fails. |
from_axis_map
classmethod
¶
from_axis_map(input_coordinate_system: CoordinateSystem, output_coordinate_system: CoordinateSystem, axis_map: Mapping[AxisRef, AxisRef], scale: Mapping[AxisRef, float] | None = None, translation: Mapping[AxisRef, float] | None = None, broadcast_output_axes: Sequence[AxisRef] = (), constant_output_axes: Mapping[AxisRef, float] | None = None, axis_transforms: Mapping[AxisRef, BaseTransform] | None = None, name: str | None = None) -> Self
Build a transform by stating which axis corresponds to which.
Every argument but axis_transforms keeps the meaning it has in
:meth:AffineTransform.from_axis_map, so a caller who needs no
per-axis block writes exactly today's call and the non-uniform case
is a one-key diff rather than a different idiom.
Blocks are assembled as one 1-D block per axis_transforms entry,
plus one affine block covering every remaining input axis -- which
is also where broadcast_output_axes and constant_output_axes
ride. Sub-coordinate-systems are derived internally; callers never
mint them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_coordinate_system
|
CoordinateSystem
|
The system to map from. |
required |
output_coordinate_system
|
CoordinateSystem
|
The system to map to. |
required |
axis_map
|
Mapping[AxisRef, AxisRef]
|
|
required |
scale
|
Mapping[AxisRef, float] or None
|
Per-input-axis scale factors, for the affine axes only. |
None
|
translation
|
Mapping[AxisRef, float] or None
|
Per-input-axis translations, for the affine axes only. |
None
|
broadcast_output_axes
|
Sequence[AxisRef]
|
Output axes the input has no extent along. |
()
|
constant_output_axes
|
Mapping[AxisRef, float] or None
|
Output axes pinned to a fixed value. |
None
|
axis_transforms
|
Mapping[AxisRef, BaseTransform] or None
|
|
None
|
name
|
str or None
|
Optional name. |
None
|
Returns:
| Type | Description |
|---|---|
ByDimensionTransform
|
The assembled transform. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an axis appears in both |
map_coordinates ¶
Map points forward, block by block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinates
|
ndarray
|
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
imap_coordinates ¶
Map points back, block by block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinates
|
ndarray
|
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
map_bounding_box ¶
map_bounding_box(box: AxisAlignedBoundingBox, output_coordinate_system: CoordinateSystem) -> AxisAlignedBoundingBox
Map a box forward, exactly, block by block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
box
|
AxisAlignedBoundingBox
|
A box in the input coordinate system. |
required |
output_coordinate_system
|
CoordinateSystem
|
The system to express the result in. |
required |
Returns:
| Type | Description |
|---|---|
AxisAlignedBoundingBox
|
The mapped box. |
imap_bounding_box ¶
imap_bounding_box(box: AxisAlignedBoundingBox) -> AxisAlignedBoundingBox
Map a box back, exactly, block by block.
A monotonic 1-D block maps an interval's endpoints through its own monotonic function, which is order-preserving and therefore exact rather than merely conservative.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
box
|
AxisAlignedBoundingBox
|
A box in the output coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
AxisAlignedBoundingBox
|
The box in the input coordinate system. |
input_domain ¶
Collect each block's own domain, in container axis indices.
Blocks partition the input axes, so the union needs no reconciling: an affine block contributes nothing and a table-backed one contributes its own span.
Returns:
| Type | Description |
|---|---|
dict[int, tuple[float, float]]
|
Input axis index to |
broadcast_output_axes ¶
axis_correspondence ¶
Read the correspondence off the block declarations, structurally.
The reason this is a transform method rather than a matrix read. Each block already states which container axes it consumes and which it produces, so the answer needs no matrix and is available even when a block has none. A block is asked for its own correspondence and its answer is translated into container axis indices.
Returns:
| Type | Description |
|---|---|
dict[int, int]
|
Input axis index to output axis index. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If two blocks claim the same output axis, or a block is itself a shear. Blocks partition the axes by construction, so the former can only happen if the blocks were built by hand. |
restrict ¶
restrict(fixed: Mapping[AxisRef | int, float], input_coordinate_system: CoordinateSystem | None = None) -> BaseTransform
Pin input axes, deciding per block what that means.
Three cases, per the design:
- Block entirely fixed -- evaluated at the fixed value, giving a
constant that is folded into the remaining translation. Legal
whether or not the block is affine, which is why a sliced
non-uniform axis costs nothing. Note this evaluates the block
rather than recursing into its own
restrict: there is nothing left of its domain afterwards. - Block entirely free -- passed through unchanged.
- Block straddling both -- legal only if the block is affine, which is ordinary matrix algebra. A straddling non-affine block has no closed form for "fix one input, what is left as a function of the others", and raises.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fixed
|
Mapping[AxisRef | int, float]
|
|
required |
input_coordinate_system
|
CoordinateSystem or None
|
Needed only to resolve named axes. |
None
|
Returns:
| Type | Description |
|---|---|
BaseTransform
|
A transform over the free input axes. |
Raises:
| Type | Description |
|---|---|
NonAffineTransformError
|
If a non-affine block straddles the fixed/free split. |
to_affine ¶
to_affine() -> AffineTransform | None
Assemble a block-diagonal affine, or None if any block is not.
The axes are disjoint by construction, so this is direct assembly rather than general linear algebra.
Returns:
| Type | Description |
|---|---|
AffineTransform or None
|
The equivalent affine, or |
inverse ¶
inverse() -> BaseTransform | None
Invert every block, or return None if any block cannot.
Returns:
| Type | Description |
|---|---|
BaseTransform or None
|
The inverse, or |
map_direction ¶
Map a displacement vector, if every block is affine.
imap_direction ¶
Map a displacement vector back, if every block is affine.
map_region ¶
map_region(region: ConvexRegion) -> ConvexRegion
Map a convex region, if every block is affine.
imap_region ¶
imap_region(region: ConvexRegion, output_coordinate_system: CoordinateSystem) -> ConvexRegion
Map a convex region back, exactly.
Two cases, and the split is the design's:
- Axis-aligned -- every half-space normal lies along a single
axis, which is what
AxisAlignedSelectionproduces and so what the whole slicing path actually sends. Each axis is an interval, and an interval through a monotonic block maps by its endpoints, so the pull-back is exact even across a non-affine block. - Anything else -- handled by the equivalent affine when every block has one, and otherwise refused.
The refusal is a permanent, documented restriction, not a TODO.
A half-space whose normal mixes a non-affine block's axis with
another has no exact preimage, and no approximation is offered.
Nothing produces that case today -- PlaneSelection is an
unimplemented stub -- but an oblique selection over a non-uniform
axis would.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
region
|
ConvexRegion
|
A region in the output coordinate system. |
required |
output_coordinate_system
|
CoordinateSystem
|
This transform's input system, which the result is expressed in. Named for the abstract signature, which reads from the caller's side. |
required |
Returns:
| Type | Description |
|---|---|
ConvexRegion
|
The region in the input coordinate system. |
TransformBlock
pydantic-model
¶
Bases: BaseModel
One block of a :class:ByDimensionTransform.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transform
|
BaseTransform
|
The transform this block applies. |
required |
input_axes
|
tuple[int, ...]
|
Which input axes of the container it consumes, in order. |
required |
output_axes
|
tuple[int, ...]
|
Which output axes of the container it produces, in order. May be
longer than |
required |
input_coordinate_system
|
CoordinateSystem
|
The block's own input system. Carried so bounding-box delegation
can stamp a sub-box with the right id; its |
required |
output_coordinate_system
|
CoordinateSystem
|
The block's own output system, likewise. |
required |
Config:
frozen:True
Fields:
-
transform(BaseTransform) -
input_axes(tuple[int, ...]) -
output_axes(tuple[int, ...]) -
input_coordinate_system(CoordinateSystem) -
output_coordinate_system(CoordinateSystem)
Validators:
-
_validate
CoordinateSystem
pydantic-model
¶
Bases: BaseModel
An ordered set of axes.
Coordinate systems are frozen (D7). Changing the axes of a system means constructing a replacement, so that a transform already validated against a system cannot silently face a different one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system_type
|
Literal['coordinate_system']
|
Discriminator field. |
required |
name
|
str
|
Human-readable name. |
required |
axes
|
tuple[Axis, ...]
|
The axes, in order. At least one is required and their ids must be unique. |
required |
id
|
UUID4
|
Unique identifier. Auto-generated. |
required |
Config:
frozen:True
Fields:
Validators:
-
_validate_axes
axis_by_name ¶
Return the single axis with the given name.
Axis names are not required to be unique (Q6), so ambiguity is a property of the query rather than of the model: this raises instead of returning the first match.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The axis name to look up. |
required |
Returns:
| Type | Description |
|---|---|
Axis
|
The matching axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no axis has that name. |
ValueError
|
If more than one axis has that name. |
resolve ¶
Resolve an axis reference to an axis index in this system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis
|
AxisRef
|
An axis name or an axis id. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The index of the referenced axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the reference does not name an axis of this system. |
ValueError
|
If a name is ambiguous within this system. |
DataCoordinateSystem
pydantic-model
¶
Bases: CoordinateSystem
The intrinsic coordinate system of a datastore (e.g. voxel indices).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system_type
|
Literal['data']
|
Discriminator field. |
required |
datastore_id
|
UUID4
|
The id of the datastore this system belongs to. |
required |
Fields:
-
name(str) -
axes(tuple[Axis, ...]) -
id(UUID4) -
coordinate_system_type(Literal['data']) -
datastore_id(UUID4)
Validators:
-
_validate_axes
axis_by_name ¶
Return the single axis with the given name.
Axis names are not required to be unique (Q6), so ambiguity is a property of the query rather than of the model: this raises instead of returning the first match.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The axis name to look up. |
required |
Returns:
| Type | Description |
|---|---|
Axis
|
The matching axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no axis has that name. |
ValueError
|
If more than one axis has that name. |
resolve ¶
Resolve an axis reference to an axis index in this system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis
|
AxisRef
|
An axis name or an axis id. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The index of the referenced axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the reference does not name an axis of this system. |
ValueError
|
If a name is ambiguous within this system. |
RenderedCoordinateSystem
pydantic-model
¶
Bases: CoordinateSystem
The 2D or 3D scene drawn on one canvas.
This is scene space in world units (R1); the camera transform from scene space to normalized device coordinates is a separate concern and is not modelled here.
There is deliberately no world_coordinate_system backref: the link
to the world system is carried by the transform between them, which
already stores both ids (D33).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system_type
|
Literal['rendered']
|
Discriminator field. |
required |
name
|
str
|
Human-readable name. Defaults to |
required |
canvas_id
|
UUID4
|
The id of the canvas this system is drawn on. One system per canvas (R2). |
required |
Fields:
-
axes(tuple[Axis, ...]) -
id(UUID4) -
coordinate_system_type(Literal['rendered']) -
name(str) -
canvas_id(UUID4)
Validators:
-
_validate_axes -
_validate_rank
axis_by_name ¶
Return the single axis with the given name.
Axis names are not required to be unique (Q6), so ambiguity is a property of the query rather than of the model: this raises instead of returning the first match.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The axis name to look up. |
required |
Returns:
| Type | Description |
|---|---|
Axis
|
The matching axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no axis has that name. |
ValueError
|
If more than one axis has that name. |
resolve ¶
Resolve an axis reference to an axis index in this system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis
|
AxisRef
|
An axis name or an axis id. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The index of the referenced axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the reference does not name an axis of this system. |
ValueError
|
If a name is ambiguous within this system. |
resolve_axis ¶
from_world
classmethod
¶
from_world(world_coordinate_system: WorldCoordinateSystem, displayed_axes: Sequence[AxisRef], canvas_id: UUID4, name: str = 'rendered') -> Self
Build a rendered system from the world axes it displays.
Each rendered axis corresponds to exactly one world axis, so
name, axis_type and unit are inherited from that world
axis rather than re-specified (R5). The axis ids are fresh:
these are distinct axes of a distinct coordinate system, and
reusing the world ids would make index_of ambiguous across
systems.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
world_coordinate_system
|
WorldCoordinateSystem
|
The world system being displayed. |
required |
displayed_axes
|
Sequence[AxisRef]
|
The world axes to display, in rendered axis order. The order is the point: it is where the renderer's axis convention stops being an unwritten rule. |
required |
canvas_id
|
UUID4
|
The id of the canvas this system is drawn on. |
required |
name
|
str
|
Human-readable name for the new system. |
'rendered'
|
Returns:
| Type | Description |
|---|---|
RenderedCoordinateSystem
|
A system of the same rank as |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
VisualCoordinateSystem
pydantic-model
¶
Bases: CoordinateSystem
The space one visual's GPU geometry is indexed in (D45).
This is the space node.local.matrix maps from: the index space
of the array a datastore returned, or the normalized proxy-box space
a multiscale volume's vertex shader emits. It is not the data
coordinate system -- a request drops collapsed axes (numpy applies an
integer index and the axis disappears) and may start at a non-zero
origin.
There is one per visual per render mode, not one per visual: a
multiscale visual's 3D node is indexed in normalized space while its
2D node is in level-0 pixel coordinates. Everything that varies per
request -- the collapsed voxel indices and the window origin -- lives
on the paired visual -> data transform, not here, so a system is
rebuilt only when the displayed axes change.
Deliberately not per chunk: a multiscale brick's position never exists as a CPU-side transform, and modelling one per brick would produce objects no consumer reads.
Unlike :class:RenderedCoordinateSystem there is no rank check. A
rendered system is 2D or 3D because that is what a camera draws; a
visual system is whatever rank the request retained.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system_type
|
Literal['visual']
|
Discriminator field. |
required |
name
|
str
|
Human-readable name. Defaults to |
required |
visual_id
|
UUID4
|
The id of the visual whose geometry is indexed in this space. |
required |
Fields:
-
axes(tuple[Axis, ...]) -
id(UUID4) -
coordinate_system_type(Literal['visual']) -
name(str) -
visual_id(UUID4)
Validators:
-
_validate_axes
axis_by_name ¶
Return the single axis with the given name.
Axis names are not required to be unique (Q6), so ambiguity is a property of the query rather than of the model: this raises instead of returning the first match.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The axis name to look up. |
required |
Returns:
| Type | Description |
|---|---|
Axis
|
The matching axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no axis has that name. |
ValueError
|
If more than one axis has that name. |
resolve ¶
Resolve an axis reference to an axis index in this system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis
|
AxisRef
|
An axis name or an axis id. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The index of the referenced axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the reference does not name an axis of this system. |
ValueError
|
If a name is ambiguous within this system. |
resolve_axis ¶
from_data
classmethod
¶
from_data(data_coordinate_system: DataCoordinateSystem, retained_axes: Sequence[AxisRef], visual_id: UUID4, name: str = 'visual') -> Self
Build a visual system from the data axes a request retains.
Mirrors :meth:RenderedCoordinateSystem.from_world exactly: each
visual axis corresponds to one data axis, so name,
axis_type and unit are inherited from it rather than
re-specified (R5), and the axis ids are fresh because these
are distinct axes of a distinct system (D33).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_coordinate_system
|
DataCoordinateSystem
|
The data system the array is drawn from. |
required |
retained_axes
|
Sequence[AxisRef]
|
The data axes the request keeps, in the order the returned
array carries them. Collapsed axes are absent; they are
pinned on the paired |
required |
visual_id
|
UUID4
|
The id of the visual this space belongs to. |
required |
name
|
str
|
Human-readable name for the new system. |
'visual'
|
Returns:
| Type | Description |
|---|---|
VisualCoordinateSystem
|
A system of the same rank as |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
WorldCoordinateSystem
pydantic-model
¶
Bases: CoordinateSystem
A coordinate system that data can be transformed into for rendering.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system_type
|
Literal['world']
|
Discriminator field. |
required |
name
|
str
|
Human-readable name. Defaults to |
required |
Fields:
Validators:
-
_validate_axes
axis_by_name ¶
Return the single axis with the given name.
Axis names are not required to be unique (Q6), so ambiguity is a property of the query rather than of the model: this raises instead of returning the first match.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The axis name to look up. |
required |
Returns:
| Type | Description |
|---|---|
Axis
|
The matching axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no axis has that name. |
ValueError
|
If more than one axis has that name. |
resolve ¶
Resolve an axis reference to an axis index in this system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis
|
AxisRef
|
An axis name or an axis id. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The index of the referenced axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the reference does not name an axis of this system. |
ValueError
|
If a name is ambiguous within this system. |
AxisAlignedBoundingBox
pydantic-model
¶
Bases: BaseModel
An axis-aligned box, in one named coordinate system.
Bounds may be infinite: an unbounded axis is a first-class value
here (D31), and it is the answer a region gives for a displayed axis
nothing constrains. nan is rejected.
The coordinate system id is carried so that handing a world-space box to a data-space operation is a caught error rather than a silent wrong answer -- the same class of mistake the axis-order bugs in this repo's history were.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system
|
UUID4
|
The id of the coordinate system these bounds are expressed in. |
required |
min_coordinate
|
ndarray
|
Lower bounds, one per axis. May contain |
required |
max_coordinate
|
ndarray
|
Upper bounds, one per axis. May contain |
required |
Config:
frozen:Truearbitrary_types_allowed:True
Fields:
-
coordinate_system(UUID4) -
min_coordinate(ndarray) -
max_coordinate(ndarray)
Validators:
-
_coerce_bounds→min_coordinate,max_coordinate -
_validate_bounds
Plane
pydantic-model
¶
Bases: BaseModel
The set of points where normal . p == offset.
Point-normal form is deliberately not used: the transform rules of
design section 4.1 are stated directly on (normal, offset), and a
stored point would have to be re-derived on every map anyway.
The normal is not required to be unit length (D28). It is required to be finite and non-zero: a zero normal does not describe a plane.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system
|
UUID4
|
The id of the coordinate system this plane is expressed in. |
required |
normal
|
ndarray
|
The plane normal. Finite, non-zero, any magnitude. Read-only: a plane is changed by building a new one. |
required |
offset
|
float
|
The plane offset. Finite (D44). |
required |
Config:
frozen:Truearbitrary_types_allowed:True
Fields:
-
coordinate_system(UUID4) -
normal(ndarray) -
offset(float)
Validators:
-
_coerce_normal→normal -
_validate_offset→offset -
_validate_normal
from_point_normal
classmethod
¶
from_point_normal(coordinate_system: CoordinateSystem, point: ArrayLike, normal: ArrayLike, axes: Sequence[AxisRef] | None = None) -> Self
Build the plane through point with the given normal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system
|
CoordinateSystem
|
The system the plane is expressed in. |
required |
point
|
ArrayLike
|
A point on the plane. |
required |
normal
|
ArrayLike
|
The plane normal, the same length as point. |
required |
axes
|
Sequence[AxisRef] or None
|
The axes point and normal are given on, by name or id, in
the order of their entries. Every other axis of the system
gets a zero normal component: the plane does not constrain it.
|
None
|
Returns:
| Type | Description |
|---|---|
Plane
|
With |
signed_distance ¶
Distance of each point from the plane, positive along the normal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
ArrayLike
|
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
closest_point_to ¶
The point of the plane nearest to point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
ArrayLike
|
One |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
point moved along the normal onto the plane. |
DegenerateNormalError ¶
Bases: RuntimeError
Raised when a normal or plane maps to the zero vector (D32).
NonAffineTransformError ¶
Bases: RuntimeError
Raised where a transform must be affine and is not.
Two kinds of place raise it. The first is the GPU boundary: a pygfx node transform is a 4x4 matrix, and there is no matrix that expresses a non-uniform axis, so a non-affine block on a displayed axis has no honest answer -- the message names the axis, because the fix is to stop displaying it. The second is an operation that needs a spatially constant Jacobian -- a direction, a normal, a plane, a half-space -- which an irregularly spaced axis does not have.
It is deliberately one error rather than two: the underlying reason is the same in both, that there is no affine answer to hand back.
NonInvertibleTransformError ¶
Bases: RuntimeError
Raised when an inverse is required but cannot be computed.
AxisCoordinates
pydantic-model
¶
Bases: BaseModel
The sample positions of one irregularly-spaced axis.
values are the world positions of the sample centres -- one per
data index -- and edges are the world positions of the axis's two
outer edges. That is exactly the centre-versus-edge distinction a
data store already makes: a gridded axis of size voxels spans
(-0.5, size - 0.5) because a voxel is centred on its index, and this
class is the same statement for an axis whose spacing is irregular.
Keeping edges explicit is what lets a non-uniform axis answer the
question every store answers::
world extent on an axis = transform.map(store.axis_extents)
Mapping a store's extent means mapping data index -0.5, which linear
interpolation cannot extrapolate to from values alone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
tuple[float, ...]
|
World position of each sample centre, strictly increasing. A tuple
rather than an ndarray so that equality and hashing stay total --
an ndarray field makes |
required |
edges
|
tuple[float, float] or None
|
World position of the axis's outer edges. Must contain |
required |
Config:
frozen:True
Fields:
Validators:
-
_validate
resolved_edges
cached
property
¶
The outer edges, extrapolated by half a gap when not given.
NonUniformAxisTransform
pydantic-model
¶
Bases: BaseTransform
A 1-D transform between data indices and an irregular world axis.
Wraps Bijection(GridInterpolation, GridInterpolation) so the model
satisfies :class:~cellier.transform._base.BaseTransform's
transform: Transform field and reports a 1 -> 1 dimensionality
that validate_against can check. The wrapped object is not what
executes: the mapping is done here, because GridInterpolation.apply
allocates its output with zeros_like(coords) and so silently
truncates an integer input array.
Out of range does not clamp. Outside the axis's span the result is
nan -- there is no preimage, and a store has no data there. Earlier
revisions of the design clamped to the edge sample, which meant an
acquisition ending at 9 s re-showed its 9 s frame at 10, 11 and 12 s as
though it were data. nan rather than an exception because a call
maps a whole array and some of its points may be in range while others
are not.
Point and interval queries differ deliberately.
:meth:map_coordinates / :meth:imap_coordinates are point queries and
return nan outside the span. :meth:map_bounding_box /
:meth:imap_bounding_box are interval queries and intersect with the
span, so an interval that merely overhangs an end comes back clipped
rather than empty. Both are correct: a slice position outside the data
selects nothing, while a trail window reaching back past the first frame
should still start at the first frame. Do not "simplify" the two into
one behaviour -- collapsing to point semantics makes every trail window
vanish near the start of an axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinates
|
AxisCoordinates
|
The sample centres and outer edges. |
required |
interpolation
|
Literal['linear', 'nearest']
|
How a position between samples is mapped. Defaults to Note this differs from cellier's |
required |
Config:
frozen:Truearbitrary_types_allowed:True
Fields:
-
name(str | None) -
input_coordinate_system(UUID4) -
output_coordinate_system(UUID4) -
id(UUID4) -
transform_type(Literal['nonuniform_axis']) -
coordinates(AxisCoordinates) -
interpolation(Literal['linear', 'nearest']) -
transform(Bijection)
Validators:
-
_derive_transform
output_ndim
property
¶
output_ndim: int
Number of output dimensions, read from the wrapped transform.
validate_against ¶
validate_against(input_coordinate_system: CoordinateSystem, output_coordinate_system: CoordinateSystem) -> None
Check this transform against the two systems it claims to join.
The four checks of D9: both ids match, and both axis counts match the wrapped transform's dimensionality. A future registry calls this on insertion; defining it now means the registry inherits a decided rule rather than inventing one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_coordinate_system
|
CoordinateSystem
|
The system this transform should map from. |
required |
output_coordinate_system
|
CoordinateSystem
|
The system this transform should map to. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any of the four checks fails. |
broadcast_output_axes ¶
broadcast_output_axes() -> frozenset[UUID4]
Return the ids of the output axes the input has no extent along.
A dataset broadcast over an axis occupies all of it, not the point
its zero matrix row maps to, so a caller measuring where the data is
-- a world bounding box, say -- must skip these axes rather than read
a mapped coordinate off them. Structural, like
:meth:axis_correspondence: a block container answers from its
blocks, with no matrix involved.
Returns:
| Type | Description |
|---|---|
frozenset[UUID4]
|
Output axis ids. Empty unless a subclass records broadcasts. |
map_coordinates ¶
Map data indices to world positions.
Accepts (N, 1) or (1,) and returns matching rank. Positions
outside [-0.5, n_samples - 0.5] come back as nan.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinates
|
ndarray
|
Data indices. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
World positions, |
imap_coordinates ¶
Map world positions back to data indices.
Positions outside the axis's world span come back as nan: the
data does not reach there, and clamping to the edge sample would
present the last acquired plane as though it were data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinates
|
ndarray
|
World positions. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Data indices, |
map_bounding_box ¶
map_bounding_box(box: AxisAlignedBoundingBox, output_coordinate_system: CoordinateSystem) -> AxisAlignedBoundingBox
Map an index interval forward, exactly.
A monotonic map takes an interval's endpoints to the result's endpoints, so this is exact rather than merely conservative. The interval is intersected with the axis's span rather than rejected when it overhangs -- see the class docstring on point versus interval semantics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
box
|
AxisAlignedBoundingBox
|
A 1-D box in the input coordinate system. |
required |
output_coordinate_system
|
CoordinateSystem
|
The system to express the result in. |
required |
Returns:
| Type | Description |
|---|---|
AxisAlignedBoundingBox
|
The mapped interval, in the output coordinate system. |
imap_bounding_box ¶
imap_bounding_box(box: AxisAlignedBoundingBox) -> AxisAlignedBoundingBox
Map a world interval back, exactly.
Intersected with the axis's world span, not rejected: this is the call a trail window's endpoints go through, and a window reaching past the first sample must still start at the first sample.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
box
|
AxisAlignedBoundingBox
|
A 1-D box in the output coordinate system. |
required |
Returns:
| Type | Description |
|---|---|
AxisAlignedBoundingBox
|
The interval in the input coordinate system. |
map_direction ¶
Not answerable: the Jacobian varies along the axis.
imap_direction ¶
Not answerable: the Jacobian varies along the axis.
map_normal ¶
Not answerable: the Jacobian varies along the axis.
imap_normal ¶
Not answerable: the Jacobian varies along the axis.
map_region ¶
map_region(region: ConvexRegion) -> ConvexRegion
Not answerable: a half-space needs a constant normal.
imap_region ¶
imap_region(region: ConvexRegion, output_coordinate_system: CoordinateSystem) -> ConvexRegion
Not answerable: a half-space needs a constant normal.
input_domain ¶
The table's index span, (-0.5, n_samples - 0.5).
The same edge convention a gridded store reports: sample i is
centred on integer i and its cell reaches half a unit either
side, so the axis as a whole spans half a unit past its end samples.
Returns:
| Type | Description |
|---|---|
dict[int, tuple[float, float]]
|
|
axis_correspondence ¶
inverse ¶
inverse() -> BaseTransform | None
No inverse as a transform of this class.
The map is invertible -- :meth:imap_coordinates performs the
inversion -- but its inverse is not itself a table of world
positions per data index, which is what this class models. Rather
than invent a second class nothing yet consumes, this returns
None and callers use the imap_* methods.
Returns:
| Type | Description |
|---|---|
None
|
Always. |
to_affine ¶
Never affine.
Returns:
| Type | Description |
|---|---|
None
|
Always. An irregular table has no matrix, which is the whole reason this class exists. |
restrict ¶
restrict(fixed: Mapping[AxisRef | int, float], input_coordinate_system: CoordinateSystem | None = None) -> BaseTransform
Always raises; a 1-D leaf has nothing left after being fixed.
Fixing this transform's only axis leaves a zero-dimensional
transform, which is not modelled. Collapsing a fully-fixed axis to
a constant is the container's job:
ByDimensionTransform.restrict evaluates such a block directly
rather than delegating here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fixed
|
Mapping[AxisRef | int, float]
|
Ignored. |
required |
input_coordinate_system
|
CoordinateSystem or None
|
Ignored. |
None
|
Raises:
| Type | Description |
|---|---|
NonAffineTransformError
|
Always. |
__eq__ ¶
Compare by endpoints, table and interpolation, not by id.
Defined explicitly because the wrapped transformnd classes
define no __eq__ at all: two independently built leaves over
the same table would otherwise compare unequal by object identity.
ConvexRegion
pydantic-model
¶
Bases: BaseModel
An intersection of half-spaces. An empty tuple is all of space.
This is the query type; :class:AxisAlignedBoundingBox is the
answer type, and :meth:from_bounding_box / :meth:bounding_box
bridge them. There is deliberately only one region type (D38): an
axis-aligned selection, an oblique slab and a camera frustum are the
same object, and the axis-aligned fast path is an implementation
detail rather than a kind.
ndim is stored rather than derived. An unbounded region has no
half-spaces at all, so nothing else in the model carries its rank.
It is validated against every half-space normal, so it cannot
disagree with them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system
|
UUID4
|
The id of the coordinate system this region is expressed in. |
required |
ndim
|
int
|
The rank of that coordinate system. |
required |
half_spaces
|
tuple[HalfSpace, ...]
|
The constraints. Empty means all of space. |
required |
Config:
frozen:Truearbitrary_types_allowed:True
Fields:
Validators:
-
_validate_rank
bounding_box ¶
bounding_box() -> AxisAlignedBoundingBox
Return the smallest axis-aligned box containing this region.
Exact, not conservative: the bounds come from 2 * ndim linear
programs, or -- when every normal lies along a single axis --
are read off directly, which is roughly 1000x faster and covers
every axis-aligned selection shipping today (D40).
An unconstrained axis comes back -inf / +inf.
A zero-thickness region is not special-cased (D42): the box comes
back with min == max on that axis, and converting that to
voxel indices, with whatever rounding rule applies, is the
datastore's job.
Returns:
| Type | Description |
|---|---|
AxisAlignedBoundingBox
|
The exact bounds, in this region's coordinate system. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the region is empty, and so has no bounds. Check
:meth: |
contains ¶
Whether each point satisfies every constraint.
This answers a question about continuous points and is not
the voxel selection API. A zero-thickness region is a
measure-zero set, so this is float-exact and effectively always
False on one (D42). Voxel selection goes through
:meth:bounding_box plus the datastore's rounding rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
ndarray
|
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
simplify ¶
simplify() -> ConvexRegion
Drop constraints that have become vacuous.
Vacuous constraints appear under composition: pulling a region back through a transform with no extent along some axis turns every constraint on that axis into a zero normal, which either excludes nothing or excludes everything.
Constraints that are infeasible are kept, not dropped:
dropping one would change an empty region into a non-empty one.
Ask :meth:is_empty for that.
Returns:
| Type | Description |
|---|---|
ConvexRegion
|
A region with the same solution set and no vacuous constraints. |
unbounded
classmethod
¶
unbounded(coordinate_system: CoordinateSystem) -> Self
Return the region that is all of a coordinate system's space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system
|
CoordinateSystem
|
The system the region lives in. The object, not the id: the rank has to come from somewhere, and an empty constraint set does not carry it. |
required |
Returns:
| Type | Description |
|---|---|
ConvexRegion
|
A region with no constraints. |
from_bounding_box
classmethod
¶
from_bounding_box(box: AxisAlignedBoundingBox) -> Self
Build the region equivalent to an axis-aligned box.
Infinite bounds contribute no constraint, since p_i <= inf
excludes nothing (and an infinite offset is rejected by D44).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
box
|
AxisAlignedBoundingBox
|
The box to convert. |
required |
Returns:
| Type | Description |
|---|---|
ConvexRegion
|
A region with up to |
from_axis_slabs
classmethod
¶
from_axis_slabs(coordinate_system: CoordinateSystem, slabs: Mapping[AxisRef, tuple[float, float]]) -> Self
Build a region from per-axis (centre, half_thickness) slabs.
An axis absent from slabs is unbounded. The region has no
notion of "displayed" versus "collapsed" (R3): any axis may be
bounded, and a displayed axis is bounded exactly as readily as a
collapsed one. That distinction lives in the transform, as the
difference between a column and a constant column.
A half_thickness of zero is allowed and produces a
zero-thickness slab (D42).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system
|
CoordinateSystem
|
The system the region lives in. |
required |
slabs
|
Mapping[AxisRef, tuple[float, float]]
|
|
required |
Returns:
| Type | Description |
|---|---|
ConvexRegion
|
A region with two constraints per named axis. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a half thickness is negative. |
from_plane_slab
classmethod
¶
from_plane_slab(coordinate_system: CoordinateSystem, normal: ndarray, offset: float, half_thickness: float) -> Self
Build an oblique slab of a given thickness about a plane.
The slab is abs(normal . p - offset) <= half_thickness *
norm(normal), so half_thickness is a distance in the
coordinate system's units regardless of the normal's magnitude.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinate_system
|
CoordinateSystem
|
The system the region lives in. |
required |
normal
|
ndarray
|
The plane normal. Need not be unit length (D28). |
required |
offset
|
float
|
The plane offset. |
required |
half_thickness
|
float
|
Half the slab thickness, as a distance. Zero is allowed. |
required |
Returns:
| Type | Description |
|---|---|
ConvexRegion
|
A region with two opposed constraints. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the normal is zero or the wrong rank, or the half thickness is negative. |
intersection
classmethod
¶
intersection(*regions: ConvexRegion) -> Self
Intersect regions that share a coordinate system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*regions
|
ConvexRegion
|
At least one region, all in the same coordinate system. |
()
|
Returns:
| Type | Description |
|---|---|
ConvexRegion
|
A region carrying every constraint of every input. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no regions are given, or they do not all share a coordinate system and rank. |
HalfSpace
pydantic-model
¶
Bases: BaseModel
The set {p : normal . p <= offset}.
There is no inside field: the sign of the normal carries the
side, so a slab is two half-spaces with opposed normals (D37). There
is no coordinate system id either -- every constraint in a region is
necessarily in the same system, so storing it per-constraint would
only create something that can disagree with itself.
Unlike :class:~cellier.transform.Plane, a zero normal is
allowed. That is not an oversight: pulling a world constraint back
through a transform with no extent along that axis produces exactly
that, and the honest answers are "vacuous" (offset >= 0) and
"infeasible" (offset < 0) rather than an error (D39).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
normal
|
ndarray
|
The constraint normal. Finite, any magnitude, possibly zero. |
required |
offset
|
float
|
The constraint offset. Finite (D44). |
required |
Config:
frozen:Truearbitrary_types_allowed:True
Fields:
-
normal(ndarray) -
offset(float)
Validators:
-
_coerce_normal→normal -
_validate_offset→offset -
_validate_normal
RegionSelection
pydantic-model
¶
Bases: BaseModel
A slice position and its extent, as one frozen artifact.
This is the authoritative artifact the slicer consumes. A
DimsManager becomes an editor that holds the ergonomic
parameterization -- an index and a thickness, the thing a GUI binds
to -- and emits one of these; an oblique editor holds a plane and a
thickness and emits the same type. The slicer consumes one thing and
does not know which editor produced it (R6).
The numbers stay in one place. The transform's constant column holds the slice position and the region holds its extent. Both are downstream of the editor's index, and neither is stored twice.
The region is in world coordinates, not rendered. Rendered space has only the two or three displayed axes and so cannot express a thickness on a collapsed one; world space expresses both, and obliquity is carried by the half-space normals rather than by another coordinate system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transform
|
AffineTransform
|
The rendered -> world embedding (D34). Its translation column carries the slice indices. |
required |
region
|
ConvexRegion
|
The selected region, in the transform's output (world) coordinate system. |
required |
Config:
frozen:True
Fields:
-
transform(AffineTransform) -
region(ConvexRegion)
Validators:
-
_validate_consistency
slice_position
property
¶
The world point the rendered origin maps to.
This is the transform's translation column, which is where the slice indices live (D35).