#pragma once // Diagnostic only. Include after import mean_field; invoke inside WithState. #include #include #include #include #include #include #include #include #include #include #include #include "plateau_enthalpy_step_diagnostics.hpp" namespace experiment::plateau_diagnostics { namespace pressure_obstruction_detail { inline std::ofstream Open(const std::filesystem::path &path) { if (std::filesystem::exists(path)) throw std::runtime_error("Refusing to overwrite pressure-obstruction output: " + path.string()); std::ofstream stream(path); stream.exceptions(std::ios::failbit | std::ios::badbit); stream << std::setprecision(17); return stream; } inline const char *ReasonName(mean_field::operators::StellarEquilibriumPreparationRejectionReason reason) { using R = mean_field::operators::StellarEquilibriumPreparationRejectionReason; switch (reason) { case R::inverted_geometry: return "inverted_geometry"; case R::non_finite_geometry: return "non_finite_geometry"; case R::thermodynamic_domain: return "thermodynamic_domain"; case R::non_finite_thermodynamics: return "non_finite_thermodynamics"; case R::inadmissible_physics: return "inadmissible_physics"; case R::non_finite_physics: return "non_finite_physics"; } return "unknown"; } inline bool Finite(const mfem::Vector &v) { for (int i = 0; i < v.Size(); ++i) if (!std::isfinite(v(i))) return false; return true; } struct Limit { std::string family; int element{-1}, point{-1}, attribute{-1}; mfem::IntegrationPoint ip; double alpha{std::numeric_limits::infinity()}; double h{std::numeric_limits::quiet_NaN()}; double dh{std::numeric_limits::quiet_NaN()}; }; inline void AddAlpha(std::vector &values, double alpha, double boundary) { if (!std::isfinite(alpha) || alpha <= 0.0) return; // The boundary itself is deliberately excluded: algebraically equivalent // state addition / polynomial contractions can differ at roundoff there. if (std::isfinite(boundary) && boundary > 0.0 && std::abs(alpha / boundary - 1.0) < 1.e-10) return; for (double previous : values) if (std::abs(alpha - previous) <= 1.e-12 * std::max(alpha, previous)) return; values.push_back(alpha); } } // namespace pressure_obstruction_detail template void ProbePressureObstruction(State &s, const mean_field::fem::FEM &f, double n, const mfem::Vector &normalizedBase, const mfem::Vector &normalizedDirection, const std::filesystem::path &output) { using namespace mean_field; namespace detail = pressure_obstruction_detail; using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; if (!f.mesh || !f.enthalpyFes || !f.displacementFes || !f.compactificationCoordinate) throw std::invalid_argument("Pressure obstruction requires complete field/mapping data."); int ranks = 0; if (MPI_Comm_size(f.mesh->GetComm(), &ranks) != MPI_SUCCESS || ranks != 1) throw std::invalid_argument("Pressure obstruction requires exactly one MPI rank."); if (normalizedBase.Size() != s.Problem().StateSize() || normalizedDirection.Size() != normalizedBase.Size() || !detail::Finite(normalizedBase) || !detail::Finite(normalizedDirection)) throw std::invalid_argument("Pressure obstruction requires finite compatible base/direction vectors."); if (!std::filesystem::is_directory(output)) throw std::invalid_argument("Pressure obstruction requires an existing output directory."); // Own these snapshots: callers may pass one of the live workspaces. const mfem::Vector frozen(normalizedBase), direction(normalizedDirection), savedCorrection(s.normalizedCorrection); auto restore = [&]() { s.trialNormalizedState = frozen; if (!s.PrepareTrial()) throw std::runtime_error("Pressure-obstruction frozen base is inadmissible."); }; auto finish = [&]() { restore(); s.physicalInverse->Refresh(); s.normalizedCorrection = savedCorrection; }; try { restore(); s.normalizedCorrection = direction; mfem::Vector physicalBase, physicalDirection; s.normalizedOperator->DenormalizeState(frozen, physicalBase); s.normalizedOperator->DenormalizeState(direction, physicalDirection); const auto measures = MeasureEnthalpyStep(s, f, n, &physicalBase); const double boundary = measures.at("alpha_thermodynamic"); if (measures.at("thermodynamic_base_admissible") != 1.0 || std::isnan(boundary)) throw std::runtime_error("Production-rule scan found an invalid frozen enthalpy base."); auto summary = detail::Open(output / "pressure_obstruction_summary.csv"); summary << "metric,value\n"; for (const auto &[key, value] : measures) summary << key << ',' << value << '\n'; summary << "base_normalized_residual," << s.trialNormalizedResidual.Norml2() << '\n'; summary << "normalized_direction_norm," << direction.Norml2() << '\n'; mfem::Vector enthalpyDirection(direction.Size()), shapeDirection(direction.Size()); enthalpyDirection = 0.0; shapeDirection = 0.0; int hOffset = -1, hSize = -1, shapeSize = -1; for (const auto &block : s.Problem().GetManifest().valueBlocks()) { if (block.stableId == "specific_enthalpy") { hOffset = block.offset; hSize = block.size; for (int i = 0; i < block.size; ++i) enthalpyDirection(block.offset + i) = direction(block.offset + i); } if (block.stableId == "surface_deformation") { shapeSize = block.size; for (int i = 0; i < block.size; ++i) shapeDirection(block.offset + i) = direction(block.offset + i); } } if (hOffset < 0 || hSize <= 0 || shapeSize <= 0) throw std::runtime_error("Pressure obstruction could not locate enthalpy and shape blocks."); summary << "normalized_enthalpy_direction_norm," << enthalpyDirection.Norml2() << '\n'; summary << "normalized_shape_direction_norm," << shapeDirection.Norml2() << '\n'; std::array limits; for (int i = 0; i < 2; ++i) { auto &limit = limits[i]; const std::string prefix = i == 0 ? "" : "pressure_"; limit.family = i == 0 ? "closure" : "pressure"; limit.alpha = measures.at(prefix + "alpha_h"); limit.element = static_cast(measures.at(prefix + "limiting_element")); limit.point = static_cast(measures.at(prefix + "limiting_point")); limit.attribute = static_cast(measures.at(prefix + "limiting_attribute")); limit.h = measures.at(prefix + "limiting_h"); limit.dh = measures.at(prefix + "limiting_dh"); limit.ip.Set3(measures.at(prefix + "limiting_ip_x"), measures.at(prefix + "limiting_ip_y"), measures.at(prefix + "limiting_ip_z")); } // Surface is a material interface, not necessarily the outer mesh boundary. std::vector touchesSurface(static_cast(f.mesh->GetNE()), false); const int surfaceAttribute = DomainSchema::template boundary_attribute(); for (int b = 0; b < f.mesh->GetNBE(); ++b) { if (f.mesh->GetBdrAttribute(b) != surfaceAttribute) continue; const int face = f.mesh->GetBdrElementFaceIndex(b); if (face < 0) throw std::runtime_error("Missing stellar-surface mesh face."); auto *faces = f.mesh->GetFaceElementTransformations(face); if (!faces) throw std::runtime_error("Missing stellar-surface face transformation."); for (int element : {faces->Elem1No, faces->Elem2No}) if (element >= 0 && element < f.mesh->GetNE() && DomainSchema::template attribute_belongs_to(f.mesh->GetAttribute(element))) touchesSurface[static_cast(element)] = true; } mfem::ParGridFunction displacement(f.displacementFes.get()), trialH(f.enthalpyFes.get()); displacement.SetFromTrueDofs(s.Problem().GetPhysicalOperator().GetGeneratedVolumeDisplacement()); mapping::GridFunctionMappingEvaluator mapper(s.Problem().GetDiscretization().domainMapper(), displacement, *f.compactificationCoordinate); auto location = detail::Open(output / "pressure_obstruction_limits.csv"); location << "family,element,point,attribute,alpha_h,h,dh,ip_x,ip_y,ip_z,element_touches_stellar_surface," "mapping_status,reference_x,reference_y,reference_z,reference_radius,physical_x,physical_y," "physical_z,physical_radius\n"; const double nan = std::numeric_limits::quiet_NaN(); for (const auto &limit : limits) { location << limit.family << ',' << limit.element << ',' << limit.point << ',' << limit.attribute << ',' << limit.alpha << ',' << limit.h << ',' << limit.dh << ',' << limit.ip.x << ',' << limit.ip.y << ',' << limit.ip.z << ','; if (limit.element < 0) { location << "-1,-1,nan,nan,nan,nan,nan,nan,nan,nan\n"; continue; } mapping::MappingPointContext point; auto *transformation = f.mesh->GetElementTransformation(limit.element); const auto status = mapper.EvaluatePoint(*transformation, limit.ip, point); location << touchesSurface[static_cast(limit.element)] << ',' << static_cast(status); if (status == mapping::MappingStatus::valid) { for (int d = 0; d < 3; ++d) location << ',' << point.reference_position(d); location << ',' << point.reference_position.Norml2(); for (int d = 0; d < 3; ++d) location << ',' << point.physical_position(d); location << ',' << point.physical_position.Norml2() << '\n'; } else location << ",nan,nan,nan,nan,nan,nan,nan,nan\n"; } summary.flush(); location.flush(); auto trials = detail::Open(output / "pressure_obstruction_trials.csv"); trials << "direction,alpha,alpha_over_union_bound,admissible,reason,reason_name,stage,eos_code," "eos_code_applicable,min_jacobian,normalized_residual_norm,physical_residual_norm," "closure_limiting_h_prediction,pressure_limiting_h_prediction," "closure_limiting_h_evaluated,pressure_limiting_h_evaluated\n"; auto blocks = detail::Open(output / "pressure_obstruction_blocks.csv"); blocks << "direction,alpha,block,normalized_residual_norm\n"; const auto adapter = field::make_field_dof_grid_function_adapter(*f.enthalpyFes); mfem::Vector candidatePhysical, reducedH(hSize); std::vector fullAlphas, splitAlphas; if (std::isfinite(boundary) && boundary > 0.0) { for (double factor : {.5, .9, .99, 1.01, 1.1, 2.0}) detail::AddAlpha(fullAlphas, factor * boundary, boundary); for (double factor : {.9, .99, 1.01, 1.1}) detail::AddAlpha(splitAlphas, factor * boundary, boundary); } else { // Zero: every positive step may violate h>=0. Infinity: no sampled bound. for (double a : (boundary == 0.0 ? std::array{1.e-6, 1.e-4} : std::array{.5, 1.0})) detail::AddAlpha(splitAlphas, a, boundary); for (double a : splitAlphas) detail::AddAlpha(fullAlphas, a, boundary); } for (double a : {1.0, .5, .25, .125, .0625}) detail::AddAlpha(fullAlphas, a, boundary); summary << "candidate_trial_count," << fullAlphas.size() + 2 * splitAlphas.size() << '\n'; summary.flush(); auto probe = [&](const char *name, const mfem::Vector &d, const std::vector &alphas, bool changesH) { for (double alpha : alphas) { restore(); s.normalizedCorrection = d; s.trialNormalizedState = frozen; s.trialNormalizedState.Add(alpha, d); s.normalizedOperator->DenormalizeState(s.trialNormalizedState, candidatePhysical); if (!detail::Finite(candidatePhysical) || !detail::Finite(s.trialNormalizedState)) throw std::runtime_error("Pressure-obstruction trial coefficients overflowed; no trial was committed."); for (int i = 0; i < hSize; ++i) reducedH(i) = candidatePhysical(hOffset + i); adapter.scatter(reducedH, trialH); std::array evaluated{nan, nan}; for (int i = 0; i < 2; ++i) if (limits[i].element >= 0) evaluated[i] = trialH.GetValue(limits[i].element, limits[i].ip); const auto result = s.PrepareTrial(); trials << name << ',' << alpha << ',' << (std::isfinite(boundary) && boundary > 0.0 ? alpha / boundary : nan) << ',' << result.has_value() << ','; if (result) { trials << "-1,none,-1,-1,0," << result->minimumJacobianDeterminant << ',' << s.trialNormalizedResidual.Norml2() << ',' << s.normalizedOperator->GetPhysicalResidual().Norml2(); for (const auto &row : s.Problem().GetManifest().residualBlocks()) { long double squared = 0.0; for (int i = 0; i < row.size; ++i) { const long double value = s.trialNormalizedResidual(row.offset + i); squared += value * value; } blocks << name << ',' << alpha << ',' << row.stableId << ',' << std::sqrt(squared) << '\n'; } } else { const auto &error = result.error(); const bool thermodynamic = error.reason == operators::StellarEquilibriumPreparationRejectionReason::thermodynamic_domain || error.reason == operators::StellarEquilibriumPreparationRejectionReason::non_finite_thermodynamics; trials << static_cast(error.reason) << ',' << detail::ReasonName(error.reason) << ',' << static_cast(error.stage) << ',' << static_cast(error.thermodynamicErrorCode) << ',' << thermodynamic << ',' << error.minimumJacobianDeterminant << ",nan,nan"; } for (const auto &limit : limits) trials << ',' << (limit.h + (changesH ? alpha * limit.dh : 0.0)); for (double value : evaluated) trials << ',' << value; trials << '\n'; trials.flush(); blocks.flush(); } }; probe("full", direction, fullAlphas, true); probe("enthalpy_only", enthalpyDirection, splitAlphas, true); probe("shape_only", shapeDirection, splitAlphas, false); finish(); } catch (...) { const auto error = std::current_exception(); try { finish(); } catch (...) { s.normalizedCorrection = savedCorrection; std::throw_with_nested(std::runtime_error("Pressure-obstruction diagnostics could not restore the frozen base.")); } std::rethrow_exception(error); } } // Supplemental, preparation-free inspection of the ACTUAL algebraic surface // carrier rows. Boundary-only interpolation is a contribution to the interior // polynomial, not its trace evaluated on the physical stellar surface. template void ProbeSurfaceTrace(State &s, const mean_field::fem::FEM &f, double n, const mfem::Vector &normalizedBase, const mfem::Vector &normalizedDirection, const std::filesystem::path &output) { using namespace mean_field; namespace detail = pressure_obstruction_detail; using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; using EnthalpyField = field::Field; if (!f.mesh || !f.enthalpyFes || !f.quadratureFactory) throw std::invalid_argument("Surface trace inspection requires an enthalpy FEM."); if (normalizedBase.Size() != s.Problem().StateSize() || normalizedDirection.Size() != normalizedBase.Size() || !detail::Finite(normalizedBase) || !detail::Finite(normalizedDirection)) throw std::invalid_argument("Surface trace inspection requires finite compatible vectors."); if (!std::filesystem::is_directory(output)) throw std::invalid_argument("Surface trace inspection requires an existing output directory."); const mfem::Vector savedCorrection(s.normalizedCorrection), direction(normalizedDirection); mfem::Vector physicalBase, physicalDirection; s.normalizedOperator->DenormalizeState(normalizedBase, physicalBase); s.normalizedOperator->DenormalizeState(direction, physicalDirection); // No PrepareTrial, Refresh, BuildResidual, or accepted-state mutation here. try { s.normalizedCorrection = direction; const auto measures = MeasureEnthalpyStep(s, f, n, &physicalBase); s.normalizedCorrection = savedCorrection; int offset = -1, size = -1; for (const auto &block : s.Problem().GetManifest().valueBlocks()) if (block.stableId == "specific_enthalpy") { offset = block.offset; size = block.size; } const auto &surface = s.Problem().GetPressureSurfaceRows(); if (offset < 0 || size <= 0 || surface.field_size() != size || surface.size() <= 0) throw std::runtime_error("Surface trace inspection found an incompatible or empty carrier map."); mfem::Vector boundaryH(size), boundaryDh(size); boundaryH = 0.0; boundaryDh = 0.0; auto rows = detail::Open(output / "surface_trace_rows.csv"); rows << "surface_index,reduced_h_index,root_index,base_h,direction_dh,h_plus_dh\n"; std::array minimum, maximum, maximumAbs{}; minimum.fill(std::numeric_limits::infinity()); maximum.fill(-std::numeric_limits::infinity()); std::array squared{}; std::array negative{}, nonzero{}; double alphaSurface = std::numeric_limits::infinity(); bool baseNonnegative = true; for (int i = 0; i < surface.size(); ++i) { const int reduced = surface.reduced_dofs()[i]; if (reduced < 0 || reduced >= size) throw std::runtime_error("Surface carrier row is outside the enthalpy block."); const double h = physicalBase(offset + reduced), dh = physicalDirection(offset + reduced); if (!std::isfinite(h) || !std::isfinite(dh) || !std::isfinite(h + dh)) throw std::runtime_error("Surface trace coefficients are nonfinite."); boundaryH(reduced) = h; boundaryDh(reduced) = dh; rows << i << ',' << reduced << ',' << offset + reduced << ',' << h << ',' << dh << ',' << h + dh << '\n'; const std::array values{h, dh, h + dh}; for (int j = 0; j < 3; ++j) { minimum[j] = std::min(minimum[j], values[j]); maximum[j] = std::max(maximum[j], values[j]); maximumAbs[j] = std::max(maximumAbs[j], std::abs(values[j])); squared[j] += static_cast(values[j]) * values[j]; negative[j] += values[j] < 0.0; nonzero[j] += values[j] != 0.0; } baseNonnegative = baseNonnegative && h >= 0.0; if (dh < 0.0) alphaSurface = std::min(alphaSurface, -h / dh); } auto summary = detail::Open(output / "surface_trace_summary.csv"); summary << "metric,value\nphysical_state_size," << physicalBase.Size() << "\nh_root_offset," << offset << "\nh_block_size," << size << "\nsurface_row_count," << surface.size() << '\n'; const std::array names{"base_h", "direction_dh", "h_plus_dh"}; for (int j = 0; j < 3; ++j) { summary << names[j] << "_minimum," << minimum[j] << '\n' << names[j] << "_maximum," << maximum[j] << '\n' << names[j] << "_maximum_absolute," << maximumAbs[j] << '\n' << names[j] << "_norm," << std::sqrt(squared[j]) << '\n' << names[j] << "_negative_count," << negative[j] << '\n' << names[j] << "_nonzero_count," << nonzero[j] << '\n'; } summary << "surface_coefficient_base_nonnegative," << baseNonnegative << '\n' << "surface_coefficient_alpha_h," << (baseNonnegative ? alphaSurface : std::numeric_limits::quiet_NaN()) << '\n' << "closure_alpha_h," << measures.at("closure_alpha_h") << '\n' << "pressure_alpha_h," << measures.at("pressure_alpha_h") << '\n'; const auto adapter = field::make_field_dof_grid_function_adapter(*f.enthalpyFes); mfem::ParGridFunction hBoundary(f.enthalpyFes.get()), dhBoundary(f.enthalpyFes.get()); adapter.scatter(boundaryH, hBoundary); adapter.scatter(boundaryDh, dhBoundary); mfem::Vector fullH(size), fullDh(size); for (int i = 0; i < size; ++i) { fullH(i) = physicalBase(offset + i); fullDh(i) = physicalDirection(offset + i); } mfem::ParGridFunction hFull(f.enthalpyFes.get()), dhFull(f.enthalpyFes.get()); adapter.scatter(fullH, hFull); adapter.scatter(fullDh, dhFull); auto nodes = detail::Open(output / "surface_trace_nodes.csv"); nodes << "family,element,attribute,geometry_type,fe_order,local_node,node_count,ip_x,ip_y,ip_z," "h,dh,alpha_union,h_plus_alpha_union_dh,h_plus_dh\n"; auto limits = detail::Open(output / "surface_trace_limits.csv"); limits << "family,element,point,attribute,geometry_type,transformation_order_w,eos_extra_order,rule_points," "ip_x,ip_y,ip_z,alpha_h,full_h,full_dh,boundary_h,boundary_dh,interior_h,interior_dh," "full_h_plus_dh,boundary_h_plus_dh,interior_h_plus_dh\n"; for (bool pressure : {false, true}) { const std::string prefix = pressure ? "pressure_" : ""; const int element = static_cast(measures.at(prefix + "limiting_element")); const int point = static_cast(measures.at(prefix + "limiting_point")); if (element < 0) continue; // No decreasing sample: the summary retains the infinite bound. auto *transformation = f.mesh->GetElementTransformation(element); const auto *finiteElement = f.enthalpyFes->GetFE(element); const auto &elementNodes = finiteElement->GetNodes(); const double alphaUnion = measures.at("alpha_thermodynamic"); // Evaluate the scattered physical fields at FE nodes; do not infer // nodal values by indexing potentially signed/oriented local DOFs. // A shared limiting element is exported once per family for identity. for (int node = 0; node < elementNodes.GetNPoints(); ++node) { const auto &ip = elementNodes.IntPoint(node); const double h = hFull.GetValue(element, ip), dh = dhFull.GetValue(element, ip); nodes << (pressure ? "pressure" : "closure") << ',' << element << ',' << f.mesh->GetAttribute(element) << ',' << static_cast(transformation->GetGeometryType()) << ',' << finiteElement->GetOrder() << ',' << node << ',' << elementNodes.GetNPoints() << ',' << ip.x << ',' << ip.y << ',' << ip.z << ',' << h << ',' << dh << ',' << alphaUnion << ',' << (std::isfinite(alphaUnion) ? h + alphaUnion * dh : std::numeric_limits::quiet_NaN()) << ',' << h + dh << '\n'; } const int extraOrder = static_cast(measures.at(prefix + "eos_extra_order")); const auto query = pressure ? EnthalpyField::template make_query( quadrature::QuadratureRole::discretization, transformation->OrderW(), std::array{extraOrder}, utils::DOMAINS::STELLAR, quadrature::MappingKind::general) : EnthalpyField::template make_query( quadrature::QuadratureRole::discretization, transformation->OrderW(), std::array{extraOrder}, utils::DOMAINS::STELLAR, quadrature::MappingKind::general); const auto rule = f.quadratureFactory->get(query, transformation->GetGeometryType()); if (!rule.integration_rule || point < 0 || point >= rule.integration_rule->GetNPoints()) throw std::runtime_error("Surface trace inspection could not resolve its production limiting point."); const auto &ip = rule.integration_rule->IntPoint(point); const double h = measures.at(prefix + "limiting_h"), dh = measures.at(prefix + "limiting_dh"); const double bh = hBoundary.GetValue(element, ip), bdh = dhBoundary.GetValue(element, ip); limits << (pressure ? "pressure" : "closure") << ',' << element << ',' << point << ',' << f.mesh->GetAttribute(element) << ',' << static_cast(transformation->GetGeometryType()) << ',' << transformation->OrderW() << ',' << extraOrder << ',' << rule.integration_rule->GetNPoints() << ',' << ip.x << ',' << ip.y << ',' << ip.z << ',' << measures.at(prefix + "alpha_h") << ',' << h << ',' << dh << ',' << bh << ',' << bdh << ',' << h - bh << ',' << dh - bdh << ',' << h + dh << ',' << bh + bdh << ',' << (h - bh) + (dh - bdh) << '\n'; } } catch (...) { s.normalizedCorrection = savedCorrection; throw; } } } // namespace experiment::plateau_diagnostics