build(python): gridfire uses fourdst wheel in python mode

This commit is contained in:
2026-06-12 14:30:59 -04:00
parent 3b9a6eba5a
commit 61cd7359d4
16 changed files with 469 additions and 321 deletions

View File

@@ -130,7 +130,7 @@ struct MultiscalePartitioningEngineViewScratchPad final : AbstractScratchPad {
* @brief Check whether the scratchpad has been initialized.
* @return true if initialized with a valid SUNContext, false otherwise.
*/
[[nodiscard]] bool is_initialized() const override { return has_initialized; }
[[nodiscard]] bool is_initialized() const override;
/**
* @brief Initialize the scratchpad by creating a SUNDIALS context.
@@ -150,16 +150,7 @@ struct MultiscalePartitioningEngineViewScratchPad final : AbstractScratchPad {
* SUNContext ctx = scratch.sun_ctx;
* @endcode
*/
void initialize() {
if (has_initialized) return;
const int flag = SUNContext_Create(SUN_COMM_NULL, &sun_ctx);
if (flag != 0) {
throw std::runtime_error("Failed to create SUNContext in MultiscalePartitioningEngineViewScratchPad.");
}
has_initialized = true;
}
void initialize();
/**
* @brief Destructor that properly releases SUNDIALS resources.
@@ -167,13 +158,7 @@ struct MultiscalePartitioningEngineViewScratchPad final : AbstractScratchPad {
* Clears all QSE solvers before freeing the SUNContext to ensure
* proper cleanup order and avoid dangling references.
*/
~MultiscalePartitioningEngineViewScratchPad() override {
qse_solvers.clear();
if (sun_ctx != nullptr) {
SUNContext_Free(&sun_ctx);
sun_ctx = nullptr;
}
}
~MultiscalePartitioningEngineViewScratchPad() override;
/**
* @brief Create a partial copy of this scratchpad.
@@ -196,20 +181,7 @@ struct MultiscalePartitioningEngineViewScratchPad final : AbstractScratchPad {
*
* @endcode
*/
[[nodiscard]] std::unique_ptr<AbstractScratchPad> clone() const override {
auto clone_pad = std::make_unique<MultiscalePartitioningEngineViewScratchPad>();
clone_pad->qse_groups = this->qse_groups;
clone_pad->dynamic_species = this->dynamic_species;
clone_pad->algebraic_species = this->algebraic_species;
clone_pad->composition_cache = this->composition_cache;
clone_pad->initialize();
clone_pad->qse_solvers.reserve(this->qse_solvers.size());
for (const auto& solver : qse_solvers) {
clone_pad->qse_solvers.push_back(solver->clone(clone_pad->sun_ctx)); // Must rebind context to new SUNContext
}
return clone_pad;
}
[[nodiscard]] std::unique_ptr<AbstractScratchPad> clone() const override;
};
} // namespace gridfire::engine::scratch

View File

@@ -0,0 +1,52 @@
#include "gridfire/engine/views/engine_multiscale.h"
#include "gridfire/engine/scratchpads/scratchpad_abstract.h"
#include "gridfire/engine/scratchpads/types.h"
#include "gridfire/engine/scratchpads/engine_multiscale_scratchpad.h"
#include "fourdst/atomic/atomicSpecies.h"
#include <vector>
#include <memory>
#include <unordered_map>
#include "sundials/sundials_context.h"
namespace gridfire::engine::scratch {
bool MultiscalePartitioningEngineViewScratchPad::is_initialized() const{ return has_initialized; }
void MultiscalePartitioningEngineViewScratchPad::initialize() {
if (has_initialized) return;
const int flag = SUNContext_Create(SUN_COMM_NULL, &sun_ctx);
if (flag != 0) {
throw std::runtime_error("Failed to create SUNContext in MultiscalePartitioningEngineViewScratchPad.");
}
has_initialized = true;
}
MultiscalePartitioningEngineViewScratchPad::~MultiscalePartitioningEngineViewScratchPad() {
qse_solvers.clear();
if (sun_ctx != nullptr) {
SUNContext_Free(&sun_ctx);
sun_ctx = nullptr;
}
}
std::unique_ptr<AbstractScratchPad> MultiscalePartitioningEngineViewScratchPad::clone() const {
auto clone_pad = std::make_unique<MultiscalePartitioningEngineViewScratchPad>();
clone_pad->qse_groups = this->qse_groups;
clone_pad->dynamic_species = this->dynamic_species;
clone_pad->algebraic_species = this->algebraic_species;
clone_pad->composition_cache = this->composition_cache;
clone_pad->initialize();
clone_pad->qse_solvers.reserve(this->qse_solvers.size());
for (const auto& solver : qse_solvers) {
clone_pad->qse_solvers.push_back(solver->clone(clone_pad->sun_ctx)); // Must rebind context to new SUNContext
}
return clone_pad;
}
}

View File

@@ -12,6 +12,7 @@ gridfire_sources = files(
'lib/engine/procedures/construction.cpp',
'lib/engine/diagnostics/dynamic_engine_diagnostics.cpp',
'lib/engine/types/jacobian.cpp',
'lib/engine/scratchpads/engine_multiscale_scratchpad.cpp',
'lib/reaction/reaction.cpp',
'lib/reaction/reaclib.cpp',
'lib/reaction/weak/weak.cpp',
@@ -67,39 +68,97 @@ gridfire_link_args = cpp.get_supported_link_arguments(
)
if get_option('build_python')
gridfire_link_whole += [libcomposition, libconst, liblogging]
if get_option('plugin_support')
gridfire_link_whole += [libplugin]
error('plugin_support is not available when build_python=true: '
+ 'the fourdst wheel does not ship libplugin, and bundling it '
+ 'would break cross-package type compatibility.')
endif
# ---- rpaths ---------------------------------------------------------
# Wheel layout (both packages share one site-packages):
# <site-packages>/gridfire/_gridfire.<tag>.so
# <site-packages>/gridfire/lib/libgridfire.{so,dylib}
# <site-packages>/fourdst/lib/lib{composition,logging,const}.*
# <site-packages>/fourdst/lib/vendor/libreflect_cpp.*
#
# libgridfire (in gridfire/lib/) needs: itself-dir, ../../fourdst/lib{,/vendor}
# _gridfire (in gridfire/) needs: ./lib, ../fourdst/lib{,/vendor}
#
# Platform split:
# * macOS / Mach-O: LC_RPATH cannot hold colon-separated lists,
# meson carries a user-supplied build_rpath as a single string, and
# install_rpath is only applied by `meson install` (which
# meson-python never runs). So on darwin we emit one raw
# `-Wl,-rpath,<path>` link arg per path, and deliberately do NOT
# set build_rpath/install_rpath (a real `meson install` would then
# add install_rpath on top of the link args, producing duplicate
# LC_RPATH entries, which macOS 26+ dyld treats as a hard load
# error).
# * Linux / ELF: colon-joined DT_RUNPATH is the native format, and
# install_rpath is baked into the binary at link time, so the
# kwargs work for both meson-python wheels and `meson install`.
#
# NOTE: compose paths with '+', never the '/' join operator —
# 'x' / '/y' silently discards 'x' in Meson.
#
# gridfire_ext_rpath_args / gridfire_ext_rpath are consumed later by
# build-python/meson.build for the _gridfire extension module (this
# subdir is processed before build-python in the root meson.build).
if host_machine.system() == 'darwin'
gridfire_lib_rpath_args = [
'-Wl,-rpath,@loader_path',
'-Wl,-rpath,@loader_path/../../fourdst/lib',
'-Wl,-rpath,@loader_path/../../fourdst/lib/vendor',
]
gridfire_lib_rpath = ''
gridfire_ext_rpath_args = [
'-Wl,-rpath,@loader_path/lib',
'-Wl,-rpath,@loader_path/../fourdst/lib',
'-Wl,-rpath,@loader_path/../fourdst/lib/vendor',
]
gridfire_ext_rpath = ''
else
gridfire_lib_rpath_args = []
gridfire_lib_rpath = '$ORIGIN:' + '$ORIGIN/../../fourdst/lib:' + '$ORIGIN/../../fourdst/lib/vendor'
gridfire_ext_rpath_args = []
gridfire_ext_rpath = '$ORIGIN/lib:' + '$ORIGIN/../fourdst/lib:' + '$ORIGIN/../fourdst/lib/vendor'
endif
libgridfire = shared_library('gridfire',
gridfire_sources,
include_directories: include_directories('include'),
dependencies: gridfire_build_dependencies,
cpp_args: gridfire_args,
link_whole: gridfire_link_whole,
link_args: gridfire_link_args,
install: true,
install_dir: gridfire_libdir)
gridfire_sources,
include_directories: include_directories('include'),
dependencies: gridfire_build_dependencies,
cpp_args: gridfire_args,
link_whole: gridfire_link_whole,
link_args: gridfire_link_args + gridfire_lib_rpath_args,
install: true,
install_dir: gridfire_libdir,
build_rpath: gridfire_lib_rpath,
install_rpath: gridfire_lib_rpath,
)
else
libgridfire = library('gridfire',
gridfire_sources,
include_directories: include_directories('include'),
dependencies: gridfire_build_dependencies,
link_whole: gridfire_link_whole,
link_args: gridfire_link_args,
cpp_args: gridfire_args,
install : true)
gridfire_sources,
include_directories: include_directories('include'),
dependencies: gridfire_build_dependencies,
link_whole: gridfire_link_whole,
link_args: gridfire_link_args,
cpp_args: gridfire_args,
install : true
)
endif
if get_option('build_python')
gridfire_iface_deps = []
foreach d : gridfire_build_dependencies
gridfire_iface_deps += d.partial_dependency(compile_args: true, includes: true)
endforeach
gridfire_iface_dep = declare_dependency(
dependencies: gridfire_build_dependencies,
).partial_dependency(compile_args: true, includes: true)
gridfire_dep = declare_dependency(
include_directories: include_directories('include'),
link_with: libgridfire,
dependencies: gridfire_iface_deps,
dependencies: [gridfire_iface_dep],
compile_args: gridfire_args,
)
else
@@ -114,8 +173,8 @@ endif
meson.override_dependency('gridfire', gridfire_dep)
install_subdir('include/gridfire',
install_dir: gridfire_includedir,
exclude_files: ['utils/config.h.in'],
install_dir: gridfire_includedir,
exclude_files: ['utils/config.h.in'],
)
@@ -126,4 +185,3 @@ if get_option('build_c_api')
message('Configuring C API...')
subdir('extern')
endif

