Files
mfem/tests/unit/fem/test_datacollection.cpp
T
ce5fd63d0a dfem phase 1 changes
Co-authored-by: camierjs <camierjs@gmail.com>
Co-authored-by: Veselin Dobrev <dobrev@llnl.gov>
2025-05-29 13:02:23 -07:00

385 lines
13 KiB
C++

// Copyright (c) 2010-2025, Lawrence Livermore National Security, LLC. Produced
// at the Lawrence Livermore National Laboratory. All Rights reserved. See files
// LICENSE and NOTICE for details. LLNL-CODE-806117.
//
// This file is part of the MFEM library. For more information and source code
// availability visit https://mfem.org.
//
// MFEM is free software; you can redistribute it and/or modify it under the
// terms of the BSD-3 license. We welcome feedback and contributions, see file
// CONTRIBUTING.md for details.
#include "mfem.hpp"
#include "unit_tests.hpp"
#include "general/tinyxml2.h"
#include <stdio.h>
#ifndef _WIN32
#include <unistd.h> // rmdir
#else
#include <direct.h> // _rmdir
#define rmdir(dir) _rmdir(dir)
#endif
#ifdef MFEM_USE_HDF5
#include <hdf5.h>
#include <hdf5_hl.h>
#endif
using namespace mfem;
TEST_CASE("Save and load from collections", "[DataCollection]")
{
SECTION("VisIt data files")
{
// Set up a small mesh and a couple of grid function on that mesh
Mesh mesh = Mesh::MakeCartesian2D(2, 3, Element::QUADRILATERAL, 0, 2.0, 3.0);
FiniteElementCollection *fec = new LinearFECollection;
FiniteElementSpace *fespace = new FiniteElementSpace(&mesh, fec);
GridFunction *u = new GridFunction(fespace);
GridFunction *v = new GridFunction(fespace);
int N = u->Size();
for (int i = 0; i < N; ++i)
{
(*u)(i) = double(i);
(*v)(i) = double(N - i - 1);
}
int intOrder = 3;
QuadratureSpace *qspace = new QuadratureSpace(&mesh, intOrder);
QuadratureFunction *qs = new QuadratureFunction(qspace, 1);
QuadratureFunction *qv = new QuadratureFunction(qspace, 2);
int Nq = qs->Size();
for (int i = 0; i < Nq; ++i)
{
(*qs)(i) = double(i);
(*qv)(2*i+0) = double(i);
(*qv)(2*i+1) = double(Nq - i - 1);
}
SECTION("Uncompressed MFEM format")
{
// Collect the mesh and grid functions into a DataCollection and test that they got in there
VisItDataCollection dc("base", &mesh);
dc.RegisterField("u", u);
dc.RegisterField("v", v);
dc.RegisterQField("qs",qs);
dc.RegisterQField("qv",qv);
dc.SetCycle(5);
dc.SetTime(8.0);
REQUIRE(dc.GetMesh() == &mesh);
bool has_u = dc.HasField("u");
REQUIRE(has_u);
bool has_v = dc.HasField("v");
REQUIRE(has_v);
bool has_qs = dc.HasQField("qs");
REQUIRE(has_qs);
bool has_qv = dc.HasQField("qv");
REQUIRE(has_qv);
REQUIRE(dc.GetCycle() == 5);
REQUIRE(dc.GetTime() == 8.0);
// Save the DataCollection and load it into a new DataCollection for comparison
dc.SetPadDigitsCycle(5);
dc.SetPadDigitsRank(5);
dc.Save();
VisItDataCollection dc_new("base");
dc_new.SetPadDigitsCycle(5);
dc_new.SetPadDigitsRank(5);
dc_new.Load(dc.GetCycle());
Mesh* mesh_new = dc_new.GetMesh();
GridFunction *u_new = dc_new.GetField("u");
GridFunction *v_new = dc_new.GetField("v");
QuadratureFunction *qs_new = dc_new.GetQField("qs");
QuadratureFunction *qv_new = dc_new.GetQField("qv");
REQUIRE(mesh_new);
REQUIRE(u_new);
REQUIRE(v_new);
REQUIRE(qs_new);
REQUIRE(qv_new);
// Compare some collection parameters for old and new
std::string name, name_new;
name = dc.GetCollectionName();
name_new = dc_new.GetCollectionName();
REQUIRE(name == name_new);
REQUIRE(dc.GetCycle() == dc_new.GetCycle());
REQUIRE(dc.GetTime() == dc_new.GetTime());
// Compare the new mesh with the old mesh
// (Just a basic comparison here, a full comparison should be done in Mesh unit testing)
REQUIRE(mesh.Dimension() == mesh_new->Dimension());
REQUIRE(mesh.SpaceDimension() == mesh_new->SpaceDimension());
Vector vert, vert_diff;
mesh.GetVertices(vert);
mesh_new->GetVertices(vert_diff);
vert_diff -= vert;
REQUIRE(vert_diff.Normlinf() < 1e-10);
// Compare the old and new grid functions
// (Just a basic comparison here, a full comparison should be done in GridFunction unit testing)
Vector u_diff(*u_new), v_diff(*v_new);
u_diff -= *u;
v_diff -= *v;
REQUIRE(u_diff.Normlinf() < 1e-10);
REQUIRE(v_diff.Normlinf() < 1e-10);
// Compare the old and new quadrature functions
// (Just a basic comparison here, a full comparison should be done in GridFunction unit testing)
Vector qs_diff(*qs_new), qv_diff(*qv_new);
qs_diff -= *qs;
qv_diff -= *qv;
REQUIRE(qs_diff.Normlinf() < 1e-10);
REQUIRE(qv_diff.Normlinf() < 1e-10);
// Cleanup all the files
REQUIRE(remove("base_00005.mfem_root") == 0);
REQUIRE(remove("base_00005/mesh.00000") == 0);
REQUIRE(remove("base_00005/u.00000") == 0);
REQUIRE(remove("base_00005/v.00000") == 0);
REQUIRE(remove("base_00005/qs.00000") == 0);
REQUIRE(remove("base_00005/qv.00000") == 0);
REQUIRE(rmdir("base_00005") == 0);
}
#ifdef MFEM_USE_ZLIB
SECTION("Compressed MFEM format")
{
// Collect the mesh and grid functions into a DataCollection and test that they got in there
VisItDataCollection dc("base", &mesh);
dc.RegisterField("u", u);
dc.RegisterField("v", v);
dc.RegisterQField("qs",qs);
dc.RegisterQField("qv",qv);
dc.SetCycle(5);
dc.SetTime(8.0);
REQUIRE(dc.GetMesh() == &mesh);
bool has_u = dc.HasField("u");
REQUIRE(has_u);
bool has_v = dc.HasField("v");
REQUIRE(has_v);
bool has_qs = dc.HasQField("qs");
REQUIRE(has_qs);
bool has_qv = dc.HasQField("qv");
REQUIRE(has_qv);
REQUIRE(dc.GetCycle() == 5);
REQUIRE(dc.GetTime() == 8.0);
// Save the DataCollection and load it into a new DataCollection for comparison
dc.SetPadDigitsCycle(5);
dc.SetPadDigitsRank(5);
dc.SetCompression(true);
dc.Save();
VisItDataCollection dc_new("base");
dc_new.SetPadDigitsCycle(5);
dc_new.SetPadDigitsRank(5);
dc_new.Load(dc.GetCycle());
Mesh *mesh_new = dc_new.GetMesh();
GridFunction *u_new = dc_new.GetField("u");
GridFunction *v_new = dc_new.GetField("v");
QuadratureFunction *qs_new = dc_new.GetQField("qs");
QuadratureFunction *qv_new = dc_new.GetQField("qv");
REQUIRE(mesh_new);
REQUIRE(u_new);
REQUIRE(v_new);
REQUIRE(qs_new);
REQUIRE(qv_new);
// Compare some collection parameters for old and new
std::string name, name_new;
name = dc.GetCollectionName();
name_new = dc_new.GetCollectionName();
REQUIRE(name == name_new);
REQUIRE(dc.GetCycle() == dc_new.GetCycle());
REQUIRE(dc.GetTime() == dc_new.GetTime());
// Compare the new mesh with the old mesh
// (Just a basic comparison here, a full comparison should be done in Mesh unit testing)
REQUIRE(mesh.Dimension() == mesh_new->Dimension());
REQUIRE(mesh.SpaceDimension() == mesh_new->SpaceDimension());
Vector vert, vert_diff;
mesh.GetVertices(vert);
mesh_new->GetVertices(vert_diff);
vert_diff -= vert;
REQUIRE(vert_diff.Normlinf() < 1e-10);
// Compare the old and new grid functions
// (Just a basic comparison here, a full comparison should be done in GridFunction unit testing)
Vector u_diff(*u_new), v_diff(*v_new);
u_diff -= *u;
v_diff -= *v;
REQUIRE(u_diff.Normlinf() < 1e-10);
REQUIRE(v_diff.Normlinf() < 1e-10);
// Compare the old and new quadrature functions
// (Just a basic comparison here, a full comparison should be done in GridFunction unit testing)
Vector qs_diff(*qs_new), qv_diff(*qv_new);
qs_diff -= *qs;
qv_diff -= *qv;
REQUIRE(qs_diff.Normlinf() < 1e-10);
REQUIRE(qv_diff.Normlinf() < 1e-10);
// Cleanup all the files
REQUIRE(remove("base_00005.mfem_root") == 0);
REQUIRE(remove("base_00005/mesh.00000") == 0);
REQUIRE(remove("base_00005/u.00000") == 0);
REQUIRE(remove("base_00005/v.00000") == 0);
REQUIRE(remove("base_00005/qs.00000") == 0);
REQUIRE(remove("base_00005/qv.00000") == 0);
REQUIRE(rmdir("base_00005") == 0);
}
#endif
delete fec;
delete fespace;
delete u;
delete v;
delete qspace;
delete qs;
delete qv;
}
}
void SaveDataCollection(DataCollection &dc, int cycle, double t)
{
dc.SetCycle(cycle);
dc.SetTime(t);
dc.Save();
}
TEST_CASE("ParaView restart mode", "[ParaView]")
{
Mesh mesh = Mesh::MakeCartesian2D(2, 3, Element::QUADRILATERAL);
H1_FECollection fec(1, mesh.Dimension());
FiniteElementSpace fes(&mesh, &fec);
GridFunction u(&fes);
u = 0.0;
// Write initial dataset with three timesteps: 0, 1, 2.
{
ParaViewDataCollection dc("ParaView", &mesh);
dc.RegisterField("u", &u);
SaveDataCollection(dc, 0, 0);
SaveDataCollection(dc, 1, 1);
SaveDataCollection(dc, 2, 2);
}
// Using restart mode, append to the existing dataset, overwriting timesteps
// 1 and 2 with 1 and 1.5.
{
ParaViewDataCollection dc("ParaView", &mesh);
dc.UseRestartMode(true);
dc.RegisterField("u", &u);
SaveDataCollection(dc, 1, 1.0);
SaveDataCollection(dc, 2, 1.5);
}
// Parse the resulting PVD file, and verify that the structure is correct,
// and that it contains three timesteps: 0, 1, and 1.5.
using namespace tinyxml2;
auto StringCompare = [](const char *s1, const char *s2)
{
if (s1 == NULL || s2 == NULL) { return false; }
return strcmp(s1, s2) == 0;
};
auto VerifyDataset = [StringCompare](const XMLElement *ds, double t_ref)
{
REQUIRE(ds);
REQUIRE(StringCompare(ds->Name(), "DataSet"));
const char *timestep = ds->Attribute("timestep");
REQUIRE(timestep);
double t = std::stod(timestep);
REQUIRE(t == MFEM_Approx(t_ref));
};
XMLDocument xml;
xml.LoadFile("ParaView/ParaView.pvd");
REQUIRE(xml.ErrorID() == XML_SUCCESS);
const XMLElement *vtkfile = xml.FirstChildElement();
REQUIRE(vtkfile);
REQUIRE(StringCompare(vtkfile->Name(), "VTKFile"));
const XMLElement *collection = vtkfile->FirstChildElement();
REQUIRE(collection);
REQUIRE(StringCompare(collection->Name(), "Collection"));
const XMLElement *dataset = collection->FirstChildElement();
VerifyDataset(dataset, 0.0);
dataset = dataset->NextSiblingElement();
VerifyDataset(dataset, 1.0);
dataset = dataset->NextSiblingElement();
VerifyDataset(dataset, 1.5);
REQUIRE(dataset->NextSiblingElement() == nullptr);
// Clean up
for (int c=0; c<=2; ++c)
{
std::string prefix = "ParaView/Cycle00000" + std::to_string(c);
REQUIRE(remove((prefix + "/data.pvtu").c_str()) == 0);
REQUIRE(remove((prefix + "/proc000000.vtu").c_str()) == 0);
REQUIRE(rmdir(prefix.c_str()) == 0);
}
REQUIRE(remove("ParaView/ParaView.pvd") == 0);
REQUIRE(rmdir("ParaView") == 0);
}
#ifdef MFEM_USE_HDF5
TEST_CASE("ParaView VTKHDF restart mode", "[ParaView]")
{
Mesh mesh = Mesh::MakeCartesian2D(2, 3, Element::QUADRILATERAL);
H1_FECollection fec(1, mesh.Dimension());
FiniteElementSpace fes(&mesh, &fec);
GridFunction u(&fes);
u = 0.0;
// Write initial dataset with three timesteps: 0, 1, 2.
{
ParaViewHDFDataCollection dc("ParaView", &mesh);
dc.RegisterField("u", &u);
SaveDataCollection(dc, 0, 0);
SaveDataCollection(dc, 1, 1);
SaveDataCollection(dc, 2, 2);
}
auto read_time_vals = []()
{
const hid_t f = H5Fopen("ParaView.vtkhdf", H5F_ACC_RDONLY, H5P_DEFAULT);
hsize_t size;
H5LTget_dataset_info(f, "/VTKHDF/Steps/Values", &size, nullptr, nullptr);
std::vector<double> vals(size);
H5LTread_dataset(f, "/VTKHDF/Steps/Values", H5T_NATIVE_DOUBLE, vals.data());
H5Fclose(f);
return vals;
};
// Check that the time steps were written correctly
REQUIRE(read_time_vals() == std::vector<double>({0.0, 1.0, 2.0}));
// Using restart mode, append to the existing dataset, overwriting timesteps
// 1 and 2 with 1 and 1.5.
{
ParaViewHDFDataCollection dc("ParaView", &mesh);
dc.UseRestartMode(true);
dc.RegisterField("u", &u);
SaveDataCollection(dc, 1, 1.0);
SaveDataCollection(dc, 2, 1.5);
}
// Check that the time steps were updated correctly
REQUIRE(read_time_vals() == std::vector<double>({0.0, 1.0, 1.5}));
// Clean up
REQUIRE(remove("ParaView.vtkhdf") == 0);
}
#endif // MFEM_USE_HDF5