feat(mean_field): added initial implementation

note this implementation lacks many tests
This commit is contained in:
2026-07-15 09:44:43 -04:00
commit 9bc4f2758a
49 changed files with 171811 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
module;
#include <mfem.hpp>
module mean_field;
namespace mean_field::utils {
bool GetReferencePoint(
const fem::FEM &fem,
const mfem::Vector &x_phys_target,
mfem::Vector &x_ref
) {
const int dim = fem.mesh->Dimension();
x_ref = x_phys_target;
mfem::Array<int> init_elem;
mfem::Array<mfem::IntegrationPoint> init_ip;
mfem::DenseMatrix init_P(dim, 1);
init_P.SetCol(0, x_ref);
fem.mesh->FindPoints(init_P, init_elem, init_ip, false);
if (init_elem.Size() == 0 || init_elem[0] < 0) {
mfem::Vector origin(dim);
origin = 0.0;
mfem::DenseMatrix P_origin(dim, 1);
P_origin.SetCol(0, origin);
mfem::Array<int> origin_elem;
mfem::Array<mfem::IntegrationPoint> origin_ip;
fem.mesh->FindPoints(P_origin, origin_elem, origin_ip, false);
if (origin_elem.Size() > 0 && origin_elem[0] >= 0 && !fem.mapping->IsIdentity()) {
mfem::ElementTransformation *T0 = fem.mesh->GetElementTransformation(origin_elem[0]);
T0->SetIntPoint(&origin_ip[0]);
mfem::DenseMatrix J0(dim, dim), J0_inv(dim, dim);
fem.mapping->ComputeJacobian(*T0, J0);
mfem::CalcInverse(J0, J0_inv);
J0_inv.Mult(x_phys_target, x_ref);
}
init_P.SetCol(0, x_ref);
fem.mesh->FindPoints(init_P, init_elem, init_ip, false);
if (init_elem.Size() == 0 || init_elem[0] < 0) {
double norm = x_ref.Norml2();
if (norm > 1e-15) {
double scale = 0.9 * RADIUS / norm;
if (scale < 1.0) {
x_ref *= scale;
}
}
init_P.SetCol(0, x_ref);
fem.mesh->FindPoints(init_P, init_elem, init_ip, false);
if (init_elem.Size() == 0 || init_elem[0] < 0) {
x_ref = 0.0;
}
}
}
constexpr int max_iter = 50;
mfem::Array<int> elem_ids;
mfem::Array<mfem::IntegrationPoint> ips;
mfem::DenseMatrix P(dim, 1);
mfem::Vector d(dim);
mfem::Vector residual(dim);
mfem::Vector step(dim);
mfem::DenseMatrix J_map(dim, dim);
mfem::DenseMatrix J_map_inv(dim, dim);
int find_failures = 0;
for (int iter = 0; iter < max_iter; ++iter) {
P.SetCol(0, x_ref);
fem.mesh->FindPoints(P, elem_ids, ips, false);
if (elem_ids.Size() == 0 || elem_ids[0] < 0) {
find_failures++;
if (find_failures > 10) return false;
double norm = x_ref.Norml2();
if (norm > 1e-15) {
x_ref *= 0.5 * RADIUS / norm;
} else {
x_ref = 0.0;
}
continue;
}
int elemID = elem_ids[0];
const mfem::IntegrationPoint &ip = ips[0];
mfem::ElementTransformation *T = fem.mesh->GetElementTransformation(elemID);
T->SetIntPoint(&ip);
mfem::Vector current_x_phys(dim);
fem.mapping->GetPhysicalPoint(*T, ip, current_x_phys);
for (int i = 0; i < dim; ++i) {
residual(i) = current_x_phys(i) - x_phys_target(i);
}
if (constexpr double tol = 1e-12; residual.Norml2() < tol) {
return true;
}
fem.mapping->ComputeJacobian(*T, J_map);
mfem::CalcInverse(J_map, J_map_inv);
J_map_inv.Mult(residual, step);
double alpha = 1.0;
mfem::Vector x_ref_candidate(dim);
bool found_valid = false;
for (int ls = 0; ls < 8; ++ls) {
x_ref_candidate = x_ref;
x_ref_candidate.Add(-alpha, step);
P.SetCol(0, x_ref_candidate);
fem.mesh->FindPoints(P, elem_ids, ips, false);
if (elem_ids.Size() > 0 && elem_ids[0] >= 0) {
found_valid = true;
break;
}
alpha *= 0.5;
}
if (found_valid) {
x_ref = x_ref_candidate;
} else {
find_failures++;
if (find_failures > 10) return false;
if (double norm = x_ref.Norml2(); norm > 1e-15) {
x_ref *= 0.5 * RADIUS / norm;
} else {
x_ref = 0.0;
}
}
}
return false;
}
double EvalGridFunctionAtPoint(
const fem::FEM &fem,
const mfem::ParGridFunction &u,
const mfem::Vector &x,
const mapping::COORDINATE_SPACE vspace,
const mapping::COORDINATE_SPACE rspace
) {
mfem::Vector x_search;
if (vspace == mapping::COORDINATE_SPACE::PHYSICAL && fem.has_mapping()) {
GetReferencePoint(fem, x, x_search);
} else {
x_search = x;
}
mfem::Array<int> elem_ids;
mfem::Array<mfem::IntegrationPoint> ips;
mfem::DenseMatrix P(x_search.Size(), 1);
P.SetCol(0, x_search);
fem.mesh->FindPoints(P, elem_ids, ips, false);
double local_val = 0.0;
if (elem_ids.Size() > 0 && elem_ids[0] >= 0) {
const double val = u.GetValue(elem_ids[0], ips[0]);
if (rspace == mapping::COORDINATE_SPACE::PHYSICAL && !fem.has_mapping()) {
MFEM_ABORT("Physical evaluation mode requested but no mapping provided. Check domain bounds and mapping setup.");
}
local_val = val;
}
double global_val = 0.0;
MPI_Allreduce(&local_val, &global_val, 1, MPI_DOUBLE, MPI_MAX, fem.H1_fes->GetComm());
return global_val;
}
}

