Skip to content

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 {0}. Recording it is not reconstructible later (D25).

required

Config:

  • frozen: True
  • arbitrary_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
input_ndim property
input_ndim: int

Number of input dimensions, read from the wrapped transform.

output_ndim property
output_ndim: int

Number of output dimensions, read from the wrapped transform.

matrix property
matrix: ndarray

The full (D_out + 1, D_in + 1) augmented matrix.

linear property
linear: ndarray

The (D_out, D_in) linear block.

translation property
translation: ndarray

The (D_out,) translation column.

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 None when there is no left inverse.

map_coordinates
map_coordinates(coordinates: ndarray) -> ndarray

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
imap_coordinates(coordinates: ndarray) -> ndarray

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_direction(direction: ndarray) -> ndarray

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
imap_direction(direction: ndarray) -> ndarray

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_normal(normal: ndarray) -> ndarray

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
imap_normal(normal: ndarray) -> ndarray

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_plane(plane: Plane) -> 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
imap_plane(plane: Plane) -> 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 broadcast_axes to axis indices.

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

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 broadcast_axes to axis indices. Required for the same reason it is on :meth:map_bounding_box and :meth:then: the field holds axis ids and the arithmetic needs indices, and a transform stores only ids.

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 other's input.

required
output_coordinate_system CoordinateSystem

other's output system.

required

Returns:

Type Description
AffineTransform

A transform from this one's input to other's output.

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]

{input axis: output axis}. Every input axis must appear. Mapped pairs must share an axis_type (D26); units are not checked (D16).

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 broadcast_axes.

()
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 broadcast_axes: a broadcast axis is unbounded, a slice index is not (D35).

None
name str or None

Optional name for the transform.

None

Returns:

Type Description
AffineTransform

A transform of shape (D_out + 1, D_in + 1).

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 axis_type.

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 (D_out + 1, D_in + 1) augmented matrix.

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
input_domain() -> dict[int, tuple[float, float]]

An affine transform has no intrinsic domain, so this is empty.

A matrix maps every real coordinate to another; whatever bounds the data has come from the store's extent, not from the transform.

Returns:

Type Description
dict[int, tuple[float, float]]

Always empty.

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
axis_correspondence() -> dict[int, int]

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 _check_transform_no_rotation, which imposes the same restriction on the multiscale brick shader.

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]

{input axis: value}. Values are exact -- nothing here rounds or clamps them.

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__
__eq__(other: object) -> bool

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.

__hash__
__hash__() -> int

Hash by endpoints, matrix and broadcast axes.

Defined alongside __eq__ because transformnd.Affine defines __eq__ without __hash__ and is therefore unhashable, which would make a frozen model containing one raise on hash().

Axis pydantic-model

Bases: BaseModel

A single axis of a coordinate system.

Parameters:

Name Type Description Default
name str

Human-readable name, e.g. "z". Names are not required to be unique within a coordinate system, but querying an ambiguous name raises.

required
axis_type AxisType

The RFC-5 axis type. Required: there is no default, because a channel or time axis silently typed as "space" fails far from where it was constructed. Serialized as type; both spellings parse.

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 ("discrete") or measured positions ("continuous").

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 axis_type this has a default, and the default is "continuous". A wrong axis_type selects the wrong plane silently; a wrong sampling degrades to the behaviour that predates the field, which is a visible half-interval lag rather than a silent wrong answer. Gridded stores set it themselves -- a voxel grid is sample-indexed by construction -- so the value is only ever authored for geometry.

required
id UUID4

Unique identifier. Auto-generated.

required

Config:

  • frozen: True

Fields:

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 transformnd transform.

required
id UUID4

Unique identifier. Auto-generated. Not (input, output), because two coordinate systems may be joined by more than one transform (D14).

required

Fields:

  • name (str | None)
  • input_coordinate_system (UUID4)
  • output_coordinate_system (UUID4)
  • transform (Transform)
  • id (UUID4)
