├── .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 | ![2d bounds error](2D_bounds_error.png) 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 | ![2d bounds unmatching corners](2D_bounds_nonunique.png) 24 | ![2d bounds averaged corners](2D_bounds_averaged.png) 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 | ![rich repr](_static/rich-repr-example.png) 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 | ![cartopy rotated pole projection](cartopy_rotated_pole.png) 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 | ![rich repr](_static/rich-repr-example.png) 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 | --------------------------------------------------------------------------------