View File

@@ -0,0 +1,124 @@
module;
#include <mfem.hpp>
#include <expected>
module mean_field;
import :boundary.contexts;
namespace mean_field::utils {
DOMAINS operator|(
DOMAINS lhs,
DOMAINS rhs
) {
return static_cast<DOMAINS>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
DOMAINS operator&(
DOMAINS lhs,
DOMAINS rhs
) {
return static_cast<DOMAINS>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
}
void populate_element_mask(
const mfem::Mesh* mesh,
const DOMAINS domain,
mfem::Array<int> &mask
) {
const int max_attr = mesh->attributes.Max();
mask.SetSize(max_attr);
mask = 0;
if ((domain & DOMAINS::CORE) == DOMAINS::CORE && max_attr >= 1) {
mask[0] = 1;
}
if ((domain & DOMAINS::ENVELOPE) == DOMAINS::ENVELOPE && max_attr >= 2) {
mask[1] = 1;
}
if ((domain & DOMAINS::VACUUM) == DOMAINS::VACUUM && max_attr >= 3) {
mask[2] = 1;
}
}
void populate_domain_tdofs(
const mfem::ParFiniteElementSpace *fes,
const mfem::Array<int> &element_mask,
mfem::Array<int> &ess_tdof
) {
mfem::Array<int> vdof_marker(fes->GetVSize());
vdof_marker = 0;
for (int i = 0; i < fes->GetMesh()->GetNE(); i++) {
const int attr = fes->GetMesh()->GetAttribute(i);
if (element_mask[attr - 1]) {
mfem::Array<int> dofs;
fes->GetElementVDofs(i, dofs);
for (int j = 0; j < dofs.Size(); j++) {
int index = dofs[j];
if (index < 0) index = -1 - index;
vdof_marker[index] = 1;
}
}
}
fes->MarkerToList(vdof_marker, ess_tdof);
}
std::expected<boundary::Bounds, boundary::BoundsError> discover_bounds(
const mfem::Mesh *mesh,
const int vacuum_attr
) {
double local_min_r = std::numeric_limits<double>::max();
double local_max_r = -std::numeric_limits<double>::max();
bool found_vacuum = false;
for (int i = 0; i < mesh->GetNE(); ++i) {
if (mesh->GetAttribute(i) == vacuum_attr) {
found_vacuum = true;
mfem::Array<int> vertices;
mesh->GetElementVertices(i, vertices);
for (const int v: vertices) {
const double *coords = mesh->GetVertex(v);
double r = std::sqrt(coords[0] * coords[0] + coords[1] * coords[1] + coords[2] * coords[2]);
local_min_r = std::min(local_min_r, r);
local_max_r = std::max(local_max_r, r);
}
}
}
double global_min_r, global_max_r;
int global_found_vacuum;
int l_found = found_vacuum ? 1 : 0;
MPI_Comm comm = MPI_COMM_WORLD;
if (const auto *pmesh = dynamic_cast<const mfem::ParMesh *>(mesh)) {
comm = pmesh->GetComm();
}
MPI_Allreduce(&local_min_r, &global_min_r, 1, MPI_DOUBLE, MPI_MIN, comm);
MPI_Allreduce(&local_max_r, &global_max_r, 1, MPI_DOUBLE, MPI_MAX, comm);
MPI_Allreduce(&l_found, &global_found_vacuum, 1, MPI_INT, MPI_MAX, comm);
if (global_found_vacuum) {
return boundary::Bounds(global_min_r, global_max_r);
}
return std::unexpected(boundary::BoundsError::CANNOT_FIND_VACUUM);
}
int get_mesh_order(
const mfem::Mesh &mesh
) {
if (mesh.GetNodes() != nullptr) {
return mesh.GetNodes()->FESpace()->GetMaxElementOrder();
}
return 1;
}
}