Issue reading/writing functions from separate runs on the same mesh

Hello,

I am running into an issue regarding function IO. I have a mesh saved using adios4dolfinx with the BP4 writer. I then have a program which solves a stochastic partial differential equation (SPDE) for a number of samples, and saves a fem.Function for each sample, also using adios4dolfinx and the BP4 writer. I would like to be able to add new sample results to an existing *.bp output in case I need more. Let me elaborate with a minimal working example.


First, we generate a mesh, for the MWE this is a 2D square mesh with the same number of cells in each direction. The script for generating it is shown below, let us call the file mesh.py. To generate the mesh run python3 mesh.py 30 ./mesh.bp.

mesh.py

from mpi4py import MPI
import sys
from pathlib import Path
import adios4dolfinx as adx
import dolfinx.mesh as mesh

if __name__ == "__main__":
    assert (
        len(sys.argv) == 3
    ), f"Usage: python3 {sys.argv[0]} <Number of cells for each direction> <Path to save mesh as *.bp>"
    n_cells: int = int(sys.argv[1])
    save_path: Path = Path(sys.argv[2])

    domain: mesh.Mesh = mesh.create_rectangle(
        comm=MPI.COMM_WORLD, points=[[-1.0, -1.0], [1.0, 1.0]], n=[n_cells, n_cells]
    )

    adx.write_mesh(filename=save_path, mesh=domain)

Next, we generate artificial fem.Function output. Here I picked the function f: \mathbb{R}^2 \to \mathbb{R}: (x, y) \mapsto x + y. We will do this twice, emulating two runs which generate one sample each by running mpiexec -np 4 python3 function.py ./mesh.bp FirstRun ./out.bp and mpiexec -np 4 python3 function.py ./mesh.bp SecondRun ./out.bp. The code for the function.py script is shown below. Note that on the first run, we read the mesh from mesh.bp and save it, together with the partitioning, in out.bp. On the second run we read the mesh from out.bp using the saved partitioning. The functions from both runs are now saved in out.bp under the names “FirstRun” and “SecondRun”.

function.py

from mpi4py import MPI
import sys
from pathlib import Path
import adios4dolfinx as adx
import dolfinx.mesh as mesh
import dolfinx.fem as fem
import numpy as np
import numpy.typing as npt

if __name__ == "__main__":
    assert (
        len(sys.argv) == 4
    ), f"Usage: python3 {sys.argv[0]} <Path to mesh *.bp directory> <Name of function> <Path to output *.bp directory>"
    mesh_path: Path = Path(sys.argv[1])
    fnc_name: str = str(sys.argv[2])
    out_path: Path = Path(sys.argv[3])

    if out_path.exists():
        domain: mesh.Mesh = adx.read_mesh(
            filename=out_path, comm=MPI.COMM_WORLD, read_from_partition=True
        )
    else:
        domain: mesh.Mesh = adx.read_mesh(filename=mesh_path, comm=MPI.COMM_WORLD)
        adx.write_mesh(filename=out_path, mesh=domain, store_partition_info=True)

    element: fem.ElementMetaData = fem.ElementMetaData(family="Lagrange", degree=1)
    V: fem.FunctionSpace = fem.functionspace(mesh=domain, element=element)
    fnc = fem.Function(V)
    assert isinstance(fnc, fem.Function)

    def fnc_eval(x: npt.NDArray) -> npt.NDArray:
        return np.sum(x, axis=0)

    fnc.interpolate(u0=fnc_eval)
    fnc.name = fnc_name

    adx.write_function(filename=out_path, u=fnc)

Lastly, we visualize these two functions using pyvista by executing python3 view.py ./out.bp FirstRun SecondRun. The view.py script is shown below. What we then see is the function saved from the first run on the left and of the second run on the right. The first looks as you would expect, and the second looks wrong, though it still has a resemblance to what it should look like.

view.py

from mpi4py import MPI
import sys
from pathlib import Path
import adios4dolfinx as adx
import dolfinx.mesh as mesh
import dolfinx.fem as fem
from dolfinx.plot import vtk_mesh
import numpy as np
import pyvista as pv

if __name__ == "__main__":
    assert (
        len(sys.argv) == 4
    ), f"Usage: python3 {sys.argv[0]} <Path to mesh & data *.bp directory> <Name of first function> <Name of second function>"
    mesh_path: Path = Path(sys.argv[1])
    fnc_name_1: str = str(sys.argv[2])
    fnc_name_2: str = str(sys.argv[3])

    domain: mesh.Mesh = adx.read_mesh(filename=mesh_path, comm=MPI.COMM_WORLD)

    element: fem.ElementMetaData = fem.ElementMetaData(family="Lagrange", degree=1)
    V: fem.FunctionSpace = fem.functionspace(mesh=domain, element=element)
    fnc_1, fnc_2 = fem.Function(V), fem.Function(V)
    assert isinstance(fnc_1, fem.Function) and isinstance(fnc_2, fem.Function)

    adx.read_function(filename=mesh_path, u=fnc_1, name=fnc_name_1)
    adx.read_function(filename=mesh_path, u=fnc_2, name=fnc_name_2)

    pv.set_plot_theme("paraview")
    pl = pv.Plotter(shape=(1, 2))

    grid = pv.UnstructuredGrid(*vtk_mesh(V))
    grid_1 = grid.copy()
    grid_1.point_data[fnc_name_1] = np.real(fnc_1.x.array)
    grid_2 = grid.copy()
    grid_2.point_data[fnc_name_2] = np.real(fnc_2.x.array)

    pl.subplot(0, 0)
    pl.show_axes()
    pl.camera_position = "xy"
    pl.zoom_camera(0.25)
    pl.add_mesh(grid_1, show_edges=True, show_scalar_bar=True)

    pl.subplot(0, 1)
    pl.show_axes()
    pl.camera_position = "xy"
    pl.zoom_camera(0.25)
    pl.add_mesh(grid_2, show_edges=True, show_scalar_bar=True)

    pl.show()

So the question is, what am I doing wrong here? Thanks in advance for the help.

I am using dolfinx version 0.10.0.post1 and adios4dolfinx version 0.10.0.post0.


For convenience, here is a run.sh bash script for running the MWE assuming the *.py files mentioned are present.

run.sh

#!/bin/bash

python3 mesh.py 30 ./mesh.bp
mpiexec -np 4 python3 function.py ./mesh.bp FirstRun ./out.bp
mpiexec -np 4 python3 function.py ./mesh.bp SecondRun ./out.bp
python3 view.py ./out.bp FirstRun SecondRun

If one exchanges the adx.write_function(filename=out_path, u=fnc) line in the function.py code with adx.write_function_on_input_mesh(filename=out_path, u=fnc), the “FirstRun” function is incorrect and the “SecondRun” function is correct.

I was able to fix the issue by modifying the function.py to the code below. Along with the change mentioned above, we re-read the mesh from out.bp when it was read originally from mesh.bp. Doing this double read does not seem like an optimal solution though.

function.py

from mpi4py import MPI
import sys
from pathlib import Path
import adios4dolfinx as adx
import dolfinx.mesh as mesh
import dolfinx.fem as fem
import numpy as np
import numpy.typing as npt

