diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 9a000b3e987640b707004b02eb5d1b0027961f73..3f4611f76a3ff5ac4df500d4dc85e8f960bcfe05 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -24,7 +24,6 @@ add_library(pyarb MODULE pyarb.cpp recipe.cpp schedule.cpp - strings.cpp ) target_link_libraries(pyarb PRIVATE arbor pybind11::module) diff --git a/python/context.cpp b/python/context.cpp index bab2b27813d147661440edb427f00000201350eb..19eb59efcd23ba26eaf05c2fdcee34db3be946ec 100644 --- a/python/context.cpp +++ b/python/context.cpp @@ -11,7 +11,7 @@ #include "context.hpp" #include "conversion.hpp" #include "error.hpp" -#include "strings.hpp" +#include "strprintf.hpp" #ifdef ARB_MPI_ENABLED #include "mpi.hpp" @@ -19,6 +19,18 @@ namespace pyarb { +std::ostream& operator<<(std::ostream& o, const context_shim& ctx) { + auto& c = ctx.context; + const bool gpu = arb::has_gpu(c); + const bool mpi = arb::has_mpi(c); + return + o << "<context: threads " << arb::num_threads(c) + << ", gpu " << (gpu? "yes": "no") + << ", mpi " << (mpi? "yes": "no") + << " ranks " << arb::num_ranks(c) + << ">"; +} + // A Python shim that holds the information that describes an arb::proc_allocation. struct proc_allocation_shim { arb::util::optional<int> gpu_id = {}; @@ -51,23 +63,9 @@ struct proc_allocation_shim { } }; -// Helper template for printing C++ optional types in Python. -// Prints either the value, or None if optional value is not set. -template <typename T> -std::string to_string(const arb::util::optional<T>& o) { - if (!o) return "None"; - - std::stringstream s; - s << *o; - return s.str(); -} - -std::string proc_alloc_string(const proc_allocation_shim& a) { - std::stringstream s; - s << "<hardware resource allocation: threads " << a.num_threads - << ", gpu id " << to_string(a.gpu_id); - s << ">"; - return s.str(); +std::ostream& operator<<(std::ostream& o, const proc_allocation_shim& alloc) { + return o << "<hardware resource allocation: threads " << alloc.num_threads + << ", gpu id " << alloc.gpu_id << ">"; } void register_contexts(pybind11::module& m) { @@ -90,8 +88,8 @@ void register_contexts(pybind11::module& m) { "Corresponds to the integer index used to identify GPUs in CUDA API calls.") .def_property_readonly("has_gpu", &proc_allocation_shim::has_gpu, "Whether a GPU is being used (True/False).") - .def("__str__", &proc_alloc_string) - .def("__repr__", &proc_alloc_string); + .def("__str__", util::to_string<proc_allocation_shim>) + .def("__repr__", util::to_string<proc_allocation_shim>); // context pybind11::class_<context_shim> context(m, "context", "An opaque handle for the hardware resources used in a simulation."); @@ -166,8 +164,8 @@ void register_contexts(pybind11::module& m) { "The number of distributed domains (equivalent to the number of MPI ranks).") .def_property_readonly("rank", [](const context_shim& ctx){return arb::rank(ctx.context);}, "The numeric id of the local domain (equivalent to MPI rank).") - .def("__str__", [](const context_shim& c){return context_string(c.context);}) - .def("__repr__", [](const context_shim& c){return context_string(c.context);}); + .def("__str__", util::to_string<context_shim>) + .def("__repr__", util::to_string<context_shim>); } } // namespace pyarb diff --git a/python/conversion.hpp b/python/conversion.hpp index ad0ae582d5fa24477863144fae2f404012fe7b85..6db53d8ec4287be78cdfc9112d108e5809dfb649 100644 --- a/python/conversion.hpp +++ b/python/conversion.hpp @@ -5,6 +5,7 @@ #include <pybind11/stl.h> #include <arbor/util/optional.hpp> + #include "error.hpp" // from https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html?highlight=boost%3A%3Aoptional#c-17-library-containers diff --git a/python/identifiers.cpp b/python/identifiers.cpp index 3aae5a61c8790d798a825c97e804e1139dc23f96..78824eff83f07e1d46bc5c614917e6d9cbda89ba 100644 --- a/python/identifiers.cpp +++ b/python/identifiers.cpp @@ -1,13 +1,16 @@ +#include <ostream> #include <string> #include <arbor/common_types.hpp> #include <pybind11/pybind11.h> -#include "strings.hpp" +#include "strprintf.hpp" namespace pyarb { +using util::pprintf; + void register_identifiers(pybind11::module& m) { using namespace pybind11::literals; @@ -36,8 +39,8 @@ void register_identifiers(pybind11::module& m) { "The global identifier of the cell.") .def_readwrite("index", &arb::cell_member_type::index, "Cell-local index of the item.") - .def("__str__", &cell_member_string) - .def("__repr__", &cell_member_string); + .def("__str__", [](arb::cell_member_type m) {return pprintf("<cell member: gid {}, index {}>", m.gid, m.index);}) + .def("__repr__",[](arb::cell_member_type m) {return pprintf("<cell member: gid {}, index {}>", m.gid, m.index);}); pybind11::enum_<arb::cell_kind>(m, "cell_kind", "Enumeration used to identify the cell kind, used by the model to group equal kinds in the same cell group.") diff --git a/python/mpi.cpp b/python/mpi.cpp index 15f1b5df9d582ed756f155b3dc0f0d8aa06a2762..6afb861a59e2ef13710fb51fce0cd1021bcb4a5e 100644 --- a/python/mpi.cpp +++ b/python/mpi.cpp @@ -1,9 +1,11 @@ #include <sstream> #include <string> +#include <pybind11/pybind11.h> + #include <arbor/version.hpp> -#include <pybind11/pybind11.h> +#include "strprintf.hpp" #ifdef ARB_MPI_ENABLED #include <mpi.h> @@ -84,13 +86,13 @@ int mpi_is_finalized() { // Define the stringifier for mpi_comm_shim here, to minimise the ifdefication // elsewhere in this wrapper code. -std::string mpi_comm_string(const mpi_comm_shim& c) { - std::stringstream s; - - s << "<mpi_comm: "; - if (c.comm==MPI_COMM_WORLD) s << "MPI_COMM_WORLD>"; - else s << c.comm << ">"; - return s.str(); +std::ostream& operator<<(std::ostream& o, const mpi_comm_shim& c) { + if (c.comm==MPI_COMM_WORLD) { + return o << "<mpi communicator: MPI_COMM_WORLD>"; + } + else { + return o << "<mpi communicator: " << c.comm << ">"; + } } void register_mpi(pybind11::module& m) { @@ -100,8 +102,8 @@ void register_mpi(pybind11::module& m) { mpi_comm .def(pybind11::init<>()) .def(pybind11::init([](pybind11::object o){return mpi_comm_shim(o);})) - .def("__str__", &mpi_comm_string) - .def("__repr__", &mpi_comm_string); + .def("__str__", util::to_string<mpi_comm_shim>) + .def("__repr__", util::to_string<mpi_comm_shim>); m.def("mpi_init", &mpi_init, "Initialize MPI with MPI_THREAD_SINGLE, as required by Arbor."); m.def("mpi_finalize", &mpi_finalize, "Finalize MPI (calls MPI_Finalize)"); diff --git a/python/recipe.cpp b/python/recipe.cpp index 8bb0980ea3818844e599101c13f679d3374b2d3a..413c00be8727676cdfc71fc1901abd6acb40d079 100644 --- a/python/recipe.cpp +++ b/python/recipe.cpp @@ -15,6 +15,7 @@ #include "error.hpp" #include "event_generator.hpp" +#include "strprintf.hpp" #include "recipe.hpp" namespace pyarb { @@ -111,20 +112,14 @@ std::vector<arb::event_generator> py_recipe_shim::event_generators(arb::cell_gid // TODO: implement py_recipe_shim::probe_info -std::string connection_string(const arb::cell_connection& c) { - std::stringstream s; - s << "<connection: (" << c.source.gid << "," << c.source.index << ")" - << " -> (" << c.dest.gid << "," << c.dest.index << ")" - << " , delay " << c.delay << ", weight " << c.weight << ">"; - return s.str(); +std::string con_to_string(const arb::cell_connection& c) { + return util::pprintf("<connection: ({},{}) -> ({},{}), delay {}, weight {}>", + c.source.gid, c.source.index, c.dest.gid, c.dest.index, c.delay, c.weight); } -std::string gap_junction_connection_string(const arb::gap_junction_connection& gc) { - std::stringstream s; - s << "<connection: (" << gc.local.gid << "," << gc.local.index << ")" - << " -> (" << gc.peer.gid << "," << gc.peer.index << ")" - << " , conductance " << gc.ggap << ">"; - return s.str(); +std::string gj_to_string(const arb::gap_junction_connection& gc) { + return util::pprintf("<gap junction: ({},{}) <-> ({},{}), conductance {}>", + gc.local.gid, gc.local.index, gc.peer.gid, gc.peer.index, gc.ggap); } void register_recipe(pybind11::module& m) { @@ -157,8 +152,8 @@ void register_recipe(pybind11::module& m) { "The weight of the connection (unit: Sâ‹…cmâ»Â²).") .def_readwrite("delay", &arb::cell_connection::delay, "The delay time of the connection (unit: ms).") - .def("__str__", &connection_string) - .def("__repr__", &connection_string); + .def("__str__", &con_to_string) + .def("__repr__", &con_to_string); // Gap Junction Connections pybind11::class_<arb::gap_junction_connection> gap_junction_connection(m, "gap_junction_connection", @@ -182,8 +177,8 @@ void register_recipe(pybind11::module& m) { "Other half of the gap junction connection.") .def_readwrite("ggap", &arb::gap_junction_connection::ggap, "Gap junction conductance (unit: μS).") - .def("__str__", &gap_junction_connection_string) - .def("__repr__", &gap_junction_connection_string); + .def("__str__", &gj_to_string) + .def("__repr__", &gj_to_string); // Recipes pybind11::class_<py_recipe, diff --git a/python/schedule.cpp b/python/schedule.cpp index 2b81a148df5a746366a92ce8e2494b0b5617e147..f295b85c65c1c76ebcf90197db38e9b0ec479c0e 100644 --- a/python/schedule.cpp +++ b/python/schedule.cpp @@ -6,9 +6,28 @@ #include "conversion.hpp" #include "schedule.hpp" +#include "strprintf.hpp" namespace pyarb { +std::ostream& operator<<(std::ostream& o, const regular_schedule_shim& x) { + return o << "<regular_schedule: tstart " + << x.tstart << " ms, dt " + << x.dt << " ms, tstop " + << x.tstop << " ms>"; +} + +std::ostream& operator<<(std::ostream& o, const explicit_schedule_shim& e) { + o << "<explicit_schedule: times ["; + return util::csv(o, e.times) << "] ms>"; +}; + +std::ostream& operator<<(std::ostream& o, const poisson_schedule_shim& p) { + return o << "<poisson_schedule: tstart " << p.tstart << " ms" + << ", freq " << p.freq << " Hz" + << ", seed " << p.seed << ">"; +}; + // // regular_schedule shim // @@ -148,8 +167,8 @@ void register_schedules(pybind11::module& m) { "No events delivered after this time (in ms).") .def_property("dt", ®ular_schedule_shim::get_dt, ®ular_schedule_shim::set_dt, "The interval between time points (in ms).") - ; //.def("__str__", &schedule_regular_string) - //.def("__repr__",&schedule_regular_string); + .def("__str__", util::to_string<regular_schedule_shim>) + .def("__repr__", util::to_string<regular_schedule_shim>); // Explicit schedule pybind11::class_<explicit_schedule_shim> explicit_schedule(m, "explicit_schedule", @@ -164,8 +183,8 @@ void register_schedules(pybind11::module& m) { " times: A list of times (in ms, default []).") .def_property("times", &explicit_schedule_shim::get_times, &explicit_schedule_shim::set_times, "A list of times (in ms).") - ; //.def("__str__", &schedule_explicit_string) - //.def("__repr__",&schedule_explicit_string); + .def("__str__", util::to_string<explicit_schedule_shim>) + .def("__repr__", util::to_string<explicit_schedule_shim>); // Poisson schedule pybind11::class_<poisson_schedule_shim> poisson_schedule(m, "poisson_schedule", @@ -184,53 +203,9 @@ void register_schedules(pybind11::module& m) { "The expected frequency (in Hz).") .def_readwrite("seed", &poisson_schedule_shim::seed, "The seed for the random number generator.") - ; //.def("__str__", &schedule_poisson_string) - //.def("__repr__",&schedule_poisson_string); + .def("__str__", util::to_string<poisson_schedule_shim>) + .def("__repr__", util::to_string<poisson_schedule_shim>); } } -/* -// Helper template for printing C++ optional types in Python. -// Prints either the value, or None if optional value is not set. -template <typename T> -std::string to_string(const arb::util::optional<T>& o, std::string unit) { - if (!o) return "None"; - - std::stringstream s; - s << *o << " " << unit; - return s.str(); -} - -std::string schedule_regular_string(const regular_schedule_shim& r) { - std::stringstream s; - s << "<regular_schedule: " - << "tstart " << to_string(r.tstart, "ms") << ", " - << "dt " << r.dt << " ms, " - << "tstop " << to_string(r.tstop, "ms") << ">"; - return s.str(); -}; - -std::string schedule_explicit_string(const explicit_schedule_shim& e) { - std::stringstream s; - s << "<explicit_schedule: times ["; - bool first = true; - for (auto t: e.times) { - if (!first) { - s << " "; - } - s << t; - first = false; - } - s << "] ms>"; - return s.str(); -}; - -std::string schedule_poisson_string(const poisson_schedule_shim& p) { - std::stringstream s; - s << "<poisson_schedule: tstart " << p.tstart << " ms" - << ", freq " << p.freq << " Hz" - << ", seed " << p.seed << ">"; - return s.str(); -}; -*/ diff --git a/python/strings.cpp b/python/strings.cpp deleted file mode 100644 index a2eb0e4ecdc03ded63aab4705fb243edb0534f7a..0000000000000000000000000000000000000000 --- a/python/strings.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include <sstream> -#include <string> - -#include <arbor/common_types.hpp> -#include <arbor/context.hpp> - -#include "strings.hpp" - -namespace pyarb { - -std::string cell_member_string(const arb::cell_member_type& m) { - std::stringstream s; - s << "<cell_member: gid " << m.gid - << ", index " << m.index << ">"; - return s.str(); -} - -std::string context_string(const arb::context& c) { - std::stringstream s; - const bool gpu = arb::has_gpu(c); - const bool mpi = arb::has_mpi(c); - s << "<context: threads " << arb::num_threads(c) - << ", gpu " << (gpu? "yes": "None") - << ", distributed " << (mpi? "MPI": "local") - << " ranks " << arb::num_ranks(c) - << ">"; - return s.str(); -} - -} // namespace pyarb diff --git a/python/strings.hpp b/python/strings.hpp deleted file mode 100644 index 0e378af476c17269adc5f5f7574c77c2492a72a9..0000000000000000000000000000000000000000 --- a/python/strings.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include <string> - -#include <arbor/common_types.hpp> -#include <arbor/context.hpp> - -// Utilities for generating string representations of types. -namespace pyarb { - -std::string cell_member_string(const arb::cell_member_type&); -std::string context_string(const arb::context&); - -} // namespace pyarb diff --git a/python/strprintf.hpp b/python/strprintf.hpp new file mode 100644 index 0000000000000000000000000000000000000000..431f83d2de6ae0010ebe0a85693382589022cfdd --- /dev/null +++ b/python/strprintf.hpp @@ -0,0 +1,190 @@ +#pragma once + +// printf-like routines that return std::string. + +#include <cstdio> +#include <memory> +#include <string> +#include <sstream> +#include <system_error> +#include <utility> +#include <vector> + +#include <arbor/util/optional.hpp> + +namespace pyarb { +namespace util { + +// Use ADL to_string or std::to_string, falling back to ostream formatting: + +namespace impl_to_string { + using std::to_string; + + template <typename T, typename = void> + struct select { + static std::string str(const T& value) { + std::ostringstream o; + o << value; + return o.str(); + } + }; + + // Can be eplaced with std::void_t in c++17. + template <typename ...Args> + using void_t = void; + + template <typename T> + struct select<T, void_t<decltype(to_string(std::declval<T>()))>> { + static std::string str(const T& v) { + return to_string(v); + } + }; +} + +template <typename T> +std::string to_string(const T& value) { + return impl_to_string::select<T>::str(value); +} + +// Use snprintf to format a string, with special handling for standard +// smart pointer types and strings. + +namespace impl { + template <typename X> + X sprintf_arg_translate(const X& x) { return x; } + + inline const char* sprintf_arg_translate(const std::string& x) { return x.c_str(); } + + template <typename T, typename Deleter> + T* sprintf_arg_translate(const std::unique_ptr<T, Deleter>& x) { return x.get(); } + + template <typename T> + T* sprintf_arg_translate(const std::shared_ptr<T>& x) { return x.get(); } +} + +template <typename... Args> +std::string strprintf(const char* fmt, Args&&... args) { + thread_local static std::vector<char> buffer(1024); + + for (;;) { + int n = std::snprintf(buffer.data(), buffer.size(), fmt, impl::sprintf_arg_translate(std::forward<Args>(args))...); + if (n<0) { + throw std::system_error(errno, std::generic_category()); + } + else if ((unsigned)n<buffer.size()) { + return std::string(buffer.data(), n); + } + buffer.resize(2*n); + } +} + +template <typename... Args> +std::string strprintf(const std::string& fmt, Args&&... args) { + return strprintf(fmt.c_str(), std::forward<Args>(args)...); +} + +// Substitute instances of '{}' in the format string with the following parameters, +// using default std::ostream formatting. + +namespace impl { + inline void pprintf_(std::ostringstream& o, const char* s) { + o << s; + } + + template <typename T, typename... Tail> + void pprintf_(std::ostringstream& o, const char* s, T&& value, Tail&&... tail) { + const char* t = s; + while (*t && !(t[0]=='{' && t[1]=='}')) { + ++t; + } + o.write(s, t-s); + if (*t) { + o << std::forward<T>(value); + pprintf_(o, t+2, std::forward<Tail>(tail)...); + } + } +} + +template <typename... Args> +std::string pprintf(const char *s, Args&&... args) { + std::ostringstream o; + impl::pprintf_(o, s, std::forward<Args>(args)...); + return o.str(); +} + +namespace impl { + + template <typename Seq> + struct sepval { + const Seq& seq_; + const char* sep_; + + sepval(const Seq& seq, const char* sep): seq_(seq), sep_(sep) {} + + friend std::ostream& operator<<(std::ostream& o, const sepval& s) { + bool first = true; + for (auto& x: s.seq_) { + if (!first) o << s.sep_; + first = false; + o << x; + } + return o; + } + }; + + template <typename Seq> + struct sepval_lim { + const Seq& seq_; + const char* sep_; + unsigned count_; + + sepval_lim(const Seq& seq, const char* sep, unsigned count): seq_(seq), sep_(sep), count_(count) {} + + friend std::ostream& operator<<(std::ostream& o, const sepval_lim& s) { + bool first = true; + unsigned n = s.count_; + for (auto& x: s.seq_) { + if (!first) { + o << s.sep_; + first = false; + } + if (!n) { + return o << "..."; + } + --n; + o << x; + } + return o; + } + }; +} + +template <typename Seq> +std::ostream& sepval(std::ostream& o, const char* sep, const Seq& seq) { + return o << impl::sepval<Seq>(seq, sep); +} + +template <typename Seq> +std::ostream& sepval(std::ostream& o, const char* sep, const Seq& seq, unsigned n) { + return o << impl::sepval_lim<Seq>(seq, sep, n); +} + +template <typename Seq> +std::ostream& csv(std::ostream& o, const Seq& seq) { + return o << impl::sepval<Seq>(seq, ", "); +} + +template <typename Seq> +std::ostream& csv(std::ostream& o, const Seq& seq, unsigned n) { + return o << impl::sepval_lim<Seq>(seq, ", ", n); +} + +} // namespace util + +template <typename T> +std::ostream& operator<<(std::ostream& o, const arb::util::optional<T>& x) { + return o << (x? util::to_string(*x): "None"); +} + +} // namespace pyarb + diff --git a/python/test/unit_distributed/test_contexts_arbmpi.py b/python/test/unit_distributed/test_contexts_arbmpi.py index 947eb45858100c06caec7b68048a638421e8df33..c999c343cba880b7a2c2f7bf0628e3e31df3aa7c 100644 --- a/python/test/unit_distributed/test_contexts_arbmpi.py +++ b/python/test/unit_distributed/test_contexts_arbmpi.py @@ -44,7 +44,7 @@ class Contexts_arbmpi(unittest.TestCase): comm = arb.mpi_comm() # test that by default communicator is MPI_COMM_WORLD - self.assertEqual(str(comm), '<mpi_comm: MPI_COMM_WORLD>') + self.assertEqual(str(comm), '<mpi communicator: MPI_COMM_WORLD>') def test_context_arbmpi(self): comm = arb.mpi_comm() diff --git a/python/test/unit_distributed/test_contexts_mpi4py.py b/python/test/unit_distributed/test_contexts_mpi4py.py index c93171751ae3f7b310838b72bbbc6bed2bb8f9bd..2a58c8bcc61e477a137168e2c063766cb445cb5b 100644 --- a/python/test/unit_distributed/test_contexts_mpi4py.py +++ b/python/test/unit_distributed/test_contexts_mpi4py.py @@ -37,7 +37,7 @@ class Contexts_mpi4py(unittest.TestCase): comm = arb.mpi_comm(mpi.COMM_WORLD) # test that set communicator is MPI_COMM_WORLD - self.assertEqual(str(comm), '<mpi_comm: MPI_COMM_WORLD>') + self.assertEqual(str(comm), '<mpi communicator: MPI_COMM_WORLD>') def test_context_mpi4py(self): comm = arb.mpi_comm(mpi.COMM_WORLD)