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

379 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 13:26 +0000

1""" 

2Gmsh interface to io4dolfinx. 

3 

4Reads and writes Gmsh ``.msh`` files (MSH 2.2 and 4.1, ASCII and binary) through the 

5Gmsh Python API, reusing :mod:`dolfinx.io.gmsh` for the geometry, the topology and the 

6node ordering. All file access happens on rank 0. Only the element types of 

7``dolfinx.io.gmsh`` are supported, and DOLFINx 0.11 or later is required. 

8 

9In DOLFINx terms: 

10 

11- As in :func:`dolfinx.io.gmsh.model_to_mesh`, the cells are the elements of the 

12 highest dimension in a physical group, and every cell is in exactly one physical 

13 group. Node ``i`` has node tag ``i + 1``. An MSH file holds a single mesh, so meshes 

14 are not time dependent. 

15- Point and cell data are post-processing views (``$NodeData``/``$ElementData``) keyed 

16 by node and element tags. The steps of a view are its time series. Complex data is 

17 split into the views ``real_{name}`` and ``imag_{name}``, as in 

18 :class:`dolfinx.io.XDMFFile`. 

19- Mesh tags are physical groups, with the physical tag as the tag value. 

20 

21SPDX License identifier: MIT 

22 

23Copyright: Jørgen S. Dokken and Simula Research Laboratory 

24""" 

25 

26from pathlib import Path 

27from typing import Any, Callable, TypeVar 

28 

29from mpi4py import MPI 

30 

31import basix 

32import dolfinx 

33import fenicsx_compat 

34import numpy as np 

35import numpy.typing as npt 

36from dolfinx.io.gmsh import ( 

37 _gmsh_to_cells, 

38 cell_perm_array, 

39 extract_geometry, 

40 extract_topology_and_markers, 

41) 

42 

43try: 

44 import gmsh 

45except ImportError: 

46 raise ModuleNotFoundError("The gmsh-backend requires gmsh") 

47 

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

49from ...utils import check_file_exists, codim_names 

50from .. import FileMode, ReadMode 

51 

52read_mode = ReadMode.serial 

53 

54# Gmsh options changed by this backend, restored if Gmsh was initialized by the caller 

55_options = ("General.Terminal", "Mesh.Binary", "Mesh.SaveAll", "PostProcessing.SaveMesh") 

56 

57Local = TypeVar("Local") 

58Shared = TypeVar("Shared") 

59 

60 

61def _run_on_root( 

62 comm: MPI.Comm, func: Callable[[list[int]], tuple[Local, Shared]] 

63) -> tuple[Local | None, Shared]: 

64 """Run a function in a Gmsh session on rank 0 and share its outcome with all ranks. 

65 

66 If Gmsh is already initialized, e.g. by the caller, the function runs in a new model, 

67 which is removed afterwards together with the views it created, and the previous 

68 model and options are restored. Otherwise Gmsh is initialized and finalized around it. 

69 

70 Args: 

71 comm: MPI communicator. Collective on all ranks. 

72 func: Function to run on rank 0. It takes the tags of the views that existed 

73 before the session, which do not belong to the file, and returns a tuple of 

74 data kept on rank 0 and data broadcast to all ranks. 

75 

76 Returns: 

77 Tuple containing: 

78 - The first output of ``func`` on rank 0, ``None`` on other ranks 

79 - The second output of ``func`` on all ranks 

80 

81 Raises: 

82 NotImplementedError: If DOLFINx is older than 0.11. 

83 Exception: An exception raised by ``func`` is raised on all ranks. 

84 """ 

85 if fenicsx_compat.before("0.11.0.dev0"): 

86 raise NotImplementedError("The gmsh backend requires DOLFINx 0.11 or later.") 

87 

88 result: Local | None = None 

89 shared: Shared | None = None 

90 error: Exception | None = None 

91 if comm.rank == 0: 

92 initialized = gmsh.isInitialized() 

93 if initialized: 

94 previous_model = gmsh.model.getCurrent() 

95 previous_options = {option: gmsh.option.getNumber(option) for option in _options} 

96 # Views are global in gmsh, not per model, so existing ones must be skipped and kept 

97 preexisting_views = list(gmsh.view.getTags()) 

98 gmsh.model.add("io4dolfinx") 

99 else: 

100 gmsh.initialize(interruptible=False) 

101 preexisting_views = [] 

102 try: 

103 gmsh.option.setNumber("General.Terminal", 0) 

104 result, shared = func(preexisting_views) 

105 except Exception as e: 

106 error = e 

107 finally: 

108 if initialized: 

109 for view in gmsh.view.getTags(): 

110 if view not in preexisting_views: 

111 gmsh.view.remove(view) 

112 gmsh.model.remove() 

113 gmsh.model.setCurrent(previous_model) 

114 for option, value in previous_options.items(): 

115 gmsh.option.setNumber(option, value) 

116 else: 

117 gmsh.finalize() 

118 error, shared = comm.bcast((error, shared), root=0) 

119 if error is not None: 

120 raise error 

121 return result, shared # type: ignore[return-value] 

122 

123 

124def _find_view( 

125 name: str, preexisting_views: list[int] 

126) -> tuple[list[int], npt.NDArray[np.float64]] | None: 

