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.
707 lines
35 KiB
C++
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);
|
|
}
|