Skip to content

Commit b846df0

Browse files
Merge pull request #2455 from CliMA/as/bilinear-horizontal-interp
Simpler horizontal remapping methods for diagnostics
2 parents d8e32ba + 31e34ad commit b846df0

File tree

13 files changed

+1231
-72
lines changed

13 files changed

+1231
-72
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ ClimaCore.jl Release Notes
44
main
55
-------
66

7+
- Add support for bilinear interpolation for diagnostics to the Remapper. [2455](https://github.com/CliMA/ClimaCore.jl/pull/2455)
8+
79
v0.14.49
810
-------
911
- Add `PressureInterpolator` [2422](https://github.com/CliMA/ClimaCore.jl/pull/2422)

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[deps]
2+
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
23
ClimaComms = "3a4d1b5c-c61d-41fd-a00a-5873ba7a1b0d"
34
ClimaCore = "d414da3d-4745-48bb-8d80-42e94e092884"
45
ClimaCoreMakie = "908f55d8-4145-4867-9c14-5dad1a479e4d"

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ withenv("GKSwstype" => "nul") do
6666
doctest = true,
6767
modules = [
6868
ClimaCore,
69+
ClimaCore.Remapping,
6970
ClimaCoreVTK,
7071
ClimaCoreSpectra,
7172
ClimaCorePlots,

docs/src/APIs/remapping_api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ Remapping.pressure_space
1414
Remapping.update!
1515
Remapping.interpolate_pressure
1616
Remapping.interpolate_pressure!
17+
Remapping.AbstractRemappingMethod
18+
Remapping.SpectralElementRemapping
19+
Remapping.BilinearRemapping
1720
```

docs/src/remapping.md

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,12 @@ given `field`. To obtain such coordinates, you can call the
4545
functions. These functions return an `Array` with the coordinates over which
4646
interpolation will occur. These arrays are of type `Geometry.Point`s.
4747

48-
By default, vertical interpolation is switched off and the `field` is evaluated
49-
directly on the levels.
48+
By default, vertical interpolation is off (field evaluated on levels). Horizontal
49+
interpolation: `SpectralElementRemapping()` (default; uses spectral element quadrature weights) or `BilinearRemapping()`:
50+
51+
```julia
52+
interpolated_array = Remapping.interpolate(field; horizontal_method = Remapping.BilinearRemapping())
53+
```
5054

5155
`ClimaCore.Remapping.interpolate` allocates new output arrays. As such, it is
5256
not suitable for performance-critical applications.
@@ -76,6 +80,8 @@ hcoords = [Geometry.LatLongPoint(lat, long) for long in longpts, lat in latpts]
7680
zcoords = [Geometry.ZPoint(z) for z in zpts]
7781

7882
interpolated_array = interpolate(field, hcoords, zcoords)
83+
# Or, to use bilinear remapping without spectral element weighting:
84+
# interpolate(field, hcoords, zcoords; horizontal_method = Remapping.BilinearRemapping())
7985
```
8086
The output is defined on the Cartesian product of `hcoords` with `zcoords`.
8187

@@ -89,6 +95,154 @@ using CairoMakie
8995
heatmap(ClimaCore.Remapping.interpolate(field))
9096
```
9197

98+
### Remapping methods: Bilinear vs SpectralElementRemapping
99+
100+
Two horizontal remapping methods are available:
101+
102+
- **`SpectralElementRemapping()`** (default): Uses spectral element quadrature weights for high-order polynomial interpolation. More accurate for smooth fields but can produce overshoots/undershoots near discontinuities.
103+
- **`BilinearRemapping()`**: Uses bilinear interpolation on the 2×2 GLL cell containing each target point. More conservative (bounds-preserving) but lower-order accuracy.
104+
105+
Both methods can be used with `interpolate_array` or `Remapper`:
106+
107+
```julia
108+
using ClimaCore.Remapping: SpectralElementRemapping, BilinearRemapping
109+
110+
# Use spectral remapping (default)
111+
interpolated = Remapping.interpolate_array(field, xpts, ypts)
112+
113+
# Use bilinear remapping
114+
interpolated = Remapping.interpolate_array(
115+
field, xpts, ypts; horizontal_method = BilinearRemapping()
116+
)
117+
118+
# With Remapper
119+
remapper = Remapper(space; target_hcoords, horizontal_method = BilinearRemapping())
120+
```
121+
122+
#### Slotted-cylinder example (demo of horizontal remapping types)
123+
124+
```@example remap_visualization
125+
using ClimaComms
126+
using ClimaCore:
127+
Geometry, Domains, Meshes, Topologies, Spaces, Fields, Remapping, Quadratures
128+
using CairoMakie
129+
130+
device = ClimaComms.CPUSingleThreaded()
131+
nelements_horz = 6
132+
Nq = 4
133+
n_interp = 24
134+
135+
# Simple test field: disk with slot (discontinuous)
136+
slot_radius = 0.15
137+
slot_cx, slot_cy = 0.5, 0.5
138+
slot_half_width = 0.025
139+
slot_y_hi = slot_cy + slot_radius
140+
141+
horzdomain = Domains.RectangleDomain(
142+
Geometry.XPoint(0.0) .. Geometry.XPoint(1.0),
143+
Geometry.YPoint(0.0) .. Geometry.YPoint(1.0),
144+
x1periodic = true, x2periodic = true,
145+
)
146+
147+
quad = Quadratures.GLL{Nq}()
148+
horzmesh = Meshes.RectilinearMesh(horzdomain, nelements_horz, nelements_horz)
149+
horztopology = Topologies.Topology2D(ClimaComms.SingletonCommsContext(device), horzmesh)
150+
space = Spaces.SpectralElementSpace2D(horztopology, quad)
151+
152+
coords = Fields.coordinate_field(space)
153+
function slotted_cylinder(x, y)
154+
in_disk = (x - slot_cx)^2 + (y - slot_cy)^2 <= slot_radius^2
155+
in_slot = (abs(x - slot_cx) <= slot_half_width) && (y >= slot_cy) && (y <= slot_y_hi)
156+
return (in_disk && !in_slot) ? 1.0 : 0.0
157+
end
158+
field = @. slotted_cylinder(coords.x, coords.y)
159+
Spaces.weighted_dss!(field)
160+
161+
xpts = range(Geometry.XPoint(0.0), Geometry.XPoint(1.0), length = n_interp)
162+
ypts = range(Geometry.YPoint(0.0), Geometry.YPoint(1.0), length = n_interp)
163+
164+
# Compare both methods
165+
interp_bilinear = Remapping.interpolate_array(
166+
field, xpts, ypts; horizontal_method = Remapping.BilinearRemapping(),
167+
)
168+
interp_spectral = Remapping.interpolate_array(
169+
field, xpts, ypts; horizontal_method = Remapping.SpectralElementRemapping(),
170+
)
171+
172+
# Error (bilinear − spectral): highlights where the methods differ
173+
err_bilinear_spectral = interp_bilinear .- interp_spectral
174+
175+
# Raw data at GLL nodes (source field before interpolation)
176+
x_se = Float64[]
177+
y_se = Float64[]
178+
vals_se = Float64[]
179+
Fields.byslab(space) do slabidx
180+
x_data = parent(Fields.slab(coords.x, slabidx))
181+
y_data = parent(Fields.slab(coords.y, slabidx))
182+
f_data = parent(Fields.slab(field, slabidx))
183+
for j in 1:Nq, i in 1:Nq
184+
push!(x_se, x_data[i, j])
185+
push!(y_se, y_data[i, j])
186+
push!(vals_se, f_data[i, j])
187+
end
188+
end
189+
190+
x_plot = [p.x for p in xpts]
191+
y_plot = [p.y for p in ypts]
192+
193+
fig = Figure(size = (1200, 700))
194+
ax1 = Axis(fig[1, 1], title = "Bilinear", xlabel = "x", ylabel = "y")
195+
hm1 = heatmap!(
196+
ax1, x_plot, y_plot, interp_bilinear';
197+
colorrange = (0, 1), colormap = :viridis,
198+
lowclip = :orange, highclip = :red,
199+
)
200+
Colorbar(fig[1, 2], hm1; label = "value")
201+
202+
ax2 = Axis(fig[1, 3], title = "Spectral", xlabel = "x", ylabel = "y")
203+
hm2 = heatmap!(
204+
ax2, x_plot, y_plot, interp_spectral';
205+
colorrange = (0, 1), colormap = :viridis,
206+
lowclip = :orange, highclip = :red,
207+
)
208+
Colorbar(fig[1, 4], hm2; label = "value")
209+
210+
ax3 = Axis(fig[1, 5], title = "Error (bilinear − spectral)", xlabel = "x", ylabel = "y")
211+
erange = extrema(err_bilinear_spectral)
212+
hm3 = heatmap!(
213+
ax3, x_plot, y_plot, err_bilinear_spectral';
214+
colorrange = erange, colormap = :RdBu,
215+
)
216+
Colorbar(fig[1, 6], hm3; label = "error")
217+
218+
# Row 2: raw spectral element grid (exact values at GLL nodes)
219+
# Swap (y_se, x_se) so orientation matches heatmaps (slab i,j vs display x,y convention)
220+
ax_se = Axis(
221+
fig[2, 1],
222+
title = "Raw spectral element grid (GLL nodes)",
223+
xlabel = "x",
224+
ylabel = "y",
225+
)
226+
sc_se = scatter!(
227+
ax_se, y_se, x_se;
228+
color = vals_se,
229+
colorrange = (0, 1),
230+
colormap = :viridis,
231+
lowclip = :orange,
232+
highclip = :red,
233+
markersize = 8,
234+
)
235+
boundary_pos = (0:nelements_horz) ./ nelements_horz
236+
vlines!(ax_se, boundary_pos; color = :pink, linewidth = 2)
237+
hlines!(ax_se, boundary_pos; color = :pink, linewidth = 2)
238+
limits!(ax_se, 0, 1, 0, 1)
239+
Colorbar(fig[2, 2], sc_se; label = "value")
240+
241+
fig
242+
```
243+
244+
Row 1: heatmaps use **orange** for undershoots (&lt; 0) and **red** for overshoots (&gt; 1). The spectral method produces overshoots/undershoots near the discontinuity; bilinear stays in [0, 1]. The error panel (bilinear − spectral) shows where the two methods differ. Row 2: raw field values at the GLL nodes (the source data); pink lines show element boundaries.
245+
92246
### The `Remapper` object
93247

94248
A `Remapping.Remapper` is an object that is tied to a specified `Space` and can

0 commit comments

Comments
 (0)