test(tests): added test suite

The test suite is extensive. Note that LLMs were used to aid in the development of the test suite. All LLM generated code was manually reviewed.
This commit is contained in:
2026-11-09 13:53:17 -05:00
parent 74e55a6859
commit cb20faeb3d
202 changed files with 35505 additions and 953 deletions

View File

@@ -0,0 +1,556 @@
// The nonlinear surface and bilinear extension retain MeanField's independent chain-rule fixture.
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <memory>
#include <numeric>
#include <span>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "serif/geometry/deformation/composition.hpp"
#include "serif/geometry/deformation/linearization.hpp"
namespace
{
namespace deformation = serif::geometry::deformation;
namespace spaces = deformation::spaces;
using Catch::Matchers::WithinAbs;
/** @brief Reject fixture misuse as an error value so failure recovery exercises the production composition. */
[[nodiscard]] deformation::DeformationStatus validate_output(const std::span<double> output, const std::size_t expected_size)
{
if (output.size() != expected_size)
{
return std::unexpected(deformation::DeformationFailure{.reason = deformation::DeformationFailureReason::invalid_size, .message = "The analytic deformation fixture received an incompatible output."});
}
return {};
}
/** @brief Provide independently known quadratic maps with two inputs and a variable number of outputs. */
template <typename Input, typename Output, std::size_t OutputSize>
struct QuadraticMap
{
using InputSpace = Input;
using OutputSpace = Output;
static constexpr bool is_linear = false;
struct Workspace
{
std::size_t actions{0};
};
// Each row defines a*x + b*y + c*x*x + d*x*y + e*y*y.
std::array<std::array<double, 5>, OutputSize> coefficients;
/** @brief Keep the analytic parameter space small enough to inspect by hand. */
[[nodiscard]] std::size_t input_size() const noexcept
{
return 2;
}
/** @brief Allow the extension to have more outputs than surface coordinates. */
[[nodiscard]] std::size_t output_size() const noexcept
{
return OutputSize;
}
/** @brief Make caller-owned action state visible in recovery tests. */
[[nodiscard]] Workspace make_workspace() const
{
return {};
}
/** @brief Evaluate the independent quadratic definition, including both pure and mixed terms. */
[[nodiscard]] deformation::DeformationStatus build_displacement(const std::span<const double> state, const std::span<double> output, Workspace& workspace) const
{
if (const auto status = deformation::validate_deformation_values(state, input_size()); !status)
{
return status;
}
if (const auto status = validate_output(output, output_size()); !status)
{
return status;
}
++workspace.actions;
for (std::size_t row = 0; row < OutputSize; ++row)
{
const std::array<double, 5>& values = coefficients[row];
output[row] = values[0] * state[0] + values[1] * state[1] + values[2] * state[0] * state[0] + values[3] * state[0] * state[1] + values[4] * state[1] * state[1];
}
return deformation::validate_deformation_values(output, output_size());
}
/** @brief Apply the analytic gradient of each quadratic row. */
[[nodiscard]] deformation::DeformationStatus apply_jacobian(const std::span<const double> state, const std::span<const double> direction, const std::span<double> output, Workspace& workspace) const
{
if (const auto status = deformation::validate_deformation_values(state, input_size()); !status)
{
return status;
}
if (const auto status = deformation::validate_deformation_values(direction, input_size()); !status)
{
return status;
}
if (const auto status = validate_output(output, output_size()); !status)
{
return status;
}
++workspace.actions;
for (std::size_t row = 0; row < OutputSize; ++row)
{
const std::array<double, 2> row_gradient = gradient(row, state);
output[row] = row_gradient[0] * direction[0] + row_gradient[1] * direction[1];
}
return deformation::validate_deformation_values(output, output_size());
}
/** @brief Reverse the exact same mathematical derivative without constructing a matrix. */
[[nodiscard]] deformation::DeformationStatus apply_jacobian_transpose(const std::span<const double> state, const std::span<const double> dual, const std::span<double> output, Workspace& workspace) const
{
if (const auto status = deformation::validate_deformation_values(state, input_size()); !status)
{
return status;
}
if (const auto status = deformation::validate_deformation_values(dual, output_size()); !status)
{
return status;
}
if (const auto status = validate_output(output, input_size()); !status)
{
return status;
}
++workspace.actions;
std::ranges::fill(output, 0.0);
for (std::size_t row = 0; row < OutputSize; ++row)
{
const std::array<double, 2> row_gradient = gradient(row, state);
output[0] += row_gradient[0] * dual[row];
output[1] += row_gradient[1] * dual[row];
}
return deformation::validate_deformation_values(output, input_size());
}
/** @brief Contract the constant quadratic Hessians with direction and fixed output dual. */
[[nodiscard]] deformation::DeformationStatus apply_pullback_derivative(const std::span<const double> state, const std::span<const double> direction, const std::span<const double> dual, const std::span<double> output, Workspace& workspace) const
{
if (const auto status = deformation::validate_deformation_values(state, input_size()); !status)
{
return status;
}
if (const auto status = deformation::validate_deformation_values(direction, input_size()); !status)
{
return status;
}
if (const auto status = deformation::validate_deformation_values(dual, output_size()); !status)
{
return status;
}
if (const auto status = validate_output(output, input_size()); !status)
{
return status;
}
++workspace.actions;
std::ranges::fill(output, 0.0);
for (std::size_t row = 0; row < OutputSize; ++row)
{
const std::array<double, 5>& values = coefficients[row];
output[0] += (2.0 * values[2] * direction[0] + values[3] * direction[1]) * dual[row];
output[1] += (values[3] * direction[0] + 2.0 * values[4] * direction[1]) * dual[row];
}
return deformation::validate_deformation_values(output, input_size());
}
/** @brief Keep the fixture's analytic row derivative in one easily checked expression. */
[[nodiscard]] std::array<double, 2> gradient(const std::size_t row, const std::span<const double> state) const
{
const std::array<double, 5>& values = coefficients[row];
return {values[0] + 2.0 * values[2] * state[0] + values[3] * state[1], values[1] + values[3] * state[0] + 2.0 * values[4] * state[1]};
}
};
using NonlinearSurface = QuadraticMap<spaces::ShapeParameters, spaces::SurfaceDisplacement, 2>;
using NonlinearExtension = QuadraticMap<spaces::SurfaceDisplacement, spaces::VolumeDisplacement, 3>;
/** @brief Retain MeanField's surface definition (x*x + y, x*y). */
[[nodiscard]] std::shared_ptr<const NonlinearSurface> nonlinear_surface()
{
return std::make_shared<const NonlinearSurface>(NonlinearSurface{.coefficients = {{{0.0, 1.0, 1.0, 0.0, 0.0}, {0.0, 0.0, 0.0, 1.0, 0.0}}}});
}
/** @brief Retain the bilinear extension with distinct row coefficients from MeanField's nonlinear fixture. */
[[nodiscard]] std::shared_ptr<const NonlinearExtension> nonlinear_extension()
{
return std::make_shared<const NonlinearExtension>(NonlinearExtension{.coefficients = {{{0.01, -0.02, 0.0, 0.0005, 0.0}, {0.011, -0.018, 0.0, 0.001, 0.0}, {0.012, -0.016, 0.0, 0.0015, 0.0}}}});
}
/** @brief Supply the minimal linear-map contract with an independently known nonsymmetric matrix. */
template <typename Input, typename Output>
struct LinearMap
{
using InputSpace = Input;
using OutputSpace = Output;
using Workspace = deformation::EmptyDeformationWorkspace;
static constexpr bool is_linear = true;
std::size_t input_count{2};
/** @brief Permit an intentionally incompatible count for factory validation. */
[[nodiscard]] std::size_t input_size() const noexcept
{
return input_count;
}
/** @brief Fix two rows for the explicit matrix used in the test. */
[[nodiscard]] std::size_t output_size() const noexcept
{
return 2;
}
/** @brief Apply [[2, 1], [-1, 3]], with no derivative-specific implementation. */
[[nodiscard]] deformation::DeformationStatus apply(const std::span<const double> input, const std::span<double> output, Workspace = {}) const
{
if (const auto status = deformation::validate_deformation_values(input, input_size()); !status)
{
return status;
}
if (const auto status = validate_output(output, output_size()); !status)
{
return status;
}
output[0] = 2.0 * input[0] + input[1];
output[1] = -input[0] + 3.0 * input[1];
return deformation::validate_deformation_values(output, output_size());
}
/** @brief Use an analytic transpose to test the generic linear derivative branch. */
[[nodiscard]] deformation::DeformationStatus apply_transpose(const std::span<const double> input, const std::span<double> output, Workspace = {}) const
{
if (const auto status = deformation::validate_deformation_values(input, output_size()); !status)
{
return status;
}
if (const auto status = validate_output(output, input_size()); !status)
{
return status;
}
output[0] = 2.0 * input[0] - input[1];
output[1] = input[0] + 3.0 * input[1];
return deformation::validate_deformation_values(output, input_size());
}
};
using LinearSurface = LinearMap<spaces::ShapeParameters, spaces::SurfaceDisplacement>;
using LinearExtension = LinearMap<spaces::SurfaceDisplacement, spaces::VolumeDisplacement>;
/** @brief Build centered differences without borrowing any production derivative calculation. */
[[nodiscard]] std::array<double, 2> perturb(const std::array<double, 2>& state, const std::array<double, 2>& direction, const double step)
{
return {state[0] + step * direction[0], state[1] + step * direction[1]};
}
/** @brief Compare work without depending on a backend vector representation. */
[[nodiscard]] double inner_product(const std::span<const double> first, const std::span<const double> second)
{
REQUIRE(first.size() == second.size());
return std::inner_product(first.begin(), first.end(), second.begin(), 0.0);
}
/** @brief Give a nonlinear physical residual whose derivative contains cross-component couplings. */
[[nodiscard]] std::array<double, 3> volume_force(const std::span<const double> displacement)
{
return {1.0 + displacement[0] * displacement[0] + 0.5 * displacement[1], -0.2 + displacement[1] * displacement[1] - 0.3 * displacement[2], 0.7 + displacement[2] * displacement[2] + 0.1 * displacement[0]};
}
/** @brief Verify the surviving curvature term when either factor is linear. */
template <deformation::DeformationMap Map>
void check_mixed_pullback(const Map& map)
{
auto workspace = deformation::make_deformation_workspace(map);
const std::array state{0.37, -0.21};
const std::array direction{-0.42, 0.63};
constexpr double step = 2.0e-6;
const std::array plus = perturb(state, direction, step);
const std::array minus = perturb(state, direction, -step);
const std::vector<double> dual(map.output_size(), 0.7);
std::array<double, 2> derivative{};
std::array<double, 2> plus_transpose{};
std::array<double, 2> minus_transpose{};
REQUIRE(deformation::apply_pullback_derivative(map, state, direction, dual, derivative, workspace));
REQUIRE(deformation::apply_jacobian_transpose(map, plus, dual, plus_transpose, workspace));
REQUIRE(deformation::apply_jacobian_transpose(map, minus, dual, minus_transpose, workspace));
for (std::size_t parameter = 0; parameter < derivative.size(); ++parameter)
{
CHECK_THAT(derivative[parameter], WithinAbs((plus_transpose[parameter] - minus_transpose[parameter]) / (2.0 * step), 2.0e-10));
}
CHECK(std::abs(derivative[0]) + std::abs(derivative[1]) > 1.0e-5);
}
} // namespace
TEST_CASE("Nonlinear domain deformation preserves the complete chain rule and virtual work", "[unit][deformation][composition]")
{
const auto composed = deformation::compose_deformation(nonlinear_surface(), nonlinear_extension());
REQUIRE(composed);
auto workspace = deformation::make_deformation_workspace(*composed);
const std::array state{0.37, -0.21};
const std::array direction{-0.42, 0.63};
const std::array dual{0.9, -0.35, 0.24};
constexpr double step = 2.0e-6;
const std::array plus = perturb(state, direction, step);
const std::array minus = perturb(state, direction, -step);
const auto plus_displacement = deformation::evaluate_displacement(*composed, plus);
const auto minus_displacement = deformation::evaluate_displacement(*composed, minus);
REQUIRE(plus_displacement);
REQUIRE(minus_displacement);
std::array<double, 3> tangent{};
std::array<double, 2> transpose{};
std::array<double, 2> pullback{};
std::array<double, 2> plus_transpose{};
std::array<double, 2> minus_transpose{};
REQUIRE(deformation::apply_jacobian(*composed, state, direction, tangent, workspace));
REQUIRE(deformation::apply_jacobian_transpose(*composed, state, dual, transpose, workspace));
REQUIRE(deformation::apply_pullback_derivative(*composed, state, direction, dual, pullback, workspace));
REQUIRE(deformation::apply_jacobian_transpose(*composed, plus, dual, plus_transpose, workspace));
REQUIRE(deformation::apply_jacobian_transpose(*composed, minus, dual, minus_transpose, workspace));
for (std::size_t row = 0; row < tangent.size(); ++row)
{
CHECK_THAT(tangent[row], WithinAbs(((*plus_displacement)[row] - (*minus_displacement)[row]) / (2.0 * step), 2.0e-12));
}
CHECK_THAT(inner_product(tangent, dual), WithinAbs(inner_product(direction, transpose), 2.0e-15));
for (std::size_t parameter = 0; parameter < pullback.size(); ++parameter)
{
CHECK_THAT(pullback[parameter], WithinAbs((plus_transpose[parameter] - minus_transpose[parameter]) / (2.0 * step), 3.0e-12));
}
CHECK(std::abs(pullback[0]) > 1.0e-3);
CHECK(std::abs(pullback[1]) > 1.0e-3);
}
TEST_CASE("Nonlinear shape force derivative includes geometric and physical residual contributions", "[unit][deformation][composition]")
{
const auto composed = deformation::compose_deformation(nonlinear_surface(), nonlinear_extension());
REQUIRE(composed);
auto workspace = deformation::make_deformation_workspace(*composed);
const std::array state{0.37, -0.21};
const std::array direction{-0.42, 0.63};
const auto displacement = deformation::evaluate_displacement(*composed, state);
REQUIRE(displacement);
const std::array force = volume_force(*displacement);
std::array<double, 3> displacement_direction{};
REQUIRE(deformation::apply_jacobian(*composed, state, direction, displacement_direction, workspace));
const std::array force_direction{
2.0 * (*displacement)[0] * displacement_direction[0] + 0.5 * displacement_direction[1],
2.0 * (*displacement)[1] * displacement_direction[1] - 0.3 * displacement_direction[2],
2.0 * (*displacement)[2] * displacement_direction[2] + 0.1 * displacement_direction[0]};
std::array<double, 2> geometric_contribution{};
std::array<double, 2> physical_contribution{};
REQUIRE(deformation::apply_pullback_derivative(*composed, state, direction, force, geometric_contribution, workspace));
REQUIRE(deformation::apply_jacobian_transpose(*composed, state, force_direction, physical_contribution, workspace));
constexpr double step = 2.0e-6;
const std::array plus = perturb(state, direction, step);
const std::array minus = perturb(state, direction, -step);
const auto plus_displacement = deformation::evaluate_displacement(*composed, plus);
const auto minus_displacement = deformation::evaluate_displacement(*composed, minus);
REQUIRE(plus_displacement);
REQUIRE(minus_displacement);
std::array<double, 2> plus_force{};
std::array<double, 2> minus_force{};
REQUIRE(deformation::apply_jacobian_transpose(*composed, plus, volume_force(*plus_displacement), plus_force, workspace));
REQUIRE(deformation::apply_jacobian_transpose(*composed, minus, volume_force(*minus_displacement), minus_force, workspace));
for (std::size_t parameter = 0; parameter < plus_force.size(); ++parameter)
{
const double centered_difference = (plus_force[parameter] - minus_force[parameter]) / (2.0 * step);
CHECK_THAT(geometric_contribution[parameter] + physical_contribution[parameter], WithinAbs(centered_difference, 3.0e-12));
CHECK(std::abs(geometric_contribution[parameter]) > 1.0e-3);
CHECK(std::abs(physical_contribution[parameter]) > 1.0e-7);
}
}
TEST_CASE("Linear composition needs only forward and transpose actions and recovers after rejected scratch", "[unit][deformation][composition]")
{
const auto composed = deformation::compose_deformation(std::make_shared<const LinearSurface>(), std::make_shared<const LinearExtension>());
REQUIRE(composed);
auto workspace = deformation::make_deformation_workspace(*composed);
const std::array state{0.37, -0.21};
const std::array direction{-0.42, 0.63};
const std::array dual{0.9, -0.35};
std::array<double, 2> output{};
REQUIRE(deformation::build_displacement(*composed, state, output, workspace));
CHECK_THAT(output[0], WithinAbs(3.0 * state[0] + 5.0 * state[1], 1.0e-15));
CHECK_THAT(output[1], WithinAbs(-5.0 * state[0] + 8.0 * state[1], 1.0e-15));
REQUIRE(deformation::apply_pullback_derivative(*composed, state, direction, dual, output, workspace));
CHECK(output == std::array<double, 2>{0.0, 0.0});
const std::array enormous{std::numeric_limits<double>::max(), std::numeric_limits<double>::max()};
CHECK_FALSE(deformation::build_displacement(*composed, enormous, output, workspace));
REQUIRE(deformation::apply_jacobian_transpose(*composed, state, dual, output, workspace));
CHECK_THAT(output[0], WithinAbs(3.0 * dual[0] - 5.0 * dual[1], 1.0e-15));
CHECK_THAT(output[1], WithinAbs(5.0 * dual[0] + 8.0 * dual[1], 1.0e-15));
}
TEST_CASE("Mixed linear and nonlinear deformation factors retain their curvature", "[unit][deformation][composition]")
{
const auto nonlinear_surface_only = deformation::compose_deformation(nonlinear_surface(), std::make_shared<const LinearExtension>());
const auto nonlinear_extension_only = deformation::compose_deformation(std::make_shared<const LinearSurface>(), nonlinear_extension());
REQUIRE(nonlinear_surface_only);
REQUIRE(nonlinear_extension_only);
check_mixed_pullback(*nonlinear_surface_only);
check_mixed_pullback(*nonlinear_extension_only);
}
TEST_CASE("Composition rejects incompatible factors and invalid actions without preventing workspace reuse", "[unit][deformation][composition]")
{
const auto surface = nonlinear_surface();
CHECK_FALSE(deformation::compose_deformation(surface, std::make_shared<const LinearExtension>(LinearExtension{.input_count = 3})));
CHECK_FALSE(deformation::compose_deformation(std::shared_ptr<const NonlinearSurface>{}, nonlinear_extension()));
CHECK_FALSE(deformation::compose_deformation(surface, std::shared_ptr<const NonlinearExtension>{}));
const auto composed = deformation::compose_deformation(surface, nonlinear_extension());
REQUIRE(composed);
auto workspace = deformation::make_deformation_workspace(*composed);
const std::array state{0.37, -0.21};
const std::array direction{-0.42, 0.63};
const std::array dual{0.9, -0.35, 0.24};
const std::array invalid{std::numeric_limits<double>::quiet_NaN(), 0.0};
std::array<double, 3> volume{};
std::array<double, 2> output{};
std::array<double, 1> wrong{};
CHECK_FALSE(deformation::build_displacement(*composed, wrong, volume, workspace));
CHECK_FALSE(deformation::apply_jacobian(*composed, state, wrong, volume, workspace));
CHECK_FALSE(deformation::apply_jacobian_transpose(*composed, state, wrong, output, workspace));
CHECK_FALSE(deformation::apply_pullback_derivative(*composed, state, direction, dual, wrong, workspace));
CHECK_FALSE(deformation::apply_pullback_derivative(*composed, invalid, direction, dual, output, workspace));
REQUIRE(deformation::build_displacement(*composed, state, volume, workspace));
REQUIRE(deformation::apply_pullback_derivative(*composed, state, direction, dual, output, workspace));
CHECK(workspace.surface_workspace.actions > 0);
CHECK(workspace.extension_workspace.actions > 0);
}
TEST_CASE("A deformation linearization retains its accepted shape independently of subsequent trials", "[unit][deformation][composition]")
{
const auto surface = nonlinear_surface();
std::vector<double> trial{0.37, -0.21};
const deformation::DeformationLinearization<NonlinearSurface> accepted{.map = surface, .parameters = trial};
auto workspace = deformation::make_deformation_workspace(*surface);
const std::array direction{-0.42, 0.63};
const std::array dual{0.9, -0.35};
std::array<double, 2> tangent{};
std::array<double, 2> transpose{};
std::array<double, 2> pullback{};
trial = {8.0, 9.0};
REQUIRE(deformation::apply_jacobian(accepted, direction, tangent, workspace));
REQUIRE(deformation::apply_jacobian_transpose(accepted, dual, transpose, workspace));
REQUIRE(deformation::apply_pullback_derivative(accepted, direction, dual, pullback, workspace));
CHECK_THAT(tangent[0], WithinAbs(2.0 * 0.37 * direction[0] + direction[1], 1.0e-15));
CHECK_THAT(tangent[1], WithinAbs(-0.21 * direction[0] + 0.37 * direction[1], 1.0e-15));
CHECK_THAT(transpose[0], WithinAbs(2.0 * 0.37 * dual[0] - 0.21 * dual[1], 1.0e-15));
CHECK_THAT(transpose[1], WithinAbs(dual[0] + 0.37 * dual[1], 1.0e-15));
CHECK_THAT(pullback[0], WithinAbs(2.0 * direction[0] * dual[0] + direction[1] * dual[1], 1.0e-15));
CHECK_THAT(pullback[1], WithinAbs(direction[0] * dual[1], 1.0e-15));
}
TEST_CASE("Nonlinear composed actions reject overlapping state direction and dual before changing any data", "[unit][deformation][composition]")
{
const auto composed = deformation::compose_deformation(nonlinear_surface(), nonlinear_extension());
REQUIRE(composed);
auto workspace = deformation::make_deformation_workspace(*composed);
const std::array state{0.37, -0.21};
const std::array direction{-0.42, 0.63};
const std::array dual{0.9, -0.35, 0.24};
// Exercise direct map methods as well as the generic interface, with exact and partial overlap.
for (const bool direct : {false, true})
{
for (int action = 0; action < 4; ++action)
{
for (int aliased_input = 0; aliased_input < 3; ++aliased_input)
{
if ((aliased_input == 1 && action != 1 && action != 3) || (aliased_input == 2 && action < 2))
{
continue;
}
for (const int offset : {0, 1})
{
CAPTURE(direct, action, aliased_input, offset);
std::array<double, 6> storage{0.37, -0.21, 0.24, 5.0, 7.0, 9.0};
const std::array original = storage;
std::span<const double> input_state = state;
std::span<const double> input_direction = direction;
std::span<const double> input_dual = dual;
if (aliased_input == 0)
{
input_state = std::span<const double>(storage).first(2);
}
else if (aliased_input == 1)
{
input_direction = std::span<const double>(storage).first(2);
}
else
{
input_dual = std::span<const double>(storage).first(3);
}
const std::size_t output_size = action < 2 ? 3 : 2;
const std::span<double> output = std::span<double>(storage).subspan(offset, output_size);
deformation::DeformationStatus status;
if (action == 0)
{
status = direct ? composed->build_displacement(input_state, output, workspace) : deformation::build_displacement(*composed, input_state, output, workspace);
}
else if (action == 1)
{
status = direct ? composed->apply_jacobian(input_state, input_direction, output, workspace) : deformation::apply_jacobian(*composed, input_state, input_direction, output, workspace);
}
else if (action == 2)
{
status = direct ? composed->apply_jacobian_transpose(input_state, input_dual, output, workspace) : deformation::apply_jacobian_transpose(*composed, input_state, input_dual, output, workspace);
}
else
{
status = direct ? composed->apply_pullback_derivative(input_state, input_direction, input_dual, output, workspace) : deformation::apply_pullback_derivative(*composed, input_state, input_direction, input_dual, output, workspace);
}
REQUIRE_FALSE(status);
CHECK(status.error().reason == deformation::DeformationFailureReason::invalid_configuration);
CHECK(storage == original);
CHECK(workspace.surface_workspace.actions == 0);
CHECK(workspace.extension_workspace.actions == 0);
}
}
}
}
std::array<double, 2> recovered{}, reference{};
auto independent_workspace = composed->make_workspace();
REQUIRE(deformation::apply_pullback_derivative(*composed, state, direction, dual, recovered, workspace));
REQUIRE(composed->apply_pullback_derivative(state, direction, dual, reference, independent_workspace));
CHECK(recovered == reference);
}
TEST_CASE("Generic actions protect custom nonlinear maps and linear zero derivatives from aliases", "[unit][deformation][composition]")
{
const auto nonlinear = nonlinear_surface();
auto nonlinear_workspace = deformation::make_deformation_workspace(*nonlinear);
const std::array state{0.37, -0.21};
const std::array direction{-0.42, 0.63};
std::array<double, 2> storage{0.9, -0.35};
const std::array original = storage;
CHECK_FALSE(deformation::build_displacement(*nonlinear, storage, storage, nonlinear_workspace));
CHECK_FALSE(deformation::apply_jacobian(*nonlinear, state, storage, storage, nonlinear_workspace));
CHECK_FALSE(deformation::apply_jacobian_transpose(*nonlinear, state, storage, storage, nonlinear_workspace));
CHECK_FALSE(deformation::apply_pullback_derivative(*nonlinear, state, direction, storage, storage, nonlinear_workspace));
CHECK(storage == original);
CHECK(nonlinear_workspace.actions == 0);
const LinearSurface linear;
auto linear_workspace = deformation::make_deformation_workspace(linear);
CHECK_FALSE(deformation::apply_pullback_derivative(linear, state, direction, storage, storage, linear_workspace));
CHECK(storage == original);
// Adjacent slices in one allocation are disjoint and remain a valid allocation-free calling pattern.
std::array<double, 4> adjacent{0.37, -0.21, 0.0, 0.0};
REQUIRE(deformation::build_displacement(*nonlinear, std::span<const double>(adjacent).first(2), std::span<double>(adjacent).last(2), nonlinear_workspace));
CHECK_THAT(adjacent[2], WithinAbs(state[0] * state[0] + state[1], 1.0e-15));
CHECK_THAT(adjacent[3], WithinAbs(state[0] * state[1], 1.0e-15));
}