Coverage for /dolfinx-env/lib/python3.14/site-packages/io4dolfinx/backends/adios2/backend.py: 94%

408 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-21 11:50 +0000

1import warnings 

2from pathlib import Path 

3from typing import Any 

4 

5from mpi4py import MPI 

6 

7import adios2 

8import dolfinx 

9import fenicsx_compat 

10import numpy as np 

11import numpy.typing as npt 

12 

13from ...structures import ArrayData, FunctionData, MeshData, MeshTagsData, ReadMeshData 

14from ...utils import check_file_exists, compute_local_range 

15from .. import DEFAULT_MESH_NAME, FileMode, ReadMode, get_mesh_name 

16from .helpers import ( 

17 ADIOSFile, 

18 adios_to_numpy_dtype, 

19 check_variable_exists, 

20 read_adjacency_list, 

21 read_array, 

22) 

23 

24adios2 = fenicsx_compat.resolve_adios_scope(adios2) 

25 

26read_mode = ReadMode.parallel 

27 

28 

29def get_default_backend_args(arguments: dict[str, Any] | None) -> dict[str, Any]: 

30 """Get default arguements (sets engine to BP4).""" 

31 args = dict(arguments) if arguments else {} # Copy: do not mutate the caller's dict 

32 if "engine" not in args.keys(): 

33 args["engine"] = "BP4" 

34 if "legacy" not in args.keys(): 

35 args["legacy"] = False # Only used for legacy HDF5 meshtags 

36 if "name" not in args.keys(): 

37 args["name"] = DEFAULT_MESH_NAME 

38 return args 

39 

40 

41def mesh_prefix(backend_args: dict[str, Any] | None) -> str: 

42 """ADIOS2 variable-name prefix for the mesh named in ``backend_args``. 

43 

44 The default mesh gets an *empty* prefix, so that it is stored under exactly 

45 the names used before checkpoints could hold more than one mesh. Files 

46 written by earlier versions therefore stay readable, and a file holding only 

47 the default mesh stays readable by them. Any other mesh is namespaced under 

48 ``"<name>/"``. 

49 

50 .. note:: 

51 This is io4dolfinx's own checkpoint schema, not ADIOS2's VTX 

52 visualization schema -- no ``vtk.xml`` attribute is written, and these 

53 files are not readable by ParaView's VTX reader whether they hold one 

54 mesh or several. VTX supports only a single mesh per file, so holding 

55 several here costs nothing that was ever available. For a 

56 visualizable file with several meshes use the ``vtkhdf`` backend, whose 

57 ``MultiBlockDataSet`` is designed for it. 

58 

59 Args: 

60 backend_args: Arguments to backend 

61 

62 Returns: 

63 The prefix to prepend to every variable and attribute name of that mesh. 

64 """ 

65 name = get_mesh_name(backend_args) 

66 return "" if name == DEFAULT_MESH_NAME else f"{name}/" 

67 

68 

69def _seek_step(adios_file, variable: str, start: int = 0) -> int: 

70 """Advance the reader to the first step at or after ``start`` holding ``variable``. 

71 

72 With several meshes in one file a step no longer holds every variable, so a 

73 reader cannot assume the one it wants is in step 0. Leaves the file *inside* 

74 the matching step (the caller is responsible for ``EndStep``). 

75 

76 Args: 

77 adios_file: The open :class:`ADIOSFile` 

78 variable: Name of the variable to seek 

79 start: First step to consider 

80 

81 Returns: 

82 Index of the step holding ``variable``. 

83 

84 Raises: 

85 KeyError: If no step holds ``variable``. 

86 """ 

87 for i in range(start, adios_file.file.Steps()): 

88 adios_file.file.BeginStep() 

89 if variable in adios_file.io.AvailableVariables().keys(): 

90 return i 

91 adios_file.file.EndStep() 

92 raise KeyError(f"'{variable}' not found in file") 

93 

94 

95def convert_file_mode(mode: FileMode) -> adios2.Mode: # type: ignore[override] 

96 match mode: 

97 case FileMode.append: 

98 return adios2.Mode.Append 

99 case FileMode.write: 

100 return adios2.Mode.Write 

101 case FileMode.read: 

102 return adios2.Mode.Read 

103 case _: 

104 raise NotImplementedError(f"FileMode {mode} not implemented.") 

105 

106 

107def write_attributes( 

108 filename: Path | str, 

109 comm: MPI.Comm, 

110 name: str, 

111 attributes: dict[str, np.ndarray], 

112 backend_args: dict[str, Any] | None = None, 

113): 

114 """Write attributes to file using ADIOS2. 

115 

116 Args: 

117 filename: Path to file to write to 

118 comm: MPI communicator used in storage 

119 name: Name of the attributes 

120 attributes: Dictionary of attributes to write to file 

121 engine: ADIOS2 engine to use 

122 """ 

123 

124 adios = adios2.ADIOS(comm) 

125 backend_args = get_default_backend_args(backend_args) 

126 

127 with ADIOSFile( 

128 adios=adios, 

129 filename=filename, 

130 mode=adios2.Mode.Append, 

131 io_name="AttributeWriter", 

132 engine=backend_args["engine"], 

133 ) as adios_file: 

134 adios_file.file.BeginStep() 

135 

136 for k, v in attributes.items(): 

137 adios_file.io.DefineAttribute(f"{name}_{k}", v) 

138 

139 adios_file.file.PerformPuts() 

140 adios_file.file.EndStep() 

141 

142 

143def read_attributes( 

144 filename: Path | str, 

145 comm: MPI.Comm, 

146 name: str, 

147 backend_args: dict[str, Any] | None = None, 

148) -> dict[str, np.ndarray]: 

149 """Read attributes from file using ADIOS2. 

150 

151 Args: 

152 filename: Path to file to read from 

153 comm: MPI communicator used in storage 

154 name: Name of the attributes 

155 engine: ADIOS2 engine to use 

156 Returns: 

157 The attributes 

158 """ 

