├── .binder
└── environment.yml
├── .deepsource.toml
├── .github
├── dependabot.yml
├── release.yml
└── workflows
│ ├── ci.yaml
│ ├── parse_logs.py
│ ├── pypi.yaml
│ ├── testpypi-release.yaml
│ └── upstream-dev-ci.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── .tributors
├── CITATION.cff
├── LICENSE
├── README.rst
├── cf_xarray
├── __init__.py
├── accessor.py
├── coding.py
├── criteria.py
├── datasets.py
├── formatting.py
├── geometry.py
├── groupers.py
├── helpers.py
├── options.py
├── parametric.py
├── py.typed
├── scripts
│ ├── make_doc.py
│ └── print_versions.py
├── sgrid.py
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_accessor.py
│ ├── test_coding.py
│ ├── test_geometry.py
│ ├── test_groupers.py
│ ├── test_helpers.py
│ ├── test_options.py
│ ├── test_parametric.py
│ ├── test_scripts.py
│ └── test_units.py
├── units.py
└── utils.py
├── ci
├── doc.yml
├── environment-all-min-deps.yml
├── environment-no-optional-deps.yml
├── environment.yml
└── upstream-dev-env.yml
├── codecov.yml
├── doc
├── 2D_bounds_averaged.png
├── 2D_bounds_error.png
├── 2D_bounds_nonunique.png
├── Makefile
├── _static
│ ├── dataset-diagram-logo.tex
│ ├── full-logo.png
│ ├── logo.png
│ ├── logo.svg
│ ├── rich-repr-example.png
│ └── style.css
├── api.rst
├── bounds.md
├── cartopy_rotated_pole.png
├── coding.md
├── conf.py
├── contributing.rst
├── coord_axes.md
├── custom-criteria.md
├── dsg.md
├── examples
│ └── introduction.ipynb
├── faq.md
├── flags.md
├── geometry.md
├── grid_mappings.md
├── howtouse.md
├── index.rst
├── make.bat
├── parametricz.md
├── plotting.md
├── provenance.md
├── quickstart.md
├── roadmap.rst
├── selecting.md
├── sgrid_ugrid.md
├── units.md
└── whats-new.rst
└── pyproject.toml
/.binder/environment.yml:
--------------------------------------------------------------------------------
1 | name: cf_xarray
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - python=3
6 | - matplotlib-base
7 | - netcdf4
8 | - pip
9 | - xarray
10 | - pooch
11 | - pip:
12 | - git+https://github.com/xarray-contrib/cf-xarray
13 |
--------------------------------------------------------------------------------
/.deepsource.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | test_patterns = ["cf_xarray/tests/test_*.py"]
4 |
5 | exclude_patterns = [
6 | "doc/**",
7 | "ci/**"
8 | ]
9 |
10 | [[analyzers]]
11 | name = "python"
12 | enabled = true
13 |
14 | [analyzers.meta]
15 | runtime_version = "3.x.x"
16 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | authors:
4 | - dependabot
5 | - pre-commit-ci
6 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - "main"
6 | pull_request:
7 | branches:
8 | - "*"
9 | schedule:
10 | - cron: "0 13 * * 1"
11 |
12 | concurrency:
13 | group: ${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | env:
17 | COLUMNS: 120
18 |
19 | jobs:
20 | build:
21 | name: Build (${{ matrix.python-version }}, ${{ matrix.os }}), ${{ matrix.env }}
22 | runs-on: ${{ matrix.os }}
23 | defaults:
24 | run:
25 | shell: bash -l {0}
26 | strategy:
27 | fail-fast: false
28 | matrix:
29 | os: ["ubuntu-latest"]
30 | python-version: ["3.10", "3.13"]
31 | env: [""]
32 | include:
33 | - env: "all-min-deps"
34 | python-version: "3.10"
35 | os: ubuntu-latest
36 | - env: "no-optional-deps"
37 | python-version: "3.13"
38 | os: ubuntu-latest
39 | steps:
40 | - uses: actions/checkout@v4
41 | with:
42 | fetch-depth: 0 # Fetch all history for all branches and tags.
43 | - name: Set environment variables
44 | run: |
45 | echo "PYTHON_VERSION=${{ matrix.python-version }}" >> $GITHUB_ENV
46 | if [[ "${{ matrix.env }}" == "" ]] ;
47 | then
48 | echo "CONDA_ENV_FILE=ci/environment.yml" >> $GITHUB_ENV
49 | fi
50 | if [[ "${{ matrix.env }}" == "no-optional-deps" ]] ;
51 | then
52 | echo "CONDA_ENV_FILE=ci/environment-no-optional-deps.yml" >> $GITHUB_ENV
53 | fi
54 | if [[ "${{ matrix.env }}" == "all-min-deps" ]] ;
55 | then
56 | echo "CONDA_ENV_FILE=ci/environment-all-min-deps.yml" >> $GITHUB_ENV
57 | fi
58 | - name: Set up conda environment
59 | uses: mamba-org/setup-micromamba@v2
60 | with:
61 | environment-file: ${{ env.CONDA_ENV_FILE }}
62 | environment-name: cf_xarray_test
63 | cache-environment: true
64 | create-args: >-
65 | python=${{ matrix.python-version }}
66 | - name: Install cf_xarray
67 | run: |
68 | python -m pip install --no-deps -e .
69 | - name: Run Tests
70 | run: |
71 | pytest -n auto --cov=./ --cov-report=xml
72 | - name: Upload code coverage to Codecov
73 | uses: codecov/codecov-action@v5.4.2
74 | with:
75 | file: ./coverage.xml
76 | flags: unittests
77 | env_vars: RUNNER_OS,PYTHON_VERSION
78 | name: codecov-umbrella
79 | fail_ci_if_error: false
80 |
81 | mypy:
82 | name: mypy
83 | runs-on: "ubuntu-latest"
84 | defaults:
85 | run:
86 | shell: bash -l {0}
87 | strategy:
88 | matrix:
89 | python-version: ["3.10", "3.13"]
90 | steps:
91 | - uses: actions/checkout@v4
92 | with:
93 | fetch-depth: 0 # Fetch all history for all branches and tags.
94 | - name: Set up conda environment
95 | uses: mamba-org/setup-micromamba@v2
96 | with:
97 | environment-file: ci/environment.yml
98 | environment-name: cf_xarray_test
99 | cache-environment: true
100 | create-args: >-
101 | python=${{ matrix.python-version }}
102 | - name: Install cf_xarray
103 | run: |
104 | python -m pip install --no-deps -e .
105 | - name: Install mypy
106 | run: |
107 | python -m pip install 'mypy'
108 | - name: Run mypy
109 | run: |
110 | python -m mypy --install-types --non-interactive --cobertura-xml-report mypy_report cf_xarray/
111 | - name: Upload mypy coverage to Codecov
112 | uses: codecov/codecov-action@v5.4.2
113 | with:
114 | file: mypy_report/cobertura.xml
115 | flags: mypy
116 | env_vars: PYTHON_VERSION
117 | name: codecov-umbrella
118 | fail_ci_if_error: false
119 |
--------------------------------------------------------------------------------
/.github/workflows/parse_logs.py:
--------------------------------------------------------------------------------
1 | # type: ignore
2 | import argparse
3 | import functools
4 | import json
5 | import pathlib
6 | import textwrap
7 | from dataclasses import dataclass
8 |
9 | from pytest import CollectReport, TestReport
10 |
11 |
12 | @dataclass
13 | class SessionStart:
14 | pytest_version: str
15 | outcome: str = "status"
16 |
17 | @classmethod
18 | def _from_json(cls, json):
19 | json_ = json.copy()
20 | json_.pop("$report_type")
21 | return cls(**json_)
22 |
23 |
24 | @dataclass
25 | class SessionFinish:
26 | exitstatus: str
27 | outcome: str = "status"
28 |
29 | @classmethod
30 | def _from_json(cls, json):
31 | json_ = json.copy()
32 | json_.pop("$report_type")
33 | return cls(**json_)
34 |
35 |
36 | def parse_record(record):
37 | report_types = {
38 | "TestReport": TestReport,
39 | "CollectReport": CollectReport,
40 | "SessionStart": SessionStart,
41 | "SessionFinish": SessionFinish,
42 | }
43 | cls = report_types.get(record["$report_type"])
44 | if cls is None:
45 | raise ValueError(f"unknown report type: {record['$report_type']}")
46 |
47 | return cls._from_json(record)
48 |
49 |
50 | @functools.singledispatch
51 | def format_summary(report):
52 | return f"{report.nodeid}: {report}"
53 |
54 |
55 | @format_summary.register
56 | def _(report: TestReport):
57 | message = report.longrepr.chain[0][1].message
58 | return f"{report.nodeid}: {message}"
59 |
60 |
61 | @format_summary.register
62 | def _(report: CollectReport):
63 | message = report.longrepr.split("\n")[-1].removeprefix("E").lstrip()
64 | return f"{report.nodeid}: {message}"
65 |
66 |
67 | def format_report(reports, py_version):
68 | newline = "\n"
69 | summaries = newline.join(format_summary(r) for r in reports)
70 | message = textwrap.dedent(
71 | """\
72 | Python {py_version} Test Summary
73 |
74 | ```
75 | {summaries}
76 | ```
77 |
78 |
79 | """
80 | ).format(summaries=summaries, py_version=py_version)
81 | return message
82 |
83 |
84 | if __name__ == "__main__":
85 | parser = argparse.ArgumentParser()
86 | parser.add_argument("filepath", type=pathlib.Path)
87 | args = parser.parse_args()
88 |
89 | py_version = args.filepath.stem.split("-")[1]
90 |
91 | print("Parsing logs ...")
92 |
93 | lines = args.filepath.read_text().splitlines()
94 | reports = [parse_record(json.loads(line)) for line in lines]
95 |
96 | failed = [report for report in reports if report.outcome == "failed"]
97 |
98 | message = format_report(failed, py_version=py_version)
99 |
100 | output_file = pathlib.Path("pytest-logs.txt")
101 | print(f"Writing output file to: {output_file.absolute()}")
102 | output_file.write_text(message)
103 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yaml:
--------------------------------------------------------------------------------
1 | name: Build and Upload to PyPI
2 | on:
3 | release:
4 | types:
5 | - published
6 | push:
7 | tags:
8 | - 'v*'
9 |
10 | jobs:
11 | build-artifacts:
12 | runs-on: ubuntu-latest
13 | if: github.repository == 'xarray-contrib/cf-xarray'
14 | steps:
15 | - uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 | - uses: actions/setup-python@v5
19 | name: Install Python
20 | with:
21 | python-version: "3.10"
22 |
23 | - name: Install dependencies
24 | run: |
25 | python -m pip install --upgrade pip
26 | python -m pip install build twine
27 |
28 | - name: Build tarball and wheels
29 | run: |
30 | git clean -xdf
31 | git restore -SW .
32 | python -m build
33 |
34 | - name: Check built artifacts
35 | run: |
36 | python -m twine check --strict dist/*
37 | pwd
38 | if [ -f dist/cf_xarray-0.0.0.tar.gz ]; then
39 | echo "❌ INVALID VERSION NUMBER"
40 | exit 1
41 | else
42 | echo "✅ Looks good"
43 | fi
44 | - uses: actions/upload-artifact@v4
45 | with:
46 | name: releases
47 | path: dist
48 |
49 | test-built-dist:
50 | needs: build-artifacts
51 | runs-on: ubuntu-latest
52 | steps:
53 | - uses: actions/setup-python@v5
54 | name: Install Python
55 | with:
56 | python-version: "3.10"
57 | - uses: actions/download-artifact@v4
58 | with:
59 | name: releases
60 | path: dist
61 | - name: List contents of built dist
62 | run: |
63 | ls -ltrh
64 | ls -ltrh dist
65 |
66 | - name: Verify the built dist/wheel is valid
67 | if: github.event_name == 'push'
68 | run: |
69 | python -m pip install --upgrade pip
70 | python -m pip install dist/cf_xarray*.whl
71 | python -m cf_xarray.scripts.print_versions
72 |
73 | - name: Publish package to TestPyPI
74 | if: github.event_name == 'push'
75 | uses: pypa/gh-action-pypi-publish@v1.12.4
76 | with:
77 | password: ${{ secrets.TESTPYPI_TOKEN }}
78 | repository_url: https://test.pypi.org/legacy/
79 | verbose: true
80 |
81 |
82 | upload-to-pypi:
83 | needs: test-built-dist
84 | if: github.event_name == 'release'
85 | runs-on: ubuntu-latest
86 |
87 | environment:
88 | name: pypi
89 | url: https://pypi.org/p/cf-xarray
90 | permissions:
91 | id-token: write
92 |
93 | steps:
94 | - uses: actions/download-artifact@v4
95 | with:
96 | name: releases
97 | path: dist
98 | - name: Publish package to PyPI
99 | uses: pypa/gh-action-pypi-publish@v1.12.4
100 | with:
101 | verbose: true
102 |
--------------------------------------------------------------------------------
/.github/workflows/testpypi-release.yaml:
--------------------------------------------------------------------------------
1 | name: Build and Upload to TestPyPI
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | pull_request:
8 | types: [opened, reopened, synchronize, labeled]
9 | branches:
10 | - "*"
11 | workflow_dispatch:
12 |
13 | # no need for concurrency limits
14 |
15 | jobs:
16 | build-artifacts:
17 | if: ${{ contains( github.event.pull_request.labels.*.name, 'test-build') && github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }}
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v4
21 | with:
22 | fetch-depth: 0
23 |
24 | - uses: actions/setup-python@v5
25 | name: Install Python
26 | with:
27 | python-version: "3.10"
28 |
29 | - name: Install dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | python -m pip install build twine
33 | python -m pip install tomli tomli_w
34 |
35 | # - name: Disable local versions
36 | # run: |
37 | # python .github/workflows/configure-testpypi-version.py pyproject.toml
38 | # git update-index --assume-unchanged pyproject.toml
39 | # cat pyproject.toml
40 |
41 | - name: Build tarball and wheels
42 | run: |
43 | git clean -xdf
44 | python -m build
45 |
46 | - name: Check built artifacts
47 | run: |
48 | python -m twine check --strict dist/*
49 | if [ -f dist/cf_xarray-0.0.0.tar.gz ]; then
50 | echo "❌ INVALID VERSION NUMBER"
51 | exit 1
52 | else
53 | echo "✅ Looks good"
54 | fi
55 |
56 | - uses: actions/upload-artifact@v4
57 | with:
58 | name: releases
59 | path: dist
60 |
61 | test-built-dist:
62 | needs: build-artifacts
63 | runs-on: ubuntu-latest
64 | steps:
65 | - uses: actions/setup-python@v5
66 | name: Install Python
67 | with:
68 | python-version: "3.10"
69 | - uses: actions/download-artifact@v4
70 | with:
71 | name: releases
72 | path: dist
73 | - name: List contents of built dist
74 | run: |
75 | ls -ltrh
76 | ls -ltrh dist
77 |
78 | - name: Verify the built dist/wheel is valid
79 | run: |
80 | python -m pip install --upgrade pip
81 | python -m pip install dist/cf_xarray*.whl
82 | python -m cf_xarray.scripts.print_versions
83 |
84 | # - name: Publish package to TestPyPI
85 | # uses: pypa/gh-action-pypi-publish@v1.6.4
86 | # with:
87 | # password: ${{ secrets.TESTPYPI_TOKEN }}
88 | # repository_url: https://test.pypi.org/legacy/
89 | # verbose: true
90 |
--------------------------------------------------------------------------------
/.github/workflows/upstream-dev-ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI Upstream
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | types: [opened, reopened, synchronize, labeled]
8 | branches:
9 | - main
10 | schedule:
11 | - cron: "0 0 * * *" # Daily “At 00:00” UTC
12 | workflow_dispatch: # allows you to trigger the workflow run manually
13 |
14 | concurrency:
15 | group: ${{ github.workflow }}-${{ github.ref }}
16 | cancel-in-progress: true
17 |
18 | env:
19 | COLUMNS: 120
20 |
21 | jobs:
22 | upstream-dev:
23 | name: upstream-dev
24 | runs-on: ubuntu-latest
25 | if: ${{ (contains( github.event.pull_request.labels.*.name, 'test-upstream') && github.event_name == 'pull_request') || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }}
26 | defaults:
27 | run:
28 | shell: bash -l {0}
29 | strategy:
30 | fail-fast: false
31 | matrix:
32 | python-version: ["3.13"]
33 | steps:
34 | - uses: actions/checkout@v4
35 | with:
36 | fetch-depth: 0 # Fetch all history for all branches and tags.
37 | - name: Set environment variables
38 | run: |
39 | echo "PYTHON_VERSION=${{ matrix.python-version }}" >> $GITHUB_ENV
40 | - name: Set up conda environment
41 | uses: mamba-org/setup-micromamba@v2
42 | with:
43 | environment-file: ci/upstream-dev-env.yml
44 | environment-name: cf_xarray_test
45 | cache-env: False
46 | create-args: >-
47 | python=${{ matrix.python-version }}
48 | - name: Install cf_xarray
49 | run: |
50 | python -m pip install --no-deps -e .
51 | - name: Run Tests
52 | if: success()
53 | id: status
54 | run: |
55 | pytest -rf -n auto --cov=./ --cov-report=xml \
56 | --report-log output-${{ matrix.python-version }}-log.jsonl
57 | - name: Generate and publish the report
58 | if: |
59 | failure()
60 | && steps.status.outcome == 'failure'
61 | && github.event_name == 'schedule'
62 | && github.repository_owner == 'xarray-contrib'
63 | uses: xarray-contrib/issue-from-pytest-log@v1
64 | with:
65 | log-path: output-${{ matrix.python-version }}-log.jsonl
66 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 | cf_xarray/_version.py
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | doc/_build/
74 | doc/generated/
75 | cf_xarray/tests/_build/
76 |
77 | # PyBuilder
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # Pycharm config
135 | .idea
136 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ci:
2 | autoupdate_schedule: quarterly
3 |
4 | repos:
5 | - repo: https://github.com/asottile/pyupgrade
6 | rev: v3.19.1
7 | hooks:
8 | - id: pyupgrade
9 | args: ["--py310-plus"]
10 |
11 | - repo: https://github.com/astral-sh/ruff-pre-commit
12 | # Ruff version.
13 | rev: 'v0.9.3'
14 | hooks:
15 | - id: ruff
16 | args: ["--fix", "--show-fixes"]
17 | - id: ruff-format
18 |
19 | - repo: https://github.com/rstcheck/rstcheck
20 | rev: v6.2.4
21 | hooks:
22 | - id: rstcheck
23 | additional_dependencies: [sphinx, tomli]
24 | args: ['--config', 'pyproject.toml']
25 |
26 | - repo: https://github.com/executablebooks/mdformat
27 | rev: 0.7.21
28 | hooks:
29 | - id: mdformat
30 | additional_dependencies:
31 | - mdformat-black
32 | - mdformat-myst
33 |
34 | - repo: https://github.com/nbQA-dev/nbQA
35 | rev: 1.9.1
36 | hooks:
37 | - id: nbqa
38 | entry: nbqa mdformat
39 | name: nbqa-mdformat
40 | alias: nbqa-mdformat
41 | additional_dependencies: [mdformat==0.7.17]
42 |
43 | - repo: https://github.com/pre-commit/pre-commit-hooks
44 | rev: v5.0.0
45 | hooks:
46 | - id: trailing-whitespace
47 | - id: end-of-file-fixer
48 | - id: check-toml
49 | - id: check-yaml
50 | - id: debug-statements
51 |
52 | - repo: https://github.com/citation-file-format/cff-converter-python
53 | rev: "44e8fc9"
54 | hooks:
55 | - id: validate-cff
56 |
57 | - repo: https://github.com/abravalheri/validate-pyproject
58 | rev: v0.23
59 | hooks:
60 | - id: validate-pyproject
61 |
62 | - repo: https://github.com/pre-commit/pygrep-hooks
63 | rev: v1.10.0
64 | hooks:
65 | # - id: python-check-blanket-type-ignore
66 | - id: rst-backticks
67 | - id: rst-directive-colons
68 | - id: rst-inline-touching-normal
69 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | sphinx:
3 | configuration: doc/conf.py
4 | build:
5 | os: ubuntu-lts-latest
6 | tools:
7 | python: mambaforge-latest
8 | jobs:
9 | post_checkout:
10 | - (git --no-pager log --pretty="tformat:%s" -1 | grep -vqF "[skip-rtd]") || exit 183
11 | pre_install:
12 | - conda list sphinx
13 | - conda list cf_xarray
14 | - conda list
15 |
16 | conda:
17 | environment: ci/doc.yml
18 |
19 | formats: []
20 |
--------------------------------------------------------------------------------
/.tributors:
--------------------------------------------------------------------------------
1 | {
2 | "dcherian": {
3 | "name": "Deepak Cherian",
4 | "bio": "physical oceanographer",
5 | "blog": "http://www.cherian.net",
6 | "orcid": "0000-0002-6861-8734",
7 | "affiliation": "National Center for Atmospheric Research"
8 | },
9 | "malmans2": {
10 | "name": "Mattia Almansi",
11 | "blog": "https://malmans2.github.io",
12 | "orcid": "0000-0001-6849-3647",
13 | "affiliation": "National Oceanography Centre",
14 | "bio": "Scientific Software Engineer @bopen\r\n\r\nFormerly: Physical Oceanographer specialized in high performance computing at JHU and NOC"
15 | },
16 | "aulemahal": {
17 | "name": "Pascal Bourgault",
18 | "bio": "Physical oceanography graduate turned climate science specialist and scientific developer.",
19 | "orcid": "0000-0003-1192-0403",
20 | "affiliation": "Ouranos Inc"
21 | },
22 | "keewis": {
23 | "name": "Justus Magin",
24 | "orcid": "0000-0002-4254-8002",
25 | "affiliation": "IFREMER"
26 | },
27 | "jukent": {
28 | "name": "Julia Kent",
29 | "affiliation": "National Center for Atmospheric Research",
30 | "orcid": "0000-0002-5611-8986"
31 | },
32 | "kthyng": {
33 | "name": "Kristen Thyng",
34 | "bio": "MetOcean Data Scientist at Axiom Data Science. Associate Editor in Chief at the Journal for Open Source Software (JOSS). Wrote cmocean colormaps.",
35 | "blog": "http://kristenthyng.com",
36 | "orcid": "0000-0002-8746-614X",
37 | "affiliation": "Axiom Data Science"
38 | },
39 | "jhamman": {
40 | "name": "Joe Hamman",
41 | "bio": "Scientist and Engineer and Human.\r\n",
42 | "blog": "http://joehamman.com",
43 | "orcid": "0000-0001-7479-8439",
44 | "affiliation": "CarbonPlan"
45 | },
46 | "withshubh": {
47 | "name": "Shubhendra Singh Chauhan",
48 | "bio": "Developer Advocate at @deepsourcelabs 🥑 \r\n👨🏻💻 work profile: @shubhendra-deepsource",
49 | "blog": "camelcaseguy.com"
50 | },
51 | "ocefpaf": {
52 | "name": "Filipe Fernandes",
53 | "bio": "Filipe Fernandes is a Physical oceanographer by training, turned research software engineer, and a software packager hobbyist.",
54 | "blog": "http://ocefpaf.github.io/python4oceanographers",
55 | "orcid": "0000-0003-4165-2913",
56 | "affiliation": "IOOS"
57 | },
58 | "rcaneill": {
59 | "name": "Romain Caneill",
60 | "bio": "PhD student in Physical Oceanography. Doing numerical modelling using NEMO, and obs analyzes. ",
61 | "blog": "https://fediscience.org/@rcaneill",
62 | "orcid": "0000-0001-6649-4275",
63 | "affiliation": "Gothenburg University"
64 | },
65 | "sol1105": {
66 | "name": "Martin Schupfner",
67 | "orcid": "0000-0001-8075-589X",
68 | "affiliation": "DKRZ (German Climate Computing Center)"
69 | },
70 | "mathause": {
71 | "name": "Mathias Hauser",
72 | "orcid": "0000-0002-0057-4878",
73 | "affiliation": "ETH Zurich"
74 | },
75 | "bzah": {
76 | "name": "Abel Aoun",
77 | "bio": "Software engineer for climate and earth observation data processing.",
78 | "orcid": "0000-0003-2289-2890",
79 | "affiliation": "CERFACS"
80 | },
81 | "jthielen": {
82 | "name": "Jonathan Thielen",
83 | "bio": "Meteorology graduate student @ CSU. I mostly do research with MCSs and machine learning. A bit of a Python fanatic.",
84 | "blog": "thielen.science",
85 | "orcid": "0000-0002-5479-0189"
86 | },
87 | "mankoff": {
88 | "name": "Ken Mankoff",
89 | "bio": "Research scientist studying ice sheets and polar oceans as indicators of climate change. Advocate of open reproducible science.",
90 | "blog": "http://kenmankoff.com",
91 | "orcid": "0000-0001-5453-2019",
92 | "affiliation": "Goddard Institute for Space Studies"
93 | },
94 | "larsbuntemeyer": {
95 | "name": "Lars Buntemeyer",
96 | "blog": "larsbuntemeyer.github.io",
97 | "orcid": "0000-0002-0849-2404",
98 | "affiliation": "Helmholtz-Zentrum Hereon"
99 | },
100 | "lukelbd": {
101 | "name": "Luke Davis",
102 | "bio": "PhD candidate in climate science at the Department of Atmospheric Science, Colorado State University.",
103 | "orcid": "0000-0003-1072-4638"
104 | },
105 | "MuellerSeb": {
106 | "name": "Sebastian Müller",
107 | "orcid": "0000-0001-9060-4008",
108 | "affiliation": "Helmholtz Centre for Environmental Research - UFZ",
109 | "bio": "I'm a Model-Keeper at the Department for Computational Hydrosystems within the Helmholtz Centre for Environmental Research (UFZ) in Leipzig.",
110 | "blog": "https://geostat-framework.org"
111 | },
112 | "tomvothecoder": {
113 | "name": "Tom Vo",
114 | "bio": "Climate Science | Software Engineer | Python | Full Stack (React, Django, TypeScript) | DevOps",
115 | "blog": "tomvo.me/career",
116 | "orcid": "0000-0002-2461-0191",
117 | "affiliation": "Lawrence Livermore National Laboratory"
118 | },
119 | "aidanheerdegen": {
120 | "name": "Aidan Heerdegen",
121 | "bio": "Model Release Team Lead for the Australian Earth-System Simulator National Research Infrastructure (ACCESS-NRI)",
122 | "blog": "https://www.access-nri.org.au"
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/CITATION.cff:
--------------------------------------------------------------------------------
1 | # This CITATION.cff file was generated with cffinit.
2 | # Visit https://bit.ly/cffinit to generate yours today!
3 |
4 | cff-version: 1.2.0
5 | title: cf_xarray
6 | message: >-
7 | If you use this software, please cite it using these
8 | metadata.
9 | type: software
10 | authors:
11 | - affiliation: 'National Center for Atmospheric Research, USA'
12 | family-names: Cherian
13 | given-names: Deepak
14 | orcid: 'https://orcid.org/0000-0002-6861-8734'
15 | - affiliation: 'National Oceanography Centre, Southampton, UK'
16 | family-names: Almansi
17 | given-names: Mattia
18 | orcid: 'https://orcid.org/0000-0001-6849-3647'
19 | - affiliation: 'Ouranos, Inc.'
20 | family-names: Bourgault
21 | given-names: Pascal
22 | orcid: 'https://orcid.org/0000-0003-1192-0403'
23 | - affiliation: Axiom Data Science
24 | family-names: Thyng
25 | given-names: Kristen
26 | orcid: 'https://orcid.org/0000-0002-8746-614X'
27 | - family-names: Thielen
28 | given-names: Jonathan
29 | orcid: 'https://orcid.org/0000-0002-5479-0189'
30 | affiliation: 'Iowa State University, Ames, IA, USA'
31 | - family-names: Magin
32 | given-names: Justus
33 | orcid: 'https://orcid.org/0000-0002-4254-8002'
34 | affiliation: IFREMER
35 | - family-names: Aoun
36 | given-names: Abel
37 | orcid: 'https://orcid.org/0000-0003-2289-2890'
38 | affiliation: CERFACS
39 | - family-names: Buntemeyer
40 | given-names: Lars
41 | orcid: 'https://orcid.org/0000-0002-0849-2404'
42 | affiliation: Helmholtz-Zentrum Hereon
43 | - family-names: Caneill
44 | given-names: Romain
45 | orcid: 'https://orcid.org/0000-0001-6649-4275'
46 | affiliation: Gothenburg University
47 | - family-names: Davis
48 | given-names: Luke
49 | orcid: 'https://orcid.org/0000-0003-1072-4638'
50 | affiliation: Colorado State University
51 | - family-names: Fernandes
52 | given-names: Filipe
53 | orcid: 'https://orcid.org/0000-0003-4165-2913'
54 | affiliation: IOOS
55 | - family-names: Hauser
56 | given-names: Matthias
57 | orcid: 'https://orcid.org/0000-0002-0057-4878'
58 | affiliation: ETH Zürich
59 | - family-names: Heerdegen
60 | given-names: Aidan
61 | orcid: 'https://orcid.org/0000-0002-4481-4896'
62 | affiliation: 'Australian National University'
63 | - affiliation: 'National Center for Atmospheric Research, USA'
64 | family-names: Kent
65 | given-names: Julia
66 | orcid: 'https://orcid.org/0000-0002-5611-8986'
67 | - family-names: Mankoff
68 | given-names: Ken
69 | orcid: 'https://orcid.org/0000-0001-5453-2019'
70 | affiliation: Goddard Institute for Space Studies
71 | - family-names: Müller
72 | given-names: Sebastian
73 | orcid: 'https://orcid.org/0000-0001-9060-4008'
74 | affiliation: Helmholtz Centre for Environmental Research - UFZ
75 | - family-names: Schupfner
76 | given-names: Martin
77 | orcid: 'https://orcid.org/0000-0001-8075-589X'
78 | affiliation: DKRZ (German Climate Computing Centre)
79 | - family-names: Vo
80 | given-names: Tom
81 | orcid: 'https://orcid.org/0000-0002-2461-0191'
82 | affiliation: Lawrence Livermore National Laboratory
83 | - family-names: Haëck
84 | given-names: Clément
85 | affiliation: Laboratoire d'Océanographie et du Climat (LOCEAN), Paris
86 | - family-names: Boutte
87 | given-names: Jason
88 | orcid: 'https://orcid.org/0009-0009-3996-3772'
89 | affiliation: Lawrence Livermore National Laboratory
90 | identifiers:
91 | - type: doi
92 | value: 10.5281/zenodo.4749735
93 | description: Zenodo DOI
94 | repository-code: 'https://github.com/xarray-contrib/cf-xarray'
95 | url: 'https://cf-xarray.readthedocs.io'
96 | abstract: >-
97 | cf_xarray provides an accessor (DataArray.cf or
98 | Dataset.cf) that allows you to interpret Climate and
99 | Forecast metadata convention attributes present on xarray
100 | objects.
101 | keywords:
102 | - cf-conventions
103 | - xarray
104 | - metadata
105 | license: Apache-2.0
106 | # commit: 1b373a21b558423da8f22c3ec79f58737871719b
107 | version: 0.8.2
108 | date-released: '2023-06-23'
109 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://img.shields.io/static/v1.svg?logo=Jupyter&label=Pangeo+Binder&message=GCE+us-central1&color=blue&style=for-the-badge
2 | :target: https://binder.pangeo.io/v2/gh/xarray-contrib/cf-xarray/main?urlpath=lab
3 | :alt: Binder
4 |
5 | .. image:: https://img.shields.io/readthedocs/cf-xarray/latest.svg?style=for-the-badge
6 | :target: https://cf-xarray.readthedocs.io/en/latest/?badge=latest
7 | :alt: Documentation Status
8 |
9 | .. image:: https://img.shields.io/github/actions/workflow/status/xarray-contrib/cf-xarray/ci.yaml?branch=main&logo=github&style=for-the-badge
10 | :target: https://github.com/xarray-contrib/cf-xarray/actions
11 | :alt: GitHub Workflow CI Status
12 |
13 | .. image:: https://results.pre-commit.ci/badge/github/xarray-contrib/cf-xarray/main.svg
14 | :target: https://results.pre-commit.ci/latest/github/xarray-contrib/cf-xarray/main
15 | :alt: pre-commit.ci status
16 |
17 | .. image:: https://codecov.io/gh/xarray-contrib/cf-xarray/branch/main/graph/badge.svg?token=hR3x9559bZ
18 | :target: https://codecov.io/gh/xarray-contrib/cf-xarray
19 | :alt: Code Coverage
20 |
21 | .. image:: https://img.shields.io/pypi/v/cf-xarray.svg?style=for-the-badge
22 | :target: https://pypi.org/project/cf-xarray
23 | :alt: Python Package Index
24 |
25 | .. image:: https://img.shields.io/conda/vn/conda-forge/cf_xarray.svg?style=for-the-badge
26 | :target: https://anaconda.org/conda-forge/cf_xarray
27 | :alt: Conda Version
28 |
29 | .. image:: https://zenodo.org/badge/267381269.svg
30 | :target: https://zenodo.org/badge/latestdoi/267381269
31 |
32 | .. image:: https://img.shields.io/badge/fair--software.eu-%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8B-yellow
33 | :target: https://fair-software.eu
34 |
35 | cf-xarray
36 | =========
37 |
38 | A lightweight convenience wrapper for using CF attributes on xarray objects.
39 |
40 | For example you can use ``.cf.mean("latitude")`` instead of ``.mean("lat")`` if appropriate attributes are set! This allows you to write code that does not require knowledge of specific dimension or coordinate names particular to a dataset.
41 |
42 | See more in the `introductory notebook `_.
43 |
44 | Try out our Earthcube 2021 Annual Meeting notebook `submission `_.
45 |
--------------------------------------------------------------------------------
/cf_xarray/__init__.py:
--------------------------------------------------------------------------------
1 | import xarray
2 | from packaging.version import Version
3 |
4 | from . import geometry as geometry
5 | from . import sgrid # noqa
6 | from .accessor import CFAccessor # noqa
7 | from .coding import ( # noqa
8 | decode_compress_to_multi_index,
9 | encode_multi_index_as_compress,
10 | )
11 | from .geometry import cf_to_shapely, shapely_to_cf # noqa
12 | from .helpers import bounds_to_vertices, vertices_to_bounds # noqa
13 | from .options import set_options # noqa
14 | from .utils import _get_version
15 |
16 | if Version(xarray.__version__) >= Version("2024.07.0"):
17 | from . import groupers as groupers
18 |
19 | __version__ = _get_version()
20 |
--------------------------------------------------------------------------------
/cf_xarray/coding.py:
--------------------------------------------------------------------------------
1 | """
2 | Encoders and decoders for CF conventions not implemented by Xarray.
3 | """
4 |
5 | import numpy as np
6 | import pandas as pd
7 | import xarray as xr
8 |
9 |
10 | def encode_multi_index_as_compress(ds, idxnames=None):
11 | """
12 | Encode a MultiIndexed dimension using the "compression by gathering" CF convention.
13 |
14 | Parameters
15 | ----------
16 | ds : xarray.Dataset
17 | Dataset with at least one MultiIndexed dimension.
18 | idxnames : hashable or iterable of hashable, optional
19 | Dimensions that are MultiIndex-ed. If None, will detect all MultiIndex-ed dimensions.
20 |
21 | Returns
22 | -------
23 | xarray.Dataset
24 | Encoded Dataset with ``name`` as a integer coordinate with a ``"compress"`` attribute.
25 |
26 | References
27 | ----------
28 | CF conventions on `compression by gathering `_
29 | """
30 | if idxnames is None:
31 | idxnames = tuple(
32 | name
33 | for name, idx in ds.indexes.items()
34 | if isinstance(idx, pd.MultiIndex)
35 | # After the flexible indexes refactor, all MultiIndex Levels
36 | # have a MultiIndex but the name won't match.
37 | # Prior to that refactor, there is only a single MultiIndex with name=None
38 | and (idx.name == name if idx.name is not None else True)
39 | )
40 | elif isinstance(idxnames, str):
41 | idxnames = (idxnames,)
42 |
43 | if not idxnames:
44 | raise ValueError("No MultiIndex-ed dimensions found in Dataset.")
45 |
46 | encoded = ds.reset_index(idxnames)
47 | for idxname in idxnames:
48 | mindex = ds.indexes[idxname]
49 | coords = dict(zip(mindex.names, mindex.levels, strict=False))
50 | encoded.update(coords)
51 | for c in coords:
52 | encoded[c].attrs = ds[c].attrs
53 | encoded[c].encoding = ds[c].encoding
54 | encoded[idxname] = np.ravel_multi_index(mindex.codes, mindex.levshape)
55 | encoded[idxname].attrs = ds[idxname].attrs.copy()
56 | if (
57 | "compress" in encoded[idxname].encoding
58 | or "compress" in encoded[idxname].attrs
59 | ):
60 | raise ValueError(
61 | f"Does not support the 'compress' attribute in {idxname}.encoding or {idxname}.attrs. "
62 | "This is generated automatically."
63 | )
64 | encoded[idxname].attrs["compress"] = " ".join(mindex.names)
65 | return encoded
66 |
67 |
68 | def decode_compress_to_multi_index(encoded, idxnames=None):
69 | """
70 | Decode a compressed variable to a pandas MultiIndex.
71 |
72 | Parameters
73 | ----------
74 | encoded : xarray.Dataset
75 | Encoded Dataset with variables that use "compression by gathering".capitalize.
76 | idxnames : hashable or iterable of hashable, optional
77 | Variable names that represents a compressed dimension. These variables must have
78 | the attribute ``"compress"``. If None, will detect all indexes with a ``"compress"``
79 | attribute and decode those.
80 |
81 | Returns
82 | -------
83 | xarray.Dataset
84 | Decoded Dataset with ``name`` as a MultiIndexed dimension.
85 |
86 | References
87 | ----------
88 | CF conventions on `compression by gathering `_
89 | """
90 | decoded = xr.Dataset(data_vars=encoded.data_vars, attrs=encoded.attrs.copy())
91 | if idxnames is None:
92 | idxnames = tuple(
93 | name for name in encoded.indexes if "compress" in encoded[name].attrs
94 | )
95 | elif isinstance(idxnames, str):
96 | idxnames = (idxnames,)
97 |
98 | for idxname in idxnames:
99 | if "compress" not in encoded[idxname].attrs:
100 | raise ValueError("Attribute 'compress' not found in provided Dataset.")
101 |
102 | if not isinstance(encoded, xr.Dataset):
103 | raise ValueError(
104 | f"Must provide a Dataset. Received {type(encoded)} instead."
105 | )
106 |
107 | names = encoded[idxname].attrs["compress"].split(" ")
108 | shape = [encoded.sizes[dim] for dim in names]
109 | indices = np.unravel_index(encoded[idxname].data, shape)
110 | try:
111 | from xarray.indexes import PandasMultiIndex
112 |
113 | variables = {
114 | dim: encoded[dim].isel({dim: xr.Variable(data=index, dims=idxname)})
115 | for dim, index in zip(names, indices, strict=False)
116 | }
117 | decoded = decoded.assign_coords(variables).set_xindex(
118 | names, PandasMultiIndex
119 | )
120 | except ImportError:
121 | arrays = [
122 | encoded[dim].data[index]
123 | for dim, index in zip(names, indices, strict=False)
124 | ]
125 | mindex = pd.MultiIndex.from_arrays(arrays, names=names)
126 | decoded.coords[idxname] = mindex
127 |
128 | decoded[idxname].attrs = encoded[idxname].attrs.copy()
129 | for coord in names:
130 | variable = encoded._variables[coord]
131 | decoded[coord].attrs = variable.attrs.copy()
132 | decoded[coord].encoding = variable.encoding.copy()
133 | del decoded[idxname].attrs["compress"]
134 |
135 | return decoded
136 |
--------------------------------------------------------------------------------
/cf_xarray/criteria.py:
--------------------------------------------------------------------------------
1 | """
2 | Criteria for identifying axes and coordinate variables.
3 | Reused with modification from MetPy under the terms of the BSD 3-Clause License.
4 | Copyright (c) 2017 MetPy Developers.
5 | """
6 |
7 | try:
8 | import regex as re
9 | except ImportError:
10 | import re # type: ignore[no-redef]
11 |
12 | from collections.abc import Mapping, MutableMapping
13 | from typing import Any
14 |
15 | #: CF Roles understood by cf-xarray
16 | _DSG_ROLES = ["timeseries_id", "profile_id", "trajectory_id"]
17 | #: Geometry types understood by cf-xarray
18 | _GEOMETRY_TYPES = ("line", "point", "polygon")
19 |
20 | cf_role_criteria: Mapping[str, Mapping[str, str]] = {
21 | k: {"cf_role": k}
22 | for k in (
23 | # CF Discrete sampling geometry
24 | *_DSG_ROLES,
25 | # SGRID
26 | "grid_topology",
27 | # UGRID
28 | "mesh_topology",
29 | )
30 | }
31 |
32 | # A grid mapping varibale is anything with a grid_mapping_name attribute
33 | grid_mapping_var_criteria: Mapping[str, Mapping[str, Any]] = {
34 | "grid_mapping": {"grid_mapping_name": re.compile(".")}
35 | }
36 |
37 | # A geometry container is anything with a geometry_type attribute
38 | geometry_var_criteria: Mapping[str, Mapping[str, Any]] = {
39 | "geometry": {"geometry_type": re.compile(".")},
40 | **{k: {"geometry_type": k} for k in _GEOMETRY_TYPES},
41 | }
42 |
43 | coordinate_criteria: MutableMapping[str, MutableMapping[str, tuple]] = {
44 | "latitude": {
45 | "standard_name": ("latitude",),
46 | "units": (
47 | "degree_north",
48 | "degree_N",
49 | "degreeN",
50 | "degrees_north",
51 | "degrees_N",
52 | "degreesN",
53 | ),
54 | "_CoordinateAxisType": ("Lat",),
55 | },
56 | "longitude": {
57 | "standard_name": ("longitude",),
58 | "units": (
59 | "degree_east",
60 | "degree_E",
61 | "degreeE",
62 | "degrees_east",
63 | "degrees_E",
64 | "degreesE",
65 | ),
66 | "_CoordinateAxisType": ("Lon",),
67 | },
68 | "Z": {
69 | "standard_name": (
70 | "model_level_number",
71 | "atmosphere_ln_pressure_coordinate",
72 | "atmosphere_sigma_coordinate",
73 | "atmosphere_hybrid_sigma_pressure_coordinate",
74 | "atmosphere_hybrid_height_coordinate",
75 | "atmosphere_sleve_coordinate",
76 | "ocean_sigma_coordinate",
77 | "ocean_s_coordinate",
78 | "ocean_s_coordinate_g1",
79 | "ocean_s_coordinate_g2",
80 | "ocean_sigma_z_coordinate",
81 | "ocean_double_sigma_coordinate",
82 | ),
83 | "_CoordinateAxisType": (
84 | "GeoZ",
85 | "Height",
86 | "Pressure",
87 | ),
88 | "axis": ("Z",),
89 | "cartesian_axis": ("Z",),
90 | "grads_dim": ("z",),
91 | },
92 | "vertical": {
93 | "standard_name": (
94 | "air_pressure",
95 | "height",
96 | "depth",
97 | "geopotential_height",
98 | # computed dimensional coordinate name
99 | "altitude",
100 | "height_above_geopotential_datum",
101 | "height_above_reference_ellipsoid",
102 | "height_above_mean_sea_level",
103 | ),
104 | "positive": ("up", "down"),
105 | },
106 | "X": {
107 | "standard_name": (
108 | "projection_x_coordinate",
109 | "grid_longitude",
110 | "projection_x_angular_coordinate",
111 | ),
112 | "_CoordinateAxisType": ("GeoX",),
113 | "axis": ("X",),
114 | "cartesian_axis": ("X",),
115 | "grads_dim": ("x",),
116 | },
117 | "Y": {
118 | "standard_name": (
119 | "projection_y_coordinate",
120 | "grid_latitude",
121 | "projection_y_angular_coordinate",
122 | ),
123 | "_CoordinateAxisType": ("GeoY",),
124 | "axis": ("Y",),
125 | "cartesian_axis": ("Y",),
126 | "grads_dim": ("y",),
127 | },
128 | "T": {
129 | "standard_name": ("time",),
130 | "_CoordinateAxisType": ("Time",),
131 | "axis": ("T",),
132 | "cartesian_axis": ("T",),
133 | "grads_dim": ("t",),
134 | },
135 | }
136 |
137 | coordinate_criteria["time"] = coordinate_criteria["T"]
138 |
139 | # "long_name" and "standard_name" criteria are the same. For convenience.
140 | for coord in coordinate_criteria:
141 | coordinate_criteria[coord]["long_name"] = coordinate_criteria[coord][
142 | "standard_name"
143 | ]
144 | coordinate_criteria["X"]["long_name"] += ("cell index along first dimension",)
145 | coordinate_criteria["Y"]["long_name"] += ("cell index along second dimension",)
146 |
147 |
148 | #: regular expressions for guess_coord_axis
149 | regex = {
150 | "time": re.compile("\\bt\\b|(time|min|hour|day|week|month|year)[0-9]*"),
151 | "Z": re.compile(
152 | "(z|nav_lev|gdep|lv_|[o]*lev|bottom_top|sigma|h(ei)?ght|altitude|depth|"
153 | "isobaric|pres|isotherm)[a-z_]*[0-9]*"
154 | ),
155 | "Y": re.compile("y|j|nlat|rlat|nj"),
156 | "latitude": re.compile("y?(nav_lat|lat|gphi)[a-z0-9]*"),
157 | "X": re.compile("x|i|nlon|rlon|ni"),
158 | "longitude": re.compile("x?(nav_lon|lon|glam)[a-z0-9]*"),
159 | }
160 | regex["T"] = regex["time"]
161 |
--------------------------------------------------------------------------------
/cf_xarray/formatting.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import warnings
4 | from collections.abc import Hashable, Iterable
5 | from functools import partial
6 |
7 | import numpy as np
8 |
9 | STAR = " * "
10 | TAB = len(STAR) * " "
11 |
12 | try:
13 | from rich.table import Table
14 | except ImportError:
15 | Table = None # type: ignore[assignment, misc]
16 |
17 |
18 | def _format_missing_row(row: str, rich: bool) -> str:
19 | if rich:
20 | return f"[grey62]{row}[/grey62]"
21 | else:
22 | return row
23 |
24 |
25 | def _format_varname(name, rich: bool):
26 | return name
27 |
28 |
29 | def _format_subtitle(name: str, rich: bool) -> str:
30 | if rich:
31 | return f"[bold]{name}[/bold]"
32 | else:
33 | return name
34 |
35 |
36 | def _format_cf_name(name: str, rich: bool) -> str:
37 | if rich:
38 | return f"[color(33)]{name}[/color(33)]"
39 | else:
40 | return name
41 |
42 |
43 | def make_text_section(
44 | accessor,
45 | subtitle: str,
46 | attr: str | dict,
47 | dims=None,
48 | valid_keys=None,
49 | valid_values=None,
50 | default_keys=None,
51 | rich: bool = False,
52 | ):
53 | from .accessor import sort_maybe_hashable
54 |
55 | if dims is None:
56 | dims = []
57 | with warnings.catch_warnings():
58 | warnings.simplefilter("ignore")
59 | if isinstance(attr, str):
60 | try:
61 | vardict: dict[str, Iterable[Hashable]] = getattr(accessor, attr, {})
62 | except ValueError:
63 | vardict = {}
64 | else:
65 | assert isinstance(attr, dict)
66 | vardict = attr
67 | if valid_keys:
68 | vardict = {k: v for k, v in vardict.items() if k in valid_keys}
69 |
70 | # Sort keys if there aren't extra keys,
71 | # preserve default keys order otherwise.
72 | default_keys = [] if not default_keys else list(default_keys)
73 | extra_keys = list(set(vardict) - set(default_keys))
74 | ordered_keys = sorted(vardict) if extra_keys else default_keys
75 | vardict = {key: vardict[key] for key in ordered_keys if key in vardict}
76 |
77 | # Keep only valid values (e.g., coords or data_vars)
78 | if valid_values is not None:
79 | vardict = {
80 | key: set(value).intersection(valid_values)
81 | for key, value in vardict.items()
82 | if set(value).intersection(valid_values)
83 | }
84 |
85 | # Star for keys with dims only, tab otherwise
86 | rows = [
87 | (
88 | f"{STAR if dims and set(value) <= set(dims) else TAB}"
89 | f"{_format_cf_name(key, rich)}: "
90 | f"{_format_varname(sort_maybe_hashable(value), rich)}"
91 | )
92 | for key, value in vardict.items()
93 | ]
94 |
95 | # Append missing default keys followed by n/a
96 | if default_keys:
97 | missing_keys = [key for key in default_keys if key not in vardict]
98 | if missing_keys:
99 | rows.append(
100 | _format_missing_row(TAB + ", ".join(missing_keys) + ": n/a", rich)
101 | )
102 | elif not rows:
103 | rows.append(_format_missing_row(TAB + "n/a", rich))
104 |
105 | return _print_rows(subtitle, rows, rich)
106 |
107 |
108 | def _print_rows(subtitle: str, rows: list[str], rich: bool):
109 | subtitle = f"{subtitle.rjust(20)}:"
110 |
111 | # Add subtitle to the first row, align other rows
112 | rows = [
113 | (
114 | _format_subtitle(subtitle, rich=rich) + row
115 | if i == 0
116 | else len(subtitle) * " " + row
117 | )
118 | for i, row in enumerate(rows)
119 | ]
120 |
121 | return "\n".join(rows) + "\n\n"
122 |
123 |
124 | def _format_conventions(string: str, rich: bool):
125 | row = _print_rows(
126 | subtitle="Conventions",
127 | rows=[_format_cf_name(TAB + string, rich=rich)],
128 | rich=rich,
129 | )
130 | if rich:
131 | row = row.rstrip()
132 | return row
133 |
134 |
135 | def _maybe_panel(textgen, title: str, rich: bool):
136 | if rich:
137 | from rich.panel import Panel
138 |
139 | kwargs = dict(
140 | expand=True,
141 | title_align="left",
142 | title=f"[bold][color(244)]{title}[/bold][/color(244)]",
143 | highlight=True,
144 | width=100,
145 | )
146 | if isinstance(textgen, Table):
147 | return Panel(textgen, padding=(0, 20), **kwargs) # type: ignore[arg-type]
148 | else:
149 | text = "".join(textgen)
150 | return Panel(f"[color(241)]{text.rstrip()}[/color(241)]", **kwargs) # type: ignore[arg-type]
151 | else:
152 | text = "".join(textgen)
153 | return title + ":\n" + text
154 |
155 |
156 | def _get_bit_length(dtype):
157 | # Check if dtype is a numpy dtype, if not, convert it
158 | if not isinstance(dtype, np.dtype):
159 | dtype = np.dtype(dtype)
160 |
161 | # Calculate the bit length
162 | bit_length = 8 * dtype.itemsize
163 |
164 | return bit_length
165 |
166 |
167 | def _unpackbits(mask, bit_length):
168 | # Ensure the array is a numpy array
169 | arr = np.asarray(mask)
170 |
171 | # Create an output array of the appropriate shape
172 | output_shape = arr.shape + (bit_length,)
173 | output = np.zeros(output_shape, dtype=np.uint8)
174 |
175 | # Unpack bits
176 | for i in range(bit_length):
177 | output[..., i] = (arr >> i) & 1
178 |
179 | return output[..., ::-1]
180 |
181 |
182 | def _max_chars_for_bit_length(bit_length):
183 | """
184 | Find the maximum characters needed for a fixed-width display
185 | for integer values of a certain bit_length. Use calculation
186 | for signed integers, since it conservatively will always have
187 | enough characters for signed or unsigned.
188 | """
189 | # Maximum value for signed integers of this bit length
190 | max_val = 2 ** (bit_length - 1) - 1
191 | # Add 1 for the negative sign
192 | return len(str(max_val)) + 1
193 |
194 |
195 | def find_set_bits(mask, value, repeated_masks, bit_length):
196 | bitpos = np.arange(bit_length)[::-1]
197 | if mask not in repeated_masks:
198 | if value == 0:
199 | return [-1]
200 | elif value is not None:
201 | return [int(np.log2(value))]
202 | else:
203 | return [int(np.log2(mask))]
204 | else:
205 | allset = bitpos[_unpackbits(mask, bit_length) == 1]
206 | setbits = bitpos[_unpackbits(mask & value, bit_length) == 1]
207 | return [b if abs(b) in setbits else -b for b in allset]
208 |
209 |
210 | def _format_flags(accessor, rich):
211 | from .accessor import create_flag_dict
212 |
213 | try:
214 | flag_dict = create_flag_dict(accessor._obj)
215 | except ValueError:
216 | return _print_rows(
217 | "Flag Meanings", ["Invalid Mapping. Check attributes."], rich
218 | )
219 |
220 | masks = [m for m, _ in flag_dict.values()]
221 | repeated_masks = {m for m in masks if masks.count(m) > 1}
222 | excl_flags = [f for f, (m, v) in flag_dict.items() if m in repeated_masks]
223 | # indep_flags = [
224 | # f
225 | # for f, (m, _) in flag_dict.items()
226 | # if m is not None and m not in repeated_masks
227 | # ]
228 |
229 | bit_length = _get_bit_length(accessor._obj.dtype)
230 | mask_width = _max_chars_for_bit_length(bit_length)
231 | key_width = max(len(key) for key in flag_dict)
232 |
233 | bit_text = []
234 | value_text = []
235 | for key, (mask, value) in flag_dict.items():
236 | if mask is None:
237 | bit_text.append("✗" if rich else "")
238 | value_text.append(str(value))
239 | continue
240 | bits = find_set_bits(mask, value, repeated_masks, bit_length)
241 | bitstring = ["."] * bit_length
242 | if bits == [-1]:
243 | continue
244 | else:
245 | for b in bits:
246 | bitstring[abs(b)] = _format_cf_name("1" if b >= 0 else "0", rich)
247 | text = "".join(bitstring[::-1])
248 | value_text.append(
249 | f"{mask:{mask_width}} & {value}"
250 | if key in excl_flags and value is not None
251 | else f"{mask:{mask_width}}"
252 | )
253 | bit_text.append(text if rich else f" / Bit: {text}")
254 |
255 | if rich:
256 | from rich import box
257 | from rich.table import Table
258 |
259 | table = Table(
260 | box=box.SIMPLE,
261 | width=None,
262 | title_justify="left",
263 | padding=(0, 2),
264 | header_style="bold color(244)",
265 | )
266 |
267 | table.add_column("Meaning", justify="left")
268 | table.add_column("Value", justify="right")
269 | table.add_column("Bits", justify="center")
270 |
271 | for val, bit, key in zip(value_text, bit_text, flag_dict, strict=False):
272 | table.add_row(_format_cf_name(key, rich), val, bit)
273 |
274 | return table
275 |
276 | else:
277 | rows = []
278 | for val, bit, key in zip(value_text, bit_text, flag_dict, strict=False):
279 | rows.append(
280 | f"{TAB}{_format_cf_name(key, rich):>{key_width}}: {TAB} {val} {bit}"
281 | )
282 | return _print_rows("Flag Meanings", rows, rich)
283 |
284 |
285 | def _format_dsg_roles(accessor, dims, rich):
286 | from .criteria import _DSG_ROLES
287 |
288 | yield make_text_section(
289 | accessor,
290 | "CF Roles",
291 | "cf_roles",
292 | dims=dims,
293 | valid_keys=_DSG_ROLES,
294 | rich=rich,
295 | )
296 |
297 |
298 | def _format_geometries(accessor, dims, rich):
299 | yield make_text_section(
300 | accessor,
301 | "CF Geometries",
302 | "geometries",
303 | dims=dims,
304 | # valid_keys=_DSG_ROLES,
305 | rich=rich,
306 | )
307 |
308 |
309 | def _format_coordinates(accessor, dims, coords, rich):
310 | from .accessor import _AXIS_NAMES, _CELL_MEASURES, _COORD_NAMES
311 |
312 | section = partial(
313 | make_text_section, accessor=accessor, dims=dims, valid_values=coords, rich=rich
314 | )
315 |
316 | yield section(subtitle="CF Axes", attr="axes", default_keys=_AXIS_NAMES)
317 | yield section(
318 | subtitle="CF Coordinates", attr="coordinates", default_keys=_COORD_NAMES
319 | )
320 | yield section(
321 | subtitle="Cell Measures", attr="cell_measures", default_keys=_CELL_MEASURES
322 | )
323 | yield section(subtitle="Standard Names", attr="standard_names")
324 | yield section(subtitle="Bounds", attr="bounds")
325 | yield section(subtitle="Grid Mappings", attr="grid_mapping_names")
326 |
327 |
328 | def _format_data_vars(accessor, data_vars, rich):
329 | from .accessor import _CELL_MEASURES
330 |
331 | section = partial(
332 | make_text_section,
333 | accessor=accessor,
334 | dims=None,
335 | valid_values=data_vars,
336 | rich=rich,
337 | )
338 |
339 | yield section(
340 | subtitle="Cell Measures", attr="cell_measures", default_keys=_CELL_MEASURES
341 | )
342 | yield section(subtitle="Standard Names", attr="standard_names")
343 | yield section(subtitle="Bounds", attr="bounds")
344 | yield section(subtitle="Grid Mappings", attr="grid_mapping_names")
345 |
346 |
347 | def _format_sgrid(accessor, axes, rich):
348 | yield make_text_section(
349 | accessor,
350 | "CF role",
351 | "cf_roles",
352 | valid_keys=["grid_topology"],
353 | rich=rich,
354 | )
355 |
356 | yield make_text_section(
357 | accessor,
358 | "Axes",
359 | axes,
360 | accessor._obj.dims,
361 | valid_values=accessor._obj.dims,
362 | default_keys=axes.keys(),
363 | rich=rich,
364 | )
365 |
--------------------------------------------------------------------------------
/cf_xarray/groupers.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 | import numpy as np
4 | import pandas as pd
5 | from xarray import Variable
6 | from xarray.groupers import EncodedGroups, Grouper, UniqueGrouper
7 |
8 |
9 | @dataclass
10 | class FlagGrouper(Grouper):
11 | """
12 | Grouper object that allows convenient categorical grouping by a CF flag variable.
13 |
14 | Labels in the grouped output will be restricted to those listed in ``flag_meanings``.
15 | """
16 |
17 | def factorize(self, group) -> EncodedGroups:
18 | if "flag_values" not in group.attrs or "flag_meanings" not in group.attrs:
19 | raise ValueError(
20 | "FlagGrouper can only be used with flag variables that have"
21 | "`flag_values` and `flag_meanings` specified in attrs."
22 | )
23 |
24 | values = np.array(group.attrs["flag_values"])
25 | full_index = pd.Index(group.attrs["flag_meanings"].split(" "))
26 |
27 | grouper = UniqueGrouper(labels=values)
28 |
29 | # TODO: we could optimize here, since `group` is already factorized,
30 | # but there are subtleties. For example, the attrs must be up to date,
31 | # any value that is not in flag_values will cause an error, etc.
32 | ret = grouper.factorize(group)
33 |
34 | ret.codes.attrs.pop("flag_values")
35 | ret.codes.attrs.pop("flag_meanings")
36 |
37 | return EncodedGroups(
38 | codes=ret.codes,
39 | full_index=full_index,
40 | unique_coord=Variable(
41 | dims=ret.codes.name, data=np.array(full_index), attrs=ret.codes.attrs
42 | ),
43 | group_indices=ret.group_indices,
44 | )
45 |
46 | def reset(self):
47 | raise NotImplementedError()
48 |
--------------------------------------------------------------------------------
/cf_xarray/helpers.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Hashable, Sequence
4 |
5 | import numpy as np
6 | import xarray as xr
7 | from xarray import DataArray
8 |
9 |
10 | def _guess_bounds_1d(da, dim):
11 | """
12 | Guess bounds values given a 1D coordinate variable.
13 | Assumes equal spacing on either side of the coordinate label.
14 | This is an approximation only.
15 | Output has an added "bounds" dimension at the end.
16 | """
17 | if dim not in da.dims:
18 | (dim,) = da.cf.axes[dim]
19 | ADDED_INDEX = False
20 | if dim not in da.coords:
21 | # For proper alignment in the lines below, we need an index on dim.
22 | da = da.assign_coords({dim: da[dim]})
23 | ADDED_INDEX = True
24 |
25 | diff = da.diff(dim)
26 | lower = da - diff / 2
27 | upper = da + diff / 2
28 | bounds = xr.concat([lower, upper], dim="bounds")
29 |
30 | first = (bounds.isel({dim: 0}) - diff.isel({dim: 0})).assign_coords(
31 | {dim: da[dim][0]}
32 | )
33 | result = xr.concat([first, bounds], dim=dim).transpose(..., "bounds")
34 | if ADDED_INDEX:
35 | result = result.drop_vars(dim)
36 | return result
37 |
38 |
39 | def _guess_bounds_2d(da, dims):
40 | """
41 | Guess bounds values given a 2D coordinate variable.
42 | Assumes equal spacing on either side of the coordinate label.
43 | This is a coarse approximation, especially for curvilinear grids.
44 | Output has an added "bounds" dimension at the end.
45 | """
46 | daX = _guess_bounds_1d(da, dims[0]).rename(bounds="Xbnds")
47 | daXY = _guess_bounds_1d(daX, dims[1]).rename(bounds="Ybnds")
48 | # At this point, we might have different corners for adjacent cells, we average them together to have a nice grid
49 | # To make this vectorized and keep the edges, we'll pad with NaNs and ignore them in the averages
50 | daXYp = (
51 | daXY.pad({d: (1, 1) for d in dims}, mode="constant", constant_values=np.nan)
52 | .transpose(*dims, "Xbnds", "Ybnds")
53 | .values
54 | ) # Tranpose for an easier notation
55 | # Mean of the corners that should be the same point.
56 | daXYm = np.stack(
57 | (
58 | # Lower left corner (mean of : upper right of the lower left cell, lower right of the upper left cell, and so on, ccw)
59 | np.nanmean(
60 | np.stack(
61 | (
62 | daXYp[:-2, :-2, 1, 1],
63 | daXYp[:-2, 1:-1, 1, 0],
64 | daXYp[1:-1, 1:-1, 0, 0],
65 | daXYp[1:-1, :-2, 0, 1],
66 | )
67 | ),
68 | axis=0,
69 | ),
70 | # Upper left corner
71 | np.nanmean(
72 | np.stack(
73 | (
74 | daXYp[:-2, 1:-1, 1, 1],
75 | daXYp[:-2, 2:, 1, 0],
76 | daXYp[1:-1, 2:, 0, 0],
77 | daXYp[1:-1, 1:-1, 0, 1],
78 | )
79 | ),
80 | axis=0,
81 | ),
82 | # Upper right
83 | np.nanmean(
84 | np.stack(
85 | (
86 | daXYp[1:-1, 1:-1, 1, 1],
87 | daXYp[1:-1, 2:, 1, 0],
88 | daXYp[2:, 2:, 0, 0],
89 | daXYp[2:, 1:-1, 0, 1],
90 | )
91 | ),
92 | axis=0,
93 | ),
94 | # Lower right
95 | np.nanmean(
96 | np.stack(
97 | (
98 | daXYp[1:-1, :-2, 1, 1],
99 | daXYp[1:-1, 1:-1, 1, 0],
100 | daXYp[2:, 1:-1, 0, 0],
101 | daXYp[2:, :-2, 0, 1],
102 | )
103 | ),
104 | axis=0,
105 | ),
106 | ),
107 | axis=-1,
108 | )
109 | return xr.DataArray(daXYm, dims=(*dims, "bounds"), coords=da.coords)
110 |
111 |
112 | def bounds_to_vertices(
113 | bounds: DataArray,
114 | bounds_dim: Hashable,
115 | core_dims=None,
116 | order: str | None = "counterclockwise",
117 | ) -> DataArray:
118 | """
119 | Convert bounds variable to vertices.
120 |
121 | There are 2 covered cases:
122 | - 1D coordinates, with bounds of shape (N, 2),
123 | converted to vertices of shape (N+1,)
124 | - 2D coordinates, with bounds of shape (N, M, 4).
125 | converted to vertices of shape (N+1, M+1).
126 |
127 | Parameters
128 | ----------
129 | bounds : DataArray
130 | The bounds to convert.
131 | bounds_dim : str
132 | The name of the bounds dimension of `bounds` (the one of length 2 or 4).
133 | core_dims : list, optional
134 | List of core dimensions for apply_ufunc. This must not include bounds_dims.
135 | The shape of ``(*core_dims, bounds_dim)`` must be (N, 2) or (N, M, 4).
136 | order : {'counterclockwise', 'clockwise', None}
137 | Valid for 2D coordinates only (i.e. bounds of shape (..., N, M, 4), ignored otherwise.
138 | Order the bounds are given in, assuming that ax0-ax1-upward is a right handed
139 | coordinate system, where ax0 and ax1 are the two first dimensions of `bounds`.
140 | If None, the counterclockwise version is computed and then verified. If the
141 | check fails the clockwise version is returned. See Notes for more details.
142 |
143 | Returns
144 | -------
145 | DataArray
146 | Either of shape (N+1,) or (N+1, M+1). New vertex dimensions are named
147 | from the initial dimension and suffix "_vertices".
148 |
149 | Notes
150 | -----
151 | Getting the correct axes "order" is tricky. There are no real standards for
152 | dimension names or even axes order, even though the CF conventions mentions the
153 | ax0-ax1-upward (counterclockwise bounds) as being the default. Moreover, xarray can
154 | tranpose data without raising any warning or error, which make attributes
155 | unreliable.
156 |
157 | References
158 | ----------
159 | Please refer to the CF conventions document : http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#cell-boundaries.
160 | """
161 |
162 | if core_dims is None:
163 | core_dims = [dim for dim in bounds.dims if dim != bounds_dim]
164 |
165 | output_sizes = {f"{dim}_vertices": bounds.sizes[dim] + 1 for dim in core_dims}
166 | output_core_dims = list(output_sizes.keys())
167 |
168 | n_core_dims = len(core_dims)
169 | nbounds = bounds[bounds_dim].size
170 |
171 | if not (n_core_dims == 2 and nbounds == 4) and not (
172 | n_core_dims == 1 and nbounds == 2
173 | ):
174 | raise ValueError(
175 | f"Bounds format not understood. Got {bounds.dims} with shape {bounds.shape}."
176 | )
177 |
178 | return xr.apply_ufunc(
179 | _bounds_helper,
180 | bounds,
181 | input_core_dims=[core_dims + [bounds_dim]],
182 | dask="parallelized",
183 | kwargs={"n_core_dims": n_core_dims, "nbounds": nbounds, "order": order},
184 | output_core_dims=[output_core_dims],
185 | dask_gufunc_kwargs=dict(output_sizes=output_sizes),
186 | output_dtypes=[bounds.dtype],
187 | )
188 |
189 |
190 | def _bounds_helper(values, n_core_dims, nbounds, order):
191 | if n_core_dims == 2 and nbounds == 4:
192 | # Vertices case (2D lat/lon)
193 | if order in ["counterclockwise", None]:
194 | # Names assume we are drawing axis 1 upward et axis 2 rightward.
195 | bot_left = values[..., :, :, 0]
196 | bot_right = values[..., :, -1:, 1]
197 | top_right = values[..., -1:, -1:, 2]
198 | top_left = values[..., -1:, :, 3]
199 | vertex_vals = np.block([[bot_left, bot_right], [top_left, top_right]])
200 | if order is None: # We verify if the ccw version works.
201 | calc_bnds = vertices_to_bounds(vertex_vals).values
202 | order = (
203 | "counterclockwise" if np.allclose(calc_bnds, values) else "clockwise"
204 | )
205 | if order == "clockwise":
206 | bot_left = values[..., :, :, 0]
207 | top_left = values[..., -1:, :, 1]
208 | top_right = values[..., -1:, -1:, 2]
209 | bot_right = values[..., :, -1:, 3]
210 | # Our assumption was wrong, axis 1 is rightward and axis 2 is upward
211 | vertex_vals = np.block([[bot_left, bot_right], [top_left, top_right]])
212 | elif n_core_dims == 1 and nbounds == 2:
213 | # Middle points case (1D lat/lon)
214 | vertex_vals = np.concatenate((values[..., :, 0], values[..., -1:, 1]), axis=-1)
215 |
216 | return vertex_vals
217 |
218 |
219 | def vertices_to_bounds(
220 | vertices: DataArray, out_dims: Sequence[str] = ("bounds", "x", "y")
221 | ) -> DataArray:
222 | """
223 | Convert vertices to CF-compliant bounds.
224 |
225 | There are 2 covered cases:
226 | - 1D coordinates, with vertices of shape (N+1,),
227 | converted to bounds of shape (N, 2)
228 | - 2D coordinates, with vertices of shape (N+1, M+1).
229 | converted to bounds of shape (N, M, 4).
230 |
231 | Parameters
232 | ----------
233 | vertices : DataArray
234 | The vertices to convert. Must be of shape (N + 1) or (N + 1, M + 1).
235 | out_dims : Sequence[str],
236 | The name of the dimension in the output. The first is the 'bounds'
237 | dimension and the following are the coordinate dimensions.
238 |
239 | Returns
240 | -------
241 | DataArray
242 | Either of shape (2, N) or (4, N, M).
243 |
244 | References
245 | ----------
246 | Please refer to the CF conventions document : http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#cell-boundaries.
247 | """
248 | if vertices.ndim == 1:
249 | bnd_vals = np.stack((vertices[:-1], vertices[1:]), axis=0)
250 | elif vertices.ndim == 2:
251 | bnd_vals = np.stack(
252 | (
253 | vertices[:-1, :-1],
254 | vertices[:-1, 1:],
255 | vertices[1:, 1:],
256 | vertices[1:, :-1],
257 | ),
258 | axis=0,
259 | )
260 | else:
261 | raise ValueError(
262 | f"vertices format not understood. Got {vertices.dims} with shape {vertices.shape}."
263 | )
264 | return xr.DataArray(bnd_vals, dims=out_dims[: vertices.ndim + 1]).transpose(
265 | ..., out_dims[0]
266 | )
267 |
--------------------------------------------------------------------------------
/cf_xarray/options.py:
--------------------------------------------------------------------------------
1 | """
2 | Started from xarray options.py
3 | """
4 |
5 | import copy
6 | from collections.abc import MutableMapping
7 | from typing import Any
8 |
9 | from .utils import always_iterable
10 |
11 | OPTIONS: MutableMapping[str, Any] = {
12 | "custom_criteria": [],
13 | "warn_on_missing_variables": True,
14 | }
15 |
16 |
17 | class set_options: # numpydoc ignore=PR01,PR02
18 | """
19 | Set options for cf-xarray in a controlled context.
20 |
21 | Parameters
22 | ----------
23 | custom_criteria : dict
24 | Translate from axis, coord, or custom name to
25 | variable name optionally using ``custom_criteria``. Default: [].
26 | warn_on_missing_variables : bool
27 | Whether to raise a warning when variables referred to in attributes
28 | are not present in the object.
29 |
30 | Examples
31 | --------
32 |
33 | You can use ``set_options`` either as a context manager:
34 |
35 | >>> import numpy as np
36 | >>> import xarray as xr
37 | >>> my_custom_criteria = {"ssh": {"name": "elev$"}}
38 | >>> ds = xr.Dataset({"elev": np.arange(1000)})
39 | >>> with cf_xarray.set_options(custom_criteria=my_custom_criteria):
40 | ... xr.testing.assert_identical(ds["elev"], ds.cf["ssh"])
41 |
42 | Or to set global options:
43 |
44 | >>> cf_xarray.set_options(custom_criteria=my_custom_criteria)
45 | >>> xr.testing.assert_identical(ds["elev"], ds.cf["ssh"])
46 | """
47 |
48 | def __init__(self, **kwargs):
49 | self.old = {}
50 | for k in kwargs:
51 | if k not in OPTIONS:
52 | raise ValueError(
53 | f"argument name {k!r} is not in the set of valid options {set(OPTIONS)!r}"
54 | )
55 | self.old[k] = OPTIONS[k]
56 | self._apply_update(kwargs)
57 |
58 | def _apply_update(self, options_dict):
59 | options_dict = copy.deepcopy(options_dict)
60 | for k in options_dict:
61 | if k == "custom_criteria":
62 | options_dict["custom_criteria"] = always_iterable(
63 | options_dict["custom_criteria"], allowed=(tuple, list)
64 | )
65 | OPTIONS.update(options_dict)
66 |
67 | def __enter__(self):
68 | return
69 |
70 | def __exit__(self, type, value, traceback):
71 | self._apply_update(self.old)
72 |
--------------------------------------------------------------------------------
/cf_xarray/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xarray-contrib/cf-xarray/4be96ff2e40e7a7fc7ff6d57744c4a42b6cbaa94/cf_xarray/py.typed
--------------------------------------------------------------------------------
/cf_xarray/scripts/make_doc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 |
5 | from pandas import DataFrame
6 |
7 | from cf_xarray.accessor import _AXIS_NAMES, _COORD_NAMES
8 | from cf_xarray.criteria import coordinate_criteria, regex
9 |
10 |
11 | def main():
12 | """
13 | Make all additional files needed to build the documentations.
14 | """
15 |
16 | make_criteria_csv()
17 | make_regex_csv()
18 |
19 |
20 | def make_criteria_csv():
21 | """
22 | Make criteria tables:
23 | _build/csv/{all,axes,coords}_criteria.csv
24 | """
25 |
26 | csv_dir = "_build/csv"
27 | os.makedirs(csv_dir, exist_ok=True)
28 |
29 | # Criteria tables
30 | df = DataFrame.from_dict(coordinate_criteria)
31 | df = df.dropna(axis=1, how="all")
32 | df = df.map(lambda x: ", ".join(sorted(x)) if isinstance(x, tuple) else x)
33 | df = df.sort_index(axis=0).sort_index(axis=1)
34 |
35 | # All criteria
36 | df.transpose().to_csv(os.path.join(csv_dir, "all_criteria.csv"))
37 |
38 | # Axes and coordinates
39 | for keys, name in zip(
40 | [_AXIS_NAMES, _COORD_NAMES], ["axes", "coords"], strict=False
41 | ):
42 | subdf = df[sorted(keys)].dropna(axis=1, how="all")
43 | subdf = subdf.dropna(axis=1, how="all").transpose()
44 | subdf.transpose().to_csv(os.path.join(csv_dir, f"{name}_criteria.csv"))
45 |
46 |
47 | def make_regex_csv():
48 | """
49 | Make regex tables:
50 | _build/csv/all_regex.csv
51 | """
52 |
53 | csv_dir = "_build/csv"
54 | os.makedirs(csv_dir, exist_ok=True)
55 | df = DataFrame(regex, index=[0])
56 | df = df.map(lambda x: f"``{str(x)[11:-1]}``")
57 | df = df.sort_index(axis=1).transpose()
58 | df.to_csv(os.path.join(csv_dir, "all_regex.csv"), header=False)
59 |
60 |
61 | if __name__ == "__main__":
62 | main()
63 |
--------------------------------------------------------------------------------
/cf_xarray/scripts/print_versions.py:
--------------------------------------------------------------------------------
1 | if __name__ == "__main__":
2 | import cf_xarray
3 |
4 | print(cf_xarray.__version__)
5 |
--------------------------------------------------------------------------------
/cf_xarray/sgrid.py:
--------------------------------------------------------------------------------
1 | SGRID_DIM_ATTRS = [
2 | "face_dimensions",
3 | "volume_dimensions",
4 | # the following are optional and should be redundant with the above
5 | # at least for dimension names
6 | # "face1_dimensions",
7 | # "face2_dimensions",
8 | # "face3_dimensions",
9 | "edge1_dimensions",
10 | "edge2_dimensions",
11 | # "edge3_dimensions",
12 | ]
13 |
14 |
15 | def parse_axes(ds):
16 | import re
17 |
18 | (gridvar,) = ds.cf.cf_roles["grid_topology"]
19 | grid = ds[gridvar]
20 | pattern = re.compile("\\s?(.*?):\\s*(.*?)\\s+(?:\\(padding:(.+?)\\))?")
21 | ndim = grid.attrs["topology_dimension"]
22 | axes_names = ["X", "Y", "Z"][:ndim]
23 | axes = dict(
24 | zip(
25 | axes_names,
26 | ({k} for k in grid.attrs["node_dimensions"].split(" ")),
27 | strict=False,
28 | )
29 | )
30 | for attr in SGRID_DIM_ATTRS:
31 | if attr in grid.attrs:
32 | matches = re.findall(pattern, grid.attrs[attr] + "\n")
33 | assert len(matches) == ndim, matches
34 | for ax, match in zip(axes_names, matches, strict=False):
35 | axes[ax].update(set(match[:2]))
36 |
37 | if ndim == 2 and "vertical_dimensions" in grid.attrs:
38 | matches = re.findall(pattern, grid.attrs["vertical_dimensions"] + "\n")
39 | assert len(matches) == 1
40 | axes["Z"] = set(matches[0][:2])
41 |
42 | return axes
43 |
--------------------------------------------------------------------------------
/cf_xarray/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | import re
3 | from contextlib import contextmanager
4 |
5 | import dask
6 | import pytest
7 | from packaging import version
8 |
9 |
10 | @contextmanager
11 | def raises_regex(error, pattern):
12 | __tracebackhide__ = True
13 | with pytest.raises(error) as excinfo:
14 | yield
15 | message = str(excinfo.value)
16 | if not re.search(pattern, message):
17 | raise AssertionError(
18 | f"exception {excinfo.value!r} did not match pattern {pattern!r}"
19 | )
20 |
21 |
22 | class CountingScheduler:
23 | """Simple dask scheduler counting the number of computes.
24 |
25 | Reference: https://stackoverflow.com/questions/53289286/"""
26 |
27 | def __init__(self, max_computes=0):
28 | self.total_computes = 0
29 | self.max_computes = max_computes
30 |
31 | def __call__(self, dsk, keys, **kwargs):
32 | self.total_computes += 1
33 | if self.total_computes > self.max_computes:
34 | raise RuntimeError(
35 | f"Too many computes. Total:{self.total_computes} > max: {self.max_computes}."
36 | )
37 | return dask.get(dsk, keys, **kwargs)
38 |
39 |
40 | def raise_if_dask_computes(max_computes=0):
41 | scheduler = CountingScheduler(max_computes)
42 | return dask.config.set(scheduler=scheduler)
43 |
44 |
45 | def _importorskip(modname, minversion=None):
46 | try:
47 | mod = importlib.import_module(modname)
48 | has = True
49 | if minversion is not None:
50 | if LooseVersion(mod.__version__) < LooseVersion(minversion):
51 | raise ImportError("Minimum version not satisfied")
52 | except ImportError:
53 | has = False
54 | func = pytest.mark.skipif(not has, reason=f"requires {modname}")
55 | return has, func
56 |
57 |
58 | def LooseVersion(vstring):
59 | # Our development version is something like '0.10.9+aac7bfc'
60 | # This function just ignored the git commit id.
61 | vstring = vstring.split("+")[0]
62 | return version.parse(vstring)
63 |
64 |
65 | has_cftime, requires_cftime = _importorskip("cftime")
66 | has_scipy, requires_scipy = _importorskip("scipy")
67 | has_shapely, requires_shapely = _importorskip("shapely")
68 | has_pint, requires_pint = _importorskip("pint")
69 | has_pooch, requires_pooch = _importorskip("pooch")
70 | _, requires_rich = _importorskip("rich")
71 | has_regex, requires_regex = _importorskip("regex")
72 |
--------------------------------------------------------------------------------
/cf_xarray/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | import xarray as xr
4 |
5 |
6 | @pytest.fixture(scope="session")
7 | def geometry_ds():
8 | pytest.importorskip("shapely")
9 |
10 | from shapely.geometry import MultiPoint, Point
11 |
12 | # empty/fill workaround to avoid numpy deprecation(warning) due to the array interface of shapely geometries.
13 | geoms = np.empty(4, dtype=object)
14 | geoms[:] = [
15 | MultiPoint([(1.0, 2.0), (2.0, 3.0)]),
16 | Point(3.0, 4.0),
17 | Point(4.0, 5.0),
18 | Point(3.0, 4.0),
19 | ]
20 |
21 | ds = xr.Dataset(
22 | {
23 | "data": xr.DataArray(
24 | range(len(geoms)),
25 | dims=("index",),
26 | attrs={
27 | "coordinates": "crd_x crd_y",
28 | },
29 | ),
30 | "time": xr.DataArray([0, 0, 0, 1], dims=("index",)),
31 | }
32 | )
33 | shp_ds = ds.assign(geometry=xr.DataArray(geoms, dims=("index",)))
34 | # Here, since it should not be present in shp_ds
35 | ds.data.attrs["geometry"] = "geometry_container"
36 |
37 | cf_ds = ds.assign(
38 | x=xr.DataArray([1.0, 2.0, 3.0, 4.0, 3.0], dims=("node",), attrs={"axis": "X"}),
39 | y=xr.DataArray([2.0, 3.0, 4.0, 5.0, 4.0], dims=("node",), attrs={"axis": "Y"}),
40 | node_count=xr.DataArray([2, 1, 1, 1], dims=("index",)),
41 | crd_x=xr.DataArray([1.0, 3.0, 4.0, 3.0], dims=("index",), attrs={"nodes": "x"}),
42 | crd_y=xr.DataArray([2.0, 4.0, 5.0, 4.0], dims=("index",), attrs={"nodes": "y"}),
43 | geometry_container=xr.DataArray(
44 | attrs={
45 | "geometry_type": "point",
46 | "node_count": "node_count",
47 | "node_coordinates": "x y",
48 | "coordinates": "crd_x crd_y",
49 | }
50 | ),
51 | )
52 |
53 | cf_ds = cf_ds.set_coords(["x", "y", "crd_x", "crd_y"])
54 |
55 | return cf_ds, shp_ds
56 |
--------------------------------------------------------------------------------
/cf_xarray/tests/test_coding.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import pytest
4 | import xarray as xr
5 |
6 | import cf_xarray as cfxr
7 |
8 |
9 | @pytest.mark.parametrize(
10 | "mindex",
11 | [
12 | pd.MultiIndex.from_product([["a", "b"], [1, 2]], names=("lat", "lon")),
13 | pd.MultiIndex.from_arrays(
14 | [["a", "b", "c", "d"], [1, 2, 4, 10]], names=("lat", "lon")
15 | ),
16 | pd.MultiIndex.from_arrays(
17 | [["a", "b", "b", "a"], [1, 2, 1, 2]], names=("lat", "lon")
18 | ),
19 | ],
20 | )
21 | @pytest.mark.parametrize("idxnames", ["foo", "landpoint", ("landpoint",), None])
22 | def test_compression_by_gathering_multi_index_roundtrip(mindex, idxnames):
23 | dim = "foo" if idxnames == "foo" else "landpoint"
24 | dataset = xr.Dataset(
25 | data_vars={"landsoilt": (dim, np.random.randn(4), {"foo": "bar"})},
26 | coords={
27 | dim: (dim, mindex, {"long_name": "land point number"}),
28 | "coord1": (dim, [1, 2, 3, 4], {"foo": "baz"}),
29 | },
30 | attrs={"dataset": "test dataset"},
31 | )
32 | dataset.lat.attrs["standard_name"] = "latitude"
33 | dataset.lon.attrs["standard_name"] = "longitude"
34 |
35 | encoded = cfxr.encode_multi_index_as_compress(dataset, idxnames)
36 | roundtrip = cfxr.decode_compress_to_multi_index(encoded, idxnames)
37 | assert "compress" not in roundtrip[dim].encoding
38 | xr.testing.assert_identical(roundtrip, dataset)
39 |
40 | dataset[dim].attrs["compress"] = "lat lon"
41 | with pytest.raises(ValueError):
42 | cfxr.encode_multi_index_as_compress(dataset, idxnames)
43 |
--------------------------------------------------------------------------------
/cf_xarray/tests/test_groupers.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | import xarray as xr
4 | from xarray.testing import assert_identical
5 |
6 | pytest.importorskip("xarray", "2024.07.0")
7 |
8 | from cf_xarray.datasets import flag_excl
9 | from cf_xarray.groupers import FlagGrouper
10 |
11 |
12 | def test_flag_grouper():
13 | ds = flag_excl.to_dataset().set_coords("flag_var").copy(deep=True)
14 | ds["foo"] = ("time", np.arange(8))
15 | actual = ds.groupby(flag_var=FlagGrouper()).mean()
16 | expected = ds.groupby("flag_var").mean()
17 | expected["flag_var"] = ["flag_1", "flag_2", "flag_3"]
18 | expected["flag_var"].attrs["standard_name"] = "flag_mutual_exclusive"
19 | assert_identical(actual, expected)
20 |
21 | del ds.flag_var.attrs["flag_values"]
22 | with pytest.raises(ValueError):
23 | ds.groupby(flag_var=FlagGrouper())
24 |
25 | ds.flag_var.attrs["flag_values"] = [0, 1, 2]
26 | del ds.flag_var.attrs["flag_meanings"]
27 | with pytest.raises(ValueError):
28 | ds.groupby(flag_var=FlagGrouper())
29 |
30 |
31 | @pytest.mark.parametrize(
32 | "values",
33 | [
34 | [1, 2],
35 | [1, 2, 3], # value out of range of flag_values
36 | ],
37 | )
38 | def test_flag_grouper_optimized(values):
39 | ds = xr.Dataset(
40 | {"foo": ("x", values, {"flag_values": [0, 1, 2], "flag_meanings": "a b c"})}
41 | )
42 | ret = FlagGrouper().factorize(ds.foo)
43 | expected = ds.foo
44 | expected.data[ds.foo.data > 2] = -1
45 | del ds.foo.attrs["flag_meanings"]
46 | del ds.foo.attrs["flag_values"]
47 | assert_identical(ret.codes, ds.foo)
48 |
--------------------------------------------------------------------------------
/cf_xarray/tests/test_helpers.py:
--------------------------------------------------------------------------------
1 | from numpy.testing import assert_array_equal
2 | from xarray.testing import assert_equal
3 |
4 | import cf_xarray as cfxr # noqa
5 |
6 | from ..datasets import airds, mollwds, rotds
7 |
8 | try:
9 | from dask.array import Array as DaskArray
10 | except ImportError:
11 | DaskArray = None # type: ignore[assignment, misc]
12 |
13 |
14 | def test_bounds_to_vertices() -> None:
15 | # 1D case
16 | ds = airds.cf.add_bounds(["lon", "lat", "time"])
17 | lat_c = cfxr.bounds_to_vertices(ds.lat_bounds, bounds_dim="bounds")
18 | assert_array_equal(ds.lat.values + 1.25, lat_c.values[:-1])
19 |
20 | # 2D case
21 | lat_ccw = cfxr.bounds_to_vertices(
22 | mollwds.lat_bounds, bounds_dim="bounds", order="counterclockwise"
23 | )
24 | lat_no = cfxr.bounds_to_vertices(
25 | mollwds.lat_bounds, bounds_dim="bounds", order=None
26 | )
27 | assert_equal(mollwds.lat_vertices, lat_ccw)
28 | assert_equal(lat_no, lat_ccw)
29 |
30 | # 2D case with precision issues, check if CF- order is "detected" correctly
31 | lon_ccw = cfxr.bounds_to_vertices(
32 | rotds.lon_bounds, bounds_dim="bounds", order="counterclockwise"
33 | )
34 | lon_no = cfxr.bounds_to_vertices(rotds.lon_bounds, bounds_dim="bounds", order=None)
35 | assert_equal(lon_no, lon_ccw)
36 |
37 | # Transposing the array changes the bounds direction
38 | ds = mollwds.transpose("x", "y", "x_vertices", "y_vertices", "bounds")
39 | lon_cw = cfxr.bounds_to_vertices(
40 | ds.lon_bounds, bounds_dim="bounds", order="clockwise"
41 | )
42 | lon_no2 = cfxr.bounds_to_vertices(ds.lon_bounds, bounds_dim="bounds", order=None)
43 | assert_equal(ds.lon_vertices, lon_cw)
44 | assert_equal(ds.lon_vertices, lon_no2)
45 |
46 | # Preserves dask-backed arrays
47 | if DaskArray is not None:
48 | lon_bounds = ds.lon_bounds.chunk()
49 | lon_c = cfxr.bounds_to_vertices(
50 | lon_bounds, bounds_dim="bounds", order="clockwise"
51 | )
52 | assert isinstance(lon_c.data, DaskArray)
53 |
54 |
55 | def test_vertices_to_bounds() -> None:
56 | # 1D case
57 | ds = airds.cf.add_bounds(["lon", "lat", "time"])
58 | lat_c = cfxr.bounds_to_vertices(ds.lat_bounds, bounds_dim="bounds")
59 | lat_b = cfxr.vertices_to_bounds(lat_c, out_dims=("bounds", "lat"))
60 | assert_array_equal(ds.lat_bounds, lat_b)
61 |
62 | # Datetime
63 | time_c = cfxr.bounds_to_vertices(ds.time_bounds, bounds_dim="bounds")
64 | time_b = cfxr.vertices_to_bounds(time_c, out_dims=("bounds", "time"))
65 | assert_array_equal(ds.time_bounds, time_b)
66 |
67 | # 2D case
68 | lon_b = cfxr.vertices_to_bounds(mollwds.lon_vertices, out_dims=("bounds", "x", "y"))
69 | assert_array_equal(mollwds.lon_bounds, lon_b)
70 |
--------------------------------------------------------------------------------
/cf_xarray/tests/test_options.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests OPTIONS logic brought in from xarray.
3 | """
4 |
5 | import pytest
6 |
7 | import cf_xarray as cfxr
8 |
9 |
10 | def test_options():
11 | # test for inputting a nonexistent option
12 | with pytest.raises(ValueError):
13 | cfxr.set_options(DISPLAY_WIDTH=80)
14 |
--------------------------------------------------------------------------------
/cf_xarray/tests/test_scripts.py:
--------------------------------------------------------------------------------
1 | import os
2 | from tempfile import TemporaryDirectory
3 |
4 | from cf_xarray.scripts import make_doc
5 |
6 |
7 | def test_make_doc():
8 | names = [
9 | "axes_criteria",
10 | "coords_criteria",
11 | "all_criteria",
12 | "all_regex",
13 | ]
14 | tables_to_check = [f"_build/csv/{name}.csv" for name in names]
15 |
16 | # Create _build/csv in a temporary directory
17 | owd = os.getcwd()
18 | with TemporaryDirectory() as tmpdirname:
19 | try:
20 | os.chdir(os.path.dirname(tmpdirname))
21 | make_doc.main()
22 | assert all(os.path.exists(path) for path in tables_to_check)
23 | finally:
24 | # Always return to original working directory
25 | os.chdir(owd)
26 |
--------------------------------------------------------------------------------
/cf_xarray/tests/test_units.py:
--------------------------------------------------------------------------------
1 | r"""Tests the operation of cf_xarray's ported unit support code.
2 |
3 | Reused with modification from MetPy under the terms of the BSD 3-Clause License.
4 | Copyright (c) 2017 MetPy Developers.
5 | """
6 |
7 | import pytest
8 |
9 | pytest.importorskip("pint")
10 |
11 | from ..units import units as ureg
12 |
13 |
14 | def test_added_degrees_units():
15 | """Test that our added degrees units are present in the registry."""
16 | # Test equivalence of abbreviations/aliases to our defined names
17 | assert str(ureg("degrees_N").units) == "degrees_north"
18 | assert str(ureg("degreesN").units) == "degrees_north"
19 | assert str(ureg("degree_north").units) == "degrees_north"
20 | assert str(ureg("degree_N").units) == "degrees_north"
21 | assert str(ureg("degreeN").units) == "degrees_north"
22 | assert str(ureg("degrees_E").units) == "degrees_east"
23 | assert str(ureg("degreesE").units) == "degrees_east"
24 | assert str(ureg("degree_east").units) == "degrees_east"
25 | assert str(ureg("degree_E").units) == "degrees_east"
26 | assert str(ureg("degreeE").units) == "degrees_east"
27 |
28 | # Test equivalence of our defined units to base units
29 | assert ureg("degrees_north") == ureg("degrees")
30 | assert ureg("degrees_north").to_base_units().units == ureg.radian
31 | assert ureg("degrees_east") == ureg("degrees")
32 | assert ureg("degrees_east").to_base_units().units == ureg.radian
33 |
34 | assert ureg("degrees").to_base_units().units == ureg.radian
35 |
36 |
37 | def test_gpm_unit():
38 | """Test that the gpm unit does alias to meters."""
39 | x = 1 * ureg("gpm")
40 | assert str(x.units) == "meter"
41 |
42 |
43 | def test_psu_unit():
44 | """Test that the psu unit are present in the registry."""
45 | x = 1 * ureg("psu")
46 | assert str(x.units) == "practical_salinity_unit"
47 |
48 |
49 | def test_percent_units():
50 | """Test that percent sign units are properly parsed and interpreted."""
51 | assert str(ureg("%").units) == "percent"
52 |
53 |
54 | def test_integer_units():
55 | """Test that integer 1 units is equal to dimensionless"""
56 | # need to explicitly use parse_units to bypass the runtime type checking
57 | # in the quantity constructor
58 | assert str(ureg.parse_units(1)) == "dimensionless"
59 |
60 |
61 | def test_udunits_power_syntax():
62 | """Test that UDUNITS style powers are properly parsed and interpreted."""
63 | assert ureg("m2 s-2").units == ureg.m**2 / ureg.s**2
64 |
65 |
66 | def test_udunits_power_syntax_parse_units():
67 | """Test that UDUNITS style powers are properly parsed and interpreted."""
68 | assert ureg.parse_units("m2 s-2") == ureg.m**2 / ureg.s**2
69 |
70 |
71 | @pytest.mark.parametrize(
72 | ["units", "expected"],
73 | (
74 | ("kg ** 2", "kg2"),
75 | ("m ** -1", "m-1"),
76 | ("m ** 2 / s ** 2", "m2 s-2"),
77 | ("m ** 3 / (kg * s ** 2)", "m3 kg-1 s-2"),
78 | ("", "1"),
79 | ),
80 | )
81 | def test_udunits_format(units, expected):
82 | u = ureg.parse_units(units)
83 | if units == "":
84 | # The non-shortened dimensionless can only work with recent pint
85 | pytest.importorskip("pint", minversion="0.24.1")
86 |
87 | assert f"{u:~cf}" == expected
88 | assert f"{u:cf}" == expected
89 |
90 |
91 | @pytest.mark.parametrize(
92 | "alias",
93 | [ureg("Celsius"), ureg("degC"), ureg("C"), ureg("deg_C"), ureg("degrees_Celsius")],
94 | )
95 | def test_temperature_aliases(alias):
96 | assert alias == ureg("celsius")
97 |
--------------------------------------------------------------------------------
/cf_xarray/units.py:
--------------------------------------------------------------------------------
1 | """Module to provide unit support via pint approximating UDUNITS/CF."""
2 |
3 | import functools
4 | import re
5 |
6 | import pint
7 | from packaging.version import Version
8 |
9 | from .utils import emit_user_level_warning
10 |
11 |
12 | @pint.register_unit_format("cf")
13 | def short_formatter(unit, registry, **options):
14 | """Return a CF-compliant unit string from a `pint` unit.
15 |
16 | Parameters
17 | ----------
18 | unit : pint.UnitContainer
19 | Input unit.
20 | registry : pint.UnitRegistry
21 | The associated registry
22 | **options
23 | Additional options (may be ignored)
24 |
25 | Returns
26 | -------
27 | out : str
28 | Units following CF-Convention, using symbols.
29 | """
30 | # pint 0.24.1 gives {"dimensionless": 1} for non-shortened dimensionless units
31 | # CF uses "1" to denote fractions and dimensionless quantities
32 | if unit == {"dimensionless": 1} or not unit:
33 | return "1"
34 |
35 | # If u is a name, get its symbol (same as pint's "~" pre-formatter)
36 | # otherwise, assume a symbol (pint should have already raised on invalid units before this)
37 | unit = pint.util.UnitsContainer(
38 | {
39 | registry._get_symbol(u) if u in registry._units else u: exp
40 | for u, exp in unit.items()
41 | }
42 | )
43 |
44 | # Change in formatter signature in pint 0.24
45 | if Version(pint.__version__) < Version("0.24"):
46 | args = (unit.items(),)
47 | else:
48 | # Numerators splitted from denominators
49 | args = (
50 | ((u, e) for u, e in unit.items() if e >= 0),
51 | ((u, e) for u, e in unit.items() if e < 0),
52 | )
53 |
54 | out = pint.formatter(*args, as_ratio=False, product_fmt=" ", power_fmt="{}{}")
55 | # To avoid potentiel unicode problems in netCDF. In both cases, this unit is not recognized by udunits
56 | return out.replace("Δ°", "delta_deg")
57 |
58 |
59 | # ------
60 | # Reused with modification from MetPy under the terms of the BSD 3-Clause License.
61 | # Copyright (c) 2015,2017,2019 MetPy Developers.
62 | # Create registry, with preprocessors for UDUNITS-style powers (m2 s-2) and percent signs
63 | units = pint.UnitRegistry(
64 | autoconvert_offset_to_baseunit=True,
65 | preprocessors=[
66 | functools.partial(
67 | re.compile(
68 | r"(?<=[A-Za-z])(?![A-Za-z])(? bool:
39 | """Check if an array contains cftime.datetime objects"""
40 | # Copied / adapted from xarray.core.common
41 | if cftime is None:
42 | return False
43 | else:
44 | if array.dtype == np.dtype("O") and array.size > 0:
45 | sample = array.ravel()[0]
46 | if _is_duck_dask_array(sample):
47 | sample = sample.compute()
48 | if isinstance(sample, np.ndarray):
49 | sample = sample.item()
50 | return isinstance(sample, cftime.datetime)
51 | else:
52 | return False
53 |
54 |
55 | def _is_datetime_like(da: DataArray) -> bool:
56 | if np.issubdtype(da.dtype, np.datetime64) or np.issubdtype(
57 | da.dtype, np.timedelta64
58 | ):
59 | return True
60 | # if cftime was not imported, _contains_cftime_datetimes will return False
61 | if _contains_cftime_datetimes(da.data):
62 | return True
63 |
64 | return False
65 |
66 |
67 | def parse_cell_methods_attr(attr: str) -> dict[str, str]:
68 | """
69 | Parse cell_methods attributes (format is 'measure: name').
70 |
71 | Parameters
72 | ----------
73 | attr : str
74 | String to parse
75 |
76 | Returns
77 | -------
78 | Dictionary mapping measure to name
79 | """
80 | strings = [s for scolons in attr.split(":") for s in scolons.split()]
81 | if len(strings) % 2 != 0:
82 | raise ValueError(f"attrs['cell_measures'] = {attr!r} is malformed.")
83 |
84 | return dict(
85 | zip(strings[slice(0, None, 2)], strings[slice(1, None, 2)], strict=False)
86 | )
87 |
88 |
89 | def invert_mappings(*mappings):
90 | """Takes a set of mappings and iterates through, inverting to make a
91 | new mapping of value: set(keys). Keys are deduplicated to avoid clashes between
92 | standard_name and coordinate names."""
93 | merged = defaultdict(set)
94 | for mapping in mappings:
95 | for k, v in mapping.items():
96 | for name in v:
97 | merged[name] |= {k}
98 | return merged
99 |
100 |
101 | def always_iterable(obj: Any, allowed=(tuple, list, set, dict)) -> Iterable:
102 | return [obj] if not isinstance(obj, allowed) else obj
103 |
104 |
105 | def parse_cf_standard_name_table(source=None):
106 | """"""
107 |
108 | if not source:
109 | import pooch
110 |
111 | downloader = pooch.HTTPDownloader(
112 | # https://github.com/readthedocs/readthedocs.org/issues/11763
113 | headers={"User-Agent": "cf-xarray"}
114 | )
115 |
116 | source = pooch.retrieve(
117 | "https://raw.githubusercontent.com/cf-convention/cf-convention.github.io/"
118 | "master/Data/cf-standard-names/current/src/cf-standard-name-table.xml",
119 | known_hash=None,
120 | downloader=downloader,
121 | )
122 | root = ElementTree.parse(source).getroot()
123 |
124 | # Build dictionaries
125 | info = {}
126 | table = {}
127 | aliases = {}
128 | for child in root:
129 | if child.tag == "entry":
130 | key = child.attrib.get("id")
131 | table[key] = {}
132 | for item in ["canonical_units", "grib", "amip", "description"]:
133 | parsed = child.findall(item)
134 | attr = item.replace("canonical_", "")
135 | table[key][attr] = (parsed[0].text or "") if parsed else ""
136 | elif child.tag == "alias":
137 | alias = child.attrib.get("id")
138 | key = child.findall("entry_id")[0].text
139 | aliases[alias] = key
140 | else:
141 | info[child.tag] = child.text
142 |
143 | return info, table, aliases
144 |
145 |
146 | def _get_version():
147 | __version__ = "unknown"
148 | try:
149 | from ._version import __version__
150 | except ImportError:
151 | pass
152 | return __version__
153 |
154 |
155 | def find_stack_level(test_mode=False) -> int:
156 | """Find the first place in the stack that is not inside xarray.
157 |
158 | This is unless the code emanates from a test, in which case we would prefer
159 | to see the xarray source.
160 |
161 | This function is taken from pandas.
162 |
163 | Parameters
164 | ----------
165 | test_mode : bool
166 | Flag used for testing purposes to switch off the detection of test
167 | directories in the stack trace.
168 |
169 | Returns
170 | -------
171 | stacklevel : int
172 | First level in the stack that is not part of xarray.
173 | """
174 | import cf_xarray as cfxr
175 |
176 | pkg_dir = os.path.dirname(cfxr.__file__)
177 | test_dir = os.path.join(pkg_dir, "tests")
178 |
179 | # https://stackoverflow.com/questions/17407119/python-inspect-stack-is-slow
180 | frame = inspect.currentframe()
181 | n = 0
182 | while frame:
183 | fname = inspect.getfile(frame)
184 | if fname.startswith(pkg_dir) and (not fname.startswith(test_dir) or test_mode):
185 | frame = frame.f_back
186 | n += 1
187 | else:
188 | break
189 | return n
190 |
191 |
192 | def emit_user_level_warning(message, category=None):
193 | """Emit a warning at the user level by inspecting the stack trace."""
194 | stacklevel = find_stack_level()
195 | warnings.warn(message, category=category, stacklevel=stacklevel)
196 |
--------------------------------------------------------------------------------
/ci/doc.yml:
--------------------------------------------------------------------------------
1 | name: cf-xarray-doc
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - pip
6 | - python
7 | - matplotlib-base
8 | - netcdf4
9 | - pooch
10 | - xarray
11 | - sphinx<8
12 | - sphinx-copybutton
13 | - numpydoc
14 | - sphinx-autosummary-accessors
15 | - ipython
16 | - ipykernel
17 | - ipywidgets
18 | - pandas
19 | - pooch
20 | - pint
21 | - regex
22 | - shapely
23 | - furo>=2024
24 | - myst-nb
25 | - pip:
26 | - -e ../
27 |
--------------------------------------------------------------------------------
/ci/environment-all-min-deps.yml:
--------------------------------------------------------------------------------
1 | name: cf_xarray_test
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - pytest-cov
6 | - pytest
7 | - pytest-xdist
8 | - dask
9 | - flox
10 | - lxml
11 | - matplotlib-base
12 | - netcdf4
13 | - pandas
14 | - pint==0.19
15 | - pooch
16 | - regex
17 | - rich
18 | - pooch
19 | - scipy
20 | - shapely
21 | - xarray==2023.09.0
22 | - pip
23 | - pip:
24 | - pytest-pretty
25 |
--------------------------------------------------------------------------------
/ci/environment-no-optional-deps.yml:
--------------------------------------------------------------------------------
1 | name: cf_xarray_test
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - pytest-cov
6 | - pytest
7 | - pytest-xdist
8 | - dask
9 | - matplotlib-base
10 | - netcdf4
11 | - pandas
12 | - pooch
13 | - xarray
14 |
--------------------------------------------------------------------------------
/ci/environment.yml:
--------------------------------------------------------------------------------
1 | name: cf_xarray_test
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - pytest-cov
6 | - pytest
7 | - pytest-xdist
8 | - dask
9 | - flox
10 | - lxml
11 | - matplotlib-base
12 | - netcdf4
13 | - pandas
14 | - pint
15 | - pooch
16 | - regex
17 | - rich
18 | - pooch
19 | - scipy
20 | - shapely
21 | - xarray
22 | - pip
23 | - pip:
24 | - pytest-pretty
25 |
--------------------------------------------------------------------------------
/ci/upstream-dev-env.yml:
--------------------------------------------------------------------------------
1 | name: cf_xarray_test
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - pytest-cov
6 | - pytest
7 | - pytest-xdist
8 | - pytest-reportlog
9 | - dask
10 | - matplotlib-base
11 | - netcdf4
12 | - pandas
13 | - pooch
14 | - rich
15 | - shapely
16 | - pip
17 | - pip:
18 | - pytest-pretty
19 | - git+https://github.com/pydata/xarray.git
20 | - git+https://github.com/dcherian/flox.git
21 | - git+https://github.com/hgrecco/pint.git
22 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - "*/tests/*"
3 | - "cf_xarray/datasets.py"
4 |
--------------------------------------------------------------------------------
/doc/2D_bounds_averaged.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xarray-contrib/cf-xarray/4be96ff2e40e7a7fc7ff6d57744c4a42b6cbaa94/doc/2D_bounds_averaged.png
--------------------------------------------------------------------------------
/doc/2D_bounds_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xarray-contrib/cf-xarray/4be96ff2e40e7a7fc7ff6d57744c4a42b6cbaa94/doc/2D_bounds_error.png
--------------------------------------------------------------------------------
/doc/2D_bounds_nonunique.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xarray-contrib/cf-xarray/4be96ff2e40e7a7fc7ff6d57744c4a42b6cbaa94/doc/2D_bounds_nonunique.png
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
22 | .PHONY: clean
23 | clean:
24 | rm -rf $(BUILDDIR)/*
25 | rm -rf generated/*
26 |
--------------------------------------------------------------------------------
/doc/_static/dataset-diagram-logo.tex:
--------------------------------------------------------------------------------
1 | \documentclass[class=minimal,border=0pt,convert={size=600,outext=.png}]{standalone}
2 | % \documentclass[class=minimal,border=0pt]{standalone}
3 | \usepackage[scaled]{helvet}
4 | \renewcommand*\familydefault{\sfdefault}
5 |
6 | % ===========================================================================
7 | % The code below (used to define the \tikzcuboid command) is copied,
8 | % unmodified, from a tex.stackexchange.com answer by the user "Tom Bombadil":
9 | % http://tex.stackexchange.com/a/29882/8335
10 | %
11 | % It is licensed under the Creative Commons Attribution-ShareAlike 3.0
12 | % Unported license: http://creativecommons.org/licenses/by-sa/3.0/
13 | % ===========================================================================
14 |
15 | \usepackage[usenames,dvipsnames]{color}
16 | \usepackage{tikz}
17 | \usepackage{keyval}
18 | \usepackage{ifthen}
19 |
20 | %====================================
21 | %emphasize vertices --> switch and emph style (e.g. thick,black)
22 | %====================================
23 | \makeatletter
24 | % Standard Values for Parameters
25 | \newcommand{\tikzcuboid@shiftx}{0}
26 | \newcommand{\tikzcuboid@shifty}{0}
27 | \newcommand{\tikzcuboid@dimx}{3}
28 | \newcommand{\tikzcuboid@dimy}{3}
29 | \newcommand{\tikzcuboid@dimz}{3}
30 | \newcommand{\tikzcuboid@scale}{1}
31 | \newcommand{\tikzcuboid@densityx}{1}
32 | \newcommand{\tikzcuboid@densityy}{1}
33 | \newcommand{\tikzcuboid@densityz}{1}
34 | \newcommand{\tikzcuboid@rotation}{0}
35 | \newcommand{\tikzcuboid@anglex}{0}
36 | \newcommand{\tikzcuboid@angley}{90}
37 | \newcommand{\tikzcuboid@anglez}{225}
38 | \newcommand{\tikzcuboid@scalex}{1}
39 | \newcommand{\tikzcuboid@scaley}{1}
40 | \newcommand{\tikzcuboid@scalez}{sqrt(0.5)}
41 | \newcommand{\tikzcuboid@linefront}{black}
42 | \newcommand{\tikzcuboid@linetop}{black}
43 | \newcommand{\tikzcuboid@lineright}{black}
44 | \newcommand{\tikzcuboid@fillfront}{white}
45 | \newcommand{\tikzcuboid@filltop}{white}
46 | \newcommand{\tikzcuboid@fillright}{white}
47 | \newcommand{\tikzcuboid@shaded}{N}
48 | \newcommand{\tikzcuboid@shadecolor}{black}
49 | \newcommand{\tikzcuboid@shadeperc}{25}
50 | \newcommand{\tikzcuboid@emphedge}{N}
51 | \newcommand{\tikzcuboid@emphstyle}{thick}
52 |
53 | % Definition of Keys
54 | \define@key{tikzcuboid}{shiftx}[\tikzcuboid@shiftx]{\renewcommand{\tikzcuboid@shiftx}{#1}}
55 | \define@key{tikzcuboid}{shifty}[\tikzcuboid@shifty]{\renewcommand{\tikzcuboid@shifty}{#1}}
56 | \define@key{tikzcuboid}{dimx}[\tikzcuboid@dimx]{\renewcommand{\tikzcuboid@dimx}{#1}}
57 | \define@key{tikzcuboid}{dimy}[\tikzcuboid@dimy]{\renewcommand{\tikzcuboid@dimy}{#1}}
58 | \define@key{tikzcuboid}{dimz}[\tikzcuboid@dimz]{\renewcommand{\tikzcuboid@dimz}{#1}}
59 | \define@key{tikzcuboid}{scale}[\tikzcuboid@scale]{\renewcommand{\tikzcuboid@scale}{#1}}
60 | \define@key{tikzcuboid}{densityx}[\tikzcuboid@densityx]{\renewcommand{\tikzcuboid@densityx}{#1}}
61 | \define@key{tikzcuboid}{densityy}[\tikzcuboid@densityy]{\renewcommand{\tikzcuboid@densityy}{#1}}
62 | \define@key{tikzcuboid}{densityz}[\tikzcuboid@densityz]{\renewcommand{\tikzcuboid@densityz}{#1}}
63 | \define@key{tikzcuboid}{rotation}[\tikzcuboid@rotation]{\renewcommand{\tikzcuboid@rotation}{#1}}
64 | \define@key{tikzcuboid}{anglex}[\tikzcuboid@anglex]{\renewcommand{\tikzcuboid@anglex}{#1}}
65 | \define@key{tikzcuboid}{angley}[\tikzcuboid@angley]{\renewcommand{\tikzcuboid@angley}{#1}}
66 | \define@key{tikzcuboid}{anglez}[\tikzcuboid@anglez]{\renewcommand{\tikzcuboid@anglez}{#1}}
67 | \define@key{tikzcuboid}{scalex}[\tikzcuboid@scalex]{\renewcommand{\tikzcuboid@scalex}{#1}}
68 | \define@key{tikzcuboid}{scaley}[\tikzcuboid@scaley]{\renewcommand{\tikzcuboid@scaley}{#1}}
69 | \define@key{tikzcuboid}{scalez}[\tikzcuboid@scalez]{\renewcommand{\tikzcuboid@scalez}{#1}}
70 | \define@key{tikzcuboid}{linefront}[\tikzcuboid@linefront]{\renewcommand{\tikzcuboid@linefront}{#1}}
71 | \define@key{tikzcuboid}{linetop}[\tikzcuboid@linetop]{\renewcommand{\tikzcuboid@linetop}{#1}}
72 | \define@key{tikzcuboid}{lineright}[\tikzcuboid@lineright]{\renewcommand{\tikzcuboid@lineright}{#1}}
73 | \define@key{tikzcuboid}{fillfront}[\tikzcuboid@fillfront]{\renewcommand{\tikzcuboid@fillfront}{#1}}
74 | \define@key{tikzcuboid}{filltop}[\tikzcuboid@filltop]{\renewcommand{\tikzcuboid@filltop}{#1}}
75 | \define@key{tikzcuboid}{fillright}[\tikzcuboid@fillright]{\renewcommand{\tikzcuboid@fillright}{#1}}
76 | \define@key{tikzcuboid}{shaded}[\tikzcuboid@shaded]{\renewcommand{\tikzcuboid@shaded}{#1}}
77 | \define@key{tikzcuboid}{shadecolor}[\tikzcuboid@shadecolor]{\renewcommand{\tikzcuboid@shadecolor}{#1}}
78 | \define@key{tikzcuboid}{shadeperc}[\tikzcuboid@shadeperc]{\renewcommand{\tikzcuboid@shadeperc}{#1}}
79 | \define@key{tikzcuboid}{emphedge}[\tikzcuboid@emphedge]{\renewcommand{\tikzcuboid@emphedge}{#1}}
80 | \define@key{tikzcuboid}{emphstyle}[\tikzcuboid@emphstyle]{\renewcommand{\tikzcuboid@emphstyle}{#1}}
81 | % Commands
82 | \newcommand{\tikzcuboid}[1]{
83 | \setkeys{tikzcuboid}{#1} % Process Keys passed to command
84 | \pgfmathsetmacro{\vectorxx}{\tikzcuboid@scalex*cos(\tikzcuboid@anglex)}
85 | \pgfmathsetmacro{\vectorxy}{\tikzcuboid@scalex*sin(\tikzcuboid@anglex)}
86 | \pgfmathsetmacro{\vectoryx}{\tikzcuboid@scaley*cos(\tikzcuboid@angley)}
87 | \pgfmathsetmacro{\vectoryy}{\tikzcuboid@scaley*sin(\tikzcuboid@angley)}
88 | \pgfmathsetmacro{\vectorzx}{\tikzcuboid@scalez*cos(\tikzcuboid@anglez)}
89 | \pgfmathsetmacro{\vectorzy}{\tikzcuboid@scalez*sin(\tikzcuboid@anglez)}
90 | \begin{scope}[xshift=\tikzcuboid@shiftx, yshift=\tikzcuboid@shifty, scale=\tikzcuboid@scale, rotate=\tikzcuboid@rotation, x={(\vectorxx,\vectorxy)}, y={(\vectoryx,\vectoryy)}, z={(\vectorzx,\vectorzy)}]
91 | \pgfmathsetmacro{\steppingx}{1/\tikzcuboid@densityx}
92 | \pgfmathsetmacro{\steppingy}{1/\tikzcuboid@densityy}
93 | \pgfmathsetmacro{\steppingz}{1/\tikzcuboid@densityz}
94 | \newcommand{\dimx}{\tikzcuboid@dimx}
95 | \newcommand{\dimy}{\tikzcuboid@dimy}
96 | \newcommand{\dimz}{\tikzcuboid@dimz}
97 | \pgfmathsetmacro{\secondx}{2*\steppingx}
98 | \pgfmathsetmacro{\secondy}{2*\steppingy}
99 | \pgfmathsetmacro{\secondz}{2*\steppingz}
100 | \foreach \x in {\steppingx,\secondx,...,\dimx}
101 | { \foreach \y in {\steppingy,\secondy,...,\dimy}
102 | { \pgfmathsetmacro{\lowx}{(\x-\steppingx)}
103 | \pgfmathsetmacro{\lowy}{(\y-\steppingy)}
104 | \filldraw[fill=\tikzcuboid@fillfront,draw=\tikzcuboid@linefront] (\lowx,\lowy,\dimz) -- (\lowx,\y,\dimz) -- (\x,\y,\dimz) -- (\x,\lowy,\dimz) -- cycle;
105 |
106 | }
107 | }
108 | \foreach \x in {\steppingx,\secondx,...,\dimx}
109 | { \foreach \z in {\steppingz,\secondz,...,\dimz}
110 | { \pgfmathsetmacro{\lowx}{(\x-\steppingx)}
111 | \pgfmathsetmacro{\lowz}{(\z-\steppingz)}
112 | \filldraw[fill=\tikzcuboid@filltop,draw=\tikzcuboid@linetop] (\lowx,\dimy,\lowz) -- (\lowx,\dimy,\z) -- (\x,\dimy,\z) -- (\x,\dimy,\lowz) -- cycle;
113 | }
114 | }
115 | \foreach \y in {\steppingy,\secondy,...,\dimy}
116 | { \foreach \z in {\steppingz,\secondz,...,\dimz}
117 | { \pgfmathsetmacro{\lowy}{(\y-\steppingy)}
118 | \pgfmathsetmacro{\lowz}{(\z-\steppingz)}
119 | \filldraw[fill=\tikzcuboid@fillright,draw=\tikzcuboid@lineright] (\dimx,\lowy,\lowz) -- (\dimx,\lowy,\z) -- (\dimx,\y,\z) -- (\dimx,\y,\lowz) -- cycle;
120 | }
121 | }
122 | \ifthenelse{\equal{\tikzcuboid@emphedge}{Y}}%
123 | {\draw[\tikzcuboid@emphstyle](0,\dimy,0) -- (\dimx,\dimy,0) -- (\dimx,\dimy,\dimz) -- (0,\dimy,\dimz) -- cycle;%
124 | \draw[\tikzcuboid@emphstyle] (0,0,\dimz) -- (0,\dimy,\dimz) -- (\dimx,\dimy,\dimz) -- (\dimx,0,\dimz) -- cycle;%
125 | \draw[\tikzcuboid@emphstyle](\dimx,0,0) -- (\dimx,\dimy,0) -- (\dimx,\dimy,\dimz) -- (\dimx,0,\dimz) -- cycle;%
126 | }%
127 | {}
128 | \end{scope}
129 | }
130 |
131 | \makeatother
132 |
133 | \begin{document}
134 |
135 | \begin{tikzpicture}
136 | % \tikzcuboid{%
137 | % shiftx=21cm,%
138 | % shifty=8cm,%
139 | % scale=1.00,%
140 | % rotation=0,%
141 | % densityx=2,%
142 | % densityy=2,%
143 | % densityz=2,%
144 | % dimx=4,%
145 | % dimy=3,%
146 | % dimz=3,%
147 | % linefront=purple!75!black,%
148 | % linetop=purple!50!black,%
149 | % lineright=purple!25!black,%
150 | % fillfront=purple!25!white,%
151 | % filltop=purple!50!white,%
152 | % fillright=purple!75!white,%
153 | % emphedge=Y,%
154 | % emphstyle=ultra thick,
155 | % }
156 | \tikzcuboid{%
157 | shiftx=21cm,%
158 | shifty=11.6cm,%
159 | scale=1.00,%
160 | rotation=0,%
161 | densityx=2,%
162 | densityy=2,%
163 | densityz=2,%
164 | dimx=4,%
165 | dimy=3,%
166 | dimz=3,%
167 | linefront=teal!75!black,%
168 | linetop=teal!50!black,%
169 | lineright=teal!25!black,%
170 | fillfront=teal!25!white,%
171 | filltop=teal!50!white,%
172 | fillright=teal!75!white,%
173 | emphedge=Y,%
174 | emphstyle=ultra thick,
175 | }
176 | % \tikzcuboid{%
177 | % shiftx=26.8cm,%
178 | % shifty=8cm,%
179 | % scale=1.00,%
180 | % rotation=0,%
181 | % densityx=10000,%
182 | % densityy=2,%
183 | % densityz=2,%
184 | % dimx=0,%
185 | % dimy=3,%
186 | % dimz=3,%
187 | % linefront=orange!75!black,%
188 | % linetop=orange!50!black,%
189 | % lineright=orange!25!black,%
190 | % fillfront=orange!25!white,%
191 | % filltop=orange!50!white,%
192 | % fillright=orange!100!white,%
193 | % emphedge=Y,%
194 | % emphstyle=ultra thick,
195 | % }
196 | % \tikzcuboid{%
197 | % shiftx=28.6cm,%
198 | % shifty=8cm,%
199 | % scale=1.00,%
200 | % rotation=0,%
201 | % densityx=10000,%
202 | % densityy=2,%
203 | % densityz=2,%
204 | % dimx=0,%
205 | % dimy=3,%
206 | % dimz=3,%
207 | % linefront=purple!75!black,%
208 | % linetop=purple!50!black,%
209 | % lineright=purple!25!black,%
210 | % fillfront=purple!25!white,%
211 | % filltop=purple!50!white,%
212 | % fillright=red!75!white,%
213 | % emphedge=Y,%
214 | % emphstyle=ultra thick,
215 | % }
216 | % \tikzcuboid{%
217 | % shiftx=27.1cm,%
218 | % shifty=10.1cm,%
219 | % scale=1.00,%
220 | % rotation=0,%
221 | % densityx=100,%
222 | % densityy=2,%
223 | % densityz=100,%
224 | % dimx=0,%
225 | % dimy=3,%
226 | % dimz=0,%
227 | % emphedge=Y,%
228 | % emphstyle=ultra thick,
229 | % }
230 | % \tikzcuboid{%
231 | % shiftx=27.1cm,%
232 | % shifty=10.1cm,%
233 | % scale=1.00,%
234 | % rotation=180,%
235 | % densityx=100,%
236 | % densityy=100,%
237 | % densityz=2,%
238 | % dimx=0,%
239 | % dimy=0,%
240 | % dimz=3,%
241 | % emphedge=Y,%
242 | % emphstyle=ultra thick,
243 | % }
244 | % \tikzcuboid{%
245 | % shiftx=26.8cm,%
246 | % shifty=11.4cm,%
247 | % scale=1.00,%
248 | % rotation=0,%
249 | % densityx=100,%
250 | % densityy=2,%
251 | % densityz=100,%
252 | % dimx=0,%
253 | % dimy=3,%
254 | % dimz=0,%
255 | % emphedge=Y,%
256 | % emphstyle=ultra thick,
257 | % }
258 | % \tikzcuboid{%
259 | % shiftx=25.3cm,%
260 | % shifty=12.9cm,%
261 | % scale=1.00,%
262 | % rotation=180,%
263 | % densityx=100,%
264 | % densityy=100,%
265 | % densityz=2,%
266 | % dimx=0,%
267 | % dimy=0,%
268 | % dimz=3,%
269 | % emphedge=Y,%
270 | % emphstyle=ultra thick,
271 | % }
272 | % \fill (27.1,10.1) circle[radius=2pt];
273 | % \node [font=\fontsize{100}{100}\fontfamily{phv}\selectfont, anchor=west, text width=9cm, color=white!50!black] at (30,10.6) {\textbf{\emph{x}}};
274 | % \node [font=\fontsize{100}{100}\fontfamily{phv}\selectfont, anchor=west, text width=9cm] at (32,10.25) {{array}};
275 | \end{tikzpicture}
276 |
277 | \end{document}
278 |
--------------------------------------------------------------------------------
/doc/_static/full-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xarray-contrib/cf-xarray/4be96ff2e40e7a7fc7ff6d57744c4a42b6cbaa94/doc/_static/full-logo.png
--------------------------------------------------------------------------------
/doc/_static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xarray-contrib/cf-xarray/4be96ff2e40e7a7fc7ff6d57744c4a42b6cbaa94/doc/_static/logo.png
--------------------------------------------------------------------------------
/doc/_static/rich-repr-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xarray-contrib/cf-xarray/4be96ff2e40e7a7fc7ff6d57744c4a42b6cbaa94/doc/_static/rich-repr-example.png
--------------------------------------------------------------------------------
/doc/_static/style.css:
--------------------------------------------------------------------------------
1 | .xr-wrap {
2 | font-size: 0.85em;
3 | margin-left: 1.25em;
4 | padding-left: 1.25em;
5 | border-left: thin var(--color-foreground-muted) solid;
6 | }
7 | .xr-array-wrap, .xr-var-data, .xr-var-preview {
8 | font-size: 0.9em;
9 | }
10 | .table-wrapper{
11 | font-family: var(--font-stack--monospace);
12 | font-size: var(--font-size--small--2);
13 | }
14 | .gp {
15 | color: darkorange;
16 | }
17 |
--------------------------------------------------------------------------------
/doc/api.rst:
--------------------------------------------------------------------------------
1 | API
2 | ===
3 |
4 | .. currentmodule:: cf_xarray
5 |
6 | Top-level API
7 | -------------
8 |
9 | .. autosummary::
10 | :toctree: generated/
11 |
12 | bounds_to_vertices
13 | vertices_to_bounds
14 | shapely_to_cf
15 | cf_to_shapely
16 | set_options
17 | encode_multi_index_as_compress
18 | decode_compress_to_multi_index
19 |
20 | Geometries
21 | ----------
22 | .. autosummary::
23 | :toctree: generated/
24 |
25 | geometry.decode_geometries
26 | geometry.encode_geometries
27 | geometry.shapely_to_cf
28 | geometry.cf_to_shapely
29 | geometry.GeometryNames
30 |
31 |
32 | Groupers
33 | --------
34 | .. autosummary::
35 | :toctree: generated/
36 |
37 | groupers.FlagGrouper
38 |
39 | .. currentmodule:: xarray
40 |
41 | DataArray
42 | ---------
43 |
44 | .. _daattr:
45 |
46 | Attributes
47 | ~~~~~~~~~~
48 |
49 | .. autosummary::
50 | :toctree: generated/
51 | :template: autosummary/accessor_attribute.rst
52 |
53 | DataArray.cf.axes
54 | DataArray.cf.cell_measures
55 | DataArray.cf.cf_roles
56 | DataArray.cf.coordinates
57 | DataArray.cf.formula_terms
58 | DataArray.cf.grid_mapping_name
59 | DataArray.cf.is_flag_variable
60 | DataArray.cf.standard_names
61 | DataArray.cf.plot
62 |
63 |
64 | .. _dameth:
65 |
66 | Methods
67 | ~~~~~~~
68 |
69 | .. autosummary::
70 | :toctree: generated/
71 | :template: autosummary/accessor_method.rst
72 |
73 | DataArray.cf.__getitem__
74 | DataArray.cf.__repr__
75 | DataArray.cf.add_canonical_attributes
76 | DataArray.cf.differentiate
77 | DataArray.cf.guess_coord_axis
78 | DataArray.cf.keys
79 | DataArray.cf.rename_like
80 |
81 | Flag Variables
82 | ++++++++++++++
83 |
84 | cf_xarray supports rich comparisons for `CF flag variables`_. Flag masks are not yet supported.
85 |
86 | .. autosummary::
87 | :toctree: generated/
88 | :template: autosummary/accessor_method.rst
89 |
90 | DataArray.cf.__lt__
91 | DataArray.cf.__le__
92 | DataArray.cf.__eq__
93 | DataArray.cf.__ne__
94 | DataArray.cf.__ge__
95 | DataArray.cf.__gt__
96 | DataArray.cf.isin
97 |
98 |
99 | Dataset
100 | -------
101 |
102 | .. _dsattr:
103 |
104 | Attributes
105 | ~~~~~~~~~~
106 |
107 | .. autosummary::
108 | :toctree: generated/
109 | :template: autosummary/accessor_attribute.rst
110 |
111 | Dataset.cf.axes
112 | Dataset.cf.bounds
113 | Dataset.cf.cell_measures
114 | Dataset.cf.cf_roles
115 | Dataset.cf.coordinates
116 | Dataset.cf.formula_terms
117 | Dataset.cf.grid_mapping_names
118 | Dataset.cf.geometries
119 | Dataset.cf.standard_names
120 |
121 | .. _dsmeth:
122 |
123 | Methods
124 | ~~~~~~~
125 |
126 | .. autosummary::
127 | :toctree: generated/
128 | :template: autosummary/accessor_method.rst
129 |
130 | Dataset.cf.__getitem__
131 | Dataset.cf.__repr__
132 | Dataset.cf.add_bounds
133 | Dataset.cf.add_canonical_attributes
134 | Dataset.cf.bounds_to_vertices
135 | Dataset.cf.decode_vertical_coords
136 | Dataset.cf.differentiate
137 | Dataset.cf.get_bounds
138 | Dataset.cf.get_bounds_dim_name
139 | Dataset.cf.guess_coord_axis
140 | Dataset.cf.keys
141 | Dataset.cf.rename_like
142 |
143 |
144 | .. _`CF flag variables`: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags
145 |
--------------------------------------------------------------------------------
/doc/bounds.md:
--------------------------------------------------------------------------------
1 | ```{eval-rst}
2 | .. currentmodule:: xarray
3 | ```
4 |
5 | # Bounds Variables
6 |
7 | ```{seealso}
8 | 1. [CF conventions on coordinate bounds](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#cell-boundaries)
9 | 1. {py:attr}`Dataset.cf.bounds`,
10 | 1. {py:func}`Dataset.cf.add_bounds`,
11 | 1. {py:func}`cf_xarray.bounds_to_vertices`,
12 | 1. {py:func}`cf_xarray.vertices_to_bounds`
13 | ```
14 |
15 | `cf_xarray` supports parsing [coordinate bounds](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#cell-boundaries) as encoded in the CF `bounds` attribute. A useful feature for incomplete dataset is also the automatic bounds estimation possible through `cf.add_bounds`. This method will estimate the missing bounds by finding the middle points between elements of the given coordinate, but also by extrapolating to find the outer bounds of the grid. This linear estimation works well with rectilinear grids, but it is only a coarse approximation for curvilinear and simple irregular grids.
16 |
17 | As an example, we present a "rotated pole" grid. It is defined on a rotated rectilinear grid which uses the `rlat` and `rlon` 1D coordinates, over North America at a resolution of 0.44°. The datasets comes with 2D `lat` and `lon` coordinates. `cf_xarray` will estimate the bounds by linear interpolation (extrapolation at the edges) of the existing `lon` and `lat`, which yields good results on parts of the grid where the rotation is small. However the errors is larger in other places, as seen when visualizing the distance in degrees between the estimated bounds and the true bounds.
18 |
19 | 
20 |
21 | For grids with a strong curvature between the cartesian axes and the lat/lon coordinates, the basic linear interpolation done for each point individually can yield grid cells with unmatching corners. The next figure shows such a case as it would be expected that the 4 corners within the red circle would all be the same point. To circumvent this issue, `cf_xarray` will average together these 4 different results, as shown on the last figure.
22 |
23 | 
24 | 
25 |
26 | This last examples illustrates again that `cf_xarray` can only estimate the grid bounds, grid metrics provided by the data producer will always be better.
27 |
--------------------------------------------------------------------------------
/doc/cartopy_rotated_pole.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xarray-contrib/cf-xarray/4be96ff2e40e7a7fc7ff6d57744c4a42b6cbaa94/doc/cartopy_rotated_pole.png
--------------------------------------------------------------------------------
/doc/coding.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: cf_xarray
12 | ```
13 |
14 | ```{code-cell}
15 | ---
16 | tags: [remove-cell]
17 | ---
18 | import cf_xarray as cfxr
19 | import numpy as np
20 | import pandas as pd
21 | import xarray as xr
22 | xr.set_options(display_expand_data=False)
23 | ```
24 |
25 | # Encoding and decoding
26 |
27 | `cf_xarray` aims to support encoding and decoding variables using CF conventions not yet implemented by Xarray.
28 |
29 | ## Geometries
30 |
31 | See [](./geometry.md) for more.
32 |
33 | ## Compression by gathering
34 |
35 | The ["compression by gathering"](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#compression-by-gathering)
36 | convention could be used for either {py:class}`pandas.MultiIndex` objects or `pydata/sparse` arrays.
37 |
38 | ### MultiIndex
39 |
40 | `cf_xarray` provides {py:func}`encode_multi_index_as_compress` and {py:func}`decode_compress_to_multi_index` to encode MultiIndex-ed
41 | dimensions using "compression by gethering".
42 |
43 | Here's a test dataset
44 |
45 | ```{code-cell}
46 | ds = xr.Dataset(
47 | {"landsoilt": ("landpoint", np.random.randn(4), {"foo": "bar"})},
48 | {
49 | "landpoint": pd.MultiIndex.from_product(
50 | [["a", "b"], [1, 2]], names=("lat", "lon")
51 | )
52 | },
53 | )
54 | ds
55 | ```
56 |
57 | First encode (note the `"compress"` attribute on the `landpoint` variable)
58 |
59 | ```{code-cell}
60 | encoded = cfxr.encode_multi_index_as_compress(ds, "landpoint")
61 | encoded
62 | ```
63 |
64 | At this point, we can write `encoded` to a CF-compliant dataset using {py:func}`xarray.Dataset.to_netcdf` for example.
65 | After reading that file, decode using
66 |
67 | ```{code-cell}
68 | decoded = cfxr.decode_compress_to_multi_index(encoded, "landpoint")
69 | decoded
70 | ```
71 |
72 | We roundtrip perfectly
73 |
74 | ```{code-cell}
75 | ds.identical(decoded)
76 | ```
77 |
78 | ### Sparse arrays
79 |
80 | This is unsupported currently but a pull request is welcome!
81 |
--------------------------------------------------------------------------------
/doc/conf.py:
--------------------------------------------------------------------------------
1 | # complexity documentation build configuration file, created by
2 | # sphinx-quickstart on Tue Jul 9 22:26:36 2013.
3 | #
4 | # This file is execfile()d with the current directory set to its containing dir.
5 | #
6 | # Note that not all possible configuration values are present in this
7 | # autogenerated file.
8 | #
9 | # All configuration values have a default; values that are commented out
10 | # serve to show the default.
11 |
12 | import datetime
13 | import os
14 | import sys
15 |
16 | import sphinx_autosummary_accessors
17 |
18 | import cf_xarray # noqa
19 | from cf_xarray.scripts import make_doc
20 |
21 | make_doc.main()
22 |
23 | # If extensions (or modules to document with autodoc) are in another directory,
24 | # add these directories to sys.path here. If the directory is relative to the
25 | # documentation root, use os.path.abspath to make it absolute, like shown here.
26 | # sys.path.insert(0, os.path.abspath('.'))
27 |
28 | cwd = os.getcwd()
29 | parent = os.path.dirname(cwd)
30 | sys.path.insert(0, parent)
31 |
32 |
33 | # -- General configuration -----------------------------------------------------
34 |
35 | # If your documentation needs a minimal Sphinx version, state it here.
36 | # needs_sphinx = "6.0"
37 |
38 | # Add any Sphinx extension module names here, as strings. They can be extensions
39 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
40 | extensions = [
41 | "sphinx.ext.autodoc",
42 | "sphinx.ext.viewcode",
43 | "sphinx.ext.autosummary",
44 | "sphinx.ext.doctest",
45 | "sphinx.ext.intersphinx",
46 | "sphinx.ext.extlinks",
47 | "numpydoc",
48 | "sphinx_autosummary_accessors",
49 | "IPython.sphinxext.ipython_directive",
50 | "myst_nb",
51 | "sphinx_copybutton",
52 | ]
53 |
54 | extlinks = {
55 | "issue": ("https://github.com/xarray-contrib/cf-xarray/issues/%s", "GH#%s"),
56 | "pr": ("https://github.com/xarray-contrib/cf-xarray/pull/%s", "GH#%s"),
57 | }
58 |
59 | # Add any paths that contain templates here, relative to this directory.
60 | templates_path = ["_templates", sphinx_autosummary_accessors.templates_path]
61 |
62 | # The suffix of source filenames.
63 | source_suffix = ".rst"
64 |
65 | # Enable notebook execution
66 | # https://nbsphinx.readthedocs.io/en/0.4.2/never-execute.html
67 | # nbsphinx_execute = 'auto'
68 | # Allow errors in all notebooks by
69 | # nbsphinx_allow_errors = True
70 |
71 | # Disable cell timeout
72 | nbsphinx_timeout = -1
73 |
74 | nbsphinx_prolog = """
75 | {% set docname = env.doc2path(env.docname, base=None) %}
76 |
77 | You can run this notebook in a `live session `_ |Binder| or view it `on Github `_.
79 |
80 | .. |Binder| image:: https://mybinder.org/badge_logo.svg
81 | :target: https://binder.pangeo.io/v2/gh/xarray-contrib/cf-xarray/main?urlpath=lab/tree/doc/{{ docname }}
82 | """
83 |
84 | # The encoding of source files.
85 | # source_encoding = 'utf-8-sig'
86 |
87 | # The master toctree document.
88 | master_doc = "index"
89 |
90 | # General information about the project.
91 | project = "cf_xarray"
92 | current_year = datetime.datetime.now().year
93 | copyright = f"2020-{current_year}, cf_xarray Developers"
94 | author = "cf_xarray Developers"
95 | # The version info for the project you're documenting, acts as replacement for
96 | # |version| and |release|, also used in various other places throughout the
97 | # built documents.
98 | #
99 | # The short X.Y version.
100 | # version = cf_xarray.__version__.split("+")[0]
101 | # The full version, including alpha/beta/rc tags.
102 | # release = cf_xarray.__version__
103 |
104 | # The language for content autogenerated by Sphinx. Refer to documentation
105 | # for a list of supported languages.
106 | # language = None
107 |
108 | # There are two options for replacing |today|: either, you set today to some
109 | # non-false value, then it is used:
110 | # today = ''
111 | # Else, today_fmt is used as the format for a strftime call.
112 | # today_fmt = '%B %d, %Y'
113 |
114 | # List of patterns, relative to source directory, that match files and
115 | # directories to ignore when looking for source files.
116 | exclude_patterns = ["_build"]
117 |
118 | # The reST default role (used for this markup: `text`) to use for all documents.
119 | # default_role = None
120 |
121 | # If true, '()' will be appended to :func: etc. cross-reference text.
122 | # add_function_parentheses = True
123 |
124 | # If true, the current module name will be prepended to all description
125 | # unit titles (such as .. function::).
126 | # add_module_names = True
127 |
128 | # If true, sectionauthor and moduleauthor directives will be shown in the
129 | # output. They are ignored by default.
130 | # show_authors = False
131 |
132 | # The name of the Pygments (syntax highlighting) style to use.
133 | pygments_style = "igor"
134 |
135 | # A list of ignored prefixes for module index sorting.
136 | # modindex_common_prefix = []
137 |
138 | # If true, keep warnings as "system message" paragraphs in the built documents.
139 | # keep_warnings = False
140 |
141 |
142 | # -- Options for HTML output ---------------------------------------------------
143 |
144 | # The theme to use for HTML and HTML Help pages. See the documentation for
145 | # a list of builtin themes.
146 | html_theme = "furo"
147 | # html_theme = "pydata_sphinx_theme"
148 |
149 | # Theme options are theme-specific and customize the look and feel of a theme
150 | # further. For a list of options available for each theme, see the
151 | # documentation.
152 | css_vars = {
153 | "admonition-font-size": "0.9rem",
154 | "font-size--small": "92%",
155 | "font-size--small--2": "87.5%",
156 | }
157 | html_theme_options = dict(
158 | sidebar_hide_name=True,
159 | light_css_variables=css_vars,
160 | dark_css_variables=css_vars,
161 | )
162 |
163 | html_context = {
164 | "github_user": "xarray-contrib",
165 | "github_repo": "cf-xarray",
166 | "github_version": "main",
167 | "doc_path": "doc",
168 | }
169 |
170 | # Add any paths that contain custom themes here, relative to this directory.
171 | # html_theme_path = []
172 |
173 | # The name for this set of Sphinx documents. If None, it defaults to
174 | # " v documentation".
175 | # html_title = None
176 |
177 | # A shorter title for the navigation bar. Default is the same as html_title.
178 | # html_short_title = None
179 |
180 | # The name of an image file (relative to this directory) to place at the top
181 | # of the sidebar.
182 | html_logo = "_static/logo.png"
183 |
184 | # The name of an image file (within the static path) to use as favicon of the
185 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
186 | # pixels large.
187 | # html_favicon = None
188 |
189 | # Add any paths that contain custom static files (such as style sheets) here,
190 | # relative to this directory. They are copied after the builtin static files,
191 | # so a file named "default.css" will overwrite the builtin "default.css".
192 | html_static_path = ["_static"]
193 | html_css_files = ["style.css"]
194 |
195 |
196 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
197 | # using the given strftime format.
198 | # html_last_updated_fmt = '%b %d, %Y'
199 |
200 | # If true, SmartyPants will be used to convert quotes and dashes to
201 | # typographically correct entities.
202 | # html_use_smartypants = True
203 |
204 | # Custom sidebar templates, maps document names to template names.
205 | # html_sidebars = {}
206 |
207 | # Additional templates that should be rendered to pages, maps page names to
208 | # template names.
209 | # html_additional_pages = {}
210 |
211 | # If false, no module index is generated.
212 | # html_domain_indices = True
213 |
214 | # If false, no index is generated.
215 | # html_use_index = True
216 |
217 | # If true, the index is split into individual pages for each letter.
218 | # html_split_index = False
219 |
220 | # If true, links to the reST sources are added to the pages.
221 | # html_show_sourcelink = True
222 |
223 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
224 | # html_show_sphinx = True
225 |
226 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
227 | # html_show_copyright = True
228 |
229 | # If true, an OpenSearch description file will be output, and all pages will
230 | # contain a tag referring to it. The value of this option must be the
231 | # base URL from which the finished HTML is served.
232 | # html_use_opensearch = ''
233 |
234 | # This is the file name suffix for HTML files (e.g. ".xhtml").
235 | # html_file_suffix = None
236 |
237 | # Output file base name for HTML help builder.
238 | htmlhelp_basename = "cf_xarraydoc"
239 |
240 | # -- Options for manual page output --------------------------------------------
241 |
242 | # One entry per manual page. List of tuples
243 | # (source start file, name, description, authors, manual section).
244 | man_pages = [("index", "cf_xarray", "cf_xarray Documentation", [author], 1)]
245 |
246 | # If true, show URL addresses after external links.
247 | # man_show_urls = False
248 |
249 | intersphinx_mapping = {
250 | "python": ("https://docs.python.org/3/", None),
251 | "xarray": ("https://xarray.pydata.org/en/stable/", None),
252 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None),
253 | }
254 |
255 | autosummary_generate = True
256 |
257 | autodoc_typehints = "description"
258 | autodoc_typehints_description_target = "documented"
259 | autodoc_default_options = {
260 | "members": True,
261 | "undoc-members": True,
262 | "private-members": True,
263 | }
264 | napoleon_use_param = True
265 | napoleon_use_rtype = True
266 |
267 | numpydoc_show_class_members = False
268 | # Report warnings for all validation checks except the ones listed after "all"
269 | numpydoc_validation_checks = {"all", "ES01", "EX01", "SA01", "SA04"}
270 | # don't report on objects that match any of these regex
271 | numpydoc_validation_exclude = {
272 | "cf_xarray.accessor.",
273 | r"\.__repr__$",
274 | "cf_xarray.set_options.",
275 | }
276 |
--------------------------------------------------------------------------------
/doc/contributing.rst:
--------------------------------------------------------------------------------
1 | .. currentmodule:: cf_xarray
2 |
3 | .. ipython:: python
4 | :suppress:
5 |
6 | import cf_xarray.accessor
7 |
8 |
9 | .. _contribut:
10 |
11 | Contributing
12 | ------------
13 |
14 | This section will be expanded later. For now it lists docstrings for a number of internal variables, classes and functions.
15 |
16 | Variables
17 | ~~~~~~~~~
18 |
19 | .. autodata:: cf_xarray.accessor._AXIS_NAMES
20 | .. autodata:: cf_xarray.accessor._CELL_MEASURES
21 | .. autodata:: cf_xarray.accessor._COORD_NAMES
22 | .. autodata:: cf_xarray.accessor._WRAPPED_CLASSES
23 |
24 |
25 | Attribute parsing
26 | +++++++++++++++++
27 |
28 | This dictionary contains criteria for identifying axis and coords using CF attributes. It was copied from MetPy
29 |
30 | .. autosummary::
31 | :toctree: generated/
32 |
33 | ~accessor.coordinate_criteria
34 |
35 | .. csv-table::
36 | :file: _build/csv/all_criteria.csv
37 | :header-rows: 1
38 | :stub-columns: 1
39 |
40 | Classes
41 | ~~~~~~~
42 |
43 | .. autosummary::
44 | :toctree: generated/
45 | :template: autosummary/accessor_class.rst
46 |
47 |
48 | ~accessor.CFAccessor
49 | ~accessor._CFWrappedClass
50 | ~accessor._CFWrappedPlotMethods
51 |
52 | Functions
53 | ~~~~~~~~~
54 |
55 |
56 | Primarily for developer reference. Some of these could become public API if necessary.
57 |
58 | .. autosummary::
59 | :toctree: generated/
60 |
61 | ~accessor._getattr
62 | ~accessor._getitem
63 | ~accessor._get_all
64 | ~accessor._get_axis_coord
65 | ~accessor._get_bounds
66 | ~accessor._get_coords
67 | ~accessor._get_custom_criteria
68 | ~accessor._get_dims
69 | ~accessor._get_groupby_time_accessor
70 | ~accessor._get_indexes
71 | ~accessor._get_measure
72 | ~accessor._get_with_standard_name
73 |
--------------------------------------------------------------------------------
/doc/coord_axes.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: xarray
12 | ```
13 |
14 | ```{code-cell}
15 | ---
16 | tags: [remove-cell]
17 | ---
18 | import xarray as xr
19 | xr.set_options(display_expand_data=False)
20 | ```
21 |
22 | # Axes and Coordinates
23 |
24 | One powerful feature of `cf_xarray` is the ability to refer to named dimensions by standard `axis` or `coordinate` names in Dataset or DataArray methods.
25 |
26 | For example, one can call `ds.cf.mean("latitude")` instead of `ds.mean("lat")`
27 |
28 | ```{code-cell}
29 | from cf_xarray.datasets import airds
30 |
31 | # identical to airds.mean("lat")
32 | airds.cf.mean("latitude")
33 | ```
34 |
35 | ```{tip}
36 | Most xarray methods are wrapped by cf-xarray. Simply access them as `DataArray.cf.method(dim="latitude")` for example and try it! If something does not work, please raise an issue.
37 | ```
38 |
39 | (coordinate-criteria)=
40 |
41 | ## Coordinate Criteria
42 |
43 | How does this work? `cf_xarray` has an internal table of criteria (mostly copied from MetPy) that lets it identify specific coordinate names `"latitude", "longitude", "vertical", "time"`.
44 |
45 | ```{tip}
46 | See {ref}`custom_criteria` to find out how to define your own custom criteria.
47 | ```
48 |
49 | This table lists these internal criteria
50 |
51 | ```{eval-rst}
52 | .. csv-table::
53 | :file: _build/csv/coords_criteria.csv
54 | :header-rows: 1
55 | :stub-columns: 1
56 | ```
57 |
58 | Any DataArray that has `standard_name: "latitude"` or `_CoordinateAxisType: "Lat"` or `"units": "degrees_north"` in its `attrs` will be identified as the `"latitude"` variable by cf-xarray. Similarly for other coordinate names.
59 |
60 | ## Axis Names
61 |
62 | Similar criteria exist for the concept of "axes".
63 |
64 | ```{eval-rst}
65 | .. csv-table::
66 | :file: _build/csv/axes_criteria.csv
67 | :header-rows: 1
68 | :stub-columns: 1
69 | ```
70 |
71 | ## `.axes` and `.coordinates` properties
72 |
73 | Alternatively use the special properties {py:attr}`DataArray.cf.axes` or {py:attr}`DataArray.cf.coordinates` to access the variable names. These properties return dictionaries that map "CF names" to a list of variable names. Note that a list is always returned even if only one variable name matches the name `"latitude"` (for example).
74 |
75 | ```{code-cell}
76 | airds.cf.axes
77 | ```
78 |
79 | ```{code-cell}
80 | airds.cf.coordinates
81 | ```
82 |
83 | ## Axes or Coordinate?
84 |
85 | TODO describe latitude vs Y; longitude vs X; vertical vs Z
86 |
87 | ## Checking presence of axis or coordinate
88 |
89 | Note that a given "CF name" is only present if there is at least one variable that can be identified with that name. The `airds` dataset has no `"vertical"` coordinate or `"Z"` axis, so those keys are not present. So to check whether a `"vertical"` coordinate or `"Z"` axis is present, one can
90 |
91 | ```{code-cell}
92 | "Z" in airds.cf.axes
93 | ```
94 |
95 | ```{code-cell}
96 | "vertical" in airds.cf.coordinates
97 | ```
98 |
99 | Or one can check the dataset as a whole:
100 |
101 | ```{code-cell}
102 | "Z" in airds.cf
103 | ```
104 |
105 | ## Using the repr
106 |
107 | It is always useful to check the variables identified by cf-xarray using the `repr`
108 |
109 | ```{code-cell}
110 | airds.cf
111 | ```
112 |
--------------------------------------------------------------------------------
/doc/custom-criteria.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: cf_xarray
12 | ```
13 |
14 | ```{code-cell}
15 | ---
16 | tags: [remove-cell]
17 | ---
18 | import xarray as xr
19 | xr.set_options(display_expand_data=False)
20 | ```
21 |
22 | (custom_criteria)=
23 |
24 | # Custom Criteria
25 |
26 | Fundamentally, cf_xarray uses rules or "criteria" to interpret user input using the
27 | attributes of an Xarray object (`.attrs`). These criteria are simple dictionaries. For example, here are the criteria used for identifying a "latitude" variable:
28 |
29 | ```python
30 | coordinate_criteria = {
31 | "latitude": {
32 | "standard_name": ("latitude",),
33 | "units": (
34 | "degree_north",
35 | "degree_N",
36 | "degreeN",
37 | "degrees_north",
38 | "degrees_N",
39 | "degreesN",
40 | ),
41 | "_CoordinateAxisType": ("Lat",),
42 | },
43 | }
44 | ```
45 |
46 | This dictionary maps the user input (`"latitude"`) to another dictionary which in turn maps an attribute name to a tuple of acceptable values for that attribute. So any variable with either `standard_name: latitude` or `_CoordinateAxisType: Lat_` or any of the `unit`s listed above will match the user-input `"latitude"`.
47 |
48 | cf_xarray lets you provide your own custom criteria in addition to those built-in. Here's an example:
49 |
50 | ```{code-cell}
51 | import cf_xarray as cfxr
52 | import numpy as np
53 | import xarray as xr
54 |
55 | ds = xr.Dataset({
56 | "salt1": ("x", np.arange(10), {"standard_name": "sea_water_salinity"}),
57 | "salt2": ("x", np.arange(10), {"standard_name": "sea_water_practical_salinity"}),
58 | })
59 |
60 | # first define our criteria
61 | salt_criteria = {
62 | "sea_water_salinity": {
63 | "standard_name": "sea_water_salinity|sea_water_practical_salinity"
64 | }
65 | }
66 | ```
67 |
68 | Now we apply our custom criteria temporarily using {py:func}`set_options` as a context manager. The following sets `"sea_water_salinity"` as an alias for variables that have either `"sea_water_salinity"` or `"sea_water_practical_salinity"` (note the use of regular expressions as a value). Here's how that works in practice
69 |
70 | ```{code-cell}
71 | with cfxr.set_options(custom_criteria=salt_criteria):
72 | salty = ds.cf[["sea_water_salinity"]]
73 | salty
74 | ```
75 |
76 | Note that `salty` contains both `salt1` and `salt2`. Without setting these criteria, we would only get `salt1` by default
77 |
78 | ```{code-cell}
79 | ds.cf[["sea_water_salinity"]]
80 | ```
81 |
82 | We can also use {py:func}`set_options` to set the criteria globally.
83 |
84 | ```{code-cell}
85 | cfxr.set_options(custom_criteria=salt_criteria)
86 | ds.cf[["sea_water_salinity"]]
87 | ```
88 |
89 | Again we get back both `salt1` and `salt2`. To limit side effects of setting criteria globally, we recommend that you use `set_options` as a context manager.
90 |
91 | ```{tip}
92 | To reset your custom criteria use `cfxr.set_options(custom_criteria=())`
93 | ```
94 |
95 | You can also match on the variable name, though be careful!
96 |
97 | ```{code-cell}
98 | salt_criteria = {
99 | "salinity": {"name": "salt*"}
100 | }
101 | cfxr.set_options(custom_criteria=salt_criteria)
102 |
103 | ds.cf[["salinity"]]
104 | ```
105 |
106 | ## More complex matches with `regex`
107 |
108 | Here is an example of a more complicated custom criteria, which requires the package [`regex`](https://github.com/mrabarnett/mrab-regex) to be installed since a behavior (allowing global flags like "(?i)" for matching case insensitive) was recently deprecated in the `re` package. The custom criteria, called "vocab", matches – case insensitive – to the variable alias "sea_ice_u" a variable whose name includes "sea" and "ice" and "u" but not "qc" or "status", or "sea" and "ice" and "x" and "vel" but not "qc" or "status".
109 |
110 | ```{code-cell}
111 | import cf_xarray as cfxr
112 | import xarray as xr
113 |
114 | vocab = {"sea_ice_u": {"name": "(?i)^(?!.*(qc|status))(?=.*sea)(?=.*ice)(?=.*u)|(?i)^(?!.*(qc|status))(?=.*sea)(?=.*ice)(?=.*x)(?=.*vel)"}}
115 | ds = xr.Dataset()
116 | ds["sea_ice_velocity_x"] = [0,1,2]
117 |
118 | with cfxr.set_options(custom_criteria=vocab):
119 | seaiceu = ds.cf["sea_ice_u"]
120 | seaiceu
121 | ```
122 |
--------------------------------------------------------------------------------
/doc/dsg.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: xarray
12 | ```
13 |
14 | ```{code-cell}
15 | ---
16 | tags: [remove-cell]
17 | ---
18 | import cf_xarray
19 | import numpy as np
20 | import xarray as xr
21 | xr.set_options(display_expand_data=False)
22 | ```
23 |
24 | # Discrete Sampling Geometries
25 |
26 | ```{seealso}
27 | 1. [CF conventions on Discrete Sampling Geometries](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html#discrete-sampling-geometries)
28 | 1. {py:attr}`Dataset.cf.cf_roles`
29 | ```
30 |
31 | `cf_xarray` supports identifying variables by the [`cf_role` attribute](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html#discrete-sampling-geometries).
32 |
33 | ```{code-cell}
34 | ds = xr.Dataset(
35 | {"temp": ("x", np.arange(10))},
36 | coords={"cast": ("x", np.arange(10), {"cf_role": "profile_id"})}
37 | )
38 | ds.cf
39 | ```
40 |
41 | Access `"cast"` using it's `cf_role`
42 |
43 | ```{code-cell}
44 | ds.cf["profile_id"]
45 | ```
46 |
47 | Find all `cf_role` variables using {py:attr}`Dataset.cf.cf_roles` and {py:attr}`DataArray.cf.cf_roles`
48 |
49 | ```{code-cell}
50 | ds.cf.cf_roles
51 | ```
52 |
--------------------------------------------------------------------------------
/doc/faq.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions
2 |
3 | ## I find `.cf` repr hard to read!
4 |
5 | Install [rich](https://rich.readthedocs.io) and load the Jupyter extension for easier-to-read reprs.
6 |
7 | ```python
8 | %load_ext rich
9 |
10 | import cf_xarray
11 | import xarray as xr
12 |
13 | ds = xr.tutorial.open_dataset("air_temperature")
14 | ds.cf
15 | ```
16 |
17 | 
18 |
--------------------------------------------------------------------------------
/doc/flags.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: xarray
12 | ```
13 |
14 | (flags)=
15 |
16 | # Flag Variables
17 |
18 | ```{seealso}
19 | 1. [CF conventions on flag variables and masks](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags)
20 | ```
21 |
22 | `cf_xarray` has some support for [flag variables](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags), including flag masks.
23 |
24 | ## Flag Values
25 |
26 | ```{code-cell}
27 | import cf_xarray
28 | import xarray as xr
29 |
30 | da = xr.DataArray(
31 | [1, 2, 1, 1, 2, 2, 3, 3, 3, 3],
32 | dims=("time",),
33 | attrs={
34 | "flag_values": [1, 2, 3],
35 | "flag_meanings": "atlantic_ocean pacific_ocean indian_ocean",
36 | "standard_name": "region",
37 | },
38 | name="region",
39 | )
40 | da.cf
41 | ```
42 |
43 | Now you can perform meaningful boolean comparisons that take advantage of the `flag_meanings` attribute:
44 |
45 | ```{code-cell}
46 | # compare to da == 1
47 | da.cf == "atlantic_ocean"
48 | ```
49 |
50 | Similarly with membership tests using {py:meth}`DataArray.cf.isin`
51 |
52 | ```{code-cell}
53 | # compare to da.isin([2, 3])
54 | da.cf.isin(["indian_ocean", "pacific_ocean"])
55 | ```
56 |
57 | You can also check whether a DataArray has the appropriate attributes to be recognized as a flag variable using {py:meth}`DataArray.cf.is_flag_variable`
58 |
59 | ```{code-cell}
60 | da.cf.is_flag_variable
61 | ```
62 |
63 | ## GroupBy
64 |
65 | Flag variables, such as that above, are naturally used for GroupBy operations.
66 | cf-xarray provides a `FlagGrouper` that understands the `flag_meanings` and `flag_values` attributes.
67 |
68 | Let's load an example dataset where the `flag_var` array has the needed attributes.
69 |
70 | ```{code-cell}
71 | import cf_xarray as cfxr
72 | import numpy as np
73 |
74 | from cf_xarray.datasets import flag_excl
75 |
76 | ds = flag_excl.to_dataset().set_coords('flag_var')
77 | ds["foo"] = ("time", np.arange(8))
78 | ds.flag_var
79 | ```
80 |
81 | Now use the :py:class:`~cf_xarray.groupers.FlagGrouper` to group by this flag variable:
82 |
83 | ```{code-cell}
84 | from cf_xarray.groupers import FlagGrouper
85 |
86 | ds.groupby(flag_var=FlagGrouper()).mean()
87 | ```
88 |
89 | Note how the output coordinate has the values from `flag_meanings`!
90 |
91 | ```{seealso}
92 | See the Xarray docs on using [Grouper objects](https://docs.xarray.dev/en/stable/user-guide/groupby.html#grouper-objects).
93 | ```
94 |
95 | ## Flag Masks
96 |
97 | ```{warning}
98 | Interpreting flag masks is very lightly tested.
99 | Please double-check the results and open an issue
100 | or pull request to suggest improvements.
101 | ```
102 |
103 | Load an example dataset:
104 |
105 | ```{code-cell}
106 | from cf_xarray.datasets import flag_indep
107 |
108 | flag_indep
109 | ```
110 |
111 | ```{code-cell}
112 | flag_indep.cf
113 | ```
114 |
115 | ```{code-cell}
116 | flag_indep.cf == "flag_1"
117 | ```
118 |
119 | And `isin`
120 |
121 | ```{code-cell}
122 | flag_indep.cf.isin(["flag_1", "flag_3"])
123 | ```
124 |
125 | ## Combined masks and values
126 |
127 | ```{warning}
128 | Interpreting a mix of flag masks and flag values
129 | is very lightly tested. Please double-check the results
130 | and open an issue or pull request to suggest improvements.
131 | ```
132 |
133 | Load an example dataset:
134 |
135 | ```{code-cell}
136 | from cf_xarray.datasets import flag_mix
137 |
138 | flag_mix
139 | ```
140 |
141 | ```{code-cell}
142 | flag_mix.cf
143 | ```
144 |
145 | ```{code-cell}
146 | flag_mix.cf == 'flag_4'
147 | ```
148 |
--------------------------------------------------------------------------------
/doc/geometry.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: xarray
12 | ```
13 |
14 | # Geometries
15 |
16 | ```{seealso}
17 | 1. [The CF conventions on Geometries](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#geometries)
18 | 1. {py:attr}`Dataset.cf.geometries`
19 | ```
20 |
21 | ```{eval-rst}
22 | .. currentmodule:: cf_xarray
23 | ```
24 |
25 | First read an example dataset with CF-encoded geometries
26 |
27 | ```{code-cell}
28 | import cf_xarray as cfxr
29 | import cf_xarray.datasets
30 | import xarray as xr
31 |
32 | ds = cfxr.datasets.encoded_point_dataset()
33 | ds
34 | ```
35 |
36 | The {py:attr}`Dataset.cf.geometries` property will yield a mapping from geometry type to geometry container variable name.
37 |
38 | ```{code-cell}
39 | ds.cf.geometries
40 | ```
41 |
42 | The `"geometry"` name is special, and will return the geometry *container* present in the dataset
43 |
44 | ```{code-cell}
45 | ds.cf["geometry"]
46 | ```
47 |
48 | Request all variables needed to represent a geometry as a Dataset using the geometry type as key.
49 |
50 | ```{code-cell}
51 | ds.cf[["point"]]
52 | ```
53 |
54 | You *must* request a Dataset as return type, that is provide the list `["point]`, because the CF conventions encode geometries across multiple variables with dimensions that are not present on all variables. Xarray's data model does *not* allow representing such a collection of variables as a DataArray.
55 |
56 | ## Encoding & decoding
57 |
58 | `cf_xarray` can convert between vector geometries represented as shapely objects
59 | and CF-compliant array representations of those geometries.
60 |
61 | Let's start by creating an xarray object containing some shapely geometries. This example uses
62 | a `xr.DataArray` but these functions also work with a `xr.Dataset` where one of the data variables
63 | contains an array of shapes.
64 |
65 | ```{warning}
66 | `cf_xarray` does not support handle multiple types of shapes (Point, Line, Polygon) in one
67 | `xr.DataArray`, but multipart geometries are supported and can be mixed with single-part
68 | geometries of the same type.
69 | ```
70 |
71 | `cf-xarray` provides {py:func}`geometry.encode_geometries` and {py:func}`geometry.decode_geometries` to
72 | encode and decode xarray Datasets to/from a CF-compliant form that can be written to any array storage format.
73 |
74 | For example, here is a Dataset with shapely geometries
75 |
76 | ```{code-cell}
77 | ds = cfxr.datasets.point_dataset()
78 | ds
79 | ```
80 |
81 | Encode with the CF-conventions
82 |
83 | ```{code-cell}
84 | encoded = cfxr.geometry.encode_geometries(ds)
85 | encoded
86 | ```
87 |
88 | This dataset can then be written to any format supported by Xarray.
89 | To decode back to shapely geometries, reverse the process using {py:func}`geometry.decode_geometries`
90 |
91 | ```{code-cell}
92 | decoded = cfxr.geometry.decode_geometries(encoded)
93 | ds.identical(decoded)
94 | ```
95 |
96 | ### Limitations
97 |
98 | The following limitations can be relaxed in the future. PRs welcome!
99 |
100 | 1. cf-xarray uses `"geometry_container"` as the name for the geometry variable always. If there are multiple geometry variables then `"geometry_N"`is used where N is an integer >= 0. cf-xarray behaves similarly for all associated geometry variables names: i.e. `"node"`, `"node_count"`, `"part_node_count"`, `"part"`, `"interior_ring"`. `"x"`, `"y"` (with suffixes if needed) are always the node coordinate variable names, and `"crd_x"`, `"crd_y"` are the nominal X, Y coordinate locations. None of this is configurable at the moment.
101 | 1. CF xarray will not set the `"geometry"` attribute that links a variable to a geometry by default unless the geometry variable is a dimension coordinate for that variable. This heuristic works OK for vector data cubes (e.g. [xvec](https://xvec.readthedocs.io/en/stable/)). You should set the `"geometry"` attribute manually otherwise. Suggestions for better behaviour here are very welcome.
102 |
103 | ## Lower-level conversions
104 |
105 | Encoding a single DataArray is possible using {py:func}`geometry.shapely_to_cf`.
106 |
107 | ```{code-cell}
108 | da = ds["geometry"]
109 | ds_cf = cfxr.shapely_to_cf(da)
110 | ds_cf
111 | ```
112 |
113 | This function returns a `xr.Dataset` containing the CF fields needed to reconstruct the
114 | geometries. In particular there are:
115 |
116 | - `'x'`, `'y'` : the node coordinates
117 | - `'crd_x'`, `'crd_y'` : the feature coordinates (might have different names if `grid_mapping` is available).
118 | - `'node_count'` : The number of nodes per feature. Always present for Lines and Polygons. For
119 | Points: only present if there are multipart geometries.
120 | - `'part_node_count'` : The number of nodes per individual geometry. Only for Lines with multipart
121 | geometries and for Polygons with multipart geometries or holes.
122 | - `'interior_ring'` : Integer boolean indicating whether ring is interior or exterior. Only for
123 | Polygons with holes.
124 | - `'geometry_container`' : Empty variable with attributes describing the geometry type.
125 |
126 | Here are the attributes on `geometry_container`. This pattern mimics the convention of
127 | specifying spatial reference information in the attrs of the empty array `spatial_ref`.
128 |
129 | ```{code-cell}
130 | ds_cf.geometry_container.attrs
131 | ```
132 |
133 | ```{note}
134 | Z axis is not yet supported for any shapes.
135 | ```
136 |
137 | This `xr.Dataset` can be converted back into a `xr.DataArray` of shapely geometries:
138 |
139 | ```{code-cell}
140 | cfxr.cf_to_shapely(ds_cf)
141 | ```
142 |
143 | This conversion adds coordinates that aren't in the `xr.DataArray` that we started with.
144 | By default these are called `'crd_x'` and `'crd_y'` unless `grid_mapping` is specified.
145 |
146 | ## Gotchas
147 |
148 | For MultiPolygons with holes the CF notation is slightly ambiguous on which hole is associated
149 | with which polygon. This is problematic because shapely stores holes within the polygon
150 | object that they are associated with. `cf_xarray` assumes that the shapes are interleaved
151 | such that the holes (interior rings) are associated with the exteriors (exterior rings) that
152 | immediately precede them.
153 |
--------------------------------------------------------------------------------
/doc/grid_mappings.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: xarray
12 | ```
13 |
14 | # Grid Mappings
15 |
16 | ```{seealso}
17 | 1. [CF conventions on grid mappings and projections](https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#grid-mappings-and-projections)
18 | 1. {py:attr}`Dataset.cf.grid_mapping_names`
19 | 1. {py:attr}`DataArray.cf.grid_mapping_name`
20 | ```
21 |
22 | `cf_xarray` understands the concept of coordinate projections using the [grid_mapping](https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#grid-mappings-and-projections) attribute convention. For example, the dataset might contain two sets of coordinates:
23 |
24 | - native coordinates in which the data is defined, e.g., regular 1D coordinates
25 | - projected coordinates which probably denote some "real" coordinates in [latitude and longitude](https://en.wikipedia.org/wiki/Geographic_coordinate_system#Latitude_and_longitude)
26 |
27 | Due to the projection, those real coordinates are probably 2D data variables. The `grid_mapping` attribute of a data variable makes a connection to another data variable defining the coordinate reference system (CRS) of those native coordinates. It should enable you to project the native coordinates into any other CRS, including the real 2D latitude and longitude coordinates. This is often useful for plotting, e.g., you can [tell cartopy how to correctly plot coastlines](https://scitools.org.uk/cartopy/docs/latest/tutorials/understanding_transform.html) for the CRS your data is defined in.
28 |
29 | ## Extracting grid mapping info
30 |
31 | ### Dataset
32 |
33 | To access `grid_mapping` attributes, consider this example:
34 |
35 | ```{code-cell}
36 | from cf_xarray.datasets import rotds
37 |
38 | rotds
39 | ```
40 |
41 | The related grid mappings can be discovered using `Dataset.cf.grid_mapping_names` which maps a
42 | ["grid mapping name"](http://cfconventions.org/cf-conventions/cf-conventions.html#appendix-grid-mappings) to the
43 | appropriate variable name:
44 |
45 | ```{code-cell}
46 | rotds.cf.grid_mapping_names
47 | ```
48 |
49 | Access the `grid_mapping` variable as
50 |
51 | ```{code-cell}
52 | rotds.cf["grid_mapping"]
53 | ```
54 |
55 | ### DataArrays
56 |
57 | Grid mapping variables are propagated when extracting DataArrays:
58 |
59 | ```{code-cell}
60 | da = rotds.cf["temp"]
61 | da
62 | ```
63 |
64 | To find the grid mapping name use the singular {py:attr}`DataArray.cf.grid_mapping_name`
65 |
66 | ```{code-cell}
67 | da.cf.grid_mapping_name
68 | ```
69 |
70 | And to get the grid mapping variable
71 |
72 | ```{code-cell}
73 | da.cf["grid_mapping"]
74 | ```
75 |
76 | ## Use `grid_mapping` in projections
77 |
78 | The grid mapping information use very useful in projections, e.g., for plotting. [pyproj](https://pyproj4.github.io/pyproj/stable/api/crs/crs.html#pyproj.crs.CRS.from_cf) understands CF conventions right away, e.g.
79 |
80 | ```python
81 | from pyproj import CRS
82 |
83 | CRS.from_cf(rotds.cf["grid_mapping"].attrs)
84 | ```
85 |
86 | gives you more details on the projection:
87 |
88 | ```
89 |
90 | Name: undefined
91 | Axis Info [ellipsoidal]:
92 | - lon[east]: Longitude (degree)
93 | - lat[north]: Latitude (degree)
94 | Area of Use:
95 | - undefined
96 | Coordinate Operation:
97 | - name: Pole rotation (netCDF CF convention)
98 | - method: Pole rotation (netCDF CF convention)
99 | Datum: World Geodetic System 1984
100 | - Ellipsoid: WGS 84
101 | - Prime Meridian: Greenwich
102 | ```
103 |
104 | For use in cartopy, there is some more overhead due to [this issue](https://github.com/SciTools/cartopy/issues/2099). So you should select the right cartopy CRS and just feed in the grid mapping info:
105 |
106 | ```python
107 | from cartopy import crs as ccrs
108 |
109 | grid_mapping = rotds.cf["grid_mapping"]
110 | pole_latitude = grid_mapping.grid_north_pole_latitude
111 | pole_longitude = grid_mapping.grid_north_pole_longitude
112 |
113 | ccrs.RotatedPole(pole_longitude, pole_latitude)
114 | ```
115 |
116 | 
117 |
--------------------------------------------------------------------------------
/doc/howtouse.md:
--------------------------------------------------------------------------------
1 | ---
2 | hide-toc: true
3 | ---
4 |
5 | ```{eval-rst}
6 | .. currentmodule:: xarray
7 | ```
8 |
9 | # How to use cf_xarray
10 |
11 | There are four ways one can use cf_xarray.
12 |
13 | ## Use CF standard names
14 |
15 | Use "CF names" like `standard_name`, coordinates like `"latitude"`, axes like `"X"` instead of actual variable names. For e.g.
16 |
17 | ## Extract actual variable names
18 |
19 | Use `cf_xarray` to extract the appropriate variable name through the properties:
20 |
21 | ## Rename to a custom vocabulary
22 |
23 | Use {py:meth}`Dataset.rename`, or {py:meth}`Dataset.cf.rename_like` to rename variables to your preferences
24 |
25 | ## Define custom criteria for a custom vocabulary
26 |
27 | Define custom criteria to avoid explicity renaming but still work with your datasets using a custom vocabulary.
28 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | |
2 | |
3 |
4 | .. image:: _static/full-logo.png
5 | :align: center
6 | :width: 70%
7 |
8 | |
9 | |
10 |
11 | Welcome to ``cf_xarray``
12 | ========================
13 |
14 | ``cf_xarray`` mainly provides an accessor (``DataArray.cf`` or ``Dataset.cf``) that allows
15 | you to interpret `Climate and Forecast metadata convention `_ attributes present on `xarray `_ objects.
16 |
17 | Installing
18 | ----------
19 |
20 | ``cf_xarray`` can be installed using ``pip``
21 |
22 | >>> pip install cf_xarray
23 |
24 |
25 | or using ``conda``
26 |
27 | >>> conda install -c conda-forge cf_xarray
28 |
29 |
30 | .. toctree::
31 | :maxdepth: 2
32 | :hidden:
33 | :caption: In-depth Examples
34 |
35 | examples/introduction
36 | EarthCube 2021 demo
37 | CESM ocean model demo
38 | COSIMA ocean-sea ice model demo
39 |
40 | .. toctree::
41 | :maxdepth: 2
42 | :hidden:
43 | :caption: User Guide
44 |
45 | quickstart
46 | howtouse
47 | faq
48 | coord_axes
49 | selecting
50 | flags
51 | units
52 | parametricz
53 | bounds
54 | grid_mappings
55 | coding
56 | dsg
57 | sgrid_ugrid
58 | geometry
59 | plotting
60 | custom-criteria
61 | provenance
62 | API Reference
63 |
64 | .. toctree::
65 | :maxdepth: 2
66 | :hidden:
67 | :caption: For contributors
68 |
69 | Contributing Guide
70 | Development Roadmap
71 | Whats New
72 | GitHub repository
73 |
--------------------------------------------------------------------------------
/doc/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/doc/parametricz.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | hide-toc: true
9 | ---
10 |
11 | ```{eval-rst}
12 | .. currentmodule:: xarray
13 | ```
14 |
15 | ```{code-cell}
16 | ---
17 | tags: [remove-cell]
18 | ---
19 | import xarray as xr
20 | xr.set_options(display_expand_data=False)
21 | ```
22 |
23 | # Parametric Vertical Coordinates
24 |
25 | ```{seealso}
26 | 1. [CF conventions on parametric vertical coordinates](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#parametric-vertical-coordinate)
27 | 2. {py:meth}`Dataset.cf.decode_vertical_coords`
28 | 3. {py:attr}`Dataset.cf.formula_terms`
29 | ```
30 |
31 | `cf_xarray` supports decoding [parametric vertical coordinates](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#parametric-vertical-coordinate) encoded in the `formula_terms` attribute using {py:meth}`Dataset.cf.decode_vertical_coords`.
32 |
33 | ## Decoding parametric coordinates
34 |
35 | ```{code-cell}
36 | from cf_xarray.datasets import romsds
37 |
38 | romsds
39 | ```
40 |
41 | Now we decode the vertical coordinates **in-place**. Note the new `z_rho` variable. `cf_xarray` sees that `s_rho` has a `formula_terms` attribute, looks up the right formula using `s_rho.attrs["standard_name"]` and computes a new vertical coordinate variable.
42 |
43 | ```{code-cell}
44 | romsds.cf.decode_vertical_coords(outnames={'s_rho': 'z_rho'}) # adds new z_rho variable
45 | romsds.z_rho
46 | ```
47 |
48 | ## Formula terms
49 |
50 | To see whether decoding is possible, use the {py:attr}`Dataset.cf.formula_terms` attribute
51 |
52 | ```{code-cell}
53 | romsds.cf.formula_terms
54 | ```
55 |
--------------------------------------------------------------------------------
/doc/plotting.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: xarray
12 | ```
13 |
14 | ```{code-cell}
15 | ---
16 | tags: [remove-cell]
17 | ---
18 | import xarray as xr
19 | import matplotlib as mpl
20 | mpl.rcParams["figure.dpi"] = 120
21 | mpl.rcParams["font.size"] = 9
22 | xr.set_options(display_expand_data=False)
23 | ```
24 |
25 | # Plotting
26 |
27 | Plotting is where `cf_xarray` really shines in our biased opinion.
28 |
29 | ```{code-cell}
30 | from cf_xarray.datasets import airds
31 |
32 | air = airds.air
33 | air.cf
34 | ```
35 |
36 | ```{tip}
37 | Only ``DataArray.plot`` is currently supported.
38 | ```
39 |
40 | ## Using CF standard names
41 |
42 | Note the use of `"latitude"` and `"longitude"` (or `"X"` and `"Y"`) in the following as a "standard" substitute for the dataset-specific `"lat"` and `"lon"` variables.
43 |
44 | ```{code-cell}
45 | air.isel(time=0).cf.plot(x="X", y="Y")
46 | ```
47 |
48 | ```{code-cell}
49 | air.cf.isel(T=1, Y=[0, 1, 2]).cf.plot(x="longitude", hue="latitude")
50 | ```
51 |
52 | ```{code-cell}
53 | air.cf.plot(x="longitude", y="latitude", col="T")
54 | ```
55 |
56 | ## Automatic axis placement
57 |
58 | Now let's create a fake dataset representing a `(x,z)` cross-section of the ocean. The vertical coordinate here is "pressure" which increases downwards.
59 | We follow CF conventions and mark `pres` as `axis: Z, positive: "down"` to indicate these characeristics.
60 |
61 | ```{code-cell}
62 | import matplotlib as mpl
63 | import numpy as np
64 | import xarray as xr
65 |
66 | ds = xr.Dataset(
67 | coords={
68 | "pres": ("pres", np.arange(20), {"axis": "Z", "positive": "down"}),
69 | "x": ("x", np.arange(50), {"axis": "X"})
70 | }
71 | )
72 | ds["temp"] = 20 * xr.ones_like(ds.x) * np.exp(- ds.pres / 30)
73 | ds.temp.cf
74 | ```
75 |
76 | The default xarray plot has some deficiencies
77 |
78 | ```{code-cell}
79 | ds.temp.plot(cmap=mpl.cm.RdBu_r)
80 | ```
81 |
82 | cf_xarray can interpret attributes to make two decisions:
83 |
84 | 1. That `pres` should be the Y-Axis
85 | 1. Since `pres` increases downwards (`positive: "down"`), the axis should be reversed so that low pressure is at the top of the plot.
86 | Now we have a more physically meaningful figure where warmer water is at the top of the water column!
87 |
88 | ```{code-cell}
89 | ds.temp.cf.plot(cmap=mpl.cm.RdBu_r)
90 | ```
91 |
--------------------------------------------------------------------------------
/doc/provenance.md:
--------------------------------------------------------------------------------
1 | # History & provenance tracking
2 |
3 | `cf_xarray` will eventually provide functions that add a [`cell_methods` attribute](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#cell-methods), and a `history` attribute, that plug in to xarray's attribute tracking functionality.
4 |
5 | Progress is blocked by a few pull requests:
6 |
7 | 1. An [Xarray pull request](https://github.com/pydata/xarray/pull/5668) to pass "context" to a custom `keep_attrs` handler.
8 | 1. Two cf_xarray pull requests that leverage the above: [1](https://github.com/xarray-contrib/cf-xarray/pull/253), [2](https://github.com/xarray-contrib/cf-xarray/pull/259)
9 |
10 | ```{tip}
11 | If this capability is of interest, contributions toward finishing the above Pull Requests are very welcome.
12 | ```
13 |
--------------------------------------------------------------------------------
/doc/quickstart.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: xarray
12 | ```
13 |
14 | ```{code-cell}
15 | ---
16 | tags: [remove-cell]
17 | ---
18 | import xarray as xr
19 | xr.set_options(display_expand_data=False)
20 | ```
21 |
22 | # Quickstart
23 |
24 | `cf_xarray` allows you to write code that works on many datasets by interpreting CF-compliant attributes (`.attrs`) present on xarray {py:class}`~xarray.DataArray` or {py:class}`~xarray.Dataset` objects. First, let's load a dataset.
25 |
26 | ```{code-cell}
27 | import cf_xarray as cfxr
28 | import xarray as xr
29 |
30 | xr.set_options(keep_attrs=True)
31 |
32 | ds = xr.tutorial.open_dataset("air_temperature")
33 | ds
34 | ```
35 |
36 | ## Finding CF information
37 |
38 | `cf_xarray` registers an "accessor" named `cf` on import. For a quick overview of attributes that `cf_xarray` can interpret use `.cf` This will display the "repr" or a representation of all detected CF information.
39 |
40 | ```{code-cell}
41 | ds.cf
42 | ```
43 |
44 | The plain text repr can be a little hard to read. In a Jupyter environment simply install [`rich`](https://rich.readthedocs.io) and
45 | use the Jupyter extension with `%load_ext rich`. Then `ds.cf` will automatically use the `rich` representation.
46 | See [the rich docs](https://rich.readthedocs.io/en/stable/introduction.html#ipython-extension) for more.
47 |
48 | ```python
49 | %load_ext rich
50 |
51 | ds.cf
52 | ```
53 |
54 | 
55 |
56 | ## Using attributes
57 |
58 | Now instead of the usual xarray names on the right, you can use the "CF names" on the left.
59 |
60 | ```{code-cell}
61 | ds.cf.mean("latitude") # identical to ds.mean("lat")
62 | ```
63 |
64 | This works because the attributes `standard_name: "latitude"` and `units: "degrees_north"` are present on `ds.latitude`
65 |
66 | ```{code-cell}
67 | ds.lat.attrs
68 | ```
69 |
70 | ```{tip}
71 | For a list of criteria used to identify the "latitude" variable (for e.g.) see {ref}`coordinate-criteria`.
72 | ```
73 |
74 | Similarly we could use `ds.cf.mean("Y")` because the attribute `axis: "Y"` is present.
75 |
76 | ```{tip}
77 | For best results, we recommend you tell xarray to preserve attributes as much as possible using `xr.set_options(keep_attrs=True)`
78 | but be warned, this can preserve out-of-date metadata.
79 | ```
80 |
81 | ```{tip}
82 | Sometimes datasets don't have all the necessary attributes. Use {py:meth}`~xarray.Dataset.cf.guess_coord_axis`
83 | and {py:meth}`~xarray.Dataset.cf.add_canonical_attributes` to automatically add attributes to variables that match some heuristics.
84 | ```
85 |
86 | ## Indexing
87 |
88 | We can use these "CF names" to index into the dataset
89 |
90 | ```{code-cell}
91 | ds.cf["latitude"]
92 | ```
93 |
94 | This is particularly useful if a `standard_name` attribute is present. For demonstration purposes lets add one:
95 |
96 | ```{code-cell}
97 | ds.air.attrs["standard_name"] = "air_temperature"
98 | ds.cf["air_temperature"]
99 | ```
100 |
101 | ## Finding variable names
102 |
103 | Sometimes it is more useful to extract the actual variable names associated with a given "CF name". `cf_xarray` exposes these variable names under a few properties:
104 |
105 | - {py:attr}`Dataset.cf.axes`,
106 | - {py:attr}`Dataset.cf.bounds`,
107 | - {py:attr}`Dataset.cf.cell_measures`,
108 | - {py:attr}`Dataset.cf.cf_roles`,
109 | - {py:attr}`Dataset.cf.coordinates`,
110 | - {py:attr}`Dataset.cf.formula_terms`,
111 | - {py:attr}`Dataset.cf.grid_mapping_names`, and
112 | - {py:attr}`Dataset.cf.standard_names`.
113 |
114 | These properties all return dictionaries mapping a standard key name to a list of matching variable names in the Dataset or DataArray.
115 |
116 | ```{code-cell}
117 | ds.cf.axes
118 | ```
119 |
120 | ```{code-cell}
121 | ds.cf.coordinates
122 | ```
123 |
124 | ```{code-cell}
125 | ds.cf.standard_names
126 | ```
127 |
--------------------------------------------------------------------------------
/doc/roadmap.rst:
--------------------------------------------------------------------------------
1 | .. currentmodule:: cf_xarray
2 |
3 | Roadmap
4 | -------
5 |
6 | Goals
7 | =====
8 |
9 | 1. Enable easy use of additional CF attributes that are not decoded by xarray.
10 |
11 | 2. Provide a consolidated set of public helper functions that other packages can use to avoid
12 | duplicated efforts in parsing CF attributes.
13 |
14 | Scope
15 | =====
16 |
17 |
18 | 1. This package will not provide a full implementation of the CF data model using xarray objects.
19 | This use case should be served by Iris.
20 |
21 | 2. Unit support is left to ``pint-xarray`` and future xarray support for ``pint`` until it is clear
22 | that there is a need for some functionality.
23 |
24 | 3. Projections and CRS stuff is left to ``rioxarray`` and other geo-xarray packages. Some helper
25 | functions could be folded in to ``cf-xarray`` to encourage consolidation in that sub-ecosystem.
26 |
27 | Design principles
28 | =================
29 |
30 | 1. Be uncomplicated.
31 |
32 | Avoid anything that requires saving state in accessor objects (for now).
33 |
34 | 2. Be friendly.
35 |
36 | Users should be allowed to mix CF names and variables names from the parent xarray object e.g.
37 | ``ds.cf.plot(x="X", y="model_depth")``. This allows use with "imperfectly tagged" datasets.
38 |
39 | 3. Be loud when wrapping to avoid confusion.
40 |
41 | For e.g. the ``repr`` for ``cf.groupby("X")`` should make it clear that this is a
42 | CF-wrapped ``Resample`` instance i.e. ``cf.groupby("X").mean("T")`` is allowed. Docstrings
43 | should also clearly indicate wrapping by ``cf-xarray``; for e.g. ``ds.cf.isel``.
44 |
45 | 4. Allow easy debugging and help build understanding.
46 |
47 | Since ``cf_xarray`` depends on ``attrs`` being present and since ``attrs`` are easily lost in xarray
48 | operations, we should allow easy diagnosis of what ``cf_xarray`` can decode for a particular
49 | object.
50 |
--------------------------------------------------------------------------------
/doc/selecting.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | ```{eval-rst}
11 | .. currentmodule:: xarray
12 | ```
13 |
14 | ```{code-cell}
15 | ---
16 | tags: [remove-cell]
17 | ---
18 | import numpy as np
19 | import xarray as xr
20 | xr.set_options(display_expand_data=False)
21 | ```
22 |
23 | # Selecting DataArrays
24 |
25 | ```{seealso}
26 | CF conventions on
27 | 1. [coordinate variables](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#coordinate-types)
28 | 1. [cell bounds and measures](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#_data_representative_of_cells)
29 | 1. [standard name](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#standard-name)
30 | 1. [ancillary data](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#ancillary-data)
31 | ```
32 |
33 | A powerful feature of `cf_xarray` is the ability select DataArrays using special "CF names" like the "latitude", or "longitude" coordinate names, "X" or "Y" axes names, oreven using the `standard_name` attribute if present.
34 |
35 | To demonstrate this, let's load a few datasets
36 |
37 | ```{code-cell}
38 | from cf_xarray.datasets import airds, anc, multiple, popds as pop
39 | ```
40 |
41 | ## By axis and coordinate name
42 |
43 | Lets select the `"X"` axis on `airds`.
44 |
45 | ```{code-cell}
46 | # identical to airds["lon"]
47 | airds.cf["X"]
48 | ```
49 |
50 | This works because `airds.lon.attrs` contains `axis: "X"`
51 |
52 | ```{code-cell}
53 | airds.cf
54 | ```
55 |
56 | ## By standard name
57 |
58 | The variable `airds.air` has `standard_name: "air_temperature"`, so we can use that to pull it out:
59 |
60 | ```{code-cell}
61 | airds.cf["air_temperature"]
62 | ```
63 |
64 | ## By `cf_role`
65 |
66 | `cf_xarray` supports identifying variables by the [`cf_role` attribute](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html#discrete-sampling-geometries).
67 |
68 | ```{code-cell}
69 | ds = xr.Dataset(
70 | {"temp": ("x", np.arange(10))},
71 | coords={"cast": ("x", np.arange(10), {"cf_role": "profile_id"})}
72 | )
73 | ds.cf["profile_id"]
74 | ```
75 |
76 | ## Associated variables
77 |
78 | `.cf[key]` will return a DataArray or Dataset containing all variables associated with the `key` including ancillary variables and bounds variables (if possible).
79 |
80 | In the following, note that the "ancillary variables" `q_error_limit` and `q_detection_limit` were also returned
81 |
82 | ```{code-cell}
83 | anc.cf["specific_humidity"]
84 | ```
85 |
86 | even though they are "data variables" and not "coordinate variables" in the original Dataset.
87 |
88 | ```{code-cell}
89 | anc
90 | ```
91 |
92 | ## Selecting multiple variables
93 |
94 | Sometimes a Dataset may contain multiple `X` or multiple `longitude` variables. In that case a simple `.cf["X"]` will raise an error. Instead follow Xarray convention and pass a list `.cf[["X"]]` to receive a Dataset with all available `"X"` variables
95 |
96 | ```{code-cell}
97 | multiple.cf[["X"]]
98 | ```
99 |
100 | ```{code-cell}
101 | pop.cf[["longitude"]]
102 | ```
103 |
104 | ## Mixing names
105 |
106 | cf_xarray aims to be as friendly as possible, so it is possible to mix "CF names" and normal variable names. Here we select `UVEL` and `TEMP` by using the `standard_name` of `TEMP` (which is `sea_water_potential_temperature`)
107 |
108 | ```{code-cell}
109 | pop.cf[["sea_water_potential_temperature", "UVEL"]]
110 | ```
111 |
--------------------------------------------------------------------------------
/doc/sgrid_ugrid.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | ---
9 |
10 | # SGRID / UGRID
11 |
12 | ```{seealso}
13 | 1. [SGRID conventions](https://sgrid.github.io/sgrid/)
14 | 1. [UGRID conventions](http://ugrid-conventions.github.io/ugrid-conventions/)
15 | ```
16 |
17 | ## SGRID
18 |
19 | `cf_xarray` can parse the attributes on the `grid_topology` variable to identify dimension names with axes names `X`, `Y`, `Z`.
20 |
21 | Consider this representative dataset
22 |
23 | ```{code-cell}
24 | from cf_xarray.datasets import sgrid_roms
25 |
26 | sgrid_roms
27 | ```
28 |
29 | A new `SGRID` section is added to the repr:
30 |
31 | ```{code-cell}
32 | sgrid_roms.cf
33 | ```
34 |
35 | ### Topology variable
36 |
37 | `cf_xarray` supports identifying `grid_topology` using the `cf_role` attribute.
38 |
39 | ```{code-cell}
40 | sgrid_roms.cf["grid_topology"]
41 | ```
42 |
43 | ### Dimensions
44 |
45 | Let's look at the repr again:
46 |
47 | ```{code-cell}
48 | sgrid_roms.cf
49 | ```
50 |
51 | Note that `xi_u`, `eta_u` were identified as `X`, `Y` axes even though
52 | there is no data associated with them. So now the following will return `xi_u`
53 |
54 | ```{code-cell}
55 | sgrid_roms.cf["X"]
56 | ```
57 |
58 | ```{tip}
59 | The repr only shows variable names that can be used as `object[variable_name]`. That is why
60 | only `xi_u`, `eta_u` are listed in the repr even though the attributes on the `grid_topology`
61 | variable `grid` list many more dimension names.
62 | ```
63 |
64 | ## UGRID
65 |
66 | ### Topology variable
67 |
68 | `cf_xarray` supports identifying the `mesh_topology` variable using the `cf_role` attribute.
69 |
70 | ## More?
71 |
72 | Further support for interpreting the SGRID and UGRID conventions can be added. Contributions are welcome!
73 |
--------------------------------------------------------------------------------
/doc/units.md:
--------------------------------------------------------------------------------
1 | ---
2 | jupytext:
3 | text_representation:
4 | format_name: myst
5 | kernelspec:
6 | display_name: Python 3
7 | name: python3
8 | hide-toc: true
9 | ---
10 |
11 | # Units
12 |
13 | ```{seealso}
14 | 1. [CF conventions on units](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#units)
15 | ```
16 |
17 | The xarray ecosystem supports unit-aware arrays using [pint](https://pint.readthedocs.io) and [pint-xarray](https://pint-xarray.readthedocs.io). Some changes are required to make these packages work well with [UDUNITS format recommended by the CF conventions](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#units).
18 |
19 | `cf_xarray` makes those recommended changes when you `import cf_xarray.units`. These changes allow pint to parse and format UDUNIT units strings, and add several custom units like `degrees_north` for latitude, `psu` for ocean salinity, etc. Be aware that pint supports some units that UDUNITS does not recognize but `cf-xarray` will not try to detect them and raise an error. For example, a temperature subtraction returns "delta_degC" units in pint, which does not exist in UDUNITS.
20 |
21 | ## Formatting units
22 |
23 | For now, only the short format using [symbols](https://docs.unidata.ucar.edu/udunits/current/udunits2lib.html#Syntax) is supported:
24 |
25 | ```{code-cell}
26 | from pint import application_registry as ureg
27 | import cf_xarray.units
28 |
29 | u = ureg.Unit("m ** 3 / s ** 2")
30 | f"{u:cf}" # or {u:~cf}, both return the same short format
31 | ```
32 |
--------------------------------------------------------------------------------
/doc/whats-new.rst:
--------------------------------------------------------------------------------
1 | .. currentmodule:: xarray
2 |
3 | What's New
4 | ----------
5 |
6 | For updates since Feb 2023 see the `Github releases page `_.
7 |
8 | v0.8.9 (Feb 06, 2023)
9 | =====================
10 | - Convert integer (e.g. ``1``) units to string (e.g. ``"1"``) for pint. By `Justus Magin`_.
11 |
12 | v0.8.8 (Jan 19, 2023)
13 | =====================
14 | - Add conversion between CF geometries and Shapely objects for polygons. By `Julia Signell`_.
15 | - Support 32bit wide bit masks. By `Michael St Laurent`_.
16 |
17 | v0.8.7 (Dec 19, 2023)
18 | =====================
19 | - Add conversion between CF geometries and Shapely objects for lines. By `Julia Signell`_.
20 |
21 | v0.8.5 (Oct 24, 2023)
22 | =====================
23 | - Fix for ``get_bounds_dim_name``. By `Deepak Cherian`_.
24 |
25 | v0.8.2 (July 05, 2023)
26 | ======================
27 | - Better reprs for flag masks. By `Deepak Cherian`_.
28 |
29 | v0.8.2 (June 23, 2023)
30 | ======================
31 | - Support for flag masks. By `Clément Haëck`_ and `Deepak Cherian`_.
32 |
33 | v0.8.1 (May 9, 2023)
34 | ====================
35 | - Stop bundling the standard name table and use ``pooch`` (new optional dependency) to download when needed.
36 | By `Deepak Cherian`_.
37 | - Major performance improvements. By `Deepak Cherian`_.
38 | - Fix bug where code was looking for ``T`` axis in SGRID info.
39 | By `Kristen Thyng`_.
40 | - Support ``X``, ``Y`` for rotated pole grids. By `Lars Buntemeyer`_.
41 |
42 |
43 | v0.8.0 (Feb 8, 2023)
44 | ====================
45 |
46 | - Support interpreting `SGRID Conventions `_ to identify
47 | X, Y, Z axes. By `Deepak Cherian`_.
48 | - Add a `rich `_ repr. (:pr:`409`).
49 | Use ``rich.print(ds.cf)`` or ``%load_ext rich`` in a Jupyter session to
50 | view a much richer representation of the ``.cf`` accessor. By `Deepak Cherian`_.
51 | - Support interpreting the ``grid_mapping`` attribute: (:pr:`391`).
52 | See :py:meth:`Dataset.cf.grid_mapping_names` and ``Dataset.cf["grid_mapping"]``,
53 | ``DataArray.cf["grid_mapping"]``. By `Lars Buntemeyer`_ and `Deepak Cherian`_.
54 |
55 |
56 | v0.7.9 (Jan 31, 2023)
57 | =====================
58 |
59 | - Fix packaging of v0.7.8. That release was yanked off PyPI.
60 |
61 | v0.7.8 (Jan 31, 2023)
62 | =====================
63 |
64 | - Optionally use the ``regex`` package to continue supporting global flags in regular expressions that are not at start of pattern (:pr:`408`). By `Kristen Thyng`_
65 | - Added link to docs for a new example "COSIMA ocean-sea ice model demo" (:pr:`397`). By `Aidan Heerdegen`_
66 |
67 | v0.7.7 (Jan 14, 2023)
68 | =====================
69 |
70 | - Fix to ``geometry.points_to_cf`` to support shapely 2.0. (:pr:`386`).
71 | By `Pascal Bourgault`_
72 |
73 | v0.7.6 (Dec 07, 2022)
74 | =====================
75 |
76 | - Fix to ``cf.add_bounds`` to support all types of curved grids (:pr:`376`).
77 | By `Pascal Bourgault`_
78 | - Allow custom criteria to match the variable name of DataArray objects (:pr:`379`). By `Mathias Hauser`_
79 | - Support new Xarray indexes API when creating MultiIndex for "compression by gathering" datasets.
80 | (:pr:`381`). By `Deepak Cherian`_
81 |
82 | v0.7.5 (Nov 15, 2022)
83 | =====================
84 |
85 | - ``cf.add_bounds`` can estimate 2D bounds using an approximate linear interpolation (:pr:`370`).
86 | By `Pascal Bourgault`_.
87 | - Improve detection of bounds order by rellaxing check a bit (:pr:`361`).
88 | By `Lars Buntemeyer`_.
89 | - Performance improvements. (:pr:`358`). By `Luke Davis`_
90 | - Fix coordinate/axis detection (:pr:`359`). By `Martin Schupfner`_
91 |
92 | v0.7.4 (July 14, 2022)
93 | ======================
94 |
95 | - Major performance improvement for ``__getitem_`` (:pr:`349`).
96 | By `Deepak Cherian`_.
97 | - Raise warning instead of error with malformed ``cell_measures`` attributes. Warnings
98 | now print names of variables with malformed attributes (:pr:`350`).
99 | By `Deepak Cherian`_.
100 |
101 | v0.7.3 (June 30, 2022)
102 | ======================
103 | - :py:meth:`Dataset.cf.guess_coord_axis` now skips known axes/coordinates and only returns a single guess per variable.
104 | Additional attributes such as ``units`` must be added to known axes/coordinates using :py:meth:`Dataset.cf.add_canonical_attributes`.
105 | By `Mattia Almansi`_.
106 | - Increased support for ``cf_role`` variables. Added :py:attr:`Dataset.cf.cf_roles` By `Deepak Cherian`_.
107 |
108 | v0.7.2 (April 5, 2022)
109 | ======================
110 | - added encoder and decoder for writing pandas MultiIndex-es to file using "compression by gathering".
111 | See :doc:`coding` for more. By `Deepak Cherian`_.
112 | - added another type of vertical coordinate to decode: ``ocean_sigma_coordinate``. By `Kristen Thyng`_.
113 |
114 | v0.7.0 (January 24, 2022)
115 | =========================
116 | - Many improvements to autoguessing for plotting. By `Deepak Cherian`_
117 | - Fix detection of datetime-like variables. By `Romain Caneill`_
118 | - Integrate more unit aliases from ``xclim`` (Thanks!). By `Deepak Cherian`_
119 | - Significantly expanded and improved documention. By `Deepak Cherian`_.
120 | - integrate ``xclim``'s CF-compliant unit formatter. :pr:`278`. By `Justus Magin`_.
121 | - Fix dropping of bad variable names. :pr:`291`. By `Tom Vo`_.
122 |
123 | v0.6.3 (December 16, 2021)
124 | ==========================
125 | - Packaging improvements. By `Filipe Fernandes`_.
126 | - cf_xarray now works with Datasets created using ``_to_temp_dataset``. By `Pascal Bourgault`_.
127 |
128 | v0.6.2 (December 7, 2021)
129 | =========================
130 | - Various bug fixes.
131 | - New ``cf_xarray.geometry`` submodule with functions :py:func:`shapely_to_cf`` and :py:func:`cf_to_shapely` to convert between CF-compliant geometry datasets and DataArrays storing shapely geometries (new optional dependency). By `Pascal Bourgault`_.
132 |
133 | v0.6.1 (August 16, 2021)
134 | ========================
135 | - Support detecting pint-backed Variables with units-based criteria. By `Deepak Cherian`_.
136 | - Support reshaping nD bounds arrays to (n-1)D vertex arrays. By `Deepak Cherian`_.
137 | - Support rich comparisons with ``DataArray.cf`` and :py:meth:`DataArray.cf.isin` for `flag variables`_.
138 | By `Deepak Cherian`_ and `Julius Busecke`_
139 |
140 | v0.6.0 (June 29, 2021)
141 | ======================
142 | - Support indexing by ``cf_role`` attribute. By `Deepak Cherian`_.
143 | - Implemented :py:meth:`Dataset.cf.add_canonical_attributes` and :py:meth:`DataArray.cf.add_canonical_attributes`
144 | to add CF canonical attributes. By `Mattia Almansi`_.
145 | - Begin adding support for units with a unit registry for pint arrays. :pr:`197`.
146 | By `Jon Thielen`_ and `Justus Magin`_.
147 | - :py:meth:`Dataset.cf.rename_like` also updates the ``bounds`` and ``cell_measures`` attributes. By `Mattia Almansi`_.
148 | - Support of custom vocabularies/criteria: user can input criteria for identifying variables by their name and attributes to be able to refer to them by custom names like ``ds.cf["ssh"]``. :pr:`234`. By `Kristen Thyng`_ and `Deepak Cherian`_.
149 |
150 | v0.5.2 (May 11, 2021)
151 | =====================
152 |
153 | - Add some explicit support for CMIP6 output. By `Deepak Cherian`_.
154 | - Replace the ``dims`` argument of :py:meth:`Dataset.cf.add_bounds` with ``keys``, allowing to use CF keys. By `Mattia Almansi`_.
155 | - Added :py:attr:`DataArray.cf.formula_terms` and :py:attr:`Dataset.cf.formula_terms`.
156 | By `Deepak Cherian`_.
157 | - Added :py:attr:`Dataset.cf.bounds` to return a dictionary mapping valid keys to the variable names of their bounds. By `Mattia Almansi`_.
158 | - :py:meth:`DataArray.cf.differentiate` and :py:meth:`Dataset.cf.differentiate` can optionally correct
159 | sign of the derivative by interpreting the ``"positive"`` attribute. By `Deepak Cherian`_.
160 |
161 | v0.5.1 (Feb 24, 2021)
162 | =====================
163 |
164 | Minor bugfix release, thanks to `Pascal Bourgault`_.
165 |
166 | v0.5.0 (Feb 24, 2021)
167 | =====================
168 |
169 | - Replace ``cf.describe()`` with :py:meth:`Dataset.cf.__repr__`. By `Mattia Almansi`_.
170 | - Automatically set ``x`` or ``y`` for :py:attr:`DataArray.cf.plot`. By `Deepak Cherian`_.
171 | - Added scripts to document coordinate and axes criteria with tables. By `Mattia Almansi`_.
172 | - Support for ``.drop_vars()``, ``.drop_sel()``, ``.drop_dims()``, ``.set_coords()``, ``.reset_coords()``. By `Mattia Almansi`_.
173 | - Support for using ``standard_name`` in more functions. (:pr:`128`) By `Deepak Cherian`_
174 | - Allow :py:meth:`DataArray.cf.__getitem__` with standard names. By `Deepak Cherian`_
175 | - Rewrite the ``values`` of :py:attr:`Dataset.coords` and :py:attr:`Dataset.data_vars` with objects returned
176 | by :py:meth:`Dataset.cf.__getitem__`. This allows extraction of DataArrays when there are clashes
177 | between DataArray names and "special" CF names like ``T``.
178 | (:issue:`129`, :pr:`130`). By `Deepak Cherian`_
179 | - Retrieve bounds dimension name with :py:meth:`Dataset.cf.get_bounds_dim_name`. By `Pascal Bourgault`_.
180 | - Fix iteration and arithmetic with ``GroupBy`` objects. By `Deepak Cherian`_.
181 |
182 | v0.4.0 (Jan 22, 2021)
183 | =====================
184 | - Support for arbitrary cell measures indexing. By `Mattia Almansi`_.
185 | - Avoid using ``grid_latitude`` and ``grid_longitude`` for detecting latitude and longitude variables.
186 | By `Pascal Bourgault`_.
187 |
188 | v0.3.1 (Nov 25, 2020)
189 | =====================
190 | - Support :py:attr:`Dataset.cf.cell_measures`. By `Deepak Cherian`_.
191 | - Added :py:attr:`Dataset.cf.axes` to return a dictionary mapping available Axis standard names to variable names of an xarray object, :py:attr:`Dataset.cf.coordinates` for Coordinates, :py:attr:`Dataset.cf.cell_measures` for Cell Measures, and :py:attr:`Dataset.cf.standard_names` for all variables. `Kristen Thyng`_ and `Mattia Almansi`_.
192 | - Changed :py:meth:`Dataset.cf.get_valid_keys` to :py:meth:`Dataset.cf.keys`. `Kristen Thyng`_.
193 | - Added :py:meth:`Dataset.cf.decode_vertical_coords` for decoding of parameterized vertical coordinate variables.
194 | (:issue:`34`, :pr:`103`). `Deepak Cherian`_.
195 | - Added top-level :py:func:`~cf_xarray.bounds_to_vertices` and :py:func:`~cf_xarray.vertices_to_bounds` as well as :py:meth:`Dataset.cf.bounds_to_vertices`
196 | to convert from coordinate bounds in a CF format (shape (nx, 2)) to a vertices format (shape (nx+1)).
197 | (:pr:`108`). `Pascal Bourgault`_.
198 |
199 | v0.3.0 (Sep 27, 2020)
200 | =====================
201 | This release brings changes necessary to make ``cf_xarray`` more useful with the ROMS
202 | model in particular. Thanks to Kristen Thyng for opening many issues.
203 |
204 | - ``vertical`` and ``Z`` are not synonyms any more. In particular, the attribute
205 | ``positive: up`` now will only match ``vertical`` and not ``Z``. `Deepak Cherian`_.
206 | - Fixed tests that would only pass if ran in a specific order. `Julia Kent`_.
207 |
208 | v0.2.1 (Aug 06, 2020)
209 | =====================
210 | - Support for the ``bounds`` attribute. (:pr:`68`, :issue:`32`). `Deepak Cherian`_.
211 | - Add :py:meth:`Dataset.cf.guess_coord_axis` to automagically guess axis and coord names, and add
212 | appropriate attributes. (:pr:`67`, :issue:`46`). `Deepak Cherian`_.
213 |
214 | v0.2.0 (Jul 28, 2020)
215 | =====================
216 |
217 | - ``cf_xarray`` is now available on conda-forge. Thanks to `Anderson Banihirwe`_ and `Filipe Fernandes`_
218 | - Remap datetime accessor syntax for groupby. E.g. ``.cf.groupby("T.month")`` → ``.cf.groupby("ocean_time.month")``.
219 | (:pr:`64`, :issue:`6`). `Julia Kent`_.
220 | - Added :py:meth:`Dataset.cf.rename_like` to rename matching variables. Only coordinate variables
221 | i.e. those that match the criteria for ``("latitude", "longitude", "vertical", "time")``
222 | are renamed for now. (:pr:`55`) `Deepak Cherian`_.
223 | - Added :py:meth:`Dataset.cf.add_bounds` to add guessed bounds for 1D coordinates. (:pr:`53`) `Deepak Cherian`_.
224 |
225 | v0.1.5
226 | ======
227 |
228 | - Begin documenting things for contributors in :ref:`contribut`.
229 | - Parse ``ancillary_variables`` attribute. These variables are converted to coordinate variables.
230 | - Support :py:meth:`Dataset.reset_index`
231 | - Wrap ``.sizes`` and ``.chunks``. (:pr:`42`) `Deepak Cherian`_.
232 |
233 | >>> ds.cf.sizes
234 | {'X': 53, 'Y': 25, 'T': 2920, 'longitude': 53, 'latitude': 25, 'time': 2920}
235 |
236 |
237 | v0.1.4
238 | ======
239 |
240 | - Support indexing by ``standard_name``
241 | - Set default ``xincrease`` and ``yincrease`` by interpreting the ``positive`` attribute.
242 |
243 | v0.1.3
244 | ======
245 |
246 | - Support expanding key to multiple dimension names.
247 |
248 | .. _`Mattia Almansi`: https://github.com/malmans2
249 | .. _`Justus Magin`: https://github.com/keewis
250 | .. _`Jon Thielen`: https://github.com/jthielen
251 | .. _`Anderson Banihirwe`: https://github.com/andersy005
252 | .. _`Pascal Bourgault`: https://github.com/aulemahal
253 | .. _`Deepak Cherian`: https://github.com/dcherian
254 | .. _`Filipe Fernandes`: https://github.com/ocefpaf
255 | .. _`Julia Kent`: https://github.com/jukent
256 | .. _`Kristen Thyng`: https://github.com/kthyng
257 | .. _`Julius Busecke`: https://github.com/jbusecke
258 | .. _`Tom Vo`: https://github.com/tomvothecoder
259 | .. _`Romain Caneill`: https://github.com/rcaneill
260 | .. _`Lars Buntemeyer`: https://github.com/larsbuntemeyer
261 | .. _`Luke Davis`: https://github.com/lukelbd
262 | .. _`Martin Schupfner`: https://github.com/sol1105
263 | .. _`Mathias Hauser`: https://github.com/mathause
264 | .. _`Aidan Heerdegen`: https://github.com/aidanheerdegen
265 | .. _`Clément Haëck`: https://github.com/Descanonge
266 | .. _`Julia Signell`: https://github.com/jsignell
267 | .. _`Michael St Laurent`: https://github.com/mps010160
268 | .. _`flag variables`: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags
269 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "cf_xarray"
3 | description = "A convenience wrapper for using CF attributes on xarray objects"
4 | readme = "README.rst"
5 | requires-python = ">=3.10"
6 | license = {file = "LICENSE"}
7 | keywords = ["xarray", "metadata", "CF conventions"]
8 | classifiers = [
9 | "Development Status :: 5 - Production/Stable",
10 | "License :: OSI Approved :: Apache Software License",
11 | "Natural Language :: English",
12 | "Operating System :: OS Independent",
13 | "Programming Language :: Python",
14 | "Programming Language :: Python :: 3.10",
15 | "Programming Language :: Python :: 3.11",
16 | "Programming Language :: Python :: 3.12",
17 | "Programming Language :: Python :: 3.13",
18 | ]
19 | dependencies = [
20 | "xarray>=2023.09.0",
21 | ]
22 | dynamic = ["version"]
23 |
24 | [project.optional-dependencies]
25 | all = ["matplotlib", "pint >=0.18, !=0.24.0", "shapely", "regex", "rich", "pooch"]
26 |
27 | [project.urls]
28 | homepage = "https://cf-xarray.readthedocs.io"
29 | documentation = "https://cf-xarray.readthedocs.io"
30 | repository = "https://github.com/xarray-contrib/cf-xarray.git"
31 | changelog = "https://cf-xarray.readthedocs.io/en/latest/whats-new.html"
32 |
33 | [build-system]
34 | requires = [
35 | # xarray is need for dynamic version string
36 | "xarray>=2023.09.0",
37 | "setuptools>=45",
38 | "setuptools_scm[toml]>=6.2",
39 | ]
40 | build-backend = "setuptools.build_meta"
41 |
42 | [tool.setuptools]
43 | packages = ["cf_xarray", "cf_xarray.scripts"]
44 |
45 | [tool.setuptools.exclude-package-data]
46 | cf_xarray = ["tests/*"]
47 |
48 | [tool.setuptools.dynamic]
49 | version = {attr = "cf_xarray.__version__"}
50 |
51 | [tool.setuptools_scm]
52 | fallback_version = "999"
53 | write_to = "cf_xarray/_version.py"
54 | write_to_template= '__version__ = "{version}"'
55 | tag_regex= "^(?Pv)?(?P[^\\+]+)(?P.*)?$"
56 |
57 | [tool.ruff]
58 | target-version = "py310"
59 | builtins = ["ellipsis"]
60 | exclude = [
61 | ".eggs",
62 | "doc",
63 | ]
64 |
65 | [tool.ruff.lint]
66 | # E402: module level import not at top of file
67 | # E501: line too long - let black worry about that
68 | # E731: do not assign a lambda expression, use a def
69 | ignore = [
70 | "E402",
71 | "E501",
72 | "E731",
73 | "B018",
74 | "B015",
75 | ]
76 | select = [
77 | # Bugbear
78 | "B",
79 | # Pyflakes
80 | "F",
81 | # Pycodestyle
82 | "E",
83 | "W",
84 | # isort
85 | "I",
86 | # Pyupgrade
87 | "UP",
88 | ]
89 |
90 | [tool.ruff.lint.isort]
91 | known-first-party = ["cf_xarray"]
92 | known-third-party = [
93 | "dask",
94 | "matplotlib",
95 | "numpy",
96 | "pandas",
97 | "pint",
98 | "pkg_resources",
99 | "pytest",
100 | "setuptools",
101 | "sphinx_autosummary_accessors",
102 | "xarray"
103 | ]
104 |
105 | [tool.ruff.format]
106 | # Enable reformatting of code snippets in docstrings.
107 | docstring-code-format = true
108 |
109 |
110 | [tool.pytest]
111 | python_files = "test_*.py"
112 | testpaths = ["cf_xarray/tests"]
113 |
114 | [tool.rstcheck]
115 | report_level = "WARNING"
116 | ignore_roles = [
117 | "pr",
118 | "issue",
119 | ]
120 | ignore_directives = [
121 | "ipython",
122 | "autodata",
123 | "autosummary",
124 | ]
125 |
126 | [tool.nbqa.md]
127 | mdformat = true
128 |
129 | [tool.mypy]
130 | exclude = "doc|flycheck"
131 | files = "cf_xarray/"
132 | show_error_codes = true
133 | warn_unused_ignores = true
134 | warn_unreachable = true
135 | enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
136 |
137 |
138 | [[tool.mypy.overrides]]
139 | module=[
140 | "cftime",
141 | "pandas",
142 | "pooch",
143 | "pint",
144 | "matplotlib",
145 | "pytest",
146 | "shapely",
147 | "shapely.geometry",
148 | "xarray.core.pycompat",
149 | ]
150 | ignore_missing_imports = true
151 |
152 | [tool.coverage.run]
153 | omit = ["*/tests/*"]
154 |
--------------------------------------------------------------------------------