input_ndim property
input_ndim: int

Number of input dimensions, read from the wrapped 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.

map_coordinates abstractmethod
map_coordinates(coordinates: ndarray) -> ndarray

Map points from the input system to the output system.

imap_coordinates abstractmethod
imap_coordinates(coordinates: ndarray) -> ndarray

Map points from the output system back to the input system.

map_direction abstractmethod
map_direction(direction: ndarray) -> ndarray

Map displacement vectors forward (contravariant).

imap_direction abstractmethod
imap_direction(direction: ndarray) -> ndarray

Map displacement vectors back (contravariant).

map_normal abstractmethod
map_normal(normal: ndarray) -> ndarray

Map plane normals forward (covariant).

imap_normal abstractmethod
imap_normal(normal: ndarray) -> ndarray

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

Map an axis-aligned box back, conservatively.

map_plane abstractmethod
map_plane(plane: Plane) -> Plane

Map a plane forward.

imap_plane abstractmethod
imap_plane(plane: Plane) -> Plane

Map a plane back.

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
input_domain() -> dict[int, tuple[float, float]]

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 (low, high), in input coordinates. Bounds are inclusive.

axis_correspondence abstractmethod
axis_correspondence() -> dict[int, int]

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]

{input axis: value}. An int key is an axis index and needs nothing else; a name or id key needs input_coordinate_system to resolve against.

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

self for an affine transform.

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: True
  • arbitrary_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
input_ndim property
input_ndim: int

Number of input dimensions, read from the wrapped 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]

{input axis: output axis}. Every input axis must appear, including those in axis_transforms.

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

{input axis: 1-D transform}. The transform carries that axis's whole mapping.

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 axis_transforms and scale or translation, if a per-axis transform is not 1-D, or for any reason AffineTransform.from_axis_map would raise.

map_coordinates
map_coordinates(coordinates: ndarray) -> ndarray

Map points forward, block by block.

Parameters:

Name Type Description Default
coordinates ndarray

(N, D_in) or (D_in,) points.

required

Returns:

Type Description
ndarray

(N, D_out) or (D_out,), matching the input's rank.

imap_coordinates
imap_coordinates(coordinates: ndarray) -> ndarray

Map points back, block by block.

Parameters:

Name Type Description Default
coordinates ndarray

(N, D_out) or (D_out,) points.

required

Returns:

Type Description
ndarray

(N, D_in) or (D_in,), matching the input's rank.

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

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
input_domain() -> dict[int, tuple[float, float]]

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 (low, high), for bounded axes only.

broadcast_output_axes
broadcast_output_axes() -> frozenset[UUID]

Union of every block's broadcast output axes.

Blocks keep the container's axis ids on their own sub-systems, so a block's answer needs no translation.

Returns:

Type Description
frozenset[UUID]

Output axis ids.

axis_correspondence
axis_correspondence() -> dict[int, int]

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]

{input axis: value}. Exact; nothing here rounds or clamps.

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 None when some block has no matrix.

inverse
inverse() -> BaseTransform | None

Invert every block, or return None if any block cannot.

Returns:

Type Description
BaseTransform or None

The inverse, or None.

map_direction
map_direction(direction: ndarray) -> ndarray

Map a displacement vector, if every block is affine.

imap_direction
imap_direction(direction: ndarray) -> ndarray

Map a displacement vector back, if every block is affine.

map_normal
map_normal(normal: ndarray) -> ndarray

Map a normal, if every block is affine.

imap_normal
imap_normal(normal: ndarray) -> ndarray

Map a normal back, if every block is affine.

map_plane
map_plane(plane)

Map a plane, if every block is affine.

imap_plane
imap_plane(plane)

Map a plane 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 AxisAlignedSelection produces 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.

__eq__
__eq__(other: object) -> bool

Compare by endpoints and blocks, not by id.

__hash__
__hash__() -> int

