Line data Source code
1 0 : \cond NEVER
2 : Distributed under the MIT License.
3 : See LICENSE.txt for details.
4 : \endcond
5 :
6 : # Implementing SpECTRE vectors {#implementing_vectors}
7 :
8 : \tableofcontents
9 :
10 : # Overview of SpECTRE Vectors {#general_structure}
11 :
12 : In SpECTRE, sets of contiguous or related data are stored in specializations of
13 : vector data types. The canonical implementation of this is the `DataVector`,
14 : which is used for storage of a contiguous sequence of doubles which support a
15 : wide variety of mathematical operations and represent data on a grid used during
16 : an evolution or elliptic solve. However, we support the ability to easily
17 : generate similar vector types which can hold data of a different type
18 : (e.g. `std::complex<double>`), or support a different set of mathematical
19 : operations. SpECTRE vector classes are derived from the class template
20 : `VectorImpl`. The remainder of this brief guide gives a description of the tools
21 : for defining additional vector types.
22 :
23 : For reference, all functions described here can also be found in brief in the
24 : Doxygen documentation for VectorImpl.hpp, and a simple reference implementation
25 : can be found in DataVector.hpp and DataVector.cpp.
26 :
27 : # The class definition {#class_definition}
28 :
29 : SpECTRE vector types inherit from vector types implemented in the
30 : high-performance arithmetic library
31 : [Blaze](https://bitbucket.org/blaze-lib/blaze). Using inheritance, SpECTRE
32 : vectors gracefully make use of the math functions defined for the Blaze types,
33 : but can be customized for the specific needs in SpECTRE computations.
34 :
35 : The trio of template parameters for `VectorImpl` are the type of the stored data
36 : (e.g. `double` for `DataVector`), the result type for mathematical operations,
37 : and the static size. The result type is used by Blaze to ensure that only
38 : compatible vector types are used together in mathematical expressions. For
39 : example, a vector representing `double` data on a grid (`DataVector`) cannot be
40 : added to a vector representing spectral coefficients (`ModalVector`). This
41 : avoids subtle bugs that arise when vector types are unintentionally mixed.
42 : In nearly all cases the result type will be the vector type that is being
43 : defined, so, for instance, `DataVector` is a derived class of
44 : `VectorImpl<double, DataVector, 5>`. This template pattern is known as the
45 : ["Curiously Recurring Template Pattern"](https://en.wikipedia.org/wiki/
46 : Curiously_recurring_template_pattern) (CRTP).
47 : The static size is used as an optimization for small vector sizes. If your
48 : vector is small, rather than doing heap allocations, it will use stack
49 : allocations in order to be efficient. The default static size is set by a global
50 : constexpr bool `default_vector_impl_static_size`.
51 :
52 : For the Blaze system to use the CRTP inheritance appropriately, it requires the
53 : specification of separate type traits in the `blaze` namespace.
54 : These traits can usually be declared in a standard form, so are abstracted in a
55 : macro. For any new vector `MyNewVector`, the pattern that must appear at the
56 : beginning of the file (i.e. before the class definition) is:
57 : ```
58 : /// \cond
59 : class MyNewVector;
60 : /// \endcond
61 : namespace blaze {
62 : DECLARE_GENERAL_VECTOR_BLAZE_TRAITS(MyNewVector);
63 : }
64 : ```
65 : The class template `VectorImpl` defines various constructors, assignment
66 : operators, and iterator generation members. Most of these are inherited from
67 : Blaze types, but in addition, the methods `set_data_ref`, and `pup` are defined
68 : for use in SpECTRE. All except for the assignment operators and constructors
69 : will be implicitly inherited from `VectorImpl`. The assignment and constructors
70 : may be inherited calling the following alias code in the vector class
71 : definition:
72 : ```
73 : using VectorImpl<T,VectorType,StaticSize>::operator=;
74 : using VectorImpl<T,VectorType,StaticSize>::VectorImpl;
75 : ```
76 :
77 : Only the mathematical operations supported on the base Blaze types are supported
78 : by default. Those operations are determined by the storage type `T` and by the
79 : Blaze library. See [blaze-wiki/Vector_Operations](https://bitbucket.org/
80 : blaze-lib/blaze/wiki/Vector%20Operations).
81 :
82 : # Allowed operator specification {#blaze_definitions}
83 :
84 : Blaze keeps track of the return type of unary and binary operations using "type
85 : trait" structs. These specializations for vector types should be placed in the
86 : header file associated with the `VectorImpl` specialization. For `DataVector`,
87 : the specializations are defined in `DataStructures/DataVector.hpp`. The presence
88 : or absence of template specializations of these structs also determines the set
89 : of allowed operations between the vector type and other types. For example, if
90 : adding a `double` to a `DataVector` should be allowed and the result should be
91 : treated as a `DataVector` for subsequent operations, then the struct
92 : `blaze::AddTrait<DataVector, double>` needs to be defined as follows:
93 :
94 : ```
95 : namespace blaze {
96 : // the `template <>` head tells the compiler that
97 : // `AddTrait<DataVector, double>` is a class template specialization
98 : template <>
99 : struct AddTrait<DataVector, double> {
100 : // the `Type` alias tells blaze that the result should be treated like a
101 : // `DataVector` for any further operations
102 : using Type = DataVector;
103 : };
104 : } // namespace blaze
105 : ```
106 :
107 : Note that this only adds support for `DataVector + double`, not `double +
108 : DataVector`. To get the latter the following AddTrait specialization must be
109 : defined
110 :
111 : ```
112 : namespace blaze {
113 : // the `template <>` head tells the compiler that
114 : // `AddTrait<double, DataVector>` is a class template specialization
115 : template <>
116 : struct AddTrait<double, DataVector> {
117 : // the `Type` alias tells blaze that the result should be treated like a
118 : // `DataVector` for any further operations
119 : using Type = DataVector;
120 : };
121 : } // namespace blaze
122 : ```
123 :
124 : Four helper macros are defined to assist with generating the many
125 : specializations that binary operations may require. Both of these macros must be
126 : put inside the blaze namespace for them to work correctly.
127 :
128 : The first of these helper macros is
129 : `BLAZE_TRAIT_SPECIALIZE_BINARY_TRAIT(VECTOR_TYPE, BLAZE_MATH_TRAIT)`, which will
130 : define all of the pairwise operations (`BLAZE_MATH_TRAIT`) for the vector type
131 : (`VECTOR_TYPE`) with itself and for the vector type with its `value_type`. This
132 : reduces the three specializations similar to the above code blocks to a single
133 : line call,
134 :
135 : ```
136 : namespace blaze {
137 : BLAZE_TRAIT_SPECIALIZE_BINARY_TRAIT(DataVector, AddTrait)
138 : } // namespace blaze
139 : ```
140 :
141 : The second helper macro is provided to easily define all of the arithmetic
142 : operations that will typically be supported for a vector type with its value
143 : type. The macro is
144 : `VECTOR_BLAZE_TRAIT_SPECIALIZE_ARITHMETIC_TRAITS(VECTOR_TYPE)`, and defines all
145 : of:
146 : - `IsVector<VECTOR_TYPE>` to `std::true_type`
147 : - `TransposeFlag<VECTOR_TYPE>`, which informs Blaze of the interpretation of the
148 : data as a "column" or "row" vector
149 : - `AddTrait` for the `VECTOR_TYPE` and its value type (3 Blaze struct
150 : specializations)
151 : - `SubTrait` for the `VECTOR_TYPE` and its value type (3 Blaze struct
152 : specializations)
153 : - `MultTrait` for the `VECTOR_TYPE` and its value type (3 Blaze struct
154 : specializations)
155 : - `DivTrait` for the `VECTOR_TYPE` and its value type (3 Blaze struct
156 : specializations)
157 :
158 : This macro is similarly intended to be used in the `blaze` namespace and can
159 : substantially simplify these specializations for new vector types. For instance,
160 : the call for `DataVector` is:
161 :
162 : ```
163 : namespace blaze {
164 : VECTOR_BLAZE_TRAIT_SPECIALIZE_ARITHMETIC_TRAITS(DataVector)
165 : } // namespace blaze
166 : ```
167 :
168 : The third helper macro is provided to define a combination of Blaze traits for
169 : symmetric operations of a vector type with a second type (which may or may not
170 : be a vector type). The macro is
171 : `BLAZE_TRAIT_SPECIALIZE_COMPATIBLE_BINARY_TRAIT(VECTOR, COMPATIBLE, TRAIT)`, and
172 : defines the appropriate trait for the two combinations `<VECTOR, COMPATIBLE>`
173 : and `<COMPATIBLE, VECTOR>`, and defines the result type to be `VECTOR`. For
174 : instance, to support the multiplication of a `ComplexDataVector` with a
175 : `DataVector` and have the result be a `ComplexDataVector`, the following macro
176 : call should be included in the `blaze` namespace:
177 :
178 : ```
179 : namespace blaze {
180 : BLAZE_TRAIT_SPECIALIZE_COMPATIBLE_BINARY_TRAIT(ComplexDataVector, DataVector,
181 : MultTrait);
182 : } // namespace blaze
183 : ```
184 :
185 : Finally, the fourth helper macro is provided to define all of the blaze traits
186 : which are considered either unary or binary maps. This comprises most named
187 : unary functions (like `sin()` or `sqrt()`) and named binary functions (like
188 : `hypot()` and `atan2()`). The macro
189 : `VECTOR_BLAZE_TRAIT_SPECIALIZE_ALL_MAP_TRAITS(VECTOR_TYPE)` broadly specializes
190 : all blaze-defined maps in which the given `VECTOR_TYPE` as the sole argument
191 : (for unary maps) or both arguments (for binary maps). This macro is also
192 : intended to be used in the blaze namespace. The call for `DataVector` is:
193 :
194 : ```
195 : namespace blaze {
196 : VECTOR_BLAZE_TRAIT_SPECIALIZE_ALL_MAP_TRAITS(DataVector)
197 : } // namespace blaze
198 : ```
199 :
200 : # Supporting operations for `std::array`s of vectors {#array_vector_definitions}
201 :
202 : In addition to operations between SpECTRE vectors, it is useful to gracefully
203 : handle operations between `std::arrays` of vectors element-wise. There are
204 : general macros defined for handling operations between array specializations:
205 : `DEFINE_STD_ARRAY_BINOP` and `DEFINE_STD_ARRAY_INPLACE_BINOP` from
206 : `Utilities/StdArrayHelpers.hpp`.
207 :
208 : In addition, there is a macro for rapidly generating addition and subtraction
209 : between arrays of vectors and arrays of their data types. The macro
210 : `MAKE_STD_ARRAY_VECTOR_BINOPS(VECTOR_TYPE)` will define:
211 : - the element-wise `+` and `-` with `std::array<VECTOR_TYPE, N>` and
212 : `std::array<VECTOR_TYPE, N>`
213 : - the element-wise `+` and `-` of either ordering of
214 : `std::array<VECTOR_TYPE, N>` with `std::array<VECTOR_TYPE::value_type, N>`
215 : - the `+=` and `-=` of `std::array<VECTOR_TYPE, N>` with a
216 : `std::array<VECTOR_TYPE, N>`
217 : - the `+=` and `-=` of `std::array<VECTOR_TYPE, N>` with a
218 : `std::array<VECTOR_TYPE::value_type, N>`.
219 :
220 : # Equivalence operators {#Vector_type_equivalence}
221 :
222 : Equivalence operators are supported by the Blaze type inheritance. The
223 : equivalence operator `==` evaluates to true on a pair of vectors if they are the
224 : same size and contain the same values, regardless of ownership.
225 :
226 : # MakeWithValueImpl {#Vector_MakeWithValueImpl}
227 :
228 : SpECTRE offers the convenience function `make_with_value` for various types. The
229 : typical behavior for a SpECTRE vector type is to create a new vector type of the
230 : same type and length initialized with the value provided as the second argument
231 : in all entries. This behavior may be created by placing the macro
232 : `MAKE_WITH_VALUE_IMPL_DEFINITION_FOR(VECTOR_TYPE)` in the .hpp file. Any other
233 : specializations of `MakeWithValueImpl` will need to be written manually.
234 :
235 : # Interoperability with other data types {#Vector_tensor_and_variables}
236 :
237 : When additional vector types are added, small changes are necessary if they are
238 : to be used as the base container type either for `Tensor`s or for `Variables` (a
239 : `Variables` contains `Tensor`s), which contain some vector type.
240 :
241 : In `Tensor.hpp`, there is a `static_assert` which white-lists the possible types
242 : that can be used as the storage type in `Tensor`s. Any new vectors must be added
243 : to that white-list if they are to be used within `Tensor`s.
244 :
245 : `Variables` is templated on the storage type of the stored `Tensor`s. However,
246 : any new data type should be appropriately tested. New vector types should be
247 : tested by invoking new versions of existing testing functions templated on the
248 : new vector type, rather than `DataVector`.
249 :
250 : # Writing tests {#Vector_tests}
251 :
252 : In addition to the utilities for generating new vector types, there are a number
253 : of convenience functions and utilities for easily generating the tests necessary
254 : to verify that the vectors function appropriately. These utilities are in
255 : `VectorImplTestHelper.hpp`, and documented individually in the
256 : TestingFrameworkGroup. Presented here are the salient details for rapidly
257 : assembling basic tests for vectors.
258 :
259 : ## Utility check functions
260 : Each of these functions is intended to encapsulate a single frequently used unit
261 : test and is templated (in order) on the vector type and the value type to be
262 : generated. The default behavior is to uniformly sample values between -100 and
263 : 100, but alternative bounds may be passed in via the function arguments.
264 :
265 : ### `TestHelpers::VectorImpl::vector_test_construct_and_assign()`
266 : This function tests a battery of construction and assignment operators for the
267 : vector type.
268 :
269 : ### `TestHelpers::VectorImpl::vector_test_serialize()`
270 : This function tests that vector types can be serialized and deserialized,
271 : retaining their data.
272 :
273 : ### `TestHelpers::VectorImpl::vector_test_ref()`
274 : This function tests the `set_data_ref` method of sharing data between vectors,
275 : and that the appropriate owning flags and move operations are handled correctly.
276 :
277 : ### `TestHelpers::VectorImpl::vector_test_math_after_move()`
278 : Tests several combinations of math operations and ownership before and after use
279 : of `std::move`.
280 :
281 : ### `TestHelpers::VectorImpl::vector_ref_test_size_error()`
282 : This function intentionally generates an error when assigning values from one
283 : vector to a differently sized, non-owning vector (made non-owning by use of
284 : `set_data_ref`). The assertion test which calls this function should search for
285 : the string "Must copy/move/assign into same size". Three forms of the test are
286 : provided, which are switched between using a value from the enum
287 : `RefSizeErrorTestKind` in the first function argument:
288 : - `RefSizeErrorTestKind::Copy`: tests that the size error is appropriately
289 : generated when copying to a non-owning vector of the wrong size. This has
290 : "copy" in the message.
291 : - `RefSizeErrorTestKind::ExpressionAssign`: tests that the size error is
292 : appropriately generated when assigning the result of a mathematical expression
293 : to a non-owning vector of the wrong size. This has "assign" in the message.
294 : - `RefSizeErrorTestKind::Move`: tests that the size error is appropriately
295 : generated when a vector is `std::move`d into a non-owning vector of the wrong
296 : size. This has "move" in the message.
297 :
298 : ## `TestHelpers::VectorImpl::test_functions_with_vector_arguments()`
299 :
300 : This is a general function for testing the mathematical operation of vector
301 : types with other vector types and/or their base types, with or without various
302 : reference wrappers. This may be used to efficiently test the full set of
303 : permitted math operations on a vector. See the documentation of
304 : `test_functions_with_vector_arguments()` for full usage details.
305 :
306 : An example simple use case for the math test utility:
307 : \snippet Test_DataVector.cpp test_functions_with_vector_arguments_example
308 :
309 : More use cases of this functionality can be found in `Test_DataVector.cpp`.
310 :
311 : # Vector storage nuts and bolts {#Vector_storage}
312 :
313 : Internally, all vector classes inherit from the templated `VectorImpl`, which
314 : inherits from a `blaze::CustomVector`. Most of the mathematical operations are
315 : supported through the Blaze inheritance, which ensures that the math operations
316 : execute the optimized forms in Blaze.
317 :
318 : Blaze also offers the possibility of restricting operations via `groups` in the
319 : `blaze::CustomVector` template arguments.
320 : Currently, we do not use the `blaze::GroupTag` functionality to determine
321 : available operations for vectors, but in principle this feature could allow us
322 : to further simplify our operator choice logic in the SpECTRE vector code.
323 :
324 : SpECTRE vectors can be either "owning" or "non-owning". If a vector is owning,
325 : it allocates and controls the data it has access to, and is responsible for
326 : eventually freeing that data when the vector goes out of scope. If the vector is
327 : non-owning, it acts as a (possibly complete) "view" of otherwise allocated
328 : memory. Non-owning vectors do not manage memory, nor can they change size. The
329 : two cases of data ownership cause the underlying data to be handled fairly
330 : differently, so we will discuss each in turn.
331 :
332 : When a SpECTRE vector is constructed as owning, or becomes owning, its memory
333 : is allocated in one of two ways.
334 :
335 : 1. The size of the vector is larger than the `StaticSize` template parameter to
336 : `VectorImpl`. In that case, it allocates its own block of memory of
337 : appropriate size, and stores a pointer to that memory in a `std::unique_ptr`
338 : named `owned_data_`. The `std::unique_ptr` ensures that the SpECTRE vector
339 : needs to perform no further direct memory management, and that the memory
340 : will be appropriately managed whenever the `std::unique_ptr owned_data_`
341 : member is deleted or moved.
342 : 2. The size of the vector is less than or equal to the `StaticSize` template. In
343 : this case, the data is stored on the stack in a `std::array<T, StaticSize>`
344 : member variable called `static_owned_data_`. Since it is on the stack, this
345 : doesn't require any memory management by the user.
346 :
347 : In either case, the base `blaze::CustomVector` must also be told about the
348 : pointer, which is always accomplished by calling the protected function
349 : `VectorImpl.reset_pointer_vector(const size_t set_size)`, which sets the
350 : `blaze::CustomVector` internal pointer to either the pointer obtained by
351 : `std::unique_pointer.get()` or the pointer obtained by `std::array.data()`
352 : depending on the size of the vector.
353 :
354 : When a SpECTRE vector is constructed as non-owning by the `VectorImpl(ValueType*
355 : start, size_t set_size)` constructor, or becomes non-owning by the
356 : `set_data_ref` function, neither the internal `std::unique_ptr` named
357 : `owned_data_` nor the internal `std::array` named `static_owned_data_`
358 : points to the data represented by the vector and both can be thought of as
359 : "inactive" for the purposes of computation and memory management. This behavior
360 : is desirable, because otherwise the `std::unique_ptr` would attempt to free
361 : memory that is presumed to be also used elsewhere, causing difficult to diagnose
362 : memory errors. And we needn't worry about the `std::array` because it's
363 : allocated on the stack. The non-owning SpECTRE vector updates the base
364 : `blaze::CustomVector` pointer directly by calling `blaze::CustomVector.reset`
365 : from the derived class (on itself).
|