#include #include #include #include #include #include #include #include #include #include #include #include #include #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; /** @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 coefficients = std::vector(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(backend::make_deformation_graph(), 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 state, std::span 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 state, std::span direction, std::span 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 state, std::span dual, std::span 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, std::span direction, std::span dual, std::span 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 input, std::span output, Workspace = {}) const { output[0] = input[0]; return {}; } /// Identity force transfer preserves virtual work exactly. [[nodiscard]] deformation::DeformationStatus apply_transpose(std::span input, std::span 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(); const auto interpolation = cache.get(); const auto map = cache.get(); const std::vector original = cache.get(); const std::vector 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() == Status::reused); CHECK(reused->status() == Status::reused); CHECK(reused->status() == Status::reused); CHECK(reused->status() == Status::reused); CHECK(cache.get() == surface); CHECK(cache.get() == interpolation); CHECK(cache.get() == map); CHECK(cache.get() == 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(); const auto interpolation = cache.get(); const auto map = cache.get(); const std::vector original = cache.get(); fixture.coefficients[0] += 0.03; const auto report = cache.try_prepare(fixture.inputs()); REQUIRE(report); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(cache.get() == reference); CHECK(cache.get() == interpolation); CHECK(cache.get() == map); CHECK(cache.get() != 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(); const auto surface = cache.get(); const std::vector original = cache.get(); fixture.model.stellar_interior.radial_power = 3.0; const auto report = cache.try_prepare(fixture.inputs()); REQUIRE(report); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(cache.get() == interpolation); CHECK(cache.get() == surface); CHECK(cache.get()->interpolation() == interpolation); CHECK(cache.get() != 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(); const auto extension = cache.get(); const std::vector original = cache.get(); fixture.model.surface.reference_center[0] = 0.02; const auto report = cache.try_prepare(fixture.inputs()); REQUIRE(report); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(cache.get() == interpolation); CHECK(cache.get() == extension); CHECK(cache.get() != 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(); const std::vector original = cache.get(); auto workspace = deformation::make_deformation_workspace(*accepted.map); std::vector 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().parameters == accepted.parameters); CHECK(cache.get() == original); REQUIRE(deformation::apply_jacobian(accepted, accepted.parameters, action, workspace)); CHECK(action == original); REQUIRE(cache.try_prepare(inputs)); CHECK(cache.get().parameters == fixture.coefficients); CHECK(accepted.parameters != fixture.coefficients); CHECK(cache.get() != 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(); const auto interpolation = cache.get(); const std::vector original = cache.get(); 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() == Status::reused); CHECK(report->status() == (physical ? Status::computed : Status::reused)); CHECK(report->status() == (physical ? Status::computed : Status::reused)); CHECK(report->status() == (physical ? Status::reused : Status::computed)); CHECK(report->status() == (physical ? Status::reused : Status::computed)); CHECK(report->status() == Status::computed); CHECK((cache.get() != reference) == physical); CHECK((cache.get() == interpolation) == physical); CHECK(cache.get() == 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(); const auto interpolation = cache.get(); const std::vector original = cache.get(); ::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() == Status::reused); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::reused); CHECK(cache.get()->positions != reference->positions); CHECK(cache.get() == interpolation); CHECK(cache.get() != original); auto fresh = make_cache(fixture); REQUIRE(fresh.try_prepare(fixture.inputs())); CHECK(cache.get()->positions == fresh.get()->positions); CHECK(cache.get() == fresh.get()); } 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(); const auto interpolation = cache.get(); const std::vector original = cache.get(); ::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() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::computed); CHECK(cache.get() == reference); CHECK(cache.get()->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()->data().logical_radii == fresh.get()->data().logical_radii); CHECK(cache.get() == fresh.get()); for (std::size_t index = 0; index < original.size(); ++index) { CHECK(cache.get()[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(); const std::vector original = cache.get(); const int core = Schema::domain_id(); const int envelope = Schema::domain_id(); 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() == Status::reused); CHECK(report->status() == Status::reused); CHECK(report->status() == Status::computed); CHECK(cache.get() != interpolation); CHECK(cache.get() == 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(); const auto interpolation = cache.get(); 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() == Status::computed); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(cache.get() != reference); CHECK(cache.get() != 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::quiet_NaN()); auto cache = make_cache(fixture); const auto reference_report = cache.try_prepare(fixture.inputs(), dependencies::RequestedResults{}); REQUIRE(reference_report); CHECK(reference_report->computed_count() == 2); CHECK(cache.contains()); CHECK_FALSE(cache.contains()); CHECK_FALSE(cache.contains()); CHECK_FALSE(cache.contains()); REQUIRE(cache.try_prepare(fixture.inputs(), dependencies::RequestedResults{})); const auto interpolation = cache.get(); CHECK_FALSE(cache.try_prepare(fixture.inputs())); CHECK_FALSE(cache.ready()); CHECK_THROWS_AS(cache.get(), std::logic_error); std::ranges::fill(fixture.coefficients, 0.01); const auto recovered = cache.try_prepare(fixture.inputs()); REQUIRE(recovered); CHECK(recovered->status() == Status::reused); CHECK(cache.get() == 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(); const auto surface = cache.get(); const auto accepted_count = cache.preparation_count(); const std::vector accepted_coefficients = fixture.coefficients; SECTION("Nonfinite trial parameter") { fixture.coefficients.back() = std::numeric_limits::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(), std::logic_error); CHECK_THROWS_AS(cache.get(), std::logic_error); CHECK_THROWS_AS(cache.get(), std::logic_error); fixture.coefficients = accepted_coefficients; const auto recovered = cache.try_prepare(fixture.inputs()); REQUIRE(recovered); CHECK(recovered->status() == Status::reused); CHECK(recovered->status() == Status::reused); CHECK(cache.get() == interpolation); CHECK(cache.get() == surface); CHECK(cache.get().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(); 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(), std::logic_error); fixture.model.stellar_interior.radial_power = 2.0; const auto recovered = cache.try_prepare(fixture.inputs()); REQUIRE(recovered); CHECK(recovered->status() == Status::reused); CHECK(cache.get() == 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 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(), 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 coefficients{1.0}; const auto surface = std::make_shared(); const auto extension = std::make_shared(); const auto inputs = dependencies::make_dependency_context( dependencies::own(surface), dependencies::own(extension), dependencies::borrow(coefficients)); auto cache = dependencies::make_computation_cache(deformation::make_deformation_graph(), inputs); REQUIRE(cache.try_prepare(inputs)); const auto accepted = cache.get(); auto workspace = deformation::make_deformation_workspace(*accepted.map); const std::array direction{1.0}; std::array action{}; REQUIRE(deformation::apply_jacobian(accepted, direction, action, workspace)); CHECK(action[0] == 2.0); const std::vector original = cache.get(); 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() == Status::reused); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(report->status() == Status::computed); CHECK(cache.get() == original); REQUIRE(deformation::apply_jacobian(cache.get(), direction, action, workspace)); CHECK(action[0] == -2.0); REQUIRE(deformation::apply_jacobian_transpose(cache.get(), direction, action, workspace)); CHECK(action[0] == -2.0); REQUIRE(deformation::apply_pullback_derivative(cache.get(), 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::quiet_NaN(); fixture.model.stellar_interior.radial_power = 0.5; auto computations = std::tuple_cat(backend::make_deformation_computations(), 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(std::move(graph), fixture.inputs()); const auto requested = dependencies::RequestedResults{}; const auto initial = cache.try_prepare(fixture.inputs(), requested); REQUIRE(initial); CHECK(initial->computed_count() == 4); CHECK(initial->status() == Status::computed); CHECK(initial->status() == Status::computed); CHECK(initial->status() == Status::computed); CHECK(initial->status() == Status::computed); CHECK(initial->status() == Status::not_required); CHECK(initial->status() == Status::not_required); CHECK(initial->status() == Status::not_required); CHECK_FALSE(cache.contains()); CHECK_FALSE(cache.contains()); CHECK_FALSE(cache.contains()); CHECK_FALSE(cache.contains()); CHECK_FALSE(cache.contains()); const std::vector metric = cache.get(); 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() == 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() == Status::reused); CHECK(cache.get() == 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() == 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() == Status::reused); CHECK(changed_surface->status() == Status::computed); CHECK(changed_surface->status() == 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() == Status::computed); CHECK(changed_reference->status() == Status::computed); CHECK(changed_reference->status() == 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() == *fresh_metric); CHECK(cache.get() != metric); CHECK_FALSE(cache.contains()); CHECK_FALSE(cache.contains()); CHECK_FALSE(cache.contains()); } 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 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(packed).subspan(offset, fixture.coefficients.size())); auto cache = dependencies::make_computation_cache( backend::make_deformation_graph(), inputs); REQUIRE(cache.try_prepare(inputs)); const auto frozen = cache.get(); 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() == Status::reused); CHECK(cache.report().status() == Status::reused); CHECK(cache.report().status() == Status::computed); CHECK(cache.report().status() == Status::computed); CHECK(cache.get().parameters.front() == packed[offset]); CHECK(frozen.parameters == fixture.coefficients); std::vector equal_coefficients(packed.begin() + offset, packed.end() - 3); const auto equal_inputs = backend::make_deformation_inputs(fixture.model, fixture.discretization, std::span(equal_coefficients)); REQUIRE(cache.try_prepare(equal_inputs)); CHECK(cache.report().computed_count() == 0); equal_coefficients.front() = std::numeric_limits::quiet_NaN(); REQUIRE_FALSE(cache.try_prepare(equal_inputs)); CHECK_FALSE(cache.contains()); CHECK(frozen.parameters == fixture.coefficients); REQUIRE(cache.try_prepare(inputs)); const auto short_inputs = backend::make_deformation_inputs(fixture.model, fixture.discretization, std::span(packed).subspan(offset, fixture.coefficients.size() - 1)); REQUIRE_FALSE(cache.try_prepare(short_inputs)); CHECK_FALSE(cache.contains()); REQUIRE(cache.try_prepare(inputs)); CHECK(cache.get().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 original = fixture.coefficients; fixture.coefficients.resize(fixture.coefficients.capacity() + 1, 0.0); REQUIRE_FALSE(cache.try_prepare(inputs)); CHECK_FALSE(cache.contains()); fixture.coefficients = original; REQUIRE(cache.try_prepare(inputs)); CHECK(cache.get().parameters == original); }