Commit 3c54de5f authored by Robert Maynard's avatar Robert Maynard

Add release notes for v1.5.0

parent d3d3e441
# 0-sample-topic
This is a sample release note for the change in a topic.
Developers should add similar notes for each topic branch
making a noteworthy change. Each document should be named
and titled to match the topic name to avoid merge conflicts.
This diff is collapsed.
Add ability to test exact neighbor offset locations in BoundaryState.
The following methods:
```
BoundaryState::InXBoundary
BoundaryState::InYBoundary
BoundaryState::InZBoundary
BoundaryState::InBoundary
```
have been renamed to:
```
BoundaryState::IsRadiusInXBoundary
BoundaryState::IsRadiusInYBoundary
BoundaryState::IsRadiusInZBoundary
BoundaryState::IsRadiusInBoundary
```
to distinguish them from the new methods:
```
BoundaryState::IsNeighborInXBoundary
BoundaryState::IsNeighborInYBoundary
BoundaryState::IsNeighborInZBoundary
BoundaryState::IsNeighborInBoundary
```
which check a specific neighbor sample offset instead of a full radius.
The method `BoundaryState::ClampNeighborIndex` has also been added, which clamps
a 3D neighbor offset vector to the dataset boundaries.
This allows iteration through only the valid points in a neighborhood using
either of the following patterns:
Using `ClampNeighborIndex` to restrict the iteration space:
```
struct MyWorklet : public vtkm::worklet::WorkletPointNeighborhood
{
public:
using ControlSignature = void(CellSetIn, FieldInNeighborhood, FieldOut);
using ExecutionSignature = void(_2, Boundary, _3);
template <typename InNeighborhoodT, typename OutDataT>
VTKM_EXEC void operator()(const InNeighborhoodT& inData,
const vtkm::exec::BoundaryState &boundary,
OutDataT& outData) const
{
// Clamp the radius to the dataset bounds (discard out-of-bounds points).
const auto minRadius = boundary.ClampNeighborIndex({-10, -10, -10});
const auto maxRadius = boundary.ClampNeighborIndex({10, 10, 10});
for (vtkm::IdComponent k = minRadius[2]; k <= maxRadius[2]; ++k)
{
for (vtkm::IdComponent j = minRadius[1]; j <= maxRadius[1]; ++j)
{
for (vtkm::IdComponent i = minRadius[0]; i <= maxRadius[0]; ++i)
{
outData = doSomeConvolution(i, j, k, outdata, inData.Get(i, j, k));
}
}
}
}
};
```
or, using `IsNeighborInBoundary` methods to skip out-of-bounds loops:
```
struct MyWorklet : public vtkm::worklet::WorkletPointNeighborhood
{
public:
using ControlSignature = void(CellSetIn, FieldInNeighborhood, FieldOut);
using ExecutionSignature = void(_2, Boundary, _3);
template <typename InNeighborhoodT, typename OutDataT>
VTKM_EXEC void operator()(const InNeighborhoodT& inData,
const vtkm::exec::BoundaryState &boundary,
OutDataT& outData) const
{
for (vtkm::IdComponent k = -10; k <= 10; ++k)
{
if (!boundary.IsNeighborInZBoundary(k))
{
continue;
}
for (vtkm::IdComponent j = -10; j <= 10; ++j)
{
if (!boundary.IsNeighborInYBoundary(j))
{
continue;
}
for (vtkm::IdComponent i = -10; i <= 10; ++i)
{
if (!boundary.IsNeighborInXBoundary(i))
{
continue;
}
outData = doSomeConvolution(i, j, k, outdata, inData.Get(i, j, k));
}
}
}
}
};
```
The latter is useful for implementing a convolution that substitutes a constant
value for out-of-bounds indices:
```
struct MyWorklet : public vtkm::worklet::WorkletPointNeighborhood
{
public:
using ControlSignature = void(CellSetIn, FieldInNeighborhood, FieldOut);
using ExecutionSignature = void(_2, Boundary, _3);
template <typename InNeighborhoodT, typename OutDataT>
VTKM_EXEC void operator()(const InNeighborhoodT& inData,
const vtkm::exec::BoundaryState &boundary,
OutDataT& outData) const
{
for (vtkm::IdComponent k = -10; k <= 10; ++k)
{
for (vtkm::IdComponent j = -10; j <= 10; ++j)
{
for (vtkm::IdComponent i = -10; i <= 10; ++i)
{
if (boundary.IsNeighborInBoundary({i, j, k}))
{
outData = doSomeConvolution(i, j, k, outdata, inData.Get(i, j, k));
}
else
{ // substitute zero for out-of-bounds samples:
outData = doSomeConvolution(i, j, k, outdata, 0);
}
}
}
}
}
};
```
# Add ability to get an array from a Field for a particular type
Previously, whenever you got an array from a `Field` object from a call to
an `ApplyPolicy`, you would get back a `VariantArrayHandle` that allows you
to cast to multiple types. To use that, you then have to cast it to
multiple different types and multiple different storage.
Often, this is what you want. If you are operating on a field, then you
want to cast to the native type. But there are also cases where you know a
specific type you want. For example, if you are operating on two fields, it
makes sense to find the exact type for the first field and then cast the
second field to that type if necessary rather than pointlessly unroll
templates for the cross of every possible combination. Also, we are not
unrolling for different storage types or attempting to create a virtual
array. Instead, we are using an `ArrayHandleMultiplexer` so that you only
have to compile for this array once.
This is done through a new version of `ApplyPolicy`. This version takes a
type of the array as its first template argument, which must be specified.
This requires having a list of potential storage to try. It will use that
to construct an `ArrayHandleMultiplexer` containing all potential types.
This list of storages comes from the policy. A `StorageList` item was added
to the policy. It is also sometimes necessary for a filter to provide its
own special storage types. Thus, an `AdditionalFieldStorage` type was added
to `Filter` which is set to a `ListTag` of storage types that should be
added to those specified by the policy.
Types are automatically converted. So if you ask for a `vtkm::Float64` and
field contains a `vtkm::Float32`, it will the array wrapped in an
`ArrayHandleCast` to give the expected type.
Here is an example where you are doing an operation on a field and
coordinate system. The superclass finds the correct type of the field. Your
result is just going to follow the type of the field.
``` cpp
template <typename T, typename StorageType, typename DerivedPolicy>
inline VTKM_CONT vtkm::cont::DataSet MyFilter::DoExecute(
const vtkm::cont::DataSet& inDataSet,
const vtkm::cont::ArrayHandle<T, StorageType>& field,
const vtkm::filter::FieldMetadata& fieldMetadata,
vtkm::filter::PolicyBase<DerivedPolicy> policy)
{
vtkm::cont::CoordinateSystem coords = inDataSet.GetCoordianteSystem();
auto coordsArray = vtkm::filter::ApplyPolicy<T>(coords, policy, *this);
```
# Add ArrayGetValues to retrieve a subset of ArrayHandle values from a device.
An algorithm will often want to pull just a single value (or small subset of
values) back from a device to check the results of a computation. Previously,
there was no easy way to do this, and algorithm developers would often
transfer vast quantities of data back to the host just to check a single value.
The new `vtkm::cont::ArrayGetValue` and `vtkm::cont::ArrayGetValues` functions
simplify this operations and provide a method to just retrieve a portion of an
array.
This utility provides several convenient overloads:
A single id may be passed into ArrayGetValue, or multiple ids may be specified
to ArrayGetValues as an ArrayHandle<vtkm::Id>, a std::vector<vtkm::Id>, a
c-array (pointer and size), or as a brace-enclosed initializer list.
The single result from ArrayGetValue may be returned or written to an output
argument. Multiple results from ArrayGetValues may be returned as an
std::vector<T>, or written to an output argument as an ArrayHandle<T> or a
std::vector<T>.
Examples:
```
vtkm::cont::ArrayHandle<T> data = ...;
// Fetch the first value in an array handle:
T firstVal = vtkm::cont::ArrayGetValue(0, data);
// Fetch the first and third values in an array handle:
std::vector<T> firstAndThird = vtkm::cont::ArrayGetValues({0, 2}, data);
// Fetch the first and last values in an array handle:
std::vector<T> firstAndLast =
vtkm::cont::ArrayGetValues({0, data.GetNumberOfValues() - 1}, data);
// Fetch the first 4 values into an array handle:
const std::vector<vtkm::Id> ids{0, 1, 2, 3};
vtkm::cont::ArrayHandle<T> firstFour;
vtkm::cont::ArrayGetValues(ids, data, firstFour);
```
# Add `ArrayHandleDecorator`.
`ArrayHandleDecorator` is given a `DecoratorImpl` class and a list of one or
more source `ArrayHandle`s. There are no restrictions on the size or type of
the source `ArrayHandle`s.
The decorator implementation class is described below:
```
struct ExampleDecoratorImplementation
{
// Takes one portal for each source array handle (only two shown).
// Returns a functor that defines:
//
// ValueType operator()(vtkm::Id id) const;
//
// which takes an index and returns a value which should be produced by
// the source arrays somehow. This ValueType will be the ValueType of the
// ArrayHandleDecorator.
//
// Both SomeFunctor::operator() and CreateFunctor must be const.
//
template <typename Portal1Type, typename Portal2Type>
SomeFunctor CreateFunctor(Portal1Type portal1, Portal2Type portal2) const;
// Takes one portal for each source array handle (only two shown).
// Returns a functor that defines:
//
// void operator()(vtkm::Id id, ValueType val) const;
//
// which takes an index and a value, which should be used to modify one
// or more of the source arrays.
//
// CreateInverseFunctor is optional; if not provided, the
// ArrayHandleDecorator will be read-only. In addition, if all of the
// source ArrayHandles are read-only, the inverse functor will not be used
// and the ArrayHandleDecorator will be read only.
//
// Both SomeInverseFunctor::operator() and CreateInverseFunctor must be
// const.
//
template <typename Portal1Type, typename Portal2Type>
SomeInverseFunctor CreateInverseFunctor(Portal1Type portal1,
Portal2Type portal2) const;
};
```
Some example implementation classes are provided below:
Reverse a ScanExtended:
```
// Decorator implementation that reverses the ScanExtended operation.
//
// The resulting ArrayHandleDecorator will take an array produced by the
// ScanExtended algorithm and return the original ScanExtended input.
//
// Some interesting things about this:
// - The ArrayHandleDecorator's ValueType will not be the same as the
// ScanPortal's ValueType. The Decorator ValueType is determined by the
// return type of Functor::operator().
// - The ScanPortal has more values than the ArrayHandleDecorator. The
// number of values the ArrayHandleDecorator should hold is set during
// construction and may differ from the arrays it holds.
template <typename ValueType>
struct ScanExtendedToNumIndicesDecorImpl
{
template <typename ScanPortalType>
struct Functor
{
ScanPortalType ScanPortal;
VTKM_EXEC_CONT
ValueType operator()(vtkm::Id idx) const
{
return static_cast<ValueType>(this->ScanPortal.Get(idx + 1) -
this->ScanPortal.Get(idx));
}
};
template <typename ScanPortalType>
Functor<ScanPortalType> CreateFunctor(ScanPortalType portal) const
{
return {portal};
}
};
auto numIndicesOrig = vtkm::cont::make_ArrayHandleCounting(ValueType{0},
ValueType{1},
ARRAY_SIZE);
vtkm::cont::ArrayHandle<vtkm::Id> scan;
vtkm::cont::Algorithm::ScanExtended(
vtkm::cont::make_ArrayHandleCast<vtkm::Id>(numIndicesOrig),
scan);
auto numIndicesDecor = vtkm::cont::make_ArrayHandleDecorator(
ARRAY_SIZE,
ScanExtendedToNumIndicesDecorImpl<ValueType>{},
scan);
```
Combine two other `ArrayHandle`s using an arbitrary binary operation:
```
// Decorator implementation that demonstrates how to create functors that
// hold custom state. Here, the functors have a customizable Operation
// member.
//
// This implementation is used to create a read-only ArrayHandleDecorator
// that combines the values in two other ArrayHandles using an arbitrary
// binary operation (e.g. vtkm::Maximum, vtkm::Add, etc).
template <typename ValueType, typename OperationType>
struct BinaryOperationDecorImpl
{
OperationType Operation;
// The functor use to read values. Note that it holds extra state in
// addition to the portals.
template <typename Portal1Type, typename Portal2Type>
struct Functor
{
Portal1Type Portal1;
Portal2Type Portal2;
OperationType Operation;
VTKM_EXEC_CONT
ValueType operator()(vtkm::Id idx) const
{
return this->Operation(static_cast<ValueType>(this->Portal1.Get(idx)),
static_cast<ValueType>(this->Portal2.Get(idx)));
}
};
// A non-variadic example of a factory function to produce a functor. This
// is where the extra state is passed into the functor.
template <typename P1T, typename P2T>
Functor<P1T, P2T> CreateFunctor(P1T p1, P2T p2) const
{
return {p1, p2, this->Operation};
}
};
BinaryOperationDecorImpl<ValueType, vtkm::Maximum> factory{vtkm::Maximum{}};
auto decorArray = vtkm::cont::make_ArrayHandleDecorator(ARRAY_SIZE,
factory,
array1,
array2);
```
A factory that does a complex and invertible operation on three portals:
```
// Decorator implemenation 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.
//
// Constructs functors that take three portals.
//
// The first portal's values are accessed in reverse order.
// The second portal's values are accessed in normal order.
// The third portal's values are accessed via ((idx + 3) % size).
//
// Functor will return the max of the first two added to the third.
//
// InverseFunctor will update the third portal such that the Functor would
// return the indicated value.
struct InvertibleDecorImpl
{
// The functor used for reading data from the three portals.
template <typename Portal1Type, typename Portal2Type, typename Portal3Type>
struct Functor
{
using ValueType = typename Portal1Type::ValueType;
Portal1Type Portal1;
Portal2Type Portal2;
Portal3Type Portal3;
VTKM_EXEC_CONT ValueType operator()(vtkm::Id idx) const
{
const auto idx1 = this->Portal1.GetNumberOfValues() - idx - 1;
const auto idx2 = idx;
const auto idx3 = (idx + 3) % this->Portal3.GetNumberOfValues();
const auto v1 = this->Portal1.Get(idx1);
const auto v2 = this->Portal2.Get(idx2);
const auto v3 = this->Portal3.Get(idx3);
return vtkm::Max(v1, v2) + v3;
}
};
// The functor used for writing. Only Portal3 is written to, the other
// portals may be read-only.
template <typename Portal1Type, typename Portal2Type, typename Portal3Type>
struct InverseFunctor
{
using ValueType = typename Portal1Type::ValueType;
Portal1Type Portal1;
Portal2Type Portal2;
Portal3Type Portal3;
VTKM_EXEC_CONT void operator()(vtkm::Id idx, const ValueType &vIn) const
{
const auto v1 = this->Portal1.Get(this->Portal1.GetNumberOfValues() - idx - 1);
const auto v2 = this->Portal2.Get(idx);
const auto vNew = static_cast<ValueType>(vIn - vtkm::Max(v1, v2));
this->Portal3.Set((idx + 3) % this->Portal3.GetNumberOfValues(), vNew);
}
};
// Factory function that takes 3 portals as input and creates an instance
// of Functor with them. Variadic template parameters are used here, but are
// not necessary.
template <typename... PortalTs>
Functor<typename std::decay<PortalTs>::type...>
CreateFunctor(PortalTs&&... portals) const
{
VTKM_STATIC_ASSERT(sizeof...(PortalTs) == 3);
return {std::forward<PortalTs>(portals)...};
}
// Factory function that takes 3 portals as input and creates an instance
// of InverseFunctor with them. Variadic template parameters are used here,
// but are not necessary.
template <typename... PortalTs>
InverseFunctor<typename std::decay<PortalTs>::type...>
CreateInverseFunctor(PortalTs&&... portals) const
{
VTKM_STATIC_ASSERT(sizeof...(PortalTs) == 3);
return {std::forward<PortalTs>(portals)...};
}
};
// Note that only ah3 must be writable for ahInv to be writable. ah1 and ah2
// may be read-only arrays.
auto ahInv = vtkm::cont::make_ArrayHandleDecorator(ARRAY_SIZE,
InvertibleDecorImpl{},
ah1,
ah2,
ah3);
```
# Add ArrayHandleMultiplexer
`vtkm::cont::ArrayHandleMultiplexer` is a fancy `ArrayHandle` that can
mimic being any one of a list of other `ArrayHandle`s. When declared, a set
of a list of `ArrayHandle`s is given to `ArrayHandleMultiplexer`. To use
the `ArrayHandleMultiplexer` it is set to an instance of one of these other
`ArrayHandle`s. Thus, once you compile code to use an
`ArrayHandleMultiplexer`, you can at runtime select any of the types it
supports.
The intention is convert the data from a `vtkm::cont::VariantArrayHandle`
to a `vtkm::cont::ArrayHandleMultiplexer` of some known types. The
`ArrayHandleMultiplexer` can be compiled statically (that is, no virtual
methods are needed). Although the compiler must implement all possible
implementations of the multiplexer, two or more `ArrayHandleMultiplexer`s
can be used together without having to compile every possible combination
of all of them.
## Motivation
`ArrayHandle` is a very flexible templated class that allows us to use the
compiler to adapt our code to pretty much any type of memory layout or
on-line processing. Unfortunately, the template approach requires the code
to know the exact type during compile time.
That is a problem when retrieving data from a
`vtkm::cont::VariantArrayHandle`, which is the case, for example, when
getting data from a `vtkm::cont::DataSet`. The actual type of the array
stored in a `vtkm::cont::VariantArrayHandle` is generally not known at
compile time at the code location where the data is pulled.
Our first approach to this problem was to use metatemplate programming to
iterate over all possible types in the `VariantArrayHandle`. Although this
works, it means that if two or more `VariantArrayHandle`s are dispatched in
a function call, the compiler needs to generate all possible combinations
of the two. This causes long compile times and large executable sizes. It
has lead us to limit the number of types we support, which causes problems
with unsupported arrays.
Our second approach to this problem was to create `ArrayHandleVirtual` to
hide the array type behind a virtual method. This works very well, but is
causing us significant problems on accelerators. Although virtual methods
are supported by CUDA, there are numerous problems that can come up with
the compiled code (such as unknown stack depths or virtual methods
extending across libraries). It is also unknown what problems we will
encounter with other accelerator architectures.
`ArrayHandleMultiplexer` is meant to be a compromise between these two
approaches. Although we are still using metatemplate programming tricks to
iterate over multiple implementations, this compiler looping is localized
to the code to lookup values in the array. This, it is a small amount of
code that needs to be created for each version supported by the
`ArrayHandle`. Also, the code paths can be created independently for each
`ArrayHandleMultiplexer`. Thus, you do not get into the problem of a
combinatorial explosion of types that need to be addressed.
Although `ArrayHandleMultiplexer` still has the problem of being unable to
store a type that is not explicitly listed, the localized expression should
allow us to support many types. By default, we are adding lots of
`ArrayHandleCast`s to the list of supported types. The intention of this is
to allow a filter to specify a value type it operates on and then cast
everything to that type. This further allows us to reduce combination of
types that we have to support.
## Use
The `ArrayHandleMultiplexer` templated class takes a variable number of
template parameters. All the template parameters are expected to be types
of `ArrayHandle`s that the `ArrayHandleMultiplexer` can assume.
For example, let's say we have a use case where we need an array of
indices. Normally, the indices are sequential (0, 1, 2,...), but sometimes
we need to define a custom set of indices. When the indices are sequential,
then an `ArrayHandleIndex` is the best representation. Normally if you also
need to support general arrays you would first have to deep copy the
indices into a physical array. However, with an `ArrayHandleMultiplexer`
you can support both.
``` cpp
vtkm::cont::ArrayHandleMultiplexer<vtkm::cont::ArrayHandleIndex,
vtkm::cont::ArrayHandle<vtkm::Id>> indices;
indices = vtkm::cont::ArrayHandleIndex(ARRAY_SIZE);
```
`indices` can now be used like any other `ArrayHandle`, but for now is
behaving like an `ArrayHandleIndex`. That is, it takes (almost) no actual
space. But if you need to use explicit indices, you can set the `indices`
array to an actual array of indices
``` cpp
vtkm::cont::ArrayHandle<vtkm::Id> indicesInMemory;
// Fill indicesInMemory...
indices = indicesInMemory;
```
All the code that uses `indices` will continue to work.
## Variant
To implement `ArrayHandleMultiplexer`, the class `vtkm::internal::Variant`
was introduced. Although this is an internal class that is not exposed
through the array handle, it is worth documenting its addition as it will
be useful to implement other multiplexing type of objects (such as for
cell sets and locators).
`vtkm::internal::Variant` is a simplified version of C++17's `std::variant`
or boost's `variant`. One of the significant differences between VTK-m's
`Variant` and these other versions is that VTK-m's version does not throw
exceptions on error. Instead, behavior becomes undefined. This is
intentional as not all platforms support exceptions and there could be
consequences on just the possibility for those that do.
Like the aforementioned classes that `vtkm::internal::Variant` is based on,
it behaves much like a `union` of a set of types. Those types are listed as
the `Variant`'s template parameters. The `Variant` can be set to any one of
these types either through construction or assignment. You can also use the
`Emplace` method to construct the object in a `Variant`.
``` cpp
vtkm::internal::Variant<int, float, std::string> variant(5);
// variant is now an int.
variant = 5.0f;
// variant is now a float.
variant.Emplace<std::string>("Hello world");
// variant is now an std::string.
```
The `Variant` maintains the index of which type it is holding. It has
several helpful items to manage the type and index of contained objects:
* `GetIndex()`: A method to retrieve the template parameter index of the
type currently held. In the previous example, the index starts at 0,
becomes 1, then becomes 2.
* `GetIndexOf<T>()`: A static method that returns a `constexpr` of the
index of a given type. In the previous example,
`variant.GetIndexOf<float>()` would return 1.
* `Get<T or I>()`: Given a type, returns the contained object as that
type. Given a number, returns the contained object as a type of the
corresponding index. In the previous example, either `variant.Get<1>()`
or `variant.Get<float>()` would return the `float` value. The behavior
is undefined if the object is not the requested type.
* `IsValid()`: A method that can be used to determine whether the
`Variant` holds an object that can be operated on.
* `Reset()`: A method to remove any contained object and restore to an
invalid state.
Finally, `Variant` contains a `CastAndCall` method. This method takes a
functor followed by a list of optional arguments. The contained object is
cast to the appropriate type and the functor is called with the cast object
followed by the provided arguments. If the functor returns a value, that
value is returned by `CastAndCall`.
`CastAndCall` is an important functionality that makes it easy to wrap
multiplexer objects around a `Variant`. For example, here is how you could
implement executing the `Value` method in an implicit function multiplexer.
``` cpp
class ImplicitFunctionMultiplexer
{
vtkm::internal::Variant<Box, Plane, Sphere> ImplicitFunctionVariant;
// ...
struct ValueFunctor
{
template <typename ImplicitFunctionType>
vtkm::FloatDefault operator()(const ImplicitFunctionType& implicitFunction,
const vtkm::Vec<vtkm::FloatDefault, 3>& point)
{
return implicitFunction.Value(point);
}
};
vtkm::FloatDefault Value(const vtkm::Vec<vtkm::FloatDefault, 3>& point) const
{
return this->ImplicitFunctionVariant.CastAndCall(ValueFunctor{}, point);
}
```
# Added ArrayHandleSOA
`ArrayHandleSOA` behaves like a regular `ArrayHandle` (with a basic
storage) except that if you specify a `ValueType` of a `Vec` or a
`Vec`-like, it will actually store each component in a separate physical
array. When data are retrieved from the array, they are reconstructed into
`Vec` objects as expected.
The intention of this array type is to help cover the most common ways data
is lain out in memory. Typically, arrays of data are either an "array of
structures" like the basic storage where you have a single array of
structures (like `Vec`) or a "structure of arrays" where you have an array
of a basic type (like `float`) for each component of the data being
represented. The `ArrayHandleSOA` makes it easy to cover this second case
without creating special types.
`ArrayHandleSOA` can be constructed from a collection of `ArrayHandle` with
basic storage. This allows you to construct `Vec` arrays from components
without deep copies.
``` cpp
std::vector<vtkm::Float32> accel0;
std::vector<vtkm::Float32> accel1;
std::vector<vtkm::Float32> accel2;
// Let's say accel arrays are set to some field of acceleration vectors by
// some other software.
vtkm::cont::ArrayHandle<vtkm::Float32> accelHandle0 = vtkm::cont::make_ArrayHandle(accel0);
vtkm::cont::ArrayHandle<vtkm::Float32> accelHandle1 = vtkm::cont::make_ArrayHandle(accel1);
vtkm::cont::ArrayHandle<vtkm::Float32> accelHandle2 = vtkm::cont::make_ArrayHandle(accel2);
vtkm::cont::ArrayHandleSOA<vtkm::Vec3f_32> accel = { accelHandle0, accelHandle1, accelHandle2 };
```
Also provided are constructors and versions of `make_ArrayHandleSOA` that
take `std::vector` or C arrays as either initializer lists or variable
arguments.
``` cpp
std::vector<vtkm::Float32> accel0;
std::vector<vtkm::Float32> accel1;
std::vector<vtkm::Float32> accel2;
// Let's say accel arrays are set to some field of acceleration vectors by
// some other software.
vtkm::cont::ArrayHandleSOA<vtkm::Vec3f_32> accel = { accel0, accel1, accel2 };
```
However, setting arrays is a little awkward because you also have to
specify the length. This is done either outside the initializer list or as
the first argument.
``` cpp
vtkm::cont::make_ArrayHandleSOA({ array0, array1, array2 }, ARRAY_SIZE);
```