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.
557 lines
29 KiB
C++
557 lines
29 KiB
C++
// 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));
|
|
}
|