diff --git a/.gitignore b/.gitignore index 4773b69a..7399279a 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,4 @@ cython_debug/ # Ignore checkpoint files *.ckpt +checkpoints/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 18a8d603..59b640da 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,6 +5,12 @@ # Required version: 2 +# Set the version of Python +build: + os: ubuntu-22.04 + tools: + python: "3.8" + # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py diff --git a/CITATION.cff b/CITATION.cff index 753dc268..4a059fca 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,39 +2,27 @@ # Visit https://bit.ly/cffinit to generate yours today! cff-version: 1.2.0 -title: "ClimateLearn: Benchmarking Machine Learning for Data-driven Climate Science" +title: "ClimateLearn: Benchmarking Machine Learning for Weather and Climate Modeling" message: >- If you use this software, please cite it using the metadata from this file. type: software authors: - - given-names: Hritik - family-names: Bansal - email: hbansal@g.ucla.edu - affiliation: 'University of California, Los Angeles' - - given-names: Shashank - family-names: Goel - email: shashankgoel@g.ucla.edu + - given-names: Tung + family-names: Nguyen + email: tungnd@cs.ucla.edu affiliation: 'University of California, Los Angeles' - given-names: Jason family-names: Jewik - email: jason.jewik@cs.ucla.edu - affiliation: 'University of California, Los Angeles' - - given-names: Siddharth - family-names: Nandy - email: sidd.nandy@gmail.com + email: jason.jewik@ucla.edu affiliation: 'University of California, Los Angeles' - - given-names: Tung - family-names: Nguyen - email: tungnd@g.ucla.edu - affiliation: 'University of California, Los Angeles' - - given-names: Seongbin - family-names: Park - email: shannonsbpark@gmail.com + - given-names: Hritik + family-names: Bansal + email: hbansal@ucla.edu affiliation: 'University of California, Los Angeles' - - given-names: Jingchen - family-names: Tang - email: tangtang1228@ucla.edu + - given-names: Prakhar + family-names: Sharma + email: prakhar6sharma@gmail.com affiliation: 'University of California, Los Angeles' - given-names: Aditya family-names: Grover diff --git a/LICENSE b/LICENSE index 402a9480..cbd466f2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -# MIT License -# -#@title Copyright (c) 2021 CCAI Community Authors { display-mode: "form" } -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. \ No newline at end of file +MIT License + +Copyright (c) 2021-present Machine Intelligence Group at UCLA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 9f62cf9f..3c7c32ff 100644 --- a/README.md +++ b/README.md @@ -3,33 +3,26 @@ [![Documentation Status](https://readthedocs.org/projects/climatelearn/badge/?version=latest)](https://climatelearn.readthedocs.io/en/latest/?badge=latest) [![CI Build Status](https://github.com/aditya-grover/climate-learn/actions/workflows/ci.yaml/badge.svg)](https://github.com/aditya-grover/climate-learn/actions/workflows/ci.yaml) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![Google Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WiNEK1BHsiGzo_bT9Fcm8lea2H_ghNfa) +[![Google Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1LcecQLgLtwaHOwbvJAxw9UjCxfM0RMrX?usp=sharing) **ClimateLearn** is a Python library for accessing state-of-the-art climate data and machine learning models in a standardized, straightforward way. This library provides access to multiple datasets, a zoo of baseline approaches, and a suite of metrics and visualizations for large-scale benchmarking of statistical downscaling and temporal forecasting methods. For further context on our past motivation and future plans, check out our announcement [blog post](https://aditya-grover.github.io/blog/2023/climate-learn/). ## Usage -[**Python3**](https://www.python.org/) is required. +[**Python 3.8+**](https://www.python.org/) is required. The xESMF package has to be installed separately since one of its dependencies, ESMpy, is available only through Conda. ``` +conda install -c conda-forge xesmf pip install climate-learn ``` ### Quickstart -We have a series of tutorial Jupyter notebooks in the `notebooks` folder. We recommend reading them in the following order to see a typical ClimateLearn workflow. -1. Data Processing -2. Model Training & Evaluation -3. Visualization - -To run the notebooks, please upload them to [Google Colab](https://colab.research.google.com/). +We have a quickstart notebook in the `notebooks` folder titled `Quickstart.ipynb`. It is intended for use in Google Colab and can be launched by clicking the Google Colab badge above or this link: https://colab.research.google.com/drive/1LcecQLgLtwaHOwbvJAxw9UjCxfM0RMrX?usp=sharing. We also previewed some key features of ClimateLearn at a spotlight tutorial in the "Tackling Climate Change with Machine Learning" Workshop at the Neural Information Processing Systems 2022 Conference. The slides and recorded talk can be found on [Climate Change AI's website](https://www.climatechange.ai/papers/neurips2022/114). ### Documentation Find us on [ReadTheDocs](https://climatelearn.readthedocs.io/). -### Integrations -- [Weights & Biases](https://wandb.ai/site) - ## About Us ClimateLearn is managed by the Machine Intelligence Group at UCLA, headed by [Professor Aditya Grover](https://aditya-grover.github.io). diff --git a/docs/source/conf.py b/docs/source/conf.py index 6dc14c48..b6aa76f1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,20 +10,17 @@ # -- Project information project = "ClimateLearn" -copyright = "2022; Bansal, Goel, Jewik, Nandy, Nguyen, Park, Tang, Grover" -author = """ - Hritik Bansal, - Shashank Goel, - Jason Jewik, - Siddharth Nandy, +copyright = "2023; Nguyen, Jewik, Bansal, Sharma, Grover" +author = """ Tung Nguyen, - Seongbin Park, - Jingchen Tang, + Jason Jewik, + Hritik Bansal, + Prakhar Sharma, Aditya Grover """ -release = "0.1" -version = "0.1.0" +release = "1.0.0" +version = "1.0.0" # -- General configuration diff --git a/docs/source/index.rst b/docs/source/index.rst index 3de3bd4f..adf8e6f6 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,36 +12,56 @@ statistical downscaling and temporal forecasting methods. .. note:: - This project is under active development. - -About Us --------- -ClimateLearn is managed by the Machine Intelligence Group at UCLA, headed by -`Professor Aditya Grover `_. + This project is under active development. The API might undergo extensive + changes in the near future. Getting Started --------------- -Please see the `quickstart section `_ of our GitHub repository. +`Python 3.8+ `_ is required. The xESMF package has +to be installed separately since one of its dependencies, ESMpy, is available +only through Conda. + +.. code-block:: shell + + conda install -c conda-forge xesmf + pip install climate-learn + +We have a quickstart notebook in the ``notebooks`` folder titled +``Quickstart.ipynb`` that walks through an example usage of ClimateLearn for +weather forecasting from downloading the data through visualizing the +predictions of a trained model. It is intended for use in Google Colab and can +be launched by clicking +`this link `_. .. toctree:: :caption: User Guide :maxdepth: 2 - user-guide/datasets + user-guide/tasks_and_datasets user-guide/models user-guide/metrics user-guide/visualizations -.. toctree:: - :caption: API Reference - :maxdepth: 1 - :glob: - - reference/* - .. toctree:: :caption: Development Guide :maxdepth: 1 development-guide/for-developers - development-guide/for-maintainers \ No newline at end of file + development-guide/for-maintainers + +Why did we build ClimateLearn? +------------------------------ + +In recent years, there has been a growing interest in the application of +ML-based methods for weather and climate modeling. While there are some +leaderboard benchmarks, such as WeatherBench, ClimateBench, and FloodNet, that +propose datasets and baselines for specific tasks in climate science, a +holistic software ecosystem that encompasses the entire data, modeling, and +evaluation pipeline across several tasks is lacking. Hence, we built +ClimateLearn to standardize datasets, model implementations, and evaluation +protocols for rigorous and reproducible data-driven climate science. + +About Us +-------- +ClimateLearn is built and maintained by the Machine Intelligence Group at UCLA, +headed by `Professor Aditya Grover `_. \ No newline at end of file diff --git a/docs/source/reference/data.rst b/docs/source/reference/data.rst deleted file mode 100644 index a081a973..00000000 --- a/docs/source/reference/data.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. role:: python(code) - :language: python - :class: highlight - -climate_learn.data -================== - -data.download -------------- -.. autofunction:: climate_learn.data.download.download - -.. autofunction:: climate_learn.data.download._download_copernicus - -.. autofunction:: climate_learn.data.download._download_esgf - -.. autofunction:: climate_learn.data.download._download_weatherbench - -data.module ------------ -.. autofunction:: climate_learn.data.module.collate_fn - -.. autoclass:: climate_learn.data.module.DataModule - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/reference/models.rst b/docs/source/reference/models.rst deleted file mode 100644 index 1fa69937..00000000 --- a/docs/source/reference/models.rst +++ /dev/null @@ -1,7 +0,0 @@ -climate_learn.models -==================== - -Coming soon! - -.. .. autoclass:: climate_learn.models.modules.linear.LinearLitModule -.. :members: forward \ No newline at end of file diff --git a/docs/source/reference/training.rst b/docs/source/reference/training.rst deleted file mode 100644 index 275bc77d..00000000 --- a/docs/source/reference/training.rst +++ /dev/null @@ -1,4 +0,0 @@ -climate_learn.training -====================== - -Coming soon! \ No newline at end of file diff --git a/docs/source/reference/utils.rst b/docs/source/reference/utils.rst deleted file mode 100644 index be783a9f..00000000 --- a/docs/source/reference/utils.rst +++ /dev/null @@ -1,24 +0,0 @@ -climate_learn.utils -=================== - -utils.data ----------- -.. autofunction:: climate_learn.utils.data.load_dataset - -.. autofunction:: climate_learn.utils.data.view - -utils.datetime --------------- -.. autodata:: climate_learn.utils.datetime.Year - -.. autoclass:: climate_learn.utils.datetime.Days - :members: - -.. autoclass:: climate_learn.utils.datetime.Hours - :members: - -utils.visualize ---------------- -.. autofunction:: climate_learn.utils.visualize.visualize - -.. autofunction:: climate_learn.utils.visualize.visualize_mean_bias diff --git a/docs/source/user-guide/datasets.rst b/docs/source/user-guide/datasets.rst deleted file mode 100644 index d6ef708f..00000000 --- a/docs/source/user-guide/datasets.rst +++ /dev/null @@ -1,54 +0,0 @@ -Datasets -======== - -The package currently supports two climate datasets, ERA5 and CMIP6. The datasets can be downloaded through the package via multiple sources. - - -ERA5 ------------------- -The ERA5 dataset provides hourly estimates of a large number of atmospheric, land and oceanic climate variables. [#]_ It can be downloaded through Copernicus or via the WeatherBench data repository [#]_. - -Copernicus -^^^^^^^^^^^^^^ - -.. code-block:: python - - download(source = "copernicus", variable = "2m_temperature", dataset = "era5", year = 1979, api_key = api_key) - -Though it depends on the variable, the average download time for a single variable via Copernicus is around 25 minutes. The API key can be geenrated on the `cds website `_. - -Weatherbench -^^^^^^^^^^^^^^^ - -.. code-block:: python - - download(root = path, source = "weatherbench", variable = "2m_temperature", dataset = "era5", resolution = "5.625") - -The authors of the weatherbench paper have made the ERA5 dataset readily available in three resolutions: 1.4062, 2.8125, and 5.625 degrees. The average download time for a single variable is around 5 minutes. - - -CMIP6 ------------------- -The CMIP6 dataset contains simulation data from a variety of climate models. - -Weatherbench -^^^^^^^^^^^^^^ - -.. code-block:: python - - download(root = path, source = "weatherbench", variable = "2m_temperature", dataset = "cmip6", resolution = "5.625") - -The authors of the WeatherBench paper have made a regridded historical climate run in CMIP6 readily available in the same data repository as the ERA5 data. The average download time for a single variable is around 5 minutes. The available resolutions are 1.4062 and 5.625 degrees. - -ESGF -^^^^^^^^^^^^^^ - -.. code-block:: python - - download(root = path, dataset = "cmip6", variable = "temperature", resolution = "5.625", institutionID="MPI-M", sourceID="MPI-ESM1-2-HR", exprID="historical") - -The CMIP6 data is also available through the Earth System Grid Federation (ESGF)'s own servers. It takes several hours to download a single variable, but as long as the server is available, any variable of any simulation can be downloaded. - - -.. [#] `This part is quoted from ECMWF `_ -.. [#] `link to data repository `_ \ No newline at end of file diff --git a/docs/source/user-guide/images/animated_input.png b/docs/source/user-guide/images/animated_input.png new file mode 100644 index 00000000..972377cb Binary files /dev/null and b/docs/source/user-guide/images/animated_input.png differ diff --git a/docs/source/user-guide/images/animated_input.png:Zone.Identifier b/docs/source/user-guide/images/animated_input.png:Zone.Identifier new file mode 100644 index 00000000..053d1127 --- /dev/null +++ b/docs/source/user-guide/images/animated_input.png:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=about:internet diff --git a/docs/source/user-guide/images/bias_at_index.png b/docs/source/user-guide/images/bias_at_index.png new file mode 100644 index 00000000..37a05c35 Binary files /dev/null and b/docs/source/user-guide/images/bias_at_index.png differ diff --git a/docs/source/user-guide/images/bias_at_index.png:Zone.Identifier b/docs/source/user-guide/images/bias_at_index.png:Zone.Identifier new file mode 100644 index 00000000..053d1127 --- /dev/null +++ b/docs/source/user-guide/images/bias_at_index.png:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=about:internet diff --git a/docs/source/user-guide/images/gt_at_index.png b/docs/source/user-guide/images/gt_at_index.png new file mode 100644 index 00000000..57dcbfdb Binary files /dev/null and b/docs/source/user-guide/images/gt_at_index.png differ diff --git a/docs/source/user-guide/images/gt_at_index.png:Zone.Identifier b/docs/source/user-guide/images/gt_at_index.png:Zone.Identifier new file mode 100644 index 00000000..053d1127 --- /dev/null +++ b/docs/source/user-guide/images/gt_at_index.png:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=about:internet diff --git a/docs/source/user-guide/images/mean_bias.png b/docs/source/user-guide/images/mean_bias.png new file mode 100644 index 00000000..0f678640 Binary files /dev/null and b/docs/source/user-guide/images/mean_bias.png differ diff --git a/docs/source/user-guide/images/mean_bias.png:Zone.Identifier b/docs/source/user-guide/images/mean_bias.png:Zone.Identifier new file mode 100644 index 00000000..053d1127 --- /dev/null +++ b/docs/source/user-guide/images/mean_bias.png:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=about:internet diff --git a/docs/source/user-guide/images/prediction_at_index.png b/docs/source/user-guide/images/prediction_at_index.png new file mode 100644 index 00000000..7821d66b Binary files /dev/null and b/docs/source/user-guide/images/prediction_at_index.png differ diff --git a/docs/source/user-guide/images/visualize.png b/docs/source/user-guide/images/visualize.png deleted file mode 100644 index 90236d88..00000000 Binary files a/docs/source/user-guide/images/visualize.png and /dev/null differ diff --git a/docs/source/user-guide/images/visualize_at_index.png:Zone.Identifier b/docs/source/user-guide/images/visualize_at_index.png:Zone.Identifier new file mode 100644 index 00000000..053d1127 --- /dev/null +++ b/docs/source/user-guide/images/visualize_at_index.png:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=about:internet diff --git a/docs/source/user-guide/images/visualize_mean_bias.png b/docs/source/user-guide/images/visualize_mean_bias.png deleted file mode 100644 index 64fb2c25..00000000 Binary files a/docs/source/user-guide/images/visualize_mean_bias.png and /dev/null differ diff --git a/docs/source/user-guide/metrics.rst b/docs/source/user-guide/metrics.rst index dc0ae6d0..255bd6a8 100644 --- a/docs/source/user-guide/metrics.rst +++ b/docs/source/user-guide/metrics.rst @@ -1,118 +1,27 @@ Metrics ======= -Currently, there are 12 metrics supported in ClimateLearn, including commonly used metrics like :ref:`Mean Squared Error` (MSE), :ref:`Root Mean Squared Error` (RMSE), and :ref:`Pearson Correlation Coefficient`, for forecasting and downscaling tasks. Part of these metrics are applied as loss functions in the training, validation, and test steps according to specific types of method used. The rest are used as evaluation metrics in the test steps. - - -Mean Squared Error ------------------- -.. code-block:: python - - def mse(pred, y, vars, mask=None, transform_pred=False, transform=None, lat=None, log_steps=None, log_days=None, log_day=None, clim=None) - -The `mse `_ function computes `mean square error `_, a risk metric corresponding to the expected value of the squared error or loss. This is used as default training loss function in downscaling task. - - -Root Mean Squared Error ------------------------ -.. code-block:: python - - def rmse(pred, y, vars, mask=None, transform_pred=False, transform=None, lat=None, log_steps=None, log_days=None, log_day=None, clim=None) - -The `rmse `_ function computes `root-mean-square error `_, the square root of the second sample moment of the differences between predicted values and observed values or the quadratic mean of these differences. - -.. math:: \sqrt{\frac{1}{N_{lat}N_{lon}} \sum_{N_{lat}}\sum_{N_{lon}}(prediction - truth)^2 } - -The size of ``pred`` and ``y`` being ``[N, C, H, W]``, and the mean is computed over sampling points of the grid of size ``H * W``, with ``N`` being the size of ensemble, ``C`` being the number of channels. This is used in the validation and test steps's upsampling in the downscaling task. - - -Latitude-Weighted RMSE ----------------------- - -.. code-block:: python - - def lat_weighted_rmse(pred, y, vars, mask=None, transform_pred=True, transform=None, lat=None, log_steps=None, log_days=None, log_day=None, clim=None) - -The `lat_weighted_rmse `_ function is similar to the regular :ref:`RMSE`, but is given a weight for every pixel in the grid map according to the latitude value on the earth. - -.. math:: \sqrt{\frac{1}{N_{lat}N_{lon}} \sum^{N_{lat}}_j \sum^{N_{lon}}_k L(j)(prediction - truth)^2 } - -Pixels near the equator are given more weight because the earth is curved leading to less area towards the pole. - -.. math:: L(j) = \frac{cos(lat(j))}{ \frac{1}{N_{lat}} \sum_j^{N_{lat}} cos(lat(j))} - -This metric is being used to evaluate all the baseline methods, and the validation/test steps of deterministic methods. - - -Anomaly Correlation Coefficient -------------------------------- - -.. code-block:: python - - def lat_weighted_acc(pred, y, vars, mask=None, transform_pred=True, transform=None, lat=None, log_steps=None, log_days=None, log_day=None, clim=None) - -The `lat_weighted_acc `_ is an weighted version of Anomaly Correlation Coefficient (ACC). - -.. math:: ACC = \frac{\overline{(pred - clim)(truth - clim)}}{\sqrt{\overline{(pred - clim)^2} \space \overline{(truth - clim)^2}}} - -ACC is one of the most widely used measures in the verification of spatial fields. It is the spatial correlation between a forecast anomaly relative to climatology, and a verifying analysis anomaly relative to climatology. ACC represents a measure of how well the forecast anomalies have represented the observed anomalies and shows how well the predicted values from a forecast model "fit" with the real-life data [#]_. This metric is used in the test step of forecasting task for deterministic method. - - -Pearson Correlation Coefficient -------------------------------- - -.. code-block:: python - - def pearson(pred, y, vars, mask=None, transform_pred=False, transform=None, lat=None, log_steps=None, log_days=None, log_day=None, clim=None) - -The `pearson `_ (PCC) is a measure of linear correlation between two sets of data. It is the ratio between the covariance of two variables and the product of their standard deviations. It is calculated using `scipy.stats.pearsonr(x, y) `_ with the ``pred`` and ``truth`` as input. - -.. math:: PCC = \frac{\sum (x - m_x)(y - m_y)}{\sqrt{\sum (x - m_x)^2 \sum (y - m_y)^2}} - -This metric is used in the validation/test steps for downscaling task. - -Mean Bias ---------- - -.. code-block:: python - - def mean_bias(pred, y, vars, mask=None, transform_pred=False, transform=None, lat=None, log_steps=None, log_days=None, log_day=None, clim=None) - -The `mean_bias `_ is a function that calculates the absolute difference between spatial mean of predictions and observations. - -.. math:: \sqrt{\frac{1}{N_{lat}N_{lon}} \sum_{N_{lat}}\sum_{N_{lon}}|prediction - truth| } - -This metric is used in the :doc:`visualization ` to give a direct idea of the difference between prediction and truth value. It is also used in the validation/test steps for downscaling task. - -Latitude-Weighted Spread-Skill Ratio ------------------------------------- - -.. code-block:: python - - def lat_weighted_spread_skill_ratio(pred, y, vars, mask=None, transform_pred=True, transform=None, lat=None, log_steps=None, log_days=None, log_day=None, clim=None) - -`lat_weighted_spread_skill_ratio `_ is a latitude-weighted version of spread-skill ratio, which is a first-order measure of the reliability of the ensemble. - -.. math:: \sqrt{\frac{1}{N_{lat}N_{lon}} \sum^{N_{lat}}_j \sum^{N_{lon}}_k L(j)var(f_{j,k}) } - -where :math:`var(f_{j,k})` is the variance in the ensemble dimension. -This metric is being used as one of the validation loss function for the parametric prediction of probabilistic neural networks. - - -Latitude-Weighted CRPS ----------------------- - -.. code-block:: python - - def crps_gaussian(pred, y, vars, mask=None, transform_pred=None, transform=None, lat=None, log_steps=None, log_days=None, log_day=None, clim=None) - -The `crps_gaussian `_ calculates the latitude-weighted Continuous Ranked Probability Score, in order to evaluate the calibration and sharpness of the ensemble. CRPS is a measure of how good forecasts are in matching observed outcomes [#]_. Where: - -- CRPS = 0 the forecast is wholly accurate; -- CRPS = 1 the forecast is wholly inaccurate. - -This metric is being used as one of the train/validation loss function for the probabilistic method. - - -.. [#] `This part is quoted from ECMWF `_ -.. [#] `This part is quoted from ECMWF appendices `_ \ No newline at end of file +ClimateLearn provides the following metrics for deterministic predictions. + +- Mean squared error (MSE) +- Mean squared error skill score (MSESS) +- Mean absolute error (MAE) +- Root mean squared error (RMSE) +- Anomaly correlation coefficient (ACC) +- Pearson's correlation coefficient (Pearson) +- Mean bias +- Normalized root mean squared error (NRMSEs) +- Normalized root mean squared error in global mean (NRMSEg) + +For probabilistic forecasts, the library provides the following metrics. + +- Gaussian continuous ranked probability score (Gaussian CRPS) +- Gaussian spread +- Gaussian spread-skill ratio (Gaussian SSR) + +We refer to the following sources for the definitions and motivations for these +metrics: + +- https://geo.libretexts.org/Bookshelves/Meteorology_and_Climate_Science/Practical_Meteorology_(Stull)/20%3A_Numerical_Weather_Prediction_(NWP)/20.7%3A_Forecast_Quality_and_Verfication +- https://repository.library.noaa.gov/view/noaa/48746 +- https://arxiv.org/abs/2205.00865 \ No newline at end of file diff --git a/docs/source/user-guide/models.rst b/docs/source/user-guide/models.rst index 3563884e..0460385b 100644 --- a/docs/source/user-guide/models.rst +++ b/docs/source/user-guide/models.rst @@ -1,179 +1,105 @@ Models ====== -ClimateLearn's model modules are configurable based on the problem -(forecasting, using the ``ForecastLitModule`` module, and downscaling, -using the ``DownscaleLitModule`` module) and the desired model archiecture. -Currently, three deep neural network architectures are supported: -#. Convolutional neural networks: the CNN is a widely used architecture for visual recognition tasks. A constrained version of the standard neural network, CNNs capitalize on knowledge of the input's structure as an image. ClimateLearn suports two popular variants of CNNs: - - a. ResNet: ResNets are a popular variant of CNNs [#]_ that have been used to achieve weather forecasting for variables such as temperature and geopotential [#]_. - - b. U-Net: U-Nets are a CNN variant that entails both downsampling and upsampling convolutions. Their development and popularity in the biomedical space [#]_ paved the way for ClimateLearn's implementation, allowing users to benchmark U-Nets for climate modeling tasks. - -#. Vision transformers: ViTs are the latest contemporary to CNNs for visual recognition [#]_. The utility of ViTs for representing climate variables is largely under-explored, but has been used for short-range temperature forecasting [#]_. - -.. [#] `Deep Residual Learning for Image Recognition `_ -.. [#] `Data-driven medium-range weather prediction with a Resnet pretrained on climate simulations: A new model for WeatherBench `_ -.. [#] `U-Net: Convolutional Networks for Biomedical Image Segmentation `_ -.. [#] `An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale `_ -.. [#] `TENT: Tensorized Encoder Transformer for Temperature Forecasting `_ - - -Initialization --------------- - -Models are initialized by the ``load_model`` function in the -``climate_learn.models`` module, which accepts an input for the desired -architecture (``"vit"``, ``"resnet"``, or ``"unet"``) and the desired -task (``"forecasting"`` or ``"downscaling"``). The function also accepts -optional keyword arguments for the model and task optimizer specifically. -Below is an example of initializing a ViT model for temporal forecasting. +ClimateLearn supports a variety of baselines and deep learning models, as shown +in the table below. + ++---------------+--------------------+----------------+----------------------------------+ +| Type | Model | Relevant Tasks | Notes | ++===============+====================+================+==================================+ +| Baseline | Climatology | Forecasting | | +| +--------------------+----------------+----------------------------------+ +| | Persistence | Forecasting | | +| +--------------------+----------------+----------------------------------+ +| | Interpolation | Downscaling | Nearest, bilinear are available. | +| +--------------------+----------------+----------------------------------+ +| | Linear regression | | Forecasting | | Not practical for hi-res data, | +| | | | Downscaling | | or data with many variables. | +| | | | Projection | | ++---------------+--------------------+----------------+----------------------------------+ +| Deep learning | ResNet | | Forecasting | | +| | | | Downscaling | | +| | | | Projection | | +| +--------------------+----------------+----------------------------------+ +| | U-net | | Forecasting | | +| | | | Downscaling | | +| | | | Projection | | +| +--------------------+----------------+----------------------------------+ +| | Vision transformer | | Forecasting | | +| | | | Downscaling | | +| | | | Projection | | ++---------------+--------------------+----------------+----------------------------------+ + +Loading a Model +--------------- + +In order to construct a model, ClimateLearn requires an instantiated data +module to determine the number of input and output channels. Suppose this +data module is called ``dm``. Then, one can load baselines by name as such: .. code-block:: python - :linenos: - - from climate_learn.models import load_model - model_kwargs = { - "n_blocks": 4 - } - optim_kwargs = { - "lr": 1e-4, - } - model_module = load_model( - name="vit", - task="forecasting", - model_kwargs=None, - optim_kwargs=optim_kwargs - ) - -Training --------- -The ``climate_learn.training`` module provides a ``Trainer`` class for -fitting and testing models. The trainer is initialized with parameters -such as the seed, the accelerator, and the maximimum number of epochs. + import climate_learn as cl -The trainer has two functions, ``fit`` and ``test``, used for fitting -and testing the argument model on the argument data module. Each -function assumes ``model_module`` and ``data_module`` are initialized -for the same task (both forecasting or both downscaling). See -:doc:`Metrics ` for more information on the metrics -on which the model is tested in ``Trainer.test()``. + climatology = cl.load_forecasting_module( + data_module=dm, + architecture="climatology" + ) + interpolation = cl.load_downscaling_module( + data_module=dm, + architecture="nearest-interpolation" + ) -Below is an example of fitting and testing a model with a given data module. +We also currently provide one deep learning architecture, with its associated +optimizer and learning rate scheduler, by +`Rasp & Theurey (2020) `_. .. code-block:: python - :linenos: - from climate_learn.training import Trainer + import climate_learn as cl - trainer = Trainer( - seed = 0, - accelerator = "gpu", - precision = 16, - max_epochs = 5, + resnet = cl.load_forecasting_module( + data_module=dm, + architecture="rasp-theurey-2020" ) - trainer.fit(model_module, data_module) - - trainer.test(model_module, data_module) +.. note:: -Example -------- + Our goal for the future is to implement as many architectures from the + literature as we can find for fair comparison and benchmarking. If you + would like to contribute, please open an + `issue on our GitHub repository `_. -The following can be run in Google Colab. +ClimateLearn also supports customization of the provided architectures in two +ways. First, one can specify the customization in the loading function itself. -.. nbinput:: ipython3 - :execution-count: 1 - - %%capture - !pip install git+https://github.com/aditya-grover/climate-learn.git - -.. nbinput:: ipython3 - :execution-count: 2 - - # Download WeatherBench 2m_temperature data to Google Drive - from google.colab import drive - from climate_learn.data import download +.. code-block:: python - drive.mount("/content/drive") - download( - root="/content/drive/MyDrive/Climate/.climate_tutorial", - source="weatherbench", - variable="2m_temperature", - dataset="era5", - resolution="5.625" - ) + import climate_learn as cl -.. nbinput:: ipython3 - :execution-count: 3 - - # Load data module for forecasting task - from climate_learn.utils.datetime import Year, Days, Hours - from climate_learn.data import DataModule - - data_module = DataModule( - dataset = "ERA5", - task = "forecasting", - root_dir = "/content/drive/MyDrive/Climate/.climate_tutorial/data/weatherbench/era5/5.625/", - in_vars = ["2m_temperature"], - out_vars = ["2m_temperature"], - train_start_year = Year(1979), - val_start_year = Year(2015), - test_start_year = Year(2017), - end_year = Year(2018), - pred_range = Days(3), - subsample = Hours(6), - batch_size = 128, - num_workers = 1 + model = cl.load_forecasting_module( + data_module=dm, + model="resnet", + model_kwargs={"n_blocks": 4, "history": 5}, + optim="adamw", + optim_kwargs={"lr": 5e-4}, + sched="linear-warmup-cosine-annealing", + sched_kwargs={"warmup_epochs": 5, "max_epochs": 50} ) -.. nbinput:: ipython3 - :execution-count: 4 - - # Load U-Net model - from climate_learn.models import load_model - - model_kwargs = { - "in_channels": len(data_module.hparams.in_vars), - "out_channels": len(data_module.hparams.out_vars), - "n_blocks": 4 - } - - optim_kwargs = { - "lr": 1e-4, - "weight_decay": 1e-5, - "warmup_epochs": 1, - "max_epochs": 5, - } - - model_module = load_model( - name="unet", - task="forecasting", - model_kwargs=model_kwargs, - optim_kwargs=optim_kwargs - ) +Second, one can insantiate the model, optimizer, and learning rate scheduler +directly. Note that one can mix directly instantiated and ClimateLearn-provided +options. -.. nbinput:: ipython3 - :execution-count: 5 - - from climate_learn.training import Trainer - - # Initialize model trainer - trainer = Trainer( - seed = 0, - accelerator = "gpu", - precision = 16, - max_epochs = 5, - ) - -.. nbinput:: ipython3 - :execution-count: 6 +.. code-block:: python - trainer.fit(model_module, data_module) + import climate_learn as cl + from torch.optim import SGD + from torch.optim.lr_scheduler import ReduceLROnPlateau -.. nbinput:: ipython3 - :execution-count: 7 - - trainer.test(model_module, data_module) + model = cl.load_forecasting_module( + data_module=dm, + model=cl.models.hub.ResNet(...), + optim=SGD(...), + sched=ReduceLROnPlateau(...) + ) \ No newline at end of file diff --git a/docs/source/user-guide/tasks_and_datasets.rst b/docs/source/user-guide/tasks_and_datasets.rst new file mode 100644 index 00000000..e4aa0fec --- /dev/null +++ b/docs/source/user-guide/tasks_and_datasets.rst @@ -0,0 +1,423 @@ +Tasks and Datasets +================== + +ClimateLearn supports multiple tasks and datasets for weather and climate +modeling. First, we introduce the tasks to motivate the choice of datasets. +Then, we describe the datasets that are available through ClimateLearn and +show code examples of how to download the data. Finally, we show how to process +the data with ClimateLearn and prepare them for use with your machine learning +models. + +Tasks +----- + +**Weather forecasting** is the task of predicting the weather at a future time +step :math:`t + \Delta t` given the weather conditions at the current step +:math:`t` and optionally steps preceding :math:`t`. A ML model receives an +input of shape :math:`C\times H\times W` and predicts an output of shape +:math:`C'\times H\times W`. :math:`C` and :math:`C'` denote the number of input +and output channels, respectively, which contain variables such as geopotential, +temperature, and humidity. :math:`H` and :math:`W` denote the spatial coverage +and resolution of the data, which depend on the region studied and how densely +we grid it. + +**Downscaling** Due to their high computational cost, existing climate models +often use large grid cells, leading to low-resolution predictions. While useful +for understanding large-scale climate trends, these do not provide sufficient +detail to analyze local phenomena and design regional policies. The process of +correcting biases in climate model outputs and mapping them to higher +resolutions is known as downscaling. ML models for downscaling are trained to +map an input of shape :math:`C\times H\times W` to a higher resolution output +:math:`C'\times H'\times W'`, where :math:`H'\gt H` and :math:`W'\gt W`. + +**Climate projection** aims to obtain long-term predictions of the climate under +different forcings, *e.g.*, greenhouse gas emissions. For instance, one might +want to predict the annual mean distributions of variables such as surface +temperature and precipitation given levels of atmospheric carbon dioxide and +methane. + +ERA5 Dataset +------------ + +**ERA5** is a reanalysis dataset maintained by the European Center for +Medium-Range Weather Forecasting (ECMWF). In its raw format, ERA5 contains +hourly data from 1979 to the current time on a grid with cells of width and +height :math:`0.25^\circ` of the Earth, with different climate variables at +37 different pressure levels plus the planet's surface. This corresponds to +nearly 400,000 data samples, each a matrix of shape :math:`721\times 1440`. +Since this is too big for most deep learning models, ClimateLearn supports +downloading a smaller, pre-processed version of ERA5 data from WeatherBench. + +.. _weatherbench-era5-download: + +Downloading from WeatherBench +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +ClimateLearn provides ERA5 data through two sources. One source is +`WeatherBench `_. + +.. code-block:: python + + import climate_learn as cl + + root_directory = "/home/user/climate-learn" + variable = "2m_temperature" + cl.data.download_weatherbench( + dst=f"{root_directory}/{variable}", + dataset="era5", + variable=variable, + resolution=5.625 # optional, default is 5.625 + ) + +Note that ERA5 has both single-level and pressure-level variables. WeatherBench +provides temperature at 850 hPa and geopotential at 500 hPa separate from +temperature at all pressure levels and geopotential at all pressure levels. We +recommend you to download these variables as such: + +.. code-block:: python + + import climate_learn as cl + + root_directory = "/home/user/climate-learn" + cl.data.download_weatherbench( + f"{root_directory}/temperature", + dataset="era5", + variable="temperature_850", + resolution=5.625 # optional, default is 5.625 + ) + cl.data.download_weatherbench( + f"{root_directory}/geopotential", + dataset="era5", + variable="geopotential_500", + resolution=5.625 # optional, default is 5.625 + ) + +.. _weatherbench-era5-reference: + +WeatherBench Quick Reference +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following variables can be downloaded from WeatherBench at 1.40625, 2.8125, +and 5.625 degree resolutions. The temporal coverage is 1978 to 2018 at hourly +intervals, and the pressure levels are 50, 250, 500, 600, 700, 850, and 925 hPa. + ++-----------------+----------------------------------+----------------------------------+ +| Type | Variable | Notes | ++=================+==================================+==================================+ +| Single-level | ``2m_temperature`` | | +| +----------------------------------+----------------------------------+ +| | ``10m_u_component_of_wind`` | | +| +----------------------------------+----------------------------------+ +| | ``10m_v_component_of_wind`` | | +| +----------------------------------+----------------------------------+ +| | ``geopotential_500`` | Extracted from ``geopotential``. | +| +----------------------------------+----------------------------------+ +| | ``land-sea mask`` | Download as ``constants``. | +| +----------------------------------+----------------------------------+ +| | ``mean_sea_level_pressure`` | | +| +----------------------------------+----------------------------------+ +| | ``orography`` | Download as ``constants``. | +| +----------------------------------+----------------------------------+ +| | ``surface_pressure`` | | +| +----------------------------------+----------------------------------+ +| | ``temperature_850`` | Extracted from ``temperature``. | +| +----------------------------------+----------------------------------+ +| | ``toa_incident_solar_radiation`` | | +| +----------------------------------+----------------------------------+ +| | ``total_cloud_cover`` | | +| +----------------------------------+----------------------------------+ +| | ``total_precipitation`` | | ++-----------------+----------------------------------+----------------------------------+ +| Pressure levels | ``geopotential`` | | +| +----------------------------------+----------------------------------+ +| | ``potential_vorticity`` | | +| +----------------------------------+----------------------------------+ +| | ``relative_humidity`` | | +| +----------------------------------+----------------------------------+ +| | ``specific_humidity`` | | +| +----------------------------------+----------------------------------+ +| | ``temperature`` | | +| +----------------------------------+----------------------------------+ +| | ``u_component_of_wind`` | | +| +----------------------------------+----------------------------------+ +| | ``v_component_of_wind`` | | +| +----------------------------------+----------------------------------+ +| | ``vorticity`` | | ++-----------------+----------------------------------+----------------------------------+ + +Downloading from Copernicus +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +While we generally recommend using WeatherBench, ClimateLearn also provides +access to ERA5 data through +`Copernicus `_. +Copernicus ERA5 data is not pre-processed and requires an API key, which can be +obtained by following the instructions at this link: https://cds.climate.copernicus.eu/api-how-to. +Once you have the API key, the following code will download ERA5 data from +Copernicus. The API key only needs to be provided on the first function call. + +.. code-block:: python + + import climate_learn as cl + + root_directory = "/home/user/climate-learn" + variable = "2m_temperature" + year = 2000 + cl.data.download_copernicus_era5( + dst=f"{root_directory}/{variable}", + variable=variable, + year=year, + pressure=False, # optional, default is False + api_key={YOUR_API_KEY_HERE} # optional, only required on first call + ) + +We refer to the Copernicus documentation for ERA5 data on +`single levels `_ +and +`pressure levels `_ +for details about available years and variables. + +CMIP6 Data Collection +--------------------- + +**CMIP6** is a collection of simulated data from the Coupled Model +Intercomparison Project Phase 6 (CMIP6), an international effort across +different climate modeling groups to compare and evaluate their global climate +models. ClimateLearn facilitates access to data produced by the MPI-ESM1.2-HR +model of CMIP6 as it contains similar climate variables as those represented in +ERA5. MPI-ESM1.2-HR provides data from 1850 to 2015 at 6 hour intervals on a +grid with cells of width and height :math:`1^\circ`. Since this corresponds to +data that is too big for most deep learning models, ClimateLearn provides +a smaller version of the raw MPI-ESM1.2-HR data. + +.. _weatherbench-cmip6-download: + +Downloading from WeatherBench +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Besides providing ERA5 data, `WeatherBench `_ +also provides data from MPI-ESM1.2-HR of CMIP6. + +.. code-block:: python + + import climate_learn as cl + + root_directory = "/home/user/climate-learn" + variable = "temperature" + cl.data.download_weatherbench( + dst=f"{root_directory}/{variable}", + dataset="cmip6", + variable=variable, + resolution=5.625 # optional, default is 5.625 + ) + +.. _weatherbench-cmip6-reference: + +WeatherBench Quick Reference +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following variables can be downloaded from WeatherBench at 2.8125 and +5.625 degree resolutions. The temporal coverage is 1850 to 2015 at hourly +intervals, and the pressure levels are 50, 250, 500, 600, 700, 850, and 925 hPa +(same as ERA5 provided by WeatherBench). + ++-------------------------+ +| Variable | ++=========================+ +| ``geopotential`` | ++-------------------------+ +| ``specific_humidity`` | ++-------------------------+ +| ``temperature`` | ++-------------------------+ +| ``u_component_of_wind`` | ++-------------------------+ +| ``v_component_of_wind`` | ++-------------------------+ + +Downloading from the ESGF +^^^^^^^^^^^^^^^^^^^^^^^^^ + +While we generally recommend using WeatherBench, ClimateLearn also provides +access to the CMIP6 data through the +`Earth System Grid Federation (ESGF) `_. + +.. code-block:: python + + import climate_learn as cl + + root_directory = "/home/user/climate-learn" + variable = "tas" + cl.data.download_mpi_esm1_2_hr( + dst=f"{root_directory}/{variable}", + variable=variable, + years=(1850, 2015), # optional, (1850, 2015) is the default range + ) + +ESGF Quick Reference +^^^^^^^^^^^^^^^^^^^^ + +The following data can be downloaded from ESGF at 100km resolution, or about +:math:`0.8^\circ`. The temporal coverage is 1850 to 2015 (non-inclusive end) +at 6 hour intervals. + ++-----------+------------------------------------------+ +| Variable | Long Name | ++===========+==========================================+ +| ``ps`` | Surface air pressure | ++-----------+------------------------------------------+ +| ``tsl`` | Temperature of soil | ++-----------+------------------------------------------+ +| ``tas`` | Near-surface air temperature | ++-----------+------------------------------------------+ +| ``huss`` | Near-surface specific humidity | ++-----------+------------------------------------------+ +| ``vas`` | Northward near-surface wind | ++-----------+------------------------------------------+ +| ``uas`` | Eastward near-surface wind | ++-----------+------------------------------------------+ +| ``mrsos`` | Moisture in upper portion of soil column | ++-----------+------------------------------------------+ +| ``mrsol`` | Total water content of soil layer | ++-----------+------------------------------------------+ +| ``ta`` | Air temperature | ++-----------+------------------------------------------+ +| ``hus`` | Specific humidity | ++-----------+------------------------------------------+ +| ``va`` | Northward wind | ++-----------+------------------------------------------+ +| ``psl`` | Sea level pressure | ++-----------+------------------------------------------+ +| ``ua`` | Eastward wind | ++-----------+------------------------------------------+ +| ``zg`` | Geopotential height | ++-----------+------------------------------------------+ + +PRISM Dataset +------------- + +**PRISM** is a dataset of various observed atmospheric variables like +precipitation and temperature over the conterminous United States at varying +spatial and temporal resolutions from 1895 to present day. It is maintained +by the PRISM Climtae Group at Oregon State University. At the highest publicly +available resolution, PRISM contains daily data on a grid with cells of width +and height 4 km (approximately :math:`0.03^\circ`). Since this also corresponds +to data that is too big for most deep learning models, ClimateLearn provides +a regridded version of raw PRISM data to :math:`0.75^\circ` resolution. + +.. code-block:: python + + import climate_learn as cl + + root_directory = "/home/user/climate-learn" + variable = "tmax" + cl.data.download_prism( + dst=f"{root_directory}/{variable}", + variable=variable, + years=(1981, 2023), # optional, (1981, 2023) is the default range + ) + +The temporal coverage for the data ClimateLearn facilitates access to is 1981 +to present year (inclusive) at daily intervals. We refer to the documentation +for descriptions of the available variables: +https://prism.oregonstate.edu/documents/PRISM_datasets.pdf. + +.. note:: + + The script at `climate_learn/data/download.py` can be run standalone to + download data as well. + +Data Processing +--------------- + +From WeatherBench +^^^^^^^^^^^^^^^^^ + +The following assumes you have downloaded ERA5 data from Weatherbench to the +directory ``/home/user/climate-learn/``. + +.. code-block:: python + + from climate_learn.data.processing.nc2npz import convert_nc2npz + + convert_nc2npz( + root_dir="/home/user/climate-learn", + save_dir="/home/user/climate-learn/processed", + variables=["temperature", "geopotential"], + start_train_year=1979, + start_val_year=2015, + start_test_year=2017, + end_year=2018, + num_shards=16 + ) + +If you also have constants data downloaded, the above code snippet will handle +it automatically. You do not have to specify ``constants`` for the ``variables`` +argument. + +Extreme ERA5 Dataset +^^^^^^^^^^^^^^^^^^^^ + +**Extreme-ERA5** is a subset of ERA5 that we have curated to evaluate +forecasting performance for extreme weather events. Specifically, we consider +events where individual climate variables exceed critical values locally. +Heat waves and cold snaps are examples of such events that are intuitively +familiar. To generate the extreme ERA5 dataset, ClimateLearn requires ERA5 +data downloaded from WeatherBench. Then, run the script at +``src/climate_learn/data/processing/era5_extreme.py``. + +From PRISM +^^^^^^^^^^ + +Use the scripts at +``src/climate_learn/data/processing/era5_cropped.py`` and +``src/climate_learn/data/processing/prism.py``. + +.. note:: + + Currently, ClimateLearn normalizes all data to :math:`\mathcal{N}(0,1)`. We + recognize that this might not be the best transform for every variable. For + example, it is unreasonable to model precipitation according to a Gaussian + distribution. In the future, we will add support for different transforms in + data processing. + +Loading Data +------------ + +Once data has been downloaded and processed, it can be loaded into PyTorch +dataloaders for forecasting and downscaling. Legal arguments to the ``task`` +parameter are ``direct-forecasting``, ``iterative-forecasting``, +``continuous-forecasting``, and ``downscaling``. + +.. code:: python + + import climate_learn as cl + dm = cl.data.IterDataModule( + task, + inp_root_dir, + out_root_dir, + in_vars, + out_vars, + src="era5", + history=3, + window=6, + pred_range=args.pred_range, + subsample=6, + batch_size=128, + num_workers=8, + ) + +One can also load data for climate projection. + +.. code:: python + + import climate_learn as cl + dm = cl.data.ClimateBenchDataModule( + root_dir, + variables, + out_variables, + train_ratio=0.9, + history=10, + batch_size=16, + num_workers=1, + ) \ No newline at end of file diff --git a/docs/source/user-guide/visualizations.rst b/docs/source/user-guide/visualizations.rst index dec1a742..927a3418 100644 --- a/docs/source/user-guide/visualizations.rst +++ b/docs/source/user-guide/visualizations.rst @@ -1,149 +1,74 @@ Visualizations ============== -Suppose that you have loaded a data module and a model module from ClimateLearn: +Visualizing a Specific Prediction +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. code-block:: python - :linenos: - - from climate_learn.data import DataModule - from climate_learn.models import load_model - - data_module = DataModule(...) - model_module = load_model(...) - -The ``climate_learn.utils`` module provides functions to visualize data and -model outputs. These functions assume ``model_module`` and ``data_module`` -are initialized for the same task (both forecasting or both downscaling). - -To produce visualizations of initial condition, ground truth, prediction, and -bias [#]_, do the following. +For a specific prediction, ClimateLearn can show the bias, defined as the +predicted value minus the observed value at each grid cell. This is useful +for intuitively understanding which direction the model is erring in. .. code-block:: python - :linenos: - from climate_learn.utils import visualize - visualize(model_module, data_module) - -By default, ``visualize`` will pick 2 random dates in the test dataset. You can -change the number of dates it selects to ``n`` dates by passing ``split=n`` as -a parameter. Alternatively, you can specify exact dates by passing a list of -datetime strings formatted as ``YYYY-mm-dd:HH`` (*e.g.*, "2017-06-10:12"). See -:doc:`climate_learn.utils <../reference/utils>` for further details. + import climate_learn as cl + + # assuming we are forecasting geopotential from ERA5 + dm = cl.IterDataModule(...) + model = cl.load_forecasting_module(...) + denorm = model.test_target_transforms[0] + + cl.utils.visualize( + model, + dm, + in_transform=denorm, + out_transform=denorm, + variable="geopotential", + src="era5", + index=0 # visualize the first sample of the test set + ) -To produce visualizations of a model's mean bias on the test dataset, do the -following. +.. image:: images/gt_at_index.png -.. code-block:: python - :linenos: +.. image:: images/prediction_at_index.png - from climate_learn.utils import visualize_mean_bias - visualize_mean_bias(model_module, data_module) +.. image:: images/bias_at_index.png -.. [#] Bias is defined as *predicted* minus *observed*. +In the case that history is greater than 1 (*i.e.*, the model is given a +sequence of historical weather states as input), ``cl.utils.visualize`` +returns an object which can be animated. See the +`quickstart notebook `_ +for an interactive example of this. -Example -------- +.. code-block:: python -The following can be run in Google Colab. + from IPython.display import HTML + in_graphic = cl.utils.visualize(...) + HTML(in_graphic.to_jshtml()) -.. nbinput:: ipython3 - :execution-count: 1 +.. image:: images/animated_input.png - %%capture - !pip install git+https://github.com/aditya-grover/climate-learn.git +Visualizing Average Performance +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. nbinput:: ipython3 - :execution-count: 2 +ClimateLearn can also display the mean bias, which is the average bias across +the entire testing set. This is helpful for understanding trends in the model's +predictions. - # Download WeatherBench 2m_temperature data to Google Drive - from google.colab import drive - from climate_learn.data import download +.. code-block:: python - drive.mount("/content/drive") - download( - root="/content/drive/MyDrive/Climate/.climate_tutorial", - source="weatherbench", - variable="2m_temperature", - dataset="era5", - resolution="5.625" - ) + import climate_learn as cl -.. nbinput:: ipython3 - :execution-count: 3 - - # Load data module for forecasting task - from climate_learn.utils.datetime import Year, Days, Hours - from climate_learn.data import DataModule - - data_module = DataModule( - dataset = "ERA5", - task = "forecasting", - root_dir = "/content/drive/MyDrive/Climate/.climate_tutorial/data/weatherbench/era5/5.625/", - in_vars = ["2m_temperature"], - out_vars = ["2m_temperature"], - train_start_year = Year(1979), - val_start_year = Year(2015), - test_start_year = Year(2017), - end_year = Year(2018), - pred_range = Days(3), - subsample = Hours(6), - batch_size = 128, - num_workers = 1 - ) + # assuming we are forecasting geopotential from ERA5 + dm = cl.IterDataModule(...) + model = cl.load_forecasting_module(...) + denorm = model.test_target_transforms[0] -.. nbinput:: ipython3 - :execution-count: 4 - - # Load ResNet model - from climate_learn.models import load_model - - model_kwargs = { - "in_channels": len(data_module.hparams.in_vars), - "out_channels": len(data_module.hparams.out_vars), - "n_blocks": 4 - } - - optim_kwargs = { - "lr": 1e-4, - "weight_decay": 1e-5, - "warmup_epochs": 1, - "max_epochs": 5, - } - - model_module = load_model( - name="resnet", - task="forecasting", - model_kwargs=model_kwargs, - optim_kwargs=optim_kwargs + cl.utils.visualize_mean_bias( + dm, + model, + out_transform=denorm, + variable="geopotential", + src="era5" ) -.. nbinput:: ipython3 - :execution-count: 5 - - # Visualize ResNet model performance on two dates in the test set - from climate_learn.utils import visualize - visualize(model_module, data_module, samples=["2017-06-01:12", "2017-08-01:18"]) - -.. nboutput:: - :execution-count: 5 - - .. image:: images/visualize.png - :alt: Visualizations produced by ``utils.visualize``. - -.. nbinput:: ipython3 - :execution-count: 6 - - # Visualize ResNet model mean bias across the entire test set - from climate_learn.utils import visualize_mean_bias - visualize_mean_bias(model_module, data_module) - -.. nboutput:: - :execution-count: 6 - - .. image:: images/visualize_mean_bias.png - :alt: Mean bias visualization produced by ``utils.visualize_mean_bias``. - -*Note:* These visualizations were produced using a trained ResNet model, but -training is omitted from this example. Please see :doc:`Models ` for -model training. +.. image:: images/mean_bias.png \ No newline at end of file diff --git a/experiments/climate_projection/climatebench.py b/experiments/climate_projection/climatebench.py new file mode 100644 index 00000000..4555c665 --- /dev/null +++ b/experiments/climate_projection/climatebench.py @@ -0,0 +1,134 @@ +# Standard library +from argparse import ArgumentParser + +# Third party +import climate_learn as cl +import pytorch_lightning as pl +from pytorch_lightning.callbacks import ( + EarlyStopping, + ModelCheckpoint, + RichModelSummary, + RichProgressBar, +) +from pytorch_lightning.loggers.tensorboard import TensorBoardLogger + + +parser = ArgumentParser() +parser.add_argument("climatebench_dir") +parser.add_argument("model", choices=["resnet", "unet", "vit"]) +parser.add_argument( + "variable", + choices=["tas", "diurnal_temperature_range", "pr", "pr90"], + help="The variable to predict.", +) +parser.add_argument("--summary_depth", type=int, default=1) +parser.add_argument("--max_epochs", type=int, default=50) +parser.add_argument("--patience", type=int, default=10) +parser.add_argument("--gpu", type=int, default=-1) +parser.add_argument("--checkpoint", default=None) +args = parser.parse_args() + +# Set up data +variables = ["CO2", "SO2", "CH4", "BC"] +out_variables = args.variable +dm = cl.data.ClimateBenchDataModule( + args.climatebench_dir, + variables=variables, + out_variables=out_variables, + train_ratio=0.9, + history=10, + batch_size=16, + num_workers=1, +) + +# Set up deep learning model +if args.model == "resnet": + model_kwargs = { # override some of the defaults + "in_channels": 4, + "out_channels": 1, + "history": 10, + "n_blocks": 28, + } +elif args.model == "unet": + model_kwargs = { # override some of the defaults + "in_channels": 4, + "out_channels": 1, + "history": 10, + "ch_mults": (1, 2, 2), + "is_attn": (False, False, False), + } +elif args.model == "vit": + model_kwargs = { # override some of the defaults + "img_size": (32, 64), + "in_channels": 4, + "out_channels": 1, + "history": 10, + "patch_size": 2, + "embed_dim": 128, + "depth": 8, + "decoder_depth": 2, + "learn_pos_emb": True, + "num_heads": 4, + } +optim_kwargs = {"lr": 5e-4, "weight_decay": 1e-5, "betas": (0.9, 0.99)} +sched_kwargs = { + "warmup_epochs": 5, + "max_epochs": 50, + "warmup_start_lr": 1e-8, + "eta_min": 1e-8, +} +model = cl.load_climatebench_module( + data_module=dm, + model=args.model, + model_kwargs=model_kwargs, + optim="adamw", + optim_kwargs=optim_kwargs, + sched="linear-warmup-cosine-annealing", + sched_kwargs=sched_kwargs, +) + +# Set up trainer +pl.seed_everything(0) +default_root_dir = f"{args.model}_climatebench_{args.variable}" +logger = TensorBoardLogger(save_dir=f"{default_root_dir}/logs") +early_stopping = "val/mse:aggregate" +callbacks = [ + RichProgressBar(), + RichModelSummary(max_depth=args.summary_depth), + EarlyStopping(monitor=early_stopping, patience=args.patience), + ModelCheckpoint( + dirpath=f"{default_root_dir}/checkpoints", + monitor=early_stopping, + filename="epoch_{epoch:03d}", + auto_insert_metric_name=False, + ), +] +trainer = pl.Trainer( + logger=logger, + callbacks=callbacks, + default_root_dir=default_root_dir, + accelerator="gpu" if args.gpu != -1 else None, + devices=[args.gpu] if args.gpu != -1 else None, + max_epochs=args.max_epochs, + strategy="ddp", + precision="16", + log_every_n_steps=1, +) + +# Train and evaluate model from scratch +if args.checkpoint is None: + trainer.fit(model, datamodule=dm) + trainer.test(model, datamodule=dm, ckpt_path="best") +# Evaluate saved model checkpoint +else: + model = cl.LitModule.load_from_checkpoint( + args.checkpoint, + net=model.net, + optimizer=model.optimizer, + lr_scheduler=None, + train_loss=None, + val_loss=None, + test_loss=model.test_loss, + test_target_transforms=model.test_target_transforms, + ) + trainer.test(model, datamodule=dm) diff --git a/experiments/downscaling/era5_era5_baselines.py b/experiments/downscaling/era5_era5_baselines.py new file mode 100644 index 00000000..66551633 --- /dev/null +++ b/experiments/downscaling/era5_era5_baselines.py @@ -0,0 +1,39 @@ +# Standard library +from argparse import ArgumentParser + +# Third party +import climate_learn as cl +import pytorch_lightning as pl + + +parser = ArgumentParser() +parser.add_argument("era5_low_res_dir") +parser.add_argument("era5_high_res_dir") +args = parser.parse_args() + +# Set up data +in_vars = out_vars = [ + "2m_temperature", + "geopotential_500", + "temperature_850", +] +dm = cl.data.IterDataModule( + "downscaling", + args.era5_low_res_dir, + args.era5_high_res_dir, + in_vars, + out_vars, + subsample=1, + batch_size=32, + num_workers=4, +) +dm.setup() + +# Set up baseline models +nearest = cl.load_downscaling_module(data_module=dm, preset="nearest-interpolation") +bilinear = cl.load_downscaling_module(data_module=dm, preset="bilinear-interpolation") + +# Evaluate baselines (no training needed) +trainer = pl.Trainer() +trainer.test(nearest, dm) +trainer.test(bilinear, dm) diff --git a/experiments/downscaling/era5_era5_deep_learning.py b/experiments/downscaling/era5_era5_deep_learning.py new file mode 100644 index 00000000..1e55d63a --- /dev/null +++ b/experiments/downscaling/era5_era5_deep_learning.py @@ -0,0 +1,121 @@ +# Standard library +from argparse import ArgumentParser + +# Third party +import climate_learn as cl +from climate_learn.data.processing.era5_constants import ( + PRESSURE_LEVEL_VARS, + DEFAULT_PRESSURE_LEVELS, +) +import pytorch_lightning as pl +from pytorch_lightning.callbacks import ( + EarlyStopping, + ModelCheckpoint, + RichModelSummary, + RichProgressBar, +) +from pytorch_lightning.loggers.tensorboard import TensorBoardLogger + + +parser = ArgumentParser() +parser.add_argument("era5_low_res_dir") +parser.add_argument("era5_high_res_dir") +parser.add_argument("preset", choices=["resnet", "unet", "vit"]) +parser.add_argument( + "variable", choices=["t2m", "z500", "t850"], help="The variable to predict." +) +parser.add_argument("--summary_depth", type=int, default=1) +parser.add_argument("--max_epochs", type=int, default=50) +parser.add_argument("--patience", type=int, default=5) +parser.add_argument("--gpu", type=int, default=-1) +parser.add_argument("--checkpoint", default=None) +args = parser.parse_args() + +# Set up data +variables = [ + "land_sea_mask", + "orography", + "lattitude", + "toa_incident_solar_radiation", + "2m_temperature", + "10m_u_component_of_wind", + "10m_v_component_of_wind", + "geopotential", + "temperature", + "relative_humidity", + "specific_humidity", + "u_component_of_wind", + "v_component_of_wind", +] +out_var_dict = { + "t2m": "2m_temperature", + "z500": "geopotential_500", + "t850": "temperature_850", +} +in_vars = [] +for var in variables: + if var in PRESSURE_LEVEL_VARS: + for level in DEFAULT_PRESSURE_LEVELS: + in_vars.append(var + "_" + str(level)) + else: + in_vars.append(var) +dm = cl.data.IterDataModule( + "downscaling", + args.era5_low_res_dir, + args.era5_high_res_dir, + in_vars, + out_vars=[out_var_dict[args.variable]], + subsample=1, + batch_size=32, + buffer_size=2000, + num_workers=4, +) +dm.setup() + +# Set up deep learning model +model = cl.load_downscaling_module(data_module=dm, architecture=args.preset) + +# Setup trainer +pl.seed_everything(0) +default_root_dir = f"{args.preset}_downscaling_{args.variable}" +logger = TensorBoardLogger(save_dir=f"{default_root_dir}/logs") +early_stopping = "val/mse:aggregate" +callbacks = [ + RichProgressBar(), + RichModelSummary(max_depth=args.summary_depth), + EarlyStopping(monitor=early_stopping, patience=args.patience), + ModelCheckpoint( + dirpath=f"{default_root_dir}/checkpoints", + monitor=early_stopping, + filename="epoch_{epoch:03d}", + auto_insert_metric_name=False, + ), +] +trainer = pl.Trainer( + logger=logger, + callbacks=callbacks, + default_root_dir=default_root_dir, + accelerator="gpu" if args.gpu != -1 else None, + devices=[args.gpu] if args.gpu != -1 else None, + max_epochs=args.max_epochs, + strategy="ddp", + precision="16", +) + +# Train and evaluate model from scratch +if args.checkpoint is None: + trainer.fit(model, datamodule=dm) + trainer.test(model, datamodule=dm, ckpt_path="best") +# Evaluate saved model checkpoint +else: + model = cl.LitModule.load_from_checkpoint( + args.checkpoint, + net=model.net, + optimizer=model.optimizer, + lr_scheduler=None, + train_loss=None, + val_loss=None, + test_loss=model.test_loss, + test_target_tranfsorms=model.test_target_transforms, + ) + trainer.test(model, datamodule=dm) diff --git a/experiments/downscaling/era5_prism_baselines.py b/experiments/downscaling/era5_prism_baselines.py new file mode 100644 index 00000000..4ed8fe32 --- /dev/null +++ b/experiments/downscaling/era5_prism_baselines.py @@ -0,0 +1,46 @@ +# Standard library +from argparse import ArgumentParser + +# Third party +import climate_learn as cl +from climate_learn.transforms import Mask, Denormalize +import pytorch_lightning as pl + + +parser = ArgumentParser() +parser.add_argument("era5_cropped_dir") +parser.add_argument("prism_processed_dir") +args = parser.parse_args() + +# Set up data +dm = cl.data.ERA5toPRISMDataModule( + args.era5_cropped_dir, + args.prism_processed_dir, + batch_size=32, + num_workers=4, +) +dm.setup() + +# Set up baseline models +mask = Mask(dm.get_out_mask()) +denorm = Denormalize(dm) +denorm_mask = lambda x: denorm(mask(x)) +nearest = cl.load_downscaling_module( + data_module=dm, + preset="nearest-interpolation", + train_target_transform=mask, + val_target_transform=[denorm_mask, denorm_mask, denorm_mask, mask], + test_target_transform=[denorm_mask, denorm_mask, denorm_mask], +) +bilinear = cl.load_downscaling_module( + data_module=dm, + preset="bilinear-interpolation", + train_target_transform=mask, + val_target_transform=[denorm_mask, denorm_mask, denorm_mask, mask], + test_target_transform=[denorm_mask, denorm_mask, denorm_mask], +) + +# Evaluate baselines (no training needed) +trainer = pl.Trainer() +trainer.test(nearest, dm) +trainer.test(bilinear, dm) diff --git a/experiments/downscaling/era5_prism_deep_learning.py b/experiments/downscaling/era5_prism_deep_learning.py new file mode 100644 index 00000000..706a282b --- /dev/null +++ b/experiments/downscaling/era5_prism_deep_learning.py @@ -0,0 +1,133 @@ +# Standard library +from argparse import ArgumentParser + +# Third party +import climate_learn as cl +from climate_learn.models.hub import VisionTransformer, Interpolation +from climate_learn.transforms import Mask, Denormalize +import pytorch_lightning as pl +from pytorch_lightning.callbacks import ( + EarlyStopping, + ModelCheckpoint, + RichModelSummary, + RichProgressBar, +) +from pytorch_lightning.loggers.tensorboard import TensorBoardLogger +import torch.nn as nn + + +parser = ArgumentParser() +parser.add_argument("era5_cropped_dir") +parser.add_argument("prism_processed_dir") +parser.add_argument("preset", choices=["resnet", "unet", "vit"]) +parser.add_argument("--summary_depth", type=int, default=1) +parser.add_argument("--max_epochs", type=int, default=50) +parser.add_argument("--patience", type=int, default=5) +parser.add_argument("--gpu", type=int, default=-1) +parser.add_argument("--checkpoint", default=None) +args = parser.parse_args() + +# Set up data +dm = cl.data.ERA5toPRISMDataModule( + args.era5_cropped_dir, + args.prism_processed_dir, + batch_size=32, + num_workers=4, +) +dm.setup() + +# Set up masking +mask = Mask(dm.get_out_mask().to(device=f"cuda:{args.gpu}")) +denorm = Denormalize(dm) +denorm_mask = lambda x: denorm(mask(x)) + +# Default ViT preset is optimized for ERA5 to ERA5 downscaling, so we +# modify the architecture for ERA5 to PRISM +if args.preset == "vit": + net = nn.Sequential( + Interpolation((32, 64), "bilinear"), + VisionTransformer( + img_size=(32, 64), + in_channels=1, + out_channels=1, + history=1, + patch_size=2, + learn_pos_emb=True, + embed_dim=128, + depth=8, + decoder_depth=2, + num_heads=4, + ), + ) + optim_kwargs = {"lr": 1e-5, "weight_decay": 1e-5, "betas": (0.9, 0.99)} + sched_kwargs = { + "warmup_epochs": 5, + "max_epochs": 50, + "warmup_start_lr": 1e-8, + "eta_min": 1e-8, + } + model = cl.load_downscaling_module( + data_module=dm, + model=net, + optim="adamw", + optim_kwargs=optim_kwargs, + sched="linear-warmup-cosine-annealing", + sched_kwargs=sched_kwargs, + train_target_transform=mask, + val_target_transform=[denorm_mask, denorm_mask, denorm_mask, mask], + test_target_transform=[denorm_mask, denorm_mask, denorm_mask], + ) +# Default presets for ResNet and U-net are ready to use out of the box +else: + model = cl.load_downscaling_module( + data_module=dm, + architecture=args.preset, + train_target_transform=mask, + val_target_transform=[denorm_mask, denorm_mask, denorm_mask, mask], + test_target_transform=[denorm_mask, denorm_mask, denorm_mask], + ) + +# Setup trainer +pl.seed_everything(0) +default_root_dir = f"{args.preset}_downscaling_prism" +logger = TensorBoardLogger(save_dir=f"{default_root_dir}/logs") +early_stopping = "val/mse:aggregate" +callbacks = [ + RichProgressBar(), + RichModelSummary(max_depth=args.summary_depth), + EarlyStopping(monitor=early_stopping, patience=args.patience), + ModelCheckpoint( + dirpath=f"{default_root_dir}/checkpoints", + monitor=early_stopping, + filename="epoch_{epoch:03d}", + auto_insert_metric_name=False, + ), +] +trainer = pl.Trainer( + logger=logger, + callbacks=callbacks, + default_root_dir=default_root_dir, + accelerator="gpu" if args.gpu != -1 else None, + devices=[args.gpu] if args.gpu != -1 else None, + max_epochs=args.max_epochs, + strategy="ddp", + precision="16", +) + +# Train and evaluate model from scratch +if args.checkpoint is None: + trainer.fit(model, datamodule=dm) + trainer.test(model, datamodule=dm, ckpt_path="best") +# Evaluate saved model checkpoint +else: + model = cl.LitModule.load_from_checkpoint( + args.checkpoint, + net=model.net, + optimizer=model.optimizer, + lr_scheduler=None, + train_loss=None, + val_loss=None, + test_loss=model.test_loss, + test_target_transforms=model.test_target_transforms, + ) + trainer.test(model, datamodule=dm) diff --git a/experiments/forecasting/cmip6_cmip6_baselines.py b/experiments/forecasting/cmip6_cmip6_baselines.py new file mode 100644 index 00000000..8354850c --- /dev/null +++ b/experiments/forecasting/cmip6_cmip6_baselines.py @@ -0,0 +1,39 @@ +# Standard library +from argparse import ArgumentParser + +# Third party +import climate_learn as cl +import pytorch_lightning as pl + + +parser = ArgumentParser() +parser.add_argument("cmip6_dir") +parser.add_argument("pred_range", type=int, choices=[6, 24, 72, 120, 240]) +args = parser.parse_args() + +# Set up data +in_vars = out_vars = ["air_temperature", "geopotential_500", "temperature_850"] +dm = cl.data.IterDataModule( + "direct-forecasting", + args.cmip6_dir, + args.cmip6_dir, + in_vars, + out_vars, + src="mpi-esm1-2-hr", + history=3, + window=6, + pred_range=args.pred_range, + subsample=6, + batch_size=128, + num_workers=8, +) +dm.setup() + +# Set up baseline models +climatology = cl.load_forecasting_module(data_module=dm, preset="climatology") +persistence = cl.load_forecasting_module(data_module=dm, preset="persistence") + +# Evaluate baslines (no training needed) +trainer = pl.Trainer() +trainer.test(climatology, dm) +trainer.test(persistence, dm) diff --git a/experiments/forecasting/cmip6_cmip6_deep_learning.py b/experiments/forecasting/cmip6_cmip6_deep_learning.py new file mode 100644 index 00000000..0f3f56b9 --- /dev/null +++ b/experiments/forecasting/cmip6_cmip6_deep_learning.py @@ -0,0 +1,153 @@ +# Standard library +from argparse import ArgumentParser + +# Third party +import climate_learn as cl +from climate_learn.data.processing.cmip6_constants import ( + PRESSURE_LEVEL_VARS, + DEFAULT_PRESSURE_LEVELS, +) +import pytorch_lightning as pl +from pytorch_lightning.callbacks import ( + EarlyStopping, + ModelCheckpoint, + RichModelSummary, + RichProgressBar, +) +from pytorch_lightning.loggers.tensorboard import TensorBoardLogger + + +parser = ArgumentParser() +parser.add_argument("cmip6_dir") +parser.add_argument("model", choices=["resnet", "unet", "vit"]) +parser.add_argument("pred_range", type=int, choices=[6, 24, 72, 120, 240]) +parser.add_argument("--summary_depth", type=int, default=1) +parser.add_argument("--max_epochs", type=int, default=50) +parser.add_argument("--patience", type=int, default=5) +parser.add_argument("--gpu", type=int, default=-1) +parser.add_argument("--checkpoint", default=None) +args = parser.parse_args() + +# Set up data +variables = [ + "air_temperature", + "geopotential", + "temperature", + "specific_humidity", + "u_component_of_wind", + "v_component_of_wind", +] +in_vars = [] +for var in variables: + if var in PRESSURE_LEVEL_VARS: + for level in DEFAULT_PRESSURE_LEVELS: + in_vars.append(var + "_" + str(level)) + else: + in_vars.append(var) +out_variables = ["air_temperature", "geopotential_500", "temperature_850"] +out_vars = [] +for var in out_variables: + if var in PRESSURE_LEVEL_VARS: + for level in DEFAULT_PRESSURE_LEVELS: + out_vars.append(var + "_" + str(level)) + else: + out_vars.append(var) +dm = cl.data.IterDataModule( + "direct-forecasting", + args.cmip6_dir, + args.cmip6_dir, + in_vars, + out_vars, + history=3, + window=6, + pred_range=args.pred_range, + subsample=6, + buffer_size=2000, + batch_size=128, + num_workers=4, +) +dm.setup() + +# Set up deep learning model +if args.model == "resnet": + model_kwargs = { # override some of the defaults + "in_channels": 36, + "out_channels": 3, + "history": 3, + "n_blocks": 28, + } +elif args.model == "unet": + model_kwargs = { # override some of the defaults + "in_channels": 36, + "out_channels": 3, + "history": 3, + "ch_mults": (1, 2, 2), + "is_attn": (False, False, False), + } +elif args.model == "vit": + model_kwargs = { # override some of the defaults + "img_size": (32, 64), + "in_channels": 36, + "out_channels": 3, + "history": 3, + "patch_size": 2, + "embed_dim": 128, + "depth": 8, + "decoder_depth": 2, + "learn_pos_emb": True, + "num_heads": 4, + } +model = cl.load_forecasting_module( + data_module=dm, + model=args.model, + model_kwargs=model_kwargs, + optim="adamw", + optim_kwargs={"lr": 5e-4, "weight_decay": 1e-5}, + sched="linear-warmup-cosine-annealing", + sched_kwargs={"warmup_epochs": 5, "max_epoch": 50}, +) + +# Setup trainer +pl.seed_everything(0) +default_root_dir = f"{args.model}_forecasting_{args.pred_range}" +logger = TensorBoardLogger(save_dir=f"{default_root_dir}/logs") +early_stopping = "val/lat_mse:aggregate" +callbacks = [ + RichProgressBar(), + RichModelSummary(max_depth=args.summary_depth), + EarlyStopping(monitor=early_stopping, patience=args.patience), + ModelCheckpoint( + dirpath=f"{default_root_dir}/checkpoints", + monitor=early_stopping, + filename="epoch_{epoch:03d}", + auto_insert_metric_name=False, + ), +] +trainer = pl.Trainer( + logger=logger, + callbacks=callbacks, + default_root_dir=default_root_dir, + accelerator="gpu" if args.gpu != -1 else None, + devices=[args.gpu] if args.gpu != -1 else None, + max_epochs=args.max_epochs, + strategy="ddp", + precision="16", +) + +# Train and evaluate model from scratch +if args.checkpoint is None: + trainer.fit(model, datamodule=dm) + trainer.test(model, datamodule=dm, ckpt_path="best") +# Evaluate saved model checkpoint +else: + model = cl.LitModule.load_from_checkpoint( + args.checkpoint, + net=model.net, + optimizer=model.optimizer, + lr_scheduler=None, + train_loss=None, + val_loss=None, + test_loss=model.test_loss, + test_target_tranfsorms=model.test_target_transforms, + ) + trainer.test(model, datamodule=dm) diff --git a/experiments/forecasting/era5_era5_baselines.py b/experiments/forecasting/era5_era5_baselines.py new file mode 100644 index 00000000..1ba4bd96 --- /dev/null +++ b/experiments/forecasting/era5_era5_baselines.py @@ -0,0 +1,39 @@ +# Standard library +from argparse import ArgumentParser + +# Third party +import climate_learn as cl +import pytorch_lightning as pl + + +parser = ArgumentParser() +parser.add_argument("era5_dir") +parser.add_argument("pred_range", type=int, choices=[6, 24, 72, 120, 240]) +args = parser.parse_args() + +# Set up data +in_vars = out_vars = ["2m_temperature", "geopotential_500", "temperature_850"] +dm = cl.data.IterDataModule( + "direct-forecasting", + args.era5_dir, + args.era5_dir, + in_vars, + out_vars, + src="era5", + history=3, + window=6, + pred_range=args.pred_range, + subsample=6, + batch_size=128, + num_workers=8, +) +dm.setup() + +# Set up baseline models +climatology = cl.load_forecasting_module(data_module=dm, preset="climatology") +persistence = cl.load_forecasting_module(data_module=dm, preset="persistence") + +# Evaluate baslines (no training needed) +trainer = pl.Trainer() +trainer.test(climatology, dm) +trainer.test(persistence, dm) diff --git a/experiments/forecasting/era5_era5_deep_learning.py b/experiments/forecasting/era5_era5_deep_learning.py new file mode 100644 index 00000000..6870b993 --- /dev/null +++ b/experiments/forecasting/era5_era5_deep_learning.py @@ -0,0 +1,277 @@ +# Standard library +from argparse import ArgumentParser + +# Third party +import climate_learn as cl +from climate_learn.data.processing.era5_constants import ( + PRESSURE_LEVEL_VARS, + DEFAULT_PRESSURE_LEVELS, +) +import pytorch_lightning as pl +from pytorch_lightning.callbacks import ( + EarlyStopping, + ModelCheckpoint, + RichModelSummary, + RichProgressBar, +) +from pytorch_lightning.loggers.tensorboard import TensorBoardLogger + + +parser = ArgumentParser() + +parser.add_argument("--summary_depth", type=int, default=1) +parser.add_argument("--max_epochs", type=int, default=50) +parser.add_argument("--patience", type=int, default=5) +parser.add_argument("--gpu", type=int, default=-1) +parser.add_argument("--checkpoint", default=None) + +subparsers = parser.add_subparsers( + help="Whether to perform direct, iterative, or continuous forecasting.", + dest="forecast_type", +) +direct = subparsers.add_parser("direct") +iterative = subparsers.add_parser("iterative") +continuous = subparsers.add_parser("continuous") + +direct.add_argument("era5_dir") +direct.add_argument("model", choices=["resnet", "unet", "vit"]) +direct.add_argument("pred_range", type=int, choices=[6, 24, 72, 120, 240]) + +iterative.add_argument("era5_dir") +iterative.add_argument("model", choices=["resnet", "unet", "vit"]) +iterative.add_argument("pred_range", type=int, choices=[6, 24, 72, 120, 240]) + +continuous.add_argument("era5_dir") +continuous.add_argument("model", choices=["resnet", "unet", "vit"]) + +args = parser.parse_args() + +# Set up data +variables = [ + "geopotential", + "temperature", + "u_component_of_wind", + "v_component_of_wind", + "relative_humidity", + "specific_humidity", + "2m_temperature", + "10m_u_component_of_wind", + "10m_v_component_of_wind", + "toa_incident_solar_radiation", + "land_sea_mask", + "orography", + "lattitude", +] +in_vars = [] +for var in variables: + if var in PRESSURE_LEVEL_VARS: + for level in DEFAULT_PRESSURE_LEVELS: + in_vars.append(var + "_" + str(level)) + else: + in_vars.append(var) +if args.forecast_type in ("direct", "continuous"): + out_variables = ["2m_temperature", "geopotential_500", "temperature_850"] +elif args.forecast_type == "iterative": + out_variables = variables +out_vars = [] +for var in out_variables: + if var in PRESSURE_LEVEL_VARS: + for level in DEFAULT_PRESSURE_LEVELS: + out_vars.append(var + "_" + str(level)) + else: + out_vars.append(var) +if args.forecast_type in ("direct", "iterative"): + dm = cl.data.IterDataModule( + f"{args.forecast_type}-forecasting", + args.era5_dir, + args.era5_dir, + in_vars, + out_vars, + src="era5", + history=3, + window=6, + pred_range=args.pred_range, + subsample=6, + batch_size=128, + num_workers=8, + ) +elif args.forecast_type == "continuous": + dm = cl.data.IterDataModule( + "continuous-forecasting", + args.era5_dir, + args.era5_dir, + in_vars, + out_vars, + src="era5", + history=3, + window=6, + pred_range=1, + max_pred_range=120, + random_lead_time=True, + hrs_each_step=1, + subsample=6, + batch_size=128, + buffer_size=2000, + num_workers=8, + ) +dm.setup() + +# Set up deep learning model +in_channels = 49 +if args.forecast_type == "continuous": + in_channels += 1 # time dimension +if args.forecast_type == "iterative": # iterative predicts every var + out_channels = in_channels +else: + out_channels = 3 +if args.model == "resnet": + model_kwargs = { # override some of the defaults + "in_channels": in_channels, + "out_channels": out_channels, + "history": 3, + "n_blocks": 28, + } +elif args.model == "unet": + model_kwargs = { # override some of the defaults + "in_channels": in_channels, + "out_channels": out_channels, + "history": 3, + "ch_mults": (1, 2, 2), + "is_attn": (False, False, False), + } +elif args.model == "vit": + model_kwargs = { # override some of the defaults + "img_size": (32, 64), + "in_channels": in_channels, + "out_channels": out_channels, + "history": 3, + "patch_size": 2, + "embed_dim": 128, + "depth": 8, + "decoder_depth": 2, + "learn_pos_emb": True, + "num_heads": 4, + } +optim_kwargs = {"lr": 5e-4, "weight_decay": 1e-5, "betas": (0.9, 0.99)} +sched_kwargs = { + "warmup_epochs": 5, + "max_epochs": 50, + "warmup_start_lr": 1e-8, + "eta_min": 1e-8, +} +model = cl.load_forecasting_module( + data_module=dm, + model=args.model, + model_kwargs=model_kwargs, + optim="adamw", + optim_kwargs=optim_kwargs, + sched="linear-warmup-cosine-annealing", + sched_kwargs=sched_kwargs, +) + +# Setup trainer +pl.seed_everything(0) +default_root_dir = f"{args.model}_{args.forecast_type}_forecasting_{args.pred_range}" +logger = TensorBoardLogger(save_dir=f"{default_root_dir}/logs") +early_stopping = "val/lat_mse:aggregate" +callbacks = [ + RichProgressBar(), + RichModelSummary(max_depth=args.summary_depth), + EarlyStopping(monitor=early_stopping, patience=args.patience), + ModelCheckpoint( + dirpath=f"{default_root_dir}/checkpoints", + monitor=early_stopping, + filename="epoch_{epoch:03d}", + auto_insert_metric_name=False, + ), +] +trainer = pl.Trainer( + logger=logger, + callbacks=callbacks, + default_root_dir=default_root_dir, + accelerator="gpu" if args.gpu != -1 else None, + devices=[args.gpu] if args.gpu != -1 else None, + max_epochs=args.max_epochs, + strategy="ddp", + precision="16", +) + + +# Define testing regime for iterative forecasting +def iterative_testing(model, trainer, args, from_checkpoint=False): + for lead_time in [6, 24, 72, 120, 240]: + n_iters = lead_time // args.pred_range + model.set_mode("iter") + model.set_n_iters(n_iters) + test_dm = cl.data.IterDataModule( + "iterative-forecasting", + args.era5_dir, + args.era5_dir, + in_vars, + out_vars, + src="era5", + history=3, + window=6, + pred_range=lead_time, + subsample=1, + ) + if from_checkpoint: + trainer.test(model, datamodule=test_dm) + else: + trainer.test(model, datamodule=test_dm, ckpt_path="best") + + +# Define testing regime for continuous forecasting +def continuous_testing(model, trainer, args, from_checkpoint=False): + for lead_time in [6, 24, 72, 120, 240]: + test_dm = cl.data.IterDataModule( + "continuous-forecasting", + args.era5_dir, + args.era5_dir, + in_vars, + out_vars, + src="era5", + history=3, + window=6, + pred_range=lead_time, + max_pred_range=lead_time, + random_lead_time=False, + hrs_each_step=1, + subsample=1, + batch_size=128, + buffer_size=2000, + num_workers=8, + ) + if from_checkpoint: + trainer.test(model, datamodule=test_dm) + else: + trainer.test(model, datamodule=test_dm, ckpt_path="best") + + +# Train and evaluate model from scratch +if args.checkpoint is None: + trainer.fit(model, datamodule=dm) + if args.forecast_type == "direct": + trainer.test(model, datamodule=dm, ckpt_path="best") + elif args.forecast_type == "iterative": + iterative_testing(model, trainer, args) + elif args.forecast_type == "continuous": + continuous_testing(model, trainer, args) +# Evaluate saved model checkpoint +else: + model = cl.LitModule.load_from_checkpoint( + args.checkpoint, + net=model.net, + optimizer=model.optimizer, + lr_scheduler=None, + train_loss=None, + val_loss=None, + test_loss=model.test_loss, + test_target_tranfsorms=model.test_target_transforms, + ) + if args.forecast_type == "direct": + trainer.test(model, datamodule=dm) + elif args.forecast_type == "iterative": + iterative_testing(model, trainer, args, from_checkpoint=True) + elif args.forecast_type == "continuous": + continuous_testing(model, trainer, args, from_checkpoint=True) diff --git a/notebooks/1-Data_Processing.ipynb b/notebooks/1-Data_Processing.ipynb deleted file mode 100644 index 7ed58ff4..00000000 --- a/notebooks/1-Data_Processing.ipynb +++ /dev/null @@ -1,475 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "99jkSa_KmrDH" - }, - "source": [ - "# Data Processing\n", - "\n", - "ClimateLearn makes it super easy to prepare data for your machine learning pipelines. In this tutorial, we'll see how to download [ERA5](https://www.ecmwf.int/en/forecasts/datasets/reanalysis-datasets/era5) data from [WeatherBench](https://github.com/pangeo-data/WeatherBench) and prepare it for both the forecasting and [downscaling](https://uaf-snap.org/how-do-we-do-it/downscaling) tasks. This tutorial is intended for use in Google Colab." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Google Colab setup\n", - "You might need to restart the kernel after installing ClimateLearn so that your Colab environment knows to use the correct package versions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install climate-learn" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from google.colab import drive\n", - "drive.mount(\"/content/drive\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Download\n", - "\n", - "The following cell will take several minutes to run - the scale of climate data is huge!" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "QmQG73ZpQNHP", - "outputId": "e4d79d00-c5b8-4bb3-bf2a-75f94e737bec" - }, - "outputs": [], - "source": [ - "from climate_learn.data import download\n", - "\n", - "root = \"/content/drive/MyDrive/ClimateLearn\"\n", - "source = \"weatherbench\"\n", - "dataset = \"era5\"\n", - "resolution = \"5.625\"\n", - "variable = \"2m_temperature\"\n", - "\n", - "download(root=root, source=source, dataset=dataset, resolution=resolution, variable=variable)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "bSt6h_Q-oqjK" - }, - "source": [ - "ClimateLearn comes with some utilities to view the downloaded data in its raw format. This can be useful as a quick sanity check that you have the data you expect. Climate data is natively stored in the [NetCDF format](https://www.unidata.ucar.edu/software/netcdf/), which means it comes bundled with lots of helpful named metadata such as latitude, longitude, and time. However, we want the data in a form that can be easily ingested by PyTorch machine learning models." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 357 - }, - "id": "97hHL2Z7-Z86", - "outputId": "2b960774-065b-4eb4-d3e3-e18001acab32" - }, - "outputs": [], - "source": [ - "from climate_learn.utils.data import load_dataset, view\n", - "\n", - "my_dataset = load_dataset(f\"{root}/data/{source}/{dataset}/{resolution}/{variable}\")\n", - "view(my_dataset)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "3XM3rITW9Y3-" - }, - "source": [ - "## Preparing data for forecasting" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this cell, we specify the dataset arguments. The temporal range of ERA5 data on WeatherBench is 1979 to 2018." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "EK2UD49hQ3om", - "outputId": "6650bc42-8a22-4f53-fa4e-b32b8c5f6887" - }, - "outputs": [], - "source": [ - "from climate_learn.data.climate_dataset.args import ERA5Args\n", - "\n", - "years = range(1979, 2018)\n", - "data_args = ERA5Args(\n", - " root_dir=f\"{root}/data/{source}/{dataset}/{resolution}/\",\n", - " variables=[variable],\n", - " years=years,\n", - " name=dataset\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we specify the task arguments. In this case we are interested in forecasting only `2m_temperature` using only `2m_temperature`, but one could specify additional variables, provided that the data for those variables is downloaded. The prediction range is in hours, so if we want to predict 3 days ahead, we provide `3*24`. Further, we subsample every 6 hours of the day since weather conditions do not change significantly on hourly intervals." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from climate_learn.data.task.args import ForecastingArgs\n", - "\n", - "forecasting_args = ForecastingArgs(\n", - " in_vars=[dataset + \":\" + variable],\n", - " out_vars=[dataset + \":\" + variable],\n", - " pred_range=3*24,\n", - " subsample=6\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As the scale of climate data is huge, we need to specify how we want to load the data in the CPU memory. ClimateLearn allows us to either load the entire data into memory or shard it and then load it in chunks. The latter comes with the overhead of loading data multiple times in every epoch. In this tutorial, as the data has just single variable, we would use the first technique to load the data." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "from climate_learn.data.dataset.args import MapDatasetArgs\n", - "\n", - "map_dataset_args = MapDatasetArgs(\n", - " climate_dataset_args=data_args,\n", - " task_args=forecasting_args\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we specify the data module, where we define our train-validation-testing split and the batch size." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "from climate_learn.data import DataModule\n", - "\n", - "modified_args_for_train_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"years\": range(1979, 2015)\n", - " }\n", - "}\n", - "train_dataset_args = map_dataset_args.create_copy(modified_args_for_train_dataset)\n", - "\n", - "modified_args_for_val_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"years\": range(2015, 2017)\n", - " }\n", - "}\n", - "val_dataset_args = map_dataset_args.create_copy(modified_args_for_val_dataset)\n", - "\n", - "modified_args_for_test_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"years\": range(2017, 2019)\n", - " }\n", - "}\n", - "test_dataset_args = map_dataset_args.create_copy(\n", - " modified_args_for_test_dataset\n", - ")\n", - "\n", - "data_module = DataModule(\n", - " train_dataset_args,\n", - " val_dataset_args,\n", - " test_dataset_args,\n", - " batch_size=128,\n", - " num_workers=1\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "srfsF01OLV-C" - }, - "source": [ - "## Preparing data for downscaling\n", - "\n", - "In the [downscaling task](https://uaf-snap.org/how-do-we-do-it/downscaling), we want to build a machine learning model that can map low-resolution weather patterns (source) to high-resolution weather patterns (target). In the previous section, we already downloaded a dataset for `2m_temperature` at 5.625 degrees resolution. Here, let's download a dataset also for `2m_temperature` but at 2.8125 degrees resolution." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "u3tRve6-h0sI", - "outputId": "bd5501e2-ac7c-4b2f-9edc-584c3e054e74" - }, - "outputs": [], - "source": [ - "hi_resolution = \"2.8125\"\n", - "download(root=root, source=source, dataset=dataset, resolution=hi_resolution, variable=variable)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "YsR8lhdjXejR" - }, - "source": [ - "Next, we specify the dataset arguments. This is the same procedure as for forecasting, but with two datasets now: one set of arguments is for the source, and another set of arguments is for the target." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "7T9N7cL4oFKm", - "outputId": "897e622e-6f29-4211-be36-c2e8250c4bf3" - }, - "outputs": [], - "source": [ - "lowres_data_args = ERA5Args(\n", - " root_dir=f\"{root}/data/{source}/{dataset}/{resolution}/\",\n", - " variables=[variable],\n", - " years=years,\n", - " name=\"lowres\"\n", - ")\n", - "\n", - "highres_data_args = ERA5Args(\n", - " root_dir=f\"{root}/data/{source}/{dataset}/{hi_resolution}\",\n", - " variables=[variable],\n", - " years=years,\n", - " name=\"highres\"\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we need to wrap these multiple dataset sources into one." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "from climate_learn.data.climate_dataset.args import StackedClimateDatasetArgs\n", - "\n", - "data_args = StackedClimateDatasetArgs(\n", - " data_args=[lowres_data_args, highres_data_args], name=dataset\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then, we specify the task arguments." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "from climate_learn.data.task.args import DownscalingArgs\n", - "\n", - "downscaling_args = DownscalingArgs(\n", - " in_vars=[dataset + \":lowres:\" + variable],\n", - " out_vars=[dataset + \":highres:\" + variable],\n", - " subsample=6,\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We again need to specifiy how to load data into memory. This time let's try the sharding approach. Other than the `climate_dataset_args` and `task_args`, we also need to specify the number of chunks we want to shard the dataset into. Note that in `ERA5` the data for different years is stored in different files. Thus, we can't have nuber of chunks greater than the number of training years." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "from climate_learn.data.dataset.args import ShardDatasetArgs\n", - "\n", - "shard_data_args = ShardDatasetArgs(\n", - " climate_dataset_args=data_args,\n", - " task_args=downscaling_args,\n", - " n_chunks=5\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we specify the data module, which looks the same as for the forecasting task." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "modified_args_for_train_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"child_data_args\": [\n", - " {\"years\": range(1979, 2015)},\n", - " {\"years\": range(1979, 2015)},\n", - " ]\n", - " }\n", - "}\n", - "train_dataset_args = shard_data_args.create_copy(modified_args_for_train_dataset)\n", - "\n", - "modified_args_for_val_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"child_data_args\": [\n", - " {\"years\": range(2015, 2017)},\n", - " {\"years\": range(2015, 2017)},\n", - " ]\n", - " },\n", - " \"n_chunks\": 1\n", - "}\n", - "val_dataset_args = shard_data_args.create_copy(modified_args_for_val_dataset)\n", - "\n", - "modified_args_for_test_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"child_data_args\": [\n", - " {\"years\": range(2017, 2019)},\n", - " {\"years\": range(2017, 2019)},\n", - " ]\n", - " },\n", - " \"n_chunks\": 1\n", - "}\n", - "test_dataset_args = shard_data_args.create_copy(\n", - " modified_args_for_test_dataset\n", - ")\n", - "\n", - "data_module = DataModule(\n", - " train_dataset_args,\n", - " val_dataset_args,\n", - " test_dataset_args,\n", - " batch_size=128,\n", - " num_workers=1\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Congralutions! Now you know how to load and process data with ClimateLearn. Please visit our [docs](https://climatelearn.readthedocs.io/en/latest/user-guide/datasets.html) to learn more." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "provenance": [] - }, - "gpuClass": "standard", - "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.9.7" - }, - "vscode": { - "interpreter": { - "hash": "5b35d5811d64db97cad819926e9e0ba09b354a75e2ee95b259c11201fc783944" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/2-Model_Training_Evaluation.ipynb b/notebooks/2-Model_Training_Evaluation.ipynb deleted file mode 100644 index ea46f36e..00000000 --- a/notebooks/2-Model_Training_Evaluation.ipynb +++ /dev/null @@ -1,617 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Model Training and Evaluation\n", - "\n", - "ClimateLearn provides a variety of baseline models to perform forecasting and [downscaling](https://uaf-snap.org/how-do-we-do-it/downscaling). In this tutorial, we'll see how to train a [ResNet model](https://en.wikipedia.org/wiki/Residual_neural_network) to do both. This tutorial is intended for use in Google Colab. Before starting, ensure that you are on a GPU runtime." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Google Colab setup\n", - "You might need to restart the kernel after installing ClimateLearn so that your Colab environment knows to use the correct package versions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install climate-learn" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from google.colab import drive\n", - "drive.mount(\"/content/drive\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "99jkSa_KmrDH", - "tags": [] - }, - "source": [ - "## Forecasting\n", - "\n", - "### Data preparation\n", - "The second cell of this section can be skipped if the data is already downloaded to your Drive. See the \"Data Processing\" notebook for more details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "root = \"/content/drive/MyDrive/ClimateLearn\"\n", - "source = \"weatherbench\"\n", - "dataset = \"era5\"\n", - "resolution = \"5.625deg\"\n", - "variable = \"geopotential\"\n", - "years = range(1979, 2018)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "mTzr2Liw-SEv", - "outputId": "eedffac1-e708-4678-b7bf-05ebded7865d" - }, - "outputs": [], - "source": [ - "from climate_learn.data import download\n", - "download(root=root, source=source, dataset=dataset, resolution=resolution, variable=variable)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from climate_learn.data import DataModule\n", - "from climate_learn.data.climate_dataset.args import ERA5Args\n", - "from climate_learn.data.dataset.args import MapDatasetArgs\n", - "from climate_learn.data.task.args import ForecastingArgs\n", - "\n", - "data_args = ERA5Args(\n", - " root_dir=f\"{root}/data/{source}/{dataset}/{resolution}/\",\n", - " variables=[variable],\n", - " years=years,\n", - " name=dataset\n", - ")\n", - "\n", - "forecasting_args = ForecastingArgs(\n", - " in_vars=[dataset + \":\" + variable],\n", - " out_vars=[dataset + \":\" + variable],\n", - " pred_range=3*24,\n", - " subsample=6\n", - ")\n", - "\n", - "map_dataset_args = MapDatasetArgs(\n", - " climate_dataset_args=data_args,\n", - " task_args=forecasting_args\n", - ")\n", - "\n", - "modified_args_for_train_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"years\": range(1979, 2015)\n", - " }\n", - "}\n", - "train_dataset_args = map_dataset_args.create_copy(modified_args_for_train_dataset)\n", - "\n", - "modified_args_for_val_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"years\": range(2015, 2017)\n", - " }\n", - "}\n", - "val_dataset_args = map_dataset_args.create_copy(modified_args_for_val_dataset)\n", - "\n", - "modified_args_for_test_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"years\": range(2017, 2019)\n", - " }\n", - "}\n", - "test_dataset_args = map_dataset_args.create_copy(\n", - " modified_args_for_test_dataset\n", - ")\n", - "\n", - "data_module = DataModule(\n", - " train_dataset_args,\n", - " val_dataset_args,\n", - " test_dataset_args,\n", - " batch_size=32,\n", - " num_workers=8\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "yWXsiZ5freTG" - }, - "source": [ - "### Model initialization\n", - "Let's load some presets to get points of comparison." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "paTI33tP5R4H" - }, - "outputs": [], - "source": [ - "import climate_learn as cl\n", - "\n", - "climatology = cl.load_forecasting_module(data_module=data_module, preset=\"climatology\")\n", - "persistence = cl.load_forecasting_module(data_module=data_module, preset=\"persistence\")\n", - "linreg = cl.load_forecasting_module(data_module=data_module, preset=\"linear-regression\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The linear regression model needs training. Climatology and persistence do not require training." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "trainer = cl.Trainer()\n", - "trainer.fit(linreg, data_module)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's see how these do on the test data." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "trainer.test(climatology, data_module)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "trainer.test(persistence, data_module)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "trainer.test(linreg, data_module)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, let's load a more complex model, like the architecture used by [Rasp and Theurey (2020)](https://arxiv.org/abs/2008.08626) for the [WeatherBench](https://github.com/pangeo-data/WeatherBench) SoTA." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "rasp_theurey = cl.load_forecasting_module(data_module=data_module, preset=\"rasp-theurey-2020\")\n", - "trainer.fit(rasp_theurey, data_module)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "trainer.test(rasp_theurey, data_module)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "sWigLES4s22j" - }, - "source": [ - "Ideally, the model's predictions have a strong correlation with the ground truth, which would be indicated by a high [anomaly correlation coefficient](https://climatelearn.readthedocs.io/en/latest/user-guide/metrics.html#anomaly-correlation-coefficient) value. We also want our model to achieve a smaller [latitude-weighted root mean square error](https://climatelearn.readthedocs.io/en/latest/user-guide/metrics.html#anomaly-correlation-coefficient) than the climatological forecast.\n", - "\n", - "Also, ClimateLearn supports more advanced functionality for loading forecasting models. See our docs to learn more." - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "provenance": [] - }, - "gpuClass": "standard", - "kernelspec": { - "display_name": "climate", - "language": "python", - "name": "climate" - }, - "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.10.9" - }, - "vscode": { - "interpreter": { - "hash": "5b35d5811d64db97cad819926e9e0ba09b354a75e2ee95b259c11201fc783944" - } - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "129ac0f36052427aa8b30c90dde58f47": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "859e5ae0abf849e4b3210ac64ac5b65a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "8a962a93fd25439888de07922ec1dae5": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "9f6338e3a9fb4a4fa4a3b5da14e6b471": { - "model_module": "@jupyter-widgets/output", - "model_module_version": "1.0.0", - "model_name": "OutputModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/output", - "_model_module_version": "1.0.0", - "_model_name": "OutputModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/output", - "_view_module_version": "1.0.0", - "_view_name": "OutputView", - "layout": "IPY_MODEL_d124da129802411086077527493bf39b", - "msg_id": "", - "outputs": [ - { - "data": { - "text/html": "
Testing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35/35 0:00:15 • 0:00:00 2.23it/s  \n
\n", - "text/plain": "\u001b[37mTesting\u001b[0m \u001b[38;2;98;6;224m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[37m35/35\u001b[0m \u001b[38;5;245m0:00:15 • 0:00:00\u001b[0m \u001b[38;5;249m2.23it/s\u001b[0m \n" - }, - "metadata": {}, - "output_type": "display_data" - } - ] - } - }, - "b575b66e6d61441091aa5da106711ec1": { - "model_module": "@jupyter-widgets/output", - "model_module_version": "1.0.0", - "model_name": "OutputModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/output", - "_model_module_version": "1.0.0", - "_model_name": "OutputModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/output", - "_view_module_version": "1.0.0", - "_view_name": "OutputView", - "layout": "IPY_MODEL_129ac0f36052427aa8b30c90dde58f47", - "msg_id": "", - "outputs": [ - { - "data": { - "text/html": "
Epoch 0/0  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 136/136 0:00:32 • 0:00:00 4.43it/s loss: 1.1 train/2m_temperature:   \n                                                                                 1.099 train/loss: 1.099           \n
\n", - "text/plain": "\u001b[37mEpoch 0/0 \u001b[0m \u001b[38;2;98;6;224m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[37m136/136\u001b[0m \u001b[38;5;245m0:00:32 • 0:00:00\u001b[0m \u001b[38;5;249m4.43it/s\u001b[0m \u001b[37mloss: 1.1 train/2m_temperature: \u001b[0m\n \u001b[37m1.099 train/loss: 1.099 \u001b[0m\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ] - } - }, - "d124da129802411086077527493bf39b": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "da5e5740522b4a00931b4351f3d99f5c": { - "model_module": "@jupyter-widgets/output", - "model_module_version": "1.0.0", - "model_name": "OutputModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/output", - "_model_module_version": "1.0.0", - "_model_name": "OutputModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/output", - "_view_module_version": "1.0.0", - "_view_name": "OutputView", - "layout": "IPY_MODEL_8a962a93fd25439888de07922ec1dae5", - "msg_id": "", - "outputs": [ - { - "data": { - "text/html": "
Testing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 137/137 0:00:32 • 0:00:00 4.17it/s  \n
\n", - "text/plain": "\u001b[37mTesting\u001b[0m \u001b[38;2;98;6;224m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[37m137/137\u001b[0m \u001b[38;5;245m0:00:32 • 0:00:00\u001b[0m \u001b[38;5;249m4.17it/s\u001b[0m \n" - }, - "metadata": {}, - "output_type": "display_data" - } - ] - } - }, - "f1b2b711e84a4e469d28ce506c5fbcc8": { - "model_module": "@jupyter-widgets/output", - "model_module_version": "1.0.0", - "model_name": "OutputModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/output", - "_model_module_version": "1.0.0", - "_model_name": "OutputModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/output", - "_view_module_version": "1.0.0", - "_view_name": "OutputView", - "layout": "IPY_MODEL_859e5ae0abf849e4b3210ac64ac5b65a", - "msg_id": "", - "outputs": [ - { - "data": { - "text/html": "
Epoch 4/4  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 24/24 0:00:19 • 0:00:00 1.24it/s loss: 0.0597 train/2m_temperature:  \n                                                                               0.056 train/loss: 0.056             \n
\n", - "text/plain": "Epoch 4/4 \u001b[38;2;98;6;224m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[37m24/24\u001b[0m \u001b[38;5;245m0:00:19 • 0:00:00\u001b[0m \u001b[38;5;249m1.24it/s\u001b[0m \u001b[37mloss: 0.0597 train/2m_temperature: \u001b[0m\n \u001b[37m0.056 train/loss: 0.056 \u001b[0m\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ] - } - } - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/3-Visualization.ipynb b/notebooks/3-Visualization.ipynb deleted file mode 100644 index 1d6d24d6..00000000 --- a/notebooks/3-Visualization.ipynb +++ /dev/null @@ -1,355 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "rSRCNgYzUwaf" - }, - "source": [ - "# Visualization\n", - "\n", - "ClimateLearn provides tools to generate visualizations of model predictions for both forecasting and [downscaling](https://uaf-snap.org/how-do-we-do-it/downscaling). In this tutorial, we'll see how to visualize bias and mean bias. This tutorial is intended for use in Google Colab. Before starting, ensure that you are on a GPU runtime." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Google Colab setup\n", - "\n", - "You might need to restart the kernel after installing ClimateLearn so that your Colab environment knows to use the correct package versions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install climate-learn" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from google.colab import drive\n", - "drive.mount(\"/content/drive\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "99jkSa_KmrDH" - }, - "source": [ - "## Forecasting\n", - "\n", - "### Data preparation\n", - "The second cell of this section can be skipped if the data is already downloaded to your Drive. See the \"Data Processing\" notebook for mor details." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "root = \"/content/drive/MyDrive/ClimateLearn\"\n", - "source = \"weatherbench\"\n", - "dataset = \"era5\"\n", - "resolution = \"5.625\"\n", - "variable = \"2m_temperature\"\n", - "years = range(1979, 2018)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "from climate_learn.data import download\n", - "download(root=root, source=source, dataset=dataset, resolution=resolution, variable=variable)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from climate_learn.data import DataModule\n", - "from climate_learn.data.climate_dataset.args import ERA5Args\n", - "from climate_learn.data.dataset.args import MapDatasetArgs\n", - "from climate_learn.data.task.args import ForecastingArgs\n", - "\n", - "data_args = ERA5Args(\n", - " root_dir=f\"{root}/data/{source}/{dataset}/{resolution}/\",\n", - " variables=[variable],\n", - " years=years,\n", - " name=dataset\n", - ")\n", - "\n", - "forecasting_args = ForecastingArgs(\n", - " in_vars=[dataset + \":\" + variable],\n", - " out_vars=[dataset + \":\" + variable],\n", - " pred_range=3*24,\n", - " subsample=6\n", - ")\n", - "\n", - "map_dataset_args = MapDatasetArgs(\n", - " climate_dataset_args=data_args,\n", - " task_args=forecasting_args\n", - ")\n", - "\n", - "modified_args_for_train_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"years\": range(1979, 2015)\n", - " }\n", - "}\n", - "train_dataset_args = map_dataset_args.create_copy(modified_args_for_train_dataset)\n", - "\n", - "modified_args_for_val_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"years\": range(2015, 2017)\n", - " }\n", - "}\n", - "val_dataset_args = map_dataset_args.create_copy(modified_args_for_val_dataset)\n", - "\n", - "modified_args_for_test_dataset = {\n", - " \"climate_dataset_args\": {\n", - " \"years\": range(2017, 2019)\n", - " }\n", - "}\n", - "test_dataset_args = map_dataset_args.create_copy(\n", - " modified_args_for_test_dataset\n", - ")\n", - "\n", - "data_module = DataModule(\n", - " train_dataset_args,\n", - " val_dataset_args,\n", - " test_dataset_args,\n", - " batch_size=128,\n", - " num_workers=1\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Model training\n", - "\n", - "See the \"Model Training & Evaluation\" notebook for more details." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import climate_learn as cl\n", - "\n", - "rasp_theurey = cl.load_forecasting_module(preset=\"rasp-theurey-2020\")\n", - "trainer = cl.Trainer()\n", - "trainer.fit(rasp_theurey, data_module)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "tcPCvx8AbPFZ" - }, - "source": [ - "### Visualization" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "SRQLNyO_yPhn" - }, - "source": [ - "We visualize the **bias**, given by the difference in the predicted and the ground truth values.\n", - "\n", - "Visualization is done on the test set. ClimateLearn allows you to specify the exact times to visualize. Alternatively, you can specify a number $n$, and ClimateLearn will randomly sample $n$ times from the test set." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "colab": { - "background_save": true - }, - "id": "G-AM0-CPbTNl", - "outputId": "6c8c41b2-e671-49b1-f267-81f7d8c443da" - }, - "outputs": [], - "source": [ - "from climate_learn.utils import visualize\n", - "visualize(rasp_theurey, data_module, samples=[\"2017-06-01:12\", \"2017-08-01:18\"])" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "C6HYhl551E8_" - }, - "source": [ - "In addition to visualizing the bias the model has for each individual data point, we can also visualize the mean bias across the entire test set." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": { - "background_save": true - }, - "id": "H7Qjfu-H1VEd", - "outputId": "337ad5a8-44fb-45dd-ae4a-9ddddd91992d" - }, - "outputs": [], - "source": [ - "from climate_learn.utils import visualize_mean_bias\n", - "visualize_mean_bias(rasp_theurey.cuda(), data_module)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Congratulations! Now you know how to produce visualizations of model predictions on the forecasting task. Please visit our [docs](https://climatelearn.readthedocs.io/en/latest/user-guide/visualizations.html) to learn more." - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "provenance": [] - }, - "gpuClass": "standard", - "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.10.9" - }, - "vscode": { - "interpreter": { - "hash": "5b35d5811d64db97cad819926e9e0ba09b354a75e2ee95b259c11201fc783944" - } - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "74ce30a285964993b98d538aa40c4eee": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "9dda786fe5834d40b445dab3d341cdb4": { - "model_module": "@jupyter-widgets/output", - "model_module_version": "1.0.0", - "model_name": "OutputModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/output", - "_model_module_version": "1.0.0", - "_model_name": "OutputModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/output", - "_view_module_version": "1.0.0", - "_view_name": "OutputView", - "layout": "IPY_MODEL_74ce30a285964993b98d538aa40c4eee", - "msg_id": "", - "outputs": [ - { - "data": { - "text/html": "
Epoch 4/4  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 136/136 0:00:30 • 0:00:00 4.46it/s loss: 0.0354 train/2m_temperature:\n                                                                                 0.035 train/loss: 0.035           \n
\n", - "text/plain": "Epoch 4/4 \u001b[38;2;98;6;224m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[37m136/136\u001b[0m \u001b[38;5;245m0:00:30 • 0:00:00\u001b[0m \u001b[38;5;249m4.46it/s\u001b[0m \u001b[37mloss: 0.0354 train/2m_temperature:\u001b[0m\n \u001b[37m0.035 train/loss: 0.035 \u001b[0m\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ] - } - } - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/MC_Dropout.ipynb b/notebooks/MC_Dropout.ipynb new file mode 100644 index 00000000..b280cd89 --- /dev/null +++ b/notebooks/MC_Dropout.ipynb @@ -0,0 +1,401 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "51d7335c-43dc-4c6c-8f3f-45f5ef504a85", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "071f1216-9687-4f42-82ed-f0346ebf2bb3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import climate_learn as cl\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import os\n", + "from scipy.stats import rankdata\n", + "import torch" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "01f7deb9-9cab-4e78-9bfe-567578a6e615", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "dm = cl.data.IterDataModule(\n", + " \"downscaling\",\n", + " os.environ[\"ERA5_5DEG\"],\n", + " os.environ[\"ERA5_2DEG\"],\n", + " [\"2m_temperature\", \"temperature_850\", \"geopotential_500\"],\n", + " [\"2m_temperature\"],\n", + " src=\"era5\",\n", + " history=1\n", + ")\n", + "dm.setup()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5f7d5ea1-b65d-4931-9321-46c4b102111b", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading preset: resnet\n", + "Using preset optimizer\n", + "Using preset learning rate scheduler\n", + "Loading training loss: mse\n", + "No train transform\n", + "Loading validation loss: rmse\n", + "Loading validation loss: pearson\n", + "Loading validation loss: mean_bias\n", + "Loading validation loss: mse\n", + "Loading validation transform: denormalize\n", + "Loading validation transform: denormalize\n", + "Loading validation transform: denormalize\n", + "No validation transform\n", + "Loading test loss: rmse\n", + "Loading test loss: pearson\n", + "Loading test loss: mean_bias\n", + "Loading test transform: denormalize\n", + "Loading test transform: denormalize\n", + "Loading test transform: denormalize\n" + ] + }, + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'climate_learn.utils.datetime'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[4], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m model \u001b[38;5;241m=\u001b[39m cl\u001b[38;5;241m.\u001b[39mload_downscaling_module(data_module\u001b[38;5;241m=\u001b[39mdm, preset\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mresnet\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 2\u001b[0m checkpoint \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m../checkpoints/resnet_downscaling_t2m/checkpoints/last.ckpt\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m----> 3\u001b[0m model \u001b[38;5;241m=\u001b[39m \u001b[43mcl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mLitModule\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mload_from_checkpoint\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcheckpoint\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnet\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmodel\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mnet\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/site-packages/pytorch_lightning/core/saving.py:139\u001b[0m, in \u001b[0;36mModelIO.load_from_checkpoint\u001b[0;34m(cls, checkpoint_path, map_location, hparams_file, strict, **kwargs)\u001b[0m\n\u001b[1;32m 59\u001b[0m \u001b[38;5;129m@classmethod\u001b[39m\n\u001b[1;32m 60\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mload_from_checkpoint\u001b[39m(\n\u001b[1;32m 61\u001b[0m \u001b[38;5;28mcls\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 66\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[1;32m 67\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Self: \u001b[38;5;66;03m# type: ignore[valid-type]\u001b[39;00m\n\u001b[1;32m 68\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 69\u001b[0m \u001b[38;5;124;03m Primary way of loading a model from a checkpoint. When Lightning saves a checkpoint\u001b[39;00m\n\u001b[1;32m 70\u001b[0m \u001b[38;5;124;03m it stores the arguments passed to ``__init__`` in the checkpoint under ``\"hyper_parameters\"``.\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 137\u001b[0m \u001b[38;5;124;03m y_hat = pretrained_model(x)\u001b[39;00m\n\u001b[1;32m 138\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 139\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_load_from_checkpoint\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 140\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 141\u001b[0m \u001b[43m \u001b[49m\u001b[43mcheckpoint_path\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 142\u001b[0m \u001b[43m \u001b[49m\u001b[43mmap_location\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 143\u001b[0m \u001b[43m \u001b[49m\u001b[43mhparams_file\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 144\u001b[0m \u001b[43m \u001b[49m\u001b[43mstrict\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 145\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 146\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/site-packages/pytorch_lightning/core/saving.py:160\u001b[0m, in \u001b[0;36m_load_from_checkpoint\u001b[0;34m(cls, checkpoint_path, map_location, hparams_file, strict, **kwargs)\u001b[0m\n\u001b[1;32m 158\u001b[0m map_location \u001b[38;5;241m=\u001b[39m cast(_MAP_LOCATION_TYPE, \u001b[38;5;28;01mlambda\u001b[39;00m storage, loc: storage)\n\u001b[1;32m 159\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m pl_legacy_patch():\n\u001b[0;32m--> 160\u001b[0m checkpoint \u001b[38;5;241m=\u001b[39m \u001b[43mpl_load\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcheckpoint_path\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmap_location\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmap_location\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 162\u001b[0m \u001b[38;5;66;03m# convert legacy checkpoints to the new format\u001b[39;00m\n\u001b[1;32m 163\u001b[0m checkpoint \u001b[38;5;241m=\u001b[39m _pl_migrate_checkpoint(\n\u001b[1;32m 164\u001b[0m checkpoint, checkpoint_path\u001b[38;5;241m=\u001b[39m(checkpoint_path \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(checkpoint_path, (\u001b[38;5;28mstr\u001b[39m, Path)) \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[1;32m 165\u001b[0m )\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/site-packages/lightning_fabric/utilities/cloud_io.py:48\u001b[0m, in \u001b[0;36m_load\u001b[0;34m(path_or_url, map_location)\u001b[0m\n\u001b[1;32m 46\u001b[0m fs \u001b[38;5;241m=\u001b[39m get_filesystem(path_or_url)\n\u001b[1;32m 47\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m fs\u001b[38;5;241m.\u001b[39mopen(path_or_url, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrb\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[0;32m---> 48\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mtorch\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mload\u001b[49m\u001b[43m(\u001b[49m\u001b[43mf\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmap_location\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmap_location\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/site-packages/torch/serialization.py:789\u001b[0m, in \u001b[0;36mload\u001b[0;34m(f, map_location, pickle_module, weights_only, **pickle_load_args)\u001b[0m\n\u001b[1;32m 787\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m 788\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m pickle\u001b[38;5;241m.\u001b[39mUnpicklingError(UNSAFE_MESSAGE \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mstr\u001b[39m(e)) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;28mNone\u001b[39m\n\u001b[0;32m--> 789\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_load\u001b[49m\u001b[43m(\u001b[49m\u001b[43mopened_zipfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmap_location\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpickle_module\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mpickle_load_args\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 790\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m weights_only:\n\u001b[1;32m 791\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/site-packages/torch/serialization.py:1131\u001b[0m, in \u001b[0;36m_load\u001b[0;34m(zip_file, map_location, pickle_module, pickle_file, **pickle_load_args)\u001b[0m\n\u001b[1;32m 1129\u001b[0m unpickler \u001b[38;5;241m=\u001b[39m UnpicklerWrapper(data_file, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mpickle_load_args)\n\u001b[1;32m 1130\u001b[0m unpickler\u001b[38;5;241m.\u001b[39mpersistent_load \u001b[38;5;241m=\u001b[39m persistent_load\n\u001b[0;32m-> 1131\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43munpickler\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mload\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1133\u001b[0m torch\u001b[38;5;241m.\u001b[39m_utils\u001b[38;5;241m.\u001b[39m_validate_loaded_sparse_tensors()\n\u001b[1;32m 1135\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/pickle.py:1213\u001b[0m, in \u001b[0;36m_Unpickler.load\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1211\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mEOFError\u001b[39;00m\n\u001b[1;32m 1212\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(key, bytes_types)\n\u001b[0;32m-> 1213\u001b[0m \u001b[43mdispatch\u001b[49m\u001b[43m[\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m]\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1214\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m _Stop \u001b[38;5;28;01mas\u001b[39;00m stopinst:\n\u001b[1;32m 1215\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m stopinst\u001b[38;5;241m.\u001b[39mvalue\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/pickle.py:1529\u001b[0m, in \u001b[0;36m_Unpickler.load_global\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1527\u001b[0m module \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mreadline()[:\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m]\u001b[38;5;241m.\u001b[39mdecode(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mutf-8\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 1528\u001b[0m name \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mreadline()[:\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m]\u001b[38;5;241m.\u001b[39mdecode(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mutf-8\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m-> 1529\u001b[0m klass \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfind_class\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmodule\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1530\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mappend(klass)\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/site-packages/torch/serialization.py:1124\u001b[0m, in \u001b[0;36m_load..UnpicklerWrapper.find_class\u001b[0;34m(self, mod_name, name)\u001b[0m\n\u001b[1;32m 1122\u001b[0m \u001b[38;5;28;01mpass\u001b[39;00m\n\u001b[1;32m 1123\u001b[0m mod_name \u001b[38;5;241m=\u001b[39m load_module_mapping\u001b[38;5;241m.\u001b[39mget(mod_name, mod_name)\n\u001b[0;32m-> 1124\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfind_class\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmod_name\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/site-packages/pytorch_lightning/_graveyard/legacy_import_unpickler.py:24\u001b[0m, in \u001b[0;36mRedirectingUnpickler.find_class\u001b[0;34m(self, module, name)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m module \u001b[38;5;241m!=\u001b[39m new_module:\n\u001b[1;32m 23\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mRedirecting import of \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmodule\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m.\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m to \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnew_module\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m.\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m---> 24\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfind_class\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnew_module\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/climate/lib/python3.10/pickle.py:1580\u001b[0m, in \u001b[0;36m_Unpickler.find_class\u001b[0;34m(self, module, name)\u001b[0m\n\u001b[1;32m 1578\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m module \u001b[38;5;129;01min\u001b[39;00m _compat_pickle\u001b[38;5;241m.\u001b[39mIMPORT_MAPPING:\n\u001b[1;32m 1579\u001b[0m module \u001b[38;5;241m=\u001b[39m _compat_pickle\u001b[38;5;241m.\u001b[39mIMPORT_MAPPING[module]\n\u001b[0;32m-> 1580\u001b[0m \u001b[38;5;28;43m__import__\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mmodule\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1581\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mproto \u001b[38;5;241m>\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m4\u001b[39m:\n\u001b[1;32m 1582\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _getattribute(sys\u001b[38;5;241m.\u001b[39mmodules[module], name)[\u001b[38;5;241m0\u001b[39m]\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'climate_learn.utils.datetime'" + ] + } + ], + "source": [ + "model = cl.load_downscaling_module(data_module=dm, preset=\"resnet\")\n", + "checkpoint = \"../checkpoints/resnet_downscaling_t2m/checkpoints/last.ckpt\"\n", + "model = cl.LitModule.load_from_checkpoint(checkpoint, net=model.net)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "53da7885-2b5c-49f3-9798-fd310b9aa53b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for batch in dm.test_dataloader():\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "56a95287-4c91-4bf5-88f3-7ef460c70263", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "x, y = batch[:2]" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "15e5dd11-8884-4389-81a4-45f1e97ef0be", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "ensemble_forecast = cl.utils.get_monte_carlo_predictions(\n", + " x.to(device=\"cuda:2\"),\n", + " mm.to(device=\"cuda:2\"),\n", + " 50\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "46f8d772-38f7-4b7e-ab3b-e37a7c55508a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([50, 64, 3, 32, 64])" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ensemble_forecast.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "23b74992-8c14-491d-b8f3-2f8b74749cfa", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "var, mean = torch.var_mean(ensemble_forecast, 0)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "c7396247-f1cf-4987-adec-0ab16080b004", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(64, 32, 64)\n" + ] + } + ], + "source": [ + "# Compute rank histogram for 2m_temperature across the batch\n", + "channel = variables.index(\"2m_temperature\")\n", + "obs = y.detach().cpu().numpy()[:,channel]\n", + "print(obs.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "4338b1f4-676e-4fc4-9524-102b1a3a3d6d", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(50, 64, 32, 64)\n" + ] + } + ], + "source": [ + "ensemble = ensemble_forecast.detach().cpu().numpy()[:,:,channel]\n", + "print(ensemble.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "e4ac55a9-0e78-4ff7-a149-536aa4927e02", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(51, 64, 32, 64)\n" + ] + } + ], + "source": [ + "combined = np.vstack((obs[np.newaxis], ensemble))\n", + "print(combined.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "5d129365-81c8-415f-8cb3-2f47632a48ab", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(51, 64, 32, 64)\n" + ] + } + ], + "source": [ + "ranks = np.apply_along_axis(lambda x: rankdata(x, method=\"min\"), 0, combined)\n", + "print(ranks.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "f7b64a0b-ed5b-4861-9b0a-847a5496ffce", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(64, 32, 64)\n" + ] + } + ], + "source": [ + "ties = np.sum(ranks[0] == ranks[1:], axis=0)\n", + "print(ties.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "22e2c91b-215f-46bf-9840-a6ffaa53b16c", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0]\n" + ] + } + ], + "source": [ + "ranks = ranks[0]\n", + "tie = np.unique(ties)\n", + "print(tie)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "de096ba8-507c-42ab-94c1-e55aa7dc0ad9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for i in range(1, len(tie)):\n", + " idx = ranks[ties == tie[i]]\n", + " ranks[ties == tie[i]] = [\n", + " np.random.randint(idx[j], idx[j] + tie[i] + 1, tie[i])[0]\n", + " for j in range(len(idx))\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "eb84f93c-61b1-4ade-9b36-0fd873ba5739", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "hist = np.histogram(\n", + " ranks,\n", + " bins=np.linspace(0.5,combined.shape[0]+0.5,combined.shape[0]+1)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "934110f8-2509-4b3a-ab00-ebdf3f4de5fe", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjkAAAGeCAYAAAB2GhCmAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAuZ0lEQVR4nO3de1RV553/8Q+g54CXc/AGR0ZUUtMo8RYx4smtk0g9NbQrRtPR1EmoMXFpIKOQeKFNMU27gsuseKtGmpu4VuJ4mTXaRhMMgxGnEW8YJl6ZpDGBVA+YSThHGQWE/fujP/Z4KqaiWMqT92utZ8Wzn+959rOfsNb+rM3emzDLsiwBAAAYJry9JwAAAHAjEHIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACN1au8JtKempiadOnVK3bt3V1hYWHtPBwAAXAXLsnT27FnFxcUpPPwbrtdYrTBgwABL0mXtySeftCzLss6fP289+eSTVs+ePa2uXbtakyZNsvx+f8gYn3/+uXX//fdbUVFRVp8+faxnnnnGamhoCKl5//33rdtuu81yOBzWd77zHWvt2rWXzWXVqlXWgAEDLKfTaY0ZM8bat29faw7FsizLqqysbPF4aDQajUaj/f23ysrKbzzPt+pKzoEDB9TY2Gh/PnLkiL7//e/rxz/+sSQpMzNT27dv1+bNm+V2u5WRkaFJkybpgw8+kCQ1NjYqNTVVHo9He/bs0enTp/Xoo4+qc+fOeuGFFyRJJ0+eVGpqqmbNmqW33npLRUVFevzxx9W3b1/5fD5J0saNG5WVlaW8vDwlJydr+fLl8vl8Ki8vV0xMzFUfT/fu3SVJlZWVcrlcrVkKAADQToLBoOLj4+3z+BW1+vLHJebMmWN95zvfsZqamqyamhqrc+fO1ubNm+3+48ePW5KskpISy7Is65133rHCw8NDru6sWbPGcrlcVl1dnWVZljV//nzr1ltvDdnPlClTLJ/PZ38eM2aMlZ6ebn9ubGy04uLirNzc3FbNPxAIWJKsQCDQqu8BAID2c7Xn72u+8bi+vl5vvvmmHnvsMYWFham0tFQNDQ1KSUmxawYPHqz+/furpKREklRSUqJhw4YpNjbWrvH5fAoGgzp69Khdc+kYzTXNY9TX16u0tDSkJjw8XCkpKXbNldTV1SkYDIY0AABgpmsOOVu3blVNTY1++tOfSpL8fr8cDoeio6ND6mJjY+X3++2aSwNOc39z3zfVBINBnT9/Xl9++aUaGxtbrGke40pyc3PldrvtFh8f36pjBgAAHcc1h5zXX39dEyZMUFxcXFvO54bKzs5WIBCwW2VlZXtPCQAA3CDX9Aj5559/rv/4j//Qv//7v9vbPB6P6uvrVVNTE3I1p6qqSh6Px67Zv39/yFhVVVV2X/N/m7ddWuNyuRQVFaWIiAhFRES0WNM8xpU4nU45nc7WHSwAAOiQrulKztq1axUTE6PU1FR7W1JSkjp37qyioiJ7W3l5uSoqKuT1eiVJXq9Xhw8fVnV1tV1TWFgol8ulxMREu+bSMZprmsdwOBxKSkoKqWlqalJRUZFdAwAA0OqnqxobG63+/ftbCxYsuKxv1qxZVv/+/a2dO3daBw8etLxer+X1eu3+ixcvWkOHDrXGjx9vlZWVWQUFBVafPn2s7Oxsu+bTTz+1unTpYs2bN886fvy4tXr1aisiIsIqKCiwazZs2GA5nU4rPz/fOnbsmDVz5kwrOjr6snfy/DU8XQUAQMdztefvVoecHTt2WJKs8vLyy/qaXwbYo0cPq0uXLtaDDz5onT59OqTms88+syZMmGBFRUVZvXv3tp5++ukWXwY4cuRIy+FwWDfddFOLLwP8zW9+Y/Xv399yOBzWmDFjrL1797b2UAg5AAB0QFd7/g6zLMtq10tJ7SgYDMrtdisQCPAyQAAAOoirPX/zBzoBAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIx0TX/WAQAAQJIGLtx+xb7PFqdese9vgSs5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGCkVoecP/3pT/rnf/5n9erVS1FRURo2bJgOHjxo91uWpZycHPXt21dRUVFKSUnRxx9/HDLGV199pWnTpsnlcik6OlozZszQuXPnQmo++ugj3X333YqMjFR8fLyWLFly2Vw2b96swYMHKzIyUsOGDdM777zT2sMBAACGalXI+frrr3XnnXeqc+fOevfdd3Xs2DG99NJL6tGjh12zZMkSrVy5Unl5edq3b5+6du0qn8+nCxcu2DXTpk3T0aNHVVhYqG3btmn37t2aOXOm3R8MBjV+/HgNGDBApaWlevHFF/Xcc8/plVdesWv27Nmjhx9+WDNmzNCHH36oiRMnauLEiTpy5Mj1rAcAADBEmGVZ1tUWL1y4UB988IH+8z//s8V+y7IUFxenp59+Ws8884wkKRAIKDY2Vvn5+Zo6daqOHz+uxMREHThwQKNHj5YkFRQU6P7779cXX3yhuLg4rVmzRj//+c/l9/vlcDjsfW/dulUnTpyQJE2ZMkW1tbXatm2bvf+xY8dq5MiRysvLu6rjCQaDcrvdCgQCcrlcV7sMAADg/xu4cPsV+z5bnHpD9nm15+9WXcn5/e9/r9GjR+vHP/6xYmJidNttt+nVV1+1+0+ePCm/36+UlBR7m9vtVnJyskpKSiRJJSUlio6OtgOOJKWkpCg8PFz79u2za+655x474EiSz+dTeXm5vv76a7vm0v001zTvpyV1dXUKBoMhDQAAmKlVIefTTz/VmjVrdPPNN2vHjh2aPXu2/uVf/kXr1q2TJPn9fklSbGxsyPdiY2PtPr/fr5iYmJD+Tp06qWfPniE1LY1x6T6uVNPc35Lc3Fy53W67xcfHt+bwAQBAB9KqkNPU1KRRo0bphRde0G233aaZM2fqiSeeuOpfD7W37OxsBQIBu1VWVrb3lAAAwA3SqpDTt29fJSYmhmwbMmSIKioqJEkej0eSVFVVFVJTVVVl93k8HlVXV4f0X7x4UV999VVITUtjXLqPK9U097fE6XTK5XKFNAAAYKZWhZw777xT5eXlIdv++7//WwMGDJAkJSQkyOPxqKioyO4PBoPat2+fvF6vJMnr9aqmpkalpaV2zc6dO9XU1KTk5GS7Zvfu3WpoaLBrCgsLdcstt9hPcnm93pD9NNc07wcAAHy7dWpNcWZmpu644w698MIL+qd/+ift379fr7zyiv1od1hYmObOnatf//rXuvnmm5WQkKBf/OIXiouL08SJEyX9+crPD37wA/vXXA0NDcrIyNDUqVMVFxcnSfrJT36iX/7yl5oxY4YWLFigI0eOaMWKFVq2bJk9lzlz5uh73/ueXnrpJaWmpmrDhg06ePBgyGPm7ak97jYHAAD/p1Uh5/bbb9eWLVuUnZ2t559/XgkJCVq+fLmmTZtm18yfP1+1tbWaOXOmampqdNddd6mgoECRkZF2zVtvvaWMjAyNGzdO4eHhmjx5slauXGn3u91uvffee0pPT1dSUpJ69+6tnJyckHfp3HHHHVq/fr2effZZ/exnP9PNN9+srVu3aujQodezHgAAwBCtek+OaW7ke3K4kgMA+DYw5j05AAAAHQUhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADBSq0LOc889p7CwsJA2ePBgu//ChQtKT09Xr1691K1bN02ePFlVVVUhY1RUVCg1NVVdunRRTEyM5s2bp4sXL4bU7Nq1S6NGjZLT6dSgQYOUn59/2VxWr16tgQMHKjIyUsnJydq/f39rDgUAABiu1Vdybr31Vp0+fdpuf/jDH+y+zMxMvf3229q8ebOKi4t16tQpTZo0ye5vbGxUamqq6uvrtWfPHq1bt075+fnKycmxa06ePKnU1FTde++9Kisr09y5c/X4449rx44dds3GjRuVlZWlRYsW6dChQxoxYoR8Pp+qq6uvdR0AAIBhWh1yOnXqJI/HY7fevXtLkgKBgF5//XUtXbpU9913n5KSkrR27Vrt2bNHe/fulSS99957OnbsmN58802NHDlSEyZM0K9+9SutXr1a9fX1kqS8vDwlJCTopZde0pAhQ5SRkaGHHnpIy5Yts+ewdOlSPfHEE5o+fboSExOVl5enLl266I033miLNQEAAAZodcj5+OOPFRcXp5tuuknTpk1TRUWFJKm0tFQNDQ1KSUmxawcPHqz+/furpKREklRSUqJhw4YpNjbWrvH5fAoGgzp69Khdc+kYzTXNY9TX16u0tDSkJjw8XCkpKXbNldTV1SkYDIY0AABgplaFnOTkZOXn56ugoEBr1qzRyZMndffdd+vs2bPy+/1yOByKjo4O+U5sbKz8fr8kye/3hwSc5v7mvm+qCQaDOn/+vL788ks1Nja2WNM8xpXk5ubK7XbbLT4+vjWHDwAAOpBOrSmeMGGC/e/hw4crOTlZAwYM0KZNmxQVFdXmk2tr2dnZysrKsj8Hg0GCDgAAhrquR8ijo6P13e9+V5988ok8Ho/q6+tVU1MTUlNVVSWPxyNJ8ng8lz1t1fz5r9W4XC5FRUWpd+/eioiIaLGmeYwrcTqdcrlcIQ0AAJjpukLOuXPn9Mc//lF9+/ZVUlKSOnfurKKiIru/vLxcFRUV8nq9kiSv16vDhw+HPAVVWFgol8ulxMREu+bSMZprmsdwOBxKSkoKqWlqalJRUZFdAwAA0KqQ88wzz6i4uFifffaZ9uzZowcffFARERF6+OGH5Xa7NWPGDGVlZen9999XaWmppk+fLq/Xq7Fjx0qSxo8fr8TERD3yyCP6r//6L+3YsUPPPvus0tPT5XQ6JUmzZs3Sp59+qvnz5+vEiRN6+eWXtWnTJmVmZtrzyMrK0quvvqp169bp+PHjmj17tmprazV9+vQ2XBoAANCRteqenC+++EIPP/yw/ud//kd9+vTRXXfdpb1796pPnz6SpGXLlik8PFyTJ09WXV2dfD6fXn75Zfv7ERER2rZtm2bPni2v16uuXbsqLS1Nzz//vF2TkJCg7du3KzMzUytWrFC/fv302muvyefz2TVTpkzRmTNnlJOTI7/fr5EjR6qgoOCym5EBAMC3V5hlWVZ7T6K9BINBud1uBQKBNr8/Z+DC7Vfs+2xxapvuCwCA9tIe57urPX/zt6sAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAka4r5CxevFhhYWGaO3euve3ChQtKT09Xr1691K1bN02ePFlVVVUh36uoqFBqaqq6dOmimJgYzZs3TxcvXgyp2bVrl0aNGiWn06lBgwYpPz//sv2vXr1aAwcOVGRkpJKTk7V///7rORwAAGCQaw45Bw4c0G9/+1sNHz48ZHtmZqbefvttbd68WcXFxTp16pQmTZpk9zc2Nio1NVX19fXas2eP1q1bp/z8fOXk5Ng1J0+eVGpqqu69916VlZVp7ty5evzxx7Vjxw67ZuPGjcrKytKiRYt06NAhjRgxQj6fT9XV1dd6SAAAwCDXFHLOnTunadOm6dVXX1WPHj3s7YFAQK+//rqWLl2q++67T0lJSVq7dq327NmjvXv3SpLee+89HTt2TG+++aZGjhypCRMm6Fe/+pVWr16t+vp6SVJeXp4SEhL00ksvaciQIcrIyNBDDz2kZcuW2ftaunSpnnjiCU2fPl2JiYnKy8tTly5d9MYbb1zPegAAAENcU8hJT09XamqqUlJSQraXlpaqoaEhZPvgwYPVv39/lZSUSJJKSko0bNgwxcbG2jU+n0/BYFBHjx61a/5ybJ/PZ49RX1+v0tLSkJrw8HClpKTYNQAA4NutU2u/sGHDBh06dEgHDhy4rM/v98vhcCg6Ojpke2xsrPx+v11zacBp7m/u+6aaYDCo8+fP6+uvv1ZjY2OLNSdOnLji3Ovq6lRXV2d/DgaDf+VoAQBAR9WqKzmVlZWaM2eO3nrrLUVGRt6oOd0wubm5crvddouPj2/vKQEAgBukVSGntLRU1dXVGjVqlDp16qROnTqpuLhYK1euVKdOnRQbG6v6+nrV1NSEfK+qqkoej0eS5PF4LnvaqvnzX6txuVyKiopS7969FRER0WJN8xgtyc7OViAQsFtlZWVrDh8AAHQgrQo548aN0+HDh1VWVma30aNHa9q0afa/O3furKKiIvs75eXlqqiokNfrlSR5vV4dPnw45CmowsJCuVwuJSYm2jWXjtFc0zyGw+FQUlJSSE1TU5OKiorsmpY4nU65XK6QBgAAzNSqe3K6d++uoUOHhmzr2rWrevXqZW+fMWOGsrKy1LNnT7lcLj311FPyer0aO3asJGn8+PFKTEzUI488oiVLlsjv9+vZZ59Venq6nE6nJGnWrFlatWqV5s+fr8cee0w7d+7Upk2btH37dnu/WVlZSktL0+jRozVmzBgtX75ctbW1mj59+nUtCAAAMEOrbzz+a5YtW6bw8HBNnjxZdXV18vl8evnll+3+iIgIbdu2TbNnz5bX61XXrl2Vlpam559/3q5JSEjQ9u3blZmZqRUrVqhfv3567bXX5PP57JopU6bozJkzysnJkd/v18iRI1VQUHDZzcgAAODbKcyyLKu9J9FegsGg3G63AoFAm//qauDC7Vfs+2xxapvuCwCA9tIe57urPX/zt6sAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASK0KOWvWrNHw4cPlcrnkcrnk9Xr17rvv2v0XLlxQenq6evXqpW7dumny5MmqqqoKGaOiokKpqanq0qWLYmJiNG/ePF28eDGkZteuXRo1apScTqcGDRqk/Pz8y+ayevVqDRw4UJGRkUpOTtb+/ftbcygAAMBwrQo5/fr10+LFi1VaWqqDBw/qvvvu0wMPPKCjR49KkjIzM/X2229r8+bNKi4u1qlTpzRp0iT7+42NjUpNTVV9fb327NmjdevWKT8/Xzk5OXbNyZMnlZqaqnvvvVdlZWWaO3euHn/8ce3YscOu2bhxo7KysrRo0SIdOnRII0aMkM/nU3V19fWuBwAAMESYZVnW9QzQs2dPvfjii3rooYfUp08frV+/Xg899JAk6cSJExoyZIhKSko0duxYvfvuu/rhD3+oU6dOKTY2VpKUl5enBQsW6MyZM3I4HFqwYIG2b9+uI0eO2PuYOnWqampqVFBQIElKTk7W7bffrlWrVkmSmpqaFB8fr6eeekoLFy686rkHg0G53W4FAgG5XK7rWYbLDFy4/Yp9ny1ObdN9AQDQXtrjfHe15+9rviensbFRGzZsUG1trbxer0pLS9XQ0KCUlBS7ZvDgwerfv79KSkokSSUlJRo2bJgdcCTJ5/MpGAzaV4NKSkpCxmiuaR6jvr5epaWlITXh4eFKSUmxa66krq5OwWAwpAEAADO1OuQcPnxY3bp1k9Pp1KxZs7RlyxYlJibK7/fL4XAoOjo6pD42NlZ+v1+S5Pf7QwJOc39z3zfVBINBnT9/Xl9++aUaGxtbrGke40pyc3PldrvtFh8f39rDBwAAHUSrQ84tt9yisrIy7du3T7Nnz1ZaWpqOHTt2I+bW5rKzsxUIBOxWWVnZ3lMCAAA3SKfWfsHhcGjQoEGSpKSkJB04cEArVqzQlClTVF9fr5qampCrOVVVVfJ4PJIkj8dz2VNQzU9fXVrzl09kVVVVyeVyKSoqShEREYqIiGixpnmMK3E6nXI6na09ZAAA0AFd93tympqaVFdXp6SkJHXu3FlFRUV2X3l5uSoqKuT1eiVJXq9Xhw8fDnkKqrCwUC6XS4mJiXbNpWM01zSP4XA4lJSUFFLT1NSkoqIiuwYAAKBVV3Kys7M1YcIE9e/fX2fPntX69eu1a9cu7dixQ263WzNmzFBWVpZ69uwpl8ulp556Sl6vV2PHjpUkjR8/XomJiXrkkUe0ZMkS+f1+Pfvss0pPT7evsMyaNUurVq3S/Pnz9dhjj2nnzp3atGmTtm//v7u3s7KylJaWptGjR2vMmDFavny5amtrNX369DZcGgAA0JG1KuRUV1fr0Ucf1enTp+V2uzV8+HDt2LFD3//+9yVJy5YtU3h4uCZPnqy6ujr5fD69/PLL9vcjIiK0bds2zZ49W16vV127dlVaWpqef/55uyYhIUHbt29XZmamVqxYoX79+um1116Tz+eza6ZMmaIzZ84oJydHfr9fI0eOVEFBwWU3IwMAgG+v635PTkfGe3IAALg+Rr4nBwAA4O8ZIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwUqtCTm5urm6//XZ1795dMTExmjhxosrLy0NqLly4oPT0dPXq1UvdunXT5MmTVVVVFVJTUVGh1NRUdenSRTExMZo3b54uXrwYUrNr1y6NGjVKTqdTgwYNUn5+/mXzWb16tQYOHKjIyEglJydr//79rTkcAABgsFaFnOLiYqWnp2vv3r0qLCxUQ0ODxo8fr9raWrsmMzNTb7/9tjZv3qzi4mKdOnVKkyZNsvsbGxuVmpqq+vp67dmzR+vWrVN+fr5ycnLsmpMnTyo1NVX33nuvysrKNHfuXD3++OPasWOHXbNx40ZlZWVp0aJFOnTokEaMGCGfz6fq6urrWQ8AAGCIMMuyrGv98pkzZxQTE6Pi4mLdc889CgQC6tOnj9avX6+HHnpIknTixAkNGTJEJSUlGjt2rN5991398Ic/1KlTpxQbGytJysvL04IFC3TmzBk5HA4tWLBA27dv15EjR+x9TZ06VTU1NSooKJAkJScn6/bbb9eqVaskSU1NTYqPj9dTTz2lhQsXXtX8g8Gg3G63AoGAXC7XtS5DiwYu3H7Fvs8Wp7bpvgAAaC/tcb672vP3dd2TEwgEJEk9e/aUJJWWlqqhoUEpKSl2zeDBg9W/f3+VlJRIkkpKSjRs2DA74EiSz+dTMBjU0aNH7ZpLx2iuaR6jvr5epaWlITXh4eFKSUmxa1pSV1enYDAY0gAAgJmuOeQ0NTVp7ty5uvPOOzV06FBJkt/vl8PhUHR0dEhtbGys/H6/XXNpwGnub+77pppgMKjz58/ryy+/VGNjY4s1zWO0JDc3V263227x8fGtP3AAANAhXHPISU9P15EjR7Rhw4a2nM8NlZ2drUAgYLfKysr2nhIAALhBOl3LlzIyMrRt2zbt3r1b/fr1s7d7PB7V19erpqYm5GpOVVWVPB6PXfOXT0E1P311ac1fPpFVVVUll8ulqKgoRUREKCIiosWa5jFa4nQ65XQ6W3/AAACgw2nVlRzLspSRkaEtW7Zo586dSkhICOlPSkpS586dVVRUZG8rLy9XRUWFvF6vJMnr9erw4cMhT0EVFhbK5XIpMTHRrrl0jOaa5jEcDoeSkpJCapqamlRUVGTXAACAb7dWXclJT0/X+vXr9bvf/U7du3e3739xu92KioqS2+3WjBkzlJWVpZ49e8rlcumpp56S1+vV2LFjJUnjx49XYmKiHnnkES1ZskR+v1/PPvus0tPT7asss2bN0qpVqzR//nw99thj2rlzpzZt2qTt2//vDu6srCylpaVp9OjRGjNmjJYvX67a2lpNnz69rdYGAAB0YK0KOWvWrJEk/eM//mPI9rVr1+qnP/2pJGnZsmUKDw/X5MmTVVdXJ5/Pp5dfftmujYiI0LZt2zR79mx5vV517dpVaWlpev755+2ahIQEbd++XZmZmVqxYoX69eun1157TT6fz66ZMmWKzpw5o5ycHPn9fo0cOVIFBQWX3YwMAAC+na7rPTkdHe/JAQDg+hj7nhwAAIC/V4QcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEitDjm7d+/Wj370I8XFxSksLExbt24N6bcsSzk5Oerbt6+ioqKUkpKijz/+OKTmq6++0rRp0+RyuRQdHa0ZM2bo3LlzITUfffSR7r77bkVGRio+Pl5Lliy5bC6bN2/W4MGDFRkZqWHDhumdd95p7eEAAABDtTrk1NbWasSIEVq9enWL/UuWLNHKlSuVl5enffv2qWvXrvL5fLpw4YJdM23aNB09elSFhYXatm2bdu/erZkzZ9r9wWBQ48eP14ABA1RaWqoXX3xRzz33nF555RW7Zs+ePXr44Yc1Y8YMffjhh5o4caImTpyoI0eOtPaQAACAgcIsy7Ku+cthYdqyZYsmTpwo6c9XceLi4vT000/rmWeekSQFAgHFxsYqPz9fU6dO1fHjx5WYmKgDBw5o9OjRkqSCggLdf//9+uKLLxQXF6c1a9bo5z//ufx+vxwOhyRp4cKF2rp1q06cOCFJmjJlimpra7Vt2zZ7PmPHjtXIkSOVl5d3VfMPBoNyu90KBAJyuVzXugwtGrhw+xX7Pluc2qb7AgCgvbTH+e5qz99tek/OyZMn5ff7lZKSYm9zu91KTk5WSUmJJKmkpETR0dF2wJGklJQUhYeHa9++fXbNPffcYwccSfL5fCovL9fXX39t11y6n+aa5v20pK6uTsFgMKQBAAAztWnI8fv9kqTY2NiQ7bGxsXaf3+9XTExMSH+nTp3Us2fPkJqWxrh0H1eqae5vSW5urtxut93i4+Nbe4gAAKCD+FY9XZWdna1AIGC3ysrK9p4SAAC4Qdo05Hg8HklSVVVVyPaqqiq7z+PxqLq6OqT/4sWL+uqrr0JqWhrj0n1cqaa5vyVOp1MulyukAQAAM7VpyElISJDH41FRUZG9LRgMat++ffJ6vZIkr9ermpoalZaW2jU7d+5UU1OTkpOT7Zrdu3eroaHBriksLNQtt9yiHj162DWX7qe5pnk/AADg263VIefcuXMqKytTWVmZpD/fbFxWVqaKigqFhYVp7ty5+vWvf63f//73Onz4sB599FHFxcXZT2ANGTJEP/jBD/TEE09o//79+uCDD5SRkaGpU6cqLi5OkvSTn/xEDodDM2bM0NGjR7Vx40atWLFCWVlZ9jzmzJmjgoICvfTSSzpx4oSee+45HTx4UBkZGde/KgAAoMPr1NovHDx4UPfee6/9uTl4pKWlKT8/X/Pnz1dtba1mzpypmpoa3XXXXSooKFBkZKT9nbfeeksZGRkaN26cwsPDNXnyZK1cudLud7vdeu+995Senq6kpCT17t1bOTk5Ie/SueOOO7R+/Xo9++yz+tnPfqabb75ZW7du1dChQ69pIQAAgFmu6z05HR3vyQEA4Pp8a96TAwAA8PeCkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYqcOHnNWrV2vgwIGKjIxUcnKy9u/f395TAgAAfwc6dMjZuHGjsrKytGjRIh06dEgjRoyQz+dTdXV1e08NAAC0sw4dcpYuXaonnnhC06dPV2JiovLy8tSlSxe98cYb7T01AADQzjq19wSuVX19vUpLS5WdnW1vCw8PV0pKikpKSlr8Tl1dnerq6uzPgUBAkhQMBtt8fk11/3vFvub9DV2044o1R37pa9MaAMC3x9/y/HI157u21jyuZVnfXGh1UH/6058sSdaePXtCts+bN88aM2ZMi99ZtGiRJYlGo9FoNJoBrbKy8huzQoe9knMtsrOzlZWVZX9uamrSV199pV69eiksLOyaxgwGg4qPj1dlZaVcLldbTRWXYI1vLNb3xmONbzzW+Mb7e1pjy7J09uxZxcXFfWNdhw05vXv3VkREhKqqqkK2V1VVyePxtPgdp9Mpp9MZsi06OrpN5uNyudr9f7rpWOMbi/W98VjjG481vvH+XtbY7Xb/1ZoOe+Oxw+FQUlKSioqK7G1NTU0qKiqS1+ttx5kBAIC/Bx32So4kZWVlKS0tTaNHj9aYMWO0fPly1dbWavr06e09NQAA0M46dMiZMmWKzpw5o5ycHPn9fo0cOVIFBQWKjY39m83B6XRq0aJFl/0aDG2HNb6xWN8bjzW+8VjjG68jrnGYZf21568AAAA6ng57Tw4AAMA3IeQAAAAjEXIAAICRCDkAAMBIhJzrsHr1ag0cOFCRkZFKTk7W/v3723tKHdbu3bv1ox/9SHFxcQoLC9PWrVtD+i3LUk5Ojvr27auoqCilpKTo448/bp/JdlC5ubm6/fbb1b17d8XExGjixIkqLy8Pqblw4YLS09PVq1cvdevWTZMnT77shZto2Zo1azR8+HD7RWler1fvvvuu3c/atr3FixcrLCxMc+fOtbexztfnueeeU1hYWEgbPHiw3d/R1peQc402btyorKwsLVq0SIcOHdKIESPk8/lUXV3d3lPrkGprazVixAitXr26xf4lS5Zo5cqVysvL0759+9S1a1f5fD5duHDhbzzTjqu4uFjp6enau3evCgsL1dDQoPHjx6u2ttauyczM1Ntvv63NmzeruLhYp06d0qRJk9px1h1Hv379tHjxYpWWlurgwYO677779MADD+jo0aOSWNu2duDAAf32t7/V8OHDQ7azztfv1ltv1enTp+32hz/8we7rcOvbJn8t81tozJgxVnp6uv25sbHRiouLs3Jzc9txVmaQZG3ZssX+3NTUZHk8HuvFF1+0t9XU1FhOp9P613/913aYoRmqq6stSVZxcbFlWX9e086dO1ubN2+2a44fP25JskpKStprmh1ajx49rNdee421bWNnz561br75ZquwsND63ve+Z82ZM8eyLH6G28KiRYusESNGtNjXEdeXKznXoL6+XqWlpUpJSbG3hYeHKyUlRSUlJe04MzOdPHlSfr8/ZL3dbreSk5NZ7+sQCAQkST179pQklZaWqqGhIWSdBw8erP79+7POrdTY2KgNGzaotrZWXq+XtW1j6enpSk1NDVlPiZ/htvLxxx8rLi5ON910k6ZNm6aKigpJHXN9O/Qbj9vLl19+qcbGxsverBwbG6sTJ06006zM5ff7JanF9W7uQ+s0NTVp7ty5uvPOOzV06FBJf15nh8Nx2R+tZZ2v3uHDh+X1enXhwgV169ZNW7ZsUWJiosrKyljbNrJhwwYdOnRIBw4cuKyPn+Hrl5ycrPz8fN1yyy06ffq0fvnLX+ruu+/WkSNHOuT6EnKAb6H09HQdOXIk5HftuH633HKLysrKFAgE9G//9m9KS0tTcXFxe0/LGJWVlZozZ44KCwsVGRnZ3tMx0oQJE+x/Dx8+XMnJyRowYIA2bdqkqKiodpzZteHXVdegd+/eioiIuOyO8qqqKnk8nnaalbma15T1bhsZGRnatm2b3n//ffXr18/e7vF4VF9fr5qampB61vnqORwODRo0SElJScrNzdWIESO0YsUK1raNlJaWqrq6WqNGjVKnTp3UqVMnFRcXa+XKlerUqZNiY2NZ5zYWHR2t7373u/rkk0865M8xIecaOBwOJSUlqaioyN7W1NSkoqIieb3edpyZmRISEuTxeELWOxgMat++fax3K1iWpYyMDG3ZskU7d+5UQkJCSH9SUpI6d+4css7l5eWqqKhgna9RU1OT6urqWNs2Mm7cOB0+fFhlZWV2Gz16tKZNm2b/m3VuW+fOndMf//hH9e3bt2P+HLf3nc8d1YYNGyyn02nl5+dbx44ds2bOnGlFR0dbfr+/vafWIZ09e9b68MMPrQ8//NCSZC1dutT68MMPrc8//9yyLMtavHixFR0dbf3ud7+zPvroI+uBBx6wEhISrPPnz7fzzDuO2bNnW26329q1a5d1+vRpu/3v//6vXTNr1iyrf//+1s6dO62DBw9aXq/X8nq97TjrjmPhwoVWcXGxdfLkSeujjz6yFi5caIWFhVnvvfeeZVms7Y1y6dNVlsU6X6+nn37a2rVrl3Xy5Enrgw8+sFJSUqzevXtb1dXVlmV1vPUl5FyH3/zmN1b//v0th8NhjRkzxtq7d297T6nDev/99y1Jl7W0tDTLsv78GPkvfvELKzY21nI6nda4ceOs8vLy9p10B9PS+kqy1q5da9ecP3/eevLJJ60ePXpYXbp0sR588EHr9OnT7TfpDuSxxx6zBgwYYDkcDqtPnz7WuHHj7IBjWaztjfKXIYd1vj5Tpkyx+vbtazkcDusf/uEfrClTpliffPKJ3d/R1jfMsiyrfa4hAQAA3DjckwMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkf4fbCxqFrJvN5EAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.bar(\n", + " range(1, ensemble.shape[0]+2),\n", + " hist[0]\n", + ")\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (climate)", + "language": "python", + "name": "climate" + }, + "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.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/Quickstart.ipynb b/notebooks/Quickstart.ipynb new file mode 100644 index 00000000..2302c4ca --- /dev/null +++ b/notebooks/Quickstart.ipynb @@ -0,0 +1,3812 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "XxiostdVuW6X" + }, + "source": [ + "# ClimateLearn Quickstart\n", + "\n", + "This notebook shows how to develop a weather forecasting model with deep learning in ClimateLearn from end-to-end. First, we install the library from the GitHub repository." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "eMDZ4vfZirax", + "outputId": "acbb7ee7-f0fb-4bc2-b3e3-929c51d32a42" + }, + "outputs": [], + "source": [ + "!pip install climate-learn" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "E0o_tgqHumKg" + }, + "source": [ + "Mount the Google Drive file system, then import ClimateLearn and related libraries." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "UB89ASOWi3pN", + "outputId": "547dbbd2-d63c-42e5-8b8f-8e3eabbe9d6b" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mounted at /content/drive\n" + ] + } + ], + "source": [ + "from google.colab import drive\n", + "drive.mount(\"/content/drive\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "z-R_gkMGjkFJ" + }, + "outputs": [], + "source": [ + "import climate_learn as cl\n", + "from climate_learn.data.processing.nc2npz import convert_nc2npz\n", + "from IPython.display import HTML\n", + "import pytorch_lightning as pl\n", + "from pytorch_lightning.callbacks import (\n", + " EarlyStopping,\n", + " ModelCheckpoint,\n", + " RichModelSummary,\n", + " RichProgressBar\n", + ")\n", + "from pytorch_lightning.loggers.tensorboard import TensorBoardLogger\n", + "import torch" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "_4T9dgInu_No" + }, + "source": [ + "Download temperature and geopotential data from the ERA5 dataset. For more info about ERA5, please see these links:\n", + "- https://rmets.onlinelibrary.wiley.com/doi/full/10.1002/qj.3803\n", + "- https://en.wikipedia.org/wiki/ECMWF_re-analysis\n", + "\n", + "Since temperature and geopotential are available at many different pressure levels ([what are pressure levels?](https://en.wikipedia.org/wiki/Atmospheric_pressure)), the complete ERA5 data for these two variables is very large and will take a long time to download. Moreover, it will require more storage space than is allotted to a free Google Drive account. For the sake of this quickstart, we will use pre-processed data provided by [WeatherBench](https://mediatum.ub.tum.de/1524895) for temperature at 850 hPa and geopotential at 500 hPa, which are called `temperature_850` and `geopotential_500`. Even at just one pressure level for each variable, the following code cells will still take a couple minutes to run, so we do not recommend you do not use hardware acceleration since [Google Colab has usage limits for GPU](https://stackoverflow.com/a/66142367)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "FzmMM2oVjcqq", + "outputId": "ec83ca15-f9ce-46d0-a17a-11efd7f329b1" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.10/dist-packages/urllib3/connectionpool.py:1056: InsecureRequestWarning: Unverified HTTPS request is being made to host 'dataserv.ub.tum.de'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html#ssl-warnings\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "cl.data.download_weatherbench(\n", + " dst=\"/content/drive/MyDrive/ClimateLearn/temperature\",\n", + " dataset=\"era5\",\n", + " variable=\"temperature_850\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "kqyCLX0gzEW-", + "outputId": "84f7bc21-4617-4db0-85f8-e8c920b52ea3" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.10/dist-packages/urllib3/connectionpool.py:1056: InsecureRequestWarning: Unverified HTTPS request is being made to host 'dataserv.ub.tum.de'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html#ssl-warnings\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "cl.data.download_weatherbench(\n", + " dst=\"/content/drive/MyDrive/ClimateLearn/geopotential\",\n", + " dataset=\"era5\",\n", + " variable=\"geopotential_500\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "Kb_VffKBwUy8" + }, + "source": [ + "The following function call processes the WeatherBench ERA5 data into a form that is easily ingestable for PyTorch models and defines the training-validation-testing splits. In addition, we shard the data to create sets of smaller files rather than one large file for each split." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LHQnPpBclliL", + "outputId": "ff7be932-2ae7-44e3-a136-f15b116c5fe8" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 36/36 [01:22<00:00, 2.31s/it]\n", + "100%|██████████| 2/2 [00:04<00:00, 2.29s/it]\n", + "100%|██████████| 1/1 [00:02<00:00, 2.20s/it]\n" + ] + } + ], + "source": [ + "convert_nc2npz(\n", + " root_dir=\"/content/drive/MyDrive/ClimateLearn\",\n", + " save_dir=\"/content/drive/MyDrive/ClimateLearn/processed\",\n", + " variables=[\"temperature\", \"geopotential\"],\n", + " start_train_year=1979,\n", + " start_val_year=2015,\n", + " start_test_year=2017,\n", + " end_year=2018,\n", + " num_shards=16\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "83FYCcuaxKE-" + }, + "source": [ + "The downloaded and processed data is loaded into a PyTorch Lightning data module. In the following code cell, we use the following settings:\n", + "- `subsample = 6`. The dataset is subsampled at 6 hour intervals; this is done so that training is faster, but one could also use no subsampling (_i.e._, `subsample = 1`, which is the default).\n", + "- `pred_range = 24`. The model's objective is to predict `2m_temperature` 24 hours in the future.\n", + "- `history = 3`. When making a prediction, the model is given data at time `t`, `t-subsample`, and `t-subsample*2`.\n", + "- `task = \"direct-forecasting\"`. Given the inputs, the model directly predicts the outputs at `pred_range`. Other methods of forecasting are iterative forecasting and continuous forecasting. We refer to section 3 of [this paper by Rasp and Theurey](https://arxiv.org/pdf/2008.08626.pdf) for a description of these forecasting types.\n", + "\n", + "Note further that `in_vars` and `out_vars` are the same, meaning the model consumes historical temperature and geopotential as input and produces predicted temperature and geopotential as output.\n", + "\n", + "Before running this next code cell, we recommend switching to a GPU-accelerated runtime then re-running all code cells related to installation and library imports. You do _NOT_ need to re-download/process the data. Those should be saved to your Google Drive." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "HZ_9mMR4kA49" + }, + "outputs": [], + "source": [ + "dm = cl.data.IterDataModule(\n", + " task=\"direct-forecasting\",\n", + " inp_root_dir=\"/content/drive/MyDrive/ClimateLearn/processed\",\n", + " out_root_dir=\"/content/drive/MyDrive/ClimateLearn/processed\",\n", + " in_vars=[\"temperature\", \"geopotential\"],\n", + " out_vars=[\"temperature\", \"geopotential\"],\n", + " src=\"era5\",\n", + " subsample=6,\n", + " pred_range=24,\n", + " history=3,\n", + " batch_size=32\n", + ")\n", + "dm.setup()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "5w5eu-uzy097" + }, + "source": [ + "Run baseline methods, climatology and persistence. Climatology uses the average value observed in the training data as its predictions. Persistence uses the last observed value as its predictio. For our setup, that would mean using the values of `2m_temperature` and `toa_incident_solar_radiation` at time `t` as the predictions for time `t+24`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "46913ebf2920448ab7e4421fff41e7bd", + "a83b8bb369174edf999ad804cb1a9c4f", + "2f97f73cab694612bb3151797ab85d50", + "ce082e1180e546d0a45676e112673f02", + "96cf4617cf7f44929754d7f5390ecde9", + "339c3a61ba8f47d89cdb437baea61056", + "513a2a318bbe4c448835337736ebe9d5", + "6c4afba98d934616b3fd5f277140db49", + "7c956ab21a9e43dd88cb28247622cd3f", + "1da5c642bf634cb5b35ccba3619ff4c9", + "e5be2704139e488392222b0e68453d58", + "97668a06aa65408bb09f8c85e67dfd1c", + "4f9ecc20290a4adb9be40faa1b44a056", + "5e0fa8d1d3f444d288dba5f75a078f63", + "64da7f47dc7f404bbedee37a18b3d4ed", + "16e44229fb724c3f85bc0b6f7dd86c45", + "13abcd9a2e724e059e6d4c538ffe889a", + "ac0fc5f828644428927b4df101958b9b", + "f1f44758bf1742909f3d207875d25007", + "ab5a4140314e4d4ca64b6a34050572d4", + "c438da1a4c064403a237be437a105583", + "eeb7d4a7fd2f491b9d150709f85d344f" + ] + }, + "id": "PsOEY44RklR6", + "outputId": "ba3a1f3d-7850-4a4e-c4bf-9ec377e50f5a" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading architecture: climatology\n", + "Using optimizer associated with architecture\n", + "Using learning rate scheduler associated with architecture\n", + "Loading training loss: lat_mse\n", + "No train transform\n", + "Loading validation loss: lat_rmse\n", + "Loading validation loss: lat_acc\n", + "Loading validation loss: lat_mse\n", + "Loading validation transform: denormalize\n", + "Loading validation transform: denormalize\n", + "No validation transform\n", + "Loading test loss: lat_rmse\n", + "Loading test loss: lat_acc\n", + "Loading test transform: denormalize\n", + "Loading test transform: denormalize\n", + "Loading architecture: persistence\n", + "Using optimizer associated with architecture\n", + "Using learning rate scheduler associated with architecture\n", + "Loading training loss: lat_mse\n", + "No train transform\n", + "Loading validation loss: lat_rmse\n", + "Loading validation loss: lat_acc\n", + "Loading validation loss: lat_mse\n", + "Loading validation transform: denormalize\n", + "Loading validation transform: denormalize\n", + "No validation transform\n", + "Loading test loss: lat_rmse\n", + "Loading test loss: lat_acc\n", + "Loading test transform: denormalize\n", + "Loading test transform: denormalize\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:pytorch_lightning.utilities.rank_zero:GPU available: True (cuda), used: True\n", + "INFO:pytorch_lightning.utilities.rank_zero:TPU available: False, using: 0 TPU cores\n", + "INFO:pytorch_lightning.utilities.rank_zero:IPU available: False, using: 0 IPUs\n", + "INFO:pytorch_lightning.utilities.rank_zero:HPU available: False, using: 0 HPUs\n", + "WARNING:pytorch_lightning.loggers.tensorboard:Missing logger folder: /content/lightning_logs\n", + "INFO:pytorch_lightning.accelerators.cuda:LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "46913ebf2920448ab7e4421fff41e7bd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Testing: 0it [00:00, ?it/s]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
+              "┃        Test metric                 DataLoader 0        ┃\n",
+              "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
+              "│   test/lat_acc:aggregate       0.002286637867969616    │\n",
+              "│ test/lat_acc:geopotential     -0.0020452924620714425   │\n",
+              "│  test/lat_acc:temperature      0.006618568198010681    │\n",
+              "│  test/lat_rmse:aggregate        534.9848123508632      │\n",
+              "│ test/lat_rmse:geopotential      1064.5965083803287     │\n",
+              "│ test/lat_rmse:temperature       5.373116321397814      │\n",
+              "└────────────────────────────┴────────────────────────────┘\n",
+              "
\n" + ], + "text/plain": [ + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1m Test metric \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m DataLoader 0 \u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", + "│\u001b[36m \u001b[0m\u001b[36m test/lat_acc:aggregate \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.002286637867969616 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mtest/lat_acc:geopotential \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m -0.0020452924620714425 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36m test/lat_acc:temperature \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.006618568198010681 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36m test/lat_rmse:aggregate \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 534.9848123508632 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mtest/lat_rmse:geopotential\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 1064.5965083803287 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mtest/lat_rmse:temperature \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 5.373116321397814 \u001b[0m\u001b[35m \u001b[0m│\n", + "└────────────────────────────┴────────────────────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:pytorch_lightning.accelerators.cuda:LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "97668a06aa65408bb09f8c85e67dfd1c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Testing: 0it [00:00, ?it/s]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
+              "┃        Test metric                 DataLoader 0        ┃\n",
+              "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
+              "│   test/lat_acc:aggregate        0.8103965440378824     │\n",
+              "│ test/lat_acc:geopotential       0.8191593687462576     │\n",
+              "│  test/lat_acc:temperature       0.8016337193295074     │\n",
+              "│  test/lat_rmse:aggregate        303.2970449929333      │\n",
+              "│ test/lat_rmse:geopotential      603.4956555367604      │\n",
+              "│ test/lat_rmse:temperature       3.0984344491063727     │\n",
+              "└────────────────────────────┴────────────────────────────┘\n",
+              "
\n" + ], + "text/plain": [ + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1m Test metric \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m DataLoader 0 \u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", + "│\u001b[36m \u001b[0m\u001b[36m test/lat_acc:aggregate \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.8103965440378824 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mtest/lat_acc:geopotential \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.8191593687462576 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36m test/lat_acc:temperature \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.8016337193295074 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36m test/lat_rmse:aggregate \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 303.2970449929333 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mtest/lat_rmse:geopotential\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 603.4956555367604 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mtest/lat_rmse:temperature \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 3.0984344491063727 \u001b[0m\u001b[35m \u001b[0m│\n", + "└────────────────────────────┴────────────────────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[{'test/lat_rmse:temperature': 3.0984344491063727,\n", + " 'test/lat_rmse:geopotential': 603.4956555367604,\n", + " 'test/lat_rmse:aggregate': 303.2970449929333,\n", + " 'test/lat_acc:temperature': 0.8016337193295074,\n", + " 'test/lat_acc:geopotential': 0.8191593687462576,\n", + " 'test/lat_acc:aggregate': 0.8103965440378824}]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "climatology = cl.load_forecasting_module(\n", + " data_module=dm, architecture=\"climatology\"\n", + ")\n", + "persistence = cl.load_forecasting_module(\n", + " data_module=dm, architecture=\"persistence\"\n", + ")\n", + "\n", + "trainer = pl.Trainer()\n", + "trainer.test(climatology, dm)\n", + "trainer.test(persistence, dm)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "VkNg5SrUzOeV" + }, + "source": [ + "ClimateLearn provides standard metrics. For forecasting, it displays the latitude weighted RMSE and the latitude weighted ACC. Lower RMSE is better, while higher ACC is better. ACC has a range of [0, 1]. We use latitude weighting to adjust for the fact that we flatten the curved surface of the Earth to a 2D grid, which is squishes information at the equator and stretches information near the poles. For more info about these metrics, see this link: https://geo.libretexts.org/Bookshelves/Meteorology_and_Climate_Science/Practical_Meteorology_(Stull)/20%3A_Numerical_Weather_Prediction_(NWP)/20.7%3A_Forecast_Quality_and_Verfication\n", + "\n", + "Also, you might have noticed the metrics with `aggregate` as the suffix. These represent averages. For example, `lat_rmse:aggregate` is the average of `lat_rmse:temperature` and `lat_rmse:geopotential`.\n", + "\n", + "Besides these metrics, ClimateLearn also provides visualization tools. In the following cell, we first get the denormalization tranfsorm to transform the data returned by the PyTorch Lightning data module, which was normalized to $\\mathcal{N}(0,1)$, back into its original range. As we can see the logging messages displayed in the previous cell's output, the persistence model's 0-th test tranfsormation is denormalization.\n", + "\n", + "Then, we visualize the ground truth, prediction, and bias for the persistence prediction made on the 0-th sample of the testing set. Bias is defined as predicted minus observed (see the link provided above). It is useful to gain a visual understanding of model performance. In this example, we can see that persistence generally underpredicts the true values.\n", + "\n", + "For weather forecasting with history greater than 1, the visualization function also returns a value which we save here as `in_graphic`. This graphic can be animated, as seen in the next code cell." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "Yz9jaapwzlTd", + "outputId": "536397b1-4074-4fa7-8953-58a0e0aa36cd" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "0it [00:00, ?it/s]\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAo0AAAFJCAYAAAD+PBdyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB5OklEQVR4nO3deXhTVeI+8DdJm3TfaEuplLLJvgmOnSIgIlKWQRFXRC1YQByKSl2AEaGIIwiCqCDoKIsO/AQcRUUGLSCgUlZBFgUtgqwFLHRfkibn9wffZgjNPSdpaEvJ+3me+2hzzr059+Tm5nCX9+qEEAJERERERBL62m4AEREREV37OGgkIiIiIiUOGomIiIhIiYNGIiIiIlLioJGIiIiIlDhoJCIiIiIlDhqJiIiISImDRiIiIiJS4qCRiIiIiJQ4aCSqBjqdDunp6bXdDE2NGzfG3/72t9puBrmgqtvSpk2boNPpsGnTJpfqz5w5E61atYLNZnP7vaiyhQsXolGjRigrK6vtphBdNRw0Uq05evQoUlNT0aJFCwQEBCAgIABt2rTBmDFjsG/fvtpuXrU7ffo00tPTsXfv3mpZ/s8//4z09HQcO3asWpZ/vXvnnXewZMmSGnmvtWvX1uo/MvLz8/Haa69h/Pjx0Our52dBCIFHH30UOp0OCQkJKC4udlqvuLgY8+fPR58+fdCgQQMEBwfjpptuwoIFC2C1WjWX36VLF/z973+vUtvMZjN69eoFnU6Hu+++W/N9cnJyMGvWLPTo0QNRUVEICwvDX//6V6xYsaJS3WHDhsFsNuPdd9+tUpuIrkmCqBZ8+eWXIiAgQISEhIgnn3xSLFy4ULz33nsiLS1NNG7cWOh0OnHs2LHabmaVARBTpkyR1tm5c6cAIBYvXlwtbVi1apUAIL799ttKZfHx8WLAgAHV8r7Xi7Zt24rbbrutRt5rzJgxQmt3XFJSIiwWi9vL/PbbbzU//yu98cYbIiQkRJSUlLj9Pq6aMGGCACD69+8v9Hq9GDhwoCgvL69Ub//+/UKn04nevXuLmTNnioULF4p77rlHABCPPfaY02WfPn1a6HQ6sWbNGrfbZbPZxMMPPywAiAEDBggA4u9//7vTul9++aXw9fUVd999t5g7d66YN2+euP322wUAMXny5Er1X3jhBREfHy9sNpvb7SK6FnHQSDUuKytLBAYGitatW4vTp09XKrdYLOLNN98Ux48fly6nsLCwuproseoYNBYVFbnVBg4aPXOtDBqryp1BY4cOHcQjjzxyVd//cgsWLBAAxPjx44UQQixdulTo9XoxevToSnXPnz8vDhw4UOn14cOHCwDit99+q1T2wQcfCH9/f1FcXOx228aPHy90Op2YP3++EEKIadOmCQBixowZler+/vvvlf4xa7PZRK9evYTJZKq0T9q1a5cAIDZs2OB2u4iuRRw0Uo0bNWqUACC2bdvm8jzJyckiMDBQZGVliX79+omgoCBx9913CyEuDR7T0tJEw4YNhdFoFC1atBCzZs1y+Nf90aNHNQdoVw7wpkyZYv9xSk5OFqGhoSIkJEQMGzas0sCttLRUPPPMMyIyMlIEBQWJgQMHihMnTigHjRU/6FdOFe277bbbRNu2bcWuXbtE9+7dhb+/v3j66aedtrdCfHy8SE5OFkIIsXjxYqfLrxhAVAwav/vuO/GXv/xFmEwm0aRJE7F06dJKy83KyhJZWVma63K5n376SfTo0UP4+fmJG264QUybNk0sWrRIABBHjx51qLt27VrRrVs3ERAQIIKCgkT//v2dDhY2bNhgrxcaGiruuusu8fPPPzvUqfjMfvnlF3H//feL4OBgERERIZ566qlKR88sFot4+eWXRdOmTYXRaBTx8fFi4sSJorS01KEvr+y7yweQFy9eFE8//bR9m2vWrJmYMWOGsFqt9joV29ysWbPEu+++a3+/m2++WezYscNeLzk52elnVeHKz/vYsWPiySefFC1atBB+fn4iIiJC3HfffZX619VB4++//y4AiCVLlji8fnn7582bJ5o0aSL8/f3FnXfeKY4fPy5sNpt4+eWXxQ033CD8/PzEXXfdJXJyciot/4svvhAGg0FMnDjR4fUPP/xQ6PV68eqrr0rbd/lyAIgvvviiUtngwYNF//797X//+uuvYvDgwaJ+/frCZDKJG264QTz44IMiNzfXYb758+cLnU4nFixY4PD6K6+8InQ6nVi+fLlLbXvrrbcEALFv375KZRXbIdH1wOeqnusmcsGaNWvQvHlzJCQkuDVfeXk5kpKS0K1bN7z++usICAiAEAJ33XUXvv32W6SkpKBTp074+uuv8fzzz+PUqVN44403qtzOBx54AE2aNMH06dPx448/4v3330d0dDRee+01e50RI0bg3//+Nx5++GF07doVGzduxIABA5TLbt26NV5++WVMnjwZo0aNQvfu3QEAXbt2tdfJyclBv3798NBDD+GRRx5B/fr1XW57jx498NRTT+Gtt97CP/7xD7Ru3dr+vhWysrJw3333ISUlBcnJyVi0aBGGDRuGLl26oG3btvZ6d9xxBwAor408deoUbr/9duh0OkycOBGBgYF4//33YTKZKtX96KOPkJycjKSkJLz22msoLi7GggUL0K1bN+zZsweNGzcGAKxfvx79+vVD06ZNkZ6ejpKSErz99tu49dZb8eOPP9rrVXjggQfQuHFjTJ8+Hdu2bcNbb72Fixcv4sMPP7TXGTFiBJYuXYr77rsPzz77LLZv347p06fjl19+wWeffQYAmDt3LsaOHYugoCC8+OKLAGDv/+LiYtx22204deoUnnjiCTRq1Ahbt27FxIkTcebMGcydO9ehTcuXL0dBQQGeeOIJ6HQ6zJw5E4MHD8bvv/8OX19fPPHEEzh9+jQyMjLw0UcfSfsYAHbu3ImtW7fioYceQsOGDXHs2DEsWLAAPXv2xM8//4yAgADlMi63detWAEDnzp2dli9btgxmsxljx47FhQsXMHPmTDzwwAPo1asXNm3ahPHjxyMrKwtvv/02nnvuOSxatMihrQ899BAmTJiAV155xWG5Fdc3Dh8+HHFxcXjkkUek7czOzgYAREZGOrxusViwfv16vPrqqwAuXZ+YlJSEsrIyjB07FjExMTh16hTWrFmD3NxchIaGAgC++OILPP3001i4cCFGjRrlsMwXX3wRer0ew4YNQ0xMDG6//fYqtQ241K8//PCDdH6iOqO2R63kXfLy8gQAMWjQoEplFy9eFOfPn7dPl59qqjgaM2HCBId5Vq9eLQCIV155xeH1++67T+h0OvsRsqocaXz88ccd6t1zzz2iXr169r/37t3r9PqniuujPDk9fdtttwkAYuHChcr2Vrj8SKMQ6tPTAMSWLVvsr507d06YTCbx7LPPVqobHx8vXRchhBg7dqzQ6XRiz5499tdycnJERESEw5HGgoICERYWJkaOHOkwf3Z2tggNDXV4vVOnTiI6OtrhCNZPP/0k9Hq9w/VtFZ/ZXXfd5bDMv//97wKA+Omnn4QQ//vMRowY4VDvueeeEwDExo0b7a9pnZ6eNm2aCAwMFL/++qvD6xMmTBAGg8F+WUXFNlevXj1x4cIFe73PP/9cABBffvml/TXZ6ekrP29np2AzMzMFAPHhhx/aX3P1SOOkSZMEAFFQUODwekX7o6KiHI7QTZw4UQAQHTt2dLjWcsiQIcJoNDocsb1aysrKRJs2bUSTJk0qXd+5YcMGh+1rz549AoBYtWrVVW+HMzk5OSI6Olp0797dafmoUaOEv79/jbSFqLrx7mmqUfn5+QCAoKCgSmU9e/ZEVFSUfZo/f36lOk8++aTD32vXroXBYMBTTz3l8Pqzzz4LIQT++9//Vrmto0ePdvi7e/fuyMnJsa/D2rVrAaDSez/zzDNVfs/LmUwmDB8+/Kosy5k2bdrYj3ACQFRUFFq2bInff//dod6xY8dcugN73bp1SExMRKdOneyvRUREYOjQoQ71MjIykJubiyFDhuDPP/+0TwaDAQkJCfj2228BAGfOnMHevXsxbNgwRERE2Ofv0KED7rzzTnv/X27MmDEOf48dOxbA/z6riv+mpaU51Hv22WcBAF999ZVyPVetWoXu3bsjPDzcof29e/eG1WrFli1bHOo/+OCDCA8Pt/9d0edX9rOr/P397f9vsViQk5OD5s2bIywsDD/++KPby8vJyYGPj4/T7yQA3H///fajcwDsZwgeeeQR+Pj4OLxuNptx6tQpt9ugkpqaip9//hnz5s1zeE/g0mfapk0b+1HnirZ+/fXXmndoXy02mw1Dhw5Fbm4u3n77bad1wsPDUVJSUu1tIaoJPD1NNSo4OBgAUFhYWKns3XffRUFBAc6ePev0VJWPjw8aNmzo8Noff/yB2NhY+3IrVJyG/eOPP6rc1kaNGjn8XfHDf/HiRYSEhOCPP/6AXq9Hs2bNHOq1bNmyyu95uRtuuAFGo/GqLMuZK9cPuLSOFy9erNLy/vjjDyQmJlZ6vXnz5g5///bbbwCAXr16OV1OSEiIfXmA8/5s3bo1vv76axQVFSEwMND++o033uhQr1mzZtDr9fZBb8VndmWbYmJiEBYW5tL28ttvv2Hfvn2IiopyWn7u3DmHv2XbUVWUlJRg+vTpWLx4MU6dOgUhhL0sLy+vSsuUubL9FYOyuLg4p69Xdb20zJo1C//6178wbdo09O/fv1L5V199hYEDB9r/btKkCdLS0jBnzhwsW7YM3bt3x1133YVHHnnEYfB7NYwdOxbr1q3Dhx9+iI4dOzqtU/H56HS6q/reRLWBg0aqUaGhoWjQoAEOHDhQqaziCIbWUS2TyVTlDDmtHbYs981gMDh9/fIf6ep0+RElV8jWxZnaWr+K8OiPPvoIMTExlcqvPJLkCa3P3ZMfcJvNhjvvvBMvvPCC0/IWLVo4/H21+3ns2LFYvHgxnnnmGSQmJiI0NBQ6nQ4PPfRQlYK569Wrh/LychQUFFT6xxeg3f6a2H6WLFmC8ePHY/To0Zg0aVKl8qNHj+LQoUNYsGCBw+uzZ8/GsGHD8Pnnn+Obb77BU089Zb/O9cp/eFbV1KlT8c4772DGjBl49NFHNetdvHgRAQEBbn+fia5FHDRSjRswYADef/997NixA7fccotHy4qPj8f69esr/eAdOnTIXg787+hObm6uw/yeHImMj4+HzWbDkSNHHI6GHT582KX5qzpwCQ8Pr7QeZrMZZ86cuSrLr6r4+HhkZWVVev3K1yqOzEZHR6N3797S5QHO+/PQoUOIjIx0OMoIXDoK2KRJE4f3ttls9lOXFZ/Zb7/95nBT0NmzZ5Gbm2t/T0C7/5o1a4bCwkJp293lzmf1ySefIDk5GbNnz7a/VlpaWmmbcFWrVq0AXBqAdejQoUrLqA6ff/45RowYgcGDBzu9VAW4dJQxNDQU3bp1q1TWvn17tG/fHpMmTcLWrVtx6623YuHChZVuyKmK+fPnIz09Hc888wzGjx8vrXv06FGHbY2oLuM1jVTjXnjhBQQEBODxxx/H2bNnK5W7c6Sif//+sFqtmDdvnsPrb7zxBnQ6Hfr16wfg0inPyMjIStebvfPOO1VYg0sqlv3WW285vH7l3bNaKgY87v7YN2vWrNJ6vPfee5WONFZ1+Vc6cuQIjhw5oqyXlJSEzMxMhyfcXLhwAcuWLatULyQkBK+++iosFkul5Zw/fx4A0KBBA3Tq1AlLly51WIcDBw7gm2++cXqq8srBRcV1ZhWfVcU8V35Gc+bMAQCHO98DAwOd9t0DDzyAzMxMfP3115XKcnNzUV5eXul1FXc+K4PBUOk78vbbb7t9pLlCxSUFu3btqtL81WHLli146KGH0KNHDyxbtkzzDMPatWvRp08fh6PT+fn5lT6D9u3bQ6/XX5VH+q1YsQJPPfUUhg4dat9uZH788UeHVASiuoxHGqnG3XjjjVi+fDmGDBmCli1bYujQoejYsSOEEDh69CiWL18OvV7v0mmkgQMH4vbbb8eLL76IY8eOoWPHjvjmm2/w+eef45lnnnG43nDEiBGYMWMGRowYgZtvvhlbtmzBr7/+WuX16NSpE4YMGYJ33nkHeXl56Nq1KzZs2OD0aJszzZo1Q1hYGBYuXIjg4GAEBgYiISHB4UiZMyNGjMDo0aNx77334s4778RPP/2Er7/+ulLcR6dOnWAwGPDaa68hLy8PJpMJvXr1QnR0tFvr6WrkzgsvvIB///vfuPPOOzF27Fh75E6jRo1w4cIF+9G0kJAQLFiwAI8++ig6d+6Mhx56CFFRUTh+/Di++uor3HrrrfZ/BMyaNQv9+vVDYmIiUlJS7JE7oaGhTh+7d/ToUdx1113o27cvMjMz7XFIFdebdezYEcnJyXjvvfeQm5uL2267DTt27MDSpUsxaNAgh2iVLl26YMGCBXjllVfQvHlzREdHo1evXnj++efxxRdf4G9/+5s9oqioqAj79+/HJ598gmPHjjmNXpHp0qULgEs3VSUlJcFgMOChhx5yWvdvf/sbPvroI4SGhqJNmzbIzMzE+vXrUa9ePbfes0LTpk3Rrl07rF+/Ho8//niVlnE1/fHHH7jrrrug0+lw3333YdWqVQ7lHTp0QIcOHVBSUoJvv/0WCxcudCjfuHEjUlNTcf/996NFixYoLy/HRx99BIPBgHvvvdejtu3YsQOPPfYY6tWrhzvuuKPSP4i6du2Kpk2b2v/evXs3Lly4gLvvvtuj9yW6ZtTWbdtEWVlZ4sknnxTNmzcXfn5+wt/fX7Rq1UqMHj1a7N2716FuRbi3MwUFBWLcuHEiNjZW+Pr6ihtvvLFSuLcQl6JKUlJSRGhoqAgODhYPPPCAOHfunGbkzvnz5x3mrwjMvjxEuaSkRDz11FOiXr16IjAw0OVw7wqff/65aNOmjfDx8XEa7u2M1WoV48ePF5GRkSIgIEAkJSWJrKysSpE7Qgjxr3/9SzRt2lQYDAan4d5Xuu222yrFzLgauSPEpbiT7t27C5PJJBo2bCimT59uDz7Ozs52qPvtt9+KpKQkERoaKvz8/ESzZs3EsGHDxK5duxzqrV+/Xtx6663C399fhISEiIEDB2qGe//888/ivvvuE8HBwSI8PFykpqY6DfeeOnWqaNKkifD19RVxcXGVwr2FuBQBNGDAABEcHFwp3LugoEBMnDhRNG/eXBiNRhEZGSm6du0qXn/9dWE2m4UQjuHYV7py+ygvLxdjx44VUVFRQqfTScO9L168KIYPH24PlE9KShKHDh2q9Pm780SYOXPmiKCgIIc4H632Vyz3ykibiu/Hzp07le8noxV8XzFV9MWaNWuETqcTZ8+edZj/999/F48//rho1qyZPfz89ttvF+vXr/eoXUJoh+ZXTFfGZ40fP140atSIjxGk64ZOiBq6qp+IvNIzzzyDd999F4WFhZo3T3gqPT0dU6dOxfnz590+ykeX7rpu2rQpZs6ciZSUlNpujkv+/ve/Y9euXdixY0dtN8WpsrIyNG7cGBMmTMDTTz9d280huip4TSMRXTUlJSUOf+fk5OCjjz5Ct27dqm3ASJ4LDQ3FCy+8gFmzZlXpDuza0KlTJ0ydOrW2m6Fp8eLF8PX1rZT3SlSX8UgjEV01nTp1Qs+ePdG6dWucPXsWH3zwAU6fPo0NGzagR48e1fa+PNJIRFT9eCMMEV01/fv3xyeffIL33nsPOp0OnTt3xgcffFCtA0YiIqoZPD1NRFfNq6++il9//RXFxcUoKirCd999d1XzDLWkp6dDCMGjjER0TUlPT4dOp3OYKrJRASA7OxuPPvooYmJiEBgYiM6dO+M///mPwzIuXLiAoUOHIiQkBGFhYUhJSan0VLV9+/ahe/fu8PPzQ1xcHGbOnFmpLatWrUKrVq3g5+eH9u3bO30UqwoHjURERETVpG3btjhz5ox9+v777+1ljz32GA4fPowvvvgC+/fvx+DBg/HAAw9gz5499jpDhw7FwYMHkZGRgTVr1mDLli0YNWqUvTw/Px99+vRBfHw8du/ejVmzZiE9PR3vvfeevc7WrVsxZMgQpKSkYM+ePRg0aBAGDRrk9OlsMrymkYiIiKgapKenY/Xq1Q4PPbhcUFCQPbe2Qr169fDaa69hxIgR+OWXX9CmTRvs3LkTN998MwBg3bp16N+/P06ePInY2FgsWLAAL774IrKzs2E0GgEAEyZMwOrVq+1PR3vwwQdRVFSENWvW2N/nr3/9Kzp16lQp61SG1zRexmaz4fTp0wgODubD5YmIiOoIIQQKCgoQGxur+QQhZ0pLS2E2m91+ryvHCCaTCSaTyWn93377DbGxsfDz80NiYiKmT5+ORo0aAbgUCL9ixQoMGDAAYWFhWLlyJUpLS9GzZ08AQGZmJsLCwuwDRgDo3bs39Ho9tm/fjnvuuQeZmZno0aOHfcAIXHry1muvvYaLFy8iPDwcmZmZSEtLc2hXUlISVq9e7da6c9B4mdOnTyMuLq62m0FERERVcOLECZeeJgZcGjDG+gfhItx7BGdQUFClawqnTJni9ClVCQkJWLJkCVq2bIkzZ85g6tSp6N69Ow4cOIDg4GCsXLkSDz74IOrVqwcfHx8EBATgs88+Q/PmzQFcuubxyqd4+fj4ICIiAtnZ2fY6Vz5JrH79+vay8PBwZGdn21+7vE7FMlzFQeNlgoODAQDPvPE7TP7BTuvYavFkvr4aD36qjqzW5lUM1dnnQrFwT1fb5kHjPe1zWdyep8tW9Zsnn5nHbZPM7/l6ezR7tdJ5cIW6p2dWVPPLyg2Gqs8LAKqDSgaDvIJs8ToPd7q1uc9WL+DqtKNKqmH/UFZSgLnjmtp/x11hNptxEVYs9WuKABdv8SiGDcmFv+PEiRMICQmxv651lLHiuffApUdgJiQkID4+HitXrkRKSgpeeukl5ObmYv369YiMjMTq1avxwAMP4LvvvkP79u1dXpeawkHjZSq+hCb/YJj8Q5zW4aCx5nHQWNX3rr5lX9ODRsmb2zhodErv6aBRsXPioPHq83igX4u3wXryPVLtH6rSL4E+BgTqXHv4gE5cOioZEhLiMGh0VVhYGFq0aIGsrCwcOXIE8+bNw4EDB9C2bVsAQMeOHfHdd99h/vz5WLhwIWJiYnDu3DmHZZSXl+PChQuIiYkBAMTExODs2bMOdSr+VtWpKHcV754mIiIir2XwM8Dg7+Lk59mTrQoLC3HkyBE0aNAAxcXFAFDpGkyDwWB/MlNiYiJyc3Oxe/due/nGjRths9mQkJBgr7NlyxZYLBZ7nYyMDLRs2RLh4eH2Ohs2bHB4n4yMDCQmJrrVfg4aiYiIyGvpfHVuTe547rnnsHnzZhw7dgxbt27FPffcA4PBgCFDhqBVq1Zo3rw5nnjiCezYsQNHjhzB7NmzkZGRgUGDBgEAWrdujb59+2LkyJHYsWMHfvjhB6SmpuKhhx5CbGwsAODhhx+G0WhESkoKDh48iBUrVuDNN990uPHl6aefxrp16zB79mwcOnQI6enp2LVrF1JTU91aH56eJiIiIq+l99FB7+K1BHqbe4PGkydPYsiQIcjJyUFUVBS6deuGbdu2ISoqCgCwdu1aTJgwAQMHDkRhYSGaN2+OpUuXon///vZlLFu2DKmpqbjjjjug1+tx77334q233rKXh4aG4ptvvsGYMWPQpUsXREZGYvLkyQ5Zjl27dsXy5csxadIk/OMf/8CNN96I1atXo127dm6tD3MaL5Ofn4/Q0FCMX3ie1zRegdc0Vg2vaazCsnlNY5Xwmkat5fOaRqfzX2fXNJaV5OO10VHIy8tz+VrDit/8L5u2RaDetdPORTYrBv5+0K33uZ7wSKMTcbG+8A/0dVqm2P9U605CRbrzU7RL9VuqKrdKdgKyMgAoV6QdlJfLy83l2o0rK5M33GKRl5st8saXlcobX16uPb/FrJjXIi9XDa48GbCq/tWtV3wRZD/UqkGCimrgZynT7jeLWb4xmRXlNsnnCQDlio3Zx0f7hykg2E86b2Cw87sz7fMHON9nVTCatD8TXx/VwMyzgZt00KiaVzUoVLTNR/ErJ5tdtV6q/b1qfl/JOEW1z/b05mm9TvaPK9VBBPmyVeWyXVNV5y0pkm//MnqDDnoX90t6q3dnOHPQSERERF5LZ9BB5+KgUVerWUW1j4NGIiIi8lpuHWnkoJGIiIjIO+n0OpevX9UpTt1f7zhoJCIiIq+lM+ihU11cW1HXk8fZXAc4aCQiIiKvxdPTrqsz4d6NGzeGTqerNI0ZMwYA0LNnz0plo0ePruVWExER0bVMp9PZT1ErJ09vW6/j6syRxp07d8Jq/V+cxYEDB3DnnXfi/vvvt782cuRIvPzyy/a/AwICqvRefkYBf6PzQ9CqKAIZT2MKPCHL7HOFKjZHVm5RROaUmeXlJaXyNy+VlJdJ4lcuzStvnKexOFZJx6iyDj0li8XxkWV9ADD5yXcNPj7yf2/Kyn185fOqLi2qzp22Kt5FlWfo5ydfgJ9Je35/eaIO/IzycpOvfHvy89XeFk2+8u3YoJN/B4Xi6EuRWXt7KimT95lVEaYsi44B1NE0srOSer182aptVfUVl+3zVeut4mOQv7nJR/sz9TXIP2/V76DZKv9MLeXa86vWW7PPJNFrKjoDXD7SqNjcrnt1ZtBYkZ5eYcaMGWjWrBluu+02+2sBAQFuP3ybiIiIvJdbkTtefiNMnTk9fTmz2Yx///vfePzxxx2OOixbtgyRkZFo164dJk6caH8YuJaysjLk5+c7TEREROQ9dHq9W5M3qzNHGi+3evVq5ObmYtiwYfbXHn74YcTHxyM2Nhb79u3D+PHjcfjwYXz66aeay5k+fTqmTp1aAy0mIiKia5FbkTu1+di3a0CdHDR+8MEH6NevH2JjY+2vXf5g7vbt26NBgwa44447cOTIETRr1szpciZOnIi0tDT73/n5+YiLi6u+hhMREdE1xa27p7389HSdGzT+8ccfWL9+vfQIIgAkJCQAALKysjQHjSaTCSaT4upzIiIium7xSKPr6tygcfHixYiOjsaAAQOk9fbu3QsAaNCgQQ20ioiIiOoinc71axV1qoiF61ydGjTabDYsXrwYycnJ8PH5X9OPHDmC5cuXo3///qhXrx727duHcePGoUePHujQoUMttpiIiIiuZTzS6Lo6NWhcv349jh8/jscff9zhdaPRiPXr12Pu3LkoKipCXFwc7r33XkyaNKlK71Nq1kHn63zDKJTfkI0ys3aIU7kiR0ooghr1io1VNrtq2Z7mOMoWb1O8t6pfykrlGXLFRdpBj7IyACgrLlO8tyJEUsHX6KtZpvo8Ve+de+6itNzop33pRVBooHTekIggaXlAsPyyDqtVOwdSlZ2p2lZ9FRmTgUHau7XgQPm8YSHyz6R+mDzXs55/gXz5vtoJDcFW+efpVyZPd/CxyHdOOkm/lgs/6bxmY7C0vMRXXp7nF6ZZdq4kRDpvsVn+mZVZ5Ed+yq2q/aZ2v/go8icNkqxDADAqDkr5SPIQDYqMSH8fi7TcT1Huq9PelnWKMEKzTXu/BgBlVvnQotiiHTpaZpF/3qUan7eLTwF0yq1rGj3Mz6zr6tSgsU+fPk6/4HFxcdi8eXMttIiIiIjqMr2PAXof+WDVXrc6n8JRB9SpQSMRERHR1cTT067joJGIiIi8FgeNruOgkYiIiLwWB42u46CRiIiIvNalQaOLkTscNBIRERF5J53e9bundYq78a93HDQSERGR1+Lpaddx0OiEzaadW1hYJM+YM0qCuUxG+caWX6AKS1TkGUry7yxmebttihQB1fdE9kUyGuVRBr6+8tMCISHyTDB/f+3lq/71WJQnz7Y7k3VC/t4h8jzDhs1v0CwLqyfPSvRV9JuhjfxpRz4+2v2q2vHZrIqcNsX2JCNUG5tO3rawMPn2EBut3W+xEfLsyyhVzqJenqUYnv+HtNx08qR2Yc456bzWi/L3FhZ5Lp/OpJ2N5x8kz1n0j6ovLTdFN5GWm4NkuZ7ynEaheNav2SIvzy2UFqOktOrZulbF18BkkrctIlR7W44Kky880l++Yg1s8n1XYMFZzTKf4lzpvLApfk+MAdJyc0C4ZllOeLx03t8LnO/3DFb59i+j07vxRBgX612vOGgkIiIir8Ujja7joJGIiIi8FgeNruOgkYiIiLwWT0+7joNGIiIi8lo80ug6DhqJiIjIa/FIo+s4aCQiIiLvpdMpUxsc6noxDhqd+Fv9nQgOch6Hcjq+qXTeXLN2BMvFElnsBHD2ovzjyCuQxz8EBWlHjZSWyuN8CgvlcQUmkzz+xWSSxLsovmQhQfJ/uUWGydc7NEB73Xz18ngWq4iRlhebtSNzAKCoVN4vWtFNrrAo4jxKSuXlPpLNSShSb8rkyTQoKJQ3TrZ81T43wF++PcTJ019wQ6h2FEm4MV86b7BNHmsTnHdaWm46nSUtLz+hHclTdPyMfN6SMmm5j798/2KKCJWWS5cdLI/FMVjkG6MJ2uVhJnnslV7nLy33Nci/g0ZfeXlhifb2lpMr/wJfyJGvt1URL+Xrqx1NE6n4uIIM8sidwHztSB0AMEq2Veu5bOm85fnyaCqbWf574hug/Zk2rFdPOm/DmIZOX88vKpHOJ6PTuXF6moNGIiIiIu/E09Ou46CRiIiIvBZvhHEdB41ERETktXik0XXevfZERETk1XT6/x1tVE/uLTs9Pf3SNZOXTa1atXKok5mZiV69eiEwMBAhISHo0aMHSkr+d43mhQsXMHToUISEhCAsLAwpKSkoLHS8pnXfvn3o3r07/Pz8EBcXh5kzZ1Zqy6pVq9CqVSv4+fmhffv2WLt2rXsrAw4aiYiIyIu5PmB0/TT25dq2bYszZ87Yp++//95elpmZib59+6JPnz7YsWMHdu7cidTUVOgvO6I5dOhQHDx4EBkZGVizZg22bNmCUaNG2cvz8/PRp08fxMfHY/fu3Zg1axbS09Px3nvv2ets3boVQ4YMQUpKCvbs2YNBgwZh0KBBOHDggFvrwtPTRERE5L30+kuTq3Xd5OPjg5gY50kd48aNw1NPPYUJEybYX2vZsqX9/3/55ResW7cOO3fuxM033wwAePvtt9G/f3+8/vrriI2NxbJly2A2m7Fo0SIYjUa0bdsWe/fuxZw5c+yDyzfffBN9+/bF888/DwCYNm0aMjIyMG/ePCxcuNDldeGRRiIiIvJaeoPBrQm4dHTv8qmsTDsS67fffkNsbCyaNm2KoUOH4vjx4wCAc+fOYfv27YiOjkbXrl1Rv3593HbbbZWORIaFhdkHjADQu3dv6PV6bN++3V6nR48eMBqN9jpJSUk4fPgwLl68aK/Tu3dvh3YlJSUhMzPTrb7ikUYn/H/6FgH+fk7LbryhsXTe4mjtHMc/w+Kk8wYao6XlZ/3kOWylkmw9n3BFlpmP/N8PIQHyXL5gk/YXxs8gD/3z0cuz0KxC3jZ/gyQDzpYjnde3XJ7tJYzyfisN187lBICLtgjNspJy+edZYDZKy0ssiraZtfvNapOfYgmSR+MhOFD+3pZy+fwyYUHybLu4MHlGXJRR+zMPsMhzGk2ledJyQ5k8U1Dky+cvy8nVLFPlMFrN8k5V5TTqTdrbk0+0PK/U3ECeT3sxrIm8XPI9EFBsi77yfgk2yreXhsHyzEAfvXa/2hp5dlxFr5Pv23xxoerzCvl+VWdTBL1Kglx1iuxLlfJi+X619LwkD/UPeRaqX+QJ58uU/QAqVOXu6bg4x9/zKVOmID09vVL9hIQELFmyBC1btsSZM2cwdepUdO/eHQcOHMDvv/8O4NJ1j6+//jo6deqEDz/8EHfccQcOHDiAG2+8EdnZ2YiOdhwb+Pj4ICIiAtnZl/I0s7Oz0aSJ43ewfv369rLw8HBkZ2fbX7u8TsUyXMVBIxEREXkvnRunp//vTpgTJ04gJOR/ofcmk/N/tPXr18/+/x06dEBCQgLi4+OxcuVKtG7dGgDwxBNPYPjw4QCAm266CRs2bMCiRYswffr0qqxNteKgkYiIiLyXOze4/F+9kJAQh0Gjq8LCwtCiRQtkZWWhV69eAIA2bdo41GndurX9FHZMTAzOnTvnUF5eXo4LFy7Yr5OMiYnB2bOOTwCq+FtVR+taSy115ppG1W3rpaWlGDNmDOrVq4egoCDce++9lTqIiIiI6HI6nd6tyROFhYU4cuQIGjRogMaNGyM2NhaHDx92qPPrr78iPj4eAJCYmIjc3Fzs3r3bXr5x40bYbDYkJCTY62zZsgUWy/8uxcjIyEDLli0RHh5ur7NhwwaH98nIyEBiYqJb7a8zg0ZAftv6uHHj8OWXX2LVqlXYvHkzTp8+jcGDB9dia4mIiOiap9e5N7nhueeew+bNm3Hs2DFs3boV99xzDwwGA4YMGQKdTofnn38eb731Fj755BNkZWXhpZdewqFDh5CSkgLg0lHHvn37YuTIkdixYwd++OEHpKam4qGHHkJsbCwA4OGHH4bRaERKSgoOHjyIFStW4M0330RaWpq9HU8//TTWrVuH2bNn49ChQ0hPT8euXbuQmprq1vrUqdPTWret5+Xl4YMPPsDy5cvth3sXL16M1q1bY9u2bfjrX/9a000lIiKiOqA6nwhz8uRJDBkyBDk5OYiKikK3bt2wbds2REVFAQCeeeYZlJaWYty4cbhw4QI6duyIjIwMNGvWzL6MZcuWITU1FXfccQf0ej3uvfdevPXWW/by0NBQfPPNNxgzZgy6dOmCyMhITJ482SHLsWvXrli+fDkmTZqEf/zjH7jxxhuxevVqtGvXzq31qVODxorb1v38/JCYmIjp06ejUaNG2L17NywWi8Pt5K1atUKjRo2QmZmpOWgsKytzuE0+P19+ZyURERFdX6rz2dMff/yxss6ECRMcchqvFBERgeXLl0uX0aFDB3z33XfSOvfffz/uv/9+ZXtk6szp6Yrb1tetW4cFCxbg6NGj6N69OwoKCpCdnQ2j0YiwsDCHeVS3k0+fPh2hoaH26cpb6ImIiOg6p9NVPEvQhcn9J8JcT+rMkUbZbev+/opQOQ0TJ050OOefn5+PuLg4lOfmobzUefafr/85p69X8PfVzkKLsspz1oxB8jyy2IBAabmPTjuPzGST52aZyuX5cwarIgNLEpUmrPJ/m1ghzyMs8wmQlvtZtHP7gnJPSucVenkemU7xmQULeZZaPR/tdbP4ye+8ywluJC0/X1ZPWl5o1H5vm5Dv+FQ5jqpsTV+DdrnRIO/TAB/5thZqkGchBpVp5zSqchiNBX9Ky3XF8oxI1fVOxtBg7Vl95btj1Wkx30jtLEQA0Mc31ywrqH+jdN58f3mGrBXy75G/0N7/GHTy739xufPM3AplVl9peZzuD2m5X2GuZpmxRLsMAPRm+X5VZ5Hv06HX/szLA+T7h4Jw+f6hNECxPYRq59vqgsKk8/oobgYxKvJMcVY7i7H0hHyfbdMIgRXlilxKieo80ni9qTNHGq90+W3rMTExMJvNyM3Ndaijup3cZDLZb5uv6u3zREREVIdVPEbQ1cmL1dm1v/y29S5dusDX19fhdvLDhw/j+PHjbt9OTkRERN7jyjg/1eTN6szp6eeeew4DBw5EfHw8Tp8+jSlTpthvWw8NDUVKSgrS0tIQERGBkJAQjB07FomJibxzmoiIiLRV4Ykw3qrODBpVt62/8cYb9lvRy8rKkJSUhHfeeaeWW01ERETXMl7T6Lo6M2hU3bbu5+eH+fPnY/78+TXUIiIiIqrzKu6MdrWuF6szg0YiIiKiq86dJ73wSCNdSVitEFbnt++LEnmUgM6sHWPgf0EeJeCXe0reMMW/cKTxMYpoGSiiYyAkmToAhCRaRhjkkRi6ckXESrk8tkJXXChZtnYMEQAIg6JfFH2uKymSlhskOxhfxbL9/bKk5fXDG0jLL4Y10Swr0IVK5xWKSB4BeblNaK+bUS//TCLL5N8T/4vy2CtDmfZnIvt+AoCuQB7JI1QRKmHyGCTfIO2EBl/VtqixT6ogTPJomgsNO2qWmQ3y2DIfm/w7GlKWKy1XRnZJ6IR8vX1Ktb//AOBTeEG+/CLthzqIQvmyrYoHQtjM8vXWST5zn3ryyJzwYvl7l4XVl5bbJPtsa4B8/1AYKF92SL78t8w3VzsWS2+U/15o/RbpfBTfHwl3nint6bOn6zoOGomIiMh78UijyzhoJCIiIq9Vnc+evt5w0EhERETeS6dz/fGAzGkkIiIi8lIGPaC6pvjyul6Mg0YiIiLyWjw97ToOGomIiMh7MafRZRw0EhERkffSuXH3NK9ppCuVnP4TPibnWVHGohLpvKYS7XJDULB0Xr2/PCsNvtq5WgAg/AO1y3xN0nl1xQXyclVGnFHSNsW8kGR2AYCwyHP9ZFlowlIuf29VPqWCKJevm61c8f4SOsXOyRD0u7Q8OvSAdll4lHTe8tBoaXlhSKy0vNRHe1sMKZHnLPrn/CEt1+fJtxfZ9iYUuXnWYnnupmpbNki+gwBgDdfuV6uffP8gy58EAIMiv7Ler99rF1rk/aLqNxVRpp2PqfOR5/IJRdaqirVI3m86X+33Ly+Q5zQWHJXnEVrN8u+/b4D2ftn4pzxf0l/yWwMAflGKzNEA7e1Nb5LnmYYXXZSWG3LOSMst2dmaZeUF8s/LnO+8vKys6tsJcxpdx0EjEREReS/mNLqMg0YiIiLyXrym0WUcNBIREZH3Yk6jyzhoJCIiIu+l11+aXK3rxThoJCIiIu/F09Mu46CRiIiIvBdvhHEZB41ERETkvXQ6N440ctBIVyg6nwe90Xl2V8kFeZ6hf752rldQXAPpvD7R8mw82wV5Pp1VkilWXizP9CpX5E+qHp1kk+QVCpuQzmswyjdD2bIBQO+j/cxQnaQMAPS+8vdW5TCWl5RJy80FxdplhfIsNE/7zS9MOzPQLzJMOq+xvjzHMTy8nrRc9q9xW16udFZRpujTQnmOm7VUe36bWZ7lJoS8z1Xbi2+J/DPVZ5/WLDOonn2ryIg0K77jQjK/6vtvVayXbDsHgPJS7X5XbefCJs9SVc9f9eXL2g0AZYXy/EqDr3y/6WPS3p5MIfLt3KL4zPzO/Skt9w0J0i5UDKBU2bmlefLfSYtke1Gtl9ZnUqr4bkvxRhiXcdBIRERE3os3wriMg0YiIiLyXjzS6DIOGomIiMh78e5pl3HQSERERN5L58bpaQ4aiYiIiLwUT0+7jINGIiIi8l48Pe2yOjNonD59Oj799FMcOnQI/v7+6Nq1K1577TW0bNnSXqdnz57YvHmzw3xPPPEEFi5c6NZ7lZeYYSl3HsNgs8rjG2QxKmW58ggFU4h2HAcAlOXLYy1KLmhH7pTmy2NMysvKpeWq2AqdJGLF1995fFEFH5M8asQYaJKXB/lplgU2iJDOq1qv0gv58nLFZ1qQrT1/yUVVtIQ8YkVFFvdhCjZK5/UL85eWy6JCAHUckCc8iWBRfd4qeh/5D0Z1rrcq9soTVrP8+194Th6hUpYvj+SxmrU/M6tF8Xkq9rk6g2dHfkwh8u+CTHmJvN98/OXbgzFQ+71VvzWebmul5y9qllkV8TWqKCJVuWx7U32/tSLWyi3yz0KKRxpdVmeGzJs3b8aYMWOwbds2ZGRkwGKxoE+fPigqcvzRHjlyJM6cOWOfZs6cWUstJiIiomteReSOq5MXqzNrv27dOgwbNgxt27ZFx44dsWTJEhw/fhy7d+92qBcQEICYmBj7FBISUkstJiIiomud0Osh9AYXJ/eGTenp6dDpdA5Tq1atKrdBCPTr1w86nQ6rV692KDt+/DgGDBiAgIAAREdH4/nnn0d5ueOR1U2bNqFz584wmUxo3rw5lixZUuk95s+fj8aNG8PPzw8JCQnYsWOHW+sC1KFB45Xy8vIAABERjqcfly1bhsjISLRr1w4TJ05EcbH2Kd2ysjLk5+c7TERERORFKq5pdHVyU9u2bR3OgH7//feV6sydOxc6J6e+rVYrBgwYALPZjK1bt2Lp0qVYsmQJJk+ebK9z9OhRDBgwALfffjv27t2LZ555BiNGjMDXX39tr7NixQqkpaVhypQp+PHHH9GxY0ckJSXh3Llzbq1Lnbmm8XI2mw3PPPMMbr31VrRr187++sMPP4z4+HjExsZi3759GD9+PA4fPoxPP/3U6XKmT5+OqVOn1lSziYiI6BojdDoIF69VdLXe5Xx8fBATE6NZvnfvXsyePRu7du1CgwaOjxv+5ptv8PPPP2P9+vWoX78+OnXqhGnTpmH8+PFIT0+H0WjEwoUL0aRJE8yePRsA0Lp1a3z//fd44403kJSUBACYM2cORo4cieHDhwMAFi5ciK+++gqLFi3ChAkTXF6XOnmkccyYMThw4AA+/vhjh9dHjRqFpKQktG/fHkOHDsWHH36Izz77DEeOHHG6nIkTJyIvL88+nThxoiaaT0RERNeKKhxpvPIsZVmZ9s2mv/32G2JjY9G0aVMMHToUx48ft5cVFxfj4Ycfxvz5850OLDMzM9G+fXvUr1/f/lpSUhLy8/Nx8OBBe53evXs7zJeUlITMzEwAgNlsxu7dux3q6PV69O7d217HVXVu0Jiamoo1a9bg22+/RcOGDaV1ExISAABZWVlOy00mE0JCQhwmIiIi8iIVd0+7OgGIi4tDaGiofZo+fbrTRSckJGDJkiVYt24dFixYgKNHj6J79+4oKLiUSDBu3Dh07doVd999t9P5s7OzHQaMAOx/Z2dnS+vk5+ejpKQEf/75J6xWq9M6FctwVZ05PS2EwNixY/HZZ59h06ZNaNKkiXKevXv3AkClw71EREREANy7K/r/6p04ccLhQJPJ5Dwarl+/fvb/79ChAxISEhAfH4+VK1ciKioKGzduxJ49e6re9hpWZwaNY8aMwfLly/H5558jODjYPjoODQ2Fv78/jhw5guXLl6N///6oV68e9u3bh3HjxqFHjx7o0KGDW+9VcK4Qwufqd03+GXnWmdUsz+WzKfLMZLleVot82eX5nmUC6ny1r/Mw+Mu/jMYAeY6jb6A8A87XX/tmJ0uxPJ/Sx0+e0VZ4Nk9arsq/NBeYtee9qF0GANYS+eetKpfR+8gzPw3+8pvCVJ+pQZK9aTDK59VL8iUBQO9hLp8ndAbFeityHPW+kn5RrLcql0+Vb2fTyJ4FPM8MtZTKM/Jk+xfVdmwrl+cV6n3k24Ns3wTI940GyeflCp1B3m9WH+1+s5XL51V9DyzF8v1Leal2uaVEkcNYpvg9UeT+2iR9rvqOaWUCF5ZXPaexKtc0VvXsZFhYGFq0aIGsrCzs378fR44cQVhYmEOde++9F927d8emTZsQExNT6S7ns2fPAoD9dHZMTIz9tcvrhISEwN/fHwaDAQaDwWkd2bWWztSZ09MLFixAXl4eevbsiQYNGtinFStWAACMRiPWr1+PPn36oFWrVnj22Wdx77334ssvv6zllhMREdE1q5rvnr5cYWEhjhw5ggYNGmDChAnYt28f9u7da58A4I033sDixYsBAImJidi/f7/DXc4ZGRkICQlBmzZt7HU2bNjg8D4ZGRlITEwEcGl81KVLF4c6NpsNGzZssNdxVZ050iiE/F+bcXFxlZ4GQ0RERCQjdHoIFweDrtar8Nxzz2HgwIGIj4/H6dOnMWXKFBgMBgwZMgRRUVFOj/Q1atTIfglenz590KZNGzz66KOYOXMmsrOzMWnSJIwZM8Z+Snz06NGYN28eXnjhBTz++OPYuHEjVq5cia+++sq+zLS0NCQnJ+Pmm2/GLbfcgrlz56KoqMh+N7Wr6sygkYiIiOiqq8bHCJ48eRJDhgxBTk4OoqKi0K1bN2zbtg1RUVEuzW8wGLBmzRo8+eSTSExMRGBgIJKTk/Hyyy/b6zRp0gRfffUVxo0bhzfffBMNGzbE+++/b4/bAYAHH3wQ58+fx+TJk5GdnY1OnTph3bp1lW6OUeGgkYiIiLyWgBtHGt28qu/KaEDl8p2cVY2Pj8fatWul8/Xs2VN5Q01qaipSU1Pdas+VOGgkIiIi71WNRxqvN1W+ovPIkSOYNGkShgwZYr9A87///a89bJKIiIjomqfTuXEjDAeNbtu8eTPat2+P7du349NPP0VhYSEA4KeffsKUKVOuagOJiIiIqktF5I6rkzer0unpCRMm4JVXXkFaWhqCg4Ptr/fq1Qvz5s27ao2rLSUXSqE3eJbPVR1UmWFCktMoLPK7z1W5e6qsNBnVe1sV+ZM6RQacsEnWW1LmClVembBWPStRmXWoKPckp9FT6u1BO4fNpsgT1JsV26JqfklIr87DjEdVlqJQZFDKdrha+XP295aWqsmy81TZeHpfRVai4jPT+Wp/ZgbFsYvq3hPLvkeybQlQb0+yfTIAmIu19y8Go3zNS3Ll2ZqqDFnZ9lCu2Od6khkMyPvFqsiA1FJk8yBr2J0oHQ8jd+q6Kq39/v37cc8991R6PTo6Gn/++afHjSIiIiKqCQI6tyZvVqVBY1hYGM6cOVPp9T179uCGG27wuFFERERENaEip9HVyZtVae0feughjB8/HtnZ2dDpdLDZbPjhhx/w3HPP4bHHHrvabSQiIiKqHjX4RJi6rkpr/+qrr6JVq1aIi4tDYWEh2rRpgx49eqBr166YNGnS1W4jERERUbXgjTCuq9KNMEajEf/617/w0ksv4cCBAygsLMRNN92EG2+88Wq3j4iIiKjaVOdjBK83HoV7N2rUCI0aNbpabSEiIiKqWXoDhN7F+/RdrXedcnnQmJaW5vJC58yZU6XGXCtEudCMatH5KmIxJLE4ekU8gypyQ6Xcqh054BMg/6hVbVNFKHhC9d6ekMVKAIBe0eeqz0QoUh5k8/v6efZAJoMiBkUV9+EJVSyGwaT9PfA49saDOCxPv4OqtusVsVgyqngoVfyTMh5KsnyDj2dHT1QRLLKfGlWEUnXzJKJJFcHkybauivMqK/AgYgaufGZVp/w9kZSpfmNVEW5V4c5d0d5+97TLv1pXPtPwxx9/RHl5OVq2bAkA+PXXX2EwGNClS5er20IiIiKiasLT065zedD47bff2v9/zpw5CA4OxtKlSxEeHg4AuHjxIoYPH47u3btf/VYSERERVQcd3Hj2dLW25JpXpSHz7NmzMX36dPuAEQDCw8PxyiuvYPbs2VetcURERETVSUDv1uTNqnRRVX5+Ps6fP1/p9fPnz6OgoMDjRhERERHVBHeidLw9cqdKQ+Z77rkHw4cPx6effoqTJ0/i5MmT+M9//oOUlBQMHjz4areRiIiIqFrwiTCuq9KRxoULF+K5557Dww8/DIvl0h17Pj4+SElJwaxZs65qA4mIiIiqC++edl2VBo0BAQF45513MGvWLBw5cgQA0KxZMwQGBl7VxhERERFVJ9497TqPguICAwPRoUOHq9WWa4aPvw98NLLgVLlcMqocNlW5iiwTzNM8MZ1e3jbZ/Kq8QE9z+zyhzN1TzO9J0pmnuZzKLVGybqrcTU+zMw1GSfadKvtS8T3wZH69B99fwPN+sUnaplPkFdoU+XTKzFHJunua6anqV/lnJs+2tFZjniDg2Wfq6T5d1i/livX2dP/h6fzSZSvzTCVlqoxHjXxaH6sHnyOvaXRZlQaNt99+O3SSjtu4cWOVG0RERERUU3h62nVVGjR26tTJ4W+LxYK9e/fiwIEDSE5OvhrtIiIiIqp2PD3tuioNGt944w2nr6enp6OwsNCjBhERERHVFB5pdN1VHTI/8sgjWLRo0dVcZJXMnz8fjRs3hp+fHxISErBjx47abhIRERFdgwTciNzx8nDvq7r2mZmZ8PPzu5qLdNuKFSuQlpaGKVOm4Mcff0THjh2RlJSEc+fO1Wq7iIiI6NpTcaTR1cmbVen09JUB3kIInDlzBrt27cJLL710VRpWVXPmzMHIkSMxfPhwAJcyJb/66issWrQIEyZMqNW2ERER0bXl0t3Trl7TyEGj20JCQhzuntbr9WjZsiVefvll9OnT56o1zl1msxm7d+/GxIkTHdrWu3dvZGZmVqpfVlaGsrIy+9/5+fmX5jHoqhzD4Ek8hDJqwMNYjOoki+yozighALCarZplnkakqKg+E0+iiFQ8iczw9BSD6jOR9YtBFQ1zTUeJVN+pKU+3h9qk/J55EP9kUGytnu5fqnO/quoXWdtV27Hqt0b13tX5PVHxZFvXWi+9B0cAeU2j66o0aFyyZMlVbsbV8eeff8JqtaJ+/foOr9evXx+HDh2qVH/69OmYOnVqTTWPiIiIrjHMaXRdlf7Z3LRpU+Tk5FR6PTc3F02bNvW4UTVl4sSJyMvLs08nTpyo7SYRERFRDRJC59bkzap0pPHYsWOwWiufEiwrK8OpU6c8blRVRUZGwmAw4OzZsw6vnz17FjExMZXqm0wmmEymmmoeERERXXPcuSvau++edmvQ+MUXX9j//+uvv0ZoaKj9b6vVig0bNqBx48ZXrXHuMhqN6NKlCzZs2IBBgwYBAGw2GzZs2IDU1NRaaxcRERFdm3hNo+vcGjRWDMR0Ol2lJ7/4+vqicePGmD179lVrXFWkpaUhOTkZN998M2655RbMnTsXRUVF9rupiYiIiCrYoIfNxSOIrta7Xrk1aLTZLt2t1aRJE+zcuRORkZHV0ihPPPjggzh//jwmT56M7OxsdOrUCevWrat0cwwRERERjzS6rkrXNB49evRqt+OqSk1N5eloIiIiUnLnBhfeCOOit956C6NGjYKfnx/eeustad2nnnrK44bVJqvZBmsV8/0qjsY6o9fLD2tXPeFRTZnpZavGbDsP88A8yVr0MVXp30V21nLPstCkPMzlrM5cT1X2nSc5a1bFlm7wlX9PVBmRsrbZFN8D1bYqrKpvadVPXSmXrGibTfGZyVqmmlfFo21NMa9qW7SaFZ9pNWe1yigzKBXbuoynGbSyflF9T1Q8zc6U0epTj7ZBHml0mctb7BtvvIGioiL7/2tNc+fOra62EhEREV1V1fkYwfT0dOh0OoepVatWAIALFy5g7NixaNmyJfz9/dGoUSM89dRTyMvLc1jG8ePHMWDAAAQEBCA6OhrPP/88ysvLHeps2rQJnTt3hslkQvPmzZ3mac+fPx+NGzeGn58fEhISsGPHDvc6Cm4cabz8lPS1fnqaiIiIyBXVfaSxbdu2WL9+vf1vH59LQ6/Tp0/j9OnTeP3119GmTRv88ccfGD16NE6fPo1PPvkEwKVkmgEDBiAmJgZbt27FmTNn8Nhjj8HX1xevvvoqgEtjsgEDBmD06NFYtmwZNmzYgBEjRqBBgwZISkoCAKxYsQJpaWlYuHAhEhISMHfuXCQlJeHw4cOIjo52eV2qdGz85ZdfRnFxcaXXS0pK8PLLL1dlkUREREQ1TsCNcO8qDBp9fHwQExNjnypuIm7Xrh3+85//YODAgWjWrBl69eqFf/7zn/jyyy/tRxK/+eYb/Pzzz/j3v/+NTp06oV+/fpg2bRrmz58Ps9kMAFi4cCGaNGmC2bNno3Xr1khNTcV9992HN954w96GOXPmYOTIkRg+fDjatGmDhQsXIiAgAIsWLXJrXao0aJw6dSoKCwsrvV5cXMzH8hEREVGdYYPOrQkA8vPzHaaysjLN5f/222+IjY1F06ZNMXToUBw/flyzbl5eHkJCQuxHIzMzM9G+fXuHBJikpCTk5+fj4MGD9jq9e/d2WE5SUhIyMzMBAGazGbt373aoo9fr0bt3b3sdV1Vp0CiEgM7J8xd/+uknREREVGWRRERERDWuKtc0xsXFITQ01D5Nnz7d6bITEhKwZMkSrFu3DgsWLMDRo0fRvXt3FBQUVKr7559/Ytq0aRg1apT9tezs7EqRgRV/Z2dnS+vk5+ejpKQEf/75J6xWq9M6FctwlVu3loaHh9sv5GzRooXDwNFqtaKwsBCjR492qwFEREREtaUqkTsnTpxASEiI/XWtRxL369fP/v8dOnRAQkIC4uPjsXLlSqSkpNjL8vPzMWDAALRp0wbp6elVWIua4dagce7cuRBC4PHHH8fUqVMdHiNoNBrRuHFjJCYmXvVGEhEREVUHAddvcKkI9gkJCXEYNLoqLCwMLVq0QFZWlv21goIC9O3bF8HBwfjss8/g6+trL4uJial0l/PZs2ftZRX/rXjt8johISHw9/eHwWCAwWBwWqdiGa5ya9BY8ejAJk2aoGvXrg4rdj2xmq2wVseTgpz/Q8SuOnP3qnPZquUrMyIVbVPlsPn4a2/GnmT6Aa5kTFbfI6UMBnm5qm3+4UbNssKzRVVp0v/euxaz71RkGXGW0nLNMlcYLPIPxaNMUT/5soVBkV/pQR6qKgMSinLVe5dL+t3THEZZNu6lCvJiVX6uJ+/t61f1nNjq7he9WXu9Pf1+q/ar1jKrR8t3ptxW9WXWZLh3YWEhjhw5gkcffRTApSOMSUlJMJlM+OKLL+Dn5+dQPzExEf/85z9x7tw5+13OGRkZCAkJQZs2bex11q5d6zBfRkaG/SCe0WhEly5dsGHDBvvjoG02GzZs2OD2g1Cq9G257bbb7APG0tLSSheEEhEREdUF1ZnT+Nxzz2Hz5s04duwYtm7dinvuuQcGgwFDhgxBfn4++vTpg6KiInzwwQfIz89HdnY2srOzYbVeGgT36dMHbdq0waOPPoqffvoJX3/9NSZNmoQxY8bYT4mPHj0av//+O1544QUcOnQI77zzDlauXIlx48bZ25GWloZ//etfWLp0KX755Rc8+eSTKCoqwvDhw91anyr9M6i4uBgvvPACVq5ciZycnErlFStLREREdC2rziONJ0+exJAhQ5CTk4OoqCh069YN27ZtQ1RUFDZt2oTt27cDAJo3b+4w39GjR9G4cWMYDAasWbMGTz75JBITExEYGIjk5GSHeMMmTZrgq6++wrhx4/Dmm2+iYcOGeP/99+0ZjQDw4IMP4vz585g8eTKys7PRqVMnrFu3rtLNMSpVGjQ+//zz+Pbbb7FgwQI8+uijmD9/Pk6dOoV3330XM2bMqMoiiYiIiGqcgOuP8XX3Qq6PP/5Ys6xnz54QQr3E+Pj4SqefnS1rz5490jqpqalun46+UpUGjV9++SU+/PBD9OzZE8OHD0f37t3RvHlzxMfHY9myZRg6dKhHjSIiIiKqCTV5TWNdV6VrGi9cuICmTZsCuHQH0YULFwAA3bp1w5YtW65e64iIiIiqUXVe03i9qdKgsWnTpvbnT7dq1QorV64EcOkI5OUxPERERETXMpcfIejGEcnrVZVOTw8fPhw//fQTbrvtNkyYMAEDBw7EvHnzYLFYMGfOnKvdxhpnvlgOX53z6wx8QuSxGAb/qscYqGIrhCJiwWDUfm/fQHk8UulF7UcgAZ5FJOh8VV8yRZ8q+sVq1m6bsLp6pYpzel9F7k01kq0XoI61sFkk/aKI89D7yv89aQyW7zpk762KdzIYFduDj7xtZQVmabmM+YI8kkfvI/9MVNu63qfqPzi+gfL1Nig+M72Pdr96GsmlK1ftH7S3l3J4FoOksyr2D5JtEQAsRdrvr/q8bOXyfhEWi7TcYNL+TFS/F6pIHWuJYt/nr12k9zBKrPRP+XewPF+yf7Co9k3O+6VUeBC548YRRG8/0lilQePlt3H37t0bhw4dwu7duxEZGYl///vfV61xRERERNXJJi5Nrtb1ZlclmTg+Ph6DBw9GaGgoPvjgg6uxSCIiIqJqZxN6tyZvVvW4eiIiIqI6TohLk6t1vRkHjUREROS1bNDB5uK1iq7Wu15x0EhEREReizmNrnNr0Dh48GBpeW5uridtISIiIqpRPD3tOrcGjaoMxtDQUDz22GMeNYiIiIiopjByx3VuDRoXL15cXe24phj89DDond8hJcthBACDJNdPlaOmosrlMkhuhldl28kyHgF5lhkACEm2lqxdAAB5hKQyr0yV6+cJVZ6hKktRxmaRf56WUnmfy7Y1QN12T8hyGFVUn5cqr1RFtr2o+sw32LM+8wmQ71KNgdrlvgHyL4IsZxEATEFGabnBqP3esjJX3ttcWCotL75QLC2XvneAfHsoL5N/T8xF8uXLMgMtiqxDZY6j/K1hK9duu+q3RkU1v17jNw5w4fdAsW9SUWUxymhloeqEDpDHDWu3h5E7LqsT944fO3YMKSkpaNKkCfz9/dGsWTNMmTIFZrPZoY5Op6s0bdu2rRZbTkRERNc0d54Gw2sar32HDh2CzWbDu+++i+bNm+PAgQMYOXIkioqK8PrrrzvUXb9+Pdq2bWv/u169ejXdXCIiIqojeE2j6+rEoLFv377o27ev/e+mTZvi8OHDWLBgQaVBY7169RATE1PTTSQiIqI6iJE7rqsTp6edycvLQ0RERKXX77rrLkRHR6Nbt2744osvpMsoKytDfn6+w0RERETeo+JIo6uTN6uTg8asrCy8/fbbeOKJJ+yvBQUFYfbs2Vi1ahW++uordOvWDYMGDZIOHKdPn47Q0FD7FBcXVxPNJyIiomuEq9czupPneL2q1UHjhAkTnN68cvl06NAhh3lOnTqFvn374v7778fIkSPtr0dGRiItLQ0JCQn4y1/+ghkzZuCRRx7BrFmzNN9/4sSJyMvLs08nTpyotnUlIiKia0/F3dOuTt6sVq9pfPbZZzFs2DBpnaZNm9r///Tp07j99tvRtWtXvPfee8rlJyQkICMjQ7PcZDLBZDK53F4iIiK6vvBGGNfV6qAxKioKUVFRLtU9deoUbr/9dnTp0gWLFy+WZkxV2Lt3Lxo0aOB2u4yhvjAaqi/7T4sqGyvA319aXpZv1iwrOl8inVeVASnLYVSxlXv2LVPl+vmFaA/8ff3l2Xc2q7xttnJ5HqFVkbUom7+8TL5sHz/5eqvarpfkFarmVeVyqsiyEmVZhQDg6y//h5wq71Sa02iU97mqbSqmED9peUBEgGRe+fdblbupyrfUSfaZ/lHyBzf4BgVKy4tOnZOWy74HlhJFnqAiY9ZPL+83VUakbHvyNOtU9T1TfcdlPM2nlWX3qrJx/RTrZQ62SMvLIrR/q6yKbEyt/Emb1Qr8Jp1VE8O9XVcn7p4+deoUevbsifj4eLz++us4f/68vaziTumlS5fCaDTipptuAgB8+umnWLRoEd5///1aaTMRERFd+2xwI9y7Wlty7asTg8aMjAxkZWUhKysLDRs2dCgTlx0rnjZtGv744w/4+PigVatWWLFiBe67776abi4RERHVETw97bo6MWgcNmyY8trH5ORkJCcn10yDiIiI6LrAQaPr6sSgkYiIiKg62IQONhejdFytd73ioJGIiIi8Fo80uo6DRiIiIvJaNhtgdfEOF0XQyHWPg0YnTMG+MPk4jzNQRSjIYhBU8S+y6BgAMAbJ4zz0Gm0GgLJ8eexE0fkiaXmpX5m03GrW/iapYiVMwUZpeUA97ZgSAPCPCNJedpg8KsTgK/8KmAvk/WYp1o6OAORRI8o4H7M89kYVB2K1VD3uxxQsL1eRbYumIPnn7af4zHwD5PPL3ru8VB4FUl4q/zxVfa76jppCtdfNJ9CzyJ3yIkWsllX7MzWY5H2qk/QpAPgGytc7MFo70sdSLN+3qL4nsighAPALlferj5/2ftkYpJhXEQ+l/A4L7f2mTTGvql9UDEbtfZ9qvfRG+W9ZueIzLcnRfmRv0Xn543zL8p0vW1deXvXIHTee9OLtT4ThoJGIiIi8Fk9Pu46DRiIiIvJa7jwekI8RJCIiIvJSPNLoOg4aiYiIyGtx0Og6DhqJiIjIa/H0tOs4aCQiIiKvxSONruOgkYiIiLyWzeZ6/iJzGqmShonNEGxynkNlKSyVzivLvvKLDJPO63dDjLRc36iptNwSpj2/oUSefSV+3iMtL8j6Q1peeqFQWi7jXy9EWh7UOFZabmjeUrPMGhQhn7fwgrQc589Ki615udJym0U7a81aLM/VsxTIszPLS1TZmdrvrcorVJFl2wGAryRz0DdYnrtpqhcmLTdE1JOWw0eyW7PJs+1Eqfz7bSuRf2Y6gzzPEJJMQZ1Onv8myuW5feV58u+4OV/7O6pst4J/bP0ql5cXyrdzW5k8O1PWp4A6g9IUK9nv1r9BOq81KFxarlNsb7py7XXT556XzmtT7Xs82Fb1gfKsVF1wmLRc+Mj3D2EW7X2X7cKf0nnLzjjfJ+eXmoEfdkrn1VKdRxrT09MxdepUh9datmyJQ4cOAQBKS0vx7LPP4uOPP0ZZWRmSkpLwzjvvoH79/31njh8/jieffBLffvstgoKCkJycjOnTp8Pnsn3dpk2bkJaWhoMHDyIuLg6TJk3CsGHDHN53/vz5mDVrFrKzs9GxY0e8/fbbuOWWW9xaH/m3jYiIiOg6VjFodHVyV9u2bXHmzBn79P3339vLxo0bhy+//BKrVq3C5s2bcfr0aQwePNhebrVaMWDAAJjNZmzduhVLly7FkiVLMHnyZHudo0ePYsCAAbj99tuxd+9ePPPMMxgxYgS+/vpre50VK1YgLS0NU6ZMwY8//oiOHTsiKSkJ586dc2tdOGgkIiIir2XD/26GUU7/N09+fr7DVFamffTUx8cHMTEx9ikyMhIAkJeXhw8++ABz5sxBr1690KVLFyxevBhbt27Ftm3bAADffPMNfv75Z/z73/9Gp06d0K9fP0ybNg3z58+H2XzpSPXChQvRpEkTzJ49G61bt0Zqairuu+8+vPHGG/Y2zJkzByNHjsTw4cPRpk0bLFy4EAEBAVi0aJFbfcVBIxEREXktIYRbEwDExcUhNDTUPk2fPl1z+b/99htiY2PRtGlTDB06FMePHwcA7N69GxaLBb1797bXbdWqFRo1aoTMzEwAQGZmJtq3b+9wujopKQn5+fk4ePCgvc7ly6ioU7EMs9mM3bt3O9TR6/Xo3bu3vY6reE0jERERea2qXNN44sQJhIT873p8k8n587oTEhKwZMkStGzZEmfOnMHUqVPRvXt3HDhwANnZ2TAajQgLC3OYp379+sjOzgYAZGdnOwwYK8orymR18vPzUVJSgosXL8JqtTqtU3Ftpas4aCQiIiKvJdy4e1r8X72QkBCHQaOWfv362f+/Q4cOSEhIQHx8PFauXAl/f+0bBq9VPD1NREREXqu6b4S5XFhYGFq0aIGsrCzExMTAbDYjNzfXoc7Zs2cRE3Pprv6YmBicPXu2UnlFmaxOSEgI/P39ERkZCYPB4LROxTJcxUEjEREReS2Xb4Jx48kxWgoLC3HkyBE0aNAAXbp0ga+vLzZs2GAvP3z4MI4fP47ExEQAQGJiIvbv3+9wl3NGRgZCQkLQpk0be53Ll1FRp2IZRqMRXbp0cahjs9mwYcMGex1X8fS0E0F33YPgQOdZcoai3Cov1xoQKi0vCG8kLT/nI88M+z1Xkl/nJ50VHe/oJC2PbrNXWh5+/pRmmShXZAKGR0mLs5vcKi0/Zdb+l5LZKt/E/cPlbWsUf0xaHpJ3UlruU1qgWearyN3zs8rbpitTZApe0M55s+bmSecVVkW+nCLXzycqUvu9G7eWznshsoW0PKAsV1puLNXOK9Rb5H2mN8uz7QySfDkAsPnJ8+0sAWGaZUIv71NTnjwz1HjiiLRclmeol2VbAvBt3ERaXtS4o7Q8P0A7pzE6+yfpvLrff5GWq/IrDWHyLMXymMaaZbmRzaXzFunlpydtkH+mOmifExU3yI/pBAjtfQsAhBbI900GybZe7uv8+rwKhUHyo1MXDfJ9erlNe3sLMBRL5w0xO89xNBcWAlPfl86rpTpzGp977jkMHDgQ8fHxOH36NKZMmQKDwYAhQ4YgNDQUKSkpSEtLQ0REBEJCQjB27FgkJibir3/9KwCgT58+aNOmDR599FHMnDkT2dnZmDRpEsaMGWO/jnL06NGYN28eXnjhBTz++OPYuHEjVq5cia+++srejrS0NCQnJ+Pmm2/GLbfcgrlz56KoqAjDhw93a304aCQiIiKvJWwCwsVDiK7Wq3Dy5EkMGTIEOTk5iIqKQrdu3bBt2zZERV0aWL/xxhvQ6/W49957HcK9KxgMBqxZswZPPvkkEhMTERgYiOTkZLz88sv2Ok2aNMFXX32FcePG4c0330TDhg3x/vvvIykpyV7nwQcfxPnz5zF58mRkZ2ejU6dOWLduXaWbY1Q4aCQiIiKv5c5pZ3dPT3/88cfScj8/P8yfPx/z58/XrBMfH4+1a9dKl9OzZ0/s2SN/sltqaipSU1OldVQ4aCQiIiKvVZ2np683HDQSERGR17JaBaxW10aDrta7XtWZu6cbN24MnU7nMM2YMcOhzr59+9C9e3f4+fkhLi4OM2fOrKXWEhERUV1Qk5E7dV2dOtL48ssvY+TIkfa/g4OD7f+fn5+PPn36oHfv3li4cCH279+Pxx9/HGFhYRg1alRtNJeIiIiucTYhYHNxNOhqvetVnRo0BgcHawZRLlu2DGazGYsWLYLRaETbtm2xd+9ezJkzx+1B4y+h3RAY5DxKISSqSDqvHtpRJWVCHmOQWxYkLT9+Tp4ef+S4dvSEn5/8oPKFAnm0RKeGEdLymGjteAe9TR6JccG3gbT89zx5fEOJWTvWQnXRsp/RV75s043S8sAQeUySLVi734XQSects8njOlRiWzmPpgCAG87LL5j2KbwoLRcm+bZYGBGvWfanX5x8XqvzuKsKJpN8Wwzx044T8jfLo4Z8rGZpeZlR/h09r5dvy1kXtGOxCkvk39FucUel5bGKmCSjrFAnf28RFCYtLzMFS8tLoB1FVBoq77OAGO0IJQDQK6KnrOHR0vKcqFaaZScs8pgzVaSXanyh12tXsFjl33+LVb5e9fzl/RoYrB25U2KV/1adK5R/D3KL5P2SL0nVMSpGJaGBzvcfxUXy7URG2P73pBdX6nqzOnN6GgBmzJiBevXq4aabbsKsWbNQflk+V2ZmJnr06AGj8X+7xqSkJBw+fBgXLzr/ASwrK0N+fr7DRERERN5DQEAIFyfwSGOd8NRTT6Fz586IiIjA1q1bMXHiRJw5cwZz5swBcOmB3U2aOAbQXv5Q7/DwygGv06dPx9SpU6u/8URERHRNqsqzp71VrR5pnDBhQqWbW66cDh06BOBSmnnPnj3RoUMHjB49GrNnz8bbb7+NsjL5ExpkJk6ciLy8PPt04sSJq7VqREREVAe4fJTx/yZvVqtHGp999lkMGzZMWqdp06ZOX09ISEB5eTmOHTuGli1buvRQ7yuZTCb7Y3iIiIjI+1RnuPf1plYHjVFRUfZH6bhr79690Ov1iI6+dDFwYmIiXnzxRVgsFvj6Xrq5ISMjAy1btnR6apqIiIioOh8jeL2pEzfCZGZmYu7cufjpp5/w+++/Y9myZRg3bhweeeQR+4Dw4YcfhtFoREpKCg4ePIgVK1bgzTffRFpaWi23noiIiK5VzGl0XZ24EcZkMuHjjz9Geno6ysrK0KRJE4wbN85hQBgaGopvvvkGY8aMQZcuXRAZGYnJkyczo5GIiIg02WwCNhePILpa73pVJwaNnTt3xrZt25T1OnTogO+++87j9zuVH4QAq/McKj9feYacTFm5/MCuXh7bB52iPCRYO9erzCzf0AsluVkA8EdumHz+QD/NMqsij7C4RJ6VWFQmzysrNWsv3yKPiERBsXzZOXp5uV6nvd4A4Cv5hhkkGW0AYCmX95u/SX4bn58hTPu9oztI5zVEyTuuUMhz+S6UOc85BYCCPPnnbbHKvyeh/vIsRZtJe/4yo/zz0unkn0mpTT5/Tqm8X2w27c/0hgj5TX0GIf9MygNDpeW+YdoZkbDJMx5tip1T6J9HpOVhloOaZTqzPGdRZ5Z/3sJfOwMSAMxBkvUGcNZaX7Ps6Hl5HmGx4j7MAMUl8z4G7e3NKtlWAPX+o7hMsS1KvsMlZfL3tiruIDYozmGaJLsA1bKLNdqmarOMOze48EYYIiIiIi/FcG/XcdBIREREXouPEXQdB41ERETktXh62nUcNBIREZHX4o0wruOgkYiIiLyWO1E6Xn6gkYNGIiIi8l5CuBHu7eWjRg4aiYiIyGsJN26E4aCRKjmfq4e/xXnQVIAkAw4A/IzaG5QskwsAoIiZUuXy3RCtvYByRfadTREjUFgin7+wRDu/UpW75Wl5qSQrrbRMPrOPQd7pPj6qcmkx/CQfuVGxbF8f+fYiFPmXp/O18+tOCXm2XalZ/nmXyqPzUC6J/dMrMtxU22KZRR5+ZwnUfgNfgzx3T5XTaLHKcztVvycRgdoba5ipRDqvWSdf76KQWGl5gCRz1FBWJJ1XZ5VnROosqqxFSblVnhEpfOS5nkKRvWn1kZcb9drrJtufA0CZxbM8Q5vkO6ycV5HjqMr19YRq2aq2l1m0yyySMgAo09j3lCiyhmX4GEHXcdBIREREXstqFbBaXRsMulrvesVBIxEREXktHml0HQeNRERE5LWY0+g6DhqJiIjIa9lsrucvqq65vt5x0EhERERei0caXcdBIxEREXktXtPoOg4anfAzXpqcUW0vpWbtLAKdIqdAr4gx8CRCQRWBoGJQxKTI4oQUyTHS2AlXmCSJHIH+8obLomEAdb8JRXmRJEVFHnICqDKY1P/grXq/WhUbumq9ZXSKbUmv2NBz8uTluYUaX16ov0OqPlXNryqXbasXTPJInWB/eVxQiDFCWh4QGqdZ5q+Xx/346uQZS3oPNgi9kH8JjeWKtlnkWSvlBu3tAQCCDIWaZY3C5XE/F/y0o8YAoLBU/hMr+71Q7JqUv0Xl8pQkabmnB9NUsVoyvvIuh1GrSxXrK8NBo+s4aCQiIiKvZYPr4d42cNBIRERE5JV4pNF1HDQSERGR1+KNMK7joJGIiIi8lrAJlyN3eKSRiIiIyEvx9LTrPLjHiYiIiKhuqzg97epUVTNmzIBOp8Mzzzxjfy07OxuPPvooYmJiEBgYiM6dO+M///mPw3wXLlzA0KFDERISgrCwMKSkpKCw0PGu/3379qF79+7w8/NDXFwcZs6cWen9V61ahVatWsHPzw/t27fH2rVr3V4HDhqJiIjIawmbza2pKnbu3Il3330XHTp0cHj9sccew+HDh/HFF19g//79GDx4MB544AHs2bPHXmfo0KE4ePAgMjIysGbNGmzZsgWjRo2yl+fn56NPnz6Ij4/H7t27MWvWLKSnp+O9996z19m6dSuGDBmClJQU7NmzB4MGDcKgQYNw4MABt9ZDJ7z9qs7L5OfnIzQ0FO9+lQv/wBCndVRHpmV5hqoMN6Mi0NDXR76x+kqyEg16+bJ1HsYICEkmYLlVvuIWq/zfLuZyxfySclUOo+rzVO0flDmPknL1vPLGqTIkZfOXl6uWLS+3KdomW77FIm94ebm8XLXL8vHR3p5MJoN0Xl+jfFs0KMJUjUZ5uUmyfB950+CjuJjIV1Hub9TuN39T1fctAFBuk6+3TVIe7GeRzhtilOc0+hvKpOU+OvkXzSo5dmK2yTMeC8z+8vIyeehgqUX7vWX7NQCwKvrckwxaVS6valuT5faqqH4vtPbJJUX5eGJAGPLy8hAS4vz3+0oVv/n3pO6HrynYpXksZQX4bF57t96nsLAQnTt3xjvvvINXXnkFnTp1wty5cwEAQUFBWLBgAR599FF7/Xr16uG1117DiBEj8Msvv6BNmzbYuXMnbr75ZgDAunXr0L9/f5w8eRKxsbFYsGABXnzxRWRnZ8NovLTNTpgwAatXr8ahQ4cAAA8++CCKioqwZs0a+/v89a9/RadOnbBw4UKX1gPgkUYiIiLyYlU5PZ2fn+8wlZVp/+NlzJgxGDBgAHr37l2prGvXrlixYgUuXLgAm82Gjz/+GKWlpejZsycAIDMzE2FhYfYBIwD07t0ber0e27dvt9fp0aOHfcAIAElJSTh8+DAuXrxor3Pl+yclJSEzM9OtvqoTg8ZNmzZBp9M5nXbu3AkAOHbsmNPybdu21XLriYiI6FpVcSOMqxMAxMXFITQ01D5Nnz7d6bI//vhj/Pjjj5rlK1euhMViQb169WAymfDEE0/gs88+Q/PmzQFcuuYxOjraYR4fHx9EREQgOzvbXqd+/foOdSr+VtWpKHdVnbh7umvXrjhz5ozDay+99BI2bNjgMPoGgPXr16Nt27b2v+vVq1cjbSQiIqK6pyp3T584ccLh9LTJyWNAT5w4gaeffhoZGRnw8/NzuryXXnoJubm5WL9+PSIjI7F69Wo88MAD+O6779C+ffsqrE31qhODRqPRiJiYGPvfFosFn3/+OcaOHVvpec716tVzqEtERESkxQYbbC4+P92GS/VCQkKU1zTu3r0b586dQ+fOne2vWa1WbNmyBfPmzcPhw4cxb948HDhwwH6wq2PHjvjuu+8wf/58LFy4EDExMTh37pzDcsvLy3HhwgX7WCcmJgZnz551qFPxt6qOu+OlOnF6+kpffPEFcnJyMHz48Epld911F6Kjo9GtWzd88cUX0uWUlZVVui6BiIiIvIewuXOK2vXl3nHHHdi/fz/27t1rn26++WYMHToUe/fuRXFxMQBAr3ccihkMBtj+746fxMRE5ObmYvfu3fbyjRs3wmazISEhwV5ny5YtsFj+d1NZRkYGWrZsifDwcHudDRs2OLxPRkYGEhMTXV8h1NFB4wcffICkpCQ0bNjQ/lpQUBBmz56NVatW4auvvkK3bt0waNAg6cBx+vTpDtckxMXF1UTziYiI6BpRlWsaXREcHIx27do5TIGBgahXrx7atWuHVq1aoXnz5njiiSewY8cOHDlyBLNnz0ZGRgYGDRoEAGjdujX69u2LkSNHYseOHfjhhx+QmpqKhx56CLGxsQCAhx9+GEajESkpKTh48CBWrFiBN998E2lpafa2PP3001i3bh1mz56NQ4cOIT09Hbt27UJqaqpbfVWrg8YJEyZo3uBSMVXcLl7h5MmT+Prrr5GSkuLwemRkJNLS0pCQkIC//OUvmDFjBh555BHMmjVL8/0nTpyIvLw8+3TixIlqWU8iIiK6NlmtVremq8XX1xdr165FVFQUBg4ciA4dOuDDDz/E0qVL0b9/f3u9ZcuWoVWrVrjjjjvQv39/dOvWzSGDMTQ0FN988w2OHj2KLl264Nlnn8XkyZMdshy7du2K5cuX47333kPHjh3xySefYPXq1WjXrp1bba7VnMbz588jJydHWqdp06YOt5FPmzYNb7/9Nk6dOgVfX3kG1vz58/HKK69UuolGS0Vm0+ofziIwSCunUZ4h5UlvqnIc9Tr5wmXlOtW88rdWzu9JzqMs4xEArMo+1y5XfV7KrDObvGdU+XTyDElVzpqq7dJiabkqf9KTZQNAebl2mdlS9YxHALAo59dunHrZnmVEqhgM2p+pn5/8EvPAQHmQo59JlRGpXabKiDTJd7UIUOQ8+vlq/9CafOQ/wr56z8r1uqqFMQOATSgyZG3yz8xslXesKqNWRvV7odony+ZX/daoeJr7WxVFhfm4+9aYKuU09nlsO3yNQS7NYzEX4psPE9x6n+tJrd4IExUVhaioKJfrCyGwePFiPPbYY8oBIwDs3bsXDRo08KSJREREdB0Twgbh4sWKrta7XtWJu6crbNy4EUePHsWIESMqlS1duhRGoxE33XQTAODTTz/FokWL8P7779d0M4mIiKiOqErkjreqU4PGDz74AF27dkWrVq2clk+bNg1//PEHfHx80KpVK6xYsQL33XdfDbeSiIiI6gx3bnDhoLHuWL58uWZZcnIykpOTa7A1REREVNfZhBs5jTw9TUREROSdeHradRw0EhERkdcSwgahipS4rK4346DRCYPeBoPe+YahSKaos5RfA0V0DRSxOdJFK5dddcqYIoO83EdjO3CVMFY9DsjTMCxZlJGny67Otqu2B9UnIiQxKZ5ua6roKU9I0nj+773l0TKeRHZ5Gt9i0Fc9kkvVp+rYm+rbf3gaHWM0yD8zVblMde43Va7FYZNesQ3K8Eij6zhoJCIiIq/FyB3XcdBIREREXstmA2wuHkF08Sz2dYuDRiIiIvJawubGNY1ePmrkoJGIiIi8Fq9pdB0HjUREROS1eE2j6zhoJCIiIq/FI42u46DxMuL/ckKKiwpquSXepTajI1SqM/amNiN3PEwSucYjd6q+bJXajdxRxNp4ELmjSsyqzsgdVVSKPHCnej8T1XpLv2PVjJE7jip+t0UVdj7l5gKXr1W0lhe5vfzrCQeNl8nJyQEAPNi7RS23hIiIiNxVUFCA0NBQl+oajUbExMRg14YH3HqPmJgYGI3GqjSvztOJqgzLr1O5ubkIDw/H8ePHXd7ovEF+fj7i4uJw4sQJhISE1HZzrhnsF+fYL86xX5xjvzjHfnFOq1+EECgoKEBsbCz0etXx6f8pLS2F2Wx2qw1GoxF+fn5uzXO94JHGy1RsaKGhofySOhESEsJ+cYL94hz7xTn2i3PsF+fYL84565eqHOzx8/Pz2gFgVbg+HCciIiIir8VBIxEREREpcdB4GZPJhClTpsBkMtV2U64p7Bfn2C/OsV+cY784x35xjv3iHPuldvFGGCIiIiJS4pFGIiIiIlLioJGIiIiIlDhoJCIiIiIlDhqJiIiISImDxsvMnz8fjRs3hp+fHxISErBjx47ablKNSk9Ph06nc5hatWplLy8tLcWYMWNQr149BAUF4d5778XZs2drscXVY8uWLRg4cCBiY2Oh0+mwevVqh3IhBCZPnowGDRrA398fvXv3xm+//eZQ58KFCxg6dChCQkIQFhaGlJQUFBYW1uBaXH2qfhk2bFil7adv374Oda63fpk+fTr+8pe/IDg4GNHR0Rg0aBAOHz7sUMeV783x48cxYMAABAQEIDo6Gs8//zzKy8trclWuKlf6pWfPnpW2l9GjRzvUud76ZcGCBejQoYM9mDoxMRH//e9/7eXeuK0A6n7xxm3lWsVB4/9ZsWIF0tLSMGXKFPz444/o2LEjkpKScO7cudpuWo1q27Ytzpw5Y5++//57e9m4cePw5ZdfYtWqVdi8eTNOnz6NwYMH12Jrq0dRURE6duyI+fPnOy2fOXMm3nrrLSxcuBDbt29HYGAgkpKSUFpaaq8zdOhQHDx4EBkZGVizZg22bNmCUaNG1dQqVAtVvwBA3759Hbaf//f//p9D+fXWL5s3b8aYMWOwbds2ZGRkwGKxoE+fPigqKrLXUX1vrFYrBgwYALPZjK1bt2Lp0qVYsmQJJk+eXBurdFW40i8AMHLkSIftZebMmfay67FfGjZsiBkzZmD37t3YtWsXevXqhbvvvhsHDx4E4J3bCqDuF8D7tpVrliAhhBC33HKLGDNmjP1vq9UqYmNjxfTp02uxVTVrypQpomPHjk7LcnNzha+vr1i1apX9tV9++UUAEJmZmTXUwpoHQHz22Wf2v202m4iJiRGzZs2yv5abmytMJpP4f//v/wkhhPj5558FALFz5057nf/+979Cp9OJU6dO1Vjbq9OV/SKEEMnJyeLuu+/WnMcb+uXcuXMCgNi8ebMQwrXvzdq1a4VerxfZ2dn2OgsWLBAhISGirKysZlegmlzZL0IIcdttt4mnn35acx5v6BchhAgPDxfvv/8+t5UrVPSLENxWriU80gjAbDZj9+7d6N27t/01vV6P3r17IzMzsxZbVvN+++03xMbGomnTphg6dCiOHz8OANi9ezcsFotDH7Vq1QqNGjXyqj46evQosrOzHfohNDQUCQkJ9n7IzMxEWFgYbr75Znud3r17Q6/XY/v27TXe5pq0adMmREdHo2XLlnjyySeRk5NjL/OGfsnLywMAREREAHDte5OZmYn27dujfv369jpJSUnIz893ONJSl13ZLxWWLVuGyMhItGvXDhMnTkRxcbG97HrvF6vVio8//hhFRUVITEzktvJ/ruyXCt68rVxLfGq7AdeCP//8E1ar1WGDA4D69evj0KFDtdSqmpeQkIAlS5agZcuWOHPmDKZOnYru3bvjwIEDyM7OhtFoRFhYmMM89evXR3Z2du00uBZUrKuzbaWiLDs7G9HR0Q7lPj4+iIiIuK77qm/fvhg8eDCaNGmCI0eO4B//+Af69euHzMxMGAyG675fbDYbnnnmGdx6661o164dALj0vcnOzna6PVWU1XXO+gUAHn74YcTHxyM2Nhb79u3D+PHjcfjwYXz66acArt9+2b9/PxITE1FaWoqgoCB89tlnaNOmDfbu3evV24pWvwDeu61cizhoJLt+/frZ/79Dhw5ISEhAfHw8Vq5cCX9//1psGdUFDz30kP3/27dvjw4dOqBZs2bYtGkT7rjjjlpsWc0YM2YMDhw44HAdMGn3y+XXsrZv3x4NGjTAHXfcgSNHjqBZs2Y13cwa07JlS+zduxd5eXn45JNPkJycjM2bN9d2s2qdVr+0adPGa7eVaxFPTwOIjIyEwWCodJfa2bNnERMTU0utqn1hYWFo0aIFsrKyEBMTA7PZjNzcXIc63tZHFesq21ZiYmIq3UBVXl6OCxcueFVfNW3aFJGRkcjKygJwffdLamoq1qxZg2+//RYNGza0v+7K9yYmJsbp9lRRVpdp9YszCQkJAOCwvVyP/WI0GtG8eXN06dIF06dPR8eOHfHmm296/bai1S/OeMu2ci3ioBGXNtYuXbpgw4YN9tdsNhs2bNjgcE2FtyksLMSRI0fQoEEDdOnSBb6+vg59dPjwYRw/ftyr+qhJkyaIiYlx6If8/Hxs377d3g+JiYnIzc3F7t277XU2btwIm81m39l5g5MnTyInJwcNGjQAcH32ixACqamp+Oyzz7Bx40Y0adLEodyV701iYiL279/vMKDOyMhASEiI/fRcXaPqF2f27t0LAA7by/XWL87YbDaUlZV57baipaJfnPHWbeWaUNt34lwrPv74Y2EymcSSJUvEzz//LEaNGiXCwsIc7sa63j377LNi06ZN4ujRo+KHH34QvXv3FpGRkeLcuXNCCCFGjx4tGjVqJDZu3Ch27dolEhMTRWJiYi23+uorKCgQe/bsEXv27BEAxJw5c8SePXvEH3/8IYQQYsaMGSIsLEx8/vnnYt++feLuu+8WTZo0ESUlJfZl9O3bV9x0001i+/bt4vvvvxc33nijGDJkSG2t0lUh65eCggLx3HPPiczMTHH06FGxfv160blzZ3HjjTeK0tJS+zKut3558sknRWhoqNi0aZM4c+aMfSouLrbXUX1vysvLRbt27USfPn3E3r17xbp160RUVJSYOHFibazSVaHql6ysLPHyyy+LXbt2iaNHj4rPP/9cNG3aVPTo0cO+jOuxXyZMmCA2b94sjh49Kvbt2ycmTJggdDqd+Oabb4QQ3rmtCCHvF2/dVq5VHDRe5u233xaNGjUSRqNR3HLLLWLbtm213aQa9eCDD4oGDRoIo9EobrjhBvHggw+KrKwse3lJSYn4+9//LsLDw0VAQIC45557xJkzZ2qxxdXj22+/FQAqTcnJyUKIS7E7L730kqhfv74wmUzijjvuEIcPH3ZYRk5OjhgyZIgICgoSISEhYvjw4aKgoKAW1ubqkfVLcXGx6NOnj4iKihK+vr4iPj5ejBw5stI/uq63fnHWHwDE4sWL7XVc+d4cO3ZM9OvXT/j7+4vIyEjx7LPPCovFUsNrc/Wo+uX48eOiR48eIiIiQphMJtG8eXPx/PPPi7y8PIflXG/98vjjj4v4+HhhNBpFVFSUuOOOO+wDRiG8c1sRQt4v3rqtXKt0QghRc8c1iYiIiKgu4jWNRERERKTEQSMRERERKXHQSERERERKHDQSERERkRIHjURERESkxEEjERERESlx0EhEREREShw0EhEREZESB41E5BWOHTsGnU5nf27t1abT6bB69epqWTYR0bWAg0YiqhHDhg3DoEGDau394+LicObMGbRr1w4AsGnTJuh0OuTm5tZam4iI6hKf2m4AEVFNMBgMiImJqe1mEBHVWTzSSES1bvPmzbjllltgMpnQoEEDTJgwAeXl5fbynj174qmnnsILL7yAiIgIxMTEID093WEZhw4dQrdu3eDn54c2bdpg/fr1DqeMLz89fezYMdx+++0AgPDwcOh0OgwbNgwA0LhxY8ydO9dh2Z06dXJ4v99++w09evSwv1dGRkaldTpx4gQeeOABhIWFISIiAnfffTeOHTvmaVcREdUaDhqJqFadOnUK/fv3x1/+8hf89NNPWLBgAT744AO88sorDvWWLl2KwMBAbN++HTNnzsTLL79sH6xZrVYMGjQIAQEB2L59O9577z28+OKLmu8ZFxeH//znPwCAw4cP48yZM3jzzTddaq/NZsPgwYNhNBqxfft2LFy4EOPHj3eoY7FYkJSUhODgYHz33Xf44YcfEBQUhL59+8JsNrvTPURE1wyeniaiWvXOO+8gLi4O8+bNg06nQ6tWrXD69GmMHz8ekydPhl5/6d+2HTp0wJQpUwAAN954I+bNm4cNGzbgzjvvREZGBo4cOYJNmzbZT0H/85//xJ133un0PQ0GAyIiIgAA0dHRCAsLc7m969evx6FDh/D1118jNjYWAPDqq6+iX79+9jorVqyAzWbD+++/D51OBwBYvHgxwsLCsGnTJvTp08e9TiIiugZw0EhEteqXX35BYmKifXAFALfeeisKCwtx8uRJNGrUCMClQePlGjRogHPnzgG4dLQwLi7O4ZrFW265pdraGxcXZx8wAkBiYqJDnZ9++glZWVkIDg52eL20tBRHjhyplnYREVU3DhqJqE7w9fV1+Fun08Fms13199Hr9RBCOLxmsVjcWkZhYSG6dOmCZcuWVSqLioryqH1ERLWFg0YiqlWtW7fGf/7zHwgh7Ecbf/jhBwQHB6Nhw4YuLaNly5Y4ceIEzp49i/r16wMAdu7cKZ3HaDQCuHQ95OWioqJw5swZ+9/5+fk4evSoQ3tPnDiBM2fOoEGDBgCAbdu2OSyjc+fOWLFiBaKjoxESEuLSOhARXet4IwwR1Zi8vDzs3bvXYRo1ahROnDiBsWPH4tChQ/j8888xZcoUpKWl2a9nVLnzzjvRrFkzJCcnY9++ffjhhx8wadIkAHA47X25+Ph46HQ6rFmzBufPn0dhYSEAoFevXvjoo4/w3XffYf/+/UhOTobBYLDP17t3b7Ro0QLJycn46aef8N1331W66Wbo0KGIjIzE3Xffje+++w5Hjx7Fpk2b8NRTT+HkyZNV6ToiolrHQSMR1ZhNmzbhpptucpimTZuGtWvXYseOHejYsSNGjx6NlJQU+6DPFQaDAatXr0ZhYSH+8pe/YMSIEfaBnJ+fn9N5brjhBkydOhUTJkxA/fr1kZqaCgCYOHEibrvtNvztb3/DgAEDMGjQIDRr1sw+n16vx2effYaSkhLccsstGDFiBP75z386LDsgIABbtmxBo0aNMHjwYLRu3RopKSkoLS3lkUciqrN04sqLd4iIrgM//PADunXrhqysLIdBHxERVQ0HjUR0Xfjss88QFBSEG2+8EVlZWXj66acRHh6O77//vrabRkR0XeCNMER0XSgoKMD48eNx/PhxREZGonfv3pg9e3ZtN4uI6LrBI41EREREpMQbYYiIiIhIiYNGIiIiIlLioJGIiIiIlDhoJCIiIiIlDhqJiIiISImDRiIiIiJS4qCRiIiIiJQ4aCQiIiIipf8PtKEC1HELSjkAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAo0AAAFJCAYAAAD+PBdyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB0kUlEQVR4nO3dd3hUVeI+8Hd6eu8SQu9FCZpvkCoxoeiKshZEBUQsC6igKOwqRXSxgaCgrA38KS7IqqiIaABBkYCIIIKCgCAgCT29zGTm/P5gM8uQuefMZAgp836eZx7InHPvPffMvZOTW96rE0IIEBERERFJ6Ou6AURERERU/3HQSERERERKHDQSERERkRIHjURERESkxEEjERERESlx0EhEREREShw0EhEREZESB41EREREpMRBIxEREREpcdBI5IFmzZph5MiRzp/Xr18PnU6H9evXX7Rl6HQ6TJ8+/aLNj+qf6dOnQ6fT1Wjavn37om/fvh7VLS4uRlxcHJYsWVKjZZErm82G5ORkvPrqq3XdFKI6xUEj1XuLFy+GTqdzvgICAtCmTRuMGzcOx48fr+vmeWXVqlUcGNaBX375BdOnT8ehQ4dqfVmlpaWYPn36Rf2Dwlvz5s1DaGgobrvttlpbxvbt2xEaGgq9Xo8PPvhAs97WrVsxbtw4dOzYEcHBwWjatCluueUW/Pbbb5rTfPbZZ9Dr9cjLy6tR27744guYTCYEBgZi48aNmvXWrl2Lu+++G23atEFQUBBatGiBe+65B7m5uS71TCYTJk6ciGeeeQbl5eU1ahNRoyCI6rlFixYJAOKpp54S7777rnjjjTfEiBEjhF6vF82bNxclJSW13oaUlBQxYsQI5892u12UlZUJu93u1XzGjh0rtHa7srIyYbPZfGkmaVi+fLkAIL7++utaX9bJkycFADFt2rRqZTabTZSVldVovn369BF9+vRR1rNarSI2Nlb885//rNFyPHHo0CGRmJgokpKSRLdu3YTFYhHffPON27pDhw4VCQkJYvz48eKNN94QM2fOFPHx8SI4OFj8/PPPbqe57777RPfu3WvUth9++EGEhISIjh07ihYtWoioqCjx66+/uq2bmpoqmjdvLh577DHxxhtviClTpojQ0FARHx8vcnNzXeqePXtWmM1m8dZbb9WoXUSNgbFuh6xEnhs4cCC6d+8OALjnnnsQHR2NOXPm4JNPPsGwYcPcTlNSUoLg4OCL3ha9Xo+AgICLOs+LPT+qf4xGI4zG2v3aXblyJU6ePIlbbrmlVuZ/9uxZDBw4EHq9HuvXr0dMTAyuvfZa3HDDDdi0aRPatWvnUn/ixIl4//33YTabne/deuut6Ny5M5599lm899571ZaxatUq3H333V637dChQ7juuuvQrFkzrFu3DmVlZejXrx8GDhyIzZs3Iz4+3qX+nDlz0LNnT+j1/zvpNmDAAPTp0wfz58/H008/7Xw/IiICmZmZWLx4cY3aRtQY8PQ0NVjXXHMNAODgwYMAgJEjRyIkJAQHDhzAoEGDEBoaiuHDhwMAHA4H5s6di44dOyIgIADx8fG47777cPbsWZd5CiHw9NNPo0mTJggKCkK/fv2we/fuasvWuqZxy5YtGDRoECIjIxEcHIwuXbpg3rx5zvYtWLAAAFxOt1dxd03j9u3bMXDgQISFhSEkJAT9+/fH5s2bXepUnb7/7rvvMHHiRMTGxiI4OBg33ngjTp486VK3oKAAe/bsQUFBgbJ/HQ4Hpk+fjqSkJGdf/PLLL9Wu7wSA/Px8PPzww0hOTobFYkGrVq3w3HPPweFwuNQrKSnBI4884qzXtm1bvPjiixBCuNTT6XQYN24clixZgrZt2yIgIACpqan45ptvqrVT1UeLFy/GzTffDADo16+fs9/P/+y++OIL9OrVC8HBwQgNDcXgwYOrfe5V29eff/6JIUOGICQkBLGxsXj00Udht9sBnBu0xMbGAgBmzJjhXFbV5+rumsZFixbhmmuuQVxcHCwWCzp06IDXXntN8eloW7FiBZo1a4aWLVu6bf/hw4dx3XXXISQkBJdddplzm/z5559xzTXXIDg4GCkpKXj//ferzbuiogI33HADioqKsGHDBrRq1QoRERHIzs5Gq1atMGDAgGqnlHv06OEyYASA1q1bo2PHjvj111+rLePnn3/GkSNHMHjwYOd7r7zyCjp27IigoCBERkaie/fu1dp35swZDBw4ELGxsVi3bh1iY2PRtGlTrF+/Hnq9HoMHD0ZJSYnLNL1793YZMFa9FxUV5bZt1157LTZu3IgzZ85UKyPyBxw0UoN14MABAEB0dLTzvcrKSmRlZSEuLg4vvvgihg4dCgC47777MGnSJFx99dWYN28eRo0ahSVLliArKws2m805/dSpU/Hkk0+ia9eueOGFF9CiRQtkZmZW+2XjTnZ2Nnr37o1ffvkFDz30EGbPno1+/fph5cqVzjZce+21AIB3333X+dKye/du9OrVCz/99BMee+wxPPnkkzh48CD69u2LLVu2VKs/fvx4/PTTT5g2bRoeeOABfPbZZxg3bpxLnY8//hjt27fHxx9/rFyfKVOmYMaMGejevTteeOEFtG7dGllZWdX6orS0FH369MF7772Hu+66Cy+//DKuvvpqTJkyBRMnTnTWE0LgL3/5C1566SUMGDAAc+bMQdu2bTFp0iSXelU2bNiAhx9+GHfccQeeeuopnD59GgMGDMCuXbu86qPevXvjwQcfBAD8/e9/d/Z7+/btAZz7LAYPHoyQkBA899xzePLJJ/HLL7+gZ8+e1a6BtNvtyMrKQnR0NF588UX06dMHs2fPxuuvvw4AiI2NdQ74brzxRueybrrpJs1+fu2115CSkoK///3vmD17NpKTk/G3v/3NOZjz1qZNm9CtWze3ZXa7HQMHDkRycjKef/55NGvWDOPGjcPixYsxYMAAdO/eHc899xxCQ0Nx1113Of8gA859fnfddRcOHTqEDRs2uAxKqwaO8fHxGDRoEIqLi6VtFELg+PHjiImJqVa2atUqxMXFOc8qvPHGG3jwwQfRoUMHzJ07FzNmzMDll1/usg9UDWbNZrNzwFglOTkZ69evR35+Pm6++WZUVlZK21ZcXIzi4mK3bUtNTYUQAps2bZLOg6jRqstz40SeqLqmcc2aNeLkyZPiyJEjYunSpSI6OloEBgaKo0ePCiGEGDFihAAgJk+e7DL9t99+KwCIJUuWuLy/evVql/dPnDghzGazGDx4sHA4HM56f//73wUAl2sav/76a5dr5CorK0Xz5s1FSkqKOHv2rMtyzp+X7JpGXHAd3JAhQ4TZbBYHDhxwvnfs2DERGhoqevfuXa1/MjIyXJY1YcIEYTAYRH5+frW6ixYtctuGKnl5ecJoNIohQ4a4vD99+vRqfTFz5kwRHBwsfvvtN5e6kydPFgaDQRw+fFgIIcSKFSsEAPH000+71PvrX/8qdDqd2L9/v0tfABA//PCD870//vhDBAQEiBtvvNHrPtK6prGoqEhERESIMWPGVFv/8PBwl/ertq+nnnrKpe4VV1whUlNTnT/LrmmcNm1atc+/tLS0Wr2srCzRokULl/c8uabRZrMJnU4nHnnkkWplVe0//1rHs2fPisDAQKHT6cTSpUud7+/Zs0dzHS6Gd999VwBwe31gr169XLavG264QXTs2LFW2uHOzJkzBQCxdu3aamXHjh0TAMRzzz13ydpDVJ/wSCM1GBkZGYiNjUVycjJuu+02hISE4OOPP8Zll13mUu+BBx5w+Xn58uUIDw/Htddei1OnTjlfqampCAkJwddffw0AWLNmDaxWK8aPH+9yCvHhhx9Wtm379u04ePAgHn74YURERLiU1SRixW6346uvvsKQIUPQokUL5/uJiYm4/fbbsXHjRhQWFrpMc++997osq1evXrDb7fjjjz+c740cORJCiGqnly+0du1aVFZW4m9/+5vL++PHj69Wd/ny5ejVqxciIyNd+jcjIwN2u915SnnVqlUwGAzOo35VHnnkEQgh8MUXX7i8n56ejtTUVOfPTZs2xQ033IAvv/wSdru9Rn10oezsbOTn52PYsGEubTcYDEhLS3NuG+e7//77XX7u1asXfv/9d+lyZAIDA53/LygowKlTp9CnTx/8/vvvHl1GcL4zZ85ACIHIyEjNOvfcc4/z/xEREWjbti2Cg4NdroFs27YtIiIifFovLXv27MHYsWORnp6OESNGuJTl5+cjJyfH5dR0REQEjh49iq1bt170tlzom2++wYwZM3DLLbc4L385X1W/njp1qtbbQlQf8UYYajAWLFiANm3awGg0Ij4+Hm3btq12PZLRaESTJk1c3tu3bx8KCgoQFxfndr4nTpwAAOfgqnXr1i7lsbGx0l/CwP9OlXfq1MnzFZI4efIkSktL0bZt22pl7du3h8PhwJEjR9CxY0fn+02bNnWpV9XmC6/b9ERVX7Rq1crl/aioqGp9sW/fPuzcudPllOD5zu/fpKQkhIaGVluf85dZ5cLPAQDatGmD0tJS57Wa3vbRhfbt2wcAbgcIABAWFubyc0BAQLX1jIyMrFEfV/nuu+8wbdo05OTkoLS01KWsoKAA4eHhXs9TXHCNaBV37Q8PD0eTJk2q/XETHh7u03q5k5eXh8GDByM8PBz/+c9/YDAYXMq//PJLAEBmZqbzvccffxxr1qzBVVddhVatWiEzMxO33347rr766ovatj179uDGG29Ep06d8Oabb7qtU9WvNc3aJGroOGikBuOqq65yXuekxWKxVBtIOhwOadCx1mCnobnw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3EFDWaG0HIXy3M/y3BOaZSW5p6XTWovlbZNt5yoOq/z3gbC634eFYjqZmpyePnLkCMLC/vdlZLG4/10ycOBA5/+7dOmCtLQ0pKSk4IMPPkD79u0BAPfddx9GjRoFALjiiiuwdu1avP3225oD0brEQSMRERH5L51e/dSA8+sCCAsLcxk0eioiIgJt2rTB/v37cc011wAAOnTo4FKnffv2zlPYCQkJOHHCdYBfWVmJM2fOOK+TTEhIwPHjrmHwVT+r6mhda6mFp6eJiIjIf3l857QO8DGnsbi4GAcOHEBiYiKaNWuGpKQk7N2716XOb7/9hpSUFABAeno68vPzsW3bNmf5unXr4HA4kJaW5qzzzTffwGb739HW7OxstG3bFpGRkc46a9eudVlOdnY20tPTvWp/gxk0qrKOysvLMXbsWERHRyMkJARDhw6tNqomIiIiOp9Op/fq5Y1HH30UGzZswKFDh7Bp0ybceOONMBgMGDZsGHQ6HSZNmoSXX34Z//nPf7B//348+eST2LNnD0aPHg3g3FHHAQMGYMyYMfj+++/x3XffYdy4cbjtttuQlJQEALj99tthNpsxevRo7N69G8uWLcO8efMwceJEZzseeughrF69GrNnz8aePXswffp0/PDDDxg3bpxX69OgTk937NgRa9ascf5sNP6v+RMmTMDnn3+O5cuXIzw8HOPGjcNNN92E7777ri6aSkRERA2BN0cQvTzSePToUQwbNgynT59GbGwsevbsic2bNyM29ty1tA8//DDKy8sxYcIEnDlzBl27dkV2djZatmzpnMeSJUswbtw49O/fH3q9HkOHDsXLL7/sLA8PD8dXX32FsWPHIjU1FTExMZg6dapLlmOPHj3w/vvv44knnsDf//53tG7dGitWrECnTp28Wp8GNWjUyjoqKCjAW2+9hffff995jcCiRYvQvn17bN68Gf/3f/93qZtKREREDUBtPnt66dKlyjqTJ092yWm8UFRUFN5//33pPLp06YJvv/1WWufmm2/GzTffrGyPTIM5PQ1oZx1t27YNNpvNJYOoXbt2aNq0qTSDqKKiolrWEhEREfkPz58G4/ld1o1Vgxk0yrKO8vLyYDabERER4TKNKoNo1qxZLjlLycnJtbwWREREVK/odFXPEvTg5d+DxgZzelqWdRQYqJ2NKDNlyhSXC0ULCwuRnJwMXUUJ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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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z57Fjh07AADff/894uLi0L9/f9twvr6+eP7551FUVFTldOrVLl68iPXr1+ORRx5BYWGhre0XLlxAeno6Dh06hJMnT9qNM2zYMLs4nTvvvBMWiwV//vmndF5qrvx+i4uLcf78eXTq1AlCCOzcudPl6V24cAHh4eGq9SFDhti1v2PHjhBCYMiQIbb39Ho9br31Vvzf//2fy/PXUlJSgocffhj+/v548803q9S///57u9PXYWFhAIBvvvkGVqu12ttzpbNnz+Lxxx9HUlISxo4d63CY8PBwnD9/vkbbQUSM8SGy6dChA9LS0pCWloYBAwZg1apVaNmyJUaOHAmTyaQ6XmlpKSZNmoTExEQYjUZERUUhOjoaeXl5yM/Ptw03bdo05OXloVmzZmjTpg1efvll7N692622VnaGmjRpUqV29XuHDh1Cfn4+YmJiEB0dbfcqKirC2bNn7abZvHlzu/ENBgMaNWpUpQOWkJCAwMBAu/eaNWsGALbrP//88080bdrUrkMKAC1atLCbp5rDhw9DCIFXX321StsnT54MALb2V6pfv77d35WdNUfXezrj2LFjGDRoECIiIhAUFITo6GjcddddAGD3/bpCOLh2sdLV7Q8NDQUAJCYmVnnf3c+kxmKx4LHHHsP+/fvx5ZdfIiEhwa6em5uL33//3a4D+eijj+KOO+7A008/jdjYWDz22GNYtmxZtXcmi4uLcd9996GwsBDffPNNlWsjKwkhXMrjJCL38BpIIhU6nQ7dunXDnDlzcOjQIdVrxp577jksXLgQo0aNQmpqKkJDQ6EoCh577DG7H9EuXbrgyJEj+Oabb7BmzRp89NFHeOedd7BgwQI8/fTTNfY5rFYrYmJisGTJEof1yhtYrkeVy++ll15Cenq6w2Gu7jDr9XqHw8k6bWosFgu6d++OixcvYty4cUhOTkZgYCBOnjyJQYMGudVJioyMlHb81Nrv6H13PpPM0KFD8d1332HJkiW4++67q9R/+OEH+Pn5oVu3brb3/P398fPPP+Onn37CqlWrsHr1aixduhR333031qxZo/p5XGEymdCvXz/s3r0bP/74I1q3bq067KVLl9C0aVOP50lEcuxAEklUVFQAAIqKilSH+fLLL5GRkYFZs2bZ3isrK6ty5zIAREREYPDgwRg8eDCKiorQpUsXTJkyxeUOZOUNGIcPH65Su/q9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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "denorm = persistence.test_target_transforms[0]\n", + "in_graphic = cl.utils.visualize_at_index(\n", + " persistence,\n", + " dm,\n", + " in_transform=denorm,\n", + " out_transform=denorm,\n", + " variable=\"geopotential\",\n", + " src=\"era5\",\n", + " index=0\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 578 + }, + "id": "XCqRAgQFz-_P", + "outputId": "f6def5a6-a008-45c7-9f1f-9ce9a04114be" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "HTML(in_graphic.to_jshtml())" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "--Mjjc621iNc" + }, + "source": [ + "Moreover, ClimateLearn can display the mean bias, which is the average bias at each coordinate across the entire testing set." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 345 + }, + "id": "-zjwVjWZ2E5W", + "outputId": "20260d7f-9a55-4e9f-e0e5-0011ccccb8c7" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "43it [00:01, 29.08it/s]\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAngAAAE2CAYAAADyLe+7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABuUElEQVR4nO3deXxTZdo//s9J0iTdknRfoFA22dcitYqA0qFVR0URBfErIBYX0JG6Mj9lc2aKgogLA6OOoD4wID6OiuMwwyI4akEFEQFhAMGyNKULXdM02/37g6fR2NzXSQulJL3er1deSq5zn5ycLXfP8jmKEEKAMcYYY4yFDE1bTwBjjDHGGLuwuIPHGGOMMRZiuIPHGGOMMRZiuIPHGGOMMRZiuIPHGGOMMRZiuIPHGGOMMRZiuIPHGGOMMRZiuIPHGGOMMRZiuIPHGGOMMRZiuIPHWIiZN28eFEVp68lgQe7dd99FbGwsamtrm922vLwckZGR+OSTT1phyhhjgeAOHgtZq1atgqIoUBQFn3/+eZO6EAJpaWlQFAW//e1v22AKA5eenu79LoqiwGg0okePHnj88cdRUVHR1pPnl81mw7x587Bt27a2npRLzoEDBzBv3jwcP368rSfFL7fbjblz5+Khhx5CVFSU9/309HS/28o777wDrVaL3Nxc2O12xMXF4d5778UzzzxzMSebMfYL3MFjIc9oNGLNmjVN3t++fTtOnjwJg8HQBlPVfIMGDcI777yDd955B6+++iqys7OxdOlS5Obm+gz39NNPo76+vo2m8mc2mw3z58/nDp4fBw4cwPz58y/ZDt6GDRtw6NAhTJ8+XXXY1atXY8qUKcjOzsYHH3wAo9EIALj//vuxe/dubN26tbUnlzHmh66tJ4Cx1nb99ddj/fr1ePnll6HT/bzKr1mzBhkZGSgrK2vDqQtchw4dcNddd3n/fe+99yIqKgqLFy/G4cOH0aNHDwCATqfz+Z6s9dXV1SEyMrKtJ+OCTcfKlStx1VVXoUOHDuRwa9euxeTJk3Httdfiww8/9HbuAKB3797o168fVq1ahWuvvfa8p4kx1jx8BI+FvIkTJ6K8vBybNm3yvudwOPDee+/hzjvv9NvG4/Fg6dKl6Nu3L4xGI5KSknDffffh7NmzPsN9+OGHuOGGG5CamgqDwYBu3brh2Wefhdvt9hlu1KhR6NevHw4cOIBrrrkGERER6NChA55//vnz+m7JyckA4NOh83cN3sqVK3HttdciMTERBoMBffr0wfLly5uM75tvvkFOTg7i4+MRHh6OLl264J577vEZpri4GAcPHoTT6ZRO1/Hjx5GQkAAAmD9/vvfU8rx587zDHDx4ELfddhtiY2NhNBoxdOhQfPTRRz7jaTzN/vnnn+Phhx9GQkICLBYL7rvvPjgcDlRWVuLuu+9GTEwMYmJi8MQTT0AI4TMdiqJg8eLFePHFF9G5c2eEh4dj5MiR2LdvX5Ppbs40bd++HQ8++CASExPRsWNHAMBPP/2EBx98ED179kR4eDji4uIwfvx4nyN1q1atwvjx4wEA11xzjXfeNB7p/PV8apSeno4pU6YENB0A8M9//hNXX301IiMjER0djRtuuAH79++XLrNGdrsdGzduRHZ2Njncu+++i7vuugujRo3CRx995NO5a/Sb3/wGGzZs8FkmjLGLg//MZyEvPT0dWVlZ+Nvf/obrrrsOwLkfv6qqKkyYMAEvv/xykzb33XcfVq1ahalTp+Lhhx/GsWPH8Oqrr+Lbb7/FF198gbCwMADnfmSjoqKQn5+PqKgobN26FXPmzEF1dTUWLVrkM86zZ88iNzcXt956K26//Xa89957ePLJJ9G/f3/vdFGcTqf3aKPdbse3336LJUuWYMSIEejSpQvZdvny5ejbty9uuukm6HQ6bNiwAQ8++CA8Hg9mzJgBADhz5gzGjBmDhIQEPPXUU7BYLDh+/Djef/99n3HNnj0bb731Fo4dO4b09HS/n5eQkIDly5fjgQcewC233IJbb70VADBgwAAAwP79+71HiJ566ilERkbi3XffxdixY/G///u/uOWWW3zG99BDDyE5ORnz58/Hjh078Nprr8FiseDLL79Ep06d8Kc//QmffPIJFi1ahH79+uHuu+/2af/222+jpqYGM2bMgN1ux0svvYRrr70W33//PZKSklo0TQ8++CASEhIwZ84c1NXVAQC+/vprfPnll5gwYQI6duyI48ePY/ny5Rg1ahQOHDiAiIgIjBgxAg8//DBefvll/P73v0fv3r0BwPvf5vI3He+88w4mT56MnJwcPPfcc7DZbFi+fDmGDx+Ob7/9VrrcAGDXrl1wOBwYMmSIdJj//d//xaRJkzBixAhs2LAB4eHhfofLyMjAiy++iP3796Nfv34t+n6MsRYSjIWolStXCgDi66+/Fq+++qqIjo4WNptNCCHE+PHjxTXXXCOEEKJz587ihhtu8Lb7z3/+IwCI1atX+4xv48aNTd5vHN8v3XfffSIiIkLY7XbveyNHjhQAxNtvv+19r6GhQSQnJ4tx48apfpfOnTsLAE1eV111lSgrK/MZdu7cueLXm7a/6czJyRFdu3b1/vvvf/+7d35RJk+eLACIY8eOkcOVlpYKAGLu3LlNaqNHjxb9+/f3mUcej0dceeWVokePHt73GpdhTk6O8Hg83vezsrKEoiji/vvv977ncrlEx44dxciRI73vHTt2TAAQ4eHh4uTJk973d+7cKQCIWbNmtXiahg8fLlwul8/38jefCwsLmyz79evXCwDi008/bTK8bJ517txZTJ48WXU6ampqhMViEXl5eT7trVarMJvNTd7/tTfeeEMAEN9//73faUhNTRU6nU6MGjVK1NXVkeP68ssvBQCxbt06cjjG2IXHp2hZu3D77bejvr4eH3/8MWpqavDxxx9LT8+uX78eZrMZv/nNb1BWVuZ9ZWRkICoqCp9++ql32F8euaipqUFZWRmuvvpq2Gw2HDx40Ge8UVFRPtfQ6fV6DBs2DD/++GNA3yEzMxObNm3Cpk2b8PHHH+OPf/wj9u/fj5tuukn1popfTmdVVRXKysowcuRI/Pjjj6iqqgIAWCwWAMDHH39Mnn5dtWoVhBDkUSBKRUUFtm7dittvv907z8rKylBeXo6cnBwcPnwYp06d8mkzbdo0n9POmZmZEEJg2rRp3ve0Wi2GDh3qd36OHTvW53qyYcOGITMz0xvj0ZJpysvLg1ar9Xnvl/PZ6XSivLwc3bt3h8Viwe7du1swt9T9ejo2bdqEyspKTJw40Wf91Wq1yMzM9Fl//SkvLwcAxMTE+K1XVFTA5XKhY8eO0iN3jRrHESzXuTIWSvgULWsXEhISkJ2djTVr1sBms8HtduO2227zO+zhw4dRVVWFxMREv/UzZ854/3///v14+umnsXXrVlRXV/sM19hxatSxY8cm18bFxMRg7969AX2H+Ph4n+uibrjhBvTs2RO33XYb3njjDTz00EPStl988QXmzp2LwsJC2Gy2JtNpNpsxcuRIjBs3DvPnz8eLL76IUaNGYezYsbjzzjsv6J3GR44cgRACzzzzjDRG48yZMz4dsk6dOvnUzWYzACAtLa3J+7++ThKA9waUX7rsssvw7rvvtnia/J0Wr6+vR0FBAVauXIlTp075XHv26/XhQvn1dBw+fBgApDc2mEymgMYrJNfNjR49Gp06dcLy5csRGxuLl156SXUcnMvI2MXHHTzWbtx5553Iy8uD1WrFdddd5z1i9WsejweJiYlYvXq133rjzQOVlZUYOXIkTCYTFixYgG7dusFoNGL37t148skn4fF4fNr9+mhPI9kPaSBGjx4NAPjss8+kHbyjR49i9OjR6NWrF5YsWYK0tDTo9Xp88sknePHFF73TqSgK3nvvPezYsQMbNmzAv/71L9xzzz144YUXsGPHDp88tPPR+HmPPfYYcnJy/A7TvXt3n3/L5p2/91syP1syTf6OXj300ENYuXIlHnnkEWRlZcFsNkNRFEyYMKHJ+tBcv75xRzYdjZ/zzjvveG/C+SW1O6zj4uIAnLtm9Jc3bfzSq6++irNnz+Lll19GTEyM35tCGscBnPvjhDF2cXEHj7Ubt9xyC+677z7s2LED69atkw7XrVs3bN68GVdddRV5Cmrbtm0oLy/H+++/jxEjRnjfP3bs2AWdborL5QIA8mkDGzZsQENDAz766COfI2GyU3VXXHEFrrjiCvzxj3/EmjVrMGnSJKxduxb33ntvs6ZNdtSma9euAICwsDDVOzUvlMajWr/03//+13ua+UJN03vvvYfJkyfjhRde8L5nt9tRWVnpMxx1RCsmJqbJ8A6HA8XFxQFNQ7du3QAAiYmJLfouvXr1AnBuPe7fv7/fYTQaDd5++21UVVVh/vz5iI2NxcMPP9xkuMZtoaU3kDDGWo6vwWPtRlRUFJYvX4558+bhxhtvlA53++23w+1249lnn21Sc7lc3h/fxqNHvzxi5HA48Oc///nCTjhhw4YNAICBAwdKh/E3nVVVVVi5cqXPcGfPnm1y9GvQoEEAgIaGBu97gcSkAEBERAQANOmsJCYmYtSoUfjLX/7it9NSWlpKjrclPvjgA59r6L766ivs3LnTe/fyhZomrVbbZB6+8sorTY6+NWbV/XreAOc6aJ999pnPe6+99pr0CN6v5eTkwGQy4U9/+pPfZaT2XTIyMqDX6/HNN9+Qw4WFheG9997DVVddhUceeQTvvPNOk2F27doFs9mMvn37BjTtjLELh4/gsXZl8uTJqsOMHDkS9913HwoKCrBnzx6MGTMGYWFhOHz4MNavX4+XXnoJt912G6688krExMRg8uTJePjhh6EoCt55551Wy/w6deoU/ud//gfAuY7kd999h7/85S+Ij48nr78bM2YM9Ho9brzxRtx3332ora3F66+/jsTERJ/OzFtvvYU///nPuOWWW9CtWzfU1NTg9ddfh8lkwvXXX+8dLpCYFODcqcM+ffpg3bp1uOyyyxAbG4t+/fqhX79+WLZsGYYPH47+/fsjLy8PXbt2RUlJCQoLC3Hy5El899135z/DfqF79+4YPnw4HnjgATQ0NGDp0qWIi4vDE0884R3mQkzTb3/7W7zzzjswm83o06cPCgsLsXnzZu9pz0aDBg2CVqvFc889h6qqKhgMBm9O4b333ov7778f48aNw29+8xt89913+Ne//hXwaU6TyYTly5fj//2//4chQ4ZgwoQJSEhIQFFREf7xj3/gqquuwquvviptbzQaMWbMGGzevBkLFiwgPysiIgL/+Mc/MHLkSNxzzz0wm8246aabvPVNmzbhxhtv5GvwGGsLbXHrLmMXwy9jUii/jklp9Nprr4mMjAwRHh4uoqOjRf/+/cUTTzwhTp8+7R3miy++EFdccYUIDw8Xqamp4oknnhD/+te/mkRgjBw5UvTt27fJZ0yePFl07txZ9bv8OiZFo9GIxMREMXHiRHHkyBGfYf3FpHz00UdiwIABwmg0ivT0dPHcc8+JN9980yfuZPfu3WLixImiU6dOwmAwiMTERPHb3/5WfPPNN02mGQHEpAhxLiYjIyND6PX6JvEfR48eFXfffbdITk4WYWFhokOHDuK3v/2teO+997zDyJZh43csLS1tMm2RkZHefzfGpCxatEi88MILIi0tTRgMBnH11VeL7777rsn0ns80CSHE2bNnxdSpU0V8fLyIiooSOTk54uDBg00iToQQ4vXXXxddu3YVWq3WZ31xu93iySefFPHx8SIiIkLk5OSII0eOSGNSZOv3p59+KnJycoTZbBZGo1F069ZNTJkypcny9Of9998XiqKIoqIin/dl24rVahXdu3cXRqPR+z1++OEHAUBs3rxZ9fMYYxeeIgRHjDPGQtPx48fRpUsXLFq0CI899lhbT07QcLvd6NOnD26//Xa/lyoE4pFHHsFnn32GXbt28RE8xtoAX4PHGGPMh1arxYIFC7Bs2TLyBh6Z8vJy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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cl.utils.visualize_mean_bias(\n", + " dm,\n", + " persistence,\n", + " out_transform=denorm,\n", + " variable=\"temperature\",\n", + " src=\"era5\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "QVNKY63S2h7j" + }, + "source": [ + "Next, we can train a deep learning model to do weather forecasting. In the following code cell, we load the ResNet-based model architecture defined by [Rasp & Theurey, 2020](https://arxiv.org/abs/2008.08626)." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "GekzeZn8kzIS", + "outputId": "1747e109-6a73-4ee5-97a9-6529a9d64423" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading architecture: rasp-theurey-2020\n", + "Using optimizer associated with architecture\n", + "Using learning rate scheduler associated with architecture\n", + "Loading training loss: lat_mse\n", + "No train transform\n", + "Loading validation loss: lat_rmse\n", + "Loading validation loss: lat_acc\n", + "Loading validation loss: lat_mse\n", + "Loading validation transform: denormalize\n", + "Loading validation transform: denormalize\n", + "No validation transform\n", + "Loading test loss: lat_rmse\n", + "Loading test loss: lat_acc\n", + "Loading test transform: denormalize\n", + "Loading test transform: denormalize\n" + ] + } + ], + "source": [ + "resnet = cl.load_forecasting_module(\n", + " data_module=dm,\n", + " architecture=\"rasp-theurey-2020\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "Sr5aluX53QMC" + }, + "source": [ + "To train the model, we leverage PyTorch Lightning for GPU acceleration, specification of floating-point precision (we use 16-bit here to speed up training), and callbacks such as early stopping, model checkpointing, and logging to TensorBoard. Recall from previous text cells why we use `lat_mse:aggregate` as the early stopping criterion. For sake of example, we train for just one epoch, but to obtain good results, one should train for much longer." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "g6WrKJw-k_t-", + "outputId": "2d0f1933-7cd4-43fa-aada-e91db20187ea" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:lightning_fabric.utilities.seed:Global seed set to 0\n", + "/usr/local/lib/python3.10/dist-packages/lightning_fabric/connector.py:555: UserWarning: 16 is supported for historical reasons but its usage is discouraged. Please set your precision to 16-mixed instead!\n", + " rank_zero_warn(\n", + "INFO:pytorch_lightning.utilities.rank_zero:Using 16bit Automatic Mixed Precision (AMP)\n", + "INFO:pytorch_lightning.utilities.rank_zero:Trainer already configured with model summary callbacks: []. Skipping setting a default `ModelSummary` callback.\n", + "INFO:pytorch_lightning.utilities.rank_zero:GPU available: True (cuda), used: True\n", + "INFO:pytorch_lightning.utilities.rank_zero:TPU available: False, using: 0 TPU cores\n", + "INFO:pytorch_lightning.utilities.rank_zero:IPU available: False, using: 0 IPUs\n", + "INFO:pytorch_lightning.utilities.rank_zero:HPU available: False, using: 0 HPUs\n" + ] + } + ], + "source": [ + "pl.seed_everything(0)\n", + "default_root_dir = f\"resnet_forecasting_24hrs\"\n", + "logger = TensorBoardLogger(save_dir=f\"{default_root_dir}/logs\")\n", + "early_stopping = \"val/lat_mse:aggregate\"\n", + "callbacks = [\n", + " RichProgressBar(),\n", + " RichModelSummary(max_depth=1),\n", + " EarlyStopping(\n", + " monitor=early_stopping,\n", + " patience=5\n", + " ),\n", + " ModelCheckpoint(\n", + " dirpath=f\"{default_root_dir}/checkpoints\",\n", + " monitor=early_stopping,\n", + " filename=\"epoch_{epoch:03d}\",\n", + " auto_insert_metric_name=False,\n", + " )\n", + "]\n", + "trainer = pl.Trainer(\n", + " logger=logger,\n", + " callbacks=callbacks,\n", + " default_root_dir=default_root_dir,\n", + " accelerator=\"gpu\" if torch.cuda.is_available() else None,\n", + " devices=[0] if torch.cuda.is_available() else None,\n", + " max_epochs=1,\n", + " precision=\"16\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 248, + "referenced_widgets": [ + "3cd97411741d4ec792774956eea5f57f", + "9a741cc53a82407e8c9c394629fe6759" + ] + }, + "id": "3aj3xyCusC99", + "outputId": "aafa5f9c-24ed-42d1-ed54-a1271fdcfa95" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:pytorch_lightning.loggers.tensorboard:Missing logger folder: resnet_forecasting_24hrs/logs/lightning_logs\n", + "INFO:pytorch_lightning.accelerators.cuda:LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "data": { + "text/html": [ + "
┏━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━┓\n",
+              "┃    Name  Type    Params ┃\n",
+              "┡━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━┩\n",
+              "│ 0 │ net  │ ResNet │  5.7 M │\n",
+              "└───┴──────┴────────┴────────┘\n",
+              "
\n" + ], + "text/plain": [ + "┏━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━┓\n", + "┃\u001b[1;35m \u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m \u001b[0m┃\u001b[1;35m \u001b[0m\u001b[1;35mName\u001b[0m\u001b[1;35m \u001b[0m┃\u001b[1;35m \u001b[0m\u001b[1;35mType \u001b[0m\u001b[1;35m \u001b[0m┃\u001b[1;35m \u001b[0m\u001b[1;35mParams\u001b[0m\u001b[1;35m \u001b[0m┃\n", + "┡━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━┩\n", + "│\u001b[2m \u001b[0m\u001b[2m0\u001b[0m\u001b[2m \u001b[0m│ net │ ResNet │ 5.7 M │\n", + "└───┴──────┴────────┴────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Trainable params: 5.7 M                                                                                            \n",
+              "Non-trainable params: 0                                                                                            \n",
+              "Total params: 5.7 M                                                                                                \n",
+              "Total estimated model params size (MB): 22                                                                         \n",
+              "
\n" + ], + "text/plain": [ + "\u001b[1mTrainable params\u001b[0m: 5.7 M \n", + "\u001b[1mNon-trainable params\u001b[0m: 0 \n", + "\u001b[1mTotal params\u001b[0m: 5.7 M \n", + "\u001b[1mTotal estimated model params size (MB)\u001b[0m: 22 \n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3cd97411741d4ec792774956eea5f57f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:pytorch_lightning.utilities.rank_zero:`Trainer.fit` stopped: `max_epochs=1` reached.\n" + ] + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "trainer.fit(resnet, datamodule=dm)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "Yd_dAIiY5DEU" + }, + "source": [ + "After fitting the model, we can test it using the best checkpoint saved during training." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 358, + "referenced_widgets": [ + "7dc2b997e4654ecdb777802d6f34ae8b", + "d49a429e44954fe3bb15c588e93e3214" + ] + }, + "id": "Hq8kQY0itptH", + "outputId": "60d33175-87fa-4c37-af23-b52642adf1ac" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:pytorch_lightning.utilities.rank_zero:Restoring states from the checkpoint path at /content/resnet_forecasting_24hrs/checkpoints/epoch_000.ckpt\n", + "INFO:pytorch_lightning.accelerators.cuda:LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "INFO:pytorch_lightning.utilities.rank_zero:Loaded model weights from the checkpoint at /content/resnet_forecasting_24hrs/checkpoints/epoch_000.ckpt\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7dc2b997e4654ecdb777802d6f34ae8b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
+              "┃        Test metric                 DataLoader 0        ┃\n",
+              "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
+              "│   test/lat_acc:aggregate        0.9151675165488576     │\n",
+              "│ test/lat_acc:geopotential       0.9311339698777382     │\n",
+              "│  test/lat_acc:temperature       0.8992010632199775     │\n",
+              "│  test/lat_rmse:aggregate        190.11091135048534     │\n",
+              "│ test/lat_rmse:geopotential      378.04462978028846     │\n",
+              "│ test/lat_rmse:temperature       2.1771929206822804     │\n",
+              "└────────────────────────────┴────────────────────────────┘\n",
+              "
\n" + ], + "text/plain": [ + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1m Test metric \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m DataLoader 0 \u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", + "│\u001b[36m \u001b[0m\u001b[36m test/lat_acc:aggregate \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.9151675165488576 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mtest/lat_acc:geopotential \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.9311339698777382 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36m test/lat_acc:temperature \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.8992010632199775 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36m test/lat_rmse:aggregate \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 190.11091135048534 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mtest/lat_rmse:geopotential\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 378.04462978028846 \u001b[0m\u001b[35m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mtest/lat_rmse:temperature \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 2.1771929206822804 \u001b[0m\u001b[35m \u001b[0m│\n", + "└────────────────────────────┴────────────────────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[{'test/lat_rmse:temperature': 2.1771929206822804,\n", + " 'test/lat_rmse:geopotential': 378.04462978028846,\n", + " 'test/lat_rmse:aggregate': 190.11091135048534,\n", + " 'test/lat_acc:temperature': 0.8992010632199775,\n", + " 'test/lat_acc:geopotential': 0.9311339698777382,\n", + " 'test/lat_acc:aggregate': 0.9151675165488576}]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "trainer.test(resnet, datamodule=dm, ckpt_path=\"best\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "xGPeLgNi5cLr" + }, + "source": [ + "As before, let's visualize the bias of our deep learning model on the first sample of the testing set (the mean bias computation will take a while, even on GPU)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "lJTcCfxT7tsh", + "outputId": "18134f9f-9f4c-42c4-ad6c-e7cd916f852e" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "0it [00:00, ?it/s]\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAo0AAAFJCAYAAAD+PBdyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB5OklEQVR4nO3deXhTVeI+8DdJm3TfaEuplLLJvgmOnSIgIlKWQRFXRC1YQByKSl2AEaGIIwiCqCDoKIsO/AQcRUUGLSCgUlZBFgUtgqwFLHRfkibn9wffZgjNPSdpaEvJ+3me+2hzzr059+Tm5nCX9+qEEAJERERERBL62m4AEREREV37OGgkIiIiIiUOGomIiIhIiYNGIiIiIlLioJGIiIiIlDhoJCIiIiIlDhqJiIiISImDRiIiIiJS4qCRiIiIiJQ4aCSqBjqdDunp6bXdDE2NGzfG3/72t9puBrmgqtvSpk2boNPpsGnTJpfqz5w5E61atYLNZnP7vaiyhQsXolGjRigrK6vtphBdNRw0Uq05evQoUlNT0aJFCwQEBCAgIABt2rTBmDFjsG/fvtpuXrU7ffo00tPTsXfv3mpZ/s8//4z09HQcO3asWpZ/vXvnnXewZMmSGnmvtWvX1uo/MvLz8/Haa69h/Pjx0Our52dBCIFHH30UOp0OCQkJKC4udlqvuLgY8+fPR58+fdCgQQMEBwfjpptuwoIFC2C1WjWX36VLF/z973+vUtvMZjN69eoFnU6Hu+++W/N9cnJyMGvWLPTo0QNRUVEICwvDX//6V6xYsaJS3WHDhsFsNuPdd9+tUpuIrkmCqBZ8+eWXIiAgQISEhIgnn3xSLFy4ULz33nsiLS1NNG7cWOh0OnHs2LHabmaVARBTpkyR1tm5c6cAIBYvXlwtbVi1apUAIL799ttKZfHx8WLAgAHV8r7Xi7Zt24rbbrutRt5rzJgxQmt3XFJSIiwWi9vL/PbbbzU//yu98cYbIiQkRJSUlLj9Pq6aMGGCACD69+8v9Hq9GDhwoCgvL69Ub//+/UKn04nevXuLmTNnioULF4p77rlHABCPPfaY02WfPn1a6HQ6sWbNGrfbZbPZxMMPPywAiAEDBggA4u9//7vTul9++aXw9fUVd999t5g7d66YN2+euP322wUAMXny5Er1X3jhBREfHy9sNpvb7SK6FnHQSDUuKytLBAYGitatW4vTp09XKrdYLOLNN98Ux48fly6nsLCwuproseoYNBYVFbnVBg4aPXOtDBqryp1BY4cOHcQjjzxyVd//cgsWLBAAxPjx44UQQixdulTo9XoxevToSnXPnz8vDhw4UOn14cOHCwDit99+q1T2wQcfCH9/f1FcXOx228aPHy90Op2YP3++EEKIadOmCQBixowZler+/vvvlf4xa7PZRK9evYTJZKq0T9q1a5cAIDZs2OB2u4iuRRw0Uo0bNWqUACC2bdvm8jzJyckiMDBQZGVliX79+omgoCBx9913CyEuDR7T0tJEw4YNhdFoFC1atBCzZs1y+Nf90aNHNQdoVw7wpkyZYv9xSk5OFqGhoSIkJEQMGzas0sCttLRUPPPMMyIyMlIEBQWJgQMHihMnTigHjRU/6FdOFe277bbbRNu2bcWuXbtE9+7dhb+/v3j66aedtrdCfHy8SE5OFkIIsXjxYqfLrxhAVAwav/vuO/GXv/xFmEwm0aRJE7F06dJKy83KyhJZWVma63K5n376SfTo0UP4+fmJG264QUybNk0sWrRIABBHjx51qLt27VrRrVs3ERAQIIKCgkT//v2dDhY2bNhgrxcaGiruuusu8fPPPzvUqfjMfvnlF3H//feL4OBgERERIZ566qlKR88sFot4+eWXRdOmTYXRaBTx8fFi4sSJorS01KEvr+y7yweQFy9eFE8//bR9m2vWrJmYMWOGsFqt9joV29ysWbPEu+++a3+/m2++WezYscNeLzk52elnVeHKz/vYsWPiySefFC1atBB+fn4iIiJC3HfffZX619VB4++//y4AiCVLlji8fnn7582bJ5o0aSL8/f3FnXfeKY4fPy5sNpt4+eWXxQ033CD8/PzEXXfdJXJyciot/4svvhAGg0FMnDjR4fUPP/xQ6PV68eqrr0rbd/lyAIgvvviiUtngwYNF//797X//+uuvYvDgwaJ+/frCZDKJG264QTz44IMiNzfXYb758+cLnU4nFixY4PD6K6+8InQ6nVi+fLlLbXvrrbcEALFv375KZRXbIdH1wOeqnusmcsGaNWvQvHlzJCQkuDVfeXk5kpKS0K1bN7z++usICAiAEAJ33XUXvv32W6SkpKBTp074+uuv8fzzz+PUqVN44403qtzOBx54AE2aNMH06dPx448/4v3330d0dDRee+01e50RI0bg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O+wDRiG8c1sRQt4v3rqtXKt0QghRc8c1iYiIiKgu4jWNRERERKTEQSMRERERKXHQSERERERKHDQSERERkRIHjURERESkxEEjERERESlx0EhEREREShw0EhEREZESB41E5BWOHTsGnU5nf27t1abT6bB69epqWTYR0bWAg0YiqhHDhg3DoEGDau394+LicObMGbRr1w4AsGnTJuh0OuTm5tZam4iI6hKf2m4AEVFNMBgMiImJqe1mEBHVWTzSSES1bvPmzbjllltgMpnQoEEDTJgwAeXl5fbynj174qmnnsILL7yAiIgIxMTEID093WEZhw4dQrdu3eDn54c2bdpg/fr1DqeMLz89fezYMdx+++0AgPDwcOh0OgwbNgwA0LhxY8ydO9dh2Z06dXJ4v99++w09evSwv1dGRkaldTpx4gQeeOABhIWFISIiAnfffTeOHTvmaVcREdUaDhqJqFadOnUK/fv3x1/+8hf89NNPWLBgAT744AO88sorDvWWLl2KwMBAbN++HTNnzsTLL79sH6xZrVYMGjQIAQEB2L59O9577z28+OKLmu8ZFxeH//znPwCAw4cP48yZM3jzzTddaq/NZsPgwYNhNBqxfft2LFy4EOPHj3eoY7FYkJSUhODgYHz33Xf44YcfEBQUhL59+8JsNrvTPURE1wyeniaiWvXOO+8gLi4O8+bNg06nQ6tWrXD69GmMHz8ekydPhl5/6d+2HTp0wJQpUwAAN954I+bNm4cNGzbgzjvvREZGBo4cOYJNmzbZT0H/85//xJ133un0PQ0GAyIiIgAA0dHRCAsLc7m969evx6FDh/D1118jNjYWAPDqq6+iX79+9jorVqyAzWbD+++/D51OBwBYvHgxwsLCsGnTJvTp08e9TiIiugZw0EhEteqXX35BYmKifXAFALfeeisKCwtx8uRJNGrUCMClQePlGjRogHPnzgG4dLQwLi7O4ZrFW265pdraGxcXZx8wAkBiYqJDnZ9++glZWVkIDg52eL20tBRHjhyplnYREVU3DhqJqE7w9fV1+Fun08Fms13199Hr9RBCOLxmsVjcWkZhYSG6dOmCZcuWVSqLioryqH1ERLWFg0YiqlWtW7fGf/7zHwgh7Ecbf/jhBwQHB6Nhw4YuLaNly5Y4ceIEzp49i/r16wMAdu7cKZ3HaDQCuHQ95OWioqJw5swZ+9/5+fk4evSoQ3tPnDiBM2fOoEGDBgCAbdu2OSyjc+fOWLFiBaKjoxESEuLSOhARXet4IwwR1Zi8vDzs3bvXYRo1ahROnDiBsWPH4tChQ/j8888xZcoUpKWl2a9nVLnzzjvRrFkzJCcnY9++ffjhhx8wadIkAHA47X25+Ph46HQ6rFmzBufPn0dhYSEAoFevXvjoo4/w3XffYf/+/UhOTobBYLDP17t3b7Ro0QLJycn46aef8N1331W66Wbo0KGIjIzE3Xffje+++w5Hjx7Fpk2b8NRTT+HkyZNV6ToiolrHQSMR1ZhNmzbhpptucpimTZuGtWvXYseOHejYsSNGjx6NlJQU+6DPFQaDAatXr0ZhYSH+8pe/YMSIEfaBnJ+fn9N5brjhBkydOhUTJkxA/fr1kZqaCgCYOHEibrvtNvztb3/DgAEDMGjQIDRr1sw+n16vx2effYaSkhLccsstGDFiBP75z386LDsgIABbtmxBo0aNMHjwYLRu3RopKSkoLS3lkUciqrN04sqLd4iIrgM//PADunXrhqysLIdBHxERVQ0HjUR0Xfjss88QFBSEG2+8EVlZWXj66acRHh6O77//vrabRkR0XeCNMER0XSgoKMD48eNx/PhxREZGonfv3pg9e3ZtN4uI6LrBI41EREREpMQbYYiIiIhIiYNGIiIiIlLioJGIiIiIlDhoJCIiIiIlDhqJiIiISImDRiIiIiJS4qCRiIiIiJQ4aCQiIiIipf8PtKEC1HELSjkAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAApAAAAFJCAYAAAAsbB0qAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB3X0lEQVR4nO3deXhTZfo+8DtJs3VJ9xVKKWvZ0apYRAStVMVR3MZ1BERQf6AjuMGMso0zOK64gn5V0BmcQdxHHLSggCKgIGW1CMgOLVv3NkuT9/cH00hszpMmUkrJ/bmuXEqe8yZvTs45eXuW++iUUgpERERERE2kb+kOEBEREVHrwgEkEREREQWFA0giIiIiCgoHkEREREQUFA4giYiIiCgoHEASERERUVA4gCQiIiKioHAASURERERB4QCSiIiIiILCASRRkHQ6HaZOndrS3aBmsHTpUuh0OixdujTotlOnToVOp2vy9FdccQVGjx4d9PuQfzfddBN+//vft3Q3iMIGB5AU9ubOnQudTufzSElJweDBg/Hf//63pbsXFmprazF16tSQBm6heOWVVzB37txT8l7+rFixAl988QUeeeSRZnuP8vJy9OzZEzqdDuPGjdOcbu/evZg2bRrOO+88xMfHIykpCYMGDcLixYs125SVlSEiIgLvvvtuSH3bs2cPMjIyoNPp8PTTT2tOV1xcjIcffhh9+/ZFTEwM0tPTMXToUKxZs6bRtI888gjef/99rF+/PqQ+EVGQFFGYmzNnjgKgpk+frv7xj3+ot99+Wz311FOqR48eCoD6z3/+4zN9XV2dcrlcLdTbM9Phw4cVADVlypRT8n49evRQF110UaPn3W63qqurU263O+jXnDJlimrqJvXqq69WQ4YMCfo9msrhcKhBgwYpo9GoCgoKFAD197//3e+0L774orJarermm29WL730kpo5c6Y6++yzFQD15ptv+m3zr3/9S0VERKiysrKg+3bs2DHVvXt3ZbPZ1MCBA5VOp1P//ve//U77wAMPqLi4ODVq1Cj16quvqieffFJ17NhRGQwGVVhY2Gj68847T/3hD38Iuk9EFDwOICnsNQwgv//+e5/njx07poxGo7rllltaqGfh43QZQP4WTR1AlpaWqoiICPX666+f1Pdv4PF41M0336xMJpP66KOPlMfjUaNHj1Y6nU7961//ajT9pk2b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dTUGDhyIqVOnBj2AbLgAY/v27Y1qv36uY8eOWLx4MS644AKfw7Far7l161Z06NDB+7zT6cTOnTsbXTV84MAB1NTU+OyF/OmnnwDAexV4VlYWNmzYAI/H47MXsri42Oc9tfYYNfTDaDQ2ev/foql7qDZu3IiffvoJb731ls/FKr/lKt+cnBy8//77IbdvLg899BDmzJmDmTNn+pxycKKFCxdi8ODBjZYjvV6PSy65BJdccgmeffZZ/O1vf8Of//xnfPXVV7/5e/N4PLj99tuxZMkSvPvuu969v/7U19dj7969uOqqq37TexJRYDyETaTB5XLhiy++gMlk8h5y9cdgMDTaE/Tiiy/C7Xb7PHf06FGff0dHR6NTp05wOBw+79mUGJ+MjAz07NkTb7/9ts/gdtmyZdi4caPPtL///e/hdrvxl7/8pdHr1NfXewe6+fn5MJlMeOGFF3w+zxtvvIGKigqfw5YNbV999VXvv51OJ1599VUkJycjNzcXwPHb9ZWUlHivgG5o9+KLLyI6Oto7GGi40vfXg+6UlBQMGjQIr776qt95cmI8TzCioqL8DvB/rWHv2YnzQymF559/PqT3BYC8vDyUlZU1y/mLoXrqqafw9NNP409/+hP++Mc/+p3G5XKhsLCw0XJw7NixRtP27dsXAHyW7VDde++9mD9/Pl555RXvFfpatmzZArvdjv79+//m9yUiGfdAEv3Pf//7X++esUOHDuGdd97Btm3bMHHiRNhsNs12V155Jf7xj38gNjYW3bt3x8qVK7F48WIkJib6TNe9e3cMGjQIubm5SEhIwJo1a/Dee+/53GYumBifv/3tb7j66qtxwQUXYOTIkSgrK8NLL72Enj17+gwqL7roItx1112YMWMGioqKMGTIEBiNRmzbtg0LFizA888/j+uvvx7JycmYNGkSpk2bhssuuwxXXXUVtm7dildeeQXnnntuozijjIwM/P3vf8euXbvQpUsXzJ8/H0VFRXjttde8cT9jxozBq6++ihEjRmDt2rVo37493nvvPaxYsQIzZ85ETEwMgOOHQbt374758+ejS5cuSEhIQM+ePdGzZ0+8/PLLGDBgAHr16oXRo0ejQ4cOKC0txcqVK7Fv376Qbl2Xm5uLWbNm4fHHH0enTp2QkpLi95BtTk4OOnbsiAcffBD79++HzWbD+++//5vOPRw6dCgiIiKwePFijBkzJuTXOVk+/PBDPPzww+jcuTO6deuGf/7znz71Sy+9FKmpqfjmm29QWVnZaAA5ffp0LF++HEOHDkVWVhYOHTqEV155BW3btsWAAQN+U99mzpyJV155BXl5eYiMjGzUt2uuucZnD3hhYSEiIyO9sVhE1Ixa5uJvotOHvxgfi8Wi+vbtq2bNmuUTqaJU4xifsrIyNXLkSJWUlKSio6NVQUGBKi4uVllZWT5xPI8//rg677zzVFxcnLJarSonJ0f99a9/9UbeKBVcjI9SSv373/9WOTk5ymw2q549e6pPPvlEXXfddSonJ6fRtK+99prKzc1VVqtVxcTEqF69eqmHH35YHThwwGe6l156SeXk5Cij0ahSU1PVPffc0+iWdRdddJHq0aOHWrNmjcrLy1MWi0VlZWWpl156qdH7lpaWeuePyWRSvXr1UnPmzGk03bfffqtyc3OVyWRqNI937Nihbr/9dpWWlqaMRqNq06aNuvLKK9V7773nnUbrjkL+onlKSkrU0KFDVUxMjALgjcPxN+2WLVtUfn6+io6OVklJSWr06NFq/fr1CoDP5wjmVoZXXXWVuuSSS3ye0+p/w+v+OiZn+PDhKioqqknvJ2l4fa1Hw7x48MEHVffu3Ru1X7Jkibr66qtVRkaGMplMKiMjQ918883qp59++s19Gz58uNi3nTt3+kzfr18/ddttt/3m9yWiwHRKneSzsImoRfXt2xfJycnNejeOQYMG4ciRI9i0aVOzvceZ7Ouvv8agQYNQXFzcai746N69O6688ko8+eSTLd0Vv4qKinD22Wfjhx9+8B5CJ6Lmw3MgiVopl8vlvcinwdKlS7F+/Xrvrfno9HThhRdiyJAhp+1g7NecTiduvPFGjBw5sqW7oumJJ57A9ddfz8Ej0SnCPZBErdSuXbuQn5+P2267DRkZGSguLsbs2bMRGxuLTZs2NToH82TiHkgiovDGi2iIWqn4+Hjk5ubi9ddfx+HDhxEVFYWhQ4fiiSeeaNbBIxEREfdAEhEREVFQeA4kEREREQWFA0giIiIiCgrPgTyBx+PBgQMHEBMT0+RbnREREVHLUkqhqqoKGRkZPrdNDcRut8PpdAb1XiaTCRaLJdgunnE4gDzBgQMHkJmZ2dLdICIiohDs3bsXbdu2bdK0drsdGdZolMEdeOITpKWlYefOnWE/iOQA8gQNt1UrnjEWMRaz32n0Gs976zFxmjXXgf1y2wCvrTPKX5fOGqVdizCKbT1V5WK9bvc+se6qsWvWbD3koOSK9cViPaZTllhX9S7NWkRH+b23vfJvsd7lzmHye0fFiXXPrm2aNV0n7ftrA4C+svE9hn1eu0K+nZ67U2+xLjEePSDWlUFennCsVLNU36Gn2NRgrxLr+gDLqjs+RbPmMcob/NqYNLHujLCKdVN9Xcjt48p2i213xZ8l1jtvXiDWazZv0axF9ZGXlfoS+d7s9ZXVcr1Kux7Vq4fYtm7bdrFuP1wu1k2xkWLdkpKk/dqHjohtzYnxYj3Q9kfiTJJ3Zph2/yjWdRZ5WfVExWjW7HFtxLbmmqNiPRD9PuE+8MnpYltdnf9lqarWjk4j/+T9HW8Kp9OJMrjxlqUDIpt4Rl8tPBhe8jOcTicHkC3dgdNJw2HrGIsZNqvWAFJeYPSR2nWXxmv+8tqBBpDyj7ZOeO+AA8h6+XNFmE1i3VWv/ReczSq/tscs903ru2igXNorfkSkvBGNjpBXAVuA9ioqwEZa6LsuSv5h0wcYjHicct0d4PUlxroAnzvA8oQ67e+8PkC/DAZ5b4De7RDr0ucONIA0REeL9cADSEPI7W0ueb5ER8s/jDZh/QcAvbAORwdYzusDrIP1AQ4BupzSe8v9NlrkbY8xwPbDFGDbZRU+mzFAW0uAbXag7Y/EGa29QwAATAHmW+ABpPbyZgrw3mbI255ApN9JBNg+6HTy9iGU08+iIgyI0snrrvf1VXB7K89kHEASERFR2DJYDDDomzaANHgAyDvcwwYHkERERBS2dEYddPqm7bnUeXiBbQMOIImIiChs6SN00DdxAKnnANKLA0giIiIKW9wDGRoOIP34vMdjiIyy+a21i5dPfjDp6zVrrhz5HItoY61YT6zXvrIVAEr02tEFHiVfYZYG+Srr+KxNYt1j0j7xud4on2gem50j1u1xGWL9aKT2587cuVRsm/Hs02K9esFrYj2qZ3ex7jyifSW1JbNGbAvlEcu6SPlEd4NTWJ488ongNVm9xHrkhuVi3ZVzjvZrR6eKbZ0G+eKA7QnyVfldLTs0a5X6BLFtVb18Ar8xwAn0KQb5auX4o9pXFB9Mlq+EjoK8vPyQM0qs70nVXl4uStwgtkUXuRy/ZalYN0sX2cQnyy8+NFcsJxzS/r4BQFnk9cQTob19MkTKV1nvjZavsnZ65At8aoWLFy0R8oVJZV0uFOtxZvlCl00HtT9bxV6xKbq3ky9kW7xavkNyh/ban/vnHdqJHgDQJsN/2zpdJYAJYlsteoMOekMT90C6OYBswAEkERERhS2dQQddEweQOnAA2YADSCIiIgpbQe2B5ADSiwNIIiIiCls6fRDnQCoOIBtwAElERERhS2fQQ2do2p1odJDP7wwnHEASERFR2OIh7NA0bch9Gmjfvj10Ol2jx9ixYwEAgwYNalS7++67W7jXREREdDrT6XTew9gBHyHcKvFM1Wr2QH7//fdwu3+J0Ni0aRMuvfRS3HDDDd7nRo8ejenTp3v/HRkZ2r2Ad+zxwBLpP0KlvFq+H237FO3ohXiLHNMTKGrHHiHHUWw+kKhZ65hcJbY1QDt+CAAOZOaJdY/wt0hq+Vax7fa0i8S6W8nxRzZUataK2w0V22bVbhbrh2+bLNYjqraJdcNe7TwMT4wcEaJik8T6vuSzxbrdox2VkbNpntjWGK29LAHAx+0fEeuuGu2N7Prv5PXgovPk2KfkKDnOpqiqq2Yt0iQv5xF6OTqpvUf+vjfWy/FH1QbtyKqqffJyHh8t933jDnn70S5d+zv5oqSP2LZjihxfltNFjozRCbFRkUd2i22NDnnb5UpoI9Y9AW5RZ7dqr4f79dnyaweIdNEHONzZ2aO9/Tmg6yC27WXYKNbLDGliXa/T/twqwFHaZevkZW3/7iNi/etPtNejlCw5tq30gP+IPaddXk4kOgOavAdSxyPYXq1mD2RycjLS0tK8j08//RQdO3bERRf9MviIjIz0mcZm87+gEREREQG/xPg09RGMGTNm4Nxzz0VMTAxSUlIwbNgwbN3qu1PFbrdj7NixSExMRHR0NK677jqUlvrmPu/ZswdDhw5FZGQkUlJS8NBDD6G+3vePyqVLl+Lss8+G2WxGp06dMHfu3JDmR1O1mgHkiZxOJ/75z3/ijjvu8NmdPG/ePCQlJaFnz56YNGkSamvlPR0OhwOVlZU+DyIiIgofOr0+qEcwli1bhrFjx2LVqlUoLCyEy+XCkCFDUFPzy5GU8ePH4z//+Q8WLFiAZcuW4cCBA7j22mu9dbfbjaFDh8LpdOLbb7/FW2+9hblz52Ly5F+OkO3cuRNDhw7F4MGDUVRUhPvvvx933nknPv/8898+gzS0mkPYJ/roo49QXl6OESNGeJ+75ZZbkJWVhYyMDGzYsAGPPPIItm7dig8++EDzdWbMmIFp06adgh4TERHR6SioGJ//TffrHU5msxlmc+PTbxYtWuTz77lz5yIlJQVr167FwIEDUVFRgTfeeAPvvPMOLr74YgDAnDlz0K1bN6xatQrnn38+vvjiC2zZsgWLFy9Gamoq+vbti7/85S945JFHMHXqVJhMJsyePRvZ2dl45plnAADdunXDN998g+eeew4FBQVBz5OmaJV7IN944w1cfvnlyMj45VyJMWPGoKCgAL169cKtt96Kt99+Gx9++CF27NC+zdWkSZNQUVHhfewVzlcjIiKiM0/DVdhNfQBAZmYmYmNjvY8ZM2Y06b0qKioAAAkJx2+runbtWrhcLuTn53unycnJQbt27bBy5UoAwMqVK9GrVy+kpv5yG9iCggJUVlZi8+bN3mlOfI2GaRpeozm0uj2Qu3fvxuLFi8U9iwDQr18/AMD27dvRsWNHv9No/cVARERE4SGUPZB79+71uc6iKWMJj8eD+++/HxdccAF69uwJACgpKYHJZEJcXJzPtKmpqSgpKfFOc+LgsaHeUJOmqaysRF1dHaxWa5M+XzBa3QByzpw5SElJwdCh8tW1RUVFAID09PRT0CsiIiJqjXS6pp/bqNMdn85mswV9oe7YsWOxadMmfPPNN0H38XTUqgaQHo8Hc+bMwfDhwxER8UvXd+zYgXfeeQdXXHEFEhMTsWHDBowfPx4DBw5E7969W7DHREREdDoLZQ9ksMaNG4dPP/0Uy5cvR9u2bb3Pp6Wlwel0ory83GcvZGlpKdLS0rzTfPfddz6v13CV9onT/PrK7dLSUthstmbZ+wi0sgHk4sWLsWfPHtxxxx0+z5tMJixevBgzZ85ETU0NMjMzcd111+HRRx8N6X1uPnsHYqKj/dYSDsuZhg5DsmbNpeQvMfqIfA5mZUJ7sd4zTTt7q13dj2LbY9GZYn1PrZwp1i6yRLN2OK6z2DYacsZcpEu+On51RU/NWqxVO5cTAMojzhXrqSgX6zWR2t83AOy5TPsirVij/LlTa38W61l7lot118YfNGuOAfJJ1e9XXCrWk23auX4A4BCiLi7tL292Lqx8X6zXWtqJ9Uibdn5e21p5/TXWystaebycC5hpOiTWK83aObLuaHkPyMLv5O1HQrz8w7Z9j3aOZJ8uctsKu3amKABUxsrrQfraD7WLTnkdjTCZxLqy28W6s1d/sW5yaueKpln2iW3LhO09AByqixPr5ZHa29WOi2eKbSPayOtBTKScWVyeoX0ULyJNzkM9UCXveSs/5v/3s8HVIwZo1hwOOWjxoj4Ov8/XVHvwzhNiU01B3YnGE9wAUimFe++9Fx9++CGWLl2K7GzfbUhubi6MRiOWLFmC6667DgCwdetW7NmzB3l5x/OX8/Ly8Ne//hWHDh1CSkoKAKCwsBA2mw3du3f3TvPZZ5/5vHZhYaH3NZpDqxpADhkyBMpPwmlmZiaWLVvWAj0iIiKi1kwfYYA+Qg6c904bKGX9V8aOHYt33nkHH3/8MWJiYrznLMbGxsJqtSI2NhajRo3ChAkTkJCQAJvNhnvvvRd5eXk4//zzARwf+3Tv3h1/+MMf8OSTT6KkpASPPvooxo4d6z338u6778ZLL72Ehx9+GHfccQe+/PJLvPvuu1i4cGFQ/Q1GqxpAEhEREZ1MzXkIe9asWQCO3275RHPmzPFGET733HPQ6/W47rrr4HA4UFBQgFdeecU7rcFgwKeffop77rkHeXl5iIqKwvDhw33uvJednY2FCxdi/PjxeP7559G2bVu8/vrrzRbhA3AASURERGGsOQeQ/o6a/prFYsHLL7+Ml19+WXOarKysRoeof23QoEFYt25dUP37LTiAJCIiorB1Ki6iORNxAElERERh6/gAsokxPhxAenEASURERGFLp2/6Vdg6NweQDTiAJCIiorDFQ9ih4QDSj4j/ewIRZqPf2o9rd4ttM8/vpFmLG3C+2FaZ5Kw1S2S8WE+I1M5Lc5rkXK600vVivTY5Uqw7lfZtnKrq5baZOnmebnF2Fesd47TzL006OWMukHar54l1nUZeaIOanrGatX+tytCsAcBV58r5d+UpZ4t146VXadaUkjeCiUrOeYyzytl7WSbtTFMF+b3XGK8U631r5fzLDGjnZ1ZFpohto/VylIc+wHxJqpOzXNMd2tmfh+K7iG3vuEA7xxEADEquV+u0l8VjTjkz0OWR50tCpbwOI0l7Wd/w5+fFpr3HXi3W7Xv3i3V9yUdi3XmkXLt2VM4FTcuUcyA7nXOOWK9dX6RZc9TLy1pEehuxrrPLObMZeu35ZnFWiW1j4uU7vFXkyhmVXVPLNWsdnFvEtpGlu/w+X1lTK7aT6PRB3ImmidOFAw4giYiIKGxxD2RoOIAkIiKisMUBZGg4gCQiIqKwxUPYoeEAkoiIiMIW90CGhgNIIiIiClvcAxkaDiCJiIgofOl0xx9NnZYAcADpV/zFF8IWafVbi0zfJLa1dOumWXOlthfbVsXKsQyWunKxXgHtmJ8M11GxrceoHcMDAG3s2+X3jkzVrLUr+15sezClj1hP0VeI9a93aL93ZZVHbOtwyvU+XR4S6+1jy8R6RV2UZu1358oRIfVKjk45y/mtWN9o1o6N2laq3S8A6JEhz/MyuxzNVGfWfv2MY/I65EqUl8XC6iFivb5SewNfXi1v/JNi5eXhEtdXYr0qWo43STymHfMTE6kdRwUAMYe2iXV9VblYL+l8s2Ytt+QTse2udoPEuqFajlCpj9beNvWedrfY1r19q1iPPOssse45ckism9K0o53KPl4mtq23yzFhqx7/Wqzn/jFXsxbTKUtsC7cc87O3x1CxnnZ4o2Yt0O9BAuT7O1+UJkcIbXN00Ky5IuQ4u9ok//OlzlIjtpPodEEcwuYA0osDSCIiIgpbPIQdGg4giYiIKGzxIprQcABJREREYYt7IEPDASQRERGFLZ2+6XsWdRw/enEASURERGGLh7BDwwEkERERhS+9/vijqdMSAA4giYiIKIzpDQboDXJs2onT0nEcQPpR3v4cuKOj/dYMWXLmGJxVmqVDtk5i08Qa7Yw4ANBBzqjrtPMz7bYOu9jWHZcs1h1W7Rw3ANAp7VwwT4RJbHvMFSfWO9b/KNZvSd2sWfs08mKxbZQcOQa7Uz5cUV0f4AWk99bLuWUGnZzzdtCmnTkKAG1xULMW0zZObHvM4X/5b7Cr1CjW7a4MzZohUf5cxeVtxXqdQ94DsOeg9uufkyO/t8Ml/zh4DPKyvN8t9/07S45mrYvxsNj2aFs5JzbRuV+st1M7NWsV6dr9AoC4erlvh5J7iPWUI1s0azs7XCa2TUuVt5u1AXID9e3k79xQr71tjD7/WrGtRycvL5ET5SxHl2WXZu2AXs5aDfR7YXPI35nBqZ3dKdUAwLxfziTVZfcV6wlm7ZzIErSTXzvC/29NtVv7tzcQHsIODQeQREREFL50QRzC5lU0XhxAEhERUfgKYg8kuAfSq9UMpadOnXr8dkMnPHJyfjnsYrfbMXbsWCQmJiI6OhrXXXcdSktLW7DHREREdLrT6fRBPei4VjUnevTogYMHD3of33zzjbc2fvx4/Oc//8GCBQuwbNkyHDhwANdeK5+/QkRERGFOrwvuQQBa2SHsiIgIpKWlNXq+oqICb7zxBt555x1cfPHxiybmzJmDbt26YdWqVTj//PNPdVeJiIioFeCdaELTqubEtm3bkJGRgQ4dOuDWW2/Fnj17AABr166Fy+VCfn6+d9qcnBy0a9cOK1eu1Hw9h8OByspKnwcRERGFj4arsJv6oONazQCyX79+mDt3LhYtWoRZs2Zh586duPDCC1FVVYWSkhKYTCbExcX5tElNTUVJSYnma86YMQOxsbHeR2ZmZjN/CiIiIjqt6HQN9zNswoMDyAat5hD25Zdf7v3/3r17o1+/fsjKysK7774Lq9Ua0mtOmjQJEyZM8P67srISmZmZ2KPaI0rZ/LZJtRwSXzPBob0X0+Y8IrZVejlTzOCUcwPL250t1iVlESli3a3kvzXqPdqL0k9GOYuxh17OFDM65HyvCLt2plh+8g9i25hju8V6aVpvsW6ul/PSjhpTNWuV7hixbc/vXxHrJef/Xqyb3dp9MxvkjLl4s7ys9WnnEutrftb+bKuKGp+GcqLB58p5p2uKtL9vALh8oHZWY6y5TmzrMcnL+XcO+XSYsqPyJnWfsPlwuuR10BYZIM/QVi/WM45t0qwZS3eJbauy5W1LhV7+43tp9VDNWnaknFHbNsB2z1wpXyxZGy/3zXJkj2bNZDkqtnVFyfm4naK183EBoAyJmjWPW14W0wLMl8iSn8Q6IszatUNypmh9Rbn83rVy39r21P5s39SdK7bVGr/VVoc+nGEOZGhazR7IX4uLi0OXLl2wfft2pKWlwel0ory83Gea0tJSv+dMNjCbzbDZbD4PIiIiCiMNtzJs6oMAtOIBZHV1NXbs2IH09HTk5ubCaDRiyZIl3vrWrVuxZ88e5OXltWAviYiI6HT264jAQA86rtUcwn7wwQfxu9/9DllZWThw4ACmTJkCg8GAm2++GbGxsRg1ahQmTJiAhIQE2Gw23HvvvcjLy+MV2ERERKSNd6IJSasZQO7btw8333wzjh49iuTkZAwYMACrVq1CcvLxezg/99xz0Ov1uO666+BwOFBQUIBXXpHPISMiIqLwxnMgQ9NqBpD//ve/xbrFYsHLL7+Ml19++RT1iIiIiFq9hiusmzotAWhFA0giIiKiky6YO8xwD6QXB5B+WAxOWA0Ov7VIZ4XY1lj0tWbNk3vJb+rXdts5Yj3OUKZZ0ys5GiWQw/ZYsd7VpB3Fk26W40UOop1YP2qT401iY8s1a5VK7nfcsdViPc0tx9VUpHQV6513/VezVtbuLLGto4d8/m5i2Xaxrnf5X4YBwJEix/is3p8h1rumydFK3TO1o1nO66jdLwA4WBUt1qWYHkCOabPXy22Nejkqp7xG3mRmxMmRNPHR2u1jzXLb6Ag5Miq5ZpdYt3/6nmbN2FeOq3KY5O9k1c8JYt0g7LSxGOTtw5bo/mK9Tew+sW52yZEy8Gh/57p6p/zah+UYsHhhHQSAaPNhzZrdIm+7XKYosW6ql7ddymDUrjnkZXHbe9q/cwCQM+IKub2hh2ZtT6kcZ2fS6HZdjdxOEsw9rnkv7F9wAElEREThi3sgQ8IBJBEREYUt3gs7NBxAEhERUfjS6Zp+i0LmQHpxKE1EREThy6AHDIYmPoIbNi1fvhy/+93vkJGRAZ1Oh48++sinrpTC5MmTkZ6eDqvVivz8fGzb5ntNwbFjx3DrrbfCZrMhLi4Oo0aNQnW17y1dN2zYgAsvvBAWiwWZmZl48sknQ5oVweAAkoiIiMJWwyHspj6CUVNTgz59+mhGDD755JN44YUXMHv2bKxevRpRUVEoKCiA3f7LhUy33norNm/ejMLCQnz66adYvnw5xowZ461XVlZiyJAhyMrKwtq1a/HUU09h6tSpeO2110KbIU3EQ9hEREQUvpoxB/Lyyy/H5Zdf7remlMLMmTPx6KOP4uqrrwYAvP3220hNTcVHH32Em266CT/++CMWLVqE77//HuecczyJ5cUXX8QVV1yBp59+GhkZGZg3bx6cTifefPNNmEwm9OjRA0VFRXj22Wd9BponG/dAEhERUfjS6X65EjvQ43/nQFZWVvo8HA45ssmfnTt3oqSkBPn5+d7nYmNj0a9fP6xcuRIAsHLlSsTFxXkHjwCQn58PvV6P1atXe6cZOHAgTKZfYsoKCgqwdetWlJVpx/v9VtwD6UeErh4Rev/ZZKbaar/PN9BnddKsGe2VYtvamFSxnqQ7JNajao5q1iKPyHllrhg5xy3NKOcGRv60WbOmLHJbfYacA1dukedLhFs7qy0iQK5fSbd8sZ549CexbjusnX8JAHUpHTRrZqe8LAWi9HLu2eEU7aw1t05e9S9st1Osp+//Xqzrq8s1axXtzxbbmmPbi3WDTv5O45SwHtjljen2iJ5iPSFaztZLt2rn+gFAUrX2emj9YYXYFmltxbIrVs5L1V9zq2atwmwT264u6ybWu2TIP54l5WbNmk6nxLYJpnKxHnf4Z7Gurw/ww+7Rfn/XD9+JTSPOGyDWVYALLmqsiZq1qDrt5RgATDXHxLonSs6R1NUKWa4B+t1t/G1ifU/Xy8R6yVHtDMv4GLEp4qL8r/+1Znm7IAklBzIzM9Pn+SlTpmDq1KlBvW9JSQkAIDXV9zcuNTXVWyspKUFKiu+6HRERgYSEBJ9psrOzG71GQy0+Pj6ofjUVB5BEREQUvkLIgdy7dy9stl/+8DKbtf9IOlPxEDYRERGFr4ZzIJv6AGCz2XweoQwg09LSAAClpaU+z5eWlnpraWlpOHTI9+hjfX09jh075jONv9c48T2aAweQREREFL4aciCb+jhJsrOzkZaWhiVLlnifq6ysxOrVq5GXlwcAyMvLQ3l5OdauXeud5ssvv4TH40G/fv280yxfvhwu1y+n2BQWFqJr167Ndvga4ACSiIiIwpleH9wjCNXV1SgqKkJRURGA4xfOFBUVYc+ePdDpdLj//vvx+OOP45NPPsHGjRtx++23IyMjA8OGDQMAdOvWDZdddhlGjx6N7777DitWrMC4ceNw0003ISMjAwBwyy23wGQyYdSoUdi8eTPmz5+P559/HhMmTDiZc6kRngNJRERE4asZY3zWrFmDwYMHe//dMKgbPnw45s6di4cffhg1NTUYM2YMysvLMWDAACxatAgWi8XbZt68eRg3bhwuueQS6PV6XHfddXjhhRe89djYWHzxxRcYO3YscnNzkZSUhMmTJzdrhA/AASQRERGFsxAuommqQYMGQSntK/11Oh2mT5+O6dOna06TkJCAd955R3yf3r174+uvvw6qb78VB5BEREQUvnS6IPZA8l7YDTiA9MOqq0GkxsJUZ5VPSK1Ps2rWYg5sEdvatq0X69V9LxHrZVFtNGs65RHbVkbJV2rF1Mn5dva2XcW6JLLyoFg3CDmPAFAVqZ1/l1azXWwbKEuxLlrO1jN45FxAS4X2Z3NGa2fAAYDbIF/VZ1B2sR5bI7y3STuHDZCzNQFAt1fO3kO8dq6o2SHnoXao/UF+b7c8zyvj2mnWai3y+pvllj9XeVSyWE+q2iXWDW7tTELVJktsWx8l9706Rl6HoysPaBcDXECaF7dJrO9ya+edAsB5bUo1azrIOZDRrnKxvi9JzhWN8cjZn9FR2n3zpGnn+gKA3SRn3KoAg5J6vUm7KOy1AgBHdJJcN8uBijrp9bO1SwDgNFjEelV9gPcWsj+zkmrFtm0s/r+vqggh1zKQYC6O4QDSiwNIIiIiCl/BXBwT5EU0ZzIOIImIiCh8cQ9kSDiAJCIiovDVjFdhn8k4gCQiIqLwpQviEDYHkF4cQBIREVH44iHskHAASUREROGLh7BD0moGkDNmzMAHH3yA4uJiWK1W9O/fH3//+9/Rtesv8TGDBg3CsmXLfNrdddddmD17dlDvFeUsR5Sj3m8tUCxDpUU75qMs+2KxbUryDrHuNgiRDwDcSjuS5ki0HBFidVeL9UqrHGcTX7lbs6YPELsSaJ4G+tyJx7SjehyRcvRJ5JGdYt0e31asewxGsV6VqB1vYnTJcRV1ljixrlduse7Sa2eziPEhABIr5DgbZ06uWJe+U4OzTn5ta5xYN9XKsSzGeu14o0CRUPoAsUypjgqxXhMpx/xI3Dbt+CEgcNxNhEf+bC6LTfu9A6xjDoMcV5MScUSs66G9rIpxMk0Q7yoR60onR3XVG7U/m1WI4QIAd4Scf+Q2yNs2abtpqJe/TwSIZrOW7xfr9th0zZrDrL2sAIBbLw8d4vTlYl0Xq/2dR+lrxLaWev+/VS633E7uEPdAhqLVDKWXLVuGsWPHYtWqVSgsLITL5cKQIUNQU+O70IwePRoHDx70Pp588skW6jERERGd9prxXthnslazB3LRokU+/547dy5SUlKwdu1aDBw40Pt8ZGQk0tLkQF0iIiIiAFB6fcCbSpw4LR3XaudERcXxQ0kJCb53vJg3bx6SkpLQs2dPTJo0CbW12ocJHQ4HKisrfR5EREQURhrOgWzqgwC0oj2QJ/J4PLj//vtxwQUXoGfPnt7nb7nlFmRlZSEjIwMbNmzAI488gq1bt+KDDz7w+zozZszAtGnTTlW3iYiI6DSjdDqoJp7b2NTpwkGrHECOHTsWmzZtwjfffOPz/JgxY7z/36tXL6Snp+OSSy7Bjh070LFjx0avM2nSJEyYMMH778rKSmRmZjZfx4mIiOj0wquwQ9LqBpDjxo3Dp59+iuXLl6NtW/kK2X79+gEAtm/f7ncAaTabYTbLV9ERERHRGYxXYYek1QwglVK499578eGHH2Lp0qXIzs4O2KaoqAgAkJ6uHVdAREREYSyYq6t5EY1XqxlAjh07Fu+88w4+/vhjxMTEoKTkePZXbGwsrFYrduzYgXfeeQdXXHEFEhMTsWHDBowfPx4DBw5E7969g3ovj94Ij95/vl+VUc4VjK4v16zZI6LEtnuieoh1g07O/bPqtPP1tLKzvG3t5WI9UE5chEM7g8ttssqvbbSIdZNDvripLipJu19CJiAA6Fxy1pq5OkC+XV2VWHdHxmrW6q0xYlspxxEAou1H5fc2aa/ekS55njoD5MAdNbcR6+lVxdqvHSDnMRBHtPb3HYgnQH5doL0LEc4AGXVOeXmoN2h/p54I+SrQ6JpDYj1QJuGxSO3vzBIgQy/SWS7WA2W5Atrz1Vorr2O1UXIGbWSN3N5lkre7UvanS1h/ASCiXs40DbTtkjIo6yLl5Ty6fK9Yd5vlz220a/ftUJS8gybaHSAPVS9vP6IN2r9HBiEzFAAMGt+X1vNNwXMgQ9NqBpCzZs0CcDws/ERz5szBiBEjYDKZsHjxYsycORM1NTXIzMzEddddh0cffbQFektEREStAs+BDEmrGUCqAHcryMzMbHQXGiIiIiKJ0umbsBf9l2npuFYzgCQiIiI66XgRTUg4gCQiIqKwpRDEHsjWe/+Vk44DSCIiIgpf3AMZkpCH0jt27MCjjz6Km2++GYcOHb868L///S82b9580jpHRERE1Kx0uiBuZcgBZIOQBpDLli1Dr169sHr1anzwwQeorj5+Sf769esxZcqUk9pBIiIioubSEOPT1AcdF9Ih7IkTJ+Lxxx/HhAkTEBPzS5bdxRdfjJdeeumkda6luAxmuDTy2hJr5Oytiqg0zZrJLWcStnEelvsVIecpWuu0cwF3R/cS26aa5AytvfoOYt2cpJ0bluQpEdsGyju0uORsPUtdmWYtUJ6hKy5VrHsi5PxLU72cIylmPQZIFrDVybl/Oo+cl6ZXcl1Sa44T6yl1u8S6R6e9adEpj9i2xpoo1iMd5WIdOu08xcjyfWLTyqRO8msHYHTIy6reoL28RLgdYttAy6LRKWe9Rhm1s/vq9fJrm+1y7p/dKufjmh3a7QOto7oAy3GFTb4jmUHVi3XpswdaVgOtY4HydaV1ONB7qwDLQ71R/r1wCfVoj/x9GwPkX1qM8tAi4djPmrWaGHmbrDTWb63nm4QxPiEJaU5s3LgR11xzTaPnU1JScOSIHOpKREREdLpQ0AX1oONCGkDGxcXh4MGDjZ5ft24d2rSR71BBREREdLpoyIFs6oOOC2lO3HTTTXjkkUdQUlICnU4Hj8eDFStW4MEHH8Ttt99+svtIRERE1DyafAFNEIe6w0BIc+Jvf/sbcnJykJmZierqanTv3h0DBw5E//79eetAIiIiajV4EU1oQrqIxmQy4f/+7//w2GOPYdOmTaiursZZZ52Fzp07n+z+ERERETUb3sowNL8pSLxdu3Zo167dyeoLERER0amlN0Dpm3gVd1OnCwNNHkBOmDChyS/67LPPhtSZ1uBwZJZYzzi2SbPmMRjFtvoAsSywyOXSGO09wIkeORImkDbYI9YttdqxD26DHDcRU7tfrNdFJYn10riumrXU8q1i2yqbfNGXWy9/Z4k12hFCAFAnRNIEikYxOuRYFpcpSqxLPAH+io6plZcXh1mIJwJgcGvH1ZgdlWLb2Eo5aqc2Kjnk9w7EWiunSHgCLA8Rdvk780Rpt7dUloptHTHy5y63yX/MGzzaUV0JR34S21YmtBfr1rpjYr3OmqBZM7rkSJhA62CgmJ7aCDkmKPWI9s0vamzpYlsEuCI34KBEjPKSY77cAWJ8aoV5DgDV+ljNWoxb3q4F2gsXVyn/XriN2tFtJmeN2Fbrc7n1oe8PC+bqal6F/Ysmz/F169b5/PuHH35AfX09unY9/uP9008/wWAwIDc39+T2kIiIiKiZ8BB2aJo8gPzqq6+8///ss88iJiYGb731FuLjjwfIlpWVYeTIkbjwwgtPfi+JiIiImoMOQdwLu1l70qqENJR+5plnMGPGDO/gEQDi4+Px+OOP45lnnjlpnSMiIiJqTgr6oB50XEgnDVRWVuLw4ca33Tt8+DCqquRbeRERERGdLoKJ52GMzy9CGkpfc801GDlyJD744APs27cP+/btw/vvv49Ro0bh2muvPdl9JCIiImoWvBNNaELaAzl79mw8+OCDuOWWW+ByHb+yLyIiAqNGjcJTTz11UjtIRERE1Fx4FXZoQhpKR0ZG4pVXXsHRo0exbt06rFu3DseOHcMrr7yCqKjQo0WIiIiITqVTsQfy5ZdfRvv27WGxWNCvXz989913J/lTnHq/KUg8KioKvXv3Pll9OW1YnFWwOP1ncNkccmZhRax2FpvR7RDbegJkhgX6yydCp52H5tHJr22pk3O/aqJSxLrdop0pFlEvf26XOVp+b3O8WLe6tbP3Aq3sVnu5WA+UxVhvlTPmYo7t0qzZbali24pIOf8y0PIksTjlc5WPRmWK9eSqn8W6oV47izFgfqWYjRd4PQG0cwMDfV8Gp5xJqCzye9fFyrmBDqP2Z9db5OU8UG6oxRXo/HPt7YczUn5vS4D3dkfIIbV6j/a2qcYi5xXG1DU+3z4Y0ZUHxLqU9agCbDcj6u1iPdB21+wIPT/3t0qya+etRrhqxbamKjkvtS5OzteVXj9C2HYAgNFo9f98vbzuSpr7HMj58+djwoQJmD17Nvr164eZM2eioKAAW7duRUqK/Nt6OgtpADl48GDohJn45ZdfhtwhIiIiolOluQ9hP/vssxg9ejRGjhwJ4PhpgAsXLsSbb76JiRMnBv16p4uQBpB9+/b1+bfL5UJRURE2bdqE4cOHn4x+ERERETW7UILEKyt976hlNpthNje+w47T6cTatWsxadIk73N6vR75+flYuXLlb+h1ywtpAPncc8/5fX7q1KmorpYP+RERERGdLkLZA5mZ6Xuaz5QpUzB16tRG0x85cgRutxupqb6nLKWmpqK4uDi0Dp8mTur16LfddhvefPPNk/mSITkTT1YlIiKik08hiIto/jds2rt3LyoqKryPE/cwhouTOoBcuXIlLBb5ZOrm1nCy6pQpU/DDDz+gT58+KCgowKFDh1q0X0RERHT6adgD2dQHANhsNp+Hv8PXAJCUlASDwYDS0lKf50tLS5GWltbsn605hXQI+9dh4UopHDx4EGvWrMFjjz12UjoWqjP1ZFUiIiI6+Y5fhd3UcyCDu4jGZDIhNzcXS5YswbBhwwAAHo8HS5Yswbhx44Lt6mklpAGkzWbzuQpbr9eja9eumD59OoYMGXLSOhesYE9WdTgccDh+iUJpOCnWUn0YVlXj/00CLDxGt3asg8HtEtvWmuTYFpPw2gBgqdc+/9QaIKbHEyAyQq88ct2j/dlcASI+PHp5MYy2HxXrZVbtGA5XbHuxbaRTjiepD9D3QFEbJpPGcgTAHeBzB4rpMTkCxLYIy6ozQJROWpl8bk5NtBxBpFNuzZolUHRSXaVYjxSrcoSQChQBFGD91gVYD8w18rJab/O/lwIADG45vsRpjhHrUlQOABiEdTTQj2eg5TxQ3y2ucs1aZPVvOzpUHtderEcG+GxGl3b8i8soL23OAPVAcVllsVmaNYOSv8/qAPFHlnrtbQ8A6Dza62h9gM9VmS7H98WX7RTrBmEdr0ruKLY1O/y3dTjkzytp7quwJ0yYgOHDh+Occ87Beeedh5kzZ6Kmpsa7o6u1CmkAOXfu3JPcjZMj2JNVZ8yYgWnTpp2q7hEREdFpprlzIG+88UYcPnwYkydPRklJCfr27YtFixY1Gqu0NiGdA9mhQwccPdr4L+3y8nJ06NDhN3fqVJk0aZLPSbB79+5t6S4RERHRKaSULqhHKMaNG4fdu3fD4XBg9erV6Nev30n+FKdeSHsgd+3aBbe78e5vh8OB/fvlO7U0p2BPVtXKbSIiIqJw8cvV1U2Zlo4LagD5ySefeP//888/R2zsL7evc7vdWLJkCdq3b3/SOhesM/lkVSIiIjr5mvscyDNVUAPIhkGZTqdrdMcZo9GI9u3b45lnnjlpnQvFmXqyKhEREZ18HujhaeKexaZOFw6CGkB6PMevQMzOzsb333+PpCT5quGWcKaerEpEREQnH/dAhiakcyB37pQv0W9p48aN4yFrIiIiCiiYi2NCvYjmTNTkAeQLL7yAMWPGwGKx4IUXXhCnve+++35zx1qSyxoHZ6T/nLxAOXBHjBmaNVtEudjWAzmjLqbqgFivitF+7+qoFLFtdO1hsW6tPSLWpfw8ZYkXm9aabPJLKyXW4+sOinWJtapUrFfHtRXrZrucI+k0R2vWYo7Kf4jVxLcT61VR8l716Drt7yxQ9qYrQE5kRL2cSVojZNQ5IuTXTnDJ+ZeBciLLkrto1iLrjoltrZXycq4MRrGuFzIoAcB2eJtmrTw1R2wr5RUCgF7I3gQAV4RVsxZdtkdsWxPXRn7tALmBUo5khF5elhBg/Y+rkPvusMSK9VpromYtyi4vL/LSABgCfGfuSO310FYlb9dc0fK2yaOTf0+kzFJHtHx0MWn/OrHuipHbe6K1tw/WWnmeH43zn/RSBe0c5EC4BzI0TR5APvfcc7j11lthsVjw3HPPaU6n0+la/QCSiIiIwgMHkKFp8gDyxMPWp/shbCIiIqKm4AAyNCFdTjR9+nTU1tY2er6urg7Tp0//zZ0iIiIiOhUUgggS5wDSK6QB5LRp01Bd3fh8g9raWt4akIiIiFoND3RBPei4kK7CVkpB5+eiifXr1yMhQb7BOxEREdHpgoewQxPUADI+Ph46nQ46nQ5dunTxGUS63W5UV1fj7rvvPumdJCIiImoOjPEJTVADyJkzZ0IphTvuuAPTpk3zuZWhyWRC+/btkZeXd9I7SURERNQcFJq+Z1EOlQovQQ0gG25fmJ2djf79+8NoDJSC1TrpPC7oPS6/NYdZzhSzqXLNmqlezgSzuuVMwfoAWWvRNUKmYYAsNb3b/+dt4DZa5HqEdt0RIfdbHyBb0+KqEut2U4xmLf6Idu4eEDjfLlD2nsskfzaTs0azVpHcWWxrdsq5ZvHlchqCwaH93rUpcjZnoLxTh0k73xIALC7tvrsMZrGtS8jOBAAEyqh0CzmSAdYDe4J2lioA6Dxy1mJNov+MugZuIX/TVrFXbOsyay/ngJy1CAAGt3ZGZXWAzFGlk0+XD5SHqvTamYR1ljixbVTNIbEeUVMm1j0G+WdO2n5ECOsvANit8npkdMjrcFLZDs1ahU3OeYzwyJmjgQZELqt2/q716G75tSPkZS3QsiqpC5ALHFvjPx9TVyt/VxLugQxNSOdAXnTRRd7/t9vtcDp9F2SbTV4AiIiIiE4HPAcyNCFdhV1bW4tx48YhJSUFUVFRiI+P93kQERERtQZNjvAJYk9lOAhpAPnQQw/hyy+/xKxZs2A2m/H6669j2rRpyMjIwNtvv32y+0hERETULBQATxMfPAfyFyEdwv7Pf/6Dt99+G4MGDcLIkSNx4YUXolOnTsjKysK8efNw6623nux+EhEREZ10PAcyNCHtgTx27Bg6dDh+srjNZsOxY8dvfj5gwAAsX7785PWOiIiIqBk1nAPZ1AcdF9IAskOHDt77Yefk5ODdd98FcHzP5InRPkRERESnM54DGZqQDmGPHDkS69evx0UXXYSJEyfid7/7HV566SW4XC48++yzJ7uPp9yx6Cw4Y/zHEFjdciyD1aEdZ1FhTRXbxtWVBO6c4KgtW7OWsXul2NZlSxLrtdZEsV4XoR3bEOgvNpPHLtadAWKAbNUHtN9biA8BAsePGFxy3xApXzTmFCJnoqvk79tpkdMM7JHydyIFL0XVHRXbasVYNYgt2yXW7VHay1OgGJ9A0SmuAPPFJEWn+LmD1omqrSliPabaf4SI971dct/deu3oM6c1TmxrLd8v1h3RyWLdaK/Ufu8Ay3Gg+KJ6o1WuR2h/5yZXrdjWEyCeyBX92+5+ZnRrr+NH4jqJbeOr94l1Xb0cteMxas+XQOtoVaS8rHp08ravLCZTsxZtlKOy9EpeHqS4KgCw1GlHLwVqqxX7ZK8PaTgDgFdhhyqkOT5+/Hjv/+fn56O4uBhr165FUlIS/vnPf560zhERERE1J486/mjqtHRcSIewfy0rKwvXXnstYmNj8cYbb5yMlyQiIiJqdh6lD+pBx4W+z5eIiIiolVMq4E2qfKal4ziAJCIiorDlgQ6eJp7b2NTpwgEHkERERBS2mAMZmqAGkNdee61YLy8v/y19ISIiIjqleAg7NEENIANlPMbGxuL222//TR0iIiIiOlUY4xOaoAaQc+bMaa5+nFZ08EAPj99aoMwyvadesxZfI+e4aeVbNag1aGctAkBS1S7t4gGhBkAXLefAWRzaGXIA4DRo58BFeOQsNKtTfu1APEK2nt4gL+KGQDltEXIGnfXobvn1bdpZbYFyHgPluEnZegBQE5OuWTMH+D4DqY2RM02l7E6j2yG2DZRvGSiDDsJ3LuUwAkBsxR6xXmVrI9YDrSfW6kOaNUdUgM9tlzMmLW7tbQ8AVKTmaL92gNxPo6tOrLsDZDWaHVWaNYdZ3q5J29SmcJjl9cxaq523GF0u5zxWx7UV6xEB5ptL+OzVlt+2HhiUPN8swndiEeYJIPcbADwRci6olK9prdfuFwBY7eX++xRg/ZAwxic0reJ69F27dmHUqFHIzs6G1WpFx44dMWXKFDidTp9pdDpdo8eqVatasOdERER0WgvmLjQ8B9KrVVxEU1xcDI/Hg1dffRWdOnXCpk2bMHr0aNTU1ODpp5/2mXbx4sXo0aOH99+JifJfcURERBS+eA5kaFrFAPKyyy7DZZdd5v13hw4dsHXrVsyaNavRADIxMRFpaWmnuotERETUCjHGJzSt4hC2PxUVFUhIaHwP1KuuugopKSkYMGAAPvnkE/E1HA4HKisrfR5EREQUPhr2QDb1Qce1ygHk9u3b8eKLL+Kuu+7yPhcdHY1nnnkGCxYswMKFCzFgwAAMGzZMHETOmDEDsbGx3kdmpvbN5YmIiOjM09TzH4PJiwwHLTqAnDhxot8LX058FBcX+7TZv38/LrvsMtxwww0YPXq09/mkpCRMmDAB/fr1w7nnnosnnngCt912G5566inN9580aRIqKiq8j7179zbbZyUiIqLTT8NV2E190HEteg7kAw88gBEjRojTdOjQwfv/Bw4cwODBg9G/f3+89tprAV+/X79+KCws1KybzWaYzXIUChEREZ25eBFNaFp0AJmcnIzk5OQmTbt//34MHjwYubm5mDNnDvT6wDtPi4qKkJ6unYWn2a+jxbDZ/efY1VuixbbVkdq5f/X6AJmCLjn/Kr1so1gvSeqpWctof1hsq3PLOXBKL2cSWl3a548GypCzB8hpC5S9GeHUzv8ylco5jXDaxbI7tZ3cXicvh/WmKM2a1G8AcAaYL4Hy8aKqSrXfu0rOeatN6SDWHUZ5PajVaddjPfJ7V1san9t8orjqA2LdadJ+b0+A5bjaliHWIwJkWJrsAc6jFn59ovZtkdtGyJtrXb28DkfUay/rUUd3ya/tljMHy7LyxHp5hPZ23ooA+X0B/sa3VGlnawKANcAv/rG4bM2araZEbFtmlC/YdMZq56ECQHSNdt/N9fJ2zyFkrQJAvU7OPI2I0M7ArYyVT+fSymL0to+Q12EDtJenmgj5hiU6s//v0+kK/YAqg8RD0yquwt6/fz8GDRqErKwsPP300zh8+JfBUMMV12+99RZMJhPOOussAMAHH3yAN998E6+//nqL9JmIiIhOfx4EESTerD1pXVrFRTSFhYXYvn07lixZgrZt2yI9Pd37ONFf/vIX5Obmol+/fvj4448xf/58jBw5soV6TURERKe70+Uq7L/+9a/o378/IiMjERcX53eaPXv2YOjQoYiMjERKSgoeeugh1Nf7Ho1aunQpzj77bJjNZnTq1Alz585t9Dovv/wy2rdvD4vFgn79+uG7774Lur+tYgA5YsQIKKX8PhoMHz4cW7ZsQU1NDSoqKrB69Wpcf/31LdhrIiIiOt2dLgNIp9OJG264Affcc4/futvtxtChQ+F0OvHtt9/irbfewty5czF58mTvNDt37sTQoUMxePBgFBUV4f7778edd96Jzz//3DvN/PnzMWHCBEyZMgU//PAD+vTpg4KCAhw6JJ8O8mutYgBJRERE1Bw8ShfUo7lMmzYN48ePR69evfzWv/jiC2zZsgX//Oc/0bdvX1x++eX4y1/+gpdfftl7a+fZs2cjOzsbzzzzDLp164Zx48bh+uuvx3PPPed9nWeffRajR4/GyJEj0b17d8yePRuRkZF48803g+ovB5BEREQUtkLZA/nrm5A4HPIFdifDypUr0atXL6SmpnqfKygoQGVlJTZv3uydJj8/36ddQUEBVq5cCeD4Xs61a9f6TKPX65Gfn++dpqk4gCQiIqKw5fEA7iY+PP+7iiYzM9PnRiQzZsxo9n6WlJT4DB4BeP9dUlIiTlNZWYm6ujocOXIEbrfb7zQNr9FUreIq7FPNE2GCx+g/O8JhliMGpKieSiW3VSZ513hNUpxYT6zeo1nTO+W/jo616SLWbWVyHI5RY34BgN0aL7a1R8iRME6DVawrIUrHECdHhBgOy5Ew+lo5lsWRJMdd6DzacRVSxA8gx64AQH2ERazXxGhHjMjvHDhiKM5RLdY9QjRKlVFeHqSIDwBwGeXlQWKpKxfrTrO8LCqdHANUGy3HkumF5SF6T7FmDQA8bTqKdWk9AACTQ3tZdpvlJULvkpdFs1uOnPEYtPtW4YmTXzvQ+p8kbzddBnk9SSjboVnzGOQonMzDa8S62yT3/UhcJ81actk2sW2kTv7cFTZ521Rp0I7aiVTy+m2uPiLWYwOsoxYhBqgsRo5OO2zwH81XbZBj8CTB3GGmYbq9e/fCZvslak0rU3rixIn4+9//Lr7mjz/+iJycnCb29vTBASQRERGFrVCCxG02m88AUkuwN0yRpKWlNbpaurS01Ftr+G/DcydOY7PZYLVaYTAYYDAY/E7T8BpNxQEkERERha1gblEY7K0Mg7lhSiB5eXn461//ikOHDiEl5fhNSwoLC2Gz2dC9e3fvNJ999plPu8LCQuTlHQ/7N5lMyM3NxZIlSzBs2DAAgMfjwZIlSzBu3Lig+sNzIImIiChsnS4xPnv27EFRURH27NkDt9uNoqIiFBUVobr6+CkFQ4YMQffu3fGHP/wB69evx+eff45HH30UY8eO9R5Cv/vuu/Hzzz/j4YcfRnFxMV555RW8++67GD9+vPd9JkyYgP/7v//DW2+9hR9//BH33HMPampqgs7N5h5IIiIiCluny72wJ0+ejLfeesv774Y763311VcYNGgQDAYDPv30U9xzzz3Iy8tDVFQUhg8fjunTp3vbZGdnY+HChRg/fjyef/55tG3bFq+//joKCgq809x44404fPgwJk+ejJKSEvTt2xeLFi1qdGFNIBxAEhERUdhqzkPYwZg7d67fu8acKCsrq9Eh6l8bNGgQ1q1bJ04zbty4oA9Z/xoHkERERBS2Tpc9kK0NB5BEREQUtjwn5Ds2ZVo6jgNIP47FtocrJsZvzeQOPQ8tw3VUbBsoxy2mVM4FwxHtTEPnnr1i07hdP4l1Q4r/7K0G9YnadRWZKLZNORog/84gL6bm0p2aNUfxj2JbfbocW+BOlTPJnCb/y0mDOpN2zIMnQKbgQafctxyXfIjCelT7O9fVVIhtA/2ZraLk+IoYIePyWFRbsW2FW85LjdTL2ZzR5dqf23hYXg9UVm+xXh+hnXcKAOUm+RwiW/0xzdrRvpeLbWPLtXNeAcAVIMOyzhKnWTO75NxPpzFSrJtccg5klV47+9Ogk3M/PZDXk0qDvH2p88g5kDbjQc2auUbeZuurysS6QcmjjYyj+zRrjuQssa3LKGd3HnTL2+y4CO31qAJyVqvVJm+bpLxTADhk086/rHbLy3GNy/86WOOU31PCPZCh4QCSiIiIwhYHkKHhAJKIiIjClgdBXETTrD1pXTiAJCIiorCllIJq4q7Fpk4XDjiAJCIiorDFQ9ih4QCSiIiIwpYK4irsANdFhRUOIImIiChscQ9kaDiAJCIiorB1utyJprXhANKPaEcZoo0uv7WIejkH0lgn5OvpdGJbt8kqdyzAvvNDS77VrOkj5Cy1pPbt5beulrP3dLHaWWxmh9y2PE7OO4s/ul2s1+/drVmLiJMzBT01cv6dzlMv1qstCWK9yqOdl1jhkHPcbEY5W0/Vy7mhyiB855UBciDT5fxLj0nOQzS4HZq1NvtXi231FXL2njs+RazrhAw6FWEU21oqS8R6WUqOWI/QycuLErYBCSWbxbaH0/uI9TIl5yFG6LX7lhogxzF+b5FYr166VKxbDmrnX1ofnCy2XVUtf+4jFfK2rWu6vI7vtPbSrGXrNoltrQFyIOsyuop1aXdW5O4NctvMbmI51lIlvzW0l0Wjzv/vX4MSc3uxnuqUM0tTKrQzjVMD/M55DP7X4coAWaYS7oEMDQeQREREFLaUR0E1cddiU6cLBxxAEhERUdjiIezQcABJREREYYuHsEPDASQRERGFLbdbwe1u2siwqdOFA/ks/NNI+/btodPpfB5PPPGEzzQbNmzAhRdeCIvFgszMTDz55JMt1FsiIiJqDRr2QDb1Qce1qj2Q06dPx+jRo73/jomJ8f5/ZWUlhgwZgvz8fMyePRsbN27EHXfcgbi4OIwZM6YluktERESnOY9S8DRxZNjU6cJBqxpAxsTEIC0tzW9t3rx5cDqdePPNN2EymdCjRw8UFRXh2WefDXoAGb3hK8RYLf6LqW3kxqX7NUu6qBjNGgDUds0T62bXLrGe2Fc7YsTQSY6T0AWI6anpcp5Y/zFCO2oj0SS/dryrVKzXxaSK9aic3pq1HRmDxLbROjnqIrb6gFjXBdiYpNbv1ax1KS0W26q9O8T60e/kmA+7EN0U17Oz2NZgrxbrnuh4se42aMf8VKTIy6I1Ro7x8ejlKJ5j0ZmaNT20I34AIMIjx5foAkSMpByWo3gciz7WrO3fpr3tAICM/zdarJcmDhbrtfUa2zQACUY5QixQ/JEKcBsPU7T2e0fWHBHb2izyd1LnkA+keZRc71ypHStl3P2j2LY6p79Y32/qKNaP2LV/Ezr2yBDbppRuFOuxkfJ6tKa6p2atr22r2DauXDs6DQDcRjnmy3JAO5rNkd5BbnvY/3s7a+rEdhLlafodZngnml+0mkPYAPDEE08gMTERZ511Fp566inU1/+Sa7Zy5UoMHDgQJpPJ+1xBQQG2bt2KsjL/WV0OhwOVlZU+DyIiIgofCgpKNfEB7oFs0Gr2QN533304++yzkZCQgG+//RaTJk3CwYMH8eyzzwIASkpKkJ2d7dMmNTXVW4uPb7zHZMaMGZg2bVrzd56IiIhOS7wXdmhadA/kxIkTG10Y8+tHcfHxw3wTJkzAoEGD0Lt3b9x999145pln8OKLL8Lh0L7jRSCTJk1CRUWF97F3r/bhRiIiIjrzNHnv4/8edFyL7oF84IEHMGLECHGaDh38nw/Rr18/1NfXY9euXejatSvS0tJQWup7Ll3Dv7XOmzSbzTCb5XM1iIiI6MzFIPHQtOgAMjk5GcnJySG1LSoqgl6vR0rK8fvi5uXl4c9//jNcLheMxuMnfBcWFqJr165+D18TERER8VaGoWkVF9GsXLkSM2fOxPr16/Hzzz9j3rx5GD9+PG677Tbv4PCWW26ByWTCqFGjsHnzZsyfPx/PP/88JkyY0MK9JyIiotMVcyBD0youojGbzfj3v/+NqVOnwuFwIDs7G+PHj/cZHMbGxuKLL77A2LFjkZubi6SkJEyePJkZkERERKTJ41HwNHHPYlOnCwc6xTNCvSorKxEbG4vNa1chJjra7zS2KjmrTem1s/dMVXLeWVlqN7GuCxAfEF2pnVnoMssZlIHy7ZRO3lldbU0S65JSt5zzGGeU45U2H22rWWsf5z/CqUHHyrVivTZK/lyxu34Q6579e7SLenme6iOjxLouyv8y2kCZtbP3PJE2ua1e/ttS55az+exx2hl2VVb5tJVqyMtqIE6PSbO246h8OkvnRDk7L0Enr8NxFcL3DcC0RztXcE+fa8S20nIOAAeOyMtTUpz29iPaIudj9o7+Say7hNxPANhY1l6zVlalvc0EAGOA3RwJ0fKy2DtSzltN2LhYs/bzPxeKbVP7Zov1iKtuEusrdQM1a7mmIrHtIbN23ikAmHXyBaYOpf2dxagKsW2g36Io+zGxXhGpvc33QF4eEqr9X+xaWV2D7AFXoKKiAjabvH3ztvnfb/4fZ5bAbG1aG0ddJZ6/Py2o9zlTtYo9kERERETNgUHioeEAkoiIiMIWb2UYGg4giYiIKGwFk+/Is/5+wQEkERERhS1eRBMaDiCJiIgobAUTz8MdkL/gAJKIiIjCllJBBIlzBOnFASQRERGFLRXERTQcQP6CA0g/dFCaOVfGAFmO+toqzZorSc7tckREivVqj5yPVxrbRrNmM8hZipb6arEeVXNIrFud2q9vcsjvHauTszWdbvlzDzL9rFmLKK8T20ZUybl/li8/FeuepASxXt3/Ks1a7LZVYttD3fPFuq1aO/cTACLs2t9poFxP3bpv5bpFzv0zdzVq1owOeVmrjesl1u0e+b1La7SXl7Zx8ntb9fLysscpZzEejpEzTWu7XqBZ+2atVWy7fo28nlis2vmXABAZo50LarbIPwW1uXJGbb0cIwmP0mnWIuTYPxj08g92apT2NhcAoqtKxHp9uxzNWvuhcp6hIV1eHnbbuoj12qPaH/5f+84V2/bOlpfVJLO83U2u116eLPZyse2+mO5i/bBJXg/Ka7SX9W5mOXO01up/m1vnlpd/CW9lGBoOIImIiChsud0KbnfTBoZNnS4ccABJREREYYt7IEPDASQRERGFLeZAhoYDSCIiIgpbHk/T8x09vJWhl3wmPREREdEZrGEPZFMfzWHXrl0YNWoUsrOzYbVa0bFjR0yZMgVOp9Nnug0bNuDCCy+ExWJBZmYmnnzyyUavtWDBAuTk5MBisaBXr1747LPPGn3eyZMnIz09HVarFfn5+di2bVvQfeYAkoiIiMJWwzmQTX00h+LiYng8Hrz66qvYvHkznnvuOcyePRt/+tOfvNNUVlZiyJAhyMrKwtq1a/HUU09h6tSpeO2117zTfPvtt7j55psxatQorFu3DsOGDcOwYcOwadMm7zRPPvkkXnjhBcyePRurV69GVFQUCgoKYLfbg+qzTvGAvldlZSViY2Px9dodiI72HwXS6agcbyLFo1TGZ4ltY4/tFOs6j5yVUW+J1qyVxbYX2ypox2wAgKW+RqxLosv3inXjYble266nWI/ctV6z5j4qxy7V99GOVQGAiDo5CkP3849y3aIdneLofLbY1ry9KMBry7EvqrJCu5gmx49AycdpnPHpYt18UDtaaVfOULGt3aM9zwCgjXOHWK+yJmnWyurl2KVUnRyNtM3ZQazHm+T15GBNrGbtWLV29BEAVARYBV0uuZ4Sr72p3yendCE5Xt4+tE10iPXsKO35alD1Yts6RIl1K+QZ44C8PB12xmu/tkGeqYkGefuyx6EdrQYAPx3Ujm7r3/6g2LZNxSaxvsvWV6xXOLXnqznA5+5YL2/3Aones0GzdrDLxWJbvfL/O1hVXY1eZ5+LiooK2Gy2JvWj4Tf/1onbYbLIcXENnPYqzHuiU1DvE6qnnnoKs2bNws8/H9+ezpo1C3/+859RUlICk+l4bNHEiRPx0Ucfobi4GABw4403oqamBp9++ksE3fnnn4++ffti9uzZUEohIyMDDzzwAB588EEAQEVFBVJTUzF37lzcdNNNTe4f90ASERFR2PLgeJB4kx7/y4iurKz0eTgc8h9RoaioqEBCwi9/9K5cuRIDBw70Dh4BoKCgAFu3bkVZWZl3mvx83wzhgoICrFy5EgCwc+dOlJSU+EwTGxuLfv36eadpKg4giYiIKGyFcgg7MzMTsbGx3seMGTNOap+2b9+OF198EXfddZf3uZKSEqSm+oa0N/y7pKREnObE+ont/E3TVLwKm4iIiMJWKDE+e/fu9TmEbTb7v0PWxIkT8fe//118zR9//BE5Ob/cEWn//v247LLLcMMNN2D06NFN6ldL4ACSiIiIwpbyqCbH+DTsgbTZbE06B/KBBx7AiBEjxGk6dPjl3OoDBw5g8ODB6N+/v8/FMQCQlpaG0tJSn+ca/p2WliZOc2K94bn09HSfafr27Rvw85yIA0giIiIKW815J5rk5GQkJyc3adr9+/dj8ODByM3NxZw5c6DX+55lmJeXhz//+c9wuVwwGo9fdFdYWIiuXbsiPj7eO82SJUtw//33e9sVFhYiLy8PAJCdnY20tDQsWbLEO2CsrKzE6tWrcc899wT12XgOJBEREYWt0yEHcv/+/Rg0aBDatWuHp59+GocPH0ZJSYnPeYm33HILTCYTRo0ahc2bN2P+/Pl4/vnnMWHCBO80f/zjH7Fo0SI888wzKC4uxtSpU7FmzRqMGzcOAKDT6XD//ffj8ccfxyeffIKNGzfi9ttvR0ZGBoYNGxZUn7kHkoiIiMKW8nigmniLmaZOF6zCwkJs374d27dvR9u2vjFrDYPW2NhYfPHFFxg7dixyc3ORlJSEyZMnY8yYMd5p+/fvj3feeQePPvoo/vSnP6Fz58746KOP0LPnL3F4Dz/8MGpqajBmzBiUl5djwIABWLRoESxC7Jw/zIE8QUMm1Ja1KxET7T9TURcgH08foC6Jrtgn1mtsGXLdFKdZs9RXi213urPFeluTnElWC+0MyuxtC8W2Fe1zxbrtsJyQr7MLOXCV5WJbBIhe0MVqZ8QBQH1Cmvz6RdqxCBFt5CzGPT2vEusGyLmgRo/2Z4svkzNHA6kOsCxG1AcXSHsiQ/1vi8OwbvpGs1Z27pVy27pjYt1h1s5xBICDhnZiPat2s2btK8cAsa3FKG9b2sUcFetuZdCspSg5/zIiwHdirZHzENXKJdqv3fdcsW19lLwOlsfK87xWp71tAoAqt3Y92lArtt10WM5DTYiS51vbyMOatYp6OZfQ7jaJ9WijvA7We7QPQJbZtfMpAcAUIW97KurkvrWPLdOsScspAHg0Mourqytx8TntQ8qBvGbcRhjNTcuBdDmq8OFLvU5JDuTpjnsgiYiIKGyFchU2tZJzIJcuXQqdTuf38f333wM4fh9Jf/VVq1a1cO+JiIjodHU63MqwNWoVeyD79++Pgwd9D6E+9thjWLJkCc455xyf5xcvXowePXp4/52YmHhK+khEREStT3NehX0maxUDSJPJ5M0uAgCXy4WPP/4Y9957L3Q63/MhEhMTfaYlIiIi0uKBB54mXr/gQfNcRNMatYpD2L/2ySef4OjRoxg5cmSj2lVXXYWUlBQMGDAAn3zyifg6Doej0f0siYiIKHwoTzCHsVu6t6ePVjmAfOONN1BQUOBzqXt0dDSeeeYZLFiwAAsXLsSAAQMwbNgwcRA5Y8YMn3tZZmZmnoruExER0WmC50CGpkUHkBMnTtS8OKbhUVxc7NNm3759+PzzzzFq1Cif55OSkjBhwgT069cP5557Lp544gncdttteOqppzTff9KkSaioqPA+9u7d2yyfk4iIiE5Pbrc7qAcd16LnQAZ7j0gAmDNnDhITE3HVVXJGHgD069cPhYWFmnWz2ez3BujflHaDtdp/vlOgK/h7pGvnyO0ujxPb9k6SM6WKK+Q9pJVHtfOzSuR4O2QkyfWluzuI9awM7ff+MeEOsa2qkN87Lq67WK+oM2rWUtvKOW6dUSzWv6k6W6y3ja0S68Xdr9asmY3ywvTzmgC5f+lyXlqtEEFnisgR29qdYhnJAY7j1Nq1/zY1RsifOylGfvNUa7lYz+6oXU8oXi62dbbpItZjf9LOMwSA2CQ5H1NXpb0iXmH6SWxbni3nJUZVlIp1j0F7c18R3UZsW2+Sc/3KLaliveKyPO2awyq2PVotv7dH/tiwmuVltbxaez2qqo0T2xoD/IJGmuUJ1pVqZ8Gu/1FeD7p3bvzbdSJnfaAsR+1apEWeZ99/L9cNBnmQtT9N+wcnI0l+7e37/OdA2mv9P98UvIgmNC06gAzmHpHA8fylOXPm4Pbbb/feB1JSVFTkc7NwIiIiohMp5YFq4smNTZ0uHLSKq7AbfPnll9i5cyfuvPPORrW33noLJpMJZ511FgDggw8+wJtvvonXX3/9VHeTiIiIWgnugQxNqxpAvvHGG+jfvz9ycvwffvvLX/6C3bt3IyIiAjk5OZg/fz6uv/76U9xLIiIiajWCuTiGA0ivVjWAfOeddzRrw4cPx/Dhw09hb4iIiKi186ggciB5CNurVQ0giYiIiE4mHsIODQeQREREFLaU8kB5eBFNsDiA9CMzoQZR0f6jHXYfjRLb7q+M1ayVVcuxmxuQJdbr3XJMQVWddr1tsvxXU32AaKuMVDky5qddLs3aOT3kz324XH7t1UXyCnvJ+dqd31cmf1/rK84R633aV4v1QBEkh8u053t1tTzTDQb5+9YFSK0o/kk7wigpSY4A2fWznK3ktGt/3wCQ2SFBs7Z5zR6x7XW/7yjWdx2SI2O2Rl+pWatPkmdaXES9WDd1GyjWaxxyOkS5QXtZ75wi3wmr2il/ZxXuXmI9M1r79Q9WxMhtbfLyUGOX+xZv1o67OuqR19H02Dqxbq+Xf8bKauTvRFqPOmXIy7ndJW+7SsrkvnVI1f5sfbpZxLY1drGM2gD1Tlna7120U96uRUXJ87RXZ/m9v9+oHVF0YedysW1FTYrf5+tMjPE51TiAJCIiorDFGJ/QcABJREREYcvjATxN3LPYxCPdYYEDSCIiIgpbyhPEOZAcQXpxAElERERhi+dAhoYDSCIiIgpbPAcyNBxAEhERUdjiHsjQcAB5AqWOLxg1NdqRE3U1cvSK0aNdr6uRIx/MkP+yqffIMQV1Ndp1s+63xfjYnfJ7O+q04y5qA8QXBZovjjo5xqOmWnsxrq2VF/G6Wvlz1VTLMT61LpNYt9dqx1046uSZrtfLfasNON+0Y3zstXLsitOuvQ4AgNMhx5s46rTnu8spv3ZtjRxnY3eIZZh02utRoCgso5JjfFwG+TurDRDjU1er/Z3VVMufu8Ylf2e1dvm9qw3ar19bLW8fqvUBvjOX/KUYndrrUU2AmB1lDBClEyDGp1ZYBwHAIWzbag2/Lcanrkbe9tVUa2/bamu0o24AoM4e4PcgwHoivbe9NsDnrg2wHsibTXHbV10tL2t1Nf7jjepqjy/fDb/jwah3VjX53EZ3fU3Qr3+m0qlQ5vYZ6ueff0bHjnIGHREREZ2e9u7di7Zt2zZpWrvdjuzsbJSUlAT1Hmlpadi5cycsFjmr80zHAeQJysvLER8fjz179iA2VjsQPNxUVlYiMzMTe/fuhc1ma+nunDY4X/zjfPGP88U/zhf/OF/805ovSilUVVUhIyMDer285/dEdrsdTqe8t/fXTCZT2A8eAR7C9tGw0MXGxnKF9cNms3G++MH54h/ni3+cL/5xvvjH+eKfv/kSyo4fi8XCwWCImj5MJyIiIiICB5BEREREFCQOIE9gNpsxZcoUmM3y1Y7hhvPFP84X/zhf/ON88Y/zxT/OF/84X04fvIiGiIiIiILCPZBEREREFBQOIImIiIgoKBxAEhEREVFQOIAkIiIioqBwAHmCl19+Ge3bt4fFYkG/fv3w3XfftXSXTqmpU6dCp9P5PHJycrx1u92OsWPHIjExEdHR0bjuuutQWlragj1uHsuXL8fvfvc7ZGRkQKfT4aOPPvKpK6UwefJkpKenw2q1Ij8/H9u2bfOZ5tixY7j11lths9kQFxeHUaNGoTrAfbVPd4Hmy4gRIxotP5dddpnPNGfafJkxYwbOPfdcxMTEICUlBcOGDcPWrVt9pmnKerNnzx4MHToUkZGRSElJwUMPPYT6evm+3KezpsyXQYMGNVpe7r77bp9pzrT5MmvWLPTu3dsbgp2Xl4f//ve/3no4LitA4PkSjstKa8AB5P/Mnz8fEyZMwJQpU/DDDz+gT58+KCgowKFDh1q6a6dUjx49cPDgQe/jm2++8dbGjx+P//znP1iwYAGWLVuGAwcO4Nprr23B3jaPmpoa9OnTBy+//LLf+pNPPokXXngBs2fPxurVqxEVFYWCggLY7XbvNLfeeis2b96MwsJCfPrpp1i+fDnGjBlzqj5Cswg0XwDgsssu81l+/vWvf/nUz7T5smzZMowdOxarVq1CYWEhXC4XhgwZgpqaGu80gdYbt9uNoUOHwul04ttvv8Vbb72FuXPnYvLkyS3xkU6KpswXABg9erTP8vLkk096a2fifGnbti2eeOIJrF27FmvWrMHFF1+Mq6++Gps3bwYQnssKEHi+AOG3rLQKipRSSp133nlq7Nix3n+73W6VkZGhZsyY0YK9OrWmTJmi+vTp47dWXl6ujEajWrBggfe5H3/8UQFQK1euPEU9PPUAqA8//ND7b4/Ho9LS0tRTTz3lfa68vFyZzWb1r3/9Syml1JYtWxQA9f3333un+e9//6t0Op3av3//Ket7c/r1fFFKqeHDh6urr75as004zJdDhw4pAGrZsmVKqaatN5999pnS6/WqpKTEO82sWbOUzWZTDofj1H6AZvLr+aKUUhdddJH64x//qNkmHOaLUkrFx8er119/ncvKrzTMF6W4rJyuuAcSgNPpxNq1a5Gfn+99Tq/XIz8/HytXrmzBnp1627ZtQ0ZGBjp06IBbb70Ve/bsAQCsXbsWLpfLZx7l5OSgXbt2YTWPdu7ciZKSEp/5EBsbi379+nnnw8qVKxEXF4dzzjnHO01+fj70ej1Wr159yvt8Ki1duhQpKSno2rUr7rnnHhw9etRbC4f5UlFRAQBISEgA0LT1ZuXKlejVqxdSU1O90xQUFKCystJnD0xr9uv50mDevHlISkpCz549MWnSJNTW1nprZ/p8cbvd+Pe//42amhrk5eVxWfmfX8+XBuG8rJyuIlq6A6eDI0eOwO12+yx8AJCamori4uIW6tWp169fP8ydOxddu3bFwYMHMW3aNFx44YXYtGkTSkpKYDKZEBcX59MmNTUVJSUlLdPhFtDwWf0tKw21kpISpKSk+NQjIiKQkJBwRs+ryy67DNdeey2ys7OxY8cO/OlPf8Lll1+OlStXwmAwnPHzxePx4P7778cFF1yAnj17AkCT1puSkhK/y1NDrbXzN18A4JZbbkFWVhYyMjKwYcMGPPLII9i6dSs++OADAGfufNm4cSPy8vJgt9sRHR2NDz/8EN27d0dRUVFYLyta8wUI32XldMcBJHldfvnl3v/v3bs3+vXrh6ysLLz77ruwWq0t2DNqDW666Sbv//fq1Qu9e/dGx44dsXTpUlxyySUt2LNTY+zYsdi0aZPPecOkPV9OPPe1V69eSE9PxyWXXIIdO3agY8eOp7qbp0zXrl1RVFSEiooKvPfeexg+fDiWLVvW0t1qcVrzpXv37mG7rJzueAgbQFJSEgwGQ6Or3UpLS5GWltZCvWp5cXFx6NKlC7Zv3460tDQ4nU6Ul5f7TBNu86jhs0rLSlpaWqOLr+rr63Hs2LGwmlcdOnRAUlIStm/fDuDMni/jxo3Dp59+iq+++gpt27b1Pt+U9SYtLc3v8tRQa8205os//fr1AwCf5eVMnC8mkwmdOnVCbm4uZsyYgT59+uD5558P+2VFa774Ey7LyumOA0gcX3Bzc3OxZMkS73MejwdLlizxOQcj3FRXV2PHjh1IT09Hbm4ujEajzzzaunUr9uzZE1bzKDs7G2lpaT7zobKyEqtXr/bOh7y8PJSXl2Pt2rXeab788kt4PB7vhi8c7Nu3D0ePHkV6ejqAM3O+KKUwbtw4fPjhh/jyyy+RnZ3tU2/KepOXl4eNGzf6DK4LCwths9m8h/Bam0DzxZ+ioiIA8FlezrT54o/H44HD4QjbZUVLw3zxJ1yXldNOS1/Fc7r497//rcxms5o7d67asmWLGjNmjIqLi/O5qutM98ADD6ilS5eqnTt3qhUrVqj8/HyVlJSkDh06pJRS6u6771bt2rVTX375pVqzZo3Ky8tTeXl5Ldzrk6+qqkqtW7dOrVu3TgFQzz77rFq3bp3avXu3UkqpJ554QsXFxamPP/5YbdiwQV199dUqOztb1dXVeV/jsssuU2eddZZavXq1+uabb1Tnzp3VzTff3FIf6aSQ5ktVVZV68MEH1cqVK9XOnTvV4sWL1dlnn606d+6s7Ha79zXOtPlyzz33qNjYWLV06VJ18OBB76O2ttY7TaD1pr6+XvXs2VMNGTJEFRUVqUWLFqnk5GQ1adKklvhIJ0Wg+bJ9+3Y1ffp0tWbNGrVz50718ccfqw4dOqiBAwd6X+NMnC8TJ05Uy5YtUzt37lQbNmxQEydOVDqdTn3xxRdKqfBcVpSS50u4LiutAQeQJ3jxxRdVu3btlMlkUuedd55atWpVS3fplLrxxhtVenq6MplMqk2bNurGG29U27dv99br6urU//t//0/Fx8eryMhIdc0116iDBw+2YI+bx1dffaUANHoMHz5cKXU8yuexxx5Tqampymw2q0suuURt3brV5zWOHj2qbr75ZhUdHa1sNpsaOXKkqqqqaoFPc/JI86W2tlYNGTJEJScnK6PRqLKystTo0aMb/QF2ps0Xf/MDgJozZ453mqasN7t27VKXX365slqtKikpST3wwAPK5XKd4k9z8gSaL3v27FEDBw5UCQkJymw2q06dOqmHHnpIVVRU+LzOmTZf7rjjDpWVlaVMJpNKTk5Wl1xyiXfwqFR4LitKyfMlXJeV1kCnlFKnbn8nEREREbV2PAeSiIiIiILCASQRERERBYUDSCIiIiIKCgeQRERERBQUDiCJiIiIKCgcQBIRERFRUDiAJCIiIqKgcABJREREREHhAJKIwsKuXbug0+m899E92XQ6HT766KNmeW0iotMNB5BEdEqMGDECw4YNa7H3z8zMxMGDB9GzZ08AwNKlS6HT6VBeXt5ifSIiaq0iWroDRESngsFgQFpaWkt3g4jojMA9kETU4pYtW4bzzjsPZrMZ6enpmDhxIurr6731QYMG4b777sPDDz+MhIQEpKWlYerUqT6vUVxcjAEDBsBisaB79+5YvHixz2HlEw9h79q1C4MHDwYAxMfHQ6fTYcSIEQCA9u3bY+bMmT6v3bdvX5/327ZtGwYOHOh9r8LCwkafae/evfj973+PuLg4JCQk4Oqrr8auXbt+66wiIjotcABJRC1q//79uOKKK3Duuedi/fr1mDVrFt544w08/vjjPtO99dZbiIqKwurVq/Hkk09i+vTp3oGb2+3GsGHDEBkZidWrV+O1117Dn//8Z833zMzMxPvvvw8A2Lp1Kw4ePIjnn3++Sf31eDy49tprYTKZsHr1asyePRuPPPKIzzQulwsFBQWIiYnB119/jRUrViA6OhqXXXYZnE5nMLOHiOi0xEPYRNSiXnnlFWRmZuKll16CTqdDTk4ODhw4gEceeQSTJ0+GXn/879zevXtjypQpAIDOnTvjpZdewpIlS3DppZeisLAQO3bswNKlS72Hqf/617/i0ksv9fueBoMBCQkJAICUlBTExcU1ub+LFy9GcXExPv/8c2RkZAAA/va3v+Hyyy/3TjN//nx4PB68/vrr0Ol0AIA5c+YgLi4OS5cuxZAhQ4KbSUREpxkOIImoRf3444/Iy8vzDrQA4IILLkB1dTX27duHdu3aATg+gDxReno6Dh06BOD4XsTMzEyfcxzPO++8ZutvZmamd/AIAHl5eT7TrF+/Htu3b0dMTIzP83a7HTt27GiWfhERnUocQBJRq2A0Gn3+rdPp4PF4Tvr76PV6KKV8nnO5XEG9RnV1NXJzczFv3rxGteTk5N/UPyKi0wEHkETUorp164b3338fSinvXsgVK1YgJiYGbdu2bdJrdO3aFXv37kVpaSlSU1MBAN9//73YxmQyATh+/uSJkpOTcfDgQe+/KysrsXPnTp/+7t27FwcPHkR6ejoAYNWqVT6vcfbZZ2P+/PlISUmBzWZr0mcgImpNeBENEZ0yFRUVKCoq8nmMGTMGe/fuxb333ovi4mJ8/PHHmDJlCiZMmOA9/zGQSy+9FB07dsTw4cOxYcMGrFixAo8++igA+BwaP1FWVhZ0Oh0+/fRTHD58GNXV1QCAiy++GP/4xz/w9ddfY+PGjRg+fDgMBoO3XX5+Prp06YLhw4dj/fr1+PrrrxtdsHPrrbciKSkJV199Nb7++mvs3LkTS5cuxX333Yd9+/aFMuuIiE4rHEAS0SmzdOlSnHXWWT6Pv/zlL/jss8/w3XffoU+fPrj77rsxatQo7wCwKQwGAz766CNUV1fj3HPPxZ133ukd1FksFr9t2rRpg2nTpmHixIlITU3FuHHjAACTJk3CRRddhCuvvBJDhw7FsGHD0LFjR287vV6PDz/8EHV1dTjvvPNw55134q9//avPa0dGRmL58uVo164drr32WnTr1g2jRo2C3W7nHkkiOiPo1K9P9iEiOgOsWLECAwYMwPbt230GgERE9NtxAElEZ4QPP/wQ0dHR6Ny5M7Zv344//vGPiI+PxzfffNPSXSMiOuPwIhoiOiNUVVXhkUcewZ49e5CUlIT8/Hw888wzLd0tIqIzEvdAEhEREVFQeBENEREREQWFA0giIiIiCgoHkEREREQUFA4giYiIiCgoHEASERERUVA4gCQiIiKioHAASURERERB4QCSiIiIiILy/wHuY0ZbJ3EXUgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "denorm = resnet.test_target_transforms[0]\n", + "in_graphic = cl.utils.visualize_at_index(\n", + " resnet.to(device=\"cuda:0\"),\n", + " dm,\n", + " in_transform=denorm,\n", + " out_transform=denorm,\n", + " variable=\"geopotential\",\n", + " src=\"era5\",\n", + " index=0\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "qecdiyfS5H2U" + }, + "source": [ + "Congratulations on completing this quickstart of ClimateLearn. In this tutorial, we used ClimateLearn to download and process data from ERA5. Then, we evaluated two baseline methods, persistence and climatology, followed by visualizations of persistence's bias on one specific testing sample and its mean bias across all testing samples. Finally, we trained a deep learning model and visualized its bias on a specific testing sampkle.\n", + "\n", + "Since ClimateLearn was designed to be plug-and-play, each step of this pipeline can be customized.\n", + "\n", + "- Instead of using ERA5 data for forecasting, maybe you would like to use data from CMIP6. Specifically, ClimateLearn supports downloading and processing data from the [MPI-ESM1.2-HR](https://agupubs.onlinelibrary.wiley.com/doi/toc/10.1002/\\(ISSN\\)1942-2466.MPIESM1-2) model with the following function `cl.data.download_mpi_esm1_2_hr`.\n", + "- Or, instead of doing `direct-forecasting`, you could perform `continuous-forecasting` or `iterative-forecasting` by swapping out the first argument to the constructor of `cl.data.IterDataModule`.\n", + "- You could also change the deep learning model, optimizer, and learning rate scheduler by loading a different architecture or specifying your own bespoke solutions like:\n", + "```python\n", + "model = cl.load_forecasting_module(\n", + " data_module=dm,\n", + " model=\"resnet\",\n", + " model_kwargs={\"n_blocks\": 4, \"history\": 5},\n", + " optim=\"adamw\",\n", + " optim_kwargs={\"lr\": 5e-4},\n", + " sched=\"linear-warmup-cosine-annealing\",\n", + " sched_kwargs={\"warmup_epochs\": 5, \"max_epochs\": 50}\n", + ")\n", + "```\n", + "\n", + "The source code for ClimateLearn is publicly available on GitHub at https://github.com/aditya-grover/climate-learn, and the documentation website is https://climatelearn.readthedocs.io/." + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "13abcd9a2e724e059e6d4c538ffe889a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "16e44229fb724c3f85bc0b6f7dd86c45": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": "inline-flex", + "flex": null, + "flex_flow": "row wrap", + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": "100%" + } + }, + "1da5c642bf634cb5b35ccba3619ff4c9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2f97f73cab694612bb3151797ab85d50": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_6c4afba98d934616b3fd5f277140db49", + "max": 1, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_7c956ab21a9e43dd88cb28247622cd3f", + "value": 1 + } + }, + "339c3a61ba8f47d89cdb437baea61056": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3cd97411741d4ec792774956eea5f57f": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_9a741cc53a82407e8c9c394629fe6759", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
Epoch 0/0   1530/-- 0:10:27 • -:--:-- 2.83it/s v_num: 0 train/lat_mse:aggregate:  \n                                                                                0.019                              \n
\n", + "text/plain": "\u001b[37mEpoch 0/0 \u001b[0m \u001b[38;2;94;10;208m━\u001b[0m\u001b[38;2;97;7;219m━\u001b[0m\u001b[38;2;98;6;224m━\u001b[0m\u001b[38;2;97;7;219m━\u001b[0m\u001b[38;2;94;10;208m━\u001b[0m\u001b[38;2;89;16;189m━\u001b[0m\u001b[38;2;84;23;166m━\u001b[0m\u001b[38;2;78;31;141m━\u001b[0m\u001b[38;2;71;40;115m━\u001b[0m\u001b[38;2;66;47;92m━\u001b[0m\u001b[38;2;61;53;73m━\u001b[0m\u001b[38;2;58;56;62m━\u001b[0m\u001b[38;2;58;58;58m━\u001b[0m\u001b[38;2;58;56;62m━\u001b[0m\u001b[38;2;61;53;73m━\u001b[0m\u001b[38;2;66;47;92m━\u001b[0m\u001b[38;2;71;40;115m━\u001b[0m\u001b[38;2;78;32;141m━\u001b[0m\u001b[38;2;84;23;166m━\u001b[0m\u001b[38;2;89;16;189m━\u001b[0m\u001b[38;2;94;10;208m━\u001b[0m\u001b[38;2;97;7;219m━\u001b[0m\u001b[38;2;98;6;224m━\u001b[0m\u001b[38;2;97;7;219m━\u001b[0m\u001b[38;2;94;10;208m━\u001b[0m\u001b[38;2;89;16;189m━\u001b[0m\u001b[38;2;84;23;166m━\u001b[0m\u001b[38;2;78;31;141m━\u001b[0m\u001b[38;2;71;40;115m━\u001b[0m\u001b[38;2;66;47;92m━\u001b[0m\u001b[38;2;61;53;73m━\u001b[0m\u001b[38;2;58;56;62m━\u001b[0m\u001b[38;2;58;58;58m━\u001b[0m \u001b[37m1530/--\u001b[0m \u001b[38;5;245m0:10:27 • -:--:--\u001b[0m \u001b[38;5;249m2.83it/s\u001b[0m \u001b[37mv_num: 0 train/lat_mse:aggregate: \u001b[0m\n \u001b[37m0.019 \u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "46913ebf2920448ab7e4421fff41e7bd": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_a83b8bb369174edf999ad804cb1a9c4f", + "IPY_MODEL_2f97f73cab694612bb3151797ab85d50", + "IPY_MODEL_ce082e1180e546d0a45676e112673f02" + ], + "layout": "IPY_MODEL_96cf4617cf7f44929754d7f5390ecde9" + } + }, + "4f9ecc20290a4adb9be40faa1b44a056": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_13abcd9a2e724e059e6d4c538ffe889a", + "placeholder": "​", + "style": "IPY_MODEL_ac0fc5f828644428927b4df101958b9b", + "value": "Testing DataLoader 0: " + } + }, + "513a2a318bbe4c448835337736ebe9d5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5e0fa8d1d3f444d288dba5f75a078f63": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_f1f44758bf1742909f3d207875d25007", + "max": 1, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_ab5a4140314e4d4ca64b6a34050572d4", + "value": 1 + } + }, + "64da7f47dc7f404bbedee37a18b3d4ed": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_c438da1a4c064403a237be437a105583", + "placeholder": "​", + "style": "IPY_MODEL_eeb7d4a7fd2f491b9d150709f85d344f", + "value": " 40/? [00:01<00:00, 21.88it/s]" + } + }, + "6c4afba98d934616b3fd5f277140db49": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": "2", + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7c956ab21a9e43dd88cb28247622cd3f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "7dc2b997e4654ecdb777802d6f34ae8b": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_d49a429e44954fe3bb15c588e93e3214", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
Testing  43/-- 0:00:07 • -:--:-- 5.97it/s  \n
\n", + "text/plain": "\u001b[37mTesting\u001b[0m \u001b[38;2;97;7;219m━\u001b[0m\u001b[38;2;98;6;224m━\u001b[0m\u001b[38;2;97;7;219m━\u001b[0m\u001b[38;2;94;10;208m━\u001b[0m\u001b[38;2;89;16;189m━\u001b[0m\u001b[38;2;84;23;166m━\u001b[0m\u001b[38;2;78;31;141m━\u001b[0m\u001b[38;2;71;40;115m━\u001b[0m\u001b[38;2;66;47;92m━\u001b[0m\u001b[38;2;61;53;73m━\u001b[0m\u001b[38;2;58;56;62m━\u001b[0m\u001b[38;2;58;58;58m━\u001b[0m\u001b[38;2;58;56;62m━\u001b[0m\u001b[38;2;61;53;73m━\u001b[0m\u001b[38;2;66;47;92m━\u001b[0m\u001b[38;2;71;40;115m━\u001b[0m\u001b[38;2;78;32;141m━\u001b[0m\u001b[38;2;84;23;166m━\u001b[0m\u001b[38;2;89;16;189m━\u001b[0m\u001b[38;2;94;10;208m━\u001b[0m\u001b[38;2;97;7;219m━\u001b[0m\u001b[38;2;98;6;224m━\u001b[0m\u001b[38;2;97;7;219m━\u001b[0m\u001b[38;2;94;10;208m━\u001b[0m\u001b[38;2;89;16;189m━\u001b[0m\u001b[38;2;84;23;166m━\u001b[0m\u001b[38;2;78;31;141m━\u001b[0m\u001b[38;2;71;40;115m━\u001b[0m\u001b[38;2;66;47;92m━\u001b[0m\u001b[38;2;61;53;73m━\u001b[0m\u001b[38;2;58;56;62m━\u001b[0m\u001b[38;2;58;58;58m━\u001b[0m\u001b[38;2;58;56;62m━\u001b[0m\u001b[38;2;61;53;73m━\u001b[0m\u001b[38;2;66;47;92m━\u001b[0m\u001b[38;2;71;40;115m━\u001b[0m\u001b[38;2;78;32;141m━\u001b[0m\u001b[38;2;84;23;166m━\u001b[0m\u001b[38;2;89;16;189m━\u001b[0m\u001b[38;2;94;10;208m━\u001b[0m \u001b[37m43/--\u001b[0m \u001b[38;5;245m0:00:07 • -:--:--\u001b[0m \u001b[38;5;249m5.97it/s\u001b[0m \n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "96cf4617cf7f44929754d7f5390ecde9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": "inline-flex", + "flex": null, + "flex_flow": "row wrap", + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": "100%" + } + }, + "97668a06aa65408bb09f8c85e67dfd1c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_4f9ecc20290a4adb9be40faa1b44a056", + "IPY_MODEL_5e0fa8d1d3f444d288dba5f75a078f63", + "IPY_MODEL_64da7f47dc7f404bbedee37a18b3d4ed" + ], + "layout": "IPY_MODEL_16e44229fb724c3f85bc0b6f7dd86c45" + } + }, + "9a741cc53a82407e8c9c394629fe6759": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a83b8bb369174edf999ad804cb1a9c4f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_339c3a61ba8f47d89cdb437baea61056", + "placeholder": "​", + "style": "IPY_MODEL_513a2a318bbe4c448835337736ebe9d5", + "value": "Testing DataLoader 0: " + } + }, + "ab5a4140314e4d4ca64b6a34050572d4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "ac0fc5f828644428927b4df101958b9b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "c438da1a4c064403a237be437a105583": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ce082e1180e546d0a45676e112673f02": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_1da5c642bf634cb5b35ccba3619ff4c9", + "placeholder": "​", + "style": "IPY_MODEL_e5be2704139e488392222b0e68453d58", + "value": " 40/? [00:02<00:00, 19.80it/s]" + } + }, + "d49a429e44954fe3bb15c588e93e3214": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e5be2704139e488392222b0e68453d58": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "eeb7d4a7fd2f491b9d150709f85d344f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "f1f44758bf1742909f3d207875d25007": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": "2", + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/pyproject.toml b/pyproject.toml index 8f61f8bb..b942f32b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,25 +4,21 @@ build-backend = "setuptools.build_meta" [project] name = "climate_learn" -version = "0.0.2" +version = "1.0.0" authors = [ - { name = "MINT at UCLA", email = "jason.jewik@cs.ucla.edu" }, - { name = "Hritik Bansal", email = "hbansal@g.ucla.edu" }, - { name = "Shashank Goel", email = "shashankgoel@g.ucla.edu" }, - { name = "Siddharth Nandy", email = "sidd.nandy@gmail.com" }, - { name = "Tung Nguyen", email = "tungnd@g.ucla.edu" }, - { name = "Seongbin Park", email = "shannonsbpark@gmail.com" }, - { name = "Jingchen Tang", email = "tangtang1228@ucla.edu" }, - { name = "Jason Jewik", email = "jason.jewik@cs.ucla.edu" }, + { name = "MINT at UCLA", email = "jason.jewik@ucla.edu" }, + { name = "Tung Nguyen", email = "tungnd@cs.ucla.edu" }, + { name = "Jason Jewik", email = "jason.jewik@ucla.edu" }, + { name = "Hritik Bansal", email = "hbansal@ucla.edu" }, + { name = "Prakhar Sharma", email = "prakhar6sharma@gmail.com" }, { name = "Aditya Grover", email = "adityag@cs.ucla.edu" }, ] -description = "ClimateLearn: Benchmarking Machine Learning for Data-driven Climate Science" +description = "ClimateLearn: Benchmarking Machine Learning for Weather and Climate Modeling" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" classifiers = [ "Development Status :: 2 - Pre-Alpha", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -47,8 +43,10 @@ dependencies = [ "pytorch-lightning>=1.9.0", "scikit-learn>=1.0.2", "timm==0.9.2", + "tensorboard==2.11.2", "wandb>=0.13.9", "xarray>=0.20.2", + "rasterio>=1.3.7" ] [project.optional-dependencies] diff --git a/src/climate_learn/__init__.py b/src/climate_learn/__init__.py index e3c862d8..cbf23058 100644 --- a/src/climate_learn/__init__.py +++ b/src/climate_learn/__init__.py @@ -1,2 +1,12 @@ -from .loaders import * -from .trainer import Trainer +from .utils.loaders import ( + load_model_module, + load_forecasting_module, + load_climatebench_module, + load_downscaling_module, + load_architecture, + load_optimizer, + load_lr_scheduler, + load_loss, + load_transform, +) +from .models import LitModule diff --git a/src/climate_learn/data/README.md b/src/climate_learn/data/README.md new file mode 100644 index 00000000..e9cd732e --- /dev/null +++ b/src/climate_learn/data/README.md @@ -0,0 +1 @@ +The code contained in `climate_dataset/`, `dataset/`, `task/`, and `module.py` is experimental. We do not recommend their usage at this time. \ No newline at end of file diff --git a/src/climate_learn/data/__init__.py b/src/climate_learn/data/__init__.py index f56d04a2..e6021143 100644 --- a/src/climate_learn/data/__init__.py +++ b/src/climate_learn/data/__init__.py @@ -1,7 +1,4 @@ -from .climate_dataset import * -from .dataset import * from .download import * -from .itermodule import * -from .module import * -from .nc2npz import convert_nc2npz -from .task import * +from .itermodule import IterDataModule +from .mapmodule import ERA5toPRISMDataModule +from .climatebench_module import ClimateBenchDataModule diff --git a/src/climate_learn/data/climate_dataset/cmip6/constants.py b/src/climate_learn/data/climate_dataset/cmip6/constants.py new file mode 100644 index 00000000..f5c6829b --- /dev/null +++ b/src/climate_learn/data/climate_dataset/cmip6/constants.py @@ -0,0 +1,37 @@ +NAME_TO_VAR = { + "geopotential": "zg", + "u_component_of_wind": "u", + "v_component_of_wind": "v", + "temperature": "ta", + "specific_humidity": "hus", + "air_temperature": "tas", +} + +VAR_TO_NAME = {v: k for k, v in NAME_TO_VAR.items()} + +SINGLE_LEVEL_VARS = [ + "air_temperature", +] + +PRESSURE_LEVEL_VARS = [ + "geopotential", + "u_component_of_wind", + "v_component_of_wind", + "temperature", + "specific_humidity", +] + +DEFAULT_PRESSURE_LEVELS = [50, 250, 500, 600, 700, 850, 925] + +CONSTANTS = [] + +NAME_LEVEL_TO_VAR_LEVEL = {} + +for var in SINGLE_LEVEL_VARS: + NAME_LEVEL_TO_VAR_LEVEL[var] = NAME_TO_VAR[var] + +for var in PRESSURE_LEVEL_VARS: + for l in DEFAULT_PRESSURE_LEVELS: + NAME_LEVEL_TO_VAR_LEVEL[var + "_" + str(l)] = NAME_TO_VAR[var] + "_" + str(l) + +VAR_LEVEL_TO_NAME_LEVEL = {v: k for k, v in NAME_LEVEL_TO_VAR_LEVEL.items()} diff --git a/src/climate_learn/data/climate_dataset/cmip6_iterdataset.py b/src/climate_learn/data/climate_dataset/cmip6_iterdataset.py new file mode 100644 index 00000000..b06b5a35 --- /dev/null +++ b/src/climate_learn/data/climate_dataset/cmip6_iterdataset.py @@ -0,0 +1,210 @@ +# Standard library +import math +import os +import random +from typing import Union + +# Third party +import numpy as np +import torch +from torch.utils.data import IterableDataset + + +def shuffle_two_list(list1, list2): + list1_shuf = [] + list2_shuf = [] + index_shuf = list(range(len(list1))) + random.shuffle(index_shuf) + for i in index_shuf: + list1_shuf.append(list1[i]) + list2_shuf.append(list2[i]) + return list1_shuf, list2_shuf + + +class NpyReader(IterableDataset): + def __init__( + self, + inp_file_list, + out_file_list, + variables, + out_variables, + shuffle: bool = False, + ) -> None: + super().__init__() + assert len(inp_file_list) == len(out_file_list) + self.inp_file_list = [f for f in inp_file_list if "climatology" not in f] + self.out_file_list = [f for f in out_file_list if "climatology" not in f] + self.variables = variables + self.out_variables = out_variables if out_variables is not None else variables + self.shuffle = shuffle + + def __iter__(self): + if self.shuffle: + self.inp_file_list, self.out_file_list = shuffle_two_list( + self.inp_file_list, self.out_file_list + ) + + n_files = len(self.inp_file_list) + + worker_info = torch.utils.data.get_worker_info() + if worker_info is None: + iter_start = 0 + iter_end = n_files + else: + if not torch.distributed.is_initialized(): + rank = 0 + world_size = 1 + else: + rank = torch.distributed.get_rank() + world_size = torch.distributed.get_world_size() + num_workers_per_ddp = worker_info.num_workers + num_shards = num_workers_per_ddp * world_size + per_worker = n_files // num_shards + worker_id = rank * num_workers_per_ddp + worker_info.id + iter_start = worker_id * per_worker + iter_end = iter_start + per_worker + + for idx in range(iter_start, iter_end): + path_inp = self.inp_file_list[idx] + path_out = self.out_file_list[idx] + inp = np.load(path_inp) + if path_out == path_inp: + out = inp + else: + out = np.load(path_out) + yield {k: np.squeeze(inp[k], axis=1) for k in self.variables}, { + k: np.squeeze(out[k], axis=1) for k in self.out_variables + }, self.variables, self.out_variables + + +class Forecast(IterableDataset): + def __init__( + self, dataset: NpyReader, pred_range: int = 6, history: int = 3, window: int = 6 + ) -> None: + super().__init__() + self.dataset = dataset + assert pred_range % 6 == 0 + self.pred_range = pred_range // 6 + self.history = history + assert window % 6 == 0 + self.window = window // 6 + + def __iter__(self): + for inp_data, out_data, variables, out_variables in self.dataset: + inp_data = { + k: torch.from_numpy(inp_data[k].astype(np.float32)) + .unsqueeze(0) + .repeat_interleave(self.history, dim=0) + for k in inp_data.keys() + } + out_data = { + k: torch.from_numpy(out_data[k].astype(np.float32)) + for k in out_data.keys() + } + for key in inp_data.keys(): + for t in range(self.history): + inp_data[key][t] = inp_data[key][t].roll(-t * self.window, dims=0) + + last_idx = -((self.history - 1) * self.window + self.pred_range) + + inp_data = { + k: inp_data[k][:, :last_idx].transpose(0, 1) + for k in inp_data.keys() # N, T, H, W + } + + inp_data_len = inp_data[variables[0]].size(0) + + predict_ranges = torch.ones(inp_data_len).to(torch.long) * self.pred_range + output_ids = ( + torch.arange(inp_data_len) + + (self.history - 1) * self.window + + predict_ranges + ) + out_data = {k: out_data[k][output_ids] for k in out_data.keys()} + yield inp_data, out_data, variables, out_variables + + +class Downscale(IterableDataset): + def __init__(self, dataset: NpyReader) -> None: + super().__init__() + self.dataset = dataset + + def __iter__(self): + for inp_data, out_data, variables, out_variables in self.dataset: + inp_data = { + k: torch.from_numpy(inp_data[k].astype(np.float32)) + for k in inp_data.keys() + } + out_data = { + k: torch.from_numpy(out_data[k].astype(np.float32)) + for k in out_data.keys() + } + yield inp_data, out_data, variables, out_variables + + +class IndividualDataIter(IterableDataset): + def __init__( + self, + dataset: Union[Forecast, Downscale], + transforms: torch.nn.Module, + output_transforms: torch.nn.Module, + subsample: int = 6, + ): + super().__init__() + self.dataset = dataset + self.transforms = transforms + self.output_transforms = output_transforms + self.subsample = subsample + + def __iter__(self): + for inp, out, variables, out_variables in self.dataset: + inp_shapes = set([inp[k].shape[0] for k in inp.keys()]) + out_shapes = set([out[k].shape[0] for k in out.keys()]) + assert len(inp_shapes) == 1 + assert len(out_shapes) == 1 + inp_len = next(iter(inp_shapes)) + out_len = next(iter(out_shapes)) + assert inp_len == out_len + for i in range(0, inp_len, self.subsample): + x = {k: inp[k][i] for k in inp.keys()} + y = {k: out[k][i] for k in out.keys()} + if self.transforms is not None: + if isinstance(self.dataset, Forecast): + x = { + k: self.transforms[k](x[k].unsqueeze(1)).squeeze(1) + for k in x.keys() + } + elif isinstance(self.dataset, Downscale): + x = { + k: self.transforms[k](x[k].unsqueeze(0)).squeeze(0) + for k in x.keys() + } + else: + raise RuntimeError(f"Not supported task.") + if self.output_transforms is not None: + y = { + k: self.output_transforms[k](y[k].unsqueeze(0)).squeeze(0) + for k in y.keys() + } + yield x, y, variables, out_variables + + +class ShuffleIterableDataset(IterableDataset): + def __init__(self, dataset: IndividualDataIter, buffer_size: int) -> None: + super().__init__() + assert buffer_size > 0 + self.dataset = dataset + self.buffer_size = buffer_size + + def __iter__(self): + buf = [] + for x in self.dataset: + if len(buf) == self.buffer_size: + idx = random.randint(0, self.buffer_size - 1) + yield buf[idx] + buf[idx] = x + else: + buf.append(x) + random.shuffle(buf) + while buf: + yield buf.pop() diff --git a/src/climate_learn/data/climate_dataset/era5/constants.py b/src/climate_learn/data/climate_dataset/era5/constants.py index 9f9ac775..d1151566 100644 --- a/src/climate_learn/data/climate_dataset/era5/constants.py +++ b/src/climate_learn/data/climate_dataset/era5/constants.py @@ -1,12 +1,3 @@ -NAME_TO_CMIP = { - "geopotential": "zg", - "u_component_of_wind": "ua", - "v_component_of_wind": "va", - "temperature": "ta", - "relative_humidity": "r", - "specific_humidity": "hus", -} - NAME_TO_VAR = { "2m_temperature": "t2m", "10m_u_component_of_wind": "u10", @@ -58,7 +49,7 @@ DEFAULT_PRESSURE_LEVELS = [50, 250, 500, 600, 700, 850, 925] -CONSTANTS = ["orography", "lsm", "slt", "lat2d", "lon2d"] +CONSTANTS = ["orography", "land_sea_mask", "slt", "lattitude", "longitude"] NAME_LEVEL_TO_VAR_LEVEL = {} diff --git a/src/climate_learn/data/climate_dataset/era5_continuous_iterdataset.py b/src/climate_learn/data/climate_dataset/era5_continuous_iterdataset.py new file mode 100644 index 00000000..5ef818a6 --- /dev/null +++ b/src/climate_learn/data/climate_dataset/era5_continuous_iterdataset.py @@ -0,0 +1,207 @@ +# Standard library +import math +import os +import random +from typing import Union + +# Third party +import numpy as np +import torch +from torch.utils.data import IterableDataset + + +def shuffle_two_list(list1, list2): + list1_shuf = [] + list2_shuf = [] + index_shuf = list(range(len(list1))) + random.shuffle(index_shuf) + for i in index_shuf: + list1_shuf.append(list1[i]) + list2_shuf.append(list2[i]) + return list1_shuf, list2_shuf + + +class NpyReader(IterableDataset): + def __init__( + self, + inp_file_list, + out_file_list, + variables, + out_variables, + shuffle: bool = False, + ) -> None: + super().__init__() + assert len(inp_file_list) == len(out_file_list) + self.inp_file_list = [f for f in inp_file_list if "climatology" not in f] + self.out_file_list = [f for f in out_file_list if "climatology" not in f] + self.variables = variables + self.out_variables = out_variables if out_variables is not None else variables + self.shuffle = shuffle + + def __iter__(self): + if self.shuffle: + self.inp_file_list, self.out_file_list = shuffle_two_list( + self.inp_file_list, self.out_file_list + ) + + n_files = len(self.inp_file_list) + + worker_info = torch.utils.data.get_worker_info() + if worker_info is None: + iter_start = 0 + iter_end = n_files + else: + if not torch.distributed.is_initialized(): + rank = 0 + world_size = 1 + else: + rank = torch.distributed.get_rank() + world_size = torch.distributed.get_world_size() + num_workers_per_ddp = worker_info.num_workers + num_shards = num_workers_per_ddp * world_size + per_worker = n_files // num_shards + worker_id = rank * num_workers_per_ddp + worker_info.id + iter_start = worker_id * per_worker + iter_end = iter_start + per_worker + + for idx in range(iter_start, iter_end): + path_inp = self.inp_file_list[idx] + path_out = self.out_file_list[idx] + inp = np.load(path_inp) + if path_out == path_inp: + out = inp + else: + out = np.load(path_out) + yield {k: np.squeeze(inp[k], axis=1) for k in self.variables}, { + k: np.squeeze(out[k], axis=1) for k in self.out_variables + }, self.variables, self.out_variables + + +class Forecast(IterableDataset): + def __init__( + self, + dataset: NpyReader, + random_lead_time: bool = True, + min_pred_range=6, + max_pred_range: int = 120, + hrs_each_step: int = 1, + history: int = 3, + window: int = 6, + ) -> None: + super().__init__() + if not random_lead_time: + assert min_pred_range == max_pred_range + self.dataset = dataset + self.random_lead_time = random_lead_time + self.min_pred_range = min_pred_range + self.max_pred_range = max_pred_range + self.hrs_each_step = hrs_each_step + self.history = history + self.window = window + + def __iter__(self): + for inp_data, out_data, variables, out_variables in self.dataset: + inp_data = { + k: torch.from_numpy(inp_data[k].astype(np.float32)) + .unsqueeze(0) + .repeat_interleave(self.history, dim=0) + for k in inp_data.keys() + } + out_data = { + k: torch.from_numpy(out_data[k].astype(np.float32)) + for k in out_data.keys() + } + for key in inp_data.keys(): + for t in range(self.history): + inp_data[key][t] = inp_data[key][t].roll(-t * self.window, dims=0) + + last_idx = -((self.history - 1) * self.window + self.max_pred_range) + + inp_data = { + k: inp_data[k][:, :last_idx].transpose(0, 1) + for k in inp_data.keys() # N, T, H, W + } + + inp_data_len = inp_data[variables[0]].size(0) + dtype = inp_data[variables[0]].dtype + + if self.random_lead_time: + predict_ranges = torch.randint( + low=self.min_pred_range, + high=self.max_pred_range + 1, + size=(inp_data_len,), + ) + else: + predict_ranges = ( + torch.ones(inp_data_len).to(torch.long) * self.max_pred_range + ) + lead_times = self.hrs_each_step * predict_ranges / 100 + lead_times = lead_times.to(dtype) + output_ids = ( + torch.arange(inp_data_len) + + (self.history - 1) * self.window + + predict_ranges + ) + + out_data = {k: out_data[k][output_ids] for k in out_data.keys()} + yield inp_data, out_data, lead_times, variables, out_variables + + +class IndividualDataIter(IterableDataset): + def __init__( + self, + dataset: Forecast, + transforms: torch.nn.Module, + output_transforms: torch.nn.Module, + subsample: int = 6, + ): + super().__init__() + self.dataset = dataset + self.transforms = transforms + self.output_transforms = output_transforms + self.subsample = subsample + + def __iter__(self): + for inp, out, lead_times, variables, out_variables in self.dataset: + inp_shapes = set([inp[k].shape[0] for k in inp.keys()]) + out_shapes = set([out[k].shape[0] for k in out.keys()]) + assert len(inp_shapes) == 1 + assert len(out_shapes) == 1 + inp_len = next(iter(inp_shapes)) + out_len = next(iter(out_shapes)) + assert inp_len == out_len + for i in range(0, inp_len, self.subsample): + x = {k: inp[k][i] for k in inp.keys()} + y = {k: out[k][i] for k in out.keys()} + if self.transforms is not None: + x = { + k: self.transforms[k](x[k].unsqueeze(1)).squeeze(1) + for k in x.keys() + } + if self.output_transforms is not None: + y = { + k: self.output_transforms[k](y[k].unsqueeze(0)).squeeze(0) + for k in y.keys() + } + yield x, y, lead_times[i], variables, out_variables + + +class ShuffleIterableDataset(IterableDataset): + def __init__(self, dataset: IndividualDataIter, buffer_size: int) -> None: + super().__init__() + assert buffer_size > 0 + self.dataset = dataset + self.buffer_size = buffer_size + + def __iter__(self): + buf = [] + for x in self.dataset: + if len(buf) == self.buffer_size: + idx = random.randint(0, self.buffer_size - 1) + yield buf[idx] + buf[idx] = x + else: + buf.append(x) + random.shuffle(buf) + while buf: + yield buf.pop() diff --git a/src/climate_learn/data/climate_dataset/era5_iterdataset.py b/src/climate_learn/data/climate_dataset/era5_iterdataset.py index eeebb54d..40214584 100644 --- a/src/climate_learn/data/climate_dataset/era5_iterdataset.py +++ b/src/climate_learn/data/climate_dataset/era5_iterdataset.py @@ -59,7 +59,7 @@ def __iter__(self): world_size = torch.distributed.get_world_size() num_workers_per_ddp = worker_info.num_workers num_shards = num_workers_per_ddp * world_size - per_worker = int(math.floor(n_files / float(num_shards))) + per_worker = n_files // num_shards worker_id = rank * num_workers_per_ddp + worker_info.id iter_start = worker_id * per_worker iter_end = iter_start + per_worker @@ -72,8 +72,8 @@ def __iter__(self): out = inp else: out = np.load(path_out) - yield {k: inp[k] for k in self.variables}, { - k: out[k] for k in self.out_variables + yield {k: np.squeeze(inp[k], axis=1) for k in self.variables}, { + k: np.squeeze(out[k], axis=1) for k in self.out_variables }, self.variables, self.out_variables @@ -89,34 +89,37 @@ def __init__( def __iter__(self): for inp_data, out_data, variables, out_variables in self.dataset: - x = np.concatenate( - [inp_data[k].astype(np.float32) for k in inp_data.keys()], axis=1 - ) - x = torch.from_numpy(x) - y = np.concatenate( - [out_data[k].astype(np.float32) for k in out_data.keys()], axis=1 - ) - y = torch.from_numpy(y) - - inputs = x.unsqueeze(0).repeat_interleave(self.history, dim=0) - for t in range(self.history): - inputs[t] = inputs[t].roll(-t * self.window, dims=0) + inp_data = { + k: torch.from_numpy(inp_data[k].astype(np.float32)) + .unsqueeze(0) + .repeat_interleave(self.history, dim=0) + for k in inp_data.keys() + } + out_data = { + k: torch.from_numpy(out_data[k].astype(np.float32)) + for k in out_data.keys() + } + for key in inp_data.keys(): + for t in range(self.history): + inp_data[key][t] = inp_data[key][t].roll(-t * self.window, dims=0) last_idx = -((self.history - 1) * self.window + self.pred_range) - inputs = inputs[:, :last_idx].transpose(0, 1) # N, T, C, H, W + inp_data = { + k: inp_data[k][:, :last_idx].transpose(0, 1) + for k in inp_data.keys() # N, T, H, W + } - predict_ranges = ( - torch.ones(inputs.shape[0]).to(torch.long) * self.pred_range - ) + inp_data_len = inp_data[variables[0]].size(0) + + predict_ranges = torch.ones(inp_data_len).to(torch.long) * self.pred_range output_ids = ( - torch.arange(inputs.shape[0]) + torch.arange(inp_data_len) + (self.history - 1) * self.window + predict_ranges ) - outputs = y[output_ids] - - yield inputs, outputs, variables, out_variables + out_data = {k: out_data[k][output_ids] for k in out_data.keys()} + yield inp_data, out_data, variables, out_variables class Downscale(IterableDataset): @@ -126,16 +129,15 @@ def __init__(self, dataset: NpyReader) -> None: def __iter__(self): for inp_data, out_data, variables, out_variables in self.dataset: - x = np.concatenate( - [inp_data[k].astype(np.float32) for k in inp_data.keys()], axis=1 - ) - x = torch.from_numpy(x) - y = np.concatenate( - [out_data[k].astype(np.float32) for k in out_data.keys()], axis=1 - ) - y = torch.from_numpy(y) - - yield x, y, variables, out_variables + inp_data = { + k: torch.from_numpy(inp_data[k].astype(np.float32)) + for k in inp_data.keys() + } + out_data = { + k: torch.from_numpy(out_data[k].astype(np.float32)) + for k in out_data.keys() + } + yield inp_data, out_data, variables, out_variables class IndividualDataIter(IterableDataset): @@ -144,22 +146,45 @@ def __init__( dataset: Union[Forecast, Downscale], transforms: torch.nn.Module, output_transforms: torch.nn.Module, + subsample: int = 6, ): super().__init__() self.dataset = dataset self.transforms = transforms self.output_transforms = output_transforms + self.subsample = subsample def __iter__(self): for inp, out, variables, out_variables in self.dataset: - assert inp.shape[0] == out.shape[0] - for i in range(inp.shape[0]): + inp_shapes = set([inp[k].shape[0] for k in inp.keys()]) + out_shapes = set([out[k].shape[0] for k in out.keys()]) + assert len(inp_shapes) == 1 + assert len(out_shapes) == 1 + inp_len = next(iter(inp_shapes)) + out_len = next(iter(out_shapes)) + assert inp_len == out_len + for i in range(0, inp_len, self.subsample): + x = {k: inp[k][i] for k in inp.keys()} + y = {k: out[k][i] for k in out.keys()} if self.transforms is not None: - yield self.transforms(inp[i]), self.output_transforms( - out[i] - ), variables, out_variables - else: - yield inp[i], out[i], variables, out_variables + if isinstance(self.dataset, Forecast): + x = { + k: self.transforms[k](x[k].unsqueeze(1)).squeeze(1) + for k in x.keys() + } + elif isinstance(self.dataset, Downscale): + x = { + k: self.transforms[k](x[k].unsqueeze(0)).squeeze(0) + for k in x.keys() + } + else: + raise RuntimeError(f"Not supported task.") + if self.output_transforms is not None: + y = { + k: self.output_transforms[k](y[k].unsqueeze(0)).squeeze(0) + for k in y.keys() + } + yield x, y, variables, out_variables class ShuffleIterableDataset(IterableDataset): diff --git a/src/climate_learn/data/climatebench_dataset.py b/src/climate_learn/data/climatebench_dataset.py new file mode 100644 index 00000000..96729881 --- /dev/null +++ b/src/climate_learn/data/climatebench_dataset.py @@ -0,0 +1,187 @@ +import os +from typing import Dict + +import numpy as np +import torch +import xarray as xr +from torch.utils.data import Dataset +from torchvision.transforms import transforms + + +def load_x_y(data_path, list_simu, out_var): + x_all, y_all = {}, {} + for simu in list_simu: + input_name = "inputs_" + simu + ".nc" + output_name = "outputs_" + simu + ".nc" + if "hist" in simu: + # load inputs + input_xr = xr.open_dataset(os.path.join(data_path, input_name)) + + # load outputs + output_xr = xr.open_dataset(os.path.join(data_path, output_name)).mean( + dim="member" + ) + output_xr = ( + output_xr.assign( + {"pr": output_xr.pr * 86400, "pr90": output_xr.pr90 * 86400} + ) + .rename({"lon": "longitude", "lat": "latitude"}) + .transpose("time", "latitude", "longitude") + .drop(["quantile"]) + ) + + # Concatenate with historical data in the case of scenario 'ssp126', 'ssp370' and 'ssp585' + else: + # load inputs + input_xr = xr.open_mfdataset( + [ + os.path.join(data_path, "inputs_historical.nc"), + os.path.join(data_path, input_name), + ] + ).compute() + + # load outputs + output_xr = xr.concat( + [ + xr.open_dataset( + os.path.join(data_path, "outputs_historical.nc") + ).mean(dim="member"), + xr.open_dataset(os.path.join(data_path, output_name)).mean( + dim="member" + ), + ], + dim="time", + ).compute() + output_xr = ( + output_xr.assign( + {"pr": output_xr.pr * 86400, "pr90": output_xr.pr90 * 86400} + ) + .rename({"lon": "longitude", "lat": "latitude"}) + .transpose("time", "latitude", "longitude") + .drop(["quantile"]) + ) + + print(input_xr.dims, output_xr.dims, simu) + + x = input_xr.to_array().to_numpy() + x = x.transpose(1, 0, 2, 3).astype(np.float32) # N, C, H, W + x_all[simu] = x + + y = output_xr[out_var].to_array().to_numpy() # 1, N, H, W + # y = np.expand_dims(y, axis=1) # N, 1, H, W + y = y.transpose(1, 0, 2, 3).astype(np.float32) + y_all[simu] = y + + temp = xr.open_dataset( + os.path.join(data_path, "inputs_" + list_simu[0] + ".nc") + ).compute() + if "latitude" in temp: + lat = np.array(temp["latitude"]) + lon = np.array(temp["longitude"]) + else: + lat = np.array(temp["lat"]) + lon = np.array(temp["lon"]) + + return x_all, y_all, lat, lon + + +def input_for_training(x, skip_historical, history, len_historical): + time_length = x.shape[0] + # If we skip historical data, the first sequence created has as last element the first scenario data point + if skip_historical: + X_train_to_return = np.array( + [ + x[i : i + history] + for i in range(len_historical - history + 1, time_length - history + 1) + ] + ) + # Else we just go through the whole dataset historical + scenario (does not matter in the case of 'hist-GHG' and 'hist_aer') + else: + X_train_to_return = np.array( + [x[i : i + history] for i in range(0, time_length - history + 1)] + ) + + return X_train_to_return + + +def output_for_training(y, skip_historical, history, len_historical): + time_length = y.shape[0] + # If we skip historical data, the first sequence created has as target element the first scenario data point + if skip_historical: + Y_train_to_return = np.array( + [ + y[i + history - 1] + for i in range(len_historical - history + 1, time_length - history + 1) + ] + ) + # Else we just go through the whole dataset historical + scenario (does not matter in the case of 'hist-GHG' and 'hist_aer') + else: + Y_train_to_return = np.array( + [y[i + history - 1] for i in range(0, time_length - history + 1)] + ) + + return Y_train_to_return + + +def split_train_val(x, y, train_ratio=0.9): + shuffled_ids = np.random.permutation(x.shape[0]) + train_len = int(train_ratio * x.shape[0]) + train_ids = shuffled_ids[:train_len] + val_ids = shuffled_ids[train_len:] + return x[train_ids], y[train_ids], x[val_ids], y[val_ids] + + +class ClimateBenchDataset(Dataset): + def __init__( + self, X_train_all, Y_train_all, variables, out_variables, lat, partition="train" + ): + super().__init__() + self.X_train_all = X_train_all + self.Y_train_all = Y_train_all + self.len_historical = 165 + self.variables = variables + self.out_variables = out_variables + self.lat = lat + self.partition = partition + + if partition == "train": + self.inp_transform = self.get_normalize(self.X_train_all) + # self.out_transform = self.get_normalize(self.Y_train_all) + self.out_transform = transforms.Normalize(np.array([0.0]), np.array([1.0])) + else: + self.inp_transform = None + self.out_transform = None + + if partition == "test": + # only use 2080 - 2100 according to ClimateBench + self.X_train_all = self.X_train_all[-21:] + self.Y_train_all = self.Y_train_all[-21:] + self.get_rmse_normalization() + + def get_normalize(self, data): + mean = np.mean(data, axis=(0, 1, 3, 4)) + std = np.std(data, axis=(0, 1, 3, 4)) + return transforms.Normalize(mean, std) + + def set_normalize(self, inp_normalize, out_normalize): # for val and test + self.inp_transform = inp_normalize + self.out_transform = out_normalize + + def get_rmse_normalization(self): + y_avg = torch.from_numpy(self.Y_train_all).squeeze(1).mean(0) # H, W + w_lat = np.cos(np.deg2rad(self.lat)) # (H,) + w_lat = w_lat / w_lat.mean() + w_lat = ( + torch.from_numpy(w_lat) + .unsqueeze(-1) + .to(dtype=y_avg.dtype, device=y_avg.device) + ) # (H, 1) + self.y_normalization = torch.abs(torch.mean(y_avg * w_lat)) + + def __len__(self): + return self.X_train_all.shape[0] + + def __getitem__(self, index): + inp = self.inp_transform(torch.from_numpy(self.X_train_all[index])) + out = self.out_transform(torch.from_numpy(self.Y_train_all[index])) + return inp, out, self.variables, self.out_variables diff --git a/src/climate_learn/data/climatebench_module.py b/src/climate_learn/data/climatebench_module.py new file mode 100644 index 00000000..6f85bd72 --- /dev/null +++ b/src/climate_learn/data/climatebench_module.py @@ -0,0 +1,171 @@ +import os + +import numpy as np +import torch +import torch.nn.functional as F +import pytorch_lightning as pl +from torch.utils.data import DataLoader + +from climate_learn.data.climatebench_dataset import ( + ClimateBenchDataset, + input_for_training, + load_x_y, + output_for_training, + split_train_val, +) + + +def collate_climate_fn(batch): + inp = torch.stack([batch[i][0] for i in range(len(batch))]) + out = torch.stack([batch[i][1] for i in range(len(batch))]) + variables = batch[0][2] + out_variables = batch[0][3] + return ( + inp, + out, + variables, + out_variables, + ) + + +class ClimateBenchDataModule(pl.LightningDataModule): + def __init__( + self, + root_dir, # contains metadata and train + val + test + history=10, + list_train_simu=[ + "ssp126", + "ssp370", + "ssp585", + "historical", + "hist-GHG", + "hist-aer", + ], + list_test_simu=["ssp245"], + variables=["CO2", "SO2", "CH4", "BC"], + out_variables="tas", + train_ratio=0.9, + batch_size: int = 128, + num_workers: int = 1, + pin_memory: bool = False, + ): + super().__init__() + + # this line allows to access init params with 'self.hparams' attribute + self.save_hyperparameters(logger=False) + + if isinstance(out_variables, str): + out_variables = [out_variables] + self.hparams.out_variables = out_variables + + # split train and val datasets + dict_x_train_val, dict_y_train_val, lat, lon = load_x_y( + os.path.join(root_dir, "train_val"), list_train_simu, out_variables + ) + self.lat, self.lon = lat, lon + x_train_val = np.concatenate( + [ + input_for_training( + dict_x_train_val[simu], + skip_historical=(i < 2), + history=history, + len_historical=165, + ) + for i, simu in enumerate(dict_x_train_val.keys()) + ], + axis=0, + ) # N, T, C, H, W + y_train_val = np.concatenate( + [ + output_for_training( + dict_y_train_val[simu], + skip_historical=(i < 2), + history=history, + len_historical=165, + ) + for i, simu in enumerate(dict_y_train_val.keys()) + ], + axis=0, + ) # N, 1, H, W + x_train, y_train, x_val, y_val = split_train_val( + x_train_val, y_train_val, train_ratio + ) + + self.dataset_train = ClimateBenchDataset( + x_train, y_train, variables, out_variables, lat, "train" + ) + self.dataset_val = ClimateBenchDataset( + x_val, y_val, variables, out_variables, lat, "val" + ) + self.dataset_val.set_normalize( + self.dataset_train.inp_transform, self.dataset_train.out_transform + ) + + dict_x_test, dict_y_test, _, _ = load_x_y( + os.path.join(root_dir, "test"), list_test_simu, out_variables + ) + x_test = input_for_training( + dict_x_test[list_test_simu[0]], + skip_historical=True, + history=history, + len_historical=165, + ) + y_test = output_for_training( + dict_y_test[list_test_simu[0]], + skip_historical=True, + history=history, + len_historical=165, + ) + self.dataset_test = ClimateBenchDataset( + x_test, y_test, variables, out_variables, lat, "test" + ) + self.dataset_test.set_normalize( + self.dataset_train.inp_transform, self.dataset_train.out_transform + ) + + def get_lat_lon(self): + return self.lat, self.lon + + def get_data_dims(self): + x, y = self.train_dataset[0] + y = F.pad(y, (2, 2, 3, 3)) + return x.unsqueeze(0).shape, y.unsqueeze(0).shape + + def get_climatology(self, split="test"): + return {self.hparams.out_variables[0]: self.dataset_test.y_normalization} + + def get_data_variables(self): + return self.hparams.variables, self.hparams.out_variables + + def train_dataloader(self): + return DataLoader( + self.dataset_train, + batch_size=self.hparams.batch_size, + shuffle=True, + # drop_last=True, + num_workers=self.hparams.num_workers, + pin_memory=self.hparams.pin_memory, + collate_fn=collate_climate_fn, + ) + + def val_dataloader(self): + return DataLoader( + self.dataset_val, + batch_size=self.hparams.batch_size, + shuffle=False, + # drop_last=True, + num_workers=self.hparams.num_workers, + pin_memory=self.hparams.pin_memory, + collate_fn=collate_climate_fn, + ) + + def test_dataloader(self): + return DataLoader( + self.dataset_test, + batch_size=self.hparams.batch_size, + shuffle=False, + # drop_last=True, + num_workers=self.hparams.num_workers, + pin_memory=self.hparams.pin_memory, + collate_fn=collate_climate_fn, + ) diff --git a/src/climate_learn/data/download.py b/src/climate_learn/data/download.py index 4f62d215..e2a62fbe 100644 --- a/src/climate_learn/data/download.py +++ b/src/climate_learn/data/download.py @@ -1,274 +1,154 @@ # Standard library -import argparse +from argparse import ArgumentParser +from ftplib import FTP import os -import subprocess +import re +import requests +from zipfile import ZipFile # Third party import cdsapi +from tqdm import tqdm, trange -# Local application -from .climate_dataset.era5.constants import NAME_TO_CMIP - - -months = [str(i).rjust(2, "0") for i in range(1, 13)] -days = [str(i).rjust(2, "0") for i in range(1, 32)] -times = [str(i).rjust(2, "0") + ":00" for i in range(0, 24)] - -# TODO: write exceptions in the docstrings -# TODO: figure out how to better specify legal args for dataset, variable, -# and resolution -# TODO: for download ESGF, do we have to download all the years? -# TODO: can main even be run without runtime warning? maybe we should get rid of it - - -def _download_copernicus(root, dataset, variable, year, pressure=False, api_key=None): - """Downloads data from the Copernicus Climate Data Store (CDS). - Data is stored at `root/dataset/variable/` as NetCDF4 (`.nc`) files. - Skips the download if a file of the expected naming convention already - exists at the download destination. More info: - https://cds.climate.copernicus.eu/cdsapp#!/home - - :param root: The root data directory. - :type root: str - :param dataset: The dataset to download. Currently, only "era5" is - supported. - :type dataset: str - :param variable: The variable to download from the specified dataset. - :type variable: str - :param pressure: Whether to download data from different pressure levels - instead of single-level. Defaults to `False`. - :type pressure: bool, optional - :param api_key: An API key for accessing CDS. Defaults to `None`. See here - for more info: https://cds.climate.copernicus.eu/api-how-to. - :type api_key: str, optional - """ - if dataset not in ["era5"]: - raise Exception("Dataset not supported") +def download_copernicus_era5(dst, variable, year, pressure=False, api_key=None): if api_key is not None: content = f"url: https://cds.climate.copernicus.eu/api/v2\nkey: {api_key}" - open(f"{os.environ['HOME']}/.cdsapirc", "w").write(content) - - path = os.path.join(root, dataset, variable, f"{variable}_{year}_0.25deg.nc") - print( - f"Downloading {dataset} {variable} data for year {year} from copernicus to {path}" - ) - - if os.path.exists(path): - return - - os.makedirs(os.path.dirname(path), exist_ok=True) - + home_dir = os.environ["HOME"] + with open(os.path.join(home_dir, ".cdsapirc"), "w") as f: + f.write(content) + os.makedirs(dst, exist_ok=True) + client = cdsapi.Client() download_args = { "product_type": "reanalysis", "format": "netcdf", "variable": variable, "year": str(year), - "month": months, - "day": days, - "time": times, + "month": [str(i).rjust(2, "0") for i in range(1, 13)], + "day": [str(i).rjust(2, "0") for i in range(1, 32)], + "time": [str(i).rjust(2, "0") + ":00" for i in range(0, 24)], } - - client = cdsapi.Client() - - if not pressure: - client.retrieve( - "reanalysis-era5-single-levels", - download_args, - path, - ) - else: + if pressure: + src = "reanalysis-era5-pressure-levels" download_args["pressure_level"] = [1000, 850, 500, 50] - client.retrieve( - "reanalysis-era5-pressure-levels", - download_args, - path, - ) - - -def _download_esgf( - root, - dataset, - variable, - institutionID="MPI-M", - sourceID="MPI-ESM1-2-HR", - exprID="historical", -): - """Downloads data from the Earth System Grid Federation (ESGF). - Data is stored at `root/dataset/pre-regrided/variable/` as a NetCDF4 - (`.nc`) file. Skips the download if a file of the expected naming - convention already exists at the download destination. More info: - https://esgf-node.llnl.gov/projects/cmip6/ - - :param root: The root data directory. - :type root: str - :param dataset: The dataset to download. Currently, only "cmip6" is - supported. - :type dataset: str - :param variable: The variable to download from the specified dataset. - :type variable: str - :param instituionID: TODO - :type institutionID: str, optional - :param sourceID: TODO - :type sourceID: str, optional - :param exprID: TODO - :type exprID: str, optional - """ - if dataset not in ["cmip6"]: - raise Exception("Dataset not supported") - - path = os.path.join(root, dataset, "pre-regrided", variable) - print(f"Downloading {dataset} {variable} data from esgf to {path}") - - os.makedirs(os.path.dirname(path), exist_ok=True) - - year_strings = [f"{y}01010600-{y+5}01010000" for y in range(1850, 2015, 5)] - for yr in year_strings: - file_name = ("{var}_6hrPlevPt_{sourceID}_{exprID}_r1i1p1f1_gn_{yr}.nc").format( - var=NAME_TO_CMIP[variable], yr=yr, sourceID=sourceID, exprID=exprID + else: + src = "reanalysis-era5-single-levels" + client.retrieve(src, download_args, dst / f"{variable}_{year}_0.25deg.nc") + + +def download_mpi_esm1_2_hr(dst, variable, years=(1850, 2015)): + os.makedirs(dst, exist_ok=True) + year_strings = [f"{y}01010600-{y+5}01010000" for y in range(*years, 5)] + inst = "MPI-M" + src = "MPI-ESM1-2-HR" + exp = "historical" + for yr in tqdm(year_strings): + remote_fn = f"{variable}_6hrPlevPt_{src}_{exp}_r1i1p1f1_gn_{yr}.nc" + url = ( + "https://esgf-data1.llnl.gov/thredds/fileServer/css03_data/CMIP6/" + f"CMIP/{inst}/{src}/{exp}/r1i1p1f1/6hrPlevPt/{variable}/gn/" + f"v20190815/{remote_fn}" ) + resp = requests.get(url, verify=False, stream=True) + local_fn = os.path.join(dst, remote_fn) + with open(local_fn, "wb") as file: + for chunk in resp.iter_content(chunk_size=1024): + file.write(chunk) - file_path = os.path.join(path, file_name) - if os.path.exists(file_path): - print(file_name, "exists") - - else: - url = ( - "https://esgf-data1.llnl.gov/thredds/fileServer/css03_data/CMIP6/CMIP/{institutionID}/{sourceID}/{exprID}/r1i1p1f1/6hrPlevPt/" - "{variable}/gn/v20190815/{file}" - ).format( - yr_string=yr, - variable=NAME_TO_CMIP[variable], - file=file_name, - institutionID=institutionID, - sourceID=sourceID, - exprID=exprID, - ) - subprocess.check_call(["wget", "--no-check-certificate", url, "-P", path]) - -def _download_weatherbench(root, dataset, variable, resolution="1.40625"): - """Downloads data from WeatherBench. - Data is stored at `root/dataset/resolution/variable/` as NetCDF4 - (`.nc`) files. Skips the download if a file of the expected naming - convention already exists at the download destination. More info: - https://mediatum.ub.tum.de/1524895 - - :param root: The root data directory - :type root: str - :param dataset: The dataset to download. Currently, "era5" and "cmip6" are - supported. - :type dataset: str - :param variable: The variable to download from the specified dataset. - :type variable: str - :param resolution: The desired data resolution in degrees. Can be - "1.40625", "2.8125", and "5.625". Default is "1.40625". - :type resolution: str, optional - """ +def download_weatherbench(dst, dataset, variable, resolution=5.625): + os.makedirs(dst, exist_ok=True) if dataset not in ["era5", "cmip6"]: - raise Exception("Dataset not supported") - - path = os.path.join(root, dataset, resolution, variable) - print( - f"Downloading {dataset} {variable} data for {resolution} resolution from weatherbench to {path}" - ) - if os.path.exists(path): - return - os.makedirs(os.path.dirname(path), exist_ok=True) - + raise RuntimeError("Dataset not supported") + url = "https://dataserv.ub.tum.de/s/m1524895/download?path=%2F" + res = f"{resolution}deg" if dataset == "era5": - if variable != "constants": - url = ( - "https://dataserv.ub.tum.de/s/m1524895" - "/download?path=%2F{resolution}deg%2F{variable}&files={variable}_{resolution}deg.zip" - ).format(resolution=resolution, variable=variable) - elif variable == "constants": - url = ( - "https://dataserv.ub.tum.de/s/m1524895" - "/download?path=%2F{resolution}deg%2Fconstants&files=constants_{resolution}deg.nc" - ).format(resolution=resolution) + ext = ".nc" if variable == "constants" else ".zip" + remote_fn = f"{variable}_{res}{ext}" + url = f"{url}{res}%2F{variable}&files={remote_fn}" elif dataset == "cmip6": - url = ( - "https://dataserv.ub.tum.de/s/m1524895" - "/download?path=%2FCMIP%2FMPI-ESM%2F{resolution}deg%2F{variable}&files={variable}_{resolution}deg.zip" - ).format(resolution=resolution, variable=variable) - - if variable != "constants": - subprocess.check_call( - ["wget", "--no-check-certificate", url, "-O", path + ".zip"] - ) - subprocess.check_call(["unzip", path + ".zip", "-d", path]) + ext = ".zip" + remote_fn = f"{variable}_{res}{ext}" + url = f"{url}CMIP%2FMPI-ESM%2F{res}%2F{variable}&files={remote_fn}" + resp = requests.get(url, verify=False, stream=True) + if variable == "constants": + local_fn = os.path.join(dst, "constants.nc") else: - subprocess.check_call(["mkdir", path]) - subprocess.check_call( - [ - "wget", - "--no-check-certificate", - url, - "-O", - os.path.join(path, "constants.nc"), - ] - ) - - -def download(source, **kwargs): - r"""Download interface. - - :param source: The data source to download from: "copernicus", - "weatherbench", or "esgf". - :param type: str - :param \**kwargs: arguments to pass to the source-specific download - function: :py:func:`_download_copernicus`, - :py:func:`_download_weatherbench`, :py:func:`_download_esgf` - """ - - # TODO: this was appropriate for the Colab tutorial, but should we - # keep it for future releases? - if "root" not in kwargs or kwargs["root"] is None: - kwargs["root"] = ".climate_tutorial" - - kwargs["root"] = os.path.join(kwargs["root"], f"data/{source}") - - if source == "copernicus": - _download_copernicus(**kwargs) - elif source == "weatherbench": - _download_weatherbench(**kwargs) - elif source == "esgf": - _download_esgf(**kwargs) + local_fn = os.path.join(dst, remote_fn) + # TODO: add a progress wheel to indicate it is running. + # I don't think a progress bar with tqdm is doable since the total size + # of the file is not known a priori. + with open(local_fn, "wb") as file: + for chunk in resp.iter_content(chunk_size=1024): + file.write(chunk) + if ext == ".zip": + with ZipFile(local_fn) as myzip: + myzip.extractall(dst) + os.unlink(local_fn) + + +def download_prism(dst, variable, years=(1981, 2023)): + os.makedirs(dst, exist_ok=True) + ftp = FTP("prism.oregonstate.edu") + ftp.login() + for year in trange(*years): + ftp.cwd(f"/daily/{variable}/{year}") + for remote_fn in tqdm(ftp.nlst(), leave=False): + local_fn = os.path.join(dst, remote_fn) + with open(local_fn, "wb") as file: + ftp.retrbinary(f"RETR {remote_fn}", file.write) + subdir_name = re.search(r"\d{8}", remote_fn)[0] + subdir_path = os.path.join(dst, subdir_name) + os.mkdir(subdir_path) + with ZipFile(local_fn) as myzip: + myzip.extractall(path=subdir_path) + os.unlink(local_fn) + ftp.quit() def main(): - parser = argparse.ArgumentParser() - - subparsers = parser.add_subparsers(dest="source") - - subparser = subparsers.add_parser("copernicus") - subparser.add_argument("--root", type=str, default=None) - subparser.add_argument("--variable", type=str, required=True) - subparser.add_argument("--dataset", type=str, choices=["era5"], required=True) - subparser.add_argument("--year", type=int, required=True) - subparser.add_argument("--pressure", action="store_true", default=False) - subparser.add_argument("--api_key", type=str, default=None) - - subparser = subparsers.add_parser("weatherbench") - subparser.add_argument("--root", type=str, default=None) - subparser.add_argument("--variable", type=str, required=True) - subparser.add_argument( - "--dataset", type=str, choices=["era5", "cmip6"], required=True + parser = ArgumentParser(description="ClimateLearn's download utility.") + subparsers = parser.add_subparsers( + help="Data provider to download from.", dest="provider" ) - subparser.add_argument("--resolution", type=str, default="5.625") - - subparser = subparsers.add_parser("esgf") - subparser.add_argument("--root", type=str, default=None) - subparser.add_argument("--variable", type=str, required=True) - subparser.add_argument("--dataset", type=str, choices=["era5"], required=True) - subparser.add_argument("--resolution", type=str, default="5.625") - subparser.add_argument("--institutionID", type=str, default="MPI-M") - subparser.add_argument("--sourceID", type=str, default="MPI-ESM1-2-HR") - subparser.add_argument("--exprID", type=str, default="historical") + copernicus_era5 = subparsers.add_parser("copernicus-era5") + mpi_esm1_2_hr = subparsers.add_parser("mpi_esm1_2_hr") + weatherbench = subparsers.add_parser("weatherbench") + prism = subparsers.add_parser("prism") + + copernicus_era5.add_argument("dst", help="Destination to store downloaded files.") + copernicus_era5.add_argument("var", help="Variable to download.") + copernicus_era5.add_argument("year", type=int) + copernicus_era5.add_argument("--pressure", action="store_true", default=False) + copernicus_era5.add_argument("--api_key") + + mpi_esm1_2_hr.add_argument("dst", help="Destination to store downloaded files.") + mpi_esm1_2_hr.add_argument("var", help="Variable to download.") + mpi_esm1_2_hr.add_argument("--start", type=int, default=1850) + mpi_esm1_2_hr.add_argument("--end", type=int, default=2015) + + weatherbench.add_argument("dst", help="Destination to store downloaded files.") + weatherbench.add_argument("dataset", choices=["era5", "cmip6"]) + weatherbench.add_argument("var") + weatherbench.add_argument("--res", type=float, default=5.625) + + prism.add_argument("dst", help="Destination to store downloaded files.") + prism.add_argument("var", help="Variable to download.") + prism.add_argument("--start", type=int, default=1981) + prism.add_argument("--end", type=int, default=2023) args = parser.parse_args() - download(**vars(args)) + + if args.provider == "copernicus_era5": + download_copernicus_era5( + args.dst, args.var, args.year, args.pressure, args.api_key + ) + elif args.provider == "mpi_esm1_2_hr": + download_mpi_esm1_2_hr(args.dst, args.var, (args.start, args.end)) + elif args.provider == "weatherbench": + download_weatherbench(args.dst, args.var, args.res) + elif args.provider == "prism": + download_prism(args.dst, args.var, (args.start, args.end)) if __name__ == "__main__": diff --git a/src/climate_learn/data/iterdataset.py b/src/climate_learn/data/iterdataset.py new file mode 100644 index 00000000..6430962c --- /dev/null +++ b/src/climate_learn/data/iterdataset.py @@ -0,0 +1,287 @@ +# Standard library +import random + +# Third party +import numpy as np +import torch +from torch.utils.data import IterableDataset + + +def shuffle_two_list(list1, list2): + list1_shuf = [] + list2_shuf = [] + index_shuf = list(range(len(list1))) + random.shuffle(index_shuf) + for i in index_shuf: + list1_shuf.append(list1[i]) + list2_shuf.append(list2[i]) + return list1_shuf, list2_shuf + + +class NpyReader(IterableDataset): + def __init__( + self, + inp_file_list, + out_file_list, + variables, + out_variables, + shuffle=False, + ): + super().__init__() + assert len(inp_file_list) == len(out_file_list) + self.inp_file_list = [f for f in inp_file_list if "climatology" not in f] + self.out_file_list = [f for f in out_file_list if "climatology" not in f] + self.variables = variables + self.out_variables = out_variables if out_variables is not None else variables + self.shuffle = shuffle + + def __iter__(self): + if self.shuffle: + self.inp_file_list, self.out_file_list = shuffle_two_list( + self.inp_file_list, self.out_file_list + ) + + n_files = len(self.inp_file_list) + + worker_info = torch.utils.data.get_worker_info() + if worker_info is None: + iter_start = 0 + iter_end = n_files + else: + if not torch.distributed.is_initialized(): + rank = 0 + world_size = 1 + else: + rank = torch.distributed.get_rank() + world_size = torch.distributed.get_world_size() + num_workers_per_ddp = worker_info.num_workers + num_shards = num_workers_per_ddp * world_size + per_worker = n_files // num_shards + worker_id = rank * num_workers_per_ddp + worker_info.id + iter_start = worker_id * per_worker + iter_end = iter_start + per_worker + + for idx in range(iter_start, iter_end): + path_inp = self.inp_file_list[idx] + path_out = self.out_file_list[idx] + inp = np.load(path_inp) + if path_out == path_inp: + out = inp + else: + out = np.load(path_out) + yield {k: np.squeeze(inp[k], axis=1) for k in self.variables}, { + k: np.squeeze(out[k], axis=1) for k in self.out_variables + }, self.variables, self.out_variables + + +class DirectForecast(IterableDataset): + def __init__(self, dataset, src, pred_range=6, history=3, window=6): + super().__init__() + self.dataset = dataset + self.history = history + if src == "era5": + self.pred_range = pred_range + self.window = window + elif src == "mpi-esm1-2-hr": + assert pred_range % 6 == 0 + assert window % 6 == 0 + self.pred_range = pred_range // 6 + self.window = window // 6 + + def __iter__(self): + for inp_data, out_data, variables, out_variables in self.dataset: + inp_data = { + k: torch.from_numpy(inp_data[k].astype(np.float32)) + .unsqueeze(0) + .repeat_interleave(self.history, dim=0) + for k in inp_data.keys() + } + out_data = { + k: torch.from_numpy(out_data[k].astype(np.float32)) + for k in out_data.keys() + } + for key in inp_data.keys(): + for t in range(self.history): + inp_data[key][t] = inp_data[key][t].roll(-t * self.window, dims=0) + + last_idx = -((self.history - 1) * self.window + self.pred_range) + + inp_data = { + k: inp_data[k][:, :last_idx].transpose(0, 1) + for k in inp_data.keys() # N, T, H, W + } + + inp_data_len = inp_data[variables[0]].size(0) + + predict_ranges = torch.ones(inp_data_len).to(torch.long) * self.pred_range + output_ids = ( + torch.arange(inp_data_len) + + (self.history - 1) * self.window + + predict_ranges + ) + out_data = {k: out_data[k][output_ids] for k in out_data.keys()} + yield inp_data, out_data, variables, out_variables + + +class ContinuousForecast(IterableDataset): + def __init__( + self, + dataset, + random_lead_time=True, + min_pred_range=6, + max_pred_range=120, + hrs_each_step=1, + history=3, + window=6, + ): + super().__init__() + if not random_lead_time: + assert min_pred_range == max_pred_range + self.dataset = dataset + self.random_lead_time = random_lead_time + self.min_pred_range = min_pred_range + self.max_pred_range = max_pred_range + self.hrs_each_step = hrs_each_step + self.history = history + self.window = window + + def __iter__(self): + for inp_data, out_data, variables, out_variables in self.dataset: + inp_data = { + k: torch.from_numpy(inp_data[k].astype(np.float32)) + .unsqueeze(0) + .repeat_interleave(self.history, dim=0) + for k in inp_data.keys() + } + out_data = { + k: torch.from_numpy(out_data[k].astype(np.float32)) + for k in out_data.keys() + } + for key in inp_data.keys(): + for t in range(self.history): + inp_data[key][t] = inp_data[key][t].roll(-t * self.window, dims=0) + + last_idx = -((self.history - 1) * self.window + self.max_pred_range) + + inp_data = { + k: inp_data[k][:, :last_idx].transpose(0, 1) + for k in inp_data.keys() # N, T, H, W + } + + inp_data_len = inp_data[variables[0]].size(0) + dtype = inp_data[variables[0]].dtype + + if self.random_lead_time: + predict_ranges = torch.randint( + low=self.min_pred_range, + high=self.max_pred_range + 1, + size=(inp_data_len,), + ) + else: + predict_ranges = ( + torch.ones(inp_data_len).to(torch.long) * self.max_pred_range + ) + lead_times = self.hrs_each_step * predict_ranges / 100 + lead_times = lead_times.to(dtype) + output_ids = ( + torch.arange(inp_data_len) + + (self.history - 1) * self.window + + predict_ranges + ) + + out_data = {k: out_data[k][output_ids] for k in out_data.keys()} + yield inp_data, out_data, lead_times, variables, out_variables + + +class Downscale(IterableDataset): + def __init__(self, dataset): + super().__init__() + self.dataset = dataset + + def __iter__(self): + for inp_data, out_data, variables, out_variables in self.dataset: + inp_data = { + k: torch.from_numpy(inp_data[k].astype(np.float32)) + for k in inp_data.keys() + } + out_data = { + k: torch.from_numpy(out_data[k].astype(np.float32)) + for k in out_data.keys() + } + yield inp_data, out_data, variables, out_variables + + +class IndividualDataIter(IterableDataset): + def __init__( + self, + dataset, + transforms, + output_transforms, + subsample=6, + ): + super().__init__() + self.dataset = dataset + self.transforms = transforms + self.output_transforms = output_transforms + self.subsample = subsample + + def __iter__(self): + for sample in self.dataset: + if isinstance(self.dataset, (DirectForecast, Downscale)): + inp, out, variables, out_variables = sample + elif isinstance(self.dataset, ContinuousForecast): + inp, out, lead_times, variables, out_variables = sample + inp_shapes = set([inp[k].shape[0] for k in inp.keys()]) + out_shapes = set([out[k].shape[0] for k in out.keys()]) + assert len(inp_shapes) == 1 + assert len(out_shapes) == 1 + inp_len = next(iter(inp_shapes)) + out_len = next(iter(out_shapes)) + assert inp_len == out_len + for i in range(0, inp_len, self.subsample): + x = {k: inp[k][i] for k in inp.keys()} + y = {k: out[k][i] for k in out.keys()} + if self.transforms is not None: + if isinstance(self.dataset, (DirectForecast, ContinuousForecast)): + x = { + k: self.transforms[k](x[k].unsqueeze(1)).squeeze(1) + for k in x.keys() + } + elif isinstance(self.dataset, Downscale): + x = { + k: self.transforms[k](x[k].unsqueeze(0)).squeeze(0) + for k in x.keys() + } + else: + raise RuntimeError(f"Not supported task.") + if self.output_transforms is not None: + y = { + k: self.output_transforms[k](y[k].unsqueeze(0)).squeeze(0) + for k in y.keys() + } + if isinstance(self.dataset, (DirectForecast, Downscale)): + result = x, y, variables, out_variables + elif isinstance(self.dataset, ContinuousForecast): + result = x, y, lead_times[i], variables, out_variables + yield result + + +class ShuffleIterableDataset(IterableDataset): + def __init__(self, dataset, buffer_size): + super().__init__() + assert buffer_size > 0 + self.dataset = dataset + self.buffer_size = buffer_size + + def __iter__(self): + buf = [] + for x in self.dataset: + if len(buf) == self.buffer_size: + idx = random.randint(0, self.buffer_size - 1) + yield buf[idx] + buf[idx] = x + else: + buf.append(x) + random.shuffle(buf) + while buf: + yield buf.pop() diff --git a/src/climate_learn/data/itermodule.py b/src/climate_learn/data/itermodule.py index 6b926ba3..128b76a3 100644 --- a/src/climate_learn/data/itermodule.py +++ b/src/climate_learn/data/itermodule.py @@ -1,23 +1,28 @@ # Standard library +import copy import glob -from typing import Optional +import os +from typing import Dict, Optional # Third party +import numpy as np import torch from torch.utils.data import DataLoader, IterableDataset from torchvision.transforms import transforms -from pytorch_lightning import LightningDataModule +import pytorch_lightning as pl # Local application -from .climate_dataset.era5_iterdataset import * -from ..utils.datetime import Hours -from .module import collate_fn +from .iterdataset import ( + NpyReader, + DirectForecast, + ContinuousForecast, + Downscale, + IndividualDataIter, + ShuffleIterableDataset, +) -# TODO: include exceptions in docstrings -# TODO: document legal input/output variables for each dataset - -class IterDataModule(LightningDataModule): +class IterDataModule(pl.LightningDataModule): """ClimateLearn's iter data module interface. Encapsulates dataset/task-specific data modules.""" @@ -28,54 +33,64 @@ def __init__( out_root_dir, in_vars, out_vars, - history: int = 1, - window: int = 6, - pred_range=Hours(6), - subsample=Hours(1), + src=None, + history=1, + window=6, + pred_range=6, + random_lead_time=True, + max_pred_range=120, + hrs_each_step=1, + subsample=1, buffer_size=10000, batch_size=64, num_workers=0, pin_memory=False, ): - r""" - .. highlight:: python - - :param task: The name of the task. Currently supported options - are: "forecasting", "downscaling". - :type task: str - :param inp_root_dir: The path to the local directory containing the - specified input dataset. - :type inp_root_dir: str - :param out_root_dir: The path to the local directory containing the - specified out dataset. - :type out_root_dir: str - :param in_vars: A list of input variables to use. - :type in_vars: List[str] - :param out_vars: A list of output variables to use. - :type out_vars: List[str] - """ super().__init__() - self.save_hyperparameters(logger=False) - - if task == "forecasting": - assert inp_root_dir == out_root_dir - self.dataset_caller = Forecast + if task in ("direct-forecasting", "iterative-forecasting"): + self.dataset_caller = DirectForecast + self.dataset_arg = { + "src": src, + "pred_range": pred_range, + "history": history, + "window": window, + } + self.collate_fn = collate_fn + elif task == "continuous-forecasting": + self.dataset_caller = ContinuousForecast self.dataset_arg = { - "pred_range": pred_range.hours(), + "random_lead_time": random_lead_time, + "min_pred_range": pred_range, + "max_pred_range": max_pred_range, + "hrs_each_step": hrs_each_step, "history": history, "window": window, } - else: # downscaling + self.collate_fn = collate_fn_continuous + elif task == "downscaling": self.dataset_caller = Downscale self.dataset_arg = {} + self.collate_fn = collate_fn - self.inp_lister_train = glob.glob(os.path.join(inp_root_dir, "train", "*.npz")) - self.out_lister_train = glob.glob(os.path.join(out_root_dir, "train", "*.npz")) - self.inp_lister_val = glob.glob(os.path.join(inp_root_dir, "val", "*.npz")) - self.out_lister_val = glob.glob(os.path.join(out_root_dir, "val", "*.npz")) - self.inp_lister_test = glob.glob(os.path.join(inp_root_dir, "test", "*.npz")) - self.out_lister_test = glob.glob(os.path.join(out_root_dir, "test", "*.npz")) + self.inp_lister_train = sorted( + glob.glob(os.path.join(inp_root_dir, "train", "*.npz")) + ) + self.out_lister_train = sorted( + glob.glob(os.path.join(out_root_dir, "train", "*.npz")) + ) + self.inp_lister_val = sorted( + glob.glob(os.path.join(inp_root_dir, "val", "*.npz")) + ) + self.out_lister_val = sorted( + glob.glob(os.path.join(out_root_dir, "val", "*.npz")) + ) + self.inp_lister_test = sorted( + glob.glob(os.path.join(inp_root_dir, "test", "*.npz")) + ) + self.out_lister_test = sorted( + glob.glob(os.path.join(out_root_dir, "test", "*.npz")) + ) self.transforms = self.get_normalize(inp_root_dir, in_vars) self.output_transforms = self.get_normalize(out_root_dir, out_vars) @@ -89,65 +104,124 @@ def get_lat_lon(self): lon = np.load(os.path.join(self.hparams.out_root_dir, "lon.npy")) return lat, lon + def get_data_variables(self): + out_vars = copy.deepcopy(self.hparams.out_vars) + if "2m_temperature_extreme_mask" in out_vars: + out_vars.remove("2m_temperature_extreme_mask") + return self.hparams.in_vars, out_vars + + def get_data_dims(self): + lat = len(np.load(os.path.join(self.hparams.out_root_dir, "lat.npy"))) + lon = len(np.load(os.path.join(self.hparams.out_root_dir, "lon.npy"))) + forecasting_tasks = [ + "direct-forecasting", + "iterative-forecasting", + "continuous-forecasting", + ] + if self.hparams.task in forecasting_tasks: + in_size = torch.Size( + [ + self.hparams.batch_size, + self.hparams.history, + len(self.hparams.in_vars), + lat, + lon, + ] + ) + elif self.hparams.task == "downscaling": + in_size = torch.Size( + [self.hparams.batch_size, len(self.hparams.in_vars), lat, lon] + ) + ##TODO: change out size + out_vars = copy.deepcopy(self.hparams.out_vars) + if "2m_temperature_extreme_mask" in out_vars: + out_vars.remove("2m_temperature_extreme_mask") + out_size = torch.Size([self.hparams.batch_size, len(out_vars), lat, lon]) + return in_size, out_size + def get_normalize(self, root_dir, variables): normalize_mean = dict(np.load(os.path.join(root_dir, "normalize_mean.npz"))) - mean = [] - for var in variables: - if var != "total_precipitation": - mean.append(normalize_mean[var]) - else: - mean.append(np.array([0.0])) - normalize_mean = np.concatenate(mean) normalize_std = dict(np.load(os.path.join(root_dir, "normalize_std.npz"))) - normalize_std = np.concatenate([normalize_std[var] for var in variables]) - return transforms.Normalize(normalize_mean, normalize_std) + return { + var: transforms.Normalize(normalize_mean[var][0], normalize_std[var][0]) + for var in variables + } def get_out_transforms(self): - return self.output_transforms + out_transforms = {} + for key in self.output_transforms.keys(): + if key == "2m_temperature_extreme_mask": + continue + out_transforms[key] = self.output_transforms[key] + return out_transforms def get_climatology(self, split="val"): path = os.path.join(self.hparams.out_root_dir, split, "climatology.npz") clim_dict = np.load(path) - clim = np.concatenate([clim_dict[var] for var in self.hparams.out_vars]) - clim = torch.from_numpy(clim) - return clim + new_clim_dict = {} + for var in self.hparams.out_vars: + if var == "2m_temperature_extreme_mask": + continue + new_clim_dict[var] = torch.from_numpy( + np.squeeze(clim_dict[var].astype(np.float32), axis=0) + ) + return new_clim_dict def setup(self, stage: Optional[str] = None): # load datasets only if they're not loaded already - if not self.data_train and not self.data_val and not self.data_test: - self.data_train = ShuffleIterableDataset( - IndividualDataIter( + if stage != "test": + if not self.data_train and not self.data_val and not self.data_test: + self.data_train = ShuffleIterableDataset( + IndividualDataIter( + self.dataset_caller( + NpyReader( + inp_file_list=self.inp_lister_train, + out_file_list=self.out_lister_train, + variables=self.hparams.in_vars, + out_variables=self.hparams.out_vars, + shuffle=True, + ), + **self.dataset_arg, + ), + transforms=self.transforms, + output_transforms=self.output_transforms, + subsample=self.hparams.subsample, + ), + buffer_size=self.hparams.buffer_size, + ) + + self.data_val = IndividualDataIter( self.dataset_caller( NpyReader( - inp_file_list=self.inp_lister_train, - out_file_list=self.out_lister_train, + inp_file_list=self.inp_lister_val, + out_file_list=self.out_lister_val, variables=self.hparams.in_vars, out_variables=self.hparams.out_vars, - shuffle=True, + shuffle=False, ), **self.dataset_arg, ), transforms=self.transforms, output_transforms=self.output_transforms, - ), - buffer_size=self.hparams.buffer_size, - ) + subsample=self.hparams.subsample, + ) - self.data_val = IndividualDataIter( - self.dataset_caller( - NpyReader( - inp_file_list=self.inp_lister_val, - out_file_list=self.out_lister_val, - variables=self.hparams.in_vars, - out_variables=self.hparams.out_vars, - shuffle=False, + self.data_test = IndividualDataIter( + self.dataset_caller( + NpyReader( + inp_file_list=self.inp_lister_test, + out_file_list=self.out_lister_test, + variables=self.hparams.in_vars, + out_variables=self.hparams.out_vars, + shuffle=False, + ), + **self.dataset_arg, ), - **self.dataset_arg, - ), - transforms=self.transforms, - output_transforms=self.output_transforms, - ) - + transforms=self.transforms, + output_transforms=self.output_transforms, + subsample=self.hparams.subsample, + ) + else: self.data_test = IndividualDataIter( self.dataset_caller( NpyReader( @@ -161,6 +235,7 @@ def setup(self, stage: Optional[str] = None): ), transforms=self.transforms, output_transforms=self.output_transforms, + subsample=self.hparams.subsample, ) def train_dataloader(self): @@ -170,7 +245,7 @@ def train_dataloader(self): drop_last=False, num_workers=self.hparams.num_workers, pin_memory=self.hparams.pin_memory, - collate_fn=collate_fn, + collate_fn=self.collate_fn, ) def val_dataloader(self): @@ -181,7 +256,7 @@ def val_dataloader(self): drop_last=False, num_workers=self.hparams.num_workers, pin_memory=self.hparams.pin_memory, - collate_fn=collate_fn, + collate_fn=self.collate_fn, ) def test_dataloader(self): @@ -192,5 +267,63 @@ def test_dataloader(self): drop_last=False, num_workers=self.hparams.num_workers, pin_memory=self.hparams.pin_memory, - collate_fn=collate_fn, + collate_fn=self.collate_fn, + ) + + +def collate_fn(batch): + def handle_dict_features(t: Dict[str, torch.tensor]) -> torch.tensor: + t = torch.stack(tuple(t.values())) + if len(t.size()) == 4: + return torch.transpose(t, 0, 1) + return t + + inp = torch.stack([handle_dict_features(batch[i][0]) for i in range(len(batch))]) + has_extreme_mask = False + for key in batch[0][1]: + if key == "2m_temperature_extreme_mask": + has_extreme_mask = True + if not has_extreme_mask: + out = torch.stack( + [handle_dict_features(batch[i][1]) for i in range(len(batch))] ) + variables = list(batch[0][0].keys()) + out_variables = list(batch[0][1].keys()) + return inp, out, variables, out_variables + out = [] + mask = [] + for i in range(len(batch)): + out_dict = {} + mask_dict = {} + for key in batch[i][1]: + if key == "2m_temperature_extreme_mask": + mask_dict[key] = batch[i][1][key] + else: + out_dict[key] = batch[i][1][key] + out.append(handle_dict_features(out_dict)) + if mask_dict != {}: + mask.append(handle_dict_features(mask_dict)) + out = torch.stack(out) + if mask != []: + mask = torch.stack(mask) + variables = list(batch[0][0].keys()) + out_variables = list(out_dict.keys()) + return inp, out, mask, variables, out_variables + + +def collate_fn_continuous(batch): + def handle_dict_features(t: Dict[str, torch.tensor]) -> torch.tensor: + t = torch.stack(tuple(t.values())) + if len(t.size()) == 4: + return torch.transpose(t, 0, 1) + return t + + inp = torch.stack([handle_dict_features(batch[i][0]) for i in range(len(batch))]) + out = torch.stack([handle_dict_features(batch[i][1]) for i in range(len(batch))]) + lead_times = torch.stack([batch[i][2] for i in range(len(batch))]) + b, t, _, h, w = inp.shape + lead_times = lead_times.reshape(b, 1, 1, 1, 1).repeat(1, t, 1, h, w) + inp = torch.cat((inp, lead_times), dim=2) + variables = list(batch[0][0].keys()) + out_variables = list(batch[0][1].keys()) + return inp, out, variables, out_variables diff --git a/src/climate_learn/data/mapmodule.py b/src/climate_learn/data/mapmodule.py new file mode 100644 index 00000000..f20b81e6 --- /dev/null +++ b/src/climate_learn/data/mapmodule.py @@ -0,0 +1,107 @@ +import os +import numpy as np +import pytorch_lightning as pl +import torch +import torch.nn.functional as F +from torch.utils.data import DataLoader + +from .npzdataset import NpzDataset + + +def collate_fn(batch): + inp = torch.stack([batch[i][0] for i in range(len(batch))]) + out = torch.stack([batch[i][1] for i in range(len(batch))]) + out = F.pad(out, (2, 2, 3, 3)) + return inp, out, ["daily_tmax"], ["daily_tmax"] + + +class ERA5toPRISMDataModule(pl.LightningDataModule): + def __init__(self, in_root_dir, out_root_dir, batch_size=32, num_workers=4): + super().__init__() + self.save_hyperparameters(logger=False) + self.hparams.out_vars = ["daily_tmax"] + self.hparams.history = 1 + self.hparams.task = "downscaling" + + def setup(self, stage="foobar"): + self.train_dataset = NpzDataset( + os.path.join(self.hparams.in_root_dir, "train.npz"), + os.path.join(self.hparams.out_root_dir, "train.npz"), + ) + self.in_transform = self.train_dataset.in_transform + self.out_transform = self.train_dataset.out_transform + self.val_dataset = NpzDataset( + os.path.join(self.hparams.in_root_dir, "val.npz"), + os.path.join(self.hparams.out_root_dir, "val.npz"), + self.in_transform, + self.out_transform, + ) + self.test_dataset = NpzDataset( + os.path.join(self.hparams.in_root_dir, "test.npz"), + os.path.join(self.hparams.out_root_dir, "test.npz"), + self.in_transform, + self.out_transform, + ) + self.out_mask = torch.from_numpy( + np.load(os.path.join(self.hparams.out_root_dir, "mask.npy")) + ) + with open(os.path.join(self.hparams.in_root_dir, "coords.npz"), "rb") as f: + npz = np.load(f) + self.in_lat = torch.from_numpy(npz["lat"]) + self.in_lon = torch.from_numpy(npz["lon"]) + with open(os.path.join(self.hparams.out_root_dir, "coords.npz"), "rb") as f: + npz = np.load(f) + self.out_lat = torch.from_numpy(npz["lat"]) + self.out_lon = torch.from_numpy(npz["lon"]) + + def get_lat_lon(self): + return self.out_lat, self.out_lon + + def get_data_dims(self): + x, y = self.train_dataset[0] + y = F.pad(y, (2, 2, 3, 3)) + return x.unsqueeze(0).shape, y.unsqueeze(0).shape + + def get_data_variables(self): + return ["daily_tmax"], ["daily_tmax"] + + def get_climatology(self, split): + if split == "train": + return self.train_dataset.out_per_pixel_mean + elif split == "val": + return self.val_dataset.out_per_pixel_mean + elif split == "test": + return self.test_dataset.out_per_pixel_mean + else: + raise NotImplementedError() + + def get_out_transforms(self): + return self.out_transform + + def get_out_mask(self): + padded_mask = F.pad(self.out_mask, (2, 2, 3, 3)) + return padded_mask + + def train_dataloader(self): + return DataLoader( + self.train_dataset, + batch_size=self.hparams.batch_size, + num_workers=self.hparams.num_workers, + collate_fn=collate_fn, + ) + + def val_dataloader(self): + return DataLoader( + self.val_dataset, + batch_size=self.hparams.batch_size, + num_workers=self.hparams.num_workers, + collate_fn=collate_fn, + ) + + def test_dataloader(self): + return DataLoader( + self.test_dataset, + batch_size=self.hparams.batch_size, + num_workers=self.hparams.num_workers, + collate_fn=collate_fn, + ) diff --git a/src/climate_learn/data/npzdataset.py b/src/climate_learn/data/npzdataset.py new file mode 100644 index 00000000..32f50d7d --- /dev/null +++ b/src/climate_learn/data/npzdataset.py @@ -0,0 +1,49 @@ +import numpy as np +import torch +from torch.utils.data import Dataset +from torchvision.transforms import transforms + + +class NpzDataset(Dataset): + def __init__( + self, npz_in_file, npz_out_file, in_transform=None, out_transform=None + ): + super().__init__() + with open(npz_in_file, "rb") as f: + npz = np.load(f) + self.in_per_pixel_mean = torch.from_numpy(npz["mean"]) + self.in_per_pixel_std = torch.from_numpy(npz["std"]) + self.in_data = torch.from_numpy(npz["data"]) + self.in_data = self.in_data.unsqueeze(1) + self.in_total_mean = np.nanmean(npz["data"]) + self.in_total_std = np.nanstd(npz["data"]) + with open(npz_out_file, "rb") as f: + npz = np.load(f) + self.out_per_pixel_mean = torch.from_numpy(npz["mean"]) + self.out_per_pixel_std = torch.from_numpy(npz["std"]) + self.out_data = torch.from_numpy(npz["data"]) + self.out_data = self.out_data.unsqueeze(1) + self.out_total_mean = np.nanmean(npz["data"]) + self.out_total_std = np.nanstd(npz["data"]) + if in_transform is None: + self.in_transform = transforms.Normalize( + self.in_total_mean, self.in_total_std + ) + else: + self.in_transform = in_transform + if out_transform is None: + self.out_transform = transforms.Normalize( + self.out_total_mean, self.out_total_std + ) + else: + self.out_transform = out_transform + if len(self.in_data) != len(self.out_data): + raise RuntimeError("length of input and output data do not match") + + def __len__(self): + return len(self.in_data) + + def __getitem__(self, i): + x = self.in_transform(self.in_data[i]) + y = self.out_transform(self.out_data[i]) + return x, y diff --git a/src/climate_learn/data/processing/climatebench.py b/src/climate_learn/data/processing/climatebench.py new file mode 100644 index 00000000..ab8dab6a --- /dev/null +++ b/src/climate_learn/data/processing/climatebench.py @@ -0,0 +1,89 @@ +import os +from glob import glob + +import click +import numpy as np +import xarray as xr +import xesmf as xe + + +@click.command() +@click.argument("path", type=click.Path(exists=True)) +@click.option("--save_path", type=str) +@click.option("--ddeg_out", type=float, default=5.625) +def main(path, save_path, ddeg_out): + if not os.path.exists(save_path): + os.makedirs(save_path, exist_ok=True) + + list_simu = [ + "hist-GHG.nc", + "hist-aer.nc", + "historical.nc", + "ssp126.nc", + "ssp370.nc", + "ssp585.nc", + "ssp245.nc", + ] + ps = glob(os.path.join(path, f"*.nc")) + ps_ = [] + for p in ps: + for simu in list_simu: + if simu in p: + ps_.append(p) + ps = ps_ + + constant_vars = ["CO2", "CH4"] + for p in ps: + x = xr.open_dataset(p) + if "input" in p: + for v in constant_vars: + x[v] = x[v].expand_dims( + dim={"latitude": 96, "longitude": 144}, axis=(1, 2) + ) + x_regridded = regrid(x, ddeg_out, reuse_weights=False) + x_regridded.to_netcdf(os.path.join(save_path, os.path.basename(p))) + + +def regrid( + ds_in, ddeg_out, method="bilinear", reuse_weights=True, cmip=False, rename=None +): + if "latitude" in ds_in.coords: + ds_in = ds_in.rename({"latitude": "lat", "longitude": "lon"}) + if cmip: + ds_in = ds_in.drop(("lat_bnds", "lon_bnds")) + if hasattr(ds_in, "plev_bnds"): + ds_in = ds_in.drop(("plev_bnds")) + if hasattr(ds_in, "time_bnds"): + ds_in = ds_in.drop(("time_bnds")) + if rename is not None: + ds_in = ds_in.rename({rename[0]: rename[1]}) + + # Create output grid + grid_out = xr.Dataset( + { + "lat": (["lat"], np.arange(-90 + ddeg_out / 2, 90, ddeg_out)), + "lon": (["lon"], np.arange(0, 360, ddeg_out)), + } + ) + + # Create regridder + regridder = xe.Regridder( + ds_in, grid_out, method, periodic=True, reuse_weights=reuse_weights + ) + ds_out = regridder(ds_in, keep_attrs=True).astype("float32") + + if rename is not None: + if rename[0] == "zg": + ds_out["z"] *= 9.807 + if rename[0] == "rsdt": + ds_out["tisr"] *= 60 * 60 + ds_out = ds_out.isel(time=slice(1, None, 12)) + ds_out = ds_out.assign_coords( + {"time": ds_out.time + np.timedelta64(90, "m")} + ) + + return ds_out + + +if __name__ == "__main__": + main() diff --git a/src/climate_learn/data/processing/cmip6_constants.py b/src/climate_learn/data/processing/cmip6_constants.py new file mode 100644 index 00000000..ea83e0bf --- /dev/null +++ b/src/climate_learn/data/processing/cmip6_constants.py @@ -0,0 +1,46 @@ +NAME_TO_VAR = { + "geopotential": "zg", + "u_component_of_wind": "u", + "v_component_of_wind": "v", + "temperature": "ta", + "specific_humidity": "hus", + "air_temperature": "tas", +} + +VAR_TO_NAME = {v: k for k, v in NAME_TO_VAR.items()} + +SINGLE_LEVEL_VARS = [ + "air_temperature", +] + +PRESSURE_LEVEL_VARS = [ + "geopotential", + "u_component_of_wind", + "v_component_of_wind", + "temperature", + "specific_humidity", +] + +VAR_TO_UNIT = { + "air_temperature": "C", + "geopotential": "m^2/s^2", + "u_component_of_wind": "m/s", + "v_component_of_wind": "m/s", + "temperature": "C", + "specific_humidity": "kg/kg" +} + +DEFAULT_PRESSURE_LEVELS = [50, 250, 500, 600, 700, 850, 925] + +CONSTANTS = [] + +NAME_LEVEL_TO_VAR_LEVEL = {} + +for var in SINGLE_LEVEL_VARS: + NAME_LEVEL_TO_VAR_LEVEL[var] = NAME_TO_VAR[var] + +for var in PRESSURE_LEVEL_VARS: + for l in DEFAULT_PRESSURE_LEVELS: + NAME_LEVEL_TO_VAR_LEVEL[var + "_" + str(l)] = NAME_TO_VAR[var] + "_" + str(l) + +VAR_LEVEL_TO_NAME_LEVEL = {v: k for k, v in NAME_LEVEL_TO_VAR_LEVEL.items()} diff --git a/src/climate_learn/data/processing/era5_constants.py b/src/climate_learn/data/processing/era5_constants.py new file mode 100644 index 00000000..39c73bdf --- /dev/null +++ b/src/climate_learn/data/processing/era5_constants.py @@ -0,0 +1,84 @@ +NAME_TO_VAR = { + "2m_temperature": "t2m", + "10m_u_component_of_wind": "u10", + "10m_v_component_of_wind": "v10", + "mean_sea_level_pressure": "msl", + "surface_pressure": "sp", + "toa_incident_solar_radiation": "tisr", + "total_precipitation": "tp", + "land_sea_mask": "lsm", + "orography": "orography", + "lattitude": "lat2d", + "geopotential": "z", + "u_component_of_wind": "u", + "v_component_of_wind": "v", + "temperature": "t", + "relative_humidity": "r", + "specific_humidity": "q", + "vorticity": "vo", + "potential_vorticity": "pv", + "total_cloud_cover": "tcc", +} + +VAR_TO_NAME = {v: k for k, v in NAME_TO_VAR.items()} + +SINGLE_LEVEL_VARS = [ + "2m_temperature", + "10m_u_component_of_wind", + "10m_v_component_of_wind", + "mean_sea_level_pressure", + "surface_pressure", + "toa_incident_solar_radiation", + "total_precipitation", + "total_cloud_cover", + "land_sea_mask", + "orography", + "lattitude", +] + +PRESSURE_LEVEL_VARS = [ + "geopotential", + "u_component_of_wind", + "v_component_of_wind", + "temperature", + "relative_humidity", + "specific_humidity", + "vorticity", + "potential_vorticity", +] + +VAR_TO_UNIT = { + "2m_temperature": "K", + "10m_u_component_of_wind": "m/s", + "10m_v_component_of_wind": "m/s", + "mean_sea_level_pressure": "Pa", + "surface_pressure": "Pa", + "toa_incident_solar_radiation": "J/m^2", + "total_precipitation": "m", + "total_cloud_cover": None, # dimensionless + "land_sea_mask": None, # dimensionless + "orography": None, # dimensionless + "geopotential": "m^2/s^2", + "u_component_of_wind": "m/s", + "v_component_of_wind": "m/s", + "temperature": "K", + "relative_humidity": "%", + "specific_humidity": "kg/kg", + "voriticity": "1/s", + "potential_vorticity": "K m^2 / (kg s)" +} + +DEFAULT_PRESSURE_LEVELS = [50, 250, 500, 600, 700, 850, 925] + +CONSTANTS = ["orography", "land_sea_mask", "slt", "lattitude", "longitude"] + +NAME_LEVEL_TO_VAR_LEVEL = {} + +for var in SINGLE_LEVEL_VARS: + NAME_LEVEL_TO_VAR_LEVEL[var] = NAME_TO_VAR[var] + +for var in PRESSURE_LEVEL_VARS: + for l in DEFAULT_PRESSURE_LEVELS: + NAME_LEVEL_TO_VAR_LEVEL[var + "_" + str(l)] = NAME_TO_VAR[var] + "_" + str(l) + +VAR_LEVEL_TO_NAME_LEVEL = {v: k for k, v in NAME_LEVEL_TO_VAR_LEVEL.items()} diff --git a/src/climate_learn/data/processing/era5_cropped.py b/src/climate_learn/data/processing/era5_cropped.py new file mode 100644 index 00000000..4b04d5a3 --- /dev/null +++ b/src/climate_learn/data/processing/era5_cropped.py @@ -0,0 +1,99 @@ +# Standard library +from argparse import ArgumentParser +import glob +import os + +# Third party +import numpy as np +import xarray as xr + + +parser = ArgumentParser( + description="Crops ERA5 data for ERA5 to PRISM downscaling experiments." +) +parser.add_argument( + "source", help="The local directory containing raw ERA5 2.8125 degree files." +) +parser.add_argument( + "destination", help="The destination directory for the processed files." +) +parser.add_argument( + "--train_end", default=2015, type=int, help="The last year of training data." +) +parser.add_argument( + "--val_end", default=2016, type=int, help="The last year of validation data." +) +parser.add_argument( + "--test_end", default=2018, type=int, help="The last year of testing data." +) +args = parser.parse_args() + +# Concatenate all 2m temperature xarray files +filelist = glob.glob(os.path.join(args.source, "2m_temperature", "*.nc")) +filelist = sorted(filelist) +xarr = None +for fi in filelist: + if xarr is None: + xarr = xr.open_dataset(fi) + else: + xarr = xr.concat((xarr, xr.open_dataset(fi)), dim="time") +lats = xarr.lat.data +lons = xarr.lon.data + +# PRISM spatial bounds +bottom = 24.10 +top = 49.94 +left = 234.98 +right = 293.48 + +# Get train data +prism_start_date = "1981-01-01" +train_data = xarr.sel( + { + "time": slice(prism_start_date, f"{args.train_end}-12-31"), + "lat": slice(bottom, top), + "lon": slice(left, right), + } +) +train_data = train_data.resample(time="1D").max(dim="time") +train_mean = train_data.mean(dim="time")["t2m"].data +train_std = train_data.std(dim="time")["t2m"].data +train_narr = train_data["t2m"].data +with open(os.path.join(args.destination, "train.npz"), "wb") as f: + np.savez(f, data=train_narr, mean=train_mean, std=train_std) + +# Get validation data +val_data = xarr.sel( + { + "time": slice(f"{args.train_end+1}-01-01", f"{args.val_end}-12-31"), + "lat": slice(bottom, top), + "lon": slice(left, right), + } +) +val_data = val_data.resample(time="1D").max(dim="time") +val_mean = val_data.mean(dim="time")["t2m"].data +val_std = val_data.std(dim="time")["t2m"].data +val_narr = val_data["t2m"].data +with open(os.path.join(args.destination, "val.npz"), "wb") as f: + np.savez(f, data=val_narr, mean=val_mean, std=val_std) + +# Get test data +test_data = xarr.sel( + { + "time": slice(f"{args.val_end+1}-01-01", f"{args.test_end}-12-31"), + "lat": slice(bottom, top), + "lon": slice(left, right), + } +) +test_data = test_data.resample(time="1D").max(dim="time") +test_mean = test_data.mean(dim="time")["t2m"].data +test_std = test_data.std(dim="time")["t2m"].data +test_narr = test_data["t2m"].data +with open(os.path.join(args.destination, "test.npz"), "wb") as f: + np.savez(f, data=test_narr, mean=test_mean, std=test_std) + +# Save latitude and longitude +cropped_lats = train_data.lat.data +cropped_lons = train_data.lon.data +with open(os.path.join(args.destination, "coords.npz"), "wb") as f: + np.savez(f, lat=cropped_lats, lon=cropped_lons) diff --git a/src/climate_learn/data/processing/era5_extreme.py b/src/climate_learn/data/processing/era5_extreme.py new file mode 100644 index 00000000..c4807e23 --- /dev/null +++ b/src/climate_learn/data/processing/era5_extreme.py @@ -0,0 +1,201 @@ +from argparse import ArgumentParser +import glob +import os + +from ..climate_dataset import ERA5Args +from ..task import ForecastingArgs +from ..dataset import MapDataset, MapDatasetArgs + +import torch +import numpy as np + + +parser = ArgumentParser(description="Generates the masks for ERA5 Extreme.") +parser.add_argument("source", help="The directory where the raw ERA5 data is stored.") +parser.add_argument( + "source_npz", help="The directory where the processed ERA5 data is stored." +) +parser.add_argument("target", help="The directory to save the processed files.") +args = parser.parse_args() + +era_args = ERA5Args(args.source, ["2m_temperature"], range(1979, 2015)) +forecasting_args = ForecastingArgs( + in_vars=["era5:2m_temperature"], + out_vars=["era5:2m_temperature"], + constants=[], + history=1, + window=1, + pred_range=1, +) + +map_dataset_args = MapDatasetArgs(era_args, forecasting_args) +map_dataset = MapDataset(map_dataset_args) +map_dataset.setup() + +constants_data = map_dataset.data.get_constants_data() +const_data = map_dataset.task.create_constants_data(constants_data, 0) +data = [] +for index in range(map_dataset.length): + raw_index = map_dataset.task.get_raw_index(index) + raw_data = map_dataset.data.get_item(raw_index) + data.append(map_dataset.task.create_inp_out(raw_data, constants_data, 0)) + + +def handle_dict_features(t): + t = torch.stack(tuple(t.values())) + if len(t.size()) == 4: + return torch.transpose(t, 0, 1) + return t + + +inp = torch.stack([handle_dict_features(data[i][0]) for i in range(len(data))]) +out = torch.stack([handle_dict_features(data[i][1]) for i in range(len(data))]) +if const_data != {}: + const = handle_dict_features(const_data) +else: + const = None + +time_horizon = 7 * 24 +window = 1 +mean_tensor = [] +for i in range(time_horizon, inp.size(0), window): + mean_tensor.append(torch.mean(inp[i - time_horizon : i], dim=0)) +mean_tensor = torch.stack(mean_tensor) + +l_mean_tensor = torch.roll(mean_tensor, 1, -1) +r_mean_tensor = torch.roll(mean_tensor, -1, -1) +d_mean_tensor = torch.roll(mean_tensor, 1, -2) +u_mean_tensor = torch.roll(mean_tensor, -1, -2) + +ld_mean_tensor = torch.roll(l_mean_tensor, 1, -2) +lu_mean_tensor = torch.roll(l_mean_tensor, -1, -2) +rd_mean_tensor = torch.roll(r_mean_tensor, 1, -2) +ru_mean_tensor = torch.roll(r_mean_tensor, -1, -2) + +g_mean_tensor = 4 * mean_tensor +g_mean_tensor += l_mean_tensor + r_mean_tensor + d_mean_tensor + u_mean_tensor +g_mean_tensor += 0.25 * ( + ld_mean_tensor + lu_mean_tensor + rd_mean_tensor + ru_mean_tensor +) +g_mean_tensor = g_mean_tensor / 9 + +sorted_g_mean_tensor, sorted_args_g_mean_tensor = torch.sort(g_mean_tensor, dim=0) + +low_percentile = 0.05 +low_threshold_index = int(low_percentile * g_mean_tensor.size(0)) + +high_percentile = 0.95 +high_threshold_index = int(high_percentile * g_mean_tensor.size(0)) + +low_threshold = sorted_g_mean_tensor[low_threshold_index].numpy() +high_threshold = sorted_g_mean_tensor[high_threshold_index].numpy() + +low_threshold = np.squeeze(low_threshold, axis=0) +high_threshold = np.squeeze(high_threshold, axis=0) + +file_list = glob.glob(os.path.join(args.source_npz, "*.npz")) +file_list = [f for f in file_list if "climatology" not in f] + +years = list(range(2017, 2019)) +file_list_by_years = [[] for _ in years] +for file_name in file_list: + year = int((file_name.split("/")[-1]).split("_")[0]) + year_index = year - years[0] + file_list_by_years[year_index].append(file_name) + + +def sort_func(file_name): + index = int(((file_name.split("/")[-1]).split("_")[1]).split(".")[0]) + return index + + +for file_list_by_year in file_list_by_years: + file_list_by_year.sort(key=sort_func, reverse=False) + +time_horizon = 7 * 24 +for file_list in file_list_by_years: + yearly_data = {} + n_instances_in_shard = 0 + for file in file_list: + data = np.load(file) + if yearly_data == {}: + yearly_data = data + random_key = next(iter(data.keys())) + n_instances_in_shard = data[random_key].shape[0] + else: + yearly_data = { + k: np.concatenate((yearly_data[k], data[k]), axis=0) + for k in yearly_data.keys() + } + random_key = next(iter(data.keys())) + assert n_instances_in_shard == data[random_key].shape[0] + air_temp = yearly_data["2m_temperature"] + mean_tensor = [] + for i in range(time_horizon, air_temp.shape[0]): + curr_mean = np.mean(air_temp[i - time_horizon : i], axis=0) + mean_tensor.append(curr_mean) + mean_tensor = np.stack(mean_tensor, axis=0) + + l_mean_tensor = np.roll(mean_tensor, 1, -1) + r_mean_tensor = np.roll(mean_tensor, -1, -1) + d_mean_tensor = np.roll(mean_tensor, 1, -2) + u_mean_tensor = np.roll(mean_tensor, -1, -2) + + ld_mean_tensor = np.roll(l_mean_tensor, 1, -2) + lu_mean_tensor = np.roll(l_mean_tensor, -1, -2) + rd_mean_tensor = np.roll(r_mean_tensor, 1, -2) + ru_mean_tensor = np.roll(r_mean_tensor, -1, -2) + + g_mean_tensor = 4 * mean_tensor + g_mean_tensor += l_mean_tensor + r_mean_tensor + d_mean_tensor + u_mean_tensor + g_mean_tensor += 0.25 * ( + ld_mean_tensor + lu_mean_tensor + rd_mean_tensor + ru_mean_tensor + ) + g_mean_tensor = g_mean_tensor / 9 + + threshold_instances = np.zeros_like(air_temp[0], dtype=air_temp.dtype) + air_temp_extreme_mask = np.zeros_like(air_temp, dtype=air_temp.dtype) + for i in range(time_horizon, air_temp.shape[0]): + curr_g_mean = g_mean_tensor[i - time_horizon] + curr_mask = np.logical_or( + curr_g_mean < low_threshold, curr_g_mean > high_threshold + ).astype(air_temp.dtype) + air_temp_extreme_mask[i] = curr_mask + threshold_instances += curr_mask + n_instances = np.min(threshold_instances) + yearly_data["2m_temperature_extreme_mask"] = air_temp_extreme_mask + + for shard_id, file in enumerate(file_list): + start_index = shard_id * n_instances_in_shard + end_index = start_index + n_instances_in_shard + new_file_name = os.path.join(args.target, file.split("/")[-1]) + sharded_data = { + k: yearly_data[k][start_index:end_index] for k in yearly_data.keys() + } + np.savez(new_file_name, **sharded_data) + + print( + air_temp_extreme_mask.sum(), + air_temp.shape[0] * air_temp.shape[-1] * air_temp.shape[-2], + ) + +newfile_list = glob.glob(os.path.join(args.target, "*.npz")) +newfile_list = [f for f in newfile_list if "climatology" not in f] +years = list(range(2017, 2019)) +newfile_list_by_years = [[] for _ in years] +for file_name in newfile_list: + year = int((file_name.split("/")[-1]).split("_")[0]) + year_index = year - years[0] + newfile_list_by_years[year_index].append(file_name) +for newfile_list_by_year in newfile_list_by_years: + newfile_list_by_year.sort(key=sort_func, reverse=False) + +for newfile_list, file_list in zip(newfile_list_by_years, file_list_by_years): + for new_file, file in zip(newfile_list, file_list): + new_data = np.load(new_file) + data = np.load(file) + for k in new_data.keys(): + if k == "2m_temperature_extreme_mask": + continue + else: + assert (new_data[k] == data[k]).all() diff --git a/src/climate_learn/data/nc2npz.py b/src/climate_learn/data/processing/nc2npz.py similarity index 91% rename from src/climate_learn/data/nc2npz.py rename to src/climate_learn/data/processing/nc2npz.py index d8177d57..23d11ce0 100644 --- a/src/climate_learn/data/nc2npz.py +++ b/src/climate_learn/data/processing/nc2npz.py @@ -9,7 +9,7 @@ from tqdm import tqdm # Local application -from .climate_dataset.era5.constants import ( +from .era5_constants import ( DEFAULT_PRESSURE_LEVELS, NAME_TO_VAR, VAR_TO_NAME, @@ -63,7 +63,15 @@ def nc2np(path, variables, years, save_dir, partition, num_shards_per_year): if len(ds[code].shape) == 3: # surface level variables ds[code] = ds[code].expand_dims("val", axis=1) # remove the last 24 hours if this year has 366 days - np_vars[var] = ds[code].to_numpy()[-HOURS_PER_YEAR:] + if code == "tp": # accumulate 6 hours and log transform + tp = ds[code].to_numpy() + tp_cum_6hrs = np.cumsum(tp, axis=0) + tp_cum_6hrs[6:] = tp_cum_6hrs[6:] - tp_cum_6hrs[:-6] + eps = 0.001 + tp_cum_6hrs = np.log(eps + tp_cum_6hrs) - np.log(eps) + np_vars[var] = tp_cum_6hrs[-HOURS_PER_YEAR:] + else: + np_vars[var] = ds[code].to_numpy()[-HOURS_PER_YEAR:] if partition == "train": # compute mean and std of each var in each year @@ -141,6 +149,8 @@ def nc2np(path, variables, years, save_dir, partition, num_shards_per_year): # E[X] = E[E[X|Y]] mean = mean.mean(axis=0) normalize_mean[var] = mean + if var == "total_precipitation": + normalize_mean[var] = np.zeros_like(normalize_mean[var]) normalize_std[var] = std np.savez(os.path.join(save_dir, "normalize_mean.npz"), **normalize_mean) diff --git a/src/climate_learn/data/processing/prism.py b/src/climate_learn/data/processing/prism.py new file mode 100644 index 00000000..95bf3a24 --- /dev/null +++ b/src/climate_learn/data/processing/prism.py @@ -0,0 +1,121 @@ +# Standard library +from argparse import ArgumentParser +import glob +import os + +# Third party +import numpy as np +import rasterio as rio +from tqdm import tqdm +import xesmf as xe + + +parser = ArgumentParser(description="Processes PRISM data.") +parser.add_argument( + "source", help="The local directory containing raw PRISM files. See download.py." +) +parser.add_argument( + "destination", help="The destination directory for the processed files." +) +parser.add_argument( + "--target_res", + type=float, + default=0.75, + help="The desired target resolution in degrees.", +) +parser.add_argument( + "--train_end", default="2016", help="The first year of validation data." +) +parser.add_argument("--val_end", default="2017", help="The first year of testing data.") +parser.add_argument("--test_end", default="2018", help="The last year of testing data.") +args = parser.parse_args() + +root = args.source +subdirs = sorted(os.listdir(root)) + +# Build regridder +dataset = rio.open(glob.glob(os.path.join(root, subdirs[0], "*.bil"))[0]) +lats = np.empty(dataset.height, dtype=float) +lons = np.empty(dataset.width, dtype=float) +for i in range(dataset.height): + lats[i] = (dataset.transform * (dataset.width // 2, i))[1] +for i in range(dataset.width): + lons[i] = (dataset.transform * (i, dataset.height // 2))[0] % 360 + +target_res = args.target_res +scaling_factor = 0.032 / target_res +target_width = round(dataset.width * scaling_factor) +target_height = round(dataset.height * scaling_factor) +grid_in = {"lon": lons, "lat": lats} +grid_out = { + "lon": np.linspace(lons.min(), lons.max(), target_width), + "lat": np.linspace(lats.min(), lats.max(), target_height), +} +regridder = xe.Regridder(grid_in, grid_out, "bilinear") + +# Get mask +arr = dataset.read(1) +mask = (arr != -9999).astype(int) + +# Define function to fix border +masked_arr = np.where(mask, arr, np.nan) +arr_out = regridder(masked_arr) +first_row = np.empty(arr_out.shape[1]) +first_row[:] = np.nan + + +def fix(arr): + return np.vstack((first_row, arr[1:])) + + +# Process PRISM data +all_prism_data = [] +for sd in tqdm(subdirs): + dataset = rio.open(glob.glob(os.path.join(root, sd, "*.bil"))[0]) + arr = dataset.read(1) + masked_arr = np.where(mask, arr, np.nan) + arr_out = regridder(masked_arr) + fixed_arr = fix(arr_out) + all_prism_data.append(fixed_arr) +all_prism_data = np.stack(all_prism_data, 0) + +# Build train/val/test splits +train_end, val_end, test_end = None, None, None +for i, sd in enumerate(subdirs): + if train_end is None and sd.startswith(args.train_end): + train_end = i + if val_end is None and sd.startswith(args.val_end): + val_end = i + if sd.startswith(args.test_end): + test_end = i + +train = all_prism_data[:train_end] +train_mean = train.mean(axis=0) +train_std = train.std(axis=0) + +val = all_prism_data[train_end:val_end] +val_mean = val.mean(axis=0) +val_std = val.std(axis=0) + +test = all_prism_data[val_end:test_end] +test_mean = test.mean(axis=0) +test_std = test.std(axis=0) + +regridded_mask = np.where(np.isnan(train[0]), 0, 1) + +# Save outputs +dest = args.destination +with open(f"{args.destination}/train.npz", "wb") as f: + np.savez(f, data=train, mean=train_mean, std=train_std) + +with open(f"{args.destination}/val.npz", "wb") as f: + np.savez(f, data=val, mean=val_mean, std=val_std) + +with open(f"{args.destination}/test.npz", "wb") as f: + np.savez(f, data=test, mean=test_mean, std=test_std) + +with open(f"{args.destination}/coords.npz", "wb") as f: + np.savez(f, lat=grid_out["lat"], lon=grid_out["lon"]) + +with open(f"{args.destination}/mask.npy", "wb") as f: + np.save(f, regridded_mask) diff --git a/src/climate_learn/metrics/functional.py b/src/climate_learn/metrics/functional.py index 8ca1aa5c..c993bf8d 100644 --- a/src/climate_learn/metrics/functional.py +++ b/src/climate_learn/metrics/functional.py @@ -1,13 +1,17 @@ # Standard library from typing import Optional, Union +# Local application +from .utils import Pred, handles_probabilistic + # Third party import torch import torch.nn.functional as F +@handles_probabilistic def mse( - pred: Union[torch.FloatTensor, torch.DoubleTensor], + pred: Pred, target: Union[torch.FloatTensor, torch.DoubleTensor], aggregate_only: bool = False, lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, @@ -15,60 +19,104 @@ def mse( error = (pred - target).square() if lat_weights is not None: error = error * lat_weights + per_channel_losses = error.mean([0, 2, 3]) loss = error.mean() - if not aggregate_only: - per_channel_losses = error.mean([0, 2, 3]) - loss = loss.unsqueeze(0) - loss = torch.cat((per_channel_losses, loss)) - return loss + if aggregate_only: + return loss + return torch.cat((per_channel_losses, loss.unsqueeze(0))) + + +@handles_probabilistic +def msess( + pred: Pred, + target: Union[torch.FloatTensor, torch.DoubleTensor], + climatology: Union[torch.FloatTensor, torch.DoubleTensor], + aggregate_only: bool = False, + lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, +) -> Union[torch.FloatTensor, torch.DoubleTensor]: + pred_mse = mse(pred, target, aggregate_only, lat_weights) + clim_mse = mse(climatology, target, aggregate_only, lat_weights) + return 1 - pred_mse / clim_mse + + +@handles_probabilistic +def mae( + pred: Pred, + target: Union[torch.FloatTensor, torch.DoubleTensor], + aggregate_only: bool = False, + lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, +) -> Union[torch.FloatTensor, torch.DoubleTensor]: + error = (pred - target).abs() + if lat_weights is not None: + error = error * lat_weights + per_channel_losses = error.mean([0, 2, 3]) + loss = error.mean() + if aggregate_only: + return loss + return torch.cat((per_channel_losses, loss.unsqueeze(0))) +@handles_probabilistic def rmse( - pred: Union[torch.FloatTensor, torch.DoubleTensor], + pred: Pred, target: Union[torch.FloatTensor, torch.DoubleTensor], aggregate_only: bool = False, lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, + mask=None, ) -> Union[torch.FloatTensor, torch.DoubleTensor]: error = (pred - target).square() if lat_weights is not None: error = error * lat_weights - loss = error.mean().sqrt() - if not aggregate_only: - per_channel_losses = error.mean([0, 2, 3]).sqrt() - loss = loss.unsqueeze(0) - loss = torch.cat((per_channel_losses, loss)) - return loss + if mask is not None: + error = error * mask + eps = 1e-9 + masked_lat_weights = torch.mean(mask, dim=(1, 2, 3), keepdim=True) + eps + error = error / masked_lat_weights + per_channel_losses = error.mean([2, 3]).sqrt().mean(0) + loss = per_channel_losses.mean() + if aggregate_only: + return loss + return torch.cat((per_channel_losses, loss.unsqueeze(0))) +@handles_probabilistic def acc( - pred: Union[torch.FloatTensor, torch.DoubleTensor], + pred: Pred, target: Union[torch.FloatTensor, torch.DoubleTensor], climatology: Optional[Union[torch.FloatTensor, torch.DoubleTensor]], aggregate_only: bool = False, lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, + mask=None, ) -> Union[torch.FloatTensor, torch.DoubleTensor]: pred = pred - climatology target = target - climatology - pred_prime = pred - pred.mean([0, 2, 3], keepdims=True) - target_prime = target - target.mean([0, 2, 3], keepdims=True) - if lat_weights is not None: - numer = (lat_weights * pred_prime * target_prime).sum([0, 2, 3]) - denom1 = (lat_weights * pred_prime.square()).sum([0, 2, 3]) - denom2 = (lat_weights * target_prime.square()).sum([0, 2, 3]) - else: - numer = (pred_prime * target_prime).sum([0, 2, 3]) - denom1 = pred_prime.square().sum([0, 2, 3]) - denom2 = target_prime.square().sum([0, 2, 3]) - per_channel_losses = numer / (denom1 * denom2).sqrt() - loss = per_channel_losses.mean() - if not aggregate_only: - loss = loss.unsqueeze(0) - loss = torch.cat((per_channel_losses, loss)) - return loss + per_channel_acc = [] + for i in range(pred.shape[1]): + pred_prime = pred[:, i] - pred[:, i].mean() + target_prime = target[:, i] - target[:, i].mean() + if mask is not None: + eps = 1e-9 + numer = (mask * lat_weights * pred_prime * target_prime).sum() + denom1 = ((mask + eps) * lat_weights * pred_prime.square()).sum() + denom2 = ((mask + eps) * lat_weights * target_prime.square()).sum() + else: + numer = (lat_weights * pred_prime * target_prime).sum() + denom1 = (lat_weights * pred_prime.square()).sum() + denom2 = (lat_weights * target_prime.square()).sum() + numer = (lat_weights * pred_prime * target_prime).sum() + denom1 = (lat_weights * pred_prime.square()).sum() + denom2 = (lat_weights * target_prime.square()).sum() + per_channel_acc.append(numer / (denom1 * denom2).sqrt()) + per_channel_acc = torch.stack(per_channel_acc) + result = per_channel_acc.mean() + if aggregate_only: + return result + return torch.cat((per_channel_acc, result.unsqueeze(0))) +@handles_probabilistic def pearson( - pred: Union[torch.FloatTensor, torch.DoubleTensor], + pred: Pred, target: Union[torch.FloatTensor, torch.DoubleTensor], aggregate_only: bool = False, ) -> Union[torch.FloatTensor, torch.DoubleTensor]: @@ -84,17 +132,20 @@ def pearson( return coeff +@handles_probabilistic def mean_bias( - pred: Union[torch.FloatTensor, torch.DoubleTensor], + pred: Pred, target: Union[torch.FloatTensor, torch.DoubleTensor], aggregate_only: bool = False, ) -> Union[torch.FloatTensor, torch.DoubleTensor]: - result = target.mean() - pred.mean() - if not aggregate_only: - per_channel_mean_bias = target.mean([0, 2, 3]) - pred.mean([0, 2, 3]) - result = result.unsqueeze(0) - result = torch.cat((per_channel_mean_bias, result)) - return result + per_channel_mb = [] + for i in range(pred.shape[1]): + per_channel_mb.append(target[:, i].mean() - pred[:, i].mean()) + per_channel_mb = torch.stack(per_channel_mb) + result = per_channel_mb.mean() + if aggregate_only: + return result + return torch.cat((per_channel_mb, result.unsqueeze(0))) def _flatten_channel_wise(x: torch.Tensor) -> torch.Tensor: @@ -105,4 +156,94 @@ def _flatten_channel_wise(x: torch.Tensor) -> torch.Tensor: :return: A tensor of shape [C,B*H*W]. :rtype: torch.Tensor """ - return torch.stack([xi.flatten() for xi in torch.tensor_split(x, 2, 1)]) + subtensors = torch.tensor_split(x, x.shape[1], 1) + result = torch.stack([t.flatten() for t in subtensors]) + return result + + +def gaussian_crps( + pred: torch.distributions.Normal, + target: Union[torch.FloatTensor, torch.DoubleTensor], + aggregate_only: bool = False, + lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, +) -> Union[torch.FloatTensor, torch.DoubleTensor]: + mean, std = pred.loc, pred.scale + z = (target - mean) / std + standard_normal = torch.distributions.Normal( + torch.zeros_like(pred), torch.ones_like(pred) + ) + pdf = torch.exp(standard_normal.log_prob(z)) + cdf = standard_normal.cdf(z) + crps = std * (z * (2 * cdf - 1) + 2 * pdf - 1 / torch.pi) + if lat_weights is not None: + crps = crps * lat_weights + per_channel_losses = crps.mean([0, 2, 3]) + loss = crps.mean() + if aggregate_only: + return loss + return torch.cat((per_channel_losses, loss.unsqueeze(0))) + + +def gaussian_spread( + pred: torch.distributions.Normal, + aggregate_only: bool = False, + lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, +) -> Union[torch.FloatTensor, torch.DoubleTensor]: + variance = torch.square(pred.scale) + if lat_weights is not None: + variance = variance * lat_weights + per_channel_losses = variance.mean([2, 3]).sqrt().mean(0) + loss = variance.mean() + if aggregate_only: + return loss + return torch.cat((per_channel_losses, loss.unsqueeze(0))) + + +def gaussian_spread_skill_ratio( + pred: torch.distributions.Normal, + target: Union[torch.FloatTensor, torch.DoubleTensor], + aggregate_only: bool = False, + lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, +) -> Union[torch.FloatTensor, torch.DoubleTensor]: + spread = gaussian_spread(pred, aggregate_only, lat_weights) + error = rmse(pred, target, aggregate_only, lat_weights) + return spread / error + + +def nrmses( + pred: Union[torch.FloatTensor, torch.DoubleTensor], + target: Union[torch.FloatTensor, torch.DoubleTensor], + clim: Union[torch.FloatTensor, torch.DoubleTensor], + aggregate_only: bool = False, + lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, +) -> Union[torch.FloatTensor, torch.DoubleTensor]: + y_normalization = clim.squeeze() + error = (pred.mean(dim=0) - target.mean(dim=0)) ** 2 # (C, H, W) + if lat_weights is not None: + error = error * lat_weights.squeeze(0) + per_channel_losses = error.mean(dim=(-2, -1)).sqrt() / y_normalization # C + loss = per_channel_losses.mean() + if aggregate_only: + return loss + return torch.cat((per_channel_losses, loss.unsqueeze(0))) + + +def nrmseg( + pred: Union[torch.FloatTensor, torch.DoubleTensor], + target: Union[torch.FloatTensor, torch.DoubleTensor], + clim: Union[torch.FloatTensor, torch.DoubleTensor], + aggregate_only: bool = False, + lat_weights: Optional[Union[torch.FloatTensor, torch.DoubleTensor]] = None, +) -> Union[torch.FloatTensor, torch.DoubleTensor]: + y_normalization = clim.squeeze() + if lat_weights is not None: + pred = pred * lat_weights + target = target * lat_weights + pred = pred.mean(dim=(-2, -1)) # N, C + target = target.mean(dim=(-2, -1)) # N, C + error = (pred - target) ** 2 + per_channel_losses = error.mean(0).sqrt() / y_normalization # C + loss = per_channel_losses.mean() + if aggregate_only: + return loss + return torch.cat((per_channel_losses, loss.unsqueeze(0))) diff --git a/src/climate_learn/metrics/metrics.py b/src/climate_learn/metrics/metrics.py index dfca2a8b..7583d2af 100644 --- a/src/climate_learn/metrics/metrics.py +++ b/src/climate_learn/metrics/metrics.py @@ -164,6 +164,7 @@ def __call__( self, pred: Union[torch.FloatTensor, torch.DoubleTensor], target: Union[torch.FloatTensor, torch.DoubleTensor], + mask=None, ) -> Union[torch.FloatTensor, torch.DoubleTensor]: r""" .. highlight:: python @@ -178,6 +179,8 @@ def __call__( RMSE, and the preceding elements are the channel-wise RMSEs. :rtype: torch.FloatTensor|torch.DoubleTensor """ + if mask is not None: + return rmse(pred, target, self.aggregate_only, mask) return rmse(pred, target, self.aggregate_only) @@ -189,6 +192,7 @@ def __call__( self, pred: Union[torch.FloatTensor, torch.DoubleTensor], target: Union[torch.FloatTensor, torch.DoubleTensor], + mask=None, ) -> Union[torch.FloatTensor, torch.DoubleTensor]: r""" .. highlight:: python @@ -204,6 +208,8 @@ def __call__( :rtype: torch.FloatTensor|torch.DoubleTensor """ super().cast_to_device(pred) + if mask is not None: + return rmse(pred, target, self.aggregate_only, self.lat_weights, mask) return rmse(pred, target, self.aggregate_only, self.lat_weights) @@ -220,6 +226,7 @@ def __call__( self, pred: Union[torch.FloatTensor, torch.DoubleTensor], target: Union[torch.FloatTensor, torch.DoubleTensor], + mask=None, ) -> Union[torch.FloatTensor, torch.DoubleTensor]: r""" .. highlight:: python @@ -237,6 +244,8 @@ def __call__( :rtype: torch.FloatTensor|torch.DoubleTensor """ super().cast_to_device(pred) + if mask is not None: + return acc(pred, target, self.climatology, self.aggregate_only, mask) return acc(pred, target, self.climatology, self.aggregate_only) @@ -254,6 +263,7 @@ def __call__( self, pred: Union[torch.FloatTensor, torch.DoubleTensor], target: Union[torch.FloatTensor, torch.DoubleTensor], + mask=None, ) -> Union[torch.FloatTensor, torch.DoubleTensor]: r""" .. highlight:: python @@ -272,6 +282,15 @@ def __call__( """ LatitudeWeightedMetric.cast_to_device(self, pred) ClimatologyBasedMetric.cast_to_device(self, pred) + if mask is not None: + return acc( + pred, + target, + self.climatology, + self.aggregate_only, + self.lat_weights, + mask, + ) return acc( pred, target, self.climatology, self.aggregate_only, self.lat_weights ) diff --git a/src/climate_learn/metrics/utils.py b/src/climate_learn/metrics/utils.py index 4544bdf3..51b923da 100644 --- a/src/climate_learn/metrics/utils.py +++ b/src/climate_learn/metrics/utils.py @@ -1,11 +1,14 @@ # Standard library from dataclasses import dataclass -from typing import List +from functools import wraps +from typing import List, Union # Third party import numpy.typing as npt import torch +Pred = Union[torch.FloatTensor, torch.DoubleTensor, torch.distributions.Normal] + @dataclass class MetricsMetaInfo: @@ -26,3 +29,13 @@ def decorator(metric_class): return metric_class return decorator + + +def handles_probabilistic(metric): + @wraps(metric) + def wrapper(pred: Pred, *args, **kwargs): + if isinstance(pred, torch.distributions.Normal): + pred = pred.loc + return metric(pred, *args, **kwargs) + + return wrapper diff --git a/src/climate_learn/models/hub/climatology.py b/src/climate_learn/models/hub/climatology.py index ca925f6a..60add83d 100644 --- a/src/climate_learn/models/hub/climatology.py +++ b/src/climate_learn/models/hub/climatology.py @@ -3,16 +3,18 @@ # Third party from torch import nn +from torchvision import transforms @register("climatology") class Climatology(nn.Module): - def __init__(self, clim): + def __init__(self, clim, mean, std): super().__init__() + self.norm = transforms.Normalize(mean, std) self.clim = clim # clim.shape = [C,H,W] def forward(self, x): # x.shape = [B,T,C,H,W] - yhat = self.clim.unsqueeze(0).repeat(x.shape[0], 1, 1, 1) + yhat = self.norm(self.clim).unsqueeze(0).repeat(x.shape[0], 1, 1, 1) # yhat.shape = [B,C,H,W] return yhat diff --git a/src/climate_learn/models/hub/interpolation.py b/src/climate_learn/models/hub/interpolation.py index 408b269d..ad41780c 100644 --- a/src/climate_learn/models/hub/interpolation.py +++ b/src/climate_learn/models/hub/interpolation.py @@ -14,4 +14,5 @@ def __init__(self, size, mode): self.mode = mode def forward(self, x): - return F.interpolate(x, self.size, mode=self.mode) + yhat = F.interpolate(x, self.size, mode=self.mode) + return yhat diff --git a/src/climate_learn/models/hub/vit.py b/src/climate_learn/models/hub/vit.py index a760ceb3..90c2c8e0 100644 --- a/src/climate_learn/models/hub/vit.py +++ b/src/climate_learn/models/hub/vit.py @@ -32,8 +32,10 @@ def __init__( self.in_channels = in_channels * history self.out_channels = out_channels self.patch_size = patch_size + self.patch_embed = PatchEmbed(img_size, patch_size, self.in_channels, embed_dim) self.num_patches = self.patch_embed.num_patches + self.pos_embed = nn.Parameter( torch.zeros(1, self.num_patches, embed_dim), requires_grad=learn_pos_emb ) @@ -55,17 +57,13 @@ def __init__( ] ) self.norm = nn.LayerNorm(embed_dim) + self.head = nn.ModuleList() for _ in range(decoder_depth): self.head.append(nn.Linear(embed_dim, embed_dim)) self.head.append(nn.GELU()) + self.head.append(nn.Linear(embed_dim, out_channels * patch_size**2)) self.head = nn.Sequential(*self.head) - self.final = PeriodicConv2D( - (self.num_patches * embed_dim) // (img_size[0] * img_size[1]), - self.out_channels, - kernel_size=7, - padding=3, - ) self.initialize_weights() def initialize_weights(self): @@ -87,18 +85,20 @@ def _init_weights(self, m): nn.init.constant_(m.bias, 0) nn.init.constant_(m.weight, 1.0) - def unpatchify(self, patches): - b, num_patches, embed_dim = patches.shape + def unpatchify(self, x: torch.Tensor): + """ + x: (B, L, V * patch_size**2) + return imgs: (B, V, H, W) + """ p = self.patch_size - h, w = self.img_size - hh, ww = h // p, w // p - c = (num_patches * embed_dim) // (h * w) - if hh * ww != patches.shape[1]: - raise RuntimeError("Cannot unpatchify") - x = patches.reshape((b, hh, ww, p, p, c)) + c = self.out_channels + h = self.img_size[0] // p + w = self.img_size[1] // p + assert h * w == x.shape[1] + x = x.reshape(shape=(x.shape[0], h, w, p, p, c)) x = torch.einsum("nhwpqc->nchpwq", x) - x = x.reshape((b, -1, h, w)) - return x + imgs = x.reshape(shape=(x.shape[0], c, h * p, w * p)) + return imgs def forward_encoder(self, x: torch.Tensor): # x.shape = [B,C,H,W] @@ -113,15 +113,13 @@ def forward_encoder(self, x: torch.Tensor): return x def forward(self, x): - # x.shape = [B,T,in_channels,H,W] - x = x.flatten(1, 2) + if len(x.shape) == 5: # x.shape = [B,T,in_channels,H,W] + x = x.flatten(1, 2) # x.shape = [B,T*in_channels,H,W] x = self.forward_encoder(x) # x.shape = [B,num_patches,embed_dim] x = self.head(x) # x.shape = [B,num_patches,embed_dim] - x = self.unpatchify(x) - # x.shape = [B,(num_patches*embed_dim)//(H*W),H,W] - preds = self.final(x) + preds = self.unpatchify(x) # preds.shape = [B,out_channels,H,W] return preds diff --git a/src/climate_learn/models/module.py b/src/climate_learn/models/module.py index 699a1b9d..c76287b1 100644 --- a/src/climate_learn/models/module.py +++ b/src/climate_learn/models/module.py @@ -1,8 +1,12 @@ # Standard library from typing import Callable, List, Optional, Tuple, Union +# Local application +from ..data.processing.era5_constants import CONSTANTS + # Third party import torch +import torch.nn.functional as F from torch.optim.lr_scheduler import _LRScheduler as LRScheduler import pytorch_lightning as pl @@ -44,6 +48,20 @@ def __init__( " losses which do not rqeuire transformation." ) self.test_target_transforms = test_target_transforms + self.mode = "direct" + + def set_mode(self, mode): + self.mode = mode + + def set_n_iters(self, iters): + self.n_iters = iters + + def replace_constant(self, y, yhat, out_variables): + for i in range(yhat.shape[1]): + # if constant replace with ground-truth value + if out_variables[i] in CONSTANTS: + yhat[:, i] = y[:, i] + return yhat def forward(self, x: torch.Tensor) -> torch.Tensor: return self.net(x) @@ -55,6 +73,7 @@ def training_step( ) -> torch.Tensor: x, y, in_variables, out_variables = batch yhat = self(x).to(device=y.device) + yhat = self.replace_constant(y, yhat, out_variables) if self.train_target_transform: yhat = self.train_target_transform(yhat) y = self.train_target_transform(y) @@ -63,12 +82,12 @@ def training_step( loss_dict = {} if losses.dim() == 0: # aggregate loss only loss = losses - loss_dict[f"{loss_name}:aggregate"] = loss + loss_dict[f"train/{loss_name}:aggregate"] = loss else: # per channel + aggregate for var_name, loss in zip(out_variables, losses): - loss_dict[f"{loss_name}:{var_name}"] = loss + loss_dict[f"train/{loss_name}:{var_name}"] = loss loss = losses[-1] - loss_dict[f"{loss_name}:aggregate"] = loss + loss_dict[f"train/{loss_name}:aggregate"] = loss self.log_dict( loss_dict, prog_bar=True, @@ -90,13 +109,64 @@ def test_step( batch: Tuple[torch.Tensor, torch.Tensor, List[str], List[str]], batch_idx: int, ) -> torch.Tensor: - self.evaluate(batch, "test") + if self.mode == "direct": + self.evaluate(batch, "test") + if self.mode == "iter": + self.evaluate_iter(batch, self.n_iters, "test") def evaluate( self, batch: Tuple[torch.Tensor, torch.Tensor, List[str], List[str]], stage: str ): x, y, in_variables, out_variables = batch yhat = self(x).to(device=y.device) + yhat = self.replace_constant(y, yhat, out_variables) + if stage == "val": + loss_fns = self.val_loss + transforms = self.val_target_transforms + elif stage == "test": + loss_fns = self.test_loss + transforms = self.test_target_transforms + else: + raise RuntimeError("Invalid evaluation stage") + loss_dict = {} + for i, lf in enumerate(loss_fns): + if transforms is not None and transforms[i] is not None: + yhat_ = transforms[i](yhat) + y_ = transforms[i](y) + losses = lf(yhat_, y_) + loss_name = getattr(lf, "name", f"loss_{i}") + if losses.dim() == 0: # aggregate loss + loss_dict[f"{stage}/{loss_name}:agggregate"] = losses + else: # per channel + aggregate + for var_name, loss in zip(out_variables, losses): + name = f"{stage}/{loss_name}:{var_name}" + loss_dict[name] = loss + loss_dict[f"{stage}/{loss_name}:aggregate"] = losses[-1] + self.log_dict( + loss_dict, + on_step=False, + on_epoch=True, + sync_dist=True, + batch_size=len(batch[0]), + ) + return loss_dict + + def evaluate_iter( + self, + batch: Tuple[torch.Tensor, torch.Tensor, List[str], List[str]], + n_iters: int, + stage: str, + ): + x, y, in_variables, out_variables = batch + + x_iter = x + for _ in range(n_iters): + yhat_iter = self(x_iter).to(device=x_iter.device) + yhat_iter = self.replace_constant(y, yhat_iter, out_variables) + x_iter = x_iter[:, 1:] + x_iter = torch.cat((x_iter, yhat_iter.unsqueeze(1)), dim=1) + yhat = yhat_iter + if stage == "val": loss_fns = self.val_loss transforms = self.val_target_transforms @@ -108,17 +178,20 @@ def evaluate( loss_dict = {} for i, lf in enumerate(loss_fns): if transforms is not None and transforms[i] is not None: - yhat_T = transforms[i](yhat) - y_T = transforms[i](y) - losses = lf(yhat_T, y_T) + yhat_t = transforms[i](yhat) + y_t = transforms[i](y) + else: + yhat_t = yhat + y_t = y + losses = lf(yhat_t, y_t) loss_name = getattr(lf, "name", f"loss_{i}") if losses.dim() == 0: # aggregate loss - loss_dict[f"{loss_name}:agggregate"] = losses + loss_dict[f"{stage}/{loss_name}:agggregate"] = losses else: # per channel + aggregate for var_name, loss in zip(out_variables, losses): - name = f"{loss_name}:{var_name}" + name = f"{stage}/{loss_name}:{var_name}" loss_dict[name] = loss - loss_dict[f"{loss_name}:aggregate"] = losses[-1] + loss_dict[f"{stage}/{loss_name}:aggregate"] = losses[-1] self.log_dict( loss_dict, on_step=False, @@ -131,4 +204,14 @@ def evaluate( def configure_optimizers(self): if self.lr_scheduler is None: return self.optimizer - return {"optimizer": self.optimizer, "lr_scheduler": self.lr_scheduler} + if isinstance(self.lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau): + scheduler = { + "scheduler": self.lr_scheduler, + "monitor": self.trainer.favorite_metric, + "interval": "epoch", + "frequency": 1, + "strict": True, + } + else: + scheduler = self.lr_scheduler + return {"optimizer": self.optimizer, "lr_scheduler": scheduler} diff --git a/src/climate_learn/trainer.py b/src/climate_learn/trainer.py deleted file mode 100644 index 943c8656..00000000 --- a/src/climate_learn/trainer.py +++ /dev/null @@ -1,76 +0,0 @@ -# Standard library -import logging -from warnings import warn - -# Third party -import pytorch_lightning as pl -from pytorch_lightning.callbacks import ( - EarlyStopping, - ModelCheckpoint, - RichModelSummary, - RichProgressBar, -) - -logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) - - -class Trainer(pl.Trainer): - """Wrapper for Lightning's trainer.""" - - def __init__( - self, early_stopping=None, patience=0, summary_depth=-1, seed=0, **kwargs - ): - pl.seed_everything(seed) - if "logger" not in kwargs: - kwargs["logger"] = False - if "callbacks" not in kwargs: - checkpoint_callback = ModelCheckpoint( - save_last=True, - verbose=False, - filename="epoch_{epoch:03d}", - auto_insert_metric_name=False, - ) - summary_callback = RichModelSummary(max_depth=summary_depth) - progress_callback = RichProgressBar() - callbacks = [ - checkpoint_callback, - summary_callback, - progress_callback, - ] - if early_stopping: - early_stop_callback = EarlyStopping( - monitor=early_stopping, patience=patience, verbose=False - ) - callbacks.append(early_stop_callback) - kwargs["callbacks"] = callbacks - if "strategy" not in kwargs: - if in_notebook(): - warn("In interactive environment: cannot use DDP spawn strategy") - kwargs["strategy"] = None - else: - kwargs["strategy"] = "ddp_spawn" - self.trainer = pl.Trainer(**kwargs) - - def fit(self, model_module, *args, **kwargs): - if model_module.optimizer is None: - raise RuntimeError( - "Model module has no optimizer - maybe it has no parameters?" - ) - self.trainer.fit(model_module, *args, **kwargs) - - def test(self, model_module, *args, **kwargs): - self.trainer.test(model_module, *args, **kwargs) - - -# https://stackoverflow.com/a/22424821 -def in_notebook(): - try: - from IPython import get_ipython - - if "IPKernelApp" not in get_ipython().config: # pragma: no cover - return False - except ImportError: - return False - except AttributeError: - return False - return True diff --git a/src/climate_learn/transforms/__init__.py b/src/climate_learn/transforms/__init__.py index c82ce05e..ba636543 100644 --- a/src/climate_learn/transforms/__init__.py +++ b/src/climate_learn/transforms/__init__.py @@ -1,2 +1,3 @@ from .denormalize import Denormalize +from .mask import Mask from .registry import TRANSFORMS_REGISTRY diff --git a/src/climate_learn/transforms/denormalize.py b/src/climate_learn/transforms/denormalize.py index e56c717c..96dc5d3c 100644 --- a/src/climate_learn/transforms/denormalize.py +++ b/src/climate_learn/transforms/denormalize.py @@ -3,7 +3,7 @@ # Local application from .registry import register -from ..data import DataModule, IterDataModule +from ..data import IterDataModule # Third party import torch @@ -12,14 +12,17 @@ @register("denormalize") class Denormalize: - def __init__(self, data_module: Union[DataModule, IterDataModule]): - super().__init__() + def __init__(self, data_module: IterDataModule): norm = data_module.get_out_transforms() if norm is None: raise RuntimeError("norm was 'None', did you setup the data module?") # Hotfix to work with dict style data - mean_norm = torch.tensor([norm[k].mean for k in norm.keys()]) - std_norm = torch.tensor([norm[k].std for k in norm.keys()]) + if isinstance(norm, dict): + mean_norm = torch.tensor([norm[k].mean for k in norm.keys()]) + std_norm = torch.tensor([norm[k].std for k in norm.keys()]) + else: + mean_norm = norm.mean + std_norm = norm.std std_denorm = 1 / std_norm mean_denorm = -mean_norm * std_denorm self.transform = transforms.Normalize(mean_denorm, std_denorm) diff --git a/src/climate_learn/transforms/mask.py b/src/climate_learn/transforms/mask.py new file mode 100644 index 00000000..06beb8b3 --- /dev/null +++ b/src/climate_learn/transforms/mask.py @@ -0,0 +1,20 @@ +# Standard library +from typing import Union + +# Local application +from .registry import register + +# Third party +import torch + + +@register("mask") +class Mask: + def __init__(self, mask: torch.IntTensor, val=0): + self.mask = mask + self.val = val + + def __call__(self, x) -> Union[torch.FloatTensor, torch.DoubleTensor]: + self.mask = self.mask.to(x.device) + res = torch.where(self.mask == 1, x, self.val) + return res diff --git a/src/climate_learn/utils/__init__.py b/src/climate_learn/utils/__init__.py index 2ee7be80..af87ec1a 100644 --- a/src/climate_learn/utils/__init__.py +++ b/src/climate_learn/utils/__init__.py @@ -1 +1,18 @@ -from .visualize import * +from .visualize import ( + visualize_at_index, + visualize_mean_bias, + visualize_sample, + rank_histogram, +) +from .loaders import ( + load_model_module, + load_forecasting_module, + load_downscaling_module, + load_climatebench_module, + load_architecture, + load_optimizer, + load_lr_scheduler, + load_loss, + load_transform, +) +from .mc_dropout import get_monte_carlo_predictions diff --git a/src/climate_learn/utils/data.py b/src/climate_learn/utils/data.py deleted file mode 100644 index c79c49de..00000000 --- a/src/climate_learn/utils/data.py +++ /dev/null @@ -1,28 +0,0 @@ -# Standard library -import os - -# Third party -from IPython.display import display -import xarray as xr - - -def load_dataset(dir): - """ - Loads a dataset from a directory of NetCDF files. - - :param dir: The directory to open. - :type dir: str - :return: An xarray dataset object. - :rtype: xarray.Dataset - """ - return xr.open_mfdataset(os.path.join(dir, "*.nc")) - - -def view(dataset): - """ - Displays the given dataset in the current IPython notebook. - - :param dataset: The dataset to show. - :type dataset: xarray.Dataset - """ - display(dataset) diff --git a/src/climate_learn/utils/datetime.py b/src/climate_learn/utils/datetime.py deleted file mode 100644 index d3fae5fa..00000000 --- a/src/climate_learn/utils/datetime.py +++ /dev/null @@ -1,58 +0,0 @@ -Year = int -"""A type definition for representing years.""" - - -class Days: - """A data object that represents a number of days. - - :param value: A number of days. - :type value: int|float - """ - - def __init__(self, value): - """Constructor method""" - self.value = value - - def days(self): - """Getter method. - - :return: The number of days represented by this object. - :rtype: int|float - """ - return self.value - - def hours(self): - """Getter method. - - :return: The number of hours represented by this object. - :rtype: int - """ - return int(self.value * 24) - - -class Hours: - """A data object that represents a number of hours. - - :param value: A number of hours. - :type value: int|float - """ - - def __init__(self, value): - """Constructor method""" - self.value = value - - def days(self): - """Getter method. - - :return: The number of days represented by this object. - :rtype: int - """ - return self.value // 24 - - def hours(self): - """Getter method. - - :return: The number of hours represented by this object. - :rtype: int|float - """ - return self.value diff --git a/src/climate_learn/loaders.py b/src/climate_learn/utils/loaders.py similarity index 70% rename from src/climate_learn/loaders.py rename to src/climate_learn/utils/loaders.py index d67925ab..900b5d9e 100644 --- a/src/climate_learn/loaders.py +++ b/src/climate_learn/utils/loaders.py @@ -1,21 +1,23 @@ # Standard library -from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Union +from typing import Any, Callable, Dict, Iterable, Optional, Union from functools import partial import warnings # Local application -from .data import DataModule, IterDataModule -from .models import LitModule, MODEL_REGISTRY -from .models.hub import ( +from ..data import IterDataModule +from ..models import LitModule, MODEL_REGISTRY +from ..models.hub import ( Climatology, Interpolation, LinearRegression, Persistence, ResNet, + Unet, + VisionTransformer, ) -from .models.lr_scheduler import LinearWarmupCosineAnnealingLR -from .transforms import TRANSFORMS_REGISTRY -from .metrics import MetricsMetaInfo, METRICS_REGISTRY +from ..models.lr_scheduler import LinearWarmupCosineAnnealingLR +from ..transforms import TRANSFORMS_REGISTRY +from ..metrics import MetricsMetaInfo, METRICS_REGISTRY # Third party import torch @@ -25,8 +27,8 @@ def load_model_module( task: str, - data_module: Union[DataModule, IterDataModule], - preset: Optional[str] = None, + data_module, + architecture: Optional[str] = None, model: Optional[Union[str, nn.Module]] = None, model_kwargs: Optional[Dict[str, Any]] = None, optim: Optional[Union[str, torch.optim.Optimizer]] = None, @@ -46,11 +48,11 @@ def load_model_module( if lat is None and lon is None: raise RuntimeError("Data module has not been set up yet.") # Load the model - if preset is None and model is None: - raise RuntimeError("Please specify 'preset' or 'model'") - elif preset: - print(f"Loading preset: {preset}") - model, optimizer, lr_scheduler = load_preset(task, data_module, preset) + if architecture is None and model is None: + raise RuntimeError("Please specify 'architecture' or 'model'") + elif architecture: + print(f"Loading architecture: {architecture}") + model, optimizer, lr_scheduler = load_architecture(task, data_module, architecture) elif isinstance(model, str): print(f"Loading model: {model}") model_cls = MODEL_REGISTRY.get(model, None) @@ -66,29 +68,35 @@ def load_model_module( else: raise TypeError("'model' must be str or nn.Module") # Load the optimizer - if preset is None and optim is None: - raise RuntimeError("Please specify 'preset' or 'optim'") - elif preset: - print("Using preset optimizer") + if architecture is None and optim is None: + raise RuntimeError("Please specify 'architecture' or 'optim'") + elif architecture: + print("Using optimizer associated with architecture") elif isinstance(optim, str): print(f"Loading optimizer {optim}") optimizer = load_optimizer(model, optim, optim_kwargs) elif isinstance(optim, torch.optim.Optimizer): + optimizer = optim print("Using custom optimizer") else: raise TypeError("'optim' must be str or torch.optim.Optimizer") - # Load the LR scheduler - if preset is None and sched is None: - raise RuntimeError("Please specify 'preset' or 'sched'") - elif preset: - print("Using preset learning rate scheduler") + # Load the LR scheduler, if specified + if architecture: + print("Using learning rate scheduler associated with architecture") + elif sched is None: + lr_scheduler = None elif isinstance(sched, str): print(f"Loading learning rate scheduler: {sched}") lr_scheduler = load_lr_scheduler(sched, optimizer, sched_kwargs) - elif isinstance(sched, LRScheduler): + elif isinstance(sched, LRScheduler) or isinstance( + sched, torch.optim.lr_scheduler.ReduceLROnPlateau + ): + lr_scheduler = sched print("Using custom learning rate scheduler") else: - raise TypeError("'sched' must be str or torch.optim.lr_scheduler._LRScheduler") + raise TypeError( + "'sched' must be str, None, or torch.optim.lr_scheduler._LRScheduler" + ) # Load training loss in_vars, out_vars = get_data_variables(data_module) lat, lon = data_module.get_lat_lon() @@ -101,6 +109,18 @@ def load_model_module( print("Using custom training loss") else: raise TypeError("'train_loss' must be str or Callable") + # Load training transform + if isinstance(train_target_transform, str): + print(f"Loading training transform: {train_target_transform}") + train_transform = load_transform(train_target_transform, data_module) + elif isinstance(train_target_transform, Callable): + print("Using custom training transform") + train_transform = train_target_transform + elif train_target_transform is None: + print("No train transform") + train_transform = train_target_transform + else: + raise TypeError("'train_target_transform' must be str, callable, or None") # Load validation loss if not isinstance(val_loss, Iterable): raise TypeError("'val_loss' must be an iterable") @@ -116,32 +136,6 @@ def load_model_module( val_losses.append(vl) else: raise TypeError("each 'val_loss' must be str or Callable") - # Load test loss - if not isinstance(test_loss, Iterable): - raise TypeError("'test_loss' must be an iterable") - test_losses = [] - for tl in test_loss: - if isinstance(tl, str): - clim = get_climatology(data_module, "test") - metainfo = MetricsMetaInfo(in_vars, out_vars, lat, lon, clim) - print(f"Loading validation loss: {tl}") - test_losses.append(load_loss(tl, False, metainfo)) - elif isinstance(tl, Callable): - print("Using custom validation loss") - test_losses.append(tl) - else: - raise TypeError("each 'test_loss' must be str or Callable") - # Load training transform - if isinstance(train_target_transform, str): - print(f"Loading training transform: {train_target_transform}") - train_transform = load_transform(train_target_transform, data_module) - elif isinstance(train_target_transform, Callable): - print("Using custom training transform") - train_transform = train_target_transform - elif train_target_transform is None: - train_transform = train_target_transform - else: - raise TypeError("'train_target_transform' must be str, callable, or None") # Load validation transform val_transforms = [] if isinstance(val_target_transform, Iterable): @@ -152,8 +146,11 @@ def load_model_module( elif isinstance(vt, Callable): print("Using custom validation transform") val_transforms.append(vt) + elif vt is None: + print("No validation transform") + val_transforms.append(None) else: - raise TypeError("each 'val_transform' must be str or Callable") + raise TypeError("each 'val_transform' must be str, Callable, or None") elif val_target_transform is None: val_transforms = val_target_transform else: @@ -161,18 +158,36 @@ def load_model_module( "'val_target_transform' must be an iterable of strings/callables," " or None" ) + # Load test loss + if not isinstance(test_loss, Iterable): + raise TypeError("'test_loss' must be an iterable") + test_losses = [] + for tl in test_loss: + if isinstance(tl, str): + clim = get_climatology(data_module, "test") + metainfo = MetricsMetaInfo(in_vars, out_vars, lat, lon, clim) + print(f"Loading test loss: {tl}") + test_losses.append(load_loss(tl, False, metainfo)) + elif isinstance(tl, Callable): + print("Using custom testing loss") + test_losses.append(tl) + else: + raise TypeError("each 'test_loss' must be str or Callable") # Load test transform test_transforms = [] if isinstance(test_target_transform, Iterable): for tt in test_target_transform: if isinstance(tt, str): - print(f"Loading validation transform: {tt}") + print(f"Loading test transform: {tt}") test_transforms.append(load_transform(tt, data_module)) elif isinstance(tt, Callable): - print("Using custom validation transform") + print("Using custom test transform") test_transforms.append(tt) + elif tt is None: + print("No test transform") + test_transforms.append(None) else: - raise TypeError("each 'test_transform' must be str or Callable") + raise TypeError("each 'test_transform' must be str, Callable, or None") elif test_target_transform is None: test_transforms = test_target_transform else: @@ -199,43 +214,58 @@ def load_model_module( load_model_module, task="forecasting", train_loss="lat_mse", - val_loss=["lat_rmse", "lat_acc"], + val_loss=["lat_rmse", "lat_acc", "lat_mse"], test_loss=["lat_rmse", "lat_acc"], train_target_transform=None, - val_target_transform=["denormalize", "denormalize"], + val_target_transform=["denormalize", "denormalize", None], test_target_transform=["denormalize", "denormalize"], ) +load_climatebench_module = partial( + load_model_module, + task="forecasting", + train_loss="mse", + val_loss=["mse"], + test_loss=["lat_nrmses", "lat_nrmseg", "lat_nrmse"], + train_target_transform=None, + val_target_transform=[nn.Identity()], + test_target_transform=[nn.Identity(), nn.Identity(), nn.Identity()], +) + load_downscaling_module = partial( load_model_module, task="downscaling", train_loss="mse", - val_loss=["rmse", "pearson", "mean_bias"], + val_loss=["rmse", "pearson", "mean_bias", "mse"], test_loss=["rmse", "pearson", "mean_bias"], train_target_transform=None, - val_target_transform=["denormalize", "denormalize"], - test_target_transform=["denormalize", "denormalize"], + val_target_transform=["denormalize", "denormalize", "denormalize", None], + test_target_transform=["denormalize", "denormalize", "denormalize"], ) -def load_preset(task, data_module, preset): +def load_architecture(task, data_module, architecture): in_vars, out_vars = get_data_variables(data_module) in_shape, out_shape = get_data_dims(data_module) def raise_not_impl(): raise NotImplementedError( - f"{preset} is not an implemented preset for the {task} task. If" - " you think it should be, please raise an issue at" + f"{architecture} is not an implemented architecture for the {task}" + " task. If you think it should be, please raise an issue at" " https://github.com/aditya-grover/climate-learn/issues." ) if task == "forecasting": history, in_channels, in_height, in_width = in_shape[1:] out_channels, out_height, out_width = out_shape[1:] - if preset.lower() == "climatology": - model = Climatology(get_climatology(data_module, "train")) + if architecture.lower() == "climatology": + norm = data_module.get_out_transforms() + mean_norm = torch.tensor([norm[k].mean for k in norm.keys()]) + std_norm = torch.tensor([norm[k].std for k in norm.keys()]) + clim = get_climatology(data_module, "train") + model = Climatology(clim, mean_norm, std_norm) optimizer = lr_scheduler = None - elif preset == "persistence": + elif architecture == "persistence": if not set(out_vars).issubset(in_vars): raise RuntimeError( "Persistence requires the output variables to be a subset of" @@ -244,13 +274,13 @@ def raise_not_impl(): channels = [in_vars.index(o) for o in out_vars] model = Persistence(channels) optimizer = lr_scheduler = None - elif preset.lower() == "linear-regression": + elif architecture.lower() == "linear-regression": in_features = history * in_channels * in_height * in_width out_features = out_channels * out_height * out_width model = LinearRegression(in_features, out_features) optimizer = load_optimizer(model, "SGD", {"lr": 1e-5}) lr_scheduler = None - elif preset.lower() == "rasp-theurey-2020": + elif architecture.lower() == "rasp-theurey-2020": model = ResNet( in_channels=in_channels, out_channels=out_channels, @@ -270,7 +300,7 @@ def raise_not_impl(): elif task == "downscaling": in_channels, in_height, in_width = in_shape[1:] out_channels, out_height, out_width = out_shape[1:] - if preset.lower() in ( + if architecture.lower() in ( "bilinear-interpolation", "nearest-interpolation", ): @@ -279,11 +309,48 @@ def raise_not_impl(): "Interpolation requires the output variables to match the" " input variables." ) - interpolation_mode = preset.split("-")[0] - model = Interpolation(out_height * out_width, interpolation_mode) + interpolation_mode = architecture.split("-")[0] + model = Interpolation((out_height, out_width), interpolation_mode) optimizer = lr_scheduler = None else: - raise_not_impl() + if architecture == "resnet": + backbone = ResNet(in_channels, out_channels, n_blocks=28) + elif architecture == "unet": + backbone = Unet( + in_channels, out_channels, ch_mults=[1, 1, 2], n_blocks=4 + ) + elif architecture == "vit": + backbone = VisionTransformer( + (64, 128), + in_channels, + out_channels, + history=1, + patch_size=2, + learn_pos_emb=True, + embed_dim=128, + depth=4, + decoder_depth=1, + num_heads=4, + mlp_ratio=4, + ) + else: + raise_not_impl() + model = nn.Sequential( + Interpolation((out_height, out_width), "bilinear"), backbone + ) + optimizer = load_optimizer( + model, "adamw", {"lr": 1e-5, "weight_decay": 1e-5, "betas": (0.9, 0.99)} + ) + lr_scheduler = load_lr_scheduler( + "linear-warmup-cosine-annealing", + optimizer, + { + "warmup_epochs": 5, + "max_epochs": 50, + "warmup_start_lr": 1e-8, + "eta_min": 1e-8, + }, + ) return model, optimizer, lr_scheduler @@ -320,6 +387,10 @@ def load_lr_scheduler( lr_scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, **sched_kwargs) elif sched == "linear-warmup-cosine-annealing": lr_scheduler = LinearWarmupCosineAnnealingLR(optimizer, **sched_kwargs) + elif sched == "reduce-lr-on-plateau": + lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( + optimizer, **sched_kwargs + ) else: raise NotImplementedError( f"{sched} is not an implemented learning rate scheduler. If you" @@ -354,16 +425,11 @@ def load_transform(transform_name, data_module): def get_data_dims(data_module): - for batch in data_module.train_dataloader(): - x, y, _, _ = batch - break - return x.shape, y.shape + return data_module.get_data_dims() def get_data_variables(data_module): - in_vars = data_module.train_dataset.task.in_vars - out_vars = data_module.train_dataset.task.out_vars - return in_vars, out_vars + return data_module.get_data_variables() def get_climatology(data_module, split): @@ -371,5 +437,6 @@ def get_climatology(data_module, split): if clim is None: raise RuntimeError("Climatology has not yet been set.") # Hotfix to work with dict style data - clim = torch.stack(tuple(clim.values())) + if isinstance(clim, dict): + clim = torch.stack(tuple(clim.values())) return clim diff --git a/src/climate_learn/utils/mc_dropout.py b/src/climate_learn/utils/mc_dropout.py new file mode 100644 index 00000000..af506403 --- /dev/null +++ b/src/climate_learn/utils/mc_dropout.py @@ -0,0 +1,19 @@ +import torch + + +def enable_dropout(model_module): + for m in model_module.modules(): + if m._get_name() == "Dropout": + m.train() + + +def get_monte_carlo_predictions(batch, model_module, n_ensemble_members): + model_module.eval() + enable_dropout(model_module) + ensemble_predictions = [] + for _ in range(n_ensemble_members): + with torch.no_grad(): + prediction = model_module.forward(batch) + ensemble_predictions.append(prediction) + ensemble_predictions = torch.stack(ensemble_predictions) + return ensemble_predictions diff --git a/src/climate_learn/utils/visualize.py b/src/climate_learn/utils/visualize.py index 7dc5b9f9..2a51e4c5 100644 --- a/src/climate_learn/utils/visualize.py +++ b/src/climate_learn/utils/visualize.py @@ -1,168 +1,185 @@ -# Standard library -from datetime import datetime -import os -import random - -# Third party import matplotlib.pyplot as plt +import matplotlib.animation as animation import numpy as np -import torch +from scipy.stats import rankdata from tqdm import tqdm - -from climate_learn.data.task import Downscaling, Forecasting -from climate_learn.data.dataset import MapDataset - -# TODO: include exceptions in docstrings - - -def interpolate_input(x: torch.Tensor, y: torch.Tensor): - # interpolate input to match output size - out_h, out_w = y.shape[-2], y.shape[-1] - x = torch.nn.functional.interpolate(x, (out_h, out_w), mode="bilinear") - return x - - -def visualize(model_module, data_module, split="test", samples=2, save_dir=None): - """Visualizes model bias. - - :param model_module: A ClimateLearn model. - :type model_module: LightningModule - :param data_module: A ClimateLearn dataset. - :type data_module: LightningDataModule - :param split: "train", "val", or "test". - :type split: str, optional - :param samples: The exact days or the number of days to visualize. If provided as - exact days, this should be a list of datetime strings, each formatted as - "YYYY-mm-dd:HH". If provided as the number of days, it must be an int n. In - this case, n days are randomly sampled from the given split. - :type samples: List[str]|int, optional - :param save_dir: The directory to save the visualization to. Defaults to `None`, - meaning the visualization is not saved. - :type save_dir: str, optional - """ - if save_dir is not None: - os.makedirs(save_dir, exist_ok=True) - - # dataset.setup() - task_dataset = eval(f"data_module.{split}_dataset") - if not isinstance(task_dataset, MapDataset): - raise RuntimeError(f"visualize is supported only for Map style datasets") - - if type(samples) == int: - idxs = random.sample(range(0, len(task_dataset)), samples) - elif type(samples) == list: - time = task_dataset.get_time() - idxs = [ - np.searchsorted(time, np.datetime64(datetime.strptime(dt, "%Y-%m-%d:%H"))) - for dt in samples - ] +from ..data.processing.era5_constants import VAR_TO_UNIT as ERA5_VAR_TO_UNIT +from ..data.processing.cmip6_constants import VAR_TO_UNIT as CMIP6_VAR_TO_UNIT + + +def visualize_at_index(mm, dm, in_transform, out_transform, variable, src, index=0): + lat, lon = dm.get_lat_lon() + extent = [lon.min(), lon.max(), lat.min(), lat.max()] + channel = dm.hparams.out_vars.index(variable) + history = dm.hparams.history + if src == "era5": + variable_with_units = f"{variable} ({ERA5_VAR_TO_UNIT[variable]})" + elif src == "cmip6": + variable_with_units = f"{variable} ({CMIP6_VAR_TO_UNIT[variable]})" + elif src == "prism": + variable_with_units = f"Daily Max Temperature (C)" else: - raise Exception( - "Invalid type for samples; Allowed int or list[datetime.datetime or np.datetime64]" + raise NotImplementedError(f"{src} is not a supported source") + + counter = 0 + adj_index = None + for batch in tqdm(dm.test_dataloader()): + x, y = batch[:2] + batch_size = x.shape[0] + if index in range(counter, counter + batch_size): + adj_index = index - counter + x = x.to(mm.device) + pred = mm.forward(x) + break + counter += batch_size + + if adj_index is None: + raise RuntimeError("Given index could not be found") + xx = x[adj_index] + if dm.hparams.task == "continuous-forecasting": + xx = xx[:, :-1] + + # Create animation/plot of the input sequence + if history > 1: + in_fig, in_ax = plt.subplots() + in_ax.set_title(f"Input Sequence: {variable_with_units}") + in_ax.set_xlabel("Longitude") + in_ax.set_ylabel("Latitude") + imgs = [] + for time_step in range(history): + img = in_transform(xx[time_step])[channel].detach().cpu().numpy() + if src == "era5": + img = np.flip(img, 0) + img = in_ax.imshow(img, cmap=plt.cm.coolwarm, animated=True, extent=extent) + imgs.append([img]) + cax = in_fig.add_axes( + [ + in_ax.get_position().x1 + 0.02, + in_ax.get_position().y0, + 0.02, + in_ax.get_position().y1 - in_ax.get_position().y0, + ] ) - - fig, axes = plt.subplots(len(idxs), 4, figsize=(30, 3 * len(idxs)), squeeze=False) - - for index, idx in enumerate(idxs): - x, y, const = task_dataset[idx] # 1, 1, 32, 64 - ## Hotfix merging constants data with input data - x = {**x, **const} - x = torch.stack(tuple(x.values())) - ## Handles the case for forecasting input as it has history in it - if len(x.size()) == 4: - x = torch.transpose(x, 0, 1) - y = torch.stack(tuple(x.values())) - - if len(x.shape) == 3: - x = x.unsqueeze(0) - x = interpolate_input(x, y) - pred = model_module.forward(x.unsqueeze(0)) # 1, 1, 32, 64 - - inv_normalize = model_module.denormalization - init_condition, gt = inv_normalize(x), inv_normalize(y) - init_condition = np.flip(init_condition.detach().cpu().squeeze().numpy(), 0) - pred = inv_normalize(pred) - pred = np.flip(pred.detach().cpu().squeeze().numpy(), 0) - gt = np.flip(gt.detach().cpu().squeeze().numpy(), 0) - bias = pred - gt - - for i, np_array in enumerate([init_condition, gt, pred, bias]): - ax = axes[index][i] - im = ax.imshow(np_array) - im.set_cmap(cmap=plt.cm.coolwarm) - fig.colorbar(im, ax=ax) - - if isinstance(task_dataset, Forecasting): - axes[index][0].set_title("Initial condition [Kelvin]") - axes[index][1].set_title("Ground truth [Kelvin]") - axes[index][2].set_title("Prediction [Kelvin]") - axes[index][3].set_title("Bias [Kelvin]") - elif isinstance(task_dataset, Downscaling): - axes[index][0].set_title("Low resolution data [Kelvin]") - axes[index][1].set_title("High resolution data [Kelvin]") - axes[index][2].set_title("Downscaled [Kelvin]") - axes[index][3].set_title("Bias [Kelvin]") - else: - raise NotImplementedError - - fig.tight_layout() - - if save_dir is not None: - plt.savefig(os.path.join(save_dir, "visualize.png")) + in_fig.colorbar(in_ax.get_images()[0], cax=cax) + anim = animation.ArtistAnimation(in_fig, imgs, interval=1000, repeat_delay=2000) + plt.close() else: + if dm.hparams.task == "downscaling": + img = in_transform(xx)[channel].detach().cpu().numpy() + else: + img = in_transform(xx[0])[channel].detach().cpu().numpy() + if src == "era5": + img = np.flip(img, 0) + visualize_sample(img, extent, f"Input: {variable_with_units}") + anim = None plt.show() - -def visualize_mean_bias(model_module, data_module, save_dir=None): - """Visualizes mean model bias on the test set. - - :param model_module: A ClimateLearn model. - :type model_module: LightningModule - :param data_module: A ClimateLearn dataset. - :type data_module: LightningDataModule - :param save_dir: The directory to save the visualization to. Defaults to `None`, - meaning the visualization is not saved. - :type save_dir: str, optional - """ - if save_dir is not None: - os.makedirs(save_dir, exist_ok=True) - - loader = data_module.test_dataloader() - - all_mean_bias = [] - for batch in tqdm(loader): - x, y, _, _ = batch # B, 1, 32, 64 - x = x.to(model_module.device) - y = y.to(model_module.device) - if len(x.shape) == 5: - x = x.squeeze(1) - x = interpolate_input(x, y) - pred = model_module.forward(x) # B, 1, 32, 64 - - inv_normalize = model_module.denormalization - init_condition, gt = inv_normalize(x), inv_normalize(y) - init_condition = np.flip(init_condition.detach().cpu().numpy(), 2) - pred = inv_normalize(pred) - pred = np.flip(pred.detach().cpu().numpy(), 2) - gt = np.flip(gt.detach().cpu().numpy(), 2) - bias = pred - gt # B, 1, 32, 64 - mean_bias = np.mean(bias, axis=0) - all_mean_bias.append(mean_bias) - - all_mean_bias = np.stack(all_mean_bias, axis=0) - mean_bias = np.mean(all_mean_bias, axis=0) - - fig, axes = plt.subplots(1, 1, figsize=(12, 4), squeeze=False) - ax = axes[0, 0] - - im = ax.imshow(mean_bias.squeeze()) - im.set_cmap(cmap=plt.cm.coolwarm) - fig.colorbar(im, ax=ax) - ax.set_title("Mean bias [Kelvin]") - - fig.tight_layout() - - if save_dir is not None: - plt.savefig(os.path.join(save_dir, "visualize_mean_bias.png")) + # Plot the ground truth + yy = out_transform(y[adj_index]) + yy = yy[channel].detach().cpu().numpy() + if src == "era5": + yy = np.flip(yy, 0) + visualize_sample(yy, extent, f"Ground truth: {variable_with_units}") + plt.show() + + # Plot the prediction + ppred = out_transform(pred[adj_index]) + ppred = ppred[channel].detach().cpu().numpy() + if src == "era5": + ppred = np.flip(ppred, 0) + visualize_sample(ppred, extent, f"Prediction: {variable_with_units}") + plt.show() + + # Plot the bias + bias = ppred - yy + visualize_sample(bias, extent, f"Bias: {variable_with_units}") + plt.show() + + # None, if no history + return anim + + +def visualize_sample(img, extent, title): + fig, ax = plt.subplots() + ax.set_title(title) + ax.set_xlabel("Longitude") + ax.set_ylabel("Latitude") + cmap = plt.cm.coolwarm + cmap.set_bad("black", 1) + ax.imshow(img, cmap=cmap, extent=extent) + cax = fig.add_axes( + [ + ax.get_position().x1 + 0.02, + ax.get_position().y0, + 0.02, + ax.get_position().y1 - ax.get_position().y0, + ] + ) + fig.colorbar(ax.get_images()[0], cax=cax) + return (fig, ax) + + +def visualize_mean_bias(dm, mm, out_transform, variable, src): + lat, lon = dm.get_lat_lon() + extent = [lon.min(), lon.max(), lat.min(), lat.max()] + channel = dm.hparams.out_vars.index(variable) + if src == "era5": + variable_with_units = f"{variable} ({ERA5_VAR_TO_UNIT[variable]})" + elif src == "cmip6": + variable_with_units = f"{variable} ({CMIP6_VAR_TO_UNIT[variable]})" + elif src == "prism": + variable_with_units = f"Daily Max Temperature (C)" else: - plt.show() + raise NotImplementedError(f"{src} is not a supported source") + + all_biases = [] + for batch in tqdm(dm.test_dataloader()): + x, y = batch[:2] + x = x.to(mm.device) + y = y.to(mm.device) + pred = mm.forward(x) + pred = out_transform(pred)[:, channel].detach().cpu().numpy() + obs = out_transform(y)[:, channel].detach().cpu().numpy() + bias = pred - obs + all_biases.append(bias) + + fig, ax = plt.subplots() + all_biases = np.concatenate(all_biases) + mean_bias = np.mean(all_biases, axis=0) + if src == "era5": + mean_bias = np.flip(mean_bias, 0) + ax.imshow(mean_bias, cmap=plt.cm.coolwarm, extent=extent) + ax.set_title(f"Mean Bias: {variable_with_units}") + + cax = fig.add_axes( + [ + ax.get_position().x1 + 0.02, + ax.get_position().y0, + 0.02, + ax.get_position().y1 - ax.get_position().y0, + ] + ) + fig.colorbar(ax.get_images()[0], cax=cax) + plt.show() + + +# based on https://github.com/oliverangelil/rankhistogram/tree/master +def rank_histogram(obs, ensemble, channel): + obs = obs.numpy()[:, channel] + ensemble = ensemble.numpy()[:, :, channel] + combined = np.vstack((obs[np.newaxis], ensemble)) + ranks = np.apply_along_axis(lambda x: rankdata(x, method="min"), 0, combined) + ties = np.sum(ranks[0] == ranks[1:], axis=0) + ranks = ranks[0] + tie = np.unique(ties) + for i in range(1, len(tie)): + idx = ranks[ties == tie[i]] + ranks[ties == tie[i]] = [ + np.random.randint(idx[j], idx[j] + tie[i] + 1, tie[i])[0] + for j in range(len(idx)) + ] + hist = np.histogram( + ranks, bins=np.linspace(0.5, combined.shape[0] + 0.5, combined.shape[0] + 1) + ) + plt.bar(range(1, ensemble.shape[0] + 2), hist[0]) + plt.show() diff --git a/tests/data/climate_dataset/args/test_climate_dataset_args.py b/tests/data/experimental/climate_dataset/args/test_climate_dataset_args.py similarity index 84% rename from tests/data/climate_dataset/args/test_climate_dataset_args.py rename to tests/data/experimental/climate_dataset/args/test_climate_dataset_args.py index dfc9fd70..066e2030 100644 --- a/tests/data/climate_dataset/args/test_climate_dataset_args.py +++ b/tests/data/experimental/climate_dataset/args/test_climate_dataset_args.py @@ -1,6 +1,8 @@ from climate_learn.data.climate_dataset.args import ClimateDatasetArgs +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestClimateDatasetArgsInstantiation: def test_initialization(self): ClimateDatasetArgs( diff --git a/tests/data/climate_dataset/args/test_era5_args.py b/tests/data/experimental/climate_dataset/args/test_era5_args.py similarity index 85% rename from tests/data/climate_dataset/args/test_era5_args.py rename to tests/data/experimental/climate_dataset/args/test_era5_args.py index 23825bcc..98dfb491 100644 --- a/tests/data/climate_dataset/args/test_era5_args.py +++ b/tests/data/experimental/climate_dataset/args/test_era5_args.py @@ -1,6 +1,8 @@ from climate_learn.data.climate_dataset.args import ERA5Args +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestERA5ArgsInstantiation: def test_initialization(self): ERA5Args( diff --git a/tests/data/climate_dataset/args/test_stacked_climate_dataset_args.py b/tests/data/experimental/climate_dataset/args/test_stacked_climate_dataset_args.py similarity index 91% rename from tests/data/climate_dataset/args/test_stacked_climate_dataset_args.py rename to tests/data/experimental/climate_dataset/args/test_stacked_climate_dataset_args.py index 5d065b90..441c09be 100644 --- a/tests/data/climate_dataset/args/test_stacked_climate_dataset_args.py +++ b/tests/data/experimental/climate_dataset/args/test_stacked_climate_dataset_args.py @@ -3,8 +3,10 @@ ERA5Args, StackedClimateDatasetArgs, ) +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestStackedClimateDatasetArgsInstantiation: def test_initialization(self): data_args = [] diff --git a/tests/data/climate_dataset/test_climate_dataset.py b/tests/data/experimental/climate_dataset/test_climate_dataset.py similarity index 86% rename from tests/data/climate_dataset/test_climate_dataset.py rename to tests/data/experimental/climate_dataset/test_climate_dataset.py index f30fe033..c7294682 100644 --- a/tests/data/climate_dataset/test_climate_dataset.py +++ b/tests/data/experimental/climate_dataset/test_climate_dataset.py @@ -1,6 +1,8 @@ from climate_learn.data.climate_dataset import ClimateDatasetArgs, ClimateDataset +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestClimateDatasetInstantiation: def test_initialization(self): ClimateDataset( diff --git a/tests/data/climate_dataset/test_era5.py b/tests/data/experimental/climate_dataset/test_era5.py similarity index 86% rename from tests/data/climate_dataset/test_era5.py rename to tests/data/experimental/climate_dataset/test_era5.py index 6973cb3f..5aa64376 100644 --- a/tests/data/climate_dataset/test_era5.py +++ b/tests/data/experimental/climate_dataset/test_era5.py @@ -1,6 +1,8 @@ from climate_learn.data.climate_dataset import ERA5Args, ERA5 +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestERA5Instantiation: def test_initialization(self): ERA5( diff --git a/tests/data/climate_dataset/test_stacked_climate_dataset.py b/tests/data/experimental/climate_dataset/test_stacked_climate_dataset.py similarity index 93% rename from tests/data/climate_dataset/test_stacked_climate_dataset.py rename to tests/data/experimental/climate_dataset/test_stacked_climate_dataset.py index 0d351c7b..49d73cd0 100644 --- a/tests/data/climate_dataset/test_stacked_climate_dataset.py +++ b/tests/data/experimental/climate_dataset/test_stacked_climate_dataset.py @@ -6,8 +6,10 @@ StackedClimateDatasetArgs, StackedClimateDataset, ) +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestStackedClimateDatasetInstantiation: def test_initialization(self): data_args = [] diff --git a/tests/data/dataset/args/test_map_dataset_args.py b/tests/data/experimental/dataset/args/test_map_dataset_args.py similarity index 92% rename from tests/data/dataset/args/test_map_dataset_args.py rename to tests/data/experimental/dataset/args/test_map_dataset_args.py index bd7c4fd4..a9626590 100644 --- a/tests/data/dataset/args/test_map_dataset_args.py +++ b/tests/data/experimental/dataset/args/test_map_dataset_args.py @@ -1,8 +1,10 @@ from climate_learn.data.climate_dataset.args import ClimateDatasetArgs from climate_learn.data.task.args import TaskArgs from climate_learn.data.dataset.args import MapDatasetArgs +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestMapDatasetArgsInstantiation: def test_initialization(self): climate_dataset_args = ClimateDatasetArgs( diff --git a/tests/data/dataset/args/test_shard_dataset_args.py b/tests/data/experimental/dataset/args/test_shard_dataset_args.py similarity index 92% rename from tests/data/dataset/args/test_shard_dataset_args.py rename to tests/data/experimental/dataset/args/test_shard_dataset_args.py index 769b555e..fc1fb008 100644 --- a/tests/data/dataset/args/test_shard_dataset_args.py +++ b/tests/data/experimental/dataset/args/test_shard_dataset_args.py @@ -1,8 +1,10 @@ from climate_learn.data.climate_dataset.args import ClimateDatasetArgs from climate_learn.data.task.args import TaskArgs from climate_learn.data.dataset.args import ShardDatasetArgs +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestShardDatasetArgsInstantiation: def test_initialization(self): climate_dataset_args = ClimateDatasetArgs( diff --git a/tests/data/dataset/test_map_dataset.py b/tests/data/experimental/dataset/test_map_dataset.py similarity index 92% rename from tests/data/dataset/test_map_dataset.py rename to tests/data/experimental/dataset/test_map_dataset.py index 60598ee2..1e9f14cf 100644 --- a/tests/data/dataset/test_map_dataset.py +++ b/tests/data/experimental/dataset/test_map_dataset.py @@ -1,8 +1,10 @@ from climate_learn.data.climate_dataset.args import ClimateDatasetArgs from climate_learn.data.task.args import TaskArgs from climate_learn.data.dataset import MapDatasetArgs, MapDataset +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestMapDatasetInstantiation: def test_initialization(self): climate_dataset_args = ClimateDatasetArgs( diff --git a/tests/data/dataset/test_shard_dataset.py b/tests/data/experimental/dataset/test_shard_dataset.py similarity index 92% rename from tests/data/dataset/test_shard_dataset.py rename to tests/data/experimental/dataset/test_shard_dataset.py index c07e7dc2..b32105bc 100644 --- a/tests/data/dataset/test_shard_dataset.py +++ b/tests/data/experimental/dataset/test_shard_dataset.py @@ -1,8 +1,10 @@ from climate_learn.data.climate_dataset.args import ClimateDatasetArgs from climate_learn.data.task.args import TaskArgs from climate_learn.data.dataset import ShardDatasetArgs, ShardDataset +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestShardDatasetInstantiation: def test_initialization(self): climate_dataset_args = ClimateDatasetArgs( diff --git a/tests/data/task/args/test_downscaling_args.py b/tests/data/experimental/task/args/test_downscaling_args.py similarity index 85% rename from tests/data/task/args/test_downscaling_args.py rename to tests/data/experimental/task/args/test_downscaling_args.py index 9d5e5d52..2cea8ce0 100644 --- a/tests/data/task/args/test_downscaling_args.py +++ b/tests/data/experimental/task/args/test_downscaling_args.py @@ -1,6 +1,8 @@ from climate_learn.data.task.args import DownscalingArgs +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestDownscalingArgsInstantiation: def test_initialization(self): DownscalingArgs( diff --git a/tests/data/task/args/test_forecasting_args.py b/tests/data/experimental/task/args/test_forecasting_args.py similarity index 87% rename from tests/data/task/args/test_forecasting_args.py rename to tests/data/experimental/task/args/test_forecasting_args.py index aa85afe1..7d5f5609 100644 --- a/tests/data/task/args/test_forecasting_args.py +++ b/tests/data/experimental/task/args/test_forecasting_args.py @@ -1,6 +1,8 @@ from climate_learn.data.task.args import ForecastingArgs +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestForecastingArgsInstantiation: def test_initialization(self): ForecastingArgs( diff --git a/tests/data/task/args/test_task_args.py b/tests/data/experimental/task/args/test_task_args.py similarity index 85% rename from tests/data/task/args/test_task_args.py rename to tests/data/experimental/task/args/test_task_args.py index 5ff9fbec..88009c9d 100644 --- a/tests/data/task/args/test_task_args.py +++ b/tests/data/experimental/task/args/test_task_args.py @@ -1,6 +1,8 @@ from climate_learn.data.task.args import TaskArgs +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestTaskArgsInstantiation: def test_initialization(self): TaskArgs( diff --git a/tests/data/task/test_downscaling.py b/tests/data/experimental/task/test_downscaling.py similarity index 87% rename from tests/data/task/test_downscaling.py rename to tests/data/experimental/task/test_downscaling.py index ab0eb0dd..1e25000e 100644 --- a/tests/data/task/test_downscaling.py +++ b/tests/data/experimental/task/test_downscaling.py @@ -1,6 +1,8 @@ from climate_learn.data.task import DownscalingArgs, Downscaling +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestDownscalingInstantiation: def test_initialization(self): Downscaling( diff --git a/tests/data/task/test_forecasting.py b/tests/data/experimental/task/test_forecasting.py similarity index 89% rename from tests/data/task/test_forecasting.py rename to tests/data/experimental/task/test_forecasting.py index 81668f5a..9d37e6ab 100644 --- a/tests/data/task/test_forecasting.py +++ b/tests/data/experimental/task/test_forecasting.py @@ -1,6 +1,8 @@ from climate_learn.data.task import ForecastingArgs, Forecasting +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestForecastingInstantiation: def test_initialization(self): Forecasting( diff --git a/tests/data/task/test_task.py b/tests/data/experimental/task/test_task.py similarity index 86% rename from tests/data/task/test_task.py rename to tests/data/experimental/task/test_task.py index 466a7cb1..b686320a 100644 --- a/tests/data/task/test_task.py +++ b/tests/data/experimental/task/test_task.py @@ -1,6 +1,8 @@ from climate_learn.data.task import TaskArgs, Task +import pytest +@pytest.mark.skip("Shelving map/shard datasets") class TestTaskInstantiation: def test_initialization(self): Task( diff --git a/tests/data/test_module.py b/tests/data/experimental/test_module.py similarity index 98% rename from tests/data/test_module.py rename to tests/data/experimental/test_module.py index eda46049..2a56bedc 100644 --- a/tests/data/test_module.py +++ b/tests/data/experimental/test_module.py @@ -9,7 +9,7 @@ GITHUB_ACTIONS = os.environ.get("GITHUB_ACTIONS") == "true" -@pytest.mark.skipif(GITHUB_ACTIONS, reason="only works locally") +@pytest.mark.skip("Shelving map/shard datasets") class TestModuleInstantiation: def test_map_initialization(self): climate_dataset_args = ERA5Args( diff --git a/tests/data/test_download.py b/tests/data/test_download.py new file mode 100644 index 00000000..e58a2a1f --- /dev/null +++ b/tests/data/test_download.py @@ -0,0 +1,37 @@ +import climate_learn as cl +import pytest + + +# The following tests should work as examples, but they are skipped since they +# take a long time to run. + + +@pytest.mark.skip() +def test_download_prism_tmax(tmp_path): + dst = tmp_path / "prism" + num_days_in_2019 = 365 + cl.data.download_prism(dst, variable="tmax", years=(2019, 2020)) + num_subdirs = len(list(dst.glob("*"))) + assert num_subdirs == num_days_in_2019 + + +@pytest.mark.skip() +def test_download_weatherbench_era5_constants(tmp_path): + dataset = "era5" + variable = "constants" + res = 5.625 + dst = tmp_path / "weatherbench" / dataset + cl.data.download_weatherbench(dst, dataset, variable, res) + expected_output_file = dst / f"{variable}_{res}deg.nc" + assert expected_output_file.exists() + + +@pytest.mark.skip() +def test_download_weatherbench_era5_t2m(tmp_path): + dataset = "era5" + variable = "2m_temperature" + res = 5.625 + dst = tmp_path / "weatherbench" / dataset / variable + cl.data.download_weatherbench(dst, dataset, variable, res) + expected_num_years = 40 + assert len(list(dst.iterdir())) == expected_num_years diff --git a/tests/loaders/test_presets.py b/tests/loaders/test_presets.py index 87225924..11e660e2 100644 --- a/tests/loaders/test_presets.py +++ b/tests/loaders/test_presets.py @@ -23,7 +23,7 @@ def test_known_forecasting_presets(preset): mock_dm = MockDataModule(32, 3, 2, 2, 32, 64) mock_dm.setup() - model, optimizer, lr_scheduler = cl.load_preset( + model, optimizer, lr_scheduler = cl.load_architecture( "forecasting", mock_dm, preset=preset ) if preset == FORECASTING_PRESETS[0]: @@ -56,7 +56,7 @@ def test_known_forecasting_presets(preset): def test_illegal_persistence(mock_dm): mock_dm.setup() with pytest.raises(RuntimeError) as exc_info: - cl.load_preset("forecasting", mock_dm, preset="persistence") + cl.load_architecture("forecasting", mock_dm, preset="persistence") assert str(exc_info.value) == ( "Persistence requires the output variables to be a subset of the input" " variables." @@ -67,7 +67,7 @@ def test_illegal_persistence(mock_dm): def test_known_downscaling_presets(preset): mock_dm = MockDataModule(32, 0, 3, 3, 32, 64) mock_dm.setup() - model, optimizer, lr_scheduler = cl.load_preset( + model, optimizer, lr_scheduler = cl.load_architecture( "downscaling", mock_dm, preset=preset ) if preset in DOWNSCALING_PRESETS[:3]: