changeset 924:12e8e431b43c feature/laplace_opset

Start restructuring Laplace making it more minimal.
author Vidar Stiernström <vidar.stiernstrom@it.uu.se>
date Mon, 21 Feb 2022 13:12:47 +0100
parents 88bf50821cf5
children 6b47a9ee1632
files src/SbpOperators/volumeops/laplace/laplace.jl test/SbpOperators/volumeops/laplace/laplace_test.jl
diffstat 2 files changed, 58 insertions(+), 325 deletions(-) [+]
line wrap: on
line diff
--- a/src/SbpOperators/volumeops/laplace/laplace.jl	Mon Feb 21 13:11:17 2022 +0100
+++ b/src/SbpOperators/volumeops/laplace/laplace.jl	Mon Feb 21 13:12:47 2022 +0100
@@ -1,157 +1,38 @@
-export Laplace
-export laplace
-# REVIEW: Makes more sense to me to have the exports at the top of the file.
-# Might as well start fixing that.
-
-# REVIEW:
-# Design discussions has led to attempt a restructuring of Laplace to a more
-# minimal type, holding the tensor mapping and a stencil set. This allows
-# construction of associated tensor mappings, e.g. boundary operators, based on the
-# stencil set while keeping the type simpler.
-
-# REVIEW: The style of name `Laplace` might clash with other concepts. When
-# thinking about implementing the variable second derivative I think I will
-# have to create it as a full TM for the full dimensional problem instead of
-# building it as a 1D operator and then use that with outer products. The
-# natural name there would be `VariableSecondDerivative` (or something
-# similar). But the similarity of the two names would suggest that `Laplace`
-# and `VariableSecondDerivative` are the same kind of thing, which they
-# wouldn't be.
-#
-# How do we distinguish the kind of type we are implementing here and what we
-# could potentially do for the variable second derivative?
-#
-# I see two ways out:
-#   * Come up with a name for these sets of operators and change `Laplace` accordingly.
-#   * Come up with a name for the bare operators and change `VariableSecondDerivative` accordingly.
-
 """
-    Laplace{T, Dim, TMdiffop} <: TensorMapping{T,Dim,Dim}
-    Laplace(grid, filename; order)
+    Laplace{T, DiffOp} <: TensorMapping{T,Dim,Dim}
+    Laplace(grid::Equidistant, stencil_set)
 
 Implements the Laplace operator, approximating ∑d²/xᵢ² , i = 1,...,`Dim` as a
-`TensorMapping`. Additionally, `Laplace` stores the inner product and boundary
-operators relevant for constructing a SBP finite difference scheme as a `TensorMapping`.
-
-`Laplace(grid, filename; order)` creates the Laplace operator defined on `grid`,
-where the operators are read from TOML. The differential operator is created
-using `laplace(grid,...)`.
-
-Note that all properties of Laplace, excluding the differential operator `Laplace.D`, are
-abstract types. For performance reasons, they should therefore be
-accessed via the provided getter functions (e.g `inner_product(::Laplace)`).
+`TensorMapping`. Additionally `Laplace` stores the stencil set (parsed from TOML) 
+used to construct the `TensorMapping`.
 """
-struct Laplace{T, Dim, TMdiffop<:TensorMapping{T,Dim,Dim}} <: TensorMapping{T,Dim,Dim}
-    D::TMdiffop # Differential operator
-    H::TensorMapping # Inner product operator
-    H_inv::TensorMapping # Inverse inner product operator
-    e::StaticDict{<:BoundaryIdentifier,<:TensorMapping} # Boundary restriction operators.
-    d::StaticDict{<:BoundaryIdentifier,<:TensorMapping} # Normal derivative operators
-    H_boundary::StaticDict{<:BoundaryIdentifier,<:TensorMapping} # Boundary quadrature operators
+struct Laplace{T, DiffOp<:TensorMapping{T,Dim,Dim}} <: TensorMapping{T,Dim,Dim}
+    D::DiffOp# Differential operator
+    stencil_set # Stencil set of the operator
 end
 