Hash by endpoints and block structure.

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 input_axes: a block that broadcasts over an output axis is non-square, which is normal here rather than an edge case.

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 id matches transform.input_coordinate_system.

required
output_coordinate_system CoordinateSystem

The block's own output system, likewise.

required

Config:

  • frozen: True

Fields:

Validators:

  • _validate
is_affine property
is_affine: bool

Whether this block can be expressed as a matrix.

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:

  • coordinate_system_type (Literal['coordinate_system'])
  • name (str)
  • axes (tuple[Axis, ...])
  • id (UUID4)

Validators:

  • _validate_axes
ndim property
ndim: int

Number of axes.

axis_names
axis_names() -> tuple[str, ...]

Return the axis names, in order.

index_of
index_of(axis_id: UUID4) -> int

Return the index of the axis with the given id.

Parameters:

Name Type Description Default
axis_id UUID4

The id to look up.

required

Returns:

Type Description
int

The index of the axis in axes.

Raises:

Type Description
KeyError

If no axis in this system has that id.

axis_by_name
axis_by_name(name: str) -> Axis

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(axis: AxisRef) -> int

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
resolve_axis(axis: AxisRef) -> Axis

Resolve an axis reference to the :class:Axis itself.

Parameters:

Name Type Description Default
axis AxisRef

An axis name or an axis id.

required

Returns:

Type Description
Axis

The referenced axis.

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. name is the datastore's name by convention (Q7), but the model has no access to the datastore and does not validate that.

required

Fields:

  • name (str)
  • axes (tuple[Axis, ...])
  • id (UUID4)
  • coordinate_system_type (Literal['data'])
  • datastore_id (UUID4)

Validators:

  • _validate_axes
ndim property
ndim: int

Number of axes.

axis_names
axis_names() -> tuple[str, ...]

Return the axis names, in order.

index_of
index_of(axis_id: UUID4) -> int

Return the index of the axis with the given id.

Parameters:

Name Type Description Default
axis_id UUID4

The id to look up.

required

Returns:

Type Description
int

The index of the axis in axes.

Raises:

Type Description
KeyError

If no axis in this system has that id.

axis_by_name
axis_by_name(name: str) -> Axis

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(axis: AxisRef) -> int

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
resolve_axis(axis: AxisRef) -> Axis

Resolve an axis reference to the :class:Axis itself.

Parameters:

Name Type Description Default
axis AxisRef

An axis name or an axis id.

required

Returns:

Type Description
Axis

The referenced axis.

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 "rendered".

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
ndim property
ndim: int

Number of axes.

axis_names
axis_names() -> tuple[str, ...]

Return the axis names, in order.

index_of
index_of(axis_id: UUID4) -> int

Return the index of the axis with the given id.

Parameters:

Name Type Description Default
axis_id UUID4

The id to look up.

required

Returns:

Type Description
int

The index of the axis in axes.

Raises:

Type Description
KeyError

If no axis in this system has that id.

axis_by_name
axis_by_name(name: str) -> Axis

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(axis: AxisRef) -> int

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
resolve_axis(axis: AxisRef) -> Axis

Resolve an axis reference to the :class:Axis itself.

Parameters:

Name Type Description Default
axis AxisRef

An axis name or an axis id.

required

Returns:

Type Description
Axis

The referenced 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 displayed_axes.

Raises:

Type Description
ValueError

If displayed_axes names the same world axis twice, or has a rank other than 2 or 3.

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 "visual".

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
ndim property
ndim: int

Number of axes.

axis_names
axis_names() -> tuple[str, ...]

Return the axis names, in order.

index_of
index_of(axis_id: UUID4) -> int

Return the index of the axis with the given id.

Parameters:

Name Type Description Default
axis_id UUID4

The id to look up.

required

Returns:

Type Description
int

The index of the axis in axes.

Raises:

Type Description
KeyError

If no axis in this system has that id.

axis_by_name
axis_by_name(name: str) -> Axis

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(axis: AxisRef) -> int

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
resolve_axis(axis: AxisRef) -> Axis

Resolve an axis reference to the :class:Axis itself.

Parameters:

Name Type Description Default
axis AxisRef

An axis name or an axis id.

required

Returns:

Type Description
Axis

The referenced 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 visual -> data transform as constant_output_axes.

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 retained_axes.

Raises:

Type Description
ValueError

If retained_axes names the same data axis twice.

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 "world".

required

Fields:

Validators:

  • _validate_axes
ndim property
ndim: int

Number of axes.

axis_names
axis_names() -> tuple[str, ...]

Return the axis names, in order.

index_of
index_of(axis_id: UUID4) -> int

Return the index of the axis with the given id.

Parameters:

Name Type Description Default
axis_id UUID4

The id to look up.

required

Returns:

Type Description
int

The index of the axis in axes.

Raises:

Type Description
KeyError

If no axis in this system has that id.

axis_by_name
axis_by_name(name: str) -> Axis

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(axis: AxisRef) -> int

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
resolve_axis(axis: AxisRef) -> Axis

Resolve an axis reference to the :class:Axis itself.

Parameters:

Name Type Description Default
axis AxisRef

An axis name or an axis id.

required

Returns:

Type Description
Axis

The referenced axis.

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 -inf.

required
max_coordinate ndarray

Upper bounds, one per axis. May contain +inf.

required

Config:

  • frozen: True
  • arbitrary_types_allowed: True

Fields:

  • coordinate_system (UUID4)
  • min_coordinate (ndarray)
  • max_coordinate (ndarray)

Validators:

  • _coerce_bounds → min_coordinate, max_coordinate
  • _validate_bounds
ndim property
ndim: int

Number of axes.

__eq__
__eq__(other: object) -> bool

Compare by coordinate system and bounds (D44).

__hash__
__hash__() -> int

Hash by coordinate system and bounds (D44).

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: True
  • arbitrary_types_allowed: True

Fields:

  • coordinate_system (UUID4)
  • normal (ndarray)
  • offset (float)

Validators:

  • _coerce_normal → normal
  • _validate_offset → offset
  • _validate_normal
ndim property
ndim: int

Number of axes.

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 means every axis of the system, in its order.

None

Returns:

Type Description
Plane

With offset = normal . point.

signed_distance
signed_distance(points: ArrayLike) -> ndarray

Distance of each point from the plane, positive along the normal.

Parameters:

Name Type Description Default
points ArrayLike

(n, ndim) points, or one (ndim,) point.

required

Returns:

Type Description
ndarray

(normal . p - offset) / |normal|, one value per point.

closest_point_to
closest_point_to(point: ArrayLike) -> ndarray

The point of the plane nearest to point.

Parameters:

Name Type Description Default
point ArrayLike

One (ndim,) point.

required

Returns:

Type Description
ndarray

point moved along the normal onto the plane.

__eq__
__eq__(other: object) -> bool

Compare by coordinate system, normal and offset (D44).

__hash__
__hash__() -> int

Hash by coordinate system, normal and offset (D44).

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 __eq__ return an array, which degrades every containing model's comparison.

required
edges tuple[float, float] or None

World position of the axis's outer edges. None (the default) extrapolates by half the gap at each end, which is the choice that gives the first and last samples a full nearest-neighbour catchment rather than a half one. The padding is naturally asymmetric when the first and last gaps differ.

Must contain values: edges[0] <= values[0] and edges[1] >= values[-1]. A narrower span would make samples unreachable, which is silent data loss; shorten values instead.

required

Config:

  • frozen: True

Fields:

Validators:

  • _validate
resolved_edges cached property
resolved_edges: tuple[float, float]

The outer edges, extrapolated by half a gap when not given.

values_array cached property
values_array: ndarray

values as a read-only float array.

