#include "mfem.hpp" #include "stroid/config/config.h" #include "stroid/IO/mesh.h" #include #include "stroid/version.h" #include #include #include #include #include #include #include #include #include #include #include namespace stroid::IO { namespace { std::string format_header(const StroidMesh& mesh, const std::string& comment) { auto now = std::chrono::system_clock::now(); version v; std::stringstream vs; vs << v; std::string header = std::format(R"(# STROID MESH # NOTE: STROID MESH IS A THIN WRAPPER AROUND MFEM's NATIVE MESH FORMAT # STRUCTURE: # - Type : Serial or Parallel (S for Serial, P for Parallel) # - mesh : the primary computational domain which can be of n order and be h-refined # - reference mesh : a reference, linear order mesh, used to ensure that the primary mesh remains well formed # - config : The configuration options initially used to generate the mesh # - refinement-levels : the total number of refinement levels the primary mesh has been subjected too # NOTE: EACH BLOCK OF DATA IS STORED BETWEEN "BEGIN BLOCK \n ... \nEND BLOCK # PARSING THE UNDERLYING MFEM NATIVE MESH FORMAT CAN BE DONE WITH MFEM'S STREAM READER # IF YOU EXTRACT THE RAW CONTENTS BETWEEN THOSE LINES BEGIN BLOCK HEADER MESH_TYPE:{} REFINEMENT_LEVELS:{} DATE_CREATED:{:%Y-%m-%d} COMMENT:{} STROID_VERSION:{} END BLOCK HEADER)", mesh.type == MFEM_MESH_TYPE::PARALLEL ? "P" : "S", mesh.refinement_levels, now, comment, vs.str(), mesh.refinement_levels ); return header; } std::string format_primary_mesh(const StroidMesh& mesh) { std::stringstream ss; ss.precision(8); mesh.mesh->Print(ss); std::string pmesh = std::format("BEGIN BLOCK PMESH\n{}END BLOCK PMESH", ss.str()); return pmesh; } template std::string format_opt(const std::optional opt, T default_val) { if (opt.has_value()) { return std::format("{}", opt.value()); } return std::format("{}", default_val); } std::string format_reference_mesh(const StroidMesh& mesh) { std::stringstream ss; ss.precision(8); mesh.reference_mesh->Print(ss); std::string rmesh = std::format("BEGIN BLOCK RMESH\n{}END BLOCK RMESH", ss.str()); return rmesh; } std::string format_config(const StroidMesh& mesh) { config::MeshConfig d; config::OptimizationMethods d_opt = d.optimization_methods.value_or(config::OptimizationMethods{false, true}); config::OptimizationMethods m_opt = mesh.config.optimization_methods.value_or(d_opt); std::string config_str = std::format(R"(BEGIN BLOCK CONFIG # refiniment_levels: Initial number of levels of refinmenet, note the value in the header may be more up to date # std::optional # default: 4 refinement_levels:{} # order: Polynomial / geometric order to use when constructing the mesh # std::optional # default: 3 order:{} # include_external_domain: Whether or not to include the external domain in the mesh generally used for applying boundary conditions at infinity # std::optional # default: true include_external_domain:{} # r_core: the radius of the stellar core region (in reference space) # std::optional # default: 0.25 r_core:{} # r_star: the radius of the stellar surface (in reference space) # std::optional # default: 1.0 r_star:{} # flattening: the flattening of the star (in reference space) where 0 is spherical and >0 is oblate. Note that this parameter is not equivalent to solving for the structure of a rotating model # std::optional # default: 0.0 flattening:{} # r_infinity: the radius of the outer boundary of the mesh (in reference space) # std::optional # default: 6.0 r_infinity:{} # r_instability: the radius inside which computations of geometry are skipped to avoid a core singularity # std::optional # default: 1e-14 r_instability:{} # core_steepness: Controls the rate of transition of the core-to-envelope transition # std::optional # default: 1.0 core_steepness:{} # continuity_order: order of continuity to force from teh core-envelope transition (0 = discontinuous, 1=C1 continuity, etc...) # std::optional # default: 2 continuity_order:{} # surface_bdr_id: the boundary id to tag the stellar surface boundary elements as # std::optional # default: 1 surface_bdr_id:{} # inf_bdr_id: the boundary id to tag the outer boundary elements as # std::optional # default: 2 inf_bdr_id:{} # core_id: the material attribute to tag elements in the core region as # std::optional # default 1 core_id:{} # envelope_id: the material attribute to tag elements in the envelope as # std::optional # default 2 envelope_id:{} # vacuum_id: the material attribute to tag elements in the vacuum region as # std::optional # default 3 vacuum_id:{} # optimization_method: struct for storing which optimization methods are being used # includes tmop and smoothstep booleans optimization_methods-tmop:{} optimization_methods-smoothstep:{} END BLOCK CONFIG)", format_opt(mesh.config.refinement_levels, d.refinement_levels.value()), format_opt(mesh.config.order, d.order.value()), format_opt(mesh.config.include_external_domain, d.include_external_domain.value()), format_opt(mesh.config.r_core, d.r_core.value()), format_opt(mesh.config.r_star, d.r_star.value()), format_opt(mesh.config.flattening, d.flattening.value()), format_opt(mesh.config.r_infinity, d.r_infinity.value()), format_opt(mesh.config.r_instability, d.r_instability.value()), format_opt(mesh.config.core_steepness, d.core_steepness.value()), format_opt(mesh.config.continuity_order, d.continuity_order.value()), format_opt(mesh.config.surface_bdr_id, d.surface_bdr_id.value()), format_opt(mesh.config.inf_bdr_id, d.inf_bdr_id.value()), format_opt(mesh.config.core_id, d.core_id.value()), format_opt(mesh.config.envelope_id, d.envelope_id.value()), format_opt(mesh.config.vacuum_id, d.vacuum_id.value()), m_opt.tmop.value_or(false), m_opt.smoothstep.value_or(true)); return config_str; } } namespace { constexpr std::string_view BEGIN_PREFIX = "BEGIN BLOCK "; constexpr std::string_view END_PREFIX = "END BLOCK "; std::string_view trim(std::string_view s) { const auto b = s.find_first_not_of(" \t\r\n"); if (b == std::string_view::npos) return {}; const auto e = s.find_last_not_of(" \t\r\n"); return s.substr(b, e - b + 1); } std::expected parse_bool(std::string_view v) { std::string s(trim(v)); std::ranges::transform(s, s.begin(), [](const unsigned char c) { return static_cast(std::tolower(c)); }); if (s == "true" || s == "1") return true; if (s == "false" || s == "0") return false; return std::unexpected(std::format("invalid bool value '{}'", v)); } template std::expected parse_int(std::string_view v) { const std::string_view s = trim(v); std::string temp(s); try { size_t pos = 0; if constexpr (std::is_signed_v) { long long val = std::stoll(temp, &pos); if (pos != temp.size() || val < std::numeric_limits::min() || val > std::numeric_limits::max()) { return std::unexpected(std::format("invalid integer value '{}'", v)); } return static_cast(val); } else { unsigned long long val = std::stoull(temp, &pos); if (pos != temp.size() || val > std::numeric_limits::max()) { return std::unexpected(std::format("invalid integer value '{}'", v)); } return static_cast(val); } } catch (const std::exception&) { return std::unexpected(std::format("invalid integer value '{}'", v)); } } std::expected parse_double(std::string_view v) { const std::string_view s = trim(v); std::string temp(s); try { size_t pos = 0; double out = std::stod(temp, &pos); if (pos != temp.size()) { return std::unexpected(std::format("invalid floating-point value '{}'", v)); } return out; } catch (const std::exception&) { return std::unexpected(std::format("invalid floating-point value '{}'", v)); } } std::expected, std::string> extract_blocks(std::istream& is) { std::map blocks; std::string line; std::string current; std::string buffer; bool in_block = false; while (std::getline(is, line)) { const std::string_view t = trim(line); if (!in_block) { if (t.starts_with(BEGIN_PREFIX)) { current = std::string(trim(t.substr(BEGIN_PREFIX.size()))); if (current.empty()) return std::unexpected("found 'BEGIN BLOCK' with no block name"); if (blocks.contains(current)) return std::unexpected(std::format("duplicate block '{}'", current)); buffer.clear(); in_block = true; } } else { if (t.starts_with(END_PREFIX)) { if (const std::string end_name(trim(t.substr(END_PREFIX.size()))); end_name != current) return std::unexpected(std::format( "mismatched block markers: opened '{}' but closed '{}'", current, end_name)); blocks.emplace(std::move(current), std::move(buffer)); current.clear(); buffer.clear(); in_block = false; } else { std::string_view raw = line; if (!raw.empty() && raw.back() == '\r') raw.remove_suffix(1); buffer.append(raw); buffer.push_back('\n'); } } } if (in_block) return std::unexpected(std::format("unterminated block '{}' (missing END BLOCK)", current)); return blocks; } std::expected parse_header(const std::string& content, StroidMesh& out) { std::istringstream iss(content); std::string line; std::optional type; std::optional ref_levels; while (std::getline(iss, line)) { const std::string_view t = trim(line); if (t.empty() || t.starts_with('#')) continue; const auto colon = t.find(':'); if (colon == std::string_view::npos) continue; const std::string_view key = trim(t.substr(0, colon)); const std::string_view val = trim(t.substr(colon + 1)); if (key == "MESH_TYPE") { if (val == "P") type = MFEM_MESH_TYPE::PARALLEL; else if (val == "S") type = MFEM_MESH_TYPE::SERIAL; else return std::unexpected(std::format("unknown MESH_TYPE '{}'", val)); } else if (key == "REFINEMENT_LEVELS") { auto r = parse_int(val); if (!r) return std::unexpected("REFINEMENT_LEVELS: " + r.error()); ref_levels = *r; } } if (!type) return std::unexpected("HEADER block missing MESH_TYPE"); out.type = *type; out.refinement_levels = ref_levels.value_or(0); return {}; } std::expected parse_config(const std::string& content) { config::MeshConfig cfg; config::OptimizationMethods opt = cfg.optimization_methods.value_or(config::OptimizationMethods{}); using Handler = std::function(std::string_view)>; auto as_int = [](std::optional* f) { return [f](const std::string_view v) -> std::expected { auto r = parse_int(v); if (!r) return std::unexpected(r.error()); *f = *r; return {}; }; }; auto as_size = [](std::optional* f) { return [f](const std::string_view v) -> std::expected { auto r = parse_int(v); if (!r) return std::unexpected(r.error()); *f = *r; return {}; }; }; auto as_double = [](std::optional* f) { return [f](const std::string_view v) -> std::expected { auto r = parse_double(v); if (!r) return std::unexpected(r.error()); *f = *r; return {}; }; }; auto as_bool = [](std::optional* f) { return [f](const std::string_view v) -> std::expected { auto r = parse_bool(v); if (!r) return std::unexpected(r.error()); *f = *r; return {}; }; }; const std::unordered_map handlers = { {"refinement_levels", as_int(&cfg.refinement_levels)}, {"order", as_int(&cfg.order)}, {"include_external_domain", as_bool(&cfg.include_external_domain)}, {"r_core", as_double(&cfg.r_core)}, {"r_star", as_double(&cfg.r_star)}, {"flattening", as_double(&cfg.flattening)}, {"r_infinity", as_double(&cfg.r_infinity)}, {"r_instability", as_double(&cfg.r_instability)}, {"core_steepness", as_double(&cfg.core_steepness)}, {"continuity_order", as_size(&cfg.continuity_order)}, {"surface_bdr_id", as_size(&cfg.surface_bdr_id)}, {"inf_bdr_id", as_size(&cfg.inf_bdr_id)}, {"core_id", as_size(&cfg.core_id)}, {"envelope_id", as_size(&cfg.envelope_id)}, {"vacuum_id", as_size(&cfg.vacuum_id)}, {"optimization_methods-tmop", as_bool(&opt.tmop)}, {"optimization_methods-smoothstep", as_bool(&opt.smoothstep)}, }; std::istringstream iss(content); std::string line; while (std::getline(iss, line)) { const std::string_view t = trim(line); if (t.empty() || t.starts_with('#')) continue; const auto colon = t.find(':'); if (colon == std::string_view::npos) continue; const std::string_view key = trim(t.substr(0, colon)); const std::string_view val = trim(t.substr(colon + 1)); const auto it = handlers.find(key); if (it == handlers.end()) continue; if (auto r = it->second(val); !r) return std::unexpected(std::format("{}: {}", key, r.error())); } cfg.optimization_methods = opt; return cfg; } std::expected, std::string> load_serial_mesh(const std::string& raw) { if (trim(raw).empty()) return std::unexpected("empty mesh block"); std::istringstream iss(raw); try { return std::make_unique(iss); } catch (const std::exception& e) { return std::unexpected(std::string("MFEM failed to parse mesh: ") + e.what()); } } struct ParsedMeta { StroidMesh mesh; std::string pmesh_raw; std::string rmesh_raw; }; std::expected parse_metadata(std::istream& is) { auto blocks = extract_blocks(is); if (!blocks) return std::unexpected(blocks.error()); auto need = [&](std::string_view name) -> std::expected { const auto it = blocks->find(std::string(name)); if (it == blocks->end()) return std::unexpected(std::format("missing required block '{}'", name)); return it->second; }; ParsedMeta pm{}; const auto header = need("HEADER"); if (!header) return std::unexpected(header.error()); if (auto r = parse_header(*header, pm.mesh); !r) return std::unexpected(r.error()); const auto config = need("CONFIG"); if (!config) return std::unexpected(config.error()); auto cfg = parse_config(*config); if (!cfg) return std::unexpected("CONFIG block -> " + cfg.error()); pm.mesh.config = std::move(*cfg); const auto pmesh = need("PMESH"); if (!pmesh) return std::unexpected(pmesh.error()); pm.pmesh_raw = *pmesh; const auto rmesh = need("RMESH"); if (!rmesh) return std::unexpected(rmesh.error()); pm.rmesh_raw = *rmesh; return pm; } } void SaveStroidMesh(const StroidMesh &mesh, const std::string &filename, const std::string &comment) { std::ofstream ofs(filename); // First Write a header with some information std::string header = format_header(mesh, comment); std::string pmesh = format_primary_mesh(mesh); std::string rmesh = format_reference_mesh(mesh); std::string config = format_config(mesh); ofs << header << "\n"; ofs << pmesh << "\n"; ofs << rmesh << "\n"; ofs << config << "\n"; } void SaveMesh(const mfem::Mesh& mesh, const std::string& filename) { std::ofstream ofs(filename); ofs.precision(8); mesh.Print(ofs); } void SaveMesh(const stroid::StroidMesh &mesh, const std::string &filename) { SaveMesh(*mesh.mesh, filename); } void SaveVTU(mfem::Mesh &mesh, const std::string &exportName) { mfem::ParaViewDataCollection pd(exportName, &mesh); pd.SetDataFormat(mfem::VTKFormat::BINARY); pd.SetHighOrderOutput(true); pd.Save(); } void SaveVTU(const stroid::StroidMesh &mesh, const std::string &exportName) { SaveVTU(*mesh.mesh, exportName); } void ViewMesh(mfem::Mesh &mesh, const std::string& title, const VISUALIZATION_MODE mode, const std::string &vishost, int visport) { mfem::socketstream sol_sock(vishost.c_str(), visport); if (!sol_sock.is_open()) { std::cerr << "Unable to connect to GLVis server at " << vishost << ':' << visport << std::endl; return; } mfem::L2_FECollection fec(0, mesh.Dimension()); mfem::FiniteElementSpace fes(&mesh, &fec); mfem::GridFunction attr_gf(&fes); attr_gf = 0.0; switch (mode) { case VISUALIZATION_MODE::ELEMENT_ID: for (int i = 0; i < mesh.GetNE(); i++) { attr_gf(i) = static_cast(mesh.GetAttribute(i)); } break; case VISUALIZATION_MODE::BOUNDARY_ELEMENT_ID: attr_gf = 0.0; for (int i = 0; i < mesh.GetNBE(); i++) { int elem_index, side_index; mesh.GetBdrElementAdjacentElement(i, elem_index, side_index); attr_gf(elem_index) = static_cast(mesh.GetBdrAttribute(i)); } break; case VISUALIZATION_MODE::NONE: default: break; } sol_sock.precision(8); sol_sock << "solution\n" << mesh << attr_gf; sol_sock << "window_title '" << title << "'\n"; sol_sock << "keys iMj\n"; sol_sock << std::flush; } void ViewMesh(const stroid::StroidMesh &mesh, const std::string &title, VISUALIZATION_MODE mode, const std::string &vishost, int visport) { ViewMesh(*mesh.mesh, title, mode, vishost, visport); } void VisualizeFaceValence(mfem::Mesh& mesh, const std::string &vishost, int visport) { mfem::L2_FECollection fec(0, 3); mfem::FiniteElementSpace fes(&mesh, &fec); mfem::GridFunction valence_gf(&fes); for (int i = 0; i < mesh.GetNBE(); i++) { int f, o; mesh.GetBdrElementFace(i, &f, &o); int e1, e2; mesh.GetFaceElements(f, &e1, &e2); int valence = (e2 >= 0) ? 2 : 1; valence_gf(i) = static_cast(valence); } // View in GLVis mfem::socketstream sol_sock(vishost.c_str(), visport); if (sol_sock.is_open()) { sol_sock << "solution\n" << mesh << valence_gf; sol_sock << "window_title 'Boundary Valence: 1=Surface, 2=Internal'\n"; sol_sock << "keys am\n" << std::flush; } } void VisualizeFaceValence(const stroid::StroidMesh &mesh, const std::string &vishost, int visport) { VisualizeFaceValence(*mesh.mesh, vishost, visport); } std::expected ParseStroidMesh(std::istream& is) { auto pm = parse_metadata(is); if (!pm) return std::unexpected(pm.error()); if (pm->mesh.type != MFEM_MESH_TYPE::SERIAL) { return std::unexpected( "parsed a PARALLEL StroidMesh, but ParseStroidMesh(std::istream&) can only " "reconstruct serial meshes; use the MPI-aware overload " "ParseStroidMesh(std::istream&, MPI_Comm) (requires MFEM_USE_MPI)"); } auto m = load_serial_mesh(pm->pmesh_raw); if (!m) return std::unexpected("PMESH -> " + m.error()); auto rm = load_serial_mesh(pm->rmesh_raw); if (!rm) return std::unexpected("RMESH -> " + rm.error()); pm->mesh.mesh = std::move(*m); pm->mesh.reference_mesh = std::move(*rm); return std::move(pm->mesh); } std::expected LoadStroidMesh(const std::string& filename) { std::ifstream ifs(filename); if (!ifs.is_open()) return std::unexpected(std::format("could not open file '{}'", filename)); return ParseStroidMesh(ifs); } #ifdef MFEM_USE_MPI std::expected ParseStroidMesh(std::istream& is, MPI_Comm comm) { auto pm = parse_metadata(is); if (!pm) return std::unexpected(pm.error()); auto build = [&](const std::string& raw) -> std::expected, std::string> { if (trim(raw).empty()) return std::unexpected("empty mesh block"); std::istringstream iss(raw); try { if (pm->mesh.type == MFEM_MESH_TYPE::PARALLEL) return std::unique_ptr(new mfem::ParMesh(comm, iss)); return std::make_unique(iss); } catch (const std::exception& e) { return std::unexpected(std::string("MFEM failed to parse mesh: ") + e.what()); } }; auto m = build(pm->pmesh_raw); if (!m) return std::unexpected("PMESH -> " + m.error()); auto rm = build(pm->rmesh_raw); if (!rm) return std::unexpected("RMESH -> " + rm.error()); pm->mesh.mesh = std::move(*m); pm->mesh.reference_mesh = std::move(*rm); return std::move(pm->mesh); } std::expected LoadStroidMesh(const std::string& filename, MPI_Comm comm) { std::ifstream ifs(filename); if (!ifs.is_open()) return std::unexpected(std::format("could not open file '{}'", filename)); return ParseStroidMesh(ifs, comm); } #endif // MFEM_USE_MPI }