Coverage for /dolfinx-env/lib/python3.12/site-packages/io4dolfinx/backends/adios2/helpers.py: 82%
129 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 18:20 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 18:20 +0000
1"""
2Helpers reading/writing data with ADIOS2
3"""
5from __future__ import annotations
7import shutil
8from contextlib import contextmanager
9from pathlib import Path
10from typing import NamedTuple
12from mpi4py import MPI
14import adios2
15import dolfinx.graph
16import numpy as np
17import numpy.typing as npt
19from ...utils import compute_local_range, valid_function_types
22def resolve_adios_scope(adios2):
23 scope = adios2.bindings if hasattr(adios2, "bindings") else adios2
24 if not scope.is_built_with_mpi:
25 raise ImportError("ADIOS2 must be built with MPI support")
26 return scope
29adios2 = resolve_adios_scope(adios2)
32__all__ = [
33 "AdiosFile",
34 "ADIOSFile",
35 "check_variable_exists",
36 "read_array",
37 "read_adjacency_list",
38 "adios_to_numpy_dtype",
39]
41adios_to_numpy_dtype = {
42 "float": np.float32,
43 "double": np.float64,
44 "float complex": np.complex64,
45 "double complex": np.complex128,
46 "uint32_t": np.uint32,
47}
50class AdiosFile(NamedTuple):
51 io: adios2.IO
52 file: adios2.Engine
55@contextmanager
56def ADIOSFile(
57 adios: adios2.ADIOS,
58 filename: Path | str,
59 engine: str,
60 mode: adios2.Mode,
61 io_name: str,
62 comm: MPI.Comm | None = None,
63):
64 io = adios.DeclareIO(io_name)
65 io.SetEngine(engine)
66 # ADIOS2 sometimes struggles with existing files/folders it should overwrite
67 if mode == adios2.Mode.Write:
68 filename = Path(filename)
69 if filename.exists() and comm is not None and comm.rank == 0:
70 if filename.is_dir():
71 shutil.rmtree(filename)
72 else:
73 filename.unlink()
74 if comm is not None:
75 comm.Barrier()
77 file = io.Open(str(filename), mode)
78 try:
79 yield AdiosFile(io=io, file=file)
80 finally:
81 file.Close()
82 adios.RemoveIO(io_name)
85def check_variable_exists(
86 adios: adios2.ADIOS,
87 filename: Path | str,
88 variable: str,
89 engine: str,
90) -> bool:
91 io_name = f"{variable}_reader"
93 if not Path(filename).exists():
94 return False
96 variable_found = False
97 with ADIOSFile(
98 adios=adios,
99 engine=engine,
100 filename=filename,
101 mode=adios2.Mode.Read,
102 io_name=io_name,
103 ) as adios_file:
104 # Find step that has cell permutation
105 for _ in range(adios_file.file.Steps()):
106 adios_file.file.BeginStep()
107 if variable in adios_file.io.AvailableVariables().keys():
108 variable_found = True
109 break
110 adios_file.file.EndStep()
112 # Not sure if this is needed, but just in case
113 if variable in adios_file.io.AvailableVariables().keys():
114 variable_found = True
115 return variable_found
118def read_adjacency_list(
119 adios: adios2.ADIOS,
120 comm: MPI.Comm,
121 filename: Path | str,
122 data_name: str,
123 offsets_name: str,
124 engine: str,
125) -> dolfinx.graph.AdjacencyList:
126 """
127 Read an adjacency-list from an ADIOS file with given communicator.
128 The adjancency list is split in to a flat array (data) and its corresponding offset.
130 Args:
131 adios: The ADIOS instance
132 comm: The MPI communicator used to read the data
133 filename: Path to input file
134 data_name: Name of variable containing the indices of the adjacencylist
135 dofmap_offsets: Name of variable containing offsets of the adjacencylist
136 engine: Type of ADIOS engine to use for reading data
138 Returns:
139 The local part of dofmap from input dofs
141 .. note::
142 No MPI communication is done during this call
143 """
145 # Open ADIOS engine
146 io_name = f"{data_name=}_reader"
148 with ADIOSFile(
149 adios=adios,
150 engine=engine,
151 filename=filename,
152 mode=adios2.Mode.Read,
153 io_name=io_name,
154 ) as adios_file:
155 # First find step with dofmap offsets, to be able to read
156 # in a full row of the dofmap
157 for _ in range(adios_file.file.Steps()):
158 adios_file.file.BeginStep()
159 if offsets_name in adios_file.io.AvailableVariables().keys():
160 break
161 adios_file.file.EndStep()
162 if offsets_name not in adios_file.io.AvailableVariables().keys():
163 raise KeyError(f"Dof offsets not found at '{offsets_name}' in {filename}")
165 # Get global shape of dofmap-offset, and read in data with an overlap
166 d_offsets = adios_file.io.InquireVariable(offsets_name)
167 shape = d_offsets.Shape()
168 num_nodes = shape[0] - 1
169 local_range = compute_local_range(comm, num_nodes)
171 # As the offsets are one longer than the number of cells, we need to read in with an overlap
172 if len(shape) == 1:
173 d_offsets.SetSelection([[local_range[0]], [local_range[1] + 1 - local_range[0]]])
174 in_offsets = np.empty(
175 local_range[1] + 1 - local_range[0],
176 dtype=d_offsets.Type().strip("_t"),
177 )
178 else:
179 d_offsets.SetSelection(
180 [
181 [local_range[0], 0],
182 [local_range[1] + 1 - local_range[0], shape[1]],
183 ]
184 )
185 in_offsets = np.empty(
186 (local_range[1] + 1 - local_range[0], shape[1]),
187 dtype=d_offsets.Type().strip("_t"),
188 )
190 adios_file.file.Get(d_offsets, in_offsets, adios2.Mode.Sync)
191 in_offsets = in_offsets.squeeze()
193 # Assuming dofmap is saved in stame step
194 # Get the relevant part of the dofmap
195 if data_name not in adios_file.io.AvailableVariables().keys():
196 raise KeyError(f"Dofs not found at {data_name} in {filename}")
197 cell_dofs = adios_file.io.InquireVariable(data_name)
198 if len(shape) == 1:
199 cell_dofs.SetSelection([[in_offsets[0]], [in_offsets[-1] - in_offsets[0]]])
200 in_dofmap = np.empty(in_offsets[-1] - in_offsets[0], dtype=cell_dofs.Type().strip("_t"))
201 else:
202 cell_dofs.SetSelection([[in_offsets[0], 0], [in_offsets[-1] - in_offsets[0], shape[1]]])
203 in_dofmap = np.empty(
204 (in_offsets[-1] - in_offsets[0], shape[1]),
205 dtype=cell_dofs.Type().strip("_t"),
206 )
207 assert shape[1] == 1
209 in_dofmap = np.empty(in_offsets[-1] - in_offsets[0], dtype=cell_dofs.Type().strip("_t"))
210 adios_file.file.Get(cell_dofs, in_dofmap, adios2.Mode.Sync)
211 in_offsets -= in_offsets[0]
212 adios_file.file.EndStep()
214 # Return local dofmap
215 return dolfinx.graph.adjacencylist(in_dofmap, in_offsets.astype(np.int32))
218def read_array(
219 adios: adios2.ADIOS,
220 filename: Path | str,
221 array_name: str,
222 engine: str,
223 comm: MPI.Comm,
224 time: float = 0.0,
225 time_name: str = "",
226 legacy: bool = False,
227) -> tuple[npt.NDArray[valid_function_types], int]:
228 """
229 Read an array from file, return the global starting position of the local array
231 Args:
232 adios: The ADIOS instance
233 filename: Path to file to read array from
234 array_name: Name of array in file
235 engine: Name of engine to use to read file
236 comm: MPI communicator used for reading the data
237 time_name: Name of time variable for modern checkpoints
238 legacy: If True ignore time_name and read the first available step
239 Returns:
240 Local part of array and its global starting position
241 """
243 with ADIOSFile(
244 adios=adios,
245 engine=engine,
246 filename=filename,
247 mode=adios2.Mode.Read,
248 io_name="ArrayReader",
249 ) as adios_file:
250 # Get time-stamp from first available step
251 if legacy:
252 for i in range(adios_file.file.Steps()):
253 adios_file.file.BeginStep()
254 if array_name in adios_file.io.AvailableVariables().keys():
255 break
256 adios_file.file.EndStep()
257 if array_name not in adios_file.io.AvailableVariables().keys():
258 raise KeyError(f"No array found at {array_name}")
259 else:
260 for i in range(adios_file.file.Steps()):
261 adios_file.file.BeginStep()
262 if time_name in adios_file.io.AvailableVariables().keys():
263 arr = adios_file.io.InquireVariable(time_name)
264 time_shape = arr.Shape()
265 arr.SetSelection([[0], [time_shape[0]]])
266 times = np.empty(time_shape[0], dtype=adios_to_numpy_dtype[arr.Type()])
267 adios_file.file.Get(arr, times, adios2.Mode.Sync)
268 if times[0] == time:
269 break
270 if i == adios_file.file.Steps() - 1:
271 raise KeyError(
272 f"No data associated with {time_name}={time} found in {filename}"
273 )
275 adios_file.file.EndStep()
277 if time_name not in adios_file.io.AvailableVariables().keys():
278 raise KeyError(f"No data associated with {time_name}={time} found in {filename}")
280 if array_name not in adios_file.io.AvailableVariables().keys():
281 raise KeyError(f"No array found at {time=} for {array_name}")
283 arr = adios_file.io.InquireVariable(array_name)
284 arr_shape = arr.Shape()
285 # TODO: Should we always pick the first element?
286 assert len(arr_shape) >= 1
287 arr_range = compute_local_range(comm, arr_shape[0])
289 if len(arr_shape) == 1:
290 arr.SetSelection([[arr_range[0]], [arr_range[1] - arr_range[0]]])
291 vals = np.empty(arr_range[1] - arr_range[0], dtype=adios_to_numpy_dtype[arr.Type()])
292 else:
293 arr.SetSelection([[arr_range[0], 0], [arr_range[1] - arr_range[0], arr_shape[1]]])
294 vals = np.empty(
295 (arr_range[1] - arr_range[0], arr_shape[1]),
296 dtype=adios_to_numpy_dtype[arr.Type()],
297 )
298 assert arr_shape[1] == 1
300 adios_file.file.Get(arr, vals, adios2.Mode.Sync)
301 adios_file.file.EndStep()
303 return vals.reshape(-1), arr_range[0]