159 check_file_exists(filename) 

160 adios = adios2.ADIOS(comm) 

161 backend_args = get_default_backend_args(backend_args) 

162 with ADIOSFile( 

163 adios=adios, 

164 filename=filename, 

165 mode=adios2.Mode.Read, 

166 engine=backend_args["engine"], 

167 io_name="AttributesReader", 

168 ) as adios_file: 

169 adios_file.file.BeginStep() 

170 attributes = {} 

171 for k in adios_file.io.AvailableAttributes().keys(): 

172 if k.startswith(f"{name}_"): 

173 a = adios_file.io.InquireAttribute(k) 

174 attributes[k[len(name) + 1 :]] = a.Data() 

175 adios_file.file.EndStep() 

176 return attributes 

177 

178 

179def read_timestamps( 

180 filename: Path | str, 

181 comm: MPI.Comm, 

182 function_name: str, 

183 backend_args: dict[str, Any] | None = None, 

184) -> npt.NDArray[np.float64 | str]: # type: ignore[type-var] 

185 """Read time-stamps from a checkpoint file. 

186 

187 Args: 

188 comm: MPI communicator 

189 filename: Path to file 

190 function_name: Name of the function to read time-stamps for 

191 backend_args: Arguments for backend, for instance file type. 

192 backend: What backend to use for writing. 

193 Returns: 

194 The time-stamps 

195 """ 

196 check_file_exists(filename) 

197 

198 adios = adios2.ADIOS(comm) 

199 backend_args = get_default_backend_args(backend_args) 

200 prefix = mesh_prefix(backend_args) 

201 with ADIOSFile( 

202 adios=adios, 

203 filename=filename, 

204 mode=adios2.Mode.Read, 

205 engine=backend_args["engine"], 

206 io_name="TimestepReader", 

207 ) as adios_file: 

208 time_name = f"{prefix}{function_name}_time" 

209 time_stamps = [] 

210 for _ in range(adios_file.file.Steps()): 

211 adios_file.file.BeginStep() 

212 if time_name in adios_file.io.AvailableVariables().keys(): 

213 arr = adios_file.io.InquireVariable(time_name) 

214 time_shape = arr.Shape() 

215 arr.SetSelection([[0], [time_shape[0]]]) 

216 times = np.empty( 

217 time_shape[0], 

218 dtype=adios_to_numpy_dtype[arr.Type()], 

219 ) 

220 adios_file.file.Get(arr, times, adios2.Mode.Sync) 

221 time_stamps.append(times[0]) 

222 adios_file.file.EndStep() 

223 

224 return np.array(time_stamps) 

225 

226 

227def write_mesh( 

228 filename: Path | str, 

229 comm: MPI.Comm, 

230 mesh: MeshData, 

231 backend_args: dict[str, Any] | None = None, 

232 mode: FileMode = FileMode.write, 

233 time: float = 0.0, 

234): 

235 """Write a mesh to file using ADIOS2. 

236 

237 Args: 

238 comm: MPI communicator used in storage 

239 mesh: Internal data structure for the mesh data to save to file 

240 filename: Path to file to write to 

241 backend_args: File mode and potentially the io-name. 

242 mode: Mode to use (write or append) 

243 time: Time stamp 

244 """ 

245 backend_args = get_default_backend_args(backend_args) 

246 if "io_name" not in backend_args.keys(): 

247 backend_args["io_name"] = "MeshWriter" 

248 prefix = mesh_prefix(backend_args) 

249 

250 mode = convert_file_mode(mode) 

251 gdim = mesh.local_geometry.shape[1] 

252 adios = adios2.ADIOS(comm) 

253 

254 # Topology is constant in time and written once. In append mode it may 

255 # already be present either because this mesh was written at an earlier time 

256 # step, or because a *different* mesh was written first -- so the test is 

257 # whether this mesh's topology exists, not whether the file is new. 

258 topology_exists = False 

259 if mode == adios2.Mode.Append: 

260 topology_exists = check_variable_exists( 

261 adios, filename, f"{prefix}Topology", engine=backend_args["engine"] 

262 ) 

263 

264 with ADIOSFile( 

265 adios=adios, 

266 filename=filename, 

267 mode=mode, 

268 comm=comm, 

269 engine=backend_args["engine"], 

270 io_name=backend_args["io_name"], 

271 ) as adios_file: 

272 adios_file.file.BeginStep() 

273 # Write geometry 

274 pointvar = adios_file.io.DefineVariable( 

275 f"{prefix}Points", 

276 mesh.local_geometry, 

277 shape=[mesh.num_nodes_global, gdim], 

278 start=[mesh.local_geometry_pos[0], 0], 

279 count=[mesh.local_geometry_pos[1] - mesh.local_geometry_pos[0], gdim], 

280 ) 

281 adios_file.file.Put(pointvar, mesh.local_geometry, adios2.Mode.Sync) 

282 

283 if not topology_exists: 

284 adios_file.io.DefineAttribute(f"{prefix}CellType", mesh.cell_type) 

285 adios_file.io.DefineAttribute( 

286 f"{prefix}Degree", np.array([mesh.degree], dtype=np.int32) 

287 ) 

288 adios_file.io.DefineAttribute( 

289 f"{prefix}LagrangeVariant", np.array([mesh.lagrange_variant], dtype=np.int32) 

290 ) 

291 # Write topology (on;y on first write as topology is constant) 

292 num_dofs_per_cell = mesh.local_topology.shape[1] 

293 dvar = adios_file.io.DefineVariable( 

294 f"{prefix}Topology", 

295 mesh.local_topology, 

296 shape=[mesh.num_cells_global, num_dofs_per_cell], 

297 start=[mesh.local_topology_pos[0], 0], 

298 count=[ 

299 mesh.local_topology_pos[1] - mesh.local_topology_pos[0], 

300 num_dofs_per_cell, 

301 ], 

302 ) 

