Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSD applied to Dansgaard-Oeschger events #48

Merged
merged 13 commits into from
Oct 14, 2023
8 changes: 7 additions & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
[deps]
BSplineKit = "093aae92-e908-43d7-9660-e50ee39d5a0a"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2"
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244"
DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8"
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4"
Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306"
Loess = "4345ca2d-374a-55d4-8d30-97f9976e7612"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"

[compat]
Documenter = "0.27"
Documenter = "0.27"
17 changes: 12 additions & 5 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
cd(@__DIR__)

using TransitionsInTimeseries, Statistics, StatsBase

using Literate

# process examples and add then in a sidebar column
example_files = readdir(joinpath(@__DIR__, "src", "examples"))
jl_indices = [f[end-2:end] == ".jl" for f in example_files]
jl_examples = example_files[jl_indices]

example_pages = String[]
for file in example_files
for file in jl_examples
mkdownname = splitext(file)[1]*".md"
Literate.markdown("src/examples/$(file)", "src/examples"; credit = false)
push!(example_pages, "examples/$(mkdownname)")
end

# Sort pages with increasing complexity rather than alphabetically
permute!(example_pages, [3, 2, 1])

pages = [
"index.md",
"tutorial.md",
"api.md",
"Examples" => example_pages,
]
Expand All @@ -27,6 +31,9 @@ Downloads.download(
)
include("build_docs_with_style.jl")

using DocumenterCitations
bib = CitationBibliography(joinpath(@__DIR__, "src", "refs.bib"); style=:authoryear)

build_docs_with_style(pages, TransitionsInTimeseries, Statistics, StatsBase;
authors = "Jan Swierczek-Jereczek <[email protected]>, George Datseris <[email protected]>"
)
authors = "Jan Swierczek-Jereczek <[email protected]>, "*
"George Datseris <[email protected]>", bib)
254 changes: 254 additions & 0 deletions docs/src/examples/do-events.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
#=
# Dansgaard-Oescher events
Datseris marked this conversation as resolved.
Show resolved Hide resolved

