LCOV - code coverage report
Current view: top level - src/tensor_outputs - XDMFTensorOutput.C (source / functions) Hit Total Coverage
Test: idaholab/swift: #92 (25e020) with base b3cd84 Lines: 162 231 70.1 %
Date: 2025-09-10 17:10:32 Functions: 8 9 88.9 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /**********************************************************************/
       2             : /*                    DO NOT MODIFY THIS HEADER                       */
       3             : /*             Swift, a Fourier spectral solver for MOOSE             */
       4             : /*                                                                    */
       5             : /*            Copyright 2024 Battelle Energy Alliance, LLC            */
       6             : /*                        ALL RIGHTS RESERVED                         */
       7             : /**********************************************************************/
       8             : 
       9             : #include "XDMFTensorOutput.h"
      10             : #include "TensorProblem.h"
      11             : #include "Conversion.h"
      12             : 
      13             : #include <ATen/core/TensorBody.h>
      14             : #include <cstddef>
      15             : #include <cstdint>
      16             : #include <filesystem>
      17             : 
      18             : #ifdef LIBMESH_HAVE_HDF5
      19             : namespace
      20             : {
      21             : void addDataToHDF5(hid_t file_id,
      22             :                    const std::string & dataset_name,
      23             :                    const char * data,
      24             :                    std::vector<std::size_t> & ndim,
      25             :                    hid_t type);
      26             : }
      27             : #endif
      28             : 
      29             : registerMooseObject("SwiftApp", XDMFTensorOutput);
      30             : 
      31             : InputParameters
      32          22 : XDMFTensorOutput::validParams()
      33             : {
      34          22 :   auto params = TensorOutput::validParams();
      35          22 :   params.addClassDescription("Output a tensor in XDMF format.");
      36             : #ifdef LIBMESH_HAVE_HDF5
      37          44 :   params.addParam<bool>("enable_hdf5", false, "Use HDF5 for binary data storage.");
      38             : #endif
      39          22 :   MultiMooseEnum outputMode("CELL NODE OVERSIZED_NODAL");
      40          44 :   outputMode.addDocumentation("CELL", "Output as discontinuous elemental fields.");
      41          44 :   outputMode.addDocumentation(
      42             :       "NODE",
      43             :       "Output as nodal fields, increasing each box dimension by one and duplicating values from "
      44             :       "opposing surfaces to create a periodic continuation.");
      45          44 :   outputMode.addDocumentation("OVERSIZED_NODAL",
      46             :                               "Output oversized tensors as nodal fields without forcing "
      47             :                               "periodicity (suitable for displacement variables).");
      48             : 
      49          44 :   params.addParam<MultiMooseEnum>("output_mode", outputMode, "Output as cell or node data");
      50          44 :   params.addParam<bool>("transpose",
      51          44 :                         true,
      52             :                         "The Paraview XDMF reader swaps x-y (x-z in 3d), so we transpose the "
      53             :                         "tensors before we output to make the data look right in Paraview.");
      54          22 :   return params;
      55          22 : }
      56             : 
      57          12 : XDMFTensorOutput::XDMFTensorOutput(const InputParameters & parameters)
      58             :   : TensorOutput(parameters),
      59          12 :     _dim(_domain.getDim()),
      60          12 :     _frame(0),
      61          24 :     _transpose(getParam<bool>("transpose"))
      62             : #ifdef LIBMESH_HAVE_HDF5
      63             :     ,
      64          24 :     _enable_hdf5(getParam<bool>("enable_hdf5")),
      65          24 :     _hdf5_name(_file_base + ".h5")
      66             : #endif
      67             : {
      68          36 :   const auto output_mode = getParam<MultiMooseEnum>("output_mode").getSetValueIDs<OutputMode>();
      69             :   const auto nbuffers = _out_buffers.size();
      70             : 
      71          12 :   if (output_mode.size() == 0)
      72             :     // default all to Cell
      73          20 :     for (const auto & pair : _out_buffers)
      74          16 :       _output_mode[pair.first] = OutputMode::CELL;
      75           8 :   else if (output_mode.size() != nbuffers)
      76           0 :     paramError(
      77             :         "output_mode", "Specify one output mode per buffer.", output_mode.size(), " != ", nbuffers);
      78             :   else
      79             :   {
      80           8 :     const auto & buffer_name = getParam<std::vector<TensorInputBufferName>>("buffer");
      81          22 :     for (const auto i : make_range(nbuffers))
      82          14 :       _output_mode[buffer_name[i]] = output_mode[i];
      83             :   }
      84             : 
      85             : #ifdef LIBMESH_HAVE_HDF5
      86             :   // Check if the library is thread-safe
      87             :   hbool_t is_threadsafe;
      88          12 :   H5is_library_threadsafe(&is_threadsafe);
      89          12 :   if (!is_threadsafe)
      90             :   {
      91          12 :     for (const auto & output : _tensor_problem.getOutputs())
      92           2 :       if (output.get() != this && dynamic_cast<XDMFTensorOutput *>(output.get()))
      93           2 :         mooseError(
      94             :             "Using an hdf5 library that is not threadsafe and multiple XDMF output objects. "
      95             :             "Consolidate the XDMF outputs or build Swift with a thread safe build of libhdf5.");
      96          10 :     mooseWarning("Using an hdf5 library that is not threadsafe.");
      97             :   }
      98             : #endif
      99          10 : }
     100             : 
     101          16 : XDMFTensorOutput::~XDMFTensorOutput()
     102             : {
     103             : #ifdef LIBMESH_HAVE_HDF5
     104           8 :   if (_enable_hdf5)
     105           8 :     H5Fclose(_hdf5_file_id);
     106             : #endif
     107          16 : }
     108             : 
     109             : void
     110           8 : XDMFTensorOutput::init()
     111             : {
     112             :   // get mesh metadata
     113           8 :   auto sdim = Moose::stringify(_dim);
     114             :   std::vector<Real> origin;
     115             :   std::vector<Real> dgrid;
     116          24 :   for (const auto i : make_range(_dim))
     117             :   {
     118             :     // we need to transpose the tensor because of
     119             :     // https://discourse.paraview.org/t/axis-swapped-with-xdmf-topologytype-3dcorectmesh/3059/4
     120          16 :     const auto j = _transpose ? _dim - i - 1 : i;
     121          16 :     _ndata[0].push_back(_domain.getGridSize()[j]);
     122          16 :     _ndata[1].push_back(_domain.getGridSize()[j] + 1);
     123          16 :     _nnode.push_back(_domain.getGridSize()[j] + 1);
     124          16 :     dgrid.push_back(_domain.getGridSpacing()(j));
     125          16 :     origin.push_back(_domain.getDomainMin()(j));
     126             :   }
     127          16 :   _data_grid[0] = Moose::stringify(_ndata[0], " ");
     128          16 :   _data_grid[1] = Moose::stringify(_ndata[1], " ");
     129          16 :   _node_grid = Moose::stringify(_nnode, " ");
     130             : 
     131             :   //
     132             :   // setup XDMF skeleton
     133             :   //
     134             : 
     135             :   // Top level xdmf block
     136           8 :   auto xdmf = _doc.append_child("Xdmf");
     137           8 :   xdmf.append_attribute("xmlns:xi") = "http://www.w3.org/2003/XInclude";
     138           8 :   xdmf.append_attribute("Version") = "2.2";
     139             : 
     140             :   // Domain
     141           8 :   auto domain = xdmf.append_child("Domain");
     142             : 
     143             :   // - Topology
     144           8 :   auto topology = domain.append_child("Topology");
     145           8 :   topology.append_attribute("TopologyType") = (sdim + "DCoRectMesh").c_str();
     146           8 :   topology.append_attribute("Dimensions").set_value(_node_grid.c_str());
     147             : 
     148             :   // -  Geometry
     149           8 :   auto geometry = domain.append_child("Geometry");
     150           8 :   std::string type = "ORIGIN_";
     151           8 :   const char * dxyz[] = {"DX", "DY", "DZ"};
     152          24 :   for (const auto i : make_range(_dim))
     153          16 :     type += dxyz[i];
     154           8 :   geometry.append_attribute("Type") = type.c_str();
     155             : 
     156             :   // -- Origin
     157             :   {
     158           8 :     auto data = geometry.append_child("DataItem");
     159           8 :     data.append_attribute("Format").set_value("XML");
     160           8 :     data.append_attribute("Dimensions") = sdim.c_str();
     161          16 :     data.append_child(pugi::node_pcdata).set_value(Moose::stringify(origin, " ").c_str());
     162             :   }
     163             : 
     164             :   // -- Grid spacing
     165             :   {
     166           8 :     auto data = geometry.append_child("DataItem");
     167           8 :     data.append_attribute("Format") = "XML";
     168           8 :     data.append_attribute("Dimensions") = sdim.c_str();
     169          16 :     data.append_child(pugi::node_pcdata).set_value(Moose::stringify(dgrid, " ").c_str());
     170             :   }
     171             : 
     172             :   // - TimeSeries Grid
     173           8 :   _tgrid = domain.append_child("Grid");
     174           8 :   _tgrid.append_attribute("Name") = "TimeSeries";
     175           8 :   _tgrid.append_attribute("GridType") = "Collection";
     176           8 :   _tgrid.append_attribute("CollectionType") = "Temporal";
     177             : 
     178             :   // write XDMF file
     179           8 :   _doc.save_file((_file_base + ".xmf").c_str());
     180             : #ifdef LIBMESH_HAVE_HDF5
     181             :   // delete HDF5 file
     182           8 :   if (_enable_hdf5)
     183             :   {
     184           8 :     std::filesystem::remove(_hdf5_name);
     185             :     // open new file
     186           8 :     _hdf5_file_id = H5Fcreate(_hdf5_name.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
     187           8 :     if (_hdf5_file_id < 0)
     188             :     {
     189           0 :       H5Eprint(H5E_DEFAULT, stderr);
     190           0 :       mooseError("Error opening HDF5 file '", _hdf5_name, "'.");
     191             :     }
     192             :   }
     193             : #endif
     194          16 : }
     195             : 
     196             : void
     197          88 : XDMFTensorOutput::output()
     198             : {
     199             :   // add grid for new timestep
     200          88 :   auto grid = _tgrid.append_child("Grid");
     201          88 :   grid.append_attribute("Name") = ("T" + Moose::stringify(_frame)).c_str();
     202          88 :   grid.append_attribute("GridType") = "Uniform";
     203             : 
     204             :   // time
     205          88 :   auto time = grid.append_child("Time");
     206          88 :   time.append_attribute("Value") = _time;
     207             : 
     208             :   // add references
     209          88 :   grid.append_child("xi:include").append_attribute("xpointer") = "xpointer(//Xdmf/Domain/Topology)";
     210          88 :   grid.append_child("xi:include").append_attribute("xpointer") = "xpointer(//Xdmf/Domain/Geometry)";
     211             : 
     212             :   // loop over buffers
     213         352 :   for (const auto & [buffer_name, original_buffer] : _out_buffers)
     214             :   {
     215             :     // skip empty tensors (no device)
     216         264 :     if (!original_buffer->defined())
     217           0 :       continue;
     218             : 
     219         264 :     const auto output_mode = _output_mode[buffer_name];
     220             :     torch::Tensor buffer;
     221             : 
     222             :     // we need to transpose the tensor because of
     223             :     // https://discourse.paraview.org/t/axis-swapped-with-xdmf-topologytype-3dcorectmesh/3059/4
     224         264 :     switch (output_mode)
     225             :     {
     226          44 :       case OutputMode::NODE:
     227          44 :         if (_transpose)
     228             :         {
     229           0 :           if (_dim == 2)
     230             :           {
     231           0 :             buffer = buffer = torch::transpose(extendTensor(*original_buffer), 0, 1).contiguous();
     232           0 :             break;
     233             :           }
     234           0 :           else if (_dim == 3)
     235             :           {
     236           0 :             buffer = buffer = torch::transpose(extendTensor(*original_buffer), 0, 2).contiguous();
     237           0 :             break;
     238             :           }
     239             :         }
     240          44 :         buffer = extendTensor(*original_buffer);
     241          44 :         break;
     242             : 
     243         220 :       case OutputMode::CELL:
     244             :       case OutputMode::OVERSIZED_NODAL:
     245         220 :         if (_transpose)
     246             :         {
     247           0 :           if (_dim == 2)
     248             :           {
     249           0 :             buffer = torch::transpose(*original_buffer, 0, 1).contiguous();
     250           0 :             break;
     251             :           }
     252           0 :           else if (_dim == 3)
     253             :           {
     254           0 :             buffer = torch::transpose(*original_buffer, 0, 2).contiguous();
     255             :           }
     256             :           break;
     257             :         }
     258             : 
     259         220 :         buffer = *original_buffer;
     260             :         break;
     261             :     }
     262             : 
     263             :     const auto is_cell = output_mode == OutputMode::CELL;
     264             : 
     265             :     const auto sizes = buffer.sizes();
     266             :     const auto total_dims = sizes.size();
     267             : 
     268         264 :     if (total_dims < _dim)
     269           0 :       mooseError("Tensor has fewer dimensions than specified spatial dimension.");
     270             : 
     271             :     int64_t num_grid_fields = 1;
     272         792 :     for (const auto i : make_range(_dim))
     273         528 :       num_grid_fields *= sizes[i];
     274             : 
     275             :     int64_t num_scalar_fields = 1;
     276         264 :     for (std::size_t i = _dim; i < total_dims; ++i)
     277           0 :       num_scalar_fields *= sizes[i];
     278             : 
     279         528 :     std::vector<int64_t> reshape_sizes = {num_grid_fields, num_scalar_fields};
     280         264 :     const auto reshaped = buffer.reshape(reshape_sizes);
     281             : 
     282             :     // now loop over scalar components
     283         264 :     const std::array<std::string, 3> xyz = {"x", "y", "z"};
     284         528 :     for (const auto index : make_range(num_scalar_fields))
     285             :     {
     286             :       auto name =
     287             :           buffer_name + (num_scalar_fields > 1
     288         528 :                              ? "_" + (num_scalar_fields <= 3 ? xyz[index] : Moose::stringify(index))
     289         264 :                              : "");
     290             : 
     291         264 :       buffer = reshaped.select(1, index).contiguous();
     292             : 
     293         264 :       auto attr = grid.append_child("Attribute");
     294         264 :       attr.append_attribute("Name") = name.c_str();
     295         308 :       attr.append_attribute("Center") = output_mode == OutputMode::CELL ? "Cell" : "Node";
     296         264 :       auto data = attr.append_child("DataItem");
     297         264 :       data.append_attribute("DataType") = "Float";
     298             : 
     299             :       // TODO: recompute data grid from sizes[0:_dim]
     300         308 :       data.append_attribute("Dimensions") = _data_grid[is_cell ? 0 : 1].c_str();
     301             : 
     302             :       // save file
     303         528 :       const auto setname = name + "." + Moose::stringify(_frame);
     304             : 
     305             :       char * raw_ptr = static_cast<char *>(buffer.data_ptr());
     306         264 :       std::size_t raw_size = buffer.numel();
     307             : 
     308             : #ifdef LIBMESH_HAVE_HDF5
     309         264 :       if (_enable_hdf5)
     310             :       {
     311         264 :         if (buffer.dtype() == torch::kFloat32)
     312           0 :           addDataToHDF5(_hdf5_file_id, setname, raw_ptr, _ndata[is_cell ? 0 : 1], H5T_NATIVE_FLOAT);
     313         264 :         else if (buffer.dtype() == torch::kFloat64)
     314         264 :           addDataToHDF5(
     315         264 :               _hdf5_file_id, setname, raw_ptr, _ndata[is_cell ? 0 : 1], H5T_NATIVE_DOUBLE);
     316           0 :         else if (buffer.dtype() == torch::kInt32)
     317           0 :           addDataToHDF5(_hdf5_file_id, setname, raw_ptr, _ndata[is_cell ? 0 : 1], H5T_NATIVE_INT32);
     318           0 :         else if (buffer.dtype() == torch::kInt64)
     319           0 :           addDataToHDF5(_hdf5_file_id, setname, raw_ptr, _ndata[is_cell ? 0 : 1], H5T_NATIVE_INT64);
     320             :         else
     321           0 :           mooseError("Unsupported output type");
     322             : 
     323         264 :         data.append_attribute("Format") = "HDF";
     324         264 :         const auto h5path = _hdf5_name + ":/" + setname;
     325         264 :         data.append_child(pugi::node_pcdata).set_value(h5path.c_str());
     326             :       }
     327             :       else
     328             : #endif
     329             :       {
     330           0 :         if (buffer.dtype() == torch::kFloat32)
     331             :         {
     332           0 :           data.append_attribute("Precision") = "4";
     333           0 :           raw_size *= 4;
     334             :         }
     335           0 :         else if (buffer.dtype() == torch::kFloat64)
     336             :         {
     337           0 :           data.append_attribute("Precision") = "8";
     338           0 :           raw_size *= 8;
     339             :         }
     340           0 :         else if (buffer.dtype() == torch::kInt32 || buffer.dtype() == torch::kInt64)
     341             :         {
     342           0 :           data.append_attribute("Precision") = "1";
     343             :           raw_size *= 1;
     344             :         }
     345             :         else
     346           0 :           mooseError("Unsupported output type");
     347             : 
     348           0 :         const auto fname = _file_base + "." + setname + ".bin";
     349           0 :         auto file = std::fstream(fname.c_str(), std::ios::out | std::ios::binary);
     350           0 :         file.write(raw_ptr, raw_size);
     351           0 :         file.close();
     352             : 
     353           0 :         data.append_attribute("Format") = "Binary";
     354           0 :         data.append_attribute("Endian") = "Little";
     355           0 :         data.append_child(pugi::node_pcdata).set_value(fname.c_str());
     356           0 :       }
     357             :     }
     358             :   }
     359             : 
     360             :   // write XDMF file
     361          88 :   _doc.save_file((_file_base + ".xmf").c_str());
     362             : 
     363             : #ifdef LIBMESH_HAVE_HDF5
     364             :   // flush hdf5 file contents to disk
     365          88 :   if (_enable_hdf5)
     366          88 :     H5Fflush(_hdf5_file_id, H5F_SCOPE_GLOBAL);
     367             : #endif
     368             : 
     369             :   // increment frame
     370          88 :   _frame++;
     371          88 : }
     372             : 
     373             : torch::Tensor
     374          44 : XDMFTensorOutput::extendTensor(torch::Tensor tensor)
     375             : {
     376             :   // for nodal data we increase each dimension by one and fill in a copy of the slice at 0
     377             :   torch::Tensor first;
     378             :   using torch::indexing::Slice;
     379             : 
     380          44 :   if (_dim == 3)
     381             :   {
     382           0 :     first = tensor.index({0, Slice(), Slice()}).unsqueeze(0);
     383           0 :     tensor = torch::cat({tensor, first}, 0);
     384           0 :     first = tensor.index({Slice(), 0, Slice()}).unsqueeze(1);
     385           0 :     tensor = torch::cat({tensor, first}, 1);
     386           0 :     first = tensor.index({Slice(), Slice(), 0}).unsqueeze(2);
     387           0 :     tensor = torch::cat({tensor, first}, 2);
     388             :   }
     389             : 
     390          44 :   else if (_dim == 2)
     391             :   {
     392         132 :     first = tensor.index({0}).unsqueeze(0);
     393         132 :     tensor = torch::cat({tensor, first}, 0);
     394         264 :     first = tensor.index({Slice(), 0}).unsqueeze(1);
     395         132 :     tensor = torch::cat({tensor, first}, 1);
     396             :   }
     397             :   else
     398           0 :     mooseError("Unsupported tensor dimension");
     399             : 
     400          88 :   return tensor.contiguous();
     401          88 : }
     402             : 
     403             : torch::Tensor
     404           0 : XDMFTensorOutput::upsampleTensor(torch::Tensor tensor)
     405             : {
     406             :   // For nodal nonperiodic data we transform into reciprocal space, add one additional K-vector on
     407             :   // each dimension, and transform back. This should amount to interpolating the spectral "shape
     408             :   // functions" at the nodes, rather than at the cell centers.
     409           0 :   std::vector<int64_t> shape(tensor.sizes().begin(), tensor.sizes().end());
     410           0 :   for (const auto i : make_range(_dim))
     411           0 :     shape[i]++;
     412             : 
     413             :   // return back transform with frequency padding
     414             :   return torch::fft::irfftn(
     415           0 :       _domain.fft(tensor), torch::IntArrayRef(shape.data(), _dim), _domain.getDimIndices());
     416           0 : }
     417             : 
     418             : #ifdef LIBMESH_HAVE_HDF5
     419             : namespace
     420             : {
     421             : void
     422         264 : addDataToHDF5(hid_t file_id,
     423             :               const std::string & dataset_name,
     424             :               const char * data,
     425             :               std::vector<std::size_t> & ndim,
     426             :               hid_t type)
     427             : {
     428             :   hid_t dataset_id, dataspace_id, plist_id;
     429             :   herr_t status;
     430             : 
     431             :   // Open the file in read/write mode, create if it doesn't exist
     432             : 
     433             :   // hsize_t chunk_dims[RANK];
     434         264 :   std::vector<hsize_t> dims(ndim.begin(), ndim.end());
     435             : 
     436             :   // Check if the dataset already exists
     437         264 :   if (H5Lexists(file_id, dataset_name.c_str(), H5P_DEFAULT) > 0)
     438           0 :     mooseError("Dataset '", dataset_name, "' already exists in HDF5 file.");
     439             : 
     440             :   // Create a new dataset
     441         264 :   dataspace_id = H5Screate_simple(dims.size(), dims.data(), nullptr);
     442         264 :   if (dataspace_id < 0)
     443           0 :     mooseError("Error creating dataspace");
     444             : 
     445         264 :   plist_id = H5Pcreate(H5P_DATASET_CREATE);
     446         264 :   if (plist_id < 0)
     447           0 :     mooseError("Error creating property list");
     448             : 
     449         264 :   status = H5Pset_chunk(plist_id, dims.size(), dims.data());
     450         264 :   if (status < 0)
     451           0 :     mooseError("Error setting chunking");
     452             : 
     453         264 :   if (H5Zfilter_avail(H5Z_FILTER_DEFLATE))
     454             :   {
     455             :     unsigned filter_info;
     456         264 :     H5Zget_filter_info(H5Z_FILTER_DEFLATE, &filter_info);
     457         264 :     if (filter_info & H5Z_FILTER_CONFIG_ENCODE_ENABLED)
     458             :     {
     459         264 :       status = H5Pset_deflate(plist_id, 9);
     460         264 :       if (status < 0)
     461           0 :         mooseError("Error setting compression filter");
     462             :     }
     463             :   }
     464             : 
     465         264 :   dataset_id = H5Dcreate(
     466             :       file_id, dataset_name.c_str(), type, dataspace_id, H5P_DEFAULT, plist_id, H5P_DEFAULT);
     467         264 :   if (dataset_id < 0)
     468             :   {
     469             :     mooseInfo(dataset_id,
     470           0 :               ' ',
     471             :               file_id,
     472           0 :               ' ',
     473           0 :               dataset_name.c_str(),
     474           0 :               ' ',
     475             :               type,
     476           0 :               ' ',
     477             :               dataspace_id,
     478           0 :               ' ',
     479           0 :               H5P_DEFAULT,
     480           0 :               ' ',
     481             :               plist_id,
     482           0 :               ' ',
     483           0 :               H5P_DEFAULT);
     484           0 :     mooseError("Error creating dataset");
     485             :   }
     486             : 
     487             :   // Write data to the dataset
     488         264 :   status = H5Dwrite(dataset_id, type, H5S_ALL, dataspace_id, H5P_DEFAULT, data);
     489             : 
     490             :   // Close resources
     491         264 :   H5Pclose(plist_id);
     492         264 :   H5Dclose(dataset_id);
     493         264 :   H5Sclose(dataspace_id);
     494         264 : }
     495             : }
     496             : #endif

Generated by: LCOV version 1.14