Upgrading from Orekit 13.X to Orekit 14.0

Version 14.0 of Orekit introduced some incompatible API changes with respect to versions 13.x. These changes are summarized in the following table. The next paragraphs give hints about how users should change application source code to adapt to this new version.

change related issues
custom GNSS time systems issue 1775
revamping of rinex files issue 1689, issue 1776
fast number formatting for scientific notation issue 1780
created clocks package issue 1802
add abstract class for detectors based on BodyShape issue 1244
move (Field)OrbitBlender to propagation package issue 1806
create cr3bp subpackage inside orbit issue 1807
use String in API for GNSS observation types issue 1810
create covariance subpackage inside propagation issue 1789
remove field parameter of FieldDSSTPropagator and FieldNumericalPropagator issue 1809
refactored (Field)AbsolutePVCoordinates and deleted (Field)ShiftingPVCoordinatesProvider issue 1830
introduced dedicated classes for signal travel time issue 1835
replaced ConstantPVCoordinatesProvider issue 1840
refactored counting handlers issue 1842
updated Lambert solver issue 1781
revamped GNSSPropagatorBuilder issue 1850
revamped all PropagatorBuilder hierarchy issue 1867
introduced EventFunction issue 1879
Make IntervalEventTrigger and StartStopEventsTrigger non abstract issue 1884
Refactoring indirect control switches issue 1885
Fxed typo in EOPHistory getNonRotatingOriginNutationCorrection method name issue 1910
Reduce code duplication and renaming for estimators issue 1911
Abstract away observer requirements in measurement classes issue 1745
Renaming about clocks to avoid ambiguity issue 1922
Make measurement noise a modifier instead of argument for builder issue 1945
Removed default implementation of setupMatricesComputation issue 1953
Introduction of EarthBasedStation issue 1966
Added getBaseInitialState issue 1974
Use of Optional for optional fields in CCSDS issue 1103
Added width check to FastLongFormatter issue 1975
Optional participants pos-vel in measurements issue 1973
Removed IOException from DataFilter.filter issue 1973
Use EOPFittedModel instead of EOPFitter to build PredictedEOPHistory issue 1973
Creation of ClockModel to generalize QuadraticClockModel issue 1924
Added controlled loading of EOP data issue 1992

Custom GNSS Time Systems

overview of the change

In the 13.X series, TimeSystem was an enumerate, and therefore included only a limited set of values, preventing it to be used for custom GNSS systems that are not yet officially recognized by IGS. TimeSystem has therefore been changed to an interface to allow for custom implementations, and a new PredefinedTimeSystem enumerate has been set up containing the official implementations.

how to adapt existing source code

If users need to initialize a value from the predefined lists (say GALILEO time system), then they should use PredefinedTimeSystem.GALILEO enumerate constant. If they just need to pass around a time system that has already been created (say parsed from some file or created from scratch by a simulator), then they should rather use the TimeSystem type, as it is more general and can be used for both predefined and custom time systems.

Revamping of Rinex Files

overview of the change

In the 13.X series, RinexClock was a flat structure that was inconsistent with RinexObservation and RinexNavigation and had no associated writer. The leap seconds setting in all Rinex files was also inconsistent (in fact, it is inconsistent in the standard themselves as LEAP SECONDS in clock file does not have the same meaning in observation and navigation files…).

Support for version 4.02 of Rinex observation and navigation files was added in 13.X series, but only partially. A few messages could not be parsed (NavIC ionosphere messages type L1NV, subtypes KLO and NEQN, GLONASS ionosphere messages LXOC). There was no support for writing Rinex navigation files.

Inconsistencies in clock files and support for missing navigation messages have been resolved by revamping the classes hierarchy, RinexClockWriter RinexNavigationWriter and many support container classes have been created. Note that the GLONASS L1OC and L3OC CDMA navigation messages are still not supported (they are silently ignored when parsing navigation files).

The change mainly implies several getters that were directly in RinexClock or in some navigation messages have been moved around. A new RinexClockHeader has been added, and the clock comments are now regular instances of RinexComment that can be accessed individually instead of being a single concatenated string. The getters and setters for leap seconds now explicitly state whereas they correspond to the separation between UTC and TAI or to the separation between UTC and GNSS (really GPS). A new IonosphereAij intermediate container has been added to IonosphereNequickGMessage (and reused in the new IonosphereNavICNeQuickNMessage). In anticipation of future support for GLONASS L1OC and L3OC CDMA navigation messages, the GLONASSNavigationMessage has been renamed GLONASSFdmaNavigationMessage.

The change also implied that the navigation messages in the org.propagation.analytical.gnss.data packages now require a message type at construction time (LNAV, CNAV, C2NV…).

how to adapt existing source code

Users should access the data from the header by first retrieving the header using rinexClock.getHeader(), and then access the relevant data (using for example rinexClockHeader.getReceivers()). The value returned by rinexClock.getComments() is now a list of RinexComment, each instance containing the line number and the text. The {get|set}LeapSeconds and {get|set}NumberOfLeapSeconds methods have been replaced by {get|set}LeapSecondsTAI and {get|set}LeapSecondsGNSS. Access to the aᵢⱼ coefficients from IonosphereNequickGMessage now uses an intermediate getAij method to retrieve the intermediate container. References to the GLONASSNavigationMessage name should be changed to the new name GLONASSFdmaNavigationMessage.

If navigation messages were built from scratch (instead of being parsed from Rinex navigation files), then the message type should be added to the constructor call.

Fast Number Formatting for Scientific Notation

overview of the change

In the 13.X series, the FastDoubleFormatter was added, alongside FastLongFormatter, for speeding up writing of large files. The double formatter could only support decimal format, but not scientific format, which is used for example in Rinex clock and Rinex navigation files. This limitation has been lifted in 14.0 by adding a new FastScientificFormatter.

