Repository: CliMA/ClimaOcean.jl
Pull request #475: Better interpolation info shown to user
Author: login: navidcy
Created: 2025-04-17T23:47:16Z | Merged: 2025-04-18T06:05:59Z

Closes #476

Also gives more informative message to the user regarding why the extra interpolation passes are skipped.

```Julia
using ClimaOcean
using Oceananigans

underlying_grid = TripolarGrid(size = (360, 180, 50),
                               z = (-5000, 0),
                               halo = (5, 5, 4),
                               first_pole_longitude = 70,
                               north_poles_latitude = 55)
```
```Julia
bottom_height = regrid_bathymetry(underlying_grid;
                                  major_basins = 2)
[ Info: Interpolating bathymetry of size (21600, 10800, 1) onto a TripolarGrid target grid of size (360, 180, 50)
360×180×1 Field{Center, Center, Nothing} reduced over dims = (3,) on OrthogonalSphericalShellGrid on CPU
├── grid: 360×180×50 OrthogonalSphericalShellGrid{Float64, Periodic, Oceananigans.Grids.RightConnected, Bounded} on CPU with 5×5×4 halo and with precomputed metrics
├── boundary conditions: FieldBoundaryConditions
│   └── west: Periodic, east: Periodic, south: ZeroFlux, north: Zipper, bottom: Nothing, top: Nothing, immersed: Default
└── data: 370×190×1 OffsetArray(::Array{Float64, 3}, -4:365, -4:185, 1:1) with eltype Float64 with indices -4:365×-4:185×1:1
    └── max=6102.41, min=-9452.9, mean=-1960.54
```
```Julia
bottom_height_with_extra_passes = regrid_bathymetry(underlying_grid;
                                                    interpolation_passes = 6,
                                                    major_basins = 2)
[ Info: Interpolation passes of bathymetry size (21600, 10800, 1) onto a TripolarGrid target grid of size (360, 180, 50):
[ Info:     pass 1 to size (18060, 9030, 1)
[ Info:     pass 2 to size (14520, 7260, 1)
[ Info:     pass 3 to size (10980, 5490, 1)
[ Info:     pass 4 to size (7440, 3720, 1)
[ Info:     pass 5 to size (3900, 1950, 1)
[ Info:     pass 6 to size (360, 180, 1)
360×180×1 Field{Center, Center, Nothing} reduced over dims = (3,) on OrthogonalSphericalShellGrid on CPU
├── grid: 360×180×50 OrthogonalSphericalShellGrid{Float64, Periodic, Oceananigans.Grids.RightConnected, Bounded} on CPU with 5×5×4 halo and with precomputed metrics
├── boundary conditions: FieldBoundaryConditions
│   └── west: Periodic, east: Periodic, south: ZeroFlux, north: Zipper, bottom: Nothing, top: Nothing, immersed: Default
└── data: 370×190×1 OffsetArray(::Array{Float64, 3}, -4:365, -4:185, 1:1) with eltype Float64 with indices -4:365×-4:185×1:1
    └── max=6005.79, min=-9210.06, mean=-1960.37
```

cc @taimoorsohail 

Linked issue #476: Bathymetry info messages for skipping interpolation are tad misleading
There was a discussion that started regarding this in https://github.com/CliMA/ClimaOcean.jl/pull/388 (see https://github.com/CliMA/ClimaOcean.jl/pull/388#issuecomment-2709058675) but we haven't done anything about it.

After looking into it I think what the code is doing is correct (that is, what we'd expect it to do) but the info messages to the user are misleading.

The message would often be:

```julia
┌ Info: Skipping passes for interpolating bathymetry of size (21600, 10800, 1) 
│ to target grid of size (360, 180, 100). Interpolation passes may only 
│ be used to coarsen bathymetry and require that the bathymetry 
└ is finer than the target grid in both horizontal directions.
```

But what is meant to say here is that it's skipping the _extra_ interpolation passes that were passed by the user via `interpolation_passes` kwarg. There is _always_ one interpolation pass.

I refactored the messaging to the user in #475

cc @taimoorsohail
Linked issue comments: user: login: navidcy
created_at: 2025-04-18T00:03:57Z
body: With an improvement such that suggested in https://github.com/CliMA/ClimaOcean.jl/pull/393 (which wasn't merge on main btw) this might become irrelevant. But until that happens it'd be nice to make the info messages clearer. #475 attempts to do that.
author_association: MEMBER
user: login: glwagner
created_at: 2025-04-18T01:31:50Z
body: https://github.com/CliMA/ClimaOcean.jl/pull/466 just deletes the check and message. It's not useful (its a performance optimization that we don't need) and we need a better interface anyways.
author_association: MEMBER
user: login: navidcy
created_at: 2025-04-18T01:54:30Z
body: > [#466](https://github.com/CliMA/ClimaOcean.jl/pull/466) just deletes the check and message. It's not useful (its a performance optimization that we don't need) and we need a better interface anyways.

Fair, but #475 includes also more informative messages + validation of `interpolation_passes`.
I'll drop the check there.
author_association: MEMBER
user: login: glwagner
created_at: 2025-04-18T01:57:11Z
body: But I don't think we should even have a user interface that allows inputting interpolation passes 😢 
author_association: MEMBER
user: login: navidcy
created_at: 2025-04-18T02:00:16Z
body: Yes, I agree. But until that goes away users deserve ~meaningful~ not getting misleading info messages.
author_association: MEMBER
user: login: glwagner
created_at: 2025-04-18T02:01:54Z
body: I think I am proposing a different work that has identical effort but leads to a sustainable pattern that we can continue to work on
author_association: MEMBER
user: login: glwagner
created_at: 2025-04-18T02:02:26Z
body: change the kwarg to `smoothing` and define `InterpolationPasses(n)` and then define `n` as the _additional_ passes
author_association: MEMBER
user: login: glwagner
created_at: 2025-04-18T02:02:50Z
body: then vlidate in the constructor `InterpolationPasses`
author_association: MEMBER
user: login: glwagner
created_at: 2025-04-18T02:03:04Z
body: pretty much the same effort I think
author_association: MEMBER
user: login: navidcy
created_at: 2025-04-18T02:08:54Z
body: OK, I'll work on that in #475 
author_association: MEMBER

Diff:
diff --git a/src/Bathymetry.jl b/src/Bathymetry.jl
index 4b6cda0e9..7e570f606 100644
--- a/src/Bathymetry.jl
+++ b/src/Bathymetry.jl
@@ -9,9 +9,7 @@ using Oceananigans.DistributedComputations
 using Oceananigans
 using Oceananigans.Architectures: architecture, on_architecture
 using Oceananigans.DistributedComputations: DistributedGrid, reconstruct_global_grid, barrier!, all_reduce
-using Oceananigans.Grids: halo_size, λnodes, φnodes
-using Oceananigans.Grids: x_domain, y_domain
-using Oceananigans.Grids: topology
+using Oceananigans.Grids: halo_size, λnodes, φnodes, x_domain, y_domain, topology
 using Oceananigans.Utils: pretty_filesize, launch!
 using Oceananigans.Fields: interpolate!
 using Oceananigans.BoundaryConditions
@@ -126,6 +124,14 @@ function regrid_bathymetry(target_grid;
                            interpolation_passes = 1,
                            major_basins = 1) # Allow an `Inf` number of "lakes"
 
+    if isinteger(interpolation_passes)
+        interpolation_passes = convert(Int, interpolation_passes)
+    end
+
+    if interpolation_passes isa Nothing || !isa(interpolation_passes, Int) || interpolation_passes ≤ 0
+        return throw(ArgumentError("interpolation_passes has to be an integer ≥ 1"))
+    end
+
     filepath = download_bathymetry(; url, dir, filename)
     dataset = Dataset(filepath, "r")
 
@@ -202,6 +208,12 @@ end
 # Here we can either use `regrid!` (three dimensional version) or `interpolate!`.
 function interpolate_bathymetry_in_passes(native_z, target_grid;
                                           passes = 10)
+
+    gridtype = target_grid isa TripolarGrid ? "TripolarGrid" :
+               target_grid isa LatitudeLongitudeGrid ? "LatitudeLongitudeGrid" :
+               target_grid isa RectilinearGrid ? "RectilinearGrid" :
+               error("unknown target grid type")
+
     Nλt, Nφt = Nt = size(target_grid)
     Nλn, Nφn = Nn = size(native_z)
 
@@ -215,11 +227,25 @@ function interpolate_bathymetry_in_passes(native_z, target_grid;
     # If so, skip interpolation passes, as they are not needed.
     if resxt < resxn || resyt < resyn
         target_z = Field{Center, Center, Nothing}(target_grid)
+
+        @info "Interpolating bathymetry of size $Nn onto a $gridtype target grid of size $Nt"
         interpolate!(target_z, native_z)
-        @info string("Skipping passes for interpolating bathymetry of size $Nn ", '\n',
-                     "to target grid of size $Nt. Interpolation passes may only ", '\n',
-                     "be used to coarsen bathymetry and require that the bathymetry ", '\n',
-                     "is finer than the target grid in both horizontal directions.")
+
+        if passes > 0
+            passes_str = passes == 1 ? "1 interpolation pass" : "$passes interpolation passes"
+
+            @info string("Skipping extra $passes_str for bathymetry.", '\n',
+                        "Extra interpolation passes may only be used to coarsen bathymetry", '\n',
+                        "and require that the bathymetry is finer than the target grid in", '\n',
+                        "both horizontal directions.", '\n',
+                        "Grid info:", '\n',
+                        "target grid", '\n',
+                        "├── minimum x-spacings ", @sprintf("%.3e", resxt), " m", '\n',
+                        "└── minimum x-spacings ", @sprintf("%.3e", resyt), " m", '\n',
+                        "bathymetry grid", '\n',
+                        "├── minimum x-spacings ", @sprintf("%.3e", resxn), " m", '\n',
+                        "└── minimum x-spacings ", @sprintf("%.3e", resyn), " m")
+        end
         return target_z
     end
 
@@ -239,10 +265,10 @@ function interpolate_bathymetry_in_passes(native_z, target_grid;
     old_z  = native_z
     TX, TY = topology(target_grid)
 
+    @info "Interpolation passes of bathymetry size $(size(old_z)) onto a $gridtype target grid of size $Nt:"
     for pass = 1:passes - 1
         new_size = (Nλ[pass], Nφ[pass], 1)
-
-        @debug "Bathymetry interpolation pass $pass with size $new_size"
+        @info "    pass $pass to size $new_size"
 
         new_grid = LatitudeLongitudeGrid(architecture(target_grid), Float32,
                                          size = new_size,
@@ -257,6 +283,8 @@ function interpolate_bathymetry_in_passes(native_z, target_grid;
         old_z = new_z
     end
 
+    new_size = (Nλ[passes], Nφ[passes], 1)
+    @info "    pass $passes to size $new_size"
     target_z = Field{Center, Center, Nothing}(target_grid)
     interpolate!(target_z, old_z)
 
diff --git a/test/test_bathymetry.jl b/test/test_bathymetry.jl
index 7c5a56312..58af10e36 100644
--- a/test/test_bathymetry.jl
+++ b/test/test_bathymetry.jl
@@ -1,3 +1,5 @@
+include("runtests_setup.jl")
+
 using Oceananigans
 using Statistics
 using ClimaOcean
@@ -26,10 +28,10 @@ using ClimaOcean.Bathymetry: remove_minor_basins!
         # Test that remove_minor_basins!(Z, 2) remove the correct number of Basins
         bottom_height = Field{Center, Center, Nothing}(grid)
         control_bottom_height = Field{Center, Center, Nothing}(grid)
-        
-        # A two basins bathymetry
+
+        # A two-basins bathymetry
         bottom(x, y) = - 1000 * Int((x < 10) | (x > 50))
-        
+
         set!(bottom_height, bottom)
         set!(control_bottom_height, bottom)
 
@@ -39,12 +41,12 @@ using ClimaOcean.Bathymetry: remove_minor_basins!
 
         # This should have removed the left basin
         remove_minor_basins!(bottom_height, 1)
-        
-        # The remaning bottom cells that are not immersed should be only on the right hand side
+
+        # The remaining bottom cells that are not immersed should be only on the right hand side
         # The left half of the domain should be fully immersed, i.e., bottom == 0
         @test sum(view(bottom_height, 1:50, :, 1)) == 0
 
-        # While the right side should be not immersed, with a mean bottom depth 
+        # While the right side should be not immersed, with a mean bottom depth
         # of -1000 meters
         @test mean(view(bottom_height, 51:100, :, 1)) == -1000
 


Reviews:
user: login: glwagner
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-18T01:27:25Z
user: login: glwagner
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T02:03:33Z
user: login: navidcy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T02:06:07Z
user: login: navidcy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T02:06:51Z

Review comments:
diff_hunk: @@ -126,6 +124,14 @@ function regrid_bathymetry(target_grid;
                            interpolation_passes = 1,
path: src/Bathymetry.jl
user: login: glwagner
body: ```suggestion
                           smoothing = InterpolationPasses(0),
```
created_at: 2025-04-18T02:03:33Z
author_association: MEMBER
diff_hunk: @@ -126,6 +124,14 @@ function regrid_bathymetry(target_grid;
                            interpolation_passes = 1,
path: src/Bathymetry.jl
user: login: navidcy
body: are you suggesting we do the refactor of the bathymetry smoothing in this PR?
created_at: 2025-04-18T02:06:07Z
author_association: MEMBER
diff_hunk: @@ -126,6 +124,14 @@ function regrid_bathymetry(target_grid;
                            interpolation_passes = 1,
path: src/Bathymetry.jl
user: login: navidcy
body: it's not a one-line edit, it requires some more work
created_at: 2025-04-18T02:06:51Z
author_association: MEMBER

PR comments:
user: login: navidcy
created_at: 2025-04-18T01:22:32Z
body: Since there is always 1 interpolation pass it makes no sense to be able to provide `intepolation_passes = 0` or `interpolation_passes = nothing`. This misleads the user thinking that there won't be any interpolation. I added a validation to throw an informative error if user does that.
author_association: MEMBER
user: login: glwagner
created_at: 2025-04-18T01:26:40Z
body: lets do this --- but do we agree that we should actually eliminate this code in favor of better bathymetry smoothing methods?
author_association: MEMBER
user: login: glwagner
created_at: 2025-04-18T01:27:11Z
body: I'd advise against working on input validation since it is potentially wasted effort
author_association: MEMBER
user: login: glwagner
created_at: 2025-04-18T01:28:18Z
body: The problem is that the first interpolation is not a "pass". The first interpolation is necessary. What happens after is smoothing. We should not conflate the necessary step with the smoothing. Instead, we need an interface for smoothing. Implementing an interface for bathymetry smoothing is far preferred to complicated validation of a confusing interface in my opinion.
author_association: MEMBER
user: login: navidcy
created_at: 2025-04-18T02:05:20Z
body: > The problem is that the first interpolation is not a "pass".

Yes, that was part of the misleading messaging; this PR fixes that.

> The first interpolation is necessary. What happens after is smoothing. We should not conflate the necessary step with the smoothing.

> Instead, we need an interface for smoothing.

Totally agree. Let's merge this and work on something like #393. What's wrong with having better messaging until we fix it. The only argument is perhaps wasted of time. But now I've done it, I wasted that time unfortunately.. :(

> Implementing an interface for bathymetry smoothing is far preferred to complicated validation of a confusing interface in my opinion.

Agree
author_association: MEMBER
user: login: navidcy
created_at: 2025-04-18T02:59:25Z
body: closing this in favor of #478 
author_association: MEMBER
user: login: navidcy
created_at: 2025-04-18T05:05:13Z
body: > lets do this --- but do we agree that we should actually eliminate this code in favor of better bathymetry smoothing methods?

totally agree
and your idea of applying the smoothing to the native grid of the bathymetry _before_ interpolate make sense
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-04-18T06:05:41Z
body: ## [Codecov](https://app.codecov.io/gh/CliMA/ClimaOcean.jl/pull/475?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CliMA) Report
Attention: Patch coverage is `41.17647%` with `10 lines` in your changes missing coverage. Please review.
> Project coverage is 25.48%. Comparing base [(`33e4392`)](https://app.codecov.io/gh/CliMA/ClimaOcean.jl/commit/33e4392b3811d0f9f474352c266594f90dc0c334?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CliMA) to head [(`c499a8b`)](https://app.codecov.io/gh/CliMA/ClimaOcean.jl/commit/c499a8bee8a00d1bf865c3bb982c2b2062319beb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CliMA).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/CliMA/ClimaOcean.jl/pull/475?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CliMA) | Patch % | Lines |
|---|---|---|
| [src/Bathymetry.jl](https://app.codecov.io/gh/CliMA/ClimaOcean.jl/pull/475?src=pr&el=tree&filepath=src%2FBathymetry.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CliMA#diff-c3JjL0JhdGh5bWV0cnkuamw=) | 41.17% | [10 Missing :warning: ](https://app.codecov.io/gh/CliMA/ClimaOcean.jl/pull/475?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CliMA) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #475      +/-   ##
==========================================
+ Coverage   25.35%   25.48%   +0.12%     
==========================================
  Files          47       47              
  Lines        2571     2586      +15     
==========================================
+ Hits          652      659       +7     
- Misses       1919     1927       +8     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/CliMA/ClimaOcean.jl/pull/475?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CliMA).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CliMA).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE