diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index ed87571f00b035dddce3b2a66184133d1da737d4..ad34fce7044a8855f9b11a882fdac6fb52623b0b 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -20,6 +20,7 @@ add_library(pyarb MODULE config.cpp context.cpp domain_decomposition.cpp + error.cpp event_generator.cpp identifiers.cpp morphology.cpp diff --git a/python/domain_decomposition.cpp b/python/domain_decomposition.cpp index 3b43d215c840cef56457f1781a4dc35d3f3ac067..a87710130b3f20a4fffbbe6b8ae400f564f8a3aa 100644 --- a/python/domain_decomposition.cpp +++ b/python/domain_decomposition.cpp @@ -10,6 +10,7 @@ #include <arbor/load_balance.hpp> #include "context.hpp" +#include "error.hpp" #include "recipe.hpp" #include "strprintf.hpp" @@ -104,7 +105,13 @@ void register_domain_decomposition(pybind11::module& m) { // The Python recipe has to be shimmed for passing to the function that takes a C++ recipe. m.def("partition_load_balance", [](std::shared_ptr<py_recipe>& recipe, const context_shim& ctx, arb::partition_hint_map hint_map) { - return arb::partition_load_balance(py_recipe_shim(recipe), ctx.context, std::move(hint_map)); + try { + return arb::partition_load_balance(py_recipe_shim(recipe), ctx.context, std::move(hint_map)); + } + catch (...) { + py_reset_and_throw(); + throw; + } }, "Construct a domain_decomposition that distributes the cells in the model described by recipe\n" "over the distributed and local hardware resources described by context.\n" diff --git a/python/error.cpp b/python/error.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a4d8c48a7436693f3c218a2eaa8a50ed9560d04d --- /dev/null +++ b/python/error.cpp @@ -0,0 +1,18 @@ +#include <pybind11/pybind11.h> + +#include "error.hpp" + +namespace pyarb { + +std::exception_ptr py_exception; +std::mutex py_callback_mutex; + +void py_reset_and_throw() { + if (py_exception) { + std::exception_ptr copy = py_exception; + py_exception = nullptr; + std::rethrow_exception(copy); + } +} + +} // namespace pyarb diff --git a/python/error.hpp b/python/error.hpp index f3b0cb6eda738f856e6e58f2e233139910995438..d475f2fa64eb6d81316dc80e5e576cee0e1c04e0 100644 --- a/python/error.hpp +++ b/python/error.hpp @@ -1,10 +1,14 @@ #pragma once +#include <mutex> #include <stdexcept> #include <string> namespace pyarb { +extern std::exception_ptr py_exception; +extern std::mutex py_callback_mutex; + // Python wrapper errors struct pyarb_error: std::runtime_error { @@ -19,4 +23,26 @@ void assert_throw(bool pred, const char* msg) { if (!pred) throw pyarb_error(msg); } +// This function resets a python exception to nullptr +// and rethrows a copy of the set python exception. +// It should be used in serial code +// just before handing control back to Python. +void py_reset_and_throw(); + +template <typename L> +auto try_catch_pyexception(L func, const char* msg){ + std::lock_guard<std::mutex> g(py_callback_mutex); + try { + if(!py_exception) { + return func(); + } + else { + throw pyarb_error(msg); + } + } + catch (pybind11::error_already_set& e) { + py_exception = std::current_exception(); + throw; + } +} } // namespace pyarb diff --git a/python/recipe.cpp b/python/recipe.cpp index 0f50326996c34c5f7a8c126b95aad3d54214898a..35283549326e2ea1d4e16ba02f93fb16d4d8732e 100644 --- a/python/recipe.cpp +++ b/python/recipe.cpp @@ -24,8 +24,9 @@ namespace pyarb { // The py::recipe::cell_decription returns a pybind11::object, that is // unwrapped and copied into a arb::util::unique_any. arb::util::unique_any py_recipe_shim::get_cell_description(arb::cell_gid_type gid) const { - pybind11::gil_scoped_acquire guard; - return convert_cell(impl_->cell_description(gid)); + return try_catch_pyexception( + [&](){ return convert_cell(impl_->cell_description(gid)); }, + "Python error already thrown"); } arb::probe_info cable_probe(std::string kind, arb::cell_member_type id, arb::mlocation loc) { @@ -44,7 +45,7 @@ arb::probe_info cable_probe(std::string kind, arb::cell_member_type id, arb::mlo return arb::probe_info{id, pkind, probe}; }; -std::vector<arb::event_generator> py_recipe_shim::event_generators(arb::cell_gid_type gid) const { +std::vector<arb::event_generator> convert_gen(std::vector<pybind11::object> pygens, arb::cell_gid_type gid) { using namespace std::string_literals; using pybind11::isinstance; using pybind11::cast; @@ -52,9 +53,6 @@ std::vector<arb::event_generator> py_recipe_shim::event_generators(arb::cell_gid // Aquire the GIL because it must be held when calling isinstance and cast. pybind11::gil_scoped_acquire guard; - // Get the python list of pyarb::event_generator_shim from the python front end. - auto pygens = impl_->event_generators(gid); - std::vector<arb::event_generator> gens; gens.reserve(pygens.size()); @@ -75,7 +73,11 @@ std::vector<arb::event_generator> py_recipe_shim::event_generators(arb::cell_gid return gens; } -// TODO: implement py_recipe_shim::probe_info +std::vector<arb::event_generator> py_recipe_shim::event_generators(arb::cell_gid_type gid) const { + return try_catch_pyexception( + [&](){ return convert_gen(impl_->event_generators(gid), gid); }, + "Python error already thrown"); +} std::string con_to_string(const arb::cell_connection& c) { return util::pprintf("<arbor.connection: source ({},{}), destination ({},{}), delay {}, weight {}>", diff --git a/python/recipe.hpp b/python/recipe.hpp index 3e3492424701795493df5780863b8320bd38538f..ce1ef326f26d2952a64e2ffc67c9679d0b94bd2a 100644 --- a/python/recipe.hpp +++ b/python/recipe.hpp @@ -122,8 +122,10 @@ public: py_recipe_shim(std::shared_ptr<py_recipe> r): impl_(std::move(r)) {} + const char* msg = "Python error already thrown"; + arb::cell_size_type num_cells() const override { - return impl_->num_cells(); + return try_catch_pyexception([&](){ return impl_->num_cells(); }, msg); } // The pyarb::recipe::cell_decription returns a pybind11::object, that is @@ -131,40 +133,40 @@ public: arb::util::unique_any get_cell_description(arb::cell_gid_type gid) const override; arb::cell_kind get_cell_kind(arb::cell_gid_type gid) const override { - return impl_->cell_kind(gid); + return try_catch_pyexception([&](){ return impl_->cell_kind(gid); }, msg); } arb::cell_size_type num_sources(arb::cell_gid_type gid) const override { - return impl_->num_sources(gid); + return try_catch_pyexception([&](){ return impl_->num_sources(gid); }, msg); } arb::cell_size_type num_targets(arb::cell_gid_type gid) const override { - return impl_->num_targets(gid); + return try_catch_pyexception([&](){ return impl_->num_targets(gid); }, msg); } arb::cell_size_type num_gap_junction_sites(arb::cell_gid_type gid) const override { - return impl_->num_gap_junction_sites(gid); + return try_catch_pyexception([&](){ return impl_->num_gap_junction_sites(gid); }, msg); } std::vector<arb::event_generator> event_generators(arb::cell_gid_type gid) const override; std::vector<arb::cell_connection> connections_on(arb::cell_gid_type gid) const override { - return impl_->connections_on(gid); + return try_catch_pyexception([&](){ return impl_->connections_on(gid); }, msg); } std::vector<arb::gap_junction_connection> gap_junctions_on(arb::cell_gid_type gid) const override { - return impl_->gap_junctions_on(gid); + return try_catch_pyexception([&](){ return impl_->gap_junctions_on(gid); }, msg); } arb::cell_size_type num_probes(arb::cell_gid_type gid) const override { - return impl_->num_probes(gid); + return try_catch_pyexception([&](){ return impl_->num_probes(gid); }, msg); } arb::probe_info get_probe(arb::cell_member_type id) const override { - return impl_->get_probe(id); + return try_catch_pyexception([&](){ return impl_->get_probe(id); }, msg); } - // TODO: wrap + // TODO: wrap and make thread safe arb::util::any get_global_properties(arb::cell_kind kind) const override { if (kind==arb::cell_kind::cable) { arb::cable_cell_global_properties gprop; diff --git a/python/simulation.cpp b/python/simulation.cpp index 60137614ae81f1c3a5bc0fb0f15a39a46aeb48bf..03b019063fe726a491392f3e686fdc1792373d66 100644 --- a/python/simulation.cpp +++ b/python/simulation.cpp @@ -22,7 +22,13 @@ void register_simulation(pybind11::module& m) { // before forwarding it to the arb::recipe constructor. .def(pybind11::init( [](std::shared_ptr<py_recipe>& rec, const arb::domain_decomposition& decomp, const context_shim& ctx) { - return new arb::simulation(py_recipe_shim(rec), decomp, ctx.context); + try { + return new arb::simulation(py_recipe_shim(rec), decomp, ctx.context); + } + catch (...) { + py_reset_and_throw(); + throw; + } }), // Release the python gil, so that callbacks into the python recipe don't deadlock. pybind11::call_guard<pybind11::gil_scoped_release>(),