diff --git a/.gitmodules b/.gitmodules index 6e6650ca..6fcd874a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,7 @@ -[submodule "pybind11"] - path = gitmodules/pybind11 - url = https://github.com/pybind/pybind11 - shallow = true [submodule "partmc"] path = gitmodules/partmc url = https://github.com/compdyn/partmc shallow = true -[submodule "pybind11_json"] - path = gitmodules/pybind11_json - url = https://github.com/pybind/pybind11_json - shallow = true [submodule "json"] path = gitmodules/json url = https://github.com/nlohmann/json @@ -58,3 +50,13 @@ path = gitmodules/hdf5 url = https://github.com/HDFGroup/hdf5.git shallow = true +[submodule "gitmodules/nanobind"] + path = gitmodules/nanobind + url = https://github.com/wjakob/nanobind +[submodule "gitmodules/nanobind_json"] + path = gitmodules/nanobind_json + url = https://github.com/Griger5/nanobind_json +[submodule "."] + branch = pypartmc +[submodule "nanobind_json"] + branch = pypartmc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bfa52bae..d6a42354 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ files: '.py' exclude: '.git' -default_stages: [commit] +default_stages: [pre-commit] repos: - repo: https://github.com/psf/black diff --git a/CMakeLists.txt b/CMakeLists.txt index d1f05c68..b8d7e1aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,10 @@ endforeach() project(_PyPartMC LANGUAGES C CXX Fortran) -find_package(PythonInterp REQUIRED) +find_package(Python 3.8 + REQUIRED COMPONENTS Interpreter Development.Module + OPTIONAL_COMPONENTS Development.SABIModule) +message(STATUS "Python_EXECUTABLE= ${Python_EXECUTABLE}") set(CMAKE_POSITION_INDEPENDENT_CODE ON) @@ -33,6 +36,12 @@ if(CMAKE_Fortran_COMPILER_ID STREQUAL GNU) add_compile_options($<$,$>:-fcheck=bounds>) endif() +# Shadow the CMake intrinsic install function so that included CMake code from dependency submodules does not +# interfere with "make install" issued by scikit-build (note that our intentional install() call is replaced with +# _install() below - following https://cmake.org/pipermail/cmake/2011-March/043320.html) +macro(install) +endmacro(install) + macro(add_prefix prefix rootlist) set(outlist) foreach(root ${${rootlist}}) @@ -49,12 +58,28 @@ add_definitions("-DSUNDIALS_INT64_T=1") ### sources ######################################################################################## set(PyPartMC_sources - pypartmc.cpp json_resource.cpp spec_file_pypartmc.cpp sys.cpp - run_sect.F90 run_sect_opt.F90 run_exact.F90 run_exact_opt.F90 aero_binned.F90 - run_part.F90 run_part_opt.F90 util.F90 aero_data.F90 aero_state.F90 env_state.F90 gas_data.F90 - gas_state.F90 scenario.F90 condense.F90 aero_particle.F90 bin_grid.F90 - camp_core.F90 photolysis.F90 aero_mode.F90 aero_dist.F90 bin_grid.cpp condense.cpp run_part.cpp - run_sect.cpp run_exact.cpp scenario.cpp util.cpp output.cpp output.F90 rand.cpp rand.F90 + pypartmc.cpp + util.cpp + util.F90 + sys.cpp + rand.cpp + rand.F90 + aero_data.F90 + spec_file_pypartmc.cpp + json_resource.cpp + bin_grid.F90 + bin_grid.cpp + aero_mode.F90 + aero_dist.F90 + env_state.F90 + gas_data.F90 + aero_particle.F90 + aero_state.F90 +# json_resource.cpp spec_file_pypartmc.cpp sys.cpp +# run_part.F90 run_part_opt.F90 util.F90 aero_data.F90 aero_state.F90 env_state.F90 gas_data.F90 +# gas_state.F90 scenario.F90 condense.F90 aero_particle.F90 bin_grid.F90 +# camp_core.F90 photolysis.F90 aero_mode.F90 aero_dist.F90 bin_grid.cpp condense.cpp run_part.cpp +# scenario.cpp util.cpp output.cpp output.F90 rand.cpp rand.F90 ) add_prefix(src/ PyPartMC_sources) @@ -512,14 +537,17 @@ if(DEFINED ENV{MOSAIC_HOME}) endif() ### PYBIND11 & PyPartMC ############################################################################ +find_package(nanobind CONFIG REQUIRED) -add_subdirectory(gitmodules/pybind11) -pybind11_add_module(_PyPartMC ${PyPartMC_sources}) +# add_subdirectory(gitmodules/pybind11) +add_subdirectory(gitmodules/nanobind) +nanobind_add_module(_PyPartMC STABLE_ABI ${PyPartMC_sources}) add_dependencies(_PyPartMC partmclib) set(PYPARTMC_INCLUDE_DIRS "${CMAKE_BINARY_DIR}/include;" "${CMAKE_SOURCE_DIR}/gitmodules/json/include;" - "${CMAKE_SOURCE_DIR}/gitmodules/pybind11_json/include;" + "${CMAKE_SOURCE_DIR}/gitmodules/nanobind/include;" + "${CMAKE_SOURCE_DIR}/gitmodules/nanobind_json/include;" "${CMAKE_SOURCE_DIR}/gitmodules/span/include;" "${CMAKE_SOURCE_DIR}/gitmodules/string_view-standalone/include;" "${CMAKE_SOURCE_DIR}/gitmodules/optional/include;" @@ -545,7 +573,7 @@ endif() foreach(target _PyPartMC) target_compile_options(${target} PRIVATE $<$:/W4 /WX> - $<$>:-Wall -Wextra -Wpedantic -Werror> + # $<$>:-Wall -Wextra -Wpedantic -Werror> $<$>:-Wno-unused-parameter> ) endforeach() @@ -555,31 +583,32 @@ file(GLOB PyPartMC_headers ${CMAKE_SOURCE_DIR}/src/*.hpp) if (NOT "${CMAKE_REQUIRED_INCLUDES}" STREQUAL "") message("CMAKE_REQUIRED_INCLUDES not empty! (${CMAKE_REQUIRED_INCLUDES})") endif() -foreach(file ${PyPartMC_headers}) - set(CMAKE_REQUIRED_INCLUDES "${PYPARTMC_INCLUDE_DIRS};${pybind11_INCLUDE_DIRS}") - set(CMAKE_REQUIRED_FLAGS "-Werror") - string(REGEX REPLACE "[\-./:]" "_" file_var ${file}) - check_cxx_source_compiles(" - // https://github.com/nlohmann/json/issues/1408 - #if defined(_WIN32) || defined(_WIN64) - # define HAVE_SNPRINTF - #endif - #include \"${file}\" - int main() { return 0;} - " - _header_self_contained_${file_var} - ) - unset(CMAKE_REQUIRED_INCLUDES) - unset(CMAKE_REQUIRED_FLAGS) - if (NOT _header_self_contained_${file_var}) - message(SEND_ERROR "non-self-contained header: ${file}") - if (${CMAKE_VERSION} VERSION_LESS "3.26.0") - file(READ "${CMAKE_BINARY_DIR}/CMakeFiles/CMakeError.log" tmp) - else() - file(READ "${CMAKE_BINARY_DIR}/CMakeFiles/CMakeConfigureLog.yaml" tmp) - endif() - message(FATAL_ERROR ${tmp}) - endif() - unset(file_var) -endforeach() - +# foreach(file ${PyPartMC_headers}) +# set(CMAKE_REQUIRED_INCLUDES "${PYPARTMC_INCLUDE_DIRS};${pybind11_INCLUDE_DIRS}") +# set(CMAKE_REQUIRED_FLAGS "-Werror") +# string(REGEX REPLACE "[\-./:]" "_" file_var ${file}) +# check_cxx_source_compiles(" +# // https://github.com/nlohmann/json/issues/1408 +# #if defined(_WIN32) || defined(_WIN64) +# # define HAVE_SNPRINTF +# #endif +# #include \"${file}\" +# int main() { return 0;} +# " +# _header_self_contained_${file_var} +# ) +# unset(CMAKE_REQUIRED_INCLUDES) +# unset(CMAKE_REQUIRED_FLAGS) +# if (NOT _header_self_contained_${file_var}) +# message(SEND_ERROR "non-self-contained header: ${file}") +# if (${CMAKE_VERSION} VERSION_LESS "3.26.0") +# file(READ "${CMAKE_BINARY_DIR}/CMakeFiles/CMakeError.log" tmp) +# else() +# file(READ "${CMAKE_BINARY_DIR}/CMakeFiles/CMakeConfigureLog.yaml" tmp) +# endif() +# message(FATAL_ERROR ${tmp}) +# endif() +# unset(file_var) +# endforeach() + +_install(TARGETS _PyPartMC LIBRARY DESTINATION PyPartMC) diff --git a/gitmodules/nanobind b/gitmodules/nanobind new file mode 160000 index 00000000..d4b245ad --- /dev/null +++ b/gitmodules/nanobind @@ -0,0 +1 @@ +Subproject commit d4b245ad69f729c3d2095be4c1cb5b94810dae26 diff --git a/gitmodules/nanobind_json b/gitmodules/nanobind_json new file mode 160000 index 00000000..6e9e15fe --- /dev/null +++ b/gitmodules/nanobind_json @@ -0,0 +1 @@ +Subproject commit 6e9e15fee7fee798ec3259cb1fa1829c3991b567 diff --git a/gitmodules/pybind11 b/gitmodules/pybind11 deleted file mode 160000 index 68a0b2df..00000000 --- a/gitmodules/pybind11 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 68a0b2dfd8cb3f5ac1846f22b6a8d0d539cb493c diff --git a/gitmodules/pybind11_json b/gitmodules/pybind11_json deleted file mode 160000 index 32043f43..00000000 --- a/gitmodules/pybind11_json +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 32043f433ed987b2c2ce99d689ec337bcbd4ba95 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..dac32a76 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.setuptools_scm] +local_scheme = "no-local-version" +version_scheme = "post-release" + +[build-system] +requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2", "setuptools-scm==8.3.1"] +build-backend = "scikit_build_core.build" + +[project] +name = "PyPartMC" +dynamic = ["version"] +description = "Python interface to PartMC" +readme = "README.md" +requires-python = ">=3.8" +authors = [ + {name = "https://github.com/open-atmos/PyPartMC/graphs/contributors", email = "nriemer@illinois.edu"} +] +classifiers = [ + "License :: GPL-3.0", +] + +[project.urls] +Documentation = "https://open-atmos.github.io/PyPartMC" +Source = "https://github.com/open-atmos/PyPartMC/" +Tracker = "https://github.com/open-atmos/PyPartMC/issues" \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 1547f25a..00000000 --- a/setup.py +++ /dev/null @@ -1,168 +0,0 @@ -#################################################################################################### -# This file is a part of PyPartMC licensed under the GNU General Public License v3 (LICENSE file) # -# Copyright (C) 2022 University of Illinois Urbana-Champaign # -# Authors: https://github.com/open-atmos/PyPartMC/graphs/contributors # -#################################################################################################### - -import os -import re -import subprocess -import sys -from pathlib import Path - -from setuptools import Extension, find_packages, setup -from setuptools.command.build_ext import build_ext - -# Convert distutils Windows platform specifiers to CMake -A arguments -PLAT_TO_CMAKE = { - "win32": "Win32", - "win-amd64": "x64", - "win-arm32": "ARM", - "win-arm64": "ARM64", -} - - -# A CMakeExtension needs a sourcedir instead of a file list. -# The name must be the _single_ output extension from the CMake build. -# If you need multiple extensions, see scikit-build. -class CMakeExtension(Extension): # pylint: disable=too-few-public-methods - def __init__(self, name, sourcedir=""): - Extension.__init__(self, name, sources=[]) - self.sourcedir = os.path.abspath(sourcedir) - - -class CMakeBuild(build_ext): - def build_extension(self, ext): # pylint: disable=too-many-branches - extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) - - # required for auto-detection & inclusion of auxiliary "native" libs - if not extdir.endswith(os.path.sep): - extdir += os.path.sep - - debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug - cfg = "Debug" if debug else "Release" - - # CMake lets you override the generator - we need to check this. - # Can be set with Conda-Build, for example. - cmake_generator = os.environ.get("CMAKE_GENERATOR", "") - - # Set Python_EXECUTABLE instead if you use PYBIND11_FINDPYTHON - cmake_args = [ - f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}", - f"-DPYTHON_EXECUTABLE={sys.executable}", - f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm - ] - build_args = [] - # Adding CMake arguments set as environment variable - # (needed e.g. to build for ARM OSx on conda-forge) - if "CMAKE_ARGS" in os.environ: - cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item] - - # In this example, we pass in the version to C++. You might not need to. - cmake_args += [f"-DVERSION_INFO={self.distribution.get_version()}"] - - if self.compiler.compiler_type != "msvc": - # Using Ninja-build since it a) is available as a wheel and b) - # multithreads automatically. MSVC would require all variables be - # exported for Ninja to pick it up, which is a little tricky to do. - # Users can override the generator with CMAKE_GENERATOR in CMake - # 3.15+. - if not cmake_generator: - try: - # pylint: disable=unused-import,import-outside-toplevel - import ninja # noqa: F401 - - cmake_args += ["-GNinja"] - except ImportError: - pass - - else: - # Single config generators are handled "normally" - single_config = any(x in cmake_generator for x in ("NMake", "Ninja")) - - # CMake allows an arch-in-generator style for backward compatibility - contains_arch = any(x in cmake_generator for x in ("ARM", "Win64")) - - # Specify the arch if using MSVC generator, but only if it doesn't - # contain a backward-compatibility arch spec already in the - # generator name. - if not single_config and not contains_arch: - cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]] - - # Multi-config generators have a different way to specify configs - if not single_config: - cmake_args += [ - f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}" - ] - build_args += ["--config", cfg] - - if sys.platform.startswith("darwin"): - # Cross-compile support for macOS - respect ARCHFLAGS if set - archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", "")) - if archs: - # pylint: disable=consider-using-f-string - cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))] - - # Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level - # across all generators. - if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ: - # self.parallel is a Python 3 only way to set parallel jobs by hand - # using -j in the build_ext call, not supported by pip or PyPA-build. - if hasattr(self, "parallel") and self.parallel: - # CMake 3.12+ only. - build_args += [f"-j{self.parallel}"] - - build_temp = os.path.join(self.build_temp, ext.name) - if not os.path.exists(build_temp): - os.makedirs(build_temp) - - subprocess.check_call(["cmake", ext.sourcedir] + cmake_args, cwd=build_temp) - subprocess.check_call(["cmake", "--build", "."] + build_args, cwd=build_temp) - - -# The information here can also be placed in setup.cfg - better separation of -# logic and declaration, and simpler if you include description/version in a file. -setup( - name="pypartmc", - use_scm_version={ - "local_scheme": "no-local-version", - "version_scheme": "post-release", - }, - author="PyPartMC team (see https://github.com/open-atmos/PyPartMC/graphs/contributors)", - author_email="nriemer@illinois.edu", - description="Python interface to PartMC", - long_description=(Path(__file__).parent / "README.md").read_text(), - long_description_content_type="text/markdown", - packages=find_packages(include=["PyPartMC", "PyPartMC.*"]), - ext_modules=[CMakeExtension("_PyPartMC")], - cmdclass={"build_ext": CMakeBuild}, - zip_safe=False, - python_requires=">=3.7", - setup_requires=["setuptools_scm"], - install_requires=["numpy"], - license="GPL-3.0", - project_urls={ - "Tracker": "https://github.com/open-atmos/PyPartMC/issues", - "Documentation": "https://open-atmos.github.io/PyPartMC", - "Source": "https://github.com/open-atmos/PyPartMC/", - }, - extras_require={ - "tests": [ - "pytest", - "pytest-order", - "fastcore!=1.5.8", # https://github.com/fastai/fastcore/issues/439 - "ghapi", - "scipy", - ], - "examples": [ - "matplotlib!=3.10.0", - "ipywidgets", - "voila", - "open-atmos-jupyter-utils", - "PySDM", - "PyMieScatt", - "SciPy", - "dustpy; platform_system != 'Windows'", - ], - }, -) diff --git a/PyPartMC/__init__.py b/src/PyPartMC/__init__.py similarity index 86% rename from PyPartMC/__init__.py rename to src/PyPartMC/__init__.py index 0b60e98d..543bd88b 100644 --- a/PyPartMC/__init__.py +++ b/src/PyPartMC/__init__.py @@ -69,10 +69,11 @@ def __generate_si(): SI-prefix-aware unit multipliers, resulting in e.g.: `p = 1000 * si.hPa` notation. Note: no dimensional analysis is done! """ -with __build_extension_env(): - import _PyPartMC - from _PyPartMC import * - from _PyPartMC import __all__ as _PyPartMC_all # pylint: disable=no-name-in-module - from _PyPartMC import __version__, __versions_of_build_time_dependencies__ +# with __build_extension_env(): +# from . import _PyPartMC +from ._PyPartMC import * - __all__ = tuple([*_PyPartMC_all, "si"]) +# from ._PyPartMC import __all__ as _PyPartMC_all # pylint: disable=no-name-in-module +# from ._PyPartMC import __version__, __versions_of_build_time_dependencies__ + +# __all__ = tuple([*_PyPartMC_all, "si"]) diff --git a/src/aero_data.hpp b/src/aero_data.hpp index 4143959f..f632f835 100644 --- a/src/aero_data.hpp +++ b/src/aero_data.hpp @@ -8,7 +8,6 @@ #include "pmc_resource.hpp" #include "json_resource.hpp" -#include "pybind11/stl.h" #include "aero_data_parameters.hpp" extern "C" void f_aero_data_ctor(void *ptr) noexcept; @@ -179,6 +178,7 @@ struct AeroData { &data[idx] ); } + return data; } @@ -227,16 +227,16 @@ struct AeroData { ); char name[AERO_SOURCE_NAME_LEN]; - auto names = pybind11::tuple(len); + auto names = nanobind::list(); for (int idx = 0; idx < len; idx++) { f_aero_data_source_name_by_index( self.ptr.f_arg(), &idx, name ); - names[idx] = std::string(name); + names.append(std::string(name)); } - return names; + return nanobind::tuple(names); } static auto names(const AeroData &self) { @@ -248,16 +248,16 @@ struct AeroData { ); char name[AERO_NAME_LEN]; - auto names = pybind11::tuple(len); + auto names = nanobind::list(); for (int idx = 0; idx < len; idx++) { f_aero_data_spec_name_by_index( self.ptr.f_arg(), &idx, name ); - names[idx] = std::string(name); + names.append(std::string(name)); } - return names; + return nanobind::tuple(names); } static auto kappa(const AeroData &self) { @@ -275,6 +275,7 @@ struct AeroData { &data[idx] ); } + return data; } @@ -293,6 +294,7 @@ struct AeroData { &data[idx] ); } + return data; } }; diff --git a/src/aero_mode.hpp b/src/aero_mode.hpp index 4ed3d8d9..51549eca 100644 --- a/src/aero_mode.hpp +++ b/src/aero_mode.hpp @@ -7,7 +7,6 @@ #pragma once #include "pmc_resource.hpp" -#include "pybind11/stl.h" #include "aero_data.hpp" #include "bin_grid.hpp" diff --git a/src/aero_particle.hpp b/src/aero_particle.hpp index eca3cc48..2cbfffec 100644 --- a/src/aero_particle.hpp +++ b/src/aero_particle.hpp @@ -9,7 +9,6 @@ #include "pmc_resource.hpp" #include "aero_data.hpp" #include "env_state.hpp" -#include "pybind11/stl.h" #include extern "C" void f_aero_particle_ctor(void *ptr) noexcept; @@ -46,7 +45,6 @@ extern "C" void f_aero_particle_id(const void *aero_particle_ptr, int64_t *val) extern "C" void f_aero_particle_refract_shell(const void *aero_particle_ptr, std::complex *val, const int *arr_size) noexcept; extern "C" void f_aero_particle_refract_core(const void *aero_particle_ptr, std::complex *val, const int *arr_size) noexcept; -namespace py = pybind11; struct AeroParticle { PMCResource ptr; std::shared_ptr aero_data; diff --git a/src/aero_state.hpp b/src/aero_state.hpp index 53f63606..74f10c0f 100644 --- a/src/aero_state.hpp +++ b/src/aero_state.hpp @@ -12,8 +12,9 @@ #include "aero_particle.hpp" #include "env_state.hpp" #include "bin_grid.hpp" -#include "pybind11/stl.h" #include "tl/optional.hpp" +// #include +#include extern "C" void f_aero_state_ctor( void *ptr @@ -318,8 +319,8 @@ struct AeroState { static auto masses( const AeroState &self, - const tl::optional> &include, - const tl::optional> &exclude + const tl::optional> &include, + const tl::optional> &exclude ) { int len; f_aero_state_len( @@ -388,8 +389,8 @@ struct AeroState { static auto diameters( const AeroState &self, - const tl::optional> &include, - const tl::optional> &exclude + const tl::optional> &include, + const tl::optional> &exclude ) { int len; f_aero_state_len( @@ -421,8 +422,8 @@ struct AeroState { static auto volumes( const AeroState &self, - const tl::optional> &include, - const tl::optional> &exclude + const tl::optional> &include, + const tl::optional> &exclude ) { int len; f_aero_state_len( @@ -502,9 +503,9 @@ struct AeroState { static auto mixing_state( const AeroState &self, - const tl::optional> &include, - const tl::optional> &exclude, - const tl::optional> &group + const tl::optional> &include, + const tl::optional> &exclude, + const tl::optional> &group ) { int len; f_aero_state_len( diff --git a/src/bin_grid.cpp b/src/bin_grid.cpp index 446539a7..42c0c45e 100644 --- a/src/bin_grid.cpp +++ b/src/bin_grid.cpp @@ -18,6 +18,7 @@ std::valarray histogram_1d( ); int data_size = values.size(); std::valarray data(len); + f_bin_grid_histogram_1d( bin_grid.ptr.f_arg(), begin(values), diff --git a/src/bin_grid.hpp b/src/bin_grid.hpp index 40ebe389..f22060ac 100644 --- a/src/bin_grid.hpp +++ b/src/bin_grid.hpp @@ -7,7 +7,9 @@ #pragma once #include "pmc_resource.hpp" -#include "pybind11/stl.h" +#include +#include +#include "nanobind/stl/string.h" extern "C" void f_bin_grid_ctor(void *ptr) noexcept; @@ -65,15 +67,19 @@ extern "C" void f_bin_grid_histogram_2d( const int *y_grid_size ) noexcept; +namespace nb = nanobind; + struct BinGrid { PMCResource ptr; - BinGrid(const int &n_bin, const std::string &grid_type, const double &min, const double &max) : + BinGrid(const int &n_bin, const nb::str &grid_type, const double &min, const double &max) : ptr(f_bin_grid_ctor, f_bin_grid_dtor) { + const std::string grid_type_str {grid_type.c_str()}; + int type = 0; - if (grid_type == "log") type = 1; - if (grid_type == "linear") type = 2; + if (grid_type_str == "log") type = 1; + if (grid_type_str == "linear") type = 2; if (type == 0) throw std::invalid_argument( "Invalid grid spacing." ); @@ -108,6 +114,7 @@ struct BinGrid { begin(data), &len ); + return data; } @@ -119,11 +126,13 @@ struct BinGrid { &len ); std::valarray data(len); + f_bin_grid_centers( self.ptr.f_arg(), begin(data), &len ); + return data; } diff --git a/src/gas_data.hpp b/src/gas_data.hpp index 7bcd0103..1c645c23 100644 --- a/src/gas_data.hpp +++ b/src/gas_data.hpp @@ -9,8 +9,8 @@ #include "json_resource.hpp" #include "pmc_resource.hpp" #include "gas_data_parameters.hpp" -#include "pybind11/stl.h" -#include "pybind11_json/pybind11_json.hpp" +#include "nanobind/nanobind.h" +#include "nanobind_json/nanobind_json.hpp" extern "C" void f_gas_data_ctor(void *ptr) noexcept; extern "C" void f_gas_data_dtor(void *ptr) noexcept; @@ -26,14 +26,14 @@ struct GasData { PMCResource ptr; const nlohmann::json json; - GasData(const pybind11::tuple &tpl) : + GasData(const nanobind::tuple &tpl) : ptr(f_gas_data_ctor, f_gas_data_dtor), json(tpl) { auto json_array = nlohmann::json::array(); for (const auto item : tpl) json_array.push_back(nlohmann::json::object({{ - item.cast(), + nanobind::cast(item), nlohmann::json::array() }})); @@ -82,16 +82,16 @@ struct GasData { ); char name[GAS_NAME_LEN]; - auto names = pybind11::tuple(len); + nanobind::list names; for (int idx = 0; idx < len; idx++) { f_gas_data_spec_name_by_index( self.ptr.f_arg(), &idx, name ); - names[idx] = std::string(name); + names.append(nanobind::str(name)); } - return names; + return nanobind::tuple(names); } }; diff --git a/src/input_guard.hpp b/src/input_guard.hpp index 9ac7b18f..0bb238c9 100644 --- a/src/input_guard.hpp +++ b/src/input_guard.hpp @@ -1,9 +1,7 @@ -#include #include #include #include #include -#include "pybind11_json/pybind11_json.hpp" #include "nlohmann/json.hpp" struct InputGuard { diff --git a/src/pypartmc.cpp b/src/pypartmc.cpp index 99c6f9af..ec641971 100644 --- a/src/pypartmc.cpp +++ b/src/pypartmc.cpp @@ -4,85 +4,133 @@ # Authors: https://github.com/open-atmos/PyPartMC/graphs/contributors # ##################################################################################################*/ -#include "pybind11/pybind11.h" +#include "nanobind/nanobind.h" +#include "nanobind/stl/complex.h" +#include "nanobind/stl/vector.h" +#include "nanobind/stl/string.h" +#include "nanobind/stl/shared_ptr.h" +#include "nanobind/stl/tuple.h" +#include "nanobind/stl/detail/nb_optional.h" +#include "nanobind/ndarray.h" #include "nlohmann/json.hpp" -#include "pybind11_json/pybind11_json.hpp" -#include "pybind11/complex.h" +#include "nanobind_json/nanobind_json.hpp" #include "sundials/sundials_config.h" #include "camp/version.h" +#include "tl/optional.hpp" #include "util.hpp" #include "rand.hpp" -#include "run_part.hpp" -#include "run_part_opt.hpp" -#include "run_sect.hpp" -#include "run_sect_opt.hpp" -#include "run_exact.hpp" -#include "run_exact_opt.hpp" -#include "aero_binned.hpp" +// #include "run_part.hpp" +// #include "run_part_opt.hpp" #include "aero_data.hpp" #include "aero_dist.hpp" #include "aero_mode.hpp" +#include "aero_particle.hpp" #include "aero_state.hpp" #include "env_state.hpp" #include "gas_data.hpp" -#include "gas_state.hpp" -#include "condense.hpp" +// #include "gas_state.hpp" +// #include "condense.hpp" #include "bin_grid.hpp" -#include "camp_core.hpp" -#include "photolysis.hpp" -#include "output.hpp" -#include "output_parameters.hpp" +// #include "camp_core.hpp" +// #include "photolysis.hpp" +// #include "output.hpp" +// #include "output_parameters.hpp" #define STRINGIFY(x) #x #define MACRO_STRINGIFY(x) STRINGIFY(x) -namespace py = pybind11; +namespace nb = nanobind; +namespace nl = nlohmann; -namespace PYBIND11_NAMESPACE { namespace detail { - template - struct type_caster> : optional_caster> {}; -}} - -PYBIND11_MODULE(_PyPartMC, m) { - m.doc() = R"pbdoc( - PyPartMC is a Python interface to PartMC. - )pbdoc"; - - m.def("run_part", &run_part, "Do a particle-resolved Monte Carlo simulation."); - m.def("run_part_timestep", &run_part_timestep, "Do a single time step"); - m.def("run_part_timeblock", &run_part_timeblock, "Do a time block"); - - m.def("condense_equilib_particles", &condense_equilib_particles, R"pbdoc( - Call condense_equilib_particle() on each particle in the aerosol - to ensure that every particle has its water content in - equilibrium. - )pbdoc"); - m.def("condense_equilib_particle", &condense_equilib_particle, R"pbdoc( - Determine the water equilibrium state of a single particle. - )pbdoc"); - - m.def("run_sect", &run_sect, "Do a 1D sectional simulation (Bott 1998 scheme)."); - m.def("run_exact", &run_exact, "Do an exact solution simulation."); - - py::class_(m, "AeroBinned", - R"pbdoc( - Aerosol number and volume distributions stored per size bin. - These quantities are densities both in volume (per m^3) and in radius - (per log_width). - )pbdoc" - ) - .def(py::init>()) - .def(py::init, const BinGrid&>()) - .def_property_readonly("num_conc", AeroBinned::num_conc, - "Returns the number concentration of each bin (#/m^3/log_width)") - .def_property_readonly("vol_conc", AeroBinned::vol_conc, - "Returns the volume concentration per bin per species (m^3/m^3/log_width)") - .def("add_aero_dist", AeroBinned::add_aero_dist, - "Adds an AeroDist to an AeroBinned") - ; +NAMESPACE_BEGIN(nanobind) +NAMESPACE_BEGIN(detail) + +template struct type_caster> { + NB_TYPE_CASTER(std::valarray, const_name("[") + const_name("std::valarray") + const_name("]")) + + using Caster = make_caster; + + bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept { + if (nb::isinstance(src)) { + try { + auto py_array = nb::cast(src); + size_t size = py_array.size(); + + value.resize(size); - py::class_>(m, "AeroData", + for (size_t i = 0; i < size; i++) { + value[i] = nb::cast(py_array[i]); + } + + return true; + } + catch (...) { + PyErr_Clear(); + return false; + } + } + else if (nb::isinstance>>(src)) { + try { + auto py_array = nb::cast>>(src); + size_t size = py_array.size(); + auto *data = py_array.data(); + + value.resize(size); + + for (size_t i = 0; i < size; i++) { + value[i] = data[i]; + } + + return true; + } + catch (...) { + PyErr_Clear(); + return false; + } + } + + return false; + } + + template + static handle from_cpp(T &&src, rv_policy policy, cleanup_list *cleanup) { + nb::list obj; + for (const auto& elem : src) { + obj.append(elem); + } + + return obj.release(); + } +}; + +template struct type_caster> : optional_caster> {}; + +NAMESPACE_END(detail) +NAMESPACE_END(nanobind) + +NB_MODULE(_PyPartMC, m) { + // m.doc() = R"pbdoc( + // PyPartMC is a Python interface to PartMC. + // )pbdoc"; + + // m.def("run_part", &run_part, "Do a particle-resolved Monte Carlo simulation."); + // m.def("run_part_timestep", &run_part_timestep, "Do a single time step"); + // m.def("run_part_timeblock", &run_part_timeblock, "Do a time block"); + // m.def("condense_equilib_particles", &condense_equilib_particles, R"pbdoc( + // Call condense_equilib_particle() on each particle in the aerosol + // to ensure that every particle has its water content in + // equilibrium. + // )pbdoc"); + // m.def("condense_equilib_particle", &condense_equilib_particle, R"pbdoc( + // Determine the water equilibrium state of a single particle. + // )pbdoc"); + + // // TODO #65 + // //m.def("run_sect", &run_sect, "Do a 1D sectional simulation (Bott 1998 scheme)."); + // //m.def("run_exact", &run_exact, "Do an exact solution simulation."); + + nb::class_(m, "AeroData", R"pbdoc( Aerosol material properties and associated data. @@ -104,24 +152,24 @@ PYBIND11_MODULE(_PyPartMC, m) { same, but without the \c _a suffix. )pbdoc" ) - .def(py::init()) + .def(nb::init()) .def("spec_by_name", AeroData::spec_by_name, "Returns the number of the species in AeroData with the given name") .def("__len__", AeroData::__len__, "Number of aerosol species") - .def_property_readonly("n_source", AeroData::n_source, + .def_prop_ro("n_source", AeroData::n_source, "Number of aerosol sources") - .def_property_readonly("sources", AeroData::sources, "return list of source names") - .def_property("frac_dim", &AeroData::get_frac_dim, &AeroData::set_frac_dim, + .def_prop_ro("sources", AeroData::sources, "return list of source names") + .def_prop_rw("frac_dim", &AeroData::get_frac_dim, &AeroData::set_frac_dim, "Volume fractal dimension (1)") - .def_property("vol_fill_factor", &AeroData::get_vol_fill_factor, + .def_prop_rw("vol_fill_factor", &AeroData::get_vol_fill_factor, &AeroData::set_vol_fill_factor, "Volume filling factor (1)") - .def_property("prime_radius", &AeroData::get_prime_radius, &AeroData::set_prime_radius, + .def_prop_rw("prime_radius", &AeroData::get_prime_radius, &AeroData::set_prime_radius, "Radius of primary particles (m)") - .def_property_readonly("densities", &AeroData::densities, + .def_prop_ro("densities", &AeroData::densities, "Return array of aerosol species densities") - .def_property_readonly("kappa", &AeroData::kappa, + .def_prop_ro("kappa", &AeroData::kappa, "Returns array of aerosol species hygroscopicity parameter kappa") - .def_property_readonly("molecular_weights", &AeroData::molecular_weights, + .def_prop_ro("molecular_weights", &AeroData::molecular_weights, "Returns array of aerosol species molecular weights") .def("density", &AeroData::density, "Return density of an aerosol species") .def("rad2vol", AeroData::rad2vol, @@ -132,11 +180,11 @@ PYBIND11_MODULE(_PyPartMC, m) { "Convert geometric diameter (m) to mass-equivalent volume (m^3).") .def("vol2diam", AeroData::vol2diam, "Convert mass-equivalent volume (m^3) to geometric diameter (m).") - .def_property_readonly("species", AeroData::names, + .def_prop_ro("species", AeroData::names, "returns list of aerosol species names") ; - py::class_(m, "AeroParticle", + nb::class_(m, "AeroParticle", R"pbdoc( Single aerosol particle data structure. @@ -146,59 +194,59 @@ PYBIND11_MODULE(_PyPartMC, m) { m^3) of the i'th aerosol species. )pbdoc" ) - .def(py::init, const std::valarray&>()) - .def_property_readonly("volumes", AeroParticle::volumes, + .def(nb::init, const std::valarray&>()) + .def_prop_ro("volumes", AeroParticle::volumes, "Constituent species volumes (m^3)") - .def_property_readonly("volume", AeroParticle::volume, + .def_prop_ro("volume", AeroParticle::volume, "Total volume of the particle (m^3).") .def("species_volume", - py::overload_cast(&AeroParticle::species_volume), + nb::overload_cast(&AeroParticle::species_volume), "Volume of a single species in the particle (m^3).") .def("species_volume", - py::overload_cast(&AeroParticle::species_volume_by_name), + nb::overload_cast(&AeroParticle::species_volume_by_name), "Volume of a single species in the particle (m^3).") - .def_property_readonly("dry_volume", AeroParticle::dry_volume, + .def_prop_ro("dry_volume", AeroParticle::dry_volume, "Total dry volume of the particle (m^3).") - .def_property_readonly("radius", AeroParticle::radius, + .def_prop_ro("radius", AeroParticle::radius, "Total radius of the particle (m).") - .def_property_readonly("dry_radius", AeroParticle::dry_radius, + .def_prop_ro("dry_radius", AeroParticle::dry_radius, "Total dry radius of the particle (m).") - .def_property_readonly("diameter", AeroParticle::diameter, + .def_prop_ro("diameter", AeroParticle::diameter, "Total diameter of the particle (m).") - .def_property_readonly("dry_diameter", AeroParticle::dry_diameter, + .def_prop_ro("dry_diameter", AeroParticle::dry_diameter, "Total dry diameter of the particle (m).") - .def_property_readonly("mass", AeroParticle::mass, + .def_prop_ro("mass", AeroParticle::mass, "Total mass of the particle (kg).") - .def("species_mass", py::overload_cast(&AeroParticle::species_mass), + .def("species_mass", nb::overload_cast(&AeroParticle::species_mass), "Mass of a single species in the particle (kg).") - .def("species_mass", py::overload_cast(&AeroParticle::species_mass_by_name), + .def("species_mass", nb::overload_cast(&AeroParticle::species_mass_by_name), "Mass of a single species in the particle (kg).") - .def_property_readonly("species_masses", AeroParticle::species_masses, + .def_prop_ro("species_masses", AeroParticle::species_masses, "Mass of all species in the particle (kg).") - .def_property_readonly("solute_kappa", AeroParticle::solute_kappa, + .def_prop_ro("solute_kappa", AeroParticle::solute_kappa, "Returns the average of the solute kappas (1).") - .def_property_readonly("moles", AeroParticle::moles, + .def_prop_ro("moles", AeroParticle::moles, "Total moles in the particle (1).") - .def_property_readonly("absorb_cross_sect", AeroParticle::absorb_cross_sect, + .def_prop_ro("absorb_cross_sect", AeroParticle::absorb_cross_sect, "Absorption cross-section (m^-2).") - .def_property_readonly("scatter_cross_sect", AeroParticle::scatter_cross_sect, + .def_prop_ro("scatter_cross_sect", AeroParticle::scatter_cross_sect, "Scattering cross-section (m^-2).") - .def_property_readonly("asymmetry", AeroParticle::asymmetry, + .def_prop_ro("asymmetry", AeroParticle::asymmetry, "Asymmetry parameter (1).") - .def_property_readonly("refract_shell", AeroParticle::refract_shell, + .def_prop_ro("refract_shell", AeroParticle::refract_shell, "Refractive index of the shell (1).") - .def_property_readonly("refract_core", AeroParticle::refract_core, + .def_prop_ro("refract_core", AeroParticle::refract_core, "Refractive index of the core (1).") - .def_property_readonly("sources", AeroParticle::sources, + .def_prop_ro("sources", AeroParticle::sources, "Number of original particles from each source that coagulated to form particle.") - .def_property_readonly("least_create_time", AeroParticle::least_create_time, + .def_prop_ro("least_create_time", AeroParticle::least_create_time, "First time a constituent was created (s).") - .def_property_readonly("greatest_create_time", AeroParticle::greatest_create_time, + .def_prop_ro("greatest_create_time", AeroParticle::greatest_create_time, "Last time a constituent was created (s).") - .def_property_readonly("id", AeroParticle::id, "Unique ID number.") + .def_prop_ro("id", AeroParticle::id, "Unique ID number.") .def("mobility_diameter", AeroParticle::mobility_diameter, "Mobility diameter of the particle (m).") - .def_property_readonly("density", AeroParticle::density, + .def_prop_ro("density", AeroParticle::density, "Average density of the particle (kg/m^3)") .def("approx_crit_rel_humid", AeroParticle::approx_crit_rel_humid, "Returns the approximate critical relative humidity (1).") @@ -214,7 +262,7 @@ PYBIND11_MODULE(_PyPartMC, m) { "Sets the aerosol particle volumes.") ; - py::class_(m, "AeroState", + nb::class_(m, "AeroState", R"pbdoc( The current collection of aerosol particles. @@ -228,38 +276,39 @@ PYBIND11_MODULE(_PyPartMC, m) { is typically cleared each time we output data to disk. )pbdoc" ) - .def(py::init, const double, const std::string>()) + .def(nb::init, const double, const std::string>()) .def("__len__", AeroState::__len__, "returns current number of particles") - .def_property_readonly("total_num_conc", AeroState::total_num_conc, + .def_prop_ro("total_num_conc", AeroState::total_num_conc, "returns the total number concentration of the population") - .def_property_readonly("total_mass_conc", AeroState::total_mass_conc, + .def_prop_ro("total_mass_conc", AeroState::total_mass_conc, "returns the total mass concentration of the population") - .def_property_readonly("num_concs", AeroState::num_concs, + .def_prop_ro("num_concs", AeroState::num_concs, "returns the number concentration of each particle in the population") .def("masses", AeroState::masses, "returns the total mass of each particle in the population", - py::arg("include") = py::none(), py::arg("exclude") = py::none()) + nb::arg("include") = nb::none(), nb::arg("exclude") = nb::none() + ) .def("volumes", AeroState::volumes, "returns the volume of each particle in the population", - py::arg("include") = py::none(), py::arg("exclude") = py::none()) - .def_property_readonly("dry_diameters", AeroState::dry_diameters, + nb::arg("include") = nb::none(), nb::arg("exclude") = nb::none()) + .def_prop_ro("dry_diameters", AeroState::dry_diameters, "returns the dry diameter of each particle in the population") .def("mobility_diameters", AeroState::mobility_diameters, "returns the mobility diameter of each particle in the population") .def("diameters", AeroState::diameters, "returns the diameter of each particle in the population", - py::arg("include") = py::none(), py::arg("exclude") = py::none()) + nb::arg("include") = nb::none(), nb::arg("exclude") = nb::none()) .def("crit_rel_humids", AeroState::crit_rel_humids, "returns the critical relative humidity of each particle in the population") .def("make_dry", AeroState::make_dry, "Make all particles dry (water set to zero).") - .def_property_readonly("ids", AeroState::ids, + .def_prop_ro("ids", AeroState::ids, "returns the IDs of all particles.") .def("mixing_state", AeroState::mixing_state, "returns the mixing state parameters (d_alpha, d_gamma, chi) of the population", - py::arg("include") = py::none(), py::arg("exclude") = py::none(), - py::arg("group") = py::none()) + nb::arg("include") = nb::none(), nb::arg("exclude") = nb::none(), + nb::arg("group") = nb::none()) .def("bin_average_comp", AeroState::bin_average_comp, "composition-averages population using BinGrid") .def("particle", AeroState::get_particle, @@ -268,8 +317,8 @@ PYBIND11_MODULE(_PyPartMC, m) { "returns a random particle from the population") .def("dist_sample", AeroState::dist_sample, "sample particles for AeroState from an AeroDist", - py::arg("AeroDist"), py::arg("sample_prop") = 1.0, py::arg("create_time") = 0.0, - py::arg("allow_doubling") = true, py::arg("allow_halving") = true) + nb::arg("AeroDist"), nb::arg("sample_prop") = 1.0, nb::arg("create_time") = 0.0, + nb::arg("allow_doubling") = true, nb::arg("allow_halving") = true) .def("add_particle", AeroState::add_particle, "add a particle to an AeroState") .def("add", AeroState::add, R"pbdoc(aero_state += aero_state_delta, including combining the @@ -296,7 +345,7 @@ PYBIND11_MODULE(_PyPartMC, m) { .def("zero", AeroState::zero, "remove all particles from an AeroState") ; - py::class_>(m, "GasData", + nb::class_(m, "GasData", R"pbdoc( Constant gas data. @@ -307,19 +356,18 @@ PYBIND11_MODULE(_PyPartMC, m) { is gas_state%%mix_rat(i). )pbdoc" ) - .def(py::init()) + .def(nb::init()) .def("__len__", GasData::__len__, "returns number of gas species") - .def_property_readonly("n_spec", GasData::__len__) + .def_prop_ro("n_spec", GasData::__len__) .def("__str__", GasData::__str__, "returns a string with JSON representation of the object") .def("spec_by_name", GasData::spec_by_name, "returns the number of the species in gas with the given name") - .def_property_readonly("species", GasData::names, "returns list of gas species names") + .def_prop_ro("species", GasData::names, "returns list of gas species names") ; - py::class_(m, - "EnvState", + nb::class_(m, "EnvState", R"pbdoc( Current environment state. @@ -329,253 +377,228 @@ PYBIND11_MODULE(_PyPartMC, m) { scenario_t. )pbdoc" ) - .def(py::init()) + .def(nb::init()) .def("set_temperature", EnvState::set_temperature, "sets the temperature of the environment state") - .def_property_readonly("temp", EnvState::temp, + .def_prop_ro("temp", EnvState::temp, "returns the current temperature of the environment state") - .def_property_readonly("rh", EnvState::rh, + .def_prop_ro("rh", EnvState::rh, "returns the current relative humidity of the environment state") - .def_property_readonly("elapsed_time", EnvState::get_elapsed_time, + .def_prop_ro("elapsed_time", EnvState::get_elapsed_time, "returns time since start_time (s).") - .def_property_readonly("start_time", EnvState::get_start_time, + .def_prop_ro("start_time", EnvState::get_start_time, "returns start time (s since 00:00 UTC on start_day)") - .def_property("height", &EnvState::get_height, &EnvState::set_height, + .def_prop_rw("height", &EnvState::get_height, &EnvState::set_height, "Box height (m)") - .def_property("pressure", &EnvState::get_pressure, &EnvState::set_pressure, + .def_prop_rw("pressure", &EnvState::get_pressure, &EnvState::set_pressure, "Ambient pressure (Pa)") - .def_property_readonly("air_density", &EnvState::air_density, + .def_prop_ro("air_density", &EnvState::air_density, "Air density (kg m^{-3})") - .def_property("additive_kernel_coefficient", &EnvState::get_additive_kernel_coefficient, &EnvState::set_additive_kernel_coefficient, + .def_prop_rw("additive_kernel_coefficient", &EnvState::get_additive_kernel_coefficient, &EnvState::set_additive_kernel_coefficient, "Scaling coefficient for additive coagulation kernel.") ; - py::class_(m, - "Photolysis", - R"pbdoc( - PartMC interface to a photolysis module - )pbdoc" - ) - .def(py::init<>()) - ; - - py::class_(m, - "CampCore", - R"pbdoc( - An interface between PartMC and the CAMP - )pbdoc" - ) - .def(py::init<>()) - ; - - py::class_(m, - "Scenario", - R"pbdoc( - This is everything needed to drive the scenario being simulated. - - The temperature, pressure, emissions and background states are profiles - prescribed as functions of time by giving a number of times and - the corresponding data. Simple data such as temperature and pressure is - linearly interpolated between times, with constant interpolation - outside of the range of times. Gases and aerosols are - interpolated with gas_state_interp_1d() and - aero_dist_interp_1d(), respectively. - )pbdoc" - ) - .def( - py::init< - const GasData&, - const AeroData&, - const nlohmann::json& - >(), - "instantiates and initializes from a JSON object" - ) - .def("__str__", Scenario::__str__, - "returns a string with JSON representation of the object") - .def("init_env_state", Scenario::init_env_state, - "initializes the EnvState") - .def("aero_emissions", Scenario::get_dist, - "returns aero_emissions AeroDists at a given index") - .def_property_readonly("aero_emissions_n_times", Scenario::get_emissions_n_times, - "returns the number of times specified for emissions") - .def_property_readonly("aero_emissions_rate_scale", Scenario::emission_rate_scale, - "Aerosol emission rate scales at set-points (1)") - .def_property_readonly("aero_emissions_time", Scenario::emission_time) - .def("aero_background", Scenario::get_aero_background_dist, - "returns aero_background AeroDists at a given index") - .def_property_readonly("aero_dilution_n_times", Scenario::get_aero_dilution_n_times, - "returns the number of times specified for dilution") - .def_property_readonly("aero_dilution_rate", Scenario::aero_dilution_rate, - "Aerosol-background dilution rates at set-points (s^{-1})") - .def_property_readonly("aero_dilution_time", Scenario::aero_dilution_time, - "Aerosol-background dilution set-point times (s)") - - ; - - py::class_(m, - "GasState", - R"pbdoc( - Current state of the gas mixing ratios in the system. - - The gas species are defined by the gas_data_t structure, so that - \c gas_state%%mix_rat(i) is the current mixing ratio of the gas - with name \c gas_data%%name(i), etc. - - By convention, if gas_state_is_allocated() return \c .false., - then the gas_state is treated as zero for all operations on - it. This will be the case for new \c gas_state_t structures. - )pbdoc" - ) - .def(py::init>(), - "instantiates and initializes based on GasData") - .def("__setitem__", GasState::set_item) - //.def("__setitem__", GasState::set_items) - .def("__getitem__", GasState::get_item) - //.def("__getitem__", GasState::get_items) - .def("__len__", GasState::__len__, "returns number of gas species") - .def_property_readonly("n_spec", GasState::__len__, - "returns number of gas species") - .def("__str__", GasState::__str__, - "returns a string with JSON representation of the object") - .def("set_size", GasState::set_size, - "sets the GasState to the size of GasData") - .def("mix_rat", GasState::mix_rat, - "returns the mixing ratio of a gas species") - .def_property("mix_rats", &GasState::mix_rats, &GasState::set_mix_rats, - "provides access (read of write) to the array of mixing ratios") - ; - - py::class_(m, - "RunPartOpt", - "Options controlling the execution of run_part()." - ) - .def(py::init()) - .def_property_readonly("t_max", RunPartOpt::t_max, "total simulation time") - .def_property_readonly("del_t", RunPartOpt::del_t, "time step") - ; - - py::class_(m, - "RunSectOpt", - "Options controlling the execution of run_sect()." - ) - .def(py::init()) - .def_property_readonly("t_max", RunSectOpt::t_max, "total simulation time") - .def_property_readonly("del_t", RunSectOpt::del_t, "time step") - ; - - py::class_(m, - "RunExactOpt", - "Options controlling the execution of run_exact()." - ) - .def(py::init()) - .def_property_readonly("t_max", RunExactOpt::t_max, "total simulation time") - ; - - py::class_(m,"BinGrid") - .def(py::init()) + // py::class_(m, + // "Photolysis", + // R"pbdoc( + // PartMC interface to a photolysis module + // )pbdoc" + // ) + // .def(py::init<>()) + // ; + + // py::class_(m, + // "CampCore", + // R"pbdoc( + // An interface between PartMC and the CAMP + // )pbdoc" + // ) + // .def(py::init<>()) + // ; + + // py::class_(m, + // "Scenario", + // R"pbdoc( + // This is everything needed to drive the scenario being simulated. + + // The temperature, pressure, emissions and background states are profiles + // prescribed as functions of time by giving a number of times and + // the corresponding data. Simple data such as temperature and pressure is + // linearly interpolated between times, with constant interpolation + // outside of the range of times. Gases and aerosols are + // interpolated with gas_state_interp_1d() and + // aero_dist_interp_1d(), respectively. + // )pbdoc" + // ) + // .def( + // py::init< + // const GasData&, + // const AeroData&, + // const nlohmann::json& + // >(), + // "instantiates and initializes from a JSON object" + // ) + // .def("__str__", Scenario::__str__, + // "returns a string with JSON representation of the object") + // .def("init_env_state", Scenario::init_env_state, + // "initializes the EnvState") + // .def("aero_emissions", Scenario::get_dist, + // "returns aero_emissions AeroDists at a given index") + // .def_property_readonly("aero_emissions_n_times", Scenario::get_emissions_n_times, + // "returns the number of times specified for emissions") + // .def_property_readonly("aero_emissions_rate_scale", Scenario::emission_rate_scale, + // "Aerosol emission rate scales at set-points (1)") + // .def_property_readonly("aero_emissions_time", Scenario::emission_time) + // .def("aero_background", Scenario::get_aero_background_dist, + // "returns aero_background AeroDists at a given index") + // .def_property_readonly("aero_dilution_n_times", Scenario::get_aero_dilution_n_times, + // "returns the number of times specified for dilution") + // .def_property_readonly("aero_dilution_rate", Scenario::aero_dilution_rate, + // "Aerosol-background dilution rates at set-points (s^{-1})") + // .def_property_readonly("aero_dilution_time", Scenario::aero_dilution_time, + // "Aerosol-background dilution set-point times (s)") + + // ; + + // py::class_(m, + // "GasState", + // R"pbdoc( + // Current state of the gas mixing ratios in the system. + + // The gas species are defined by the gas_data_t structure, so that + // \c gas_state%%mix_rat(i) is the current mixing ratio of the gas + // with name \c gas_data%%name(i), etc. + + // By convention, if gas_state_is_allocated() return \c .false., + // then the gas_state is treated as zero for all operations on + // it. This will be the case for new \c gas_state_t structures. + // )pbdoc" + // ) + // .def(py::init>(), + // "instantiates and initializes based on GasData") + // .def("__setitem__", GasState::set_item) + // //.def("__setitem__", GasState::set_items) + // .def("__getitem__", GasState::get_item) + // //.def("__getitem__", GasState::get_items) + // .def("__len__", GasState::__len__, "returns number of gas species") + // .def_property_readonly("n_spec", GasState::__len__, + // "returns number of gas species") + // .def("__str__", GasState::__str__, + // "returns a string with JSON representation of the object") + // .def("set_size", GasState::set_size, + // "sets the GasState to the size of GasData") + // .def("mix_rat", GasState::mix_rat, + // "returns the mixing ratio of a gas species") + // .def_property("mix_rats", &GasState::mix_rats, &GasState::set_mix_rats, + // "provides access (read of write) to the array of mixing ratios") + // ; + + // py::class_(m, + // "RunPartOpt", + // "Options controlling the execution of run_part()." + // ) + // .def(py::init()) + // .def_property_readonly("t_max", RunPartOpt::t_max, "total simulation time") + // .def_property_readonly("del_t", RunPartOpt::del_t, "time step") + // ; + + nb::class_(m,"BinGrid") + .def(nb::init()) .def("__len__", BinGrid::__len__, "returns number of bins") - .def_property_readonly("edges", BinGrid::edges, "Bin edges") - .def_property_readonly("centers", BinGrid::centers, "Bin centers") - .def_property_readonly("widths", BinGrid::widths, "Bin widths") + .def_prop_ro("edges", BinGrid::edges, "Bin edges") + .def_prop_ro("centers", BinGrid::centers, "Bin centers") + .def_prop_ro("widths", BinGrid::widths, "Bin widths") ; - py::class_(m,"AeroMode") - .def(py::init()) - .def_property("num_conc", &AeroMode::get_num_conc, &AeroMode::set_num_conc, + nb::class_(m,"AeroMode") + .def(nb::init()) + .def_prop_rw("num_conc", &AeroMode::get_num_conc, &AeroMode::set_num_conc, "provides access (read or write) to the total number concentration of a mode") .def("num_dist", &AeroMode::num_dist, "returns the binned number concenration of a mode") - .def_property("vol_frac", &AeroMode::get_vol_frac, + .def_prop_rw("vol_frac", &AeroMode::get_vol_frac, &AeroMode::set_vol_frac, "Species fractions by volume") - .def_property("vol_frac_std", &AeroMode::get_vol_frac_std, + .def_prop_rw("vol_frac_std", &AeroMode::get_vol_frac_std, &AeroMode::set_vol_frac_std, "Species fraction standard deviation") - .def_property("char_radius", &AeroMode::get_char_radius, + .def_prop_rw("char_radius", &AeroMode::get_char_radius, &AeroMode::set_char_radius, "Characteristic radius, with meaning dependent on mode type (m)") - .def_property("gsd", &AeroMode::get_gsd, + .def_prop_rw("gsd", &AeroMode::get_gsd, &AeroMode::set_gsd, "Geometric standard deviation") .def("set_sample", &AeroMode::set_sampled) - .def_property_readonly("sample_num_conc", &AeroMode::get_sample_num_conc, + .def_prop_ro("sample_num_conc", &AeroMode::get_sample_num_conc, "Sample bin number concentrations (m^{-3})") - .def_property_readonly("sample_radius", &AeroMode::get_sample_radius, + .def_prop_ro("sample_radius", &AeroMode::get_sample_radius, "Sample bin radii (m).") - .def_property("type", &AeroMode::get_type, &AeroMode::set_type, + .def_prop_rw("type", &AeroMode::get_type, &AeroMode::set_type, "Mode type (given by module constants)") - .def_property("name", &AeroMode::get_name, &AeroMode::set_name, + .def_prop_rw("name", &AeroMode::get_name, &AeroMode::set_name, "Mode name, used to track particle sources") ; - py::class_(m,"AeroDist") - .def(py::init, const nlohmann::json&>()) - .def_property_readonly("n_mode", &AeroDist::get_n_mode, + nb::class_(m,"AeroDist") + .def(nb::init, const nlohmann::json&>()) + .def_prop_ro("n_mode", &AeroDist::get_n_mode, "Number of aerosol modes") - .def_property_readonly("num_conc", &AeroDist::get_total_num_conc, + .def_prop_ro("num_conc", &AeroDist::get_total_num_conc, "Total number concentration of a distribution (#/m^3)") .def("mode", AeroDist::get_mode, "returns the mode of a given index") ; m.def( - "histogram_1d", &histogram_1d, py::return_value_policy::copy, + "histogram_1d", &histogram_1d, nb::rv_policy::copy, "Return a 1D histogram with of the given weighted data, scaled by the bin sizes." ); m.def( - "histogram_2d", &histogram_2d, py::return_value_policy::copy, + "histogram_2d", &histogram_2d, nb::rv_policy::copy, "Return a 2D histogram with of the given weighted data, scaled by the bin sizes." ); // TODO #120: auto util = m.def_submodule("util", "..."); m.def( - "pow2_above", &pow2_above, py::return_value_policy::copy, + "pow2_above", &pow2_above, nb::rv_policy::copy, "Return the least power-of-2 that is at least equal to n." ); m.def( - "sphere_vol2rad", &sphere_vol2rad, py::return_value_policy::copy, + "sphere_vol2rad", &sphere_vol2rad, nb::rv_policy::copy, "Convert mass-equivalent volume (m^3) to geometric radius (m) for spherical particles." ); m.def( - "rad2diam", &rad2diam, py::return_value_policy::copy, + "rad2diam", &rad2diam, nb::rv_policy::copy, "Convert radius (m) to diameter (m)." ); m.def( - "sphere_rad2vol", &sphere_rad2vol, py::return_value_policy::copy, + "sphere_rad2vol", &sphere_rad2vol, nb::rv_policy::copy, "Convert geometric radius (m) to mass-equivalent volume for spherical particles." ); m.def( - "diam2rad", &diam2rad, py::return_value_policy::copy, + "diam2rad", &diam2rad, nb::rv_policy::copy, "Convert diameter (m) to radius (m)." ); - m.def( - "loss_rate_dry_dep", &loss_rate_dry_dep, py::return_value_policy::copy, - "Compute and return the dry deposition rate for a given particle." - ); + // m.def( + // "loss_rate_dry_dep", &loss_rate_dry_dep, py::return_value_policy::copy, + // "Compute and return the dry deposition rate for a given particle." + // ); - m.def( - "loss_rate", &loss_rate, py::return_value_policy::copy, - "Evaluate a loss rate function." - ); + // m.def( + // "loss_rate", &loss_rate, py::return_value_policy::copy, + // "Evaluate a loss rate function." + // ); - m.def( - "output_state", &output_state, "Output current state to netCDF file." - ); + // m.def( + // "output_state", &output_state, "Output current state to netCDF file." + // ); - m.def( - "input_state", &input_state, "Read current state from run_part netCDF output file." - ); - - m.def( - "input_sectional", &input_sectional, "Read current state from run_sect netCDF output file." - ); - - m.def( - "input_exact", &input_exact, "Read current state from run_exact netCDF output file." - ); + // m.def( + // "input_state", &input_state, "Read current state from netCDF output file." + // ); m.def( "rand_init", &rand_init, "Initializes the random number generator to the state defined by the given seed. If the seed is 0 then a seed is auto-generated from the current time" @@ -585,57 +608,50 @@ PYBIND11_MODULE(_PyPartMC, m) { "rand_normal", &rand_normal, "Generates a normally distributed random number with the given mean and standard deviation" ); - m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); - - auto vobtd = py::dict(); - vobtd["pybind11"] = MACRO_STRINGIFY(PYBIND11_VERSION_MAJOR) "." MACRO_STRINGIFY(PYBIND11_VERSION_MINOR) "." MACRO_STRINGIFY(PYBIND11_VERSION_PATCH); - vobtd["PartMC"] = PARTMC_VERSION; - vobtd["SUNDIALS"] = SUNDIALS_VERSION; - vobtd["CAMP"] = CAMP_VERSION; - // TODO #164 - // - expose git hashes? - // - more submodules (netCDF, ...) - m.attr("__versions_of_build_time_dependencies__") = vobtd; - - m.attr("__all__") = py::make_tuple( - "__version__", - "AeroBinned", - "AeroData", - "AeroDist", - "AeroMode", - "AeroState", - "AeroParticle", - "BinGrid", - "CampCore", - "EnvState", - "GasData", - "GasState", - "Photolysis", - "RunPartOpt", - "RunSectOpt", - "RunExactOpt", - "Scenario", - "condense_equilib_particles", - "run_part", - "run_part_timeblock", - "run_part_timestep", - "run_sect", - "run_exact", - "pow2_above", - "condense_equilib_particle", - "histogram_1d", - "histogram_2d", - "sphere_vol2rad", - "rad2diam", - "sphere_rad2vol", - "diam2rad", - "loss_rate_dry_dep", - "loss_rate", - "output_state", - "input_state", - "input_sectional", - "input_exact", - "rand_init", - "rand_normal" - ); + // m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); + + // auto vobtd = py::dict(); + // vobtd["pybind11"] = MACRO_STRINGIFY(PYBIND11_VERSION_MAJOR) "." MACRO_STRINGIFY(PYBIND11_VERSION_MINOR) "." MACRO_STRINGIFY(PYBIND11_VERSION_PATCH); + // vobtd["PartMC"] = PARTMC_VERSION; + // vobtd["SUNDIALS"] = SUNDIALS_VERSION; + // vobtd["CAMP"] = CAMP_VERSION; + // // TODO #164 + // // - expose git hashes? + // // - more submodules (netCDF, ...) + // m.attr("__versions_of_build_time_dependencies__") = vobtd; + + // m.attr("__all__") = py::make_tuple( + // "__version__", + // "AeroData", + // "AeroDist", + // "AeroMode", + // "AeroState", + // "AeroParticle", + // "BinGrid", + // "CampCore", + // "EnvState", + // "GasData", + // "GasState", + // "Photolysis", + // "RunPartOpt", + // "Scenario", + // "condense_equilib_particles", + // "run_part", + // "run_part_timeblock", + // "run_part_timestep", + // "pow2_above", + // "condense_equilib_particle", + // "histogram_1d", + // "histogram_2d", + // "sphere_vol2rad", + // "rad2diam", + // "sphere_rad2vol", + // "diam2rad", + // "loss_rate_dry_dep", + // "loss_rate", + // "output_state", + // "input_state", + // "rand_init", + // "rand_normal" + // ); } diff --git a/tests/test_aero_mode.py b/tests/test_aero_mode.py index bb2651fd..2f4ab6a9 100644 --- a/tests/test_aero_mode.py +++ b/tests/test_aero_mode.py @@ -315,7 +315,7 @@ def test_fixed_segfault_case_on_circular_reference(): ppmc.AeroMode(aero_data, fishy_ctor_arg) # assert - assert "incompatible constructor arguments" in str(exc_info.value) + assert "incompatible function arguments" in str(exc_info.value) @staticmethod @pytest.mark.skipif(platform.machine() == "arm64", reason="TODO #348")