-function Laplace(grid, filename; order)
-    
-    # Read stencils
-    stencil_set = read_stencil_set(filename; order)
-    # TODO: Removed once we can construct the volume and
-    # boundary operators by op(grid, read_stencil_set(fn; order,...)).
-    D_inner_stecil = parse_stencil(stencil_set["D2"]["inner_stencil"])
-    D_closure_stencils = parse_stencil.(stencil_set["D2"]["closure_stencils"])
-    H_inner_stencils = parse_scalar(stencil_set["H"]["inner"])
-    H_closure_stencils = parse_tuple(stencil_set["H"]["closure"])
-    e_closure_stencil = parse_stencil(stencil_set["e"]["closure"])
-    d_closure_stencil = parse_stencil(stencil_set["d1"]["closure"])
-    # REVIEW: Do we add the methods to get rid of this in this branch or a new one?
+"""
+    `Laplace(grid::Equidistant, stencil_set)`
 
-    # Volume operators
-    Δ =  laplace(grid, D_inner_stecil, D_closure_stencils)
-    H =  inner_product(grid, H_inner_stencils, H_closure_stencils)
-    H⁻¹ = inverse_inner_product(grid, H_inner_stencils, H_closure_stencils)
-
-    # Boundary operator - id pairs
-    ids = boundary_identifiers(grid)
-    # REVIEW: Change suggestion: Seems more readable to me but pretty subjective so feel free to ignore
-    e_pairs  = map(id -> Pair(id, boundary_restriction(grid, e_closure_stencil, id)),                            ids)
-    d_pairs  = map(id -> Pair(id, normal_derivative(grid, d_closure_stencil, id)),                               ids)
-    Hᵧ_pairs = map(id -> Pair(id, inner_product(boundary_grid(grid, id), H_inner_stencils, H_closure_stencils)), ids)
-
-    return Laplace(Δ, H, H⁻¹, StaticDict(e_pairs), StaticDict(d_pairs), StaticDict(Hᵧ_pairs))
+Creates the `Laplace`` operator `Δ` on `grid` given a parsed TOML
+`stencil_set`. See also [`laplace`](@ref).
+"""
+function Laplace(grid::Equidistant, stencil_set)
+    inner_stencil = parse_stencil(stencil_set["D2"]["inner_stencil"])
+    closure_stencils = parse_stencil.(stencil_set["D2"]["closure_stencils"])
+    Δ = laplace(grid, inner_stencil,closure_stencils)
+    return Laplace(Δ,stencil_set)
 end
 
-# TODO: Consider pretty printing of the following form
-# Base.show(io::IO, L::Laplace{T, Dim}) where {T,Dim,TM} =  print(io, "Laplace{$T, $Dim, $TM}(", L.D, L.H, L.H_inv, L.e, L.d, L.H_boundary, ")")
-# REVIEW: Should leave a todo here to update this once we have some pretty printing for tensor mappings in general.
-
 LazyTensors.range_size(L::Laplace) = LazyTensors.range_size(L.D)
 LazyTensors.domain_size(L::Laplace) = LazyTensors.domain_size(L.D)
 LazyTensors.apply(L::Laplace, v::AbstractArray, I...) = LazyTensors.apply(L.D,v,I...)
 
-
-"""
-    inner_product(L::Laplace)
-
-Returns the inner product operator associated with `L`
-"""
-inner_product(L::Laplace) = L.H
-
-
-"""
-    inverse_inner_product(L::Laplace)
-
-Returns the inverse of the inner product operator associated with `L`
-"""
-inverse_inner_product(L::Laplace) = L.H_inv
-
+# TODO: Implement pretty printing of Laplace once pretty printing of TensorMappings is implemented. 
+# Base.show(io::IO, L::Laplace) = ...
 
 """
-    boundary_restriction(L::Laplace, id::BoundaryIdentifier)
-    boundary_restriction(L::Laplace, ids::Tuple)
-    boundary_restriction(L::Laplace, ids...)
-
-Returns boundary restriction operator(s) associated with `L` for the boundary(s)
-identified by id(s).
-"""
-boundary_restriction(L::Laplace, id::BoundaryIdentifier) = L.e[id]
-boundary_restriction(L::Laplace, ids::Tuple) = map(id-> L.e[id], ids)
-boundary_restriction(L::Laplace, ids...) = boundary_restriction(L, ids)
-# REVIEW: I propose changing the following implementations according to the
-# above. There are some things we're missing with regards to
-# `BoundaryIdentifier`, for example we should be able to handle groups of
-# boundaries as a single `BoundaryIdentifier`. I don't know if we can figure
-# out the interface for that now or if we save it for the future but either
-# way these methods will be affected.
-
-
-
-"""
-    normal_derivative(L::Laplace, id::BoundaryIdentifier)
-    normal_derivative(L::Laplace, ids::NTuple{N,BoundaryIdentifier})
-    normal_derivative(L::Laplace, ids...)
-
-Returns normal derivative operator(s) associated with `L` for the boundary(s)
-identified by id(s).
-"""
-normal_derivative(L::Laplace, id::BoundaryIdentifier) = L.d[id]
-normal_derivative(L::Laplace, ids::NTuple{N,BoundaryIdentifier}) where N = ntuple(i->L.d[ids[i]],N)
-normal_derivative(L::Laplace, ids::Vararg{BoundaryIdentifier,N}) where N = ntuple(i->L.d[ids[i]],N)
-
-
-"""
-    boundary_quadrature(L::Laplace, id::BoundaryIdentifier)
-    boundary_quadrature(L::Laplace, ids::NTuple{N,BoundaryIdentifier})
-    boundary_quadrature(L::Laplace, ids...)
-
-Returns boundary quadrature operator(s) associated with `L` for the boundary(s)
-identified by id(s).
-"""
-boundary_quadrature(L::Laplace, id::BoundaryIdentifier) = L.H_boundary[id]
-boundary_quadrature(L::Laplace, ids::NTuple{N,BoundaryIdentifier}) where N = ntuple(i->L.H_boundary[ids[i]],N)
-boundary_quadrature(L::Laplace, ids::Vararg{BoundaryIdentifier,N}) where N = ntuple(i->L.H_boundary[ids[i]],N)
-
-
-"""
-    laplace(grid::EquidistantGrid{Dim}, inner_stencil, closure_stencils)
+    laplace(grid::EquidistantGrid, inner_stencil, closure_stencils)
 
 Creates the Laplace operator operator `Δ` as a `TensorMapping`
 
@@ -161,12 +42,12 @@
 
 On a one-dimensional `grid`, `Δ` is equivalent to `second_derivative`. On a
 multi-dimensional `grid`, `Δ` is the sum of multi-dimensional `second_derivative`s
-where the sum is carried out lazily.
+where the sum is carried out lazily.  See also [`second_derivative`](@ref).
 """
-function laplace(grid::EquidistantGrid, inner_stencil, closure_stencils)
-    Δ = second_derivative(grid, inner_stencil, closure_stencils, 1)
+function laplace(grid::Equidistant, inner_stencil, closure_stencils)
+    second_derivative(grid, inner_stencil, closure_stencils, 1)
     for d = 2:dimension(grid)
         Δ += second_derivative(grid, inner_stencil, closure_stencils, d)
     end
     return Δ
-end
+end
\ No newline at end of file
--- a/test/SbpOperators/volumeops/laplace/laplace_test.jl	Mon Feb 21 13:11:17 2022 +0100
+++ b/test/SbpOperators/volumeops/laplace/laplace_test.jl	Mon Feb 21 13:12:47 2022 +0100
@@ -4,17 +4,12 @@
 using Sbplib.Grids
 using Sbplib.LazyTensors
 using Sbplib.RegionIndices
