Architecture
What we develop in the step-by-step guide is best described as an adapted code, not an adapter. We directly modified the main solver routines, which means that now the coupling-related code is tightly integrated with the solver code. If you are the maintainer of the solver code, you know best what architecture to follow. However, if you are developing an adapter for someone else’s solver, there are also ways to separate the adapter code from the solver code, making the adapter easier to maintain and distribute/publish and hence more useful to the community.
In this section, we look into developing an adapter as a class in the solver code, as a wrapper that calls the solver and preCICE APIs, or as a solver plugin:
This categorization is not always clear, as it often depends on our perspective: which is the executable application being coupled? A ready-to-use simulation program, a code using tools from a toolkit, or an external script delegating execution? This section is mainly meant to provide ideas and examples on how to better structure such an adapter code.
Adapter class
Encapsulating the adapter into a class separates the preCICE calls from the rest of the solver code. This is very similar to directly modifying the solver code (with some modifications still needed), but the solver calls a few high-level adapter functions, instead of directly calling the preCICE API. Such encapsulation avoids duplicating additional checks and details and makes maintenance easier.
This method is particularly useful for adapters used in frameworks that the user uses to create concrete simulations. Examples here include the FEniCS adapter and the deal.II adapter.
Even when directly modifying the code, it makes sense to hide as many changes as possible behind a class API. Examples for this include the CalculiX adapter and the SU2 adapter for v6 and earlier (as described in Alexander Rusch’s thesis).
Adapter classes can still be provided as optional modules of the main codebase. Examples include the deal.II adapter, the G+smo adapter, the DUNE adapter, and the DuMux adapter
Adapter plugin
Some solvers provide a plugin interface. Examples include OpenFOAM (function objects) and Fluent (UDF files). The solver exposes specific “hooks”/”callbacks”, which trigger the execution of external code at specific stages of the simulation. Creating an adapter as such a plugin is non-intrusive and makes it easy to distribute the adapter independently.
While this is a generally preferable architecture, it comes with some requirements. The solver needs to provide the following functionality via the plugin interface:
- Executing additional steps during the configuration, setup, and teardown
- Access to the (boundary) mesh coordinates (read)
- Access to the (boundary) values (read/write)
- Access to the time step size (read/write)
- Controlling the exit condition of the time loop
- Checkpointing (if implicit coupling is required):
- Storing and restoring the time
- Storing and restoring the internal solution
The trickiest part is typically the checkpointing. However, in some cases, this can alternatively be implemented as (a) controlling when the solver moves to the next time step or discards the current solution and tries again, (b) storing restart files and restarting from them (inefficient, but might not be an issue in some cases).
Examples of adapters implemented as plugins include the OpenFOAM adapter (with the plugin approach explained in detail in Gerasimos Chourdakis’ thesis) and the Fluent adapter.
Adapter wrapper
Some solvers provide an API, meaning that specific functions of the solver can be executed from external programs. A common example is a Python API, which allows us to write a Python wrapper for the solver. This wrapper then can call both the solver and preCICE, making the wrapper an adapter.
Similarly to the plugin approach, this approach is non-intrusive to the solver code and makes it easy to distribute the adapter independently.
Examples of adapters implemented as wrappers include the SU2 adapter and the code_aster adapter. A further example from the literature is the TAU adapter.
Even when the solver does not provide an API and cannot be modified (e.g., in case of proprietary codes), a wrapper that starts the executable of the solver with modified configuration might be a possible, last-resort solution. An example from the literature includes the CAMRAD-II adapter.
Configuration
Besides the preCICE configuration, every adapter also needs its own configuration. This includes information such as where the preCICE configuration file is located, which participant is this solver acting as, and which data is read and written from which part of the mesh. This configuration is typically sourced from a dedicated configuration file, which is read directly by the adapter. For example, for an FSI simulation coupling OpenFOAM and CalculiX, three configuration files are needed:
Configuration prototypes
While still prototyping, it might be convenient to avoid reading a configuration file. An easier approach might be hard-coding the configuration in the solver code. In case you are coupling two participants using the same solver, you could read only the participant name from the command line.
Examples of this approach are the minimal reference implementations (solver dummies), the 1D elastic tube tutorial codes, and the Nutils examples.
Configuration format
An adapter configuration file can be written in any configuration format. Choose a format that is easy for the user of the respective solver to understand and write, as well as easy for the adapter to parse. Common standard formats include YAML and JSON, for which we develop generation and validation tools. Both are used by existing adapters and have good tooling support. YAML is primarily designed for configuration files, it is more convenient to read and write, and supports comments. JSON is primarily designed for data interchange, focuses on interoperability, and is often the primary format that some tools use. In the future, we aim to have the adapter configuration files automatically generated by default.
Adapter configuration schema
We provide a standard preCICE adapter configuration schema to facilitate the interoperability between adapters. Expecting configuration in this general format has several benefits:
- allows you to generate and validate adapter configuration files using the provided preCICE tools (including the MetaConfigurator)
- allows users to more easily understand the structure of a simulation case
- allows the community to use existing adapter configuration files with your adapter, with minimal, if any, modifications
- is a requirement for publishing your adapter as a preCICE-conforming adapter
Best practices
As the preCICE ecosystem expands, we are designing standards and guidelines to make the community-contributed components interoperable. See the contribution guidelines for adapters and for application cases.