Open In Colab   Open in Kaggle

Tutorial 9: Paleoclimate Reanalysis Products#

Week 1, Day 4, Paleoclimate

Content creators: Sloane Garelick

Content reviewers: Yosmely Bermúdez, Dionessa Biton, Katrina Dobson, Maria Gonzalez, Will Gregory, Nahid Hasan, Paul Heubel, Sherry Mi, Beatriz Cosenza Muralles, Brodie Pearson, Jenna Pearson, Agustina Pesce, Chi Zhang, Ohad Zivan

Content editors: Yosmely Bermúdez, Zahra Khodakaramimaghsoud, Paul Heubel, Jenna Pearson, Agustina Pesce, Chi Zhang, Ohad Zivan

Production editors: Wesley Banfield, Paul Heubel, Jenna Pearson, Konstantine Tsafatinos, Chi Zhang, Ohad Zivan

Our 2024 Sponsors: CMIP, NFDI4Earth

Tutorial Objectives#

Estimated timing of tutorial: 20 minutes

As we discussed in the video, proxies and models both have advantages and limitations for reconstructing past changes in Earth’s climate system. One approach for combining the strengths of both paleoclimate proxies and models is data assimilation. This is the same approach used in Day 2, except instead of simulations of Earth’s recent past, we are using a simulation that spans many thousands of years back in time and is constrained by proxies, rather than modern instrumental measurements. The results of this process are called reanalysis products.

In this tutorial, we’ll look at paleoclimate reconstructions from the Last Glacial Maximum Reanalysis (LGMR) product from Osman et al. (2021), which contains temperature for the past 24,000 years.

During this tutorial, you will:

  • Plot a time series of the paleoclimate reconstruction

  • Create global maps and zonal mean plots of temperature anomalies

  • Assess how and why LGM to present temperature anomalies vary with latitude

Setup#

# installations ( uncomment and run this cell ONLY when using google colab or kaggle )

# !pip install cartopy
# imports
import pooch
import os
import tempfile
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt

import cartopy.crs as ccrs

Install and import feedback gadget#

Hide code cell source
# @title Install and import feedback gadget

!pip3 install vibecheck datatops --quiet

from vibecheck import DatatopsContentReviewContainer
def content_review(notebook_section: str):
    return DatatopsContentReviewContainer(
        "",  # No text prompt
        notebook_section,
        {
            "url": "https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab",
            "name": "comptools_4clim",
            "user_key": "l5jpxuee",
        },
    ).render()


feedback_prefix = "W1D4_T9"

Figure Settings#

Hide code cell source
# @title Figure Settings
import ipywidgets as widgets  # interactive display

%config InlineBackend.figure_format = 'retina'
plt.style.use(
    "https://raw.githubusercontent.com/neuromatch/climate-course-content/main/cma.mplstyle"
)

Helper functions#

Hide code cell source
# @title Helper functions

def pooch_load(filelocation=None, filename=None, processor=None):
    shared_location = "/home/jovyan/shared/Data/tutorials/W1D4_Paleoclimate"  # this is different for each day
    user_temp_cache = tempfile.gettempdir()

    if os.path.exists(os.path.join(shared_location, filename)):
        file = os.path.join(shared_location, filename)
    else:
        file = pooch.retrieve(
            filelocation,
            known_hash=None,
            fname=os.path.join(user_temp_cache, filename),
            processor=processor,
        )

    return file

Video 1: Paleoclimate Data Assimilation#

Submit your feedback#

Hide code cell source
# @title Submit your feedback
content_review(f"{feedback_prefix}_Paleoclimate_Data_Assimilation_Video")
If you want to download the slides: https://osf.io/download/f2ynq/

Submit your feedback#

Hide code cell source
# @title Submit your feedback
content_review(f"{feedback_prefix}_Paleoclimate_Data_Assimilation_Slides")

Section 1: Load the LGMR Paleoclimate Reconstruction#

This dataset contains a reconstruction of surface air temperature (SAT) from the product Last Glacial Maximum Reanalysis (LGMR). Note that this data starts from 100 years before present and goes back in time to ~24,000 BP. The period of time from ~21,000 to 18,000 years ago is referred to as the Last Glacial Maximum (LGM). The LGM was the most recent glacial period in Earth’s history. During this time, northern hemisphere ice sheets were larger, global sea level was lower, atmospheric CO2 was lower, and global mean temperature was cooler. (Note: if you are interested in looking at data for the present to last millenium, that reanalyses product is available here.)

