NEURON
matexp.cpp
Go to the documentation of this file.
1 /*
2  * Copyright 2025 David McDougall
3  * See the top-level LICENSE file for details.
4  *
5  * SPDX-License-Identifier: BSD-3-Clause
6  */
7 
8 #include <catch2/catch_test_macros.hpp>
9 
10 #include "ast/all.hpp"
11 #include "parser/nmodl_driver.hpp"
15 #include "utils/test_utils.hpp"
16 
18 
19 //=============================================================================
20 // Tests for solver method "matexp"
21 //=============================================================================
22 
23 std::string run_matexp_visitor(const std::string& text, bool verbatim) {
24  using namespace nmodl;
25  using namespace visitor;
28 
29  NmodlDriver driver;
30  const auto& ast = driver.parse_string(text);
31 
32  SymtabVisitor().visit_program(*ast);
33 
34  MatexpVisitor().visit_program(*ast);
35 
36  std::string nmodl;
37  if (verbatim) {
38  nmodl = to_nmodl(ast);
39  } else {
40  nmodl = to_nmodl(ast, {AstNodeType::VERBATIM});
41  }
42 
43  return nmodl;
44 }
45 
46 SCENARIO("Solve a KINETIC block using the matexp method", "[visitor][matexp]") {
47  GIVEN("KINETIC block, to be solved in initial and breakpoint blocks") {
48  std::string input_nmodl = R"(
49  INITIAL {
50  SOLVE test_kin STEADYSTATE matexp
51  }
52 
53  BREAKPOINT {
54  SOLVE test_kin METHOD matexp
55  }
56 
57  PARAMETER {
58  a = 0.654
59  b = 0.129
60  }
61 
62  STATE {
63  x
64  y
65  }
66 
67  KINETIC test_kin {
68  ~ x <-> y (a, b)
69  CONSERVE x + y = 3.14159
70  })";
71  std::string expect_output = R"(
72  INITIAL {
73  MATEXP_SOLVE (1) {
74  nmodl_eigen_j[0] = nmodl_eigen_j[0]-(a)*nmodl_dt
75  nmodl_eigen_j[1] = nmodl_eigen_j[1]+(a)*nmodl_dt
76  nmodl_eigen_j[3] = nmodl_eigen_j[3]-(b)*nmodl_dt
77  nmodl_eigen_j[2] = nmodl_eigen_j[2]+(b)*nmodl_dt
78  } CONSERVE(CONSERVE x+y = 3.14159)
79  }
80 
81  BREAKPOINT {
82  SOLVE test_kin METHOD matexp
83  }
84 
85  PARAMETER {
86  a = 0.654
87  b = 0.129
88  }
89 
90  STATE {
91  x
92  y
93  }
94 
95  MATEXP_SOLVE (0) {
96  nmodl_eigen_j[0] = nmodl_eigen_j[0]-(a)*nmodl_dt
97  nmodl_eigen_j[1] = nmodl_eigen_j[1]+(a)*nmodl_dt
98  nmodl_eigen_j[3] = nmodl_eigen_j[3]-(b)*nmodl_dt
99  nmodl_eigen_j[2] = nmodl_eigen_j[2]+(b)*nmodl_dt
100  } CONSERVE(CONSERVE x+y = 3.14159)
101  )";
102  THEN("Replace the kinetic block with its solution in a procedure") {
103  auto result = run_matexp_visitor(input_nmodl, true);
104  REQUIRE(reindent_text(expect_output) == reindent_text(result));
105  }
106  }
107 }
108 
109 SCENARIO("Solve multiple CONSERVE using the matexp method", "[visitor][matexp]") {
110  GIVEN("KINETIC block with multiple CONSERVE statements") {
111  std::string input_nmodl = R"(
112  INITIAL {
113  SOLVE test_kin STEADYSTATE matexp
114  }
115 
116  BREAKPOINT {
117  SOLVE test_kin METHOD matexp
118  }
119 
120  STATE {
121  x
122  y
123  z
124  }
125 
126  KINETIC test_kin {
127  CONSERVE x + y = 3.14159
128  CONSERVE z = 42
129  })";
130  std::string expect_output = R"(
131  INITIAL {
132  MATEXP_SOLVE (1) {
133  } CONSERVE(CONSERVE x+y = 3.14159, CONSERVE z = 42)
134  }
135 
136  BREAKPOINT {
137  SOLVE test_kin METHOD matexp
138  }
139 
140  STATE {
141  x
142  y
143  z
144  }
145 
146  MATEXP_SOLVE (0) {
147  } CONSERVE(CONSERVE x+y = 3.14159, CONSERVE z = 42)
148  )";
149  THEN("Accept multiple CONSERVE statements as valid") {
150  auto result = run_matexp_visitor(input_nmodl, true);
151  REQUIRE(reindent_text(expect_output) == reindent_text(result));
152  }
153  }
154 }
155 
156 SCENARIO("Test exponential decay using the matexp method", "[visitor][matexp]") {
157  GIVEN("KINETIC block, to be solved in initial and breakpoint blocks") {
158  std::string input_nmodl = R"(
159  INITIAL {
160  SOLVE test_kin STEADYSTATE matexp
161  }
162 
163  BREAKPOINT {
164  SOLVE test_kin METHOD matexp
165  }
166 
167  PARAMETER {
168  a = 0.129
169  }
170 
171  STATE {
172  x
173  }
174 
175  KINETIC test_kin {
176  ~ x -> (a)
177  })";
178  std::string expect_output = R"(
179  INITIAL {
180  MATEXP_SOLVE (1) {
181  nmodl_eigen_j[0] = nmodl_eigen_j[0]-(a)*nmodl_dt
182  }
183  }
184 
185  BREAKPOINT {
186  SOLVE test_kin METHOD matexp
187  }
188 
189  PARAMETER {
190  a = 0.129
191  }
192 
193  STATE {
194  x
195  }
196 
197  MATEXP_SOLVE (0) {
198  nmodl_eigen_j[0] = nmodl_eigen_j[0]-(a)*nmodl_dt
199  }
200  )";
201  THEN("Replace the kinetic block with its solution in a procedure") {
202  auto result = run_matexp_visitor(input_nmodl, true);
203  REQUIRE(reindent_text(expect_output) == reindent_text(result));
204  }
205  }
206 }
207 
208 SCENARIO("Mix matexp and sparse solver methods", "[visitor][matexp]") {
209  GIVEN("KINETIC block, to be solved by multiple methods") {
210  std::string input_nmodl = R"(
211  INITIAL {
212  SOLVE test_kin STEADYSTATE sparse
213  }
214 
215  BREAKPOINT {
216  SOLVE test_kin METHOD matexp
217  }
218 
219  STATE {
220  x
221  y
222  }
223 
224  KINETIC test_kin {
225  ~ x <-> y (a, b)
226  })";
227  std::string expect_output = R"(
228  INITIAL {
229  SOLVE test_kin STEADYSTATE sparse
230  }
231 
232  BREAKPOINT {
233  SOLVE test_kin METHOD matexp
234  }
235 
236  STATE {
237  x
238  y
239  }
240 
241  KINETIC test_kin {
242  ~ x <-> y (a, b)
243  }
244 
245  MATEXP_SOLVE (0) {
246  nmodl_eigen_j[0] = nmodl_eigen_j[0]-(a)*nmodl_dt
247  nmodl_eigen_j[1] = nmodl_eigen_j[1]+(a)*nmodl_dt
248  nmodl_eigen_j[3] = nmodl_eigen_j[3]-(b)*nmodl_dt
249  nmodl_eigen_j[2] = nmodl_eigen_j[2]+(b)*nmodl_dt
250  }
251  )";
252  THEN(
253  "The original KINETIC block is kept unchanged,"
254  "its solution is inserted into a new PROCEDURE block") {
255  auto result = run_matexp_visitor(input_nmodl, true);
256  REQUIRE(reindent_text(expect_output) == reindent_text(result));
257  }
258  }
259 }
260 
261 SCENARIO("Solve multiple blocks with matexp", "[visitor][matexp]") {
262  GIVEN("Mutliple KINETIC block to be solved") {
263  std::string input_nmodl = R"(
264  INITIAL {
265  SOLVE test_kin_1 STEADYSTATE matexp
266  SOLVE test_kin_2 STEADYSTATE matexp
267  }
268 
269  BREAKPOINT {
270  SOLVE test_kin_1 METHOD matexp
271  SOLVE test_kin_2 METHOD matexp
272  }
273 
274  STATE {
275  a
276  b
277  x
278  y
279  }
280 
281  KINETIC test_kin_1 {
282  ~ a <-> b (1.1, 2.2)
283  }
284 
285  KINETIC test_kin_2 {
286  ~ x <-> y (3.3, 4.4)
287  })";
288  std::string expect_output = R"(
289  INITIAL {
290  MATEXP_SOLVE (1) {
291  nmodl_eigen_j[0] = nmodl_eigen_j[0]-(1.1)*nmodl_dt
292  nmodl_eigen_j[1] = nmodl_eigen_j[1]+(1.1)*nmodl_dt
293  nmodl_eigen_j[5] = nmodl_eigen_j[5]-(2.2)*nmodl_dt
294  nmodl_eigen_j[4] = nmodl_eigen_j[4]+(2.2)*nmodl_dt
295  }
296  MATEXP_SOLVE (1) {
297  nmodl_eigen_j[10] = nmodl_eigen_j[10]-(3.3)*nmodl_dt
298  nmodl_eigen_j[11] = nmodl_eigen_j[11]+(3.3)*nmodl_dt
299  nmodl_eigen_j[15] = nmodl_eigen_j[15]-(4.4)*nmodl_dt
300  nmodl_eigen_j[14] = nmodl_eigen_j[14]+(4.4)*nmodl_dt
301  }
302  }
303 
304  BREAKPOINT {
305  SOLVE test_kin_1 METHOD matexp
306  SOLVE test_kin_2 METHOD matexp
307  }
308 
309  STATE {
310  a
311  b
312  x
313  y
314  }
315 
316  MATEXP_SOLVE (0) {
317  nmodl_eigen_j[0] = nmodl_eigen_j[0]-(1.1)*nmodl_dt
318  nmodl_eigen_j[1] = nmodl_eigen_j[1]+(1.1)*nmodl_dt
319  nmodl_eigen_j[5] = nmodl_eigen_j[5]-(2.2)*nmodl_dt
320  nmodl_eigen_j[4] = nmodl_eigen_j[4]+(2.2)*nmodl_dt
321  }
322 
323  MATEXP_SOLVE (0) {
324  nmodl_eigen_j[10] = nmodl_eigen_j[10]-(3.3)*nmodl_dt
325  nmodl_eigen_j[11] = nmodl_eigen_j[11]+(3.3)*nmodl_dt
326  nmodl_eigen_j[15] = nmodl_eigen_j[15]-(4.4)*nmodl_dt
327  nmodl_eigen_j[14] = nmodl_eigen_j[14]+(4.4)*nmodl_dt
328  }
329  )";
330  THEN("All KINETIC block are solved") {
331  auto result = run_matexp_visitor(input_nmodl, true);
332  REQUIRE(reindent_text(expect_output) == reindent_text(result));
333  }
334  }
335 }
336 
337 SCENARIO("Give non-linear equations to the matexp solver", "[visitor][matexp]") {
338  GIVEN("KINETIC block with x + y <-> z reaction statement") {
339  std::string input_nmodl = R"(
340  BREAKPOINT {
341  SOLVE test_kin METHOD matexp
342  }
343  STATE {
344  x
345  y
346  z
347  }
348  KINETIC test_kin {
349  ~ x + y <-> z (123, 456)
350  })";
351  THEN("Raise an exception") {
352  REQUIRE_THROWS_AS(run_matexp_visitor(input_nmodl, true), std::invalid_argument);
353  }
354  }
355  GIVEN("KINETIC block with 2 x <-> y reaction statement") {
356  std::string input_nmodl = R"(
357  BREAKPOINT {
358  SOLVE test_kin METHOD matexp
359  }
360  STATE {
361  x
362  y
363  }
364  KINETIC test_kin {
365  ~ 2 x <-> y (123, 456)
366  })";
367  THEN("Raise an exception") {
368  REQUIRE_THROWS_AS(run_matexp_visitor(input_nmodl, true), std::invalid_argument);
369  }
370  }
371  GIVEN("KINETIC block with 1 x <-> y reaction statement") {
372  std::string input_nmodl = R"(
373  BREAKPOINT {
374  SOLVE test_kin METHOD matexp
375  }
376  STATE {
377  x
378  y
379  }
380  KINETIC test_kin {
381  ~ 1 x <-> y (123, 456)
382  })";
383  THEN("Equation is valid, do not raise exception") {
384  REQUIRE_NOTHROW(run_matexp_visitor(input_nmodl, true));
385  }
386  }
387  GIVEN("KINETIC block with << reaction statement") {
388  std::string input_nmodl = R"(
389  BREAKPOINT {
390  SOLVE test_kin METHOD matexp
391  }
392  STATE {
393  x
394  }
395  KINETIC test_kin {
396  ~ x << (123)
397  })";
398  THEN("Raise an exception") {
399  REQUIRE_THROWS_AS(run_matexp_visitor(input_nmodl, true), std::invalid_argument);
400  }
401  }
402  GIVEN("KINETIC block with -> reaction statement") {
403  std::string input_nmodl = R"(
404  BREAKPOINT {
405  SOLVE test_kin METHOD matexp
406  }
407  STATE {
408  x
409  }
410  KINETIC test_kin {
411  ~ x -> (123)
412  })";
413  THEN("Equation is valid, do not raise exception") {
414  REQUIRE_NOTHROW(run_matexp_visitor(input_nmodl, true));
415  }
416  }
417  GIVEN("KINETIC block with invalid state variable") {
418  std::string input_nmodl = R"(
419  BREAKPOINT {
420  SOLVE test_kin METHOD matexp
421  }
422  STATE {
423  x
424  }
425  KINETIC test_kin {
426  ~ y -> (123)
427  })";
428  THEN("Raise an exception") {
429  REQUIRE_THROWS_AS(run_matexp_visitor(input_nmodl, true), std::invalid_argument);
430  }
431  }
432  GIVEN("non-linear CONSERVE statement") {
433  std::string input_nmodl = R"(
434  BREAKPOINT {
435  SOLVE test_kin METHOD matexp
436  }
437  STATE {
438  x
439  }
440  KINETIC test_kin {
441  CONSERVE x + x = 1
442  })";
443  THEN("Raise an exception") {
444  REQUIRE_THROWS_AS(run_matexp_visitor(input_nmodl, true), std::invalid_argument);
445  }
446  }
447  GIVEN("CONSERVE statement with derivatives") {
448  std::string input_nmodl = R"(
449  BREAKPOINT {
450  SOLVE test_kin METHOD matexp
451  }
452  STATE {
453  x
454  }
455  KINETIC test_kin {
456  CONSERVE x' + y' = 0
457  })";
458  THEN("Raise an exception") {
459  REQUIRE_THROWS_AS(run_matexp_visitor(input_nmodl, true), std::invalid_argument);
460  }
461  }
462 }
Auto generated AST classes declaration.
Class that binds all pieces together for parsing nmodl file.
AstNodeType
Enum type for every AST node type.
Definition: ast_decl.hpp:167
bool parse_string(const std::string &input)
parser Units provided as string (used for testing)
Definition: unit_driver.cpp:40
std::string run_matexp_visitor(const std::string &text, bool verbatim)
Definition: matexp.cpp:23
SCENARIO("Solve a KINETIC block using the matexp method", "[visitor][matexp]")
Definition: matexp.cpp:46
Visitor used for generating the necessary AST nodes for matexp solver.
std::string reindent_text(const std::string &text, int indent_level)
Reindent nmodl text for text-to-text comparison.
Definition: test_utils.cpp:55
encapsulates code generation backend implementations
Definition: ast_common.hpp:26
std::string to_nmodl(const ast::Ast &node, const std::set< ast::AstNodeType > &exclude_types)
Given AST node, return the NMODL string representation.
#define text
Definition: plot.cpp:60
THIS FILE IS GENERATED AT BUILD TIME AND SHALL NOT BE EDITED.
nmodl::parser::UnitDriver driver
Definition: parser.cpp:28
Utility functions for visitors implementation.