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,116 @@
#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);
}