astrix.primitives module¶
- class Frame(rot, loc=None, ref_frame=None, backend=None)[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.
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:
- Parameters:
backend (str | Any | None)
- interp_loc(time=None, check_bounds=True)[source]¶
Get the interpolated location of the frame at the given times. If the location is static, time can be None.
- interp_rot(time=None, 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:
time (Time | None)
check_bounds (bool)
- 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 rel_rot: RotationLike¶
Get the last rotation of the frame relative to the reference frame.
- class Location[source]¶
Bases:
Generic
[T
],ABC
Abstract base class for location objects (Point, Path). ‘interp’ function is required for integration with other modules.
- property backend: str¶
- property ecef: Any¶
- property geodet: Any¶
- property time: T¶
- class Path(point, backend=None)[source]¶
-
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:
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:
- Parameters:
backend (str | Any | None)
- 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:
- class Point(ecef, time=TimeInvariant object, backend=None)[source]¶
-
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 >>> p.geodet # Convert to geodetic coordinates (lat, lon, alt) array([[153.03, 27.47, 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.
- convert_to(backend)[source]¶
Convert the Point object to a different backend.
- Return type:
- 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(origin, dir, time=TimeInvariant object, backend=None)[source]¶
Bases:
object
A ray defined by an origin point and a direction vector. Can represent multiple rays with associated times.
- Parameters:
origin (Point) – Point object with length N defining the ray origin(s) in ECEF coordinates (meters).
dir (Array) – Nx3 array of ray direction vectors in ECEF coordinates (meters). Direction vectors will be normalized to unit vectors.
time (Time, optional) – Time object associated with the rays. Must be same length as origin if provided. Defaults to None.
backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.
Notes
Origin Point must be same length as dimension 0 of dir array.
- Time cannot be provided by both origin Point and time argument.
If origin Point has associated Time, time argument must be None.
If origin Point does not have associated Time, time argument can be provided.
If neither origin Point nor time argument have Time, Ray will not have Time.
No check is made that times are monotonically increasing for interpolation. This is left to the user.
Examples
TBC
- classmethod from_endpoint(origin, endpoint, time=TimeInvariant object, backend=None)[source]¶
Create a Ray object from origin and endpoint arrays.
- Parameters:
origin (Point) – Nx3 array of ray origin points in ECEF coordinates (meters).
endpoint (Point) – Nx3 array of ray endpoint points in ECEF coordinates (meters).
time (Time, optional) – Time object associated with the rays. Must be same length as origin if provided. Defaults to None.
backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.
- Returns:
Ray object defined by the origin and direction from origin to endpoint.
- Return type:
- classmethod from_head_el(origin, head_el, time=None, backend=None)[source]¶
Create a Ray object from origin points and heading/elevation angles.
- Parameters:
origin (Array) – Nx3 array of ray origin points in ECEF coordinates (meters).
head_el (Array) – Nx2 array of heading (from north) and elevation (from horizontal) angles in degrees.
time (Time, optional) – Time object associated with the rays. Must be same length as origin if provided. Defaults to None.
backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.
- Returns:
Ray object defined by the origin and direction from heading/elevation angles.
- Return type:
- convert_to(backend)[source]¶
Convert the Ray object to a different backend.
- Return type:
- Parameters:
backend (str | Any | None)
- interp(time, check_bounds=True)[source]¶
Interpolate the Ray origin and direction to the given times.
- property backend: str¶
Get the name of the array backend in use (e.g., ‘numpy’, ‘jax’).
- property has_time: bool¶
Check if the Ray has associated Time.
- property head_el¶
Return the heading (from north) and elevation (from horizontal) angles in degrees.
- property unit: Any¶
Get the unit direction vector(s) of the ray.
- class RotationLike[source]¶
Bases:
ABC
Abstract base class for rotation objects (RotationSingle, RotationSequence). ‘convert_to’ function is required for integration with other modules.
- 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:
- Parameters:
backend (str | Any | None)
- class Time(secs, backend=None)[source]¶
Bases:
TimeLike
One or more time instances.
Represents time using seconds since Unix epoch (1970-01-01 00:00:00 UTC). Can handle single time instances or arrays of times with consistent backend support for JAX/NumPy compatibility.
- Parameters:
secs (Array | list of float | float) – Time values in seconds since Unix epoch (1970-01-01 UTC)
backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.
Examples
Single time instance:
>>> t = Time(1609459200.0) # 2021-01-01 00:00:00 UTC
Multiple times:
>>> times = Time([1609459200.0, 1609545600.0]) # Jan 1-2, 2021
From datetime:
>>> from datetime import datetime, timezone >>> dt = datetime(2021, 1, 1, tzinfo=timezone.utc) >>> t = Time.from_datetime(dt) >>> dt_list = [ ... datetime(2021, 1, 1, tzinfo=timezone.utc), ... datetime(2021, 1, 2, tzinfo=timezone.utc), ... ] >>> times = Time.from_datetime(dt_list)
Notes
All datetime objects must be timezone-aware to avoid ambiguity.
- classmethod from_datetime(time, backend=None)[source]¶
Create a Time object from a single or list of datetime objects. Will not accept timezone-unaware datetime obejects due to ambiguity.
- Return type:
- Parameters:
time (datetime | list[datetime])
backend (str | Any | None)
- convert_to(backend)[source]¶
Convert the Time object to a different backend.
- Return type:
- Parameters:
backend (str | Any | None)
- in_bounds(time)[source]¶
Check if the given time(s) are within the bounds of this Time object.
- Return type:
bool
- Parameters:
time (Time)
- property backend: str¶
Get the name of the array backend in use (e.g., ‘numpy’, ‘jax.numpy’).
- property datetime: list[datetime]¶
- property end_sec: float | Any¶
Get the end time in seconds since epoch.
- property is_increasing: bool¶
Check if the time values are strictly increasing.
- property secs: Any¶
Get the time values in seconds since epoch.
- property start_sec: float | Any¶
Get the start time in seconds since epoch.
- class TimeGroup(times, backend=None)[source]¶
Bases:
TimeLike
A group of TimeLike objects (Time, TimeInvariant, TimeGroup). Used to manage multiple time instances and determine overlapping time bounds.
- Parameters:
times (list of TimeLike) – List of TimeLike objects (Time, TimeInvariant, TimeGroup)
backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.
Examples
>>> t1 = Time.from_datetime( ... [ ... datetime(2021, 1, 1, 12, 0, 0, tzinfo=timezone.utc), ... datetime(2021, 1, 1, 13, 0, 0, tzinfo=timezone.utc), ... ] ... ) >>> t2 = Time.from_datetime( ... [ ... datetime(2021, 1, 1, 12, 30, 0, tzinfo=timezone.utc), ... datetime(2021, 1, 1, 14, 0, 0, tzinfo=timezone.utc), ... ] ... ) >>> tg = TimeGroup([t1, t2]) >>> tg.duration # Duration of overlap in seconds 1800.0
>>> overlap = tg.overlap_bounds # Overlapping time range >>> assert overlap[0].datetime[0] == datetime( ... 2021, 1, 1, 12, 30, 0, tzinfo=timezone.utc ... ) >>> assert overlap[1].datetime[0] == datetime( ... 2021, 1, 1, 13, 0, 0, tzinfo=timezone.utc ... )
>>> tg.in_bounds( ... Time.from_datetime(datetime(2021, 1, 1, 12, 45, 0, tzinfo=timezone.utc)) ... ) True
- convert_to(backend)[source]¶
Convert the TimeGroup object to a different backend.
- Return type:
- Parameters:
backend (str | Any | None)
- in_bounds(time)[source]¶
Check if the given time(s) are within the overlap bounds of this TimeGroup.
- Return type:
bool
- Parameters:
time (Time)
- property backend: str¶
- property duration: float | Any¶
Get the duration of the overlap bounds in seconds.
- property extreme_bounds: tuple[TimeLike, TimeLike]¶
Get the extreme bounds of the TimeGroup as Time objects.
- property is_invariant: bool¶
- class TimeInvariant[source]¶
Bases:
TimeLike
Class for static time-like objects (static Time). ‘in_bounds’ function is required for integration with other modules.
- class TimeLike[source]¶
Bases:
ABC
Abstract base class for time-like objects (Time, TimeSequence). ‘in_bounds’ function is required for integration with other modules.
- class Velocity(vec, time, _xp)[source]¶
Bases:
object
Velocity vector(s) in ECEF coordinates (vx, vy, vz) in m/s. Associated with a Time object for the time instances of the velocities. Internal use only, typically created from Path objects. No data validation is performed.
- Parameters:
vec (Array) – Velocity vectors in ECEF coordinates (vx, vy, vz) in m/s. Shape (n, 3).
time (Time) – Time object associated with the velocities. Length must match number of velocity vectors.
backend (BackendArg, optional) – Array backend to use (numpy, jax, etc.). Defaults to numpy.
_xp (Any)
Examples
Velocity objects are typically created from Path objects.
>>> 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 >>> vel = path.vel >>> vel.magnitude # Velocity magnitudes in m/s array([1.91049732, 2.29128785, 2.6925824]) >>> vel.unit # Unit velocity vectors array([[0.52342392, 0.83747828, 0.15702718], [0.43643578, 0.87287156, 0.21821789], [0.37139068, 0.89133762, 0.25997347]])
- convert_to(backend)[source]¶
Convert the Velocity object to a different backend.
- Return type:
- Parameters:
backend (str | Any | None)
- property backend: str¶
- property magnitude: Any¶
Get the velocity magnitude in m/s.
- property unit: Any¶
Get the unit velocity vector.
-
vec:
Any
¶