Milestone 6 - Project Results

Expected Results

This is an example for the expected results:

Annual Temperature Animation

Annual Temperature calculated with the 2D-EBM model:

Annual temperature animation

Mean Temperature Plot using the 2D-EBM

Plot of the mean and average temperature calculated by using the 2D-EBM and then averaging over the results:

Mean temperature plot

Cologne Temperature using the 2D-EBM

Cologne temperature calculated using the 2D-EBM model:

Cologne temperature plot

Temperature for the NASA CO2 Data

Annual Temperatures from 1959 to 2020 based on NASA CO2 data:

Temperature for NASA CO2 data

Mean Temperature Plot for Ziegler's data

Plot of the mean and average temperature calculated by using the 2D-EBM with Ziegler's parameters and then averaging over the results:

Mean temperature plot for Ziegler's data

⚠ Warning!
We strongly suggest to first work on the solutions on your own/within your group without checking directly for the reference solutions.

Files Download

The Python implementation of milestone 6 can be downloaded here (Julia version will be added soon):

Scripts for Milestone 6

You can also check out our Julia and Python implementations of milestone 6 in this site.

Julia implementation of milestone 6

include("milestone1.jl")
include("milestone2.jl")
include("milestone3.jl")
include("milestone4.jl")
include("milestone5.jl")

using SparseArrays

function timestep_euler_backward_2d(jacobian, delta_t)
    A = factorize(sparse(I - delta_t * jacobian))

    function timestep_function(temperature, t, delta_t,
                               mesh, diffusion_coeff, heat_capacity, solar_forcing,
                               radiative_cooling)
        if t == 1
            t_old = size(temperature, 3)
        else
            t_old = t - 1
        end
        # Similar to MS3, we have to solve the equation
        # T_t = T_{t-1} + delta_t * f(T_t, t),
        # where f(T, t) = R(T) + F(t).
        # We use the fact that R is linear and thus can be written as R(T) = AT, where A is the Jacobian of R.
        # Solving for T_t yields
        # T_t = (I - delta_t * A)^{-1} * (T_{t-1} + delta_t * F(t)).
        @views source_terms = calc_source_terms_ebm_2d(heat_capacity,
                                                       solar_forcing[:, :, t],
                                                       radiative_cooling)

        return @views temperature[:, :, t] = reshape(A \ vec(temperature[:, :, t_old] +
                                                         delta_t * source_terms),
                                                     (mesh.n_latitude, mesh.n_longitude))
    end

    return timestep_function
end

function co2_evolution(jacobian, mesh, diffusion_coeff, heat_capacity, solar_forcing)
    # Read CO2 data
    co2_data = readdlm(joinpath(@__DIR__, "input", "co2_nasa.dat"))

    # Assume that only data for full years is available
    n_years = Int(size(co2_data, 1) / 12)

    average_co2 = [sum(co2_data[(12y + 1):(12y + 12), 4]) / 12 for y in 0:(n_years - 1)]

    ntimesteps = size(solar_forcing, 3)

    average_temperatures = zeros(n_years)
    annual_temperatures = zeros(ntimesteps * n_years)

    timestep_function = timestep_euler_backward_2d(jacobian, 1 / ntimesteps)

    temperature_grid = 15 *
                       ones((mesh.n_latitude, mesh.n_longitude, size(solar_forcing, 3)))

    for y in 1:n_years
        radiative_cooling = calc_radiative_cooling_co2(average_co2[y])
        (temperature_grid,
         area_mean_temp) = compute_equilibrium_2d(timestep_function,
                                                  mesh, diffusion_coeff,
                                                  heat_capacity,
                                                  solar_forcing,
                                                  radiative_cooling,
                                                  rel_error=1.0e-2,
                                                  initial_temperature=temperature_grid)
        annual_temperatures[(ntimesteps * (y - 1) + 1):(ntimesteps * y)] = area_mean_temp
        average_temperatures[y] = sum(area_mean_temp) / ntimesteps
    end

    first_year = Int(co2_data[1, 1])
    last_year = Int(co2_data[end, 1])

    return annual_temperatures, average_temperatures, first_year, last_year
end

