18th OpenFOAM Workshop - Genoa, July 11-14, 2023

Getting started with
OpenFOAM-preCICE
for FSI simulations

Gerasimos Chourdakis, Technical University of Munich
chourdak@in.tum.de

Uncoupled simulation

Import preCICE


                        import numpy
                        
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        dt = 0.01
                        t = 0

                        while True:
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          t = t + dt 
                          if(t > 0.1):
                            break
                    

Import preCICE


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        dt = 0.01
                        t = 0

                        while True:
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          t = t + dt 
                          if(t > 0.1):
                            break
                    

Configure preCICE


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )

                        dt = 0.01
                        t = 0

                        while True:
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          t = t + dt 
                          if(t > 0.1):
                            break
                    

Define the coupling mesh


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )
                        
                        # Get the preCICE mesh id
                        mesh_name  = "Generator-Mesh"
                        mesh_id    = interface.get_mesh_id(mesh_name)
                        
                        # Define the coupling mesh
                        vertices   = [[1, y0] for y0 in y[:-1]]
                        vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
                        
                        dt = 0.01
                        t = 0

                        while True:
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          t = t + dt 
                          if(t > 0.1):
                            break
                    

Initialize and finalize preCICE


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )
                        
                        # Get the preCICE mesh id
                        mesh_name  = "Generator-Mesh"
                        mesh_id    = interface.get_mesh_id(mesh_name)
                        
                        # Define the coupling mesh
                        vertices   = [[1, y0] for y0 in y[:-1]]
                        vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
                        
                        interface.initialize()

                        dt = 0.01
                        t = 0

                        while True:
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          t = t + dt 
                          if(t > 0.1):
                            break
                        
                        interface.finalize()
                    

Advance the coupling


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )
                                             
                        # Get the preCICE mesh id
                        mesh_name  = "Generator-Mesh"
                        mesh_id    = interface.get_mesh_id(mesh_name)
                         
                        # Define the coupling mesh
                        vertices   = [[1, y0] for y0 in y[:-1]]
                        vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
                        
                        interface.initialize()

                        dt = 0.01
                        t = 0

                        while True:
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          
                          
                          t = t + dt 
                          if(t > 0.1):
                            break
                        
                        interface.finalize()
                    

Advance the coupling


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )
                        
                         # Get the preCICE mesh id
                         mesh_name  = "Generator-Mesh"
                         mesh_id    = interface.get_mesh_id(mesh_name)
                         
                         # Define the coupling mesh
                         vertices   = [[1, y0] for y0 in y[:-1]]
                         vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
                        
                        interface.initialize()

                        dt = 0.01
                        t = 0

                        while True:
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          interface.advance(dt)
                          
                          t = t + dt 
                          if(t > 0.1):
                            break
                        
                        interface.finalize()
                    

Advance the coupling


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )
                        
                        # Get the preCICE mesh id
                        mesh_name  = "Generator-Mesh"
                        mesh_id    = interface.get_mesh_id(mesh_name)
                        
                        # Define the coupling mesh
                        vertices   = [[1, y0] for y0 in y[:-1]]
                        vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
                        
                        interface.initialize()

                        dt = 0.01
                        t = 0

                        while interface.is_coupling_ongoing():
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          interface.advance(dt)
                          
                          t = t + dt 
                        
                        
                        
                        interface.finalize()
                    

Advance the coupling


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )
                        
                        # Get the preCICE mesh id
                        mesh_name  = "Generator-Mesh"
                        mesh_id    = interface.get_mesh_id(mesh_name)
                        
                        # Define the coupling mesh
                        vertices   = [[1, y0] for y0 in y[:-1]]
                        vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
                        
                        interface.initialize()

                        dt = 0.01
                        t = 0

                        while interface.is_coupling_ongoing():
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          precice_dt = interface.advance(dt)
                          
                          t = t + dt 
                        
                        
                        
                        interface.finalize()
                    

Advance the coupling


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )
                        
                        # Get the preCICE mesh id
                        mesh_name  = "Generator-Mesh"
                        mesh_id    = interface.get_mesh_id(mesh_name)
                        
                        # Define the coupling mesh
                        vertices   = [[1, y0] for y0 in y[:-1]]
                        vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
                        
                        precice_dt = interface.initialize()

                        dt = 0.01
                        t = 0

                        while interface.is_coupling_ongoing():
                          dt = np.minimum(dt, precice_dt)
                          
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          precice_dt = interface.advance(dt)
                          
                          t = t + dt 
                        
                        interface.finalize()
                    
Not there yet, but let's run it
(similar changes in propagator.py - try it at home)

Nothing happening?

Did we forget something?