303 adios_file.file.Put(dvar, mesh.local_topology) 

304 

305 # Add partitioning data 

306 if mesh.store_partition: 

307 assert mesh.partition_range is not None 

308 par_data = adios_file.io.DefineVariable( 

309 f"{prefix}PartitioningData", 

310 mesh.ownership_array, 

311 shape=[mesh.partition_global], 

312 start=[mesh.partition_range[0]], 

313 count=[ 

314 mesh.partition_range[1] - mesh.partition_range[0], 

315 ], 

316 ) 

317 adios_file.file.Put(par_data, mesh.ownership_array) 

318 assert mesh.ownership_offset is not None 

319 par_offset = adios_file.io.DefineVariable( 

320 f"{prefix}PartitioningOffset", 

321 mesh.ownership_offset, 

322 shape=[mesh.num_cells_global + 1], 

323 start=[mesh.local_topology_pos[0]], 

324 count=[mesh.local_topology_pos[1] - mesh.local_topology_pos[0] + 1], 

325 ) 

326 adios_file.file.Put(par_offset, mesh.ownership_offset) 

327 assert mesh.partition_processes is not None 

328 adios_file.io.DefineAttribute( 

329 f"{prefix}PartitionProcesses", 

330 np.array([mesh.partition_processes], dtype=np.int32), 

331 ) 

332 if topology_exists and mesh.store_partition: 

333 warnings.warn("Partitioning data is not written in append mode") 

334 

335 # Add time step to file 

336 t_arr = np.array([time], dtype=np.float64) 

337 time_var = adios_file.io.DefineVariable( 

338 f"{prefix}MeshTime", 

339 t_arr, 

340 shape=[1], 

341 start=[0], 

342 count=[1 if comm.rank == 0 else 0], 

343 ) 

344 adios_file.file.Put(time_var, t_arr) 

345 

346 adios_file.file.PerformPuts() 

347 adios_file.file.EndStep() 

348 

349 

350def read_mesh_data( 

351 filename: Path | str, 

352 comm: MPI.Comm, 

353 time: str | float | None = 0.0, 

354 read_from_partition: bool = False, 

355 backend_args: dict[str, Any] | None = None, 

356) -> ReadMeshData: 

357 """Read an ADIOS2 mesh data for use with DOLFINx. 

358 

359 Args: 

360 filename: Path to input file 

361 comm: The MPI communciator to distribute the mesh over 

362 engine: ADIOS engine to use for reading (BP4, BP5 or HDF5) 

363 time: Time stamp associated with mesh 

364 legacy: If checkpoint was made prior to time-dependent mesh-writer set to True 

365 read_from_partition: Read mesh with partition from file 

366 Returns: 

367 The mesh topology, geometry, UFL domain and partition function 

368 """ 

369 

370 adios = adios2.ADIOS(comm) 

371 backend_args = get_default_backend_args(backend_args) 

372 legacy = backend_args.get("legacy", False) 

373 io_name = backend_args.get("io_name", "MeshReader") 

374 engine = backend_args["engine"] 

375 prefix = mesh_prefix(backend_args) 

376 with ADIOSFile( 

377 adios=adios, 

378 filename=filename, 

379 mode=adios2.Mode.Read, 

380 engine=engine, 

381 io_name=io_name, 

382 ) as adios_file: 

383 # Get time independent mesh variables (mesh topology and cell type info) 

384 # first. With several meshes in the file this mesh's topology need not be 

385 # in step 0, so seek the step holding it. 

386 try: 

387 step = _seek_step(adios_file, f"{prefix}Topology") 

388 except KeyError as e: 

389 raise KeyError(f"Mesh topology not found at {prefix}Topology in {filename}") from e 

390 topology = adios_file.io.InquireVariable(f"{prefix}Topology") 

391 shape = topology.Shape() 

392 local_range = compute_local_range(comm, shape[0]) 

393 topology.SetSelection([[local_range[0], 0], [local_range[1] - local_range[0], shape[1]]]) 

394 mesh_topology = np.empty((local_range[1] - local_range[0], shape[1]), dtype=np.int64) 

395 adios_file.file.Get(topology, mesh_topology, adios2.Mode.Deferred) 

396 

397 # Check validity of partitioning information 

398 if read_from_partition: 

399 if f"{prefix}PartitionProcesses" not in adios_file.io.AvailableAttributes().keys(): 

400 raise KeyError(f"Partitioning information not found in {filename}") 

401 par_num_procs = adios_file.io.InquireAttribute(f"{prefix}PartitionProcesses") 

402 num_procs = par_num_procs.Data()[0] 

403 if num_procs != comm.size: 

404 raise ValueError(f"Number of processes in file ({num_procs})!=({comm.size=})") 

405 

406 # Get mesh cell type 

407 if f"{prefix}CellType" not in adios_file.io.AvailableAttributes().keys(): 

408 raise KeyError(f"Mesh cell type not found at {prefix}CellType in {filename}") 

409 celltype = adios_file.io.InquireAttribute(f"{prefix}CellType") 

410 cell_type = celltype.DataString()[0] 

411 

412 # Get basix info 

413 if f"{prefix}LagrangeVariant" not in adios_file.io.AvailableAttributes().keys(): 

414 raise KeyError(f"Mesh LagrangeVariant not found in {filename}") 

415 lvar = adios_file.io.InquireAttribute(f"{prefix}LagrangeVariant").Data()[0] 

416 if f"{prefix}Degree" not in adios_file.io.AvailableAttributes().keys(): 

417 raise KeyError(f"Mesh degree not found in {filename}") 

418 degree = adios_file.io.InquireAttribute(f"{prefix}Degree").Data()[0] 

419 

420 if not legacy: 

421 # Geometry for this time stamp is written in the same step as the 

422 # topology or a later one, so resume the scan from there. 

423 time_name = f"{prefix}MeshTime" 