This change implied changing the existing FastDoubleFormatter to an abstract class which cannot be instanciated anymore, and introducing a FastDecimalFormatter class. Both FastDecimalFormatter and FastScientificFormatter extend the FastDoubleFormatter abstract class and can be instanciated.

how to adapt existing source code

Users who built FastDecimalFormatter instances should now build FastDecimalFormatter instances. Users who only used already built instances can keep their variables declared as FastDoubleFormatter instances.

Created Clocks Package

overview of the change

In the 13.X series, the most classes related to clock models were in the org.orekit.time package. Two classes related to quadratic models were in the org.orekit.estimation.measurements package.

As more and more models are added, these became cumbersome, so a dedicated package org.orekit.time.clock has been created. The existing classes were moved there and new models added in 14.0 were created there too.

how to adapt existing source code

Users of clock models and clock offsets just need to update their import statements to get the class from their new package.

Add abstract class for detectors based on BodyShape

overview of the change

There was some code duplication and inconsistencies in naming between the following classes: (Field)AltitudeDetector, (Field)LongitudeCrossingDetector, (Field)LongitudeRangeCrossingDetector, (Field)LongitudeExtremumDetector, (Field)LatitudeCrossingDetector, (Field)LatitudeRangeCrossingDetector, (Field)LatitudeExtremumDetector.

An abstract class has been introduced for the standard detectors and the Field ones.

how to adapt existing source code

The consequence is that the getter is now called getBodyShape and returns a BodyShape. Please change your calls consequently.

Move (Field)OrbitBlender to propagation package

overview of the change

OrbitBlender and (Field)OrbitBlender depend on classes inside propagation. They have been moved to this package.

how to adapt existing source code

Change the imports by replacing “orbit” with “propagation”.

Create cr3bp subpackage inside orbit

overview of the change

The package orbit had classes related to the third-body problem with dependencies to the propagation package. For a cleaner architecture, they have been moved to a new subpackage, called cr3bp. This mirrors what is already done inside propagation.

how to adapt existing source code

Change the imports by replacing “orbit” with “orbit.cr3bp” for the following classes: LibrationOrbit, LibrationOrbitFamily, LibrationOrbitType, LyapunovOrbit, RichardsonExpansion, CR3BPDifferentialCorrection, HaloOrbit

Use String in API for GNSS Observation Types

overview of the change

Since 13.0, ObservationType was an interface and PredefinedObservationType is an enumerate that provided official Rinex types. This architecture allowed users to implement their own CustomObservationType for upcoming missions, using a custom typeBuilder when parsing Rinex files. Such missions would typically reuse existing names like C1C or L1C but with different supported SatelliteSystem not yet included in Rinex standards.

There were several places in Orekit (dual frequency smoother, single frequency smoother, Sinex bias, Rinex observation header,…) where ObservationType was used as a key in a Map or as an element in a List. This prevented mixing predefined types with custom types, and this prevented searching for elements in the maps since one cannot override the equals method in enumerate to for example just compare the name of the observable. A more generic way would be to use only the name (C1C, L1C…) in these containers, and build the real ObservationType (either predefined ones or custome ones) by applying the typeBuilder only much later in the data flow.

how to adapt existing source code

In many constructors, methods and containers, types have been changed from ObservationType to String. Users who called these constructors or methods with an ObservationType type could just use type.getName() instead to use the name of the observation type. Users who retrieved ObservationType for containers (like for example RinexObservationHeader.getNbObsPerSat() or DifferentialSignalBias.getAvailableObservationPairs()) should adjust the type of the container. Note that the ObservationData that is retrieved when parsing the data part of a Rinex observation file (i.e. not the header) are still full-blown ObservationType, that were built at parse time by applying the typeBuilder to the String name (this is typically how custom observation types are parsed in custom missions). The SinexBiasParser does not need a typeBuilder anymore to be built, this parameter can just be removed from users calls.

Create covariance subpackage inside propagation

overview of the change

Covariance-related code has been growing recently, but still seated at the root of propagation. It now has its own subpackage.

how to adapt existing source code

Change the imports by replacing “propagation” with “propagation.covariance” for LinearKeplerianCovarianceMapper, LinearKeplerianCovarianceHandler, AbstractStateCovarianceInterpolator, FieldStateCovariance and all the StateCovariance[...] classes.

Remove field parameter of FieldDSSTPropagator and FieldNumericalPropagator

overview of the change

The field parameter of FieldDSSTPropagator and FieldNumericalPropagator has been removed. This parameter was useless since the field is accessible from the provided integrator.

how to adapt existing source code

Simply remove the first argument that was used with the constructors.

Refactored AbsolutePVCoordinates and deleted ShiftingPVCoordinatesProvider

overview of the change

The classes AbsolutePVCoordinates and ShifingPVCoordinatesProvider (as well as their Field equivalent) were pretty much duplicates. The former inherited from PVCoordinates (polymorphism) and the latter used it as attribute (class composition). Only (Field)AbsolutePVCoordinates has been kept, but switching to composition. This allowed to remove a lot of code duplication with (Field)Orbit. After all, AbsolutePVCoordinates was originally introduced as an alternative coordinates system not presupposing to be in a two-body problem.

how to adapt existing source code

It is not possible to cast AbsolutePVCoordinates as PVCoordinates anymore, you need to call the getter first. Most methods have been preserved. For the ones that have not (like toDerivativeStructureVector), call getPVCoordinates first.

Introduced dedicated classes for signal travel time

overview of the change

The methods for signal time travel were all static in AbstractMeasurement. They have been moved to dedicated classes in its own high level package signal.

how to adapt existing source code

Instead of calling signalTimeOfFlightAdjustableEmitter, instantiate AdjustableEmitterSignalTimer and call compute. The same applies for the adjustable emitter.

Replace ConstantPVCoordinatesProvider

overview of the change

The class ConstantPVCoordinatesProvider has been removed as it had no Field in it. Now, GeodeticExtendedPositionProvider covers both needs for Earth-fixed frames. For inertial ones, ConstantPositionProvider can be used.

how to adapt existing source code

If your frame is Earth-fixed, you can switch to GeodeticExtendedPositionProvider that will also work with Field. If it is inertial, you can call ConstantPositionProvider.

Refactored counting handlers

overview of the change

The class CountAndContinue is now standalone, with immutable Action. Also, its count cannot be reset. For more flexible options, CountingHandler is not abstract anymore and can be used readily, counting all by default and returning a custom Action.

how to adapt existing source code

If you want to reset the count whilst always returning CONTINUE, you can use (Field)CountingHandler. Otherwise, you can stick to (Field)CountAndContinue.

Updated Lambert solver

overview of the change

LambertSolver has been updated to use a more recent algorithm based on Dario Izzo's solver and Gim Der's work. The solver can now find all possible solutions for a given problem set-up up the maximum number of revolutions. Solutions are now returned as objects of class LambertSolution, with the following fields:

  • nRev: the number of complete revolutions completed for this solution. Accessible via getNRev()
  • pathType: the type of path followed for this solution (low path, high path or minimum energy path; possible values are defined in enumerate LambertPathType). Accessible via getPathType()
  • orbitType: the type of orbit geometry for this solution (elliptic, parabolic or hyperbolic; possible values are defined in enumerate LambertOrbitType). Accessible via getOrbitType()
  • boundaryConditions: an object of class LambertBoundaryConditions that contains the boundary conditions for the problem setup used to compute this solution. Accessible via getBoundaryConditions()
  • boundaryVelocities: an object of class LambertBoundaryVelocities that contains the initial and final velocities corresponding to this solution. Accessible via getBoundaryVelocities() Note that method solve from a LambertSolver still receives as input posigrade, a flag indicating if the transfer should be posigrade or retrograde. Additionally, the method can be called with or without specifying nRev, the number of complete revolutions that should be completed during the transfer:
  • If not specified, the solver will find all possible solutions from 0 complete revolutions up to the maximum number of revolutions. This results in a total of 2 * (nRevMax + 1) possible solutions (1 for 0 complete revolutions, and 2 for each revolution number; two such sets of solutions, of posigrade and retrograde transfers, can be found)
  • If specified, the solver will find only the solutions for the given number of complete revolutions, as long as this is below the maximum possible number of revolutions. This results in either 1 or 2 possible solutions (1 for 0 complete revolutions, and 2 for each revolution number; one for posigrade and one for retrograde transfers) For consistency, regardless of how the method is called, the returned solutions will always be a List<LambertSolution>. It shoud also be noted that now LambertSolver uses internally a Householder solver, and therefore the maximum number of iterations, atol and rtol can be chosen when instantiating a LambertSolver. Alternatively, a constructor receiving only mu is available that uses default values for these (2000 iterations, 1.0e-5 for atol and 1.0e-7 for rtol). This also ensures backward compatibility with existing code.

how to adapt existing source code

Existing calls to method solve are still accepted since they use the same signature as the current call providing a specific value for nRev. However, since now method solve always returns a List<LambertSolution>, you will need to update the code to take this into account. The previous Lambert solver always returned the low path solution. In the current implementation, for multi-revolution transfers, the low-path solution will always be returned as the first element of the list. Therefore, to adapt existing code, you can retrieve the first element of the returned list.

Revamped GNSSPropagatorBuilder

overview of the change

As of 13.X, GNSSPropagatorBuilder was a standalone class, not related at all with PropagatorBuilder and with a different API. This prevented using such builders in orbit determination, as for example BatchLSModel requires an array of PropagatorBuilder instances. The class also assumed some defaults for inertial and body fixed frames that were confusing (for example an ITRF frame without tidal effect, which is not compatible with geodetic precision needs).

The GNSSPropagatorBuilder has therefore been completely revamped. It is now a specialization of AbstractAnalyticalPropagatorBuilder and therefore implements the PropagatorBuilder API. All of its original constructors and methods have been changed to match the interface API and superclass constructors, the only thing that remains is its name and two private fields. There are no default frames at all.

The FieldGnssPropagatorBuilder on the other hand was mimicked on GNSSPropagatorBuilder but there are no equivalent PropagatorBuilder. The class was added with 13.0 for consistency reasons, but these reasons are not relevant anymore and the class is not really useful. It was therefore completely removed.

A side effect of the builders changes is that the {Field}XxxAlmanac.getPropagator() methods for all supported satellites (Beidou, NavIC, GPS, QZSS, Galileo, civilian and legacy messages) now also requires providing the inertial and body fixed frames.

how to adapt existing source code

Users that used GNSSPropagatorBuilder must change all their uses. The single constructor requires the inertial and body fixed frames to be set up at construction time, and these frames are now immutable, so the corresponding setters have been removed. The setters for mass and attitude provider as well as the propagator building method have all been renamed to match the PropagatorBuilder API.

Users that used FieldGnssPropagatorBuilder should directly build the FieldGnssPropagator by themselves, there are no builder for that anymore.

Users that called {Field}XxxAlmanac.getPropagator() must now pass the inertial and body fixed frames when creating the propagator.

Revamped all PropagatorBuilder Hierarchy

overview of the change

As of 13.X, PropagatorBuilder implementations based on AbstractPropagatorBuilder used a template orbit to set up some metadata that will be used later on each time its build method is called for building the initial state for a new propagator. This AbstractPropagatorBuilder constructor extracted the date, frame, mu, orbit type and from the orbit type it extracted the parameter drivers. It also provided getters for these elements, as required by the PropagatorBuilder interface.

This did not work for GNSSPropagatorBuilder and GNSSPropagator. GNSSPropagator uses specific parameters which look a lot like KeplerianOrbit but are really not KeplerianOrbit as they are defined with respect to a rotating Earth frame (the ascending node is a longitude, not a right ascension). There were no OrbitType for these, and the orbital parameter drivers were specific. Using a first throw-away GNSSPropagator in the GNSSPropagatorBuilder constructor to get the template orbit from the initial orbit generated by GNSSPropagator didn't work in practice, because GNSSPropagator did output CartesianOrbit (which are orbits, as required by AbstractPropagatorBuilder). CartesianOrbit has nothing to do with the not-really-keplerian orbital parameters input (GNSSPropagator input and output were different types…), and there are non-Keplerian elements appended to the 6 Keplerian ones. Similar issues happened wit TLE which are also specific, with also different input and output types, and also include one non-Keplerian element (the B-star parameter).

So a new marker interface OrbitalParameters has been added, on top of the existing Orbit, TLE, and GNSSOrbitalElements classes. A new OrbitalParameterFactory interface has been added with a complete hierarchy of implementations (4 implementations for regular orbits, one implementation for TLE and 14 implementations for the various GNSS almanac and navigation messages). This factory interface references the ParameterDriversList associated with both the Keplerian orbital parameters and the non-Keplerian ones, and provides a createFromDrivers method to build a new set of orbital parameters from the current values held by the drivers. The parameter drivers can be automatically reset from an orbit thanks to a reset(orbit) method. Note that the argument of the reset method is always an Orbit, even when the orbital parameters are not really orbits, like TLE or GNSS orbital elements. This allows to feed the factory from regular propagators or from classical ephemeris files. The factory also provides the additional elements the AbstractPropagatorBuilder initially used, like date, frame, mu and position angle type. The {Field}GNSSOrbitalElements built by the factories now embed an intermediate {Field}KeplerianOrbit that uses a dedicated frame built by freezing the Earth frame to the inertial frame at epoch. This trick allows the right ascension of the ascending node (which corresponds to the {Field}KeplerianOrbit API) to be numerically equal to the longitude of the ascending node as used in GNSS world. In order to be able to build this intermediate frame (and change it if time is changed), the factory is built with both the inertial frame and the Earth frame. These additional frames are also required for building parsers like SEM parsers, YUMA parsers, and Ntrip clients.

All propagator builders are now built using an OrbitalParameterFactory implementation, which replaces both the template orbit and the associated frame, mu and position angle type that were used previously. As a convenience, a new factory method had been added in the Orbit and GNSSOrbitalElements classes, so a factory can easily be built from a template, hence simplifying transition from the previous API.

The class hierarchy containing {Field}GNSSOrbitalElements has been redesigned and its depth has been considerably reduced. The GNSSOrbitalElementsDriversProvider class that was formerly the abstract class at the top level of the hierarchy has been renamed GNSSOrbitalElementsFactory and now implements the OrbitalParameterFactory interface instead of the ParameterDriversProvider interface. It is not the superclass of {Field}GNSSOrbitalElements anymore. The GNSSOrbitalElements now contain a KeplerianOrbit, using a special inertial frame which is an Earth frame frozen at some time. The intermediate CommonGnssData and AbstractAlmanac classes have been removed and their content merged into GNSSOrbitalElements. Their subclasses are now immutable, as changing the parameters is done by creating a new set of orbital elements using GNSSOrbitalElementsFactory.

The getOrbitType, getPositionAngleType, getInitialOrbitDate, getFrame, getMu, and getOrbitalParametersDrivers getters in the PropagatorBuilder interface have been replaced by a single getOrbitalParameterFactory method, and this factory provides the desired elements. Note that getOrbitType returns null for TLE as there are no real orbit type there. It returns OrbitType.KEPLERIAN for GNSS as the propagator uses a hacked KeplerianOrbit internally.

A side effect of using KeplerianOrbit for GNSS is that the state transition and Jacobian matrices generated by GnssHarvester are now based on Keplerian elements and not Cartesian elements anymore.

The existing TleGenerationAlgorithm has been changed from an interface to an abstract class that implements OrbitalParameterFactory<TLE> and contains both the template TLE and the driver for the B-star non-Keplerian model parameter. There are still two implementations, one based on a fixed point method and one based on the least squares method. The static stateToTLE methods in {Field}TLE nows take TleGenerationAlgorithm instead of a template TLE.

As the B-star driver has been moved to the factory, the TLE class by itself does not provide anymore a getParametersDrivers method to retrieve the B-star parameter (B-star is now immutable at the TLE class level). The field version of the various selectExtrapolator methods also don't take anymore an additional parameters array to hold the B-star as it is fixed within the TLE itself.

Classical propagator builders that previously required a template Orbit now require an OrbitalParameterFactory that generates objects belonging to the Orbit hierarchy. The DSSTPropagatorBuilder is special, though, as it requires explicitly an EquinoctialOrbitFactory. GNSS propagators require an OrbitalParameterFactory that generates objects belonging to the GNSSOrbitalElements hierarchy. Beware that since GNSS orbital elements refer to an Earth frame frozen at a specific date to become an inertial frame, the factory date must have been initialized properly (by calling factory.setWeekAndTime(week, time)). Setting the date allows the frozen frame to be set properly, otherwise a null pointer exception will be thrown when the propagator builder attempts to use the frame.

As the TleGenerationAlgorithm factory already holds all other data (template TLE, angle type, position scale), the TLEPropagatorBuilder now only needs one TleGenerationAlgorithm to be built.

As a side effect, RTCM messages constructors (except for GLONASS) now have a factory argument, alongside satellite ID and accuracy indicator. This should not affect many users as the messages are normally built on the fly by Orekit itself. Users usually retrieve the already built message by registering message observers to a NtripClient and wait for notifications when the messages are already built. They should not see the underlying factory that was used. The message getters that take a TimeScales arguments (in order to avoid DefaultDataContext warnings) has been removed, as the no argument getter do generate this warning anymore (because the TimeScales is already known to the factory). The getters for some message data (for example code on L2 channel) are now directly available in the navigation message returned, so they are not in the RTCM messages anymore.

Another side effect of the change is that AbstractAnalyticalGradientConverter has been moved upward in the package hierarchy.

how to adapt existing source code

As the builders now require an OrbitalParameterFactory instance and as it is possible to create a factory from a template orbit by calling its factory, constructors calls of the form:

PropagatorBuilder builder = new SomeBuilder(templateOrbit, positionAngleType, positionScale);

should be replaced by

PropagatorBuilder builder = new SomeBuilder(templateOrbit.factory(positionAngleType, positionScale));

For GNSS propagators the constructor calls of the form:

GNSSPropagatorBuilder builder = new GNSSPropagatorBuilder(orbitalElements, inertial, bodyFixed);

should be replaced by

GNSSPropagatorBuilder<GalileoNavigationMessage> builder = orbitalElements.builder(inertial, bodyFixed);

For TLE propagators the constructor calls of the form:

TLEPropagatorBuilder builder = new TLEPropagatorBuilder(templateTLE,
                                                        positionAngleType,
                                                        positionScale,
                                                        new FixedPointTleGenerationAlgorithm());

should be replaced by

TLEPropagatorBuilder builder = new TLEPropagatorBuilder(new FixedPointTleGenerationAlgorithm(templateTLE,
                                                                                             positionAngleType,
                                                                                             positionScale));

As the getters have been moved from the PropagatorBuilder interface to the OrbitalParameterFactory interface, calls of the form:

AbsoluteDate date = builder.getInitialOrbitDate();

should be replaced by

AbsoluteDate date = builder.getOrbitalParameterFactory().getDate();

Accessing the initial orbit is done by replacing

Orbit orbit = builder.createInitialOrbit();

should be replaced by (assuming the builder is already known to generate Orbit elements and not TLE or GNSSOrbitalElements)

Orbit orbit = builder.getOrbitalParameterFactory().createFromDrivers();

As the {Field}GNSSOrbitalElements embed an intermediate {Field}KeplerianOrbit, calls of the form (take semi major axis as an example):

double a = element.getSma();

should be replaced by

double a = element.getOrbit().getA();

Use of the state transition matrices and Jacobian matrices generated by GNSS propagators should also be adapted as they are now based on Keplerian elements (they were based on Cartesian elements in previous versions).

As explained before, the TLE.B_STAR constant has been moved, and should now be referenced as TleGenerationAlgorithm.B_STAR. The similar constants for GNSS elements (SEMI_MAJOR_AXIS, ECCENTRICITY, TIME, INCLINATION_RATE, INCLINATION_COSINE, AF0, AF1…) that were initially defined either in the GNSSOrbitalElements class or in its former GNSSOrbitalElementsDriversProvider abstract superclass or in the CommonGnssData class are now defined in either the GNSSOrbitalElementsFactory or the NonKeplerianDriversFactory concrete classes.

As the getters with a TimeScales argument in the various Rtcm####Data classes have been removed, calls of the form (taking QZSS legacy message as an example):

QZSSLegacyNavigationMessage message = rtcm1044Data.getQzssNavigationMessage(timeScales);

should be replaced by

QZSSLegacyNavigationMessage message = rtcm1044Data.getQzssNavigationMessage();

As the getters for some parts of navigation messages are now available in the messages itself rather than in the RTCM message, calls of the form:

int code = rtcm19Data.getGpsCodeOnL2();

should be replaced by

int code = rtcm19Data.getGpsNavigationMessage().getL2Codes();

If users extended the AbstractAnalyticalGradientConverter class in custom classes, they should update the import has the class has been moved upward in the package hierarchy.

Introduced EventFunction

overview of the change

A functional interface called EventFunction has been introduced to define the so-called g function whose roots represent the event occurrences. This new interface requires implementing the value method both for double and Field, the latter having a default version based on the former. The rationale is twofold: first separate when possible the event function evaluation logic from the detection itself, and second to make the connection between standard detectors and their Field counterpart easier. The second point in particular opens the door for many practical benefits.

how to adapt existing source code

The only API breaking change is that the method dependsOnTimeOnly has been move from (Field)EventDetector to EventFunction.

Make IntervalEventTrigger and StartStopEventsTrigger non abstract

overview of the change

The classes IntervalEventTrigger and StartStopEventsTrigger are not abstract anymore. The reason is that with the introduction of EventFunction, it is much easier to define FieldEventDetector from EventDetector. Thus implementations of the Fielded versions of the triggers are possible and not abstract anymore. For specific uses, this default conversion might not be enough, in which case one should still override the method(s).

how to adapt existing source code

The main change is that constructors of IntervalEventTrigger and StartStopEventsTrigger are now public and no longer protected. The other change is with the signature of convertIntervalDetector, convertStartDetector and convertStopDetector. They use to return an extension of FieldEventDetector, but this was a leftover of a previous change to generalize the output to FieldEventDetector instead of FieldAbstractDetector. They do not need to be typed for a functional reason and thus now simply return FieldEventDetector.

Refactoring of indirect control switches

overview of the change

Following the introduction of EventFunction, the way switches in indirect control are managed has been changed. Dedicated detectors are not necessary anymore.

how to adapt existing source code

The classes ControlSwitchDetector and FieldControlSwitchDetector have been replaced respectively by ControlSwitchFunction and FieldSwitchFunction, the former being abstract and the latter an inner class.

Fixed typo in EOPHistory method name

overview of the change

A typo has been fixed in EOPHistory class for the method getNonRotatingOriginNutationCorrection.

how to adapt existing source code

The method EOPHistory#getNonRotatinOriginNutationCorrection have been replaced by EOPHistory#getNonRotatingOriginNutationCorrection to fix the typo.

Reduce code duplication and renaming for estimators

overview of the change

The abstract classes for least squares and Kalman filters now inherit from the same interface ParameterEstimator. This allows to decrease code duplication on how the parameter drivers are managed. Naming disparities have also been fixed.

how to adapt existing source code

The method getPropagatorParametersDrivers in BatchLSEstimator is now called getPropagationParametersDrivers like for Kalman filters.

Abstract away receiver requirements in measurement classes

overview of the change

As of v13.X, all measurement classes that required data from an observing object (a GPS satellite, a ground observation station, etc.) could only accept either a GroundStation or the PVCoordinatesProvider for a GPS satellite depending on the type of measurement being used. It was not directly possible for example, to create a OneWayGNSSRange measurement using a signal sent from a ground site, nor to create a Range measurement using a space-based observer. In order to rectify this issue, v14.0 has created a space-based observer class ObserverSatellite, and rewritten the inheritance structure such that both the new class ObserverSatellite and the already-existing class GroundStation both inherit from a new interface named Observer. All measurement classes in estimation/measurements that formerly accepted a GroundStation value or a PVCoordinatesProvider value for the GPS satellite trajectory in the OneWayGNSS measurement classes now instead accept an Observer that can be either a GroundStation or an ObserverSatellite. Additionally, in order to accomodate this change, the ‘OneWayGNSS’ measurement builder classes which formerly accepted two ObservableSatellite values now accept one ObservableSatellite and one Observer, which again can be either a GroundStation or an ObserverSatellite. Finally, in order to accomodate this change, the AbstractIonosphericModel class and the IonosphericModel interface, along with all the classes that implement these parent objects, have been rewritten to automatically calculate the ionospheric delay between two objects without needing the user to distinguish whether the link between them is a space-ground link or a space-space link.

how to adapt existing source code

For measurement classes that previously accepted GroundStation observers, no change is required. For the OneWayGNSSRange, OneWayGNSSPhase, and OneWayGNSSRangeRate measurements, instead of entering the PVCoordinatesProvider and the QuadraticClockModel directly, it is now necessary to use these values to create an ObserverSatellite and then enter the ObserverSatellite as the first input value when creating one of these measurements (note: use of the QuadraticClockModel is optional when creating the ObserverSatellite and really only required if you plan to introduce observer clock bias). The same is true for the creation of the OneWayGNSS measurement builder classes, except in this case the Observer object will replace what was the second ObservableSatellite instance in these constructors.

For the creation of any child class of IonosphericModel, it is now necessary to enter a OneAxisEllipsoid earth object as the first value of the constructor function. (Note: the TroposphericDelay classes will not work for space-space transmissions, and if a space-based observer is entered into these classes, the code will throw an error.)

Renaming about clocks to avoid ambiguity

overview of the change

Clock handling used the term offset for two different purposes, and this created some ambiguities. In some places, it referred to a complete time-dependent model, typically with a bias, a rate and an acceleration. In some other places, it referred only to the constant bias part. In order to avoid these ambiguities, the term offset nor refers to the full model, and the constant part is now named bias.

The QuadraticClockModel.getClockOffsetDriver() method has been renamed QuadraticClockModel.getClockBiasDriver(). Corresponding methods were also added to the new MeasurementParticipant interface and its implementation classes. The suffix of the drivers have also been adapted, for example names of the form sat-0-clock-offset are now sat-0-clock-bias. The {Field}ClockOffset.getOffset() getter has been renamed {Field}ClockOffset.getBias().

how to adapt existing source code

Users calling the methods QuadraticClockModel.getClockOffsetDriver() or {Field}ClockOffset.getOffset() methods, or the driver names should use the new names.

Make measurement noise a modifier instead of argument for builder

overview of the change

The class AbstractMeasurementBuilder used the noise source as an argument at construction. However, there is no reason to treat it differently than other measurement modifiers like biases, so a dedicated EstimationModifier was created to handle it, named MeasurementNoise.

how to adapt existing source code

If you were passing null as a noise source, simply remove the first argument when constructing your builder. If instead you were passing a noise source, you now need to call addModifier on the instance, wrapped in a MeasurementNoise.

Removed default implementation of setupMatricesComputation

overview of the change

Since version 11.1, the setupMatricesComputation method was declared at th Propagator interface level with a default implementation that throws an UnsupportedOperationException. The rationale was that only numerical propagator did implement the method when it was moved to interface level. At that time, it was already foreseen the default implementation shoul be removed once all propagators do implement the method.

Now, all Orekit-provided propagators do implement the method, so the default implementation from the interface can be safely removed.

how to adapt existing source code

There is no need to adapt anything for Orekit-provided propagators as they already implement everything. The only adaptation needed is for custom propagators that inherited the default implementation. They should now implement the method by themselves. This can be done by adding the following code snippet in the custom propagator code:

public MatricesHarvester setupMatricesComputation(final String stmName, final RealMatrix initialStm,
                                                  final DoubleArrayDictionary initialJacobianColumns) {
    throw new UnsupportedOperationException();
}

Made GroundStation work with arbitrary body and created EarthBasedStation

overview of the change

The class GroundStation had become overspecialized with Earth-bound sensors. It has been made more generic by removing the polar and meridian offset and drifts, as well as the dependence on EOPs. For the latter, EarthBasedStation has been introduced, inheriting from GroundStation.

how to adapt existing source code

If you were estimating polar/merdian offset/drift, simply call EarthBasedStation instead of GroundStation. The latter can be still used if you are not using these parameter drivers.

Added getBaseInitialState

overview of the change

Up to 13.X, the Propagator and FieldPropagator interfaces both had a getInitialState method to retrieve the initial state. This state was set up either from implementations constructors or using the resetInitialState method. This could include some unmanaged data in addition to orbit, attitude and mass.

It was possible to add managed data to the state generated by the propagate method, either using addAdditionalDataProvider (available to all propagators) or addAdditionalDerivativesProvider for integrated propagators. These additional states are not returned by getInitialState, hence getInitialState and propagate(getInitialState().getDate()) returned different values.

In order to improve consistency, the getInitialState now also includes the additional state data. The former behavior (i.e. returning state without additional data) can be recovered by using the new getBaseInitialState() method that has been added to the Propagator and FieldPropagator interfaces.

how to adapt existing source code

Use code will most probably not need any modification. If the side effect of having the additional data evaluated (as the additional data providers are automatically called when getInitialState is called) is costly and not useful, then users can call getBaseInitialState() instead for a lower overhead.

Use of Optional for optional fields in CCSDS

overview of the change

Getters for not-mandatory fields in CCSDS data classes return java.util.Optional<T> instead of using null (for object types) or Double.NaN (for numeric types) as sentinel values.

Mandatory fields continue to return their values directly (non-optional). Callers should use Optional.isEmpty() to check if a field was present in the parsed file, or Optional.get() / Optional.orElse(default) to access the value.

Setters still accept the raw type (including null), maintaining backward compatibility for programmatic construction of messages.

Creation of ClockModel to generalize QuadraticClockModel

The QuadraticClockModel class was the clock model class used throughout Orekit to store and model clocks. This has been replaced with a ClockModel interface and a PolynomialClockModel class. Classes that used to hold QuadraticClockModels now by default create PolynomialClockModel classes. However, those clocks can be set to other clock models such as ClockSum, PerfectClockModel, or user-defined clock models.

The methods for getting and setting clock bias, rate, and acceleration values have changed as well. The values returned from .getClockBiasDriver will depend on the clock instance that is used. However, getting the value for how far the clock is off of the nominal time is provided through .getOffset, which will take in a given time and return the ClockOffset class.

how to adapt existing source code

The QuadraticClockModel was replaced with PolynomialClockModel

// The clock is created within the satellite and each value set separately
ObserverSatellite remote = new ObserverSatellite("GNSS-remote", new KeplerianPropagator(o2));
remote.getClockBiasDriver().setReferenceDate(AbsoluteDate.ARBITRARY_EPOCH);
remote.getClockBiasDriver().setValue(1.0e-16);
remote.getClockDriftDriver().setReferenceDate(AbsoluteDate.ARBITRARY_EPOCH);
remote.getClockDriftDriver().setValue(0);
remote.getClockAccelerationDriver().setReferenceDate(AbsoluteDate.ARBITRARY_EPOCH);
remote.getClockAccelerationDriver().setValue(0);

was replaced by:

// The clock is created explicitly and passed into the constructor of the satellite class
PolynomialClockModel remoteClockModel = new PolynomialClockModel(AbsoluteDate.ARBITRARY_EPOCH, 1e-16);
ObserverSatellite remote = new ObserverSatellite("GNSS-remote", new KeplerianPropagator(o2), remoteClockModel);

Previous code checked got clock drift drivers directly from stations:

station.getClockDriftDriver().setValue(groundClockDrift);

was replaced by:

station.getClockModel().getRateDriver().setValue(groundClockDrift);

The .getOffset method to get a FieldClockOffset object was renamed to .getFieldOffset

final FieldClockOffset<T> co = clockModel.getOffset(t0F.shiftedBy(dtF));

was replaced by:

final FieldClockOffset<T> co = clockModel.getFieldOffset(t0F.shiftedBy(dtF));

Added width check to FastLongFormatter

overview of the change

The FastLongFormatter is a much faster implementation of String.format with fixed number of digits for long values. It is intended to be used when very large files with fixed format are generated, like Rinex or SP3 files. A few formatters are set up first with a configured width and flag selecting whether padding on the left should use zero digit or blank characters. These formatters are then reused throughout file generation, with much lower overhead than String.format.

In order to be compatible with String.format, the width is only a minimum width (hence the zero or blank padding); the formatter is allowed to exceed this width if needed. If for example a formatter is set up with 3 characters width and the long value is -12345, the six characters string -12345 is produced without error. Exceeding the prescribed width may generate errors with some file formats.

In order to prevent these errors the formatter now has an additional flag set up at construction to either allow or forbid exceeding the prescribed width. When this flag is set, this means the width is not a minimum width to be blank or zero padded, but is rather a mandatory width, so the string produced would always have exactly the prescribed width. An exception is thrown when this width is exceeded.

how to adapt existing source code

Users that built FastLongFormatter should now set up the forbidWidthOverstep constructor argument. If they want to preserve compatibility with previous versions, this parameter should be set to false, which means width can be exceeded. If they want to ensure width is never exceeded, this parameter should be set to true, which implies an OrekitException will be thrown if width would be exceeded when attempting to format a too large value.

Optional participants pos-vel in measurements

overview of the change

Before this change, measurement models computed and stored the full kinematic states of all the participants, in the same order as signal travel path. However, this can be the bottleneck, especially for massive measurement generation. A new flag dependsOnParticipantsStates has been added to modifiers, with the default implementation returning false. In a derivative-free theoretical measurement (class EstimatedMeasurementBase), the method getParticipants will return an empty array if all the modifiers defined in its originator AbstractMeasurement have a negative flag. Note that the class EstimatedMeasurement still holds this information.

