Bonus Tutorial 7: Assessing Climate Forcings#
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, Mauro Tripaldi, Chi Zhang, Ohad Zivan
Content editors: Yosmely Bermúdez, Paul Heubel, Zahra Khodakaramimaghsoud, 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: 25 minutes
In this tutorial, you will use data analysis tools and climate concepts you’ve learned today, and on previous days, to assess the forcings of climate variations observed in paleoclimate records.
By the end of this tutorial you will be able to:
Plot and interpret temperature reconstructions from speleothem oxygen isotopes
Generate and plot time series of solar insolation
Assess the orbital forcings on monsoon intensity over the past 400,000 years using spectral analysis
Setup#
# installations ( uncomment and run this cell ONLY when using google colab or kaggle )
# !pip install pyleoclim
# !pip install climlab
# you may get errors about fortran extensions not working, and that is ok we are not using those here
# in W1D5 you will learn how to install climlab in colab using conda which will fix these errors and give you
# full functionality of the package
# imports
# for Google Colab users: you might get a numpy.dtype error here, restart your session and rerun the code and it should solve it.
import pooch
import os
import tempfile
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import pyleoclim as pyleo
from climlab import constants as const
from climlab.solar.orbital import OrbitalTable
from climlab.solar.insolation import daily_insolation
Downloading data from 'http://www.atmos.albany.edu/facstaff/brose/resources/climlab_data/absorptivity/abs_ems_factors_fastvx.c030508.nc' to file '/home/runner/.cache/pooch/61707eb16bd77eb262c3c7db3706b35d-abs_ems_factors_fastvx.c030508.nc'.
Downloading data from 'http://www.atmos.albany.edu/facstaff/brose/resources/climlab_data/orbital/orbit91' to file '/home/runner/.cache/pooch/f5d05db954ff021372b5a39d7b9bb4c7-orbit91'.
---------------------------------------------------------------------------
TimeoutError Traceback (most recent call last)
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/connection.py:239, in HTTPConnection._new_conn(self)
238 try:
--> 239 sock = connection.create_connection(
240 (self._dns_host, self.port),
241 self.timeout,
242 source_address=self.source_address,
243 socket_options=self.socket_options,
244 )
245 except socket.gaierror as e:
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/util/connection.py:85, in create_connection(address, timeout, source_address, socket_options)
84 try:
---> 85 raise err
86 finally:
87 # Break explicitly a reference cycle
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/util/connection.py:73, in create_connection(address, timeout, source_address, socket_options)
72 sock.bind(source_address)
---> 73 sock.connect(sa)
74 # Break explicitly a reference cycle
TimeoutError: timed out
The above exception was the direct cause of the following exception:
ConnectTimeoutError Traceback (most recent call last)
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/connectionpool.py:793, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
792 # Make the request on the HTTPConnection object
--> 793 response = self._make_request(
794 conn,
795 method,
796 url,
797 timeout=timeout_obj,
798 body=body,
799 headers=headers,
800 chunked=chunked,
801 retries=retries,
802 response_conn=response_conn,
803 preload_content=preload_content,
804 decode_content=decode_content,
805 **response_kw,
806 )
808 # Everything went great!
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/connectionpool.py:499, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
498 try:
--> 499 conn.request(
500 method,
501 url,
502 body=body,
503 headers=headers,
504 chunked=chunked,
505 preload_content=preload_content,
506 decode_content=decode_content,
507 enforce_content_length=enforce_content_length,
508 )
510 # We are swallowing BrokenPipeError (errno.EPIPE) since the server is
511 # legitimately able to close the connection after sending a valid response.
512 # With this behaviour, the received response is still readable.
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/connection.py:567, in HTTPConnection.request(self, method, url, body, headers, chunked, preload_content, decode_content, enforce_content_length)
566 self.putheader(header, value)
--> 567 self.endheaders()
569 # If we're given a body we start sending that in chunks.
File ~/micromamba/envs/climatematch/lib/python3.11/http/client.py:1346, in HTTPConnection.endheaders(self, message_body, encode_chunked)
1345 raise CannotSendHeader()
-> 1346 self._send_output(message_body, encode_chunked=encode_chunked)
File ~/micromamba/envs/climatematch/lib/python3.11/http/client.py:1106, in HTTPConnection._send_output(self, message_body, encode_chunked)
1105 del self._buffer[:]
-> 1106 self.send(msg)
1108 if message_body is not None:
1109
1110 # create a consistent interface to message_body
File ~/micromamba/envs/climatematch/lib/python3.11/http/client.py:1044, in HTTPConnection.send(self, data)
1043 if self.auto_open:
-> 1044 self.connect()
1045 else:
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/connection.py:398, in HTTPConnection.connect(self)
397 def connect(self) -> None:
--> 398 self.sock = self._new_conn()
399 if self._tunnel_host:
400 # If we're tunneling it means we're connected to our proxy.
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/connection.py:248, in HTTPConnection._new_conn(self)
247 except SocketTimeout as e:
--> 248 raise ConnectTimeoutError(
249 self,
250 f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
251 ) from e
253 except OSError as e:
ConnectTimeoutError: (<HTTPConnection(host='www.atmos.albany.edu', port=80) at 0x796e9c788d10>, 'Connection to www.atmos.albany.edu timed out. (connect timeout=30)')
The above exception was the direct cause of the following exception:
MaxRetryError Traceback (most recent call last)
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/requests/adapters.py:696, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
695 try:
--> 696 resp = conn.urlopen(
697 method=request.method,
698 url=url,
699 body=request.body, # type: ignore[arg-type] # urllib3 stubs don't accept Iterable[bytes | str]
700 headers=request.headers, # type: ignore[arg-type] # urllib3#3072
701 redirect=False,
702 assert_same_host=False,
703 preload_content=False,
704 decode_content=False,
705 retries=self.max_retries,
706 timeout=resolved_timeout,
707 chunked=chunked,
708 )
710 except (ProtocolError, OSError) as err:
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/connectionpool.py:847, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
845 new_e = ProtocolError("Connection aborted.", new_e)
--> 847 retries = retries.increment(
848 method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2]
849 )
850 retries.sleep()
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/urllib3/util/retry.py:555, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
554 reason = error or ResponseError(cause)
--> 555 raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type]
557 log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)
MaxRetryError: HTTPConnectionPool(host='www.atmos.albany.edu', port=80): Max retries exceeded with url: /facstaff/brose/resources/climlab_data/orbital/orbit91 (Caused by ConnectTimeoutError(<HTTPConnection(host='www.atmos.albany.edu', port=80) at 0x796e9c788d10>, 'Connection to www.atmos.albany.edu timed out. (connect timeout=30)'))
During handling of the above exception, another exception occurred:
ConnectTimeout Traceback (most recent call last)
Cell In[2], line 14
10
11 import pyleoclim as pyleo
12
13 from climlab import constants as const
---> 14 from climlab.solar.orbital import OrbitalTable
15 from climlab.solar.insolation import daily_insolation
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/climlab/solar/orbital/__init__.py:42
39 import xarray as xr
40 from .table import _get_Berger_data
---> 42 OrbitalTable = _get_Berger_data()
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/climlab/solar/orbital/table.py:15, in _get_Berger_data(verbose)
12 def _get_Berger_data(verbose=True):
13 '''Read in the Berger and Loutre orbital table as a pandas dataframe, convert to xarray
14 '''
---> 15 orbit91handle = pooch.retrieve(path,
16 known_hash="3afc20dda7b385bdd366bc4c9cf60be02d8defdb4c0f317430ca8386d62f81a3")
17 orbit91_pd = pd.read_csv(orbit91handle, sep='\s+', skiprows=1,)
18 # As xarray structure with the dimension named 'kyear'
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/pooch/core.py:242, in retrieve(url, known_hash, fname, path, processor, downloader, progressbar)
239 if downloader is None:
240 downloader = choose_downloader(url, progressbar=progressbar)
--> 242 stream_download(url, full_path, known_hash, downloader, pooch=None)
244 if known_hash is None:
245 get_logger().info(
246 "SHA256 hash of downloaded file: %s\n"
247 "Use this value as the 'known_hash' argument of 'pooch.retrieve'"
(...) 250 file_hash(str(full_path)),
251 )
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/pooch/core.py:823, in stream_download(url, fname, known_hash, downloader, pooch, retry_if_failed)
819 try:
820 # Stream the file to a temporary so that we can safely check its
821 # hash before overwriting the original.
822 with temporary_file(path=str(fname.parent)) as tmp:
--> 823 downloader(url, tmp, pooch)
824 hash_matches(tmp, known_hash, strict=True, source=str(fname.name))
825 shutil.move(tmp, str(fname))
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/pooch/downloaders.py:230, in HTTPDownloader.__call__(self, url, output_file, pooch, check_only)
228 # pylint: enable=consider-using-with
229 try:
--> 230 response = requests.get(url, timeout=timeout, **kwargs)
231 response.raise_for_status()
232 content = response.iter_content(chunk_size=self.chunk_size)
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/requests/api.py:87, in get(url, params, **kwargs)
74 def get(
75 url: _t.UriType, params: _t.ParamsType = None, **kwargs: Unpack[_t.GetKwargs]
76 ) -> Response:
77 r"""Sends a GET request.
78
79 :param url: URL for the new :class:`Request` object.
(...) 84 :rtype: requests.Response
85 """
---> 87 return request("get", url, params=params, **kwargs)
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/requests/api.py:71, in request(method, url, **kwargs)
67 # By using the 'with' statement we are sure the session is closed, thus we
68 # avoid leaving sockets open which can trigger a ResourceWarning in some
69 # cases, and look like a memory leak in others.
70 with sessions.Session() as session:
---> 71 return session.request(method=method, url=url, **kwargs)
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/requests/sessions.py:651, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
646 send_kwargs = {
647 "timeout": timeout,
648 "allow_redirects": allow_redirects,
649 }
650 send_kwargs.update(settings)
--> 651 resp = self.send(prep, **send_kwargs)
653 return resp
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/requests/sessions.py:784, in Session.send(self, request, **kwargs)
781 start = preferred_clock()
783 # Send the request
--> 784 r = adapter.send(request, **kwargs)
786 # Total elapsed time of the request (approximately)
787 elapsed = preferred_clock() - start
File ~/micromamba/envs/climatematch/lib/python3.11/site-packages/requests/adapters.py:717, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
714 if isinstance(e.reason, ConnectTimeoutError):
715 # TODO: Remove this in 3.0.0: see #2811
716 if not isinstance(e.reason, NewConnectionError):
--> 717 raise ConnectTimeout(e, request=request)
719 if isinstance(e.reason, ResponseError):
720 raise RetryError(e, request=request)
ConnectTimeout: HTTPConnectionPool(host='www.atmos.albany.edu', port=80): Max retries exceeded with url: /facstaff/brose/resources/climlab_data/orbital/orbit91 (Caused by ConnectTimeoutError(<HTTPConnection(host='www.atmos.albany.edu', port=80) at 0x796e9c788d10>, 'Connection to www.atmos.albany.edu timed out. (connect timeout=30)'))
Install and import feedback gadget#
Show 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_T7"
Figure Settings#
Show code cell source
# @title Figure Settings
import ipywidgets as widgets # interactive display
%config InlineBackend.figure_format = 'retina'
plt.style.use(
"https://raw.githubusercontent.com/abodner/climate-course-content/main/cma.mplstyle"
)
Helper functions#
Show 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: Interpreting Climate Forcings#
Submit your feedback#
Show code cell source
# @title Submit your feedback
content_review(f"{feedback_prefix}_Interpreting_Climate_Forcings_Video")
If you want to download the slides: https://osf.io/download/ztfwk/
Submit your feedback#
Show code cell source
# @title Submit your feedback
content_review(f"{feedback_prefix}_Interpreting_Climate_Forcings_Slides")
Section 1: Understanding Climate Forcings#
A common task in paleoclimatology is to relate a proxy record (or several of them) to the particular forcing(s) that is thought to dominate that particular record (e.g., based on the proxy, location, etc.). We’ve already spent some time in earlier tutorials learning about the influence of Earth’s orbital configuration on glacial-interglacial cycles. In this tutorial, we’ll assess the climate forcings of monsoon intensity over the past 400,000 years.
Recall from the video that monsoons are seasonal changes in the direction of the strongest wind and precipitation that are primarily driven by variations in seasonal insolation. Land and the ocean have different abilities to hold onto heat. Land cools and warms much faster than the ocean does due to high heat capacity. This temperature difference leads to a pressure difference that drives atmospheric circulations called monsoons.
Summer (Northern Hemisphere): the land is warmer than the ocean, so the winds blow towards the land, resulting in heavy rainfall over land.
Winter (Northern Hemisphere): the land is cooler than the ocean, so the winds blow away from the land, resulting in heavy rainfall over the ocean and decreased rainfall over land.
On longer timescales, changes in insolation and the mean climate state can drive changes in monsoon intensity. To assess these long-term changes, we can analyze paleoclimate reconstructions from monsoon regions such as India, Southeast Asia or Africa. δ18O records from speleothems in Chinese caves are broadly interpreted to reflect continental-scale monsoon circulations.
In this tutorial, we’ll plot and analyze a composite of three δ18O speleothem records from multiple caves in China (Sanbao, Hulu, and Dongge caves) from Cheng et al. (2016). We will then assess the relationship between the climate signals recorded by the speleothem δ18O and solar insolation.
First, we can download and plot the speleothem oxygen isotope data:
# download the data from the url
filename_Sanbao_composite = "Sanbao_composite.csv"
url_Sanbao_composite = "https://raw.githubusercontent.com/LinkedEarth/paleoHackathon/main/data/Orbital_records/Sanbao_composite.csv"
data = pd.read_csv(
pooch_load(filelocation=url_Sanbao_composite, filename=filename_Sanbao_composite)
)
# create a pyleo.Series
d18O_data = pyleo.Series(
time=data["age"] / 1000,
time_name="Age",
time_unit="kyr BP",
value=-data["d18O"],
value_name=r"$\delta^{18}$O",
value_unit="\u2030",
)
d18O_data.plot()
Downloading data from 'https://raw.githubusercontent.com/LinkedEarth/paleoHackathon/main/data/Orbital_records/Sanbao_composite.csv' to file '/tmp/Sanbao_composite.csv'.
SHA256 hash of downloaded file: 79755093d2b1fcfe888f141cbd36d112bbe38ab002c722ad2c391b387b0d0743
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
Time axis values sorted in ascending order
(<Figure size 1000x400 with 1 Axes>,
<Axes: xlabel='Age [kyr BP]', ylabel='$\\delta^{18}$O [‰]'>)
You may notice that in the figure we just made, the δ18O values on the y-axis is plotted with more positive values up, whereas in previous tutorials, we’ve plotted isotopic data with more negative values up (since more negative/“depleted” suggests warmer temperatures or increased rainfall). However, the pre-processed δ18O data that we’re using in this tutorial was multipled by -1, so now a more positive/“enriched” value suggests warmer temperatures or increased rainfall. In other words, in this figure, upward on the y-axis is increased monsoon intensity and downward on the y-axis is decreased monsoon intensity.
Let’s apply what we learned in the previous tutorial to perform spectral analysis on the speleothem oxygen isotope data. Recall from the previous tutorial that spectral analysis can help us identify dominant cyclicities in the data, which can be useful for assessing potential climate forcings.
Here we’ll use the Weighted Wavelet Z-Transform (WWZ) method you learned about in the previous tutorial:
# standardize the data
d18O_stnd = d18O_data.interp(step=0.5).standardize() # save it for future use
# calculate the WWZ spectral analysis
d18O_wwz = d18O_stnd.spectral(method="wwz")
# plot WWZ results
d18O_wwz.plot(xlim=[100, 5], ylim=[0.001, 1000])
(<Figure size 1000x400 with 1 Axes>,
<Axes: xlabel='Period [kyr]', ylabel='PSD'>)
Coding Exercises 1#
The dominant spectral power is at ~23,000 years. This suggests a link between monsoon intensity and orbital precession! Is this peak significant? Use the skills you learned in the last tutorial to test the significance of this peak at the 95% confidence level. For this exercise, input
number = 30as the default value which will take a long time to run.
# perform significance test with 5 surrogates
d18O_wwz_sig = ...
# plot the results
_ = ...
Example output:
Submit your feedback#
Show code cell source
# @title Submit your feedback
content_review(f"{feedback_prefix}_Coding_Exercises_1")
Section 2: Constructing Insolation Curves#
To further explore and confirm the relationship between monsoon intensity and orbital precession, let’s take a look at insolation data and compare this to the speleothem δ18O records from Asia. Recall that insolation is controlled by variations in Earth’s orbital cycles (eccentricity, obliquity, precession), so by comparing the δ18O record to insolation, we can assess the influence of orbital variations on δ18O and monsoon intensity.
To compute solar insolation, we can use the package climlab by Brian Rose. Let’s create a time series over the past 400,000 years of changes in summer insolation at 31.67ºN, which is the latitude of Sanbao, one of the caves from which the speleothem records were produced.
# specify time interval and units
kyears = np.linspace(-400, 0, 1001)
# subset of orbital parameters for specified time
orb = OrbitalTable.interp(kyear=kyears)
days = np.linspace(0, const.days_per_year, 365)
# generate insolation at Sanbao latitude (31.67)
Qsb = daily_insolation(31.67, days, orb)
# julian days 152-243 are JJA
Qsb_jja = np.mean(Qsb[:, 151:243], axis=1)
Now we can store this data as a Series in Pyleoclim and plot the data versus time:
ts_qsb = pyleo.Series(
time=-kyears,
time_name="Age",
time_unit="ky BP",
value=Qsb_jja,
value_name="JJA Insolation",
value_unit=r"$W.m^{-2}$",
)
ts_qsb.plot()
Next, let’s plot and compare the speleothem δ18O data and the solar insolation data:
# standardize the insolation data
ts_qsb_stnd = ts_qsb.standardize()
# create a MultipleSeries of the speleothem d18O record and insolation data
compare = [d18O_stnd, ts_qsb_stnd]
ms_compare = pyleo.MultipleSeries(compare, time_unit="kyr BP", name=None)
# create a stackplot to compare the data
ms_compare.stackplot()
By visually comparing the time series of the two records, we can see similarites at orbital scales. To confirm this, we can use spectral analysis to determine the dominant spectral power of the insolation data:
# calculate the WWZ spectral analysis
psd_wwz = ts_qsb_stnd.spectral(method="wwz")
psd_wwz.plot()
Questions 2: Climate Connection#
What is the dominant spectral power in summer insolation at 31ºN latitude? How does this compare to the speleothem data?
Why might there be a relationship between solar insolation and monsoon intensity? What does the common spectral power in both the insolation and δ18O records suggest about the climate forcings driving monsoon intensity in this region?
Submit your feedback#
Show code cell source
# @title Submit your feedback
content_review(f"{feedback_prefix}_Questions_2")
Summary#
In this tutorial, you’ve gained valuable insights into the complex world of paleoclimatology and climate forcings. Here’s a recap of what you’ve learned:
You’ve discovered how to plot and interpret temperature reconstructions derived from speleothem oxygen isotopes.
You’ve assessed the likely impact of orbital forcings on monsoon intensity over the past 400,000 years using spectral analysis.
By comparing the δ18O record to insolation, you’ve developed a deeper understanding of the relationship between solar insolation and monsoon intensity.
Resources#
Code for this tutorial is based on an existing notebook from LinkedEarth that explore forcing and responses in paleoclimate data.
Data from the following sources are used in this tutorial:
Cheng, H., Edwards, R., Sinha, A. et al. The Asian monsoon over the past 640,000 years and ice age terminations. Nature 534, 640–646 (2016). https://doi.org/10.1038/nature18591