-using Sbplib.StaticDicts
 
+# Default stencils (4th order)
 operator_path = sbp_operators_path()*"standard_diagonal.toml"
-# Default stencils (4th order)
 stencil_set = read_stencil_set(operator_path; order=4)
 inner_stencil = parse_stencil(stencil_set["D2"]["inner_stencil"])
 closure_stencils = parse_stencil.(stencil_set["D2"]["closure_stencils"])
-e_closure = parse_stencil(stencil_set["e"]["closure"])
-d_closure = parse_stencil(stencil_set["d1"]["closure"])
-quadrature_interior = parse_scalar(stencil_set["H"]["inner"])
-quadrature_closure = parse_tuple(stencil_set["H"]["closure"])
 
 @testset "Laplace" begin
     g_1D = EquidistantGrid(101, 0.0, 1.)
@@ -23,173 +18,16 @@
 
         @testset "1D" begin
             Δ = laplace(g_1D, inner_stencil, closure_stencils)
-            H = inner_product(g_1D, quadrature_interior, quadrature_closure)
-            Hi = inverse_inner_product(g_1D, quadrature_interior, quadrature_closure)
-
-            (id_l, id_r) = boundary_identifiers(g_1D)
-
-            e_l = boundary_restriction(g_1D, e_closure,id_l)
-            e_r = boundary_restriction(g_1D, e_closure,id_r)
-            e_dict = StaticDict(id_l => e_l, id_r => e_r)
-
-            d_l = normal_derivative(g_1D, d_closure,id_l)
-            d_r = normal_derivative(g_1D, d_closure,id_r)
-            d_dict = StaticDict(id_l => d_l, id_r => d_r)
-
-            H_l = inner_product(boundary_grid(g_1D,id_l), quadrature_interior, quadrature_closure)
-            H_r = inner_product(boundary_grid(g_1D,id_r), quadrature_interior, quadrature_closure)
-            Hb_dict = StaticDict(id_l => H_l, id_r => H_r)
-
-            L = Laplace(g_1D, operator_path; order=4)
-            @test L == Laplace(Δ, H, Hi, e_dict, d_dict, Hb_dict)
-            @test L isa TensorMapping{T,1,1}  where T
-            @inferred Laplace(Δ, H, Hi, e_dict, d_dict, Hb_dict)
-            # REVIEW: The tests above seem very tied to the implementation. Is
-            # it important that the components of the operator set are stored
-            # in static dicts? Is something like below better?
-            #
-            # ```
-            # L = Laplace(g_1D, operator_path; order=4)
-            # @test L isa TensorMapping{T,1,1}  where T
-            # @test boundary_restriction(L,id_l) ==  boundary_restriction(g_1D, e_closure,id_l)
-            # ...
-            # ```
-            # I guess this is more or less simply a reorganization of the test and skipping testing for the struct layout
+            @test Laplace(g_1D, stencil_set) == Laplace(Δ, stencil_set)
+            @test Laplace(g_1D, stencil_set) isa TensorMapping{T,1,1}  where T
         end
         @testset "3D" begin
             Δ = laplace(g_3D, inner_stencil, closure_stencils)
