11.08., 9:00 - 11:00: Due to updates GitLab will be unavailable for some minutes between 09:00 and 11:00.

Commit b3227b9d authored by Jens Petit's avatar Jens Petit

Add expression templates (#4)

  * using underlying Eigen expression templates
  * scalar operations with expression templates
  * save DataContainer meta info in expressions
  * add unary operators
  * adding enum for DataHandlerMapCPU type
  * added expression templates readme
  * removed operators between DataContainers, scalars and DataHandlers
  * in-place operations using expressions
  * test cases
  * benchmark script
parent 44973887
Pipeline #195464 passed with stages
in 5 minutes and 49 seconds
......@@ -19,6 +19,7 @@ endif ()
option(ELSA_TESTING "Enable building the unit tests" ${ELSA_MASTER_PROJECT})
option(ELSA_CREATE_JUNIT_REPORTS "Enable creating JUnit style reports when running tests" ON)
option(ELSA_COVERAGE "Enable test coverage computation and reporting" OFF)
option(ELSA_BENCHMARKS "Enable elsa benchmark test cases" OFF)
option(GIT_SUBMODULE "Enable updating the submodules during build" ${ELSA_MASTER_PROJECT})
option(ELSA_INSTALL "Enable generating the install targets for make install" ${ELSA_MASTER_PROJECT})
......@@ -87,7 +88,7 @@ if(ELSA_TESTING)
enable_testing()
add_subdirectory(thirdparty/Catch2)
add_custom_target(tests
add_custom_target(tests
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Build and run all the tests.")
......
......@@ -45,6 +45,18 @@ We use a testing style described as [Behaviour-Driven
Development](https://github.com/catchorg/Catch2/blob/master/docs/tutorial.md#bdd-style) (BDD). Please
follow this style when adding new tests.
## Benchmarking
You can use the catch testing framework to do [benchmarking
](https://github.com/catchorg/Catch2/blob/master/docs/benchmarks.md). If so, add your benchmarking
case following this template
```cmake
if(ELSA_BENCHMARKS)
ELSA_TEST(BenchmarkName)
endif()
```
which ensures that the test case is only registered and build if the cmake option was
enabled.
## Style Guide
We use the tool `clang-format` to autoformat our code with the [given style
file](.clang-format). Please make sure that your code is formatted accordingly, otherwise the CI
......
......@@ -9,7 +9,6 @@ Core Type Declarations
.. doxygenfile:: elsa.h
DataDescriptor
==============
......@@ -23,17 +22,14 @@ BlockDescriptor
DataContainer
=============
.. doxygenclass:: elsa::DataContainer
LinearOperator
==============
.. doxygenclass:: elsa::LinearOperator
Implementation Details
======================
......@@ -42,7 +38,6 @@ Cloneable
.. doxygenclass:: elsa::Cloneable
DataHandler
-----------
......@@ -52,3 +47,9 @@ DataHandlerCPU
--------------
.. doxygenclass:: elsa::DataHandlerCPU
Expression
----------
.. mdinclude:: expression_templates.md
.. doxygenclass:: elsa::Expression
In elsa, we are using expression templates for fast and efficient mathematical operations while at the same time having intuitive syntax. This technique is also known as lazy-evaluation which means that computations are delayed until the results are actually needed.
Take the example
```cpp
auto expression = dataContainer1 + dataContainer2;
```
where the type of `expression` is `elsa::Expression<operator+, DataContainer<real_t>, DataContainer<real_t>>`.
The `expression` contains the work *to be done* rather than the actual result. Only when assigning to a `DataContainer` or constructing a new DataContainer, the expression gets evaluated automatically
```cpp
DataContainer result = dataContainer1 + dataContainer2;
```
into `result`.
Nesting is easily possible like
```cpp
auto expression = dataContainer1 + dataContainer2 * expression;
```
which will store an expression tree as the type information.
Please note also that the operators/member functions available on an `Expression` type are different from a `DataContainer`.
An expression has a member function `eval()`. However, calling `eval()` will *not* return a `DataContainer` but depending on the current `DataHandler` the underlying raw data computation result.
If single element-wise access is necessary, it is possible to call `expression.eval()[index]`. Note that this is computational very expensive as the whole expression gets evaluated at every index individually.
......@@ -17,7 +17,9 @@ set(MODULE_HEADERS
DataHandler.h
DataHandlerCPU.h
DataHandlerMapCPU.h
LinearOperator.h)
LinearOperator.h
Expression.h
ExpressionPredicates.h)
# list all the code files of the module
set(MODULE_SOURCES
......@@ -65,4 +67,4 @@ endif(ELSA_TESTING)
registerComponent(${ELSA_MODULE_NAME})
# install the module
InstallElsaModule(${ELSA_MODULE_NAME} ${ELSA_MODULE_TARGET_NAME} ${ELSA_MODULE_EXPORT_TARGET})
\ No newline at end of file
InstallElsaModule(${ELSA_MODULE_NAME} ${ELSA_MODULE_TARGET_NAME} ${ELSA_MODULE_EXPORT_TARGET})
......@@ -13,7 +13,8 @@ namespace elsa
DataContainer<data_t>::DataContainer(const DataDescriptor& dataDescriptor,
DataHandlerType handlerType)
: _dataDescriptor{dataDescriptor.clone()},
_dataHandler{createDataHandler(handlerType, _dataDescriptor->getNumberOfCoefficients())}
_dataHandler{createDataHandler(handlerType, _dataDescriptor->getNumberOfCoefficients())},
_dataHandlerType{handlerType}
{
}
......@@ -22,7 +23,8 @@ namespace elsa
const Eigen::Matrix<data_t, Eigen::Dynamic, 1>& data,
DataHandlerType handlerType)
: _dataDescriptor{dataDescriptor.clone()},
_dataHandler{createDataHandler(handlerType, _dataDescriptor->getNumberOfCoefficients())}
_dataHandler{createDataHandler(handlerType, _dataDescriptor->getNumberOfCoefficients())},
_dataHandlerType{handlerType}
{
if (_dataHandler->getSize() != data.size())
throw std::invalid_argument("DataContainer: initialization vector has invalid size");
......@@ -33,7 +35,9 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>::DataContainer(const DataContainer<data_t>& other)
: _dataDescriptor{other._dataDescriptor->clone()}, _dataHandler{other._dataHandler->clone()}
: _dataDescriptor{other._dataDescriptor->clone()},
_dataHandler{other._dataHandler->clone()},
_dataHandlerType{other._dataHandlerType}
{
}
......@@ -48,6 +52,8 @@ namespace elsa
} else {
_dataHandler = other._dataHandler->clone();
}
_dataHandlerType = other._dataHandlerType;
}
return *this;
......@@ -56,7 +62,8 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>::DataContainer(DataContainer<data_t>&& other) noexcept
: _dataDescriptor{std::move(other._dataDescriptor)},
_dataHandler{std::move(other._dataHandler)}
_dataHandler{std::move(other._dataHandler)},
_dataHandlerType{std::move(other._dataHandlerType)}
{
// leave other in a valid state
other._dataDescriptor = nullptr;
......@@ -67,12 +74,15 @@ namespace elsa
DataContainer<data_t>& DataContainer<data_t>::operator=(DataContainer<data_t>&& other)
{
_dataDescriptor = std::move(other._dataDescriptor);
if (_dataHandler) {
*_dataHandler = std::move(*other._dataHandler);
} else {
_dataHandler = std::move(other._dataHandler);
}
_dataHandlerType = std::move(other._dataHandlerType);
// leave other in a valid state
other._dataDescriptor = nullptr;
other._dataHandler = nullptr;
......@@ -147,30 +157,6 @@ namespace elsa
return _dataHandler->sum();
}
template <typename data_t>
DataContainer<data_t> DataContainer<data_t>::square() const
{
return DataContainer<data_t>(*_dataDescriptor, _dataHandler->square());
}
template <typename data_t>
DataContainer<data_t> DataContainer<data_t>::sqrt() const
{
return DataContainer<data_t>(*_dataDescriptor, _dataHandler->sqrt());
}
template <typename data_t>
DataContainer<data_t> DataContainer<data_t>::exp() const
{
return DataContainer<data_t>(*_dataDescriptor, _dataHandler->exp());
}
template <typename data_t>
DataContainer<data_t> DataContainer<data_t>::log() const
{
return DataContainer<data_t>(*_dataDescriptor, _dataHandler->log());
}
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator+=(const DataContainer<data_t>& dc)
{
......@@ -242,6 +228,8 @@ namespace elsa
switch (handlerType) {
case DataHandlerType::CPU:
return std::make_unique<DataHandlerCPU<data_t>>(std::forward<Args>(args)...);
case DataHandlerType::MAP_CPU:
return std::make_unique<DataHandlerCPU<data_t>>(std::forward<Args>(args)...);
default:
throw std::invalid_argument("DataContainer: unknown handler type");
}
......@@ -249,8 +237,11 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>::DataContainer(const DataDescriptor& dataDescriptor,
std::unique_ptr<DataHandler<data_t>> dataHandler)
: _dataDescriptor{dataDescriptor.clone()}, _dataHandler{std::move(dataHandler)}
std::unique_ptr<DataHandler<data_t>> dataHandler,
DataHandlerType dataType)
: _dataDescriptor{dataDescriptor.clone()},
_dataHandler{std::move(dataHandler)},
_dataHandlerType{dataType}
{
}
......@@ -286,7 +277,8 @@ namespace elsa
const auto& ithDesc = blockDesc->getDescriptorOfBlock(i);
index_t blockSize = ithDesc.getNumberOfCoefficients();
return DataContainer<data_t>{ithDesc, _dataHandler->getBlock(startIndex, blockSize)};
return DataContainer<data_t>{ithDesc, _dataHandler->getBlock(startIndex, blockSize),
DataHandlerType::MAP_CPU};
}
template <typename data_t>
......@@ -305,7 +297,8 @@ namespace elsa
// getBlock() returns a pointer to non-const DH, but that's fine as it gets wrapped in a
// constant container
return DataContainer<data_t>{ithDesc, _dataHandler->getBlock(startIndex, blockSize)};
return DataContainer<data_t>{ithDesc, _dataHandler->getBlock(startIndex, blockSize),
DataHandlerType::MAP_CPU};
}
template <typename data_t>
......@@ -314,7 +307,8 @@ namespace elsa
if (dataDescriptor.getNumberOfCoefficients() != getSize())
throw std::invalid_argument("DataContainer: view must have same size as container");
return DataContainer<data_t>{dataDescriptor, _dataHandler->getBlock(0, getSize())};
return DataContainer<data_t>{dataDescriptor, _dataHandler->getBlock(0, getSize()),
DataHandlerType::MAP_CPU};
}
template <typename data_t>
......@@ -325,7 +319,8 @@ namespace elsa
// getBlock() returns a pointer to non-const DH, but that's fine as it gets wrapped in a
// constant container
return DataContainer<data_t>{dataDescriptor, _dataHandler->getBlock(0, getSize())};
return DataContainer<data_t>{dataDescriptor, _dataHandler->getBlock(0, getSize()),
DataHandlerType::MAP_CPU};
}
template <typename data_t>
......@@ -400,6 +395,28 @@ namespace elsa
return const_reverse_iterator(cbegin());
}
template <typename data_t>
typename DataContainer<data_t>::HandlerTypes_t DataContainer<data_t>::getHandlerPtr() const
{
DataContainer<data_t>::HandlerTypes_t handler;
if (_dataHandlerType == DataHandlerType::CPU) {
handler = static_cast<DataHandlerCPU<data_t>*>(_dataHandler.get());
}
if (_dataHandlerType == DataHandlerType::MAP_CPU) {
handler = static_cast<DataHandlerMapCPU<data_t>*>(_dataHandler.get());
}
return handler;
}
template <typename data_t>
DataHandlerType DataContainer<data_t>::getDataHandlerType() const
{
return _dataHandlerType;
}
// ------------------------------------------
// explicit template instantiation
template class DataContainer<float>;
......
This diff is collapsed.
......@@ -2,6 +2,9 @@
#include "elsaDefines.h"
#include "Cloneable.h"
#include "ExpressionPredicates.h"
#include <Eigen/Core>
namespace elsa
{
......@@ -26,6 +29,20 @@ namespace elsa
template <typename data_t = real_t>
class DataHandler : public Cloneable<DataHandler<data_t>>
{
/// for enabling accessData()
template <class Operand, std::enable_if_t<isDataContainer<Operand>, int>>
friend constexpr auto evaluateOrReturn(Operand const& operand);
/// for enabling accessData()
friend DataContainer<data_t>;
protected:
/// convenience typedef for the Eigen::Matrix data vector
using DataVector_t = Eigen::Matrix<data_t, Eigen::Dynamic, 1>;
/// convenience typedef for the Eigen::Map
using DataMap_t = Eigen::Map<DataVector_t>;
public:
/// return the size of the stored data (i.e. number of elements in linearized data vector)
virtual index_t getSize() const = 0;
......@@ -51,18 +68,6 @@ namespace elsa
/// return the sum of all elements of the data vector
virtual data_t sum() const = 0;
/// return a new DataHandler with element-wise squared values of this one
virtual std::unique_ptr<DataHandler<data_t>> square() const = 0;
/// return a new DataHandler with element-wise square roots of this one
virtual std::unique_ptr<DataHandler<data_t>> sqrt() const = 0;
/// return a new DataHandler with element-wise exponentials of this one
virtual std::unique_ptr<DataHandler<data_t>> exp() const = 0;
/// return a new DataHandler with element-wise logarithms of this one
virtual std::unique_ptr<DataHandler<data_t>> log() const = 0;
/// compute in-place element-wise addition of another vector v
virtual DataHandler<data_t>& operator+=(const DataHandler<data_t>& v) = 0;
......@@ -170,120 +175,11 @@ namespace elsa
/// derived classes should override this method to implement move assignment
virtual void assign(DataHandler<data_t>&& other) = 0;
};
/// element-wise addition of two DataHandlers
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator+(const DataHandler<data_t>& left,
const DataHandler<data_t>& right)
{
auto result = left.clone();
*result += right;
return result;
}
/// element-wise subtraction of two DataHandlers
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator-(const DataHandler<data_t>& left,
const DataHandler<data_t>& right)
{
auto result = left.clone();
*result -= right;
return result;
}
/// element-wise multiplication of two DataHandlers
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator*(const DataHandler<data_t>& left,
const DataHandler<data_t>& right)
{
auto result = left.clone();
*result *= right;
return result;
}
/// element-wise division of two DataHandlers
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator/(const DataHandler<data_t>& left,
const DataHandler<data_t>& right)
{
auto result = left.clone();
*result /= right;
return result;
}
/// addition of DataHandler with scalar
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator+(data_t left, const DataHandler<data_t>& right)
{
auto result = right.clone();
*result += left;
return result;
}
/// addition of scalar with DataHandler
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator+(const DataHandler<data_t>& left, data_t right)
{
auto result = left.clone();
*result += right;
return result;
}
/// subtraction of DataHandler from scalar
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator-(data_t left, const DataHandler<data_t>& right)
{
auto result = right.clone();
*result *= -1;
*result += left;
return result;
}
/// subtraction of scalar from DataHandler
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator-(const DataHandler<data_t>& left, data_t right)
{
auto result = left.clone();
*result -= right;
return result;
}
/// multiplication of DataHandler with scalar
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator*(data_t left, const DataHandler<data_t>& right)
{
auto result = right.clone();
*result *= left;
return result;
}
/// multiplication of scalar with DataHandler
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator*(const DataHandler<data_t>& left, data_t right)
{
auto result = left.clone();
*result *= right;
return result;
}
/// division of scalar by DataHandler
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator/(data_t left, const DataHandler<data_t>& right)
{
auto result = right.clone();
*result = left;
*result /= right;
return result;
}
/// division of DataHandler by scalar
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> operator/(const DataHandler<data_t>& left, data_t right)
{
auto result = left.clone();
*result /= right;
return result;
}
/// derived classes return underlying data
virtual DataMap_t accessData() = 0;
/// derived classes return underlying data
virtual DataMap_t accessData() const = 0;
};
} // namespace elsa
......@@ -98,42 +98,6 @@ namespace elsa
return _data->sum();
}
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> DataHandlerCPU<data_t>::square() const
{
auto result = std::make_unique<DataHandlerCPU<data_t>>(getSize(), false);
*result->_data = _data->array().square();
return result;
}
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> DataHandlerCPU<data_t>::sqrt() const
{
auto result = std::make_unique<DataHandlerCPU<data_t>>(getSize(), false);
*result->_data = _data->array().sqrt();
return result;
}
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> DataHandlerCPU<data_t>::exp() const
{
auto result = std::make_unique<DataHandlerCPU<data_t>>(getSize(), false);
*result->_data = _data->array().exp();
return result;
}
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> DataHandlerCPU<data_t>::log() const
{
auto result = std::make_unique<DataHandlerCPU<data_t>>(getSize(), false);
*result->_data = _data->array().log();
return result;
}
template <typename data_t>
DataHandler<data_t>& DataHandlerCPU<data_t>::operator+=(const DataHandler<data_t>& v)
{
......@@ -450,6 +414,19 @@ namespace elsa
map->getSize());
}
template <typename data_t>
typename DataHandlerCPU<data_t>::DataMap_t DataHandlerCPU<data_t>::accessData() const
{
return DataMap_t(&(_data->operator[](0)), getSize());
}
template <typename data_t>
typename DataHandlerCPU<data_t>::DataMap_t DataHandlerCPU<data_t>::accessData()
{
detach();
return DataMap_t(&(_data->operator[](0)), getSize());
}
// ------------------------------------------
// explicit template instantiation
template class DataHandlerCPU<float>;
......
......@@ -45,10 +45,16 @@ namespace elsa
/// declare DataHandlerMapCPU as friend, allows the use of Eigen for improved performance
friend DataHandlerMapCPU<data_t>;
/// used for testing only and defined in test file
friend long useCount<>(const DataHandlerCPU<data_t>& dh);
protected:
/// convenience typedef for the Eigen::Matrix data vector
using DataVector_t = Eigen::Matrix<data_t, Eigen::Dynamic, 1>;
/// convenience typedef for the Eigen::Map
using DataMap_t = Eigen::Map<DataVector_t>;
public:
/// delete default constructor (having no information makes no sense)
DataHandlerCPU() = delete;
......@@ -104,18 +110,6 @@ namespace elsa
/// return the sum of all elements of the data vector
data_t sum() const override;
/// return a new DataHandler with element-wise squared values of this one
std::unique_ptr<DataHandler<data_t>> square() const override;
/// return a new DataHandler with element-wise square roots of this one
std::unique_ptr<DataHandler<data_t>> sqrt() const override;
/// return a new DataHandler with element-wise exponentials of this one
std::unique_ptr<DataHandler<data_t>> exp() const override;
/// return a new DataHandler with element-wise logarithms of this one
std::unique_ptr<DataHandler<data_t>> log() const override;
/// copy assign another DataHandlerCPU to this, other types handled in assign()
DataHandlerCPU<data_t>& operator=(const DataHandlerCPU<data_t>& v);
......@@ -162,9 +156,6 @@ namespace elsa
std::unique_ptr<const DataHandler<data_t>>
getBlock(index_t startIndex, index_t numberOfElements) const override;
/// used for testing only and defined in test file
friend long useCount<>(const DataHandlerCPU<data_t>& dh);
protected:
/// the vector storing the data
std::shared_ptr<DataVector_t> _data;
......@@ -184,6 +175,12 @@ namespace elsa
/// move the data stored in other if other is of the same type, otherwise copy the data
void assign(DataHandler<data_t>&& other) override;
/// return non-const version of data
DataMap_t accessData() override;
/// return const version of data
DataMap_t accessData() const override;
private:
/// creates the deep copy for the copy-on-write mechanism
void detach();
......
......@@ -98,42 +98,6 @@ namespace elsa
return _map.sum();
}
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> DataHandlerMapCPU<data_t>::square() const
{
auto result = std::make_unique<DataHandlerCPU<data_t>>(getSize(), false);
*result->_data = _map.array().square();
return result;
}
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> DataHandlerMapCPU<data_t>::sqrt() const
{
auto result = std::make_unique<DataHandlerCPU<data_t>>(getSize(), false);
*result->_data = _map.array().sqrt();
return result;
}
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> DataHandlerMapCPU<data_t>::exp() const
{
auto result = std::make_unique<DataHandlerCPU<data_t>>(getSize(), false);
*result->_data = _map.array().exp();
return result;
}
template <typename data_t>
std::unique_ptr<DataHandler<data_t>> DataHandlerMapCPU<data_t>::log() const
{
auto result = std::make_unique<DataHandlerCPU<data_t>>(getSize(), false);
*result->_data = _map.array().log();
return result;
}
template <typename data_t>
DataHandler<data_t>& DataHandlerMapCPU<data_t>::operator+=(const DataHandler<data_t>& v)
{
......@@ -369,6 +333,19 @@ namespace elsa
assign(other);
}
template <typename data_t>
typename DataHandlerMapCPU<data_t>::DataMap_t DataHandlerMapCPU<data_t>::accessData()
{
_dataOwner->detach();
return _map;
}
template <typename data_t>
typename DataHandlerMapCPU<data_t>::DataMap_t DataHandlerMapCPU<data_t>::accessData() const
{
return _map;
}
// ------------------------------------------
// explicit template instantiation
template class DataHandlerMapCPU<float>;
......
......@@ -48,6 +48,7 @@ namespace elsa
protected:
/// convenience typedef for the Eigen::Matrix data vector
using DataVector_t = Eigen::Matrix<data_t, Eigen::Dynamic, 1>;
/// convenience typedef for the Eigen::Map
using DataMap_t = Eigen::Map<DataVector_t>;
......@@ -85,18 +86,6 @@ namespace elsa
/// return the sum of all elements of the data vector
data_t sum() const override;
/// return a new DataHandler with element-wise squared values of this one
std::unique_ptr<DataHandler<data_t>> square() const override;
/// return a new DataHandler with element-wise square roots of this one
std::unique_ptr<DataHandler<data_t>> sqrt() const override;
/// return a new DataHandler with element-wise exponentials of this one
std::unique_ptr<DataHandler<data_t>> exp() const override;
/// return a new DataHandler with element-wise logarithms of this one
std::unique_ptr<DataHandler<data_t>> log() const override;
/// compute in-place element-wise addition of another vector v
DataHandler<data_t>& operator+=(const DataHandler<data_t>& v) override;
......@@ -162,6 +151,12 @@ namespace elsa
void assign(DataHandler<data_t>&& other) override;
/// return non-const version of the data
DataMap_t accessData() override;
/// return const version of the data
DataMap_t accessData() const override;
private:
/**
* \brief Construct a DataHandlerMapCPU referencing a sequential block of data owned by
......@@ -173,5 +168,4 @@ namespace elsa
*/
DataHandlerMapCPU(DataHandlerCPU<data_t>* dataOwner, data_t* data, index_t n);
};
} // namespace elsa