#include #include #include #include #include #include #include #include #include #include #include #include "serif/utils/serialize/top.hpp" namespace { namespace serialization = serif::utils::serialize; constexpr double pi = std::numbers::pi; const double monopole_basis = 1/std::sqrt(4*pi); struct Directory { std::filesystem::path path; Directory() { static std::atomic next{0}; path = std::filesystem::temp_directory_path()/("serif-top-test-"+ std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+"-"+std::to_string(next++)); } ~Directory() { std::error_code error; std::filesystem::remove_all(path, error); } }; serialization::TopPolytropeSource sphere() { return {.polytropic_index=1., .gravitational_constant=1., .mass=4./pi, .angular_velocity=0., .central_density=1., .central_enthalpy=4./pi, .surface_radius=[](double, double)->serialization::Result { return 1.; }, .enthalpy=[](double radius, double, double)->serialization::Result { if (radius==0.) { return 4./pi; } if (radius==1.) { return 0.; } return (4./pi)*std::sin(pi*radius)/(pi*radius); }}; } serialization::TopExportOptions small_options() { return {.radial_points=32, .maximum_degree=4, .angular_points=12, .azimuthal_points=4}; } struct Data { int nr{}, degree{}; std::vector scales, surface; std::vector> h, hz, hzz; }; Data read(const std::filesystem::path& path) { Data result; std::ifstream scales(path/"lambda_eps"); double value; while (scales >> value) { result.scales.push_back(value); } REQUIRE(result.scales.size()==9); std::ifstream surface(path/"domains_boundaries"); surface >> result.degree; for (int l=0; l<=result.degree; ++l) { surface >> value; CHECK(value==0.); } for (int l=0; l<=result.degree; ++l) { surface >> value; result.surface.push_back(value); } std::ifstream fields(path/"enthalpy_et_der"); int count; fields >> result.nr >> count; REQUIRE(count==result.degree+1); std::string line; std::getline(fields, line); std::getline(fields, line); int index; for (int k=0; k<=result.degree/2; ++k) { fields >> index; REQUIRE(index==k+1); for (int i=0; i> value; } } auto block = [&]() { std::getline(fields, line); std::getline(fields, line); std::vector> values(result.degree+1, std::vector(result.nr)); for (int l=0; l<=result.degree; l+=2) { fields >> index; REQUIRE(index==l); for (double& sample : values[l]) { fields >> sample; REQUIRE(std::isfinite(sample)); } } return values; }; result.h=block(); result.hz=block(); result.hzz=block(); REQUIRE(fields.good()); return result; } bool close(double actual, double expected, double tolerance) { return std::abs(actual-expected)<=tolerance*std::max(1., std::abs(expected)); } } TEST_CASE("TOP export reproduces the analytic n1 sphere and Chebyshev derivatives", "[serialize][top]") { const Directory directory; const auto result=serialization::export_top(directory.path, sphere(), small_options()); if (!result) { INFO(result.error().path << ": " << result.error().message); } REQUIRE(result); CHECK(close(result->lambda, pi*pi, 2e-14)); CHECK(close(result->alpha, pi*pi/3, 2e-14)); CHECK(close(result->angular_frequency_unit, std::sqrt(4*pi), 2e-14)); CHECK(std::abs(result->flattening)<1e-14); CHECK(result->maximum_relative_surface_projection_error<1e-13); CHECK(result->minimum_enthalpy==0.); const auto data=read(directory.path); CHECK(close(data.surface[0], 1/monopole_basis, 2e-14)); CHECK(close(data.h[0][0]*monopole_basis, 1., 2e-14)); CHECK(data.h[0].back()==0.); CHECK(std::abs(data.hz[0][0]*monopole_basis)<1e-11); CHECK(close(data.hzz[0][0]*monopole_basis, -pi*pi/3, 1e-8)); CHECK(close(data.hz[0].back()*monopole_basis, -1., 1e-11)); CHECK(close(data.hzz[0].back()*monopole_basis, 2., 1e-8)); for (int i=0; iserialization::Result { return radius(mu); }, .enthalpy=[&](double r, double mu, double)->serialization::Result { const double surface=radius(mu); if (r==0.) { return 5.; } if (r==surface) { return 0.; } double lower=0., upper=1.; for (int step=0; step<60; ++step) { const double z=(lower+upper)/2, a=(5*z*z*z-3*std::pow(z,5))/2; if (1.76*z+a*(surface-1.76)source_equatorial_radius, 2.12, 1e-14)); CHECK(close(result->equatorial_radius, 2.12, 1e-14)); CHECK(close(result->polar_radius, 1.76, 1e-14)); CHECK(result->maximum_relative_surface_projection_error<1e-13); CHECK(close(result->rotation_over_kepler/std::sqrt(3*result->alpha), .31/std::sqrt(24*pi), 1e-14)); const auto data=read(directory.path); CHECK(close(data.surface[0], 2/(2.12*monopole_basis), 1e-13)); CHECK(close(data.surface[2], -.24*std::sqrt(4*pi/5)/2.12, 1e-13)); for (int i=0; iserialization::Result { return 3.; }; scaled.enthalpy=[source](double r, double mu, double phi)->serialization::Result { return 90*source.enthalpy(r/3,mu,phi).value(); }; const auto a=serialization::export_top(first.path,source,small_options()); const auto b=serialization::export_top(second.path,scaled,small_options()); REQUIRE(a); REQUIRE(b); CHECK(close(a->lambda,b->lambda,1e-13)); CHECK(close(a->alpha,b->alpha,1e-13)); CHECK(close(a->rotation,b->rotation,1e-13)); CHECK(close(a->rotation_over_kepler,b->rotation_over_kepler,1e-13)); CHECK(close(b->angular_frequency_unit/a->angular_frequency_unit,std::sqrt(10.),1e-13)); } TEST_CASE("TOP rejects unsupported backgrounds and reports sampler failures before publication", "[serialize][top]") { const Directory directory; auto source=sphere(); auto options=small_options(); SECTION("odd angular degree") { options.maximum_degree=3; } SECTION("undersampled angular projection") { options.angular_points=4; } SECTION("invalid resolution") { options.radial_points=1; } SECTION("invalid tolerance") { options.relative_surface_tolerance=-1; } SECTION("missing callback") { source.enthalpy={}; } SECTION("invalid units") { source.gravitational_constant=0; } SECTION("singular vacuum index") { source.polytropic_index=.5; } SECTION("nonfinite rotation") { source.angular_velocity=std::numeric_limits::quiet_NaN(); } SECTION("invalid surface") { source.surface_radius=[](double,double)->serialization::Result { return -1.; }; } SECTION("nonaxisymmetric surface") { source.surface_radius=[](double,double phi)->serialization::Result { return 1+.1*std::cos(phi); }; } SECTION("north south asymmetry") { source.surface_radius=[](double mu,double)->serialization::Result { return 1+.1*mu; }; } SECTION("surface projection loss") { source.surface_radius=[](double mu,double)->serialization::Result { return 1+.1*(231*std::pow(mu,6)-315*std::pow(mu,4)+105*mu*mu-5)/16; }; } SECTION("degenerate vacuum surface slope") { source.enthalpy=[](double r,double,double)->serialization::Result { return 4/pi*std::pow(1-r*r,2); }; } SECTION("finite pressure surface") { source.enthalpy=[](double,double,double)->serialization::Result { return 4/pi; }; } SECTION("negative interior") { source.enthalpy=[](double r,double,double)->serialization::Result { return r==0. ? 4/pi : -.01; }; } SECTION("zero interior") { source.enthalpy=[](double r,double,double)->serialization::Result { return r==0. ? 4/pi : 0.; }; } SECTION("nonaxisymmetric enthalpy") { source.enthalpy=[](double r,double,double phi)->serialization::Result { return 4/pi*(1-r*r)*(1+.1*r*std::cos(phi)); }; } SECTION("nonfinite enthalpy") { source.enthalpy=[](double,double,double)->serialization::Result { return std::numeric_limits::infinity(); }; } SECTION("sampler error path") { source.enthalpy=[](double,double,double)->serialization::Result { return std::unexpected(serialization::Error{"element/7","Sample unavailable"}); }; } const auto result=serialization::export_top(directory.path,source,options); REQUIRE_FALSE(result); CHECK_FALSE(result.error().path.empty()); CHECK_FALSE(std::filesystem::exists(directory.path)); } TEST_CASE("TOP export never replaces destinations including a destination created during sampling", "[serialize][top]") { const Directory directory; auto source=sphere(); SECTION("existing empty directory") { std::filesystem::create_directory(directory.path); } SECTION("existing nonempty directory") { std::filesystem::create_directory(directory.path); std::ofstream(directory.path/"preserved") << "original"; } SECTION("existing file") { std::ofstream(directory.path) << "original"; } SECTION("publication race") { source.roundoff_density_clamps=[&]() { std::filesystem::create_directory(directory.path); std::ofstream(directory.path/"preserved") << "original"; return std::size_t{0}; }; } const auto result=serialization::export_top(directory.path,source,small_options()); REQUIRE_FALSE(result); CHECK(std::filesystem::exists(directory.path)); CHECK_FALSE(std::filesystem::exists(directory.path/"lambda_eps")); if (std::filesystem::exists(directory.path/"preserved")) { std::ifstream input(directory.path/"preserved"); std::string text; input >> text; CHECK(text=="original"); } bool temporary_remains=false; for (const auto& entry : std::filesystem::directory_iterator(directory.path.parent_path())) { temporary_remains |= entry.path().filename().string().starts_with("."+directory.path.filename().string()+".top-tmp-"); } CHECK_FALSE(temporary_remains); } TEST_CASE("TOP records explicit adapter roundoff corrections", "[serialize][top]") { const Directory directory; auto source=sphere(); source.roundoff_density_clamps=[] { return std::size_t{3}; }; const auto result=serialization::export_top(directory.path,source,small_options()); REQUIRE(result); CHECK(result->roundoff_density_clamps==3); } TEST_CASE("TOP records and removes only a tolerated surface defect", "[serialize][top]") { const Directory directory; auto source=sphere(); const auto original=source.enthalpy; source.enthalpy=[original](double r,double mu,double phi)->serialization::Result { if (r==1.) { return (4/pi)*1e-8; } return original(r,mu,phi); }; const auto result=serialization::export_top(directory.path,source,small_options()); REQUIRE(result); CHECK(close(result->maximum_relative_surface_enthalpy,1e-8,1e-15)); const auto data=read(directory.path); for (int l=0;l<=data.degree;l+=2) { CHECK(data.h[l].back()==0.); } } TEST_CASE("TOP separates even enthalpy truncation from symmetry error", "[serialize][top]") { const Directory truncated, resolved; auto source=sphere(); source.enthalpy=[](double r,double mu,double)->serialization::Result { const double p6=(231*std::pow(mu,6)-315*std::pow(mu,4)+105*mu*mu-5)/16; return (4/pi)*(1-r*r)*(1+.1*std::pow(r,6)*p6); }; auto options=small_options(); const auto low=serialization::export_top(truncated.path,source,options); REQUIRE(low); CHECK(low->maximum_relative_enthalpy_symmetry_error<1e-14); CHECK(low->maximum_relative_enthalpy_projection_error>1e-3); options.maximum_degree=6; const auto high=serialization::export_top(resolved.path,source,options); REQUIRE(high); CHECK(high->maximum_relative_enthalpy_symmetry_error<1e-14); CHECK(high->maximum_relative_enthalpy_projection_error<1e-13); }