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.
117 lines
6.8 KiB
C++
117 lines
6.8 KiB
C++
#include <limits>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include "serif/solver/continuation/polytropic.hpp"
|
|
#include "serif/solver/continuation/options.hpp"
|
|
|
|
namespace {
|
|
namespace family = serif::solver::continuation;
|
|
namespace controller = serif::solver::continuation;
|
|
/** @brief Make physical settings tests independent of finite-element resources. */
|
|
[[nodiscard]] auto bound_family() {
|
|
const auto target = serif::model::make_stellar_model(serif::eos::models::Polytrope{2.0, 0.73}, []() {},
|
|
serif::model::FixedAngularMomentum{serif::dimensions::AngularMomentumValue{0.1}});
|
|
return family::in(family::PolytropicIndex{}, family::AngularMomentum{})
|
|
.with(family::preserve_pressure_at_reference_density(serif::dimensions::DensityValue{0.2})).bind(target);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Continuation starting settings reject duplicates and invalid domains without mutation", "[unit][solver][continuation][settings]") {
|
|
const auto bound = bound_family();
|
|
auto options = family::make_options(bound);
|
|
CHECK(options.start == bound.starting_coordinates());
|
|
CHECK_FALSE(options.explicit_start);
|
|
family::starting_at(family::PolytropicIndex{}, 1.5).apply(options);
|
|
CHECK(options.start == std::array<double, 2>{1.5, 0.0});
|
|
CHECK(options.specified == std::array<bool, 2>{true, false});
|
|
CHECK(options.explicit_start);
|
|
CHECK_THROWS_AS(family::starting_at(family::PolytropicIndex{}, 1.7).apply(options), std::invalid_argument);
|
|
CHECK(options.start == std::array<double, 2>{1.5, 0.0});
|
|
for (double invalid : {0.5, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::infinity()}) {
|
|
auto unchanged = family::make_options(bound);
|
|
CHECK_THROWS_AS(family::starting_at(family::PolytropicIndex{}, invalid).apply(unchanged), std::invalid_argument);
|
|
CHECK(unchanged.start == bound.starting_coordinates());
|
|
CHECK(unchanged.specified == std::array<bool, 2>{false, false});
|
|
CHECK_FALSE(unchanged.explicit_start);
|
|
}
|
|
CHECK_THROWS_AS(family::starting_at(family::AngularMomentum{}, serif::dimensions::AngularMomentumValue{-0.01}).apply(options), std::invalid_argument);
|
|
CHECK(options.specified == std::array<bool, 2>{true, false});
|
|
family::starting_at(family::AngularMomentum{}, serif::dimensions::AngularMomentumValue{0.01}).apply(options);
|
|
CHECK(options.start == std::array<double, 2>{1.5, 0.01});
|
|
CHECK(options.specified == std::array<bool, 2>{true, true});
|
|
CHECK_THROWS_AS(family::starting_at(family::AngularMomentum{}, serif::dimensions::AngularMomentumValue{0.02}).apply(options), std::invalid_argument);
|
|
CHECK(options.start == std::array<double, 2>{1.5, 0.01});
|
|
}
|
|
|
|
TEST_CASE("Continuation physical ordering and numerical controls are independent of argument order", "[unit][solver][continuation][settings]") {
|
|
const auto bound = bound_family();
|
|
controller::CoordinateOptions<2> numerical;
|
|
numerical.initial_step_fraction = {0.1, 0.2};
|
|
numerical.minimum_step_fraction = {0.001, 0.002};
|
|
numerical.maximum_step_fraction = {0.5, 0.75};
|
|
numerical.growth_factor = 1.25;
|
|
numerical.shrink_factor = 0.4;
|
|
numerical.maximum_attempts = 21;
|
|
numerical.maximum_rejections_per_axis = 3;
|
|
numerical.order = controller::CoordinateOrder::strict;
|
|
const auto first = family::make_options(bound, family::prefer(family::AngularMomentum{}), family::numerics(numerical));
|
|
const auto second = family::make_options(bound, family::numerics(numerical), family::prefer(family::AngularMomentum{}));
|
|
for (const auto* options : {&first, &second}) {
|
|
CHECK(options->has_ordering);
|
|
CHECK(options->has_numerics);
|
|
CHECK(options->steps.order == controller::CoordinateOrder::preferred);
|
|
CHECK(options->steps.axis_order == std::array<std::size_t, 2>{1, 0});
|
|
CHECK(options->steps.initial_step_fraction == numerical.initial_step_fraction);
|
|
CHECK(options->steps.minimum_step_fraction == numerical.minimum_step_fraction);
|
|
CHECK(options->steps.maximum_step_fraction == numerical.maximum_step_fraction);
|
|
CHECK(options->steps.growth_factor == numerical.growth_factor);
|
|
CHECK(options->steps.shrink_factor == numerical.shrink_factor);
|
|
CHECK(options->steps.maximum_attempts == numerical.maximum_attempts);
|
|
CHECK(options->steps.maximum_rejections_per_axis == numerical.maximum_rejections_per_axis);
|
|
CHECK(options->start == bound.starting_coordinates());
|
|
CHECK_FALSE(options->explicit_start);
|
|
}
|
|
auto options = first;
|
|
CHECK_THROWS_AS(family::prefer(family::PolytropicIndex{}).apply(options), std::invalid_argument);
|
|
CHECK_THROWS_AS(family::stages(family::PolytropicIndex{}, family::AngularMomentum{}).apply(options), std::invalid_argument);
|
|
numerical.maximum_attempts = 99;
|
|
CHECK_THROWS_AS(family::numerics(numerical).apply(options), std::invalid_argument);
|
|
CHECK(options.steps.maximum_attempts == 21);
|
|
CHECK(options.steps.initial_step_fraction == first.steps.initial_step_fraction);
|
|
CHECK(options.steps.axis_order == first.steps.axis_order);
|
|
const auto preferred = family::prefer(family::AngularMomentum{});
|
|
const auto strict = family::stages(family::AngularMomentum{}, family::PolytropicIndex{});
|
|
/** @brief A second ordering never partially changes an already selected route. */
|
|
const auto reject_second = [&](const auto& initial, const auto& duplicate) {
|
|
auto retained = family::make_options(bound, initial);
|
|
const auto order = retained.steps.order;
|
|
const auto axes = retained.steps.axis_order;
|
|
CHECK_THROWS_AS(duplicate.apply(retained), std::invalid_argument);
|
|
CHECK(retained.steps.order == order);
|
|
CHECK(retained.steps.axis_order == axes);
|
|
};
|
|
reject_second(preferred, preferred);
|
|
reject_second(strict, strict);
|
|
reject_second(preferred, strict);
|
|
reject_second(strict, preferred);
|
|
}
|
|
|
|
TEST_CASE("Strict physical continuation stages select a complete coordinate route", "[unit][solver][continuation][settings]") {
|
|
const auto bound = bound_family();
|
|
auto options = family::make_options(bound, family::stages(family::AngularMomentum{}, family::PolytropicIndex{}));
|
|
options.steps.initial_step_fraction.fill(1.0);
|
|
controller::CoordinateController<2> route(options.start, bound.target_coordinates(), options.steps);
|
|
const auto rotation = route.propose();
|
|
REQUIRE(rotation);
|
|
CHECK(rotation->axis == 1);
|
|
CHECK(rotation->coordinates == std::array<double, 2>{1.0, 0.1});
|
|
route.resolve(rotation->id, controller::AttemptOutcome::success);
|
|
const auto index = route.propose();
|
|
REQUIRE(index);
|
|
CHECK(index->axis == 0);
|
|
CHECK(index->coordinates == bound.target_coordinates());
|
|
route.resolve(index->id, controller::AttemptOutcome::success);
|
|
CHECK(route.status() == controller::ControllerStatus::complete);
|
|
}
|