Coverage for /dolfinx-env/lib/python3.14/site-packages/io4dolfinx/readers.py: 97%

202 statements  

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

1# Copyright (C) 2023 Jørgen Schartum Dokken 

2# 

3# This file is part of io4dolfinx 

4# 

5# SPDX-License-Identifier: MIT 

6 

7from __future__ import annotations 

8 

9import logging 

10import pathlib 

11import typing 

12from pathlib import Path 

13from typing import Any 

14 

15from mpi4py import MPI 

16 

17import basix 

18import dolfinx 

19import fenicsx_compat 

20import numpy as np 

21import numpy.typing as npt 

22import ufl 

23 

24from .backends import ReadMode, get_backend 

25from .comm_helpers import all_to_all, send_dofs_and_recv_values 

26from .utils import ( 

27 check_file_exists, 

28 compute_dofmap_pos, 

29 compute_insert_position, 

30 compute_local_range, 

31 index_owner, 

32) 

33 

34__all__ = ["read_mesh_from_legacy_h5", "read_function_from_legacy_h5", "read_point_data"] 

35logger = logging.getLogger(__name__) 

36 

37 

38def _with_mesh_name( 

39 backend_args: dict[str, Any] | None, mesh_name: str | None 

40) -> dict[str, Any] | None: 

41 """Select which mesh in the file a backend call refers to. 

42 

43 See :func:`io4dolfinx.checkpointing._with_mesh_name`; duplicated here to keep 

44 :mod:`io4dolfinx.readers` free of a circular import. 

45 """ 

46 if mesh_name is None: 

47 return backend_args 

48 args = dict(backend_args) if backend_args else {} 

49 args["name"] = mesh_name 

50 return args 

51 

52 

53def map_dofmap(dofmap: dolfinx.graph.AdjacencyList, bs: int | np.int64) -> npt.NDArray[np.int64]: 

54 """ 

55 Map xxxyyyzzz to xyzxyz 

56 """ 

57 

58 in_dofmap = dofmap.array 

59 in_offsets = dofmap.offsets 

60 

61 mapped_dofmap = np.empty_like(in_dofmap) 

62 for i in range(len(in_offsets) - 1): 

63 pos_begin, pos_end = ( 

64 in_offsets[i] - in_offsets[0], 

65 in_offsets[i + 1] - in_offsets[0], 

66 ) 

67 dofs_i = in_dofmap[pos_begin:pos_end] 

68 assert (pos_end - pos_begin) % bs == 0 

69 num_dofs_local = int((pos_end - pos_begin) // bs) 

70 for k in range(bs): 

71 for j in range(num_dofs_local): 

72 mapped_dofmap[int(pos_begin + j * bs + k)] = dofs_i[int(num_dofs_local * k + j)] 

73 return mapped_dofmap.astype(np.int64) 

74 

75 

76def send_cells_and_receive_dofmap_index( 

77 filename: pathlib.Path, 

78 comm: MPI.Comm, 

79 source_ranks: npt.NDArray[np.int32], 

80 dest_ranks: npt.NDArray[np.int32], 

81 dest_size: npt.NDArray[np.int32], 

82 output_owners: npt.NDArray[np.int32], 

83 input_cells: npt.NDArray[np.int64], 

84 dofmap_pos: npt.NDArray[np.int32], 

85 num_cells_global: int | np.int64, 

86 dofmap_path: str, 

87 xdofmap_path: str, 

88 bs: int | np.int64, 

89 backend: str | None, 

90) -> npt.NDArray[np.int64]: 

91 """ 

92 Given a set of positions in input dofmap, give the global input index of this dofmap entry 

93 in input file. 

94 """ 

95 check_file_exists(filename) 

96 

97 recv_size = np.zeros(len(source_ranks), dtype=np.int32) 

98 assert isinstance(comm, MPI.Intracomm) 

99 mesh_to_data_comm = comm.Create_dist_graph_adjacent( 

100 source_ranks.tolist(), dest_ranks.tolist(), reorder=False 

101 ) 

102 # Send sizes to create data structures for receiving from NeighAlltoAllv 

103 all_to_all(mesh_to_data_comm, dest_size, recv_size) 

104 

105 # Sort output for sending and fill send data 

106 out_cells = np.zeros(len(output_owners), dtype=np.int64) 

107 out_pos = np.zeros(len(output_owners), dtype=np.int32) 

108 proc_to_dof = np.zeros_like(input_cells, dtype=np.int32) 

109 insertion_array = compute_insert_position(output_owners, dest_ranks, dest_size) 

110 out_cells[insertion_array] = input_cells 

111 out_pos[insertion_array] = dofmap_pos 

112 proc_to_dof[insertion_array] = np.arange(len(input_cells), dtype=np.int32) 

113 del insertion_array 

114 

115 # Prepare data-structures for receiving 

116 total_incoming = sum(recv_size) 

117 inc_cells = np.zeros(total_incoming, dtype=np.int64) 

118 inc_pos = np.zeros(total_incoming, dtype=np.intc) 

119 

120 # Send data 

121 s_msg = [out_cells, dest_size, MPI.INT64_T] 

122 r_msg = [inc_cells, recv_size, MPI.INT64_T] 

123 mesh_to_data_comm.Neighbor_alltoallv(s_msg, r_msg) 

124 

125 s_msg = [out_pos, dest_size, MPI.INT32_T] 

126 r_msg = [inc_pos, recv_size, MPI.INT32_T] 

127 mesh_to_data_comm.Neighbor_alltoallv(s_msg, r_msg) 

128 mesh_to_data_comm.Free() 

129 

130 backend_cls = get_backend(backend) 

131 # Read dofmap from file 

132 backend_args = {"dofmap": dofmap_path, "offsets": xdofmap_path} 

133 if backend == "adios2": 

134 backend_args.update({"engine": "HDF5"}) 

135 input_dofs = backend_cls.read_dofmap(filename, comm, name="", backend_args=backend_args) 

136 # Map to xyz 

137 mapped_dofmap = map_dofmap(input_dofs, bs).astype(np.int64) 

138 

139 # Extract dofmap data 

140 local_cell_range = compute_local_range(comm, num_cells_global) 

141 input_cell_positions = inc_cells - local_cell_range[0] 

142 in_offsets = input_dofs.offsets 

143 read_pos = (in_offsets[input_cell_positions] + inc_pos - in_offsets[0]).astype(np.int32) 

144 input_dofs = mapped_dofmap[read_pos] 

145 del input_cell_positions, read_pos 

146 

147 # Send input dofs back to owning process 

148 data_to_mesh_comm = comm.Create_dist_graph_adjacent( 

149 dest_ranks.tolist(), source_ranks.tolist(), reorder=False 

150 ) 

151 

152 incoming_global_dofs = np.zeros(sum(dest_size), dtype=np.int64) 

153 s_msg = [input_dofs, recv_size, MPI.INT64_T] 

154 r_msg = [incoming_global_dofs, dest_size, MPI.INT64_T] 

155 data_to_mesh_comm.Neighbor_alltoallv(s_msg, r_msg) 

156 

157 # Sort incoming global dofs as they were inputted 

158 sorted_global_dofs = np.zeros_like(incoming_global_dofs, dtype=np.int64) 

159 assert len(incoming_global_dofs) == len(input_cells) 

160 sorted_global_dofs[proc_to_dof] = incoming_global_dofs 

161 data_to_mesh_comm.Free() 

162 return sorted_global_dofs 

163 

164 

165def read_mesh_from_legacy_h5( 

166 filename: pathlib.Path, 

167 comm: MPI.Comm, 

168 group: str, 

169 cell_type: str = "tetrahedron", 

170 backend: str | None = None, 

171 max_facet_to_cell_links: int = 2, 

172) -> dolfinx.mesh.Mesh: 

173 """ 

174 Read mesh from `h5`-file generated by legacy DOLFIN `HDF5File.write` or `XDMF.write_checkpoint`. 

175 

176 Args: 

177 comm: MPI communicator to distribute mesh over 

178 filename: Path to `h5` or `xdmf` file 

179 group: Name of mesh in `h5`-file 

180 cell_type: What type of cell type, by default tetrahedron. 

181 backend: The IO backend to use when reading the mesh (must 

182 support legacy mesh reading, e.g., "adios2"). 

183 max_facet_to_cell_links: Maximum number of cells a facet 

184 can be connected to. 

185 """ 

186 logger.debug(f"Reading mesh from {filename} at group {group}") 

187 logger.debug(f"Using backend {backend} with max_facet_to_cell_links {max_facet_to_cell_links}") 

188 # Make sure we use the HDF5File and check that the file is present 

189 check_file_exists(filename) 

190 

191 backend_cls = get_backend(backend) 

192 mesh_topology, mesh_geometry, ct = backend_cls.read_legacy_mesh(filename, comm, group) 

193 if ct is not None: 

194 cell_type = ct 

195 # Create DOLFINx mesh 

196 element = basix.ufl.element( 

197 basix.ElementFamily.P, 

198 cell_type, 

199 1, 

200 basix.LagrangeVariant.equispaced, 

201 shape=(mesh_geometry.shape[1],), 

202 ) 

203 domain = ufl.Mesh(element) 

204 

205 return dolfinx.mesh.create_mesh( 

206 comm=MPI.COMM_WORLD, 

207 cells=mesh_topology, 

208 x=mesh_geometry, 

209 e=domain, 

210 partitioner=None, 

211 max_facet_to_cell_links=max_facet_to_cell_links, 

212 ) 

213 

214 

215def read_function_from_legacy_h5( 

216 filename: pathlib.Path, 

217 comm: MPI.Comm, 

218 u: dolfinx.fem.Function, 

219 group: str = "mesh", 

220 step: typing.Optional[int] = None, 

221 vector_group: str | None = None, 

222 backend: str | None = None, 

223): 

224 """ 

225 Read function from a `h5`-file generated by legacy DOLFIN `HDF5File.write` 

226 or `XDMF.write_checkpoint`. 

227 

228 

229 Args: 

230 comm : MPI communicator to distribute mesh over 

231 filename : Path to `h5` or `xdmf` file 

232 u : The function used to stored the read values 

233 group : Group within the `h5` file where the function is stored, by default "mesh" 

234 step : The time step used when saving the checkpoint. If not provided it will assume that 

235 the function is saved as a regular function (i.e with `HDF5File.write`) 

236 backend: The IO backend 

237 """ 

238 logger.debug(f"Reading function from {filename} at group {group}") 

239 logger.debug(f"Using backend {backend} with group {group} and step {step}") 

240 # Make sure we use the HDF5File and check that the file is present 

241 filename = pathlib.Path(filename) 

242 if filename.suffix == ".xdmf": 

243 filename = filename.with_suffix(".h5") 

244 if not filename.is_file(): 

245 raise FileNotFoundError(f"File {filename} does not exist") 

246 

247 V = u.function_space 

248 mesh = u.function_space.mesh 

249 if u.function_space.element.needs_dof_transformations: 

250 raise RuntimeError( 

251 "Function-spaces requiring dof permutations are not compatible with legacy data" 

252 ) 

253 # ----------------------Step 1--------------------------------- 

254 # Compute index of input cells, and position in input dofmap 

255 local_cells, dof_pos = compute_dofmap_pos(u.function_space) 

256 input_cells = mesh.topology.original_cell_index[local_cells] 

257 

258 # Compute mesh->input communicator 

259 # 1.1 Compute mesh->input communicator 

260 num_cells_global = mesh.topology.index_map(mesh.topology.dim).size_global 

261 backend_cls = get_backend(backend) 

262 owners: npt.NDArray[np.int32] 

263 if backend_cls.read_mode == ReadMode.serial: 

264 owners = np.zeros(len(input_cells), dtype=np.int32) 

265 elif backend_cls.read_mode == ReadMode.parallel: 

266 owners = index_owner(V.mesh.comm, input_cells, num_cells_global) 

267 else: 

268 raise NotImplementedError(f"{backend_cls.read_mode} not implemented") 

269 

270 unique_owners, owner_count = np.unique(owners, return_counts=True) 

271 # FIXME: In C++ use NBX to find neighbourhood 

272 assert isinstance(mesh.comm, MPI.Intracomm) 

273 _tmp_comm = mesh.comm.Create_dist_graph( 

274 [mesh.comm.rank], [len(unique_owners)], unique_owners.tolist(), reorder=False 

275 ) 

276 source, dest, _ = _tmp_comm.Get_dist_neighbors() 

277 _tmp_comm.Free() 

278 # Strip out any / 

279 group = group.strip("/") 

280 if step is not None: 

281 group = f"{group}/{group}_{step}" 

282 vector_group = vector_group or "vector" 

283 else: 

284 vector_group = vector_group or "vector_0" 

285 

286 # ----------------------Step 2-------------------------------- 

287 # Get global dofmap indices from input process 

288 bs = V.dofmap.bs 

289 num_cells_global = mesh.topology.index_map(mesh.topology.dim).size_global 

290 dofmap_indices = send_cells_and_receive_dofmap_index( 

291 filename, 

292 comm, 

293 np.asarray(source, dtype=np.int32), 

294 np.asarray(dest, dtype=np.int32), 

295 owner_count.astype(np.int32), 

296 owners, 

297 input_cells, 

298 dof_pos, 

299 num_cells_global, 

300 f"/{group}/cell_dofs", 

301 f"/{group}/x_cell_dofs", 

302 bs, 

303 backend=backend, 

304 ) 

305 

306 # ----------------------Step 3--------------------------------- 

307 dof_owner: npt.NDArray[np.int32] 

308 if backend_cls.read_mode == ReadMode.serial: 

309 dof_owner = np.zeros(len(dofmap_indices), dtype=np.int32) 

310 elif backend_cls.read_mode == ReadMode.parallel: 

311 # Compute owner of global dof on distributed input data 

312 num_dof_global = V.dofmap.index_map_bs * V.dofmap.index_map.size_global 

313 dof_owner = index_owner(comm=mesh.comm, indices=dofmap_indices, N=num_dof_global) 

314 else: 

315 raise NotImplementedError(f"{backend_cls.read_mode} not implemented") 

316 

317 # Create MPI neigh comm to owner. 

318 # NOTE: USE NBX in C++ 

319 

320 # Read input data 

321 local_array, starting_pos = backend_cls.read_hdf5_array( 

322 comm, filename, f"/{group}/{vector_group}", backend_args=None 

323 ) 

324 

325 # Send global dof indices to correct input process, and receive value of given dof 

326 local_values = send_dofs_and_recv_values( 

327 dofmap_indices, dof_owner, comm, local_array, starting_pos 

328 ) 

329 

330 # ----------------------Step 4--------------------------------- 

331 # Populate local part of array and scatter forward 

332 u.x.array[: len(local_values)] = local_values 

333 u.x.scatter_forward() 

334 

335 

336def create_geometry_function_space(mesh: dolfinx.mesh.Mesh, N: int) -> dolfinx.fem.FunctionSpace: 

337 """Reconstruct a vector space with the N components using the geometry dofmap to ensure 

338 a 1-1 mapping between mesh nodes and DOFs.""" 

339 geom_imap = mesh.geometry.index_map() 

340 geom_dofmap = fenicsx_compat.dofmap(mesh) 

341 ufl_domain = mesh.ufl_domain() 

342 assert ufl_domain is not None 

343 sub_el = ufl_domain.ufl_coordinate_element().sub_elements[0] 

344 adj_list = dolfinx.cpp.graph.AdjacencyList_int32(geom_dofmap) 

345 

346 value_shape: tuple[int, ...] 

347 if N == 1: 

348 ufl_el = sub_el 

349 value_shape = () 

350 else: 

351 ufl_el = basix.ufl.blocked_element(sub_el, shape=(N,)) 

352 value_shape = (N,) 

353 

354 _fe_constructor: ( 

355 type[dolfinx.cpp.fem.FiniteElement_float32] | type[dolfinx.cpp.fem.FiniteElement_float64] 

356 ) 

357 _fem_constructor: ( 

358 type[dolfinx.cpp.fem.FunctionSpace_float32] | type[dolfinx.cpp.fem.FunctionSpace_float64] 

359 ) 

360 if ufl_el.dtype == np.float32: 

361 _fe_constructor = dolfinx.cpp.fem.FiniteElement_float32 

362 _fem_constructor = dolfinx.cpp.fem.FunctionSpace_float32 

363 elif ufl_el.dtype == np.float64: 

364 _fe_constructor = dolfinx.cpp.fem.FiniteElement_float64 

365 _fem_constructor = dolfinx.cpp.fem.FunctionSpace_float64 

366 else: 

367 raise RuntimeError(f"Unsupported type {ufl_el.dtype}") 

368 cpp_el = fenicsx_compat.finite_element_ctor_kwargs( 

369 _fe_constructor, 

370 ufl_el.basix_element._e, 

371 gdim=mesh.geometry.dim, 

372 value_shape=value_shape, 

373 ) 

374 dof_layout = dolfinx.cpp.fem.create_element_dof_layout(cpp_el, []) 

375 cpp_dofmap = dolfinx.cpp.fem.DofMap( 

376 dof_layout, fenicsx_compat.unwrap_index_map(geom_imap), N, adj_list, N 

377 ) 

378 

379 # Create function space 

380 cpp_space = _fem_constructor(mesh._cpp_object, cpp_el, cpp_dofmap) # type: ignore[arg-type] 

381 

382 return dolfinx.fem.FunctionSpace(mesh, ufl_el, cpp_space) 

383 

384 

385def read_point_data( 

386 filename: Path | str, 

387 name: str, 

388 mesh: dolfinx.mesh.Mesh, 

389 time: float | None = None, 

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

391 backend: str = "xdmf", 

392 mesh_name: str | None = None, 

393) -> dolfinx.fem.Function: 

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

395 

396 Note: 

397 Backend has to implement :meth:`io4dolfinx.backends.IOBackend.read_point_data`. 

398 

399 Args: 

400 filename: Path to file 

401 name: Name of point data 

402 mesh: The corresponding :py:class:`dolfinx.mesh.Mesh`. 

403 time: Time-step to read from. 

404 mesh_name: Name of the mesh in the file the data belongs to. Defaults 

405 to the mesh written without an explicit name. 

406 

407 Returns: 

408 A function in the space equivalent to the mesh 

409 coordinate element (up to shape). 

410 """ 

411 

412 backend_args = _with_mesh_name(backend_args, mesh_name) 

413 logger.debug(f"Reading point data from {filename} with name {name} at time {time}") 

414 logger.debug(f"Using backend {backend} with arguments {backend_args}") 

415 backend_cls = get_backend(backend) 

416 dataset, local_range_start = backend_cls.read_point_data( 

417 filename=filename, name=name, comm=mesh.comm, time=time, backend_args=backend_args 

418 ) 

419 

420 num_components = dataset.shape[1] 

421 

422 # Create appropriate function space (based on coordinate map) 

423 V = create_geometry_function_space(mesh, num_components) 

424 uh = dolfinx.fem.Function(V, name=name, dtype=dataset.dtype) 

425 # Assume that mesh is first order for now 

426 x_dofmap = fenicsx_compat.dofmap(mesh) 

427 igi = np.array(mesh.geometry.input_global_indices, dtype=np.int64) 

428 

429 # This is dependent on how the data is read in. If distributed equally this is correct 

430 global_geom_input = igi[x_dofmap] 

431 

432 if backend_cls.read_mode == ReadMode.parallel: 

433 num_nodes_global = mesh.geometry.index_map().size_global 

434 global_geom_owner = index_owner(mesh.comm, global_geom_input.reshape(-1), num_nodes_global) 

435 elif backend_cls.read_mode == ReadMode.serial: 

436 # This is correct if everything is read in on rank 0 

437 global_geom_owner = np.zeros(len(global_geom_input.flatten()), dtype=np.int32) 

438 else: 

439 raise NotImplementedError(f"{backend_cls.read_mode} not implemented") 

440 

441 for i in range(num_components): 

442 arr_i = send_dofs_and_recv_values( 

443 global_geom_input.reshape(-1), 

444 global_geom_owner, 

445 mesh.comm, 

446 dataset[:, i], 

447 local_range_start, 

448 ) 

449 dof_pos = x_dofmap.reshape(-1) * num_components + i 

450 uh.x.array[dof_pos] = arr_i 

451 uh.x.scatter_forward() 

452 return uh 

453 

454 

455def read_cell_data( 

456 filename: Path | str, 

457 name: str, 

458 mesh: dolfinx.mesh.Mesh, 

459 time: float | None = None, 

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

461 backend: str = "xdmf", 

462 mesh_name: str | None = None, 

463) -> dolfinx.fem.Function: 

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

465 

466 Note: 

467 Backend has to implement :meth:`io4dolfinx.backends.IOBackend.read_cell_data`. 

468 

469 Args: 

470 filename: Path to file 

471 name: Name of point data 

472 mesh: The corresponding :py:class:`dolfinx.mesh.Mesh`. 

473 time: Time-step to read from. 

474 mesh_name: Name of the mesh in the file the data belongs to. Defaults 

475 to the mesh written without an explicit name. 

476 

477 Returns: 

478 A function in a DG-0 space on the mesh. The cells not found in input is set to zero. 

479 """ 

480 backend_args = _with_mesh_name(backend_args, mesh_name) 

481 

482 backend_cls = get_backend(backend) 

483 

484 topology, dofs = backend_cls.read_cell_data( 

485 filename=filename, name=name, comm=mesh.comm, time=time, backend_args=backend_args 

486 ) 

487 num_components = dofs.shape[1] 

488 shape: tuple[int, ...] 

489 if num_components == 1: 

490 shape = () 

491 else: 

492 shape = (num_components,) 

493 V = dolfinx.fem.functionspace(mesh, ("DG", 0, shape)) 

494 u = dolfinx.fem.Function(V, dtype=dofs.dtype) 

495 data_array = u.x.array.reshape(-1, num_components) 

496 for i in range(num_components): 

497 local_entities, local_values = dolfinx.io.distribute_entity_data( 

498 mesh, mesh.topology.dim, topology, dofs[:, i].copy() 

499 ) 

500 adj = dolfinx.graph.adjacencylist(local_entities) 

501 order = np.arange(len(local_values), dtype=np.int32) 

502 mt = dolfinx.mesh.meshtags_from_entities(mesh, mesh.topology.dim, adj, order) 

503 data_array[mt.indices, i] = local_values[mt.values] 

504 u.x.scatter_forward() 

505 return u