Files
MeanField_SERiF/tests/serialize/restart.cpp
Emily Boudreaux cb20faeb3d test(tests): added test suite
The test suite is extensive. Note that LLMs were used to aid in the development of the test suite. All LLM generated code was manually reviewed.
2026-11-09 13:53:17 -05:00

135 lines
7.7 KiB
C++

#include <algorithm>
#include <chrono>
#include <filesystem>
#include <memory>
#include <tuple>
#include <type_traits>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "mfem/general/communication.hpp"
#include "serif/backend/mfem/serialize/configuration.hpp"
#include "serif/solver/equilibrium.hpp"
#include "serif/backend/mfem/solver/seed/state.hpp"
#include "serif/solver/seed/polytrope.hpp"
#include "serif/tests/solver/stellar_fixture.hpp"
#include "serif/utils/serialize/archive.hpp"
#include "serif/utils/serialize/solver.hpp"
namespace {
namespace serialization = serif::utils::serialize;
namespace backend_serialization = serif::backend::mfem::serialize;
namespace solver = serif::solver;
namespace fixture = serif::tests::solver;
/** @brief Keep independently running tests from sharing an archive and remove it on every exit path. */
struct TemporaryArchive {
std::filesystem::path path = std::filesystem::temp_directory_path() /
("serif-newton-restart-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".serif");
/** @brief Test failures must not leave temporary scientific files behind. */
~TemporaryArchive() {
std::error_code error;
std::filesystem::remove(path, error);
}
};
}
TEST_CASE("A moving stellar checkpoint survives destruction, file loading and Newton continuation", "[serialize][restart][moving_restart]") {
if (!::mfem::Mpi::IsInitialized()) {
::mfem::Mpi::Init();
}
using Model = decltype(fixture::make_model());
std::weak_ptr<const void> original_resources;
const auto saved = [&] {
const Model model = fixture::make_model();
const auto radial = solver::seed::make_polytropic_seed(model);
REQUIRE(radial);
const auto finite_elements = serif::model::make_stellar_discretization(model, {.radius = radial->scales.radius},
serif::geometry::deformation::DomainDeformation<serif::geometry::deformation::SphericalHarmonicSurface>{
.surface = {.maximum_degree = 0}, .stellar_interior = {}, .vacuum = {}});
original_resources = finite_elements.owner;
const auto initial = solver::seed::backend::mfem::make_polytropic_state(model, finite_elements, *radial);
REQUIRE(initial);
const auto scales = solver::normalization::prepare_stellar_reference_scales(
serif::dimensions::MassValue{1.0}, serif::dimensions::LengthValue{1.0});
REQUIRE(scales);
const solver::normalization::NormalizationOptions normalization{.scheme = *scales};
const auto configuration = serif::model::make_discretization(serif::model::DiscretizationOptions{
.model = model, .finite_elements = finite_elements, .normalization = normalization});
const solver::SolverOptions options{.method = {.relative_tolerance = 1.0e-8, .absolute_tolerance = 1.0e-10,
.maximum_iterations = 15, .linear_solve = {.relative_tolerance = 1.0e-3, .maximum_iterations = 200}}};
const auto uninterrupted = solver::solve_equilibrium(configuration, *initial, options);
INFO(uninterrupted.message());
REQUIRE(uninterrupted.converged());
const auto stop = [](const solver::SolveEvent& event, const auto&, const auto&) {
return event.kind == solver::EventKind::accepted ? solver::SolveControl::stop : solver::SolveControl::continue_solve;
};
const auto interrupted = solver::solve_equilibrium(configuration, *initial, options, stop);
INFO(interrupted.message());
REQUIRE(interrupted.status() == solver::SolveStatus::stopped);
REQUIRE(interrupted.has_checkpoint());
CHECK(interrupted.diagnostics().accepted_iterations == 1);
const auto checkpoint = interrupted.checkpoint();
CHECK(std::ranges::any_of(checkpoint.state.template get<fixture::Shape>(), [](double value) { return value != 0.0; }));
auto contents = serialization::pack(configuration, options, interrupted);
return std::tuple{std::move(contents), uninterrupted.state(), uninterrupted.diagnostics(),
std::type_identity<std::remove_cvref_t<decltype(configuration)>>{}, std::type_identity<std::remove_cvref_t<decltype(options)>>{}};
}();
REQUIRE(original_resources.expired());
using State = std::remove_cvref_t<decltype(std::get<1>(saved))>;
using Checkpoint = solver::NewtonCheckpoint<State>;
using Configuration = typename std::remove_cvref_t<decltype(std::get<3>(saved))>::type;
using Options = typename std::remove_cvref_t<decltype(std::get<4>(saved))>::type;
const TemporaryArchive archive;
const auto written = serialization::write(archive.path, std::get<0>(saved), {.author = "SERiF regression", .comment = "Accepted moving-star Newton boundary"});
REQUIRE(written);
auto reader = serialization::Reader::open(archive.path);
REQUIRE(reader);
const auto checkpoint = reader->read<Checkpoint>("solution/checkpoint");
if (!checkpoint) {
INFO(checkpoint.error().path << ": " << checkpoint.error().message);
}
REQUIRE(checkpoint);
const TemporaryArchive text_archive;
REQUIRE(serialization::write(text_archive.path, serialization::pack(*checkpoint), {.format = serialization::Format::text}));
auto text_reader = serialization::Reader::open(text_archive.path);
REQUIRE(text_reader);
const auto text_checkpoint = text_reader->read<Checkpoint>();
REQUIRE(text_checkpoint);
CHECK(serialization::encode(*text_checkpoint) == serialization::encode(*checkpoint));
const auto configuration_node = reader->node("configuration");
REQUIRE(configuration_node);
const auto configuration = backend_serialization::restore_configuration<Configuration>(*configuration_node);
if (!configuration) {
INFO(configuration.error().path << ": " << configuration.error().message);
}
REQUIRE(configuration);
const auto options = reader->read<Options>();
REQUIRE(options);
const auto resumed = solver::resume_equilibrium(*configuration, *checkpoint, *options);
INFO(resumed.message());
REQUIRE(resumed.converged());
const State& expected = std::get<1>(saved);
REQUIRE(resumed.state().values().size() == expected.values().size());
for (std::size_t index = 0; index < expected.values().size(); ++index) {
CHECK(resumed.state().values()[index] == Catch::Approx(expected.values()[index]).margin(2.0e-10).epsilon(2.0e-10));
}
const solver::SolveDiagnostics& diagnostics = std::get<2>(saved);
CHECK(resumed.diagnostics().initial_residual_norm == diagnostics.initial_residual_norm);
CHECK(resumed.diagnostics().convergence_threshold == diagnostics.convergence_threshold);
CHECK(resumed.diagnostics().accepted_iterations == diagnostics.accepted_iterations);
CHECK(resumed.diagnostics().line_search_trials == diagnostics.line_search_trials);
CHECK(resumed.diagnostics().rejected_trials == diagnostics.rejected_trials);
CHECK(resumed.diagnostics().total_linear_iterations == diagnostics.total_linear_iterations);
CHECK(reader->contains("solution/model"));
CHECK(reader->contains("solution/reference_scales"));
CHECK(reader->contains("solution/coordinates"));
CHECK(reader->read<Checkpoint>("solution/checkpoint"));
auto corrupted = serialization::encode(*checkpoint);
corrupted.children.at("progress").children.at("correction") = serialization::encode(std::vector<double>{});
CHECK_FALSE(serialization::decode<Checkpoint>(corrupted));
corrupted = serialization::encode(*checkpoint);
corrupted.children.at("strategy") = serialization::encode(std::string{"unknown_stateful_strategy"});
CHECK_FALSE(serialization::decode<Checkpoint>(corrupted));
}