Files
MeanField/experiments/plateau_enthalpy_step_diagnostics.hpp

227 lines
13 KiB
C++

#pragma once
// Include after import mean_field. This observer does not change accepted
// coefficients, the prepared operator, or the Newton correction.
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <map>
#include <stdexcept>
#include <string>
#include <mfem.hpp>
#include <mpi.h>
namespace experiment::plateau_diagnostics {
// Computes the forward h>=0 boundary along the CURRENT normalizedCorrection
// on exactly the production closure and pressure-force quadrature points.
// n must match the active polytropic EOS. Scalar
// enthalpy coefficients pull back unchanged under the moving-domain map,
// so h_q(alpha)=h_q(0)+alpha*dh_q: no geometry derivative is needed here.
//
// Source convention: prepared_barotropic_closure.cpp get_eos_rule (lines
// 102-157) and TryPrepare's basis contraction/domain check (lines 593-632).
// The production operator rejects h<0 strictly, despite the standalone
// Polytrope EOS extension to h<=0. Pressure-force uses the PressureForce
// query with extraOrder=ceil(n*p_h), as in prepared_pressure_force.cpp
// lines 226-282, and also rejects h<0 (lines 787-803). Its tensor and dense
// contraction paths sample the same polynomial and can differ at roundoff.
// The union bound concerns h>=0 only; geometry and other finite-data tests
// can still impose stricter limits.
//
// alpha_h=+infinity means no sampled enthalpy upper bound in this direction.
// alpha_h=NaN means the accepted base was already invalid or a sampled value
// was nonfinite. alpha_h=0 means a sampled h=0 has dh<0. A step exactly at
// the reported boundary can differ at roundoff due to floating-point state
// addition and contraction. alpha_h remains the closure alias;
// closure_alpha_h, pressure_alpha_h, and alpha_thermodynamic are explicit.
// No candidate is prepared or committed here. physicalBase optionally
// selects a caller-owned frozen trial state instead of the accepted state;
// its coefficients must use this context's physical root layout.
template <class State>
std::map<std::string, double> MeasureEnthalpyStep(State &state, const mean_field::fem::FEM &fem, const double n,
const mfem::Vector *physicalBase = nullptr) {
if (!std::isfinite(n) || n < 1.0)
throw std::invalid_argument("Enthalpy step diagnostics require finite n>=1.");
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using ClosureDomain = mean_field::field::FieldDomainT<mean_field::field::Density>;
using PressureDomain = mean_field::field::FieldDomainT<mean_field::field::Enthalpy>;
using EnthalpyField = mean_field::field::Field<mean_field::field::Enthalpy>;
int ranks = 0;
MPI_Comm_size(fem.mesh->GetComm(), &ranks);
if (ranks != 1)
throw std::invalid_argument("Enthalpy step diagnostics require exactly one MPI rank.");
if (!fem.enthalpyFes || !fem.densityFes || !fem.displacementFes || !fem.quadratureFactory) {
throw std::invalid_argument("Enthalpy step diagnostics received an incomplete FEM.");
}
const double maximumExtra = n * mean_field::field::Enthalpy::Scalar::familyOrder;
if (!std::isfinite(maximumExtra) || maximumExtra > std::numeric_limits<int>::max()) {
throw std::invalid_argument("Enthalpy step diagnostics received an invalid effective EOS order.");
}
int offset = -1, size = -1;
for (const auto &block : state.Problem().GetManifest().valueBlocks()) {
if (block.stableId == "specific_enthalpy") {
offset = block.offset;
size = block.size;
break;
}
}
mfem::Vector physicalCorrection;
state.normalizedOperator->DenormalizeState(state.normalizedCorrection, physicalCorrection);
const auto &accepted = physicalBase ? *physicalBase : state.AcceptedPhysicalState();
mfem::ParGridFunction h(fem.enthalpyFes.get()), dh(fem.enthalpyFes.get());
const auto adapter =
mean_field::field::make_field_dof_grid_function_adapter<mean_field::field::Enthalpy, DomainSchema>(
*fem.enthalpyFes);
if (offset < 0 || size != adapter.dof_map().reduced_size() || offset + size > accepted.Size() ||
physicalCorrection.Size() != accepted.Size()) {
throw std::invalid_argument("Enthalpy step diagnostics found an incompatible enthalpy block.");
}
mfem::Vector reducedH(size), reducedDh(size);
for (int i = 0; i < size; ++i) {
reducedH(i) = accepted(offset + i);
reducedDh(i) = physicalCorrection(offset + i);
}
adapter.scatter(reducedH, h);
adapter.scatter(reducedDh, dh);
const double nan = std::numeric_limits<double>::quiet_NaN();
const double inf = std::numeric_limits<double>::infinity();
const auto scan = [&](const bool pressure) -> std::map<std::string, double> {
const int extraOrder =
static_cast<int>(std::ceil((pressure ? n : n - 1.0) * mean_field::field::Enthalpy::Scalar::familyOrder));
double alpha = inf, minH = inf, minDh = inf, minFullStepH = inf;
double limitingH = nan, limitingDh = nan;
int limitingElement = -1, limitingPoint = -1, limitingAttribute = -1;
std::array<double, 3> limitingIp{nan, nan, nan};
int minimumHElement = -1, minimumHPoint = -1;
int rulePointCountMin = std::numeric_limits<int>::max(), rulePointCountMax = 0;
std::uint64_t samples = 0, nonfiniteSamples = 0, negativeBaseSamples = 0, decreasingSamples = 0,
negativeFullStepSamples = 0;
mfem::Array<int> dofs;
mfem::Vector localH, localDh, shape;
for (int element = 0; element < fem.mesh->GetNE(); ++element) {
auto *transformation = fem.mesh->GetElementTransformation(element);
const bool inSupport =
pressure ? DomainSchema::template attribute_belongs_to<PressureDomain>(transformation->Attribute)
: DomainSchema::template attribute_belongs_to<ClosureDomain>(transformation->Attribute);
if (!inSupport)
continue;
const auto &densityElement = *fem.densityFes->GetFE(element);
const auto &enthalpyElement = *fem.enthalpyFes->GetFE(element);
if (enthalpyElement.GetOrder() != mean_field::field::Enthalpy::Scalar::familyOrder ||
(!pressure && densityElement.GetOrder() != mean_field::field::Density::Scalar::familyOrder) ||
(pressure && fem.displacementFes->GetFE(element)->GetOrder() !=
mean_field::field::Displacement::Vector::familyOrder)) {
throw std::invalid_argument(
"Enthalpy step diagnostics require the registered production field orders.");
}
const auto query =
pressure ? EnthalpyField::template make_query<mean_field::field::Enthalpy::Form::PressureForce>(
mean_field::quadrature::QuadratureRole::discretization, transformation->OrderW(),
std::array<int, 1>{extraOrder}, mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general)
: EnthalpyField::template make_query<mean_field::field::Enthalpy::Form::EosClosureSource>(
mean_field::quadrature::QuadratureRole::discretization, transformation->OrderW(),
std::array<int, 1>{extraOrder}, mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general);
const auto resolution = fem.quadratureFactory->get(query, transformation->GetGeometryType());
if (!resolution.integration_rule)
throw std::runtime_error("Missing production EOS quadrature rule.");
const auto &rule = *resolution.integration_rule;
rulePointCountMin = std::min(rulePointCountMin, rule.GetNPoints());
rulePointCountMax = std::max(rulePointCountMax, rule.GetNPoints());
const auto basis = fem.GetReferenceTables().GetScalarTable(enthalpyElement, rule);
auto *dofTransformation = fem.enthalpyFes->GetElementDofs(element, dofs);
h.GetSubVector(dofs, localH);
dh.GetSubVector(dofs, localDh);
if (dofTransformation) {
dofTransformation->InvTransformPrimal(localH);
dofTransformation->InvTransformPrimal(localDh);
}
shape.SetSize(enthalpyElement.GetDof());
for (int q = 0; q < rule.GetNPoints(); ++q) {
++samples;
basis->GetValues().GetRow(q, shape);
const double hq = localH * shape;
const double dhq = localDh * shape;
if (!std::isfinite(hq) || !std::isfinite(dhq)) {
++nonfiniteSamples;
continue;
}
if (hq < minH) {
minH = hq;
minimumHElement = element;
minimumHPoint = q;
}
minDh = std::min(minDh, dhq);
minFullStepH = std::min(minFullStepH, hq + dhq);
negativeBaseSamples += hq < 0.0;
negativeFullStepSamples += hq + dhq < 0.0;
if (dhq < 0.0) {
++decreasingSamples;
const double candidate = -hq / dhq;
if (candidate < alpha) {
alpha = candidate;
limitingH = hq;
limitingDh = dhq;
limitingElement = element;
limitingPoint = q;
limitingAttribute = transformation->Attribute;
const auto &ip = rule.IntPoint(q);
limitingIp = {ip.x, ip.y, ip.z};
}
}
}
}
const bool valid = samples > 0 && nonfiniteSamples == 0 && negativeBaseSamples == 0;
return {{"alpha_h", valid ? alpha : nan},
{"alpha_h_raw_crossing", alpha},
{"alpha_h_clipped_to_one", valid ? std::min(1.0, alpha) : nan},
{"enthalpy_base_admissible", valid ? 1.0 : 0.0},
{"eos_extra_order", static_cast<double>(extraOrder)},
{"samples", static_cast<double>(samples)},
{"nonfinite_samples", static_cast<double>(nonfiniteSamples)},
{"negative_base_samples", static_cast<double>(negativeBaseSamples)},
{"decreasing_samples", static_cast<double>(decreasingSamples)},
{"predicted_negative_full_step_samples", static_cast<double>(negativeFullStepSamples)},
{"minimum_h", samples > nonfiniteSamples ? minH : nan},
{"minimum_dh", samples > nonfiniteSamples ? minDh : nan},
{"predicted_minimum_h_full_step", samples > nonfiniteSamples ? minFullStepH : nan},
{"minimum_h_element", static_cast<double>(minimumHElement)},
{"minimum_h_point", static_cast<double>(minimumHPoint)},
{"limiting_element", static_cast<double>(limitingElement)},
{"limiting_point", static_cast<double>(limitingPoint)},
{"limiting_attribute", static_cast<double>(limitingAttribute)},
{"limiting_h", limitingH},
{"limiting_dh", limitingDh},
{"limiting_ip_x", limitingIp[0]},
{"limiting_ip_y", limitingIp[1]},
{"limiting_ip_z", limitingIp[2]},
{"quadrature_points_per_element_minimum", samples ? static_cast<double>(rulePointCountMin) : nan},
{"quadrature_points_per_element_maximum", static_cast<double>(rulePointCountMax)}};
};
auto result = scan(false);
const auto pressure = scan(true);
result["closure_alpha_h"] = result.at("alpha_h");
result["closure_samples"] = result.at("samples");
for (const auto &[name, value] : pressure)
result["pressure_" + name] = value;
const bool unionValid =
result.at("enthalpy_base_admissible") != 0.0 && pressure.at("enthalpy_base_admissible") != 0.0;
const double unionAlpha = unionValid ? std::min(result.at("alpha_h"), pressure.at("alpha_h")) : nan;
result["alpha_thermodynamic"] = unionAlpha;
result["alpha_thermodynamic_clipped_to_one"] = unionValid ? std::min(1.0, unionAlpha) : nan;
result["thermodynamic_base_admissible"] = unionValid ? 1.0 : 0.0;
// 0=closure, 1=pressure-force, -1=invalid or no finite upper bound.
result["thermodynamic_limiting_family"] =
unionValid && std::isfinite(unionAlpha) ? (pressure.at("alpha_h") < result.at("alpha_h") ? 1.0 : 0.0) : -1.0;
result["thermodynamic_minimum_h"] = std::isfinite(result.at("minimum_h")) && std::isfinite(pressure.at("minimum_h"))
? std::min(result.at("minimum_h"), pressure.at("minimum_h"))
: nan;
return result;
}
} // namespace experiment::plateau_diagnostics