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