Loading...
Searching...
No Matches
xdmf.hpp
Go to the documentation of this file.
1
2#pragma once
3
4#include <fstream>
5
6#include "mpi/mpi.hpp"
8#include "util/filesystem.hpp"
9#include "util/logging.hpp"
10#include "util/result.hpp"
11#include "util/xml.hpp"
12
13namespace terra::io {
14
15/// @brief XDMF output that simultaneously serves for visualization with software like Paraview and as a simulation
16/// checkpoint.
17///
18/// # Overview
19///
20/// Writes simulation data (time-series for a constant mesh) and metadata into a directory.
21/// The written data simultaneously serves for visualization with tools that can read XDMF files (e.g., Paraview) and
22/// can be used to restore the grid data, e.g., to continue a previous simulation (aka checkpoint).
23/// This does not involve data duplication. The data required for XDMF is directly used as a checkpoint.
24///
25/// Currently restricted to the spherical shell mesh data structure.
26/// Interprets data as block-structured wedge-element meshes.
27///
28/// # File overview (what is written)
29///
30/// The mesh data has to be added upon construction.
31/// None, one, or many scalar or vector-valued grids can be added afterward.
32///
33/// Each write() call then writes out
34/// - binary data files for each added grid data item containing the raw values,
35/// - an .xmf file to be used for visualization of all the binary files (read by an XDMF reader).
36///
37/// The first write() call also writes (once)
38/// - the mesh data (grid coordinates/topology) to binary files
39/// - a checkpoint metadata file required to read in the checkpoints
40///
41/// For time-dependent runs, call write() in, e.g., every timestep. The mesh written in the first call will be
42/// referenced from each .xmf file.
43///
44/// # Output file precision
45///
46/// The actually written data type can be selected regardless of the underlying data type of the allocated data for the
47/// mesh points, topology, and each data grid. Defaults are selected via default parameters.
48/// Concretely, you can write your double precision fields in single precision without converting them manually.
49/// Note that this means that your checkpoints obviously have the same precision that you specify here.
50///
51/// # Other notes
52///
53/// All added data grids must have different (Kokkos::View-)labels.
54///
55/// Uses MPI IO for fast parallel output.
56///
57/// # Checkpoints
58///
59/// To recover a checkpoint, use the function \ref read_xdmf_checkpoint_grid() (or
60/// \ref read_xdmf_checkpoint_metadata() to just inspect the structure of the checkpoint).
61///
62/// Note that the checkpoint can only be read using the same domain partitioning (i.e., the 'topology' of the
63/// \ref DistributedDomain used when the checkpoint was written must be identical) - BUT the number of MPI processes
64/// does not need to match (nor does the distribution of subdomains to ranks need to match). So you can technically
65/// (if the amount of main memory permits) read a checkpoint from a large parallel simulation with only one or a few
66/// processes (possibly useful for post-processing).
67///
68/// ## Picking the last step during checkpoint recovery
69///
70/// The .xmf file for each write() call is written last (after the binary data).
71/// Thus, if the corresponding .xmf step has been written, the parallel binary data output should be completed.
72///
73/// # Checkpoint data binary format
74///
75/// All data is written to a single binary file per grid data item and per time step.
76/// Each subdomain is written contiguously. Concretely, per subdomain (also applies to the grid point coordinates)
77///
78/// @code
79/// for ( each subdomain )
80/// {
81/// for ( int r = 0; r < subdomain_size_r; r++ )
82/// {
83/// for ( int y = 0; y < subdomain_size_y; y++ )
84/// {
85/// for ( int x = 0; x < subdomain_size_x; x++ )
86/// {
87/// for ( int d = 0; d < point_dim; d++ )
88/// {
89/// stream << data( subdomain_id, x, y, r, d )
90/// }
91/// }
92/// }
93/// }
94/// }
95/// @endcode
96///
97/// The subdomain order depends on the various factors and can be basically random.
98/// The concrete ordering (as well as the data type) is reflected in the checkpoint metadata (see below).
99///
100/// # Checkpoint metadata format (required only once per time series)
101///
102/// See \ref terra::grid::shell::SubdomainInfo for how the global subdomain ID is encoded.
103///
104/// @code
105/// version: i32
106/// num_subdomains_per_diamond_lateral_direction: i32
107/// num_subdomains_per_diamond_radial_direction: i32
108/// subdomain_size_x: i32
109/// subdomain_size_y: i32
110/// subdomain_size_r: i32
111/// radii: array: f64, entries: num_subdomains_per_diamond_radial_direction * (subdomain_size_r - 1) + 1
112/// grid_scalar_bytes i32 // new in checkpoint version 1, number of float bytes for writing the grid (4 or 8 byte float)
113/// num_grid_data_files: i32
114/// list (size = num_grid_data_files)
115/// [
116/// grid_name_string_size_bytes: i32
117/// grid_name_string: grid_name_string_size_bytes * 8
118/// scalar_data_type: i32 // 0: signed integer, 1: unsigned integer, 2: float
119/// scalar_bytes: i32
120/// vec_dim: i32
121/// ]
122/// checkpoint_subdomain_ordering: i32
123/// if (checkpoint_subdomain_ordering == 0) {
124/// // The following list contains the encoded global_subdomain_ids (as i64) in the order in which the
125/// // subdomains are written to the data files. To find out where a certain subdomain is located in the
126/// // file, the starting offset must be set equal to the index of the global_subdomain_id in the list below,
127/// // multiplied by the size of a single chunk of data per subdomain.
128/// // That means that in the worst case, the entire list must be read to find the correct subdomain.
129/// // However, this way it is easy to _write_ the metadata since we do not need to globally sort the subdomain
130/// // positions in the parallel setting, and we get away with O(1) local memory usage during parsing.
131/// // Lookup is O(n), though.
132/// //
133/// // An alternative would be to sort the list before writing the checkpoint and store the _position of the
134/// // subdomain data_ instead of the global_subdomain_id. Since the global_subdomain_id is sortable, an
135/// // implicit mapping from global_subdomain_id to this list's index can be accomplished.
136/// // Then look up the position of the data in O(1).
137/// // However, that would require reducing the entire list on one process which is in theory not scalable
138/// // (plus the sorting approach is a tiny bit more complicated).
139/// // Use a different value for checkpoint_subdomain_ordering in that case and extend the file format.
140/// list (size = "num_global_subdomains" = 10 * num_subdomains_per_diamond_lateral_direction * num_subdomains_per_diamond_lateral_direction * num_subdomains_per_diamond_radial_direction)
141/// [
142/// global_subdomain_id: i64
143/// ]
144/// }
145/// @endcode
146///
147template < typename InputGridScalarType >
149{
150 public:
151 /// @brief Used to specify the output type when writing floating point data.
152 ///
153 /// Values are the number of bytes.
154 enum class OutputTypeFloat : int
155 {
156 Float32 = 4,
157 Float64 = 8,
158 };
159
160 /// @brief Used to specify the output type when writing (signed) integer data.
161 ///
162 /// Values are the number of bytes.
163 enum class OutputTypeInt : int
164 {
165 Int32 = 4,
166 Int64 = 8,
167 };
168
169 /// @brief Constructs an XDMFOutput object.
170 ///
171 /// All data will be written to the specified directory (it is a good idea to pass an empty directory).
172 ///
173 /// Construction does not write any data, yet.
174 ///
175 /// @param directory_path Path to a directory that the data shall be written to. If the directory does not exist,
176 /// it will be created during the first write() call. If it does already exist, data will be
177 /// overwritten.
178 /// @param distributed_domain \ref DistributedDomain instance.
179 /// @param coords_shell_device Lateral spherical shell grid coordinates (see \ref subdomain_unit_sphere_single_shell_coords()).
180 /// @param coords_radii_device Spherical shell radii (see \ref subdomain_shell_radii()).
181 /// @param output_type_points Floating point data type to use for mesh coordinate output.
182 /// @param output_type_connectivity Integer data type to use for mesh connectivity output.
184 const std::string& directory_path,
185 const grid::shell::DistributedDomain& distributed_domain,
186 const grid::Grid3DDataVec< InputGridScalarType, 3 >& coords_shell_device,
187 const grid::Grid2DDataScalar< InputGridScalarType >& coords_radii_device,
188 const OutputTypeFloat output_type_points = OutputTypeFloat::Float32,
189 const OutputTypeInt output_type_connectivity = OutputTypeInt::Int64 )
190 : directory_path_( directory_path )
191 , distributed_domain_( distributed_domain )
192 , coords_shell_device_( coords_shell_device )
193 , coords_radii_device_( coords_radii_device )
194 , output_type_points_( output_type_points )
195 , output_type_connectivity_( output_type_connectivity )
196 {
197 if ( coords_shell_device.extent( 0 ) != coords_radii_device.extent( 0 ) )
198 {
199 Kokkos::abort( "XDMF: Number of subdomains of shell and radii coords does not match." );
200 }
201 }
202
203 /// @brief Set the write counter manually.
204 ///
205 /// This will only affect the step number attached to the file names. The geometry is still written once during the
206 /// first write() call.
207 void set_write_counter( int write_counter ) { write_counter_ = write_counter; }
208
209 /// @brief Adds a new scalar data grid to be written out.
210 ///
211 /// Does not write any data to file yet - call write() for writing the next time step.
212 template < typename InputScalarDataType >
213 void
215 const OutputTypeFloat output_type = OutputTypeFloat::Float32 )
216 {
217 if ( first_write_happened_ )
218 {
219 Kokkos::abort( "XDMF::add(): Cannot add data after write() has been called." );
220 }
221
222 check_extents( data );
223
224 if ( is_label_taken( data.label() ) )
225 {
226 Kokkos::abort( ( "Cannot add data with label '" + data.label() +
227 "' - data with identical label has been added previously." )
228 .c_str() );
229 }
230
231 if constexpr ( std::is_same_v< InputScalarDataType, double > )
232 {
233 device_data_views_scalar_double_.push_back( { data, output_type } );
234 }
235 else if constexpr ( std::is_same_v< InputScalarDataType, float > )
236 {
237 device_data_views_scalar_float_.push_back( { data, output_type } );
238 }
239 else
240 {
241 Kokkos::abort( "XDMF::add(): Grid data type not supported (yet)." );
242 }
243 }
244
245 /// @brief Adds a new vector-valued data grid to be written out.
246 ///
247 /// Does not write any data to file yet - call write() for writing the next time step.
248 template < typename InputScalarDataType, int VecDim >
249 void
251 const OutputTypeFloat output_type = OutputTypeFloat::Float32 )
252 {
253 if ( first_write_happened_ )
254 {
255 Kokkos::abort( "XDMF::add(): Cannot add data after write() has been called." );
256 }
257
258 check_extents( data );
259
260 if ( is_label_taken( data.label() ) )
261 {
262 Kokkos::abort( ( "Cannot add data with label '" + data.label() +
263 "' - data with identical label has been added previously." )
264 .c_str() );
265 }
266
267 if constexpr ( std::is_same_v< InputScalarDataType, double > )
268 {
269 device_data_views_vec_double_.push_back( { data, output_type } );
270 }
271 else if constexpr ( std::is_same_v< InputScalarDataType, float > )
272 {
273 device_data_views_vec_float_.push_back( { data, output_type } );
274 }
275 else
276 {
277 Kokkos::abort( "XDMF::add(): Grid data type not supported (yet)." );
278 }
279 }
280
281 /// @brief Writes a "time step".
282 ///
283 /// Will write one .xmf file with the current counter as part of the name such that the files can be opened as a
284 /// time series.
285 ///
286 /// The first write() call after construction will also write the mesh data (binary files) that will be referenced
287 /// from later .xmf files, as well as checkpoint metadata.
288 ///
289 /// For each added data grid, one binary file is written. The data is copied to the host if required.
290 /// The write() calls will allocate temporary storage on the host if host and device memory are not shared.
291 /// Currently, for data grids, some host-side temporary buffers are kept after this method returns (the sizes depend
292 /// on the type of data added) to avoid frequent reallocation.
293 void write()
294 {
295 using util::XML;
296
297 const auto geometry_file_base = "geometry.bin";
298 const auto topology_file_base = "topology.bin";
299
300 const auto readme_file_path = directory_path_ + "/README.md";
301
302 const auto geometry_file_path = directory_path_ + "/" + geometry_file_base;
303 const auto topology_file_path = directory_path_ + "/" + topology_file_base;
304
305 const auto step_file_path = directory_path_ + "/step_" + std::to_string( write_counter_ ) + ".xmf";
306
307 const int num_subdomains = coords_shell_device_.extent( 0 );
308 const int nodes_x = coords_shell_device_.extent( 1 );
309 const int nodes_y = coords_shell_device_.extent( 2 );
310 const int nodes_r = coords_radii_device_.extent( 1 );
311
312 const int64_t number_of_nodes_local = static_cast< int64_t >( num_subdomains ) * nodes_x * nodes_y * nodes_r;
313 const int64_t number_of_elements_local = static_cast< int64_t >( num_subdomains ) * ( nodes_x - 1 ) * ( nodes_y - 1 ) * ( nodes_r - 1 ) * 2;
314
315 if ( !first_write_happened_ )
316 {
317 // Number of global nodes and elements.
318
319 int64_t num_nodes_elements_subdomains_global[3] = {
320 number_of_nodes_local, number_of_elements_local, static_cast< int64_t >( num_subdomains ) };
321 MPI_Allreduce( MPI_IN_PLACE, &num_nodes_elements_subdomains_global, 3, MPI_LONG_LONG, MPI_SUM, MPI_COMM_WORLD );
322 number_of_nodes_global_ = num_nodes_elements_subdomains_global[0];
323 number_of_elements_global_ = num_nodes_elements_subdomains_global[1];
324 number_of_subdomains_global_ = num_nodes_elements_subdomains_global[2];
325
326 // Check MPI write offset
327
328 // To be populated:
329 // First entry: number of nodes of processes before this
330 // Second entry: number of elements of processes before this
331 // Third entry: number of subdomains of processes before this
332 int64_t offsets[3];
333
334 int64_t local_values[3] = { number_of_nodes_local, number_of_elements_local, static_cast< int64_t >( num_subdomains ) };
335
336 // Compute the prefix sum (inclusive)
337 MPI_Scan( &local_values, &offsets, 3, MPI_LONG_LONG, MPI_SUM, MPI_COMM_WORLD );
338
339 // Subtract the local value to get the sum of all values from processes with ranks < current rank
340 number_of_nodes_offset_ = offsets[0] - local_values[0];
341 number_of_elements_offset_ = offsets[1] - local_values[1];
342 number_of_subdomains_offset_ = offsets[2] - local_values[2];
343
344 // Create a directory on root
345
346 util::prepare_empty_directory( directory_path_ );
347
348 // Add a README to the directory (what to keep, what the data contains, some notes on how to use paraview).
349
350 if ( mpi::rank() == 0 )
351 {
352 std::ofstream readme_stream( readme_file_path );
353 readme_stream
354 << "# This directory contains the output of the XDMF writer (for visualization and checkpointing).\n";
355 readme_stream << "\n";
356 readme_stream << "Overview:\n";
357 readme_stream << "- `geometry.bin`: cartesian node coordinates\n";
358 readme_stream << "- `topology.bin`: connectivity/topology/elements (whatever you want to call it)\n";
359 readme_stream << "- `checkpoint_metadata.bin`: metadata for checkpointing\n";
360 readme_stream
361 << "- `step_*.xmf`: xmf file (open this with Paraview for visualization) for each write() step\n";
362 readme_stream << "- `<some_name>_*.bin`: binary grid data (per write() step)\n";
363 readme_stream.close();
364 }
365
366 // Write mesh binary data if first write
367
368 // Node points.
369 {
370 std::stringstream geometry_stream;
371 switch ( output_type_points_ )
372 {
374 write_geometry_binary_data< float >( geometry_stream );
375 break;
377 write_geometry_binary_data< double >( geometry_stream );
378 break;
379 default:
380 Kokkos::abort( "XDMF: Unknown output type for geometry." );
381 }
382
383 MPI_File fh;
384 MPI_File_open(
385 MPI_COMM_WORLD, geometry_file_path.c_str(), MPI_MODE_CREATE | MPI_MODE_RDWR, MPI_INFO_NULL, &fh );
386
387 // Define the file view: each process writes its local data sequentially
388 MPI_Offset disp = number_of_nodes_offset_ * 3 * static_cast< int64_t >( output_type_points_ );
389 MPI_File_set_view( fh, disp, MPI_CHAR, MPI_CHAR, "native", MPI_INFO_NULL );
390
391 std::string geom_str = geometry_stream.str();
392
393 // Write data collectively
394 MPI_File_write_all(
395 fh, geom_str.data(), static_cast< int >( geom_str.size() ), MPI_CHAR, MPI_STATUS_IGNORE );
396
397 // Close the file
398 MPI_File_close( &fh );
399 }
400
401 // Connectivity/topology/elements (whatever you want to call it).
402 {
403 std::stringstream topology_stream;
404 switch ( output_type_connectivity_ )
405 {
407 write_topology_binary_data< int32_t >( topology_stream, number_of_nodes_offset_ );
408 break;
410 write_topology_binary_data< int64_t >( topology_stream, number_of_nodes_offset_ );
411 break;
412 default:
413 Kokkos::abort( "XDMF: Unknown output type for topology." );
414 }
415
416 MPI_File fh;
417 MPI_File_open(
418 MPI_COMM_WORLD, topology_file_path.c_str(), MPI_MODE_CREATE | MPI_MODE_RDWR, MPI_INFO_NULL, &fh );
419
420 // Define the file view: each process writes its local data sequentially
421 MPI_Offset disp = static_cast< int64_t >( 6 ) * number_of_elements_offset_ * static_cast< int64_t >( output_type_connectivity_ );
422 MPI_File_set_view( fh, disp, MPI_CHAR, MPI_CHAR, "native", MPI_INFO_NULL );
423
424 std::string topo_str = topology_stream.str();
425
426 // Write data collectively
427 MPI_File_write_all(
428 fh, topo_str.data(), static_cast< int >( topo_str.size() ), MPI_CHAR, MPI_STATUS_IGNORE );
429
430 // Close the file
431 MPI_File_close( &fh );
432 }
433
434 // Checkpoint metadata
435 {
436 write_checkpoint_metadata();
437 }
438 }
439
440 // Build XML skeleton.
441
442 auto xml = XML( "Xdmf", { { "Version", "2.0" } } );
443 auto domain = XML( "Domain" );
444 auto grid = XML( "Grid", { { "Name", "Grid" }, { "GridType", "Uniform" } } );
445
446 auto geometry =
447 XML( "Geometry", { { "Type", "XYZ" } } )
448 .add_child(
449 XML( "DataItem",
450 { { "Format", "Binary" },
451 { "DataType", "Float" },
452 { "Precision", std::to_string( static_cast< int >( output_type_points_ ) ) },
453 { "Endian", "Little" },
454 { "Dimensions", std::to_string( number_of_nodes_global_ ) + " " + std::to_string( 3 ) } },
455 geometry_file_base ) );
456
457 grid.add_child( geometry );
458
459 auto topology =
460 XML( "Topology",
461 { { "Type", "Wedge" }, { "NumberOfElements", std::to_string( number_of_elements_global_ ) } } )
462 .add_child(
463 XML( "DataItem",
464 { { "Format", "Binary" },
465 { "DataType", "Int" },
466 { "Precision", std::to_string( static_cast< int >( output_type_connectivity_ ) ) },
467 { "Endian", "Little" },
468 { "Dimensions", std::to_string( number_of_elements_global_ * 6 ) } },
469 topology_file_base ) );
470
471 grid.add_child( topology );
472
473 // Write attribute data for each attached grid.
474
475 for ( const auto& [data, output_type] : device_data_views_scalar_float_ )
476 {
477 const auto attribute = write_scalar_attribute_file( data, output_type );
478 grid.add_child( attribute );
479 }
480
481 for ( const auto& [data, output_type] : device_data_views_scalar_double_ )
482 {
483 const auto attribute = write_scalar_attribute_file( data, output_type );
484 grid.add_child( attribute );
485 }
486
487 for ( const auto& [data, output_type] : device_data_views_vec_float_ )
488 {
489 const auto attribute = write_vec_attribute_file( data, output_type );
490 grid.add_child( attribute );
491 }
492
493 for ( const auto& [data, output_type] : device_data_views_vec_double_ )
494 {
495 const auto attribute = write_vec_attribute_file( data, output_type );
496 grid.add_child( attribute );
497 }
498
499 domain.add_child( grid );
500 xml.add_child( domain );
501
502 if ( mpi::rank() == 0 )
503 {
504 std::ofstream step_stream( step_file_path );
505 step_stream << "<?xml version=\"1.0\" ?>\n";
506 step_stream << "<!DOCTYPE Xdmf SYSTEM \"Xdmf.dtd\" []>\n";
507 step_stream << xml.to_string();
508 step_stream.close();
509 }
510
511 write_counter_++;
512 first_write_happened_ = true;
513 }
514
515 private:
516 template < typename GridDataType >
517 void check_extents( const GridDataType& data )
518 {
519 if ( data.extent( 0 ) != coords_radii_device_.extent( 0 ) )
520 {
521 Kokkos::abort( "XDMF: Number of subdomains of added data item does not match mesh." );
522 }
523
524 if ( data.extent( 1 ) != coords_shell_device_.extent( 1 ) )
525 {
526 Kokkos::abort( "XDMF: Dim x of added data item does not match mesh." );
527 }
528
529 if ( data.extent( 2 ) != coords_shell_device_.extent( 2 ) )
530 {
531 Kokkos::abort( "XDMF: Dim y of added data item does not match mesh." );
532 }
533
534 if ( data.extent( 3 ) != coords_radii_device_.extent( 1 ) )
535 {
536 Kokkos::abort( "XDMF: Dim r of added data item does not match mesh." );
537 }
538 }
539
540 bool is_label_taken( const std::string& label )
541 {
542 for ( auto [grid, _] : device_data_views_scalar_double_ )
543 {
544 if ( grid.label() == label )
545 {
546 return true;
547 }
548 }
549
550 for ( auto [grid, _] : device_data_views_scalar_float_ )
551 {
552 if ( grid.label() == label )
553 {
554 return true;
555 }
556 }
557
558 for ( auto [grid, _] : device_data_views_vec_double_ )
559 {
560 if ( grid.label() == label )
561 {
562 return true;
563 }
564 }
565
566 for ( auto [grid, _] : device_data_views_vec_float_ )
567 {
568 if ( grid.label() == label )
569 {
570 return true;
571 }
572 }
573
574 return false;
575 }
576
577 template < std::floating_point FloatingPointOutputType >
578 void write_geometry_binary_data( std::stringstream& out )
579 {
580 // Copy mesh to host.
581 // We assume the mesh is only written once so we throw away the host copies after this method returns.
582 const typename grid::Grid3DDataVec< InputGridScalarType, 3 >::HostMirror coords_shell_host =
583 Kokkos::create_mirror_view_and_copy( Kokkos::HostSpace{}, coords_shell_device_ );
584 const typename grid::Grid2DDataScalar< InputGridScalarType >::HostMirror coords_radii_host =
585 Kokkos::create_mirror_view_and_copy( Kokkos::HostSpace{}, coords_radii_device_ );
586
587 for ( int local_subdomain_id = 0; local_subdomain_id < coords_shell_host.extent( 0 ); local_subdomain_id++ )
588 {
589 for ( int r = 0; r < coords_radii_host.extent( 1 ); r++ )
590 {
591 for ( int y = 0; y < coords_shell_host.extent( 2 ); y++ )
592 {
593 for ( int x = 0; x < coords_shell_host.extent( 1 ); x++ )
594 {
595 const auto c =
596 grid::shell::coords( local_subdomain_id, x, y, r, coords_shell_host, coords_radii_host );
597
598 for ( int d = 0; d < 3; d++ )
599 {
600 const auto cd = static_cast< FloatingPointOutputType >( c( d ) );
601 out.write( reinterpret_cast< const char* >( &cd ), sizeof( FloatingPointOutputType ) );
602 }
603 }
604 }
605 }
606 }
607 }
608
609 template < std::integral IntegerOutputType >
610 void write_topology_binary_data( std::stringstream& out, int64_t number_of_nodes_offset )
611 {
612 const int num_subdomains = coords_shell_device_.extent( 0 );
613 const int nodes_x = coords_shell_device_.extent( 1 );
614 const int nodes_y = coords_shell_device_.extent( 2 );
615 const int nodes_r = coords_radii_device_.extent( 1 );
616
617 const IntegerOutputType stride_0 = static_cast< IntegerOutputType >( nodes_x ) * nodes_y * nodes_r;
618 const IntegerOutputType stride_1 = static_cast< IntegerOutputType >( nodes_x ) * nodes_y;
619 const IntegerOutputType stride_2 = nodes_x;
620 const IntegerOutputType offset = static_cast< IntegerOutputType >( number_of_nodes_offset );
621
622 for ( int local_subdomain_id = 0; local_subdomain_id < num_subdomains; local_subdomain_id++ )
623 {
624 for ( int r = 0; r < nodes_r - 1; r++ )
625 {
626 for ( int y = 0; y < nodes_y - 1; y++ )
627 {
628 for ( int x = 0; x < nodes_x - 1; x++ )
629 {
630 // Hex nodes
631 IntegerOutputType v[8];
632
633 v[0] = offset + local_subdomain_id * stride_0 + r * stride_1 + y * stride_2 + x;
634 v[1] = v[0] + 1;
635 v[2] = v[0] + nodes_x;
636 v[3] = v[0] + nodes_x + 1;
637
638 v[4] = offset + local_subdomain_id * stride_0 + ( r + 1 ) * stride_1 +
639 y * stride_2 + x;
640 v[5] = v[4] + 1;
641 v[6] = v[4] + nodes_x;
642 v[7] = v[4] + nodes_x + 1;
643
644 IntegerOutputType wedge_0[6] = { v[0], v[1], v[2], v[4], v[5], v[6] };
645 IntegerOutputType wedge_1[6] = { v[3], v[2], v[1], v[7], v[6], v[5] };
646
647 out.write( reinterpret_cast< const char* >( wedge_0 ), sizeof( IntegerOutputType ) * 6 );
648 out.write( reinterpret_cast< const char* >( wedge_1 ), sizeof( IntegerOutputType ) * 6 );
649 }
650 }
651 }
652 }
653 }
654
655 template < typename ScalarTypeIn, typename ScalarTypeOut >
656 void write_scalar_attribute_binary_data(
657 const grid::Grid4DDataScalar< ScalarTypeIn >& device_data,
658 std::stringstream& out )
659 {
660 // Copy data to host.
661 if constexpr ( std::is_same_v< ScalarTypeIn, double > )
662 {
663 if ( !host_data_mirror_scalar_double_.has_value() )
664 {
665 host_data_mirror_scalar_double_ = Kokkos::create_mirror( Kokkos::HostSpace{}, device_data );
666 }
667
668 Kokkos::deep_copy( host_data_mirror_scalar_double_.value(), device_data );
669
670 const auto& host_data = host_data_mirror_scalar_double_.value();
671
672 for ( int local_subdomain_id = 0; local_subdomain_id < host_data.extent( 0 ); local_subdomain_id++ )
673 {
674 for ( int r = 0; r < host_data.extent( 3 ); r++ )
675 {
676 for ( int y = 0; y < host_data.extent( 2 ); y++ )
677 {
678 for ( int x = 0; x < host_data.extent( 1 ); x++ )
679 {
680 const auto value = static_cast< ScalarTypeOut >( host_data( local_subdomain_id, x, y, r ) );
681 out.write( reinterpret_cast< const char* >( &value ), sizeof( ScalarTypeOut ) );
682 }
683 }
684 }
685 }
686 }
687 else if constexpr ( std::is_same_v< ScalarTypeIn, float > )
688 {
689 if ( !host_data_mirror_scalar_float_.has_value() )
690 {
691 host_data_mirror_scalar_float_ = Kokkos::create_mirror( Kokkos::HostSpace{}, device_data );
692 }
693
694 Kokkos::deep_copy( host_data_mirror_scalar_float_.value(), device_data );
695
696 const auto& host_data = host_data_mirror_scalar_float_.value();
697
698 for ( int local_subdomain_id = 0; local_subdomain_id < host_data.extent( 0 ); local_subdomain_id++ )
699 {
700 for ( int r = 0; r < host_data.extent( 3 ); r++ )
701 {
702 for ( int y = 0; y < host_data.extent( 2 ); y++ )
703 {
704 for ( int x = 0; x < host_data.extent( 1 ); x++ )
705 {
706 const auto value = static_cast< ScalarTypeOut >( host_data( local_subdomain_id, x, y, r ) );
707 out.write( reinterpret_cast< const char* >( &value ), sizeof( ScalarTypeOut ) );
708 }
709 }
710 }
711 }
712 }
713 else
714 {
715 Kokkos::abort( "XDMF: Only double precision grids supported for scalar attributes." );
716 }
717 }
718
719 template < typename ScalarTypeIn >
720 util::XML write_scalar_attribute_file(
721 const grid::Grid4DDataScalar< ScalarTypeIn >& data,
722 const OutputTypeFloat& output_type )
723 {
724 const auto attribute_file_base = data.label() + "_" + std::to_string( write_counter_ ) + ".bin";
725 const auto attribute_file_path = directory_path_ + "/" + attribute_file_base;
726
727 {
728 std::stringstream attribute_stream;
729 switch ( output_type )
730 {
732 write_scalar_attribute_binary_data< ScalarTypeIn, float >( data, attribute_stream );
733 break;
735 write_scalar_attribute_binary_data< ScalarTypeIn, double >( data, attribute_stream );
736 break;
737 }
738
739 MPI_File fh;
740 MPI_File_open(
741 MPI_COMM_WORLD, attribute_file_path.c_str(), MPI_MODE_CREATE | MPI_MODE_RDWR, MPI_INFO_NULL, &fh );
742
743 // Define the file view: each process writes its local data sequentially
744 MPI_Offset disp = number_of_nodes_offset_ * static_cast< int64_t >( output_type_points_ );
745 MPI_File_set_view( fh, disp, MPI_CHAR, MPI_CHAR, "native", MPI_INFO_NULL );
746
747 std::string attr_str = attribute_stream.str();
748
749 // Write data collectively
750 MPI_File_write_all(
751 fh, attr_str.data(), static_cast< int >( attr_str.size() ), MPI_CHAR, MPI_STATUS_IGNORE );
752
753 // Close the file
754 MPI_File_close( &fh );
755 }
756
757 auto attribute =
758 util::XML( "Attribute", { { "Name", data.label() }, { "AttributeType", "Scalar" }, { "Center", "Node" } } )
759 .add_child(
760 util::XML(
761 "DataItem",
762 { { "Format", "Binary" },
763 { "DataType", "Float" },
764 { "Precision", std::to_string( static_cast< int >( output_type ) ) },
765 { "Endian", "Little" },
766 { "Dimensions", std::to_string( number_of_nodes_global_ ) } },
767 attribute_file_base ) );
768
769 return attribute;
770 }
771
772 template < typename ScalarTypeIn, typename ScalarTypeOut, int VecDim >
773 void write_vec_attribute_binary_data(
774 const grid::Grid4DDataVec< ScalarTypeIn, VecDim >& device_data,
775 std::stringstream& out )
776 {
777 // Copy data to host.
778 if constexpr ( std::is_same_v< ScalarTypeIn, double > )
779 {
780 if ( !host_data_mirror_vec_double_.has_value() )
781 {
782 host_data_mirror_vec_double_ = grid::create_mirror( Kokkos::HostSpace{}, device_data );
783 }
784
785 grid::deep_copy( host_data_mirror_vec_double_.value(), device_data );
786
787 const auto& host_data = host_data_mirror_vec_double_.value();
788
789 for ( int local_subdomain_id = 0; local_subdomain_id < host_data.extent( 0 ); local_subdomain_id++ )
790 {
791 for ( int r = 0; r < host_data.extent( 3 ); r++ )
792 {
793 for ( int y = 0; y < host_data.extent( 2 ); y++ )
794 {
795 for ( int x = 0; x < host_data.extent( 1 ); x++ )
796 {
797 for ( int d = 0; d < VecDim; d++ )
798 {
799 const auto value =
800 static_cast< ScalarTypeOut >( host_data( local_subdomain_id, x, y, r, d ) );
801 out.write( reinterpret_cast< const char* >( &value ), sizeof( ScalarTypeOut ) );
802 }
803 }
804 }
805 }
806 }
807 }
808 else if constexpr ( std::is_same_v< ScalarTypeIn, float > )
809 {
810 if ( !host_data_mirror_vec_float_.has_value() )
811 {
812 host_data_mirror_vec_float_ = grid::create_mirror( Kokkos::HostSpace{}, device_data );
813 }
814
815 grid::deep_copy( host_data_mirror_vec_float_.value(), device_data );
816
817 const auto& host_data = host_data_mirror_vec_float_.value();
818
819 for ( int local_subdomain_id = 0; local_subdomain_id < host_data.extent( 0 ); local_subdomain_id++ )
820 {
821 for ( int r = 0; r < host_data.extent( 3 ); r++ )
822 {
823 for ( int y = 0; y < host_data.extent( 2 ); y++ )
824 {
825 for ( int x = 0; x < host_data.extent( 1 ); x++ )
826 {
827 for ( int d = 0; d < VecDim; d++ )
828 {
829 const auto value =
830 static_cast< ScalarTypeOut >( host_data( local_subdomain_id, x, y, r, d ) );
831 out.write( reinterpret_cast< const char* >( &value ), sizeof( ScalarTypeOut ) );
832 }
833 }
834 }
835 }
836 }
837 }
838 else
839 {
840 Kokkos::abort( "XDMF: Only double precision grids supported for vector-valued attributes." );
841 }
842 }
843
844 template < typename ScalarTypeIn, int VecDim >
845 util::XML write_vec_attribute_file(
846 const grid::Grid4DDataVec< ScalarTypeIn, VecDim >& data,
847 const OutputTypeFloat& output_type )
848 {
849 const auto attribute_file_base = data.label() + "_" + std::to_string( write_counter_ ) + ".bin";
850 const auto attribute_file_path = directory_path_ + "/" + attribute_file_base;
851
852 {
853 std::stringstream attribute_stream;
854 switch ( output_type )
855 {
857 write_vec_attribute_binary_data< ScalarTypeIn, float >( data, attribute_stream );
858 break;
860 write_vec_attribute_binary_data< ScalarTypeIn, double >( data, attribute_stream );
861 break;
862 }
863
864 MPI_File fh;
865 MPI_File_open(
866 MPI_COMM_WORLD, attribute_file_path.c_str(), MPI_MODE_CREATE | MPI_MODE_RDWR, MPI_INFO_NULL, &fh );
867
868 // Define the file view: each process writes its local data sequentially
869 MPI_Offset disp = static_cast< int64_t >( VecDim ) * number_of_nodes_offset_ * static_cast< int64_t >( output_type_points_ );
870 MPI_File_set_view( fh, disp, MPI_CHAR, MPI_CHAR, "native", MPI_INFO_NULL );
871
872 std::string attr_str = attribute_stream.str();
873
874 // Write data collectively
875 MPI_File_write_all(
876 fh, attr_str.data(), static_cast< int >( attr_str.size() ), MPI_CHAR, MPI_STATUS_IGNORE );
877
878 // Close the file
879 MPI_File_close( &fh );
880 }
881
882 auto attribute =
883 util::XML( "Attribute", { { "Name", data.label() }, { "AttributeType", "Vector" }, { "Center", "Node" } } )
884 .add_child(
885 util::XML(
886 "DataItem",
887 { { "Format", "Binary" },
888 { "DataType", "Float" },
889 { "Precision", std::to_string( static_cast< int >( output_type ) ) },
890 { "Endian", "Little" },
891 { "Dimensions",
892 std::to_string( number_of_nodes_global_ ) + " " + std::to_string( VecDim ) } },
893 attribute_file_base ) );
894
895 return attribute;
896 }
897
898 void write_checkpoint_metadata()
899 {
900 const auto checkpoint_metadata_file_path = directory_path_ + "/" + "checkpoint_metadata.bin";
901
902 std::stringstream checkpoint_metadata_stream;
903
904 auto write_i32 = [&]( const int32_t value ) {
905 checkpoint_metadata_stream.write( reinterpret_cast< const char* >( &value ), sizeof( int32_t ) );
906 };
907
908 auto write_i64 = [&]( const int64_t value ) {
909 checkpoint_metadata_stream.write( reinterpret_cast< const char* >( &value ), sizeof( int64_t ) );
910 };
911
912 auto write_f64 = [&]( const double value ) {
913 checkpoint_metadata_stream.write( reinterpret_cast< const char* >( &value ), sizeof( double ) );
914 };
915
916 write_i32( 1 ); // version
917 write_i32( distributed_domain_.domain_info().num_subdomains_per_diamond_side() );
918 write_i32( distributed_domain_.domain_info().num_subdomains_in_radial_direction() );
919
920 write_i32( coords_shell_device_.extent( 1 ) ); // subdomain_size_x
921 write_i32( coords_shell_device_.extent( 2 ) ); // subdomain_size_y
922 write_i32( coords_radii_device_.extent( 1 ) ); // subdomain_size_r
923
924 for ( const auto r : distributed_domain_.domain_info().radii() )
925 {
926 write_f64( static_cast< double >( r ) );
927 }
928
929 write_i32( static_cast< int32_t >( output_type_points_ ) );
930
931 write_i32(
932 device_data_views_scalar_float_.size() + device_data_views_scalar_double_.size() +
933 device_data_views_vec_float_.size() + device_data_views_vec_double_.size() );
934
935 for ( const auto& [data, output_type] : device_data_views_scalar_float_ )
936 {
937 write_i32( data.label().size() );
938 checkpoint_metadata_stream.write( data.label().data(), data.label().size() );
939 write_i32( 2 ); // scalar_data_type
940 if ( output_type == OutputTypeFloat::Float32 )
941 {
942 write_i32( sizeof( float ) );
943 }
944 else if ( output_type == OutputTypeFloat::Float64 )
945 {
946 write_i32( sizeof( double ) );
947 }
948 else
949 {
950 Kokkos::abort( "Invalid output type." );
951 }
952
953 write_i32( 1 ); // vec_dim
954 }
955
956 for ( const auto& [data, output_type] : device_data_views_scalar_double_ )
957 {
958 write_i32( data.label().size() );
959 checkpoint_metadata_stream.write( data.label().data(), data.label().size() );
960 write_i32( 2 ); // scalar_data_type
961 if ( output_type == OutputTypeFloat::Float32 )
962 {
963 write_i32( sizeof( float ) );
964 }
965 else if ( output_type == OutputTypeFloat::Float64 )
966 {
967 write_i32( sizeof( double ) );
968 }
969 else
970 {
971 Kokkos::abort( "Invalid output type." );
972 }
973 write_i32( 1 ); // vec_dim
974 }
975
976 for ( const auto& [data, output_type] : device_data_views_vec_float_ )
977 {
978 write_i32( data.label().size() );
979 checkpoint_metadata_stream.write( data.label().data(), data.label().size() );
980 write_i32( 2 ); // scalar_data_type
981 if ( output_type == OutputTypeFloat::Float32 )
982 {
983 write_i32( sizeof( float ) );
984 }
985 else if ( output_type == OutputTypeFloat::Float64 )
986 {
987 write_i32( sizeof( double ) );
988 }
989 else
990 {
991 Kokkos::abort( "Invalid output type." );
992 }
993 write_i32( data.extent( 4 ) ); // vec_dim
994 }
995
996 for ( const auto& [data, output_type] : device_data_views_vec_double_ )
997 {
998 write_i32( data.label().size() );
999 checkpoint_metadata_stream.write( data.label().data(), data.label().size() );
1000 write_i32( 2 ); // scalar_data_type
1001 if ( output_type == OutputTypeFloat::Float32 )
1002 {
1003 write_i32( sizeof( float ) );
1004 }
1005 else if ( output_type == OutputTypeFloat::Float64 )
1006 {
1007 write_i32( sizeof( double ) );
1008 }
1009 else
1010 {
1011 Kokkos::abort( "Invalid output type." );
1012 }
1013 write_i32( data.extent( 4 ) ); // vec_dim
1014 }
1015
1016 write_i32( 0 ); // checkpoint_subdomain_ordering
1017
1018 // for ( int local_subdomain_id = 0; local_subdomain_id < coords_shell_device_.extent( 0 ); local_subdomain_id++ )
1019 // {
1020 // write_i64( distributed_domain_.subdomain_info_from_local_idx( local_subdomain_id ).global_id() );
1021 // }
1022
1023 MPI_File fh;
1024 MPI_File_open(
1025 MPI_COMM_WORLD,
1026 checkpoint_metadata_file_path.c_str(),
1027 MPI_MODE_CREATE | MPI_MODE_RDWR,
1028 MPI_INFO_NULL,
1029 &fh );
1030
1031 // Define the file view: each process writes its local data sequentially
1032 MPI_Offset disp = 0;
1033 const auto offset_metadata_size_bytes = static_cast< int >( checkpoint_metadata_stream.str().size() );
1034 if ( mpi::rank() != 0 )
1035 {
1036 // We'll write the global metadata just via rank 0.
1037 // Next up: each process writes the global subdomain IDs of their local subdomains in parallel.
1038 // We have previously also filled the stream with the global metadata on non-root processes to facilitate
1039 // computing its length (we need to know where to start writing).
1040 // After computing its length and before writing the subdomain ids we clear the stream here.
1041 disp = offset_metadata_size_bytes + number_of_subdomains_offset_ * sizeof( int64_t );
1042 checkpoint_metadata_stream.clear();
1043 checkpoint_metadata_stream.str( "" );
1044 }
1045
1046 // Writing the global subdomain ids of the local subdomains now in contiguous chunks per process.
1047 // Must be the order of the local_subdomain_id here to adhere to the ordering of the data output (which also
1048 // writes in that order).
1049 for ( int local_subdomain_id = 0; local_subdomain_id < coords_shell_device_.extent( 0 ); local_subdomain_id++ )
1050 {
1051 write_i64( distributed_domain_.subdomain_info_from_local_idx( local_subdomain_id ).global_id() );
1052 }
1053
1054 MPI_File_set_view( fh, disp, MPI_CHAR, MPI_CHAR, "native", MPI_INFO_NULL );
1055
1056 std::string checkpoint_metadata_str = checkpoint_metadata_stream.str();
1057
1058 // Write data collectively
1059 MPI_File_write_all(
1060 fh,
1061 checkpoint_metadata_str.data(),
1062 static_cast< int >( checkpoint_metadata_str.size() ),
1063 MPI_CHAR,
1064 MPI_STATUS_IGNORE );
1065
1066 // Close the file
1067 MPI_File_close( &fh );
1068 }
1069
1070 std::string directory_path_;
1071
1072 grid::shell::DistributedDomain distributed_domain_;
1073
1074 grid::Grid3DDataVec< InputGridScalarType, 3 > coords_shell_device_;
1075 grid::Grid2DDataScalar< InputGridScalarType > coords_radii_device_;
1076
1077 OutputTypeFloat output_type_points_;
1078 OutputTypeInt output_type_connectivity_;
1079
1080 // Collecting all views that are written on every write call.
1081 std::vector< std::pair< grid::Grid4DDataScalar< double >, OutputTypeFloat > > device_data_views_scalar_double_;
1082 std::vector< std::pair< grid::Grid4DDataScalar< float >, OutputTypeFloat > > device_data_views_scalar_float_;
1083
1084 std::vector< std::pair< grid::Grid4DDataVec< double, 3 >, OutputTypeFloat > > device_data_views_vec_double_;
1085 std::vector< std::pair< grid::Grid4DDataVec< float, 3 >, OutputTypeFloat > > device_data_views_vec_float_;
1086
1087 // Just a single mirror for buffering during write.
1088 std::optional< grid::Grid4DDataScalar< double >::HostMirror > host_data_mirror_scalar_double_;
1089 std::optional< grid::Grid4DDataScalar< float >::HostMirror > host_data_mirror_scalar_float_;
1090
1091 std::optional< grid::Grid4DDataVec< double, 3 >::HostMirror > host_data_mirror_vec_double_;
1092 std::optional< grid::Grid4DDataVec< float, 3 >::HostMirror > host_data_mirror_vec_float_;
1093
1094 int write_counter_ = 0;
1095 bool first_write_happened_ = false;
1096
1097 int64_t number_of_nodes_offset_ = -1;
1098 int64_t number_of_elements_offset_ = -1;
1099 int64_t number_of_subdomains_offset_ = -1;
1100
1101 int64_t number_of_nodes_global_ = -1;
1102 int64_t number_of_elements_global_ = -1;
1103 int64_t number_of_subdomains_global_ = -1;
1104};
1105
1106/// Captures the format of the checkpoint metadata.
1107/// See \ref XDMFOutput for details on the format.
1109{
1110 int32_t version{};
1113 int32_t size_x{};
1114 int32_t size_y{};
1115 int32_t size_r{};
1116
1117 std::vector< double > radii;
1118
1120
1122 {
1123 std::string grid_name_string;
1125 int32_t scalar_bytes{};
1126 int32_t vec_dim{};
1127 };
1128
1129 std::vector< GridDataFile > grid_data_files;
1130
1132
1134};
1135
1136/// @brief Reads metadata from an XDMF/checkpoint directory. See \ref XDMFOutput for details.
1137///
1138/// @param checkpoint_directory path to the directory containing the XDMF data
1139/// @return Populated \ref CheckpointMetadata struct.
1140[[nodiscard]] inline util::Result< CheckpointMetadata >
1141 read_xdmf_checkpoint_metadata( const std::string& checkpoint_directory )
1142{
1143 const auto metadata_file = checkpoint_directory + "/" + "checkpoint_metadata.bin";
1144
1145 if ( !std::filesystem::exists( metadata_file ) )
1146 {
1147 return { "Could not find file: " + metadata_file };
1148 }
1149
1150 MPI_File fh;
1151 MPI_File_open( MPI_COMM_WORLD, metadata_file.c_str(), MPI_MODE_RDONLY, MPI_INFO_NULL, &fh );
1152 MPI_Offset filesize;
1153 MPI_File_get_size( fh, &filesize );
1154
1155 std::vector< char > buffer( filesize );
1156 MPI_File_read_all( fh, buffer.data(), filesize, MPI_BYTE, MPI_STATUS_IGNORE );
1157 MPI_File_close( &fh );
1158
1159 std::string data( buffer.data(), buffer.size() );
1160
1161 std::istringstream in( data, std::ios::binary );
1162
1163 auto read_i32 = [&]( int32_t& value ) {
1164 in.read( reinterpret_cast< char* >( &value ), sizeof( value ) );
1165 if ( !in )
1166 {
1167 return 1;
1168 }
1169 return 0;
1170 };
1171
1172 auto read_i64 = [&]( int64_t& value ) {
1173 in.read( reinterpret_cast< char* >( &value ), sizeof( value ) );
1174 if ( !in )
1175 {
1176 return 1;
1177 }
1178 return 0;
1179 };
1180
1181 auto read_f64 = [&]( double& value ) {
1182 in.read( reinterpret_cast< char* >( &value ), sizeof( value ) );
1183 if ( !in )
1184 {
1185 return 1;
1186 }
1187 return 0;
1188 };
1189
1190 const std::string read_error = "Failed to read from input stream.";
1191
1192 CheckpointMetadata metadata;
1193
1194 if ( read_i32( metadata.version ) )
1195 return read_error;
1196 if ( read_i32( metadata.num_subdomains_per_diamond_lateral_direction ) )
1197 return read_error;
1198 if ( read_i32( metadata.num_subdomains_per_diamond_radial_direction ) )
1199 return read_error;
1200 if ( read_i32( metadata.size_x ) )
1201 return read_error;
1202 if ( read_i32( metadata.size_y ) )
1203 return read_error;
1204 if ( read_i32( metadata.size_r ) )
1205 return read_error;
1206
1207 for ( int i = 0; i < metadata.num_subdomains_per_diamond_radial_direction * ( metadata.size_r - 1 ) + 1; i++ )
1208 {
1209 double r;
1210 if ( read_f64( r ) )
1211 return read_error;
1212
1213 if ( i > 0 && r <= metadata.radii.back() )
1214 {
1215 return { "Radii are not sorted correctly in checkpoint." };
1216 }
1217
1218 metadata.radii.push_back( r );
1219 }
1220
1221 if ( metadata.version > 0 )
1222 {
1223 // new in version 1: number of bytes for grid points data
1224 if ( read_i32( metadata.grid_scalar_bytes ) )
1225 return read_error;
1226 }
1227
1228 int32_t num_grid_data_files;
1229 if ( read_i32( num_grid_data_files ) )
1230 return read_error;
1231
1232 metadata.grid_data_files.resize( num_grid_data_files );
1233 for ( auto& [grid_name_string, scalar_data_type, scalar_bytes, vec_dim] : metadata.grid_data_files )
1234 {
1235 int32_t grid_name_string_size_bytes;
1236 if ( read_i32( grid_name_string_size_bytes ) )
1237 return read_error;
1238
1239 grid_name_string = std::string( grid_name_string_size_bytes, '\0' );
1240 in.read( &grid_name_string[0], grid_name_string_size_bytes );
1241 if ( !in )
1242 return read_error;
1243
1244 if ( read_i32( scalar_data_type ) )
1245 return read_error;
1246 if ( read_i32( scalar_bytes ) )
1247 return read_error;
1248 if ( read_i32( vec_dim ) )
1249 return read_error;
1250 }
1251
1252 if ( read_i32( metadata.checkpoint_subdomain_ordering ) )
1253 return read_error;
1254
1255 if ( metadata.checkpoint_subdomain_ordering == 0 )
1256 {
1257 const auto num_global_subdomains = 10 * metadata.num_subdomains_per_diamond_lateral_direction *
1260
1261 metadata.checkpoint_ordering_0_global_subdomain_ids.resize( num_global_subdomains );
1262 for ( auto& global_subdomain_id : metadata.checkpoint_ordering_0_global_subdomain_ids )
1263 {
1264 if ( read_i64( global_subdomain_id ) )
1265 return read_error + " (global_subdomain_id reading error)";
1266 }
1267 }
1268
1269 return metadata;
1270}
1271
1272/// @brief Reads a single grid at a single write step from an XDMF checkpoint.
1273///
1274/// See \ref XDMFOutput for details.
1275///
1276/// @param checkpoint_directory path to the directory containing the XDMF data
1277/// @param data_label the Kokkos::View label of the grid data that shall be read in
1278/// @param step the "timestep" to read
1279/// @param distributed_domain \ref DistributedDomain instance that has the same topology as the one used when writing
1280/// the checkpoint
1281/// @param grid_data_device [out] properly sized Kokkos::View (can live on a device) to write the checkpoint to
1282template < typename GridDataType >
1284 const std::string& checkpoint_directory,
1285 const std::string& data_label,
1286 const int step,
1287 const grid::shell::DistributedDomain& distributed_domain,
1288 GridDataType& grid_data_device )
1289{
1290 auto checkpoint_metadata_result = read_xdmf_checkpoint_metadata( checkpoint_directory );
1291
1292 if ( checkpoint_metadata_result.is_err() )
1293 {
1294 return "Could not read checkpoint metadata: " + checkpoint_metadata_result.error();
1295 }
1296
1297 const auto& checkpoint_metadata = checkpoint_metadata_result.unwrap();
1298
1299 if ( !( checkpoint_metadata.version == 0 || checkpoint_metadata.version == 1 ) )
1300 {
1301 return {
1302 "Supported checkpoint verions: 0, 1. This checkpoint has version " +
1303 std::to_string( checkpoint_metadata.version ) + "." };
1304 }
1305
1306 // Check whether we have checkpoint metadata for the requested data label.
1307
1308 std::optional< CheckpointMetadata::GridDataFile > requested_grid_data_file;
1309 for ( const auto& grid_data_file : checkpoint_metadata.grid_data_files )
1310 {
1311 if ( grid_data_file.grid_name_string == data_label )
1312 {
1313 requested_grid_data_file = grid_data_file;
1314 break;
1315 }
1316 }
1317
1318 if ( !requested_grid_data_file.has_value() )
1319 {
1320 return { "Could not find requested data (" + data_label + ") in checkpoint" };
1321 }
1322
1323 // Check if we can also find a binary data file in the checkpoint directory.
1324
1325 const auto data_file_path = checkpoint_directory + "/" + data_label + "_" + std::to_string( step ) + ".bin";
1326
1327 if ( !std::filesystem::exists( data_file_path ) )
1328 {
1329 return { "Could not find checkpoint data file for requested label and step." };
1330 }
1331
1332 // Now we compare the grid extents with the metadata.
1333
1334 if ( grid_data_device.extent( 1 ) != checkpoint_metadata.size_x ||
1335 grid_data_device.extent( 2 ) != checkpoint_metadata.size_y ||
1336 grid_data_device.extent( 3 ) != checkpoint_metadata.size_r )
1337 {
1338 return { "Grid data extents do not match metadata." };
1339 }
1340
1341 // Let's try to read now.
1342 // We will attempt to read all chunks for the local subdomains (that are possibly distributed in the file)
1343 // into one array. In a second step we'll copy that into our grid data. That is not 100% memory efficient as we
1344 // have another copy (the buffer). We could write directly with the std library, but I am not sure if that is
1345 // scalable.
1346
1347 if ( checkpoint_metadata.checkpoint_subdomain_ordering != 0 )
1348 {
1349 return { "Checkpoint ordering type is not 0. Not supported." };
1350 }
1351
1352 // Figure out where the subdomain data is positioned in the file.
1353
1354 const auto num_local_subdomains = grid_data_device.extent( 0 );
1355
1356 const auto local_subdomain_num_scalars_v = checkpoint_metadata.size_x * checkpoint_metadata.size_y *
1357 checkpoint_metadata.size_r * requested_grid_data_file.value().vec_dim;
1358
1359 const auto local_subdomain_data_size_in_bytes =
1360 local_subdomain_num_scalars_v * requested_grid_data_file.value().scalar_bytes;
1361
1362 std::vector< MPI_Aint > local_subdomain_offsets_in_file_bytes( num_local_subdomains );
1363 std::vector< int > local_subdomain_num_bytes( num_local_subdomains, local_subdomain_data_size_in_bytes );
1364
1365 for ( int local_subdomain_id = 0; local_subdomain_id < num_local_subdomains; local_subdomain_id++ )
1366 {
1367 const auto global_subdomain_id =
1368 distributed_domain.subdomain_info_from_local_idx( local_subdomain_id ).global_id();
1369
1370 const auto index_in_file =
1371 std::ranges::find( checkpoint_metadata.checkpoint_ordering_0_global_subdomain_ids, global_subdomain_id ) -
1372 checkpoint_metadata.checkpoint_ordering_0_global_subdomain_ids.begin();
1373
1374 local_subdomain_offsets_in_file_bytes[local_subdomain_id] = index_in_file * local_subdomain_data_size_in_bytes;
1375 }
1376
1377 // Read with MPI
1378
1379 std::vector< char > buffer( num_local_subdomains * local_subdomain_data_size_in_bytes );
1380
1381 MPI_File fh;
1382 MPI_File_open( MPI_COMM_WORLD, data_file_path.c_str(), MPI_MODE_RDONLY, MPI_INFO_NULL, &fh );
1383
1384 MPI_Datatype filetype;
1385 MPI_Type_create_hindexed(
1386 num_local_subdomains,
1387 local_subdomain_num_bytes.data(),
1388 local_subdomain_offsets_in_file_bytes.data(),
1389 MPI_CHAR,
1390 &filetype );
1391 MPI_Type_commit( &filetype );
1392
1393 // Set each rank’s view of the file to its own scattered layout
1394 MPI_File_set_view( fh, 0, MPI_CHAR, filetype, "native", MPI_INFO_NULL );
1395
1396 MPI_File_read_all( fh, buffer.data(), buffer.size(), MPI_CHAR, MPI_STATUS_IGNORE );
1397
1398 MPI_Type_free( &filetype );
1399 MPI_File_close( &fh );
1400
1401 // Now write from buffer to grid.
1402
1403 typename GridDataType::HostMirror grid_data_host = grid::create_mirror( Kokkos::HostSpace{}, grid_data_device );
1404
1405 const auto checkpoint_is_float =
1406 requested_grid_data_file.value().scalar_data_type == 2 && requested_grid_data_file.value().scalar_bytes == 4;
1407 const auto checkpoint_is_double =
1408 requested_grid_data_file.value().scalar_data_type == 2 && requested_grid_data_file.value().scalar_bytes == 8;
1409
1410 if ( !( checkpoint_is_float || checkpoint_is_double ) )
1411 {
1412 return { "Unsupported data type in checkpoint." };
1413 }
1414
1415 // Wrap vector in a stream
1416 std::string_view view( reinterpret_cast< const char* >( buffer.data() ), buffer.size() );
1417 std::istringstream stream{ std::string( view ) }; // make a copy to own the buffer
1418
1419 auto read_f32 = [&]() -> float {
1420 float value;
1421 stream.read( reinterpret_cast< char* >( &value ), sizeof( float ) );
1422 if ( stream.fail() )
1423 {
1424 Kokkos::abort( "Failed to read from stream." );
1425 }
1426 return value;
1427 };
1428
1429 auto read_f64 = [&]() -> double {
1430 double value;
1431 stream.read( reinterpret_cast< char* >( &value ), sizeof( double ) );
1432 if ( stream.fail() )
1433 {
1434 Kokkos::abort( "Failed to read from stream." );
1435 }
1436 return value;
1437 };
1438
1439 for ( int local_subdomain_id = 0; local_subdomain_id < grid_data_host.extent( 0 ); local_subdomain_id++ )
1440 {
1441 for ( int r = 0; r < grid_data_host.extent( 3 ); r++ )
1442 {
1443 for ( int y = 0; y < grid_data_host.extent( 2 ); y++ )
1444 {
1445 for ( int x = 0; x < grid_data_host.extent( 1 ); x++ )
1446 {
1447 if constexpr (
1448 std::is_same_v< GridDataType, grid::Grid4DDataScalar< float > > ||
1449 std::is_same_v< GridDataType, grid::Grid4DDataScalar< double > > )
1450 {
1451 if ( requested_grid_data_file.value().vec_dim != 1 )
1452 {
1453 return { "Checkpoint is scalar, passed grid data view dims do not match." };
1454 }
1455
1456 // scalar data
1457 if ( checkpoint_is_float )
1458 {
1459 grid_data_host( local_subdomain_id, x, y, r ) =
1460 static_cast< GridDataType::value_type >( read_f32() );
1461 }
1462 else if ( checkpoint_is_double )
1463 {
1464 grid_data_host( local_subdomain_id, x, y, r ) =
1465 static_cast< GridDataType::value_type >( read_f64() );
1466 }
1467 }
1468 else if constexpr (
1469 std::is_same_v< GridDataType, grid::Grid4DDataVec< float, 3 > > ||
1470 std::is_same_v< GridDataType, grid::Grid4DDataVec< double, 3 > > )
1471 {
1472 if ( requested_grid_data_file.value().vec_dim != 3 )
1473 {
1474 return { "Checkpoint is vector-valued, passed grid data view dims do not match." };
1475 }
1476
1477 // vector-valued data
1478 for ( int d = 0; d < grid_data_host.extent( 4 ); d++ )
1479 {
1480 if ( checkpoint_is_float )
1481 {
1482 grid_data_host( local_subdomain_id, x, y, r, d ) =
1483 static_cast< GridDataType::value_type >( read_f32() );
1484 }
1485 else if ( checkpoint_is_double )
1486 {
1487 grid_data_host( local_subdomain_id, x, y, r, d ) =
1488 static_cast< GridDataType::value_type >( read_f64() );
1489 }
1490 }
1491 }
1492 }
1493 }
1494 }
1495 }
1496
1497 grid::deep_copy( grid_data_device, grid_data_host );
1498 Kokkos::fence();
1499
1500 return { util::Ok{} };
1501}
1502
1503} // namespace terra::io
Parallel data structure organizing the thick spherical shell metadata for distributed (MPI parallel) ...
Definition spherical_shell.hpp:2518
const SubdomainInfo & subdomain_info_from_local_idx(const LocalSubdomainIdx subdomain_idx) const
Definition spherical_shell.hpp:2655
const DomainInfo & domain_info() const
Returns a const reference.
Definition spherical_shell.hpp:2647
int num_subdomains_per_diamond_side() const
Definition spherical_shell.hpp:847
int num_subdomains_in_radial_direction() const
Definition spherical_shell.hpp:849
int64_t global_id() const
Scrambles the four indices (diamond ID, x, y, r) into a single integer.
Definition spherical_shell.hpp:663
XDMF output that simultaneously serves for visualization with software like Paraview and as a simulat...
Definition xdmf.hpp:149
void set_write_counter(int write_counter)
Set the write counter manually.
Definition xdmf.hpp:207
OutputTypeFloat
Used to specify the output type when writing floating point data.
Definition xdmf.hpp:155
OutputTypeInt
Used to specify the output type when writing (signed) integer data.
Definition xdmf.hpp:164
XDMFOutput(const std::string &directory_path, const grid::shell::DistributedDomain &distributed_domain, const grid::Grid3DDataVec< InputGridScalarType, 3 > &coords_shell_device, const grid::Grid2DDataScalar< InputGridScalarType > &coords_radii_device, const OutputTypeFloat output_type_points=OutputTypeFloat::Float32, const OutputTypeInt output_type_connectivity=OutputTypeInt::Int64)
Constructs an XDMFOutput object.
Definition xdmf.hpp:183
void add(const grid::Grid4DDataScalar< InputScalarDataType > &data, const OutputTypeFloat output_type=OutputTypeFloat::Float32)
Adds a new scalar data grid to be written out.
Definition xdmf.hpp:214
void add(const grid::Grid4DDataVec< InputScalarDataType, VecDim > &data, const OutputTypeFloat output_type=OutputTypeFloat::Float32)
Adds a new vector-valued data grid to be written out.
Definition xdmf.hpp:250
void write()
Writes a "time step".
Definition xdmf.hpp:293
Definition result.hpp:13
Definition xml.hpp:14
int r
Definition EpsilonDivDiv_kernel_gen.py:345
value
Definition EpsilonDivDiv_kernel_gen.py:285
local_subdomain_id
Definition EpsilonDivDiv_kernel_gen.py:23
dense::Vec< typename CoordsShellType::value_type, 3 > coords(const int subdomain, const int x, const int y, const int r, const CoordsShellType &coords_shell, const CoordsRadiiType &coords_radii)
Definition spherical_shell.hpp:2871
Grid4DDataVec< ScalarType, VecDim >::HostMirror create_mirror(Kokkos::HostSpace space, const Grid4DDataVec< ScalarType, VecDim > &src)
Create a host mirror for Grid4DDataVec.
Definition grid_types.hpp:118
Kokkos::View< ScalarType ***[VecDim], Layout > Grid3DDataVec
Definition grid_types.hpp:42
Kokkos::View< ScalarType ****, Layout > Grid4DDataScalar
Definition grid_types.hpp:27
void deep_copy(typename Grid4DDataVec< ScalarType, VecDim >::HostMirror &dst, const Grid4DDataVec< ScalarType, VecDim > &src)
Deep copy from device Grid4DDataVec to host mirror.
Definition grid_types.hpp:136
Kokkos::View< ScalarType **, Layout > Grid2DDataScalar
Definition grid_types.hpp:21
Definition lookup_table_2d_reader.hpp:12
util::Result read_xdmf_checkpoint_grid(const std::string &checkpoint_directory, const std::string &data_label, const int step, const grid::shell::DistributedDomain &distributed_domain, GridDataType &grid_data_device)
Reads a single grid at a single write step from an XDMF checkpoint.
Definition xdmf.hpp:1283
util::Result< CheckpointMetadata > read_xdmf_checkpoint_metadata(const std::string &checkpoint_directory)
Reads metadata from an XDMF/checkpoint directory. See XDMFOutput for details.
Definition xdmf.hpp:1141
MPIRank rank()
Definition mpi.hpp:13
bool prepare_empty_directory(const std::string &path_str, bool root_only=true)
Prepares an empty directory with the passed path.
Definition filesystem.hpp:16
SoA (Structure-of-Arrays) 4D vector grid data.
Definition grid_types.hpp:51
std::string label() const
Get the label (derived from first component by stripping "_d0" suffix).
Definition grid_types.hpp:83
int32_t scalar_bytes
Definition xdmf.hpp:1125
int32_t vec_dim
Definition xdmf.hpp:1126
int32_t scalar_data_type
Definition xdmf.hpp:1124
std::string grid_name_string
Definition xdmf.hpp:1123
Definition xdmf.hpp:1109
int32_t size_y
Definition xdmf.hpp:1114
int32_t num_subdomains_per_diamond_radial_direction
Definition xdmf.hpp:1112
int32_t version
Definition xdmf.hpp:1110
int32_t num_subdomains_per_diamond_lateral_direction
Definition xdmf.hpp:1111
int32_t size_x
Definition xdmf.hpp:1113
int32_t size_r
Definition xdmf.hpp:1115
int32_t checkpoint_subdomain_ordering
Definition xdmf.hpp:1131
int32_t grid_scalar_bytes
Definition xdmf.hpp:1119
std::vector< double > radii
Definition xdmf.hpp:1117
std::vector< int64_t > checkpoint_ordering_0_global_subdomain_ids
Definition xdmf.hpp:1133
std::vector< GridDataFile > grid_data_files
Definition xdmf.hpp:1129
Definition result.hpp:9