function plot_co2_evolution(jacobian, mesh, diffusion_coeff, heat_capacity, solar_forcing)
    (annual_temperatures, average_temperatures, first_year,
     last_year) = co2_evolution(jacobian, mesh, diffusion_coeff,
                                heat_capacity, solar_forcing)

    n_timesteps = length(annual_temperatures)

    average_temperatures_per_month = [average_temperatures[floor(Int,
                                                                                      (t - 1) / 48) + 1]
                                      for t in 1:n_timesteps]

    labels = first_year:10:last_year

    p = plot(average_temperatures_per_month, label="average temperature",
             xlims=(1, n_timesteps), xticks=(1:480:n_timesteps, labels),
             ylabel="surface temperature [°C]",
             title="Annual temperature with CO2 data from NASA")
    plot!(p, annual_temperatures, label="annual temperature")

    return p
end

function calc_albedo_ziegler(geo_dat)
    function legendre(latitude)
        return 0.5 * (3 * sin(latitude)^2 - 1)
    end

    function albedo(surface_type, latitude)
        if surface_type == 1
            return 0.32 + 0.05 * legendre(latitude)
        elseif surface_type == 2
            return 0.6
        elseif surface_type == 3
            return 0.7
        elseif surface_type == 5
            return 0.289 + 0.08 * legendre(latitude)
        else
            error("Unknown surface type $surface_type.")
        end
    end

    nlatitude, nlongitude = size(geo_dat)
    y_lat = range(pi / 2, -pi / 2, length=nlatitude)

    # Map surface type to albedo.
    return [albedo(geo_dat[i, j], y_lat[i]) for i in 1:nlatitude, j in 1:nlongitude]
end

function calc_heat_capacity_ziegler(geo_dat)
    sec_per_yr = 3.15576e7  # seconds per year

    c_atmos = 0.79e6
    c_ocean = 394.47e6
    c_seaice = 4.83e6
    c_land = 1.87e6
    c_snow = 1.52e6

    function heat_capacity(surface_type)
        if surface_type == 1
            capacity_surface = c_land
        elseif surface_type == 2
            capacity_surface = c_seaice
        elseif surface_type == 3
            capacity_surface = c_snow
        elseif surface_type == 5
            capacity_surface = c_ocean
        else
            error("Unknown surface type $surface_type.")
        end

        return (capacity_surface + c_atmos) / sec_per_yr
    end

    nlatitude, nlongitude = size(geo_dat)
    return [heat_capacity(geo_dat[i, j]) for i in 1:nlatitude, j in 1:nlongitude]
end

function calc_diffusion_coefficients_ziegler(geo_dat)
    nlatitude, nlongitude = size(geo_dat)

    coeff_ocean_poles = 0.4
    coeff_ocean_equator = 0.9
    coeff_equator = 0.9
    coeff_north_pole = 0.45
    coeff_south_pole = 0.12

    function diffusion_coefficient(i, j)
        # Compute the i value of the equator
        i_equator = div(nlatitude, 2) + 1

        theta = pi * (i - 1) / (nlatitude - 1)
        colat = sin(theta)^5

        geo = geo_dat[i, j]
        if geo == 5  # ocean
            return coeff_ocean_poles + (coeff_ocean_equator - coeff_ocean_poles) * colat
        else  # land, sea ice, etc
            if i <= i_equator  # northern hemisphere
                # on the equator colat=1 -> coefficients for norhern/southern hemisphere cancels out
                return coeff_north_pole + (coeff_equator - coeff_north_pole) * colat
            else  # southern hemisphere
                return coeff_south_pole + (coeff_equator - coeff_south_pole) * colat
            end
        end
    end

    return [diffusion_coefficient(i, j) for i in 1:nlatitude, j in 1:nlongitude]
end

