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 new file mode 100644 index 0000000..94888e1 --- /dev/null +++ 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/build-python/meson.build b/build-python/meson.build new file mode 100644 index 0000000..bd87563 --- /dev/null +++ b/build-python/meson.build @@ -0,0 +1,17 @@ +# --- Python Extension Setup --- +py_installation = import('python').find_installation('python3') + +py_mod = py_installation.extension_module( + 'fourdsse_bindings', # Name of the generated .so/.pyd file (without extension) + sources: [ + meson.project_source_root() + '/src/python/composition/bindings.cpp', + ], + dependencies : [ + pybind11_dep, + composition_dep, + species_weight_dep + ], + cpp_args : ['-UNDEBUG'], # Example: Ensure assertions are enabled if needed + install : true, + subdir: 'fourdstar' # Optional: Install the module inside a 'fourdsse' Python package directory +) \ 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 new file mode 100644 index 0000000..1ebe206 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = [ + "meson-python>=0.15.0", # Use a recent version + "meson>=1.6.0", # Specify your Meson version requirement + "pybind11>=2.10" # pybind11 headers needed at build time +] +build-backend = "mesonpy" + +[project] +name = "fourdstar" # Choose your Python package name +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] + +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/composition/meson.build b/src/composition/meson.build index 8b297b4..fe928d8 100644 --- a/src/composition/meson.build +++ b/src/composition/meson.build @@ -9,13 +9,12 @@ composition_headers = files( dependencies = [ probe_dep, - config_dep, quill_dep, species_weight_dep ] # Define the libcomposition library so it can be linked against by other parts of the build system -libcomposition = static_library('composition', +libcomposition = library('composition', composition_sources, include_directories: include_directories('public'), cpp_args: ['-fvisibility=default'], diff --git a/src/composition/public/composition.h b/src/composition/public/composition.h index 30c61fc..94e644b 100644 --- a/src/composition/public/composition.h +++ b/src/composition/public/composition.h @@ -190,7 +190,6 @@ namespace composition{ */ class Composition { private: - Config& m_config = Config::getInstance(); Probe::LogManager& m_logManager = Probe::LogManager::getInstance(); quill::Logger* m_logger = m_logManager.getLogger("log"); diff --git a/src/config/meson.build b/src/config/meson.build index 289b086..f51e06c 100644 --- a/src/config/meson.build +++ b/src/config/meson.build @@ -8,7 +8,7 @@ config_headers = files( ) # Define the libconfig library so it can be linked against by other parts of the build system -libconfig = static_library('config', +libconfig = library('config', config_sources, include_directories: include_directories('public'), cpp_args: ['-fvisibility=default'], diff --git a/src/config/public/config.h b/src/config/public/config.h index be33163..33a901c 100644 --- a/src/config/public/config.h +++ b/src/config/public/config.h @@ -156,7 +156,12 @@ public: template T get(const std::string &key, T defaultValue) { if (!m_loaded) { - throw std::runtime_error("Error! Config file not loaded"); +// ONLY THROW ERROR IF HARSH OR WARN CONFIGURATION +#if defined(CONFIG_HARSH) + throw std::runtime_error("Error! Config file not loaded. To disable this error, recompile with CONFIG_HARSH=0"); +#elif defined(CONFIG_WARN) + std::cerr << "Warning! Config file not loaded. This instance of 4DSSE was compiled with CONFIG_WARN so the code will continue using only default values" << std::endl; +#endif } // --- Check if the key has already been checked for existence if (std::find(unknownKeys.begin(), unknownKeys.end(), key) != unknownKeys.end()) { diff --git a/src/eos/meson.build b/src/eos/meson.build index f9979b4..f7abb67 100644 --- a/src/eos/meson.build +++ b/src/eos/meson.build @@ -18,7 +18,7 @@ dependencies = [ macros_dep, ] # Define the libconst library so it can be linked against by other parts of the build system -libeos = static_library('eos', +libeos = library('eos', eos_sources, include_directories: include_directories('public'), cpp_args: ['-fvisibility=default'], diff --git a/src/meshIO/meson.build b/src/meshIO/meson.build index a684138..cc4faa7 100644 --- a/src/meshIO/meson.build +++ b/src/meshIO/meson.build @@ -10,7 +10,7 @@ dependencies = [ mfem_dep ] # Define the libmeshIO library so it can be linked against by other parts of the build system -libmeshIO = static_library('meshIO', +libmeshIO = library('meshIO', meshIO_sources, include_directories: include_directories('public'), cpp_args: ['-fvisibility=default'], diff --git a/src/meson.build b/src/meson.build index 2946320..280c34a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -23,3 +23,6 @@ subdir('resource') # Physics Libraries subdir('network') subdir('poly') + +# Python Bindings +subdir('python') diff --git a/src/network/meson.build b/src/network/meson.build index 2add636..41abf3f 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -19,7 +19,7 @@ dependencies = [ ] # Define the libnetwork library so it can be linked against by other parts of the build system -libnetwork = static_library('network', +libnetwork = library('network', network_sources, include_directories: include_directories('public'), dependencies: dependencies, diff --git a/src/poly/utils/meson.build b/src/poly/utils/meson.build index 9b73678..0b94b21 100644 --- a/src/poly/utils/meson.build +++ b/src/poly/utils/meson.build @@ -32,7 +32,7 @@ dependencies = [ types_dep, ] -libpolyutils = static_library('polyutils', +libpolyutils = library('polyutils', polyutils_sources, include_directories : include_directories('./public'), cpp_args: ['-fvisibility=default'], diff --git a/src/probe/meson.build b/src/probe/meson.build index cf6accf..186586b 100644 --- a/src/probe/meson.build +++ b/src/probe/meson.build @@ -35,7 +35,7 @@ dependencies = [ ] # Define the liblogger library so it can be linked against by other parts of the build system -libprobe = static_library('probe', +libprobe = library('probe', probe_sources, include_directories: include_directories('public'), cpp_args: ['-fvisibility=default'], 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/fourdstar/__init__.py b/src/python/fourdstar/__init__.py new file mode 100644 index 0000000..e69de29 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 diff --git a/src/resource/meson.build b/src/resource/meson.build index bc32862..7b4df2b 100644 --- a/src/resource/meson.build +++ b/src/resource/meson.build @@ -23,7 +23,7 @@ dependencies = [ libResourceHeader_dep = declare_dependency(include_directories: include_directories('public')) # Define the libresourceManager library so it can be linked against by other parts of the build system -libresourceManager = static_library('resourceManager', +libresourceManager = library('resourceManager', resourceManager_sources, include_directories: include_directories('public'), cpp_args: ['-fvisibility=default'], diff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap new file mode 100644 index 0000000..b4024bb --- /dev/null +++ b/subprojects/pybind11.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = pybind11-2.13.5 +source_url = https://github.com/pybind/pybind11/archive/refs/tags/v2.13.5.tar.gz +source_filename = pybind11-2.13.5.tar.gz +source_hash = b1e209c42b3a9ed74da3e0b25a4f4cd478d89d5efbb48f04b277df427faf6252 +patch_filename = pybind11_2.13.5-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/pybind11_2.13.5-1/get_patch +patch_hash = ecb031b830481560b3d8487ed63ba4f5509a074be42f5d19af64d844c795e15b +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/pybind11_2.13.5-1/pybind11-2.13.5.tar.gz +wrapdb_version = 2.13.5-1 + +[provide] +pybind11 = pybind11_dep diff --git a/tests/composition_sandbox/comp.cpp b/tests/composition_sandbox/comp.cpp index b628108..13e2c56 100644 --- a/tests/composition_sandbox/comp.cpp +++ b/tests/composition_sandbox/comp.cpp @@ -9,7 +9,6 @@ int main(int argv, char* argc[]) { } else { pathToConfigFile = "config.json"; } - Config::getInstance().loadConfig(pathToConfigFile); composition::Composition comp; std::vector symbols = {"H-1", "He-4"}; comp.registerSymbol(symbols);