From c3c1722f5130bfe374acdbad01e57fc76893e5cd Mon Sep 17 00:00:00 2001
From: Leif Denby
Date: Thu, 21 Nov 2024 08:01:08 +0100
Subject: [PATCH] Add "datastores" to represent input data from zarr, npy, etc
(#66)
Introduce new "datastores" concept where loading of input data from disk into `pytorch.Dataset` (`neural_lam.WeatherDataset`) is split into two layers: a) datastores that work with and return `xr.DataArray` objects for the whole time-series and b) `neural_lam.WeatherDataset` which consumes output from a datastore, takes care of time-sampling and produces `pytorch.Tensor`-based training samples. Currently, two kinds of datastores are implemented: 1) reading of zarr-based training datasets produced with `mllam-data-prep` and 2) reading of the npyfiles-based MEPS example dataset included with neural-lam `v0.1.0`.
---------
Co-authored-by: SimonKamuk <43374850+SimonKamuk@users.noreply.github.com>
Co-authored-by: joeloskarsson
Co-authored-by: Leif Denby
Co-authored-by: Joel Oskarsson
Co-authored-by: Simon Adamov
Co-authored-by: Simon Adamov
Co-authored-by: Kasper Hintz
---
.../workflows/ci-pdm-install-and-test-cpu.yml | 12 +-
.../workflows/ci-pdm-install-and-test-gpu.yml | 12 +-
.../workflows/ci-pip-install-and-test-cpu.yml | 12 +-
.../workflows/ci-pip-install-and-test-gpu.yml | 12 +-
.github/workflows/pre-commit.yml | 2 +-
.gitignore | 11 +-
CHANGELOG.md | 6 +
README.md | 364 ++++++--
figures/component_dependencies.png | Bin 203632 -> 0 bytes
neural_lam/__init__.py | 1 -
neural_lam/config.py | 225 +++--
.../{create_mesh.py => create_graph.py} | 232 +++--
neural_lam/create_grid_features.py | 63 --
neural_lam/data_config.yaml | 64 --
neural_lam/datastore/__init__.py | 26 +
neural_lam/datastore/base.py | 553 +++++++++++
neural_lam/datastore/mdp.py | 464 +++++++++
neural_lam/datastore/npyfilesmeps/__init__.py | 2 +
.../compute_standardization_stats.py} | 251 ++---
neural_lam/datastore/npyfilesmeps/config.py | 66 ++
neural_lam/datastore/npyfilesmeps/store.py | 788 ++++++++++++++++
neural_lam/datastore/plot_example.py | 189 ++++
neural_lam/loss_weighting.py | 106 +++
neural_lam/models/ar_model.py | 249 +++--
neural_lam/models/base_graph_model.py | 17 +-
neural_lam/models/base_hi_graph_model.py | 6 +-
neural_lam/models/graph_lam.py | 6 +-
neural_lam/models/hi_lam.py | 9 +-
neural_lam/models/hi_lam_parallel.py | 9 +-
plot_graph.py => neural_lam/plot_graph.py | 33 +-
neural_lam/train_model.py | 152 ++-
neural_lam/utils.py | 136 +--
neural_lam/vis.py | 51 +-
neural_lam/weather_dataset.py | 877 +++++++++++++-----
pyproject.toml | 30 +-
tests/__init__.py | 0
tests/conftest.py | 106 +++
tests/datastore_examples/.gitignore | 2 +
.../mdp/danra_100m_winds/.gitignore | 2 +
.../mdp/danra_100m_winds/config.yaml | 9 +
.../mdp/danra_100m_winds/danra.datastore.yaml | 99 ++
tests/dummy_datastore.py | 449 +++++++++
tests/test_cli.py | 14 +-
tests/test_config.py | 72 ++
tests/test_datasets.py | 261 ++++++
tests/test_datastores.py | 384 ++++++++
tests/test_graph_creation.py | 119 +++
tests/test_mllam_dataset.py | 142 ---
tests/test_time_slicing.py | 146 +++
tests/test_training.py | 103 ++
50 files changed, 5765 insertions(+), 1179 deletions(-)
delete mode 100644 figures/component_dependencies.png
rename neural_lam/{create_mesh.py => create_graph.py} (74%)
delete mode 100644 neural_lam/create_grid_features.py
delete mode 100644 neural_lam/data_config.yaml
create mode 100644 neural_lam/datastore/__init__.py
create mode 100644 neural_lam/datastore/base.py
create mode 100644 neural_lam/datastore/mdp.py
create mode 100644 neural_lam/datastore/npyfilesmeps/__init__.py
rename neural_lam/{create_parameter_weights.py => datastore/npyfilesmeps/compute_standardization_stats.py} (71%)
create mode 100644 neural_lam/datastore/npyfilesmeps/config.py
create mode 100644 neural_lam/datastore/npyfilesmeps/store.py
create mode 100644 neural_lam/datastore/plot_example.py
create mode 100644 neural_lam/loss_weighting.py
rename plot_graph.py => neural_lam/plot_graph.py (88%)
create mode 100644 tests/__init__.py
create mode 100644 tests/conftest.py
create mode 100644 tests/datastore_examples/.gitignore
create mode 100644 tests/datastore_examples/mdp/danra_100m_winds/.gitignore
create mode 100644 tests/datastore_examples/mdp/danra_100m_winds/config.yaml
create mode 100644 tests/datastore_examples/mdp/danra_100m_winds/danra.datastore.yaml
create mode 100644 tests/dummy_datastore.py
create mode 100644 tests/test_config.py
create mode 100644 tests/test_datasets.py
create mode 100644 tests/test_datastores.py
create mode 100644 tests/test_graph_creation.py
delete mode 100644 tests/test_mllam_dataset.py
create mode 100644 tests/test_time_slicing.py
create mode 100644 tests/test_training.py
diff --git a/.github/workflows/ci-pdm-install-and-test-cpu.yml b/.github/workflows/ci-pdm-install-and-test-cpu.yml
index c5da88cc..8fb4df79 100644
--- a/.github/workflows/ci-pdm-install-and-test-cpu.yml
+++ b/.github/workflows/ci-pdm-install-and-test-cpu.yml
@@ -39,17 +39,17 @@ jobs:
- name: Load cache data
uses: actions/cache/restore@v4
with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ path: tests/datastore_examples/npyfilesmeps/meps_example_reduced.zip
+ key: ${{ runner.os }}-meps-reduced-example-data-v0.2.0
restore-keys: |
- ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ ${{ runner.os }}-meps-reduced-example-data-v0.2.0
- name: Run tests
run: |
- pdm run pytest
+ pdm run pytest -vv -s
- name: Save cache data
uses: actions/cache/save@v4
with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ path: tests/datastore_examples/npyfilesmeps/meps_example_reduced.zip
+ key: ${{ runner.os }}-meps-reduced-example-data-v0.2.0
diff --git a/.github/workflows/ci-pdm-install-and-test-gpu.yml b/.github/workflows/ci-pdm-install-and-test-gpu.yml
index 9ab4f379..43a701c2 100644
--- a/.github/workflows/ci-pdm-install-and-test-gpu.yml
+++ b/.github/workflows/ci-pdm-install-and-test-gpu.yml
@@ -44,17 +44,17 @@ jobs:
- name: Load cache data
uses: actions/cache/restore@v4
with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ path: tests/datastore_examples/npyfilesmeps/meps_example_reduced.zip
+ key: ${{ runner.os }}-meps-reduced-example-data-v0.2.0
restore-keys: |
- ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ ${{ runner.os }}-meps-reduced-example-data-v0.2.0
- name: Run tests
run: |
- pdm run pytest
+ pdm run pytest -vv -s
- name: Save cache data
uses: actions/cache/save@v4
with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ path: tests/datastore_examples/npyfilesmeps/meps_example_reduced.zip
+ key: ${{ runner.os }}-meps-reduced-example-data-v0.2.0
diff --git a/.github/workflows/ci-pip-install-and-test-cpu.yml b/.github/workflows/ci-pip-install-and-test-cpu.yml
index 81e402c5..b131596d 100644
--- a/.github/workflows/ci-pip-install-and-test-cpu.yml
+++ b/.github/workflows/ci-pip-install-and-test-cpu.yml
@@ -29,17 +29,17 @@ jobs:
- name: Load cache data
uses: actions/cache/restore@v4
with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ path: tests/datastore_examples/npyfilesmeps/meps_example_reduced.zip
+ key: ${{ runner.os }}-meps-reduced-example-data-v0.2.0
restore-keys: |
- ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ ${{ runner.os }}-meps-reduced-example-data-v0.2.0
- name: Run tests
run: |
- python -m pytest
+ python -m pytest -vv -s
- name: Save cache data
uses: actions/cache/save@v4
with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ path: tests/datastore_examples/npyfilesmeps/meps_example_reduced.zip
+ key: ${{ runner.os }}-meps-reduced-example-data-v0.2.0
diff --git a/.github/workflows/ci-pip-install-and-test-gpu.yml b/.github/workflows/ci-pip-install-and-test-gpu.yml
index ce68946a..3afcca5a 100644
--- a/.github/workflows/ci-pip-install-and-test-gpu.yml
+++ b/.github/workflows/ci-pip-install-and-test-gpu.yml
@@ -34,17 +34,17 @@ jobs:
- name: Load cache data
uses: actions/cache/restore@v4
with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ path: tests/datastore_examples/npyfilesmeps/meps_example_reduced.zip
+ key: ${{ runner.os }}-meps-reduced-example-data-v0.2.0
restore-keys: |
- ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ ${{ runner.os }}-meps-reduced-example-data-v0.2.0
- name: Run tests
run: |
- python -m pytest
+ python -m pytest -vv -s
- name: Save cache data
uses: actions/cache/save@v4
with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
+ path: tests/datastore_examples/npyfilesmeps/meps_example_reduced.zip
+ key: ${{ runner.os }}-meps-reduced-example-data-v0.2.0
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index ad2b1a9c..71e28ad7 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12"]
+ python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v2
- name: Set up Python
diff --git a/.gitignore b/.gitignore
index 022206f5..fdb51d3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,14 +1,14 @@
### Project Specific ###
wandb
-slurm_log*
saved_models
lightning_logs
data
graphs
*.sif
sweeps
-test_*.sh
.vscode
+*.html
+*.zarr
*slurm*
### Python ###
@@ -75,8 +75,15 @@ tags
# Coc configuration directory
.vim
+.vscode
+
+# macos
+.DS_Store
+__MACOSX
# pdm (https://pdm-project.org/en/stable/)
.pdm-python
+.venv
+
# exclude pdm.lock file so that both cpu and gpu versions of torch will be accepted by pdm
pdm.lock
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18cf5d4d..12cf54f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased](https://github.com/joeloskarsson/neural-lam/compare/v0.2.0...HEAD)
+### Added
+
+- Introduce Datastores to represent input data from different sources, including zarr and numpy.
+ [\#66](https://github.com/mllam/neural-lam/pull/66)
+ @leifdenby @sadamov
+
## [v0.2.0](https://github.com/joeloskarsson/neural-lam/releases/tag/v0.2.0)
### Added
diff --git a/README.md b/README.md
index 416f7e8c..e21b7c24 100644
--- a/README.md
+++ b/README.md
@@ -63,18 +63,7 @@ Still, some restrictions are inevitable:
-## A note on the limited area setting
-Currently we are using these models on a limited area covering the Nordic region, the so called MEPS area (see [paper](#graph-based-neural-weather-prediction-for-limited-area-modeling)).
-There are still some parts of the code that is quite specific for the MEPS area use case.
-This is in particular true for the mesh graph creation (`python -m neural_lam.create_mesh`) and some of the constants set in a `data_config.yaml` file (path specified in `python -m neural_lam.train_model --data_config ` ).
-There is ongoing efforts to refactor the code to be fully area-agnostic.
-See issues [4](https://github.com/mllam/neural-lam/issues/4) and [24](https://github.com/mllam/neural-lam/issues/24) for more about this.
-See also the [weather-model-graphs](https://github.com/mllam/weather-model-graphs) package for constructing graphs for arbitrary areas.
-
-# Using Neural-LAM
-Below follows instructions on how to use Neural-LAM to train and evaluate models.
-
-## Installation
+# Installing Neural-LAM
When installing `neural-lam` you have a choice of either installing with
directly `pip` or using the `pdm` package manager.
@@ -91,7 +80,7 @@ expects the most recent version of CUDA on your system.
We cover all the installation options in our [github actions ci/cd
setup](.github/workflows/) which you can use as a reference.
-### Using `pdm`
+## Using `pdm`
1. Clone this repository and navigate to the root directory.
2. Install `pdm` if you don't have it installed on your system (either with `pip install pdm` or [following the install instructions](https://pdm-project.org/latest/#installation)).
@@ -100,7 +89,7 @@ setup](.github/workflows/) which you can use as a reference.
4. Install a specific version of `torch` with `pdm run python -m pip install torch --index-url https://download.pytorch.org/whl/cpu` for a CPU-only version or `pdm run python -m pip install torch --index-url https://download.pytorch.org/whl/cu111` for CUDA 11.1 support (you can find the correct URL for the variant you want on [PyTorch webpage](https://pytorch.org/get-started/locally/)).
5. Install the dependencies with `pdm install` (by default this in include the). If you will be developing `neural-lam` we recommend to install the development dependencies with `pdm install --group dev`. By default `pdm` installs the `neural-lam` package in editable mode, so you can make changes to the code and see the effects immediately.
-### Using `pip`
+## Using `pip`
1. Clone this repository and navigate to the root directory.
> If you are happy using the latest version of `torch` with GPU support (expecting the latest version of CUDA is installed on your system) you can skip to step 3.
@@ -108,41 +97,291 @@ setup](.github/workflows/) which you can use as a reference.
3. Install the dependencies with `python -m pip install .`. If you will be developing `neural-lam` we recommend to install in editable mode and install the development dependencies with `python -m pip install -e ".[dev]"` so you can make changes to the code and see the effects immediately.
-## Data
-Datasets should be stored in a directory called `data`.
-See the [repository format section](#format-of-data-directory) for details on the directory structure.
+# Using Neural-LAM
+
+Once `neural-lam` is installed you will be able to train/evaluate models. For this you will in general need two things:
+
+1. **Data to train/evaluate the model**. To represent this data we use a concept of
+ *datastores* in Neural-LAM (see the [Data](#data-the-datastore-and-weatherdataset-classes) section for more details).
+ In brief, a datastore implements the process of loading data from disk in a
+ specific format (for example zarr or numpy files) by implementing an
+ interface that provides the data in a data-structure that can be used within
+ neural-lam. A datastore is used to create a `pytorch.Dataset`-derived
+ class that samples the data in time to create individual samples for
+ training, validation and testing.
+
+2. **The graph structure** is used to define message-passing GNN layers,
+ that are trained to emulate fluid flow in the atmosphere over time. The
+ graph structure is created for a specific datastore.
+
+Any command you run in neural-lam will include the path to a configuration file
+to be used (usually called `config.yaml`). This configuration file defines the
+path to the datastore configuration you wish to use and allows you to configure
+different aspects about the training and evaluation of the model.
+
+The path you provide to the neural-lam config (`config.yaml`) also sets the
+root directory relative to which all other paths are resolved, as in the parent
+directory of the config becomes the root directory. Both the datastore and
+graphs you generate are then stored in subdirectories of this root directory.
+Exactly how and where a specific datastore expects its source data to be stored
+and where it stores its derived data is up to the implementation of the
+datastore.
+
+In general the folder structure assumed in Neural-LAM is as follows (we will
+assume you placed `config.yaml` in a folder called `data`):
+
+```
+data/
+├── config.yaml - Configuration file for neural-lam
+├── danra.datastore.yaml - Configuration file for the datastore, referred to from config.yaml
+└── graphs/ - Directory containing graphs for training
+```
+
+And the content of `config.yaml` could in this case look like:
+```yaml
+datastore:
+ kind: mdp
+ config_path: danra.datastore.yaml
+training:
+ state_feature_weighting:
+ __config_class__: ManualStateFeatureWeighting
+ values:
+ u100m: 1.0
+ v100m: 1.0
+```
+
+For now the neural-lam config only defines two things: 1) the kind of data
+store and the path to its config, and 2) the weighting of different features in
+the loss function. If you don't define the state feature weighting it will default
+to weighting all features equally.
+
+(This example is taken from the `tests/datastore_examples/mdp` directory.)
+
+
+Below follows instructions on how to use Neural-LAM to train and evaluate
+models, with details first given for each kind of datastore implemented
+and later the graph generation. Once `neural-lam` has been installed the
+general process is:
+
+1. Run any pre-processing scripts to generate the necessary derived data that your chosen datastore requires
+2. Run graph-creation step
+3. Train the model
+
+## Data (the `DataStore` and `WeatherDataset` classes)
+
+To enable flexibility in what input-data sources can be used with neural-lam,
+the input-data representation is split into two parts:
+
+1. A "datastore" (represented by instances of
+ [neural_lam.datastore.BaseDataStore](neural_lam/datastore/base.py)) which
+ takes care of loading a given category (state, forcing or static) and split
+ (train/val/test) of data from disk and returning it as a `xarray.DataArray`.
+ The returned data-array is expected to have the spatial coordinates
+ flattened into a single `grid_index` dimension and all variables and vertical
+ levels stacked into a feature dimension (named as `{category}_feature`). The
+ datastore also provides information about the number, names and units of
+ variables in the data, the boundary mask, normalisation values and grid
+ information.
+
+2. A `pytorch.Dataset`-derived class (called
+ `neural_lam.weather_dataset.WeatherDataset`) which takes care of sampling in
+ time to create individual samples for training, validation and testing. The
+ `WeatherDataset` class is also responsible for normalising the values and
+ returning `torch.Tensor`-objects.
+
+There are currently two different datastores implemented in the codebase:
+
+1. `neural_lam.datastore.MDPDatastore` which represents loading of
+ *training-ready* datasets in zarr format created with the
+ [mllam-data-prep](https://github.com/mllam/mllam-data-prep) package.
+ Training-ready refers to the fact that this data has been transformed
+ (variables have been stacked, spatial coordinates have been flattened,
+ statistics for normalisation have been calculated, etc) to be ready for
+ training. `mllam-data-prep` can combine any number of datasets that can be
+ read with [xarray](https://github.com/pydata/xarray) and the processing can
+ either be done at run-time or as a pre-processing step before calling
+ neural-lam.
+
+2. `neural_lam.datastore.NpyFilesDatastoreMEPS` which reads MEPS data from
+ `.npy`-files in the format introduced in neural-lam `v0.1.0`. Note that this
+ datastore is specific to the format of the MEPS dataset, but can act as an
+ example for how to create similar numpy-based datastores.
+
+If neither of these options fit your need you can create your own datastore by
+subclassing the `neural_lam.datastore.BaseDataStore` class or
+`neural_lam.datastore.BaseRegularGridDatastore` class (if your data is stored on
+a regular grid) and implementing the abstract methods.
+
+
+### MDP (mllam-data-prep) Datastore - `MDPDatastore`
+
+With `MDPDatastore` (the mllam-data-prep datastore) all the selection,
+transformation and pre-calculation steps that are needed to go from
+for example gridded weather data to a format that is optimised for training
+in neural-lam, are done in a separate package called
+[mllam-data-prep](https://github.com/mllam/mllam-data-prep) rather than in
+neural-lam itself.
+Specifically, the `mllam-data-prep` datastore configuration (for example
+[danra.datastore.yaml](tests/datastore_examples/mdp/danra.datastore.yaml))
+specifies a) what source datasets to read from, b) what variables to select, c)
+what transformations of dimensions and variables to make, d) what statistics to
+calculate (for normalisation) and e) how to split the data into training,
+validation and test sets (see full details about the configuration specification
+in the [mllam-data-prep README](https://github.com/mllam/mllam-data-prep)).
+
+From a datastore configuration `mllam-data-prep` returns the transformed
+dataset as an `xr.Dataset` which is then written in zarr-format to disk by
+`neural-lam` when the datastore is first initiated (the path of the dataset is
+derived from the datastore config, so that from a config named `danra.datastore.yaml` the resulting dataset is stored in `danra.datastore.zarr`).
+You can also run `mllam-data-prep` directly to create the processed dataset by providing the path to the datastore configuration file:
+
+```bash
+python -m mllam_data_prep --config data/danra.datastore.yaml
+```
+
+If you will be working on a large dataset (on the order of 10GB or more) it
+could be beneficial to produce the processed `.zarr` dataset before using it
+in neural-lam so that you can do the processing across multiple CPU cores in parallel. This is done by including the `--dask-distributed-local-core-fraction` argument when calling mllam-data-prep to set the fraction of your system's CPU cores that should be used for processing (see the
+[mllam-data-prep
+README for details](https://github.com/mllam/mllam-data-prep?tab=readme-ov-file#creating-large-datasets-with-daskdistributed)).
+
+For example:
+
+```bash
+python -m mllam_data_prep --config data/danra.datastore.yaml --dask-distributed-local-core-fraction 0.5
+```
+
+### NpyFiles MEPS Datastore - `NpyFilesDatastoreMEPS`
+
+Version `v0.1.0` of Neural-LAM was built to train from numpy-files from the
+MEPS weather forecasts dataset.
+To enable this functionality to live on in later versions of neural-lam we have
+built a datastore called `NpyFilesDatastoreMEPS` which implements functionality
+to read from these exact same numpy-files. At this stage this datastore class
+is very much tied to the MEPS dataset, but the code is written in a way where
+it quite easily could be adapted to work with numpy-based weather
+forecast/analysis files in future.
The full MEPS dataset can be shared with other researchers on request, contact us for this.
-A tiny subset of the data (named `meps_example`) is available in `example_data.zip`, which can be downloaded from [here](https://liuonline-my.sharepoint.com/:f:/g/personal/joeos82_liu_se/EuiUuiGzFIFHruPWpfxfUmYBSjhqMUjNExlJi9W6ULMZ1w?e=97pnGX).
+A tiny subset of the data (named `meps_example`) is available in
+`example_data.zip`, which can be downloaded from
+[here](https://liuonline-my.sharepoint.com/:f:/g/personal/joeos82_liu_se/EuiUuiGzFIFHruPWpfxfUmYBSjhqMUjNExlJi9W6ULMZ1w?e=97pnGX).
+
Download the file and unzip in the neural-lam directory.
-Graphs used in the initial paper are also available for download at the same link (but can as easily be re-generated using `python -m neural_lam.create_mesh`).
+Graphs used in the initial paper are also available for download at the same link (but can as easily be re-generated using `python -m neural_lam.create_graph`).
Note that this is far too little data to train any useful models, but all pre-processing and training steps can be run with it.
It should thus be useful to make sure that your python environment is set up correctly and that all the code can be ran without any issues.
-## Pre-processing
-An overview of how the different pre-processing steps, training and files depend on each other is given in this figure:
-
-
-
-In order to start training models at least three pre-processing steps have to be run:
+The following datastore configuration works with the MEPS dataset:
+
+```yaml
+# meps.datastore.yaml
+dataset:
+ name: meps_example
+ num_forcing_features: 16
+ var_longnames:
+ - pres_heightAboveGround_0_instant
+ - pres_heightAboveSea_0_instant
+ - nlwrs_heightAboveGround_0_accum
+ - nswrs_heightAboveGround_0_accum
+ - r_heightAboveGround_2_instant
+ - r_hybrid_65_instant
+ - t_heightAboveGround_2_instant
+ - t_hybrid_65_instant
+ - t_isobaricInhPa_500_instant
+ - t_isobaricInhPa_850_instant
+ - u_hybrid_65_instant
+ - u_isobaricInhPa_850_instant
+ - v_hybrid_65_instant
+ - v_isobaricInhPa_850_instant
+ - wvint_entireAtmosphere_0_instant
+ - z_isobaricInhPa_1000_instant
+ - z_isobaricInhPa_500_instant
+ var_names:
+ - pres_0g
+ - pres_0s
+ - nlwrs_0
+ - nswrs_0
+ - r_2
+ - r_65
+ - t_2
+ - t_65
+ - t_500
+ - t_850
+ - u_65
+ - u_850
+ - v_65
+ - v_850
+ - wvint_0
+ - z_1000
+ - z_500
+ var_units:
+ - Pa
+ - Pa
+ - W/m\textsuperscript{2}
+ - W/m\textsuperscript{2}
+ - "-"
+ - "-"
+ - K
+ - K
+ - K
+ - K
+ - m/s
+ - m/s
+ - m/s
+ - m/s
+ - kg/m\textsuperscript{2}
+ - m\textsuperscript{2}/s\textsuperscript{2}
+ - m\textsuperscript{2}/s\textsuperscript{2}
+ num_timesteps: 65
+ num_ensemble_members: 2
+ step_length: 3
+ remove_state_features_with_index: [15]
+grid_shape_state:
+- 268
+- 238
+projection:
+ class_name: LambertConformal
+ kwargs:
+ central_latitude: 63.3
+ central_longitude: 15.0
+ standard_parallels:
+ - 63.3
+ - 63.3
+```
+
+Which you can then use in a neural-lam configuration file like this:
+
+```yaml
+# config.yaml
+datastore:
+ kind: npyfilesmeps
+ config_path: meps.datastore.yaml
+training:
+ state_feature_weighting:
+ __config_class__: ManualStateFeatureWeighting
+ values:
+ u100m: 1.0
+ v100m: 1.0
+```
-* `python -m neural_lam.create_mesh`
-* `python -m neural_lam.create_grid_features`
-* `python -m neural_lam.create_parameter_weights`
+For npy-file based datastores you must separately run the command that creates the variables used for standardization:
+
+```bash
+python -m neural_lam.datastore.npyfilesmeps.compute_standardization_stats
+```
+
+### Graph creation
-### Create graph
Run `python -m neural_lam.create_mesh` with suitable options to generate the graph you want to use (see `python neural_lam.create_mesh --help` for a list of options).
The graphs used for the different models in the [paper](#graph-based-neural-weather-prediction-for-limited-area-modeling) can be created as:
-* **GC-LAM**: `python -m neural_lam.create_mesh --graph multiscale`
-* **Hi-LAM**: `python -m neural_lam.create_mesh --graph hierarchical --hierarchical` (also works for Hi-LAM-Parallel)
-* **L1-LAM**: `python -m neural_lam.create_mesh --graph 1level --levels 1`
+* **GC-LAM**: `python -m neural_lam.create_graph --config_path --name multiscale`
+* **Hi-LAM**: `python -m neural_lam.create_graph --config_path --name hierarchical --hierarchical` (also works for Hi-LAM-Parallel)
+* **L1-LAM**: `python -m neural_lam.create_graph --config_path --name 1level --levels 1`
The graph-related files are stored in a directory called `graphs`.
-### Create remaining static features
-To create the remaining static files run `python -m neural_lam.create_grid_features` and `python -m neural_lam.create_parameter_weights`.
-
## Weights & Biases Integration
The project is fully integrated with [Weights & Biases](https://www.wandb.ai/) (W&B) for logging and visualization, but can just as easily be used without it.
When W&B is used, training configuration, training/test statistics and plots are sent to the W&B servers and made available in an interactive web interface.
@@ -160,15 +399,17 @@ wandb off
```
## Train Models
-Models can be trained using `python -m neural_lam.train_model`.
+Models can be trained using `python -m neural_lam.train_model --config_path `.
Run `python neural_lam.train_model --help` for a full list of training options.
A few of the key ones are outlined below:
-* `--dataset`: Which data to train on
+* `--config_path`: Path to the configuration for neural-lam (for example in `data/myexperiment/config.yaml`).
* `--model`: Which model to train
* `--graph`: Which graph to use with the model
+* `--epochs`: Number of epochs to train for
* `--processor_layers`: Number of GNN layers to use in the processing part of the model
-* `--ar_steps`: Number of time steps to unroll for when making predictions and computing the loss
+* `--ar_steps_train`: Number of time steps to unroll for when making predictions and computing the loss
+* `--ar_steps_eval`: Number of time steps to unroll for during validation steps
Checkpoints of trained models are stored in the `saved_models` directory.
The implemented models are:
@@ -208,13 +449,14 @@ python -m neural_lam.train_model --model hi_lam_parallel --graph hierarchical ..
Checkpoint files for our models trained on the MEPS data are available upon request.
## Evaluate Models
-Evaluation is also done using `python -m neural_lam.train_model`, but using the `--eval` option.
+Evaluation is also done using `python -m neural_lam.train_model --config_path `, but using the `--eval` option.
Use `--eval val` to evaluate the model on the validation set and `--eval test` to evaluate on test data.
-Most of the training options are also relevant for evaluation (not `ar_steps`, evaluation always unrolls full forecasts).
+Most of the training options are also relevant for evaluation.
Some options specifically important for evaluation are:
* `--load`: Path to model checkpoint file (`.ckpt`) to load parameters from
* `--n_example_pred`: Number of example predictions to plot during evaluation.
+* `--ar_steps_eval`: Number of time steps to unroll for during evaluation
**Note:** While it is technically possible to use multiple GPUs for running evaluation, this is strongly discouraged. If using multiple devices the `DistributedSampler` will replicate some samples to make sure all devices have the same batch size, meaning that evaluation metrics will be unreliable.
A possible workaround is to just use batch size 1 during evaluation.
@@ -223,47 +465,7 @@ This issue stems from PyTorch Lightning. See for example [this PR](https://githu
# Repository Structure
Except for training and pre-processing scripts all the source code can be found in the `neural_lam` directory.
Model classes, including abstract base classes, are located in `neural_lam/models`.
-
-## Format of data directory
-It is possible to store multiple datasets in the `data` directory.
-Each dataset contains a set of files with static features and a set of samples.
-The samples are split into different sub-directories for training, validation and testing.
-The directory structure is shown with examples below.
-Script names within parenthesis denote the script used to generate the file.
-```
-data
-├── dataset1
-│ ├── samples - Directory with data samples
-│ │ ├── train - Training data
-│ │ │ ├── nwp_2022040100_mbr000.npy - A time series sample
-│ │ │ ├── nwp_2022040100_mbr001.npy
-│ │ │ ├── ...
-│ │ │ ├── nwp_2022043012_mbr001.npy
-│ │ │ ├── nwp_toa_downwelling_shortwave_flux_2022040100.npy - Solar flux forcing
-│ │ │ ├── nwp_toa_downwelling_shortwave_flux_2022040112.npy
-│ │ │ ├── ...
-│ │ │ ├── nwp_toa_downwelling_shortwave_flux_2022043012.npy
-│ │ │ ├── wtr_2022040100.npy - Open water features for one sample
-│ │ │ ├── wtr_2022040112.npy
-│ │ │ ├── ...
-│ │ │ └── wtr_202204012.npy
-│ │ ├── val - Validation data
-│ │ └── test - Test data
-│ └── static - Directory with graph information and static features
-│ ├── nwp_xy.npy - Coordinates of grid nodes (part of dataset)
-│ ├── surface_geopotential.npy - Geopotential at surface of grid nodes (part of dataset)
-│ ├── border_mask.npy - Mask with True for grid nodes that are part of border (part of dataset)
-│ ├── grid_features.pt - Static features of grid nodes (neural_lam.create_grid_features)
-│ ├── parameter_mean.pt - Means of state parameters (neural_lam.create_parameter_weights)
-│ ├── parameter_std.pt - Std.-dev. of state parameters (neural_lam.create_parameter_weights)
-│ ├── diff_mean.pt - Means of one-step differences (neural_lam.create_parameter_weights)
-│ ├── diff_std.pt - Std.-dev. of one-step differences (neural_lam.create_parameter_weights)
-│ ├── flux_stats.pt - Mean and std.-dev. of solar flux forcing (neural_lam.create_parameter_weights)
-│ └── parameter_weights.npy - Loss weights for different state parameters (neural_lam.create_parameter_weights)
-├── dataset2
-├── ...
-└── datasetN
-```
+Notebooks for visualization and analysis are located in `docs`.
## Format of graph directory
The `graphs` directory contains generated graph structures that can be used by different graph-based models.
diff --git a/figures/component_dependencies.png b/figures/component_dependencies.png
deleted file mode 100644
index fae77cab4655f8f1259565117a17ce86b1054959..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 203632
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%0DXYm0cDj@V2FD*)%$lNCmN?dH*3#HIO
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