changeset 1854:654a2b7e6824 tooling/benchmarks

Merge default
author Jonatan Werpers <jonatan@werpers.com>
date Sat, 11 Jan 2025 10:19:47 +0100
parents 2b5480e2d4bf (current diff) a12708e48499 (diff)
children 1566c0dc4e3f
files README.md
diffstat 91 files changed, 3720 insertions(+), 1535 deletions(-) [+]
line wrap: on
line diff
--- a/Manifest.toml	Wed May 31 08:59:34 2023 +0200
+++ b/Manifest.toml	Sat Jan 11 10:19:47 2025 +0100
@@ -1,149 +1,92 @@
 # This file is machine-generated - editing it directly is not advised
 
-julia_version = "1.8.5"
+julia_version = "1.11.2"
 manifest_format = "2.0"
-project_hash = "a36735c53cfa4453f39635046eeaa47a4ea1231b"
-
-[[deps.Adapt]]
-deps = ["LinearAlgebra", "Requires"]
-git-tree-sha1 = "cc37d689f599e8df4f464b2fa3870ff7db7492ef"
-uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
-version = "3.6.1"
-
-[[deps.ArrayInterface]]
-deps = ["ArrayInterfaceCore", "Compat", "IfElse", "LinearAlgebra", "SnoopPrecompile", "Static"]
-git-tree-sha1 = "dedc16cbdd1d32bead4617d27572f582216ccf23"
-uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
-version = "6.0.25"
-
-[[deps.ArrayInterfaceCore]]
-deps = ["LinearAlgebra", "SnoopPrecompile", "SparseArrays", "SuiteSparse"]
-git-tree-sha1 = "e5f08b5689b1aad068e01751889f2f615c7db36d"
-uuid = "30b0a656-2188-435a-8636-2ec0e6a096e2"
-version = "0.1.29"
+project_hash = "a8bfbeb10ca8c44ef39fdba75f02b23fc92b2c6a"
 
 [[deps.Artifacts]]
 uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
