Files
MeanField_SERiF/tests/solver/continuation_settings.cpp
Emily Boudreaux cb20faeb3d 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.
2026-11-09 13:53:17 -05:00

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);
}