From 17c2847b0984ce1e68f43e5ed7655a927069881e Mon Sep 17 00:00:00 2001 From: Helen Theissen Date: Wed, 11 Sep 2024 15:20:01 +0100 Subject: [PATCH 1/6] Latest release 0.3.0 (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix (global version) (#12) * Global Encoder-Processor-Decoder graph (#9) * feat: Initial implementation of global graphs + fixes Co-authored by: Mario Santa Cruz Co-authored-by: Helen Theissen Co-authored-by: Sara Hahner Co-authored-by: Jesper Dramsch * fix: attributes as torch.float32 * new test: attributes must be float32 * fix typo * Homogeneize base builders * improve test docstrings * homogeneize (name as class attribute) * new input config * new default * remove dataclass from attribute classes * fix: config nodes name * 6 generate graphs from icosahedral meshes (#11) * Global Encoder-Processor-Decoder graph (#9) * feat: Initial implementation of global graphs Co-authored by: Mario Santa Cruz Co-authored-by: Helen Theissen Co-authored-by: Jesper Dramsch * fix: attributes as torch.float32 * new test: attributes must be float32 * fix typo * Homogeneize base builders * improve test docstrings * homogeneize (name as class attribute) * new input config * new default * feat: Initial implementation of global graphs Co-authored by: Mario Santa Cruz * add cli command * Ignore .pt files * run pre-commit * docstring + log erros * initial tests * feat: initial version of AttributeBuilder * refactor: separate into node edge attribute builders * feat: edge_length moved to edges/attributes.py * remove __init__ * bugfix (encoder edge lengths) + refector * feat: support path and dict for `config` argument * fix: error * refactor: naming * fix: pre-commit * feat: builders icosahedral * feat: Add icosahedral graph generation Co-authored-by: Mario Santa Cruz * refactor: remove create_shere * feat: Icosahedral edge builder * feat: hexagonal graph generation Co-authored-by: Mario Santa Cruz * feat: hexagonal builders * fix: AOI not implemented yet * fix: abstractmethod and renaming * chore: add dependencies * test: add tests for trimesh * test: add tests for hex (h3) * fix: imports * fix: output type * refactor: delete unused file * refactor: renaming and positioning * feat: ensure src and dst always the same * fix: imports * fix: edge_name not supported * test: add tests for TriIcosahedralEdges * fix: assert missing for Hexagonal edges * test: hexagonal edges * fix: avoid same name * fix: imports * fix: conflicts * update tests * Include xhops to hexagonal edges * docs: update docstrings * fix: update attribute name * refactor: rename multiscale nodes * refactor: rename icosahedral nodes * improve: clarity of function * improve: function syntax * refactor: simplify resolution assignment * refactor: improve variable naming in icosahedral graph generation * more comments * refactor: naming * refactor: separate into functions * refactor: remove unused code * refactor: remove unused options and rename * doc: clarify cells and nodes * fix: add return statements * homogeneize: tri & hex edges * naming: xhops to x_hops * naming: x_hops in docstring * docstring * blank lines * fix: add return statement * simplify icoshaedral edges * feat: select edge builder method based on ico node type * test: adjust tests to new MultiScaleEdges * docs: improve docstrings * fix: remove LAM filtering for icosahedral, leave for next PR * h3 (v4) not supported --------- * [feature] add changelog (#21) * ci: changelog check * feat: add changelog * fix: on develop * Skip no-commit-to-branch in code QA (#17) * Clean nodes after building the graph (#23) * feat: clean graph of unneeded attributes after creation Co-authored-by: Mario Santa Cruz Co-authored-by: Helen Theissen Co-authored-by: Jesper Dramsch * Make graph filename argument optional (#24) * hotfix: make graph saving optional Co-authored-by: Jesper Dramsch Co-authored-by: Mario Santa Cruz Co-authored-by: Helen Theissen * doc: update CHANGELOG Co-authored-by: Jesper Dramsch * chore: add rtd pr preview * HEALPix nodes (#16) [feature] HEALPix node builder Co-authored-by: theissenhelen * 25 integrate reusable workflows (#26) * ci: add public pr label * ci: add downstream-ci workflow * ci: add ci-config * fix: pre-commit * docs: update changelog * ci: switch main on downstream-ci * ci: run ci on push on main * Hotfix/clean graph (#28) * Add binary dependencies to ci-config.yml (#29) * Add binary dependencies to ci-config.yml * docs: update changelog --------- Co-authored-by: theissenhelen * ci: inherit pypi publish flow (#27) * ci: inherit pypi publish flow Co-authored-by: Jesper Dramsch * docs: update changelog * ci: only downstream-ci for changes in src and tests --------- Co-authored-by: Jesper Dramsch * Bugfix/31 ensure latlon input numpy array (#32) * #31 Ensure lat lon values passed as numpy arrays * fix formatting * Updated CHANGELOG.md * Remove bug in the graph cleaning function (#33) * fix: remove the bug in the graph cleaning function Co-authored-by: Jesper Dramsch * docs: update changelog --------- Co-authored-by: Jesper Dramsch Co-authored-by: Gert Mertes <13658335+gmertes@users.noreply.github.com> * Move to reusable workflows for QA and Testing (#34) * ci: move to reusable workflows for QA and Testing * docs: changelog add ci reusable workflow * ci: downstream ci ignore docs * ci: remove anchor, unsupported by github * ci: add changelog release updater (#35) * ci: changelog updater PR (#36) * ci: correct inputs typo * ci: fix changelog updater * docs: ci fixes changelog * ci: premissions * ci: typo * Ci/fix pr permission (#37) * ci: add pull-requests permissions * ci: add on workflow dispatch * ci: remove 3.9 tests (#39) * ci: remove 3.9 tests * ci: remove testing of python 3.9 * dics: update changelog * Fix support for Python 3.9 (#38) * fix: support py3.9 Co-authored-by: Jesper Dramsch * fix: update & homogeneize changelog * fix: update tests to support py39 * fix: style * ci: rollback 3.9 tests * refactor: annotations not used in tests --------- Co-authored-by: Jesper Dramsch * Ci/fix_double_notes (#40) * ci: avoid duplication of release notes * ci: try write-all * ci: fix permissions * docs: update changelog * ci: fix indent * Inspection tools (#22) Co-authored-by: Ana Prieto Nemesio Co-authored-by: Helen Theissen * [pre-commit.ci] pre-commit autoupdate (#41) updates: - [github.com/psf/black-pre-commit-mirror: 24.4.2 → 24.8.0](https://github.com/psf/black-pre-commit-mirror/compare/24.4.2...24.8.0) - [github.com/astral-sh/ruff-pre-commit: v0.4.6 → v0.6.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.6...v0.6.2) - [github.com/tox-dev/pyproject-fmt: 2.1.3 → 2.2.1](https://github.com/tox-dev/pyproject-fmt/compare/2.1.3...2.2.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * [pre-commit.ci] pre-commit autoupdate (#43) updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * First version of documentation (#18) Documentation web page --------- Co-authored-by: Helen Theissen Co-authored-by: Jesper Dramsch * [Changelog] Update to 0.3.0 (#44) * [create-pull-request] automated change * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: theissenhelen <23357105+theissenhelen@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --------- Co-authored-by: Mario Santa Cruz <48736305+JPXKQX@users.noreply.github.com> Co-authored-by: Mario Santa Cruz Co-authored-by: Jesper Dramsch Co-authored-by: Gert Mertes <13658335+gmertes@users.noreply.github.com> Co-authored-by: Mario Santa Cruz Co-authored-by: Iain Russell <40060766+iainrussell@users.noreply.github.com> Co-authored-by: Rilwan (Akanni) Adewoyin <18564167+Rilwan-Adewoyin@users.noreply.github.com> Co-authored-by: Ana Prieto Nemesio Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: theissenhelen <23357105+theissenhelen@users.noreply.github.com> --- .github/ci-config.yml | 9 + .github/workflows/changelog-pr-update.yml | 15 + .../workflows/changelog-release-update.yml | 35 ++ .github/workflows/ci.yml | 52 ++ .github/workflows/label-public-pr.yml | 10 + .github/workflows/python-publish.yml | 65 +-- .github/workflows/python-pull-request.yml | 23 + .github/workflows/readthedocs-pr-update.yml | 22 + .pre-commit-config.yaml | 6 +- CHANGELOG.md | 79 +++ docs/_static/cutoff.jpg | Bin 0 -> 25822 bytes docs/_static/hetero_data_graph.txt | 30 ++ docs/cli/create.rst | 15 + docs/cli/describe.rst | 15 + docs/cli/inspect.rst | 15 + docs/cli/introduction.rst | 29 ++ docs/dev/code_structure.rst | 81 +++ docs/dev/contributing.rst | 151 ++++++ docs/dev/testing.rst | 192 +++++++ docs/graphs/edge_attributes.rst | 44 ++ docs/graphs/edges.rst | 28 + docs/graphs/edges/cutoff.rst | 52 ++ docs/graphs/edges/knn.rst | 25 + docs/graphs/edges/multi_scale.rst | 35 ++ docs/graphs/introduction.rst | 78 +++ docs/graphs/node_attributes.rst | 23 + docs/graphs/node_attributes/weights.rst | 10 + docs/graphs/node_coordinates.rst | 37 ++ docs/graphs/node_coordinates/healpix.csv | 11 + docs/graphs/node_coordinates/healpix.rst | 30 ++ docs/graphs/node_coordinates/hex_refined.csv | 10 + .../hex_refined_icosahedron.rst | 36 ++ docs/graphs/node_coordinates/npz_file.rst | 30 ++ .../tri_refined_icosahedron.rst | 31 ++ docs/graphs/node_coordinates/zarr_dataset.rst | 39 ++ docs/graphs/yaml/attributes_weights.yaml | 10 + docs/index.rst | 91 +++- docs/installing.rst | 31 -- docs/modules/dates.rst | 8 - docs/modules/edge_attributes.rst | 11 + docs/modules/edge_builder.rst | 11 + docs/modules/graph_creator.rst | 14 + docs/modules/graph_inspector.rst | 17 + docs/modules/node_attributes.rst | 11 + docs/modules/node_builder.rst | 11 + docs/overview.rst | 127 +++++ docs/usage/getting_started.rst | 117 +++++ docs/usage/schemas/global.excalidraw | 479 ++++++++++++++++++ docs/usage/schemas/global.png | Bin 0 -> 21043 bytes docs/usage/schemas/global_wo-proc.excalidraw | 344 +++++++++++++ docs/usage/schemas/global_wo-proc.png | Bin 0 -> 18259 bytes docs/usage/yaml/global.txt | 21 + docs/usage/yaml/global.yaml | 23 + docs/usage/yaml/global_with-attrs.txt | 21 + docs/usage/yaml/global_with-attrs.yaml | 32 ++ docs/usage/yaml/global_wo-proc.png | Bin 0 -> 18259 bytes docs/usage/yaml/global_wo-proc.txt | 20 + docs/usage/yaml/global_wo-proc.yaml | 17 + docs/usage/yaml/nodes.yaml | 10 + docs/usage/yaml/nodes_with-attrs.yaml | 10 + pyproject.toml | 6 + src/anemoi/graphs/commands/create.py | 31 +- src/anemoi/graphs/commands/describe.py | 21 + src/anemoi/graphs/commands/inspect.py | 47 ++ src/anemoi/graphs/create.py | 109 ++-- src/anemoi/graphs/descriptor.py | 216 ++++++++ src/anemoi/graphs/edges/__init__.py | 3 +- src/anemoi/graphs/edges/attributes.py | 28 +- src/anemoi/graphs/edges/builder.py | 110 +++- src/anemoi/graphs/edges/directional.py | 4 +- src/anemoi/graphs/generate/hexagonal.py | 229 +++++++++ src/anemoi/graphs/generate/icosahedral.py | 205 ++++++++ src/anemoi/graphs/inspector.py | 63 +++ src/anemoi/graphs/nodes/__init__.py | 4 +- src/anemoi/graphs/nodes/attributes.py | 23 +- src/anemoi/graphs/nodes/builder.py | 171 ++++++- src/anemoi/graphs/nodes/weights.py | 61 +++ src/anemoi/graphs/plotting/__init__.py | 0 src/anemoi/graphs/plotting/displots.py | 103 ++++ .../graphs/plotting/interactive_html.py | 242 +++++++++ src/anemoi/graphs/plotting/prepare.py | 188 +++++++ src/anemoi/graphs/utils.py | 6 +- tests/conftest.py | 9 +- tests/edges/test_multiscale_edges.py | 64 +++ tests/nodes/test_healpix.py | 51 ++ tests/nodes/test_hex_nodes.py | 34 ++ tests/nodes/test_tri_nodes.py | 34 ++ tests/nodes/test_zarr.py | 2 +- tests/test_create.py | 54 ++ tests/test_graphs.py | 38 -- 90 files changed, 4703 insertions(+), 252 deletions(-) create mode 100644 .github/ci-config.yml create mode 100644 .github/workflows/changelog-pr-update.yml create mode 100644 .github/workflows/changelog-release-update.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/label-public-pr.yml create mode 100644 .github/workflows/python-pull-request.yml create mode 100644 .github/workflows/readthedocs-pr-update.yml create mode 100644 CHANGELOG.md create mode 100644 docs/_static/cutoff.jpg create mode 100644 docs/_static/hetero_data_graph.txt create mode 100644 docs/cli/create.rst create mode 100644 docs/cli/describe.rst create mode 100644 docs/cli/inspect.rst create mode 100644 docs/cli/introduction.rst create mode 100644 docs/dev/code_structure.rst create mode 100644 docs/dev/contributing.rst create mode 100644 docs/dev/testing.rst create mode 100644 docs/graphs/edge_attributes.rst create mode 100644 docs/graphs/edges.rst create mode 100644 docs/graphs/edges/cutoff.rst create mode 100644 docs/graphs/edges/knn.rst create mode 100644 docs/graphs/edges/multi_scale.rst create mode 100644 docs/graphs/introduction.rst create mode 100644 docs/graphs/node_attributes.rst create mode 100644 docs/graphs/node_attributes/weights.rst create mode 100644 docs/graphs/node_coordinates.rst create mode 100644 docs/graphs/node_coordinates/healpix.csv create mode 100644 docs/graphs/node_coordinates/healpix.rst create mode 100644 docs/graphs/node_coordinates/hex_refined.csv create mode 100644 docs/graphs/node_coordinates/hex_refined_icosahedron.rst create mode 100644 docs/graphs/node_coordinates/npz_file.rst create mode 100644 docs/graphs/node_coordinates/tri_refined_icosahedron.rst create mode 100644 docs/graphs/node_coordinates/zarr_dataset.rst create mode 100644 docs/graphs/yaml/attributes_weights.yaml delete mode 100644 docs/installing.rst delete mode 100644 docs/modules/dates.rst create mode 100644 docs/modules/edge_attributes.rst create mode 100644 docs/modules/edge_builder.rst create mode 100644 docs/modules/graph_creator.rst create mode 100644 docs/modules/graph_inspector.rst create mode 100644 docs/modules/node_attributes.rst create mode 100644 docs/modules/node_builder.rst create mode 100644 docs/overview.rst create mode 100644 docs/usage/getting_started.rst create mode 100644 docs/usage/schemas/global.excalidraw create mode 100644 docs/usage/schemas/global.png create mode 100644 docs/usage/schemas/global_wo-proc.excalidraw create mode 100644 docs/usage/schemas/global_wo-proc.png create mode 100644 docs/usage/yaml/global.txt create mode 100644 docs/usage/yaml/global.yaml create mode 100644 docs/usage/yaml/global_with-attrs.txt create mode 100644 docs/usage/yaml/global_with-attrs.yaml create mode 100644 docs/usage/yaml/global_wo-proc.png create mode 100644 docs/usage/yaml/global_wo-proc.txt create mode 100644 docs/usage/yaml/global_wo-proc.yaml create mode 100644 docs/usage/yaml/nodes.yaml create mode 100644 docs/usage/yaml/nodes_with-attrs.yaml create mode 100644 src/anemoi/graphs/commands/describe.py create mode 100644 src/anemoi/graphs/commands/inspect.py create mode 100644 src/anemoi/graphs/descriptor.py create mode 100644 src/anemoi/graphs/generate/hexagonal.py create mode 100644 src/anemoi/graphs/generate/icosahedral.py create mode 100644 src/anemoi/graphs/inspector.py create mode 100644 src/anemoi/graphs/nodes/weights.py create mode 100644 src/anemoi/graphs/plotting/__init__.py create mode 100644 src/anemoi/graphs/plotting/displots.py create mode 100644 src/anemoi/graphs/plotting/interactive_html.py create mode 100644 src/anemoi/graphs/plotting/prepare.py create mode 100644 tests/edges/test_multiscale_edges.py create mode 100644 tests/nodes/test_healpix.py create mode 100644 tests/nodes/test_hex_nodes.py create mode 100644 tests/nodes/test_tri_nodes.py create mode 100644 tests/test_create.py delete mode 100644 tests/test_graphs.py diff --git a/.github/ci-config.yml b/.github/ci-config.yml new file mode 100644 index 0000000..6138e63 --- /dev/null +++ b/.github/ci-config.yml @@ -0,0 +1,9 @@ +dependencies: | + ecmwf/ecbuild + MathisRosenhauer/libaec@master + ecmwf/eccodes + ecmwf/eckit + ecmwf/odc +dependency_branch: develop +parallelism_factor: 8 +self_build: false # Only for python packages diff --git a/.github/workflows/changelog-pr-update.yml b/.github/workflows/changelog-pr-update.yml new file mode 100644 index 0000000..4bc51df --- /dev/null +++ b/.github/workflows/changelog-pr-update.yml @@ -0,0 +1,15 @@ +name: Check Changelog Update on PR +on: + pull_request: + types: [assigned, opened, synchronize, reopened, labeled, unlabeled] + branches: + - main + - develop +jobs: + Check-Changelog: + name: Check Changelog Action + runs-on: ubuntu-20.04 + steps: + - uses: tarides/changelog-check-action@v2 + with: + changelog: CHANGELOG.md diff --git a/.github/workflows/changelog-release-update.yml b/.github/workflows/changelog-release-update.yml new file mode 100644 index 0000000..17d9525 --- /dev/null +++ b/.github/workflows/changelog-release-update.yml @@ -0,0 +1,35 @@ +# .github/workflows/update-changelog.yaml +name: "Update Changelog" + +on: + release: + types: [released] + workflow_dispatch: ~ + +permissions: + pull-requests: write + contents: write + +jobs: + update: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.target_commitish }} + + - name: Update Changelog + uses: stefanzweifel/changelog-updater-action@v1 + with: + latest-version: ${{ github.event.release.tag_name }} + heading-text: ${{ github.event.release.name }} + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + branch: docs/changelog-update-${{ github.event.release.tag_name }} + title: '[Changelog] Update to ${{ github.event.release.tag_name }}' + add-paths: | + CHANGELOG.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f9e9f91 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: ci + +on: + # Trigger the workflow on push to master or develop, except tag creation + push: + branches: + - 'main' + - 'develop' + tags-ignore: + - '**' + paths-ignore: + - "docs/**" + - "CHANGELOG.md" + - "README.md" + + # Trigger the workflow on pull request + pull_request: + paths-ignore: + - "docs/**" + - "CHANGELOG.md" + - "README.md" + + # Trigger the workflow manually + workflow_dispatch: ~ + + # Trigger after public PR approved for CI + pull_request_target: + types: [labeled] + paths-ignore: + - "docs/**" + - "CHANGELOG.md" + - "README.md" + +jobs: + # Run CI including downstream packages on self-hosted runners + downstream-ci: + name: downstream-ci + if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} + uses: ecmwf-actions/downstream-ci/.github/workflows/downstream-ci.yml@main + with: + anemoi-graphs: ecmwf/anemoi-graphs@${{ github.event.pull_request.head.sha || github.sha }} + codecov_upload: true + secrets: inherit + + # Build downstream packages on HPC + downstream-ci-hpc: + name: downstream-ci-hpc + if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} + uses: ecmwf-actions/downstream-ci/.github/workflows/downstream-ci.yml@main + with: + anemoi-graphs: ecmwf/anemoi-graphs@${{ github.event.pull_request.head.sha || github.sha }} + secrets: inherit diff --git a/.github/workflows/label-public-pr.yml b/.github/workflows/label-public-pr.yml new file mode 100644 index 0000000..59b2bfa --- /dev/null +++ b/.github/workflows/label-public-pr.yml @@ -0,0 +1,10 @@ +# Manage labels of pull requests that originate from forks +name: label-public-pr + +on: + pull_request_target: + types: [opened, synchronize] + +jobs: + label: + uses: ecmwf-actions/reusable-workflows/.github/workflows/label-pr.yml@v2 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 666f65d..2cb554a 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -4,71 +4,24 @@ name: Upload Python Package on: - - push: {} - release: types: [created] jobs: quality: - name: Code QA - runs-on: ubuntu-latest - steps: - - run: sudo apt-get install -y pandoc # Needed by sphinx for notebooks - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - uses: pre-commit/action@v3.0.1 + uses: ecmwf-actions/reusable-workflows/.github/workflows/qa-precommit-run.yml@v2 + with: + skip-hooks: "no-commit-to-branch" checks: strategy: - fail-fast: false matrix: - platform: ["ubuntu-latest", "macos-latest"] - python-version: ["3.10"] - - name: Python ${{ matrix.python-version }} on ${{ matrix.platform }} - runs-on: ${{ matrix.platform }} - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install - run: | - pip install -e .[all,tests] - pip freeze - - - name: Tests - run: pytest + python-version: ["3.9", "3.10"] + uses: ecmwf-actions/reusable-workflows/.github/workflows/qa-pytest-pyproject.yml@v2 + with: + python-version: ${{ matrix.python-version }} deploy: - - if: ${{ github.event_name == 'release' }} - runs-on: ubuntu-latest needs: [checks, quality] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.x - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build wheel twine - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - python -m build - twine upload dist/* + uses: ecmwf-actions/reusable-workflows/.github/workflows/cd-pypi.yml@v2 + secrets: inherit diff --git a/.github/workflows/python-pull-request.yml b/.github/workflows/python-pull-request.yml new file mode 100644 index 0000000..0ebecb1 --- /dev/null +++ b/.github/workflows/python-pull-request.yml @@ -0,0 +1,23 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Code Quality checks for PRs + +on: + push: + pull_request_target: + types: [opened, synchronize, reopened] + +jobs: + quality: + uses: ecmwf-actions/reusable-workflows/.github/workflows/qa-precommit-run.yml@v2 + with: + skip-hooks: "no-commit-to-branch" + + checks: + strategy: + matrix: + python-version: ["3.9", "3.10"] + uses: ecmwf-actions/reusable-workflows/.github/workflows/qa-pytest-pyproject.yml@v2 + with: + python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/readthedocs-pr-update.yml b/.github/workflows/readthedocs-pr-update.yml new file mode 100644 index 0000000..ebf07e7 --- /dev/null +++ b/.github/workflows/readthedocs-pr-update.yml @@ -0,0 +1,22 @@ +name: Read the Docs PR Preview +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + # Execute this action only on PRs that touch + # documentation files. + paths: + - "docs/**" + +permissions: + pull-requests: write + +jobs: + documentation-links: + runs-on: ubuntu-latest + steps: + - uses: readthedocs/actions/preview@v1 + with: + project-slug: "anemoi-graphs" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4b6367..c042b1f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - id: check-added-large-files # Check for large files added to git - id: check-merge-conflict # Check for files that contain merge conflict - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black args: [--line-length=120] @@ -34,7 +34,7 @@ repos: - --force-single-line-imports - --profile black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.6 + rev: v0.6.3 hooks: - id: ruff # Next line if for documenation cod snippets @@ -65,6 +65,6 @@ repos: - id: optional-dependencies-all args: ["--inplace", "--exclude-keys=dev,docs,tests", "--group=dev=all,docs,tests"] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "2.1.3" + rev: "2.2.1" hooks: - id: pyproject-fmt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5beab08 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# Changelog + +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). + +Please add your functional changes to the appropriate section in the PR. +Keep it human-readable, your future self will thank you! + +## [Unreleased](https://github.com/ecmwf/anemoi-graphs/compare/0.3.0...HEAD) + +## [0.3.0 Anemoi-graphs, minor release](https://github.com/ecmwf/anemoi-graphs/compare/0.2.1...0.3.0) - 2024-09-03 + +### Added + +- HEALPixNodes - nodebuilder based on Hierarchical Equal Area isoLatitude Pixelation of a sphere. + +- Inspection tools: interactive plots, and distribution plots of edge & node attributes. + +- Graph description print in the console. + +- CLI entry point: 'anemoi-graphs inspect ...'. + +- added downstream-ci pipeline and cd-pypi reusable workflow + +- Changelog release updater + +- Create package documentation. + + +### Changed + +- fix: added support for Python3.9. +- fix: bug in graph cleaning method +- fix: `anemoi-graphs create` CLI argument is casted to a Path. +- ci: fix missing binary dependency in ci-config.yaml +- fix: Updated `get_raw_values` method in `AreaWeights` to ensure compatibility with `scipy.spatial.SphericalVoronoi` by converting `latitudes` and `longitudes` to NumPy arrays before passing them to the `latlon_rad_to_cartesian` function. This resolves an issue where the function would fail if passed Torch Tensors directly. +- ci: Reusable workflows for push, PR, and releases +- ci: ignore docs for downstream ci +- ci: changed Changelog action to create PR +- ci: fixes and permissions on changelog updater + +### Removed + +## [0.2.1](https://github.com/ecmwf/anemoi-graphs/compare/0.2.0...0.2.1) - Anemoi-graph Release, bug fix release + +### Added + +### Changed + +- Fix The 'save_path' argument of the GraphCreator class is optional, allowing users to create graphs without saving them. + +### Removed + +## [0.2.0](https://github.com/ecmwf/anemoi-graphs/compare/0.1.0...0.2.0) - Anemoi-graph Release, Icosahedral graph building + +### Added + +- New node builders by iteratively refining an icosahedron: TriNodes, HexNodes. +- New edge builders for building multi-scale connections. +- Added Changelog + +### Changed + +### Removed + +## [0.1.0](https://github.com/ecmwf/anemoi-graphs/releases/tag/0.1.0) - Initial Release, Global graph building + +### Added + +- Documentation +- Initial implementation for global graph building on the fly from Zarr and NPZ datasets + +### Changed + +### Removed + + diff --git a/docs/_static/cutoff.jpg b/docs/_static/cutoff.jpg new file mode 100644 index 0000000000000000000000000000000000000000..90f20dd0f77b1993f2dad5fcdc7031b44be73ec3 GIT binary patch literal 25822 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