diff --git a/.gitignore b/.gitignore index 36dfbbb..92cc732 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,8 @@ subprojects/yaml-cpp/ subprojects/quill/ subprojects/googletest-1.15.2/ subprojects/opat-core/ +subprojects/pybind11*/ +subprojects/packagecache/ .vscode/ diff --git a/assets/static/atomic/include/atomicSpecies.h b/assets/static/atomic/include/atomicSpecies.h index 64c6596..be265b3 100644 --- a/assets/static/atomic/include/atomicSpecies.h +++ b/assets/static/atomic/include/atomicSpecies.h @@ -1,5 +1,4 @@ -#ifndef SPECIES_MASS_DATA_H -#define SPECIES_MASS_DATA_H +#pragma once #include #include #include @@ -7205,4 +7204,3 @@ namespace chemSpecies { {"Og-295", Og_295}, }; }; // namespace chemSpecies -#endif // SPECIES_MASS_DATA_H diff --git a/build-config/meson.build b/build-config/meson.build index f7f6466..be2adc5 100644 --- a/build-config/meson.build +++ b/build-config/meson.build @@ -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') \ No newline at end of file diff --git a/build-config/mfem/meson.build b/build-config/mfem/meson.build index a642f0e..560f6ab 100644 --- a/build-config/mfem/meson.build +++ b/build-config/mfem/meson.build @@ -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') diff --git a/build-config/pybind/meson.build b/build-config/pybind/meson.build index e69de29..94888e1 100644 --- a/build-config/pybind/meson.build +++ b/build-config/pybind/meson.build @@ -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 \ No newline at end of file diff --git a/build-config/quill/meson.build b/build-config/quill/meson.build index 3c5a89d..a069255 100644 --- a/build-config/quill/meson.build +++ b/build-config/quill/meson.build @@ -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') \ No newline at end of file +quill_dep = quill_sp.dependency('quill') \ No newline at end of file diff --git a/build-config/yaml-cpp/meson.build b/build-config/yaml-cpp/meson.build index 27c9332..8a81a82 100644 --- a/build-config/yaml-cpp/meson.build +++ b/build-config/yaml-cpp/meson.build @@ -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') \ No newline at end of file +yaml_cpp_dep = yaml_cpp_sp.dependency('yaml-cpp') \ No newline at end of file diff --git a/meson.build b/meson.build index b654bc3..5fb3b51 100644 --- a/meson.build +++ b/meson.build @@ -28,3 +28,7 @@ subdir('src') if get_option('build_tests') subdir('tests') endif + +if get_option('build_python') + subdir('build-python') +endif \ No newline at end of file diff --git a/meson_options.txt b/meson_options.txt index 56baeee..cc87109 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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)' +) diff --git a/pyproject.toml b/pyproject.toml index 530c8f1..1ebe206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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/ \ No newline at end of file + +authors = [ + {name = "Emily M. Boudreaux", email = "emily@boudreauxmail.com"}, +] +maintainers = [ + {name = "Emily M. Boudreaux", email = "emily@boudreauxmail.com"} +] \ No newline at end of file diff --git a/src/python/composition/bindings.cpp b/src/python/composition/bindings.cpp new file mode 100644 index 0000000..8658fc6 --- /dev/null +++ b/src/python/composition/bindings.cpp @@ -0,0 +1,162 @@ +#include +#include // Needed for vectors, maps, sets, strings +#include // Needed for binding std::vector, std::map etc if needed directly + +#include + +#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_(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 ""; + }); + + py::class_(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(&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(&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 ""; + }); + + // --- Binding for the main Composition class --- + py::class_(comp_submodule, "Composition") + // Constructors + .def(py::init<>(), "Default constructor") + .def(py::init&>(), + py::arg("symbols"), + "Constructor taking a list of symbols to register (defaults to mass fraction mode)") + // .def(py::init&>(), py::arg("symbols")) // Binding std::set constructor is possible but often less convenient from Python + .def(py::init&, const std::vector&, 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(&composition::Composition::registerSymbol), + py::arg("symbol"), py::arg("massFracMode") = true, "Register a single symbol.") + .def("registerSymbol", py::overload_cast&, 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(&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&>(&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(&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&>(&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(&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(&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(&composition::Composition::getComposition, py::const_), + py::arg("symbol"), "Returns a tuple (CompositionEntry, GlobalComposition) for the symbol. Requires finalization.") + // Binding the version returning map 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_(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 + + +} \ No newline at end of file diff --git a/src/python/composition/meson.build b/src/python/composition/meson.build new file mode 100644 index 0000000..2449f5b --- /dev/null +++ b/src/python/composition/meson.build @@ -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, +) diff --git a/src/python/meson.build b/src/python/meson.build new file mode 100644 index 0000000..33d6298 --- /dev/null +++ b/src/python/meson.build @@ -0,0 +1 @@ +subdir('composition') \ No newline at end of file