Skip to content

Commit 6e540bd

Browse files
committed
Add Visualize.plot_bias!
This functionality is already in ClimaCoupler and should belong in ClimaAnalysis. This commit ports over plot_bias! to ClimaAnalysis.
1 parent c416390 commit 6e540bd

File tree

7 files changed

+209
-0
lines changed

7 files changed

+209
-0
lines changed

NEWS.md

+23
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,29 @@ julia> units(se_var)
253253
"K^2"
254254
```
255255

256+
### Plotting bias
257+
258+
Building upon the other features introduced in this release, you can now directly plot bias
259+
and root mean squared error between two variables with the `plot_bias_on_globe!` function.
260+
Typically, this is done to compare simulated data against observations.
261+
262+
In the example below, we plot the bias between our simulation and some observations stored
263+
in `ta_1d_average.nc`.
264+
265+
```julia
266+
import ClimaAnalysis
267+
import ClimaAnalysis.Visualize: plot_bias_on_globe!
268+
import GeoMakie
269+
import CairoMakie
270+
271+
obs_var = ClimaAnalysis.OutputVar("ta_1d_average.nc")
272+
sim_var = ClimaAnalysis.get(ClimaAnalysis.simdir("simulation_output"), "ta")
273+
274+
fig = CairoMakie.Figure()
275+
plot_bias_on_globe!(fig, sim_var, obs_var)
276+
CairoMakie.save("myfigure.pdf", fig)
277+
```
278+
256279
## Bug fixes
257280

258281
- Increased the default value for `warp_string` to 72.

docs/src/api.md

+1
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,5 @@ Visualize.oceanmask
106106
Visualize.landmask
107107
Visualize.contour2D_on_globe!
108108
Visualize.heatmap2D_on_globe!
109+
Visualize.plot_bias_on_globe!
109110
```

docs/src/assets/bias_plot.png

181 KB
Loading

docs/src/visualize.md

+26
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,29 @@ more easily.
4545
The output might look something like:
4646

4747
![oceanmask](./assets/oceanmask.png)
48+
49+
### Plotting bias
50+
51+
After [computing the bias](@ref bias) between observational and simulation data, you may
52+
want to plot the bias and display information such as the root mean squared error (RMSE) and
53+
the global bias in the plot. To do this, you use the function [`plot_bias_on_globe!(fig, sim,
54+
obs)`](@ref Visualize.plot_bias_on_globe!). In the example below, we plot the bias between our
55+
simulation and some observations stored in `ta_1d_average.nc`.
56+
57+
```julia
58+
import ClimaAnalysis
59+
import ClimaAnalysis.Visualize: plot_bias_on_globe!
60+
import GeoMakie
61+
import CairoMakie
62+
63+
obs_var = ClimaAnalysis.OutputVar("ta_1d_average.nc")
64+
sim_var = ClimaAnalysis.get(ClimaAnalysis.simdir("simulation_output"), "ta")
65+
66+
fig = CairoMakie.Figure()
67+
plot_bias_on_globe!(fig, sim_var, obs_var)
68+
CairoMakie.save("myfigure.pdf", fig)
69+
```
70+
71+
The output produces something like:
72+
73+
![biasplot](./assets/bias_plot.png)

ext/ClimaAnalysisGeoMakieExt.jl

+121
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,125 @@ function Visualize.contour2D_on_globe!(
254254
)
255255
end
256256

