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
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'
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
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.
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 = unclassifiedOBJECT_NAME = MUSKETBALL
OBJECT_ID = 1971-067D
CENTER_NAME = EARTH
TIME_SYSTEM = UTCMAN_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 AEROSPACEOBJECT_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 blockUSER_DEFINED_NORAD_CAT_ID
block appended to the opm metadata blockMy 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.