diff --git a/dumux/linear/helmholtzoperator.hh b/dumux/linear/helmholtzoperator.hh
new file mode 100644
index 0000000000000000000000000000000000000000..b025c00a1808eb5e80cf1896572f050a33b28bc7
--- /dev/null
+++ b/dumux/linear/helmholtzoperator.hh
@@ -0,0 +1,316 @@
+// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+// vi: set et ts=4 sw=4 sts=4:
+/*****************************************************************************
+ *   See the file COPYING for full copying permissions.                      *
+ *                                                                           *
+ *   This program is free software: you can redistribute it and/or modify    *
+ *   it under the terms of the GNU General Public License as published by    *
+ *   the Free Software Foundation, either version 3 of the License, or       *
+ *   (at your option) any later version.                                     *
+ *                                                                           *
+ *   This program is distributed in the hope that it will be useful,         *
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of          *
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the            *
+ *   GNU General Public License for more details.                            *
+ *                                                                           *
+ *   You should have received a copy of the GNU General Public License       *
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
+ *****************************************************************************/
+/*!
+ * \file
+ * \ingroup Linear
+ * \brief Scalar Helmholtz operator to be used as a solver component
+ */
+#ifndef DUMUX_LINEAR_HELMHOLTZ_OPERATOR_HH
+#define DUMUX_LINEAR_HELMHOLTZ_OPERATOR_HH
+
+#include <dumux/common/math.hh>
+#include <dumux/common/properties.hh>
+#include <dumux/common/numeqvector.hh>
+#include <dumux/discretization/cellcentered/tpfa/computetransmissibility.hh>
+#include <dumux/common/boundarytypes.hh>
+#include <dumux/common/fvproblem.hh>
+#include <dumux/assembly/fvassembler.hh>
+
+namespace Dumux::Detail::HelmholtzOperator {
+
+template <class Traits>
+class HelmholtzModelVolumeVariables
+{
+    using Scalar = typename Traits::PrimaryVariables::value_type;
+public:
+    using PrimaryVariables = typename Traits::PrimaryVariables;
+
+    template<class ElementSolution, class Problem, class Element, class SubControlVolume>
+    void update(const ElementSolution& elemSol, const Problem&, const Element&, const SubControlVolume& scv)
+    { priVars_ = elemSol[scv.indexInElement()]; }
+
+    Scalar priVar(const int pvIdx) const { return priVars_[pvIdx]; }
+    const PrimaryVariables& priVars() const { return priVars_; }
+    Scalar extrusionFactor() const { return 1.0; }
+
+private:
+    PrimaryVariables priVars_;
+};
+
+template<class TypeTag>
+class HelmholtzModelLocalResidual
+: public GetPropType<TypeTag, Properties::BaseLocalResidual>
+{
+    using ParentType = GetPropType<TypeTag, Properties::BaseLocalResidual>;
+    using Scalar = GetPropType<TypeTag, Properties::Scalar>;
+    using Problem = GetPropType<TypeTag, Properties::Problem>;
+    using NumEqVector = Dumux::NumEqVector<GetPropType<TypeTag, Properties::PrimaryVariables>>;
+    using VolumeVariables = typename GetPropType<TypeTag, Properties::GridVolumeVariables>::VolumeVariables;
+    using ElementVolumeVariables = typename GetPropType<TypeTag, Properties::GridVolumeVariables>::LocalView;
+    using ElementFluxVariablesCache = typename GetPropType<TypeTag, Properties::GridFluxVariablesCache>::LocalView;
+    using GridGeometry = GetPropType<TypeTag, Properties::GridGeometry>;
+    using FVElementGeometry = typename GridGeometry::LocalView;
+    using SubControlVolume = typename FVElementGeometry::SubControlVolume;
+    using SubControlVolumeFace = typename FVElementGeometry::SubControlVolumeFace;
+    using GridView = typename GridGeometry::GridView;
+    using Element = typename GridView::template Codim<0>::Entity;
+    static constexpr int dimWorld = GridView::dimensionworld;
+public:
+    using ParentType::ParentType;
+
+    NumEqVector computeStorage(const Problem&,
+                               const SubControlVolume&,
+                               const VolumeVariables&) const
+    { return NumEqVector(0.0); }
+
+    NumEqVector computeFlux(const Problem& problem,
+                            const Element&, const FVElementGeometry& fvGeometry,
+                            const ElementVolumeVariables& elemVolVars,
+                            const SubControlVolumeFace& scvf,
+                            const ElementFluxVariablesCache& elemFluxVarsCache) const
+    {
+        NumEqVector flux(0.0);
+
+        // CVFE schemes
+        if constexpr (GridGeometry::discMethod == DiscretizationMethods::box
+            || GridGeometry::discMethod == DiscretizationMethods::fcdiamond
+            || GridGeometry::discMethod == DiscretizationMethods::pq1bubble)
+        {
+            const auto& fluxVarCache = elemFluxVarsCache[scvf];
+            Dune::FieldVector<Scalar, dimWorld> gradConcentration(0.0);
+            for (const auto& scv : scvs(fvGeometry))
+            {
+                const auto& volVars = elemVolVars[scv];
+                gradConcentration.axpy(volVars.concentration(), fluxVarCache.gradN(scv.indexInElement()));
+            }
+
+            NumEqVector flux;
+            flux[0] = -1.0*vtmv(scvf.unitOuterNormal(), problem.a(), gradConcentration)*scvf.area();
+        }
+
+        // CCTpfa
+        else if constexpr (GridGeometry::discMethod == DiscretizationMethods::cctpfa)
+        {
+            const auto& insideScv = fvGeometry.scv(scvf.insideScvIdx());
+            const auto& insideV = elemVolVars[insideScv];
+            const auto& outsideV = elemVolVars[scvf.outsideScvIdx()];
+            const auto ti = computeTpfaTransmissibility(
+                fvGeometry, scvf, insideScv, problem.a(), insideV.extrusionFactor()
+            );
+
+            const auto tij = [&]{
+                if (scvf.boundary())
+                    return scvf.area()*ti;
+                else
+                {
+                    const auto& outsideScv = fvGeometry.scv(scvf.outsideScvIdx());
+                    const Scalar tj = -computeTpfaTransmissibility(
+                        fvGeometry, scvf, outsideScv, problem.a(), outsideV.extrusionFactor()
+                    );
+
+                    return scvf.area()*(ti * tj)/(ti + tj);
+                }
+            }();
+
+            const auto u = insideV.priVar(0);
+            const auto v = outsideV.priVar(0);
+            flux[0] = tij*(u - v);
+        }
+        else
+            DUNE_THROW(Dune::NotImplemented, "Discretization method " << GridGeometry::discMethod);
+
+        return flux;
+    }
+
+    NumEqVector computeSource(const Problem& problem,
+                              const Element&, const FVElementGeometry&,
+                              const ElementVolumeVariables& elemVolVars,
+                              const SubControlVolume& scv) const
+    {
+        NumEqVector source(0.0);
+        source[0] = -problem.b()*elemVolVars[scv].priVar(0);
+        return source;
+    }
+};
+
+template<class TypeTag>
+class HelmholtzModelHomogeneousNeumannProblem : public FVProblem<TypeTag>
+{
+    using ParentType = FVProblem<TypeTag>;
+    using GridGeometry = GetPropType<TypeTag, Properties::GridGeometry>;
+    using Scalar = GetPropType<TypeTag, Properties::Scalar>;
+    using BoundaryTypes = Dumux::BoundaryTypes<GetPropType<TypeTag, Properties::ModelTraits>::numEq()>;
+public:
+    HelmholtzModelHomogeneousNeumannProblem(std::shared_ptr<const GridGeometry> gridGeometry, Scalar a, Scalar b)
+    : ParentType(gridGeometry)
+    , a_(a), b_(b)
+    {}
+
+    template<class GlobalPosition>
+    BoundaryTypes boundaryTypesAtPos(const GlobalPosition& globalPos) const
+    { BoundaryTypes values; values.setAllNeumann(); return values; }
+
+    Scalar a() const { return a_; }
+    Scalar b() const { return b_; }
+private:
+    Scalar a_, b_;
+};
+
+template<class G, class D, class S>
+struct Policy
+{
+    using Scalar = S;
+    using Grid = G;
+    using Discretization = D;
+};
+
+template<class P>
+struct TTag {
+    using InheritsFrom = std::tuple<typename P::Discretization>;
+};
+
+} // end namespace Dumux::Detail::HelmholtzOperator
+
+namespace Dumux::Properties {
+
+//! Set the default type of scalar values to double
+template<class TypeTag, class P>
+struct Scalar<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{ using type = typename P::Scalar; };
+
+//! Set the default primary variable vector to a vector of size of number of equations
+template<class TypeTag, class P>
+struct PrimaryVariables<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{
+    using type = Dune::FieldVector<
+        GetPropType<TypeTag, Properties::Scalar>,
+        GetPropType<TypeTag, Properties::ModelTraits>::numEq()
+    >;
+};
+
+template<class TypeTag, class P>
+struct ModelTraits<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{
+    struct Traits { static constexpr int numEq() { return 1; } };
+    using type = Traits;
+};
+
+template<class TypeTag, class P>
+struct LocalResidual<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{ using type = Dumux::Detail::HelmholtzOperator::HelmholtzModelLocalResidual<TypeTag>; };
+
+template<class TypeTag, class P>
+struct Problem<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{ using type = Dumux::Detail::HelmholtzOperator::HelmholtzModelHomogeneousNeumannProblem<TypeTag>; };
+
+template<class TypeTag, class P>
+struct VolumeVariables<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{
+    struct Traits { using PrimaryVariables = GetPropType<TypeTag, Properties::PrimaryVariables>; };
+    using type = Dumux::Detail::HelmholtzOperator::HelmholtzModelVolumeVariables<Traits>;
+};
+
+template<class TypeTag, class P>
+struct EnableGridVolumeVariablesCache<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{ static constexpr bool value = true; };
+
+template<class TypeTag, class P>
+struct EnableGridFluxVariablesCache<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{ static constexpr bool value = true; };
+
+template<class TypeTag, class P>
+struct EnableGridGeometryCache<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{ static constexpr bool value = true; };
+
+template<class TypeTag, class P>
+struct Grid<TypeTag, Dumux::Detail::HelmholtzOperator::TTag<P>>
+{ using type = typename P::Grid; };
+
+} // end namespace Dumux::Properties
+
+namespace Dumux {
+
+/*!
+ * \brief make a Helmholtz matrix operator (aΔ + bI)
+ * \tparam Discretization the discretization model type tag
+ * \param gridView the gridView on which to assemble the discrete operator
+ * \param a the weight for the Laplacian
+ * \param b the weight for the mass matrix
+ *
+ * For example: a=-1.0 and b=0.0 will yield a negative Laplacian operator
+ * For example: a=0.0 and b=1.0 will yield the mass matrix
+ * For example: a=1.0 and b=1.0 will yield the unweighted Helmholtz operator
+ *
+ * Useful as a preconditioner component or for testing solvers
+ */
+template<class Discretization, class GridView, class Scalar>
+auto makeHelmholtzMatrix(const GridView& gridView, const Scalar a = 1.0, const Scalar b = 1.0)
+{
+    using TypeTag = Detail::HelmholtzOperator::TTag<
+        Detail::HelmholtzOperator::Policy<typename GridView::Grid, Discretization, Scalar>
+    >;
+    using GridGeometry = GetPropType<TypeTag, Properties::GridGeometry>;
+    auto gridGeometry = std::make_shared<GridGeometry>(gridView);
+
+    using Problem = GetPropType<TypeTag, Properties::Problem>;
+    auto problem = std::make_shared<Problem>(gridGeometry, a, b);
+
+    using SolutionVector = GetPropType<TypeTag, Properties::SolutionVector>;
+    using GridVariables = GetPropType<TypeTag, Properties::GridVariables>;
+    auto gridVariables = std::make_shared<GridVariables>(problem, gridGeometry);
+    SolutionVector x(gridGeometry->numDofs());
+    gridVariables->init(x);
+
+    using Assembler = FVAssembler<TypeTag, DiffMethod::numeric>;
+    auto assembler = std::make_shared<Assembler>(problem, gridGeometry, gridVariables);
+    using A = typename Assembler::JacobianMatrix;
+    using V = typename Assembler::ResidualType;
+    auto jacobian = std::make_shared<A>();
+    auto residual = std::make_shared<V>();
+    assembler->setLinearSystem(jacobian, residual);
+    assembler->assembleJacobian(x);
+    return jacobian;
+};
+
+/*!
+ * \brief make a Laplace matrix operator aΔ
+ */
+template<class Discretization, class GridView, class Scalar>
+auto makeLaplaceMatrix(const GridView& gridView, const Scalar a = 1.0)
+{ return createHelmholtzMatrix(gridView, a, 0.0); };
+
+template<class LinearOperator, class Discretization, class GridView, class Scalar>
+std::shared_ptr<LinearOperator> makeHelmholtzLinearOperator(const GridView& gridView, const Scalar a, const Scalar b)
+{ return std::make_shared<LinearOperator>(makeHelmholtzMatrix<Discretization>(gridView, a, b)); }
+
+template<class LinearOperator, class Discretization, class GridView, class Comm, class Scalar>
+std::shared_ptr<LinearOperator> makeHelmholtzLinearOperator(const GridView& gridView, const Comm& comm, const Scalar a, const Scalar b)
+{ return std::make_shared<LinearOperator>(makeHelmholtzMatrix<Discretization>(gridView, a, b), comm); }
+
+template<class LinearOperator, class Discretization, class GridView, class Scalar>
+std::shared_ptr<LinearOperator> makeLaplaceLinearOperator(const GridView& gridView, const Scalar a)
+{ return std::make_shared<LinearOperator>(makeLaplaceMatrix<Discretization>(gridView, a)); }
+
+template<class LinearOperator, class Discretization, class GridView, class Comm, class Scalar>
+std::shared_ptr<LinearOperator> makeLaplaceLinearOperator(const GridView& gridView, const Comm& comm, const Scalar a)
+{ return std::make_shared<LinearOperator>(makeLaplaceMatrix<Discretization>(gridView, a), comm); }
+
+} // end namespace Dumux
+
+#endif
diff --git a/test/linear/CMakeLists.txt b/test/linear/CMakeLists.txt
index 328f8065d9b00988ab7081e846eda29ef2a6d564..c401d7b0b1d2b8038f2340324a4fefc7e5aeab27 100644
--- a/test/linear/CMakeLists.txt
+++ b/test/linear/CMakeLists.txt
@@ -2,6 +2,9 @@ dune_symlink_to_source_files(FILES "params.input")
 dumux_add_test(NAME test_linearsolver
                SOURCES test_linearsolver.cc
                LABELS linear unit)
+dumux_add_test(NAME test_parallel_amg_smoothers
+               SOURCES test_parallel_amg_smoothers.cc
+               LABELS linear unit)
 dumux_add_test(NAME test_scalarproducts
                SOURCES test_scalarproducts.cc
                LABELS linear unit)
diff --git a/test/linear/test_parallel_amg_smoothers.cc b/test/linear/test_parallel_amg_smoothers.cc
new file mode 100644
index 0000000000000000000000000000000000000000..013bdfa375107a0e292b9a1046f20d1670acd4de
--- /dev/null
+++ b/test/linear/test_parallel_amg_smoothers.cc
@@ -0,0 +1,97 @@
+#include <config.h>
+
+#include <iostream>
+
+#include <dune/common/timer.hh>
+#include <dune/common/exceptions.hh>
+#include <dune/common/fvector.hh>
+#include <dune/grid/yaspgrid.hh>
+#include <dune/istl/bvector.hh>
+#include <dune/istl/solvers.hh>
+#include <dune/istl/paamg/amg.hh>
+
+#include <dumux/common/initialize.hh>
+#include <dumux/common/parameters.hh>
+#include <dumux/discretization/cctpfa.hh>
+#include <dumux/linear/helmholtzoperator.hh>
+#include <dumux/linear/preconditioners.hh>
+
+template<class Smoother, class LOP, class Vector>
+void testSmoother(LOP& lop, const Vector& b)
+{
+    using Comm = Dune::Amg::SequentialInformation;
+    using Amg = Dune::Amg::AMG<LOP, Vector, Smoother, Comm>;
+    using SmootherArgs = typename Dune::Amg::SmootherTraits<Smoother>::Arguments;
+    using Criterion = Dune::Amg::CoarsenCriterion<Dune::Amg::SymmetricCriterion<typename LOP::matrix_type, Dune::Amg::FirstDiagonal>>;
+
+    Dune::Amg::Parameters params(15, 2000, 1.2, 1.6, Dune::Amg::atOnceAccu);
+    Criterion criterion(params);
+    SmootherArgs smootherArgs;
+    smootherArgs.iterations = 2;
+    smootherArgs.relaxationFactor = 0.8;
+    auto comm = std::make_shared<Comm>();
+    auto amg = std::make_shared<Amg>(lop, criterion, smootherArgs, *comm);
+    Dune::CGSolver<Vector> solver(lop, *amg, 1e-15, 200, 1);
+    Dune::InverseOperatorResult result;
+
+    Dune::Timer timer;
+
+    auto x = b; x = 0.0; auto bTmp = b;
+    solver.apply(x, bTmp, result);
+    if (!result.converged)
+        DUNE_THROW(Dune::Exception, "Solver did not converge!");
+
+    std::cout << "Solver took " << timer.elapsed() << " seconds" << std::endl;
+
+    using std::abs;
+    if (abs(x.two_norm2() - x.size()) > 1e-10*x.size())
+        DUNE_THROW(Dune::Exception,
+            "Wrong result: " << x.two_norm2()
+            << " (expected " << x.size() << ")"
+            << " difference " << abs(x.two_norm2() - x.size())
+            << " tolerance " << 1e-10*x.size()
+        );
+}
+
+int main(int argc, char* argv[])
+{
+    using namespace Dumux;
+
+    Dumux::initialize(argc, argv);
+
+    Parameters::init([](Dune::ParameterTree& p){
+        p["Problem.Name"] = "problem";
+    });
+
+    using Grid = Dune::YaspGrid<2>;
+    using Pos = Dune::FieldVector<typename Grid::ctype, 2>;
+    std::array<int, 2> cells{{300, 300}};
+    auto grid = std::make_shared<Grid>(Pos(1.0), cells);
+
+    Dune::Timer timer;
+    auto helmholtzMatrix = makeHelmholtzMatrix<
+        Dumux::Properties::TTag::CCTpfaModel
+    >(grid->leafGridView(), 1.0, 1.0);
+    std::cout << "Operator assembly took " << timer.elapsed() << " seconds" << std::endl;
+
+    using Matrix = std::decay_t<decltype(helmholtzMatrix)>::element_type;
+    using Vector = Dune::BlockVector<Dune::FieldVector<double, 1>>;
+    Vector x(helmholtzMatrix->N());
+    Vector b(helmholtzMatrix->M());
+    x = 1.0;
+
+    // test the other interface too
+    timer.reset();
+    auto lop = makeHelmholtzLinearOperator<
+        Dune::MatrixAdapter<Matrix, Vector, Vector>,
+        Dumux::Properties::TTag::CCTpfaModel
+    >(grid->leafGridView(), 1.0, 1.0);
+    lop->apply(x, b);
+    std::cout << "Operator assembly took " << timer.elapsed() << " seconds" << std::endl;
+
+    testSmoother<Dumux::ParMTSSOR<Matrix, Vector, Vector>>(*lop, b);
+    testSmoother<Dumux::ParMTSOR<Matrix, Vector, Vector>>(*lop, b);
+    testSmoother<Dumux::ParMTJac<Matrix, Vector, Vector>>(*lop, b);
+
+    return 0;
+}