Coverage for / dolfinx-env / lib / python3.12 / site-packages / io4dolfinx / checkpointing.py: 97%
206 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-26 18:16 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-26 18:16 +0000
1# Copyright (C) 2023-2026 Jørgen Schartum Dokken
2#
3# This file is part of io4dolfinx
4#
5# SPDX-License-Identifier: MIT
7from __future__ import annotations
9import typing
10from pathlib import Path
11from typing import Any
13from mpi4py import MPI
15import basix
16import dolfinx
17import numpy as np
18import numpy.typing as npt
19import ufl
20from packaging.version import Version
22from .backends import FileMode, ReadMode, get_backend
23from .comm_helpers import (
24 send_and_recv_cell_perm,
25 send_dofmap_and_recv_values,
26 send_dofs_and_recv_values,
27)
28from .readers import create_geometry_function_space
29from .structures import ArrayData, FunctionData, MeshTagsData
30from .utils import (
31 check_file_exists,
32 compute_dofmap_pos,
33 compute_local_range,
34 index_owner,
35 unroll_dofmap,
36 unroll_insert_position,
37)
38from .writers import prepare_meshdata_for_storage
39from .writers import write_function as _internal_function_writer
40from .writers import write_mesh as _internal_mesh_writer
42__all__ = [
43 "read_mesh",
44 "write_function",
45 "read_function",
46 "write_mesh",
47 "read_meshtags",
48 "write_meshtags",
49 "read_attributes",
50 "write_attributes",
51]
54def write_attributes(
55 filename: Path | str,
56 comm: MPI.Intracomm,
57 name: str,
58 attributes: dict[str, np.ndarray],
59 backend_args: dict[str, typing.Any] | None = None,
60 backend: str = "adios2",
61):
62 """Write attributes to file.
64 Args:
65 filename: Path to file to write to
66 comm: MPI communicator used in storage
67 name: Name of the attributes
68 attributes: Dictionary of attributes to write to file
69 backend_args: Arguments for backend, for instance file type.
70 backend: What backend to use for writing.
71 """
72 backend_cls = get_backend(backend)
73 backend_args = backend_cls.get_default_backend_args(backend_args)
74 backend_cls.write_attributes(filename, comm, name, attributes, backend_args)
77def read_attributes(
78 filename: Path | str,
79 comm: MPI.Intracomm,
80 name: str,
81 backend_args: dict[str, typing.Any] | None = None,
82 backend: str = "adios2",
83) -> dict[str, typing.Any]:
84 """Read attributes from file.
86 Args:
87 filename: Path to file to read from
88 comm: MPI communicator used in storage
89 name: Name of the attributes
90 backend_args: Arguments for backend, for instance file type.
91 backend: What backend to use for writing.
92 Returns:
93 The attributes
94 """
95 backend_cls = get_backend(backend)
96 backend_args = backend_cls.get_default_backend_args(backend_args)
97 return backend_cls.read_attributes(filename, comm, name, backend_args)
100def read_timestamps(
101 filename: Path | str,
102 comm: MPI.Intracomm,
103 function_name: str,
104 backend_args: dict[str, typing.Any] | None = None,
105 backend: str = "adios2",
106) -> npt.NDArray[np.float64 | str]: # type: ignore[type-var]
107 """
108 Read time-stamps from a checkpoint file.
110 Args:
111 comm: MPI communicator
112 filename: Path to file
113 function_name: Name of the function to read time-stamps for
114 backend_args: Arguments for backend, for instance file type.
115 backend: What backend to use for writing.
116 Returns:
117 The time-stamps
118 """
119 check_file_exists(filename)
120 backend_cls = get_backend(backend)
121 backend_args = backend_cls.get_default_backend_args(backend_args)
122 return backend_cls.read_timestamps(filename, comm, function_name, backend_args)
125def write_meshtags(
126 filename: Path | str,
127 mesh: dolfinx.mesh.Mesh,
128 meshtags: dolfinx.mesh.MeshTags,
129 meshtag_name: typing.Optional[str] = None,
130 backend_args: dict[str, Any] | None = None,
131 backend: str = "adios2",
132):
133 """
134 Write meshtags associated with input mesh to file.
136 .. note::
137 For this checkpoint to work, the mesh must be written to file
138 using :func:`write_mesh` before calling this function.
140 Args:
141 filename: Path to save meshtags (with file-extension)
142 mesh: The mesh associated with the meshtags
143 meshtags: The meshtags to write to file
144 meshtag_name: Name of the meshtag. If None, the meshtag name is used.
145 backend_args: Option to IO backend.
146 backend: IO backend
147 """
149 # Extract data from meshtags (convert to global geometry node indices for each entity)
150 tag_entities = meshtags.indices
151 dim = meshtags.dim
152 num_tag_entities_local = mesh.topology.index_map(dim).size_local
153 local_tag_entities = tag_entities[tag_entities < num_tag_entities_local]
154 local_values = meshtags.values[: len(local_tag_entities)]
156 num_saved_tag_entities = len(local_tag_entities)
157 local_start = mesh.comm.exscan(num_saved_tag_entities, op=MPI.SUM)
158 local_start = local_start if mesh.comm.rank != 0 else 0
159 global_num_tag_entities = mesh.comm.allreduce(num_saved_tag_entities, op=MPI.SUM)
160 dof_layout = mesh.geometry.cmap.create_dof_layout()
161 if hasattr(dof_layout, "num_entity_closure_dofs"):
162 num_dofs_per_entity = dof_layout.num_entity_closure_dofs(dim)
163 else:
164 num_dofs_per_entity = len(dof_layout.entity_closure_dofs(dim, 0))
166 entities_to_geometry = dolfinx.cpp.mesh.entities_to_geometry(
167 mesh._cpp_object, dim, local_tag_entities, False
168 )
170 indices = (
171 mesh.geometry.index_map()
172 .local_to_global(entities_to_geometry.reshape(-1))
173 .reshape(entities_to_geometry.shape)
174 )
175 name = meshtag_name or meshtags.name
177 tag_ct = dolfinx.cpp.mesh.cell_entity_type(mesh.topology.cell_type, dim, 0).name
178 tag_data = MeshTagsData(
179 values=local_values,
180 num_entities_global=global_num_tag_entities,
181 num_dofs_per_entity=num_dofs_per_entity,
182 indices=indices,
183 name=name,
184 local_start=local_start,
185 dim=meshtags.dim,
186 cell_type=tag_ct,
187 )
189 # Get backend and default arguments
190 backend_cls = get_backend(backend)
191 backend_args = backend_cls.get_default_backend_args(backend_args)
192 return backend_cls.write_meshtags(filename, mesh.comm, tag_data, backend_args=backend_args)
195def read_meshtags(
196 filename: Path | str,
197 mesh: dolfinx.mesh.Mesh,
198 meshtag_name: str,
199 backend_args: dict[str, Any] | None = None,
200 backend: str = "adios2",
201) -> dolfinx.mesh.MeshTags:
202 """
203 Read meshtags from file and return a :class:`dolfinx.mesh.MeshTags` object.
205 Args:
206 filename: Path to meshtags file (with file-extension)
207 mesh: The mesh associated with the meshtags
208 meshtag_name: The name of the meshtag to read
209 engine: Adios2 Engine
210 Returns:
211 The meshtags
212 """
213 check_file_exists(filename)
214 backend_cls = get_backend(backend)
215 backend_args = backend_cls.get_default_backend_args(backend_args)
216 data = backend_cls.read_meshtags_data(filename, mesh.comm, meshtag_name, backend_args)
218 local_entities, local_values = dolfinx.io.distribute_entity_data(
219 mesh, int(data.dim), data.indices, data.values
220 )
221 mesh.topology.create_connectivity(data.dim, 0)
222 mesh.topology.create_connectivity(data.dim, mesh.topology.dim)
224 adj = dolfinx.graph.adjacencylist(local_entities)
226 local_values = np.array(local_values, dtype=np.int32)
228 mt = dolfinx.mesh.meshtags_from_entities(mesh, int(data.dim), adj, local_values)
229 mt.name = meshtag_name
230 return mt
233def read_function(
234 filename: Path | str,
235 u: dolfinx.fem.Function,
236 time: float = 0.0,
237 name: str | None = None,
238 backend_args: dict[str, Any] | None = None,
239 backend: str = "adios2",
240):
241 """
242 Read checkpoint from file and fill it into `u`.
244 Args:
245 filename: Path to checkpoint
246 u: Function to fill
247 time: Time-stamp associated with checkpoint
248 name: If not provided, `u.name` is used to search through the input file for the function
249 """
250 check_file_exists(filename)
252 mesh = u.function_space.mesh
253 comm = mesh.comm
254 if name is None:
255 name = u.name
257 # ----------------------Step 1---------------------------------
258 # Compute index of input cells and get cell permutation
259 num_owned_cells = mesh.topology.index_map(mesh.topology.dim).size_local
260 input_cells = mesh.topology.original_cell_index[:num_owned_cells]
261 mesh.topology.create_entity_permutations()
262 cell_perm = mesh.topology.get_cell_permutation_info()[:num_owned_cells]
264 # Compute mesh->input communicator
265 # 1.1 Compute mesh->input communicator
266 backend_cls = get_backend(backend)
267 owners: npt.NDArray[np.int32]
268 if backend_cls.read_mode == ReadMode.serial:
269 owners = np.zeros(input_cells, dtype=np.int32)
270 elif backend_cls.read_mode == ReadMode.parallel:
271 num_cells_global = mesh.topology.index_map(mesh.topology.dim).size_global
272 owners = index_owner(mesh.comm, input_cells, num_cells_global)
273 else:
274 raise NotImplementedError(f"{backend_cls.read_mode} not implemented")
275 # -------------------Step 2------------------------------------
276 # Send and receive global cell index and cell perm
277 inc_cells, inc_perms = send_and_recv_cell_perm(input_cells, cell_perm, owners, mesh.comm)
279 # -------------------Step 3-----------------------------------
280 # Read dofmap from file and compute dof owners
281 check_file_exists(filename)
282 backend_cls = get_backend(backend)
283 backend_args = backend_cls.get_default_backend_args(backend_args)
285 input_dofmap = backend_cls.read_dofmap(filename, comm, name, backend_args)
287 # Compute owner of dofs in dofmap
288 dof_owner: npt.NDArray[np.int32]
289 if backend_cls.read_mode == ReadMode.serial:
290 dof_owner = np.zeros(len(input_dofmap.array), dtype=np.int32)
291 elif backend_cls.read_mode == ReadMode.parallel:
292 num_dofs_global = (
293 u.function_space.dofmap.index_map.size_global * u.function_space.dofmap.index_map_bs
294 )
295 dof_owner = index_owner(comm, input_dofmap.array.astype(np.int64), num_dofs_global)
296 else:
297 raise NotImplementedError(f"{backend_cls.read_mode} not implemented")
299 # --------------------Step 4-----------------------------------
300 # Read array from file and communicate them to input dofmap process
301 input_array, starting_pos = backend_cls.read_dofs(filename, comm, name, time, backend_args)
303 recv_array = send_dofs_and_recv_values(
304 input_dofmap.array.astype(np.int64), dof_owner, comm, input_array, starting_pos
305 )
307 # -------------------Step 5--------------------------------------
308 # Invert permutation of input data based on input perm
309 # Then apply current permutation to the local data
310 element = u.function_space.element
311 if element.needs_dof_transformations:
312 bs = u.function_space.dofmap.bs
314 # Read input cell permutations on dofmap process
315 local_input_range = compute_local_range(comm, num_cells_global)
316 input_local_cell_index = inc_cells - local_input_range[0]
317 input_perms = backend_cls.read_cell_perms(comm, filename, backend_args)
319 # Start by sorting data array by cell permutation
320 num_dofs_per_cell = input_dofmap.offsets[1:] - input_dofmap.offsets[:-1]
321 assert np.allclose(num_dofs_per_cell, num_dofs_per_cell[0])
323 # Sort dofmap by input local cell index
324 input_perms_sorted = input_perms[input_local_cell_index]
325 unrolled_dofmap_position = unroll_insert_position(
326 input_local_cell_index, num_dofs_per_cell[0]
327 )
328 dofmap_sorted_by_input = recv_array[unrolled_dofmap_position]
330 # First invert input data to reference element then transform to current mesh
331 element.Tt_apply(dofmap_sorted_by_input, input_perms_sorted, bs)
332 element.Tt_inv_apply(dofmap_sorted_by_input, inc_perms, bs)
333 # Compute invert permutation
334 inverted_perm = np.empty_like(unrolled_dofmap_position)
335 inverted_perm[unrolled_dofmap_position] = np.arange(
336 len(unrolled_dofmap_position), dtype=inverted_perm.dtype
337 )
338 recv_array = dofmap_sorted_by_input[inverted_perm]
340 # ------------------Step 6----------------------------------------
341 # For each dof owned by a process, find the local position in the dofmap.
342 V = u.function_space
343 local_cells, dof_pos = compute_dofmap_pos(V)
344 input_cells = V.mesh.topology.original_cell_index[local_cells]
345 num_cells_global = V.mesh.topology.index_map(V.mesh.topology.dim).size_global
347 if backend_cls.read_mode == ReadMode.serial:
348 owners = np.zeros(len(input_cells), dtype=np.int32)
349 elif backend_cls.read_mode == ReadMode.parallel:
350 owners = index_owner(V.mesh.comm, input_cells, num_cells_global)
351 else:
352 raise NotImplementedError(f"{backend_cls.read_mode} not implemented")
354 unique_owners, owner_count = np.unique(owners, return_counts=True)
355 # FIXME: In C++ use NBX to find neighbourhood
356 sub_comm = V.mesh.comm.Create_dist_graph(
357 [V.mesh.comm.rank], [len(unique_owners)], unique_owners, reorder=False
358 )
359 source, dest, _ = sub_comm.Get_dist_neighbors()
360 sub_comm.Free()
362 owned_values = send_dofmap_and_recv_values(
363 comm,
364 np.asarray(source, dtype=np.int32),
365 np.asarray(dest, dtype=np.int32),
366 owners,
367 owner_count.astype(np.int32),
368 input_cells,
369 dof_pos,
370 num_cells_global,
371 recv_array,
372 input_dofmap.offsets,
373 )
374 u.x.array[: len(owned_values)] = owned_values
375 u.x.scatter_forward()
378def read_mesh(
379 filename: Path | str,
380 comm: MPI.Intracomm,
381 ghost_mode: dolfinx.mesh.GhostMode = dolfinx.mesh.GhostMode.shared_facet,
382 time: float | str | None = 0.0,
383 read_from_partition: bool = False,
384 backend_args: dict[str, Any] | None = None,
385 backend: str = "adios2",
386 max_facet_to_cell_links: int = 2,
387) -> dolfinx.mesh.Mesh:
388 """
389 Read an ADIOS2 mesh into DOLFINx.
391 Args:
392 filename: Path to input file
393 comm: The MPI communciator to distribute the mesh over
394 ghost_mode: Ghost mode to use for mesh. If `read_from_partition`
395 is set to `True` this option is ignored.
396 time: Time stamp associated with mesh
397 read_from_partition: Read mesh with partition from file
398 backend_args: List of arguments to reader backend
399 max_facet_to_cell_links: Maximum number of cells a facet
400 can be connected to.
401 Returns:
402 The distributed mesh
403 """
404 # Read in data in a distributed fashin
405 check_file_exists(filename)
406 backend_cls = get_backend(backend)
407 backend_args = backend_cls.get_default_backend_args(backend_args)
409 # Let each backend handle what should be default behavior when reading mesh
410 # with or without time stamp.
411 dist_in_data = backend_cls.read_mesh_data(
412 filename,
413 comm,
414 time=time,
415 read_from_partition=read_from_partition,
416 backend_args=backend_args,
417 )
419 # Create DOLFINx mesh
420 element = basix.ufl.element(
421 basix.ElementFamily.P,
422 dist_in_data.cell_type,
423 dist_in_data.degree,
424 basix.LagrangeVariant(int(dist_in_data.lvar)),
425 shape=(dist_in_data.x.shape[1],),
426 dtype=dist_in_data.x.dtype,
427 )
428 domain = ufl.Mesh(element)
429 if (partition_graph := dist_in_data.partition_graph) is not None:
431 def partitioner(comm: MPI.Intracomm, n, m, topo):
432 assert len(topo[0]) % (len(partition_graph.offsets) - 1) == 0
433 if Version(dolfinx.__version__) > Version("0.9.0"):
434 return partition_graph._cpp_object
435 else:
436 return partition_graph
437 else:
438 try:
439 partitioner = dolfinx.cpp.mesh.create_cell_partitioner(
440 ghost_mode, max_facet_to_cell_links=max_facet_to_cell_links
441 )
442 except TypeError:
443 partitioner = dolfinx.cpp.mesh.create_cell_partitioner(ghost_mode)
445 # Should change to the commented code below when we require python
446 # minimum version to be >=3.12 see https://github.com/python/cpython/pull/116198
447 # import inspect
448 # sig = inspect.signature(dolfinx.mesh.create_cell_partitioner)
449 # part_kwargs = {}
450 # if "max_facet_to_cell_links" in list(sig.parameters.keys()):
451 # part_kwargs["max_facet_to_cell_links"] = max_facet_to_cell_links
452 # partitioner = dolfinx.cpp.mesh.create_cell_partitioner(ghost_mode, **part_kwargs)
454 return dolfinx.mesh.create_mesh(
455 comm,
456 cells=dist_in_data.cells,
457 x=dist_in_data.x,
458 e=domain,
459 partitioner=partitioner,
460 )
463def write_mesh(
464 filename: Path,
465 mesh: dolfinx.mesh.Mesh,
466 mode: FileMode = FileMode.write,
467 time: float = 0.0,
468 store_partition_info: bool = False,
469 backend_args: dict[str, Any] | None = None,
470 backend: str = "adios2",
471):
472 """
473 Write a mesh to file.
475 Args:
476 filename: Path to save mesh (without file-extension)
477 mesh: The mesh to write to file
479 store_partition_info: Store mesh partitioning (including ghosting) to file
480 """
481 mesh_data = prepare_meshdata_for_storage(mesh=mesh, store_partition_info=store_partition_info)
483 _internal_mesh_writer(
484 filename,
485 mesh.comm,
486 mesh_data=mesh_data,
487 time=time,
488 backend_args=backend_args,
489 backend=backend,
490 mode=mode,
491 )
494def write_function(
495 filename: Path | str,
496 u: dolfinx.fem.Function,
497 time: float = 0.0,
498 mode: FileMode = FileMode.append,
499 name: str | None = None,
500 backend_args: dict[str, Any] | None = None,
501 backend: str = "adios2",
502):
503 """
504 Write function checkpoint to file.
506 Args:
507 u: Function to write to file
508 time: Time-stamp for simulation
509 filename: Path to write to
510 mode: Write or append.
511 name: Name of function to write. If None, the name of the function is used.
512 backend_args: Arguments to the IO backend.
513 backend: The backend to use
514 """
515 dofmap = u.function_space.dofmap
516 values = u.x.array
517 mesh = u.function_space.mesh
518 comm = mesh.comm
519 mesh.topology.create_entity_permutations()
520 cell_perm = mesh.topology.get_cell_permutation_info()
521 num_cells_local = mesh.topology.index_map(mesh.topology.dim).size_local
522 local_cell_range = mesh.topology.index_map(mesh.topology.dim).local_range
523 num_cells_global = mesh.topology.index_map(mesh.topology.dim).size_global
525 # Convert local dofmap into global_dofmap
526 dmap = dofmap.list
527 num_dofs_per_cell = dmap.shape[1]
528 dofmap_bs = dofmap.bs
529 num_dofs_local_dmap = num_cells_local * num_dofs_per_cell * dofmap_bs
530 index_map_bs = dofmap.index_map_bs
532 # Unroll dofmap for block size
533 unrolled_dofmap = unroll_dofmap(dofmap.list[:num_cells_local, :], dofmap_bs)
534 dmap_loc = (unrolled_dofmap // index_map_bs).reshape(-1)
535 dmap_rem = (unrolled_dofmap % index_map_bs).reshape(-1)
537 # Convert imap index to global index
538 imap_global = dofmap.index_map.local_to_global(dmap_loc)
539 dofmap_global = imap_global * index_map_bs + dmap_rem
540 dofmap_imap = dolfinx.common.IndexMap(mesh.comm, num_dofs_local_dmap)
542 # Compute dofmap offsets
543 local_dofmap_offsets = np.arange(num_cells_local + 1, dtype=np.int64)
544 local_dofmap_offsets[:] *= num_dofs_per_cell * dofmap_bs
545 local_dofmap_offsets += dofmap_imap.local_range[0]
547 num_dofs_global = dofmap.index_map.size_global * dofmap.index_map_bs
548 local_dof_range = np.asarray(dofmap.index_map.local_range) * dofmap.index_map_bs
549 num_dofs_local = local_dof_range[1] - local_dof_range[0]
551 # Create internal data structure for function data to write to file
552 function_data = FunctionData(
553 cell_permutations=cell_perm[:num_cells_local].copy(),
554 local_cell_range=local_cell_range,
555 num_cells_global=num_cells_global,
556 dofmap_array=dofmap_global,
557 dofmap_offsets=local_dofmap_offsets,
558 dofmap_range=dofmap_imap.local_range,
559 global_dofs_in_dofmap=dofmap_imap.size_global,
560 values=values[:num_dofs_local].copy(),
561 dof_range=local_dof_range,
562 num_dofs_global=num_dofs_global,
563 name=name or u.name,
564 )
565 # Write to file
566 fname = Path(filename)
567 _internal_function_writer(
568 fname, comm, function_data, time, backend_args=backend_args, backend=backend, mode=mode
569 )
572def read_function_names(
573 filename: Path | str,
574 comm: MPI.Intracomm,
575 backend_args: dict[str, Any] | None = None,
576 backend: str = "h5py",
577) -> list[str]:
578 """Read all function names from a file.
580 Args:
581 filename: Path to file
582 comm: MPI communicator to launch IO on.
583 backend_args: Arguments to backend
585 Returns:
586 A list of function names.
587 """
588 backend_cls = get_backend(backend)
589 return backend_cls.read_function_names(filename, comm, backend_args=backend_args)
592def write_point_data(
593 filename: Path | str,
594 u: dolfinx.fem.Function,
595 time: str | float | None,
596 mode: FileMode,
597 backend_args: dict[str, Any] | None,
598 backend: str = "vtkhdf",
599):
600 """Write function to file by interpolating into geometry nodes.
603 Args:
604 filename: Path to file
605 u: The function to store
606 time: Time stamp
607 mode: Append or write
608 backend_args: The backend arguments
609 backend: Which backend to use.
610 """
611 V = create_geometry_function_space(u.function_space.mesh, int(np.prod(u.ufl_shape)))
612 v_out = dolfinx.fem.Function(V, name=u.name, dtype=u.x.array.dtype)
613 v_out.interpolate(u)
614 comm = v_out.function_space.mesh.comm
615 data_shape = (V.dofmap.index_map.size_global, V.dofmap.index_map_bs)
616 local_range = V.dofmap.index_map.local_range
617 num_dofs_local = V.dofmap.index_map.size_local
618 data = v_out.x.array.reshape(-1, V.dofmap.index_map_bs)[:num_dofs_local]
619 ad = ArrayData(
620 name=v_out.name, values=data, global_shape=data_shape, local_range=local_range, type="Point"
621 )
622 backend_cls = get_backend(backend)
623 return backend_cls.write_data(
624 filename, comm=comm, mode=mode, time=time, array_data=ad, backend_args=backend_args
625 )
628def write_cell_data(
629 filename: Path | str,
630 u: dolfinx.fem.Function,
631 time: str | float | None,
632 mode: FileMode,
633 backend_args: dict[str, Any] | None,
634 backend: str = "vtkhdf",
635):
636 """Write function to file by interpolating into cell midpoints.
639 Args:
640 filename: Path to file
641 point_data: Data to write to file
642 time: Time stamp
643 mode: Append or write
644 backend_args: The backend arguments
645 """
646 V = dolfinx.fem.functionspace(u.function_space.mesh, ("DG", 0, u.ufl_shape))
647 v_out = dolfinx.fem.Function(V, name=u.name, dtype=u.x.array.dtype)
648 v_out.interpolate(u)
649 comm = v_out.function_space.mesh.comm
650 data_shape = (V.dofmap.index_map.size_global, V.dofmap.index_map_bs)
651 local_range = V.dofmap.index_map.local_range
652 num_dofs_local = V.dofmap.index_map.size_local
653 data = v_out.x.array.reshape(-1, V.dofmap.index_map_bs)[:num_dofs_local]
655 backend_cls = get_backend(backend)
656 ad = ArrayData(
657 name=v_out.name, values=data, global_shape=data_shape, local_range=local_range, type="Cell"
658 )
659 backend_cls = get_backend(backend)
660 return backend_cls.write_data(
661 filename, comm=comm, mode=mode, time=time, array_data=ad, backend_args=backend_args
662 )