View File

@@ -1,3 +1,13 @@
try:
import fourdst as fst
except ImportError as e:
raise ImportError(
"gridfire requires the fourdst package (its C++ types and shared "
"libraries come from there). pip install fourdst."
) from e
from ._gridfire import *
from ._gridfire import *
import sys
@@ -70,4 +80,49 @@ def gf_credits():
"Emily M. Boudreaux - Lead Developer",
"Aaron Dotter - Co-Developer",
"4D-STAR Collaboration - Contributors"
]
]
import os
from pathlib import Path
from typing import List
_PACKAGE_DIR = Path(__file__).resolve().parent
def gf_get_include_dirs():
return [
os.fspath(_PACKAGE_DIR / "include"),
os.fspath(_PACKAGE_DIR / "include" / "gridfire" / "vendor"),
]
def gf_get_lib_dirs():
return [
os.fspath(_PACKAGE_DIR / "lib"),
]
def gf_get_rpath_flags() -> List[str]:
return ["-Wl,-rpath," + os.fspath(_PACKAGE_DIR / "lib")]
def gf_get_lib_flags() -> List[str]:
flags = ["-L" + d for d in gf_get_lib_dirs()]
flags += ["-lgridfire"]
return flags
def gf_get_include_flags() -> List[str]:
return ["-I" + d for d in gf_get_include_dirs()]
def gf_get_extra_flags() -> List[str]:
return ['--std=c++23', '-fPIC']
def gf_compiler_flags(just_gridfire=False):
flags = []
if not just_gridfire:
flags.extend(fourdst_flags = fst.get_compiler_flags())
flags.extend(gf_get_rpath_flags())
flags.extend(gf_get_lib_flags())
flags.extend(gf_get_include_flags())
flags.extend(gf_get_extra_flags())
return flags
def gf_get_compiler_flags_formatted(just_gridfire=False) -> int:
flags = gf_compiler_flags(just_gridfire)
print(" ".join(flags))
return 0

View File

@@ -17,14 +17,6 @@ namespace py = pybind11;
void register_solver_bindings(const py::module &m) {
auto py_cvode_timestep_context = py::class_<gridfire::solver::PointSolverTimestepContext>(m, "PointSolverTimestepContext");
py_cvode_timestep_context.def_readonly("t", &gridfire::solver::PointSolverTimestepContext::t);
py_cvode_timestep_context.def_property_readonly(
"state",
[](const gridfire::solver::PointSolverTimestepContext& self) -> std::vector<double> {
const sunrealtype* nvec_data = N_VGetArrayPointer(self.state);
const sunindextype length = N_VGetLength(self.state);
return {nvec_data, nvec_data + length};
}
);
py_cvode_timestep_context.def_readonly("dt", &gridfire::solver::PointSolverTimestepContext::dt);
py_cvode_timestep_context.def_readonly("last_step_time", &gridfire::solver::PointSolverTimestepContext::last_step_time);
py_cvode_timestep_context.def_readonly("T9", &gridfire::solver::PointSolverTimestepContext::T9);
@@ -57,6 +49,20 @@ void register_solver_bindings(const py::module &m) {
return self.getPhysicalComposition();
}
);
py_cvode_timestep_context.def_property_readonly(
"rawState",
[](const gridfire::solver::PointSolverTimestepContext& self) -> std::vector<double> {
const std::span<const double> s = self.rawState();
return std::vector<double>(s.begin(), s.end());
}
);
py_cvode_timestep_context.def("abundance",
py::overload_cast<size_t>(&gridfire::solver::PointSolverTimestepContext::abundance, py::const_),
py::arg("species_index"));
py_cvode_timestep_context.def("abundance",
py::overload_cast<const fourdst::atomic::Species&>(&gridfire::solver::PointSolverTimestepContext::abundance, py::const_),
py::arg("species"));
py_cvode_timestep_context.def_property_readonly("accumulatedSpecificEnergy", &gridfire::solver::PointSolverTimestepContext::accumulatedSpecificEnergy);