Qinghai Zhang 1 An Introduction to Software Engineering for Mathematicians Motivating Questions 2014-SEP-30 explicit (IMEX) scheme has the following steps: for s = 1, 2, · · · , ns , ns ns X X [I] [E] as,j L φ(j) , t(j) , φ(s) = φn + k as,j f t(j) + k Q1. What is math? Q2. Why do we care about math? j=1 j=1 Q3. What is software and software engineering? φn+1 = φn + k Q4. Why do we care about software engineering? ns X s=1 (s) +k b[E] s f t ns X (6a) (s) (s) , b[I] ,t s L φ s=1 (6b) 2 An Illustrating Example where the superscript (s) denotes an intermediate stage, t(s) = tn + cs k the time of that stage, ns the number of stages, and A, b, c the standard coefficients of the Butcher tableau. Consider numerically solving the diffusion equation ∂φ = f (x, t) + ν∇2 φ, ∂t (1) Software Quality on a rectangular domain Ω ⊂ RD , where u is a given 3 velocity field and f a given forcing term. The initial condition is φ(x, t) = φ0 (x) and the boundary condition is S1. Correctness: perform the tasks as defined by their specifications; Dirichlet, Neumann, Robin, or a mixed one of the above. The problem domain Ω is discretized into square control S2. Ease of use volumes, each of which denoted by a multi-index i ∈ ZD ; the region of cell i is represented by S3. Reusability: serve for the construction of many different applications; Ci := xO + ih, xO + (i + 1) h , (2) S4. Extendibility: easily adapt to changes of specifications; where xO is some fixed origin of the coordinates, h the uniform grid size, and 1 ∈ ZD the multi-index with all its components equal to one. The averaged φ over cell i S5. Robustness: react appropriately to abnormal conditions; is denoted by Z S6. Efficiency: place as few demands as possible on hard1 ware resources to achieve a certain metric; φ (x) dx. (3) hφii := D h Ci S7. Compatibility: easily to be combined with others; A fourth-order finite-volume discretization of the 2 S8. Portability: easily to be transferred to various hardLaplacian is L hφii = ∇ φ C + O(h4 ) with i ware and software environments; 1 X L hφii := − hφii+2ed + 16 hφii+ed (4) 12h2 d 4 An Example of OOP − 30 hφii + 16 hφii−ed − hφii−2ed , Below is some pseudo-code illustrating the paradigm of object-oriented programming (OOP). where ed ∈ ZD is a multi-index with 1 as its d-th component and 0 otherwise. Integrating (1) and applying (3) LevelData data; and (4) yield a system of ODEs that approximates (1) data.define(xo, nCells, h); ScalarFunction initFunc; within O(h4 ): initFunc.setData(data); ScalarFunction bcFunc; d hφi = L hφi + hf (t)i , (5) bcFunc.define(...); dt BoundaryCondition bc; for which we can use the “method of lines” to advance bc.define("Dirichlet", bcFunc); the solution for each time step. For example, an implicit- DiscreteLaplacian<4> lapl; 1 Qinghai Zhang An Introduction to Software Engineering for Mathematicians 5 lapl.define(xo, nCells, h); ScalarFunction forcingFunc; forcingFunc.define(...); TimeIntegrator ti; ti.define("ERK-ESDIRK", lapl, forcingFunc); for (int i=0; i<nTimeSteps; i++) { Real t = t0 + i*dt; ti.timeStep(data, t); } ti.report(data); 2014-SEP-30 Some OOP principles 5.1 Abstraction 5.2 Encapsulation 5.3 Inheritance: reusing the interface 5.4 Polymorphism: jects 6 interchanging ob- Testing Some real C++ code are also included at the end of T1. unit tests; this document. T2. acceptance test. • TimeIntegrator.H: interface class for time integrators. 7 Debugging • ExplicitRungeKutta.H: an concrete class capturD1. a NP-complete problem; ing explicit Runge-Kutta methods. D2. use patterns to avoid cross-level bugs; • ExplicitRungeKutta.cpp: the definition of various explicit Runge-Kutta methods. D3. search by bisections. 2 File: /home/tsinghai/Dao/src/TimeIntegrators/TimeIntegrator.H 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 #ifndef _TIMEINTEGRATOR_H_ #define _TIMEINTEGRATOR_H_ /** * Abstract base interface for time integrators. * F is either FArrayBox or FluxBox */ template<class F> class TimeIntegrator { public: virtual ~TimeIntegrator(){} virtual void updateTimeStepSize(const Real& dt) = 0; /// /// Advance one time step for the unknown. /// virtual void timeStep(Vector<LevelData<F>*>& U, Real time) = 0; }; #endif Page 1 of 1 File: /home/tsinghai/Dao/src/TimeIntegrators/ExplicitRungeKutta.H 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 #ifndef _EXPLICITRUNGEKUTTA_H_ #define _EXPLICITRUNGEKUTTA_H_ #include "BoundaryConditionFactory.H" #include "HierarchyDataOps.H" /// The list of implemented ERK methods. enum ERK_Type { ForwardEuler=1, ClassicRK4, nERK_Type }; /// The compile time constants of orders-of-accuracy template<ERK_Type Type> struct ERK_Order; template<> struct ERK_Order<ForwardEuler> { enum {val=1}; }; template<> struct ERK_Order<ClassicRK4> { enum {val=4}; }; /// The compile time constants of numbers of stages template<ERK_Type Type> struct ERK_NumStages; template<> struct ERK_NumStages<ForwardEuler> { enum {val=1}; }; template<> struct ERK_NumStages<ClassicRK4> { enum {val=4}; }; /// The Butcher Tableau of ERK methods template <ERK_Type Type> struct ERK_ButcherTableau { static const int nS = ERK_NumStages<Type>::val; // The first nStages numbers are the nominators // while the last one their common denominator. static const int a[nS][nS+1]; static const int b[nS+1]; static const int c[nS+1]; }; /// default ERK methods for different orders-of-accuracy. template<unsigned int Order> struct ERK_DefaultMethods; template<> struct ERK_DefaultMethods<1> { enum {val=ForwardEuler}; }; template<> struct ERK_DefaultMethods<4> { enum {val=ClassicRK4}; }; Page 1 of 3 File: /home/tsinghai/Dao/src/TimeIntegrators/ExplicitRungeKutta.H 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 /** * This class encapsulates timeStepping algorithms * of an ERK (explicit Runge-Kutta) method. * To implement a new ERK method, * all one needs to do is to * (1) add a new key to the enumerations in ERK_Type; * (2) set its order with template specialization of ERK_Order; * (3) set its # of stages with template specialization of ERK_NumStages; * (4) specify its Butcher tableau in ExplicitRungeKutta.cpp. * In other words, the following class stays the same * if the ERK method can be expressed with a single Butcher tableau. */ template<ERK_Type Type, /// The type of the ERK method class F, /// FArrayBox or FluxBox? class EOS> /// Equation Op strategies class ExplicitRungeKutta { public: /// type acronyms typedef BoundaryConditionBase typedef RefCountedPtr<BCB> typedef ERK_ButcherTableau<Type> typedef HierarchyDataOps typedef Vector<LevelData<F>*> typedef Vector<Vector<LevelData<F>*> > BCB; BCP; ERC; HOP; VLF; VVL; static const int nStages = ERC::nS; /// gamma is never used, just for interface uniformality static const int gamma = 1; void define(const Real& dx, const Vector<int>& refV, const BCP phiBC) { m_dx = dx; m_refV = refV; m_phiBC = phiBC; // initialize the Butcher arrays from ERC a.resize(nStages); for (int i=0; i<a.size(); i++) a[i].resize(nStages); b.resize(nStages); c.resize(nStages); for (int i=0; i<a.size(); i++) { for (int j=0; j<a[i].size(); j++) a[i][j] = static_cast<Real>(ERC::a[i][j])/ERC::a[i][ERC::nS]; b[i] = static_cast<Real>(ERC::b[i])/ERC::b[ERC::nS]; c[i] = static_cast<Real>(ERC::c[i])/ERC::c[ERC::nS]; } } /// /// Advance one time step for the unknown. /// The container imp here is intended for interface uniformity /// and never used. /// void timeStep(const Real& time, const Real& dt, VLF& u, VVL& phi, VVL& exp, VVL& imp, EOS& eos) { for (int s=0; s<nStages; s++) HOP::assign(phi[s], u); // Loop over all the stages for (int ns=0; ns<nStages; ns++) { pout() << " ERK stage: " << ns+1 << endl; for (int j=0; j<ns; j++) { HOP::incr(phi[ns], exp[j], a[ns][j]*dt); } Page 2 of 3 File: /home/tsinghai/Dao/src/TimeIntegrators/ExplicitRungeKutta.H 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 // The current intermediate time const Real ts = time + c[ns]*dt; m_phiBC->setTime(ts); eos.computeOperators(phi[ns], exp[ns], imp[ns], ts); // add results of implicit operators to explicit results // since we are using explicit Runge-Kutta methods. HOP::incr(exp[ns], imp[ns], 1.0); eos.stageStepPostProcessing(phi[ns], ts); } // calculate the results of the final stage HOP::assign(u, phi[0]); for (int s=0; s<nStages; s++) HOP::incr(u, exp[s], b[s]*dt); HOP::checkData(*u[0]); // set BC of u for other operators like vorticity. m_phiBC->fillGhostCells(u, m_dx, m_refV, false); // false: homogeneous } private: // The Butcher arrays from ERC Vector<Vector<Real> > a; Vector<Real> b; Vector<Real> c; BCP m_phiBC; Real m_dx; Vector<int> m_refV; }; #endif Page 3 of 3 File: /home/tsinghai/Dao/src/TimeIn…grators/ExplicitRungeKutta.cpp 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 #include "ExplicitRungeKutta.H" ///-----------------------------------------------/// Butcher tableau of the forward Euler method ///-----------------------------------------------template<> const int ERK_ButcherTableau<ForwardEuler>::a[][2] = { {0, 1} }; template<> const int ERK_ButcherTableau<ForwardEuler>::b[] = { 1, 1 }; template<> const int ERK_ButcherTableau<ForwardEuler>::c[] = { 0, 1 }; ///-------------------------------------------------------/// Butcher tableau of the fourth-order Runge-Kutta method ///-------------------------------------------------------template<> const int ERK_ButcherTableau<ClassicRK4>::a[][5] = { {0, 0, 0, 0, 1}, {1, 0, 0, 0, 2}, {0, 1, 0, 0, 2}, {0, 0, 1, 0, 1} }; template<> const int ERK_ButcherTableau<ClassicRK4>::b[] = { 1, 2, 2, 1, 6 }; template<> const int ERK_ButcherTableau<ClassicRK4>::c[] = { 0, 1, 1, 2, 2 }; Page 1 of 1