Files
MeanField_SERiF/tests/deformation/computations.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

707 lines
35 KiB
C++

#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);
}