Line data Source code
1 : #include "DataHandlerCPU.h"
2 : #include "DataHandlerMapCPU.h"
3 : #include "Error.h"
4 : #include "TypeCasts.hpp"
5 :
6 : #include "DataDescriptor.h"
7 :
8 : #include <iostream>
9 :
10 : #if WITH_FFTW
11 : #define EIGEN_FFTW_DEFAULT
12 : #endif
13 : #include <unsupported/Eigen/FFT>
14 :
15 : namespace elsa
16 : {
17 :
18 : template <typename data_t>
19 : DataHandlerCPU<data_t>::DataHandlerCPU(index_t size)
20 : : _data(std::make_shared<DataVector_t>(size))
21 65708 : {
22 65708 : }
23 :
24 : template <typename data_t>
25 : DataHandlerCPU<data_t>::DataHandlerCPU(DataVector_t const& vector)
26 : : _data{std::make_shared<DataVector_t>(vector)}
27 691 : {
28 691 : }
29 :
30 : template <typename data_t>
31 : DataHandlerCPU<data_t>::DataHandlerCPU(const DataHandlerCPU<data_t>& other)
32 : : _data{other._data}, _associatedMaps{}
33 15791 : {
34 15791 : }
35 :
36 : template <typename data_t>
37 : DataHandlerCPU<data_t>::DataHandlerCPU(DataHandlerCPU<data_t>&& other)
38 : : _data{std::move(other._data)}, _associatedMaps{std::move(other._associatedMaps)}
39 5 : {
40 5 : for (auto& map : _associatedMaps)
41 5 : map->_dataOwner = this;
42 5 : }
43 :
44 : template <typename data_t>
45 : DataHandlerCPU<data_t>::~DataHandlerCPU()
46 82195 : {
47 82195 : for (auto& map : _associatedMaps)
48 5 : map->_dataOwner = nullptr;
49 82195 : }
50 :
51 : template <typename data_t>
52 : index_t DataHandlerCPU<data_t>::getSize() const
53 22160971 : {
54 22160971 : return static_cast<index_t>(_data->size());
55 22160971 : }
56 :
57 : template <typename data_t>
58 : data_t& DataHandlerCPU<data_t>::operator[](index_t index)
59 13537746 : {
60 13537746 : detach();
61 13537746 : return (*_data)[index];
62 13537746 : }
63 :
64 : template <typename data_t>
65 : const data_t& DataHandlerCPU<data_t>::operator[](index_t index) const
66 4186559 : {
67 4186559 : return (*_data)[index];
68 4186559 : }
69 :
70 : template <typename data_t>
71 : data_t DataHandlerCPU<data_t>::dot(const DataHandler<data_t>& v) const
72 641 : {
73 641 : if (v.getSize() != getSize())
74 5 : throw InvalidArgumentError("DataHandlerCPU: dot product argument has wrong size");
75 :
76 : // use Eigen if the other handler is CPU or Map, otherwise use the slow fallback version
77 636 : if (auto otherHandler = downcast_safe<DataHandlerCPU<data_t>>(&v)) {
78 631 : return _data->dot(*otherHandler->_data);
79 631 : } else if (auto otherHandler = downcast_safe<DataHandlerMapCPU<data_t>>(&v)) {
80 5 : return _data->dot(otherHandler->_map);
81 5 : } else {
82 0 : return this->slowDotProduct(v);
83 0 : }
84 636 : }
85 :
86 : template <typename data_t>
87 : GetFloatingPointType_t<data_t> DataHandlerCPU<data_t>::squaredL2Norm() const
88 21533 : {
89 21533 : return _data->squaredNorm();
90 21533 : }
91 :
92 : template <typename data_t>
93 : GetFloatingPointType_t<data_t> DataHandlerCPU<data_t>::l2Norm() const
94 1773 : {
95 1773 : return _data->norm();
96 1773 : }
97 :
98 : template <typename data_t>
99 : index_t DataHandlerCPU<data_t>::l0PseudoNorm() const
100 14 : {
101 14 : using FloatType = GetFloatingPointType_t<data_t>;
102 14 : return (_data->array().cwiseAbs() >= std::numeric_limits<FloatType>::epsilon()).count();
103 14 : }
104 :
105 : template <typename data_t>
106 : GetFloatingPointType_t<data_t> DataHandlerCPU<data_t>::l1Norm() const
107 18 : {
108 18 : return _data->array().abs().sum();
109 18 : }
110 :
111 : template <typename data_t>
112 : GetFloatingPointType_t<data_t> DataHandlerCPU<data_t>::lInfNorm() const
113 18 : {
114 18 : return _data->array().abs().maxCoeff();
115 18 : }
116 :
117 : template <typename data_t>
118 : data_t DataHandlerCPU<data_t>::sum() const
119 10 : {
120 10 : return _data->sum();
121 10 : }
122 :
123 : template <typename data_t>
124 : data_t DataHandlerCPU<data_t>::minElement() const
125 36 : {
126 36 : if constexpr (isComplex<data_t>) {
127 34 : throw LogicError("DataHandlerCPU: minElement of complex type not supported");
128 34 : } else {
129 34 : return _data->minCoeff();
130 34 : }
131 36 : }
132 :
133 : template <typename data_t>
134 : data_t DataHandlerCPU<data_t>::maxElement() const
135 14 : {
136 14 : if constexpr (isComplex<data_t>) {
137 12 : throw LogicError("DataHandlerCPU: maxElement of complex type not supported");
138 12 : } else {
139 12 : return _data->maxCoeff();
140 12 : }
141 14 : }
142 :
143 : template <typename data_t>
144 : DataHandler<data_t>& DataHandlerCPU<data_t>::fft(const DataDescriptor& source_desc,
145 : FFTNorm norm)
146 146 : {
147 146 : this->base_fft<true>(source_desc, norm);
148 146 : return *this;
149 146 : }
150 :
151 : template <typename data_t>
152 : DataHandler<data_t>& DataHandlerCPU<data_t>::ifft(const DataDescriptor& source_desc,
153 : FFTNorm norm)
154 250 : {
155 250 : this->base_fft<false>(source_desc, norm);
156 250 : return *this;
157 250 : }
158 :
159 : template <typename data_t>
160 : DataHandler<data_t>& DataHandlerCPU<data_t>::operator+=(const DataHandler<data_t>& v)
161 8347 : {
162 8347 : if (v.getSize() != getSize())
163 5 : throw InvalidArgumentError("DataHandler: addition argument has wrong size");
164 :
165 8342 : detach();
166 :
167 : // use Eigen if the other handler is CPU or Map, otherwise use the slow fallback version
168 8342 : if (auto otherHandler = downcast_safe<DataHandlerCPU<data_t>>(&v)) {
169 8337 : *_data += *otherHandler->_data;
170 8337 : } else if (auto otherHandler = downcast_safe<DataHandlerMapCPU<data_t>>(&v)) {
171 5 : *_data += otherHandler->_map;
172 5 : } else {
173 0 : this->slowAddition(v);
174 0 : }
175 :
176 8342 : return *this;
177 8342 : }
178 :
179 : template <typename data_t>
180 : DataHandler<data_t>& DataHandlerCPU<data_t>::operator-=(const DataHandler<data_t>& v)
181 11760 : {
182 11760 : if (v.getSize() != getSize())
183 5 : throw InvalidArgumentError("DataHandler: subtraction argument has wrong size");
184 :
185 11755 : detach();
186 :
187 : // use Eigen if the other handler is CPU or Map, otherwise use the slow fallback version
188 11755 : if (auto otherHandler = downcast_safe<DataHandlerCPU<data_t>>(&v)) {
189 11750 : *_data -= *otherHandler->_data;
190 11750 : } else if (auto otherHandler = downcast_safe<DataHandlerMapCPU<data_t>>(&v)) {
191 5 : *_data -= otherHandler->_map;
192 5 : } else {
193 0 : this->slowSubtraction(v);
194 0 : }
195 :
196 11755 : return *this;
197 11755 : }
198 :
199 : template <typename data_t>
200 : DataHandler<data_t>& DataHandlerCPU<data_t>::operator*=(const DataHandler<data_t>& v)
201 25 : {
202 25 : if (v.getSize() != getSize())
203 5 : throw InvalidArgumentError("DataHandler: multiplication argument has wrong size");
204 :
205 20 : detach();
206 :
207 : // use Eigen if the other handler is CPU or Map, otherwise use the slow fallback version
208 20 : if (auto otherHandler = downcast_safe<DataHandlerCPU<data_t>>(&v)) {
209 15 : _data->array() *= otherHandler->_data->array();
210 15 : } else if (auto otherHandler = downcast_safe<DataHandlerMapCPU<data_t>>(&v)) {
211 5 : _data->array() *= otherHandler->_map.array();
212 5 : } else {
213 0 : this->slowMultiplication(v);
214 0 : }
215 :
216 20 : return *this;
217 20 : }
218 :
219 : template <typename data_t>
220 : DataHandler<data_t>& DataHandlerCPU<data_t>::operator/=(const DataHandler<data_t>& v)
221 25 : {
222 25 : if (v.getSize() != getSize())
223 5 : throw InvalidArgumentError("DataHandler: division argument has wrong size");
224 :
225 20 : detach();
226 :
227 : // use Eigen if the other handler is CPU or Map, otherwise use the slow fallback version
228 20 : if (auto otherHandler = downcast_safe<DataHandlerCPU<data_t>>(&v)) {
229 15 : _data->array() /= otherHandler->_data->array();
230 15 : } else if (auto otherHandler = downcast_safe<DataHandlerMapCPU<data_t>>(&v)) {
231 5 : _data->array() /= otherHandler->_map.array();
232 5 : } else {
233 0 : this->slowDivision(v);
234 0 : }
235 :
236 20 : return *this;
237 20 : }
238 :
239 : template <typename data_t>
240 : DataHandlerCPU<data_t>& DataHandlerCPU<data_t>::operator=(const DataHandlerCPU<data_t>& v)
241 65 : {
242 65 : if (v.getSize() != getSize())
243 5 : throw InvalidArgumentError("DataHandler: assignment argument has wrong size");
244 :
245 60 : attach(v._data);
246 60 : return *this;
247 60 : }
248 :
249 : template <typename data_t>
250 : DataHandlerCPU<data_t>& DataHandlerCPU<data_t>::operator=(DataHandlerCPU<data_t>&& v)
251 15 : {
252 15 : if (v.getSize() != getSize())
253 5 : throw InvalidArgumentError("DataHandler: assignment argument has wrong size");
254 :
255 10 : attach(std::move(v._data));
256 :
257 10 : for (auto& map : v._associatedMaps)
258 10 : map->_dataOwner = this;
259 :
260 10 : _associatedMaps.splice(_associatedMaps.end(), std::move(v._associatedMaps));
261 :
262 : // make sure v no longer owns the object
263 10 : v._data.reset();
264 10 : return *this;
265 10 : }
266 :
267 : template <typename data_t>
268 : DataHandler<data_t>& DataHandlerCPU<data_t>::operator+=(data_t scalar)
269 15 : {
270 15 : detach();
271 15 : _data->array() += scalar;
272 15 : return *this;
273 15 : }
274 :
275 : template <typename data_t>
276 : DataHandler<data_t>& DataHandlerCPU<data_t>::operator-=(data_t scalar)
277 15 : {
278 15 : detach();
279 15 : _data->array() -= scalar;
280 15 : return *this;
281 15 : }
282 :
283 : template <typename data_t>
284 : DataHandler<data_t>& DataHandlerCPU<data_t>::operator*=(data_t scalar)
285 514 : {
286 514 : detach();
287 514 : _data->array() *= scalar;
288 514 : return *this;
289 514 : }
290 :
291 : template <typename data_t>
292 : DataHandler<data_t>& DataHandlerCPU<data_t>::operator/=(data_t scalar)
293 15 : {
294 15 : detach();
295 15 : _data->array() /= scalar;
296 15 : return *this;
297 15 : }
298 :
299 : template <typename data_t>
300 : DataHandler<data_t>& DataHandlerCPU<data_t>::operator=(data_t scalar)
301 3721 : {
302 3721 : detach();
303 3721 : _data->setConstant(scalar);
304 3721 : return *this;
305 3721 : }
306 :
307 : template <typename data_t>
308 : std::unique_ptr<DataHandler<data_t>> DataHandlerCPU<data_t>::getBlock(index_t startIndex,
309 : index_t numberOfElements)
310 3337 : {
311 3337 : if (startIndex >= getSize() || numberOfElements > getSize() - startIndex)
312 10 : throw InvalidArgumentError("DataHandler: requested block out of bounds");
313 :
314 3327 : return std::unique_ptr<DataHandlerMapCPU<data_t>>{
315 3327 : new DataHandlerMapCPU{this, _data->data() + startIndex, numberOfElements}};
316 3327 : }
317 :
318 : template <typename data_t>
319 : std::unique_ptr<const DataHandler<data_t>>
320 : DataHandlerCPU<data_t>::getBlock(index_t startIndex, index_t numberOfElements) const
321 210 : {
322 210 : if (startIndex >= getSize() || numberOfElements > getSize() - startIndex)
323 0 : throw InvalidArgumentError("DataHandler: requested block out of bounds");
324 :
325 : // using a const_cast here is fine as long as the DataHandlers never expose the internal
326 : // Eigen objects
327 210 : auto mutableThis = const_cast<DataHandlerCPU<data_t>*>(this);
328 210 : auto mutableData = const_cast<data_t*>(_data->data() + startIndex);
329 210 : return std::unique_ptr<const DataHandlerMapCPU<data_t>>{
330 210 : new DataHandlerMapCPU{mutableThis, mutableData, numberOfElements}};
331 210 : }
332 :
333 : template <typename data_t>
334 : DataHandlerCPU<data_t>* DataHandlerCPU<data_t>::cloneImpl() const
335 15621 : {
336 15621 : return new DataHandlerCPU<data_t>(*this);
337 15621 : }
338 :
339 : template <typename data_t>
340 : bool DataHandlerCPU<data_t>::isEqual(const DataHandler<data_t>& other) const
341 1907 : {
342 1907 : if (const auto otherHandler = downcast_safe<DataHandlerMapCPU<data_t>>(&other)) {
343 :
344 65 : if (_data->size() != otherHandler->_map.size())
345 5 : return false;
346 :
347 60 : if (_data->data() != otherHandler->_map.data() && *_data != otherHandler->_map)
348 10 : return false;
349 :
350 50 : return true;
351 1842 : } else if (const auto otherHandler = downcast_safe<DataHandlerCPU<data_t>>(&other)) {
352 :
353 1842 : if (_data->size() != otherHandler->_data->size())
354 5 : return false;
355 :
356 1837 : if (_data->data() != otherHandler->_data->data() && *_data != *otherHandler->_data)
357 51 : return false;
358 :
359 1786 : return true;
360 1786 : } else
361 0 : return false;
362 1907 : }
363 :
364 : template <typename data_t>
365 : void DataHandlerCPU<data_t>::assign(const DataHandler<data_t>& other)
366 17895 : {
367 :
368 17895 : if (const auto otherHandler = downcast_safe<DataHandlerMapCPU<data_t>>(&other)) {
369 22 : if (getSize() == otherHandler->_dataOwner->getSize()) {
370 10 : attach(otherHandler->_dataOwner->_data);
371 12 : } else {
372 12 : detachWithUninitializedBlock(0, getSize());
373 12 : *_data = otherHandler->_map;
374 12 : }
375 17873 : } else if (const auto otherHandler = downcast_safe<DataHandlerCPU<data_t>>(&other)) {
376 17873 : attach(otherHandler->_data);
377 17873 : } else
378 0 : this->slowAssign(other);
379 17895 : }
380 :
381 : template <typename data_t>
382 : void DataHandlerCPU<data_t>::assign(DataHandler<data_t>&& other)
383 8004 : {
384 8004 : if (const auto otherHandler = downcast_safe<DataHandlerMapCPU<data_t>>(&other)) {
385 20 : if (getSize() == otherHandler->_dataOwner->getSize()) {
386 10 : attach(otherHandler->_dataOwner->_data);
387 10 : } else {
388 10 : detachWithUninitializedBlock(0, getSize());
389 10 : *_data = otherHandler->_map;
390 10 : }
391 7984 : } else if (const auto otherHandler = downcast_safe<DataHandlerCPU<data_t>>(&other)) {
392 7984 : attach(std::move(otherHandler->_data));
393 :
394 7984 : for (auto& map : otherHandler->_associatedMaps)
395 10 : map->_dataOwner = this;
396 :
397 7984 : _associatedMaps.splice(_associatedMaps.end(), std::move(otherHandler->_associatedMaps));
398 :
399 : // make sure v no longer owns the object
400 7984 : otherHandler->_data.reset();
401 7984 : } else
402 0 : this->slowAssign(other);
403 8004 : }
404 :
405 : template <typename data_t>
406 : void DataHandlerCPU<data_t>::detach()
407 14000381 : {
408 14000381 : if (_data.use_count() != 1) {
409 16933 : #pragma omp barrier
410 16933 : #pragma omp single
411 16933 : {
412 16933 : data_t* oldData = _data->data();
413 :
414 : // create deep copy of vector
415 16933 : _data = std::make_shared<DataVector_t>(*_data);
416 :
417 : // modify all associated maps
418 16933 : for (auto map : _associatedMaps)
419 16933 : new (&map->_map) Eigen::Map<DataVector_t>(
420 16933 : _data->data() + (map->_map.data() - oldData), map->getSize());
421 16933 : }
422 16933 : }
423 14000381 : }
424 :
425 : template <typename data_t>
426 : void DataHandlerCPU<data_t>::detachWithUninitializedBlock(index_t startIndex,
427 : index_t numberOfElements)
428 775 : {
429 775 : if (_data.use_count() != 1) {
430 : // allocate new vector
431 74 : auto newData = std::make_shared<DataVector_t>(getSize());
432 :
433 : // copy elements before start of block
434 74 : newData->head(startIndex) = _data->head(startIndex);
435 74 : newData->tail(getSize() - startIndex - numberOfElements) =
436 74 : _data->tail(getSize() - startIndex - numberOfElements);
437 :
438 : // modify all associated maps
439 74 : for (auto map : _associatedMaps)
440 76 : new (&map->_map) Eigen::Map<DataVector_t>(
441 76 : newData->data() + (map->_map.data() - _data->data()), map->getSize());
442 :
443 74 : _data = newData;
444 74 : }
445 775 : }
446 :
447 : template <typename data_t>
448 : void DataHandlerCPU<data_t>::attach(const std::shared_ptr<DataVector_t>& data)
449 18049 : {
450 18049 : data_t* oldData = _data->data();
451 :
452 : // shallow copy
453 18049 : _data = data;
454 :
455 : // modify all associated maps
456 18049 : for (auto& map : _associatedMaps)
457 136 : new (&map->_map) Eigen::Map<DataVector_t>(_data->data() + (map->_map.data() - oldData),
458 136 : map->getSize());
459 18049 : }
460 :
461 : template <typename data_t>
462 : void DataHandlerCPU<data_t>::attach(std::shared_ptr<DataVector_t>&& data)
463 7994 : {
464 7994 : data_t* oldData = _data->data();
465 :
466 : // shallow copy
467 7994 : _data = std::move(data);
468 :
469 : // modify all associated maps
470 7994 : for (auto& map : _associatedMaps)
471 20 : new (&map->_map) Eigen::Map<DataVector_t>(_data->data() + (map->_map.data() - oldData),
472 20 : map->getSize());
473 7994 : }
474 :
475 : template <typename data_t>
476 : typename DataHandlerCPU<data_t>::DataMap_t DataHandlerCPU<data_t>::accessData() const
477 0 : {
478 0 : return DataMap_t(&(_data->operator[](0)), getSize());
479 0 : }
480 :
481 : template <typename data_t>
482 : typename DataHandlerCPU<data_t>::DataMap_t DataHandlerCPU<data_t>::accessData()
483 211024 : {
484 211024 : detach();
485 211024 : return DataMap_t(&(_data->operator[](0)), getSize());
486 211024 : }
487 :
488 : template <typename data_t>
489 : template <bool is_forward>
490 : void DataHandlerCPU<data_t>::base_fft(const DataDescriptor& source_desc, FFTNorm norm)
491 396 : {
492 396 : if constexpr (isComplex<data_t>) {
493 :
494 : // now that we change this datahandler,
495 : // copy-on-write it.
496 0 : this->detach();
497 :
498 0 : const auto& src_shape = source_desc.getNumberOfCoefficientsPerDimension();
499 0 : const auto& src_dims = source_desc.getNumberOfDimensions();
500 :
501 0 : typename DataVector_t::Scalar* this_data = this->_data->data();
502 :
503 : // TODO: fftw variant
504 :
505 : // generalization of an 1D-FFT
506 : // walk over each dimenson and 1d-fft one 'line' of data
507 1188 : for (index_t dim_idx = 0; dim_idx < src_dims; ++dim_idx) {
508 : // jumps in the data for the current dimension's data
509 : // dim_size[0] * dim_size[1] * ...
510 : // 1 for dim_idx == 0.
511 792 : const index_t stride = src_shape.head(dim_idx).prod();
512 :
513 : // number of coefficients for the current dimension
514 792 : const index_t dim_size = src_shape(dim_idx);
515 :
516 : // number of coefficients for the other dimensions
517 : // this is the number of 1d-ffts we'll do
518 : // e.g. shape=[2, 3, 4] and we do dim_idx=2 (=shape 4)
519 : // -> src_shape.prod() == 24 / 4 = 6 == 2*3
520 792 : const index_t other_dims_size = src_shape.prod() / dim_size;
521 :
522 792 : #ifndef EIGEN_FFTW_DEFAULT
523 : // when using eigen+fftw, this corrupts the memory, so don't parallelize.
524 : // error messages may include:
525 : // * double free or corruption (fasttop)
526 : // * malloc_consolidate(): unaligned fastbin chunk detected
527 792 : #pragma omp parallel for
528 792 : #endif
529 : // do all the 1d-ffts along the current dimensions axis
530 792 : for (index_t i = 0; i < other_dims_size; ++i) {
531 :
532 0 : index_t ray_start = i;
533 : // each time i is a multiple of stride,
534 : // jump forward the current+previous dimensions' shape product
535 : // (draw an indexed 3d cube to visualize this)
536 0 : ray_start += (stride * (dim_size - 1)) * ((i - (i % stride)) / stride);
537 :
538 : // this is one "ray" through the volume
539 0 : Eigen::Map<DataVector_t, Eigen::AlignmentType::Unaligned, Eigen::InnerStride<>>
540 0 : input_map(this_data + ray_start, dim_size, Eigen::InnerStride<>(stride));
541 :
542 0 : using inner_t = GetFloatingPointType_t<typename DataVector_t::Scalar>;
543 :
544 0 : Eigen::FFT<inner_t> fft_op;
545 :
546 : // disable any scaling in eigen - normally it does 1/n for ifft
547 0 : fft_op.SetFlag(Eigen::FFT<inner_t>::Flag::Unscaled);
548 :
549 0 : Eigen::Matrix<std::complex<inner_t>, Eigen::Dynamic, 1> fft_in{dim_size};
550 0 : Eigen::Matrix<std::complex<inner_t>, Eigen::Dynamic, 1> fft_out{dim_size};
551 :
552 : // eigen internally copies the fwd input matrix anyway if
553 : // it doesn't have stride == 1
554 0 : fft_in =
555 0 : input_map.block(0, 0, dim_size, 1).template cast<std::complex<inner_t>>();
556 :
557 0 : if (unlikely(dim_size == 1)) {
558 : // eigen kiss-fft crashes for size=1...
559 0 : fft_out = fft_in;
560 0 : } else {
561 : // arguments for in and out _must not_ be the same matrix!
562 : // they will corrupt wildly otherwise.
563 0 : if constexpr (is_forward) {
564 0 : fft_op.fwd(fft_out, fft_in);
565 18068 : if (norm == FFTNorm::FORWARD) {
566 0 : fft_out /= dim_size;
567 18068 : } else if (norm == FFTNorm::ORTHO) {
568 0 : fft_out /= std::sqrt(dim_size);
569 0 : }
570 18068 : } else {
571 0 : fft_op.inv(fft_out, fft_in);
572 26228 : if (norm == FFTNorm::BACKWARD) {
573 26228 : fft_out /= dim_size;
574 >1844*10^16 : } else if (norm == FFTNorm::ORTHO) {
575 0 : fft_out /= std::sqrt(dim_size);
576 0 : }
577 0 : }
578 0 : }
579 :
580 : // we can't directly use the map as fft output,
581 : // since Eigen internally just uses the pointer to
582 : // the map's first element, and doesn't respect stride at all..
583 0 : input_map.block(0, 0, dim_size, 1) = fft_out.template cast<data_t>();
584 0 : }
585 792 : }
586 396 : } else {
587 0 : throw Error{"fft with non-complex input container not supported"};
588 0 : }
589 396 : }
590 :
591 : // ------------------------------------------
592 : // explicit template instantiation
593 : template class DataHandlerCPU<float>;
594 : template class DataHandlerCPU<complex<float>>;
595 : template class DataHandlerCPU<double>;
596 : template class DataHandlerCPU<complex<double>>;
597 : template class DataHandlerCPU<index_t>;
598 : } // namespace elsa
|