Write & read data


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )
                        
                        # Get the preCICE mesh id
                        mesh_name  = "Generator-Mesh"
                        mesh_id    = interface.get_mesh_id(mesh_name)
                         
                        # Define the coupling mesh
                        vertices   = [[1, y0] for y0 in y[:-1]]
                        vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
                        



                        
                        precice_dt = interface.initialize()

                        dt = 0.01
                        t = 0

                        while interface.is_coupling_ongoing():
                          dt = np.minimum(dt, precice_dt)
                          
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          

                          precice_dt = interface.advance(dt)

                          
                          t = t + dt 
                        
                        interface.finalize()
                    

Write & read data


                        import numpy
                        import precice
                        
                        # generate mesh
                        n = 20
                        y = numpy.linspace(0, 1, n + 1)

                        # preCICE setup
                        participant_name     = "Generator"
                        config_file_name     = "../precice-config.xml"
                        solver_process_index = 0
                        solver_process_size  = 1
                        interface = 
                            precice.Interface(  
                                               participant_name,
                                               config_file_name,
                                               solver_process_index,
                                               solver_process_size
                                             )
                        
                        # Get the preCICE mesh id
                        mesh_name  = "Generator-Mesh"
                        mesh_id    = interface.get_mesh_id(mesh_name)
                         
                        # Define the coupling mesh
                        vertices   = [[1, y0] for y0 in y[:-1]]
                        vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
                        
                        # Get the exchanged data id
                        data_name = "Data"
                        data_id = interface.get_data_id(data_name, mesh_id)
                        
                        precice_dt = interface.initialize()

                        dt = 0.01
                        t = 0

                        while interface.is_coupling_ongoing():
                          dt = np.minimum(dt, precice_dt)
                          
                          print("Generating data")
                          u = 1 - 2 * numpy.random.rand(n)
                          
                          interface.write_block_scalar_data(data_id, vertex_ids, u)
                          precice_dt = interface.advance(dt)
                          # interface.read_block_scalar_data(data_id, vertex_ids, u)
                          
                          t = t + dt 
                        
                        interface.finalize()
                    
It should work now!

Data is being transfered!

Most basic case: uni-directional, serial-explicit, nearest-neighbor mapping, ...

preCICE configuration

Visual representation of the precice-config.xml Visual representation of precice-config.xml using the config visualizer.

Configure OpenFOAM


                // fluid-openfoam/0/U
                    
                flap
                {
                    type            movingWallVelocity;
                    value           uniform (0 0 0);
                }
                

                // fluid-openfoam/0/pointDisplacement
                    
                flap
                {
                    type            fixedValue;
                    value           $internalField;
                }
                

                // fluid-openfoam/constant/dynamicMeshDict
                    
                solver      displacementLaplacian;
                

Load the OpenFOAM adapter


                // fluid-openfoam/system/controlDict
                functions
                {
                    preCICE_Adapter
                    {
                        type preciceAdapterFunctionObject;
                        libs ("libpreciceAdapterFunctionObject.so");
                    }
                }

                // Don't forget to build the adapter with Allwmake
                

Configure the OpenFOAM adapter


                // fluid-openfoam/system/preciceDict
                preciceConfig "../precice-config.xml";
                participant Fluid;

                modules (FSI);

                interfaces {
                  Interface1 {
                    mesh Fluid-Mesh;
                    patches (flap);
                    locations faceCenters;

                    readData (Displacement);

                    writeData (Force);
                  };
                };

                FSI {
                  rho rho [1 -3 0 0 0 0 0] 1;
                }
                

Configure the CalculiX adapter


                // solid-calculix/config.yml
                    
                participants:

                    Solid:
                        interfaces:
                        - nodes-mesh: Solid-Mesh
                          patch: surface
                          read-data: [Force]
                          write-data: [Displacement]
                          
                precice-config-file: ../precice-config.xml
                

Configure preCICE

