NEURON
wrapper.cpp
Go to the documentation of this file.
1 /*
2  * Copyright 2023 Blue Brain Project, EPFL.
3  * See the top-level LICENSE file for details.
4  *
5  * SPDX-License-Identifier: Apache-2.0
6  */
7 #include <filesystem>
8 #include <optional>
9 #include <set>
10 #include <vector>
11 
12 // 3rd party headers
13 #include <fmt/format.h>
14 #include <pybind11/embed.h>
15 #include <pybind11/stl.h>
16 
17 // NMODL headers
19 #include "pybind/ode_py.hpp"
20 #include "pybind/wrapper.hpp"
21 #include "pybind/pyembed.hpp"
22 #include "utils/common_utils.hpp"
23 
24 
25 namespace fs = std::filesystem;
26 namespace py = pybind11;
27 using namespace py::literals;
28 
29 namespace nmodl {
30 namespace pybind_wrappers {
31 
32 // This wrapper is used for obtaining better coverage in `ode.py`.
33 // Since we embed the `ode.py` as a string, there is no way to check what was covered via running
34 // pytest or similar. Instead, we use the coverage.py API directly.
35 static void run_python_script(const std::string& script, const py::dict& locals) {
36 #ifdef NRN_ENABLE_COVERAGE
37  // to prevent race conditions during testing, we generate a random suffix
38  const auto& suffix =
40 
41  py::exec(fmt::format(R"(
42 import coverage
43 cov = coverage.Coverage(data_suffix='{}')
44 cov.start()
45 )",
46  suffix),
47  locals);
48  const auto& code_with_mapping = std::string("exec(compile(r'''" + ode_py + script + "''', '" +
49  ode_py_path + "', 'exec'))");
50  py::exec(code_with_mapping, locals);
51 #else
52  py::exec(ode_py + script, locals);
53 #endif
54 
55 #ifdef NRN_ENABLE_COVERAGE
56  const auto& path = fs::current_path() / fmt::format("coverage_{}.xml", suffix);
57  py::exec(fmt::format(R"(
58 cov.stop()
59 cov.save()
60 # Check if we have any coverage data
61 data = cov.get_data()
62 if data.measured_files():
63  cov.xml_report(outfile='{}')
64 )",
65  path.string()),
66  locals);
67 #endif
68 }
69 
70 std::tuple<std::vector<std::string>, std::vector<std::string>, std::string>
71 call_solve_linear_system(const std::vector<std::string>& eq_system,
72  const std::vector<std::string>& state_vars,
73  const std::set<std::string>& vars,
74  bool small_system,
75  bool elimination,
76  const std::string& tmp_unique_prefix,
77  const std::set<std::string>& function_calls) {
78  const auto locals = py::dict("eq_strings"_a = eq_system,
79  "state_vars"_a = state_vars,
80  "vars"_a = vars,
81  "small_system"_a = small_system,
82  "do_cse"_a = elimination,
83  "function_calls"_a = function_calls,
84  "tmp_unique_prefix"_a = tmp_unique_prefix);
85  std::string script = R"(
86 exception_message = ""
87 try:
88  solutions, new_local_vars = solve_lin_system(eq_strings,
89  state_vars,
90  vars,
91  function_calls,
92  tmp_unique_prefix,
93  small_system,
94  do_cse)
95 except Exception as e:
96  # if we fail, fail silently and return empty string
97  import traceback
98  solutions = [""]
99  new_local_vars = [""]
100  exception_message = traceback.format_exc()
101 )";
102  run_python_script(script, locals);
103  // returns a vector of solutions, i.e. new statements to add to block:
104  auto solutions = locals["solutions"].cast<std::vector<std::string>>();
105  // and a vector of new local variables that need to be declared in the block:
106  auto new_local_vars = locals["new_local_vars"].cast<std::vector<std::string>>();
107  // may also return a python exception message:
108  auto exception_message = locals["exception_message"].cast<std::string>();
109 
110  return {std::move(solutions), std::move(new_local_vars), std::move(exception_message)};
111 }
112 
113 
114 std::tuple<std::vector<std::string>, std::string> call_solve_nonlinear_system(
115  const std::vector<std::string>& eq_system,
116  const std::vector<std::string>& state_vars,
117  const std::set<std::string>& vars,
118  const std::set<std::string>& function_calls) {
119  const auto locals = py::dict("equation_strings"_a = eq_system,
120  "state_vars"_a = state_vars,
121  "vars"_a = vars,
122  "function_calls"_a = function_calls);
123  std::string script = R"(
124 exception_message = ""
125 try:
126  solutions = solve_non_lin_system(equation_strings,
127  state_vars,
128  vars,
129  function_calls)
130 except Exception as e:
131  # if we fail, fail silently and return empty string
132  import traceback
133  solutions = [""]
134  exception_message = traceback.format_exc()
135 )";
136 
137  run_python_script(script, locals);
138  // returns a vector of solutions, i.e. new statements to add to block:
139  auto solutions = locals["solutions"].cast<std::vector<std::string>>();
140  // may also return a python exception message:
141  auto exception_message = locals["exception_message"].cast<std::string>();
142 
143  return {std::move(solutions), std::move(exception_message)};
144 }
145 
146 
147 std::tuple<std::string, std::string> call_diffeq_solver(const std::string& node_as_nmodl,
148  const std::string& dt_var,
149  const std::set<std::string>& vars,
150  bool use_pade_approx,
151  const std::set<std::string>& function_calls,
152  const std::string& method) {
153  const auto locals = py::dict("equation_string"_a = node_as_nmodl,
154  "dt_var"_a = dt_var,
155  "vars"_a = vars,
156  "use_pade_approx"_a = use_pade_approx,
157  "function_calls"_a = function_calls);
158 
159  if (method == codegen::naming::EULER_METHOD) {
160  // replace x' = f(x) differential equation
161  // with forwards Euler timestep:
162  // x = x + f(x) * dt
163  std::string script = R"(
164 exception_message = ""
165 try:
166  solution = forwards_euler2c(equation_string, dt_var, vars, function_calls)
167 except Exception as e:
168  # if we fail, fail silently and return empty string
169  import traceback
170  solution = ""
171  exception_message = traceback.format_exc()
172 )";
173 
174  run_python_script(script, locals);
175  } else if (method == codegen::naming::CNEXP_METHOD) {
176  // replace x' = f(x) differential equation
177  // with analytic solution for x(t+dt) in terms of x(t)
178  // x = ...
179  std::string script = R"(
180 exception_message = ""
181 try:
182  solution = integrate2c(equation_string, dt_var, vars,
183  use_pade_approx)
184 except Exception as e:
185  # if we fail, fail silently and return empty string
186  import traceback
187  solution = ""
188  exception_message = traceback.format_exc()
189 )";
190 
191  run_python_script(script, locals);
192  } else {
193  // nothing to do, but the caller should know.
194  return {};
195  }
196  auto solution = locals["solution"].cast<std::string>();
197  auto exception_message = locals["exception_message"].cast<std::string>();
198 
199  return {std::move(solution), std::move(exception_message)};
200 }
201 
202 
203 std::tuple<std::string, std::string> call_analytic_diff(
204  const std::vector<std::string>& expressions,
205  const std::set<std::string>& used_names_in_block) {
206  auto locals = py::dict("expressions"_a = expressions, "vars"_a = used_names_in_block);
207  std::string script = R"(
208 exception_message = ""
209 try:
210  rhs = expressions[-1].split("=", 1)[1]
211  solution = differentiate2c(rhs,
212  "v",
213  vars,
214  expressions[:-1]
215  )
216 except Exception as e:
217  # if we fail, fail silently and return empty string
218  import traceback
219  solution = ""
220  exception_message = traceback.format_exc()
221 )";
222 
223  run_python_script(script, locals);
224 
225  auto solution = locals["solution"].cast<std::string>();
226  auto exception_message = locals["exception_message"].cast<std::string>();
227 
228  return {std::move(solution), std::move(exception_message)};
229 }
230 
231 std::tuple<std::string, std::string> call_diff2c(
232  const std::string& expression,
233  const std::pair<std::string, std::optional<int>>& variable,
234  const std::unordered_set<std::string>& indexed_vars) {
235  std::string statements;
236  // only indexed variables require special treatment
237  for (const auto& var: indexed_vars) {
238  statements += fmt::format("_allvars.append(sp.IndexedBase('{}', shape=[1]))\n", var);
239  }
240  auto [name, property] = variable;
241  if (property.has_value()) {
242  name = fmt::format("sp.IndexedBase('{}', shape=[1])", name);
243  statements += fmt::format("_allvars.append({})", name);
244  } else {
245  name = fmt::format("'{}'", name);
246  }
247  auto locals = py::dict("expression"_a = expression);
248  std::string script =
249  fmt::format(R"(
250 _allvars = []
251 {}
252 variable = {}
253 exception_message = ""
254 try:
255  solution = differentiate2c(expression,
256  variable,
257  _allvars,
258  )
259 except Exception as e:
260  # if we fail, fail silently and return empty string
261  solution = ""
262  exception_message = str(e)
263 )",
264  statements,
265  property.has_value() ? fmt::format("{}[{}]", name, property.value()) : name);
266 
267  run_python_script(script, locals);
268 
269  auto solution = locals["solution"].cast<std::string>();
270  auto exception_message = locals["exception_message"].cast<std::string>();
271 
272  return {std::move(solution), std::move(exception_message)};
273 }
274 
276  pybind11::initialize_interpreter(true);
277 }
278 
280  pybind11::finalize_interpreter();
281 }
282 
283 // Prevent mangling for easier `dlsym`.
284 extern "C" {
292  &call_diff2c};
293 }
294 }
295 
296 } // namespace pybind_wrappers
297 } // namespace nmodl
Common utility functions for file/dir manipulation.
std::string generate_random_string(const int len, UseNumbersInString use_numbers)
Generate random std::string of length len based on a uniform distribution.
double var(InputIterator begin, InputIterator end)
Definition: ivocvect.h:108
const char * name
Definition: init.cpp:16
void move(Item *q1, Item *q2, Item *q3)
Definition: list.cpp:200
static constexpr char CNEXP_METHOD[]
cnexp method in nmodl
static constexpr char EULER_METHOD[]
euler method in nmodl
std::tuple< std::vector< std::string >, std::string > call_solve_nonlinear_system(const std::vector< std::string > &eq_system, const std::vector< std::string > &state_vars, const std::set< std::string > &vars, const std::set< std::string > &function_calls)
Definition: wrapper.cpp:114
std::tuple< std::string, std::string > call_diff2c(const std::string &expression, const std::pair< std::string, std::optional< int >> &variable, const std::unordered_set< std::string > &indexed_vars)
Differentiates an expression with respect to a variable.
Definition: wrapper.cpp:231
void initialize_interpreter_func()
Definition: wrapper.cpp:275
void finalize_interpreter_func()
Definition: wrapper.cpp:279
std::tuple< std::string, std::string > call_analytic_diff(const std::vector< std::string > &expressions, const std::set< std::string > &used_names_in_block)
Definition: wrapper.cpp:203
static void run_python_script(const std::string &script, const py::dict &locals)
Definition: wrapper.cpp:35
std::tuple< std::string, std::string > call_diffeq_solver(const std::string &node_as_nmodl, const std::string &dt_var, const std::set< std::string > &vars, bool use_pade_approx, const std::set< std::string > &function_calls, const std::string &method)
Definition: wrapper.cpp:147
NMODL_EXPORT pybind_wrap_api nmodl_init_pybind_wrapper_api() noexcept
Definition: wrapper.cpp:285
std::tuple< std::vector< std::string >, std::vector< std::string >, std::string > call_solve_linear_system(const std::vector< std::string > &eq_system, const std::vector< std::string > &state_vars, const std::set< std::string > &vars, bool small_system, bool elimination, const std::string &tmp_unique_prefix, const std::set< std::string > &function_calls)
Definition: wrapper.cpp:71
encapsulates code generation backend implementations
Definition: ast_common.hpp:26
static char suffix[256]
Definition: nocpout.cpp:135
#define NMODL_EXPORT
Definition: wrapper.hpp:73