diff --git a/.gitignore b/.gitignore index 3f91e9fa5..c3e2b0161 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ build/ .venv/ __pycache__/ *.pyc +*.egg-info # Rust Cargo.lock diff --git a/growing-mesh/A/run.sh b/growing-mesh/A/run.sh new file mode 100755 index 000000000..290a35172 --- /dev/null +++ b/growing-mesh/A/run.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e -u + +python3 -m venv .venv +. .venv/bin/activate +pip install ../solver-python + +if [ $# -eq 0 ]; then + growing A +else + mpirun -n "$@" growing A +fi diff --git a/growing-mesh/B/run.sh b/growing-mesh/B/run.sh new file mode 100755 index 000000000..02eb71c98 --- /dev/null +++ b/growing-mesh/B/run.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e -u + +python3 -m venv .venv +. .venv/bin/activate +pip install ../solver-python + +if [ $# -eq 0 ]; then + growing B +else + mpirun -n "$@" growing B +fi diff --git a/growing-mesh/README.md b/growing-mesh/README.md new file mode 100644 index 000000000..4a1f4ac4d --- /dev/null +++ b/growing-mesh/README.md @@ -0,0 +1,49 @@ +--- +title: Growing Mesh +permalink: tutorials-growing-mesh.html +keywords: python, remeshing +summary: The growing mesh case is a showcase example of two solvers which grow their mesh at predefined points in time. +--- + +{% note %} +Get the [case files of this tutorial](https://github.com/precice/tutorials/tree/master/growing-mesh). Read how in the [tutorials introduction](https://precice.org/tutorials.html). +{% endnote %} + +## Setup + +The problem consists of a unit-square uniformly discretized by 768 x 768 nodes. +Running in parallel is only allowed for 1, 4, 9, or 16 ranks. +The unit square is partitioned equally among the ranks of a solver. + +The mesh starts with 2 nodes in z direction and at a given frequency, 2 nodes are added to the mesh, changing only the load per rank, not the partitioning. + +## Configuration + +preCICE configuration (image generated using the [precice-config-visualizer](https://precice.org/tooling-config-visualization.html)): + +![preCICE configuration visualization](images/tutorials-growing-mesh-precice-config.png) + +## Available solvers + +There are two solvers that define the same mesh: + +- A who runs first +- B who runs second + +## Running the Simulation + +Pass the amount of ranks to the run script of the solvers. +Not passing a number, runs the simulation on a single rank. +To run both on a two rank each, use: + +```bash +cd A +./run.sh 2 +``` + +and + +```bash +cd B +./run.sh 2 +``` diff --git a/growing-mesh/images/tutorials-growing-mesh-precice-config.png b/growing-mesh/images/tutorials-growing-mesh-precice-config.png new file mode 100644 index 000000000..f46f4ab57 Binary files /dev/null and b/growing-mesh/images/tutorials-growing-mesh-precice-config.png differ diff --git a/growing-mesh/precice-config.xml b/growing-mesh/precice-config.xml new file mode 100644 index 000000000..ca6be2ca5 --- /dev/null +++ b/growing-mesh/precice-config.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/growing-mesh/solver-python/pyproject.toml b/growing-mesh/solver-python/pyproject.toml new file mode 100644 index 000000000..f3969b34f --- /dev/null +++ b/growing-mesh/solver-python/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "growing" +version = "0" +dependencies = [ + "numpy", + "pyprecice @ git+https://github.com/precice/python-bindings.git@develop", + "mpi4py" +] + +[project.scripts] +growing = "solver:main" diff --git a/growing-mesh/solver-python/solver.py b/growing-mesh/solver-python/solver.py new file mode 100644 index 000000000..5a5faa11b --- /dev/null +++ b/growing-mesh/solver-python/solver.py @@ -0,0 +1,121 @@ +#!/bin/python3 + +import precice +import numpy as np +import math +import sys +from mpi4py import MPI + +import argparse + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("participant", choices=["A", "B"]) + parser.add_argument("--config", "-c", default="../precice-config.xml") + parser.add_argument("--no-remesh", dest="remesh", action="store_false") + args = parser.parse_args() + + participant_name = args.participant + remote_name = "A" if participant_name == "B" else "B" + + # x is partitioned per rank and doesn't change + nx = 256 * 3 + x = 0.0, 1.0 + ny = 256 * 3 + y = 0.0, 1.0 + + # y grows over time + newNodesPerEvent = 2 + eventFrequency = 3 # time windows + dz = 0.1 + + # Handle partitioning + world = MPI.COMM_WORLD + size: int = world.size + rank: int = world.rank + + parts: int = int(math.sqrt(size)) + assert parts**2 == size, "size must be a square value" + assert math.remainder(nx, parts) == 0, f"{nx=} must be dividable by {parts=}" + + # current parition in x, y + px = rank % parts + py = rank // parts + + # nodes per parition + nxp = nx // parts + nyp = ny // parts + + # node slide per partition + nxps = slice(nxp * px, nxp * (px + 1)) + nyps = slice(nyp * py, nyp * (py + 1)) + + print(f"{rank=} {nxps=} {nyps=}") + + def requiresEvent(tw): + return tw % eventFrequency == 0 + + assert not requiresEvent(eventFrequency - 1) + assert requiresEvent(eventFrequency) + assert not requiresEvent(eventFrequency + 1) + + def eventsAt(tw): + # First event block at tw=0, second at eventFrequency + return 1 + math.floor(tw / eventFrequency) + + assert eventsAt(0) == 1 + assert eventsAt(eventFrequency - 1) == 1 + assert eventsAt(eventFrequency) == 2 + assert eventsAt(eventFrequency + 1) == 2 + + def getMeshAtTimeWindow(tw): + znodes = eventsAt(tw) * newNodesPerEvent + + xs = np.linspace(x[0], x[1], nx)[nxps] + ys = np.linspace(y[0], y[1], ny)[nyps] + zs = np.array(range(znodes)) * dz + + return np.reshape([(x, y, z) for z in zs for y in ys for x in xs], (-1, 3)) + + participant = precice.Participant(participant_name, args.config, rank, size) + + mesh_name = participant_name + "-Mesh" + read_data_name = "Data-" + remote_name + write_data_name = "Data-" + participant_name + + coords = getMeshAtTimeWindow(0) + vertex_ids = participant.set_mesh_vertices(mesh_name, coords) + participant.initialize() + + tw = 1 + while participant.is_coupling_ongoing(): + dt = participant.get_max_time_step_size() + + data = participant.read_data(mesh_name, read_data_name, vertex_ids, dt) + if rank == 0: + print(data) + + if args.remesh and requiresEvent(tw): + oldCount = len(coords) + coords = getMeshAtTimeWindow(tw) + if rank == 0: + print( + f"Event grows local mesh from {oldCount} to { + len(coords)} and global mesh from { + oldCount * + size} to { + len(coords) * + size}") + participant.reset_mesh(mesh_name) + vertex_ids = participant.set_mesh_vertices(mesh_name, coords) + + data = np.full(len(coords), tw) + participant.write_data(mesh_name, write_data_name, vertex_ids, data) + + participant.advance(dt) + tw += 1 + + +if __name__ == "__main__": + main()