127 """Find the views of the current Gmsh session holding the data with a given name. 

128 

129 A gmsh view holds all time steps of one field, each step storing its time. 

130 Complex data is stored as two views, ``real_{name}`` and ``imag_{name}``, as 

131 :class:`dolfinx.io.XDMFFile` does. 

132 

133 Args: 

134 name: Name of the data 

135 preexisting_views: Tags of the views that do not belong to the file, which are skipped 

136 

137 Returns: 

138 ``None`` if there is no such data, otherwise a tuple containing: 

139 - The view tags, ``[view]`` for real data and ``[real, imag]`` for complex data 

140 - The time of each step of the views 

141 """ 

142 views = { 

143 gmsh.view.option.getString(view, "Name"): view 

144 for view in gmsh.view.getTags() 

145 if view not in preexisting_views 

146 } 

147 real, imag = f"real_{name}", f"imag_{name}" 

148 if name in views: 

149 found = [views[name]] 

150 elif real in views and imag in views: 

151 found = [views[real], views[imag]] 

152 elif real in views or imag in views: 

153 raise RuntimeError( 

154 f"Found only one part of complex data '{name}', expected '{real}' and '{imag}'." 

155 ) 

156 else: 

157 return None 

158 num_steps = int(gmsh.view.option.getNumber(found[0], "NbTimeStep")) 

159 times = [gmsh.view.getModelData(found[0], step)[3] for step in range(num_steps)] 

160 return found, np.array(times, dtype=np.float64) 

161 

162 

163def _tagged_entities() -> dict[ 

164 int, 

165 tuple[npt.NDArray[np.int64], npt.NDArray[np.int64], npt.NDArray[np.int32], str, int] | None, 

166]: 

167 """Get the elements in physical groups of the current Gmsh model, by dimension. 

168 

169 Uses :func:`dolfinx.io.gmsh.extract_topology_and_markers`, so as in 

170 :func:`dolfinx.io.gmsh.model_to_mesh` the highest dimension holds the cells, and an 

171 element in several physical groups appears once per group. 

172 

173 Returns: 

174 Map from topological dimension to ``None`` if the elements of that dimension are 

175 of several types or of a type not supported by :mod:`dolfinx.io.gmsh`, otherwise 

176 to a tuple containing: 

177 - The element tags, sorted 

178 - The element nodes as zero-based node indices in DOLFINx ordering, of shape 

179 ``(num_elements, num_nodes_per_element)``, one row per element tag 

180 - The physical tag of each element 

181 - The DOLFINx cell type of the elements 

182 - The degree of the elements 

183 """ 

184 topologies, _ = extract_topology_and_markers(gmsh.model) 

185 if len(topologies) == 0: 

186 raise ValueError("No physical groups in the Gmsh model.") 

187 types_by_dim: dict[int, list[int]] = {} 

188 for gmsh_type in topologies: 

189 if gmsh_type in _gmsh_to_cells: 

190 cell_type = dolfinx.mesh.to_type(_gmsh_to_cells[gmsh_type][0]) 

191 dim = dolfinx.mesh.cell_dim(cell_type) 

192 else: 

193 dim = gmsh.model.mesh.getElementProperties(gmsh_type)[1] 

194 types_by_dim.setdefault(dim, []).append(gmsh_type) 

195 

196 entities: dict[int, Any] = {} 

197 for dim, types in types_by_dim.items(): 

198 if len(types) > 1 or types[0] not in _gmsh_to_cells: 

199 entities[dim] = None 

200 continue 

201 cell_name, degree = _gmsh_to_cells[types[0]] 

202 data = topologies[types[0]] 

203 tags = data["entity_tags"].astype(np.int64) 

204 order = np.argsort(tags, kind="stable") 

205 nodes = data["topology"].astype(np.int64) 

206 perm = cell_perm_array(dolfinx.mesh.to_type(cell_name), nodes.shape[1]) 

207 values = data["cell_data"].astype(np.int32) 

208 entities[dim] = (tags[order], nodes[order][:, perm], values[order], cell_name, degree) 

209 return entities 

210 

211 

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

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

214 

215 Args: 

216 arguments: Input backend arguments 

217 

218 Returns: 

219 The arguments, with defaults for: 

220 - ``"gdim"``: Geometric dimension of a mesh read from file, ``3`` as in 

221 :func:`dolfinx.io.gmsh.read_from_msh` 

222 - ``"dtype"``: Floating point type of the coordinates and data read from file, 

223 ``np.float64`` 

224 - ``"binary"``: Write binary instead of ASCII MSH. ``None`` keeps the encoding 

225 of an existing file and writes a new one as ASCII. 

226 """ 

227 args: dict[str, Any] = {"gdim": 3, "dtype": np.float64, "binary": None} 

228 args.update(arguments or {}) 

229 return args 

230 

231 

232def write_attributes( 

233 filename: Path | str, 

234 comm: MPI.Comm, 

235 name: str, 

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

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

238): 

239 """Write attributes to file. 

240 

241 Args: 

242 filename: Path to file to write to 

243 comm: MPI communicator used in storage 

244 name: Name of the attribute group 

245 attributes: Dictionary of attributes to write 

246 backend_args: Arguments to backend 

247 """ 

248 raise NotImplementedError("The gmsh backend cannot write attributes.") 

249 

250 

251def read_attributes( 

252 filename: Path | str, 

253 comm: MPI.Comm, 

254 name: str, 

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

256) -> dict[str, Any]: 

257 """Read attributes from file. 

258 

259 Args: 

260 filename: Path to file to read from 

261 comm: MPI communicator used in storage 

262 name: Name of the attribute group 

263 backend_args: Arguments to backend 

264 

265 Returns: 

266 Dictionary of attributes read from file 

267 """ 

268 raise NotImplementedError("The gmsh backend cannot read attributes.") 

269 

270 

271def read_timestamps( 

272 filename: Path | str, 

273 comm: MPI.Comm, 

274 function_name: str, 

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

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

277 """Read the times of the steps of point or cell data. 

278 

279 Args: 

280 filename: Path to file to read from 

281 comm: MPI communicator used in storage 

282 function_name: Name of the point or cell data 

283 backend_args: Arguments to backend 

284 

285 Returns: 

286 The time of each step, on all ranks 

287 """ 

288 check_file_exists(filename) 

289 

290 def read(preexisting_views: list[int]) -> tuple[None, npt.NDArray[np.float64]]: 

291 gmsh.merge(str(filename)) 

292 if (found := _find_view(function_name, preexisting_views)) is None: 

293 raise KeyError(f"Could not find data '{function_name}' in {filename}.") 

294 return None, found[1] 

295 

296 return _run_on_root(comm, read)[1] 

297 

298 

299def read_function_names( 

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

301) -> list[str]: 

302 """Read the names of all point and cell data in a file. 

303 

304 Args: 

305 filename: Path to file 

306 comm: MPI communicator to launch IO on. 

307 backend_args: Arguments to backend 

308 

309 Returns: 

310 The names of the views in the file, on all ranks. A complex pair of views 

311 ``real_{name}`` and ``imag_{name}`` is listed as ``name``. 

312 """ 

313 check_file_exists(filename) 

314 

315 def read(preexisting_views: list[int]) -> tuple[None, list[str]]: 

316 gmsh.merge(str(filename)) 

317 views = [view for view in gmsh.view.getTags() if view not in preexisting_views] 

318 names = [gmsh.view.option.getString(view, "Name") for view in views] 

319 complex_names = {n[5:] for n in names if n.startswith("real_") and f"imag_{n[5:]}" in names} 

320 functions = [ 

321 n[5:] if n.startswith(("real_", "imag_")) and n[5:] in complex_names else n 

322 for n in names 

323 ] 

324 return None, list(dict.fromkeys(functions)) 

325 

326 return _run_on_root(comm, read)[1] 

327 

328 

329def write_mesh( 

330 filename: Path | str, 

331 comm: MPI.Comm, 

332 mesh: MeshData, 

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

334 mode: FileMode = FileMode.write, 

335 time: float = 0.0, 

336): 

337 """Write a mesh to a Gmsh file. 

338 

339 Nodes and cells get the tag one above their global index. The cells form a single 

340 entity in the physical group with tag 1, the default cell marker, as every cell must 

341 be in a physical group (see :func:`dolfinx.io.gmsh.model_to_mesh`). Cell tags written 

342 with :func:`write_meshtags` replace it. 

343 

344 Args: 

345 filename: Path to file to write to, with suffix ``.msh`` 

346 comm: MPI communicator used in storage 

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

348 backend_args: Arguments to backend, ``"binary"`` selects binary MSH 

349 mode: Only :attr:`FileMode.write`, as an MSH file holds one mesh 

350 time: Ignored, as an MSH file holds one mesh 

351 """ 

352 if mode != FileMode.write: 

353 raise NotImplementedError("The gmsh backend writes a single mesh per file.") 

354 if Path(filename).suffix != ".msh": 

355 raise ValueError(f"Expected a filename with suffix .msh, got {filename}.") 

356 if mesh.degree > 2 and mesh.lagrange_variant != int(basix.LagrangeVariant.equispaced): 

357 raise NotImplementedError("Gmsh only supports equispaced Lagrange geometries.") 

358 cell = (mesh.cell_type, mesh.degree) 

359 if (gmsh_type := next((t for t, c in _gmsh_to_cells.items() if c == cell), None)) is None: 

360 raise NotImplementedError( 

361 f"Gmsh has no element for a {mesh.cell_type} of degree {mesh.degree}." 

362 ) 

363 backend_args = get_default_backend_args(backend_args) 

364 

365 # Rank order is global index order 

366 nodes = comm.gather(mesh.local_geometry, root=0) 

367 cells = comm.gather(mesh.local_topology, root=0) 

368 

369 def write(preexisting_views: list[int]) -> tuple[None, None]: 

370 assert nodes is not None and cells is not None 

371 x = np.vstack(nodes) 

372 x_3d = np.zeros((x.shape[0], 3), dtype=np.float64) 

373 x_3d[:, : x.shape[1]] = x 

374 topology = np.vstack(cells) 

375 gmsh_topology = np.empty_like(topology) 

376 cell_type = dolfinx.mesh.to_type(mesh.cell_type) 

377 gmsh_topology[:, cell_perm_array(cell_type, topology.shape[1])] = topology 

378 

379 tdim = dolfinx.mesh.cell_dim(cell_type) 

380 gmsh.model.addDiscreteEntity(tdim, 1) 

381 gmsh.model.mesh.addNodes(tdim, 1, np.arange(1, x.shape[0] + 1), x_3d.reshape(-1)) 

382 gmsh.model.mesh.addElementsByType( 

383 1, 

384 gmsh_type, 

385 np.arange(1, topology.shape[0] + 1), 

386 gmsh_topology.reshape(-1) + 1, 

387 ) 

388 gmsh.model.addPhysicalGroup(tdim, [1], 1) 

389 gmsh.option.setNumber("Mesh.Binary", int(bool(backend_args["binary"]))) 

390 gmsh.option.setNumber("Mesh.SaveAll", 1) 

391 gmsh.write(str(filename)) 

392 return None, None 

393 

394 _run_on_root(comm, write) 

395 

396 

397def read_mesh_data( 

398 filename: Path | str, 

399 comm: MPI.Comm, 

400 time: str | float | None = None, 

401 read_from_partition: bool = False, 

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

403) -> ReadMeshData: 

404 """Read mesh data from a Gmsh file. 

405 

406 As in :func:`dolfinx.io.gmsh.model_to_mesh`, the cells are the elements of the 

407 highest dimension in a physical group, and every cell must be in exactly one physical 

408 group. The cells must be of one type, and node tags contiguous. All data is read on 

409 rank 0. 

410 

411 Args: 

412 filename: Path to file to read from 

413 comm: MPI communicator to distribute the mesh over 

414 time: Ignored, as an MSH file holds one mesh 

415 read_from_partition: Read mesh with partition from file (not supported) 

416 backend_args: Arguments to backend, see :func:`get_default_backend_args` 

417 

418 Returns: 

419 Internal data structure for the mesh data read from file 

420 """ 

421 if read_from_partition: 

422 raise NotImplementedError("The gmsh backend cannot read partitioning data.") 

423 check_file_exists(filename) 

424 backend_args = get_default_backend_args(backend_args) 

425 gdim = backend_args["gdim"] 

426 

427 def read( 

428 preexisting_views: list[int], 

429 ) -> tuple[tuple[npt.NDArray[np.float64], npt.NDArray[np.int64]], tuple[str, int, int]]: 

430 gmsh.merge(str(filename)) 

431 x = extract_geometry(gmsh.model)[:, :gdim] 

432 tagged = _tagged_entities() 

433 tdim = max(tagged) 

434 if (cell_data := tagged[tdim]) is None: 

435 raise NotImplementedError(f"Mixed or unsupported cell types in {filename}.") 

436 tags, cells, _, cell_type, degree = cell_data 

437 num_cells = sum(len(t) for t in gmsh.model.mesh.getElements(dim=tdim, tag=-1)[1]) 

438 if len(tags) != num_cells or len(np.unique(tags)) != len(tags): 

439 raise ValueError( 

440 f"All cells are expected to be in exactly one physical group, as in " 

441 f"dolfinx.io.gmsh.model_to_mesh; found {len(tags)} tags for {num_cells} cells." 

442 ) 

443 return (x, cells), (cell_type, degree, cells.shape[1]) 

444 

445 local, (cell_type, degree, num_nodes_per_cell) = _run_on_root(comm, read) 

446 if local is None: 

447 x = np.zeros((0, gdim), dtype=np.float64) 

448 cells = np.zeros((0, num_nodes_per_cell), dtype=np.int64) 

449 else: 

450 x, cells = local 

451 return ReadMeshData( 

452 cells=cells, 

453 cell_type=cell_type, 

454 x=x.astype(backend_args["dtype"]), 

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

456 degree=degree, 

457 ) 

458 

459 

460def write_meshtags( 

461 filename: str | Path, 

462 comm: MPI.Comm, 

463 data: MeshTagsData, 

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

465): 

466 """Write mesh tags to a Gmsh file as physical groups. 

467 

468 Each tag value becomes a physical group with the value as tag, named 

469 ``f"{data.name}_{value}"``. Gmsh physical tags are positive and unique per 

470 dimension, so tag values must be positive. 

471 

472 Cell tags replace the marker of the cells they cover, moving them to the physical 

473 group of their value, while other cells keep their marker, e.g. the default marker 1 

474 of :func:`write_mesh`. Markers no cell has any more are removed. Entities of lower 

475 dimension are added as new elements, and only one set of mesh tags can be written 

476 per lower dimension. 

477 

478 Args: 

479 filename: Path to file to write to, holding a mesh written with 

480 :func:`write_mesh` 

481 comm: MPI communicator used in storage 

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

483 backend_args: Arguments to backend, ``"binary"`` selects binary MSH 

484 """ 

485 check_file_exists(filename) 

486 backend_args = get_default_backend_args(backend_args) 

487 indices = comm.gather(data.indices, root=0) 

488 values = comm.gather(data.values, root=0) 

489 

490 def write(preexisting_views: list[int]) -> tuple[None, None]: 

491 assert indices is not None and values is not None 

492 entities = np.vstack(indices).astype(np.int64) 

493 tag_values = np.hstack(values).astype(np.int32) 

494 if np.any(tag_values <= 0): 

495 raise ValueError(f"Gmsh physical tags must be positive, got {np.unique(tag_values)}.") 

496 if (binary := backend_args["binary"]) is None: 

497 # The second line of an MSH file is "version file-type data-size" 

498 with open(filename, "rb") as file: 

499 file.readline() 

500 binary = file.readline().split()[1] == b"1" 

501 

502 gmsh.merge(str(filename)) 

503 tagged = _tagged_entities() 

504 tdim = max(tagged) 

505 if (cell_data := tagged[tdim]) is None: 

506 raise NotImplementedError(f"Mixed or unsupported cell types in {filename}.") 

507 cell_tags, cells, _, _, degree = cell_data 

508 dim = data.dim 

509 groups = [tag for d, tag in gmsh.model.getPhysicalGroups() if d == dim] 

510 if dim < tdim and len(groups) > 0: 

511 raise NotImplementedError( 

512 f"{filename} already has physical groups of dimension {dim}, and only one " 

513 "set of mesh tags can be written per dimension below the cells." 

514 ) 

515 

516 assert data.cell_type is not None 

517 entity_degree = 0 if data.cell_type == "point" else degree 

518 cell = (data.cell_type, entity_degree) 

519 if (gmsh_type := next((t for t, c in _gmsh_to_cells.items() if c == cell), None)) is None: 

520 raise NotImplementedError( 

521 f"Gmsh has no element for a {data.cell_type} of degree {entity_degree}." 

522 ) 

523 gmsh_entities = np.empty_like(entities) 

524 perm = cell_perm_array(dolfinx.mesh.to_type(data.cell_type), entities.shape[1]) 

525 gmsh_entities[:, perm] = entities 

526 

527 if dim == tdim: 

528 # Find the element tag of each tagged cell by matching node sets 

529 _, inverse = np.unique( 

530 np.sort(np.vstack([cells, entities]), axis=1), axis=0, return_inverse=True 

531 ) 

532 inverse = inverse.reshape(-1) 

533 cell_position = np.empty(inverse.max() + 1, dtype=np.int64) 

534 cell_position[inverse[: len(cells)]] = np.arange(len(cells)) 

535 element_tags = cell_tags[cell_position[inverse[len(cells) :]]] 

536 # Gmsh rejects removing elements an entity does not hold 

537 for d, entity in gmsh.model.getEntities(tdim): 

538 entity_tags = gmsh.model.mesh.getElements(d, entity)[1] 

539 held = np.intersect1d(np.hstack([*entity_tags, []]), element_tags) 

540 if len(held) > 0: 

541 gmsh.model.mesh.removeElements(d, entity, held.astype(np.int64)) 

542 else: 

543 first = gmsh.model.mesh.getMaxElementTag() + 1 

544 element_tags = np.arange(first, first + len(entities), dtype=np.int64) 

545 

546 for value in np.unique(tag_values): 

547 selected = tag_values == value 

548 if value in groups: 

549 # A marker the cells already have, e.g. the default marker 1 

550 entity = gmsh.model.getEntitiesForPhysicalGroup(dim, int(value))[0] 

551 gmsh.model.setPhysicalName(dim, int(value), f"{data.name}_{value}") 

552 else: 

553 entity = gmsh.model.addDiscreteEntity(dim) 

554 gmsh.model.addPhysicalGroup(dim, [entity], int(value), f"{data.name}_{value}") 

555 gmsh.model.mesh.addElementsByType( 

556 entity, gmsh_type, element_tags[selected], gmsh_entities[selected].reshape(-1) + 1 

557 ) 

558 if dim == tdim: 

559 # Remove the markers no cell has any more 

560 for tag in groups: 

561 group_entities = gmsh.model.getEntitiesForPhysicalGroup(dim, tag) 

562 num_cells = sum( 

563 len(t) for e in group_entities for t in gmsh.model.mesh.getElements(dim, e)[1] 

564 ) 

565 if num_cells == 0: 

566 gmsh.model.removePhysicalGroups([(dim, tag)]) 

567 

568 gmsh.option.setNumber("Mesh.Binary", int(binary)) 

569 gmsh.option.setNumber("Mesh.SaveAll", 1) 

570 gmsh.write(str(filename)) 

571 # Append the views without repeating the mesh 

572 gmsh.option.setNumber("PostProcessing.SaveMesh", 0) 

573 for view in gmsh.view.getTags(): 

574 if view not in preexisting_views: 

575 gmsh.view.write(view, str(filename), append=True) 

576 return None, None 

577 

578 _run_on_root(comm, write) 

579 

580 

581def read_meshtags_data( 

582 filename: str | Path, 

583 comm: MPI.Comm, 

584 name: str, 

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

586) -> MeshTagsData: 

587 """Read mesh tags from the physical groups of a Gmsh file. 

588 

589 Args: 

590 filename: Path to file to read from 

591 comm: MPI communicator used in storage 

592 name: ``"cell"``, ``"facet"``, ``"ridge"`` or ``"peak"`` for all physical groups of 

593 that codimension, as in :func:`dolfinx.io.gmsh.model_to_mesh`. Otherwise the 

594 physical group called ``name``, or the groups called ``f"{name}_{tag}"``, as 

595 written by :func:`write_meshtags`. 

596 backend_args: Arguments to backend 

597 

598 Returns: 

599 Internal data structure for the mesh tags read from file, with the physical tag as 

600 value. All data is on rank 0. 

