From 97339e2613c401640aa06ecb25cdc2b80a63db50 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Wed, 26 Oct 2022 20:22:20 +0200 Subject: [PATCH 01/26] Add formating and linting pre-commit hooks --- .pre-commit-config.yaml | 41 +++++++++++++++++++++++++++++++++++++++++ paper/paper.md | 4 ++-- 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..9f254bba --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,41 @@ +repos: + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + + - repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + + - repo: https://github.com/pycqa/flake8 + rev: 5.0.4 + hooks: + - id: flake8 + additional_dependencies: + - flake8-blind-except + - flake8-bugbear + - flake8-builtins + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.981 + hooks: + - id: mypy + additional_dependencies: + - numpy + - pytest + - types-python-dateutil + - types-requests + - xarray + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-added-large-files + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + args: ["--fix=lf"] + - id: trailing-whitespace diff --git a/paper/paper.md b/paper/paper.md index 6df84b5c..476923f4 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -26,9 +26,9 @@ For Python, increased performance and accuracy is optionally available for certa Numpy is optional to enable multi-dimensional array inputs, but most of the functions work with Python alone (without Numpy). Other functions that are iterative could possibly be sped up with modules such as Cython or Numba. -PyMap3D is targeted for users needing conversions between coordinate systems for observation platforms near Earth's surface, +PyMap3D is targeted for users needing conversions between coordinate systems for observation platforms near Earth's surface, whether underwater, ground-based or space-based platforms. -This includes rocket launches, orbiting spacecrafts, UAVs, cameras, radars and many more. +This includes rocket launches, orbiting spacecrafts, UAVs, cameras, radars and many more. By adding ellipsoid parameters, it could be readily be used for other planets as well. The coordinate systems included are: * ECEF (Earth centered, Earth fixed) From 0cddc4ee9f4d0a0deccdd2389abb0b9415330141 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Wed, 26 Oct 2022 20:24:48 +0200 Subject: [PATCH 02/26] Add pyupgrade pre-commit hook --- .pre-commit-config.yaml | 6 ++++++ Examples/compare/compare_lox.py | 4 +--- Examples/compare/compare_vdist.py | 5 ++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f254bba..e883db59 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,10 @@ repos: + - repo: https://github.com/asottile/pyupgrade + rev: v3.0.0 + hooks: + - id: pyupgrade + args: ["--py37-plus"] + - repo: https://github.com/pycqa/isort rev: 5.10.1 hooks: diff --git a/Examples/compare/compare_lox.py b/Examples/compare/compare_lox.py index 9ca2c2a6..5c1b8684 100644 --- a/Examples/compare/compare_lox.py +++ b/Examples/compare/compare_lox.py @@ -5,12 +5,10 @@ from math import isclose from pathlib import Path -from matlab_mapping import matlab_mapping - import matlab.engine +from matlab_mapping import matlab_mapping from pymap3d.lox import loxodrome_direct - cwd = Path(__file__).parent eng = matlab.engine.start_matlab("-nojvm") eng.addpath(eng.genpath(str(cwd)), nargout=0) diff --git a/Examples/compare/compare_vdist.py b/Examples/compare/compare_vdist.py index 8c764e48..6dc7127e 100644 --- a/Examples/compare/compare_vdist.py +++ b/Examples/compare/compare_vdist.py @@ -3,12 +3,11 @@ import sys from math import isclose, nan -import numpy as np from pathlib import Path -from matlab_mapping import matlab_mapping - import matlab.engine +import numpy as np +from matlab_mapping import matlab_mapping from pymap3d.vincenty import vdist cwd = Path(__file__).parent From 0134584a989e3e8be05a03f4bcd58e3ab1c07d6e Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Wed, 26 Oct 2022 20:35:44 +0200 Subject: [PATCH 03/26] Add prettier and markdownlint pre-commit hook --- .github/workflows/README.md | 2 + .github/workflows/ci.yml | 37 ++++++++------- .github/workflows/ci_stdlib_only.yml | 35 +++++++-------- .github/workflows/publish-python-package.yml | 32 ++++++------- .lgtm.yml | 2 +- .pre-commit-config.yaml | 11 +++++ README.md | 38 ++++++++-------- codemeta.json | 24 +++++----- paper/paper.md | 47 ++++++++++---------- 9 files changed, 116 insertions(+), 112 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index a5cd6049..cc6e5598 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,7 +1,9 @@ # GitHub Actions CI workflows + Definitions for GitHub Actions (continuous integration) workflows ## Publishing + To publish a new version of the `pymap3d` package to PyPI, create and publish a release in GitHub (preferrably from a Git tag) for the version; the workflow will automatically build and publish an sdist and wheel from the tag. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0eb4f37a..fad43d77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,41 +3,40 @@ name: ci on: push: paths: - - "**.py" - - .github/workflows/ci.yml - - "!scripts/" + - "**.py" + - .github/workflows/ci.yml + - "!scripts/" pull-request: - - "**.py" - - .github/workflows/ci.yml + - "**.py" + - .github/workflows/ci.yml jobs: - full: runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest] include: - - os: macos-latest - python-version: '3.11' - - os: windows-latest - python-version: '3.11' + - os: macos-latest + python-version: "3.11" + - os: windows-latest + python-version: "3.11" steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} - - run: pip install .[full,tests,lint] + - run: pip install .[full,tests,lint] - - run: flake8 - - run: mypy + - run: flake8 + - run: mypy - - run: pytest + - run: pytest coverage: runs-on: ubuntu-latest diff --git a/.github/workflows/ci_stdlib_only.yml b/.github/workflows/ci_stdlib_only.yml index fe75ff8f..0cb4c4b0 100644 --- a/.github/workflows/ci_stdlib_only.yml +++ b/.github/workflows/ci_stdlib_only.yml @@ -3,35 +3,34 @@ name: ci_stdlib_only on: push: paths: - - "**.py" - - .github/workflows/ci_stdlib_only.yml - - "!scripts/" + - "**.py" + - .github/workflows/ci_stdlib_only.yml + - "!scripts/" pull-request: - - "**.py" - - .github/workflows/ci_stdlib_only.yml + - "**.py" + - .github/workflows/ci_stdlib_only.yml jobs: - stdlib_only: runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} strategy: matrix: - python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11'] - os: ['ubuntu-latest'] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + os: ["ubuntu-latest"] include: - - os: macos-latest - python-version: '3.11' - - os: windows-latest - python-version: '3.11' + - os: macos-latest + python-version: "3.11" + - os: windows-latest + python-version: "3.11" steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} - - run: pip install .[tests] + - run: pip install .[tests] - - run: pytest + - run: pytest diff --git a/.github/workflows/publish-python-package.yml b/.github/workflows/publish-python-package.yml index 8fb4447d..7816576a 100644 --- a/.github/workflows/publish-python-package.yml +++ b/.github/workflows/publish-python-package.yml @@ -8,25 +8,21 @@ jobs: release: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 - - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' + - name: Install builder + run: pip install build - - name: Update pip - run: pip install -U pip + - name: Build package + run: pyproject-build --outdir dist/ . - - name: Install builder - run: pip install build - - - name: Build package - run: pyproject-build --outdir dist/ . - - - name: Publish package - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.lgtm.yml b/.lgtm.yml index 1d233433..e0900d35 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -1,6 +1,6 @@ path_classifiers: test: - - exclude: scripts + - exclude: scripts extraction: python: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e883db59..842572a6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,17 @@ repos: - types-requests - xarray + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.3.2 + hooks: + - id: prettier + + - repo: https://github.com/markdownlint/markdownlint + rev: v0.11.0 + hooks: + - id: markdownlint + args: ["--rules", "~MD013,~MD032"] + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: diff --git a/README.md b/README.md index fe19cc74..9c74c02e 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ Thanks to our [contributors](./.github/contributors.md). ## Similar toolboxes in other code languages -* [Matlab, GNU Octave](https://github.com/geospace-code/matmap3d) -* [Fortran](https://github.com/geospace-code/maptran3d) -* [Rust](https://github.com/gberrante/map_3d) +- [Matlab, GNU Octave](https://github.com/geospace-code/matmap3d) +- [Fortran](https://github.com/geospace-code/maptran3d) +- [Rust](https://github.com/gberrante/map_3d) ## Prerequisites @@ -112,18 +112,18 @@ dist_m, azimuth_deg = pmv.vdist(lat1, lon1, lat2, lon2) Additional functions: -* loxodrome_inverse: rhumb line distance and azimuth between ellipsoid points (lat,lon) akin to Matlab `distance('rh', ...)` and `azimuth('rh', ...)` -* loxodrome_direct -* geodetic latitude transforms to/from: parametric, authalic, isometric, and more in pymap3d.latitude +- loxodrome_inverse: rhumb line distance and azimuth between ellipsoid points (lat,lon) akin to Matlab `distance('rh', ...)` and `azimuth('rh', ...)` +- loxodrome_direct +- geodetic latitude transforms to/from: parametric, authalic, isometric, and more in pymap3d.latitude Abbreviations: -* [AER: Azimuth, Elevation, Range](https://en.wikipedia.org/wiki/Spherical_coordinate_system) -* [ECEF: Earth-centered, Earth-fixed](https://en.wikipedia.org/wiki/ECEF) -* [ECI: Earth-centered Inertial using IERS](https://www.iers.org/IERS/EN/Home/home_node.html) via `astropy` -* [ENU: East North Up](https://en.wikipedia.org/wiki/Axes_conventions#Ground_reference_frames:_ENU_and_NED) -* [NED: North East Down](https://en.wikipedia.org/wiki/North_east_down) -* [radec: right ascension, declination](https://en.wikipedia.org/wiki/Right_ascension) +- [AER: Azimuth, Elevation, Range](https://en.wikipedia.org/wiki/Spherical_coordinate_system) +- [ECEF: Earth-centered, Earth-fixed](https://en.wikipedia.org/wiki/ECEF) +- [ECI: Earth-centered Inertial using IERS](https://www.iers.org/IERS/EN/Home/home_node.html) via `astropy` +- [ENU: East North Up](https://en.wikipedia.org/wiki/Axes_conventions#Ground_reference_frames:_ENU_and_NED) +- [NED: North East Down](https://en.wikipedia.org/wiki/North_east_down) +- [radec: right ascension, declination](https://en.wikipedia.org/wiki/Right_ascension) ### Ellipsoid @@ -160,20 +160,20 @@ As noted above, use list comprehension if you need vector data without Numpy. ### Caveats -* Atmospheric effects neglected in all functions not invoking AstroPy. +- Atmospheric effects neglected in all functions not invoking AstroPy. Would need to update code to add these input parameters (just start a GitHub Issue to request). -* Planetary perturbations and nutation etc. not fully considered. +- Planetary perturbations and nutation etc. not fully considered. ## Notes As compared to [PyProj](https://github.com/jswhit/pyproj): -* PyMap3D does not require anything beyond pure Python for most transforms -* Astronomical conversions are done using (optional) AstroPy for established accuracy -* PyMap3D API is similar to Matlab Mapping Toolbox, while PyProj's interface is quite distinct -* PyMap3D intrinsically handles local coordinate systems such as ENU, +- PyMap3D does not require anything beyond pure Python for most transforms +- Astronomical conversions are done using (optional) AstroPy for established accuracy +- PyMap3D API is similar to Matlab Mapping Toolbox, while PyProj's interface is quite distinct +- PyMap3D intrinsically handles local coordinate systems such as ENU, while PyProj ENU requires some [additional effort](https://github.com/jswhit/pyproj/issues/105). -* PyProj is oriented towards points on the planet surface, while PyMap3D handles points on or above the planet surface equally well, particularly important for airborne vehicles and remote sensing. +- PyProj is oriented towards points on the planet surface, while PyMap3D handles points on or above the planet surface equally well, particularly important for airborne vehicles and remote sensing. ### AstroPy.Units.Quantity diff --git a/codemeta.json b/codemeta.json index 55114f50..93a96df5 100644 --- a/codemeta.json +++ b/codemeta.json @@ -15,21 +15,17 @@ "applicationCategory": "geospace", "developmentStatus": "active", "funder": { - "@type": "Organization", - "name": "AFOSR" + "@type": "Organization", + "name": "AFOSR" }, - "keywords": [ - "coordinate transformation" - ], - "programmingLanguage": [ - "Python" - ], + "keywords": ["coordinate transformation"], + "programmingLanguage": ["Python"], "author": [ - { - "@type": "Person", - "@id": "https://orcid.org/0000-0002-1637-6526", - "givenName": "Michael", - "familyName": "Hirsch" - } + { + "@type": "Person", + "@id": "https://orcid.org/0000-0002-1637-6526", + "givenName": "Michael", + "familyName": "Hirsch" + } ] } diff --git a/paper/paper.md b/paper/paper.md index 476923f4..5cbe4de8 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -1,15 +1,15 @@ --- -title: 'PyMap3D: 3-D coordinate conversions for terrestrial and geospace environments' +title: "PyMap3D: 3-D coordinate conversions for terrestrial and geospace environments" tags: authors: - name: Michael Hirsch orcid: 0000-0002-1637-6526 affiliation: "1, 2" affiliations: - - name: Boston University ECE Dept. - index: 1 - - name: SciVision, Inc. - index: 2 + - name: Boston University ECE Dept. + index: 1 + - name: SciVision, Inc. + index: 2 date: 29 January 2018 bibliography: paper.bib --- @@ -18,8 +18,8 @@ bibliography: paper.bib PyMap3D [@pymap3d] is a pure Python coordinate transformation program that converts between geographic coordinate systems and local coordinate systems useful for airborne, space and remote sensing systems. Additional standalone coordinate conversions are provided for Matlab/GNU Octave and Fortran. -A subset of PyMap3D functions using syntax compatible with the $1000 Matlab Mapping Toolbox is provided for Matlab and GNU Octave users in the ``matlab/`` directory. -A modern Fortran 2018 implementation of many of the PyMap3D routines is provided in the ``fortran/`` directory. +A subset of PyMap3D functions using syntax compatible with the $1000 Matlab Mapping Toolbox is provided for Matlab and GNU Octave users in the `matlab/` directory. +A modern Fortran 2018 implementation of many of the PyMap3D routines is provided in the `fortran/` directory. The Fortran procedures are "elemental", so they may be used for massively parallel processing of arbitrarily shaped coordinate arrays. For Python, increased performance and accuracy is optionally available for certain functions with AstroPy. @@ -31,33 +31,34 @@ whether underwater, ground-based or space-based platforms. This includes rocket launches, orbiting spacecrafts, UAVs, cameras, radars and many more. By adding ellipsoid parameters, it could be readily be used for other planets as well. The coordinate systems included are: -* ECEF (Earth centered, Earth fixed) -* ENU (East, North, Up) -* NED (North, East, Down) -* ECI (Earth Centered Inertial) -* Geodetic (Latitude, Longitude, Altitude) -* Horizontal Celestial (Alt-Az or Az-El) -* Equatorial Celestial (Right Ascension, Declination) + +- ECEF (Earth centered, Earth fixed) +- ENU (East, North, Up) +- NED (North, East, Down) +- ECI (Earth Centered Inertial) +- Geodetic (Latitude, Longitude, Altitude) +- Horizontal Celestial (Alt-Az or Az-El) +- Equatorial Celestial (Right Ascension, Declination) Additionally, Vincenty [@vincenty, @veness] geodesic distances and direction are computed. PyMap3D has already seen usage in projects including -* [EU ECSEL project 662107 SWARMs](http://swarms.eu/) -* Rafael Defense Systems DataHack 2017 -* HERA radiotelescope -* Mahali (NSF Grant: AGS-1343967) -* Solar Eclipse network (NSF Grant: AGS-1743832) -* High Speed Auroral Tomography (NSF Grant: AGS-1237376) [@7368896] + +- [EU ECSEL project 662107 SWARMs](http://swarms.eu/) +- Rafael Defense Systems DataHack 2017 +- HERA radiotelescope +- Mahali (NSF Grant: AGS-1343967) +- Solar Eclipse network (NSF Grant: AGS-1743832) +- High Speed Auroral Tomography (NSF Grant: AGS-1237376) [@7368896] ## Other Programs Other Python geodesy programs include: -* [PyGeodesy](https://github.com/mrJean1/PyGeodesy) MIT license -* [PyProj](https://github.com/jswhit/pyproj) ISC license +- [PyGeodesy](https://github.com/mrJean1/PyGeodesy) MIT license +- [PyProj](https://github.com/jswhit/pyproj) ISC license These programs are targeted for geodesy experts, and require additional packages beyond Python that may not be readily accessible to users. Further, these programs do not include all the functions of PyMap3D, and do not have the straightforward function-based API of PyMap3D. - # References From e3e886ac9af32fbcbbc3b56c11015ba3d4ca11c8 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Thu, 27 Oct 2022 15:13:07 +0200 Subject: [PATCH 04/26] Use stricter mypy configuration --- pyproject.toml | 1 + src/pymap3d/aer.py | 8 ++++---- src/pymap3d/ecef.py | 14 ++++++++------ src/pymap3d/enu.py | 6 +++--- src/pymap3d/latitude.py | 26 +++++++++++++------------- src/pymap3d/los.py | 2 +- src/pymap3d/lox.py | 12 ++++++------ src/pymap3d/ned.py | 9 ++++----- src/pymap3d/rcurve.py | 8 ++++---- src/pymap3d/rsphere.py | 14 +++++++------- src/pymap3d/spherical.py | 4 ++-- src/pymap3d/utils.py | 3 ++- src/pymap3d/vincenty.py | 6 +++--- 13 files changed, 58 insertions(+), 55 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cabd9091..23ce3981 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ files = ["src", "Examples", "scripts"] ignore_missing_imports = true strict_optional = false show_column_numbers = true +show_error_codes = true [tool.pytest.ini_options] filterwarnings = [ diff --git a/src/pymap3d/aer.py b/src/pymap3d/aer.py index f67af76f..b43dfcea 100644 --- a/src/pymap3d/aer.py +++ b/src/pymap3d/aer.py @@ -23,7 +23,7 @@ def ecef2aer( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -72,7 +72,7 @@ def geodetic2aer( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -120,7 +120,7 @@ def aer2geodetic( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -269,7 +269,7 @@ def aer2ecef( lat0, lon0, alt0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ diff --git a/src/pymap3d/ecef.py b/src/pymap3d/ecef.py index ab968a6e..de06e4ad 100644 --- a/src/pymap3d/ecef.py +++ b/src/pymap3d/ecef.py @@ -32,7 +32,7 @@ def geodetic2ecef( lat, lon, alt, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -85,7 +85,7 @@ def ecef2geodetic( x, y, z, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -253,7 +253,7 @@ def ecef2enu( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -365,7 +365,7 @@ def uvw2enu(u, v, w, lat0, lon0, deg: bool = True) -> tuple: return East, North, Up -def eci2geodetic(x, y, z, t: datetime, ell: Ellipsoid = None, *, deg: bool = True) -> tuple: +def eci2geodetic(x, y, z, t: datetime, ell: Ellipsoid | None = None, *, deg: bool = True) -> tuple: """ convert Earth Centered Internal ECI to geodetic coordinates @@ -406,7 +406,9 @@ def eci2geodetic(x, y, z, t: datetime, ell: Ellipsoid = None, *, deg: bool = Tru return ecef2geodetic(xecef, yecef, zecef, ell, deg) -def geodetic2eci(lat, lon, alt, t: datetime, ell: Ellipsoid = None, *, deg: bool = True) -> tuple: +def geodetic2eci( + lat, lon, alt, t: datetime, ell: Ellipsoid | None = None, *, deg: bool = True +) -> tuple: """ convert geodetic coordinates to Earth Centered Internal ECI @@ -454,7 +456,7 @@ def enu2ecef( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ diff --git a/src/pymap3d/enu.py b/src/pymap3d/enu.py index 67b40e4c..33d49630 100644 --- a/src/pymap3d/enu.py +++ b/src/pymap3d/enu.py @@ -15,7 +15,7 @@ __all__ = ["enu2aer", "aer2enu", "enu2geodetic", "geodetic2enu"] -def enu2aer(e, n, u, deg: bool = True) -> tuple: +def enu2aer(e, n, u, deg=True): """ ENU to Azimuth, Elevation, Range @@ -115,7 +115,7 @@ def enu2geodetic( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -163,7 +163,7 @@ def geodetic2enu( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ diff --git a/src/pymap3d/latitude.py b/src/pymap3d/latitude.py index e74b9152..37dbc029 100644 --- a/src/pymap3d/latitude.py +++ b/src/pymap3d/latitude.py @@ -45,7 +45,7 @@ def geoc2geod( geocentric_lat, geocentric_distance, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ): """ @@ -89,7 +89,7 @@ def geoc2geod( return degrees(geodetic_lat) if deg else geodetic_lat -def geodetic2geocentric(geodetic_lat, alt_m, ell: Ellipsoid = None, deg: bool = True): +def geodetic2geocentric(geodetic_lat, alt_m, ell: Ellipsoid | None = None, deg: bool = True): """ convert geodetic latitude to geocentric latitude on spheroid surface @@ -128,7 +128,7 @@ def geodetic2geocentric(geodetic_lat, alt_m, ell: Ellipsoid = None, deg: bool = geod2geoc = geodetic2geocentric -def geocentric2geodetic(geocentric_lat, alt_m, ell: Ellipsoid = None, deg: bool = True): +def geocentric2geodetic(geocentric_lat, alt_m, ell: Ellipsoid | None = None, deg: bool = True): """ converts from geocentric latitude to geodetic latitude @@ -164,7 +164,7 @@ def geocentric2geodetic(geocentric_lat, alt_m, ell: Ellipsoid = None, deg: bool return degrees(geodetic_lat) if deg else geodetic_lat -def geodetic2isometric(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): +def geodetic2isometric(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): """ computes isometric latitude on an ellipsoid @@ -224,7 +224,7 @@ def geodetic2isometric(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): return isometric_lat -def isometric2geodetic(isometric_lat, ell: Ellipsoid = None, deg: bool = True): +def isometric2geodetic(isometric_lat, ell: Ellipsoid | None = None, deg: bool = True): """ converts from isometric latitude to geodetic latitude @@ -260,7 +260,7 @@ def isometric2geodetic(isometric_lat, ell: Ellipsoid = None, deg: bool = True): return degrees(geodetic_lat) if deg else geodetic_lat -def conformal2geodetic(conformal_lat, ell: Ellipsoid = None, deg: bool = True): +def conformal2geodetic(conformal_lat, ell: Ellipsoid | None = None, deg: bool = True): """ converts from conformal latitude to geodetic latitude @@ -305,7 +305,7 @@ def conformal2geodetic(conformal_lat, ell: Ellipsoid = None, deg: bool = True): return degrees(geodetic_lat) if deg else geodetic_lat -def geodetic2conformal(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): +def geodetic2conformal(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): """ converts from geodetic latitude to conformal latitude @@ -351,7 +351,7 @@ def geodetic2conformal(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): # %% rectifying -def geodetic2rectifying(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): +def geodetic2rectifying(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): """ converts from geodetic latitude to rectifying latitude @@ -397,7 +397,7 @@ def geodetic2rectifying(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): return degrees(rectifying_lat) if deg else rectifying_lat -def rectifying2geodetic(rectifying_lat, ell: Ellipsoid = None, deg: bool = True): +def rectifying2geodetic(rectifying_lat, ell: Ellipsoid | None = None, deg: bool = True): """ converts from rectifying latitude to geodetic latitude @@ -443,7 +443,7 @@ def rectifying2geodetic(rectifying_lat, ell: Ellipsoid = None, deg: bool = True) # %% authalic -def geodetic2authalic(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): +def geodetic2authalic(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): """ converts from geodetic latitude to authalic latitude @@ -487,7 +487,7 @@ def geodetic2authalic(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): return degrees(authalic_lat) if deg else authalic_lat -def authalic2geodetic(authalic_lat, ell: Ellipsoid = None, deg: bool = True): +def authalic2geodetic(authalic_lat, ell: Ellipsoid | None = None, deg: bool = True): """ converts from authalic latitude to geodetic latitude @@ -530,7 +530,7 @@ def authalic2geodetic(authalic_lat, ell: Ellipsoid = None, deg: bool = True): # %% parametric -def geodetic2parametric(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): +def geodetic2parametric(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): """ converts from geodetic latitude to parametric latitude @@ -564,7 +564,7 @@ def geodetic2parametric(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): return degrees(parametric_lat) if deg else parametric_lat -def parametric2geodetic(parametric_lat, ell: Ellipsoid = None, deg: bool = True): +def parametric2geodetic(parametric_lat, ell: Ellipsoid | None = None, deg: bool = True): """ converts from parametric latitude to geodetic latitude diff --git a/src/pymap3d/los.py b/src/pymap3d/los.py index d4e05c7c..7640ef1b 100644 --- a/src/pymap3d/los.py +++ b/src/pymap3d/los.py @@ -23,7 +23,7 @@ def lookAtSpheroid( h0, az, tilt, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ diff --git a/src/pymap3d/lox.py b/src/pymap3d/lox.py index 37055ec7..02a10e2d 100644 --- a/src/pymap3d/lox.py +++ b/src/pymap3d/lox.py @@ -34,7 +34,7 @@ COS_EPS = 1e-9 -def meridian_dist(lat, ell: Ellipsoid = None, deg: bool = True) -> float: +def meridian_dist(lat, ell: Ellipsoid | None = None, deg: bool = True) -> float: """ Computes the ground distance on an ellipsoid from the equator to the input latitude. @@ -55,7 +55,7 @@ def meridian_dist(lat, ell: Ellipsoid = None, deg: bool = True) -> float: return meridian_arc(0.0, lat, ell, deg) -def meridian_arc(lat1, lat2, ell: Ellipsoid = None, deg: bool = True) -> float: +def meridian_arc(lat1, lat2, ell: Ellipsoid | None = None, deg: bool = True) -> float: """ Computes the ground distance on an ellipsoid between two latitudes. @@ -88,7 +88,7 @@ def loxodrome_inverse( lon1, lat2, lon2, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple[float, float]: """ @@ -180,7 +180,7 @@ def loxodrome_direct( lon1, rng, a12, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -261,7 +261,7 @@ def loxodrome_direct( return lat2, lon2 -def departure(lon1, lon2, lat, ell: Ellipsoid = None, deg: bool = True) -> float: +def departure(lon1, lon2, lat, ell: Ellipsoid | None = None, deg: bool = True) -> float: """ Computes the distance along a specific parallel between two meridians. @@ -289,7 +289,7 @@ def departure(lon1, lon2, lat, ell: Ellipsoid = None, deg: bool = True) -> float return rcurve.parallel(lat, ell=ell, deg=False) * (abs(lon2 - lon1) % pi) -def meanm(lat, lon, ell: Ellipsoid = None, deg: bool = True) -> tuple: +def meanm(lat, lon, ell: Ellipsoid | None = None, deg: bool = True) -> tuple: """ Computes geographic mean for geographic points on an ellipsoid diff --git a/src/pymap3d/ned.py b/src/pymap3d/ned.py index c8885767..bf66a43d 100644 --- a/src/pymap3d/ned.py +++ b/src/pymap3d/ned.py @@ -1,5 +1,4 @@ """ Transforms involving NED North East Down """ - from __future__ import annotations from .ecef import ecef2enu, ecef2enuv, ecef2geodetic, enu2ecef @@ -73,7 +72,7 @@ def ned2geodetic( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -122,7 +121,7 @@ def ned2ecef( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -168,7 +167,7 @@ def ecef2ned( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -217,7 +216,7 @@ def geodetic2ned( lat0, lon0, h0, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ diff --git a/src/pymap3d/rcurve.py b/src/pymap3d/rcurve.py index d3ec1cda..87d056c0 100644 --- a/src/pymap3d/rcurve.py +++ b/src/pymap3d/rcurve.py @@ -9,7 +9,7 @@ __all__ = ["parallel", "meridian", "transverse", "geocentric_radius"] -def geocentric_radius(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): +def geocentric_radius(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): """ compute geocentric radius at geodetic latitude @@ -29,7 +29,7 @@ def geocentric_radius(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): ) -def parallel(lat, ell: Ellipsoid = None, deg: bool = True) -> float: +def parallel(lat, ell: Ellipsoid | None = None, deg: bool = True) -> float: """ computes the radius of the small circle encompassing the globe at the specified latitude @@ -55,7 +55,7 @@ def parallel(lat, ell: Ellipsoid = None, deg: bool = True) -> float: return cos(lat) * transverse(lat, ell, deg=False) -def meridian(lat, ell: Ellipsoid = None, deg: bool = True): +def meridian(lat, ell: Ellipsoid | None = None, deg: bool = True): """computes the meridional radius of curvature for the ellipsoid like Matlab rcurve('meridian', ...) @@ -82,7 +82,7 @@ def meridian(lat, ell: Ellipsoid = None, deg: bool = True): return f1 / sqrt(f2**3) -def transverse(lat, ell: Ellipsoid = None, deg: bool = True): +def transverse(lat, ell: Ellipsoid | None = None, deg: bool = True): """computes the radius of the curve formed by a plane intersecting the ellipsoid at the latitude which is normal to the surface of the ellipsoid diff --git a/src/pymap3d/rsphere.py b/src/pymap3d/rsphere.py index 020dce5d..6c775059 100644 --- a/src/pymap3d/rsphere.py +++ b/src/pymap3d/rsphere.py @@ -23,7 +23,7 @@ ] -def eqavol(ell: Ellipsoid = None) -> float: +def eqavol(ell: Ellipsoid | None = None) -> float: """computes the radius of the sphere with equal volume as the ellipsoid Parameters @@ -44,7 +44,7 @@ def eqavol(ell: Ellipsoid = None) -> float: return ell.semimajor_axis * (1 - f / 3 - f**2 / 9) -def authalic(ell: Ellipsoid = None) -> float: +def authalic(ell: Ellipsoid | None = None) -> float: """computes the radius of the sphere with equal surface area as the ellipsoid Parameters @@ -71,7 +71,7 @@ def authalic(ell: Ellipsoid = None) -> float: return ell.semimajor_axis -def rectifying(ell: Ellipsoid = None) -> float: +def rectifying(ell: Ellipsoid | None = None) -> float: """computes the radius of the sphere with equal meridional distances as the ellipsoid Parameters @@ -94,7 +94,7 @@ def euler( lon1, lat2, lon2, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ): """computes the Euler radii of curvature at the midpoint of the @@ -140,7 +140,7 @@ def euler( return rho * nu / den -def curve(lat, ell: Ellipsoid = None, deg: bool = True, method: str = "mean"): +def curve(lat, ell: Ellipsoid | None = None, deg: bool = True, method: str = "mean"): """computes the arithmetic average of the transverse and meridional radii of curvature at a specified latitude point @@ -175,7 +175,7 @@ def curve(lat, ell: Ellipsoid = None, deg: bool = True, method: str = "mean"): raise ValueError("method must be mean or norm") -def triaxial(ell: Ellipsoid = None, method: str = "mean") -> float: +def triaxial(ell: Ellipsoid | None = None, method: str = "mean") -> float: """computes triaxial average of the semimajor and semiminor axes of the ellipsoid Parameters @@ -202,7 +202,7 @@ def triaxial(ell: Ellipsoid = None, method: str = "mean") -> float: raise ValueError("method must be mean or norm") -def biaxial(ell: Ellipsoid = None, method: str = "mean") -> float: +def biaxial(ell: Ellipsoid | None = None, method: str = "mean") -> float: """computes biaxial average of the semimajor and semiminor axes of the ellipsoid Parameters diff --git a/src/pymap3d/spherical.py b/src/pymap3d/spherical.py index 5c1b30e4..aa99ea4b 100644 --- a/src/pymap3d/spherical.py +++ b/src/pymap3d/spherical.py @@ -19,7 +19,7 @@ def geodetic2spherical( lat, lon, alt, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ @@ -90,7 +90,7 @@ def spherical2geodetic( lat, lon, radius, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, deg: bool = True, ) -> tuple: """ diff --git a/src/pymap3d/utils.py b/src/pymap3d/utils.py index bbc8a324..66d4dd58 100644 --- a/src/pymap3d/utils.py +++ b/src/pymap3d/utils.py @@ -8,6 +8,7 @@ try: from numpy import asarray + from numpy.typing import ArrayLike except ImportError: pass @@ -45,7 +46,7 @@ def sph2cart(az, el, r) -> tuple: return x, y, z -def sanitize(lat, ell: Ellipsoid | None, deg: bool) -> tuple: +def sanitize(lat, ell: Ellipsoid | None, deg: bool) -> tuple[float | ArrayLike, Ellipsoid]: if ell is None: ell = Ellipsoid.from_name("wgs84") diff --git a/src/pymap3d/vincenty.py b/src/pymap3d/vincenty.py index 537d0d14..7f017fc1 100644 --- a/src/pymap3d/vincenty.py +++ b/src/pymap3d/vincenty.py @@ -36,7 +36,7 @@ def vdist( Lon1, Lat2, Lon2, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, ) -> tuple: """ Using the reference ellipsoid, compute the distance between two points @@ -278,7 +278,7 @@ def vreckon( Lon1, Rng, Azim, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, ) -> tuple: """ This is the Vincenty "forward" solution. @@ -462,7 +462,7 @@ def track2( lon1, lat2, lon2, - ell: Ellipsoid = None, + ell: Ellipsoid | None = None, npts: int = 100, deg: bool = True, ) -> tuple[list, list]: From 86cc496257cadc00c00430954996cc872019b2bd Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Fri, 28 Oct 2022 14:00:06 +0200 Subject: [PATCH 05/26] Use mypy strict mode --- .flake8 | 2 + .pre-commit-config.yaml | 6 + Examples/compare/compare_vdist.py | 6 +- Examples/plot_geodetic2ecef.py | 5 +- pyproject.toml | 2 +- scripts/benchmark_vincenty.py | 2 +- src/pymap3d/__init__.py | 2 +- src/pymap3d/_types.py | 8 + src/pymap3d/aer.py | 288 +++++++++++++++--- src/pymap3d/azelradec.py | 55 +++- src/pymap3d/ecef.py | 335 ++++++++++++++++++--- src/pymap3d/eci.py | 44 ++- src/pymap3d/ellipsoid.py | 13 +- src/pymap3d/enu.py | 156 ++++++++-- src/pymap3d/haversine.py | 114 ++++++- src/pymap3d/latitude.py | 324 ++++++++++++++++++-- src/pymap3d/los.py | 55 +++- src/pymap3d/lox.py | 126 ++++++-- src/pymap3d/mathfun.py | 13 +- src/pymap3d/ned.py | 290 +++++++++++++++--- src/pymap3d/rcurve.py | 70 ++++- src/pymap3d/rsphere.py | 81 ++++- src/pymap3d/sidereal.py | 46 ++- src/pymap3d/spherical.py | 81 ++++- src/pymap3d/tests/test_aer.py | 43 ++- src/pymap3d/tests/test_eci.py | 28 +- src/pymap3d/tests/test_ellipsoid.py | 14 +- src/pymap3d/tests/test_enu.py | 22 +- src/pymap3d/tests/test_geodetic.py | 46 ++- src/pymap3d/tests/test_latitude.py | 30 +- src/pymap3d/tests/test_look_spheroid.py | 16 +- src/pymap3d/tests/test_ned.py | 28 +- src/pymap3d/tests/test_pyproj.py | 6 +- src/pymap3d/tests/test_rcurve.py | 10 +- src/pymap3d/tests/test_rhumb.py | 29 +- src/pymap3d/tests/test_rsphere.py | 18 +- src/pymap3d/tests/test_sidereal.py | 20 +- src/pymap3d/tests/test_sky.py | 8 +- src/pymap3d/tests/test_spherical.py | 47 +-- src/pymap3d/tests/test_time.py | 24 +- src/pymap3d/tests/test_vincenty.py | 2 +- src/pymap3d/tests/test_vincenty_dist.py | 6 +- src/pymap3d/tests/test_vincenty_vreckon.py | 10 +- src/pymap3d/timeconv.py | 47 ++- src/pymap3d/utils.py | 83 ++++- src/pymap3d/vallado.py | 51 ++++ src/pymap3d/vincenty.py | 162 +++++++--- 47 files changed, 2395 insertions(+), 479 deletions(-) create mode 100644 src/pymap3d/_types.py diff --git a/.flake8 b/.flake8 index d7fa0faf..3a566e3d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,5 @@ [flake8] ignore = E501, W503 exclude = .git,__pycache__,.eggs/,doc/,docs/,build/,dist/,archive/,env/ +per-file-ignores = + test_eci.py:F401 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 842572a6..52cedb48 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,3 +56,9 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - id: trailing-whitespace + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-check-blanket-type-ignore + - id: python-no-eval diff --git a/Examples/compare/compare_vdist.py b/Examples/compare/compare_vdist.py index 6dc7127e..6c7fc489 100644 --- a/Examples/compare/compare_vdist.py +++ b/Examples/compare/compare_vdist.py @@ -18,13 +18,13 @@ has_map = matlab_mapping(eng) -def distance(lat1, lon1, lat2, lon2) -> tuple[float, float]: +def distance(lat1: float, lon1: float, lat2: float, lon2: float) -> tuple[float, float]: """Using Matlab Engine to do same thing as Pymap3d""" if has_map: - return eng.distance(lat1, lon1, lat2, lon2, eng.wgs84Ellipsoid(), nargout=2) + return eng.distance(lat1, lon1, lat2, lon2, eng.wgs84Ellipsoid(), nargout=2) # type: ignore[no-any-return] else: - return eng.matmap3d.vdist(lat1, lon1, lat2, lon2, nargout=2) + return eng.matmap3d.vdist(lat1, lon1, lat2, lon2, nargout=2) # type: ignore[no-any-return] dlast, alast = nan, nan diff --git a/Examples/plot_geodetic2ecef.py b/Examples/plot_geodetic2ecef.py index 16c94876..d920c5b6 100644 --- a/Examples/plot_geodetic2ecef.py +++ b/Examples/plot_geodetic2ecef.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse +from typing import Any import matplotlib.pyplot as mpl import numpy as np @@ -15,7 +18,7 @@ x, y, z = pm.geodetic2ecef(lat, lon, args.alt_m) -def panel(ax, val, name: str, cmap: str = None): +def panel(ax: Any, val: Any, name: str, cmap: str | None = None) -> None: hi = ax.pcolormesh(lon, lat, val, cmap=cmap) ax.set_title(name) fg.colorbar(hi, ax=ax).set_label(name + " [m]") diff --git a/pyproject.toml b/pyproject.toml index 23ce3981..f8d1f9ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,8 @@ known_third_party = ["pymap3d"] [tool.mypy] files = ["src", "Examples", "scripts"] +strict = true ignore_missing_imports = true -strict_optional = false show_column_numbers = true show_error_codes = true diff --git a/scripts/benchmark_vincenty.py b/scripts/benchmark_vincenty.py index be1688d8..0ab40c9a 100755 --- a/scripts/benchmark_vincenty.py +++ b/scripts/benchmark_vincenty.py @@ -36,7 +36,7 @@ def bench_vreckon(N: int) -> float: az = np.random.random(N) tic = time.monotonic() - _, _ = vreckon(ll0[0], ll0[1], sr, az) + _, _ = vreckon(ll0[0], ll0[1], sr, az) # type: ignore[call-overload] return time.monotonic() - tic diff --git a/src/pymap3d/__init__.py b/src/pymap3d/__init__.py index 1d5a4c03..bed599b1 100644 --- a/src/pymap3d/__init__.py +++ b/src/pymap3d/__init__.py @@ -100,4 +100,4 @@ __all__ += ["aer2eci", "eci2aer", "ecef2eci", "eci2ecef"] except ImportError: - from .vallado import azel2radec, radec2azel + from .vallado import azel2radec, radec2azel # type: ignore[no-redef] diff --git a/src/pymap3d/_types.py b/src/pymap3d/_types.py new file mode 100644 index 00000000..2d1ad120 --- /dev/null +++ b/src/pymap3d/_types.py @@ -0,0 +1,8 @@ +from typing import Any, Sequence, Union + +try: + from numpy.typing import NDArray +except ImportError: + pass + +ArrayLike = Union[Sequence[float], "NDArray[Any]"] diff --git a/src/pymap3d/aer.py b/src/pymap3d/aer.py index b43dfcea..141757b2 100644 --- a/src/pymap3d/aer.py +++ b/src/pymap3d/aer.py @@ -3,29 +3,61 @@ from __future__ import annotations from datetime import datetime - -from .ecef import ecef2enu, ecef2geodetic, enu2uvw, geodetic2ecef -from .ellipsoid import Ellipsoid -from .enu import aer2enu, enu2aer, geodetic2enu +from typing import Any, overload try: + from numpy.typing import NDArray + from .eci import ecef2eci, eci2ecef except ImportError: pass +from ._types import ArrayLike +from .ecef import ecef2enu, ecef2geodetic, enu2uvw, geodetic2ecef +from .ellipsoid import Ellipsoid +from .enu import aer2enu, enu2aer, geodetic2enu + __all__ = ["aer2ecef", "ecef2aer", "geodetic2aer", "aer2geodetic", "eci2aer", "aer2eci"] +@overload def ecef2aer( - x, - y, - z, - lat0, - lon0, - h0, + x: float, + y: float, + z: float, + lat0: float, + lon0: float, + h0: float, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float]: + pass + + +@overload +def ecef2aer( + x: ArrayLike, + y: ArrayLike, + z: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def ecef2aer( + x: float | ArrayLike, + y: float | ArrayLike, + z: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ compute azimuth, elevation and slant range from an Observer to a Point with ECEF coordinates. @@ -60,21 +92,49 @@ def ecef2aer( srange : float slant range [meters] """ - xEast, yNorth, zUp = ecef2enu(x, y, z, lat0, lon0, h0, ell, deg=deg) + xEast, yNorth, zUp = ecef2enu(x, y, z, lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] return enu2aer(xEast, yNorth, zUp, deg=deg) +@overload def geodetic2aer( - lat, - lon, - h, - lat0, - lon0, - h0, + lat: float, + lon: float, + h: float, + lat0: float, + lon0: float, + h0: float, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float]: + pass + + +@overload +def geodetic2aer( + lat: ArrayLike, + lon: ArrayLike, + h: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def geodetic2aer( + lat: float | ArrayLike, + lon: float | ArrayLike, + h: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ gives azimuth, elevation and slant range from an Observer to a Point with geodetic coordinates. @@ -108,21 +168,49 @@ def geodetic2aer( srange : float slant range [meters] """ - e, n, u = geodetic2enu(lat, lon, h, lat0, lon0, h0, ell, deg=deg) + e, n, u = geodetic2enu(lat, lon, h, lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] return enu2aer(e, n, u, deg=deg) +@overload +def aer2geodetic( + az: float, + el: float, + srange: float, + lat0: float, + lon0: float, + h0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def aer2geodetic( + az: ArrayLike, + el: ArrayLike, + srange: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def aer2geodetic( - az, - el, - srange, - lat0, - lon0, - h0, + az: float | ArrayLike, + el: float | ArrayLike, + srange: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ gives geodetic coordinates of a point with az, el, range from an observer at lat0, lon0, h0 @@ -158,12 +246,52 @@ def aer2geodetic( alt : float altitude above ellipsoid (meters) """ - x, y, z = aer2ecef(az, el, srange, lat0, lon0, h0, ell=ell, deg=deg) + x, y, z = aer2ecef(az, el, srange, lat0, lon0, h0, ell=ell, deg=deg) # type: ignore[misc, arg-type] return ecef2geodetic(x, y, z, ell=ell, deg=deg) -def eci2aer(x, y, z, lat0, lon0, h0, t: datetime, *, deg: bool = True) -> tuple: +@overload +def eci2aer( + x: float, + y: float, + z: float, + lat0: float, + lon0: float, + h0: float, + t: datetime, + *, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def eci2aer( + x: ArrayLike, + y: ArrayLike, + z: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + t: datetime, + *, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def eci2aer( + x: float | ArrayLike, + y: float | ArrayLike, + z: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, + t: datetime, + *, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ takes Earth Centered Inertial x,y,z ECI coordinates of point and gives az, el, slant range from Observer @@ -198,25 +326,57 @@ def eci2aer(x, y, z, lat0, lon0, h0, t: datetime, *, deg: bool = True) -> tuple: """ try: - xecef, yecef, zecef = eci2ecef(x, y, z, t) + xecef, yecef, zecef = eci2ecef(x, y, z, t) # type: ignore[arg-type] except NameError: raise ImportError("pip install numpy") - return ecef2aer(xecef, yecef, zecef, lat0, lon0, h0, deg=deg) + return ecef2aer(xecef, yecef, zecef, lat0, lon0, h0, deg=deg) # type: ignore[arg-type] + + +@overload +def aer2eci( + az: float, + el: float, + srange: float, + lat0: float, + lon0: float, + h0: float, + t: datetime, + ell: Ellipsoid | None = None, + *, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def aer2eci( + az: ArrayLike, + el: ArrayLike, + srange: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + t: datetime, + ell: Ellipsoid | None = None, + *, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass def aer2eci( - az, - el, - srange, - lat0, - lon0, - h0, + az: float | ArrayLike, + el: float | ArrayLike, + srange: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, t: datetime, - ell=None, + ell: Ellipsoid | None = None, *, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ gives ECI of a point from an observer at az, el, slant range @@ -254,7 +414,7 @@ def aer2eci( ECEF z coordinate (meters) """ - x, y, z = aer2ecef(az, el, srange, lat0, lon0, h0, ell, deg=deg) + x, y, z = aer2ecef(az, el, srange, lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] try: return ecef2eci(x, y, z, t) @@ -262,16 +422,44 @@ def aer2eci( raise ImportError("pip install numpy") +@overload +def aer2ecef( + az: float, + el: float, + srange: float, + lat0: float, + lon0: float, + alt0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def aer2ecef( + az: ArrayLike, + el: ArrayLike, + srange: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + alt0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def aer2ecef( - az, - el, - srange, - lat0, - lon0, - alt0, + az: float | ArrayLike, + el: float | ArrayLike, + srange: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + alt0: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ converts target azimuth, elevation, range from observer at lat0,lon0,alt0 to ECEF coordinates. @@ -312,10 +500,10 @@ def aer2ecef( if srange==NaN, z=NaN """ # Origin of the local system in geocentric coordinates. - x0, y0, z0 = geodetic2ecef(lat0, lon0, alt0, ell, deg=deg) + x0, y0, z0 = geodetic2ecef(lat0, lon0, alt0, ell, deg=deg) # type: ignore[misc, arg-type] # Convert Local Spherical AER to ENU - e1, n1, u1 = aer2enu(az, el, srange, deg=deg) + e1, n1, u1 = aer2enu(az, el, srange, deg=deg) # type: ignore[arg-type] # Rotating ENU to ECEF - dx, dy, dz = enu2uvw(e1, n1, u1, lat0, lon0, deg=deg) + dx, dy, dz = enu2uvw(e1, n1, u1, lat0, lon0, deg=deg) # type: ignore[arg-type] # Origin + offset from origin equals position in ECEF return x0 + dx, y0 + dy, z0 + dz diff --git a/src/pymap3d/azelradec.py b/src/pymap3d/azelradec.py index 73b286d0..2b714946 100644 --- a/src/pymap3d/azelradec.py +++ b/src/pymap3d/azelradec.py @@ -5,7 +5,14 @@ from __future__ import annotations from datetime import datetime +from typing import Any, overload +try: + from numpy.typing import NDArray +except ImportError: + pass + +from ._types import ArrayLike from .timeconv import str2dt # astropy can't handle xarray times (yet) from .vallado import azel2radec as vazel2radec from .vallado import radec2azel as vradec2azel @@ -20,6 +27,7 @@ __all__ = ["radec2azel", "azel2radec"] +@overload def azel2radec( az_deg: float, el_deg: float, @@ -27,6 +35,27 @@ def azel2radec( lon_deg: float, time: datetime, ) -> tuple[float, float]: + pass + + +@overload +def azel2radec( + az_deg: ArrayLike, + el_deg: ArrayLike, + lat_deg: float, + lon_deg: float, + time: datetime, +) -> tuple[NDArray[Any], NDArray[Any]]: + pass + + +def azel2radec( + az_deg: float | ArrayLike, + el_deg: float | ArrayLike, + lat_deg: float, + lon_deg: float, + time: datetime, +) -> tuple[float, float] | tuple[NDArray[Any], NDArray[Any]]: """ viewing angle (az, el) to sky coordinates (ra, dec) @@ -63,9 +92,10 @@ def azel2radec( return sky.ra.deg, sky.dec.deg except NameError: - return vazel2radec(az_deg, el_deg, lat_deg, lon_deg, time) + return vazel2radec(az_deg, el_deg, lat_deg, lon_deg, time) # type: ignore[arg-type] +@overload def radec2azel( ra_deg: float, dec_deg: float, @@ -73,6 +103,27 @@ def radec2azel( lon_deg: float, time: datetime, ) -> tuple[float, float]: + pass + + +@overload +def radec2azel( + ra_deg: ArrayLike, + dec_deg: ArrayLike, + lat_deg: float, + lon_deg: float, + time: datetime, +) -> tuple[NDArray[Any], NDArray[Any]]: + pass + + +def radec2azel( + ra_deg: float | ArrayLike, + dec_deg: float | ArrayLike, + lat_deg: float, + lon_deg: float, + time: datetime, +) -> tuple[float, float] | tuple[NDArray[Any], NDArray[Any]]: """ sky coordinates (ra, dec) to viewing angle (az, el) @@ -104,4 +155,4 @@ def radec2azel( return altaz.az.degree, altaz.alt.degree except NameError: - return vradec2azel(ra_deg, dec_deg, lat_deg, lon_deg, time) + return vradec2azel(ra_deg, dec_deg, lat_deg, lon_deg, time) # type: ignore[arg-type] diff --git a/src/pymap3d/ecef.py b/src/pymap3d/ecef.py index de06e4ad..9d6b6e11 100644 --- a/src/pymap3d/ecef.py +++ b/src/pymap3d/ecef.py @@ -1,8 +1,11 @@ """ Transforms involving ECEF: earth-centered, earth-fixed frame """ from __future__ import annotations +from typing import Any, Sequence, overload + try: from numpy import asarray, finfo, where + from numpy.typing import NDArray from .eci import ecef2eci, eci2ecef except ImportError: @@ -11,6 +14,7 @@ from datetime import datetime from math import pi +from ._types import ArrayLike from .ellipsoid import Ellipsoid from .mathfun import atan, atan2, cos, degrees, hypot, radians, sin, sqrt, tan from .utils import sanitize @@ -28,13 +32,35 @@ ] +@overload +def geodetic2ecef( + lat: float, + lon: float, + alt: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def geodetic2ecef( + lat: ArrayLike, + lon: ArrayLike, + alt: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def geodetic2ecef( - lat, - lon, - alt, + lat: float | ArrayLike, + lon: float | ArrayLike, + alt: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ point transformation from Geodetic of specified ellipsoid (default WGS-84) to ECEF @@ -81,13 +107,35 @@ def geodetic2ecef( return x, y, z +@overload def ecef2geodetic( - x, - y, - z, + x: float, + y: float, + z: float, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float]: + pass + + +@overload +def ecef2geodetic( + x: ArrayLike, + y: ArrayLike, + z: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def ecef2geodetic( + x: float | ArrayLike, + y: float | ArrayLike, + z: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ convert ECEF (meters) to geodetic coordinates @@ -128,6 +176,10 @@ def ecef2geodetic( except NameError: pass + assert ( + not isinstance(x, Sequence) and not isinstance(y, Sequence) and not isinstance(z, Sequence) + ) + r = sqrt(x**2 + y**2 + z**2) E = sqrt(ell.semimajor_axis**2 - ell.semiminor_axis**2) @@ -189,7 +241,7 @@ def ecef2geodetic( ) try: - if inside.any(): # type: ignore + if inside.any(): # type: ignore[union-attr] # avoid all false assignment bug alt[inside] = -alt[inside] except (TypeError, AttributeError): @@ -203,7 +255,38 @@ def ecef2geodetic( return lat, lon, alt -def ecef2enuv(u, v, w, lat0, lon0, deg: bool = True) -> tuple: +@overload +def ecef2enuv( + u: float, + v: float, + w: float, + lat0: float, + lon0: float, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def ecef2enuv( + u: ArrayLike, + v: ArrayLike, + w: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def ecef2enuv( + u: float | ArrayLike, + v: float | ArrayLike, + w: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ VECTOR from observer to target ECEF => ENU @@ -246,16 +329,44 @@ def ecef2enuv(u, v, w, lat0, lon0, deg: bool = True) -> tuple: return uEast, vNorth, wUp +@overload +def ecef2enu( + x: float, + y: float, + z: float, + lat0: float, + lon0: float, + h0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload def ecef2enu( - x, - y, - z, - lat0, - lon0, - h0, + x: ArrayLike, + y: ArrayLike, + z: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def ecef2enu( + x: float | ArrayLike, + y: float | ArrayLike, + z: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ from observer to target, ECEF => ENU @@ -288,19 +399,44 @@ def ecef2enu( target up ENU coordinate (meters) """ - x0, y0, z0 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) - return uvw2enu(x - x0, y - y0, z - z0, lat0, lon0, deg=deg) + x0, y0, z0 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] + + return uvw2enu(x - x0, y - y0, z - z0, lat0, lon0, deg=deg) # type: ignore[misc, arg-type, operator] + + +@overload +def enu2uvw( + east: float, + north: float, + up: float, + lat0: float, + lon0: float, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def enu2uvw( + east: ArrayLike, + north: ArrayLike, + up: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass def enu2uvw( - east, - north, - up, - lat0, - lon0, + east: float | ArrayLike, + north: float | ArrayLike, + up: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Parameters ---------- @@ -333,7 +469,38 @@ def enu2uvw( return u, v, w -def uvw2enu(u, v, w, lat0, lon0, deg: bool = True) -> tuple: +@overload +def uvw2enu( + u: float, + v: float, + w: float, + lat0: float, + lon0: float, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def uvw2enu( + u: ArrayLike, + v: ArrayLike, + w: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def uvw2enu( + u: float | ArrayLike, + v: float | ArrayLike, + w: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Parameters ---------- @@ -365,7 +532,35 @@ def uvw2enu(u, v, w, lat0, lon0, deg: bool = True) -> tuple: return East, North, Up -def eci2geodetic(x, y, z, t: datetime, ell: Ellipsoid | None = None, *, deg: bool = True) -> tuple: +@overload +def eci2geodetic( + x: float, y: float, z: float, t: datetime, ell: Ellipsoid | None = None, *, deg: bool = True +) -> tuple[float, float, float]: + pass + + +@overload +def eci2geodetic( + x: ArrayLike, + y: ArrayLike, + z: ArrayLike, + t: datetime, + ell: Ellipsoid | None = None, + *, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def eci2geodetic( + x: float | ArrayLike, + y: float | ArrayLike, + z: float | ArrayLike, + t: datetime, + ell: Ellipsoid | None = None, + *, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ convert Earth Centered Internal ECI to geodetic coordinates @@ -399,16 +594,48 @@ def eci2geodetic(x, y, z, t: datetime, ell: Ellipsoid | None = None, *, deg: boo """ try: - xecef, yecef, zecef = eci2ecef(x, y, z, t) + xecef, yecef, zecef = eci2ecef(x, y, z, t) # type: ignore[arg-type] except NameError: raise ImportError("pip install numpy") return ecef2geodetic(xecef, yecef, zecef, ell, deg) +@overload def geodetic2eci( - lat, lon, alt, t: datetime, ell: Ellipsoid | None = None, *, deg: bool = True -) -> tuple: + lat: float, + lon: float, + alt: float, + t: datetime, + ell: Ellipsoid | None = None, + *, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def geodetic2eci( + lat: ArrayLike, + lon: ArrayLike, + alt: ArrayLike, + t: datetime, + ell: Ellipsoid | None = None, + *, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def geodetic2eci( + lat: float | ArrayLike, + lon: float | ArrayLike, + alt: float | ArrayLike, + t: datetime, + ell: Ellipsoid | None = None, + *, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ convert geodetic coordinates to Earth Centered Internal ECI @@ -441,7 +668,7 @@ def geodetic2eci( geodetic2eci() a.k.a lla2eci() """ - x, y, z = geodetic2ecef(lat, lon, alt, ell, deg) + x, y, z = geodetic2ecef(lat, lon, alt, ell, deg) # type: ignore[misc, arg-type] try: return ecef2eci(x, y, z, t) @@ -449,16 +676,44 @@ def geodetic2eci( raise ImportError("pip install numpy") +@overload +def enu2ecef( + e1: float, + n1: float, + u1: float, + lat0: float, + lon0: float, + h0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def enu2ecef( + e1: ArrayLike, + n1: ArrayLike, + u1: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def enu2ecef( - e1, - n1, - u1, - lat0, - lon0, - h0, + e1: float | ArrayLike, + n1: float | ArrayLike, + u1: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ ENU to ECEF @@ -492,7 +747,7 @@ def enu2ecef( z target z ECEF coordinate (meters) """ - x0, y0, z0 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) - dx, dy, dz = enu2uvw(e1, n1, u1, lat0, lon0, deg=deg) + x0, y0, z0 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] + dx, dy, dz = enu2uvw(e1, n1, u1, lat0, lon0, deg=deg) # type: ignore[misc, arg-type] return x0 + dx, y0 + dy, z0 + dz diff --git a/src/pymap3d/eci.py b/src/pymap3d/eci.py index 910d711d..07b701e8 100644 --- a/src/pymap3d/eci.py +++ b/src/pymap3d/eci.py @@ -3,21 +3,40 @@ from __future__ import annotations from datetime import datetime +from typing import Any, overload from numpy import array, atleast_1d, column_stack, cos, empty, sin +from numpy.typing import NDArray try: import astropy.units as u from astropy.coordinates import GCRS, ITRS, CartesianRepresentation, EarthLocation except ImportError: pass - +from ._types import ArrayLike from .sidereal import greenwichsrt, juliandate __all__ = ["eci2ecef", "ecef2eci"] -def eci2ecef(x, y, z, time: datetime) -> tuple: +@overload +def eci2ecef(x: float, y: float, z: float, time: datetime) -> tuple[float, float, float]: + pass + + +@overload +def eci2ecef( + x: ArrayLike, y: ArrayLike, z: ArrayLike, time: datetime +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def eci2ecef( + x: float | ArrayLike, + y: float | ArrayLike, + z: float | ArrayLike, + time: datetime, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Observer => Point ECI => ECEF @@ -71,7 +90,24 @@ def eci2ecef(x, y, z, time: datetime) -> tuple: return x_ecef, y_ecef, z_ecef -def ecef2eci(x, y, z, time: datetime) -> tuple: +@overload +def ecef2eci(x: float, y: float, z: float, time: datetime) -> tuple[float, float, float]: + pass + + +@overload +def ecef2eci( + x: ArrayLike, y: ArrayLike, z: ArrayLike, time: datetime +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def ecef2eci( + x: float | ArrayLike, + y: float | ArrayLike, + z: float | ArrayLike, + time: datetime, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Point => Point ECEF => ECI @@ -127,6 +163,6 @@ def ecef2eci(x, y, z, time: datetime) -> tuple: return x_eci, y_eci, z_eci -def R3(x: float): +def R3(x: float) -> NDArray[Any]: """Rotation matrix for ECI""" return array([[cos(x), sin(x), 0], [-sin(x), cos(x), 0], [0, 0, 1]]) diff --git a/src/pymap3d/ellipsoid.py b/src/pymap3d/ellipsoid.py index 006b8c1c..f0c6f189 100644 --- a/src/pymap3d/ellipsoid.py +++ b/src/pymap3d/ellipsoid.py @@ -1,10 +1,10 @@ """Minimal class for planetary ellipsoids""" from __future__ import annotations -from math import sqrt -from dataclasses import dataclass, field + import sys -import warnings +from dataclasses import dataclass, field +from math import sqrt from typing import Dict # for Python < 3.9 if sys.version_info >= (3, 8): @@ -71,7 +71,7 @@ class Ellipsoid: def __init__( self, semimajor_axis: float, semiminor_axis: float, name: str = "", model: str = "" - ): + ) -> None: """ Ellipsoidal model of world @@ -152,12 +152,11 @@ def __init__( } @classmethod - def from_name(cls, name: str) -> Ellipsoid | None: + def from_name(cls, name: str) -> Ellipsoid: """Create an Ellipsoid from a name.""" if name not in cls.models: - warnings.warn(f"{name} model not implemented") - return None + raise ValueError(f"{name} model not implemented") return cls( cls.models[name]["a"], cls.models[name]["b"], name=cls.models[name]["name"], model=name diff --git a/src/pymap3d/enu.py b/src/pymap3d/enu.py index 33d49630..d7241794 100644 --- a/src/pymap3d/enu.py +++ b/src/pymap3d/enu.py @@ -2,12 +2,15 @@ from __future__ import annotations from math import tau +from typing import Any, overload try: from numpy import asarray + from numpy.typing import NDArray except ImportError: pass +from ._types import ArrayLike from .ecef import ecef2geodetic, enu2ecef, geodetic2ecef, uvw2enu from .ellipsoid import Ellipsoid from .mathfun import atan2, cos, degrees, hypot, radians, sin @@ -15,7 +18,29 @@ __all__ = ["enu2aer", "aer2enu", "enu2geodetic", "geodetic2enu"] -def enu2aer(e, n, u, deg=True): +@overload +def enu2aer( + e: float, + n: float, + u: float, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def enu2aer( + e: ArrayLike, + n: ArrayLike, + u: ArrayLike, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def enu2aer( + e: float | ArrayLike, n: float | ArrayLike, u: float | ArrayLike, deg: bool = True +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ ENU to Azimuth, Elevation, Range @@ -45,16 +70,20 @@ def enu2aer(e, n, u, deg=True): # 1 millimeter precision for singularity stability try: + e = asarray(e) + n = asarray(n) + u = asarray(u) e[abs(e) < 1e-3] = 0.0 n[abs(n) < 1e-3] = 0.0 u[abs(u) < 1e-3] = 0.0 - except TypeError: + except (TypeError, NameError): + assert isinstance(e, float) and isinstance(n, float) and isinstance(u, float) if abs(e) < 1e-3: - e = 0.0 # type: ignore + e = 0.0 if abs(n) < 1e-3: - n = 0.0 # type: ignore + n = 0.0 if abs(u) < 1e-3: - u = 0.0 # type: ignore + u = 0.0 r = hypot(e, n) slantRange = hypot(r, u) @@ -68,7 +97,29 @@ def enu2aer(e, n, u, deg=True): return az, elev, slantRange -def aer2enu(az, el, srange, deg: bool = True) -> tuple: +@overload +def aer2enu( + az: float, + el: float, + srange: float, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def aer2enu( + az: ArrayLike, + el: ArrayLike, + srange: ArrayLike, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def aer2enu( + az: float | ArrayLike, el: float | ArrayLike, srange: float | ArrayLike, deg: bool = True +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Azimuth, Elevation, Slant range to target to East, North, Up @@ -100,6 +151,7 @@ def aer2enu(az, el, srange, deg: bool = True) -> tuple: if (asarray(srange) < 0).any(): raise ValueError("Slant range [0, Infinity)") except NameError: + assert isinstance(srange, int) or isinstance(srange, float) if srange < 0: raise ValueError("Slant range [0, Infinity)") @@ -108,16 +160,44 @@ def aer2enu(az, el, srange, deg: bool = True) -> tuple: return r * sin(az), r * cos(az), srange * sin(el) +@overload +def enu2geodetic( + e: float, + n: float, + u: float, + lat0: float, + lon0: float, + h0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def enu2geodetic( + e: ArrayLike, + n: ArrayLike, + u: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def enu2geodetic( - e, - n, - u, - lat0, - lon0, - h0, + e: float | ArrayLike, + n: float | ArrayLike, + u: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ East, North, Up to target to geodetic coordinates @@ -151,21 +231,49 @@ def enu2geodetic( altitude above ellipsoid (meters) """ - x, y, z = enu2ecef(e, n, u, lat0, lon0, h0, ell, deg=deg) + x, y, z = enu2ecef(e, n, u, lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] return ecef2geodetic(x, y, z, ell, deg=deg) +@overload +def geodetic2enu( + lat: float, + lon: float, + h: float, + lat0: float, + lon0: float, + h0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def geodetic2enu( + lat: ArrayLike, + lon: ArrayLike, + h: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def geodetic2enu( - lat, - lon, - h, - lat0, - lon0, - h0, + lat: float | ArrayLike, + lon: float | ArrayLike, + h: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Parameters ---------- @@ -196,7 +304,7 @@ def geodetic2enu( u : float Up ENU """ - x1, y1, z1 = geodetic2ecef(lat, lon, h, ell, deg=deg) - x2, y2, z2 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) + x1, y1, z1 = geodetic2ecef(lat, lon, h, ell, deg=deg) # type: ignore[misc, arg-type] + x2, y2, z2 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] - return uvw2enu(x1 - x2, y1 - y2, z1 - z2, lat0, lon0, deg=deg) + return uvw2enu(x1 - x2, y1 - y2, z1 - z2, lat0, lon0, deg=deg) # type: ignore[arg-type] diff --git a/src/pymap3d/haversine.py b/src/pymap3d/haversine.py index a6dd4937..501cc1f3 100644 --- a/src/pymap3d/haversine.py +++ b/src/pymap3d/haversine.py @@ -8,18 +8,56 @@ and gives virtually identical result within double precision arithmetic limitations """ +from __future__ import annotations + +from typing import Any, Sequence, overload try: from astropy.coordinates.angle_utilities import angular_separation except ImportError: pass +try: + from numpy import asarray + from numpy.typing import NDArray +except ImportError: + pass + +from ._types import ArrayLike from .mathfun import asin, cos, degrees, radians, sqrt __all__ = ["anglesep", "anglesep_meeus", "haversine"] -def anglesep_meeus(lon0: float, lat0: float, lon1: float, lat1: float, deg: bool = True) -> float: +@overload +def anglesep_meeus( + lon0: float, + lat0: float, + lon1: float, + lat1: float, + deg: bool = True, +) -> float: + pass + + +@overload +def anglesep_meeus( + lon0: ArrayLike, + lat0: ArrayLike, + lon1: ArrayLike, + lat1: ArrayLike, + deg: bool = True, +) -> NDArray[Any]: + pass + + +def anglesep_meeus( + lon0: float | ArrayLike, + lat0: float | ArrayLike, + lon1: float | ArrayLike, + lat1: float | ArrayLike, + deg: bool = True, +) -> float | NDArray[Any]: """ Parameters ---------- @@ -58,15 +96,57 @@ def anglesep_meeus(lon0: float, lat0: float, lon1: float, lat1: float, deg: bool lat0 = radians(lat0) lon1 = radians(lon1) lat1 = radians(lat1) + else: + try: + lon0 = asarray(lon0) + lat0 = asarray(lat0) + lon1 = asarray(lon1) + lat1 = asarray(lat1) + except NameError: + pass + assert ( + not isinstance(lon0, Sequence) + and not isinstance(lat0, Sequence) + and not isinstance(lon1, Sequence) + and not isinstance(lat1, Sequence) + ) sep_rad = 2 * asin( sqrt(haversine(lat0 - lat1) + cos(lat0) * cos(lat1) * haversine(lon0 - lon1)) ) - return degrees(sep_rad) if deg else sep_rad + return degrees(sep_rad) if deg else sep_rad # type: ignore[no-any-return] -def anglesep(lon0: float, lat0: float, lon1: float, lat1: float, deg: bool = True) -> float: +@overload +def anglesep( + lon0: float, + lat0: float, + lon1: float, + lat1: float, + deg: bool = True, +) -> float: + pass + + +@overload +def anglesep( + lon0: ArrayLike, + lat0: ArrayLike, + lon1: ArrayLike, + lat1: ArrayLike, + deg: bool = True, +) -> NDArray[Any]: + pass + + +def anglesep( + lon0: float | ArrayLike, + lat0: float | ArrayLike, + lon1: float | ArrayLike, + lat1: float | ArrayLike, + deg: bool = True, +) -> float | NDArray[Any]: """ Parameters ---------- @@ -97,16 +177,40 @@ def anglesep(lon0: float, lat0: float, lon1: float, lat1: float, deg: bool = Tru lat0 = radians(lat0) lon1 = radians(lon1) lat1 = radians(lat1) + else: + try: + lon0 = asarray(lon0) + lat0 = asarray(lat0) + lon1 = asarray(lon1) + lat1 = asarray(lat1) + except NameError: + pass + assert ( + not isinstance(lon0, Sequence) + and not isinstance(lat0, Sequence) + and not isinstance(lon1, Sequence) + and not isinstance(lat1, Sequence) + ) try: sep_rad = angular_separation(lon0, lat0, lon1, lat1) except NameError: - sep_rad = anglesep_meeus(lon0, lat0, lon1, lat1, deg=False) + sep_rad = anglesep_meeus(lon0, lat0, lon1, lat1, deg=False) # type: ignore[arg-type] - return degrees(sep_rad) if deg else sep_rad + return degrees(sep_rad) if deg else sep_rad # type: ignore[no-any-return] +@overload def haversine(theta: float) -> float: + pass + + +@overload +def haversine(theta: ArrayLike) -> NDArray[Any]: + pass + + +def haversine(theta: float | ArrayLike) -> float | NDArray[Any]: """ Compute haversine diff --git a/src/pymap3d/latitude.py b/src/pymap3d/latitude.py index 37dbc029..3b1de190 100644 --- a/src/pymap3d/latitude.py +++ b/src/pymap3d/latitude.py @@ -3,8 +3,16 @@ from __future__ import annotations from math import pi +from typing import Any, Sequence, overload + +try: + from numpy.typing import NDArray + from nupy import asarray +except ImportError: + pass from . import rcurve +from ._types import ArrayLike from .ellipsoid import Ellipsoid from .mathfun import ( asinh, @@ -42,12 +50,32 @@ ] +@overload +def geoc2geod( + geocentric_lat: float, + geocentric_distance: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> float: + pass + + +@overload +def geoc2geod( + geocentric_lat: ArrayLike, + geocentric_distance: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> NDArray[Any]: + pass + + def geoc2geod( - geocentric_lat, - geocentric_distance, + geocentric_lat: float | ArrayLike, + geocentric_distance: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -): +) -> float | NDArray[Any]: """ convert geocentric latitude to geodetic latitude, consider mean sea level altitude @@ -78,6 +106,12 @@ def geoc2geod( """ geocentric_lat, ell = sanitize(geocentric_lat, ell, deg) + try: + geocentric_distance = asarray(geocentric_distance) + except NameError: + pass + assert not isinstance(geocentric_distance, Sequence) + r = geocentric_distance / ell.semimajor_axis geodetic_lat = ( @@ -89,7 +123,42 @@ def geoc2geod( return degrees(geodetic_lat) if deg else geodetic_lat -def geodetic2geocentric(geodetic_lat, alt_m, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def geodetic2geocentric( + geodetic_lat: float, + alt_m: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> float: + pass + + +@overload +def geodetic2geocentric( + geodetic_lat: ArrayLike, + alt_m: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> NDArray[Any]: + pass + + +@overload +def geodetic2geocentric( + geodetic_lat: ArrayLike, + alt_m: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> NDArray[Any]: + pass + + +def geodetic2geocentric( + geodetic_lat: float | ArrayLike, + alt_m: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> float | NDArray[Any]: """ convert geodetic latitude to geocentric latitude on spheroid surface @@ -120,15 +189,50 @@ def geodetic2geocentric(geodetic_lat, alt_m, ell: Ellipsoid | None = None, deg: """ geodetic_lat, ell = sanitize(geodetic_lat, ell, deg) r = rcurve.transverse(geodetic_lat, ell, deg=False) - geocentric_lat = atan((1 - ell.eccentricity**2 * (r / (r + alt_m))) * tan(geodetic_lat)) + geocentric_lat = atan((1 - ell.eccentricity**2 * (r / (r + alt_m))) * tan(geodetic_lat)) # type: ignore[operator] - return degrees(geocentric_lat) if deg else geocentric_lat + return degrees(geocentric_lat) if deg else geocentric_lat # type: ignore[no-any-return] geod2geoc = geodetic2geocentric -def geocentric2geodetic(geocentric_lat, alt_m, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def geocentric2geodetic( + geocentric_lat: float, + alt_m: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> float: + pass + + +@overload +def geocentric2geodetic( + geocentric_lat: ArrayLike, + alt_m: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> NDArray[Any]: + pass + + +@overload +def geocentric2geodetic( + geocentric_lat: ArrayLike, + alt_m: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> NDArray[Any]: + pass + + +def geocentric2geodetic( + geocentric_lat: float | ArrayLike, + alt_m: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> float | NDArray[Any]: """ converts from geocentric latitude to geodetic latitude @@ -159,12 +263,28 @@ def geocentric2geodetic(geocentric_lat, alt_m, ell: Ellipsoid | None = None, deg """ geocentric_lat, ell = sanitize(geocentric_lat, ell, deg) r = rcurve.transverse(geocentric_lat, ell, deg=False) - geodetic_lat = atan(tan(geocentric_lat) / (1 - ell.eccentricity**2 * (r / (r + alt_m)))) + geodetic_lat = atan(tan(geocentric_lat) / (1 - ell.eccentricity**2 * (r / (r + alt_m)))) # type: ignore[operator] - return degrees(geodetic_lat) if deg else geodetic_lat + return degrees(geodetic_lat) if deg else geodetic_lat # type: ignore[no-any-return] + + +@overload +def geodetic2isometric( + geodetic_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass -def geodetic2isometric(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def geodetic2isometric( + geodetic_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass + + +def geodetic2isometric( + geodetic_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ computes isometric latitude on an ellipsoid @@ -210,7 +330,7 @@ def geodetic2isometric(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = T i = abs(coslat) <= COS_EPS try: - isometric_lat[i] = sign(geodetic_lat[i]) * inf # type: ignore + isometric_lat[i] = sign(geodetic_lat[i]) * inf # type: ignore[index] except TypeError: if i: isometric_lat = sign(geodetic_lat) * inf @@ -219,12 +339,28 @@ def geodetic2isometric(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = T isometric_lat = degrees(isometric_lat) try: - return isometric_lat.squeeze()[()] + return isometric_lat.squeeze()[()] # type: ignore[no-any-return] except AttributeError: - return isometric_lat + return isometric_lat # type: ignore[no-any-return] + + +@overload +def isometric2geodetic( + isometric_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass + + +@overload +def isometric2geodetic( + isometric_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass -def isometric2geodetic(isometric_lat, ell: Ellipsoid | None = None, deg: bool = True): +def isometric2geodetic( + isometric_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ converts from isometric latitude to geodetic latitude @@ -257,10 +393,26 @@ def isometric2geodetic(isometric_lat, ell: Ellipsoid | None = None, deg: bool = conformal_lat = 2 * atan(exp(isometric_lat)) - (pi / 2) geodetic_lat = conformal2geodetic(conformal_lat, ell, deg=False) - return degrees(geodetic_lat) if deg else geodetic_lat + return degrees(geodetic_lat) if deg else geodetic_lat # type: ignore[no-any-return] + + +@overload +def conformal2geodetic( + conformal_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass + + +@overload +def conformal2geodetic( + conformal_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass -def conformal2geodetic(conformal_lat, ell: Ellipsoid | None = None, deg: bool = True): +def conformal2geodetic( + conformal_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ converts from conformal latitude to geodetic latitude @@ -302,10 +454,26 @@ def conformal2geodetic(conformal_lat, ell: Ellipsoid | None = None, deg: bool = + f4 * sin(8 * conformal_lat) ) - return degrees(geodetic_lat) if deg else geodetic_lat + return degrees(geodetic_lat) if deg else geodetic_lat # type: ignore[no-any-return] + + +@overload +def geodetic2conformal( + geodetic_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass -def geodetic2conformal(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def geodetic2conformal( + geodetic_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass + + +def geodetic2conformal( + geodetic_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ converts from geodetic latitude to conformal latitude @@ -347,11 +515,27 @@ def geodetic2conformal(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = T except ZeroDivisionError: conformal_lat = pi / 2 - return degrees(conformal_lat) if deg else conformal_lat + return degrees(conformal_lat) if deg else conformal_lat # type: ignore[no-any-return] # %% rectifying -def geodetic2rectifying(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def geodetic2rectifying( + geodetic_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass + + +@overload +def geodetic2rectifying( + geodetic_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass + + +def geodetic2rectifying( + geodetic_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ converts from geodetic latitude to rectifying latitude @@ -380,6 +564,8 @@ def geodetic2rectifying(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = """ geodetic_lat, ell = sanitize(geodetic_lat, ell, deg) + assert isinstance(ell, Ellipsoid) + n = ell.thirdflattening f1 = 3 * n / 2 - 9 * n**3 / 16 f2 = 15 * n**2 / 16 - 15 * n**4 / 32 @@ -394,10 +580,26 @@ def geodetic2rectifying(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = + f4 * sin(8 * geodetic_lat) ) - return degrees(rectifying_lat) if deg else rectifying_lat + return degrees(rectifying_lat) if deg else rectifying_lat # type: ignore[no-any-return] + +@overload +def rectifying2geodetic( + rectifying_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass -def rectifying2geodetic(rectifying_lat, ell: Ellipsoid | None = None, deg: bool = True): + +@overload +def rectifying2geodetic( + rectifying_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass + + +def rectifying2geodetic( + rectifying_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ converts from rectifying latitude to geodetic latitude @@ -439,11 +641,25 @@ def rectifying2geodetic(rectifying_lat, ell: Ellipsoid | None = None, deg: bool + f4 * sin(8 * rectifying_lat) ) - return degrees(geodetic_lat) if deg else geodetic_lat + return degrees(geodetic_lat) if deg else geodetic_lat # type: ignore[no-any-return] # %% authalic -def geodetic2authalic(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def geodetic2authalic(geodetic_lat: float, ell: Ellipsoid | None = None, deg: bool = True) -> float: + pass + + +@overload +def geodetic2authalic( + geodetic_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass + + +def geodetic2authalic( + geodetic_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ converts from geodetic latitude to authalic latitude @@ -484,10 +700,24 @@ def geodetic2authalic(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = Tr - f3 * sin(6 * geodetic_lat) ) - return degrees(authalic_lat) if deg else authalic_lat + return degrees(authalic_lat) if deg else authalic_lat # type: ignore[no-any-return] + + +@overload +def authalic2geodetic(authalic_lat: float, ell: Ellipsoid | None = None, deg: bool = True) -> float: + pass + +@overload +def authalic2geodetic( + authalic_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass -def authalic2geodetic(authalic_lat, ell: Ellipsoid | None = None, deg: bool = True): + +def authalic2geodetic( + authalic_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ converts from authalic latitude to geodetic latitude @@ -526,11 +756,27 @@ def authalic2geodetic(authalic_lat, ell: Ellipsoid | None = None, deg: bool = Tr + f3 * sin(6 * authalic_lat) ) - return degrees(geodetic_lat) if deg else geodetic_lat + return degrees(geodetic_lat) if deg else geodetic_lat # type: ignore[no-any-return] # %% parametric -def geodetic2parametric(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def geodetic2parametric( + geodetic_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass + + +@overload +def geodetic2parametric( + geodetic_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass + + +def geodetic2parametric( + geodetic_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ converts from geodetic latitude to parametric latitude @@ -561,10 +807,26 @@ def geodetic2parametric(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = parametric_lat = atan(sqrt(1 - (ell.eccentricity) ** 2) * tan(geodetic_lat)) - return degrees(parametric_lat) if deg else parametric_lat + return degrees(parametric_lat) if deg else parametric_lat # type: ignore[no-any-return] -def parametric2geodetic(parametric_lat, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def parametric2geodetic( + parametric_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass + + +@overload +def parametric2geodetic( + parametric_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass + + +def parametric2geodetic( + parametric_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ converts from parametric latitude to geodetic latitude @@ -594,4 +856,4 @@ def parametric2geodetic(parametric_lat, ell: Ellipsoid | None = None, deg: bool geodetic_lat = atan(tan(parametric_lat) / sqrt(1 - (ell.eccentricity) ** 2)) - return degrees(geodetic_lat) if deg else geodetic_lat + return degrees(geodetic_lat) if deg else geodetic_lat # type: ignore[no-any-return] diff --git a/src/pymap3d/los.py b/src/pymap3d/los.py index 7640ef1b..5eb11115 100644 --- a/src/pymap3d/los.py +++ b/src/pymap3d/los.py @@ -3,29 +3,58 @@ from __future__ import annotations from math import nan, pi +from typing import Any, Sequence, overload try: from numpy import asarray + from numpy.typing import NDArray except ImportError: pass -from .aer import aer2enu +from ._types import ArrayLike from .ecef import ecef2geodetic, enu2uvw, geodetic2ecef from .ellipsoid import Ellipsoid +from .enu import aer2enu from .mathfun import sqrt __all__ = ["lookAtSpheroid"] +@overload def lookAtSpheroid( - lat0, - lon0, - h0, - az, - tilt, + lat0: float, + lon0: float, + h0: float, + az: float, + tilt: float, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +@overload +def lookAtSpheroid( + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + az: ArrayLike, + tilt: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def lookAtSpheroid( + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, + az: float | ArrayLike, + tilt: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Calculates line-of-sight intersection with Earth (or other ellipsoid) surface from above surface / orbit @@ -74,20 +103,22 @@ def lookAtSpheroid( if (h0 < 0).any(): raise ValueError("Intersection calculation requires altitude [0, Infinity)") except NameError: - if h0 < 0: + if h0 < 0: # type: ignore[operator] raise ValueError("Intersection calculation requires altitude [0, Infinity)") + assert not isinstance(tilt, Sequence) + a = ell.semimajor_axis b = ell.semimajor_axis c = ell.semiminor_axis el = tilt - 90.0 if deg else tilt - pi / 2 - e, n, u = aer2enu(az, el, srange=1.0, deg=deg) + e, n, u = aer2enu(az, el, srange=1.0, deg=deg) # type: ignore[arg-type] # fixed 1 km slant range - u, v, w = enu2uvw(e, n, u, lat0, lon0, deg=deg) - x, y, z = geodetic2ecef(lat0, lon0, h0, deg=deg) + u, v, w = enu2uvw(e, n, u, lat0, lon0, deg=deg) # type: ignore[arg-type] + x, y, z = geodetic2ecef(lat0, lon0, h0, deg=deg) # type: ignore[arg-type] value = -(a**2) * b**2 * w * z - a**2 * c**2 * v * y - b**2 * c**2 * u * x radical = ( @@ -109,7 +140,7 @@ def lookAtSpheroid( # %% Return nan if radical < 0 or d < 0 because LOS vector does not point towards Earth try: - radical[radical < 0] = nan + radical[radical < 0] = nan # type: ignore[index] except TypeError: if radical < 0: radical = nan diff --git a/src/pymap3d/lox.py b/src/pymap3d/lox.py index 02a10e2d..82a0f7b4 100644 --- a/src/pymap3d/lox.py +++ b/src/pymap3d/lox.py @@ -2,14 +2,18 @@ from __future__ import annotations +from math import pi, tau +from typing import Any, Sequence, overload + try: - from numpy import array, broadcast_arrays + from numpy import array, asarray, broadcast_arrays + from numpy.typing import NDArray except ImportError: pass -from math import pi, tau from . import rcurve, rsphere +from ._types import ArrayLike from .ellipsoid import Ellipsoid from .latitude import ( authalic2geodetic, @@ -34,7 +38,17 @@ COS_EPS = 1e-9 -def meridian_dist(lat, ell: Ellipsoid | None = None, deg: bool = True) -> float: +@overload +def meridian_dist(lat: float, ell: Ellipsoid | None = None, deg: bool = True) -> float: + pass + + +@overload +def meridian_dist(lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True) -> float: + pass + + +def meridian_dist(lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True) -> float: """ Computes the ground distance on an ellipsoid from the equator to the input latitude. @@ -52,10 +66,24 @@ def meridian_dist(lat, ell: Ellipsoid | None = None, deg: bool = True) -> float: dist : float distance (meters) """ - return meridian_arc(0.0, lat, ell, deg) + return meridian_arc(0.0, lat, ell, deg) # type: ignore[arg-type] + + +@overload +def meridian_arc(lat1: float, lat2: float, ell: Ellipsoid | None = None, deg: bool = True) -> float: + pass -def meridian_arc(lat1, lat2, ell: Ellipsoid | None = None, deg: bool = True) -> float: +@overload +def meridian_arc( + lat1: ArrayLike, lat2: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass + + +def meridian_arc( + lat1: float | ArrayLike, lat2: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float: """ Computes the ground distance on an ellipsoid between two latitudes. @@ -80,17 +108,17 @@ def meridian_arc(lat1, lat2, ell: Ellipsoid | None = None, deg: bool = True) -> rlat1 = geodetic2rectifying(lat1, ell, deg=False) rlat2 = geodetic2rectifying(lat2, ell, deg=False) - return rsphere.rectifying(ell) * abs(rlat2 - rlat1) + return rsphere.rectifying(ell) * abs(rlat2 - rlat1) # type: ignore[no-any-return] def loxodrome_inverse( - lat1, - lon1, - lat2, - lon2, + lat1: float | ArrayLike | Sequence[Sequence[float]], + lon1: float | ArrayLike | Sequence[Sequence[float]], + lat2: float | ArrayLike | Sequence[Sequence[float]], + lon2: float | ArrayLike | Sequence[Sequence[float]], ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple[float, float]: +) -> tuple[float, float] | tuple[NDArray[Any], NDArray[Any]]: """ computes the arc length and azimuth of the loxodrome between two points on the surface of the reference ellipsoid @@ -141,9 +169,14 @@ def loxodrome_inverse( try: lat1, lon1, lat2, lon2 = broadcast_arrays(lat1, lon1, lat2, lon2) - except NameError: pass + assert ( + not isinstance(lat1, Sequence) + and not isinstance(lon1, Sequence) + and not isinstance(lat2, Sequence) + and not isinstance(lon2, Sequence) + ) # compute changes in isometric latitude and longitude between points disolat = geodetic2isometric(lat2, deg=False, ell=ell) - geodetic2isometric( @@ -156,15 +189,15 @@ def loxodrome_inverse( aux = abs(cos(az12)) # compute distance along loxodromic curve - dist = meridian_arc(lat2, lat1, deg=False, ell=ell) / aux + dist = meridian_arc(lat2, lat1, deg=False, ell=ell) / aux # type: ignore[arg-type] # straight east or west i = aux < COS_EPS try: - dist[i] = departure(lon2[i], lon1[i], lat1[i], ell, deg=False) + dist[i] = departure(lon2[i], lon1[i], lat1[i], ell, deg=False) # type: ignore[index] except (AttributeError, TypeError): if i: - dist = departure(lon2, lon1, lat1, ell, deg=False) + dist = departure(lon2, lon1, lat1, ell, deg=False) # type: ignore[arg-type] if deg: az12 = degrees(az12) % 360.0 @@ -176,13 +209,13 @@ def loxodrome_inverse( def loxodrome_direct( - lat1, - lon1, - rng, - a12, + lat1: float | ArrayLike, + lon1: float | ArrayLike, + rng: float | ArrayLike, + a12: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float] | tuple[NDArray[Any], NDArray[Any]]: """ Given starting lat, lon with arclength and azimuth, compute final lat, lon @@ -214,6 +247,12 @@ def loxodrome_direct( if deg: lat1, lon1, a12 = radians(lat1), radians(lon1), radians(a12) + else: + try: + a12 = asarray(a12) + except NameError: + pass + assert not isinstance(a12, Sequence) a12 = a12 % tau @@ -224,10 +263,11 @@ def loxodrome_direct( if (rng < 0).any(): raise ValueError("ground distance must be >= 0") except NameError: - if abs(lat1) > pi / 2: + if abs(lat1) > pi / 2: # type: ignore[operator, arg-type] raise ValueError("-90 <= latitude <= 90") - if rng < 0: + if rng < 0: # type: ignore[operator] raise ValueError("ground distance must be >= 0") + assert not isinstance(rng, Sequence) # compute rectifying sphere latitude and radius reclat = geodetic2rectifying(lat1, ell, deg=False) @@ -245,7 +285,7 @@ def loxodrome_direct( dlon = tan(a12) * (newiso - iso) try: - dlon[i] = sign(pi - a12[i]) * rng[i] / rcurve.parallel(lat1[i], ell=ell, deg=False) # type: ignore + dlon[i] = sign(pi - a12[i]) * rng[i] / rcurve.parallel(lat1[i], ell=ell, deg=False) # type: ignore[index, call-overload] except (AttributeError, TypeError): if i: # straight east or west dlon = sign(pi - a12) * rng / rcurve.parallel(lat1, ell=ell, deg=False) @@ -256,12 +296,40 @@ def loxodrome_direct( lat2, lon2 = degrees(lat2), degrees(lon2) try: - return lat2.squeeze()[()], lon2.squeeze()[()] # type: ignore + return lat2.squeeze()[()], lon2.squeeze()[()] except AttributeError: return lat2, lon2 -def departure(lon1, lon2, lat, ell: Ellipsoid | None = None, deg: bool = True) -> float: +@overload +def departure( + lon1: float, + lon2: float, + lat: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> float: + pass + + +@overload +def departure( + lon1: ArrayLike, + lon2: ArrayLike, + lat: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> NDArray[Any]: + pass + + +def departure( + lon1: float | ArrayLike, + lon2: float | ArrayLike, + lat: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> float | NDArray[Any]: """ Computes the distance along a specific parallel between two meridians. @@ -286,10 +354,12 @@ def departure(lon1, lon2, lat, ell: Ellipsoid | None = None, deg: bool = True) - if deg: lon1, lon2, lat = radians(lon1), radians(lon2), radians(lat) - return rcurve.parallel(lat, ell=ell, deg=False) * (abs(lon2 - lon1) % pi) + return rcurve.parallel(lat, ell=ell, deg=False) * (abs(lon2 - lon1) % pi) # type: ignore[no-any-return, operator] -def meanm(lat, lon, ell: Ellipsoid | None = None, deg: bool = True) -> tuple: +def meanm( + lat: float | ArrayLike, lon: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> tuple[float, float]: """ Computes geographic mean for geographic points on an ellipsoid @@ -317,7 +387,7 @@ def meanm(lat, lon, ell: Ellipsoid | None = None, deg: bool = True) -> tuple: lat = geodetic2authalic(lat, ell, deg=False) - x, y, z = sph2cart(lon, lat, array(1.0)) + x, y, z = sph2cart(lon, lat, array(1.0)) # type: ignore[arg-type] lonbar, latbar, _ = cart2sph(x.sum(), y.sum(), z.sum()) latbar = authalic2geodetic(latbar, ell, deg=False) diff --git a/src/pymap3d/mathfun.py b/src/pymap3d/mathfun.py index 0db9bd81..52c8c248 100644 --- a/src/pymap3d/mathfun.py +++ b/src/pymap3d/mathfun.py @@ -1,6 +1,7 @@ """ import from Numpy, and if not available fallback to math stdlib """ +from typing import cast try: from numpy import arcsin as asin @@ -25,7 +26,7 @@ tan, ) except ImportError: - from math import ( # type: ignore + from math import ( # type: ignore[misc] asin, asinh, atan, @@ -44,10 +45,10 @@ tan, ) - def power(x, y): # type: ignore - return pow(x, y) + def power(x: float, y: float) -> float: # type: ignore[misc] + return cast(float, pow(x, y)) - def sign(x) -> float: # type: ignore + def sign(x: float) -> float: # type: ignore[misc] """signum function""" if x < 0: y = -1.0 @@ -58,9 +59,9 @@ def sign(x) -> float: # type: ignore return y - def cbrt(x) -> float: # type: ignore + def cbrt(x: float) -> float: # type: ignore[misc] """math.cbrt was added in Python 3.11""" - return x ** (1 / 3) + return cast(float, x ** (1 / 3)) __all__ = [ diff --git a/src/pymap3d/ned.py b/src/pymap3d/ned.py index bf66a43d..405b04bb 100644 --- a/src/pymap3d/ned.py +++ b/src/pymap3d/ned.py @@ -1,12 +1,43 @@ """ Transforms involving NED North East Down """ from __future__ import annotations +from typing import Any, Sequence, overload + +try: + from numpy import asarray + from numpy.typing import NDArray +except ImportError: + pass + +from ._types import ArrayLike from .ecef import ecef2enu, ecef2enuv, ecef2geodetic, enu2ecef from .ellipsoid import Ellipsoid from .enu import aer2enu, enu2aer, geodetic2enu -def aer2ned(az, elev, slantRange, deg: bool = True) -> tuple: +@overload +def aer2ned( + az: float, + elev: float, + slantRange: float, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def aer2ned( + az: ArrayLike, + elev: ArrayLike, + slantRange: ArrayLike, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def aer2ned( + az: float | ArrayLike, elev: float | ArrayLike, slantRange: float | ArrayLike, deg: bool = True +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ converts azimuth, elevation, range to target from observer to North, East, Down @@ -31,12 +62,34 @@ def aer2ned(az, elev, slantRange, deg: bool = True) -> tuple: d : float Down NED coordinate (meters) """ - e, n, u = aer2enu(az, elev, slantRange, deg=deg) + e, n, u = aer2enu(az, elev, slantRange, deg=deg) # type: ignore[arg-type] return n, e, -u -def ned2aer(n, e, d, deg: bool = True) -> tuple: +@overload +def ned2aer( + n: float, + e: float, + d: float, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def ned2aer( + n: ArrayLike, + e: ArrayLike, + d: ArrayLike, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def ned2aer( + n: float | ArrayLike, e: float | ArrayLike, d: float | ArrayLike, deg: bool = True +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ converts North, East, Down to azimuth, elevation, range @@ -62,19 +115,53 @@ def ned2aer(n, e, d, deg: bool = True) -> tuple: slantRange : float slant range [meters] """ - return enu2aer(e, n, -d, deg=deg) + try: + d = asarray(d) + except NameError: + pass + assert not isinstance(d, Sequence) + + return enu2aer(e, n, -d, deg=deg) # type: ignore[arg-type] + + +@overload +def ned2geodetic( + n: float, + e: float, + d: float, + lat0: float, + lon0: float, + h0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def ned2geodetic( + n: ArrayLike, + e: ArrayLike, + d: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass def ned2geodetic( - n, - e, - d, - lat0, - lon0, - h0, + n: float | ArrayLike, + e: float | ArrayLike, + d: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Converts North, East, Down to target latitude, longitude, altitude @@ -109,21 +196,55 @@ def ned2geodetic( target altitude above geodetic ellipsoid (meters) """ - x, y, z = enu2ecef(e, n, -d, lat0, lon0, h0, ell, deg=deg) + try: + d = asarray(d) + except NameError: + pass + assert not isinstance(d, Sequence) + + x, y, z = enu2ecef(e, n, -d, lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] return ecef2geodetic(x, y, z, ell, deg=deg) +@overload +def ned2ecef( + n: float, + e: float, + d: float, + lat0: float, + lon0: float, + h0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def ned2ecef( + n: ArrayLike, + e: ArrayLike, + d: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def ned2ecef( - n, - e, - d, - lat0, - lon0, - h0, + n: float | ArrayLike, + e: float | ArrayLike, + d: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ North, East, Down to target ECEF coordinates @@ -157,19 +278,53 @@ def ned2ecef( z : float ECEF z coordinate (meters) """ - return enu2ecef(e, n, -d, lat0, lon0, h0, ell, deg=deg) + try: + d = asarray(d) + except NameError: + pass + assert not isinstance(d, Sequence) + + return enu2ecef(e, n, -d, lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] + + +@overload +def ecef2ned( + x: float, + y: float, + z: float, + lat0: float, + lon0: float, + h0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def ecef2ned( + x: ArrayLike, + y: ArrayLike, + z: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass def ecef2ned( - x, - y, - z, - lat0, - lon0, - h0, + x: float | ArrayLike, + y: float | ArrayLike, + z: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Convert ECEF x,y,z to North, East, Down @@ -204,21 +359,49 @@ def ecef2ned( Down NED coordinate (meters) """ - e, n, u = ecef2enu(x, y, z, lat0, lon0, h0, ell, deg=deg) + e, n, u = ecef2enu(x, y, z, lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] return n, e, -u +@overload +def geodetic2ned( + lat: float, + lon: float, + h: float, + lat0: float, + lon0: float, + h0: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def geodetic2ned( + lat: ArrayLike, + lon: ArrayLike, + h: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def geodetic2ned( - lat, - lon, - h, - lat0, - lon0, - h0, + lat: float | ArrayLike, + lon: float | ArrayLike, + h: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ convert latitude, longitude, altitude of target to North, East, Down from observer @@ -253,12 +436,43 @@ def geodetic2ned( d : float Down NED coordinate (meters) """ - e, n, u = geodetic2enu(lat, lon, h, lat0, lon0, h0, ell, deg=deg) + e, n, u = geodetic2enu(lat, lon, h, lat0, lon0, h0, ell, deg=deg) # type: ignore[misc, arg-type] return n, e, -u -def ecef2nedv(x, y, z, lat0, lon0, deg: bool = True) -> tuple[float, float, float]: +@overload +def ecef2nedv( + x: float, + y: float, + z: float, + lat0: float, + lon0: float, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def ecef2nedv( + x: ArrayLike, + y: ArrayLike, + z: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def ecef2nedv( + x: float | ArrayLike, + y: float | ArrayLike, + z: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + deg: bool = True, +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ for VECTOR between two points @@ -289,6 +503,6 @@ def ecef2nedv(x, y, z, lat0, lon0, deg: bool = True) -> tuple[float, float, floa d : float Down NED coordinate (meters) """ - e, n, u = ecef2enuv(x, y, z, lat0, lon0, deg=deg) + e, n, u = ecef2enuv(x, y, z, lat0, lon0, deg=deg) # type: ignore[misc, arg-type] return n, e, -u diff --git a/src/pymap3d/rcurve.py b/src/pymap3d/rcurve.py index 87d056c0..9c8b09c4 100644 --- a/src/pymap3d/rcurve.py +++ b/src/pymap3d/rcurve.py @@ -2,6 +2,14 @@ from __future__ import annotations +from typing import Any, overload + +try: + from numpy.typing import NDArray +except ImportError: + pass + +from ._types import ArrayLike from .ellipsoid import Ellipsoid from .mathfun import cos, sin, sqrt from .utils import sanitize @@ -9,7 +17,21 @@ __all__ = ["parallel", "meridian", "transverse", "geocentric_radius"] -def geocentric_radius(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def geocentric_radius(geodetic_lat: float, ell: Ellipsoid | None = None, deg: bool = True) -> float: + pass + + +@overload +def geocentric_radius( + geodetic_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass + + +def geocentric_radius( + geodetic_lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ compute geocentric radius at geodetic latitude @@ -29,7 +51,19 @@ def geocentric_radius(geodetic_lat, ell: Ellipsoid | None = None, deg: bool = Tr ) -def parallel(lat, ell: Ellipsoid | None = None, deg: bool = True) -> float: +@overload +def parallel(lat: float, ell: Ellipsoid | None = None, deg: bool = True) -> float: + pass + + +@overload +def parallel(lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True) -> NDArray[Any]: + pass + + +def parallel( + lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """ computes the radius of the small circle encompassing the globe at the specified latitude @@ -55,7 +89,19 @@ def parallel(lat, ell: Ellipsoid | None = None, deg: bool = True) -> float: return cos(lat) * transverse(lat, ell, deg=False) -def meridian(lat, ell: Ellipsoid | None = None, deg: bool = True): +@overload +def meridian(lat: float, ell: Ellipsoid | None = None, deg: bool = True) -> float: + pass + + +@overload +def meridian(lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True) -> NDArray[Any]: + pass + + +def meridian( + lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """computes the meridional radius of curvature for the ellipsoid like Matlab rcurve('meridian', ...) @@ -79,10 +125,22 @@ def meridian(lat, ell: Ellipsoid | None = None, deg: bool = True): f1 = ell.semimajor_axis * (1 - ell.eccentricity**2) f2 = 1 - (ell.eccentricity * sin(lat)) ** 2 - return f1 / sqrt(f2**3) + return f1 / sqrt(f2**3) # type: ignore[no-any-return] + + +@overload +def transverse(lat: float, ell: Ellipsoid | None = None, deg: bool = True) -> float: + pass + + +@overload +def transverse(lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True) -> NDArray[Any]: + pass -def transverse(lat, ell: Ellipsoid | None = None, deg: bool = True): +def transverse( + lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float | NDArray[Any]: """computes the radius of the curve formed by a plane intersecting the ellipsoid at the latitude which is normal to the surface of the ellipsoid @@ -106,4 +164,4 @@ def transverse(lat, ell: Ellipsoid | None = None, deg: bool = True): lat, ell = sanitize(lat, ell, deg) - return ell.semimajor_axis / sqrt(1 - (ell.eccentricity * sin(lat)) ** 2) + return ell.semimajor_axis / sqrt(1 - (ell.eccentricity * sin(lat)) ** 2) # type: ignore[no-any-return] diff --git a/src/pymap3d/rsphere.py b/src/pymap3d/rsphere.py index 6c775059..677545d0 100644 --- a/src/pymap3d/rsphere.py +++ b/src/pymap3d/rsphere.py @@ -2,12 +2,16 @@ from __future__ import annotations +from typing import Any, Sequence, overload + try: from numpy import asarray + from numpy.typing import NDArray except ImportError: pass from . import rcurve +from ._types import ArrayLike from .ellipsoid import Ellipsoid from .mathfun import cos, degrees, log, radians, sin, sqrt from .vincenty import vdist @@ -66,7 +70,7 @@ def authalic(ell: Ellipsoid | None = None) -> float: f1 = ell.semimajor_axis**2 / 2 f2 = (1 - e**2) / (2 * e) f3 = log((1 + e) / (1 - e)) - return sqrt(f1 * (1 + f2 * f3)) + return sqrt(f1 * (1 + f2 * f3)) # type: ignore[no-any-return] else: return ell.semimajor_axis @@ -86,17 +90,53 @@ def rectifying(ell: Ellipsoid | None = None) -> float: """ if ell is None: ell = Ellipsoid.from_name("wgs84") - return ((ell.semimajor_axis ** (3 / 2) + ell.semiminor_axis ** (3 / 2)) / 2) ** (2 / 3) + return ((ell.semimajor_axis ** (3 / 2) + ell.semiminor_axis ** (3 / 2)) / 2) ** (2 / 3) # type: ignore[no-any-return] + + +@overload +def euler( + lat1: float, + lon1: float, + lat2: float, + lon2: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> float: + pass + + +@overload +def euler( + lat1: ArrayLike, + lon1: ArrayLike, + lat2: float, + lon2: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> NDArray[Any]: + pass +@overload def euler( - lat1, - lon1, - lat2, - lon2, + lat1: ArrayLike, + lon1: ArrayLike, + lat2: ArrayLike, + lon2: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -): +) -> NDArray[Any]: + pass + + +def euler( + lat1: float | ArrayLike, + lon1: float | ArrayLike, + lat2: float | ArrayLike, + lon2: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> float | NDArray[Any]: """computes the Euler radii of curvature at the midpoint of the great circle arc defined by the endpoints (lat1,lon1) and (lat2,lon2) @@ -123,11 +163,12 @@ def euler( lat1, lat2 = asarray(lat1), asarray(lat2) except NameError: pass + assert not isinstance(lat1, Sequence) and not isinstance(lat2, Sequence) latmid = lat1 + (lat2 - lat1) / 2 # compute the midpoint # compute azimuth - az = vdist(lat1, lon1, lat2, lon2, ell=ell)[1] + az = vdist(lat1, lon1, lat2, lon2, ell=ell)[1] # type: ignore[misc, arg-type] # compute meridional and transverse radii of curvature rho = rcurve.meridian(latmid, ell, deg=True) @@ -137,10 +178,26 @@ def euler( den = rho * sin(az) ** 2 + nu * cos(az) ** 2 # compute radius of the arc from point 1 to point 2 - return rho * nu / den + return rho * nu / den # type: ignore[no-any-return] + + +@overload +def curve( + lat: float, ell: Ellipsoid | None = None, deg: bool = True, method: str = "mean" +) -> float: + pass + + +@overload +def curve( + lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, method: str = "mean" +) -> NDArray[Any]: + pass -def curve(lat, ell: Ellipsoid | None = None, deg: bool = True, method: str = "mean"): +def curve( + lat: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, method: str = "mean" +) -> float | NDArray[Any]: """computes the arithmetic average of the transverse and meridional radii of curvature at a specified latitude point @@ -197,7 +254,7 @@ def triaxial(ell: Ellipsoid | None = None, method: str = "mean") -> float: if method == "mean": return (2 * ell.semimajor_axis + ell.semiminor_axis) / 3 elif method == "norm": - return (ell.semimajor_axis**2 * ell.semiminor_axis) ** (1 / 3) + return (ell.semimajor_axis**2 * ell.semiminor_axis) ** (1 / 3) # type: ignore[no-any-return] else: raise ValueError("method must be mean or norm") @@ -224,6 +281,6 @@ def biaxial(ell: Ellipsoid | None = None, method: str = "mean") -> float: if method == "mean": return (ell.semimajor_axis + ell.semiminor_axis) / 2 elif method == "norm": - return sqrt(ell.semimajor_axis * ell.semiminor_axis) + return sqrt(ell.semimajor_axis * ell.semiminor_axis) # type: ignore[no-any-return] else: raise ValueError("method must be mean or norm") diff --git a/src/pymap3d/sidereal.py b/src/pymap3d/sidereal.py index ff79f13b..66e4e0f7 100644 --- a/src/pymap3d/sidereal.py +++ b/src/pymap3d/sidereal.py @@ -1,7 +1,10 @@ # Copyright (c) 2014-2018 Michael Hirsch, Ph.D. """ manipulations of sidereal time """ +from __future__ import annotations + from datetime import datetime from math import tau +from typing import Sequence, cast, overload from .timeconv import str2dt @@ -16,7 +19,19 @@ __all__ = ["datetime2sidereal", "juliandate", "greenwichsrt"] +@overload def datetime2sidereal(time: datetime, lon_radians: float) -> float: + pass + + +@overload +def datetime2sidereal(time: Sequence[datetime], lon_radians: float) -> list[float]: + pass + + +def datetime2sidereal( + time: datetime | Sequence[datetime], lon_radians: float +) -> float | list[float]: """ Convert ``datetime`` to local sidereal time @@ -34,14 +49,15 @@ def datetime2sidereal(time: datetime, lon_radians: float) -> float: tsr : float Local sidereal time """ - if isinstance(time, (tuple, list)): + if isinstance(time, Sequence): return [datetime2sidereal(t, lon_radians) for t in time] try: - tsr = ( + tsr = cast( + float, Time(time) .sidereal_time(kind="apparent", longitude=Longitude(lon_radians, unit=u.radian)) - .radian + .radian, ) except NameError: jd = juliandate(str2dt(time)) @@ -53,7 +69,17 @@ def datetime2sidereal(time: datetime, lon_radians: float) -> float: return tsr +@overload def juliandate(time: datetime) -> float: + pass + + +@overload +def juliandate(time: Sequence[datetime]) -> list[float]: + pass + + +def juliandate(time: datetime | Sequence[datetime]) -> float | list[float]: """ Python datetime to Julian time (days since Jan 1, 4713 BCE) @@ -72,7 +98,7 @@ def juliandate(time: datetime) -> float: jd : float Julian date (days since Jan 1, 4713 BCE) """ - if isinstance(time, (tuple, list)): + if isinstance(time, Sequence): return list(map(juliandate, time)) if time.month < 3: @@ -89,7 +115,17 @@ def juliandate(time: datetime) -> float: return int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + time.day + B - 1524.5 + C +@overload def greenwichsrt(Jdate: float) -> float: + pass + + +@overload +def greenwichsrt(Jdate: Sequence[float]) -> list[float]: + pass + + +def greenwichsrt(Jdate: float | Sequence[float]) -> float | list[float]: """ Convert Julian time to sidereal time @@ -107,7 +143,7 @@ def greenwichsrt(Jdate: float) -> float: tsr : float Sidereal time """ - if isinstance(Jdate, (tuple, list)): + if isinstance(Jdate, Sequence): return list(map(greenwichsrt, Jdate)) # %% Vallado Eq. 3-42 p. 184, Seidelmann 3.311-1 diff --git a/src/pymap3d/spherical.py b/src/pymap3d/spherical.py index aa99ea4b..4698333a 100644 --- a/src/pymap3d/spherical.py +++ b/src/pymap3d/spherical.py @@ -5,6 +5,15 @@ """ from __future__ import annotations +from typing import Any, Sequence, overload + +try: + from numpy import asarray + from numpy.typing import NDArray +except ImportError: + pass + +from ._types import ArrayLike from .ellipsoid import Ellipsoid from .mathfun import asin, atan2, cbrt, degrees, hypot, power, radians, sin, sqrt from .utils import sanitize @@ -15,13 +24,35 @@ ] +@overload +def geodetic2spherical( + lat: float, + lon: float, + alt: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def geodetic2spherical( + lat: ArrayLike, + lon: ArrayLike, + alt: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def geodetic2spherical( - lat, - lon, - alt, + lat: float | ArrayLike, + lon: float | ArrayLike, + alt: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float | NDArray[Any], float | NDArray[Any], float | NDArray[Any]]: """ point transformation from Geodetic of specified ellipsoid (default WGS-84) to geocentric spherical of the same ellipsoid @@ -61,6 +92,12 @@ def geodetic2spherical( lat, ell = sanitize(lat, ell, deg) if deg: lon = radians(lon) + else: + try: + lon = asarray(lon) + except NameError: + pass + assert not isinstance(lon, Sequence) # Pre-compute to avoid repeated trigonometric functions sinlat = sin(lat) @@ -86,13 +123,35 @@ def geodetic2spherical( return slat, lon, radius +@overload +def spherical2geodetic( + lat: float, + lon: float, + radius: float, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[float, float, float]: + pass + + +@overload +def spherical2geodetic( + lat: ArrayLike, + lon: ArrayLike, + radius: ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + def spherical2geodetic( - lat, - lon, - radius, + lat: float | ArrayLike, + lon: float | ArrayLike, + radius: float | ArrayLike, ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[float | NDArray[Any], float | NDArray[Any], float | NDArray[Any]]: """ point transformation from geocentric spherical of specified ellipsoid (default WGS-84) to geodetic of the same ellipsoid @@ -127,6 +186,12 @@ def spherical2geodetic( lat, ell = sanitize(lat, ell, deg) if deg: lon = radians(lon) + else: + try: + lon = asarray(lon) + except NameError: + pass + assert not isinstance(lon, Sequence) # Pre-compute to avoid repeated trigonometric functions sinlat = sin(lat) diff --git a/src/pymap3d/tests/test_aer.py b/src/pymap3d/tests/test_aer.py index 22bda739..0b14a6e6 100644 --- a/src/pymap3d/tests/test_aer.py +++ b/src/pymap3d/tests/test_aer.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from math import radians import pymap3d as pm @@ -12,7 +14,11 @@ @pytest.mark.parametrize( "aer,lla,xyz", [((33, 70, 1000), (42, -82, 200), (660930.2, -4701424.0, 4246579.6))] ) -def test_aer2ecef(aer, lla, xyz): +def test_aer2ecef( + aer: tuple[float, float, float], + lla: tuple[float, float, float], + xyz: tuple[float, float, float], +) -> None: x, y, z = pm.aer2ecef(*aer, *lla) assert x == approx(xyz[0]) assert y == approx(xyz[1]) @@ -41,7 +47,11 @@ def test_aer2ecef(aer, lla, xyz): ((660930.19276, -4701424.22296, 4246579.60463), (42, -82, 200), (33, 70, 1000)), ], ) -def test_ecef2aer(xyz, lla, aer): +def test_ecef2aer( + xyz: tuple[float, float, float], + lla: tuple[float, float, float], + aer: tuple[float, float, float], +) -> None: assert pm.ecef2aer(*xyz, *lla) == approx(aer) rlla = (radians(lla[0]), radians(lla[1]), lla[2]) @@ -50,7 +60,7 @@ def test_ecef2aer(xyz, lla, aer): @pytest.mark.parametrize("aer,enu", [((33, 70, 1000), (186.2775, 286.8422, 939.6926))]) -def test_aer_enu(aer, enu): +def test_aer_enu(aer: tuple[float, float, float], enu: tuple[float, float, float]) -> None: e, n, u = pm.aer2enu(*aer) assert e == approx(enu[0]) assert n == approx(enu[1]) @@ -76,8 +86,33 @@ def test_aer_enu(aer, enu): assert pm.enu2aer(*enu, deg=False) == approx(raer) +@pytest.mark.parametrize("aer,enu", [(([33], [70], [1000]), ([186.2775], [286.8422], [939.6926]))]) +def test_aer_enu_list( + aer: tuple[list[float], list[float], list[float]], + enu: tuple[list[float], list[float], list[float]], +) -> None: + np = pytest.importorskip("numpy") + + enu1 = pm.aer2enu(*aer) + assert np.isclose(enu1, enu).all() + + raer = ([radians(aer[0][0])], [radians(aer[1][0])], aer[2]) + + enu1 = pm.aer2enu(*raer, deg=False) + assert np.isclose(enu1, enu).all() + + with pytest.raises(ValueError): + pm.aer2enu(aer[0], aer[1], [-1]) + + aer1 = pm.enu2aer(*enu) + assert np.isclose(aer1, aer).all() + + raer1 = pm.enu2aer(*enu, deg=False) + assert np.isclose(raer1, raer).all() + + @pytest.mark.parametrize("aer,ned", [((33, 70, 1000), (286.8422, 186.2775, -939.6926))]) -def test_aer_ned(aer, ned): +def test_aer_ned(aer: tuple[float, float, float], ned: tuple[float, float, float]) -> None: assert pm.aer2ned(*aer) == approx(ned) with pytest.raises(ValueError): diff --git a/src/pymap3d/tests/test_eci.py b/src/pymap3d/tests/test_eci.py index cc8b4728..2c919a2b 100644 --- a/src/pymap3d/tests/test_eci.py +++ b/src/pymap3d/tests/test_eci.py @@ -10,55 +10,55 @@ astropy = None -def test_eci2ecef(): +def test_eci2ecef() -> None: pytest.importorskip("numpy") # this example from Matlab eci2ecef docs - eci = [-2981784, 5207055, 3161595] + eci = [-2981784.0, 5207055.0, 3161595.0] utc = datetime(2019, 1, 4, 12) - ecef = pm.eci2ecef(*eci, utc) + ecef = pm.eci2ecef(*eci, utc) # type: ignore[call-overload] rel = 0.025 if astropy is None else 0.0001 assert ecef == approx([-5.7627e6, -1.6827e6, 3.1560e6], rel=rel) -def test_ecef2eci(): +def test_ecef2eci() -> None: pytest.importorskip("numpy") # this example from Matlab ecef2eci docs ecef = [-5762640, -1682738, 3156028] utc = datetime(2019, 1, 4, 12) - eci = pm.ecef2eci(*ecef, utc) + eci = pm.ecef2eci(*ecef, utc) # type: ignore[call-overload] rel = 0.01 if astropy is None else 0.0001 assert eci == approx([-2981810.6, 5207039.5, 3161595.1], rel=rel) -def test_eci2geodetic(): +def test_eci2geodetic() -> None: pytest.importorskip("numpy") eci = [-2981784, 5207055, 3161595] utc = datetime(2019, 1, 4, 12) - lla = pm.eci2geodetic(*eci, utc) + lla = pm.eci2geodetic(*eci, utc) # type: ignore[call-overload] rel = 0.01 if astropy is None else 0.0001 assert lla == approx([27.880801, -163.722058, 408850.646], rel=rel) -def test_geodetic2eci(): +def test_geodetic2eci() -> None: pytest.importorskip("numpy") lla = [27.880801, -163.722058, 408850.646] utc = datetime(2019, 1, 4, 12) - eci = pm.geodetic2eci(*lla, utc) + eci = pm.geodetic2eci(*lla, utc) # type: ignore[call-overload] rel = 0.01 if astropy is None else 0.0001 assert eci == approx([-2981784, 5207055, 3161595], rel=rel) -def test_eci_aer(): +def test_eci2aer() -> None: # test coords from Matlab eci2aer pytest.importorskip("numpy") t = datetime(2022, 1, 2, 3, 4, 5) @@ -66,17 +66,17 @@ def test_eci_aer(): eci = [4500000, -45000000, 3000000] lla = [28, -80, 100] - aer = pm.eci2aer(*eci, *lla, t) + aer = pm.eci2aer(*eci, *lla, t) # type: ignore[call-overload] rel = 0.01 if astropy is None else 0.0001 - assert aer == approx([314.9945, -53.0089, 5.026e7], rel=rel) + eci = pm.aer2eci(*aer, *lla, t) # type: ignore[call-overload] - eci2 = pm.aer2eci(*aer, *lla, t) + eci2 = pm.aer2eci(*aer, *lla, t) # type: ignore[call-overload] rel = 0.1 if astropy is None else 0.001 assert eci2 == approx(eci, rel=rel) with pytest.raises(ValueError): - pm.aer2eci(aer[0], aer[1], -1, *lla, t) + pm.aer2eci(aer[0], aer[1], -1, *lla, t) # type: ignore[call-overload] diff --git a/src/pymap3d/tests/test_ellipsoid.py b/src/pymap3d/tests/test_ellipsoid.py index 02098787..93e67c14 100755 --- a/src/pymap3d/tests/test_ellipsoid.py +++ b/src/pymap3d/tests/test_ellipsoid.py @@ -1,6 +1,6 @@ +import pymap3d as pm import pytest from pytest import approx -import pymap3d as pm xyz0 = (660e3, -4700e3, 4247e3) @@ -44,11 +44,19 @@ ("pluto", 0.0), ], ) -def test_reference(model, f): +def test_reference(model: str, f: float) -> None: assert pm.Ellipsoid.from_name(model).flattening == approx(f) -def test_ellipsoid(): +@pytest.mark.parametrize("name", ["foo", "bar", "baz"]) +def test_bad_reference(name: str) -> None: + with pytest.raises(ValueError) as excinfo: + pm.Ellipsoid.from_name(name) + + assert str(excinfo.value) == f"{name} model not implemented" + + +def test_ellipsoid() -> None: assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("maupertuis")) == approx( [42.123086280313906, -82.00647850636021, -13462.822154350226] diff --git a/src/pymap3d/tests/test_enu.py b/src/pymap3d/tests/test_enu.py index ed68b386..3cf55a17 100644 --- a/src/pymap3d/tests/test_enu.py +++ b/src/pymap3d/tests/test_enu.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from math import radians import pymap3d as pm @@ -10,35 +12,41 @@ @pytest.mark.parametrize("xyz", [(0, A, 50), ([0], [A], [50])], ids=("scalar", "list")) -def test_scalar_enu(xyz): +def test_scalar_enu( + xyz: tuple[float, float, float] | tuple[list[float], list[float], list[float]] +) -> None: """ verify we can handle the wide variety of input data type users might use """ if isinstance(xyz[0], list): pytest.importorskip("numpy") - enu = pm.ecef2enu(*xyz, 0, 90, -100) - assert pm.enu2ecef(*enu, 0, 90, -100) == approx(xyz) + enu = pm.ecef2enu(*xyz, 0, 90, -100) # type: ignore[call-overload] + assert pm.enu2ecef(*enu, 0, 90, -100) == approx(xyz) # type: ignore[call-overload] -def test_array_enu(): +def test_array_enu() -> None: np = pytest.importorskip("numpy") xyz = (np.asarray(0), np.asarray(A), np.asarray(50)) llh = (np.asarray(0), np.asarray(90), np.asarray(-100)) enu = pm.ecef2enu(*xyz, *llh) - assert pm.enu2ecef(*enu, *llh) == approx(xyz) + assert pm.enu2ecef(*enu, *llh) == approx(xyz) # type: ignore[call-overload] xyz = (np.atleast_1d(0), np.atleast_1d(A), np.atleast_1d(50)) llh = (np.atleast_1d(0), np.atleast_1d(90), np.atleast_1d(-100)) enu = pm.ecef2enu(*xyz, *llh) - assert pm.enu2ecef(*enu, *llh) == approx(xyz) + assert pm.enu2ecef(*enu, *llh) == approx(xyz) # type: ignore[call-overload] @pytest.mark.parametrize( "enu,lla,xyz", [((0, 0, 0), (0, 0, 0), (A, 0, 0)), ((0, 0, 1000), (0, 0, 0), (A + 1000, 0, 0))] ) -def test_enu_ecef(enu, lla, xyz): +def test_enu_ecef( + enu: tuple[float, float, float], + lla: tuple[float, float, float], + xyz: tuple[float, float, float], +) -> None: x, y, z = pm.enu2ecef(*enu, *lla) assert x == approx(xyz[0]) assert y == approx(xyz[1]) diff --git a/src/pymap3d/tests/test_geodetic.py b/src/pymap3d/tests/test_geodetic.py index 4c569c97..1ad80d42 100755 --- a/src/pymap3d/tests/test_geodetic.py +++ b/src/pymap3d/tests/test_geodetic.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from math import isnan, nan, radians, sqrt import pymap3d as pm @@ -39,7 +41,9 @@ @pytest.mark.parametrize("lla", [lla0, ([lla0[0]], [lla0[1]], [lla0[2]])], ids=("scalar", "list")) -def test_scalar_geodetic2ecef(lla): +def test_scalar_geodetic2ecef( + lla: tuple[float, float, float] | tuple[list[float], list[float], list[float]] +) -> None: """ verify we can handle the wide variety of input data type users might use """ @@ -56,7 +60,7 @@ def test_scalar_geodetic2ecef(lla): assert lla1 == approx(lla, rel=1e-4) -def test_array_geodetic2ecef(): +def test_array_geodetic2ecef() -> None: np = pytest.importorskip("numpy") lla = (np.asarray(lla0[0]), np.asarray(lla0[1]), np.asarray(lla0[2])) @@ -69,7 +73,9 @@ def test_array_geodetic2ecef(): @pytest.mark.parametrize("xyz", [xyz0, ([xyz0[0]], [xyz0[1]], [xyz0[2]])], ids=("scalar", "list")) -def test_scalar_ecef2geodetic(xyz): +def test_scalar_ecef2geodetic( + xyz: tuple[float, float, float] | tuple[list[float], list[float], list[float]] +) -> None: """ verify we can handle the wide variety of input data type users might use """ @@ -86,7 +92,7 @@ def test_scalar_ecef2geodetic(xyz): assert xyz1 == approx(xyz, rel=1e-4) -def test_array_ecef2geodetic(): +def test_array_ecef2geodetic() -> None: np = pytest.importorskip("numpy") xyz = (np.asarray(xyz0[0]), np.asarray(xyz0[1]), np.asarray(xyz0[2])) @@ -98,7 +104,7 @@ def test_array_ecef2geodetic(): assert np.isclose(pm.geodetic2ecef(*lla), xyz).all() -def test_inside_ecef2geodetic(): +def test_inside_ecef2geodetic() -> None: np = pytest.importorskip("numpy") # test values with no points inside ellipsoid @@ -128,7 +134,7 @@ def test_inside_ecef2geodetic(): assert alts == approx(lla0_array_inside[2]) -def test_xarray_ecef(): +def test_xarray_ecef() -> None: xarray = pytest.importorskip("xarray") lla = xarray.DataArray(list(lla0)) @@ -138,7 +144,7 @@ def test_xarray_ecef(): assert lla1 == approx(lla) -def test_pandas_ecef(): +def test_pandas_ecef() -> None: pandas = pytest.importorskip("pandas") x, y, z = pm.geodetic2ecef( @@ -151,7 +157,7 @@ def test_pandas_ecef(): assert alt == approx(lla0[2]) -def test_ecef(): +def test_ecef() -> None: xyz = pm.geodetic2ecef(*lla0) assert xyz == approx(xyz0) @@ -170,12 +176,12 @@ def test_ecef(): @pytest.mark.parametrize("lla, xyz", llaxyz) -def test_geodetic2ecef(lla, xyz): +def test_geodetic2ecef(lla: tuple[float, float, float], xyz: tuple[float, float, float]) -> None: assert pm.geodetic2ecef(*lla) == approx(xyz, abs=atol_dist) @pytest.mark.parametrize("xyz, lla", xyzlla) -def test_ecef2geodetic(xyz, lla): +def test_ecef2geodetic(xyz: tuple[float, float, float], lla: tuple[float, float, float]) -> None: lat, lon, alt = pm.ecef2geodetic(*xyz) assert lat == approx(lla[0]) assert lon == approx(lla[1]) @@ -189,7 +195,11 @@ def test_ecef2geodetic(xyz, lla): ((0, 90, 10000), (0, 0, 10000), (0, 0, 0)), ], ) -def test_aer_geodetic(aer, lla, lla0): +def test_aer_geodetic( + aer: tuple[float, float, float], + lla: tuple[float, float, float], + lla0: tuple[float, float, float], +) -> None: lat1, lon1, alt1 = pm.aer2geodetic(*aer, *lla0) assert lat1 == approx(lla[0]) assert lon1 == approx(lla[1]) @@ -213,7 +223,7 @@ def test_aer_geodetic(aer, lla, lla0): ) -def test_scalar_nan(): +def test_scalar_nan() -> None: a, e, r = pm.geodetic2aer(nan, nan, nan, *lla0) assert isnan(a) and isnan(e) and isnan(r) @@ -221,7 +231,7 @@ def test_scalar_nan(): assert isnan(lat) and isnan(lon) and isnan(alt) -def test_allnan(): +def test_allnan() -> None: np = pytest.importorskip("numpy") anan = np.empty((10, 10)) anan.fill(nan) @@ -229,7 +239,7 @@ def test_allnan(): assert np.isnan(pm.aer2geodetic(anan, anan, anan, *lla0)).all() -def test_somenan(): +def test_somenan() -> None: np = pytest.importorskip("numpy") xyz = np.stack((xyz0, (nan, nan, nan))) @@ -238,7 +248,9 @@ def test_somenan(): @pytest.mark.parametrize("xyz, lla", xyzlla) -def test_numpy_ecef2geodetic(xyz, lla): +def test_numpy_ecef2geodetic( + xyz: tuple[float, float, float], lla: tuple[float, float, float] +) -> None: np = pytest.importorskip("numpy") lat, lon, alt = pm.ecef2geodetic( *np.array( @@ -254,7 +266,9 @@ def test_numpy_ecef2geodetic(xyz, lla): @pytest.mark.parametrize("lla, xyz", llaxyz) -def test_numpy_geodetic2ecef(lla, xyz): +def test_numpy_geodetic2ecef( + lla: tuple[float, float, float], xyz: tuple[float, float, float] +) -> None: np = pytest.importorskip("numpy") x, y, z = pm.geodetic2ecef( *np.array( diff --git a/src/pymap3d/tests/test_latitude.py b/src/pymap3d/tests/test_latitude.py index b786da4b..8495e861 100644 --- a/src/pymap3d/tests/test_latitude.py +++ b/src/pymap3d/tests/test_latitude.py @@ -10,7 +10,7 @@ "geodetic_lat,alt_m,geocentric_lat", [(0, 0, 0), (90, 0, 90), (-90, 0, -90), (45, 0, 44.80757678), (-45, 0, -44.80757678)], ) -def test_geodetic_alt_geocentric(geodetic_lat, alt_m, geocentric_lat): +def test_geodetic_alt_geocentric(geodetic_lat: float, alt_m: float, geocentric_lat: float) -> None: assert latitude.geod2geoc(geodetic_lat, alt_m) == approx(geocentric_lat) r = rcurve.geocentric_radius(geodetic_lat) @@ -19,6 +19,8 @@ def test_geodetic_alt_geocentric(geodetic_lat, alt_m, geocentric_lat): latitude.geocentric2geodetic(geocentric_lat, 1e5 + alt_m) ) + assert latitude.geoc2geod([geocentric_lat], [r]) == approx(geodetic_lat) + assert latitude.geod2geoc(geodetic_lat, 1e5 + alt_m) == approx( latitude.geodetic2geocentric(geodetic_lat, 1e5 + alt_m) ) @@ -28,7 +30,7 @@ def test_geodetic_alt_geocentric(geodetic_lat, alt_m, geocentric_lat): "geodetic_lat,geocentric_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.80757678), (-45, -44.80757678)], ) -def test_geodetic_geocentric(geodetic_lat, geocentric_lat): +def test_geodetic_geocentric(geodetic_lat: float, geocentric_lat: float) -> None: assert latitude.geodetic2geocentric(geodetic_lat, 0) == approx(geocentric_lat) assert latitude.geodetic2geocentric(radians(geodetic_lat), 0, deg=False) == approx( @@ -41,7 +43,7 @@ def test_geodetic_geocentric(geodetic_lat, geocentric_lat): ) -def test_numpy_geodetic_geocentric(): +def test_numpy_geodetic_geocentric() -> None: pytest.importorskip("numpy") assert latitude.geodetic2geocentric([45, 0], 0) == approx([44.80757678, 0]) assert latitude.geocentric2geodetic([44.80757678, 0], 0) == approx([45, 0]) @@ -58,7 +60,7 @@ def test_numpy_geodetic_geocentric(): (89, 271.275), ], ) -def test_geodetic_isometric(geodetic_lat, isometric_lat): +def test_geodetic_isometric(geodetic_lat: float, isometric_lat: float) -> None: isolat = latitude.geodetic2isometric(geodetic_lat) assert isolat == approx(isometric_lat) assert isinstance(isolat, float) @@ -73,7 +75,7 @@ def test_geodetic_isometric(geodetic_lat, isometric_lat): ) -def test_numpy_geodetic_isometric(): +def test_numpy_geodetic_isometric() -> None: pytest.importorskip("numpy") assert latitude.geodetic2isometric([45, 0]) == approx([50.227466, 0]) assert latitude.isometric2geodetic([50.227466, 0]) == approx([45, 0]) @@ -83,7 +85,7 @@ def test_numpy_geodetic_isometric(): "geodetic_lat,conformal_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.80768406), (-45, -44.80768406), (89, 88.99327)], ) -def test_geodetic_conformal(geodetic_lat, conformal_lat): +def test_geodetic_conformal(geodetic_lat: float, conformal_lat: float) -> None: clat = latitude.geodetic2conformal(geodetic_lat) assert clat == approx(conformal_lat) assert isinstance(clat, float) @@ -98,7 +100,7 @@ def test_geodetic_conformal(geodetic_lat, conformal_lat): ) -def test_numpy_geodetic_conformal(): +def test_numpy_geodetic_conformal() -> None: pytest.importorskip("numpy") assert latitude.geodetic2conformal([45, 0]) == approx([44.80768406, 0]) assert latitude.conformal2geodetic([44.80768406, 0]) == approx([45, 0]) @@ -108,7 +110,7 @@ def test_numpy_geodetic_conformal(): "geodetic_lat,rectifying_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.855682), (-45, -44.855682)], ) -def test_geodetic_rectifying(geodetic_lat, rectifying_lat): +def test_geodetic_rectifying(geodetic_lat: float, rectifying_lat: float) -> None: assert latitude.geodetic2rectifying(geodetic_lat) == approx(rectifying_lat) assert latitude.geodetic2rectifying(radians(geodetic_lat), deg=False) == approx( radians(rectifying_lat) @@ -120,7 +122,7 @@ def test_geodetic_rectifying(geodetic_lat, rectifying_lat): ) -def test_numpy_geodetic_rectifying(): +def test_numpy_geodetic_rectifying() -> None: pytest.importorskip("numpy") assert latitude.geodetic2rectifying([45, 0]) == approx([44.855682, 0]) assert latitude.rectifying2geodetic([44.855682, 0]) == approx([45, 0]) @@ -130,7 +132,7 @@ def test_numpy_geodetic_rectifying(): "geodetic_lat,authalic_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.87170288), (-45, -44.87170288)], ) -def test_geodetic_authalic(geodetic_lat, authalic_lat): +def test_geodetic_authalic(geodetic_lat: float, authalic_lat: float) -> None: assert latitude.geodetic2authalic(geodetic_lat) == approx(authalic_lat) assert latitude.geodetic2authalic(radians(geodetic_lat), deg=False) == approx( radians(authalic_lat) @@ -142,7 +144,7 @@ def test_geodetic_authalic(geodetic_lat, authalic_lat): ) -def test_numpy_geodetic_authalic(): +def test_numpy_geodetic_authalic() -> None: pytest.importorskip("numpy") assert latitude.geodetic2authalic([45, 0]) == approx([44.87170288, 0]) assert latitude.authalic2geodetic([44.87170288, 0]) == approx([45, 0]) @@ -152,7 +154,7 @@ def test_numpy_geodetic_authalic(): "geodetic_lat,parametric_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.9037878), (-45, -44.9037878)], ) -def test_geodetic_parametric(geodetic_lat, parametric_lat): +def test_geodetic_parametric(geodetic_lat: float, parametric_lat: float) -> None: assert latitude.geodetic2parametric(geodetic_lat) == approx(parametric_lat) assert latitude.geodetic2parametric(radians(geodetic_lat), deg=False) == approx( radians(parametric_lat) @@ -164,14 +166,14 @@ def test_geodetic_parametric(geodetic_lat, parametric_lat): ) -def test_numpy_geodetic_parametric(): +def test_numpy_geodetic_parametric() -> None: pytest.importorskip("numpy") assert latitude.geodetic2parametric([45, 0]) == approx([44.9037878, 0]) assert latitude.parametric2geodetic([44.9037878, 0]) == approx([45, 0]) @pytest.mark.parametrize("lat", [91, -91]) -def test_badvals(lat): +def test_badvals(lat: float) -> None: # geodetic_isometric is not included on purpose with pytest.raises(ValueError): latitude.geodetic2geocentric(lat, 0) diff --git a/src/pymap3d/tests/test_look_spheroid.py b/src/pymap3d/tests/test_look_spheroid.py index 0915e718..f8ed9126 100644 --- a/src/pymap3d/tests/test_look_spheroid.py +++ b/src/pymap3d/tests/test_look_spheroid.py @@ -17,7 +17,7 @@ (125, 45, 9.99481382, -19.992528, 1.414324671e3), ], ) -def test_losint(az, tilt, lat, lon, sr): +def test_losint(az: float, tilt: float, lat: float, lon: float, sr: float) -> None: lla0 = (10, -20, 1e3) lat1, lon1, sr1 = los.lookAtSpheroid(*lla0, az, tilt=tilt) @@ -32,19 +32,19 @@ def test_losint(az, tilt, lat, lon, sr): assert isinstance(sr1, float) -def test_badval(): +def test_badval() -> None: with pytest.raises(ValueError): los.lookAtSpheroid(0, 0, -1, 0, 0) -def test_array_los(): +def test_array_los() -> None: np = pytest.importorskip("numpy") az = [0.0, 10.0, 125.0] tilt = [30.0, 45.0, 90.0] - lat, lon, sr = los.lookAtSpheroid(*lla0, az, tilt) + lat, lon, sr = los.lookAtSpheroid(*lla0, az, tilt) # type: ignore[call-overload] truth = np.array( [ @@ -60,27 +60,27 @@ def test_array_los(): assert np.column_stack((lat, lon, sr)) == approx(truth, nan_ok=True) -def test_xarray_los(): +def test_xarray_los() -> None: xarray = pytest.importorskip("xarray") lla = xarray.DataArray(list(lla0)) az = xarray.DataArray([0.0] * 2) tilt = xarray.DataArray([30.0] * 2) - lat, lon, sr = los.lookAtSpheroid(*lla, az, tilt) + lat, lon, sr = los.lookAtSpheroid(*lla, az, tilt) # type: ignore[call-overload] assert lat == approx(42.00103959) assert lon == approx(lla0[1]) assert sr == approx(230.9413173) -def test_pandas_los(): +def test_pandas_los() -> None: pandas = pytest.importorskip("pandas") lla = pandas.Series(lla0) az = pandas.Series([0.0] * 2) tilt = pandas.Series([30.0] * 2) - lat, lon, sr = los.lookAtSpheroid(*lla, az, tilt) + lat, lon, sr = los.lookAtSpheroid(*lla, az, tilt) # type: ignore[call-overload] assert lat == approx(42.00103959) assert lon == approx(lla0[1]) assert sr == approx(230.9413173) diff --git a/src/pymap3d/tests/test_ned.py b/src/pymap3d/tests/test_ned.py index 54d0b8a2..b9784f43 100755 --- a/src/pymap3d/tests/test_ned.py +++ b/src/pymap3d/tests/test_ned.py @@ -1,4 +1,5 @@ import pymap3d as pm +import pytest from pytest import approx lla0 = (42, -82, 200) @@ -9,7 +10,7 @@ B = ELL.semiminor_axis -def test_ecef_ned(): +def test_ecef_ned() -> None: enu = pm.aer2enu(*aer0) ned = (enu[1], enu[0], -enu[2]) xyz = pm.aer2ecef(*aer0, *lla0) @@ -22,7 +23,7 @@ def test_ecef_ned(): assert pm.ned2ecef(*ned, *lla0) == approx(xyz) -def test_enuv_nedv(): +def test_enuv_nedv() -> None: vx, vy, vz = (5, 3, 2) ve, vn, vu = (5.368859646588048, 3.008520763668120, -0.352347711524077) assert pm.ecef2enuv(vx, vy, vz, *lla0[:2]) == approx((ve, vn, vu)) @@ -30,7 +31,7 @@ def test_enuv_nedv(): assert pm.ecef2nedv(vx, vy, vz, *lla0[:2]) == approx((vn, ve, -vu)) -def test_ned_geodetic(): +def test_ned_geodetic() -> None: lat1, lon1, alt1 = pm.aer2geodetic(*aer0, *lla0) enu3 = pm.geodetic2enu(lat1, lon1, alt1, *lla0) @@ -53,3 +54,24 @@ def test_ned_geodetic(): assert isinstance(lat, float) assert isinstance(lon, float) assert isinstance(alt, float) + + +def test_ned_geodetic_list() -> None: + np = pytest.importorskip("numpy") + + lla1 = tuple(map(lambda z: [z], lla0)) + aer1 = tuple(map(lambda z: [z], aer0)) + + lla2 = pm.aer2geodetic(*aer1, *lla1) # type: ignore[call-overload] + + enu3 = pm.geodetic2enu(*lla2, *lla1) + ned3 = (enu3[1], enu3[0], -enu3[2]) + + ned4 = pm.geodetic2ned(*lla2, *lla1) + assert np.isclose(ned3, ned4).all() + + lla3 = pm.enu2geodetic(*enu3, *lla1) + assert np.isclose(lla2, lla3).all() + + lla4 = pm.ned2geodetic(*ned3, *lla1) # type: ignore[call-overload] + assert np.isclose(lla2, lla4).all() diff --git a/src/pymap3d/tests/test_pyproj.py b/src/pymap3d/tests/test_pyproj.py index 103b2aed..c64ccc8a 100755 --- a/src/pymap3d/tests/test_pyproj.py +++ b/src/pymap3d/tests/test_pyproj.py @@ -6,7 +6,7 @@ lla0 = [42, -82, 200] -def test_compare_vicenty(): +def test_compare_vicenty() -> None: taz, tsr = 38, 3000 pyproj = pytest.importorskip("pyproj") @@ -20,10 +20,10 @@ def test_compare_vicenty(): assert (p4az, p4sr) == approx((taz, tsr)) -def test_compare_geodetic(): +def test_compare_geodetic() -> None: pyproj = pytest.importorskip("pyproj") - xyz = pm.geodetic2ecef(*lla0) + xyz = pm.geodetic2ecef(*lla0) # type: ignore[call-overload] ecef = pyproj.Proj(proj="geocent", ellps="WGS84", datum="WGS84") lla = pyproj.Proj(proj="latlong", ellps="WGS84", datum="WGS84") diff --git a/src/pymap3d/tests/test_rcurve.py b/src/pymap3d/tests/test_rcurve.py index 60cedf57..2046c39f 100644 --- a/src/pymap3d/tests/test_rcurve.py +++ b/src/pymap3d/tests/test_rcurve.py @@ -10,11 +10,11 @@ @pytest.mark.parametrize( "lat,curvature", [(0, A), (90, 0), (-90, 0), (45.0, 4517590.87884893), (-45, 4517590.87884893)] ) -def test_rcurve_parallel(lat, curvature): +def test_rcurve_parallel(lat: float, curvature: float) -> None: assert rcurve.parallel(lat) == approx(curvature, abs=1e-9, rel=1e-6) -def test_numpy_parallel(): +def test_numpy_parallel() -> None: pytest.importorskip("numpy") assert rcurve.parallel([0, 90]) == approx([A, 0], abs=1e-9, rel=1e-6) @@ -29,15 +29,15 @@ def test_numpy_parallel(): (-45, 6367381.8156), ], ) -def test_rcurve_meridian(lat, curvature): +def test_rcurve_meridian(lat: float, curvature: float) -> None: assert rcurve.meridian(lat) == approx(curvature) -def test_numpy_meridian(): +def test_numpy_meridian() -> None: pytest.importorskip("numpy") assert rcurve.meridian([0, 90]) == approx([6335439.327, 6399593.6258]) -def test_numpy_transverse(): +def test_numpy_transverse() -> None: pytest.importorskip("numpy") assert rcurve.transverse([-90, 0, 90]) == approx([6399593.6258, A, 6399593.6258]) diff --git a/src/pymap3d/tests/test_rhumb.py b/src/pymap3d/tests/test_rhumb.py index 4160400e..f8817319 100755 --- a/src/pymap3d/tests/test_rhumb.py +++ b/src/pymap3d/tests/test_rhumb.py @@ -1,10 +1,12 @@ +from math import radians + import pymap3d.lox as lox import pytest from pytest import approx @pytest.mark.parametrize("lat,dist", [(0, 0), (90, 10001965.729)]) -def test_meridian_dist(lat, dist): +def test_meridian_dist(lat: float, dist: float) -> None: assert lox.meridian_dist(lat) == approx(dist) @@ -18,7 +20,7 @@ def test_meridian_dist(lat, dist): (40, 80, 4455610.84159), ], ) -def test_meridian_arc(lat1, lat2, arclen): +def test_meridian_arc(lat1: float, lat2: float, arclen: float) -> None: """ meridianarc(deg2rad(40), deg2rad(80), wgs84Ellipsoid) """ @@ -36,8 +38,9 @@ def test_meridian_arc(lat1, lat2, arclen): (-90, 0, 0, 10018754.1714), ], ) -def test_departure(lon1, lon2, lat, dist): +def test_departure(lon1: float, lon2: float, lat: float, dist: float) -> None: assert lox.departure(lon1, lon2, lat) == approx(dist) + # assert lox.departure([lon1], [lon2], [lat]) == approx(dist) @pytest.mark.parametrize( @@ -52,7 +55,9 @@ def test_departure(lon1, lon2, lat, dist): (-1, 0, 0, 0, 110574.4, 0), ], ) -def test_loxodrome_inverse(lat1, lon1, lat2, lon2, arclen, az): +def test_loxodrome_inverse( + lat1: float, lon1: float, lat2: float, lon2: float, arclen: float, az: float +) -> None: """ distance('rh', 40, -80, 65, -148, wgs84Ellipsoid) azimuth('rh', 40, -80, 65, -148, wgs84Ellipsoid) @@ -65,7 +70,7 @@ def test_loxodrome_inverse(lat1, lon1, lat2, lon2, arclen, az): assert isinstance(rhaz, float) -def test_numpy_loxodrome_inverse(): +def test_numpy_loxodrome_inverse() -> None: pytest.importorskip("numpy") d, a = lox.loxodrome_inverse([40, 40], [-80, -80], 65, -148) assert d == approx(5248666.209) @@ -75,7 +80,7 @@ def test_numpy_loxodrome_inverse(): d, a = lox.loxodrome_inverse([40, 40], [-80, -80], 65, [-148, -148]) -def test_numpy_2d_loxodrome_inverse(): +def test_numpy_2d_loxodrome_inverse() -> None: pytest.importorskip("numpy") d, a = lox.loxodrome_inverse([[40, 40], [40, 40]], [[-80, -80], [-80, -80]], 65, -148) assert d == approx(5248666.209) @@ -105,7 +110,9 @@ def test_numpy_2d_loxodrome_inverse(): (-1, 0, 110574.4, 0, 0, 0), ], ) -def test_loxodrome_direct(lat0, lon0, rng, az, lat1, lon1): +def test_loxodrome_direct( + lat0: float, lon0: float, rng: float, az: float, lat1: float, lon1: float +) -> None: """ reckon('rh', 40, -80, 10, 30, wgs84Ellipsoid) """ @@ -116,18 +123,22 @@ def test_loxodrome_direct(lat0, lon0, rng, az, lat1, lon1): assert isinstance(lon2, float) -def test_numpy_loxodrome_direct(): +def test_numpy_loxodrome_direct() -> None: pytest.importorskip("numpy") lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], [10, 10], [30, 30]) assert lat == approx(40.000078) assert lon == approx(-79.99994145) + lat, lon = lox.loxodrome_direct( + [radians(40)], [radians(-80)], [radians(10)], [radians(30)], deg=False + ) + lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], 10, 30) lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], [10, 10], 30) lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], 10, [30, 30]) @pytest.mark.parametrize("lat,lon", [([0, 45, 90], [0, 45, 90])]) -def test_meanm(lat, lon): +def test_meanm(lat: float, lon: float) -> None: pytest.importorskip("numpy") assert lox.meanm(lat, lon) == approx([47.26967, 18.460557]) diff --git a/src/pymap3d/tests/test_rsphere.py b/src/pymap3d/tests/test_rsphere.py index 2ad4b62e..249c750a 100644 --- a/src/pymap3d/tests/test_rsphere.py +++ b/src/pymap3d/tests/test_rsphere.py @@ -8,7 +8,7 @@ A = ell.semimajor_axis -def test_geocentric_radius(): +def test_geocentric_radius() -> None: assert rcurve.geocentric_radius(0) == approx(ell.semimajor_axis) assert rcurve.geocentric_radius(90) == approx(ell.semiminor_axis) assert rcurve.geocentric_radius(45) == approx(6367490.0) @@ -16,35 +16,35 @@ def test_geocentric_radius(): @pytest.mark.parametrize("bad_lat", [-91, 91]) -def test_geocentric_radius_badval(bad_lat): +def test_geocentric_radius_badval(bad_lat: float) -> None: with pytest.raises(ValueError): rcurve.geocentric_radius(bad_lat) -def test_rsphere_eqavol(): +def test_rsphere_eqavol() -> None: assert rsphere.eqavol() == approx(6371000.8049) -def test_rsphere_authalic(): +def test_rsphere_authalic() -> None: assert rsphere.authalic() == approx(6371007.1809) -def test_rsphere_rectifying(): +def test_rsphere_rectifying() -> None: assert rsphere.rectifying() == approx(6367449.1458) -def test_rsphere_biaxial(): +def test_rsphere_biaxial() -> None: assert rsphere.biaxial() == approx(6367444.657) -def test_rsphere_triaxial(): +def test_rsphere_triaxial() -> None: assert rsphere.triaxial() == approx(6371008.77) -def test_rsphere_euler(): +def test_rsphere_euler() -> None: assert rsphere.euler(42, 82, 44, 100) == approx(6386606.829131) -def test_numpy_rsphere_euler(): +def test_numpy_rsphere_euler() -> None: pytest.importorskip("numpy") assert rsphere.euler([42, 0], [82, 0], 44, 100) == approx([6386606.829131, 6363111.70923164]) diff --git a/src/pymap3d/tests/test_sidereal.py b/src/pymap3d/tests/test_sidereal.py index 563928ee..e1c3d2c9 100755 --- a/src/pymap3d/tests/test_sidereal.py +++ b/src/pymap3d/tests/test_sidereal.py @@ -13,7 +13,7 @@ @pytest.mark.parametrize("time", [t0, [t0]]) -def test_sidereal(time): +def test_sidereal(time: datetime) -> None: # http://www.jgiesen.de/astro/astroJS/siderealClock/ tsr = pmd.datetime2sidereal(time, radians(lon)) if isinstance(tsr, list): @@ -21,11 +21,25 @@ def test_sidereal(time): assert tsr == approx(sra, rel=1e-5) -def test_anglesep(): +def test_anglesep() -> None: pytest.importorskip("astropy") assert pmh.anglesep(35, 23, 84, 20) == approx(ha) -def test_anglesep_meeus(): +def test_anglesep_list() -> None: + # %% compare with astropy + np = pytest.importorskip("numpy") + ha1 = pmh.anglesep([radians(35)], [radians(23)], [radians(84)], [radians(20)], False) + assert np.isclose(ha1, radians(ha)).all() + + +def test_anglesep_meeus() -> None: # %% compare with astropy assert pmh.anglesep_meeus(35, 23, 84, 20) == approx(ha) + + +def test_anglesep_meeus_list() -> None: + # %% compare with astropy + np = pytest.importorskip("numpy") + ha1 = pmh.anglesep_meeus([radians(35)], [radians(23)], [radians(84)], [radians(20)], False) + assert np.isclose(ha1, radians(ha)).all() diff --git a/src/pymap3d/tests/test_sky.py b/src/pymap3d/tests/test_sky.py index 778e2f9a..3dfa4612 100755 --- a/src/pymap3d/tests/test_sky.py +++ b/src/pymap3d/tests/test_sky.py @@ -11,23 +11,23 @@ radec = (166.5032081149338, 55.000011165405752) -def test_azel2radec(): +def test_azel2radec() -> None: radec1 = pm.azel2radec(*azel, lat, lon, t0) assert radec1 == approx(radec, rel=0.01) -def test_numpy_azel2radec(): +def test_numpy_azel2radec() -> None: pytest.importorskip("numpy") radec1 = pm.azel2radec([180.1, 180.1], [80, 80], lat, lon, t0) assert radec1 == approx(radec, rel=0.01) -def test_radec2azel(): +def test_radec2azel() -> None: azel1 = pm.radec2azel(*radec, lat, lon, t0) assert azel1 == approx(azel, rel=0.01) -def test_numpy_radec2azel(): +def test_numpy_radec2azel() -> None: pytest.importorskip("numpy") azel1 = pm.radec2azel([166.503208, 166.503208], [55, 55], lat, lon, t0) assert azel1 == approx(azel, rel=0.01) diff --git a/src/pymap3d/tests/test_spherical.py b/src/pymap3d/tests/test_spherical.py index f6abcfda..8f874b7e 100755 --- a/src/pymap3d/tests/test_spherical.py +++ b/src/pymap3d/tests/test_spherical.py @@ -1,13 +1,15 @@ +from __future__ import annotations + +from typing import Any + import pytest from pytest import approx try: from numpy import asarray + from numpy.typing import NDArray except ImportError: - - def asarray(*args): # type: ignore - "dummy function to convert values to arrays" - return args + pass import pymap3d as pm @@ -16,7 +18,7 @@ def asarray(*args): # type: ignore A = ELL.semimajor_axis B = ELL.semiminor_axis -llrlla = [ +llrlla: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = [ ((0, 0, A - 1), (0, 0, -1)), ((0, 90, A - 1), (0, 90, -1)), ((0, -90, A + 1), (0, -90, 1)), @@ -25,7 +27,7 @@ def asarray(*args): # type: ignore ((90, 15, B - 1), (90, 15, -1)), ((-90, 0, B + 1), (-90, 0, 1)), ] -llallr = [ +llallr: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = [ ((0, 0, -1), (0, 0, A - 1)), ((0, 90, -1), (0, 90, A - 1)), ((0, -90, 1), (0, -90, A + 1)), @@ -36,53 +38,62 @@ def asarray(*args): # type: ignore ] llallr_list = [([[i] for i in lla], llr) for lla, llr in llallr] llrlla_list = [([[i] for i in llr], lla) for llr, lla in llrlla] -llallr_array = [([asarray(i) for i in lla], llr) for lla, llr in llallr] -llrlla_array = [([asarray(i) for i in llr], lla) for llr, lla in llrlla] +try: + llallr_array = [([asarray(i) for i in lla], llr) for lla, llr in llallr] + llrlla_array = [([asarray(i) for i in llr], lla) for llr, lla in llrlla] +except NameError: + llallr_array = [] + llrlla_array = [] + atol_dist = 1e-6 # 1 micrometer @pytest.mark.parametrize("lla, llr", llallr) -def test_geodetic2spherical(lla, llr): +def test_geodetic2spherical( + lla: tuple[float, float, float], llr: tuple[float, float, float] +) -> None: coords = pm.geodetic2spherical(*lla) assert coords[:2] == approx(llr[:2]) assert coords[2] == approx(llr[2], abs=atol_dist) @pytest.mark.parametrize("llr, lla", llrlla) -def test_spherical2geodetic(llr, lla): +def test_spherical2geodetic( + llr: tuple[float, float, float], lla: tuple[float, float, float] +) -> None: coords = pm.spherical2geodetic(*llr) assert coords[:2] == approx(lla[:2]) assert coords[2] == approx(lla[2], abs=atol_dist) @pytest.mark.parametrize("lla, llr", llallr_list) -def test_geodetic2spherical_list(lla, llr): +def test_geodetic2spherical_list(lla: list[list[float]], llr: tuple[float, float, float]) -> None: pytest.importorskip("numpy") - coords = pm.geodetic2spherical(*lla) + coords = pm.geodetic2spherical(*lla) # type: ignore[call-overload] assert coords[:2] == approx(llr[:2]) assert coords[2] == approx(llr[2], abs=atol_dist) @pytest.mark.parametrize("llr, lla", llrlla_list) -def test_spherical2geodetic_list(llr, lla): +def test_spherical2geodetic_list(llr: list[list[float]], lla: tuple[float, float, float]) -> None: pytest.importorskip("numpy") - coords = pm.spherical2geodetic(*llr) + coords = pm.spherical2geodetic(*llr) # type: ignore[call-overload] assert coords[:2] == approx(lla[:2]) assert coords[2] == approx(lla[2], abs=atol_dist) @pytest.mark.parametrize("lla, llr", llallr_array) -def test_geodetic2spherical_array(lla, llr): +def test_geodetic2spherical_array(lla: list[NDArray[Any]], llr: tuple[float, float, float]) -> None: pytest.importorskip("numpy") - coords = pm.geodetic2spherical(*lla) + coords = pm.geodetic2spherical(*lla) # type: ignore[call-overload] assert coords[:2] == approx(llr[:2]) assert coords[2] == approx(llr[2], abs=atol_dist) @pytest.mark.parametrize("llr, lla", llrlla_array) -def test_spherical2geodetic_array(llr, lla): +def test_spherical2geodetic_array(llr: list[NDArray[Any]], lla: tuple[float, float, float]) -> None: pytest.importorskip("numpy") - coords = pm.spherical2geodetic(*llr) + coords = pm.spherical2geodetic(*llr) # type: ignore[call-overload] assert coords[:2] == approx(lla[:2]) assert coords[2] == approx(lla[2], abs=atol_dist) diff --git a/src/pymap3d/tests/test_time.py b/src/pymap3d/tests/test_time.py index 2edb4d50..43f0d2e6 100755 --- a/src/pymap3d/tests/test_time.py +++ b/src/pymap3d/tests/test_time.py @@ -1,4 +1,13 @@ +from __future__ import annotations + from datetime import datetime +from typing import cast + +try: + from numpy import datetime64 + from numpy.typing import NDArray +except ImportError: + pass import pymap3d.sidereal as pms import pytest @@ -8,11 +17,11 @@ t0 = datetime(2014, 4, 6, 8) -def test_juliantime(): +def test_juliantime() -> None: assert pms.juliandate(t0) == approx(2.456753833333e6) -def test_types(): +def test_types() -> None: np = pytest.importorskip("numpy") assert str2dt(t0) == t0 # passthrough assert str2dt("2014-04-06T08:00:00") == t0 @@ -24,16 +33,17 @@ def test_types(): assert (np.asarray(str2dt(t1)) == t0).all() -def test_datetime64(): +def test_datetime64() -> None: np = pytest.importorskip("numpy") - t1 = np.datetime64(t0) + t1: datetime64 | NDArray[datetime64] + t1 = cast(datetime64, np.datetime64(t0)) assert str2dt(t1) == t0 - t1 = np.array([np.datetime64(t0), np.datetime64(t0)]) + t1 = cast(NDArray[datetime64], np.array([np.datetime64(t0), np.datetime64(t0)])) assert (str2dt(t1) == t0).all() -def test_xarray_time(): +def test_xarray_time() -> None: xarray = pytest.importorskip("xarray") t = {"time": t0} @@ -45,7 +55,7 @@ def test_xarray_time(): assert (str2dt(ds["time"]) == t0).all() -def test_pandas_time(): +def test_pandas_time() -> None: pandas = pytest.importorskip("pandas") t = pandas.Series(t0) diff --git a/src/pymap3d/tests/test_vincenty.py b/src/pymap3d/tests/test_vincenty.py index a9717675..7f253047 100755 --- a/src/pymap3d/tests/test_vincenty.py +++ b/src/pymap3d/tests/test_vincenty.py @@ -2,7 +2,7 @@ from pytest import approx -def test_track2(): +def test_track2() -> None: lats, lons = vincenty.track2(40, 80, 65, -148, npts=3) assert lats == approx([40, 69.633139886, 65]) assert lons == approx([80, 113.06849104, -148]) diff --git a/src/pymap3d/tests/test_vincenty_dist.py b/src/pymap3d/tests/test_vincenty_dist.py index a2d50733..63e13e39 100644 --- a/src/pymap3d/tests/test_vincenty_dist.py +++ b/src/pymap3d/tests/test_vincenty_dist.py @@ -21,7 +21,7 @@ (90, 0, -90, 0, 2.000393145e7, 180), ], ) -def test_unit(lat, lon, lat1, lon1, srange, az): +def test_unit(lat: float, lon: float, lat1: float, lon1: float, srange: float, az: float) -> None: dist, az1 = vincenty.vdist(lat, lon, lat1, lon1) assert dist == approx(srange, rel=0.005) assert az1 == approx(az) @@ -30,7 +30,7 @@ def test_unit(lat, lon, lat1, lon1, srange, az): assert isinstance(az1, float) -def test_vector(): +def test_vector() -> None: pytest.importorskip("numpy") asr, aaz = vincenty.vdist(10, 20, [10.02137267, 10.01917819], [20.0168471, 20.0193493]) @@ -39,7 +39,7 @@ def test_vector(): @pytest.mark.parametrize("lat,lon,slantrange,az", [(10, 20, 3e3, 38), (0, 0, 0, 0)]) -def test_identity(lat, lon, slantrange, az): +def test_identity(lat: float, lon: float, slantrange: float, az: float) -> None: lat1, lon1 = vincenty.vreckon(lat, lon, slantrange, az) dist, az1 = vincenty.vdist(lat, lon, lat1, lon1) diff --git a/src/pymap3d/tests/test_vincenty_vreckon.py b/src/pymap3d/tests/test_vincenty_vreckon.py index ef0ee718..78d72fe9 100644 --- a/src/pymap3d/tests/test_vincenty_vreckon.py +++ b/src/pymap3d/tests/test_vincenty_vreckon.py @@ -26,7 +26,7 @@ (0, 0, 2.00375e7, -90, 0, 180), ], ) -def test_unit(lat, lon, srange, az, lato, lono): +def test_unit(lat: float, lon: float, srange: float, az: float, lato: float, lono: float) -> None: lat1, lon1 = vincenty.vreckon(lat, lon, srange, az) assert lat1 == approx(lato) @@ -36,15 +36,15 @@ def test_unit(lat, lon, srange, az, lato, lono): assert isinstance(lon1, float) -def test_az_vector(): +def test_az_vector() -> None: pytest.importorskip("numpy") - a, b = vincenty.vreckon(*ll0, sr1[0], az1) + a, b = vincenty.vreckon(*ll0, sr1[0], az1) # type: ignore[call-overload] assert a == approx(lat2) assert b == approx(lon2) -def test_both_vector(): +def test_both_vector() -> None: pytest.importorskip("numpy") - a, b = vincenty.vreckon(10, 20, sr1, az1) + a, b = vincenty.vreckon(10, 20, sr1, az1) # type: ignore[call-overload] assert a == approx(lat3) assert b == approx(lon3) diff --git a/src/pymap3d/timeconv.py b/src/pymap3d/timeconv.py index e2566d42..25b43242 100644 --- a/src/pymap3d/timeconv.py +++ b/src/pymap3d/timeconv.py @@ -4,6 +4,13 @@ from __future__ import annotations from datetime import datetime +from typing import Any, List, cast, overload + +try: + from numpy import datetime64 + from numpy.typing import NDArray +except ImportError: + pass try: import dateutil.parser @@ -13,7 +20,24 @@ __all__ = ["str2dt"] -def str2dt(time: str | datetime) -> datetime: +@overload +def str2dt(time: str | datetime | datetime64) -> datetime: + pass + + +@overload +def str2dt(time: list[str] | list[datetime]) -> list[datetime]: + pass + + +@overload +def str2dt(time: NDArray[datetime64]) -> NDArray[Any]: + pass + + +def str2dt( + time: str | datetime | datetime64 | list[str] | list[datetime] | NDArray[datetime64], +) -> datetime | list[datetime] | NDArray[Any]: """ Converts times in string or list of strings to datetime(s) @@ -37,19 +61,18 @@ def str2dt(time: str | datetime) -> datetime: raise ImportError("pip install dateutil") # some sort of iterable - try: - if isinstance(time[0], datetime): - return time - elif isinstance(time[0], str): - return [dateutil.parser.parse(t) for t in time] - except IndexError: - pass - except NameError: - raise ImportError("pip install dateutil") + if isinstance(time, list): + try: + if isinstance(time[0], datetime): + return cast(List[datetime], time) + elif isinstance(time[0], str): + return [dateutil.parser.parse(cast(str, t)) for t in time] + except NameError: + raise ImportError("pip install dateutil") # pandas/xarray try: - return time.values.astype("datetime64[us]").astype(datetime) + return time.values.astype("datetime64[us]").astype(datetime) # type: ignore[no-any-return, union-attr] except AttributeError: pass @@ -59,4 +82,4 @@ def str2dt(time: str | datetime) -> datetime: except AttributeError: pass - return time + return time # type: ignore[return-value] diff --git a/src/pymap3d/utils.py b/src/pymap3d/utils.py index 66d4dd58..78b5f0d2 100644 --- a/src/pymap3d/utils.py +++ b/src/pymap3d/utils.py @@ -5,30 +5,70 @@ from __future__ import annotations from math import pi +from typing import Any, Sequence, overload try: from numpy import asarray - from numpy.typing import ArrayLike + from numpy.typing import NDArray except ImportError: pass +from ._types import ArrayLike from .ellipsoid import Ellipsoid from .mathfun import atan2, cos, hypot, radians, sin __all__ = ["cart2pol", "pol2cart", "cart2sph", "sph2cart"] -def cart2pol(x, y) -> tuple: +@overload +def cart2pol(x: float, y: float) -> tuple[float, float]: + pass + + +@overload +def cart2pol(x: ArrayLike, y: ArrayLike) -> tuple[NDArray[Any], NDArray[Any]]: + pass + + +def cart2pol( + x: float | ArrayLike, y: float | ArrayLike +) -> tuple[float, float] | tuple[NDArray[Any], NDArray[Any]]: """Transform Cartesian to polar coordinates""" return atan2(y, x), hypot(x, y) -def pol2cart(theta, rho) -> tuple: +@overload +def pol2cart(theta: float, rho: float) -> tuple[float, float]: + pass + + +@overload +def pol2cart(theta: ArrayLike, rho: ArrayLike) -> tuple[NDArray[Any], NDArray[Any]]: + pass + + +def pol2cart( + theta: float | ArrayLike, rho: float | ArrayLike +) -> tuple[float, float] | tuple[NDArray[Any], NDArray[Any]]: """Transform polar to Cartesian coordinates""" return rho * cos(theta), rho * sin(theta) -def cart2sph(x, y, z) -> tuple: +@overload +def cart2sph(x: float, y: float, z: float) -> tuple[float, float, float]: + pass + + +@overload +def cart2sph( + x: ArrayLike, y: ArrayLike, z: ArrayLike +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def cart2sph( + x: float | ArrayLike, y: float | ArrayLike, z: float | ArrayLike +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """Transform Cartesian to spherical coordinates""" hxy = hypot(x, y) r = hypot(hxy, z) @@ -37,7 +77,21 @@ def cart2sph(x, y, z) -> tuple: return az, el, r -def sph2cart(az, el, r) -> tuple: +@overload +def sph2cart(az: float, el: float, r: float) -> tuple[float, float, float]: + pass + + +@overload +def sph2cart( + az: ArrayLike, el: ArrayLike, r: ArrayLike +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def sph2cart( + az: float | ArrayLike, el: float | ArrayLike, r: float | ArrayLike +) -> tuple[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """Transform spherical to Cartesian coordinates""" rcos_theta = r * cos(el) x = rcos_theta * cos(az) @@ -46,7 +100,19 @@ def sph2cart(az, el, r) -> tuple: return x, y, z -def sanitize(lat, ell: Ellipsoid | None, deg: bool) -> tuple[float | ArrayLike, Ellipsoid]: +@overload +def sanitize(lat: float, ell: Ellipsoid | None, deg: bool) -> tuple[float, Ellipsoid]: + pass + + +@overload +def sanitize(lat: ArrayLike, ell: Ellipsoid | None, deg: bool) -> tuple[NDArray[Any], Ellipsoid]: + pass + + +def sanitize( + lat: float | ArrayLike, ell: Ellipsoid | None, deg: bool +) -> tuple[float | NDArray[Any], Ellipsoid]: if ell is None: ell = Ellipsoid.from_name("wgs84") @@ -55,15 +121,16 @@ def sanitize(lat, ell: Ellipsoid | None, deg: bool) -> tuple[float | ArrayLike, lat = asarray(lat) except NameError: pass + assert not isinstance(lat, Sequence) if deg: lat = radians(lat) try: - if (abs(lat) > pi / 2).any(): # type: ignore + if (abs(lat) > pi / 2).any(): # type: ignore[attr-defined, operator] raise ValueError("-pi/2 <= latitude <= pi/2") except AttributeError: - if abs(lat) > pi / 2: # type: ignore + if abs(lat) > pi / 2: # type: ignore[operator] raise ValueError("-pi/2 <= latitude <= pi/2") return lat, ell diff --git a/src/pymap3d/vallado.py b/src/pymap3d/vallado.py index 4286b373..2efbc488 100644 --- a/src/pymap3d/vallado.py +++ b/src/pymap3d/vallado.py @@ -9,13 +9,21 @@ from __future__ import annotations from datetime import datetime +from typing import Any, overload +try: + from numpy.typing import NDArray +except ImportError: + pass + +from ._types import ArrayLike from .mathfun import asin, atan2, cos, degrees, radians, sin from .sidereal import datetime2sidereal __all__ = ["azel2radec", "radec2azel"] +@overload def azel2radec( az_deg: float, el_deg: float, @@ -23,6 +31,27 @@ def azel2radec( lon_deg: float, time: datetime, ) -> tuple[float, float]: + pass + + +@overload +def azel2radec( + az_deg: ArrayLike, + el_deg: ArrayLike, + lat_deg: float, + lon_deg: float, + time: datetime, +) -> tuple[NDArray[Any], NDArray[Any]]: + pass + + +def azel2radec( + az_deg: float | ArrayLike, + el_deg: float | ArrayLike, + lat_deg: float, + lon_deg: float, + time: datetime, +) -> tuple[float, float] | tuple[NDArray[Any], NDArray[Any]]: """ converts azimuth, elevation to right ascension, declination @@ -73,6 +102,7 @@ def azel2radec( return degrees(lst - lha) % 360, degrees(dec) +@overload def radec2azel( ra_deg: float, dec_deg: float, @@ -80,6 +110,27 @@ def radec2azel( lon_deg: float, time: datetime, ) -> tuple[float, float]: + pass + + +@overload +def radec2azel( + ra_deg: ArrayLike, + dec_deg: ArrayLike, + lat_deg: float, + lon_deg: float, + time: datetime, +) -> tuple[NDArray[Any], NDArray[Any]]: + pass + + +def radec2azel( + ra_deg: float | ArrayLike, + dec_deg: float | ArrayLike, + lat_deg: float, + lon_deg: float, + time: datetime, +) -> tuple[float, float] | tuple[NDArray[Any], NDArray[Any]]: """ converts right ascension, declination to azimuth, elevation diff --git a/src/pymap3d/vincenty.py b/src/pymap3d/vincenty.py index 7f017fc1..e9222d5c 100644 --- a/src/pymap3d/vincenty.py +++ b/src/pymap3d/vincenty.py @@ -7,12 +7,15 @@ import logging from copy import copy from math import nan, pi +from typing import Any, Sequence, overload try: from numpy import atleast_1d + from numpy.typing import NDArray except ImportError: pass +from ._types import ArrayLike from .ellipsoid import Ellipsoid from .mathfun import ( asin, @@ -31,13 +34,34 @@ __all__ = ["vdist", "vreckon", "track2"] +@overload def vdist( - Lat1, - Lon1, - Lat2, - Lon2, + Lat1: float, Lon1: float, Lat2: float, Lon2: float, ell: Ellipsoid | None = None +) -> tuple[float, float] | tuple[NDArray[Any], NDArray[Any]]: + pass + + +@overload +def vdist( + Lat1: float, Lon1: float, Lat2: ArrayLike, Lon2: ArrayLike, ell: Ellipsoid | None = None +) -> tuple[float, NDArray[Any]]: + pass + + +@overload +def vdist( + Lat1: ArrayLike, Lon1: ArrayLike, Lat2: ArrayLike, Lon2: ArrayLike, ell: Ellipsoid | None = None +) -> tuple[NDArray[Any], NDArray[Any]]: + pass + + +def vdist( + Lat1: float | ArrayLike, + Lon1: float | ArrayLike, + Lat2: float | ArrayLike, + Lon2: float | ArrayLike, ell: Ellipsoid | None = None, -) -> tuple: +) -> tuple[float | NDArray[Any], float | NDArray[Any]]: """ Using the reference ellipsoid, compute the distance between two points within a few millimeters of accuracy, compute forward azimuth, @@ -120,7 +144,7 @@ def vdist( if (abs(Lat1) > 90).any() | (abs(Lat2) > 90).any(): raise ValueError("Input latitudes must be in [-90, 90] degrees.") except NameError: - if (abs(Lat1) > 90) | (abs(Lat2) > 90): # type: ignore + if (abs(Lat1) > 90) | (abs(Lat2) > 90): # type: ignore[operator, arg-type] raise ValueError("Input latitudes must be in [-90, 90] degrees.") # %% Supply WGS84 earth ellipsoid axis lengths in meters: a = ell.semimajor_axis @@ -149,15 +173,15 @@ def vdist( U2 = atan((1 - f) * tan(lat2)) lon1 = lon1 % (2 * pi) lon2 = lon2 % (2 * pi) - L = abs(lon2 - lon1) + L: NDArray[Any] = abs(lon2 - lon1) try: L[L > pi] = 2 * pi - L[L > pi] except TypeError: if L > pi: - L = 2 * pi - L # type: ignore + L = 2 * pi - L - lamb = copy(L) # NOTE: program will fail without copy! + lamb: NDArray[Any] | float = copy(L) # NOTE: program will fail without copy! itercount = 0 warninggiven = False notdone = True @@ -167,7 +191,7 @@ def vdist( if not warninggiven: logging.warning("Essentially antipodal points--precision may be reduced slightly.") - lamb = pi # type: ignore + lamb = pi break lambdaold = copy(lamb) @@ -213,20 +237,20 @@ def vdist( # print(f'then, lambda(21752) = {lamb[21752],20}) # correct for convergence failure for essentially antipodal points try: - i = (lamb > pi).any() # type: ignore + i = (lamb > pi).any() # type: ignore[union-attr, assignment] except AttributeError: - i = lamb > pi + i = lamb > pi # type: ignore[assignment] if i: logging.warning( "Essentially antipodal points encountered. Precision may be reduced slightly." ) warninggiven = True - lambdaold = pi # type: ignore - lamb = pi # type: ignore + lambdaold = pi + lamb = pi try: - notdone = (abs(lamb - lambdaold) > 1e-12).any() # type: ignore + notdone = (abs(lamb - lambdaold) > 1e-12).any() except AttributeError: notdone = abs(lamb - lambdaold) > 1e-12 @@ -251,11 +275,11 @@ def vdist( # %% From point #1 to point #2 # correct sign of lambda for azimuth calcs: - lamb = abs(lamb) + lamb = abs(lamb) # type: ignore[assignment] try: i = sign(sin(lon2 - lon1)) * sign(sin(lamb)) < 0 - lamb[i] = -lamb[i] + lamb[i] = -lamb[i] # type: ignore[index] except TypeError: if sign(sin(lon2 - lon1)) * sign(sin(lamb)) < 0: lamb = -lamb @@ -273,13 +297,35 @@ def vdist( return dist_m, az +@overload +def vreckon( + Lat1: float, + Lon1: float, + Rng: float, + Azim: float, + ell: Ellipsoid | None = None, +) -> tuple[float, float]: + pass + + +@overload +def vreckon( + Lat1: ArrayLike, + Lon1: ArrayLike, + Rng: ArrayLike, + Azim: ArrayLike, + ell: Ellipsoid | None = None, +) -> tuple[NDArray[Any] | NDArray[Any]]: + pass + + def vreckon( - Lat1, - Lon1, - Rng, - Azim, + Lat1: float | ArrayLike, + Lon1: float | ArrayLike, + Rng: float | ArrayLike, + Azim: float | ArrayLike, ell: Ellipsoid | None = None, -) -> tuple: +) -> tuple[float, float] | tuple[NDArray[Any] | NDArray[Any]]: """ This is the Vincenty "forward" solution. @@ -347,9 +393,9 @@ def vreckon( if (Rng < 0.0).any(): raise ValueError("Ground distance must be positive") except NameError: - if abs(Lat1) > 90.0: # type: ignore + if abs(Lat1) > 90.0: # type: ignore[operator, arg-type] raise ValueError("Input lat. must be between -90 and 90 deg., inclusive.") - if Rng < 0.0: + if Rng < 0.0: # type: ignore[operator] raise ValueError("Ground distance must be positive") if ell is not None: @@ -457,15 +503,41 @@ def vreckon( return degrees(lat2), lon2 +@overload +def track2( + lat1: float, + lon1: float, + lat2: float, + lon2: float, + ell: Ellipsoid | None = None, + npts: int = 100, + deg: bool = True, +) -> tuple[list[float], list[float]]: + pass + + +@overload +def track2( + lat1: ArrayLike, + lon1: ArrayLike, + lat2: ArrayLike, + lon2: ArrayLike, + ell: Ellipsoid | None = None, + npts: int = 100, + deg: bool = True, +) -> tuple[list[NDArray[Any]], list[NDArray[Any]]]: + pass + + def track2( - lat1, - lon1, - lat2, - lon2, + lat1: float | ArrayLike, + lon1: float | ArrayLike, + lat2: float | ArrayLike, + lon2: float | ArrayLike, ell: Ellipsoid | None = None, npts: int = 100, deg: bool = True, -) -> tuple[list, list]: +) -> tuple[list[float], list[float]] | tuple[list[NDArray[Any]], list[NDArray[Any]]]: """ computes great circle tracks starting at the point lat1, lon1 and ending at lat2, lon2 @@ -504,14 +576,28 @@ def track2( if npts < 2: raise ValueError("npts must be greater than 1") + try: + lat1 = atleast_1d(lat1) + lon1 = atleast_1d(lon1) + lat2 = atleast_1d(lat2) + lon2 = atleast_1d(lon2) + except NameError: + pass + assert ( + not isinstance(lat1, Sequence) + and not isinstance(lon1, Sequence) + and not isinstance(lat2, Sequence) + and not isinstance(lon2, Sequence) + ) + if npts == 2: - return [lat1, lat2], [lon1, lon2] + return [lat1, lat2], [lon1, lon2] # type: ignore[return-value] if deg: - rlat1 = radians(lat1) - rlon1 = radians(lon1) - rlat2 = radians(lat2) - rlon2 = radians(lon2) + rlat1: float | NDArray[Any] = radians(lat1) + rlon1: float | NDArray[Any] = radians(lon1) + rlat2: float | NDArray[Any] = radians(lat2) + rlon2: float | NDArray[Any] = radians(lon2) else: rlat1, rlon1, rlat2, rlon2 = lat1, lon1, lat2, lon2 @@ -527,7 +613,7 @@ def track2( "cannot compute intermediate points on a great circle whose endpoints are antipodal" ) - distance, azimuth = vdist(lat1, lon1, lat2, lon2) + distance, azimuth = vdist(lat1, lon1, lat2, lon2) # type: ignore[arg-type] incdist = distance / (npts - 1) latpt = lat1 @@ -535,8 +621,8 @@ def track2( lons = [lonpt] lats = [latpt] for _ in range(npts - 2): - latptnew, lonptnew = vreckon(latpt, lonpt, incdist, azimuth) - azimuth = vdist(latptnew, lonptnew, lat2, lon2, ell=ell)[1] + latptnew, lonptnew = vreckon(latpt, lonpt, incdist, azimuth) # type: ignore[arg-type] + azimuth = vdist(latptnew, lonptnew, lat2, lon2, ell=ell)[1] # type: ignore[arg-type] lats.append(latptnew) lons.append(lonptnew) latpt = latptnew @@ -548,4 +634,4 @@ def track2( lats = list(map(radians, lats)) lons = list(map(radians, lons)) - return lats, lons + return lats, lons # type: ignore[return-value] From 7649c3a4f57e496d4b4967a3d099ee257257a343 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sat, 29 Oct 2022 01:15:37 +0200 Subject: [PATCH 06/26] Fix failing tests without numpy --- src/pymap3d/latitude.py | 2 +- src/pymap3d/tests/test_latitude.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pymap3d/latitude.py b/src/pymap3d/latitude.py index 3b1de190..e76152b0 100644 --- a/src/pymap3d/latitude.py +++ b/src/pymap3d/latitude.py @@ -6,8 +6,8 @@ from typing import Any, Sequence, overload try: + from numpy import asarray from numpy.typing import NDArray - from nupy import asarray except ImportError: pass diff --git a/src/pymap3d/tests/test_latitude.py b/src/pymap3d/tests/test_latitude.py index 8495e861..294d33d9 100644 --- a/src/pymap3d/tests/test_latitude.py +++ b/src/pymap3d/tests/test_latitude.py @@ -19,13 +19,23 @@ def test_geodetic_alt_geocentric(geodetic_lat: float, alt_m: float, geocentric_l latitude.geocentric2geodetic(geocentric_lat, 1e5 + alt_m) ) - assert latitude.geoc2geod([geocentric_lat], [r]) == approx(geodetic_lat) - assert latitude.geod2geoc(geodetic_lat, 1e5 + alt_m) == approx( latitude.geodetic2geocentric(geodetic_lat, 1e5 + alt_m) ) +@pytest.mark.parametrize( + "geodetic_lat,alt_m,geocentric_lat", + [(0, 0, 0), (90, 0, 90), (-90, 0, -90), (45, 0, 44.80757678), (-45, 0, -44.80757678)], +) +def test_geodetic_alt_geocentric_list( + geodetic_lat: float, alt_m: float, geocentric_lat: float +) -> None: + pytest.importorskip("numpy") + r = rcurve.geocentric_radius(geodetic_lat) + assert latitude.geoc2geod([geocentric_lat], [r]) == approx(geodetic_lat) + + @pytest.mark.parametrize( "geodetic_lat,geocentric_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.80757678), (-45, -44.80757678)], From a991ec41324622ba8041c1e23d34f2b2260dd31e Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sat, 29 Oct 2022 12:53:29 +0200 Subject: [PATCH 07/26] Make pymap3d PEP 561 compliant --- setup.cfg | 4 ++++ src/pymap3d/py.typed | 0 2 files changed, 4 insertions(+) create mode 100644 src/pymap3d/py.typed diff --git a/setup.cfg b/setup.cfg index 0d173b88..8b4e6292 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,12 +26,16 @@ project_urls = python_requires = >= 3.7 packages = find: zip_safe = False +include_package_data = true package_dir= =src [options.packages.find] where=src +[options.package_data] +pymap3d = py.typed + [options.extras_require] tests = pytest diff --git a/src/pymap3d/py.typed b/src/pymap3d/py.typed new file mode 100644 index 00000000..e69de29b From 1ca969bf9dd9dbbbf1dedd62a34585289ef3e049 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sat, 29 Oct 2022 14:49:17 +0200 Subject: [PATCH 08/26] Fix CI workflows for pull requests --- .github/workflows/ci.yml | 6 +++++- .github/workflows/ci_stdlib_only.yml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fad43d77..43d33294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,11 @@ on: - "**.py" - .github/workflows/ci.yml - "!scripts/" - pull-request: + pull_request: + types: + - opened + - synchronize + paths: - "**.py" - .github/workflows/ci.yml diff --git a/.github/workflows/ci_stdlib_only.yml b/.github/workflows/ci_stdlib_only.yml index 0cb4c4b0..10c52ba7 100644 --- a/.github/workflows/ci_stdlib_only.yml +++ b/.github/workflows/ci_stdlib_only.yml @@ -6,7 +6,11 @@ on: - "**.py" - .github/workflows/ci_stdlib_only.yml - "!scripts/" - pull-request: + pull_request: + types: + - opened + - synchronize + paths: - "**.py" - .github/workflows/ci_stdlib_only.yml From f96676a5b751d47bf17f25c8e677605e948f2d07 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sat, 29 Oct 2022 16:32:58 +0200 Subject: [PATCH 09/26] Improve coverage of timeconv --- src/pymap3d/tests/test_time.py | 28 ++++++++++++++++++++++------ src/pymap3d/timeconv.py | 4 ++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/pymap3d/tests/test_time.py b/src/pymap3d/tests/test_time.py index 43f0d2e6..5e3bae93 100755 --- a/src/pymap3d/tests/test_time.py +++ b/src/pymap3d/tests/test_time.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +from importlib.util import find_spec from typing import cast try: @@ -15,6 +16,9 @@ from pytest import approx t0 = datetime(2014, 4, 6, 8) +t1 = datetime(2014, 4, 6, 8, 1, 2) +t0_str = "2014-04-06T08:00:00" +t1_str = "2014-04-06T08:01:02" def test_juliantime() -> None: @@ -24,13 +28,25 @@ def test_juliantime() -> None: def test_types() -> None: np = pytest.importorskip("numpy") assert str2dt(t0) == t0 # passthrough - assert str2dt("2014-04-06T08:00:00") == t0 - ti = [str2dt("2014-04-06T08:00:00"), str2dt("2014-04-06T08:01:02")] - to = [t0, datetime(2014, 4, 6, 8, 1, 2)] - assert ti == to # even though ti is numpy array of datetime and to is list of datetime - t1 = [t0, t0] - assert (np.asarray(str2dt(t1)) == t0).all() + if find_spec("dateutil"): + assert str2dt(t0_str) == t0 + else: + with pytest.raises(ImportError) as excinfo: + str2dt(t0_str) + assert str(excinfo.value) == "pip install python-dateutil" + + if find_spec("dateutil"): + ti = str2dt([t0_str, t1_str]) + to = [t0, t1] + assert ti == to + else: + with pytest.raises(ImportError) as excinfo: + str2dt([t0_str, t1_str]) + assert str(excinfo.value) == "pip install python-dateutil" + + t2 = [t0, t0] + assert (np.asarray(str2dt(t2)) == t0).all() def test_datetime64() -> None: diff --git a/src/pymap3d/timeconv.py b/src/pymap3d/timeconv.py index 25b43242..d38ff90f 100644 --- a/src/pymap3d/timeconv.py +++ b/src/pymap3d/timeconv.py @@ -58,7 +58,7 @@ def str2dt( try: return dateutil.parser.parse(time) except NameError: - raise ImportError("pip install dateutil") + raise ImportError("pip install python-dateutil") # some sort of iterable if isinstance(time, list): @@ -68,7 +68,7 @@ def str2dt( elif isinstance(time[0], str): return [dateutil.parser.parse(cast(str, t)) for t in time] except NameError: - raise ImportError("pip install dateutil") + raise ImportError("pip install python-dateutil") # pandas/xarray try: From 24dd16c7bbd9f9f4623d95ee9553ef5b69090f8a Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sat, 29 Oct 2022 17:03:39 +0200 Subject: [PATCH 10/26] Add coverage to CI --- .coveragerc | 7 +++---- .github/workflows/ci.yml | 9 ++++----- setup.cfg | 1 + 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.coveragerc b/.coveragerc index 5fbefdc0..51018c50 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,9 +3,7 @@ branch = True source = src/ -omit = - archive/* - */.local/* +# Do not omit tests folder as the tests coverage could not be 100% [report] # Regexes for lines to exclude from consideration @@ -25,7 +23,8 @@ exclude_lines = if 0: if __name__ == .__main__.: -ignore_errors = True + # Don't complain about function overloading + @overload [html] directory = coverage_html_report diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43d33294..4d522b18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,14 +31,13 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - name: Set up Python + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - run: pip install .[full,tests,lint] - - - run: flake8 - - run: mypy + - name: Install tests dependencies + run: pip install .[full,tests] - run: pytest diff --git a/setup.cfg b/setup.cfg index 8b4e6292..d0136834 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,5 +58,6 @@ core = full = astropy xarray + pandas proj = pyproj From 1f4c108f5fede12f5fca0d20f8f9affc5e9b457a Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sat, 29 Oct 2022 17:48:46 +0200 Subject: [PATCH 11/26] Add coverage, mypy and black badges --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9c74c02e..2ca181f1 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ ![Actions Status](https://github.com/geospace-code/pymap3d/workflows/ci_stdlib_only/badge.svg) [![image](https://img.shields.io/pypi/pyversions/pymap3d.svg)](https://pypi.python.org/pypi/pymap3d) [![PyPi Download stats](http://pepy.tech/badge/pymap3d)](http://pepy.tech/project/pymap3d) +![mypy](https://img.shields.io/badge/mypy-checked-blue) +![black](https://img.shields.io/badge/code%20style-black-black) Pure Python (no prerequistes beyond Python itself) 3-D geographic coordinate conversions and geodesy. API similar to popular $1000 Matlab Mapping Toolbox routines for Python From a58baf7fcd5fd51e3a7fda552b8d1db8457808e5 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sat, 29 Oct 2022 19:15:23 +0200 Subject: [PATCH 12/26] Add CONTRIBUTING.md --- CONTRIBUTING.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..96827bb1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# Contributing + +## Setting up an environment + +Clone the `PyMap3D`. + +Inside the repository, create a virtual environment. + +```bash +python3 -m venv env +``` + +Activate the virtual environment. + +```bash +source ./env/bin/activate +``` + +Upgrade `pip`. + +```bash +pip install --upgrade pip +``` + +Install the development dependencies. + +```bash +pip install -e .[full,tests,lint,format] +``` + +Install [pre-commit](https://pre-commit.com/) so that your code is formatted and checked when you are doing a commit. + +```bash +pip install pre-commit +pre-commit install +``` + +### Visual Studio Code + +If you are using VSCode, here are the settings to activate on save, + +- `black` and `isort` to format. +- `flake8` and `mypy` to lint. + +```json +{ + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, + "python.formatting.provider": "black", + "python.linting.flake8Enabled": true, + "python.linting.mypyEnabled": true +} +``` + +## Testing + +To test the code use [pytest](https://docs.pytest.org/en/7.1.x/). + +```bash +pytest +``` + +To do the full coverage of `PyMap3d`, you must combine the coverage of the tests with, + +- Vanilla Python +- NumPy installed +- Full dependencies installed + +Here is how to do it with a newly created environment, + +```bash +pip install -e .[tests] +pytest --cov=src --cov-report=html +pip install numpy +pytest --cov=src --cov-report=html --cov-append +pip install -e .[full] +pytest --cov=src --cov-report=html --cov-append +``` + +## Committing + +After doing `git commit`, `pre-commit` will check the committed code. +The check can be passed, skipped or failed. +If the check failed, it is possible it auto-fixed the code, so you will only need to stage and commit again for it to pass. +If it did not auto-fixed the code, you will need to do it manually. +`pre-commit` will only check the code that is staged, the unstaged code will be stashed during the checks. From 735425e4d3e635b3d3f4194215d629df6a8c7c68 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sun, 30 Oct 2022 08:23:12 +0100 Subject: [PATCH 13/26] Add name to codecov CI --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d522b18..1df05f36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Set up Python uses: actions/setup-python@v4 with: @@ -57,7 +58,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install tests and lint dependencies + - name: Install tests and coverage dependencies run: pip install -e .[tests,coverage] - name: Collect coverage without NumPy From 25db2d676cbb1536b39d8f796a57d0c2c5b278bf Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sun, 30 Oct 2022 21:19:22 +0100 Subject: [PATCH 14/26] Autoupdate pre-commit --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 52cedb48..d4e6bcb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.0.0 + rev: v3.2.0 hooks: - id: pyupgrade args: ["--py37-plus"] @@ -11,7 +11,7 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black @@ -25,7 +25,7 @@ repos: - flake8-builtins - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 + rev: v0.982 hooks: - id: mypy additional_dependencies: @@ -36,12 +36,12 @@ repos: - xarray - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.3.2 + rev: v3.0.0-alpha.4 hooks: - id: prettier - repo: https://github.com/markdownlint/markdownlint - rev: v0.11.0 + rev: v0.12.0 hooks: - id: markdownlint args: ["--rules", "~MD013,~MD032"] From fcee86d56db34c6e4aaed2130727bf598aa63bed Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Wed, 2 Nov 2022 14:00:36 +0100 Subject: [PATCH 15/26] Move .coveragerc into pyproject.toml --- .coveragerc | 30 ------------------------------ pyproject.toml | 13 +++++++++++++ 2 files changed, 13 insertions(+), 30 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 51018c50..00000000 --- a/.coveragerc +++ /dev/null @@ -1,30 +0,0 @@ -# .coveragerc to control coverage.py -[run] -branch = True -source = src/ - -# Do not omit tests folder as the tests coverage could not be 100% - -[report] -# Regexes for lines to exclude from consideration -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - - # Don't complain about missing debug-only code: - def __repr__ - if self\.debug - - # Don't complain if tests don't hit defensive assertion code: - raise AssertionError - raise NotImplementedError - - # Don't complain if non-runnable code isn't run: - if 0: - if __name__ == .__main__.: - - # Don't complain about function overloading - @overload - -[html] -directory = coverage_html_report diff --git a/pyproject.toml b/pyproject.toml index f8d1f9ee..5dc5b94c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,3 +37,16 @@ exclude_lines = [ # Don't complain about function overloading "@overload", ] + +[tool.coverage.run] +branch = true +source = "src/" + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't complain about function overloading + "@overload", +] From 3be00ef77a73a37a12b1e5c0d1007b6df975de3f Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Wed, 2 Nov 2022 14:01:35 +0100 Subject: [PATCH 16/26] Remove CI types filter on pull requests --- .github/workflows/ci.yml | 3 --- .github/workflows/ci_stdlib_only.yml | 3 --- 2 files changed, 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1df05f36..2772f157 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,6 @@ on: - .github/workflows/ci.yml - "!scripts/" pull_request: - types: - - opened - - synchronize paths: - "**.py" - .github/workflows/ci.yml diff --git a/.github/workflows/ci_stdlib_only.yml b/.github/workflows/ci_stdlib_only.yml index 10c52ba7..8a979412 100644 --- a/.github/workflows/ci_stdlib_only.yml +++ b/.github/workflows/ci_stdlib_only.yml @@ -7,9 +7,6 @@ on: - .github/workflows/ci_stdlib_only.yml - "!scripts/" pull_request: - types: - - opened - - synchronize paths: - "**.py" - .github/workflows/ci_stdlib_only.yml From 90c1371fe6b4be36516aabd699685bbbd35285de Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Tue, 15 Nov 2022 08:21:52 +0100 Subject: [PATCH 17/26] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4e6bcb4..dd636fab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.2.0 + rev: v3.2.2 hooks: - id: pyupgrade args: ["--py37-plus"] @@ -25,7 +25,7 @@ repos: - flake8-builtins - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 + rev: v0.990 hooks: - id: mypy additional_dependencies: From d0f668ca5416a8dfd178e81c346c3c1398b8fa9a Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Tue, 15 Nov 2022 08:24:24 +0100 Subject: [PATCH 18/26] Fix mathfun.py mypy errors --- src/pymap3d/mathfun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymap3d/mathfun.py b/src/pymap3d/mathfun.py index 52c8c248..a176309e 100644 --- a/src/pymap3d/mathfun.py +++ b/src/pymap3d/mathfun.py @@ -26,7 +26,7 @@ tan, ) except ImportError: - from math import ( # type: ignore[misc] + from math import ( # type: ignore[assignment] asin, asinh, atan, From 82403c64746e5b24f32854872df8d7528723fb01 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Tue, 15 Nov 2022 10:17:59 +0100 Subject: [PATCH 19/26] Fix pyproject.toml --- pyproject.toml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5dc5b94c..f8d1f9ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,16 +37,3 @@ exclude_lines = [ # Don't complain about function overloading "@overload", ] - -[tool.coverage.run] -branch = true -source = "src/" - -[tool.coverage.report] -# Regexes for lines to exclude from consideration -exclude_lines = [ - # Have to re-enable the standard pragma - "pragma: no cover", - # Don't complain about function overloading - "@overload", -] From 6bf6d0909fe9cae0d85d13ce1e8af61461e2072c Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Tue, 15 Nov 2022 10:19:40 +0100 Subject: [PATCH 20/26] Apply pyupgrade --- Examples/compare/matlab_aerospace.py | 2 +- Examples/compare/matlab_mapping.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/compare/matlab_aerospace.py b/Examples/compare/matlab_aerospace.py index ee2e510a..7d7c413a 100644 --- a/Examples/compare/matlab_aerospace.py +++ b/Examples/compare/matlab_aerospace.py @@ -15,6 +15,6 @@ def matlab_aerospace(eng: matlab.engine.matlabengine.MatlabEngine) -> bool: if d.is_dir(): eng.addpath(str(d), nargout=0) else: - raise EnvironmentError(f"Matlab {eng.version()} does not have Aerospace Toolbox") + raise OSError(f"Matlab {eng.version()} does not have Aerospace Toolbox") return has_aero diff --git a/Examples/compare/matlab_mapping.py b/Examples/compare/matlab_mapping.py index f155f11d..d6802a97 100644 --- a/Examples/compare/matlab_mapping.py +++ b/Examples/compare/matlab_mapping.py @@ -15,6 +15,6 @@ def matlab_mapping(eng: matlab.engine.matlabengine.MatlabEngine) -> bool: if d.is_dir(): eng.addpath(str(d), nargout=0) else: - raise EnvironmentError(f"Matlab {eng.version()} does not have Mapping Toolbox") + raise OSError(f"Matlab {eng.version()} does not have Mapping Toolbox") return has From 751be8b82ab945ed40f4e9bc59076494a55bb735 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Tue, 15 Nov 2022 10:20:06 +0100 Subject: [PATCH 21/26] Fix mypy errors --- Examples/compare/compare_ecef2eci.py | 14 +++++++------- Examples/compare/compare_lox.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Examples/compare/compare_ecef2eci.py b/Examples/compare/compare_ecef2eci.py index 81e3f473..ff67b494 100644 --- a/Examples/compare/compare_ecef2eci.py +++ b/Examples/compare/compare_ecef2eci.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 from __future__ import annotations -from pathlib import Path from datetime import datetime -from pytest import approx +from pathlib import Path import matlab.engine -from pymap3d.eci import ecef2eci, eci2ecef - from matlab_aerospace import matlab_aerospace +from pymap3d.eci import ecef2eci, eci2ecef +from pytest import approx cwd = Path(__file__).parent @@ -18,14 +17,15 @@ has_aero = matlab_aerospace(eng) -def test_ecef_eci(): +def test_ecef_eci() -> None: - ecef = [-5762640.0, -1682738.0, 3156028.0] + ecef = (-5762640.0, -1682738.0, 3156028.0) utc = datetime(2019, 1, 4, 12) eci = ecef2eci(*ecef, utc) utc_matlab = eng.datetime(utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second) + eci_matlab: tuple[float, float, float] if has_aero: eci_matlab = eng.ecef2eci(utc_matlab, *ecef, nargout=3) else: @@ -38,7 +38,7 @@ def test_ecef_eci(): ecef = eci2ecef(*eci_matlab, utc) if has_aero: - ecef_matlab = eng.eci2ecef(utc_matlab, *eci_matlab, nargout=3) # type: ignore + ecef_matlab = eng.eci2ecef(utc_matlab, *eci_matlab, nargout=3) else: ecef_matlab = eng.matmap3d.eci2ecef(utc_matlab, *eci_matlab, nargout=3) diff --git a/Examples/compare/compare_lox.py b/Examples/compare/compare_lox.py index 5c1b8684..39478ac1 100644 --- a/Examples/compare/compare_lox.py +++ b/Examples/compare/compare_lox.py @@ -19,9 +19,9 @@ def reckon(lat1: float, lon1: float, rng: float, az: float) -> tuple[float, float]: """Using Matlab Engine to do same thing as Pymap3d""" if has_map: - return eng.reckon("rh", lat1, lon1, rng, az, eng.wgs84Ellipsoid(), nargout=2) + return eng.reckon("rh", lat1, lon1, rng, az, eng.wgs84Ellipsoid(), nargout=2) # type: ignore[no-any-return] else: - return eng.matmap3d.vreckon(lat1, lon1, rng, az, nargout=2) + return eng.matmap3d.vreckon(lat1, lon1, rng, az, nargout=2) # type: ignore[no-any-return] clat, clon, rng = 35.0, 140.0, 50000.0 # arbitrary From 198d9d6b6e98b9d2b414c86ee0e492f811d7614f Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Tue, 15 Nov 2022 10:23:40 +0100 Subject: [PATCH 22/26] Fix pyproject.toml coverage configuration --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f8d1f9ee..70cedbf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ filterwarnings = [ [tool.coverage.run] branch = true -source = "src/" +source = ["src"] [tool.coverage.report] # Regexes for lines to exclude from consideration From 7c5c4fc8756f4a11be7a2f1a8b301b542f7d1201 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Tue, 15 Nov 2022 20:37:47 +0100 Subject: [PATCH 23/26] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd636fab..43bec6e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - flake8-builtins - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.990 + rev: v0.991 hooks: - id: mypy additional_dependencies: From a180efb96791a5696956163dfe32fb3ed2517223 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Wed, 16 Nov 2022 17:56:32 +0100 Subject: [PATCH 24/26] Replace Flake8 by Ruff --- .flake8 | 5 ----- .pre-commit-config.yaml | 16 +++++++--------- CONTRIBUTING.md | 4 ++-- pyproject.toml | 3 +++ setup.cfg | 5 +---- 5 files changed, 13 insertions(+), 20 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 3a566e3d..00000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -ignore = E501, W503 -exclude = .git,__pycache__,.eggs/,doc/,docs/,build/,dist/,archive/,env/ -per-file-ignores = - test_eci.py:F401 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43bec6e3..6a7b9b10 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,13 @@ repos: - id: pyupgrade args: ["--py37-plus"] + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.121 + hooks: + - id: ruff + args: + - --fix + - repo: https://github.com/pycqa/isort rev: 5.10.1 hooks: @@ -15,15 +22,6 @@ repos: hooks: - id: black - - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 - hooks: - - id: flake8 - additional_dependencies: - - flake8-blind-except - - flake8-bugbear - - flake8-builtins - - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.991 hooks: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96827bb1..894a2795 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,8 @@ pre-commit install If you are using VSCode, here are the settings to activate on save, - `black` and `isort` to format. -- `flake8` and `mypy` to lint. +- `mypy` to lint. +- Install [charliermarsh.ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) extension to lint (`ruff` is a fast equivalent to `flake8`) ```json { @@ -51,7 +52,6 @@ If you are using VSCode, here are the settings to activate on save, } }, "python.formatting.provider": "black", - "python.linting.flake8Enabled": true, "python.linting.mypyEnabled": true } ``` diff --git a/pyproject.toml b/pyproject.toml index 70cedbf6..22e59dfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,9 @@ line-length = 100 profile = "black" known_third_party = ["pymap3d"] +[tool.ruff] +ignore = ["E501"] + [tool.mypy] files = ["src", "Examples", "scripts"] diff --git a/setup.cfg b/setup.cfg index d0136834..944ce49b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,10 +42,7 @@ tests = coverage = pytest-cov lint = - flake8 - flake8-bugbear - flake8-builtins - flake8-blind-except + ruff mypy >= 0.800 types-python-dateutil types-requests From 6c33a236d1570f7b7a4e55925065b6e95c8ed83d Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Wed, 16 Nov 2022 19:09:57 +0100 Subject: [PATCH 25/26] Enable more linter in Ruff --- pyproject.toml | 14 +++++++++++++- src/pymap3d/tests/test_ned.py | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 22e59dfb..329b8d58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,19 @@ profile = "black" known_third_party = ["pymap3d"] [tool.ruff] -ignore = ["E501"] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "S", # flake8-bandit + "B", # flake8-bugbear + "A", # flake8-builtins + "C", # flake8-comprehensions + "Q", # flake8-quotes + "YTT", # flake8-2020 + "M", # Meta rules +] +ignore = ["E501", "S101"] [tool.mypy] files = ["src", "Examples", "scripts"] diff --git a/src/pymap3d/tests/test_ned.py b/src/pymap3d/tests/test_ned.py index b9784f43..58b8f7af 100755 --- a/src/pymap3d/tests/test_ned.py +++ b/src/pymap3d/tests/test_ned.py @@ -59,8 +59,8 @@ def test_ned_geodetic() -> None: def test_ned_geodetic_list() -> None: np = pytest.importorskip("numpy") - lla1 = tuple(map(lambda z: [z], lla0)) - aer1 = tuple(map(lambda z: [z], aer0)) + lla1 = tuple([z] for z in lla0) + aer1 = tuple([z] for z in aer0) lla2 = pm.aer2geodetic(*aer1, *lla1) # type: ignore[call-overload] From aaee876460ee3823753ff467f1119eaf1d7209db Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Wed, 16 Nov 2022 19:17:03 +0100 Subject: [PATCH 26/26] Make Ellipsoid class instead of dataclass --- src/pymap3d/ellipsoid.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/pymap3d/ellipsoid.py b/src/pymap3d/ellipsoid.py index f0c6f189..22bc579e 100644 --- a/src/pymap3d/ellipsoid.py +++ b/src/pymap3d/ellipsoid.py @@ -3,9 +3,7 @@ from __future__ import annotations import sys -from dataclasses import dataclass, field from math import sqrt -from typing import Dict # for Python < 3.9 if sys.version_info >= (3, 8): from typing import TypedDict @@ -21,7 +19,6 @@ class Model(TypedDict): b: float -@dataclass class Ellipsoid: """ generate reference ellipsoid parameters @@ -60,15 +57,6 @@ class Ellipsoid: feel free to suggest additional ellipsoids """ - model: str # short name - name: str # name for printing - semimajor_axis: float - semiminor_axis: float - flattening: float - thirdflattening: float - eccentricity: float - models = field(default_factory=Dict[str, Model]) - def __init__( self, semimajor_axis: float, semiminor_axis: float, name: str = "", model: str = "" ) -> None: @@ -97,7 +85,7 @@ def __init__( self.semimajor_axis = semimajor_axis self.semiminor_axis = semiminor_axis - models = { + models: dict[str, Model] = { # Earth ellipsoids "maupertuis": {"name": "Maupertuis (1738)", "a": 6397300.0, "b": 6363806.283}, "plessis": {"name": "Plessis (1817)", "a": 6376523.0, "b": 6355862.9333},