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.
This commit is contained in:
2026-11-09 13:53:17 -05:00
parent 74e55a6859
commit cb20faeb3d
202 changed files with 35505 additions and 953 deletions

View File

@@ -0,0 +1,706 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <memory>
#include <span>
#include <stdexcept>
#include <tuple>
#include <utility>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_approx.hpp>
#include "mfem/fem/coefficient.hpp"
#include "serif/backend/mfem/geometry/deformation/computations.hpp"
#include "serif/tests/deformation/mesh.hpp"
#include "serif/utils/dependencies/computation_cache.hpp"
namespace
{
namespace deformation = serif::geometry::deformation;
namespace backend = deformation::backend::mfem;
namespace dependencies = serif::utils::dependencies;
namespace keys = deformation::keys;
using Status = dependencies::PreparationStatus;
using Model = deformation::DomainDeformation<deformation::SphericalHarmonicSurface>;
/** @brief Keep borrowed meshes and shape coefficients alive through every graph preparation. */
struct GraphFixture
{
serif::tests::deformation::PairedMesh mesh;
backend::ReferenceDiscretization discretization;
Model model{.surface = {.maximum_degree = 2}, .stellar_interior = {}, .vacuum = {}};
std::vector<double> coefficients = std::vector<double>(9, 0.01);
/// Resolve domain attributes through the same schema used by the stellar mesh.
GraphFixture() : discretization(backend::deformation_discretization(mesh.scalar_space, mesh.vector_space, *mesh.generated.reference_mesh))
{
}
/// Reconstructed context wrappers must not invalidate unchanged physical values.
[[nodiscard]] auto inputs() const
{
return backend::make_deformation_inputs(model, discretization, coefficients);
}
};
/** @brief Construct the dependency cache without performing numerical reference compilation. */
[[nodiscard]] auto make_cache(const GraphFixture& fixture)
{
return dependencies::make_computation_cache<deformation::DeformationFailure>(backend::make_deformation_graph<deformation::SphericalHarmonicSurface>(), fixture.inputs());
}
/** @brief A one-coordinate nonlinear surface distinguishes equal displacements from unequal tangents. */
struct SquaredSurface
{
using InputSpace = deformation::spaces::ShapeParameters;
using OutputSpace = deformation::spaces::SurfaceDisplacement;
struct Workspace
{
};
static constexpr bool is_linear = false;
/// A single shape coordinate suffices to expose the derivative-sign regression.
[[nodiscard]] std::size_t input_size() const
{
return 1;
}
/// This algebraic fixture has one trace component.
[[nodiscard]] std::size_t output_size() const
{
return 1;
}
/// The polynomial needs no temporary numerical storage.
[[nodiscard]] Workspace make_workspace() const
{
return {};
}
/// Opposite coefficients deliberately produce identical displacement values.
[[nodiscard]] deformation::DeformationStatus build_displacement(std::span<const double> state, std::span<double> output, Workspace&) const
{
if (const auto status = deformation::validate_deformation_values(state, 1); !status)
{
return status;
}
output[0] = state[0] * state[0];
return {};
}
/// Retain the base-state sign in the tangent even when the displacement is unchanged.
[[nodiscard]] deformation::DeformationStatus apply_jacobian(std::span<const double> state, std::span<const double> direction, std::span<double> output, Workspace&) const
{
output[0] = 2.0 * state[0] * direction[0];
return {};
}
/// A scalar transpose preserves the same work-conjugate derivative.
[[nodiscard]] deformation::DeformationStatus apply_jacobian_transpose(std::span<const double> state, std::span<const double> dual, std::span<double> output, Workspace&) const
{
output[0] = 2.0 * state[0] * dual[0];
return {};
}
/// Nonlinear shape coordinates contribute the Hessian term to a Newton force action.
[[nodiscard]] deformation::DeformationStatus apply_pullback_derivative(std::span<const double>, std::span<const double> direction, std::span<const double> dual, std::span<double> output, Workspace&) const
{
output[0] = 2.0 * direction[0] * dual[0];
return {};
}
};
/** @brief Preserve the nonlinear surface value through a simple exact linear extension. */
struct UnitExtension
{
using InputSpace = deformation::spaces::SurfaceDisplacement;
using OutputSpace = deformation::spaces::VolumeDisplacement;
using Workspace = deformation::EmptyDeformationWorkspace;
static constexpr bool is_linear = true;
/// This extension accepts the fixture's single trace component.
[[nodiscard]] std::size_t input_size() const
{
return 1;
}
/// A single volume value makes the expected derivative independent of a mesh.
[[nodiscard]] std::size_t output_size() const
{
return 1;
}
/// Identity extension preserves the physical value exactly.
[[nodiscard]] deformation::DeformationStatus apply(std::span<const double> input, std::span<double> output, Workspace = {}) const
{
output[0] = input[0];
return {};
}
/// Identity force transfer preserves virtual work exactly.
[[nodiscard]] deformation::DeformationStatus apply_transpose(std::span<const double> input, std::span<double> output, Workspace = {}) const
{
return apply(input, output);
}
};
} // namespace
TEST_CASE("Deformation graph construction is lazy and equal physical inputs reuse reference work", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
CHECK_FALSE(cache.ready());
CHECK(cache.preparation_count() == 0);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto surface = cache.get<keys::SurfaceMap>();
const auto interpolation = cache.get<keys::RadialInterpolation>();
const auto map = cache.get<keys::DomainMap>();
const std::vector<double> original = cache.get<keys::VolumeDisplacement>();
const std::vector<double> copied_coefficients = fixture.coefficients;
const auto equal_inputs = backend::make_deformation_inputs(fixture.model, fixture.discretization, copied_coefficients);
const auto reused = cache.try_prepare(equal_inputs);
REQUIRE(reused);
CHECK(reused->computed_count() == 0);
CHECK(reused->status<keys::ReferenceSurface>() == Status::reused);
CHECK(reused->status<keys::SurfaceMap>() == Status::reused);
CHECK(reused->status<keys::VolumeExtension>() == Status::reused);
CHECK(reused->status<keys::Linearization>() == Status::reused);
CHECK(cache.get<keys::SurfaceMap>() == surface);
CHECK(cache.get<keys::RadialInterpolation>() == interpolation);
CHECK(cache.get<keys::DomainMap>() == map);
CHECK(cache.get<keys::VolumeDisplacement>() == original);
}
TEST_CASE("Changing shape coefficients recomputes only deformation values and their linearization", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto reference = cache.get<keys::ReferenceSurface>();
const auto interpolation = cache.get<keys::RadialInterpolation>();
const auto map = cache.get<keys::DomainMap>();
const std::vector<double> original = cache.get<keys::VolumeDisplacement>();
fixture.coefficients[0] += 0.03;
const auto report = cache.try_prepare(fixture.inputs());
REQUIRE(report);
CHECK(report->status<keys::ReferenceSurface>() == Status::reused);
CHECK(report->status<keys::RadialInterpolation>() == Status::reused);
CHECK(report->status<keys::SurfaceMap>() == Status::reused);
CHECK(report->status<keys::VolumeExtension>() == Status::reused);
CHECK(report->status<keys::DomainMap>() == Status::reused);
CHECK(report->status<keys::SurfaceDisplacement>() == Status::computed);
CHECK(report->status<keys::VolumeDisplacement>() == Status::computed);
CHECK(report->status<keys::Linearization>() == Status::computed);
CHECK(cache.get<keys::ReferenceSurface>() == reference);
CHECK(cache.get<keys::RadialInterpolation>() == interpolation);
CHECK(cache.get<keys::DomainMap>() == map);
CHECK(cache.get<keys::VolumeDisplacement>() != original);
}
TEST_CASE("Changing the stellar radial power retains interpolation and the surface representation", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto interpolation = cache.get<keys::RadialInterpolation>();
const auto surface = cache.get<keys::SurfaceMap>();
const std::vector<double> original = cache.get<keys::VolumeDisplacement>();
fixture.model.stellar_interior.radial_power = 3.0;
const auto report = cache.try_prepare(fixture.inputs());
REQUIRE(report);
CHECK(report->status<keys::ReferenceSurface>() == Status::reused);
CHECK(report->status<keys::RadialInterpolation>() == Status::reused);
CHECK(report->status<keys::SurfaceMap>() == Status::reused);
CHECK(report->status<keys::SurfaceDisplacement>() == Status::reused);
CHECK(report->status<keys::VolumeExtension>() == Status::computed);
CHECK(report->status<keys::DomainMap>() == Status::computed);
CHECK(report->status<keys::VolumeDisplacement>() == Status::computed);
CHECK(report->status<keys::Linearization>() == Status::computed);
CHECK(cache.get<keys::RadialInterpolation>() == interpolation);
CHECK(cache.get<keys::SurfaceMap>() == surface);
CHECK(cache.get<keys::VolumeExtension>()->interpolation() == interpolation);
CHECK(cache.get<keys::VolumeDisplacement>() != original);
}
TEST_CASE("Changing the reference center invalidates radial surface directions without rebuilding logical interpolation", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto interpolation = cache.get<keys::RadialInterpolation>();
const auto extension = cache.get<keys::VolumeExtension>();
const std::vector<double> original = cache.get<keys::SurfaceDisplacement>();
fixture.model.surface.reference_center[0] = 0.02;
const auto report = cache.try_prepare(fixture.inputs());
REQUIRE(report);
CHECK(report->status<keys::ReferenceSurface>() == Status::reused);
CHECK(report->status<keys::RadialInterpolation>() == Status::reused);
CHECK(report->status<keys::VolumeExtension>() == Status::reused);
CHECK(report->status<keys::SurfaceMap>() == Status::computed);
CHECK(report->status<keys::DomainMap>() == Status::computed);
CHECK(report->status<keys::SurfaceDisplacement>() == Status::computed);
CHECK(report->status<keys::VolumeDisplacement>() == Status::computed);
CHECK(report->status<keys::Linearization>() == Status::computed);
CHECK(cache.get<keys::RadialInterpolation>() == interpolation);
CHECK(cache.get<keys::VolumeExtension>() == extension);
CHECK(cache.get<keys::SurfaceDisplacement>() != original);
}
TEST_CASE("Cached deformation linearizations freeze borrowed coefficients until the next accepted preparation", "[integration][deformation]")
{
GraphFixture fixture;
const auto inputs = fixture.inputs();
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(inputs));
const auto accepted = cache.get<keys::Linearization>();
const std::vector<double> original = cache.get<keys::VolumeDisplacement>();
auto workspace = deformation::make_deformation_workspace(*accepted.map);
std::vector<double> action(original.size());
REQUIRE(deformation::apply_jacobian(accepted, accepted.parameters, action, workspace));
CHECK(action == original);
fixture.coefficients[0] += 0.2;
CHECK(accepted.parameters != fixture.coefficients);
CHECK(cache.get<keys::Linearization>().parameters == accepted.parameters);
CHECK(cache.get<keys::VolumeDisplacement>() == original);
REQUIRE(deformation::apply_jacobian(accepted, accepted.parameters, action, workspace));
CHECK(action == original);
REQUIRE(cache.try_prepare(inputs));
CHECK(cache.get<keys::Linearization>().parameters == fixture.coefficients);
CHECK(accepted.parameters != fixture.coefficients);
CHECK(cache.get<keys::VolumeDisplacement>() != original);
}
TEST_CASE("Physical and logical coordinate revisions invalidate only their own reference mathematics", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto reference = cache.get<keys::ReferenceSurface>();
const auto interpolation = cache.get<keys::RadialInterpolation>();
const std::vector<double> original = cache.get<keys::VolumeDisplacement>();
bool physical = false;
SECTION("Physical mesh node revision")
{
physical = true;
fixture.mesh.generated.mesh->NodesUpdated();
}
SECTION("Logical mesh node revision")
{
fixture.mesh.generated.reference_mesh->NodesUpdated();
}
const auto report = cache.try_prepare(fixture.inputs());
REQUIRE(report);
CHECK(report->status<keys::SurfaceNodes>() == Status::reused);
CHECK(report->status<keys::ReferenceSurface>() == (physical ? Status::computed : Status::reused));
CHECK(report->status<keys::SurfaceMap>() == (physical ? Status::computed : Status::reused));
CHECK(report->status<keys::RadialInterpolation>() == (physical ? Status::reused : Status::computed));
CHECK(report->status<keys::VolumeExtension>() == (physical ? Status::reused : Status::computed));
CHECK(report->status<keys::Linearization>() == Status::computed);
CHECK((cache.get<keys::ReferenceSurface>() != reference) == physical);
CHECK((cache.get<keys::RadialInterpolation>() == interpolation) == physical);
CHECK(cache.get<keys::VolumeDisplacement>() == original);
}
TEST_CASE("Physical coordinate edits reuse interpolation and agree with a fresh deformation compilation", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto reference = cache.get<keys::ReferenceSurface>();
const auto interpolation = cache.get<keys::RadialInterpolation>();
const std::vector<double> original = cache.get<keys::VolumeDisplacement>();
::mfem::VectorFunctionCoefficient stretch(3, [](const ::mfem::Vector& input, ::mfem::Vector& output)
{
output = input;
output[2] *= 0.85;
});
fixture.mesh.generated.mesh->Transform(stretch);
fixture.mesh.generated.mesh->NodesUpdated();
const auto report = cache.try_prepare(fixture.inputs());
REQUIRE(report);
CHECK(report->status<keys::SurfaceNodes>() == Status::reused);
CHECK(report->status<keys::ReferenceSurface>() == Status::computed);
CHECK(report->status<keys::RadialInterpolation>() == Status::reused);
CHECK(cache.get<keys::ReferenceSurface>()->positions != reference->positions);
CHECK(cache.get<keys::RadialInterpolation>() == interpolation);
CHECK(cache.get<keys::VolumeDisplacement>() != original);
auto fresh = make_cache(fixture);
REQUIRE(fresh.try_prepare(fixture.inputs()));
CHECK(cache.get<keys::ReferenceSurface>()->positions == fresh.get<keys::ReferenceSurface>()->positions);
CHECK(cache.get<keys::VolumeDisplacement>() == fresh.get<keys::VolumeDisplacement>());
}
TEST_CASE("Logical coordinate edits reuse the physical surface and agree with a fresh deformation compilation", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto reference = cache.get<keys::ReferenceSurface>();
const auto interpolation = cache.get<keys::RadialInterpolation>();
const std::vector<double> original = cache.get<keys::VolumeDisplacement>();
::mfem::VectorFunctionCoefficient scale(3, [](const ::mfem::Vector& input, ::mfem::Vector& output)
{
output = input;
output *= 2.0;
});
fixture.mesh.generated.reference_mesh->Transform(scale);
fixture.mesh.generated.reference_mesh->NodesUpdated();
const auto report = cache.try_prepare(fixture.inputs());
REQUIRE(report);
CHECK(report->status<keys::SurfaceNodes>() == Status::reused);
CHECK(report->status<keys::ReferenceSurface>() == Status::reused);
CHECK(report->status<keys::RadialInterpolation>() == Status::computed);
CHECK(cache.get<keys::ReferenceSurface>() == reference);
CHECK(cache.get<keys::RadialInterpolation>()->data().stellar_radius == Catch::Approx(2.0 * interpolation->data().stellar_radius));
auto fresh = make_cache(fixture);
REQUIRE(fresh.try_prepare(fixture.inputs()));
CHECK(cache.get<keys::RadialInterpolation>()->data().logical_radii == fresh.get<keys::RadialInterpolation>()->data().logical_radii);
CHECK(cache.get<keys::VolumeDisplacement>() == fresh.get<keys::VolumeDisplacement>());
for (std::size_t index = 0; index < original.size(); ++index)
{
CHECK(cache.get<keys::VolumeDisplacement>()[index] == Catch::Approx(original[index]).margin(1.0e-12));
}
}
TEST_CASE("Material attribute edits are tracked without requiring a mesh revision bump", "[integration][deformation]")
{
namespace domains = serif::discretization::domain;
using Schema = serif::tests::deformation::Schema;
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto interpolation = cache.get<keys::RadialInterpolation>();
const std::vector<double> original = cache.get<keys::VolumeDisplacement>();
const int core = Schema::domain_id<domains::CoreDomain>();
const int envelope = Schema::domain_id<domains::EnvelopeDomain>();
int selected = -1;
for (int element = 0; element < fixture.mesh.generated.mesh->GetNE(); ++element)
{
if (fixture.mesh.generated.mesh->GetAttribute(element) == core)
{
selected = element;
break;
}
}
REQUIRE(selected >= 0);
fixture.mesh.generated.mesh->GetElement(selected)->SetAttribute(envelope);
fixture.mesh.generated.reference_mesh->GetElement(selected)->SetAttribute(envelope);
const auto report = cache.try_prepare(fixture.inputs());
REQUIRE(report);
CHECK(report->status<keys::SurfaceNodes>() == Status::reused);
CHECK(report->status<keys::ReferenceSurface>() == Status::reused);
CHECK(report->status<keys::RadialInterpolation>() == Status::computed);
CHECK(cache.get<keys::RadialInterpolation>() != interpolation);
CHECK(cache.get<keys::VolumeDisplacement>() == original);
}
TEST_CASE("Distinct mesh and space identities invalidate same-sized deformation data", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto reference = cache.get<keys::ReferenceSurface>();
const auto interpolation = cache.get<keys::RadialInterpolation>();
GraphFixture other;
REQUIRE(fixture.mesh.scalar_space.GetVSize() == other.mesh.scalar_space.GetVSize());
REQUIRE(fixture.mesh.vector_space.GetVSize() == other.mesh.vector_space.GetVSize());
const auto report = cache.try_prepare(other.inputs());
REQUIRE(report);
CHECK(report->status<keys::ReferenceSurface>() == Status::computed);
CHECK(report->status<keys::RadialInterpolation>() == Status::computed);
CHECK(report->status<keys::SurfaceMap>() == Status::computed);
CHECK(report->status<keys::VolumeExtension>() == Status::computed);
CHECK(cache.get<keys::ReferenceSurface>() != reference);
CHECK(cache.get<keys::RadialInterpolation>() != interpolation);
}
TEST_CASE("Requested reference results do not evaluate invalid trial shape coefficients", "[integration][deformation]")
{
GraphFixture fixture;
std::ranges::fill(fixture.coefficients, std::numeric_limits<double>::quiet_NaN());
auto cache = make_cache(fixture);
const auto reference_report = cache.try_prepare(fixture.inputs(), dependencies::RequestedResults<keys::ReferenceSurface>{});
REQUIRE(reference_report);
CHECK(reference_report->computed_count() == 2);
CHECK(cache.contains<keys::ReferenceSurface>());
CHECK_FALSE(cache.contains<keys::SurfaceDisplacement>());
CHECK_FALSE(cache.contains<keys::VolumeDisplacement>());
CHECK_FALSE(cache.contains<keys::Linearization>());
REQUIRE(cache.try_prepare(fixture.inputs(), dependencies::RequestedResults<keys::RadialInterpolation>{}));
const auto interpolation = cache.get<keys::RadialInterpolation>();
CHECK_FALSE(cache.try_prepare(fixture.inputs()));
CHECK_FALSE(cache.ready());
CHECK_THROWS_AS(cache.get<keys::RadialInterpolation>(), std::logic_error);
std::ranges::fill(fixture.coefficients, 0.01);
const auto recovered = cache.try_prepare(fixture.inputs());
REQUIRE(recovered);
CHECK(recovered->status<keys::RadialInterpolation>() == Status::reused);
CHECK(cache.get<keys::RadialInterpolation>() == interpolation);
}
TEST_CASE("Rejected trial coefficients hide old results and preserve accepted reference factors for recovery", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto interpolation = cache.get<keys::RadialInterpolation>();
const auto surface = cache.get<keys::SurfaceMap>();
const auto accepted_count = cache.preparation_count();
const std::vector<double> accepted_coefficients = fixture.coefficients;
SECTION("Nonfinite trial parameter")
{
fixture.coefficients.back() = std::numeric_limits<double>::quiet_NaN();
}
SECTION("Wrong coefficient count")
{
fixture.coefficients.push_back(0.01);
}
const auto failed = cache.try_prepare(fixture.inputs());
REQUIRE_FALSE(failed);
CHECK_FALSE(cache.ready());
CHECK(cache.preparation_count() == accepted_count);
CHECK_THROWS_AS(cache.get<keys::SurfaceDisplacement>(), std::logic_error);
CHECK_THROWS_AS(cache.get<keys::VolumeDisplacement>(), std::logic_error);
CHECK_THROWS_AS(cache.get<keys::Linearization>(), std::logic_error);
fixture.coefficients = accepted_coefficients;
const auto recovered = cache.try_prepare(fixture.inputs());
REQUIRE(recovered);
CHECK(recovered->status<keys::RadialInterpolation>() == Status::reused);
CHECK(recovered->status<keys::SurfaceMap>() == Status::reused);
CHECK(cache.get<keys::RadialInterpolation>() == interpolation);
CHECK(cache.get<keys::SurfaceMap>() == surface);
CHECK(cache.get<keys::Linearization>().parameters == accepted_coefficients);
}
TEST_CASE("Invalid extension options cannot leave a deformation cache publicly ready", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
const auto interpolation = cache.get<keys::RadialInterpolation>();
fixture.model.stellar_interior.radial_power = 0.5;
const auto failed = cache.try_prepare(fixture.inputs());
REQUIRE_FALSE(failed);
CHECK_FALSE(cache.ready());
CHECK_THROWS_AS(cache.get<keys::DomainMap>(), std::logic_error);
fixture.model.stellar_interior.radial_power = 2.0;
const auto recovered = cache.try_prepare(fixture.inputs());
REQUIRE(recovered);
CHECK(recovered->status<keys::RadialInterpolation>() == Status::reused);
CHECK(cache.get<keys::RadialInterpolation>() == interpolation);
}
TEST_CASE("Removing the surface boundary label invalidates graph preparation immediately", "[integration][deformation]")
{
GraphFixture fixture;
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs()));
std::vector<int> attributes(fixture.mesh.generated.mesh->GetNBE());
for (int boundary = 0; boundary < fixture.mesh.generated.mesh->GetNBE(); ++boundary)
{
attributes[boundary] = fixture.mesh.generated.mesh->GetBdrAttribute(boundary);
if (attributes[boundary] == fixture.mesh.surface_attribute)
{
fixture.mesh.generated.mesh->GetBdrElement(boundary)->SetAttribute(999);
}
}
const auto failed = cache.try_prepare(fixture.inputs());
REQUIRE_FALSE(failed);
CHECK_FALSE(cache.ready());
CHECK_THROWS_AS(cache.get<keys::VolumeDisplacement>(), std::logic_error);
for (int boundary = 0; boundary < fixture.mesh.generated.mesh->GetNBE(); ++boundary)
{
fixture.mesh.generated.mesh->GetBdrElement(boundary)->SetAttribute(attributes[boundary]);
}
REQUIRE(cache.try_prepare(fixture.inputs()));
}
TEST_CASE("Equal nonlinear displacements still refresh the deformation tangent and its accepted state", "[unit][deformation]")
{
std::vector<double> coefficients{1.0};
const auto surface = std::make_shared<const SquaredSurface>();
const auto extension = std::make_shared<const UnitExtension>();
const auto inputs = dependencies::make_dependency_context(
dependencies::own<keys::SurfaceMap>(surface),
dependencies::own<keys::VolumeExtension>(extension),
dependencies::borrow<keys::ShapeCoefficients>(coefficients));
auto cache = dependencies::make_computation_cache<deformation::DeformationFailure>(deformation::make_deformation_graph<SquaredSurface, UnitExtension>(), inputs);
REQUIRE(cache.try_prepare(inputs));
const auto accepted = cache.get<keys::Linearization>();
auto workspace = deformation::make_deformation_workspace(*accepted.map);
const std::array<double, 1> direction{1.0};
std::array<double, 1> action{};
REQUIRE(deformation::apply_jacobian(accepted, direction, action, workspace));
CHECK(action[0] == 2.0);
const std::vector<double> original = cache.get<keys::VolumeDisplacement>();
coefficients[0] = -1.0;
REQUIRE(deformation::apply_jacobian(accepted, direction, action, workspace));
CHECK(action[0] == 2.0);
const auto report = cache.try_prepare(inputs);
REQUIRE(report);
CHECK(report->status<keys::DomainMap>() == Status::reused);
CHECK(report->status<keys::SurfaceDisplacement>() == Status::computed);
CHECK(report->status<keys::VolumeDisplacement>() == Status::computed);
CHECK(report->status<keys::Linearization>() == Status::computed);
CHECK(cache.get<keys::VolumeDisplacement>() == original);
REQUIRE(deformation::apply_jacobian(cache.get<keys::Linearization>(), direction, action, workspace));
CHECK(action[0] == -2.0);
REQUIRE(deformation::apply_jacobian_transpose(cache.get<keys::Linearization>(), direction, action, workspace));
CHECK(action[0] == -2.0);
REQUIRE(deformation::apply_pullback_derivative(cache.get<keys::Linearization>(), direction, direction, action, workspace));
CHECK(action[0] == 2.0);
CHECK(accepted.parameters[0] == 1.0);
}
TEST_CASE("Requested shape metrics prepare only the reference surface and ignore trial shape and volume extension choices", "[integration][deformation]")
{
GraphFixture fixture;
fixture.coefficients[0] = std::numeric_limits<double>::quiet_NaN();
fixture.model.stellar_interior.radial_power = 0.5;
auto computations = std::tuple_cat(backend::make_deformation_computations<deformation::SphericalHarmonicSurface>(), std::tuple{backend::make_surface_metric_computation()});
/// Additional reference physics joins the same dependency graph without adding a preparation lifecycle.
auto graph = std::apply([](auto... nodes)
{
return dependencies::make_computation_graph(std::move(nodes)...);
},
std::move(computations));
auto cache = dependencies::make_computation_cache<deformation::DeformationFailure>(std::move(graph), fixture.inputs());
const auto requested = dependencies::RequestedResults<keys::SurfaceMetric>{};
const auto initial = cache.try_prepare(fixture.inputs(), requested);
REQUIRE(initial);
CHECK(initial->computed_count() == 4);
CHECK(initial->status<keys::SurfaceNodes>() == Status::computed);
CHECK(initial->status<keys::ReferenceSurface>() == Status::computed);
CHECK(initial->status<keys::SurfaceMap>() == Status::computed);
CHECK(initial->status<keys::SurfaceMetric>() == Status::computed);
CHECK(initial->status<keys::RadialInterpolation>() == Status::not_required);
CHECK(initial->status<keys::VolumeExtension>() == Status::not_required);
CHECK(initial->status<keys::VolumeDisplacement>() == Status::not_required);
CHECK_FALSE(cache.contains<keys::RadialInterpolation>());
CHECK_FALSE(cache.contains<keys::VolumeExtension>());
CHECK_FALSE(cache.contains<keys::DomainMap>());
CHECK_FALSE(cache.contains<keys::SurfaceDisplacement>());
CHECK_FALSE(cache.contains<keys::VolumeDisplacement>());
const std::vector<double> metric = cache.get<keys::SurfaceMetric>();
REQUIRE(metric.size() == fixture.coefficients.size());
CHECK(std::ranges::all_of(metric, [](double value)
{
return std::isfinite(value) && value > 0.0;
}));
fixture.coefficients[0] = 0.2;
const auto changed_coefficients = cache.try_prepare(fixture.inputs(), requested);
REQUIRE(changed_coefficients);
CHECK(changed_coefficients->computed_count() == 0);
CHECK(changed_coefficients->status<keys::SurfaceMetric>() == Status::reused);
fixture.model.stellar_interior.radial_power = 3.0;
const auto changed_extension = cache.try_prepare(fixture.inputs(), requested);
REQUIRE(changed_extension);
CHECK(changed_extension->computed_count() == 0);
CHECK(changed_extension->status<keys::SurfaceMetric>() == Status::reused);
CHECK(cache.get<keys::SurfaceMetric>() == metric);
fixture.mesh.generated.reference_mesh->NodesUpdated();
const auto changed_logical_reference = cache.try_prepare(fixture.inputs(), requested);
REQUIRE(changed_logical_reference);
CHECK(changed_logical_reference->computed_count() == 0);
CHECK(changed_logical_reference->status<keys::SurfaceMetric>() == Status::reused);
fixture.model.surface.reference_center[0] = 0.02;
const auto changed_surface = cache.try_prepare(fixture.inputs(), requested);
REQUIRE(changed_surface);
CHECK(changed_surface->status<keys::ReferenceSurface>() == Status::reused);
CHECK(changed_surface->status<keys::SurfaceMap>() == Status::computed);
CHECK(changed_surface->status<keys::SurfaceMetric>() == Status::computed);
::mfem::VectorFunctionCoefficient stretch(3, [](const ::mfem::Vector& input, ::mfem::Vector& output)
{
output = input;
output[2] *= 0.85;
});
fixture.mesh.generated.mesh->Transform(stretch);
fixture.mesh.generated.mesh->NodesUpdated();
const auto changed_reference = cache.try_prepare(fixture.inputs(), requested);
REQUIRE(changed_reference);
CHECK(changed_reference->status<keys::ReferenceSurface>() == Status::computed);
CHECK(changed_reference->status<keys::SurfaceMap>() == Status::computed);
CHECK(changed_reference->status<keys::SurfaceMetric>() == Status::computed);
const auto reference = backend::sample_reference_surface(fixture.mesh.scalar_space, fixture.mesh.surface_attribute);
REQUIRE(reference);
const auto embedding = deformation::build_surface_embedding(fixture.model.surface, reference->positions);
REQUIRE(embedding);
const auto fresh_metric = backend::surface_parameter_mass_diagonal(fixture.mesh.scalar_space, fixture.mesh.surface_attribute, *reference, *embedding);
REQUIRE(fresh_metric);
CHECK(cache.get<keys::SurfaceMetric>() == *fresh_metric);
CHECK(cache.get<keys::SurfaceMetric>() != metric);
CHECK_FALSE(cache.contains<keys::RadialInterpolation>());
CHECK_FALSE(cache.contains<keys::VolumeExtension>());
CHECK_FALSE(cache.contains<keys::VolumeDisplacement>());
}
TEST_CASE("Packed shape views track only their values and retain independent linearizations", "[integration][deformation][shape_view]") {
GraphFixture fixture;
constexpr std::size_t offset = 4;
std::vector<double> packed(offset + fixture.coefficients.size() + 3, 0.0);
std::ranges::copy(fixture.coefficients, packed.begin() + offset);
const auto inputs = backend::make_deformation_inputs(fixture.model, fixture.discretization,
std::span<const double>(packed).subspan(offset, fixture.coefficients.size()));
auto cache = dependencies::make_computation_cache<deformation::DeformationFailure>(
backend::make_deformation_graph<deformation::SphericalHarmonicSurface>(), inputs);
REQUIRE(cache.try_prepare(inputs));
const auto frozen = cache.get<keys::Linearization>();
CHECK(frozen.parameters == fixture.coefficients);
packed.front() = 2.0;
REQUIRE(cache.try_prepare(inputs));
CHECK(cache.report().computed_count() == 0);
packed[offset] += 0.02;
REQUIRE(cache.try_prepare(inputs));
CHECK(cache.report().status<keys::ReferenceSurface>() == Status::reused);
CHECK(cache.report().status<keys::RadialInterpolation>() == Status::reused);
CHECK(cache.report().status<keys::SurfaceDisplacement>() == Status::computed);
CHECK(cache.report().status<keys::Linearization>() == Status::computed);
CHECK(cache.get<keys::Linearization>().parameters.front() == packed[offset]);
CHECK(frozen.parameters == fixture.coefficients);
std::vector<double> equal_coefficients(packed.begin() + offset, packed.end() - 3);
const auto equal_inputs = backend::make_deformation_inputs(fixture.model, fixture.discretization,
std::span<const double>(equal_coefficients));
REQUIRE(cache.try_prepare(equal_inputs));
CHECK(cache.report().computed_count() == 0);
equal_coefficients.front() = std::numeric_limits<double>::quiet_NaN();
REQUIRE_FALSE(cache.try_prepare(equal_inputs));
CHECK_FALSE(cache.contains<keys::Linearization>());
CHECK(frozen.parameters == fixture.coefficients);
REQUIRE(cache.try_prepare(inputs));
const auto short_inputs = backend::make_deformation_inputs(fixture.model, fixture.discretization,
std::span<const double>(packed).subspan(offset, fixture.coefficients.size() - 1));
REQUIRE_FALSE(cache.try_prepare(short_inputs));
CHECK_FALSE(cache.contains<keys::Linearization>());
REQUIRE(cache.try_prepare(inputs));
CHECK(cache.get<keys::Linearization>().parameters.front() == packed[offset]);
}
TEST_CASE("Borrowed shape vectors retain current storage after resizing", "[integration][deformation][shape_view]") {
GraphFixture fixture;
const auto inputs = fixture.inputs();
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(inputs));
const std::vector<double> original = fixture.coefficients;
fixture.coefficients.resize(fixture.coefficients.capacity() + 1, 0.0);
REQUIRE_FALSE(cache.try_prepare(inputs));
CHECK_FALSE(cache.contains<keys::Linearization>());
fixture.coefficients = original;
REQUIRE(cache.try_prepare(inputs));
CHECK(cache.get<keys::Linearization>().parameters == original);
}