function simulation_ziegler(geo_dat, mesh, true_longitude)
    albedo = calc_albedo_ziegler(geo_dat)
    heat_capacity = calc_heat_capacity_ziegler(geo_dat)

    solar_forcing = calc_solar_forcing(albedo, true_longitude)
    ntimesteps = length(true_longitude)
    diffusion_coeff = calc_diffusion_coefficients_ziegler(geo_dat)

    co2_ppm = 315.0
    radiative_cooling = calc_radiative_cooling_co2(co2_ppm, 315.0, 210.2)

    jacobian = calc_jacobian_ebm_2d(mesh, diffusion_coeff, heat_capacity, 2.13)

    (temperature,
     area_mean_temp) = compute_equilibrium_2d(timestep_euler_backward_2d(jacobian,
                                                                         1 / ntimesteps),
                                              mesh, diffusion_coeff, heat_capacity,
                                              solar_forcing,
                                              radiative_cooling)

    # Copied from MS4
    # Area mean of pointwise annual temperature
    annual_mean_temperature_north = [calc_mean_north(temperature[:, :, t], mesh.area)
                                     for t in 1:ntimesteps]
    annual_mean_temperature_south = [calc_mean_south(temperature[:, :, t], mesh.area)
                                     for t in 1:ntimesteps]
    annual_mean_temperature_total = [calc_mean(temperature[:, :, t], mesh.area)
                                     for t in 1:ntimesteps]

    average_temperature_north = sum(annual_mean_temperature_north) / ntimesteps
    average_temperature_south = sum(annual_mean_temperature_south) / ntimesteps
    average_temperature_total = sum(annual_mean_temperature_total) / ntimesteps

    p = plot_annual_temperature_north_south(annual_mean_temperature_north,
                                            annual_mean_temperature_south,
                                            annual_mean_temperature_total,
                                            average_temperature_north,
                                            average_temperature_south,
                                            average_temperature_total)

    return p
end

# Run code
function milestone6()
    geo_dat = read_geography(joinpath(@__DIR__, "input", "The_World128x65.dat"))
    mesh = Mesh(geo_dat)

    albedo = calc_albedo(geo_dat)
    heat_capacity = calc_heat_capacity(geo_dat)

    # Compute solar forcing
    true_longitude = read_true_longitude(joinpath(@__DIR__, "input", "True_Longitude.dat"))
    solar_forcing = calc_solar_forcing(albedo, true_longitude)
    ntimesteps = length(true_longitude)

    # Compute and plot diffusion coefficient
    diffusion_coeff = calc_diffusion_coefficients(geo_dat)

    co2_ppm = 315.0
    radiative_cooling = calc_radiative_cooling_co2(co2_ppm)

    jacobian = calc_jacobian_ebm_2d(mesh, diffusion_coeff, heat_capacity)

    (temperature,
     _) = compute_equilibrium_2d(timestep_euler_backward_2d(jacobian,
                                                            1 / ntimesteps),
                                 mesh, diffusion_coeff, heat_capacity,
                                 solar_forcing,
                                 radiative_cooling)

    plot_mean_temperature = plot_temperature(temperature, geo_dat, 1)

    # Copied from MS4
    # Area mean of pointwise annual temperature
    annual_mean_temperature_north = [calc_mean_north(temperature[:, :, t], mesh.area)
                                     for t in 1:ntimesteps]
    annual_mean_temperature_south = [calc_mean_south(temperature[:, :, t], mesh.area)
                                     for t in 1:ntimesteps]
    annual_mean_temperature_total = [calc_mean(temperature[:, :, t], mesh.area)
                                     for t in 1:ntimesteps]

    average_temperature_north = sum(annual_mean_temperature_north) / ntimesteps
    average_temperature_south = sum(annual_mean_temperature_south) / ntimesteps
    average_temperature_total = sum(annual_mean_temperature_total) / ntimesteps

    plot_temperature_ = plot_annual_temperature_north_south(annual_mean_temperature_north,
                                                            annual_mean_temperature_south,
                                                            annual_mean_temperature_total,
                                                            average_temperature_north,
                                                            average_temperature_south,
                                                            average_temperature_total)

    # Compute temperature in Cologne.
    # Cologne lies about halfway between these two grid points.
    annual_temperature_cologne = (temperature[15, 68, :] +
                                  temperature[15, 69, :]) / 2
    average_temperature_cologne = sum(annual_temperature_cologne) / ntimesteps

    plot_cologne = plot_annual_temperature(annual_temperature_cologne,
                                           average_temperature_cologne,
                                           "Annual temperature with CO2 = $co2_ppm [ppm] in Cologne")

    plot_temperature_co2 = plot_co2_evolution(jacobian, mesh, diffusion_coeff,
                                              heat_capacity, solar_forcing)

    plot_ziegler = simulation_ziegler(geo_dat, mesh, true_longitude)

    # Animate annual temperature
    anim = @animate for ts in 1:ntimesteps
        plot_temperature(temperature, geo_dat, ts)
    end

    plot_temperature_day_80 = plot_temperature(temperature, geo_dat, 1)
    gif_temperature = gif(anim, joinpath(@__DIR__, "annual_temperature.gif"), fps=7)

    # Show the plots
    display(plot_mean_temperature)
    display(plot_temperature_)
    display(plot_cologne)
    display(plot_temperature_co2)
    display(plot_ziegler)
    display(gif_temperature)

    return plot_mean_temperature, plot_temperature_, plot_cologne,
           plot_temperature_co2, plot_ziegler, plot_temperature_day_80, gif_temperature
end

Python implementation of milestone 6

import os

import imageio
import numpy as np
from matplotlib import pyplot as plt
from scipy import sparse

from milestone1 import read_geography
from milestone2 import calc_albedo, calc_heat_capacity, calc_solar_forcing, read_true_longitude
from milestone3 import calc_radiative_cooling_co2, calc_mean, plot_annual_temperature
from milestone4 import plot_annual_temperature_north_south, calc_mean_north, calc_mean_south, plot_temperature
from milestone5 import calc_diffusion_coefficients, calc_source_terms_ebm_2d, Mesh, compute_equilibrium_2d, \
    calc_jacobian_ebm_2d


def timestep_euler_backward_2d(jacobian, delta_t_):
    m, n = jacobian.shape
    eye = sparse.eye(m, n, format="csc")
    jacobian = sparse.csc_matrix(jacobian)
    solve = sparse.linalg.factorized(eye - delta_t_ * jacobian)

    def timestep_function(temperature, t, delta_t,
                          mesh, _, heat_capacity, solar_forcing, radiative_cooling):
        # Similar to MS3, we have to solve the equation
        # T_t = T_{t-1} + delta_t * f(T_t, t),
        # where f(T, t) = R(T) + F(t).
        # We use the fact that R is linear and thus can be written as R(T) = AT, where A is the Jacobian of R.
        # Solving for T_t yields
        # T_t = (I - delta_t * A)^{-1} * (T_{t-1} + delta_t * F(t)).
        source_terms = calc_source_terms_ebm_2d(heat_capacity, solar_forcing[:, :, t], radiative_cooling)

        temperature[:, :, t] = np.reshape(solve((temperature[:, :, t - 1] + delta_t * source_terms).flatten()),
                                          (mesh.n_latitude, mesh.n_longitude))

    return timestep_function


def co2_evolution(jacobian, mesh, diffusion_coeff, heat_capacity, solar_forcing):
    # Read CO2 data
    co2_data = np.genfromtxt("input/co2_nasa.dat")

    # Assume that only data for full years is available
    n_years = int(co2_data.shape[0] / 12)

    average_co2 = np.array(
        [np.sum(co2_data[12 * y:12 * (y + 1), 3]) / 12 for y in range(n_years)]
    )

    ntimesteps = solar_forcing.shape[2]

    average_temperatures = np.zeros(n_years)
    annual_temperatures = np.zeros(ntimesteps * n_years)

    timestep_function = timestep_euler_backward_2d(jacobian, 1 / ntimesteps)

    temperature = np.zeros((mesh.n_latitude, mesh.n_longitude, solar_forcing.shape[2]))

    for y in range(n_years):
        radiative_cooling = calc_radiative_cooling_co2(average_co2[y])
        temperature, area_mean_temp = compute_equilibrium_2d(timestep_function,
                                                   mesh, diffusion_coeff, heat_capacity, solar_forcing,
                                                   radiative_cooling,
                                                   initial_temperature=temperature, rel_error=1e-2)
        annual_temperatures[ntimesteps * y:ntimesteps * (y + 1)] = area_mean_temp
        average_temperatures[y] = np.sum(area_mean_temp) / ntimesteps

    first_year = int(co2_data[0, 0])
    last_year = int(co2_data[-1, 0])

    return annual_temperatures, average_temperatures, first_year, last_year


def plot_co2_evolution(jacobian, mesh, diffusion_coeff, heat_capacity, solar_forcing):
    fig, ax = plt.subplots()

    annual_temperatures, average_temperatures, first_year, last_year = co2_evolution(jacobian, mesh, diffusion_coeff,
                                                                                     heat_capacity, solar_forcing)
    n_timesteps = len(annual_temperatures)

    average_temperatures_per_month = np.array(
        [average_temperatures[int(t / 48)] for t in range(n_timesteps)]
    )

    plt.plot(average_temperatures_per_month, label="average temperature")
    plt.plot(annual_temperatures, label="annual temperature")

    plt.xlim((0, n_timesteps - 1))
    labels = np.arange(first_year, last_year, 10)
    plt.xticks(np.arange(0, n_timesteps - 1, 480), labels)
    ax.set_ylabel("surface temperature [°C]")
    plt.grid()
    plt.title("Annual temperature with CO2 data from NASA")
    plt.legend(loc="upper right")

    plt.tight_layout()
    plt.show()