424 for i in range(step, adios_file.file.Steps()): 

425 if i > step: 

426 adios_file.file.BeginStep() 

427 if time_name in adios_file.io.AvailableVariables().keys(): 

428 arr = adios_file.io.InquireVariable(time_name) 

429 time_shape = arr.Shape() 

430 arr.SetSelection([[0], [time_shape[0]]]) 

431 times = np.empty(time_shape[0], dtype=adios_to_numpy_dtype[arr.Type()]) 

432 adios_file.file.Get(arr, times, adios2.Mode.Sync) 

433 if times[0] == time: 

434 break 

435 if i == adios_file.file.Steps() - 1: 

436 raise KeyError( 

437 f"No data associated with {time_name}={time} found in {filename}" 

438 ) 

439 

440 adios_file.file.EndStep() 

441 

442 if time_name not in adios_file.io.AvailableVariables().keys(): 

443 raise KeyError(f"No data associated with {time_name}={time} found in {filename}") 

444 

445 # Get mesh geometry 

446 if f"{prefix}Points" not in adios_file.io.AvailableVariables().keys(): 

447 raise KeyError(f"Mesh coordinates not found at {prefix}Points in {filename}") 

448 geometry = adios_file.io.InquireVariable(f"{prefix}Points") 

449 x_shape = geometry.Shape() 

450 geometry_range = compute_local_range(comm, x_shape[0]) 

451 geometry.SetSelection( 

452 [ 

453 [geometry_range[0], 0], 

454 [geometry_range[1] - geometry_range[0], x_shape[1]], 

455 ] 

456 ) 

457 mesh_geometry = np.empty( 

458 (geometry_range[1] - geometry_range[0], x_shape[1]), 

459 dtype=adios_to_numpy_dtype[geometry.Type()], 

460 ) 

461 adios_file.file.Get(geometry, mesh_geometry, adios2.Mode.Deferred) 

462 adios_file.file.PerformGets() 

463 adios_file.file.EndStep() 

464 

465 if read_from_partition: 

466 partition_graph = read_adjacency_list( 

467 adios, 

468 comm, 

469 filename, 

470 f"{prefix}PartitioningData", 

471 f"{prefix}PartitioningOffset", 

472 backend_args["engine"], 

473 ) 

474 else: 

475 partition_graph = None 

476 

477 return ReadMeshData( 

478 cells=mesh_topology, 

479 cell_type=cell_type, 

480 x=mesh_geometry, 

481 degree=degree, 

482 lvar=lvar, 

483 partition_graph=partition_graph, 

484 ) 

485 

486 

487def write_meshtags( 

488 filename: str | Path, 

489 comm: MPI.Comm, 

490 data: MeshTagsData, 

491 backend_args: dict[str, Any] | None = None, 

492): 

493 """Write mesh tags to file. 

494 

495 Args: 

496 filename: Path to file to write to 

497 comm: MPI communicator used in storage 

498 data: Internal data structure for the mesh tags to save to file 

499 backend_args: Arguments to backend 

500 """ 

501 backend_args = {} if backend_args is None else backend_args 

502 io_name = backend_args.get("io_name", "MeshTagWriter") 

503 engine = backend_args.get("engine", "BP4") 

504 prefix = mesh_prefix(backend_args) 

505 adios = adios2.ADIOS(comm) 

506 with ADIOSFile( 

507 adios=adios, 

508 filename=filename, 

509 mode=adios2.Mode.Append, 

510 engine=engine, 

511 io_name=io_name, 

512 ) as adios_file: 

513 adios_file.file.BeginStep() 

514 

515 # Write meshtag topology 

516 topology_var = adios_file.io.DefineVariable( 

517 f"{prefix}{data.name}_topology", 

518 data.indices, 

519 shape=[data.num_entities_global, data.num_dofs_per_entity], 

520 start=[data.local_start, 0], 

521 count=[len(data.indices), data.num_dofs_per_entity], 

522 ) 

523 adios_file.file.Put(topology_var, data.indices, adios2.Mode.Sync) 

524 

525 # Write meshtag values 

526 vals = np.array(data.values) 

527 values_var = adios_file.io.DefineVariable( 

528 f"{prefix}{data.name}_values", 

529 vals, 

530 shape=[data.num_entities_global], 

531 start=[data.local_start], 

532 count=[len(data.indices)], 

533 ) 

534 adios_file.file.Put(values_var, vals, adios2.Mode.Sync) 

535 

536 # Write meshtag dim 

537 adios_file.io.DefineAttribute( 

538 f"{prefix}{data.name}_dim", np.array([data.dim], dtype=np.uint8) 

539 ) 

540 adios_file.file.PerformPuts() 

541 adios_file.file.EndStep() 

542 

543 

544def read_meshtags_data( 

545 filename: str | Path, comm: MPI.Comm, name: str, backend_args: dict[str, Any] | None = None 

546) -> MeshTagsData: 

547 """Read mesh tags from file. 

548 

549 Args: 

550 filename: Path to file to read from 

551 comm: MPI communicator used in storage 

552 name: Name of the mesh tags to read 

553 backend_args: Arguments to backend 

554 

555 Returns: 

556 Internal data structure for the mesh tags read from file 

557 """ 

558 

559 adios = adios2.ADIOS(comm) 

560 backend_args = get_default_backend_args(backend_args) 

561 io_name = backend_args.get("io_name", "MeshTagsReader") 

562 engine = backend_args["engine"] 

563 legacy = backend_args["legacy"] 

564 prefix = mesh_prefix(backend_args) 

565 with ADIOSFile( 

566 adios=adios, 

567 filename=filename, 

568 mode=adios2.Mode.Read, 

569 engine=engine, 

570 io_name=io_name, 

571 ) as adios_file: 

572 if not legacy: 

573 # Get mesh cell type 

574 dim_attr_name = f"{prefix}{name}_dim" 

575 step = 0 

