Class Guides
Most classes in Yggdrasil are purely virtual. This allows for the interaction between class species (like physics classes and integrators) to be abstracted and predictable.
Physics Classes
The primary class that you’ll interact with in Yggdrasil is the Physics class. This class contains the prescription
for the State vector and all of the necessary logic for computing derivatives of the State vector. The general concept
for how physics classes work in Yggdrasil is that the NodeList holds immutable quantities that are valid for the beginning
of an integration step, while the State vector (usually a subset copy of some of the Fields on the NodeList) may update
throughout a step.
Yggdrasil’s integrators expect assigned (derived) physics classes to override the physics base class method EvaluateDerivatives
at least, and some may also override PrestepInitialize and FinalizeStep as well.
Let’s use constantGravity.cc in the src/Physics directory as an example of a derived class that overrides only the derivatives.
4template <int dim>
5class ConstantGravity : public Physics<dim> {
6protected:
7 Lin::Vector<dim> gravityVector;
8 double dtmin;
9public:
10 using Vector = Lin::Vector<dim>;
11 using VectorField = Field<Vector>;
12 using ScalarField = Field<double>;
13
14 ConstantGravity(NodeList* nodeList, PhysicalConstants& constants, Vector& gravityVector) :
15 Physics<dim>(nodeList,constants),
16 gravityVector(gravityVector) {
17
18 int numNodes = nodeList->size();
19 this->template EnrollFields<Vector>({"acceleration", "velocity", "position"});
20
21 for (int i=0; i<numNodes; ++i)
22 nodeList->getField<Vector>("acceleration")->setValue(i,gravityVector);
23
24 this->template EnrollStateFields<Vector>({"velocity", "position"});
25 }
26
27 ~ConstantGravity() {}
In the constructor for ConstantGravity, we pass in the NodeList pointer, a PhysicalConstants object, and a Vector that
prescribes the direction and magnitude of the acceleration. Then we enroll the acceleration, velocity, and position Vector
Fields using the type templated EnrollFields method from the Physics base class.
This ensures that these fields exist on the NodeList, and if they don’t, this
method will create them.
Next we enroll the position and velocity Fields to the State.
In the case of
the constant gravity package, the only derivatives are the position derivative and velocity derivative, and as a standard, we keep
Fields whose derivatives we want to iterate throughout an integration step
on the State object. For that reason, we do not assign acceleration to the State
object for this physics class since it never changes.
36 virtual void
37 EvaluateDerivatives(const State<dim>* initialState, State<dim>& deriv, const double time, const double dt) override {
38 NodeList* nodeList = this->nodeList;
39 PhysicalConstants constants = this->constants;
40 int numNodes = nodeList->size();
41
42 VectorField* position = initialState->template getField<Vector>("position");
43 VectorField* acceleration = nodeList->getField<Vector>("acceleration");
44 // since the acceleration is literally constant, we keep it on the nodeList
45 VectorField* velocity = initialState->template getField<Vector>("velocity");
46
47 VectorField* dxdt = deriv.template getField<Vector>("position");
48 VectorField* dvdt = deriv.template getField<Vector>("velocity");
49
50 dxdt->copyValues(velocity);
51 dxdt->operator+(*acceleration*dt);
52 dvdt->copyValues(acceleration);
53
54 double local_dtmin = 1e30;
55
56 #pragma omp parallel for reduction(min:local_dtmin)
57 for (int i=0; i<numNodes ; ++i) {
58 double amag = acceleration->getValue(i).mag2();
59 double vmag = velocity->getValue(i).mag2();
60 local_dtmin = std::min(local_dtmin,vmag/amag);
61 }
62 dtmin = local_dtmin;
63 this->lastDt = dt;
64 }
EvaluateDerivatives is used to compute the derivatives of the State object at each node. In this case, we are computing the
change in velocity and position due to a constant acceleration, so we have a pair of ODEs:
The integrator invokes this method with intial and derivatives State vectors and a time step, and the physics class stores the value of those
derivatives back to the deriv State object.
Note
The integrator will call this method multiple times per time step if the particular integrator in question is of high temporal order.
Thus, the value of dt here may be a partial step and the initial State object may change within a time step,
but in the case of a simple, forward Euler integration (\(\mathcal{O}(\Delta t)\)), dt=0 means that we are
computing the derivatives using State vector quantities evaluated at the current time.
The final bit of code in EvaluateDerivatives merely calculates the minimum timestep based on a velocity condition.
Generally, the base class method for FinalizeStep should suffice for most physics
packages as the base class takes care of copying the final State after an integration step back to the NodeList
on the derived physics class.
Note
If you don’t want to recapitulate what is already in the base class FinalizeStep method, but you’d still like to
tie up some loose ends at the end of an integration step (e.g. you may want to set floor limits on certain
quantities), you can do so by overriding the FinalChecks() method.
By dafault, this method doesn’t do anything, but is always called after FinalizeStep.
Integrator Classes
Nihoggr’s integrators follow a basic pattern of initializing the state of the physics objects at each step, evaluating derivatives, advancing the state of each physics object from those derivatives, applying boundary conditions, and then finalizing each physics object’s state. An example of this pattern for simple forward Euler integration is shown below:
template <int dim>
Integrator<dim>::Integrator(std::vector<Physics<dim>*> packages, double dtmin, bool verbose)
: packages(packages), dt(dtmin), dtmin(dtmin), cycle(0), time(0.0), verbose(verbose) {}
template <int dim>
Integrator<dim>::~Integrator() {}
template <int dim>
void Integrator<dim>::Step() {
if (cycle == 0) {
for (Physics<dim>* physics : packages)
physics->ZeroTimeInitialize();
}
for (Physics<dim>* physics : packages)
{
physics->UpdateState();
physics->PreStepInitialize();
State<dim> finalState = Integrate(physics);
physics->ApplyBoundaries(&finalState);
physics->FinalizeStep(&finalState);
}
time += dt;
cycle += 1;
VoteDt();
}
template <int dim>
State<dim>
Integrator<dim>::Integrate(Physics<dim>* physics) {
const State<dim>* state = physics->getState();
State<dim> derivatives(state->size());
derivatives.ghost(state);
physics->EvaluateDerivatives(state, derivatives, time, 0);
derivatives *= dt;
State<dim> newState(state->size());
newState = state->deepCopy();
newState += derivatives;
return newState;
}
template <int dim>
void Integrator<dim>::VoteDt() {
double smallestDt = 1e30;
for (Physics<dim>* physics : packages) {
double newdt = physics->EstimateTimestep();
if (newdt < smallestDt) {
smallestDt = newdt;
if (verbose)
std::cout << physics->name() << " requested timestep of " << newdt << "\n";
}
}
dt = (dt < smallestDt ? dt + 0.2 * (smallestDt - dt) : smallestDt);
this->dt = std::max(dt, this->dtmin) * this->dtMultiplier;
}
Note
While this class is a foward Euler integrator, it is also the base class for all integrators in Yggdrasil, and as such,
its Step() method is rarely overridden by derived classes. Instead, the most common override will be to the Integrate()
method.
Equations of State
Equations of state are possibly the simplest classes in Yggdrasil. They consume Field objects (or referenced doubles)
and set the values of other Fields according to their respective closure equations. Most of the logic for how they
work is self-described by the base class interface file equationOfState.hh.
9// Base class for Equation of State
10class EquationOfState {
11public:
12 EquationOfState(PhysicalConstants& constants) {}
13
14 virtual ~EquationOfState() {}
15
16 // Field-based methods
17
18 virtual void
19 setPressure(Field<double>* pressure, Field<double>* density, Field<double>* internalEnergy) const = 0;
20
21 virtual void
22 setInternalEnergy(Field<double>* internalEnergy, Field<double>* density, Field<double>* pressure) const = 0;
23
24 virtual void
25 setSoundSpeed(Field<double>* soundSpeed, Field<double>* density, Field<double>* internalEnergy) const = 0;
26
27 virtual void
28 setTemperature(Field<double>* temperature, Field<double>* density, Field<double>* internalEnergy) const = 0;
29
30 virtual void
31 setInternalEnergyFromTemperature(Field<double>* internalEnergy, Field<double>* density, Field<double>* temperature) const = 0;
32
33 // Scalar-based methods
34
35 virtual void
36 setPressure(double* pressure, double* density, double* internalEnergy) const = 0;
37
38 virtual void
39 setInternalEnergy(double* internalEnergy, double* density, double* pressure) const = 0;
40
41 virtual void
42 setSoundSpeed(double* soundSpeed, double* density,double* internalEnergy) const = 0;
43
44 virtual void
45 setTemperature(double* temperature, double* density, double* internalEnergy) const = 0;
46
47 virtual void
48 setInternalEnergyFromTemperature(double* internalEnergy, double* density, double* temperature) const = 0;
49
50 virtual std::string
Note
Equations of state are not typically templated in Yggdrasil since they act only on scalar Fields.