601 """ 

602 check_file_exists(filename) 

603 

604 def read( 

605 preexisting_views: list[int], 

606 ) -> tuple[tuple[npt.NDArray[np.int64], npt.NDArray[np.int32]], tuple[int, int]]: 

607 gmsh.merge(str(filename)) 

608 tagged = _tagged_entities() 

609 tdim = max(tagged) 

610 groups = gmsh.model.getPhysicalGroups() 

611 if name in codim_names: 

612 dim = tdim - codim_names[name] 

613 group_tags = [tag for d, tag in groups if d == dim] 

614 else: 

615 matches = [ 

616 (d, tag) 

617 for d, tag in groups 

618 if gmsh.model.getPhysicalName(d, tag) in (name, f"{name}_{tag}") 

619 ] 

620 if len(matches) == 0: 

621 available = sorted(gmsh.model.getPhysicalName(d, tag) for d, tag in groups) 

622 raise KeyError( 

623 f"Could not find physical group '{name}' in {filename}. Available: {available}" 

624 ) 

625 if len(dims := {d for d, _ in matches}) != 1: 

626 raise RuntimeError(f"Physical groups '{name}' have several dimensions {dims}.") 

627 dim = dims.pop() 

628 group_tags = [tag for _, tag in matches] 

629 if dim not in tagged: 

630 return (np.zeros((0, 0), dtype=np.int64), np.zeros(0, dtype=np.int32)), (dim, 0) 

631 if (entity_data := tagged[dim]) is None: 

632 raise NotImplementedError(f"Mixed or unsupported element types of dimension {dim}.") 

633 _, entities, tag_values, _, _ = entity_data 

634 keep = np.isin(tag_values, group_tags) 

635 return (entities[keep], tag_values[keep]), (dim, entities.shape[1]) 

636 

637 local, (dim, num_nodes_per_entity) = _run_on_root(comm, read) 

638 if local is None: 

639 local = (np.zeros((0, num_nodes_per_entity), dtype=np.int64), np.zeros(0, dtype=np.int32)) 

640 return MeshTagsData(name=name, values=local[1], indices=local[0], dim=dim) 

641 

642 

643def read_dofmap( 

644 filename: str | Path, 

645 comm: MPI.Comm, 

646 name: str, 

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

648) -> dolfinx.graph.AdjacencyList: 

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

650 

651 Args: 

652 filename: Path to file to read from 

653 comm: MPI communicator used in storage 

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

655 backend_args: Arguments to backend 

656 

657 Returns: 

658 Dofmap as an :class:`dolfinx.graph.AdjacencyList` 

659 """ 

660 raise NotImplementedError("The gmsh backend cannot make checkpoints.") 

661 

662 

