File: GeodesicIntegrator.h

package info (click to toggle)
simbody 3.7%2Bdfsg-4
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 72,896 kB
  • sloc: cpp: 248,827; ansic: 18,240; sh: 29; makefile: 24
file content (402 lines) | stat: -rw-r--r-- 18,360 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
#ifndef SimTK_SIMMATH_GEODESIC_INTEGRATOR_H_
#define SimTK_SIMMATH_GEODESIC_INTEGRATOR_H_

/* -------------------------------------------------------------------------- *
 *                        Simbody(tm): SimTKmath                              *
 * -------------------------------------------------------------------------- *
 * This is part of the SimTK biosimulation toolkit originating from           *
 * Simbios, the NIH National Center for Physics-Based Simulation of           *
 * Biological Structures at Stanford, funded under the NIH Roadmap for        *
 * Medical Research, grant U54 GM072970. See https://simtk.org/home/simbody.  *
 *                                                                            *
 * Portions copyright (c) 2012 Stanford University and the Authors.           *
 * Authors: Michael Sherman                                                   *
 * Contributors:                                                              *
 *                                                                            *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may    *
 * not use this file except in compliance with the License. You may obtain a  *
 * copy of the License at http://www.apache.org/licenses/LICENSE-2.0.         *
 *                                                                            *
 * Unless required by applicable law or agreed to in writing, software        *
 * distributed under the License is distributed on an "AS IS" BASIS,          *
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
 * See the License for the specific language governing permissions and        *
 * limitations under the License.                                             *
 * -------------------------------------------------------------------------- */

/** @file
 * Contains a stripped-down low overhead numerical integrator for use with
 * small, fixed-sized sets of Differential-Algebraic equations such as arise
 * when computing geodesics over smooth surfaces.
 */

#include "SimTKcommon.h"
#include "simmath/internal/common.h"

namespace SimTK {

/** This is a stripped-down numerical integrator for small ODE or DAE problems
whose size is known at compile time, with no provision for discrete variables,
event detection, or interpolation. You cannot use this integrator to advance
a Simbody System; see Integrator instead. Everything is defined in this header
file so that the integration can proceed with virtually no overhead. Templates
are used rather than run-time polymorphism, so there are no virtual function
calls. The system of equations is given as a template object that must
implement particular methods which the compiler may inline if they are simple
enough.

<h3>Class Equations</h3>
This integrator is instantiated with a class that encapsulates the system of
equations to be solved, and must provide compile time constants and methods 
with the following signatures:
@code
class MyEquations {
public:
    enum { NQ=5,       // number of 2nd order equations
           NC=3,       // total number of position & velocity constraints
           N = 2*NQ }; // total number of states y

    // Calculate state derivatives ydot given time and state y.
    void calcDerivs(Real t, const Vec<N>& y, Vec<N>& ydot) const;

    // Calculate amount by which the given time and state y violate the
    // constraints and return the constraint errors in cerr.
    void calcConstraintErrors(Real t, const Vec<N>& y, Vec<NC>& cerr) const;

    // Given a time and state y, ensure that the state satisfies the constraints
    // to within the indicated absolute tolerance, by performing the shortest
    // (i.e. least squares) projection of the state back to the constraint
    // manifold. Return false if the desired tolerance cannot be achieved. 
    // Otherwise (true return), a subsequent call to calcConstraintErrors() 
    // would return each |cerr[i]|<=consTol.
    bool projectIfNeeded(Real consTol, Real t, Vec<N>& y) const;
};

@endcode

<h3>Usage</h3>

@code
// Create an object of a type that satisfies the Equations description above.
MyEquations eqns(...); // ... is whatever arguments you require.
// Instantiate an integrator for this system of equations and specify the
// integration accuracy and constraint tolerance.
GeodesicIntegrator<MyEquations> integ(eqns, accuracy, constraintTol);
// Initialize; will project constraints if necessary.
integ.initialize(t0, y0);
// Integrate to time finalTime, getting output every completed step.
while (true) {
    std::cout << "t=" << integ.getTime() << " y=" << integ.getY() << "\n";
    if (integ.getTime() == finalTime)
        break;
    integ.takeOneStep(finalTime);
}
@endcode

<h3>Mathematical Overview</h3>

This is an explicit, variable-step integrator solving a 2nd-order DAE 
structured as an ODE-on-a-manifold system[1] like this:
<pre>
        (1)  udot = f(t,q,u)      NQ dynamic differential equations
        (2)  qdot = u             NQ kinematic differential equations
        (3)  0    = c(t,q,u)      NC constraints
</pre>
Here the "dot" suffix indicates differentiation with respect to the independent
variable t which we'll refer to as time here although it can be anything (for
geodesic calculations it is arc length). We'll  
call the second order variables q the "position variables", and their time 
derivatives u the "velocity variables". Collected together we call the state 
y={q,u}. At the beginning of a step, we expect to have been given initial 
conditions t0,q0,u0 such that |c(t0,q0,u0)|<=tol. The user provides the accuracy 
requirement and constraint tolerance. We solve the system to that accuracy while
keeping the constraints within tolerance. The integrator returns after taking
a successful step which may involve trial evaluations that are retracted.

By "ODE on a manifold" we mean that the ODE (1,2) automatically satisfies the 
condition that IF c==0, THEN cdot=0, where
<pre>
    cdot=Dc/Dt + Dc/Dq*qdot + Dc/Du*udot
</pre>
This means that satisfaction of the acceleration-level constraints is built
into the dynamic differential equations (1) so that we need only deal with
relatively slow drift of the solution away from the position and velocity
constraint manifolds.

To handle the constraint drift we use the method of coordinate projection and
expect the supplied Equations object to be able to perform a least-squares
projection of a state (q,u) to move it onto the constraint manifolds.

[1] Hairer, Lubich, Wanner, "Geometric Numerical Integration: 
Structure-Preserving Algorithms for Ordinary Differential Equations", 2nd ed., 
section IV.4, pg 109ff, Springer, 2006. **/
template <class Eqn>
class GeodesicIntegrator {
public:
    enum { NQ = Eqn::NQ, NC = Eqn::NC, N = 2*NQ };

