From 5d73fe1f4801a9cab8cdb5bced410c4f8066a55d Mon Sep 17 00:00:00 2001
From: Simon Adamov
Date: Tue, 21 Jan 2025 11:11:13 +0100
Subject: [PATCH] merge with main
---
.cirun.yml | 16 +
.github/pull_request_template.md | 2 +-
.../workflows/ci-pdm-install-and-test-cpu.yml | 55 ++
.../workflows/ci-pdm-install-and-test-gpu.yml | 60 ++
.../workflows/ci-pip-install-and-test-cpu.yml | 45 +
.../workflows/ci-pip-install-and-test-gpu.yml | 50 +
.github/workflows/pre-commit.yml | 2 +-
.github/workflows/run_tests.yml | 43 -
.gitignore | 16 +-
.pre-commit-config.yaml | 1 +
CHANGELOG.md | 50 +-
README.md | 484 +++++++---
create_grid_features.py | 63 --
figures/component_dependencies.png | Bin 203632 -> 0 bytes
neural_lam/__init__.py | 9 +
neural_lam/config.py | 225 +++--
create_mesh.py => neural_lam/create_graph.py | 236 +++--
neural_lam/data_config.yaml | 64 --
neural_lam/datastore/__init__.py | 26 +
neural_lam/datastore/base.py | 558 +++++++++++
neural_lam/datastore/mdp.py | 467 ++++++++++
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/interaction_net.py | 4 +-
neural_lam/loss_weighting.py | 106 +++
neural_lam/models/__init__.py | 6 +
neural_lam/models/ar_model.py | 328 +++++--
neural_lam/models/base_graph_model.py | 25 +-
neural_lam/models/base_hi_graph_model.py | 14 +-
neural_lam/models/graph_lam.py | 14 +-
neural_lam/models/hi_lam.py | 15 +-
neural_lam/models/hi_lam_parallel.py | 15 +-
plot_graph.py => neural_lam/plot_graph.py | 38 +-
train_model.py => neural_lam/train_model.py | 171 ++--
neural_lam/utils.py | 138 ++-
neural_lam/vis.py | 77 +-
neural_lam/weather_dataset.py | 876 +++++++++++++-----
pyproject.toml | 58 +-
requirements.txt | 17 -
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 | 12 +
tests/test_config.py | 72 ++
tests/test_datasets.py | 261 ++++++
tests/test_datastores.py | 384 ++++++++
tests/test_graph_creation.py | 119 +++
tests/test_imports.py | 8 +
tests/test_mllam_dataset.py | 138 ---
tests/test_time_slicing.py | 146 +++
tests/test_training.py | 103 ++
57 files changed, 6284 insertions(+), 1296 deletions(-)
create mode 100644 .cirun.yml
create mode 100644 .github/workflows/ci-pdm-install-and-test-cpu.yml
create mode 100644 .github/workflows/ci-pdm-install-and-test-gpu.yml
create mode 100644 .github/workflows/ci-pip-install-and-test-cpu.yml
create mode 100644 .github/workflows/ci-pip-install-and-test-gpu.yml
delete mode 100644 .github/workflows/run_tests.yml
delete mode 100644 create_grid_features.py
delete mode 100644 figures/component_dependencies.png
create mode 100644 neural_lam/__init__.py
rename create_mesh.py => neural_lam/create_graph.py (73%)
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 create_parameter_weights.py => neural_lam/datastore/npyfilesmeps/compute_standardization_stats.py (70%)
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
create mode 100644 neural_lam/models/__init__.py
rename plot_graph.py => neural_lam/plot_graph.py (87%)
rename train_model.py => neural_lam/train_model.py (64%)
delete mode 100644 requirements.txt
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_cli.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
create mode 100644 tests/test_imports.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/.cirun.yml b/.cirun.yml
new file mode 100644
index 00000000..21b03ab4
--- /dev/null
+++ b/.cirun.yml
@@ -0,0 +1,16 @@
+# setup for using github runners via https://cirun.io/
+runners:
+ - name: "aws-runner"
+ # Cloud Provider: AWS
+ cloud: "aws"
+ # https://aws.amazon.com/ec2/instance-types/g4/
+ instance_type: "g4ad.xlarge"
+ # Deep Learning Base OSS Nvidia Driver GPU AMI (Ubuntu 22.04), Frankfurt region
+ machine_image: "ami-0ba41b554b28d24a4"
+ # use Frankfurt region
+ region: "eu-central-1"
+ preemptible: false
+ # Add this label in the "runs-on" param in .github/workflows/.yml
+ # So that this runner is created for running the workflow
+ labels:
+ - "cirun-aws-runner"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index b4bf15ea..9d4aeb54 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -26,7 +26,7 @@
- [ ] I have updated the [README](README.MD) to cover introduced code changes
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have given the PR a name that clearly describes the change, written in imperative form ([context](https://www.gitkraken.com/learn/git/best-practices/git-commit-message#using-imperative-verb-form)).
-- [ ] I have requested a reviewer and an assignee (assignee is responsible for merging)
+- [ ] I have requested a reviewer and an assignee (assignee is responsible for merging). This applies only if you have write access to the repo, otherwise feel free to tag a maintainer to add a reviewer and assignee.
## Checklist for reviewers
diff --git a/.github/workflows/ci-pdm-install-and-test-cpu.yml b/.github/workflows/ci-pdm-install-and-test-cpu.yml
new file mode 100644
index 00000000..8fb4df79
--- /dev/null
+++ b/.github/workflows/ci-pdm-install-and-test-cpu.yml
@@ -0,0 +1,55 @@
+# cicd workflow for running tests with pytest
+# needs to first install pdm, then install torch cpu manually and then install the package
+# then run the tests
+
+name: test (pdm install, cpu)
+
+on: [push, pull_request]
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install pdm
+ run: |
+ python -m pip install pdm
+
+ - name: Create venv
+ run: |
+ pdm venv create --with-pip
+ pdm use --venv in-project
+
+ - name: Install torch (CPU)
+ run: |
+ pdm run python -m pip install torch --index-url https://download.pytorch.org/whl/cpu
+ # check that the CPU version is installed
+
+ - name: Install package (including dev dependencies)
+ run: |
+ pdm install --group :all
+
+ - name: Print and check torch version
+ run: |
+ pdm run python -c "import torch; print(torch.__version__)"
+ pdm run python -c "import torch; assert torch.__version__.endswith('+cpu')"
+
+ - name: Load cache data
+ uses: actions/cache/restore@v4
+ with:
+ 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.2.0
+
+ - name: Run tests
+ run: |
+ pdm run pytest -vv -s
+
+ - name: Save cache data
+ uses: actions/cache/save@v4
+ with:
+ 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
new file mode 100644
index 00000000..43a701c2
--- /dev/null
+++ b/.github/workflows/ci-pdm-install-and-test-gpu.yml
@@ -0,0 +1,60 @@
+# cicd workflow for running tests with pytest
+# needs to first install pdm, then install torch cpu manually and then install the package
+# then run the tests
+
+name: test (pdm install, gpu)
+
+on: [push, pull_request]
+
+jobs:
+ tests:
+ runs-on: "cirun-aws-runner--${{ github.run_id }}"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Set up Python 3.9
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+
+ - name: Install pdm
+ run: |
+ python -m pip install pdm
+
+ - name: Create venv
+ run: |
+ pdm config venv.in_project False
+ pdm config venv.location /opt/dlami/nvme/venv
+ pdm venv create --with-pip
+
+ - name: Install torch (GPU CUDA 12.1)
+ run: |
+ pdm run python -m pip install torch --index-url https://download.pytorch.org/whl/cu121
+
+ - name: Print and check torch version
+ run: |
+ pdm run python -c "import torch; print(torch.__version__)"
+ pdm run python -c "import torch; assert not torch.__version__.endswith('+cpu')"
+
+ - name: Install package (including dev dependencies)
+ run: |
+ pdm install --group :all
+
+ - name: Load cache data
+ uses: actions/cache/restore@v4
+ with:
+ 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.2.0
+
+ - name: Run tests
+ run: |
+ pdm run pytest -vv -s
+
+ - name: Save cache data
+ uses: actions/cache/save@v4
+ with:
+ 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
new file mode 100644
index 00000000..b131596d
--- /dev/null
+++ b/.github/workflows/ci-pip-install-and-test-cpu.yml
@@ -0,0 +1,45 @@
+# cicd workflow for running tests with pytest
+# needs to first install pdm, then install torch cpu manually and then install the package
+# then run the tests
+
+name: test (pip install, cpu)
+
+on: [push, pull_request]
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install torch (CPU)
+ run: |
+ python -m pip install torch --index-url https://download.pytorch.org/whl/cpu
+
+ - name: Install package (including dev dependencies)
+ run: |
+ python -m pip install ".[dev]"
+
+ - name: Print and check torch version
+ run: |
+ python -c "import torch; print(torch.__version__)"
+ python -c "import torch; assert torch.__version__.endswith('+cpu')"
+
+ - name: Load cache data
+ uses: actions/cache/restore@v4
+ with:
+ 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.2.0
+
+ - name: Run tests
+ run: |
+ python -m pytest -vv -s
+
+ - name: Save cache data
+ uses: actions/cache/save@v4
+ with:
+ 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
new file mode 100644
index 00000000..3afcca5a
--- /dev/null
+++ b/.github/workflows/ci-pip-install-and-test-gpu.yml
@@ -0,0 +1,50 @@
+# cicd workflow for running tests with pytest
+# needs to first install pdm, then install torch cpu manually and then install the package
+# then run the tests
+
+name: test (pip install, gpu)
+
+on: [push, pull_request]
+
+jobs:
+ tests:
+ runs-on: "cirun-aws-runner--${{ github.run_id }}"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Set up Python 3.9
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+
+ - name: Install torch (GPU CUDA 12.1)
+ run: |
+ python -m pip install torch --index-url https://download.pytorch.org/whl/cu121
+
+ - name: Install package (including dev dependencies)
+ run: |
+ python -m pip install ".[dev]"
+
+ - name: Print and check torch version
+ run: |
+ python -c "import torch; print(torch.__version__)"
+ python -c "import torch; assert not torch.__version__.endswith('+cpu')"
+
+ - name: Load cache data
+ uses: actions/cache/restore@v4
+ with:
+ 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.2.0
+
+ - name: Run tests
+ run: |
+ python -m pytest -vv -s
+
+ - name: Save cache data
+ uses: actions/cache/save@v4
+ with:
+ 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/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
deleted file mode 100644
index 4c677908..00000000
--- a/.github/workflows/run_tests.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-name: Unit Tests
-
-on:
- # trigger on pushes to any branch
- push:
- # and also on PRs to main
- pull_request:
- branches:
- - main
-
-jobs:
- build:
- runs-on: ubuntu-latest
- strategy:
- matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12"]
-
- steps:
- - uses: actions/checkout@v3
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- pip install torch-geometric>=2.5.2
- - name: Load cache data
- uses: actions/cache/restore@v4
- with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
- restore-keys: |
- ${{ runner.os }}-meps-reduced-example-data-v0.1.0
- - name: Test with pytest
- run: |
- pytest -v -s
- - name: Save cache data
- uses: actions/cache/save@v4
- with:
- path: data
- key: ${{ runner.os }}-meps-reduced-example-data-v0.1.0
diff --git a/.gitignore b/.gitignore
index 65e9f6f8..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,3 +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/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 815a92e1..dfbf8b60 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -35,3 +35,4 @@ repos:
hooks:
- id: flake8
description: Check Python code for correctness, consistency and adherence to best practices
+ additional_dependencies: [Flake8-pyproject]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69140a11..32961b16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [unreleased](https://github.com/joeloskarsson/neural-lam/compare/v0.1.0...HEAD)
+## [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
+
+### Fixed
+
+- Fix wandb environment variable disabling wandb during tests. Now correctly uses WANDB_MODE=disabled. [\#94](https://github.com/mllam/neural-lam/pull/94) @joeloskarsson
+
+- Fix bugs introduced with datastores functionality relating visualation plots [\#91](https://github.com/mllam/neural-lam/pull/91) @leifdenby
+
+## [v0.2.0](https://github.com/joeloskarsson/neural-lam/releases/tag/v0.2.0)
### Added
- Added tests for loading dataset, creating graph, and training model based on reduced MEPS dataset stored on AWS S3, along with automatic running of tests on push/PR to GitHub, including push to main branch. Added caching of test data to speed up running tests.
@@ -31,9 +45,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- added github pull-request template to ease contribution and review process
[\#53](https://github.com/mllam/neural-lam/pull/53), @leifdenby
+- ci/cd setup for running both CPU and GPU-based testing both with pdm and pip based installs [\#37](https://github.com/mllam/neural-lam/pull/37), @khintz, @leifdenby
+
### Changed
- Optional multi-core/GPU support for statistics calculation in `create_parameter_weights.py`
+- Clarify routine around requesting reviewer and assignee in PR template
+ [\#74](https://github.com/mllam/neural-lam/pull/74)
+ @joeloskarsson
+
+- Argument Parser updated to use action="store_true" instead of 0/1 for boolean arguments.
+ (https://github.com/mllam/neural-lam/pull/72)
+ @ErikLarssonDev
+
+- Optional multi-core/GPU support for statistics calculation in `create_parameter_weights.py`
[\#22](https://github.com/mllam/neural-lam/pull/22)
@sadamov
@@ -88,6 +112,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[\#52](https://github.com/mllam/neural-lam/pull/52)
@joeloskarsson
+- Cap numpy version to < 2.0.0 (this cap was removed in #37, see below)
+ [\#68](https://github.com/mllam/neural-lam/pull/68)
+ @joeloskarsson
+
+- Remove numpy < 2.0.0 version cap
+ [\#37](https://github.com/mllam/neural-lam/pull/37)
+ @leifdenby
+
+- turn `neural-lam` into a python package by moving all `*.py`-files into the
+ `neural_lam/` source directory and updating imports accordingly. This means
+ all cli functions are now invoke through the package name, e.g. `python -m
+ neural_lam.train_model` instead of `python train_model.py` (and can be done
+ anywhere once the package has been installed).
+ [\#32](https://github.com/mllam/neural-lam/pull/32), @leifdenby
+
+- move from `requirements.txt` to `pyproject.toml` for defining package dependencies.
+ [\#37](https://github.com/mllam/neural-lam/pull/37), @leifdenby
+
+- Add slack and new publication info to readme
+ [\#78](https://github.com/mllam/neural-lam/pull/78)
+ @joeloskarsson
+
## [v0.1.0](https://github.com/joeloskarsson/neural-lam/releases/tag/v0.1.0)
First tagged release of `neural-lam`, matching Oskarsson et al 2023 publication
diff --git a/README.md b/README.md
index 26d844f7..7c7cd3a1 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,14 @@
+[](https://join.slack.com/t/ml-lam/shared_invite/zt-2t112zvm8-Vt6aBvhX7nYa6Kbj_LkCBQ)

-
+[](https://github.com/mllam/neural-lam/actions/workflows/ci-pdm-install-and-test-gpu.yml)
+[](https://github.com/mllam/neural-lam/actions/workflows/ci-pdm-install-and-test-cpu.yml)
Neural-LAM is a repository of graph-based neural weather prediction models for Limited Area Modeling (LAM).
+Also global forecasting is possible, but currently on a [different branch](https://github.com/mllam/neural-lam/tree/prob_model_global) ([planned to be merged with main](https://github.com/mllam/neural-lam/issues/63)).
The code uses [PyTorch](https://pytorch.org/) and [PyTorch Lightning](https://lightning.ai/pytorch-lightning).
Graph Neural Networks are implemented using [PyG](https://pyg.org/) and logging is set up through [Weights & Biases](https://wandb.ai/).
@@ -15,8 +18,15 @@ The repository contains LAM versions of:
* GraphCast, by [Lam et al. (2023)](https://arxiv.org/abs/2212.12794).
* The hierarchical model from [Oskarsson et al. (2023)](https://arxiv.org/abs/2309.17370).
-For more information see our paper: [*Graph-based Neural Weather Prediction for Limited Area Modeling*](https://arxiv.org/abs/2309.17370).
-If you use Neural-LAM in your work, please cite:
+# Publications
+For a more in-depth scientific introduction to machine learning for LAM weather forecasting see the publications listed here.
+As the code in the repository is continuously evolving, the latest version might feature some small differences to what was used for these publications.
+We retain some paper-specific branches for reproducibility purposes.
+
+
+*If you use Neural-LAM in your work, please cite the relevant paper(s)*.
+
+#### [Graph-based Neural Weather Prediction for Limited Area Modeling](https://arxiv.org/abs/2309.17370)
```
@inproceedings{oskarsson2023graphbased,
title={Graph-based Neural Weather Prediction for Limited Area Modeling},
@@ -25,12 +35,20 @@ If you use Neural-LAM in your work, please cite:
year={2023}
}
```
-As the code in the repository is continuously evolving, the latest version might feature some small differences to what was used in the paper.
-See the branch [`ccai_paper_2023`](https://github.com/joeloskarsson/neural-lam/tree/ccai_paper_2023) for a revision of the code that reproduces the workshop paper.
+See the branch [`ccai_paper_2023`](https://github.com/joeloskarsson/neural-lam/tree/ccai_paper_2023) for a revision of the code that reproduces this workshop paper.
-We plan to continue updating this repository as we improve existing models and develop new ones.
-Collaborations around this implementation are very welcome.
-If you are working with Neural-LAM feel free to get in touch and/or submit pull requests to the repository.
+#### [Probabilistic Weather Forecasting with Hierarchical Graph Neural Networks](https://arxiv.org/abs/2406.04759)
+```
+@inproceedings{oskarsson2024probabilistic,
+ title = {Probabilistic Weather Forecasting with Hierarchical Graph Neural Networks},
+ author = {Oskarsson, Joel and Landelius, Tomas and Deisenroth, Marc Peter and Lindsten, Fredrik},
+ booktitle = {Advances in Neural Information Processing Systems},
+ volume = {37},
+ year = {2024},
+}
+```
+See the branches [`prob_model_lam`](https://github.com/mllam/neural-lam/tree/prob_model_lam) and [`prob_model_global`](https://github.com/mllam/neural-lam/tree/prob_model_global) for revisions of the code that reproduces this paper.
+The global and probabilistic models from this paper are not yet fully merged with `main` (see issues [62](https://github.com/mllam/neural-lam/issues/62) and [63](https://github.com/mllam/neural-lam/issues/63)).
# Modularity
The Neural-LAM code is designed to modularize the different components involved in training and evaluating neural weather prediction models.
@@ -45,74 +63,330 @@ 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](https://arxiv.org/abs/2309.17370)).
-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 (`create_mesh.py`) and some of the constants set in a `data_config.yaml` file (path specified in `train_model.py --data_config` ).
-If there is interest to use Neural-LAM for other areas it is not a substantial undertaking to refactor the code to be fully area-agnostic.
-We would be happy to support such enhancements.
-See the issues https://github.com/joeloskarsson/neural-lam/issues/2, https://github.com/joeloskarsson/neural-lam/issues/3 and https://github.com/joeloskarsson/neural-lam/issues/4 for some initial ideas on how this could be done.
+# Installing Neural-LAM
+
+When installing `neural-lam` you have a choice of either installing with
+directly `pip` or using the `pdm` package manager.
+We recommend using `pdm` as it makes it easy to add/remove packages while
+keeping versions consistent (it automatically updates the `pyproject.toml`
+file), makes it easy to handle virtual environments and includes the
+development toolchain packages installation too.
+
+**regarding `torch` installation**: because `torch` creates different package
+variants for different CUDA versions and cpu-only support you will need to install
+`torch` separately if you don't want the most recent GPU variant that also
+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`
+
+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)).
+> 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 5.
+3. Create a virtual environment for pdm to use with `pdm venv create --with-pip`.
+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`
+
+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.
+2. Install a specific version of `torch` with `python -m pip install torch --index-url https://download.pytorch.org/whl/cpu` for a CPU-only version or `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/)).
+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.
+
# Using Neural-LAM
-Below follows instructions on how to use Neural-LAM to train and evaluate models.
-## Installation
-Follow the steps below to create the necessary python environment.
+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
+ weights:
+ u100m: 1.0
+ v100m: 1.0
+```
-1. Install GEOS for your system. For example with `sudo apt-get install libgeos-dev`. This is necessary for the Cartopy requirement.
-2. Use python 3.9.
-3. Install version 2.0.1 of PyTorch. Follow instructions on the [PyTorch webpage](https://pytorch.org/get-started/previous-versions/) for how to set this up with GPU support on your system.
-4. Install required packages specified in `requirements.txt`.
-5. Install PyTorch Geometric version 2.2.0. This can be done by running
+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
```
-TORCH="2.0.1"
-CUDA="cu117"
-pip install pyg-lib==0.2.0 torch-scatter==2.1.1 torch-sparse==0.6.17 torch-cluster==1.6.1\
- torch-geometric==2.3.1 -f https://pytorch-geometric.com/whl/torch-${TORCH}+${CUDA}.html
+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
```
-You will have to adjust the `CUDA` variable to match the CUDA version on your system or to run on CPU. See the [installation webpage](https://pytorch-geometric.readthedocs.io/en/latest/install/installation.html) for more information.
-## 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.
+### 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.
-All graphs used in the paper are also available for download at the same link (but can as easily be re-generated using `create_mesh.py`).
-Note that this is far too little data to train any useful models, but all scripts can be ran with it.
+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 scripts and files depend on each other is given in this figure:
-
-
-
-In order to start training models at least three pre-processing scripts have to be ran:
+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
+```
-* `create_mesh.py`
-* `create_grid_features.py`
-* `create_parameter_weights.py`
+For npy-file based datastores you must separately run the command that creates the variables used for standardization:
-### Create graph
-Run `create_mesh.py` with suitable options to generate the graph you want to use (see `python create_mesh.py --help` for a list of options).
-The graphs used for the different models in the [paper](https://arxiv.org/abs/2309.17370) can be created as:
+```bash
+python -m neural_lam.datastore.npyfilesmeps.compute_standardization_stats
+```
-* **GC-LAM**: `python create_mesh.py --graph multiscale`
-* **Hi-LAM**: `python create_mesh.py --graph hierarchical --hierarchical 1` (also works for Hi-LAM-Parallel)
-* **L1-LAM**: `python create_mesh.py --graph 1level --levels 1`
+### Graph creation
-The graph-related files are stored in a directory called `graphs`.
+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:
-### Create remaining static features
-To create the remaining static files run the scripts `create_grid_features.py` and `create_parameter_weights.py`.
+* **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`.
## 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.
If W&B is turned off, logging instead saves everything locally to a directory like `wandb/dryrun...`.
-The W&B project name is set to `neural-lam`, but this can be changed in the flags of `train_model.py` (using argsparse).
+The W&B project name is set to `neural-lam`, but this can be changed in the flags of `python -m neural_lam.train_model` (using argsparse).
See the [W&B documentation](https://docs.wandb.ai/) for details.
If you would like to login and use W&B, run:
@@ -125,15 +399,17 @@ wandb off
```
## Train Models
-Models can be trained using `train_model.py`.
-Run `python train_model.py --help` for a full list of training options.
+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:
@@ -141,16 +417,16 @@ The implemented models are:
### Graph-LAM
This is the basic graph-based LAM model.
The encode-process-decode framework is used with a mesh graph in order to make one-step pedictions.
-This model class is used both for the L1-LAM and GC-LAM models from the [paper](https://arxiv.org/abs/2309.17370), only with different graphs.
+This model class is used both for the L1-LAM and GC-LAM models from the [paper](#graph-based-neural-weather-prediction-for-limited-area-modeling), only with different graphs.
To train 1L-LAM use
```
-python train_model.py --model graph_lam --graph 1level ...
+python -m neural_lam.train_model --model graph_lam --graph 1level ...
```
To train GC-LAM use
```
-python train_model.py --model graph_lam --graph multiscale ...
+python -m neural_lam.train_model --model graph_lam --graph multiscale ...
```
### Hi-LAM
@@ -158,7 +434,7 @@ A version of Graph-LAM that uses a hierarchical mesh graph and performs sequenti
To train Hi-LAM use
```
-python train_model.py --model hi_lam --graph hierarchical ...
+python -m neural_lam.train_model --model hi_lam --graph hierarchical ...
```
### Hi-LAM-Parallel
@@ -167,66 +443,29 @@ Not included in the paper as initial experiments showed worse results than Hi-LA
To train Hi-LAM-Parallel use
```
-python train_model.py --model hi_lam_parallel --graph hierarchical ...
+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 `train_model.py`, 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. This issue stems from PyTorch Lightning. See for example [this draft PR](https://github.com/Lightning-AI/torchmetrics/pull/1886) for more discussion and ongoing work to remedy this.
+**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.
+This issue stems from PyTorch Lightning. See for example [this PR](https://github.com/Lightning-AI/torchmetrics/pull/1886) for more discussion.
# 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 (create_grid_features.py)
-│ ├── parameter_mean.pt - Means of state parameters (create_parameter_weights.py)
-│ ├── parameter_std.pt - Std.-dev. of state parameters (create_parameter_weights.py)
-│ ├── diff_mean.pt - Means of one-step differences (create_parameter_weights.py)
-│ ├── diff_std.pt - Std.-dev. of one-step differences (create_parameter_weights.py)
-│ ├── flux_stats.pt - Mean and std.-dev. of solar flux forcing (create_parameter_weights.py)
-│ └── parameter_weights.npy - Loss weights for different state parameters (create_parameter_weights.py)
-├── 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.
@@ -234,13 +473,13 @@ The structure is shown with examples below:
```
graphs
├── graph1 - Directory with a graph definition
-│ ├── m2m_edge_index.pt - Edges in mesh graph (create_mesh.py)
-│ ├── g2m_edge_index.pt - Edges from grid to mesh (create_mesh.py)
-│ ├── m2g_edge_index.pt - Edges from mesh to grid (create_mesh.py)
-│ ├── m2m_features.pt - Static features of mesh edges (create_mesh.py)
-│ ├── g2m_features.pt - Static features of grid to mesh edges (create_mesh.py)
-│ ├── m2g_features.pt - Static features of mesh to grid edges (create_mesh.py)
-│ └── mesh_features.pt - Static features of mesh nodes (create_mesh.py)
+│ ├── m2m_edge_index.pt - Edges in mesh graph (neural_lam.create_mesh)
+│ ├── g2m_edge_index.pt - Edges from grid to mesh (neural_lam.create_mesh)
+│ ├── m2g_edge_index.pt - Edges from mesh to grid (neural_lam.create_mesh)
+│ ├── m2m_features.pt - Static features of mesh edges (neural_lam.create_mesh)
+│ ├── g2m_features.pt - Static features of grid to mesh edges (neural_lam.create_mesh)
+│ ├── m2g_features.pt - Static features of mesh to grid edges (neural_lam.create_mesh)
+│ └── mesh_features.pt - Static features of mesh nodes (neural_lam.create_mesh)
├── graph2
├── ...
└── graphN
@@ -250,9 +489,9 @@ graphs
To keep track of levels in the mesh graph, a list format is used for the files with mesh graph information.
In particular, the files
```
-│ ├── m2m_edge_index.pt - Edges in mesh graph (create_mesh.py)
-│ ├── m2m_features.pt - Static features of mesh edges (create_mesh.py)
-│ ├── mesh_features.pt - Static features of mesh nodes (create_mesh.py)
+│ ├── m2m_edge_index.pt - Edges in mesh graph (neural_lam.create_mesh)
+│ ├── m2m_features.pt - Static features of mesh edges (neural_lam.create_mesh)
+│ ├── mesh_features.pt - Static features of mesh nodes (neural_lam.create_mesh)
```
all contain lists of length `L`, for a hierarchical mesh graph with `L` layers.
For non-hierarchical graphs `L == 1` and these are all just singly-entry lists.
@@ -263,10 +502,10 @@ In addition, hierarchical mesh graphs (`L > 1`) feature a few additional files w
```
├── graph1
│ ├── ...
-│ ├── mesh_down_edge_index.pt - Downward edges in mesh graph (create_mesh.py)
-│ ├── mesh_up_edge_index.pt - Upward edges in mesh graph (create_mesh.py)
-│ ├── mesh_down_features.pt - Static features of downward mesh edges (create_mesh.py)
-│ ├── mesh_up_features.pt - Static features of upward mesh edges (create_mesh.py)
+│ ├── mesh_down_edge_index.pt - Downward edges in mesh graph (neural_lam.create_mesh)
+│ ├── mesh_up_edge_index.pt - Upward edges in mesh graph (neural_lam.create_mesh)
+│ ├── mesh_down_features.pt - Static features of downward mesh edges (neural_lam.create_mesh)
+│ ├── mesh_up_features.pt - Static features of upward mesh edges (neural_lam.create_mesh)
│ ├── ...
```
These files have the same list format as the ones above, but each list has length `L-1` (as these edges describe connections between levels).
@@ -285,5 +524,6 @@ from the root directory of the repository.
Furthermore, all tests in the ```tests``` directory will be run upon pushing changes by a github action. Failure in any of the tests will also reject the push/PR.
# Contact
-If you are interested in machine learning models for LAM, have questions about our implementation or ideas for extending it, feel free to get in touch.
-You can open a github issue on this page, or (if more suitable) send an email to [joel.oskarsson@liu.se](mailto:joel.oskarsson@liu.se).
+If you are interested in machine learning models for LAM, have questions about the implementation or ideas for extending it, feel free to get in touch.
+There is an open [mllam slack channel](https://join.slack.com/t/ml-lam/shared_invite/zt-2t112zvm8-Vt6aBvhX7nYa6Kbj_LkCBQ) that anyone can join (after following the link you have to request to join, this is to avoid spam bots).
+You can also open a github issue on this page, or (if more suitable) send an email to [joel.oskarsson@liu.se](mailto:joel.oskarsson@liu.se).
diff --git a/create_grid_features.py b/create_grid_features.py
deleted file mode 100644
index 4f058e17..00000000
--- a/create_grid_features.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Standard library
-import os
-from argparse import ArgumentParser
-
-# Third-party
-import numpy as np
-import torch
-
-# First-party
-from neural_lam import config
-
-
-def main():
- """
- Pre-compute all static features related to the grid nodes
- """
- parser = ArgumentParser(description="Training arguments")
- parser.add_argument(
- "--data_config",
- type=str,
- default="neural_lam/data_config.yaml",
- help="Path to data config file (default: neural_lam/data_config.yaml)",
- )
- args = parser.parse_args()
- config_loader = config.Config.from_file(args.data_config)
-
- static_dir_path = os.path.join("data", config_loader.dataset.name, "static")
-
- # -- Static grid node features --
- grid_xy = torch.tensor(
- np.load(os.path.join(static_dir_path, "nwp_xy.npy"))
- ) # (2, N_y, N_x)
- grid_xy = grid_xy.flatten(1, 2).T # (N_grid, 2)
- pos_max = torch.max(torch.abs(grid_xy))
- grid_xy = grid_xy / pos_max # Divide by maximum coordinate
-
- geopotential = torch.tensor(
- np.load(os.path.join(static_dir_path, "surface_geopotential.npy"))
- ) # (N_y, N_x)
- geopotential = geopotential.flatten(0, 1).unsqueeze(1) # (N_grid,1)
- gp_min = torch.min(geopotential)
- gp_max = torch.max(geopotential)
- # Rescale geopotential to [0,1]
- geopotential = (geopotential - gp_min) / (gp_max - gp_min) # (N_grid, 1)
-
- grid_border_mask = torch.tensor(
- np.load(os.path.join(static_dir_path, "border_mask.npy")),
- dtype=torch.int64,
- ) # (N_y, N_x)
- grid_border_mask = (
- grid_border_mask.flatten(0, 1).to(torch.float).unsqueeze(1)
- ) # (N_grid, 1)
-
- # Concatenate grid features
- grid_features = torch.cat(
- (grid_xy, geopotential, grid_border_mask), dim=1
- ) # (N_grid, 4)
-
- torch.save(grid_features, os.path.join(static_dir_path, "grid_features.pt"))
-
-
-if __name__ == "__main__":
- main()
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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