The $\delta^{18}O$ timeseries of the North Greenland Ice Core Project ([NGRIP](https://en.wikipedia.org/wiki/North_Greenland_Ice_Core_Project)) are, to this date, the best proxy record for the Dansgaard-Oeschger events ([DO-events](https://en.wikipedia.org/wiki/Dansgaard%E2%80%93Oeschger_event)). DO-events are sudden warming episodes of the North Atlantic, reaching to 10 degrees of regional warming within 100 years. They happened quasi-periodically over the last glacial cycle due to transitions between strong and weak states of the Atlantic Meridional Overturning Circulation and might be therefore be the most prominent examples of abrupt transitions in the field of climate science. We here propose to hindcast these events by applying the theory of Critical Slowing Down (CSD) on the NGRIP data, which can be found [here](https://www.iceandclimate.nbi.ku.dk/data/) in its raw format. This analysis has already been done in [boers-early-warning-2018](@cite) and we here try to reproduce Figure 2.d-f.

## Preprocessing NGRIP

Over this example, it will appear that the convenience of TransitionsInTimeseries to leads the bulk of the code to be written for plotting and preprocessing. The latter consists in various steps $i = \lbrace 1,2,3 \rbrace$:
1. Load the data, reverse and offset it to have time vector = time before 2000 AD.
2. Filter non-unique points in time and sort the data.
3. Regrid the data from uneven to even sampling.

The time and $\delta^{18}O$ vectors resulting from the $i$-th preprocessing step are respectively called $t_i$ and $x_i$. The final step consists in obtaining a residual $r$, i.e. the fluctuations of the system around the attractor, which, within the CSD theory, is assumed to be tracked.
=#

using DelimitedFiles, Downloads, DSP, BSplineKit, Loess

function load_ngrip()
tmp = Base.download("https://raw.githubusercontent.com/JuliaDynamics/JuliaDynamics/"*
"master/timeseries/NGRIP.csv")
data, labels = readdlm(tmp, header = true)
return reverse(data[:, 1]) .- 2000, reverse(data[:, 2]) # (time, delta-18-0) vectors
end

uniqueidx(v) = unique(i -> v[i], eachindex(v))
function keep_unique(t, x)
unique_idx = uniqueidx(t)
return t[unique_idx], x[unique_idx]
end

function sort_timeseries!(t, x)
p = sortperm(t)
permute!(t, p)
permute!(x, p)
return nothing
end

function regrid2evensampling(t, x, dt)
itp = BSplineKit.interpolate(t, x, BSplineOrder(4))
tspan = (ceil(minimum(t)), floor(maximum(t)))
t_even = collect(tspan[1]:dt:tspan[2])
x_even = itp.(t_even)
return t_even, x_even
end

function chebyshev_filter(t, x, fcutoff)
ii = 10 # Chebyshev filtering requires to prune first points of timeseries.
responsetype = Highpass(fcutoff, fs = 1/dt)
designmethod = Chebyshev1(8, 0.05)
r = filt(digitalfilter(responsetype, designmethod), x)
xtrend = x - r
return t[ii:end], x[ii:end], xtrend[ii:end], r[ii:end]
end

dt, fcutoff = 5.0, 0.95*0.01 # dt = 5 yr and cutoff ≃ 0.01 yr^-1 as in (Boers 2018)
t1, x1 = load_ngrip()
t2, x2 = keep_unique(t1, x1)
sort_timeseries!(t2, x2)
t3, x3 = regrid2evensampling(t2, x2, dt)
t, x, xtrend, r = chebyshev_filter(t3, x3, fcutoff)

#=
Let's now go to the last preprocessing step and visualize our data in what will become our main figure. For the segmentation of the DO-events, we rely on the tabulated data from [rasmussen-stratigraphic-2014](@cite) (which will soon be available as downloadable):
=#

using CairoMakie

function loess_filter(t, x; span = 0.005)
loessmodel = loess(t, x, span = span)
xtrend = Loess.predict(loessmodel, t)
r = x - xtrend
return t, x, xtrend, r
end

function kyr_xticks(tticks_yr)
tticks_kyr = ["$t" for t in Int.(tticks_yr ./ 1e3)]
return (tticks_yr, tticks_kyr)
end

function plot_do(traw, xraw, tfilt, xfilt, t, r, t_transitions, xlims, xticks)
fig = Figure(resolution = (1600, 1200), fontsize = 24)

## Original timeseries with transition marked by vertical lines
ax1 = Axis(fig[1, 1], xlabel = L"Time (kyr) $\,$", ylabel = L"$\delta^{18}$O (permil)",
xaxisposition = :top, xticks = xticks)
lines!(ax1, traw, xraw, color = (:gray70, 0.5))
lines!(ax1, tfilt, xfilt, color = :gray10, linewidth = 3)
vlines!(ax1, t_transitions, color = Cycled(1), linewidth = 3)

## Residual timeseries
ax2 = Axis(fig[2, 1], ylabel = L"Residual $\,$", xticks = xticks,
xticksvisible = false, xticklabelsvisible = false)
lines!(ax2, t, r, color = :gray50, linewidth = 1)

## Axes for variance and AC1 timeseries
ax3 = Axis(fig[3, 1], ylabel = L"Variance $\,$", xticks = xticks,
xticksvisible = false, xticklabelsvisible = false)
ax4 = Axis(fig[4, 1], xlabel = L"Time (kyr) $\,$", ylabel = L"Lag-1 autocor. $\,$",
xticks = xticks)

axs = [ax1, ax2, ax3, ax4]
[xlims!(ax, xlims) for ax in axs]
ylims!(axs[1], (-48, -34))
rowgap!(fig.layout, 10)
return fig, axs
end

xlims = (-60e3, -10e3)
xticks = kyr_xticks(-60e3:5e3:5e3)
t_rasmussen = -[59440, 58280, 55800, 54220, 49280, 46860, 43340, 41460, 40160, 38220,
35480, 33740, 32500, 28900, 27780, 23340, 14692, 11703]
tloess, _, xloess, rloess = loess_filter(t3, x3) # loess-filtered signal for visualization
fig, axs = plot_do(t3, x3, tloess, xloess, t, r, t_rasmussen, xlims, xticks)
fig

#=
## Hindcast on NGRIP data

As one can see... there is not much to see so far. Residuals are impossible to simply eye-ball and we therefore use TransitionsInTimeseries to study the evolution, measured by the ridge-regression slope, of the residual's variance and lag-1 autocorrelation (AC1) over time. In many examples of the literature, including [boers-early-warning-2018](@cite), the CSD analysis is performed over segments (sometimes only one) of the timeseries, such that a significance value is obtained for each segment. Dealing with segments can be easily done in TransitionsInTimeseries and is demonstrated here:
=#

using TransitionsInTimeseries, StatsBase
using Random: Xoshiro

ac1(x) = sum(autocor(x, [1])) # AC1 from StatsBase
indicators = (var, ac1)
indconfig = WindowedIndicatorConfig(indicators, [];
whichtime = last, width_ind = Int(200÷dt)) # window size = 200 years
signif = SurrogatesSignificance(n = 1_000, tail = :right, rng = Xoshiro(1995))
tmargins = [200, 200] # exclude transitions from the segments
pvalues, indicator_results = segmented_significance(t, r, t_rasmussen, tmargins,
indconfig, RidgeRegressionSlope(), signif)

function plot_segment_analysis!(axs, pvalues, t_transitions, indicator_results, margins, opts)
for i in eachindex(indicator_results) # loop over the segments
## Unpack indicator results
tind = indicator_results[i][:, 1]
ind = indicator_results[i][:, 2:end]

for j in axes(pvalues, 2) # loop over the indicators
if !isinf(pvalues[i, j]) # only plot if enough data points for analysis
## Plot indicator timeseries and its linear regression
lines!(axs[j+2], tind, ind[:, j], color = Cycled(1))
m, p = ridgematrix(tind, 0.0) * ind[:, j]
if pvalues[i, j] < 0.05
vlines!(axs[1], t_transitions[i] - margins[2]; opts[j]...)
vlines!(axs[j+2], t_transitions[i] - margins[2]; opts[j]...)
lines!(axs[j+2], tind, m .* tind .+ p, color = :gray10, linewidth = 3)
else
lines!(axs[j+2], tind, m .* tind .+ p, color = :gray10, linewidth = 3,
linestyle = :dash)
end
end
end
end
return nothing
end
opts = [(color = Cycled(2), linewidth = 3), # plotting options
(color = :orange, linewidth = 3, linestyle = :dash)]
plot_segment_analysis!(axs, pvalues, t_rasmussen, indicator_results, tmargins, opts)
fig

#=
In [boers-early-warning-2018](@cite), 13/16 and 7/16 true positives are respectively found for the variance and AC1, with 16 referring to the total number of transitions. The timeseries actually includes 18 transition but some segments are too small to be analysed. In contrast to [boers-early-warning-2018](@cite), we here respectively find 10/16 true positives for the variance and 4/16 for AC1. This mismatch points out that packages like TransitionsInTimeseries are wishful for research to be reproducible, especially since CSD is gaining attention - not only within the scientific community but also in popular media.

## Limitations of hindcast

Finding transition indicators by defining segments, as done above, is arguably misleading when it comes to an operational prediction task. In fact, one introduces a somewhat biased view by only running the analysis until time steps right before the transition. We here propose to perform the same analysis but only until few hundred years before the transition:
=#

early_tmargins = [200, 700] # take early end margin
fig, axs = plot_do(t3, x3, tloess, xloess, t, r, t_rasmussen, xlims, xticks)
pvalues, indicator_results = segmented_significance(t, r, t_rasmussen, early_tmargins,
indconfig, RidgeRegressionSlope(), signif)
plot_segment_analysis!(axs, pvalues, t_rasmussen, indicator_results, early_tmargins, opts)
fig

#=
For the variance and AC1, we here respectively find 7 and 3 positives, although the transitions are still far ahead. This shows that what CSD actually captures is the potential widening induced by a shift of the forcing parameter. For some systems, this happens before a transition but can also happen with no transition ahead in the "near" future. We therefore believe, as already suggested in some studies, that "resilience-loss indicators" is a more accurate name than "early-warning signals" when using CSD.

We draw attention upon the fact that the $\delta^{18}O$ timeseries is noisy and sparsely re-sampled. Furthermore, interpolating over time introduces a potential bias in the statistics, even if performed on a coarse grid. These sources of error come along the usual problem of arbitrarily choosing (1) a filtering method, (2) windowing parameters and (3) appropriate metrics. The NGRIP data therefore represents an example that should be handled with care - as many others where CSD analysis has been applied on transitions in the field of geoscience.

!!! info "Future improvement"
Supporting the computations for uneven timeseries is a planned improvement of TransitionsInTimeseries. This will avoid the need of regridding data on coarse grids and will prevent from introducing any bias.

## Hindcasting simulated DO-events

In CLIMBER-X[willeit-earth-2022](@cite), an Earth Model of Intermediate Complexity (EMIC), DO-like events can be triggered by forcing the North Atlantic with a (white noise) freshwater input. Simulated DO-like events present the big advantage of being evenly sampled in time and free of measurement noise. Unlike the segment analysis as the one performed above, the analysis fully relying on sliding windows (as introduced in the [Tutorial](@ref)) is a way to generate true positives while simultaneously checking for false positives, since transition metrics are computed for (almost) all time steps. We run this analysis over two exemplary simulation outputs:
=#

opts = [(color = (:orange, 0.1), linewidth = 3),
(color = Cycled(2), linewidth = 3, linestyle = :dash)]

function perform_sliding_analysis(t, r, indicators, change_metrics, itime, ctime,
istride, cstride, dt, n)
indconfig = WindowedIndicatorConfig(indicators, change_metrics; whichtime = last,
width_ind = Int(itime ÷ dt), stride_ind = istride,
width_cha = Int(ctime ÷ dt), stride_cha = cstride)
results = estimate_indicator_changes(indconfig, r, t)
signif = SurrogatesSignificance(n = n, tail = :right, rng = Xoshiro(1995))
_ = significant_transitions(results, signif)
return signif.pvalues, results
end

function plot_sliding_analysis!(axs, pvalues, results, threshold)
for j in axes(pvalues, 2)
vlines!(axs[1], results.t_change[pvalues[:, j] .< threshold]; opts[j]...)
lines!(axs[2+j], results.t_indicator, results.x_indicator[:, j], color = Cycled(1))
vlines!(axs[2+j], results.t_change[pvalues[:, j] .< threshold]; opts[j]...)
end
return nothing
end

t_transitions = [[1850, 2970, 3970, 5070, 5810, 7050, 8050], [3500, 4400, 5790, 7200, 8140]]
figvec = Figure[]
for i in 1:2
## Download the data and perform loess filtering on it
tmp = Base.download("https://raw.githubusercontent.com/JuliaDynamics/JuliaDynamics/" *
"master/timeseries/climberx-do$(i)-omaxa.csv")
data = readdlm(tmp)
tcx, xcx = data[1, :], data[2, :]
t, x, xtrend, r = loess_filter(tcx, xcx, span = 0.02)

## Initialize figure
xlims = (0, last(tcx))
xticks = kyr_xticks(xlims[1]:1e3:xlims[2])
fig, axs = plot_do(tcx, xcx, t, xtrend, t, r, t_transitions[i], xlims, xticks)
ylims!(axs[1], (5, 40))
axs[1].ylabel = L"Max. Atlantic overturning (Sv) $\,$"

## Run sliding analysis and update figure with results
dt = mean(diff(tcx))
pvalues, results = perform_sliding_analysis(t, r, indicators, RidgeRegressionSlope(),
200.0, 50.0, 1, 1, dt, 1_000)
plot_sliding_analysis!(axs, pvalues, results, 0.01)
vlines!(axs[1], t_transitions[i], color = Cycled(1), linewidth = 3)
push!(figvec, fig)
end
figvec[1]

#=
We here notice that transitions are always preceeded (early enough but not too early) by a significant increase of AC1, sometimes accompanied by the variance. Diagnostics seem to be very reliable, since all transitions lead to both indicators showing significant increases, synchronously and/or subsequently to the transition. To make sure we were not simply lucky, let's look at another simulation:
=#

figvec[2]

#=
Things look equally good here! Assuming that these simulations capture the DO dynamics well, one can hope that higher resolution data with less noise allows to robustly predict and/or diagnose DO events. We here draw attention upon the fact that even $dt = 1 \, \mathrm{yr}$ is a relatively sparse sampling for a physical, simulated process displaying transitions with a quasi-period of $T \in [1000, 1500] \, \mathrm{yr}$.

# References
Datseris marked this conversation as resolved.
Show resolved Hide resolved

```@bibliography
```
=#
Loading
Loading