diff --git a/actions/generate_recipe.py b/actions/generate_recipe.py index 62e5d6ce8..e7719eca6 100644 --- a/actions/generate_recipe.py +++ b/actions/generate_recipe.py @@ -31,7 +31,6 @@ "git", "make", "libtool", - "pybind11", "sysroot_linux-64", } diff --git a/doc/source/acknowledgements.rst b/doc/source/acknowledgements.rst index bd48a7f1a..7f867b850 100644 --- a/doc/source/acknowledgements.rst +++ b/doc/source/acknowledgements.rst @@ -421,17 +421,14 @@ for parsing mmCIF files. This is used under the terms of the MIT or Boost Software License (the version bundled with gemmi is MIT, while the version on GitHub is BSL) -pyboost11 ---------- +zpp::serializer +--------------- -Thanks to Yung-Yu's excellent -`blog post `__ -on how to interconvert between boost::python and pybind11, and for providing -`pyboost11.hpp `__ -as a header-only library to support interconversion. This was really useful -to let me mix pybind11-wrapped gemmi objects with boost::python-wrapped sire. -The code is used under the terms of the liberal open source license -given at the top of the header file. +:mod:`sire` bundles the header-only +`zpp::serializer `__ library (the +copy distributed with gemmi) to exchange gemmi structures with the gemmi +Python module using gemmi's own binary serialization. This is used under +the terms of the MIT license. RDKit ----- diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index d025046a7..2ac03be4c 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -54,6 +54,10 @@ organisation on `GitHub `__. * Add link atoms to the QM region when using mechanical embedding. +* Updated to gemmi 0.7, which uses nanobind rather than pybind11 for its Python + bindings. Structures are now exchanged with the gemmi Python module using gemmi's + own binary serialization, removing the pybind11 dependency. + `2026.2.0 `__ - September 2026 ---------------------------------------------------------------------------------------------- diff --git a/pixi.toml b/pixi.toml index 2a8630ac1..6fd846aa7 100644 --- a/pixi.toml +++ b/pixi.toml @@ -10,7 +10,6 @@ platforms = ["linux-64", "linux-aarch64", "osx-arm64", "win-64"] python = "*" cmake = ">=3.30.0" git = "*" -pybind11 = "*" gsl = "*" libboost-devel = "*" libboost-python-devel = "*" @@ -30,7 +29,7 @@ make = "*" libtool = "*" sysroot_linux-64 = "==2.17" kartograf = ">=1.0.0" -gemmi = ">=0.6.4,<0.7.0" +gemmi = ">=0.7.0,<0.8.0" # Host dependencies for local source builds (not used by generate_recipe.py) [target.linux-64.host-dependencies] @@ -51,14 +50,14 @@ cxx-compiler = "*" make = "*" libtool = "*" kartograf = ">=1.0.0" -gemmi = ">=0.6.4,<0.7.0" +gemmi = ">=0.7.0,<0.8.0" [target.osx-arm64.host-dependencies] c-compiler = "*" cxx-compiler = "*" [target.win-64.dependencies] -gemmi = ">=0.6.4,<0.7.0" +gemmi = ">=0.7.0,<0.8.0" [target.win-64.host-dependencies] c-compiler = "*" @@ -164,17 +163,17 @@ rdkit = ">=2023.0.0" [feature.test.target.linux-64.dependencies] kartograf = ">=1.0.0" -gemmi = ">=0.6.4,<0.7.0" +gemmi = ">=0.7.0,<0.8.0" [feature.test.target.linux-aarch64.dependencies] kartograf = ">=1.0.0" [feature.test.target.osx-arm64.dependencies] kartograf = ">=1.0.0" -gemmi = ">=0.6.4,<0.7.0" +gemmi = ">=0.7.0,<0.8.0" [feature.test.target.win-64.dependencies] -gemmi = ">=0.6.4,<0.7.0" +gemmi = ">=0.7.0,<0.8.0" # kartograf excluded from Windows test deps diff --git a/wrapper/Convert/CMakeLists.txt b/wrapper/Convert/CMakeLists.txt index 7eacb4607..9528c860f 100644 --- a/wrapper/Convert/CMakeLists.txt +++ b/wrapper/Convert/CMakeLists.txt @@ -10,8 +10,7 @@ set( CMAKE_CXX_FLAGS "${OPTIMISED_CXX_FLAGS}" ) add_subdirectory (SireRDKit) add_subdirectory (SireOpenMM) -# SireGemmi currently disabled due to build issues -# add_subdirectory (SireGemmi) +add_subdirectory (SireGemmi) # installation set( INSTALLDIR ${SIRE_PYTHON}/sire/legacy/Convert ) diff --git a/wrapper/Convert/SireGemmi/CMakeLists.txt b/wrapper/Convert/SireGemmi/CMakeLists.txt index 69173463a..d0d0fa5d3 100644 --- a/wrapper/Convert/SireGemmi/CMakeLists.txt +++ b/wrapper/Convert/SireGemmi/CMakeLists.txt @@ -4,23 +4,19 @@ # ########################################## -# Find gemmi and pybind11 libraries +# Find gemmi library find_package(gemmi) -find_package(pybind11) -if (${gemmi_FOUND} AND ${pybind11_FOUND}) +if (${gemmi_FOUND}) # Only compile and install if we have gemmi message( STATUS "Compiling SireGemmi converter" ) get_target_property(GEMMI_LOCATION gemmi::gemmi_cpp IMPORTED_LOCATION_RELEASE) message( STATUS "gemmi location: ${GEMMI_LOCATION}" ) - message( STATUS "PYBIND11 INCLUDE ${pybind11_INCLUDE_DIR}") - message( STATUS "PYBIND11 LIBRARY ${pybind11_LIBRARIES}" ) message( STATUS "ZLIB LIBRARIES ${ZLIB_LIBRARIES}" ) # Third Party dependencies of this module include_directories( ${PYTHON_INCLUDE_DIR} - ${pybind11_INCLUDE_DIR} ${gemmi_INCLUDE_DIR} ) # Sire include paths @@ -59,7 +55,6 @@ if (${gemmi_FOUND} AND ${pybind11_FOUND}) SIRE_SireStream SIRE_SireError gemmi::gemmi_cpp - ${pybind11_LIBRARIES} ) include( LimitSirePythonExportSymbols ) diff --git a/wrapper/Convert/SireGemmi/_SireGemmi.main.cpp b/wrapper/Convert/SireGemmi/_SireGemmi.main.cpp index 482f668d7..5c2865aee 100644 --- a/wrapper/Convert/SireGemmi/_SireGemmi.main.cpp +++ b/wrapper/Convert/SireGemmi/_SireGemmi.main.cpp @@ -2,33 +2,51 @@ // (C) Christopher Woods, GPL >= 3 License #include "boost/python.hpp" -#include "boost/python/converter/registry.hpp" #include "sire_gemmi.h" -#include "Helpers/pyboost11.hpp" +#include namespace bp = boost::python; using namespace SireGemmi; -#include +bp::object sire_to_gemmi_state_py(const SireSystem::System &system, + const SireBase::PropertyMap &map) +{ + const auto state = sire_to_gemmi_state(system, map); + + return bp::object(bp::handle<>( + PyBytes_FromStringAndSize(state.data(), state.size()))); +} + +SireSystem::System gemmi_state_to_sire_py(const bp::object &state, + const SireBase::PropertyMap &map) +{ + char *data = nullptr; + Py_ssize_t size = 0; + + if (PyBytes_AsStringAndSize(state.ptr(), &data, &size) != 0) + bp::throw_error_already_set(); + + return gemmi_state_to_sire(std::string(data, size), map); +} BOOST_PYTHON_MODULE(_SireGemmi) { - bp::def("sire_to_gemmi", - &sire_to_gemmi, + bp::def("_sire_to_gemmi_state", + &sire_to_gemmi_state_py, (bp::arg("mols"), bp::arg("map")), - "Convert sire system to a gemmi structure"); + "Convert sire system to the pickle state of a gemmi Structure"); - bp::def("gemmi_to_sire", - &gemmi_to_sire, - (bp::arg("mols"), bp::arg("map")), - "Convert a gemmi Structure to a sire system"); + bp::def("_gemmi_state_to_sire", + &gemmi_state_to_sire_py, + (bp::arg("state"), bp::arg("map")), + "Convert the pickle state of a gemmi Structure to a sire system"); + + bp::scope().attr("_gemmi_version") = GEMMI_VERSION; bp::def("_register_pdbx_loader", ®ister_pdbx_loader, "Internal function called once used to register PDBx support"); - - pyboost11::converter(); } diff --git a/wrapper/Convert/SireGemmi/sire_gemmi.cpp b/wrapper/Convert/SireGemmi/sire_gemmi.cpp index ce76a91cb..073424717 100644 --- a/wrapper/Convert/SireGemmi/sire_gemmi.cpp +++ b/wrapper/Convert/SireGemmi/sire_gemmi.cpp @@ -2,24 +2,29 @@ #include "sire_gemmi.h" #include "gemmi/cif.hpp" +#include "gemmi/mmcif.hpp" #include "gemmi/modify.hpp" #include "gemmi/polyheur.hpp" +#include "gemmi/read_cif.hpp" #include "gemmi/to_cif.hpp" #include "gemmi/to_mmcif.hpp" -#include "gemmi/mmcif.hpp" + +// gemmi/serialize.hpp requires the zpp serializer to be included first +#include "gemmi/serialize.hpp" +#include "third_party/serializer.h" #include "SireIO/pdbx.h" #include "SireMol/core.h" -#include "SireMol/moleditor.h" #include "SireMol/element.h" +#include "SireMol/moleditor.h" -#include "SireMol/atomproperty.hpp" -#include "SireMol/atomelements.h" -#include "SireMol/atomcoords.h" #include "SireMol/atomcharges.h" -#include "SireMol/connectivity.h" +#include "SireMol/atomcoords.h" +#include "SireMol/atomelements.h" +#include "SireMol/atomproperty.hpp" #include "SireMol/bondhunter.h" +#include "SireMol/connectivity.h" #include "SireMol/iswater.h" @@ -30,9 +35,9 @@ #include "SireError/errors.h" -#include -#include #include +#include +#include namespace cif = gemmi::cif; @@ -1265,6 +1270,30 @@ namespace SireGemmi return lines; } + SireSystem::System gemmi_state_to_sire(const std::string &state, + const SireBase::PropertyMap &map) + { + gemmi::Structure structure; + + zpp::serializer::memory_view_input_archive in( + reinterpret_cast(state.data()), state.size()); + in(structure); + + return gemmi_to_sire(structure, map); + } + + std::string sire_to_gemmi_state(const SireSystem::System &system, + const SireBase::PropertyMap &map) + { + const auto structure = sire_to_gemmi(system, map); + + std::vector data; + zpp::serializer::memory_output_archive out(data); + out(structure); + + return std::string(data.begin(), data.end()); + } + void register_pdbx_loader() { SireIO::PDBxReaderFunction reader_function(&pdbx_reader_function); diff --git a/wrapper/Convert/SireGemmi/sire_gemmi.h b/wrapper/Convert/SireGemmi/sire_gemmi.h index c1eb80e17..de643f5da 100644 --- a/wrapper/Convert/SireGemmi/sire_gemmi.h +++ b/wrapper/Convert/SireGemmi/sire_gemmi.h @@ -11,6 +11,7 @@ namespace gemmi #include "SireBase/propertymap.h" #include +#include namespace SireGemmi { @@ -21,6 +22,13 @@ namespace SireGemmi gemmi::Structure sire_to_gemmi(const SireSystem::System &system, const SireBase::PropertyMap &map); + // Structures cross to Python as gemmi's pickle state (zpp serialized) + SireSystem::System gemmi_state_to_sire(const std::string &state, + const SireBase::PropertyMap &map); + + std::string sire_to_gemmi_state(const SireSystem::System &system, + const SireBase::PropertyMap &map); + void register_pdbx_loader(); } diff --git a/wrapper/Convert/SireGemmi/third_party/LICENSE.zpp_serializer b/wrapper/Convert/SireGemmi/third_party/LICENSE.zpp_serializer new file mode 100644 index 000000000..ffb4dcdcb --- /dev/null +++ b/wrapper/Convert/SireGemmi/third_party/LICENSE.zpp_serializer @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Eyal Z + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/wrapper/Convert/SireGemmi/third_party/serializer.h b/wrapper/Convert/SireGemmi/third_party/serializer.h new file mode 100644 index 000000000..7126b29fe --- /dev/null +++ b/wrapper/Convert/SireGemmi/third_party/serializer.h @@ -0,0 +1,3372 @@ +#ifndef ZPP_SERIALIZER_H +#define ZPP_SERIALIZER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if __cplusplus >= 201703L +#include +#include +#endif +#ifdef ZPP_SERIALIZER_FREESTANDING +#include +#else +#include +#include +#include +#endif + +namespace zpp +{ + /** + * Supports serialization of objects and polymorphic objects. + * Example of non polymorphic serialization: + * ~~~ + * class point + * { + * public: + * point() = default; + * point(int x, int y) noexcept : + * m_x(x), + * m_y(y) + * { + * } + * + * friend zpp::serializer::access; + * template + * static void serialize(Archive & archive, Self & self) + * { + * archive(self.m_x, self.m_y); + * } + * + * int get_x() const noexcept + * { + * return m_x; + * } + * + * int get_y() const noexcept + * { + * return m_y; + * } + * + * private: + * int m_x = 0; + * int m_y = 0; + * }; + * + * static void foo() + * { + * std::vector data; + * zpp::serializer::memory_input_archive in(data); + * zpp::serializer::memory_output_archive out(data); + * + * out(point(1337, 1338)); + * + * point my_point; + * in(my_point); + * + * std::cout << my_point.get_x() << ' ' << my_point.get_y() << '\n'; + * } + * ~~~ + * + * Example of polymorphic serialization: + * ~~~ + * class person : public zpp::serializer::polymorphic + * { + * public: + * person() = default; + * explicit person(std::string name) noexcept : + * m_name(std::move(name)) + * { + * } + * + * friend zpp::serializer::access; + * template + * static void serialize(Archive & archive, Self & self) + * { + * archive(self.m_name); + * } + * + * const std::string & get_name() const noexcept + * { + * return m_name; + * } + * + * virtual void print() const + * { + * std::cout << "person: " << m_name; + * } + * + * private: + * std::string m_name; + * }; + * + * class student : public person + * { + * public: + * student() = default; + * student(std::string name, std::string university) noexcept : + * person(std::move(name)), + * m_university(std::move(university)) + * { + * } + * + * friend zpp::serializer::access; + * template + * static void serialize(Archive & archive, Self & self) + * { + * person::serialize(archive, self); + * archive(self.m_university); + * } + * + * virtual void print() const + * { + * std::cout << "student: " << person::get_name() << ' ' << + * m_university << '\n'; + * } + * + * private: + * std::string m_university; + * }; + * + * namespace + * { + * zpp::serializer::register_types< + * zpp::serializer::make_type, + * zpp::serializer::make_type > _; } // + * + * static void foo() + * { + * std::vector data; + * zpp::serializer::memory_input_archive in(data); + * zpp::serializer::memory_output_archive out(data); + * + * std::unique_ptr my_person = + * std::make_unique("1337", "1337University"); out(my_person); + * + * my_person = nullptr; + * in(my_person); + * + * my_person->print(); + * } + * + * static void bar() + * { + * std::vector data; + * zpp::serializer::memory_input_archive in(data); + * zpp::serializer::memory_output_archive out(data); + * + * out(zpp::serializer::as_polymorphic(student("1337", + * "1337University"))); + * + * std::unique_ptr my_person; + * in(my_person); + * + * my_person->print(); + * } + * ~~~ + */ + namespace serializer + { +#ifdef ZPP_SERIALIZER_FREESTANDING + namespace freestanding + { + /** + * Returns the error category for a given error code enumeration type, + * using an argument dependent lookup of a user implemented category + * function. + */ + template + decltype(auto) category() + { + return category(ErrorCode{}); + } + + /** + * The error category which responsible for translating error codes to + * error messages. + */ + class error_category + { + public: + /** + * Returns the error category name. + */ + virtual std::string_view name() const noexcept = 0; + + /** + * Return the error message for a given error code. + * For success codes, it is unspecified what value is returned. + * For convienience, you may return zpp::error::no_error for success. + * All other codes must return non empty string views. + */ + virtual std::string_view message(int code) const noexcept = 0; + + /** + * Returns true if success code, else false. + */ + bool success(int code) const + { + return code == m_success_code; + } + + protected: + /** + * Creates an error category whose success code is 'success_code'. + */ + constexpr error_category(int success_code) : m_success_code(success_code) + { + } + + /** + * Destroys the error category. + */ + ~error_category() = default; + + private: + /** + * The success code. + */ + int m_success_code{}; + }; + + /** + * Creates an error category, whose name and success + * code are specified, as well as a message translation + * logic that returns the error message for every error code. + * Note: message translation must not throw. + */ + template + constexpr auto make_error_category(std::string_view name, + ErrorCode success_code, + Messages &&messages) + { + // Create a category with the name and messages. + class category final : public error_category, + private std::remove_reference_t + { + public: + constexpr category(std::string_view name, + ErrorCode success_code, + Messages &&messages) : error_category(std::underlying_type_t(success_code)), + std::remove_reference_t( + std::forward(messages)), + m_name(name) + { + } + + std::string_view name() const noexcept override + { + return m_name; + } + + std::string_view message(int code) const noexcept override + { + return this->operator()(ErrorCode{code}); + } + + private: + std::string_view m_name; + } category(name, success_code, std::forward(messages)); + + // Return the category. + return category; + } + + /** + * Represents an error to be initialized from an error code + * enumeration. + * The error code enumeration must have 'int' as underlying type. + * Defining an error code enum and a category for it goes as follows. + * Example: + * ~~~ + * namespace my_namespace + * { + * enum class my_error : int + * { + * success = 0, + * something_bad = 1, + * something_really_bad = 2, + * }; + * + * inline const zpp::error_category & category(my_error) + * { + * constexpr static auto error_category = + * zpp::make_error_category("my_category", my_error::success, + * [](auto code) -> std::string_view { + * switch (code) { + * case my_error::success: + * return zpp::error::no_error; + * case my_code:something_bad: + * return "Something bad happened."; + * case my_error::something_really_bad: + * return "Something really bad happened."; + * default: + * return "Unknown error occurred."; + * } + * } + * ); + * return error_category; + * } + * } // my_namespace + * ~~~ + */ + class error + { + public: + /** + * Disables default construction. + */ + error() = delete; + + /** + * Constructs an error from an error code enumeration, the + * category is looked by using argument dependent lookup on a + * function named 'category' that receives the error code + * enumeration value. + */ + template + error(ErrorCode error_code) : m_category(std::addressof( + zpp::serializer::freestanding::category())), + m_code(std::underlying_type_t(error_code)) + { + } + + /** + * Constructs an error from an error code enumeration, the + * category is given explicitly in this overload. + */ + template + error(ErrorCode error_code, const error_category &category) : m_category(std::addressof(category)), + m_code(std::underlying_type_t(error_code)) + { + } + + /** + * Returns the error category. + */ + const error_category &category() const + { + return *m_category; + } + + /** + * Returns the error code. + */ + int code() const + { + return m_code; + } + + /** + * Returns the error message. Calling this on + * a success error is implementation defined according + * to the error category. + */ + std::string_view message() const + { + return m_category->message(m_code); + } + + /** + * Returns true if the error indicates success, else false. + */ + explicit operator bool() const + { + return m_category->success(m_code); + } + + /** + * No error message value. + */ + static constexpr std::string_view no_error{}; + + private: + /** + * The error category. + */ + const error_category *m_category{}; + + /** + * The error code. + */ + int m_code{}; + }; + } // namespace freestanding + + enum class error : int + { + success = 0, + out_of_range = 1, + variant_is_valueless = 2, + null_pointer_serialization = 3, + }; + + inline const freestanding::error_category &category(error) + { + constexpr static auto error_category = + freestanding::make_error_category( + "zpp::serializer", + error::success, + [](auto code) -> std::string_view + { + switch (code) + { + case error::success: + return freestanding::error::no_error; + case error::out_of_range: + return "[zpp::serializer] Out of range error"; + case error::variant_is_valueless: + return "[zpp::serializer] Cannot serialize a " + "valueless variant."; + case error::null_pointer_serialization: + return "[zpp::serializer] Cannot serialize a null " + "pointer."; + default: + return "[zpp::serializer] Unknown error occurred."; + } + }); + return error_category; + } +#endif // ZPP_SERIALIZER_FREESTANDING + + namespace detail + { + /** + * Map any sequence of types to void. + */ + template + using void_t = void; + + /** + * Tests if all conditions are true, empty means true. + * Example: + * ~~~ + * all_of::value == false + * all_of::value == true + * all_of::value == false + * all_of<>::value == true + * ~~~ + */ + template + struct all_of : std::true_type + { + }; + + template + struct all_of : std::false_type + { + }; + + template + struct all_of : all_of + { + }; + + template <> + struct all_of : std::true_type + { + }; + + /** + * Remove const of container value_type + */ + template + struct container_nonconst_value_type + { + using type = std::remove_const_t; + }; + + /** + * Same as above, except in case of std::map and std::unordered_map, and + * similar, we also need to remove the const of the key type. + */ + template