In previous steps, we only considered explicit coupling. We now move onto implicit coupling, so sub-iterating each time step multiple times until a convergence threshold is reached. This stabilzes strongly-coupled problems.

The main ingredient needed for implicit coupling is move backwards in time. For that, we need a flux capacitor. Just kidding :wink:. What we really need is that your solver can write and read iteration checkpoints. An iteration checkpoint should contain all the information necessary to reload a previous state of your solver. What exactly is needed depends solely on your solver. preCICE tells you when you need to write and read checkpoints. To this end, preCICE uses the following action interface:

bool isActionRequired(const std::string& action)
void markActionFulfilled(const std::string& action)
const std::string& constants::actionReadIterationCheckpoint()
const std::string& constants::actionWriteIterationCheckpoint()
  • isActionRequired inquires the necessity of a certain action. It takes a string argument to reference the action.
  • markActionFulfilled tells preCICE that the action is fulfilled. This is a simple safeguard. If a certain action is required and you did not mark it as fulfilled preCICE will complain.
  • The Methods in the precice::constants namespace return strings to reference specific actions. For implicit coupling, we need actionReadIterationCheckpoint and actionWriteIterationCheckpoint.

Let’s extend our example code to also handle implicit coupling.

turnOnSolver(); //e.g. setup and partition mesh

precice::SolverInterface precice("FluidSolver","precice-config.xml",rank,size); // constructor

const std::string& coric = precice::constants::actionReadIterationCheckpoint();
const std::string& cowic = precice::constants::actionWriteIterationCheckpoint();

int dim = precice.getDimension();
int meshID = precice.getMeshID("FluidMesh");
int vertexSize; // number of vertices at wet surface
// determine vertexSize
double* coords = new double[vertexSize*dim]; // coords of vertices at wet surface
// determine coordinates
int* vertexIDs = new int[vertexSize];
precice.setMeshVertices(meshID, vertexSize, coords, vertexIDs);
delete[] coords;

int displID = precice.getDataID("Displacements", meshID);
int forceID = precice.getDataID("Forces", meshID);
double* forces = new double[vertexSize*dim];
double* displacements = new double[vertexSize*dim];

double solverDt; // solver time step size
double preciceDt; // maximum precice time step size
double dt; // actual time step size
preciceDt = precice.initialize();
while (precice.isCouplingOngoing()){
  if(precice.isActionRequired(cowic)){
    saveOldState(); // save checkpoint
    precice.markActionFulfilled(cowic);
  }
  solverDt = beginTimeStep(); // e.g. compute adaptive dt
  dt = min(preciceDt, solverDt);
  precice.readBlockVectorData(displID, vertexSize, vertexIDs, displacements);
  setDisplacements(displacements);
  solveTimeStep(dt);
  computeForces(forces);
  precice.writeBlockVectorData(forceID, vertexSize, vertexIDs, forces);
  preciceDt = precice.advance(dt);
  if(precice.isActionRequired(coric)){ // time step not converged
    reloadOldState(); // set variables back to checkpoint
    precice.markActionFulfilled(coric);
  }
  else{ // time step converged
    endTimeStep(); // e.g. update variables, increment time
  }
}
precice.finalize(); // frees data structures and closes communication channels
delete[] vertexIDs, forces, displacements;
turnOffSolver();

The methods saveOldState and reloadOldState need to be provided by your solver. You wonder when writing and reading checkpoints is required? Well, that’s no black magic. In the first coupling iteration of each time window, preCICE tells you to write a checkpoint. In every iteration in which the coupling does not converge, preCICE tells you to read a checkpoint. This gets a bit more complicated if your solver subcycles (we learned this in Step 5), but preCICE still does the right thing. By the way, the actual convergence measure is computed in advance in case you wondered about that as well.

Of course, with the adapted code above, explicit coupling still works. You do not need to alter your code for that. In case of explicit coupling, both actions reading and writing iteration checkpoints always return false.

At this state, you can again test your adapted solver against a solver dummy. Make sure to adjust the config file for implicit coupling scheme:

[...]
<coupling-scheme:serial-implicit>
  <participants first="FluidSolver" second="SolidSolver" />
  <max-time-windows value="10" />
  <time-window-size value="1.0" />
  <max-iterations value="15" />
  <relative-convergence-measure limit="1e-3" data="Displacements" mesh="StructureMesh"/>
  <exchange data="Forces" mesh="StructureMesh" from="FluidSolver" to="SolidSolver" />
  <exchange data="Displacements" mesh="StructureMesh" from="SolidSolver" to="FluidSolver"/>
</coupling-scheme:serial-implicit>
[...]