if __name__ == "__main__":
    assert (
        len(sys.argv) == 4
    ), f"Usage: python3 {sys.argv[0]} <Path to mesh *.bp directory> <Name of function> <Path to output *.bp directory>"
    mesh_path: Path = Path(sys.argv[1])
    fnc_name: str = str(sys.argv[2])
    out_path: Path = Path(sys.argv[3])

    if out_path.exists():
        domain: mesh.Mesh = adx.read_mesh(
            filename=out_path, comm=MPI.COMM_WORLD, read_from_partition=True
        )
    else:
        domain: mesh.Mesh = adx.read_mesh(filename=mesh_path, comm=MPI.COMM_WORLD)
        adx.write_mesh(filename=out_path, mesh=domain, store_partition_info=True)
        domain: mesh.Mesh = adx.read_mesh(
            filename=out_path, comm=MPI.COMM_WORLD, read_from_partition=True
        )

    element: fem.ElementMetaData = fem.ElementMetaData(family="Lagrange", degree=1)
    V: fem.FunctionSpace = fem.functionspace(mesh=domain, element=element)
    fnc = fem.Function(V)
    assert isinstance(fnc, fem.Function)

    def fnc_eval(x: npt.NDArray) -> npt.NDArray:
        return np.sum(x, axis=0)

    fnc.interpolate(u0=fnc_eval)
    fnc.name = fnc_name

    adx.write_function_on_input_mesh(filename=out_path, u=fnc)

Hi @Aaron,

I am happy you were able to resolve the issue.

However, I would suggest the following approach:

from mpi4py import MPI
import sys
from pathlib import Path
import adios4dolfinx as adx
import dolfinx.mesh as mesh
import dolfinx.fem as fem
import numpy as np
import numpy.typing as npt

if __name__ == "__main__":
    assert (
        len(sys.argv) == 4
    ), f"Usage: python3 {sys.argv[0]} <Path to mesh *.bp directory> <Name of function> <Path to output *.bp directory>"
    mesh_path: Path = Path(sys.argv[1])
    fnc_name: str = str(sys.argv[2])
    out_path: Path = Path(sys.argv[3])
    exists = out_path.exists()
    if exists:
        domain: mesh.Mesh = adx.read_mesh(
            filename=out_path, comm=MPI.COMM_WORLD, read_from_partition=True
        )
    else:
        domain: mesh.Mesh = adx.read_mesh(filename=mesh_path, comm=MPI.COMM_WORLD)
        adx.write_mesh(filename=out_path, mesh=domain, store_partition_info=True)

    element: fem.ElementMetaData = fem.ElementMetaData(family="Lagrange", degree=1)
    V: fem.FunctionSpace = fem.functionspace(mesh=domain, element=element)
    fnc = fem.Function(V)
    assert isinstance(fnc, fem.Function)

    def fnc_eval(x: npt.NDArray) -> npt.NDArray:
        return np.sum(x, axis=0)

    fnc.interpolate(u0=fnc_eval)
    fnc.name = fnc_name
    
    if exists:
        adx.write_function_on_input_mesh(
            filename=out_path, u=fnc)
    else:
        adx.write_function(filename=out_path, u=fnc)

that should cover both cases for you and there is no double read.

Hello Jorgen,

Thank you for the response and suggestion. Unfortunately that did not resolve the issue, the function from the first run is not saved correctly (or not read correctly when plotting).

That doesn’t make sense to me, as the code in should be unchanged for the first write compared to your original, working code.
It only changes the write to write on input mesh if the mesh was read from the output file (which was the case that you got working in your second post).

It doesn’t make sense to me either. Would it be possible for you to run the MWE with your modifications? If it works for you, then perhaps something is wrong in my installation.

I ran the following:

python3 mesh.py 30 ./mesh.bp
mpiexec -np 4 python3 function.py ./mesh.bp FirstRun ./out.bp
mpiexec -np 4 python3 function.py ./mesh.bp SecondRun ./out.bp
python3 view.py ./out.bp FirstRun SecondRun

where I use your mesh.py and view.py, but my function.py implementation in a clean folder (I used conda to install adios4dolfinx, dolfinx, pyvista etc), which yielded the plot:

Thank you, then there is likely something wrong with my installation.

It dawned on me what I did wrong. I didn’t copy the solution you sent, just modified my script with the if-else branch for the function writing and commented out the reloading. I used out_path.exists() for both branches which is of course always true in the second if-else statement since we make the file if it doesn’t exist in the first if-else statement.

Apologies for the logic oversight, and thanks again for looking into it.

1 Like