From 2008f523c8743e42b2849b886711bde1f7e6a72a Mon Sep 17 00:00:00 2001
From: Luc Maisonobe <luc@orekit.org>
Date: Wed, 10 Sep 2014 11:23:46 +0200
Subject: [PATCH] Added a way to reuse transform interpolator from one run to
 another.

This is done by dumping its state into a file, thus avoiding costly
initialization.
---
 .../java/org/orekit/rugged/api/Rugged.java    | 269 +++++++++++++++---
 .../org/orekit/rugged/api/RuggedMessages.java |   4 +-
 .../utils/SpacecraftToObservedBody.java       |  28 +-
 .../org/orekit/rugged/RuggedMessages_en.utf8  |   6 +
 .../org/orekit/rugged/RuggedMessages_fr.utf8  |   6 +
 .../orekit/rugged/api/RuggedMessagesTest.java |   2 +-
 .../org/orekit/rugged/api/RuggedTest.java     | 210 ++++++++++++++
 src/site/xdoc/changes.xml                     |   4 +
 8 files changed, 490 insertions(+), 39 deletions(-)

diff --git a/core/src/main/java/org/orekit/rugged/api/Rugged.java b/core/src/main/java/org/orekit/rugged/api/Rugged.java
index e617532f..5ef06400 100644
--- a/core/src/main/java/org/orekit/rugged/api/Rugged.java
+++ b/core/src/main/java/org/orekit/rugged/api/Rugged.java
@@ -16,6 +16,11 @@
  */
 package org.orekit.rugged.api;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -23,6 +28,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.commons.math3.analysis.differentiation.DerivativeStructure;
+import org.apache.commons.math3.exception.util.LocalizedFormats;
 import org.apache.commons.math3.geometry.euclidean.threed.FieldVector3D;
 import org.apache.commons.math3.geometry.euclidean.threed.Rotation;
 import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
