diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 5fbefdc0..00000000 --- a/.coveragerc +++ /dev/null @@ -1,31 +0,0 @@ -# .coveragerc to control coverage.py -[run] -branch = True -source = src/ - -omit = - archive/* - */.local/* - -[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__.: - -ignore_errors = True - -[html] -directory = coverage_html_report diff --git a/.flake8 b/.flake8 deleted file mode 100644 index d7fa0faf..00000000 --- a/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -ignore = E501, W503 -exclude = .git,__pycache__,.eggs/,doc/,docs/,build/,dist/,archive/,env/ 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..2772f157 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,41 +3,41 @@ name: ci on: push: paths: - - "**.py" - - .github/workflows/ci.yml - - "!scripts/" - pull-request: - - "**.py" - - .github/workflows/ci.yml + - "**.py" + - .github/workflows/ci.yml + - "!scripts/" + pull_request: + paths: + - "**.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 - - run: pip install .[full,tests,lint] + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} - - run: flake8 - - run: mypy + - name: Install tests dependencies + run: pip install .[full,tests] - - run: pytest + - run: pytest coverage: runs-on: ubuntu-latest @@ -55,7 +55,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 diff --git a/.github/workflows/ci_stdlib_only.yml b/.github/workflows/ci_stdlib_only.yml index fe75ff8f..8a979412 100644 --- a/.github/workflows/ci_stdlib_only.yml +++ b/.github/workflows/ci_stdlib_only.yml @@ -3,35 +3,35 @@ name: ci_stdlib_only on: push: paths: - - "**.py" - - .github/workflows/ci_stdlib_only.yml - - "!scripts/" - pull-request: - - "**.py" - - .github/workflows/ci_stdlib_only.yml + - "**.py" + - .github/workflows/ci_stdlib_only.yml + - "!scripts/" + pull_request: + paths: + - "**.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 new file mode 100644 index 00000000..6a7b9b10 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,62 @@ +repos: + - repo: https://github.com/asottile/pyupgrade + rev: v3.2.2 + hooks: + - 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: + - id: isort + + - repo: https://github.com/psf/black + rev: 22.10.0 + hooks: + - id: black + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.991 + hooks: + - id: mypy + additional_dependencies: + - numpy + - pytest + - types-python-dateutil + - types-requests + - xarray + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.0.0-alpha.4 + hooks: + - id: prettier + + - repo: https://github.com/markdownlint/markdownlint + rev: v0.12.0 + hooks: + - id: markdownlint + args: ["--rules", "~MD013,~MD032"] + + - 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 + + - 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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..894a2795 --- /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. +- `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 +{ + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, + "python.formatting.provider": "black", + "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. 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 9ca2c2a6..39478ac1 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) @@ -21,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 diff --git a/Examples/compare/compare_vdist.py b/Examples/compare/compare_vdist.py index 8c764e48..6c7fc489 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 @@ -19,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/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 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/README.md b/README.md index fe19cc74..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 @@ -19,9 +21,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 +114,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 +162,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 6df84b5c..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,46 +18,47 @@ 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. 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) -* 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 diff --git a/pyproject.toml b/pyproject.toml index cabd9091..329b8d58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,12 +9,28 @@ line-length = 100 profile = "black" known_third_party = ["pymap3d"] +[tool.ruff] +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"] +strict = true ignore_missing_imports = true -strict_optional = false show_column_numbers = true +show_error_codes = true [tool.pytest.ini_options] filterwarnings = [ @@ -26,7 +42,7 @@ filterwarnings = [ [tool.coverage.run] branch = true -source = "src/" +source = ["src"] [tool.coverage.report] # Regexes for lines to exclude from consideration 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/setup.cfg b/setup.cfg index 0d173b88..944ce49b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,22 +26,23 @@ 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 coverage = pytest-cov lint = - flake8 - flake8-bugbear - flake8-builtins - flake8-blind-except + ruff mypy >= 0.800 types-python-dateutil types-requests @@ -54,5 +55,6 @@ core = full = astropy xarray + pandas proj = pyproj 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 f67af76f..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: 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 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, - y, - z, - lat0, - lon0, - h0, - ell: Ellipsoid = None, + 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]]: """ 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, - ell: Ellipsoid = None, + 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, - ell: Ellipsoid = None, + 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, - el, - srange, - lat0, - lon0, - h0, + az: float, + el: float, + srange: float, + lat0: float, + lon0: float, + h0: float, t: datetime, - ell=None, + ell: Ellipsoid | None = None, *, deg: bool = True, -) -> tuple: +) -> 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: float | ArrayLike, + el: float | ArrayLike, + srange: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + h0: float | ArrayLike, + t: datetime, + ell: Ellipsoid | None = None, + *, + deg: bool = True, +) -> 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, - ell: Ellipsoid = None, + 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 ab968a6e..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, - lon, - alt, - ell: Ellipsoid = None, + lat: ArrayLike, + lon: ArrayLike, + alt: ArrayLike, + ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def geodetic2ecef( + lat: float | ArrayLike, + lon: float | ArrayLike, + alt: float | ArrayLike, + ell: Ellipsoid | None = None, + deg: bool = True, +) -> 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, - ell: Ellipsoid = None, + 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, - ell: Ellipsoid = None, + 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, - north, - up, - lat0, - lon0, + east: float, + north: float, + up: float, + lat0: float, + lon0: float, deg: bool = True, -) -> tuple: +) -> 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: float | ArrayLike, + north: float | ArrayLike, + up: float | ArrayLike, + lat0: float | ArrayLike, + lon0: float | ArrayLike, + deg: bool = True, +) -> 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, *, 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,14 +594,48 @@ def eci2geodetic(x, y, z, t: datetime, ell: Ellipsoid = None, *, deg: bool = Tru """ 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) -def geodetic2eci(lat, lon, alt, t: datetime, ell: Ellipsoid = None, *, deg: bool = True) -> tuple: +@overload +def geodetic2eci( + 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 @@ -439,7 +668,7 @@ def geodetic2eci(lat, lon, alt, t: datetime, ell: Ellipsoid = None, *, deg: bool 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) @@ -447,16 +676,44 @@ def geodetic2eci(lat, lon, alt, t: datetime, ell: Ellipsoid = None, *, deg: bool 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, - ell: Ellipsoid = None, + 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 @@ -490,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..22bc579e 100644 --- a/src/pymap3d/ellipsoid.py +++ b/src/pymap3d/ellipsoid.py @@ -1,11 +1,9 @@ """Minimal class for planetary ellipsoids""" from __future__ import annotations -from math import sqrt -from dataclasses import dataclass, field + import sys -import warnings -from typing import Dict # for Python < 3.9 +from math import sqrt 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,18 +57,9 @@ 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: """ Ellipsoidal model of world @@ -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}, @@ -152,12 +140,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 67b40e4c..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: bool = True) -> tuple: +@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: bool = True) -> tuple: # 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: bool = True) -> tuple: 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, - n, - u, - lat0, - lon0, - h0, - ell: Ellipsoid = None, + e: ArrayLike, + n: ArrayLike, + u: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def enu2geodetic( + 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[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, - ell: Ellipsoid = None, + 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 e74b9152..e76152b0 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 import asarray + from numpy.typing import NDArray +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, - geocentric_distance, - ell: Ellipsoid = None, + geocentric_lat: ArrayLike, + geocentric_distance: ArrayLike, + ell: Ellipsoid | None = None, deg: bool = True, -): +) -> NDArray[Any]: + pass + + +def geoc2geod( + 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, 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, deg: bool = """ 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, 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, deg: bool """ 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 # type: ignore[no-any-return] + + +@overload +def geodetic2isometric( + geodetic_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass - return degrees(geodetic_lat) if deg else geodetic_lat +@overload +def geodetic2isometric( + geodetic_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass -def geodetic2isometric(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): + +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, deg: bool = True): 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, deg: bool = True): 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, 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, deg: bool = True): 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, 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, deg: bool = True): + 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 + +@overload +def geodetic2conformal( + geodetic_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass -def geodetic2conformal(geodetic_lat, ell: Ellipsoid = None, deg: bool = True): + +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, deg: bool = True): 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, 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, deg: bool = True): """ 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, deg: bool = True): + 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 + + +@overload +def rectifying2geodetic( + rectifying_lat: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> NDArray[Any]: + pass -def rectifying2geodetic(rectifying_lat, ell: Ellipsoid = None, deg: bool = True): +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, deg: bool = True) + 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, 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, deg: bool = True): - 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] -def authalic2geodetic(authalic_lat, ell: Ellipsoid = None, deg: bool = True): +@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: 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, deg: bool = True): + 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, 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, deg: bool = True): 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] + +@overload +def parametric2geodetic( + parametric_lat: float, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass -def parametric2geodetic(parametric_lat, ell: Ellipsoid = None, deg: bool = True): + +@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, deg: bool = True) 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 d4e05c7c..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, - ell: Ellipsoid = None, + 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 37055ec7..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, 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, 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 + + +@overload +def meridian_arc( + lat1: ArrayLike, lat2: ArrayLike, ell: Ellipsoid | None = None, deg: bool = True +) -> float: + pass -def meridian_arc(lat1, lat2, ell: Ellipsoid = None, deg: bool = True) -> float: +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, deg: bool = True) -> float: 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, - ell: Ellipsoid = None, + 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, - ell: Ellipsoid = None, + 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, 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, deg: bool = True) -> float 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, 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, 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..a176309e 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[assignment] 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 c8885767..405b04bb 100644 --- a/src/pymap3d/ned.py +++ b/src/pymap3d/ned.py @@ -1,13 +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 @@ -32,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 @@ -63,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, - ell: Ellipsoid = None, + 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 @@ -110,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, - e, - d, - lat0, - lon0, - h0, - ell: Ellipsoid = None, + n: ArrayLike, + e: ArrayLike, + d: ArrayLike, + lat0: ArrayLike, + lon0: ArrayLike, + h0: ArrayLike, + ell: Ellipsoid | None = None, deg: bool = True, -) -> tuple: +) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: + pass + + +def ned2ecef( + 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[float, float, float] | tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ North, East, Down to target ECEF coordinates @@ -158,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, - y, - z, - lat0, - lon0, - h0, - ell: Ellipsoid = None, + 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 ecef2ned( + 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]]: """ Convert ECEF x,y,z to North, East, Down @@ -205,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, - ell: Ellipsoid = None, + 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 @@ -254,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 @@ -290,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/py.typed b/src/pymap3d/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/pymap3d/rcurve.py b/src/pymap3d/rcurve.py index d3ec1cda..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, 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, deg: bool = True): ) -def parallel(lat, ell: Ellipsoid = 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, deg: bool = True) -> float: return cos(lat) * transverse(lat, ell, deg=False) -def meridian(lat, ell: Ellipsoid = 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, 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, 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, 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 020dce5d..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 @@ -23,7 +27,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 +48,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 @@ -66,12 +70,12 @@ def authalic(ell: Ellipsoid = 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 -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 @@ -86,17 +90,53 @@ def rectifying(ell: Ellipsoid = 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, - ell: Ellipsoid = None, + 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, 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 @@ -175,7 +232,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 @@ -197,12 +254,12 @@ def triaxial(ell: Ellipsoid = 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") -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 @@ -224,6 +281,6 @@ def biaxial(ell: Ellipsoid = 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 5c1b30e4..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, - ell: Ellipsoid = None, + 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, - ell: Ellipsoid = None, + 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..294d33d9 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) @@ -24,11 +24,23 @@ def test_geodetic_alt_geocentric(geodetic_lat, alt_m, geocentric_lat): ) +@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)], ) -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 +53,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 +70,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 +85,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 +95,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 +110,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 +120,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 +132,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 +142,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 +154,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 +164,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 +176,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..58b8f7af 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([z] for z in lla0) + aer1 = tuple([z] for z in 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..5e3bae93 100755 --- a/src/pymap3d/tests/test_time.py +++ b/src/pymap3d/tests/test_time.py @@ -1,4 +1,14 @@ +from __future__ import annotations + from datetime import datetime +from importlib.util import find_spec +from typing import cast + +try: + from numpy import datetime64 + from numpy.typing import NDArray +except ImportError: + pass import pymap3d.sidereal as pms import pytest @@ -6,34 +16,50 @@ 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(): +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 - 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(): +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 +71,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..d38ff90f 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) @@ -34,22 +58,21 @@ def str2dt(time: str | datetime) -> datetime: try: return dateutil.parser.parse(time) except NameError: - raise ImportError("pip install dateutil") + raise ImportError("pip install python-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 python-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 bbc8a324..78b5f0d2 100644 --- a/src/pymap3d/utils.py +++ b/src/pymap3d/utils.py @@ -5,29 +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 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) @@ -36,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) @@ -45,7 +100,19 @@ def sph2cart(az, el, r) -> tuple: return x, y, z -def sanitize(lat, ell: Ellipsoid | None, deg: bool) -> tuple: +@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") @@ -54,15 +121,16 @@ def sanitize(lat, ell: Ellipsoid | None, deg: bool) -> tuple: 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 537d0d14..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, - ell: Ellipsoid = None, -) -> tuple: + 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[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, - ell: Ellipsoid = None, -) -> tuple: + Lat1: float | ArrayLike, + Lon1: float | ArrayLike, + Rng: float | ArrayLike, + Azim: float | ArrayLike, + ell: Ellipsoid | None = None, +) -> 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, - ell: Ellipsoid = None, + 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]