What are best practices to write a new adapter?

Alex started a good discussion on the mailing list (May 3, 2019) about best practices when writing a new adapter:

I wanted to ask if there are some additional guidelines on how to write good adapters for preCICE. I have the feeling that I might think too much about it, but I wanted to see what you guys are thinking about that.

Adapter advice

I am in the process of writing some adapter for the projects here. It is mainly for coupling DuMuX [1]. DuMuX itself is not a big solver that you compile once and then use it via configuration files (as OpenFOAM for example), but more a toolbox of functions (similar to FEniCS) where you can easily set up a solver tailored to your problem. One usually defines a main.cc that drives the solving process (time loop etc) and problem.hh which described the problem in detail (equations used, boundary conditions etc.).

Currently, I have two use cases:

  1. Bidirectional coupling: Coupling DuMuX with DuMuX. Exchange temperature and heat-flux. The problem is very similar to the CHT test case from the tutorials.
  2. Unidirectional “coupling”: Coupling DuMuX with some other solver SolverB that receives data from the DuMuX solver. No data is transferred back to DuMuX. It is not clear what data should be transferred within that project. I want to be able to transfer several quantities that might live on the same mesh, but could also live on several different meshes.

At the moment, I have several adapters for my two use cases that I would like to merge.

Case 1:

  1. For the first use case I wrote an adapter that has function calls tailored to the use case like writeHeatFlux and readTemperature and it has some convenience function for users like getTemperatureOnFace( DumuxFaceId ). There is no configuration file for the adapter since the use case is fixed at the moment. I do not see a clear benefit from adding a configuration file.

Case 2:

2.1 The Dumux adapter here is more general. It has rather general functions like announceQuantity(name) and getQuantityOnFace(name) as the transferred quantities might vary/change a lot. This adapter has no configuration file either. We have to touch the main.cc for DuMuX anyway to set up the test case and the interface mesh so I do not see the clear benefit of having an additional configuration file.

2.2 For SolverB I have a very simple adapter that is tailored to the current use case where a scalar quantity is used. The solver already uses a JSON configuration file. The Solver is rather general and should read an arbitrary number of dataset from preCICE on an arbitraty number of meshes. Therefore, I see a clear benefit for having a configuration file. I would prefer JSON as file format since a parser is part of the solver.

Question

How would you merge something like that into one code base?

A possible approach would be to write a very general base adapter and derive a class from it that would be tailored to the actual solver and use case. However, that means I might end up with plenty of very special classes.

General questions/remarks regarding adapters

  • What are indications of a good adapter?
  • How specific should functions be?
    • ReadScalarQuantity(quantityId) vs ReadTemperature()
  • What is the preferred format for the adapter configuration? There seems to be no clear rule what file format to use. preCICE uses XML, but the adapters usually bring additional dependencies (even though a XML parser must be present due to preCICE).
    • preCICE -> XML
    • OpenFOAM -> YAML (additional dependency)
    • Calculix -> YAML (additional dependency)
    • FEniCS -> JSON
    • SU2 -> Uses SU2-configuration file?!
    • Why do the C/C++ adapters not use XML as it is a dependency for preCICE already?
  • Can I use the preCICE logging features for my adapter do I have to set up my own logging?
  • I usually try to keep the number of dependencies low so it feels like a natural thing to reuse libraries that I need for preCICE anyway (boost and XML). Why is this not done in the adapters? Is XML or the parser too complicated/annoying?

The adapter example on the homepage [2] does not answer all the questions. I also checked the available adapters, but they follow different approaches due to different authors, programming languages and solvers they are written for.

TL;DR (short version)

  • What functionality should a wrapper have to be called “adapter”? Even with an adapter I might have to change my source code, but in a more convenient way tailored to my solver. How much easier does it have to be to count as an “adapter”.
  • Are there any common agreements on nomenclature, minimum required feature set etc?
  • How verbose should functions of that wrapper be? If my adapter reads
    temperatures, should the adapter provide a function called readTemperature or rather readQuantity( "temperature" ). I have the impression that more descriptive function names make a configuration file less necessary and easier for users to understand that have to add it to their code. At the same time it restricts the use cases I can use my adapter for.
  • When do you think a configuration file for an adapter is needed?

Makis answered:

I really like the concept of announceQuantity(name) , but this could be complicated to make it general, at least for setting values for the right objects. In the OpenFOAM adapter we have all the generic functionality into abstract classes, which we then specialize for each type of boundary field. Therefore, the TemperatureBoundary.announceQuantity("MyTemperature") would be specific to Temperature.

Maybe, since DuMuX is a toolkit, your adapter should also be a toolkit: a set of convenience functions to extract boundary values, set the time step size etc, which could then be easily used in any DuMuX-based solver. With the term “adapter” we usually mean any adapted solver (and ideally a solver plug-in), but this would certainly qualify as a good adapter [toolkit]. The closer the architecture you use (i.e. access through an interface), the better.

Regarding configuration files, I can mainly tell you that there are historical reasons and we are not exactly happy with the current situation either. Here are a few hints:

  • XML was always in preCICE, as it is a very descriptive language. Annoying to edit and our format certainly not perfect, but changing this would be a major change in preCICE, for which we don’t have any argument strong enough at the moment. The libxml2 was added as a dependency later, to avoid maintaining our own parser.
  • YAML came when we started having user-ready adapters and we had imagined a uniform way of configuring them. It is a much nicer language to edit, but not yet as standard. It is also foreign to all solver users.
  • We now try to just stick to the solvers’ file formats whenever possible. This way, we reduce the dependencies and do not require to learn another language. However, some solvers do not have a (preferable) file format (e.g. FEniCS), so there we have to be creative.

To your specific questions:

  • What are indications of a good adapter?

Covering all the features of preCICE and making it easy to adapt for a different scenario/physics. A plug-in is ideal, but not a must. Tight integration into the code that makes it difficult to update to newer solver/preCICE versions should be avoided. We want to write some guidelines, but until then, here is an interesting issue (and flowchart).

  • How specific should functions be?
  - `ReadScalarQuantity`(quantityId) vs `ReadTemperature() 

I would aim for ReadScalarQuantity , at least in the background, but this depends on your solver framework style.

  • What is the preferred format for the adapter configuration? There seems to be no clear rule what file format to use. preCICE uses XML, but the adapters usually bring additional dependencies (even though a XML parser must be present due to preCICE).

Whatever is the preferred choice for the solver. If none, choose something standard that does not require dependencies. I am not a fan of JSON for configuration files, but it’s definitely a good choice.

  • Can I use the preCICE logging features for my adapter do I have to set up my own logging

There is no such functionality in the API. Why would you need that? You could directly use Boost.log in your own solver if you need a library, which is also required by preCICE.

  • I usually try to keep the number of dependencies low so it feels like a natural thing to reuse libraries that I need for preCICE anyway (boost and XML). Why is this not done in the adapters? Is XML or the parser too complicated/annoying?

The adapters are essentially extensions of the solvers. They will be built with the same built system. Therefore, we try to stick with what the solver depends on. Other than that, see the history analysis above.

… and I added:

  • How to write a good adapter is a difficult question. There are obvious things such as supporting all preCICE features or allowing for easy extension to other coupling physics. But besides this, our current recommendation is to stick to the environment of the coupled solver as much as possible (building, configuration, code style, …).
  • Many of the official adapter have historical reasons why they are the way they are. Some are also only meant to be “examples to copy from” and not general adapters (dealii, MBDyn, FEniCS to some extend).
  • As Makis suggested, for DuMuX, I would also recommend to make the adapter a toolkit. And I would recommend to start small and improve incrementally. Don’t design the general adapter from scratch. At the moment, I am working with Gertjan van Zwieten on a very similar issue with Nutils. Nutils is also a toolkit comparable to FEniCS. We started with a first example. And then we did another example (so just as you started). Next step will be now to collect things we do often within helper functions, but which are still located in the example scripts. Once, we are sure about the helper functions, we want to either open a separate repository for a Nutils adapter toolkit or directly merge the helper functions to the main Nutils repository. This brings me to my last point.
  • In the long run, there are only two good solutions for an adapter: Either a plugin functionality, such as we use for the OpenFOAM adapter (or for Fluent). Then, the adapter can have its own building environment, its own versions, … and can be maintained separately. Here, I highly recommend the master thesis from Makis (we are currently working on extending the thesis to a paper). The other option is to integrate the adapter within the solver. Of course, this needs a tight collaboration with the developers of the solver and also some long-term commitment. This would also be the best solution for SU2 and CalculiX, in my opinion. For SU2, there have already been efforts in this direction.