n_samples property
n_samples: int

Number of samples on the axis.

index_domain cached property
index_domain: tuple[float, float]

The valid span in data-index units, (-0.5, n_samples - 0.5).

The same edge convention a gridded store reports, so a leaf and a grid answer axis_extents in the same units.

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 "linear", which is what makes this a continuous coordinate map.

Note this differs from cellier's "nearest" default for image sampling, and deliberately: that convention is about choosing a sample to read, while this is about mapping a position. The pipeline's nearest-ness is applied one layer up, by round_world_to_voxel -- and a linear inverse followed by that rounding is exactly nearest-neighbour in world units, verified across the whole domain. Choosing "nearest" here instead snaps positions to sample centres, which also collapses the axis's extent onto (values[0], values[-1]); that is self-consistent, but it is not the continuous map the slicing path expects.

required

Config:

  • frozen: True
  • arbitrary_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
input_ndim property
input_ndim: int

Number of input dimensions, read from the wrapped 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_coordinates(coordinates: ndarray) -> ndarray

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, nan where there is no preimage.

imap_coordinates
imap_coordinates(coordinates: ndarray) -> ndarray

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, nan where there is no preimage.

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

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
map_direction(direction: ndarray) -> ndarray

Not answerable: the Jacobian varies along the axis.

imap_direction
imap_direction(direction: ndarray) -> ndarray

Not answerable: the Jacobian varies along the axis.

map_normal
map_normal(normal: ndarray) -> ndarray

Not answerable: the Jacobian varies along the axis.

imap_normal
imap_normal(normal: ndarray) -> ndarray

Not answerable: the Jacobian varies along the axis.

map_plane
map_plane(plane)

Not answerable: a plane needs a spatially constant Jacobian.

imap_plane
imap_plane(plane)

Not answerable: a plane needs a spatially constant Jacobian.

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
input_domain() -> dict[int, tuple[float, float]]

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]]

{0: (low, high)}.

axis_correspondence
axis_correspondence() -> dict[int, int]

The leaf is 1-D, so its one input axis reaches its one output axis.

Structural, with no matrix involved -- which is the point: a non-affine transform can still say which axis becomes which.

Returns:

Type Description
dict[int, int]

Always {0: 0}.

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
to_affine() -> None

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__
__eq__(other: object) -> bool

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.

__hash__
__hash__() -> int

Hash by endpoints, table and interpolation.

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: True
  • arbitrary_types_allowed: True

Fields:

Validators:

  • _validate_rank
normals property
normals: ndarray

The (M, ndim) stack of constraint normals.

offsets property
offsets: ndarray

The (M,) stack of constraint offsets.

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:is_empty first when that is possible.

contains
contains(points: ndarray) -> ndarray

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

(N, ndim) or (ndim,) points.

required

Returns:

Type Description
ndarray

(N,) booleans, or a scalar boolean for 1-D input.

is_empty
is_empty() -> bool

Whether the constraints are mutually infeasible.

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 2 * ndim constraints.

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]]

{axis: (centre, half_thickness)}, keyed by axis name or axis id.

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: True
  • arbitrary_types_allowed: True

Fields:

  • normal (ndarray)
  • offset (float)

Validators:

  • _coerce_normal → normal
  • _validate_offset → offset
  • _validate_normal
ndim property
ndim: int

Number of axes.

is_vacuous
is_vacuous() -> bool

Whether this constraint excludes nothing (a zero normal, offset >= 0).

is_infeasible
is_infeasible() -> bool

Whether this constraint excludes everything (a zero normal, offset < 0).

__eq__
__eq__(other: object) -> bool

Compare by normal and offset (D44).

__hash__
__hash__() -> int

Hash by normal and offset (D44).

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:

Validators:

  • _validate_consistency
slice_position property
slice_position: ndarray

The world point the rendered origin maps to.

This is the transform's translation column, which is where the slice indices live (D35).