Commit 0689eaae authored by Praetorius, Simon's avatar Praetorius, Simon

Reduce errors and compiletime in tree traversal

parent efebae43
#include(CheckIncludeFileCXX)
include(CheckCXXSourceCompiles)
#include(CheckCXXSymbolExists)
# fold expressions (a + ...)
check_cxx_source_compiles("
......@@ -30,4 +28,16 @@ check_cxx_source_compiles("
return f<1>();
}
" AMDIS_HAS_CXX_CONSTEXPR_IF
)
check_cxx_source_compiles("
#include <iostream>
#include <tuple>
int main()
{
auto tup = std::make_tuple(0, 'a', 3.14);
for... (auto elem : tup)
std::cout << elem << std::endl;
}
" AMDIS_HAS_EXPANSION_STATEMENTS
)
\ No newline at end of file
......@@ -49,6 +49,7 @@
/* some detected compiler features may be used in AMDiS */
#cmakedefine AMDIS_HAS_CXX_FOLD_EXPRESSIONS 1
#cmakedefine AMDIS_HAS_CXX_CONSTEXPR_IF 1
#cmakedefine AMDIS_HAS_EXPANSION_STATEMENTS 1
/* end amdis
Everything below here will be overwritten
......
......@@ -24,4 +24,4 @@ add_dependencies(examples
stokes1.2d
stokes3.2d
navier_stokes.2d
convection_diffusion.2d)
\ No newline at end of file
convection_diffusion.2d)
......@@ -146,9 +146,10 @@ namespace AMDiS
if (!segment.boundary())
continue;
auto index = segment.boundarySegmentIndex();
Dune::Hybrid::ifElse(Dune::Std::is_detected<HasBoundaryId, Segment>{},
[&](auto id) { boundaryIds_[index] = id(segment).boundaryId(); });
Dune::Hybrid::ifElse(Dune::Std::is_detected<HasBoundaryId, Segment>{}, [&](auto id) {
auto index = segment.boundarySegmentIndex();
boundaryIds_[index] = id(segment).boundaryId();
});
}
}
}
......
......@@ -22,8 +22,8 @@
#include <amdis/Output.hpp>
#include <amdis/common/ConcurrentCache.hpp>
#include <amdis/typetree/Traversal.hpp>
#include <amdis/typetree/TreeContainer.hpp>
#include <amdis/typetree/Visitor.hpp>
namespace AMDiS
{
......@@ -135,7 +135,7 @@ namespace AMDiS
auto lv = basis_->localView();
auto const& idSet = gv.grid().localIdSet();
forEachLeafNode_(lv.tree(), [&](auto const& node, auto const& tp) {
for_each_leaf_node(lv.tree(), [&](auto const& node, auto const& tp) {
nodeDataTransfer_[tp].preAdaptInit(lv, coeff, node);
});
......@@ -148,7 +148,7 @@ namespace AMDiS
lv.bind(e);
auto& treeContainer = it.first->second;
forEachLeafNode_(lv.tree(), [&](auto const& node, auto const& tp) {
for_each_leaf_node(lv.tree(), [&](auto const& node, auto const& tp) {
nodeDataTransfer_[tp].cacheLocal(treeContainer[tp]);
});
}
......@@ -201,7 +201,7 @@ namespace AMDiS
};
restrictLocalCompleted = true;
forEachLeafNode_(lv.tree(), [&](auto const& node, auto const& tp) {
for_each_leaf_node(lv.tree(), [&](auto const& node, auto const& tp) {
restrictLocalCompleted &=
nodeDataTransfer_[tp].restrictLocal(father, treeContainer[tp], xInChildCached,
childContainer[tp], init);
......@@ -224,7 +224,7 @@ namespace AMDiS
auto gv = basis_->gridView();
auto lv = basis_->localView();
auto const& idSet = gv.grid().localIdSet();
forEachLeafNode_(lv.tree(), [&](auto const& node, auto const& tp) {
for_each_leaf_node(lv.tree(), [&](auto const& node, auto const& tp) {
nodeDataTransfer_[tp].postAdaptInit(lv, coeff, node);
});
......@@ -243,7 +243,7 @@ namespace AMDiS
if (it != persistentContainer_.end()) {
lv.bind(e);
auto const& treeContainer = it->second;
forEachLeafNode_(lv.tree(), [&](auto const& node, auto const& tp) {
for_each_leaf_node(lv.tree(), [&](auto const& node, auto const& tp) {
nodeDataTransfer_[tp].copyLocal(treeContainer[tp]);
});
finished_[index] = true;
......@@ -275,7 +275,7 @@ namespace AMDiS
return fatherGeo.local(childGeo.global(x));
};
forEachLeafNode_(lv.tree(), [&](auto const& node, auto const& tp) {
for_each_leaf_node(lv.tree(), [&](auto const& node, auto const& tp) {
nodeDataTransfer_[tp].prolongLocal(father, treeContainer[tp], xInFather, init);
});
......
......@@ -11,8 +11,8 @@
#include <amdis/BoundaryCondition.hpp>
#include <amdis/common/Concepts.hpp>
#include <amdis/typetree/RangeType.hpp>
#include <amdis/typetree/Traversal.hpp>
#include <amdis/typetree/TreeData.hpp>
#include <amdis/typetree/Visitor.hpp>
namespace AMDiS
{
......
......@@ -196,7 +196,7 @@ std::vector<D> PeriodicBC<D,MI>::
coords(Node const& tree, std::vector<std::size_t> const& localIndices) const
{
std::vector<D> dofCoords(localIndices.size());
AMDiS::forEachLeafNode_(tree, [&](auto const& node, auto const& tp)
for_each_leaf_node(tree, [&](auto const& node, auto const& tp)
{
std::size_t size = node.finiteElement().size();
auto geometry = node.element().geometry();
......
......@@ -177,7 +177,7 @@ void ProblemStat<Traits>::createMatricesAndVectors()
rhs_ = std::make_shared<SystemVector>(*globalBasis_, NO_OPERATION);
auto localView = globalBasis_->localView();
AMDiS::forEachNode_(localView.tree(), [&,this](auto const& node, auto treePath)
for_each_node(localView.tree(), [&,this](auto const& node, auto treePath)
{
std::string i = to_string(treePath);
estimates_[i].resize(globalBasis_->gridView().indexSet().size(0));
......@@ -205,7 +205,7 @@ void ProblemStat<Traits>::createMarker()
{
marker_.clear();
auto localView = globalBasis_->localView();
AMDiS::forEachNode_(localView.tree(), [&,this](auto const& node, auto treePath)
for_each_node(localView.tree(), [&,this](auto const& node, auto treePath)
{
std::string componentName = name_ + "->marker[" + to_string(treePath) + "]";
......@@ -232,7 +232,7 @@ void ProblemStat<Traits>::createFileWriter()
{
filewriter_.clear();
auto localView = globalBasis_->localView();
forEachNode_(localView.tree(), [&,this](auto const& node, auto treePath)
for_each_node(localView.tree(), [&,this](auto const& node, auto treePath)
{
std::string componentName = name_ + "->output[" + to_string(treePath) + "]";
......@@ -428,9 +428,9 @@ buildAfterAdapt(AdaptInfo& /*adaptInfo*/, Flag /*flag*/, bool asmMatrix, bool as
rhs_->init(asmVector);
auto localView = globalBasis_->localView();
forEachNode_(localView.tree(), [&,this](auto const& rowNode, auto rowTp) {
for_each_node(localView.tree(), [&,this](auto const& rowNode, auto rowTp) {
auto rowBasis = Dune::Functions::subspaceBasis(*globalBasis_, rowTp);
forEachNode_(localView.tree(), [&,this](auto const& colNode, auto colTp) {
for_each_node(localView.tree(), [&,this](auto const& colNode, auto colTp) {
auto colBasis = Dune::Functions::subspaceBasis(*globalBasis_, colTp);
for (auto bc : dirichletBCs_[rowNode][colNode])
bc->init(rowBasis, colBasis);
......@@ -456,8 +456,8 @@ buildAfterAdapt(AdaptInfo& /*adaptInfo*/, Flag /*flag*/, bool asmMatrix, bool as
systemMatrix_->finish(asmMatrix);
rhs_->finish(asmVector);
forEachNode_(localView.tree(), [&,this](auto const& rowNode, auto) {
forEachNode_(localView.tree(), [&,this](auto const& colNode, auto) {
for_each_node(localView.tree(), [&,this](auto const& rowNode, auto) {
for_each_node(localView.tree(), [&,this](auto const& colNode, auto) {
// finish boundary condition
for (auto bc : dirichletBCs_[rowNode][colNode])
bc->fillBoundaryCondition(*systemMatrix_, *solution_, *rhs_, rowNode, colNode);
......
......@@ -2,7 +2,6 @@
#include <initializer_list>
#include <amdis/common/Apply.hpp>
#include <amdis/common/Index.hpp>
#include <amdis/common/Range.hpp>
......@@ -16,34 +15,52 @@ namespace AMDiS
void ignored_evaluation(std::initializer_list<T>&&) { /* do nothing */ }
}
template <std::size_t... I, class Tuple, class Functor>
constexpr void for_each(std::index_sequence<I...>, Tuple&& tuple, Functor&& f)
{
using std::get;
#if AMDIS_HAS_EXPANSION_STATEMENTS
for... (auto&& t : tuple) { f(FWD(t)); }
#elif AMDIS_HAS_CXX_FOLD_EXPRESSIONS
(f(get<I>(tuple)),...);
#else
Impl_::ignored_evaluation<int>({0, (f(get<I>(tuple)), 0)...});
#endif
}
template <class Tuple, class Functor>
constexpr void for_each(Tuple&& tuple, Functor&& f)
{
#if AMDIS_HAS_CXX_FOLD_EXPRESSIONS
Tools::apply([f=std::move(f)](auto&&... t) { (f(FWD(t)),...); }, tuple);
#else
Tools::apply([f=std::move(f)](auto&&... t) {
Impl_::ignored_evaluation<int>({0, (f(FWD(t)), 0)...});
}, tuple);
#endif
Tools::for_each(std::make_index_sequence<Size_v<std::remove_reference_t<Tuple>>>{}, FWD(tuple), FWD(f));
}
template <std::size_t I0 = 0, std::size_t... I, class Functor>
constexpr void for_range(std::index_sequence<I...>, Functor&& f)
{
#if AMDIS_HAS_CXX_FOLD_EXPRESSIONS
(f(index_t<I0+I>{}),...);
#else
Impl_::ignored_evaluation<int>({0, (f(index_t<I0+I>{}), 0)...});
#endif
}
template <std::size_t I0, std::size_t I1, class Functor>
constexpr void for_range(index_t<I0> i0, index_t<I1> i1, Functor&& f)
{
Tools::for_each(range_t<I0,I1>{}, FWD(f));
Tools::for_range<I0>(std::make_index_sequence<std::size_t(I1-I0)>{}, FWD(f));
}
template <std::size_t N, class Functor>
constexpr void for_range(index_t<N>, Functor&& f)
{
Tools::for_each(range_t<0,N>{}, FWD(f));
Tools::for_range(std::make_index_sequence<N>{}, FWD(f));
}
template <std::size_t I0, std::size_t I1, class Functor>
constexpr void for_range(Functor&& f)
{
Tools::for_each(range_t<I0,I1>{}, FWD(f));
Tools::for_range<I0>(std::make_index_sequence<std::size_t(I1-I0)>{}, FWD(f));
}
} // end namespace Tools
......
......@@ -65,6 +65,12 @@ namespace AMDiS
template <class T>
using owner = T;
/// A functor with no operation
struct NoOp
{
template <class... T>
constexpr void operator()(T&&...) const { /* no nothing */ }
};
/// Create a unique_ptr by copy/move construction
template <class Obj>
......
......@@ -147,7 +147,7 @@ LocalFunction::operator()(Domain const& x) const
auto&& coefficients = *globalFunction_.dofVector_;
auto&& nodeToRangeEntry = globalFunction_.nodeToRangeEntry_;
forEachLeafNode_(*subTree_, [&,this](auto const& node, auto const& tp)
for_each_leaf_node(*subTree_, [&,this](auto const& node, auto const& tp)
{
auto&& fe = node.finiteElement();
auto&& localBasis = fe.localBasis();
......@@ -193,7 +193,7 @@ GradientLocalFunction::operator()(Domain const& x) const
auto&& coefficients = *globalFunction_.dofVector_;
auto&& nodeToRangeEntry = globalFunction_.nodeToRangeEntry_;
forEachLeafNode_(*subTree_, [&,this](auto const& node, auto const& tp)
for_each_leaf_node(*subTree_, [&,this](auto const& node, auto const& tp)
{
// TODO: may DOFVectorView::Range to FieldVector type if necessary
using LocalDerivativeTraits
......
......@@ -2,7 +2,7 @@
#include <amdis/Assembler.hpp>
#include <amdis/LocalOperator.hpp>
#include <amdis/typetree/Visitor.hpp>
#include <amdis/typetree/Traversal.hpp>
#include <amdis/utility/AssembleOperators.hpp>
namespace AMDiS {
......@@ -75,8 +75,8 @@ assemble(RowLocalView const& rowLocalView, ColLocalView const& colLocalView)
auto const& element = rowLocalView.element();
auto geometry = element.geometry();
forEachNode_(rowLocalView.tree(), [&](auto const& rowNode, auto) {
forEachNode_(colLocalView.tree(), [&](auto const& colNode, auto) {
for_each_node(rowLocalView.tree(), [&](auto const& rowNode, auto) {
for_each_node(colLocalView.tree(), [&](auto const& colNode, auto) {
auto& matOp = operators_[rowNode][colNode];
if (matOp) {
matOp.bind(element, geometry);
......
......@@ -2,7 +2,7 @@
#include <amdis/Assembler.hpp>
#include <amdis/LocalOperator.hpp>
#include <amdis/typetree/Visitor.hpp>
#include <amdis/typetree/Traversal.hpp>
#include <amdis/utility/AssembleOperators.hpp>
namespace AMDiS {
......@@ -66,7 +66,7 @@ assemble(LocalView const& localView)
auto const& element = localView.element();
auto geometry = element.geometry();
forEachNode_(localView.tree(), [&](auto const& node, auto) {
for_each_node(localView.tree(), [&](auto const& node, auto) {
auto& rhsOp = operators_[node];
if (rhsOp) {
rhsOp.bind(element, geometry);
......
......@@ -7,5 +7,4 @@ install(FILES
TreeContainer.hpp
TreeData.hpp
TreePath.hpp
Visitor.hpp
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/amdis/typetree)
......@@ -3,139 +3,257 @@
#include <dune/common/hybridutilities.hh>
#include <dune/common/rangeutilities.hh>
#include <dune/typetree/childextraction.hh>
#include <dune/typetree/nodetags.hh>
#include <dune/typetree/treepath.hh>
#include <dune/typetree/visitor.hh>
#include <amdis/common/ForEach.hpp>
#include <amdis/common/Logical.hpp>
#include <amdis/common/Range.hpp>
#include <amdis/common/TypeTraits.hpp>
namespace AMDiS
{
// forward declaration of main engine struct
template <typename NodeTag, bool visit = true>
struct TraverseTree;
// Do not visit nodes the visitor is not interested in
template <typename NodeTag>
struct TraverseTree<NodeTag, false>
{
template <typename Node, typename Visitor, typename TreePath>
static void apply(const Node& node, const Visitor& visitor, TreePath const& tp)
{}
};
#ifndef DOXYGEN
// some implementation details
template <class Node, class Index>
struct HybridChildType
: HybridChildType<std::remove_const_t<Node>, std::remove_const_t<Index>> {};
template <class Node>
struct HybridChildType<Node, std::size_t>
{
using type = typename Node::template Child<0>::Type;
};
template <class Node, std::size_t K>
struct HybridChildType<Node, Dune::index_constant<K>>
{
using type = typename Node::template Child<K>::Type;
};
template <class NodeTag, class Node>
constexpr std::size_t hybridDegree(NodeTag, Node const& node)
{
return Dune::TypeTree::degree(node);
}
template <class Node>
constexpr auto hybridDegree(Dune::TypeTree::CompositeNodeTag, Node const& node)
{
return Dune::index_constant<Node::CHILDREN>{};
}
template <std::size_t k, std::size_t n>
constexpr bool notLastChild(Dune::index_constant<k> const&, Dune::index_constant<n> const&)
{
return k < n-1;
}
constexpr bool notLastChild(std::size_t k, std::size_t n)
{
return k < n-1;
}
#endif
template <class NodeTag>
struct TraverseTree<NodeTag, true>
{
template <typename N, typename V, typename TreePath>
static void apply(N&& n, V&& v, TreePath const& tp)
// NOTE: backport of dune/typetree/traversal.hpp from Dune 2.7
namespace AMDiS {
enum class TreePathType
{
using Node = std::remove_reference_t<N>;
using Visitor = std::remove_reference_t<V>;
DYNAMIC, STATIC
};
namespace Impl {
// This is a constexpr version of the ternery operator c?t1:t1.
// In contrast to the latter the type of t1 and t2 can be different.
// Notice that std::conditional would not do the trick, because
// it only selects between types.
template<bool c, class T1, class T2,
std::enable_if_t<c, int> = 0>
constexpr auto conditionalValue(T1&& t1, T2&& t2) {
return std::forward<T1>(t1);
}
template<bool c, class T1, class T2,
std::enable_if_t<not c, int> = 0>
constexpr auto conditionalValue(T1&& t1, T2&& t2) {
return std::forward<T2>(t2);
}
/* The signature is the same as for the public applyToTree
* function in Dune::Typetree, despite the additionally passed
* treePath argument. The path passed here is associated to
* the tree and the relative paths of the children (wrt. to tree)
* are appended to this. Hence the behavior of the public function
* is resembled by passing an empty treePath.
*/
/*
* This is the overload for leaf traversal
*/
template<class T, class TP, class V,
std::enable_if_t<remove_cvref_t<T>::isLeaf, int> = 0>
void apply_to_tree(T&& tree, TP treePath, V&& visitor)
{
visitor.leaf(tree, treePath);
}
/*
* This is the general overload doing child traversal.
*/
template<class T, class TP, class V,
std::enable_if_t<not remove_cvref_t<T>::isLeaf, int> = 0>
void apply_to_tree(T&& tree, TP treePath, V&& visitor)
{
// Do we really want to take care for const-ness of the Tree
// when instantiating VisitChild below? I'd rather expect this:
// using Tree = remove_cvref_t<T>;
// using Visitor = remove_cvref_t<V>;
using Tree = std::remove_reference_t<T>;
using Visitor = std::remove_reference_t<V>;
visitor.pre(tree, treePath);
// Use statically encoded degree unless tree
// is a power node and dynamic traversal is requested.
constexpr auto useDynamicTraversal = (Tree::isPower and Visitor::treePathType==TreePathType::DYNAMIC);
auto degree = conditionalValue<useDynamicTraversal>(Tree::degree(), Dune::index_constant<Tree::degree()>{});
auto indices = Dune::range(degree);
Dune::Hybrid::forEach(indices, [&](auto i) {
auto childTP = Dune::TypeTree::push_back(treePath, i);
auto&& child = tree.child(i);
using Child = TYPEOF(child);
visitor.beforeChild(tree, child, treePath, i);
// This requires that visiotor.in(...) can always be instantiated,
// even if there's a single child only.
if (i>0)
visitor.in(tree, treePath);
static constexpr auto visitChild = Visitor::template VisitChild<Tree,Child,TP>::value;
#if AMDIS_HAS_CXX_CONSTEXPR_IF
if constexpr(visitChild)
applyToTree(child, childTP, visitor);
#else // AMDIS_HAS_CXX_CONSTEXPR_IF
Dune::Hybrid::ifElse(bool_t<visitChild>{}, [&] (auto /*id*/) {
applyToTree(child, childTP, visitor);
});
#endif // AMDIS_HAS_CXX_CONSTEXPR_IF
visitor.afterChild(tree, child, treePath, i);
});
visitor.post(tree, treePath);
}
// Overload for leaf nodes
template<class Tree, class TP, class Pre, class Leaf, class Post,
std::enable_if_t<remove_cvref_t<Tree>::isLeaf, int> = 0>
void for_each_node(Tree&& tree, TP treePath, Pre&& /*preFunc*/, Leaf&& leafFunc, Post&& /*postFunc*/)
{
leafFunc(tree, treePath);
}
// Overload for non-leaf nodes
// Forward declaration needed for recursion
template<class Tree, class TP, class Pre, class Leaf, class Post,
std::enable_if_t<not remove_cvref_t<Tree>::isLeaf,int> = 0>
void for_each_node(Tree&& tree, TP treePath, Pre&& preFunc, Leaf&& leafFunc, Post&& postFunc);
// Helper for power nodes
template<class Tree, class TP, class Pre, class Leaf, class Post, std::size_t... I,
std::enable_if_t<remove_cvref_t<Tree>::isPower, int> = 0>
void for_each_node_unfold(Tree&& tree, TP treePath, Pre&& preFunc, Leaf&& leafFunc, Post&& postFunc, std::index_sequence<I...>)
{
for (std::size_t i = 0; i < sizeof...(I); ++i)
Impl::for_each_node(tree.child(i), Dune::TypeTree::push_back(treePath, i), preFunc, leafFunc, postFunc);
}
v.pre(FWD(n),tp);
// Helper for composite nodes
template<class Tree, class TP, class Pre, class Leaf, class Post, std::size_t... I,
std::enable_if_t<not remove_cvref_t<Tree>::isPower, int> = 0>
void for_each_node_unfold(Tree&& tree, TP treePath, Pre&& preFunc, Leaf&& leafFunc, Post&& postFunc, std::index_sequence<I...>)
{
(void)std::initializer_list<int>{(
Impl::for_each_node(tree.child(Dune::index_constant<I>{}),
Dune::TypeTree::push_back(treePath, Dune::index_constant<I>{}), preFunc, leafFunc, postFunc),0)...
};
}
auto const deg = hybridDegree(NodeTag{}, n);
Dune::Hybrid::forEach(Dune::range(deg), [&](auto const _k)
/*
* Traverse tree and visit each node. The signature is the same
* as for the public for_each_node function in Dune::Typtree,
* despite the additionally passed treePath argument. The path
* passed here is associated to the tree and the relative
* paths of the children (wrt. to tree) are appended to this.
* Hence the behavior of the public function is resembled
* by passing an empty treePath.
*
* See also the specialization for leaf-nodes.
*/
template<class Tree, class TP, class Pre, class Leaf, class Post,
std::enable_if_t<not remove_cvref_t<Tree>::isLeaf,int>>
void for_each_node(Tree&& tree, TP treePath, Pre&& preFunc, Leaf&& leafFunc, Post&& postFunc)
{
// always call beforeChild(), regardless of the value of visit
v.beforeChild(FWD(n),n.child(_k),tp,_k);
auto indices = std::make_index_sequence<TYPEOF(tree)::degree()>{};
preFunc(tree, treePath);
Impl::for_each_node_unfold(tree, treePath, preFunc, leafFunc, postFunc, indices);
postFunc(tree, treePath);
}
} // namespace Impl
// ********************************************************************************
// Public Interface
// ********************************************************************************
// descend to child
using C = typename HybridChildType<Node, decltype(_k)>::type;
const bool visit = Visitor::template VisitChild<Node,C,TreePath>::value;
TraverseTree<Dune::TypeTree::NodeTag<C>,visit>::apply(n.child(_k),FWD(v),push_back(tp, _k));
//! Apply visitor to TypeTree.
/**
* \code
* #include <amdis/typetree/Traversal.hpp>
* \endcode
* This function applies the given visitor to the given tree. Both visitor and tree may be const
* or non-const (if the compiler supports rvalue references, they may even be a non-const temporary).
*
* \note The visitor must implement the interface laid out by DefaultVisitor (most easily achieved by
* inheriting from it) and specify the required type of tree traversal (static or dynamic) by
* inheriting from either StaticTraversal or DynamicTraversal.
*
* \param tree The tree the visitor will be applied to.
* \param visitor The visitor to apply to the tree.
*/
template<typename Tree, typename Visitor>
void apply_to_tree(Tree&& tree, Visitor&& visitor)
{
auto root = Dune::TypeTree::hybridTreePath();
Impl::apply_to_tree(tree, root, visitor);
}
// always call afterChild(), regardless of the value of visit
v.afterChild(FWD(n),n.child(_k),tp,_k);
/**
* \brief Traverse tree and visit each node
*
* All passed callback functions are called with the
* node and corresponding treepath as arguments.
*
* \param tree The tree to traverse
* \param preFunc This function is called for each inner node before visiting its children
* \param leafFunc This function is called for each leaf node
* \param postFunc This function is called for each inner node after visiting its children
*/
template<class Tree, class Pre, class Leaf, class Post>
void for_each_node(Tree&& tree, Pre&& preFunc, Leaf&& leafFunc, Post&& postFunc)
{