We will calculate the global mean temperature from the LGM to 100 years before present from a paleoclimate data assimilation to asses how Earth’s climate varied over the past 24,000 years.

First let’s download the paleoclimate data assimilation reconstruction for surface air temperature (SAT).

filename_LGMR_SAT_climo = "LGMR_SAT_climo.nc"
url_LGMR_SAT_climo = "https://www.ncei.noaa.gov/pub/data/paleo/reconstructions/osman2021/LGMR_SAT_climo.nc"

ds = xr.open_dataset(
    pooch_load(filelocation=url_LGMR_SAT_climo, filename=filename_LGMR_SAT_climo)
)
ds
---------------------------------------------------------------------------
timeout                                   Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/urllib3/connectionpool.py:468, in HTTPConnectionPool._make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    464         except BaseException as e:
    465             # Remove the TypeError from the exception chain in
    466             # Python 3 (including for exceptions like SystemExit).
    467             # Otherwise it looks like a bug in the code.
--> 468             six.raise_from(e, None)
    469 except (SocketTimeout, BaseSSLError, SocketError) as e:

File <string>:3, in raise_from(value, from_value)

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/urllib3/connectionpool.py:463, in HTTPConnectionPool._make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    462 try:
--> 463     httplib_response = conn.getresponse()
    464 except BaseException as e:
    465     # Remove the TypeError from the exception chain in
    466     # Python 3 (including for exceptions like SystemExit).
    467     # Otherwise it looks like a bug in the code.

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/http/client.py:1377, in HTTPConnection.getresponse(self)
   1376 try:
-> 1377     response.begin()
   1378 except ConnectionError:

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/http/client.py:320, in HTTPResponse.begin(self)
    319 while True:
--> 320     version, status, reason = self._read_status()
    321     if status != CONTINUE:

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/http/client.py:281, in HTTPResponse._read_status(self)
    280 def _read_status(self):
--> 281     line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
    282     if len(line) > _MAXLINE:

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/socket.py:716, in SocketIO.readinto(self, b)
    715 try:
--> 716     return self._sock.recv_into(b)
    717 except timeout:

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/ssl.py:1275, in SSLSocket.recv_into(self, buffer, nbytes, flags)
   1272         raise ValueError(
   1273           "non-zero flags not allowed in calls to recv_into() on %s" %
   1274           self.__class__)
-> 1275     return self.read(nbytes, buffer)
   1276 else:

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/ssl.py:1133, in SSLSocket.read(self, len, buffer)
   1132 if buffer is not None:
-> 1133     return self._sslobj.read(len, buffer)
   1134 else:

timeout: The read operation timed out

During handling of the above exception, another exception occurred:

ReadTimeoutError                          Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/requests/adapters.py:667, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    666 try:
--> 667     resp = conn.urlopen(
    668         method=request.method,
    669         url=url,
    670         body=request.body,
    671         headers=request.headers,
    672         redirect=False,
    673         assert_same_host=False,
    674         preload_content=False,
    675         decode_content=False,
    676         retries=self.max_retries,
    677         timeout=timeout,
    678         chunked=chunked,
    679     )
    681 except (ProtocolError, OSError) as err:

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/urllib3/connectionpool.py:802, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    800     e = ProtocolError("Connection aborted.", e)
--> 802 retries = retries.increment(
    803     method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]
    804 )
    805 retries.sleep()

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/urllib3/util/retry.py:552, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
    551 if read is False or not self._is_method_retryable(method):
--> 552     raise six.reraise(type(error), error, _stacktrace)
    553 elif read is not None:

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/urllib3/packages/six.py:770, in reraise(tp, value, tb)
    769         raise value.with_traceback(tb)
--> 770     raise value
    771 finally:

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/urllib3/connectionpool.py:716, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    715 # Make the request on the httplib connection object.
--> 716 httplib_response = self._make_request(
    717     conn,
    718     method,
    719     url,
    720     timeout=timeout_obj,
    721     body=body,
    722     headers=headers,
    723     chunked=chunked,
    724 )
    726 # If we're going to release the connection in ``finally:``, then
    727 # the response doesn't need to know about the connection. Otherwise
    728 # it will also try to release it and we'll have a double-release
    729 # mess.

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/urllib3/connectionpool.py:470, in HTTPConnectionPool._make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    469 except (SocketTimeout, BaseSSLError, SocketError) as e:
--> 470     self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
    471     raise

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/urllib3/connectionpool.py:358, in HTTPConnectionPool._raise_timeout(self, err, url, timeout_value)
    357 if isinstance(err, SocketTimeout):
--> 358     raise ReadTimeoutError(
    359         self, url, "Read timed out. (read timeout=%s)" % timeout_value
    360     )
    362 # See the above comment about EAGAIN in Python 3. In Python 2 we have
    363 # to specifically catch it and throw the timeout error

ReadTimeoutError: HTTPSConnectionPool(host='www.ncei.noaa.gov', port=443): Read timed out. (read timeout=30)

During handling of the above exception, another exception occurred:

ReadTimeout                               Traceback (most recent call last)
Cell In[10], line 5
      1 filename_LGMR_SAT_climo = "LGMR_SAT_climo.nc"
      2 url_LGMR_SAT_climo = "https://www.ncei.noaa.gov/pub/data/paleo/reconstructions/osman2021/LGMR_SAT_climo.nc"
      4 ds = xr.open_dataset(
----> 5     pooch_load(filelocation=url_LGMR_SAT_climo, filename=filename_LGMR_SAT_climo)
      6 )
      7 ds

Cell In[5], line 10, in pooch_load(filelocation, filename, processor)
      8     file = os.path.join(shared_location, filename)
      9 else:
---> 10     file = pooch.retrieve(
     11         filelocation,
     12         known_hash=None,
     13         fname=os.path.join(user_temp_cache, filename),
     14         processor=processor,
     15     )
     17 return file

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/pooch/core.py:239, in retrieve(url, known_hash, fname, path, processor, downloader, progressbar)
    236 if downloader is None:
    237     downloader = choose_downloader(url, progressbar=progressbar)
--> 239 stream_download(url, full_path, known_hash, downloader, pooch=None)
    241 if known_hash is None:
    242     get_logger().info(
    243         "SHA256 hash of downloaded file: %s\n"
    244         "Use this value as the 'known_hash' argument of 'pooch.retrieve'"
   (...)
    247         file_hash(str(full_path)),
    248     )

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/pooch/core.py:807, in stream_download(url, fname, known_hash, downloader, pooch, retry_if_failed)
    803 try:
    804     # Stream the file to a temporary so that we can safely check its
    805     # hash before overwriting the original.
    806     with temporary_file(path=str(fname.parent)) as tmp:
--> 807         downloader(url, tmp, pooch)
    808         hash_matches(tmp, known_hash, strict=True, source=str(fname.name))
    809         shutil.move(tmp, str(fname))

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/pooch/downloaders.py:220, in HTTPDownloader.__call__(self, url, output_file, pooch, check_only)
    218     # pylint: enable=consider-using-with
    219 try:
--> 220     response = requests.get(url, timeout=timeout, **kwargs)
    221     response.raise_for_status()
    222     content = response.iter_content(chunk_size=self.chunk_size)

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/requests/api.py:73, in get(url, params, **kwargs)
     62 def get(url, params=None, **kwargs):
     63     r"""Sends a GET request.
     64 
     65     :param url: URL for the new :class:`Request` object.
   (...)
     70     :rtype: requests.Response
     71     """
---> 73     return request("get", url, params=params, **kwargs)

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/requests/api.py:59, in request(method, url, **kwargs)
     55 # By using the 'with' statement we are sure the session is closed, thus we
     56 # avoid leaving sockets open which can trigger a ResourceWarning in some
     57 # cases, and look like a memory leak in others.
     58 with sessions.Session() as session:
---> 59     return session.request(method=method, url=url, **kwargs)

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/requests/sessions.py:589, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    584 send_kwargs = {
    585     "timeout": timeout,
    586     "allow_redirects": allow_redirects,
    587 }
    588 send_kwargs.update(settings)
--> 589 resp = self.send(prep, **send_kwargs)
    591 return resp

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/requests/sessions.py:703, in Session.send(self, request, **kwargs)
    700 start = preferred_clock()
    702 # Send the request
--> 703 r = adapter.send(request, **kwargs)
    705 # Total elapsed time of the request (approximately)
    706 elapsed = preferred_clock() - start

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/requests/adapters.py:713, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    711     raise SSLError(e, request=request)
    712 elif isinstance(e, ReadTimeoutError):
--> 713     raise ReadTimeout(e, request=request)
    714 elif isinstance(e, _InvalidHeader):
    715     raise InvalidHeader(e, request=request)

ReadTimeout: HTTPSConnectionPool(host='www.ncei.noaa.gov', port=443): Read timed out. (read timeout=30)

Section 1.1: Plotting the Temperature Time Series#

Now that the data is loaded, we can plot a time series of the temperature data to assess global changes. However, the dimensions of the sat_mean variable are age-lat-lon, so we first need to weight the data and calculate a global mean.

# assign weights
weights = np.cos(np.deg2rad(ds.lat))

# calculate the global mean surface temperature
sat_global_mean = ds.sat.weighted(weights).mean(dim=["lat", "lon"])
sat_global_mean

Now that we calculated our global mean, we can plot the results as a time series to assess global changes in temperature over the past 24,000 years:

# plot the global mean surface temperature since the LGM
f, ax1 = plt.subplots(1, 1)
ax1.plot(ds["age"], sat_global_mean, linewidth=3)

ax1.set_xlim(ds["age"].max().values, ds["age"].min().values)
ax1.set_ylabel("Global Mean SAT for LGM ($^\circ$C)", fontsize=16)
ax1.set_xlabel("Age (yr BP)", fontsize=16)

Questions 1.1: Climate Connection#

  1. How has global temperature varied over the past 24,000 years?

  2. What climate forcings may have contributed to the increase in temperature ~17,000 years ago?

# to_remove explanation

"""
1.In the span of the last 24,000 years, there has been a considerable shift in global temperatures, transitioning from the colder phase of the Last Glacial Maximum to the relatively warmer Holocene epoch we are currently experiencing. This transformation was not linear, with periods of rapid warming interspersed with intervals of relative stability or even temporary cooling.
2.The increase in temperature around 17,000 years ago was likely driven by a combination of climate forcings: obliquity and eccentricity, both affect the amount of solar radiation reaching the Earth's surface.

"""
"\n1.In the span of the last 24,000 years, there has been a considerable shift in global temperatures, transitioning from the colder phase of the Last Glacial Maximum to the relatively warmer Holocene epoch we are currently experiencing. This transformation was not linear, with periods of rapid warming interspersed with intervals of relative stability or even temporary cooling.\n2.The increase in temperature around 17,000 years ago was likely driven by a combination of climate forcings: obliquity and eccentricity, both affect the amount of solar radiation reaching the Earth's surface.\n\n"

Submit your feedback#

Hide code cell source
# @title Submit your feedback
content_review(f"{feedback_prefix}_Questions_1_1")

Section 1.2: Plotting a Temperature Anomaly Map#

The reanalysis contains spatial reconstructions, so we can also make figures showing spatial temperature anomalies for different time periods (i.e., the change in temperature between two specified times). The anomaly that we’ll interpret is the difference between global temperature from 18,000 to 21,000 years ago (i.e. “LGM”) and 100 to 1,000 years ago (i.e. “modern”) . First, we’ll calculate the average temperatures for each time period.

# calculate the LGM (18,000-21,000 year) mean temperature
lgm = ds.sat.sel(age=slice("18000", "21000"), lon=slice(0, 357.5), lat=slice(-90, 90))
lgm_mean = lgm.mean(dim="age")

# calculate the "modern" (100-1000 year) mean temperature
modern = ds.sat.sel(age=slice("100", "1000"), lon=slice(0, 357.5), lat=slice(-90, 90))
modern_mean = modern.mean(dim="age")

Now we can calculate the anomaly and create a map to visualize the change in temperature from the LGM to present in different parts on Earth.

sat_change = modern_mean - lgm_mean
# make a map of changes
fig, ax = plt.subplots(subplot_kw={"projection": ccrs.Robinson()})
ax.set_global()
sat_change.plot(
    ax=ax,
    transform=ccrs.PlateCarree(),
    x="lon",
    y="lat",
    cmap="Reds",
    vmax=30,
    cbar_kwargs={"orientation": "horizontal", "label": "$\Delta$SAT ($^\circ$C)"},
)
ax.coastlines()
ax.set_title(f"Modern - LGM SAT ($^\circ$C)", loc="center", fontsize=16)
ax.gridlines(color="k", linewidth=1, linestyle=(0, (1, 5)))
ax.spines["geo"].set_edgecolor("black")

Before we interpret this data, another useful way to visualize this data is through a plot of zonal mean temperature (the average temperature for all locations at a single latitude). Once we calculate this zonal mean, we can create a plot of LGM to present temperature anomalies versus latitude.

zonal_mean = sat_change.mean(dim="lon")
latitude = ds.lat
# Make a zonal mean figure of the changes
fig, ax1 = plt.subplots(1, 1)
ax1.plot(zonal_mean, latitude)
ax1.axvline(x=0, color="gray", alpha=1, linestyle=":", linewidth=2)
ax1.set_ylim(-90, 90)
ax1.set_xlabel("$\Delta$T ($^\circ$C)")
ax1.set_ylabel("Latitude ($^\circ$)")
ax1.set_title(
    f"Zonal-mean $\Delta$T ($^\circ$C) changes",  # ohad comment: same changes
    loc="center",
)

Questions 1.2: Climate Connection#

Looking at both the map and zonal mean plot, consider the following questions:

  1. How does the temperature anomaly vary with latitude?

  2. What might be causing spatial differences in the temperature anomaly?

# to_remove explanation

"""
1. Although there was a global increase in temperature between the LGM to present, the magnitude of this warming (i.e., the temperature anomaly) varied with latitude. Generally, the high latitudes, particularly in the north experienced larger warming than the tropics.

2. This pattern is known as polar amplification, which is caused by a number of factors including changes in albedo as ice sheets melt, changes in atmospheric and oceanic circulation, and changes in the distribution of solar insolation.

"""
'\n1. Although there was a global increase in temperature between the LGM to present, the magnitude of this warming (i.e., the temperature anomaly) varied with latitude. Generally, the high latitudes, particularly in the north experienced larger warming than the tropics.\n\n2. This pattern is known as polar amplification, which is caused by a number of factors including changes in albedo as ice sheets melt, changes in atmospheric and oceanic circulation, and changes in the distribution of solar insolation.\n\n'

Submit your feedback#

Hide code cell source
# @title Submit your feedback
content_review(f"{feedback_prefix}_Questions_1_2")

Summary#

In this last tutorial of this day, you explored the intersection of paleoclimate proxies and models through reanalysis products, specifically analyzing the Last Glacial Maximum Reanalysis (LGMR) from Osman et al. (2021).

Through this tutorial, you’ve learned a range of new skills and knowledge:

  • Interpreting Paleoclimate Reconstructions: You have learned how to decode and visualize the time series of a paleoclimate reconstruction of surface air temprature from a reanalysis product in order to enhance your understanding of the temperature variations from the Last Glacial Maximum to the present day.

  • Constructing Global Temperature Anomaly Maps: You’ve acquired the ability to construct and understand global maps that represent temperature anomalies, providing a more holistic view of the Earth’s climate during the Last Glacial Maximum.

  • Examining Latitude’s Role in Temperature Variations: You’ve explored how temperature anomalies from the Last Glacial Maximum to the present day vary by latitude and pondered the reasons behind these variations, enriching your understanding of latitudinal effects on global climate patterns.

Resources#

The code for this notebook is based on code available from Erb et al. (2022) and workflow presented during the Paleoclimate Data Assimilation Workshop 2022.

Data from the following sources are used in this tutorial:

  • Matthew B. Osman, Jessica E. Tierney, Jiang Zhu, Robert Tardif, Gregory J. Hakim, Jonathan King, Christopher J. Poulsen. 2021. Globally resolved surface temperatures since the Last Glacial Maximum. Nature, 599, 239-244. http://doi.org/10.1038/s41586-021-03984-4.

  • King, J. M., Tierney, J., Osman, M., Judd, E. J., & Anchukaitis, K. J. (2023). DASH: A MATLAB Toolbox for Paleoclimate Data Assimilation. Geoscientific Model Development, preprint. https://doi.org/10.5194/egusphere-2023-68.