#include #include #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{1.5, 0.0}); CHECK(options.specified == std::array{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{1.5, 0.0}); for (double invalid : {0.5, std::numeric_limits::quiet_NaN(), std::numeric_limits::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{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{true, false}); family::starting_at(family::AngularMomentum{}, serif::dimensions::AngularMomentumValue{0.01}).apply(options); CHECK(options.start == std::array{1.5, 0.01}); CHECK(options.specified == std::array{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{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{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{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); }