OPM writer example

Is there any Python example of maneuver writing in OPM format?

I guess the class OpmWriter (ORbit Extrapolation KIT 11.2 API) is needed.

Thank you very much,
Alfredo

Hi @acapobianchi ,

Here is what I have been using successfully - may need to modify some params to fit your needs, but I am using state vectors in EME2000 and maneuver vectors in RTN.

import datetime
import sys
import traceback
from typing import Union

import orekit
from orekit import JavaError
from java.io import File
from java.lang import StringBuilder
from java.util import ArrayList
from org.orekit.bodies import CelestialBodyFactory
from org.orekit.data import DataContext, DirectoryCrawler
from org.orekit.files.ccsds.definitions import BodyFacade, FrameFacade, CelestialBodyFrame, OrbitRelativeFrame, TimeSystem
from org.orekit.files.ccsds.ndm import WriterBuilder
from org.orekit.files.ccsds.ndm.odm import CommonMetadata, StateVector
from org.orekit.files.ccsds.ndm.odm.opm import OpmData, Maneuver
from org.orekit.files.ccsds.section import Header, Segment
from org.orekit.files.ccsds.utils.generation import KvnGenerator
from org.orekit.frames import FramesFactory
from org.orekit.time import AbsoluteDate, AGILeapSecondFilesLoader, TimeScalesFactory
from org.orekit.utils import IERSConventions

orekit_data_path = '<Path to orekit-data directory>'
opm_output_path = '<Path to output OPM file>'

orekit.initVM()

DATA_CONTEXT = DataContext.getDefault()
orekit_data_manager = DATA_CONTEXT.getDataProvidersManager()
orekit_data_manager.addProvider(DirectoryCrawler(File(orekit_data_path)))
TimeScalesFactory.addUTCTAIOffsetsLoader(AGILeapSecondFilesLoader(AGILeapSecondFilesLoader.DEFAULT_SUPPORTED_NAMES))

UTC_TIME = TimeScalesFactory.getUTC()
CCSDS_VERSION = 2.0
J2K_FRAME = FramesFactory.getEME2000()
RTN_FRAME = OrbitRelativeFrame.RTN
FRAME_FACADE = FrameFacade(J2K_FRAME, CelestialBodyFrame.EME2000, RTN_FRAME, None, '')
EARTH_BODY_FACADE = BodyFacade(CelestialBodyFactory.EARTH, CelestialBodyFactory.getEarth())


def opm_maneuver_message_writer(sat_info: dict, maneuver_dict: dict, state_vector: dict) -> Union[None, str]:
    """
    :param sat_info: {'originator': str, 'object_name': str, 'object_id': str, 'mass_kg': float}
    :param maneuver_dict: {'ignition_epoch' str, 'duration_s': float, 'delta_mass_kg': flaot, 'rtn_dv_mps': list[float]}
    :param state_vector: {'epoch': str, 'elements': list[float]} - assumed j2000
    :return:
    """
    # Header
    opm_header = Header(CCSDS_VERSION)
    opm_header.setFormatVersion(CCSDS_VERSION)
    opm_header.setCreationDate(AbsoluteDate(datetime.datetime.utcnow().isoformat(), UTC_TIME))
    opm_header.setOriginator(sat_info['originator'])

    # OPM metadata
    opm_metadata = CommonMetadata()
    opm_metadata.setObjectName(sat_info['object_name'].title())
    opm_metadata.setObjectID(sat_info['object_id'])
    opm_metadata.setCenter(EARTH_BODY_FACADE)
    opm_metadata.setReferenceFrame(FRAME_FACADE.map(J2K_FRAME))
    opm_metadata.setTimeSystem(TimeSystem.UTC)

    # Maneuver in an ArrayList
    opm_maneuver_list = ArrayList()
    opm_maneuver = Maneuver()
    opm_maneuver.setEpochIgnition(AbsoluteDate(maneuver_dict['ignition_epoch'], UTC_TIME))
    opm_maneuver.setDuration(maneuver_dict['duration_s'])
    opm_maneuver.setDeltaMass(maneuver_dict['delta_mass_kg'])
    opm_maneuver.setReferenceFrame(FRAME_FACADE.parse('RTN', IERSConventions.IERS_2010, False, DATA_CONTEXT, True, True, True))
    opm_maneuver.setDV(0, maneuver_dict['rtn_dv_mps'][0])
    opm_maneuver.setDV(1, maneuver_dict['rtn_dv_mps'][1])
    opm_maneuver.setDV(2, maneuver_dict['rtn_dv_mps'][2])
    opm_maneuver_list.add(opm_maneuver)

    # State Vector
    opm_state_vector = StateVector()
    opm_state_vector.setEpoch(AbsoluteDate(state_vector['epoch'], UTC_TIME))
    for idx in range(3):
        opm_state_vector.setP(idx, state_vector['elements'][idx])
        opm_state_vector.setV(idx, state_vector['elements'][idx + 3])

    # Validate sections
    errs = _validate_ccsds_segment(segment_list=[opm_header, opm_metadata, opm_maneuver, opm_state_vector])
    if errs:
        return errs

    # Construct OPM Data, then validate as a whole
    opm_data = OpmData(opm_state_vector, None, None, None, opm_maneuver_list, None, sat_info['mass_kg'])
    errs = _validate_ccsds_segment(segment_list=[opm_data])
    if errs:
        return errs

    # Construct the Segment
    opm_segment = Segment(opm_metadata, opm_data)

    # Writer
    opm_writer = WriterBuilder().buildOpmWriter()
    opm_output_java_string = StringBuilder()
    kvn_gen = KvnGenerator(opm_output_java_string, 1, 'opm_maneuver_message', 0)

    # Write header and segment
    opm_writer.writeHeader(kvn_gen, opm_header)
    opm_writer.writeSegmentContent(kvn_gen, CCSDS_VERSION, opm_segment)

    # Write the file
    opm_string = opm_output_java_string.toString()
    with open(opm_output_path, 'w') as fp:
        fp.write(opm_string)


def _validate_ccsds_segment(segment_list: list) -> Union[None, str]:
    block = None
    try:
        for block in segment_list:
            block.validate(CCSDS_VERSION)
    except JavaError:
        exc_info = sys.exc_info()
        traceback.print_exception(*exc_info)
        return f'Invalid {str(type(block)).split(".")[-1]} - see console traceback'
1 Like

Thanks @aerow610!
I could make it work in my Python script.

Just as a curiosity, the keywords:

USER_DEFINED_CLASSIFICATION
USER_DEFINED_NORAD_CAT_ID
USER_DEFINED_MAN_PURPOSE
USER_DEFINED_TCA
USER_DEFINED_MAN_STATUS

are not printed, while they are obligatory in an OPM, according to https://www.space-track.org/documents/Spaceflight_Safety_Handbook_for_Operators.pdf

Thank you very much.
Kind Regards,
Alfredo

Hi @acapobianchi

According to CCSDS OPM standard, those keys don’t exist in an OPM.
However, an OPM can contain USER_DEFINED_X keys, but they are not mandatory.

Best regards,
Bryan

If you need to write these user defined keys due to some ICD, then you just have to add them
to the OPM.
Form Java, it would be:

  opm.getUserDefinedBlock().addEntry("CLASSIFICATION", classification);
  opm.getUserDefinedBlock().addEntry("NORAD_CAT_ID", noradCatId);
  opm.getUserDefinedBlock().addEntry("MAN_PURPOSE", manPurpose);
  opm.getUserDefinedBlock().addEntry("TCA", tca);
  opm.getUserDefinedBlock().addEntry("MAN_STATUS", manStatus);

Note that the keys must not include the USER_DEFINED_ prefix. This is because what would appear in a kvn file as USER_DEFINED_CLASSIFICATION=U would rather appear in a XML file as

  <USED_DEFINED parameter="CLASSIFICATION">U</USER_DEFINED>

hence the USER_DEFINED_ part is managed by the Orekit generators themselves, depending on the format.

1 Like

Thanks @luc, this is exactly what I was looking for!

I’ve tried to code it and I’ve obtained a slightly different result from the desired one.
Here’s an example OPM file from space-track:

CCSDS_OPM_VERS = 2.0
CREATION_DATE = 2016-06-20T07:58:48
ORIGINATOR = A SAMPLE ORGANIZATION
USER_DEFINED_RELEASABILITY = PUBLIC
USER_DEFINED_CLASSIFICATION = unclassified

OBJECT_NAME = MUSKETBALL
OBJECT_ID = 1971-067D
CENTER_NAME = EARTH
TIME_SYSTEM = UTC

MAN_EPOCH_IGNITION = 2016-06-23T12:52:21.800
MAN_DURATION = 248.4 [s]
MAN_REF_FRAME = RTN
MAN_DV_1 = -0.00000100 [km/s]
MAN_DV_2 = 0.00039833 [km/s]
MAN_DV_3 = 0.00000000 [km/s]
USER_DEFINED_MAN_PURPOSE = COLA
USER_DEFINED_TCA = 2016-06-25T12:30:45
USER_DEFINED_MAN_STATUS = PREDICTED

While this one is what I generated:

CCSDS_OPM_VERS = 2.0
CREATION_DATE = 2022-07-06T17:59:10.35318
ORIGINATOR = EPIC AEROSPACE

OBJECT_NAME = CHIMERA_LEO_1
OBJECT_ID = 1971-067D
CENTER_NAME = Earth
REF_FRAME = EME2000
TIME_SYSTEM = UTC
EPOCH = 2022-10-25T10:49:19.811081743358134
X = -6889.14191318812
Y = -429.72222309613164
Z = 15.107132936733054
X_DOT = -0.04243320089967974
Y_DOT = 0.9784449868038718
Z_DOT = 7.536099070469933
MAN_EPOCH_IGNITION = 2022-10-25T10:49:19.811081743358134
MAN_DURATION = 2.000000345094037
MAN_DELTA_MASS = 0.6555319643101711
MAN_REF_FRAME = RTN
MAN_DV_1 = 0.0
MAN_DV_2 = 0.0
MAN_DV_3 = -0.0120263
USER_DEFINED_CLASSIFICATION = UNCLASSIFIED
USER_DEFINED_NORAD_CAT_ID = 90001
USER_DEFINED_MAN_PURPOSE = LEOP
USER_DEFINED_MAN_STATUS = PREDICTED

As you can see, the USER_DEFINED_ blocks were all appended to the maneuver block, while, in order to comply with JSpOC ICD, I wanted:

  • USER_DEFINED_CLASSIFICATION block appended to the Header block
  • USER_DEFINED_NORAD_CAT_ID block appended to the opm metadata block

My Python code is:

def opm_maneuver_message_writer(self, s, maneuver_dict: dict, opm_output_path) -> Union[None, str]:

        '''

        Writes a maneuver in OPM format, in compliance with space-track.og Spaceflight_Safety_Handbook_for_Operators.pdf

        s : [SpacecraftState]     pre-maneuver state

        maneuver_dict: {'ignition_epoch' str, 'duration_s': float, 'delta_mass_kg': float, 'rtn_dv_mps': list[float], 'purpose': str, 'status': str} - RTN assumed

        opm_output_path : [str]    ouput OPM file

        thanks to:

            https://forum.orekit.org/u/aerow610

            https://forum.orekit.org/u/luc

        deprecated input arguments

        # :param sat_info: {'originator': str, 'object_name': str, 'object_id': str, 'mass_kg': float}

        # :param state_vector: {'epoch': str, 'elements': list[float]} - assumed j2000

        '''

        # Header

        opm_header = Header(CCSDS_VERSION)

        opm_header.setFormatVersion(CCSDS_VERSION)

        opm_header.setCreationDate(AbsoluteDate(datetime.utcnow().isoformat(), UTC))

        opm_header.setOriginator('EPIC AEROSPACE')

        # OPM metadata

        opm_metadata = CommonMetadata()

        opm_metadata.setObjectName(self.satelliteName)

        opm_metadata.setObjectID(self.objId)                           # [str] from Intl. Des.

        opm_metadata.setCenter(EARTH_BODY_FACADE)

        opm_metadata.setReferenceFrame(FRAME_FACADE.map(J2K_FRAME))    # ALERT: M2K frame hardcoded

        opm_metadata.setTimeSystem(TimeSystem.UTC)

        # Maneuver in an ArrayList

        opm_maneuver_list = ArrayList()

        opm_maneuver = OPMManeuver()

        opm_maneuver.setEpochIgnition(maneuver_dict['ignition_epoch'])

        opm_maneuver.setDuration(maneuver_dict['duration_s'])

        opm_maneuver.setDeltaMass(maneuver_dict['delta_mass_kg'])

        opm_maneuver.setReferenceFrame(FRAME_FACADE.parse('RTN', IERSConventions.IERS_2010, False, DATA_CONTEXT, True, True, True)) # ALERT: RTN hardcoded

        opm_maneuver.setDV(0, maneuver_dict['rtn_dv_mps'][0])

        opm_maneuver.setDV(1, maneuver_dict['rtn_dv_mps'][1])

        opm_maneuver.setDV(2, maneuver_dict['rtn_dv_mps'][2])

        opm_maneuver_list.add(opm_maneuver)

        # State Vector

        opm_state_vector = StateVector()

        opm_state_vector.setEpoch(s.getDate())

        p = s.getPVCoordinates(J2K_FRAME).getPosition()

        v = s.getPVCoordinates(J2K_FRAME).getVelocity()

        opm_state_vector.setP(0, p.getX())

        opm_state_vector.setP(1, p.getY())

        opm_state_vector.setP(2, p.getZ())

        opm_state_vector.setV(0, v.getX())

        opm_state_vector.setV(1, v.getY())

        opm_state_vector.setV(2, v.getZ())

        # Validate sections

        errs = self._validate_ccsds_segment(segment_list=[opm_header, opm_metadata, opm_maneuver, opm_state_vector])

        if errs:

            return errs

        # Construct OPM Data, then validate as a whole

        clssification = UserDefined()

        clssification.addEntry('CLASSIFICATION', 'UNCLASSIFIED')

        opm_data = OpmData(opm_state_vector, None, None, None, opm_maneuver_list, clssification, s.getMass())

        # opm_data.getUserDefinedBlock().addEntry('CLASSIFICATION', 'UNCLASSIFIED')

        opm_data.getUserDefinedBlock().addEntry('NORAD_CAT_ID', self.satelliteNumber)

        opm_data.getUserDefinedBlock().addEntry('MAN_PURPOSE', maneuver_dict['purpose'])

        # opm_data.getUserDefinedBlock().addEntry('TCA', tca)

        opm_data.getUserDefinedBlock().addEntry('MAN_STATUS', maneuver_dict['status'])

        errs = self._validate_ccsds_segment(segment_list=[opm_data])

        if errs:

            return errs

        # Construct the Segment

        opm_segment = Segment(opm_metadata, opm_data)

        # Writer

        opm_writer = WriterBuilder().buildOpmWriter()

        opm_output_java_string = StringBuilder()

        kvn_gen = KvnGenerator(opm_output_java_string, 1, 'opm_maneuver_message', 0)

        # Write header and segment

        opm_writer.writeHeader(kvn_gen, opm_header)

        opm_writer.writeSegmentContent(kvn_gen, CCSDS_VERSION, opm_segment)

        # Write the file

        opm_string = opm_output_java_string.toString()

        with open(opm_output_path, 'w') as fp:

            fp.write(opm_string)

    def _validate_ccsds_segment(self,segment_list: list) -> Union[None, str]:

        block = None

        try:

            for block in segment_list:

                block.validate(CCSDS_VERSION)

        except JavaError:

            exc_info = sys.exc_info()

            traceback.print_exception(*exc_info)

            return f'Invalid {str(type(block)).split(".")[-1]} - see console traceback'

How can I move:

  • USER_DEFINED_CLASSIFICATION block to the Header section?
  • USER_DEFINED_NORAD_CAT_ID block to the opm metadata section?

I’ve noticed that class Header (OREKIT 12.0.1 API) has no getUserDefinedBlock() method…

Thank you very much,
Alfredo

CCSDS standard does not allow USER_DEFINED keys in header nor in metadata.
They are allowed only in OPM data, OMM data and will be allowed in OCM data when ODM V3 is published. They are even not allowed in OEM data, probably because OEM data is line-oriented with no keywords at all.

Thanks @luc for the information.
So, I understand that sample OPM https://www.space-track.org/documents/ASampleOrganization_5383_6SK.OPM is not CCSDS compliant, since it has USER_DEFINED keys in Header. Is that correct?
Thank you,
Alfredo

Yes, this is what I think.
Orekit surely cannot produce this, and I even think it will complain at it if you attempt to parse it.

The latest published standard is ODM V2.
Version 3 of the standard is upcoming but not published yet. I do have it because Orekit will be one of the two independent implementations that are required before CCSDS validates a standard (the other implementation will be STK, and we will exchange messages in the new format during the validation process). Orekit already supports the draft V3, but as the standard is not published yet, it is considered experimental and should not be used for production, V2 is currently the way to go.

Thanks @luc for the information,

it is very good to know that Orekit is one of the two independent implementations required before CCSDS validates a standard! This adds rationale to our decision to use Orekit at EPIC AEROSPACE.

So, since I generated and validated the above OPM with Orekit, I can state that it is compliant with the latest published version of CCSDS OPM standard, correct?

By the way, I’m looking at public OPMs available at space-track.org and I see that they comply with the Handbook for Operators but not with CCSDS (login required):

https://www.space-track.org/files/getFile?fileID=5751943
https://www.space-track.org/files/getFile?fileID=4206581

Do you think space-track.org will be able to process correctly my OPM files (generated by Orekit), even if the obligatory USER_DEFINED blocks are not exactly were they expect them? Is it a good idea to ask them for confirmation?

Thank you very much.
Kind Regards,
Alfredo

Thanks for the kind words, it is really appreciated.

Orekit is standard compliant when writing CCSDS files. Following classical development best practices, it is more lenient when parsing, as long as the parsed file is close enough to the standard. So what Orekit produces can probably be fed to other systems. If it fails, it would be a bug (either on Orekit side or the other system side).

I cannot be sure how space-trace will deal with the files. You should ask them about that.

1 Like