changeset 1943:48c49c04f3b2 feature/grids/manifolds

Merge feature/grids/parameter_spaces
author Jonatan Werpers <jonatan@werpers.com>
date Fri, 07 Feb 2025 15:29:40 +0100
parents b8395f69ad80 (diff) 15be190a40cd (current diff)
children 2a923e673cfc
files
diffstat 13 files changed, 498 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/Manifest.toml	Fri Feb 07 15:29:05 2025 +0100
+++ b/Manifest.toml	Fri Feb 07 15:29:40 2025 +0100
@@ -2,7 +2,7 @@
 
 julia_version = "1.11.3"
 manifest_format = "2.0"
-project_hash = "383b10bc09a08d519764abf167e40dae29b16b2e"
+project_hash = "51374910f6b7831392791bcca0079860f1023f47"
 
 [[deps.Artifacts]]
 uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
--- a/Project.toml	Fri Feb 07 15:29:05 2025 +0100
+++ b/Project.toml	Fri Feb 07 15:29:40 2025 +0100
@@ -9,6 +9,7 @@
 TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
 
 [weakdeps]
+Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
 Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
 SparseArrayKit = "a9a3c162-d163-4c15-8926-b8794fbefed2"
 SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
@@ -16,6 +17,7 @@
 
 [extensions]
 DiffinitiveMakieExt = "Makie"
+DiffinitivePlotsExt = "Plots"
 DiffinitiveSparseArrayKitExt = ["SparseArrayKit", "Tokens"]
 DiffinitiveSparseArraysExt = ["SparseArrays", "Tokens"]
 
@@ -24,6 +26,7 @@
 StaticArrays = "1.0"
 TOML = "1.0"
 Makie = "0.21"
+Plots = "1.40"
 SparseArrayKit = "0.4"
 Tokens = "0.1.1"
 SparseArrays = "1.10"
--- a/docs/make.jl	Fri Feb 07 15:29:05 2025 +0100
+++ b/docs/make.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -30,6 +30,7 @@
     "operator_file_format.md",
     "grids_and_grid_functions.md",
     "matrix_and_tensor_representations.md",
