{ "cells": [ { "cell_type": "markdown", "id": "31229d12", "metadata": {}, "source": [ "# CAMulator: an AI Emulator of the Community Atmosphere Model (CAM)\n", "\n", "**CAMulator** is a machine-learned copy (an *emulator*) of the Community Atmosphere Model (CAM) in NCAR's climate model, CESM. Given the\n", "sea-surface temperature (SST), sea ice, CO2 and sunlight as boundary conditions (forcing) it steps the whole global atmosphere forward 6 hours at a\n", "time, thousands of times faster than CESM. It was trained from a coupled CESM simulation of 1980-2010. You can find the [paper to version 1 of CAMulator here](https://arxiv.org/abs/2504.06007).\n", "\n", "CAMulator was trained using the Community Research Earth DIgital Twin (CREDIT) framework. [CREDIT Documentation](https://miles-credit.readthedocs.io/en/latest/).\n", "\n", "This tutorial runs CAMulator with a pretrained model and some mostly specific settings. However, there are also addition access points to running CAMulator:\n", "* [CAMulator on HuggingFace](https://huggingface.co/willychap/camulator)\n", "* [CAMulator with NVidia Earth2Studio](https://nvidia.github.io/earth2studio/main/modules/generated/models/px/CAMulator/)\n", "\n", "**In this practical you will:**\n", "1. See how long a *weather* forecast can be trusted, and why the *climate* can still be predicted after that;\n", "2. Check whether CAMulator gets Africa's climate right: where and when it rains through the year;\n", "3. Change the ocean (an El Niño, a warmer Gulf of Guinea, a warmer planet, ...) and see what changes in Africa, and\n", " learn to tell a real change from noise;\n", "4. Find some examples where the model might have learned a non-physical response.\n", "\n", "| part | question | time | computer time |\n", "|---|----------------------------------------------------------------------------|---|---|\n", "| 1 | How long can a weather forecast be trusted? | ~15 min | ~3 min |\n", "| 2 | Does CAMulator have Africa's climate? | ~30 min | ~10 min |\n", "| 3 | What if the ocean changes? (pick an experiment; try more if you have time) | ~40 min | ~5 min each |\n", "| 4 | Where we might see a non-physical response? | ~15 min | ~5 min |\n", "\n", "**How to use this notebook.** Run the cells from top to bottom (Shift+Enter). Cells that start a simulation say so and\n", "take a few minutes (see above). Cells marked **Settings** hold the few things a user can change.\n", "\n", "Any \"truth\" data shown is modeled CESM data (not observations), that CAMulator learned from." ] }, { "cell_type": "markdown", "id": "194b2230", "metadata": {}, "source": [ "## Setup\n", "Install CREDIT (the software that runs CAMulator), download the model and the data, and check the computer.\n", "\n", "The pip install command should only need to be run once, which installs the environment in a local directory (`camulator_env/`) and then downlaods the data and utility functions directly from huggin face. If the Kernel dies later on, or you want to come back to this at a later date, the follow cell should be skipped." ] }, { "cell_type": "code", "execution_count": null, "id": "0a893cfe", "metadata": { "ExecuteTime": { "end_time": "2026-10-19T17:01:36.745824Z", "start_time": "2026-10-19T17:01:30.592931Z" } }, "outputs": [], "source": [ "%%time\n", "%pip install \"miles-credit==2026.2.0\" huggingface_hub ipywidgets --target=./camulator_env\n", "import sys\n", "from pathlib import Path\n", "sys.path.append('./camulator_env')\n", "from huggingface_hub import snapshot_download\n", "from IPython.display import clear_output\n", "clear_output()\n", "\n", "data_path = snapshot_download(repo_id=\"charlie-becker/CAMulator_IC_and_forcing\", repo_type=\"dataset\",\n", " local_dir=\"./CAMulator_data\",\n", " allow_patterns=['*statics*', 'forcing_climo.nc', 'initial_conditions_1981.nc', 'aew_*',\n", " 'truth_aew_*', 'truth_monthly_*'])\n", "model_path = snapshot_download(repo_id=\"charlie-becker/CAMulator\", repo_type=\"model\", local_dir=\"./CAMulator_model\")\n", "print('Data :', data_path)\n", "print('Model:', model_path)" ] }, { "cell_type": "code", "execution_count": null, "id": "b08777a9", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "#### Where things are (change only if your folders are elsewhere) #################################################\n", "MODEL_DIR = Path.home() / 'notebooks/CAMulator_model'\n", "DATA_DIR = Path.home() / 'notebooks/CAMulator_data'\n", "WORK_DIR = Path.home() / 'notebooks/CAMulator_output'\n", "CHECKPOINT = 'checkpoint.pt00069.pt'\n", "####################################################################################################################\n", "\n", "sys.path.insert(0, str(MODEL_DIR)) # the helper code ships with the model, in CAMulator_model/utils/\n", "from utils import Tutorial, simple, diagnostics as dg\n", "\n", "tut = Tutorial(MODEL_DIR, DATA_DIR, WORK_DIR, CHECKPOINT, everyday_units=True)\n", "tut.check()" ] }, { "cell_type": "markdown", "id": "a49dc40e", "metadata": {}, "source": [ "## Part 1: How long can a weather forecast be trusted?\n", "\n", "When shifting from weather to climate, our sources of predictability change. A weather to climate question: **how\n", "long does a weather forecast stay useful, and what limits it?**\n", "\n", "**The case.** September 2013 over West Africa, a year CAMulator never saw in training, with a train of African easterly\n", "waves (the weather systems that bring much of the Sahel's rain).\n", "\n", "**Experiment** (two runs, 30 days each, with the real forcing of September 2013):\n", "- **the forecast:** CAMulator started from CESM's exact state on Sep 6;\n", "- **a copy:** the same, but with small random errors (about 1 K) added to the initial condition temperatures, similar to\n", " errors in real observations.\n", "\n", "So: **copy vs forecast** should demonstrate a **chaotic** system, how fast a tiny starting difference grows (the \"butterfly effect\"). **Forecast vs\n", "truth** shows *model error*: the start was perfect, so every difference is CAMulator not being exactly CESM.\n", "\n", "We'll measure the typical difference in surface pressure over the globe, in hPa (big\n", "pressure differences mean the highs and lows are in different places).\n", "\n", "**Some questions to consider:**\n", "1. Does the difference between the copy and the forecast grow forever, or level off? After how many days?\n", "2. Which grows faster: chaos (forecast1 vs forecast2) or model error (forecast vs truth)?" ] }, { "cell_type": "code", "execution_count": null, "id": "24b3b5b2", "metadata": {}, "outputs": [], "source": [ "# --- User Defined Settings ---------------------------------------------------------------------------------------------------------\n", "WEATHER_START = '2013-09-06' # starting states exist for 00Z on Sep 6-10, 2013\n", "WEATHER_LENGTH = '30d'\n", "STARTING_ERRORS = 1.0 # K: size of the copy's random starting errors (try 0.1)\n", "MAP_DAYS = [3, 20] # days after the start shown on the rain maps\n", "WEATHER_MAP = {'lat': (-10, 35), 'lon': (-45, 50)} # West Africa and the tropical Atlantic" ] }, { "cell_type": "code", "execution_count": null, "id": "fa66aba8", "metadata": {}, "outputs": [], "source": [ "%%time\n", "# Runs the forecast and the copy (about 3 minutes).\n", "starting_states = str(tut.data_dir / 'aew_initial_conditions_%Y.nc') # CESM's states, Sep 6-10 2013\n", "forcing_2013 = str(tut.data_dir / 'aew_forcing_%Y.nc') # the real forcing, Sep 6 - Oct 12 2013\n", "forecast_config = tut.member('part1/forecast', start=WEATHER_START, length=WEATHER_LENGTH, forcing=forcing_2013,\n", " ic_template=starting_states, outputs=['PS', 'PRECT'])\n", "copy_config = tut.member('part1/copy', start=WEATHER_START, length=WEATHER_LENGTH, forcing=forcing_2013,\n", " ic_template=starting_states, outputs=['PS', 'PRECT'], amplitude=STARTING_ERRORS, seed=1)\n", "tut.run([forecast_config, copy_config])\n", "clear_output()" ] }, { "cell_type": "code", "execution_count": null, "id": "bd6f50f1", "metadata": {}, "outputs": [], "source": [ "%%time\n", "truth = tut.truth('truth_aew_2013.nc') # what CESM did\n", "forecast = tut.load('part1/forecast')\n", "copy = tut.load('part1/copy')\n", "\n", "chaos = simple.drift(copy, forecast) # same model, slightly different start\n", "model_error = simple.drift(forecast, truth) # perfect start, but CAMulator is not CESM\n", "unrelated = dg.plateau(chaos) # where the copy levels off: as different as two random days\n", "\n", "fig, ax = plt.subplots(figsize=(12, 5), layout='constrained')\n", "ax.plot(chaos['lead'], chaos, lw=2.5, label=f'Copy vs forecast: chaos ({STARTING_ERRORS:g} K starting errors)')\n", "ax.plot(model_error['lead'], model_error, color='k', lw=2.5, label='Forecast vs truth: model error')\n", "ax.axhline(unrelated, color='0.5', ls='--', label=f'As different as two random days (~{unrelated:.0f} hPa)')\n", "ax.set_xlabel(f'Days since the start ({WEATHER_START})')\n", "ax.set_ylabel('Typical surface-pressure difference (hPa)')\n", "ax.set_title('How far apart do the runs drift?', fontsize=14, fontweight='bold')\n", "ax.legend()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "id": "0f361d87", "metadata": {}, "outputs": [], "source": [ "# The rain on two days: truth (CESM) and forecast side by side.\n", "start = dg.noleap(WEATHER_START)\n", "panels = {}\n", "for day in MAP_DAYS:\n", " one_day = slice(start + pd.Timedelta(days=day), start + pd.Timedelta(days=day, hours=18))\n", " panels[f'Truth, day {day}'] = truth['PRECT'].sel(time=one_day).mean('time')\n", " panels[f'Forecast, day {day}'] = forecast['PRECT'].sel(time=one_day).mean('time')\n", "simple.show_maps(panels, 'rain', region=WEATHER_MAP, title='Rain over West Africa: truth (left) vs forecast (right)')" ] }, { "cell_type": "code", "execution_count": null, "id": "dd7f1967", "metadata": {}, "outputs": [], "source": [ "half_way = unrelated / 2\n", "for name, curve in [('The copy (chaos)', chaos), ('The forecast (model error)', model_error)]:\n", " day = dg.time_to_reach(curve, half_way)\n", " when = f'after about {day:.0f} days' if np.isfinite(day) else f'not within {WEATHER_LENGTH}'\n", " print(f'{name} is half-way to \"two random days\" {when}.')" ] }, { "cell_type": "markdown", "id": "a1fb1462", "metadata": {}, "source": [ "### Thoughts\n", "- After how many days is the forecast no better than a random day? Was chaos or model error the bigger problem?\n", "- Look at the rain maps: on which day do the forecast's storms stop matching the truth's?\n", "- If *weather* can't be predicted beyond a few weeks, what could still be predicted for next season, or for a warmer\n", " world?\n", "\n", "**From weather to climate.** The weather is forgotten within weeks, but the *climate*, the average weather and its\n", "seasons, is set by things the atmosphere cannot forget: the sun, the ocean temperature and sea ice. The rest of this\n", "practical runs CAMulator for a full year and asks what *those* control." ] }, { "cell_type": "markdown", "id": "fa9796de", "metadata": {}, "source": [ "## Part 2: Does CAMulator have Africa's climate?\n", "\n", "**Experiment.** Run CAMulator for one year with the *average* forcing: each day's SST, sea ice and sunlight averaged over\n", "1980-2014, so no El Niño or other special year. Then run the same year **again**, changing nothing except tiny random\n", "errors (1 K) in the starting temperatures.\n", "\n", "**Why run it twice?** Part 1 showed that tiny starting errors grow until, after 2-3 weeks, the two runs have completely\n", "different weather. So the two runs are two equally likely years of weather under the *same* ocean, ice and sun. Nothing\n", "we changed caused the differences between them: they are chance, which we call **noise**. The noise tells us how much\n", "one year can differ from another just by luck.\n", "- In part 2 (the grey dashed line): a difference between CAMulator and CESM is likely only a real model error if *both*\n", " CAMulator simulations show it;\n", "- In part 3: an experiment also has its own random weather, so only a change clearly bigger than the noise was caused\n", " by the experiment.\n", "\n", "**What to expect:** global averages differ very little between the two runs (about 0.1 K), but one region in one month\n", "can differ a lot, especially rain in the rainy season (1-2 mm/day).\n", "\n", "Each run starts on Dec 1, 1981, and we use the twelve months Jan-Dec 1982 (December is a warm-up, so the run has\n", "forgotten its starting weather). We compare monthly averages with CESM's **average over 35 years** (1980-2014). That\n", "average has had its year-to-year luck averaged away, so the black CESM lines are smooth, while each CAMulator line is a\n", "single year and varies with its own weather.\n", "\n", "**Some Questions to Condier:**\n", "1. Where is Africa's rain band in June-August, and where in December-February? Will CAMulator put it in the same places\n", " as CESM?\n", "2. In which months does each region get its rain: the Sahel, East Africa, Southern Africa?" ] }, { "cell_type": "code", "execution_count": null, "id": "26830ac0", "metadata": { "ExecuteTime": { "end_time": "2026-10-19T17:15:42.830706Z", "start_time": "2026-10-19T17:15:42.818496Z" } }, "outputs": [], "source": [ "# --- User Defined Settings ---------------------------------------------------------------------------------------------------------\n", "TRUTH_FILE = 'truth_monthly_1980-2014.nc' # CESM's monthly means, averaged over 1980-2014\n", "VARIABLE = 'rain' # what to look at: 'rain', 'temperature', 'olr' or 'vapour' (see below)" ] }, { "cell_type": "markdown", "id": "bae0405d", "metadata": {}, "source": [ "The variables: **rain** (mm/day); **temperature** at 2 m (K); **OLR**, outgoing longwave radiation (W/m²), the heat the\n", "Earth radiates to space, which is *low* above deep, cold cloud tops, so low OLR marks the rain band as satellites see it;\n", "**vapour**, the column water vapour (mm)." ] }, { "cell_type": "code", "execution_count": null, "id": "52f325de", "metadata": {}, "outputs": [], "source": [ "%%time\n", "# Runs one year twice (about 5 minutes each). Runs that already exist are not repeated.\n", "simple.run_year(tut, 'control')\n", "simple.run_year(tut, 'control_repeat', starting_errors=1.0)\n", "clear_output()" ] }, { "cell_type": "markdown", "id": "bf346f6c", "metadata": {}, "source": [ "**Reading the maps.** Each row shows CAMulator, then CESM (same colour scale), then **CAMulator minus CESM** (its own\n", "colour scale, centred on zero: for rain, green = CAMulator wetter, brown = drier). Part of every difference is just one\n", "year of weather, so look for large, smooth patterns rather than single specks." ] }, { "cell_type": "code", "execution_count": null, "id": "d55dc266", "metadata": {}, "outputs": [], "source": [ "control = simple.load_year(tut, 'control') # monthly averages, Jan-Dec\n", "repeat = simple.load_year(tut, 'control_repeat')\n", "cesm = simple.load_truth(tut, TRUTH_FILE)\n", "cesm_name = cesm.attrs.get('label', 'CESM')\n", "\n", "simple.show_comparison(control, cesm, VARIABLE, ['year'], names=('CAMulator', cesm_name),\n", " title=f'{simple.label(VARIABLE)}: the average over the year')" ] }, { "cell_type": "code", "execution_count": null, "id": "39a94de4", "metadata": {}, "outputs": [], "source": [ "simple.show_comparison(control, cesm, VARIABLE, ['JJA', 'DJF'], names=('CAMulator', cesm_name),\n", " region=simple.AFRICA, boxes=simple.REGIONS,\n", " title=f'{simple.label(VARIABLE)} over Africa in two seasons (boxes: the regions below)')" ] }, { "cell_type": "code", "execution_count": null, "id": "a0bce414", "metadata": {}, "outputs": [], "source": [ "lines = {'CAMulator': control, 'CAMulator, run again': repeat, cesm_name: cesm}\n", "simple.show_months(lines, VARIABLE, grey=['CAMulator, run again'],\n", " title=f'{simple.label(VARIABLE)}, month by month, in three regions')\n", "simple.show_months(lines, 'temperature', regions={'Whole globe': None}, grey=['CAMulator, run again'],\n", " title='Global-mean temperature through the year: does CAMulator stay stable?')" ] }, { "cell_type": "code", "execution_count": null, "id": "be17c4b1", "metadata": {}, "outputs": [], "source": [ "print('Averages over the whole globe and year:')\n", "for variable in simple.VARIABLES:\n", " camulator = float(simple.global_mean(simple.season(control, 'year')[variable]))\n", " again = float(simple.global_mean(simple.season(repeat, 'year')[variable]))\n", " truth_val = float(simple.global_mean(simple.season(cesm, 'year')[variable]))\n", " print(f' {simple.label(variable):<26s} CAMulator {camulator:7.2f} (run again: {again:7.2f}) '\n", " f'{cesm_name}: {truth_val:7.2f}')" ] }, { "cell_type": "markdown", "id": "7f8a74af", "metadata": {}, "source": [ "### Thoughts\n", "- Does CAMulator put Africa's rain band in the right places in both seasons? Where is it too wet or too dry?\n", "- Do the three regions get their rain in the right months? How does CESM compare to CAMulator? (CAMulator learned\n", " from CESM, so it can only be as good as CESM.)\n", "- The grey dashed line is CAMulator run again, with the same forcing: the gap between the two CAMulator lines is the\n", " noise of one year of weather. Which differences from CESM appear in *both* CAMulator runs? Only those are real model\n", " errors; the rest could be chance. (Neither CAMulator line should be expected to match CESM month by month: one year\n", " of weather against a 35-year average.)" ] }, { "cell_type": "markdown", "id": "acaa880b", "metadata": {}, "source": [ "## Part 3: What if the ocean changes?\n", "\n", "The ocean contains the atmosphere's long memory: its temperature changes slowly and steers where the rain falls, months\n", "ahead. That is what makes seasonal forecasts possible.\n", "\n", "**Pick one experiment**, run it for a year, and compare it with\n", "the control. If you have time, come back and try another: each takes about 5 minutes, and finished runs are kept.\n", "\n", "| experiment | what changes | why it matters for Africa |\n", "|---|---|---|\n", "| `'el_nino'` | the eastern equatorial Pacific up to 3 K warmer, all year | El Niño shifts rain around the whole tropics, including East and southern Africa |\n", "| `'indian_ocean_dipole'` | western Indian Ocean 1.5 K warmer, eastern 1.5 K cooler | a positive dipole brought floods to East Africa in 1997 and 2019 |\n", "| `'warm_gulf_of_guinea'` | the Gulf of Guinea up to 2 K warmer (an \"Atlantic Niño\") | a warm Gulf of Guinea changes the West African monsoon |\n", "| `'warm_ocean_2K'` | the whole ocean 2 K warmer | a simple warmer world (try `'warm_ocean_4K'` too) |\n", "| `'no_arctic_sea_ice'` | all Arctic sea ice removed | can a change far away reach Africa? |\n", "\n", "Each experiment is compared with the control. But the control and the experiment have different weather, so some\n", "difference is just noise. **The noise map** (control *run again* minus control) shows how big that noise is.\n", "\n", "\n", "**Consider:** For your experiment, where will it get wetter, and where drier?" ] }, { "cell_type": "code", "execution_count": null, "id": "abf390d4", "metadata": {}, "outputs": [], "source": [ "# --- User Defined Settings ---------------------------------------------------------------------------------------------------------\n", "EXPERIMENT = 'el_nino' # 'el_nino', 'indian_ocean_dipole', 'warm_gulf_of_guinea', 'warm_ocean_2K', 'warm_ocean_4K'\n", " # or 'no_arctic_sea_ice'\n", "VARIABLE = 'rain' # 'rain', 'temperature', 'olr' or 'vapour'\n", "SEASON = 'SON' # the season for the Africa map: 'DJF', 'MAM', 'JJA', 'SON' or 'year'" ] }, { "cell_type": "code", "execution_count": null, "id": "7dd826a1", "metadata": {}, "outputs": [], "source": [ "simple.show_sst_change(tut, EXPERIMENT) # what this experiment does to the ocean (no simulation)" ] }, { "cell_type": "code", "execution_count": null, "id": "50baafaf", "metadata": {}, "outputs": [], "source": [ "%%time\n", "# Runs the experiment for one year (about 5 minutes; skipped if it already exists).\n", "simple.run_year(tut, EXPERIMENT, experiment=EXPERIMENT)\n", "clear_output()" ] }, { "cell_type": "code", "execution_count": null, "id": "52f68536", "metadata": {}, "outputs": [], "source": [ "control = simple.load_year(tut, 'control') # from part 2 (loaded again, in case the kernel restarted)\n", "repeat = simple.load_year(tut, 'control_repeat')\n", "experiment = simple.load_year(tut, EXPERIMENT)\n", "change = experiment - control # the experiment's effect, plus noise\n", "noise = repeat - control # noise only: same forcing, different weather\n", "\n", "simple.show_maps({'Experiment minus control': simple.season(change, 'year')[VARIABLE],\n", " 'Noise: control run again minus control': simple.season(noise, 'year')[VARIABLE]},\n", " VARIABLE, difference=True, title=f'{simple.EXPERIMENTS[EXPERIMENT][0]}: change over the year')" ] }, { "cell_type": "code", "execution_count": null, "id": "55e867d3", "metadata": {}, "outputs": [], "source": [ "simple.show_maps({f'Experiment minus control, {SEASON}': simple.season(change, SEASON)[VARIABLE],\n", " f'Noise, {SEASON}': simple.season(noise, SEASON)[VARIABLE]},\n", " VARIABLE, region=simple.AFRICA, difference=True, boxes=simple.REGIONS,\n", " title=f'{simple.EXPERIMENTS[EXPERIMENT][0]}: change over Africa, {SEASON}')" ] }, { "cell_type": "code", "execution_count": null, "id": "8437edf2", "metadata": {}, "outputs": [], "source": [ "simple.show_months({'Control': control, f'Experiment ({EXPERIMENT})': experiment, 'Control, run again (noise)': repeat},\n", " VARIABLE, grey=['Control, run again (noise)'],\n", " title=f'{simple.label(VARIABLE)}, month by month: experiment vs control')" ] }, { "cell_type": "code", "execution_count": null, "id": "7213bbf2", "metadata": {}, "outputs": [], "source": [ "print(f'{simple.EXPERIMENTS[EXPERIMENT][0]}')\n", "print(f'Change: experiment minus control, averaged over each region in {SEASON}.')\n", "print('Noise: the typical difference between the two control runs in that region (one month\\'s average).\\n')\n", "for name, region in {'Whole globe': None, **simple.REGIONS}.items():\n", " for variable in ('temperature', 'rain'):\n", " effect = float(dg.area_mean(simple.season(change, SEASON)[variable], region))\n", " noise_level = simple.noise_level(noise, variable, region)\n", " verdict = 'bigger than the noise' if abs(effect) > noise_level else 'could be noise'\n", " name_, units = simple.VARIABLES[variable][:2]\n", " print(f' {name:<20s} {name_:<16s} change {effect:+6.2f} {units:<7s} noise ±{noise_level:4.2f} {verdict}')" ] }, { "cell_type": "markdown", "id": "3562b16c", "metadata": {}, "source": [ "### Thoughts\n", "- Where is the change clearly bigger than the noise map? Is the change local, or far away?\n", "- Try changing `SEASON` and run the last few cells again: in which season is the effect on your region largest?\n", "- Does what you see match what you predicted, and what is known about this kind of event?" ] }, { "cell_type": "markdown", "id": "6754112f", "metadata": {}, "source": [ "## Part 4: Where might we see a non-physical response?\n", "\n", "When air warms it can hold more water vapor: about **7 % more per degree** per the **Clausius-Clapeyron** relation; a bit\n", "less in warm air, about 6 %/K at 27 °C, and more in cold air. A model of a warmer world should follow it *everywhere*, and in physical models the rainiest places, like the equatorial rain band, get even\n", "rainier (\"the wet gets wetter\").\n", "\n", "Does CAMulator follow the rule in a warmer world?\n", "\n", "**Experiment.** The control from part 2 and the `'warm_ocean_2K'` experiment from part 3 (it runs now if you haven't\n", "run it yet).\n", "\n", "**Considerations**\n", "1. Over the whole globe, does water vapor rise by about 7 % per K of warming?\n", "2. Does it follow the Clausius-Clapeyron curve across latitude, and does the equatorial rain band get wetter?" ] }, { "cell_type": "code", "execution_count": null, "id": "b4d7e4af", "metadata": { "ExecuteTime": { "end_time": "2026-10-19T17:29:34.267139Z", "start_time": "2026-10-19T17:29:34.250457Z" } }, "outputs": [], "source": [ "# --- User Defined Settings ---------------------------------------------------------------------------------------------------------\n", "WARM_RUN = 'warm_ocean_2K' # try 'warm_ocean_4K': further outside what CAMulator learned from" ] }, { "cell_type": "code", "execution_count": null, "id": "279bdd58", "metadata": {}, "outputs": [], "source": [ "%%time\n", "simple.run_year(tut, WARM_RUN, experiment=WARM_RUN) # skipped if you already ran it in part 3\n", "clear_output()" ] }, { "cell_type": "code", "execution_count": null, "id": "c73dd41b", "metadata": {}, "outputs": [], "source": [ "control = simple.load_year(tut, 'control')\n", "warm = simple.load_year(tut, WARM_RUN)\n", "warming = float(simple.global_mean(simple.season(warm - control, 'year')['temperature']))\n", "vapour = 100 * float(simple.global_mean(simple.season(warm, 'year')['vapour'])\n", " / simple.global_mean(simple.season(control, 'year')['vapour']) - 1)\n", "rain = 100 * float(simple.global_mean(simple.season(warm, 'year')['rain'])\n", " / simple.global_mean(simple.season(control, 'year')['rain']) - 1)\n", "print(f'The world warmed by {warming:.2f} K (2-m temperature).')\n", "print(f'Water vapour: {vapour:+.1f} %, that is {vapour / warming:+.1f} % per K (the rule: about 7 % per K)')\n", "print(f'Rain: {rain:+.1f} %, that is {rain / warming:+.1f} % per K (physical models: about 2-3 % per K)')" ] }, { "cell_type": "code", "execution_count": null, "id": "ad617e6c", "metadata": {}, "outputs": [], "source": [ "lat = simple.by_latitude(tut, control, warm)\n", "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 5), layout='constrained')\n", "ax1.plot(lat['vapour_rate']['latitude'], lat['vapour_rate'], 'k', lw=2.5, label='CAMulator')\n", "ax1.plot(lat['clausius_clapeyron']['latitude'], lat['clausius_clapeyron'], 'C1', lw=2.5,\n", " label='Clausius-Clapeyron (the physics)')\n", "ax1.set_title('Water vapour over the ocean: % increase per K of warming')\n", "ax1.set_xlabel('Latitude (negative = south)')\n", "ax1.set_ylabel('Increase in water vapour (% per K)')\n", "ax1.legend()\n", "\n", "ax2.plot(lat['rain_change']['latitude'], lat['rain_change'], 'k', lw=2.5, label='Change in rain (warm minus control)')\n", "ax2.axhline(0, color='0.5', lw=0.8)\n", "ax2.set_title('Rain: change at each latitude (all longitudes)')\n", "ax2.set_xlabel('Latitude (negative = south)')\n", "ax2.set_ylabel('Change in rain (mm/day)')\n", "twin = ax2.twinx()\n", "twin.plot(lat['control_rain']['latitude'], lat['control_rain'], color='C0', lw=1.5, alpha=0.7)\n", "twin.set_ylabel('Control rain (mm/day): the equatorial rain band is the peak', color='C0')\n", "ax2.legend(loc='lower left')\n", "for ax in (ax1, ax2):\n", " ax.set_xlim(-60, 60)\n", "fig.suptitle(f'A warmer ocean ({WARM_RUN}): does CAMulator follow the physics?', fontsize=14, fontweight='bold')\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "23f4f6b5", "metadata": {}, "source": [ "### Thoughts\n", "- How could an AI climate model be trained to do better here?\n", "\n", "## Wrap-up\n", "- **Weather** is forgotten within weeks : chaos and model error both limit a forecast.\n", "- **Climate** is set by the sun, the ocean and the ice (parts 2-3): CAMulator gets the broad patterns correct,\n", " with some errors in the details, and one year of weather is noisy: trust a difference only where it is clearly bigger\n", " than the noise.\n", "- **AI emulators** can be right on average and wrong in the details, especially outside their training data. Was the model trained on conditions like these?" ] }, { "cell_type": "code", "execution_count": null, "id": "d835dfb2d764238d", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" } }, "nbformat": 4, "nbformat_minor": 5 }