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:
654
tests/sandbox/complete_sandbox.cpp
Normal file
654
tests/sandbox/complete_sandbox.cpp
Normal file
@@ -0,0 +1,654 @@
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <numbers>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <print>
|
||||
|
||||
#include "mfem/fem/intrules.hpp"
|
||||
#include "serif/runtime.hpp"
|
||||
#include "serif/backend/mfem/serialize/discretization.hpp"
|
||||
#include "serif/backend/mfem/serialize/top.hpp"
|
||||
#include "serif/utils/serialize/serialize.hpp"
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/model/finite_elements.hpp"
|
||||
#include "serif/solver/equilibrium.hpp"
|
||||
#include "serif/solver/preconditioner/default_preconditioners.hpp"
|
||||
#include "serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.hpp"
|
||||
#include "serif/solver/seed/state.hpp"
|
||||
#include "serif/utils/laneemden/solution.hpp"
|
||||
#include "polytropic_model.hpp"
|
||||
#include "stellar_rayfem.hpp"
|
||||
#include "serif/tests/solver/virial.hpp"
|
||||
#include "libRayfem/init.hpp"
|
||||
|
||||
namespace {
|
||||
// Defaults: the n=2, J=0.1 star (Omega/Omega_K ~0.70, flattening ~0.22), the fastest rotator in the ESTER/TOP grid
|
||||
// that converges from the spherical seed. L=12 resolves its surface: all 33 reference modes match within 0.55 PLATO
|
||||
// sigma (L=8 leaves 3.65 sigma). --angular-momentum 0.05 --surface-degree 8 gives the original benchmark star.
|
||||
struct Arguments {
|
||||
bool visualize = true;
|
||||
bool visualize_requested = false;
|
||||
double angular_momentum = 0.1;
|
||||
double polytropic_index = 2.0;
|
||||
std::optional<double> polytropic_constant;
|
||||
bool surface_power_density = true;
|
||||
int surface_degree = 12;
|
||||
int mesh_refinement = 0;
|
||||
int surface_refinement = 1;
|
||||
int equatorial_refinement = 1;
|
||||
double equatorial_band_degrees = 10.0;
|
||||
int radial_surface_refinement = 3;
|
||||
/// Compactified infinity surface, in stellar radii. The vacuum map folds once the equatorial bulge reaches
|
||||
/// 1/(F - 1) stellar radii, so the fastest rotators need F below the library default of 4.
|
||||
double infinity_radius = 4.0;
|
||||
/// Converged archive (profile_solve --save-state) with this n, K and discretization to start Newton from.
|
||||
std::filesystem::path seed_state;
|
||||
/// Fixed volume quadrature order; empty uses SERiF's automatic rule (~35 on this mesh). Order 11 matches
|
||||
/// the automatic rule's ESTER/TOP agreement for the benchmark star at ~1/14 of the memory.
|
||||
std::optional<int> quadrature_order = 11;
|
||||
/// Potential-block inverse: sparse LU or three fixed BoomerAMG cycles (SERiF's default recipe). Empty (auto)
|
||||
/// uses LU when this build has a sparse_direct backend that works on this machine.
|
||||
std::optional<bool> potential_inverse_lu;
|
||||
std::array<double, 3> rotation_axis{0.0, 0.0, 1.0};
|
||||
/// Tidally locked companion on +x: its mass (G = M = 1) and separation; zero mass means a single star.
|
||||
double companion_mass = 0.0;
|
||||
double separation = 0.0;
|
||||
/// The star spins about its own axis at the rate J sets and feels only the companion's tide (asynchronous Roche).
|
||||
bool asynchronous = false;
|
||||
/// Continuation: solve first at separation_start, then step geometrically to separation, seeding each solve.
|
||||
double separation_start = 0.0;
|
||||
int separation_steps = 0;
|
||||
/// Converged binary surface radius on a (theta, phi) grid, for plotting.
|
||||
std::filesystem::path surface_table;
|
||||
bool mesh_only = false;
|
||||
int viewer_port = 5555;
|
||||
std::string viewer;
|
||||
std::filesystem::path top_export;
|
||||
serif::utils::serialize::TopExportOptions top_options;
|
||||
};
|
||||
|
||||
Arguments parse_arguments(int argc, char** argv) {
|
||||
Arguments args;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string_view option(argv[i]);
|
||||
if (option == "--visualize") { args.visualize = args.visualize_requested = true; continue; }
|
||||
if (option == "--no-visualize") { args.visualize = false; continue; }
|
||||
if (option == "--surface-power-density") { args.surface_power_density = true; continue; }
|
||||
if (option == "--ordinary-density") { args.surface_power_density = false; continue; }
|
||||
if (option == "--mesh-only") { args.mesh_only = true; continue; }
|
||||
if (option == "--asynchronous") { args.asynchronous = true; continue; }
|
||||
if (option == "--rotation-axis") {
|
||||
for (double& component : args.rotation_axis) {
|
||||
if (++i == argc) throw std::invalid_argument("--rotation-axis requires three components: X Y Z");
|
||||
const std::string value(argv[i]);
|
||||
std::size_t consumed = 0;
|
||||
component = std::stod(value, &consumed);
|
||||
if (consumed != value.size() || !std::isfinite(component))
|
||||
throw std::invalid_argument("Rotation-axis components must be finite numbers");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (++i == argc) throw std::invalid_argument("Missing value for " + std::string(option));
|
||||
const std::string value(argv[i]);
|
||||
std::size_t consumed = 0;
|
||||
if (option == "--export-top") { args.top_export = value; continue; }
|
||||
if (option == "--surface-table") { args.surface_table = value; continue; }
|
||||
if (option == "--seed-state") { args.seed_state = value; continue; }
|
||||
if (option == "--quadrature-order" && value == "auto") { args.quadrature_order.reset(); continue; }
|
||||
if (option == "--potential-inverse") {
|
||||
if (value != "lu" && value != "amg" && value != "auto") throw std::invalid_argument("--potential-inverse takes auto, lu or amg");
|
||||
if (value == "auto") args.potential_inverse_lu.reset();
|
||||
else args.potential_inverse_lu = value == "lu";
|
||||
continue;
|
||||
}
|
||||
if (option == "--viewer") { args.viewer = value; args.visualize = args.visualize_requested = true; continue; }
|
||||
if (option == "--angular-momentum") args.angular_momentum = std::stod(value, &consumed);
|
||||
else if (option == "--polytropic-index") args.polytropic_index = std::stod(value, &consumed);
|
||||
else if (option == "--K") args.polytropic_constant = std::stod(value, &consumed);
|
||||
else if (option == "--surface-degree") args.surface_degree = std::stoi(value, &consumed);
|
||||
else if (option == "--mesh-refinement") args.mesh_refinement = std::stoi(value, &consumed);
|
||||
else if (option == "--surface-refinement") args.surface_refinement = std::stoi(value, &consumed);
|
||||
else if (option == "--equatorial-refinement") args.equatorial_refinement = std::stoi(value, &consumed);
|
||||
else if (option == "--equatorial-band-degrees") args.equatorial_band_degrees = std::stod(value, &consumed);
|
||||
else if (option == "--infinity-radius") args.infinity_radius = std::stod(value, &consumed);
|
||||
else if (option == "--radial-surface-refinement") args.radial_surface_refinement = std::stoi(value, &consumed);
|
||||
else if (option == "--quadrature-order") args.quadrature_order = std::stoi(value, &consumed);
|
||||
else if (option == "--top-radial-points") args.top_options.radial_points = std::stoi(value, &consumed);
|
||||
else if (option == "--top-degree") args.top_options.maximum_degree = std::stoi(value, &consumed);
|
||||
else if (option == "--top-angular-points") args.top_options.angular_points = std::stoi(value, &consumed);
|
||||
else if (option == "--top-azimuthal-points") args.top_options.azimuthal_points = std::stoi(value, &consumed);
|
||||
else if (option == "--viewer-port") args.viewer_port = std::stoi(value, &consumed);
|
||||
else if (option == "--companion-mass") args.companion_mass = std::stod(value, &consumed);
|
||||
else if (option == "--separation") args.separation = std::stod(value, &consumed);
|
||||
else if (option == "--separation-start") args.separation_start = std::stod(value, &consumed);
|
||||
else if (option == "--separation-steps") args.separation_steps = std::stoi(value, &consumed);
|
||||
else throw std::invalid_argument("Unknown option: " + std::string(option));
|
||||
if (consumed != value.size()) throw std::invalid_argument("Invalid value for " + std::string(option));
|
||||
}
|
||||
if (!std::isfinite(args.polytropic_index) || args.polytropic_index < 1.0 || args.polytropic_index >= 3.0)
|
||||
throw std::invalid_argument("Use a polytropic index in [1,3) for this vacuum benchmark");
|
||||
if (!std::isfinite(args.angular_momentum) || args.surface_degree < 0 ||
|
||||
args.viewer_port < 1 || args.viewer_port > 65534)
|
||||
throw std::invalid_argument("Use finite angular momentum, nonnegative surface degree and viewer port 1..65534");
|
||||
if (args.polytropic_constant && (!std::isfinite(*args.polytropic_constant) || *args.polytropic_constant <= 0.0))
|
||||
throw std::invalid_argument("K must be finite and positive");
|
||||
if (args.quadrature_order && *args.quadrature_order < 0)
|
||||
throw std::invalid_argument("The volume quadrature order must be nonnegative, or auto");
|
||||
if (args.mesh_refinement < 0 || args.surface_refinement < 0 || args.equatorial_refinement < 0 || args.radial_surface_refinement < 0)
|
||||
throw std::invalid_argument("Mesh, surface, equatorial and radial surface refinement counts must be nonnegative");
|
||||
if (!std::isfinite(args.equatorial_band_degrees) || args.equatorial_band_degrees <= 0.0 || args.equatorial_band_degrees > 35.0)
|
||||
throw std::invalid_argument("The equatorial band half-angle must satisfy 0 < degrees <= 35");
|
||||
if (!std::isfinite(args.infinity_radius) || args.infinity_radius <= 1.0)
|
||||
throw std::invalid_argument("The infinity radius must be a finite number of stellar radii above 1");
|
||||
if (std::ranges::all_of(args.rotation_axis, [](double x) { return x == 0.0; }))
|
||||
throw std::invalid_argument("The rotation axis must be nonzero");
|
||||
if (!std::isfinite(args.companion_mass) || !std::isfinite(args.separation) || args.companion_mass < 0.0 ||
|
||||
(args.companion_mass > 0.0) != (args.separation > 0.0))
|
||||
throw std::invalid_argument("--companion-mass and --separation go together: a positive mass at a separation of several stellar radii");
|
||||
if (args.companion_mass > 0.0 && (args.rotation_axis != std::array<double, 3>{0.0, 0.0, 1.0} || !args.top_export.empty() || !args.seed_state.empty()))
|
||||
throw std::invalid_argument("A companion orbits about the default z axis; TOP export and seed states are single-star only");
|
||||
if (args.separation_steps < 0 || (args.separation_steps > 0 && !(args.companion_mass > 0.0 && std::isfinite(args.separation_start) && args.separation_start > 0.0)) ||
|
||||
(!args.surface_table.empty() && args.companion_mass <= 0.0))
|
||||
throw std::invalid_argument("--separation-steps needs a companion and a positive --separation-start; --surface-table needs a companion");
|
||||
if (args.asynchronous && args.companion_mass <= 0.0)
|
||||
throw std::invalid_argument("--asynchronous needs a companion");
|
||||
if (args.mesh_only && args.visualize_requested)
|
||||
throw std::invalid_argument("--mesh-only reports mesh statistics without solving; omit --visualize");
|
||||
if (args.mesh_only) args.visualize = false;
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief K that gives the nonrotating polytrope of index n a radius of 1 when G = M = 1.
|
||||
* @note Lane-Emden scaling: R = xi_1 alpha and M = 4 pi alpha^3 rho_c (-xi_1^2 theta'(xi_1)), with
|
||||
* alpha^2 = (n+1) K rho_c^(1/n-1) / (4 pi G). For n = 1 this is 2/pi.
|
||||
*/
|
||||
double unit_radius_polytropic_constant(const double n) {
|
||||
// K is a model constant, so integrate well below the seed solver's default step.
|
||||
const auto lane_emden = serif::utils::laneemden::solve(n, {.step = 1.0e-4, .maximum_steps = 1000000});
|
||||
if (!lane_emden || !lane_emden->surface)
|
||||
throw std::runtime_error("Cannot find the Lane-Emden surface for n = " + std::to_string(n));
|
||||
const double xi = lane_emden->surface->xi;
|
||||
const double alpha = 1.0 / xi;
|
||||
const double central_density = 1.0 / (4.0 * std::numbers::pi * alpha * alpha * alpha * (-xi * xi * lane_emden->surface->derivative));
|
||||
return 4.0 * std::numbers::pi * alpha * alpha * std::pow(central_density, 1.0 - 1.0 / n) / (n + 1.0);
|
||||
}
|
||||
|
||||
/** @brief Resolve --potential-inverse auto against the compiled sparse_direct backend. */
|
||||
bool use_lu(const Arguments& args) {
|
||||
return args.potential_inverse_lu.value_or(serif::solver::preconditioner::backend::mfem::sparse_direct_available());
|
||||
}
|
||||
|
||||
void print_options(const Arguments& args, const double K) {
|
||||
std::println("complete_sandbox options (G = M = 1; K gives a nonrotating radius of 1 unless --K is set):");
|
||||
std::println(" polytropic index n {}", args.polytropic_index);
|
||||
std::println(" polytropic constant K {:.17g}{}", K, args.polytropic_constant ? " (--K)" : " (computed from Lane-Emden)");
|
||||
if (args.companion_mass > 0.0)
|
||||
std::println(" companion mass {} at separation {} on +x, {}{}", args.companion_mass, args.separation,
|
||||
args.asynchronous ? "tide only (asynchronous)" : "tidally locked (Omega from force balance)",
|
||||
args.separation_steps ? std::format("; continued from {} in {} steps", args.separation_start, args.separation_steps) : std::string{});
|
||||
if (args.companion_mass <= 0.0 || args.asynchronous)
|
||||
std::println(" angular momentum J {}", args.angular_momentum);
|
||||
std::println(" rotation axis ({}, {}, {})", args.rotation_axis[0], args.rotation_axis[1], args.rotation_axis[2]);
|
||||
std::println(" surface harmonic degree {}", args.surface_degree);
|
||||
std::println(" density representation {}", args.surface_power_density ? "SurfacePower t^n on surface-adjacent cells" : "ordinary");
|
||||
std::println(" mesh refinement {}", args.mesh_refinement);
|
||||
std::println(" surface refinement {}", args.surface_refinement);
|
||||
std::println(" equatorial refinement {} (band half-angle {} degrees)", args.equatorial_refinement, args.equatorial_band_degrees);
|
||||
std::println(" radial surface refinement {}", args.radial_surface_refinement);
|
||||
std::println(" volume quadrature {}", args.quadrature_order
|
||||
? std::format("fixed order {} ({} points per hexahedron)", *args.quadrature_order,
|
||||
::mfem::IntRules.Get(::mfem::Geometry::CUBE, *args.quadrature_order).GetNPoints())
|
||||
: std::string{"automatic (SERiF's per-form rule)"});
|
||||
namespace inverse_backend = serif::solver::preconditioner::backend::mfem;
|
||||
std::println(" potential inverse {}{}", use_lu(args) ? std::format("sparse LU ({})", inverse_backend::sparse_direct_backend()) : std::string{"3 fixed BoomerAMG cycles"},
|
||||
args.potential_inverse_lu ? "" : " (auto)");
|
||||
std::println(" visualization {}", args.visualize ? std::format("RayFEM on port {}", args.viewer_port) : "off");
|
||||
std::println(" TOP export {}", args.top_export.empty() ? "off" : std::format("{} ({} radial, degree {}, {} angular, {} azimuthal)",
|
||||
args.top_export.string(), args.top_options.radial_points, args.top_options.maximum_degree,
|
||||
args.top_options.angular_points, args.top_options.azimuthal_points));
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief One RayFEM connection shared by every continuation step, so all their frames form one sequence.
|
||||
* @note Every step uses the same reference mesh, so it is sent once and frame indices keep increasing across steps.
|
||||
*/
|
||||
struct LiveStream {
|
||||
RayFEM::Client client;
|
||||
std::uint32_t next_frame = 0;
|
||||
bool mesh_sent = false;
|
||||
bool enabled = true;
|
||||
bool continuation = false;
|
||||
int step = 0;
|
||||
|
||||
/** @brief Start or reuse the viewer on the requested port. */
|
||||
static std::unique_ptr<LiveStream> open(const Arguments& args) {
|
||||
RayFEM::WindowParams window{.port = args.viewer_port, .executable = args.viewer};
|
||||
#ifdef SERIF_RAYFEM_EXECUTABLE
|
||||
const char* viewer_override = std::getenv("RAYFEM_EXECUTABLE");
|
||||
if (window.executable.empty() && (!viewer_override || !*viewer_override))
|
||||
window.executable = SERIF_RAYFEM_EXECUTABLE;
|
||||
#endif
|
||||
const auto viewer = RayFEM::init(window);
|
||||
if (!viewer) throw std::runtime_error("Cannot initialize RayFEM: " + viewer.error);
|
||||
if (viewer.status == RayFEM::InitStatus::Started)
|
||||
std::println("Started RayFEM: {}", viewer.executable);
|
||||
else
|
||||
std::println("Reusing RayFEM on port {}", args.viewer_port);
|
||||
auto stream = std::unique_ptr<LiveStream>(new LiveStream{RayFEM::Client("127.0.0.1", args.viewer_port)});
|
||||
// Frames go out from a background thread: a send returns once the frame is copied,
|
||||
// so Newton never waits for the viewer or the network.
|
||||
stream->client.startQueue();
|
||||
return stream;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Build, solve and report one star; Binary adds the tidally locked companion and changes the model's type.
|
||||
* @note seed (same discretization) replaces the spherical start; converged receives the solved coordinates.
|
||||
* With a stream, accepted iterates are sent to RayFEM, tagged with the stream's continuation step and the separation.
|
||||
*/
|
||||
template <bool Binary>
|
||||
int solve_star(const Arguments& args, const double K, std::vector<double>* converged, const std::vector<double>* seed,
|
||||
LiveStream* stream) {
|
||||
namespace model = serif::model;
|
||||
namespace equations = serif::equations;
|
||||
namespace dimensions = serif::dimensions;
|
||||
namespace solver = serif::solver;
|
||||
namespace normalization = solver::normalization;
|
||||
|
||||
// 1. Describe the star with M=G=1, the requested n and J, and K (by default a nonrotating radius of 1).
|
||||
// Deferred equations read the same owned prescriptions used by continuation.
|
||||
// A tidally locked companion sits on +x: rotation is about the barycenter, and Omega's row fixes the star's center of mass.
|
||||
const double companion_mass = args.companion_mass, separation = args.separation;
|
||||
const auto stellar_equations = [=](const auto& physics) {
|
||||
const auto eos = model::borrow_eos(physics.equation_of_state());
|
||||
const auto& rotation = physics.template specification<model::FixedAngularMomentum>();
|
||||
const double mass = physics.template specification<model::FixedTotalMass>().target().value();
|
||||
std::optional<equations::PointMassCompanion> companion;
|
||||
auto center = rotation.center(); // the star stays at the specification's center; a binary rotates about the barycenter
|
||||
if (companion_mass > 0.0) companion.emplace(companion_mass, equations::RigidRotation::Vector{separation, 0.0, 0.0}, !Binary);
|
||||
if constexpr (Binary) center[0] += companion_mass * separation / (mass + companion_mass);
|
||||
auto omega_row = [&] {
|
||||
if constexpr (Binary) return equations::BarycenterConstraint<0, serif::discretization::blocks::rotation::angular_velocity>{mass};
|
||||
else return equations::AngularMomentumConstraint{rotation.target().value(), rotation.axis(), rotation.center()};
|
||||
}();
|
||||
return model::EquilibriumEquations{
|
||||
.interior_equations = equations::EquationRegistry{
|
||||
equations::BarotropicDensityBalance{eos, dimensions::PressureValue{0.0}, rotation.axis(), center, companion},
|
||||
equations::PrimalPoissonBalance{4.0 * std::numbers::pi},
|
||||
equations::MassNormalization{mass},
|
||||
std::move(omega_row)
|
||||
},
|
||||
.shape_force = equations::SurfacePotentialBalance{rotation.axis(), center, companion},
|
||||
.surface_condition = serif::geometry::surface::Isobaric{dimensions::PressureValue{0.0}}
|
||||
};
|
||||
};
|
||||
|
||||
const double n = args.polytropic_index;
|
||||
const auto stellar_model = model::make_stellar_model(serif::eos::models::Polytrope{n, K}, stellar_equations,
|
||||
model::FixedTotalMass{dimensions::MassValue{1.0}}, model::FixedAngularMomentum{model::FixedAngularMomentum::Parameters{
|
||||
.angular_momentum = dimensions::AngularMomentumValue{Binary ? 0.0 : args.angular_momentum}, .axis = args.rotation_axis}});
|
||||
const auto& rotation = stellar_model.template specification<model::FixedAngularMomentum>();
|
||||
|
||||
// 2. Obtain a radial initial guess while preserving the EOS and the target mass.
|
||||
const auto radial = solver::seed::make_polytropic_seed(stellar_model);
|
||||
if (!radial) {
|
||||
std::println(stderr, "Cannot prepare the radial star: {}", radial.error());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 3. Derive field spaces from their term types and own all numerical resources.
|
||||
// Surface harmonics remain an explicit approximation choice.
|
||||
namespace deformation = serif::geometry::deformation;
|
||||
namespace reconstruction = serif::discretization::reconstruction;
|
||||
using Density = serif::discretization::blocks::density::mass;
|
||||
const deformation::DomainDeformation<deformation::SphericalHarmonicSurface> moving_boundary{
|
||||
.surface = {.maximum_degree = args.surface_degree}, .stellar_interior = {.radial_power = 2.0}, .vacuum = {}};
|
||||
const auto finite_elements = model::make_stellar_discretization(stellar_model,
|
||||
{.radius = radial->scales.radius, .infinity_radius_fraction = args.infinity_radius, .refinement = args.mesh_refinement,
|
||||
.surface_refinement = {.levels = args.surface_refinement, .equatorial_levels = args.equatorial_refinement,
|
||||
.equatorial_half_angle_degrees = args.equatorial_band_degrees, .radial_levels = args.radial_surface_refinement,
|
||||
.axis = rotation.axis()}}, moving_boundary,
|
||||
reconstruction::SurfacePower<Density>{.density_exponent = args.surface_power_density ? n : 1.0});
|
||||
const auto& mesh = *finite_elements.owner->resources.meshes.front();
|
||||
using Schema = serif::discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
|
||||
const int surface_attribute = Schema::boundary_id<serif::discretization::domain::StellarSurfaceBoundary>();
|
||||
int surface_faces = 0;
|
||||
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary)
|
||||
surface_faces += mesh.GetBdrAttribute(boundary) == surface_attribute;
|
||||
std::println("Mesh: {} elements, {} stellar surface faces; rotation axis = ({}, {}, {})",
|
||||
mesh.GetNE(), surface_faces, rotation.axis()[0], rotation.axis()[1], rotation.axis()[2]);
|
||||
if (args.mesh_only) return 0;
|
||||
auto initial_state = solver::seed::make_polytropic_state(stellar_model, finite_elements, *radial);
|
||||
if (!initial_state) {
|
||||
std::println(stderr, "Cannot project the radial star: {}", initial_state.error());
|
||||
return 1;
|
||||
}
|
||||
// Start the orbit at Kepler's rate for two point masses.
|
||||
const double kepler_omega = std::sqrt((1.0 + companion_mass) / (separation * separation * separation));
|
||||
if constexpr (Binary) initial_state->template get<serif::discretization::blocks::rotation::angular_velocity>()[0] = kepler_omega;
|
||||
if (seed) std::ranges::copy(*seed, initial_state->values().begin());
|
||||
|
||||
if (!args.seed_state.empty()) {
|
||||
// Continuation: replace the spherical seed with a converged star of the same n, K and discretization.
|
||||
namespace archive = serif::utils::serialize;
|
||||
using State = std::remove_cvref_t<decltype(*initial_state)>;
|
||||
const auto saved = archive::Reader::open(args.seed_state);
|
||||
if (!saved) throw std::runtime_error(saved.error().message);
|
||||
const auto loaded = saved->read<State>("state");
|
||||
const auto old_n = saved->read<double>("n"), old_K = saved->read<double>("K"), old_J = saved->read<double>("J");
|
||||
const auto converged = saved->read<bool>("converged");
|
||||
const auto geometry = saved->node("finite_elements");
|
||||
if (!loaded || !old_n || !old_K || !old_J || !converged || !*converged || *old_n != n || *old_K != K || !geometry ||
|
||||
*geometry != serif::backend::mfem::serialize::save_discretization(finite_elements) || loaded->layout() != initial_state->layout())
|
||||
throw std::runtime_error("--seed-state must be a converged archive with the same n, K, discretization and layout");
|
||||
std::ranges::copy(loaded->values(), initial_state->values().begin());
|
||||
std::println("Seeded from {} (converged at J = {})", args.seed_state.string(), *old_J);
|
||||
}
|
||||
|
||||
// Deduced here: clang does not deduce these class templates inside the generic lambda below.
|
||||
const auto normalization_options = normalization::NormalizationOptions{
|
||||
.scheme = normalization::LaneEmdenApproximation{.n = n, .central_density = radial->scales.central_density}};
|
||||
// 4-6. Solve and report with the chosen potential inverse; each choice is its own preconditioner type.
|
||||
const auto solve_and_report = [&](const auto potential_inverse) -> int {
|
||||
// 4. Freeze sensible numerical scales independently of the physical prescriptions.
|
||||
auto configuration = model::make_discretization(model::DiscretizationOptions{
|
||||
.model = stellar_model,
|
||||
.finite_elements = finite_elements,
|
||||
.normalization = normalization::make_normalization(normalization_options),
|
||||
.preconditioner = solver::preconditioner::make_primal_preconditioner(initial_state->layout(), potential_inverse)
|
||||
});
|
||||
|
||||
// 5. Select numerical work limits and observe accepted stars without managing evaluators or caches.
|
||||
const auto observe = [](const solver::SolveEvent& event, const auto&, const auto&) {
|
||||
if (event.kind == solver::EventKind::initial) {
|
||||
std::println("Initial normalized residual = {}", event.metric->residual_norm);
|
||||
} else if (event.kind == solver::EventKind::accepted) {
|
||||
std::println("Newton step {}: normalized residual = {}, accepted step = {}", event.iteration + 1, event.metric->residual_norm, event.step);
|
||||
}
|
||||
std::fflush(stdout);
|
||||
};
|
||||
const solver::SolverOptions options{
|
||||
.method = {.relative_tolerance = 1.0e-8, .absolute_tolerance = 1.0e-10, .maximum_iterations = 30,
|
||||
.linear_solve = {.relative_tolerance = 0.03, .maximum_iterations = 200}},
|
||||
.linear_solver = {.restart_length = 160}
|
||||
};
|
||||
solver::VolumeOptions volume_options{};
|
||||
if (args.quadrature_order) {
|
||||
namespace quadrature = serif::discretization::quadrature;
|
||||
quadrature::RuleSet rules;
|
||||
rules.set_role_control(quadrature::QuadratureRole::discretization, {.fixed_order = *args.quadrature_order});
|
||||
volume_options.quadrature_policy = quadrature::Policy{std::move(rules)};
|
||||
}
|
||||
const auto solution = [&] {
|
||||
if (!stream)
|
||||
return solver::solve_equilibrium(std::move(configuration), std::move(*initial_state), options, observe, volume_options);
|
||||
|
||||
auto problem = solver::backend::mfem::make_observed_stellar_problem(std::move(configuration), *initial_state,
|
||||
options.strategies, volume_options, serif::utils::dependencies::DependencyContext<>{},
|
||||
serif::utils::dependencies::NoGraphObservers{});
|
||||
serif::visualization::backend::mfem::StellarFrame frame(*problem.binding().discretization);
|
||||
auto& client = stream->client;
|
||||
constexpr std::uint64_t mesh_id = 1;
|
||||
// Frames are Newton iterates of one equilibrium, not moments in time.
|
||||
RayFEM::FrameSchema schema;
|
||||
schema.values = {
|
||||
{"Newton step", RayFEM::ValueType::Integer, true, "", "Accepted Newton steps so far; 0 is the initial state"},
|
||||
{"residual", RayFEM::ValueType::Real, false, "", "Normalized nonlinear residual"},
|
||||
{"step length", RayFEM::ValueType::Real, false, "", "Accepted damped Newton step length (1 is a full step)"},
|
||||
{"Omega", RayFEM::ValueType::Real, false, "", "Angular velocity of the rigid rotation"},
|
||||
{"virial ratio", RayFEM::ValueType::Real, false, "", "(2T + 3 integral of P)/|W|; 1 in equilibrium"},
|
||||
{"virial error", RayFEM::ValueType::Real, false, "", "(2T + W + 3 integral of P)/|W| at the finest quadrature; 0 in equilibrium"},
|
||||
{"mass", RayFEM::ValueType::Real, false, "M", "Integrated mass"},
|
||||
{"virial", RayFEM::ValueType::Text, false, "", "Why the vacuum virial could not be measured"}};
|
||||
schema.tags = {{"initial", "The seeded state before any Newton step"},
|
||||
{"resumed", "A state resumed from a checkpoint"},
|
||||
{"accepted", "An accepted Newton iterate"},
|
||||
{"converged", "Meets the solver's residual tolerances"}};
|
||||
schema.has_time = false;
|
||||
schema.primary = "Newton step";
|
||||
if (stream->continuation) {
|
||||
// Newton steps restart in each continuation step, so frames are ordered by step.
|
||||
schema.values.insert(schema.values.begin(), {
|
||||
{"continuation step", RayFEM::ValueType::Integer, true, "", "Continuation step; 0 is the widest separation"},
|
||||
{"separation", RayFEM::ValueType::Real, true, "R0", "Orbital separation of the companion"}});
|
||||
schema.primary = "continuation step";
|
||||
}
|
||||
// RayFEM orients the view and measures flattening about this axis; a binary rotates about the barycenter.
|
||||
auto axis_origin = rotation.center();
|
||||
if constexpr (Binary) {
|
||||
const double mass = stellar_model.template specification<model::FixedTotalMass>().target().value();
|
||||
axis_origin[0] += companion_mass * separation / (mass + companion_mass);
|
||||
}
|
||||
const auto& spin = rotation.axis();
|
||||
schema.axes = {{"rotation", {spin[0], spin[1], spin[2]}, {axis_origin[0], axis_origin[1], axis_origin[2]},
|
||||
Binary ? "Orbital rotation axis through the barycenter" : "Rigid rotation axis"}};
|
||||
// A locked binary's barycenter moves with the separation, so each step re-registers the schema.
|
||||
if (stream->enabled && !client.registerSchema(schema)) throw std::runtime_error(client.getLastError());
|
||||
if (stream->enabled && !stream->mesh_sent) {
|
||||
if (!client.sendMesh(mesh_id, frame.mesh())) throw std::runtime_error(client.getLastError());
|
||||
stream->mesh_sent = true;
|
||||
}
|
||||
double initial_residual = 0;
|
||||
const auto live_observe = [&](const solver::SolveEvent& event, const auto& state, const auto& system) {
|
||||
observe(event, state, system);
|
||||
if (!stream->enabled || (event.kind != solver::EventKind::initial && event.kind != solver::EventKind::resumed &&
|
||||
event.kind != solver::EventKind::accepted)) return;
|
||||
try {
|
||||
// StandardNewton has just evaluated this state. The cache is unsafe at finished
|
||||
// after a rejected trial, so only initial/resumed/accepted events are streamed.
|
||||
frame.update(state, problem.evaluator().cache());
|
||||
const std::uint32_t frame_index = stream->next_frame;
|
||||
RayFEM::FrameMetadata metadata{.frame_index = frame_index, .sim_time = 0.0};
|
||||
const double residual = event.metric->residual_norm;
|
||||
if (event.kind != solver::EventKind::accepted) initial_residual = residual;
|
||||
const int newton_step = event.kind == solver::EventKind::accepted ? event.iteration + 1 : 0;
|
||||
if (stream->continuation)
|
||||
std::snprintf(metadata.label, sizeof(metadata.label), "step %d a=%.4g Newton %d |R|=%.3g",
|
||||
stream->step, separation, newton_step, residual);
|
||||
else
|
||||
std::snprintf(metadata.label, sizeof(metadata.label), "n=%.3g J=%.3g Newton %u |R|=%.3g",
|
||||
n, args.angular_momentum, frame_index, residual);
|
||||
const double omega = state.template get<serif::discretization::blocks::rotation::angular_velocity>()[0];
|
||||
metadata.values = {{"Newton step", std::int64_t{newton_step}}, {"residual", residual}, {"Omega", omega}};
|
||||
if (stream->continuation) {
|
||||
metadata.values.push_back({"continuation step", std::int64_t{stream->step}});
|
||||
metadata.values.push_back({"separation", separation});
|
||||
}
|
||||
if (event.kind == solver::EventKind::accepted) metadata.values.push_back({"step length", event.step});
|
||||
// The physical check, independent of the algebraic residual.
|
||||
const auto& axis = rotation.axis();
|
||||
const auto virial = companion_mass > 0.0 ? std::expected<serif::tests::solver::VacuumVirialAssessment, std::string>{
|
||||
std::unexpect, "the vacuum virial omits the companion's potential"} :
|
||||
serif::tests::solver::vacuum_virial(*problem.binding().discretization, state, stellar_model.equation_of_state(),
|
||||
equations::RigidRotation{{omega*axis[0], omega*axis[1], omega*axis[2]}, rotation.center()});
|
||||
if (virial) {
|
||||
const auto& sample = virial->samples.back();
|
||||
metadata.values.push_back({"virial ratio", (2.0*sample.kinetic_energy + 3.0*sample.pressure_integral)/std::abs(sample.potential_energy)});
|
||||
metadata.values.push_back({"virial error", sample.energy_virial()});
|
||||
metadata.values.push_back({"mass", sample.mass});
|
||||
} else metadata.values.push_back({"virial", virial.error().substr(0, 1000)});
|
||||
metadata.tags = {event.kind == solver::EventKind::initial ? "initial" : event.kind == solver::EventKind::resumed ? "resumed" : "accepted"};
|
||||
if (residual <= options.method.absolute_tolerance || residual <= options.method.relative_tolerance*initial_residual)
|
||||
metadata.tags.push_back("converged");
|
||||
const auto send_start = std::chrono::steady_clock::now();
|
||||
if (!client.sendFrame(mesh_id, frame.fields(), metadata, &frame.displacement()))
|
||||
throw std::runtime_error(client.getLastError());
|
||||
const std::chrono::duration<double, std::milli> send_time = std::chrono::steady_clock::now() - send_start;
|
||||
std::println("RayFEM frame {} (queued in {:.2f} ms): interior elements {}, max nodal displacement {}, virial {}",
|
||||
frame_index, send_time.count(), frame.mesh().GetNE(), frame.displacement().Normlinf(),
|
||||
virial ? std::format("ratio {:.12g} error {:.3g}", (2.0*virial->samples.back().kinetic_energy +
|
||||
3.0*virial->samples.back().pressure_integral)/std::abs(virial->samples.back().potential_energy),
|
||||
virial->samples.back().energy_virial()) : "unavailable: " + virial.error());
|
||||
++stream->next_frame;
|
||||
std::fflush(stdout);
|
||||
} catch (const std::exception& error) {
|
||||
stream->enabled = false;
|
||||
std::println(stderr, "Live visualization disabled: {}", error.what());
|
||||
}
|
||||
};
|
||||
auto result = solver::solve_newton(problem, std::move(*initial_state), options, live_observe);
|
||||
// The last frames, the converged state among them, may still be queued.
|
||||
if (!client.flush(60000)) std::println(stderr, "RayFEM: queued frames were still unsent after 60 s");
|
||||
if (const auto queue = client.queueStats(); queue.failed)
|
||||
std::println(stderr, "RayFEM: {} queued packets were not sent", queue.failed);
|
||||
return result;
|
||||
}();
|
||||
if (!solution.converged()) {
|
||||
std::println(stderr, "Stellar solve stopped: {}", solution.message());
|
||||
if (solution.diagnostics().last_linear_solve) {
|
||||
const auto& linear = *solution.diagnostics().last_linear_solve;
|
||||
std::println(stderr, "Last linear solve: {} iterations, true relative residual {}", linear.iterations, linear.relative_true_residual_norm);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The result retains the physical definition, accepted coefficients and their matching system.
|
||||
using Omega = serif::discretization::blocks::rotation::angular_velocity;
|
||||
std::println("Converged after {} steps: Omega = {}, normalized residual = {}", solution.diagnostics().accepted_iterations,
|
||||
solution.state().template get<Omega>()[0], *solution.diagnostics().final_residual_norm);
|
||||
if (companion_mass > 0.0) {
|
||||
// Radii from the star's center toward the companion (+x), away from it, along the orbit (+y) and along the axis.
|
||||
namespace deformation_backend = serif::geometry::deformation::backend::mfem;
|
||||
const auto& state = solution.state();
|
||||
const auto inputs = deformation_backend::make_deformation_inputs(finite_elements.deformation.model,
|
||||
finite_elements.deformation.reference, state.template get<serif::discretization::blocks::surface_deformation::coefficients>());
|
||||
using Surface = std::remove_cvref_t<decltype(finite_elements.deformation.model.surface)>;
|
||||
auto cache = serif::utils::dependencies::make_computation_cache<serif::geometry::deformation::DeformationFailure>(
|
||||
deformation_backend::make_deformation_graph<Surface>(), inputs);
|
||||
if (!cache.try_prepare(inputs, serif::utils::dependencies::KeyList<serif::geometry::deformation::keys::VolumeDisplacement>{}))
|
||||
throw std::runtime_error("Cannot reconstruct the converged surface");
|
||||
serif::backend::mfem::serialize::detail::TopModelSampler sampler(finite_elements.template binding<Density>(),
|
||||
state.template get<Density>(), finite_elements.deformation.reference,
|
||||
cache.template get<serif::geometry::deformation::keys::VolumeDisplacement>(), rotation.axis(), {0.0, 0.0, 0.0});
|
||||
const auto radius = [&](double mu, double phi) { return sampler.surface_radius(mu, phi).value(); };
|
||||
if constexpr (Binary)
|
||||
std::println("Orbit: Omega / Omega_Kepler(point masses) = {:.9f}", solution.state().template get<Omega>()[0] / kepler_omega);
|
||||
else
|
||||
std::println("Spin: Omega_spin / Omega_orbit(Kepler, point masses) = {:.6f}", solution.state().template get<Omega>()[0] / kepler_omega);
|
||||
std::println("Radii: toward companion {:.6f}, away {:.6f}, along orbit {:.6f}, polar {:.6f}",
|
||||
radius(0.0, 0.0), radius(0.0, std::numbers::pi), radius(0.0, 0.5 * std::numbers::pi), radius(1.0, 0.0));
|
||||
if (!args.surface_table.empty()) {
|
||||
std::ofstream table(args.surface_table);
|
||||
table << "# theta_deg phi_deg radius (star center at the origin, companion at +x, separation " << separation << ")\n";
|
||||
for (int i = 0; i <= 90; ++i) {
|
||||
for (int j = 0; j < 90; ++j) {
|
||||
const double theta = 2.0 * i, phi = 4.0 * j;
|
||||
table << theta << ' ' << phi << ' ' << radius(std::cos(theta * std::numbers::pi / 180.0), phi * std::numbers::pi / 180.0) << '\n';
|
||||
}
|
||||
}
|
||||
std::println("Surface table written to {}", args.surface_table.string());
|
||||
}
|
||||
}
|
||||
if (converged) converged->assign(solution.state().values().begin(), solution.state().values().end());
|
||||
if (!args.top_export.empty()) {
|
||||
const auto exported = serif::utils::serialize::export_top(args.top_export, finite_elements, solution, args.top_options);
|
||||
if (!exported) {
|
||||
std::println(stderr, "TOP export failed at {}: {}", exported.error().path, exported.error().message);
|
||||
return 1;
|
||||
}
|
||||
std::println("TOP model written to {}: {} radial points, degree {}, angular frequency unit {}",
|
||||
exported->directory.string(), exported->radial_points, exported->maximum_degree, exported->angular_frequency_unit);
|
||||
std::println("TOP projection: surface error {}, enthalpy angular truncation {}, surface symmetry error {}, enthalpy symmetry error {}",
|
||||
exported->maximum_relative_surface_projection_error, exported->maximum_relative_enthalpy_projection_error,
|
||||
exported->maximum_relative_surface_symmetry_error,
|
||||
exported->maximum_relative_enthalpy_symmetry_error);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
if (use_lu(args)) return solve_and_report(solver::preconditioner::strategies::inverse::SparseDirect{});
|
||||
return solve_and_report(solver::preconditioner::strategies::inverse::FixedAMG{.cycles = 3});
|
||||
}
|
||||
|
||||
/** @brief Solve one star, or continue a binary inward in separation, seeding each solve with the previous star. */
|
||||
template <bool Binary>
|
||||
int run(const Arguments& args, const double K) {
|
||||
const auto stream = args.visualize ? LiveStream::open(args) : nullptr;
|
||||
if (args.separation_steps > 0) {
|
||||
if (stream) stream->continuation = true;
|
||||
std::vector<double> state;
|
||||
for (int step = 0; step <= args.separation_steps; ++step) {
|
||||
Arguments stage = args;
|
||||
stage.separation = args.separation_start * std::pow(args.separation / args.separation_start, static_cast<double>(step) / args.separation_steps);
|
||||
if (step < args.separation_steps) stage.surface_table.clear();
|
||||
if (stream) stream->step = step;
|
||||
std::println("Continuation step {} of {}: separation {}", step, args.separation_steps, stage.separation);
|
||||
const std::vector<double> seed = state;
|
||||
if (const int status = solve_star<Binary>(stage, K, &state, step ? &seed : nullptr, stream.get()); status != 0) return status;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return solve_star<Binary>(args, K, nullptr, nullptr, stream.get());
|
||||
}
|
||||
|
||||
/** @brief Show the complete route from physical equations to an owning converged stellar solution. */
|
||||
int main(int argc, char** argv) try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--help") {
|
||||
std::println("Usage: complete_sandbox [--angular-momentum J] [--surface-degree L]\n"
|
||||
" [--polytropic-index N] [--K K] [--surface-power-density | --ordinary-density]\n"
|
||||
" [--mesh-refinement N] [--surface-refinement N]\n"
|
||||
" [--equatorial-refinement N] [--equatorial-band-degrees D]\n"
|
||||
" [--radial-surface-refinement N] [--quadrature-order N | auto]\n"
|
||||
" [--potential-inverse auto | lu | amg]\n"
|
||||
" [--rotation-axis X Y Z] [--mesh-only] [--infinity-radius F] [--seed-state FILE]\n"
|
||||
" [--visualize | --no-visualize] [--viewer PATH] [--viewer-port PORT]\n"
|
||||
" [--export-top DIRECTORY] [--top-radial-points N] [--top-degree L]\n"
|
||||
" [--top-angular-points N] [--top-azimuthal-points N]\n"
|
||||
" [--companion-mass MC --separation A [--separation-start A0 --separation-steps N]\n"
|
||||
" [--surface-table FILE]]\n"
|
||||
"Defaults: the n=2, J=0.1 rotator (Omega/Omega_K ~0.70) compared with ESTER/TOP: n=2, J=0.1, L=12, SurfacePower density,\n"
|
||||
"surface refinement 1, equatorial refinement 1 with a 10 degree band, radial surface refinement 3,\n"
|
||||
"axis=(0,0,1), streamed live to RayFEM. G=M=1 and, unless --K is given, K makes the nonrotating radius 1.\n"
|
||||
"--angular-momentum 0.05 --surface-degree 8 gives the slower n=2, J=0.05 benchmark star.\n"
|
||||
"--surface-power-density (default) selects t^n reconstruction on surface-adjacent density elements;\n"
|
||||
"--ordinary-density selects the plain finite-element density.\n"
|
||||
"Radial surface refinement halves only the stellar-side surface shell, radially, per level.\n"
|
||||
"--quadrature-order fixes the volume quadrature order (default 11); auto uses SERiF's automatic rule.\n"
|
||||
"--potential-inverse lu factors the potential block exactly (build option sparse_direct); amg uses SERiF's\n"
|
||||
"3-cycle BoomerAMG; auto (default) uses lu when this build and machine support it, else amg.\n"
|
||||
"Surface degree controls harmonics; refinement controls mesh cells.\n"
|
||||
"Surface refinement adds isotropic interface layers; equatorial refinement adds latitude splits.\n"
|
||||
"Equatorial rings conservatively cover the requested band (0 < D <= 35 degrees).\n"
|
||||
"--infinity-radius places compactified infinity at F stellar radii (default 4); the vacuum map folds once the\n"
|
||||
"equatorial bulge reaches 1/(F-1), so stars flatter than ~0.33 need F < 4.\n"
|
||||
"--seed-state starts Newton from a converged profile_solve --save-state archive with the same n, K and\n"
|
||||
"discretization (any J): continuation for fast rotators that do not converge from the spherical seed.\n"
|
||||
"--mesh-only prints mesh counts and exits (no viewer). Viewer defaults: enabled, port 5555.\n"
|
||||
"Live frames show initial and accepted Newton states, not physical time.\n"
|
||||
"--export-top writes a converged polytrope to a new directory for TOP.\n"
|
||||
"--companion-mass MC --separation A adds a tidally locked point-mass companion on +x: rotation is about the\n"
|
||||
"barycenter and Omega follows from force balance (the star's center of mass stays at the origin); J is unused.\n"
|
||||
"--separation-start A0 --separation-steps N solves first at A0 (from the spherical seed), then at N separations\n"
|
||||
"stepping geometrically to A, each seeded by the previous star; every step streams to one RayFEM sequence,\n"
|
||||
"tagged with its continuation step and separation. Close or massive\n"
|
||||
"companions need this: from a sphere Newton stalls well inside the Roche lobe. Use --infinity-radius 3 near it.\n"
|
||||
"--asynchronous instead spins the star about its own axis at the rate --angular-momentum sets, with only the\n"
|
||||
"companion's tide acting (its uniform pull removed): the asynchronous Roche approximation.\n"
|
||||
"--surface-table FILE writes the converged binary's surface radius on a (theta, phi) grid (phi = 0 faces the companion).\n"
|
||||
"TOP defaults: 64 radial points, maximum even degree 16, 48 angular and 8 azimuthal samples.");
|
||||
return 0;
|
||||
}
|
||||
const auto args = parse_arguments(argc, argv);
|
||||
serif::initialize();
|
||||
const double K = args.polytropic_constant.value_or(unit_radius_polytropic_constant(args.polytropic_index));
|
||||
print_options(args, K);
|
||||
return args.companion_mass > 0.0 && !args.asynchronous ? run<true>(args, K) : run<false>(args, K);
|
||||
} catch (const std::exception& error) {
|
||||
std::println(stderr, "{}", error.what());
|
||||
return 1;
|
||||
}
|
||||
67
tests/sandbox/deformation_sandbox.cpp
Normal file
67
tests/sandbox/deformation_sandbox.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include <print>
|
||||
#include <vector>
|
||||
#include "serif/model/finite_elements.hpp"
|
||||
#include "serif/types.hpp"
|
||||
#include "serif/geometry/deformation/finite_elements.hpp"
|
||||
#include "serif/utils/dependencies/computation_cache.hpp"
|
||||
#include "polytropic_model.hpp"
|
||||
|
||||
/**
|
||||
* @brief Deform a stellar surface, extend its motion, and reuse the reference calculations for a second shape.
|
||||
*
|
||||
* The physical model chooses radial displacement lengths in normalized real harmonics, a quadratic interior response,
|
||||
* and fixed reference infinity. The owning discretization binds those choices to paired physical and logical meshes. Deformation reference computations wait until try_prepare. Coefficients are borrowed; finite_elements and shape_coefficients must outlive the inputs.
|
||||
* Changing model options requires rebuilding the lightweight inputs; changing the coefficient vector does not.
|
||||
*
|
||||
* The same computations can be appended to an equation graph with make_deformation_computations. They do not own another
|
||||
* preparation cache. SurfaceMetric is an optional separate reference calculation, independent of the volume extension.
|
||||
*
|
||||
* Low-level derivative actions use spans and an explicitly allocated workspace. They do not assemble a composite Jacobian.
|
||||
* For nonlinear surface or extension models the same interface includes the derivative of the force pullback.
|
||||
* This MFEM reference compiler currently supports conforming serial three-dimensional nodal spaces with byNODES displacement.
|
||||
*/
|
||||
int main() {
|
||||
namespace deformation = serif::geometry::deformation;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace keys = deformation::keys;
|
||||
|
||||
const deformation::DomainDeformation model{
|
||||
.surface = deformation::SphericalHarmonicSurface{.maximum_degree = 2},
|
||||
.stellar_interior = deformation::PowerLawRadialExtension{.radial_power = 2.0},
|
||||
.vacuum = deformation::FixedInfinityRadialExtension{}
|
||||
};
|
||||
std::vector<double> shape_coefficients(9, 0.0);
|
||||
shape_coefficients[0] = 0.02;
|
||||
const auto finite_elements = serif::model::make_stellar_discretization(serif::examples::make_polytropic_model(),
|
||||
{.geometry_order = 2}, model);
|
||||
const auto inputs = deformation::make_deformation_inputs(model, finite_elements.deformation.reference, shape_coefficients);
|
||||
auto cache = dependencies::make_computation_cache<deformation::DeformationFailure>(
|
||||
deformation::make_deformation_graph<deformation::SphericalHarmonicSurface>(), inputs);
|
||||
const auto prepared = cache.try_prepare(inputs);
|
||||
if (!prepared) {
|
||||
std::println(stderr, "Stellar deformation failed: {}", prepared.error().error.message);
|
||||
return 1;
|
||||
}
|
||||
std::println("{} shape coefficients produce {} volume displacement coefficients.", shape_coefficients.size(), cache.get<keys::VolumeDisplacement>().size());
|
||||
|
||||
const auto accepted_shape = cache.get<keys::Linearization>();
|
||||
auto workspace = deformation::make_deformation_workspace(*accepted_shape.map);
|
||||
std::vector<double> shape_direction(shape_coefficients.size(), 0.0);
|
||||
// Increasing degree, then signed order places the axisymmetric quadrupole Y(2, 0) at coordinate six.
|
||||
shape_direction[6] = 1.0;
|
||||
serif::Vector displacement_direction(static_cast<int>(accepted_shape.map->output_size()));
|
||||
if (const auto action = deformation::apply_jacobian(accepted_shape, shape_direction, displacement_direction, workspace); !action) {
|
||||
std::println(stderr, "Shape tangent failed: {}", action.error().message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
shape_coefficients[6] = 0.01;
|
||||
const auto updated = cache.try_prepare(inputs);
|
||||
if (!updated) {
|
||||
std::println(stderr, "Updated stellar deformation failed: {}", updated.error().error.message);
|
||||
return 1;
|
||||
}
|
||||
const bool reference_reused = updated->status<keys::RadialInterpolation>() == dependencies::PreparationStatus::reused;
|
||||
std::println("Reference radial interpolation reused after changing shape: {}.", reference_reused);
|
||||
return reference_reused ? 0 : 1;
|
||||
}
|
||||
292
tests/sandbox/dependencies_sandbox.cpp
Normal file
292
tests/sandbox/dependencies_sandbox.cpp
Normal file
@@ -0,0 +1,292 @@
|
||||
#include <print>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
|
||||
#include "serif/utils/dependencies/dependencies.hpp"
|
||||
|
||||
|
||||
/**
|
||||
* Demonstration code for how to use the SERiF computational graph system
|
||||
* The broad idea here is that there are many calculations which depend on each other but which may not necessarily always need to be recomputed just
|
||||
* because another part of the model has changed. We model this as a combination of an acyclic computational graph, defined at compile time, and
|
||||
* a runtime cache of values. The general workflow is that a developer defines the "context" of the computational graph, think of this
|
||||
* as all of the inputs to the graph, or maybe more specifically, anything that could change due to some external event (e.g. a time step, a newton step, etc...)
|
||||
* This context is probably the most CS intensive part of this code as the developer needs to think about lifetimes and ownership.
|
||||
*
|
||||
* **Context**
|
||||
* Contexts are made with the make_dependency_context function, this accepts a variadic pack of bindings between a type and a runtime value.
|
||||
* You can generate these yourself using the binding functions and types; however, we provide two convenience wrappers which generally developers should consider
|
||||
* using.
|
||||
* - borrow: Used to bind a type to a reference to a value. This is useful for values which are owned by the caller and which the caller will manage the lifetime of (takes an lvalue).
|
||||
* - own: Used to bind a type to a value which is owned by the context (copies an lvalue or moves an rvalue)
|
||||
* Each of these functions accepts a type as a template parameter and a runtime value as an argument. The type is used as an ID, this is key to understand, if we write
|
||||
* \code{.cpp}
|
||||
* using namespace serif::utils::dependencies;
|
||||
*
|
||||
* double x{1.0};
|
||||
* auto context = make_dependency_context(borrow<keys::Input>(x));
|
||||
* \endcode
|
||||
*
|
||||
* we are not saying that x is of type keys::Input, this is clearly not the case. Rather, we are registering in the computational graph a binding
|
||||
* between the type keys::Input and the runtime value x. Think of this as naming the various inputs to the graph.
|
||||
*
|
||||
* **Computations**
|
||||
* After defining a context we can define a computation, each computation defines a node, while its ordered input keys define the incoming edges.
|
||||
* Consider the following example (assuming the context from above):
|
||||
*
|
||||
* \code{.cpp}
|
||||
* auto func = make_computation<keys::Output, InputList<keys::Input>>(
|
||||
* [](const double _x) -> double {
|
||||
* return _x * _x;
|
||||
* }
|
||||
* );
|
||||
* \endcode
|
||||
*
|
||||
* what we have defined here is a node which takes a single input tagged keys::Input and produces an output tagged as keys::Output. The input key
|
||||
* declares an edge from that source to this node. Other nodes can use keys::Output as an input, and a successfully prepared result can be read with get
|
||||
* or retained with share. These connections allow us to form a larger graph. Consider now the following code
|
||||
*
|
||||
* \code{.cpp}
|
||||
* using namespace serif::utils::dependencies;
|
||||
*
|
||||
* struct tags {
|
||||
* struct X{};
|
||||
* struct Y{};
|
||||
*
|
||||
* struct Output1{};
|
||||
* struct Output2{};
|
||||
* };
|
||||
*
|
||||
* double x{1.0};
|
||||
* double y{2.0};
|
||||
* auto context = make_dependency_context(borrow<tags::X>(x), borrow<tags::Y>(y));
|
||||
*
|
||||
* auto func1 = make_computation<tags::Output1, InputList<tags::X>>(
|
||||
* [](const double _x) -> double {
|
||||
* return _x * _x;
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* auto func2 = make_computation<tags::Output2, InputList<tags::Output1, tags::Y>>(
|
||||
* [](const double _x, const double _y) -> double {
|
||||
* return _x + _y;
|
||||
* }
|
||||
* );
|
||||
* \endcode
|
||||
*
|
||||
* we have now defined two computations, the first takes a single input, whichever input is tagged as tags::X and produces an output tagged as tags::Output1.
|
||||
* The second computation takes two inputs, the first is the output of the first computation (lookup by the tag) and the second is whichever input is tagged as tags::Y,
|
||||
* and produces an output tagged as tags::Output2. Note that the order of arguments to the lambda must be the same as the order of the tags in the InputList, the
|
||||
* dependencies module will automatically order arguments based on the InputList. There is type checking to ensure that the types match, but if you have two
|
||||
* inputs of the same type that are flipped there is no way for the system to inspect that and warn you. Further, depending on your compiler and flags
|
||||
* there may be implicit casting between convertable types (e.g. int <-> double).
|
||||
*
|
||||
* @todo We may want to remove the ability to have implicit casting between types, this is a bit of a footgun and may lead to subtle bugs.
|
||||
*
|
||||
* @note This is primary goal of this system, split computations into small pieces with the ability to cache intermediate results and reuse
|
||||
* as many of those intermediate results as we can can prove is safe to do. The whole reason this system is so complex is that very purpose. There is a fundamental
|
||||
* problem with high dimensional stellar models, they are always going to be slow, therefore we need to accept some degree of complexity in order to make them tractable.
|
||||
*
|
||||
* **Computation Cache**
|
||||
* Once we have the computations we need to connect them together, generally use the helper function make_computation_cache which
|
||||
* takes a context and a variadic pack of computations. There are *a lot* of compile time checks which run to ensure that the graph is valid, the context
|
||||
* describes what is needed to compute the graph, and that each computation is valid. Generally if a static analysis error or compile error pops up here
|
||||
* it probably means there is a mistake in the definition of the context or the computations. I've tried to make these errors as clear as possible; however,
|
||||
* there is only so much that you can do with the C++ compiler. I recommend first checking, probably with a drawn-out diagram, that all the
|
||||
* tags are correct and what you expect.
|
||||
*
|
||||
* A developer can also choose to use the make_computation_graph function to explicitly name a graph. This
|
||||
* is useful when naming or reusing a collection of computations (make_computation_cache
|
||||
* calls make_computation_graph and then fixes the source types from the supplied context without retaining the context). Now consider the following code assuming the context and computations from above:
|
||||
*
|
||||
* \code{.cpp}
|
||||
* auto graph_cache = make_computation_cache(context, func1, func2);
|
||||
* \endcode
|
||||
*
|
||||
* At this point we have a cache which we can access various parts of; however, no computations have been run yet. To run a computation we must call try_prepare.
|
||||
*
|
||||
* try_prepare returns std::expected: ordinary rejection is a value, and no candidate results or snapshots are published on failure.
|
||||
* Exceptions from a callable or snapshot policy propagate with the same cache protection. Previously retained results remain valid,
|
||||
* but get and share require a successful current preparation. Always check the report before reading results.
|
||||
*
|
||||
* try_prepare takes the context and evaluates the requested nodes and their prerequisites, reusing unchanged results. By default every result
|
||||
* is requested, so the first call evaluates the entire graph. RequestedResults can restrict evaluation to a branch.
|
||||
*
|
||||
* \code{.cpp}
|
||||
* auto report = graph_cache.try_prepare(context);
|
||||
* if (!report) {
|
||||
* return; // Handle report.error() before reading the cache.
|
||||
* }
|
||||
*
|
||||
* double func1_result = graph_cache.template get<tags::Output1>();
|
||||
* double func2_result = graph_cache.template get<tags::Output2>();
|
||||
*
|
||||
* auto func1_status = report->template status<tags::Output1>();
|
||||
* auto func2_status = report->template status<tags::Output2>();
|
||||
*
|
||||
* std::string_view func1_status_string = preparation_status_name_map[func1_status]; // note this is an inline map defined in serif::utils::dependencies
|
||||
* std::string_view func2_status_string = preparation_status_name_map[func2_status];
|
||||
*
|
||||
* std::println("Func1 Result: {}, Func2 Result: {}, Func1 Status: {}, Func2 Status: {}", func1_result, func2_result, func1_status_string, func2_status_string);
|
||||
* \endcode
|
||||
*
|
||||
* Now what we could do is change only one part of the input context and call try_prepare again. Lets say we change only y, such that only func2 is stale.
|
||||
*
|
||||
* \code{.cpp}
|
||||
* y = 3.0;
|
||||
* report = graph_cache.try_prepare(context);
|
||||
* if (!report) {
|
||||
* return; // Handle report.error() before reading the cache.
|
||||
* }
|
||||
*
|
||||
* double func1_result = graph_cache.template get<tags::Output1>();
|
||||
* double func2_result = graph_cache.template get<tags::Output2>();
|
||||
*
|
||||
* auto func1_status = report->template status<tags::Output1>();
|
||||
* auto func2_status = report->template status<tags::Output2>();
|
||||
*
|
||||
* std::string_view func1_status_string = preparation_status_name_map[func1_status]; // note this is an inline map defined in serif::utils::dependencies
|
||||
* std::string_view func2_status_string = preparation_status_name_map[func2_status];
|
||||
*
|
||||
* std::println("Func1 Result: {}, Func2 Result: {}, Func1 Status: {}, Func2 Status: {}", func1_result, func2_result, func1_status_string, func2_status_string);
|
||||
* \endcode
|
||||
*/
|
||||
int main() {
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace keys = dependencies::keys;
|
||||
|
||||
struct Tags {
|
||||
struct Inputs {
|
||||
struct AT{};
|
||||
struct BT{};
|
||||
struct CT{};
|
||||
|
||||
struct MT{};
|
||||
struct IT{};
|
||||
};
|
||||
|
||||
struct Outputs {
|
||||
struct QuadOutput{};
|
||||
struct LinearOutput{};
|
||||
};
|
||||
};
|
||||
|
||||
double A = 1.0;
|
||||
double B = 2.0;
|
||||
double C = 3.0;
|
||||
double x = 1.0;
|
||||
double m = 2.0;
|
||||
double i = 3.0;
|
||||
|
||||
const auto graph_context = dependencies::make_dependency_context(
|
||||
dependencies::borrow<keys::Input>(x),
|
||||
dependencies::borrow<Tags::Inputs::AT>(A),
|
||||
dependencies::borrow<Tags::Inputs::BT>(B),
|
||||
dependencies::borrow<Tags::Inputs::CT>(C),
|
||||
dependencies::borrow<Tags::Inputs::MT>(m),
|
||||
dependencies::borrow<Tags::Inputs::IT>(i)
|
||||
);
|
||||
|
||||
const auto quadratic_function = dependencies::make_computation<Tags::Outputs::QuadOutput, dependencies::InputList<keys::Input, Tags::Inputs::AT, Tags::Inputs::BT, Tags::Inputs::CT>>(
|
||||
[](const double _x, const double i_A, const double i_B, const double i_C) -> double {
|
||||
double t = 0.0;
|
||||
for (int i = 0; i < 1000000; ++i) {
|
||||
t = _x * i_A;
|
||||
}
|
||||
return i_A * _x * _x + i_B * _x + i_C;
|
||||
}
|
||||
);
|
||||
const auto linear_function = dependencies::make_computation<Tags::Outputs::LinearOutput, dependencies::InputList<Tags::Outputs::QuadOutput, Tags::Inputs::MT, Tags::Inputs::IT>>(
|
||||
[](const double _x, const double _m, const double _b) -> double {
|
||||
double t = 0.0;
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
t = _x * _m;
|
||||
}
|
||||
return _m * _x + _b;
|
||||
}
|
||||
);
|
||||
|
||||
// The cache owns callbacks; the application owns accumulated measurements.
|
||||
using Clock = std::chrono::steady_clock;
|
||||
std::chrono::nanoseconds quadratic_elapsed{};
|
||||
std::size_t quadratic_executions = 0;
|
||||
std::size_t quadratic_consumptions = 0;
|
||||
double consumed_quadratic = 0.0;
|
||||
const auto observers = dependencies::make_observers(
|
||||
dependencies::observe_node<Tags::Outputs::QuadOutput>(
|
||||
[]() {
|
||||
std::println("Before quadratic node execution");
|
||||
return Clock::now();
|
||||
},
|
||||
[&](Clock::time_point& started, dependencies::NodeOutcome) noexcept {
|
||||
std::println("After quadratic node execution");
|
||||
quadratic_elapsed += std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now() - started);
|
||||
++quadratic_executions;
|
||||
}
|
||||
),
|
||||
dependencies::observe_node<Tags::Outputs::LinearOutput>(
|
||||
[]() {
|
||||
std::println("Before linear node execution");
|
||||
return 1;
|
||||
},
|
||||
[](int& r, dependencies::NodeOutcome result) noexcept {
|
||||
std::println("After linear node execution, result: {}", result);
|
||||
}
|
||||
),
|
||||
dependencies::observe_edge<Tags::Outputs::QuadOutput, Tags::Outputs::LinearOutput>(
|
||||
[&](const double& value) {
|
||||
consumed_quadratic = value;
|
||||
++quadratic_consumptions;
|
||||
}
|
||||
)
|
||||
);
|
||||
const auto graph = dependencies::make_computation_graph(quadratic_function, linear_function);
|
||||
auto graph_cache = dependencies::make_computation_cache(graph, graph_context, observers);
|
||||
|
||||
auto display_results = [](auto& report, auto& cache, auto& start_time, auto& end_time) -> void {
|
||||
if (!report) {
|
||||
std::println(stderr, "Computation failed: {}", report.error().error);
|
||||
return;
|
||||
}
|
||||
|
||||
const double result = cache.template get<Tags::Outputs::LinearOutput>();
|
||||
const double quad_result = cache.template get<Tags::Outputs::QuadOutput>();
|
||||
const dependencies::PreparationStatus linear_status = report->template status<Tags::Outputs::LinearOutput>();
|
||||
const dependencies::PreparationStatus quad_status = report->template status<Tags::Outputs::QuadOutput>();
|
||||
const auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end_time - start_time).count();
|
||||
|
||||
std::string_view linear_status_string = dependencies::preparation_status_name_map[linear_status];
|
||||
std::string_view quad_status_string = dependencies::preparation_status_name_map[quad_status];
|
||||
|
||||
std::println("Linear Result: {}, Quad Result: {}, Linear Status: {}, Quad Status: {} Duration: {} nanoseconds", result, quad_result, linear_status_string, quad_status_string, duration);
|
||||
};
|
||||
|
||||
auto start_time = std::chrono::high_resolution_clock::now();
|
||||
auto graph_report = graph_cache.try_prepare(graph_context);
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
display_results(graph_report, graph_cache, start_time, end_time);
|
||||
|
||||
// Change x and cause the entire graph to recompute
|
||||
x = 2.0;
|
||||
start_time = std::chrono::high_resolution_clock::now();
|
||||
graph_report = graph_cache.try_prepare(graph_context);
|
||||
end_time = std::chrono::high_resolution_clock::now();
|
||||
display_results(graph_report, graph_cache, start_time, end_time);
|
||||
|
||||
// Change just part of the linear function but allow it to reuse the quadratic result
|
||||
m = 3.0;
|
||||
start_time = std::chrono::high_resolution_clock::now();
|
||||
graph_report = graph_cache.try_prepare(graph_context);
|
||||
end_time = std::chrono::high_resolution_clock::now();
|
||||
display_results(graph_report, graph_cache, start_time, end_time);
|
||||
|
||||
// change nothing and allow the entire graph to reuse the previous results
|
||||
start_time = std::chrono::high_resolution_clock::now();
|
||||
graph_report = graph_cache.try_prepare(graph_context);
|
||||
end_time = std::chrono::high_resolution_clock::now();
|
||||
display_results(graph_report, graph_cache, start_time, end_time);
|
||||
|
||||
// The third request consumes a reused quadratic value; the fourth executes no hooks.
|
||||
std::println("Quadratic executions: {}, edge consumptions: {}, last consumed value: {}, total node time: {} nanoseconds",
|
||||
quadratic_executions, quadratic_consumptions, consumed_quadratic, quadratic_elapsed.count());
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
#include "serif/dimensions/runtime/concepts.hpp"
|
||||
#include <cstdio>
|
||||
|
||||
int main() {
|
||||
|
||||
@@ -1,53 +1,22 @@
|
||||
#include <print>
|
||||
|
||||
#include "serif/discretization/domain/schema/schemas.hpp"
|
||||
#include "mfem.hpp"
|
||||
#include "serif/discretization/domain/schema/validation/all.hpp"
|
||||
#include "serif/discretization/domain/schema/validation/results.hpp"
|
||||
#include "stroid/stroid.h"
|
||||
#include "serif/discretization/blocks/base.hpp"
|
||||
#include "serif/dimensions/runtime/runtime.hpp"
|
||||
#include "serif/model/finite_elements.hpp"
|
||||
#include "serif/backend/mfem/discretization/schema.hpp"
|
||||
#include "serif/discretization/forms/forms.hpp"
|
||||
#include "serif/discretization/forms/form_traits.hpp"
|
||||
#include "serif/discretization/blocks/concepts.hpp"
|
||||
#include "serif/discretization/forms/concept.hpp"
|
||||
#include "serif/discretization/forms/runtime.hpp"
|
||||
#include "serif/discretization/blocks/runtime.hpp"
|
||||
#include "serif/discretization/blocks/concepts.hpp"
|
||||
#include "serif/discretization/blocks/traits.hpp"
|
||||
#include "serif/discretization/blocks/concepts.hpp"
|
||||
#include "serif/utils/misc/concepts/enumeration.hpp"
|
||||
#include "serif/discretization/forms/operands.hpp"
|
||||
|
||||
#include "serif/utils/misc/concepts/filter.hpp"
|
||||
#include "polytropic_model.hpp"
|
||||
|
||||
/** @brief Build term-selected spaces and inspect their physical support on an owned stellar mesh. */
|
||||
int main() {
|
||||
using serif::discretization::domain::schema::validation::SchemaValidationResult;
|
||||
using serif::discretization::domain::schema::validation::validate_schema;
|
||||
using serif::discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
|
||||
using namespace serif::discretization::blocks;
|
||||
namespace blocks = serif::discretization::blocks;
|
||||
using Schema = serif::discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
|
||||
|
||||
const auto smesh_result = stroid::IO::LoadStroidMesh("sandbox.smesh");
|
||||
if (not smesh_result.has_value()) {
|
||||
throw std::runtime_error("Failed to load sandbox.smesh");
|
||||
}
|
||||
|
||||
const stroid::StroidMesh& smesh = smesh_result.value();
|
||||
|
||||
SchemaValidationResult result = validate_schema<CoreEnvelopeVacuumDomainSchema>(*smesh.mesh);
|
||||
const auto star = serif::examples::make_polytropic_model();
|
||||
const auto finite_elements = serif::model::make_stellar_discretization(star);
|
||||
const auto& density = finite_elements.template binding<blocks::density::mass>();
|
||||
const auto result = serif::backend::mfem::discretization::validate_schema<Schema>(density.mesh());
|
||||
std::println("{}", result);
|
||||
|
||||
std::println("{}", serif::discretization::forms::mass<serif::discretization::blocks::gravity::gradient, serif::discretization::blocks::gravity::gradient>::name);
|
||||
|
||||
using AllTerms = serif::utils::types::TypeList<
|
||||
density::mass,
|
||||
displacement::vector,
|
||||
surface_deformation::coefficients,
|
||||
surface_deformation::shape_equilibrium,
|
||||
gravity::potential,
|
||||
gravity::gradient,
|
||||
enthalpy::specific
|
||||
>;
|
||||
|
||||
using SpatialTerms = serif::utils::misc::concepts::FilterT<AllTerms, IsSpatialTerm>;
|
||||
}
|
||||
std::println("Density: {} coefficients; potential: {} coefficients", density.size(),
|
||||
finite_elements.template binding<blocks::gravity::potential>().size());
|
||||
std::println("{}", serif::discretization::forms::mass<blocks::gravity::potential, blocks::gravity::potential>::name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -6,19 +6,8 @@
|
||||
#include "serif/discretization/blocks/base.hpp"
|
||||
#include "serif/discretization/blocks/fields.hpp"
|
||||
#include "serif/discretization/domain/physical_domains.hpp"
|
||||
#include "serif/eos/runtime/details.hpp"
|
||||
#include "serif/eos/runtime/concepts.hpp"
|
||||
#include "serif/eos/runtime/views.hpp"
|
||||
int main() {
|
||||
const serif::eos::models::Polytrope polytrope(-1, 0.6);
|
||||
// std::println("Density: {}", serif::eos::evaluate<serif::dimensions::Density>(polytrope, serif::dimensions::SpecificEnthalpyValue{1.0}).value());
|
||||
// const serif::eos::runtime::EOSView view(polytrope);
|
||||
// const auto result = view.try_evaluate<serif::dimensions::Density>(serif::dimensions::SpecificEnthalpyValue{1.0});
|
||||
// if (result.has_value()) {
|
||||
// std::println("From runtime view: Density: {}", result->value());
|
||||
// } else {
|
||||
// std::println("From runtime view: EOS view is unavailable.");
|
||||
// }
|
||||
//
|
||||
// std::println("{}", serif::discretization::blocks::gravity::name);
|
||||
}
|
||||
48
tests/sandbox/equations_sandbox.cpp
Normal file
48
tests/sandbox/equations_sandbox.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include <cmath>
|
||||
#include <print>
|
||||
|
||||
#include "serif/eos/evaluation.hpp"
|
||||
#include "serif/eos/models/polytropic.hpp"
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/equations/surface_potential.hpp"
|
||||
|
||||
/** @brief Evaluate manufactured interior and surface samples and inspect their exact derivatives. */
|
||||
int main() {
|
||||
namespace equations = serif::equations;
|
||||
namespace dimensions = serif::dimensions;
|
||||
|
||||
const serif::eos::models::Polytrope eos{1.0, 0.8};
|
||||
const double density = 0.5, angular_velocity = 0.3, surface_level = 1.2;
|
||||
const dimensions::PressureValue surface_pressure{0.0};
|
||||
const equations::BarotropicDensityBalance density_balance{eos, surface_pressure};
|
||||
const equations::SurfacePotentialBalance surface;
|
||||
using Balance = decltype(density_balance);
|
||||
|
||||
// Density supplies the pressure potential through the EOS; it is not another solved field.
|
||||
const auto surface_density = serif::eos::evaluate<dimensions::MassDensity>(eos, surface_pressure);
|
||||
const double pressure_potential = serif::eos::evaluate<dimensions::BarotropicPressurePotential>(eos, dimensions::DensityValue{density}).value();
|
||||
const double surface_pressure_potential = serif::eos::evaluate<dimensions::BarotropicPressurePotential>(eos, surface_density).value();
|
||||
equations::EquationPoint<Balance::Operands> mechanics;
|
||||
mechanics.position = {0.3, 0.4, 0.0};
|
||||
mechanics.fields.get<Balance::Density>() = density;
|
||||
mechanics.fields.get<Balance::AngularVelocity>() = angular_velocity;
|
||||
mechanics.fields.get<Balance::SurfaceLevel>() = surface_level;
|
||||
mechanics.fields.get<Balance::Potential>() = surface_level - pressure_potential + surface_pressure_potential +
|
||||
angular_velocity * angular_velocity * density_balance.rotation().centrifugal_potential(mechanics.position);
|
||||
const auto interior_balance = density_balance.evaluate(mechanics);
|
||||
|
||||
equations::EquationPoint<decltype(surface)::Operands> boundary;
|
||||
boundary.position = mechanics.position;
|
||||
boundary.fields.get<decltype(surface)::Potential>() = surface_level +
|
||||
angular_velocity * angular_velocity * surface.rotation().centrifugal_potential(boundary.position);
|
||||
boundary.fields.get<decltype(surface)::SurfaceLevel>() = surface_level;
|
||||
boundary.fields.get<decltype(surface)::AngularVelocity>() = angular_velocity;
|
||||
const auto surface_balance = surface.evaluate(boundary);
|
||||
|
||||
std::println("Density balance residual = {:.3e}; angular-velocity derivative = {}",
|
||||
interior_balance.residual, interior_balance.field_derivatives.get<Balance::AngularVelocity>());
|
||||
std::println("Surface potential residual = {:.3e}; derivative with respect to surface level = {}",
|
||||
surface_balance.residual, surface_balance.field_derivatives.get<decltype(surface)::SurfaceLevel>());
|
||||
// complete_sandbox assembles these same physical equations on its finite-element mesh.
|
||||
return std::abs(interior_balance.residual) < 1.0e-12 && std::abs(surface_balance.residual) < 1.0e-12 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#include <print>
|
||||
|
||||
#include "serif/geometry/surface/isobaric.hpp"
|
||||
#include "serif/utils/dependencies/binding.hpp"
|
||||
#include "serif/utils/dependencies/key_list.hpp"
|
||||
#include "serif/geometry/deformation/deformation.hpp"
|
||||
|
||||
int main() {
|
||||
std::println("Hello, World!");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "serif/utils/laneemden/laneemden.hpp"
|
||||
#include <print>
|
||||
|
||||
int main() {
|
||||
const auto solution = serif::utils::laneemden::solve(-3.0, {
|
||||
.step = 0.01,
|
||||
.maximum_radius = 100.0,
|
||||
.maximum_steps = 10000,
|
||||
.surface_tolerance = 1e-10
|
||||
});
|
||||
|
||||
if (solution.has_value()) {
|
||||
std::println("{:xi}", *solution);
|
||||
} else {
|
||||
std::println("Lane-Emden error: {}", solution.error());
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,36 @@
|
||||
#include <cmath>
|
||||
#include <print>
|
||||
|
||||
#include "serif/mapping/context.hpp"
|
||||
#include "serif/mapping/variation.hpp"
|
||||
#include "serif/mapping/compactification/compactification.hpp"
|
||||
#include "serif/mapping/status.hpp"
|
||||
#include "serif/mapping/compactification/kelvin.hpp"
|
||||
#include "serif/mapping/domain_mapper.hpp"
|
||||
#include "serif/mapping/transformations.hpp"
|
||||
#include "serif/mapping/mapping.hpp"
|
||||
#include "serif/types.hpp"
|
||||
|
||||
#include "serif/utils/user_options/mapping/compactification/kelvin.hpp"
|
||||
/** @brief Map an exterior reference point toward physical infinity using the selected native vector storage. */
|
||||
int main() {
|
||||
std::println("Hello, World!");
|
||||
}
|
||||
namespace mapping = serif::mapping;
|
||||
namespace compactification = mapping::compactification;
|
||||
|
||||
const compactification::KelvinCompactification exterior({.r_star_ref = 1.0, .r_inf_ref = 4.0});
|
||||
serif::Vector position(3), coordinate_gradient(3);
|
||||
position = 0.0;
|
||||
position[0] = 2.0;
|
||||
coordinate_gradient = 0.0;
|
||||
coordinate_gradient[0] = 1.0 / 3.0;
|
||||
serif::DenseMatrix displacement_jacobian(3);
|
||||
displacement_jacobian = 0.0;
|
||||
for (int dimension = 0; dimension < 3; ++dimension) {
|
||||
displacement_jacobian(dimension, dimension) = 1.0;
|
||||
}
|
||||
const compactification::ExteriorMapInput input{
|
||||
.reference_position = position, .displaced_position = position,
|
||||
.displacement_jacobian = displacement_jacobian,
|
||||
.compactification_coordinate_gradient = coordinate_gradient,
|
||||
.compactification_coordinate = 1.0 / 3.0};
|
||||
compactification::ExteriorMapResult result;
|
||||
const auto status = exterior.evaluate(input, result);
|
||||
if (status != mapping::MappingStatus::VALID) {
|
||||
std::println(stderr, "Exterior mapping failed.");
|
||||
return 1;
|
||||
}
|
||||
std::println("Reference radius {} maps to physical radius {}", position[0], result.physical_position[0]);
|
||||
return std::isfinite(result.physical_position[0]) ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
sandboxes = [
|
||||
'rayfem_sandbox',
|
||||
'discritization_sandbox',
|
||||
'eos_sandbox',
|
||||
'finite_sandbox',
|
||||
'dimensions_sandbox',
|
||||
'quadrature_sandbox',
|
||||
'meta_sandbox',
|
||||
'mapping_sandbox'
|
||||
'mapping_sandbox',
|
||||
'equations_sandbox',
|
||||
'geometry_sandbox',
|
||||
'surface_sandbox',
|
||||
'deformation_sandbox',
|
||||
'solver_sandbox',
|
||||
'complete_sandbox',
|
||||
'normalization_sandbox',
|
||||
'preconditioner_sandbox',
|
||||
'model_sandbox',
|
||||
'lane-emden_sandbox',
|
||||
]
|
||||
|
||||
expected_errors = {}
|
||||
@@ -18,12 +29,36 @@ foreach entry : get_option('sandbox_expected_errors')
|
||||
endforeach
|
||||
|
||||
foreach sandbox: sandboxes
|
||||
sandbox_timeout = sandbox in ['solver_sandbox', 'complete_sandbox'] ? 3600 : 30
|
||||
sandbox_cpp_args = []
|
||||
sandbox_link_depends = []
|
||||
sandbox_dependencies = [serif_dep, nameof_dep]
|
||||
sandbox_includes = []
|
||||
if sandbox == 'complete_sandbox'
|
||||
viewer_path = ''
|
||||
if rayfem_project.get_variable('build_viewer')
|
||||
viewer_target = rayfem_project.get_variable('rayfem_viewer')
|
||||
viewer_path = viewer_target.full_path()
|
||||
sandbox_link_depends += [viewer_target]
|
||||
endif
|
||||
sandbox_cpp_args += ['-DSERIF_RAYFEM_EXECUTABLE="' + viewer_path + '"']
|
||||
# Streamed frames report the vacuum virial measured by the solver tests.
|
||||
sandbox_dependencies += [stroid_dep]
|
||||
sandbox_includes += [include_directories('../include')]
|
||||
endif
|
||||
sandbox_exe = executable(
|
||||
sandbox,
|
||||
sandbox + '.cpp',
|
||||
dependencies: [serif_dep, stroid_dep, nameof_dep],
|
||||
dependencies: sandbox_dependencies,
|
||||
include_directories: sandbox_includes,
|
||||
cpp_args: sandbox_cpp_args,
|
||||
link_depends: sandbox_link_depends,
|
||||
build_rpath: mfem_runtime_prefix == '' ? '' : mfem_runtime_prefix / 'lib',
|
||||
)
|
||||
if sandbox in ['solver_sandbox', 'complete_sandbox'] and not mfem_has_mpi
|
||||
# These examples select the default AMG inverse, which requires MPI/Hypre.
|
||||
continue
|
||||
endif
|
||||
if expected_errors.has_key(sandbox)
|
||||
test(
|
||||
sandbox, python_build,
|
||||
@@ -32,6 +67,26 @@ foreach sandbox: sandboxes
|
||||
suite: 'sandbox',
|
||||
)
|
||||
else
|
||||
test(sandbox, sandbox_exe, workdir: meson.project_source_root(), suite: 'sandbox')
|
||||
test(sandbox, sandbox_exe, workdir: meson.project_source_root(), suite: 'sandbox', timeout: sandbox_timeout)
|
||||
endif
|
||||
endforeach
|
||||
|
||||
# This example uses only the dependency headers, independently of MFEM and SERiF linking.
|
||||
dependencies_sandbox = executable(
|
||||
'dependencies_sandbox',
|
||||
'dependencies_sandbox.cpp',
|
||||
dependencies: [magic_enum_dep],
|
||||
include_directories: include_dir,
|
||||
)
|
||||
test('dependencies_sandbox', dependencies_sandbox, suite: 'sandbox')
|
||||
|
||||
# Demonstrate archive ownership and continuation without leaving a file in the source tree.
|
||||
serialization_sandbox = executable(
|
||||
'serialization_sandbox',
|
||||
'serialization_sandbox.cpp',
|
||||
dependencies: [serif_dep],
|
||||
build_rpath: mfem_runtime_prefix == '' ? '' : mfem_runtime_prefix / 'lib',
|
||||
)
|
||||
if mfem_has_mpi
|
||||
test('serialization_sandbox', serialization_sandbox, suite: 'sandbox', timeout: 1800, is_parallel: false)
|
||||
endif
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
int main() {
|
||||
using namespace serif::utils::errors;
|
||||
puts("Hello, world!\n");
|
||||
serif_assert<SERiFErrorCode::EOS_ERROR, "This is a message that should be seen at compile time. I am now going to make this very long to make sure that it does not get truncated. If you want to exercise this turn the following bool to false (generally set to true to allow compilation)", true>();
|
||||
serif_ensure<SERiFErrorCode::PRECONDITIONER_INCOMPATIBLE_STRATEGY, std::invalid_argument>(false, "This is a test error {}", "message");
|
||||
}
|
||||
67
tests/sandbox/model_sandbox.cpp
Normal file
67
tests/sandbox/model_sandbox.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include <numbers>
|
||||
#include <print>
|
||||
|
||||
#include "serif/eos/models/polytropic.hpp"
|
||||
#include "serif/equations/angular_momentum.hpp"
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/equations/surface_potential.hpp"
|
||||
#include "serif/equations/mass_normalization.hpp"
|
||||
#include "serif/equations/primal_gravity.hpp"
|
||||
#include "serif/equations/registry.hpp"
|
||||
#include "serif/model/description.hpp"
|
||||
#include "serif/model/eos_reference.hpp"
|
||||
#include "serif/geometry/surface/isobaric.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace model = serif::model;
|
||||
namespace equations = serif::equations;
|
||||
namespace dimensions = serif::dimensions;
|
||||
namespace surface = serif::geometry::surface;
|
||||
}
|
||||
|
||||
/** @brief Inspect an explicitly chosen rotating equilibrium formulation without creating a mesh or solving it. */
|
||||
int main()
|
||||
{
|
||||
// This list is the physical formulation. The EOS supplies relations; it does not choose equations.
|
||||
const auto equilibrium = [](const auto& physics)
|
||||
{
|
||||
const auto eos = model::borrow_eos(physics.equation_of_state());
|
||||
const auto& rotation = physics.template specification<model::FixedAngularMomentum>();
|
||||
|
||||
return model::EquilibriumEquations
|
||||
{
|
||||
.interior_equations = equations::EquationRegistry
|
||||
{
|
||||
equations::BarotropicDensityBalance{eos, dimensions::PressureValue{0.0}, rotation.axis(), rotation.center()},
|
||||
equations::PrimalPoissonBalance{4.0 * std::numbers::pi},
|
||||
equations::MassNormalization{physics.template specification<model::FixedTotalMass>().target().value()},
|
||||
equations::AngularMomentumConstraint{rotation.target().value(), rotation.axis(), rotation.center()}
|
||||
},
|
||||
.shape_force = equations::SurfacePotentialBalance{rotation.axis(), rotation.center()},
|
||||
.surface_condition = surface::Isobaric{dimensions::PressureValue{0.0}}
|
||||
};
|
||||
};
|
||||
|
||||
// Mass and angular momentum are prescribed. Density, potential, surface level, angular velocity and shape are solved.
|
||||
const auto star = model::StellarModel
|
||||
{
|
||||
serif::eos::models::Polytrope{3.0, 0.25},
|
||||
equilibrium,
|
||||
model::FixedTotalMass{dimensions::MassValue{1.0}},
|
||||
model::FixedAngularMomentum{dimensions::AngularMomentumValue{0.02}}
|
||||
};
|
||||
|
||||
// In this example G = 1 in the chosen physical units, and scalar Poisson balance supplies gravity.
|
||||
// Rigid rotation uses the same axis and origin in the density equation and on the pressure surface.
|
||||
// Resolving reads immutable physics once. Keep star alive while the resolved equations borrow its EOS.
|
||||
const auto chosen = model::resolve_equilibrium_equations(star);
|
||||
std::println("Interior equations:");
|
||||
chosen.interior_equations.for_each([](const auto& equation)
|
||||
{
|
||||
std::println(" {}", equation.name);
|
||||
});
|
||||
std::println("Shape equation: {}", chosen.shape_force.name);
|
||||
std::println("Surface pressure: {}", chosen.surface_condition.target_pressure().value());
|
||||
std::println("Prescribed mass: {}; angular momentum: {}", star.specification<model::FixedTotalMass>().target().value(), star.specification<model::FixedAngularMomentum>().target().value());
|
||||
}
|
||||
145
tests/sandbox/normalization_sandbox.cpp
Normal file
145
tests/sandbox/normalization_sandbox.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <print>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "serif/equations/mass_normalization.hpp"
|
||||
#include "serif/equations/primal_gravity.hpp"
|
||||
#include "serif/solver/normalization/normalization.hpp"
|
||||
#include "serif/utils/dependencies/computation_cache.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace normalization = serif::solver::normalization;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace dimensions = serif::dimensions;
|
||||
|
||||
/** @brief Convert illustrative physical coefficients and assembled residuals into one solve's fixed coordinates. */
|
||||
void show_coordinate_conversions(const normalization::DiagonalNormalization& coordinates, const std::array<double, 3>& physical_state, const std::array<double, 3>& physical_residual)
|
||||
{
|
||||
// These orders follow the layout below: [potential, surface potential level] and [Poisson balance, mass constraint].
|
||||
std::array<double, 3> normalized_state{};
|
||||
std::array<double, 3> normalized_residual{};
|
||||
coordinates.normalize_state(physical_state, normalized_state);
|
||||
coordinates.normalize_residual(physical_residual, normalized_residual);
|
||||
|
||||
std::println("Physical potential coefficients: [{:.6g}, {:.6g}]; surface potential level: {:.6g}", physical_state[0], physical_state[1], physical_state[2]);
|
||||
std::println("Normalized potential coefficients: [{:.6g}, {:.6g}]; surface potential level: {:.6g}", normalized_state[0], normalized_state[1], normalized_state[2]);
|
||||
std::println("Physical assembled Poisson residual: [{:.6g}, {:.6g}]; mass residual: {:.6g}", physical_residual[0], physical_residual[1], physical_residual[2]);
|
||||
std::println("Normalized Poisson residual: [{:.6g}, {:.6g}]; mass residual: {:.6g}", normalized_residual[0], normalized_residual[1], normalized_residual[2]);
|
||||
std::println("Residual norm in these coordinates: {:.6g}", std::sqrt(coordinates.residual_norm_squared(physical_residual)));
|
||||
|
||||
// Physical equations still consume physical coefficients. Convert a solver's coordinates back before evaluation.
|
||||
std::array<double, 3> recovered_state{};
|
||||
coordinates.denormalize_state(normalized_state, recovered_state);
|
||||
std::println("Recovered physical potential coefficients: [{:.6g}, {:.6g}]; surface potential level: {:.6g}", recovered_state[0], recovered_state[1], recovered_state[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Prepare, apply, reuse, and explicitly replace the normalization for a small selection of stellar coordinates.
|
||||
*
|
||||
* Potential and the surface potential level use GM/R, Poisson balance uses GM/R^3, and the mass constraint uses M.
|
||||
* PhysicalValueRule reads the unknown's Quantity; PhysicalResidualRule reads the equation's ResidualQuantity.
|
||||
* Those quantities need not agree, even when an equation closes a particular unknown.
|
||||
*
|
||||
* The illustrative metric contains two diagonal entries m_i = integral psi_i^2 dV_ref in the field's coefficient order.
|
||||
* A configured stellar discretization builds reference metrics in its computational graph automatically.
|
||||
* complete_sandbox demonstrates that path; this small example supplies a diagonal explicitly.
|
||||
* State factors are sqrt(m_i)/state_scale; assembled weak-form residual factors are 1/(sqrt(m_i)*residual_scale).
|
||||
* Algebraic coordinates use a unit metric. This is a diagonal metric, not the full mass-matrix norm.
|
||||
*
|
||||
* The example starts with supplied coefficients and assembled residuals; it does not assemble a complete stellar model.
|
||||
* Reference units must be consistent with those physical quantities and the reference geometry.
|
||||
*/
|
||||
int main()
|
||||
{
|
||||
using Potential = serif::discretization::blocks::gravity::potential;
|
||||
using SurfaceLevel = serif::discretization::blocks::barotropic_constant::mass_normalization;
|
||||
using PoissonBalance = serif::equations::PrimalPoissonBalance;
|
||||
using MassConstraint = serif::equations::MassNormalization;
|
||||
using PotentialMetric = normalization::ReferenceVolumeMetric<Potential>;
|
||||
|
||||
// The plan chooses physical scaling. The layout independently chooses the order and sizes of packed blocks.
|
||||
using Plan = normalization::NormalizationPlan<
|
||||
dependencies::Bindings<normalization::PhysicalValueRule<Potential>, normalization::PhysicalValueRule<SurfaceLevel>>,
|
||||
dependencies::Bindings<normalization::PhysicalResidualRule<PoissonBalance>, normalization::PhysicalResidualRule<MassConstraint>>>;
|
||||
const normalization::CoordinateLayout<dependencies::KeyList<Potential, SurfaceLevel>, dependencies::KeyList<PoissonBalance, MassConstraint>> layout{
|
||||
.state_sizes = {2, 1},
|
||||
.residual_sizes = {2, 1}
|
||||
};
|
||||
|
||||
const dimensions::QuantityValue<dimensions::Length> reference_radius{2.0};
|
||||
const dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant{1.0};
|
||||
auto reference = normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{8.0}, reference_radius, gravitational_constant);
|
||||
if (!reference)
|
||||
{
|
||||
std::println(stderr, "Cannot prepare reference scales: {}", reference.error());
|
||||
return 1;
|
||||
}
|
||||
normalization::StellarReferenceScales reference_scales = *reference;
|
||||
const std::vector<double> reference_metric{4.0, 9.0};
|
||||
|
||||
// Borrowed inputs must outlive the cache. Trial coefficients are absent: changing a candidate cannot change its units.
|
||||
const auto inputs = dependencies::make_dependency_context(
|
||||
dependencies::borrow<normalization::keys::Layout>(layout),
|
||||
dependencies::borrow<normalization::keys::Scales>(reference_scales),
|
||||
dependencies::borrow<PotentialMetric>(reference_metric));
|
||||
|
||||
// Factories only declare work. Both coordinate roles share this ordinary dependency cache.
|
||||
auto computations = normalization::make_normalization_computations<Plan>(layout);
|
||||
auto cache = std::apply([&](auto... nodes)
|
||||
{
|
||||
return dependencies::make_computation_cache(inputs, std::move(nodes)...);
|
||||
}, std::move(computations));
|
||||
|
||||
const auto prepared = cache.try_prepare(inputs);
|
||||
if (!prepared)
|
||||
{
|
||||
std::println(stderr, "Cannot prepare reference normalization: {}", prepared.error().error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const normalization::StellarReferenceScales& scales = reference_scales;
|
||||
std::println("Reference mass: {:.6g}; radius: {:.6g}; G: {:.6g}", scales.mass.value(), scales.radius.value(), scales.gravitational_constant.value());
|
||||
std::println("Potential and surface-potential scale: {:.6g}; Poisson scale: {:.6g}; mass scale: {:.6g}", scales.specific_energy.value(), scales.inverse_time_squared.value(), scales.mass.value());
|
||||
|
||||
// This owning copy fixes the coordinates for a solve, even if the cache later prepares another reference.
|
||||
const normalization::DiagonalNormalization solve_coordinates = cache.get<normalization::keys::Normalization>();
|
||||
const std::array physical_state{-8.0, -12.0, 6.0};
|
||||
const std::array physical_residual{0.2, -0.3, 0.4};
|
||||
show_coordinate_conversions(solve_coordinates, physical_state, physical_residual);
|
||||
|
||||
const auto unchanged = cache.try_prepare(inputs);
|
||||
if (!unchanged)
|
||||
{
|
||||
std::println(stderr, "Cannot reuse reference normalization: {}", unchanged.error().error);
|
||||
return 1;
|
||||
}
|
||||
std::println("Unchanged reference: {} computations repeated.", unchanged->computed_count());
|
||||
|
||||
// After this solve, explicitly choose a new reference mass for the next solve. The accepted copy remains valid.
|
||||
reference = normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{16.0}, reference_radius, gravitational_constant);
|
||||
if (!reference)
|
||||
{
|
||||
std::println(stderr, "Cannot prepare the next reference scales: {}", reference.error());
|
||||
return 1;
|
||||
}
|
||||
reference_scales = *reference;
|
||||
const auto updated = cache.try_prepare(inputs);
|
||||
if (!updated)
|
||||
{
|
||||
std::println(stderr, "Cannot prepare the next reference normalization: {}", updated.error().error);
|
||||
return 1;
|
||||
}
|
||||
const normalization::DiagonalNormalization next_solve_coordinates = cache.get<normalization::keys::Normalization>();
|
||||
std::array<double, 3> old_reference_state{};
|
||||
std::array<double, 3> new_reference_state{};
|
||||
solve_coordinates.normalize_state(physical_state, old_reference_state);
|
||||
next_solve_coordinates.normalize_state(physical_state, new_reference_state);
|
||||
std::println("After doubling reference mass, surface potential level in original coordinates: {:.6g}; next solve's coordinates: {:.6g}", old_reference_state[2], new_reference_state[2]);
|
||||
return 0;
|
||||
}
|
||||
44
tests/sandbox/polytropic_model.hpp
Normal file
44
tests/sandbox/polytropic_model.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
#include "serif/eos/models/polytropic.hpp"
|
||||
#include "serif/equations/angular_momentum.hpp"
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/equations/mass_normalization.hpp"
|
||||
#include "serif/equations/primal_gravity.hpp"
|
||||
#include "serif/equations/surface_potential.hpp"
|
||||
#include "serif/geometry/surface/isobaric.hpp"
|
||||
#include "serif/model/eos_reference.hpp"
|
||||
#include "serif/model/stellar_model.hpp"
|
||||
#include "serif/model/specifications/fixed_total_mass.hpp"
|
||||
#include "serif/model/specifications/fixed_angular_momentum.hpp"
|
||||
#include "serif/utils/error/errors.hpp"
|
||||
|
||||
namespace serif::examples {
|
||||
/** @brief Solve density mechanics, primal gravity and the isobar shape at fixed mass, EOS and angular momentum. */
|
||||
[[nodiscard]] inline auto make_polytropic_model(double n = 1.0, double K = 2.0 / std::numbers::pi, double mass = 1.0, double angular_momentum = 0.0, double G = 1.0, double surface_pressure = 0.0) {
|
||||
const auto equations = [G, surface_pressure](const auto& physics) {
|
||||
const auto eos = model::borrow_eos(physics.equation_of_state());
|
||||
const double index = physics.equation_of_state().polytropic_index();
|
||||
const auto& rotation = physics.template specification<model::FixedAngularMomentum>();
|
||||
// Resolve physical compatibility with the deferred equation definition, without restricting the EOS itself.
|
||||
utils::errors::serif_ensure<utils::errors::SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(
|
||||
!(index == 3.0 && rotation.target().value() == 0.0 && surface_pressure == 0.0),
|
||||
"Fixed K and M do not determine a unique nonrotating n=3 vacuum equilibrium: compatible mass leaves radius free, and incompatible mass has no solution.");
|
||||
return model::EquilibriumEquations{
|
||||
.interior_equations = serif::equations::EquationRegistry{
|
||||
serif::equations::BarotropicDensityBalance{eos, dimensions::PressureValue{surface_pressure}, rotation.axis(), rotation.center()},
|
||||
serif::equations::PrimalPoissonBalance{4.0 * std::numbers::pi * G},
|
||||
serif::equations::MassNormalization{physics.template specification<model::FixedTotalMass>().target().value()},
|
||||
serif::equations::AngularMomentumConstraint{rotation.target().value(), rotation.axis(), rotation.center()}
|
||||
},
|
||||
.shape_force = serif::equations::SurfacePotentialBalance{rotation.axis(), rotation.center()},
|
||||
.surface_condition = geometry::surface::Isobaric{dimensions::PressureValue{surface_pressure}}
|
||||
};
|
||||
};
|
||||
return model::make_stellar_model(eos::models::Polytrope{n, K}, equations,
|
||||
model::FixedTotalMass{dimensions::MassValue{mass}}, model::FixedAngularMomentum{dimensions::AngularMomentumValue{angular_momentum}});
|
||||
}
|
||||
|
||||
}
|
||||
102
tests/sandbox/preconditioner_sandbox.cpp
Normal file
102
tests/sandbox/preconditioner_sandbox.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <print>
|
||||
#include <utility>
|
||||
|
||||
#include "serif/types.hpp"
|
||||
#include "serif/operators/linearized_block.hpp"
|
||||
#include "serif/discretization/blocks/fields.hpp"
|
||||
#include "serif/solver/preconditioner/preconditioner.hpp"
|
||||
#include "serif/utils/dependencies/computation_cache.hpp"
|
||||
|
||||
namespace {
|
||||
/** @brief Make the complete accepted Jacobian of this illustrative two-coordinate example. */
|
||||
[[nodiscard]] std::shared_ptr<const serif::DenseMatrix> material_jacobian(double density_response) {
|
||||
auto matrix = std::make_shared<serif::DenseMatrix>(2);
|
||||
(*matrix)(0, 0) = density_response;
|
||||
(*matrix)(0, 1) = 3.0;
|
||||
(*matrix)(1, 0) = 0.0;
|
||||
(*matrix)(1, 1) = 4.0;
|
||||
return matrix;
|
||||
}
|
||||
|
||||
/** @brief Publish one frozen physical block; real discretized equations supply volume_field_block and pressure_field_block instead. */
|
||||
[[nodiscard]] serif::operators::LinearizedBlock scalar_block(double entry) {
|
||||
auto matrix = std::make_shared<serif::DenseMatrix>(1);
|
||||
(*matrix)(0, 0) = entry;
|
||||
return serif::operators::linearized_block(std::shared_ptr<const serif::DenseMatrix>(std::move(matrix)));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/** @brief Demonstrate public preparation, reuse, MFEM application, and accepted-factor ownership with a transparent 2x2 system. */
|
||||
int main() {
|
||||
namespace preconditioner = serif::solver::preconditioner;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace inverse = preconditioner::strategies::inverse;
|
||||
namespace coupling = preconditioner::strategies::coupling;
|
||||
using Density = serif::discretization::blocks::density::mass;
|
||||
using Potential = serif::discretization::blocks::gravity::potential;
|
||||
|
||||
std::println("Illustrative 2x2 material block; these coefficients do not define a stellar model.");
|
||||
// Correct potential first, then subtract its response from the density equation.
|
||||
const auto density = preconditioner::make_preconditioner<Density>(inverse::Diagonal{});
|
||||
const auto potential = preconditioner::make_preconditioner<Potential>(inverse::Diagonal{});
|
||||
const auto choice = preconditioner::couple(density, potential, coupling::SecondThenFirst{});
|
||||
using Recipe = decltype(choice);
|
||||
using DensityBlock = preconditioner::keys::Block<dependencies::KeyList<Density>, dependencies::KeyList<Density>>;
|
||||
|
||||
// Physical blocks and the full Jacobian come from one accepted state, in (density, potential) order.
|
||||
auto accepted_jacobian = material_jacobian(2.0);
|
||||
auto density_block = scalar_block((*accepted_jacobian)(0, 0));
|
||||
const auto context = dependencies::make_dependency_context(
|
||||
dependencies::borrow<DensityBlock>(density_block),
|
||||
preconditioner::physical_block<Potential, Potential>(scalar_block((*accepted_jacobian)(1, 1))),
|
||||
preconditioner::physical_block<Density, Potential>(scalar_block((*accepted_jacobian)(0, 1))));
|
||||
|
||||
// Describing a graph performs no numerical setup; preparation explicitly accepts its inputs.
|
||||
const auto graph = preconditioner::make_preconditioner_graph(choice);
|
||||
auto cache = dependencies::make_computation_cache<preconditioner::Failure>(graph, context);
|
||||
const auto prepared = cache.try_prepare(context);
|
||||
if (!prepared) {
|
||||
std::println(stderr, "Cannot prepare material preconditioner: {}", prepared.error().error.message);
|
||||
return 1;
|
||||
}
|
||||
std::println("Initial preparation: {} computations", prepared->computed_count());
|
||||
|
||||
// Sharing fixes the numerical factors for this solve, even after the cache accepts another state.
|
||||
const auto frozen = cache.share<Recipe::Result>();
|
||||
preconditioner::Solver original_solver(*frozen, accepted_jacobian);
|
||||
original_solver.SetOperator(*accepted_jacobian);
|
||||
serif::Vector residual(2), correction(2);
|
||||
residual[0] = 10.0;
|
||||
residual[1] = 8.0;
|
||||
correction = 99.0;
|
||||
original_solver.Mult(residual, correction);
|
||||
std::println("J = [[2, 3], [0, 4]], residual = [10, 8]: correction = [{:.3f}, {:.3f}]", correction[0], correction[1]);
|
||||
|
||||
const auto unchanged = cache.try_prepare(context);
|
||||
if (!unchanged) {
|
||||
std::println(stderr, "Cannot reuse material preconditioner: {}", unchanged.error().error.message);
|
||||
return 1;
|
||||
}
|
||||
std::println("Unchanged accepted state: {} computations", unchanged->computed_count());
|
||||
|
||||
// Replacing an accepted block changes its identity; the shared dependency engine refreshes only its consumers.
|
||||
accepted_jacobian = material_jacobian(4.0);
|
||||
density_block = scalar_block((*accepted_jacobian)(0, 0));
|
||||
const auto refreshed = cache.try_prepare(context);
|
||||
if (!refreshed) {
|
||||
std::println(stderr, "Cannot refresh material preconditioner: {}", refreshed.error().error.message);
|
||||
return 1;
|
||||
}
|
||||
const auto current = cache.share<Recipe::Result>();
|
||||
preconditioner::Solver current_solver(*current, accepted_jacobian);
|
||||
current_solver.SetOperator(*accepted_jacobian);
|
||||
current_solver.Mult(residual, correction);
|
||||
std::println("Density response 2 -> 4: {} computations; correction = [{:.3f}, {:.3f}]", refreshed->computed_count(), correction[0], correction[1]);
|
||||
|
||||
// The old adapter still owns both its original Jacobian and original inverse; never rebind it to the new Jacobian.
|
||||
original_solver.Mult(residual, correction);
|
||||
std::println("Original frozen solve remains: [{:.3f}, {:.3f}]", correction[0], correction[1]);
|
||||
return 0;
|
||||
}
|
||||
@@ -3,24 +3,19 @@
|
||||
|
||||
#include "serif/discretization/forms/concept.hpp"
|
||||
#include "serif/discretization/forms/forms.hpp"
|
||||
#include "serif/discretization/quadrature/rules.hpp"
|
||||
#include "serif/discretization/quadrature/policy.hpp"
|
||||
#include "serif/discretization/quadrature/backend/mfem/mfem_static_order.hpp"
|
||||
#include "serif/discretization/quadrature/backend/order.hpp"
|
||||
#include "serif/discretization/quadrature/backend/mfem/concepts.hpp"
|
||||
#include "serif/discretization/quadrature/backend/mfem/configure.hpp"
|
||||
#include "serif/discretization/quadrature/backend/mfem/mfem_resolver.hpp"
|
||||
|
||||
#include <print>
|
||||
|
||||
/** @brief Compare quadrature policies; the deliberately negative order demonstrates input validation. */
|
||||
int main() {
|
||||
using namespace serif::discretization::quadrature;
|
||||
using namespace serif::discretization::domain;
|
||||
using namespace serif::discretization::forms;
|
||||
|
||||
using GravityMassForm = divergence<serif::discretization::blocks::gravity::gradient, serif::discretization::blocks::gravity::gradient>;
|
||||
using PotentialMassForm = mass<serif::discretization::blocks::gravity::potential, serif::discretization::blocks::gravity::potential>;
|
||||
|
||||
auto query = make_query<GravityMassForm>(
|
||||
auto query = make_query<PotentialMassForm>(
|
||||
QuadratureRole::discretization,
|
||||
AllDomains{},
|
||||
MappingKind::affine,
|
||||
@@ -63,7 +58,7 @@ int main() {
|
||||
}
|
||||
);
|
||||
|
||||
boostedRuleSet.set_form_control<GravityMassForm>(
|
||||
boostedRuleSet.set_form_control<PotentialMassForm>(
|
||||
RuleControl{
|
||||
.fixed_order = std::nullopt,
|
||||
.boost = 3
|
||||
@@ -99,7 +94,7 @@ int main() {
|
||||
}
|
||||
);
|
||||
|
||||
fixedRuleSet.set_form_control<GravityMassForm>(
|
||||
fixedRuleSet.set_form_control<PotentialMassForm>(
|
||||
RuleControl{
|
||||
.fixed_order = 11,
|
||||
.boost = 3
|
||||
|
||||
25
tests/sandbox/rayfem_sandbox.cpp
Normal file
25
tests/sandbox/rayfem_sandbox.cpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#include <cmath>
|
||||
#include <print>
|
||||
|
||||
#include "mfem.hpp"
|
||||
#include "libRayfem/io/serif.hpp"
|
||||
#include "libRayfem/mesh/converter.hpp"
|
||||
|
||||
/** @brief Exercise the visualization dependency without opening a window or running a solve. */
|
||||
int main() {
|
||||
auto mesh = mfem::Mesh::MakeCartesian2D(2, 2, mfem::Element::QUADRILATERAL, true, 1.0, 1.0);
|
||||
mfem::H1_FECollection collection(2, 2);
|
||||
mfem::FiniteElementSpace space(&mesh, &collection);
|
||||
mfem::GridFunction field(&space);
|
||||
mfem::FunctionCoefficient coefficient([](const mfem::Vector& x) { return x[0] + 2.0 * x[1]; });
|
||||
field.ProjectCoefficient(coefficient);
|
||||
|
||||
const auto surface = RayFEM::BuildSurface(mesh, &field);
|
||||
if (!RayFEM::SerifLoadingAvailable() || surface.values.empty() ||
|
||||
std::abs(surface.min_value) > 1.0e-12 || std::abs(surface.max_value - 3.0) > 1.0e-12) {
|
||||
std::println(stderr, "RayFEM could not evaluate the parent MFEM field or load SERiF archives.");
|
||||
return 1;
|
||||
}
|
||||
std::println("RayFEM evaluated the MFEM field through serif_dep; SERiF archive support is available.");
|
||||
return 0;
|
||||
}
|
||||
86
tests/sandbox/serialization_sandbox.cpp
Normal file
86
tests/sandbox/serialization_sandbox.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <print>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include "serif/runtime.hpp"
|
||||
#include "serif/model/finite_elements.hpp"
|
||||
#include "serif/backend/mfem/serialize/configuration.hpp"
|
||||
#include "serif/solver/equilibrium.hpp"
|
||||
#include "serif/solver/seed/state.hpp"
|
||||
#include "serif/solver/seed/polytrope.hpp"
|
||||
#include "polytropic_model.hpp"
|
||||
#include "serif/utils/serialize/archive.hpp"
|
||||
#include "serif/utils/serialize/solver.hpp"
|
||||
|
||||
namespace {
|
||||
/** @brief Keep this example's error reporting separate from its save-and-resume workflow. */
|
||||
template <typename T, typename Error>
|
||||
T checked(std::expected<T, Error> result) {
|
||||
if (!result) {
|
||||
if constexpr (requires { result.error().path; result.error().message; }) {
|
||||
serif::utils::errors::serif_ensure<serif::utils::errors::SERiFErrorCode::INVALID_ARGUMENT>(false,
|
||||
"{}: {}", result.error().path, result.error().message);
|
||||
} else {
|
||||
serif::utils::errors::serif_ensure<serif::utils::errors::SERiFErrorCode::INVALID_ARGUMENT>(false, "{}", result.error());
|
||||
}
|
||||
}
|
||||
if constexpr (!std::is_void_v<T>) {
|
||||
return std::move(*result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Stop at an accepted Newton boundary, save the star, and continue with newly reconstructed resources. */
|
||||
int main(int argc, char** argv) {
|
||||
namespace solver = serif::solver;
|
||||
namespace serialization = serif::utils::serialize;
|
||||
namespace configuration_io = serif::utils::serialize;
|
||||
|
||||
serif::initialize();
|
||||
|
||||
// const std::filesystem::path path = argc > 1 ? argv[1] : "stellar_checkpoint.serif";
|
||||
//
|
||||
// const auto star = serif::examples::make_polytropic_model();
|
||||
// const auto radial = checked(solver::seed::make_polytropic_seed(star));
|
||||
// const auto finite_elements = serif::model::make_stellar_discretization(star,
|
||||
// {.radius = radial.scales.radius},
|
||||
// serif::geometry::deformation::DomainDeformation<serif::geometry::deformation::SphericalHarmonicSurface>{
|
||||
// .surface = {.maximum_degree = 0}, .stellar_interior = {}, .vacuum = {}});
|
||||
// const auto initial = checked(solver::seed::make_polytropic_state(star, finite_elements, radial));
|
||||
// const solver::normalization::NormalizationOptions normalization{
|
||||
// .scheme = checked(solver::normalization::prepare_stellar_reference_scales(
|
||||
// serif::dimensions::MassValue{1.0}, serif::dimensions::LengthValue{1.0}))};
|
||||
// const auto configuration = serif::model::make_discretization(serif::model::DiscretizationOptions{
|
||||
// .model = star, .finite_elements = finite_elements, .normalization = normalization});
|
||||
// const solver::SolverOptions options{
|
||||
// .linear_solver = {
|
||||
// .print_level = 1
|
||||
// }
|
||||
// };
|
||||
// const auto stop_after_step = [](const solver::SolveEvent& event, const auto&, const auto&) {
|
||||
// return event.kind == solver::EventKind::accepted ? solver::SolveControl::stop : solver::SolveControl::continue_solve;
|
||||
// };
|
||||
// const auto stopped = solver::solve_equilibrium(configuration, initial, options, stop_after_step);
|
||||
//
|
||||
// // Each component remains independently readable. No cache, Jacobian or preconditioner is written.
|
||||
// checked(serialization::write(path, serialization::pack(configuration, options, stopped),
|
||||
// {.author = "SERiF example", .comment = "Accepted Newton step; ready to continue"}));
|
||||
//
|
||||
// auto reader = checked(serialization::Reader::open(path));
|
||||
// using Configuration = std::remove_cvref_t<decltype(configuration)>;
|
||||
// const auto restored = checked(configuration_io::restore_configuration<Configuration>(checked(reader.node("configuration"))));
|
||||
// const auto checkpoint = checked(reader.read<decltype(stopped.checkpoint())>("solution/checkpoint"));
|
||||
// const auto restored_options = checked(reader.read<std::remove_cvref_t<decltype(options)>>());
|
||||
// const auto resumed = solver::resume_equilibrium(restored, checkpoint, restored_options);
|
||||
// std::println("{} ({} accepted iterations, including those before saving)", resumed.message(), resumed.diagnostics().accepted_iterations);
|
||||
// return resumed.converged() ? 0 : 1;
|
||||
auto result = serif::utils::serialize::Reader::open("stellar_checkpoint.serif");
|
||||
if (result.has_value()) {
|
||||
std::println("Opened checkpoint from SERiF version: {}", result->serif_version());
|
||||
} else {
|
||||
std::println("Failed to open checkpoint: {} ({})", result.error().message, result.error().path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#include <print>
|
||||
|
||||
#include "serif/runtime.hpp"
|
||||
#include "serif/model/finite_elements.hpp"
|
||||
#include "serif/solver/equilibrium.hpp"
|
||||
#include "serif/solver/seed/state.hpp"
|
||||
#include "polytropic_model.hpp"
|
||||
|
||||
/** @brief Solve one nonrotating polytrope with the default Newton, normalization, and inverse choices. */
|
||||
int main() {
|
||||
serif::initialize();
|
||||
namespace solver = serif::solver;
|
||||
namespace normalization = solver::normalization;
|
||||
|
||||
// Physical prescriptions stay fixed while Newton changes the numerical state.
|
||||
const auto model = serif::examples::make_polytropic_model();
|
||||
const auto radial = solver::seed::make_polytropic_seed(model);
|
||||
if (!radial) {
|
||||
std::println(stderr, "Cannot prepare the radial star: {}", radial.error());
|
||||
return 1;
|
||||
}
|
||||
const auto finite_elements = serif::model::make_stellar_discretization(model,
|
||||
{.radius = radial->scales.radius},
|
||||
serif::geometry::deformation::DomainDeformation<serif::geometry::deformation::SphericalHarmonicSurface>{
|
||||
.surface = {.maximum_degree = 2}, .stellar_interior = {}, .vacuum = {}});
|
||||
auto initial = solver::seed::make_polytropic_state(model, finite_elements, *radial);
|
||||
if (!initial) {
|
||||
std::println(stderr, "Cannot project the initial star: {}", initial.error());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The reference is numerical bookkeeping; it does not constrain central density.
|
||||
auto configuration = serif::model::make_discretization(serif::model::DiscretizationOptions{
|
||||
.model = model,
|
||||
.finite_elements = finite_elements,
|
||||
.normalization = normalization::make_normalization(normalization::NormalizationOptions{
|
||||
.scheme = normalization::LaneEmdenApproximation{.n = 1.0, .central_density = radial->scales.central_density}})
|
||||
});
|
||||
const auto result = solver::solve_equilibrium(std::move(configuration), std::move(*initial));
|
||||
std::println("{}; accepted Newton steps: {}; final normalized residual: {}", result.message(),
|
||||
result.diagnostics().accepted_iterations, result.diagnostics().final_residual_norm.value_or(-1.0));
|
||||
if (!result.converged() && result.diagnostics().last_linear_solve) {
|
||||
const auto& linear = *result.diagnostics().last_linear_solve;
|
||||
std::println(stderr, "Last linear solve: {} iterations, true relative residual {}", linear.iterations, linear.relative_true_residual_norm);
|
||||
}
|
||||
return result.converged() ? 0 : 1;
|
||||
}
|
||||
|
||||
152
tests/sandbox/stellar_rayfem.hpp
Normal file
152
tests/sandbox/stellar_rayfem.hpp
Normal file
@@ -0,0 +1,152 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "mfem/fem/gridfunc.hpp"
|
||||
#include "mfem/mesh/submesh/submesh.hpp"
|
||||
#include "mfem/mesh/submesh/transfermap.hpp"
|
||||
#include "libRayfem/network/client.hpp"
|
||||
#include "serif/backend/mfem/discretization/field_values.hpp"
|
||||
#include "serif/backend/mfem/model/volume_displacement.hpp"
|
||||
#include "serif/discretization/blocks/fields.hpp"
|
||||
|
||||
namespace serif::visualization::backend::mfem {
|
||||
/**
|
||||
* @brief Own visualization copies on the stellar interior's reference submesh.
|
||||
* @note update() requires the evaluator cache for this exact state. For StandardNewton,
|
||||
* use initial/resumed/accepted events, never finished or a rejected trial. The existing
|
||||
* nodal displacement is sent separately; neither the solver mesh nor state is changed.
|
||||
* Reconstructed density is sampled into a higher-order discontinuous display field. This
|
||||
* polynomial display approximation never enters the solve or physical diagnostics.
|
||||
*/
|
||||
template <typename Discretization>
|
||||
class StellarFrame {
|
||||
using Density = discretization::blocks::density::mass;
|
||||
using Potential = discretization::blocks::gravity::potential;
|
||||
using Displacement = discretization::blocks::displacement::vector;
|
||||
using Shape = discretization::blocks::surface_deformation::coefficients;
|
||||
|
||||
Discretization m_fe;
|
||||
::mfem::GridFunction m_parent_density, m_parent_potential, m_parent_displacement;
|
||||
::mfem::SubMesh m_mesh;
|
||||
std::unique_ptr<::mfem::L2_FECollection> m_display_density_collection;
|
||||
::mfem::FiniteElementSpace m_density_space, m_potential_space, m_displacement_space;
|
||||
::mfem::GridFunction m_density, m_potential, m_displacement;
|
||||
std::unique_ptr<::mfem::TransferMap> m_density_transfer;
|
||||
::mfem::TransferMap m_potential_transfer, m_displacement_transfer;
|
||||
|
||||
/** @brief Extract the material mesh while preserving parent element reference coordinates. */
|
||||
static ::mfem::SubMesh interior_mesh(const Discretization& fe) {
|
||||
const auto* parent = fe.template binding<Displacement>().space().GetMesh();
|
||||
if (fe.template binding<Density>().space().GetMesh() != parent ||
|
||||
fe.template binding<Potential>().space().GetMesh() != parent) {
|
||||
throw std::invalid_argument("Stellar visualization requires a common parent mesh");
|
||||
}
|
||||
::mfem::Array<int> attributes;
|
||||
for (int attribute : fe.deformation.reference.stellar_attributes) attributes.Append(attribute);
|
||||
if (attributes.Size() == 0) throw std::invalid_argument("No stellar material attributes to visualize");
|
||||
return ::mfem::SubMesh::CreateFromDomain(*parent, attributes);
|
||||
}
|
||||
|
||||
/** @brief Use additional display nodes only when an ordinary GridFunction cannot represent density. */
|
||||
static std::unique_ptr<::mfem::L2_FECollection> density_display_collection(const Discretization& fe) {
|
||||
const auto& binding = fe.template binding<Density>();
|
||||
if (binding.reconstruction() == nullptr) return {};
|
||||
return std::make_unique<::mfem::L2_FECollection>(binding.space().GetMaxElementOrder() + 3, 3, ::mfem::BasisType::GaussLobatto);
|
||||
}
|
||||
|
||||
/** @brief Copy supported physical coefficients into an ordinary visualization field. */
|
||||
template <typename Term, typename Values>
|
||||
void scatter(const Values& values, ::mfem::GridFunction& field) {
|
||||
const auto& binding = m_fe.template binding<Term>();
|
||||
if (values.size() != static_cast<std::size_t>(binding.size())) {
|
||||
throw std::invalid_argument("State and visualization binding differ");
|
||||
}
|
||||
::mfem::Vector reduced(binding.size());
|
||||
for (int i = 0; i < reduced.Size(); ++i) reduced[i] = values[i];
|
||||
field = binding.scatter(reduced);
|
||||
}
|
||||
|
||||
/** @brief Sample reconstructed density into a display-only polynomial on each material element. */
|
||||
template <typename Values>
|
||||
void sample_density(const Values& values) {
|
||||
using namespace utils::errors;
|
||||
|
||||
const auto& binding = m_fe.template binding<Density>();
|
||||
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(values.size() == static_cast<std::size_t>(binding.size()),
|
||||
"Density state and visualization binding have different coefficient counts.");
|
||||
::mfem::Vector reduced(binding.size());
|
||||
std::ranges::copy(values, reduced.GetData());
|
||||
discretization::backend::mfem::FieldValues<Density> density(binding, reduced);
|
||||
const auto& parent_elements = m_mesh.GetParentElementIDMap();
|
||||
::mfem::Array<int> dofs;
|
||||
::mfem::Vector local;
|
||||
for (int element = 0; element < m_mesh.GetNE(); ++element) {
|
||||
const auto& nodes = m_density_space.GetFE(element)->GetNodes();
|
||||
density.values(parent_elements[element], nodes, local);
|
||||
m_density_space.GetElementDofs(element, dofs);
|
||||
m_density.SetSubVector(dofs, local);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/** @brief Retain geometry and field copies for successive accepted solver states. */
|
||||
explicit StellarFrame(Discretization fe)
|
||||
: m_fe(std::move(fe)),
|
||||
m_parent_density(&m_fe.template binding<Density>().space()),
|
||||
m_parent_potential(&m_fe.template binding<Potential>().space()),
|
||||
m_parent_displacement(&m_fe.template binding<Displacement>().space()),
|
||||
m_mesh(interior_mesh(m_fe)),
|
||||
m_display_density_collection(density_display_collection(m_fe)),
|
||||
m_density_space(&m_mesh, m_display_density_collection ? m_display_density_collection.get() : m_parent_density.FESpace()->FEColl(),
|
||||
m_parent_density.FESpace()->GetVDim(), m_parent_density.FESpace()->GetOrdering()),
|
||||
m_potential_space(&m_mesh, m_parent_potential.FESpace()->FEColl(),
|
||||
m_parent_potential.FESpace()->GetVDim(), m_parent_potential.FESpace()->GetOrdering()),
|
||||
m_displacement_space(&m_mesh, m_parent_displacement.FESpace()->FEColl(),
|
||||
m_parent_displacement.FESpace()->GetVDim(), m_parent_displacement.FESpace()->GetOrdering()),
|
||||
m_density(&m_density_space), m_potential(&m_potential_space), m_displacement(&m_displacement_space),
|
||||
m_potential_transfer(m_parent_potential, m_potential),
|
||||
m_displacement_transfer(m_parent_displacement, m_displacement) {
|
||||
if (!m_display_density_collection) m_density_transfer = std::make_unique<::mfem::TransferMap>(m_parent_density, m_density);
|
||||
}
|
||||
|
||||
StellarFrame(const StellarFrame&) = delete;
|
||||
StellarFrame& operator=(const StellarFrame&) = delete;
|
||||
StellarFrame(StellarFrame&&) = delete;
|
||||
StellarFrame& operator=(StellarFrame&&) = delete;
|
||||
|
||||
/** @brief Publish one accepted state through its matching cached displacement. */
|
||||
template <typename State, typename Cache>
|
||||
void update(const State& state, const Cache& cache) {
|
||||
namespace deformation = geometry::deformation;
|
||||
const auto& linearization = cache.template get<deformation::keys::Linearization>();
|
||||
if (!std::ranges::equal(linearization.parameters, state.template get<Shape>())) {
|
||||
throw std::invalid_argument("Visualization state and cached deformation differ");
|
||||
}
|
||||
if (m_density_transfer) {
|
||||
scatter<Density>(state.template get<Density>(), m_parent_density);
|
||||
m_density_transfer->Transfer(m_parent_density, m_density);
|
||||
} else {
|
||||
sample_density(state.template get<Density>());
|
||||
}
|
||||
scatter<Potential>(state.template get<Potential>(), m_parent_potential);
|
||||
m_parent_displacement = m_fe.template binding<Displacement>().scatter(
|
||||
cache.template get<deformation::backend::mfem::volume_displacement_keys::Coefficients>());
|
||||
m_potential_transfer.Transfer(m_parent_potential, m_potential);
|
||||
m_displacement_transfer.Transfer(m_parent_displacement, m_displacement);
|
||||
}
|
||||
|
||||
/** @brief Return the unchanged computational mesh sent to the viewer. */
|
||||
[[nodiscard]] ::mfem::Mesh& mesh() { return m_mesh; }
|
||||
/** @brief Return the separate physical displacement applied by the viewer. */
|
||||
[[nodiscard]] ::mfem::GridFunction& displacement() { return m_displacement; }
|
||||
/** @brief Label a reconstructed density display explicitly as sampled data. */
|
||||
[[nodiscard]] std::vector<RayFEM::NamedField> fields() {
|
||||
return {{m_density_transfer ? "density" : "density (sampled)", &m_density}, {"potential", &m_potential}, {"displacement", &m_displacement}};
|
||||
}
|
||||
};
|
||||
}
|
||||
48
tests/sandbox/surface_sandbox.cpp
Normal file
48
tests/sandbox/surface_sandbox.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include <print>
|
||||
#include <utility>
|
||||
|
||||
#include "serif/eos/models/polytropic.hpp"
|
||||
#include "serif/geometry/surface/surface.hpp"
|
||||
#include "serif/utils/dependencies/computation_cache.hpp"
|
||||
|
||||
/**
|
||||
* @brief Prescribe one pressure and evaluate its residual over several already sampled boundary points.
|
||||
*
|
||||
* The EOS catalog and available state select the quantity enforcing the condition. This example binds density, as the
|
||||
* stellar model does; other models may bind another state. IsobaricSurface itself only knows the prescribed pressure.
|
||||
*
|
||||
* The ordinary dependency cache owns preparation. The EOS response is computed once for this pressure-only relation,
|
||||
* then reused across all boundary samples and subsequent changes to their density. No EOS evaluation occurs at graph construction.
|
||||
* Sampling a finite-element field on the boundary is deliberately a separate numerical operation.
|
||||
*/
|
||||
int main() {
|
||||
namespace surface = serif::geometry::surface;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace quantities = serif::dimensions;
|
||||
using Density = serif::discretization::blocks::density::mass;
|
||||
using EOS = serif::eos::models::Polytrope;
|
||||
using State = dependencies::Bindings<dependencies::Binding<quantities::MassDensity, Density>>;
|
||||
|
||||
const EOS equation_of_state{3.0, 0.25};
|
||||
const surface::IsobaricSurface boundary{quantities::PressureValue{0.03125}};
|
||||
const auto context = dependencies::make_dependency_context(
|
||||
dependencies::borrow<surface::keys::EquationOfState>(equation_of_state, dependencies::track_by([](const EOS& model) {
|
||||
return std::pair{model.polytropic_index(), model.polytropic_constant()};
|
||||
})),
|
||||
surface::pressure_condition(boundary),
|
||||
surface::boundary_values<Density>({0.05, 0.1, 0.2})
|
||||
);
|
||||
|
||||
auto graph = surface::make_pressure_surface_graph<EOS, State>();
|
||||
auto cache = dependencies::make_computation_cache<surface::SurfaceFailure>(std::move(graph), context);
|
||||
const auto preparation = cache.try_prepare(context);
|
||||
if (!preparation) {
|
||||
std::println(stderr, "Surface preparation failed: {}", preparation.error().error.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (const auto& point : cache.get<surface::keys::Linearization>()) {
|
||||
std::println("Boundary density residual: {}; derivative: {}", point.residual, point.field_derivatives.get<Density>());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user