Repository: CliMA/ClimaLand.jl
Pull request #1109: Automatically pick mask resolution
Author: login: Sbozzolo
Created: 2025-04-15T16:12:15Z | Merged: 2025-04-16T22:08:12Z

All the information needed to select the resolution for the land-sea mask is in the space. This commit automatically select the correct file to use when working with land-sea masks.



Diff:
diff --git a/experiments/long_runs/land.jl b/experiments/long_runs/land.jl
index 3f43dda6921..29c36f8e22e 100644
--- a/experiments/long_runs/land.jl
+++ b/experiments/long_runs/land.jl
@@ -373,7 +373,7 @@ function setup_prob(
     diag_cb = ClimaDiagnostics.DiagnosticsCallback(diagnostic_handler)
 
     driver_cb = ClimaLand.DriverUpdateCallback(updateat, updatefunc)
-    mask = ClimaLand.landsea_mask(surface_space)
+    mask = ClimaLand.landsea_mask(domain)
     nancheck_freq = Dates.Month(1)
     nancheck_cb = ClimaLand.NaNCheckCallback(
         nancheck_freq,
diff --git a/experiments/long_runs/snowy_land.jl b/experiments/long_runs/snowy_land.jl
index d186b303b54..dda9a8432ea 100644
--- a/experiments/long_runs/snowy_land.jl
+++ b/experiments/long_runs/snowy_land.jl
@@ -430,7 +430,7 @@ function setup_prob(
     end
     report_cb = SciMLBase.DiscreteCallback(every1000steps, report)
 
-    mask = ClimaLand.landsea_mask(surface_space)
+    mask = ClimaLand.landsea_mask(domain)
     nancheck_freq = Dates.Month(1)
     nancheck_cb = ClimaLand.NaNCheckCallback(
         nancheck_freq,
diff --git a/experiments/long_runs/soil.jl b/experiments/long_runs/soil.jl
index 25792eaea0e..0981b3ad3f4 100644
--- a/experiments/long_runs/soil.jl
+++ b/experiments/long_runs/soil.jl
@@ -217,7 +217,7 @@ function setup_prob(
     diag_cb = ClimaDiagnostics.DiagnosticsCallback(diagnostic_handler)
 
     driver_cb = ClimaLand.DriverUpdateCallback(updateat, updatefunc)
-    mask = ClimaLand.landsea_mask(surface_space)
+    mask = ClimaLand.landsea_mask(domain)
     nancheck_freq = Dates.Month(1)
     nancheck_cb = ClimaLand.NaNCheckCallback(
         nancheck_freq,
diff --git a/src/shared_utilities/Domains.jl b/src/shared_utilities/Domains.jl
index 3dc75b45dc6..2ac358bd7ff 100644
--- a/src/shared_utilities/Domains.jl
+++ b/src/shared_utilities/Domains.jl
@@ -971,6 +971,77 @@ depth(space::ClimaCore.Spaces.CenterFiniteDifferenceSpace) =
         space.grid.topology.mesh.domain.coord_min
     ).z
 
+"""
+    horizontal_resolution_degrees(domain::AbstractDomain)
+
+Return a tuple with the approximate resolution on the domain in degrees along
+the two directions.
+
+For boxes and planes, the order is `(latitude, longitude)`.
+
+Examples
+=======
+
+```jldoctest
+julia> using ClimaLand
+
+julia> domain = ClimaLand.Domains.SphericalShell(;
+        radius = 6300e3,
+        depth = 15.,
+        nelements = (10, 3),
+        dz_tuple = ((1.0, 0.05)),
+    );
+
+julia> ClimaLand.Domains.average_horizontal_resolution_degrees(domain)
+(9.0, 9.0)
+
+julia> domain = ClimaLand.Domains.Plane(;
+        xlim = (50000.0, 80000.),
+        ylim = (30000.0, 40000.),
+        longlat = (-118.14452, 34.14778),
+        nelements = (20, 3),
+    );
+
+julia> ClimaLand.Domains.average_horizontal_resolution_degrees(domain)
+(0.05839157, 0.20961125)
+```
+"""
+function average_horizontal_resolution_degrees(
+    domain::Union{SphericalShell, SphericalSurface},
+)
+    num_elements_lat = num_elements_lon = first(domain.nelements)
+    quad = ClimaCore.Spaces.quadrature_style(domain.space.surface)
+    num_points_per_element =
+        ClimaCore.Quadratures.unique_degrees_of_freedom(quad)
+
+    # There are 2 full cubed-sphere panels along latitudes, and 4 along
+    # longitudes
+    return (
+        180 / (2 * num_points_per_element * num_elements_lat),
+        360 / (4 * num_points_per_element * num_elements_lon),
+    )
+end
+
+function average_horizontal_resolution_degrees(domain::Union{Plane, HybridBox})
+    isnothing(domain.longlat) &&
+        error("Can only compute resolution with domains in latlong")
+
+    # x is Lat, y is Lon because ClimaCore defines LatLongPoints
+    num_elements_lat, num_elements_lon = domain.nelements[1:2]
+    delta_lat = domain.xlim[2] - domain.xlim[1]
+    delta_lon = domain.ylim[2] - domain.ylim[1]
+
+    quad = ClimaCore.Spaces.quadrature_style(domain.space.surface)
+    num_points_per_element =
+        ClimaCore.Quadratures.unique_degrees_of_freedom(quad)
+
+    return (
+        delta_lat / (num_points_per_element * num_elements_lat),
+        delta_lon / (num_points_per_element * num_elements_lon),
+    )
+end
+
+
 export AbstractDomain
 export Column, Plane, HybridBox, Point, SphericalShell, SphericalSurface
 export coordinates,
@@ -981,5 +1052,6 @@ export coordinates,
     top_face_to_surface,
     obtain_surface_domain,
     linear_interpolation_to_surface!,
-    get_Δz
+    get_Δz,
+    average_horizontal_resolution_degrees
 end
diff --git a/src/shared_utilities/utils.jl b/src/shared_utilities/utils.jl
index ae4f968e025..066b168cd0b 100644
--- a/src/shared_utilities/utils.jl
+++ b/src/shared_utilities/utils.jl
@@ -658,7 +658,9 @@ apply_threshold(field, value) =
 """
     landsea_mask(
         surface_space;
-        resolution = "60arcs",
+        filepath = ClimaLand.Artifacts.landseamask_file_path(;
+                 context = ClimaComms.context(surface_space),
+                 ),
         threshold = 0.5,
         regridder_type = :InterpolationsRegridder,
         extrapolation_bc = (
@@ -668,16 +670,14 @@ apply_threshold(field, value) =
         ),
     )
 
-Reads in the default Clima 60arcsecond land/sea mask, regrids to the
-`surface_space`, and treats any point with a land fraction < threshold
-as ocean.
-
-A 1degree (resolution = "1deg") and 30arcsecond (resolution = "30arcs") mask
-are also available.
+Reads in the default Clima land/sea mask, regrids to the `surface_space`, and
+treats any point with a land fraction < threshold as ocean.
 """
 function landsea_mask(
     surface_space;
-    resolution = "60arcs",
+    filepath = ClimaLand.Artifacts.landseamask_file_path(;
+        context = ClimaComms.context(surface_space),
+    ),
     threshold = 0.5,
     regridder_type = :InterpolationsRegridder,
     extrapolation_bc = (
@@ -686,11 +686,6 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
-    context = ClimaComms.context(surface_space)
-    filepath = ClimaLand.Artifacts.landseamask_file_path(;
-        resolution = resolution,
-        context = context,
-    )
     mask = SpaceVaryingInput(
         filepath,
         "landsea",
@@ -701,3 +696,23 @@ function landsea_mask(
     binary_mask = apply_threshold.(mask, threshold)
     return binary_mask
 end
+
+function landsea_mask(domain::Domains.AbstractDomain; kwargs...)
+    # average_horizontal_resolution_degrees returns a tuple with the resolution
+    # along the two directions, so we take the minimum
+    resolution_degrees = minimum(average_horizontal_resolution_degrees(domain))
+    if resolution_degrees > 1
+        resolution = "1deg"
+    else
+        resolution_arcsec = 3600resolution_degrees
+        # Pick the landsea mask at 60 arcseconds if the nodal distance is more than
+        # 120", otherwise pick the higher resolution
+        resolution = resolution_arcsec > 120 ? "60arcs" : "30arcs"
+    end
+
+    filepath = ClimaLand.Artifacts.landseamask_file_path(;
+        resolution,
+        context = ClimaComms.context(domain.space.surface),
+    )
+    return landsea_mask(domain.space.surface; filepath, kwargs...)
+end
diff --git a/src/simulations/domains.jl b/src/simulations/domains.jl
index 9b19faf8e12..20e5fee88fa 100644
--- a/src/simulations/domains.jl
+++ b/src/simulations/domains.jl
@@ -39,7 +39,6 @@ rather than increasing the polynomial order.
 function global_domain(
     FT;
     apply_mask = true,
-    mask_resolution = "60arcs",
     mask_threshold = 0.5,
     nelements = (101, 15),
     dz_tuple = (10.0, 0.05),
@@ -63,11 +62,7 @@ function global_domain(
     )
     if apply_mask
         surface_space = domain.space.surface # 2d space
-        binary_mask = landsea_mask(
-            surface_space;
-            resolution = mask_resolution,
-            threshold = mask_threshold,
-        )
+        binary_mask = landsea_mask(domain; threshold = mask_threshold)
         Spaces.set_mask!(surface_space, binary_mask)
     end
 
diff --git a/test/shared_utilities/domains.jl b/test/shared_utilities/domains.jl
index c2f25f38784..ae08c4a3f2b 100644
--- a/test/shared_utilities/domains.jl
+++ b/test/shared_utilities/domains.jl
@@ -16,7 +16,8 @@ using ClimaLand.Domains:
     obtain_face_space,
     obtain_surface_domain,
     get_Δz,
-    top_face_to_surface
+    top_face_to_surface,
+    average_horizontal_resolution_degrees
 
 FT = Float32
 @testset "Clima Core Domains, FT = $FT" begin
@@ -98,6 +99,9 @@ FT = Float32
     @test ClimaComms.context(shell_surface) == ClimaComms.context()
     @test ClimaComms.device(shell_surface) == ClimaComms.device()
 
+    @test average_horizontal_resolution_degrees(shell_surface) ==
+          (180 / (2 * n_elements_sphere[1]), 180 / (2n_elements_sphere[1]))
+
     # HybridBox
     box = HybridBox(;
         xlim = xlim,
@@ -171,6 +175,8 @@ FT = Float32
     @test typeof(xy_plane.space.surface) <:
           ClimaCore.Spaces.SpectralElementSpace2D
 
+    @test_throws ErrorException average_horizontal_resolution_degrees(xy_plane)
+
     # Plane latlong
     dxlim = (FT(50_000), FT(80_000))
     dylim = (FT(30_000), FT(40_000))
@@ -198,6 +204,12 @@ FT = Float32
     @test typeof(longlat_plane.space.surface) <:
           ClimaCore.Spaces.SpectralElementSpace2D
 
+    expected_resolution_x = (xlim_longlat[2] - xlim_longlat[1]) / nelements[1]
+    expected_resolution_y = (ylim_longlat[2] - ylim_longlat[1]) / nelements[2]
+
+    @test average_horizontal_resolution_degrees(longlat_plane) ==
+          (expected_resolution_x, expected_resolution_y)
+
     # Box latlong
     longlat_box = HybridBox(;
         xlim = dxlim,
@@ -244,6 +256,9 @@ FT = Float32
         (; surface = longlat_box.space.surface),
     )
 
+    @test average_horizontal_resolution_degrees(longlat_box) ==
+          (expected_resolution_x, expected_resolution_y)
+
     # Column
 
     z_column = Column(; zlim = zlim, nelements = nelements[3])


Reviews:
user: login: kmdeck
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T16:37:16Z
user: login: kmdeck
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T16:37:24Z
user: login: Sbozzolo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T16:39:38Z
user: login: kmdeck
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T17:47:40Z
user: login: Sbozzolo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T17:52:32Z
user: login: charleskawczynski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T19:09:14Z
user: login: charleskawczynski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T19:10:31Z
user: login: charleskawczynski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T19:10:54Z
user: login: charleskawczynski
body: Overall I'm wondering if we can write more generic code that won't need changed
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T19:11:31Z
user: login: Sbozzolo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T19:44:08Z
user: login: Sbozzolo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T00:08:24Z
user: login: Sbozzolo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T00:58:48Z
user: login: Sbozzolo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T01:21:42Z
user: login: kmdeck
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T18:58:52Z
user: login: kmdeck
body: thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-16T18:59:30Z
user: login: Sbozzolo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T20:47:44Z
user: login: Sbozzolo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T20:47:53Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c3ef3ae24123364ed38d65d50b7c931cccf2f309 and 7d48884958483b0a34dd91a592e553ef7db03fba.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `experiments/long_runs/land.jl` (1 hunks)
* `experiments/long_runs/snowy_land.jl` (1 hunks)
* `experiments/long_runs/soil.jl` (1 hunks)
* `src/shared_utilities/Domains.jl` (2 hunks)
* `src/shared_utilities/utils.jl` (3 hunks)
* `src/simulations/domains.jl` (1 hunks)
* `test/shared_utilities/domains.jl` (5 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* GitHub Check: ci 1.11 - windows-latest
* GitHub Check: ci 1.11 - ubuntu-latest
* GitHub Check: test (1.11)
* GitHub Check: ci 1.10 - windows-latest
* GitHub Check: test (1.10)
* GitHub Check: downstream ClimaCoupler.jl (1.11)
* GitHub Check: docbuild
* GitHub Check: downstream ClimaCoupler.jl (1.10)
* GitHub Check: ci 1.10 - ubuntu-latest

</details>

<details>
<summary>🔇 Additional comments (15)</summary><blockquote>

<details>
<summary>src/shared_utilities/Domains.jl (4)</summary>

`974-1008`: **Well-structured docstring with clear examples**

The docstring is comprehensive, with good examples demonstrating the function usage for different domain types.

---

`1009-1023`: **Implementation for spherical domains looks correct**

The function calculates resolution based on number of elements and quadrature points, with appropriate prefactors for latitude (2) and longitude (4) panels in the cubed-sphere layout.

---

`1025-1042`: **Good error handling for non-latlong domains**

The function properly errors when attempting to compute resolution for domains without latitude/longitude coordinates. The resolution calculation correctly uses the domain properties.

---

`1056-1056`: **Function properly exported**

The new function is correctly added to the export list.

</details>
<details>
<summary>experiments/long_runs/soil.jl (1)</summary>

`220-220`: **Updated to use domain instead of surface_space**

The code now passes the full domain object to `landsea_mask` instead of just the surface space, leveraging the new automatic resolution selection capability.

</details>
<details>
<summary>experiments/long_runs/snowy_land.jl (1)</summary>

`433-433`: **Updated to use domain instead of surface_space**

The code now passes the full domain object to `landsea_mask` instead of just the surface space, consistent with the changes in other experiment files.

</details>
<details>
<summary>experiments/long_runs/land.jl (1)</summary>

`376-376`: **Updated to use domain instead of surface_space**

The code now passes the full domain object to `landsea_mask` instead of just the surface space, maintaining consistency with other experiment files.

</details>
<details>
<summary>src/simulations/domains.jl (1)</summary>

`65-65`: **Good change: simplified mask creation by using domain directly.**

Improves code by leveraging the new automatic resolution selection capability of `landsea_mask`.

</details>
<details>
<summary>test/shared_utilities/domains.jl (4)</summary>

`19-20`: **Function import correctly added.**

Added import for the new utility function.

---

`102-103`: **Good test for spherical surface resolution.**

Properly verifies the expected resolution calculation for spherical domains.

---

`178-178`: **Good error test for unsupported domain type.**

Correctly verifies that the function throws an error for plane domains without lat-long coordinates.

---

`207-211`: **Good test for plane with lat-long coordinates.**

Correctly tests resolution calculation for lat-long plane domains.

</details>
<details>
<summary>src/shared_utilities/utils.jl (3)</summary>

`658-675`: **Updated docstring and default parameters.**

Function documentation updated to match new implementation.

---

`677-688`: **Refactored function to accept filepath directly.**

Improves flexibility by allowing direct specification of mask file.

---

`700-718`: **Good implementation of automatic resolution selection.**

New method intelligently selects mask resolution based on domain properties:
- > 1° → "1deg"
- > 120″ → "60arcs"
- ≤ 120″ → "30arcs"

This achieves the PR objective of automatic resolution selection.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T20:51:09Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/shared_utilities/utils.jl (1)</summary><blockquote>

`708-711`: **Consider adding a comment explaining the resolution thresholds**

The thresholds for selecting mask resolution (120 arcseconds) could benefit from a brief comment explaining the reasoning behind this specific value.

```diff
 # Pick the landsea mask at 60 arcseconds if the nodal distance is more than
-# 120", otherwise pick the higher resolution
+# 120" (twice the 60" resolution), otherwise pick the higher resolution
 resolution = resolution_arcsec > 120 ? "60arcs" : "30arcs"
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7d48884958483b0a34dd91a592e553ef7db03fba and d0fecae4ed8775e064dd1e50c42328e79af0aac6.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `experiments/long_runs/land.jl` (1 hunks)
* `experiments/long_runs/snowy_land.jl` (1 hunks)
* `experiments/long_runs/soil.jl` (1 hunks)
* `src/shared_utilities/Domains.jl` (2 hunks)
* `src/shared_utilities/utils.jl` (3 hunks)
* `src/simulations/domains.jl` (1 hunks)
* `test/shared_utilities/domains.jl` (5 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (6)</summary>

* experiments/long_runs/soil.jl
* experiments/long_runs/land.jl
* experiments/long_runs/snowy_land.jl
* src/simulations/domains.jl
* test/shared_utilities/domains.jl
* src/shared_utilities/Domains.jl

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (11)</summary>

* GitHub Check: ci 1.11 - windows-latest
* GitHub Check: ci 1.11 - macOS-latest
* GitHub Check: ci 1.11 - ubuntu-latest
* GitHub Check: ci 1.10 - windows-latest
* GitHub Check: ci 1.10 - macOS-latest
* GitHub Check: ci 1.10 - ubuntu-latest
* GitHub Check: test (1.11)
* GitHub Check: test (1.10)
* GitHub Check: downstream ClimaCoupler.jl (1.11)
* GitHub Check: downstream ClimaCoupler.jl (1.10)
* GitHub Check: docbuild

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>src/shared_utilities/utils.jl (2)</summary>

`658-698`: **Improved flexibility in mask file handling**

The refactored `landsea_mask` function now accepts a file path directly rather than determining it internally from a resolution parameter. This enhances flexibility and simplifies the API.

---

`700-718`: **Good implementation of automatic resolution selection**

The new overload for `landsea_mask` that accepts a domain object intelligently selects the appropriate resolution based on the domain's characteristics. The logic handles three resolution cases appropriately:
- > 1 degree: uses "1deg" mask
- > 120 arcseconds: uses "60arcs" mask
- ≤ 120 arcseconds: uses "30arcs" mask

This reduces the burden on users to specify the correct resolution manually.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T21:10:03Z

Review comments:
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
path: src/shared_utilities/utils.jl
user: login: kmdeck
body: I am actually adding a helper function for this in a separate PR:
```
"""
    effective_horizontal_resolution(nelements, npolynomial)
Helper function which returns the effective horizontal resolution of 
a global simulation in degrees
"""
function effective_horizontal_resolution(nelements, npolynomial)
    if npolynomial == 0
        return sqrt(360 * 180 / (nelements)^2 / 6)
    else
        return sqrt(360 * 180 / (nelements * npolynomial)^2 / 6)
    end
end
```

Do you want to add this to ClimaLand.Domains.jl instead in this PR?
created_at: 2025-04-15T16:37:16Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
+    # degrees by taking the nodal_distance_meters and diving by 1e5
+    nodal_distance_in_meters =
+        ClimaCore.Spaces.node_horizontal_length_scale(surface_space)
+    one_degree_in_meters = 100_000
+
+    nodal_distance_in_degrees = nodal_distance_in_meters / one_degree_in_meters
+    nodal_distance_in_arcsec = nodal_distance_in_degrees * 3600
+
+    # Pick the landsea mask at 60 arcseconds if the nodal distance is more than
+    # 120", otherwise pick the higher resolution
+    resultion = nodal_distance_in_arcsec > 120 ? "60arcs" : "30arcs"
path: src/shared_utilities/utils.jl
user: login: kmdeck
body: resolution
created_at: 2025-04-15T16:37:24Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
path: src/shared_utilities/utils.jl
user: login: Sbozzolo
body: I think this would work only for a global simulation, but we also have regional simulations, so I would recommend using the functions I used in this PR
created_at: 2025-04-15T16:39:38Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
path: src/shared_utilities/utils.jl
user: login: kmdeck
body: Could you add it to src/shared_utilities/Domains.jl?
created_at: 2025-04-15T17:47:40Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
path: src/shared_utilities/utils.jl
user: login: Sbozzolo
body: Yes
created_at: 2025-04-15T17:52:32Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
+    # degrees by taking the nodal_distance_meters and diving by 1e5
+    nodal_distance_in_meters =
+        ClimaCore.Spaces.node_horizontal_length_scale(surface_space)
+    one_degree_in_meters = 100_000
+
+    nodal_distance_in_degrees = nodal_distance_in_meters / one_degree_in_meters
+    nodal_distance_in_arcsec = nodal_distance_in_degrees * 3600
+
+    # Pick the landsea mask at 60 arcseconds if the nodal distance is more than
+    # 120", otherwise pick the higher resolution
+    resolution = nodal_distance_in_arcsec > 120 ? "60arcs" : "30arcs"
path: src/shared_utilities/utils.jl
user: login: kmdeck
body: If the resolution is coarse enough, would we pick the 1 degree mask?
created_at: 2025-04-15T18:20:42Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
path: src/shared_utilities/utils.jl
user: login: charleskawczynski
body: Shouldn't such a function live in ClimaCore?
created_at: 2025-04-15T19:09:14Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
+    # degrees by taking the nodal_distance_meters and diving by 1e5
+    nodal_distance_in_meters =
+        ClimaCore.Spaces.node_horizontal_length_scale(surface_space)
+    one_degree_in_meters = 100_000
+
+    nodal_distance_in_degrees = nodal_distance_in_meters / one_degree_in_meters
+    nodal_distance_in_arcsec = nodal_distance_in_degrees * 3600
+
+    # Pick the landsea mask at 60 arcseconds if the nodal distance is more than
+    # 120", otherwise pick the higher resolution
+    resolution = nodal_distance_in_arcsec > 120 ? "60arcs" : "30arcs"
path: src/shared_utilities/utils.jl
user: login: charleskawczynski
body: I'm not really a fan of the hard-coded things here, but if they really will never change, that's fine with me.
created_at: 2025-04-15T19:10:31Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
+    # degrees by taking the nodal_distance_meters and diving by 1e5
+    nodal_distance_in_meters =
+        ClimaCore.Spaces.node_horizontal_length_scale(surface_space)
+    one_degree_in_meters = 100_000
path: src/shared_utilities/utils.jl
user: login: charleskawczynski
body: Can you explain what this is?
created_at: 2025-04-15T19:10:54Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
+    # degrees by taking the nodal_distance_meters and diving by 1e5
+    nodal_distance_in_meters =
+        ClimaCore.Spaces.node_horizontal_length_scale(surface_space)
+    one_degree_in_meters = 100_000
path: src/shared_utilities/utils.jl
user: login: Sbozzolo
body: What specifically?
created_at: 2025-04-15T19:44:08Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
path: src/shared_utilities/utils.jl
user: login: Sbozzolo
body: > Shouldn't such a function live in ClimaCore?

I am not sure how to do this in full generality in ClimaCore. 

The function makes use of an assumption that is true in ClimaLand but not in ClimaCore: that we are on planet Earth (so that 90 degrees are 10e6 meters by definition).
This assumption is also used in our regional simulations, which are boxes with LatLong coordinates. The associated spaces do not contain the radius.

One way we could do this is by taking the difference between adjacent points of the `coordinate_field`, but I don't know how to do that.


created_at: 2025-04-16T00:08:24Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
path: src/shared_utilities/utils.jl
user: login: Sbozzolo
body: I realized there's a simple way I can do this
created_at: 2025-04-16T00:58:48Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
path: src/shared_utilities/utils.jl
user: login: Sbozzolo
body: I am not sure. There seems to be no easy way to determine how many elements we have along each direction of the horizontal coordinates.
created_at: 2025-04-16T01:21:41Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
+    # degrees by taking the nodal_distance_meters and diving by 1e5
+    nodal_distance_in_meters =
+        ClimaCore.Spaces.node_horizontal_length_scale(surface_space)
+    one_degree_in_meters = 100_000
+
+    nodal_distance_in_degrees = nodal_distance_in_meters / one_degree_in_meters
+    nodal_distance_in_arcsec = nodal_distance_in_degrees * 3600
+
+    # Pick the landsea mask at 60 arcseconds if the nodal distance is more than
+    # 120", otherwise pick the higher resolution
+    resolution = nodal_distance_in_arcsec > 120 ? "60arcs" : "30arcs"
path: src/shared_utilities/utils.jl
user: login: kmdeck
body: at least in this file, the mask we read (from a particular clima artifact) only has three options for resolution (1 deg, 60 ", 30 "). so I think the hardcoding is OK
created_at: 2025-04-16T16:12:04Z
author_association: MEMBER
diff_hunk: @@ -982,6 +982,74 @@ depth(space::ClimaCore.Spaces.CenterFiniteDifferenceSpace) =
         space.grid.topology.mesh.domain.coord_min
     ).z
 
+"""
+    horizontal_resolution_degrees(domain::AbstractDomain)
+
+Return a tuple with the approximate resolution on the domain in degrees along
+the two directions.
+
+For boxes and planes, the order is `(latitude, longitude)`.
+
+Examples
+=======
+
+```jldoctest
+julia> using ClimaLand
+
+julia> domain = ClimaLand.Domains.SphericalShell(;
+        radius = 6300e3,
+        depth = 15.,
+        nelements = (10, 3),
+        dz_tuple = ((1.0, 0.05)),
+    );
+
+julia> ClimaLand.Domains.average_horizontal_resolution_degrees(domain)
+(9.0, 9.0)
+
+julia> domain = ClimaLand.Domains.Plane(;
+        xlim = (50000.0, 80000.),
+        ylim = (30000.0, 40000.),
+        longlat = (-118.14452, 34.14778),
+        nelements = (20, 3),
+    );
+
+julia> ClimaLand.Domains.average_horizontal_resolution_degrees(domain)
+(0.05839157, 0.20961125)
+```
+"""
+function average_horizontal_resolution_degrees(
+    domain::Union{SphericalShell, SphericalSurface},
+)
+    num_elements_lat = num_elements_lon = first(domain.nelements)
+    quad = ClimaCore.Spaces.quadrature_style(domain.space.surface)
+    num_points_per_element =
+        ClimaCore.Quadratures.unique_degrees_of_freedom(quad)
+    return (
+        180 / (2 * num_points_per_element * num_elements_lat),
path: src/shared_utilities/Domains.jl
user: login: kmdeck
body: Could you add a comment about the 2 vs 4 prefactor for future users?
created_at: 2025-04-16T18:58:49Z
author_association: MEMBER
diff_hunk: @@ -686,6 +680,19 @@ function landsea_mask(
         Interpolations.Flat(),
     ),
 )
+    # Assuming 1 degree is 100 km = 1e5 m, we can estimate the resolution in
+    # degrees by taking the nodal_distance_meters and diving by 1e5
+    nodal_distance_in_meters =
+        ClimaCore.Spaces.node_horizontal_length_scale(surface_space)
+    one_degree_in_meters = 100_000
+
+    nodal_distance_in_degrees = nodal_distance_in_meters / one_degree_in_meters
+    nodal_distance_in_arcsec = nodal_distance_in_degrees * 3600
+
+    # Pick the landsea mask at 60 arcseconds if the nodal distance is more than
+    # 120", otherwise pick the higher resolution
+    resolution = nodal_distance_in_arcsec > 120 ? "60arcs" : "30arcs"
path: src/shared_utilities/utils.jl
user: login: Sbozzolo
body: The previous behavior was to pick 60 arcs, but I changed to pick 1 deg
created_at: 2025-04-16T20:47:44Z
author_association: MEMBER
diff_hunk: @@ -982,6 +982,74 @@ depth(space::ClimaCore.Spaces.CenterFiniteDifferenceSpace) =
         space.grid.topology.mesh.domain.coord_min
     ).z
 
+"""
+    horizontal_resolution_degrees(domain::AbstractDomain)
+
+Return a tuple with the approximate resolution on the domain in degrees along
+the two directions.
+
+For boxes and planes, the order is `(latitude, longitude)`.
+
+Examples
+=======
+
+```jldoctest
+julia> using ClimaLand
+
+julia> domain = ClimaLand.Domains.SphericalShell(;
+        radius = 6300e3,
+        depth = 15.,
+        nelements = (10, 3),
+        dz_tuple = ((1.0, 0.05)),
+    );
+
+julia> ClimaLand.Domains.average_horizontal_resolution_degrees(domain)
+(9.0, 9.0)
+
+julia> domain = ClimaLand.Domains.Plane(;
+        xlim = (50000.0, 80000.),
+        ylim = (30000.0, 40000.),
+        longlat = (-118.14452, 34.14778),
+        nelements = (20, 3),
+    );
+
+julia> ClimaLand.Domains.average_horizontal_resolution_degrees(domain)
+(0.05839157, 0.20961125)
+```
+"""
+function average_horizontal_resolution_degrees(
+    domain::Union{SphericalShell, SphericalSurface},
+)
+    num_elements_lat = num_elements_lon = first(domain.nelements)
+    quad = ClimaCore.Spaces.quadrature_style(domain.space.surface)
+    num_points_per_element =
+        ClimaCore.Quadratures.unique_degrees_of_freedom(quad)
+    return (
+        180 / (2 * num_points_per_element * num_elements_lat),
path: src/shared_utilities/Domains.jl
user: login: Sbozzolo
body: Added a comment
created_at: 2025-04-16T20:47:53Z
author_association: MEMBER
diff_hunk: @@ -244,6 +256,9 @@ FT = Float32
         (; surface = longlat_box.space.surface),
     )
 
+    @test average_horizontal_resolution_degrees(longlat_plane) ==
+          (expected_resolution_x, expected_resolution_y)
path: test/shared_utilities/domains.jl
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Duplicate test should be removed.**

This test repeats exactly what's already tested on lines 210-211.

```diff
-    @test average_horizontal_resolution_degrees(longlat_plane) ==
-          (expected_resolution_x, expected_resolution_y)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion

`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-16T20:51:08Z
author_association: NONE

PR comments:
user: login: Sbozzolo
created_at: 2025-04-15T19:46:16Z
body: > Overall I'm wondering if we can write more generic code that won't need changed

What do you suggest?

I am not sure how to make this more generic.
author_association: MEMBER
user: login: kmdeck
created_at: 2025-04-16T16:11:00Z
body: I think if we make this function take the (ClimaLand) domain, it's straightforward:
`get_horizontal_resolution_degrees(sphere_domain) = sqrt(360*180/sphere_domain.nelements^2/6)`
`get_horizontal_resolution_degrees(hybrid_box) = extracts dx,dy, and center lat from the domain, and computes the resolution in degrees` This can error if center lat/lon not provided.
`get_horizontal_resolution_meters(hybrid_box) = extracts |dx|, and center lat from the domain, and computes the resolution in meters` or just returns |dx| if not center_lat/lon not provided

author_association: MEMBER
user: login: Sbozzolo
created_at: 2025-04-16T17:59:07Z
body: I changed it to use `Domain`s and to return a tuple with the resolution on the two horizontal directions, and I changed the landsea mask function to be more generic and take directly a filepath.
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-04-16T20:51:05Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change revises land-sea mask generation by switching from using only the surface spatial domain (`surface_space`) to the full domain (`domain`), which includes surface and subsurface layers. The `landsea_mask` function is refactored to remove the `resolution` argument, now accepting a mask file path directly, and a new method overload is added to select the mask file automatically based on the domain’s average horizontal resolution. To support this, a new function `average_horizontal_resolution_degrees` is introduced to compute approximate horizontal resolution for various domain types. The `global_domain` function and multiple experiment scripts are updated to use the new `landsea_mask` interface. Tests are added to validate resolution computations. No public API signatures are changed except for removing the `mask_resolution` keyword from `global_domain`.

## Possibly related PRs

- CliMA/ClimaLand.jl#1108: Adjusts mask usage and compatibility functions based on ClimaCore version, which is related to mask handling but does not directly modify the `landsea_mask` interface or its argument structure.

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=CliMA/ClimaLand.jl&utm_content=1109):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE