diff --git a/Project.toml b/Project.toml index 46f57d8..e905c81 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" MPI = "da04e1cc-30fd-572f-bb4f-1f8673147195" Oceananigans = "9e8cae18-63c1-5223-a75c-80ca9d6e9a09" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] Adapt = "4" diff --git a/src/OrthogonalSphericalShellGrids.jl b/src/OrthogonalSphericalShellGrids.jl index e54cf3d..3eb98b2 100644 --- a/src/OrthogonalSphericalShellGrids.jl +++ b/src/OrthogonalSphericalShellGrids.jl @@ -21,11 +21,11 @@ using OffsetArrays @inline convert_to_0_360(x) = ((x % 360) + 360) % 360 -include("grid_utils.jl") +include("tripolar_grid_utils.jl") include("zipper_boundary_condition.jl") include("generate_tripolar_coordinates.jl") include("tripolar_grid.jl") -include("grid_extensions.jl") +include("tripolar_grid_extensions.jl") include("distributed_tripolar_grid.jl") include("with_halo.jl") include("split_explicit_free_surface.jl") diff --git a/src/distributed_tripolar_grid.jl b/src/distributed_tripolar_grid.jl index 3387e57..91b4d84 100644 --- a/src/distributed_tripolar_grid.jl +++ b/src/distributed_tripolar_grid.jl @@ -220,13 +220,13 @@ function reconstruct_global_grid(grid::DistributedTripolarGrid) north_poles_latitude = grid.conformal_mapping.north_poles_latitude first_pole_longitude = grid.conformal_mapping.first_pole_longitude - southermost_latitude = grid.conformal_mapping.southermost_latitude + southernmost_latitude = grid.conformal_mapping.southernmost_latitude return TripolarGrid(child_arch, FT; halo, size, north_poles_latitude, first_pole_longitude, - southermost_latitude, + southernmost_latitude, z) end diff --git a/src/tripolar_grid.jl b/src/tripolar_grid.jl index 85c0681..d87db8b 100644 --- a/src/tripolar_grid.jl +++ b/src/tripolar_grid.jl @@ -2,20 +2,20 @@ struct Tripolar{N, F, S} north_poles_latitude :: N first_pole_longitude :: F - southermost_latitude :: S + southernmost_latitude :: S end Adapt.adapt_structure(to, t::Tripolar) = Tripolar(Adapt.adapt(to, t.north_poles_latitude), Adapt.adapt(to, t.first_pole_longitude), - Adapt.adapt(to, t.southermost_latitude)) + Adapt.adapt(to, t.southernmost_latitude)) const TripolarGrid{FT, TX, TY, TZ, A, R, FR, Arch} = OrthogonalSphericalShellGrid{FT, TX, TY, TZ, A, R, FR, <:Tripolar, Arch} """ TripolarGrid(arch = CPU(), FT::DataType = Float64; size, - southermost_latitude = -80, + southernmost_latitude = -80, halo = (4, 4, 4), radius = R_Earth, z = (0, 1), @@ -39,7 +39,7 @@ Keyword Arguments ================= - `size`: The number of cells in the (longitude, latitude, vertical) dimensions. -- `southermost_latitude`: The southernmost `Center` latitude of the grid. Default is -80. +- `southernmost_latitude`: The southernmost `Center` latitude of the grid. Default is -80. - `halo`: The halo size in the (longitude, latitude, vertical) dimensions. Default is (4, 4, 4). - `radius`: The radius of the spherical shell. Default is `R_Earth`. - `z`: The vertical ``z``-coordinate range of the grid. Default is (0, 1). @@ -57,7 +57,7 @@ The north singularities are located at """ function TripolarGrid(arch = CPU(), FT::DataType = Float64; size, - southermost_latitude = -80, # The southermost `Center` latitude of the grid + southernmost_latitude = -80, # The southermost `Center` latitude of the grid halo = (4, 4, 4), radius = R_Earth, z = (0, 1), @@ -68,7 +68,7 @@ function TripolarGrid(arch = CPU(), FT::DataType = Float64; # to construct the grid on the GPU. This is not a huge problem as # grid generation is quite fast, but it might become for sub-kilometer grids - latitude = (southermost_latitude, 90) + latitude = (southernmost_latitude, 90) longitude = (-180, 180) focal_distance = tand((90 - north_poles_latitude) / 2) @@ -87,8 +87,8 @@ function TripolarGrid(arch = CPU(), FT::DataType = Float64; Lz, zᵃᵃᶠ, zᵃᵃᶜ, Δzᵃᵃᶠ, Δzᵃᵃᶜ = generate_coordinate(FT, Bounded(), Nz, Hz, z, :z, CPU()) # The φ coordinate is a bit more complicated because the center points start from - # southermost_latitude and end at 90ᵒ N. - φᵃᶜᵃ = collect(range(southermost_latitude, 90, length = Nφ)) + # southernmost_latitude and end at 90ᵒ N. + φᵃᶜᵃ = collect(range(southernmost_latitude, 90, length = Nφ)) Δφ = φᵃᶜᵃ[2] - φᵃᶜᵃ[1] φᵃᶠᵃ = φᵃᶜᵃ .- Δφ / 2 @@ -322,7 +322,7 @@ function TripolarGrid(arch = CPU(), FT::DataType = Float64; on_architecture(arch, Azᶜᶠᵃ), on_architecture(arch, Azᶠᶠᵃ), radius, - Tripolar(north_poles_latitude, first_pole_longitude, southermost_latitude)) + Tripolar(north_poles_latitude, first_pole_longitude, southernmost_latitude)) return grid end diff --git a/src/grid_extensions.jl b/src/tripolar_grid_extensions.jl similarity index 100% rename from src/grid_extensions.jl rename to src/tripolar_grid_extensions.jl diff --git a/src/grid_utils.jl b/src/tripolar_grid_utils.jl similarity index 78% rename from src/grid_utils.jl rename to src/tripolar_grid_utils.jl index bed6ff3..9b136e1 100644 --- a/src/grid_utils.jl +++ b/src/tripolar_grid_utils.jl @@ -38,21 +38,15 @@ end d = lat_lon_to_cartesian(φᶠᶠᵃ[ i , j+1], λᶠᶠᵃ[ i , j+1], 1) Azᶜᶜᵃ[i, j] = spherical_area_quadrilateral(a, b, c, d) * radius^2 - - a = lat_lon_to_cartesian(φᶜᶠᵃ[i-1, j ], λᶜᶠᵃ[i-1, j ], 1) - b = lat_lon_to_cartesian(φᶜᶠᵃ[ i , j ], λᶜᶠᵃ[ i , j ], 1) - c = lat_lon_to_cartesian(φᶜᶠᵃ[ i , j+1], λᶜᶠᵃ[ i , j+1], 1) - d = lat_lon_to_cartesian(φᶜᶠᵃ[i-1, j+1], λᶜᶠᵃ[i-1, j+1], 1) - - Azᶠᶜᵃ[i, j] = spherical_area_quadrilateral(a, b, c, d) * radius^2 - - a = lat_lon_to_cartesian(φᶠᶜᵃ[ i , j-1], λᶠᶜᵃ[ i , j-1], 1) - b = lat_lon_to_cartesian(φᶠᶜᵃ[i+1, j-1], λᶠᶜᵃ[i+1, j-1], 1) - c = lat_lon_to_cartesian(φᶠᶜᵃ[i+1, j ], λᶠᶜᵃ[i+1, j ], 1) - d = lat_lon_to_cartesian(φᶠᶜᵃ[ i , j ], λᶠᶜᵃ[ i , j ], 1) - - Azᶜᶠᵃ[i, j] = spherical_area_quadrilateral(a, b, c, d) * radius^2 - + + # To be able to conserve kinetic energy specifically the momentum equation, + # it is better to define the face areas as products of + # the edge lengths rather than using the spherical area of the face (cit JMC). + # TODO: find a reference to support this statement + Azᶠᶜᵃ[i, j] = Δyᶠᶜᵃ[i, j] * Δxᶠᶜᵃ[i, j] + Azᶜᶠᵃ[i, j] = Δyᶜᶠᵃ[i, j] * Δxᶜᶠᵃ[i, j] + + # Face - Face areas are calculated as the Center - Center ones a = lat_lon_to_cartesian(φᶜᶜᵃ[i-1, j-1], λᶜᶜᵃ[i-1, j-1], 1) b = lat_lon_to_cartesian(φᶜᶜᵃ[ i , j-1], λᶜᶜᵃ[ i , j-1], 1) c = lat_lon_to_cartesian(φᶜᶜᵃ[ i , j ], λᶜᶜᵃ[ i , j ], 1) diff --git a/src/with_halo.jl b/src/with_halo.jl index c5ce92b..9207ef9 100644 --- a/src/with_halo.jl +++ b/src/with_halo.jl @@ -10,14 +10,14 @@ function with_halo(new_halo, old_grid::TripolarGrid) north_poles_latitude = old_grid.conformal_mapping.north_poles_latitude first_pole_longitude = old_grid.conformal_mapping.first_pole_longitude - southermost_latitude = old_grid.conformal_mapping.southermost_latitude + southernmost_latitude = old_grid.conformal_mapping.southernmost_latitude new_grid = TripolarGrid(architecture(old_grid), eltype(old_grid); size, z, halo = new_halo, radius = old_grid.radius, north_poles_latitude, first_pole_longitude, - southermost_latitude) + southernmost_latitude) return new_grid end @@ -32,13 +32,13 @@ function with_halo(new_halo, old_grid::DistributedTripolarGrid) north_poles_latitude = old_grid.conformal_mapping.north_poles_latitude first_pole_longitude = old_grid.conformal_mapping.first_pole_longitude - southermost_latitude = old_grid.conformal_mapping.southermost_latitude + southernmost_latitude = old_grid.conformal_mapping.southernmost_latitude return TripolarGrid(arch, eltype(old_grid); halo = new_halo, size = N, north_poles_latitude, first_pole_longitude, - southermost_latitude, + southernmost_latitude, z) end diff --git a/test/dependencies_for_runtests.jl b/test/dependencies_for_runtests.jl new file mode 100644 index 0000000..6528ffc --- /dev/null +++ b/test/dependencies_for_runtests.jl @@ -0,0 +1,12 @@ +using OrthogonalSphericalShellGrids +using Oceananigans +using Oceananigans.Grids: halo_size, φnodes, λnodes +using Oceananigans.Utils +using Oceananigans.BoundaryConditions +using OrthogonalSphericalShellGrids: get_cartesian_nodes_and_vertices +using Oceananigans.CUDA +using Test + +using KernelAbstractions: @kernel, @index + +arch = CUDA.has_cuda_gpu() ? GPU() : CPU() diff --git a/test/runtests.jl b/test/runtests.jl index 3a8c9b0..d750175 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,16 +1,36 @@ -using OrthogonalSphericalShellGrids -using OrthogonalSphericalShellGrids.Oceananigans -using Oceananigans: GPU, CPU -using Oceananigans.CUDA -using Test +include("dependencies_for_runtests.jl") -arch = CUDA.has_cuda_gpu() ? GPU() : CPU() +@testset "Unit tests..." begin + grid = TripolarGrid(size = (4, 5, 1), z = (0, 1), + first_pole_longitude = 75, + north_poles_latitude = 35, + southernmost_latitude = -80) -@testset "OrthogonalSphericalShellGrids.jl" begin - # We probably do not need any unit tests. + @test grid isa TripolarGrid - # Test the grid? - grid = TripolarGrid(arch; size = (10, 10, 1)) + @test grid.Nx == 4 + @test grid.Ny == 5 + @test grid.Nz == 1 - # Test boundary conditions? + @test grid.conformal_mapping.first_pole_longitude == 75 + @test grid.conformal_mapping.north_poles_latitude == 35 + @test grid.conformal_mapping.southernmost_latitude == -80 + + λᶜᶜᵃ = λnodes(grid, Center(), Center()) + φᶜᶜᵃ = φnodes(grid, Center(), Center()) + + min_Δφ = minimum(φᶜᶜᵃ[:, 2] .- φᶜᶜᵃ[:, 1]) + + @test minimum(λᶜᶜᵃ) ≥ 0 + @test maximum(λᶜᶜᵃ) ≤ 360 + @test maximum(φᶜᶜᵃ) ≤ 90 + + # The minimum latitude is not exactly the southermost latitude because the grid + # undulates slightly to maintain the same analytical description in the whole sphere + # (i.e. constant latitude lines do not exist anywhere in this grid) + @test minimum(φᶜᶜᶜ .+ min_Δφ / 10) ≥ grid.conformal_mapping.southernmost_latitude end + +include("test_tripolar_grid.jl") +include("test_zipper_boundary_conditions.jl") + diff --git a/test/test_tripolar_grid.jl b/test/test_tripolar_grid.jl new file mode 100644 index 0000000..88bd5b8 --- /dev/null +++ b/test/test_tripolar_grid.jl @@ -0,0 +1,75 @@ +include("dependencies_for_runtests.jl") + +using Statistics: dot, norm +using Oceananigans.Utils: getregion +using Oceananigans.ImmersedBoundaries: immersed_cell + +@kernel function compute_nonorthogonality_angle!(angle, grid, xF, yF, zF) + i, j = @index(Global, NTuple) + + @inbounds begin + x⁻ = xF[i, j] + y⁻ = yF[i, j] + z⁻ = zF[i, j] + + x⁺¹ = xF[i + 1, j] + y⁺¹ = yF[i + 1, j] + z⁺¹ = zF[i + 1, j] + x⁺² = xF[i, j + 1] + y⁺² = yF[i, j + 1] + z⁺² = zF[i, j + 1] + + v1 = (x⁺¹ - x⁻, y⁺¹ - y⁻, z⁺¹ - z⁻) + v2 = (x⁺² - x⁻, y⁺² - y⁻, z⁺² - z⁻) + + # Check orthogonality by computing the angle between the vectors + cosθ = dot(v1, v2) / (norm(v1) * norm(v2)) + immersed = immersed_cell(i, j, 1, grid) + angle[i, j] = ifelse(immersed, π / 2, acos(cosθ)) - π / 2 + + # convert to degrees + angle[i, j] = rad2deg(angle[i, j]) + end +end + +@testset "Orthogonality of family of ellipses and hyperbolae..." begin + + # Test the orthogonality of a tripolar grid based on the orthogonality of a + # cubed sphere of the same size (1ᵒ in latitude and longitude) + cubed_sphere_grid = ConformalCubedSphereGrid(panel_size = (90, 90, 1), z = (0, 1)) + cubed_sphere_panel = getregion(cubed_sphere_grid, 1) + + angle_cubed_sphere = zeros(size(cubed_sphere_panel)...) + cartesian_nodes, _ = get_cartesian_nodes_and_vertices(cubed_sphere_panel, Face(), Face(), Center()) + xF, yF, zF = cartesian_nodes + Nx, Ny, _ = size(cubed_sphere_panel) + + # Exclude the corners from the computation! (They are definitely not orthogonal) + params = KernelParameters(5:Nx-5, 5:Ny-5) + + launch!(CPU(), cubed_sphere_panel, params, compute_nonorthogonality_angle!, angle_cubed_sphere, cubed_sphere_panel, xF, yF, zF) + + first_pole_longitude = λ¹ₚ = 75 + north_poles_latitude = φₚ = 35 + + λ²ₚ = λ¹ₚ + 180 + + # Build a tripolar grid at 1ᵒ + underlying_grid = TripolarGrid(; size = (360, 180, 1), first_pole_longitude, north_poles_latitude) + + # We need a bottom height field that ``masks'' the singularities + bottom_height(λ, φ) = ((abs(λ - λ¹ₚ) < 5) & (abs(φₚ - φ) < 5)) | + ((abs(λ - λ²ₚ) < 5) & (abs(φₚ - φ) < 5)) | (φ < -78) ? 1 : 0 + + # Exclude the singularities from the computation! (They are definitely not orthogonal) + tripolar_grid = ImmersedBoundaryGrid(underlying_grid, GridFittedBottom(bottom_height)) + angle_tripolar = zeros(size(tripolar_grid)...) + cartesian_nodes, _ = get_cartesian_nodes_and_vertices(tripolar_grid.underlying_grid, Face(), Face(), Center()) + xF, yF, zF = cartesian_nodes + Nx, Ny, _ = size(tripolar_grid) + + launch!(CPU(), tripolar_grid, (Nx-1, Ny-1), compute_nonorthogonality_angle!, angle_tripolar, tripolar_grid, xF, yF, zF) + + @test maximum(angle_tripolar) < maximum(angle_cubed_sphere) + @test minimum(angle_tripolar) > minimum(angle_cubed_sphere) +end \ No newline at end of file diff --git a/test/test_zipper_boundary_conditions.jl b/test/test_zipper_boundary_conditions.jl new file mode 100644 index 0000000..20b7c49 --- /dev/null +++ b/test/test_zipper_boundary_conditions.jl @@ -0,0 +1,43 @@ +include("dependencies_for_runtests.jl") + +using OrthogonalSphericalShellGrids: Zipper + +@testset "Zipper boundary conditions..." begin + grid = TripolarGrid(size = (10, 10, 1)) + Nx, Ny, _ = size(grid) + Hx, Hy, _ = halo_size(grid) + + c = CenterField(grid) + u = XFaceField(grid) + v = YFaceField(grid) + + @test c.boundary_conditions.north.classification isa Zipper + @test u.boundary_conditions.north.classification isa Zipper + @test v.boundary_conditions.north.classification isa Zipper + + @test c.boundary_conditions.north.condition == 1 + @test u.boundary_conditions.north.condition == -1 + @test v.boundary_conditions.north.condition == -1 + + set!(c, 1) + set!(u, 1) + set!(v, 1) + + fill_halo_regions!(c) + fill_halo_regions!(u) + fill_halo_regions!(v) + + north_boundary_c = view(c.data, :, Ny+1:Ny+Hy, 1) + north_boundary_v = view(v.data, :, Ny+1:Ny+Hy, 1) + @test all(north_boundary_c .== 1) + @test all(north_boundary_v .== -1) + + # U is special, because periodicity is hardcoded in the x-direction + north_interior_boundary_u = view(u.data, 2:Nx-1, Ny+1:Ny+Hy, 1) + @test all(north_interior_boundary_u .== -1) + + north_boundary_u_left = view(u.data, 1, Ny+1:Ny+Hy, 1) + north_boundary_u_right = view(u.data, Nx+1, Ny+1:Ny+Hy, 1) + @test all(north_boundary_u_left .== 1) + @test all(north_boundary_u_right .== 1) +end \ No newline at end of file