Skip to content

APM — Attitude Parameter Message

An Attitude Parameter Message (APM) carries a spacecraft's attitude state at a single epoch through one or more logical blocks — quaternion, Euler angle, angular velocity, spin, inertia, and maneuver. It is the attitude-message counterpart to the OPM: a compact snapshot for handing off attitude state or documenting a planned attitude maneuver. The message is defined by the CCSDS 504.0-B-2 Attitude Data Messages standard.

Parse and Access

Parse from file or string, then access header properties, metadata, and the attitude quaternion:

import brahe as bh
from brahe.ccsds import APM

bh.initialize_eop()

# Parse APM with a single attitude quaternion block
apm = APM.from_file("test_assets/ccsds/apm/APMExampleG1.txt")

# Header
print(f"Format version: {apm.format_version}")
print(f"Originator:     {apm.originator}")
print(f"Creation date:  {apm.creation_date}")
print(f"Message ID:     {apm.message_id}")

# Metadata
print(f"\nObject name:  {apm.object_name}")
print(f"Object ID:    {apm.object_id}")
print(f"Center name:  {apm.center_name}")
print(f"Time system:  {apm.time_system}")

# Epoch (shared by all blocks except maneuvers)
print(f"\nEpoch: {apm.epoch}")

# Attitude quaternion blocks
print(f"\nQuaternion blocks: {len(apm.quaternion_states)}")
for i, q in enumerate(apm.quaternion_states):
    print(f"\n  Block {i}:")
    print(f"    Ref frame A: {q.ref_frame_a}")
    print(f"    Ref frame B: {q.ref_frame_b}")
    wire = q.quaternion.to_vector(scalar_first=False)
    print(
        f"    Quaternion [Q1, Q2, Q3, QC]: [{wire[0]:.5f}, {wire[1]:.5f}, {wire[2]:.5f}, {wire[3]:.5f}]"
    )
use brahe as bh;
use brahe::ccsds::APM;

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

    // Parse APM with a single attitude quaternion block
    let apm = APM::from_file("test_assets/ccsds/apm/APMExampleG1.txt").unwrap();

    // Header
    println!("Format version: {}", apm.header.format_version);
    println!("Originator:     {}", apm.header.originator);
    println!("Creation date:  {}", apm.header.creation_date);
    println!(
        "Message ID:     {}",
        apm.header.message_id.as_deref().unwrap_or("None")
    );

    // Metadata
    println!("\nObject name:  {}", apm.metadata.object_name);
    println!("Object ID:    {}", apm.metadata.object_id);
    println!(
        "Center name:  {}",
        apm.metadata.center_name.as_deref().unwrap_or("None")
    );
    println!("Time system:  {}", apm.metadata.time_system);

    // Epoch (shared by all blocks except maneuvers)
    println!("\nEpoch: {}", apm.epoch);

    // Attitude quaternion blocks
    println!("\nQuaternion blocks: {}", apm.quaternion_states.len());
    for (i, q) in apm.quaternion_states.iter().enumerate() {
        println!("\n  Block {}:", i);
        println!("    Ref frame A: {}", q.ref_frame_a);
        println!("    Ref frame B: {}", q.ref_frame_b);
        let wire = q.quaternion.to_vector(false);
        println!(
            "    Quaternion [Q1, Q2, Q3, QC]: [{:.5}, {:.5}, {:.5}, {:.5}]",
            wire[0], wire[1], wire[2], wire[3]
        );
    }
}
Output
Format version: 2.0
Originator:     GSFC
Creation date:  2003-09-30 19:23:57.000 UTC
Message ID:     A7015Z1

Object name:  TRMM
Object ID:    1997-074A
Center name:  EARTH
Time system:  UTC

Epoch: 2003-09-30 14:28:15.117 UTC

Quaternion blocks: 1

  Block 0:
    Ref frame A: SC_BODY_1
    Ref frame B: ITRF1997
    Quaternion [Q1, Q2, Q3, QC]: [0.00005, 0.87543, 0.40949, 0.25678]
Format version: 2
Originator:     GSFC
Creation date:  2003-09-30 19:23:57.000 UTC
Message ID:     A7015Z1

Object name:  TRMM
Object ID:    1997-074A
Center name:  EARTH
Time system:  UTC

Epoch: 2003-09-30 14:28:15.117 UTC

Quaternion blocks: 1

  Block 0:
    Ref frame A: SC_BODY_1
    Ref frame B: ITRF1997
    Quaternion [Q1, Q2, Q3, QC]: [0.00005, 0.87543, 0.40949, 0.25678]

How APM Messages Are Organized

Every APM has a header (version, creation date, originator), metadata (object identity, center body, time system), and a single epoch that applies to every logical block except maneuvers. The attitude information itself lives in up to six repeatable logical blocks, each of which can appear zero or more times. A message is only valid to write or parse if at least one block is present.

Quaternion blocks (QUAT_START/QUAT_STOP) carry the attitude quaternion and an optional time derivative. Euler angle blocks (EULER_START/EULER_STOP) carry the same rotation as a three-angle sequence plus optional angle rates; the rotation sequence (e.g. ZXZ) is stored alongside the angles. Angular velocity blocks (ANGVEL_START/ANGVEL_STOP) carry an angular velocity vector along with the frame it is expressed in. Spin blocks (SPIN_START/SPIN_STOP) describe a spin-stabilized attitude by spin-axis right ascension, declination, phase angle, and spin rate, with an optional nutation description. Inertia blocks (INERTIA_START/INERTIA_STOP) carry the spacecraft's moment-of-inertia tensor. Maneuver blocks (MAN_START/MAN_STOP) describe a planned or executed attitude maneuver as a torque vector over a duration; unlike the other blocks, a maneuver carries its own epoch rather than using the message epoch.

Every quaternion, Euler angle, and angular velocity block declares a pair of reference frames, REF_FRAME_A and REF_FRAME_B, that together define the rotation direction: the block's values transform a vector from frame A to frame B. This A\(\to\)B convention is fixed by CCSDS 504.0-B-2 and does not depend on which frame is inertial or body-fixed — some fixtures put the spacecraft body frame first, others put it second, and the block's own REF_FRAME_A/REF_FRAME_B fields are the only reliable way to tell which.

CCSDS wire values use different units and component ordering than brahe's internal representation. Brahe converts at the KVN/XML/JSON parse and write boundary, so every value returned by the Python and Rust APIs is already in SI units and brahe's native quaternion convention:

Quantity Wire (CCSDS) Internal (brahe)
Angles (Euler, spin) degrees radians
Angle rates (Euler rates, spin rate, nutation rate) deg/s rad/s
Quaternion component order scalar-last: Q1 Q2 Q3 QC scalar-first: Quaternion::new(w, x, y, z)
Quaternion derivative order scalar-last: Q1_DOT Q2_DOT Q3_DOT QC_DOT, 1/s scalar-first vector, 1/s
Inertia tensor components kg\(\cdot\)m\(^2\) kg\(\cdot\)m\(^2\) (unchanged)
Maneuver torque N\(\cdot\)m N\(\cdot\)m (unchanged)

The quaternion reordering matters because most quaternion libraries, including brahe's Quaternion, use a scalar-first convention internally while CCSDS 504.0-B-2 fixes the wire order as scalar-last. Use Quaternion.to_vector(scalar_first=False) (Python) or Quaternion::to_vector(false) (Rust) to recover the wire-order [Q1, Q2, Q3, QC] components shown in a KVN file.

Creating and Writing APMs

Build an APM programmatically by defining a header, epoch, and metadata, then adding one or more logical blocks. The resulting message can be serialized to KVN, XML, or JSON:

import numpy as np

import brahe as bh
from brahe.ccsds import APM, APMAngularVelocity, APMQuaternionState

bh.initialize_eop()

# Create a new APM with header info
epoch = bh.Epoch.from_datetime(2024, 6, 15, 0, 0, 0.0, 0.0, bh.TimeSystem.UTC)
apm = APM("BRAHE_EXAMPLE", "LEO SAT", "2024-100A", "UTC", epoch, center_name="EARTH")
apm.message_id = "APM-2024-001"

# Attitude quaternion: spacecraft body frame aligned with ICRF (identity rotation)
apm.add_quaternion_state(
    APMQuaternionState("ICRF", "SC_BODY_1", bh.Quaternion(1.0, 0.0, 0.0, 0.0))
)

# Angular velocity: body spinning about its Z axis at Earth's rotation rate
apm.add_angular_velocity(
    APMAngularVelocity(
        "ICRF", "SC_BODY_1", "SC_BODY_1", np.array([0.0, 0.0, bh.OMEGA_EARTH])
    )
)

print(
    f"Created APM with {len(apm.quaternion_states)} quaternion block, "
    f"{len(apm.angular_velocities)} angular velocity block"
)

# Write to KVN string
kvn = apm.to_string("KVN")
print(f"\nKVN output ({len(kvn)} chars):")
print(kvn)

# Write to file
apm.to_file("/tmp/brahe_example_apm.txt", "KVN")
print("\nWritten to /tmp/brahe_example_apm.txt")