def calc_albedo_ziegler(geo_dat):
    def legendre(latitude):
        return 0.5 * (3 * np.sin(latitude) ** 2 - 1)

    def albedo(surface_type, latitude):
        if surface_type == 1:
            return 0.32 + 0.05 * legendre(latitude)
        elif surface_type == 2:
            return 0.6
        elif surface_type == 3:
            return 0.7
        elif surface_type == 5:
            return 0.289 + 0.08 * legendre(latitude)
        else:
            raise ValueError(f"Unknown surface type {surface_type}.")

    nlatitude, nlongitude = geo_dat.shape
    y_lat = np.linspace(np.pi / 2, -np.pi / 2, nlatitude)

    # Map surface type to albedo.
    return np.array([[albedo(geo_dat[i, j], y_lat[i])
                      for j in range(nlongitude)]
                     for i in range(nlatitude)])


def calc_heat_capacity_ziegler(geo_dat):
    sec_per_yr = 3.15576e7  # seconds per year

    c_atmos = 0.79e6
    c_ocean = 394.47e6
    c_seaice = 4.83e6
    c_land = 1.87e6
    c_snow = 1.52e6

    def heat_capacity(surface_type):
        if surface_type == 1:
            capacity_surface = c_land
        elif surface_type == 2:
            capacity_surface = c_seaice
        elif surface_type == 3:
            capacity_surface = c_snow
        elif surface_type == 5:
            capacity_surface = c_ocean
        else:
            raise ValueError(f"Unknown surface type {surface_type}.")

        return (capacity_surface + c_atmos) / sec_per_yr

    # Map surface type to heat capacity.
    nlatitude, nlongitude = geo_dat.shape
    return np.array([[heat_capacity(geo_dat[i, j])
                      for j in range(nlongitude)]
                     for i in range(nlatitude)])


def calc_diffusion_coefficients_ziegler(geo_dat):
    nlatitude, nlongitude = geo_dat.shape

    coeff_ocean_poles = 0.4
    coeff_ocean_equator = 0.9
    coeff_equator = 0.9
    coeff_north_pole = 0.45
    coeff_south_pole = 0.12

    def diffusion_coefficient(j, i):
        # Compute the j value of the equator
        j_equator = int(nlatitude / 2)

        theta = np.pi * j / np.real(nlatitude - 1)
        colat = np.sin(theta) ** 5

        geo = geo_dat[j, i]
        if geo == 5:  # ocean
            return coeff_ocean_poles + (coeff_ocean_equator - coeff_ocean_poles) * colat
        else:  # land, sea ice, etc
            if j <= j_equator:  # northern hemisphere
                # on the equator colat=1 -> coefficients for norhern/southern hemisphere cancels out
                return coeff_north_pole + (coeff_equator - coeff_north_pole) * colat
            else:  # southern hemisphere
                return coeff_south_pole + (coeff_equator - coeff_south_pole) * colat

    return np.array(
        [[diffusion_coefficient(j, i) for i in range(nlongitude)] for j in range(nlatitude)]
    )


def simulation_ziegler(geo_dat, mesh, true_longitude):
    albedo = calc_albedo_ziegler(geo_dat)
    heat_capacity = calc_heat_capacity_ziegler(geo_dat)

    solar_forcing = calc_solar_forcing(albedo, true_longitude)
    ntimesteps = len(true_longitude)
    diffusion_coeff = calc_diffusion_coefficients_ziegler(geo_dat)

    co2_ppm = 315.0
    radiative_cooling = calc_radiative_cooling_co2(co2_ppm, radiative_cooling_base=210.2)

    jacobian = calc_jacobian_ebm_2d(mesh, diffusion_coeff, heat_capacity, radiative_cooling_feedback=2.13)

    temperature, area_mean_temp = compute_equilibrium_2d(timestep_euler_backward_2d(jacobian, 1 / ntimesteps),
                                                         mesh, diffusion_coeff, heat_capacity, solar_forcing,
                                                         radiative_cooling)

    # Copied from MS4
    # Area mean of pointwise annual temperature
    annual_mean_temperature_north = [calc_mean_north(temperature[:, :, t], mesh_.area) for t in range(ntimesteps)]
    annual_mean_temperature_south = [calc_mean_south(temperature[:, :, t], mesh_.area) for t in range(ntimesteps)]
    annual_mean_temperature_total = [calc_mean(temperature[:, :, t], mesh_.area) for t in range(ntimesteps)]

    average_temperature_north = np.sum(annual_mean_temperature_north) / ntimesteps
    average_temperature_south = np.sum(annual_mean_temperature_south) / ntimesteps
    average_temperature_total = np.sum(annual_mean_temperature_total) / ntimesteps

    plot_annual_temperature_north_south(annual_mean_temperature_north, annual_mean_temperature_south,
                                        annual_mean_temperature_total, average_temperature_north,
                                        average_temperature_south, average_temperature_total)