@@ -169,26 +175,12 @@ public class Rugged {
                   final List<TimeStampedAngularCoordinates> quaternions, final int aInterpolationNumber,
                   final AngularDerivativesFilter aFilter, final double tStep)
         throws RuggedException {
-
-        // space reference
-        this.ellipsoid     = extend(ellipsoid);
-
-        // orbit/attitude to body converter
-        this.scToBody = new SpacecraftToObservedBody(inertialFrame, ellipsoid.getBodyFrame(),
-                                                     minDate, maxDate, overshootTolerance,
-                                                     positionsVelocities, pvInterpolationNumber, pvFilter,
-                                                     quaternions, aInterpolationNumber, aFilter,
-                                                     tStep);
-
-        // intersection algorithm
-        this.algorithm = selectAlgorithm(algorithmID, updater, maxCachedTiles);
-
-        this.sensors = new HashMap<String, LineSensor>();
-        this.finders = new HashMap<String, SensorMeanPlaneCrossing>();
-
-        setLightTimeCorrection(true);
-        setAberrationOfLightCorrection(true);
-
+        this(updater, maxCachedTiles, algorithmID, ellipsoid,
+             createInterpolator(inertialFrame, ellipsoid.getBodyFrame(),
+                                minDate, maxDate, overshootTolerance,
+                                positionsVelocities, pvInterpolationNumber, pvFilter,
+                                quaternions, aInterpolationNumber, aFilter,
+                                tStep));
     }
 
     /** Build a configured instance.
@@ -263,10 +255,166 @@ public class Rugged {
                   final CartesianDerivativesFilter pvFilter, final AngularDerivativesFilter aFilter,
                   final double tStep, final Propagator propagator)
         throws RuggedException {
-        try {
+        this(updater, maxCachedTiles, algorithmID, ellipsoid,
+             createInterpolator(inertialFrame, ellipsoid.getBodyFrame(),
+                                minDate, maxDate, overshootTolerance,
+                                interpolationStep, interpolationNumber,
+                                pvFilter, aFilter, tStep, propagator));
+    }
 
-            // space reference
-            this.ellipsoid = extend(ellipsoid);
+    /** Build a configured instance, reusing the interpolator dumped from a previous instance.
+     * <p>
+     * By default, the instance performs both light time correction (which refers
+     * to ground point motion with respect to inertial frame) and aberration of
+     * light correction (which refers to spacecraft proper velocity). Explicit calls
+     * to {@link #setLightTimeCorrection(boolean) setLightTimeCorrection}
+     * and {@link #setAberrationOfLightCorrection(boolean) setAberrationOfLightCorrection}
+     * can be made after construction if these phenomena should not be corrected.
+     * </p>
+     * @param updater updater used to load Digital Elevation Model tiles
+     * @param maxCachedTiles maximum number of tiles stored in the cache
+     * @param algorithmID identifier of algorithm to use for Digital Elevation Model intersection
+     * @param ellipsoidID identifier of reference ellipsoid
+     * @param inertialFrameID identifier of inertial frame
+     * @param bodyRotatingFrameID identifier of body rotating frame
+     * @param dumpStream stream from where to read previous instance dumped interpolator
+     * (caller opened it and remains responsible for closing it)
+     * @exception RuggedException if dump file cannot be loaded
+     * or if frames do not match the ones referenced in the dump file
+     * @see #dumpInterpolator(OutputStream)
+     */
+    public Rugged(final TileUpdater updater, final int maxCachedTiles,
+                  final AlgorithmId algorithmID, final EllipsoidId ellipsoidID,
+                  final InertialFrameId inertialFrameID, final BodyRotatingFrameId bodyRotatingFrameID,
+                  final InputStream dumpStream)
+        throws RuggedException {
+        this(updater, maxCachedTiles, algorithmID,
+             selectEllipsoid(ellipsoidID, selectBodyRotatingFrame(bodyRotatingFrameID)),
+             selectInertialFrame(inertialFrameID), dumpStream);
+    }
+
+    /** Build a configured instance, reusing the interpolator dumped from a previous instance.
+     * <p>
+     * By default, the instance performs both light time correction (which refers
+     * to ground point motion with respect to inertial frame) and aberration of
+     * light correction (which refers to spacecraft proper velocity). Explicit calls
+     * to {@link #setLightTimeCorrection(boolean) setLightTimeCorrection}
+     * and {@link #setAberrationOfLightCorrection(boolean) setAberrationOfLightCorrection}
+     * can be made after construction if these phenomena should not be corrected.
+     * </p>
+     * @param updater updater used to load Digital Elevation Model tiles
+     * @param maxCachedTiles maximum number of tiles stored in the cache
+     * @param algorithmID identifier of algorithm to use for Digital Elevation Model intersection
+     * @param ellipsoid reference ellipsoid
+     * @param inertialFrame inertial frame
+     * @param dumpStream stream from where to read previous instance dumped interpolator
+     * (caller opened it and remains responsible for closing it)
+     * @exception RuggedException if dump file cannot be loaded
+     * or if frames do not match the ones referenced in the dump file
+     * @see #dumpInterpolator(OutputStream)
+     */
+    public Rugged(final TileUpdater updater, final int maxCachedTiles,
+                  final AlgorithmId algorithmID, final OneAxisEllipsoid ellipsoid, final Frame inertialFrame,
+                  final InputStream dumpStream)
+        throws RuggedException {
+        this(updater, maxCachedTiles, algorithmID, ellipsoid,
+             createInterpolator(inertialFrame, ellipsoid.getBodyFrame(), dumpStream));
+    }
+
+    /** Build a configured instance.
+     * <p>
+     * By default, the instance performs both light time correction (which refers
+     * to ground point motion with respect to inertial frame) and aberration of
+     * light correction (which refers to spacecraft proper velocity). Explicit calls
+     * to {@link #setLightTimeCorrection(boolean) setLightTimeCorrection}
+     * and {@link #setAberrationOfLightCorrection(boolean) setAberrationOfLightCorrection}
+     * can be made after construction if these phenomena should not be corrected.
+     * </p>
+     * @param updater updater used to load Digital Elevation Model tiles
+     * @param maxCachedTiles maximum number of tiles stored in the cache
+     * @param algorithmID identifier of algorithm to use for Digital Elevation Model intersection
+     * @param ellipsoid f reference ellipsoid
+     * @param scToBody transforms interpolator
+     */
+    private Rugged(final TileUpdater updater, final int maxCachedTiles,
+                  final AlgorithmId algorithmID, final OneAxisEllipsoid ellipsoid,
+                  final SpacecraftToObservedBody scToBody) {
+
+        // space reference
+        this.ellipsoid = extend(ellipsoid);
+
+        // orbit/attitude to body converter
+        this.scToBody = scToBody;
+
+        // intersection algorithm
+        this.algorithm = selectAlgorithm(algorithmID, updater, maxCachedTiles);
+
+        this.sensors = new HashMap<String, LineSensor>();
+        this.finders = new HashMap<String, SensorMeanPlaneCrossing>();
+
+        setLightTimeCorrection(true);
+        setAberrationOfLightCorrection(true);
+
+    }
+
+    /** Create a transform interpolator from positions and quaternions lists.
+     * @param inertialFrame inertial frame
+     * @param bodyFrame observed body frame
+     * @param minDate start of search time span
+     * @param maxDate end of search time span
+     * @param overshootTolerance tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting
+     * @param positionsVelocities satellite position and velocity
+     * @param pvInterpolationNumber number of points to use for position/velocity interpolation
+     * @param pvFilter filter for derivatives from the sample to use in position/velocity interpolation
+     * @param quaternions satellite quaternions
+     * @param aInterpolationNumber number of points to use for attitude interpolation
+     * @param aFilter filter for derivatives from the sample to use in attitude interpolation
+     * @param tStep step to use for inertial frame to body frame transforms cache computations
+     * @return transforms interpolator
+     * @exception RuggedException if data needed for some frame cannot be loaded or if position
+     * or attitude samples do not fully cover the [{@code minDate}, {@code maxDate}] search time span
+     */
+    private static SpacecraftToObservedBody createInterpolator(final Frame inertialFrame, final Frame bodyFrame,
+                                                               final AbsoluteDate minDate, final AbsoluteDate maxDate,
+                                                               final double overshootTolerance,
+                                                               final List<TimeStampedPVCoordinates> positionsVelocities,
+                                                               final int pvInterpolationNumber,
+                                                               final CartesianDerivativesFilter pvFilter,
+                                                               final List<TimeStampedAngularCoordinates> quaternions,
+                                                               final int aInterpolationNumber,
+                                                               final AngularDerivativesFilter aFilter, final double tStep)
+        throws RuggedException {
+        return new SpacecraftToObservedBody(inertialFrame, bodyFrame,
+                                            minDate, maxDate, overshootTolerance,
+                                            positionsVelocities, pvInterpolationNumber, pvFilter,
+                                            quaternions, aInterpolationNumber, aFilter,
+                                            tStep);
+    }
+
+    /** Create a transform interpolator from a propagator.
+     * @param inertialFrame inertial frame
+     * @param bodyFrame observed body frame
+     * @param minDate start of search time span
+     * @param maxDate end of search time span
+     * @param overshootTolerance tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting
+     * @param interpolationStep step to use for inertial/Earth/spacraft transforms interpolations
+     * @param interpolationNumber number of points of to use for inertial/Earth/spacraft transforms interpolations
+     * @param pvFilter filter for derivatives from the sample to use in position/velocity interpolation
+     * @param aFilter filter for derivatives from the sample to use in attitude interpolation
+     * @param tStep step to use for inertial frame to body frame transforms cache computations
+     * @param propagator global propagator
+     * @return transforms interpolator
+     * @exception RuggedException if data needed for some frame cannot be loaded
+     */
+    private static SpacecraftToObservedBody createInterpolator(final Frame inertialFrame, final Frame bodyFrame,
+                                                               final AbsoluteDate minDate, final AbsoluteDate maxDate,
+                                                               final double overshootTolerance,
+                                                               final double interpolationStep, final int interpolationNumber,
+                                                               final CartesianDerivativesFilter pvFilter,
+                                                               final AngularDerivativesFilter aFilter,
+                                                               final double tStep, final Propagator propagator)
+        throws RuggedException {
+        try {
 
             // extract position/attitude samples from propagator
             final List<TimeStampedPVCoordinates> positionsVelocities =
@@ -299,25 +447,74 @@ public class Rugged {
             propagator.propagate(minDate.shiftedBy(-interpolationStep), maxDate.shiftedBy(interpolationStep));
 
             // orbit/attitude to body converter
-            this.scToBody = new SpacecraftToObservedBody(inertialFrame, ellipsoid.getBodyFrame(),
-                                                         minDate, maxDate, overshootTolerance,
-                                                         positionsVelocities, interpolationNumber, pvFilter,
-                                                         quaternions, interpolationNumber, aFilter,
-                                                         tStep);
-
-            // intersection algorithm
-            this.algorithm = selectAlgorithm(algorithmID, updater, maxCachedTiles);
+            return createInterpolator(inertialFrame, bodyFrame,
+                                      minDate, maxDate, overshootTolerance,
+                                      positionsVelocities, interpolationNumber, pvFilter,
+                                      quaternions, interpolationNumber, aFilter,
+                                      tStep);
 
-            this.sensors = new HashMap<String, LineSensor>();
-            this.finders = new HashMap<String, SensorMeanPlaneCrossing>();
-
-            setLightTimeCorrection(true);
-            setAberrationOfLightCorrection(true);
         } catch (PropagationException pe) {
             throw new RuggedException(pe, pe.getSpecifier(), pe.getParts());
         }
     }
 
+    /** Create a transform interpolator from positions and quaternions lists.
+     * @param inertialFrame inertial frame
+     * @param bodyFrame observed body frame
+     * @param dumpStream stream from where to read previous instance dumped interpolator
+     * (caller opened it and remains responsible for closing it)
+     * @return recovered interpolator from dump
+     * @exception RuggedException if dump file cannot be loaded or if
+     * frames do not match the ones referenced in the dump file
+     */
+    private static SpacecraftToObservedBody createInterpolator(final Frame inertialFrame, final Frame bodyFrame,
+                                                               final InputStream dumpStream)
+        throws RuggedException {
+        try {
+            final ObjectInputStream ois = new ObjectInputStream(dumpStream);
+            final SpacecraftToObservedBody scToBody = (SpacecraftToObservedBody) ois.readObject();
+            if (!inertialFrame.getName().equals(scToBody.getInertialFrameName())) {
+                throw new RuggedException(RuggedMessages.FRAMES_MISMATCH_WITH_INTERPOLATOR_DUMP,
+                                          inertialFrame.getName(), scToBody.getInertialFrameName());
+            }
+            if (!bodyFrame.getName().equals(scToBody.getBodyFrameName())) {
+                throw new RuggedException(RuggedMessages.FRAMES_MISMATCH_WITH_INTERPOLATOR_DUMP,
+                                          bodyFrame.getName(), scToBody.getBodyFrameName());
+            }
+            return scToBody;
+        } catch (ClassNotFoundException cnfe) {
+            throw new RuggedException(cnfe, RuggedMessages.NOT_INTERPOLATOR_DUMP_DATA);
+        } catch (ClassCastException cce) {
+            throw new RuggedException(cce, RuggedMessages.NOT_INTERPOLATOR_DUMP_DATA);
+        } catch (IOException ioe) {
+            throw new RuggedException(ioe, RuggedMessages.NOT_INTERPOLATOR_DUMP_DATA);
+        }
+    }
+
+    /** Dump frames transform interpolator.
+     * <p>
+     * This method allows to reuse the interpolator built in one instance, to build
+     * another instance by calling {@link #Rugged(TileUpdater, int, AlgorithmId, OneAxisEllipsoid, Frame, InputStream)}
+     * or {@link #Rugged(TileUpdater, int, AlgorithmId, EllipsoidId, InertialFrameId, BodyRotatingFrameId, InputStream)}.
+     * This reduces Rugged initialization time as setting up the interpolator can be long, it is
+     * mainly intended to be used when several runs are done (for example in an image processing chain)
+     * with the same configuration.
+     * </p>
+     * @param dumpStream stream where to dump the interpolator
+     * (caller opened it and remains responsible for closing it)
+     * @exception RuggedException if interpolator cannot be written to file
+     * @see #Rugged(TileUpdater, int, AlgorithmId, OneAxisEllipsoid, Frame, File)
+     * @see #Rugged(TileUpdater, int, AlgorithmId, EllipsoidId, InertialFrameId, BodyRotatingFrameId, File)
+     */
+    public void dumpInterpolator(final OutputStream dumpStream) throws RuggedException {
+        try {
+            final ObjectOutputStream oos = new ObjectOutputStream(dumpStream);
+            oos.writeObject(scToBody);
+        } catch (IOException ioe) {
+            throw new RuggedException(ioe, LocalizedFormats.SIMPLE_MESSAGE, ioe.getMessage());
+        }
+    }
+
     /** Set flag for light time correction.
      * <p>
      * This methods set the flag for compensating or not light time between
diff --git a/core/src/main/java/org/orekit/rugged/api/RuggedMessages.java b/core/src/main/java/org/orekit/rugged/api/RuggedMessages.java
index 9e3f9409..044571dd 100644
--- a/core/src/main/java/org/orekit/rugged/api/RuggedMessages.java
+++ b/core/src/main/java/org/orekit/rugged/api/RuggedMessages.java
@@ -60,7 +60,9 @@ public enum RuggedMessages implements Localizable {
     LINE_OF_SIGHT_NEVER_CROSSES_LATITUDE("line-of-sight never crosses latitude {0}"),
     LINE_OF_SIGHT_NEVER_CROSSES_LONGITUDE("line-of-sight never crosses longitude {0}"),
     LINE_OF_SIGHT_NEVER_CROSSES_ALTITUDE("line-of-sight never crosses altitude {0}"),
-    DEM_ENTRY_POINT_IS_BEHIND_SPACECRAFT("line-of-sight enters the Digital Elevation Model behind spacecraft!");
+    DEM_ENTRY_POINT_IS_BEHIND_SPACECRAFT("line-of-sight enters the Digital Elevation Model behind spacecraft!"),
+    FRAMES_MISMATCH_WITH_INTERPOLATOR_DUMP("frame {0} does not match frame {1} from interpolator dump"),
+    NOT_INTERPOLATOR_DUMP_DATA("data is not an interpolator dump");
 
     // CHECKSTYLE: resume JavadocVariable check
 
diff --git a/core/src/main/java/org/orekit/rugged/utils/SpacecraftToObservedBody.java b/core/src/main/java/org/orekit/rugged/utils/SpacecraftToObservedBody.java
index 2d8b74d0..895623cf 100644
--- a/core/src/main/java/org/orekit/rugged/utils/SpacecraftToObservedBody.java
+++ b/core/src/main/java/org/orekit/rugged/utils/SpacecraftToObservedBody.java
@@ -16,6 +16,7 @@
  */
 package org.orekit.rugged.utils;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -36,7 +37,16 @@ import org.orekit.utils.TimeStampedPVCoordinates;
 /** Provider for observation transforms.
  * @author Luc Maisonobe
  */
-public class SpacecraftToObservedBody {
+public class SpacecraftToObservedBody implements Serializable {
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 20140909L;
+
+    /** Name of the inertial frame. */
+    private final String inertialFrameName;
+
+    /** Name of the body frame. */
+    private final String bodyFrameName;
 
     /** Start of search time span. */
     private final AbsoluteDate minDate;
@@ -87,6 +97,8 @@ public class SpacecraftToObservedBody {
         throws RuggedException {
         try {
 
+            this.inertialFrameName  = inertialFrame.getName();
+            this.bodyFrameName      = bodyFrame.getName();
             this.minDate            = minDate;
             this.maxDate            = maxDate;
             this.overshootTolerance = overshootTolerance;
@@ -170,6 +182,20 @@ public class SpacecraftToObservedBody {
         }
     }
 
+    /** Get the name of the inertial frame.
+     * @return inertial frame name
+     */
+    public String getInertialFrameName() {
+        return inertialFrameName;
+    }
+
+    /** Get the name of the body frame.
+     * @return body frame name
+     */
+    public String getBodyFrameName() {
+        return bodyFrameName;
+    }
+
     /** Get the start of search time span.
      * @return start of search time span
      */
diff --git a/core/src/main/resources/assets/org/orekit/rugged/RuggedMessages_en.utf8 b/core/src/main/resources/assets/org/orekit/rugged/RuggedMessages_en.utf8
index e7e552e8..587c7eb1 100644
--- a/core/src/main/resources/assets/org/orekit/rugged/RuggedMessages_en.utf8
+++ b/core/src/main/resources/assets/org/orekit/rugged/RuggedMessages_en.utf8
@@ -30,3 +30,9 @@ LINE_OF_SIGHT_NEVER_CROSSES_ALTITUDE = line-of-sight never crosses altitude {0}
 
 # line-of-sight enters the Digital Elevation Model behind spacecraft!
 DEM_ENTRY_POINT_IS_BEHIND_SPACECRAFT = line-of-sight enters the Digital Elevation Model behind spacecraft!
+
+# frame {0} does not match frame {1} from interpolator dump
+FRAMES_MISMATCH_WITH_INTERPOLATOR_DUMP = frame {0} does not match frame {1} from interpolator dump
+
+# data is not an interpolator dump
+NOT_INTERPOLATOR_DUMP_DATA = data is not an interpolator dump
diff --git a/core/src/main/resources/assets/org/orekit/rugged/RuggedMessages_fr.utf8 b/core/src/main/resources/assets/org/orekit/rugged/RuggedMessages_fr.utf8
index db99c921..825795b4 100644
--- a/core/src/main/resources/assets/org/orekit/rugged/RuggedMessages_fr.utf8
+++ b/core/src/main/resources/assets/org/orekit/rugged/RuggedMessages_fr.utf8
@@ -30,3 +30,9 @@ LINE_OF_SIGHT_NEVER_CROSSES_ALTITUDE = la ligne de visée ne franchit jamais l''
 
 # line-of-sight enters the Digital Elevation Model behind spacecraft!
 DEM_ENTRY_POINT_IS_BEHIND_SPACECRAFT = la ligne de visée entre dans le Modèle Numérique de Terrain derrière le satellite !
+
+# frame {0} does not match frame {1} from interpolator dump
+FRAMES_MISMATCH_WITH_INTERPOLATOR_DUMP = le repère {0} ne correspond pas au repère {1} de la sauvegarde d''interpolateur
+
+# data is not an interpolator dump
+NOT_INTERPOLATOR_DUMP_DATA = les données ne correspondent pas à une sauvegarde d''interpolateur
diff --git a/core/src/test/java/org/orekit/rugged/api/RuggedMessagesTest.java b/core/src/test/java/org/orekit/rugged/api/RuggedMessagesTest.java
index 0d96c5d1..abde2c08 100644
--- a/core/src/test/java/org/orekit/rugged/api/RuggedMessagesTest.java
+++ b/core/src/test/java/org/orekit/rugged/api/RuggedMessagesTest.java
@@ -29,7 +29,7 @@ public class RuggedMessagesTest {
 
     @Test
     public void testMessageNumber() {
-        Assert.assertEquals(11, RuggedMessages.values().length);
+        Assert.assertEquals(13, RuggedMessages.values().length);
     }
 
     @Test
diff --git a/core/src/test/java/org/orekit/rugged/api/RuggedTest.java b/core/src/test/java/org/orekit/rugged/api/RuggedTest.java
index bcfc5401..63bc4b18 100644
--- a/core/src/test/java/org/orekit/rugged/api/RuggedTest.java
+++ b/core/src/test/java/org/orekit/rugged/api/RuggedTest.java
@@ -17,9 +17,13 @@
 package org.orekit.rugged.api;
 
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
 import java.io.File;
 import java.io.IOException;
 import java.io.RandomAccessFile;
+import java.io.StreamCorruptedException;
 import java.net.URISyntaxException;
 import java.nio.MappedByteBuffer;
 import java.nio.channels.FileChannel;
@@ -534,6 +538,212 @@ public class RuggedTest {
 
     }
 
+    @Test
+    public void testInterpolatorDump()
+        throws RuggedException, OrekitException, URISyntaxException {
+
+        int dimension = 200;
+
+        String path = getClass().getClassLoader().getResource("orekit-data").toURI().getPath();
+        DataProvidersManager.getInstance().addProvider(new DirectoryCrawler(new File(path)));
+        final BodyShape  earth = createEarth();
+        final Orbit      orbit = createOrbit(Constants.EIGEN5C_EARTH_MU);
+
+        AbsoluteDate crossing = new AbsoluteDate("2012-01-01T12:30:00.000", TimeScalesFactory.getUTC());
+
+        // one line sensor
+        // position: 1.5m in front (+X) and 20 cm above (-Z) of the S/C center of mass
+        // los: swath in the (YZ) plane, looking at 50° roll, ±1° aperture
+        Vector3D position = new Vector3D(1.5, 0, -0.2);
+        List<Vector3D> los = createLOS(new Rotation(Vector3D.PLUS_I, FastMath.toRadians(50.0)).applyTo(Vector3D.PLUS_K),
+                                       Vector3D.PLUS_I,
+                                       FastMath.toRadians(1.0), dimension);
+
+        // linear datation model: at reference time we get line 100, and the rate is one line every 1.5ms
+        LineDatation lineDatation = new LinearLineDatation(crossing, dimension / 2, 1.0 / 1.5e-3);
+        int firstLine = 0;
+        int lastLine  = dimension;
+        LineSensor lineSensor = new LineSensor("line", lineDatation, position, los);
+        AbsoluteDate minDate = lineSensor.getDate(firstLine);
+        AbsoluteDate maxDate = lineSensor.getDate(lastLine);
+
+        TileUpdater updater =
+                new RandomLandscapeUpdater(0.0, 9000.0, 0.5, 0x84186d1344722b8fl,
+                                           FastMath.toRadians(1.0), 257);
+
+        Rugged ruggedOriginal = new Rugged(updater, 8, AlgorithmId.DUVENHAGE,
+                                       EllipsoidId.WGS84, InertialFrameId.EME2000, BodyRotatingFrameId.ITRF,
+                                       minDate, maxDate, 5.0,
+                                       orbitToPV(orbit, earth, minDate.shiftedBy(-1.0), maxDate.shiftedBy(+1.0), 0.25), 8,
+                                       CartesianDerivativesFilter.USE_PV,
+                                       orbitToQ(orbit, earth, minDate.shiftedBy(-1.0), maxDate.shiftedBy(+1.0), 0.25), 2,
+                                       AngularDerivativesFilter.USE_R, 0.001);
+        ruggedOriginal.addLineSensor(lineSensor);
+        GeodeticPoint[] gpOriginal = ruggedOriginal.directLocalization("line", 100);
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ruggedOriginal.dumpInterpolator(bos);
+        Assert.assertTrue(bos.size() > 100000);
+        Assert.assertTrue(bos.size() < 200000);
+
+        Rugged ruggedRecovered = new Rugged(updater, 8, AlgorithmId.DUVENHAGE,
+                                            EllipsoidId.WGS84, InertialFrameId.EME2000, BodyRotatingFrameId.ITRF,
+                                            new ByteArrayInputStream(bos.toByteArray()));
+        ruggedRecovered.addLineSensor(lineSensor);
+        GeodeticPoint[] gpRecovered = ruggedRecovered.directLocalization("line", 100);
+
+        for (int i = 0; i < gpOriginal.length; ++i) {
+            Vector3D pOriginal  = earth.transform(gpOriginal[i]);
+            Vector3D pRecovered = earth.transform(gpRecovered[i]);
+            Assert.assertEquals(0.0, Vector3D.distance(pOriginal, pRecovered), 1.0e-15);
+        }
+
+    }
+
+    @Test
+    public void testInterpolatorDumpWrongFrame()
+        throws RuggedException, OrekitException, URISyntaxException {
+
+        int dimension = 200;
+
+        String path = getClass().getClassLoader().getResource("orekit-data").toURI().getPath();
+        DataProvidersManager.getInstance().addProvider(new DirectoryCrawler(new File(path)));
+        final BodyShape  earth = createEarth();
+        final Orbit      orbit = createOrbit(Constants.EIGEN5C_EARTH_MU);
+
+        AbsoluteDate crossing = new AbsoluteDate("2012-01-01T12:30:00.000", TimeScalesFactory.getUTC());
+
+        // one line sensor
+        // position: 1.5m in front (+X) and 20 cm above (-Z) of the S/C center of mass
+        // los: swath in the (YZ) plane, looking at 50° roll, ±1° aperture
+        Vector3D position = new Vector3D(1.5, 0, -0.2);
+        List<Vector3D> los = createLOS(new Rotation(Vector3D.PLUS_I, FastMath.toRadians(50.0)).applyTo(Vector3D.PLUS_K),
+                                       Vector3D.PLUS_I,
+                                       FastMath.toRadians(1.0), dimension);
+
+        // linear datation model: at reference time we get line 100, and the rate is one line every 1.5ms
+        LineDatation lineDatation = new LinearLineDatation(crossing, dimension / 2, 1.0 / 1.5e-3);
+        int firstLine = 0;
+        int lastLine  = dimension;
+        LineSensor lineSensor = new LineSensor("line", lineDatation, position, los);
+        AbsoluteDate minDate = lineSensor.getDate(firstLine);
+        AbsoluteDate maxDate = lineSensor.getDate(lastLine);
+
+        Rugged ruggedOriginal = new Rugged(null, -1, AlgorithmId.IGNORE_DEM_USE_ELLIPSOID,
+                                       EllipsoidId.WGS84, InertialFrameId.EME2000, BodyRotatingFrameId.ITRF,
+                                       minDate, maxDate, 5.0,
+                                       orbitToPV(orbit, earth, minDate.shiftedBy(-1.0), maxDate.shiftedBy(+1.0), 0.25), 8,
+                                       CartesianDerivativesFilter.USE_PV,
+                                       orbitToQ(orbit, earth, minDate.shiftedBy(-1.0), maxDate.shiftedBy(+1.0), 0.25), 2,
+                                       AngularDerivativesFilter.USE_R, 0.001);
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ruggedOriginal.dumpInterpolator(bos);
+        Assert.assertTrue(bos.size() > 100000);
+        Assert.assertTrue(bos.size() < 200000);
+
+        for (InertialFrameId iId : Arrays.asList(InertialFrameId.GCRF,
+                                                 InertialFrameId.MOD,
+                                                 InertialFrameId.TOD,
+                                                 InertialFrameId.VEIS1950)) {
+            try {
+                new Rugged(null, -1, AlgorithmId.IGNORE_DEM_USE_ELLIPSOID,
+                           EllipsoidId.WGS84, iId, BodyRotatingFrameId.ITRF,
+                           new ByteArrayInputStream(bos.toByteArray()));
+                Assert.fail("an exception should have been thrown");
+            } catch (RuggedException re) {
+                Assert.assertEquals(RuggedMessages.FRAMES_MISMATCH_WITH_INTERPOLATOR_DUMP,
+                                    re.getSpecifier());
+            }
+        }
+
+        for (BodyRotatingFrameId bId : Arrays.asList(BodyRotatingFrameId.GTOD,
+                                                     BodyRotatingFrameId.ITRF_EQUINOX)) {
+            try {
+                new Rugged(null, -1, AlgorithmId.IGNORE_DEM_USE_ELLIPSOID,
+                           EllipsoidId.WGS84, InertialFrameId.EME2000, bId,
+                           new ByteArrayInputStream(bos.toByteArray()));
+                Assert.fail("an exception should have been thrown");
+            } catch (RuggedException re) {
+                Assert.assertEquals(RuggedMessages.FRAMES_MISMATCH_WITH_INTERPOLATOR_DUMP,
+                                    re.getSpecifier());
+            }
+        }
+    }
+
+    @Test
+    public void testInterpolatorNotADump()
+        throws RuggedException, OrekitException, URISyntaxException {
+
+        String path = getClass().getClassLoader().getResource("orekit-data").toURI().getPath();
+        DataProvidersManager.getInstance().addProvider(new DirectoryCrawler(new File(path)));
+
+        // the following array is a real serialization file corresponding to the following
+        // made-up empty class that does not exist in Rugged:
+        //        public class NonExistentClass implements java.io.Serializable {
+        //            private static final long serialVersionUID = -1;
+        //        }
+        byte[] nonExistentClass = new byte[] {
+            -84, -19,   0,   5, 115, 114,
+              0,  16,  78, 111, 110,  69,
+            120, 105, 115, 116, 101, 110,
+            116,  67, 108,  97, 115, 115,
+             -1,  -1,  -1,  -1,  -1,  -1,
+             -1,  -1,   2,   0,   0, 120,
+            112
+        };
+
+        // the following array is a real serialization file of object Integer.valueOf(1)
+        byte[] integerOne = new byte[] {
+          -84, -19,   0,   5, 115, 114,
+            0,  17, 106,  97, 118,  97,
+           46, 108,  97, 110, 103,  46,
+           73, 110, 116, 101, 103, 101,
+          114,  18, -30, -96, -92,  -9,
+         -127,-121,  56,   2,   0,   1,
+           73,   0,   5, 118,  97, 108,
+          117, 101, 120, 114,   0,  16,
+          106,  97, 118,  97,  46, 108,
+           97, 110, 103,  46,  78, 117,
+          109,  98, 101, 114,-122, -84,
+         -107,  29,  11,-108, -32,-117,
+            2,   0,   0, 120, 112,   0,
+            0,   0,   1
+        };
+
+        // the following array is a truncation of the previous one
+        byte[] truncatedDump = new byte[] {
+          -84, -19,   0,   5, 115, 114,
+            0,  17, 106,  97, 118,  97
+        };
+
+        byte[] notSerialization = new byte[] {
+            1, 2, 3, 4, 5, 6
+        };
+
+        for (byte[] array : Arrays.asList(nonExistentClass, integerOne, truncatedDump, notSerialization)) {
+            try {
+                new Rugged(null, -1, AlgorithmId.IGNORE_DEM_USE_ELLIPSOID,
+                           EllipsoidId.WGS84, InertialFrameId.EME2000, BodyRotatingFrameId.ITRF,
+                           new ByteArrayInputStream(array));
+                Assert.fail("an exception should have been thrown");
+            } catch (RuggedException re) {
+                Assert.assertEquals(RuggedMessages.NOT_INTERPOLATOR_DUMP_DATA,
+                                    re.getSpecifier());
+                if (array == nonExistentClass) {
+                    Assert.assertEquals(ClassNotFoundException.class, re.getCause().getClass());
+                } else if (array == integerOne) {
+                    Assert.assertEquals(ClassCastException.class, re.getCause().getClass());
+                } else if (array == truncatedDump) {
+                    Assert.assertEquals(EOFException.class, re.getCause().getClass());
+                } else if (array == notSerialization) {
+                    Assert.assertEquals(StreamCorruptedException.class, re.getCause().getClass());
+                }
+            }
+        }
+
+    }
+
     // the following test is disabled by default
     // it is only used to check timings, and also creates a large (38M) temporary file
     @Test
diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml
index 5b656467..2751dc15 100644
--- a/src/site/xdoc/changes.xml
+++ b/src/site/xdoc/changes.xml
@@ -22,6 +22,10 @@
   <body>
     <release version="1.0" date="TBD"
              description="TBD">
+      <action dev="luc" type="add">
+        Added a way to reuse transform interpolator from one run to another
+        by dumping its state into a file, thus avoiding costly initialization.
+      </action>
       <action dev="luc" type="add">
         Time step for internal transform caching is now user-configurable.
       </action>
-- 
GitLab