+    "manifolds_charts_atlases.md"
     "Submodules" => [
         "submodules/grids.md",
         "submodules/lazy_tensors.md",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/src/manifolds_charts_atlases.md	Fri Feb 07 15:29:40 2025 +0100
@@ -0,0 +1,36 @@
+# Manifolds, Charts, and Atlases
+
+To construct grids on more complicated geometries we use manifolds described
+by one or more charts. The charts describe a mapping from some parameter space
+to the geometry that we are interested in. If there are more than one chart
+for a given geometry this collection of charts and their connection is
+described by and atlas.
+
+For the construction of differential and difference operators on a manifold
+with a chart the library needs to know the Jacobian of the mapping as a
+function of coordinates in the logical parameter space. Internally,
+Diffinitive.jl uses a local Jacobian function, `Grids.jacobian(f, ξ)`. For
+geometry objects provided by the library this function should have fast and
+efficient implementations. If you are creating your own mapping functions you
+can implement `Grids.jacobian` for your function or type, for example
+
+```julia
+f(x) = 2x
+Grids.jacobian(::typeof(f), x) = fill(2, length(x))
+```
+
+```julia
+struct F end
+(::F)(x) = 2x
+Grids.jacobian(::F, x) = fill(2,length(x))
+```
+
+You can also provide a fallback function using one of the many automatic
+differentiation packages, for example
+
+```julia
+using ForwardDiff
+Grids.jacobian(f,x) = ForwardDiff.jacobian(f,x)
+```
+
+<!-- What more needs to be said here? --/>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/DiffinitivePlotsExt.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -0,0 +1,72 @@
+module DiffinitivePlotsExt
+
+using Diffinitive.Grids
+using Plots
+
+@recipe f(::Type{<:Grid}, g::Grid) = map(Tuple,g)[:]
+
+@recipe function f(c::Chart{2,<:Rectangle}, n=5, m=n; draw_border=true, bordercolor=1)
+    Ξ = parameterspace(c)
+    ξs = range(limits(Ξ,1)..., n)
+    ηs = range(limits(Ξ,2)..., m)
+
+    label := false
+    seriescolor --> 2
+    for ξ ∈ ξs
+        @series adapted_curve_grid(η->c((ξ,η)),limits(Ξ,1))
+    end
+
+    for η ∈ ηs
+        @series adapted_curve_grid(ξ->c((ξ,η)),limits(Ξ,2))
+    end
+
+    if ~draw_border
+        return
+    end
+
+    for ξ ∈ limits(Ξ,1)
+        @series begin
+            linewidth --> 3
+            seriescolor := bordercolor
+            adapted_curve_grid(η->c((ξ,η)),limits(Ξ,1))
+        end
+    end
+
+    for η ∈ limits(Ξ,2)
+        @series begin
+            linewidth --> 3
+            seriescolor := bordercolor
+            adapted_curve_grid(ξ->c((ξ,η)),limits(Ξ,2))
+        end
+    end
+end
+
+function adapted_curve_grid(g, minmax)
+    t1, _ = PlotUtils.adapted_grid(t->g(t)[1], minmax)
+    t2, _ = PlotUtils.adapted_grid(t->g(t)[2], minmax)
+
+    ts = sort(vcat(t1,t2))
+
+    x = map(ts) do t
+        g(t)[1]
+    end
+    y = map(ts) do t
+        g(t)[2]
+    end
+
+    return x, y
+end
+
+# get_axis_limits(plt, :x)
+
+
+# ReicpesPipline/src/user_recipe.jl
+# @recipe function f(f::FuncOrFuncs{F}) where {F<:Function}
+
+# @recipe function f(f::Function, xmin::Number, xmax::Number)
+
+# _scaled_adapted_grid(f, xscale, yscale, xmin, xmax)
+
+end
+
+
--- a/src/Grids/Grids.jl	Fri Feb 07 15:29:05 2025 +0100
+++ b/src/Grids/Grids.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -24,6 +24,17 @@
 export unittetrahedron
 export unitsimplex
 
+export Chart
+
+export Atlas
+export charts
+export connections
+export boundaries
+export CartesianAtlas
+export UnstructuredAtlas
+
+export parameterspace
+
 # Grid
 export Grid
 export coordinate_size
@@ -55,6 +66,8 @@
 export spacing
 export equidistant_grid
 
+export MultiBlockBoundary
+
 
 # MappedGrid
 export MappedGrid
@@ -65,6 +78,8 @@
 
 include("parameter_space.jl")
 include("grid.jl")
+include("multiblockgrids.jl")
+include("manifolds.jl")
 include("tensor_grid.jl")
 include("equidistant_grid.jl")
 include("zero_dim_grid.jl")
--- a/src/Grids/equidistant_grid.jl	Fri Feb 07 15:29:05 2025 +0100
+++ b/src/Grids/equidistant_grid.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -154,6 +154,10 @@
 equidistant_grid(d::Interval, size::Int) = equidistant_grid(limits(d)..., size)
 equidistant_grid(hb::HyperBox, dims::Vararg{Int}) = equidistant_grid(limits(hb)..., dims...)
 
+function equidistant_grid(c::Chart, dims::Vararg{Int})
+    mapped_grid(c, ξ->jacobian(c,ξ), parameterspace(c), dims...)
+end
+
 
 CartesianBoundary{D,BID} = TensorGridBoundary{D,BID} # TBD: What should we do about the naming of this boundary?
 
--- a/src/Grids/grid.jl	Fri Feb 07 15:29:05 2025 +0100
+++ b/src/Grids/grid.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -122,6 +122,8 @@
 """
 function boundary_identifiers end
 
+# TBD: Boundary identifiers for charts and atlases?
+
 """
     boundary_grid(g::Grid, id::BoundaryIdentifier)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Grids/manifolds.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -0,0 +1,127 @@
+"""
+    Chart{D}
+
+A parametrized description of a manifold or part of a manifold.
+"""
+struct Chart{D, PST<:ParameterSpace{D}, MT}
+    mapping::MT
+    parameterspace::PST
+end
+
+Base.ndims(::Chart{D}) where D = D
+(c::Chart)(ξ) = c.mapping(ξ)
+parameterspace(c::Chart) = c.parameterspace
+
+"""
+    jacobian(c::Chart, ξ)
+
+The jacobian of the mapping evaluated at `ξ`. This defers to the
+implementation of `jacobian` for the mapping itself. If no implementation is
+available one can easily be specified for either the mapping function or the
+chart itself.
+```julia
+c = Chart(f, ps)
+jacobian(f::typeof(f), ξ) = f′(ξ)
+```
+or
+```julia
+c = Chart(f, ps)
+jacobian(c::typeof(c),ξ) = f′(ξ)
+```
+which will both allow calling `jacobian(c,ξ)`.
+"""
+jacobian(c::Chart, ξ) = jacobian(c.mapping, ξ)
+# TBD: Can we register a error hint for when jacobian is called with a function that doesn't have a registered jacobian?
+
+boundaries(c::Chart) = boundaries(parameterspace(c))
+
+
+"""
+    Atlas
+
+A collection of charts and their connections.
+Should implement methods for `charts` and `connections`.
+"""
+abstract type Atlas end
+
+"""
+    charts(::Atlas)
+
+The colloction of charts in the atlas.
+"""
+function charts end
+
+"""
+    connections(::Atlas)
+
+TBD: What exactly should this return?
+"""
+function connections end
+
+struct CartesianAtlas{D,C<:Chart,AT<:AbstractArray{C,D}} <: Atlas
+    charts::AT
+end
+
+charts(a::CartesianAtlas) = a.charts
+
+function connections(a::CartesianAtlas)
+    c = Tuple{MultiBlockBoundary, MultiBlockBoundary}[]
+
+    for d ∈ 1:ndims(charts(a))
+        Is = eachslice(CartesianIndices(charts(a)); dims=d)
+        for i ∈ 1:length(Is)-1 # For each interface between slices
+            for jk ∈ eachindex(Is[i]) # For each block in slice
+                Iᵢⱼₖ = Tuple(Is[i][jk])
+                Iᵢ₊₁ⱼₖ = Tuple(Is[i+1][jk])
+                push!(c,
+                    (
+                        MultiBlockBoundary{Iᵢⱼₖ,   CartesianBoundary{d,UpperBoundary}}(),
+                        MultiBlockBoundary{Iᵢ₊₁ⱼₖ, CartesianBoundary{d,LowerBoundary}}(),
+                    )
+                )
+            end
+        end
+    end
+
+    return c
+end
+
+function boundaries(a::CartesianAtlas)
+    bs = MultiBlockBoundary[]
+
+    for d ∈ 1:ndims(charts(a))
+        Is = eachslice(CartesianIndices(charts(a)); dims=d)
+
+        for (i,b) ∈ ((1,LowerBoundary),(length(Is),UpperBoundary)) # For first and last slice
+            for jk ∈ eachindex(Is[i]) # For each block in slice
+                Iᵢⱼₖ = Tuple(Is[i][jk])
+                push!(bs,
+                    MultiBlockBoundary{Iᵢⱼₖ,   CartesianBoundary{d,b}}(),
+                )
+            end
+        end
+    end
+
+    return bs
+end
+
+
+struct UnstructuredAtlas{C<:Chart, CN<:Tuple{MultiBlockBoundary,MultiBlockBoundary}, CV<:AbstractVector{C}, CNV<:AbstractVector{CN}} <: Atlas
+    charts::CV
+    connections::CNV
+end
+
+charts(a::UnstructuredAtlas) = a.charts
+connections(a::UnstructuredAtlas) = a.connections
+
+function boundaries(a::UnstructuredAtlas)
+    bs = MultiBlockBoundary[]
+
+    for c ∈ charts(a)
+        for b ∈ boundaries(c)
+            if !any(cn->b∈cn, connections(a))
+                push!(bs, b)
+            end
+        end
+    end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Grids/multiblockgrids.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -0,0 +1,9 @@
+"""
+    MultiBlockBoundary{N, BID} <: BoundaryIdentifier
+
+A boundary identifier for a multiblock grids. `N` Specifies which grid and
+`BID` which boundary on that grid.
+"""
+struct MultiBlockBoundary{N, BID} <: BoundaryIdentifier end
+grid_id(::MultiBlockBoundary{N, BID}) where {N, BID} = N
+boundary_id(::MultiBlockBoundary{N, BID}) where {N, BID} = BID()
--- a/test/Grids/equidistant_grid_test.jl	Fri Feb 07 15:29:05 2025 +0100
+++ b/test/Grids/equidistant_grid_test.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -163,6 +163,16 @@
         @test equidistant_grid(unitinterval(),3) == equidistant_grid(0,1,3)
         @test equidistant_grid(HyperBox((0,),(2,)),4) == equidistant_grid(@SVector[0], @SVector[2], 4)
     end
+
+
+    @testset "equidistant_grid(::Chart)" begin
+        c = Chart(unitsquare()) do (ξ,η)
+            @SVector[2ξ, 3η]
+        end
+        Grids.jacobian(c::typeof(c), ξ̄) = @SMatrix[2 0; 0 3]
+
+        @test equidistant_grid(c, 5, 4) isa Grid
+    end
 end
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/Grids/manifolds_test.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -0,0 +1,208 @@
+using Test
+
+using Diffinitive.Grids
+using Diffinitive.RegionIndices
+using Diffinitive.LazyTensors
+
+using StaticArrays
+
+west = CartesianBoundary{1,LowerBoundary}
+east = CartesianBoundary{1,UpperBoundary}
+south = CartesianBoundary{2,LowerBoundary}
+north = CartesianBoundary{2,UpperBoundary}
+bottom = CartesianBoundary{3, LowerBoundary}
+top = CartesianBoundary{3, UpperBoundary}
+
+@testset "Chart" begin
+    X(ξ) = 2ξ
+    Grids.jacobian(::typeof(X), ξ) = @SVector[2,2]
+    c = Chart(X, unitsquare())
+    @test c isa Chart{2}
+    @test c([3,2]) == [6,4]
+    @test parameterspace(c) == unitsquare()
+    @test ndims(c) == 2
+
+    @test jacobian(c, [3,2]) == [2,2]
+
+    @test_broken Set(boundaries(X,unitsquare())) == Set([east,west,south,north])
+end
+
+@testset "CartesianAtlas" begin
+    @testset "Constructors" begin
+        c = Chart(identity, unitsquare())
+        @test CartesianAtlas([c c; c c]) isa Atlas
+
+        c2 = Chart(x->2x, unitsquare())
+        @test CartesianAtlas([c c2; c2 c]) isa CartesianAtlas
+        @test CartesianAtlas(@SMatrix[c c; c c]) isa CartesianAtlas
+        @test CartesianAtlas(@SMatrix[c c2; c2 c]) isa CartesianAtlas
+    end
+
+    @testset "Getters" begin
+        c = Chart(identity, unitsquare())
+        a = CartesianAtlas([c c; c c])
+        @test charts(a) == [c c; c c]
+    end
+
+    @testset "connections" begin
+        # 2D
+        a = CartesianAtlas(fill(Chart(identity, unitsquare()), 2,3))
+
+        @test Set(connections(a)) == Set([
+            (MultiBlockBoundary{(1,1), east}(), MultiBlockBoundary{(2,1), west}()),
+            (MultiBlockBoundary{(1,1), north}(), MultiBlockBoundary{(1,2), south}()),
+            (MultiBlockBoundary{(2,1), north}(), MultiBlockBoundary{(2,2), south}()),
+            (MultiBlockBoundary{(1,2), east}(), MultiBlockBoundary{(2,2), west}()),
+            (MultiBlockBoundary{(1,2), north}(), MultiBlockBoundary{(1,3), south}()),
+            (MultiBlockBoundary{(2,2), north}(), MultiBlockBoundary{(2,3), south}()),
+            (MultiBlockBoundary{(1,3), east}(), MultiBlockBoundary{(2,3), west}()),
+        ])
+
+        # 3D
+        a = CartesianAtlas(fill(Chart(identity, unitcube()), 2,2,3))
+        @test Set(connections(a)) == Set([
+            (MultiBlockBoundary{(1,1,1), east}(),  MultiBlockBoundary{(2,1,1), west}()),
+            (MultiBlockBoundary{(1,1,1), north}(), MultiBlockBoundary{(1,2,1), south}()),
+            (MultiBlockBoundary{(2,1,1), north}(), MultiBlockBoundary{(2,2,1), south}()),
+            (MultiBlockBoundary{(1,2,1), east}(),  MultiBlockBoundary{(2,2,1), west}()),
+
+            (MultiBlockBoundary{(1,1,2), east}(),  MultiBlockBoundary{(2,1,2), west}()),
+            (MultiBlockBoundary{(1,1,2), north}(), MultiBlockBoundary{(1,2,2), south}()),
+            (MultiBlockBoundary{(2,1,2), north}(), MultiBlockBoundary{(2,2,2), south}()),
+            (MultiBlockBoundary{(1,2,2), east}(),  MultiBlockBoundary{(2,2,2), west}()),
+
+            (MultiBlockBoundary{(1,1,3), east}(),  MultiBlockBoundary{(2,1,3), west}()),
+            (MultiBlockBoundary{(1,1,3), north}(), MultiBlockBoundary{(1,2,3), south}()),
+            (MultiBlockBoundary{(2,1,3), north}(), MultiBlockBoundary{(2,2,3), south}()),
+            (MultiBlockBoundary{(1,2,3), east}(),  MultiBlockBoundary{(2,2,3), west}()),
+
+            (MultiBlockBoundary{(1,1,1), top}(), MultiBlockBoundary{(1,1,2), bottom}()),
+            (MultiBlockBoundary{(2,1,1), top}(), MultiBlockBoundary{(2,1,2), bottom}()),
+            (MultiBlockBoundary{(1,2,1), top}(), MultiBlockBoundary{(1,2,2), bottom}()),
+            (MultiBlockBoundary{(2,2,1), top}(), MultiBlockBoundary{(2,2,2), bottom}()),
+
+            (MultiBlockBoundary{(1,1,2), top}(), MultiBlockBoundary{(1,1,3), bottom}()),
+            (MultiBlockBoundary{(2,1,2), top}(), MultiBlockBoundary{(2,1,3), bottom}()),
+            (MultiBlockBoundary{(1,2,2), top}(), MultiBlockBoundary{(1,2,3), bottom}()),
+            (MultiBlockBoundary{(2,2,2), top}(), MultiBlockBoundary{(2,2,3), bottom}()),
+        ])
+    end
+
+    @testset "boundaries" begin
+        # 2D
+        a = CartesianAtlas(fill(Chart(identity, unitcube()), 2,3))
+        @test Set(boundaries(a)) == Set([
+            MultiBlockBoundary{(1,1), south}(),
+            MultiBlockBoundary{(2,1), south}(),
+            MultiBlockBoundary{(2,1), east}(),
+            MultiBlockBoundary{(2,2), east}(),
+            MultiBlockBoundary{(2,3), east}(),
+            MultiBlockBoundary{(1,3), north}(),
+            MultiBlockBoundary{(2,3), north}(),
+            MultiBlockBoundary{(1,1), west}(),
+            MultiBlockBoundary{(1,2), west}(),
+            MultiBlockBoundary{(1,3), west}(),
+        ])
+
+        # 3D
+        a = CartesianAtlas(fill(Chart(identity, unitsquare()), 2,2,3))
+        @test Set(boundaries(a)) == Set([
+            MultiBlockBoundary{(1,1,1), bottom}(),
+            MultiBlockBoundary{(2,1,1), bottom}(),
+            MultiBlockBoundary{(1,2,1), bottom}(),
+            MultiBlockBoundary{(2,2,1), bottom}(),
+
+            MultiBlockBoundary{(1,1,3), top}(),
+            MultiBlockBoundary{(2,1,3), top}(),
+            MultiBlockBoundary{(1,2,3), top}(),
+            MultiBlockBoundary{(2,2,3), top}(),
+
+            MultiBlockBoundary{(1,1,1), west}(),
+            MultiBlockBoundary{(1,2,1), west}(),
+            MultiBlockBoundary{(1,1,2), west}(),
+            MultiBlockBoundary{(1,2,2), west}(),
+            MultiBlockBoundary{(1,1,3), west}(),
+            MultiBlockBoundary{(1,2,3), west}(),
+
+            MultiBlockBoundary{(2,1,1), east}(),
+            MultiBlockBoundary{(2,2,1), east}(),
+            MultiBlockBoundary{(2,1,2), east}(),
+            MultiBlockBoundary{(2,2,2), east}(),
+            MultiBlockBoundary{(2,1,3), east}(),
+            MultiBlockBoundary{(2,2,3), east}(),
+
+            MultiBlockBoundary{(1,1,1), south}(),
+            MultiBlockBoundary{(2,1,1), south}(),
+            MultiBlockBoundary{(1,1,2), south}(),
+            MultiBlockBoundary{(2,1,2), south}(),
+            MultiBlockBoundary{(1,1,3), south}(),
+            MultiBlockBoundary{(2,1,3), south}(),
+
+            MultiBlockBoundary{(1,2,1), north}(),
+            MultiBlockBoundary{(2,2,1), north}(),
+            MultiBlockBoundary{(1,2,2), north}(),
+            MultiBlockBoundary{(2,2,2), north}(),
+            MultiBlockBoundary{(1,2,3), north}(),
+            MultiBlockBoundary{(2,2,3), north}(),
+        ])
+    end
+end
+
+@testset "UnstructuredAtlas" begin
+    @testset "Constructors" begin
+        c1 = Chart(identity, unitsquare())
+        c2 = Chart(x->2x, unitsquare())
+        cn = [
+            (MultiBlockBoundary{1, east}(), MultiBlockBoundary{2, west}()),
+            (MultiBlockBoundary{1, north}(), MultiBlockBoundary{3, west}()),
+            (MultiBlockBoundary{2, north}(),  MultiBlockBoundary{3, south}()),
+        ]
+
+        @test UnstructuredAtlas([c1, c1, c1], cn) isa UnstructuredAtlas
+        @test UnstructuredAtlas([c1, c2, c1, c2], cn) isa UnstructuredAtlas
+
+
+        cn = @SVector[
+            (MultiBlockBoundary{1, east}(), MultiBlockBoundary{2, west}()),
+            (MultiBlockBoundary{1, north}(), MultiBlockBoundary{3, west}()),
+            (MultiBlockBoundary{2, north}(),  MultiBlockBoundary{3, south}()),
+        ]
+        @test UnstructuredAtlas(@SVector[c1, c1, c1], cn) isa UnstructuredAtlas
+        @test UnstructuredAtlas(@SVector[c1, c2, c1, c2], cn) isa UnstructuredAtlas
+    end
+
+    @testset "Getters" begin
+        c = Chart(identity, unitsquare())
+        cn = [
+            (MultiBlockBoundary{1, east}(), MultiBlockBoundary{2, west}()),
+            (MultiBlockBoundary{1, north}(), MultiBlockBoundary{3, west}()),
+            (MultiBlockBoundary{2, north}(),  MultiBlockBoundary{3, south}()),
+        ]
+
+        a = UnstructuredAtlas([c, c, c], cn)
+
+        @test charts(a) == [c,c,c]
+        @test connections(a) == cn
+    end
+
+    @testset "boundaries" begin
+        c = Chart(identity, unitsquare())
+        cn = [
+            (MultiBlockBoundary{1, east}(), MultiBlockBoundary{2, west}()),
+            (MultiBlockBoundary{1, north}(), MultiBlockBoundary{3, west}()),
+            (MultiBlockBoundary{2, north}(),  MultiBlockBoundary{3, south}()),
+        ]
+
+        a = UnstructuredAtlas([c, c, c], cn)
+
+        @test_broken Set(boundaries(a)) == Set([
+            MultiBlockBoundary{1, west}(),
+            MultiBlockBoundary{1, south}(),
+            MultiBlockBoundary{2, south}(),
+            MultiBlockBoundary{2, east}(),
+            MultiBlockBoundary{3, north}(),
+            MultiBlockBoundary{3, east}(),
+        ])
+
+    end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/Grids/multiblockgrids_test.jl	Fri Feb 07 15:29:40 2025 +0100
@@ -0,0 +1,10 @@
+using Diffinitive.Grids
+
+@testset "MultiBlockBoundary" begin
+    @test MultiBlockBoundary{1,UpperBoundary}() isa BoundaryIdentifier
+
+    @test grid_id(MultiBlockBoundary{1,UpperBoundary}()) == 1
+
+    @test boundary_id(MultiBlockBoundary{1,UpperBoundary}()) == UpperBoundary()
+
+end