Custom Data Type

Issue #349 resolved
ludkinm created an issue

The wiki page: https://bitbucket.org/blaze-lib/blaze/wiki/Vector%20and%20Matrix%20Customization#!custom-data-types describes the steps to get a custom type custom::double_t working with Blaze.

The page says Blaze assumes operator+(),operator-() , operator*() and operator/()are implemented for such a custom type.

I’ve come up with a few additional operators that seem to be used in Blaze internals (and are sensible for numerical types) to get some basic Blaze functions to work as documented with a custom::double_t type.

However, some things (like evaluate and type-traits) needed a hack.

Is there support for custom numerical types? If so what other operators/functions are required to get the use of the full Blaze library?

Comments (4)

  1. Klaus Iglberger

    Hi!

    Thanks for creating this issue. Yes, Blaze provides support for custom data types and is even designed to provide as much customizability as possible. However, the wiki obviously does not explain all details and requirements properly. We will improve this as quickly as possible.

    Unfortunately you have also encountered a couple of shortcomings. For that we apologize. We have already fixed all problems that you encountered: First, commit bb0c0c7 fixes the normalize() functions for dense and sparse vectors, which haven’t been updated in a long time and also haven’t been tested in combination with custom data types. Second, you should not have to provide any evaluate() function for a custom type and no conj() function for a non-complex custom type. Commits 504957c and f3ac893 fix the evaluate() and conj() functions, respectively, such that you’re no longer required to add these functions for your custom type.

    With these changes, the following custom type should provide everything you need to compile and run your minimum example:

    namespace custom {
    
    struct double_t
    {
       constexpr double_t() = default;
       constexpr double_t( double i ) : value( i ) {}
       double value{};
    };
    
    constexpr bool operator==( double_t lhs, double_t rhs ) noexcept { return lhs.value == rhs.value; }
    constexpr bool operator!=( double_t lhs, double_t rhs ) noexcept { return !( lhs == rhs ); }
    constexpr bool operator<( double_t lhs, double_t rhs ) noexcept { return lhs.value < rhs.value; }
    constexpr bool operator>( double_t lhs, double_t rhs ) noexcept { return rhs.value < lhs.value; }
    constexpr bool operator<=( double_t lhs, double_t rhs ) noexcept { return !( rhs < lhs ); }
    constexpr bool operator>=( double_t lhs, double_t rhs ) noexcept { return !( lhs < rhs ); }
    
    constexpr double_t& operator+=( double_t& lhs, double_t rhs ) noexcept { lhs.value += rhs.value; return lhs; }
    constexpr double_t& operator-=( double_t& lhs, double_t rhs ) noexcept { lhs.value -= rhs.value; return lhs; }
    constexpr double_t& operator*=( double_t& lhs, double_t rhs ) noexcept { lhs.value *= rhs.value; return lhs; }
    constexpr double_t& operator/=( double_t& lhs, double_t rhs ) noexcept { lhs.value /= rhs.value; return lhs; }
    
    constexpr double_t operator+( double_t lhs, double_t rhs ) noexcept { return double_t{ lhs.value + rhs.value }; }
    constexpr double_t operator-( double_t lhs, double_t rhs ) noexcept { return double_t{ lhs.value - rhs.value }; }
    constexpr double_t operator*( double_t lhs, double_t rhs ) noexcept { return double_t{ lhs.value * rhs.value }; }
    constexpr double_t operator/( double_t lhs, double_t rhs ) noexcept { return double_t{ lhs.value / rhs.value }; }
    
    inline    double_t abs ( double_t d ) noexcept { return double_t{ std::abs ( d.value ) }; }
    inline    double_t sin ( double_t d ) noexcept { return double_t{ std::sin ( d.value ) }; }
    inline    double_t sqrt( double_t d ) noexcept { return double_t{ std::sqrt( d.value ) }; }
    constexpr double_t inv ( double_t d ) noexcept { return double_t{ 1.0/d.value }; }
    
    constexpr double_t min( double_t lhs, double_t rhs ) noexcept { return double_t{ blaze::min( lhs.value, rhs.value ) }; }
    constexpr double_t max( double_t lhs, double_t rhs ) noexcept { return double_t{ blaze::max( lhs.value, rhs.value ) }; }
    
    inline std::ostream& operator<<( std::ostream& os, double_t i )
    {
       return os << i.value;
    }
    
    } // namespace custom
    
    namespace blaze {
    
    template<>
    struct IsNumeric< custom::double_t > : TrueType {};
    
    } // namespace blaze
    

    Please feel free to report any other problem that you encounter with custom types. We appreciate the help!

    We will resolve this issue once the tutorial and wiki have been updated to reflect the current state. Thanks again for creating this issue,

    Best regards,

    Klaus!

  2. Klaus Iglberger

    Summary

    The documentation has been updated to reflect the current state of the Blaze library with regard to custom data types. Also, we've introduced several small improvements that make it even simpler to use custom data types (e.g. no need to deal with the IsNumeric type trait). The updated tutorial is immediately available via cloning the Blaze repository. The updated wiki will be released together with Blaze 3.8.

    Custom Data Types

    Introduction

    The Blaze library is not restricted to integral, floating point and complex data types (called numeric types in Blaze), but it supports custom data types. For instance, the following example demonstrates that it is possible to use std::string as data type:

    blaze::DynamicVector<std::string> a{ "Hello, ", "Blaze " , "Expression" };
    blaze::DynamicVector<std::string> b{ "World"  , "Library", " Templates" };
    
    const auto c( evaluate( a + b ) );
    std::cout <<  "c =\n" << c << "\n\n";
    
    const std::string maxString( max( c ) );
    std::cout << "maxString = " << std::quoted(maxString) << "\n";
    

    Output:

    c =
    ( Hello, World )
    ( Blaze Library )
    ( Expression Templates )
    
    maxString = "Hello, World"
    

    Blaze tries hard to make the use of custom data types as convenient, easy and intuitive as possible. In order to work flawlessly with Blaze, custom data types are required to provide a certain interface (depending on the operations that the type is used for). The following sections give an overview of the necessary steps to enable the use of the hypothetical custom data type custom::double_t for vector and matrix operations.

    namespace custom {
    
    struct double_t
    {
       constexpr double_t() = default;
       constexpr double_t( double i ) : value( i ) {}
       double value{};
    };
    
    } // namespace custom
    

    Arithmetic Operations

    The Blaze library assumes that a custom data type provides operator<<() for streaming, operator+=() and operator+() for additions (which for instance includes additions inside matrix/vector multiplications, matrix/matrix multiplications, reduction or norm operations), operator-=() and operator-() for subtractions, operator*=() and operator*() for multiplications and operator/=() and operator/() for divisions:

    namespace custom {
    
    constexpr double_t& operator+=( double_t& lhs, double_t rhs ) noexcept { lhs.value += rhs.value; return lhs; }
    constexpr double_t& operator-=( double_t& lhs, double_t rhs ) noexcept { lhs.value -= rhs.value; return lhs; }
    constexpr double_t& operator*=( double_t& lhs, double_t rhs ) noexcept { lhs.value *= rhs.value; return lhs; }
    constexpr double_t& operator/=( double_t& lhs, double_t rhs ) noexcept { lhs.value /= rhs.value; return lhs; }
    
    constexpr double_t operator+( double_t lhs, double_t rhs ) noexcept { return double_t{ lhs.value + rhs.value }; }
    constexpr double_t operator-( double_t lhs, double_t rhs ) noexcept { return double_t{ lhs.value - rhs.value }; }
    constexpr double_t operator*( double_t lhs, double_t rhs ) noexcept { return double_t{ lhs.value * rhs.value }; }
    constexpr double_t operator/( double_t lhs, double_t rhs ) noexcept { return double_t{ lhs.value / rhs.value }; }
    
    inline std::ostream& operator<<( std::ostream& os, double_t d )
    {
       return os << d.value;
    }
    
    } // namespace custom
    

    Example:

    int main()
    {
       blaze::DynamicVector<custom::double_t> a{ 1.0, 2.0, 3.0, 4.0 };
       blaze::DynamicVector<custom::double_t> b{ 0.1, 0.2, 0.3, 0.4 };
    
       std::cout << "a + b =\n" << ( a + b ) << "\n";
       std::cout << "a * b =\n" << ( a * b ) << "\n";
    
       std::cout << "sum(a) = " << sum(a) << "\n"
                 << "prod(a) = " << prod(a) << "\n";
    }
    

    Output:

    a + b =
    (         1.1 )
    (         2.2 )
    (         3.3 )
    (         4.4 )
    
    a * b =
    (         0.1 )
    (         0.4 )
    (         0.9 )
    (         1.6 )
    
    sum(a) = 10
    prod(a) = 24
    

    Note that similar steps are necessary if several custom data types are combined (as for instance custom::double_t and custom::float_t). Note that in this case both permutations need to be taken into account:

    custom::double_t operator+( const custom::double_t& a, const custom::float_t& b );
    custom::double_t operator+( const custom::float_t& a, const custom::double_t& b );
    // ...
    

    Please note that only built-in data types apply for vectorization and thus custom data types cannot achieve maximum performance!

    Relational Operations

    In order to compare the element type, Blaze expects the equality operator (i.e. operator==()) and the inequality operator (i.e. operator!=()). Alternatively it is possible to provide an equal() function, which distinguishes between strict and relaxed comparison:

    namespace custom {
    
    constexpr bool operator==( double_t lhs, double_t rhs ) noexcept { return lhs.value == rhs.value; }
    constexpr bool operator!=( double_t lhs, double_t rhs ) noexcept { return !( lhs == rhs ); }
    
    template< blaze::RelaxationFlag RF >
    constexpr bool equal( double_t lhs, double_t rhs ) noexcept { return blaze::equal<RF>( lhs.value, rhs.value ); }
    
    } // namespace custom
    

    Example:

    int main()
    {
       blaze::DynamicVector<custom::double_t> a{ 1.0, 2.0, 3.0, 4.0 };
       blaze::DynamicVector<custom::double_t> b{ 0.1, 0.2, 0.3, 0.4 };
    
       std::cout << "a == b: " << ( a == b ) << "\n"
                 << "a != b: " << ( a != b ) << "\n";
    }
    

    Output:

    a == b: 0
    a != b: 1
    

    Elementwise Operations

    For the different kinds of elementwise operations on vectors and matrices (abs(), sin(), cos(), sqrt(), log(), exp(), min(), max(), ...), the custom type is required to provide the according function overload. Note that the sqrt() operation may also be required for several norm computations. Also, for any inversion operation, the type is required to suport the inv() function:

    namespace custom {
    
    inline    double_t abs ( double_t d ) noexcept { return double_t{ std::abs ( d.value ) }; }
    inline    double_t sin ( double_t d ) noexcept { return double_t{ std::sin ( d.value ) }; }
    inline    double_t cos ( double_t d ) noexcept { return double_t{ std::cos ( d.value ) }; }
    inline    double_t sqrt( double_t d ) noexcept { return double_t{ std::sqrt( d.value ) }; }
    inline    double_t log ( double_t d ) noexcept { return double_t{ std::log ( d.value ) }; }
    inline    double_t exp ( double_t d ) noexcept { return double_t{ std::exp ( d.value ) }; }
    constexpr double_t inv ( double_t d ) noexcept { return double_t{ 1.0/d.value }; }
    
    constexpr double_t min( double_t lhs, double_t rhs ) noexcept { return double_t{ blaze::min( lhs.value, rhs.value ) }; }
    constexpr double_t max( double_t lhs, double_t rhs ) noexcept { return double_t{ blaze::max( lhs.value, rhs.value ) }; }
    
    } // namespace custom
    

    Example:

    int main()
    {
       blaze::DynamicVector<custom::double_t> a{ 1.0, 2.0, 3.0, 4.0 };
       blaze::DynamicVector<custom::double_t> b{ 0.1, 0.2, 0.3, 0.4 };
    
       std::cout << "abs(a) =\n" << abs(a) << "\n";
       std::cout << "sin(a) =\n" << sin(a) << "\n";
       std::cout << "cos(a) =\n" << cos(a) << "\n";
       std::cout << "sqrt(a) =\n" << sqrt(a) << "\n";
       std::cout << "log(a) =\n" << log(a) << "\n";
       std::cout << "exp(a) =\n" << exp(a) << "\n\n";
       std::cout << "min(a) =\n" << min(a) <<  "\n";
       std::cout << "max(a) =\n" << max(a) << "\n\n";
       std::cout << "min(a,b) =\n" << min(a,b) << "\n";
       std::cout << "max(a,b) =\n" << max(a,b) << "\n";
       std::cout << "norm(a) = " << norm(a) << "\n";
    }
    

    Output:

    abs(a) =
    (           1 )
    (           2 )
    (           3 )
    (           4 )
    
    sin(a) =
    (    0.841471 )
    (    0.909297 )
    (     0.14112 )
    (   -0.756802 )
    
    cos(a) =
    (    0.540302 )
    (   -0.416147 )
    (   -0.989992 )
    (   -0.653644 )
    
    sqrt(a) =
    (           1 )
    (     1.41421 )
    (     1.73205 )
    (           2 )
    
    log(a) =
    (           0 )
    (    0.693147 )
    (     1.09861 )
    (     1.38629 )
    
    exp(a) =
    (     2.71828 )
    (     7.38906 )
    (     20.0855 )
    (     54.5982 )
    
    min(a) = 1
    max(a) = 4
    
    min(a,b) =
    (         0.1 )
    (         0.2 )
    (         0.3 )
    (         0.4 )
    
    max(a,b) =
    (           1 )
    (           2 )
    (           3 )
    (           4 )
    
    norm(a) = 5.47723
    

    Adaptors

    If the custom data type is used in the context of the HermitianMatrix, UniLowerMatrix, or UniUpperMatrix adaptors, it will be necessary to provide overloads of the isZero(), isOne(), and isReal() functions:

    namespace custom {
    
    template< blaze::RelaxationFlag RF >
    constexpr bool isZero( double_t d ) { return blaze::isZero<RF>( d.value ); }
    
    template< blaze::RelaxationFlag RF >
    constexpr bool isOne ( double_t d ) { return blaze::isOne<RF> ( d.value ); }
    
    template< blaze::RelaxationFlag RF >
    constexpr bool isReal( double_t d ) { MAYBE_UNUSED( d ); return true; }
    
    } // namespace custom
    

    Example:

    int main()
    {
       blaze::UniLowerMatrix< blaze::DynamicMatrix<custom::double_t> > L
          { { 1.0, 0.0, 0.0 },
            { 2.0, 1.0, 0.0 },
            { 3.0, 4.0, 1.0 } };
    
       blaze::UniUpperMatrix< blaze::DynamicMatrix<custom::double_t> > U
          { { 1.0, 2.0, 3.0 },
            { 0.0, 1.0, 4.0 },
            { 0.0, 0.0, 1.0 } };
    
       const auto A( evaluate( L * U ) );
       std::cout << "A =\n" << A << "\n";
    }
    

    Output:

    A =
    (            1            2            3 )
    (            2            5           10 )
    (            3           10           26 )
    
  3. Log in to comment