# Verify round-trip
apm2 = APM.from_file("/tmp/brahe_example_apm.txt")
print(
    f"Round-trip: {len(apm2.quaternion_states)} quaternion block, "
    f"{len(apm2.angular_velocities)} angular velocity block"
)
use brahe as bh;
use brahe::ccsds::{
    ADMReferenceFrame, APM, APMAngularVelocity, APMMetadata, APMQuaternionState, CCSDSFormat,
    CCSDSTimeSystem,
};
use nalgebra::Vector3;

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

    // Create a new APM with header info
    let epoch = bh::Epoch::from_datetime(2024, 6, 15, 0, 0, 0.0, 0.0, bh::TimeSystem::UTC);
    let metadata =
        APMMetadata::new("LEO SAT", "2024-100A", CCSDSTimeSystem::UTC).with_center_name("EARTH");
    let mut apm = APM::new("BRAHE_EXAMPLE", metadata, epoch);
    apm.header.message_id = Some("APM-2024-001".to_string());

    // Attitude quaternion: spacecraft body frame aligned with ICRF (identity rotation)
    apm.push_quaternion_state(APMQuaternionState::new(
        ADMReferenceFrame::parse("ICRF"),
        ADMReferenceFrame::parse("SC_BODY_1"),
        bh::Quaternion::new(1.0, 0.0, 0.0, 0.0),
    ));

    // Angular velocity: body spinning about its Z axis at Earth's rotation rate
    apm.push_angular_velocity(APMAngularVelocity::new(
        ADMReferenceFrame::parse("ICRF"),
        ADMReferenceFrame::parse("SC_BODY_1"),
        ADMReferenceFrame::parse("SC_BODY_1"),
        Vector3::new(0.0, 0.0, bh::OMEGA_EARTH),
    ));

    println!(
        "Created APM with {} quaternion block, {} angular velocity block",
        apm.quaternion_states.len(),
        apm.angular_velocities.len()
    );

    // Write to KVN string
    let kvn = apm.to_string(CCSDSFormat::KVN).unwrap();
    println!("\nKVN output ({} chars):", kvn.len());
    println!("{}", kvn);

    // Write to file
    apm.to_file("/tmp/brahe_example_apm.txt", CCSDSFormat::KVN)
        .unwrap();
    println!("\nWritten to /tmp/brahe_example_apm.txt");

    // Verify round-trip
    let apm2 = APM::from_file("/tmp/brahe_example_apm.txt").unwrap();
    println!(
        "Round-trip: {} quaternion block, {} angular velocity block",
        apm2.quaternion_states.len(),
        apm2.angular_velocities.len()
    );
}
Output
Created APM with 1 quaternion block, 1 angular velocity block

KVN output (480 chars):
CCSDS_APM_VERS = 2.0
CREATION_DATE = 2026-09-07T06:56:01.6024149459
ORIGINATOR = BRAHE_EXAMPLE
MESSAGE_ID = APM-2024-001

OBJECT_NAME = LEO SAT
OBJECT_ID = 2024-100A
CENTER_NAME = EARTH
TIME_SYSTEM = UTC
EPOCH = 2024-06-15T00:00:00.000

QUAT_START
REF_FRAME_A = ICRF
REF_FRAME_B = SC_BODY_1
Q1 = 0
Q2 = 0
Q3 = 0
QC = 1
QUAT_STOP

ANGVEL_START
REF_FRAME_A = ICRF
REF_FRAME_B = SC_BODY_1
ANGVEL_FRAME = SC_BODY_1
ANGVEL_X = 0
ANGVEL_Y = 0
ANGVEL_Z = 0.00417807421629731
ANGVEL_STOP


Written to /tmp/brahe_example_apm.txt
Round-trip: 1 quaternion block, 1 angular velocity block
Created APM with 1 quaternion block, 1 angular velocity block

KVN output (478 chars):
CCSDS_APM_VERS = 2.0
CREATION_DATE = 2026-09-07T06:52:03.18514858
ORIGINATOR = BRAHE_EXAMPLE
MESSAGE_ID = APM-2024-001

OBJECT_NAME = LEO SAT
OBJECT_ID = 2024-100A
CENTER_NAME = EARTH
TIME_SYSTEM = UTC
EPOCH = 2024-06-15T00:00:00.000

QUAT_START
REF_FRAME_A = ICRF
REF_FRAME_B = SC_BODY_1
Q1 = 0
Q2 = 0
Q3 = 0
QC = 1
QUAT_STOP

ANGVEL_START
REF_FRAME_A = ICRF
REF_FRAME_B = SC_BODY_1
ANGVEL_FRAME = SC_BODY_1
ANGVEL_X = 0
ANGVEL_Y = 0
ANGVEL_Z = 0.00417807421629731
ANGVEL_STOP


Written to /tmp/brahe_example_apm.txt
Round-trip: 1 quaternion block, 1 angular velocity block

Round-Trip Fidelity

Writing and re-parsing an APM preserves all header, metadata, and logical-block values. Numeric precision may vary slightly due to floating-point formatting, but values are preserved within the precision of the output format.

KVN Format Example

An excerpt from the CCSDS 504.0-B-2 Annex G-1 example file, containing a header, metadata, and a single quaternion block:

CCSDS_APM_VERS = 2.0
CREATION_DATE = 2003-09-30T19:23:57
ORIGINATOR   = GSFC
MESSAGE_ID = A7015Z1

OBJECT_NAME  = TRMM
OBJECT_ID    = 1997-074A
CENTER_NAME  = EARTH
TIME_SYSTEM  = UTC

EPOCH     = 2003-09-30T14:28:15.1172

QUAT_START
REF_FRAME_A  = SC_BODY_1
REF_FRAME_B  = ITRF1997

Q1        = 0.00005
Q2        = 0.87543
Q3        = 0.40949
QC        = 0.25678
QUAT_STOP

Note that this quaternion block has no bracketed unit annotations — quaternion components are dimensionless. Angle-valued blocks such as Euler angle and spin blocks carry [deg] annotations, which brahe strips during parsing.


See Also