NEURON
soa_container.hpp
Go to the documentation of this file.
1 #pragma once
2 #include "backtrace_utils.h"
3 #include "memory_usage.hpp"
7 
8 #include <algorithm> // std::transform
9 #include <cstddef>
10 #include <functional>
11 #include <limits>
12 #include <mutex>
13 #include <string_view>
14 #include <type_traits>
15 #include <utility>
16 #include <vector>
17 
18 namespace neuron::container {
19 namespace detail {
20 template <typename T, typename... Ts>
21 inline constexpr bool type_in_pack_v = std::disjunction_v<std::is_same<T, Ts>...>;
22 // https://stackoverflow.com/a/67106430
23 template <typename T, typename... Types>
24 inline constexpr bool are_types_unique_v =
25  (!std::is_same_v<T, Types> && ...) && are_types_unique_v<Types...>;
26 template <typename T>
27 inline constexpr bool are_types_unique_v<T> = true;
28 // https://stackoverflow.com/a/18063608
29 template <typename T, typename Tuple>
31 template <typename T, typename... Ts>
32 struct index_of_type_helper<T, std::tuple<T, Ts...>> {
33  static constexpr std::size_t value = 0;
34 };
35 template <typename T, typename U, typename... Ts>
36 struct index_of_type_helper<T, std::tuple<U, Ts...>> {
37  static constexpr std::size_t value = 1 + index_of_type_helper<T, std::tuple<Ts...>>::value;
38 };
39 template <typename T, typename... Ts>
40 inline constexpr std::size_t index_of_type_v = []() {
41  constexpr bool Ts_are_unique = are_types_unique_v<Ts...>;
42  constexpr bool T_is_in_Ts = type_in_pack_v<T, Ts...>;
43  static_assert(Ts_are_unique,
44  "index_of_type_v<T, Ts...> assumes there are no duplicates in Ts...");
45  static_assert(T_is_in_Ts, "index_of_type_v<T, Ts...> assumes that T occurs in Ts...");
46  // make the error message better by avoiding instantiating index_of_type_helper if the
47  // assertions fail
48  if constexpr (Ts_are_unique && T_is_in_Ts) {
49  return index_of_type_helper<T, std::tuple<Ts...>>::value;
50  } else {
51  return std::numeric_limits<std::size_t>::max(); // unreachable without hitting
52  // static_assert
53  }
54 }();
55 
56 // Detect if a type T has a non-static member function called default_value
57 template <typename T, typename = void>
58 inline constexpr bool has_default_value_v = false;
59 template <typename T>
60 inline constexpr bool
61  has_default_value_v<T, std::void_t<decltype(std::declval<T>().default_value())>> = true;
62 
63 // Get the array dimension for a given field within a given tag, or 1 if the array_dimension
64 // function is not defined in the tag type
65 template <typename T>
66 auto get_array_dimension(T const& t, std::nullptr_t /* higher precedence than the fallback case */)
67  -> decltype(t.array_dimension(), 0) {
68  return t.array_dimension();
69 }
70 template <typename T>
71 auto get_array_dimension(T const& t, int i) -> decltype(t.array_dimension(i), 0) {
72  return t.array_dimension(i);
73 }
74 template <typename T>
75 int get_array_dimension(T const&, ...) {
76  return 1;
77 }
78 
79 // Detect if a type T has a non-static member function called num_variables().
80 template <typename T, typename = void>
81 struct has_num_variables: std::false_type {};
82 template <typename T>
83 struct has_num_variables<T, std::void_t<decltype(std::declval<T>().num_variables())>>
84  : std::true_type {};
85 template <typename T>
87 
88 template <class T>
89 size_t get_num_variables(T const& t) {
90  if constexpr (has_num_variables_v<T>) {
91  return t.num_variables();
92  } else {
93  return 1;
94  }
95 }
96 
97 // Get the value of a static member variable called optional, or false if it doesn't exist.
98 template <typename T, typename = void>
99 struct optional: std::false_type {};
100 template <typename T>
101 struct optional<T, std::void_t<decltype(T::optional)>> {
102  constexpr static bool value = T::optional;
103 };
104 template <typename T>
105 inline constexpr bool optional_v = optional<T>::value;
106 
107 enum struct FieldImplementation {
108  AlwaysSingle, // field always exists -> std::vector<T>
109  OptionalSingle, // field exists 0 or 1 time -> std::vector<T> that might be skipped
110  RuntimeVariable // field is duplicated a number of times that is set at runtime ->
111  // std::vector<std::vector<T>>
112 };
113 
114 template <typename Tag>
116  (has_num_variables_v<Tag> ? FieldImplementation::RuntimeVariable
117  : (optional_v<Tag> ? FieldImplementation::OptionalSingle
119 
120 // Get a name for a given field within a given tag
121 template <typename Tag>
122 auto get_name_impl(Tag const& tag, int field_index, std::nullptr_t)
123  -> decltype(static_cast<void>(tag.name(field_index)), std::string()) {
124  return tag.name(field_index);
125 }
126 
127 template <typename Tag>
128 std::string get_name_impl(Tag const& tag, int field_index, ...) {
129  auto ret = cxx_demangle(typeid(Tag).name());
130  if (field_index >= 0) {
131  ret.append(1, '#');
132  ret.append(std::to_string(field_index));
133  }
134  constexpr std::string_view prefix{"neuron::container::"};
135  if (std::string_view{ret}.substr(0, prefix.size()) == prefix) {
136  ret.erase(0, prefix.size());
137  }
138  return ret;
139 }
140 
141 /**
142  * @brief Get the nicest available name for the field_index-th instance of Tag.
143  *
144  * This should elegantly handle field_index == -1 (=> the tag doesn't get have num_variables()) and
145  * field_index being out of range.
146  */
147 template <typename Tag>
148 auto get_name(Tag const& tag, int field_index) {
149  if constexpr (has_num_variables_v<Tag>) {
150  if (field_index >= 0 && field_index < tag.num_variables()) {
151  // use tag.name(field_index) if it's available, otherwise fall back
152  return get_name_impl(tag, field_index, nullptr);
153  }
154  }
155  // no num_variables() or invalid field_index, use the fallback
156  return get_name_impl(tag, field_index, 1 /* does not match nullptr */);
157 }
158 
161 };
162 
163 /**
164  * @brief Check if the given range is a permutation of the first N integers.
165  * @return true if the permutation is trivial, false otherwise.
166  *
167  * A trivial permutation is one where i == range[i] for all i. An empty range
168  * is classed as trivial.
169  */
170 template <typename Rng>
171 bool check_permutation_vector(Rng const& range, std::size_t size) {
172  if (range.size() != size) {
173  throw std::runtime_error("invalid permutation vector: wrong size");
174  }
175  bool trivial{true};
176  std::vector<bool> seen(size, false);
177  for (auto i = 0ul; i < size; ++i) {
178  auto const val = range[i];
179  trivial = trivial && (i == val);
180  if (!(val >= 0 && val < size)) {
181  throw std::runtime_error("invalid permutation vector: value out of range");
182  }
183  if (seen[val]) {
184  throw std::runtime_error("invalid permutation vector: repeated value " +
185  std::to_string(val));
186  }
187  seen[val] = true;
188  }
189  return trivial;
190 }
191 
192 enum struct may_cause_reallocation { Yes, No };
193 
194 /** @brief Defer deleting pointers to deallocated memory.
195  *
196  * The address of a pointer to the underlying storage of `field_data` can
197  * escape the container. When deallocating the container the memory is
198  * deallocated but the pointer to the storage location is "leaked" into this
199  * vector.
200  */
201 extern std::vector<void*>* defer_delete_storage;
203 
204 /**
205  * @brief Storage for safe deletion of soa<...> containers.
206  *
207  * This is intended to prevent deleting an instance of a soa<...>-based container from invalidating
208  * any existing data handles, by keeping certain (small) values alive. Deleting these containers is
209  * probably not common (e.g. deleting a mechanism type), and only small bookkeeping-related values
210  * have to be kept alive. Generally defer_delete_storage is non-null for the lifetime of the top
211  * level Model structure, and the Model destructor deallocates (using delete[]) the pointers that
212  * are stored inside defer_delete_storage.
213  */
214 template <typename T>
215 void defer_delete(std::unique_ptr<T[]> data) {
216  static_assert(std::is_trivially_destructible_v<T>, "defer_delete does not call destructors");
217  if (data && defer_delete_storage) {
218  defer_delete_storage->push_back(data.release());
219  }
220 }
221 
222 template <typename Tag, FieldImplementation impl>
223 struct field_data {
224  static_assert(impl == FieldImplementation::AlwaysSingle ||
226  using data_type = typename Tag::type;
227  static_assert(!has_num_variables_v<Tag>);
229  : m_tag{std::move(tag)}
231  if constexpr (impl == FieldImplementation::AlwaysSingle) {
232  m_data_ptr = std::make_unique<data_type*[]>(1);
233  }
234  }
235 
237  // An unknown number of data_handle<T> in the wild may be holding references to m_data_ptr
239  }
240 
241  /**
242  * @brief Return a reference to the tag instance.
243  */
244  Tag const& tag() const {
245  return m_tag;
246  }
247 
248  template <may_cause_reallocation might_reallocate, typename Callable>
249  Callable for_each_vector(Callable callable) {
250  if constexpr (impl == FieldImplementation::OptionalSingle) {
251  if (!m_data_ptr) {
252  // inactive, optional field
253  return callable;
254  }
255  }
256  callable(m_tag, m_storage, -1, m_array_dim);
257  if constexpr (might_reallocate == may_cause_reallocation::Yes) {
258  m_data_ptr[0] = m_storage.data();
259  }
260 
261  return callable;
262  }
263 
264  template <typename Callable>
265  Callable for_each_vector(Callable callable) const {
266  if constexpr (impl == FieldImplementation::OptionalSingle) {
267  if (!m_data_ptr) {
268  // inactive, optional field
269  return callable;
270  }
271  }
272  callable(m_tag, m_storage, -1, m_array_dim);
273 
274  return callable;
275  }
276 
277  [[nodiscard]] bool active() const {
278  static_assert(impl == FieldImplementation::OptionalSingle);
279  return bool{m_data_ptr};
280  }
281 
282  void set_active(bool enable, std::size_t size) {
283  static_assert(impl == FieldImplementation::OptionalSingle);
284  if (enable == active()) {
285  return;
286  }
287  if (enable) {
288  // make sure the storage is allocated + the right size + full of default values
289  assert(m_storage.empty()); // it should be starting off empty
290  if constexpr (has_default_value_v<Tag>) {
291  m_storage.resize(size * m_array_dim, m_tag.default_value());
292  } else {
293  m_storage.resize(size * m_array_dim);
294  }
295  m_data_ptr = std::make_unique<data_type*[]>(1);
296  m_data_ptr[0] = m_storage.data();
297  } else {
298  // clear + free the storage
299  m_storage.clear();
300  m_storage.shrink_to_fit();
301  // data handles may be holding pointers to m_data_ptr (which is the reason for the
302  // deferred deletion); signal to them that they are no longer valid by writing nullptr
303  // here
304  m_data_ptr[0] = nullptr;
306  }
307  }
308 
309  [[nodiscard]] data_type* const* data_ptrs() const {
310  return m_data_ptr.get();
311  }
312 
313  [[nodiscard]] int const* array_dims() const {
314  return &m_array_dim;
315  }
316 
317  [[nodiscard]] int const* array_dim_prefix_sums() const {
318  return &m_array_dim;
319  }
320 
321  private:
322  /**
323  * @brief Tag type instance.
324  *
325  * An instance of @c soa contains an instance of @c field_data for each tag type in its @c
326  * Tags... pack. The instance of the tag type contains the metadata about the field it
327  * represents, and @c field_data adds the actual data for that field. For example, with @c Tag =
328  * @c Node::field::Voltage, which represents the voltage in a given Node, @c m_tag is just an
329  * empty type that defines the @c data_type and default value of voltages.
330  */
331  Tag m_tag;
332 
333  /**
334  * @brief Storage for the data associated with @c Tag.
335  *
336  * This is one of the "large" data arrays holding the model data. Because this specialisation of
337  * @c field_data is for @c Tag types that @b don't have @c num_variables() members, such as @c
338  * Node::field::Voltage, there is exactly one vector per instance of @c field_data. Because the
339  * fields in @c Node::storage all have array dimension 1, in that case the size of this vector
340  * is the number of Node instances in the program.
341  */
342  std::vector<data_type> m_storage;
343 
344  /**
345  * @brief Storage where we maintain an up-to-date cache of @c m_storage.data().
346  * @invariant @c m_storage.data() is equal to @c m_data_ptr.
347  * @see field_data<Tag, true>::m_data_ptrs %for the motivation.
348  *
349  * This is declared as an array (of size 1) to simplify the implementation of defer_delete.
350  * For FieldImplementation::OptionalSingle then whether or not this is null encodes whether
351  * or not the field is active. For FieldImplementation::AlwaysSingle it is never null.
352  */
353  std::unique_ptr<data_type*[]> m_data_ptr;
354 
355  /**
356  * @brief Array dimension of the data associated with @c Tag.
357  * @invariant @c m_array_dim is equal to @c m_tag.array_dimension(), if that function exists,
358  * or 1.
359  * @see field_data<Tag, true>::m_array_dims %for the motivation.
360  */
362 };
363 
364 /**
365  * @brief Storage manager for a tag type that implements num_variables().
366  *
367  * An illustrative example is that this is responsible for the storage associated with floating
368  * point mechanism data, where the number of fields is set at runtime via num_variables.
369  *
370  * As well as owning the actual storage containers, this type maintains two spans of values that
371  * can be used by other types, in particular neuron::cache::MechanismRange:
372  * - array_dims() returns a pointer to the first element of a num_variables()-sized range holding
373  * the array dimensions of the variables.
374  * - array_dim_prefix_sums() returns a pointer to the first element of a num_variables()-sized
375  * range holding the prefix sum over the array dimensions (i.e. if array_dims() returns [1, 2, 1]
376  * then array_dim_prefix_sums() returns [1, 3, 4]).
377  * - data_ptrs() returns a pointer to the first element of a num_variables()-sized range holding
378  * pointers to the start of the storage associated with each variable (i.e. the result of calling
379  * data() on the underlying vector).
380  *
381  * This is a helper type for use by neuron::container::soa and it should not be used directly.
382  */
383 template <typename Tag>
385  using data_type = typename Tag::type;
386  static_assert(has_num_variables_v<Tag>);
388  : m_tag{std::move(tag)}
389  , m_storage{m_tag.num_variables()}
390  , m_data_ptrs{std::make_unique<data_type*[]>(m_tag.num_variables())} {
391  update_data_ptr_storage();
392  auto const num = m_tag.num_variables();
393  m_array_dims.reserve(num);
394  m_array_dim_prefix_sums.reserve(num);
395  for (auto i = 0; i < m_tag.num_variables(); ++i) {
396  m_array_dims.push_back(get_array_dimension(m_tag, i));
397  m_array_dim_prefix_sums.push_back(
398  (m_array_dim_prefix_sums.empty() ? 0 : m_array_dim_prefix_sums.back()) +
399  m_array_dims.back());
400  }
401  }
402 
404  // An unknown number of data_handle<T> in the wild may be holding references to m_data_ptrs
405  defer_delete(std::move(m_data_ptrs));
406  }
407 
408  /**
409  * @brief Return a reference to the tag instance.
410  */
411  Tag const& tag() const {
412  return m_tag;
413  }
414 
415  /**
416  * @brief Return a pointer to an array of array dimensions for this tag.
417  *
418  * This avoids indirection via the tag type instances. Because array dimensions are not
419  * permitted to change, this is guaranteed to remain valid as long as the underlying soa<...>
420  * container does. This is mainly intended for use in neuron::cache::MechanismRange and friends.
421  */
422  [[nodiscard]] int const* array_dims() const {
423  return m_array_dims.data();
424  }
425 
426  /**
427  * @brief Return a pointer to an array of the prefix sum of array dimensions for this tag.
428  */
429  [[nodiscard]] int const* array_dim_prefix_sums() const {
430  return m_array_dim_prefix_sums.data();
431  }
432 
433  [[nodiscard]] int check_array_dim(int field_index, int array_index) const {
434  assert(field_index >= 0);
435  assert(array_index >= 0);
436  if (auto const num_fields = m_tag.num_variables(); field_index >= num_fields) {
437  throw std::runtime_error(get_name(m_tag, field_index) + "/" +
438  std::to_string(num_fields) + ": out of range");
439  }
440  auto const array_dim = m_array_dims[field_index];
441  if (array_index >= array_dim) {
442  throw std::runtime_error(get_name(m_tag, field_index) + ": index " +
443  std::to_string(array_index) + " out of range");
444  }
445  return array_dim;
446  }
447 
448  /**
449  * @brief Return a pointer to an array of data pointers for this tag.
450  *
451  * This array is guaranteed to be kept up to date when the actual storage is re-allocated.
452  * This is mainly intended for use in neuron::cache::MechanismRange and friends.
453  */
454  [[nodiscard]] data_type* const* data_ptrs() const {
455  return m_data_ptrs.get();
456  }
457 
458  /**
459  * @brief Invoke the given callable for each vector.
460  *
461  * @tparam might_reallocate Might the callable cause reallocation of the vector it is given?
462  * @param callable A callable to invoke.
463  */
464  template <may_cause_reallocation might_reallocate, typename Callable>
465  Callable for_each_vector(Callable callable) {
466  for (std::size_t i = 0; i < m_storage.size(); ++i) {
467  callable(m_tag, m_storage[i], i, m_array_dims[i]);
468  }
469  if constexpr (might_reallocate == may_cause_reallocation::Yes) {
470  update_data_ptr_storage();
471  }
472 
473  return callable;
474  }
475 
476  /**
477  * @brief Invoke the given callable for each vector.
478  *
479  * @param callable A callable to invoke.
480  */
481  template <typename Callable>
482  Callable for_each_vector(Callable callable) const {
483  for (std::size_t i = 0; i < m_storage.size(); ++i) {
484  callable(m_tag, m_storage[i], i, m_array_dims[i]);
485  }
486 
487  return callable;
488  }
489 
490  // TODO actually use this
491  // TODO use array_dim_prefix_sums
492  [[nodiscard]] field_index translate_legacy_index(int legacy_index) const {
493  int total{};
494  auto const num_fields = m_tag.num_variables();
495  for (auto field = 0; field < num_fields; ++field) {
496  auto const array_dim = m_array_dims[field];
497  if (legacy_index < total + array_dim) {
498  auto const array_index = legacy_index - total;
499  return {field, array_index};
500  }
501  total += array_dim;
502  }
503  throw std::runtime_error("could not translate legacy index " +
504  std::to_string(legacy_index));
505  }
506 
507  private:
509  std::transform(m_storage.begin(), m_storage.end(), m_data_ptrs.get(), [](auto& vec) {
510  return vec.data();
511  });
512  }
513  /**
514  * @brief Tag type instance.
515  *
516  * An instance of @c soa contains an instance of @c field_data for each tag type in its @c
517  * Tags... pack. The instance of the tag type contains the metadata about the field it
518  * represents, and @c field_data adds the actual data for that field. For example, with @c Tag =
519  * @c Mechanism::field::FloatingPoint, which represents RANGE variables in mechanisms, @c m_tag
520  * holds the names and array dimensions of the RANGE variables.
521  */
522  Tag m_tag;
523 
524  /**
525  * @brief Storage for the data associated with @c Tag.
526  *
527  * These are the "large" data arrays holding the model data. Because this specialisation of @c
528  * field_data is for @c Tag types that @b do have @c num_variables() members, such as @c
529  * Mechanism::field::FloatingPoint, there is an outer vector with this dimension.
530  *
531  * @invariant @c m_storage.size() is equal to @c m_tag.num_variables()
532  *
533  * For Mechanism data, this size is equal to the number of RANGE variables, while
534  * @c m_storage[i].size() is (assuming an array dimension of 1) the number of instances (in this
535  * case of the given Mechanism type) that exist in the program.
536  */
537  std::vector<std::vector<data_type>> m_storage;
538 
539  /**
540  * @brief Storage where we maintain an up-to-date cache of .data() pointers from m_storage.
541  * @invariant @c m_data_ptrs contains @c m_storage.size() elements
542  * @invariant @c m_storage[i].data() is equal to @c m_data_ptrs[i] for all @c i.
543  *
544  * This is useful because it allows @c data_handle<T> to store something like @c T** instead of
545  * having to store something like @c std::vector<T>*, which avoids hardcoding unnecessary
546  * details about the allocator and so on, and allows @c cache::MechanismRange to similarly have
547  * a C-like interface. Because @c data_handle<T> remembers references to this, we cannot free
548  * it when the container is destroyed (e.g. when a mechanism type is deleted).
549  */
550  std::unique_ptr<data_type*[]> m_data_ptrs;
551 
552  /**
553  * @brief Array dimensions of the data associated with @c Tag.
554  * @invariant @c m_storage.size() is equal to @c m_array_dims.size()
555  * @invariant @c m_array_dims[i] is equal to @c m_tag.array_dimension(i), if that function
556  * exists, or otherwise 1, for all @c i
557  *
558  * Similar to @c m_data_ptrs, this allows the array dimensions to be communicated simply across
559  * a C-like interface.
560  */
561  std::vector<int> m_array_dims;
562 
563  /**
564  * @brief Prefix sum over array dimensions for the data associated with @c Tag.
565  * @invariant @c m_storage.size() is equal to @c m_array_dim_prefix_sums.size()
566  * @invariant @c m_array_dim_prefix_sums[i] is equal to the sum of @c m_array_dims[0] .. @c
567  * m_array_dims[i] for all @c i.
568  * @todo This could be used to more efficiently convert legacy indices.
569  *
570  * This is mainly useful for logic that aids the transition from AoS to SoAoS format in NEURON.
571  * For example, the size of the old @c _p vectors in NEURON was @c
572  * m_array_dim_prefix_sums.back(), the sum over all array dimensions, which is generally larger
573  * than @c m_tag.num_variables().
574  */
575  std::vector<int> m_array_dim_prefix_sums;
576 };
577 
579  std::string_view container() const override {
580  return m_container;
581  }
582  std::string_view field() const override {
583  return m_field;
584  }
585  std::size_t size() const override {
586  return m_size;
587  }
588  std::string m_container{}, m_field{};
589  std::size_t m_size{};
590 };
591 
593  public:
595  std::vector<detail::index_column_tag::type> const& vec,
596  int field_index,
597  int array_dim) {
598  m_usage.stable_identifiers.size = vec.size() * (sizeof(vec[0]) + sizeof(size_t*));
600  (vec.capacity() - vec.size()) * sizeof(vec[0]);
601  }
602 
603  template <class Tag>
604  void operator()(Tag const& tag,
605  std::vector<typename Tag::type> const& vec,
606  int field_index,
607  int array_dim) {
609  }
610 
612  return m_usage;
613  }
614 
615  private:
617 };
618 
619 
620 } // namespace detail
621 
622 /**
623  * @brief Token whose lifetime manages the frozen state of a container.
624  *
625  * Because this cannot be defaulted constructed or moved, it cannot reach an
626  * empty/invalid state.
627  */
628 template <typename Container>
629 struct state_token {
630  /**
631  * @brief Copy a token, incrementing the frozen count of the underlying container.
632  */
633  constexpr state_token(state_token const& other)
634  : m_container{other.m_container} {
636  m_container->increase_frozen_count();
637  }
638 
639  /**
640  * @brief Copy assignment.
641  *
642  * Explicitly deleted to avoid an implicit version with the wrong semantics.
643  */
644  constexpr state_token& operator=(state_token const&) = delete;
645 
646  /**
647  * @brief Destroy a token, decrementing the frozen count of the underlying container.
648  */
651  m_container->decrease_frozen_count();
652  }
653 
654  private:
655  template <typename, typename...>
656  friend struct soa;
657  constexpr state_token(Container& container)
658  : m_container{&container} {}
659  Container* m_container{};
660 };
661 
662 /**
663  * @brief Utility for generating SOA data structures.
664  * @headerfile neuron/container/soa_container.hpp
665  * @tparam Storage Name of the actual storage type derived from soa<...>.
666  * @tparam Tags Parameter pack of tag types that define the columns
667  * included in the container. Types may not be repeated.
668  *
669  * This CRTP base class is used to implement the ~global SOA storage structs
670  * that hold (so far) Node and Mechanism data. Ownership of rows in these
671  * structs is managed via instances of the owning identifier type @ref
672  * neuron::container::owning_identifier instantiated with Storage, and
673  * non-owning reference to rows in the data structures are managed via instances
674  * of the @ref neuron::container::non_owning_identifier template instantiated
675  * with Storage. These identifiers are typically wrapped in a
676  * data-structure-specific (i.e. Node- or Mechanism-specific) interface type
677  * that provides data-structure-specific accessors and methods to obtain actual
678  * data values and more generic handle types such as @ref
679  * neuron::container::data_handle<T> and @ref
680  * neuron::container::generic_data_handle.
681  */
682 template <typename Storage, typename... Tags>
683 struct soa {
684  /**
685  * @brief Construct with default-constructed tag type instances.
686  */
687  soa()
688  : soa(Tags{}...) {}
689 
690  /**
691  * @brief Construct with specific tag instances.
692  *
693  * This is useful if the tag types are not empty, for example if the number
694  * of times a column is duplicated is a runtime value.
695  */
696  soa(Tags... tag_instances)
697  : m_data{std::move(tag_instances)...} {}
698 
699  /**
700  * @brief @ref soa is not movable
701  *
702  * This is to make it harder to accidentally invalidate pointers-to-storage
703  * in handles.
704  */
705  soa(soa&&) = delete;
706 
707  /**
708  * @brief @ref soa is not copiable
709  *
710  * This is partly to make it harder to accidentally invalidate
711  * pointers-to-storage in handles, and partly because it could be very
712  * expensive so it might be better to be more explicit.
713  */
714  soa(soa const&) = delete;
715 
716  /**
717  * @brief @ref soa is not move assignable
718  *
719  * For the same reason it isn't movable.
720  */
721  soa& operator=(soa&&) = delete;
722 
723  /**
724  * @brief @ref soa is not copy assignable
725  *
726  * For the same reasons it isn't copy constructible
727  */
728  soa& operator=(soa const&) = delete;
729 
730  /**
731  * @brief Get the size of the container.
732  *
733  * Note that this is not thread-safe if the container is not frozen, i.e.
734  * you should either hold a token showing the container is frozen, or you
735  * should ensure that no non-const operations on this container are being
736  * executed concurrently.
737  */
738  [[nodiscard]] std::size_t size() const {
739  // Check our various std::vector members are still the same size as each
740  // other. This check could be omitted in release builds...
741  auto const check_size = m_indices.size();
743  [check_size](auto const& tag, auto const& vec, int field_index, int array_dim) {
744  auto const size = vec.size();
745  assert(size % array_dim == 0);
746  assert(size / array_dim == check_size);
747  });
748  return check_size;
749  }
750 
751  /**
752  * @brief Test if the container is empty.
753  *
754  * Note that this is not thread-safe if the container is not frozen, i.e.
755  * you should either hold a token showing the container is frozen, or you
756  * should ensure that no non-const operations on this container are being
757  * executed concurrently.
758  */
759  [[nodiscard]] bool empty() const {
760  auto const result = m_indices.empty();
761  for_each_vector([result](auto const& tag, auto const& vec, int field_index, int array_dim) {
762  assert(vec.empty() == result);
763  });
764  return result;
765  }
766 
767  void shrink_to_fit() {
768  if (m_frozen_count) {
769  throw_error("shrink() called on a frozen structure");
770  }
771  for_each_vector<detail::may_cause_reallocation::Yes>(
772  [](auto const& tag, auto& vec, int field_index, int array_dim) {
773  vec.shrink_to_fit();
774  });
775  }
776 
777  private:
778  /**
779  * @brief Remove the @f$i^{\text{th}}@f$ row from the container.
780  *
781  * This is currently implemented by swapping the last element into position
782  * @f$i@f$ (if those are not the same element) and reducing the size by one.
783  * Iterators to the last element and the deleted element will be
784  * invalidated.
785  */
786  void erase(std::size_t i) {
787  // Lock access to m_frozen_count and m_sorted.
788  std::lock_guard _{m_mut};
789  if (m_frozen_count) {
790  throw_error("erase() called on a frozen structure");
791  }
792  mark_as_unsorted_impl<true>();
793  auto const old_size = size();
794  assert(i < old_size);
795  if (i != old_size - 1) {
796  // Swap ranges of size array_dim at logical positions `i` and `old_size - 1` in each
797  // vector
798  for_each_vector<detail::may_cause_reallocation::No>(
799  [i](auto const& tag, auto& vec, int field_index, int array_dim) {
800  ::std::swap_ranges(::std::next(vec.begin(), i * array_dim),
801  ::std::next(vec.begin(), (i + 1) * array_dim),
802  ::std::prev(vec.end(), array_dim));
803  });
804  // Tell the new entry at `i` that its index is `i` now.
805  m_indices[i].set_current_row(i);
806  }
807  for_each_vector<detail::may_cause_reallocation::No>(
808  [new_size = old_size - 1](auto const& tag, auto& vec, int field_index, int array_dim) {
809  vec.resize(new_size * array_dim);
810  });
811  }
812 
813  friend struct state_token<Storage>;
814  friend struct owning_identifier<Storage>;
815 
816  static_assert(detail::are_types_unique_v<Tags...>, "All tag types should be unique");
817  template <typename Tag>
818  static constexpr std::size_t tag_index_v = detail::index_of_type_v<Tag, Tags...>;
819 
820  /**
821  * @brief Apply the given function to non-const versions of all vectors.
822  *
823  * The callable has a signature compatible with:
824  *
825  * void callable(const Tag& tag,
826  * std::vector<Tag::data_type, Allocator>& v,
827  * int field_index,
828  * int array_dim)
829  *
830  * where `array_dim` is the array dimensions of the field `field_index`.
831  *
832  * @tparam might_reallocate Might the callable trigger reallocation of the vectors?
833  * @param callable Callable to invoke on each vector.
834  *
835  * If might_allocate is true then the "cached" values of .data() for each vector will be
836  * updated.
837  */
838  template <detail::may_cause_reallocation might_reallocate, typename Callable>
839  Callable for_each_vector(Callable callable) {
840  // might_reallocate is not relevant for m_indices because we do not expose the location of
841  // its storage, so it doesn't matter whether or not this triggers reallocation
842  callable(detail::index_column_tag{}, m_indices, -1, 1);
843 
844  return for_each_tag_vector_impl<might_reallocate, Callable, Tags...>(callable);
845  }
846 
847  template <detail::may_cause_reallocation might_reallocate,
848  typename Callable,
849  typename Tag,
850  typename... RemainingTags>
851  Callable for_each_tag_vector_impl(Callable callable) {
852  auto tmp_callable =
853  std::get<tag_index_v<Tag>>(m_data).template for_each_vector<might_reallocate>(callable);
854  return for_each_tag_vector_impl<might_reallocate, Callable, RemainingTags...>(tmp_callable);
855  }
856 
857  template <detail::may_cause_reallocation, typename Callable>
858  Callable for_each_tag_vector_impl(Callable callable) {
859  return callable;
860  }
861 
862  /**
863  * @brief Apply the given function to const-qualified versions of all vectors.
864  *
865  * The callable has a signature compatible with:
866  *
867  * void callable(const Tag& tag,
868  * const std::vector<Tag::data_type, Allocator>& v,
869  * int field_index,
870  * int array_dim)
871  *
872  * where `array_dim` is the array dimensions of the field `field_index`.
873  *
874  * Because of the const qualification this cannot cause reallocation and trigger updates of
875  * pointers inside m_data, so no might_reallocate parameter is needed.
876  */
877  template <typename Callable>
878  Callable for_each_vector(Callable callable) const {
879  callable(detail::index_column_tag{}, m_indices, -1, 1);
880  return for_each_tag_vector_impl<Callable, Tags...>(callable);
881  }
882 
883  template <typename Callable, typename Tag, typename... RemainingTags>
884  Callable for_each_tag_vector_impl(Callable callable) const {
885  Callable tmp_callable = std::get<tag_index_v<Tag>>(m_data).template for_each_vector<>(
886  callable);
887  return for_each_tag_vector_impl<Callable, RemainingTags...>(tmp_callable);
888  }
889 
890  template <typename Callable>
891  Callable for_each_tag_vector_impl(Callable callable) const {
892  return callable;
893  }
894 
895 
896  /**
897  * @brief Record that a state_token was copied.
898  *
899  * This should only be called from the copy constructor of a state_token,
900  * so m_frozen_count should already be non-zero.
901  */
903  std::lock_guard _{m_mut};
905  ++m_frozen_count;
906  }
907 
908  /**
909  * @brief Flag that the storage is no longer frozen.
910  *
911  * This is called from the destructor of state_token.
912  */
914  std::lock_guard _{m_mut};
916  --m_frozen_count;
917  }
918 
919  public:
920  /**
921  * @brief Return type of issue_frozen_token()
922  */
924 
925  /**
926  * @brief Create a token guaranteeing the container is in "frozen" state.
927  *
928  * This does *not* modify the "sorted" flag on the container.
929  *
930  * The token type is copy constructible but not default constructible.
931  * There is no need to check if a given instance of the token type is
932  * "valid"; if a token is held then the container is guaranteed to be
933  * frozen.
934  *
935  * The tokens returned by this function are reference counted; the
936  * container will be frozen for as long as any token is alive.
937  *
938  * Methods such as apply_reverse_permutation() take a non-const reference
939  * to one of these tokens. This is because a non-const token referring to a
940  * container with a token reference count of exactly one has an elevated
941  * status: the holder can lend it out to methods such as
942  * apply_reverse_permutation() to authorize specific pointer-invaliding
943  * operations. This is useful for implementing methods such as
944  * nrn_ensure_model_data_are_sorted() in a thread-safe way.
945  *
946  * This method can be called from multiple threads, but note that doing so
947  * can have surprising effects w.r.t. the elevated status mentioned in the
948  * previous paragraph.
949  *
950  * It is user-defined precisely what "sorted" means, but the soa<...> class
951  * makes some guarantees:
952  * - if the container is frozen, no pointers to elements in the underlying
953  * storage will be invalidated -- attempts to do so will throw or abort.
954  * - if the container is not frozen, it will remain flagged as sorted until
955  * a potentially-pointer-invalidating operation (insertion, deletion)
956  * occurs, or mark_as_unsorted() is called.
957  * To mark a container as "sorted", apply an explicit permutation to it.
958  *
959  * Note that "frozen" refers to the storage layout, not to the stored value,
960  * meaning that values inside a frozen container can still be modified --
961  * "frozen" is not "runtime const".
962  *
963  * @todo A future extension could be to preserve the sorted flag until
964  * pointers are actually, not potentially, invalidated.
965  */
967  // Lock access to m_frozen_count and m_sorted.
968  std::lock_guard _{m_mut};
969  // Increment the reference count, marking the container as frozen.
970  ++m_frozen_count;
971  // Return a token that calls decrease_frozen_count() at the end of its lifetime
972  return frozen_token_type{static_cast<Storage&>(*this)};
973  }
974 
975  /**
976  * @brief Tell the container it is sorted.
977  * @param write_token Non-const token demonstrating the caller is the only
978  * token owner.
979  *
980  * The meaning of being sorted is externally defined, so we should give
981  * external code the opportunity to say that the current order is OK. This
982  * probably only makes sense if the external code simply doesn't care about
983  * the ordering at all for some reason. This avoids having to construct a
984  * trivial permutation vector to achieve the same thing.
985  */
986  void mark_as_sorted(frozen_token_type& write_token) {
987  // Lock access to m_frozen_count and m_sorted.
988  std::lock_guard _{m_mut};
989  if (m_frozen_count != 1) {
990  throw_error("mark_as_sorted() given a token that was not the only valid one");
991  }
992  m_sorted = true;
993  }
994 
995  /**
996  * @brief Tell the container it is no longer sorted.
997  *
998  * The meaning of being sorted is externally defined, and it is possible
999  * that some external change to an input of the (external) algorithm
1000  * defining the sort order can mean that the data are no longer considered
1001  * sorted, even if nothing has actually changed inside this container.
1002  *
1003  * This method can only be called if the container is not frozen.
1004  */
1006  // Lock access to m_frozen_count and m_sorted.
1007  std::lock_guard _{m_mut};
1008  mark_as_unsorted_impl<false>();
1009  }
1010 
1011  /**
1012  * @brief Set the callback that is invoked when the container becomes unsorted.
1013  *
1014  * This is invoked by mark_as_unsorted() and when a container operation
1015  * (insertion, permutation, deletion) causes the container to transition
1016  * from being sorted to being unsorted.
1017  *
1018  * This method is not thread-safe.
1019  */
1020  void set_unsorted_callback(std::function<void()> unsorted_callback) {
1021  m_unsorted_callback = std::move(unsorted_callback);
1022  }
1023 
1024  /**
1025  * @brief Query if the underlying vectors are still "sorted".
1026  *
1027  * See the documentation of issue_frozen_token() for an explanation of what
1028  * this means. You most likely only want to call this method while holding
1029  * a token guaranteeing that the container is frozen, and therefore that
1030  * the sorted-status is fixed.
1031  */
1032  [[nodiscard]] bool is_sorted() const {
1033  return m_sorted;
1034  }
1035 
1036  /**
1037  * @brief Permute the SoA-format data using an arbitrary range of integers.
1038  * @param permutation The reverse permutation vector to apply.
1039  * @return A token guaranteeing the frozen + sorted state of the container
1040  * after the permutation was applied.
1041  *
1042  * This will fail if the container is frozen.
1043  */
1044  template <typename Arg>
1046  auto token = issue_frozen_token();
1047  apply_reverse_permutation(std::forward<Arg>(permutation), token);
1048  return token;
1049  }
1050 
1051  /**
1052  * @brief Permute the SoA-format data using an arbitrary range of integers.
1053  * @param permutation The reverse permutation vector to apply.
1054  * @param token A non-const token demonstrating that the caller is the only
1055  * party that is forcing the container to be frozen, and (non-const)
1056  * that they are authorised to transfer that status into this method
1057  */
1058  template <typename Range>
1059  void apply_reverse_permutation(Range permutation, frozen_token_type& sorted_token) {
1060  // Check that the given vector is a valid permutation of length size().
1061  // The existence of `sorted_token` means that my_size cannot become
1062  // invalid, even though we don't hold `m_mut` yet.
1063  std::size_t const my_size{size()};
1064  bool const is_trivial{detail::check_permutation_vector(permutation, my_size)};
1065  // Lock access to m_frozen_count and m_sorted.
1066  std::lock_guard _{m_mut};
1067  // Applying a permutation in general invalidates indices, so it is
1068  // forbidden if the structure is frozen, and it leaves the structure
1069  // unsorted. We therefore require that the frozen count is 1, which
1070  // corresponds to the `sorted_token` argument to this function being
1071  // the only active token.
1072  if (m_frozen_count != 1) {
1073  throw_error(
1074  "apply_reverse_permutation() given a token that was not the only valid one");
1075  }
1076  if (!is_trivial) {
1077  // Now we apply the reverse permutation in `permutation` to all of the columns in the
1078  // container. This is the algorithm from boost::algorithm::apply_reverse_permutation.
1079  for (std::size_t i = 0; i < my_size; ++i) {
1080  while (i != permutation[i]) {
1081  using ::std::swap;
1082  auto const next = permutation[i];
1083  for_each_vector<detail::may_cause_reallocation::No>(
1084  [i, next](auto const& tag, auto& vec, auto field_index, auto array_dim) {
1085  // swap the i-th and next-th array_dim-sized sub-ranges of vec
1086  ::std::swap_ranges(::std::next(vec.begin(), i * array_dim),
1087  ::std::next(vec.begin(), (i + 1) * array_dim),
1088  ::std::next(vec.begin(), next * array_dim));
1089  });
1090  swap(permutation[i], permutation[next]);
1091  }
1092  }
1093  // update the indices in the container
1094  for (auto i = 0ul; i < my_size; ++i) {
1095  m_indices[i].set_current_row(i);
1096  }
1097  // If the container was previously marked sorted, and we have just
1098  // applied a non-trivial permutation to it, then we need to call the
1099  // callback if it exists (to invalidate any caches based on the old
1100  // sort order).
1101  if (m_sorted && m_unsorted_callback) {
1103  }
1104  }
1105  // In any case, applying a permutation leaves the container in sorted
1106  // state. The caller made an explicit choice about which element should
1107  // live where, which is as much as we can hope for.
1108  m_sorted = true;
1109  }
1110 
1111 
1112  private:
1113  /**
1114  * @brief Set m_sorted = false and execute the callback.
1115  * @note The *caller* is expected to hold m_mut when this is called.
1116  */
1117  template <bool internal>
1119  if (m_frozen_count) {
1120  // Currently you can only obtain a frozen container by calling
1121  // issue_frozen_token(), which explicitly guarantees that the
1122  // container will remain sorted for the lifetime of the returned
1123  // token.
1124  throw_error("mark_as_unsorted() called on a frozen structure");
1125  }
1126  // Only execute the callback if we're transitioning from sorted to
1127  // or if this was an explicit mark_as_unsorted() call
1128  bool const execute_callback{m_sorted || !internal};
1129  m_sorted = false;
1130  if (execute_callback && m_unsorted_callback) {
1132  }
1133  }
1134 
1135  /**
1136  * @brief Create a new entry and return an identifier that owns it.
1137  *
1138  * Calling this method increases size() by one. Destroying (modulo move
1139  * operations) the returned identifier, which has the semantics of a
1140  * unique_ptr, decreases size() by one.
1141  *
1142  * Note that this has different semantics to standard library container
1143  * methods such as emplace_back(), push_back(), insert() and so on. Because
1144  * the returned identifier manages the lifetime of the newly-created entry,
1145  * discarding the return value will cause the new entry to immediately be
1146  * deleted.
1147  *
1148  * This is a low-level call that is useful for the implementation of the
1149  * owning_identifier template. The returned owning identifier is typically
1150  * wrapped inside an owning handle type that adds data-structure-specific
1151  * methods (e.g. v(), v_handle() for a Node).
1152  */
1154  // Lock access to m_frozen_count and m_sorted.
1155  std::lock_guard _{m_mut};
1156  if (m_frozen_count) {
1157  throw_error("acquire_owning_identifier() called on a frozen structure");
1158  }
1159  // The .emplace_back() methods we are about to call can trigger
1160  // reallocation and, therefore, invalidation of pointers. At present,
1161  // "sorted" is defined to mean that pointers have not been invalidated.
1162  // There are two reasonable changes that could be made here:
1163  // - possibly for release builds, we could only mark unsorted if a
1164  // reallocation *actually* happens
1165  // - "sorted" could be defined to mean that indices have not been
1166  // invalidated -- adding a new entry to the end of the container
1167  // never invalidates indices
1168  mark_as_unsorted_impl<true>();
1169  // Append to all of the vectors
1170  auto const old_size = size();
1171  for_each_vector<detail::may_cause_reallocation::Yes>(
1172  [](auto const& tag, auto& vec, auto field_index, auto array_dim) {
1173  using Tag = ::std::decay_t<decltype(tag)>;
1174  if constexpr (detail::has_default_value_v<Tag>) {
1175  vec.insert(vec.end(), array_dim, tag.default_value());
1176  } else {
1177  vec.insert(vec.end(), array_dim, {});
1178  }
1179  });
1180  // Important that this comes after the m_frozen_count check
1181  owning_identifier<Storage> index{static_cast<Storage&>(*this), old_size};
1182  // Update the pointer-to-row-number in m_indices so it refers to the
1183  // same thing as index
1185  return index;
1186  }
1187 
1188  public:
1189  /**
1190  * @brief Get a non-owning identifier to the offset-th entry.
1191  *
1192  * This method should only be called if either: there is only one thread,
1193  * or if a frozen token is held.
1194  */
1195  [[nodiscard]] non_owning_identifier<Storage> at(std::size_t offset) const {
1196  return {const_cast<Storage*>(static_cast<Storage const*>(this)), m_indices[offset]};
1197  }
1198 
1199  /**
1200  * @brief Get the instance of the given tag type.
1201  * @tparam Tag The tag type, which must be a member of the @c Tags... pack.
1202  * @return Const reference to the given tag type instance.
1203  *
1204  * For example, if this is called on the @c Node::storage then @c Tag would be something like @c
1205  * Node::field::Area, @c Node::field::RHS or @c Node::field::Voltage, which are empty types that
1206  * serve to define the default values and types of those quantities.
1207  *
1208  * At the time of writing the other possibility is that this is called on an instance of @c
1209  * Mechanism::storage, in which case @c Tag must (currently) be @c
1210  * Mechanism::field::FloatingPoint. This stores the names and array dimensions of the RANGE
1211  * variables in the mechanism (MOD file), which are only known at runtime.
1212  */
1213  template <typename Tag>
1214  [[nodiscard]] constexpr Tag const& get_tag() const {
1215  return std::get<tag_index_v<Tag>>(m_data).tag();
1216  }
1217 
1218  template <typename Tag>
1219  static constexpr bool has_tag_v = detail::type_in_pack_v<Tag, Tags...>;
1220 
1221  /**
1222  * @brief Get the offset-th element of the column named by Tag.
1223  *
1224  * Because this is returning a single value, it is permitted even when the
1225  * container is frozen. The container being frozen means that operations
1226  * that would invalidate iterators/pointers are forbidden, not that actual
1227  * data values cannot change. Note that if the container is not frozen then
1228  * care should be taken in a multi-threaded environment, as `offset` could
1229  * be invalidated by operations performed by other threads (that would fail
1230  * if the container were frozen).
1231  */
1232  template <typename Tag>
1233  [[nodiscard]] typename Tag::type& get(std::size_t offset) {
1234  static_assert(has_tag_v<Tag>);
1235  static_assert(!detail::has_num_variables_v<Tag>);
1236  auto& field_data = std::get<tag_index_v<Tag>>(m_data);
1237  if constexpr (detail::field_impl_v<Tag> == detail::FieldImplementation::OptionalSingle) {
1238  if (!field_data.active()) {
1239  std::lock_guard _{m_mut};
1240  throw_error("get(offset) called for a disabled optional field");
1241  }
1242  }
1243  return field_data.data_ptrs()[0][offset];
1244  }
1245 
1246  /**
1247  * @brief Get the offset-th element of the column named by Tag.
1248  *
1249  * If the container is not frozen then care should be taken in a
1250  * multi-threaded environment, as `offset` could be invalidated by
1251  * operations performed by other threads (that would fail if the container
1252  * were frozen).
1253  */
1254  template <typename Tag>
1255  [[nodiscard]] typename Tag::type const& get(std::size_t offset) const {
1256  static_assert(has_tag_v<Tag>);
1257  static_assert(!detail::has_num_variables_v<Tag>);
1258  auto const& field_data = std::get<tag_index_v<Tag>>(m_data);
1259  if constexpr (detail::field_impl_v<Tag> == detail::FieldImplementation::OptionalSingle) {
1260  if (!field_data.active()) {
1261  std::lock_guard _{m_mut};
1262  throw_error("get(offset) const called for a disabled optional field");
1263  }
1264  }
1265  return field_data.data_ptrs()[0][offset];
1266  }
1267 
1268  /**
1269  * @brief Get a handle to the given element of the column named by Tag.
1270  *
1271  * This is not intended to be called from multi-threaded code, and might
1272  * suffer from race conditions if the status of optional fields was being
1273  * modified concurrently.
1274  */
1275  template <typename Tag>
1278  int array_index = 0) const {
1279  static_assert(has_tag_v<Tag>);
1280  static_assert(!detail::has_num_variables_v<Tag>);
1281  auto const& field_data = std::get<tag_index_v<Tag>>(m_data);
1282  // If Tag is an optional field and that field is disabled, return a null handle.
1283  if constexpr (detail::optional_v<Tag>) {
1284  if (!field_data.active()) {
1285  return {};
1286  }
1287  }
1288  auto const array_dim = field_data.array_dims()[0];
1289  assert(array_dim > 0);
1290  assert(array_index >= 0);
1291  assert(array_index < array_dim);
1292  return {std::move(id), field_data.data_ptrs(), array_dim, array_index};
1293  }
1294 
1295  /**
1296  * @brief Get a handle to the given element of the field_index-th column named by Tag.
1297  *
1298  * This is not intended to be called from multi-threaded code.
1299  */
1300  template <typename Tag>
1303  int field_index,
1304  int array_index = 0) const {
1305  static_assert(has_tag_v<Tag>);
1306  static_assert(detail::has_num_variables_v<Tag>);
1307  auto const array_dim = std::get<tag_index_v<Tag>>(m_data).check_array_dim(field_index,
1308  array_index);
1309  return {std::move(id),
1310  std::get<tag_index_v<Tag>>(m_data).data_ptrs() + field_index,
1311  array_dim,
1312  array_index};
1313  }
1314 
1315  /**
1316  * @brief Get the offset-th element of the field_index-th instance of the column named by Tag.
1317  *
1318  * Put differently:
1319  * - offset: index of a mechanism instance
1320  * - field_index: index of a RANGE variable inside a mechanism
1321  * - array_index: offset inside an array RANGE variable
1322  *
1323  * Because this is returning a single value, it is permitted even when the
1324  * container is frozen. The container being frozen means that operations
1325  * that would invalidate iterators/pointers are forbidden, not that actual
1326  * data values cannot change. Note that if the container is not frozen then
1327  * care should be taken in a multi-threaded environment, as `offset` could
1328  * be invalidated by operations performed by other threads (that would fail
1329  * if the container were frozen).
1330  */
1331  template <typename Tag>
1332  typename Tag::type& get_field_instance(std::size_t offset,
1333  int field_index,
1334  int array_index = 0) {
1335  auto const array_dim = std::get<tag_index_v<Tag>>(m_data).check_array_dim(field_index,
1336  array_index);
1337  return std::get<tag_index_v<Tag>>(m_data)
1338  .data_ptrs()[field_index][offset * array_dim + array_index];
1339  }
1340 
1341  /**
1342  * @brief Get the offset-th element of the field_index-th instance of the column named by Tag.
1343  *
1344  * If the container is not frozen then care should be taken in a
1345  * multi-threaded environment, as `offset` could be invalidated by
1346  * operations performed by other threads (that would fail if the container
1347  * were frozen).
1348  */
1349  template <typename Tag>
1350  typename Tag::type const& get_field_instance(std::size_t offset,
1351  int field_index,
1352  int array_index = 0) const {
1353  auto const array_dim = std::get<tag_index_v<Tag>>(m_data).check_array_dim(field_index,
1354  array_index);
1355  return std::get<tag_index_v<Tag>>(m_data)
1356  .data_ptrs()[field_index][offset * array_dim + array_index];
1357  }
1358 
1359  /**
1360  * @brief Get the offset-th identifier.
1361  *
1362  * If the container is not frozen then care should be taken in a
1363  * multi-threaded environment, as `offset` could be invalidated by
1364  * operations performed by other threads (that would fail if the container
1365  * were frozen).
1366  */
1367  [[nodiscard]] non_owning_identifier_without_container get_identifier(std::size_t offset) const {
1368  return m_indices.at(offset);
1369  }
1370 
1371  /**
1372  * @brief Return a permutation-stable handle if ptr is inside us.
1373  * @todo Check const-correctness. Presumably a const version would return
1374  * data_handle<T const>, which would hold a pointer-to-const for the
1375  * container?
1376  *
1377  * This is not intended to be called from multi-threaded code if the
1378  * container is not frozen.
1379  */
1381  neuron::container::generic_data_handle input_handle) const {
1382  bool done{false};
1384  for_each_vector([this, &done, &handle, &input_handle](
1385  auto const& tag, auto const& vec, int field_index, int array_dim) {
1386  using Tag = ::std::decay_t<decltype(tag)>;
1387  if constexpr (!std::is_same_v<Tag, detail::index_column_tag>) {
1388  using Data = typename Tag::type;
1389  if (done) {
1390  // Short circuit
1391  return;
1392  }
1393  if (vec.empty()) {
1394  // input_handle can't point into an empty vector
1395  return;
1396  }
1397  if (!input_handle.holds<Data*>()) {
1398  // input_handle can't point into a vector of the wrong type
1399  return;
1400  }
1401  auto* const ptr = input_handle.get<Data*>();
1402  if (ptr < vec.data() || ptr >= std::next(vec.data(), vec.size())) {
1403  return;
1404  }
1405  auto const physical_row = ptr - vec.data();
1406  assert(physical_row < vec.size());
1407  // This pointer seems to live inside this container. This is all a bit fragile.
1408  int const array_index = physical_row % array_dim;
1409  int const logical_row = physical_row / array_dim;
1411  at(logical_row),
1412  std::get<tag_index_v<Tag>>(m_data).data_ptrs() + std::max(field_index, 0),
1413  array_dim,
1414  array_index};
1415  assert(handle.refers_to_a_modern_data_structure());
1416  done = true; // generic_data_handle doesn't convert to bool
1417  }
1418  });
1419  return handle;
1420  }
1421 
1422  /**
1423  * @brief Query whether the given pointer-to-vector is the one associated to Tag.
1424  * @todo Fix this for tag types with num_variables()?
1425  *
1426  * This is used so that one can ask a data_handle<T> if it refers to a
1427  * particular field in a particular container. It is not intended to be
1428  * called from multi-threaded code if the container is not frozen.
1429  */
1430  template <typename Tag>
1431  [[nodiscard]] bool is_storage_pointer(typename Tag::type const* ptr) const {
1432  static_assert(has_tag_v<Tag>);
1433  static_assert(!detail::has_num_variables_v<Tag>);
1434  auto* const data_ptrs = std::get<tag_index_v<Tag>>(m_data).data_ptrs();
1435  if constexpr (detail::optional_v<Tag>) {
1436  // if the field is optional and disabled, data_ptrs is null
1437  if (!data_ptrs) {
1438  return false;
1439  }
1440  }
1441  return ptr == data_ptrs[0];
1442  }
1443 
1444  /**
1445  * @brief Check if `cont` refers to a field in this container.
1446  *
1447  * This is not intended to be called from multi-threaded code if the
1448  * container is not frozen.
1449  */
1450  [[nodiscard]] std::unique_ptr<utils::storage_info> find_container_info(void const* cont) const {
1451  std::unique_ptr<utils::storage_info> opt_info;
1452  if (!cont) {
1453  return opt_info;
1454  }
1455  for_each_vector([cont,
1456  &opt_info,
1457  this](auto const& tag, auto const& vec, int field_index, int array_dim) {
1458  if (opt_info) {
1459  // Short-circuit
1460  return;
1461  }
1462  if (vec.data() != cont) {
1463  // This isn't the right vector
1464  return;
1465  }
1466  // We found the right container/tag combination! Populate the
1467  // information struct.
1468  auto impl_ptr = std::make_unique<detail::storage_info_impl>();
1469  auto& info = *impl_ptr;
1470  info.m_container = static_cast<Storage const&>(*this).name();
1471  info.m_field = detail::get_name(tag, field_index);
1472  info.m_size = vec.size();
1473  assert(info.m_size % array_dim == 0);
1474  opt_info = std::move(impl_ptr);
1475  });
1476  return opt_info;
1477  }
1478 
1479  /**
1480  * @brief Get a pointer to a range of pointers that always point to the start of the contiguous
1481  * storage.
1482  */
1483  template <typename Tag>
1484  [[nodiscard]] typename Tag::type* const* get_data_ptrs() const {
1485  return std::get<tag_index_v<Tag>>(m_data).data_ptrs();
1486  }
1487 
1488  /**
1489  * @brief Get a pointer to an array holding the array dimensions of the fields associated with
1490  * this tag.
1491  */
1492  template <typename Tag>
1493  [[nodiscard]] int const* get_array_dims() const {
1494  return std::get<tag_index_v<Tag>>(m_data).array_dims();
1495  }
1496 
1497  template <typename Tag>
1498  [[nodiscard]] int get_array_dims(int field_index) const {
1499  assert(field_index < get_num_variables<Tag>());
1500  return get_array_dims<Tag>()[field_index];
1501  }
1502 
1503  template <typename Tag>
1504  [[nodiscard]] size_t get_num_variables() const {
1505  return detail::get_num_variables(get_tag<Tag>());
1506  }
1507 
1508  /**
1509  * @brief Get a pointer to an array holding the prefix sum of array dimensions for this tag.
1510  */
1511  template <typename Tag>
1512  [[nodiscard]] int const* get_array_dim_prefix_sums() const {
1513  return std::get<tag_index_v<Tag>>(m_data).array_dim_prefix_sums();
1514  }
1515 
1516  template <typename Tag>
1517  [[nodiscard]] field_index translate_legacy_index(int legacy_index) const {
1518  return std::get<tag_index_v<Tag>>(m_data).translate_legacy_index(legacy_index);
1519  }
1520 
1521  /**
1522  * @brief Query whether the field associated with the given tag is active.
1523  */
1524  template <typename Tag>
1525  [[nodiscard]] bool field_active() const {
1526  static_assert(detail::optional_v<Tag>,
1527  "field_active can only be called with tag types for optional fields");
1528  return std::get<tag_index_v<Tag>>(m_data).active();
1529  }
1530 
1531  /**
1532  * @brief Enable/disable the fields associated with the given tags.
1533  */
1534  template <typename... TagsToChange>
1535  void set_field_status(bool enabled) {
1536  static_assert((detail::optional_v<TagsToChange> && ...),
1537  "set_field_status can only be called with tag types for optional fields");
1538  auto const size = m_indices.size();
1539  (std::get<tag_index_v<TagsToChange>>(m_data).set_active(enabled, size), ...);
1540  }
1541 
1543  detail::AccumulateMemoryUsage accumulator;
1544  auto accumulated = for_each_vector(accumulator);
1545 
1546  return accumulated.usage();
1547  }
1548 
1549  private:
1550  /**
1551  * @brief Throw an exception with a pretty prefix.
1552  * @note The *caller* is expected to hold m_mut when this is called.
1553  */
1554  [[noreturn]] void throw_error(std::string_view message) const {
1555  std::ostringstream oss;
1556  oss << cxx_demangle(typeid(Storage).name()) << "[frozen_count=" << m_frozen_count
1557  << ",sorted=" << std::boolalpha << m_sorted << "]: " << message;
1558  throw std::runtime_error(std::move(oss).str());
1559  }
1560 
1561  /**
1562  * @brief Mutex to protect m_frozen_count and m_sorted.
1563  *
1564  * The frozen tokens are used to detect, possibly concurrent, use of
1565  * incompatible operations, such as sorting while erasing rows. All
1566  * operations that modify the structure of the container must happen
1567  * sequentially.
1568  *
1569  * To prevent a different thread from obtaining a frozen token while this
1570  * thread is modifying structure of the container, this thread should lock
1571  * `m_mut`. Likewise, any thread obtaining a frozen token, should acquire a
1572  * lock on `m_mut` to ensure that there are no concurrent operations that
1573  * require sequential access to the container.
1574  *
1575  * By following this pattern the thread knows that the conditions related
1576  * to sorted-ness and froze-ness of the container are valid for the entire
1577  * duration of the operation (== member function of this class).
1578  *
1579  * Note, enforcing proper sequencing of operations is left to the calling
1580  * code. However, this mutex enforces the required thread-safety to be able
1581  * to detect invalid concurrent access patterns.
1582  */
1583  mutable std::mutex m_mut{};
1584 
1585  /**
1586  * @brief Flag for issue_frozen_token(), mark_as_unsorted() and is_sorted().
1587  */
1588  bool m_sorted{false};
1589 
1590  /**
1591  * @brief Reference count for tokens guaranteeing the container is frozen.
1592  */
1593  std::size_t m_frozen_count{};
1594 
1595  /**
1596  * @brief Pointers to identifiers that record the current physical row.
1597  */
1598  std::vector<non_owning_identifier_without_container> m_indices{};
1599 
1600  /**
1601  * @brief Collection of data columns.
1602  *
1603  * If the tag implements a num_variables() method then it is duplicated a
1604  * runtime-determined number of times and get_field_instance<Tag>(i) returns
1605  * the i-th element of the outer vector (of length num_variables()) of
1606  * vectors. If is no num_variables() method then the outer vector can be
1607  * omitted and get<Tag>() returns a vector of values.
1608  */
1609  std::tuple<detail::field_data<Tags, detail::field_impl_v<Tags>>...> m_data{};
1610 
1611  /**
1612  * @brief Callback that is invoked when the container becomes unsorted.
1613  */
1614  std::function<void()> m_unsorted_callback{};
1615 };
1616 
1617 
1618 template <class Storage, class... Tags>
1620  return data.memory_usage();
1621 }
1622 
1623 } // namespace neuron::container
int cxx_demangle(const char *symbol, char **funcname, size_t *funcname_sz)
void operator()(Tag const &tag, std::vector< typename Tag::type > const &vec, int field_index, int array_dim)
void operator()(detail::index_column_tag const &indices, std::vector< detail::index_column_tag::type > const &vec, int field_index, int array_dim)
#define i
Definition: md1redef.h:19
static double active(void *v)
Definition: cvodeobj.cpp:192
constexpr auto range(T &&iterable)
Definition: enumerate.h:32
#define assert(ex)
Definition: hocassrt.h:24
const char * name
Definition: init.cpp:16
Item * prev(Item *item)
Definition: list.cpp:94
void move(Item *q1, Item *q2, Item *q3)
Definition: list.cpp:200
Item * next(Item *item)
Definition: list.cpp:89
handle_interface< non_owning_identifier< storage > > handle
Non-owning handle to a Mechanism instance.
std::vector< void * > * defer_delete_storage
Defer deleting pointers to deallocated memory.
Definition: container.cpp:95
bool check_permutation_vector(Rng const &range, std::size_t size)
Check if the given range is a permutation of the first N integers.
void defer_delete(std::unique_ptr< T[]> data)
Storage for safe deletion of soa<...> containers.
constexpr FieldImplementation field_impl_v
VectorMemoryUsage compute_defer_delete_storage_size()
constexpr std::size_t index_of_type_v
constexpr bool has_num_variables_v
auto get_name(Tag const &tag, int field_index)
Get the nicest available name for the field_index-th instance of Tag.
size_t get_num_variables(T const &t)
constexpr bool are_types_unique_v
constexpr bool are_types_unique_v< T >
constexpr bool type_in_pack_v
auto get_name_impl(Tag const &tag, int field_index, std::nullptr_t) -> decltype(static_cast< void >(tag.name(field_index)), std::string())
constexpr bool has_default_value_v
auto get_array_dimension(T const &t, std::nullptr_t) -> decltype(t.array_dimension(), 0)
cache::ModelMemoryUsage memory_usage(const std::optional< neuron::cache::Model > &model)
std::string to_string(const T &obj)
static List * info
data_handle< T > transform(data_handle< T > handle, Transform type)
Definition: node.cpp:32
short index
Definition: cabvars.h:11
short type
Definition: cabvars.h:10
static double done(void *v)
Definition: ocbbs.cpp:251
static uint32_t value
Definition: scoprand.cpp:25
std::vector< T > data
Definition: have2want.hpp:39
Memory usage of a storage/soa container.
VectorMemoryUsage stable_identifiers
The memory usage for the stable identifiers in a soa.
VectorMemoryUsage heavy_data
The memory usage of the heavy data in a soa.
Size and capacity in bytes.
size_t size
Number of bytes used.
size_t capacity
Number of bytes allocated.
Stable handle to a generic value.
Definition: data_handle.hpp:61
Tag const & tag() const
Return a reference to the tag instance.
Callable for_each_vector(Callable callable) const
Invoke the given callable for each vector.
int const * array_dims() const
Return a pointer to an array of array dimensions for this tag.
std::vector< std::vector< data_type > > m_storage
Storage for the data associated with Tag.
std::vector< int > m_array_dim_prefix_sums
Prefix sum over array dimensions for the data associated with Tag.
std::vector< int > m_array_dims
Array dimensions of the data associated with Tag.
int const * array_dim_prefix_sums() const
Return a pointer to an array of the prefix sum of array dimensions for this tag.
std::unique_ptr< data_type *[]> m_data_ptrs
Storage where we maintain an up-to-date cache of .data() pointers from m_storage.
data_type *const * data_ptrs() const
Return a pointer to an array of data pointers for this tag.
Callable for_each_vector(Callable callable)
Invoke the given callable for each vector.
void set_active(bool enable, std::size_t size)
std::vector< data_type > m_storage
Storage for the data associated with Tag.
Tag const & tag() const
Return a reference to the tag instance.
std::unique_ptr< data_type *[]> m_data_ptr
Storage where we maintain an up-to-date cache of m_storage.data().
Callable for_each_vector(Callable callable)
Callable for_each_vector(Callable callable) const
int m_array_dim
Array dimension of the data associated with Tag.
data_type *const * data_ptrs() const
std::string_view field() const override
std::string_view container() const override
Struct used to index SoAoS data, such as array range variables.
Non-template stable handle to a generic value.
bool holds() const
Check if this handle refers to the specific type.
T get() const
Explicit conversion to any T.
A non-owning permutation-stable identifier for an entry in a container.
A non-owning permutation-stable identifier for a entry in a container.
An owning permutation-stable identifier for a entry in a container.
Utility for generating SOA data structures.
void set_unsorted_callback(std::function< void()> unsorted_callback)
Set the callback that is invoked when the container becomes unsorted.
owning_identifier< Storage > acquire_owning_identifier()
Create a new entry and return an identifier that owns it.
int const * get_array_dims() const
Get a pointer to an array holding the array dimensions of the fields associated with this tag.
void mark_as_unsorted_impl()
Set m_sorted = false and execute the callback.
bool is_sorted() const
Query if the underlying vectors are still "sorted".
soa & operator=(soa &&)=delete
soa is not move assignable
Callable for_each_vector(Callable callable) const
Apply the given function to const-qualified versions of all vectors.
soa(soa &&)=delete
soa is not movable
soa(soa const &)=delete
soa is not copiable
std::function< void()> m_unsorted_callback
Callback that is invoked when the container becomes unsorted.
bool field_active() const
Query whether the field associated with the given tag is active.
int const * get_array_dim_prefix_sums() const
Get a pointer to an array holding the prefix sum of array dimensions for this tag.
void erase(std::size_t i)
Remove the row from the container.
void decrease_frozen_count()
Flag that the storage is no longer frozen.
void increase_frozen_count()
Record that a state_token was copied.
Tag::type & get_field_instance(std::size_t offset, int field_index, int array_index=0)
Get the offset-th element of the field_index-th instance of the column named by Tag.
Tag::type & get(std::size_t offset)
Get the offset-th element of the column named by Tag.
frozen_token_type issue_frozen_token()
Create a token guaranteeing the container is in "frozen" state.
std::size_t size() const
Get the size of the container.
neuron::container::generic_data_handle find_data_handle(neuron::container::generic_data_handle input_handle) const
Return a permutation-stable handle if ptr is inside us.
Callable for_each_tag_vector_impl(Callable callable) const
std::vector< non_owning_identifier_without_container > m_indices
Pointers to identifiers that record the current physical row.
std::mutex m_mut
Mutex to protect m_frozen_count and m_sorted.
static constexpr bool has_tag_v
soa(Tags... tag_instances)
Construct with specific tag instances.
constexpr Tag const & get_tag() const
Get the instance of the given tag type.
non_owning_identifier< Storage > at(std::size_t offset) const
Get a non-owning identifier to the offset-th entry.
void mark_as_unsorted()
Tell the container it is no longer sorted.
void apply_reverse_permutation(Range permutation, frozen_token_type &sorted_token)
Permute the SoA-format data using an arbitrary range of integers.
Callable for_each_tag_vector_impl(Callable callable) const
void throw_error(std::string_view message) const
Throw an exception with a pretty prefix.
std::unique_ptr< utils::storage_info > find_container_info(void const *cont) const
Check if cont refers to a field in this container.
non_owning_identifier_without_container get_identifier(std::size_t offset) const
Get the offset-th identifier.
std::size_t m_frozen_count
Reference count for tokens guaranteeing the container is frozen.
Tag::type *const * get_data_ptrs() const
Get a pointer to a range of pointers that always point to the start of the contiguous storage.
soa & operator=(soa const &)=delete
soa is not copy assignable
void mark_as_sorted(frozen_token_type &write_token)
Tell the container it is sorted.
Callable for_each_tag_vector_impl(Callable callable)
static constexpr std::size_t tag_index_v
Callable for_each_tag_vector_impl(Callable callable)
soa()
Construct with default-constructed tag type instances.
size_t get_num_variables() const
std::tuple< detail::field_data< Tags, detail::field_impl_v< Tags > >... > m_data
Collection of data columns.
frozen_token_type apply_reverse_permutation(Arg &&permutation)
Permute the SoA-format data using an arbitrary range of integers.
Tag::type const & get(std::size_t offset) const
Get the offset-th element of the column named by Tag.
bool empty() const
Test if the container is empty.
Tag::type const & get_field_instance(std::size_t offset, int field_index, int array_index=0) const
Get the offset-th element of the field_index-th instance of the column named by Tag.
bool is_storage_pointer(typename Tag::type const *ptr) const
Query whether the given pointer-to-vector is the one associated to Tag.
data_handle< typename Tag::type > get_handle(non_owning_identifier_without_container id, int array_index=0) const
Get a handle to the given element of the column named by Tag.
bool m_sorted
Flag for issue_frozen_token(), mark_as_unsorted() and is_sorted().
int get_array_dims(int field_index) const
field_index translate_legacy_index(int legacy_index) const
void set_field_status(bool enabled)
Enable/disable the fields associated with the given tags.
StorageMemoryUsage memory_usage() const
data_handle< typename Tag::type > get_field_instance_handle(non_owning_identifier_without_container id, int field_index, int array_index=0) const
Get a handle to the given element of the field_index-th column named by Tag.
Callable for_each_vector(Callable callable)
Apply the given function to non-const versions of all vectors.
Token whose lifetime manages the frozen state of a container.
~state_token()
Destroy a token, decrementing the frozen count of the underlying container.
constexpr state_token(Container &container)
constexpr state_token & operator=(state_token const &)=delete
Copy assignment.
constexpr state_token(state_token const &other)
Copy a token, incrementing the frozen count of the underlying container.
Interface for obtaining information about model data containers.
Definition: units.cpp:83