NEURON
mechanism.cpp
Go to the documentation of this file.
4 #include "neuron/model_data.hpp"
5 
6 #include <catch2/catch_test_macros.hpp>
7 
8 #include <array>
9 #include <iostream>
10 #include <numeric>
11 #include <optional>
12 #include <random>
13 #include <sstream>
14 #include <vector>
15 
16 using namespace neuron::container::Mechanism;
17 
18 TEST_CASE("SOA-backed Mechanism data structure", "[Neuron][data_structures][mechanism]") {
19  GIVEN("A mechanism with two copies of the same tagged variable") {
20  // foo is scalar, bar is an array of dimension bar_values.size()
21  constexpr std::array bar_values{7.0, 0.7, 0.9};
22  std::vector<Variable> field_info{{"foo", 1}, {"bar", bar_values.size()}};
23  auto const num_fields = field_info.size();
24  auto const foo_index = 0;
25  auto const bar_index = 1;
26  // Have to register the storage with neuron::model(), otherwise pretty-printing of data
27  // handles won't work
28  auto const mech_type = 0;
30  auto& mech_data = neuron::model().add_mechanism(mech_type, "test_mechanism", field_info);
31  // the top-level mechanism data structure can be pretty-printed
32  THEN("The mechanism data structure can be pretty-printed") {
33  std::ostringstream oss;
34  oss << mech_data;
35  REQUIRE(oss.str() == "test_mechanism::storage{type=0, 2 fields}");
36  }
37  REQUIRE(mech_data.get_tag<field::FloatingPoint>().num_variables() == num_fields);
38  WHEN("A row is added") {
39  owning_handle mech_instance{mech_data};
40  THEN("We cannot delete the mechanism type") {
41  REQUIRE_THROWS(neuron::model().delete_mechanism(mech_type));
42  }
43  THEN("Values can be read and written") {
44  constexpr auto field0_value = 42.0;
45  mech_instance.fpfield(foo_index) = field0_value;
46  for (auto i = 0; i < bar_values.size(); ++i) {
47  mech_instance.fpfield(bar_index, i) = bar_values[i];
48  }
49  REQUIRE_THROWS(mech_instance.fpfield(num_fields));
50  REQUIRE_THROWS(mech_instance.fpfield(bar_index, bar_values.size()));
51  REQUIRE(mech_instance.fpfield(foo_index) == field0_value);
52  for (auto i = 0; i < bar_values.size(); ++i) {
53  REQUIRE(mech_instance.fpfield(bar_index, i) == bar_values[i]);
54  }
55  REQUIRE_THROWS(mech_instance.fpfield(num_fields));
56 
57  REQUIRE(mech_instance.fpfield_dimension(foo_index) == 1);
58  REQUIRE(mech_instance.fpfield_dimension(bar_index) == bar_values.size());
59  REQUIRE_THROWS(mech_instance.fpfield_dimension(num_fields));
60 
61  auto bar_handle = mech_instance.fpfield_handle(bar_index);
62  REQUIRE(*bar_handle.next_array_element(2) == 0.9);
63  REQUIRE_THROWS(*bar_handle.next_array_element(bar_values.size()));
64 
65 
66  AND_THEN("Data handles give useful information when printed") {
67  auto const require_str = [&mech_instance](std::string_view ref, auto... args) {
68  auto const dh = mech_instance.fpfield_handle(args...);
69  std::ostringstream oss;
70  oss << dh;
71  REQUIRE(oss.str() == ref);
72  };
73  // Slightly worried that the val=xxx formatting isn't portable, in which case we
74  // could generate the reference strings using the values above.
75  require_str("data_handle<double>{cont=test_mechanism foo row=0/1 val=42}",
76  foo_index);
77  require_str("data_handle<double>{cont=test_mechanism bar[0/3] row=0/1 val=7}",
78  bar_index,
79  0);
80  require_str("data_handle<double>{cont=test_mechanism bar[1/3] row=0/1 val=0.7}",
81  bar_index,
82  1);
83  require_str("data_handle<double>{cont=test_mechanism bar[2/3] row=0/1 val=0.9}",
84  bar_index,
85  2);
86  // Also cover generic_data_handle printing
88  mech_instance.fpfield_handle(foo_index)};
89  std::ostringstream oss;
90  oss << gdh;
91  REQUIRE(oss.str() ==
92  "generic_data_handle{cont=test_mechanism foo row=0/1 type=double*}");
93  }
94  std::ostringstream actual;
95  actual << mech_instance;
96  REQUIRE(actual.str() ==
97  "test_mechanism{row=0/1 fpfield[0]{ 42 } fpfield[1]{ 7 0.7 0.9 }}");
98  }
99  }
100  WHEN("Many rows are added") {
101  std::vector<double> reference_field0, reference_field1_0, reference_field1_1;
102  constexpr auto num_instances = 10;
103  std::generate_n(std::back_inserter(reference_field0), num_instances, [i = 0]() mutable {
104  auto const x = i++;
105  return x * x;
106  });
107  std::generate_n(std::back_inserter(reference_field1_0),
108  num_instances,
109  [i = 0]() mutable {
110  auto const x = i++;
111  return x * x * x;
112  });
113  std::generate_n(std::back_inserter(reference_field1_1),
114  num_instances,
115  [i = 0]() mutable {
116  auto const x = i++;
117  return x * x * x * x;
118  });
119  std::vector<owning_handle> mech_instances{};
120  for (auto i = 0; i < num_instances; ++i) {
121  auto& mech = mech_instances.emplace_back(mech_data);
122  mech.fpfield(0) = reference_field0[i];
123  mech.fpfield(1, 0) = reference_field1_0[i];
124  mech.fpfield(1, 1) = reference_field1_1[i];
125  }
126  REQUIRE(mech_data.empty() == mech_instances.empty());
127  REQUIRE(mech_data.size() == mech_instances.size());
128  enum struct StorageCheck { Skip, Match, NotMatch };
129  const auto check_field_values = [&](StorageCheck storage_should_match) {
130  THEN("The correct values can be read back") {
131  std::vector<double> current_field0, current_field1_0, current_field1_1;
132  std::transform(mech_instances.begin(),
133  mech_instances.end(),
134  std::back_inserter(current_field0),
135  [](auto const& mech) { return mech.fpfield(0); });
136  std::transform(mech_instances.begin(),
137  mech_instances.end(),
138  std::back_inserter(current_field1_0),
139  [](auto const& mech) { return mech.fpfield(1, 0); });
140  std::transform(mech_instances.begin(),
141  mech_instances.end(),
142  std::back_inserter(current_field1_1),
143  [](auto const& mech) { return mech.fpfield(1, 1); });
144  REQUIRE(current_field0 == reference_field0);
145  REQUIRE(current_field1_0 == reference_field1_0);
146  REQUIRE(current_field1_1 == reference_field1_1);
147  auto const field_matches =
148  [&](auto const& reference, int index, int array_index = 0) {
149  for (auto i = 0; i < mech_data.size(); ++i) {
150  if (mech_data.fpfield(i, index, array_index) != reference[i]) {
151  return false;
152  }
153  }
154  return true;
155  };
156  if (storage_should_match == StorageCheck::Match) {
157  AND_THEN("The underlying storage matches the reference values") {
158  REQUIRE(field_matches(reference_field0, 0));
159  REQUIRE(field_matches(reference_field1_0, 1, 0));
160  REQUIRE(field_matches(reference_field1_1, 1, 1));
161  }
162  } else if (storage_should_match == StorageCheck::NotMatch) {
163  AND_THEN("The underlying storage no longer matches the reference values") {
164  REQUIRE_FALSE(field_matches(reference_field0, 0));
165  REQUIRE_FALSE(field_matches(reference_field1_0, 1, 0));
166  REQUIRE_FALSE(field_matches(reference_field1_1, 1, 1));
167  }
168  }
169  }
170  };
171  check_field_values(StorageCheck::Match);
172  AND_WHEN("The underlying storage is permuted") {
173  std::vector<std::size_t> perm_vector(mech_instances.size());
174  std::iota(perm_vector.begin(), perm_vector.end(), 0);
175  std::mt19937 g{42};
176  std::shuffle(perm_vector.begin(), perm_vector.end(), g);
177  mech_data.apply_reverse_permutation(std::move(perm_vector));
178  check_field_values(StorageCheck::Skip);
179  }
180  auto const apply_to_all = [&](auto callable) {
181  auto const impl = [](auto callable, auto&... vecs) { (callable(vecs), ...); };
182  impl(std::move(callable),
183  mech_instances,
184  reference_field0,
185  reference_field1_0,
186  reference_field1_1);
187  };
188  AND_WHEN("An element is deleted") {
189  apply_to_all([](auto& vec) { vec.erase(vec.begin()); });
190  check_field_values(StorageCheck::Skip);
191  }
192  }
193  WHEN("We want to manipulate other mechanism data") {
194  THEN("We cannot readd the same type") {
195  REQUIRE_THROWS(
196  neuron::model().add_mechanism(mech_type, "test_mechanism_2", field_info));
197  }
198  THEN("We cannot access a non-existing mechanism") {
199  auto const non_existing_mech_type = 313;
200  REQUIRE_THROWS(neuron::model().mechanism_data(non_existing_mech_type));
201  }
202  THEN("We cannot get data for a null mechanism") {
203  auto const null_mech_type = 345;
204  neuron::model().add_mechanism(null_mech_type, "null_mechanism");
205  neuron::model().delete_mechanism(null_mech_type);
206  REQUIRE_THROWS(neuron::model().mechanism_data(null_mech_type));
207  }
208  }
209  }
210  GIVEN("A mechanism type that gets deleted") {
211  std::vector<Variable> field_info{{"foo", 1}}; // one scalar variable
212  constexpr auto foo_index = 0;
213  constexpr auto foo_value = 29.0;
214  // Register the storage with neuron::model() because we want to cover codepaths in the
215  // pretty-printing
216  auto const mech_type = 0;
218  auto& mech_data = neuron::model().add_mechanism(mech_type, "test_mechanism", field_info);
219  REQUIRE(mech_data.get_tag<field::FloatingPoint>().num_variables() == 1);
220  WHEN("A row is added and we take a handle to its value") {
221  std::optional<owning_handle> instance{std::in_place, mech_data};
222  instance->fpfield(foo_index) = foo_value;
223  auto foo_handle = instance->fpfield_handle(foo_index);
224  auto generic_foo = neuron::container::generic_data_handle{foo_handle};
225  THEN("The handle yields the expected value") {
226  REQUIRE(*foo_handle == foo_value);
227  }
228  AND_WHEN("The row is deleted again") {
229  instance.reset();
230  THEN("We can still print the handle") {
231  std::ostringstream oss;
232  oss << foo_handle;
233  REQUIRE(oss.str() == "data_handle<double>{cont=test_mechanism foo died/0}");
234  }
235  AND_WHEN("The mechanism type is also deleted") {
237  THEN("We can still print the handle, following an unusual codepath") {
238  {
239  std::ostringstream oss;
240  oss << foo_handle;
241  REQUIRE(oss.str() == "data_handle<double>{cont=unknown died/unknown}");
242  }
243  {
244  std::ostringstream oss;
245  oss << generic_foo;
246  REQUIRE(oss.str() ==
247  "generic_data_handle{cont=unknown died/unknown type=double*}");
248  }
249  }
250  }
251  }
252  }
253  }
254 }
255 
256 TEST_CASE("Model::is_valid_mechanism", "[Neuron][data_structures]") {
257  // Since `neuron::model` is a global we we've no clue what state it's in.
258  // Hence we delete what we want to use and remember that we have to assume
259  // there might be other mechanisms present that we shall ignore.
260 
261  auto& model = neuron::model();
262  std::vector<Variable> field_info{{"foo", 1}}; // just a dummy value.
263 
265  model.add_mechanism(0, "zero", field_info);
266 
268  model.add_mechanism(1, "one", field_info);
269 
271  model.add_mechanism(2, "two", field_info);
272 
274 
278 }
#define i
Definition: md1redef.h:19
TEST_CASE("SOA-backed Mechanism data structure", "[Neuron][data_structures][mechanism]")
Definition: mechanism.cpp:18
void move(Item *q1, Item *q2, Item *q3)
Definition: list.cpp:200
Model & model()
Access the global Model instance.
Definition: model_data.hpp:206
data_handle< T > transform(data_handle< T > handle, Transform type)
Definition: node.cpp:32
mech_type
short index
Definition: cabvars.h:11
#define CHECK(name)
Definition: init.cpp:97
static double ref(void *v)
Definition: ocbox.cpp:381
void delete_mechanism(int type)
Destroy the structure holding the data of a particular mechanism.
Definition: model_data.hpp:79
container::Mechanism::storage & add_mechanism(int type, Args &&... args)
Create a structure to hold the data of a new Mechanism.
Definition: model_data.hpp:60
bool is_valid_mechanism(int type) const
Definition: model_data.hpp:107
Base class defining the public API of Mechanism handles.
Definition: mechanism.hpp:71
Non-template stable handle to a generic value.