feat(python): added python bindings

This commit is contained in:
2026-07-01 11:14:12 -04:00
parent 37416adb03
commit 39e5117a24
40 changed files with 2434 additions and 99 deletions

View File

@@ -2,12 +2,428 @@
#include "stroid/config/config.h"
#include "stroid/IO/mesh.h"
#include <charconv>
#include "stroid/version.h"
#include <fstream>
#include <iostream>
#include <cstdint>
#include <format>
#include <chrono>
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 <NAME>\n ... \nEND BLOCK <NAME>
# 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 <typename T>
std::string format_opt(const std::optional<T> 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<int>
# default: 4
refinement_levels:{}
# order: Polynomial / geometric order to use when constructing the mesh
# std::optional<int>
# 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<bool>
# default: true
include_external_domain:{}
# r_core: the radius of the stellar core region (in reference space)
# std::optional<double>
# default: 0.25
r_core:{}
# r_star: the radius of the stellar surface (in reference space)
# std::optional<double>
# 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<float>
# default: 0.0
flattening:{}
# r_infinity: the radius of the outer boundary of the mesh (in reference space)
# std::optional<double>
# default: 6.0
r_infinity:{}
# r_instability: the radius inside which computations of geometry are skipped to avoid a core singularity
# std::optional<double>
# default: 1e-14
r_instability:{}
# core_steepness: Controls the rate of transition of the core-to-envelope transition
# std::optional<double>
# 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<double>
# default: 2
continuity_order:{}
# surface_bdr_id: the boundary id to tag the stellar surface boundary elements as
# std::optional<size_t>
# default: 1
surface_bdr_id:{}
# inf_bdr_id: the boundary id to tag the outer boundary elements as
# std::optional<size_t>
# default: 2
inf_bdr_id:{}
# core_id: the material attribute to tag elements in the core region as
# std::optional<size_t>
# default 1
core_id:{}
# envelope_id: the material attribute to tag elements in the envelope as
# std::optional<size_t>
# default 2
envelope_id:{}
# vacuum_id: the material attribute to tag elements in the vacuum region as
# std::optional<size_t>
# 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<bool, std::string> parse_bool(std::string_view v) {
std::string s(trim(v));
std::ranges::transform(s, s.begin(),
[](const unsigned char c) { return static_cast<char>(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::integral T>
std::expected<T, std::string> parse_int(std::string_view v) {
const std::string_view s = trim(v);
T out{};
if (const auto res = std::from_chars(s.data(), s.data() + s.size(), out); res.ec != std::errc{} || res.ptr != s.data() + s.size())
return std::unexpected(std::format("invalid integer value '{}'", v));
return out;
}
std::expected<double, std::string> parse_double(std::string_view v) {
const std::string_view s = trim(v);
double out{};
if (const auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), out); ec != std::errc{} || ptr != s.data() + s.size())
return std::unexpected(std::format("invalid floating-point value '{}'", v));
return out;
}
std::expected<std::map<std::string, std::string>, std::string> extract_blocks(std::istream& is) {
std::map<std::string, std::string> 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<void, std::string> parse_header(const std::string& content, StroidMesh& out) {
std::istringstream iss(content);
std::string line;
std::optional<MFEM_MESH_TYPE> type;
std::optional<size_t> 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<size_t>(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<config::MeshConfig, std::string> parse_config(const std::string& content) {
config::MeshConfig cfg;
config::OptimizationMethods opt =
cfg.optimization_methods.value_or(config::OptimizationMethods{});
using Handler = std::function<std::expected<void, std::string>(std::string_view)>;
auto as_int = [](std::optional<int>* f) { return [f](const std::string_view v) -> std::expected<void, std::string> { auto r = parse_int<int>(v); if (!r) return std::unexpected(r.error()); *f = *r; return {}; }; };
auto as_size = [](std::optional<size_t>* f) { return [f](const std::string_view v) -> std::expected<void, std::string> { auto r = parse_int<size_t>(v); if (!r) return std::unexpected(r.error()); *f = *r; return {}; }; };
auto as_double = [](std::optional<double>* f) { return [f](const std::string_view v) -> std::expected<void, std::string> { auto r = parse_double(v); if (!r) return std::unexpected(r.error()); *f = *r; return {}; }; };
auto as_bool = [](std::optional<bool>* f) { return [f](const std::string_view v) -> std::expected<void, std::string> { auto r = parse_bool(v); if (!r) return std::unexpected(r.error()); *f = *r; return {}; }; };
const std::unordered_map<std::string_view, Handler> 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::unique_ptr<mfem::Mesh>, 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<mfem::Mesh>(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<ParsedMeta, std::string> 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<std::string, std::string> {
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);
@@ -15,6 +431,10 @@ namespace stroid::IO {
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);
@@ -22,6 +442,10 @@ namespace stroid::IO {
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()) {
@@ -61,7 +485,12 @@ namespace stroid::IO {
sol_sock << "keys iMj\n";
sol_sock << std::flush;
}
void VisualizeFaceValence(mfem::Mesh& mesh) {
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);
@@ -78,13 +507,81 @@ namespace stroid::IO {
}
// View in GLVis
char vishost[] = "localhost";
int visport = 19916;
mfem::socketstream sol_sock(vishost, visport);
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<StroidMesh, std::string> 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<StroidMesh, std::string> 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<StroidMesh, std::string> 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::unique_ptr<mfem::Mesh>, 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<mfem::Mesh>(new mfem::ParMesh(comm, iss));
return std::make_unique<mfem::Mesh>(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<StroidMesh, std::string> 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
}