Skip to content

Vectorized Transformations

Frame transformation functions accept a batch of vectors in a single call. Passing a two-dimensional array such as (n, 6) to state_eci_to_ecef transforms every row and returns an array of the same shape; the loop, the Earth-orientation lookups, and the thread scheduling all happen inside the Rust core. A batch that shares one epoch runs hundreds of times faster than a Python loop over the scalar function because the IAU 2006/2000A rotation matrices are computed once and applied to every vector.

Batch Input

A vectorized function is the same function as the scalar one. It decides how to evaluate based on the shape of its inputs:

Input Behavior
1-D vector, single Epoch Scalar transformation, unchanged output shape (3,) or (6,)
Array with the components along axis (default -1), single Epoch Every vector is transformed with one shared epoch; the output keeps the input layout
1-D vector, sequence of n epochs The vector is transformed at each epoch; the output has shape (n, k) (or (k, n) with axis=0)
Array of n vectors, sequence of n epochs Vector i is transformed at epoch i; the output keeps the input layout

axis names the array dimension that holds the vector components, following the numpy convention used by functions such as np.linalg.norm. The default -1 matches the common (n, 6) layout where each row is a state; axis=0 selects a (6, n) layout where each column is a state. Any number of leading batch dimensions is accepted, so a (2, 3, 6) array is transformed element by element and returned as (2, 3, 6).

Epoch arguments accept an Epoch or any sequence of Epoch objects (list, tuple, or object array). The epoch count must be 1 or equal to the number of vectors; other combinations raise ValueError.

Rotation-matrix functions such as rotation_eci_to_ecef accept a sequence of epochs and return an (n, 3, 3) array.

The same rules apply to every frame family: ECI/ECEF and GCRF/ITRF, EME2000, the lunar (LCI, LFPA, LFME), Mars (MCI, MCMF), and Earth-Moon barycenter (EMBI) frames, the EMR/SER/GSE synodic frames, rotation_icrf_to_body_fixed_iau, and the generic rotation_frame_to_frame, position_frame_to_frame, and state_frame_to_frame router. Functions that can fail for a single input (synodic and router transforms, IAU rotations) raise the same RuntimeError for a batch.

import numpy as np

import brahe as bh

bh.initialize_eop()

epc = bh.Epoch(2024, 1, 1, 12, 0, 0.0, time_system=bh.UTC)

# Build a batch of ECI states: one row per satellite, columns [x, y, z, vx, vy, vz]
raan = np.linspace(0.0, 360.0, 6, endpoint=False)
states_eci = np.array(
    [
        bh.state_koe_to_eci(
            np.array([bh.R_EARTH + 500e3, 0.001, 97.8, r, 0.0, 0.0]),
            bh.AngleFormat.DEGREES,
        )
        for r in raan
    ]
)
print(f"ECI states shape: {states_eci.shape}")

# One epoch, many states: the rotation matrices are computed once
states_ecef = bh.state_eci_to_ecef(epc, states_eci)
print(f"ECEF states shape: {states_ecef.shape}")
print(
    f"First ECEF state: {np.array2string(states_ecef[0], precision=3, suppress_small=True, max_line_width=120)}"
)

# The same call with the components along the first axis
states_ecef_t = bh.state_eci_to_ecef(epc, states_eci.T, axis=0)
print(f"Transposed layout shape: {states_ecef_t.shape}")

# Many epochs, one position: a ground station tracked through inertial space
epochs = [epc + 600.0 * i for i in range(6)]
station_ecef = bh.position_geodetic_to_ecef(
    np.array([-122.4, 37.8, 0.0]), bh.AngleFormat.DEGREES
)
station_eci = bh.position_ecef_to_eci(epochs, station_ecef)
print(f"Station ECI positions shape: {station_eci.shape}")
for e, r in zip(epochs, station_eci):
    print(f"  {e}: [{r[0]:.1f}, {r[1]:.1f}, {r[2]:.1f}] m")

# Many epochs, many states: one epoch per row
states_ecef_series = bh.state_eci_to_ecef(epochs, states_eci)
print(f"Per-epoch ECEF states shape: {states_ecef_series.shape}")

# A sequence of epochs also vectorizes the rotation matrices
rotations = bh.rotation_eci_to_ecef(epochs)
print(f"Rotation matrices shape: {rotations.shape}")
use brahe as bh;
use nalgebra as na;

fn main() {
    bh::initialize_eop().unwrap();

    let epc = bh::Epoch::from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, bh::TimeSystem::UTC);

    // Build a batch of ECI states, one per satellite
    let states_eci: Vec<na::SVector<f64, 6>> = (0..6)
        .map(|i| {
            let raan = 60.0 * i as f64;
            let oe = na::SVector::<f64, 6>::new(bh::R_EARTH + 500e3, 0.001, 97.8, raan, 0.0, 0.0);
            bh::state_koe_to_eci(oe, bh::AngleFormat::Degrees)
        })
        .collect();
    println!("ECI states: {}", states_eci.len());

    // One epoch, many states: the rotation matrices are computed once
    let states_ecef = bh::states_eci_to_ecef(&[epc], &states_eci).unwrap();
    println!("ECEF states: {}", states_ecef.len());
    let s = states_ecef[0];
    println!(
        "First ECEF state: [{:.3}, {:.3}, {:.3}, {:.3}, {:.3}, {:.3}]",
        s[0], s[1], s[2], s[3], s[4], s[5]
    );

    // Many epochs, one position: a ground station tracked through inertial space
    let epochs: Vec<bh::Epoch> = (0..6).map(|i| epc + 600.0 * i as f64).collect();
    let station_ecef = bh::position_geodetic_to_ecef(
        na::Vector3::new(-122.4, 37.8, 0.0),
        bh::AngleFormat::Degrees,
    )
    .unwrap();
    let station_eci = bh::positions_ecef_to_eci(&epochs, &[station_ecef]).unwrap();
    println!("Station ECI positions: {}", station_eci.len());
    for (e, r) in epochs.iter().zip(&station_eci) {
        println!("  {}: [{:.1}, {:.1}, {:.1}] m", e, r[0], r[1], r[2]);
    }

    // Many epochs, many states: one epoch per state
    let states_ecef_series = bh::states_eci_to_ecef(&epochs, &states_eci).unwrap();
    println!("Per-epoch ECEF states: {}", states_ecef_series.len());

    // A sequence of epochs also vectorizes the rotation matrices
    let rotations = bh::rotations_eci_to_ecef(&epochs);
    println!("Rotation matrices: {}", rotations.len());
}
Output
ECI states shape: (6, 6)
ECEF states shape: (6, 6)
First ECEF state: [1233073.576 6759694.049   15958.268    1507.424    -292.801    7549.666]
Transposed layout shape: (6, 6)
Station ECI positions shape: (6, 3)
  2024-01-01 12:00:00.000 UTC: [-4667539.5, 1895484.8, 3898713.9] m
  2024-01-01 12:10:00.000 UTC: [-4745964.3, 1689123.3, 3898902.7] m
  2024-01-01 12:20:00.000 UTC: [-4815288.1, 1479529.1, 3899070.6] m
  2024-01-01 12:30:00.000 UTC: [-4875378.2, 1267103.4, 3899217.1] m
  2024-01-01 12:40:00.000 UTC: [-4926119.5, 1052252.7, 3899342.0] m
  2024-01-01 12:50:00.000 UTC: [-4967415.1, 835388.2, 3899445.0] m
Per-epoch ECEF states shape: (6, 6)
Rotation matrices shape: (6, 3, 3)
ECI states: 6
ECEF states: 6
First ECEF state: [1233073.576, 6759694.049, 15958.268, 1507.424, -292.801, 7549.666]
Station ECI positions: 6
  2024-01-01 12:00:00.000 UTC: [-4667539.5, 1895484.8, 3898713.9] m
  2024-01-01 12:10:00.000 UTC: [-4745964.3, 1689123.3, 3898902.7] m
  2024-01-01 12:20:00.000 UTC: [-4815288.1, 1479529.1, 3899070.6] m
  2024-01-01 12:30:00.000 UTC: [-4875378.2, 1267103.4, 3899217.1] m
  2024-01-01 12:40:00.000 UTC: [-4926119.5, 1052252.7, 3899342.0] m
  2024-01-01 12:50:00.000 UTC: [-4967415.1, 835388.2, 3899445.0] m
Per-epoch ECEF states: 6
Rotation matrices: 6