663def read_dofs( 

664 filename: str | Path, 

665 comm: MPI.Comm, 

666 name: str, 

667 time: float, 

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

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

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

671 

672 Args: 

673 filename: Path to file to read from 

674 comm: MPI communicator used in storage 

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

676 time: Time stamp associated with the function to read 

677 backend_args: Arguments to backend 

678 

679 Returns: 

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

681 and the global starting point on the process. 

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

683 """ 

684 raise NotImplementedError("The gmsh backend cannot make checkpoints.") 

685 

686 

687def read_cell_perms( 

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

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

690 """ 

691 Read cell permutation from file with given communicator, 

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

693 

694 Args: 

695 comm: MPI communicator used in storage 

696 filename: Path to file to read from 

697 backend_args: Arguments to backend 

698 

699 Returns: 

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

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

702 """ 

703 raise NotImplementedError("The gmsh backend cannot make checkpoints.") 

704 

705 

706def write_function( 

707 filename: Path | str, 

708 comm: MPI.Comm, 

709 u: FunctionData, 

710 time: float, 

711 mode: FileMode, 

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

713): 

714 """Write a function to file. 

715 

716 Args: 

717 filename: Path to file to write to 

718 comm: MPI communicator used in storage 

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

720 time: Time stamp associated with function 

721 mode: File-mode to store the function 

722 backend_args: Arguments to backend 

723 """ 

724 raise NotImplementedError("The gmsh backend cannot make checkpoints.") 

725 

726 

727def read_legacy_mesh( 

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

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

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

731 legacy DOLFIN HDF5-file. 

732 

733 Args: 

734 filename: Path to file to read from 

735 comm: MPI communicator used in storage 

736 group: Group in HDF5 file where mesh is stored 

737 

738 Returns: 

739 Tuple containing: 

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

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

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

743 """ 

744 raise NotImplementedError("The gmsh backend cannot read legacy DOLFIN meshes.") 

745 

746 

747def snapshot_checkpoint( 

748 filename: Path | str, 

749 mode: FileMode, 

750 u: dolfinx.fem.Function, 

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

752): 

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

754 

755 Args: 

756 filename: Path to file to read from 

757 mode: File-mode to store the function 

758 u: dolfinx function to create a snapshot checkpoint for 

759 backend_args: Arguments to backend 

760 """ 

761 raise NotImplementedError("The gmsh backend cannot make checkpoints.") 

762 

763 

764def read_hdf5_array( 

765 comm: MPI.Comm, 

766 filename: Path | str, 

767 group: str, 

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

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

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

771 

772 Args: 

773 comm: MPI communicator used in storage 

774 filename: Path to file to read from 

775 group: Group in HDF5 file where array is stored 

776 backend_args: Arguments to backend 

777 

778 Returns: 

779 Tuple containing: 

780 - Numpy array read from file 

781 - Global starting point on the process. 

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

783 """ 

784 raise NotImplementedError("The gmsh backend cannot read HDF5 arrays.") 

785 

786 

787def read_point_data( 

788 filename: Path | str, 

789 name: str, 

790 comm: MPI.Comm, 

791 time: str | float | None, 

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

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

794 """Read point data (``$NodeData``) from a Gmsh file. 

795 

796 Args: 

797 filename: Path to file 

798 name: Name of point data 

799 comm: Communicator to launch IO on. 

800 time: Time of the step to read, ``None`` for the first step 

801 backend_args: Arguments to backend, ``"dtype"`` sets the type of the data, or of 

802 its real and imaginary parts for complex data 

803 

804 Returns: 

805 Tuple containing: 

806 - The data of shape ``(num_nodes, num_components)``, ordered by node tag, on 

807 rank 0. Nodes without data get zeros. Complex data, stored as the views 

808 ``real_{name}`` and ``imag_{name}``, is combined. 

809 - The global start of the data on this process, ``0`` 

810 """ 

811 check_file_exists(filename) 

812 backend_args = get_default_backend_args(backend_args) 

813 

814 def read( 

815 preexisting_views: list[int], 

816 ) -> tuple[npt.NDArray[np.float64 | np.complex128], tuple[int, bool]]: 

817 gmsh.merge(str(filename)) 

818 if (found := _find_view(name, preexisting_views)) is None: 

819 raise KeyError(f"Could not find point data '{name}' in {filename}.") 

820 views, times = found 

821 if time is None: 

822 step = 0 

823 else: 

824 steps = np.flatnonzero(np.isclose(times, float(time))) 

825 if len(steps) != 1: 

826 raise KeyError( 

827 f"Expected one step of '{name}' at time {time} in {filename}, found " 

828 f"{len(steps)}. Available times: {times.tolist()}" 

829 ) 

830 step = int(steps[0]) 

831 parts = [] 

832 for view in views: 

833 data_type, tags, data, _, num_components = gmsh.view.getModelData(view, step) 

834 if data_type != "NodeData": 

835 raise ValueError(f"'{name}' in {filename} is {data_type}, not NodeData.") 

836 part = np.zeros((gmsh.model.mesh.getMaxNodeTag(), num_components), dtype=np.float64) 

837 part[np.asarray(tags, dtype=np.int64) - 1] = np.asarray(data).reshape( 

838 len(tags), num_components 

839 ) 

840 parts.append(part) 

841 values = parts[0] if len(parts) == 1 else parts[0] + 1j * parts[1] 

842 return values, (num_components, len(parts) == 2) 

843 

844 values, (num_components, is_complex) = _run_on_root(comm, read) 

845 dtype = backend_args["dtype"] 

846 if is_complex: 

847 # complex64 from float32 parts, complex128 from float64 

848 dtype = np.result_type(dtype, np.complex64) 

849 if values is None: 

850 values = np.zeros((0, num_components), dtype=dtype) 

851 return values.astype(dtype), 0 

852 

853 

854def read_cell_data( 

855 filename: Path | str, 

856 name: str, 

857 comm: MPI.Comm, 

858 time: str | float | None, 

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

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

861 """Read cell data (``$ElementData``) from a Gmsh file. 

862 

863 Args: 

864 filename: Path to file 

865 name: Name of cell data 

866 comm: Communicator to launch IO on. 

867 time: Time of the step to read, ``None`` for the first step 

868 backend_args: Arguments to backend, ``"dtype"`` sets the type of the data, or of 

869 its real and imaginary parts for complex data 

870 

871 Returns: 

872 Tuple containing: 

873 - The nodes of the cells with data, as zero-based node indices in DOLFINx 

874 ordering, on rank 0 

875 - The data of these cells, of shape ``(num_cells, num_components)``, on rank 0. 

876 Complex data, stored as the views ``real_{name}`` and ``imag_{name}``, is 

877 combined. 

878 """ 

879 check_file_exists(filename) 

880 backend_args = get_default_backend_args(backend_args) 

881 

882 def read( 

883 preexisting_views: list[int], 

884 ) -> tuple[ 

885 tuple[npt.NDArray[np.int64], npt.NDArray[np.float64 | np.complex128]], 

886 tuple[int, int, bool], 

887 ]: 

888 gmsh.merge(str(filename)) 

889 if (found := _find_view(name, preexisting_views)) is None: 

890 raise KeyError(f"Could not find cell data '{name}' in {filename}.") 

891 views, times = found 

892 if time is None: 

893 step = 0 

894 else: 

895 steps = np.flatnonzero(np.isclose(times, float(time))) 

896 if len(steps) != 1: 

897 raise KeyError( 

898 f"Expected one step of '{name}' at time {time} in {filename}, found " 

899 f"{len(steps)}. Available times: {times.tolist()}" 

900 ) 

901 step = int(steps[0]) 

902 tagged = _tagged_entities() 

903 if (cell_data := tagged[max(tagged)]) is None: 

904 raise NotImplementedError(f"Mixed or unsupported cell types in {filename}.") 

905 element_tags, cells = cell_data[:2] 

906 parts, part_tags = [], [] 

907 for view in views: 

908 data_type, tags, data, _, num_components = gmsh.view.getModelData(view, step) 

909 if data_type != "ElementData": 

910 raise ValueError(f"'{name}' in {filename} is {data_type}, not ElementData.") 

911 part_tags.append(np.asarray(tags, dtype=np.int64)) 

912 parts.append(np.asarray(data, dtype=np.float64).reshape(len(tags), num_components)) 

913 if len(parts) == 2 and not np.array_equal(part_tags[0], part_tags[1]): 

914 raise ValueError(f"The real and imaginary parts of '{name}' are on different cells.") 

915 rows = np.searchsorted(element_tags, part_tags[0]) 

916 if np.any(rows >= len(element_tags)) or np.any(element_tags[rows] != part_tags[0]): 

917 raise ValueError(f"'{name}' in {filename} holds data on elements that are not cells.") 

918 values = parts[0] if len(parts) == 1 else parts[0] + 1j * parts[1] 

919 return (cells[rows], values), (num_components, cells.shape[1], len(parts) == 2) 

920 

921 local, (num_components, num_nodes_per_cell, is_complex) = _run_on_root(comm, read) 

922 dtype = backend_args["dtype"] 

923 if is_complex: 

924 # complex64 from float32 parts, complex128 from float64 

925 dtype = np.result_type(dtype, np.complex64) 

926 if local is None: 

927 local = ( 

928 np.zeros((0, num_nodes_per_cell), dtype=np.int64), 

929 np.zeros((0, num_components), dtype=dtype), 

930 ) 

931 return local[0], local[1].astype(dtype) 

932 

933 

934def write_data( 

935 filename: Path | str, 

936 array_data: ArrayData, 

937 comm: MPI.Comm, 

938 time: str | float | None, 

939 mode: FileMode, 

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

941): 

942 """Write point or cell data to a Gmsh file, as a step of the view named after it. 

943 

944 Point data is written as ``$NodeData`` and cell data as ``$ElementData``, keyed by 

945 global index + 1. Complex data is split into the views ``real_{name}`` and 

946 ``imag_{name}``, as :class:`dolfinx.io.XDMFFile` does. The mesh must have been 

947 written to the file with :func:`write_mesh`, from the same mesh. 

948 

949 Args: 

950 filename: Path to file 

951 array_data: Data to write to file 

952 comm: MPI communicator to open the file with 

953 time: Time of the step, ``None`` for ``0`` 

954 mode: :attr:`FileMode.append` adds the step, or replaces the step at the same 

955 time; :attr:`FileMode.write` replaces all steps of the data 

956 backend_args: Arguments to backend, ``"binary"`` selects binary MSH 

957 """ 

958 if (data_type := {"Point": "NodeData", "Cell": "ElementData"}.get(array_data.type)) is None: 

959 raise ValueError(f"Unknown data type {array_data.type}, expected 'Point' or 'Cell'.") 

960 check_file_exists(filename) 

961 backend_args = get_default_backend_args(backend_args) 

962 step_time = 0.0 if time is None else float(time) 

963 values = comm.gather(array_data.values, root=0) 

964 

965 def write(preexisting_views: list[int]) -> tuple[None, None]: 

966 assert values is not None 

967 if (binary := backend_args["binary"]) is None: 

968 # The second line of an MSH file is "version file-type data-size" 

969 with open(filename, "rb") as file: 

970 file.readline() 

971 binary = file.readline().split()[1] == b"1" 

972 

973 gmsh.merge(str(filename)) 

974 data = np.vstack(values) 

975 name = array_data.name 

976 if np.iscomplexobj(data): 

977 parts = [(f"real_{name}", data.real), (f"imag_{name}", data.imag)] 

978 else: 

979 parts = [(name, data)] 

980 for part_name, part in parts: 

981 found = _find_view(part_name, preexisting_views) 

982 if found is not None and len(found[0]) > 1: 

983 raise ValueError(f"'{part_name}' in {filename} is complex data.") 

984 if found is not None and mode == FileMode.write: 

985 gmsh.view.remove(found[0][0]) 

986 found = None 

987 if found is None: 

988 view = gmsh.view.add(part_name) 

989 step = 0 

990 else: 

991 view, times = found[0][0], found[1] 

992 if (existing := gmsh.view.getModelData(view, 0)[0]) != data_type: 

993 raise ValueError(f"'{part_name}' in {filename} is {existing}.") 

994 same_time = np.flatnonzero(np.isclose(times, step_time)) 

995 step = int(same_time[0]) if len(same_time) > 0 else len(times) 

996 gmsh.view.addModelData( 

997 view, 

998 step, 

999 gmsh.model.getCurrent(), 

1000 data_type, 

1001 np.arange(1, part.shape[0] + 1), 

1002 part.astype(np.float64), 

1003 time=step_time, 

1004 numComponents=part.shape[1], 

1005 ) 

1006 

1007 gmsh.option.setNumber("Mesh.Binary", int(binary)) 

1008 gmsh.option.setNumber("Mesh.SaveAll", 1) 

1009 gmsh.write(str(filename)) 

1010 # Append the views without repeating the mesh 

1011 gmsh.option.setNumber("PostProcessing.SaveMesh", 0) 

1012 for v in gmsh.view.getTags(): 

1013 if v not in preexisting_views: 

1014 gmsh.view.write(v, str(filename), append=True) 

1015 return None, None 

1016 

1017 _run_on_root(comm, write)