    /** Construct an integrator for the given set of equations \a eqn, which are
    to be solved to the given \a accuracy, with constraints maintained to 
    within the given \a constraintTol. **/
    GeodesicIntegrator(const Eqn& eqn, Real accuracy, Real constraintTol) 
    :   m_eqn(eqn), m_accuracy(accuracy), m_consTol(constraintTol),
        m_hInit(NaN), m_hLast(NaN), m_hNext(Real(0.1)), m_nInitialize(0) 
    {   reinitializeCounters(); }

    /** Call this once before taking a series of steps. This sets the initial
    conditions, and calculates the starting derivatives and constraint errors.
    The constraints must be satisfied already by the given state; an error
    is thrown if not. **/
    void initialize(Real t, const Vec<N>& y) {
        ++m_nInitialize;
        reinitializeCounters();
        m_hInit = m_hLast = NaN;
        m_hNext = Real(0.1); // override if you have a better idea
        m_t = t; m_y = y;
        if (!m_eqn.projectIfNeeded(m_consTol, m_t, m_y)) {
            Vec<NC> cerr;
            m_eqn.calcConstraintErrors(m_t, m_y, cerr);
            const Real consErr = calcNormInf(cerr);
            SimTK_ERRCHK2_ALWAYS(!"projection failed",
                "GeodesicIntegrator::initialize()",
                "Couldn't project constraints to tol=%g;"
                " largest error was %g.", m_consTol, consErr);
        }
        m_eqn.calcDerivs(m_t, m_y, m_ydot);
    }

    /** Set initial time and state prior to integrating. State derivatives
    and constraint errors are calculated and an error is thrown if the 
    constraints are not already satisifed to the required tolerance. **/
    void setTimeAndState(Real t, const Vec<N>& y) {
        m_t = t; m_y = y;
        m_eqn.calcDerivs(m_t, m_y, m_ydot);
        Vec<NC> cerr;
        m_eqn.calcConstraintErrors(m_t, m_y, cerr);
        const Real consErr = calcNormInf(cerr);
        if (consErr > m_consTol)
            SimTK_ERRCHK2_ALWAYS(!"constraints not satisfied",
                "GeodesicIntegrator::setTimeAndState()",
                "Supplied state failed to satisfy constraints to tol=%g;"
                " largest error was %g.", m_consTol, consErr);
    }

    /** Use this if you think you know a better initial step size to try than
    the default. **/
    void setNextStepSizeToTry(Real h) {m_hNext=h;}
    /** Return the size of the next time step the integrator will attempt on
    the next call to takeOneStep(). **/
    Real getNextStepSizeToTry() const {return m_hNext;}

    /** Return the accuracy requirement as set in the constructor. **/
    Real getRequiredAccuracy() const {return m_accuracy;}
    /** Return the constraint tolerance as set in the constructor. **/
    Real getConstraintTolerance() const {return m_consTol;}

    /** Return the size of the first accepted step to be taken after the most
    recent initialize() call. **/
    Real getActualInitialStepSizeTaken() const {return m_hInit;}

