pax_global_header00006660000000000000000000000064145620151710014514gustar00rootroot0000000000000052 comment=0bb4350d45929ebff69047260f7934b30b5e9bae pymap3d-3.1.0/000077500000000000000000000000001456201517100130725ustar00rootroot00000000000000pymap3d-3.1.0/.flake8000066400000000000000000000001271456201517100142450ustar00rootroot00000000000000[flake8] ignore = E501, W503 exclude = .git,__pycache__,.eggs/,doc/,docs/,build/,dist/ pymap3d-3.1.0/.gitattributes000066400000000000000000000001361456201517100157650ustar00rootroot00000000000000archive/* linguist-documentation docs/* linguist-documentation paper/* linguist-documentation pymap3d-3.1.0/.github/000077500000000000000000000000001456201517100144325ustar00rootroot00000000000000pymap3d-3.1.0/.github/contributors.md000066400000000000000000000004251456201517100175120ustar00rootroot00000000000000Thanks to those who contributed code and ideas, including: ``` @aldebaran1 (robustness) @rpavlick (multiple features + functions) @cchuravy (Ellipsoid parameters) @jprMesh (more conversion functions) @Fil (docs) @SamuelMarks (docs) @Yozh2 (numerous ellipsoids and tests) ``` pymap3d-3.1.0/.github/workflows/000077500000000000000000000000001456201517100164675ustar00rootroot00000000000000pymap3d-3.1.0/.github/workflows/README.md000066400000000000000000000007701456201517100177520ustar00rootroot00000000000000# 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. Requires the repo secret `PYPI_API_TOKEN` to be set to a PyPI API token: see [PyPI's help](https://pypi.org/help/#apitoken) for instructions on how to generate one. pymap3d-3.1.0/.github/workflows/ci.yml000066400000000000000000000034351456201517100176120ustar00rootroot00000000000000name: ci on: push: paths: - "**.py" - .github/workflows/ci.yml - "!scripts/**" jobs: full: runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} strategy: matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: [ubuntu-latest] include: - os: macos-latest python-version: '3.12' - os: windows-latest python-version: '3.12' steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: pip install .[full,tests,lint] - run: flake8 - run: mypy if: ${{ matrix.python-version >= '3.9' }} - run: pytest coverage: runs-on: ubuntu-latest name: Python ${{ matrix.python-version }} strategy: matrix: python-version: ["3.12"] steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install tests and lint dependencies run: pip install -e .[tests,coverage] - name: Collect coverage without NumPy run: pytest --cov=src --cov-report=xml - name: Install NumPy run: pip install -e .[core] - name: Collect coverage with NumPy run: pytest --cov=src --cov-report=xml --cov-append # - name: Install full dependencies # run: pip install -e .[full] # - name: Test with full dependencies and collect coverage # run: pytest --cov=src --cov-report=xml --cov-append - name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./coverage.xml name: Python ${{ matrix.python-version }} pymap3d-3.1.0/.github/workflows/ci_stdlib_only.yml000066400000000000000000000012701456201517100222070ustar00rootroot00000000000000name: ci_stdlib_only on: push: paths: - "**.py" - .github/workflows/ci_stdlib_only.yml - "!scripts/**" jobs: stdlib_only: runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} strategy: matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: ['ubuntu-latest'] include: - os: macos-latest python-version: '3.12' - os: windows-latest python-version: '3.12' steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: pip install .[tests] - run: pytest pymap3d-3.1.0/.github/workflows/publish-python-package.yml000066400000000000000000000010711456201517100235670ustar00rootroot00000000000000# https://docs.pypi.org/trusted-publishers/using-a-publisher/ name: publish on: release: types: [published] jobs: release: runs-on: ubuntu-latest environment: release permissions: id-token: write steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install builder run: pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@release/v1 pymap3d-3.1.0/.gitignore000066400000000000000000000000701456201517100150570ustar00rootroot00000000000000.mypy_cache/ .pytest_cache/ __pycache__/ build/ dist/ pymap3d-3.1.0/.nojekyll000066400000000000000000000000001456201517100147100ustar00rootroot00000000000000pymap3d-3.1.0/CITATION.cff000066400000000000000000000002311456201517100147600ustar00rootroot00000000000000cff-version: 1.2.0 authors: - family-names: Hirsch given-names: Michael orcid: 0000-0002-1637-6526 title: PyGemini doi: 10.5281/zenodo.3262738 pymap3d-3.1.0/Examples/000077500000000000000000000000001456201517100146505ustar00rootroot00000000000000pymap3d-3.1.0/Examples/angle_distance.py000077500000000000000000000013101456201517100201600ustar00rootroot00000000000000#!/usr/bin/env python from argparse import ArgumentParser from pymap3d.haversine import anglesep, anglesep_meeus from pytest import approx p = ArgumentParser(description="angular distance between two sky points") p.add_argument("r0", help="right ascension: first point [deg]", type=float) p.add_argument("d0", help="declination: first point [deg]", type=float) p.add_argument("r1", help="right ascension: 2nd point [deg]", type=float) p.add_argument("d1", help="declination: 2nd point [degrees]", type=float) a = p.parse_args() dist_deg = anglesep_meeus(a.r0, a.d0, a.r1, a.d1) dist_deg_astropy = anglesep(a.r0, a.d0, a.r1, a.d1) print(f"{dist_deg:.6f} deg sep") assert dist_deg == approx(dist_deg_astropy) pymap3d-3.1.0/Examples/azel2radec.py000077500000000000000000000013551456201517100172450ustar00rootroot00000000000000#!/usr/bin/env python """ Example Kitt Peak ./demo_azel2radec.py 264.9183 37.911388 31.9583 -111.597 2014-12-25T22:00:00MST """ from argparse import ArgumentParser from pymap3d import azel2radec p = ArgumentParser( description="convert azimuth and elevation to " "right ascension and declination" ) p.add_argument("azimuth", help="azimuth [deg]", type=float) p.add_argument("elevation", help="elevation [deg]", type=float) p.add_argument("lat", help="WGS84 obs. lat [deg]", type=float) p.add_argument("lon", help="WGS84 obs. lon [deg]", type=float) p.add_argument("time", help="obs. time YYYY-mm-ddTHH:MM:SSZ") P = p.parse_args() ra, dec = azel2radec(P.azimuth, P.elevation, P.lat, P.lon, P.time) print("ra [deg] ", ra, " dec [deg] ", dec) pymap3d-3.1.0/Examples/geodetic to ENU.ipynb000066400000000000000000000014451456201517100205150ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pymap3d as pm\n", "\n", "lat0, lon0, alt0 = 4029.25745, 11146.61129, 1165.2\n", "lat, lon, alt = 4028.25746, 11147.61125, 1165.1\n", "e, n, u = pm.geodetic2enu(lat, lon, alt, lat0, lon0, alt0)\n", "print(e, n, u)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.5" } }, "nbformat": 4, "nbformat_minor": 4 } pymap3d-3.1.0/Examples/plot_geodetic2ecef.py000066400000000000000000000023061456201517100207510ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations import typing import argparse import matplotlib.pyplot as mpl import numpy as np import pymap3d as pm p = argparse.ArgumentParser() p.add_argument("alt_m", help="altitude [meters]", type=float, default=0.0, nargs="?") args = p.parse_args() lat, lon = np.meshgrid(np.arange(-90, 90, 0.1), np.arange(-180, 180, 0.2)) x, y, z = pm.geodetic2ecef(lat, lon, args.alt_m) def panel(ax, val, name: str, cmap: str | None = None): hi = ax.pcolormesh(lon, lat, val, cmap=cmap) ax.set_title(name) fg.colorbar(hi, ax=ax).set_label(name + " [m]") ax.set_xlabel("longitude [deg]") fg = mpl.figure(figsize=(16, 5)) axs: typing.Any = fg.subplots(1, 3, sharey=True) fg.suptitle("geodetic2ecef") panel(axs[0], x, "x", "bwr") panel(axs[1], y, "y", "bwr") panel(axs[2], z, "z", "bwr") axs[0].set_ylabel("latitude [deg]") fg = mpl.figure(figsize=(16, 5)) axs = fg.subplots(1, 3, sharey=True) fg.suptitle(r"|$\nabla$ geodetic2ecef|") panel(axs[0], np.hypot(*np.gradient(x)), r"|$\nabla$ x|") panel(axs[1], np.hypot(*np.gradient(y)), r"|$\nabla$ y|") panel(axs[2], np.hypot(*np.gradient(z)), r"|$\nabla$ z|") axs[0].set_ylabel("latitude [deg]") mpl.show() pymap3d-3.1.0/Examples/radec2azel.py000077500000000000000000000014341456201517100172430ustar00rootroot00000000000000#!/usr/bin/env python """ Example Kitt Peak ./radec2azel.py 257.96295344 15.437854 31.9583 -111.5967 2014-12-25T22:00:00MST """ from argparse import ArgumentParser from pymap3d import radec2azel p = ArgumentParser(description="RightAscension,Declination =>" "Azimuth,Elevation") p.add_argument("ra", help="right ascension [degrees]", type=float) p.add_argument("dec", help="declination [degrees]", type=float) p.add_argument("lat", help="WGS84 latitude of observer [degrees]", type=float) p.add_argument("lon", help="WGS84 latitude of observer [degrees]", type=float) p.add_argument("time", help="UTC time of observation YYYY-mm-ddTHH:MM:SSZ") P = p.parse_args() az_deg, el_deg = radec2azel(P.ra, P.dec, P.lat, P.lon, P.time) print("azimuth: [deg]", az_deg) print("elevation [deg]:", el_deg) pymap3d-3.1.0/Examples/vdist_poi.py000066400000000000000000000044111456201517100172220ustar00rootroot00000000000000#!/usr/bin/env python """ Example of using Google Maps queries and PyMap3D https://developers.google.com/places/web-service/search This requires a Google Cloud key, and costs a couple US cents per query. TODO: Would like to instead query a larger region, would OSM be an option? """ import functools from argparse import ArgumentParser from pathlib import Path import pandas import requests from pymap3d.vincenty import vdist URL = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?" @functools.lru_cache() def get_place_coords( place_type: str, latitude: float, longitude: float, search_radius_km: int, keyfn: Path ) -> pandas.DataFrame: """ Get places using Google Maps Places API Requires you to have a Google Cloud account with API key. """ keyfn = Path(keyfn).expanduser() key = keyfn.read_text() stub = URL + f"location={latitude},{longitude}" stub += f"&radius={search_radius_km * 1000}" stub += f"&types={place_type}" stub += f"&key={key}" r = requests.get(stub) r.raise_for_status() place_json = r.json()["results"] places = pandas.DataFrame( index=[p["name"] for p in place_json], columns=["latitude", "longitude", "distance_km", "vicinity"], ) places["latitude"] = [p["geometry"]["location"]["lat"] for p in place_json] places["longitude"] = [p["geometry"]["location"]["lng"] for p in place_json] places["vicinity"] = [p["vicinity"] for p in place_json] return places if __name__ == "__main__": p = ArgumentParser() p.add_argument( "place_type", help="Place type to search: https://developers.google.com/places/supported_types", ) p.add_argument( "searchloc", help="initial latituude, longitude to search from", nargs=2, type=float ) p.add_argument("radius", help="search radius (kilometers)", type=int) p.add_argument("refloc", help="reference location (lat, lon)", nargs=2, type=float) p.add_argument("-k", "--keyfn", help="Google Places API key file", default="~/googlemaps.key") a = p.parse_args() place_coords = get_place_coords(a.place_type, *a.searchloc, a.radius, a.keyfn) place_coords["distance_km"] = ( vdist(place_coords["latitude"], place_coords["longitude"], *a.refloc)[0] / 1e3 ) pymap3d-3.1.0/LICENSE000066400000000000000000000025271456201517100141050ustar00rootroot00000000000000Copyright (c) 2014-2022 Michael Hirsch, Ph.D. Copyright (c) 2013, Felipe Geremia Nievinski Copyright (c) 2004-2007 Michael Kleder Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pymap3d-3.1.0/README.md000066400000000000000000000156621456201517100143630ustar00rootroot00000000000000# Python 3-D coordinate conversions [![image](https://zenodo.org/badge/DOI/10.5281/zenodo.213676.svg)](https://doi.org/10.5281/zenodo.213676) [![image](http://joss.theoj.org/papers/10.21105/joss.00580/status.svg)](https://doi.org/10.21105/joss.00580) [![codecov](https://codecov.io/gh/geospace-code/pymap3d/branch/main/graph/badge.svg?token=DFWBW6TKNr)](https://codecov.io/gh/geospace-code/pymap3d) ![Actions Status](https://github.com/geospace-code/pymap3d/workflows/ci/badge.svg) ![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) Pure Python (no prerequistes beyond Python itself) 3-D geographic coordinate conversions and geodesy. Function syntax is roughly similar to Matlab Mapping Toolbox. PyMap3D is intended for non-interactive use on massively parallel (HPC) and embedded systems. [API docs](https://geospace-code.github.io/pymap3d/) 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) * [C++](https://github.com/ClancyWalters/cppmap3d) ## Prerequisites Numpy and AstroPy are optional. Algorithms from Vallado and Meeus are used if AstroPy is not present. ## Install ```sh python3 -m pip install pymap3d ``` or for the latest development code: ```sh git clone https://github.com/geospace-code/pymap3d pip install -e pymap3d ``` One can verify Python functionality after installation by: ```sh pytest pymap3d ``` ## Usage Where consistent with the definition of the functions, all arguments may be arbitrarily shaped (scalar, N-D array). ```python import pymap3d as pm x,y,z = pm.geodetic2ecef(lat,lon,alt) az,el,range = pm.geodetic2aer(lat, lon, alt, observer_lat, observer_lon, 0) ``` [Python](https://www.python.org/dev/peps/pep-0448/) [argument unpacking](https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists) can be used for compact function arguments with scalars or arbitrarily shaped N-D arrays: ```python aer = (az,el,slantrange) obslla = (obs_lat ,obs_lon, obs_alt) lla = pm.aer2geodetic(*aer, *obslla) ``` where tuple `lla` is comprised of scalar or N-D arrays `(lat,lon,alt)`. Example scripts are in the [examples](./Examples) directory. Native Python float is typically [64 bit](https://docs.python.org/3/library/stdtypes.html#typesnumeric). Numpy can select real precision bits: 32, 64, 128, etc. ### Functions Popular mapping toolbox functions ported to Python include the following, where the source coordinate system (before the "2") is converted to the desired coordinate system: ``` aer2ecef aer2enu aer2geodetic aer2ned ecef2aer ecef2enu ecef2enuv ecef2geodetic ecef2ned ecef2nedv ecef2eci eci2ecef eci2aer aer2eci geodetic2eci eci2geodetic enu2aer enu2ecef enu2geodetic geodetic2aer geodetic2ecef geodetic2enu geodetic2ned ned2aer ned2ecef ned2geodetic azel2radec radec2azel lookAtSpheroid track2 departure meanm rcurve rsphere geod2geoc geoc2geod geodetic2spherical spherical2geodetic ``` Vincenty functions "vincenty.vreckon" and "vincenty.vdist" are accessed like: ```python import pymap3d.vincenty as pmv lat2, lon2 = pmv.vreckon(lat1, lon1, ground_range_m, azimuth_deg) 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 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) ### Ellipsoid Numerous functions in pymap3d use an ellipsoid model. The default is WGS84 Ellipsoid. Numerous other ellipsoids are available in pymap3d.Ellipsoid. Print available ellipsoid models: ```python import pymap3d as pm print(pm.Ellipsoid.models) ``` Specify GRS80 ellipsoid: ```python import pymap3d as pm ell = pm.Ellipsoid.from_name('grs80') ``` ### array vs scalar Use of pymap3d on embedded systems or other streaming data applications often deal with scalar position data. These data are handled efficiently with the Python math stdlib module. Vector data can be handled via list comprehension. Those needing multidimensional data with SIMD and other Numpy and/or PyPy accelerated performance can do so automatically by installing Numpy. pymap3d seamlessly falls back to Python's math module if Numpy isn't present. To keep the code clean, only scalar data can be used without Numpy. As noted above, use list comprehension if you need vector data without Numpy. ### Caveats * 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. ## Compare to Matlab Mapping and Aerospace Toolbox The tests in files tests/test_matlab*.py selected by ```sh pytest -k matlab # run from pymap3d/ top-level directory ``` use [Matlab Engine for Python](https://www.mathworks.com/help/matlab/matlab_external/install-the-matlab-engine-for-python.html) to compare Python PyMap3D output with Matlab output using Matlab functions. ## 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, 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. ### AstroPy.Units.Quantity At this time, [AstroPy.Units.Quantity](http://docs.astropy.org/en/stable/units/) is not supported. Let us know if this is of interest. Impacts on performance would have to be considered before making Quantity a first-class citizen. For now, you can workaround by passing in the `.value` of the variable. pymap3d-3.1.0/codemeta.json000066400000000000000000000020161456201517100155450ustar00rootroot00000000000000{ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/BSD-2-Clause", "codeRepository": "https://github.com/geospace-code/pymap3d", "contIntegration": "https://github.com/geospace-code/pymap3d/actions", "dateCreated": "2014-08-01", "datePublished": "2014-08-03", "dateModified": "2020-05-10", "issueTracker": "https://github.com/geospace-code/pymap3d/issues", "name": "PyMap3d", "identifier": "10.5281/zenodo.3262738", "description": "pure-Python (Numpy optional) 3D coordinate conversions for geospace", "applicationCategory": "geospace", "developmentStatus": "active", "funder": { "@type": "Organization", "name": "AFOSR" }, "keywords": [ "coordinate transformation" ], "programmingLanguage": [ "Python" ], "author": [ { "@type": "Person", "@id": "https://orcid.org/0000-0002-1637-6526", "givenName": "Michael", "familyName": "Hirsch" } ] } pymap3d-3.1.0/paper/000077500000000000000000000000001456201517100142015ustar00rootroot00000000000000pymap3d-3.1.0/paper/codemeta.json000066400000000000000000000012041456201517100166520ustar00rootroot00000000000000{ "@context": "https://raw.githubusercontent.com/codemeta/codemeta/master/codemeta.jsonld", "@type": "Code", "author": [ { "@id": "", "@type": "Person", "email": "", "name": "Michael Hirsch, Ph.D", "affiliation": "" } ], "identifier": "http://doi.org/10.5281/zenodo.213676", "codeRepository": "https://github.com/scivision/pymap3d", "datePublished": "2018-01-28", "dateModified": "2018-01-28", "dateCreated": "2018-01-28", "description": "3-D coordinate conversions in pure Python", "keywords": "geospace,coordinates", "license": "BSD", "title": "pymap3d", "version": "1.4.0" } pymap3d-3.1.0/paper/generate.rb000077500000000000000000000114101456201517100163200ustar00rootroot00000000000000#!/usr/bin/ruby # https://gist.github.com/arfon/478b2ed49e11f984d6fb # For an OO language, this is distinctly procedural. Should probably fix that. require 'json' details = Hash.new({}) capture_params = [ { :name => "title", :message => "Enter project name." }, { :name => "url", :message => "Enter the URL of the project repository." }, { :name => "description", :message => "Enter the (short) project description." }, { :name => "license", :message => "Enter the license this software shared under. (hit enter to skip)\nFor example MIT, BSD, GPL v3.0, Apache 2.0" }, { :name => "doi", :message => "Enter the DOI of the archived version of this code. (hit enter to skip)\nFor example http://dx.doi.org/10.6084/m9.figshare.828487" }, { :name => "keywords", :message => "Enter keywords that should be associated with this project (hit enter to skip)\nComma-separated, for example: turkey, chicken, pot pie" }, { :name => "version", :message => "Enter the version of your software (hit enter to skip)\nSEMVER preferred: http://semver.org e.g. v1.0.0" } ] puts "I'm going to try and help you prepare some things for your JOSS submission" puts "If all goes well then we'll have a nice codemeta.json file soon..." puts "" puts "************************************" puts "* First, some basic details *" puts "************************************" puts "" # Loop through the desired captures and print out for clarity capture_params.each do |param| puts param[:message] print "> " input = gets details[param[:name]] = input.chomp puts "" puts "OK, your project has #{param[:name]}: #{input}" puts "" end puts "" puts "************************************" puts "* Experimental stuff *" puts "************************************" puts "" puts "Would you like me to try and build a list of authors for you?" puts "(You need to be running this script in a git repository for this to work)" print "> (Y/N)" answer = gets.chomp case answer.downcase when "y", "yes" # Use git shortlog to extract a list of author names and commit counts. # Note we don't extract emails here as there's often different emails for # each user. Instead we capture emails at the end. git_log = `git shortlog --summary --numbered --no-merges` # ["252\tMichael Jackson", "151\tMC Hammer"] authors_and_counts = git_log.split("\n").map(&:strip) authors_and_counts.each do |author_count| count, author = author_count.split("\t").map(&:strip) puts "Looks like #{author} made #{count} commits" puts "Add them to the output?" print "> (Y/N)" answer = gets.chomp # If a user chooses to add this author to the output then we ask for some # additional information including their email, ORCID and affiliation. case answer.downcase when "y", "yes" puts "What is #{author}'s email address? (hit enter to skip)" print "> " email = gets.chomp puts "What is #{author}'s ORCID? (hit enter to skip)" puts "For example: http://orcid.org/0000-0000-0000-0000" print "> " orcid = gets.chomp puts "What is #{author}'s affiliation? (hit enter to skip)" print "> " affiliation = gets.chomp details['authors'].merge!(author => { 'commits' => count, 'email' => email, 'orcid' => orcid, 'affiliation' => affiliation }) when "n", "no" puts "OK boss..." puts "" end end when "n", "no" puts "OK boss..." puts "" end puts "Reticulating splines" 5.times do print "." sleep 0.5 end puts "" puts "Generating some JSON goodness..." # TODO: work out how to use some kind of JSON template here. # Build the output list of authors from the inputs we've collected. output_authors = [] details['authors'].each do |author_name, values| entry = { "@id" => values['orcid'], "@type" => "Person", "email" => values['email'], "name" => author_name, "affiliation" => values['affiliation'] } output_authors << entry end # TODO: this is currently a static template (written out here). It would be good # to do something smarter here. output = { "@context" => "https://raw.githubusercontent.com/codemeta/codemeta/master/codemeta.jsonld", "@type" => "Code", "author" => output_authors, "identifier" => details['doi'], "codeRepository" => details['url'], "datePublished" => Time.now.strftime("%Y-%m-%d"), "dateModified" => Time.now.strftime("%Y-%m-%d"), "dateCreated" => Time.now.strftime("%Y-%m-%d"), "description" => details['description'], "keywords" => details['keywords'], "license" => details['license'], "title" => details['title'], "version" => details['version'] } File.open('codemeta.json', 'w') {|f| f.write(JSON.pretty_generate(output)) } pymap3d-3.1.0/paper/paper.bib000066400000000000000000000021401456201517100157630ustar00rootroot00000000000000 @article{vincenty, author = {T. Vincenty}, title = {Direct and Inverse Solutions of Geodesics on the Ellipsoid with Application of Nested Equations}, journal = {Survey Review}, volume = 23, number = 176, year = 1975, month = 4, pages = {88--93}, url = {https://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf}, } @online{veness, author = {Chris Veness}, title = {Vincenty solutions of geodesics on the ellipsoid}, year = 2016, url = {http://www.movable-type.co.uk/scripts/latlong-vincenty.html#direct}, } @online{pymap3d, author = {Michael Hirsch}, title = {PyMap3D: 3-D coordinate conversion software for Python and Matlab}, year = 2018, url = {https://github.com/scivision/pymap3d}, doi = {https://doi.org/10.5281/zenodo.595430}, } @ARTICLE{7368896, author={M. Hirsch and J. Semeter and M. Zettergren and H. Dahlgren and C. Goenka and H. Akbari}, journal={IEEE Transactions on Geoscience and Remote Sensing}, title={Reconstruction of Fine-Scale Auroral Dynamics}, year={2016}, volume={54}, number={5}, pages={2780-2791}, doi={10.1109/TGRS.2015.2505686}, ISSN={0196-2892}, month={May},} pymap3d-3.1.0/paper/paper.md000066400000000000000000000055701456201517100156410ustar00rootroot00000000000000--- 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 date: 29 January 2018 bibliography: paper.bib --- # Summary 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. 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, whether underwater, ground-based or space-based platforms. This includes rocket launches, orbiting spacecrafts, UAVs, cameras, radars and many more. By adding ellipsoid parameters, it could be readily be used for other planets as well. The coordinate systems included are: * ECEF (Earth centered, Earth fixed) * ENU (East, North, Up) * NED (North, East, Down) * ECI (Earth Centered Inertial) * Geodetic (Latitude, Longitude, Altitude) * Horizontal Celestial (Alt-Az or Az-El) * Equatorial Celestial (Right Ascension, Declination) 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] ## Other Programs Other Python geodesy programs include: * [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 pymap3d-3.1.0/pyproject.toml000066400000000000000000000030771456201517100160150ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "pymap3d" description = "pure Python (no prereqs) coordinate conversions, following convention of several popular Matlab routines." keywords = ["coordinate-conversion", "geodesy"] classifiers = ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering :: GIS" ] requires-python = ">=3.8" dynamic = ["version", "readme"] [tool.setuptools.dynamic] readme = {file = ["README.md"], content-type = "text/markdown"} version = {attr = "pymap3d.__version__"} [project.optional-dependencies] tests = ["pytest", "pytest-timeout"] lint = ["flake8", "flake8-bugbear", "flake8-builtins", "flake8-blind-except", "mypy", "types-python-dateutil", "types-requests"] coverage = ["pytest-cov"] format = ["black[jupyter]", "isort"] core = ["python-dateutil", "numpy >= 1.10.0"] full = ["astropy", "xarray"] proj = ["pyproj"] [tool.black] line-length = 100 [tool.isort] profile = "black" known_third_party = ["pymap3d"] [tool.mypy] files = ["src", "Examples", "scripts"] ignore_missing_imports = true [tool.coverage.run] branch = true source = ["src/"] [tool.coverage.report] # Regexes for lines to exclude from consideration exclude_lines = [ # Have to re-enable the standard pragma "pragma: no cover", # Don't complain about function overloading "@overload", ] pymap3d-3.1.0/scripts/000077500000000000000000000000001456201517100145615ustar00rootroot00000000000000pymap3d-3.1.0/scripts/benchmark_ecef2geo.py000066400000000000000000000010671456201517100206300ustar00rootroot00000000000000#!/usr/bin/env python3 """ benchmark ecef2geodetic """ import argparse import time import numpy as np from pymap3d.ecef import ecef2geodetic ll0 = (42.0, 82.0) def bench(N: int) -> float: x = np.random.random(N) y = np.random.random(N) z = np.random.random(N) tic = time.monotonic() _, _, _ = ecef2geodetic(x, y, z) return time.monotonic() - tic if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("N", type=int) args = p.parse_args() N = args.N print(f"ecef2geodetic: {bench(N):.3f} seconds") pymap3d-3.1.0/scripts/benchmark_vincenty.py000077500000000000000000000025611456201517100210130ustar00rootroot00000000000000#!/usr/bin/env python """ vreckon and vdist are iterative algorithms. How much does PyPy help over Cpython? Hmm, PyPy is slower than Cpython.. $ pypy3 tests/benchmark_vincenty.py 10000 2.1160879135131836 0.06056046485900879 $ python tests/benchmark_vincenty.py 10000 0.3325080871582031 0.02107095718383789 """ import argparse import shutil import subprocess import time from pathlib import Path import numpy as np from pymap3d.vincenty import vdist, vreckon R = Path(__file__).parent MATLAB = shutil.which("matlab") ll0 = (42.0, 82.0) def bench_vreckon(N: int) -> float: sr = np.random.random(N) az = np.random.random(N) tic = time.monotonic() _, _ = vreckon(ll0[0], ll0[1], sr, az) return time.monotonic() - tic def bench_vdist(N: int) -> float: lat = np.random.random(N) lon = np.random.random(N) tic = time.monotonic() _, _ = vdist(ll0[0], ll0[1], lat, lon) return time.monotonic() - tic if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("N", help="number of iterations", type=int) args = p.parse_args() N = args.N print(f"vreckon: {bench_vreckon(N):.3f}") print(f"vdist: {bench_vdist(N):.3f}") if MATLAB: print(f"matlab path {R}") subprocess.check_call( f'matlab -batch "helper_vdist({ll0[0]}, {ll0[1]}, {N})"', text=True, timeout=90, cwd=R ) pymap3d-3.1.0/scripts/helper_vdist.m000066400000000000000000000010001456201517100174160ustar00rootroot00000000000000function helper_vdist(lat, lon, N) addons = matlab.addons.installedAddons(); M1 = repmat(lat, N, 1); M2 = repmat(lon, N, 1); L1 = rand(N,1); L2 = rand(N,1); if any(addons.Name == "Mapping Toolbox") disp("Using Mapping Toolbox distance()") f = @() distance(lat, lon, L1, L2); elseif ~isempty(what("matmap3d")) disp("Using matmap3d.vdist()") f = @() matmap3d.vdist(M1, M2, L1, L2); else error("Matlab Mapping Toolbox is not installed") end t = timeit(f); disp(t) end pymap3d-3.1.0/src/000077500000000000000000000000001456201517100136615ustar00rootroot00000000000000pymap3d-3.1.0/src/pymap3d/000077500000000000000000000000001456201517100152365ustar00rootroot00000000000000pymap3d-3.1.0/src/pymap3d/__init__.py000066400000000000000000000046121456201517100173520ustar00rootroot00000000000000""" PyMap3D provides coordinate transforms and geodesy functions with a similar API to the Matlab Mapping Toolbox, but was of course independently derived. For all functions, the default units are: distance : float METERS angles : float DEGREES time : datetime.datetime UTC time of observation These functions may be used with any planetary body, provided the appropriate reference ellipsoid is defined. The default ellipsoid is WGS-84 deg : bool = True means degrees. False = radians. Most functions accept NumPy arrays of any shape, as well as compatible data types including AstroPy, Pandas and Xarray that have Numpy-like data properties. For clarity, we omit all these types in the docs, and just specify the scalar type. Other languages --------------- Companion packages exist for: * Matlab / GNU Octave: [Matmap3D](https://github.com/geospace-code/matmap3d) * Fortran: [Maptran3D](https://github.com/geospace-code/maptran3d) """ __version__ = "3.1.0" from .aer import aer2ecef, aer2geodetic, ecef2aer, geodetic2aer from .ecef import ( ecef2enu, ecef2enuv, ecef2geodetic, eci2geodetic, enu2ecef, enu2uvw, geodetic2ecef, geodetic2eci, uvw2enu, ) from .ellipsoid import Ellipsoid from .enu import aer2enu, enu2aer, enu2geodetic, geodetic2enu from .ned import ( aer2ned, ecef2ned, ecef2nedv, geodetic2ned, ned2aer, ned2ecef, ned2geodetic, ) from .sidereal import datetime2sidereal, greenwichsrt from .spherical import geodetic2spherical, spherical2geodetic from .timeconv import str2dt __all__ = [ "aer2ecef", "aer2geodetic", "ecef2aer", "geodetic2aer", "ecef2enu", "ecef2enuv", "ecef2geodetic", "eci2geodetic", "enu2ecef", "enu2uvw", "geodetic2ecef", "geodetic2eci", "uvw2enu", "Ellipsoid", "aer2enu", "enu2aer", "enu2geodetic", "geodetic2enu", "aer2ned", "ecef2ned", "ecef2nedv", "geodetic2ned", "ned2aer", "ned2ecef", "ned2geodetic", "datetime2sidereal", "greenwichsrt", "geodetic2spherical", "spherical2geodetic", "str2dt", "azel2radec", "radec2azel", ] try: from .aer import aer2eci, eci2aer from .azelradec import azel2radec, radec2azel from .eci import ecef2eci, eci2ecef __all__ += ["aer2eci", "eci2aer", "ecef2eci", "eci2ecef"] except ImportError: from .vallado import azel2radec, radec2azel pymap3d-3.1.0/src/pymap3d/aer.py000066400000000000000000000161511456201517100163630ustar00rootroot00000000000000""" transforms involving AER: azimuth, elevation, slant range""" 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 try: from .eci import ecef2eci, eci2ecef except ImportError: pass __all__ = ["aer2ecef", "ecef2aer", "geodetic2aer", "aer2geodetic", "eci2aer", "aer2eci"] ELL = Ellipsoid.from_name("wgs84") def ecef2aer( x, y, z, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ compute azimuth, elevation and slant range from an Observer to a Point with ECEF coordinates. ECEF input location is with units of meters Parameters ---------- x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] """ xEast, yNorth, zUp = ecef2enu(x, y, z, lat0, lon0, h0, ell, deg=deg) return enu2aer(xEast, yNorth, zUp, deg=deg) def geodetic2aer( lat, lon, h, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ gives azimuth, elevation and slant range from an Observer to a Point with geodetic coordinates. Parameters ---------- lat : float target geodetic latitude lon : float target geodetic longitude h : float target altitude above geodetic ellipsoid (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- az : float azimuth el : float elevation srange : float slant range [meters] """ e, n, u = geodetic2enu(lat, lon, h, lat0, lon0, h0, ell, deg=deg) return enu2aer(e, n, u, deg=deg) def aer2geodetic( az, el, srange, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ gives geodetic coordinates of a point with az, el, range from an observer at lat0, lon0, h0 Parameters ---------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- In reference ellipsoid system: lat : float geodetic latitude lon : float geodetic longitude alt : float altitude above ellipsoid (meters) """ x, y, z = aer2ecef(az, el, srange, lat0, lon0, h0, ell=ell, deg=deg) return ecef2geodetic(x, y, z, ell=ell, deg=deg) def eci2aer(x, y, z, lat0, lon0, h0, t: datetime, *, deg: bool = True) -> tuple: """ takes Earth Centered Inertial x,y,z ECI coordinates of point and gives az, el, slant range from Observer Parameters ---------- x : float ECI x-location [meters] y : float ECI y-location [meters] z : float ECI z-location [meters] lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) t : datetime.datetime Observation time deg : bool, optional true: degrees, false: radians Returns ------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] """ try: xecef, yecef, zecef = eci2ecef(x, y, z, t) except NameError: raise ImportError("pip install numpy") return ecef2aer(xecef, yecef, zecef, lat0, lon0, h0, deg=deg) def aer2eci( az, el, srange, lat0, lon0, h0, t: datetime, ell: Ellipsoid = ELL, *, deg: bool = True, ) -> tuple: """ gives ECI of a point from an observer at az, el, slant range Parameters ---------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) t : datetime.datetime Observation time ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- Earth Centered Inertial x,y,z x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) """ x, y, z = aer2ecef(az, el, srange, lat0, lon0, h0, ell, deg=deg) try: return ecef2eci(x, y, z, t) except NameError: raise ImportError("pip install numpy") def aer2ecef( az, el, srange, lat0, lon0, alt0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ converts target azimuth, elevation, range from observer at lat0,lon0,alt0 to ECEF coordinates. Parameters ---------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude alt0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- ECEF (Earth centered, Earth fixed) x,y,z x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) Notes ------ if srange==NaN, z=NaN """ # Origin of the local system in geocentric coordinates. x0, y0, z0 = geodetic2ecef(lat0, lon0, alt0, ell, deg=deg) # Convert Local Spherical AER to ENU e1, n1, u1 = aer2enu(az, el, srange, deg=deg) # Rotating ENU to ECEF dx, dy, dz = enu2uvw(e1, n1, u1, lat0, lon0, deg=deg) # Origin + offset from origin equals position in ECEF return x0 + dx, y0 + dy, z0 + dz pymap3d-3.1.0/src/pymap3d/azelradec.py000066400000000000000000000054111456201517100175430ustar00rootroot00000000000000""" Azimuth / elevation <==> Right ascension, declination """ from __future__ import annotations from datetime import datetime from .timeconv import str2dt # astropy can't handle xarray times (yet) from .vallado import azel2radec as vazel2radec from .vallado import radec2azel as vradec2azel try: from astropy import units as u from astropy.coordinates import ICRS, AltAz, Angle, EarthLocation, SkyCoord from astropy.time import Time except ImportError: pass __all__ = ["radec2azel", "azel2radec"] def azel2radec( az_deg: float, el_deg: float, lat_deg: float, lon_deg: float, time: datetime, ) -> tuple[float, float]: """ viewing angle (az, el) to sky coordinates (ra, dec) Parameters ---------- az_deg : float azimuth [degrees clockwize from North] el_deg : float elevation [degrees above horizon (neglecting aberration)] lat_deg : float observer latitude [-90, 90] lon_deg : float observer longitude [-180, 180] (degrees) time : datetime.datetime or str time of observation Returns ------- ra_deg : float ecliptic right ascension (degress) dec_deg : float ecliptic declination (degrees) """ try: obs = EarthLocation(lat=lat_deg * u.deg, lon=lon_deg * u.deg) direc = AltAz( location=obs, obstime=Time(str2dt(time)), az=az_deg * u.deg, alt=el_deg * u.deg ) sky = SkyCoord(direc.transform_to(ICRS())) return sky.ra.deg, sky.dec.deg except NameError: return vazel2radec(az_deg, el_deg, lat_deg, lon_deg, time) def radec2azel( ra_deg: float, dec_deg: float, lat_deg: float, lon_deg: float, time: datetime, ) -> tuple[float, float]: """ sky coordinates (ra, dec) to viewing angle (az, el) Parameters ---------- ra_deg : float ecliptic right ascension (degress) dec_deg : float ecliptic declination (degrees) lat_deg : float observer latitude [-90, 90] lon_deg : float observer longitude [-180, 180] (degrees) time : datetime.datetime or str time of observation Returns ------- az_deg : float azimuth [degrees clockwize from North] el_deg : float elevation [degrees above horizon (neglecting aberration)] """ try: obs = EarthLocation(lat=lat_deg * u.deg, lon=lon_deg * u.deg) points = SkyCoord(Angle(ra_deg, unit=u.deg), Angle(dec_deg, unit=u.deg), equinox="J2000.0") altaz = points.transform_to(AltAz(location=obs, obstime=Time(str2dt(time)))) return altaz.az.degree, altaz.alt.degree except NameError: return vradec2azel(ra_deg, dec_deg, lat_deg, lon_deg, time) pymap3d-3.1.0/src/pymap3d/ecef.py000066400000000000000000000272251456201517100165220ustar00rootroot00000000000000""" Transforms involving ECEF: earth-centered, earth-fixed frame """ from __future__ import annotations import warnings try: from numpy import asarray, empty_like, finfo, where from .eci import ecef2eci, eci2ecef except ImportError: pass from datetime import datetime from math import pi from .ellipsoid import Ellipsoid from .mathfun import atan, atan2, cos, degrees, hypot, isclose, radians, sin, sqrt, tan __all__ = [ "geodetic2ecef", "ecef2geodetic", "ecef2enuv", "ecef2enu", "enu2uvw", "uvw2enu", "eci2geodetic", "geodetic2eci", "enu2ecef", ] ELL = Ellipsoid.from_name("wgs84") def geodetic2ecef( lat, lon, alt, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ point transformation from Geodetic of specified ellipsoid (default WGS-84) to ECEF Parameters ---------- lat target geodetic latitude lon target geodetic longitude alt target altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- ECEF (Earth centered, Earth fixed) x,y,z x target x ECEF coordinate (meters) y target y ECEF coordinate (meters) z target z ECEF coordinate (meters) """ if deg: lat = radians(lat) lon = radians(lon) # radius of curvature of the prime vertical section N = ell.semimajor_axis**2 / hypot( ell.semimajor_axis * cos(lat), ell.semiminor_axis * sin(lat) ) # Compute cartesian (geocentric) coordinates given (curvilinear) geodetic coordinates. x = (N + alt) * cos(lat) * cos(lon) y = (N + alt) * cos(lat) * sin(lon) z = (N * (ell.semiminor_axis / ell.semimajor_axis) ** 2 + alt) * sin(lat) return x, y, z def ecef2geodetic( x, y, z, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ convert ECEF (meters) to geodetic coordinates Parameters ---------- x target x ECEF coordinate (meters) y target y ECEF coordinate (meters) z target z ECEF coordinate (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- lat target geodetic latitude lon target geodetic longitude alt target altitude above geodetic ellipsoid (meters) based on: You, Rey-Jer. (2000). Transformation of Cartesian to Geodetic Coordinates without Iterations. Journal of Surveying Engineering. doi: 10.1061/(ASCE)0733-9453 """ try: x = asarray(x) y = asarray(y) z = asarray(z) except NameError: pass r = sqrt(x**2 + y**2 + z**2) E = sqrt(ell.semimajor_axis**2 - ell.semiminor_axis**2) # eqn. 4a u = sqrt(0.5 * (r**2 - E**2) + 0.5 * hypot(r**2 - E**2, 2 * E * z)) hxy = hypot(x, y) huE = hypot(u, E) # eqn. 4b try: Beta = empty_like(r) ibad = isclose(u, 0) | isclose(hxy, 0) Beta[~ibad] = atan(huE[~ibad] / u[~ibad] * z[~ibad] / hxy[~ibad]) # eqn. 13 Beta[~ibad] += ( (ell.semiminor_axis * u[~ibad] - ell.semimajor_axis * huE[~ibad] + E**2) * sin(Beta[~ibad]) ) / (ell.semimajor_axis * huE[~ibad] * 1 / cos(Beta[~ibad]) - E**2 * cos(Beta[~ibad])) iz = ibad & isclose(z, 0) i1 = ibad & ~iz & (z > 0) i2 = ibad & ~iz & ~i1 Beta[iz] = 0 Beta[i1] = pi / 2 Beta[i2] = -pi / 2 except NameError: try: with warnings.catch_warnings(record=True): warnings.simplefilter("error") Beta = atan(huE / u * z / hxy) # eqn. 13 Beta += ( (ell.semiminor_axis * u - ell.semimajor_axis * huE + E**2) * sin(Beta) ) / (ell.semimajor_axis * huE * 1 / cos(Beta) - E**2 * cos(Beta)) except (ArithmeticError, RuntimeWarning): if isclose(z, 0): Beta = 0 elif z > 0: Beta = pi / 2 else: Beta = -pi / 2 # eqn. 4c # %% final output lat = atan(ell.semimajor_axis / ell.semiminor_axis * tan(Beta)) try: # patch latitude for float32 precision loss lim_pi2 = pi / 2 - finfo(Beta.dtype).eps lat = where(Beta >= lim_pi2, pi / 2, lat) lat = where(Beta <= -lim_pi2, -pi / 2, lat) except NameError: pass lon = atan2(y, x) # eqn. 7 cosBeta = cos(Beta) try: # patch altitude for float32 precision loss cosBeta = where(Beta >= lim_pi2, 0, cosBeta) cosBeta = where(Beta <= -lim_pi2, 0, cosBeta) except NameError: pass alt = hypot(z - ell.semiminor_axis * sin(Beta), hxy - ell.semimajor_axis * cosBeta) # inside ellipsoid? inside = ( x**2 / ell.semimajor_axis**2 + y**2 / ell.semimajor_axis**2 + z**2 / ell.semiminor_axis**2 < 1 ) try: if inside.any(): # avoid all false assignment bug alt[inside] = -alt[inside] except (TypeError, AttributeError): if inside: alt = -alt if deg: lat = degrees(lat) lon = degrees(lon) else: try: lat = lat.squeeze()[()] # ensures scalar in, scalar out except AttributeError: pass return lat, lon, alt def ecef2enuv(u, v, w, lat0, lon0, deg: bool = True) -> tuple: """ VECTOR from observer to target ECEF => ENU Parameters ---------- u target x ECEF coordinate (meters) v target y ECEF coordinate (meters) w target z ECEF coordinate (meters) lat0 Observer geodetic latitude lon0 Observer geodetic longitude deg : bool, optional degrees input/output (False: radians in/out) Returns ------- uEast target east ENU coordinate (meters) vNorth target north ENU coordinate (meters) wUp target up ENU coordinate (meters) """ if deg: lat0 = radians(lat0) lon0 = radians(lon0) t = cos(lon0) * u + sin(lon0) * v uEast = -sin(lon0) * u + cos(lon0) * v wUp = cos(lat0) * t + sin(lat0) * w vNorth = -sin(lat0) * t + cos(lat0) * w return uEast, vNorth, wUp def ecef2enu( x, y, z, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ from observer to target, ECEF => ENU Parameters ---------- x target x ECEF coordinate (meters) y target y ECEF coordinate (meters) z target z ECEF coordinate (meters) lat0 Observer geodetic latitude lon0 Observer geodetic longitude h0 observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- East target east ENU coordinate (meters) North target north ENU coordinate (meters) Up 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) def enu2uvw( east, north, up, lat0, lon0, deg: bool = True, ) -> tuple: """ Parameters ---------- east target east ENU coordinate (meters) north target north ENU coordinate (meters) up target up ENU coordinate (meters) lat0 Observer geodetic latitude lon0 Observer geodetic longitude deg : bool, optional degrees input/output (False: radians in/out) Results ------- u v w """ if deg: lat0 = radians(lat0) lon0 = radians(lon0) t = cos(lat0) * up - sin(lat0) * north w = sin(lat0) * up + cos(lat0) * north u = cos(lon0) * t - sin(lon0) * east v = sin(lon0) * t + cos(lon0) * east return u, v, w def uvw2enu(u, v, w, lat0, lon0, deg: bool = True) -> tuple: """ Parameters ---------- u target x ECEF coordinate (meters) v target y ECEF coordinate (meters) w target z ECEF coordinate (meters) lat0 Observer geodetic latitude lon0 Observer geodetic longitude deg : bool, optional degrees input/output (False: radians in/out) Results ------- East target east ENU coordinate (meters) North target north ENU coordinate (meters) Up target up ENU coordinate (meters) """ if deg: lat0 = radians(lat0) lon0 = radians(lon0) t = cos(lon0) * u + sin(lon0) * v East = -sin(lon0) * u + cos(lon0) * v Up = cos(lat0) * t + sin(lat0) * w North = -sin(lat0) * t + cos(lat0) * w return East, North, Up def eci2geodetic(x, y, z, t: datetime, ell: Ellipsoid = ELL, *, deg: bool = True) -> tuple: """ convert Earth Centered Internal ECI to geodetic coordinates J2000 time Parameters ---------- x ECI x-location [meters] y ECI y-location [meters] z ECI z-location [meters] t : datetime.datetime, float UTC time ell : Ellipsoid, optional planet ellipsoid model deg : bool, optional if True, degrees. if False, radians Results ------- lat geodetic latitude lon geodetic longitude alt altitude above ellipsoid (meters) eci2geodetic() a.k.a. eci2lla() """ try: xecef, yecef, zecef = eci2ecef(x, y, z, t) except NameError: raise ImportError("pip install numpy") return ecef2geodetic(xecef, yecef, zecef, ell, deg) def geodetic2eci(lat, lon, alt, t: datetime, ell: Ellipsoid = ELL, *, deg: bool = True) -> tuple: """ convert geodetic coordinates to Earth Centered Internal ECI J2000 frame Parameters ---------- lat geodetic latitude lon geodetic longitude alt altitude above ellipsoid (meters) t : datetime.datetime, float UTC time ell : Ellipsoid, optional planet ellipsoid model deg : bool, optional if True, degrees. if False, radians Results ------- x ECI x-location [meters] y ECI y-location [meters] z ECI z-location [meters] geodetic2eci() a.k.a lla2eci() """ x, y, z = geodetic2ecef(lat, lon, alt, ell, deg) try: return ecef2eci(x, y, z, t) except NameError: raise ImportError("pip install numpy") def enu2ecef( e1, n1, u1, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ ENU to ECEF Parameters ---------- e1 target east ENU coordinate (meters) n1 target north ENU coordinate (meters) u1 target up ENU coordinate (meters) lat0 Observer geodetic latitude lon0 Observer geodetic longitude h0 observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- x target x ECEF coordinate (meters) y target y ECEF coordinate (meters) 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) return x0 + dx, y0 + dy, z0 + dz pymap3d-3.1.0/src/pymap3d/eci.py000066400000000000000000000066431456201517100163610ustar00rootroot00000000000000""" transforms involving ECI earth-centered inertial """ from __future__ import annotations from datetime import datetime import numpy as np try: import astropy.units as u from astropy.coordinates import GCRS, ITRS, CartesianRepresentation, EarthLocation except ImportError: pass from .sidereal import greenwichsrt, juliandate __all__ = ["eci2ecef", "ecef2eci"] def eci2ecef(x, y, z, time: datetime) -> tuple: """ Observer => Point ECI => ECEF J2000 frame Parameters ---------- x : float ECI x-location [meters] y : float ECI y-location [meters] z : float ECI z-location [meters] time : datetime.datetime time of obsevation (UTC) Results ------- x_ecef : float x ECEF coordinate y_ecef : float y ECEF coordinate z_ecef : float z ECEF coordinate """ try: gcrs = GCRS(CartesianRepresentation(x * u.m, y * u.m, z * u.m), obstime=time) itrs = gcrs.transform_to(ITRS(obstime=time)) x_ecef = itrs.x.value y_ecef = itrs.y.value z_ecef = itrs.z.value except NameError: x = np.atleast_1d(x) y = np.atleast_1d(y) z = np.atleast_1d(z) gst = np.atleast_1d(greenwichsrt(juliandate(time))) assert ( x.shape == y.shape == z.shape ), f"shape mismatch: x: ${x.shape} y: {y.shape} z: {z.shape}" if gst.size == 1 and x.size != 1: gst = np.broadcast_to(gst, x.shape[0]) assert x.size == gst.size, f"shape mismatch: x: {x.shape} gst: {gst.shape}" eci = np.column_stack((x.ravel(), y.ravel(), z.ravel())) ecef = np.empty((x.size, 3)) for i in range(eci.shape[0]): ecef[i, :] = R3(gst[i]) @ eci[i, :].T x_ecef = ecef[:, 0].reshape(x.shape) y_ecef = ecef[:, 1].reshape(y.shape) z_ecef = ecef[:, 2].reshape(z.shape) return x_ecef, y_ecef, z_ecef def ecef2eci(x, y, z, time: datetime) -> tuple: """ Point => Point ECEF => ECI J2000 frame Parameters ---------- x : float target x ECEF coordinate y : float target y ECEF coordinate z : float target z ECEF coordinate time : datetime.datetime time of observation Results ------- x_eci : float x ECI coordinate y_eci : float y ECI coordinate z_eci : float z ECI coordinate """ try: itrs = ITRS(CartesianRepresentation(x * u.m, y * u.m, z * u.m), obstime=time) gcrs = itrs.transform_to(GCRS(obstime=time)) eci = EarthLocation(*gcrs.cartesian.xyz) x_eci = eci.x.value y_eci = eci.y.value z_eci = eci.z.value except NameError: x = np.atleast_1d(x) y = np.atleast_1d(y) z = np.atleast_1d(z) gst = np.atleast_1d(greenwichsrt(juliandate(time))) assert x.shape == y.shape == z.shape assert x.size == gst.size ecef = np.column_stack((x.ravel(), y.ravel(), z.ravel())) eci = np.empty((x.size, 3)) for i in range(x.size): eci[i, :] = R3(gst[i]).T @ ecef[i, :] x_eci = eci[:, 0].reshape(x.shape) y_eci = eci[:, 1].reshape(y.shape) z_eci = eci[:, 2].reshape(z.shape) return x_eci, y_eci, z_eci def R3(x: float): """Rotation matrix for ECI""" return np.array([[np.cos(x), np.sin(x), 0], [-np.sin(x), np.cos(x), 0], [0, 0, 1]]) pymap3d-3.1.0/src/pymap3d/ellipsoid.py000066400000000000000000000141261456201517100176000ustar00rootroot00000000000000"""Minimal class for planetary ellipsoids""" from __future__ import annotations from math import sqrt from dataclasses import dataclass, field from typing import TypedDict import sys if sys.version_info < (3, 9): from typing import Dict else: Dict = dict class Model(TypedDict): """Ellipsoid parameters""" name: str a: float b: float @dataclass class Ellipsoid: """ generate reference ellipsoid parameters as everywhere else in pymap3d, distance units are METERS Ellipsoid sources ----------------- maupertuis, plessis, everest1830, everest1830m, everest1967, airy, bessel, clarke1866, clarke1878, clarke1860, helmert, hayford, international1924, krassovsky1940, wgs66, australian, international1967, grs67, sa1969, wgs72, iers1989, iers2003: - https://en.wikipedia.org/wiki/Earth_ellipsoid#Historical_Earth_ellipsoids - https://en.wikibooks.org/wiki/PROJ.4#Spheroid wgs84: https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84 wgs84_mean: https://en.wikipedia.org/wiki/Earth_radius#Mean_radii grs80: https://en.wikipedia.org/wiki/GRS_80 io: https://doi.org/10.1006/icar.1998.5987 pz90.11: https://structure.mil.ru/files/pz-90.pdf gsk2011: https://racurs.ru/downloads/documentation/gost_r_32453-2017.pdf mars: https://tharsis.gsfc.nasa.gov/geodesy.html mercury, venus, moon, jupiter, saturn, uranus, neptune: - https://nssdc.gsfc.nasa.gov/planetary/factsheet/index.html 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 = "" ): """ Ellipsoidal model of world Parameters ---------- semimajor_axis : float semimajor axis in meters semiminor_axis : float semiminor axis in meters name: str, optional Human-friendly name for the ellipsoid model: str, optional Short name for the ellipsoid """ self.flattening = (semimajor_axis - semiminor_axis) / semimajor_axis assert self.flattening >= 0, "flattening must be >= 0" self.thirdflattening = (semimajor_axis - semiminor_axis) / (semimajor_axis + semiminor_axis) self.eccentricity = sqrt(2 * self.flattening - self.flattening**2) self.name = name self.model = model self.semimajor_axis = semimajor_axis self.semiminor_axis = semiminor_axis models = { # Earth ellipsoids "maupertuis": {"name": "Maupertuis (1738)", "a": 6397300.0, "b": 6363806.283}, "plessis": {"name": "Plessis (1817)", "a": 6376523.0, "b": 6355862.9333}, "everest1830": {"name": "Everest (1830)", "a": 6377299.365, "b": 6356098.359}, "everest1830m": { "name": "Everest 1830 Modified (1967)", "a": 6377304.063, "b": 6356103.039, }, "everest1967": { "name": "Everest 1830 (1967 Definition)", "a": 6377298.556, "b": 6356097.55, }, "airy": {"name": "Airy (1830)", "a": 6377563.396, "b": 6356256.909}, "bessel": {"name": "Bessel (1841)", "a": 6377397.155, "b": 6356078.963}, "clarke1866": {"name": "Clarke (1866)", "a": 6378206.4, "b": 6356583.8}, "clarke1878": {"name": "Clarke (1878)", "a": 6378190.0, "b": 6356456.0}, "clarke1860": {"name": "Clarke (1880)", "a": 6378249.145, "b": 6356514.87}, "helmert": {"name": "Helmert (1906)", "a": 6378200.0, "b": 6356818.17}, "hayford": {"name": "Hayford (1910)", "a": 6378388.0, "b": 6356911.946}, "international1924": {"name": "International (1924)", "a": 6378388.0, "b": 6356911.946}, "krassovsky1940": {"name": "Krassovsky (1940)", "a": 6378245.0, "b": 6356863.019}, "wgs66": {"name": "WGS66 (1966)", "a": 6378145.0, "b": 6356759.769}, "australian": {"name": "Australian National (1966)", "a": 6378160.0, "b": 6356774.719}, "international1967": { "name": "New International (1967)", "a": 6378157.5, "b": 6356772.2, }, "grs67": {"name": "GRS-67 (1967)", "a": 6378160.0, "b": 6356774.516}, "sa1969": {"name": "South American (1969)", "a": 6378160.0, "b": 6356774.719}, "wgs72": {"name": "WGS-72 (1972)", "a": 6378135.0, "b": 6356750.52001609}, "grs80": {"name": "GRS-80 (1979)", "a": 6378137.0, "b": 6356752.31414036}, "wgs84": {"name": "WGS-84 (1984)", "a": 6378137.0, "b": 6356752.31424518}, "wgs84_mean": {"name": "WGS-84 (1984) Mean", "a": 6371008.7714, "b": 6371008.7714}, "iers1989": {"name": "IERS (1989)", "a": 6378136.0, "b": 6356751.302}, "pz90.11": {"name": "ПЗ-90 (2011)", "a": 6378136.0, "b": 6356751.3618}, "iers2003": {"name": "IERS (2003)", "a": 6378136.6, "b": 6356751.9}, "gsk2011": {"name": "ГСК (2011)", "a": 6378136.5, "b": 6356751.758}, # Other worlds "mercury": {"name": "Mercury", "a": 2440500.0, "b": 2438300.0}, "venus": {"name": "Venus", "a": 6051800.0, "b": 6051800.0}, "moon": {"name": "Moon", "a": 1738100.0, "b": 1736000.0}, "mars": {"name": "Mars", "a": 3396900.0, "b": 3376097.80585952}, "jupyter": {"name": "Jupiter", "a": 71492000.0, "b": 66770054.3475922}, "io": {"name": "Io", "a": 1829.7, "b": 1815.8}, "saturn": {"name": "Saturn", "a": 60268000.0, "b": 54364301.5271271}, "uranus": {"name": "Uranus", "a": 25559000.0, "b": 24973000.0}, "neptune": {"name": "Neptune", "a": 24764000.0, "b": 24341000.0}, "pluto": {"name": "Pluto", "a": 1188000.0, "b": 1188000.0}, } @classmethod def from_name(cls, name: str) -> Ellipsoid: """Create an Ellipsoid from a name.""" return cls( cls.models[name]["a"], cls.models[name]["b"], name=cls.models[name]["name"], model=name ) pymap3d-3.1.0/src/pymap3d/enu.py000066400000000000000000000105441456201517100164030ustar00rootroot00000000000000""" transforms involving ENU East North Up """ from __future__ import annotations from math import tau try: from numpy import asarray except ImportError: pass from .ecef import ecef2geodetic, enu2ecef, geodetic2ecef, uvw2enu from .ellipsoid import Ellipsoid from .mathfun import atan2, cos, degrees, hypot, radians, sin __all__ = ["enu2aer", "aer2enu", "enu2geodetic", "geodetic2enu"] ELL = Ellipsoid.from_name("wgs84") def enu2aer(e, n, u, deg: bool = True) -> tuple: """ ENU to Azimuth, Elevation, Range Parameters ---------- e : float ENU East coordinate (meters) n : float ENU North coordinate (meters) u : float ENU Up coordinate (meters) deg : bool, optional degrees input/output (False: radians in/out) Results ------- azimuth : float azimuth to rarget elevation : float elevation to target srange : float slant range [meters] """ # 1 millimeter precision for singularity stability try: e[abs(e) < 1e-3] = 0.0 n[abs(n) < 1e-3] = 0.0 u[abs(u) < 1e-3] = 0.0 except TypeError: if abs(e) < 1e-3: e = 0.0 if abs(n) < 1e-3: n = 0.0 if abs(u) < 1e-3: u = 0.0 r = hypot(e, n) slantRange = hypot(r, u) elev = atan2(u, r) az = atan2(e, n) % tau if deg: az = degrees(az) elev = degrees(elev) return az, elev, slantRange def aer2enu(az, el, srange, deg: bool = True) -> tuple: """ Azimuth, Elevation, Slant range to target to East, North, Up Parameters ---------- az : float azimuth clockwise from north (degrees) el : float elevation angle above horizon, neglecting aberrations (degrees) srange : float slant range [meters] deg : bool, optional degrees input/output (False: radians in/out) Returns -------- e : float East ENU coordinate (meters) n : float North ENU coordinate (meters) u : float Up ENU coordinate (meters) """ if deg: el = radians(el) az = radians(az) try: if (asarray(srange) < 0).any(): raise ValueError("Slant range [0, Infinity)") except NameError: if srange < 0: raise ValueError("Slant range [0, Infinity)") r = srange * cos(el) return r * sin(az), r * cos(az), srange * sin(el) def enu2geodetic( e, n, u, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ East, North, Up to target to geodetic coordinates Parameters ---------- e : float East ENU coordinate (meters) n : float North ENU coordinate (meters) u : float Up ENU coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- lat : float geodetic latitude lon : float geodetic longitude alt : float altitude above ellipsoid (meters) """ x, y, z = enu2ecef(e, n, u, lat0, lon0, h0, ell, deg=deg) return ecef2geodetic(x, y, z, ell, deg=deg) def geodetic2enu( lat, lon, h, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ Parameters ---------- lat : float target geodetic latitude lon : float target geodetic longitude h : float target altitude above ellipsoid (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- e : float East ENU n : float North ENU u : float Up ENU """ x1, y1, z1 = geodetic2ecef(lat, lon, h, ell, deg=deg) x2, y2, z2 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) return uvw2enu(x1 - x2, y1 - y2, z1 - z2, lat0, lon0, deg=deg) pymap3d-3.1.0/src/pymap3d/haversine.py000066400000000000000000000056521456201517100176040ustar00rootroot00000000000000""" Compute angular separation in the sky using haversine Note: decimal points on constants made 0 difference in `%timeit` execution time The Meeus algorithm is about 9.5% faster than Astropy/Vicenty on my PC, and gives virtually identical result within double precision arithmetic limitations """ try: from astropy.coordinates.angle_utilities import angular_separation except ImportError: pass 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: """ Parameters ---------- lon0 : float longitude of first point lat0 : float latitude of first point lon1 : float longitude of second point lat1 : float latitude of second point deg : bool, optional degrees input/output (False: radians in/out) Returns ------- sep_rad : float angular separation Meeus p. 109 from "Astronomical Algorithms" by Jean Meeus Ch. 16 p. 111 (16.5) gives angular distance in degrees between two rightAscension,Declination points in the sky. Neglecting atmospheric effects, of course. Meeus haversine method is stable all the way to exactly 0 deg. either the arrays must be the same size, or one of them must be a scalar """ if deg: lon0 = radians(lon0) lat0 = radians(lat0) lon1 = radians(lon1) lat1 = radians(lat1) sep_rad = 2 * asin( sqrt(haversine(lat0 - lat1) + cos(lat0) * cos(lat1) * haversine(lon0 - lon1)) ) return degrees(sep_rad) if deg else sep_rad def anglesep(lon0: float, lat0: float, lon1: float, lat1: float, deg: bool = True) -> float: """ Parameters ---------- lon0 : float longitude of first point lat0 : float latitude of first point lon1 : float longitude of second point lat1 : float latitude of second point deg : bool, optional degrees input/output (False: radians in/out) Returns ------- sep_rad : float angular separation For reference, this is from astropy astropy/coordinates/angle_utilities.py Angular separation between two points on a sphere. """ if deg: lon0 = radians(lon0) lat0 = radians(lat0) lon1 = radians(lon1) lat1 = radians(lat1) try: sep_rad = angular_separation(lon0, lat0, lon1, lat1) except NameError: sep_rad = anglesep_meeus(lon0, lat0, lon1, lat1, deg=False) return degrees(sep_rad) if deg else sep_rad def haversine(theta: float) -> float: """ Compute haversine Parameters ---------- theta : float angle (radians) Results ------- htheta : float haversine of `theta` https://en.wikipedia.org/wiki/Haversine Meeus p. 111 """ return (1 - cos(theta)) / 2.0 pymap3d-3.1.0/src/pymap3d/latitude.py000066400000000000000000000377511456201517100174400ustar00rootroot00000000000000"""geodetic transforms to auxilary coordinate systems involving latitude""" from __future__ import annotations import warnings from math import pi from . import rcurve from .ellipsoid import Ellipsoid from .mathfun import ( asinh, atan, atanh, cos, degrees, exp, inf, radians, sign, sin, sqrt, tan, ) ELL = Ellipsoid.from_name("wgs84") COS_EPS = 1e-9 # tolerance for angles near abs([90, 270]) __all__ = [ "geodetic2isometric", "isometric2geodetic", "geodetic2rectifying", "rectifying2geodetic", "geodetic2conformal", "conformal2geodetic", "geodetic2parametric", "parametric2geodetic", "geodetic2geocentric", "geocentric2geodetic", "geodetic2authalic", "authalic2geodetic", "geod2geoc", "geoc2geod", ] def geoc2geod( geocentric_lat, geocentric_distance, ell: Ellipsoid = ELL, deg: bool = True, ): """ convert geocentric latitude to geodetic latitude, consider mean sea level altitude like Matlab geoc2geod() Parameters ---------- geocentric_lat : float geocentric latitude geocentric_distance: float distance from planet center, meters (NOT altitude above ground!) ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude References ---------- Long, S.A.T. "General-Altitude Transformation between Geocentric and Geodetic Coordinates. Celestial Mechanics (12), 2, p. 225-230 (1975) doi: 10.1007/BF01230214" """ if deg: geocentric_lat = radians(geocentric_lat) r = geocentric_distance / ell.semimajor_axis geodetic_lat = ( geocentric_lat + (sin(2 * geocentric_lat) / r) * ell.flattening + ((1 / r**2 + 1 / (4 * r)) * sin(4 * geocentric_lat)) * ell.flattening**2 ) return degrees(geodetic_lat) if deg else geodetic_lat def geodetic2geocentric(geodetic_lat, alt_m, ell: Ellipsoid = ELL, deg: bool = True): """ convert geodetic latitude to geocentric latitude on spheroid surface like Matlab geocentricLatitude() with alt_m = 0 like Matlab geod2geoc() Parameters ---------- geodetic_lat : float geodetic latitude alt_m: float altitude above ellipsoid ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geocentric_lat : float geocentric latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: geodetic_lat = radians(geodetic_lat) r = rcurve.transverse(geodetic_lat, ell, deg=False) geocentric_lat = atan((1 - ell.eccentricity**2 * (r / (r + alt_m))) * tan(geodetic_lat)) return degrees(geocentric_lat) if deg else geocentric_lat geod2geoc = geodetic2geocentric def geocentric2geodetic(geocentric_lat, alt_m, ell: Ellipsoid = ELL, deg: bool = True): """ converts from geocentric latitude to geodetic latitude like Matlab geodeticLatitudeFromGeocentric() when alt_m = 0 like Matlab geod2geoc() but with sea level altitude rather than planet center distance Parameters ---------- geocentric_lat : float geocentric latitude alt_m: float altitude above ellipsoid ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: geocentric_lat = radians(geocentric_lat) r = rcurve.transverse(geocentric_lat, ell, deg=False) geodetic_lat = atan(tan(geocentric_lat) / (1 - ell.eccentricity**2 * (r / (r + alt_m)))) return degrees(geodetic_lat) if deg else geodetic_lat def geodetic2isometric(geodetic_lat, ell: Ellipsoid = ELL, deg: bool = True): """ computes isometric latitude on an ellipsoid like Matlab map.geodesy.IsometricLatitudeConverter.forward() Parameters ---------- geodetic_lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- isolat : float isometric latiude Notes ----- Isometric latitude is an auxiliary latitude proportional to the spacing of parallels of latitude on an ellipsoidal mercator projection. Based on Deakin, R.E., 2010, 'The Loxodrome on an Ellipsoid', Lecture Notes, School of Mathematical and Geospatial Sciences, RMIT University, January 2010 """ if deg: geodetic_lat = radians(geodetic_lat) e = ell.eccentricity isometric_lat = asinh(tan(geodetic_lat)) - e * atanh(e * sin(geodetic_lat)) # same results # a1 = e * sin(geodetic_lat) # y = (1 - a1) / (1 + a1) # a2 = pi / 4 + geodetic_lat / 2 # isometric_lat = log(tan(a2) * (y ** (e / 2))) # isometric_lat = log(tan(a2)) + e/2 * log((1-e*sin(geodetic_lat)) / (1+e*sin(geodetic_lat))) coslat = cos(geodetic_lat) i = abs(coslat) <= COS_EPS try: isometric_lat[i] = sign(geodetic_lat[i]) * inf except TypeError: if i: isometric_lat = sign(geodetic_lat) * inf if deg: isometric_lat = degrees(isometric_lat) try: return isometric_lat.squeeze()[()] except AttributeError: return isometric_lat def isometric2geodetic(isometric_lat, ell: Ellipsoid = ELL, deg: bool = True): """ converts from isometric latitude to geodetic latitude like Matlab map.geodesy.IsometricLatitudeConverter.inverse() Parameters ---------- isometric_lat : float isometric latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: isometric_lat = radians(isometric_lat) 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 def conformal2geodetic(conformal_lat, ell: Ellipsoid = ELL, deg: bool = True): """ converts from conformal latitude to geodetic latitude like Matlab map.geodesy.ConformalLatitudeConverter.inverse() Parameters ---------- conformal_lat : float conformal latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: conformal_lat = radians(conformal_lat) e = ell.eccentricity f1 = e**2 / 2 + 5 * e**4 / 24 + e**6 / 12 + 13 * e**8 / 360 f2 = 7 * e**4 / 48 + 29 * e**6 / 240 + 811 * e**8 / 11520 f3 = 7 * e**6 / 120 + 81 * e**8 / 1120 f4 = 4279 * e**8 / 161280 geodetic_lat = ( conformal_lat + f1 * sin(2 * conformal_lat) + f2 * sin(4 * conformal_lat) + f3 * sin(6 * conformal_lat) + f4 * sin(8 * conformal_lat) ) return degrees(geodetic_lat) if deg else geodetic_lat def geodetic2conformal(geodetic_lat, ell: Ellipsoid = ELL, deg: bool = True): """ converts from geodetic latitude to conformal latitude like Matlab map.geodesy.ConformalLatitudeConverter.forward() Parameters ---------- geodetic_lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- conformal_lat : float conformal latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: geodetic_lat = radians(geodetic_lat) e = ell.eccentricity f1 = 1 - e * sin(geodetic_lat) f2 = 1 + e * sin(geodetic_lat) f3 = 1 - sin(geodetic_lat) f4 = 1 + sin(geodetic_lat) # compute conformal latitudes with correction for points at +90 try: with warnings.catch_warnings(record=True): warnings.simplefilter("error") conformal_lat = 2 * atan(sqrt((f4 / f3) * ((f1 / f2) ** e))) - (pi / 2) except (ArithmeticError, RuntimeWarning): conformal_lat = pi / 2 return degrees(conformal_lat) if deg else conformal_lat # %% rectifying def geodetic2rectifying(geodetic_lat, ell: Ellipsoid = ELL, deg: bool = True): """ converts from geodetic latitude to rectifying latitude like Matlab map.geodesy.RectifyingLatitudeConverter.forward() Parameters ---------- geodetic_lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- rectifying_lat : float rectifying latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: geodetic_lat = radians(geodetic_lat) n = ell.thirdflattening f1 = 3 * n / 2 - 9 * n**3 / 16 f2 = 15 * n**2 / 16 - 15 * n**4 / 32 f3 = 35 * n**3 / 48 f4 = 315 * n**4 / 512 rectifying_lat = ( geodetic_lat - f1 * sin(2 * geodetic_lat) + f2 * sin(4 * geodetic_lat) - f3 * sin(6 * geodetic_lat) + f4 * sin(8 * geodetic_lat) ) return degrees(rectifying_lat) if deg else rectifying_lat def rectifying2geodetic(rectifying_lat, ell: Ellipsoid = ELL, deg: bool = True): """ converts from rectifying latitude to geodetic latitude like Matlab map.geodesy.RectifyingLatitudeConverter.inverse() Parameters ---------- rectifying_lat : float latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: rectifying_lat = radians(rectifying_lat) n = ell.thirdflattening f1 = 3 * n / 2 - 27 * n**3 / 32 f2 = 21 * n**2 / 16 - 55 * n**4 / 32 f3 = 151 * n**3 / 96 f4 = 1097 * n**4 / 512 geodetic_lat = ( rectifying_lat + f1 * sin(2 * rectifying_lat) + f2 * sin(4 * rectifying_lat) + f3 * sin(6 * rectifying_lat) + f4 * sin(8 * rectifying_lat) ) return degrees(geodetic_lat) if deg else geodetic_lat # %% authalic def geodetic2authalic(geodetic_lat, ell: Ellipsoid = ELL, deg: bool = True): """ converts from geodetic latitude to authalic latitude like Matlab map.geodesy.AuthalicLatitudeConverter.forward() Parameters ---------- geodetic_lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- authalic_lat : float authalic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: geodetic_lat = radians(geodetic_lat) e = ell.eccentricity f1 = e**2 / 3 + 31 * e**4 / 180 + 59 * e**6 / 560 f2 = 17 * e**4 / 360 + 61 * e**6 / 1260 f3 = 383 * e**6 / 45360 authalic_lat = ( geodetic_lat - f1 * sin(2 * geodetic_lat) + f2 * sin(4 * geodetic_lat) - f3 * sin(6 * geodetic_lat) ) return degrees(authalic_lat) if deg else authalic_lat def authalic2geodetic(authalic_lat, ell: Ellipsoid = ELL, deg: bool = True): """ converts from authalic latitude to geodetic latitude like Matlab map.geodesy.AuthalicLatitudeConverter.inverse() Parameters ---------- authalic_lat : float latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: authalic_lat = radians(authalic_lat) e = ell.eccentricity f1 = e**2 / 3 + 31 * e**4 / 180 + 517 * e**6 / 5040 f2 = 23 * e**4 / 360 + 251 * e**6 / 3780 f3 = 761 * e**6 / 45360 geodetic_lat = ( authalic_lat + f1 * sin(2 * authalic_lat) + f2 * sin(4 * authalic_lat) + f3 * sin(6 * authalic_lat) ) return degrees(geodetic_lat) if deg else geodetic_lat # %% parametric def geodetic2parametric(geodetic_lat, ell: Ellipsoid = ELL, deg: bool = True): """ converts from geodetic latitude to parametric latitude like Matlab parametriclatitude() Parameters ---------- geodetic_lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- parametric_lat : float parametric latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: geodetic_lat = radians(geodetic_lat) parametric_lat = atan(sqrt(1 - (ell.eccentricity) ** 2) * tan(geodetic_lat)) return degrees(parametric_lat) if deg else parametric_lat def parametric2geodetic(parametric_lat, ell: Ellipsoid = ELL, deg: bool = True): """ converts from parametric latitude to geodetic latitude like Matlab geodeticLatitudeFromParametric() Parameters ---------- parametric_lat : float latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ if deg: parametric_lat = radians(parametric_lat) geodetic_lat = atan(tan(parametric_lat) / sqrt(1 - (ell.eccentricity) ** 2)) return degrees(geodetic_lat) if deg else geodetic_lat pymap3d-3.1.0/src/pymap3d/los.py000066400000000000000000000070751456201517100164160ustar00rootroot00000000000000""" Line of sight intersection of space observer to ellipsoid """ from __future__ import annotations from math import nan, pi try: from numpy import asarray except ImportError: pass from .aer import aer2enu from .ecef import ecef2geodetic, enu2uvw, geodetic2ecef from .ellipsoid import Ellipsoid from .mathfun import sqrt __all__ = ["lookAtSpheroid"] ELL = Ellipsoid.from_name("wgs84") def lookAtSpheroid( lat0, lon0, h0, az, tilt, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ Calculates line-of-sight intersection with Earth (or other ellipsoid) surface from above surface / orbit Parameters ---------- lat0 : float observer geodetic latitude lon0 : float observer geodetic longitude h0 : float observer altitude (meters) Must be non-negative since this function doesn't consider terrain az : float azimuth angle of line-of-sight, clockwise from North tilt : float tilt angle of line-of-sight with respect to local vertical (nadir = 0) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- lat : float geodetic latitude where the line-of-sight intersects with the Earth ellipsoid lon : float geodetic longitude where the line-of-sight intersects with the Earth ellipsoid d : float slant range (meters) from starting point to intersect point Values will be NaN if the line of sight does not intersect. Algorithm based on https://medium.com/@stephenhartzell/satellite-line-of-sight-intersection-with-earth-d786b4a6a9b6 Stephen Hartzell """ if ell is None: ell = ELL try: lat0 = asarray(lat0) lon0 = asarray(lon0) h0 = asarray(h0) az = asarray(az) tilt = asarray(tilt) if (h0 < 0).any(): raise ValueError("Intersection calculation requires altitude [0, Infinity)") except NameError: if h0 < 0: raise ValueError("Intersection calculation requires altitude [0, Infinity)") 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) # 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) value = -(a**2) * b**2 * w * z - a**2 * c**2 * v * y - b**2 * c**2 * u * x radical = ( a**2 * b**2 * w**2 + a**2 * c**2 * v**2 - a**2 * v**2 * z**2 + 2 * a**2 * v * w * y * z - a**2 * w**2 * y**2 + b**2 * c**2 * u**2 - b**2 * u**2 * z**2 + 2 * b**2 * u * w * x * z - b**2 * w**2 * x**2 - c**2 * u**2 * y**2 + 2 * c**2 * u * v * x * y - c**2 * v**2 * x**2 ) magnitude = a**2 * b**2 * w**2 + a**2 * c**2 * v**2 + b**2 * c**2 * u**2 # %% Return nan if radical < 0 or d < 0 because LOS vector does not point towards Earth try: radical[radical < 0] = nan except TypeError: if radical < 0: radical = nan d = (value - a * b * c * sqrt(radical)) / magnitude try: d[d < 0] = nan except TypeError: if d < 0: d = nan # %% cartesian to ellipsodal lat, lon, _ = ecef2geodetic(x + d * u, y + d * v, z + d * w, deg=deg) try: return lat.squeeze()[()], lon.squeeze()[()], d.squeeze()[()] except AttributeError: return lat, lon, d pymap3d-3.1.0/src/pymap3d/lox.py000066400000000000000000000207641456201517100164230ustar00rootroot00000000000000""" isometric latitude, meridian distance """ from __future__ import annotations try: from numpy import array, broadcast_arrays except ImportError: pass from math import pi, tau from . import rcurve, rsphere from .ellipsoid import Ellipsoid from .latitude import ( authalic2geodetic, geodetic2authalic, geodetic2isometric, geodetic2rectifying, rectifying2geodetic, ) from .mathfun import atan2, cos, degrees, radians, sign, tan from .utils import cart2sph, sph2cart __all__ = [ "loxodrome_inverse", "loxodrome_direct", "meridian_arc", "meridian_dist", "departure", "meanm", ] ELL = Ellipsoid.from_name("wgs84") COS_EPS = 1e-9 def meridian_dist(lat, ell: Ellipsoid = ELL, deg: bool = True) -> float: """ Computes the ground distance on an ellipsoid from the equator to the input latitude. Parameters ---------- lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Results ------- dist : float distance (meters) """ return meridian_arc(0.0, lat, ell, deg) def meridian_arc(lat1, lat2, ell: Ellipsoid = ELL, deg: bool = True) -> float: """ Computes the ground distance on an ellipsoid between two latitudes. Parameters ---------- lat1, lat2 : float geodetic latitudes ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Results ------- dist : float distance (meters) """ if deg: lat1, lat2 = radians(lat1), radians(lat2) rlat1 = geodetic2rectifying(lat1, ell, deg=False) rlat2 = geodetic2rectifying(lat2, ell, deg=False) return rsphere.rectifying(ell) * abs(rlat2 - rlat1) def loxodrome_inverse( lat1, lon1, lat2, lon2, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple[float, float]: """ computes the arc length and azimuth of the loxodrome between two points on the surface of the reference ellipsoid like Matlab distance('rh',...) and azimuth('rh',...) Parameters ---------- lat1 : float geodetic latitude of first point lon1 : float geodetic longitude of first point lat2 : float geodetic latitude of second point lon2 : float geodetic longitude of second point ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Results ------- lox_s : float distance along loxodrome (meters) az12 : float azimuth of loxodrome (degrees/radians) Based on Deakin, R.E., 2010, 'The Loxodrome on an Ellipsoid', Lecture Notes, School of Mathematical and Geospatial Sciences, RMIT University, January 2010 [1] Bowring, B.R., 1985, 'The geometry of the loxodrome on the ellipsoid', The Canadian Surveyor, Vol. 39, No. 3, Autumn 1985, pp.223-230. [2] Snyder, J.P., 1987, Map Projections-A Working Manual. U.S. Geological Survey Professional Paper 1395. Washington, DC: U.S. Government Printing Office, pp.15-16 and pp. 44-45. [3] Thomas, P.D., 1952, Conformal Projections in Geodesy and Cartography, Special Publication No. 251, Coast and Geodetic Survey, U.S. Department of Commerce, Washington, DC: U.S. Government Printing Office, p. 66. """ if deg: lat1, lon1, lat2, lon2 = radians(lat1), radians(lon1), radians(lat2), radians(lon2) try: lat1, lon1, lat2, lon2 = broadcast_arrays(lat1, lon1, lat2, lon2) except NameError: pass # compute changes in isometric latitude and longitude between points disolat = geodetic2isometric(lat2, deg=False, ell=ell) - geodetic2isometric( lat1, deg=False, ell=ell ) dlon = lon2 - lon1 # compute azimuth az12 = atan2(dlon, disolat) aux = abs(cos(az12)) # compute distance along loxodromic curve dist = meridian_arc(lat2, lat1, deg=False, ell=ell) / aux # straight east or west i = aux < COS_EPS try: dist[i] = departure(lon2[i], lon1[i], lat1[i], ell, deg=False) except (AttributeError, TypeError): if i: dist = departure(lon2, lon1, lat1, ell, deg=False) if deg: az12 = degrees(az12) % 360.0 try: return dist.squeeze()[()], az12.squeeze()[()] except AttributeError: return dist, az12 def loxodrome_direct( lat1, lon1, rng, a12, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ Given starting lat, lon with arclength and azimuth, compute final lat, lon like Matlab reckon('rh', ...) except that "rng" in meters instead of "arclen" degrees of arc Parameters ---------- lat1 : float inital geodetic latitude (degrees) lon1 : float initial geodetic longitude (degrees) rng : float ground distance (meters) a12 : float azimuth (degrees) clockwide from north. ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- lat2 : float final geodetic latitude (degrees) lon2 : float final geodetic longitude (degrees) """ if deg: lat1, lon1, a12 = radians(lat1), radians(lon1), radians(a12) a12 = a12 % tau try: lat1, rng, a12 = broadcast_arrays(lat1, rng, a12) if (abs(lat1) > pi / 2).any(): raise ValueError("-90 <= latitude <= 90") if (rng < 0).any(): raise ValueError("ground distance must be >= 0") except NameError: if abs(lat1) > pi / 2: raise ValueError("-90 <= latitude <= 90") if rng < 0: raise ValueError("ground distance must be >= 0") # compute rectifying sphere latitude and radius reclat = geodetic2rectifying(lat1, ell, deg=False) # compute the new points cosaz = cos(a12) lat2 = reclat + (rng / rsphere.rectifying(ell)) * cosaz # compute rectifying latitude lat2 = rectifying2geodetic(lat2, ell, deg=False) # transform to geodetic latitude newiso = geodetic2isometric(lat2, ell, deg=False) iso = geodetic2isometric(lat1, ell, deg=False) # stability near singularities i = abs(cos(a12)) < COS_EPS dlon = tan(a12) * (newiso - iso) try: dlon[i] = sign(pi - a12[i]) * rng[i] / rcurve.parallel(lat1[i], ell=ell, deg=False) except (AttributeError, TypeError): if i: # straight east or west dlon = sign(pi - a12) * rng / rcurve.parallel(lat1, ell=ell, deg=False) lon2 = lon1 + dlon if deg: lat2, lon2 = degrees(lat2), degrees(lon2) try: return lat2.squeeze()[()], lon2.squeeze()[()] except AttributeError: return lat2, lon2 def departure(lon1, lon2, lat, ell: Ellipsoid = ELL, deg: bool = True) -> float: """ Computes the distance along a specific parallel between two meridians. like Matlab departure() Parameters ---------- lon1, lon2 : float geodetic longitudes (degrees) lat : float geodetic latitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- dist: float ground distance (meters) """ if deg: lon1, lon2, lat = radians(lon1), radians(lon2), radians(lat) return rcurve.parallel(lat, ell=ell, deg=False) * (abs(lon2 - lon1) % pi) def meanm(lat, lon, ell: Ellipsoid = ELL, deg: bool = True) -> tuple: """ Computes geographic mean for geographic points on an ellipsoid like Matlab meanm() Parameters ---------- lat : sequence of float geodetic latitude (degrees) lon : sequence of float geodetic longitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- latbar, lonbar: float geographic mean latitude, longitude """ if deg: lat, lon = radians(lat), radians(lon) lat = geodetic2authalic(lat, ell, deg=False) x, y, z = sph2cart(lon, lat, array(1.0)) lonbar, latbar, _ = cart2sph(x.sum(), y.sum(), z.sum()) latbar = authalic2geodetic(latbar, ell, deg=False) if deg: latbar, lonbar = degrees(latbar), degrees(lonbar) return latbar, lonbar pymap3d-3.1.0/src/pymap3d/mathfun.py000066400000000000000000000035361456201517100172610ustar00rootroot00000000000000""" import from Numpy, and if not available fallback to math stdlib """ from __future__ import annotations try: from numpy import arcsin as asin from numpy import arcsinh as asinh from numpy import arctan as atan from numpy import arctan2 as atan2 from numpy import arctanh as atanh from numpy import ( cbrt, cos, degrees, exp, hypot, inf, isclose, isnan, linspace, log, power, radians, sign, sin, sqrt, tan, ) except ImportError: from math import ( # type: ignore asin, asinh, atan, atan2, atanh, cos, degrees, exp, hypot, inf, isclose, isnan, log, radians, sin, sqrt, tan, ) def linspace(start: float, stop: float, num: int) -> list[float]: # type: ignore """ create a list of "num" evenly spaced numbers using range and increment, including endpoint "stop" """ step = (stop - start) / (num - 1) return [start + i * step for i in range(num)] def power(x, y): # type: ignore return pow(x, y) def sign(x) -> float: # type: ignore """signum""" if x < 0: y = -1.0 elif x > 0: y = 1.0 else: y = 0.0 return y try: import math.cbrt as cbrt # type: ignore except ImportError: def cbrt(x) -> float: # type: ignore return x ** (1 / 3) __all__ = [ "asin", "asinh", "atan", "atan2", "atanh", "cbrt", "cos", "degrees", "exp", "hypot", "inf", "isclose", "isnan", "log", "power", "radians", "sign", "sin", "sqrt", "tan", ] pymap3d-3.1.0/src/pymap3d/ned.py000066400000000000000000000143121456201517100163570ustar00rootroot00000000000000""" Transforms involving NED North East Down """ from __future__ import annotations from .ecef import ecef2enu, ecef2enuv, ecef2geodetic, enu2ecef from .ellipsoid import Ellipsoid from .enu import aer2enu, enu2aer, geodetic2enu __all__ = [ "aer2ned", "ned2aer", "ned2geodetic", "ned2ecef", "ecef2ned", "geodetic2ned", "ecef2nedv", ] ELL = Ellipsoid.from_name("wgs84") def aer2ned(az, elev, slantRange, deg: bool = True) -> tuple: """ converts azimuth, elevation, range to target from observer to North, East, Down Parameters ----------- az : float azimuth elev : float elevation slantRange : float slant range [meters] deg : bool, optional degrees input/output (False: radians in/out) Results ------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) """ e, n, u = aer2enu(az, elev, slantRange, deg=deg) return n, e, -u def ned2aer(n, e, d, deg: bool = True) -> tuple: """ converts North, East, Down to azimuth, elevation, range Parameters ---------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) deg : bool, optional degrees input/output (False: radians in/out) Results ------- az : float azimuth elev : float elevation slantRange : float slant range [meters] """ return enu2aer(e, n, -d, deg=deg) def ned2geodetic( n, e, d, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ Converts North, East, Down to target latitude, longitude, altitude Parameters ---------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- lat : float target geodetic latitude lon : float target geodetic longitude h : float target altitude above geodetic ellipsoid (meters) """ x, y, z = enu2ecef(e, n, -d, lat0, lon0, h0, ell, deg=deg) return ecef2geodetic(x, y, z, ell, deg=deg) def ned2ecef( n, e, d, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ North, East, Down to target ECEF coordinates Parameters ---------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) """ return enu2ecef(e, n, -d, lat0, lon0, h0, ell, deg=deg) def ecef2ned( x, y, z, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ Convert ECEF x,y,z to North, East, Down Parameters ---------- x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) """ e, n, u = ecef2enu(x, y, z, lat0, lon0, h0, ell, deg=deg) return n, e, -u def geodetic2ned( lat, lon, h, lat0, lon0, h0, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ convert latitude, longitude, altitude of target to North, East, Down from observer Parameters ---------- lat : float target geodetic latitude lon : float target geodetic longitude h : float target altitude above geodetic ellipsoid (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) """ e, n, u = geodetic2enu(lat, lon, h, lat0, lon0, h0, ell, deg=deg) return n, e, -u def ecef2nedv(x, y, z, lat0, lon0, deg: bool = True) -> tuple[float, float, float]: """ for VECTOR between two points Parameters ---------- x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude deg : bool, optional degrees input/output (False: radians in/out) Results ------- (Vector) n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) """ e, n, u = ecef2enuv(x, y, z, lat0, lon0, deg=deg) return n, e, -u pymap3d-3.1.0/src/pymap3d/rcurve.py000066400000000000000000000053601456201517100171220ustar00rootroot00000000000000"""compute radii of curvature for an ellipsoid""" from __future__ import annotations from .ellipsoid import Ellipsoid from .mathfun import cos, sin, sqrt, radians __all__ = ["parallel", "meridian", "transverse", "geocentric_radius"] ELL = Ellipsoid.from_name("wgs84") def geocentric_radius(geodetic_lat, ell: Ellipsoid = ELL, deg: bool = True): """ compute geocentric radius at geodetic latitude https://en.wikipedia.org/wiki/Earth_radius#Geocentric_radius """ if deg: geodetic_lat = radians(geodetic_lat) return sqrt( ( (ell.semimajor_axis**2 * cos(geodetic_lat)) ** 2 + (ell.semiminor_axis**2 * sin(geodetic_lat)) ** 2 ) / ( (ell.semimajor_axis * cos(geodetic_lat)) ** 2 + (ell.semiminor_axis * sin(geodetic_lat)) ** 2 ) ) def parallel(lat, ell: Ellipsoid = ELL, deg: bool = True) -> float: """ computes the radius of the small circle encompassing the globe at the specified latitude like Matlab rcurve('parallel', ...) Parameters ---------- lat : float geodetic latitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- radius: float radius of ellipsoid (meters) """ if deg: lat = radians(lat) return cos(lat) * transverse(lat, ell, deg=False) def meridian(lat, ell: Ellipsoid = ELL, deg: bool = True): """computes the meridional radius of curvature for the ellipsoid like Matlab rcurve('meridian', ...) Parameters ---------- lat : float geodetic latitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- radius: float radius of ellipsoid """ if deg: lat = radians(lat) f1 = ell.semimajor_axis * (1 - ell.eccentricity**2) f2 = 1 - (ell.eccentricity * sin(lat)) ** 2 return f1 / sqrt(f2**3) def transverse(lat, ell: Ellipsoid = ELL, deg: bool = True): """computes the radius of the curve formed by a plane intersecting the ellipsoid at the latitude which is normal to the surface of the ellipsoid like Matlab rcurve('transverse', ...) Parameters ---------- lat : float latitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- radius: float radius of ellipsoid (meters) """ if deg: lat = radians(lat) return ell.semimajor_axis / sqrt(1 - (ell.eccentricity * sin(lat)) ** 2) pymap3d-3.1.0/src/pymap3d/rsphere.py000066400000000000000000000120011456201517100172520ustar00rootroot00000000000000""" compute radii of auxiliary spheres""" from __future__ import annotations try: from numpy import asarray except ImportError: pass from . import rcurve from .ellipsoid import Ellipsoid from .mathfun import cos, degrees, log, radians, sin, sqrt from .vincenty import vdist __all__ = [ "eqavol", "authalic", "rectifying", "euler", "curve", "triaxial", "biaxial", ] ELL = Ellipsoid.from_name("wgs84") def eqavol(ell: Ellipsoid = ELL) -> float: """computes the radius of the sphere with equal volume as the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid Returns ------- radius: float radius of sphere """ f = ell.flattening return ell.semimajor_axis * (1 - f / 3 - f**2 / 9) def authalic(ell: Ellipsoid = ELL) -> float: """computes the radius of the sphere with equal surface area as the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid Returns ------- radius: float radius of sphere """ e = ell.eccentricity if e > 0: f1 = ell.semimajor_axis**2 / 2 f2 = (1 - e**2) / (2 * e) f3 = log((1 + e) / (1 - e)) return sqrt(f1 * (1 + f2 * f3)) else: return ell.semimajor_axis def rectifying(ell: Ellipsoid = ELL) -> float: """computes the radius of the sphere with equal meridional distances as the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid Returns ------- radius: float radius of sphere """ return ((ell.semimajor_axis ** (3 / 2) + ell.semiminor_axis ** (3 / 2)) / 2) ** (2 / 3) def euler( lat1, lon1, lat2, lon2, ell: Ellipsoid = ELL, deg: bool = True, ): """computes the Euler radii of curvature at the midpoint of the great circle arc defined by the endpoints (lat1,lon1) and (lat2,lon2) Parameters ---------- lat1, lat2 : float geodetic latitudes (degrees) lon1, lon2 : float geodetic longitudes (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- radius: float radius of sphere """ if not deg: lat1, lon1, lat2, lon2 = degrees(lat1), degrees(lon1), degrees(lat2), degrees(lon2) try: lat1, lat2 = asarray(lat1), asarray(lat2) except NameError: pass latmid = lat1 + (lat2 - lat1) / 2 # compute the midpoint # compute azimuth az = vdist(lat1, lon1, lat2, lon2, ell=ell)[1] # compute meridional and transverse radii of curvature rho = rcurve.meridian(latmid, ell, deg=True) nu = rcurve.transverse(latmid, ell, deg=True) az = radians(az) den = rho * sin(az) ** 2 + nu * cos(az) ** 2 # compute radius of the arc from point 1 to point 2 return rho * nu / den def curve(lat, ell: Ellipsoid = ELL, deg: bool = True, method: str = "mean"): """computes the arithmetic average of the transverse and meridional radii of curvature at a specified latitude point Parameters ---------- lat : float geodetic latitudes (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) method: str, optional "mean" or "norm" Returns ------- radius: float radius of sphere """ if deg: lat = radians(lat) rho = rcurve.meridian(lat, ell, deg=False) nu = rcurve.transverse(lat, ell, deg=False) if method == "mean": return (rho + nu) / 2 elif method == "norm": return sqrt(rho * nu) else: raise ValueError("method must be mean or norm") def triaxial(ell: Ellipsoid = ELL, method: str = "mean") -> float: """computes triaxial average of the semimajor and semiminor axes of the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid method: str, optional "mean" or "norm" Returns ------- radius: float radius of sphere """ 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) else: raise ValueError("method must be mean or norm") def biaxial(ell: Ellipsoid = ELL, method: str = "mean") -> float: """computes biaxial average of the semimajor and semiminor axes of the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid method: str, optional "mean" or "norm" Returns ------- radius: float radius of sphere """ if method == "mean": return (ell.semimajor_axis + ell.semiminor_axis) / 2 elif method == "norm": return sqrt(ell.semimajor_axis * ell.semiminor_axis) else: raise ValueError("method must be mean or norm") pymap3d-3.1.0/src/pymap3d/sidereal.py000066400000000000000000000055641456201517100174120ustar00rootroot00000000000000# Copyright (c) 2014-2018 Michael Hirsch, Ph.D. """ manipulations of sidereal time """ from datetime import datetime from math import tau from .timeconv import str2dt try: import astropy.units as u from astropy.coordinates import Longitude from astropy.time import Time except ImportError: pass __all__ = ["datetime2sidereal", "juliandate", "greenwichsrt"] def datetime2sidereal(time: datetime, lon_radians: float) -> float: """ Convert ``datetime`` to local sidereal time from D. Vallado "Fundamentals of Astrodynamics and Applications" time : datetime.datetime time to convert lon_radians : float longitude (radians) Results ------- tsr : float Local sidereal time """ if isinstance(time, (tuple, list)): return [datetime2sidereal(t, lon_radians) for t in time] try: tsr = ( Time(time) .sidereal_time(kind="apparent", longitude=Longitude(lon_radians, unit=u.radian)) .radian ) except NameError: jd = juliandate(str2dt(time)) # %% Greenwich Sidereal time RADIANS gst = greenwichsrt(jd) # %% Algorithm 15 p. 188 rotate GST to LOCAL SIDEREAL TIME tsr = gst + lon_radians return tsr def juliandate(time: datetime) -> float: """ Python datetime to Julian time (days since Jan 1, 4713 BCE) from D.Vallado Fundamentals of Astrodynamics and Applications p.187 and J. Meeus Astronomical Algorithms 1991 Eqn. 7.1 pg. 61 Parameters ---------- time : datetime.datetime time to convert Results ------- jd : float Julian date (days since Jan 1, 4713 BCE) """ if isinstance(time, (tuple, list)): return list(map(juliandate, time)) if time.month < 3: year = time.year - 1 month = time.month + 12 else: year = time.year month = time.month A = int(year / 100.0) B = 2 - A + int(A / 4.0) C = (((time.second + time.microsecond / 1e6) / 60.0 + time.minute) / 60.0 + time.hour) / 24.0 return int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + time.day + B - 1524.5 + C def greenwichsrt(Jdate: float) -> float: """ Convert Julian time to sidereal time D. Vallado Ed. 4 Parameters ---------- Jdate: float Julian date (since Jan 1, 4713 BCE) Results ------- tsr : float Sidereal time """ if isinstance(Jdate, (tuple, list)): return list(map(greenwichsrt, Jdate)) # %% Vallado Eq. 3-42 p. 184, Seidelmann 3.311-1 tUT1 = (Jdate - 2451545.0) / 36525.0 # Eqn. 3-47 p. 188 gmst_sec = ( 67310.54841 + (876600 * 3600 + 8640184.812866) * tUT1 + 0.093104 * tUT1**2 - 6.2e-6 * tUT1**3 ) # 1/86400 and %(2*pi) implied by units of radians return gmst_sec * tau / 86400.0 % tau pymap3d-3.1.0/src/pymap3d/spherical.py000066400000000000000000000077741456201517100176010ustar00rootroot00000000000000""" Transformation of 3D coordinates between geocentric geodetic (latitude, longitude, height) and geocentric spherical (spherical latitude, longitude, radius). """ from __future__ import annotations from .ellipsoid import Ellipsoid from .mathfun import asin, atan2, cbrt, degrees, hypot, power, radians, sin, sqrt __all__ = [ "geodetic2spherical", "spherical2geodetic", ] ELL = Ellipsoid.from_name("wgs84") def geodetic2spherical( lat, lon, alt, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ point transformation from Geodetic of specified ellipsoid (default WGS-84) to geocentric spherical of the same ellipsoid Parameters ---------- lat target geodetic latitude lon target geodetic longitude alt target altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- Geocentric spherical (spherical latitude, longitude, radius lat target spherical latitude lon target longitude radius target distance to the geocenter (meters) based on: Vermeille, H., 2002. Direct transformation from geocentric coordinates to geodetic coordinates. Journal of Geodesy. 76. 451-454. doi:10.1007/s00190-002-0273-6 """ if deg: lat = radians(lat) lon = radians(lon) # Pre-compute to avoid repeated trigonometric functions sinlat = sin(lat) coslat = sqrt(1 - sinlat**2) # radius of curvature of the prime vertical section N = ell.semimajor_axis**2 / hypot( ell.semimajor_axis * coslat, ell.semiminor_axis * sinlat, ) # Instead of computing X and Y, we only compute the projection on the XY # plane: xy_projection = sqrt( X**2 + Y**2 ) xy_projection = (alt + N) * coslat z_cartesian = (alt + (1 - ell.eccentricity**2) * N) * sinlat radius = hypot(xy_projection, z_cartesian) slat = asin(z_cartesian / radius) if deg: slat = degrees(slat) lon = degrees(lon) return slat, lon, radius def spherical2geodetic( lat, lon, radius, ell: Ellipsoid = ELL, deg: bool = True, ) -> tuple: """ point transformation from geocentric spherical of specified ellipsoid (default WGS-84) to geodetic of the same ellipsoid Parameters ---------- lat target spherical latitude lon target longitude radius target distance to the geocenter (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- lat target geodetic latitude lon target geodetic longitude alt target altitude above geodetic ellipsoid (meters) based on: Vermeille, H., 2002. Direct transformation from geocentric coordinates to geodetic coordinates. Journal of Geodesy. 76. 451-454. doi:10.1007/s00190-002-0273-6 """ if deg: lat = radians(lat) lon = radians(lon) # Pre-compute to avoid repeated trigonometric functions sinlat = sin(lat) coslat = sqrt(1 - sinlat**2) Z = radius * sinlat p_0 = power(radius, 2) * coslat**2 / ell.semimajor_axis**2 q_0 = (1 - ell.eccentricity**2) / ell.semimajor_axis**2 * Z**2 r_0 = (p_0 + q_0 - ell.eccentricity**4) / 6 s_0 = ell.eccentricity**4 * p_0 * q_0 / 4 / r_0**3 t_0 = cbrt(1 + s_0 + sqrt(2 * s_0 + s_0**2)) u_0 = r_0 * (1 + t_0 + 1 / t_0) v_0 = sqrt(u_0**2 + q_0 * ell.eccentricity**4) w_0 = ell.eccentricity**2 * (u_0 + v_0 - q_0) / 2 / v_0 k = sqrt(u_0 + v_0 + w_0**2) - w_0 D = k * radius * coslat / (k + ell.eccentricity**2) hypotDZ = hypot(D, Z) glat = 2 * atan2(Z, (D + hypotDZ)) alt = (k + ell.eccentricity**2 - 1) / k * hypotDZ if deg: glat = degrees(glat) lon = degrees(lon) return glat, lon, alt pymap3d-3.1.0/src/pymap3d/tests/000077500000000000000000000000001456201517100164005ustar00rootroot00000000000000pymap3d-3.1.0/src/pymap3d/tests/__init__.py000066400000000000000000000000001456201517100204770ustar00rootroot00000000000000pymap3d-3.1.0/src/pymap3d/tests/matlab_engine.py000066400000000000000000000017311456201517100215410ustar00rootroot00000000000000import functools from pathlib import Path from datetime import datetime import matlab.engine @functools.cache def matlab_engine(): """ only cached because used by Pytest in multiple tests """ cwd = Path(__file__).parent eng = matlab.engine.start_matlab("-nojvm") eng.addpath(eng.genpath(str(cwd)), nargout=0) return eng def pydt2matdt(eng, utc: datetime): """ Python datetime.dateime to Matlab datetime """ return eng.datetime(utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second) @functools.cache def has_matmap3d(eng) -> bool: cwd = Path(__file__).parent d = cwd.parents[3] / "matmap3d" print(f"Looking in {d} for matmap3d") if d.is_dir(): eng.addpath(str(d), nargout=0) return True return False @functools.cache def has_aerospace(eng) -> bool: return eng.matlab_toolbox()["aerospace"] @functools.cache def has_mapping(eng) -> bool: return eng.matlab_toolbox()["mapping"] pymap3d-3.1.0/src/pymap3d/tests/matlab_toolbox.m000066400000000000000000000007771456201517100215770ustar00rootroot00000000000000function h = matlab_toolbox() h = struct(mapping=has_mapping(), aerospace=has_aerospace()); end function has_map = has_mapping() if usejava('jvm') addons = matlab.addons.installedAddons(); has_map = any(contains(addons.Name, 'Mapping Toolbox')); else has_map = ~isempty(ver("map")); end end function has_map = has_aerospace() if usejava('jvm') addons = matlab.addons.installedAddons(); has_map = any(contains(addons.Name, 'Aerospace Toolbox')); else has_map = ~isempty(ver("aero")); end end pymap3d-3.1.0/src/pymap3d/tests/test_aer.py000066400000000000000000000056051456201517100205660ustar00rootroot00000000000000from math import radians import pymap3d as pm import pytest from pytest import approx ELL = pm.Ellipsoid.from_name("wgs84") A = ELL.semimajor_axis B = ELL.semiminor_axis @pytest.mark.parametrize( "aer,lla,xyz", [((33, 70, 1000), (42, -82, 200), (660930.2, -4701424.0, 4246579.6))] ) def test_aer2ecef(aer, lla, xyz): # degrees xyz1 = pm.aer2ecef(*aer, *lla) assert xyz1 == approx(xyz) assert all(isinstance(n, float) for n in xyz1) # float includes np.float64 i.e. a scalar # radians raer = (radians(aer[0]), radians(aer[1]), aer[2]) rlla = (radians(lla[0]), radians(lla[1]), lla[2]) xyz1 = pm.aer2ecef(*raer, *rlla, deg=False) assert xyz1 == approx(xyz) assert all(isinstance(n, float) for n in xyz1) # bad input with pytest.raises(ValueError): pm.aer2ecef(aer[0], aer[1], -1, *lla) @pytest.mark.parametrize( "xyz, lla, aer", [ ((A - 1, 0, 0), (0, 0, 0), (0, -90, 1)), ((-A + 1, 0, 0), (0, 180, 0), (0, -90, 1)), ((0, A - 1, 0), (0, 90, 0), (0, -90, 1)), ((0, -A + 1, 0), (0, -90, 0), (0, -90, 1)), ((0, 0, B - 1), (90, 0, 0), (0, -90, 1)), ((0, 0, -B + 1), (-90, 0, 0), (0, -90, 1)), ((660930.19276, -4701424.22296, 4246579.60463), (42, -82, 200), (33, 70, 1000)), ], ) def test_ecef2aer(xyz, lla, aer): # degrees aer1 = pm.ecef2aer(*xyz, *lla) assert aer1 == approx(aer) assert all(isinstance(n, float) for n in aer1) # radians rlla = (radians(lla[0]), radians(lla[1]), lla[2]) raer = (radians(aer[0]), radians(aer[1]), aer[2]) aer1 = pm.ecef2aer(*xyz, *rlla, deg=False) assert aer1 == approx(raer) assert all(isinstance(n, float) for n in aer1) @pytest.mark.parametrize("aer,enu", [((33, 70, 1000), (186.2775, 286.8422, 939.6926))]) def test_aer_enu(aer, enu): # degrees enu1 = pm.aer2enu(*aer) assert enu1 == approx(enu) assert all(isinstance(n, float) for n in enu1) # radians raer = (radians(aer[0]), radians(aer[1]), aer[2]) enu1 = pm.aer2enu(*raer, deg=False) assert enu1 == approx(enu) assert all(isinstance(n, float) for n in enu1) # bad input with pytest.raises(ValueError): pm.aer2enu(aer[0], aer[1], -1) # degrees aer1 = pm.enu2aer(*enu) assert aer1 == approx(aer) assert all(isinstance(n, float) for n in aer1) # radians aer1 = pm.enu2aer(*enu, deg=False) assert aer1 == approx(raer) assert all(isinstance(n, float) for n in aer1) @pytest.mark.parametrize("aer,ned", [((33, 70, 1000), (286.8422, 186.2775, -939.6926))]) def test_aer_ned(aer, ned): ned1 = pm.aer2ned(*aer) assert ned1 == approx(ned) assert all(isinstance(n, float) for n in ned1) # bad value with pytest.raises(ValueError): pm.aer2ned(aer[0], aer[1], -1) aer1 = pm.ned2aer(*ned) assert aer1 == approx(aer) assert all(isinstance(n, float) for n in aer1) pymap3d-3.1.0/src/pymap3d/tests/test_eci.py000066400000000000000000000036401456201517100205540ustar00rootroot00000000000000from datetime import datetime import pymap3d as pm import pytest from pytest import approx try: import astropy except ImportError: astropy = None def test_eci2ecef(): pytest.importorskip("numpy") # this example from Matlab eci2ecef docs eci = [-2981784, 5207055, 3161595] utc = datetime(2019, 1, 4, 12) ecef = pm.eci2ecef(*eci, utc) rel = 0.025 if astropy is None else 0.0001 assert ecef == approx([-5.7627e6, -1.6827e6, 3.1560e6], rel=rel) def test_ecef2eci(): pytest.importorskip("numpy") # this example from Matlab ecef2eci docs ecef = [-5762640, -1682738, 3156028] utc = datetime(2019, 1, 4, 12) eci = pm.ecef2eci(*ecef, utc) rel = 0.01 if astropy is None else 0.0001 assert eci == approx([-2981810.6, 5207039.5, 3161595.1], rel=rel) def test_eci2geodetic(): pytest.importorskip("numpy") eci = [-2981784, 5207055, 3161595] utc = datetime(2019, 1, 4, 12) lla = pm.eci2geodetic(*eci, utc) rel = 0.01 if astropy is None else 0.0001 assert lla == approx([27.880801, -163.722058, 408850.646], rel=rel) def test_geodetic2eci(): pytest.importorskip("numpy") lla = [27.880801, -163.722058, 408850.646] utc = datetime(2019, 1, 4, 12) eci = pm.geodetic2eci(*lla, utc) rel = 0.01 if astropy is None else 0.0001 assert eci == approx([-2981784, 5207055, 3161595], rel=rel) def test_eci_aer(): # test coords from Matlab eci2aer pytest.importorskip("numpy") t = datetime(2022, 1, 2, 3, 4, 5) eci = [4500000, -45000000, 3000000] lla = [28, -80, 100] aer = pm.eci2aer(*eci, *lla, t) rel = 0.01 if astropy is None else 0.0001 assert aer == approx([314.9945, -53.0089, 5.026e7], rel=rel) eci2 = pm.aer2eci(*aer, *lla, t) 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) pymap3d-3.1.0/src/pymap3d/tests/test_ellipsoid.py000077500000000000000000000160721456201517100220060ustar00rootroot00000000000000import pytest from pytest import approx import pymap3d as pm xyz0 = (660e3, -4700e3, 4247e3) @pytest.mark.parametrize( "model,f", [ ("maupertuis", 0.005235602050865236), ("plessis", 0.003240020729165458), ("everest1830", 0.003324448922118313), ("everest1830m", 0.003324449295589469), ("everest1967", 0.003324449343845343), ("airy", 0.00334085067870327), ("bessel", 0.0033427731536659813), ("clarke1866", 0.0033900753039287908), ("clarke1878", 0.003407549790771363), ("clarke1860", 0.003407561308111843), ("helmert", 0.0033523298109184524), ("hayford", 0.003367003387062615), ("international1924", 0.003367003387062615), ("krassovsky1940", 0.0033523298336767685), ("wgs66", 0.0033528919458556804), ("australian", 0.003352891899858333), ("international1967", 0.003352896192983603), ("grs67", 0.0033529237272191623), ("sa1969", 0.003352891899858333), ("wgs72", 0.0033527794541680267), ("grs80", 0.0033528106811816882), ("wgs84", 0.0033528106647473664), ("wgs84_mean", 0.0), ("iers1989", 0.0033528131102879993), ("iers2003", 0.0033528131084554157), ("mercury", 0.0009014546199549272), ("venus", 0.0), ("moon", 0.0012082158679017317), ("mars", 0.006123875928193323), ("jupyter", 0.06604858798757626), ("io", 0.0075968738044488665), ("uranus", 0.022927344575296372), ("neptune", 0.01708124697141011), ("pluto", 0.0), ], ) def test_reference(model, f): assert pm.Ellipsoid.from_name(model).flattening == approx(f) def test_bad_name(): with pytest.raises(KeyError): pm.Ellipsoid.from_name("badname") def test_ellipsoid(): assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("maupertuis")) == approx( [42.123086280313906, -82.00647850636021, -13462.822154350226] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("plessis")) == approx( [42.008184833614905, -82.00647850636021, 1566.9219075104988] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("everest1830")) == approx( [42.01302648557789, -82.00647850636021, 1032.4153744896425] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("everest1830m")) == approx( [42.0130266467127, -82.00647850636021, 1027.7254294115853] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("everest1967")) == approx( [42.01302648557363, -82.00647850636021, 1033.2243733811288] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("airy")) == approx( [42.01397060398504, -82.00647850636021, 815.5499438015993] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("bessel")) == approx( [42.01407537004288, -82.00647850636021, 987.0246149983182] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("clarke1866")) == approx( [42.01680003414445, -82.00647850636021, 313.90267925120395] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("clarke1878")) == approx( [42.0177971504227, -82.00647850636021, 380.12002203958457] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("clarke1860")) == approx( [42.017799612218326, -82.00647850636021, 321.0980872430816] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("helmert")) == approx( [42.01464497456125, -82.00647850636021, 212.63680219872765] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("hayford")) == approx( [42.01548834310426, -82.00647850636021, 66.77070154259877] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("international1924")) == approx( [42.01548834310426, -82.00647850636021, 66.77070154259877] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("krassovsky1940")) == approx( [42.01464632634865, -82.00647850636021, 167.7043859419633] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("wgs66")) == approx( [42.014675415414274, -82.00647850636021, 269.1575142686737] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("australian")) == approx( [42.01467586302664, -82.00647850636021, 254.17989315657786] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("international1967")) == approx( [42.01467603307557, -82.00647850636021, 256.6883857005818] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("grs67")) == approx( [42.01467768000789, -82.00647850636021, 254.27066653452297] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("sa1969")) == approx( [42.01467586302664, -82.00647850636021, 254.17989315657786] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("wgs72")) == approx( [42.01466869328149, -82.00647850636021, 278.8216763935984] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("grs80")) == approx( [42.01467053601299, -82.00647850636021, 276.9137384511387] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("wgs84")) == approx( [42.01467053507479, -82.00647850636021, 276.91369158042767] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("wgs84_mean")) == approx( [41.823366301, -82.0064785, -2.13061272e3] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("iers1989")) == approx( [42.01467064467172, -82.00647850636021, 277.9191657339711] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("iers2003")) == approx( [42.01467066257621, -82.00647850636021, 277.320060889772] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("mercury")) == approx( [41.8430384333997, -82.00647850636021, 3929356.5648451606] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("venus")) == approx( [41.82336630167669, -82.00647850636021, 317078.15867127385] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("moon")) == approx( [41.842147614909734, -82.00647850636021, 4631711.995926845] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("mars")) == approx( [42.00945156056578, -82.00647850636021, 2981246.073616111] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("jupyter")) == approx( [75.3013267078341, -82.00647850636021, -61782040.202975556] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("io")) == approx( [41.82422244977044, -82.00647850636021, 6367054.626528843] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("uranus")) == approx( [47.69837228395133, -82.00647850636021, -18904824.4361074] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("neptune")) == approx( [45.931317431546425, -82.00647850636021, -18194050.781948525] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid.from_name("pluto")) == approx( [41.82336630167669, -82.00647850636021, 5180878.158671274] ) pymap3d-3.1.0/src/pymap3d/tests/test_enu.py000066400000000000000000000035721456201517100206070ustar00rootroot00000000000000from math import radians import pymap3d as pm import pytest from pytest import approx ELL = pm.Ellipsoid.from_name("wgs84") A = ELL.semimajor_axis B = ELL.semiminor_axis @pytest.mark.parametrize("xyz", [(0, A, 50), ([0], [A], [50])], ids=("scalar", "list")) def test_scalar_enu(xyz): """ 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) def test_array_enu(): 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) 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) @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): x, y, z = pm.enu2ecef(*enu, *lla) assert x == approx(xyz[0]) assert y == approx(xyz[1]) assert z == approx(xyz[2]) assert isinstance(x, float) assert isinstance(y, float) assert isinstance(z, float) rlla = (radians(lla[0]), radians(lla[1]), lla[2]) assert pm.enu2ecef(*enu, *rlla, deg=False) == approx(xyz) e, n, u = pm.ecef2enu(*xyz, *lla) assert e == approx(enu[0]) assert n == approx(enu[1]) assert u == approx(enu[2]) assert isinstance(e, float) assert isinstance(n, float) assert isinstance(u, float) e, n, u = pm.ecef2enu(*xyz, *rlla, deg=False) assert e == approx(enu[0]) assert n == approx(enu[1]) assert u == approx(enu[2]) pymap3d-3.1.0/src/pymap3d/tests/test_geodetic.py000077500000000000000000000177451456201517100216150ustar00rootroot00000000000000from math import isnan, nan, radians, sqrt import pymap3d as pm import pytest from pytest import approx lla0 = (42, -82, 200) rlla0 = (radians(lla0[0]), radians(lla0[1]), lla0[2]) xyz0 = (660675.2518247, -4700948.68316, 4245737.66222) ELL = pm.Ellipsoid.from_name("wgs84") A = ELL.semimajor_axis B = ELL.semiminor_axis xyzlla = [ ((A, 0, 0), (0, 0, 0)), ((A - 1, 0, 0), (0, 0, -1)), ((A + 1, 0, 0), (0, 0, 1)), ((0.1 * A, 0, 0), (0, 0, -0.9 * A)), ((0.001 * A, 0, 0), (0, 0, -0.999 * A)), ((0, A, 0), (0, 90, 0)), ((0, A - 1, 0), (0, 90, -1)), ((0, A + 1, 0), (0, 90, 1)), ((0, 0.1 * A, 0), (0, 90, -0.9 * A)), ((0, 0.001 * A, 0), (0, 90, -0.999 * A)), ((0, 0, B), (90, 0, 0)), ((0, 0, B + 1), (90, 0, 1)), ((0, 0, B - 1), (90, 0, -1)), ((0, 0, 0.1 * B), (90, 0, -0.9 * B)), ((0, 0, 0.001 * B), (90, 0, -0.999 * B)), ((0, 0, B - 1), (89.999999, 0, -1)), ((0, 0, B - 1), (89.99999, 0, -1)), ((0, 0, -B + 1), (-90, 0, -1)), ((0, 0, -B + 1), (-89.999999, 0, -1)), ((0, 0, -B + 1), (-89.99999, 0, -1)), ((-A + 1, 0, 0), (0, 180, -1)), ] llaxyz = [ ((0, 0, -1), (A - 1, 0, 0)), ((0, 90, -1), (0, A - 1, 0)), ((0, -90, -1), (0, -A + 1, 0)), ((90, 0, -1), (0, 0, B - 1)), ((90, 15, -1), (0, 0, B - 1)), ((-90, 0, -1), (0, 0, -B + 1)), ] atol_dist = 1e-6 # 1 micrometer @pytest.mark.parametrize("lla", [lla0, ([lla0[0]], [lla0[1]], [lla0[2]])], ids=("scalar", "list")) def test_scalar_geodetic2ecef(lla): """ verify we can handle the wide variety of input data type users might use """ if isinstance(lla[0], list): np = pytest.importorskip("numpy") scalar = False else: scalar = True xyz = pm.geodetic2ecef(*lla) lla1 = pm.ecef2geodetic(*xyz) try: np.testing.assert_allclose(lla1, lla, rtol=1e-4) except NameError: assert lla1 == approx(lla, rel=1e-4) if scalar: assert all(isinstance(n, float) for n in xyz) assert all(isinstance(n, float) for n in lla1) def test_array_geodetic2ecef(): np = pytest.importorskip("numpy") lla = (np.asarray(lla0[0]), np.asarray(lla0[1]), np.asarray(lla0[2])) xyz = pm.geodetic2ecef(*lla) np.testing.assert_allclose(pm.ecef2geodetic(*xyz), lla) lla = (np.atleast_1d(lla0[0]), np.atleast_1d(lla0[1]), np.atleast_1d(lla0[2])) xyz = pm.geodetic2ecef(*lla) np.testing.assert_allclose(pm.ecef2geodetic(*xyz), lla) @pytest.mark.parametrize("xyz", [xyz0, ([xyz0[0]], [xyz0[1]], [xyz0[2]])], ids=("scalar", "list")) def test_scalar_ecef2geodetic(xyz): """ verify we can handle the wide variety of input data type users might use """ if isinstance(xyz[0], list): np = pytest.importorskip("numpy") scalar = False else: scalar = True lla = pm.ecef2geodetic(*xyz) xyz1 = pm.geodetic2ecef(*lla) try: np.testing.assert_allclose(xyz1, xyz, rtol=1e-4) except NameError: assert xyz1 == approx(xyz, rel=1e-4) if scalar: assert all(isinstance(n, float) for n in xyz1) assert all(isinstance(n, float) for n in lla) def test_array_ecef2geodetic(): np = pytest.importorskip("numpy") xyz = (np.asarray(xyz0[0]), np.asarray(xyz0[1]), np.asarray(xyz0[2])) lla = pm.ecef2geodetic(*xyz) np.testing.assert_allclose(pm.geodetic2ecef(*lla), xyz) xyz = (np.atleast_1d(xyz0[0]), np.atleast_1d(xyz0[1]), np.atleast_1d(xyz0[2])) lla = pm.ecef2geodetic(*xyz) np.testing.assert_allclose(pm.geodetic2ecef(*lla), xyz) def test_inside_ecef2geodetic(): np = pytest.importorskip("numpy") # test values with no points inside ellipsoid lla0_array = ( np.array([lla0[0], lla0[0]]), np.array([lla0[1], lla0[1]]), np.array([lla0[2], lla0[2]]), ) xyz = pm.geodetic2ecef(*lla0_array) lats, lons, alts = pm.ecef2geodetic(*xyz) assert lats == approx(lla0_array[0]) assert lons == approx(lla0_array[1]) assert alts == approx(lla0_array[2]) # test values with some (but not all) points inside ellipsoid lla0_array_inside = ( np.array([lla0[0], lla0[0]]), np.array([lla0[1], lla0[1]]), np.array([lla0[2], -lla0[2]]), ) xyz = pm.geodetic2ecef(*lla0_array_inside) lats, lons, alts = pm.ecef2geodetic(*xyz) assert lats == approx(lla0_array_inside[0]) assert lons == approx(lla0_array_inside[1]) assert alts == approx(lla0_array_inside[2]) def test_xarray_ecef(): xarray = pytest.importorskip("xarray") lla = xarray.DataArray(list(lla0)) xyz = pm.geodetic2ecef(*lla) lla1 = pm.ecef2geodetic(*xyz) assert lla1 == approx(lla) def test_pandas_ecef(): pandas = pytest.importorskip("pandas") x, y, z = pm.geodetic2ecef( pandas.Series(lla0[0]), pandas.Series(lla0[1]), pandas.Series(lla0[2]) ) lat, lon, alt = pm.ecef2geodetic(pandas.Series(x), pandas.Series(y), pandas.Series(z)) assert lat == approx(lla0[0]) assert lon == approx(lla0[1]) assert alt == approx(lla0[2]) def test_ecef(): xyz = pm.geodetic2ecef(*lla0) assert xyz == approx(xyz0) x, y, z = pm.geodetic2ecef(*rlla0, deg=False) assert x == approx(xyz[0]) assert y == approx(xyz[1]) assert z == approx(xyz[2]) assert pm.ecef2geodetic(*xyz) == approx(lla0) assert pm.ecef2geodetic(*xyz, deg=False) == approx(rlla0) assert pm.ecef2geodetic((A - 1) / sqrt(2), (A - 1) / sqrt(2), 0) == approx([0, 45, -1]) @pytest.mark.parametrize("lla, xyz", llaxyz) def test_geodetic2ecef(lla, xyz): assert pm.geodetic2ecef(*lla) == approx(xyz, abs=atol_dist) @pytest.mark.parametrize("xyz, lla", xyzlla) def test_ecef2geodetic(xyz, lla): lat, lon, alt = pm.ecef2geodetic(*xyz) assert lat == approx(lla[0]) assert lon == approx(lla[1]) assert alt == approx(lla[2], abs=1e-9) @pytest.mark.parametrize( "aer,lla,lla0", [ ((33, 77, 1000), (42.0016981935, -81.99852, 1174.374035), (42, -82, 200)), ((0, 90, 10000), (0, 0, 10000), (0, 0, 0)), ], ) def test_aer_geodetic(aer, lla, lla0): lla1 = pm.aer2geodetic(*aer, *lla0) assert lla1 == approx(lla) assert all(isinstance(n, float) for n in lla1) raer = (radians(aer[0]), radians(aer[1]), aer[2]) rlla0 = (radians(lla0[0]), radians(lla0[1]), lla0[2]) lla1 = pm.aer2geodetic(*raer, *rlla0, deg=False) assert lla1 == approx((radians(lla[0]), radians(lla[1]), lla[2])) assert all(isinstance(n, float) for n in lla1) with pytest.raises(ValueError): pm.aer2geodetic(aer[0], aer[1], -1, *lla0) assert pm.geodetic2aer(*lla, *lla0) == approx(aer, rel=1e-3) assert pm.geodetic2aer(radians(lla[0]), radians(lla[1]), lla[2], *rlla0, deg=False) == approx( raer, rel=1e-3 ) def test_scalar_nan(): aer = pm.geodetic2aer(nan, nan, nan, *lla0) assert all(isnan(n) for n in aer) llat = pm.aer2geodetic(nan, nan, nan, *lla0) assert all(isnan(n) for n in llat) def test_allnan(): np = pytest.importorskip("numpy") anan = np.empty((10, 10)) anan.fill(nan) assert np.isnan(pm.geodetic2aer(anan, anan, anan, *lla0)).all() assert np.isnan(pm.aer2geodetic(anan, anan, anan, *lla0)).all() def test_somenan(): np = pytest.importorskip("numpy") xyz = np.stack((xyz0, (nan, nan, nan))) lat, lon, alt = pm.ecef2geodetic(xyz[:, 0], xyz[:, 1], xyz[:, 2]) assert (lat[0], lon[0], alt[0]) == approx(lla0) @pytest.mark.parametrize("xyz, lla", xyzlla) def test_numpy_ecef2geodetic(xyz, lla): np = pytest.importorskip("numpy") lla1 = pm.ecef2geodetic( *np.array( [ [xyz], ], dtype=np.float32, ).T ) assert lla1 == approx(lla) @pytest.mark.parametrize("lla, xyz", llaxyz) def test_numpy_geodetic2ecef(lla, xyz): np = pytest.importorskip("numpy") xyz1 = pm.geodetic2ecef( *np.array( [ [lla], ], dtype=np.float32, ).T ) atol_dist = 1 # meters assert xyz1 == approx(xyz, abs=atol_dist) pymap3d-3.1.0/src/pymap3d/tests/test_latitude.py000066400000000000000000000135561456201517100216360ustar00rootroot00000000000000from math import inf, radians import pymap3d.latitude as latitude import pymap3d.rcurve as rcurve import pytest from pytest import approx @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(geodetic_lat, alt_m, geocentric_lat): assert latitude.geod2geoc(geodetic_lat, alt_m) == approx(geocentric_lat) r = rcurve.geocentric_radius(geodetic_lat) assert latitude.geoc2geod(geocentric_lat, r) == approx(geodetic_lat) assert latitude.geoc2geod(geocentric_lat, 1e5 + r) == approx( latitude.geocentric2geodetic(geocentric_lat, 1e5 + alt_m) ) assert latitude.geod2geoc(geodetic_lat, 1e5 + alt_m) == approx( latitude.geodetic2geocentric(geodetic_lat, 1e5 + alt_m) ) @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): assert latitude.geodetic2geocentric(geodetic_lat, 0) == approx(geocentric_lat) assert latitude.geodetic2geocentric(radians(geodetic_lat), 0, deg=False) == approx( radians(geocentric_lat) ) assert latitude.geocentric2geodetic(geocentric_lat, 0) == approx(geodetic_lat) assert latitude.geocentric2geodetic(radians(geocentric_lat), 0, deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_geocentric(): pytest.importorskip("numpy") assert latitude.geodetic2geocentric([45, 0], 0) == approx([44.80757678, 0]) assert latitude.geocentric2geodetic([44.80757678, 0], 0) == approx([45, 0]) @pytest.mark.parametrize( "geodetic_lat, isometric_lat", [ (0, 0), (90, inf), (-90, -inf), (45, 50.227466), (-45, -50.227466), (89, 271.275), ], ) def test_geodetic_isometric(geodetic_lat, isometric_lat): isolat = latitude.geodetic2isometric(geodetic_lat) assert isolat == approx(isometric_lat) assert isinstance(isolat, float) assert latitude.geodetic2isometric(radians(geodetic_lat), deg=False) == approx( radians(isometric_lat) ) assert latitude.isometric2geodetic(isometric_lat) == approx(geodetic_lat) assert latitude.isometric2geodetic(radians(isometric_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_isometric(): pytest.importorskip("numpy") assert latitude.geodetic2isometric([45, 0]) == approx([50.227466, 0]) assert latitude.isometric2geodetic([50.227466, 0]) == approx([45, 0]) @pytest.mark.parametrize( "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): clat = latitude.geodetic2conformal(geodetic_lat) assert clat == approx(conformal_lat) assert isinstance(clat, float) assert latitude.geodetic2conformal(radians(geodetic_lat), deg=False) == approx( radians(conformal_lat) ) assert latitude.conformal2geodetic(conformal_lat) == approx(geodetic_lat) assert latitude.conformal2geodetic(radians(conformal_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_conformal(): pytest.importorskip("numpy") assert latitude.geodetic2conformal([45, 0]) == approx([44.80768406, 0]) assert latitude.conformal2geodetic([44.80768406, 0]) == approx([45, 0]) @pytest.mark.parametrize( "geodetic_lat,rectifying_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.855682), (-45, -44.855682)], ) def test_geodetic_rectifying(geodetic_lat, rectifying_lat): assert latitude.geodetic2rectifying(geodetic_lat) == approx(rectifying_lat) assert latitude.geodetic2rectifying(radians(geodetic_lat), deg=False) == approx( radians(rectifying_lat) ) assert latitude.rectifying2geodetic(rectifying_lat) == approx(geodetic_lat) assert latitude.rectifying2geodetic(radians(rectifying_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_rectifying(): pytest.importorskip("numpy") assert latitude.geodetic2rectifying([45, 0]) == approx([44.855682, 0]) assert latitude.rectifying2geodetic([44.855682, 0]) == approx([45, 0]) @pytest.mark.parametrize( "geodetic_lat,authalic_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.87170288), (-45, -44.87170288)], ) def test_geodetic_authalic(geodetic_lat, authalic_lat): assert latitude.geodetic2authalic(geodetic_lat) == approx(authalic_lat) assert latitude.geodetic2authalic(radians(geodetic_lat), deg=False) == approx( radians(authalic_lat) ) assert latitude.authalic2geodetic(authalic_lat) == approx(geodetic_lat) assert latitude.authalic2geodetic(radians(authalic_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_authalic(): pytest.importorskip("numpy") assert latitude.geodetic2authalic([45, 0]) == approx([44.87170288, 0]) assert latitude.authalic2geodetic([44.87170288, 0]) == approx([45, 0]) @pytest.mark.parametrize( "geodetic_lat,parametric_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.9037878), (-45, -44.9037878)], ) def test_geodetic_parametric(geodetic_lat, parametric_lat): assert latitude.geodetic2parametric(geodetic_lat) == approx(parametric_lat) assert latitude.geodetic2parametric(radians(geodetic_lat), deg=False) == approx( radians(parametric_lat) ) assert latitude.parametric2geodetic(parametric_lat) == approx(geodetic_lat) assert latitude.parametric2geodetic(radians(parametric_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_parametric(): pytest.importorskip("numpy") assert latitude.geodetic2parametric([45, 0]) == approx([44.9037878, 0]) assert latitude.parametric2geodetic([44.9037878, 0]) == approx([45, 0]) pymap3d-3.1.0/src/pymap3d/tests/test_look_spheroid.py000066400000000000000000000042401456201517100226520ustar00rootroot00000000000000from math import nan import pymap3d.los as los import pytest from pytest import approx lla0 = (42, -82, 200) @pytest.mark.parametrize( "az,tilt,lat,lon,sr", [ (0, 0, 10, -20, 1e3), (0, 90, nan, nan, nan), (0, 45, 10.009041667, -20, 1.41432515e3), (45, 45, 10.00639336, -19.993549978, 1.414324795e3), (125, 45, 9.99481382, -19.992528, 1.414324671e3), ], ) def test_losint(az, tilt, lat, lon, sr): lla0 = (10, -20, 1e3) lat1, lon1, sr1 = los.lookAtSpheroid(*lla0, az, tilt=tilt) nan_ok = True if tilt == 90 else False assert lat1 == approx(lat, nan_ok=nan_ok) assert lon1 == approx(lon, nan_ok=nan_ok) assert sr1 == approx(sr, nan_ok=nan_ok) assert isinstance(lat1, float) assert isinstance(lon1, float) assert isinstance(sr1, float) def test_badval(): with pytest.raises(ValueError): los.lookAtSpheroid(0, 0, -1, 0, 0) def test_array_los(): 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) truth = np.array( [ [42.00103959, lla0[1], 230.9413173], [42.00177328, -81.9995808, 282.84715651], [nan, nan, nan], ] ) assert np.column_stack((lat, lon, sr)) == approx(truth, nan_ok=True) lat, lon, sr = los.lookAtSpheroid([lla0[0]] * 3, [lla0[1]] * 3, [lla0[2]] * 3, az, tilt) assert np.column_stack((lat, lon, sr)) == approx(truth, nan_ok=True) def test_xarray_los(): 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) assert lat == approx(42.00103959) assert lon == approx(lla0[1]) assert sr == approx(230.9413173) def test_pandas_los(): 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) assert lat == approx(42.00103959) assert lon == approx(lla0[1]) assert sr == approx(230.9413173) pymap3d-3.1.0/src/pymap3d/tests/test_matlab_ecef2eci.py000066400000000000000000000040511456201517100227760ustar00rootroot00000000000000""" Compare ecef2eci() with Matlab Aerospace Toolbox """ from __future__ import annotations from datetime import datetime import pytest from pytest import approx import pymap3d try: import numpy as np from .matlab_engine import matlab_engine, has_aerospace, has_matmap3d, pydt2matdt except ImportError: pytest.skip("Matlab Engine not found", allow_module_level=True) except RuntimeError: pytest.skip("Matlab Engine configuration error", allow_module_level=True) def ecef2eci(eng, matmap3d: bool, utc_m, ecef): if matmap3d: return eng.matmap3d.ecef2eci(utc_m, *ecef, nargout=3) return np.array(eng.ecef2eci(utc_m, np.asarray(ecef), nargout=1)).squeeze() def eci2ecef(eng, matmap3d: bool, utc_m, eci): if matmap3d: return eng.matmap3d.eci2ecef(utc_m, *eci, nargout=3) return np.array(eng.eci2ecef(utc_m, np.asarray(eci), nargout=1)).squeeze() @pytest.mark.parametrize("matmap3d", [False, True]) def test_compare_ecef2eci(matmap3d): eng = matlab_engine() if matmap3d: if not has_matmap3d(eng): pytest.skip("Matmap3d not found") else: if not has_aerospace(eng): pytest.skip("Aerospace Toolbox not found") ecef = [-5762640.0, -1682738.0, 3156028.0] utc = datetime(2019, 1, 4, 12) rtol = 0.01 eci_py = pymap3d.ecef2eci(ecef[0], ecef[1], ecef[2], utc) eci_m = ecef2eci(eng, matmap3d, pydt2matdt(eng, utc), ecef) assert eci_py == approx(eci_m, rel=rtol) @pytest.mark.parametrize("matmap3d", [False, True]) def test_compare_eci2ecef(matmap3d): eng = matlab_engine() if matmap3d: if not has_matmap3d(eng): pytest.skip("Matmap3d not found") else: if not has_aerospace(eng): pytest.skip("Aerospace Toolbox not found") eci = [-3009680.518620539, 5194367.153184303, 3156028.0] utc = datetime(2019, 1, 4, 12) rtol = 0.02 ecef_py = pymap3d.eci2ecef(eci[0], eci[1], eci[2], utc) ecef_m = eci2ecef(eng, matmap3d, pydt2matdt(eng, utc), eci) assert ecef_py == approx(ecef_m, rel=rtol) pymap3d-3.1.0/src/pymap3d/tests/test_matlab_lox.py000066400000000000000000000030141456201517100221310ustar00rootroot00000000000000#!/usr/bin/env python3 """ Compare with Matlab Mapping toolbox reckon() """ from __future__ import annotations import pytest from pytest import approx try: from .matlab_engine import matlab_engine, has_matmap3d, has_mapping except ImportError: pytest.skip("Matlab Engine not found", allow_module_level=True) except RuntimeError: pytest.skip("Matlab Engine configuration error", allow_module_level=True) from pymap3d.lox import loxodrome_direct def reckon( eng, matmap3d: bool, lat1: float, lon1: float, rng: float, az: float ) -> tuple[float, float]: """Using Matlab Engine to do same thing as Pymap3d""" if matmap3d: return eng.matmap3d.vreckon(lat1, lon1, rng, az, nargout=2) return eng.reckon("rh", lat1, lon1, rng, az, eng.wgs84Ellipsoid(), nargout=2) @pytest.mark.parametrize("matmap3d", [False, True]) def test_lox_stability(matmap3d): eng = matlab_engine() if matmap3d: if not has_matmap3d(eng): pytest.skip("Matmap3d not found") else: if not has_mapping(eng): pytest.skip("Matlab Toolbox not found") clat, clon, rng = 35.0, 140.0, 50000.0 # arbitrary for i in range(20): for azi in (90 + 10.0 ** (-i), -90 + 10.0 ** (-i), 270 + 10.0 ** (-i), -270 + 10.0 ** (-i)): lat, lon = loxodrome_direct(clat, clon, rng, azi) lat_matlab, lon_matlab = reckon(eng, matmap3d, clat, clon, rng, azi) assert lat == approx(lat_matlab, rel=0.005) assert lon == approx(lon_matlab, rel=0.001) pymap3d-3.1.0/src/pymap3d/tests/test_matlab_track2.py000066400000000000000000000025541456201517100225250ustar00rootroot00000000000000#!/usr/bin/env python3 """ Compare with Matlab Mapping toolbox reckon() """ from __future__ import annotations import pytest from pytest import approx try: import numpy as np from .matlab_engine import matlab_engine, has_mapping except ImportError: pytest.skip("Matlab Engine not found", allow_module_level=True) except RuntimeError: pytest.skip("Matlab Engine configuration error", allow_module_level=True) import pymap3d.vincenty def track2(eng, lat1: float, lon1: float, lat2: float, lon2: float, npts: int, deg: bool) -> tuple: """Using Matlab Engine to do same thing as Pymap3d""" d = "degrees" if deg else "radians" lats, lons = eng.track2( "gc", lat1, lon1, lat2, lon2, eng.wgs84Ellipsoid(), d, float(npts), nargout=2 ) return np.array(lats).squeeze(), np.array(lons).squeeze() @pytest.mark.parametrize("deg", [True, False]) def test_track2_compare(deg): lat1, lon1 = 0.0, 80.0 lat2, lon2 = 0.0, 81.0 if not deg: lat1, lon1, lat2, lon2 = np.radians((lat1, lon1, lat2, lon2)) eng = matlab_engine() if not has_mapping(eng): pytest.skip("Matlab Toolbox not found") lats, lons = pymap3d.vincenty.track2(lat1, lon1, lat2, lon2, npts=4, deg=deg) lats_m, lons_m = track2(eng, lat1, lon1, lat2, lon2, npts=4, deg=deg) assert lats == approx(lats_m) assert lons == approx(lons_m) pymap3d-3.1.0/src/pymap3d/tests/test_matlab_vdist.py000066400000000000000000000027101456201517100224620ustar00rootroot00000000000000""" Compare with Matlab Mapping Toolbox distance() """ from __future__ import annotations from math import nan import pytest from pytest import approx try: from .matlab_engine import matlab_engine, has_mapping, has_matmap3d except ImportError: pytest.skip("Matlab Engine not found", allow_module_level=True) except RuntimeError: pytest.skip("Matlab Engine configuration error", allow_module_level=True) from pymap3d.vincenty import vdist def distance(eng, matmap3d: bool, lat1, lon1, lat2, lon2) -> tuple[float, float]: """Using Matlab Engine to do same thing as Pymap3d""" if matmap3d: return eng.matmap3d.vdist(lat1, lon1, lat2, lon2, nargout=2) return eng.distance(lat1, lon1, lat2, lon2, eng.wgs84Ellipsoid(), nargout=2) @pytest.mark.parametrize("matmap3d", [False, True]) def test_matlab_stability(matmap3d): eng = matlab_engine() if matmap3d: if not has_matmap3d(eng): pytest.skip("Matmap3d not found") else: if not has_mapping(eng): pytest.skip("Matlab Toolbox not found") dlast, alast = nan, nan lon1, lon2 = 0.0, 1.0 for i in range(20): lat1 = lat2 = 10.0 ** (-i) dist_m, az_deg = vdist(lat1, lon1, lat2, lon2) assert dist_m != dlast assert az_deg != alast dist_matlab, az_matlab = distance(eng, matmap3d, lat1, lon1, lat2, lon2) assert dist_m == approx(dist_matlab) assert az_deg == approx(az_matlab) pymap3d-3.1.0/src/pymap3d/tests/test_matlab_vreckon.py000066400000000000000000000042501456201517100230010ustar00rootroot00000000000000""" Compare with Matlab Mapping Toolbox reckon() """ from __future__ import annotations from math import nan import pytest from pytest import approx try: from .matlab_engine import matlab_engine, has_mapping, has_matmap3d except ImportError: pytest.skip("Matlab Engine not found", allow_module_level=True) except RuntimeError: pytest.skip("Matlab Engine configuration error", allow_module_level=True) import pymap3d.vincenty def reckon( eng, matmap3d: bool, lat1: float, lon1: float, srng: float, az: float ) -> tuple[float, float]: """Using Matlab Engine to do same thing as Pymap3d""" if matmap3d: return eng.matmap3d.vreckon(lat1, lon1, srng, az, nargout=2) return eng.reckon("gc", lat1, lon1, srng, az, eng.wgs84Ellipsoid(), nargout=2) @pytest.mark.parametrize("matmap3d", [False, True]) def test_reckon_stability(matmap3d): eng = matlab_engine() if matmap3d: if not has_matmap3d(eng): pytest.skip("Matmap3d not found") else: if not has_mapping(eng): pytest.skip("Matlab Toolbox not found") dlast, alast = nan, nan lon1, lon2 = 0.0, 1.0 for i in range(20): lat1 = lat2 = 10.0 ** (-i) dist_m, az_deg = pymap3d.vincenty.vreckon(lat1, lon1, lat2, lon2) assert dist_m != dlast assert az_deg != alast dist_matlab, az_matlab = reckon(eng, matmap3d, lat1, lon1, lat2, lon2) assert dist_m == approx(dist_matlab) assert az_deg == approx(az_matlab, rel=0.005) @pytest.mark.parametrize("matmap3d", [False, True]) def test_reckon_unit(matmap3d): """ Test various extrema and other values of interest """ eng = matlab_engine() if matmap3d: if not has_matmap3d(eng): pytest.skip("Matmap3d not found") else: if not has_mapping(eng): pytest.skip("Matlab Toolbox not found") latlon88 = 52.22610277777778, -1.2696583333333333 srng88 = 839.63 az88 = 63.02 # issue 88 lat_p, lon_p = pymap3d.vincenty.vreckon(*latlon88, srng88, az88) lat_m, lon_m = reckon(eng, matmap3d, *latlon88, srng88, az88) assert lat_p == approx(lat_m) assert lon_p == approx(lon_m) pymap3d-3.1.0/src/pymap3d/tests/test_ned.py000077500000000000000000000021751456201517100205670ustar00rootroot00000000000000import pymap3d as pm from pytest import approx lla0 = (42, -82, 200) aer0 = (33, 70, 1000) ELL = pm.Ellipsoid.from_name("wgs84") A = ELL.semimajor_axis B = ELL.semiminor_axis def test_ecef_ned(): enu = pm.aer2enu(*aer0) ned = (enu[1], enu[0], -enu[2]) xyz = pm.aer2ecef(*aer0, *lla0) ned1 = pm.ecef2ned(*xyz, *lla0) assert ned1 == approx(ned) assert pm.ned2ecef(*ned, *lla0) == approx(xyz) def test_enuv_nedv(): 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)) assert pm.ecef2nedv(vx, vy, vz, *lla0[:2]) == approx((vn, ve, -vu)) def test_ned_geodetic(): lla1 = pm.aer2geodetic(*aer0, *lla0) enu3 = pm.geodetic2enu(*lla1, *lla0) ned3 = (enu3[1], enu3[0], -enu3[2]) assert pm.geodetic2ned(*lla1, *lla0) == approx(ned3) lla2 = pm.enu2geodetic(*enu3, *lla0) assert lla2 == approx(lla1) assert all(isinstance(n, float) for n in lla2) lla2 = pm.ned2geodetic(*ned3, *lla0) assert lla2 == approx(lla1) assert all(isinstance(n, float) for n in lla2) pymap3d-3.1.0/src/pymap3d/tests/test_pyproj.py000077500000000000000000000017071456201517100213440ustar00rootroot00000000000000import pymap3d as pm import pytest from pymap3d.vincenty import vreckon from pytest import approx lla0 = [42, -82, 200] def test_compare_vicenty(): taz, tsr = 38, 3000 pyproj = pytest.importorskip("pyproj") lat2, lon2 = vreckon(10, 20, tsr, taz) p4lon, p4lat, p4a21 = pyproj.Geod(ellps="WGS84").fwd(lon2, lat2, taz, tsr) assert p4lon == approx(lon2, rel=0.0025) assert p4lat == approx(lat2, rel=0.0025) p4az, p4a21, p4sr = pyproj.Geod(ellps="WGS84").inv(20, 10, lon2, lat2) assert (p4az, p4sr) == approx((taz, tsr)) def test_compare_geodetic(): pyproj = pytest.importorskip("pyproj") xyz = pm.geodetic2ecef(*lla0) ecef = pyproj.Proj(proj="geocent", ellps="WGS84", datum="WGS84") lla = pyproj.Proj(proj="latlong", ellps="WGS84", datum="WGS84") assert pyproj.transform(lla, ecef, lla0[1], lla0[0], lla0[2]) == approx(xyz) assert pyproj.transform(ecef, lla, *xyz) == approx((lla0[1], lla0[0], lla0[2])) pymap3d-3.1.0/src/pymap3d/tests/test_rcurve.py000066400000000000000000000021441456201517100213200ustar00rootroot00000000000000import pymap3d as pm import pymap3d.rcurve as rcurve import pytest from pytest import approx ell = pm.Ellipsoid.from_name("wgs84") A = ell.semimajor_axis @pytest.mark.parametrize( "lat,curvature", [(0, A), (90, 0), (-90, 0), (45.0, 4517590.87884893), (-45, 4517590.87884893)] ) def test_rcurve_parallel(lat, curvature): assert rcurve.parallel(lat) == approx(curvature, abs=1e-9, rel=1e-6) def test_numpy_parallel(): pytest.importorskip("numpy") assert rcurve.parallel([0, 90]) == approx([A, 0], abs=1e-9, rel=1e-6) @pytest.mark.parametrize( "lat,curvature", [ (0, 6335439.327), (90, 6399593.6258), (-90, 6399593.6258), (45.0, 6367381.8156), (-45, 6367381.8156), ], ) def test_rcurve_meridian(lat, curvature): assert rcurve.meridian(lat) == approx(curvature) def test_numpy_meridian(): pytest.importorskip("numpy") assert rcurve.meridian([0, 90]) == approx([6335439.327, 6399593.6258]) def test_numpy_transverse(): pytest.importorskip("numpy") assert rcurve.transverse([-90, 0, 90]) == approx([6399593.6258, A, 6399593.6258]) pymap3d-3.1.0/src/pymap3d/tests/test_rhumb.py000077500000000000000000000100701456201517100211270ustar00rootroot00000000000000import 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): assert lox.meridian_dist(lat) == approx(dist) @pytest.mark.parametrize( "lat1,lat2,arclen", [ (0, 0, 0), (0, 90, 10001965.729), (0, -90, 10001965.729), (0, 40, 4429529.03035058), (40, 80, 4455610.84159), ], ) def test_meridian_arc(lat1, lat2, arclen): """ meridianarc(deg2rad(40), deg2rad(80), wgs84Ellipsoid) """ assert lox.meridian_arc(lat1, lat2) == approx(arclen) @pytest.mark.parametrize( "lon1,lon2,lat,dist", [ (0, 0, 0, 0), (0, 90, 0, 10018754.1714), (0, -90, 0, 10018754.1714), (90, 0, 0, 10018754.1714), (-90, 0, 0, 10018754.1714), ], ) def test_departure(lon1, lon2, lat, dist): assert lox.departure(lon1, lon2, lat) == approx(dist) @pytest.mark.parametrize( "lat1,lon1,lat2,lon2,arclen,az", [ (40, -80, 65, -148, 5248666.20853187, 302.0056736), (0, 0, 0, 1, 111319.49, 90), (0, 0, 0, -1, 111319.49, 270), (0, 1, 0, 0, 111319.49, 270), (0, -1, 0, 0, 111319.49, 90), (1, 0, 0, 0, 110574.4, 180), (-1, 0, 0, 0, 110574.4, 0), ], ) def test_loxodrome_inverse(lat1, lon1, lat2, lon2, arclen, az): """ distance('rh', 40, -80, 65, -148, wgs84Ellipsoid) azimuth('rh', 40, -80, 65, -148, wgs84Ellipsoid) """ rhdist, rhaz = lox.loxodrome_inverse(lat1, lon1, lat2, lon2) assert rhdist == approx(arclen) assert rhaz == approx(az) assert isinstance(rhdist, float) assert isinstance(rhaz, float) def test_numpy_loxodrome_inverse(): pytest.importorskip("numpy") d, a = lox.loxodrome_inverse([40, 40], [-80, -80], 65, -148) assert d == approx(5248666.209) assert a == approx(302.00567) d, a = lox.loxodrome_inverse([40, 40], [-80, -80], [65, 65], -148) d, a = lox.loxodrome_inverse([40, 40], [-80, -80], 65, [-148, -148]) def test_numpy_2d_loxodrome_inverse(): pytest.importorskip("numpy") d, a = lox.loxodrome_inverse([[40, 40], [40, 40]], [[-80, -80], [-80, -80]], 65, -148) assert d == approx(5248666.209) assert a == approx(302.00567) d, a = lox.loxodrome_inverse( [[40, 40], [40, 40]], [[-80, -80], [-80, -80]], [[65, 65], [65, 65]], -148 ) d, a = lox.loxodrome_inverse( [[40, 40], [40, 40]], [[-80, -80], [-80, -80]], 65, [[-148, -148], [-148, -148]] ) d, a = lox.loxodrome_inverse(40, -80, [[65, 65], [65, 65]], [[-148, -148], [-148, -148]]) @pytest.mark.parametrize( "lat0,lon0,rng,az,lat1,lon1", [ (40, -80, 10000, 30, 40.0000779959676, -79.9999414477481), (35, 140, 50000, 90, 35, 140.548934481815), (35, 140, 50000, -270, 35, 140.548934481815), (35, 140, 50000, 270, 35, 139.451065518185), (35, 140, 50000, -90, 35, 139.451065518185), (0, 0, 0, 0, 0, 0), (0, 0, 10018754.17, 90, 0, 90), (0, 0, 10018754.17, -90, 0, -90), (0, 0, 110574.4, 180, -1, 0), (-1, 0, 110574.4, 0, 0, 0), ], ) def test_loxodrome_direct(lat0, lon0, rng, az, lat1, lon1): """ reckon('rh', 40, -80, 10, 30, wgs84Ellipsoid) """ lat2, lon2 = lox.loxodrome_direct(lat0, lon0, rng, az) assert lat2 == approx(lat1, rel=0.005, abs=1e-6) assert lon2 == approx(lon1, rel=0.001) assert isinstance(lat2, float) assert isinstance(lon2, float) def test_numpy_loxodrome_direct(): 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([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): pytest.importorskip("numpy") assert lox.meanm(lat, lon) == approx([47.26967, 18.460557]) pymap3d-3.1.0/src/pymap3d/tests/test_rsphere.py000066400000000000000000000022131456201517100214570ustar00rootroot00000000000000import pymap3d as pm import pymap3d.rcurve as rcurve import pymap3d.rsphere as rsphere import pytest from pytest import approx ell = pm.Ellipsoid.from_name("wgs84") A = ell.semimajor_axis def test_geocentric_radius(): 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) assert rcurve.geocentric_radius(30) == approx(6372824.0) def test_rsphere_eqavol(): assert rsphere.eqavol() == approx(6371000.8049) def test_rsphere_authalic(): assert rsphere.authalic() == approx(6371007.1809) def test_rsphere_rectifying(): assert rsphere.rectifying() == approx(6367449.1458) def test_rsphere_biaxial(): assert rsphere.biaxial() == approx(6367444.657) def test_rsphere_triaxial(): assert rsphere.triaxial() == approx(6371008.77) def test_rsphere_euler(): assert rsphere.euler(42, 82, 44, 100) == approx(6386606.829131) def test_numpy_rsphere_euler(): pytest.importorskip("numpy") assert rsphere.euler([42, 0], [82, 0], 44, 100) == approx([6386606.829131, 6363111.70923164]) pymap3d-3.1.0/src/pymap3d/tests/test_sidereal.py000077500000000000000000000013451456201517100216070ustar00rootroot00000000000000from datetime import datetime from math import radians import pymap3d.haversine as pmh import pymap3d.sidereal as pmd import pytest from pytest import approx lon = -148 t0 = datetime(2014, 4, 6, 8) sra = 2.90658 ha = 45.482789587392013 @pytest.mark.parametrize("time", [t0, [t0]]) def test_sidereal(time): # http://www.jgiesen.de/astro/astroJS/siderealClock/ tsr = pmd.datetime2sidereal(time, radians(lon)) if isinstance(tsr, list): tsr = tsr[0] assert tsr == approx(sra, rel=1e-5) def test_anglesep(): pytest.importorskip("astropy") assert pmh.anglesep(35, 23, 84, 20) == approx(ha) def test_anglesep_meeus(): # %% compare with astropy assert pmh.anglesep_meeus(35, 23, 84, 20) == approx(ha) pymap3d-3.1.0/src/pymap3d/tests/test_sky.py000077500000000000000000000014721456201517100206260ustar00rootroot00000000000000from datetime import datetime import pymap3d as pm import pytest from pytest import approx lat, lon = (65, -148) lla0 = (42, -82, 200) azel = (180.1, 80) t0 = datetime(2014, 4, 6, 8) radec = (166.5032081149338, 55.000011165405752) def test_azel2radec(): radec1 = pm.azel2radec(*azel, lat, lon, t0) assert radec1 == approx(radec, rel=0.01) def test_numpy_azel2radec(): 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(): azel1 = pm.radec2azel(*radec, lat, lon, t0) assert azel1 == approx(azel, rel=0.01) def test_numpy_radec2azel(): pytest.importorskip("numpy") azel1 = pm.radec2azel([166.503208, 166.503208], [55, 55], lat, lon, t0) assert azel1 == approx(azel, rel=0.01) pymap3d-3.1.0/src/pymap3d/tests/test_spherical.py000077500000000000000000000052271456201517100217740ustar00rootroot00000000000000import pytest from pytest import approx try: from numpy import asarray except ImportError: def asarray(*args): # type: ignore "dummy function to convert values to arrays" return args import pymap3d as pm ELL = pm.Ellipsoid.from_name("wgs84") A = ELL.semimajor_axis B = ELL.semiminor_axis llrlla = [ ((0, 0, A - 1), (0, 0, -1)), ((0, 90, A - 1), (0, 90, -1)), ((0, -90, A + 1), (0, -90, 1)), ((44.807576814237606, 270, 6367490.543857), (45, 270, 1)), ((90, 0, B + 1), (90, 0, 1)), ((90, 15, B - 1), (90, 15, -1)), ((-90, 0, B + 1), (-90, 0, 1)), ] llallr = [ ((0, 0, -1), (0, 0, A - 1)), ((0, 90, -1), (0, 90, A - 1)), ((0, -90, 1), (0, -90, A + 1)), ((45, 270, 1), (44.807576814237606, 270, 6367490.543857)), ((90, 0, 1), (90, 0, B + 1)), ((90, 15, -1), (90, 15, B - 1)), ((-90, 0, 1), (-90, 0, B + 1)), ] 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] atol_dist = 1e-6 # 1 micrometer @pytest.mark.parametrize("lla, llr", llallr) def test_geodetic2spherical(lla, llr): 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): 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): pytest.importorskip("numpy") 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_list) def test_spherical2geodetic_list(llr, lla): pytest.importorskip("numpy") 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_array) def test_geodetic2spherical_array(lla, llr): pytest.importorskip("numpy") 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_array) def test_spherical2geodetic_array(llr, lla): pytest.importorskip("numpy") coords = pm.spherical2geodetic(*llr) assert coords[:2] == approx(lla[:2]) assert coords[2] == approx(lla[2], abs=atol_dist) pymap3d-3.1.0/src/pymap3d/tests/test_time.py000077500000000000000000000024761456201517100207630ustar00rootroot00000000000000from datetime import datetime import pymap3d.sidereal as pms import pytest from pymap3d.timeconv import str2dt from pytest import approx t0 = datetime(2014, 4, 6, 8) def test_juliantime(): assert pms.juliandate(t0) == approx(2.456753833333e6) def test_types(): 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() def test_datetime64(): np = pytest.importorskip("numpy") t1 = np.datetime64(t0) assert str2dt(t1) == t0 t1 = np.array([np.datetime64(t0), np.datetime64(t0)]) assert (str2dt(t1) == t0).all() def test_xarray_time(): xarray = pytest.importorskip("xarray") t = {"time": t0} ds = xarray.Dataset(t) assert str2dt(ds["time"]) == t0 t2 = {"time": [t0, t0]} ds = xarray.Dataset(t2) assert (str2dt(ds["time"]) == t0).all() def test_pandas_time(): pandas = pytest.importorskip("pandas") t = pandas.Series(t0) assert (str2dt(t) == t0).all() t = pandas.Series([t0, t0]) assert (str2dt(t) == t0).all() pymap3d-3.1.0/src/pymap3d/tests/test_vincenty.py000077500000000000000000000011441456201517100216530ustar00rootroot00000000000000import pymap3d.vincenty as vincenty import pytest from pytest import approx @pytest.mark.parametrize("deg", [True, False]) def test_track2_unit(deg): np = pytest.importorskip("numpy") lat1, lon1 = 0.0, 80.0 lat2, lon2 = 0.0, 81.0 lat0 = [0.0, 0.0, 0.0, 0.0] lon0 = [80.0, 80.33333, 80.66666, 81.0] if not deg: lat1, lon1, lat2, lon2 = np.radians((lat1, lon1, lat2, lon2)) lat0 = np.radians(lat0) lon0 = np.radians(lon0) lats, lons = vincenty.track2(lat1, lon1, lat2, lon2, npts=4, deg=deg) assert lats == approx(lat0) assert lons == approx(lon0) pymap3d-3.1.0/src/pymap3d/tests/test_vincenty_dist.py000066400000000000000000000026421456201517100226770ustar00rootroot00000000000000import pymap3d.vincenty as vincenty import pytest from pytest import approx @pytest.mark.parametrize( "lat,lon,lat1,lon1,srange,az", [ (0, 0, 0, 0, 0, 0), (0, 0, 0, 90, 1.001875e7, 90), (0, 0, 0, -90, 1.001875e7, 270), (0, 0, 0, 180, 2.00375e7, 90), (0, 0, 0, -180, 2.00375e7, 90), (0, 0, 0, 4, 445277.96, 90), (0, 0, 0, 5, 556597.45, 90), (0, 0, 0, 6, 667916.94, 90), (0, 0, 0, -6, 667916.94, 270), (0, 0, 0, 7, 779236.44, 90), (1e-16, 1e-16, 1e-16, 1, 111319.49, 90), (90, 0, 0, 0, 1.00019657e7, 180), (90, 0, -90, 0, 2.000393145e7, 180), ], ) def test_unit_vdist(lat, lon, lat1, lon1, srange, az): dist, az1 = vincenty.vdist(lat, lon, lat1, lon1) assert dist == approx(srange, rel=0.005) assert az1 == approx(az) assert isinstance(dist, float) assert isinstance(az1, float) def test_vector(): pytest.importorskip("numpy") asr, aaz = vincenty.vdist(10, 20, [10.02137267, 10.01917819], [20.0168471, 20.0193493]) assert 3e3 == approx(asr) assert aaz == approx([38, 45]) @pytest.mark.parametrize("lat,lon,slantrange,az", [(10, 20, 3e3, 38), (0, 0, 0, 0)]) def test_identity(lat, lon, slantrange, az): lat1, lon1 = vincenty.vreckon(lat, lon, slantrange, az) dist, az1 = vincenty.vdist(lat, lon, lat1, lon1) assert dist == approx(slantrange) assert az1 == approx(az) pymap3d-3.1.0/src/pymap3d/tests/test_vincenty_vreckon.py000066400000000000000000000026371456201517100234070ustar00rootroot00000000000000from math import radians import pymap3d.vincenty as vincenty import pytest from pytest import approx ll0 = [10, 20] lat2 = [10.02137267, 10.01917819] lon2 = [20.0168471, 20.0193493] az2 = [218.00292856, 225.00336316] sr1 = [3e3, 1e3] az1 = [38, 45] lat3 = (10.02137267, 10.00639286) lon3 = (20.0168471, 20.00644951) az3 = (218.00292856, 225.0011203) @pytest.mark.parametrize("deg", [True, False]) @pytest.mark.parametrize( "lat,lon,srange,az,lato,lono", [ (0, 0, 0, 0, 0, 0), (0, 0, 1.001875e7, 90, 0, 90), (0, 0, 1.001875e7, 270, 0, 270), (0, 0, 1.001875e7, -90, 0, 270), (0, 0, 2.00375e7, 90, 0, 180), (0, 0, 2.00375e7, 270, 0, 180), (0, 0, 2.00375e7, -90, 0, 180), ], ) def test_vreckon_unit(deg, lat, lon, srange, az, lato, lono): if not deg: lat, lon, az, lato, lono = map(radians, (lat, lon, az, lato, lono)) lat1, lon1 = vincenty.vreckon(lat, lon, srange, az, deg=deg) assert lat1 == approx(lato) assert isinstance(lat1, float) assert lon1 == approx(lono, rel=0.001) assert isinstance(lon1, float) def test_az_vector(): pytest.importorskip("numpy") a, b = vincenty.vreckon(*ll0, sr1[0], az1) assert a == approx(lat2) assert b == approx(lon2) def test_both_vector(): pytest.importorskip("numpy") a, b = vincenty.vreckon(10, 20, sr1, az1) assert a == approx(lat3) assert b == approx(lon3) pymap3d-3.1.0/src/pymap3d/timeconv.py000066400000000000000000000024511456201517100174360ustar00rootroot00000000000000# Copyright (c) 2014-2018 Michael Hirsch, Ph.D. """ convert strings to datetime """ from __future__ import annotations from datetime import datetime try: import dateutil.parser except ImportError: pass __all__ = ["str2dt"] def str2dt(time: str | datetime) -> datetime: """ Converts times in string or list of strings to datetime(s) Parameters ---------- time : str or datetime.datetime or numpy.datetime64 Results ------- t : datetime.datetime """ if isinstance(time, datetime): return time elif isinstance(time, str): try: return dateutil.parser.parse(time) except NameError: 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 python-dateutil") # pandas/xarray try: return time.values.astype("datetime64[us]").astype(datetime) except AttributeError: pass # Numpy.datetime64 try: return time.astype(datetime) except AttributeError: pass return time pymap3d-3.1.0/src/pymap3d/utils.py000066400000000000000000000015241456201517100167520ustar00rootroot00000000000000"""Utility functions all assume radians""" from __future__ import annotations from .mathfun import atan2, cos, hypot, sin __all__ = ["cart2pol", "pol2cart", "cart2sph", "sph2cart"] def cart2pol(x, y) -> tuple: """Transform Cartesian to polar coordinates""" return atan2(y, x), hypot(x, y) def pol2cart(theta, rho) -> tuple: """Transform polar to Cartesian coordinates""" return rho * cos(theta), rho * sin(theta) def cart2sph(x, y, z) -> tuple: """Transform Cartesian to spherical coordinates""" hxy = hypot(x, y) r = hypot(hxy, z) el = atan2(z, hxy) az = atan2(y, x) return az, el, r def sph2cart(az, el, r) -> tuple: """Transform spherical to Cartesian coordinates""" rcos_theta = r * cos(el) x = rcos_theta * cos(az) y = rcos_theta * sin(az) z = r * sin(el) return x, y, z pymap3d-3.1.0/src/pymap3d/vallado.py000066400000000000000000000062601456201517100172360ustar00rootroot00000000000000""" converts right ascension, declination to azimuth, elevation and vice versa. Normally do this via AstroPy. These functions are fallbacks for those wihtout AstroPy. Michael Hirsch implementation of algorithms from D. Vallado """ from __future__ import annotations from datetime import datetime from .mathfun import asin, atan2, cos, degrees, radians, sin from .sidereal import datetime2sidereal __all__ = ["azel2radec", "radec2azel"] def azel2radec( az_deg: float, el_deg: float, lat_deg: float, lon_deg: float, time: datetime, ) -> tuple[float, float]: """ converts azimuth, elevation to right ascension, declination Parameters ---------- az_deg : float azimuth (clockwise) to point [degrees] el_deg : float elevation above horizon to point [degrees] lat_deg : float observer WGS84 latitude [degrees] lon_deg : float observer WGS84 longitude [degrees] time : datetime.datetime time of observation Results ------- ra_deg : float right ascension to target [degrees] dec_deg : float declination of target [degrees] from D.Vallado Fundamentals of Astrodynamics and Applications p.258-259 """ if abs(lat_deg) > 90: raise ValueError("-90 <= lat <= 90") az = radians(az_deg) el = radians(el_deg) lat = radians(lat_deg) lon = radians(lon_deg) # %% Vallado "algorithm 28" p 268 dec = asin(sin(el) * sin(lat) + cos(el) * cos(lat) * cos(az)) lha = atan2( -(sin(az) * cos(el)) / cos(dec), (sin(el) - sin(lat) * sin(dec)) / (cos(dec) * cos(lat)) ) lst = datetime2sidereal(time, lon) # lon, ra in RADIANS """ by definition right ascension [0, 360) degrees """ return degrees(lst - lha) % 360, degrees(dec) def radec2azel( ra_deg: float, dec_deg: float, lat_deg: float, lon_deg: float, time: datetime, ) -> tuple[float, float]: """ converts right ascension, declination to azimuth, elevation Parameters ---------- ra_deg : float right ascension to target [degrees] dec_deg : float declination to target [degrees] lat_deg : float observer WGS84 latitude [degrees] lon_deg : float observer WGS84 longitude [degrees] time : datetime.datetime time of observation Results ------- az_deg : float azimuth clockwise from north to point [degrees] el_deg : float elevation above horizon to point [degrees] from D. Vallado "Fundamentals of Astrodynamics and Applications " 4th Edition Ch. 4.4 pg. 266-268 """ if abs(lat_deg) > 90: raise ValueError("-90 <= lat <= 90") ra = radians(ra_deg) dec = radians(dec_deg) lat = radians(lat_deg) lon = radians(lon_deg) lst = datetime2sidereal(time, lon) # RADIANS # %% Eq. 4-11 p. 267 LOCAL HOUR ANGLE lha = lst - ra # %% #Eq. 4-12 p. 267 el = asin(sin(lat) * sin(dec) + cos(lat) * cos(dec) * cos(lha)) # %% combine Eq. 4-13 and 4-14 p. 268 az = atan2( -sin(lha) * cos(dec) / cos(el), (sin(dec) - sin(el) * sin(lat)) / (cos(el) * cos(lat)) ) return degrees(az) % 360.0, degrees(el) pymap3d-3.1.0/src/pymap3d/vincenty.py000066400000000000000000000434541456201517100174610ustar00rootroot00000000000000""" Vincenty's methods for computing ground distance and reckoning """ from __future__ import annotations import warnings import logging from copy import copy from math import nan, pi, tau try: from numpy import atleast_1d except ImportError: pass from .ellipsoid import Ellipsoid from .mathfun import ( asin, atan, atan2, cos, degrees, isnan, linspace, radians, sign, sin, sqrt, tan, ) __all__ = ["vdist", "vreckon", "track2"] ELL = Ellipsoid.from_name("wgs84") def vdist(Lat1, Lon1, Lat2, Lon2, ell: Ellipsoid = ELL, deg: bool = True) -> tuple: """ Using the reference ellipsoid, compute the distance between two points within a few millimeters of accuracy, compute forward azimuth, and compute backward azimuth, all using a vectorized version of Vincenty's algorithm: Example: dist_m, azimuth_deg = vdist(lat1, lon1, lat2, lon2, ell) Parameters ---------- Lat1 : float Geodetic latitude of first point (degrees) Lon1 : float Geodetic longitude of first point (degrees) Lat2 : float Geodetic latitude of second point (degrees) Lon2 : float Geodetic longitude of second point (degrees) ell : Ellipsoid, optional reference ellipsoid Results ------- dist_m : float distance (meters) az : float azimuth (degrees) clockwise from first point to second point (forward) Original algorithm source: T. Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid with Application of Nested Equations", Survey Review, vol. 23, no. 176, April 1975, pp 88-93. Available at: http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf Notes: 1. lat1,lon1,lat2,lon2 can be any (identical) size/shape. Outputs will have the same size and shape. 2. Error correcting code, convergence failure traps, antipodal corrections, polar error corrections, WGS84 ellipsoid parameters, testing, and comments: Michael Kleder, 2004. 3. Azimuth implementation (including quadrant abiguity resolution) and code vectorization, Michael Kleder, Sep 2005. 4. Vectorization is convergence sensitive; that is, quantities which have already converged to within tolerance are not recomputed during subsequent iterations (while other quantities are still converging). 5. Vincenty describes his distance algorithm as precise to within 0.01 millimeters, subject to the ellipsoidal model. 6. For distance calculations, essentially antipodal points are treated as exactly antipodal, potentially reducing accuracy slightly. 7. Distance failures for points exactly at the poles are eliminated by moving the points by 0.6 millimeters. 8. The Vincenty distance algorithm was transcribed verbatim by Peter Cederholm, August 12, 2003. It was modified and translated to English by Michael Kleder. Mr. Cederholm's website is http://www.plan.aau.dk/~pce/ 9. Distances agree with the Mapping Toolbox, version 2.2 (R14SP3) with a max relative difference of about 5e-9, except when the two points are nearly antipodal, and except when one point is near the equator and the two longitudes are nearly 180 degrees apart. This function (vdist) is more accurate in such cases. For example, note this difference (as of this writing): ```python vdist(0.2,305,15,125) ``` > 18322827.0131551 ```python distance(0.2,305,15,125,[6378137 0.08181919]) ``` > 0 10. Azimuths FROM the north pole (either forward starting at the north pole or backward when ending at the north pole) are set to 180 degrees by convention. Azimuths FROM the south pole are set to 0 degrees by convention. 11. Azimuths agree with the Mapping Toolbox, version 2.2 (R14SP3) to within about a hundred-thousandth of a degree, except when traversing to or from a pole, where the convention for this function is described in (10), and except in the cases noted above in (9). 12. No warranties; use at your own risk. """ # %% Input check: try: Lat1 = atleast_1d(Lat1) Lon1 = atleast_1d(Lon1) Lat2 = atleast_1d(Lat2) Lon2 = atleast_1d(Lon2) except NameError: pass # %% Supply WGS84 earth ellipsoid axis lengths in meters: a = ell.semimajor_axis b = ell.semiminor_axis f = ell.flattening if deg: Lat1 = radians(Lat1) Lon1 = radians(Lon1) Lat2 = radians(Lat2) Lon2 = radians(Lon2) # keep old variable names in case someone is using them lat1, lon1, lat2, lon2 = Lat1, Lon1, Lat2, Lon2 try: if (abs(lat1) > pi / 2).any() | (abs(lat2) > pi / 2).any(): raise ValueError("Input latitudes must be in [-90, 90] degrees.") except AttributeError: if abs(lat1) > pi / 2 or abs(lat2) > pi / 2: raise ValueError("Input latitudes must be in [-90, 90] degrees.") # %% correct for errors at exact poles by adjusting 0.6 millimeters: try: i = abs(pi / 2 - abs(lat1)) < 1e-10 lat1[i] = sign(lat1[i]) * (pi / 2 - 1e-10) i = abs(pi / 2 - abs(lat2)) < 1e-10 lat2[i] = sign(lat2[i]) * (pi / 2 - 1e-10) except TypeError: if abs(pi / 2 - abs(lat1)) < 1e-10: lat1 = sign(lat1) * (pi / 2 - 1e-10) if abs(pi / 2 - abs(lat2)) < 1e-10: lat2 = sign(lat2) * (pi / 2 - 1e-10) U1 = atan((1 - f) * tan(lat1)) U2 = atan((1 - f) * tan(lat2)) lon1 = lon1 % (2 * pi) lon2 = lon2 % (2 * pi) L = abs(lon2 - lon1) try: L[L > pi] = 2 * pi - L[L > pi] except TypeError: if L > pi: L = 2 * pi - L lamb = copy(L) # NOTE: program will fail without copy! itercount = 0 warninggiven = False notdone = True while notdone: # force at least one execution itercount += 1 if itercount > 50: if not warninggiven: logging.warning("Essentially antipodal points--precision may be reduced slightly.") lamb = pi break lambdaold = copy(lamb) sinsigma = sqrt( (cos(U2) * sin(lamb)) ** 2 + (cos(U1) * sin(U2) - sin(U1) * cos(U2) * cos(lamb)) ** 2 ) cossigma = sin(U1) * sin(U2) + cos(U1) * cos(U2) * cos(lamb) # eliminate rare imaginary portions at limit of numerical precision: sinsigma = sinsigma.real cossigma = cossigma.real sigma = atan2(sinsigma, cossigma) try: with warnings.catch_warnings(record=True): warnings.simplefilter("error") sinAlpha = cos(U1) * cos(U2) * sin(lamb) / sin(sigma) alpha = asin(sinAlpha) alpha[isnan(sinAlpha)] = 0 alpha[(sinAlpha > 1) | (abs(sinAlpha - 1) < 1e-16)] = pi / 2 except (ArithmeticError, RuntimeWarning, TypeError, ValueError): try: with warnings.catch_warnings(record=True): warnings.simplefilter("error") sinAlpha = cos(U1) * cos(U2) * sin(lamb) / sin(sigma) except (ArithmeticError, RuntimeWarning): sinAlpha = 0.0 if isnan(sinAlpha): alpha = 0.0 elif sinAlpha > 1 or abs(sinAlpha - 1) < 1e-16: alpha = pi / 2 else: alpha = asin(sinAlpha) cos2sigmam = cos(sigma) - 2 * sin(U1) * sin(U2) / cos(alpha) ** 2 C = f / 16 * cos(alpha) ** 2 * (4 + f * (4 - 3 * cos(alpha) ** 2)) lamb = L + (1 - C) * f * sin(alpha) * ( sigma + C * sin(sigma) * (cos2sigmam + C * cos(sigma) * (-1 + 2.0 * cos2sigmam**2)) ) # print(f'then, lambda(21752) = {lamb[21752],20}) # correct for convergence failure for essentially antipodal points try: i = (lamb > pi).any() except AttributeError: i = lamb > pi if i: logging.warning( "Essentially antipodal points encountered. Precision may be reduced slightly." ) warninggiven = True lambdaold = pi lamb = pi try: notdone = (abs(lamb - lambdaold) > 1e-12).any() except AttributeError: notdone = abs(lamb - lambdaold) > 1e-12 u2 = cos(alpha) ** 2 * (a**2 - b**2) / b**2 A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2))) B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2))) deltasigma = ( B * sin(sigma) * ( cos2sigmam + B / 4 * ( cos(sigma) * (-1 + 2 * cos2sigmam**2) - B / 6 * cos2sigmam * (-3 + 4 * sin(sigma) ** 2) * (-3 + 4 * cos2sigmam**2) ) ) ) dist_m = b * A * (sigma - deltasigma) # %% From point #1 to point #2 # correct sign of lambda for azimuth calcs: lamb = abs(lamb) try: i = sign(sin(lon2 - lon1)) * sign(sin(lamb)) < 0 lamb[i] = -lamb[i] except TypeError: if sign(sin(lon2 - lon1)) * sign(sin(lamb)) < 0: lamb = -lamb numer = cos(U2) * sin(lamb) denom = cos(U1) * sin(U2) - sin(U1) * cos(U2) * cos(lamb) a12 = atan2(numer, denom) a12 %= tau if deg: a12 = degrees(a12) try: return dist_m.squeeze()[()], a12.squeeze()[()] except AttributeError: return dist_m, a12 def vreckon(Lat1, Lon1, Rng, Azim, ell: Ellipsoid = ELL, deg: bool = True) -> tuple: """ This is the Vincenty "forward" solution. Computes points at a specified azimuth and range in an ellipsoidal earth. Using the reference ellipsoid, travel a given distance along a given azimuth starting at a given initial point, and return the endpoint within a few millimeters of accuracy, using Vincenty's algorithm. Example: lat2, lon2 = vreckon(lat1, lon1, ground_range_m, azimuth_deg) Parameters ---------- Lat1 : float inital geodetic latitude (degrees) Lon1 : float initial geodetic longitude (degrees) Rng : float ground distance (meters) Azim : float intial azimuth (degrees) clockwide from north. ell : Ellipsoid, optional reference ellipsoid deg : bool, optional angular units degrees (default) or radians Results ------- Lat2 : float final geodetic latitude (degrees) Lon2 : float final geodetic longitude (degrees) Original algorithm: T. Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid with Application of Nested Equations", Survey Review, vol. 23, no. 176, April 1975, pp 88-93. http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf Notes: 1. The Vincenty reckoning algorithm was transcribed verbatim into JavaScript by Chris Veness. It was modified and translated to Matlab by Michael Kleder. Mr. Veness's website is: http://www.movable-type.co.uk/scripts/latlong-vincenty-direct.html 2. Error correcting code, polar error corrections, WGS84 ellipsoid parameters, testing, and comments by Michael Kleder. 3. By convention, when starting at a pole, the longitude of the initial point (otherwise meaningless) determines the longitude line along which to traverse, and hence the longitude of the final point. 4. The convention noted in (3) above creates a discrepancy with VDIST when the the intial or final point is at a pole. In the VDIST function, when traversing from a pole, the azimuth is 0 when heading away from the south pole and 180 when heading away from the north pole. In contrast, this VRECKON function uses the azimuth as noted in (3) above when traversing away form a pole. 5. In testing, where the traversal subtends no more than 178 degrees, this function correctly inverts the VDIST function to within 0.2 millimeters of distance, 5e-10 degrees of forward azimuth, and 5e-10 degrees of reverse azimuth. Precision reduces as test points approach antipodal because the precision of VDIST is reduced for nearly antipodal points. (A warning is given by VDIST.) 6. Tested but no warranty. Use at your own risk. 7. Ver 1.0, Michael Kleder, November 2007. Ver 2.0, Joaquim Luis, September 2008 Added ellipsoid and vectorized whenever possible. Also, lon2 is always converted to the [-180 180] interval. Joaquim Luis """ try: Lat1 = atleast_1d(Lat1) Lon1 = atleast_1d(Lon1) Rng = atleast_1d(Rng) Azim = atleast_1d(Azim) if (Rng < 0.0).any(): raise ValueError("Ground distance must be positive") except NameError: if Rng < 0.0: raise ValueError("Ground distance must be positive") a = ell.semimajor_axis b = ell.semiminor_axis f = ell.flattening if deg: Lat1 = radians(Lat1) # intial latitude in radians Lon1 = radians(Lon1) # intial longitude in radians Azim = radians(Azim) # in case someone is using the old variable names lat1, lon1 = Lat1, Lon1 try: if (abs(lat1) > pi / 2).any(): raise ValueError("Input lat. must be between -90 and 90 deg., inclusive.") except AttributeError: if abs(lat1) > pi / 2: raise ValueError("Input lat. must be between -90 and 90 deg., inclusive.") # correct for errors at exact poles by adjusting 0.6 millimeters: try: i = abs(pi / 2 - abs(lat1)) < 1e-10 lat1[i] = sign(lat1[i]) * (pi / 2 - (1e-10)) except TypeError: if abs(pi / 2 - abs(lat1)) < 1e-10: lat1 = sign(lat1) * (pi / 2 - (1e-10)) alpha1 = Azim # inital azimuth in radians sinAlpha1 = sin(alpha1) cosAlpha1 = cos(alpha1) tanU1 = (1 - f) * tan(lat1) cosU1 = 1 / sqrt(1 + tanU1**2) sinU1 = tanU1 * cosU1 sigma1 = atan2(tanU1, cosAlpha1) sinAlpha = cosU1 * sinAlpha1 cosSqAlpha = 1 - sinAlpha * sinAlpha uSq = cosSqAlpha * (a**2 - b**2) / b**2 A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) sigma = Rng / (b * A) sigmaP = 2 * pi sinSigma = nan cosSigma = nan cos2SigmaM = nan try: i = (abs(sigma - sigmaP) > 1e-12).any() except AttributeError: i = abs(sigma - sigmaP) > 1e-12 while i: cos2SigmaM = cos(2 * sigma1 + sigma) sinSigma = sin(sigma) cosSigma = cos(sigma) deltaSigma = ( B * sinSigma * ( cos2SigmaM + B / 4 * ( cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM) ) ) ) sigmaP = sigma sigma = Rng / (b * A) + deltaSigma try: i = (abs(sigma - sigmaP) > 1e-12).any() except AttributeError: i = abs(sigma - sigmaP) > 1e-12 tmp = sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1 lat2 = atan2( sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1, (1 - f) * sqrt(sinAlpha * sinAlpha + tmp**2), ) lamb = atan2(sinSigma * sinAlpha1, cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1) C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha)) L = lamb - ( f * (1 - C) * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM))) ) lon2 = lon1 + L # Truncates angles into the [-pi pi] range # if lon2 > pi: # lon2 = pi*((absolute(lon2)/pi) - # 2*ceil(((absolute(lon2)/pi)-1)/2)) * sign(lon2) lon2 %= tau # follow [0, 360) convention if deg: lat2 = degrees(lat2) lon2 = degrees(lon2) try: return lat2.squeeze()[()], lon2.squeeze()[()] except AttributeError: return lat2, lon2 def track2( lat1, lon1, lat2, lon2, ell: Ellipsoid = ELL, npts: int = 100, deg: bool = True, ) -> tuple[list, list]: """ computes great circle tracks starting at the point lat1, lon1 and ending at lat2, lon2 Parameters ---------- lat1 : float Geodetic latitude of first point (degrees) lon1 : float Geodetic longitude of first point (degrees) lat2 : float Geodetic latitude of second point (degrees) lon2 : float Geodetic longitude of second point (degrees) ell : Ellipsoid, optional reference ellipsoid npts : int, optional number of points (default is 100) deg : bool, optional angular units degrees (default) or radians Results ------- lats : list of float latitudes of points along track lons : list of float longitudes of points along track Based on code posted to the GMT mailing list in Dec 1999 by Jim Levens and by Jeff Whitaker """ if npts < 2: raise ValueError("npts must be greater than 1") if npts == 2: return [lat1, lat2], [lon1, lon2] if deg: lat1 = radians(lat1) lon1 = radians(lon1) lat2 = radians(lat2) lon2 = radians(lon2) gcarclen = 2.0 * asin( sqrt((sin((lat1 - lat2) / 2)) ** 2 + cos(lat1) * cos(lat2) * (sin((lon1 - lon2) / 2)) ** 2) ) # check to see if points are antipodal (if so, route is undefined). if abs(gcarclen - pi) < 1e-12: raise ValueError( "cannot compute intermediate points on a great circle whose endpoints are antipodal" ) distance, azimuth = vdist(lat1, lon1, lat2, lon2, ell, deg=False) dists = linspace(0, distance, npts) lats, lons = vreckon(lat1, lon1, dists, azimuth, ell, deg=False) if deg: lats = degrees(lats) lons = degrees(lons) return lats, lons