Add support for ArrayHandleDecorator resizing.

Fixes #428.
This commit is contained in:
Allison Vacanti 2019-12-20 17:03:36 -05:00
parent 5834c28521
commit 88bf38afe2
3 changed files with 534 additions and 18 deletions

@ -0,0 +1,114 @@
# ArrayHandleDecorator Allocate and Shrink Support
`ArrayHandleDecorator` can now be resized when given an appropriate
decorator implementation.
Since the mapping between the size of an `ArrayHandleDecorator` and its source
`ArrayHandle`s is not well defined, resize operations (such as `Shrink` and
`Allocate`) are not defined by default, and will throw an exception if called.
However, by implementing the methods `AllocateSourceArrays` and/or
`ShrinkSourceArrays` on the implementation class, resizing the decorator is
allowed. These methods are passed in a new size along with each of the
`ArrayHandleDecorator`'s source arrays, allowing developers to control how
the resize operation should affect the source arrays.
For example, the following decorator implementation can be used to create a
resizable `ArrayHandleDecorator` that is implemented using two arrays, which
are combined to produce values via the expression:
```
[decorator value i] = [source1 value i] * 10 + [source2 value i]
```
Implementation:
```c++
template <typename ValueType>
struct DecompositionDecorImpl
{
template <typename Portal1T, typename Portal2T>
struct Functor
{
Portal1T Portal1;
Portal2T Portal2;
VTKM_EXEC_CONT
ValueType operator()(vtkm::Id idx) const
{
return static_cast<ValueType>(this->Portal1.Get(idx) * 10 + this->Portal2.Get(idx));
}
};
template <typename Portal1T, typename Portal2T>
struct InverseFunctor
{
Portal1T Portal1;
Portal2T Portal2;
VTKM_EXEC_CONT
void operator()(vtkm::Id idx, const ValueType& val) const
{
this->Portal1.Set(idx, static_cast<ValueType>(std::floor(val / 10)));
this->Portal2.Set(idx, static_cast<ValueType>(std::fmod(val, 10)));
}
};
template <typename Portal1T, typename Portal2T>
VTKM_CONT Functor<typename std::decay<Portal1T>::type, typename std::decay<Portal2T>::type>
CreateFunctor(Portal1T&& p1, Portal2T&& p2) const
{
return { std::forward<Portal1T>(p1), std::forward<Portal2T>(p2) };
}
template <typename Portal1T, typename Portal2T>
VTKM_CONT InverseFunctor<typename std::decay<Portal1T>::type, typename std::decay<Portal2T>::type>
CreateInverseFunctor(Portal1T&& p1, Portal2T&& p2) const
{
return { std::forward<Portal1T>(p1), std::forward<Portal2T>(p2) };
}
// Resize methods:
template <typename Array1T, typename Array2T>
VTKM_CONT
void AllocateSourceArrays(vtkm::Id numVals, Array1T&& array1, Array2T&& array2) const
{
array1.Allocate(numVals);
array2.Allocate(numVals);
}
template <typename Array1T, typename Array2T>
VTKM_CONT
void ShrinkSourceArrays(vtkm::Id numVals, Array1T&& array1, Array2T&& array2) const
{
array1.Shrink(numVals);
array2.Shrink(numVals);
}
};
// Usage:
vtkm::cont::ArrayHandle<ValueType> a1;
vtkm::cont::ArrayHandle<ValueType> a2;
auto decor = vtkm::cont::make_ArrayHandleDecorator(0, DecompositionDecorImpl<ValueType>{}, a1, a2);
decor.Allocate(5);
{
auto decorPortal = decor.GetPortalControl();
decorPortal.Set(0, 13);
decorPortal.Set(1, 8);
decorPortal.Set(2, 43);
decorPortal.Set(3, 92);
decorPortal.Set(4, 117);
}
// a1: { 1, 0, 4, 9, 11 }
// a2: { 3, 8, 3, 2, 7 }
// decor: { 13, 8, 43, 92, 117 }
decor.Shrink(3);
// a1: { 1, 0, 4 }
// a2: { 3, 8, 3 }
// decor: { 13, 8, 43 }
```

