Add native spherical harmonic surface coordinates to coupled equilibrium
This commit is contained in:
352
tests/deformation/spherical_harmonic_surface.cpp
Normal file
352
tests/deformation/spherical_harmonic_surface.cpp
Normal file
@@ -0,0 +1,352 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
namespace spherical_harmonic_surface_tests {
|
||||
namespace deformation = mean_field::deformation;
|
||||
namespace field = mean_field::field;
|
||||
namespace domain = mean_field::utils::domain;
|
||||
using Schema = domain::CoreEnvelopeVacuumDomainSchema;
|
||||
|
||||
int ranks() {
|
||||
int count = 0;
|
||||
MPI_Comm_size(MPI_COMM_WORLD, &count);
|
||||
return count;
|
||||
}
|
||||
|
||||
auto context(const mean_field::fem::FEM &fem) {
|
||||
return deformation::SurfaceDeformationCompilationContext{
|
||||
*fem.surfaceDeformationFes, field::make_stellar_surface_scalar_dof_map<Schema>(*fem.surfaceDeformationFes)
|
||||
};
|
||||
}
|
||||
|
||||
int modeIndex(
|
||||
const deformation::PreparedSphericalHarmonicSurface &surface,
|
||||
int degree,
|
||||
int order
|
||||
) {
|
||||
int index = 0;
|
||||
for (const auto mode : surface.modes()) {
|
||||
if (mode.degree == degree && mode.order == order)
|
||||
return index;
|
||||
++index;
|
||||
}
|
||||
throw std::logic_error("Required analytic harmonic mode was not compiled.");
|
||||
}
|
||||
|
||||
double relativeDifference(
|
||||
const mfem::Vector &a,
|
||||
const mfem::Vector &b
|
||||
) {
|
||||
REQUIRE(a.Size() == b.Size());
|
||||
mfem::Vector difference(a);
|
||||
difference -= b;
|
||||
return difference.Norml2() / std::max(b.Norml2(), 1e-300);
|
||||
}
|
||||
|
||||
void fill(
|
||||
mfem::Vector &values,
|
||||
double scale = 1.
|
||||
) {
|
||||
for (int i = 0; i < values.Size(); ++i)
|
||||
values(i) = scale * std::sin(.37 * (i + 1));
|
||||
}
|
||||
} // namespace spherical_harmonic_surface_tests
|
||||
|
||||
TEST_CASE(
|
||||
"Spherical Harmonic Surface Validates Physical Mode Selection And Type Contracts",
|
||||
tags::spherical_harmonic_surface_validation
|
||||
) {
|
||||
namespace d = mean_field::deformation;
|
||||
STATIC_CHECK(d::SurfaceDeformationPrescription<d::SphericalHarmonicSurface>);
|
||||
STATIC_CHECK(d::PreparedSurfaceDeformationPrescription<d::PreparedSphericalHarmonicSurface>);
|
||||
STATIC_CHECK(d::SurfaceDeformationCompilable<d::SphericalHarmonicSurface, d::SurfaceDeformationCompilationContext>);
|
||||
STATIC_CHECK_FALSE(d::PreparedSphericalHarmonicSurface::hasNodalRadialParameters);
|
||||
|
||||
const d::SphericalHarmonicSurface full;
|
||||
const d::SphericalHarmonicSurface axisymmetric(
|
||||
d::SphericalHarmonicSurfaceOptions{.maximumDegree = 4, .maximumAzimuthalOrder = 0}
|
||||
);
|
||||
const d::SphericalHarmonicSurface orderOne(
|
||||
d::SphericalHarmonicSurfaceOptions{.maximumDegree = 4, .maximumAzimuthalOrder = 1}
|
||||
);
|
||||
const d::SphericalHarmonicSurface orderTwo(
|
||||
d::SphericalHarmonicSurfaceOptions{.maximumDegree = 4, .maximumAzimuthalOrder = 2}
|
||||
);
|
||||
const d::SphericalHarmonicSurface monopole(d::SphericalHarmonicSurfaceOptions{.maximumDegree = 0});
|
||||
CHECK(full.modeCount() == 25);
|
||||
CHECK(axisymmetric.modeCount() == 5);
|
||||
CHECK(orderOne.modeCount() == 13);
|
||||
CHECK(orderTwo.modeCount() == 19);
|
||||
CHECK(monopole.modeCount() == 1);
|
||||
CHECK(full.referenceCenter().Size() == 3);
|
||||
CHECK(full.referenceCenter().Norml2() == 0.);
|
||||
CHECK(full.descriptor().motionKind == d::SurfaceMotionKind::Radial);
|
||||
CHECK(full.descriptor().linearOnReferenceGeometry);
|
||||
CHECK(full.descriptor().supportsExactNewtonLinearization());
|
||||
|
||||
for (const auto options :
|
||||
{d::SphericalHarmonicSurfaceOptions{.maximumDegree = -1},
|
||||
d::SphericalHarmonicSurfaceOptions{.maximumDegree = 4, .maximumAzimuthalOrder = -1},
|
||||
d::SphericalHarmonicSurfaceOptions{.maximumDegree = 4, .maximumAzimuthalOrder = 5},
|
||||
d::SphericalHarmonicSurfaceOptions{.maximumDegree = std::numeric_limits<int>::max()}})
|
||||
CHECK_THROWS_AS(d::SphericalHarmonicSurface(options), std::invalid_argument);
|
||||
mfem::Vector center(2);
|
||||
center = 0.;
|
||||
CHECK_THROWS_AS(d::SphericalHarmonicSurface(center), std::invalid_argument);
|
||||
center.SetSize(3);
|
||||
center = 0.;
|
||||
center(1) = std::numeric_limits<double>::quiet_NaN();
|
||||
CHECK_THROWS_AS(d::SphericalHarmonicSurface(center), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Spherical Harmonic Surface Matches Analytic Real Harmonics And Radial Lengths",
|
||||
tags::spherical_harmonic_surface_validation
|
||||
) {
|
||||
namespace t = spherical_harmonic_surface_tests;
|
||||
namespace d = mean_field::deformation;
|
||||
auto args = test_utils::setup_args();
|
||||
auto fem = mean_field::fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(fem.okay());
|
||||
const auto context = t::context(fem);
|
||||
if (t::ranks() != 1) {
|
||||
CHECK_THROWS_AS(
|
||||
d::compileSurfaceDeformationPrescription(d::SphericalHarmonicSurface{}, context), std::invalid_argument
|
||||
);
|
||||
return;
|
||||
}
|
||||
const auto surface = d::compileSurfaceDeformationPrescription(d::SphericalHarmonicSurface{}, context);
|
||||
CHECK(surface.parameterCount() == 25);
|
||||
CHECK(surface.globalParameterCount() == 25);
|
||||
CHECK(surface.globalParameterOffset() == 0);
|
||||
CHECK(surface.surfaceDisplacementSize() == 3 * surface.surfaceNodeCount());
|
||||
REQUIRE(surface.surfaceNodeCount() > surface.parameterCount());
|
||||
CHECK(surface.nodalBasis().Height() == surface.surfaceNodeCount());
|
||||
CHECK(surface.nodalBasis().Width() == surface.parameterCount());
|
||||
const double pi = std::numbers::pi;
|
||||
for (int node = 0; node < surface.surfaceNodeCount(); ++node) {
|
||||
const double x = surface.radialDirection(node, 0), y = surface.radialDirection(node, 1),
|
||||
z = surface.radialDirection(node, 2);
|
||||
const auto check = [&](int l, int m, double exact) {
|
||||
CHECK(std::abs(surface.nodalBasis()(node, t::modeIndex(surface, l, m)) - exact) < 2e-14);
|
||||
};
|
||||
check(0, 0, 1. / std::sqrt(4. * pi));
|
||||
check(1, 0, std::sqrt(3. / (4. * pi)) * z);
|
||||
check(1, 1, -std::sqrt(3. / (4. * pi)) * x);
|
||||
check(1, -1, -std::sqrt(3. / (4. * pi)) * y);
|
||||
check(2, 0, std::sqrt(5. / (16. * pi)) * (3. * z * z - 1.));
|
||||
check(2, -2, std::sqrt(15. / (4. * pi)) * x * y);
|
||||
check(2, 2, std::sqrt(15. / (16. * pi)) * (x * x - y * y));
|
||||
}
|
||||
mfem::Vector parameters(surface.parameterCount()), displacement(surface.surfaceDisplacementSize());
|
||||
parameters = 0.;
|
||||
parameters(0) = .125 * std::sqrt(4. * pi);
|
||||
surface.buildSurfaceDisplacement(parameters, displacement);
|
||||
for (int node = 0; node < surface.surfaceNodeCount(); ++node)
|
||||
for (int component = 0; component < 3; ++component)
|
||||
CHECK(
|
||||
std::abs(
|
||||
displacement(surface.surfaceDisplacementDof(node, component)) -
|
||||
.125 * surface.radialDirection(node, component)
|
||||
) < 1e-15
|
||||
);
|
||||
|
||||
const auto axisymmetric = d::compileSurfaceDeformationPrescription(
|
||||
d::SphericalHarmonicSurface(d::SphericalHarmonicSurfaceOptions{.maximumDegree = 4, .maximumAzimuthalOrder = 0}),
|
||||
context
|
||||
);
|
||||
const auto restricted = d::compileSurfaceDeformationPrescription(
|
||||
d::SphericalHarmonicSurface(d::SphericalHarmonicSurfaceOptions{.maximumDegree = 4, .maximumAzimuthalOrder = 1}),
|
||||
context
|
||||
);
|
||||
REQUIRE(axisymmetric.parameterCount() == 5);
|
||||
REQUIRE(restricted.parameterCount() == 13);
|
||||
for (int j = 0; j < axisymmetric.parameterCount(); ++j) {
|
||||
CHECK(axisymmetric.modes()[j].degree == j);
|
||||
CHECK(axisymmetric.modes()[j].order == 0);
|
||||
for (int i = 0; i < surface.surfaceNodeCount(); ++i)
|
||||
CHECK(axisymmetric.nodalBasis()(i, j) == surface.nodalBasis()(i, t::modeIndex(surface, j, 0)));
|
||||
}
|
||||
for (const auto mode : restricted.modes())
|
||||
CHECK(std::abs(mode.order) <= 1);
|
||||
const d::SphericalHarmonicSurface unresolved(
|
||||
d::SphericalHarmonicSurfaceOptions{.maximumDegree = surface.surfaceNodeCount()}
|
||||
);
|
||||
CHECK_THROWS_AS(d::compileSurfaceDeformationPrescription(unresolved, context), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Spherical Harmonic Surface Preserves Its Exact Derivative And Dual Work",
|
||||
tags::spherical_harmonic_surface_validation
|
||||
) {
|
||||
namespace t = spherical_harmonic_surface_tests;
|
||||
namespace d = mean_field::deformation;
|
||||
auto args = test_utils::setup_args();
|
||||
auto fem = mean_field::fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(fem.okay());
|
||||
const auto context = t::context(fem);
|
||||
if (t::ranks() != 1) {
|
||||
CHECK_THROWS_AS(
|
||||
d::compileSurfaceDeformationPrescription(d::SphericalHarmonicSurface{}, context), std::invalid_argument
|
||||
);
|
||||
return;
|
||||
}
|
||||
const auto surface = d::compileSurfaceDeformationPrescription(d::SphericalHarmonicSurface{}, context);
|
||||
const mfem::DenseMatrix originalBasis(surface.nodalBasis());
|
||||
mfem::Vector parameters(surface.parameterCount()), direction(surface.parameterCount()),
|
||||
dual(surface.surfaceDisplacementSize()), action(surface.surfaceDisplacementSize()),
|
||||
pulledBack(surface.parameterCount()), second(surface.parameterCount());
|
||||
t::fill(parameters, .03);
|
||||
t::fill(direction, .07);
|
||||
t::fill(dual);
|
||||
surface.applyJacobian(parameters, direction, action);
|
||||
surface.applyJacobianTranspose(parameters, dual, pulledBack);
|
||||
const double scale = std::max({1., action.Norml2() * dual.Norml2(), direction.Norml2() * pulledBack.Norml2()});
|
||||
CHECK(std::abs((action * dual) - (direction * pulledBack)) < 2e-13 * scale);
|
||||
|
||||
constexpr double epsilon = 1e-5;
|
||||
mfem::Vector plus(parameters), minus(parameters), plusValue(action.Size()), minusValue(action.Size());
|
||||
plus.Add(epsilon, direction);
|
||||
minus.Add(-epsilon, direction);
|
||||
surface.buildSurfaceDisplacement(plus, plusValue);
|
||||
surface.buildSurfaceDisplacement(minus, minusValue);
|
||||
plusValue -= minusValue;
|
||||
plusValue /= 2. * epsilon;
|
||||
CHECK(t::relativeDifference(plusValue, action) < 2e-10);
|
||||
second = 7.;
|
||||
surface.applyPullbackDerivative(parameters, direction, dual, second);
|
||||
CHECK(second.Norml2() == 0.);
|
||||
CHECK(
|
||||
std::memcmp(
|
||||
originalBasis.GetData(), surface.nodalBasis().GetData(),
|
||||
sizeof(mfem::real_t) * originalBasis.Height() * originalBasis.Width()
|
||||
) == 0
|
||||
);
|
||||
|
||||
mfem::Vector wrong(surface.parameterCount() + 1);
|
||||
wrong = 0.;
|
||||
CHECK_THROWS_AS(surface.buildSurfaceDisplacement(wrong, action), std::invalid_argument);
|
||||
CHECK_THROWS_AS(surface.applyJacobian(parameters, wrong, action), std::invalid_argument);
|
||||
CHECK_THROWS_AS(surface.applyJacobianTranspose(parameters, dual, wrong), std::invalid_argument);
|
||||
CHECK_THROWS_AS(surface.applyPullbackDerivative(parameters, direction, dual, wrong), std::invalid_argument);
|
||||
direction(0) = std::numeric_limits<double>::quiet_NaN();
|
||||
CHECK_THROWS_AS(surface.applyJacobian(parameters, direction, action), std::invalid_argument);
|
||||
dual(0) = std::numeric_limits<double>::infinity();
|
||||
CHECK_THROWS_AS(surface.applyJacobianTranspose(parameters, dual, pulledBack), std::invalid_argument);
|
||||
// Finite entries can still overflow an accumulated modal force; accepting
|
||||
// an infinite dual would contaminate the complete Newton merit silently.
|
||||
for (int node = 0; node < surface.surfaceNodeCount(); ++node)
|
||||
for (int component = 0; component < 3; ++component)
|
||||
dual(surface.surfaceDisplacementDof(node, component)) =
|
||||
std::numeric_limits<double>::max() * surface.radialDirection(node, component);
|
||||
for (int i = 0; i < dual.Size(); ++i)
|
||||
REQUIRE(std::isfinite(dual(i)));
|
||||
CHECK_THROWS_AS(surface.applyJacobianTranspose(parameters, dual, pulledBack), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Spherical Harmonic Surface Rejects Aliased Modes Even With Enough Nodes",
|
||||
tags::spherical_harmonic_surface_validation
|
||||
) {
|
||||
namespace t = spherical_harmonic_surface_tests;
|
||||
namespace d = mean_field::deformation;
|
||||
if (t::ranks() != 1) {
|
||||
SUCCEED("MPI compilation rejection is covered by the full surface fixture.");
|
||||
return;
|
||||
}
|
||||
auto serial = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON, 1., 1., 1.);
|
||||
for (int b = 0; b < serial.GetNBE(); ++b)
|
||||
serial.GetBdrElement(b)->SetAttribute(1);
|
||||
mfem::ParMesh mesh(MPI_COMM_WORLD, serial);
|
||||
mfem::H1_FECollection collection(1, 3);
|
||||
mfem::ParFiniteElementSpace space(&mesh, &collection);
|
||||
const auto map = t::field::make_stellar_surface_scalar_dof_map<t::Schema>(space);
|
||||
const d::SurfaceDeformationCompilationContext context(space, map);
|
||||
mfem::Vector center(3);
|
||||
center = .5;
|
||||
const d::SphericalHarmonicSurface axisymmetric(
|
||||
center, d::SphericalHarmonicSurfaceOptions{.maximumDegree = 2, .maximumAzimuthalOrder = 0}
|
||||
);
|
||||
// Eight cube corners offer only two distinct cos(theta), so the three
|
||||
// axisymmetric polynomials cannot define three independent coordinates.
|
||||
if (t::ranks() == 1)
|
||||
REQUIRE(map.local_size() == 8);
|
||||
CHECK_THROWS_AS(d::compileSurfaceDeformationPrescription(axisymmetric, context), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Spherical Harmonic Domain Couples All Volume Fields Through The Existing Extensions",
|
||||
tags::spherical_harmonic_surface_validation
|
||||
) {
|
||||
namespace t = spherical_harmonic_surface_tests;
|
||||
namespace d = mean_field::deformation;
|
||||
auto args = test_utils::setup_args();
|
||||
auto fem = mean_field::fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(fem.okay());
|
||||
const auto make = [&]() {
|
||||
return d::compileDomainDeformation(
|
||||
d::SphericalHarmonicSurface(d::SphericalHarmonicSurfaceOptions{.maximumDegree = 2}),
|
||||
d::PowerLawRadialInteriorExtension{}, d::FixedInfinityRadialVacuumExtension{}, fem
|
||||
);
|
||||
};
|
||||
if (t::ranks() != 1) {
|
||||
CHECK_THROWS_AS(make(), std::invalid_argument);
|
||||
return;
|
||||
}
|
||||
auto harmonic = make();
|
||||
const auto &surface = harmonic.surfaceDeformationPrescription();
|
||||
mfem::Vector center(3);
|
||||
center = 0.;
|
||||
auto nodal = d::compileDomainDeformation(
|
||||
d::NodalRadialSurface(center), d::PowerLawRadialInteriorExtension{}, d::FixedInfinityRadialVacuumExtension{},
|
||||
fem
|
||||
);
|
||||
mfem::Vector parameters(harmonic.parameterCount()), direction(harmonic.parameterCount()),
|
||||
nodalParameters(nodal.parameterCount()), volume(harmonic.volumeDisplacementSize()),
|
||||
nodalVolume(nodal.volumeDisplacementSize()), action(volume.Size()), dual(volume.Size()),
|
||||
pulledBack(parameters.Size()), pullbackDerivative(parameters.Size());
|
||||
REQUIRE(harmonic.parameterCount() == 9);
|
||||
REQUIRE(harmonic.surfaceDisplacementSize() == nodal.surfaceDisplacementSize());
|
||||
REQUIRE(harmonic.volumeDisplacementSize() == nodal.volumeDisplacementSize());
|
||||
t::fill(parameters, .001);
|
||||
t::fill(direction, .004);
|
||||
t::fill(dual);
|
||||
surface.nodalBasis().Mult(parameters, nodalParameters);
|
||||
harmonic.buildVolumeDisplacement(parameters, volume);
|
||||
nodal.buildVolumeDisplacement(nodalParameters, nodalVolume);
|
||||
CHECK(t::relativeDifference(volume, nodalVolume) < 3e-13);
|
||||
harmonic.applyJacobian(parameters, direction, action);
|
||||
harmonic.applyJacobianTranspose(parameters, dual, pulledBack);
|
||||
CHECK(
|
||||
std::abs((action * dual) - (direction * pulledBack)) <
|
||||
1e-11 * std::max({1., action.Norml2() * dual.Norml2(), direction.Norml2() * pulledBack.Norml2()})
|
||||
);
|
||||
harmonic.applyPullbackDerivative(parameters, direction, dual, pullbackDerivative);
|
||||
CHECK(pullbackDerivative.Norml2() == 0.);
|
||||
|
||||
constexpr double epsilon = 1e-5;
|
||||
mfem::Vector plus(parameters), minus(parameters), plusVolume(volume.Size()), minusVolume(volume.Size());
|
||||
plus.Add(epsilon, direction);
|
||||
minus.Add(-epsilon, direction);
|
||||
harmonic.buildVolumeDisplacement(plus, plusVolume);
|
||||
harmonic.buildVolumeDisplacement(minus, minusVolume);
|
||||
plusVolume -= minusVolume;
|
||||
plusVolume /= 2. * epsilon;
|
||||
CHECK(t::relativeDifference(plusVolume, action) < 2e-9);
|
||||
const auto infinity =
|
||||
t::field::make_scalar_boundary_dof_map<t::domain::InfinitySurface, t::Schema>(*fem.surfaceDeformationFes);
|
||||
for (int i = 0; i < infinity.local_size(); ++i)
|
||||
for (int component = 0; component < 3; ++component)
|
||||
CHECK(volume(infinity.volume_true_dof(i) + component * fem.surfaceDeformationFes->GetTrueVSize()) == 0.);
|
||||
}
|
||||
294
tests/normalization/spherical_harmonic_surface.cpp
Normal file
294
tests/normalization/spherical_harmonic_surface.cpp
Normal file
@@ -0,0 +1,294 @@
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
namespace harmonic_surface_metric_tests {
|
||||
// Use analytic Cartesian formulas and direct boundary quadrature, rather
|
||||
// than either the harmonic basis implementation or its assembled mass
|
||||
// action. This distinguishes vector trace work from scalar radial work.
|
||||
double analyticReferenceWork(
|
||||
const mean_field::fem::FEM &fem,
|
||||
int degree
|
||||
) {
|
||||
mfem::VectorFunctionCoefficient coefficient(3, [degree](const mfem::Vector &position, mfem::Vector &value) {
|
||||
const double radius = position.Norml2();
|
||||
if (!(radius > 0.)) {
|
||||
// Interior center values cannot affect the boundary trace.
|
||||
value.SetSize(3);
|
||||
value = 0.;
|
||||
return;
|
||||
}
|
||||
const double z = position(2) / radius;
|
||||
const double harmonic =
|
||||
degree == 0 ? 1. / std::sqrt(4. * std::numbers::pi) : std::sqrt(3. / (4. * std::numbers::pi)) * z;
|
||||
value.SetSize(3);
|
||||
for (int component = 0; component < 3; ++component)
|
||||
value(component) = harmonic * position(component) / radius;
|
||||
});
|
||||
mfem::ParGridFunction tangent(fem.displacementFes.get());
|
||||
tangent.ProjectCoefficient(coefficient);
|
||||
mfem::Array<int> dofs;
|
||||
mfem::Vector local, shape;
|
||||
long double work = 0.;
|
||||
for (int b = 0; b < fem.mesh->GetNBE(); ++b) {
|
||||
if (fem.mesh->GetBdrAttribute(b) != 1)
|
||||
continue;
|
||||
const auto &element = *fem.displacementFes->GetBE(b);
|
||||
auto &transformation = *fem.displacementFes->GetBdrElementTransformation(b);
|
||||
REQUIRE(fem.displacementFes->GetBdrElementVDofs(b, dofs) == nullptr);
|
||||
tangent.GetSubVector(dofs, local);
|
||||
REQUIRE(local.Size() == 3 * element.GetDof());
|
||||
shape.SetSize(element.GetDof());
|
||||
// Match the declared FE quadrature rule, not the assembled matrix
|
||||
// or its diagonal; the integrand is independently reconstructed.
|
||||
const auto &rule = mfem::MassIntegrator::GetRule(element, element, transformation);
|
||||
for (int q = 0; q < rule.GetNPoints(); ++q) {
|
||||
const auto &point = rule.IntPoint(q);
|
||||
transformation.SetIntPoint(&point);
|
||||
element.CalcShape(point, shape);
|
||||
long double normSquared = 0.;
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
long double value = 0.;
|
||||
for (int i = 0; i < element.GetDof(); ++i)
|
||||
value += static_cast<long double>(shape(i)) * local(i + component * element.GetDof());
|
||||
normSquared += value * value;
|
||||
}
|
||||
work += point.weight * transformation.Weight() * normSquared;
|
||||
}
|
||||
}
|
||||
REQUIRE(std::isfinite(work));
|
||||
REQUIRE(work > 0.);
|
||||
return static_cast<double>(work);
|
||||
}
|
||||
|
||||
// A valid surface implementation does not automatically promise the
|
||||
// canonical nodal ordering required by this particular Riesz metric.
|
||||
class SurfaceWithoutTraceMetadata final {
|
||||
public:
|
||||
explicit SurfaceWithoutTraceMetadata(mean_field::deformation::PreparedSphericalHarmonicSurface surface)
|
||||
: m_surface(std::move(surface)) {
|
||||
}
|
||||
auto descriptor() const noexcept {
|
||||
return m_surface.descriptor();
|
||||
}
|
||||
int parameterCount() const noexcept {
|
||||
return m_surface.parameterCount();
|
||||
}
|
||||
int surfaceDisplacementSize() const noexcept {
|
||||
return m_surface.surfaceDisplacementSize();
|
||||
}
|
||||
void buildSurfaceDisplacement(
|
||||
const mfem::Vector &x,
|
||||
mfem::Vector &u
|
||||
) const {
|
||||
m_surface.buildSurfaceDisplacement(x, u);
|
||||
}
|
||||
void applyJacobian(
|
||||
const mfem::Vector &x,
|
||||
const mfem::Vector &v,
|
||||
mfem::Vector &u
|
||||
) const {
|
||||
m_surface.applyJacobian(x, v, u);
|
||||
}
|
||||
void applyJacobianTranspose(
|
||||
const mfem::Vector &x,
|
||||
const mfem::Vector &u,
|
||||
mfem::Vector &v
|
||||
) const {
|
||||
m_surface.applyJacobianTranspose(x, u, v);
|
||||
}
|
||||
void applyPullbackDerivative(
|
||||
const mfem::Vector &x,
|
||||
const mfem::Vector &v,
|
||||
const mfem::Vector &u,
|
||||
mfem::Vector &result
|
||||
) const {
|
||||
m_surface.applyPullbackDerivative(x, v, u, result);
|
||||
}
|
||||
|
||||
private:
|
||||
mean_field::deformation::PreparedSphericalHarmonicSurface m_surface;
|
||||
};
|
||||
static_assert(mean_field::deformation::PreparedSurfaceDeformationPrescription<SurfaceWithoutTraceMetadata>);
|
||||
} // namespace harmonic_surface_metric_tests
|
||||
|
||||
TEST_CASE(
|
||||
"Harmonic Surface Coordinates Receive Their Own Primal And Dual Riesz Scales Before Physical Preparation",
|
||||
"[normalization][spherical_harmonic][integration][serial]"
|
||||
) {
|
||||
using namespace mean_field;
|
||||
int ranks = 0;
|
||||
MPI_Comm_size(MPI_COMM_WORLD, &ranks);
|
||||
if (ranks != 1)
|
||||
SKIP("The current radial domain extension supports one MPI rank.");
|
||||
|
||||
// Both configurations have the same ambient trace space. Only the
|
||||
// independent surface coordinate count changes, so a nodal-diagonal
|
||||
// shortcut would fail the block-size and scale checks below.
|
||||
deformation::SphericalHarmonicSurface prescription({.maximumDegree = 4});
|
||||
int expectedModes = 25;
|
||||
SECTION("All real azimuthal modes") {
|
||||
}
|
||||
SECTION("Axisymmetric modes") {
|
||||
prescription = deformation::SphericalHarmonicSurface({.maximumDegree = 4, .maximumAzimuthalOrder = 0});
|
||||
expectedModes = 5;
|
||||
}
|
||||
|
||||
const auto args = test_utils::setup_args();
|
||||
auto finiteElements = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(finiteElements.okay());
|
||||
const normalization::PhysicalRieszDiagonal policy{dimensions::LengthValue{utils::RADIUS}, utils::G};
|
||||
auto stellarModel = model::StellarModel(
|
||||
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
|
||||
);
|
||||
auto problem = equilibrium::discretize(
|
||||
stellarModel, equilibrium::makeStellarDiscretization(std::move(finiteElements), policy, prescription)
|
||||
);
|
||||
using Problem = std::remove_cvref_t<decltype(problem)>;
|
||||
using Form = typename Problem::FormType;
|
||||
using ShapeValue = utils::blocks::surface_deformation::parameters::value;
|
||||
using ShapeResidual = utils::blocks::surface_deformation::shape_equilibrium::residual;
|
||||
constexpr int valueBlock = utils::blocks::type_index_v<ShapeValue, typename Form::value_blocks>;
|
||||
constexpr int residualBlock = utils::blocks::type_index_v<ShapeResidual, typename Form::residual_blocks>;
|
||||
STATIC_CHECK(normalization::NormalizableStellarEquilibriumProblem<Problem>);
|
||||
const auto &layout = problem.GetManifest().layout();
|
||||
const int valueOffset = layout.value_offsets()[valueBlock];
|
||||
const int residualOffset = layout.residual_offsets()[residualBlock];
|
||||
REQUIRE(layout.value_offsets()[valueBlock + 1] - valueOffset == expectedModes);
|
||||
REQUIRE(layout.residual_offsets()[residualBlock + 1] - residualOffset == expectedModes);
|
||||
REQUIRE(problem.StateSize() == problem.EquationSize());
|
||||
|
||||
const auto &physical = problem.GetPhysicalOperator();
|
||||
const auto &domain = physical.GetDomainDeformation();
|
||||
REQUIRE_FALSE(domain.hasNodalRadialParameters());
|
||||
REQUIRE(domain.parameterCount() == expectedModes);
|
||||
REQUIRE_FALSE(physical.IsPrepared());
|
||||
const auto generation = problem.GetPreparationGeneration();
|
||||
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
|
||||
CHECK_FALSE(normalized.IsPrepared());
|
||||
CHECK_FALSE(physical.IsPrepared());
|
||||
CHECK(problem.GetPreparationGeneration() == generation);
|
||||
|
||||
const auto diagonal =
|
||||
deformation::assembleSurfaceParameterMassDiagonal(physical.GetHydrostaticOperator().GetFEM(), domain);
|
||||
REQUIRE(diagonal.Size() == expectedModes);
|
||||
const auto &referenceFEM = physical.GetHydrostaticOperator().GetFEM();
|
||||
CHECK(
|
||||
diagonal(0) ==
|
||||
Catch::Approx(harmonic_surface_metric_tests::analyticReferenceWork(referenceFEM, 0)).epsilon(5e-13)
|
||||
);
|
||||
const int axialDipole = expectedModes == 5 ? 1 : 2;
|
||||
CHECK(
|
||||
diagonal(axialDipole) ==
|
||||
Catch::Approx(harmonic_surface_metric_tests::analyticReferenceWork(referenceFEM, 1)).epsilon(5e-13)
|
||||
);
|
||||
const auto scales = normalization::deriveStellarCharacteristicScales(policy, stellarModel);
|
||||
const double valueScale = normalization::physicalScale<ShapeValue>(scales);
|
||||
const double residualScale = normalization::physicalScale<ShapeResidual>(scales);
|
||||
const auto &stateFactors = normalized.GetNormalization().StateFactors();
|
||||
const auto &residualFactors = normalized.GetNormalization().ResidualFactors();
|
||||
REQUIRE(stateFactors.Size() == problem.StateSize());
|
||||
REQUIRE(residualFactors.Size() == problem.EquationSize());
|
||||
for (int i = 0; i < expectedModes; ++i) {
|
||||
REQUIRE(std::isfinite(diagonal[i]));
|
||||
REQUIRE(diagonal[i] > 0.0);
|
||||
CHECK(stateFactors[valueOffset + i] == Catch::Approx(std::sqrt(diagonal[i]) / valueScale).epsilon(3.e-14));
|
||||
CHECK(
|
||||
residualFactors[residualOffset + i] ==
|
||||
Catch::Approx(1.0 / (residualScale * std::sqrt(diagonal[i]))).epsilon(3.e-14)
|
||||
);
|
||||
// Primal and dual factors act on different root block offsets. Their
|
||||
// Gram weights cancel in the work pairing, leaving physical scales.
|
||||
CHECK(
|
||||
stateFactors[valueOffset + i] * residualFactors[residualOffset + i] ==
|
||||
Catch::Approx(1.0 / (valueScale * residualScale)).epsilon(3.e-14)
|
||||
);
|
||||
}
|
||||
bool allFactorsValid = true;
|
||||
for (int i = 0; i < stateFactors.Size(); ++i)
|
||||
allFactorsValid = allFactorsValid && std::isfinite(stateFactors[i]) && stateFactors[i] > 0.0 &&
|
||||
std::isfinite(residualFactors[i]) && residualFactors[i] > 0.0;
|
||||
CHECK(allFactorsValid);
|
||||
CHECK_FALSE(physical.IsPrepared());
|
||||
CHECK(problem.GetPreparationGeneration() == generation);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Harmonic Reference Metric Rejects Unrelated Current Spaces And Unadvertised Trace Layouts",
|
||||
"[normalization][spherical_harmonic][integration][serial]"
|
||||
) {
|
||||
using namespace mean_field;
|
||||
int ranks = 0;
|
||||
MPI_Comm_size(MPI_COMM_WORLD, &ranks);
|
||||
if (ranks != 1)
|
||||
SKIP("The current radial domain extension supports one MPI rank.");
|
||||
const auto args = test_utils::setup_args();
|
||||
auto finiteElements = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(finiteElements.okay());
|
||||
deformation::SphericalHarmonicSurface prescription({.maximumDegree = 2});
|
||||
auto prepared = deformation::compileDomainDeformation(
|
||||
prescription, deformation::PowerLawRadialInteriorExtension{}, deformation::FixedInfinityRadialVacuumExtension{},
|
||||
finiteElements
|
||||
);
|
||||
deformation::PreparedDomainDeformationRuntime runtime(std::move(prepared));
|
||||
REQUIRE(runtime.matchesCurrentDiscretization());
|
||||
REQUIRE(runtime.supportsSurfaceParameterMetric());
|
||||
|
||||
SECTION("A different current FEM with identical dimensions is not interchangeable") {
|
||||
auto other = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(other.okay());
|
||||
REQUIRE(other.surfaceDeformationFes->GetTrueVSize() == finiteElements.surfaceDeformationFes->GetTrueVSize());
|
||||
REQUIRE(other.displacementFes->GetTrueVSize() == finiteElements.displacementFes->GetTrueVSize());
|
||||
REQUIRE(other.mesh.get() != finiteElements.mesh.get());
|
||||
CHECK_THROWS_AS(deformation::assembleSurfaceParameterMassDiagonal(other, runtime), std::invalid_argument);
|
||||
CHECK(runtime.matchesCurrentDiscretization());
|
||||
}
|
||||
|
||||
SECTION("A surface tangent alone cannot certify canonical nodal vector ordering") {
|
||||
using Schema = utils::domain::CoreEnvelopeVacuumDomainSchema;
|
||||
const deformation::SurfaceDeformationCompilationContext surfaceContext{
|
||||
*finiteElements.surfaceDeformationFes,
|
||||
field::make_stellar_surface_scalar_dof_map<Schema>(*finiteElements.surfaceDeformationFes)
|
||||
};
|
||||
auto surface = deformation::compileSurfaceDeformationPrescription(prescription, surfaceContext);
|
||||
auto extensionContext = deformation::makeRadialDeformationExtensionCompilationContext<Schema>(
|
||||
*finiteElements.surfaceDeformationFes, *finiteElements.displacementFes, *finiteElements.logicalReferenceMesh
|
||||
);
|
||||
auto custom = deformation::composePreparedDomainDeformation(
|
||||
harmonic_surface_metric_tests::SurfaceWithoutTraceMetadata(std::move(surface)),
|
||||
deformation::compileInteriorDeformationExtension(
|
||||
deformation::PowerLawRadialInteriorExtension{}, extensionContext
|
||||
),
|
||||
deformation::compileVacuumDeformationExtension(
|
||||
deformation::FixedInfinityRadialVacuumExtension{}, extensionContext
|
||||
),
|
||||
*finiteElements.surfaceDeformationFes, *finiteElements.displacementFes, *finiteElements.logicalReferenceMesh
|
||||
);
|
||||
STATIC_CHECK(deformation::PreparedDomainDeformationOperator<decltype(custom)>);
|
||||
REQUIRE_FALSE(custom.supportsSurfaceParameterMetric());
|
||||
deformation::PreparedDomainDeformationRuntime customRuntime(std::move(custom));
|
||||
REQUIRE(customRuntime.matchesCurrentDiscretization());
|
||||
REQUIRE_FALSE(customRuntime.supportsSurfaceParameterMetric());
|
||||
mfem::Vector parameters(customRuntime.parameterCount()), direction(parameters.Size()),
|
||||
trace(customRuntime.surfaceDisplacementSize());
|
||||
parameters = 0.;
|
||||
direction = 1.;
|
||||
CHECK_NOTHROW(customRuntime.applySurfaceJacobian(parameters, direction, trace));
|
||||
CHECK(trace.Norml2() > 0.);
|
||||
CHECK_THROWS_AS(
|
||||
deformation::assembleSurfaceParameterMassDiagonal(finiteElements, customRuntime), std::invalid_argument
|
||||
);
|
||||
}
|
||||
}
|
||||
427
tests/operators/spherical_harmonic_equilibrium.cpp
Normal file
427
tests/operators/spherical_harmonic_equilibrium.cpp
Normal file
@@ -0,0 +1,427 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
namespace spherical_harmonic_equilibrium_test {
|
||||
using namespace mean_field;
|
||||
using Block = operators::RootBlockDescriptor;
|
||||
|
||||
[[nodiscard]] const Block &findBlock(
|
||||
const std::span<const Block> blocks,
|
||||
const std::string_view name
|
||||
) {
|
||||
const auto found =
|
||||
std::find_if(blocks.begin(), blocks.end(), [name](const Block &block) { return block.stableId == name; });
|
||||
REQUIRE(found != blocks.end());
|
||||
return *found;
|
||||
}
|
||||
|
||||
[[nodiscard]] mfem::Vector readBlock(
|
||||
const mfem::Vector &vector,
|
||||
const Block &block
|
||||
) {
|
||||
REQUIRE(block.offset >= 0);
|
||||
REQUIRE(block.offset + block.size <= vector.Size());
|
||||
mfem::Vector result(block.size);
|
||||
for (int i = 0; i < block.size; ++i)
|
||||
result(i) = vector(block.offset + i);
|
||||
return result;
|
||||
}
|
||||
|
||||
void writeBlock(
|
||||
mfem::Vector &vector,
|
||||
const Block &block,
|
||||
const mfem::Vector &values
|
||||
) {
|
||||
REQUIRE(values.Size() == block.size);
|
||||
for (int i = 0; i < block.size; ++i)
|
||||
vector(block.offset + i) = values(i);
|
||||
}
|
||||
|
||||
void checkClose(
|
||||
const mfem::Vector &actual,
|
||||
const mfem::Vector &expected,
|
||||
const double relative = 2.0e-9,
|
||||
const double absolute = 2.0e-12
|
||||
) {
|
||||
REQUIRE(actual.Size() == expected.Size());
|
||||
mfem::Vector difference(actual);
|
||||
difference -= expected;
|
||||
const double scale = std::max(actual.Norml2(), expected.Norml2());
|
||||
INFO("error = " << difference.Norml2() << ", scale = " << scale);
|
||||
REQUIRE(std::isfinite(difference.Norml2()));
|
||||
CHECK(difference.Norml2() <= absolute + relative * scale);
|
||||
}
|
||||
|
||||
void checkBlocks(
|
||||
const mfem::Vector &actual,
|
||||
const mfem::Vector &expected,
|
||||
const std::span<const Block> blocks,
|
||||
const double relative = 2.0e-9,
|
||||
const double absolute = 2.0e-12
|
||||
) {
|
||||
for (const Block &block : blocks) {
|
||||
INFO("residual block = " << block.stableId);
|
||||
checkClose(readBlock(actual, block), readBlock(expected, block), relative, absolute);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] operators::StellarEquilibriumDependencies dependencies(const std::uint64_t revision) {
|
||||
return {
|
||||
.discretization = {.identity = 8101, .revision = 1},
|
||||
.density = {.identity = 8103, .revision = revision},
|
||||
.surfaceDeformation = {.identity = 8107, .revision = revision},
|
||||
.gravityGradient = {.identity = 8111, .revision = revision},
|
||||
.gravityPotential = {.identity = 8117, .revision = revision},
|
||||
.enthalpy = {.identity = 8123, .revision = revision},
|
||||
.bernoulliConstant = {.identity = 8129, .revision = revision},
|
||||
.rotation = {.identity = 8131, .revision = revision},
|
||||
.targetMass = {.identity = 8147, .revision = 1}
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] physics::RigidRotation zeroRotation() {
|
||||
mfem::Vector omega(3), center(3);
|
||||
omega = 0.0;
|
||||
center = 0.0;
|
||||
return {omega, center};
|
||||
}
|
||||
|
||||
template <bool GeneratedRotation> [[nodiscard]] auto makeModel() {
|
||||
if constexpr (GeneratedRotation) {
|
||||
return model::StellarModel(
|
||||
eos::Polytrope({.n = 1.5, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
|
||||
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.01}})
|
||||
);
|
||||
} else {
|
||||
return model::StellarModel(
|
||||
eos::Polytrope({.n = 1.5, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Problem>
|
||||
void prepare(
|
||||
Problem &problem,
|
||||
const mfem::Vector &state,
|
||||
const std::uint64_t revision
|
||||
) {
|
||||
if constexpr (Problem::hasFixedAngularMomentum) {
|
||||
REQUIRE(problem.TryPrepare(state, dependencies(revision)).has_value());
|
||||
} else {
|
||||
REQUIRE(problem.TryPrepare(state, dependencies(revision), zeroRotation()).has_value());
|
||||
}
|
||||
}
|
||||
|
||||
// The oracle is the existing nodal problem, with explicit coefficient and
|
||||
// dual-force maps. It never calls the native modal residual or Jacobian to
|
||||
// construct an expected coupled result.
|
||||
template <
|
||||
typename ModalProblem,
|
||||
typename NodalProblem>
|
||||
[[nodiscard]] mfem::Vector liftState(
|
||||
const ModalProblem &modal,
|
||||
const NodalProblem &nodal,
|
||||
const mfem::DenseMatrix &basis,
|
||||
const mfem::Vector &source
|
||||
) {
|
||||
mfem::Vector result(nodal.StateSize());
|
||||
result = 0.0;
|
||||
for (const Block &destination : nodal.GetManifest().valueBlocks()) {
|
||||
const Block &origin = findBlock(modal.GetManifest().valueBlocks(), destination.stableId);
|
||||
const mfem::Vector values = readBlock(source, origin);
|
||||
if (destination.stableId == "surface_deformation") {
|
||||
REQUIRE(basis.Width() == origin.size);
|
||||
REQUIRE(basis.Height() == destination.size);
|
||||
mfem::Vector lifted(destination.size);
|
||||
basis.Mult(values, lifted);
|
||||
writeBlock(result, destination, lifted);
|
||||
} else {
|
||||
writeBlock(result, destination, values);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <
|
||||
typename ModalProblem,
|
||||
typename NodalProblem>
|
||||
[[nodiscard]] mfem::Vector pullResidual(
|
||||
const ModalProblem &modal,
|
||||
const NodalProblem &nodal,
|
||||
const mfem::DenseMatrix &basis,
|
||||
const mfem::Vector &source
|
||||
) {
|
||||
mfem::Vector result(modal.EquationSize());
|
||||
result = 0.0;
|
||||
for (const Block &destination : modal.GetManifest().residualBlocks()) {
|
||||
const Block &origin = findBlock(nodal.GetManifest().residualBlocks(), destination.stableId);
|
||||
const mfem::Vector values = readBlock(source, origin);
|
||||
if (destination.stableId == "surface_shape_balance") {
|
||||
REQUIRE(basis.Height() == origin.size);
|
||||
REQUIRE(basis.Width() == destination.size);
|
||||
mfem::Vector pulled(destination.size);
|
||||
basis.MultTranspose(values, pulled);
|
||||
writeBlock(result, destination, pulled);
|
||||
} else {
|
||||
writeBlock(result, destination, values);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <bool GeneratedRotation> void checkCoupledEquivalence() {
|
||||
const utils::Args args = test_utils::setup_args();
|
||||
fem::FEM modalFem = fem::setup_fem(args.mesh_file, args, 0);
|
||||
fem::FEM nodalFem = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(modalFem.okay());
|
||||
REQUIRE(nodalFem.okay());
|
||||
|
||||
const deformation::SphericalHarmonicSurface prescription{{.maximumDegree = 4}};
|
||||
using Schema = utils::domain::CoreEnvelopeVacuumDomainSchema;
|
||||
const auto surfaceMap = field::make_stellar_surface_scalar_dof_map<Schema>(*modalFem.surfaceDeformationFes);
|
||||
const auto preparedSurface = deformation::compileSurfaceDeformationPrescription(
|
||||
prescription, deformation::SurfaceDeformationCompilationContext{*modalFem.surfaceDeformationFes, surfaceMap}
|
||||
);
|
||||
const mfem::DenseMatrix basis(preparedSurface.nodalBasis());
|
||||
REQUIRE(basis.Width() == 25);
|
||||
|
||||
const normalization::PhysicalRieszDiagonal policy{dimensions::LengthValue{1.0}};
|
||||
const auto stellarModel = makeModel<GeneratedRotation>();
|
||||
auto modal = equilibrium::discretize(
|
||||
stellarModel, equilibrium::makeStellarDiscretization(std::move(modalFem), policy, prescription)
|
||||
);
|
||||
auto nodal =
|
||||
equilibrium::discretize(stellarModel, equilibrium::makeStellarDiscretization(std::move(nodalFem), policy));
|
||||
using ModalProblem = std::remove_cvref_t<decltype(modal)>;
|
||||
STATIC_CHECK_FALSE(ModalProblem::hasFixedCentralDensity);
|
||||
STATIC_CHECK(ModalProblem::hasFixedAngularMomentum == GeneratedRotation);
|
||||
STATIC_CHECK(ModalProblem::symbolicallySquare);
|
||||
REQUIRE(modal.StateSize() == modal.EquationSize());
|
||||
const Block &shape = findBlock(modal.GetManifest().valueBlocks(), "surface_deformation");
|
||||
const Block &shapeRows = findBlock(modal.GetManifest().residualBlocks(), "surface_shape_balance");
|
||||
REQUIRE(shape.size == 25);
|
||||
REQUIRE(shapeRows.size == shape.size);
|
||||
CHECK(nodal.StateSize() - modal.StateSize() == basis.Height() - basis.Width());
|
||||
|
||||
mfem::Vector state(modal.StateSize()), direction(modal.StateSize());
|
||||
state = 0.0;
|
||||
for (int i = 0; i < direction.Size(); ++i)
|
||||
direction(i) = 0.02 * std::sin(0.31 * (i + 1));
|
||||
const auto view = modal.GetManifest().stateView(state);
|
||||
view.block(utils::blocks::density_field.mass_term) = 1.0;
|
||||
view.block(utils::blocks::enthalpy_field.specific_term) = 0.7;
|
||||
view.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.8;
|
||||
if constexpr (GeneratedRotation) {
|
||||
view.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term) = 0.04;
|
||||
modal.GetManifest().stateView(direction).block(
|
||||
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
|
||||
) = 0.015;
|
||||
}
|
||||
// Nonzero orders on both real branches make an accidental m=0-only
|
||||
// integration fail. Positive h keeps fractional powers differentiable;
|
||||
// its nonzero boundary value is handled by the physical replacement rows.
|
||||
for (int mode = 0; mode < shape.size; ++mode) {
|
||||
state(shape.offset + mode) = 0.001 * std::sin(0.7 * (mode + 1));
|
||||
direction(shape.offset + mode) = 0.002 * std::cos(0.41 * (mode + 1));
|
||||
}
|
||||
for (const std::string_view name : {"gravity_gradient", "gravity_potential"}) {
|
||||
const Block &block = findBlock(modal.GetManifest().valueBlocks(), name);
|
||||
for (int i = 0; i < block.size; ++i)
|
||||
state(block.offset + i) = 0.01 * std::cos(0.13 * (i + 1));
|
||||
}
|
||||
|
||||
const mfem::Vector nodalState = liftState(modal, nodal, basis, state);
|
||||
const mfem::Vector nodalDirection = liftState(modal, nodal, basis, direction);
|
||||
prepare(modal, state, 1);
|
||||
prepare(nodal, nodalState, 1);
|
||||
checkClose(
|
||||
modal.GetPhysicalOperator().GetGeneratedVolumeDisplacement(),
|
||||
nodal.GetPhysicalOperator().GetGeneratedVolumeDisplacement(), 2.0e-12, 2.0e-14
|
||||
);
|
||||
|
||||
const auto &modalDomain = modal.GetPhysicalOperator().GetDomainDeformation();
|
||||
const auto &nodalDomain = nodal.GetPhysicalOperator().GetDomainDeformation();
|
||||
CHECK_FALSE(modalDomain.hasNodalRadialParameters());
|
||||
CHECK(nodalDomain.hasNodalRadialParameters());
|
||||
const Block &nodalShape = findBlock(nodal.GetManifest().valueBlocks(), "surface_deformation");
|
||||
const mfem::Vector modalParameters = readBlock(state, shape);
|
||||
const mfem::Vector nodalParameters = readBlock(nodalState, nodalShape);
|
||||
mfem::Vector modalTrace(modalDomain.surfaceDisplacementSize());
|
||||
mfem::Vector nodalTrace(nodalDomain.surfaceDisplacementSize());
|
||||
modalDomain.applySurfaceJacobian(modalParameters, readBlock(direction, shape), modalTrace);
|
||||
nodalDomain.applySurfaceJacobian(nodalParameters, readBlock(nodalDirection, nodalShape), nodalTrace);
|
||||
checkClose(modalTrace, nodalTrace, 3.0e-13, 2.0e-14);
|
||||
mfem::Vector traceDual(modalTrace.Size());
|
||||
for (int i = 0; i < traceDual.Size(); ++i)
|
||||
traceDual(i) = std::sin(0.23 * (i + 1));
|
||||
mfem::Vector modalDual(shape.size), nodalDual(nodalShape.size), expectedDual(shape.size);
|
||||
modalDomain.applySurfaceJacobianTranspose(modalParameters, traceDual, modalDual);
|
||||
nodalDomain.applySurfaceJacobianTranspose(nodalParameters, traceDual, nodalDual);
|
||||
basis.MultTranspose(nodalDual, expectedDual);
|
||||
checkClose(modalDual, expectedDual, 3.0e-13, 2.0e-13);
|
||||
|
||||
mfem::Vector residual, nodalResidual, action, nodalAction;
|
||||
modal.BuildResidual(residual);
|
||||
nodal.BuildResidual(nodalResidual);
|
||||
modal.ApplyLinearization(direction, action);
|
||||
nodal.ApplyLinearization(nodalDirection, nodalAction);
|
||||
checkBlocks(residual, pullResidual(modal, nodal, basis, nodalResidual), modal.GetManifest().residualBlocks());
|
||||
checkBlocks(action, pullResidual(modal, nodal, basis, nodalAction), modal.GetManifest().residualBlocks());
|
||||
|
||||
// A surface-only perturbation must also reach the retained field rows;
|
||||
// a full coupled comparison alone could overlook an inert shape block.
|
||||
mfem::Vector shapeDirection(direction.Size());
|
||||
shapeDirection = 0.0;
|
||||
writeBlock(shapeDirection, shape, readBlock(direction, shape));
|
||||
mfem::Vector shapeAction;
|
||||
modal.ApplyLinearization(shapeDirection, shapeAction);
|
||||
CHECK(
|
||||
readBlock(shapeAction, findBlock(modal.GetManifest().residualBlocks(), "poisson_balance")).Norml2() >
|
||||
1.0e-10
|
||||
);
|
||||
CHECK(readBlock(shapeAction, shapeRows).Norml2() > 1.0e-10);
|
||||
|
||||
constexpr double step = 2.0e-5;
|
||||
mfem::Vector plus(state), minus(state), plusResidual, minusResidual;
|
||||
plus.Add(step, direction);
|
||||
minus.Add(-step, direction);
|
||||
prepare(modal, plus, 2);
|
||||
modal.BuildResidual(plusResidual);
|
||||
prepare(modal, minus, 3);
|
||||
modal.BuildResidual(minusResidual);
|
||||
plusResidual -= minusResidual;
|
||||
plusResidual /= 2.0 * step;
|
||||
checkBlocks(plusResidual, action, modal.GetManifest().residualBlocks(), 2.0e-5, 2.0e-9);
|
||||
|
||||
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(modal);
|
||||
const mfem::Vector fixedStateFactors(normalized.GetNormalization().StateFactors());
|
||||
const mfem::Vector fixedResidualFactors(normalized.GetNormalization().ResidualFactors());
|
||||
REQUIRE(fixedStateFactors.Size() == modal.StateSize());
|
||||
REQUIRE(fixedResidualFactors.Size() == modal.EquationSize());
|
||||
for (int i = 0; i < fixedStateFactors.Size(); ++i) {
|
||||
REQUIRE(std::isfinite(fixedStateFactors(i)));
|
||||
REQUIRE(fixedStateFactors(i) > 0.0);
|
||||
}
|
||||
for (int i = 0; i < fixedResidualFactors.Size(); ++i) {
|
||||
REQUIRE(std::isfinite(fixedResidualFactors(i)));
|
||||
REQUIRE(fixedResidualFactors(i) > 0.0);
|
||||
}
|
||||
const auto prepareNormalized = [&](const mfem::Vector &physical, const std::uint64_t revision) {
|
||||
mfem::Vector coordinates;
|
||||
normalized.NormalizeState(physical, coordinates);
|
||||
if constexpr (GeneratedRotation) {
|
||||
(void)normalized.Prepare(coordinates, dependencies(revision));
|
||||
} else {
|
||||
(void)normalized.Prepare(coordinates, dependencies(revision), zeroRotation());
|
||||
}
|
||||
REQUIRE(normalized.IsPrepared());
|
||||
};
|
||||
prepareNormalized(state, 4);
|
||||
checkClose(normalized.GetPhysicalState(), state, 3.0e-14, 2.0e-14);
|
||||
mfem::Vector normalizedDirection, normalizedAction, expectedAction;
|
||||
normalized.NormalizeState(direction, normalizedDirection);
|
||||
normalized.Mult(normalizedDirection, normalizedAction);
|
||||
normalized.NormalizeResidual(action, expectedAction);
|
||||
checkBlocks(normalizedAction, expectedAction, modal.GetManifest().residualBlocks(), 3.0e-10, 2.0e-12);
|
||||
mfem::Vector normalizedResidual, expectedResidual;
|
||||
normalized.BuildResidual(normalizedResidual);
|
||||
normalized.NormalizeResidual(residual, expectedResidual);
|
||||
checkBlocks(normalizedResidual, expectedResidual, modal.GetManifest().residualBlocks(), 3.0e-10, 2.0e-12);
|
||||
|
||||
// Repeated accepted-state-sized changes must not silently rotate or
|
||||
// rescale persistent harmonic coordinates through a current-shape metric.
|
||||
prepareNormalized(plus, 5);
|
||||
prepareNormalized(state, 6);
|
||||
for (int i = 0; i < fixedStateFactors.Size(); ++i)
|
||||
CHECK(normalized.GetNormalization().StateFactors()(i) == fixedStateFactors(i));
|
||||
for (int i = 0; i < fixedResidualFactors.Size(); ++i)
|
||||
CHECK(normalized.GetNormalization().ResidualFactors()(i) == fixedResidualFactors(i));
|
||||
mfem::Vector restoredResidual, restoredAction;
|
||||
modal.BuildResidual(restoredResidual);
|
||||
modal.ApplyLinearization(direction, restoredAction);
|
||||
checkBlocks(restoredResidual, residual, modal.GetManifest().residualBlocks());
|
||||
checkBlocks(restoredAction, action, modal.GetManifest().residualBlocks());
|
||||
}
|
||||
} // namespace spherical_harmonic_equilibrium_test
|
||||
|
||||
TEST_CASE(
|
||||
"Native Harmonic Stellar Equilibrium Equals The Nodal Pullback Without A Central Constraint",
|
||||
tags::stellar_equilibrium_problem_integration & tags::spherical_harmonic_surface
|
||||
) {
|
||||
int ranks = 0;
|
||||
MPI_Comm_size(MPI_COMM_WORLD, &ranks);
|
||||
if (ranks != 1) {
|
||||
SUCCEED("The current radial domain extension and harmonic prescription require one MPI rank.");
|
||||
return;
|
||||
}
|
||||
SECTION("Prescribed zero rotation") {
|
||||
spherical_harmonic_equilibrium_test::checkCoupledEquivalence<false>();
|
||||
}
|
||||
SECTION("Generated rotation from nonzero target angular momentum") {
|
||||
spherical_harmonic_equilibrium_test::checkCoupledEquivalence<true>();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Harmonic Degree And Azimuthal Cutoffs Determine Native Coupled Block Sizes",
|
||||
tags::stellar_equilibrium_problem_integration & tags::spherical_harmonic_surface
|
||||
) {
|
||||
using namespace mean_field;
|
||||
using namespace spherical_harmonic_equilibrium_test;
|
||||
int ranks = 0;
|
||||
MPI_Comm_size(MPI_COMM_WORLD, &ranks);
|
||||
if (ranks != 1) {
|
||||
SUCCEED("The current radial domain extension and harmonic prescription require one MPI rank.");
|
||||
return;
|
||||
}
|
||||
struct Case final {
|
||||
int degree;
|
||||
std::optional<int> order;
|
||||
int count;
|
||||
};
|
||||
const std::array<Case, 3> cases{{{0, std::nullopt, 1}, {4, 0, 5}, {4, std::nullopt, 25}}};
|
||||
for (const Case &test : cases) {
|
||||
INFO("maximum degree = " << test.degree << ", expected parameters = " << test.count);
|
||||
const utils::Args args = test_utils::setup_args();
|
||||
fem::FEM finiteElements = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(finiteElements.okay());
|
||||
auto problem = equilibrium::discretize(
|
||||
makeModel<false>(),
|
||||
equilibrium::makeStellarDiscretization(
|
||||
std::move(finiteElements), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{1.0}},
|
||||
deformation::SphericalHarmonicSurface{
|
||||
{.maximumDegree = test.degree, .maximumAzimuthalOrder = test.order}
|
||||
}
|
||||
)
|
||||
);
|
||||
CHECK(findBlock(problem.GetManifest().valueBlocks(), "surface_deformation").size == test.count);
|
||||
CHECK(findBlock(problem.GetManifest().residualBlocks(), "surface_shape_balance").size == test.count);
|
||||
CHECK(problem.StateSize() == problem.EquationSize());
|
||||
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
|
||||
REQUIRE(normalized.GetNormalization().StateSize() == problem.StateSize());
|
||||
REQUIRE(normalized.GetNormalization().ResidualSize() == problem.EquationSize());
|
||||
for (const mfem::Vector *factors :
|
||||
{&normalized.GetNormalization().StateFactors(), &normalized.GetNormalization().ResidualFactors()}) {
|
||||
for (int i = 0; i < factors->Size(); ++i) {
|
||||
REQUIRE(std::isfinite((*factors)(i)));
|
||||
REQUIRE((*factors)(i) > 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
182
tests/preconditioning/spherical_harmonic_surface.cpp
Normal file
182
tests/preconditioning/spherical_harmonic_surface.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
namespace spherical_harmonic_preconditioner_test {
|
||||
using namespace mean_field;
|
||||
|
||||
[[nodiscard]] operators::StellarEquilibriumDependencies dependencies() {
|
||||
return {
|
||||
.discretization = {.identity = 9201, .revision = 1},
|
||||
.density = {.identity = 9203, .revision = 1},
|
||||
.surfaceDeformation = {.identity = 9209, .revision = 1},
|
||||
.gravityGradient = {.identity = 9221, .revision = 1},
|
||||
.gravityPotential = {.identity = 9227, .revision = 1},
|
||||
.enthalpy = {.identity = 9239, .revision = 1},
|
||||
.bernoulliConstant = {.identity = 9241, .revision = 1},
|
||||
.rotation = {.identity = 9257, .revision = 1},
|
||||
.targetMass = {.identity = 9277, .revision = 1}
|
||||
};
|
||||
}
|
||||
|
||||
void checkClose(
|
||||
const mfem::Vector &actual,
|
||||
const mfem::Vector &expected
|
||||
) {
|
||||
REQUIRE(actual.Size() == expected.Size());
|
||||
mfem::Vector difference(actual);
|
||||
difference -= expected;
|
||||
const double error = difference.Norml2();
|
||||
const double scale = std::max(actual.Norml2(), expected.Norml2());
|
||||
REQUIRE(std::isfinite(error));
|
||||
REQUIRE(std::isfinite(scale));
|
||||
INFO("inverse error = " << error << ", scale = " << scale);
|
||||
CHECK(error <= 2.e-12 + 3.e-11 * scale);
|
||||
}
|
||||
|
||||
// Construct the advertised triangular surrogate's RHS from a known
|
||||
// correction. This checks the inverse's parameter ordering and material
|
||||
// coupling, without asking an approximate inverse to invert the full J.
|
||||
template <typename Prepared>
|
||||
void checkTriangularInverse(
|
||||
const Prepared &inverse,
|
||||
const int modes
|
||||
) {
|
||||
const auto &couplings = inverse.GetCoupledOperator();
|
||||
const auto &offsets = couplings.GetOffsets();
|
||||
REQUIRE(offsets[2] - offsets[1] == modes);
|
||||
REQUIRE(inverse.Width() == offsets[3]);
|
||||
REQUIRE(inverse.Height() == inverse.Width());
|
||||
REQUIRE(inverse.GetSurfaceDiagonal().Size() == modes);
|
||||
mfem::Vector expected(inverse.Height()), rhs(inverse.Width()), actual(inverse.Height());
|
||||
for (int i = 0; i < expected.Size(); ++i)
|
||||
expected(i) = 0.02 * std::sin(0.31 * (i + 1));
|
||||
const mfem::Vector density(expected.GetData(), offsets[1]);
|
||||
const mfem::Vector shape(expected.GetData() + offsets[1], modes);
|
||||
const mfem::Vector enthalpy(expected.GetData() + offsets[2], offsets[3] - offsets[2]);
|
||||
mfem::Vector densityCoupling(density.Size()), enthalpyCoupling(enthalpy.Size()), cross(density.Size());
|
||||
couplings.ApplySurfaceToMaterial(shape, densityCoupling, enthalpyCoupling);
|
||||
couplings.ApplyEnthalpyToDensity(enthalpy, cross);
|
||||
for (int i = 0; i < density.Size(); ++i)
|
||||
rhs(i) = inverse.GetDensityDiagonal()(i) * density(i) + densityCoupling(i) + cross(i);
|
||||
for (int i = 0; i < modes; ++i)
|
||||
rhs(offsets[1] + i) = inverse.GetSurfaceDiagonal()(i) * shape(i);
|
||||
for (int i = 0; i < enthalpy.Size(); ++i)
|
||||
rhs(offsets[2] + i) = inverse.GetEnthalpyDiagonal()(i) * enthalpy(i) + enthalpyCoupling(i);
|
||||
actual = 0.0;
|
||||
inverse.Mult(rhs, actual);
|
||||
checkClose(actual, expected);
|
||||
const mfem::Vector actualShape(actual.GetData() + offsets[1], modes);
|
||||
checkClose(actualShape, shape);
|
||||
rhs = 0.0;
|
||||
actual = 1.0;
|
||||
inverse.Mult(rhs, actual);
|
||||
CHECK(actual.Norml2() == 0.0);
|
||||
CHECK(inverse.IsCurrent());
|
||||
}
|
||||
} // namespace spherical_harmonic_preconditioner_test
|
||||
|
||||
TEST_CASE(
|
||||
"Harmonic Surface Mass And Calibrated Inverses Use Native Parameters And Reject Nodal H1",
|
||||
"[preconditioning][material_surface][spherical_harmonic][integration][serial]"
|
||||
) {
|
||||
using namespace mean_field;
|
||||
using namespace spherical_harmonic_preconditioner_test;
|
||||
namespace backend = preconditioning::backend;
|
||||
int ranks = 0;
|
||||
MPI_Comm_size(MPI_COMM_WORLD, &ranks);
|
||||
if (ranks != 1)
|
||||
SKIP("The current radial domain extension supports one MPI rank.");
|
||||
|
||||
const auto args = test_utils::setup_args();
|
||||
auto finiteElements = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(finiteElements.okay());
|
||||
const auto stellarModel = model::StellarModel(
|
||||
eos::Polytrope({.n = 1.5, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
|
||||
);
|
||||
// A restricted azimuthal set also rules out using (L+1)^2 as the block
|
||||
// length when the actual mode catalog has fewer independent coefficients.
|
||||
const deformation::SphericalHarmonicSurface prescription{{.maximumDegree = 2, .maximumAzimuthalOrder = 1}};
|
||||
constexpr int modes = 7;
|
||||
auto problem = equilibrium::discretize(
|
||||
stellarModel,
|
||||
equilibrium::makeStellarDiscretization(std::move(finiteElements), normalization::Unnormalized{}, prescription)
|
||||
);
|
||||
mfem::Vector state(problem.StateSize());
|
||||
state = 0.0;
|
||||
const auto view = problem.GetManifest().stateView(state);
|
||||
view.block(utils::blocks::density_field.mass_term) = 1.0;
|
||||
view.block(utils::blocks::enthalpy_field.specific_term) = 0.7;
|
||||
view.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.8;
|
||||
auto shape = view.block(utils::blocks::surface_deformation_field.parameters_term);
|
||||
REQUIRE(shape.Size() == modes);
|
||||
for (int i = 0; i < modes; ++i)
|
||||
shape(i) = 0.001 * std::sin(0.7 * (i + 1));
|
||||
mfem::Vector omega(3), center(3);
|
||||
omega = 0.0;
|
||||
center = 0.0;
|
||||
const auto preparation = problem.TryPrepare(state, dependencies(), physics::RigidRotation{omega, center});
|
||||
REQUIRE(preparation.has_value());
|
||||
const auto &physical = problem.GetPhysicalOperator();
|
||||
REQUIRE(physical.IsPrepared());
|
||||
REQUIRE_FALSE(physical.GetDomainDeformation().hasNodalRadialParameters());
|
||||
|
||||
const auto block = preconditioning::materialSurfaceBlock(problem);
|
||||
STATIC_CHECK(
|
||||
std::is_same_v<
|
||||
typename std::remove_cvref_t<decltype(block)>::SurfaceSurrogate, preconditioning::SurfaceMassDiagonal>
|
||||
);
|
||||
auto prepared = preconditioning::prepare(problem, block);
|
||||
CHECK(prepared.GetStatistics().surfaceRieszAssemblies == 1);
|
||||
CHECK(prepared.GetStatistics().surfaceJacobianProbes == 0);
|
||||
CHECK(prepared.GetSurfaceDiagonalQuality().minimumAbsoluteEntryBeforeRegularization > 0.0);
|
||||
checkTriangularInverse(prepared, modes);
|
||||
const auto unchanged = prepared.Refresh(physical);
|
||||
CHECK_FALSE(unchanged.DidAnyWork());
|
||||
|
||||
const auto calibratedBlock = preconditioning::materialSurfaceBlock(
|
||||
problem, backend::Diagonal{}, backend::Diagonal{}, preconditioning::SurfaceThenMaterialTriangular{},
|
||||
{.surfaceCalibration = {
|
||||
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian,
|
||||
.probeCount = 3,
|
||||
.objective = preconditioning::SurfaceRieszCalibrationObjective::operator_action
|
||||
}}
|
||||
);
|
||||
auto calibrated = preconditioning::prepare(problem, calibratedBlock);
|
||||
const auto &calibration = calibrated.GetSurfaceCalibration();
|
||||
REQUIRE(calibration.WasCalibrated());
|
||||
REQUIRE(std::isfinite(calibration.scale));
|
||||
REQUIRE(calibration.scale != 0.0);
|
||||
CHECK(calibration.probeCount == 3);
|
||||
CHECK(calibrated.GetStatistics().surfaceJacobianProbes == 3);
|
||||
for (int i = 0; i < modes; ++i)
|
||||
CHECK(
|
||||
calibrated.GetSurfaceDiagonal()(i) ==
|
||||
Catch::Approx(calibration.scale * prepared.GetSurfaceDiagonal()(i)).epsilon(2.e-13)
|
||||
);
|
||||
checkTriangularInverse(calibrated, modes);
|
||||
|
||||
using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
|
||||
const auto h1 = preconditioning::materialSurfaceBlock(
|
||||
problem, backend::Diagonal{}, FixedAMG{backend::FixedCycles{.cycles = 1}},
|
||||
preconditioning::ApproximateMaterialSurfaceLDU{}, preconditioning::SurfaceH1MassStiffness{}
|
||||
);
|
||||
CHECK_THROWS_WITH(
|
||||
preconditioning::prepare(problem, h1), Catch::Matchers::ContainsSubstring("requires nodal radial parameters")
|
||||
);
|
||||
CHECK(prepared.IsCurrent());
|
||||
CHECK(calibrated.IsCurrent());
|
||||
}
|
||||
@@ -347,6 +347,14 @@ export namespace tags {
|
||||
nodal_radial_surface & integration & accuracy & make_tag("analytic_comparison");
|
||||
inline constexpr auto nodal_radial_surface_linearization =
|
||||
nodal_radial_surface & integration & make_tag("jacobian") & make_tag("adjoint");
|
||||
inline constexpr auto spherical_harmonic_surface =
|
||||
surface & geometry & make_tag("surface_deformation") & make_tag("spherical_harmonic");
|
||||
inline constexpr auto spherical_harmonic_surface_validation =
|
||||
spherical_harmonic_surface & unit & make_tag("validation");
|
||||
inline constexpr auto spherical_harmonic_surface_analytic =
|
||||
spherical_harmonic_surface & integration & accuracy & make_tag("analytic_comparison");
|
||||
inline constexpr auto spherical_harmonic_surface_linearization =
|
||||
spherical_harmonic_surface & integration & make_tag("jacobian") & make_tag("adjoint");
|
||||
inline constexpr auto radial_deformation_extension = deformation & mapping & make_tag("radial_extension");
|
||||
inline constexpr auto radial_deformation_extension_validation =
|
||||
radial_deformation_extension & unit & make_tag("validation");
|
||||
|
||||
Reference in New Issue
Block a user