Configure preCICE


                      <solver-interface dimensions="2">
                        <data:vector name="Force" />
                        <data:vector name="Displacement" />

                        <mesh name="Fluid-Mesh">
                          <use-data name="Force" />
                          <use-data name="Displacement" />
                        </mesh>

                        <mesh name="Solid-Mesh">
                          <use-data name="Displacement" />
                          <use-data name="Force" />
                        </mesh>

                        <participant name="Fluid">
                          <export:vtk directory="results" />
                          <use-mesh name="Fluid-Mesh" provide="yes" />
                          <use-mesh name="Solid-Mesh" from="Solid" />
                          <write-data name="Force" mesh="Fluid-Mesh" />
                          <read-data name="Displacement" mesh="Fluid-Mesh" />
                          <mapping:rbf-thin-plate-splines
                            direction="write"
                            from="Fluid-Mesh"
                            to="Solid-Mesh"
                            constraint="conservative" />
                          <mapping:rbf-thin-plate-splines
                            direction="read"
                            from="Solid-Mesh"
                            to="Fluid-Mesh"
                            constraint="consistent" />
                        </participant>

                        <participant name="Solid">
                          <use-mesh name="Solid-Mesh" provide="yes" />
                          <write-data name="Displacement" mesh="Solid-Mesh" />
                          <read-data name="Force" mesh="Solid-Mesh" />
                          <watch-point mesh="Solid-Mesh" name="Flap-Tip" coordinate="0.0;1" />
                        </participant>

                        <m2n:sockets from="Fluid" to="Solid" exchange-directory=".." />

                        <coupling-scheme:serial-explicit>
                          <participants first="Fluid" second="Solid" />
                          <max-time value="1.5" />
                          <time-window-size value="0.01" />
                          <exchange data="Force" mesh="Solid-Mesh" from="Fluid" to="Solid" />
                          <exchange data="Displacement" mesh="Solid-Mesh" from="Solid" to="Fluid" />
                        </coupling-scheme:serial-explicit>

                      </solver-interface>
                    
Notice the serial-explicit coupling scheme

What we did so far: serial-explicit scheme

Let's try an implicit coupling scheme

Configure the coupling scheme


                        <coupling-scheme:serial-implicit>
                          <participants first="Fluid" second="Solid" />
                          <max-time value="1.5" />
                          <time-window-size value="0.01" />
                          <exchange data="Force" mesh="Solid-Mesh" from="Fluid" to="Solid" />
                          <exchange data="Displacement" mesh="Solid-Mesh" from="Solid" to="Fluid" />
                          <relative-convergence-measure limit="5e-3" data="Displacement" mesh="Solid-Mesh" />
                          <relative-convergence-measure limit="5e-3" data="Force" mesh="Solid-Mesh" />
                          <max-iterations value="50" />
                          <acceleration:constant>
                            <relaxation value="0.5" />
                          </acceleration:constant>
                        </coupling-scheme:serial-implicit>
                    
Simplest case: constant under-relaxation

Too slow! Can we do better?

Improvement 1: Aitken under-relaxation


                        <acceleration:aitken>
                          <data name="Displacement" mesh="Solid-Mesh" />
                          <initial-relaxation value="0.5" />
                        </acceleration:aitken>
                    
Average iterations drop from 3.85 (constant) to 3.49 (Aitken)
Note: This simulation is reaching a steady-state, thus differences diffuse

Improvement 2: Anderson acceleration


                      <acceleration:IQN-ILS>
                        <data name="Displacement" mesh="Solid-Mesh" />
                        <initial-relaxation value="0.5" />
                        <preconditioner type="residual-sum" />
                        <filter type="QR2" limit="1e-2" />
                        <max-used-iterations value="100" />
                        <time-windows-reused value="15" />
                      </acceleration:IQN-ILS>
                    
Average iterations drop from 3.49 (Aitken) to 2.91 (IQN)

There are also parallel schemes

Can use fields from both / all participants

Improvement 3: Parallel coupling schemes


                    <coupling-scheme:parallel-implicit>
                      <time-window-size value="0.01" />
                      <max-time value="1.5" />
                      <participants first="Fluid" second="Solid" />
                      <exchange data="Force" mesh="Solid-Mesh" from="Fluid" to="Solid" />
                      <exchange data="Displacement" mesh="Solid-Mesh" from="Solid" to="Fluid" />
                      <max-iterations value="50" />
                      <relative-convergence-measure limit="5e-3" data="Displacement" mesh="Solid-Mesh" />
                      <relative-convergence-measure limit="5e-3" data="Force" mesh="Solid-Mesh" />
                      <acceleration:IQN-ILS>
                        <data name="Displacement" mesh="Solid-Mesh" />
                        <data name="Force" mesh="Solid-Mesh" />
                        <initial-relaxation value="0.5" />
                        <preconditioner type="residual-sum" />
                        <filter type="QR2" limit="1e-2" />
                        <max-used-iterations value="100" />
                        <time-windows-reused value="15" />
                      </acceleration:IQN-ILS>
                    </coupling-scheme:parallel-implicit>
                    
Average iterations drop from 2.91 (serial-implicit IQN) to 2.37 (parallel-implicit IQN)
Beware: Configuration not fine-tuned, not rigorous comparison, application context.

Rigorous comparison from the literature

Source: Dissertation of Benjamin Uekermann (2016)

Rigorous comparison from the literature

Source: Dissertation of Benjamin Uekermann (2016)