1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.math4.legacy.ode;
19
20 import java.io.Serializable;
21 import java.util.ArrayList;
22 import java.util.List;
23
24 import org.apache.commons.math4.legacy.exception.DimensionMismatchException;
25 import org.apache.commons.math4.legacy.exception.MathIllegalArgumentException;
26 import org.apache.commons.math4.legacy.exception.MaxCountExceededException;
27 import org.apache.commons.math4.legacy.exception.util.LocalizedFormats;
28 import org.apache.commons.math4.legacy.ode.sampling.StepHandler;
29 import org.apache.commons.math4.legacy.ode.sampling.StepInterpolator;
30 import org.apache.commons.math4.core.jdkmath.JdkMath;
31
32 /**
33 * This class stores all information provided by an ODE integrator
34 * during the integration process and build a continuous model of the
35 * solution from this.
36 *
37 * <p>This class act as a step handler from the integrator point of
38 * view. It is called iteratively during the integration process and
39 * stores a copy of all steps information in a sorted collection for
40 * later use. Once the integration process is over, the user can use
41 * the {@link #setInterpolatedTime setInterpolatedTime} and {@link
42 * #getInterpolatedState getInterpolatedState} to retrieve this
43 * information at any time. It is important to wait for the
44 * integration to be over before attempting to call {@link
45 * #setInterpolatedTime setInterpolatedTime} because some internal
46 * variables are set only once the last step has been handled.</p>
47 *
48 * <p>This is useful for example if the main loop of the user
49 * application should remain independent from the integration process
50 * or if one needs to mimic the behaviour of an analytical model
51 * despite a numerical model is used (i.e. one needs the ability to
52 * get the model value at any time or to navigate through the
53 * data).</p>
54 *
55 * <p>If problem modeling is done with several separate
56 * integration phases for contiguous intervals, the same
57 * ContinuousOutputModel can be used as step handler for all
58 * integration phases as long as they are performed in order and in
59 * the same direction. As an example, one can extrapolate the
60 * trajectory of a satellite with one model (i.e. one set of
61 * differential equations) up to the beginning of a maneuver, use
62 * another more complex model including thrusters modeling and
63 * accurate attitude control during the maneuver, and revert to the
64 * first model after the end of the maneuver. If the same continuous
65 * output model handles the steps of all integration phases, the user
66 * do not need to bother when the maneuver begins or ends, he has all
67 * the data available in a transparent manner.</p>
68 *
69 * <p>An important feature of this class is that it implements the
70 * <code>Serializable</code> interface. This means that the result of
71 * an integration can be serialized and reused later (if stored into a
72 * persistent medium like a file system or a database) or elsewhere (if
73 * sent to another application). Only the result of the integration is
74 * stored, there is no reference to the integrated problem by
75 * itself.</p>
76 *
77 * <p>One should be aware that the amount of data stored in a
78 * ContinuousOutputModel instance can be important if the state vector
79 * is large, if the integration interval is long or if the steps are
80 * small (which can result from small tolerance settings in {@link
81 * org.apache.commons.math4.legacy.ode.nonstiff.AdaptiveStepsizeIntegrator adaptive
82 * step size integrators}).</p>
83 *
84 * @see StepHandler
85 * @see StepInterpolator
86 * @since 1.2
87 */
88
89 public class ContinuousOutputModel
90 implements StepHandler, Serializable {
91
92 /** Serializable version identifier. */
93 private static final long serialVersionUID = -1417964919405031606L;
94
95 /** Initial integration time. */
96 private double initialTime;
97
98 /** Final integration time. */
99 private double finalTime;
100
101 /** Integration direction indicator. */
102 private boolean forward;
103
104 /** Current interpolator index. */
105 private int index;
106
107 /** Steps table. */
108 private List<StepInterpolator> steps;
109
110 /** Simple constructor.
111 * Build an empty continuous output model.
112 */
113 public ContinuousOutputModel() {
114 steps = new ArrayList<>();
115 initialTime = Double.NaN;
116 finalTime = Double.NaN;
117 forward = true;
118 index = 0;
119 }
120
121 /** Append another model at the end of the instance.
122 * @param model model to add at the end of the instance
123 * @exception MathIllegalArgumentException if the model to append is not
124 * compatible with the instance (dimension of the state vector,
125 * propagation direction, hole between the dates)
126 * @exception MaxCountExceededException if the number of functions evaluations is exceeded
127 * during step finalization
128 */
129 public void append(final ContinuousOutputModel model)
130 throws MathIllegalArgumentException, MaxCountExceededException {
131
132 if (model.steps.isEmpty()) {
133 return;
134 }
135
136 if (steps.isEmpty()) {
137 initialTime = model.initialTime;
138 forward = model.forward;
139 } else {
140
141 if (getInterpolatedState().length != model.getInterpolatedState().length) {
142 throw new DimensionMismatchException(model.getInterpolatedState().length,
143 getInterpolatedState().length);
144 }
145
146 if (forward ^ model.forward) {
147 throw new MathIllegalArgumentException(LocalizedFormats.PROPAGATION_DIRECTION_MISMATCH);
148 }
149
150 final StepInterpolator lastInterpolator = steps.get(index);
151 final double current = lastInterpolator.getCurrentTime();
152 final double previous = lastInterpolator.getPreviousTime();
153 final double step = current - previous;
154 final double gap = model.getInitialTime() - current;
155 if (JdkMath.abs(gap) > 1.0e-3 * JdkMath.abs(step)) {
156 throw new MathIllegalArgumentException(LocalizedFormats.HOLE_BETWEEN_MODELS_TIME_RANGES,
157 JdkMath.abs(gap));
158 }
159 }
160
161 for (StepInterpolator interpolator : model.steps) {
162 steps.add(interpolator.copy());
163 }
164
165 index = steps.size() - 1;
166 finalTime = (steps.get(index)).getCurrentTime();
167 }
168
169 /** {@inheritDoc} */
170 @Override
171 public void init(double t0, double[] y0, double t) {
172 initialTime = Double.NaN;
173 finalTime = Double.NaN;
174 forward = true;
175 index = 0;
176 steps.clear();
177 }
178
179 /** Handle the last accepted step.
180 * A copy of the information provided by the last step is stored in
181 * the instance for later use.
182 * @param interpolator interpolator for the last accepted step.
183 * @param isLast true if the step is the last one
184 * @exception MaxCountExceededException if the number of functions evaluations is exceeded
185 * during step finalization
186 */
187 @Override
188 public void handleStep(final StepInterpolator interpolator, final boolean isLast)
189 throws MaxCountExceededException {
190
191 if (steps.isEmpty()) {
192 initialTime = interpolator.getPreviousTime();
193 forward = interpolator.isForward();
194 }
195
196 steps.add(interpolator.copy());
197
198 if (isLast) {
199 finalTime = interpolator.getCurrentTime();
200 index = steps.size() - 1;
201 }
202 }
203
204 /**
205 * Get the initial integration time.
206 * @return initial integration time
207 */
208 public double getInitialTime() {
209 return initialTime;
210 }
211
212 /**
213 * Get the final integration time.
214 * @return final integration time
215 */
216 public double getFinalTime() {
217 return finalTime;
218 }
219
220 /**
221 * Get the time of the interpolated point.
222 * If {@link #setInterpolatedTime} has not been called, it returns
223 * the final integration time.
224 * @return interpolation point time
225 */
226 public double getInterpolatedTime() {
227 return steps.get(index).getInterpolatedTime();
228 }
229
230 /** Set the time of the interpolated point.
231 * <p>This method should <strong>not</strong> be called before the
232 * integration is over because some internal variables are set only
233 * once the last step has been handled.</p>
234 * <p>Setting the time outside of the integration interval is now
235 * allowed, but should be used with care since the accuracy of the
236 * interpolator will probably be very poor far from this interval.
237 * This allowance has been added to simplify implementation of search
238 * algorithms near the interval endpoints.</p>
239 * <p>Note that each time this method is called, the internal arrays
240 * returned in {@link #getInterpolatedState()}, {@link
241 * #getInterpolatedDerivatives()} and {@link #getInterpolatedSecondaryState(int)}
242 * <em>will</em> be overwritten. So if their content must be preserved
243 * across several calls, user must copy them.</p>
244 * @param time time of the interpolated point
245 * @see #getInterpolatedState()
246 * @see #getInterpolatedDerivatives()
247 * @see #getInterpolatedSecondaryState(int)
248 */
249 public void setInterpolatedTime(final double time) {
250
251 // initialize the search with the complete steps table
252 int iMin = 0;
253 final StepInterpolator sMin = steps.get(iMin);
254 double tMin = 0.5 * (sMin.getPreviousTime() + sMin.getCurrentTime());
255
256 int iMax = steps.size() - 1;
257 final StepInterpolator sMax = steps.get(iMax);
258 double tMax = 0.5 * (sMax.getPreviousTime() + sMax.getCurrentTime());
259
260 // handle points outside of the integration interval
261 // or in the first and last step
262 if (locatePoint(time, sMin) <= 0) {
263 index = iMin;
264 sMin.setInterpolatedTime(time);
265 return;
266 }
267 if (locatePoint(time, sMax) >= 0) {
268 index = iMax;
269 sMax.setInterpolatedTime(time);
270 return;
271 }
272
273 // reduction of the table slice size
274 while (iMax - iMin > 5) {
275
276 // use the last estimated index as the splitting index
277 final StepInterpolator si = steps.get(index);
278 final int location = locatePoint(time, si);
279 if (location < 0) {
280 iMax = index;
281 tMax = 0.5 * (si.getPreviousTime() + si.getCurrentTime());
282 } else if (location > 0) {
283 iMin = index;
284 tMin = 0.5 * (si.getPreviousTime() + si.getCurrentTime());
285 } else {
286 // we have found the target step, no need to continue searching
287 si.setInterpolatedTime(time);
288 return;
289 }
290
291 // compute a new estimate of the index in the reduced table slice
292 final int iMed = (iMin + iMax) / 2;
293 final StepInterpolator sMed = steps.get(iMed);
294 final double tMed = 0.5 * (sMed.getPreviousTime() + sMed.getCurrentTime());
295
296 if (JdkMath.abs(tMed - tMin) < 1e-6 || JdkMath.abs(tMax - tMed) < 1e-6) {
297 // too close to the bounds, we estimate using a simple dichotomy
298 index = iMed;
299 } else {
300 // estimate the index using a reverse quadratic polynom
301 // (reverse means we have i = P(t), thus allowing to simply
302 // compute index = P(time) rather than solving a quadratic equation)
303 final double d12 = tMax - tMed;
304 final double d23 = tMed - tMin;
305 final double d13 = tMax - tMin;
306 final double dt1 = time - tMax;
307 final double dt2 = time - tMed;
308 final double dt3 = time - tMin;
309 final double iLagrange = ((dt2 * dt3 * d23) * iMax -
310 (dt1 * dt3 * d13) * iMed +
311 (dt1 * dt2 * d12) * iMin) /
312 (d12 * d23 * d13);
313 index = (int) JdkMath.rint(iLagrange);
314 }
315
316 // force the next size reduction to be at least one tenth
317 final int low = JdkMath.max(iMin + 1, (9 * iMin + iMax) / 10);
318 final int high = JdkMath.min(iMax - 1, (iMin + 9 * iMax) / 10);
319 if (index < low) {
320 index = low;
321 } else if (index > high) {
322 index = high;
323 }
324 }
325
326 // now the table slice is very small, we perform an iterative search
327 index = iMin;
328 while (index <= iMax && locatePoint(time, steps.get(index)) > 0) {
329 ++index;
330 }
331
332 steps.get(index).setInterpolatedTime(time);
333 }
334
335 /**
336 * Get the state vector of the interpolated point.
337 * <p>The returned vector is a reference to a reused array, so
338 * it should not be modified and it should be copied if it needs
339 * to be preserved across several calls to the associated
340 * {@link #setInterpolatedTime(double)} method.</p>
341 * @return state vector at time {@link #getInterpolatedTime}
342 * @exception MaxCountExceededException if the number of functions evaluations is exceeded
343 * @see #setInterpolatedTime(double)
344 * @see #getInterpolatedDerivatives()
345 * @see #getInterpolatedSecondaryState(int)
346 * @see #getInterpolatedSecondaryDerivatives(int)
347 */
348 public double[] getInterpolatedState() throws MaxCountExceededException {
349 return steps.get(index).getInterpolatedState();
350 }
351
352 /**
353 * Get the derivatives of the state vector of the interpolated point.
354 * <p>The returned vector is a reference to a reused array, so
355 * it should not be modified and it should be copied if it needs
356 * to be preserved across several calls to the associated
357 * {@link #setInterpolatedTime(double)} method.</p>
358 * @return derivatives of the state vector at time {@link #getInterpolatedTime}
359 * @exception MaxCountExceededException if the number of functions evaluations is exceeded
360 * @see #setInterpolatedTime(double)
361 * @see #getInterpolatedState()
362 * @see #getInterpolatedSecondaryState(int)
363 * @see #getInterpolatedSecondaryDerivatives(int)
364 * @since 3.4
365 */
366 public double[] getInterpolatedDerivatives() throws MaxCountExceededException {
367 return steps.get(index).getInterpolatedDerivatives();
368 }
369
370 /** Get the interpolated secondary state corresponding to the secondary equations.
371 * <p>The returned vector is a reference to a reused array, so
372 * it should not be modified and it should be copied if it needs
373 * to be preserved across several calls to the associated
374 * {@link #setInterpolatedTime(double)} method.</p>
375 * @param secondaryStateIndex index of the secondary set, as returned by {@link
376 * org.apache.commons.math4.legacy.ode.ExpandableStatefulODE#addSecondaryEquations(
377 * org.apache.commons.math4.legacy.ode.SecondaryEquations)
378 * ExpandableStatefulODE.addSecondaryEquations(SecondaryEquations)}
379 * @return interpolated secondary state at the current interpolation date
380 * @see #setInterpolatedTime(double)
381 * @see #getInterpolatedState()
382 * @see #getInterpolatedDerivatives()
383 * @see #getInterpolatedSecondaryDerivatives(int)
384 * @since 3.2
385 * @exception MaxCountExceededException if the number of functions evaluations is exceeded
386 */
387 public double[] getInterpolatedSecondaryState(final int secondaryStateIndex)
388 throws MaxCountExceededException {
389 return steps.get(index).getInterpolatedSecondaryState(secondaryStateIndex);
390 }
391
392 /** Get the interpolated secondary derivatives corresponding to the secondary equations.
393 * <p>The returned vector is a reference to a reused array, so
394 * it should not be modified and it should be copied if it needs
395 * to be preserved across several calls to the associated
396 * {@link #setInterpolatedTime(double)} method.</p>
397 * @param secondaryStateIndex index of the secondary set, as returned by {@link
398 * org.apache.commons.math4.legacy.ode.ExpandableStatefulODE#addSecondaryEquations(
399 * org.apache.commons.math4.legacy.ode.SecondaryEquations)
400 * ExpandableStatefulODE.addSecondaryEquations(SecondaryEquations)}
401 * @return interpolated secondary derivatives at the current interpolation date
402 * @see #setInterpolatedTime(double)
403 * @see #getInterpolatedState()
404 * @see #getInterpolatedDerivatives()
405 * @see #getInterpolatedSecondaryState(int)
406 * @since 3.4
407 * @exception MaxCountExceededException if the number of functions evaluations is exceeded
408 */
409 public double[] getInterpolatedSecondaryDerivatives(final int secondaryStateIndex)
410 throws MaxCountExceededException {
411 return steps.get(index).getInterpolatedSecondaryDerivatives(secondaryStateIndex);
412 }
413
414 /** Compare a step interval and a double.
415 * @param time point to locate
416 * @param interval step interval
417 * @return -1 if the double is before the interval, 0 if it is in
418 * the interval, and +1 if it is after the interval, according to
419 * the interval direction
420 */
421 private int locatePoint(final double time, final StepInterpolator interval) {
422 if (forward) {
423 if (time < interval.getPreviousTime()) {
424 return -1;
425 } else if (time > interval.getCurrentTime()) {
426 return +1;
427 } else {
428 return 0;
429 }
430 }
431 if (time > interval.getPreviousTime()) {
432 return -1;
433 } else if (time < interval.getCurrentTime()) {
434 return +1;
435 } else {
436 return 0;
437 }
438 }
439 }