-
-[[deps.Compat]]
-deps = ["Dates", "LinearAlgebra", "UUIDs"]
-git-tree-sha1 = "7a60c856b9fa189eb34f5f8a6f6b5529b7942957"
-uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
-version = "4.6.1"
+version = "1.11.0"
 
 [[deps.CompilerSupportLibraries_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
-version = "1.0.1+0"
+version = "1.1.1+0"
 
 [[deps.Dates]]
 deps = ["Printf"]
 uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
-
-[[deps.IfElse]]
-git-tree-sha1 = "debdd00ffef04665ccbb3e150747a77560e8fad1"
-uuid = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173"
-version = "0.1.1"
+version = "1.11.0"
 
 [[deps.Libdl]]
 uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
+version = "1.11.0"
 
 [[deps.LinearAlgebra]]
-deps = ["Libdl", "libblastrampoline_jll"]
+deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
 uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
-
-[[deps.OffsetArrays]]
-deps = ["Adapt"]
-git-tree-sha1 = "82d7c9e310fe55aa54996e6f7f94674e2a38fcb4"
-uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
-version = "1.12.9"
+version = "1.11.0"
 
 [[deps.OpenBLAS_jll]]
 deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
 uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"
-version = "0.3.20+0"
+version = "0.3.27+1"
+
+[[deps.PrecompileTools]]
+deps = ["Preferences"]
+git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f"
+uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
+version = "1.2.1"
 
 [[deps.Preferences]]
 deps = ["TOML"]
-git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d"
+git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6"
 uuid = "21216c6a-2e73-6563-6e65-726566657250"
-version = "1.3.0"
+version = "1.4.3"
 
 [[deps.Printf]]
 deps = ["Unicode"]
 uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
+version = "1.11.0"
 
 [[deps.Random]]
-deps = ["SHA", "Serialization"]
+deps = ["SHA"]
 uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
-
-[[deps.Requires]]
-deps = ["UUIDs"]
-git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7"
-uuid = "ae029012-a4dd-5104-9daa-d747884805df"
-version = "1.3.0"
+version = "1.11.0"
 
 [[deps.SHA]]
 uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
 version = "0.7.0"
 
-[[deps.Serialization]]
-uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
-
-[[deps.SnoopPrecompile]]
-deps = ["Preferences"]
-git-tree-sha1 = "e760a70afdcd461cf01a575947738d359234665c"
-uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c"
-version = "1.0.3"
+[[deps.StaticArrays]]
+deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"]
+git-tree-sha1 = "47091a0340a675c738b1304b58161f3b0839d454"
+uuid = "90137ffa-7385-5640-81b9-e52037218182"
+version = "1.9.10"
 
-[[deps.SparseArrays]]
-deps = ["LinearAlgebra", "Random"]
-uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+    [deps.StaticArrays.extensions]
+    StaticArraysChainRulesCoreExt = "ChainRulesCore"
+    StaticArraysStatisticsExt = "Statistics"
 
-[[deps.Static]]
-deps = ["IfElse"]
-git-tree-sha1 = "08be5ee09a7632c32695d954a602df96a877bf0d"
-uuid = "aedffcd0-7271-4cad-89d0-dc628f76c6d3"
-version = "0.8.6"
-
-[[deps.StaticArrays]]
-deps = ["LinearAlgebra", "Random", "StaticArraysCore", "Statistics"]
-git-tree-sha1 = "7756ce473bd10b67245bdebdc8d8670a85f6230b"
-uuid = "90137ffa-7385-5640-81b9-e52037218182"
-version = "1.5.18"
+    [deps.StaticArrays.weakdeps]
+    ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
+    Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
 
 [[deps.StaticArraysCore]]
-git-tree-sha1 = "6b7ba252635a5eff6a0b0664a41ee140a1c9e72a"
+git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682"
 uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c"
-version = "1.4.0"
-
-[[deps.Statistics]]
-deps = ["LinearAlgebra", "SparseArrays"]
-uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
-
-[[deps.SuiteSparse]]
-deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"]
-uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9"
+version = "1.4.3"
 
 [[deps.TOML]]
 deps = ["Dates"]
 uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
-version = "1.0.0"
-
-[[deps.TiledIteration]]
-deps = ["ArrayInterface", "OffsetArrays"]
-git-tree-sha1 = "1bf2bb587a7fc99fefac2ff076b18b500128e9c0"
-uuid = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"
-version = "0.4.2"
-
-[[deps.UUIDs]]
-deps = ["Random", "SHA"]
-uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+version = "1.0.3"
 
 [[deps.Unicode]]
 uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
+version = "1.11.0"
 
 [[deps.libblastrampoline_jll]]
-deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
+deps = ["Artifacts", "Libdl"]
 uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"
-version = "5.1.1+0"
+version = "5.11.0+0"
--- a/Notes.md	Wed May 31 08:59:34 2023 +0200
+++ b/Notes.md	Sat Jan 11 10:19:47 2025 +0100
@@ -1,5 +1,41 @@
 # Notes
 
+## How to dispatch for different operators
+We have a problem in how dispatch for different operators work.
+ * We want to keep the types simple and flat (Awkward to forward `apply`)
+ * We want to dispatch SATs on the parameters of the continuous operator. (a * div for example)
+ * We want to allow keeping the same stencil_set across different calls. (maybe not so bad for the user to be responsible)
+
+Could remove the current opset idea and introduce a description of continuous operators
+ ```julia
+abstract type DifferentialOperator end
+
+struct Laplace <: DifferentialOperator end
+struct Advection <: DifferentialOperator
+    v
+end
+
+difference_operator(::Laplace, grid, stencil_set) = ... # Returns a plain LazyTensor. Replaces the current `laplace()` function.
+sat_tensors(::Laplace, grid, stencil_set, bc) = ...
+
+sat(::DifferentialOperator, grid, stencil_set, bc) = ...
+ ```
+
+
+### Update 2024-06-26
+We will run into trouble if we start assuming things about the coupling
+between the continuous and discrete setting. We could add representations of
+continuous operators but we will also need representations of discrete
+operators. Ideally it should be possible to ignore the continuous
+representations and only work with the discrete operators without losing
+functionality. The discrete representations does not have to be LazyTensors.
+The could be used as inputs to methods for `sat`, `difference_operator` and so
+on.
+
+To see need for a fully functional discrete layer we can consider the
+optimization of material parameters or something similar. In this case we do
+not necessarily want to handle continuous objects.
+
 ## Reading operators
 
 Jonatan's suggestion is to add methods to `Laplace`, `SecondDerivative` and
@@ -71,59 +107,6 @@
 dictionary-structure containing stencils, tuples, scalars and other types
 ready for input to the methods creating the operators.
 
-## Variable second derivative
-
-2020-12-08 after discussion with Vidar:
-We will have to handle the variable second derivative in a new variant of
-VolumeOperator, "SecondDerivativeVariable?". Somehow it needs to know about
-the coefficients. They should be provided as an AbstractVector. Where they are
-provided is another question. It could be that you provide a reference to the
-array to the constructor of SecondDerivativeVariable. If that array is mutable
-you are free to change it whenever and the changes should propagate
-accordingly. Another option is that the counter part to "Laplace" for this
-variable second derivate returns a function or acts like a functions that
-takes an Abstract array and returns a SecondDerivativeVariable with the
-appropriate array. This would allow syntax like `D2(a)*v`. Can this be made
-performant?
-
-For the 1d case we can have a constructor
-`SecondDerivativeVariable(D2::SecondDerivativeVariable, a)` that just creates
-a copy with a different `a`.
-
-Apart from just the second derivative in 1D we need operators for higher
-dimensions. What happens if a=a(x,y)? Maybe this can be solved orthogonally to
-the `D2(a)*v` issue, meaning that if a constant nD version of
-SecondDerivativeVariable is available then maybe it can be wrapped to support
-function like syntax. We might have to implement `SecondDerivativeVariable`
-for N dimensions which takes a N dimensional a. If this could be easily
-closured to allow D(a) syntax we would have come a long way.
-
-For `Laplace` which might use a variable D2 if it is on a curvilinear grid we
-might want to choose how to calculate the metric coefficients. They could be
-known on closed form, they could be calculated from the grid coordinates or
-they could be provided as a vector. Which way you want to do it might change
-depending on for example if you are memory bound or compute bound. This choice
-cannot be done on the grid since the grid shouldn't care about the computer
-architecture. The most sensible option seems to be to have an argument to the
-`Laplace` function which controls how the coefficients are gotten from the
-grid. The argument could for example be a function which is to be applied to
-the grid.
-
-What happens if the grid or the varible coefficient is dependent on time?
-Maybe it becomes important to support `D(a)` or even `D(t,a)` syntax in a more
-general way.
-
-```
-g = TimeDependentGrid()
-L = Laplace(g)
-function Laplace(g::TimeDependentGrid)
-    g_logical = logical(g) # g_logical is time independent
-    ... Build a L(a) assuming we can do that ...
-    a(t) = metric_coeffs(g,t)
-    return t->L(a(t))
-end
-```
-
 ## Known size of range and domain?
 Is there any reason to use a trait to differentiate between fixed size and unknown size?
 
@@ -135,19 +118,10 @@
  Maybe if we should have dynamic sizing it could be only for the range. `domain_size` would not be implemented. And the `range_size` would be a function of a vector that the LazyTensor is applied to.
 
 ## Reasearch and thinking
- - [ ] Use a trait to indicate that a LazyTensor har the same range and domain?
  - [ ] Check how the native julia doc generator works
     - [ ] Check if Vidars design docs fit in there
  - [ ] Create a macro @lazy which replaces a binary op (+,-) by its lazy equivalent? Would be a neat way to indicate which evaluations are lazy without cluttering/confusing with special characters.
- - [ ] Dispatch on Lower() instead of the type Lower so `::Lower` instead of `::Type{Lower}` ???
- 	Seems better unless there is some specific reason to use the type instead of the value.
  - [ ] Can we have a trait to tell if a LazyTensor is transposable?
- - [ ] Is it ok to have "Constructors" for abstract types which create subtypes? For example a Grids() functions that gives different kind of grids based on input?
- - [ ] Figure out how to treat the borrowing parameters of operators. Include in into the struct? Expose via function dispatched on the operator type and grid?
-
-## Identifiers for regions
-The identifiers (`Upper`, `Lower`, `Interior`) used for region indecies should probably be included in the grid module. This allows new grid types to come with their own regions.
-We implement this by refactoring RegionIndices to be agnostic to the region types and then moving the actual types to Grids.
 
 ## Regions and tensormappings
 - [ ] Use a trait to indicate if a LazyTensor uses indices with regions.
@@ -199,15 +173,14 @@
 Preferably dimensions and sizes should be checked when lazy objects are created, for example TensorApplication, TensorComposition and so on. If dimension checks decreases performance we can make them skippable later.
 
 ## Changes to `eval_on`
-There are reasons to replace `eval_on` with regular `map` from Base, and implement a kind of lazy map perhaps `lmap` that work on indexable collections.
-
-The benefit of doing this is that we can treat grids as gridfunctions for the coordinate function, and get a more flexible tool. For example `map`/`lmap` can then be used both to evaluate a function on the grid but also get a component of a vector valued grid function or similar.
+There are reasons to replace `eval_on` with regular `map` from Base, and
+implement a kind of lazy map perhaps `lmap` that work on indexable
+collections.
 
-A question is how and if we should implement `map`/`lmap` for functions like `(x,y)->x*y` or stick to just using vector inputs. There are a few options.
-
-* use `Base.splat((x,y)->x*y)` with the single argument `map`/`lmap`.
-* implement a kind of `unzip` function to get iterators for each component, which can then be used with the multiple-iterators-version of `map`/`lmap`.
-* Inspect the function in the `map`/`lmap` function to determine which matches.
+The benefit of doing this is that we can treat grids as gridfunctions for the
+coordinate function, and get a more flexible tool. For example `map`/`lmap`
+can then be used both to evaluate a function on the grid but also get a
+component of a vector valued grid function or similar.
 
 Below is a partial implementation of `lmap` with some ideas
 ```julia
@@ -237,20 +210,46 @@
 lmap(f,  I) = LazyIndexableMap(f,I)
 ```
 
-The interaction of the map methods with the probable design of multiblock functions involving nested indecies complicate the picture slightly. It's clear at the time of writing how this would work with `Base.map`. Perhaps we want to implement our own versions of both eager and lazy map.
+The interaction of the map methods with the probable design of multiblock
+functions involving nested indecies complicate the picture slightly. It's
+unclear at the time of writing how this would work with `Base.map`. Perhaps we
+want to implement our own versions of both eager and lazy map.
+
+
+### 2024-04
+MappedArrays.jl provides a simple array type and function like the description
+of LazyMapping above. One option is to remove `eval_on` completely and rely on
+destructuring arguments if handling the function input as a vector is
+undesirable.
+
+If we can let multi-block grids be iterators over grid points we could even
+handle those by specialized implementation of `map` and `mappedarray`.
 
 ## Multiblock implementation
 We want multiblock things to work very similarly to regular one block things.
 
 ### Grid functions
-Should probably support a nested indexing so that we first have an index for subgrid and then an index for nodes on that grid. E.g `g[1,2][2,3]` or `g[3][43,21]`.
+Should probably support a nested indexing so that we first have an index for
+subgrid and then an index for nodes on that grid. E.g `g[1,2][2,3]` or
+`g[3][43,21]`.
 
-We could also possibly provide a combined indexing style `g[1,2,3,4]` where the first group of indices are for the subgrid and the remaining are for the nodes.
+We could also possibly provide a combined indexing style `g[1,2,3,4]` where
+the first group of indices are for the subgrid and the remaining are for the
+nodes.
 
-We should make sure the underlying buffer for gridfunctions are continuously stored and are easy to convert to, so that interaction with for example DifferentialEquations is simple and without much boilerplate.
+We should make sure the underlying buffer for grid functions are continuously
+stored and are easy to convert to, so that interaction with for example
+DifferentialEquations is simple and without much boilerplate.
 
 #### `map` and `collect` and nested indexing
-We need to make sure `collect`, `map` and a potential lazy map work correctly through the nested indexing.
+We need to make sure `collect`, `map` and a potential lazy map work correctly
+through the nested indexing. Also see notes on `eval_on` above.
+
+Possibly this can be achieved by providing special nested indexing but not
+adhering to an array interface at the top level, instead being implemented as
+an iterator over the grid points. A custom trait can let map and other methods
+know the shape (or structure) of the nesting so that they can efficiently
+allocate result arrays.
 
 ### Tensor applications
 Should behave as grid functions
@@ -285,16 +284,6 @@
 Grid-funktioner har typen `AbstractArray{T,2} where T`.
 `T` kan vara lite vad som helst, tillexemel en SVector eller Array, eller Tuple. Tensoroperatorerna bryr sig inte om exakt vad det är, mer än att typen måste stödja de operationer som operatorn använder.
 
-En nackdel kan vara hur man ska få ut gridfunktionen för tex andra komponenten.
-
-Syntax:
-```
-f(x̄) = x̄
-gf = evalOn(g, f)
-gf[2,3] # x̄ för en viss gridpunkt
-gf[2,3][2] # x̄[2] för en viss gridpunkt
-```
-
 ### Tensor operatorer
 Vi kan ha tensor-operatorer som agerar på ett skalärt fält och ger ett vektorfält eller tensorfält.
 Vi kan också ha tensor-operatorer som agerar på ett vektorfält eller tensorfält och ger ett skalärt fält.
@@ -310,16 +299,6 @@
 
 Kanske kan man implementera `⋅(tm::LazyTensor{R,D}, v::AbstractArray{T,D})` där T är en AbstractArray, tm på något sätt har komponenter, lika många som T har element.
 
-### Komponenter som gridfunktioner
-En viktig operation för vektorfält är att kunna få ut komponenter som grid-funktioner. Detta behöver antagligen kunna ske lazy.
-Det finns ett par olika lösningar:
-* Använda map eller en lazy map (se diskussion om eval_on)
-* Implementera en egen typ av view som tar hand om detta. Eller Accessors.jl?
-* Använda en LazyTensor
-* Någon typ av lazy-broadcast
-* En lazy array som applicerar en funktion för varje element.
-
-
 ### Prestanda-aspekter
 [Vidar, Discord, 2023-03-03]
 Typiskt sett finns det två sätt att representera vektorvärda gridfunktioner AbstractArray{T,Dim} där T är en vektor över komponenterna. Man skulle alltså i 1D ha
@@ -366,3 +345,58 @@
 Could the implementation of LazyOuterProduct be simplified by making it a
 struct containing two or more LazyTensors? (using split_tuple in a similar way
 as TensorGrid)
+
+## Implementation of boundary_indices for more complex grids
+To represent boundaries of for example tet-elements we can use a type `IndexCollection` to index a grid function directly.
+
+```julia
+I = IndexCollection(...)
+v[I]
+```
+
+* This would impact how tensor grid works.
+* To make things homogenous maybe these index collections should be used for the more simple grids too.
+* The function `to_indices` from Base could be useful to implement for `IndexCollection`
+
+
+## Stencil application pipeline
+We should make sure that `@inbounds` and `Base.@propagate_inbounds` are
+applied correctly throughout the stack. When testing the performance of
+stencil application on the bugfix/sbp_operators/stencil_return_type branch
+there seemed to be some strange results where such errors could be the
+culprit.
+
+
+## Tiled loops and regions in apply
+There should be easy ways to use functionalty splitting the application of a lazy array into regions and using tiled iteration. This could make the application more efficient by reducing branching and improving cache usage in the tight loop. On commit f215ac2a5c66 and before there were some early tests regarding this in a DiffOp submodule.
+
+The main ideas were:
+```julia
+function apply_region!(D::DiffOpCartesian{2}, u::AbstractArray{T,2}, v::AbstractArray{T,2}) where T
+    apply_region!(D, u, v, Lower, Lower)
+    apply_region!(D, u, v, Lower, Interior)
+    apply_region!(D, u, v, Lower, Upper)
+    apply_region!(D, u, v, Interior, Lower)
+    apply_region!(D, u, v, Interior, Interior)
+    apply_region!(D, u, v, Interior, Upper)
+    apply_region!(D, u, v, Upper, Lower)
+    apply_region!(D, u, v, Upper, Interior)
+    apply_region!(D, u, v, Upper, Upper)
+    return nothing
+end
+```
+
+```julia
+using TiledIteration
+function apply_region_tiled!(D::DiffOpCartesian{2}, u::AbstractArray{T,2}, v::AbstractArray{T,2}, r1::Type{<:Region}, r2::Type{<:Region}) where T
+    ri = regionindices(D.grid.size, closuresize(D.op), (r1,r2))
+    # TODO: Pass Tilesize to function
+    for tileaxs ∈ TileIterator(axes(ri), padded_tilesize(T, (5,5), 2))
+        for j ∈ tileaxs[2], i ∈ tileaxs[1]
+            I = ri[i,j]
+            u[I] = apply(D, v, (Index{r1}(I[1]), Index{r2}(I[2])))
+        end
+    end
+    return nothing
+end
+```
--- a/Project.toml	Wed May 31 08:59:34 2023 +0200
+++ b/Project.toml	Sat Jan 11 10:19:47 2025 +0100
@@ -1,12 +1,30 @@
-name = "Sbplib"
+name = "Diffinitive"
 uuid = "5a373a26-915f-4769-bcab-bf03835de17b"
-authors = ["Jonatan Werpers <jonatan@werpers.com>", "Vidar Stiernström <vidar.stiernstrom@it.uu.se>, and contributors"]
-version = "0.1.0"
+authors = ["Jonatan Werpers <jonatan@werpers.com>", "Vidar Stiernström <vidar.stiernstrom@gmail.com>, and contributors"]
+version = "0.1.3"
 
 [deps]
+LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
 StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
 TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
-TiledIteration = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"
+
+[weakdeps]
+Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
+SparseArrayKit = "a9a3c162-d163-4c15-8926-b8794fbefed2"
+SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+Tokens = "040c2ec2-8d69-4aca-bf03-7d3a7092f2f6"
+
+[extensions]
+DiffinitiveMakieExt = "Makie"
+DiffinitiveSparseArrayKitExt = ["SparseArrayKit", "Tokens"]
+DiffinitiveSparseArraysExt = ["SparseArrays", "Tokens"]
 
 [compat]
 julia = "1.5"
+StaticArrays = "1.0"
+TOML = "1.0"
+Makie = "0.21"
+SparseArrayKit = "0.3"
+Tokens = "0.1.1"
+SparseArrays = "1.10"
+LinearAlgebra = "1.5"
--- a/README.md	Wed May 31 08:59:34 2023 +0200
+++ b/README.md	Sat Jan 11 10:19:47 2025 +0100
@@ -1,10 +1,10 @@
-# Sbplib
+# Diffinitive
 
 ## Running tests
 To run all tests simply run
 ```
 (@v1.5) pkg> activate .
-(Sbplib) pkg> test
+(Diffinitive) pkg> test
 ```
 
 If you want to run tests from a specific file in `test/`, you can do
@@ -69,8 +69,8 @@
 
 ```julia
 using PkgBenchmark
-import Sbplib
-r = benchmarkpkg(Sbplib)
+import Diffinitive
+r = benchmarkpkg(Diffinitive)
 
 export_markdown(stdout, r)
 ```
--- a/TODO.md	Wed May 31 08:59:34 2023 +0200
+++ b/TODO.md	Sat Jan 11 10:19:47 2025 +0100
@@ -4,11 +4,7 @@
  - [ ] Split up Notes.md in several files
 
 ## Coding
- - [ ] Ändra namn på variabler och funktioner så att det följer style-guide
- - [ ] Add new Laplace operator to DiffOps, probably named WaveEqOp(?!!?)
  - [ ] Create a struct that bundles the necessary Tensor operators for solving the wave equation.
- - [ ] Use `@inferred` in a lot of tests.
- - [ ] Replace `@inferred` tests with a benchmark suite that automatically tests for regressions.
  - [ ] Make sure we are setting tolerances in tests in a consistent way
  - [ ] Write down some coding guideline or checklist for code conventions. For example i,j,... for indices and I for multi-index
  - [ ] Clean up RegionIndices
@@ -16,11 +12,14 @@
     2. [ ] Update RegionIndices accordingly
     3. [ ] Fix the rest of the library
     Should getregion also work for getregion(::Colon,...)
- - [ ] Add possibility to create tensor mapping application with `()`, e.g `D1(v) <=> D1*v`?
  - [ ] Add custom pretty printing to LazyTensors/SbpOperators to enhance readability of e.g error messages.
        See (https://docs.julialang.org/en/v1/manual/types/#man-custom-pretty-printing)
  - [ ] Samla noggrannhets- och SBP-ness-tester för alla operatorer på ett ställe
  - [ ] Move export statements to top of each module
+ - [ ] Implement apply_transpose for
+      - [ ] ElementwiseTensorOperation
+      - [ ] VolumeOperator
+      - [ ] Laplace
 
 
  - [ ] Gå igenom alla typ parametrar och kolla om de är motiverade. Både i signaturer och typer, tex D i VariableSecondDerivative. Kan vi använda promote istället?
@@ -29,12 +28,14 @@
  - [ ] Kolla att vi har @inline på rätt ställen
  - [ ] Profilera
 
+ - [ ] Keep a lookout for allowing dependencies of package extensions (https://github.com/JuliaLang/Pkg.jl/issues/3641) This should be used to simplify the matrix extensions so that you don't have to load Tokens which is only used internally to the extension
 
 ### Grids
 
  - [ ] Multiblock grids
  - [ ] Periodic grids
  - [ ] Grids with modified boundary closures
+ - [ ] Support indexing with `:`.
 
 
 ### Benchmarks
--- a/TimeStepper.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-abstract type TimeStepper end
-
-# Returns v and t
-function getState(ts::TimeStepper)
-	error("not implemented")
-end
-
-
-function step!(ts::TimeStepper)
-	error("not implemented")
-end
-
-function stepN(ts::TimeStepper,N::Int)
-	for i ∈ 1:N
-		ts.step()
-	end
-end
-
-function stepTo(ts::TimeStepper)
-	error("Not yet implemented")
-end
-
-function evolve(ts::TimeStepper)
-	error("Not yet implemented")
-end
-
-
-mutable struct Rk4 <: TimeStepper
-	F::Function
-	k::Real
-	v::Vector
-	t::Real
-	n::UInt
-
-	function Rk4(F::Function,k::Real,v0::Vector,t0::Real)
-		# TODO: Check that F has two inputs and one output
-		v = v0
-		t = t0
-		n = 0
-		return new(F,k,v,t,n)
-	end
-end
-
-function getState(ts::Rk4)
-	return ts.t, ts.v
-end
-
-function step!(ts::Rk4)
-    k1 = ts.F(ts.v,ts.t)
-	k2 = ts.F(ts.v+0.5*ts.k*k1,ts.t+0.5*ts.k)
-	k3 = ts.F(ts.v+0.5*ts.k*k2,ts.t+0.5*ts.k)
-    k4 = ts.F(ts.v+    ts.k*k3,ts.t+    ts.k)
-    ts.v  = ts.v + (1/6)*(k1+2*(k2+k3)+k4)*ts.k
-
-	ts.n = ts.n + 1
-	ts.t = ts.t + ts.k
-
-	return nothing
-end
-
--- a/benchmark/Manifest.toml	Wed May 31 08:59:34 2023 +0200
+++ b/benchmark/Manifest.toml	Sat Jan 11 10:19:47 2025 +0100
@@ -1,63 +1,51 @@
 # This file is machine-generated - editing it directly is not advised
 
-julia_version = "1.8.5"
+julia_version = "1.11.2"
 manifest_format = "2.0"
-project_hash = "25bba7b4a00465d5a2b00b589eb10e3301c31f2a"
+project_hash = "ecfc3e12aca5be17a874aba6134ff821abf61540"
 
 [[deps.AbstractTrees]]
-git-tree-sha1 = "faa260e4cb5aba097a73fab382dd4b5819d8ec8c"
+git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177"
 uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
-version = "0.4.4"
-
-[[deps.Adapt]]
-deps = ["LinearAlgebra"]
-git-tree-sha1 = "0310e08cb19f5da31d08341c6120c047598f5b9c"
-uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
-version = "3.5.0"
+version = "0.4.5"
 
 [[deps.ArgTools]]
 uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
-version = "1.1.1"
-
-[[deps.ArrayInterface]]
-deps = ["ArrayInterfaceCore", "Compat", "IfElse", "LinearAlgebra", "Static"]
-git-tree-sha1 = "6d0918cb9c0d3db7fe56bea2bc8638fc4014ac35"
-uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
-version = "6.0.24"
-
-[[deps.ArrayInterfaceCore]]
-deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
-git-tree-sha1 = "14c3f84a763848906ac681f94cf469a851601d92"
-uuid = "30b0a656-2188-435a-8636-2ec0e6a096e2"
-version = "0.1.28"
+version = "1.1.2"
 
 [[deps.Artifacts]]
 uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
+version = "1.11.0"
 
 [[deps.Base64]]
 uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
+version = "1.11.0"
 
 [[deps.BenchmarkTools]]
-deps = ["JSON", "Logging", "Printf", "Profile", "Statistics", "UUIDs"]
-git-tree-sha1 = "d9a9701b899b30332bbcb3e1679c41cce81fb0e8"
+deps = ["Compat", "JSON", "Logging", "Printf", "Profile", "Statistics", "UUIDs"]
+git-tree-sha1 = "e38fbc49a620f5d0b660d7f543db1009fe0f8336"
 uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
-version = "1.3.2"
+version = "1.6.0"
 
 [[deps.Compat]]
-deps = ["Dates", "LinearAlgebra", "UUIDs"]
-git-tree-sha1 = "00a2cccc7f098ff3b66806862d275ca3db9e6e5a"
+deps = ["TOML", "UUIDs"]
+git-tree-sha1 = "8ae8d32e09f0dcf42a36b90d4e17f5dd2e4c4215"
 uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
-version = "4.5.0"
+version = "4.16.0"
+weakdeps = ["Dates", "LinearAlgebra"]
+
+    [deps.Compat.extensions]
+    CompatLinearAlgebraExt = "LinearAlgebra"
 
 [[deps.CompilerSupportLibraries_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
-version = "1.0.1+0"
+version = "1.1.1+0"
 
 [[deps.DataAPI]]
-git-tree-sha1 = "e8119c1a33d267e16108be441a287a6981ba1630"
+git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe"
 uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
-version = "1.14.0"
+version = "1.16.0"
 
 [[deps.DataValueInterfaces]]
 git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
@@ -67,6 +55,24 @@
 [[deps.Dates]]
 deps = ["Printf"]
 uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
+version = "1.11.0"
+
+[[deps.Diffinitive]]
+deps = ["LinearAlgebra", "StaticArrays", "TOML"]
+path = ".."
+uuid = "5a373a26-915f-4769-bcab-bf03835de17b"
+version = "0.1.3"
+
+    [deps.Diffinitive.extensions]
+    DiffinitiveMakieExt = "Makie"
+    DiffinitiveSparseArrayKitExt = ["SparseArrayKit", "Tokens"]
+    DiffinitiveSparseArraysExt = ["SparseArrays", "Tokens"]
+
+    [deps.Diffinitive.weakdeps]
+    Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
+    SparseArrayKit = "a9a3c162-d163-4c15-8926-b8794fbefed2"
+    SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+    Tokens = "040c2ec2-8d69-4aca-bf03-7d3a7092f2f6"
 
 [[deps.Downloads]]
 deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"]
@@ -75,15 +81,12 @@
 
 [[deps.FileWatching]]
 uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
-
-[[deps.IfElse]]
-git-tree-sha1 = "debdd00ffef04665ccbb3e150747a77560e8fad1"
-uuid = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173"
-version = "0.1.1"
+version = "1.11.0"
 
 [[deps.InteractiveUtils]]
 deps = ["Markdown"]
 uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
+version = "1.11.0"
 
 [[deps.IteratorInterfaceExtensions]]
 git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856"
@@ -92,9 +95,9 @@
 
 [[deps.JSON]]
 deps = ["Dates", "Mmap", "Parsers", "Unicode"]
-git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e"
+git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a"
 uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
-version = "0.21.3"
+version = "0.21.4"
 
 [[deps.LeftChildRightSiblingTrees]]
 deps = ["AbstractTrees"]
@@ -105,84 +108,95 @@
 [[deps.LibCURL]]
 deps = ["LibCURL_jll", "MozillaCACerts_jll"]
 uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
-version = "0.6.3"
+version = "0.6.4"
 
 [[deps.LibCURL_jll]]
 deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
 uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
-version = "7.84.0+0"
+version = "8.6.0+0"
 
 [[deps.LibGit2]]
-deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
+deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"]
 uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
+version = "1.11.0"
+
+[[deps.LibGit2_jll]]
+deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"]
+uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5"
+version = "1.7.2+0"
 
 [[deps.LibSSH2_jll]]
 deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
 uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
-version = "1.10.2+0"
+version = "1.11.0+1"
 
 [[deps.Libdl]]
 uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
+version = "1.11.0"
 
 [[deps.LinearAlgebra]]
-deps = ["Libdl", "libblastrampoline_jll"]
+deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
 uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
+version = "1.11.0"
 
 [[deps.Logging]]
 uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
+version = "1.11.0"
 
 [[deps.Markdown]]
 deps = ["Base64"]
 uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
+version = "1.11.0"
 
 [[deps.MbedTLS_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
-version = "2.28.0+0"
+version = "2.28.6+0"
 
 [[deps.Mmap]]
 uuid = "a63ad114-7e13-5084-954f-fe012c677804"
+version = "1.11.0"
 
 [[deps.MozillaCACerts_jll]]
 uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
-version = "2022.2.1"
+version = "2023.12.12"
 
 [[deps.Mustache]]
 deps = ["Printf", "Tables"]
-git-tree-sha1 = "1e566ae913a57d0062ff1af54d2697b9344b99cd"
+git-tree-sha1 = "3b2db451a872b20519ebb0cec759d3d81a1c6bcb"
 uuid = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
-version = "1.0.14"
+version = "1.0.20"
 
 [[deps.NetworkOptions]]
 uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
 version = "1.2.0"
 
-[[deps.OffsetArrays]]
-deps = ["Adapt"]
-git-tree-sha1 = "f71d8950b724e9ff6110fc948dff5a329f901d64"
-uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
-version = "1.12.8"
-
 [[deps.OpenBLAS_jll]]
 deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
 uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"
-version = "0.3.20+0"
+version = "0.3.27+1"
 
 [[deps.OrderedCollections]]
-git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c"
+git-tree-sha1 = "12f1439c4f986bb868acda6ea33ebc78e19b95ad"
 uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
-version = "1.4.1"
+version = "1.7.0"
 
 [[deps.Parsers]]
-deps = ["Dates", "SnoopPrecompile"]
-git-tree-sha1 = "8175fc2b118a3755113c8e68084dc1a9e63c61ee"
+deps = ["Dates", "PrecompileTools", "UUIDs"]
+git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821"
 uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
-version = "2.5.3"
+version = "2.8.1"
 
 [[deps.Pkg]]
-deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
+deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"]
 uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
-version = "1.8.0"
+version = "1.11.0"
+
+    [deps.Pkg.extensions]
+    REPLExt = "REPL"
+
+    [deps.Pkg.weakdeps]
+    REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
 
 [[deps.PkgBenchmark]]
 deps = ["BenchmarkTools", "Dates", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Pkg", "Printf", "TerminalLoggers", "UUIDs"]
@@ -190,19 +204,26 @@
 uuid = "32113eaa-f34f-5b0d-bd6c-c81e245fc73d"
 version = "0.2.12"
 
+[[deps.PrecompileTools]]
+deps = ["Preferences"]
+git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f"
+uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
+version = "1.2.1"
+
 [[deps.Preferences]]
 deps = ["TOML"]
-git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d"
+git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6"
 uuid = "21216c6a-2e73-6563-6e65-726566657250"
-version = "1.3.0"
+version = "1.4.3"
 
 [[deps.Printf]]
 deps = ["Unicode"]
 uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
+version = "1.11.0"
 
 [[deps.Profile]]
-deps = ["Printf"]
 uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79"
+version = "1.11.0"
 
 [[deps.ProgressLogging]]
 deps = ["Logging", "SHA", "UUIDs"]
@@ -210,69 +231,50 @@
 uuid = "33c8b6b6-d38a-422a-b730-caa89a2f386c"
 version = "0.1.4"
 
-[[deps.REPL]]
-deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
-uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
-
 [[deps.Random]]
-deps = ["SHA", "Serialization"]
+deps = ["SHA"]
 uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+version = "1.11.0"
 
 [[deps.SHA]]
 uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
 version = "0.7.0"
 
-[[deps.Sbplib]]
-deps = ["StaticArrays", "TOML", "TiledIteration"]
-path = ".."
-uuid = "5a373a26-915f-4769-bcab-bf03835de17b"
-version = "0.1.0"
-
-[[deps.Serialization]]
-uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
-
-[[deps.SnoopPrecompile]]
-deps = ["Preferences"]
-git-tree-sha1 = "e760a70afdcd461cf01a575947738d359234665c"
-uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c"
-version = "1.0.3"
+[[deps.StaticArrays]]
+deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"]
+git-tree-sha1 = "47091a0340a675c738b1304b58161f3b0839d454"
+uuid = "90137ffa-7385-5640-81b9-e52037218182"
+version = "1.9.10"
 
-[[deps.Sockets]]
-uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
-
-[[deps.SparseArrays]]
-deps = ["LinearAlgebra", "Random"]
-uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+    [deps.StaticArrays.extensions]
+    StaticArraysChainRulesCoreExt = "ChainRulesCore"
+    StaticArraysStatisticsExt = "Statistics"
 
-[[deps.Static]]
-deps = ["IfElse"]
-git-tree-sha1 = "c35b107b61e7f34fa3f124026f2a9be97dea9e1c"
-uuid = "aedffcd0-7271-4cad-89d0-dc628f76c6d3"
-version = "0.8.3"
-
-[[deps.StaticArrays]]
-deps = ["LinearAlgebra", "Random", "StaticArraysCore", "Statistics"]
-git-tree-sha1 = "c262c8e978048c2b095be1672c9bee55b4619521"
-uuid = "90137ffa-7385-5640-81b9-e52037218182"
-version = "1.5.24"
+    [deps.StaticArrays.weakdeps]
+    ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
+    Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
 
 [[deps.StaticArraysCore]]
-git-tree-sha1 = "6b7ba252635a5eff6a0b0664a41ee140a1c9e72a"
+git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682"
 uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c"
-version = "1.4.0"
+version = "1.4.3"
 
 [[deps.Statistics]]
-deps = ["LinearAlgebra", "SparseArrays"]
+deps = ["LinearAlgebra"]
+git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0"
 uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
+version = "1.11.1"
 
-[[deps.SuiteSparse]]
-deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"]
-uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9"
+    [deps.Statistics.extensions]
+    SparseArraysExt = ["SparseArrays"]
+
+    [deps.Statistics.weakdeps]
+    SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
 
 [[deps.TOML]]
 deps = ["Dates"]
 uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
-version = "1.0.0"
+version = "1.0.3"
 
 [[deps.TableTraits]]
 deps = ["IteratorInterfaceExtensions"]
@@ -281,55 +283,47 @@
 version = "1.0.1"
 
 [[deps.Tables]]
-deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "LinearAlgebra", "OrderedCollections", "TableTraits", "Test"]
-git-tree-sha1 = "c79322d36826aa2f4fd8ecfa96ddb47b174ac78d"
+deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"]
+git-tree-sha1 = "598cd7c1f68d1e205689b1c2fe65a9f85846f297"
 uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
-version = "1.10.0"
+version = "1.12.0"
 
 [[deps.Tar]]
 deps = ["ArgTools", "SHA"]
 uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
-version = "1.10.1"
+version = "1.10.0"
 
 [[deps.TerminalLoggers]]
 deps = ["LeftChildRightSiblingTrees", "Logging", "Markdown", "Printf", "ProgressLogging", "UUIDs"]
-git-tree-sha1 = "f53e34e784ae771eb9ccde4d72e578aa453d0554"
+git-tree-sha1 = "f133fab380933d042f6796eda4e130272ba520ca"
 uuid = "5d786b92-1e48-4d6f-9151-6b4477ca9bed"
-version = "0.1.6"
-
-[[deps.Test]]
-deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
-uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
-
-[[deps.TiledIteration]]
-deps = ["ArrayInterface", "OffsetArrays"]
-git-tree-sha1 = "1bf2bb587a7fc99fefac2ff076b18b500128e9c0"
-uuid = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"
-version = "0.4.2"
+version = "0.1.7"
 
 [[deps.UUIDs]]
 deps = ["Random", "SHA"]
 uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+version = "1.11.0"
 
 [[deps.Unicode]]
 uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
+version = "1.11.0"
 
 [[deps.Zlib_jll]]
 deps = ["Libdl"]
 uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
-version = "1.2.12+3"
+version = "1.2.13+1"
 
 [[deps.libblastrampoline_jll]]
-deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
+deps = ["Artifacts", "Libdl"]
 uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"
-version = "5.1.1+0"
+version = "5.11.0+0"
 
 [[deps.nghttp2_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
-version = "1.48.0+0"
+version = "1.59.0+0"
 
 [[deps.p7zip_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
-version = "17.4.0+0"
+version = "17.4.0+2"
--- a/benchmark/Project.toml	Wed May 31 08:59:34 2023 +0200
+++ b/benchmark/Project.toml	Sat Jan 11 10:19:47 2025 +0100
@@ -1,5 +1,5 @@
 [deps]
 BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
+Diffinitive = "5a373a26-915f-4769-bcab-bf03835de17b"
 Mustache = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
-PkgBenchmark = "32113eaa-f34f-5b0d-bd6c-c81e245fc73d"
-Sbplib = "5a373a26-915f-4769-bcab-bf03835de17b"
+PkgBenchmark = "32113eaa-f34f-5b0d-bd6c-c81e245fc73d"
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/benchmark/benchmark_laplace.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,220 @@
+using Diffinitive
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.RegionIndices
+using BenchmarkTools
+
+# TODO: Move the below benchmarks into the benchmark suite
+
+const operator_path = sbp_operators_path()*"standard_diagonal.toml"
+
+function benchmark_const_coeff_1d(;N = 100, order = 4)
+    stencil_set = read_stencil_set(operator_path; order=order)
+    g = equidistant_grid(0., 1., N)
+    D = second_derivative(g, stencil_set)
+    u = rand(size(g)...)
+    u_xx = rand(size(g)...)
+        
+    b_naive = @benchmark $u_xx .= $D*$u
+    b_reg = @benchmark $apply_region_1d!($u_xx,$u,$D)
+    b_thrd = @benchmark $apply_region_threaded_1d!($u_xx,$u,$D)
+    print_benchmark_result("Naive apply",b_naive)
+    print_benchmark_result("Region apply",b_reg)
+    print_benchmark_result("Threaded region apply",b_thrd)
+end
+
+function benchmark_var_coeff_1d(;N = 100, order = 4)
+    stencil_set = read_stencil_set(operator_path; order=order)
+    g = equidistant_grid(0., 1., N)
+    c = rand(size(g)...)
+    c_lz = eval_on(g, x -> 0.5)
+    D = second_derivative_variable(g, c, stencil_set)
+    D_lz = second_derivative_variable(g, c_lz, stencil_set)
+    u = rand(size(g)...)
+    u_xx = rand(size(g)...)
+    
+    b_naive = @benchmark $u_xx .= $D*$u
+    b_naive_lz = @benchmark $u_xx .= $D_lz*$u
+    b_reg = @benchmark $apply_region_1d!($u_xx,$u,$D)
+    b_reg_lz = @benchmark $apply_region_1d!($u_xx,$u,$D_lz)
+    b_thrd = @benchmark $apply_region_threaded_1d!($u_xx,$u,$D)
+    b_thrd_lz = @benchmark $apply_region_threaded_1d!($u_xx,$u,$D_lz)
+    print_benchmark_result("Naive apply",b_naive)
+    print_benchmark_result("Naive apply lazy coeff",b_naive_lz)
+    print_benchmark_result("Region apply",b_reg)
+    print_benchmark_result("Region apply lazy coeff",b_reg_lz)
+    print_benchmark_result("Threaded region apply",b_thrd)
+    print_benchmark_result("Threaded region apply lazy coeff",b_thrd_lz)
+end
+
+function benchmark_const_coeff_2d(;N = 100, order = 4)
+    stencil_set = read_stencil_set(operator_path; order=order)
+    g = equidistant_grid((0.,0.,),(1.,1.), N, N)
+    D = Laplace(g, stencil_set)
+    u = rand(size(g)...)
+    u_xx = rand(size(g)...)
+    if order == 2
+        clz_sz = 1
+    elseif order == 4
+        clz_sz = 4
+    else
+        error()
+    end
+
+    b_naive = @benchmark $u_xx .= $D*$u
+    b_reg = @benchmark $apply_region_2d!($u_xx,$u,$D,$clz_sz)
+    b_thrd = @benchmark $apply_region_threaded_2d!($u_xx,$u,$D,$clz_sz)
+    print_benchmark_result("Naive apply",b_naive)
+    print_benchmark_result("Region apply",b_reg)
+    print_benchmark_result("Threaded region apply",b_thrd)
+end
+
+function benchmark_var_coeff_2d(;N = 100, order = 4)
+    stencil_set = read_stencil_set(operator_path; order=order)
+    g = equidistant_grid((0.,0.,),(1.,1.), N, N)
+    c = rand(size(g)...)
+    c_lz = eval_on(g, x-> 0.5)
+    D = second_derivative_variable(g, c, stencil_set, 1) + second_derivative_variable(g, c, stencil_set, 2)
+    D_lz = second_derivative_variable(g, c_lz, stencil_set, 1) + second_derivative_variable(g, c_lz, stencil_set, 2)
+    u = rand(size(g)...)
+    u_xx = rand(size(g)...)
+
+    if order == 2
+        clz_sz = 1
+    elseif order == 4
+        clz_sz = 6
+    else
+        error()
+    end
+    
+    # Check correctnesss
+    # u_xx .= D*u
+    # u_xx_tmp = zeros(size(u_xx)...)
+    # u_xx_tmp .= u_xx
+    # apply_region_threaded_2d!(u_xx, u, D, clz_sz)
+
+    # @show sum(abs.(u_xx_tmp .- u_xx))
+    # @show pointer(u_xx_tmp) == pointer(u_xxs
+
+    
+    b_naive = @benchmark $u_xx .= $D*$u
+    b_naive_lz = @benchmark $u_xx .= $D_lz*$u
+    b_reg = @benchmark $apply_region_2d!($u_xx,$u,$D, $clz_sz)
+    b_reg_lz = @benchmark $apply_region_2d!($u_xx,$u,$D_lz, $clz_sz)
+    b_thrd = @benchmark $apply_region_threaded_2d!($u_xx,$u,$D, $clz_sz)
+    b_thrd_lz = @benchmark $apply_region_threaded_2d!($u_xx,$u,$D_lz, $clz_sz)
+    print_benchmark_result("Naive apply",b_naive)
+    print_benchmark_result("Naive apply lazy coeff",b_naive_lz)
+    print_benchmark_result("Region apply",b_reg)
+    print_benchmark_result("Region apply lazy coeff",b_reg_lz)
+    print_benchmark_result("Threaded region apply",b_thrd)
+    print_benchmark_result("Threaded region apply lazy coeff",b_thrd_lz)
+end
+
+function print_benchmark_result(title_str,res)
+    if title_str[1] != ' '
+        title_str = lpad(title_str,length(title_str)+1, " ")
+    end
+    if title_str[end] != ' '
+        title_str = rpad(title_str,length(title_str)+1, " ")
+    end
+    tot_len = 76
+    pad_len = Int(tot_len/2)
+    header = lpad(title_str,pad_len,"*")
+    header = rpad(header,tot_len,"*")
+    bottom = repeat("*",tot_len)
+    println(header)
+    display(res)
+    println(bottom)
+    return
+end
+
+function apply_region_1d!(u_xx, u, D)
+    clz_sz = SbpOperators.closure_size(D)
+    tm = D*u
+    for i ∈ @view eachindex(u)[1:clz_sz]
+        u_xx[i] = tm[Index{Lower}(i)]
+    end
+    for i ∈ @view eachindex(u)[clz_sz+1:end-clz_sz]
+        u_xx[i] = tm[Index{Interior}(i)]
+    end
+    for i ∈ @view eachindex(u)[end-clz_sz+1:end]
+        u_xx[i] = tm[Index{Upper}(i)]
+    end
+end
+
+function apply_region_threaded_1d!(u_xx, u, D)
+    clz_sz = SbpOperators.closure_size(D)
+    tm = D*u
+    for i ∈ @view eachindex(u)[1:clz_sz]
+        u_xx[i] = tm[Index{Lower}(i)]
+    end
+    Threads.@threads for i ∈ @view eachindex(u)[clz_sz+1:end-clz_sz]
+        u_xx[i] = tm[Index{Interior}(i)]
+    end
+    for i ∈ @view eachindex(u)[end-clz_sz+1:end]
+        u_xx[i] = tm[Index{Upper}(i)]
+    end
+end
+
+function apply_region_2d!(u_xx, u, D, clz_sz)
+    tm = D*u
+    for I ∈ @view CartesianIndices(u)[1:clz_sz,1:clz_sz]
+        u_xx[I] = tm[Index{Lower}(I[1]),Index{Lower}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[1:clz_sz,clz_sz+1:end-clz_sz]
+        u_xx[I] = tm[Index{Lower}(I[1]),Index{Interior}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[1:clz_sz,end-clz_sz+1:end]
+        u_xx[I] = tm[Index{Lower}(I[1]),Index{Upper}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[clz_sz+1:end-clz_sz,1:clz_sz]
+        u_xx[I] = tm[Index{Interior}(I[1]),Index{Lower}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[clz_sz+1:end-clz_sz,clz_sz+1:end-clz_sz]
+        u_xx[I] = tm[Index{Interior}(I[1]),Index{Interior}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[clz_sz+1:end-clz_sz,end-clz_sz+1:end]
+        u_xx[I] = tm[Index{Interior}(I[1]),Index{Upper}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[end-clz_sz+1:end,1:clz_sz]
+        u_xx[I] = tm[Index{Upper}(I[1]),Index{Lower}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[end-clz_sz+1:end,clz_sz+1:end-clz_sz]
+        u_xx[I] = tm[Index{Upper}(I[1]),Index{Interior}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[end-clz_sz+1:end,end-clz_sz+1:end]
+        u_xx[I] = tm[Index{Upper}(I[1]),Index{Upper}(I[2])]
+    end
+end
+
+function apply_region_threaded_2d!(u_xx, u, D, clz_sz)
+    tm = D*u
+    for I ∈ @view CartesianIndices(u)[1:clz_sz,1:clz_sz]
+        u_xx[I] = tm[Index{Lower}(I[1]),Index{Lower}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[1:clz_sz,clz_sz+1:end-clz_sz]
+        u_xx[I] = tm[Index{Lower}(I[1]),Index{Interior}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[1:clz_sz,end-clz_sz+1:end]
+        u_xx[I] = tm[Index{Lower}(I[1]),Index{Upper}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[clz_sz+1:end-clz_sz,1:clz_sz]
+        u_xx[I] = tm[Index{Interior}(I[1]),Index{Lower}(I[2])]
+    end
+    Threads.@threads for I ∈ @view CartesianIndices(u)[clz_sz+1:end-clz_sz,clz_sz+1:end-clz_sz]
+        u_xx[I] = tm[Index{Interior}(I[1]),Index{Interior}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[clz_sz+1:end-clz_sz,end-clz_sz+1:end]
+        u_xx[I] = tm[Index{Interior}(I[1]),Index{Upper}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[end-clz_sz+1:end,1:clz_sz]
+        u_xx[I] = tm[Index{Upper}(I[1]),Index{Lower}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[end-clz_sz+1:end,clz_sz+1:end-clz_sz]
+        u_xx[I] = tm[Index{Upper}(I[1]),Index{Interior}(I[2])]
+    end
+    for I ∈ @view CartesianIndices(u)[end-clz_sz+1:end,end-clz_sz+1:end]
+        u_xx[I] = tm[Index{Upper}(I[1]),Index{Upper}(I[2])]
+    end
+end
--- a/benchmark/benchmark_utils.jl	Wed May 31 08:59:34 2023 +0200
+++ b/benchmark/benchmark_utils.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -3,11 +3,11 @@
 import Mustache
 import Dates
 
-import Sbplib
+import Diffinitive
 
-const sbplib_root = splitpath(pathof(Sbplib))[1:end-2] |> joinpath
-const results_dir = mkpath(joinpath(sbplib_root, "benchmark/results"))
-const template_path = joinpath(sbplib_root, "benchmark/result.tmpl")
+const diffinitive_root = splitpath(pathof(Diffinitive))[1:end-2] |> joinpath
+const results_dir = mkpath(joinpath(diffinitive_root, "benchmark/results"))
+const template_path = joinpath(diffinitive_root, "benchmark/result.tmpl")
 
 """
     mainmain(;rev=nothing, target=nothing, baseline=nothing , kwargs...)
@@ -23,7 +23,7 @@
 For control over what happens to the benchmark result datastructure see the
 different methods of [`run_benchmark`](@ref)
 """
-function main(;rev=nothing, target=nothing, baseline=nothing , kwargs...)
+function main(;rev=nothing, target=nothing, baseline=nothing, name=nothing, kwargs...)
     if !isnothing(rev)
         r = run_benchmark(rev; kwargs...)
     elseif !isnothing(baseline)
@@ -37,7 +37,7 @@
         r = run_benchmark(;kwargs...)
     end
 
-    file_path = write_result_html(r)
+    file_path = write_result_html(r; name)
     open_in_default_browser(file_path)
 end
 
@@ -49,7 +49,7 @@
 `PkgBenchmark.BenchmarkResult`
 """
 function run_benchmark(;kwargs...)
-    r = PkgBenchmark.benchmarkpkg(Sbplib; kwargs...)
+    r = PkgBenchmark.benchmarkpkg(Diffinitive; kwargs...)
 
     rev = hg_rev() # Should be changed to hg_id() when the html can handle it.
 
@@ -137,9 +137,14 @@
     Mustache.render(io, template, Dict("title"=>dt, "content"=>content))
 end
 
-function write_result_html(r)
+function write_result_html(r; name=nothing)
     dt = Dates.format(PkgBenchmark.date(r), "yyyy-mm-dd HHMMSS")
-    file_path = joinpath(results_dir, dt*".html")
+
+    if isnothing(name)
+        file_path = joinpath(results_dir, dt*".html")
+    else
+        file_path = joinpath(results_dir, dt*" "*name*".html")
+    end
 
     open(file_path, "w") do io
         write_result_html(io, r)
@@ -153,17 +158,17 @@
 
 
 function hg_id()
-    cmd = Cmd(`hg id`, dir=sbplib_root)
+    cmd = Cmd(`hg id`, dir=diffinitive_root)
     return readchomp(addenv(cmd, "HGPLAIN"=>""))
 end
 
 function hg_rev()
-    cmd = Cmd(`hg id -i`, dir=sbplib_root)
+    cmd = Cmd(`hg id -i`, dir=diffinitive_root)
     return readchomp(addenv(cmd, "HGPLAIN"=>""))
 end
 
 function hg_update(rev)
-    cmd = Cmd(`hg update --check -r $rev`, dir=sbplib_root)
+    cmd = Cmd(`hg update --check -r $rev`, dir=diffinitive_root)
     run(addenv(cmd, "HGPLAIN"=>""))
 
     return nothing
@@ -177,9 +182,9 @@
 """
 function hg_commit(msg; secret=false)
     if secret
-        cmd = Cmd(`hg commit --verbose --secret --message $msg`, dir=sbplib_root)
+        cmd = Cmd(`hg commit --verbose --secret --message $msg`, dir=diffinitive_root)
     else
-        cmd = Cmd(`hg commit --verbose          --message $msg`, dir=sbplib_root)
+        cmd = Cmd(`hg commit --verbose          --message $msg`, dir=diffinitive_root)
     end
 
     out = readchomp(addenv(cmd, "HGPLAIN"=>""))
@@ -195,9 +200,9 @@
 """
 function hg_strip(rev; keep=false)
     if keep
-        cmd = Cmd(`hg --config extensions.strip= strip --keep -r $rev`, dir=sbplib_root)
+        cmd = Cmd(`hg --config extensions.strip= strip --keep -r $rev`, dir=diffinitive_root)
     else
-        cmd = Cmd(`hg --config extensions.strip= strip        -r $rev`, dir=sbplib_root)
+        cmd = Cmd(`hg --config extensions.strip= strip        -r $rev`, dir=diffinitive_root)
     end
 
     run(addenv(cmd, "HGPLAIN"=>""))
@@ -211,7 +216,7 @@
 Return true if the repositopry has uncommited changes.
 """
 function hg_is_dirty()
-    cmd = Cmd(`hg identify --id`, dir=sbplib_root)
+    cmd = Cmd(`hg identify --id`, dir=diffinitive_root)
     out = readchomp(addenv(cmd, "HGPLAIN"=>""))
 
     return endswith(out, "+")
--- a/benchmark/benchmarks.jl	Wed May 31 08:59:34 2023 +0200
+++ b/benchmark/benchmarks.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,9 +1,11 @@
 using BenchmarkTools
-using Sbplib
-using Sbplib.Grids
-using Sbplib.SbpOperators
-using Sbplib.RegionIndices
-using Sbplib.LazyTensors
+
+using Diffinitive
+using Diffinitive.Grids
+using Diffinitive.SbpOperators
+using Diffinitive.LazyTensors
+
+using LinearAlgebra
 
 const SUITE = BenchmarkGroup()
 
@@ -12,9 +14,9 @@
 ll(d) = ntuple(i->0., d)
 lu(d) = ntuple(i->1., d)
 
-g1 = equidistant_grid(sz(1)[1],ll(1)[1],lu(1)[1])
-g2 = equidistant_grid(sz(2),ll(2),lu(2))
-g3 = equidistant_grid(sz(3),ll(3),lu(3))
+g1 = equidistant_grid(ll(1)[1], lu(1)[1], sz(1)...)
+g2 = equidistant_grid(ll(2), lu(2), sz(2)...)
+g3 = equidistant_grid(ll(3), lu(3), sz(3)...)
 
 v1 = rand(sz(1)...)
 v2 = rand(sz(2)...)
@@ -69,6 +71,30 @@
 SUITE["derivatives"]["second_derivative"]["3D"]["z"] = @benchmarkable $u3 .= $Dz*$v3
 
 
+SUITE["derivatives"]["second_derivative_variable"] = BenchmarkGroup()
+
+c1 = map(x->sin(x)+2, g1)
+D₂ = second_derivative_variable(g1, c1, stencil_set)
+SUITE["derivatives"]["second_derivative_variable"]["1D"] = @benchmarkable $u1 .= $D₂*$v1
+
+c2 = map(x->sin(x[1] + x[2])+2, g2)
+Dx = second_derivative_variable(g2, c2, stencil_set, 1)
+Dy = second_derivative_variable(g2, c2, stencil_set, 2)
+SUITE["derivatives"]["second_derivative_variable"]["2D"] = BenchmarkGroup()
+SUITE["derivatives"]["second_derivative_variable"]["2D"]["x"] = @benchmarkable $u2 .= $Dx*$v2
+SUITE["derivatives"]["second_derivative_variable"]["2D"]["y"] = @benchmarkable $u2 .= $Dy*$v2
+
+c3 = map(x->sin(norm(x))+2, g3)
+Dx = second_derivative_variable(g3, c3, stencil_set, 1)
+Dy = second_derivative_variable(g3, c3, stencil_set, 2)
+Dz = second_derivative_variable(g3, c3, stencil_set, 3)
+SUITE["derivatives"]["second_derivative_variable"]["3D"] = BenchmarkGroup()
+SUITE["derivatives"]["second_derivative_variable"]["3D"]["x"] = @benchmarkable $u3 .= $Dx*$v3
+SUITE["derivatives"]["second_derivative_variable"]["3D"]["y"] = @benchmarkable $u3 .= $Dy*$v3
+SUITE["derivatives"]["second_derivative_variable"]["3D"]["z"] = @benchmarkable $u3 .= $Dz*$v3
+
+
+
 
 SUITE["derivatives"]["addition"] = BenchmarkGroup()
 
@@ -141,20 +167,20 @@
 Dxx = second_derivative(g2, stencil_set, 1)
 Dyy = second_derivative(g2, stencil_set, 2)
 
-e₁ₗ = boundary_restriction(g2, stencil_set, CartesianBoundary{1,Lower}())
-e₁ᵤ = boundary_restriction(g2, stencil_set, CartesianBoundary{1,Upper}())
-e₂ₗ = boundary_restriction(g2, stencil_set, CartesianBoundary{2,Lower}())
-e₂ᵤ = boundary_restriction(g2, stencil_set, CartesianBoundary{2,Upper}())
+e₁ₗ = boundary_restriction(g2, stencil_set, CartesianBoundary{1,LowerBoundary}())
+e₁ᵤ = boundary_restriction(g2, stencil_set, CartesianBoundary{1,UpperBoundary}())
+e₂ₗ = boundary_restriction(g2, stencil_set, CartesianBoundary{2,LowerBoundary}())
+e₂ᵤ = boundary_restriction(g2, stencil_set, CartesianBoundary{2,UpperBoundary}())
 
-d₁ₗ = normal_derivative(g2, stencil_set, CartesianBoundary{1,Lower}())
-d₁ᵤ = normal_derivative(g2, stencil_set, CartesianBoundary{1,Upper}())
-d₂ₗ = normal_derivative(g2, stencil_set, CartesianBoundary{2,Lower}())
-d₂ᵤ = normal_derivative(g2, stencil_set, CartesianBoundary{2,Upper}())
+d₁ₗ = normal_derivative(g2, stencil_set, CartesianBoundary{1,LowerBoundary}())
+d₁ᵤ = normal_derivative(g2, stencil_set, CartesianBoundary{1,UpperBoundary}())
+d₂ₗ = normal_derivative(g2, stencil_set, CartesianBoundary{2,LowerBoundary}())
+d₂ᵤ = normal_derivative(g2, stencil_set, CartesianBoundary{2,UpperBoundary}())
 
-H₁ₗ = inner_product(boundary_grid(g2, CartesianBoundary{1,Lower}()), stencil_set)
-H₁ᵤ = inner_product(boundary_grid(g2, CartesianBoundary{1,Upper}()), stencil_set)
-H₂ₗ = inner_product(boundary_grid(g2, CartesianBoundary{2,Lower}()), stencil_set)
-H₂ᵤ = inner_product(boundary_grid(g2, CartesianBoundary{2,Upper}()), stencil_set)
+H₁ₗ = inner_product(boundary_grid(g2, CartesianBoundary{1,LowerBoundary}()), stencil_set)
+H₁ᵤ = inner_product(boundary_grid(g2, CartesianBoundary{1,UpperBoundary}()), stencil_set)
+H₂ₗ = inner_product(boundary_grid(g2, CartesianBoundary{2,LowerBoundary}()), stencil_set)
+H₂ᵤ = inner_product(boundary_grid(g2, CartesianBoundary{2,UpperBoundary}()), stencil_set)
 
 SUITE["boundary_terms"]["pre_composition"] = @benchmarkable $u2 .= $(H⁻¹∘e₁ₗ'∘H₁ₗ∘d₁ₗ)*$v2
 SUITE["boundary_terms"]["composition"]     = @benchmarkable $u2 .= ($H⁻¹∘$e₁ₗ'∘$H₁ₗ∘$d₁ₗ)*$v2
--- a/docs/Manifest.toml	Wed May 31 08:59:34 2023 +0200
+++ b/docs/Manifest.toml	Sat Jan 11 10:19:47 2025 +0100
@@ -1,52 +1,63 @@
 # This file is machine-generated - editing it directly is not advised
 
-julia_version = "1.8.5"
+julia_version = "1.11.2"
 manifest_format = "2.0"
-project_hash = "4f0756199bb5f6739a5f4697152617efc4e0705c"
+project_hash = "c04450bb5c379e77d137cc05c4c0ab58eb1bfae9"
 
 [[deps.ANSIColoredPrinters]]
 git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c"
 uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9"
 version = "0.0.1"
 
-[[deps.Adapt]]
-deps = ["LinearAlgebra", "Requires"]
-git-tree-sha1 = "cc37d689f599e8df4f464b2fa3870ff7db7492ef"
-uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
-version = "3.6.1"
+[[deps.AbstractTrees]]
+git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177"
+uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
+version = "0.4.5"
 
-[[deps.ArrayInterface]]
-deps = ["ArrayInterfaceCore", "Compat", "IfElse", "LinearAlgebra", "SnoopPrecompile", "Static"]
-git-tree-sha1 = "dedc16cbdd1d32bead4617d27572f582216ccf23"
-uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
-version = "6.0.25"
-
-[[deps.ArrayInterfaceCore]]
-deps = ["LinearAlgebra", "SnoopPrecompile", "SparseArrays", "SuiteSparse"]
-git-tree-sha1 = "e5f08b5689b1aad068e01751889f2f615c7db36d"
-uuid = "30b0a656-2188-435a-8636-2ec0e6a096e2"
-version = "0.1.29"
+[[deps.ArgTools]]
+uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
+version = "1.1.2"
 
 [[deps.Artifacts]]
 uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
+version = "1.11.0"
 
 [[deps.Base64]]
 uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
+version = "1.11.0"
 
-[[deps.Compat]]
-deps = ["Dates", "LinearAlgebra", "UUIDs"]
-git-tree-sha1 = "7a60c856b9fa189eb34f5f8a6f6b5529b7942957"
-uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
-version = "4.6.1"
+[[deps.CodecZlib]]
+deps = ["TranscodingStreams", "Zlib_jll"]
+git-tree-sha1 = "bce6804e5e6044c6daab27bb533d1295e4a2e759"
+uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
+version = "0.7.6"
 
 [[deps.CompilerSupportLibraries_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
-version = "1.0.1+0"
+version = "1.1.1+0"
 
 [[deps.Dates]]
 deps = ["Printf"]
 uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
+version = "1.11.0"
+
+[[deps.Diffinitive]]
+deps = ["LinearAlgebra", "StaticArrays", "TOML"]
+path = ".."
+uuid = "5a373a26-915f-4769-bcab-bf03835de17b"
+version = "0.1.3"
+
+    [deps.Diffinitive.extensions]
+    DiffinitiveMakieExt = "Makie"
+    DiffinitiveSparseArrayKitExt = ["SparseArrayKit", "Tokens"]
+    DiffinitiveSparseArraysExt = ["SparseArrays", "Tokens"]
+
+    [deps.Diffinitive.weakdeps]
+    Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
+    SparseArrayKit = "a9a3c162-d163-4c15-8926-b8794fbefed2"
+    SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+    Tokens = "040c2ec2-8d69-4aca-bf03-7d3a7092f2f6"
 
 [[deps.DocStringExtensions]]
 deps = ["LibGit2"]
@@ -55,172 +66,282 @@
 version = "0.9.3"
 
 [[deps.Documenter]]
-deps = ["ANSIColoredPrinters", "Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"]
-git-tree-sha1 = "58fea7c536acd71f3eef6be3b21c0df5f3df88fd"
+deps = ["ANSIColoredPrinters", "AbstractTrees", "Base64", "CodecZlib", "Dates", "DocStringExtensions", "Downloads", "Git", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "MarkdownAST", "Pkg", "PrecompileTools", "REPL", "RegistryInstances", "SHA", "TOML", "Test", "Unicode"]
+git-tree-sha1 = "d0ea2c044963ed6f37703cead7e29f70cba13d7e"
 uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
-version = "0.27.24"
+version = "1.8.0"
+
+[[deps.Downloads]]
+deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"]
+uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
+version = "1.6.0"
+
+[[deps.Expat_jll]]
+deps = ["Artifacts", "JLLWrappers", "Libdl"]
+git-tree-sha1 = "e51db81749b0777b2147fbe7b783ee79045b8e99"
+uuid = "2e619515-83b5-522b-bb60-26c02a35a201"
+version = "2.6.4+3"
+
+[[deps.FileWatching]]
+uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
+version = "1.11.0"
+
+[[deps.Git]]
+deps = ["Git_jll"]
+git-tree-sha1 = "04eff47b1354d702c3a85e8ab23d539bb7d5957e"
+uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2"
+version = "1.3.1"
+
+[[deps.Git_jll]]
+deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"]
+git-tree-sha1 = "399f4a308c804b446ae4c91eeafadb2fe2c54ff9"
+uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb"
+version = "2.47.1+0"
 
 [[deps.IOCapture]]
 deps = ["Logging", "Random"]
-git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a"
+git-tree-sha1 = "b6d6bfdd7ce25b0f9b2f6b3dd56b2673a66c8770"
 uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89"
-version = "0.2.2"
-
-[[deps.IfElse]]
-git-tree-sha1 = "debdd00ffef04665ccbb3e150747a77560e8fad1"
-uuid = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173"
-version = "0.1.1"
+version = "0.2.5"
 
 [[deps.InteractiveUtils]]
 deps = ["Markdown"]
 uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
+version = "1.11.0"
+
+[[deps.JLLWrappers]]
+deps = ["Artifacts", "Preferences"]
+git-tree-sha1 = "a007feb38b422fbdab534406aeca1b86823cb4d6"
+uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
+version = "1.7.0"
 
 [[deps.JSON]]
 deps = ["Dates", "Mmap", "Parsers", "Unicode"]
-git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e"
+git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a"
 uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
-version = "0.21.3"
+version = "0.21.4"
+
+[[deps.LazilyInitializedFields]]
+git-tree-sha1 = "0f2da712350b020bc3957f269c9caad516383ee0"
+uuid = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf"
+version = "1.3.0"
+
+[[deps.LibCURL]]
+deps = ["LibCURL_jll", "MozillaCACerts_jll"]
+uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
+version = "0.6.4"
+
+[[deps.LibCURL_jll]]
+deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
+uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
+version = "8.6.0+0"
 
 [[deps.LibGit2]]
-deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
+deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"]
 uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
+version = "1.11.0"
+
+[[deps.LibGit2_jll]]
+deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"]
+uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5"
+version = "1.7.2+0"
+
+[[deps.LibSSH2_jll]]
+deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
+uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
+version = "1.11.0+1"
 
 [[deps.Libdl]]
 uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
+version = "1.11.0"
+
+[[deps.Libiconv_jll]]
+deps = ["Artifacts", "JLLWrappers", "Libdl"]
+git-tree-sha1 = "61dfdba58e585066d8bce214c5a51eaa0539f269"
+uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531"
+version = "1.17.0+1"
 
 [[deps.LinearAlgebra]]
-deps = ["Libdl", "libblastrampoline_jll"]
+deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
 uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
+version = "1.11.0"
 
 [[deps.Logging]]
 uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
+version = "1.11.0"
 
 [[deps.Markdown]]
 deps = ["Base64"]
 uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
+version = "1.11.0"
+
+[[deps.MarkdownAST]]
+deps = ["AbstractTrees", "Markdown"]
+git-tree-sha1 = "465a70f0fc7d443a00dcdc3267a497397b8a3899"
+uuid = "d0879d2d-cac2-40c8-9cee-1863dc0c7391"
+version = "0.1.2"
+
+[[deps.MbedTLS_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
+version = "2.28.6+0"
 
 [[deps.Mmap]]
 uuid = "a63ad114-7e13-5084-954f-fe012c677804"
+version = "1.11.0"
+
+[[deps.MozillaCACerts_jll]]
+uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
+version = "2023.12.12"
 
 [[deps.NetworkOptions]]
 uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
 version = "1.2.0"
 
-[[deps.OffsetArrays]]
-deps = ["Adapt"]
-git-tree-sha1 = "82d7c9e310fe55aa54996e6f7f94674e2a38fcb4"
-uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
-version = "1.12.9"
-
 [[deps.OpenBLAS_jll]]
 deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
 uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"
-version = "0.3.20+0"
+version = "0.3.27+1"
+
+[[deps.OpenSSL_jll]]
+deps = ["Artifacts", "JLLWrappers", "Libdl"]
+git-tree-sha1 = "7493f61f55a6cce7325f197443aa80d32554ba10"
+uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95"
+version = "3.0.15+3"
+
+[[deps.PCRE2_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15"
+version = "10.42.0+1"
 
 [[deps.Parsers]]
-deps = ["Dates", "SnoopPrecompile"]
-git-tree-sha1 = "478ac6c952fddd4399e71d4779797c538d0ff2bf"
+deps = ["Dates", "PrecompileTools", "UUIDs"]
+git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821"
 uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
-version = "2.5.8"
+version = "2.8.1"
+
+[[deps.Pkg]]
+deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"]
+uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
+version = "1.11.0"
+weakdeps = ["REPL"]
+
+    [deps.Pkg.extensions]
+    REPLExt = "REPL"
+
+[[deps.PrecompileTools]]
+deps = ["Preferences"]
+git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f"
+uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
+version = "1.2.1"
 
 [[deps.Preferences]]
 deps = ["TOML"]
-git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d"
+git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6"
 uuid = "21216c6a-2e73-6563-6e65-726566657250"
-version = "1.3.0"
+version = "1.4.3"
 
 [[deps.Printf]]
 deps = ["Unicode"]
 uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
+version = "1.11.0"
 
 [[deps.REPL]]
-deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
+deps = ["InteractiveUtils", "Markdown", "Sockets", "StyledStrings", "Unicode"]
 uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
+version = "1.11.0"
 
 [[deps.Random]]
-deps = ["SHA", "Serialization"]
+deps = ["SHA"]
 uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+version = "1.11.0"
 
-[[deps.Requires]]
-deps = ["UUIDs"]
-git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7"
-uuid = "ae029012-a4dd-5104-9daa-d747884805df"
-version = "1.3.0"
+[[deps.RegistryInstances]]
+deps = ["LazilyInitializedFields", "Pkg", "TOML", "Tar"]
+git-tree-sha1 = "ffd19052caf598b8653b99404058fce14828be51"
+uuid = "2792f1a3-b283-48e8-9a74-f99dce5104f3"
+version = "0.1.0"
 
 [[deps.SHA]]
 uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
 version = "0.7.0"
 
-[[deps.Sbplib]]
-deps = ["StaticArrays", "TOML", "TiledIteration"]
-path = ".."
-uuid = "5a373a26-915f-4769-bcab-bf03835de17b"
-version = "0.1.0"
-
 [[deps.Serialization]]
 uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
-
-[[deps.SnoopPrecompile]]
-deps = ["Preferences"]
-git-tree-sha1 = "e760a70afdcd461cf01a575947738d359234665c"
-uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c"
-version = "1.0.3"
+version = "1.11.0"
 
 [[deps.Sockets]]
 uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
-
-[[deps.SparseArrays]]
-deps = ["LinearAlgebra", "Random"]
-uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
-
-[[deps.Static]]
-deps = ["IfElse"]
-git-tree-sha1 = "08be5ee09a7632c32695d954a602df96a877bf0d"
-uuid = "aedffcd0-7271-4cad-89d0-dc628f76c6d3"
-version = "0.8.6"
+version = "1.11.0"
 
 [[deps.StaticArrays]]
-deps = ["LinearAlgebra", "Random", "StaticArraysCore", "Statistics"]
-git-tree-sha1 = "7756ce473bd10b67245bdebdc8d8670a85f6230b"
+deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"]
+git-tree-sha1 = "47091a0340a675c738b1304b58161f3b0839d454"
 uuid = "90137ffa-7385-5640-81b9-e52037218182"
-version = "1.5.18"
+version = "1.9.10"
+
+    [deps.StaticArrays.extensions]
+    StaticArraysChainRulesCoreExt = "ChainRulesCore"
+    StaticArraysStatisticsExt = "Statistics"
+
+    [deps.StaticArrays.weakdeps]
+    ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
+    Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
 
 [[deps.StaticArraysCore]]
-git-tree-sha1 = "6b7ba252635a5eff6a0b0664a41ee140a1c9e72a"
+git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682"
 uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c"
-version = "1.4.0"
+version = "1.4.3"
 
-[[deps.Statistics]]
-deps = ["LinearAlgebra", "SparseArrays"]
-uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
-
-[[deps.SuiteSparse]]
-deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"]
-uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9"
+[[deps.StyledStrings]]
+uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b"
+version = "1.11.0"
 
 [[deps.TOML]]
 deps = ["Dates"]
 uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
-version = "1.0.0"
+version = "1.0.3"
+
+[[deps.Tar]]
+deps = ["ArgTools", "SHA"]
+uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
+version = "1.10.0"
 
 [[deps.Test]]
 deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
 uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+version = "1.11.0"
 
-[[deps.TiledIteration]]
-deps = ["ArrayInterface", "OffsetArrays"]
-git-tree-sha1 = "1bf2bb587a7fc99fefac2ff076b18b500128e9c0"
-uuid = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"
-version = "0.4.2"
+[[deps.TranscodingStreams]]
+git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742"
+uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
+version = "0.11.3"
 
 [[deps.UUIDs]]
 deps = ["Random", "SHA"]
 uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+version = "1.11.0"
 
 [[deps.Unicode]]
 uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
+version = "1.11.0"
+
+[[deps.Zlib_jll]]
+deps = ["Libdl"]
+uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
+version = "1.2.13+1"
 
 [[deps.libblastrampoline_jll]]
-deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
+deps = ["Artifacts", "Libdl"]
 uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"
-version = "5.1.1+0"
+version = "5.11.0+0"
+
+[[deps.nghttp2_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
+version = "1.59.0+0"
+
+[[deps.p7zip_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
+version = "17.4.0+2"
--- a/docs/Project.toml	Wed May 31 08:59:34 2023 +0200
+++ b/docs/Project.toml	Sat Jan 11 10:19:47 2025 +0100
@@ -1,3 +1,3 @@
 [deps]
+Diffinitive = "5a373a26-915f-4769-bcab-bf03835de17b"
 Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
-Sbplib = "5a373a26-915f-4769-bcab-bf03835de17b"
--- a/docs/make.jl	Wed May 31 08:59:34 2023 +0200
+++ b/docs/make.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,14 +1,16 @@
 using Documenter
-using Sbplib
+using Diffinitive
 
-using Sbplib.DiffOps
-using Sbplib.Grids
-using Sbplib.LazyTensors
-using Sbplib.RegionIndices
-using Sbplib.SbpOperators
-using Sbplib.StaticDicts
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
+using Diffinitive.RegionIndices
+using Diffinitive.SbpOperators
 
-sitename = "Sbplib.jl"
+sitename = "Diffinitive.jl"
+
+remotes = nothing
+edit_link = nothing
+repolink = nothing
 
 if "--prettyurls" ∈ ARGS
     prettyurls = true
@@ -27,17 +29,17 @@
     "Home" => "index.md",
     "operator_file_format.md",
     "grids_and_grid_functions.md",
+    "matrix_and_tensor_representations.md",
     "Submodules" => [
         "submodules/grids.md",
-        "submodules/diff_ops.md",
         "submodules/lazy_tensors.md",
         "submodules/region_indices.md",
         "submodules/sbp_operators.md",
-        "submodules/static_dicts.md",
     ],
     "doc_index.md",
 ]
 # This ordering is not respected by @contents. See https://github.com/JuliaDocs/Documenter.jl/issues/936
 
-format=Documenter.HTML(;prettyurls)
-makedocs(;sitename, pages, format, build)
+format=Documenter.HTML(;prettyurls, edit_link, repolink)
+
+makedocs(;sitename, pages, format, build, remotes)
--- a/docs/src/grids_and_grid_functions.md	Wed May 31 08:59:34 2023 +0200
+++ b/docs/src/grids_and_grid_functions.md	Sat Jan 11 10:19:47 2025 +0100
@@ -7,6 +7,24 @@
 ## Interface for grids
 All grids are expected to work as a grid function for the coordinate function, and thus implements Julia's Indexing- and Iteration-interfaces. Notably they are *not* abstract arrays because that inteface is too restrictive for the types of grids we wish to implement.
 
+
+## Plotting
+Plotting of grids and grid functions is supported through a package extension with Makie.jl.
+
+For grids we have:
+* `plot(::Grid{<:Any,2})` (same as `lines`)
+* `lines(::Grid{<:Any,2})`
+* `scatter(::Grid{<:Any,2})`
+
+For 1D grid functions we have:
+* `plot(::Grid{<:Any,1}, ::AbstractVector)` (same as `lines`)
+* `lines(::Grid{<:Any,1}, ::AbstractVector)`
+* `scatter(::Grid{<:Any,1}, ::AbstractVector)`
+
+For 2D grid functions we have:
+* `plot(::Grid{<:Any,2}, ::AbstractArray{<:Any,2})` (constructs a 2d mesh)
+* `surface(::Grid{<:Any,2}, ::AbstractArray{<:Any,2})`
+
 ## To write about
 <!-- # TODO: -->
 * Grid functions
--- a/docs/src/index.md	Wed May 31 08:59:34 2023 +0200
+++ b/docs/src/index.md	Sat Jan 11 10:19:47 2025 +0100
@@ -1,4 +1,4 @@
-# Sbplib.jl
+# Diffinitive.jl
 
 ```@contents
 Depth = 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/src/matrix_and_tensor_representations.md	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,5 @@
+# Matrix and tensor representations
+
+Sparse matrix and sparse tensor representations of lazy tensors can be constructed by loading [Tokens.jl](http://) and one of SparseArrays.jl or [SparseArrayKit.jl](http://). Through package extensions the following methods `sparse(::LazyTensor)` and `SparseArray(::LazyTensor)` are provided.
+
+<!-- TODO figure out how to add the docstrings here --/>
--- a/docs/src/operator_file_format.md	Wed May 31 08:59:34 2023 +0200
+++ b/docs/src/operator_file_format.md	Sat Jan 11 10:19:47 2025 +0100
@@ -1,6 +1,6 @@
 # Operator file format
 
-The intention is that Sbplib.jl should be a general and extensible framework
+The intention is that Diffinitive.jl should be a general and extensible framework
 for working with finite difference methods. It therefore includes a set of
 tools for storing and sharing operator definitions as well as a set of widely
 used operators.
--- a/docs/src/submodules/diff_ops.md	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-# DiffOps
-
-## Contents
-```@contents
-Pages = ["diff_ops.md"]
-```
-
-## Index
-```@index
-Pages = ["diff_ops.md"]
-```
-
-## Public interface
-```@autodocs
-Modules = [DiffOps]
-Private = false # Hide unexported objects
-```
-
-## Internal interface
-```@autodocs
-Modules = [DiffOps]
-Public = false # Hide exported objects
-```
--- a/docs/src/submodules/static_dicts.md	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-# StaticDicts
-
-## Contents
-```@contents
-Pages = ["static_dicts.md"]
-```
-
-## Index
-```@index
-Pages = ["static_dicts.md"]
-```
-
-## Public interface
-```@autodocs
-Modules = [StaticDicts]
-Private = false # Hide unexported objects
-```
-
-## Internal interface
-```@autodocs
-Modules = [StaticDicts]
-Public = false # Hide exported objects
-```
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/DiffinitiveMakieExt.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,88 @@
+module DiffinitiveMakieExt
+
+using Diffinitive.Grids
+using Makie
+using StaticArrays
+
+
+function verticies_and_faces_and_values(g::Grid{<:Any,2}, gf::AbstractArray{<:Any, 2})
+    ps = map(Tuple, g)[:]
+    values = gf[:]
+    faces = Vector{NTuple{3,Int}}()
+
+    n,m = size(g)
+    Li = LinearIndices((1:n, 1:m))
+    for i ∈ 1:n-1, j = 1:m-1
+
+        # Add point in the middle of the patch to preserve symmetries
+        push!(ps, Tuple((g[i,j] + g[i+1,j] + g[i+1,j+1] + g[i,j+1])/4))
+        push!(values, (gf[i,j] + gf[i+1,j] + gf[i+1,j+1] + gf[i,j+1])/4)
+
+        push!(faces, (Li[i,j],     Li[i+1,j],   length(ps)))
+        push!(faces, (Li[i+1,j],   Li[i+1,j+1], length(ps)))
+        push!(faces, (Li[i+1,j+1], Li[i,j+1],   length(ps)))
+        push!(faces, (Li[i,j+1],   Li[i,j],     length(ps)))
+    end
+
+    verticies = permutedims(reinterpret(reshape,eltype(eltype(ps)), ps))
+    faces = permutedims(reinterpret(reshape,Int, faces))
+
+    return verticies, faces, values
+end
+
+
+## Grids
+
+Makie.convert_arguments(::Type{<:Scatter}, g::Grid) = (reshape(map(Point,g),:),)
+function Makie.convert_arguments(::Type{<:Lines}, g::Grid{<:AbstractVector})
+    M = collect(g)
+
+    function cat_with_NaN(a,b)
+        vcat(a,[@SVector fill(NaN, coordinate_size(g))],b)
+    end
+
+    xlines = reduce(cat_with_NaN, eachrow(M))
+    ylines = reduce(cat_with_NaN, eachcol(M))
+
+    return (cat_with_NaN(xlines,ylines),)
+end
+
+Makie.plot!(plot::Plot(Grid)) = lines!(plot, plot.attributes, plot[1])
+
+
+## Grid functions
+
+### 1D
+function Makie.convert_arguments(::Type{<:Lines}, g::Grid{<:Any,1}, gf::AbstractArray{<:Any, 1})
+    (collect(g), gf)
+end
+
+function Makie.convert_arguments(::Type{<:Scatter}, g::Grid{<:Any,1}, gf::AbstractArray{<:Any, 1})
+    (collect(g), gf)
+end
+
+Makie.plot!(plot::Plot(Grid{<:Any,1}, AbstractArray{<:Any,1})) = lines!(plot, plot.attributes, plot[1], plot[2])
+
+### 2D
+function Makie.convert_arguments(::Type{<:Surface}, g::Grid{<:Any,2}, gf::AbstractArray{<:Any, 2})
+    (getindex.(g,1), getindex.(g,2), gf)
+end
+
+function Makie.plot!(plot::Plot(Grid{<:Any,2},AbstractArray{<:Any, 2}))
+    r = @lift verticies_and_faces_and_values($(plot[1]), $(plot[2]))
+    v,f,c = (@lift $r[1]), (@lift $r[2]), (@lift $r[3])
+    mesh!(plot, plot.attributes, v, f;
+        color=c,
+        shading = NoShading,
+    )
+end
+# TBD: Can we define `mesh` instead of the above function and then forward plot! to that?
+
+function Makie.convert_arguments(::Type{<:Scatter}, g::Grid{<:Any,2}, gf::AbstractArray{<:Any, 2})
+    ps = map(g,gf) do (x,y), z
+        @SVector[x,y,z]
+    end
+    (reshape(ps,:),)
+end
+
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/DiffinitiveSparseArrayKitExt.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,22 @@
+module DiffinitiveSparseArrayKitExt
+
+using Diffinitive
+using Diffinitive.LazyTensors
+
+using SparseArrayKit
+using Tokens
+
+"""
+    SparseArray(t::LazyTensor)
+
+The sparse tensor representation of `t` with range dimensions to the left and
+domain dimensions to the right. If `L` is a `LazyTensor` with range and
+domain dimension 2 and `v` a 2-tensor, then `A = SparseArray(t)` is
+constructed so that `∑ₖ∑ₗA[i,j,k,l]*v[k,l] == L*v`.
+"""
+function SparseArrayKit.SparseArray(t::LazyTensor)
+    v = ArrayToken(:v, domain_size(t)...)
+    return Tokens._to_tensor(t*v, range_size(t), domain_size(t))
+end
+
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/DiffinitiveSparseArraysExt.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,25 @@
+module DiffinitiveSparseArraysExt
+
+using Diffinitive
+using Diffinitive.LazyTensors
+
+using SparseArrays
+using Tokens
+
+"""
+    sparse(t::LazyTensor)
+
+The sparse matrix representation of `t`.
+
+If `L` is a `LazyTensor` and `v` a tensor, then `A = sparse(L)` is constructed
+so that `A*reshape(v,:) == reshape(L*v,:)`.
+"""
+function SparseArrays.sparse(t::LazyTensor)
+    v = ArrayToken(:v, prod(domain_size(t)))
+
+    v̄ = reshape(v,domain_size(t)...)
+    tv = reshape(t*v̄, :)
+    return Tokens._to_matrix(tv, prod(range_size(t)), prod(domain_size(t)))
+end
+
+end
--- a/plotDerivative.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-g = sbp.Grid.EquidistantGrid((200,), (0.0,), (2pi,))
-op =sbp.readOperator("d2_4th.txt","h_4th.txt")
-Laplace = sbp.Laplace(g,1.0,op)
-
-init(x) = cos(x)
-v = sbp.Grid.evalOn(g,init)
-u = zeros(length(v))
-
-sbp.apply!(Laplace,u,v)
-
-@show u
-sbp.Grid.plotgridfunction(g,u)
--- a/plotDerivative2d.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-include("sbpPlot.jl")
-
-g = sbp.Grid.EquidistantGrid((100,75), (0.0, 0.0), (2pi, 3/2*pi))
-op = sbp.readOperator("d2_4th.txt","h_4th.txt")
-Laplace = sbp.Laplace(g, 1.0, op)
-
-init(x,y) = sin(x) + cos(y)
-v = sbp.Grid.evalOn(g,init)
-u = zero(v)
-
-sbp.apply!(Laplace,u,v)
-
-#@show u
-#@show u'*u
-
-plotgridfunction(g,u)
--- a/sbp.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-module sbp
-
-using Sbplib.Grids
-using Sbplib.RegionIndices
-using Sbplib.SbpOperators
-using Sbplib.DiffOps
-
-include("TimeStepper.jl")
-end  # module
--- a/sbpPlot.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-include("sbp.jl")
-using Makie
-import .sbp.Grid
-function plotgridfunction(grid::sbp.Grid.EquidistantGrid, gridfunction::AbstractArray)
-    if sbp.Grid.dimension(grid) == 1
-        plot(sbp.Grid.pointsalongdim(grid,1), gridfunction)
-    elseif sbp.Grid.dimension(grid) == 2
-        scene = surface(sbp.Grid.pointsalongdim(grid,1),sbp.Grid.pointsalongdim(grid,2), gridfunction)
-    else
-        error(string("Plot not implemented for dimension ", string(dimension(grid))))
-    end
-end
--- a/src/DiffOps/DiffOps.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,103 +0,0 @@
-module DiffOps
-
-using Sbplib.RegionIndices
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
-
-"""
-    DiffOp
-
-Supertype of differential operator discretisations.
-The action of the DiffOp is defined in the method
-    apply(D::DiffOp, v::AbstractVector, I...)
-"""
-abstract type DiffOp end
-
-function apply end
-
-function matrixRepresentation(D::DiffOp)
-    error("not implemented")
-end
-
-abstract type DiffOpCartesian{Dim} <: DiffOp end
-
-# DiffOp must have a grid of dimension Dim!!!
-function apply!(D::DiffOpCartesian{Dim}, u::AbstractArray{T,Dim}, v::AbstractArray{T,Dim}) where {T,Dim}
-    for I ∈ eachindex(D.grid)
-        u[I] = apply(D, v, I)
-    end
-
-    return nothing
-end
-export apply!
-
-function apply_region!(D::DiffOpCartesian{2}, u::AbstractArray{T,2}, v::AbstractArray{T,2}) where T
-    apply_region!(D, u, v, Lower, Lower)
-    apply_region!(D, u, v, Lower, Interior)
-    apply_region!(D, u, v, Lower, Upper)
-    apply_region!(D, u, v, Interior, Lower)
-    apply_region!(D, u, v, Interior, Interior)
-    apply_region!(D, u, v, Interior, Upper)
-    apply_region!(D, u, v, Upper, Lower)
-    apply_region!(D, u, v, Upper, Interior)
-    apply_region!(D, u, v, Upper, Upper)
-    return nothing
-end
-
-# Maybe this should be split according to b3fbef345810 after all?! Seems like it makes performance more predictable
-function apply_region!(D::DiffOpCartesian{2}, u::AbstractArray{T,2}, v::AbstractArray{T,2}, r1::Type{<:Region}, r2::Type{<:Region}) where T
-    for I ∈ regionindices(D.grid.size, closuresize(D.op), (r1,r2))
-        @inbounds indextuple = (Index{r1}(I[1]), Index{r2}(I[2]))
-        @inbounds u[I] = apply(D, v, indextuple)
-    end
-    return nothing
-end
-export apply_region!
-
-function apply_tiled!(D::DiffOpCartesian{2}, u::AbstractArray{T,2}, v::AbstractArray{T,2}) where T
-    apply_region_tiled!(D, u, v, Lower, Lower)
-    apply_region_tiled!(D, u, v, Lower, Interior)
-    apply_region_tiled!(D, u, v, Lower, Upper)
-    apply_region_tiled!(D, u, v, Interior, Lower)
-    apply_region_tiled!(D, u, v, Interior, Interior)
-    apply_region_tiled!(D, u, v, Interior, Upper)
-    apply_region_tiled!(D, u, v, Upper, Lower)
-    apply_region_tiled!(D, u, v, Upper, Interior)
-    apply_region_tiled!(D, u, v, Upper, Upper)
-    return nothing
-end
-
-using TiledIteration
-function apply_region_tiled!(D::DiffOpCartesian{2}, u::AbstractArray{T,2}, v::AbstractArray{T,2}, r1::Type{<:Region}, r2::Type{<:Region}) where T
-    ri = regionindices(D.grid.size, closuresize(D.op), (r1,r2))
-    # TODO: Pass Tilesize to function
-    for tileaxs ∈ TileIterator(axes(ri), padded_tilesize(T, (5,5), 2))
-        for j ∈ tileaxs[2], i ∈ tileaxs[1]
-            I = ri[i,j]
-            u[I] = apply(D, v, (Index{r1}(I[1]), Index{r2}(I[2])))
-        end
-    end
-    return nothing
-end
-export apply_region_tiled!
-
-function apply(D::DiffOp, v::AbstractVector)::AbstractVector
-    u = zeros(eltype(v), size(v))
-    apply!(D,v,u)
-    return u
-end
-
-# TODO: This conflicts with LazyTensors. Shouldn't DiffOps be LazyTensorOperators and use that apply?
-# export apply
-
-
-"""
-    BoundaryCondition
-A BoundaryCondition should implement the method
-    sat(::DiffOp, v::AbstractArray, data::AbstractArray, ...)
-"""
-abstract type BoundaryCondition end
-
-
-end # module
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Diffinitive.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,22 @@
+module Diffinitive
+
+include("RegionIndices/RegionIndices.jl")
+include("LazyTensors/LazyTensors.jl")
+include("Grids/Grids.jl")
+include("SbpOperators/SbpOperators.jl")
+
+export RegionIndices
+export LazyTensors
+export Grids
+export SbpOperators
+
+
+# Aqua.jl fixes
+using StaticArrays
+using .LazyTensors
+Base.:+(a::StaticArray, b::LazyArray) = a +̃ b
+Base.:+(a::LazyArray, b::StaticArray) = a +̃ b
+Base.:-(a::StaticArray, b::LazyArray) = a -̃ b
+Base.:-(a::LazyArray, b::StaticArray) = a -̃ b
+
+end
--- a/src/Grids/Grids.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/Grids/Grids.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,45 +1,52 @@
 module Grids
 
-using Sbplib.RegionIndices
-using Sbplib.LazyTensors
+using Diffinitive.LazyTensors
 using StaticArrays
+using LinearAlgebra
 
 # Grid
 export Grid
 export coordinate_size
 export component_type
+export grid_id
+export boundary_id
+export boundary_indices
+export boundary_identifiers
+export boundary_grid
+export min_spacing
+export coarsen
+export refine
+export eval_on
+export componentview
+export ArrayComponentView
+export normal
+
+export BoundaryIdentifier
+export TensorGridBoundary
+export CartesianBoundary
+export LowerBoundary
+export UpperBoundary
 
 export TensorGrid
 export ZeroDimGrid
 
-export TensorGridBoundary
-
-export grid_id
-export boundary_id
-
-export eval_on
-export getcomponent
-
-# BoundaryIdentifier
-export BoundaryIdentifier
+export EquidistantGrid
+export inverse_spacing
+export spacing
+export equidistant_grid
 
 
-# EquidistantGrid
-export EquidistantGrid
-export spacing
-export inverse_spacing
-export boundary_identifiers
-export boundary_grid
-export refine
-export coarsen
-export equidistant_grid
-export CartesianBoundary
-
-abstract type BoundaryIdentifier end
+# MappedGrid
+export MappedGrid
+export jacobian
+export logical_grid
+export mapped_grid
+export metric_tensor
 
 include("grid.jl")
 include("tensor_grid.jl")
 include("equidistant_grid.jl")
 include("zero_dim_grid.jl")
+include("mapped_grid.jl")
 
 end # module
--- a/src/Grids/equidistant_grid.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/Grids/equidistant_grid.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -15,11 +15,13 @@
 end
 
 # Indexing interface
-Base.getindex(g::EquidistantGrid, i) = g.points[i]
+Base.getindex(g::EquidistantGrid, i::Int) = g.points[i]
 Base.eachindex(g::EquidistantGrid) = eachindex(g.points)
 Base.firstindex(g::EquidistantGrid) = firstindex(g.points)
 Base.lastindex(g::EquidistantGrid) = lastindex(g.points)
 
+Base.axes(g::EquidistantGrid, d) = axes(g.points, d)
+
 # Iteration interface
 Base.iterate(g::EquidistantGrid) = iterate(g.points)
 Base.iterate(g::EquidistantGrid, state) = iterate(g.points, state)
@@ -27,6 +29,7 @@
 Base.IteratorSize(::Type{<:EquidistantGrid}) = Base.HasShape{1}()
 Base.length(g::EquidistantGrid) = length(g.points)
 Base.size(g::EquidistantGrid) = size(g.points)
+Base.size(g::EquidistantGrid, d) = size(g.points)[d]
 
 
 """
@@ -44,11 +47,32 @@
 """
 inverse_spacing(g::EquidistantGrid) = 1/step(g.points)
 
+min_spacing(g::EquidistantGrid) = spacing(g)
 
-boundary_identifiers(::EquidistantGrid) = (Lower(), Upper())
-boundary_grid(g::EquidistantGrid, id::Lower) = ZeroDimGrid(g[begin])
-boundary_grid(g::EquidistantGrid, id::Upper) = ZeroDimGrid(g[end])
+"""
+    LowerBoundary <: BoundaryIdentifier
+
+Boundary identifier for the the lower (left) boundary of a one-dimensional grid.
+
+See also: [`BoundaryIdentifier`](@ref)
+"""
+struct LowerBoundary <: BoundaryIdentifier end
+
+"""
+    UpperBoundary <: BoundaryIdentifier
 
+Boundary identifier for the the upper (right)  boundary of a one-dimensional grid.
+
+See also: [`BoundaryIdentifier`](@ref)
+"""
+struct UpperBoundary <: BoundaryIdentifier end
+
+
+boundary_identifiers(::EquidistantGrid) = (LowerBoundary(), UpperBoundary())
+boundary_grid(g::EquidistantGrid, id::LowerBoundary) = ZeroDimGrid(g[begin])
+boundary_grid(g::EquidistantGrid, id::UpperBoundary) = ZeroDimGrid(g[end])
+boundary_indices(g::EquidistantGrid, id::LowerBoundary) = (firstindex(g),)
+boundary_indices(g::EquidistantGrid, id::UpperBoundary) = (lastindex(g),)
 
 """
     refine(g::EquidistantGrid, r::Int)
@@ -84,7 +108,7 @@
 
 
 """
-    equidistant_grid(size::Dims, limit_lower, limit_upper)
+    equidistant_grid(limit_lower, limit_upper, dims...)
 
 Construct an equidistant grid with corners at the coordinates `limit_lower` and
 `limit_upper`.
@@ -95,24 +119,27 @@
 of the grid are not allowed to be negative.
 
 The number of equispaced points in each coordinate direction are given
-by the tuple `size`.
+by the tuple `dims`.
 
-Note: If `limit_lower` and `limit_upper` are integers and `size` would allow a
+Note: If `limit_lower` and `limit_upper` are integers and `dims` would allow a
 completely integer grid, `equidistant_grid` will still return a floating point
-grid. This simlifies the implementation and avoids certain surprise
-behaviours.
+grid. This simplifies the implementation and avoids certain surprise
+behaviors.
 """
-function equidistant_grid(size::Dims, limit_lower, limit_upper)
-    gs = map(equidistant_grid, size, limit_lower, limit_upper)
+function equidistant_grid(limit_lower, limit_upper, dims::Vararg{Int})
+    if !(length(limit_lower) == length(limit_upper) == length(dims))
+        throw(ArgumentError("All arguments must be of the same length"))
+    end
+    gs = map(equidistant_grid, limit_lower, limit_upper, dims)
     return TensorGrid(gs...)
 end
 
 """
-    equidistant_grid(size::Int, limit_lower::T, limit_upper::T)
+    equidistant_grid(limit_lower::T, limit_upper::T, size::Int)
 
 Constructs a 1D equidistant grid.
 """
-function equidistant_grid(size::Int, limit_lower::T, limit_upper::T) where T
+function equidistant_grid(limit_lower::Number, limit_upper::Number, size::Int)
     if any(size .<= 0)
         throw(DomainError("size must be postive"))
     end
--- a/src/Grids/grid.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/Grids/grid.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -22,21 +22,73 @@
 Base.ndims(::Grid{T,D}) where {T,D} = D
 Base.eltype(::Type{<:Grid{T}}) where T = T
 
+Base.getindex(g::Grid, I::CartesianIndex) = g[Tuple(I)...]
+
 """
     coordinate_size(g)
 
-The lenght of the coordinate vector of `Grid` `g`.
+The length of the coordinate vector of `Grid` `g`.
 """
 coordinate_size(::Type{<:Grid{T}}) where T = _ncomponents(T)
 coordinate_size(g::Grid) = coordinate_size(typeof(g)) # TBD: Name of this function?!
 
 """
-    component_type(g)
+    component_type(gf)
+
+The type of the components of the elements of `gf`. I.e if `gf` is a vector
+valued grid function, `component_view(gf)` is the element type of the vectors
+at each grid point.
+
+# Examples
+```julia-repl
+julia> component_type([[1,2], [2,3], [3,4]])
+Int64
+```
+"""
+component_type(T::Type) = eltype(eltype(T))
+component_type(t) = component_type(typeof(t))
+
+"""
+    componentview(gf, component_index...)
+
+A view of `gf` with only the components specified by `component_index...`.
 
-The type of the components of the coordinate vector of `Grid` `g`.
+# Examples
+```julia-repl
+julia> componentview([[1,2], [2,3], [3,4]],2)
+3-element ArrayComponentView{Int64, Vector{Int64}, 1, Vector{Vector{Int64}}, Tuple{Int64}}:
+ 2
+ 3
+ 4
+```
 """
-component_type(::Type{<:Grid{T}}) where T = eltype(T)
-component_type(g::Grid) = component_type(typeof(g))
+componentview(gf, component_index...) = ArrayComponentView(gf, component_index)
+
+struct ArrayComponentView{CT,T,D,AT <: AbstractArray{T,D}, IT} <: AbstractArray{CT,D}
+    v::AT
+    component_index::IT
+
+    function ArrayComponentView(v, component_index)
+        CT = typeof(first(v)[component_index...])
+        return new{CT, eltype(v), ndims(v), typeof(v), typeof(component_index)}(v,component_index)
+    end
+end
+
+Base.size(cv::ArrayComponentView) = size(cv.v)
+Base.getindex(cv::ArrayComponentView, i::Int) = cv.v[i][cv.component_index...]
+Base.getindex(cv::ArrayComponentView, I::Vararg{Int}) = cv.v[I...][cv.component_index...]
+IndexStyle(::Type{<:ArrayComponentView{<:Any,<:Any,AT}}) where AT = IndexStyle(AT)
+
+# TODO: Implement `setindex!`?
+# TODO: Implement a more general ComponentView that can handle non-AbstractArrays.
+
+
+"""
+    min_spacing(g::Grid)
+
+The smallest distance between any pair of grid points in `g`.
+"""
+function min_spacing end
 
 """
     refine(g::Grid, r)
@@ -57,6 +109,13 @@
 function coarsen end
 
 """
+   BoundaryIdentifier
+
+An identifier for a boundary of a grid.
+"""
+abstract type BoundaryIdentifier end
+
+"""
     boundary_identifiers(g::Grid)
 
 Identifiers for all the boundaries of `g`.
@@ -72,9 +131,21 @@
 # TBD: Can we implement a version here that accepts multiple ids and grouped boundaries? Maybe we need multiblock stuff?
 
 """
+    boundary_indices(g::Grid, id::BoundaryIdentifier)
+
+A collection of indices corresponding to the boundary with given id. For grids
+with Cartesian indexing these collections will be tuples with elements of type
+``Union{Int,Colon}``.
+
+When implementing this method it is expected that the returned collection can
+be used to index grid functions to obtain grid functions on the boundary grid.
+"""
+function boundary_indices end
+
+"""
     eval_on(g::Grid, f)
 
-Lazy evaluation `f` on the grid. `f` can either be on the form `f(x,y,...)`
+Lazy evaluation of `f` on the grid. `f` can either be on the form `f(x,y,...)`
 with each coordinate as an argument, or on the form `f(x̄)` taking a
 coordinate vector.
 
@@ -85,9 +156,20 @@
     if hasmethod(f, (Any,))
         return LazyTensors.LazyFunctionArray((I...)->f(g[I...]), size(g))
     else
+        # TBD This branch can be removed if we accept the trade off that we define f with the syntax f((x,y)) instead if we don't want to handle the vector in the body of f. (Add an example in the docs)
+        # Also see Notes.md
         return LazyTensors.LazyFunctionArray((I...)->f(g[I...]...), size(g))
     end
 end
 
+"""
+    eval_on(g::Grid, f::Number)
+
+Lazy evaluation of a scalar `f` on the grid.
+"""
+eval_on(g::Grid, f::Number) = return LazyTensors.LazyConstantArray(f, size(g))
+
 _ncomponents(::Type{<:Number}) = 1
 _ncomponents(T::Type{<:SVector}) = length(T)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Grids/mapped_grid.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,218 @@
+"""
+    MappedGrid{T,D} <: Grid{T,D}
+
+A grid defined by a coordinate mapping from a logical grid to some physical
+coordinates. The physical coordinates and the Jacobian are stored as grid
+functions corresponding to the logical grid.
+
+See also: [`logical_grid`](@ref), [`jacobian`](@ref), [`metric_tensor`](@ref).
+"""
+struct MappedGrid{T,D, GT<:Grid{<:Any,D}, CT<:AbstractArray{T,D}, JT<:AbstractArray{<:AbstractMatrix{<:Any}, D}} <: Grid{T,D}
+    logical_grid::GT
+    physicalcoordinates::CT
+    jacobian::JT
+
+    """
+        MappedGrid(logical_grid, physicalcoordinates, jacobian)
+
+    A MappedGrid with the given physical coordinates and jacobian.
+    """
+    function MappedGrid(logical_grid::GT, physicalcoordinates::CT, jacobian::JT) where {T,D, GT<:Grid{<:Any,D}, CT<:AbstractArray{T,D}, JT<:AbstractArray{<:AbstractMatrix{<:Any}, D}}
+        if !(size(logical_grid) == size(physicalcoordinates) == size(jacobian))
+            throw(ArgumentError("Sizes must match"))
+        end
+
+        if size(first(jacobian)) != (length(first(physicalcoordinates)),D)
+            throw(ArgumentError("The size of the jacobian must match the dimensions of the grid and coordinates"))
+        end
+
+        return new{T,D,GT,CT,JT}(logical_grid, physicalcoordinates, jacobian)
+    end
+end
+
+function Base.:(==)(a::MappedGrid, b::MappedGrid)
+    same_logical_grid = logical_grid(a) == logical_grid(b)
+    same_coordinates = collect(a) == collect(b)
+    same_jacobian = jacobian(a) == jacobian(b)
+
+    return same_logical_grid && same_coordinates && same_jacobian
+end
+
+"""
+    logical_grid(g::MappedGrid)
+
+The logical grid of `g`.
+"""
+logical_grid(g::MappedGrid) = g.logical_grid
+
+"""
+    jacobian(g::MappedGrid)
+
+The Jacobian matrix of `g` as a grid function.
+"""
+jacobian(g::MappedGrid) = g.jacobian
+
+
+# Indexing interface
+Base.getindex(g::MappedGrid, I::Vararg{Int}) = g.physicalcoordinates[I...]
+Base.eachindex(g::MappedGrid) = eachindex(g.logical_grid)
+
+Base.firstindex(g::MappedGrid, d) = firstindex(g.logical_grid, d)
+Base.lastindex(g::MappedGrid, d) = lastindex(g.logical_grid, d)
+
+# Iteration interface
+Base.iterate(g::MappedGrid) = iterate(g.physicalcoordinates)
+Base.iterate(g::MappedGrid, state) = iterate(g.physicalcoordinates, state)
+
+Base.IteratorSize(::Type{<:MappedGrid{<:Any, D}}) where D = Base.HasShape{D}()
+Base.length(g::MappedGrid) = length(g.logical_grid)
+Base.size(g::MappedGrid) = size(g.logical_grid)
+Base.size(g::MappedGrid, d) = size(g.logical_grid, d)
+
+boundary_identifiers(g::MappedGrid) = boundary_identifiers(g.logical_grid)
+boundary_indices(g::MappedGrid, id::TensorGridBoundary) = boundary_indices(g.logical_grid, id)
+
+function boundary_grid(g::MappedGrid, id::TensorGridBoundary)
+    b_indices = boundary_indices(g.logical_grid, id)
+
+    # Calculate indices of needed jacobian components
+    D = ndims(g)
+    all_indices = SVector{D}(1:D)
+    free_variable_indices = deleteat(all_indices, grid_id(id))
+    jacobian_components = (:, free_variable_indices)
+
+    # Create grid function for boundary grid jacobian
+    boundary_jacobian = componentview((@view g.jacobian[b_indices...])  , jacobian_components...)
+    boundary_physicalcoordinates = @view g.physicalcoordinates[b_indices...]
+
+    return MappedGrid(
+        boundary_grid(g.logical_grid, id),
+        boundary_physicalcoordinates,
+        boundary_jacobian,
+    )
+end
+
+# TODO: Make sure all methods of `mapped_grid` are implemented correctly and tested.
+"""
+    mapped_grid(x, J, size::Vararg{Int})
+
+A `MappedGrid` with a default logical grid on the D-dimensional unit hyper 
+box [0,1]ᴰ. `x` and `J` are functions to be evaluated on the logical grid
+and `size` determines the size of the logical grid.
+"""
+function mapped_grid(x, J, size::Vararg{Int})
+    D = length(size)
+        lg = equidistant_grid(ntuple(i->0., D), ntuple(i->1., D), size...) # TODO: Clean this up with ParamaterSpace once feature/grids/manifolds is merged
+    return mapped_grid(x, J, lg)
+end
+
+"""
+    mapped_grid(x, J, lg::Grid)
+
+A `MappedGrid` with logical grid `lg`. Physical coordinates and Jacobian are
+determined by the functions `x` and `J`.
+"""
+function mapped_grid(x, J, lg::Grid)
+    return MappedGrid(
+        lg,
+        map(x,lg),
+        map(J,lg),
+    )
+end
+
+"""
+    mapped_grid(x, J, parameterspace, size)
+
+A `MappedGrid` with logical grid `lg`. Physical coordinates and Jacobian are
+determined by the functions `x` and `J`.
+"""
+function mapped_grid(x, J, parameterspace, size::Vararg{Int})
+    lg = equidistant_grid(parameterspace, size...)
+    return mapped_grid(x, J, lg)
+end
+
+"""
+    metric_tensor(g::MappedGrid)
+
+The metric tensor of `g` as a grid function.
+"""
+function metric_tensor(g::MappedGrid)
+    return map(jacobian(g)) do ∂x∂ξ
+        ∂x∂ξ'*∂x∂ξ
+    end
+end
+
+function min_spacing(g::MappedGrid{T,1} where T)
+    n, = size(g)
+
+    ms = Inf
+    for i ∈ 1:n-1
+        ms = min(ms, norm(g[i+1]-g[i]))
+    end
+
+    return ms
+end
+
+function min_spacing(g::MappedGrid{T,2} where T)
+    n, m = size(g)
+
+    ms = Inf
+    for i ∈ 1:n-1, j ∈ 1:m-1 # loop over each cell of the grid
+
+        ms = min(
+            ms,
+            norm(g[i+1,j]-g[i,j]),
+            norm(g[i,j+1]-g[i,j]),
+
+            norm(g[i+1,j]-g[i+1,j+1]),
+            norm(g[i,j+1]-g[i+1,j+1]),
+
+            norm(g[i+1,j+1]-g[i,j]),
+            norm(g[i+1,j]-g[i,j+1]),
+        )
+        # NOTE: This could be optimized to avoid checking all interior edges twice.
+    end
+
+    return ms
+end
+
+"""
+    normal(g::MappedGrid, boundary)
+
+The outward pointing normal as a grid function on the corresponding boundary grid.
+"""
+function normal(g::MappedGrid, boundary)
+    b_indices = boundary_indices(g, boundary)
+    σ = _boundary_sign(component_type(g), boundary)
+
+    # TODO: Refactor this when `boundary_indices(g, ...)` has been made iterable.
+    return map(jacobian(g)[b_indices...]) do ∂x∂ξ
+        ∂ξ∂x = inv(∂x∂ξ)
+        k = grid_id(boundary)
+        σ*∂ξ∂x[k,:]/norm(∂ξ∂x[k,:])
+    end
+end
+
+"""
+    normal(g::MappedGrid, boundary, i...)
+
+The outward pointing normal to the specified boundary in grid point `i`.
+"""
+function normal(g::MappedGrid, boundary, i...)
+    σ = _boundary_sign(component_type(g), boundary)
+    ∂ξ∂x = inv(jacobian(g)[i...])
+
+    k = grid_id(boundary)
+    return σ*∂ξ∂x[k,:]/norm(∂ξ∂x[k,:])
+end
+
+
+function _boundary_sign(T, boundary)
+    if boundary_id(boundary) == UpperBoundary()
+        return one(T)
+    elseif boundary_id(boundary) == LowerBoundary()
+        return -one(T)
+    else
+        throw(ArgumentError("The boundary identifier must be either `LowerBoundary()` or `UpperBoundary()`"))
+    end
+end
--- a/src/Grids/tensor_grid.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/Grids/tensor_grid.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -17,7 +17,7 @@
 end
 
 # Indexing interface
-function Base.getindex(g::TensorGrid, I...)
+function Base.getindex(g::TensorGrid, I::Vararg{Int})
     szs = ndims.(g.grids)
 
     Is = LazyTensors.split_tuple(I, szs)
@@ -26,13 +26,16 @@
     return vcat(ps...)
 end
 
-Base.getindex(g::TensorGrid, I::CartesianIndex) = g[Tuple(I)...]
-
 function Base.eachindex(g::TensorGrid)
     szs = LazyTensors.concatenate_tuples(size.(g.grids)...)
     return CartesianIndices(szs)
 end
 
+function Base.axes(g::TensorGrid, d)
+    i, ld = grid_and_local_dim_index(ndims.(g.grids), d)
+    return axes(g.grids[i], ld)
+end
+
 # Iteration interface
 Base.iterate(g::TensorGrid) = iterate(Iterators.product(g.grids...)) |> _iterate_combine_coords
 Base.iterate(g::TensorGrid, state) = iterate(Iterators.product(g.grids...), state) |> _iterate_combine_coords
@@ -40,10 +43,20 @@
 _iterate_combine_coords((next,state)) = combine_coordinates(next...), state
 
 Base.IteratorSize(::Type{<:TensorGrid{<:Any, D}}) where D = Base.HasShape{D}()
-Base.eltype(::Type{<:TensorGrid{T}}) where T = T
-Base.length(g::TensorGrid) = sum(length, g.grids)
+Base.length(g::TensorGrid) = prod(length, g.grids)
 Base.size(g::TensorGrid) = LazyTensors.concatenate_tuples(size.(g.grids)...)
+Base.size(g::TensorGrid, d) = size(g)[d]
 
+function spacing(g::TensorGrid)
+    relevant_grids = filter(g->!isa(g,ZeroDimGrid),g.grids)
+    return spacing.(relevant_grids)
+end
+
+function min_spacing(g::TensorGrid)
+    relevant_grids = filter(g->!isa(g,ZeroDimGrid),g.grids)
+    d = min_spacing.(relevant_grids)
+    return minimum(d)
+end
 
 refine(g::TensorGrid, r::Int) = mapreduce(g->refine(g,r), TensorGrid, g.grids)
 coarsen(g::TensorGrid, r::Int) = mapreduce(g->coarsen(g,r), TensorGrid, g.grids)
@@ -70,7 +83,6 @@
     return LazyTensors.concatenate_tuples(per_grid...)
 end
 
-
 """
     boundary_grid(g::TensorGrid, id::TensorGridBoundary)
 
@@ -82,6 +94,16 @@
     return TensorGrid(new_grids...)
 end
 
+function boundary_indices(g::TensorGrid, id::TensorGridBoundary)
+    per_grid_ind = map(g.grids) do g
+        ntuple(i->:, ndims(g))
+    end
+
+    local_b_ind = boundary_indices(g.grids[grid_id(id)], boundary_id(id))
+    b_ind = Base.setindex(per_grid_ind, local_b_ind, grid_id(id))
+
+    return LazyTensors.concatenate_tuples(b_ind...)
+end
 
 function combined_coordinate_vector_type(coordinate_types...)
     combined_coord_length = mapreduce(_ncomponents, +, coordinate_types)
@@ -97,3 +119,27 @@
 function combine_coordinates(coords...)
     return mapreduce(SVector, vcat, coords)
 end
+
+"""
+    grid_and_local_dim_index(nds, d)
+
+Given a tuple of number of dimensions `nds`, and a global dimension index `d`,
+calculate which grid index, and local dimension, `d` corresponds to.
+
+`nds` would come from broadcasting `ndims` on the grids tuple of a
+`TensorGrid`. If you are interested in a dimension `d` of a tensor grid `g`
+```julia
+gi, ldi = grid_and_local_dim_index(ndims.(g.grids), d)
+```
+tells you which grid it belongs to (`gi`) and which index it is at within that
+grid (`ldi`).
+"""
+function grid_and_local_dim_index(nds, d)
+    I = findfirst(>=(d), cumsum(nds))
+
+    if I == 1
+        return (1, d)
+    else
+        return (I, d-cumsum(nds)[I-1])
+    end
+end
--- a/src/Grids/zero_dim_grid.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/Grids/zero_dim_grid.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -25,3 +25,4 @@
 
 boundary_identifiers(g::ZeroDimGrid) = ()
 boundary_grid(g::ZeroDimGrid, ::Any) = throw(ArgumentError("ZeroDimGrid has no boundaries"))
+boundary_indices(g::ZeroDimGrid, ::Any) = throw(ArgumentError("ZeroDimGrid has no boundaries"))
--- a/src/LazyTensors/LazyTensors.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/LazyTensors/LazyTensors.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,5 +1,11 @@
 module LazyTensors
 
+export LazyTensor
+export apply
+export apply_transpose
+export range_dim, domain_dim
+export range_size, domain_size
+
 export TensorApplication
 export TensorTranspose
 export TensorComposition
@@ -13,6 +19,10 @@
 export DomainSizeMismatch
 export RangeSizeMismatch
 
+export LazyArray
+export LazyFunctionArray
+export +̃, -̃, *̃, /̃
+
 include("lazy_tensor.jl")
 include("tensor_types.jl")
 include("lazy_array.jl")
--- a/src/LazyTensors/lazy_array.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/LazyTensors/lazy_array.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,12 +1,11 @@
 """
     LazyArray{T,D} <: AbstractArray{T,D}
 
-Array which is calcualted lazily when indexing.
+Array which is calculated lazily when indexing.
 
 A subtype of `LazyArray` will use lazy version of `+`, `-`, `*`, `/`.
 """
 abstract type LazyArray{T,D} <: AbstractArray{T,D} end
-export LazyArray
 
 struct LazyConstantArray{T,D} <: LazyArray{T,D}
 	val::T
@@ -25,7 +24,6 @@
     f::F
     size::NTuple{D,Int}
 end
-export LazyFunctionArray
 
 function LazyFunctionArray(f::F, size::NTuple{D,Int}) where {F<:Function,D}
     T = typeof(f(ones(Int, D)...))
@@ -42,7 +40,7 @@
 
 """
     LazyElementwiseOperation{T,D,Op} <: LazyArray{T,D}
-Struct allowing for lazy evaluation of elementwise operations on `AbstractArray`s.
+Struct allowing for lazy evaluation of element-wise operations on `AbstractArray`s.
 
 A `LazyElementwiseOperation` contains two arrays together with an operation.
 The operations are carried out when the `LazyElementwiseOperation` is indexed.
@@ -110,5 +108,3 @@
 
 Base.@propagate_inbounds Base.:+(a::T, b::LazyArray{T,D}) where {T,D} = a +̃ b
 Base.@propagate_inbounds Base.:-(a::T, b::LazyArray{T,D}) where {T,D} = a -̃  b
-
-export +̃, -̃, *̃, /̃
--- a/src/LazyTensors/lazy_tensor.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/LazyTensors/lazy_tensor.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,9 +1,3 @@
-export LazyTensor
-export apply
-export apply_transpose
-export range_dim, domain_dim
-export range_size, domain_size
-
 """
     LazyTensor{T,R,D}
 
--- a/src/LazyTensors/lazy_tensor_operations.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/LazyTensors/lazy_tensor_operations.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -5,7 +5,7 @@
 
 Allows the result of a `LazyTensor` applied to a vector to be treated as an `AbstractArray`.
 With a mapping `m` and a vector `v` the TensorApplication object can be created by `m*v`.
-The actual result will be calcualted when indexing into `m*v`.
+The actual result will be calculated when indexing into `m*v`.
 """
 struct TensorApplication{T,R,D, TM<:LazyTensor{<:Any,R,D}, AA<:AbstractArray{<:Any,D}} <: LazyArray{T,R}
     t::TM
@@ -52,7 +52,7 @@
 domain_size(tmt::TensorTranspose) = range_size(tmt.tm)
 
 
-struct ElementwiseTensorOperation{Op,T,R,D,T1<:LazyTensor{T,R,D},T2<:LazyTensor{T,R,D}} <: LazyTensor{T,D,R}
+struct ElementwiseTensorOperation{Op,T,R,D,T1<:LazyTensor{T,R,D},T2<:LazyTensor{T,R,D}} <: LazyTensor{T,R,D}
     tm1::T1
     tm2::T2
 
@@ -102,7 +102,7 @@
     TensorComposition(tm, tmi::IdentityTensor)
     TensorComposition(tmi::IdentityTensor, tm)
 
-Composes a `Tensormapping` `tm` with an `IdentityTensor` `tmi`, by returning `tm`
+Composes a `LazyTensor` `tm` with an `IdentityTensor` `tmi`, by returning `tm`
 """
 function TensorComposition(tm::LazyTensor{T,R,D}, tmi::IdentityTensor{T,D}) where {T,R,D}
     @boundscheck check_domain_size(tm, range_size(tmi))
@@ -121,11 +121,12 @@
 
 Base.:*(a::T, tm::LazyTensor{T}) where T = TensorComposition(ScalingTensor{T,range_dim(tm)}(a,range_size(tm)), tm)
 Base.:*(tm::LazyTensor{T}, a::T) where T = a*tm
+Base.:-(tm::LazyTensor) = (-one(eltype(tm)))*tm
 
 """
     InflatedTensor{T,R,D} <: LazyTensor{T,R,D}
 
-An inflated `LazyTensor` with dimensions added before and afer its actual dimensions.
+An inflated `LazyTensor` with dimensions added before and after its actual dimensions.
 """
 struct InflatedTensor{T,R,D,D_before,R_middle,D_middle,D_after, TM<:LazyTensor{T,R_middle,D_middle}} <: LazyTensor{T,R,D}
     before::IdentityTensor{T,D_before}
@@ -168,10 +169,10 @@
     )
 end
 
-InflatedTensor(before::IdentityTensor, tm::LazyTensor{T}) where T = InflatedTensor(before,tm,IdentityTensor{T}())
-InflatedTensor(tm::LazyTensor{T}, after::IdentityTensor) where T = InflatedTensor(IdentityTensor{T}(),tm,after)
+InflatedTensor(before::IdentityTensor, tm::LazyTensor) = InflatedTensor(before,tm,IdentityTensor{eltype(tm)}())
+InflatedTensor(tm::LazyTensor, after::IdentityTensor) = InflatedTensor(IdentityTensor{eltype(tm)}(),tm,after)
 # Resolve ambiguity between the two previous methods
-InflatedTensor(I1::IdentityTensor{T}, I2::IdentityTensor{T}) where T = InflatedTensor(I1,I2,IdentityTensor{T}())
+InflatedTensor(I1::IdentityTensor, I2::IdentityTensor) = InflatedTensor(I1,I2,IdentityTensor{promote_type(eltype(I1), eltype(I2))}())
 
 # TODO: Implement some pretty printing in terms of ⊗. E.g InflatedTensor(I(3),B,I(2)) -> I(3)⊗B⊗I(2)
 
@@ -219,7 +220,7 @@
 @doc raw"""
     LazyOuterProduct(tms...)
 
-Creates a `TensorComposition` for the outerproduct of `tms...`.
+Creates a `TensorComposition` for the outer product of `tms...`.
 This is done by separating the outer product into regular products of outer products involving only identity mappings and one non-identity mapping.
 
 First let
@@ -262,7 +263,7 @@
     return itm1∘itm2
 end
 
-LazyOuterProduct(t1::IdentityTensor{T}, t2::IdentityTensor{T}) where T = IdentityTensor{T}(t1.size...,t2.size...)
+LazyOuterProduct(t1::IdentityTensor, t2::IdentityTensor) = IdentityTensor{promote_type(eltype(t1),eltype(t2))}(t1.size...,t2.size...)
 LazyOuterProduct(t1::LazyTensor, t2::IdentityTensor) = InflatedTensor(t1, t2)
 LazyOuterProduct(t1::IdentityTensor, t2::LazyTensor) = InflatedTensor(t1, t2)
 
@@ -278,7 +279,7 @@
 `tm`.
 
 An example of when this operation is useful is when extending a one
-dimensional difference operator `D` to a 2D grid of a ceratin size. In that
+dimensional difference operator `D` to a 2D grid of a certain size. In that
 case we could have
 
 ```julia
--- a/src/LazyTensors/tensor_types.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/LazyTensors/tensor_types.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,7 +1,7 @@
 """
     IdentityTensor{T,D} <: LazyTensor{T,D,D}
 
-The lazy identity LazyTensor for a given size. Usefull for building up higher dimensional tensor mappings from lower
+The lazy identity LazyTensor for a given size. Useful for building up higher dimensional tensor mappings from lower
 dimensional ones through outer products. Also used in the Implementation for InflatedTensor.
 """
 struct IdentityTensor{T,D} <: LazyTensor{T,D,D}
@@ -57,8 +57,8 @@
 """
     DenseTensor{T,R,D,...}(A, range_indicies, domain_indicies)
 
-LazyTensor defined by the AbstractArray A. `range_indicies` and `domain_indicies` define which indicies of A should
-be considerd the range and domain of the LazyTensor. Each set of indices must be ordered in ascending order.
+LazyTensor defined by the AbstractArray A. `range_indicies` and `domain_indicies` define which indices of A should
+be considered the range and domain of the LazyTensor. Each set of indices must be ordered in ascending order.
 
 For instance, if A is a m x n matrix, and range_size = (1,), domain_size = (2,), then the DenseTensor performs the
 standard matrix-vector product on vectors of size n.
--- a/src/LazyTensors/tuple_manipulation.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/LazyTensors/tuple_manipulation.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -3,7 +3,7 @@
 
 Splits the multi-index `I` into two parts. One part which is expected to be
 used as a view, and one which is expected to be used as an index.
-Eg.
+E.g.
 ```julia-repl
 julia> LazyTensors.split_index(1, 3, 2, 1, (1,2,3,4)...)
 ((1, Colon(), Colon(), Colon(), 4), (2, 3))
@@ -33,7 +33,7 @@
     split_tuple(t, szs)
 
 Split the tuple `t` into a set of tuples of the sizes given in `szs`.
-`sum(szs)` should equal `lenght(t)`.
+`sum(szs)` should equal `length(t)`.
 
 E.g
 ```julia-repl
--- a/src/RegionIndices/RegionIndices.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/RegionIndices/RegionIndices.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -13,7 +13,7 @@
     Index{R,T}(i::T) where {R<:Region,T<:Integer} = new{R,T}(i)
     Index{R}(i::T) where {R<:Region,T<:Integer} = new{R,T}(i)
     Index(i::T, ::Type{R}) where {R<:Region,T<:Integer} = Index{R,T}(i)
-    Index(t::Tuple{T, DataType}) where {R<:Region,T<:Integer} = Index{t[2],T}(t[1]) # TBD: This is not very specific in what types are allowed in t[2]. Can this be fixed?
+    Index(t::Tuple{T, DataType}) where T<:Integer = Index{t[2],T}(t[1]) # TBD: This is not very specific in what types are allowed in t[2]. Can this be fixed?
 end
 
 export Index
@@ -25,18 +25,17 @@
 Index(R::Type{<:Region}, T::Type{<:Integer}) = Index{R,T}
 IndexTupleType(T::Type{<:Integer},R::NTuple{N, DataType} where N) = Tuple{Index.(R, T)...}
 
-Base.convert(::Type{T}, i::Index{R,T} where R) where T = i.i
+Base.convert(::Type{T}, i::Index{R,T} where R) where T <: Integer = i.i
 Base.convert(::Type{CartesianIndex}, I::NTuple{N,Index} where N) = CartesianIndex(convert.(Int, I))
 
 Base.Int(I::Index) = I.i
 Base.to_index(I::Index) = Int(I) #How to get this to work for all cases??
-Base.getindex(A::AbstractArray{T,N}, I::NTuple{N,Index}) where {T,N} = A[I...] #Is this ok??
 
 function Index(i::Integer, boundary_width::Integer, dim_size::Integer)
     return Index{getregion(i,boundary_width,dim_size)}(i)
 end
 
-IndexTuple(t::Vararg{Tuple{T, DataType}}) where T<:Integer = Index.(t)
+IndexTuple(t::Vararg{Tuple{Integer, DataType}}) = Index.(t)
 export IndexTuple
 
 # TODO: Use the values of the region structs, e.g. Lower(), for the region parameter instead of the types.
--- a/src/SbpOperators/SbpOperators.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/SbpOperators/SbpOperators.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,16 +1,22 @@
 module SbpOperators
 
+# Stencils
+export Stencil
+export CenteredStencil
+export NestedStencil
+export CenteredNestedStencil
+
 # Stencil set
 export StencilSet
 export read_stencil_set
 export get_stencil_set
 export parse_stencil
+export parse_nested_stencil
 export parse_scalar
 export parse_tuple
 export sbp_operators_path
 
 # Operators
-export boundary_quadrature
 export boundary_restriction
 export inner_product
 export inverse_inner_product
@@ -19,24 +25,42 @@
 export normal_derivative
 export first_derivative
 export second_derivative
+export second_derivative_variable
 export undivided_skewed04
-
-using Sbplib.RegionIndices
-using Sbplib.LazyTensors
-using Sbplib.Grids
+export closure_size
 
 @enum Parity begin
     odd = -1
     even = 1
 end
 
+
+# Boundary conditions
+export BoundaryCondition
+export NeumannCondition
+export DirichletCondition
+export discretize_data
+export boundary_data
+export boundary
+export sat
+export sat_tensors
+
+# Using
+using Diffinitive.RegionIndices
+using Diffinitive.LazyTensors
+using Diffinitive.Grids
+
+# Includes
 include("stencil.jl")
 include("stencil_set.jl")
+include("boundary_conditions/boundary_condition.jl")
+include("boundary_conditions/sat.jl")
 include("volumeops/volume_operator.jl")
 include("volumeops/stencil_operator_distinct_closures.jl")
 include("volumeops/constant_interior_scaling_operator.jl")
 include("volumeops/derivatives/first_derivative.jl")
 include("volumeops/derivatives/second_derivative.jl")
+include("volumeops/derivatives/second_derivative_variable.jl")
 include("volumeops/derivatives/dissipation.jl")
 include("volumeops/laplace/laplace.jl")
 include("volumeops/inner_products/inner_product.jl")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/SbpOperators/boundary_conditions/boundary_condition.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,62 @@
+"""
+    BoundaryCondition
+
+Description of a boundary condition. Implementations describe the kind of
+boundary condition, what boundary the condition applies to, and any associated
+data. Should implement [`boundary`](@ref) and may implement
+[`boundary_data`](@ref) if applicable.
+
+For examples see [`DirichletCondition`](@ref) and [`NeumannCondition`](@ref)
+"""
+abstract type BoundaryCondition end
+
+"""
+    boundary(::BoundaryCondition)
+
+The boundary identifier of the BoundaryCondition.
+"""
+function boundary end
+
+"""
+    boundary_data(::BoundaryCondition)
+
+If implemented, the data associated with the BoundaryCondition.
+"""
+function boundary_data end
+
+"""
+    discretize_data(grid, bc::BoundaryCondition)
+
+The data of `bc` as a lazily evaluated grid function on the boundary grid
+specified by `boundary(bc)`.
+"""
+function discretize_data(grid, bc::BoundaryCondition)
+    return eval_on(boundary_grid(grid, boundary(bc)), boundary_data(bc))
+end
+
+"""
+    DirichletCondition{DT,BID}
+
+A Dirichlet condition with `data::DT` on the boundary
+specified by the boundary identifier `BID`.
+"""
+struct DirichletCondition{DT,BID} <: BoundaryCondition
+    data::DT
+    boundary::BID
+end
+boundary_data(bc::DirichletCondition) = bc.data
+boundary(bc::DirichletCondition) = bc.boundary
+
+"""
+    NeumannCondition{DT,BID}
+
+A Neumann condition with `data::DT` on the boundary
+specified by the boundary identifier `BID`.
+"""
+struct NeumannCondition{DT,BID} <: BoundaryCondition
+    data::DT
+    boundary::BID
+end
+boundary_data(bc::NeumannCondition) = bc.data
+boundary(bc::NeumannCondition) = bc.boundary
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/SbpOperators/boundary_conditions/sat.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,25 @@
+"""
+    sat_tensors(op, grid, bc::BoundaryCondition; kwargs...)
+
+The penalty tensor and boundary operator used to construct a
+simultaneous-approximation-term for imposing `bc` related to `op`.
+
+For `penalty_tensor, L  = sat_tensors(...)` then `SAT(u,g) =
+penalty_tensor*(L*u - g)`  where `g` is the boundary data.
+"""
+function sat_tensors end
+
+
+"""
+    sat(op, grid, bc::BoundaryCondition; kwargs...)
+
+Simultaneous-Approximation-Term for a general `bc` to `op`. Returns a function
+`SAT(u,g)` weakly imposing `bc` when added to `op*u`.
+
+Internally `sat_tensors(op, grid, bc; ...)` is called to construct the
+necessary parts for the SAT.
+"""
+function sat(op, grid, bc::BoundaryCondition; kwargs...)
+    penalty_tensor, L = sat_tensors(op, grid, bc; kwargs...)
+    return SAT(u, g) = penalty_tensor*(L*u - g)
+end
--- a/src/SbpOperators/boundaryops/boundary_operator.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/SbpOperators/boundaryops/boundary_operator.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,27 +1,27 @@
 """
-    BoundaryOperator{T,R,N} <: LazyTensor{T,0,1}
+    BoundaryOperator{T,B,N} <: LazyTensor{T,0,1}
 
 Implements the boundary operator `op` for 1D as a `LazyTensor`
 
 `op` is the restriction of a grid function to the boundary using some closure
-`Stencil{T,N}`. The boundary to restrict to is determined by `R`. `op'` is the
+`Stencil{T,N}`. The boundary to restrict to is determined by `B`. `op'` is the
 prolongation of a zero dimensional array to the whole grid using the same
 closure stencil.
 """
-struct BoundaryOperator{T,R<:Region,N} <: LazyTensor{T,0,1}
+struct BoundaryOperator{T,B<:BoundaryIdentifier,N} <: LazyTensor{T,0,1}
     stencil::Stencil{T,N}
     size::Int
 end
 
 """
-    BoundaryOperator(grid::EquidistantGrid, closure_stencil, region)
+    BoundaryOperator(grid::EquidistantGrid, closure_stencil, boundary)
 
 Constructs the BoundaryOperator with stencil `closure_stencil` for a
 `EquidistantGrid` `grid`, restricting to to the boundary specified by
-`region`.
+`boundary`.
 """
-function BoundaryOperator(grid::EquidistantGrid, closure_stencil::Stencil{T,N}, region::Region) where {T,N}
-    return BoundaryOperator{T,typeof(region),N}(closure_stencil,size(grid)[1])
+function BoundaryOperator(grid::EquidistantGrid, closure_stencil::Stencil{T,N}, boundary::BoundaryIdentifier) where {T,N}
+    return BoundaryOperator{T,typeof(boundary),N}(closure_stencil,size(grid)[1])
 end
 
 """
@@ -29,24 +29,24 @@
 
 The size of the closure stencil.
 """
-closure_size(::BoundaryOperator{T,R,N}) where {T,R,N} = N
+closure_size(::BoundaryOperator{T,B,N}) where {T,B,N} = N
 
 LazyTensors.range_size(op::BoundaryOperator) = ()
 LazyTensors.domain_size(op::BoundaryOperator) = (op.size,)
 
-function LazyTensors.apply(op::BoundaryOperator{<:Any,Lower}, v::AbstractVector)
+function LazyTensors.apply(op::BoundaryOperator{<:Any,LowerBoundary}, v::AbstractVector)
     apply_stencil(op.stencil,v,1)
 end
 
-function LazyTensors.apply(op::BoundaryOperator{<:Any,Upper}, v::AbstractVector)
+function LazyTensors.apply(op::BoundaryOperator{<:Any,UpperBoundary}, v::AbstractVector)
     apply_stencil_backwards(op.stencil,v,op.size)
 end
 
-function LazyTensors.apply_transpose(op::BoundaryOperator{<:Any,Lower}, v::AbstractArray{<:Any,0}, i::Index{Lower})
+function LazyTensors.apply_transpose(op::BoundaryOperator{<:Any,LowerBoundary}, v::AbstractArray{<:Any,0}, i::Index{Lower})
     return op.stencil[Int(i)-1]*v[]
 end
 
-function LazyTensors.apply_transpose(op::BoundaryOperator{<:Any,Upper}, v::AbstractArray{<:Any,0}, i::Index{Upper})
+function LazyTensors.apply_transpose(op::BoundaryOperator{<:Any,UpperBoundary}, v::AbstractArray{<:Any,0}, i::Index{Upper})
     return op.stencil[op.size[1] - Int(i)]*v[]
 end
 
--- a/src/SbpOperators/operators/standard_diagonal.toml	Wed May 31 08:59:34 2023 +0200
+++ b/src/SbpOperators/operators/standard_diagonal.toml	Sat Jan 11 10:19:47 2025 +0100
@@ -20,6 +20,10 @@
 H.inner = "1"
 H.closure = ["1/2"]
 
+e.closure = ["1"]
+d1.closure = {s = ["3/2", "-2", "1/2"], c = 1}
+
+
 D1.inner_stencil = ["-1/2", "0", "1/2"]
 D1.closure_stencils = [
     {s = ["-1", "1"], c = 1},
@@ -30,8 +34,13 @@
     {s = ["1", "-2", "1"], c = 1},
 ]
 
-e.closure = ["1"]
-d1.closure = {s = ["3/2", "-2", "1/2"], c = 1}
+D2.positivity = {theta_M = "0.3636363636", theta_R = "1.000000538455350", m_b = "2"}
+
+D2variable.inner_stencil = [["1/2", "1/2", "0"],[ "-1/2", "-1", "-1/2"],["0", "1/2", "1/2"]]
+D2variable.closure_stencils = [
+        {s = [["2", "-1", "0"],["-3", "1",   "0"],["1","0","0"]], c = 1},
+]
+
 
 [[stencil_set]]
 
@@ -40,6 +49,9 @@
 H.inner = "1"
 H.closure = ["17/48", "59/48", "43/48", "49/48"]
 
+e.closure = ["1"]
+d1.closure = {s = ["11/6", "-3", "3/2", "-1/3"], c = 1}
+
 D1.inner_stencil = ["1/12","-2/3","0","2/3","-1/12"]
 D1.closure_stencils = [
     {s = [ "-24/17",  "59/34",  "-4/17", "-3/34",     "0",     "0"], c = 1},
@@ -56,5 +68,91 @@
     {s = [ "-1/49",     "0",   "59/49", "-118/49", "64/49", "-4/49"], c = 4},
 ]
 
+D2.positivity = {theta_M = "0.2505765857", theta_R = "0.577587500088313", m_b = "4"}
+
+D2variable.inner_stencil = [
+    ["-1/8",   "1/6", "-1/8",   "0",    "0"  ],
+    [ "1/6",   "1/2",  "1/2",  "1/6",   "0"  ],
+    ["-1/24", "-5/6", "-3/4", "-5/6", "-1/24"],
+    [  "0",    "1/6",  "1/2",  "1/2",  "1/6" ],
+    [  "0",     "0",  "-1/8",  "1/6", "-1/8" ],
+]
+D2variable.closure_stencils = [
+    {c = 1, s = [
+        [  "920/289",  "-59/68",              "-81031200387/366633756146",                  "-69462376031/733267512292",              "0",             "0",      "0",     "0"  ],
+        ["-1740/289",     "0",                  "6025413881/7482321554",                      "1612249989/7482321554",                "0",             "0",      "0",     "0"  ],
+        [ "1128/289",   "59/68",               "-6251815797/8526366422",                      "-639954015/17052732844",               "0",             "0",      "0",     "0"  ],
+        [ "-308/289",     "0",                  "1244724001/7482321554",                      "-752806667/7482321554",                "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                  "-148737261/10783345769",                      "148737261/10783345769",               "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                          "-3/833",                                      "3/833",                       "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                            "0",                                         "0",                           "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                            "0",                                         "0",                           "0",             "0",      "0",     "0"  ],
+    ]},
+    {c = 2, s = [
+        [   "12/17",      "0",                   "102125659/440136562",                         "27326271/440136562",                 "0",             "0",      "0",     "0"  ],
+        [  "-59/68",      "0",            "-156920047993625/159775733917868",            "-12001237118451/79887866958934",            "0",             "0",      "0",     "0"  ],
+        [    "2/17",      "0",               "1489556735319/1857857371138",                 "149729180391/1857857371138",             "0",             "0",      "0",     "0"  ],
+        [    "3/68",      "0",             "-13235456910147/159775733917868",              "3093263736297/79887866958934",            "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                 "67535018271/2349643145851",                 "-67535018271/2349643145851",             "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                         "441/181507",                                "-441/181507",                    "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                            "0",                                         "0",                           "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                            "0",                                         "0",                           "0",             "0",      "0",     "0"  ],
+    ]},
+    {c = 3, s = [
+        [  "-96/731",   "59/172",              "-6251815797/21566691538",                     "-639954015/43133383076",               "0",             "0",      "0",     "0"  ],
+        [  "118/731",     "0",              "87883847383821/79887866958934",               "8834021643069/79887866958934",            "0",             "0",      "0",     "0"  ],
+        [  "-16/731",  "-59/172",  "-1134866646907639536627/727679167377258785038",   "-13777050223300597/23487032885926596",   "-26254/557679",       "0",      "0",     "0"  ],
+        [   "-6/731",     "0",        "14509020271326561681/14850595252597118062",        "17220493277981/79887866958934",     "1500708/7993399",      "0",      "0",     "0"  ],
+        [     "0",        "0",        "-4841930283098652915/21402328452272317207",        "31597236232005/115132514146699",     "-26254/185893",       "0",      "0",     "0"  ],
+        [     "0",        "0",                 "-2318724711/1653303156799",                       "960119/1147305747",           "13564/23980197",     "0",      "0",     "0"  ],
+        [     "0",        "0",                            "0",                                         "0",                           "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                            "0",                                         "0",                           "0",             "0",      "0",     "0"  ],
+    ]},
+    {c = 4, s = [
+        [  "-36/833",     "0",                  "1244724001/21566691538",                    "-752806667/21566691538",                "0",             "0",      "0",     "0"  ],
+        [  "177/3332",    "0",            "-780891957698673/7829010961975532",            "3724542049827/79887866958934",             "0",             "0",      "0",     "0"  ],
+        [   "-6/833",     "0",        "14509020271326561681/16922771334354855466",        "2460070468283/13005001597966",      "1500708/9108757",      "0",      "0",     "0"  ],
+        [   "-9/3332",    "0",      "-217407431400324796377/207908333536359652868",   "-1950062198436997/3914505480987766",   "-7476412/9108757",    "-2/49",    "0",     "0"  ],
+        [     "0",        "0",         "4959271814984644613/21402328452272317207",       "47996144728947/115132514146699",     "4502124/9108757",     "8/49",    "0",     "0"  ],
+        [     "0",        "0",                 "-2258420001/1653303156799",                    "-1063649/8893843",             "1473580/9108757",    "-6/49",    "0",     "0"  ],
+        [     "0",        "0",                            "0",                                         "0",                           "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                            "0",                                         "0",                           "0",             "0",      "0",     "0"  ],
+    ]},
+    {c = 5, s = [
+        [     "0",        "0",                   "-49579087/10149031312",                       "49579087/10149031312",               "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",               "1328188692663/37594290333616",              "-1328188692663/37594290333616",            "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",        "-1613976761032884305/7963657098519931984",         "10532412077335/42840005263888",     "-564461/4461432",      "0",      "0",     "0"  ],
+        [     "0",        "0",         "4959271814984644613/20965546238960637264",        "15998714909649/37594290333616",      "375177/743572",      "1/6",     "0",     "0"  ],
+        [     "0",        "0",        "-8386761355510099813/128413970713633903242",    "-2224717261773437/2763180339520776",   "-280535/371786",     "-5/6",   "-1/24",   "0"  ],
+        [     "0",        "0",                 "13091810925/13226425254392",                    "35039615/213452232",          "1118749/2230716",     "1/2",    "1/6",    "0"  ],
+        [     "0",        "0",                            "0",                                          "0",                        "-1/8",           "1/6",   "-1/8",    "0"  ],
+        [     "0",        "0",                            "0",                                         "0",                           "0",             "0",      "0",     "0"  ],
+    ]},
+    {c = 6, s = [
+        [     "0",        "0",                          "-1/784",                                      "1/784",                       "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                        "8673/2904112",                              "-8673/2904112",                   "0",             "0",      "0",     "0"  ],
+        [     "0",        "0",                "-33235054191/26452850508784",                      "960119/1280713392",            "3391/6692148",      "0",      "0",     "0"  ],
+        [     "0",        "0",                  "-752806667/539854092016",                      "-1063649/8712336",             "368395/2230716",    "-1/8",     "0",     "0"  ],
+        [     "0",        "0",                 "13091810925/13226425254392",                    "35039615/213452232",          "1118749/2230716",     "1/2",    "1/6",    "0"  ],
+        [     "0",        "0",                  "-660204843/13226425254392",                    "-3290636/80044587",          "-5580181/6692148",    "-3/4",   "-5/6",  "-1/24"],
+        [     "0",        "0",                            "0",                                          "0",                         "1/6",           "1/2",    "1/2",   "1/6" ],
+        [     "0",        "0",                            "0",                                          "0",                          "0",           "-1/8",    "1/6",  "-1/8" ],
+    ]}
+]
+
+
+[[stencil_set]]
+
+order = 6
+
+H.inner = "1"
+H.closure = ["13649/43200", "12013/8640", "2711/4320", "5359/4320", "7877/8640", "43801/43200"]
+
+
+
+
+
 e.closure = ["1"]
-d1.closure = {s = ["11/6", "-3", "3/2", "-1/3"], c = 1}
+d1.closure = ["-25/12", "4", "-3", "4/3", "-1/4"]
+
+D2.positivity = {theta_M = "0.1878687080", theta_R = "0.3697", m_b = "7"}
--- a/src/SbpOperators/stencil.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/SbpOperators/stencil.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,18 +1,17 @@
-export CenteredStencil
-export CenteredNestedStencil
-
 struct Stencil{T,N}
     range::UnitRange{Int64}
     weights::NTuple{N,T}
 
-    function Stencil(range::UnitRange,weights::NTuple{N,T}) where {T, N}
+    function Stencil(range::UnitRange,weights::NTuple{N,Any}) where N
+        T = eltype(weights)
+
         @assert length(range) == N
         new{T,N}(range,weights)
     end
 end
 
 """
-    Stencil(weights::NTuple; center::Int)
+    Stencil(weights...; center::Int)
 
 Create a stencil with the given weights with element `center` as the center of the stencil.
 """
@@ -69,22 +68,29 @@
 end
 
 Base.@propagate_inbounds @inline function apply_stencil(s::Stencil, v::AbstractVector, i::Int)
-    w = zero(promote_type(eltype(s),eltype(v)))
-    @simd for k ∈ 1:length(s)
-        w += s.weights[k]*v[i + s.range[k]]
+    return sum(enumerate(s.weights)) do (k,w) #TBD: Which optimizations are needed here?
+        w*v[i + @inbounds s.range[k]]
     end
-
-    return w
 end
 
 Base.@propagate_inbounds @inline function apply_stencil_backwards(s::Stencil, v::AbstractVector, i::Int)
-    w = zero(promote_type(eltype(s),eltype(v)))
-    @simd for k ∈ length(s):-1:1
-        w += s.weights[k]*v[i - s.range[k]]
+    return sum(enumerate(s.weights)) do (k,w) #TBD: Which optimizations are needed here?
+        w*v[i - @inbounds s.range[k]]
     end
-    return w
 end
 
+# There are many options for the implementation of `apply_stencil` and
+# `apply_stencil_backwards`. Some alternatives were tried on the branch
+# bugfix/sbp_operators/stencil_return_type and can be found at the following
+# revision:
+#
+# * 237b980ffb91 (baseline)
+# * a72bab15228e (mapreduce)
+# * ffd735354d54 (multiplication)
+# * b5abd5191f2c (promote_op)
+# * 8d56846185fc (return_type)
+#
+
 function left_pad(s::Stencil, N)
     weights = LazyTensors.left_pad_tuple(s.weights, zero(eltype(s)), N)
     range = (first(s.range) - (N - length(s.weights))):last(s.range)
@@ -105,21 +111,24 @@
     s::Stencil{Stencil{T,N},M}
 end
 
+NestedStencil(;center) = NestedStencil(Stencil(;center))
+CenteredNestedStencil() = NestedStencil(CenteredStencil())
+
 # Stencil input
 NestedStencil(s::Vararg{Stencil}; center) = NestedStencil(Stencil(s... ; center))
 CenteredNestedStencil(s::Vararg{Stencil}) = NestedStencil(CenteredStencil(s...))
 
 # Tuple input
-function NestedStencil(weights::Vararg{NTuple{N,Any}}; center) where N
+function NestedStencil(weights::Vararg{NTuple{N,Any} where N}; center)
     inner_stencils = map(w -> Stencil(w...; center), weights)
     return NestedStencil(Stencil(inner_stencils... ; center))
 end
-function CenteredNestedStencil(weights::Vararg{NTuple{N,Any}}) where N
+
+function CenteredNestedStencil(weights::Vararg{NTuple{N,Any} where N})
     inner_stencils = map(w->CenteredStencil(w...), weights)
     return CenteredNestedStencil(inner_stencils...)
 end
 
-
 # Conversion
 function NestedStencil{T,N,M}(ns::NestedStencil{S,N,M}) where {T,S,N,M}
     return NestedStencil(Stencil{Stencil{T}}(ns.s))
@@ -132,7 +141,7 @@
 function Base.convert(::Type{NestedStencil{T,N,M}}, s::NestedStencil{S,N,M}) where {T,S,N,M}
     return NestedStencil{T,N,M}(s)
 end
-Base.convert(::Type{NestedStencil{T}}, stencil) where T = NestedStencil{T}(stencil)
+Base.convert(::Type{NestedStencil{T}}, stencil::NestedStencil) where T = NestedStencil{T}(stencil)
 
 function Base.promote_rule(::Type{NestedStencil{T,N,M}}, ::Type{NestedStencil{S,N,M}}) where {T,S,N,M}
     return NestedStencil{promote_type(T,S),N,M}
--- a/src/SbpOperators/stencil_set.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/SbpOperators/stencil_set.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -5,7 +5,7 @@
     StencilSet
 
 A `StencilSet` contains a set of associated stencils. The stencils
-are are stored in a table, and can be accesed by indexing into the `StencilSet`.
+are are stored in a table, and can be accessed by indexing into the `StencilSet`.
 """
 struct StencilSet
     table
@@ -21,7 +21,7 @@
 table of the `StencilSet` is a parsed TOML intended for functions like
 `parse_scalar` and `parse_stencil`.
 
-The `StencilSet` table is not parsed beyond the inital TOML parse. To get usable
+The `StencilSet` table is not parsed beyond the initial TOML parse. To get usable
 stencils use the `parse_stencil` functions on the fields of the stencil set.
 
 The reason for this is that since stencil sets are intended to be very
@@ -110,6 +110,33 @@
     end
 end
 
+
+"""
+    parse_nested_stencil(parsed_toml)
+
+Accept parsed TOML and read it as a nested tuple.
+
+See also [`read_stencil_set`](@ref), [`parse_stencil`](@ref).
+"""
+function parse_nested_stencil(parsed_toml)
+    if parsed_toml isa Array
+        weights = parse_stencil.(parsed_toml)
+        return CenteredNestedStencil(weights...)
+    end
+
+    center = parsed_toml["c"]
+    weights = parse_tuple.(parsed_toml["s"])
+    return NestedStencil(weights...; center)
+end
+
+"""
+    parse_nested_stencil(T, parsed_toml)
+
+Parse the input as a nested stencil with element type `T`.
+"""
+parse_nested_stencil(T, parsed_toml) = NestedStencil{T}(parse_nested_stencil(parsed_toml))
+
+
 """
     parse_scalar(parsed_toml)
 
--- a/src/SbpOperators/volumeops/constant_interior_scaling_operator.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/SbpOperators/volumeops/constant_interior_scaling_operator.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -19,7 +19,7 @@
     end
 end
 
-function ConstantInteriorScalingOperator(grid::EquidistantGrid, interior_weight, closure_weights)
+function ConstantInteriorScalingOperator(grid::EquidistantGrid, interior_weight::T, closure_weights::NTuple{N,T} where N) where T
     return ConstantInteriorScalingOperator(interior_weight, Tuple(closure_weights), size(grid)[1])
 end
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/SbpOperators/volumeops/derivatives/second_derivative_variable.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,188 @@
+"""
+    second_derivative_variable(g, coeff ..., [direction])
+
+The variable second derivative operator as a `LazyTensor` on the given grid.
+`coeff` is a grid function of the variable coefficient.
+
+Approximates the d/dξ c d/dξ on `g` along the coordinate dimension specified
+by `direction`.
+"""
+function second_derivative_variable end
+
+function second_derivative_variable(g::TensorGrid, coeff, stencil_set, dir::Int)
+    inner_stencil    = parse_nested_stencil(eltype(coeff), stencil_set["D2variable"]["inner_stencil"])
+    closure_stencils = parse_nested_stencil.(eltype(coeff), stencil_set["D2variable"]["closure_stencils"])
+
+    return second_derivative_variable(g, coeff, inner_stencil, closure_stencils, dir)
+end
+
+function second_derivative_variable(g::EquidistantGrid, coeff, stencil_set)
+    return second_derivative_variable(TensorGrid(g), coeff, stencil_set, 1)
+end
+
+function second_derivative_variable(g::TensorGrid, coeff, inner_stencil::NestedStencil, closure_stencils, dir)
+    check_coefficient(g, coeff)
+
+    Δxᵢ = spacing(g.grids[dir])
+    scaled_inner_stencil = scale(inner_stencil, 1/Δxᵢ^2)
+    scaled_closure_stencils = scale.(Tuple(closure_stencils), 1/Δxᵢ^2)
+    return SecondDerivativeVariable(coeff, scaled_inner_stencil, scaled_closure_stencils, dir)
+end
+
+function check_coefficient(g, coeff)
+    if ndims(g) != ndims(coeff)
+        throw(ArgumentError("The coefficient has dimension $(ndims(coeff)) while the grid is dimension $(ndims(g))"))
+    end
+
+    if size(g) != size(coeff)
+        throw(DimensionMismatch("the size $(size(coeff)) of the coefficient does not match the size $(size(g)) of the grid"))
+    end
+end
+
+
+"""
+    SecondDerivativeVariable{Dir,T,D,...} <: LazyTensor{T,D,D}
+
+A second derivative operator in direction `Dir` with a variable coefficient.
+"""
+struct SecondDerivativeVariable{Dir,T,D,M,IStencil<:NestedStencil{T},CStencil<:NestedStencil{T},TArray<:AbstractArray} <: LazyTensor{T,D,D}
+    inner_stencil::IStencil
+    closure_stencils::NTuple{M,CStencil}
+    coefficient::TArray
+
+    function SecondDerivativeVariable(coefficient::AbstractArray, inner_stencil::NestedStencil{T}, closure_stencils::NTuple{M,NestedStencil{T}}, dir) where {T,M}
+        D = ndims(coefficient)
+        IStencil = typeof(inner_stencil)
+        CStencil = eltype(closure_stencils)
+        TArray = typeof(coefficient)
+        return new{dir,T,D,M,IStencil,CStencil,TArray}(inner_stencil, closure_stencils, coefficient)
+    end
+end
+
+derivative_direction(::SecondDerivativeVariable{Dir}) where {Dir} = Dir
+
+closure_size(op::SecondDerivativeVariable) = length(op.closure_stencils)
+
+LazyTensors.range_size(op::SecondDerivativeVariable) = size(op.coefficient)
+LazyTensors.domain_size(op::SecondDerivativeVariable) = size(op.coefficient)
+
+
+function derivative_view(op, a, I)
+    d = derivative_direction(op)
+
+    Iview = Base.setindex(I,:,d)
+    return @view a[Iview...]
+end
+
+function apply_lower(op::SecondDerivativeVariable, v, I...)
+    ṽ = derivative_view(op, v, I)
+    c̃ = derivative_view(op, op.coefficient, I)
+
+    i = I[derivative_direction(op)]
+    return @inbounds apply_stencil(op.closure_stencils[i], c̃, ṽ, i)
+end
+
+function apply_interior(op::SecondDerivativeVariable, v, I...)
+    ṽ = derivative_view(op, v, I)
+    c̃ = derivative_view(op, op.coefficient, I)
+
+    i = I[derivative_direction(op)]
+    return apply_stencil(op.inner_stencil, c̃, ṽ, i)
+end
+
+function apply_upper(op::SecondDerivativeVariable, v, I...)
+    ṽ = derivative_view(op, v, I)
+    c̃ = derivative_view(op, op.coefficient, I)
+
+    i = I[derivative_direction(op)]
+    sz = domain_size(op)[derivative_direction(op)]
+    stencil = op.closure_stencils[sz-i+1]
+    return @inbounds apply_stencil_backwards(stencil, c̃, ṽ, i)
+end
+
+function LazyTensors.apply(op::SecondDerivativeVariable, v::AbstractArray, I::Vararg{Index})
+    if I[derivative_direction(op)] isa Index{Lower}
+        return apply_lower(op, v, Int.(I)...)
+    elseif I[derivative_direction(op)] isa Index{Upper}
+        return apply_upper(op, v, Int.(I)...)
+    elseif I[derivative_direction(op)] isa Index{Interior}
+        return apply_interior(op, v, Int.(I)...)
+    else
+        error("Invalid region")
+    end
+end
+
+function LazyTensors.apply(op::SecondDerivativeVariable, v::AbstractArray, I...)
+    dir = derivative_direction(op)
+    sz = domain_size(op)[dir]
+
+    i = I[dir]
+
+    I = map(i->Index(i, Interior), I)
+    if 0 < i <= closure_size(op)
+        I = Base.setindex(I, Index(i, Lower), dir)
+        return LazyTensors.apply(op, v, I...)
+    elseif closure_size(op) < i <= sz-closure_size(op)
+        I = Base.setindex(I, Index(i, Interior), dir)
+        return LazyTensors.apply(op, v, I...)
+    elseif sz-closure_size(op) < i <= sz
+        I = Base.setindex(I, Index(i, Upper), dir)
+        return LazyTensors.apply(op, v, I...)
+    else
+        error("Bounds error") # This should be `throw(BoundsError())` but the type inference is so fragile that it doesn't work. Needs investigation. / Jonatan 2023-06-08
+    end
+end
+
+
+# 2D Specific implementations to avoid type instability
+# TBD: Can this be solved by fixing the general methods instead?
+
+
+## x-direction
+function apply_lower(op::SecondDerivativeVariable{1}, v, i, j)
+    ṽ = @view v[:,j]
+    c̃ = @view op.coefficient[:,j]
+
+    return @inbounds apply_stencil(op.closure_stencils[i], c̃, ṽ, i)
+end
+
+function apply_interior(op::SecondDerivativeVariable{1}, v, i, j)
+    ṽ = @view v[:,j]
+    c̃ = @view op.coefficient[:,j]
+
+    return @inbounds apply_stencil(op.inner_stencil, c̃, ṽ, i)
+end
+
+function apply_upper(op::SecondDerivativeVariable{1}, v, i, j)
+    ṽ = @view v[:,j]
+    c̃ = @view op.coefficient[:,j]
+
+    sz = domain_size(op)[derivative_direction(op)]
+    stencil = op.closure_stencils[sz-i+1]
+    return @inbounds apply_stencil_backwards(stencil, c̃, ṽ, i)
+end
+
+
+## y-direction
+function apply_lower(op::SecondDerivativeVariable{2}, v, i, j)
+    ṽ = @view v[i,:]
+    c̃ = @view op.coefficient[i,:]
+
+    return @inbounds apply_stencil(op.closure_stencils[j], c̃, ṽ, j)
+end
+
+function apply_interior(op::SecondDerivativeVariable{2}, v, i, j)
+    ṽ = @view v[i,:]
+    c̃ = @view op.coefficient[i,:]
+
+    return @inbounds apply_stencil(op.inner_stencil, c̃, ṽ, j)
+end
+
+function apply_upper(op::SecondDerivativeVariable{2}, v, i, j)
+    ṽ = @view v[i,:]
+    c̃ = @view op.coefficient[i,:]
+
+    sz = domain_size(op)[derivative_direction(op)]
+    stencil = op.closure_stencils[sz-j+1]
+    return @inbounds apply_stencil_backwards(stencil, c̃, ṽ, j)
+end
--- a/src/SbpOperators/volumeops/laplace/laplace.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/SbpOperators/volumeops/laplace/laplace.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -52,3 +52,76 @@
     return Δ
 end
 laplace(g::EquidistantGrid, stencil_set) = second_derivative(g, stencil_set)
+
+"""
+    sat_tensors(Δ::Laplace, g::Grid, bc::DirichletCondition; H_tuning, R_tuning)
+
+The operators required to construct the SAT for imposing a Dirichlet
+condition. `H_tuning` and `R_tuning` are used to specify the strength of the
+penalty.
+
+See also: [`sat`](@ref), [`DirichletCondition`](@ref), [`positivity_decomposition`](@ref).
+"""
+function sat_tensors(Δ::Laplace, g::Grid, bc::DirichletCondition; H_tuning = 1., R_tuning = 1.)
+    id = boundary(bc)
+    set  = Δ.stencil_set
+    H⁻¹ = inverse_inner_product(g,set)
+    Hᵧ = inner_product(boundary_grid(g, id), set)
+    e = boundary_restriction(g, set, id)
+    d = normal_derivative(g, set, id)
+    B = positivity_decomposition(Δ, g, boundary(bc); H_tuning, R_tuning)
+    penalty_tensor = H⁻¹∘(d' - B*e')∘Hᵧ
+    return penalty_tensor, e
+end
+
+"""
+    sat_tensors(Δ::Laplace, g::Grid, bc::NeumannCondition)
+
+The operators required to construct the SAT for imposing a Neumann condition.
+
+See also: [`sat`](@ref), [`NeumannCondition`](@ref).
+"""
+function sat_tensors(Δ::Laplace, g::Grid, bc::NeumannCondition)
+    id = boundary(bc)
+    set  = Δ.stencil_set
+    H⁻¹ = inverse_inner_product(g,set)
+    Hᵧ = inner_product(boundary_grid(g, id), set)
+    e = boundary_restriction(g, set, id)
+    d = normal_derivative(g, set, id)
+
+    penalty_tensor = -H⁻¹∘e'∘Hᵧ
+    return penalty_tensor, d
+end
+
+"""
+    positivity_decomposition(Δ::Laplace, g::Grid, b::BoundaryIdentifier; H_tuning, R_tuning)
+
+Constructs the scalar `B` such that `d' - 1/2*B*e'` is symmetric positive
+definite with respect to the boundary quadrature. Here `d` is the normal
+derivative and `e` is the boundary restriction operator. `B` can then be used
+to form a symmetric and energy stable penalty for a Dirichlet condition. The
+parameters `H_tuning` and `R_tuning` are used to specify the strength of the
+penalty and must be greater than 1. For details we refer to
+<https://doi.org/10.1016/j.jcp.2020.109294>
+"""
+function positivity_decomposition(Δ::Laplace, g::Grid, b::BoundaryIdentifier; H_tuning, R_tuning)
+    @assert(H_tuning ≥ 1.)
+    @assert(R_tuning ≥ 1.)
+    Nτ_H, τ_R = positivity_limits(Δ,g,b)
+    return H_tuning*Nτ_H + R_tuning*τ_R
+end
+
+function positivity_limits(Δ::Laplace, g::EquidistantGrid, b::BoundaryIdentifier)
+    h = spacing(g)
+    θ_H = parse_scalar(Δ.stencil_set["H"]["closure"][1])
+    θ_R = parse_scalar(Δ.stencil_set["D2"]["positivity"]["theta_R"])
+
+    τ_H = one(eltype(Δ))/(h*θ_H)
+    τ_R = one(eltype(Δ))/(h*θ_R)
+    return τ_H, τ_R
+end
+
+function positivity_limits(Δ::Laplace, g::TensorGrid, b::BoundaryIdentifier)
+    τ_H, τ_R = positivity_limits(Δ, g.grids[grid_id(b)], b)
+    return τ_H*ndims(g), τ_R
+end
--- a/src/SbpOperators/volumeops/volume_operator.jl	Wed May 31 08:59:34 2023 +0200
+++ b/src/SbpOperators/volumeops/volume_operator.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -6,18 +6,23 @@
 struct VolumeOperator{T,N,M,K} <: LazyTensor{T,1,1}
     inner_stencil::Stencil{T,N}
     closure_stencils::NTuple{M,Stencil{T,K}}
-    size::NTuple{1,Int}
+    size::Int
     parity::Parity
+
+    function VolumeOperator(inner_stencil::Stencil{T,N}, closure_stencils::Tuple{Stencil{T,K}, Vararg{Stencil{T,K}}}, size::Int, parity::Parity) where {T,N,K}
+        M = length(closure_stencils)
+        return new{T,N,M,K}(inner_stencil, closure_stencils, size, parity)
+    end
 end
 
 function VolumeOperator(grid::EquidistantGrid, inner_stencil, closure_stencils, parity)
-    return VolumeOperator(inner_stencil, Tuple(closure_stencils), size(grid), parity)
-end
+    return VolumeOperator(inner_stencil, Tuple(closure_stencils), size(grid,1), parity)
+end # TBD: Remove this function?
 
 closure_size(::VolumeOperator{T,N,M}) where {T,N,M} = M
 
-LazyTensors.range_size(op::VolumeOperator) = op.size
-LazyTensors.domain_size(op::VolumeOperator) = op.size
+LazyTensors.range_size(op::VolumeOperator) = (op.size,)
+LazyTensors.domain_size(op::VolumeOperator) = (op.size,)
 
 function LazyTensors.apply(op::VolumeOperator, v::AbstractVector, i::Index{Lower})
     return @inbounds apply_stencil(op.closure_stencils[Int(i)], v, Int(i))
@@ -28,11 +33,11 @@
 end
 
 function LazyTensors.apply(op::VolumeOperator, v::AbstractVector, i::Index{Upper})
-    return @inbounds Int(op.parity)*apply_stencil_backwards(op.closure_stencils[op.size[1]-Int(i)+1], v, Int(i))
+    return @inbounds Int(op.parity)*apply_stencil_backwards(op.closure_stencils[op.size-Int(i)+1], v, Int(i))
 end
 
 function LazyTensors.apply(op::VolumeOperator, v::AbstractVector, i)
-    r = getregion(i, closure_size(op), op.size[1])
+    r = getregion(i, closure_size(op), op.size)
     return LazyTensors.apply(op, v, Index(i, r))
 end
 # TODO: Move this to LazyTensors when we have the region communication down.
--- a/src/Sbplib.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-module Sbplib
-
-include("StaticDicts/StaticDicts.jl")
-include("RegionIndices/RegionIndices.jl")
-include("LazyTensors/LazyTensors.jl")
-include("Grids/Grids.jl")
-include("SbpOperators/SbpOperators.jl")
-include("DiffOps/DiffOps.jl")
-
-export RegionIndices
-export LazyTensors
-export Grids
-export SbpOperators
-export DiffOps
-
-end
--- a/src/StaticDicts/StaticDicts.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-module StaticDicts
-
-export StaticDict
-
-"""
-    StaticDict{K,V,N} <: AbstractDict{K,V}
-
-A static dictionary implementing the interface for an `AbstractDict`. A
-`StaticDict` is fully immutable and after creation no changes can be made.
-
-The immutable nature means that `StaticDict` can be compared with `===`, in
-constrast to regular `Dict` or `ImmutableDict` which can not. (See
-<https://github.com/JuliaLang/julia/issues/4648> for details.) One important
-aspect of this is that `StaticDict` can be used in a struct while still
-allowing the struct to be compared using the default implementation of `==` for
-structs.
-
-Lookups are done by linear search.
-
-Duplicate keys are not allowed and an error will be thrown if they are passed
-to the constructor.
-"""
-struct StaticDict{K,V,N} <: AbstractDict{K,V}
-    pairs::NTuple{N,Pair{K,V}}
-
-    function StaticDict{K,V}(pairs::Vararg{Pair,N}) where {K,V,N}
-        if !allunique(first.(pairs))
-            throw(DomainError(pairs, "keys must be unique"))
-        end
-        return new{K,V,N}(pairs)
-    end
-end
-
-function StaticDict(pairs::Vararg{Pair})
-    K = typejoin(firsttype.(pairs)...)
-    V = typejoin(secondtype.(pairs)...)
-    return StaticDict{K,V}(pairs...)
-end
-
-StaticDict(pairs::NTuple{N,Pair} where N) = StaticDict(pairs...)
-
-function Base.get(d::StaticDict, key, default)
-    for p ∈ d.pairs
-        if key == p.first
-            return p.second
-        end
-    end
-
-    return default
-end
-
-Base.iterate(d::StaticDict) = iterate(d.pairs)
-Base.iterate(d::StaticDict, state) = iterate(d.pairs,state)
-Base.length(d::StaticDict) = length(d.pairs)
-
-
-"""
-    merge(d1::StaticDict, d2::StaticDict)
-
-Merge two `StaticDict`. Repeating keys is considered and error. This may
-change in a future version.
-"""
-function Base.merge(d1::StaticDict, d2::StaticDict)
-    return StaticDict(d1.pairs..., d2.pairs...)
-end
-
-
-"""
-    firsttype(::Pair{T1,T2})
-
-The type of the first element in the pair.
-"""
-firsttype(::Pair{T1,T2}) where {T1,T2} = T1
-
-"""
-    secondtype(::Pair{T1,T2})
-
-The type of the secondtype element in the pair.
-"""
-secondtype(::Pair{T1,T2}) where {T1,T2}  = T2
-
-end # module
--- a/test/DiffOps/DiffOps_test.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,195 +0,0 @@
-using Test
-using Sbplib.DiffOps
-using Sbplib.Grids
-using Sbplib.SbpOperators
-using Sbplib.RegionIndices
-using Sbplib.LazyTensors
-
-#
-# @testset "BoundaryValue" begin
-#     op = read_D2_operator(sbp_operators_path()*"standard_diagonal.toml"; order=4)
-#     g = EquidistantGrid((4,5), (0.0, 0.0), (1.0,1.0))
-#
-#     e_w = BoundaryValue(op, g, CartesianBoundary{1,Lower}())
-#     e_e = BoundaryValue(op, g, CartesianBoundary{1,Upper}())
-#     e_s = BoundaryValue(op, g, CartesianBoundary{2,Lower}())
-#     e_n = BoundaryValue(op, g, CartesianBoundary{2,Upper}())
-#
-#     v = zeros(Float64, 4, 5)
-#     v[:,5] = [1, 2, 3,4]
-#     v[:,4] = [1, 2, 3,4]
-#     v[:,3] = [4, 5, 6, 7]
-#     v[:,2] = [7, 8, 9, 10]
-#     v[:,1] = [10, 11, 12, 13]
-#
-#     @test e_w  isa LazyTensor{T,2,1} where T
-#     @test e_w' isa LazyTensor{T,1,2} where T
-#
-#     @test domain_size(e_w, (3,2)) == (2,)
-#     @test domain_size(e_e, (3,2)) == (2,)
-#     @test domain_size(e_s, (3,2)) == (3,)
-#     @test domain_size(e_n, (3,2)) == (3,)
-#
-#     @test size(e_w'*v) == (5,)
-#     @test size(e_e'*v) == (5,)
-#     @test size(e_s'*v) == (4,)
-#     @test size(e_n'*v) == (4,)
-#
-#     @test collect(e_w'*v) == [10,7,4,1.0,1]
-#     @test collect(e_e'*v) == [13,10,7,4,4.0]
-#     @test collect(e_s'*v) == [10,11,12,13.0]
-#     @test collect(e_n'*v) == [1,2,3,4.0]
-#
-#     g_x = [1,2,3,4.0]
-#     g_y = [5,4,3,2,1.0]
-#
-#     G_w = zeros(Float64, (4,5))
-#     G_w[1,:] = g_y
-#
-#     G_e = zeros(Float64, (4,5))
-#     G_e[4,:] = g_y
-#
-#     G_s = zeros(Float64, (4,5))
-#     G_s[:,1] = g_x
-#
-#     G_n = zeros(Float64, (4,5))
-#     G_n[:,5] = g_x
-#
-#     @test size(e_w*g_y) == (UnknownDim,5)
-#     @test size(e_e*g_y) == (UnknownDim,5)
-#     @test size(e_s*g_x) == (4,UnknownDim)
-#     @test size(e_n*g_x) == (4,UnknownDim)
-#
-#     # These tests should be moved to where they are possible (i.e we know what the grid should be)
-#     @test_broken collect(e_w*g_y) == G_w
-#     @test_broken collect(e_e*g_y) == G_e
-#     @test_broken collect(e_s*g_x) == G_s
-#     @test_broken collect(e_n*g_x) == G_n
-# end
-#
-# @testset "NormalDerivative" begin
-#     op = read_D2_operator(sbp_operators_path()*"standard_diagonal.toml"; order=4)
-#     g = EquidistantGrid((5,6), (0.0, 0.0), (4.0,5.0))
-#
-#     d_w = NormalDerivative(op, g, CartesianBoundary{1,Lower}())
-#     d_e = NormalDerivative(op, g, CartesianBoundary{1,Upper}())
-#     d_s = NormalDerivative(op, g, CartesianBoundary{2,Lower}())
-#     d_n = NormalDerivative(op, g, CartesianBoundary{2,Upper}())
-#
-#
-#     v = evalOn(g, (x,y)-> x^2 + (y-1)^2 + x*y)
-#     v∂x = evalOn(g, (x,y)-> 2*x + y)
-#     v∂y = evalOn(g, (x,y)-> 2*(y-1) + x)
-#
-#     @test d_w  isa LazyTensor{T,2,1} where T
-#     @test d_w' isa LazyTensor{T,1,2} where T
-#
-#     @test domain_size(d_w, (3,2)) == (2,)
-#     @test domain_size(d_e, (3,2)) == (2,)
-#     @test domain_size(d_s, (3,2)) == (3,)
-#     @test domain_size(d_n, (3,2)) == (3,)
-#
-#     @test size(d_w'*v) == (6,)
-#     @test size(d_e'*v) == (6,)
-#     @test size(d_s'*v) == (5,)
-#     @test size(d_n'*v) == (5,)
-#
-#     @test collect(d_w'*v) ≈ v∂x[1,:]
-#     @test collect(d_e'*v) ≈ v∂x[5,:]
-#     @test collect(d_s'*v) ≈ v∂y[:,1]
-#     @test collect(d_n'*v) ≈ v∂y[:,6]
-#
-#
-#     d_x_l = zeros(Float64, 5)
-#     d_x_u = zeros(Float64, 5)
-#     for i ∈ eachindex(d_x_l)
-#         d_x_l[i] = op.dClosure[i-1]
-#         d_x_u[i] = -op.dClosure[length(d_x_u)-i]
-#     end
-#
-#     d_y_l = zeros(Float64, 6)
-#     d_y_u = zeros(Float64, 6)
-#     for i ∈ eachindex(d_y_l)
-#         d_y_l[i] = op.dClosure[i-1]
-#         d_y_u[i] = -op.dClosure[length(d_y_u)-i]
-#     end
-#
-#     function prod_matrix(x,y)
-#         G = zeros(Float64, length(x), length(y))
-#         for I ∈ CartesianIndices(G)
-#             G[I] = x[I[1]]*y[I[2]]
-#         end
-#
-#         return G
-#     end
-#
-#     g_x = [1,2,3,4.0,5]
-#     g_y = [5,4,3,2,1.0,11]
-#
-#     G_w = prod_matrix(d_x_l, g_y)
-#     G_e = prod_matrix(d_x_u, g_y)
-#     G_s = prod_matrix(g_x, d_y_l)
-#     G_n = prod_matrix(g_x, d_y_u)
-#
-#
-#     @test size(d_w*g_y) == (UnknownDim,6)
-#     @test size(d_e*g_y) == (UnknownDim,6)
-#     @test size(d_s*g_x) == (5,UnknownDim)
-#     @test size(d_n*g_x) == (5,UnknownDim)
-#
-#     # These tests should be moved to where they are possible (i.e we know what the grid should be)
-#     @test_broken collect(d_w*g_y) ≈ G_w
-#     @test_broken collect(d_e*g_y) ≈ G_e
-#     @test_broken collect(d_s*g_x) ≈ G_s
-#     @test_broken collect(d_n*g_x) ≈ G_n
-# end
-#
-# @testset "BoundaryQuadrature" begin
-#     op = read_D2_operator(sbp_operators_path()*"standard_diagonal.toml"; order=4)
-#     g = EquidistantGrid((10,11), (0.0, 0.0), (1.0,1.0))
-#
-#     H_w = BoundaryQuadrature(op, g, CartesianBoundary{1,Lower}())
-#     H_e = BoundaryQuadrature(op, g, CartesianBoundary{1,Upper}())
-#     H_s = BoundaryQuadrature(op, g, CartesianBoundary{2,Lower}())
-#     H_n = BoundaryQuadrature(op, g, CartesianBoundary{2,Upper}())
-#
-#     v = evalOn(g, (x,y)-> x^2 + (y-1)^2 + x*y)
-#
-#     function get_quadrature(N)
-#         qc = op.quadratureClosure
-#         q = (qc..., ones(N-2*closuresize(op))..., reverse(qc)...)
-#         @assert length(q) == N
-#         return q
-#     end
-#
-#     v_w = v[1,:]
-#     v_e = v[10,:]
-#     v_s = v[:,1]
-#     v_n = v[:,11]
-#
-#     q_x = spacing(g)[1].*get_quadrature(10)
-#     q_y = spacing(g)[2].*get_quadrature(11)
-#
-#     @test H_w isa TensorOperator{T,1} where T
-#
-#     @test domain_size(H_w, (3,)) == (3,)
-#     @test domain_size(H_n, (3,)) == (3,)
-#
-#     @test range_size(H_w, (3,)) == (3,)
-#     @test range_size(H_n, (3,)) == (3,)
-#
-#     @test size(H_w*v_w) == (11,)
-#     @test size(H_e*v_e) == (11,)
-#     @test size(H_s*v_s) == (10,)
-#     @test size(H_n*v_n) == (10,)
-#
-#     @test collect(H_w*v_w) ≈ q_y.*v_w
-#     @test collect(H_e*v_e) ≈ q_y.*v_e
-#     @test collect(H_s*v_s) ≈ q_x.*v_s
-#     @test collect(H_n*v_n) ≈ q_x.*v_n
-#
-#     @test collect(H_w'*v_w) == collect(H_w'*v_w)
-#     @test collect(H_e'*v_e) == collect(H_e'*v_e)
-#     @test collect(H_s'*v_s) == collect(H_s'*v_s)
-#     @test collect(H_n'*v_n) == collect(H_n'*v_n)
-# end
--- a/test/Grids/equidistant_grid_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/Grids/equidistant_grid_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,7 +1,6 @@
-using Sbplib.Grids
+using Diffinitive.Grids
 using Test
-using Sbplib.RegionIndices
-using Sbplib.LazyTensors
+using Diffinitive.LazyTensors
 
 
 @testset "EquidistantGrid" begin
@@ -19,6 +18,9 @@
         @test g[end] == 10.0
 
         @test all(eachindex(g) .== 1:101)
+
+        @test firstindex(g) == 1
+        @test lastindex(g) == 101
     end
 
     @testset "Iterator interface" begin
@@ -28,6 +30,8 @@
         @test size(EquidistantGrid(0:10)) == (11,)
         @test size(EquidistantGrid(0:0.1:10)) == (101,)
 
+        @test size(EquidistantGrid(0:0.1:10),1) == 101
+
         @test collect(EquidistantGrid(0:0.1:0.5)) == [0.0, 0.1, 0.2, 0.3, 0.4, 0.5]
 
         @test Base.IteratorSize(EquidistantGrid{Float64, StepRange{Float64}}) == Base.HasShape{1}()
@@ -35,6 +39,10 @@
 
     @testset "Base" begin
         @test ndims(EquidistantGrid(0:10)) == 1
+
+        g = EquidistantGrid(0:0.1:10)
+        @test axes(g,1) == 1:101
+        @test axes(g) == (1:101,)
     end
 
     @testset "spacing" begin
@@ -47,16 +55,32 @@
         @test inverse_spacing(EquidistantGrid(0:0.1:10)) == 10
     end
 
+    @testset "min_spacing" begin
+        @test min_spacing(EquidistantGrid(0:10)) == 1
+        @test min_spacing(EquidistantGrid(0:0.1:10)) == 0.1
+    end
+
     @testset "boundary_identifiers" begin
         g = EquidistantGrid(0:0.1:10)
-        @test boundary_identifiers(g) == (Lower(), Upper())
+        @test boundary_identifiers(g) == (LowerBoundary(), UpperBoundary())
         @inferred boundary_identifiers(g)
     end
 
     @testset "boundary_grid" begin
         g = EquidistantGrid(0:0.1:1)
-        @test boundary_grid(g, Lower()) == ZeroDimGrid(0.0)
-        @test boundary_grid(g, Upper()) == ZeroDimGrid(1.0)
+        @test boundary_grid(g, LowerBoundary()) == ZeroDimGrid(0.0)
+        @test boundary_grid(g, UpperBoundary()) == ZeroDimGrid(1.0)
+    end
+
+    @testset "boundary_indices" begin
+        g = EquidistantGrid(0:0.1:1)
+        @test boundary_indices(g, LowerBoundary()) == (1,)
+        @test boundary_indices(g, UpperBoundary()) == (11,)
+
+        g = EquidistantGrid(2:0.1:10)
+        @test boundary_indices(g, LowerBoundary()) == (1,)
+        @test boundary_indices(g, UpperBoundary()) == (81,)
+
     end
 
     @testset "refine" begin
@@ -85,29 +109,37 @@
 
 
 @testset "equidistant_grid" begin
-    @test equidistant_grid(4,0.0,1.0) isa EquidistantGrid
-    @test equidistant_grid((4,3),(0.0,0.0),(8.0,5.0)) isa TensorGrid
+    @test equidistant_grid(0.0,1.0, 4) isa EquidistantGrid
+    @test equidistant_grid((0.0,0.0),(8.0,5.0), 4, 3) isa TensorGrid
+    @test equidistant_grid((0.0,),(8.0,), 4) isa TensorGrid
 
     # constuctor
-    @test_throws DomainError equidistant_grid(0,0.0,1.0)
-    @test_throws DomainError equidistant_grid(1,1.0,1.0)
-    @test_throws DomainError equidistant_grid(1,1.0,-1.0)
+    @test_throws DomainError equidistant_grid(0.0, 1.0, 0)
+    @test_throws DomainError equidistant_grid(1.0, 1.0, 1)
+    @test_throws DomainError equidistant_grid(1.0, -1.0, 1)
 
-    @test_throws DomainError equidistant_grid((0,0),(0.0,0.0),(1.0,1.0))
-    @test_throws DomainError equidistant_grid((1,1),(1.0,1.0),(1.0,1.0))
-    @test_throws DomainError equidistant_grid((1,1),(1.0,1.0),(-1.0,-1.0))
+    @test_throws DomainError equidistant_grid((0.0,0.0),(1.0,1.0), 0, 0)
+    @test_throws DomainError equidistant_grid((1.0,1.0),(1.0,1.0), 1, 1)
+    @test_throws DomainError equidistant_grid((1.0,1.0),(-1.0,-1.0), 1, 1)
+
+    @test_throws ArgumentError equidistant_grid((0.0,),(8.0,5.0), 4, 3, 4)
 
     @testset "Base" begin
-        @test eltype(equidistant_grid(4,0.0,1.0)) == Float64
-        @test eltype(equidistant_grid((4,3),(0,0),(1,3))) <: AbstractVector{Float64}
-        @test size(equidistant_grid(4,0.0,1.0)) == (4,)
-        @test size(equidistant_grid((5,3), (0.0,0.0), (2.0,1.0))) == (5,3)
-        @test ndims(equidistant_grid(4,0.0,1.0)) == 1
-        @test ndims(equidistant_grid((5,3), (0.0,0.0), (2.0,1.0))) == 2
+        @test eltype(equidistant_grid(0.0, 1.0, 4)) == Float64
+        @test eltype(equidistant_grid((0,0),(1,3), 4, 3)) <: AbstractVector{Float64}
+
+        @test size(equidistant_grid(0.0, 1.0, 4)) == (4,)
+        @test size(equidistant_grid((0.0,0.0), (2.0,1.0), 5, 3)) == (5,3)
+
+        @test size(equidistant_grid((0.0,0.0), (2.0,1.0), 5, 3), 1) == 5
+        @test size(equidistant_grid((0.0,0.0), (2.0,1.0), 5, 3), 2) == 3
+
+        @test ndims(equidistant_grid(0.0, 1.0, 4)) == 1
+        @test ndims(equidistant_grid((0.0,0.0), (2.0,1.0), 5, 3)) == 2
     end
 
     @testset "getindex" begin
-        g = equidistant_grid((5,3), (-1.0,0.0), (0.0,7.11))
+        g = equidistant_grid((-1.0,0.0), (0.0,7.11), 5, 3)
         gp = collect(g);
         p = [(-1.,0.)      (-1.,7.11/2)   (-1.,7.11);
             (-0.75,0.)    (-0.75,7.11/2) (-0.75,7.11);
--- a/test/Grids/grid_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/Grids/grid_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,6 +1,6 @@
 using Test
-using Sbplib.Grids
-using Sbplib.LazyTensors
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
 using StaticArrays
 
 @testset "Grid" begin
@@ -15,19 +15,21 @@
     @test coordinate_size(DummyGrid{SVector{3,Float64}, 2}()) == 3
 
     @test coordinate_size(DummyGrid{SVector{3,Float64}, 2}) == 3
+end
 
-    @testset "component_type" begin
-        @test component_type(DummyGrid{Int,1}()) == Int
-        @test component_type(DummyGrid{Float64,1}()) == Float64
-        @test component_type(DummyGrid{Rational,1}()) == Rational
+@testset "component_type" begin
+    @test component_type(DummyGrid{Int,1}()) == Int
+    @test component_type(DummyGrid{Float64,1}()) == Float64
+    @test component_type(DummyGrid{Rational,1}()) == Rational
 
-        @test component_type(DummyGrid{SVector{3,Int},2}()) == Int
-        @test component_type(DummyGrid{SVector{2,Float64},3}()) == Float64
-        @test component_type(DummyGrid{SVector{4,Rational},4}()) == Rational
+    @test component_type(DummyGrid{SVector{3,Int},2}()) == Int
+    @test component_type(DummyGrid{SVector{2,Float64},3}()) == Float64
+    @test component_type(DummyGrid{SVector{4,Rational},4}()) == Rational
 
-        @test component_type(DummyGrid{Float64,1}) == Float64
-        @test component_type(DummyGrid{SVector{2,Float64},3}) == Float64
-    end
+    @test component_type(DummyGrid{Float64,1}) == Float64
+    @test component_type(DummyGrid{SVector{2,Float64},3}) == Float64
+
+    @test component_type(fill(@SVector[1,2], 4,2)) == Int
 end
 
 @testset "eval_on" begin
@@ -38,17 +40,22 @@
     @test eval_on(ZeroDimGrid(1.), x̄->2x̄) isa LazyArray
     @test eval_on(ZeroDimGrid(1.), x̄->2x̄) == fill(2.)
 
+    @test eval_on(ZeroDimGrid(@SVector[1.,2.]), π) isa LazyArray
+    @test eval_on(ZeroDimGrid(@SVector[1.,2.]), π) == fill(π)
+
     @test eval_on(EquidistantGrid(range(0,1,length=4)), x->2x) isa LazyArray
     @test eval_on(EquidistantGrid(range(0,1,length=4)), x->2x) == 2 .* range(0,1,length=4)
 
 
-    g = equidistant_grid((5,3), (0.0,0.0), (2.0,1.0))
+    g = equidistant_grid((0.0,0.0), (2.0,1.0), 5, 3)
 
     @test eval_on(g, x̄ -> 0.) isa LazyArray
     @test eval_on(g, x̄ -> 0.) == fill(0., (5,3))
 
     @test eval_on(g, x̄ -> sin(x̄[1])*cos(x̄[2])) == map(x̄->sin(x̄[1])*cos(x̄[2]), g)
 
+    @test eval_on(g, π) == fill(π, (5,3))
+
     # Vector valued function
     @test eval_on(g, x̄ -> @SVector[x̄[2], x̄[1]]) isa LazyArray{SVector{2,Float64}}
     @test eval_on(g, x̄ -> @SVector[x̄[2], x̄[1]]) == map(x̄ -> @SVector[x̄[2], x̄[1]], g)
@@ -58,6 +65,57 @@
     @test eval_on(g, f) == map(x̄->f(x̄...), g)
 end
 
+@testset "componentview" begin
+    v = [@SMatrix[1 3; 2 4] .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+
+    @test componentview(v, 1, 1) isa AbstractArray
+    @test componentview(v, 1, :) isa AbstractArray
+
+    A = @SMatrix[
+            1 4 7;
+            2 5 8;
+            3 6 9;
+        ]
+    v = [A .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+    @test componentview(v, 2:3, 1:2) isa AbstractArray
+
+    # Correctness of the result is tested in ArrayComponentView
+end
+
+@testset "ArrayComponentView" begin
+    v = [@SMatrix[1 3; 2 4] .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+
+    @testset "==" begin
+        @test ArrayComponentView(v, (1,1)) == ArrayComponentView(v, (1,1))
+        @test ArrayComponentView(v, (1,1)) == ArrayComponentView(copy(v), (1,1))
+        @test ArrayComponentView(v, (1,1)) == [1 .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+        @test [1 .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4] == ArrayComponentView(v, (1,1))
+    end
+
+    @testset "components" begin
+        v = [@SMatrix[1 3; 2 4] .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+
+        @test ArrayComponentView(v, (1, 1))  == [1 .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+        @test ArrayComponentView(v, (1, 2))  == [3 .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+        @test ArrayComponentView(v, (2, 1))  == [2 .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+
+        @test ArrayComponentView(v, (1, :))  == [@SVector[1,3] .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+        @test ArrayComponentView(v, (2, :))  == [@SVector[2,4] .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+        @test ArrayComponentView(v, (:, 1))  == [@SVector[1,2] .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+        @test ArrayComponentView(v, (:, 2))  == [@SVector[3,4] .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+
+
+        A = @SMatrix[
+            1 4 7;
+            2 5 8;
+            3 6 9;
+        ]
+        v = [A .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+        @test ArrayComponentView(v, (1:2, 1:2)) == [@SMatrix[1 4;2 5] .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+        @test ArrayComponentView(v, (2:3, 1:2)) == [@SMatrix[2 5;3 6] .+ 100*i .+ 10*j for i ∈ 1:3, j∈ 1:4]
+    end
+end
+
 @testset "_ncomponents" begin
     @test Grids._ncomponents(Int) == 1
     @test Grids._ncomponents(Float64) == 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/Grids/mapped_grid_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,381 @@
+using Diffinitive.Grids
+using Diffinitive.RegionIndices
+using Test
+using StaticArrays
+using LinearAlgebra
+
+
+_skew_mapping(a,b) = (ξ̄->ξ̄[1]*a + ξ̄[2]*b, ξ̄->[a  b])
+
+function _partially_curved_mapping()
+    x̄((ξ, η)) = @SVector[ξ, η*(1+ξ*(ξ-1))]
+    J((ξ, η)) = @SMatrix[
+        1         0;
+        η*(2ξ-1)  1+ξ*(ξ-1);
+    ]
+
+    return x̄, J
+end
+
+function _fully_curved_mapping()
+    x̄((ξ, η)) = @SVector[2ξ + η*(1-η), 3η+(1+η/2)*ξ^2]
+    J((ξ, η)) = @SMatrix[
+        2       1-2η;
+        (2+η)*ξ 3+1/2*ξ^2;
+    ]
+
+    return x̄, J
+end
+
+@testset "MappedGrid" begin
+    @testset "Constructor" begin
+        lg = equidistant_grid((0,0), (1,1), 11, 21)
+
+        x̄ = map(ξ̄ -> 2ξ̄, lg)
+        J = map(ξ̄ -> @SArray(fill(2., 2, 2)), lg)
+        mg = MappedGrid(lg, x̄, J)
+
+        @test mg isa Grid{SVector{2, Float64},2}
+        @test jacobian(mg) isa Array{<:AbstractMatrix}
+        @test logical_grid(mg) isa Grid
+
+        @test collect(mg) == x̄
+        @test jacobian(mg) == J
+        @test logical_grid(mg) == lg
+
+
+        x̄ = map(ξ̄ -> @SVector[ξ̄[1],ξ̄[2], ξ̄[1] + ξ̄[2]], lg)
+        J = map(ξ̄ -> @SMatrix[1 0; 0 1; 1 1], lg)
+        mg = MappedGrid(lg, x̄, J)
+
+        @test mg isa Grid{SVector{3, Float64},2}
+        @test jacobian(mg) isa Array{<:AbstractMatrix}
+        @test logical_grid(mg) isa Grid
+
+        @test collect(mg) == x̄
+        @test jacobian(mg) == J
+        @test logical_grid(mg) == lg
+
+        sz1 = (10,11)
+        sz2 = (10,12)
+        @test_throws ArgumentError("Sizes must match") MappedGrid(
+            equidistant_grid((0,0), (1,1), sz2...),
+            rand(SVector{2},sz1...),
+            rand(SMatrix{2,2},sz1...),
+        )
+
+        @test_throws ArgumentError("Sizes must match") MappedGrid(
+            equidistant_grid((0,0), (1,1), sz1...),
+            rand(SVector{2},sz2...),
+            rand(SMatrix{2,2},sz1...),
+        )
+
+        @test_throws ArgumentError("Sizes must match") MappedGrid(
+            equidistant_grid((0,0), (1,1), sz1...),
+            rand(SVector{2},sz1...),
+            rand(SMatrix{2,2},sz2...),
+        )
+
+        err_str = "The size of the jacobian must match the dimensions of the grid and coordinates"
+        @test_throws ArgumentError(err_str) MappedGrid(
+            equidistant_grid((0,0), (1,1), 10, 11),
+            rand(SVector{3}, 10, 11),
+            rand(SMatrix{3,4}, 10, 11),
+        )
+
+        @test_throws ArgumentError(err_str) MappedGrid(
+            equidistant_grid((0,0), (1,1), 10, 11),
+            rand(SVector{3}, 10, 11),
+            rand(SMatrix{4,2}, 10, 11),
+        )
+    end
+
+    @testset "Indexing Interface" begin
+        lg = equidistant_grid((0,0), (1,1), 11, 21)
+        x̄ = map(ξ̄ -> 2ξ̄, lg)
+        J = map(ξ̄ -> @SArray(fill(2., 2, 2)), lg)
+        mg = MappedGrid(lg, x̄, J)
+        @test mg[1,1] == [0.0, 0.0]
+        @test mg[4,2] == [0.6, 0.1]
+        @test mg[6,10] == [1., 0.9]
+
+        @test mg[begin, begin] == [0.0, 0.0]
+        @test mg[end,end] == [2.0, 2.0]
+        @test mg[begin,end] == [0., 2.]
+
+        @test axes(mg) == (1:11, 1:21)
+
+        @testset "cartesian indexing" begin
+            cases = [
+                 (1,1) ,
+                 (3,5) ,
+                 (10,6),
+                 (1,1) ,
+                 (3,2) ,
+            ]
+
+            @testset "i = $is" for (lg, is) ∈ cases
+                @test mg[CartesianIndex(is...)] == mg[is...]
+            end
+        end
+
+        @testset "eachindex" begin
+            @test eachindex(mg) == CartesianIndices((11,21))
+        end
+
+        @testset "firstindex" begin
+            @test firstindex(mg, 1) == 1
+            @test firstindex(mg, 2) == 1
+        end
+
+        @testset "lastindex" begin
+            @test lastindex(mg, 1) == 11
+            @test lastindex(mg, 2) == 21
+        end
+    end
+
+    @testset "Iterator interface" begin
+        lg = equidistant_grid((0,0), (1,1), 11, 21)
+        x̄ = map(ξ̄ -> 2ξ̄, lg)
+        J = map(ξ̄ -> @SArray(fill(2., 2, 2)), lg)
+
+        mg = MappedGrid(lg, x̄, J)
+
+        lg2 = equidistant_grid((0,0), (1,1), 15, 11)
+        sg = MappedGrid(
+            equidistant_grid((0,0), (1,1), 15, 11),
+            map(ξ̄ -> @SArray[ξ̄[1], ξ̄[2], -ξ̄[1]], lg2), rand(SMatrix{3,2,Float64},15,11)
+        )
+
+        @test eltype(mg) == SVector{2,Float64}
+        @test eltype(sg) == SVector{3,Float64}
+
+        @test eltype(typeof(mg)) == SVector{2,Float64}
+        @test eltype(typeof(sg)) == SVector{3,Float64}
+
+        @test size(mg) == (11,21)
+        @test size(sg) == (15,11)
+
+        @test size(mg,2) == 21
+        @test size(sg,2) == 11
+
+        @test length(mg) == 231
+        @test length(sg) == 165
+
+        @test Base.IteratorSize(mg) == Base.HasShape{2}()
+        @test Base.IteratorSize(typeof(mg)) == Base.HasShape{2}()
+
+        @test Base.IteratorSize(sg) == Base.HasShape{2}()
+        @test Base.IteratorSize(typeof(sg)) == Base.HasShape{2}()
+
+        element, state = iterate(mg)
+        @test element == lg[1,1].*2
+        element, _ =  iterate(mg, state)
+        @test element == lg[2,1].*2
+
+        element, state = iterate(sg)
+        @test element == sg.physicalcoordinates[1,1]
+        element, _ = iterate(sg, state)
+        @test element == sg.physicalcoordinates[2,1]
+
+        @test collect(mg) == 2 .* lg
+    end
+
+    @testset "Base" begin
+        lg = equidistant_grid((0,0), (1,1), 11, 21)
+        x̄ = map(ξ̄ -> 2ξ̄, lg)
+        J = map(ξ̄ -> @SArray(fill(2., 2, 2)), lg)
+        mg = MappedGrid(lg, x̄, J)
+
+        @test ndims(mg) == 2
+    end
+
+    @testset "==" begin
+        sz = (15,11)
+        lg = equidistant_grid((0,0), (1,1), sz...)
+        x = rand(SVector{3,Float64}, sz...)
+        J = rand(SMatrix{3,2,Float64}, sz...)
+
+        sg = MappedGrid(lg, x, J)
+
+        sg1 = MappedGrid(equidistant_grid((0,0), (1,1), sz...), copy(x), copy(J))
+
+        sz2 = (15,12)
+        lg2 = equidistant_grid((0,0), (1,1), sz2...)
+        x2 = rand(SVector{3,Float64}, sz2...)
+        J2 = rand(SMatrix{3,2,Float64}, sz2...)
+        sg2 = MappedGrid(lg2, x2, J2)
+
+        sg3 = MappedGrid(lg, rand(SVector{3,Float64}, sz...), J)
+        sg4 = MappedGrid(lg, x, rand(SMatrix{3,2,Float64}, sz...))
+
+        @test sg == sg1
+        @test sg != sg2 # Different size
+        @test sg != sg3 # Different coordinates
+        @test sg != sg4 # Different jacobian
+    end
+
+    @testset "boundary_identifiers" begin
+        lg = equidistant_grid((0,0), (1,1), 11, 15)
+        x̄ = map(ξ̄ -> 2ξ̄, lg)
+        J = map(ξ̄ -> @SArray(fill(2., 2, 2)), lg)
+        mg = MappedGrid(lg, x̄, J)
+        @test boundary_identifiers(mg) == boundary_identifiers(lg)
+    end
+
+    @testset "boundary_indices" begin
+        lg = equidistant_grid((0,0), (1,1), 11, 15)
+        x̄ = map(ξ̄ -> 2ξ̄, lg)
+        J = map(ξ̄ -> @SArray(fill(2., 2, 2)), lg)
+        mg = MappedGrid(lg, x̄, J)
+
+        @test boundary_indices(mg, CartesianBoundary{1,LowerBoundary}()) == boundary_indices(lg,CartesianBoundary{1,LowerBoundary}())
+        @test boundary_indices(mg, CartesianBoundary{2,LowerBoundary}()) == boundary_indices(lg,CartesianBoundary{2,LowerBoundary}())
+        @test boundary_indices(mg, CartesianBoundary{1,UpperBoundary}()) == boundary_indices(lg,CartesianBoundary{1,UpperBoundary}())
+    end
+
+    @testset "boundary_grid" begin
+        x̄, J = _partially_curved_mapping()
+        mg = mapped_grid(x̄, J, 10, 11)
+        J1((ξ, η)) = @SMatrix[
+            1       ;
+            η*(2ξ-1);
+        ]
+        J2((ξ, η)) = @SMatrix[
+            0;
+            1+ξ*(ξ-1);
+        ]
+
+        function expected_bg(mg, bId, Jb)
+            lg = logical_grid(mg)
+            return MappedGrid(
+                boundary_grid(lg, bId),
+                map(x̄, boundary_grid(lg, bId)),
+                map(Jb, boundary_grid(lg, bId)),
+            )
+        end
+
+        let bid = TensorGridBoundary{1, LowerBoundary}()
+            @test boundary_grid(mg, bid) == expected_bg(mg, bid, J2)
+        end
+
+        let bid = TensorGridBoundary{1, UpperBoundary}()
+            @test boundary_grid(mg, bid) == expected_bg(mg, bid, J2)
+        end
+
+        let bid = TensorGridBoundary{2, LowerBoundary}()
+            @test boundary_grid(mg, bid) == expected_bg(mg, bid, J1)
+        end
+
+        let bid = TensorGridBoundary{2, UpperBoundary}()
+            @test boundary_grid(mg, bid) == expected_bg(mg, bid, J1)
+        end
+    end
+end
+
+@testset "mapped_grid" begin
+    x̄, J = _partially_curved_mapping()
+    mg = mapped_grid(x̄, J, 10, 11)
+    @test mg isa MappedGrid{SVector{2,Float64}, 2}
+
+    lg = equidistant_grid((0,0), (1,1), 10, 11)
+    @test logical_grid(mg) == lg
+    @test collect(mg) == map(x̄, lg)
+
+    @test mapped_grid(x̄, J, lg) == mg
+end
+
+@testset "metric_tensor" begin
+    x̄((ξ, η)) = @SVector[ξ*η, ξ + η^2]
+    J((ξ, η)) = @SMatrix[
+        η    ξ;
+        1   2η;
+    ]
+
+    g = mapped_grid(x̄, J, 10, 11)
+    G = map(logical_grid(g)) do (ξ,η)
+        @SMatrix[
+            1+η^2   ξ*η+2η;
+            ξ*η+2η  ξ^2 + 4η^2;
+        ]
+    end
+    @test metric_tensor(g) ≈ G
+end
+
+@testset "min_spacing" begin
+    let g = mapped_grid(identity, x->@SMatrix[1], 11)
+        @test min_spacing(g) ≈ 0.1
+    end
+
+    let g = mapped_grid(x->x+x.^2/2, x->@SMatrix[1 .+ x], 11)
+        @test min_spacing(g) ≈ 0.105
+    end
+
+    let g = mapped_grid(x->x + x.*(1 .- x)/2, x->@SMatrix[1.5 .- x], 11)
+        @test min_spacing(g) ≈ 0.055
+    end
+
+    let g = mapped_grid(identity, x->@SMatrix[1 0; 0 1], 11,11)
+        @test min_spacing(g) ≈ 0.1
+    end
+
+    let g = mapped_grid(identity, x->@SMatrix[1 0; 0 1], 11,21)
+        @test min_spacing(g) ≈ 0.05
+    end
+
+
+    @testset let a = @SVector[1,0], b = @SVector[1,1]/√2
+        g = mapped_grid(_skew_mapping(a,b)...,11,11)
+
+        @test min_spacing(g) ≈ 0.1*norm(b-a)
+    end
+
+    @testset let a = @SVector[1,0], b = @SVector[-1,1]/√2
+        g = mapped_grid(_skew_mapping(a,b)...,11,11)
+
+        @test min_spacing(g) ≈ 0.1*norm(a+b)
+    end
+end
+
+@testset "normal" begin
+    g = mapped_grid(_partially_curved_mapping()...,10, 11)
+
+    @test normal(g, CartesianBoundary{1,LowerBoundary}()) == fill(@SVector[-1,0], 11)
+    @test normal(g, CartesianBoundary{1,UpperBoundary}()) == fill(@SVector[1,0], 11)
+    @test normal(g, CartesianBoundary{2,LowerBoundary}()) == fill(@SVector[0,-1], 10)
+    @test normal(g, CartesianBoundary{2,UpperBoundary}()) ≈ map(boundary_grid(g,CartesianBoundary{2,UpperBoundary}())|>logical_grid) do ξ̄
+        α = 1-2ξ̄[1]
+        @SVector[α,1]/√(α^2 + 1)
+    end
+
+    g = mapped_grid(_fully_curved_mapping()...,5,4)
+
+    unit(v) = v/norm(v)
+    @testset let bId = CartesianBoundary{1,LowerBoundary}()
+        lbg = boundary_grid(logical_grid(g), bId)
+        @test normal(g, bId) ≈ map(lbg) do (ξ, η)
+            -unit(@SVector[1/2,  η/3-1/6])
+        end
+    end
+
+    @testset let bId = CartesianBoundary{1,UpperBoundary}()
+        lbg = boundary_grid(logical_grid(g), bId)
+        @test normal(g, bId) ≈ map(lbg) do (ξ, η)
+            unit(@SVector[7/2, 2η-1]/(5 + 3η + 2η^2))
+        end
+    end
+
+    @testset let bId = CartesianBoundary{2,LowerBoundary}()
+        lbg = boundary_grid(logical_grid(g), bId)
+        @test normal(g, bId) ≈ map(lbg) do (ξ, η)
+            -unit(@SVector[-2ξ, 2]/(6 + ξ^2 - 2ξ))
+        end
+    end
+
+    @testset let bId = CartesianBoundary{2,UpperBoundary}()
+        lbg = boundary_grid(logical_grid(g), bId)
+        @test normal(g, bId) ≈ map(lbg) do (ξ, η)
+            unit(@SVector[-3ξ, 2]/(6 + ξ^2 + 3ξ))
+        end
+    end
+end
--- a/test/Grids/tensor_grid_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/Grids/tensor_grid_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,7 +1,6 @@
 using Test
-using Sbplib.Grids
+using Diffinitive.Grids
 using StaticArrays
-using Sbplib.RegionIndices
 
 @testset "TensorGrid" begin
     g₁ = EquidistantGrid(range(0,1,length=11))
@@ -34,6 +33,11 @@
 
             @test TensorGrid(g₁, g₄, g₂)[3,2] isa SVector{4,Float64}
             @test TensorGrid(g₁, g₄, g₂)[3,2] == [0.2, 1., 2., 2.2]
+
+            g = TensorGrid(g₁, g₂)
+            @test g[begin, begin] == g[1,1]
+            @test g[begin, end] == g[1,6]
+            @test g[end, end] == g[11,6]
         end
 
         @testset "cartesian indexing" begin
@@ -59,6 +63,18 @@
             @test eachindex(TensorGrid(g₁, g₄)) == CartesianIndices((11,))
             @test eachindex(TensorGrid(g₁, g₄, g₂)) == CartesianIndices((11,6))
         end
+
+        @testset "firstindex" begin
+            @test firstindex(TensorGrid(g₁, g₂, g₃), 1) == 1
+            @test firstindex(TensorGrid(g₁, g₂, g₃), 2) == 1
+            @test firstindex(TensorGrid(g₁, g₂, g₃), 3) == 1
+        end
+
+        @testset "lastindex" begin
+            @test lastindex(TensorGrid(g₁, g₂, g₃), 1) == 11
+            @test lastindex(TensorGrid(g₁, g₂, g₃), 2) == 6
+            @test lastindex(TensorGrid(g₁, g₂, g₃), 3) == 10
+        end
     end
 
     @testset "Iterator interface" begin
@@ -68,12 +84,30 @@
         @test eltype(TensorGrid(g₁, g₄)) == SVector{3,Float64}
         @test eltype(TensorGrid(g₁, g₄, g₂)) == SVector{4,Float64}
 
+        @test eltype(typeof(TensorGrid(g₁, g₂))) == SVector{2,Float64}
+        @test eltype(typeof(TensorGrid(g₁, g₃))) == SVector{2,Float64}
+        @test eltype(typeof(TensorGrid(g₁, g₂, g₃))) == SVector{3,Float64}
+        @test eltype(typeof(TensorGrid(g₁, g₄))) == SVector{3,Float64}
+        @test eltype(typeof(TensorGrid(g₁, g₄, g₂))) == SVector{4,Float64}
+
         @test size(TensorGrid(g₁, g₂)) == (11,6)
         @test size(TensorGrid(g₁, g₃)) == (11,10)
         @test size(TensorGrid(g₁, g₂, g₃)) == (11,6,10)
         @test size(TensorGrid(g₁, g₄)) == (11,)
         @test size(TensorGrid(g₁, g₄, g₂)) == (11,6)
 
+        @test size(TensorGrid(g₁, g₂, g₃),1) == 11
+        @test size(TensorGrid(g₁, g₂, g₃),2) == 6
+        @test size(TensorGrid(g₁, g₂, g₃),3) == 10
+        @test size(TensorGrid(g₁, g₄, g₂),1) == 11
+        @test size(TensorGrid(g₁, g₄, g₂),2) == 6
+
+        @test length(TensorGrid(g₁, g₂)) == 66
+        @test length(TensorGrid(g₁, g₃)) == 110
+        @test length(TensorGrid(g₁, g₂, g₃)) == 660
+        @test length(TensorGrid(g₁, g₄)) == 11
+        @test length(TensorGrid(g₁, g₄, g₂)) == 66
+
         @test Base.IteratorSize(TensorGrid(g₁, g₂)) == Base.HasShape{2}()
         @test Base.IteratorSize(TensorGrid(g₁, g₃)) == Base.HasShape{2}()
         @test Base.IteratorSize(TensorGrid(g₁, g₂, g₃)) == Base.HasShape{3}()
@@ -93,6 +127,46 @@
         @test collect(TensorGrid(g₁, g₄, g₂)) == [@SVector[x,1,2,y] for x ∈ range(0,1,length=11), y ∈ range(2,3,length=6)]
     end
 
+    @testset "Base" begin
+        g₁ = EquidistantGrid(range(0,1,length=11))
+        g₂ = EquidistantGrid(range(2,3,length=6))
+        g = TensorGrid(g₁, g₂)
+
+        @test axes(g, 1) == 1:11
+        @test axes(g, 2) == 1:6
+        @test axes(g) == (1:11,1:6)
+    end
+
+    @testset "spacing" begin
+        g₁ = EquidistantGrid(range(0,1,length=11))
+        g₂ = EquidistantGrid(range(2,3,length=6))
+        g₃ = ZeroDimGrid(@SVector[1,2])
+
+        @test spacing(TensorGrid(g₁)) == (1/10,)
+        @test spacing(TensorGrid(g₂)) == (1/5,)
+
+        @test spacing(TensorGrid(g₁, g₂)) == (1/10, 1/5)
+
+        @test spacing(TensorGrid(g₁, g₃)) == (1/10,)
+        @test spacing(TensorGrid(g₃, g₂)) == (1/5,)
+
+
+        @test spacing(TensorGrid(g₁, g₂, g₁)) == (1/10, 1/5, 1/10)
+
+        @test spacing(TensorGrid(g₃, g₂, g₁)) == (1/5, 1/10)
+        @test spacing(TensorGrid(g₁, g₃, g₁)) == (1/10, 1/10)
+        @test spacing(TensorGrid(g₁, g₂, g₃)) == (1/10, 1/5)
+    end
+
+    @testset "min_spacing" begin
+        g₁ = EquidistantGrid(range(0,1,length=11))
+        g₂ = EquidistantGrid(range(2,3,length=6))
+        g₃ = ZeroDimGrid(@SVector[1,2])
+
+        @test min_spacing(TensorGrid(g₁, g₂)) == 1/10
+        @test min_spacing(TensorGrid(g₂, g₃)) == 1/5
+    end
+
     @testset "refine" begin
         g1(n) = EquidistantGrid(range(0,1,length=n))
         g2(n) = EquidistantGrid(range(2,3,length=n))
@@ -116,13 +190,28 @@
     end
 
     @testset "boundary_identifiers" begin
-        @test boundary_identifiers(TensorGrid(g₁, g₂)) == map((n,id)->TensorGridBoundary{n,id}(), (1,1,2,2), (Lower,Upper,Lower,Upper))
-        @test boundary_identifiers(TensorGrid(g₁, g₄)) == (TensorGridBoundary{1,Lower}(),TensorGridBoundary{1,Upper}())
+        @test boundary_identifiers(TensorGrid(g₁, g₂)) == map((n,id)->TensorGridBoundary{n,id}(), (1,1,2,2), (LowerBoundary,UpperBoundary,LowerBoundary,UpperBoundary))
+        @test boundary_identifiers(TensorGrid(g₁, g₄)) == (TensorGridBoundary{1,LowerBoundary}(),TensorGridBoundary{1,UpperBoundary}())
     end
 
     @testset "boundary_grid" begin
-        @test boundary_grid(TensorGrid(g₁, g₂), TensorGridBoundary{1, Upper}()) == TensorGrid(ZeroDimGrid(g₁[end]), g₂)
-        @test boundary_grid(TensorGrid(g₁, g₄), TensorGridBoundary{1, Upper}()) == TensorGrid(ZeroDimGrid(g₁[end]), g₄)
+        @test boundary_grid(TensorGrid(g₁, g₂), TensorGridBoundary{1, UpperBoundary}()) == TensorGrid(ZeroDimGrid(g₁[end]), g₂)
+        @test boundary_grid(TensorGrid(g₁, g₄), TensorGridBoundary{1, UpperBoundary}()) == TensorGrid(ZeroDimGrid(g₁[end]), g₄)
+    end
+
+    @testset "boundary_indices" begin
+        g₁ = EquidistantGrid(range(0,1,length=11))
+        g₂ = EquidistantGrid(range(2,3,length=6))
+        g₄ = ZeroDimGrid(@SVector[1,2])
+
+        @test boundary_indices(TensorGrid(g₁, g₂), TensorGridBoundary{1, LowerBoundary}()) == (1,:)
+        @test boundary_indices(TensorGrid(g₁, g₂), TensorGridBoundary{1, UpperBoundary}()) == (11,:)
+        @test boundary_indices(TensorGrid(g₁, g₂), TensorGridBoundary{2, LowerBoundary}()) == (:,1)
+        @test boundary_indices(TensorGrid(g₁, g₂), TensorGridBoundary{2, UpperBoundary}()) == (:,6)
+        @test boundary_indices(TensorGrid(g₁, g₄), TensorGridBoundary{1, LowerBoundary}()) == (1,)
+        @test boundary_indices(TensorGrid(g₁, g₄), TensorGridBoundary{1, UpperBoundary}()) == (11,)
+        @test boundary_indices(TensorGrid(g₄,g₁), TensorGridBoundary{2, LowerBoundary}()) == (1,)
+        @test boundary_indices(TensorGrid(g₄,g₁), TensorGridBoundary{2, UpperBoundary}()) == (11,)
     end
 end
 
@@ -144,3 +233,31 @@
     @test Grids.combine_coordinates(1,@SVector[2.,3]) isa SVector{3, Float64}
     @test Grids.combine_coordinates(1,@SVector[2.,3]) == [1,2,3]
 end
+
+@testset "grid_and_local_dim_index" begin
+    cases = [
+        ((1,), 1) => (1,1),
+
+        ((1,1), 1) => (1,1),
+        ((1,1), 2) => (2,1),
+
+        ((1,2), 1) => (1,1),
+        ((1,2), 2) => (2,1),
+        ((1,2), 3) => (2,2),
+
+        ((2,1), 1) => (1,1),
+        ((2,1), 2) => (1,2),
+        ((2,1), 3) => (2,1),
+
+        ((2,1,3), 1) => (1,1),
+        ((2,1,3), 2) => (1,2),
+        ((2,1,3), 3) => (2,1),
+        ((2,1,3), 4) => (3,1),
+        ((2,1,3), 5) => (3,2),
+        ((2,1,3), 6) => (3,3),
+    ]
+
+    @testset "grid_and_local_dim_index$args" for (args, expected) ∈ cases
+        @test Grids.grid_and_local_dim_index(args...) == expected
+    end
+end
--- a/test/Grids/zero_dim_grid_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/Grids/zero_dim_grid_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,5 +1,5 @@
 using Test
-using Sbplib.Grids
+using Diffinitive.Grids
 using StaticArrays
 
 @testset "ZeroDimGrid" begin
@@ -41,4 +41,8 @@
     @testset "boundary_grid" begin
         @test_throws ArgumentError("ZeroDimGrid has no boundaries") boundary_grid(ZeroDimGrid(@SVector[1.0,2.0]), :bid)
     end
+
+    @testset "boundary_indices" begin
+        @test_throws ArgumentError("ZeroDimGrid has no boundaries") boundary_indices(ZeroDimGrid(@SVector[1.0,2.0]), :bid)
+    end
 end
--- a/test/LazyTensors/lazy_array_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/LazyTensors/lazy_array_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,6 +1,6 @@
 using Test
-using Sbplib.LazyTensors
-using Sbplib.RegionIndices
+using Diffinitive.LazyTensors
+using Diffinitive.RegionIndices
 
 
 @testset "LazyArray" begin
--- a/test/LazyTensors/lazy_tensor_operations_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/LazyTensors/lazy_tensor_operations_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,16 +1,16 @@
 using Test
-using Sbplib.LazyTensors
-using Sbplib.RegionIndices
+using Diffinitive.LazyTensors
+using Diffinitive.RegionIndices
 
 using Tullio
 
-struct DummyMapping{T,R,D} <: LazyTensor{T,R,D} end
+struct TransposableDummyMapping{T,R,D} <: LazyTensor{T,R,D} end
 
-LazyTensors.apply(m::DummyMapping{T,R}, v, I::Vararg{Any,R}) where {T,R} = :apply
-LazyTensors.apply_transpose(m::DummyMapping{T,R,D}, v, I::Vararg{Any,D}) where {T,R,D} = :apply_transpose
+LazyTensors.apply(m::TransposableDummyMapping{T,R}, v, I::Vararg{Any,R}) where {T,R} = :apply
+LazyTensors.apply_transpose(m::TransposableDummyMapping{T,R,D}, v, I::Vararg{Any,D}) where {T,R,D} = :apply_transpose
 
-LazyTensors.range_size(m::DummyMapping) = :range_size
-LazyTensors.domain_size(m::DummyMapping) = :domain_size
+LazyTensors.range_size(m::TransposableDummyMapping) = :range_size
+LazyTensors.domain_size(m::TransposableDummyMapping) = :domain_size
 
 
 struct SizeDoublingMapping{T,R,D} <: LazyTensor{T,R,D}
@@ -24,7 +24,7 @@
 
 
 @testset "Mapping transpose" begin
-    m = DummyMapping{Float64,2,3}()
+    m = TransposableDummyMapping{Float64,2,3}()
     @test m' isa LazyTensor{Float64, 3,2}
     @test m'' == m
     @test apply(m',zeros(Float64,(0,0)), 0, 0, 0) == :apply_transpose
@@ -188,7 +188,7 @@
     @test a*Ã == Ã*a
     @test range_size(a*Ã) == range_size(Ã)
     @test domain_size(a*Ã) == domain_size(Ã)
-    @test a*Ã*v == a.*A*v
+    @test a*Ã*v ≈ a.*A*v rtol=1e-14
 end
 
 
--- a/test/LazyTensors/lazy_tensor_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/LazyTensors/lazy_tensor_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,5 +1,5 @@
 using Test
-using Sbplib.LazyTensors
+using Diffinitive.LazyTensors
 
 @testset "Generic Mapping methods" begin
     struct DummyMapping{T,R,D} <: LazyTensor{T,R,D} end
--- a/test/LazyTensors/tensor_types_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/LazyTensors/tensor_types_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,5 +1,5 @@
 using Test
-using Sbplib.LazyTensors
+using Diffinitive.LazyTensors
 using BenchmarkTools
 
 @testset "IdentityTensor" begin
--- a/test/LazyTensors/tuple_manipulation_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/LazyTensors/tuple_manipulation_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,5 +1,5 @@
 using Test
-using Sbplib.LazyTensors
+using Diffinitive.LazyTensors
 
 @testset "split_index" begin
     @test LazyTensors.split_index(2,1,2,2, 1,2,3,4,5,6) == ((1,2,:,5,6),(3,4))
--- a/test/Manifest.toml	Wed May 31 08:59:34 2023 +0200
+++ b/test/Manifest.toml	Sat Jan 11 10:19:47 2025 +0100
@@ -1,51 +1,58 @@
 # This file is machine-generated - editing it directly is not advised
 
-julia_version = "1.8.5"
+julia_version = "1.11.2"
 manifest_format = "2.0"
-project_hash = "f2b0634c12bbed93a17efc88d466604d5a07c465"
+project_hash = "b23d6f58220d898029330f7adcac8132668171ec"
+
+[[deps.Aqua]]
+deps = ["Compat", "Pkg", "Test"]
+git-tree-sha1 = "49b1d7a9870c87ba13dc63f8ccfcf578cb266f95"
+uuid = "4c88cf16-eb10-579e-8560-4a9242c79595"
+version = "0.8.9"
 
 [[deps.ArgTools]]
 uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
-version = "1.1.1"
+version = "1.1.2"
 
 [[deps.Artifacts]]
 uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
+version = "1.11.0"
 
 [[deps.Base64]]
 uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
+version = "1.11.0"
 
 [[deps.BenchmarkTools]]
-deps = ["JSON", "Logging", "Printf", "Profile", "Statistics", "UUIDs"]
-git-tree-sha1 = "d9a9701b899b30332bbcb3e1679c41cce81fb0e8"
+deps = ["Compat", "JSON", "Logging", "Printf", "Profile", "Statistics", "UUIDs"]
+git-tree-sha1 = "e38fbc49a620f5d0b660d7f543db1009fe0f8336"
 uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
-version = "1.3.2"
+version = "1.6.0"
 
-[[deps.ChainRulesCore]]
-deps = ["Compat", "LinearAlgebra", "SparseArrays"]
-git-tree-sha1 = "c6d890a52d2c4d55d326439580c3b8d0875a77d9"
-uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
-version = "1.15.7"
-
-[[deps.ChangesOfVariables]]
-deps = ["ChainRulesCore", "LinearAlgebra", "Test"]
-git-tree-sha1 = "844b061c104c408b24537482469400af6075aae4"
-uuid = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0"
-version = "0.1.5"
+[[deps.CodeTracking]]
+deps = ["InteractiveUtils", "UUIDs"]
+git-tree-sha1 = "7eee164f122511d3e4e1ebadb7956939ea7e1c77"
+uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"
+version = "1.3.6"
 
 [[deps.Compat]]
-deps = ["Dates", "LinearAlgebra", "UUIDs"]
-git-tree-sha1 = "61fdd77467a5c3ad071ef8277ac6bd6af7dd4c04"
+deps = ["TOML", "UUIDs"]
+git-tree-sha1 = "8ae8d32e09f0dcf42a36b90d4e17f5dd2e4c4215"
 uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
-version = "4.6.0"
+version = "4.16.0"
+weakdeps = ["Dates", "LinearAlgebra"]
+
+    [deps.Compat.extensions]
+    CompatLinearAlgebraExt = "LinearAlgebra"
 
 [[deps.CompilerSupportLibraries_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
-version = "1.0.1+0"
+version = "1.1.1+0"
 
 [[deps.Dates]]
 deps = ["Printf"]
 uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
+version = "1.11.0"
 
 [[deps.DeepDiffs]]
 git-tree-sha1 = "9824894295b62a6a4ab6adf1c7bf337b3a9ca34c"
@@ -54,13 +61,14 @@
 
 [[deps.DiffRules]]
 deps = ["IrrationalConstants", "LogExpFunctions", "NaNMath", "Random", "SpecialFunctions"]
-git-tree-sha1 = "c5b6685d53f933c11404a3ae9822afe30d522494"
+git-tree-sha1 = "23163d55f885173722d1e4cf0f6110cdbaf7e272"
 uuid = "b552c78f-8df3-52c6-915a-8e097449b14b"
-version = "1.12.2"
+version = "1.15.1"
 
 [[deps.Distributed]]
 deps = ["Random", "Serialization", "Sockets"]
 uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"
+version = "1.11.0"
 
 [[deps.DocStringExtensions]]
 deps = ["LibGit2"]
@@ -75,95 +83,152 @@
 
 [[deps.FileWatching]]
 uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
+version = "1.11.0"
 
 [[deps.Glob]]
-git-tree-sha1 = "4df9f7e06108728ebf00a0a11edee4b29a482bb2"
+git-tree-sha1 = "97285bbd5230dd766e9ef6749b80fc617126d496"
 uuid = "c27321d9-0574-5035-807b-f59d2c89b15c"
-version = "1.3.0"
+version = "1.3.1"
 
 [[deps.InteractiveUtils]]
 deps = ["Markdown"]
 uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
-
-[[deps.InverseFunctions]]
-deps = ["Test"]
-git-tree-sha1 = "49510dfcb407e572524ba94aeae2fced1f3feb0f"
-uuid = "3587e190-3f89-42d0-90ee-14403ec27112"
-version = "0.1.8"
+version = "1.11.0"
 
 [[deps.IrrationalConstants]]
-git-tree-sha1 = "7fd44fd4ff43fc60815f8e764c0f352b83c49151"
+git-tree-sha1 = "630b497eafcc20001bba38a4651b327dcfc491d2"
 uuid = "92d709cd-6900-40b7-9082-c6be49f344b6"
-version = "0.1.1"
+version = "0.2.2"
+
+[[deps.JET]]
+deps = ["CodeTracking", "InteractiveUtils", "JuliaInterpreter", "LoweredCodeUtils", "MacroTools", "Pkg", "PrecompileTools", "Preferences", "Test"]
+git-tree-sha1 = "5c5ac91e775b585864015c5c1703cee283071a47"
+uuid = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
+version = "0.9.12"
+
+    [deps.JET.extensions]
+    JETCthulhuExt = "Cthulhu"
+    ReviseExt = "Revise"
+
+    [deps.JET.weakdeps]
+    Cthulhu = "f68482b8-f384-11e8-15f7-abe071a5a75f"
+    Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
 
 [[deps.JLLWrappers]]
-deps = ["Preferences"]
-git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1"
+deps = ["Artifacts", "Preferences"]
+git-tree-sha1 = "a007feb38b422fbdab534406aeca1b86823cb4d6"
 uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
-version = "1.4.1"
+version = "1.7.0"
 
 [[deps.JSON]]
 deps = ["Dates", "Mmap", "Parsers", "Unicode"]
-git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e"
+git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a"
 uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
-version = "0.21.3"
+version = "0.21.4"
+
+[[deps.JuliaInterpreter]]
+deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"]
+git-tree-sha1 = "10da5154188682e5c0726823c2b5125957ec3778"
+uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
+version = "0.9.38"
+
+[[deps.LRUCache]]
+git-tree-sha1 = "b3cc6698599b10e652832c2f23db3cab99d51b59"
+uuid = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637"
+version = "1.6.1"
+weakdeps = ["Serialization"]
+
+    [deps.LRUCache.extensions]
+    SerializationExt = ["Serialization"]
 
 [[deps.LibCURL]]
 deps = ["LibCURL_jll", "MozillaCACerts_jll"]
 uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
-version = "0.6.3"
+version = "0.6.4"
 
 [[deps.LibCURL_jll]]
 deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
 uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
-version = "7.84.0+0"
+version = "8.6.0+0"
 
 [[deps.LibGit2]]
-deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
+deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"]
 uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
+version = "1.11.0"
+
+[[deps.LibGit2_jll]]
+deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"]
+uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5"
+version = "1.7.2+0"
 
 [[deps.LibSSH2_jll]]
 deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
 uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
-version = "1.10.2+0"
+version = "1.11.0+1"
 
 [[deps.Libdl]]
 uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
+version = "1.11.0"
 
 [[deps.LinearAlgebra]]
-deps = ["Libdl", "libblastrampoline_jll"]
+deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
 uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
+version = "1.11.0"
 
 [[deps.LogExpFunctions]]
-deps = ["ChainRulesCore", "ChangesOfVariables", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"]
-git-tree-sha1 = "45b288af6956e67e621c5cbb2d75a261ab58300b"
+deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"]
+git-tree-sha1 = "13ca9e2586b89836fd20cccf56e57e2b9ae7f38f"
 uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
-version = "0.3.20"
+version = "0.3.29"
+
+    [deps.LogExpFunctions.extensions]
+    LogExpFunctionsChainRulesCoreExt = "ChainRulesCore"
+    LogExpFunctionsChangesOfVariablesExt = "ChangesOfVariables"
+    LogExpFunctionsInverseFunctionsExt = "InverseFunctions"
+
+    [deps.LogExpFunctions.weakdeps]
+    ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
+    ChangesOfVariables = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0"
+    InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112"
 
 [[deps.Logging]]
 uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
+version = "1.11.0"
+
+[[deps.LoweredCodeUtils]]
+deps = ["JuliaInterpreter"]
+git-tree-sha1 = "688d6d9e098109051ae33d126fcfc88c4ce4a021"
+uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
+version = "3.1.0"
+
+[[deps.MacroTools]]
+git-tree-sha1 = "72aebe0b5051e5143a079a4685a46da330a40472"
+uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
+version = "0.5.15"
 
 [[deps.Markdown]]
 deps = ["Base64"]
 uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
+version = "1.11.0"
 
 [[deps.MbedTLS_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
-version = "2.28.0+0"
+version = "2.28.6+0"
 
 [[deps.Mmap]]
 uuid = "a63ad114-7e13-5084-954f-fe012c677804"
+version = "1.11.0"
 
 [[deps.MozillaCACerts_jll]]
 uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
-version = "2022.2.1"
+version = "2023.12.12"
 
 [[deps.NaNMath]]
 deps = ["OpenLibm_jll"]
-git-tree-sha1 = "a7c3d1da1189a1c2fe843a3bfa04d18d20eb3211"
+git-tree-sha1 = "030ea22804ef91648f29b7ad3fc15fa49d0e6e71"
 uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
-version = "1.0.1"
+version = "1.0.3"
 
 [[deps.NetworkOptions]]
 uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
@@ -172,51 +237,72 @@
 [[deps.OpenBLAS_jll]]
 deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
 uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"
-version = "0.3.20+0"
+version = "0.3.27+1"
 
 [[deps.OpenLibm_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "05823500-19ac-5b8b-9628-191a04bc5112"
-version = "0.8.1+0"
+version = "0.8.1+2"
 
 [[deps.OpenSpecFun_jll]]
-deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1"
+deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"]
+git-tree-sha1 = "1346c9208249809840c91b26703912dff463d335"
 uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
-version = "0.5.5+0"
+version = "0.5.6+0"
+
+[[deps.PackageExtensionCompat]]
+git-tree-sha1 = "fb28e33b8a95c4cee25ce296c817d89cc2e53518"
+uuid = "65ce6f38-6b18-4e1d-a461-8949797d7930"
+version = "1.0.2"
+weakdeps = ["Requires", "TOML"]
 
 [[deps.Parsers]]
-deps = ["Dates", "SnoopPrecompile"]
-git-tree-sha1 = "151d91d63d8d6c1a5789ecb7de51547e00480f1b"
+deps = ["Dates", "PrecompileTools", "UUIDs"]
+git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821"
 uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
-version = "2.5.4"
+version = "2.8.1"
 
 [[deps.Pkg]]
-deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
+deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"]
 uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
-version = "1.8.0"
+version = "1.11.0"
+
+    [deps.Pkg.extensions]
+    REPLExt = "REPL"
+
+    [deps.Pkg.weakdeps]
+    REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
+
+[[deps.PrecompileTools]]
+deps = ["Preferences"]
+git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f"
+uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
+version = "1.2.1"
 
 [[deps.Preferences]]
 deps = ["TOML"]
-git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d"
+git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6"
 uuid = "21216c6a-2e73-6563-6e65-726566657250"
-version = "1.3.0"
+version = "1.4.3"
 
 [[deps.Printf]]
 deps = ["Unicode"]
 uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
+version = "1.11.0"
 
 [[deps.Profile]]
-deps = ["Printf"]
 uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79"
+version = "1.11.0"
 
-[[deps.REPL]]
-deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
-uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
+[[deps.PtrArrays]]
+git-tree-sha1 = "77a42d78b6a92df47ab37e177b2deac405e1c88f"
+uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d"
+version = "1.2.1"
 
 [[deps.Random]]
-deps = ["SHA", "Serialization"]
+deps = ["SHA"]
 uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+version = "1.11.0"
 
 [[deps.Requires]]
 deps = ["UUIDs"]
@@ -230,90 +316,189 @@
 
 [[deps.Serialization]]
 uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
-
-[[deps.SnoopPrecompile]]
-deps = ["Preferences"]
-git-tree-sha1 = "e760a70afdcd461cf01a575947738d359234665c"
-uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c"
-version = "1.0.3"
+version = "1.11.0"
 
 [[deps.Sockets]]
 uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
+version = "1.11.0"
+
+[[deps.SparseArrayKit]]
+deps = ["LinearAlgebra", "PackageExtensionCompat", "TensorOperations", "TupleTools", "VectorInterface"]
+git-tree-sha1 = "77769a2a6fefd92fc7c0367c0771a2a9d2b6f8e1"
+uuid = "a9a3c162-d163-4c15-8926-b8794fbefed2"
+version = "0.4.0"
+weakdeps = ["SparseArrays"]
+
+    [deps.SparseArrayKit.extensions]
+    SparseArrayKitSparseArrays = "SparseArrays"
 
 [[deps.SparseArrays]]
-deps = ["LinearAlgebra", "Random"]
+deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"]
 uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+version = "1.11.0"
 
 [[deps.SpecialFunctions]]
-deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
-git-tree-sha1 = "d75bda01f8c31ebb72df80a46c88b25d1c79c56d"
+deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
+git-tree-sha1 = "64cca0c26b4f31ba18f13f6c12af7c85f478cfde"
 uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
-version = "2.1.7"
+version = "2.5.0"
+
+    [deps.SpecialFunctions.extensions]
+    SpecialFunctionsChainRulesCoreExt = "ChainRulesCore"
+
+    [deps.SpecialFunctions.weakdeps]
+    ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
 
 [[deps.StaticArrays]]
-deps = ["LinearAlgebra", "Random", "StaticArraysCore", "Statistics"]
-git-tree-sha1 = "2d7d9e1ddadc8407ffd460e24218e37ef52dd9a3"
+deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"]
+git-tree-sha1 = "47091a0340a675c738b1304b58161f3b0839d454"
 uuid = "90137ffa-7385-5640-81b9-e52037218182"
-version = "1.5.16"
+version = "1.9.10"
+
+    [deps.StaticArrays.extensions]
+    StaticArraysChainRulesCoreExt = "ChainRulesCore"
+    StaticArraysStatisticsExt = "Statistics"
+
+    [deps.StaticArrays.weakdeps]
+    ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
+    Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
 
 [[deps.StaticArraysCore]]
-git-tree-sha1 = "6b7ba252635a5eff6a0b0664a41ee140a1c9e72a"
+git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682"
 uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c"
-version = "1.4.0"
+version = "1.4.3"
 
 [[deps.Statistics]]
-deps = ["LinearAlgebra", "SparseArrays"]
+deps = ["LinearAlgebra"]
+git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0"
 uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
+version = "1.11.1"
+weakdeps = ["SparseArrays"]
+
+    [deps.Statistics.extensions]
+    SparseArraysExt = ["SparseArrays"]
+
+[[deps.Strided]]
+deps = ["LinearAlgebra", "StridedViews", "TupleTools"]
+git-tree-sha1 = "f9ce8284e6eec72a21de3603493eb5355fcf7f39"
+uuid = "5e0ebb24-38b0-5f93-81fe-25c709ecae67"
+version = "2.2.0"
+
+[[deps.StridedViews]]
+deps = ["LinearAlgebra", "PackageExtensionCompat"]
+git-tree-sha1 = "b60baf1998bcdccc57e1cc2c6703df1f619a3754"
+uuid = "4db3bf67-4bd7-4b4e-b153-31dc3fb37143"
+version = "0.3.2"
+
+    [deps.StridedViews.extensions]
+    StridedViewsCUDAExt = "CUDA"
+
+    [deps.StridedViews.weakdeps]
+    CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
+
+[[deps.SuiteSparse_jll]]
+deps = ["Artifacts", "Libdl", "libblastrampoline_jll"]
+uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c"
+version = "7.7.0+0"
 
 [[deps.TOML]]
 deps = ["Dates"]
 uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
-version = "1.0.0"
+version = "1.0.3"
 
 [[deps.Tar]]
 deps = ["ArgTools", "SHA"]
 uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
-version = "1.10.1"
+version = "1.10.0"
+
+[[deps.TensorOperations]]
+deps = ["LRUCache", "LinearAlgebra", "PackageExtensionCompat", "PtrArrays", "Strided", "StridedViews", "TupleTools", "VectorInterface"]
+git-tree-sha1 = "d08a24e2cb67aa0cbfcd68d0acdf2879e571126f"
+uuid = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2"
+version = "5.1.3"
+
+    [deps.TensorOperations.extensions]
+    TensorOperationsBumperExt = "Bumper"
+    TensorOperationsChainRulesCoreExt = "ChainRulesCore"
+    TensorOperationscuTENSORExt = ["cuTENSOR", "CUDA"]
+
+    [deps.TensorOperations.weakdeps]
+    Bumper = "8ce10254-0962-460f-a3d8-1f77fea1446e"
+    CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
+    ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
+    cuTENSOR = "011b41b2-24ef-40a8-b3eb-fa098493e9e1"
 
 [[deps.Test]]
 deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
 uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+version = "1.11.0"
 
 [[deps.TestSetExtensions]]
 deps = ["DeepDiffs", "Distributed", "Test"]
-git-tree-sha1 = "3a2919a78b04c29a1a57b05e1618e473162b15d0"
+git-tree-sha1 = "ccebd99935be339d2ad907589708ba1f0d62bab3"
 uuid = "98d24dd4-01ad-11ea-1b02-c9a08f80db04"
-version = "2.0.0"
+version = "3.0.0"
+
+[[deps.Tokens]]
+deps = ["SparseArrayKit", "SparseArrays"]
+git-tree-sha1 = "c4f40125383ce3bfcfcd49a1b206080b7afd9a34"
+uuid = "040c2ec2-8d69-4aca-bf03-7d3a7092f2f6"
+version = "0.1.1"
 
 [[deps.Tullio]]
-deps = ["ChainRulesCore", "DiffRules", "LinearAlgebra", "Requires"]
-git-tree-sha1 = "7871a39eac745697ee512a87eeff06a048a7905b"
+deps = ["DiffRules", "LinearAlgebra", "Requires"]
+git-tree-sha1 = "6d476962ba4e435d7f4101a403b1d3d72afe72f3"
 uuid = "bc48ee85-29a4-5162-ae0b-a64e1601d4bc"
-version = "0.3.5"
+version = "0.3.7"
+
+    [deps.Tullio.extensions]
+    TullioCUDAExt = "CUDA"
+    TullioChainRulesCoreExt = "ChainRulesCore"
+    TullioFillArraysExt = "FillArrays"
+    TullioTrackerExt = "Tracker"
+
+    [deps.Tullio.weakdeps]
+    CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
+    ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
+    FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b"
+    Tracker = "9f7883ad-71c0-57eb-9f7f-b5c9e6d3789c"
+
+[[deps.TupleTools]]
+git-tree-sha1 = "41e43b9dc950775eac654b9f845c839cd2f1821e"
+uuid = "9d95972d-f1c8-5527-a6e0-b4b365fa01f6"
+version = "1.6.0"
 
 [[deps.UUIDs]]
 deps = ["Random", "SHA"]
 uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+version = "1.11.0"
 
 [[deps.Unicode]]
 uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
+version = "1.11.0"
+
+[[deps.VectorInterface]]
+deps = ["LinearAlgebra"]
+git-tree-sha1 = "cea8abaa6e43f72f97a09cf95b80c9eb53ff75cf"
+uuid = "409d34a3-91d5-4945-b6ec-7529ddf182d8"
+version = "0.4.9"
 
 [[deps.Zlib_jll]]
 deps = ["Libdl"]
 uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
-version = "1.2.12+3"
+version = "1.2.13+1"
 
 [[deps.libblastrampoline_jll]]
-deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
+deps = ["Artifacts", "Libdl"]
 uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"
-version = "5.1.1+0"
+version = "5.11.0+0"
 
 [[deps.nghttp2_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
-version = "1.48.0+0"
+version = "1.59.0+0"
 
 [[deps.p7zip_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
-version = "17.4.0+0"
+version = "17.4.0+2"
--- a/test/Project.toml	Wed May 31 08:59:34 2023 +0200
+++ b/test/Project.toml	Sat Jan 11 10:19:47 2025 +0100
@@ -1,9 +1,14 @@
 [deps]
+Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
 BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
 Glob = "c27321d9-0574-5035-807b-f59d2c89b15c"
+JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
 LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
+SparseArrayKit = "a9a3c162-d163-4c15-8926-b8794fbefed2"
+SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
 StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
 TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
 Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
 TestSetExtensions = "98d24dd4-01ad-11ea-1b02-c9a08f80db04"
+Tokens = "040c2ec2-8d69-4aca-bf03-7d3a7092f2f6"
 Tullio = "bc48ee85-29a4-5162-ae0b-a64e1601d4bc"
--- a/test/RegionIndices/RegionIndices_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/RegionIndices/RegionIndices_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,3 +1,3 @@
-using Sbplib.RegionIndices
+using Diffinitive.RegionIndices
 using Test
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/SbpOperators/boundary_conditions/boundary_condition_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,39 @@
+using Test
+
+using Diffinitive.Grids
+using Diffinitive.RegionIndices
+using Diffinitive.SbpOperators
+
+@testset "BoundaryCondition" begin
+    grid_1d = equidistant_grid(0.0, 1.0, 11)
+    grid_2d = equidistant_grid((0.0, 0.0), (1.0,1.0), 11, 15)
+    grid_3d = equidistant_grid((0.0, 0.0, 0.0), (1.0,1.0, 1.0), 11, 15, 13)
+    (id_l,_) = boundary_identifiers(grid_1d)
+    (_,_,_,id_n) = boundary_identifiers(grid_2d)
+    (_,_,_,_,id_b,_) = boundary_identifiers(grid_3d)
+
+    g = 3.14
+    f(x,y,z) = x^2+y^2+z^2
+    @testset "Constructors" begin
+        @test DirichletCondition(g,id_l) isa DirichletCondition{Float64,LowerBoundary}
+        @test NeumannCondition(f,id_b) isa NeumannCondition{<:Function,CartesianBoundary{3,LowerBoundary}}
+    end
+
+    @testset "boundary" begin
+        @test boundary(DirichletCondition(g,id_l)) == id_l
+        @test boundary(NeumannCondition(f,id_b)) == id_b
+    end
+
+    @testset "boundary_data" begin
+        @test boundary_data(DirichletCondition(g,id_l)) == g
+        @test boundary_data(NeumannCondition(f,id_b)) == f
+    end
+
+    @testset "discretize_data" begin
+        @test fill(g) ≈ discretize_data(grid_1d,DirichletCondition(g,id_l))
+        @test g*ones(11,1) ≈ discretize_data(grid_2d,DirichletCondition(g,id_n))
+        X = repeat(0:1/10:1, inner = (1,15))
+        Y = repeat(0:1/14:1, outer = (1,11))
+        @test map((x,y)->f(x,y,0), X,Y') ≈ discretize_data(grid_3d,NeumannCondition(f,id_b))
+    end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/SbpOperators/boundary_conditions/sat_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,71 @@
+using Test
+
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
+using Diffinitive.SbpOperators
+
+stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order = 4)
+
+struct MockOp end
+
+function SbpOperators.sat_tensors(op::MockOp, g::Grid, bc::DirichletCondition; a = 1.)
+    e = boundary_restriction(g, stencil_set, boundary(bc))
+    L = a*e
+    sat_op = e'
+    return sat_op, L
+end
+
+function SbpOperators.sat_tensors(op::MockOp, g::Grid, bc::NeumannCondition)
+    e = boundary_restriction(g, stencil_set, boundary(bc))
+    d = normal_derivative(g, stencil_set, boundary(bc))
+    L = d
+    sat_op = e'
+    return sat_op, L
+end
+
+@testset "sat" begin
+    op = MockOp()
+    @testset "1D" begin
+        grid  = equidistant_grid(0., 1., 11)
+        l, r = boundary_identifiers(grid)
+        u = eval_on(grid, x-> 1. + 2x^2)
+        dc = DirichletCondition(1.0, l)
+        g_l = discretize_data(grid, dc)
+        SAT_l = sat(op, grid, dc)
+        @test SAT_l(u, g_l) ≈ zeros((size(grid))) atol = 1e-13
+        
+        nc = NeumannCondition(4.0, r)
+        g_r = discretize_data(grid, nc)
+        SAT_r = sat(op, grid, nc)
+        @test SAT_r(u, g_r) ≈ zeros((size(grid))) atol = 1e-13
+    end
+    @testset "2D" begin
+        grid  = equidistant_grid((0.,0.), (1.,1.), 11, 13)
+        W, E, S, N = boundary_identifiers(grid)
+        u = eval_on(grid, (x,y) -> x+y^2)
+
+        dc_W = DirichletCondition(1.0, W)
+        SAT_W = sat(op, grid, dc_W)
+        g_W = discretize_data(grid, dc_W)
+        r_W = zeros(size(grid))
+        r_W[1,:] .= map(y -> (y^2-1.), range(0., 1., length=13))
+        @test SAT_W(u, g_W) ≈ r_W atol = 1e-13
+
+        dc_E = DirichletCondition(2, E)
+        SAT_E = sat(op, grid, dc_E; a = 2.)
+        g_E = discretize_data(grid, dc_E)
+        r_E = zeros(size(grid))
+        r_E[end,:] .= map(y -> (2*(1. + y^2)-2.), range(0., 1., length=13))
+        @test SAT_E(u, g_E) ≈ r_E atol = 1e-13
+
+        nc_S = NeumannCondition(.0, S)
+        SAT_S = sat(op, grid, nc_S)
+        g_S = discretize_data(grid, nc_S)
+        @test SAT_S(u, g_S) ≈ zeros(size(grid)) atol = 1e-13
+
+        nc_N = NeumannCondition(2.0, N)
+        SAT_N = sat(op, grid, nc_N)
+        g_N = discretize_data(grid, nc_N)
+        @test SAT_N(u, g_N) ≈ zeros(size(grid)) atol = 1e-13
+    end
+end
--- a/test/SbpOperators/boundaryops/boundary_operator_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/boundaryops/boundary_operator_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,11 +1,11 @@
 using Test
 
-using Sbplib.LazyTensors
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.RegionIndices
-import Sbplib.SbpOperators.Stencil
-import Sbplib.SbpOperators.BoundaryOperator
+using Diffinitive.LazyTensors
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.RegionIndices
+import Diffinitive.SbpOperators.Stencil
+import Diffinitive.SbpOperators.BoundaryOperator
 
 
 @testset "BoundaryOperator" begin
@@ -13,12 +13,12 @@
     g_1D = EquidistantGrid(range(0,1,length=11))
 
     @testset "Constructors" begin
-        @test BoundaryOperator(g_1D, closure_stencil, Lower()) isa LazyTensor{T,0,1} where T
-        @test BoundaryOperator(g_1D, closure_stencil, Upper()) isa LazyTensor{T,0,1} where T
+        @test BoundaryOperator(g_1D, closure_stencil, LowerBoundary()) isa LazyTensor{T,0,1} where T
+        @test BoundaryOperator(g_1D, closure_stencil, UpperBoundary()) isa LazyTensor{T,0,1} where T
     end
 
-    op_l = BoundaryOperator(g_1D, closure_stencil, Lower())
-    op_r = BoundaryOperator(g_1D, closure_stencil, Upper())
+    op_l = BoundaryOperator(g_1D, closure_stencil, LowerBoundary())
+    op_r = BoundaryOperator(g_1D, closure_stencil, UpperBoundary())
 
     @testset "Sizes" begin
         @test domain_size(op_l) == (11,)
--- a/test/SbpOperators/boundaryops/boundary_restriction_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/boundaryops/boundary_restriction_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,32 +1,32 @@
 using Test
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
-using Sbplib.RegionIndices
-using Sbplib.SbpOperators: BoundaryOperator, Stencil
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
+using Diffinitive.RegionIndices
+using Diffinitive.SbpOperators: BoundaryOperator, Stencil
 
 @testset "boundary_restriction" begin
 	stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order = 4)
 	e_closure = parse_stencil(stencil_set["e"]["closure"])
-    g_1D = equidistant_grid(11, 0.0, 1.0)
-    g_2D = equidistant_grid((11,15), (0.0, 0.0), (1.0,1.0))
+    g_1D = equidistant_grid(0.0, 1.0, 11)
+    g_2D = equidistant_grid((0.0, 0.0), (1.0,1.0), 11, 15)
 
     @testset "boundary_restriction" begin
         @testset "1D" begin
-            e_l = boundary_restriction(g_1D,stencil_set,Lower())
-            @test e_l == BoundaryOperator(g_1D,Stencil{Float64}(e_closure),Lower())
-            @test e_l isa BoundaryOperator{T,Lower} where T
+            e_l = boundary_restriction(g_1D,stencil_set,LowerBoundary())
+            @test e_l == BoundaryOperator(g_1D,Stencil{Float64}(e_closure),LowerBoundary())
+            @test e_l isa BoundaryOperator{T,LowerBoundary} where T
             @test e_l isa LazyTensor{T,0,1} where T
 
-            e_r = boundary_restriction(g_1D,stencil_set,Upper())
-            @test e_r == BoundaryOperator(g_1D,Stencil{Float64}(e_closure),Upper())
-            @test e_r isa BoundaryOperator{T,Upper} where T
+            e_r = boundary_restriction(g_1D,stencil_set,UpperBoundary())
+            @test e_r == BoundaryOperator(g_1D,Stencil{Float64}(e_closure),UpperBoundary())
+            @test e_r isa BoundaryOperator{T,UpperBoundary} where T
             @test e_r isa LazyTensor{T,0,1} where T
         end
 
         @testset "2D" begin
-            e_w = boundary_restriction(g_2D,stencil_set,CartesianBoundary{1,Upper}())
+            e_w = boundary_restriction(g_2D,stencil_set,CartesianBoundary{1,UpperBoundary}())
             @test e_w isa InflatedTensor
             @test e_w isa LazyTensor{T,1,2} where T
         end
--- a/test/SbpOperators/boundaryops/normal_derivative_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/boundaryops/normal_derivative_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,30 +1,30 @@
 using Test
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
-using Sbplib.RegionIndices
-import Sbplib.SbpOperators.BoundaryOperator
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
+using Diffinitive.RegionIndices
+import Diffinitive.SbpOperators.BoundaryOperator
 
 @testset "normal_derivative" begin
-    g_1D = equidistant_grid(11, 0.0, 1.0)
-    g_2D = equidistant_grid((11,12), (0.0, 0.0), (1.0,1.0))
+    g_1D = equidistant_grid(0.0, 1.0, 11)
+    g_2D = equidistant_grid((0.0, 0.0), (1.0,1.0), 11, 12)
     @testset "normal_derivative" begin
     	stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order=4)
         @testset "1D" begin
-            d_l = normal_derivative(g_1D, stencil_set, Lower())
-            @test d_l == normal_derivative(g_1D, stencil_set, Lower())
-            @test d_l isa BoundaryOperator{T,Lower} where T
+            d_l = normal_derivative(g_1D, stencil_set, LowerBoundary())
+            @test d_l == normal_derivative(g_1D, stencil_set, LowerBoundary())
+            @test d_l isa BoundaryOperator{T,LowerBoundary} where T
             @test d_l isa LazyTensor{T,0,1} where T
         end
         @testset "2D" begin
-            d_w = normal_derivative(g_2D, stencil_set, CartesianBoundary{1,Lower}())
-            d_n = normal_derivative(g_2D, stencil_set, CartesianBoundary{2,Upper}())
+            d_w = normal_derivative(g_2D, stencil_set, CartesianBoundary{1,LowerBoundary}())
+            d_n = normal_derivative(g_2D, stencil_set, CartesianBoundary{2,UpperBoundary}())
             Ix = IdentityTensor{Float64}((size(g_2D)[1],))
             Iy = IdentityTensor{Float64}((size(g_2D)[2],))
-            d_l = normal_derivative(g_2D.grids[1], stencil_set, Lower())
-            d_r = normal_derivative(g_2D.grids[2], stencil_set, Upper())
-            @test d_w == normal_derivative(g_2D, stencil_set, CartesianBoundary{1,Lower}())
+            d_l = normal_derivative(g_2D.grids[1], stencil_set, LowerBoundary())
+            d_r = normal_derivative(g_2D.grids[2], stencil_set, UpperBoundary())
+            @test d_w == normal_derivative(g_2D, stencil_set, CartesianBoundary{1,LowerBoundary}())
             @test d_w ==  d_l⊗Iy
             @test d_n ==  Ix⊗d_r
             @test d_w isa LazyTensor{T,1,2} where T
--- a/test/SbpOperators/stencil_set_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/stencil_set_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,9 +1,10 @@
 using Test
 
 using TOML
-using Sbplib.SbpOperators
+using Diffinitive.SbpOperators
 
-import Sbplib.SbpOperators.Stencil
+import Diffinitive.SbpOperators.Stencil
+import Diffinitive.SbpOperators.NestedStencil
 
 @testset "readoperator" begin
     toml_str = """
@@ -170,3 +171,18 @@
     @test SbpOperators.parse_rational(2) isa Rational
     @test SbpOperators.parse_rational(2) == 2//1
 end
+
+@testset "parse_nested_stencil" begin
+    toml = TOML.parse("""
+        s1 = [["1/2", "1/2", "0"],[ "-1/2", "-1", "-1/2"],["0", "1/2", "1/2"]]
+        s2 = {s = [[  "2",  "-1", "0"],[   "-3",  "1",    "0"],["1",   "0",   "0"]], c = 1}
+        s3 = {s = [[  "2",  "-1", "0"],[   "-3",  "1",    "0"],["1",   "0",   "0"]], c = 2}
+    """)
+
+    @test parse_nested_stencil(toml["s1"]) == CenteredNestedStencil((1//2, 1//2, 0//1),( -1//2, -1//1, -1//2),(0//1, 1//2, 1//2))
+    @test parse_nested_stencil(toml["s2"]) == NestedStencil((2//1, -1//1, 0//1),( -3//1, 1//1, 0//1),(1//1, 0//1, 0//1), center = 1)
+    @test parse_nested_stencil(toml["s3"]) == NestedStencil((2//1, -1//1, 0//1),( -3//1, 1//1, 0//1),(1//1, 0//1, 0//1), center = 2)
+
+    @test parse_nested_stencil(Float64, toml["s1"]) == CenteredNestedStencil((1/2, 1/2, 0.),( -1/2, -1., -1/2),(0., 1/2, 1/2))
+    @test parse_nested_stencil(Int, toml["s2"]) == NestedStencil((2, -1, 0),( -3, 1, 0),(1, 0, 0), center = 1)
+end
--- a/test/SbpOperators/stencil_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/stencil_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,8 +1,10 @@
 using Test
-using Sbplib.SbpOperators
-import Sbplib.SbpOperators.Stencil
-import Sbplib.SbpOperators.NestedStencil
-import Sbplib.SbpOperators.scale
+using Diffinitive.SbpOperators
+using StaticArrays
+import Diffinitive.SbpOperators.Stencil
+import Diffinitive.SbpOperators.NestedStencil
+import Diffinitive.SbpOperators.scale
+import Diffinitive.SbpOperators: apply_stencil, apply_stencil_backwards
 
 @testset "Stencil" begin
     s = Stencil(-2:2, (1.,2.,2.,3.,4.))
@@ -44,6 +46,52 @@
         @test promote(Stencil(1,1;center=1), Stencil(2.,2.;center=2)) == (Stencil(1.,1.;center=1), Stencil(2.,2.;center=2))
     end
 
+    @testset "apply_stencil" begin
+        v = [1, 2, 4, 8, 16, 32, 64, 128]
+        s = Stencil(1,2,3,4, center = 2)
+        @test apply_stencil(s,v, 2) == v[1] + 2*v[2] + 3*v[3] + 4*v[4]
+        @test apply_stencil(s,v, 4) == v[3] + 2*v[4] + 3*v[5] + 4*v[6]
+        @test apply_stencil_backwards(s,v, 3) == 4*v[1] + 3*v[2] + 2*v[3] + 1*v[4]
+        @test apply_stencil_backwards(s,v, 7) == 4*v[5] + 3*v[6] + 2*v[7] + 1*v[8]
+        @test apply_stencil(s,v, 2) isa Int
+        @test apply_stencil_backwards(s,v, 7) isa Int
+
+        v = [1, 2, 4, 8, 16, 32, 64, 128]
+        s = Stencil(1.,2.,3.,4., center = 2)
+        @test apply_stencil(s,v, 4) == v[3] + 2. *v[4] + 3. *v[5] + 4. *v[6]
+        @test apply_stencil_backwards(s,v, 7) == 4. *v[5] + 3. *v[6] + 2. *v[7] + v[8]
+        @test apply_stencil(s,v, 2) isa Float64
+        @test apply_stencil_backwards(s,v, 7) isa Float64
+
+        v = [1., 2., 4., 8., 16., 32., 64., 128.]
+        s = Stencil(1,2,3,4, center = 2)
+        @test apply_stencil(s,v, 2) == v[1] + 2*v[2] + 3*v[3] + 4*v[4]
+        @test apply_stencil_backwards(s,v, 3) == 4*v[1] + 3*v[2] + 2*v[3] + 1*v[4]
+        @test apply_stencil(s,v, 2) isa Float64
+        @test apply_stencil_backwards(s,v, 3) isa Float64
+
+        v = [@SVector[1, 2], @SVector[3, 4], @SVector[5, 6], @SVector[7, 8]]
+        s = Stencil(1,2, center = 1)
+        @test apply_stencil(s,v,1) == @SVector[7, 10]
+        @test apply_stencil_backwards(s,v,3) == @SVector[11, 14]
+        @test apply_stencil(s,v,1) isa SVector{2, Int}
+        @test apply_stencil_backwards(s,v,3) isa SVector{2, Int}
+
+        v = [@SVector[1., 2.], @SVector[3., 4.], @SVector[5., 6.], @SVector[7., 8.]]
+        s = Stencil(1,2, center = 1)
+        @test apply_stencil(s,v,1) == @SVector[7., 10.]
+        @test apply_stencil_backwards(s,v,3) == @SVector[11., 14.]
+        @test apply_stencil(s,v,1) isa SVector{2, Float64}
+        @test apply_stencil_backwards(s,v,3) isa SVector{2, Float64}
+
+        v = [@SVector[1, 2], @SVector[3, 4], @SVector[5, 6], @SVector[7, 8]]
+        s = Stencil(1.,2., center = 1)
+        @test apply_stencil(s,v,1) == @SVector[7., 10.]
+        @test apply_stencil_backwards(s,v,3) == @SVector[11., 14.]
+        @test apply_stencil(s,v,1) isa SVector{2, Float64}
+        @test apply_stencil_backwards(s,v,3) isa SVector{2, Float64}
+    end
+
     @testset "type stability" begin
         s_int = CenteredStencil(1,2,3)
         s_float = CenteredStencil(1.,2.,3.)
@@ -155,6 +203,27 @@
 
         @test SbpOperators.apply_stencil(ns, c, v, 4) == 5*7 + 11*11 + 6*13
         @test SbpOperators.apply_stencil_backwards(ns, c, v, 4) == -3*3 - 7*5 - 4*7
+
+        # Different types in vector and stencil
+        ns = NestedStencil((-1.,1.,0.),(-1.,0.,1.),(0.,-2.,2.), center=2)
+        @test SbpOperators.apply_inner_stencils(ns, c, 4) isa Stencil{Float64, 3}
+        @test SbpOperators.apply_inner_stencils(ns, c, 4) == Stencil(4.,9.,10.; center=2)
+        @test SbpOperators.apply_inner_stencils_backwards(ns, c, 4) isa Stencil{Float64, 3}
+        @test SbpOperators.apply_inner_stencils_backwards(ns, c, 4) == Stencil(-5.,-9.,-8.; center=2)
+
+        @test SbpOperators.apply_stencil(ns, c, v, 4) isa Float64
+        @test SbpOperators.apply_stencil(ns, c, v, 4) == 193.
+        @test SbpOperators.apply_stencil_backwards(ns, c, v, 4) isa Float64
+        @test SbpOperators.apply_stencil_backwards(ns, c, v, 4) == -158.
+
+        # Arrays of vectors
+        ns = NestedStencil((-1.,1.,0.),(-1.,0.,1.),(0.,-2.,2.), center=2)
+        c = [  1,  3,  6, 10]
+        v = [@SVector[1, 2], @SVector[3, 4], @SVector[5, 6], @SVector[7, 8]]
+        @test SbpOperators.apply_stencil(ns, c, v, 2) isa SVector{2,Float64}
+        @test SbpOperators.apply_stencil(ns, c, v, 2) == 2v[1] + 5v[2] + 6v[3]
+        @test SbpOperators.apply_stencil_backwards(ns, c, v, 2) isa SVector{2,Float64}
+        @test SbpOperators.apply_stencil_backwards(ns, c, v, 2) == -4v[1] - 5v[2] - 3v[3]
     end
 
     @testset "type stability" begin
--- a/test/SbpOperators/volumeops/constant_interior_scaling_operator_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/volumeops/constant_interior_scaling_operator_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,9 +1,9 @@
 using Test
 
-using Sbplib.LazyTensors
-using Sbplib.SbpOperators
-import Sbplib.SbpOperators: ConstantInteriorScalingOperator
-using Sbplib.Grids
+using Diffinitive.LazyTensors
+using Diffinitive.SbpOperators
+import Diffinitive.SbpOperators: ConstantInteriorScalingOperator
+using Diffinitive.Grids
 
 @testset "ConstantInteriorScalingOperator" begin
     @test ConstantInteriorScalingOperator(1, (2,3), 10) isa ConstantInteriorScalingOperator{Int,2}
@@ -33,7 +33,7 @@
     @test_throws DomainError ConstantInteriorScalingOperator(4,(2,3), 3)
 
     @testset "Grid constructor" begin
-        g = equidistant_grid(11, 0., 2.)
+        g = equidistant_grid(0., 2., 11)
         @test ConstantInteriorScalingOperator(g, 3., (.1,.2)) isa ConstantInteriorScalingOperator{Float64}
     end
 end
--- a/test/SbpOperators/volumeops/derivatives/dissipation_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/volumeops/derivatives/dissipation_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,33 +1,22 @@
 using Test
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
-
-using Sbplib.SbpOperators: Stencil
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
 
-using Sbplib.SbpOperators: dissipation_interior_weights
-using Sbplib.SbpOperators: dissipation_interior_stencil, dissipation_transpose_interior_stencil
-using Sbplib.SbpOperators: midpoint, midpoint_transpose
-using Sbplib.SbpOperators: dissipation_lower_closure_size, dissipation_upper_closure_size
-using Sbplib.SbpOperators: dissipation_lower_closure_stencils,dissipation_upper_closure_stencils
-using Sbplib.SbpOperators: dissipation_transpose_lower_closure_stencils, dissipation_transpose_upper_closure_stencils
+using Diffinitive.SbpOperators: Stencil
 
-"""
-    monomial(x,k)
+using Diffinitive.SbpOperators: dissipation_interior_weights
+using Diffinitive.SbpOperators: dissipation_interior_stencil, dissipation_transpose_interior_stencil
+using Diffinitive.SbpOperators: midpoint, midpoint_transpose
+using Diffinitive.SbpOperators: dissipation_lower_closure_size, dissipation_upper_closure_size
+using Diffinitive.SbpOperators: dissipation_lower_closure_stencils,dissipation_upper_closure_stencils
+using Diffinitive.SbpOperators: dissipation_transpose_lower_closure_stencils, dissipation_transpose_upper_closure_stencils
 
-Evaluates ``x^k/k!` with the convetion that it is ``0`` for all ``k<0``.
-Has the property that ``d/dx monomial(x,k) = monomial(x,k-1)``
-"""
-function monomial(x,k)
-    if k < 0
-        return zero(x)
-    end
-    x^k/factorial(k)
-end
 
 @testset "undivided_skewed04" begin
-    g = equidistant_grid(20, 0., 11.)
+    monomial(x,k) = k < 0 ? zero(x) : x^k/factorial(k)
+    g = equidistant_grid(0., 11., 20)
     D,Dᵀ = undivided_skewed04(g, 1)
 
     @test D isa LazyTensor{Float64,1,1}
@@ -35,7 +24,7 @@
 
      @testset "Accuracy conditions" begin
         N = 20
-        g = equidistant_grid(N, 0//1,2//1)
+        g = equidistant_grid(0//1, 2//1, N)
         h = only(spacing(g))
         @testset "D_$p" for p ∈ [1,2,3,4]
             D,Dᵀ = undivided_skewed04(g, p)
@@ -67,7 +56,7 @@
             return Dmat
         end
 
-        g = equidistant_grid(11, 0., 1.)
+        g = equidistant_grid(0., 1., 11)
         @testset "D_$p" for p ∈ [1,2,3,4]
             D,Dᵀ = undivided_skewed04(g, p)
 
@@ -80,7 +69,7 @@
 
     @testset "2D" begin
         N = 20
-        g = equidistant_grid((N,2N), (0,0), (2,1))
+        g = equidistant_grid((0,0), (2,1), N, 2N)
         h = spacing.(g.grids)
 
         D,Dᵀ = undivided_skewed04(g, 3, 2)
--- a/test/SbpOperators/volumeops/derivatives/first_derivative_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/volumeops/derivatives/first_derivative_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,31 +1,18 @@
 using Test
 
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
-
-using Sbplib.SbpOperators: closure_size, Stencil, VolumeOperator
-
-"""
-    monomial(x,k)
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
 
-Evaluates ``x^k/k!` with the convetion that it is ``0`` for all ``k<0``.
-Has the property that ``d/dx monomial(x,k) = monomial(x,k-1)``
-"""
-function monomial(x,k)
-    if k < 0
-        return zero(x)
-    end
-    x^k/factorial(k)
-end
+using Diffinitive.SbpOperators: closure_size, Stencil, VolumeOperator
 
 @testset "first_derivative" begin
     @testset "Constructors" begin
         stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order=2)
 
-        g₁ = equidistant_grid(11, 0., 1.)
-        g₂ = equidistant_grid((11,14), (0.,1.), (1.,3.))
+        g₁ = equidistant_grid(0., 1., 11)
+        g₂ = equidistant_grid((0.,1.), (1.,3.), 11, 14)
         
         @test first_derivative(g₁, stencil_set) isa LazyTensor{Float64,1,1}
         @test first_derivative(g₂, stencil_set, 2) isa LazyTensor{Float64,2,2}
@@ -38,7 +25,9 @@
 
     @testset "Accuracy conditions" begin
         N = 20
-        g = equidistant_grid(N, 0//1,2//1)
+        g = equidistant_grid(0//1, 2//1, N)
+
+        monomial(x,k) = k < 0 ? zero(x) : x^k/factorial(k)
         @testset for order ∈ [2,4]
             stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order)
             D₁ = first_derivative(g, stencil_set)
@@ -68,7 +57,7 @@
 
     @testset "Accuracy on function" begin
         @testset "1D" begin
-            g = equidistant_grid(30, 0.,1.)
+            g = equidistant_grid(0., 1., 30)
             v = eval_on(g, x->exp(x))
             @testset for (order, tol) ∈ [(2, 6e-3),(4, 2e-4)]
                 stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order)
@@ -79,7 +68,7 @@
         end
 
         @testset "2D" begin
-            g = equidistant_grid((30,60), (0.,0.),(1.,2.))
+            g = equidistant_grid((0.,0.),(1.,2.), 30, 60)
             v = eval_on(g, (x,y)->exp(0.8x+1.2*y))
             @testset for (order, tol) ∈ [(2, 6e-3),(4, 3e-4)]
                 stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order)
--- a/test/SbpOperators/volumeops/derivatives/second_derivative_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/volumeops/derivatives/second_derivative_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,10 +1,10 @@
 using Test
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
 
-import Sbplib.SbpOperators.VolumeOperator
+import Diffinitive.SbpOperators.VolumeOperator
 
 # TODO: Refactor these test to look more like the tests in first_derivative_test.jl.
 
@@ -15,8 +15,8 @@
     closure_stencils = parse_stencil.(stencil_set["D2"]["closure_stencils"])
     Lx = 3.5
     Ly = 3.
-    g_1D = equidistant_grid(121, 0.0, Lx)
-    g_2D = equidistant_grid((121,123), (0.0, 0.0), (Lx, Ly))
+    g_1D = equidistant_grid(0.0, Lx, 121)
+    g_2D = equidistant_grid((0.0, 0.0), (Lx, Ly), 121, 123)
 
     @testset "Constructors" begin
         @testset "1D" begin
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/SbpOperators/volumeops/derivatives/second_derivative_variable_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,247 @@
+using Test
+
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
+using Diffinitive.SbpOperators
+using Diffinitive.RegionIndices
+using Diffinitive.SbpOperators: NestedStencil, CenteredNestedStencil, SecondDerivativeVariable
+
+using LinearAlgebra
+
+@testset "second_derivative_variable" begin
+    stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order=2)
+
+    @testset "1D" begin
+        g = equidistant_grid(0., 1., 11)
+        c = [  1.,  3.,  6., 10., 15., 21., 28., 36., 45., 55., 66.]
+
+        @testset "checking c" begin
+            c_short = rand(5)
+            c_long = rand(16)
+            c_higher_dimension = rand(11,11)
+
+            @test_throws DimensionMismatch("the size (5,) of the coefficient does not match the size (11,) of the grid") second_derivative_variable(g, c_short, stencil_set)
+            @test_throws DimensionMismatch("the size (16,) of the coefficient does not match the size (11,) of the grid") second_derivative_variable(g, c_long, stencil_set)
+            @test_throws ArgumentError("The coefficient has dimension 2 while the grid is dimension 1") second_derivative_variable(g, c_higher_dimension, stencil_set)
+        end
+
+        @testset "application" begin
+            function apply_to_functions(; v, c)
+                g = equidistant_grid(0., 10., 11) # h = 1
+                c̄ = eval_on(g,c)
+                v̄ = eval_on(g,v)
+
+                D₂ᶜ = second_derivative_variable(g, c̄, stencil_set)
+                return D₂ᶜ*v̄
+            end
+
+            @test apply_to_functions(v=x->1.,  c=x-> -1.) == zeros(11)
+            @test apply_to_functions(v=x->1.,  c=x-> -x ) == zeros(11)
+            @test apply_to_functions(v=x->x,   c=x->  1.) == zeros(11)
+            @test apply_to_functions(v=x->x,   c=x-> -x ) == -ones(11)
+            @test apply_to_functions(v=x->x^2, c=x->  1.) == 2ones(11)
+        end
+    end
+
+    @testset "2D" begin
+        g = equidistant_grid((0.,0.), (10.,8.), 11, 9) # h = 1
+        c = eval_on(g, (x,y)->x+y)
+
+        @testset "application" begin
+            function apply_to_functions(dir; v, c)
+                g = equidistant_grid((0.,0.), (10.,8.), 11, 9) # h = 1
+                c̄ = eval_on(g,c)
+                v̄ = eval_on(g,v)
+
+                D₂ᶜ = second_derivative_variable(g, c̄, stencil_set, dir)
+                return D₂ᶜ*v̄
+            end
+
+            # x-direction
+            @test apply_to_functions(1,v=(x,y)->1.,  c=(x,y)-> -1.) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->1.,  c=(x,y)->- x ) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->x,   c=(x,y)->  1.) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->x,   c=(x,y)-> -x ) == -ones(11,9)
+            @test apply_to_functions(1,v=(x,y)->x^2, c=(x,y)->  1.) == 2ones(11,9)
+
+            @test apply_to_functions(1,v=(x,y)->1.,  c=(x,y)->- y ) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->y,   c=(x,y)->  1.) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->y,   c=(x,y)-> -y ) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->y^2, c=(x,y)->  1.) == zeros(11,9)
+
+            # y-direction
+            @test apply_to_functions(2,v=(x,y)->1.,  c=(x,y)-> -1.) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->1.,  c=(x,y)->- y ) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->y,   c=(x,y)->  1.) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->y,   c=(x,y)-> -y ) == -ones(11,9)
+            @test apply_to_functions(2,v=(x,y)->y^2, c=(x,y)->  1.) == 2ones(11,9)
+
+            @test apply_to_functions(2,v=(x,y)->1.,  c=(x,y)->- x ) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->x,   c=(x,y)->  1.) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->x,   c=(x,y)-> -x ) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->x^2, c=(x,y)->  1.) == zeros(11,9)
+
+
+            @testset "standard diagonal operators" begin
+                c(x,y) = exp(x) + exp(1.5(1-y))
+                v(x,y) = sin(x) + cos(1.5(1-y))
+
+                Dxv(x,y) = cos(x)*exp(x) - (exp(x) + exp(1.5 - 1.5y))*sin(x)
+                Dyv(x,y) = -1.5(1.5exp(x) + 1.5exp(1.5 - 1.5y))*cos(1.5 - 1.5y) - 2.25exp(1.5 - 1.5y)*sin(1.5 - 1.5y)
+
+                g₁ = equidistant_grid((0.,0.), (1.,2.), 60, 67)
+                g₂ = refine(g₁,2)
+
+                c̄₁ = eval_on(g₁, c)
+                c̄₂ = eval_on(g₂, c)
+
+                v̄₁ = eval_on(g₁, v)
+                v̄₂ = eval_on(g₂, v)
+
+
+                function convergence_rate_estimate(stencil_set, dir, Dv_true)
+                    D₁ = second_derivative_variable(g₁, c̄₁, stencil_set, dir)
+                    D₂ = second_derivative_variable(g₂, c̄₂, stencil_set, dir)
+
+                    Dv̄₁ = D₁*v̄₁
+                    Dv̄₂ = D₂*v̄₂
+
+                    Dv₁ = eval_on(g₁,Dv_true)
+                    Dv₂ = eval_on(g₂,Dv_true)
+
+                    e₁ = norm(Dv̄₁ - Dv₁)/norm(Dv₁)
+                    e₂ = norm(Dv̄₂ - Dv₂)/norm(Dv₂)
+
+                    return log2(e₁/e₂)
+                end
+
+                stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order = 2)
+                @test convergence_rate_estimate(stencil_set, 1, Dxv) ≈ 1.5 rtol = 1e-1
+                @test convergence_rate_estimate(stencil_set, 2, Dyv) ≈ 1.5 rtol = 1e-1
+
+                stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order = 4)
+                @test convergence_rate_estimate(stencil_set, 1, Dxv) ≈ 2.5 rtol = 1e-1
+                @test convergence_rate_estimate(stencil_set, 2, Dyv) ≈ 2.5 rtol = 2e-1
+            end
+        end
+    end
+end
+
+
+@testset "SecondDerivativeVariable" begin
+    interior_stencil = CenteredNestedStencil((1/2, 1/2, 0.),(-1/2, -1., -1/2),( 0., 1/2, 1/2))
+    closure_stencils = (
+        NestedStencil(( 2.,  -1., 0.),(-3., 1.,  0.), (1., 0., 0.), center = 1),
+    )
+
+    @testset "1D" begin
+        c = [  1.,  3.,  6., 10., 15., 21., 28., 36., 45., 55., 66.]
+        @testset "Constructors" begin
+            @test SecondDerivativeVariable(c, interior_stencil, closure_stencils, 1) isa LazyTensor
+
+            D₂ᶜ = SecondDerivativeVariable(c, interior_stencil, closure_stencils, 1)
+            @test range_dim(D₂ᶜ) == 1
+            @test domain_dim(D₂ᶜ) == 1
+
+        end
+
+        @testset "sizes" begin
+            D₂ᶜ = SecondDerivativeVariable(c, interior_stencil, closure_stencils, 1)
+            @test closure_size(D₂ᶜ) == 1
+            @test range_size(D₂ᶜ) == (11,)
+            @test domain_size(D₂ᶜ) == (11,)
+        end
+
+        @testset "application" begin
+
+            function apply_to_functions(; v, c)
+                g = equidistant_grid(0., 10., 11) # h = 1
+                c̄ = eval_on(g,c)
+                v̄ = eval_on(g,v)
+
+                D₂ᶜ = SecondDerivativeVariable(c̄, interior_stencil, closure_stencils, 1)
+                return D₂ᶜ*v̄
+            end
+
+            @test apply_to_functions(v=x->1.,  c=x-> -1.) == zeros(11)
+            @test apply_to_functions(v=x->1.,  c=x-> -x ) == zeros(11)
+            @test apply_to_functions(v=x->x,   c=x->  1.) == zeros(11)
+            @test apply_to_functions(v=x->x,   c=x-> -x ) == -ones(11)
+            @test apply_to_functions(v=x->x^2, c=x->  1.) == 2ones(11)
+        end
+
+        @testset "type stability" begin
+            g = equidistant_grid(0., 10., 11) # h = 1
+            c̄ = eval_on(g,x-> -1)
+            v̄ = eval_on(g,x->1.)
+
+            D₂ᶜ = SecondDerivativeVariable(c̄, interior_stencil, closure_stencils, 1)
+
+            @inferred SbpOperators.apply_lower(D₂ᶜ, v̄, 1)
+            @inferred SbpOperators.apply_interior(D₂ᶜ, v̄, 5)
+            @inferred SbpOperators.apply_upper(D₂ᶜ, v̄, 11)
+            @inferred (D₂ᶜ*v̄)[Index(1,Lower)]
+        end
+    end
+
+    @testset "2D" begin
+        g = equidistant_grid((0.,0.), (10.,8.), 11, 9) # h = 1
+        c = eval_on(g, (x,y)->x+y)
+        @testset "Constructors" begin
+            @test SecondDerivativeVariable(c, interior_stencil, closure_stencils, 1) isa LazyTensor
+            @test SecondDerivativeVariable(c, interior_stencil, closure_stencils, 2) isa LazyTensor
+
+            D₂ᶜ = SecondDerivativeVariable(c, interior_stencil, closure_stencils, 1)
+            @test range_dim(D₂ᶜ) == 2
+            @test domain_dim(D₂ᶜ) == 2
+        end
+
+        @testset "sizes" begin
+            D₂ᶜ = SecondDerivativeVariable(c, interior_stencil, closure_stencils, 1)
+            @test range_size(D₂ᶜ) == (11,9)
+            @test domain_size(D₂ᶜ) == (11,9)
+            @test closure_size(D₂ᶜ) == 1
+
+            D₂ᶜ = SecondDerivativeVariable(c, interior_stencil, closure_stencils, 2)
+            @test range_size(D₂ᶜ) == (11,9)
+            @test domain_size(D₂ᶜ) == (11,9)
+            @test closure_size(D₂ᶜ) == 1
+        end
+
+        @testset "application" begin
+            function apply_to_functions(dir; v, c)
+                g = equidistant_grid((0.,0.), (10.,8.), 11, 9) # h = 1
+                c̄ = eval_on(g,c)
+                v̄ = eval_on(g,v)
+
+                D₂ᶜ = SecondDerivativeVariable(c̄, interior_stencil, closure_stencils, dir)
+                return D₂ᶜ*v̄
+            end
+
+            # x-direction
+            @test apply_to_functions(1,v=(x,y)->1.,  c=(x,y)-> -1.) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->1.,  c=(x,y)->- x ) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->x,   c=(x,y)->  1.) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->x,   c=(x,y)-> -x ) == -ones(11,9)
+            @test apply_to_functions(1,v=(x,y)->x^2, c=(x,y)->  1.) == 2ones(11,9)
+
+            @test apply_to_functions(1,v=(x,y)->1.,  c=(x,y)->- y ) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->y,   c=(x,y)->  1.) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->y,   c=(x,y)-> -y ) == zeros(11,9)
+            @test apply_to_functions(1,v=(x,y)->y^2, c=(x,y)->  1.) == zeros(11,9)
+
+            # y-direction
+            @test apply_to_functions(2,v=(x,y)->1.,  c=(x,y)-> -1.) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->1.,  c=(x,y)->- y ) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->y,   c=(x,y)->  1.) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->y,   c=(x,y)-> -y ) == -ones(11,9)
+            @test apply_to_functions(2,v=(x,y)->y^2, c=(x,y)->  1.) == 2ones(11,9)
+
+            @test apply_to_functions(2,v=(x,y)->1.,  c=(x,y)->- x ) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->x,   c=(x,y)->  1.) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->x,   c=(x,y)-> -x ) == zeros(11,9)
+            @test apply_to_functions(2,v=(x,y)->x^2, c=(x,y)->  1.) == zeros(11,9)
+        end
+    end
+end
+
--- a/test/SbpOperators/volumeops/inner_products/inner_product_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/volumeops/inner_products/inner_product_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,18 +1,18 @@
 using Test
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
 
-import Sbplib.SbpOperators.ConstantInteriorScalingOperator
+import Diffinitive.SbpOperators.ConstantInteriorScalingOperator
 
 @testset "Diagonal-stencil inner_product" begin
     Lx = π/2.
     Ly = Float64(π)
     Lz = 1.
-    g_1D = equidistant_grid(77, 0.0, Lx)
-    g_2D = equidistant_grid((77,66), (0.0, 0.0), (Lx,Ly))
-    g_3D = equidistant_grid((10,10, 10), (0.0, 0.0, 0.0), (Lx,Ly,Lz))
+    g_1D = equidistant_grid(0.0, Lx, 77)
+    g_2D = equidistant_grid((0.0, 0.0), (Lx,Ly), 77, 66)
+    g_3D = equidistant_grid((0.0, 0.0, 0.0), (Lx,Ly,Lz), 10, 10, 10)
     @testset "inner_product" begin
         stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order=4)
         @testset "0D" begin
--- a/test/SbpOperators/volumeops/inner_products/inverse_inner_product_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/volumeops/inner_products/inverse_inner_product_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,16 +1,16 @@
 using Test
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
 
-import Sbplib.SbpOperators.ConstantInteriorScalingOperator
+import Diffinitive.SbpOperators.ConstantInteriorScalingOperator
 
 @testset "Diagonal-stencil inverse_inner_product" begin
     Lx = π/2.
     Ly = Float64(π)
-    g_1D = equidistant_grid(77, 0.0, Lx)
-    g_2D = equidistant_grid((77,66), (0.0, 0.0), (Lx,Ly))
+    g_1D = equidistant_grid(0.0, Lx, 77)
+    g_2D = equidistant_grid((0.0, 0.0), (Lx,Ly), 77, 66)
     @testset "inverse_inner_product" begin
         stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order=4)
         @testset "0D" begin
--- a/test/SbpOperators/volumeops/laplace/laplace_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/volumeops/laplace/laplace_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,15 +1,15 @@
 using Test
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
 
 @testset "Laplace" begin
     # Default stencils (4th order)
     operator_path = sbp_operators_path()*"standard_diagonal.toml"
     stencil_set = read_stencil_set(operator_path; order=4)
-    g_1D = equidistant_grid(101, 0.0, 1.)
-    g_3D = equidistant_grid((51,101,52), (0.0, -1.0, 0.0), (1., 1., 1.))
+    g_1D = equidistant_grid(0.0, 1., 101)
+    g_3D = equidistant_grid((0.0, -1.0, 0.0), (1., 1., 1.), 51, 101, 52)
 
     @testset "Constructors" begin
         @testset "1D" begin
@@ -69,8 +69,8 @@
 @testset "laplace" begin
     operator_path = sbp_operators_path()*"standard_diagonal.toml"
     stencil_set = read_stencil_set(operator_path; order=4)
-    g_1D = equidistant_grid(101, 0.0, 1.)
-    g_3D = equidistant_grid((51,101,52), (0.0, -1.0, 0.0), (1., 1., 1.))
+    g_1D = equidistant_grid(0.0, 1., 101)
+    g_3D = equidistant_grid((0.0, -1.0, 0.0), (1., 1., 1.), 51, 101, 52)
 
     @testset "1D" begin
         Δ = laplace(g_1D, stencil_set)
@@ -88,3 +88,66 @@
     end
 end
 
+@testset "sat_tensors" begin
+    # TODO: The following tests should be implemented
+    #       1. Symmetry D'H == H'D (test_broken below)
+    #       2. Test eigenvalues of and/or solution to Poisson
+    #       3. Test tuning of Dirichlet conditions
+    #
+    #       These tests are likely easiest to implement once
+    #       we have support for generating matrices from tensors.
+
+    operator_path = sbp_operators_path()*"standard_diagonal.toml"
+    orders = (2,4)
+    tols = (5e-2,5e-4)
+    sz = (201,401)
+    g = equidistant_grid((0.,0.), (1.,1.), sz...)
+    
+    # Verify implementation of sat_tensors by testing accuracy and symmetry (TODO) 
+    # of the operator D = Δ + SAT, where SAT is the tensor composition of the 
+    # operators from sat_tensor. Note that SAT*u should approximate 0 for the 
+    # conditions chosen.
+
+    @testset "Dirichlet" begin
+        for (o, tol) ∈ zip(orders,tols)
+            stencil_set = read_stencil_set(operator_path; order=o)
+            Δ = Laplace(g, stencil_set)
+            H = inner_product(g, stencil_set)
+            u = collect(eval_on(g, (x,y) -> sin(π*x)sin(2*π*y)))
+            Δu = collect(eval_on(g, (x,y) -> -5*π^2*sin(π*x)sin(2*π*y)))
+            D = Δ 
+            for id ∈ boundary_identifiers(g)
+                D = D + foldl(∘, sat_tensors(Δ, g, DirichletCondition(0., id)))
+            end
+            e = D*u .- Δu
+            # Accuracy
+            @test sqrt(sum(H*e.^2)) ≈ 0 atol = tol
+            # Symmetry
+            r = randn(size(u))
+            @test_broken (D'∘H - H∘D)*r .≈ 0 atol = 1e-13 # TODO: Need to implement apply_transpose for D.
+        end
+    end
+
+    @testset "Neumann" begin
+        @testset "Dirichlet" begin
+            for (o, tol) ∈ zip(orders,tols)
+                stencil_set = read_stencil_set(operator_path; order=o)
+                Δ = Laplace(g, stencil_set)
+                H = inner_product(g, stencil_set)
+                u = collect(eval_on(g, (x,y) -> cos(π*x)cos(2*π*y)))
+                Δu = collect(eval_on(g, (x,y) -> -5*π^2*cos(π*x)cos(2*π*y)))
+                D = Δ 
+                for id ∈ boundary_identifiers(g)
+                    D = D + foldl(∘, sat_tensors(Δ, g, NeumannCondition(0., id)))
+                end
+                e = D*u .- Δu
+                # Accuracy
+                @test sqrt(sum(H*e.^2)) ≈ 0 atol = tol
+                # Symmetry
+                r = randn(size(u))
+                @test_broken (D'∘H - H∘D)*r .≈ 0 atol = 1e-13 # TODO: Need to implement apply_transpose for D.
+            end
+        end
+    end
+end
+
--- a/test/SbpOperators/volumeops/stencil_operator_distinct_closures_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/volumeops/stencil_operator_distinct_closures_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,14 +1,14 @@
 using Test
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.LazyTensors
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.LazyTensors
 
-import Sbplib.SbpOperators.Stencil
-import Sbplib.SbpOperators.StencilOperatorDistinctClosures
+import Diffinitive.SbpOperators.Stencil
+import Diffinitive.SbpOperators.StencilOperatorDistinctClosures
 
 @testset "StencilOperatorDistinctClosures" begin
-    g = equidistant_grid(11, 0., 1.)
+    g = equidistant_grid(0., 1., 11)
 
     lower_closure = (
         Stencil(-1,1, center=1),
--- a/test/SbpOperators/volumeops/volume_operator_test.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/SbpOperators/volumeops/volume_operator_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,23 +1,23 @@
 using Test
 
-using Sbplib.SbpOperators
-using Sbplib.Grids
-using Sbplib.RegionIndices
-using Sbplib.LazyTensors
+using Diffinitive.SbpOperators
+using Diffinitive.Grids
+using Diffinitive.RegionIndices
+using Diffinitive.LazyTensors
 
-import Sbplib.SbpOperators.Stencil
-import Sbplib.SbpOperators.VolumeOperator
-import Sbplib.SbpOperators.odd
-import Sbplib.SbpOperators.even
+import Diffinitive.SbpOperators.Stencil
+import Diffinitive.SbpOperators.VolumeOperator
+import Diffinitive.SbpOperators.odd
+import Diffinitive.SbpOperators.even
 
 
 @testset "VolumeOperator" begin
     inner_stencil = CenteredStencil(1/4, 2/4, 1/4)
     closure_stencils = (Stencil(1/2, 1/2; center=1), Stencil(2.,1.; center=2))
-    g = equidistant_grid(11,0.,1.)
+    g = equidistant_grid(0.,1., 11)
 
     @testset "Constructors" begin
-        op = VolumeOperator(inner_stencil,closure_stencils,(11,),even)
+        op = VolumeOperator(inner_stencil, closure_stencils, 11, even)
         @test op == VolumeOperator(g,inner_stencil,closure_stencils,even)
         @test op isa LazyTensor{T,1,1} where T
     end
--- a/test/StaticDicts/StaticDicts_test.jl	Wed May 31 08:59:34 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-using Test
-using Sbplib.StaticDicts
-
-@testset "StaticDicts" begin
-
-@testset "StaticDict" begin
-    @testset "constructor" begin
-        @test (StaticDict{Int,Int,N} where N) <: AbstractDict
-
-        d = StaticDict(1=>2, 3=>4)
-        @test d isa StaticDict{Int,Int}
-        @test d[1] == 2
-        @test d[3] == 4
-
-        @test StaticDict((1=>2, 3=>4)) == d
-
-        @test StaticDict() isa StaticDict
-        @test StaticDict{Int,String}() isa StaticDict{Int,String,0}
-
-        @test StaticDict(1=>3, 2=>4.) isa StaticDict{Int,Real}
-        @test StaticDict(1. =>3, 2=>4) isa StaticDict{Real,Int}
-        @test StaticDict(1. =>3, 2=>4.) isa StaticDict{Real,Real}
-
-        @test_throws DomainError StaticDict(1=>3, 1=>3)
-    end
-
-    @testset "length" begin
-        @test length(StaticDict()) == 0
-        @test length(StaticDict(1=>1)) == 1
-        @test length(StaticDict(1=>1, 2=>2)) == 2
-    end
-
-    @testset "equality" begin
-        @test StaticDict(1=>1) == StaticDict(1=>1)
-        @test StaticDict(2=>1) != StaticDict(1=>1)
-        @test StaticDict(1=>2) != StaticDict(1=>1)
-
-        @test StaticDict(1=>1) === StaticDict(1=>1) #not true for a regular Dict
-        @test StaticDict(2=>1) !== StaticDict(1=>1)
-        @test StaticDict(1=>2) !== StaticDict(1=>1)
-    end
-
-    @testset "get" begin
-        d = StaticDict(1=>2, 3=>4)
-
-        @test get(d,1,6) == 2
-        @test get(d,3,6) == 4
-        @test get(d,5,6) == 6
-    end
-
-    @testset "iterate" begin
-        pairs = [1=>2, 3=>4, 5=>6]
-
-        d = StaticDict(pairs...)
-        @test collect(d) == pairs
-    end
-
-    @testset "merge" begin
-        @test merge(
-            StaticDict(1=>3, 2=> 4),
-            StaticDict(3=>5,4=>6)) == StaticDict(
-                1=>3, 2=>4, 3=>5, 4=>6
-            )
-        @test_throws DomainError merge(StaticDict(1=>3),StaticDict(1=>3))
-    end
-end
-
-end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ext/sparse_array_kit_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,35 @@
+using Test
+
+using Diffinitive
+using Diffinitive.Grids
+using Diffinitive.SbpOperators
+
+using SparseArrayKit
+using Tokens
+using Tullio
+
+
+@testset "SparseArray" begin
+    g = equidistant_grid((0,0),(1,2), 20,30)
+    stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order=4)
+
+
+    @testset let Δ = laplace(g, stencil_set), M = SparseArray(Δ)
+        @test ndims(M) == 4
+        @test size(M) == (20,30,20,30)
+
+        v = rand(size(g)...)
+        @tullio Mv[i,j] := M[i,j,k,l]*v[k,l]
+
+        @test Mv ≈ Δ*v
+    end
+
+    @testset let dₙ = normal_derivative(g, stencil_set,CartesianBoundary{1,LowerBoundary}()), M = SparseArray(dₙ)
+        @test ndims(M) == 3
+        @test size(M) == (30,20,30)
+
+        v = rand(size(g)...)
+        @tullio Mv[i] := M[i,j,k]*v[j,k]
+        @test Mv ≈ dₙ*v
+    end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ext/sparse_arrays_test.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,34 @@
+using Test
+
+using Diffinitive
+using Diffinitive.Grids
+using Diffinitive.SbpOperators
+
+using SparseArrays
+using Tokens
+
+
+@testset "SparseArray" begin
+    g = equidistant_grid((0,0),(1,2), 20,30)
+    stencil_set = read_stencil_set(sbp_operators_path()*"standard_diagonal.toml"; order=4)
+
+
+    @testset let Δ = laplace(g, stencil_set), M = sparse(Δ)
+        @test ndims(M) == 2
+        @test size(M) == (20*30,20*30)
+
+        v = rand(size(g)...)
+
+        Mv = M*reshape(v,:)
+        @test Mv ≈ reshape(Δ*v,:)
+    end
+
+    @testset let dₙ = normal_derivative(g, stencil_set,CartesianBoundary{1,LowerBoundary}()), M = sparse(dₙ)
+        @test ndims(M) == 2
+        @test size(M) == (30,20*30)
+
+        v = rand(size(g)...)
+        Mv = M*reshape(v,:)
+        @test Mv ≈ reshape(dₙ*v,:)
+    end
+end
--- a/test/runtests.jl	Wed May 31 08:59:34 2023 +0200
+++ b/test/runtests.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -1,4 +1,7 @@
+using Diffinitive
 using Test
+using JET
+using Aqua
 using Glob
 
 """
@@ -18,7 +21,7 @@
     run_testfiles(".", globs)
 end
 
-function  run_testfiles(path, globs)
+function run_testfiles(path, globs)
     for name ∈ readdir(path)
         filepath = joinpath(path, name)
 
@@ -29,25 +32,44 @@
         end
 
         if endswith(name, "_test.jl") && any(occursin.(globs, filepath))
-            printstyled("Running "; bold=true, color=:green)
-            print(filepath)
-
-            t_start = time()
-            @testset "$name" begin
-                include(filepath)
+            log_and_time(filepath) do
+                @testset "$name" begin
+                    include(filepath)
+                end
             end
-            t_end = time()
-
-            Δt = t_end - t_start
-            printstyled(" ($(round(Δt, digits=2)) s)"; color=:light_black)
-            println()
         end
     end
 end
 
-testsetname = isempty(ARGS) ? "Sbplib.jl" : "["*join(ARGS, ", ")*"]"
+function log_and_time(f, msg)
+    printstyled("Running "; bold=true, color=:green)
+    print(msg)
+
+    t_start = time()
+    f()
+    t_end = time()
+    Δt = t_end - t_start
+    printstyled(" ($(round(Δt, digits=2)) s)"; color=:light_black)
+    println()
+end
+
+testsetname = isempty(ARGS) ? "Diffinitive.jl" : "["*join(ARGS, ", ")*"]"
 
 @testset "$testsetname" begin
+    if isempty(ARGS)
+        log_and_time("code quality tests using Aqua.jl") do
+            @testset "Code quality (Aqua.jl)" begin
+                Aqua.test_all(Diffinitive)
+            end
+        end
+
+        log_and_time("code linting using JET.jl") do
+            @testset "Code linting (JET.jl)" begin
+                JET.test_package(Diffinitive; target_defined_modules = true)
+            end
+        end
+    end
+
     run_testfiles(ARGS)
     println()
 end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/update_manifest.jl	Sat Jan 11 10:19:47 2025 +0100
@@ -0,0 +1,12 @@
+using Pkg
+
+function update_directory(d)
+    Pkg.activate(d)
+    Pkg.update()
+    println()
+end
+
+update_directory(".")
+update_directory("benchmark")
+update_directory("docs")
+update_directory("test")