feat(python-composition): added composition module interface

This commit is contained in:
2025-04-30 11:41:10 -04:00
parent 57d60d7bb7
commit 4e83b05112
13 changed files with 234 additions and 12 deletions

2
.gitignore vendored
View File

@@ -64,6 +64,8 @@ subprojects/yaml-cpp/
subprojects/quill/
subprojects/googletest-1.15.2/
subprojects/opat-core/
subprojects/pybind11*/
subprojects/packagecache/
.vscode/

View File

@@ -1,5 +1,4 @@
#ifndef SPECIES_MASS_DATA_H
#define SPECIES_MASS_DATA_H
#pragma once
#include <unordered_map>
#include <string_view>
#include <string>
@@ -7205,4 +7204,3 @@ namespace chemSpecies {
{"Og-295", Og_295},
};
}; // namespace chemSpecies
#endif // SPECIES_MASS_DATA_H

View File

@@ -5,3 +5,17 @@ subdir('yaml-cpp')
subdir('quill')
subdir('boost')
subdir('opatIO')
subdir('pybind')
# Set the config file error handling options
configErr = get_option('config_error_handling')
# build up any -D flags we need
commonCppArgs = []
if configErr == 'warn'
commonCppArgs += ['-DCONFIG_WARN']
elif configErr == 'harsh'
commonCppArgs += ['-DCONFIG_HARSH']
endif
add_project_arguments(commonCppArgs, language: 'cpp')

View File

@@ -4,10 +4,11 @@ mfem_cmake_options.add_cmake_defines({
'MFEM_ENABLE_TESTING': 'OFF',
'MFEM_ENABLE_MINIAPPS': 'OFF',
'MFEM_USE_BENCMARK': 'OFF',
'BUILD_SHARED_LIBS': 'ON',
'CMAKE_SKIP_INSTALL_RULES': 'ON'
})
mfem_sp = cmake.subproject(
'mfem',
options: mfem_cmake_options)
mfem_dep = mfem_sp.dependency('mfem')
add_project_arguments('-I' + meson.current_build_dir() + '/subprojects/mfem/__CMake_build/config', language: 'cpp')

View File

@@ -0,0 +1,5 @@
if get_option('build_python')
pybind11_proj = subproject('pybind11')
pybind11_dep = pybind11_proj.get_variable('pybind11_dep')
python3_dep = dependency('python3')
endif

View File

@@ -1,5 +1,10 @@
quill_cmake_options = cmake.subproject_options()
quill_cmake_options.add_cmake_defines({
'BUILD_SHARED_LIBS': 'ON',
'CMAKE_SKIP_INSTALL_RULES': 'ON'
})
quill_sp = cmake.subproject(
'quill'
'quill',
options: quill_cmake_options,
)
quill_dep = quill_sp.dependency('quill')
add_project_arguments('-I' + meson.current_build_dir() + '/subprojects/quill/__CMake_build', language: 'cpp')
quill_dep = quill_sp.dependency('quill')

View File

@@ -1,10 +1,11 @@
yaml_cpp_cmake_options = cmake.subproject_options()
yaml_cpp_cmake_options.add_cmake_defines({
'CMAKE_POLICY_VERSION_MINIMUM': '3.5'
'CMAKE_POLICY_VERSION_MINIMUM': '3.5',
'BUILD_SHARED_LIBS': 'ON',
'CMAKE_SKIP_INSTALL_RULES': 'ON'
})
yaml_cpp_sp = cmake.subproject(
'yaml-cpp',
options: yaml_cpp_cmake_options,
)
yaml_cpp_dep = yaml_cpp_sp.dependency('yaml-cpp')
add_project_arguments('-I' + meson.current_build_dir() + '/subprojects/yaml-cpp/__CMake_build', language: 'cpp')
yaml_cpp_dep = yaml_cpp_sp.dependency('yaml-cpp')

View File

@@ -28,3 +28,7 @@ subdir('src')
if get_option('build_tests')
subdir('tests')
endif
if get_option('build_python')
subdir('build-python')
endif

View File

@@ -1,2 +1,10 @@
option('build_tests', type: 'boolean', value: true, description: 'Build tests')
option('user_mode', type: 'boolean', value: false, description: 'Enable user mode (set mode = 0)')
option('build_python', type: 'boolean', value: true, description: 'Build Python bindings')
option(
'config_error_handling',
type: 'combo',
choices: [ 'none', 'warn', 'harsh' ],
value: 'none',
description: 'What to do if a config file fails to load: silent (none), warning (warn), or error (harsh)'
)

View File

@@ -12,5 +12,10 @@ version = "0.1.0" # Your project's version
description = "Python interface for the 4DSSE C++ project"
readme = "Readme.md"
license = { file = "LICENSE.txt" } # Reference your license file [cite: 2]
# Add authors, classifiers, URLs etc. as needed
# See: https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
authors = [
{name = "Emily M. Boudreaux", email = "emily@boudreauxmail.com"},
]
maintainers = [
{name = "Emily M. Boudreaux", email = "emily@boudreauxmail.com"}
]

View File

@@ -0,0 +1,162 @@
#include <pybind11/pybind11.h>
#include <pybind11/stl.h> // Needed for vectors, maps, sets, strings
#include <pybind11/stl_bind.h> // Needed for binding std::vector, std::map etc if needed directly
#include <string>
#include "composition.h"
#include "atomicSpecies.h"
namespace py = pybind11;
std::string sv_to_string(std::string_view sv) {
return std::string(sv);
}
std::string get_ostream_str(const composition::Composition& comp) {
std::ostringstream oss;
oss << comp;
return oss.str();
}
PYBIND11_MODULE(fourdsse_bindings, m) { // Module name must match meson.build
m.doc() = "Python bindings for the 4DSSE project"; // Optional module docstring
auto comp_submodule = m.def_submodule("composition", "Bindings for the Composition module");
auto chem_submodule = m.def_submodule("species", "Bindings for the Chemical Species module");
// --- Bindings for composition and species module ---
py::class_<composition::GlobalComposition>(comp_submodule, "GlobalComposition")
.def_readonly("specificNumberDensity", &composition::GlobalComposition::specificNumberDensity)
.def_readonly("meanParticleMass", &composition::GlobalComposition::meanParticleMass)
.def("__repr__", // Add a string representation for easy printing in Python
[](const composition::GlobalComposition &gc) {
return "<GlobalComposition(specNumDens=" + std::to_string(gc.specificNumberDensity) +
", meanMass=" + std::to_string(gc.meanParticleMass) + ")>";
});
py::class_<composition::CompositionEntry>(comp_submodule, "CompositionEntry")
.def("symbol", &composition::CompositionEntry::symbol)
.def("mass_fraction",
py::overload_cast<>(&composition::CompositionEntry::mass_fraction, py::const_),
"Gets the mass fraction of the species.")
.def("mass_fraction",
py::overload_cast<double>(&composition::CompositionEntry::mass_fraction, py::const_),
py::arg("meanMolarMass"), // Name the argument in Python
"Gets the mass fraction of the species given the mean molar mass.")
.def("number_fraction",
py::overload_cast<>(&composition::CompositionEntry::number_fraction, py::const_),
"Gets the number fraction of the species.")
.def("number_fraction",
py::overload_cast<double>(&composition::CompositionEntry::number_fraction, py::const_),
py::arg("totalMoles"),
"Gets the number fraction of the species given the total moles.")
.def("rel_abundance", &composition::CompositionEntry::rel_abundance)
.def("isotope", &composition::CompositionEntry::isotope) // Assuming Species is bound or convertible
.def("getMassFracMode", &composition::CompositionEntry::getMassFracMode)
.def("__repr__", // Optional: nice string representation
[](const composition::CompositionEntry &ce) {
// You might want to include more info here now
return "<CompositionEntry(symbol='" + ce.symbol() + "', " +
"mass_frac=" + std::to_string(ce.mass_fraction()) + ", " +
"num_frac=" + std::to_string(ce.number_fraction()) + ")>";
});
// --- Binding for the main Composition class ---
py::class_<composition::Composition>(comp_submodule, "Composition")
// Constructors
.def(py::init<>(), "Default constructor")
.def(py::init<const std::vector<std::string>&>(),
py::arg("symbols"),
"Constructor taking a list of symbols to register (defaults to mass fraction mode)")
// .def(py::init<const std::set<std::string>&>(), py::arg("symbols")) // Binding std::set constructor is possible but often less convenient from Python
.def(py::init<const std::vector<std::string>&, const std::vector<double>&, bool>(),
py::arg("symbols"), py::arg("fractions"), py::arg("massFracMode") = true,
"Constructor taking symbols, fractions, and mode (True=Mass, False=Number)")
// Methods
.def("finalize", &composition::Composition::finalize, py::arg("norm") = false,
"Finalize the composition, optionally normalizing fractions to sum to 1.")
.def("registerSymbol", py::overload_cast<const std::string&, bool>(&composition::Composition::registerSymbol),
py::arg("symbol"), py::arg("massFracMode") = true, "Register a single symbol.")
.def("registerSymbol", py::overload_cast<const std::vector<std::string>&, bool>(&composition::Composition::registerSymbol),
py::arg("symbols"), py::arg("massFracMode") = true, "Register multiple symbols.")
.def("getRegisteredSymbols", &composition::Composition::getRegisteredSymbols,
"Get the set of registered symbols.")
.def("setMassFraction", py::overload_cast<const std::string&, const double&>(&composition::Composition::setMassFraction),
py::arg("symbol"), py::arg("mass_fraction"), "Set mass fraction for a single symbol (requires massFracMode). Returns old value.")
.def("setMassFraction", py::overload_cast<const std::vector<std::string>&, const std::vector<double>&>(&composition::Composition::setMassFraction),
py::arg("symbols"), py::arg("mass_fractions"), "Set mass fractions for multiple symbols (requires massFracMode). Returns list of old values.")
.def("setNumberFraction", py::overload_cast<const std::string&, const double&>(&composition::Composition::setNumberFraction),
py::arg("symbol"), py::arg("number_fraction"), "Set number fraction for a single symbol (requires !massFracMode). Returns old value.")
.def("setNumberFraction", py::overload_cast<const std::vector<std::string>&, const std::vector<double>&>(&composition::Composition::setNumberFraction),
py::arg("symbols"), py::arg("number_fractions"), "Set number fractions for multiple symbols (requires !massFracMode). Returns list of old values.")
.def("mix", &composition::Composition::mix, py::arg("other"), py::arg("fraction"),
"Mix with another composition. Returns new Composition.")
.def("getMassFraction", py::overload_cast<const std::string&>(&composition::Composition::getMassFraction, py::const_),
py::arg("symbol"), "Get mass fraction for a symbol (calculates if needed). Requires finalization.")
.def("getMassFraction", py::overload_cast<>(&composition::Composition::getMassFraction, py::const_),
"Get dictionary of all mass fractions. Requires finalization.")
.def("getNumberFraction", py::overload_cast<const std::string&>(&composition::Composition::getNumberFraction, py::const_),
py::arg("symbol"), "Get number fraction for a symbol (calculates if needed). Requires finalization.")
.def("getNumberFraction", py::overload_cast<>(&composition::Composition::getNumberFraction, py::const_),
"Get dictionary of all number fractions. Requires finalization.")
// Note: pybind11 automatically converts std::pair to a Python tuple
.def("getComposition", py::overload_cast<const std::string&>(&composition::Composition::getComposition, py::const_),
py::arg("symbol"), "Returns a tuple (CompositionEntry, GlobalComposition) for the symbol. Requires finalization.")
// Binding the version returning map<string, Entry> requires a bit more care or helper function
// to convert the map to a Python dict if needed directly. Let's bind the pair version for now.
.def("getComposition", py::overload_cast<>(&composition::Composition::getComposition, py::const_),
"Returns a tuple (dict[str, CompositionEntry], GlobalComposition) for all symbols. Requires finalization.")
.def("subset", &composition::Composition::subset, py::arg("symbols"), py::arg("method") = "norm",
"Create a new Composition containing only the specified symbols.")
.def("hasSymbol", &composition::Composition::hasSymbol, py::arg("symbol"),
"Check if a symbol is registered.")
.def("setCompositionMode", &composition::Composition::setCompositionMode, py::arg("massFracMode"),
"Set the mode (True=Mass, False=Number). Requires finalization before switching.")
// Operator overload
.def(py::self + py::self, "Mix equally with another composition.") // Binds operator+
// Add __repr__ or __str__
.def("__repr__", [](const composition::Composition &comp) {
return get_ostream_str(comp); // Use helper for C++ operator<<
});
// --- Bindings for species module ---
py::class_<chemSpecies::Species>(chem_submodule, "Species")
.def("mass", &chemSpecies::Species::mass, "Get atomic mass (amu)")
.def("massUnc", &chemSpecies::Species::massUnc, "Get atomic mass uncertainty (amu)")
.def("bindingEnergy", &chemSpecies::Species::bindingEnergy, "Get binding energy (keV/nucleon?)") // Check units
.def("betaDecayEnergy", &chemSpecies::Species::betaDecayEnergy, "Get beta decay energy (keV?)") // Check units
.def("betaCode", [](const chemSpecies::Species& s){ return sv_to_string(s.betaCode()); }, "Get beta decay code") // Convert string_view
.def("name", [](const chemSpecies::Species& s){ return sv_to_string(s.name()); }, "Get species name (e.g., 'H-1')") // Convert string_view
.def("el", [](const chemSpecies::Species& s){ return sv_to_string(s.el()); }, "Get element symbol (e.g., 'H')") // Convert string_view
.def("nz", &chemSpecies::Species::nz, "Get NZ value")
.def("n", &chemSpecies::Species::n, "Get neutron number N")
.def("z", &chemSpecies::Species::z, "Get proton number Z")
.def("a", &chemSpecies::Species::a, "Get mass number A")
.def("__repr__",
[](const chemSpecies::Species &s) {
std::ostringstream oss;
oss << s;
return oss.str();
});
chem_submodule.attr("species") = py::cast(chemSpecies::species); // Expose the species map
}

View File

@@ -0,0 +1,16 @@
# Define the library
bindings_sources = files('bindings.cpp')
dependencies = [
composition_dep,
species_weight_dep,
python3_dep,
pybind11_dep,
]
libPYcomposition = shared_module('py_composition',
bindings_sources,
cpp_args: ['-fvisibility=default'],
install : true,
dependencies: dependencies,
)

1
src/python/meson.build Normal file
View File

@@ -0,0 +1 @@
subdir('composition')