-            H = inner_product(g_3D, quadrature_interior, quadrature_closure)
-            Hi = inverse_inner_product(g_3D, quadrature_interior, quadrature_closure)
-
-            (id_l, id_r, id_s, id_n, id_b, id_t) = boundary_identifiers(g_3D)
-            e_l = boundary_restriction(g_3D, e_closure,id_l)
-            e_r = boundary_restriction(g_3D, e_closure,id_r)
-            e_s = boundary_restriction(g_3D, e_closure,id_s)
-            e_n = boundary_restriction(g_3D, e_closure,id_n)
-            e_b = boundary_restriction(g_3D, e_closure,id_b)
-            e_t = boundary_restriction(g_3D, e_closure,id_t)
-            e_dict = StaticDict(id_l => e_l, id_r => e_r,
-                                id_s => e_s, id_n => e_n,
-                                id_b => e_b, id_t => e_t)
-
-            d_l = normal_derivative(g_3D, d_closure,id_l)
-            d_r = normal_derivative(g_3D, d_closure,id_r)
-            d_s = normal_derivative(g_3D, d_closure,id_s)
-            d_n = normal_derivative(g_3D, d_closure,id_n)
-            d_b = normal_derivative(g_3D, d_closure,id_b)
-            d_t = normal_derivative(g_3D, d_closure,id_t)
-            d_dict = StaticDict(id_l => d_l, id_r => d_r,
-                                id_s => d_s, id_n => d_n,
-                                id_b => d_b, id_t => d_t)
-
-            H_l = inner_product(boundary_grid(g_3D,id_l), quadrature_interior, quadrature_closure)
-            H_r = inner_product(boundary_grid(g_3D,id_r), quadrature_interior, quadrature_closure)
-            H_s = inner_product(boundary_grid(g_3D,id_s), quadrature_interior, quadrature_closure)
-            H_n = inner_product(boundary_grid(g_3D,id_n), quadrature_interior, quadrature_closure)
-            H_b = inner_product(boundary_grid(g_3D,id_b), quadrature_interior, quadrature_closure)
-            H_t = inner_product(boundary_grid(g_3D,id_t), quadrature_interior, quadrature_closure)
-            Hb_dict = StaticDict(id_l => H_l, id_r => H_r,
-                                 id_s => H_s, id_n => H_n,
-                                 id_b => H_b, id_t => H_t)
-
-            L = Laplace(g_3D, operator_path; order=4)
-            @test L == Laplace(Δ,H,Hi,e_dict,d_dict,Hb_dict)
-            @test L isa TensorMapping{T,3,3} where T
-            @inferred Laplace(Δ,H,Hi,e_dict,d_dict,Hb_dict)
-        end
-    end
-
-    # REVIEW: Is this testset misplaced? Should it really be inside the "Laplace" testset?
-    @testset "laplace" begin
-        @testset "1D" begin
-            L = laplace(g_1D, inner_stencil, closure_stencils)
-            @test L == second_derivative(g_1D, inner_stencil, closure_stencils)
-            @test L isa TensorMapping{T,1,1}  where T
-        end
-        @testset "3D" begin
-            L = laplace(g_3D, inner_stencil, closure_stencils)
-            @test L isa TensorMapping{T,3,3} where T
-            Dxx = second_derivative(g_3D, inner_stencil, closure_stencils, 1)
-            Dyy = second_derivative(g_3D, inner_stencil, closure_stencils, 2)
-            Dzz = second_derivative(g_3D, inner_stencil, closure_stencils, 3)
-            @test L == Dxx + Dyy + Dzz
-            @test L isa TensorMapping{T,3,3} where T
+            @test Laplace(g_3D, stencil_set) == Laplace(Δ,stencil_set)
+            @test Laplace(g_3D, stencil_set) isa TensorMapping{T,3,3} where T
         end
     end
 
-    @testset "inner_product" begin
-        L = Laplace(g_3D, operator_path; order=4)
-        @test inner_product(L) == inner_product(g_3D, quadrature_interior, quadrature_closure)
-    end
-
-    @testset "inverse_inner_product" begin
-        L = Laplace(g_3D, operator_path; order=4)
-        @test inverse_inner_product(L) == inverse_inner_product(g_3D, quadrature_interior, quadrature_closure)
-    end
-
-    @testset "boundary_restriction" begin
-        L = Laplace(g_3D, operator_path; order=4)
-        (id_l, id_r, id_s, id_n, id_b, id_t) = boundary_identifiers(g_3D)
-        @test boundary_restriction(L, id_l) == boundary_restriction(g_3D, e_closure,id_l)
-        @test boundary_restriction(L, id_r) == boundary_restriction(g_3D, e_closure,id_r)
-        @test boundary_restriction(L, id_s) == boundary_restriction(g_3D, e_closure,id_s)
-        @test boundary_restriction(L, id_n) == boundary_restriction(g_3D, e_closure,id_n)
-        @test boundary_restriction(L, id_b) == boundary_restriction(g_3D, e_closure,id_b)
-        @test boundary_restriction(L, id_t) == boundary_restriction(g_3D, e_closure,id_t)
-
-        ids = boundary_identifiers(g_3D)
-        es = boundary_restriction(L, ids)
-        @test es ==  (boundary_restriction(L, id_l),
-                      boundary_restriction(L, id_r),
-                      boundary_restriction(L, id_s),
-                      boundary_restriction(L, id_n),
-                      boundary_restriction(L, id_b),
-                      boundary_restriction(L, id_t));
-        @test es == boundary_restriction(L, ids...)
-    end
-
-    @testset "normal_derivative" begin
-        L = Laplace(g_3D, operator_path; order=4)
-        (id_l, id_r, id_s, id_n, id_b, id_t) = boundary_identifiers(g_3D)
-        @test normal_derivative(L, id_l) == normal_derivative(g_3D, d_closure,id_l)
-        @test normal_derivative(L, id_r) == normal_derivative(g_3D, d_closure,id_r)
-        @test normal_derivative(L, id_s) == normal_derivative(g_3D, d_closure,id_s)
-        @test normal_derivative(L, id_n) == normal_derivative(g_3D, d_closure,id_n)
-        @test normal_derivative(L, id_b) == normal_derivative(g_3D, d_closure,id_b)
-        @test normal_derivative(L, id_t) == normal_derivative(g_3D, d_closure,id_t)
-
-        ids = boundary_identifiers(g_3D)
-        ds = normal_derivative(L, ids)
-        @test ds ==  (normal_derivative(L, id_l),
-                      normal_derivative(L, id_r),
-                      normal_derivative(L, id_s),
-                      normal_derivative(L, id_n),
-                      normal_derivative(L, id_b),
-                      normal_derivative(L, id_t));
-        @test ds == normal_derivative(L, ids...)
-    end
-
-    @testset "boundary_quadrature" begin
-        L = Laplace(g_3D, operator_path; order=4)
-        (id_l, id_r, id_s, id_n, id_b, id_t) = boundary_identifiers(g_3D)
-        @test boundary_quadrature(L, id_l) == inner_product(boundary_grid(g_3D, id_l), quadrature_interior, quadrature_closure)
-        @test boundary_quadrature(L, id_r) == inner_product(boundary_grid(g_3D, id_r), quadrature_interior, quadrature_closure)
-        @test boundary_quadrature(L, id_s) == inner_product(boundary_grid(g_3D, id_s), quadrature_interior, quadrature_closure)
-        @test boundary_quadrature(L, id_n) == inner_product(boundary_grid(g_3D, id_n), quadrature_interior, quadrature_closure)
-        @test boundary_quadrature(L, id_b) == inner_product(boundary_grid(g_3D, id_b), quadrature_interior, quadrature_closure)
-        @test boundary_quadrature(L, id_t) == inner_product(boundary_grid(g_3D, id_t), quadrature_interior, quadrature_closure)
-
-        ids = boundary_identifiers(g_3D)
-        H_gammas = boundary_quadrature(L, ids)
-        @test H_gammas ==  (boundary_quadrature(L, id_l),
-                            boundary_quadrature(L, id_r),
-                            boundary_quadrature(L, id_s),
-                            boundary_quadrature(L, id_n),
-                            boundary_quadrature(L, id_b),
-                            boundary_quadrature(L, id_t));
-        @test H_gammas == boundary_quadrature(L, ids...)
-    end
-
     # Exact differentiation is measured point-wise. In other cases
     # the error is measured in the l2-norm.
     @testset "Accuracy" begin
