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

175 statements  

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

1""" 

2Module that uses DOLFINx/H5py to import XDMF files. 

3""" 

4 

5import re 

6from pathlib import Path 

7from typing import Any 

8from xml.etree import ElementTree 

9 

10from mpi4py import MPI 

11 

12import basix 

13import dolfinx 

14import numpy as np 

15import numpy.typing as npt 

16 

17from io4dolfinx.structures import ArrayData, FunctionData, MeshData, MeshTagsData, ReadMeshData 

18from io4dolfinx.utils import check_file_exists, compute_local_range 

19 

20from .. import FileMode, ReadMode 

21from ..h5py.backend import h5pyfile 

22 

23read_mode = ReadMode.parallel 

24 

25 

26def extract_function_names_and_timesteps(filename: Path | str) -> dict[str, list[str]]: 

27 tree = ElementTree.parse(filename) 

28 root = tree.getroot() 

29 mesh_nodes = root.findall(".//Grid[@CollectionType='Temporal']") 

30 function_names = [] 

31 for mesh in mesh_nodes: 

32 function_names.append(mesh.attrib["Name"]) 

33 

34 time_stamps: dict[str, list[str]] = {name: [] for name in function_names} 

35 for name in function_names: 

36 time_steps = root.findall(f".//Grid[@Name='{name}']") 

37 for time in time_steps: 

38 step = time.find(".//Time") 

39 if step is not None: 

40 val = step.attrib["Value"] 

41 time_stamps[name].append(val) 

42 for name in function_names: 

43 float_steps = np.argsort(np.array(list(set(time_stamps[name])), dtype=np.float64)) 

44 time_stamps[name] = np.array(list(set(time_stamps[name])), dtype=str)[float_steps].tolist() 

45 return time_stamps 

46 

47 

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

49 """Get default backend arguments given a set of input arguments. 

50 

51 Args: 

52 arguments: Input backend arguments 

53 

54 Returns: 

55 Updated backend arguments 

56 """ 

57 args = arguments or {} 

58 return args 

59 

60 

61def read_mesh_data( 

62 filename: Path | str, 

63 comm: MPI.Comm, 

64 time: str | float | None, 

65 read_from_partition: bool, 

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

67) -> ReadMeshData: 

68 """Read mesh data from file. 

69 

70 Args: 

71 filename: Path to file to read from 

72 comm: MPI communicator used in storage 

73 time: Time stamp associated with the mesh to read 

74 read_from_partition: Whether to read partition information 

75 backend_args: Arguments to backend 

76 

77 Returns: 

78 Internal data structure for the mesh data read from file 

79 """ 

80 assert not read_from_partition 

81 check_file_exists(filename) 

82 with dolfinx.io.XDMFFile(comm, filename, "r") as file: 

83 cell_shape, cell_degree = file.read_cell_type() 

84 cells = file.read_topology_data() 

85 x = file.read_geometry_data() 

86 return ReadMeshData( 

87 cells=cells, 

88 cell_type=cell_shape.name, 

89 x=x, 

90 lvar=int(basix.LagrangeVariant.equispaced), 

91 degree=cell_degree, 

92 ) 

93 

94 

95def read_point_data( 

96 filename: Path | str, 

97 name: str, 

98 comm: MPI.Comm, 

99 time: float | str | None, 

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

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

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

103 

104 Args: 

105 filename: Path to file 

106 name: Name of point data 

107 comm: Communicator to launch IO on. 

108 time: The time stamp 

109 backend_args: The backend arguments 

110 

111 Returns: 

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

113 """ 

114 check_file_exists(filename) 

115 filename = Path(filename) 

116 

117 root = ElementTree.parse(filename).getroot() 

118 backend_args = get_default_backend_args(backend_args) 

119 

120 grid = _time_step_grid(root, name, time, filename) 

121 attributes = _function_attributes(grid, name) 

122 local_range_start, dataset = _read_data_item(filename, _data_item(attributes[0]), comm) 

123 if len(attributes) == 2: 

124 _, imag = _read_data_item(filename, _data_item(attributes[1]), comm) 

125 # complex64 from float32 parts, complex128 from float64 

126 dataset = dataset + 1j * imag 

127 return dataset, local_range_start 

128 

129 

130def _time_step_grid( 

131 root: ElementTree.Element, name: str, time: str | float | None, filename: Path 

132) -> ElementTree.Element: 

133 """Grid holding function ``name`` at ``time``, or at the first time step if ``None``. 

134 

135 Both DOLFINx and legacy DOLFIN write one grid per time step, with a `Time` 

136 child, into a temporal collection named after the function. DOLFINx names 

137 the time-step grids ``name``, legacy DOLFIN ``{name}_{counter}``. 

138 

139 Raises: 

140 RuntimeError: If there is no such grid. 

141 """ 

142 collection = root.find(f".//Grid[@CollectionType='Temporal'][@Name='{name}']") 

143 if collection is not None: 

144 for grid in collection.findall("./Grid"): 

145 step_node = grid.find("./Time") 

146 if step_node is None: 

147 continue 

148 if time is None or np.isclose(float(step_node.attrib["Value"]), float(time)): 

149 return grid 

150 at_time = "" if time is None else f"(t={time})" 

151 raise RuntimeError(f"Function {name}{at_time} not found in {filename}") 

152 

153 

154def _function_attributes(grid: ElementTree.Element, name: str) -> list[ElementTree.Element]: 

155 """Attributes of ``grid`` holding function ``name``. 

156 

157 DOLFINx writes a real function as attribute ``name``, and a complex one as 

158 two attributes ``real_{name}`` and ``imag_{name}``. 

159 

160 Returns: 

161 ``[data]`` for a real function, ``[real, imag]`` for a complex one. 

162 

163 Raises: 

164 RuntimeError: If ``grid`` holds neither form, or only one complex part. 

165 """ 

166 if (node := grid.find(f"./Attribute[@Name='{name}']")) is not None: 

167 return [node] 

168 real = grid.find(f"./Attribute[@Name='real_{name}']") 

169 imag = grid.find(f"./Attribute[@Name='imag_{name}']") 

170 if real is not None and imag is not None: 

171 return [real, imag] 

172 if real is not None or imag is not None: 

173 present, missing = ("real", "imag") if real is not None else ("imag", "real") 

174 raise RuntimeError( 

175 f"Complex function {name} is incomplete: found '{present}_{name}'" 

176 f" but not '{missing}_{name}'." 

177 ) 

178 found = [attribute.attrib.get("Name") for attribute in grid.findall("./Attribute")] 

179 raise RuntimeError( 

180 f"Function {name} not found: expected attribute '{name}', or 'real_{name}'" 

181 f" and 'imag_{name}'. Attributes present: {found}." 

182 ) 

183 

184 

185def _data_item(node: ElementTree.Element) -> ElementTree.Element: 

186 """The `DataItem` directly under ``node``. 

187 

188 Raises: 

189 RuntimeError: If ``node`` has none. 

190 """ 

191 if (data_item := node.find("./DataItem")) is None: 

192 raise RuntimeError(f"<{node.tag} Name='{node.attrib.get('Name')}'> has no DataItem.") 

193 return data_item 

194 

195 

196def _read_data_item( 

197 filename: Path, data_item: ElementTree.Element, comm: MPI.Comm 

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

199 """Read this process's contiguous block of rows of an HDF5-backed `DataItem`. 

200 

201 Args: 

202 filename: The XDMF file, which HDF5 paths are relative to 

203 data_item: `DataItem` whose text is ``"file.h5:/path/to/dataset"`` 

204 comm: Communicator the rows are split over 

205 

206 Returns: 

207 The first global row read and the rows themselves. 

208 

209 Raises: 

210 RuntimeError: If the dataset's shape does not match the `Dimensions`. 

211 """ 

212 location = data_item.text.strip() if data_item.text is not None else "" 

213 data_file, sep, data_loc = location.partition(":") 

214 if data_item.attrib.get("Format") != "HDF" or not sep: 

215 raise RuntimeError(f"Expected an HDF5 DataItem 'file.h5:/path', got '{location}'.") 

216 dimensions = [int(d) for d in data_item.attrib["Dimensions"].split()] 

217 with h5pyfile(filename.parent / data_file, "r", comm=comm) as h5file: 

218 data = h5file[data_loc] 

219 # Shapes may differ by a trailing unit dimension, so compare rows and size. 

220 if data.shape[0] != dimensions[0] or data.size != int(np.prod(dimensions)): 

221 raise RuntimeError( 

222 f"Dataset {location} has shape {data.shape}, but its DataItem" 

223 f" has Dimensions {dimensions}." 

224 ) 

225 lr = compute_local_range(comm, data.shape[0]) 

226 return lr[0], data[slice(*lr)] 

227 

228 

229def read_attributes( 

230 filename: Path | str, 

231 comm: MPI.Comm, 

232 name: str, 

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

234) -> dict[str, Any]: 

235 """Read attributes from file. 

236 

237 Args: 

238 filename: Path to file to read from 

239 comm: MPI communicator used in storage 

240 name: Name of the attribute group 

241 backend_args: Arguments to backend 

242 

243 Returns: 

244 Dictionary of attributes read from file 

245 """ 

246 raise NotImplementedError("The XDMF backend cannot read attributes.") 

247 

248 

249def read_timestamps( 

250 filename: Path | str, 

251 comm: MPI.Comm, 

252 function_name: str, 

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

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

255 """Read timestamps from file. 

256 

257 Args: 

258 filename: Path to file to read from 

259 comm: MPI communicator used in storage 

260 function_name: Name of the function to read timestamps for 

261 backend_args: Arguments to backend 

262 

263 Returns: 

264 Numpy array of timestamps read from file 

265 """ 

266 tree = ElementTree.parse(filename) 

267 root = tree.getroot() 

268 time_stamps = [] 

269 time_steps = root.findall(f".//Grid[@Name='{function_name}']") 

270 for time in time_steps: 

271 step = time.find(".//Time") 

272 if step is not None: 

273 val = step.attrib["Value"] 

274 time_stamps.append(val) 

275 float_steps = np.argsort(np.array(list(set(time_stamps)), dtype=np.float64)) 

276 return np.array(list(set(time_stamps)), dtype=str)[float_steps].tolist() 

277 

278 

279def write_attributes( 

280 filename: Path | str, 

281 comm: MPI.Comm, 

282 name: str, 

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

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

285): 

286 """Write attributes to file. 

287 

288 Args: 

289 filename: Path to file to write to 

290 comm: MPI communicator used in storage 

291 name: Name of the attribute group 

292 attributes: Dictionary of attributes to write 

293 backend_args: Arguments to backend 

294 """ 

295 raise NotImplementedError("The XDMF backend cannot write attributes.") 

296 

297 

298def write_mesh( 

299 filename: Path | str, 

300 comm: MPI.Comm, 

301 mesh: MeshData, 

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

303 mode: FileMode, 

304 time: float, 

305): 

306 """ 

307 Write a mesh to file. 

308 

309 Args: 

310 comm: MPI communicator used in storage 

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

312 filename: Path to file to write to 

313 backend_args: Arguments to backend 

314 mode: File-mode to store the mesh 

315 time: Time stamp associated with the mesh 

316 """ 

317 raise NotImplementedError("The XDMF backend cannot write meshes.") 

318 

319 

320def write_meshtags( 

321 filename: str | Path, 

322 comm: MPI.Comm, 

323 data: MeshTagsData, 

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

325): 

326 """Write mesh tags to file. 

327 

328 Args: 

329 filename: Path to file to write to 

330 comm: MPI communicator used in storage 

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

332 backend_args: Arguments to backend 

333 """ 

334 raise NotImplementedError("The XDMF backend cannot write meshtags.") 

335 

336 

337def read_meshtags_data( 

338 filename: str | Path, 

339 comm: MPI.Comm, 

340 name: str, 

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

342) -> MeshTagsData: 

343 """Read mesh tags from file. 

344 

345 Args: 

346 filename: Path to file to read from 

347 comm: MPI communicator used in storage 

348 name: Name of the mesh tags to read 

349 backend_args: Arguments to backend 

350 

351 Returns: 

352 Internal data structure for the mesh tags read from file 

353 """ 

354 raise NotImplementedError("The XDMF backend cannot read meshtags.") 

355 

356 

357def read_dofmap( 

358 filename: str | Path, 

359 comm: MPI.Comm, 

360 name: str, 

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

362) -> dolfinx.graph.AdjacencyList: 

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

364 

365 Args: 

366 filename: Path to file to read from 

367 comm: MPI communicator used in storage 

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

369 backend_args: Arguments to backend 

370 

371 Returns: 

372 Dofmap as an AdjacencyList 

373 """ 

374 raise NotImplementedError("The XDMF backend cannot make checkpoints.") 

375 

376 

377def read_dofs( 

378 filename: str | Path, 

379 comm: MPI.Comm, 

380 name: str, 

381 time: float, 

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

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

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

385 

386 Args: 

387 filename: Path to file to read from 

388 comm: MPI communicator used in storage 

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

390 time: Time stamp associated with the function to read 

391 backend_args: Arguments to backend 

392 

393 Returns: 

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

395 and the global starting point on the process. 

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

397 """ 

398 raise NotImplementedError("The XDMF backend cannot make checkpoints.") 

399 

400 

401def read_cell_perms( 

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

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

404 """ 

405 Read cell permutation from file with given communicator, 

406 Split in continuous chunks based on number of cells in the input data. 

407 

408 Args: 

409 comm: MPI communicator used in storage 

410 filename: Path to file to read from 

411 backend_args: Arguments to backend 

412 

413 Returns: 

414 Contiguous sequence of permutations (with respect to input data) 

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

416 """ 

417 raise NotImplementedError("The XDMF backend cannot make checkpoints.") 

418 

419 

420def write_function( 

421 filename: Path, 

422 comm: MPI.Comm, 

423 u: FunctionData, 

424 time: float, 

425 mode: FileMode, 

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

427): 

428 """ 

429 Write a function to file. 

430 

431 Args: 

432 comm: MPI communicator used in storage 

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

434 filename: Path to file to write to 

435 time: Time stamp associated with function 

436 mode: File-mode to store the function 

437 backend_args: Arguments to backend 

438 """ 

439 raise NotImplementedError("The XDMF backend cannot make checkpoints.") 

440 

441 

442def read_legacy_mesh( 

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

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

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

446 legacy DOLFIN HDF5-file. 

447 

448 Args: 

449 filename: Path to file to read from 

450 comm: MPI communicator used in storage 

451 group: Group in HDF5 file where mesh is stored 

452 

453 Returns: 

454 Tuple containing: 

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

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

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

458 """ 

459 raise NotImplementedError("The XDMF backend cannot read legacy DOLFIN meshes.") 

460 

461 

462def snapshot_checkpoint( 

463 filename: Path | str, 

464 mode: FileMode, 

465 u: dolfinx.fem.Function, 

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

467): 

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

469 

470 Args: 

471 filename: Path to file to read from 

472 mode: File-mode to store the function 

473 u: dolfinx function to create a snapshot checkpoint for 

474 backend_args: Arguments to backend 

475 """ 

476 raise NotImplementedError("The XDMF backend cannot make checkpoints.") 

477 

478 

479def read_hdf5_array( 

480 comm: MPI.Comm, 

481 filename: Path | str, 

482 group: str, 

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

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

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

486 

487 Args: 

488 comm: MPI communicator used in storage 

489 filename: Path to file to read from 

490 group: Group in HDF5 file where array is stored 

491 backend_args: Arguments to backend 

492 

493 Returns: 

494 Tuple containing: 

495 - Numpy array read from file 

496 - Global starting point on the process. 

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

498 """ 

499 raise NotImplementedError("The XDMF backend cannot read HDF5 arrays") 

500 

501 

502def read_function_names( 

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

504) -> list[str]: 

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

506 

507 Args: 

508 filename: Path to file 

509 comm: MPI communicator to launch IO on. 

510 backend_args: Arguments to backend 

511 

512 Returns: 

513 A list of function names. 

514 """ 

515 # Find all functions in xml tree 

516 check_file_exists(filename) 

517 filename = Path(filename) 

518 

519 tree = ElementTree.parse(filename) 

520 root = tree.getroot() 

521 backend_args = get_default_backend_args(backend_args) 

522 # Functions in checkpoint format 

523 checkpoint_funcs = root.findall(".//Attribute[@ItemType='FiniteElementFunction']") 

524 names = [func.attrib["Name"] for func in checkpoint_funcs] 

525 # Temporal funcs 

526 temporal_funcs = root.findall(".//Grid[@GridType='Collection']") 

527 for func in temporal_funcs: 

528 names.append(func.attrib["Name"]) 

529 return list(set(names)) 

530 

531 

532def read_cell_data( 

533 filename: Path | str, 

534 name: str, 

535 comm: MPI.Comm, 

536 time: str | float | None, 

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

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

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

540 

541 Reads cell-centred functions written by DOLFINx (DG-0), including complex 

542 ones, and DG-0 checkpoints written by legacy DOLFIN. 

543 

544 Args: 

545 filename: Path to file 

546 name: Name of cell data 

547 comm: Communicator to launch IO on. 

548 time: The time stamp 

549 backend_args: The backend arguments 

550 Returns: 

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

552 vertex indices of the cells, dofs the degrees of 

553 freedom within that cell. 

554 """ 

555 check_file_exists(filename) 

556 filename = Path(filename) 

557 

558 root = ElementTree.parse(filename).getroot() 

559 backend_args = get_default_backend_args(backend_args) 

560 

561 grid = _time_step_grid(root, name, time, filename) 

562 attributes = _function_attributes(grid, name) 

563 values = [_cell_values(attribute) for attribute in attributes] 

564 

565 _, dofs = _read_data_item(filename, values[0], comm) 

566 if len(values) == 2: 

567 _, imag = _read_data_item(filename, values[1], comm) 

568 dofs = dofs + 1j * imag 

569 _, topology = _read_data_item(filename, _grid_topology(root, grid), comm) 

570 # One row per cell in both, so both processes' blocks cover the same cells. 

571 if topology.shape[0] != dofs.shape[0]: 

572 raise RuntimeError( 

573 f"Cell data {name} has {dofs.shape[0]} rows on this process, but the" 

574 f" topology has {topology.shape[0]}." 

575 ) 

576 return topology.astype(np.int64), dofs.reshape(dofs.shape[0], -1) 

577 

578 

579def _cell_values(attribute: ElementTree.Element) -> ElementTree.Element: 

580 """`DataItem` with one value row per cell, from a cell-data attribute. 

581 

582 Raises: 

583 ValueError: If the attribute is not DG-0 data. 

584 """ 

585 if attribute.attrib.get("ItemType") == "FiniteElementFunction": 

586 # Legacy DOLFIN checkpoint: the dof values are the `.../vector` item. 

587 family = attribute.attrib["ElementFamily"] 

588 degree = int(attribute.attrib["ElementDegree"]) 

589 if family != "DG" or degree != 0: 

590 raise ValueError( 

591 f"Cannot read in finite element function ({family}, {degree}) as cell data." 

592 ) 

593 for data_item in attribute.findall("./DataItem"): 

594 if (data_item.text or "").strip().endswith("/vector"): 

595 return data_item 

596 raise RuntimeError(f"Checkpoint {attribute.attrib['Name']} has no vector DataItem.") 

597 if (center := attribute.attrib.get("Center")) != "Cell": 

598 raise ValueError( 

599 f"Attribute {attribute.attrib['Name']} has Center='{center}'; cell data" 

600 " needs Center='Cell'." 

601 ) 

602 return _data_item(attribute) 

603 

604 

605def _grid_topology(root: ElementTree.Element, grid: ElementTree.Element) -> ElementTree.Element: 

606 """Topology `DataItem` of ``grid``, following DOLFINx's ``xi:include`` of the mesh. 

607 

608 Legacy DOLFIN writes the topology into each grid. DOLFINx refers to the mesh 

609 grid with ``xpointer(<grid path>/*[self::Topology or self::Geometry])``. 

610 

611 Raises: 

612 RuntimeError: If the topology cannot be found. 

613 """ 

614 if (topology := grid.find("./Topology")) is not None: 

615 return _data_item(topology) 

616 # Any namespace: DOLFINx declares XInclude's as https, the standard says http. 

617 if (include := grid.find("./{*}include")) is not None: 

618 pointer = include.attrib["xpointer"] 

619 match = re.fullmatch(r"xpointer\(/Xdmf(/.+?)(?:\[(\d+)\])?/\*\[.*\]\)", pointer) 

620 if match is not None: 

621 # ElementTree applies a position predicate to all same-tag siblings, 

622 # XPath to the elements left by the preceding predicates. 

623 path, position = match.groups() 

624 mesh_grids = root.findall(f".{path}") 

625 index = 0 if position is None else int(position) - 1 

626 if index < len(mesh_grids): 

627 if (topology := mesh_grids[index].find("./Topology")) is not None: 

628 return _data_item(topology) 

629 raise RuntimeError(f"No topology found for grid {grid.attrib.get('Name')}.") 

630 

631 

632def write_data( 

633 filename: Path | str, 

634 point_data: ArrayData, 

635 comm: MPI.Comm, 

636 time: str | float | None, 

637 mode: FileMode, 

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

639): 

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

641 

642 

643 Args: 

644 filename: Path to file 

645 point_data: Data to write to file 

646 time: Time stamp 

647 mode: Append or write 

648 backend_args: The backend arguments 

649 """ 

650 raise NotImplementedError("XDMF has not implemented this yet")