anemoi-utils 0.4.9__tar.gz → 0.4.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of anemoi-utils might be problematic. Click here for more details.
- anemoi_utils-0.4.11/.github/release.yml +20 -0
- anemoi_utils-0.4.11/.github/workflows/merge-main-into-develop.yml +31 -0
- anemoi_utils-0.4.11/.github/workflows/pr-conventional-commit.yml +21 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.github/workflows/python-publish.yml +0 -14
- anemoi_utils-0.4.11/.github/workflows/release-please.yml +24 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.gitignore +0 -1
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.pre-commit-config.yaml +2 -3
- anemoi_utils-0.4.11/.release-please-config.json +19 -0
- anemoi_utils-0.4.11/.release-please-manifest.json +3 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/CHANGELOG.md +19 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/PKG-INFO +3 -2
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/pyproject.toml +1 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/_version.py +2 -2
- anemoi_utils-0.4.11/src/anemoi/utils/caching.py +124 -0
- anemoi_utils-0.4.11/src/anemoi/utils/commands/requests.py +44 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/config.py +47 -4
- anemoi_utils-0.4.11/src/anemoi/utils/devtools.py +83 -0
- anemoi_utils-0.4.11/src/anemoi/utils/grids.py +97 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/humanize.py +33 -0
- anemoi_utils-0.4.11/src/anemoi/utils/logs.py +40 -0
- anemoi_utils-0.4.11/src/anemoi/utils/mars/requests.py +22 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/PKG-INFO +3 -2
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/SOURCES.txt +13 -2
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/requires.txt +1 -0
- anemoi_utils-0.4.11/tests/test_caching.py +113 -0
- anemoi_utils-0.4.11/tests/test_grids.py +28 -0
- anemoi_utils-0.4.9/.github/workflows/changelog-pr-update.yml +0 -18
- anemoi_utils-0.4.9/.github/workflows/changelog-release-update.yml +0 -45
- anemoi_utils-0.4.9/src/anemoi/utils/caching.py +0 -74
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.gitattributes +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.github/CODEOWNERS +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.github/ci-hpc-config.yml +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.github/workflows/ci.yml +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.github/workflows/label-public-pr.yml +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.github/workflows/python-pull-request.yml +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.github/workflows/readthedocs-pr-update.yml +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/.readthedocs.yaml +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/CONTRIBUTORS.md +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/LICENSE +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/README.md +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/Makefile +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/_static/logo.png +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/_static/style.css +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/_templates/.gitkeep +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/conf.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/index.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/installing.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/modules/checkpoints.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/modules/config.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/modules/dates.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/modules/grib.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/modules/humanize.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/modules/provenance.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/modules/s3.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/docs/modules/text.rst +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/setup.cfg +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/__init__.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/__main__.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/checkpoints.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/cli.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/commands/__init__.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/commands/config.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/compatibility.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/dates.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/grib.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/hindcasts.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/mars/__init__.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/mars/mars.yaml +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/provenance.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/registry.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/remote/__init__.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/remote/s3.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/remote/ssh.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/s3.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/sanitise.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/sanitize.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/text.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi/utils/timer.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/top_level.txt +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test-transfer-data/directory/b/c/x +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test-transfer-data/directory/b/y +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test-transfer-data/directory/exotic filename ;^/"'[=.,#]()/303/252/303/274/303/247/303/262/342/234/205.txt" +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test-transfer-data/directory/z +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test-transfer-data/file +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test_compatibility.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test_dates.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test_frequency.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test_provenance.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test_remote.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test_sanetise.py +0 -0
- {anemoi_utils-0.4.9 → anemoi_utils-0.4.11}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# .github/release.yml
|
|
2
|
+
# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes
|
|
3
|
+
|
|
4
|
+
changelog:
|
|
5
|
+
exclude:
|
|
6
|
+
labels:
|
|
7
|
+
- ignore-for-release
|
|
8
|
+
- no-changelog
|
|
9
|
+
authors:
|
|
10
|
+
- pre-commit-ci
|
|
11
|
+
categories:
|
|
12
|
+
- title: Breaking Changes 🛠
|
|
13
|
+
labels:
|
|
14
|
+
- breaking-change
|
|
15
|
+
- title: Exciting New Features 🎉
|
|
16
|
+
labels:
|
|
17
|
+
- enhancement
|
|
18
|
+
- title: Other Changes
|
|
19
|
+
labels:
|
|
20
|
+
- "*"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Merge main into develop
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
# Needed to read branches
|
|
10
|
+
contents: read
|
|
11
|
+
# Needed to create PR's
|
|
12
|
+
pull-requests: write
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
sync-branches:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
- name: Set up Node
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: 20
|
|
24
|
+
- name: Opening pull request
|
|
25
|
+
id: pull
|
|
26
|
+
uses: jdtx0/branch-sync@1.5.1
|
|
27
|
+
with:
|
|
28
|
+
GITHUB_TOKEN: ${{ secrets.REPO_SYNC_ACTION_PAT }}
|
|
29
|
+
FROM_BRANCH: "main"
|
|
30
|
+
TO_BRANCH: "develop"
|
|
31
|
+
PULL_REQUEST_AUTO_MERGE_METHOD: "merge"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: "Ensure Conventional Commit in PR title"
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request_target:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
- edited
|
|
8
|
+
- synchronize
|
|
9
|
+
- reopened
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
pull-requests: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
main:
|
|
16
|
+
name: Validate PR title
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: amannn/action-semantic-pull-request@v5
|
|
20
|
+
env:
|
|
21
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -8,20 +8,6 @@ on:
|
|
|
8
8
|
types: [created]
|
|
9
9
|
|
|
10
10
|
jobs:
|
|
11
|
-
quality:
|
|
12
|
-
uses: ecmwf-actions/reusable-workflows/.github/workflows/qa-precommit-run.yml@v2
|
|
13
|
-
with:
|
|
14
|
-
skip-hooks: "no-commit-to-branch"
|
|
15
|
-
|
|
16
|
-
checks:
|
|
17
|
-
strategy:
|
|
18
|
-
matrix:
|
|
19
|
-
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
20
|
-
uses: ecmwf-actions/reusable-workflows/.github/workflows/qa-pytest-pyproject.yml@v2
|
|
21
|
-
with:
|
|
22
|
-
python-version: ${{ matrix.python-version }}
|
|
23
|
-
|
|
24
11
|
deploy:
|
|
25
|
-
needs: [checks, quality]
|
|
26
12
|
uses: ecmwf-actions/reusable-workflows/.github/workflows/cd-pypi.yml@v2
|
|
27
13
|
secrets: inherit
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Run Release Please
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- develop
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release-please:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: googleapis/release-please-action@v4
|
|
16
|
+
with:
|
|
17
|
+
# this assumes that you have created a personal access token
|
|
18
|
+
# (PAT) and configured it as a GitHub action secret named
|
|
19
|
+
# `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
|
|
20
|
+
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
21
|
+
# optional. customize path to .release-please-config.json
|
|
22
|
+
config-file: .release-please-config.json
|
|
23
|
+
# Currently releases are done "from main" to have a stable branch
|
|
24
|
+
# target-branch: main
|
|
@@ -40,7 +40,7 @@ repos:
|
|
|
40
40
|
- --force-single-line-imports
|
|
41
41
|
- --profile black
|
|
42
42
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
43
|
-
rev: v0.
|
|
43
|
+
rev: v0.8.6
|
|
44
44
|
hooks:
|
|
45
45
|
- id: ruff
|
|
46
46
|
args:
|
|
@@ -64,7 +64,7 @@ repos:
|
|
|
64
64
|
hooks:
|
|
65
65
|
- id: pyproject-fmt
|
|
66
66
|
- repo: https://github.com/jshwi/docsig # Check docstrings against function sig
|
|
67
|
-
rev: v0.
|
|
67
|
+
rev: v0.66.1
|
|
68
68
|
hooks:
|
|
69
69
|
- id: docsig
|
|
70
70
|
args:
|
|
@@ -74,7 +74,6 @@ repos:
|
|
|
74
74
|
- --check-protected # Check protected methods
|
|
75
75
|
- --check-class # Check class docstrings
|
|
76
76
|
- --disable=E113 # Disable empty docstrings
|
|
77
|
-
- --summary # Print a summary
|
|
78
77
|
ci:
|
|
79
78
|
autoupdate_schedule: monthly
|
|
80
79
|
ci:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"release-type": "python",
|
|
3
|
+
"bump-minor-pre-major": true,
|
|
4
|
+
"bump-patch-for-minor-pre-major": true,
|
|
5
|
+
"separate-pull-requests": true,
|
|
6
|
+
"always-update": true,
|
|
7
|
+
"changelog-type": "github",
|
|
8
|
+
"include-component-in-tag": false,
|
|
9
|
+
"include-v-in-tag": false,
|
|
10
|
+
"pull-request-title-pattern": "chore${scope}: Preparing Next Release for ${component} ${version}",
|
|
11
|
+
"pull-request-header": ":robot:This is an automated PR using `release-please`.\n\nThe following changes will be included in the release, once the next version is triggered.\n\nContent of the next Release",
|
|
12
|
+
"pull-request-footer": "> [!IMPORTANT]\n> Merging this PR creates a release. Please refer to the [documentation](https://github.com/googleapis/release-please) if you're unsure.",
|
|
13
|
+
"packages": {
|
|
14
|
+
".": {
|
|
15
|
+
"package-name": "anemoi-utils"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
19
|
+
}
|
|
@@ -8,6 +8,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
Please add your functional changes to the appropriate section in the PR.
|
|
9
9
|
Keep it human-readable, your future self will thank you!
|
|
10
10
|
|
|
11
|
+
## 0.4.11 (2025-01-17)
|
|
12
|
+
|
|
13
|
+
<!-- Release notes generated using configuration in .github/release.yml at develop -->
|
|
14
|
+
|
|
15
|
+
## What's Changed
|
|
16
|
+
### Other Changes
|
|
17
|
+
* Feature request: Add option to read configuration from stdin by @mpartio in https://github.com/ecmwf/anemoi-utils/pull/59
|
|
18
|
+
* feat(plots): Add quick map plot for debugging by @b8raoult in https://github.com/ecmwf/anemoi-utils/pull/69
|
|
19
|
+
* feat: added-anemoi-utils-grids-and-tests by @floriankrb in https://github.com/ecmwf/anemoi-utils/pull/74
|
|
20
|
+
* feat(plot): added plotting options by @NRaoult in https://github.com/ecmwf/anemoi-utils/pull/72
|
|
21
|
+
* ci(release): Simplify Release Workflow to Minimum by @JesperDramsch in https://github.com/ecmwf/anemoi-utils/pull/78
|
|
22
|
+
* feat: adding-tools-for-grids by @floriankrb in https://github.com/ecmwf/anemoi-utils/pull/76
|
|
23
|
+
|
|
24
|
+
## New Contributors
|
|
25
|
+
* @mpartio made their first contribution in https://github.com/ecmwf/anemoi-utils/pull/59
|
|
26
|
+
* @NRaoult made their first contribution in https://github.com/ecmwf/anemoi-utils/pull/72
|
|
27
|
+
|
|
28
|
+
**Full Changelog**: https://github.com/ecmwf/anemoi-utils/compare/0.4.10...0.4.11
|
|
29
|
+
|
|
11
30
|
## [0.4.5](https://github.com/ecmwf/anemoi-utils/compare/0.4.4...0.4.5) - 2024-11-06
|
|
12
31
|
|
|
13
32
|
### What's Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.11
|
|
4
4
|
Summary: A package to hold various functions to support training of ML models on ECMWF data.
|
|
5
5
|
Author-email: "European Centre for Medium-Range Weather Forecasts (ECMWF)" <software.support@ecmwf.int>
|
|
6
6
|
License: Apache License
|
|
@@ -226,6 +226,7 @@ Requires-Python: >=3.9
|
|
|
226
226
|
License-File: LICENSE
|
|
227
227
|
Requires-Dist: aniso8601
|
|
228
228
|
Requires-Dist: importlib-metadata; python_version < "3.10"
|
|
229
|
+
Requires-Dist: numpy
|
|
229
230
|
Requires-Dist: python-dateutil
|
|
230
231
|
Requires-Dist: pyyaml
|
|
231
232
|
Requires-Dist: tomli; python_version < "3.11"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# (C) Copyright 2024 Anemoi contributors.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
#
|
|
6
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
7
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
|
+
# nor does it submit to any jurisdiction.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from threading import Lock
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
LOCK = Lock()
|
|
20
|
+
CACHE = {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_cache_path(collection):
|
|
24
|
+
return os.path.join(os.path.expanduser("~"), ".cache", "anemoi", collection)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def clean_cache(collection="default"):
|
|
28
|
+
global CACHE
|
|
29
|
+
CACHE = {}
|
|
30
|
+
path = _get_cache_path(collection)
|
|
31
|
+
if not os.path.exists(path):
|
|
32
|
+
return
|
|
33
|
+
for filename in os.listdir(path):
|
|
34
|
+
os.remove(os.path.join(path, filename))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Cacher:
|
|
38
|
+
"""This class implements a simple caching mechanism.
|
|
39
|
+
Private class, do not use directly"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, collection, expires):
|
|
42
|
+
self.collection = collection
|
|
43
|
+
self.expires = expires
|
|
44
|
+
|
|
45
|
+
def __call__(self, func):
|
|
46
|
+
|
|
47
|
+
full = f"{func.__module__}.{func.__name__}"
|
|
48
|
+
|
|
49
|
+
def wrapped(*args, **kwargs):
|
|
50
|
+
with LOCK:
|
|
51
|
+
return self.cache(
|
|
52
|
+
(full, args, kwargs),
|
|
53
|
+
lambda: func(*args, **kwargs),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return wrapped
|
|
57
|
+
|
|
58
|
+
def cache(self, key, proc):
|
|
59
|
+
|
|
60
|
+
key = json.dumps(key, sort_keys=True)
|
|
61
|
+
m = hashlib.md5()
|
|
62
|
+
m.update(key.encode("utf-8"))
|
|
63
|
+
m = m.hexdigest()
|
|
64
|
+
|
|
65
|
+
if m in CACHE:
|
|
66
|
+
return CACHE[m]
|
|
67
|
+
|
|
68
|
+
path = _get_cache_path(self.collection)
|
|
69
|
+
|
|
70
|
+
filename = os.path.join(path, m) + self.ext
|
|
71
|
+
if os.path.exists(filename):
|
|
72
|
+
data = self.load(filename)
|
|
73
|
+
if self.expires is None or data["expires"] > time.time():
|
|
74
|
+
if data["key"] == key:
|
|
75
|
+
return data["value"]
|
|
76
|
+
|
|
77
|
+
value = proc()
|
|
78
|
+
data = {"key": key, "value": value}
|
|
79
|
+
if self.expires is not None:
|
|
80
|
+
data["expires"] = time.time() + self.expires
|
|
81
|
+
|
|
82
|
+
os.makedirs(path, exist_ok=True)
|
|
83
|
+
temp_filename = self.save(filename, data)
|
|
84
|
+
os.rename(temp_filename, filename)
|
|
85
|
+
|
|
86
|
+
CACHE[m] = value
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class JsonCacher(Cacher):
|
|
91
|
+
ext = ""
|
|
92
|
+
|
|
93
|
+
def save(self, path, data):
|
|
94
|
+
temp_path = path + ".tmp"
|
|
95
|
+
with open(temp_path, "w") as f:
|
|
96
|
+
json.dump(data, f)
|
|
97
|
+
return temp_path
|
|
98
|
+
|
|
99
|
+
def load(self, path):
|
|
100
|
+
with open(path, "r") as f:
|
|
101
|
+
return json.load(f)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class NpzCacher(Cacher):
|
|
105
|
+
ext = ".npz"
|
|
106
|
+
|
|
107
|
+
def save(self, path, data):
|
|
108
|
+
temp_path = path + ".tmp.npz"
|
|
109
|
+
np.savez(temp_path, **data)
|
|
110
|
+
return temp_path
|
|
111
|
+
|
|
112
|
+
def load(self, path):
|
|
113
|
+
return np.load(path, allow_pickle=True)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# PUBLIC API
|
|
117
|
+
def cached(collection="default", expires=None, encoding="json"):
|
|
118
|
+
"""Decorator to cache the result of a function.
|
|
119
|
+
|
|
120
|
+
Default is to use a json file to store the cache, but you can also use npz files
|
|
121
|
+
to cache dict of numpy arrays.
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
return dict(json=JsonCacher, npz=NpzCacher)[encoding](collection, expires)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# (C) Copyright 2024 European Centre for Medium-Range Weather Forecasts.
|
|
2
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
3
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
4
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
5
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
6
|
+
# nor does it submit to any jurisdiction.
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from anemoi.utils.mars.requests import print_request
|
|
11
|
+
|
|
12
|
+
from . import Command
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Requests(Command):
|
|
16
|
+
"""Convert a JSON requests file to MARS format."""
|
|
17
|
+
|
|
18
|
+
def add_arguments(self, command_parser):
|
|
19
|
+
command_parser.add_argument("input")
|
|
20
|
+
command_parser.add_argument("output")
|
|
21
|
+
command_parser.add_argument("--verb", default="retrieve")
|
|
22
|
+
command_parser.add_argument("--only-one-field", action="store_true")
|
|
23
|
+
|
|
24
|
+
def run(self, args):
|
|
25
|
+
with open(args.input) as f:
|
|
26
|
+
requests = json.load(f)
|
|
27
|
+
|
|
28
|
+
if args.only_one_field:
|
|
29
|
+
for r in requests:
|
|
30
|
+
for key in (
|
|
31
|
+
"grid",
|
|
32
|
+
"area",
|
|
33
|
+
):
|
|
34
|
+
r.pop(key, None)
|
|
35
|
+
for k, v in list(r.items()):
|
|
36
|
+
if isinstance(v, list):
|
|
37
|
+
r[k] = v[-1]
|
|
38
|
+
|
|
39
|
+
with open(args.output, "w") as f:
|
|
40
|
+
for r in requests:
|
|
41
|
+
print_request(args.verb, r, file=f)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
command = Requests
|
|
@@ -50,14 +50,14 @@ class DotDict(dict):
|
|
|
50
50
|
super().__init__(*args, **kwargs)
|
|
51
51
|
|
|
52
52
|
for k, v in self.items():
|
|
53
|
-
if isinstance(v, dict):
|
|
53
|
+
if isinstance(v, dict) or is_omegaconf_dict(v):
|
|
54
54
|
self[k] = DotDict(v)
|
|
55
55
|
|
|
56
|
-
if isinstance(v, list):
|
|
57
|
-
self[k] = [DotDict(i) if isinstance(i, dict) else i for i in v]
|
|
56
|
+
if isinstance(v, list) or is_omegaconf_list(v):
|
|
57
|
+
self[k] = [DotDict(i) if isinstance(i, dict) or is_omegaconf_dict(i) else i for i in v]
|
|
58
58
|
|
|
59
59
|
if isinstance(v, tuple):
|
|
60
|
-
self[k] = [DotDict(i) if isinstance(i, dict) else i for i in v]
|
|
60
|
+
self[k] = [DotDict(i) if isinstance(i, dict) or is_omegaconf_dict(i) else i for i in v]
|
|
61
61
|
|
|
62
62
|
@classmethod
|
|
63
63
|
def from_file(cls, path: str):
|
|
@@ -106,6 +106,24 @@ class DotDict(dict):
|
|
|
106
106
|
return f"DotDict({super().__repr__()})"
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def is_omegaconf_dict(value) -> bool:
|
|
110
|
+
try:
|
|
111
|
+
from omegaconf import DictConfig
|
|
112
|
+
|
|
113
|
+
return isinstance(value, DictConfig)
|
|
114
|
+
except ImportError:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_omegaconf_list(value) -> bool:
|
|
119
|
+
try:
|
|
120
|
+
from omegaconf import ListConfig
|
|
121
|
+
|
|
122
|
+
return isinstance(value, ListConfig)
|
|
123
|
+
except ImportError:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
109
127
|
CONFIG = {}
|
|
110
128
|
CHECKED = {}
|
|
111
129
|
CONFIG_LOCK = threading.RLock()
|
|
@@ -205,6 +223,23 @@ def load_any_dict_format(path) -> dict:
|
|
|
205
223
|
if path.endswith(".toml"):
|
|
206
224
|
with open(path, "rb") as f:
|
|
207
225
|
return tomllib.load(f)
|
|
226
|
+
|
|
227
|
+
if path == "-":
|
|
228
|
+
import sys
|
|
229
|
+
|
|
230
|
+
config = sys.stdin.read()
|
|
231
|
+
|
|
232
|
+
parsers = [(yaml.safe_load, "yaml"), (json.loads, "json"), (tomllib.loads, "toml")]
|
|
233
|
+
|
|
234
|
+
for parser, parser_type in parsers:
|
|
235
|
+
try:
|
|
236
|
+
LOG.debug(f"Trying {parser_type} parser for stdin")
|
|
237
|
+
return parser(config)
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
raise ValueError("Failed to parse configuration from stdin")
|
|
242
|
+
|
|
208
243
|
except (json.JSONDecodeError, yaml.YAMLError, tomllib.TOMLDecodeError) as e:
|
|
209
244
|
LOG.warning(f"Failed to parse config file {path}", exc_info=e)
|
|
210
245
|
raise ValueError(f"Failed to parse config file {path} [{e}]")
|
|
@@ -376,3 +411,11 @@ def find(metadata, what, result=None, *, select: callable = None):
|
|
|
376
411
|
find(v, what, result)
|
|
377
412
|
|
|
378
413
|
return result
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def merge_configs(*configs):
|
|
417
|
+
result = {}
|
|
418
|
+
for config in configs:
|
|
419
|
+
_merge_dicts(result, config)
|
|
420
|
+
|
|
421
|
+
return result
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# (C) Copyright 2024 Anemoi contributors.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
#
|
|
6
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
7
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
|
+
# nor does it submit to any jurisdiction.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import cartopy.crs as ccrs
|
|
12
|
+
import cartopy.feature as cfeature
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
import matplotlib.tri as tri
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
"""FOR DEVELOPMENT PURPOSES ONLY
|
|
18
|
+
|
|
19
|
+
This module contains
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# TODO: use earthkit-plots
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def fix(lons):
|
|
27
|
+
return np.where(lons > 180, lons - 360, lons)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def plot_values(
|
|
31
|
+
values, latitudes, longitudes, title=None, missing_value=None, min_value=None, max_value=None, **kwargs
|
|
32
|
+
):
|
|
33
|
+
|
|
34
|
+
_, ax = plt.subplots(subplot_kw={"projection": ccrs.PlateCarree()})
|
|
35
|
+
ax.coastlines()
|
|
36
|
+
ax.add_feature(cfeature.BORDERS, linestyle=":")
|
|
37
|
+
|
|
38
|
+
missing_values = np.isnan(values)
|
|
39
|
+
|
|
40
|
+
if missing_value is None:
|
|
41
|
+
values = values[~missing_values]
|
|
42
|
+
longitudes = longitudes[~missing_values]
|
|
43
|
+
latitudes = latitudes[~missing_values]
|
|
44
|
+
else:
|
|
45
|
+
values = np.where(missing_values, missing_value, values)
|
|
46
|
+
|
|
47
|
+
if max_value is not None:
|
|
48
|
+
values = np.where(values > max_value, max_value, values)
|
|
49
|
+
|
|
50
|
+
if min_value is not None:
|
|
51
|
+
values = np.where(values < min_value, min_value, values)
|
|
52
|
+
|
|
53
|
+
triangulation = tri.Triangulation(fix(longitudes), latitudes)
|
|
54
|
+
|
|
55
|
+
levels = kwargs.pop("levels", 10)
|
|
56
|
+
|
|
57
|
+
_ = ax.tricontourf(triangulation, values, levels=levels, transform=ccrs.PlateCarree())
|
|
58
|
+
|
|
59
|
+
options = dict(
|
|
60
|
+
levels=levels,
|
|
61
|
+
colors="black",
|
|
62
|
+
linewidths=0.5,
|
|
63
|
+
transform=ccrs.PlateCarree(),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
options.update(kwargs)
|
|
67
|
+
|
|
68
|
+
ax.tricontour(
|
|
69
|
+
triangulation,
|
|
70
|
+
values,
|
|
71
|
+
**options,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if title is not None:
|
|
75
|
+
ax.set_title(title)
|
|
76
|
+
|
|
77
|
+
return ax
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def plot_field(field, title=None, **kwargs):
|
|
81
|
+
values = field.to_numpy(flatten=True)
|
|
82
|
+
latitudes, longitudes = field.grid_points()
|
|
83
|
+
return plot_values(values, latitudes, longitudes, title=title, **kwargs)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# (C) Copyright 2025 Anemoi contributors.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
#
|
|
6
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
7
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
|
+
# nor does it submit to any jurisdiction.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
"""Utilities for working with grids.
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from io import BytesIO
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import requests
|
|
21
|
+
|
|
22
|
+
from .caching import cached
|
|
23
|
+
|
|
24
|
+
LOG = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
GRIDS_URL_PATTERN = "https://get.ecmwf.int/repository/anemoi/grids/grid-{name}.npz"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def xyz_to_latlon(x, y, z):
|
|
31
|
+
return (
|
|
32
|
+
np.rad2deg(np.arcsin(np.minimum(1.0, np.maximum(-1.0, z)))),
|
|
33
|
+
np.rad2deg(np.arctan2(y, x)),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def latlon_to_xyz(lat, lon, radius=1.0):
|
|
38
|
+
# https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates
|
|
39
|
+
# We assume that the Earth is a sphere of radius 1 so N(phi) = 1
|
|
40
|
+
# We assume h = 0
|
|
41
|
+
#
|
|
42
|
+
phi = np.deg2rad(lat)
|
|
43
|
+
lda = np.deg2rad(lon)
|
|
44
|
+
|
|
45
|
+
cos_phi = np.cos(phi)
|
|
46
|
+
cos_lda = np.cos(lda)
|
|
47
|
+
sin_phi = np.sin(phi)
|
|
48
|
+
sin_lda = np.sin(lda)
|
|
49
|
+
|
|
50
|
+
x = cos_phi * cos_lda * radius
|
|
51
|
+
y = cos_phi * sin_lda * radius
|
|
52
|
+
z = sin_phi * radius
|
|
53
|
+
|
|
54
|
+
return x, y, z
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def nearest_grid_points(source_latitudes, source_longitudes, target_latitudes, target_longitudes):
|
|
58
|
+
from scipy.spatial import cKDTree
|
|
59
|
+
|
|
60
|
+
source_xyz = latlon_to_xyz(source_latitudes, source_longitudes)
|
|
61
|
+
source_points = np.array(source_xyz).transpose()
|
|
62
|
+
|
|
63
|
+
target_xyz = latlon_to_xyz(target_latitudes, target_longitudes)
|
|
64
|
+
target_points = np.array(target_xyz).transpose()
|
|
65
|
+
|
|
66
|
+
_, indices = cKDTree(source_points).query(target_points, k=1)
|
|
67
|
+
return indices
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@cached(collection="grids", encoding="npz")
|
|
71
|
+
def _grids(name):
|
|
72
|
+
from anemoi.utils.config import load_config
|
|
73
|
+
|
|
74
|
+
user_path = load_config().get("utils", {}).get("grids_path")
|
|
75
|
+
if user_path:
|
|
76
|
+
path = os.path.expanduser(os.path.join(user_path, f"grid-{name}.npz"))
|
|
77
|
+
if os.path.exists(path):
|
|
78
|
+
LOG.warning("Loading grids from custom user path %s", path)
|
|
79
|
+
with open(path, "rb") as f:
|
|
80
|
+
return f.read()
|
|
81
|
+
else:
|
|
82
|
+
LOG.warning("Custom user path %s does not exist", path)
|
|
83
|
+
|
|
84
|
+
url = GRIDS_URL_PATTERN.format(name=name.lower())
|
|
85
|
+
LOG.warning("Downloading grids from %s", url)
|
|
86
|
+
response = requests.get(url)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
return response.content
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def grids(name):
|
|
92
|
+
if name.endswith(".npz"):
|
|
93
|
+
return dict(np.load(name))
|
|
94
|
+
|
|
95
|
+
data = _grids(name)
|
|
96
|
+
npz = np.load(BytesIO(data))
|
|
97
|
+
return dict(npz)
|