    /** Return the number of successful time steps taken since the most recent
    initialize() call. **/
    int getNumStepsTaken() const {return m_nStepsTaken;}
    /** Return the total number of steps that were attempted since the most
    recent initialize() call. In general this will be more than the number
    of steps taken since some will be rejected. **/
    int getNumStepsAttempted() const {return m_nStepsAttempted;}
    /** How many steps were rejected because they did not satisfy the 
    accuracy requirement, since the most recent initialize() call. This is
    common but for non-stiff systems should be only a modest fraction of the
    number of steps taken. **/
    int getNumErrorTestFailures() const {return m_nErrtestFailures;}
    /** How many steps were rejected because the projectIfNeeded() method was
    unable to satisfy the constraint tolerance (since the most recent
    initialize() call). This should be very rare. **/
    int getNumProjectionFailures() const {return m_nProjectionFailures;}

    /** Return the number of calls to initialize() since construction of this
    integrator object. **/
    int getNumInitializations() const {return m_nInitialize;}

    /** Advance time and state by one error-controlled step and return, but 
    in no case advance past t=tStop. The integrator's internal time, 
    state, and state derivatives are advanced to the end of the step. If this
    step reaches \a tStop, the returned time will be \e exactly tStop. **/
    void takeOneStep(Real tStop);

    /** Return the current time. **/
    const Real& getTime() const {return m_t;}
    /** Return the complete current state as a Vec<N>. **/
    const Vec<N>&  getY() const {return m_y;}
    /** Return just the "position" variables q from the current state. **/
    const Vec<NQ>& getQ() const {return Vec<NQ>::getAs(&m_y[0]);}
    /** Return just the "velocity" variables u from the current state. **/
    const Vec<NQ>& getU() const {return Vec<NQ>::getAs(&m_y[NQ]);}
    /** Return the complete set of time derivatives of the current state. **/
    const Vec<N>&  getYDot() const {return m_ydot;}
    /** Return just the derivatives qdot of the "position" variables q. **/
    const Vec<NQ>& getQDot() const {return Vec<NQ>::getAs(&m_ydot[0]);}
    /** Return just the derivatives udot of the "velocity" variables u. **/
    const Vec<NQ>& getUDot() const {return Vec<NQ>::getAs(&m_ydot[NQ]);}

    /** This is a utility routine that returns the infinity norm (maximum
    absolute value) contained in a fixed-size, scalar Vec. **/
    template <int Z> static Real calcNormInf(const Vec<Z>& v) {
        Real norm = 0;
        for (int i=0; i < Z; ++i) {
            Real aval = std::abs(v[i]);
            if (aval > norm) norm = aval;
        }
        return norm;
    }

    /** This is a utility routine that returns the RMS norm of a fixed-size, 
    scalar Vec. **/
    template <int Z> static Real calcNormRMS(const Vec<Z>& v) {
        Real norm = 0;
        for (int i=0; i< Z; ++i) 
            norm += square(v[i]);
        return std::sqrt(norm/Z);
    }

private:
    void takeRKMStep(Real h, Vec<N>& y1, Vec<N>& y1err) const;

    const Eqn&      m_eqn;      // The DAE system to be solved.
    Real            m_accuracy; // Absolute accuracy requirement for y.
    Real            m_consTol;  // Absolute tolerance for constraint errors.

    Real            m_t;        // Current value of the independent variable.
    Vec<N>          m_y;        // Current q,u in that order.

    Real            m_hInit;    // Actual initial step taken.
    Real            m_hLast;    // Last step taken.
    Real            m_hNext;    // max step size to try next

    Vec<N>          m_ydot;     // ydot(t,y)