# Run code
if __name__ == '__main__':
    geo_dat_ = read_geography("input/The_World128x65.dat")
    mesh_ = Mesh(geo_dat_)

    albedo_ = calc_albedo(geo_dat_)
    heat_capacity_ = calc_heat_capacity(geo_dat_)

    # Compute solar forcing
    true_longitude_ = read_true_longitude("input/True_Longitude.dat")
    solar_forcing_ = calc_solar_forcing(albedo_, true_longitude_)
    ntimesteps_ = len(true_longitude_)

    diffusion_coeff_ = calc_diffusion_coefficients(geo_dat_)

    co2_ppm_ = 315.0
    radiative_cooling_ = calc_radiative_cooling_co2(co2_ppm_)

    jacobian_ = calc_jacobian_ebm_2d(mesh_, diffusion_coeff_, heat_capacity_)

    temperature_, area_mean_temp_ = compute_equilibrium_2d(timestep_euler_backward_2d(jacobian_, 1 / ntimesteps_),
                                                           mesh_, diffusion_coeff_, heat_capacity_, solar_forcing_,
                                                           radiative_cooling_)

    # Copied from MS4
    # Area mean of pointwise annual temperature
    annual_mean_temperature_north_ = [calc_mean_north(temperature_[:, :, t], mesh_.area) for t in range(ntimesteps_)]
    annual_mean_temperature_south_ = [calc_mean_south(temperature_[:, :, t], mesh_.area) for t in range(ntimesteps_)]
    annual_mean_temperature_total_ = [calc_mean(temperature_[:, :, t], mesh_.area) for t in range(ntimesteps_)]

    average_temperature_north_ = np.sum(annual_mean_temperature_north_) / ntimesteps_
    average_temperature_south_ = np.sum(annual_mean_temperature_south_) / ntimesteps_
    average_temperature_total_ = np.sum(annual_mean_temperature_total_) / ntimesteps_

    plot_annual_temperature_north_south(annual_mean_temperature_north_, annual_mean_temperature_south_,
                                        annual_mean_temperature_total_, average_temperature_north_,
                                        average_temperature_south_, average_temperature_total_)

    # Compute temperature in Cologne.
    # Cologne lies about halfway between these two grid points.
    annual_temperature_cologne = (temperature_[14, 67, :] +
                                  temperature_[14, 68, :]) / 2
    average_temperature_cologne = np.sum(annual_temperature_cologne) / ntimesteps_

    plot_annual_temperature(annual_temperature_cologne, average_temperature_cologne,
                            f"Annual temperature_ with CO2 = {co2_ppm_} [ppm] in Cologne")

    plot_co2_evolution(jacobian_, mesh_, diffusion_coeff_, heat_capacity_, solar_forcing_)

    simulation_ziegler(geo_dat_, mesh_, true_longitude_)

    # Plot temperature for each time step
    filenames = []
    for ts in range(48):
        filename_ = plot_temperature(temperature_, geo_dat_, ts, show_plot=False)
        filenames.append(filename_)

    # Build GIF
    frames = [imageio.v3.imread(filename_) for filename_ in filenames]
    imageio.mimsave("annual_temperature.gif", frames)

    # Remove files
    for filename_ in set(filenames):
        os.remove(filename_)

Created by Gregor Gassner and Andrés Rueda-Ramírez with contributions by Simone Chiocchetti, Daniel Bach, Sophia Horak, Philipp Baasch, Benjamin Bolm, Erik Faulhaber, and Luca Sommer. Last modified: April 02, 2026. Website built with Franklin.jl and the Julia programming language.