From 625e57fe8fb67119c9f44485f1e72ff564b29acc Mon Sep 17 00:00:00 2001 From: Benjamin Uekermann Date: Sat, 15 Oct 2022 17:46:21 +0200 Subject: [PATCH 1/3] Extend step 2 with Python tab --- .../couple-your-code-steering-methods.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pages/docs/couple-your-code/couple-your-code-steering-methods.md b/pages/docs/couple-your-code/couple-your-code-steering-methods.md index 325c3865062..308de74ab5c 100644 --- a/pages/docs/couple-your-code/couple-your-code-steering-methods.md +++ b/pages/docs/couple-your-code/couple-your-code-steering-methods.md @@ -24,6 +24,13 @@ What do they do? The following function allows us to query the maximum allowed time step size from preCICE: + + +
+
```cpp double getMaxTimeStepSize(); ``` @@ -53,3 +60,29 @@ while (not simulationDone()){ // time loop precice.finalize(); // frees data structures and closes communication channels turnOffSolver(); ``` +
+
+```python +import precice + +turn_on_solver() # e.g. setup and partition mesh + +precice = precice.Interface( + "FluidSolver", "precice-config.xml", rank, size +) +precice_dt = precice.initialize() + +u = initialize_solution() + +while t < t_end: # time loop + dt = compute_adaptive_dt() + dt = min(precice_dt, dt) # more about this in Step 5 + u = solve_time_step(dt, u) # returns new solution + precice_dt = precice.advance(dt) + t = t + dt + +precice.finalize() # frees data structures and closes communication channels +``` +
+
+ From 4e610908d8c90c9ae2c2807aa3eee1212b4c3dd2 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 May 2024 16:15:54 +0200 Subject: [PATCH 2/3] styles navtabs for web and pdf/print targets, uses CSS counters to highlight association between tab headers and tab content when printed, issue #211 --- css/customstyles-precice.css | 10 +++++++ css/printstyles.css | 53 +++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/css/customstyles-precice.css b/css/customstyles-precice.css index 360bc4fef2b..06e40be4a4c 100644 --- a/css/customstyles-precice.css +++ b/css/customstyles-precice.css @@ -340,4 +340,14 @@ div#tg-sb-sidebar { div#tg-sb-sidebar { position: static; } +/* style navtabs */ +.post-content ol li, .post-content ul li { + margin: 0px; +} +div.tab-content { + padding: 0px; + background-color: white; + } +div.tab-content div.tab-pane pre { + margin-top: 0px; } \ No newline at end of file diff --git a/css/printstyles.css b/css/printstyles.css index 6735dccb72a..a8fc64e7af0 100644 --- a/css/printstyles.css +++ b/css/printstyles.css @@ -40,7 +40,7 @@ a[href^="http:"]::after, a[href^="https:"]::after, a[href^="ftp:"]::after { a[href] { color: #0A76BB !important; } -a[href*="mailto"]::after, a[data-toggle="tooltip"]::after, a[href].noCrossRef::after { +a[href*="mailto"]::after, a[data-toggle="tooltip"]::after, a[href].noCrossRef::after, a[data-toggle="tab"]::after { content: ""; } @@ -219,4 +219,55 @@ pre > code { h1[id], h2[id], h3[id], h4[id], h5[id], h6[id], dt[id] { padding-top: 1.5em; margin-top: -1em; +} + +/* prepare nav tabs for printing */ +.nav > li.active > a, /* tab headers */ +.nav > li > a { + color: black; + background-color: white; + border: 1px solid #ccc; + border-radius: 4px 4px 0 0; +} +.tab-content > .tab-pane { + display: block !important; /* display non-active panes */ + position: relative; +} +div.tab-content div.tab-pane pre { + margin-top: 1em; +} +/* create counters to link tab headers to tab contents */ +.post-content ul.nav.nav-tabs { + counter-reset: tab_number; /* creates a new instance of counter with name tab_number */ +} +.post-content .nav.nav-tabs li::after { + counter-increment: tab_number; /* increment counter */ + content: counter(tab_number); /* display value in small bubble */ + position: absolute; + top: -1em; + left: -1em; + padding: 2px 5px; + background-color: white; + color: black; + font-size: 0.65em; + border-radius: 50%; + border: 1px solid #ccc; + box-shadow: 1px 1px 1px grey; +} +div.tab-content { + counter-reset: pane_number; +} +div.tab-pane::after { + counter-increment: pane_number; + content: counter(pane_number); + position: absolute; + top: -1em; + left: -1em; + padding: 2px 5px; + background-color: white; + color: black; + font-size: 0.65em; + border-radius: 50%; + border: 1px solid #ccc; + box-shadow: 1px 1px 1px gray; } \ No newline at end of file From df9b3cb1bdcc852eac683cf3beb9c15056552ca3 Mon Sep 17 00:00:00 2001 From: Srinidhi Krishna Date: Fri, 25 Apr 2025 02:44:17 +0200 Subject: [PATCH 3/3] Add Python code for `couple your code` section --- ...le-your-code-defining-mesh-connectivity.md | 149 +++++++++++++ .../couple-your-code-gradient-data.md | 59 ++++- .../couple-your-code-implicit-coupling.md | 72 ++++++ ...le-your-code-initializing-coupling-data.md | 38 ++++ .../couple-your-code-mesh-and-data-access.md | 93 +++++++- .../couple-your-code-preparing-your-solver.md | 24 ++ .../couple-your-code-steering-methods.md | 61 ++++-- .../couple-your-code-time-step-sizes.md | 205 +++++++++++++++++- 8 files changed, 679 insertions(+), 22 deletions(-) diff --git a/pages/docs/couple-your-code/couple-your-code-defining-mesh-connectivity.md b/pages/docs/couple-your-code/couple-your-code-defining-mesh-connectivity.md index 36d5e2ba5c1..1e84d02e682 100644 --- a/pages/docs/couple-your-code/couple-your-code-defining-mesh-connectivity.md +++ b/pages/docs/couple-your-code/couple-your-code-defining-mesh-connectivity.md @@ -14,6 +14,12 @@ For surface coupling in 2D, mesh connectivity boils down to defining edges betwe For volume coupling in 2D, mesh connectivity boils down to defining triangles and / or quads between vertices. In 3D, you need to define tetrahedra introduced in version `2.5.0`. All kind of connectivity can be built up directly from vertices. Triangles and quads also allow us to define them using edge IDs. + +
+
```cpp void setMeshEdge(precice::string_view meshName, VertexID firstVertexID, VertexID secondVertexID); @@ -22,7 +28,25 @@ void setMeshQuad(precice::string_view meshName, VertexID firstVertexID, VertexID void setMeshTetrahedron(precice::string_view meshName, VertexID firstVertexID, VertexID secondVertexID, VertexID thirdVertexID, VertexID fourthVertexID); ``` +
+
+ +```python +set_mesh_edge(mesh_name, first_vertex_id, second_vertex_id) +set_mesh_triangle(mesh_name, first_vertex_id, second_vertex_id, third_vertex_id) +set_mesh_quad(mesh_name, first_vertex_id, second_vertex_id, third_vertex_id, fourth_vertex_id) +set_mesh_tetrahedron(mesh_name, first_vertex_id, second_vertex_id, third_vertex_id, fourth_vertex_id) +``` + +
+
There are also bulk versions of these methods, which can be easier to handle in some cases: + +
+
```cpp void setMeshEdges(precice::string_view meshName, precice::span vertices); @@ -31,12 +55,39 @@ void setMeshQuads(precice::string_view meshName, precice::span v void setMeshTetrahedra(precice::string_view meshName, precice::span vertices); ``` +
+
+ +```python +set_mesh_edges(mesh_name, vertices) +set_mesh_triangles(mesh_name, vertices) +set_mesh_quads(mesh_name, vertices) +set_mesh_tetrahedra(mesh_name, vertices) +``` + +
+
If you do not configure any features in the preCICE configuration that require mesh connectivity, all these API functions are [no-ops](https://en.wikipedia.org/wiki/NOP_(code)). Thus, don't worry about performance. If you need a significant workload to already create this connectivity information in your adapter in the first place, you can also explicitly ask preCICE whether it is required: + +
+
```cpp bool requiresMeshConnectivityFor(precice::string_view meshName); ``` +
+
+ +```python +requires_mesh_connectivity_for(mesh_name) +``` + +
+
{% warning %} The API function `isMeshConnectivityRequired` is only supported since v2.3. {% endwarning %} @@ -52,6 +103,12 @@ Quads are only supported since v2.1. For older version, the methods only exist a {% endwarning %} The following code shows how mesh connectivity can be defined in our example. For sake of simplification, let's only define one triangle and let's assume that it consists of the first three vertices. + +
+
```cpp /* ... */ @@ -83,6 +140,30 @@ if (precice.requiresMeshConnectivityFor(meshName)) { /* ... */ ``` +
+
+ +```python +# We define the unit square in 2D +coords = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) +vertex_ids = precice.set_mesh_vertices(mesh_name, coords) + +if precice.requires_mesh_connectivity_for(mesh_name): + + # Define triangles ABC and BCD separately + precice.set_mesh_triangle(mesh_name, vertex_ids[0], vertex_ids[1], vertex_ids[2]) + precice.set_mesh_triangle(mesh_name, vertex_ids[1], vertex_ids[2], vertex_ids[3]) + + # Define triangles in one go + triangles = np.array([ + [vertex_ids[0], vertex_ids[1], vertex_ids[2]], + [vertex_ids[1], vertex_ids[2], vertex_ids[3]] + ]) + precice.set_mesh_triangles(mesh_name, triangles) +``` + +
+
## Mesh pre-processing preCICE pre-processes all provided meshes during initialization, removing duplicates and adding missing hierarchical elements. @@ -109,6 +190,12 @@ In this case you can save a mapping from Solver ID to preCICE Vertex ID, after d When iterating over the faces, get the vertex identifiers of defining points. For triangular faces, these would be the 3 corner points. Then map these Solver IDs to preCICE IDs, and use those to define your connectivity. + +
+
```cpp Participant participant(...); @@ -131,6 +218,30 @@ for (auto& tri: solver.triangularFaces) { } ``` +
+
+ +```python +participant = precice.Participant(...) + +id_maps = dict() +for vertex in solver.vertices: + vertex_id = participant.set_mesh_vertex("MyMesh", vertex.coords) + # Set the vertex_id as label + id_maps[vertex._id] = vertex_id + +for tri in solver.triangular_faces: + # Lookup the precice vertex id using solver vertex id + a = id_maps.get(tri.a._id) + b = id_maps.get(tri.b._id) + c = id_maps.get(tri.c._id) + + participant.set_mesh_triangle(mesh_name, a, b, c) +``` + +
+
+ ### Solver supports custom attributes You solver doesn't provide a unique identifier for each vertex. @@ -145,6 +256,12 @@ Examples of such attributes: In this case you can save preCICE Vertex IDs as labels directly in the solver vertices. Define the vertices using the preCICE API, then iterate over them and apply the preCICE vertex IDs as labels. When iterating over faces, get the preCICE vertex IDs from the point labels, and use those to define your connectivity. + +
+
```cpp Participant participant(...); @@ -164,6 +281,27 @@ for (auto& tri: solver.triangularFaces) { } ``` +
+
+ +```python +participant = precice.Participant(...) + +for vertex in solver.vertices: + vertex_id = participant.set_mesh_vertex("MyMesh", vertex.coords) + vertex.label = vertex_id + +for tri in solver.triangular_faces: + a = tri.a.label + b = tri.b.label + c = tri.c.label + + participant.set_mesh_triangle("MyMesh", a, b, c) +``` + +
+
+ ### Solver supports coordinates only Your solver provides coordinates only or labels are already used for another purpose. @@ -173,6 +311,12 @@ Depending on your solver, coordinates available at various stages may be subject Hence, a C++ `std::map` without custom comparator, or python `dict` may not be sufficient. An alternative would be to use a spatial index as a data structure to store this information. + +
+
```cpp Participant participant(...); @@ -224,6 +368,8 @@ class IDLookup { }; ``` +
+
In python, you could use the rtree package: ```py @@ -242,3 +388,6 @@ for tri in solver.triangularFaces: c = index.nearest(tri.c.coords) participant.set_mesh_triangle("MyMesh", a, b, c) ``` + +
+
diff --git a/pages/docs/couple-your-code/couple-your-code-gradient-data.md b/pages/docs/couple-your-code/couple-your-code-gradient-data.md index bcca473af4f..ef977cbf3cf 100644 --- a/pages/docs/couple-your-code/couple-your-code-gradient-data.md +++ b/pages/docs/couple-your-code/couple-your-code-gradient-data.md @@ -11,6 +11,12 @@ This feature is available since version 2.4.0. When using `nearest-neighbor-gradient` mapping, we require coupling data and additional gradient data. We have seen in [Step 3](couple-your-code-mesh-and-data-access.html) how to write data to the mesh. Now, we will learn how to write gradient data to the mesh. For this purpose, we use the following API method: + +
+
```cpp void writeGradientData( @@ -20,6 +26,15 @@ void writeGradientData( precice::span gradients); ``` +
+
+ +```python +write_gradient_data(mesh_name, data_name, vertex_ids, gradients) +``` + +
+
Let's consider an example for writing block vector gradient data corresponding to the vector data `v0 = (v0x, v0y) , v1 = (v1x, v1y), ... , vn = (vnx, vny)` differentiated in spatial directions x and y. The values are passed as following: @@ -31,12 +46,18 @@ The values are passed as following: ``` Let's add gradient data to our example code: + +
+
```cpp precice::Participant precice("FluidSolver", "precice-config.xml", rank, size); // constructor int meshDim = precice.getMeshDimensions("FluidMesh"); -int dataDim = precice.getMeshDimensions("FluidMesh", "Stress"); +int dataDim = precice.getDataDimensions("FluidMesh", "Stress"); /* ... */ precice.setMeshVertices("FluidMesh", coords, vertexIDs); @@ -69,6 +90,42 @@ while (not simulationDone()){ // time loop /* ... */ ``` +
+
+ +```python +precice = precice.Participant("FluidSolver", "precice-config.xml", rank, size) + +mesh_dim = precice.get_mesh_dimensions("FluidMesh") +data_dim = precice.get_data_dimensions("FluidMesh", "Stress") + +vertex_ids = precice.set_mesh_vertices("FluidMesh", coords) + +stress = np.zeros(vertex_size*data_dim) +stress_gradient = np.zeros(vertex_size*data_dim*mesh_dim) +precice.initialize() + +while not simulation_done(): + precice_dt = precice.get_max_time_step_size() + solver_dt = begin_time_step() + dt = min(precice_dt, solver_dt) + displacements = precice.read_data("FluidMesh", "Displacements", vertex_ids, dt) + set_displacement(displacements) + solve_time_step(dt) + stress = compute_stress() + + precice.write_data("FluidMesh", "Stress", vertex_ids, stress) + + if precice.requires_gradient_data_for("FluidMesh", "Stress"): + stress_gradient = compute_stress_gradient() + precice.write_gradient_data("FluidMesh", "Stress", vertex_ids, stress_gradient) + + precice.advance(dt) +``` + +
+
+ {% experimental %} This is an experimental feature. {% endexperimental %} diff --git a/pages/docs/couple-your-code/couple-your-code-implicit-coupling.md b/pages/docs/couple-your-code/couple-your-code-implicit-coupling.md index ed4eaaff8bf..b3caa7379e3 100644 --- a/pages/docs/couple-your-code/couple-your-code-implicit-coupling.md +++ b/pages/docs/couple-your-code/couple-your-code-implicit-coupling.md @@ -6,18 +6,40 @@ summary: "In previous steps, we only considered explicit coupling. We now move o --- The main ingredient needed for implicit coupling is move backwards in time. For that, we need a [flux capacitor](https://www.youtube.com/watch?v=VcZe8_RZO8c). Just kidding 😉. 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 interface: + +
+
```cpp bool requiresWritingCheckpoint() bool requiresReadingCheckpoint() ``` +
+
+ +```python +requires_writing_checkpoint() +requires_reading_checkpoint() +``` + +
+
These functions perform double duty: 1. They inform the adapter that writing or reading a checkpoint is required by the solver. 2. They let preCICE know that your adapter is capable of implicit coupling. preCICE will show an error if you configure implicit coupling without calling these functions. Let's extend our example code to also handle implicit coupling. + +
+
```cpp turnOnSolver(); //e.g. setup and partition mesh @@ -30,7 +52,27 @@ double solverDt; // solver time step size double preciceDt; // maximum precice time step size double dt; // actual time step size ``` + +
+
+ +```python +turn_on_solver() # eg: setup and partition mesh +precice = precice.Participant("FluidSolver", "precice-config.xml", rank, size) # Initialize participant + +# [... ]Define mesh +``` + +
+
+ +
+
+ ```cpp precice.initialize(); while (precice.isCouplingOngoing()){ @@ -57,6 +99,36 @@ precice.finalize(); // frees data structures and closes communication channels turnOffSolver(); ``` +
+
+ +```python +precice.initialize() +while precice.is_coupling_ongoing(): + if precice.requires_writing_checkpoint(): + save_old_state() + + precice_dt = precice.get_max_time_step_size() + solver_dt = begin_time_step() + dt = min(precice_dt, solver_dt) + displacements = precice.read_data("FluidMesh", "Displacements", vertexIDs, dt) + set_displacements(displacements) + solve_time_step(dt) + forces = compute_forces() + precice.write_data("FluidMesh", "Forces", vertex_ids, forces) + precice.advance(dt) + + if precice.requires_reading_checkpoint(): + reload_old_state() + else: + end_time_step() + +precice.finalize() +turn_off_solver() +``` + +
+
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](couple-your-code-time-step-sizes)), 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. {% important %} diff --git a/pages/docs/couple-your-code/couple-your-code-initializing-coupling-data.md b/pages/docs/couple-your-code/couple-your-code-initializing-coupling-data.md index 5f211e26c7a..0c153722f67 100644 --- a/pages/docs/couple-your-code/couple-your-code-initializing-coupling-data.md +++ b/pages/docs/couple-your-code/couple-your-code-initializing-coupling-data.md @@ -6,12 +6,33 @@ summary: "As default values, preCICE assumes that all coupling variables are zer --- By default preCICE assumes that all coupling variables are zero initially. If you want to provide non-zero initial values, you can write data before calling `initialize()`. This data will then be used as initial data. To check whether initial data is required, you can use the following function: + +
+
```cpp bool requiresInitialData() ``` +
+
+ +```python +requires_initial_data() +``` + +
+
To support data initialization, we extend our example as follows: + +
+
```cpp @@ -28,6 +49,23 @@ while (precice.isCouplingOngoing()){ } ``` +
+
+ +```python +[...] + +if precice.requires_initial_data(): + precice.write_data("FluidMesh", "Forces", vertex_ids, forces) + +precice.initialize() + +while precice.is_coupling_ongoing(): + [...] +``` + +
+
Now, you can specify at runtime if you want to initialize coupling data. For example to initialize displacements: ```xml diff --git a/pages/docs/couple-your-code/couple-your-code-mesh-and-data-access.md b/pages/docs/couple-your-code/couple-your-code-mesh-and-data-access.md index 6cc97dc8e25..c193b0374f2 100644 --- a/pages/docs/couple-your-code/couple-your-code-mesh-and-data-access.md +++ b/pages/docs/couple-your-code/couple-your-code-mesh-and-data-access.md @@ -9,6 +9,13 @@ For coupling, we need coupling meshes. Let's see how we can tell preCICE about o Coupling meshes and associated data fields are defined in the preCICE configuration file, which you probably already know from the tutorials. The concrete values, however, you can access with the API: + +
+
+ ```cpp VertexID setMeshVertex( ::precice::string_view meshName, @@ -20,10 +27,26 @@ void setMeshVertices( precice::span ids); ``` +
+
+ +```python +vertex_id = set_mesh_vertex(mesh_name, position) +vertex_ids = set_mesh_vertices(mesh_name, positions) +``` + +
+
* `setMeshVertex` defines the coordinates of a single mesh vertex and returns a vertex ID, which you can use to refer to this vertex. * `setMeshVertices` defines multiple vertices at once. So, you can use this function instead of calling `setMeshVertex` multiple times. This is also good practice for performance reasons. To write data to the coupling data structure the following API function is needed: + +
+
```cpp void Participant::writeData( @@ -33,7 +56,22 @@ void Participant::writeData( precice::span values) ``` +
+
+ +```python +write_data(mesh_name, data_name, vertex_ids, values) +``` + +
+
Similarly, there is a `readData` API function for reading coupling data: + +
+
```cpp void readData( @@ -44,9 +82,24 @@ void readData( precice::span values) const; ``` +
+
+ +```python +values = read_data(mesh_name, data_name, vertex_ids, relative_read_time) +``` + +
+
The relative read time can be anything from the current point in time (`0`) to the end of the time window (`getMaxTimeStepSize()`). We will talk about the additional argument `relativeReadTime` in detail in [the section on time interpolation](couple-your-code-waveform.html). Let's define coupling meshes and access coupling data in our example code: + +
+
```cpp turnOnSolver(); //e.g. setup and partition mesh @@ -87,6 +140,44 @@ precice.finalize(); // frees data structures and closes communication channels turnOffSolver(); ``` +
+
+ +```python +turn_on_solver() # e.g. setup and partition mesh +precice = precice.Participant("FluidSolver", "precice-config.xml", rank, size) # Initialize participant + +mesh_dim = precice.get_mesh_dimensions("FluidMesh") + +# Define coords of dimensions (vertex_size*mesh_dim) + +vertex_ids = precice.set_mesh_vertices("FluidMesh", coords) + +forces_dim = precice.get_data_dimensions("FluidMesh", "Forces") +forces = np.zeros(vertex_size*forces_dim) + +displacements_dim = precice.get_data_dimensions("FluidMesh", "Displacements") +displacements = np.zeros(vertex_size*displacements_dim) + +precice.initialize() +while participant.is_coupling_ongoing(): # time loop + precice_dt = precice.get_max_time_step_size() + solver_dt = begin_time_step() # e.g. compute adaptive dt + dt = min(precice_dt, solver_dt) + displacements = precice.read_data("FluidMesh", "Displacements", vertex_ids, dt) + set_displacements(displacements) + solve_time_step(dt) + forces = compute_forces() + precice.write_data("FluidMesh", "Forces", vertex_ids, forces) + precice.advance(dt) + end_time_step() # e.g. update variables, increment time + +precice.finalize() # frees data structures and closes communication channels +turn_off_solver() +``` + +
+
Did you see that your fluid solver now also needs to provide the functions `computeForces` and `setDisplacements`? As you are an expert in your fluid code, these functions should be easy to implement. Most probably, you already have such functionality anyway. If you are not an expert in your code try to find an expert :smirk:. Once your adapter reaches this point, it is a good idea to test your adapter against one of the [solverdummies](couple-your-code-api#minimal-reference-implementation), which then plays the role of the `SolidSolver`. @@ -127,7 +218,7 @@ You can use the following `precice-config.xml`: - + diff --git a/pages/docs/couple-your-code/couple-your-code-preparing-your-solver.md b/pages/docs/couple-your-code/couple-your-code-preparing-your-solver.md index dc1bff4dd42..f8958f2dfdd 100644 --- a/pages/docs/couple-your-code/couple-your-code-preparing-your-solver.md +++ b/pages/docs/couple-your-code/couple-your-code-preparing-your-solver.md @@ -7,6 +7,13 @@ summary: "If you want to couple your own code you need to properly understand it Let's say you want to prepare a fluid solver for fluid-structure interaction and that your code looks like this: + +
+
+ ```cpp turnOnSolver(); //e.g. setup and partition mesh @@ -18,8 +25,25 @@ while (not simulationDone()){ // time loop endTimeStep(); // e.g. update variables, increment time } turnOffSolver(); + +``` + +
+
+ +```python +turn_on_solver() #e.g. setup and partition mesh + +while not simulation_done(): # time loop + dt = begin_time_step() #e.g compute adaptive dt + solve_time_step(dt) + end_time_step() #e.g. update variables, increment time + +turn_off_solver() ``` +
+
Probably most solvers have such a structures: something in the beginning (reading input, domain decomposition), a big time loop, and something in the end. Each time step also falls into three parts: some pre-computations (e.g. computing an adaptive time step size), the actual computation (solving a linear or non-linear equation system), and something in the end (updating variables, incrementing time). Try to identify these parts in the code you want to couple. In the following steps, we will slowly add more and more calls to the preCICE API in this code snippet. Some part of the preCICE API is briefly described in each step. More precisely (no pun intended :grin:), we use the native `C++` API of preCICE. The API is, however, also available in other scientific programming languages: plain `C`, `Fortran`, `Python`, `Matlab`, `Julia`, and `Rust` (see [Application Programming Interface](couple-your-code-api)). diff --git a/pages/docs/couple-your-code/couple-your-code-steering-methods.md b/pages/docs/couple-your-code/couple-your-code-steering-methods.md index 308de74ab5c..67550aa0884 100644 --- a/pages/docs/couple-your-code/couple-your-code-steering-methods.md +++ b/pages/docs/couple-your-code/couple-your-code-steering-methods.md @@ -9,6 +9,13 @@ summary: "In this step, you get to know the most important API functions of preC As a first preparation step, you need to include the preCICE library headers. In C++, you need to include the file [precice.hpp](https://github.com/precice/precice/blob/develop/src/precice/precice.hpp). The handle to the preCICE API is the class `precice::Participant`. Its constructor requires the participant's name, the preCICE configuration file's name and the `rank` and `size` of the current thread. Once the basic preCICE interface is set up, we can _steer_ the behaviour of preCICE. For that we need the following functions: + +
+
+ ```cpp Participant( String participantName, String configurationFileName, int rank, int size ); void initialize(); @@ -16,6 +23,18 @@ void advance ( double computedTimeStepSize ); void finalize(); ``` +
+
+ +```python +participant = Participant(participant_name, configuration_file_name, rank, size) +participant.initialize() +participant.advance(computed_timestep_size) +participant.finalize() +``` + +
+
What do they do? * `initialize` establishes communication channels and sets up data structures of preCICE. @@ -23,14 +42,12 @@ What do they do? * `finalize` frees the preCICE data structures and closes communication channels. The following function allows us to query the maximum allowed time step size from preCICE: - -
-
+
```cpp double getMaxTimeStepSize(); ``` @@ -60,29 +77,37 @@ while (not simulationDone()){ // time loop precice.finalize(); // frees data structures and closes communication channels turnOffSolver(); ``` -
-
+ +
+
+ +```python +get_max_time_step() +``` + +But let’s ignore the details of time step sizes for the moment. This will be the topic of Step 5. We can now extend the code of our fluid solver: + ```python import precice turn_on_solver() # e.g. setup and partition mesh -precice = precice.Interface( +precice = precice.Participant( "FluidSolver", "precice-config.xml", rank, size ) -precice_dt = precice.initialize() -u = initialize_solution() - -while t < t_end: # time loop - dt = compute_adaptive_dt() - dt = min(precice_dt, dt) # more about this in Step 5 - u = solve_time_step(dt, u) # returns new solution - precice_dt = precice.advance(dt) - t = t + dt +precice.initialize() +while not simulation_done(): # time loop + precice_dt = precice.get_max_time_step() + solver_dt = begin_time_step() + dt = min(precice_dt, solver_dt) + solve_time_step(dt) + precice.advance(dt) + end_time_step() precice.finalize() # frees data structures and closes communication channels +turn_off_solver() ``` +
- diff --git a/pages/docs/couple-your-code/couple-your-code-time-step-sizes.md b/pages/docs/couple-your-code/couple-your-code-time-step-sizes.md index 6045a0fef02..8f3e6c3021b 100644 --- a/pages/docs/couple-your-code/couple-your-code-time-step-sizes.md +++ b/pages/docs/couple-your-code/couple-your-code-time-step-sizes.md @@ -8,6 +8,12 @@ redirect_from: --- In previous steps, you have already seen that there are quite some things going on with time step sizes. Let us now have a look at what is actually happening. + +
+
```cpp ... @@ -26,6 +32,24 @@ while (not simulationDone()){ // time loop } ``` +
+
+ +```python +... + +precice.initialize() +while not simulation_done(): # time loop + precice_dt = precice.get_max_time_step_size() + solver_dt = begin_time_step() # compute adaptive dt + dt = min(precice_dt, solver_dt) + solve_time_step(dt) + precice.advance(dt) + end_time_step() # update variables, increment time +``` + +
+
Good thing to know: in this step, you do really not have to alter your code. Everything needed is already there and can be configured. :relieved: There are basically two options to choose from in the configuration. @@ -50,24 +74,70 @@ The figure below illustrates this procedure (k is the subcycling index, the dash * After each time step, both participants tell preCICE which time step size `dt` they just used by calling `precice.advance(dt)`. This way, preCICE can keep track of the total time. `preciceDt` is the remainder time to the next window: + +
+
+ ```c++ preciceDt = precice.getMaxTimeStepSize(); ``` +
+
+ +```python +precice_dt = precice.get_max_time_step_size() +``` + +
+
* Both participants compute their next (adaptive) time step size. It can be larger or smaller than the remainder `preciceDt`. + +
+
```c++ solverDt = beginTimeStep(); ``` +
+
+ +```python +solver_dt = begin_time_step() +``` + +
+
If it is larger, the remainder `preciceDt` is used instead (orange participant, dark orange is used). If it is smaller, the participant's time step size `solverDt` can be used (blue participant, dark blue is used). These two cases are reflected in: + +
+
```c++ dt = min(preciceDt, solverDt) ``` +
+
+ +```python +dt = min(precice_dt, solver_dt) +``` + +
+
* Once both participants reach the end of the time window, coupling data is exchanged. {% note %} @@ -88,6 +158,12 @@ Such small time steps can lead to problems in the solver. One strategy to avoid this situation is to extend the last time step of a time window preventing problematic time step sizes. The follow example extends the time step negotiation between the solver and preCICE to ensure the next time step size `preciceDt - solverDt` stays over some threshold `minDt`. + +
+
```cpp ... @@ -113,6 +189,34 @@ precice.advance(dt); ... ``` +
+
+ +```python +... +solver_dt = begin_time_step() # e.g. compute adaptive dt +# Can lead to using a dt that only approximately reaches end of time window +# dt = min(precice_dt, solver_dt); +# Specify a minimal time step size + +min_dt = 10e-14 +if precice_dt - solver_dt < min_dt: + dt = precice_dt +else: + dt = solver_dt +displacements = precice.read_data("FluidMesh", "Displacements", vertex_ids, dt) +set_displacements(displacements) +solve_time_step(dt) +forces = compute_forces() +precice.write_data("FluidMesh", "Displacements", vertex_ids, forces) +# if dt = preciceDt, we will exactly reach the end of the window when calling advance +precice.advance(dt); +... +``` + +
+
+ {% note %} The strategy presented above is only one possibility. Generally, the participant knows best how to determine the allowed time step size and there often are additional requirements you might want to consider, depending on the use case and discretization techniques the participant is using. {% endnote %} @@ -126,30 +230,93 @@ The `first` participant sets the time step size. This requires that the `second` * The blue participant B is `first` and computes a step with its time step size (Step 1). * In `advance`, this time step size is given to preCICE (Step 2). + +
+
+ ```c++ precice.advance(dt); ``` +
+
+ +```python +precice.advance(dt) +``` + +
+
* preCICE has tracked the time level of the orange participant A and returns the remainder to reach B's time step size. + +
+
```c++ preciceDt = precice.getMaxTimeStepSize(); ``` +
+
+ +```python +precice_dt = precice.get_max_time_step_size() +``` + +
+
* A computes its next (adaptive) time step size. It can now be larger or smaller than the remainder. + +
+
```c++ solverDt = beginTimeStep(); ``` +
+
+ +```python +solver_dt = begin_time_step() +``` + +
+
+ If it is larger, the remainder `preciceDt` is used instead (the case below in Step 3, dark orange is used). If it is smaller, the participant's time step size `solverDt` can be used (not visualized). These two cases are again reflected in the formula: + +
+
```c++ -dt = min(preciceDt, solverDt) +dt = min(preciceDt, solverDt); ``` +
+
+ +```python +dt = min(precice_dt, solver_dt) +``` + +
+
+ * The procedure starts over with the blue participant B. {% note %} @@ -163,16 +330,50 @@ You never need to alter your code if you want to switch the first and second par ## Steering the end of the simulation One last thing about time. There is also a helper function in preCICE that allows you to steer the end of a coupled simulation: + +
+
```c++ bool isCouplingOngoing(); ``` +
+
+ +```python +is_coupling_ongoing() +``` + +
+
+ This functions looks at `max-time-windows` and `max-time` as defined in the preCICE configuration and knows when it is time to go. Then, you should call `finalize`. It replaces your `simulationDone()`. + +
+
```c++ -while (not precice.isCouplingOngoing()){ // time loop +while (precice.isCouplingOngoing()){ // time loop ... } precice.finalize(); ``` + +
+
+ +```python +while precice.is_coupling_ongoing(): # time loop + ... +precice.finalize() +``` + +
+