how to adapt existing source code

For user's defined modifiers, overwrite dependsOnParticipantsStates if the implementation (to return true) requires a call to getParticipants.

Removed IOException from DataFilter filter

overview of the change

The filtering feature in Orekit allows to stack several DataFilter instances between a raw Datasource that may for example be a file on disk and a filtered DataSource that will be used to feed some parser. The library provides several predefined filters that allows to easily load gzip or Unix-compressed files, but also Hatanaka-compressed Rinex files or fix non-compliant SP3 files produced by SGF.

The interface was basic as it declared a single one-argument method:

public interface DataFilter {
    DataSource filter(DataSource original) throws IOException;
}

When a stack of DataFilter is needed, one uses FiltersManager to apply them in row in the appropriate order (which may be different from the declaration order), and the FilterManager.applyRelevantFilters(dataSource) method is called to loop over the DataFilter.filter(dataSource) methods.

Looking at the implementations of the DataFilter interface, it appeared none of them really throwed any IOException, and it is in fact not expected custom implementations would throw such exception either.

The reason why predefined implementations do not throw any exception and user custom implementations are not expected to throw any exception is due to the delayed opening feature that is used in DataSource. Input/output exception may arise at a later stage. Once a DataSource has been filtered, one first has to get an Opener from it by calling DataSource.getOpener(); this doesn't generate any exception yet. Then the opener openStreamOnce() or openReaderOnce() is called, and this can trigger an exception, but it happens much later than the filtering stage. Of course, the exception may also happen at an even later stage, when the InputStream or the Reader is used.

This scheduling is enforced by the API, and this is the reason why the methods were called openStreamOnce() and openReaderOnce(): they can be called only once, and this lazy call is delayed until really needed. The rationale behind this design choice was that DataSource could be used to open network-based streams that cannot be rewound and re-opened.

So the exception has been removed from the interface signature, and also from FilterManager.applyRelevantFilters(original) method.

how to adapt existing source code

If users called either DataFilter.filter(original) or the FilterManager.applyRelevantFilters(original), then these methods will not throw IOException anymore, to the calling method signature may need to be adapted and IOException may need to be removed.

If users implemented the DataFilter interface by themselves and in the unlikely case they needed throw an IOException from within the filter method, then they should wrap the exception in an unchecked exception (it could be an OrekitException for example) as follows:

class MyFilter implements DataFilter {
    public DataSource filter(final DataSource original) {
        try {
            // code that could throw an IOException
        } catch (IOException ioe) {
            throw new MyUncheckedException(ioe, "cannot filter " + original.getName());
        }
    }
}

Use EOPFittedModel instead of EOPFitter to build PredictedEOPHistory

overview of the change

EOP Prediction has been introduced in Orekit since version 12.0. This introduced classes EOPfitter to perform the fitting, EOPFittedModel that holds the result of the fitting, and PredictedEOPHistory that can be used to append fitted EOP data at the end of existing EOP data.

The PredictedEOPHistory constructor required an EOPfitter instance, which was only used in a private method to build an EOPFittedModel. It is more effective for users if this constructor use an EOPFittedModel, as some users may already have done the fitting, not necessarily using the same existing rawHistory.

The last argument of PredictedEOPHistory constructor was therefore changed from EOPfitter to EOPFittedModel.

how to adapt existing source code

If users did create PredictedEOPHistory by themselves, they should perform the fitting before calling the constructor.

Code before the change:

  PredictedEOPHistory predicted =
      new PredictedEOPHistory(rawHistory, extensionDuration, fitter);

Code after the change:

  PredictedEOPHistory predicted =
      new PredictedEOPHistory(rawHistory, extensionDuration, fitter.fit(rawHistory));

Added controlled loading of EOP data

overview of the change

EOP data used to be lazily loaded using either the default or a custom data context. A finer control was needed in some cases, either to validate EOP data before use or to perform some filtering. As users now have the ability to load data by iterating over different sources, the loaded data may be scattered, unsorted, and incomplete. This is typically true with Bulletin A, where rapid data pole motion is published weekly and nutation data is published monthly, several weeks later. Before the change, as Bulletin A loader was only used as a DataLoader from with a data context, merging and sorting the incomplete EOP data was performed directly from the Bulletin A loader. Now that users have finer control and can load files individually, this sorting, deduplicating and merging step has been moved to the EOPHistory container, at construction time. The EOPHistory container can therefore be fed with scattered, unsorted, and incomplete data while still providing properly sorted history once built. It is not recommended anymore to use a SortedSet to the EOPHistory constructors, as it would hinder the internal sorting and merging process.

As part of this change for finer control, the parse method from the EopHistoryLoader.Parser interface now takes a DataSource argument instead of already opened InputStream and name.

A side effect of allowing scattered, unsorted, and incomplete EOP data to be provided to EOPHistory, the fillHistory method in the EopHistoryLoader interface changed; it now uses a Collection<EOPEntry> rather than a SortedSet<EOPEntry> argument.

how to adapt existing source code

If users implemented the EopHistoryLoader interface or its Parser sub-interface in custom classes, they must adapt their implementations according to the new methods signatures. If they called the fillHistory or parse methods by themselves, then they should adapt the call sites too.

If users called the EOPHistory container by themselves from custom loaded data, it is advised they don't use a SortedSet but rather a collection that supports duplicated entries, like List.

The EopHistoryLoader.Parser.newEopC04Parser factory method does not take an ItrfVersionProvider argument anymore, so callers should remove it. In fact, this argument was always ignored (but it was used in siblings factory methods for other file formats).