@@ -207,29 +45,43 @@
         # implies that L*v should be exact for binomials up to order 2.
         @testset "2nd order" begin
             stencil_set = read_stencil_set(operator_path; order=2)
-            inner_stencil = parse_stencil(stencil_set["D2"]["inner_stencil"])
-            closure_stencils = parse_stencil.(stencil_set["D2"]["closure_stencils"])
-            L = laplace(g_3D, inner_stencil, closure_stencils)
-            @test L*polynomials[1] ≈ zeros(Float64, size(g_3D)...) atol = 5e-9
-            @test L*polynomials[2] ≈ zeros(Float64, size(g_3D)...) atol = 5e-9
-            @test L*polynomials[3] ≈ polynomials[1] atol = 5e-9
-            @test L*v ≈ Δv rtol = 5e-2 norm = l2
+            Δ = Laplace(g_3D, stencil_set)
+            @test Δ*polynomials[1] ≈ zeros(Float64, size(g_3D)...) atol = 5e-9
+            @test Δ*polynomials[2] ≈ zeros(Float64, size(g_3D)...) atol = 5e-9
+            @test Δ*polynomials[3] ≈ polynomials[1] atol = 5e-9
+            @test Δ*v ≈ Δv rtol = 5e-2 norm = l2
         end
 
         # 4th order interior stencil, 2nd order boundary stencil,
         # implies that L*v should be exact for binomials up to order 3.
         @testset "4th order" begin
             stencil_set = read_stencil_set(operator_path; order=4)
-            inner_stencil = parse_stencil(stencil_set["D2"]["inner_stencil"])
-            closure_stencils = parse_stencil.(stencil_set["D2"]["closure_stencils"])
-            L = laplace(g_3D, inner_stencil, closure_stencils)
+            Δ = Laplace(g_3D, stencil_set)
             # NOTE: high tolerances for checking the "exact" differentiation
             # due to accumulation of round-off errors/cancellation errors?
-            @test L*polynomials[1] ≈ zeros(Float64, size(g_3D)...) atol = 5e-9
-            @test L*polynomials[2] ≈ zeros(Float64, size(g_3D)...) atol = 5e-9
-            @test L*polynomials[3] ≈ polynomials[1] atol = 5e-9
-            @test L*polynomials[4] ≈ polynomials[2] atol = 5e-9
-            @test L*v ≈ Δv rtol = 5e-4 norm = l2
+            @test Δ*polynomials[1] ≈ zeros(Float64, size(g_3D)...) atol = 5e-9
+            @test Δ*polynomials[2] ≈ zeros(Float64, size(g_3D)...) atol = 5e-9
+            @test Δ*polynomials[3] ≈ polynomials[1] atol = 5e-9
+            @test Δ*polynomials[4] ≈ polynomials[2] atol = 5e-9
+            @test Δ*v ≈ Δv rtol = 5e-4 norm = l2
         end
     end
 end
+
+@testset "laplace" begin
+    @testset "1D" begin
+        Δ = laplace(g_1D, inner_stencil, closure_stencils)
+        @test Δ == second_derivative(g_1D, inner_stencil, closure_stencils)
+        @test Δ isa TensorMapping{T,1,1}  where T
+    end
+    @testset "3D" begin
+        Δ = laplace(g_3D, inner_stencil, closure_stencils)
+        @test Δ isa TensorMapping{T,3,3} where T
+        Dxx = second_derivative(g_3D, inner_stencil, closure_stencils, 1)
+        Dyy = second_derivative(g_3D, inner_stencil, closure_stencils, 2)
+        Dzz = second_derivative(g_3D, inner_stencil, closure_stencils, 3)
+        @test Δ == Dxx + Dyy + Dzz
+        @test Δ isa TensorMapping{T,3,3} where T
+    end
+end
+