576 for i in range(adios_file.file.Steps()): 

577 adios_file.file.BeginStep() 

578 if dim_attr_name in adios_file.io.AvailableAttributes().keys(): 

579 step = i 

580 break 

581 adios_file.file.EndStep() 

582 if dim_attr_name not in adios_file.io.AvailableAttributes().keys(): 

583 raise KeyError(f"{dim_attr_name} not found in {filename}") 

584 

585 m_dim = adios_file.io.InquireAttribute(dim_attr_name) 

586 dim = int(m_dim.Data()[0]) 

587 

588 # Get mesh tags entites 

589 topology_name = f"{prefix}{name}_topology" 

590 for i in range(step, adios_file.file.Steps()): 

591 if i > step: 

592 adios_file.file.BeginStep() 

593 if topology_name in adios_file.io.AvailableVariables().keys(): 

594 break 

595 adios_file.file.EndStep() 

596 if topology_name not in adios_file.io.AvailableVariables().keys(): 

597 raise KeyError(f"{topology_name} not found in {filename}") 

598 

599 topology = adios_file.io.InquireVariable(topology_name) 

600 top_shape = topology.Shape() 

601 topology_range = compute_local_range(comm, top_shape[0]) 

602 

603 topology.SetSelection( 

604 [ 

605 [topology_range[0], 0], 

606 [topology_range[1] - topology_range[0], top_shape[1]], 

607 ] 

608 ) 

609 mesh_entities = np.empty( 

610 (topology_range[1] - topology_range[0], top_shape[1]), dtype=np.int64 

611 ) 

612 adios_file.file.Get(topology, mesh_entities, adios2.Mode.Deferred) 

613 

614 # Get mesh tags values 

615 values_name = f"{prefix}{name}_values" 

616 if values_name not in adios_file.io.AvailableVariables().keys(): 

617 raise KeyError(f"{values_name} not found") 

618 

619 values = adios_file.io.InquireVariable(values_name) 

620 val_shape = values.Shape() 

621 assert val_shape[0] == top_shape[0] 

622 values.SetSelection([[topology_range[0]], [topology_range[1] - topology_range[0]]]) 

623 tag_values = np.empty( 

624 (topology_range[1] - topology_range[0]), dtype=values.Type().strip("_t") 

625 ) 

626 adios_file.file.Get(values, tag_values, adios2.Mode.Deferred) 

627 

628 adios_file.file.PerformGets() 

629 adios_file.file.EndStep() 

630 else: 

631 # Get mesh cell type 

632 dim_attr_name = f"{name}_dim" 

633 assert adios_file.file.Steps() == 0 

634 if (ct_key := f"/{name}/topology/celltype") in adios_file.io.AvailableAttributes(): 

635 cell_type = adios_file.io.InquireAttribute(ct_key) 

636 else: 

637 raise ValueError(f"Celltype not found for meshtags {name} in {filename}.") 

638 dim = dolfinx.mesh.cell_dim(dolfinx.mesh.to_type(cell_type.DataString()[0])) 

639 

640 # Get mesh tags entites 

641 if (top_key := f"/{name}/topology") in adios_file.io.AvailableVariables(): 

642 topology = adios_file.io.InquireVariable(top_key) 

643 else: 

644 raise ValueError(f"Topology not found for meshtags {name} in {filename}.") 

645 

646 top_shape = topology.Shape() 

647 topology_range = compute_local_range(comm, top_shape[0]) 

648 

649 topology.SetSelection( 

650 [ 

651 [topology_range[0], 0], 

652 [topology_range[1] - topology_range[0], top_shape[1]], 

653 ] 

654 ) 

655 mesh_entities = np.empty( 

656 (topology_range[1] - topology_range[0], top_shape[1]), dtype=np.int64 

657 ) 

658 adios_file.file.Get(topology, mesh_entities, adios2.Mode.Deferred) 

659 

660 # Get mesh tags values 

661 if (val_key := f"/{name}/values") in adios_file.io.AvailableVariables(): 

662 values = adios_file.io.InquireVariable(val_key) 

663 else: 

664 raise ValueError(f"Values not found for meshtags {name} in {filename}.") 

665 

666 val_shape = values.Shape() 

667 assert val_shape[0] == top_shape[0] 

668 

669 values.SetSelection([[topology_range[0]], [topology_range[1] - topology_range[0]]]) 

670 tag_values = np.empty( 

671 (topology_range[1] - topology_range[0]), dtype=values.Type().strip("_t") 

672 ) 

673 adios_file.file.Get(values, tag_values, adios2.Mode.Deferred) 

674 

675 adios_file.file.PerformGets() 

676 adios_file.file.EndStep() 

677 

678 # Tag values are int32 unless the caller asks for something wider. The 

679 # submesh post codes are global cell indices, which need int64. 

680 values_dtype = np.dtype(backend_args.get("values_dtype", np.int32)) 

681 return MeshTagsData( 

682 name=name, values=tag_values.astype(values_dtype), indices=mesh_entities, dim=dim 

683 ) 

684 

685 

686def read_dofmap( 

687 filename: str | Path, comm: MPI.Comm, name: str, backend_args: dict[str, Any] | None = None 

688) -> dolfinx.graph.AdjacencyList: 

689 """Read the dofmap of a function with a given name. 

690 

691 Args: 

692 filename: Path to file to read from 

693 comm: MPI communicator used in storage 

694 name: Name of the function to read the dofmap for 

695 backend_args: Arguments to backend 

696 

697 Returns: 

698 Dofmap as an AdjacencyList 

699 """ 

700 backend_args = {} if backend_args is None else backend_args 

701 prefix = mesh_prefix(backend_args) 

702 

703 # Handles legacy io4dolfinx files, modern files, and custom location of dofmap. 

704 legacy = backend_args.get("legacy", False) 

705 xdofmap_path: str | None 

706 dofmap_path: str | None 

