diff --git a/.github/workflows/cd-pypi.yml b/.github/workflows/cd-pypi.yml new file mode 100644 index 0000000..6104026 --- /dev/null +++ b/.github/workflows/cd-pypi.yml @@ -0,0 +1,11 @@ +name: cd + +on: + push: + tags: + - '**' + +jobs: + pypi: + uses: ecmwf-actions/reusable-workflows/.github/workflows/cd-pypi.yml@v2 + secrets: inherit diff --git a/.github/workflows/legacy-ci.yml b/.github/workflows/legacy-ci.yml index 921fb15..b9619d8 100644 --- a/.github/workflows/legacy-ci.yml +++ b/.github/workflows/legacy-ci.yml @@ -36,62 +36,7 @@ jobs: python-version: 3.x - uses: pre-commit/action@v3.0.0 - unit-tests: - name: unit-tests (3.10) - if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Install Conda environment with Micromamba - uses: mamba-org/provision-with-micromamba@v14 - with: - environment-file: tests/environment-unit-tests.yml - environment-name: DEVELOP - channels: conda-forge - cache-env: true - extra-specs: | - python=3.10 - - name: Install package - run: | - python -m pip install --no-deps -e . - - name: Run tests - run: | - make unit-tests - - type-check: - needs: [unit-tests] - if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} - runs-on: ubuntu-latest - defaults: - run: - shell: bash -l {0} - - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Install Conda environment with Micromamba - uses: mamba-org/provision-with-micromamba@v12 - with: - environment-file: environment.yml - environment-name: DEVELOP - channels: conda-forge - cache-env: true - cache-env-key: ubuntu-latest-3.10 - extra-specs: | - python=3.10 - - name: Install package - run: | - python -m pip install --no-deps -e . - - name: Run code quality checks - run: | - echo type-check not used - documentation: - needs: [unit-tests] if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} runs-on: ubuntu-latest defaults: @@ -105,7 +50,7 @@ jobs: - name: Install Conda environment with Micromamba uses: mamba-org/provision-with-micromamba@v12 with: - environment-file: environment.yml + environment-file: tests/environment-unit-tests.yml environment-name: DEVELOP channels: conda-forge cache-env: true @@ -118,75 +63,3 @@ jobs: - name: Build documentation run: | make docs-build - - integration-tests: - needs: [unit-tests] - if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} - runs-on: ubuntu-latest - defaults: - run: - shell: bash -l {0} - - strategy: - matrix: - include: - - python-version: "3.10" - # extra: -minver # This will need to be uncommented and environment-minver.yml updated if we want to publish on conda - - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Install Conda environment with Micromamba - uses: mamba-org/provision-with-micromamba@v12 - with: - environment-file: tests/environment-unit-tests${{ matrix.extra }}.yml - environment-name: DEVELOP${{ matrix.extra }} - channels: conda-forge - cache-env: true - cache-env-key: ubuntu-latest-${{ matrix.python-version }}${{ matrix.extra }}. - extra-specs: | - python=${{matrix.python-version }} - - name: Install package - run: | - python -m pip install --no-deps -e . - - name: Run tests - run: | - make unit-tests - - distribution: - needs: [integration-tests, type-check, documentation] - if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Build distributions - run: | - $CONDA/bin/python -m pip install build - $CONDA/bin/python -m build - - name: Publish a Python distribution to PyPI - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} - - notify: - if: always() && ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} - needs: - - pre-commit - - unit-tests - - type-check - - documentation - - integration-tests - - distribution - runs-on: ubuntu-latest - steps: - - name: Trigger Teams notification - uses: ecmwf-actions/notify-teams@v1 - with: - incoming_webhook: ${{ secrets.MS_TEAMS_INCOMING_WEBHOOK }} - needs_context: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore index 913eabb..e46c00f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # setuptools-scm version.py +_version.py # Sphinx automatic generation of API docs/_api/ diff --git a/docs/conf.py b/docs/conf.py index d34cfb9..4854c59 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,8 +42,8 @@ autodoc_typehints = "none" # autoapi configuration -autoapi_dirs = ["../earthkit/meteo"] -autoapi_ignore = ["*/version.py", "sphinxext/*"] +autoapi_dirs = ["../src/earthkit/meteo"] +autoapi_ignore = ["*/_version.py", "sphinxext/*"] autoapi_options = [ "members", "undoc-members", diff --git a/pyproject.toml b/pyproject.toml index 1441605..7279832 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,47 @@ [build-system] -requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] +requires = ["setuptools>=61", "setuptools-scm>=8.0"] + +[project] +authors = [ + {name = "European Centre for Medium-Range Weather Forecasts (ECMWF)", email = "software.support@ecmwf.int"} +] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Operating System :: OS Independent" +] +dependencies = [ + "numpy" +] +description = "Meteorological computations" +dynamic = ["version"] +license = {text = "Apache License Version 2.0"} +name = "earthkit-meteo" +readme = "README.md" +requires-python = ">= 3.8" + +[project.optional-dependencies] +test = [ + "pytest", + "pytest-cov" +] + +[project.urls] +Documentation = "https://earthkit-meteo.readthedocs.io/" +Homepage = "https://github.com/ecmwf/earthkit-meteo/" +Issues = "https://github.com/ecmwf/earthkit-meteo.issues" +Repository = "https://github.com/ecmwf/earthkit-meteo/" [tool.coverage.run] -branch = true +branch = "true" [tool.isort] profile = "black" @@ -11,9 +50,9 @@ profile = "black" add_ignore = ["D1", "D200", "D205", "D400", "D401"] convention = "numpy" +[tool.setuptools.packages.find] +include = ["earthkit.meteo"] +where = ["src/"] + [tool.setuptools_scm] -write_to = "earthkit/meteo/version.py" -write_to_template = ''' -# Do not change! Do not track in version control! -__version__ = "{version}" -''' +version_file = "src/earthkit/meteo/_version.py" diff --git a/setup.cfg b/setup.cfg index 6e27fe8..4736e11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,39 +1,3 @@ -[metadata] -name = earthkit-meteo -license = Apache License 2.0 -description = Meteorological computations - Development Status :: 2 - Pre-Alpha - Intended Audience :: Science/Research - License :: OSI Approved :: Apache Software License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Topic :: Scientific/Engineering -long_description_content_type=text/markdown -long_description = file: README.md -test_suite = tests - -[options] -packages = find_namespace: -install_requires = - numpy - -[options.packages.find] -include = earthkit.* - -[options.extras_require] -test = - pytest - pytest-cov - [flake8] max-line-length = 110 extend-ignore = E203, W503 - -[mypy] -strict = False -ignore_missing_imports = True diff --git a/earthkit/meteo/__init__.py b/src/earthkit/meteo/__init__.py similarity index 94% rename from earthkit/meteo/__init__.py rename to src/earthkit/meteo/__init__.py index d64217c..90d22f3 100644 --- a/earthkit/meteo/__init__.py +++ b/src/earthkit/meteo/__init__.py @@ -10,7 +10,7 @@ try: # NOTE: the `version.py` file must not be present in the git repository # as it is generated by setuptools at install time - from .version import __version__ + from ._version import __version__ except ImportError: # pragma: no cover # Local copy or not installed with setuptools __version__ = "999" diff --git a/earthkit/meteo/constants/__init__.py b/src/earthkit/meteo/constants/__init__.py similarity index 100% rename from earthkit/meteo/constants/__init__.py rename to src/earthkit/meteo/constants/__init__.py diff --git a/earthkit/meteo/constants/constants.py b/src/earthkit/meteo/constants/constants.py similarity index 100% rename from earthkit/meteo/constants/constants.py rename to src/earthkit/meteo/constants/constants.py diff --git a/earthkit/meteo/extreme/__init__.py b/src/earthkit/meteo/extreme/__init__.py similarity index 100% rename from earthkit/meteo/extreme/__init__.py rename to src/earthkit/meteo/extreme/__init__.py diff --git a/earthkit/meteo/extreme/array/__init__.py b/src/earthkit/meteo/extreme/array/__init__.py similarity index 100% rename from earthkit/meteo/extreme/array/__init__.py rename to src/earthkit/meteo/extreme/array/__init__.py diff --git a/earthkit/meteo/extreme/array/cpf.py b/src/earthkit/meteo/extreme/array/cpf.py similarity index 100% rename from earthkit/meteo/extreme/array/cpf.py rename to src/earthkit/meteo/extreme/array/cpf.py diff --git a/earthkit/meteo/extreme/array/efi.py b/src/earthkit/meteo/extreme/array/efi.py similarity index 100% rename from earthkit/meteo/extreme/array/efi.py rename to src/earthkit/meteo/extreme/array/efi.py diff --git a/earthkit/meteo/extreme/array/sot.py b/src/earthkit/meteo/extreme/array/sot.py similarity index 100% rename from earthkit/meteo/extreme/array/sot.py rename to src/earthkit/meteo/extreme/array/sot.py diff --git a/earthkit/meteo/extreme/cpf.py b/src/earthkit/meteo/extreme/cpf.py similarity index 100% rename from earthkit/meteo/extreme/cpf.py rename to src/earthkit/meteo/extreme/cpf.py diff --git a/earthkit/meteo/extreme/efi.py b/src/earthkit/meteo/extreme/efi.py similarity index 100% rename from earthkit/meteo/extreme/efi.py rename to src/earthkit/meteo/extreme/efi.py diff --git a/earthkit/meteo/extreme/sot.py b/src/earthkit/meteo/extreme/sot.py similarity index 100% rename from earthkit/meteo/extreme/sot.py rename to src/earthkit/meteo/extreme/sot.py diff --git a/earthkit/meteo/score/__init__.py b/src/earthkit/meteo/score/__init__.py similarity index 100% rename from earthkit/meteo/score/__init__.py rename to src/earthkit/meteo/score/__init__.py diff --git a/earthkit/meteo/score/array/__init__.py b/src/earthkit/meteo/score/array/__init__.py similarity index 100% rename from earthkit/meteo/score/array/__init__.py rename to src/earthkit/meteo/score/array/__init__.py diff --git a/earthkit/meteo/score/array/crps.py b/src/earthkit/meteo/score/array/crps.py similarity index 100% rename from earthkit/meteo/score/array/crps.py rename to src/earthkit/meteo/score/array/crps.py diff --git a/earthkit/meteo/score/crps.py b/src/earthkit/meteo/score/crps.py similarity index 100% rename from earthkit/meteo/score/crps.py rename to src/earthkit/meteo/score/crps.py diff --git a/earthkit/meteo/solar/__init__.py b/src/earthkit/meteo/solar/__init__.py similarity index 100% rename from earthkit/meteo/solar/__init__.py rename to src/earthkit/meteo/solar/__init__.py diff --git a/earthkit/meteo/solar/array/__init__.py b/src/earthkit/meteo/solar/array/__init__.py similarity index 100% rename from earthkit/meteo/solar/array/__init__.py rename to src/earthkit/meteo/solar/array/__init__.py diff --git a/earthkit/meteo/solar/array/solar.py b/src/earthkit/meteo/solar/array/solar.py similarity index 100% rename from earthkit/meteo/solar/array/solar.py rename to src/earthkit/meteo/solar/array/solar.py diff --git a/earthkit/meteo/solar/solar.py b/src/earthkit/meteo/solar/solar.py similarity index 100% rename from earthkit/meteo/solar/solar.py rename to src/earthkit/meteo/solar/solar.py diff --git a/earthkit/meteo/stats/__init__.py b/src/earthkit/meteo/stats/__init__.py similarity index 100% rename from earthkit/meteo/stats/__init__.py rename to src/earthkit/meteo/stats/__init__.py diff --git a/earthkit/meteo/stats/array/__init__.py b/src/earthkit/meteo/stats/array/__init__.py similarity index 100% rename from earthkit/meteo/stats/array/__init__.py rename to src/earthkit/meteo/stats/array/__init__.py diff --git a/earthkit/meteo/stats/array/numpy_extended.py b/src/earthkit/meteo/stats/array/numpy_extended.py similarity index 100% rename from earthkit/meteo/stats/array/numpy_extended.py rename to src/earthkit/meteo/stats/array/numpy_extended.py diff --git a/earthkit/meteo/stats/array/quantiles.py b/src/earthkit/meteo/stats/array/quantiles.py similarity index 96% rename from earthkit/meteo/stats/array/quantiles.py rename to src/earthkit/meteo/stats/array/quantiles.py index 33e1cd6..0f9109a 100644 --- a/earthkit/meteo/stats/array/quantiles.py +++ b/src/earthkit/meteo/stats/array/quantiles.py @@ -59,6 +59,7 @@ def iter_quantiles( if method == "sort": arr = np.asarray(arr) arr.sort(axis=axis) + missing = np.isnan(arr).any(axis=axis) for q in qs: if method == "numpy": @@ -74,4 +75,5 @@ def iter_quantiles( tmp = arr.take(min(j + 1, m - 1), axis=axis) tmp *= x quantile += tmp + quantile[missing] = np.nan yield quantile diff --git a/earthkit/meteo/stats/numpy_extended.py b/src/earthkit/meteo/stats/numpy_extended.py similarity index 100% rename from earthkit/meteo/stats/numpy_extended.py rename to src/earthkit/meteo/stats/numpy_extended.py diff --git a/earthkit/meteo/stats/quantiles.py b/src/earthkit/meteo/stats/quantiles.py similarity index 100% rename from earthkit/meteo/stats/quantiles.py rename to src/earthkit/meteo/stats/quantiles.py diff --git a/earthkit/meteo/thermo/__init__.py b/src/earthkit/meteo/thermo/__init__.py similarity index 100% rename from earthkit/meteo/thermo/__init__.py rename to src/earthkit/meteo/thermo/__init__.py diff --git a/earthkit/meteo/thermo/array/__init__.py b/src/earthkit/meteo/thermo/array/__init__.py similarity index 100% rename from earthkit/meteo/thermo/array/__init__.py rename to src/earthkit/meteo/thermo/array/__init__.py diff --git a/earthkit/meteo/thermo/array/thermo.py b/src/earthkit/meteo/thermo/array/thermo.py similarity index 100% rename from earthkit/meteo/thermo/array/thermo.py rename to src/earthkit/meteo/thermo/array/thermo.py diff --git a/earthkit/meteo/thermo/thermo.py b/src/earthkit/meteo/thermo/thermo.py similarity index 100% rename from earthkit/meteo/thermo/thermo.py rename to src/earthkit/meteo/thermo/thermo.py diff --git a/earthkit/meteo/wind/__init__.py b/src/earthkit/meteo/wind/__init__.py similarity index 100% rename from earthkit/meteo/wind/__init__.py rename to src/earthkit/meteo/wind/__init__.py diff --git a/earthkit/meteo/wind/array/__init__.py b/src/earthkit/meteo/wind/array/__init__.py similarity index 100% rename from earthkit/meteo/wind/array/__init__.py rename to src/earthkit/meteo/wind/array/__init__.py diff --git a/earthkit/meteo/wind/array/wind.py b/src/earthkit/meteo/wind/array/wind.py similarity index 100% rename from earthkit/meteo/wind/array/wind.py rename to src/earthkit/meteo/wind/array/wind.py diff --git a/earthkit/meteo/wind/wind.py b/src/earthkit/meteo/wind/wind.py similarity index 100% rename from earthkit/meteo/wind/wind.py rename to src/earthkit/meteo/wind/wind.py diff --git a/tests/solar/test_solar.py b/tests/solar/test_solar.py index 0959128..bb362b8 100644 --- a/tests/solar/test_solar.py +++ b/tests/solar/test_solar.py @@ -25,3 +25,56 @@ def test_julian_day(date, expected_value): v = solar.julian_day(date) assert np.isclose(v, expected_value) + + +@pytest.mark.parametrize( + "date,expected_value", + [ + (datetime.datetime(2024, 4, 22), (12.235799080498582, 0.40707190497656276)), + ( + datetime.datetime(2024, 4, 22, 12, 0, 0), + (12.403019177270453, 0.43253901867797273), + ), + ], +) +def test_solar_declination_angle(date, expected_value): + declination, time_correction = solar.solar_declination_angle(date) + assert np.isclose(declination, expected_value[0]) + assert np.isclose(time_correction, expected_value[1]) + + +def test_cos_solar_zenith_angle(): + date = datetime.datetime(2024, 4, 22, 12, 0, 0) + latitudes = np.array([40.0]) + longitudes = np.array([18.0]) + + v = solar.cos_solar_zenith_angle(date, latitudes, longitudes) + assert np.isclose(v[0], 0.8478445449796352) + + +def test_cos_solar_zenith_angle_integrated(): + begin_date = datetime.datetime(2024, 4, 22) + end_date = datetime.datetime(2024, 4, 23) + latitudes = np.array([40.0]) + longitudes = np.array([18.0]) + + v = solar.cos_solar_zenith_angle_integrated( + begin_date, end_date, latitudes, longitudes + ) + assert np.isclose(v[0], 0.3110738757) + + +def test_incoming_solar_radiation(): + date = datetime.datetime(2024, 4, 22, 12, 0, 0) + v = solar.incoming_solar_radiation(date) + assert np.isclose(v, 4833557.3088814365) + + +def test_toa_incident_solar_radiation(): + begin_date = datetime.datetime(2024, 4, 22) + end_date = datetime.datetime(2024, 4, 23) + latitudes = np.array([40.0]) + longitudes = np.array([18.0]) + + v = solar.toa_incident_solar_radiation(begin_date, end_date, latitudes, longitudes) + assert np.isclose(v, 1503617.8237746414) diff --git a/tests/stats/test_stats.py b/tests/stats/test_stats.py index 7709313..ba7aea4 100644 --- a/tests/stats/test_stats.py +++ b/tests/stats/test_stats.py @@ -65,3 +65,16 @@ def test_quantiles(method): [5, 19, 12, 3, 45, 48, 8, 9, 7], ] ) + + +def test_quantiles_nans(): + arr = np.random.rand(100, 100, 100) + arr.ravel()[np.random.choice(arr.size, 100000, replace=False)] = np.nan + qs = [0.0, 0.25, 0.5, 0.75, 1.0] + sort = [ + quantile for quantile in stats.iter_quantiles(arr.copy(), qs, method="sort") + ] + numpy = [ + quantile for quantile in stats.iter_quantiles(arr.copy(), qs, method="numpy") + ] + assert np.all(np.isclose(sort, numpy, equal_nan=True))