@ -11,6 +11,7 @@
#define vtk_m_ArrayHandleDecorator_h
#include <vtkm/cont/ArrayHandle.h>
#include <vtkm/cont/ErrorBadType.h>
#include <vtkm/cont/Storage.h>
#include <vtkm/StaticAssert.h>
@ -137,6 +138,44 @@ public:
using type = decltype(InverseExistsTest<DecoratorImplT>(0));
};
// Tests whether DecoratorImplT has an AllocateSourceArrays(size, Arrays...) method.
template <typename DecoratorImplT, typename ArrayList>
struct IsDecoratorAllocatableImpl;
template <typename DecoratorImplT, template <typename...> class List, typename... ArrayTs>
struct IsDecoratorAllocatableImpl<DecoratorImplT, List<ArrayTs...>>
{
private:
template <
typename T,
typename U = decltype(std::declval<T>().AllocateSourceArrays(0, std::declval<ArrayTs&>()...))>
static std::true_type Exists(int);
template <typename T>
static std::false_type Exists(...);
public:
using type = decltype(Exists<DecoratorImplT>(0));
};
// Tests whether DecoratorImplT has a ShrinkSourceArrays(size, Arrays...) method.
template <typename DecoratorImplT, typename ArrayList>
struct IsDecoratorShrinkableImpl;
template <typename DecoratorImplT, template <typename...> class List, typename... ArrayTs>
struct IsDecoratorShrinkableImpl<DecoratorImplT, List<ArrayTs...>>
{
private:
template <
typename T,
typename U = decltype(std::declval<T>().ShrinkSourceArrays(0, std::declval<ArrayTs&>()...))>
static std::true_type Exists(int);
template <typename T>
static std::false_type Exists(...);
public:
using type = decltype(Exists<DecoratorImplT>(0));
};
// Deduces the type returned by DecoratorImplT::CreateFunctor when given
// the specified portals.
template <typename DecoratorImplT, typename PortalList>
@ -299,6 +338,18 @@ template <typename DecoratorImplT, typename PortalList>
using IsFunctorInvertible =
typename detail::IsFunctorInvertibleImpl<DecoratorImplT, PortalList>::type;
// Set to std::true_type if DecoratorImplT::AllocateSourceArrays can be called
// with the supplied arrays, or std::false_type otherwise.
template <typename DecoratorImplT, typename ArrayList>
using IsDecoratorAllocatable =
typename detail::IsDecoratorAllocatableImpl<DecoratorImplT, ArrayList>::type;
// Set to std::true_type if DecoratorImplT::ShrinkSourceArrays can be called
// with the supplied arrays, or std::false_type otherwise.
template <typename DecoratorImplT, typename ArrayList>
using IsDecoratorShrinkable =
typename detail::IsDecoratorShrinkableImpl<DecoratorImplT, ArrayList>::type;
// std::true_type/std::false_type depending on whether the decorator impl has a
// CreateInversePortal method AND any of the arrays are writable.
template <typename DecoratorImplT, typename PortalList>
@ -368,6 +419,10 @@ struct DecoratorStorageTraits
using IndexList = tao::seq::make_index_sequence<sizeof...(ArrayTs)>;
#endif // VTKM_USE_TAO_SEQ
// true_type/false_type depending on whether the decorator supports Allocate/Shrink:
using IsAllocatable = IsDecoratorAllocatable<DecoratorImplT, ArrayList>;
using IsShrinkable = IsDecoratorShrinkable<DecoratorImplT, ArrayList>;
// Portal lists:
// NOTE we have to pass the parameter pack here instead of using ArrayList
// with brigand::transform, since that's causing MSVC 2015 to ice:
@ -445,6 +500,41 @@ struct DecoratorStorageTraits
return { impl.CreateFunctor(portals...), impl.CreateInverseFunctor(portals...), numVals };
}
// Static dispatch for calling AllocateSourceArrays on supported implementations:
VTKM_CONT[[noreturn]] static void CallAllocate(std::false_type,
const DecoratorImplT&,
vtkm::Id,
ArrayTs&...)
{
throw vtkm::cont::ErrorBadType("Allocate not supported by this ArrayHandleDecorator.");
}
VTKM_CONT static void CallAllocate(std::true_type,
const DecoratorImplT& impl,
vtkm::Id newSize,
ArrayTs&... arrays)
{
impl.AllocateSourceArrays(newSize, arrays...);
}
// Static dispatch for calling ShrinkSourceArrays on supported implementations.
VTKM_CONT[[noreturn]] static void CallShrink(std::false_type,
const DecoratorImplT&,
vtkm::Id,
ArrayTs&...)
{
throw vtkm::cont::ErrorBadType("Shrink not supported by this ArrayHandleDecorator.");
}
VTKM_CONT static void CallShrink(std::true_type,
const DecoratorImplT& impl,
vtkm::Id newSize,
ArrayTs&... arrays)
{
impl.ShrinkSourceArrays(newSize, arrays...);
}
#ifndef VTKM_USE_TAO_SEQ
// Portal construction methods. These actually create portals.
template <template <typename...> class List, typename... Indices>
@ -526,7 +616,26 @@ struct DecoratorStorageTraits
GetPortalOutput(vtkmstd::get<Indices{}.value>(arrays), dev)...);
}
#else // VTKM_USE_TAO_SEQ
template <template <typename...> class List, typename... Indices>
VTKM_CONT static void AllocateSourceArrays(const DecoratorImplT& impl,
ArrayTupleType& arrays,
vtkm::Id numValues,
List<Indices...>)
{
CallAllocate(IsAllocatable{}, impl, numValues, vtkmstd::get<Indices{}.value>(arrays)...);
}
template <template <typename...> class List, typename... Indices>
VTKM_CONT static void ShrinkSourceArrays(const DecoratorImplT& impl,
ArrayTupleType& arrays,
vtkm::Id numValues,
List<Indices...>)
{
CallShrink(IsShrinkable{}, impl, numValues, vtkmstd::get<Indices{}.value>(arrays)...);
}
#else // VTKM_USE_TAO_SEQ
// Portal construction methods. These actually create portals.
template <template <typename, std::size_t...> class List, std::size_t... Indices>
VTKM_CONT static PortalControlType MakePortalControl(const DecoratorImplT& impl,
@ -580,6 +689,25 @@ struct DecoratorStorageTraits
return CreatePortalDecorator<PortalExecutionType<Device>>(
numValues, impl, GetPortalOutput(vtkmstd::get<Indices>(arrays), dev)...);
}
template <template <typename, std::size_t...> class List, std::size_t... Indices>
VTKM_CONT static void AllocateSourceArrays(const DecoratorImplT& impl,
ArrayTupleType& arrays,
vtkm::Id numValues,
List<std::size_t, Indices...>)
{
CallAllocate(IsAllocatable{}, impl, numValues, vtkmstd::get<Indices>(arrays)...);
}
template <template <typename, std::size_t...> class List, std::size_t... Indices>
VTKM_CONT static void ShrinkSourceArrays(const DecoratorImplT& impl,
ArrayTupleType& arrays,
vtkm::Id numValues,
List<std::size_t, Indices...>)
{
CallShrink(IsShrinkable{}, impl, numValues, vtkmstd::get<Indices>(arrays)...);
}
#endif // VTKM_USE_TAO_SEQ
};
@ -642,18 +770,21 @@ public:
}
VTKM_CONT
void Allocate(vtkm::Id)
void Allocate(vtkm::Id numValues)
{
VTKM_ASSERT(this->Valid);
// No-op. I suppose eventually we could pass numValues down to the
// implementation class and let it do something intelligent.
Traits::AllocateSourceArrays(this->Implementation, this->ArrayTuple, numValues, IndexList{});
// If the above call doesn't throw, update our state.
this->NumberOfValues = numValues;
}
VTKM_CONT
void Shrink(vtkm::Id)
void Shrink(vtkm::Id numValues)
{
VTKM_ASSERT(this->Valid);
// No-op. Again, could eventually be passed down to the implementation.
Traits::ShrinkSourceArrays(this->Implementation, this->ArrayTuple, numValues, IndexList{});
// If the above call doesn't throw, update our state.
this->NumberOfValues = numValues;
}
VTKM_CONT
@ -778,10 +909,7 @@ public:
}
VTKM_CONT
void Shrink(vtkm::Id)
{
// no-op
}
void Shrink(vtkm::Id numValues) { this->Storage->Shrink(numValues); }
VTKM_CONT
@ -845,6 +973,31 @@ private:
/// SomeInverseFunctor CreateInverseFunctor(Portal1Type portal1,
/// Portal2Type portal2) const;
///
/// // Given a set of ArrayHandles and a size, implement what should happen
/// // to the source ArrayHandles when Allocate() is called on the decorator
/// // handle.
/// //
/// // AllocateSourceArrays is optional; if not provided, the
/// // ArrayHandleDecorator will throw if its Allocate method is called. If
/// // an implementation is present and doesn't throw, the
/// // ArrayHandleDecorator's internal state is updated to show `size` as the
/// // number of values.
/// template <typename Array1Type, typename Array2Type>
/// VTKM_CONT
/// void AllocateSourceArrays(vtkm::Id size, Array1Type array1, Array2Type array2) const;
///
/// // Given a set of ArrayHandles and a size, implement what should happen to
/// // the source ArrayHandles when Shrink() is called on the decorator handle.
/// //
/// // ShrinkSourceArrays is optional; if not provided, the
/// // ArrayHandleDecorator will throw if its Shrink method is called. If
/// // an implementation is present and doesn't throw, the
/// // ArrayHandleDecorator's internal state is updated to show `size` as the
/// // number of values.
/// template <typename Array1Type, typename Array2Type>
/// VTKM_CONT
/// void ShrinkSourceArrays(vtkm::Id size, Array1Type array1, Array2Type array2) const;
///
/// };
/// ```
///

