astrix.spatial package

class Frame(rot, loc=None, ref_frame=None, backend=None, name='unnamed-frame')[source]

Bases: object

A reference frame defined by a rotation and location. Can be static or time-varying, and can have rotation defined relative to another Frame. Combines RotationLike and Location objects, and manages time associations.

Parameters:
  • rot (Rotation | RotationSequence) – A scipy Rotation object (single rotation) or RotationSequence (time-tagged rotations). If a single Rotation is provided, the frame rotation is static.

  • loc (Location, optional) – A Location object (Point or Path) defining the frame origin in ECEF coordinates. If not provided, the frame origin is assumed to be at the origin of the reference frame. If loc is provided, it must be a singular Point (1x3) for static frames. Use Path objects for time-varying locations.

  • ref_frame (Frame, optional) – A reference Frame object to define the rotation relative to. If not provided, the rotation is assumed to be absolute (e.g., from ECEF frame).

  • backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.

  • name (str)

Examples

Static frame with static rotation and location:

>>> from astrix.primitives import Frame, Point
>>> from scipy.spatial.transform import Rotation
>>>
>>> rot = Rotation.from_euler(
...     "xyz", [90, 0, 0], degrees=True
... )  # 90 degree rotation about x-axis
>>> loc = Point.from_geodet([27.47, 153.03, 0])  # Brisbane location
>>> frame_static = Frame(rot, loc)  # Frame with static rotation and location
>>> frame_static.interp_rot().as_euler("xyz", degrees=True)  # Get absolute rotation
array([[90.,  0.,  0.]])
>>> frame_static.loc.geodet  # Get frame location in geodetic coordinates
array([[153.03, 27.47, 0.0]])

Time-varying frame with rotation sequence and static location:

>>> from astrix.primitives import Time
>>> from datetime import datetime, timezone
>>>
>>> times = Time.from_datetime(
...     [
...         datetime(2021, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
...         datetime(2021, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
...         datetime(2021, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
...     ]
... )
>>> rots = Rotation.from_euler(
...     "xyz",
...     [
...         [0, 0, 0],
...         [90, 0, 0],
...         [180, 0, 0],
...     ],
...     degrees=True,
... )
>>> rot_seq = RotationSequence(rots, times)
>>> loc = Point.from_geodet([27.47, 153.03, 0])  # Brisbane location
>>> frame_dynamic_rot = Frame(
...     rot_seq, loc
... )  # Frame with time-varying rotation and static location
>>> interp_rot = frame_dynamic_rot.interp_rot(
...     Time.from_datetime(datetime(2021, 1, 1, 12, 30, 0, tzinfo=timezone.utc))
... )  # Interpolate to halfway between first and second rotation
>>> interp_rot.as_euler(
...     "xyz", degrees=True
... )  # Get interpolated absolute rotation as Euler angles
array([[45.,  0.,  0.]])
>>> frame_dynamic_rot.loc.geodet  # Get frame location in geodetic coordinates
array([[153.03, 27.47, 0.0]])

Frame defined relative to another frame:

>>> rot_ref = Rotation.from_euler(
...     "xyz", [0, 30, 0], degrees=True
... )  # Reference frame
>>> frame_ref = Frame(rot_ref, loc)  # Reference frame
>>> rot_rel = Rotation.from_euler("xyz", [0, 40, 0], degrees=True)
>>> frame = Frame(rot_rel, ref_frame=frame_ref)
>>>
>>> frame.interp_rot().as_euler(
...     "xyz", degrees=True
... )  # Absolute rotation (rot_ref * rot_rel)
array([[ 0., 70.,  0.]])
>>> frame.loc.geodet  # (Same as reference frame)
array([[153.03, 27.47, 0.0]])

Notes

  • If both loc and ref_frame are provided, the new frame location is used and the reference frame location is disregarded.

  • A TimeGroup object is created internally to manage time associations between rotation, location, and reference frame.

  • If the frame is static (single rotation and singular Point), the time properties return TIME_INVARIANT.

  • Use Path objects for time-varying locations.

convert_to(backend)[source]

Convert the Frame object to a different backend.

Return type:

Frame

Parameters:

backend (str | Any | None)

index_loc(index)[source]

Get the location of the frame at the given index.

Warning: This should only be used after downsampling so that location and rotation indeces align. Prefer interp_rot for general use.

Return type:

Point

Parameters:

index (int)

index_rot(index)[source]

Get the absolute rotation of the frame at the given index.

Warning: This should only be used after downsampling so that location and rotation indeces align. Prefer interp_rot for general use.

Return type:

Rotation

Parameters:

index (int)

interp_loc(time=TimeInvariant object, check_bounds=True)[source]

Get the interpolated location of the frame at the given times. If the location is static, time can be None.

Return type:

Point

Parameters:
interp_rot(time=TimeInvariant object, check_bounds=True)[source]

Get the interpolated absolute rotation of the frame at the given times. If all rotations are time invariant, time can be None.

Return type:

Rotation

Parameters:
replace_rot(frame_name, new_rot)[source]

Replace a rotation in the rotation chain with a new rotation.

This is an advanced feature and currently only applicable for static rotations. Should primarily be used for optimisation purposes in autograd frameworks.

Parameters:
  • frame_name (str) – Name of the frame whose rotation is to be replaced.

  • new_rot (Rotation) – New scipy Rotation object to replace the existing rotation.

Return type:

Frame

sample_at_time(time)[source]

Sample the Frame object at specific times, returning a new Frame with time-varying components sampled at those times. The new frame can then be indexed at these times directly to avoid interpolation

Parameters:

time (Time) – Time object specifying the times to sample the Frame at.

Returns:

New Frame object with components sampled at the specified times.

Return type:

Frame

property backend: str

Get the name of the array backend in use (e.g., ‘numpy’, ‘jax’).

property has_ref: bool

Check if the frame has a reference frame.

property is_static: bool

Check if the frame is static (single rotation and singular Point location).

property loc: Location[TimeLike]

Get the location of the frame in ECEF coordinates.

property name: str

Get the name of the frame.

property name_chain: list[str]

Get the names of all frames in the rotation chain, from base to current.

property path: Path

Get the time-varying Path of the frame location, if applicable. If the frame location is static, raises AttributeError.

property point: Point

Get the singular Point of the frame location, if applicable. If the frame location is time-varying, raises AttributeError.

property rel_rot: RotationLike

Get the last rotation of the frame relative to the reference frame.

property time_bounds: tuple[TimeLike, TimeLike]

Get the time bounds of the frame as a tuple (start_time, end_time). If the frame is static, returns TIME_INVARIANT.

property time_group: TimeGroup

Get the Time object associated with the frame, if any.

class Path(point, backend=None)[source]

Bases: Location[Time]

Path of multiple Point objects with associated Time. Enables interpolation between points over time and calculation of velocity. Must have at least 2 points with associated Time.

Parameters:
  • point (Point | list of Point) – Point object or list of Point objects with associated Time. If a list is provided, all Points must have the same backend and associated Time.

  • backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.

Examples

Instantiating a Path from a list of Points:

>>> from astrix.primitives import Point, Time, Path
>>> from datetime import datetime, timezone
>>> times = Time.from_datetime(
...     [
...         datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
...         datetime(2025, 1, 1, 12, 0, 1, tzinfo=timezone.utc),
...         datetime(2025, 1, 1, 12, 0, 2, tzinfo=timezone.utc),
...     ]
... )
>>> path = Path(
...     [
...         Point([1, 2, 0], time=times[0]),
...         Point([2, 3.8, 0.4], time=times[1]),
...         Point([3, 6.0, 1], time=times[2]),
...     ]
... )  # Somewhere very hot in the middle of the Earth

Interpolate the Path to a new time and get velocity:

>>> path.interp(
        Time.from_datetime(datetime(2025, 1, 1, 12, 0, 1, 500000, tzinfo=timezone.utc)),
        method="linear"
    ).ecef # Interpolate to halfway between second and third point, return ECEF array
array([[2.5, 4.9, 0.7]])
>>> vel = path.interp_vel(
        Time.from_datetime(datetime(2025, 1, 1, 12, 0, 1, 500000, tzinfo=timezone.utc)),
    )
>>> vel.magnitude  # Interpolated velocity magnitude in m/s
array([2.48997992])
>>> vel.unit  # Interpolated unit velocity vector
array([[0.40160966, 0.88354126, 0.2409658 ]])
convert_to(backend)[source]

Convert the Path object to a different backend.

Return type:

Path

Parameters:

backend (str | Any | None)

downsample(dt_max)[source]

Downsample the Path to a maximum time step of dt_max seconds. Note: This function is not JIT-compatible due to data validation checks.

Return type:

Path

Parameters:

dt_max (float)

interp(time, method='linear', check_bounds=True)[source]

Interpolate the Path to the given times using the specified method.

Parameters:
  • time (Time) – Times to interpolate to.

  • method (str, optional) – Interpolation method. Currently only ‘linear’ is supported. Defaults to ‘linear’.

  • check_bounds (bool, optional) – Whether to check if the interpolation times are within the path time bounds. Defaults to True.

Returns:

Interpolated Point object at the given times.

Return type:

Point

interp_vel(time, method='linear', check_bounds=True)[source]

Interpolate the Path velocity to the given times using the specified method. Currently only ‘linear’ interpolation is supported.

Return type:

Velocity

Parameters:
  • time (Time)

  • method (str)

  • check_bounds (bool)

time_at_alt(alt)[source]

Find the times when the Path crosses the given altitude (in metres). Uses linear interpolation between points to find the crossing times. Note: This function is not JIT-compatible due to data validation checks.

Return type:

Time

Parameters:

alt (float)

truncate(start_time=None, end_time=None)[source]

Truncate the Path to the given start and end times. If start_time or end_time is None, the Path is truncated to the start or end of the Path respectively.

Note: This function is not JIT-compatible due to data validation checks.

Return type:

Path

Parameters:
  • start_time (Time | None)

  • end_time (Time | None)

property end_time: Time

Get the end time of the Path.

property is_singular: bool

Check if the Path object represents a single point. Always False for Path objects.

property points: Point

Get the list of Point objects that make up the Path.

property start_time: Time

Get the start time of the Path.

property vel: Velocity

Get the Velocity object associated with the Path.

class Point(ecef, time=TimeInvariant object, backend=None)[source]

Bases: Location[TimeLike]

Point(s) in ECEF coordinates, stored as (x, y, z) in metres. Can represent a single point or multiple points, and can be associated with a Time object for time instances of the points.

Parameters:
  • ecef (Array) – ECEF coordinates as (x, y, z) in metres. Shape (3,) or (1,3) for single points, (n, 3) for multiple points.

  • time (TimeLike, optional) – Time object associated with the points. If provided, the length of time must match the number of points. Defaults to TIME_INVARIANT for static points.

  • backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.

Examples

Single static point:

>>> from astrix.primitives import Point
>>> p1 = Point(
...     [-5047162.4, 2568329.79, -2924521.17]
... )  # ECEF coordinates of Brisbane in metres
>>> p1.geodet  # Convert to geodetic coordinates (lat, lon, alt)
array([[-27.47, 153.03, 0.0]])
>>> p2 = Point.from_geodet([-27.47, 153.03, 0])  # lat, lon in degrees, alt in metres
>>> p2.ecef  # Convert back to ECEF coordinates
array([[-5047162.4, 2568329.79, -2924521.17]])

Multiple static points:

>>> pts = Point(
...     [
...         [-5047162.4, 2568329.79, -2924521.17],  # Brisbane
...         [-2694045.0, -4293642.0, 3857878.0],  # San Francisco
...         [3877000.0, 350000.0, 5027000.0],  # Somewhere else
...     ]
... )
>>> pt_bris = pts[0]  # First point (Brisbane)
>>> assert len(pts) == 3

Dynamic point with time:

>>> from datetime import datetime, timezone
>>> from astrix.primitives import Time
>>> times = Time.from_datetime(
...     [
...         datetime(2021, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
...         datetime(2021, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
...         datetime(2021, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
...     ]
... )
>>> pts_time = Point(
...     [
...         [-5047162.4, 2568329.79, -2924521.17],  # Brisbane
...         [-2694045.0, -4293642.0, 3857878.0],  # San Francisco
...         [3877000.0, 350000.0, 5027000.0],  # Somewhere else
...     ],
...     time=times,
... )
>>> pts.has_time
True
>>> pts.is_singular
False
>>> pts_new = pts + Point(
...     [[-1000, -1000, -1000]],
...     time=Time.from_datetime(
...         datetime(2021, 1, 1, 15, 0, 0, tzinfo=timezone.utc)
...     ),
... )
>>> assert len(pts_new) == 4

Notes

  • When associating a Time object, the length of the Time must match the number of points.

  • Use Path objects for interpolating between multiple points over time.

classmethod from_geodet(geodet, time=TimeInvariant object, backend=None)[source]

Create a Point object from geodetic coordinates (lat, lon, alt). Lat and lon are in degrees, alt is in meters.

Return type:

Point

Parameters:
  • geodet (Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | str | _NestedSequence[complex | bytes | str])

  • time (TimeLike)

  • backend (str | Any | None)

classmethod from_list(points)[source]

Create a Point object from a list of Point objects.

Return type:

Point

Parameters:

points (list[Point])

convert_to(backend)[source]

Convert the Point object to a different backend.

Return type:

Point

Parameters:

backend (str | Any | None)

property has_time: bool

Check if the Point has associated Time.

property is_singular: bool

Check if the Point object represents a single point.

class Ray(dir_rel, origin_rel=array([[0., 0., 0.]]), frame=Frame(name=ECEF, static_rot=True, static_loc=True, has_ref=False, time_bounds=(TimeInvariant object, TimeInvariant object), backend=numpy), time=TimeInvariant object, check=True, backend=None)[source]

Bases: object

A ray defined by an origin point, direction vector, reference frame, and optional time.

Parameters:
  • dir_rel (Array) – Nx3 array of ray direction vectors in local frame. Need not be normalised. E.g., (1, 0, 0) is a ray pointing along axis 1 of reference frame.

  • origin_rel (Array) – 1x3 or Nx3 array defining the ray origin(s) in local frame (meters). Typically (0,0,0) for camera reference frames, or ECEF coordinates for ECEF frame rays.

  • frame (Frame, optional) – Reference frame for the ray origin and direction.

  • time (Time, optional) – Time object associated with the rays. Must be same length as origin if provided. Defaults to TIME_INVARIANT.

  • backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.

  • check (bool)

Notes

  • For calculating metrics (e.g. az/el), the axis are assumed (1) forward, (2) right, (3) down (FRD frame).

  • Although stored in local coordiantes, rays are globally defined by their reference frame.

  • Monotonically increasing time is required for interpolation. But to prevent data-dependent control flow,

    this is not checked on initialization. Use Time.is_increasing to check if needed.

Examples

TBC

classmethod from_az_el(az_el, frame=Frame(name=ECEF, static_rot=True, static_loc=True, has_ref=False, time_bounds=(TimeInvariant object, TimeInvariant object), backend=numpy), time=TimeInvariant object, origin_rel=array([[0., 0., 0.]]), check=True, backend=None)[source]

Create a Ray object from origin points and heading/elevation angles.

Parameters:
  • az_el (Array) – Nx2 array of azimuth and elevation angles in degrees, relative to the reference frame.

  • frame (Frame, optional) – Reference frame for the ray origin and direction. Defaults to ECEF frame.

  • time (Time, optional) – Time object associated with the rays.

  • origin_rel (Array, optional) – Nx3 array of ray origin points in local frame coordinates. Defaults to (0,0,0), which is the reference frame origin.

  • check (bool, optional) – Whether to check input arrays for validity (not JIT compatible). Defaults to True.

  • backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.

Return type:

Ray

classmethod from_camera(pixel, camera, frame, backend=None)[source]

Create a Ray object from pixel coordinates and a camera model.

Parameters:
  • pixel (Pixel) – Pixel object defining the pixel coordinates and optional time.

  • camera (CameraLike) – Camera model defining the camera parameters and orientation.

  • frame (Frame) – Reference frame for the ray origin and direction.

  • backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.

Returns:

Ray object defined by the pixel coordinates and camera model.

Return type:

Ray

classmethod from_points(endpoint, origin, time=TimeInvariant object, check=True, backend=None)[source]

Create a Ray object from origin and endpoint arrays in ECEF frame.

Parameters:
  • origin (Point) – Origin points (ECEF coordinates). Must be length N or 1.

  • endpoint (Point) – End points (ECEF coordinates). Must be length N.

  • time (Time, optional) – Time object associated with the rays. Must be length N or 1. Defaults to TIME_INVARIANT (no time dependency).

  • backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.

  • check (bool)

Returns:

Ray object defined by the origin and direction from origin to endpoint.

Return type:

Ray

Notes

  • Origin and endpoint Point o

classmethod from_target_frame(target, frame, check_bounds=True, backend=None)[source]

Create a Ray object from a reference frame and target point(s).

Parameters:
  • target (Point) – Target point(s) in ECEF coordinates. Must be length N or 1.

  • frame (Frame) – Reference frame for the ray origin and direction.

  • backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.

  • check_bounds (bool)

Returns:

Ray object defined by the frame origin and direction to the target point(s).

Return type:

Ray

convert_to(backend)[source]

Convert the Ray object to a different backend.

Return type:

Ray

Parameters:

backend (str | Any | None)

interp(time, check_bounds=True)[source]

Interpolate the Ray origin and direction to the given times.

Parameters:
  • time (Time) – Times to interpolate to.

  • check_bounds (bool, optional) – Whether to check if the interpolation times are within the ray time bounds. Defaults to True.

Returns:

Interpolated Ray object at the given times.

Return type:

Ray

project_to_cam(camera)[source]

Project the Ray object to pixel coordinates using a camera model.

Parameters:

camera (CameraLike) – Camera model defining the camera parameters and orientation.

Returns:

Pixel object defining the pixel coordinates and associated time.

Return type:

Pixel

Notes

  • The Ray must be defined in the same reference frame as the camera.

  • Rays that do not intersect the image plane will result in NaN pixel coordinates.

replace_frame(frame)[source]

Replace the reference frame of the Ray without changing origin or direction. Not a transformation, but direct replacement. Use with caution.

Parameters:

frame (Frame) – New reference frame for the ray.

Returns:

Ray object with the new reference frame.

Return type:

Ray

to_ecef()[source]

Convert the Ray object to ECEF coordinates.

Return type:

Ray

to_frame(frame)[source]

Convert the Ray object to a different reference frame.

Parameters:

frame (Frame) – Reference frame to convert the ray to.

Returns:

Ray object defined in the new reference frame.

Return type:

Ray

to_ned()[source]

Convert the Ray object to a local NED frame at the ray origin.

Return type:

Ray

property az_el

Return the heading (from north) and elevation (from horizontal) angles in degrees.

property backend: str

Get the name of the array backend in use (e.g., ‘numpy’, ‘jax’).

property frame: Frame

Get the reference Frame of the ray.

property has_time: bool

Check if the Ray has associated Time.

property origin_points: Point

Get the ray origin point(s) as ECEF. Note: this involves a frame transformation. For repeated access, recommend converting the Ray to ECEF frame first using to_ecef().

property origin_rel: Any

Get the ray origin point(s) in the local frame coordinates. Typically zero for camera reference frames, or ECEF coordinates for ECEF frame rays.

property time: TimeLike

Get the associated Time object, if any.

property total_angle: Any

Return the total angle from the forward axis in degrees.

property unit_ecef: Any

Get the unit direction vector(s) of the ray in ECEF frame. Note: this involves a frame transformation. For repeated access, recommend converting the Ray to ECEF frame first using to_ecef().

property unit_rel: Any

Get the unit direction vector(s) of the ray in the local frame coordinates.

class RotationSequence(rot, time, backend=None)[source]

Bases: RotationLike

A sequence of time-tagged rotations, enabling interpolation between them. Uses scipy.spatial.transform.Slerp for interpolation.

Parameters:
  • rot (Rotation | list of Rotation) – A scipy Rotation object containing multiple rotations, or a list of such objects. If a list is provided, all elements must be scipy Rotation objects.

  • time (Time) – A Time object with time instances corresponding to each rotation. Must be the same length as the number of rotations and strictly increasing.

  • backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.

Examples

>>> from astrix.primitives import Time, RotationSequence
>>> from scipy.spatial.transform import Rotation
>>> from datetime import datetime, timezone
>>> times = Time.from_datetime(
...     [
...         datetime(2021, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
...         datetime(2021, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
...         datetime(2021, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
...     ]
... )
>>> rots = Rotation.from_euler(
...     "xyz",
...     [
...         [0, 0, 0],
...         [90, 0, 0],
...         [180, 0, 0],
...     ],
...     degrees=True,
... )
>>> rot_seq = RotationSequence(rots, times)
>>> interp_rot = rot_seq.interp(
...     Time.from_datetime(datetime(2021, 1, 1, 12, 30, 0, tzinfo=timezone.utc))
... )  # Interpolate to halfway between first and second rotation
>>> interp_rot.as_euler(
...     "xyz", degrees=True
... )  # Get interpolated rotation as Euler angles
array([[45.,  0.,  0.]])
convert_to(backend)[source]

Convert the RotationSequence object to a different backend.

Return type:

RotationSequence

Parameters:

backend (str | Any | None)

downsample(dt_max)[source]

Downsample the rotation sequence to a coarser time resolution.

Parameters:

dt_max (float) – Desired maximum time step in seconds for downsampling.

Returns:

A new RotationSequence object with downsampled rotations.

Return type:

RotationSequence

interp(time, check_bounds=True)[source]

Interpolate the rotation sequence at the given times to return Rotation(s).

Return type:

Rotation

Parameters:
  • time (Time)

  • check_bounds (bool)

property rots: Rotation

Get the underlying scipy Rotation object containing all rotations.

property time: Time

Get the Time object associated with the rotation sequence.

Submodules