257+
"""
258+
plot_bias_on_globe!(fig::Makie.Figure,
259+
sim::ClimaAnalysis.OutputVar,
260+
obs::ClimaAnalysis.OutputVar;
261+
cmap_extrema = extrema(ClimaAnalysis.bias(sim, obs).data),
262+
p_loc = (1, 1),
263+
plot_coastline = true,
264+
plot_colorbar = true,
265+
mask = nothing,
266+
more_kwargs)
267+
plot_bias_on_globe!(grid_layout::Makie.GridLayout,
268+
sim::ClimaAnalysis.OutputVar,
269+
obs::ClimaAnalysis.OutputVar;
270+
cmap_extrema = extrema(ClimaAnalysis.bias(sim, obs).data),
271+
p_loc = (1, 1),
272+
plot_coastline = true,
273+
plot_colorbar = true,
274+
mask = nothing,
275+
more_kwargs)
276+
277+
278+
Plot the bias (`sim.data - var.data`) on a projected geoid. The gloal bias and root mean
279+
squared error (RMSE) are computed and can be found in the title of the plot. This function
280+
plots the returned `OutputVar` of `ClimaAnalysis.bias(sim, obs)`. See also
281+
[`ClimaAnalysis.bias`](@ref).
282+
283+
The plot comes with labels, units, and a colorbar. This function uses a constrained colormap
284+
based on the values of `cmap_extrema`.
285+
286+
The dimensions have to be longitude and latitude.
287+
288+
`mask` has to be an object that can be plotted by `Makie.poly`. `ClimaAnalysis` comes with
289+
predefined masks, check out [`Visualize.oceanmask`](@ref) and [`Visualize.landmask`](@ref).
290+
291+
!!! note
292+
Masking does not affect the colorbar. If you have values defined beneath the map, they
293+
can still affect the colorbar.
294+
295+
Additional arguments to the plotting and axis functions
296+
=======================================================
297+
298+
`more_kwargs` can be a dictionary that maps symbols to additional options for:
299+
- the axis (`:axis`)
300+
- the plotting function (`:plot`)
301+
- the colorbar (`:cb`)
302+
- the coastline (`:coast`)
303+
- the mask (`:mask`)
304+
305+
The coastline is plotted from `GeoMakie.coastline` using the `lines!` plotting function.
306+
307+
The values are splatted in the relevant functions. Populate them with a
308+
Dictionary of `Symbol`s => values to pass additional options.
309+
"""
310+
function Visualize.plot_bias_on_globe!(
311+
place::MakiePlace,
312+
sim::ClimaAnalysis.OutputVar,
313+
obs::ClimaAnalysis.OutputVar;
314+
cmap_extrema = extrema(ClimaAnalysis.bias(sim, obs).data),
315+
p_loc = (1, 1),
316+
plot_coastline = true,
317+
plot_colorbar = true,
318+
mask = nothing,
319+
more_kwargs = Dict(
320+
:plot => Dict(),
321+
:cb => Dict(),
322+
:axis => Dict(),
323+
:coast => Dict(:color => :black),
324+
:mask => Dict(),
325+
),
326+
)
327+
bias_var = ClimaAnalysis.bias(sim, obs)
328+
global_bias = round(bias_var.attributes["global_bias"], sigdigits = 3)
329+
rmse = round(ClimaAnalysis.global_rmse(sim, obs), sigdigits = 3)
330+
units = ClimaAnalysis.units(bias_var)
331+
332+
bias_var.attributes["long_name"] *= " (RMSE: $rmse $units, Global bias: $global_bias $units)"
333+
min_level, max_level = cmap_extrema
334+
335+
# Make sure that 0 is at the center
336+
cmap = Visualize._constrained_cmap(
337+
Makie.cgrad(:vik).colors,
338+
min_level,
339+
max_level;
340+
categorical = true,
341+
)
342+
nlevels = 11
343+
# Offset so that it covers 0
344+
levels = collect(range(min_level, max_level, length = nlevels))
345+
offset = levels[argmin(abs.(levels))]
346+
levels = levels .- offset
347+
ticklabels = map(x -> string(round(x; digits = 0)), levels)
348+
ticks = (levels, ticklabels)
349+
350+
default_kwargs = Dict(
351+
:plot => Dict(
352+
:colormap => cmap,
353+
:levels => levels,
354+
:extendhigh => :auto,
355+
:extendlow => :auto,
356+
),
357+
:cb => Dict(:ticks => ticks),
358+
)
359+
360+
# Function for recursively merging two dictionaries if the values of the dictionaries
361+
# are dictionaries and the values of those are also dictionaries and so on
362+
# See: https://discourse.julialang.org/t/multi-layer-dict-merge/27261/6
363+
recursive_merge(x::AbstractDict...) = merge(recursive_merge, x...)
364+
recursive_merge(x...) = x[end]
365+
default_and_more_kwargs = recursive_merge(default_kwargs, more_kwargs)
366+
367+
return Visualize.contour2D_on_globe!(
368+
place,
369+
bias_var;
370+
p_loc = p_loc,
371+
plot_coastline = plot_coastline,
372+
plot_colorbar = plot_colorbar,
373+
mask = mask,
374+
more_kwargs = default_and_more_kwargs,
375+
)
376+
end
377+
257378
end

src/Visualize.jl

+2
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ function contour2D_on_globe! end
2828

2929
function heatmap2D_on_globe! end
3030

31+
function plot_bias_on_globe! end
32+
3133
end

test/test_GeoMakieExt.jl

+36
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,40 @@ using OrderedCollections
100100

101101
output_name = joinpath(tmp_dir, "test_contours2D_globe_with_oceanmask.png")
102102
Makie.save(output_name, fig5)
103+
104+
# Test plot_bias
105+
fig6 = Makie.Figure()
106+
107+
lon = collect(range(-179.5, 179.5, 360))
108+
lat = collect(range(-89.5, 89.5, 180))
109+
data = collect(reshape(-32400:32399, (360, 180))) ./ (32399.0 / 5.0)
110+
dims = OrderedDict(["lon" => lon, "lat" => lat])
111+
attribs =
112+
Dict("long_name" => "idk", "short_name" => "short", "units" => "kg")
113+
dim_attribs = OrderedDict([
114+
"lon" => Dict("units" => "deg"),
115+
"lat" => Dict("units" => "deg"),
116+
])
117+
var = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
118+
119+
data_zero = zeros(length(lon), length(lat))
120+
var_zero = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data_zero)
121+
122+
ClimaAnalysis.Visualize.plot_bias_on_globe!(fig6, var, var_zero)
123+
output_name = joinpath(tmp_dir, "plot_bias.png")
124+
Makie.save(output_name, fig6)
125+
126+
# Test plot bias with keyword arguments
127+
fig7 = Makie.Figure()
128+
ClimaAnalysis.Visualize.plot_bias_on_globe!(
129+
fig7,
130+
var,
131+
var_zero,
132+
more_kwargs = Dict(
133+
:axis => Dict(:title => "no title"),
134+
:plot => Dict(:extendhigh => nothing),
135+
),
136+
)
137+
output_name = joinpath(tmp_dir, "plot_bias_kwargs.png")
138+
Makie.save(output_name, fig7)
103139
end

0 commit comments

Comments
 (0)