@ -27,7 +27,7 @@ struct DecoratorTests
{
static constexpr vtkm::Id ARRAY_SIZE = 10;
// Decorator implemenation that demonstrates how to write invertible functors
// Decorator implementation that demonstrates how to write invertible functors
// that combine three array handles with complex access logic. The resulting
// ArrayHandleDecorator can be both read from and written to.
//
@ -212,6 +212,69 @@ struct DecoratorTests
}
};
// Decorator implementation that combines two source arrays using the formula
// `[source1] * 10 + [source2]` and supports resizing.
template <typename ValueType>
struct DecompositionDecorImpl
{
template <typename Portal1T, typename Portal2T>
struct Functor
{
Portal1T Portal1;
Portal2T Portal2;
VTKM_EXEC_CONT
ValueType operator()(vtkm::Id idx) const
{
return static_cast<ValueType>(this->Portal1.Get(idx) * 10 + this->Portal2.Get(idx));
}
};
template <typename Portal1T, typename Portal2T>
struct InverseFunctor
{
Portal1T Portal1;
Portal2T Portal2;
VTKM_EXEC_CONT
void operator()(vtkm::Id idx, const ValueType& val) const
{
this->Portal1.Set(idx, static_cast<ValueType>(std::floor(val / 10)));
this->Portal2.Set(idx, static_cast<ValueType>(std::fmod(val, 10)));
}
};
template <typename Portal1T, typename Portal2T>
VTKM_CONT Functor<typename std::decay<Portal1T>::type, typename std::decay<Portal2T>::type>
CreateFunctor(Portal1T&& p1, Portal2T&& p2) const
{
return { std::forward<Portal1T>(p1), std::forward<Portal2T>(p2) };
}
template <typename Portal1T, typename Portal2T>
VTKM_CONT
InverseFunctor<typename std::decay<Portal1T>::type, typename std::decay<Portal2T>::type>
CreateInverseFunctor(Portal1T&& p1, Portal2T&& p2) const
{
return { std::forward<Portal1T>(p1), std::forward<Portal2T>(p2) };
}
// Resize methods:
template <typename Array1T, typename Array2T>
VTKM_CONT void AllocateSourceArrays(vtkm::Id numVals, Array1T&& array1, Array2T&& array2) const
{
array1.Allocate(numVals);
array2.Allocate(numVals);
}
template <typename Array1T, typename Array2T>
VTKM_CONT void ShrinkSourceArrays(vtkm::Id numVals, Array1T&& array1, Array2T&& array2) const
{
array1.Shrink(numVals);
array2.Shrink(numVals);
}
};
template <typename ValueType>
void InversionTest() const
{
@ -374,24 +437,210 @@ struct DecoratorTests
}
}
template <typename ValueType>
void DecompositionTest() const
{
vtkm::cont::ArrayHandle<ValueType> a1;
vtkm::cont::ArrayHandle<ValueType> a2;
auto decor =
vtkm::cont::make_ArrayHandleDecorator(0, DecompositionDecorImpl<ValueType>{}, a1, a2);
VTKM_TEST_ASSERT(decor.GetNumberOfValues() == 0);
decor.Allocate(5);
VTKM_TEST_ASSERT(decor.GetNumberOfValues() == 5);
{
auto decorPortal = decor.GetPortalControl();
decorPortal.Set(0, 13);
decorPortal.Set(1, 8);
decorPortal.Set(2, 43);
decorPortal.Set(3, 92);
decorPortal.Set(4, 117);
}
VTKM_TEST_ASSERT(a1.GetNumberOfValues() == 5);
{
auto a1Portal = a1.GetPortalConstControl();
VTKM_TEST_ASSERT(test_equal(a1Portal.Get(0), 1));
VTKM_TEST_ASSERT(test_equal(a1Portal.Get(1), 0));
VTKM_TEST_ASSERT(test_equal(a1Portal.Get(2), 4));
VTKM_TEST_ASSERT(test_equal(a1Portal.Get(3), 9));
VTKM_TEST_ASSERT(test_equal(a1Portal.Get(4), 11));
}
VTKM_TEST_ASSERT(a2.GetNumberOfValues() == 5);
{
auto a2Portal = a2.GetPortalConstControl();
VTKM_TEST_ASSERT(test_equal(a2Portal.Get(0), 3));
VTKM_TEST_ASSERT(test_equal(a2Portal.Get(1), 8));
VTKM_TEST_ASSERT(test_equal(a2Portal.Get(2), 3));
VTKM_TEST_ASSERT(test_equal(a2Portal.Get(3), 2));
VTKM_TEST_ASSERT(test_equal(a2Portal.Get(4), 7));
}
decor.Shrink(3);
VTKM_TEST_ASSERT(decor.GetNumberOfValues() == 3);
{
auto decorPortal = decor.GetPortalConstControl();
VTKM_TEST_ASSERT(test_equal(decorPortal.Get(0), 13));
VTKM_TEST_ASSERT(test_equal(decorPortal.Get(1), 8));
VTKM_TEST_ASSERT(test_equal(decorPortal.Get(2), 43));
}
VTKM_TEST_ASSERT(a1.GetNumberOfValues() == 3);
{
auto a1Portal = a1.GetPortalConstControl();
VTKM_TEST_ASSERT(test_equal(a1Portal.Get(0), 1));
VTKM_TEST_ASSERT(test_equal(a1Portal.Get(1), 0));
VTKM_TEST_ASSERT(test_equal(a1Portal.Get(2), 4));
}
VTKM_TEST_ASSERT(a2.GetNumberOfValues() == 3);
{
auto a2Portal = a2.GetPortalConstControl();
VTKM_TEST_ASSERT(test_equal(a2Portal.Get(0), 3));
VTKM_TEST_ASSERT(test_equal(a2Portal.Get(1), 8));
VTKM_TEST_ASSERT(test_equal(a2Portal.Get(2), 3));
}
}
template <typename ValueType>
void operator()(const ValueType) const
{
InversionTest<ValueType>();
this->InversionTest<ValueType>();
BinaryOperatorTest<ValueType, vtkm::Maximum>();
BinaryOperatorTest<ValueType, vtkm::Minimum>();
BinaryOperatorTest<ValueType, vtkm::Add>();
BinaryOperatorTest<ValueType, vtkm::Subtract>();
BinaryOperatorTest<ValueType, vtkm::Multiply>();
this->BinaryOperatorTest<ValueType, vtkm::Maximum>();
this->BinaryOperatorTest<ValueType, vtkm::Minimum>();
this->BinaryOperatorTest<ValueType, vtkm::Add>();
this->BinaryOperatorTest<ValueType, vtkm::Subtract>();
this->BinaryOperatorTest<ValueType, vtkm::Multiply>();
ScanExtendedToNumIndicesTest<ValueType>();
this->ScanExtendedToNumIndicesTest<ValueType>();
this->DecompositionTest<ValueType>();
}
};
// ArrayHandleDecorator that implements AllocateSourceArrays and ShrinkSourceArrays, thus allowing
// it to be resized.
struct ResizableDecorImpl
{
// We don't actually read/write from this, so use a dummy functor:
struct Functor
{
VTKM_EXEC_CONT vtkm::Id operator()(vtkm::Id) const { return 0; }
};
template <typename... PortalTs>
VTKM_CONT Functor CreateFunctor(PortalTs...) const
{
return Functor{};
}
template <typename Array1T, typename Array2T>
void ShrinkSourceArrays(vtkm::Id newSize, Array1T& a1, Array2T& a2) const
{
VTKM_IS_ARRAY_HANDLE(Array1T);
VTKM_IS_ARRAY_HANDLE(Array2T);
// Resize each to 2*newSize:
a1.Shrink(2 * newSize);
a2.Shrink(2 * newSize);
}
template <typename Array1T, typename Array2T>
void AllocateSourceArrays(vtkm::Id newSize, Array1T& a1, Array2T& a2) const
{
VTKM_IS_ARRAY_HANDLE(Array1T);
VTKM_IS_ARRAY_HANDLE(Array2T);
// Resize each to 3*newSize:
a1.Allocate(3 * newSize);
a2.Allocate(3 * newSize);
}
};
// ArrayHandleDecorator that implements AllocateSourceArrays and ShrinkSourceArrays, thus allowing
// it to be resized.
struct NonResizableDecorImpl
{
// We don't actually read/write from this, so use a dummy functor:
struct Functor
{
VTKM_EXEC_CONT vtkm::Id operator()(vtkm::Id) const { return 0; }
};
template <typename... PortalTs>
VTKM_CONT Functor CreateFunctor(PortalTs...) const
{
return Functor{};
}
};
void ResizeTest()
{
{
vtkm::cont::ArrayHandle<vtkm::Id> a1;
vtkm::cont::ArrayHandle<vtkm::Id> a2;
ResizableDecorImpl impl;
auto decor = vtkm::cont::make_ArrayHandleDecorator(5, impl, a1, a2);
VTKM_TEST_ASSERT(decor.GetNumberOfValues() == 5);
decor.Allocate(10); // Should allocate a1&a2 to have 30 values:
VTKM_TEST_ASSERT(a1.GetNumberOfValues() == 30);
VTKM_TEST_ASSERT(a2.GetNumberOfValues() == 30);
VTKM_TEST_ASSERT(decor.GetNumberOfValues() == 10);
decor.Shrink(3); // Should resize a1&a2 to have 6 values:
VTKM_TEST_ASSERT(a1.GetNumberOfValues() == 6);
VTKM_TEST_ASSERT(a2.GetNumberOfValues() == 6);
VTKM_TEST_ASSERT(decor.GetNumberOfValues() == 3);
}
{
vtkm::cont::ArrayHandle<vtkm::Id> a1;
a1.Allocate(20);
vtkm::cont::ArrayHandle<vtkm::Id> a2;
a2.Allocate(20);
NonResizableDecorImpl impl;
auto decor = vtkm::cont::make_ArrayHandleDecorator(5, impl, a1, a2);
VTKM_TEST_ASSERT(decor.GetNumberOfValues() == 5);
// Allocate and Shrink should throw an ErrorBadType:
bool threw = false;
try
{
decor.Allocate(10);
}
catch (vtkm::cont::ErrorBadType& e)
{
std::cerr << "Caught expected exception: " << e.what() << "\n";
threw = true;
}
VTKM_TEST_ASSERT(threw, "Allocate did not throw as expected.");
VTKM_TEST_ASSERT(decor.GetNumberOfValues() == 5);
threw = false;
try
{
decor.Shrink(3);
}
catch (vtkm::cont::ErrorBadType& e)
{
std::cerr << "Caught expected exception: " << e.what() << "\n";
threw = true;
}
VTKM_TEST_ASSERT(threw, "Allocate did not throw as expected.");
VTKM_TEST_ASSERT(decor.GetNumberOfValues() == 5);
}
}
void TestArrayHandleDecorator()
{
vtkm::testing::Testing::TryTypes(DecoratorTests{}, vtkm::TypeListScalarAll{});
ResizeTest();
}
} // anonymous namespace