707 if (dofmap_path := backend_args.get("dofmap", None)) is None: 

708 if legacy: 

709 dofmap_path = "Dofmap" 

710 else: 

711 dofmap_path = f"{prefix}{name}_dofmap" 

712 

713 if (xdofmap_path := backend_args.get("offsets", None)) is None: 

714 if legacy: 

715 xdofmap_path = "XDofmap" 

716 else: 

717 xdofmap_path = f"{prefix}{name}_XDofmap" 

718 

719 engine = backend_args.get("engine", "BP4") 

720 

721 adios = adios2.ADIOS(comm) 

722 check_file_exists(filename) 

723 assert isinstance(xdofmap_path, str) 

724 return read_adjacency_list(adios, comm, filename, dofmap_path, xdofmap_path, engine=engine) 

725 

726 

727def read_dofs( 

728 filename: str | Path, 

729 comm: MPI.Comm, 

730 name: str, 

731 time: float, 

732 backend_args: dict[str, Any] | None = None, 

733) -> tuple[npt.NDArray[np.float32 | np.float64 | np.complex64 | np.complex128], int]: 

734 """Read the dofs (values) of a function with a given name from a given timestep. 

735 

736 Args: 

737 filename: Path to file to read from 

738 comm: MPI communicator used in storage 

739 name: Name of the function to read the dofs for 

740 time: Time stamp associated with the function to read 

741 backend_args: Arguments to backend 

742 

743 Returns: 

744 Contiguous sequence of degrees of freedom (with respect to input data) 

745 and the global starting point on the process. 

746 Process 0 has [0, M), process 1 [M, N), process 2 [N, O) etc. 

747 """ 

748 

749 backend_args = {} if backend_args is None else backend_args 

750 legacy = backend_args.get("legacy", False) 

751 engine = backend_args.get("engine", "BP4") 

752 io_name = backend_args.get("io_name", f"{name}_FunctionReader") 

753 prefix = mesh_prefix(backend_args) 

754 # Check that file contains the function to read 

755 adios = adios2.ADIOS(comm) 

756 check_file_exists(filename) 

757 

758 if not legacy: 

759 with ADIOSFile( 

760 adios=adios, 

761 filename=filename, 

762 mode=adios2.Mode.Read, 

763 engine=engine, 

764 io_name=io_name, 

765 ) as adios_file: 

766 variables = set( 

767 sorted( 

768 map( 

769 lambda x: x[len(prefix) :].split("_time")[0], 

770 filter( 

771 lambda x: x.startswith(prefix) and x.endswith("_time"), 

772 adios_file.io.AvailableVariables(), 

773 ), 

774 ) 

775 ) 

776 ) 

777 if name not in variables: 

778 raise KeyError(f"{name} not found in {filename}. Did you mean one of {variables}?") 

779 

780 if legacy: 

781 array_path = "Values" 

782 else: 

783 array_path = f"{prefix}{name}_values" 

784 

785 time_name = f"{prefix}{name}_time" 

786 return read_array(adios, filename, array_path, engine, comm, time, time_name, legacy=legacy) 

787 

788 

789def read_cell_perms( 

790 comm: MPI.Comm, filename: Path | str, backend_args: dict[str, Any] | None = None 

791) -> npt.NDArray[np.uint32]: 

792 """ 

793 Read cell permutation from file with given communicator, 

794 Split in continuous chunks based on number of cells in the mesh (global). 

795 

796 Args: 

797 adios: The ADIOS instance 

798 comm: The MPI communicator used to read the data 

799 filename: Path to input file 

800 variable: Name of cell-permutation variable 

801 num_cells_global: Number of cells in the mesh (global) 

802 engine: Type of ADIOS engine to use for reading data 

803 

804 Returns: 

805 Cell-permutations local to the process 

806 

807 .. note:: 

808 No MPI communication is done during this call 

809 """ 

810 adios = adios2.ADIOS(comm) 

811 check_file_exists(filename) 

812 

813 # Open ADIOS engine 

814 backend_args = {} if backend_args is None else backend_args 

815 engine = backend_args.get("engine", "BP4") 

816 prefix = mesh_prefix(backend_args) 

817 

818 cell_perms, _ = read_array( 

819 adios, filename, f"{prefix}CellPermutations", engine=engine, comm=comm, legacy=True 

820 ) 

821 

822 return cell_perms.astype(np.uint32) 

823 

824 

825def read_hdf5_array( 

826 comm: MPI.Comm, 

827 filename: Path | str, 

828 group: str, 

829 backend_args: dict[str, Any] | None = None, 

830) -> tuple[np.ndarray, int]: 

831 adios = adios2.ADIOS(comm) 

832 """Read an array from an HDF5 file. 

833 

834 Args: 

835 comm: MPI communicator used in storage 

836 filename: Path to file to read from 

837 group: Group in HDF5 file where array is stored 

838 backend_args: Arguments to backend 

839 

840 Returns: 

841 Tuple containing: 

842 - Numpy array read from file 

843 - Global starting point on the process. 

844 Process 0 has [0, M), process 1 [M, N), process 2 [N, O) etc. 

845 """ 

846 return read_array(adios, filename, group, engine="HDF5", comm=comm, legacy=True) 

847 

848 

849def write_function( 

850 filename: Path, 

851 comm: MPI.Comm, 

852 u: FunctionData, 

853 time: float = 0.0, 

854 mode: FileMode = FileMode.append, 

855 backend_args: dict[str, Any] | None = None, 

856): 

857 """ 

858 Write a function to file using ADIOS2 

859 

860 Args: 

861 comm: MPI communicator used in storage 

862 u: Internal data structure for the function data to save to file 

863 filename: Path to file to write to 

864 engine: ADIOS2 engine to use 

865 mode: ADIOS2 mode to use (write or append) 

866 time: Time stamp associated with function 

867 io_name: Internal name used for the ADIOS IO object 

868 """ 

869 adios_mode = convert_file_mode(mode) 

870 backend_args = get_default_backend_args(backend_args) 

871 engine = backend_args["engine"] 

872 io_name = backend_args.get("io_name", "{name}_writer") 

873 prefix = mesh_prefix(backend_args) 

874 

875 adios = adios2.ADIOS(comm) 

876 cell_permutations_exists = False 

877 dofmap_exists = False 

878 XDofmap_exists = False 

879 if adios_mode == adios2.Mode.Append: 

880 cell_permutations_exists = check_variable_exists( 

881 adios, filename, f"{prefix}CellPermutations", engine=engine 

882 ) 

883 dofmap_exists = check_variable_exists( 

884 adios, filename, f"{prefix}{u.name}_dofmap", engine=engine 

885 ) 

886 XDofmap_exists = check_variable_exists( 

887 adios, filename, f"{prefix}{u.name}_XDofmap", engine=engine 

888 ) 

889 

890 with ADIOSFile( 

891 adios=adios, filename=filename, mode=adios_mode, engine=engine, io_name=io_name, comm=comm 

892 ) as adios_file: 

893 adios_file.file.BeginStep() 

894 

895 if not cell_permutations_exists: 

896 # Add mesh permutations. These belong to the mesh, not the function, 

897 # so they are shared by every function on that mesh. 

898 pvar = adios_file.io.DefineVariable( 

899 f"{prefix}CellPermutations", 

900 u.cell_permutations, 

901 shape=[u.num_cells_global], 

902 start=[u.local_cell_range[0]], 

903 count=[u.local_cell_range[1] - u.local_cell_range[0]], 

904 ) 

905 adios_file.file.Put(pvar, u.cell_permutations) 

906 

907 if not dofmap_exists: 

908 # Add dofmap 

909 dofmap_var = adios_file.io.DefineVariable( 

910 f"{prefix}{u.name}_dofmap", 

911 u.dofmap_array, 

912 shape=[u.global_dofs_in_dofmap], 

913 start=[u.dofmap_range[0]], 

914 count=[u.dofmap_range[1] - u.dofmap_range[0]], 

915 ) 

916 adios_file.file.Put(dofmap_var, u.dofmap_array) 

917 

918 if not XDofmap_exists: 

919 # Add XDofmap 

920 xdofmap_var = adios_file.io.DefineVariable( 

921 f"{prefix}{u.name}_XDofmap", 

922 u.dofmap_offsets, 

923 shape=[u.num_cells_global + 1], 

924 start=[u.local_cell_range[0]], 

925 count=[u.local_cell_range[1] - u.local_cell_range[0] + 1], 

926 ) 

927 adios_file.file.Put(xdofmap_var, u.dofmap_offsets) 

928 

929 val_var = adios_file.io.DefineVariable( 

930 f"{prefix}{u.name}_values", 

931 u.values, 

932 shape=[u.num_dofs_global], 

933 start=[u.dof_range[0]], 

934 count=[u.dof_range[1] - u.dof_range[0]], 

935 ) 

936 adios_file.file.Put(val_var, u.values) 

937 

938 # Add time step to file 

939 t_arr = np.array([time], dtype=np.float64) 

940 time_var = adios_file.io.DefineVariable( 

941 f"{prefix}{u.name}_time", 

942 t_arr, 

943 shape=[1], 

944 start=[0], 

945 count=[1 if comm.rank == 0 else 0], 

946 ) 

947 adios_file.file.Put(time_var, t_arr) 

948 adios_file.file.PerformPuts() 

949 adios_file.file.EndStep() 

950 

951 

952def read_legacy_mesh( 

953 filename: Path | str, comm: MPI.Comm, group: str 

954) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.floating], str | None]: 

955 """Read in the mesh topology, geometry and (optionally) cell type from a 

956 legacy DOLFIN HDF5-file. 

957 

958 Args: 

959 filename: Path to file to read from 

960 comm: MPI communicator used in storage 

961 group: Group in HDF5 file where mesh is stored 

962 

963 Returns: 

964 Tuple containing: 

965 - Topology as a (num_cells, num_vertices_per_cell) array of global vertex indices 

966 - Geometry as a (num_vertices, geometric_dimension) array of vertex coordinates 

967 - Cell type as a string (e.g. "tetrahedron") or None if not found 

968 """ 

969 # Create ADIOS2 reader 

970 adios = adios2.ADIOS(comm) 

971 with ADIOSFile( 

972 adios=adios, 

973 filename=filename, 

974 mode=adios2.Mode.Read, 

975 io_name="Mesh reader", 

976 engine="HDF5", 

977 ) as adios_file: 

978 # Get mesh topology (distributed) 

979 if f"{group}/topology" not in adios_file.io.AvailableVariables().keys(): 

980 raise KeyError(f"Mesh topology not found at '{group}/topology'") 

981 topology = adios_file.io.InquireVariable(f"{group}/topology") 

982 shape = topology.Shape() 

983 local_range = compute_local_range(comm, shape[0]) 

984 topology.SetSelection([[local_range[0], 0], [local_range[1] - local_range[0], shape[1]]]) 

985 

986 mesh_topology = np.empty( 

987 (local_range[1] - local_range[0], shape[1]), 

988 dtype=topology.Type().strip("_t"), 

989 ) 

990 adios_file.file.Get(topology, mesh_topology, adios2.Mode.Sync) 

991 

992 # Get mesh cell type 

993 cell_type = None 

994 if f"{group}/topology/celltype" in adios_file.io.AvailableAttributes().keys(): 

995 celltype = adios_file.io.InquireAttribute(f"{group}/topology/celltype") 

996 cell_type = celltype.DataString()[0] 

997 

998 # Get mesh geometry 

999 

1000 for geometry_key in [f"{group}/geometry", f"{group}/coordinates"]: 

1001 if geometry_key in adios_file.io.AvailableVariables().keys(): 

1002 break 

1003 else: 

1004 raise KeyError( 

1005 f"Mesh coordinates not found at '{group}/coordinates' or '{group}/geometry'" 

1006 ) 

1007 

1008 geometry = adios_file.io.InquireVariable(geometry_key) 

1009 shape = geometry.Shape() 

1010 local_range = compute_local_range(comm, shape[0]) 

1011 geometry.SetSelection([[local_range[0], 0], [local_range[1] - local_range[0], shape[1]]]) 

1012 mesh_geometry = np.empty( 

1013 (local_range[1] - local_range[0], shape[1]), 

1014 dtype=adios_to_numpy_dtype[geometry.Type()], 

1015 ) 

1016 adios_file.file.Get(geometry, mesh_geometry, adios2.Mode.Sync) 

1017 

1018 return mesh_topology, mesh_geometry, cell_type 

1019 

1020 

1021def snapshot_checkpoint( 

1022 filename: Path | str, 

1023 mode: FileMode, 

1024 u: dolfinx.fem.Function, 

1025 backend_args: dict[str, Any] | None, 

1026): 

1027 """Create a snapshot checkpoint of a dolfinx function. 

1028 

1029 Args: 

1030 filename: Path to file to read from 

1031 mode: File-mode to store the function 

1032 u: dolfinx function to create a snapshot checkpoint for 

1033 backend_args: Arguments to backend 

1034 """ 

1035 adios_mode = convert_file_mode(mode) 

1036 adios = adios2.ADIOS(u.function_space.mesh.comm) 

1037 backend_args = {} if backend_args is None else backend_args 

1038 io_name = backend_args.get("io_name", "SnapshotCheckPoint") 

1039 engine = backend_args.get("engine", "BP4") 

1040 with ADIOSFile( 

1041 adios=adios, 

1042 filename=filename, 

1043 mode=adios_mode, 

1044 io_name=io_name, 

1045 engine=engine, 

1046 ) as adios_file: 

1047 if adios_mode == adios2.Mode.Write: 

1048 dofmap = u.function_space.dofmap 

1049 num_dofs_local = dofmap.index_map.size_local * dofmap.index_map_bs 

1050 local_dofs = u.x.array[:num_dofs_local].copy() 

1051 

1052 # Write to file 

1053 adios_file.file.BeginStep() 

1054 dofs = adios_file.io.DefineVariable("dofs", local_dofs, count=[num_dofs_local]) 

1055 adios_file.file.Put(dofs, local_dofs, adios2.Mode.Sync) 

1056 adios_file.file.EndStep() 

1057 elif adios_mode == adios2.Mode.Read: 

1058 adios_file.file.BeginStep() 

1059 in_variable = adios_file.io.InquireVariable("dofs") 

1060 in_variable.SetBlockSelection(u.function_space.mesh.comm.rank) 

1061 adios_file.file.Get(in_variable, u.x.array, adios2.Mode.Sync) 

1062 adios_file.file.EndStep() 

1063 u.x.scatter_forward() 

1064 else: 

1065 raise NotImplementedError(f"Mode {mode} is not implemented for snapshot checkpoint") 

1066 

1067 

1068def read_function_names( 

1069 filename: Path | str, comm: MPI.Comm, backend_args: dict[str, Any] | None 

1070) -> list[str]: 

1071 """Read all function names from a file. 

1072 

1073 Args: 

1074 filename: Path to file 

1075 comm: MPI communicator to launch IO on. 

1076 backend_args: Arguments to backend 

1077 

1078 Returns: 

1079 A list of function names. 

1080 """ 

1081 raise NotImplementedError("Reading function names are not implemented with ADIOS2") 

1082 

1083 

1084def read_point_data( 

1085 filename: Path | str, 

1086 name: str, 

1087 comm: MPI.Comm, 

1088 time: float | str | None, 

1089 backend_args: dict[str, Any] | None, 

1090) -> tuple[np.ndarray, int]: 

1091 """Read data from the nodes of a mesh. 

1092 

1093 Args: 

1094 filename: Path to file 

1095 name: Name of point data 

1096 comm: Communicator to launch IO on. 

1097 time: The time stamp 

1098 backend_args: The backend arguments 

1099 

1100 Returns: 

1101 Data local to process (contiguous, no mpi comm) and local start range 

1102 """ 

1103 raise NotImplementedError("The ADIOS2 backend cannot read point data.") 

1104 

1105 

1106def read_cell_data( 

1107 filename: Path | str, 

1108 name: str, 

1109 comm: MPI.Comm, 

1110 time: str | float | None, 

1111 backend_args: dict[str, Any] | None, 

1112) -> tuple[npt.NDArray[np.int64], np.ndarray]: 

1113 """Read data from the cells of a mesh. 

1114 

1115 Args: 

1116 filename: Path to file 

1117 name: Name of point data 

1118 comm: Communicator to launch IO on. 

1119 time: The time stamp 

1120 backend_args: The backend arguments 

1121 Returns: 

1122 A tuple (topology, dofs) where topology contains the 

1123 vertex indices of the cells, dofs the degrees of 

1124 freedom within that cell. 

1125 """ 

1126 raise NotImplementedError("The ADIOS2 backend does not support reading cell data.") 

1127 

1128 

1129def write_data( 

1130 filename: Path | str, 

1131 array_data: ArrayData, 

1132 comm: MPI.Comm, 

1133 time: str | float | None, 

1134 mode: FileMode, 

1135 backend_args: dict[str, Any] | None, 

1136): 

1137 """Write a 2D-array to file (distributed across proceses with MPI). 

1138 

1139 

1140 Args: 

1141 filename: Path to file 

1142 array_data: Data to write to file 

1143 comm: MPI communicator to open the file with. 

1144 time: Time stamp 

1145 mode: Append or write 

1146 backend_args: The backend arguments 

1147 """ 

1148 raise NotImplementedError("ADIOS2 has not implemented this yet")