    // Counters.
    int m_nInitialize; // zeroed on construction only
    void reinitializeCounters() {
        m_nStepsTaken=m_nStepsAttempted=0;
        m_nErrtestFailures=m_nProjectionFailures=0;
    }
    int m_nStepsTaken;
    int m_nStepsAttempted;
    int m_nErrtestFailures;
    int m_nProjectionFailures;
};

template <class Eqn> void
GeodesicIntegrator<Eqn>::takeOneStep(Real tStop) {
    const Real Safety = Real(0.9), MinShrink = Real(0.1), MaxGrow = Real(5);
    const Real HysteresisLow =  Real(0.9), HysteresisHigh = Real(1.2);
    const Real MaxStretch = Real(0.1);
    const Real hMin = m_t <= 1 ? SignificantReal : SignificantReal*m_t;
    const Real hStretch = MaxStretch*m_hNext;

    // Figure out the target ending time for the next step. Choosing time
    // rather than step size lets us end at exactly tStop.
    Real t1 = m_t + m_hNext; // this is the usual case
    // If a small stretching of the next step would get us to tStop, try 
    // to make it all the way in one step.
    if (t1 + hStretch > tStop) 
        t1 = tStop;

    Real h, errNorm;
    Vec<N> y1, y1err;

    // Try smaller and smaller step sizes if necessary until we get one
    // that satisfies the error requirement, and for which projection
    // succeeds.
    while (true) {
        ++m_nStepsAttempted;
        h = t1 - m_t; assert(h>0);
        takeRKMStep(h, y1, y1err);
        errNorm = calcNormInf(y1err);
        if (errNorm > m_accuracy) {
            ++m_nErrtestFailures;
            // Failed to achieve required accuracy at this step size h.
            SimTK_ERRCHK4_ALWAYS(h > hMin,
                "GeodesicIntegrator::takeOneStep()", 
                "Accuracy %g worse than required %g at t=%g with step size"
                " h=%g; can't go any smaller.", errNorm, m_accuracy, m_t, h);

            // Shrink step by (acc/err)^(1/4) for 4th order.
            Real hNew = Safety * h * std::sqrt(std::sqrt(m_accuracy/errNorm));
            hNew = clamp(MinShrink*h, hNew, HysteresisLow*h);
            t1 = m_t + hNew;
            continue;
        }
        // Accuracy achieved. Can we satisfy the constraints?
        if (m_eqn.projectIfNeeded(m_consTol, t1, y1))
            break; // all good

        // Constraint projection failed. Hopefully that's because the
        // step was too big.
        ++m_nProjectionFailures;

        SimTK_ERRCHK3_ALWAYS(h > hMin,
            "GeodesicIntegrator::takeOneStep()", 
            "Projection failed to reach constraint tolerance %g at t=%g "
            "with step size h=%g; can't shrink step further.", 
            m_consTol, m_t, h);

        const Real hNew = MinShrink*h;
        t1 = m_t + hNew;
    }

    // We achieved desired accuracy at step size h, and satisfied the
    // constraints. (t1,y1) is now the final time and state; calculate 
    // state derivatives which will be used at the start of the next step.
    ++m_nStepsTaken;
    if (m_nStepsTaken==1) m_hInit = h; // that was the initial step
    m_t = t1; m_y = y1; m_hLast = h;
    m_eqn.calcDerivs(m_t, m_y, m_ydot);

    // If the step we just took ended right at tStop, don't use it to
    // predict a new step size; instead we'll just use the same hNext we
    // would have used here if it weren't for tStop.
    if (t1 < tStop) {
        // Possibly grow step for next time.
        Real hNew = errNorm == 0 ? MaxGrow*h
            :  Safety * h * std::sqrt(std::sqrt(m_accuracy/errNorm));
        if (hNew < HysteresisHigh*h) hNew = h; // don't bother
        hNew = std::min(hNew, MaxGrow*h);
        m_hNext = hNew;
    }
}

template <class Eqn> void
GeodesicIntegrator<Eqn>::takeRKMStep(Real h, Vec<N>& y1, Vec<N>& y1err) const {
    const Real h2=h/2, h3=h/3, h6=h/6, h8=h/8;
    const Real t0=m_t, t1=m_t+h;
    const Vec<N>& y0 = m_y;
    const Vec<N>& f0 = m_ydot;
    Vec<N> f1, f2, f3, f4, ysave;
    m_eqn.calcDerivs(t0+h3, y0 + h3* f0,         f1);
    m_eqn.calcDerivs(t0+h3, y0 + h6*(f0 + f1),   f2);
    m_eqn.calcDerivs(t0+h2, y0 + h8*(f0 + 3*f2), f3);

    // We'll need this for error estimation.
    ysave = y0 + h2*(f0 - 3*f2 + 4*f3);
    m_eqn.calcDerivs(t1, ysave, f4);

    // Final value. This is the 4th order accurate estimate for 
    // y1=y(t0+h)+O(h^5): y1 = y0 + (h/6)*(f0 + 4 f3 + f4). 
    // Don't evaluate here yet because we might reject this step or we
    // might need to do a projection.
    y1 = y0 + h6*(f0 + 4*f3 + f4);

    // This is an embedded 3rd-order estimate y1hat=y(t0+h)+O(h^4).
    //     y1hat = y0 + (h/10)*(f0 + 3 f2 + 4 f3 + 2 f4)
    // We don't actually have any need for y1hat, just its 4th-order
    // error estimate y1hat-y1=(1/5)(y1-ysave) (easily verified from the above).

    for (int i=0; i<N; ++i)
        y1err[i] = std::abs(y1[i]-ysave[i]) / 5;
}

} // namespace SimTK

#endif // SimTK_SIMMATH_GEODESIC_INTEGRATOR_H_