diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index eb288386..00000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[bumpversion] -current_version = 2.6.0 -commit = True -tag = True - -[bumpversion:file:setup.py] - -[bumpversion:file:docs/conf.py] - -[bumpversion:file:aacgmv2/__init__.py] - diff --git a/.cookiecutterrc b/.cookiecutterrc deleted file mode 100644 index b4f1dc90..00000000 --- a/.cookiecutterrc +++ /dev/null @@ -1,34 +0,0 @@ -# This file exists so you can easily regenerate your project. -# -# Unfortunately cookiecutter can't use this right away so -# you have to copy this file to ~/.cookiecutterrc - -default_context: - - appveyor: 'yes' - c_extension_optional: 'no' - c_extension_support: 'yes' - codacy: 'yes' - codeclimate: 'yes' - codecov: 'yes' - command_line_interface: 'no' - coveralls: 'yes' - distribution_name: 'aacgmv2' - email: 'agb073000@utdallas.edu' - full_name: 'Angeline G. Burrell' - github_username: 'aburrell' - landscape: 'yes' - package_name: 'aacgmv2' - project_name: 'AACGM-v2 Python library' - project_short_description: '"A Python wrapper for AACGM-v2 magnetic coordinates"' - release_date: '2018-03-12' - repo_name: 'aacgmv2' - requiresio: 'yes' - scrutinizer: 'yes' - sphinx_theme: 'readthedocs' - test_matrix_configurator: 'yes' - test_runner: 'pytest' - travis: 'yes' - version: '2.0.1' - website: 'https://github.com/aburrell/aacgmv2' - year: '2018' diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 8a55c3fe..00000000 --- a/.coveragerc +++ /dev/null @@ -1,16 +0,0 @@ -[paths] -source = - aacgmv2 - c_aacgmv2 - -[run] -branch = True -source = - aacgmv2 - c_aacgmv2 -parallel = true - -[report] -show_missing = true -precision = 2 -omit = *migrations* diff --git a/.landscape.yaml b/.landscape.yaml deleted file mode 100644 index e8c75d79..00000000 --- a/.landscape.yaml +++ /dev/null @@ -1,7 +0,0 @@ -max-line-length: 140 -doc-warnings: yes -ignore-paths: - - docs - - ci - - aacgmv2/tests - - setup.py \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 36605cee..9d63669d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,22 +4,24 @@ python: - "3.6" - "3.7" - "3.8" -sudo: false env: global: - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so before_install: - python --version - uname -a - lsb_release -a install: - - pip install coveralls - - "python setup.py install" - - pip install tox-travis + - pip install numpy tox-travis coveralls + - python setup.py install script: - - tox - - coverage run --source aacgmv2 -m py.test -after_sucess: coveralls + - | + if [ $TRAVIS_PYTHON_VERSION == "3.7" ]; then + tox -e check,docs + fi + - tox -e $TRAVIS_PYTHON_VERSION +after_success: + - coveralls --rcfile=setup.cfg notifications: email: on_success: never diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 51ad468a..17ed0e7c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,25 @@ Changelog ========= + +2.6.1 (2020-09-11) +------------------ + +* Moved formerly deprecated utilities from `deprecated.py` to `utils.py` +* Removed allowance for deprecated kwarg `code` from `convert_latlon` and + `convert_latlon_arr`, as scheduled +* Updated CI to include python 3.8 everywhere +* Moved all configuration information to setup.cfg +* Fixed coveralls implementation +* Fixed broken links in the documentation +* Removed unused code analysis tools +* Improved unit test coverage +* Make PEP8 changes + + 2.6.0 (2020-01-06) ------------------------------------------ +------------------ + * Updated AACGM-v2 coefficients derived using the IGRF13 model * Updated IGRF and GUFM1 coefficients using the IGRF13 model * Added additional checks to the C code for reading the IGRF13 coefficient file @@ -15,7 +32,8 @@ Changelog 2.5.3 (2019-12-23) ------------------------------------------ +------------------ + * Changed log warning about array functions to info * Changed default method from `TRACE` to `ALLOWTRACE` * Added C wrappers for list input, removing inefficient use of `np.vectorize` @@ -28,7 +46,8 @@ Changelog 2.5.2 (2019-08-27) ------------------------------------------ +------------------ + * Added FutureWarning to deprecated functions * Updated names in licenses * Moved module structure routine tests to their own class @@ -41,14 +60,16 @@ Changelog 2.5.1 (2018-10-19) ------------------------------------------ +------------------ + * Commented out debug statement in C code * Updated environment variable warning to output to stderr instead of stdout * Added templates for pull requests, issues, and a code of conduct 2.5.0 (2018-08-08) ------------------------------------------ +------------------ + * Updated C code and coefficients to version 2.5. Changes in python code reflect changes in C code (includes going back to using environment variables instead of strings for coefficient file locations) @@ -58,20 +79,23 @@ Changelog 2.4.2 (2018-05-21) ------------------------------------------ +------------------ + * Fixed bug in convert_mlt that caused all time inputs to occur at 00:00:00 UT * Fixed year of last two updates in changelog 2.4.1 (2018-04-04) ------------------------------------------ +------------------ + * Fix bug in installation that caused files to be placed in the wrong directory * Added DOI + 2.4.0 (2018-03-21) ------------------------------------------ +------------------ * Update to use AACGM-v2.4, which includes changes to the inverse MLT and dipole tilt functions and some minor bug fixes @@ -81,38 +105,41 @@ Changelog * Updated dependencies, removing support for python 3.3 * Tested on Mac OSX * Updated comments to include units for input and output - + + 2.0.0 (2016-11-03) ------------------------------------------ +------------------ * Change method of calculating MLT, see documentation of convert_mlt for details 1.0.13 (2015-10-30) ------------------------------------------ +------------------- -* Correctly convert output of subsol() to geodetic coordinates (the error in MLT/mlon conversion was not large, typically two decimal places and below) +* Correctly convert output of subsol() to geodetic coordinates (the error in + MLT/mlon conversion was not large, typically two decimal places and below) 1.0.12 (2015-10-26) ------------------------------------------ +------------------- * Return nan in forbidden region instead of throwing exception 1.0.11 (2015-10-26) ------------------------------------------ +------------------- * Fix bug in subsolar/MLT conversion 1.0.10 (2015-10-08) ------------------------------------------ +------------------- -* No code changes, debugged automatic build/upload process and needed new version numbers along the way +* No code changes, debugged automatic build/upload process and needed new + version numbers along the way 1.0.0 (2015-10-07) ------------------------------------------ +------------------ * Initial release diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 581246ad..716503f3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -63,7 +63,7 @@ To set up `aacgmv2` for local development: ``assert`` statement for testing output, or use the numpy testing suite. 4. When you're done making changes, run all the checks, doc builder and spell - checker with `tox `_ [1]_:: + checker with `tox `_ [1]_:: tox diff --git a/MANIFEST.in b/MANIFEST.in index e75a7c3b..7af552c3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,11 +7,7 @@ recursive-include aacgmv2 *.asc recursive-include aacgmv2 *.txt recursive-include c_aacgmv2 *.txt -include .bumpversion.cfg -include .coveragerc -include .cookiecutterrc -include .isort.cfg -include .pylintrc +include setup.cfg include AUTHORS.rst include CHANGELOG.rst @@ -21,6 +17,8 @@ include LICENSE-AstAlg.txt include README.rst include CODE_OF_CONDUCT.md -include tox.ini .travis.yml appveyor.yml .codeclimate.yml .landscape.yaml +include tox.ini +include *.yml +include .*.yml global-exclude *.py[cod] __pycache__ *.so *.dylib *.dSYM .pytest_cache diff --git a/README.rst b/README.rst index 4a4780cc..1de973fa 100644 --- a/README.rst +++ b/README.rst @@ -53,7 +53,7 @@ in the full documentation. Documentation ============= -https://aacgmv2.readthedocs.org/ +https://aacgmv2.readthedocs.io/en/latest/ http://superdarn.thayer.dartmouth.edu/aacgm.html @@ -67,8 +67,7 @@ Badges - |docs| * - tests - | |travis| |appveyor| |requires| - | |landscape| |codeclimate| - | |scrutinizer| |codacy| + | |codeclimate| |scrutinizer| |codacy| |coveralls| * - package - | |version| |supported-versions| | |wheel| |supported-implementations| @@ -77,9 +76,9 @@ Badges :target: https://readthedocs.org/projects/aacgmv2 :alt: Documentation Status -.. |travis| image:: https://travis-ci.org/aburrell/aacgmv2.svg?branch=master +.. |travis| image:: https://api.travis-ci.org/aburrell/aacgmv2.svg?branch=master :alt: Travis-CI Build Status - :target: https://travis-ci.org/aburrell/aacgmv2 + :target: https://travis-ci.org/github/aburrell/aacgmv2 .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/aburrell/aacgmv2?branch=master&svg=true :alt: AppVeyor Build Status @@ -89,48 +88,42 @@ Badges :alt: Requirements Status :target: https://requires.io/github/aburrell/aacgmv2/requirements/?branch=master -.. |coveralls| image:: https://coveralls.io/repos/aburrell/aacgmv2/badge.svg?branch=master&service=github - :alt: Coverage Status +.. |coveralls| image:: https://coveralls.io/repos/github/aburrell/aacgmv2/badge.svg + :alt: Coverage Status (Coveralls) :target: https://coveralls.io/github/aburrell/aacgmv2 -.. |codecov| image:: https://codecov.io/github/aburrell/aacgmv2/coverage.svg?branch=master - :alt: Coverage Status - :target: https://codecov.io/github/aburrell/aacgmv2 - -.. |landscape| image:: https://landscape.io/github/aburrell/aacgmv2/master/landscape.svg?style=flat - :target: https://landscape.io/github/aburrell/aacgmv2/master - :alt: Code Quality Status - .. |codacy| image:: https://api.codacy.com/project/badge/Grade/b64ee44194f148f5bdb0f00c7cf16ab8 - :target: https://www.codacy.com/app/aburrell/aacgmv2?utm_source=github.com&utm_medium=referral&utm_content=aburrell/aacgmv2&utm_campaign=Badge_Grade - :alt: Codacy Code Quality Status + :alt: Codacy Code Quality Status + :target: https://www.codacy.com/manual/aburrell/aacgmv2?utm_source=github.com&utm_medium=referral&utm_content=aburrell/aacgmv2&utm_campaign=Badge_Grade -.. |codeclimate| image:: https://codeclimate.com/github/aburrell/aacgmv2/badges/gpa.svg +.. |codeclimate| image:: https://api.codeclimate.com/v1/badges/91f5a91bf3d9ba90cb57/maintainability.svg :target: https://codeclimate.com/github/aburrell/aacgmv2 :alt: CodeClimate Quality Status + .. |version| image:: https://img.shields.io/pypi/v/aacgmv2.svg?style=flat :alt: PyPI Package latest release - :target: https://pypi.python.org/pypi/aacgmv2 + :target: https://pypi.org/project/aacgmv2/ .. |downloads| image:: https://img.shields.io/pypi/dm/aacgmv2.svg?style=flat :alt: PyPI Package monthly downloads - :target: https://pypi.python.org/pypi/aacgmv2 + :target: https://pypi.org/project/aacgmv2/ .. |wheel| image:: https://img.shields.io/pypi/wheel/aacgmv2.svg?style=flat :alt: PyPI Wheel - :target: https://pypi.python.org/pypi/aacgmv2 + :target: https://pypi.org/project/aacgmv2/ .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/aacgmv2.svg?style=flat :alt: Supported versions - :target: https://pypi.python.org/pypi/aacgmv2 + :target: https://pypi.org/project/aacgmv2/ .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/aacgmv2.svg?style=flat :alt: Supported implementations - :target: https://pypi.python.org/pypi/aacgmv2 + :target: https://pypi.org/project/aacgmv2/ -.. |scrutinizer| image:: https://img.shields.io/scrutinizer/g/aburrell/aacgmv2/master.svg?style=flat +.. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/aburrell/aacgmv2/master.svg?style=flat :alt: Scrutinizer Status :target: https://scrutinizer-ci.com/g/aburrell/aacgmv2/ -.. |doi| image:: https://zenodo.org/badge/42864636.svg - :target: https://zenodo.org/badge/latestdoi/42864636 +.. |doi| image:: https://zenodo.org/badge/doi/10.5281/zenodo.3598705.svg + :alt: DOI + :target: https://zenodo.org/record/3598705 diff --git a/aacgmv2/__init__.py b/aacgmv2/__init__.py index ca2e50de..eea0e409 100644 --- a/aacgmv2/__init__.py +++ b/aacgmv2/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019 NRL +# Copyright (C) 2019 NRL # Author: Angeline Burrell # Disclaimer: This code is under the MIT license, whose details can be found at # the root in the LICENSE file @@ -22,8 +22,6 @@ """ # Imports -#--------------------------------------------------------------------- - import logging import os as _os from sys import stderr @@ -31,25 +29,23 @@ from aacgmv2.wrapper import (convert_latlon, convert_mlt, get_aacgm_coord) from aacgmv2.wrapper import (convert_latlon_arr, get_aacgm_coord_arr) from aacgmv2.wrapper import (convert_bool_to_bit, convert_str_to_bit) +from aacgmv2 import (utils) from aacgmv2 import (deprecated) from aacgmv2 import (_aacgmv2) # Define global variables -#--------------------------------------------------------------------- - -__version__ = "2.6.0" +__version__ = "2.6.1" # Define a logger object to allow easier log handling logger = logging.getLogger('aacgmv2_logger') # Altitude constraints -high_alt_coeff = 2000.0 # Tested and published in Shepherd (2014) -high_alt_trace = 6378.0 # 1 RE, these are ionospheric coordinates +high_alt_coeff = 2000.0 # Tested and published in Shepherd (2014) +high_alt_trace = 6378.0 # 1 RE, these are ionospheric coordinates # path and filename prefix for the IGRF coefficients -AACGM_v2_DAT_PREFIX = _os.path.join(_os.path.realpath( \ - _os.path.dirname(__file__)), - 'aacgm_coeffs', 'aacgm_coeffs-13-') +AACGM_v2_DAT_PREFIX = _os.path.join(_os.path.realpath( + _os.path.dirname(__file__)), 'aacgm_coeffs', 'aacgm_coeffs-13-') IGRF_COEFFS = _os.path.join(_os.path.realpath(_os.path.dirname(__file__)), 'magmodel_1590-2020.txt') @@ -72,5 +68,6 @@ _os.environ['AACGM_v2_DAT_PREFIX'] = AACGM_v2_DAT_PREFIX if __reset_warn__: - stderr.write("non-default coefficient files may be specified by running " + - "aacgmv2.wrapper.set_coeff_path before any other functions\n") + stderr.write("".join(["non-default coefficient files may be specified by ", + "running aacgmv2.wrapper.set_coeff_path before any ", + "other functions\n"])) diff --git a/aacgmv2/__main__.py b/aacgmv2/__main__.py index de5c8a64..408d14c3 100644 --- a/aacgmv2/__main__.py +++ b/aacgmv2/__main__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019 NRL +# Copyright (C) 2019 NRL # Author: Angeline Burrell # Disclaimer: This code is under the MIT license, whose details can be found at # the root in the LICENSE file @@ -13,8 +13,6 @@ import numpy as np import sys -import warnings - import aacgmv2 if sys.version_info.major >= 3: @@ -91,8 +89,10 @@ def main(): array = np.loadtxt(args.file_in, ndmin=2) if args.subcommand == 'convert': - date = dt.date.today() if args.date is None else \ - dt.datetime.strptime(args.date, '%Y%m%d') + if args.date is None: + date = dt.date.today() + else: + date = dt.datetime.strptime(args.date, '%Y%m%d') code = aacgmv2.convert_bool_to_bit(a2g=args.a2g, trace=args.trace, allowtrace=args.allowtrace, badidea=args.badidea, diff --git a/aacgmv2/deprecated.py b/aacgmv2/deprecated.py index 378e115f..525842b2 100644 --- a/aacgmv2/deprecated.py +++ b/aacgmv2/deprecated.py @@ -1,208 +1,48 @@ -# Copyright (C) 2019 NRL +# Copyright (C) 2019 NRL # Author: Angeline Burrell # Disclaimer: This code is under the MIT license, whose details can be found at # the root in the LICENSE file # # -*- coding: utf-8 -*- -"""Pythonic wrappers for AACGM-V2 C functions that were depricated in the -change from version 2.0.0 to version 2.0.2 - -References -------------------------------------------------------------------------------- -Laundal, K. M. and A. D. Richmond (2016), Magnetic Coordinate Systems, Space - Sci. Rev., doi:10.1007/s11214-016-0275-y. +"""Pythonic wrappers for formerly deprecated AACGM-V2 C functions that were +moved to the default wrapper in version 2.6.1 """ -from __future__ import division, absolute_import, unicode_literals -import datetime as dt -import numpy as np +from __future__ import absolute_import, unicode_literals import warnings import aacgmv2 -dep_str = "".join(["Deprecated routine will be removed in version 2.6.1 ", - "unless users express interest in keeping it"]) - -def subsol(year, doy, utime): - """Finds subsolar geocentric longitude and latitude. - - Parameters - ------------ - year : (int) - Calendar year between 1601 and 2100 - doy : (int) - Day of year between 1-365/366 - utime : (float) - Seconds since midnight on the specified day - - Returns - --------- - sbsllon : (float) - Subsolar longitude in degrees E for the given date/time - sbsllat : (float) - Subsolar latitude in degrees N for the given date/time +dep_str = "".join(["Routine no longer deprecated, and so has been moved to ", + "new utils module. Duplicate routine in deprecated module", + " will be removed in version 2.7.0"]) - Notes - -------- - Based on formulas in Astronomical Almanac for the year 1996, p. C24. - (U.S. Government Printing Office, 1994). Usable for years 1601-2100, - inclusive. According to the Almanac, results are good to at least 0.01 - degree latitude and 0.025 degrees longitude between years 1950 and 2050. - Accuracy for other years has not been tested. Every day is assumed to have - exactly 86400 seconds; thus leap seconds that sometimes occur on December - 31 are ignored (their effect is below the accuracy threshold of the - algorithm). - After Fortran code by A. D. Richmond, NCAR. Translated from IDL - by K. Laundal. +def subsol(year, doy, utime): + """Deprecated call to aacgmv2.utils.subsol """ warnings.warn(dep_str, category=FutureWarning) - # Convert from 4 digit year to 2 digit year - yr2 = year - 2000 - - if year >= 2101: - aacgmv2.logger.error('subsol invalid after 2100. Input year is:', year) - - # Determine if this year is a leap year - nleap = np.floor((year - 1601) / 4) - nleap = nleap - 99 - if year <= 1900: - if year <= 1600: - print('subsol.py: subsol invalid before 1601. Input year is:', year) - ncent = np.floor((year - 1601) / 100) - ncent = 3 - ncent - nleap = nleap + ncent - - # Calculate some of the coefficients needed to deterimine the mean longitude - # of the sun and the mean anomaly - l_0 = -79.549 + (-0.238699 * (yr2 - 4 * nleap) + 3.08514e-2 * nleap) - g_0 = -2.472 + (-0.2558905 * (yr2 - 4 * nleap) - 3.79617e-2 * nleap) - - # Days (including fraction) since 12 UT on January 1 of IYR2: - dfrac = (utime / 86400 - 1.5) + doy - - # Mean longitude of Sun: - l_sun = l_0 + 0.9856474 * dfrac - - # Mean anomaly: - grad = np.radians(g_0 + 0.9856003 * dfrac) - - # Ecliptic longitude: - lmrad = np.radians(l_sun + 1.915 * np.sin(grad) + 0.020 * np.sin(2 * grad)) - sinlm = np.sin(lmrad) - - # Days (including fraction) since 12 UT on January 1 of 2000: - epoch_day = dfrac + 365.0 * yr2 + nleap - - # Obliquity of ecliptic: - epsrad = np.radians(23.439 - 4.0e-7 * epoch_day) - - # Right ascension: - alpha = np.degrees(np.arctan2(np.cos(epsrad) * sinlm, np.cos(lmrad))) - - # Declination, which is the subsolar latitude: - sbsllat = np.degrees(np.arcsin(np.sin(epsrad) * sinlm)) - - # Equation of time (degrees): - etdeg = l_sun - alpha - etdeg = etdeg - 360.0 * np.round(etdeg / 360.0) - - # Apparent time (degrees): - aptime = utime / 240.0 + etdeg # Earth rotates one degree every 240 s. - - # Subsolar longitude: - sbsllon = 180.0 - aptime - sbsllon = sbsllon - 360.0 * np.round(sbsllon / 360.0) + sbsllon, sbsllat = aacgmv2.utils.subsol(year, doy, utime) return sbsllon, sbsllat -def gc2gd_lat(gc_lat): - """Convert geocentric latitude to geodetic latitude using WGS84. - - Parameters - ----------- - gc_lat : (array_like or float) - Geocentric latitude in degrees N - Returns - --------- - gd_lat : (same as input) - Geodetic latitude in degrees N +def gc2gd_lat(gc_lat): + """Deprecated call to aacgmv2.utils.gc2gd_lat """ warnings.warn(dep_str, category=FutureWarning) - - wgs84_e2 = 0.006694379990141317 - 1.0 - return np.rad2deg(-np.arctan(np.tan(np.deg2rad(gc_lat)) / wgs84_e2)) - -def igrf_dipole_axis(date): - """Get Cartesian unit vector pointing at dipole pole in the north, - according to IGRF + gd_lat = aacgmv2.utils.gc2gd_lat(gc_lat) - Parameters - ------------- - date : (dt.datetime) - Date and time + return gd_lat - Returns - ---------- - m_0: (np.ndarray) - Cartesian 3 element unit vector pointing at dipole pole in the north - (geocentric coords) - Notes - ---------- - IGRF coefficients are read from the igrf12coeffs.txt file. It should also - work after IGRF updates. The dipole coefficients are interpolated to the - date, or extrapolated if date > latest IGRF model +def igrf_dipole_axis(date): + """Deprecated call to aacgmv2.utils.igrf_dipole_axis """ warnings.warn(dep_str, category=FutureWarning) - # get time in years, as float: - year = date.year - doy = date.timetuple().tm_yday - year_days = int(dt.date(date.year, 12, 31).strftime("%j")) - year = year + doy / year_days - - # read the IGRF coefficients - with open(aacgmv2.IGRF_COEFFS, 'r') as f_igrf: - lines = f_igrf.readlines() - - years = lines[3].split()[3:][:-1] - years = np.array(years, dtype=float) # time array - - g10 = lines[4].split()[3:] - g11 = lines[5].split()[3:] - h11 = lines[6].split()[3:] - - # secular variation coefficients (for extrapolation) - g10sv = np.float32(g10[-1]) - g11sv = np.float32(g11[-1]) - h11sv = np.float32(h11[-1]) - - # model coefficients: - g10 = np.array(g10[:-1], dtype=float) - g11 = np.array(g11[:-1], dtype=float) - h11 = np.array(h11[:-1], dtype=float) - - # get the gauss coefficient at given time: - if year <= years[-1]: - # regular interpolation - g10 = np.interp(year, years, g10) - g11 = np.interp(year, years, g11) - h11 = np.interp(year, years, h11) - else: - # extrapolation - dyear = year - years[-1] - g10 = g10[-1] + g10sv * dyear - g11 = g11[-1] + g11sv * dyear - h11 = h11[-1] + h11sv * dyear - - # calculate pole position - B_0 = np.sqrt(g10**2 + g11**2 + h11**2) - - # Calculate output - m_0 = -np.array([g11, h11, g10]) / B_0 + m_0 = aacgmv2.utils.igrf_dipole_axis(date) return m_0 diff --git a/aacgmv2/tests/test_environ_aacgmv2.py b/aacgmv2/tests/environ/test_environ_aacgmv2.py similarity index 71% rename from aacgmv2/tests/test_environ_aacgmv2.py rename to aacgmv2/tests/environ/test_environ_aacgmv2.py index b9ab9770..50f16907 100644 --- a/aacgmv2/tests/test_environ_aacgmv2.py +++ b/aacgmv2/tests/environ/test_environ_aacgmv2.py @@ -2,9 +2,11 @@ from __future__ import division, absolute_import, unicode_literals import os +import sys import pytest -@pytest.mark.skip(reason='only works for first import') + +@pytest.mark.xfail class TestPyEnviron: def setup(self): self.igrf_path = os.path.join("aacgmv2", "aacgmv2", @@ -41,6 +43,11 @@ def test_good_coeff(self, aacgm_test=None, igrf_test=None): if igrf_test.find(self.igrf_path) < 0: raise AssertionError('BAD IGRF PATH') + @pytest.mark.parametrize("coeff", [("aacgm_test"), ("igrf_test")]) + def test_bad_coeff(self, coeff): + """ Test the failure of the class routine 'test_good_coeff'""" + with pytest.raises(AssertionError, match="BAD"): + self.test_good_coeff(**{coeff: "bad path"}) def test_top_parameters_default(self): """Test default module coefficients""" @@ -52,45 +59,25 @@ def test_top_parameters_default(self): self.test_good_coeff(aacgmv2.AACGM_v2_DAT_PREFIX, aacgmv2.IGRF_COEFFS) assert not aacgmv2.__reset_warn__ + del sys.modules["aacgmv2"] del aacgmv2 - def test_top_parameters_reset_aacgm(self): - """Test module reset of AACGM coefficient path""" - - self.reset_evar(evar=['AACGM_v2_DAT_PREFIX']) - os.environ['AACGM_v2_DAT_PREFIX'] = 'test_prefix' - - import aacgmv2 - - self.test_good_coeff(aacgmv2.AACGM_v2_DAT_PREFIX, aacgmv2.IGRF_COEFFS) - - assert aacgmv2.__reset_warn__ - del aacgmv2 - - def test_top_parameters_reset_igrf(self): - """Test module reset of IGRF coefficient path""" - - self.reset_evar(evar=['IGRF_COEFFS']) - os.environ['IGRF_COEFFS'] = 'test_prefix' - - import aacgmv2 - - self.test_good_coeff(aacgmv2.AACGM_v2_DAT_PREFIX, aacgmv2.IGRF_COEFFS) - - assert aacgmv2.__reset_warn__ - del aacgmv2 - - def test_top_parameters_reset_both(self): - """Test module reset of both coefficient paths""" + @pytest.mark.parametrize("evars", [(["AACGM_v2_DAT_PREFIX"]), + (["AACGM_v2_DAT_PREFIX", "IGRF_COEFFS"]), + (["IGRF_COEFFS"])]) + def test_top_parameters_reset_evar_to_specified(self, evars): + """Test module reset of AACGM environment variables""" - os.environ['AACGM_v2_DAT_PREFIX'] = 'test_prefix1' - os.environ['IGRF_COEFFS'] = 'test_prefix2' + self.reset_evar(evar=evars) + for i, evar in enumerate(evars): + os.environ[evar] = 'test_prefix{:d}'.format(i) import aacgmv2 self.test_good_coeff(aacgmv2.AACGM_v2_DAT_PREFIX, aacgmv2.IGRF_COEFFS) assert aacgmv2.__reset_warn__ + del sys.modules["aacgmv2"] del aacgmv2 def test_top_parameters_set_same(self): @@ -108,4 +95,5 @@ def test_top_parameters_set_same(self): self.test_good_coeff(aacgmv2.AACGM_v2_DAT_PREFIX, aacgmv2.IGRF_COEFFS) assert not aacgmv2.__reset_warn__ + del sys.modules["aacgmv2"] del aacgmv2 diff --git a/aacgmv2/tests/test_c_aacgmv2.py b/aacgmv2/tests/test_c_aacgmv2.py index 4835c9b4..95a3527d 100644 --- a/aacgmv2/tests/test_c_aacgmv2.py +++ b/aacgmv2/tests/test_c_aacgmv2.py @@ -4,8 +4,10 @@ import datetime as dt import numpy as np import pytest + import aacgmv2 + class TestCAACGMV2: def setup(self): """Runs before every method to create a clean testing setup""" @@ -18,131 +20,88 @@ def setup(self): self.lat_in = [45.5, 60] self.lon_in = [-23.5, 0] self.alt_in = [1135, 300] - + self.code = {'G2A': aacgmv2._aacgmv2.G2A, 'A2G': aacgmv2._aacgmv2.A2G, + 'TG2A': aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.TRACE, + 'TA2G': aacgmv2._aacgmv2.A2G + aacgmv2._aacgmv2.TRACE} self.lat_comp = {'G2A': [48.1902, 58.2194], 'A2G': [30.7550, 50.4371], 'TG2A': [48.1954, 58.2189], 'TA2G': [30.7661, 50.4410]} self.lon_comp = {'G2A': [57.7505, 80.7282], 'A2G': [-94.1724, -77.5323], 'TG2A': [57.7456, 80.7362], 'TA2G': [-94.1727, -77.5440]} self.r_comp = {'G2A': [1.1775, 1.0457], 'A2G': [1133.6246, 305.7308], - 'TG2A': [1.1775, 1.0457], 'TA2G': [1133.6282, 305.7322]} + 'TG2A': [1.1775, 1.0457], 'TA2G': [1133.6282, 305.7322]} def teardown(self): """Runs after every method to clean up previous testing""" del self.date_args, self.long_date, self.mlat, self.mlon, self.mlt del self.lat_in, self.lon_in, self.alt_in, self.lat_comp, self.lon_comp - del self.r_comp - - def test_constants(self): + del self.r_comp, self.code + + @pytest.mark.parametrize('mattr,val', [(aacgmv2._aacgmv2.G2A, 0), + (aacgmv2._aacgmv2.A2G, 1), + (aacgmv2._aacgmv2.TRACE, 2), + (aacgmv2._aacgmv2.ALLOWTRACE, 4), + (aacgmv2._aacgmv2.BADIDEA, 8), + (aacgmv2._aacgmv2.GEOCENTRIC, 16)]) + def test_constants(self, mattr, val): """Test module constants""" - ans1 = aacgmv2._aacgmv2.G2A == 0 - ans2 = aacgmv2._aacgmv2.A2G == 1 - ans3 = aacgmv2._aacgmv2.TRACE == 2 - ans4 = aacgmv2._aacgmv2.ALLOWTRACE == 4 - ans5 = aacgmv2._aacgmv2.BADIDEA == 8 - ans6 = aacgmv2._aacgmv2.GEOCENTRIC == 16 + np.testing.assert_equal(mattr, val) - assert ans1 & ans2 & ans3 & ans4 & ans5 & ans6 - del ans1, ans2, ans3, ans4, ans5, ans6 - - def test_set_datetime(self): + @pytest.mark.parametrize('idate', [0, 1]) + def test_set_datetime(self, idate): """Test set_datetime""" - for darg in self.date_args: - arg1 = aacgmv2._aacgmv2.set_datetime(*darg) is None - assert arg1 + self.mlt = aacgmv2._aacgmv2.set_datetime(*self.date_args[idate]) + assert self.mlt is None - @classmethod def test_fail_set_datetime(self): """Test unsuccessful set_datetime""" + self.long_date[0] = 1013 with pytest.raises(RuntimeError): - aacgmv2._aacgmv2.set_datetime(1013, 1, 1, 0, 0, 0) + aacgmv2._aacgmv2.set_datetime(*self.long_date) - def test_convert_G2A_coeff(self): + @pytest.mark.parametrize('idate,ckey', [(0, 'G2A'), (1, 'G2A'), + (0, 'A2G'), (1, 'A2G'), + (0, 'TG2A'), (1, 'TG2A'), + (0, 'TA2G'), (1, 'TA2G')]) + def test_convert(self, idate, ckey): """Test convert from geographic to magnetic coordinates""" - for i,darg in enumerate(self.date_args): - aacgmv2._aacgmv2.set_datetime(*darg) - (self.mlat, self.mlon, - self.rshell) = aacgmv2._aacgmv2.convert(self.lat_in[i], - self.lon_in[i], - self.alt_in[i], - aacgmv2._aacgmv2.G2A) - np.testing.assert_almost_equal(self.mlat, self.lat_comp['G2A'][i], - decimal=4) - np.testing.assert_almost_equal(self.mlon, self.lon_comp['G2A'][i], - decimal=4) - np.testing.assert_almost_equal(self.rshell, self.r_comp['G2A'][i], - decimal=4) - - def test_convert_A2G_coeff(self): - """Test convert from magnetic to geodetic coordinates""" - for i,darg in enumerate(self.date_args): - aacgmv2._aacgmv2.set_datetime(*darg) - (self.mlat, self.mlon, - self.rshell) = aacgmv2._aacgmv2.convert(self.lat_in[i], - self.lon_in[i], - self.alt_in[i], - aacgmv2._aacgmv2.A2G) - np.testing.assert_almost_equal(self.mlat, self.lat_comp['A2G'][i], - decimal=4) - np.testing.assert_almost_equal(self.mlon, self.lon_comp['A2G'][i], - decimal=4) - np.testing.assert_almost_equal(self.rshell, self.r_comp['A2G'][i], - decimal=4) + aacgmv2._aacgmv2.set_datetime(*self.date_args[idate]) + (self.mlat, self.mlon, + self.rshell) = aacgmv2._aacgmv2.convert(self.lat_in[idate], + self.lon_in[idate], + self.alt_in[idate], + self.code[ckey]) + np.testing.assert_almost_equal(self.mlat, self.lat_comp[ckey][idate], + decimal=4) + np.testing.assert_almost_equal(self.mlon, self.lon_comp[ckey][idate], + decimal=4) + np.testing.assert_almost_equal(self.rshell, self.r_comp[ckey][idate], + decimal=4) - def test_convert_arr(self): + @pytest.mark.parametrize('ckey', ['G2A', 'A2G', 'TG2A', 'TA2G']) + def test_convert_arr(self, ckey): """Test convert_arr using from magnetic to geodetic coordinates""" aacgmv2._aacgmv2.set_datetime(*self.date_args[0]) (self.mlat, self.mlon, self.rshell, bad_ind) = aacgmv2._aacgmv2.convert_arr(self.lat_in, self.lon_in, self.alt_in, - aacgmv2._aacgmv2.A2G) + self.code[ckey]) - assert len(self.mlat) == len(self.lat_in) - np.testing.assert_almost_equal(self.mlat[0], self.lat_comp['A2G'][0], + np.testing.assert_equal(len(self.mlat), len(self.lat_in)) + np.testing.assert_almost_equal(self.mlat[0], self.lat_comp[ckey][0], decimal=4) - np.testing.assert_almost_equal(self.mlon[0], self.lon_comp['A2G'][0], + np.testing.assert_almost_equal(self.mlon[0], self.lon_comp[ckey][0], decimal=4) - np.testing.assert_almost_equal(self.rshell[0], self.r_comp['A2G'][0], + np.testing.assert_almost_equal(self.rshell[0], self.r_comp[ckey][0], decimal=4) - assert bad_ind[0] == -1 - - def test_convert_G2A_TRACE(self): - """Test convert from geodetic to magnetic coordinates using trace""" - code = aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.TRACE - - for i,dargs in enumerate(self.date_args): - aacgmv2._aacgmv2.set_datetime(*dargs) - (self.mlat, self.mlon, - self.rshell) = aacgmv2._aacgmv2.convert(self.lat_in[i], - self.lon_in[i], - self.alt_in[i], code) - np.testing.assert_almost_equal(self.mlat, self.lat_comp['TG2A'][i], - decimal=4) - np.testing.assert_almost_equal(self.mlon, self.lon_comp['TG2A'][i], - decimal=4) - np.testing.assert_almost_equal(self.rshell, self.r_comp['TG2A'][i], - decimal=4) - - del code - - def test_convert_A2G_TRACE(self): - """Test convert from magnetic to geodetic coordinates using trace""" - code = aacgmv2._aacgmv2.A2G + aacgmv2._aacgmv2.TRACE - - for i,dargs in enumerate(self.date_args): - aacgmv2._aacgmv2.set_datetime(*dargs) - (self.mlat, self.mlon, - self.rshell) = aacgmv2._aacgmv2.convert(self.lat_in[i], - self.lon_in[i], - self.alt_in[i], code) - np.testing.assert_almost_equal(self.mlat, self.lat_comp['TA2G'][i], - decimal=4) - np.testing.assert_almost_equal(self.mlon, self.lon_comp['TA2G'][i], - decimal=4) - np.testing.assert_almost_equal(self.rshell, self.r_comp['TA2G'][i], - decimal=4) + np.testing.assert_equal(bad_ind[0], -1) - del code + def test_forbidden(self): + """Test convert failure""" + self.lat_in[0] = 7 + with pytest.raises(RuntimeError): + aacgmv2._aacgmv2.convert(self.lat_in[0], self.lon_in[0], 0, + aacgmv2._aacgmv2.G2A) def test_convert_high_denied(self): """Test for failure when converting to high altitude geodetic to @@ -158,8 +117,8 @@ def test_convert_high_denied(self): (aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.ALLOWTRACE, 59.9753, 57.7294, 1.8626), - (aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.BADIDEA, - 58.7286, 56.4296, 1.8626)]) + (aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.BADIDEA, + 58.7286, 56.4296, 1.8626)]) def test_convert_high(self, code, lat_comp, lon_comp, r_comp): """Test convert from high altitude geodetic to magnetic coordinates""" aacgmv2._aacgmv2.set_datetime(*self.date_args[0]) @@ -180,14 +139,14 @@ def test_convert_high(self, code, lat_comp, lon_comp, r_comp): (aacgmv2._aacgmv2.A2G + aacgmv2._aacgmv2.GEOCENTRIC, 30.6117, -94.1724, 1135.0000), - (aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.TRACE + - aacgmv2._aacgmv2.GEOCENTRIC, 48.3836, 57.7793, + (aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.TRACE + + aacgmv2._aacgmv2.GEOCENTRIC, 48.3836, 57.7793, 1.1781), - (aacgmv2._aacgmv2.A2G + aacgmv2._aacgmv2.TRACE + - aacgmv2._aacgmv2.GEOCENTRIC, 30.6227, -94.1727, + (aacgmv2._aacgmv2.A2G + aacgmv2._aacgmv2.TRACE + + aacgmv2._aacgmv2.GEOCENTRIC, 30.6227, -94.1727, 1135.0000)]) - def test_convert(self, code, lat_comp, lon_comp, r_comp): - """Test convert for different code inputs""" + def test_convert_geocentric(self, code, lat_comp, lon_comp, r_comp): + """Test convert for different code inputs with geocentric coords""" aacgmv2._aacgmv2.set_datetime(*self.date_args[0]) (self.mlat, self.mlon, self.rshell) = aacgmv2._aacgmv2.convert(self.lat_in[0], self.lon_in[0], @@ -196,33 +155,25 @@ def test_convert(self, code, lat_comp, lon_comp, r_comp): np.testing.assert_almost_equal(self.mlon, lon_comp, decimal=4) np.testing.assert_almost_equal(self.rshell, r_comp, decimal=4) - @classmethod - def test_forbidden(self): - """Test convert failure""" - with pytest.raises(RuntimeError): - aacgmv2._aacgmv2.convert(7, 0, 0, aacgmv2._aacgmv2.G2A) - @pytest.mark.parametrize('marg,mlt_comp', [(12.0, -153.6033), (25.0, 41.3967), (-1.0, 11.3967)]) def test_inv_mlt_convert(self, marg, mlt_comp): """Test MLT inversion""" - mlt_args = list(self.long_date) - mlt_args.append(marg) - self.mlon = aacgmv2._aacgmv2.inv_mlt_convert(*mlt_args) + self.long_date = list(self.long_date) + self.long_date.append(marg) + self.mlon = aacgmv2._aacgmv2.inv_mlt_convert(*self.long_date) np.testing.assert_almost_equal(self.mlon, mlt_comp, decimal=4) - del mlt_args - @pytest.mark.parametrize('marg,mlt_comp', [(12.0, -153.6033), (25.0, 41.3967), (-1.0, 11.3967)]) def test_inv_mlt_convert_yrsec(self, marg, mlt_comp): """Test MLT inversion with year and seconds of year""" dtime = dt.datetime(*self.long_date) - soy = (int(dtime.strftime("%j"))-1) * 86400 + dtime.hour * 3600 + \ - dtime.minute * 60 + dtime.second - + soy = (int(dtime.strftime("%j")) - 1) * 86400 + dtime.hour * 3600 \ + + dtime.minute * 60 + dtime.second + self.mlon = aacgmv2._aacgmv2.inv_mlt_convert_yrsec(dtime.year, soy, marg) @@ -246,9 +197,9 @@ def test_mlt_convert(self, marg, mlt_comp): def test_mlt_convert_yrsec(self, marg, mlt_comp): """Test MLT calculation using year and seconds of year""" dtime = dt.datetime(*self.long_date) - soy = (int(dtime.strftime("%j"))-1) * 86400 + dtime.hour * 3600 + \ - dtime.minute * 60 + dtime.second - + soy = (int(dtime.strftime("%j")) - 1) * 86400 + dtime.hour * 3600 \ + + dtime.minute * 60 + dtime.second + self.mlt = aacgmv2._aacgmv2.mlt_convert_yrsec(dtime.year, soy, marg) np.testing.assert_almost_equal(self.mlt, mlt_comp, decimal=4) diff --git a/aacgmv2/tests/test_cmd_aacgmv2.py b/aacgmv2/tests/test_cmd_aacgmv2.py index 3d9fa7d2..3bdcca04 100644 --- a/aacgmv2/tests/test_cmd_aacgmv2.py +++ b/aacgmv2/tests/test_cmd_aacgmv2.py @@ -8,6 +8,7 @@ import aacgmv2 + @pytest.mark.xfail class TestCmdAACGMV2: def setup(self): @@ -67,8 +68,8 @@ def test_convert_today(self): p.wait() assert os.path.isfile(self.output) data = np.loadtxt(self.output) - assert data.shape == (3,3) - + assert data.shape == (3, 3) + def test_convert_single_line(self): """ Test the command line with a single line as input """ p = subprocess.Popen(['python', '-m', 'aacgmv2', 'convert', '-i', @@ -90,15 +91,14 @@ def test_main_help(self): def test_convert_stdin_stdout(self): p = subprocess.Popen( 'echo 60 15 300 | python -m aacgmv2 convert -d 20150224', - shell=True, stdout=subprocess.PIPE) + shell=True, stdout=subprocess.PIPE) stdout, _ = p.communicate() p.wait() assert b'57.48099198 93.52895314' in stdout @pytest.mark.parametrize('pin,ref', [([], [9.0912, 9.8246, 10.5579]), - (['-v'], [-120.3687, 44.6313, -150.3687]) - ]) + (['-v'], [-120.3687, 44.6313, -150.3687])]) def test_convert_mlt_command_line(self, pin, ref): """ Test the command line MLT conversion options""" p_command = ['python', '-m', 'aacgmv2', 'convert_mlt', '-i', @@ -111,7 +111,6 @@ def test_convert_mlt_command_line(self, pin, ref): data = np.loadtxt(self.output) np.testing.assert_allclose(data, ref, rtol=self.rtol) - def test_convert_mlt_single_line(self): p = subprocess.Popen(['python', '-m', 'aacgmv2', 'convert_mlt', '-i', self.mlt_single, '20150224140015', '-o', diff --git a/aacgmv2/tests/test_dep_aacgmv2.py b/aacgmv2/tests/test_dep_aacgmv2.py index e006cb4f..aae57843 100644 --- a/aacgmv2/tests/test_dep_aacgmv2.py +++ b/aacgmv2/tests/test_dep_aacgmv2.py @@ -2,15 +2,11 @@ from __future__ import division, absolute_import, unicode_literals import datetime as dt -from io import StringIO -import logging -import numpy as np -import pytest -from sys import version_info import warnings import aacgmv2 + class TestFutureDepWarning: def setup(self): # Initialize the routine to be tested @@ -22,7 +18,7 @@ def teardown(self): del self.test_routine, self.test_args, self.test_kwargs def test_future_dep_warning(self): - """Test the implementation of FutureWarning for deprecated routines""" + """Test the implementation of FutureWarning for dupicate routines""" if self.test_routine is None: assert True else: @@ -36,7 +32,7 @@ def test_future_dep_warning(self): # Verify some things assert len(wout) == 1 assert issubclass(wout[-1].category, FutureWarning) - assert "Deprecated routine" in str(wout[-1].message) + assert "Duplicate routine" in str(wout[-1].message) class TestDepAACGMV2Warning(TestFutureDepWarning): @@ -49,13 +45,6 @@ def setup(self): def teardown(self): del self.dtime, self.test_routine, self.test_args, self.test_kwargs - def test_gc2gd_lat_warning(self): - """Test future deprecation warning for gc2gd_lat""" - - self.test_routine = aacgmv2.deprecated.gc2gd_lat - self.test_args = [60.0] - self.test_future_dep_warning() - def test_igrf_dipole_axis_warning(self): """Test future deprecation warning for igrf_dipole_axis""" @@ -63,60 +52,16 @@ def test_igrf_dipole_axis_warning(self): self.test_args = [self.dtime] self.test_future_dep_warning() -class TestDepAACGMV2: - def setup(self): - """Runs before every method to create a clean testing setup""" - self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0) - self.lat = None - self.lon = None + def test_subsol_warning(self): + """Test future deprecation warning for subsol""" - def teardown(self): - """Runs after every method to clean up previous testing""" - del self.dtime, self.lat, self.lon - - def test_subsol(self): - """Test the subsolar calculation""" - doy = int(self.dtime.strftime("%j")) - ut = self.dtime.hour * 3600.0 + self.dtime.minute * 60.0 + \ - self.dtime.second - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self.lon, self.lat = aacgmv2.deprecated.subsol(self.dtime.year, - doy, ut) - - np.testing.assert_almost_equal(self.lon, -179.2004, decimal=4) - np.testing.assert_almost_equal(self.lat, -23.0431, decimal=4) + self.test_routine = aacgmv2.deprecated.subsol + self.test_args = [self.dtime.year, 1, 1.0] + self.test_future_dep_warning() def test_gc2gd_lat(self): - """Test the geocentric to geodetic conversion""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self.lat = aacgmv2.deprecated.gc2gd_lat(45.0) - - np.testing.assert_almost_equal(self.lat, 45.1924, decimal=4) - - def test_gc2gd_lat_list(self): - """Test the geocentric to geodetic conversion""" - self.lat = [45.0, -45.0] - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self.lat = aacgmv2.deprecated.gc2gd_lat(self.lat) - - np.testing.assert_allclose(self.lat, [45.1924, -45.1924], rtol=1.0e-4) - - def test_gc2gd_lat_arr(self): - """Test the geocentric to geodetic conversion""" - self.lat = np.array([45.0, -45.0]) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self.lat = aacgmv2.deprecated.gc2gd_lat(self.lat) - - np.testing.assert_allclose(self.lat, [45.1924, -45.1924], rtol=1.0e-4) - - def test_igrf_dipole_axis(self): - """Test the IGRF dipole axis calculation""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - m = aacgmv2.deprecated.igrf_dipole_axis(self.dtime) - - np.testing.assert_allclose(m, [0.050281,-0.16057,0.98574], rtol=1.0e-4) + """Test future deprecation warning for gc2gd_lat""" + + self.test_routine = aacgmv2.deprecated.gc2gd_lat + self.test_args = [45.0] + self.test_future_dep_warning() diff --git a/aacgmv2/tests/test_py_aacgmv2.py b/aacgmv2/tests/test_py_aacgmv2.py index bac5f3fc..bfef603b 100644 --- a/aacgmv2/tests/test_py_aacgmv2.py +++ b/aacgmv2/tests/test_py_aacgmv2.py @@ -12,74 +12,29 @@ import aacgmv2 -class TestFutureDepWarning: - def setup(self): - # Initialize the routine to be tested - self.test_routine = None - self.test_args = [] - self.test_kwargs = {} - def teardown(self): - del self.test_routine, self.test_args, self.test_kwargs - - def test_future_dep_warning(self): - """Test the implementation of FutureWarning for deprecated kwargs""" - if self.test_routine is None: - assert True - else: - with warnings.catch_warnings(record=True) as wout: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - # Trigger a warning. - self.test_routine(*self.test_args, **self.test_kwargs) - - # Verify some things - assert len(wout) == 1 - assert issubclass(wout[-1].category, FutureWarning) - assert "Deprecated keyword" in str(wout[-1].message) - -class TestDepConvertWarning(TestFutureDepWarning): +class TestConvertArray: def setup(self): - self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0) - self.test_routine = None - self.test_args = [] - self.test_kwargs = {} + self.out = None + self.ref = None + self.rtol = 1.0e-4 def teardown(self): - del self.dtime, self.test_routine, self.test_args, self.test_kwargs + del self.out, self.ref, self.rtol - def test_convert_latlon_warning(self): - """Test future warning for convert_latlon""" + def evaluate_output(self, ind=None): + """ Function used to evaluate convert_latlon_arr output""" + if self.out is not None: + if ind is not None: + self.ref = [[rr[ind]] for rr in self.ref] - self.test_routine = aacgmv2.wrapper.convert_latlon - self.test_args = [60, 0, 300, self.dtime] - self.test_kwargs = {'code': 'TRACE'} - self.test_future_dep_warning() + np.testing.assert_equal(len(self.out), len(self.ref)) + for i, oo in enumerate(self.out): + if not isinstance(oo, np.ndarray): + raise TypeError("output value is not a numpy array") - def test_convert_latlon_arr_warning(self): - """Test future warning for convert_latlon_arr""" - - self.test_routine = aacgmv2.wrapper.convert_latlon_arr - self.test_args = [[60, 60], [0, 0], [300, 300], self.dtime] - self.test_kwargs = {'code': 'TRACE'} - self.test_future_dep_warning() - - def test_convert_latlon_time_error(self): - """Test single value latlon conversion with a bad datetime""" - self.test_routine = aacgmv2.wrapper.convert_latlon - self.test_args = [60, 0, 300, self.dtime] - self.test_kwargs = {'bad': 'keyword'} - with pytest.raises(TypeError): - self.test_routine(*self.test_args, **self.test_kwargs) - - def test_convert_latlon_arr_time_error(self): - """Test single value latlon conversion with a bad datetime""" - self.test_routine = aacgmv2.wrapper.convert_latlon_arr - self.test_args = [[60, 60], [0, 0], [300, 300], self.dtime] - self.test_kwargs = {'bad': 'keyword'} - with pytest.raises(TypeError): - self.test_routine(*self.test_args, **self.test_kwargs) + np.testing.assert_equal(len(oo), len(self.ref[i])) + np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) class TestConvertLatLon: @@ -96,21 +51,32 @@ def teardown(self): del self.out, self.in_args, self.rtol, self.dtime, self.ddate @pytest.mark.parametrize('alt,method_code,ref', - [(300, 'TRACE', [58.2268,81.1613,1.0457]), - (3000.0, "G2A|BADIDEA", [64.3578,83.2895,1.4694]), + [(300, 'TRACE', [58.2268, 81.1613, 1.0457]), + (3000.0, "G2A|BADIDEA", [64.3578, 83.2895, + 1.4694]), (7000.0, "G2A|TRACE|BADIDEA", - [69.3187,85.0845,2.0973])]) + [69.3187, 85.0845, 2.0973])]) def test_convert_latlon(self, alt, method_code, ref): """Test single value latlon conversion""" self.in_args.extend([alt, self.dtime, method_code]) self.out = aacgmv2.convert_latlon(*self.in_args) np.testing.assert_allclose(self.out, ref, rtol=self.rtol) + @pytest.mark.parametrize('lat,ref', + [(90.01, [83.927161, 170.1471396, 1.04481923]), + (-90.01, [-74.9814852, 17.990332, 1.044819236])]) + def test_convert_latlon_high_lat(self, lat, ref): + """Test single latlon conversion with latitude just out of bounds""" + self.in_args[0] = lat + self.in_args.extend([300, self.dtime, 'G2A']) + self.out = aacgmv2.convert_latlon(*self.in_args) + np.testing.assert_allclose(self.out, ref, rtol=self.rtol) + def test_convert_latlon_datetime_date(self): """Test single latlon conversion with date and datetime input""" self.in_args.extend([300, self.ddate, 'TRACE']) self.out = aacgmv2.convert_latlon(*self.in_args) - np.testing.assert_allclose(self.out, [58.2268,81.1613,1.0457], + np.testing.assert_allclose(self.out, [58.2268, 81.1613, 1.0457], rtol=self.rtol) @pytest.mark.skipif(version_info.major == 2, @@ -120,29 +86,25 @@ def test_convert_latlon_location_failure(self): self.out = aacgmv2.convert_latlon(0, 0, 0, self.dtime, self.in_args[-1]) assert np.all(np.isnan(np.array(self.out))) - def test_convert_latlon_time_failure(self): - """Test single value latlon conversion with a bad datetime""" - self.in_args.extend([300, None, 'TRACE']) - with pytest.raises(ValueError): - self.out = aacgmv2.convert_latlon(*self.in_args) - def test_convert_latlon_maxalt_failure(self): """test convert_latlon failure for an altitude too high for coeffs""" self.in_args.extend([2001, self.dtime, ""]) self.out = aacgmv2.convert_latlon(*self.in_args) assert np.all(np.isnan(np.array(self.out))) - def test_convert_latlon_lat_high_failure(self): - """Test error return for co-latitudes above 90 for a single value""" - with pytest.raises(ValueError): - aacgmv2.convert_latlon(91, 0, 300, self.dtime) + @pytest.mark.parametrize('in_rep,in_irep,msg', + [(None, 3, "must be a datetime object"), + (91, 0, "unrealistic latitude"), + (-91, 0, "unrealistic latitude"), + (None, 4, "unknown method code")]) + def test_convert_latlon_failure(self, in_rep, in_irep, msg): + self.in_args.extend([300, self.dtime, "G2A"]) + self.in_args[in_irep] = in_rep + with pytest.raises(ValueError, match=msg): + aacgmv2.convert_latlon(*self.in_args) - def test_convert_latlon_lat_low_failure(self): - """Test error return for co-latitudes below -90 for a single value""" - with pytest.raises(ValueError): - aacgmv2.convert_latlon(-91, 0, 300, self.dtime) -class TestConvertLatLonArr: +class TestConvertLatLonArr(TestConvertArray): def setup(self): """Runs before every method to create a clean testing setup""" self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0) @@ -165,12 +127,15 @@ def test_convert_latlon_arr_single_val(self): self.out = aacgmv2.convert_latlon_arr(self.lat_in[0], self.lon_in[0], self.alt_in[0], self.dtime, self.method) + self.evaluate_output(ind=0) - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, [self.ref[i][0]], rtol=self.rtol) + def test_convert_latlon_arr_arr_single(self): + """Test array latlon conversion for array input of shape (1,)""" + self.out = aacgmv2.convert_latlon_arr(np.array([self.lat_in[0]]), + np.array([self.lon_in[0]]), + np.array([self.alt_in[0]]), + self.dtime, self.method) + self.evaluate_output(ind=0) def test_convert_latlon_arr_list_single(self): """Test array latlon conversion for list input of single values""" @@ -178,38 +143,14 @@ def test_convert_latlon_arr_list_single(self): [self.lon_in[0]], [self.alt_in[0]], self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, [self.ref[i][0]], rtol=self.rtol) + self.evaluate_output(ind=0) def test_convert_latlon_arr_list(self): """Test array latlon conversion for list input""" self.out = aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in, self.alt_in, self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.ref[i]) - for i, oo in enumerate(self.out)] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) - - def test_convert_latlon_arr_arr_single(self): - """Test array latlon conversion for array input of shape (1,)""" - self.out = aacgmv2.convert_latlon_arr(np.array([self.lat_in[0]]), - np.array([self.lon_in[0]]), - np.array([self.alt_in[0]]), - self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, [self.ref[i][0]], rtol=self.rtol) + self.evaluate_output() def test_convert_latlon_arr_arr(self): """Test array latlon conversion for array input""" @@ -217,60 +158,41 @@ def test_convert_latlon_arr_arr(self): np.array(self.lon_in), np.array(self.alt_in), self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.ref[i]) - for i, oo in enumerate(self.out)] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) + self.evaluate_output() def test_convert_latlon_arr_list_mix(self): """Test array latlon conversion for mixed types with list""" self.out = aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in[0], self.alt_in[0], self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.ref[i]) - for i, oo in enumerate(self.out)] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) + self.evaluate_output() def test_convert_latlon_arr_arr_mix(self): """Test array latlon conversion for mixed type with an array""" self.out = aacgmv2.convert_latlon_arr(np.array(self.lat_in), self.lon_in[0], self.alt_in[0], self.dtime, self.method) + self.evaluate_output() - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.ref[i]) - for i, oo in enumerate(self.out)] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) - - def test_convert_latlon_arr_mult_failure(self): - """Test array latlon conversion for mix type with multi-dim array""" - with pytest.raises(ValueError): - aacgmv2.convert_latlon_arr(np.full(shape=(3,2), fill_value=50.0), - 0, 300, self.dtime) + def test_convert_latlon_arr_arr_mult_and_single_element(self): + """Test latlon conversion for arrays with multiple and single vals""" + self.out = aacgmv2.convert_latlon_arr(np.array(self.lat_in), + np.array([self.lon_in[0]]), + np.array(self.alt_in), + self.dtime, self.method) + self.evaluate_output() - @pytest.mark.parametrize('method_code,alt,ref', - [("BADIDEA", 3000.0, [64.3580,83.2895,1.4694]), + @pytest.mark.parametrize('method_code,alt,local_ref', + [("BADIDEA", 3000.0, + [[64.3580], [83.2895], [1.4694]]), ("BADIDEA|TRACE", 7000.0, - [69.3187,85.0845,2.0973])]) - def test_convert_latlon_arr_badidea(self, method_code, alt, ref): + [[69.3187], [85.0845], [2.0973]])]) + def test_convert_latlon_arr_badidea(self, method_code, alt, local_ref): """Test array latlon conversion for BADIDEA""" self.out = aacgmv2.convert_latlon_arr(self.lat_in[0], self.lon_in[0], [alt], self.dtime, method_code) - - assert len(self.out) == len(ref) - assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, [ref[i]], rtol=self.rtol) + self.ref = local_ref + self.evaluate_output() @pytest.mark.skipif(version_info.major == 2, reason='Not raised in Python 2') @@ -285,33 +207,25 @@ def test_convert_latlon_arr_location_failure(self): self.out = aacgmv2.convert_latlon_arr([0], [0], [0], self.dtime, "") # Test the output - assert len(self.out) == len(self.ref) + np.testing.assert_equal(len(self.out), len(self.ref)) assert np.any(~np.isfinite(np.array(self.out))) - def test_convert_latlon_arr_mult_arr_unequal_failure(self): - """Test array latlon conversion for unequal sized arrays""" - with pytest.raises(ValueError): - aacgmv2.convert_latlon_arr(np.array([[60, 61, 62], [63, 64, 65]]), - np.array([0, 1]), 300, self.dtime) - - def test_convert_latlon_arr_time_failure(self): - """Test array latlon conversion with a bad time""" - with pytest.raises(ValueError): - aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in, self.alt_in, - None, self.method) - def test_convert_latlon_arr_datetime_date(self): """Test array latlon conversion with date and datetime input""" self.out = aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in, self.alt_in, self.ddate, self.method) + self.evaluate_output() - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.ref[i]) - for i, oo in enumerate(self.out)] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) + def test_convert_latlon_arr_clip(self): + """Test array latlon conversion with latitude clipping""" + self.lat_in = [90.01, -90.01] + self.ref = [[83.92352053, -74.98110552], [170.1381271, 17.98164313], + [1.04481924, 1.04481924]] + self.out = aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in, + self.alt_in, self.ddate, + self.method) + self.evaluate_output() def test_convert_latlon_arr_maxalt_failure(self): """test convert_latlon_arr failure for altitudes too high for coeffs""" @@ -320,10 +234,21 @@ def test_convert_latlon_arr_maxalt_failure(self): [2001], self.dtime, self.method) assert np.all(np.isnan(np.array(self.out))) - def test_convert_latlon_arr_lat_failure(self): - """Test error return for co-latitudes above 90 for an array""" - with pytest.raises(ValueError): - aacgmv2.convert_latlon_arr([91, 60, -91], 0, 300, self.dtime) + @pytest.mark.parametrize('in_rep,in_irep,msg', + [(None, 3, "must be a datetime object"), + ([np.full(shape=(3, 2), fill_value=50.0), 0], + [0, 1], "unable to process multi-dimensional"), + ([50, 60, 70], 0, "arrays are mismatched"), + ([[91, 60, -91], 0, 300], [0, 1, 2], + "unrealistic latitude"), + (None, 4, "unknown method code")]) + def test_convert_latlon_arr_failure(self, in_rep, in_irep, msg): + in_args = np.array([self.lat_in, self.lon_in, self.alt_in, self.dtime, + "G2A"], dtype=object) + in_args[in_irep] = in_rep + with pytest.raises(ValueError, match=msg): + aacgmv2.convert_latlon_arr(*in_args) + class TestGetAACGMCoord: def setup(self): @@ -339,10 +264,11 @@ def teardown(self): del self.out, self.in_args, self.rtol, self.dtime, self.ddate @pytest.mark.parametrize('alt,method_code,ref', - [(300, 'TRACE', [58.2268,81.1613,0.1888]), - (3000.0, "G2A|BADIDEA", [64.3578,83.2895,0.3307]), + [(300, 'TRACE', [58.2268, 81.1613, 0.1888]), + (3000.0, "G2A|BADIDEA", [64.3578, 83.2895, + 0.3307]), (7000.0, "G2A|TRACE|BADIDEA", - [69.3187,85.0845,0.4503])]) + [69.3187, 85.0845, 0.4503])]) def test_get_aacgm_coord(self, alt, method_code, ref): """Test single value AACGMV2 calculation, defaults to TRACE""" self.in_args.extend([alt, self.dtime, method_code]) @@ -353,7 +279,7 @@ def test_get_aacgm_coord_datetime_date(self): """Test single AACGMV2 calculation with date and datetime input""" self.in_args.extend([300.0, self.ddate, 'TRACE']) self.out = aacgmv2.get_aacgm_coord(*self.in_args) - np.testing.assert_allclose(self.out, [58.2268,81.1613,0.1888], + np.testing.assert_allclose(self.out, [58.2268, 81.1613, 0.1888], rtol=self.rtol) @pytest.mark.skipif(version_info.major == 2, @@ -371,7 +297,7 @@ def test_get_aacgm_coord_maxalt_failure(self): self.in_args.extend([2001, self.dtime, ""]) self.out = aacgmv2.get_aacgm_coord(*self.in_args) assert np.all(np.isnan(np.array(self.out))) - + @pytest.mark.parametrize('in_index,value', [(3, None), (0, 91.0), (0, -91.0)]) def test_get_aacgm_coord_raise_value_error(self, in_index, value): @@ -382,7 +308,7 @@ def test_get_aacgm_coord_raise_value_error(self, in_index, value): self.out = aacgmv2.get_aacgm_coord(*self.in_args) -class TestGetAACGMCoordArr: +class TestGetAACGMCoordArr(TestConvertArray): def setup(self): """Runs before every method to create a clean testing setup""" self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0) @@ -392,7 +318,8 @@ def setup(self): self.alt_in = [300.0, 300.0] self.method = 'TRACE' self.out = None - self.ref = [[58.22676,59.31847], [81.16135,81.60797], [0.18880,0.21857]] + self.ref = [[58.22676, 59.31847], [81.16135, 81.60797], + [0.18880, 0.21857]] self.rtol = 1.0e-4 def teardown(self): @@ -405,12 +332,7 @@ def test_get_aacgm_coord_arr_single_val(self): self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in[0], self.lon_in[0], self.alt_in[0], self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, [self.ref[i][0]], rtol=self.rtol) + self.evaluate_output(ind=0) def test_get_aacgm_coord_arr_list_single(self): """Test array AACGMV2 calculation for list input of single values""" @@ -418,25 +340,7 @@ def test_get_aacgm_coord_arr_list_single(self): [self.lon_in[0]], [self.alt_in[0]], self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, [self.ref[i][0]], rtol=self.rtol) - - def test_get_aacgm_coord_arr_list(self): - """Test array AACGMV2 calculation for list input""" - self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in,self.lon_in, - self.alt_in, self.dtime, - self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.lat_in) - for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) + self.evaluate_output(ind=0) def test_get_aacgm_coord_arr_arr_single(self): """Test array AACGMV2 calculation for array with a single value""" @@ -444,13 +348,14 @@ def test_get_aacgm_coord_arr_arr_single(self): np.array([self.lon_in[0]]), np.array([self.alt_in[0]]), self.dtime, self.method) + self.evaluate_output(ind=0) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, [self.ref[i][0]], rtol=self.rtol) + def test_get_aacgm_coord_arr_list(self): + """Test array AACGMV2 calculation for list input""" + self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in, + self.alt_in, self.dtime, + self.method) + self.evaluate_output() def test_get_aacgm_coord_arr_arr(self): """Test array AACGMV2 calculation for an array""" @@ -458,47 +363,21 @@ def test_get_aacgm_coord_arr_arr(self): np.array(self.lon_in), np.array(self.alt_in), self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.lat_in) - for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) + self.evaluate_output() def test_get_aacgm_coord_arr_list_mix(self): """Test array AACGMV2 calculation for a list and floats""" self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in[0], self.alt_in[0], self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.lat_in) - for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) + self.evaluate_output() def test_get_aacgm_coord_arr_arr_mix(self): """Test array AACGMV2 calculation for an array and floats""" self.out = aacgmv2.get_aacgm_coord_arr(np.array(self.lat_in), self.lon_in[0], self.alt_in[0], self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.lat_in) - for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) - - def test_get_aacgm_coord_arr_mult_failure(self): - """Test aacgm_coord_arr failure with multi-dim array input""" - - with pytest.raises(ValueError): - (self.mlat_out, self.mlon_out, - self.mlt_out) = aacgmv2.get_aacgm_coord_arr( - np.array([[60, 61, 62], [63, 64, 65]]), 0, 300, self.dtime) + self.evaluate_output() def test_get_aacgm_coord_arr_badidea(self): """Test array AACGMV2 calculation for BADIDEA""" @@ -506,13 +385,8 @@ def test_get_aacgm_coord_arr_badidea(self): self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in[0], self.lon_in[0], [3000.0], self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out] - - self.ref = [64.3481, 83.2885, 0.3306] - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) + self.ref = [[64.3481], [83.2885], [0.3306]] + self.evaluate_output() @pytest.mark.skipif(version_info.major == 2, reason='Not raised in Python 2') @@ -521,11 +395,18 @@ def test_get_aacgm_coord_arr_location_failure(self): self.out = aacgmv2.get_aacgm_coord_arr([0], [0], [0], self.dtime, self.method) - - assert len(self.out) == len(self.ref) + np.testing.assert_equal(len(self.out), len(self.ref)) assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out] assert np.any([np.isnan(oo) for oo in self.out]) + def test_get_aacgm_coord_arr_mult_failure(self): + """Test aacgm_coord_arr failure with multi-dim array input""" + + with pytest.raises(ValueError): + (self.mlat_out, self.mlon_out, + self.mlt_out) = aacgmv2.get_aacgm_coord_arr( + np.array([[60, 61, 62], [63, 64, 65]]), 0, 300, self.dtime) + def test_get_aacgm_coord_arr_time_failure(self): """Test array AACGMV2 calculation with a bad time""" with pytest.raises(ValueError): @@ -549,13 +430,7 @@ def test_get_aacgm_coord_arr_datetime_date(self): self.ref = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in, self.alt_in, self.dtime, self.method) - - assert len(self.out) == len(self.ref) - assert [isinstance(oo, np.ndarray) and len(oo) == len(self.lat_in) - for oo in self.out] - - for i, oo in enumerate(self.out): - np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol) + self.evaluate_output() def test_get_aacgm_coord_arr_maxalt_failure(self): """test aacgm_coord_arr failure for an altitude too high for coeff""" @@ -565,7 +440,7 @@ def test_get_aacgm_coord_arr_maxalt_failure(self): self.alt_in, self.dtime, self.method) - assert len(self.out) == len(self.ref) + np.testing.assert_equal(len(self.out), len(self.ref)) assert [isinstance(oo, np.ndarray) and len(oo) == len(self.lat_in) for oo in self.out] assert np.all(np.isnan(np.array(self.out))) @@ -574,34 +449,43 @@ def test_get_aacgm_coord_arr_maxalt_failure(self): class TestConvertCode: def setup(self): self.c_method_code = None + self.ref_code = None + self.out = None def teardown(self): - del self.c_method_code + del self.c_method_code, self.ref_code, self.out + + def set_c_code(self): + """ Utility test to get desired C method code""" + if self.ref_code is not None: + self.ref_code = self.ref_code.upper() + self.c_method_code = getattr(aacgmv2._aacgmv2, self.ref_code) + + def set_bad_c_code(self): + """ Test failure to get bad code name""" + self.ref_code = "not_a_valid_code" + with pytest.raises(AttributeError): + self.set_c_code() @pytest.mark.parametrize('method_code', [('G2A'), ('A2G'), ('TRACE'), ('ALLOWTRACE'), ('BADIDEA'), ('GEOCENTRIC'), ('g2a')]) - def test_convert_str_to_bit(self, method_code): - """Test conversion from string code to bit""" - if hasattr(aacgmv2._aacgmv2, method_code.upper()): - self.c_method_code = getattr(aacgmv2._aacgmv2, method_code.upper()) - else: - raise ValueError('cannot find method in C code: {:}'.format( - method_code)) - - assert aacgmv2.convert_str_to_bit(method_code) == self.c_method_code - - - def test_convert_str_to_bit_spaces(self): - """Test conversion from string code to bit for a code with spaces""" - if(aacgmv2.convert_str_to_bit("G2A | trace") != - aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.TRACE): - raise AssertionError() - - def test_convert_str_to_bit_invalid(self): - """Test conversion from string code to bit for an invalid code""" - if aacgmv2.convert_str_to_bit("ggoogg|") != aacgmv2._aacgmv2.G2A: - raise AssertionError() + def test_standard_convert_str_to_bit(self, method_code): + """Test conversion from string code to bit for standard cases""" + self.ref_code = method_code + self.set_c_code() + self.out = aacgmv2.convert_str_to_bit(method_code) + + np.testing.assert_equal(self.out, self.c_method_code) + + @pytest.mark.parametrize('str_code,bit_ref', + [("G2A | trace", + aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.TRACE), + ("ggoogg|", aacgmv2._aacgmv2.G2A)]) + def test_non_standard_convert_str_to_bit(self, str_code, bit_ref): + """Test conversion from string code to bit for non-standard cases""" + self.out = aacgmv2.convert_str_to_bit(str_code) + np.testing.assert_equal(self.out, bit_ref) @pytest.mark.parametrize('bool_dict,method_code', [({}, 'G2A'), ({'a2g': True}, 'A2G'), @@ -611,13 +495,11 @@ def test_convert_str_to_bit_invalid(self): ({'geocentric': True}, 'GEOCENTRIC')]) def test_convert_bool_to_bit(self, bool_dict, method_code): """Test conversion from Boolean code to bit""" - if hasattr(aacgmv2._aacgmv2, method_code.upper()): - self.c_method_code = getattr(aacgmv2._aacgmv2, method_code.upper()) - else: - raise ValueError('cannot find method in C code: {:}'.format( - method_code)) + self.ref_code = method_code + self.set_c_code() + self.out = aacgmv2.convert_bool_to_bit(**bool_dict) - assert aacgmv2.convert_bool_to_bit(**bool_dict) == self.c_method_code + np.testing.assert_equal(self.out, self.c_method_code) class TestMLTConvert: @@ -632,7 +514,7 @@ def setup(self): self.mlon_list = [270.0, 80.0, -95.0] self.mlt_list = [12.0, 25.0, -1.0] self.mlon_comp = [-101.670617955439, 93.329382044561, 63.329382044561] - self.mlt_comp = [12.7780412 , 0.11137453, 12.44470786] + self.mlt_comp = [12.7780412, 0.11137453, 12.44470786] self.diff_comp = np.ones(shape=(3,)) * -10.52411552 def teardown(self): @@ -653,7 +535,7 @@ def test_datetime_exception(self): def test_inv_convert_mlt_single(self): """Test MLT inversion for a single value""" - for i,mlt in enumerate(self.mlt_list): + for i, mlt in enumerate(self.mlt_list): self.mlon_out = aacgmv2.convert_mlt(mlt, self.dtime, m2a=True) np.testing.assert_almost_equal(self.mlon_out, self.mlon_comp[i], decimal=4) @@ -692,7 +574,7 @@ def test_mlt_convert_mlon_wrapping(self): def test_mlt_convert_single(self): """Test MLT calculation for a single value""" - for i,mlon in enumerate(self.mlon_list): + for i, mlon in enumerate(self.mlon_list): self.mlt_out = aacgmv2.convert_mlt(mlon, self.dtime, m2a=False) np.testing.assert_almost_equal(self.mlt_out, self.mlt_comp[i], decimal=4) @@ -726,7 +608,7 @@ def test_mlt_convert_change(self): def test_mlt_convert_multidim_failure(self): """Test MLT calculation failure for multi-dimensional arrays""" - self.mlon_list = np.full(shape=(3,2), fill_value=50.0) + self.mlon_list = np.full(shape=(3, 2), fill_value=50.0) with pytest.raises(ValueError): aacgmv2.convert_mlt(self.mlon_list, self.dtime, m2a=False) @@ -736,75 +618,50 @@ def test_mlt_convert_mismatch_failure(self): aacgmv2.convert_mlt(self.mlon_list, [self.dtime, self.dtime2], m2a=False) + class TestCoeffPath: def setup(self): """Runs before every method to create a clean testing setup""" os.environ['IGRF_COEFFS'] = "default_igrf" os.environ['AACGM_v2_DAT_PREFIX'] = "default_coeff" - self.default_igrf = os.environ['IGRF_COEFFS'] - self.default_coeff = os.environ['AACGM_v2_DAT_PREFIX'] + self.ref = {"igrf_file": os.environ['IGRF_COEFFS'], + "coeff_prefix": os.environ['AACGM_v2_DAT_PREFIX']} def teardown(self): """Runs after every method to clean up previous testing""" - del self.default_igrf, self.default_coeff - - def test_set_coeff_path_default(self): + del self.ref + + @pytest.mark.parametrize("in_coeff", + [({}), + ({"igrf_file": "hi", "coeff_prefix": "bye"}), + ({"igrf_file": True, "coeff_prefix": True}), + ({"coeff_prefix": "hi"}), + ({"igrf_file": "hi"}), + ({"igrf_file": None, "coeff_prefix": None})]) + def test_set_coeff_path(self, in_coeff): """Test the coefficient path setting using default values""" - aacgmv2.wrapper.set_coeff_path() - - if os.environ['IGRF_COEFFS'] != self.default_igrf: - raise AssertionError() - if os.environ['AACGM_v2_DAT_PREFIX'] != self.default_coeff: - raise AssertionError() - - @classmethod - def test_set_coeff_path_string(self): - """Test the coefficient path setting using two user specified values""" - aacgmv2.wrapper.set_coeff_path("hi", "bye") - - if os.environ['IGRF_COEFFS'] != "hi": - raise AssertionError() - if os.environ['AACGM_v2_DAT_PREFIX'] != "bye": - raise AssertionError() + # Update the reference key, if needed + for ref_key in in_coeff.keys(): + if in_coeff[ref_key] is True or in_coeff[ref_key] is None: + if ref_key == "igrf_file": + self.ref[ref_key] = aacgmv2.IGRF_COEFFS + elif ref_key == "coeff_prefix": + self.ref[ref_key] = aacgmv2.AACGM_v2_DAT_PREFIX + else: + self.ref[ref_key] = in_coeff[ref_key] + + # Run the routine + aacgmv2.wrapper.set_coeff_path(**in_coeff) + + # Ensure the environment variables were set correctly + if os.environ['IGRF_COEFFS'] != self.ref['igrf_file']: + raise AssertionError("{:} != {:}".format(os.environ['IGRF_COEFFS'], + self.ref['igrf_file'])) + if os.environ['AACGM_v2_DAT_PREFIX'] != self.ref['coeff_prefix']: + raise AssertionError("{:} != {:}".format( + os.environ['AACGM_v2_DAT_PREFIX'], self.ref['coeff_prefix'])) - @classmethod - def test_set_coeff_path_true(self): - """Test the coefficient path setting using the module values""" - aacgmv2.wrapper.set_coeff_path(True, True) - - if os.environ['IGRF_COEFFS'] != aacgmv2.IGRF_COEFFS: - raise AssertionError() - if os.environ['AACGM_v2_DAT_PREFIX'] != aacgmv2.AACGM_v2_DAT_PREFIX: - raise AssertionError() - - def test_set_only_aacgm_coeff_path(self): - """Test the coefficient path setting using a mix of input""" - aacgmv2.wrapper.set_coeff_path(coeff_prefix="hi") - - if os.environ['IGRF_COEFFS'] != self.default_igrf: - raise AssertionError() - if os.environ['AACGM_v2_DAT_PREFIX'] != "hi": - raise AssertionError() - - def test_set_only_igrf_coeff_path(self): - """Test the coefficient path setting using a mix of input""" - aacgmv2.wrapper.set_coeff_path(igrf_file="hi") - - if os.environ['IGRF_COEFFS'] != "hi": - raise AssertionError() - if os.environ['AACGM_v2_DAT_PREFIX'] != self.default_coeff: - raise AssertionError() - - @classmethod - def test_set_both_mixed(self): - """Test the coefficient path setting using a mix of input""" - aacgmv2.wrapper.set_coeff_path(igrf_file=True, coeff_prefix="hi") - - if os.environ['IGRF_COEFFS'] != aacgmv2.IGRF_COEFFS: - raise AssertionError() - if os.environ['AACGM_v2_DAT_PREFIX'] != "hi": - raise AssertionError() class TestHeightReturns: def setup(self): @@ -812,7 +669,7 @@ def setup(self): self.code = aacgmv2._aacgmv2.A2G self.bad_code = aacgmv2._aacgmv2.BADIDEA self.trace_code = aacgmv2._aacgmv2.TRACE - + def teardown(self): """Runs after every method to clean up previous testing""" del self.code, self.bad_code @@ -825,33 +682,33 @@ def test_low_height_good(self): def test_high_coeff_bad(self): """ Test to see that a high altitude for coefficent use fails""" - assert not aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff+10.0, + assert not aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff + 10.0, self.code) def test_high_coeff_good(self): """ Test a high altitude for coefficent use with badidea """ - assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff+10.0, + assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff + 10.0, self.bad_code) def test_low_coeff_good(self): """ Test that a normal height succeeds""" - assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff*0.5, + assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff * 0.5, self.code) def test_high_trace_bad(self): """ Test that a high trace height fails""" - assert not aacgmv2.wrapper.test_height(aacgmv2.high_alt_trace+10.0, + assert not aacgmv2.wrapper.test_height(aacgmv2.high_alt_trace + 10.0, self.code) def test_low_trace_good(self): """ Test that a high coefficient height succeeds with trace""" - assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff+10.0, + assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff + 10.0, self.trace_code) def test_high_trace_good(self): """ Test that a high trace height succeeds with badidea""" - assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_trace+10.0, + assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_trace + 10.0, self.bad_code) @@ -870,7 +727,6 @@ def teardown(self): self.log_capture.close() del self.lwarn, self.lout, self.log_capture - def test_warning_below_ground(self): """ Test that a warning is issued if height < 0 for height test """ self.lwarn = u"conversion not intended for altitudes < 0 km" @@ -902,18 +758,19 @@ def test_warning_single_loc_in_arr(self): """ Test that user is warned they should be using simpler routine""" self.lwarn = u"for a single location, consider using" - aacgmv2.convert_latlon_arr(60, 0, 300, dt.datetime(2015,1,1,0,0,0)) + aacgmv2.convert_latlon_arr(60, 0, 300, dt.datetime(2015, 1, 1, 0, 0, 0)) self.lout = self.log_capture.getvalue() if self.lout.find(self.lwarn) < 0: raise AssertionError() + class TestTimeReturns: def setup(self): """Runs before every method to create a clean testing setup""" self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0) self.dtime2 = dt.datetime(2015, 1, 1, 10, 10, 10) self.ddate = dt.date(2015, 1, 1) - + def teardown(self): """Runs after every method to clean up previous testing""" del self.dtime, self.ddate, self.dtime2 diff --git a/aacgmv2/tests/test_struct_aacgmv2.py b/aacgmv2/tests/test_struct_aacgmv2.py index bb5efdb3..b141c2b4 100644 --- a/aacgmv2/tests/test_struct_aacgmv2.py +++ b/aacgmv2/tests/test_struct_aacgmv2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, unicode_literals -import datetime as dt import logging import numpy as np import os @@ -10,7 +9,8 @@ import aacgmv2 -#@pytest.mark.skip(reason="Not meant to be run alone") + +# @pytest.mark.skip(reason="Not meant to be run alone") class TestModuleStructure: def setup(self): @@ -69,17 +69,18 @@ def test_modules(self): else: # Get the submodules and make sure they are supposed to be there retrieved_list = list() - for imp,name,ispkg in pkgutil.iter_modules(path=aacgmv2.__path__): + for imp, name, ispkg in pkgutil.iter_modules(path=aacgmv2.__path__): assert name in self.reference_list - retrieved_list.append(name) + retrieved_list.append(name) # Test to see if all of the modules match assert len(retrieved_list) == len(self.reference_list) + class TestDepStructure(TestModuleStructure): def setup(self): self.module_name = None - self.reference_list = ["subsol", "gc2gd_lat", "igrf_dipole_axis"] + self.reference_list = ["subsol", "igrf_dipole_axis", "gc2gd_lat"] def teardown(self): del self.module_name, self.reference_list @@ -94,6 +95,26 @@ def test_dep_functions(self): self.module_name = "deprecated" self.test_module_functions() + +class TestUtilsStructure(TestModuleStructure): + def setup(self): + self.module_name = None + self.reference_list = ["subsol", "igrf_dipole_axis", "gc2gd_lat"] + + def teardown(self): + del self.module_name, self.reference_list + + def test_dep_existence(self): + """ Test the utility functions""" + self.module_name = "utils" + self.test_module_existence() + + def test_dep_functions(self): + """ Test the utility functions""" + self.module_name = "utils" + self.test_module_functions() + + class TestCStructure(TestModuleStructure): def setup(self): self.module_name = None @@ -164,44 +185,47 @@ def test_top_functions(self): def test_top_modules(self): """ Test the deprecated functions""" self.module_name = "aacgmv2" - self.reference_list = ["_aacgmv2", "wrapper", + self.reference_list = ["_aacgmv2", "wrapper", "utils", "deprecated", "__main__"] self.test_modules() - @classmethod - def test_top_parameters(self): - """Test module constants""" - - path1 = os.path.join("aacgmv2", "aacgmv2", "aacgm_coeffs", - "aacgm_coeffs-12-") - if aacgmv2.AACGM_v2_DAT_PREFIX.find(path1) < 0: - raise AssertionError() - path2 = os.path.join("aacgmv2", "aacgmv2", "magmodel_1590-2015.txt") - if aacgmv2.IGRF_COEFFS.find(path2) < 0: - raise AssertionError() +class TestTopVariables: + def setup(self): + self.alt_limits = {"coeff": 2000.0, "trace": 6378.0} + self.coeff_file = {"coeff": os.path.join("aacgmv2", "aacgmv2", + "aacgm_coeffs", + "aacgm_coeffs-13-"), + "igrf": os.path.join("aacgmv2", "aacgmv2", + "magmodel_1590-2020.txt")} - del path1, path2 + def teardown(self): + del self.alt_limits, self.coeff_file - @classmethod - def test_high_alt_variables(self): - """ Test that module altitude limits exist and are appropriate""" + @pytest.mark.parametrize("env_var,fkey", + [(aacgmv2.AACGM_v2_DAT_PREFIX, "coeff"), + (aacgmv2.IGRF_COEFFS, "igrf")]) + def test_top_parameters(self, env_var, fkey): + """Test module constants""" - if not isinstance(aacgmv2.high_alt_coeff, float): - raise TypeError("Coefficient upper limit not float") + if env_var.find(self.coeff_file[fkey]) < 0: + raise AssertionError("Bad env variable: {:} not {:}".format( + self.coeff_file[fkey], env_var)) - if not isinstance(aacgmv2.high_alt_trace, float): - raise TypeError("Trace upper limit not float") + @pytest.mark.parametrize("alt_var,alt_ref", + [(aacgmv2.high_alt_coeff, "coeff"), + (aacgmv2.high_alt_trace, "trace")]) + def test_high_alt_variables(self, alt_var, alt_ref): + """ Test that module altitude limits exist and are appropriate""" - if aacgmv2.high_alt_coeff != 2000.0: - raise ValueError("unexpected coefficient upper limit") + if not isinstance(alt_var, type(self.alt_limits[alt_ref])): + raise TypeError("Altitude limit variable isn't a float") - if aacgmv2.high_alt_trace <= aacgmv2.high_alt_trace: - raise ValueError("Trace limit lower than coefficient limit") + np.testing.assert_almost_equal(alt_var, self.alt_limits[alt_ref], + decimal=4) - @classmethod def test_module_logger(self): """ Test the module logger instance""" - + if not isinstance(aacgmv2.logger, logging.Logger): raise TypeError("Logger incorrect type") diff --git a/aacgmv2/tests/test_utils_aacgmv2.py b/aacgmv2/tests/test_utils_aacgmv2.py new file mode 100644 index 00000000..d6b015cd --- /dev/null +++ b/aacgmv2/tests/test_utils_aacgmv2.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from __future__ import division, absolute_import, unicode_literals + +import datetime as dt +import numpy as np +import pytest + +from aacgmv2 import utils + + +class TestUtilsAACGMV2: + def setup(self): + """Runs before every method to create a clean testing setup""" + self.rtol = 1.0e-4 + self.out = None + + def teardown(self): + """Runs after every method to clean up previous testing""" + del self.rtol, self.out + + @pytest.mark.parametrize('year,ref', [(1880, [-179.1494, -23.0801]), + (2015, [-179.2004, -23.0431])]) + def test_subsol(self, year, ref): + """Test the subsolar calculation""" + self.out = utils.subsol(year, 1, 0.0) + np.testing.assert_allclose(self.out, ref, rtol=self.rtol) + + @pytest.mark.parametrize('year', [(1500), (2110)]) + def test_subsol_raises_time_range(self, year): + """Test the routine failure for out-of-range dates""" + with pytest.raises(ValueError, match="subsol valid between 1601-2100"): + self.out = utils.subsol(year, 1, 0.0) + + @pytest.mark.parametrize('year,ref', + [(1500, [0.141408, -0.48357, 0.86381]), + (2015, [0.050281, -0.16057, 0.98574]), + (2110, [0.027069, -0.08006, 0.99642])]) + def test_igrf_dipole_axis(self, year, ref): + """Test the IGRF dipole axis calculation""" + self.out = utils.igrf_dipole_axis(dt.datetime(year, 1, 1)) + + np.testing.assert_allclose(self.out, ref, rtol=self.rtol) + + @pytest.mark.parametrize('gc_lat,gd_lat,mult', + [(45.0, 45.1924, False), + ([45.0, -45.0], [45.1924, -45.1924], True), + (np.array([45.0, -45.0]), + np.array([45.1924, -45.1924]), True)]) + def test_gc2gd_lat(self, gc_lat, gd_lat, mult): + """Test the geocentric to geodetic conversion""" + self.out = utils.gc2gd_lat(gc_lat) + + if mult: + np.testing.assert_allclose(self.out, gd_lat, rtol=self.rtol) + else: + np.testing.assert_almost_equal(self.out, gd_lat, decimal=4) diff --git a/aacgmv2/utils.py b/aacgmv2/utils.py new file mode 100644 index 00000000..f0389db0 --- /dev/null +++ b/aacgmv2/utils.py @@ -0,0 +1,212 @@ +# Copyright (C) 2019 NRL +# Author: Angeline Burrell +# Disclaimer: This code is under the MIT license, whose details can be found at +# the root in the LICENSE file +# +# -*- coding: utf-8 -*- +"""utilities that support the AACGM-V2 C functions. + +References +---------- +Laundal, K. M. and A. D. Richmond (2016), Magnetic Coordinate Systems, Space + Sci. Rev., doi:10.1007/s11214-016-0275-y. + +""" + +from __future__ import division, absolute_import, unicode_literals +import datetime as dt +import numpy as np + +import aacgmv2 + + +def gc2gd_lat(gc_lat): + """Convert geocentric latitude to geodetic latitude using WGS84. + + Parameters + ----------- + gc_lat : (array_like or float) + Geocentric latitude in degrees N + + Returns + --------- + gd_lat : (same as input) + Geodetic latitude in degrees N + + """ + + wgs84_e2 = 0.006694379990141317 - 1.0 + gd_lat = np.rad2deg(-np.arctan(np.tan(np.deg2rad(gc_lat)) / wgs84_e2)) + + return gd_lat + + +def subsol(year, doy, utime): + """Finds subsolar geocentric longitude and latitude. + + Parameters + ---------- + year : (int) + Calendar year between 1601 and 2100 + doy : (int) + Day of year between 1-365/366 + utime : (float) + Seconds since midnight on the specified day + + Returns + ------- + sbsllon : (float) + Subsolar longitude in degrees E for the given date/time + sbsllat : (float) + Subsolar latitude in degrees N for the given date/time + + Raises + ------ + ValueError if year is out of range + + Notes + ----- + Based on formulas in Astronomical Almanac for the year 1996, p. C24. + (U.S. Government Printing Office, 1994). Usable for years 1601-2100, + inclusive. According to the Almanac, results are good to at least 0.01 + degree latitude and 0.025 degrees longitude between years 1950 and 2050. + Accuracy for other years has not been tested. Every day is assumed to have + exactly 86400 seconds; thus leap seconds that sometimes occur on December + 31 are ignored (their effect is below the accuracy threshold of the + algorithm). + + References + ---------- + After Fortran code by A. D. Richmond, NCAR. Translated from IDL + by K. Laundal. + + """ + + # Convert from 4 digit year to 2 digit year + yr2 = year - 2000 + + if year >= 2101 or year <= 1600: + raise ValueError('subsol valid between 1601-2100. Input year is:', year) + + # Determine if this year is a leap year + nleap = np.floor((year - 1601) / 4) + nleap = nleap - 99 + if year <= 1900: + ncent = np.floor((year - 1601) / 100) + ncent = 3 - ncent + nleap = nleap + ncent + + # Calculate some of the coefficients needed to deterimine the mean longitude + # of the sun and the mean anomaly + l_0 = -79.549 + (-0.238699 * (yr2 - 4 * nleap) + 3.08514e-2 * nleap) + g_0 = -2.472 + (-0.2558905 * (yr2 - 4 * nleap) - 3.79617e-2 * nleap) + + # Days (including fraction) since 12 UT on January 1 of IYR2: + dfrac = (utime / 86400 - 1.5) + doy + + # Mean longitude of Sun: + l_sun = l_0 + 0.9856474 * dfrac + + # Mean anomaly: + grad = np.radians(g_0 + 0.9856003 * dfrac) + + # Ecliptic longitude: + lmrad = np.radians(l_sun + 1.915 * np.sin(grad) + 0.020 * np.sin(2 * grad)) + sinlm = np.sin(lmrad) + + # Days (including fraction) since 12 UT on January 1 of 2000: + epoch_day = dfrac + 365.0 * yr2 + nleap + + # Obliquity of ecliptic: + epsrad = np.radians(23.439 - 4.0e-7 * epoch_day) + + # Right ascension: + alpha = np.degrees(np.arctan2(np.cos(epsrad) * sinlm, np.cos(lmrad))) + + # Declination, which is the subsolar latitude: + sbsllat = np.degrees(np.arcsin(np.sin(epsrad) * sinlm)) + + # Equation of time (degrees): + etdeg = l_sun - alpha + etdeg = etdeg - 360.0 * np.round(etdeg / 360.0) + + # Apparent time (degrees): + aptime = utime / 240.0 + etdeg # Earth rotates one degree every 240 s. + + # Subsolar longitude: + sbsllon = 180.0 - aptime + sbsllon = sbsllon - 360.0 * np.round(sbsllon / 360.0) + + return sbsllon, sbsllat + + +def igrf_dipole_axis(date): + """Get Cartesian unit vector pointing at dipole pole in the north, + according to IGRF + + Parameters + ---------- + date : (dt.datetime) + Date and time + + Returns + ------- + m_0: (np.ndarray) + Cartesian 3 element unit vector pointing at dipole pole in the north + (geocentric coords) + + Notes + ----- + IGRF coefficients are read from the igrf12coeffs.txt file. It should also + work after IGRF updates. The dipole coefficients are interpolated to the + date, or extrapolated if date > latest IGRF model + + """ + + # get time in years, as float: + year = date.year + doy = date.timetuple().tm_yday + year_days = dt.date(date.year, 12, 31).timetuple().tm_yday + year = year + doy / year_days + + # read the IGRF coefficients + with open(aacgmv2.IGRF_COEFFS, 'r') as f_igrf: + lines = f_igrf.readlines() + + years = lines[3].split()[3:][:-1] + years = np.array(years, dtype=float) # time array + + g10 = lines[4].split()[3:] + g11 = lines[5].split()[3:] + h11 = lines[6].split()[3:] + + # secular variation coefficients (for extrapolation) + g10sv = np.float32(g10[-1]) + g11sv = np.float32(g11[-1]) + h11sv = np.float32(h11[-1]) + + # model coefficients: + g10 = np.array(g10[:-1], dtype=float) + g11 = np.array(g11[:-1], dtype=float) + h11 = np.array(h11[:-1], dtype=float) + + # get the gauss coefficient at given time: + if year <= years[-1] and year >= years[0]: + # regular interpolation + g10 = np.interp(year, years, g10) + g11 = np.interp(year, years, g11) + h11 = np.interp(year, years, h11) + else: + # extrapolation + dyear = year - years[-1] + g10 = g10[-1] + g10sv * dyear + g11 = g11[-1] + g11sv * dyear + h11 = h11[-1] + h11sv * dyear + + # calculate pole position + B_0 = np.sqrt(g10**2 + g11**2 + h11**2) + + # Calculate output + m_0 = -np.array([g11, h11, g10]) / B_0 + + return m_0 diff --git a/aacgmv2/wrapper.py b/aacgmv2/wrapper.py index 20f8b0a0..8849910d 100644 --- a/aacgmv2/wrapper.py +++ b/aacgmv2/wrapper.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019 NRL +# Copyright (C) 2019 NRL # Author: Angeline Burrell # Disclaimer: This code is under the MIT license, whose details can be found at # the root in the LICENSE file @@ -22,6 +22,7 @@ if sys.version_info.major == 2: warnings.filterwarnings('error') + def test_time(dtime): """ Test the time input and ensure it is a dt.datetime object @@ -89,8 +90,8 @@ def test_height(height, bit_code): aacgmv2.logger.warning('conversion not intended for altitudes < 0 km') # Test the conditions for using the coefficient method - if(height > aacgmv2.high_alt_coeff and - not (bit_code & (TRACE|ALLOWTRACE|BADIDEA))): + if(height > aacgmv2.high_alt_coeff + and not (bit_code & (TRACE | ALLOWTRACE | BADIDEA))): estr = ''.join(['coefficients are not valid for altitudes above ', '{:.0f} km. You '.format(aacgmv2.high_alt_coeff), 'must either use field-line tracing (trace=True or', @@ -110,11 +111,12 @@ def test_height(height, bit_code): return True + def set_coeff_path(igrf_file=False, coeff_prefix=False): """Sets the IGRF_COEFF and AACGMV_V2_DAT_PREFIX environment variables. Parameters - ----------- + ---------- igrf_file : (str or bool) Full filename of IGRF coefficient file, True to use aacgmv2.IGRF_COEFFS, or False to leave as is. (default=False) @@ -152,11 +154,12 @@ def set_coeff_path(igrf_file=False, coeff_prefix=False): return -def convert_latlon(in_lat, in_lon, height, dtime, method_code="G2A", **kwargs): + +def convert_latlon(in_lat, in_lon, height, dtime, method_code="G2A"): """Converts between geomagnetic coordinates and AACGM coordinates Parameters - ------------ + ---------- in_lat : (float) Input latitude in degrees N (code specifies type of latitude) in_lon : (float) @@ -188,20 +191,9 @@ def convert_latlon(in_lat, in_lon, height, dtime, method_code="G2A", **kwargs): Raises ------ ValueError if input is incorrect - TypeError or RuntimeError if unable to set AACGMV2 datetime + RuntimeError if unable to set AACGMV2 datetime """ - # Handle deprecated keyword arguments - for kw in kwargs.keys(): - if kw not in ['code']: - raise TypeError('unexpected keyword argument [{:s}]'.format(kw)) - else: - method_code = kwargs[kw] - warnings.warn("".join(["Deprecated keyword argument 'code' will be", - " removed in version 2.6.1, please update ", - "your routine to use 'method_code'"]), - category=FutureWarning) - # Test time dtime = test_time(dtime) @@ -238,17 +230,14 @@ def convert_latlon(in_lat, in_lon, height, dtime, method_code="G2A", **kwargs): try: c_aacgmv2.set_datetime(dtime.year, dtime.month, dtime.day, dtime.hour, dtime.minute, dtime.second) - except TypeError as terr: - raise TypeError("unable to set time for {:}: {:}".format(dtime, terr)) - except RuntimeError as rerr: - raise RuntimeError("unable to set time for {:}: {:}".format(dtime, - rerr)) + except (TypeError, RuntimeError) as err: + raise RuntimeError("cannot set time for {:}: {:}".format(dtime, err)) # convert location try: lat_out, lon_out, r_out = c_aacgmv2.convert(in_lat, in_lon, height, bit_code) - except: + except Exception: err = sys.exc_info()[0] estr = "unable to perform conversion at {:.1f},".format(in_lat) estr = "{:s}{:.1f} {:.1f} km, {:} ".format(estr, in_lon, height, dtime) @@ -258,12 +247,12 @@ def convert_latlon(in_lat, in_lon, height, dtime, method_code="G2A", **kwargs): return lat_out, lon_out, r_out -def convert_latlon_arr(in_lat, in_lon, height, dtime, method_code="G2A", - **kwargs): + +def convert_latlon_arr(in_lat, in_lon, height, dtime, method_code="G2A"): """Converts between geomagnetic coordinates and AACGM coordinates. Parameters - ------------ + ---------- in_lat : (np.ndarray or list or float) Input latitude in degrees N (method_code specifies type of latitude) in_lon : (np.ndarray or list or float) @@ -295,10 +284,10 @@ def convert_latlon_arr(in_lat, in_lon, height, dtime, method_code="G2A", Raises ------ ValueError if input is incorrect - TypeError or RuntimeError if unable to set AACGMV2 datetime + RuntimeError if unable to set AACGMV2 datetime Notes - ------- + ----- At least one of in_lat, in_lon, and height must be a list or array. If errors are encountered, NaN or Inf will be included in the input so @@ -308,60 +297,42 @@ def convert_latlon_arr(in_lat, in_lon, height, dtime, method_code="G2A", Multi-dimensional arrays are not allowed. """ - # Handle deprecated keyword arguments - for kw in kwargs.keys(): - if kw not in ['code']: - raise TypeError('unexpected keyword argument [{:s}]'.format(kw)) - else: - method_code = kwargs[kw] - warnings.warn("".join(["Deprecated keyword argument 'code' will be", - " removed in version 2.6.1, please update ", - "your routine to use 'method_code'"]), - category=FutureWarning) - # Recast the data as numpy arrays in_lat = np.array(in_lat) in_lon = np.array(in_lon) height = np.array(height) - # If one or two of these elements is a float or int, create an array + # If one or two of these elements is a float, int, or single element array, + # create an array equal to the length of the longest input test_array = np.array([len(in_lat.shape), len(in_lon.shape), len(height.shape)]) + if test_array.max() > 1: raise ValueError("unable to process multi-dimensional arrays") - - if test_array.min() == 0: + else: if test_array.max() == 0: aacgmv2.logger.info("".join(["for a single location, consider ", - "using convert_latlon or get_aacgm_coord"])) + "using convert_latlon or ", + "get_aacgm_coord"])) in_lat = np.array([in_lat]) in_lon = np.array([in_lon]) height = np.array([height]) else: - imax = test_array.argmax() - max_shape = in_lat.shape if imax == 0 else (in_lon.shape \ - if imax == 1 else height.shape) - if not test_array[0]: - in_lat = np.full(shape=max_shape, fill_value=in_lat) - if not test_array[1]: - in_lon = np.full(shape=max_shape, fill_value=in_lon) - if not test_array[2]: - height = np.full(shape=max_shape, fill_value=height) + max_len = max([len(arr) for i, arr in enumerate([in_lat, in_lon, + height]) + if test_array[i] > 0]) + + if not test_array[0] or (len(in_lat) == 1 and max_len > 1): + in_lat = np.full(shape=(max_len,), fill_value=in_lat) + if not test_array[1] or (len(in_lon) == 1 and max_len > 1): + in_lon = np.full(shape=(max_len,), fill_value=in_lon) + if not test_array[2] or (len(height) == 1 and max_len > 1): + height = np.full(shape=(max_len,), fill_value=height) # Ensure that lat, lon, and height are the same length or if the lengths # differ that the different ones contain only a single value if not (in_lat.shape == in_lon.shape and in_lat.shape == height.shape): - shape_dict = {'lat': in_lat.shape, 'lon': in_lon.shape, - 'height': height.shape} - ulen = np.unique(shape_dict.values()) - array_key = [kk for i, kk in enumerate(shape_dict.keys()) - if shape_dict[kk] != (1,)] - if len(array_key) == 3: - raise ValueError('lat, lon, and height arrays are mismatched') - elif len(array_key) == 2: - if shape_dict[array_key[0]] == shape_dict[array_dict[1]]: - raise ValueError('{:s} and {:s} arrays are mismatched'.format(\ - *array_key)) + raise ValueError('lat, lon, and height arrays are mismatched') # Test time dtime = test_time(dtime) @@ -393,16 +364,12 @@ def convert_latlon_arr(in_lat, in_lon, height, dtime, method_code="G2A", # Constrain longitudes between -180 and 180 in_lon = ((in_lon + 180.0) % 360.0) - 180.0 - # Set current date and time try: c_aacgmv2.set_datetime(dtime.year, dtime.month, dtime.day, dtime.hour, dtime.minute, dtime.second) - except TypeError as terr: - raise TypeError("unable to set time for {:}: {:}".format(dtime, terr)) - except RuntimeError as rerr: - raise RuntimeError("unable to set time for {:}: {:}".format(dtime, - rerr)) + except (TypeError, RuntimeError) as err: + raise RuntimeError("cannot set time for {:}: {:}".format(dtime, err)) try: lat_out, lon_out, r_out, bad_ind = c_aacgmv2.convert_arr(list(in_lat), @@ -426,11 +393,12 @@ def convert_latlon_arr(in_lat, in_lon, height, dtime, method_code="G2A", return lat_out, lon_out, r_out + def get_aacgm_coord(glat, glon, height, dtime, method="ALLOWTRACE"): """Get AACGM latitude, longitude, and magnetic local time Parameters - ------------ + ---------- glat : (float) Geodetic latitude in degrees N glon : (float) @@ -474,7 +442,7 @@ def get_aacgm_coord_arr(glat, glon, height, dtime, method="ALLOWTRACE"): """Get AACGM latitude, longitude, and magnetic local time Parameters - ------------ + ---------- glat : (np.array or list) Geodetic latitude in degrees N glon : (np.array or list) @@ -512,18 +480,17 @@ def get_aacgm_coord_arr(glat, glon, height, dtime, method="ALLOWTRACE"): if np.any(np.isfinite(mlon)): # Get magnetic local time mlt = convert_mlt(mlon, dtime, m2a=False) - if not isinstance(mlt, type(mlat)): - mlt = np.array([mlt]) else: mlt = np.full(shape=len(mlat), fill_value=np.nan) return mlat, mlon, mlt + def convert_str_to_bit(method_code): """convert string code specification to bit code specification Parameters - ------------ + ---------- method_code : (str) Bitwise code for passing options into converter (default=0) G2A - geographic (geodetic) to AACGM-v2 @@ -534,12 +501,12 @@ def convert_str_to_bit(method_code): GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2 Returns - -------- + ------- bit_code : (int) Method code specification in bits Notes - -------- + ----- Multiple codes should be seperated by pipes '|'. Invalid parts of the code are ignored and no code defaults to 'G2A'. @@ -559,6 +526,7 @@ def convert_str_to_bit(method_code): return bit_code + def convert_bool_to_bit(a2g=False, trace=False, allowtrace=False, badidea=False, geocentric=False): """convert boolian flags to bit code specification @@ -578,7 +546,7 @@ def convert_bool_to_bit(a2g=False, trace=False, allowtrace=False, True for geodetic, False for geocentric w/RE=6371.2 (default=False) Returns - -------- + ------- bit_code : (int) code specification in bits @@ -597,11 +565,12 @@ def convert_bool_to_bit(a2g=False, trace=False, allowtrace=False, return bit_code + def convert_mlt(arr, dtime, m2a=False): """Converts between magnetic local time (MLT) and AACGM-v2 longitude Parameters - ------------ + ---------- arr : (array-like or float) Magnetic longitudes (degrees E) or MLTs (hours) to convert dtime : (array-like or datetime.datetime) @@ -611,12 +580,12 @@ def convert_mlt(arr, dtime, m2a=False): (False). (default=False) Returns - -------- + ------- out : (np.ndarray) Converted coordinates/MLT in degrees E or hours (as appropriate) Notes - ------- + ----- This routine previously based on Laundal et al. 2016, but now uses the improved calculation available in AACGM-V2.4. diff --git a/appveyor.yml b/appveyor.yml index ab366cb6..6245af26 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -55,12 +55,11 @@ environment: PYTHON_VERSION: '3.8' PYTHON_ARCH: '64' - TOXENV: check - PYTHON_HOME: C:\Python36 - PYTHON_VERSION: '3.6' + PYTHON_HOME: C:\Python37 + PYTHON_VERSION: '3.7' PYTHON_ARCH: '32' - TOXENV: '2.7-nocover' TOXPYTHON: C:\python27\python.exe - PYTHON_HOME: C:\python27 PYTHON_VERSION: '2.7' PYTHON_ARCH: '32' @@ -72,7 +71,6 @@ environment: PYTHON_ARCH: '64' - TOXENV: '3.6-nocover' TOXPYTHON: C:\python36\python.exe - PYTHON_HOME: C:\python36 PYTHON_VERSION: '3.6' PYTHON_ARCH: '32' @@ -84,7 +82,6 @@ environment: PYTHON_ARCH: '64' - TOXENV: '3.7-nocover' TOXPYTHON: C:\python37\python.exe - PYTHON_HOME: C:\python37 PYTHON_VERSION: '3.7' PYTHON_ARCH: '32' @@ -94,6 +91,17 @@ environment: PYTHON_HOME: C:\python37-x64 PYTHON_VERSION: '3.7' PYTHON_ARCH: '64' + - TOXENV: '3.8-nocover' + TOXPYTHON: C:\python38\python.exe + PYTHON_HOME: C:\python38 + PYTHON_VERSION: '3.8' + PYTHON_ARCH: '32' + - TOXENV: '3.8-nocover' + TOXPYTHON: C:\python38-x64\python.exe + WINDOWS_SDK_VERSION: v7.1 + PYTHON_HOME: C:\python38-x64 + PYTHON_VERSION: '3.8' + PYTHON_ARCH: '64' init: - ps: echo $env:TOXENV - ps: ls C:\Python* @@ -125,6 +133,9 @@ matrix: allow_failures: - TOXENV: '2.7-nocover' - TOXENV: '3.7-nocover' + PYTHON_ARCH: '64' + - TOXENV: '3.8-nocover' + PYTHON_ARCH: '64' ### To enable remote debugging uncomment this: # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/ci/appveyor-bootstrap.py b/ci/appveyor-bootstrap.py index 7483f570..8116a586 100644 --- a/ci/appveyor-bootstrap.py +++ b/ci/appveyor-bootstrap.py @@ -54,6 +54,9 @@ def download_file(url, path): print("Downloading: {} (into {})".format(url, path)) progress = 0 + # urllib requires a reporthook function with these three inputs. + # This is why the incremental progress variable is defined above + # the report funciton. def report(count, size, total): progress_total = count * size if progress_total - progress > 1000000: diff --git a/ci/bootstrap.py b/ci/bootstrap.py index aec07822..557147bd 100644 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -3,27 +3,33 @@ from __future__ import absolute_import, print_function, unicode_literals import os +import subprocess import sys + if __name__ == "__main__": base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + print("Project path: {0}".format(base_path)) env_path = os.path.join(base_path, ".tox", "bootstrap") + if sys.platform == "win32": bin_path = os.path.join(env_path, "Scripts") else: bin_path = os.path.join(env_path, "bin") + if not os.path.exists(env_path): - import subprocess print("Making bootstrap env in: {0} ...".format(env_path)) try: subprocess.check_call(["virtualenv", env_path]) except Exception: subprocess.check_call([sys.executable, "-m", "virtualenv", env_path]) + print("Installing `jinja2` and `matrix` into bootstrap environment ...") subprocess.check_call([os.path.join(bin_path, "pip"), "install", "jinja2", "matrix"]) + activate = os.path.join(bin_path, "activate_this.py") exec(compile(open(activate, "rb").read(), activate, "exec"), dict(__file__=activate)) @@ -34,15 +40,11 @@ jinja = jinja2.Environment( loader=jinja2.FileSystemLoader(os.path.join(base_path, "ci", "templates")), - trim_blocks=True, - lstrip_blocks=True, - keep_trailing_newline=True - ) + trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) tox_envs = {} for (alias, conf) in matrix.from_file(os.path.join(base_path, "setup.cfg")).items(): python = conf["python_versions"] - #deps = conf["dependencies"] if "coverage_flags" in conf: cover = {"false": False, "true": True}[conf["coverage_flags"].lower()] @@ -51,7 +53,6 @@ tox_envs[alias] = { "python": "python" + python if "py" not in python else python, - #"deps": deps.split(), } if "coverage_flags" in conf: tox_envs[alias].update(cover=cover) diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml index 36605cee..fcd76741 100644 --- a/ci/templates/.travis.yml +++ b/ci/templates/.travis.yml @@ -1,25 +1,26 @@ language: python python: - - "2.7" - - "3.6" - - "3.7" - - "3.8" -sudo: false +{% for env, config in tox_environments|dictsort %}{% if not config.cover %} + - "{{ config.python[-3:] }}" +{% endif %}{% endfor %} env: global: - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so before_install: - python --version - uname -a - lsb_release -a install: - - pip install coveralls - - "python setup.py install" - - pip install tox-travis + - pip install numpy tox-travis coveralls + - python setup.py install script: - - tox - - coverage run --source aacgmv2 -m py.test -after_sucess: coveralls + - | + if [ $TRAVIS_PYTHON_VERSION == "3.7" ]; then + tox -e check,docs + fi + - tox -e $TRAVIS_PYTHON_VERSION +after_success: + - coveralls --rcfile=setup.cfg notifications: email: on_success: never diff --git a/ci/templates/appveyor.yml b/ci/templates/appveyor.yml index 7a5432fb..b544a185 100644 --- a/ci/templates/appveyor.yml +++ b/ci/templates/appveyor.yml @@ -6,72 +6,33 @@ environment: global: WITH_COMPILER: 'cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd' matrix: - - TOXENV: '2.7-buildonly-nocover' - TOXPYTHON: C:\python27\python.exe - WINDOWS_SDK_VERSION: v7.0 - PYTHON_HOME: C:\python27 - PYTHON_VERSION: '2.7' - PYTHON_ARCH: '32' - - TOXENV: '2.7-buildonly-nocover' - TOXPYTHON: C:\python27-x64\python.exe - WINDOWS_SDK_VERSION: v7.0 - PYTHON_HOME: C:\python27-x64 - PYTHON_VERSION: '2.7' - PYTHON_ARCH: '64' - - TOXENV: '3.6-buildonly-nocover' - TOXPYTHON: C:\python36\python.exe - WINDOWS_SDK_VERSION: v7.1 - PYTHON_HOME: C:\python36 - PYTHON_VERSION: '3.6' - PYTHON_ARCH: '32' - - TOXENV: '3.6-buildonly-nocover' - TOXPYTHON: C:\python36-x64\python.exe - WINDOWS_SDK_VERSION: v7.1 - PYTHON_HOME: C:\python36-x64 - PYTHON_VERSION: '3.6' - PYTHON_ARCH: '64' - - TOXENV: '3.7-buildonly-nocover' - TOXPYTHON: C:\python37\python.exe - WINDOWS_SDK_VERSION: v7.1 - PYTHON_HOME: C:\python37 - PYTHON_VERSION: '3.7' - PYTHON_ARCH: '32' - - TOXENV: '3.7-buildonly-nocover' - TOXPYTHON: C:\python37-x64\python.exe - WINDOWS_SDK_VERSION: v7.1 - PYTHON_HOME: C:\python37-x64 - PYTHON_VERSION: '3.7' - PYTHON_ARCH: '64' - - TOXENV: '3.8-buildonly-nocover' - TOXPYTHON: C:\python38\python.exe - WINDOWS_SDK_VERSION: v7.1 - PYTHON_HOME: C:\python38 - PYTHON_VERSION: '3.8' +{% for env, config in tox_environments|dictsort %}{% if not config.cover %} + - TOXENV: '{{ config.python[-3:] }}-buildonly-nocover' + TOXPYTHON: C:\{{ config.python.replace('.', '') }}\python.exe + WINDOWS_SDK_VERSION: v7.{{ '1' if config.python[-3] == '3' else '0' }} + PYTHON_HOME: C:\{{ config.python.replace('.', '') }} + PYTHON_VERSION: '{{ config.python[-3:] }}' PYTHON_ARCH: '32' - - TOXENV: '3.8-buildonly-nocover' - TOXPYTHON: C:\python38-x64\python.exe - WINDOWS_SDK_VERSION: v7.1 - PYTHON_HOME: C:\python38-x64 - PYTHON_VERSION: '3.8' + - TOXENV: '{{ config.python[-3:] }}-buildonly-nocover' + TOXPYTHON: C:\{{ config.python.replace('.', '') }}-x64\python.exe + WINDOWS_SDK_VERSION: v7.{{ '1' if config.python[-3] == '3' else '0' }} + PYTHON_HOME: C:\{{ config.python.replace('.', '') }}-x64 + PYTHON_VERSION: '{{ config.python[-3:] }}' PYTHON_ARCH: '64' +{% endif %}{% endfor %} - TOXENV: check - PYTHON_HOME: C:\Python36 - PYTHON_VERSION: '3.6' + PYTHON_HOME: C:\Python37 + PYTHON_VERSION: '3.7' PYTHON_ARCH: '32' -{% for env, config in tox_environments|dictsort %}{% if config.python in ('python2.6', 'python2.7', 'python3.6', 'python3.7', 'python3.8',) and not config.cover %} - - TOXENV: '{{ env }}{% if config.cover %},codecov{% endif %}' +{% for env, config in tox_environments|dictsort %}{% if config.python in ('python2.7', 'python3.6', 'python3.7', 'python3.8',) and not config.cover %} + - TOXENV: '{{ env }}' TOXPYTHON: C:\{{ config.python.replace('.', '') }}\python.exe - PYTHON_HOME: C:\{{ config.python.replace('.', '') }} PYTHON_VERSION: '{{ config.python[-3:] }}' PYTHON_ARCH: '32' - - TOXENV: '{{ env }}{% if config.cover %},codecov{% endif %}' + - TOXENV: '{{ env }}' TOXPYTHON: C:\{{ config.python.replace('.', '') }}-x64\python.exe - {%- if config.python != 'python3.5' %} - WINDOWS_SDK_VERSION: v7.{{ '1' if config.python[-3] == '3' else '0' }} - {%- endif %} - PYTHON_HOME: C:\{{ config.python.replace('.', '') }}-x64 PYTHON_VERSION: '{{ config.python[-3:] }}' PYTHON_ARCH: '64' @@ -107,6 +68,9 @@ matrix: allow_failures: - TOXENV: '2.7-nocover' - TOXENV: '3.7-nocover' + PYTHON_ARCH: '64' + - TOXENV: '3.8-nocover' + PYTHON_ARCH: '64' ### To enable remote debugging uncomment this: # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/ci/templates/tox.ini b/ci/templates/tox.ini index 4f1f1242..0db1a278 100644 --- a/ci/templates/tox.ini +++ b/ci/templates/tox.ini @@ -2,8 +2,11 @@ envlist = clean, check, -{% for env in tox_environments|sort %} +{% for env, config in tox_environments|dictsort %} {{ env }}, +{% if not config.cover %} + {{ config.python[-3:] }}-buildonly-nocover, +{% endif %} {% endfor %} report, docs @@ -15,23 +18,14 @@ setenv = passenv = * deps = - pytest - numpy + 2.7: zipp<=1.2.0 + 2.7: pytest<5.0.0 + 3.6,3.7,3.8: pytest + 2.7: numpy<=1.16.6 + 3.6,3.7,3.8: numpy commands = python setup.py clean --all build_ext --force --inplace - {posargs:py.test -vv --ignore=src --doctest-glob='*.rst'} - -[testenv:spell] -setenv = - SPELLCHECK=1 -commands = - sphinx-build -b spelling docs dist/docs -skip_install = true -usedevelop = true -deps = - -r{toxinidir}/docs/requirements.txt - sphinxcontrib-spelling - pyenchant + python -m pytest {posargs:-vv --ignore=c_aacgmv2 --doctest-glob='*.rst'} [testenv:docs] deps = @@ -52,19 +46,20 @@ passenv = * [testenv:check] -basepython = python3.6 deps = docutils check-manifest flake8 readme pygments + twine skip_install = true usedevelop = false commands = - python setup.py check --strict --metadata --restructuredtext + python setup.py sdist + twine check dist/* check-manifest {toxinidir} - flake8 src tests + flake8 --ignore=F401,W503 aacgmv2 [testenv:coveralls] deps = @@ -74,18 +69,7 @@ usedevelop = false commands = coverage combine coverage report - coveralls --merge=extension-coveralls.json [] - -[testenv:codecov] -deps = - codecov -skip_install = true -usedevelop = false -commands = - coverage combine - coverage report - coverage xml --ignore-errors - codecov [] + coveralls --rcfile=setup.cfg --merge=extension-coveralls.json [] [testenv:extension-coveralls] deps = @@ -93,10 +77,9 @@ deps = skip_install = true usedevelop = false commands = - coveralls --build-root=. --include=src --dump=extension-coveralls.json [] + coveralls --rcfile=setup.cfg --build-root=. --include=src --dump=extension-coveralls.json [] [testenv:report] -basepython = python3.6 deps = coverage skip_install = true usedevelop = false @@ -126,7 +109,7 @@ setenv = usedevelop = true commands = python setup.py clean --all build_ext --force --inplace - {posargs:py.test --cov --cov-report=term-missing -vv --doctest-glob='*.rst'} + python -m pytest {posargs:--cov --cov-report=term-missing -vv --doctest-glob='*.rst'} {% endif %} {% if config.cover or config.deps %} deps = @@ -139,28 +122,12 @@ deps = {{ dep }} {% endfor %} -{% endfor %} - -[testenv:2.7-buildonly-nocover] -basepython = {env:TOXPYTHON:python2.7} -deps = -skip_install = true -commands = - -[testenv:3.6-buildonly-nocover] -basepython = {env:TOXPYTHON:python3.6} -deps = -skip_install = true -commands = - -[testenv:3.7-buildonly-nocover] -basepython = {env:TOXPYTHON:python3.7} +{% if not config.cover %} +[testenv:{{ config.python[-3:] }}-buildonly-nocover] +basepython = {env:TOXPYTHON:{{ config.python }}} deps = skip_install = true commands = +{% endif %} -[testenv:3.8-buildonly-nocover] -basepython = {env:TOXPYTHON:python3.8} -deps = -skip_install = true -commands = +{% endfor %} diff --git a/docs/conf.py b/docs/conf.py index 978efbaf..cd5947ef 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,10 +13,6 @@ 'sphinxcontrib.napoleon', 'numpydoc' ] -if os.getenv('SPELLCHECK'): - extensions += 'sphinxcontrib.spelling', - spelling_show_suggestions = True - spelling_lang = 'en_US' source_suffix = '.rst' master_doc = 'index' @@ -24,7 +20,7 @@ year = u'2019' author = u'Angeline G. Burrell, Christer van der Meeren, Karl M. Laundal' copyright = '{0}, {1}'.format(year, author) -version = release = u'2.6.0' +version = release = u'2.6.1' # on_rtd is whether we are on readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' diff --git a/docs/installation.rst b/docs/installation.rst index a6804e45..46f02afb 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -3,10 +3,10 @@ Installation ============ This package requires NumPy, which you can install alone or as a part of SciPy. -`Some Python distributions `_ come with NumPy/SciPy pre-installed. For Python distributions +`Some Python distributions `_ come with NumPy/SciPy pre-installed. For Python distributions without NumPy/SciPy, Windows/Mac users should install -`pre-compiled binaries of NumPy/SciPy `_, and Linux users may have NumPy/SciPy -available in `their repositories `_. +`pre-compiled binaries of NumPy/SciPy `_, and Linux users may have NumPy/SciPy +available in `their repositories `_. When you have NumPy, install this package at the command line using ``pip`` [1]_:: @@ -16,8 +16,9 @@ When you have NumPy, install this package at the command line using The package has been tested with the following setups (others might work, too): * Mac (64 bit), Windows (32/64 bit), and Linux (64 bit) -* Python 2.7 (except Windows), 3.6, and 3.7 (except Windows 64 bit) +* Python 2.7 (except Windows), 3.6, 3.7 (except Windows 64 bit), + and 3.8 (except Windows 64 bit) .. [1] pip is included with Python 2 from v2.7.16 and Python 3 from v3.6. If you don't have pip, - `get it here `_. + `get it here `_. diff --git a/docs/reference/_aacgmv2.rst b/docs/reference/_aacgmv2.rst index 67f53871..71e39377 100644 --- a/docs/reference/_aacgmv2.rst +++ b/docs/reference/_aacgmv2.rst @@ -1,7 +1,7 @@ aacgmv2._aacgmv2 ================ -This submodule contains the interface to the AACGM-v2 C library. For the user-friendly wrapper, see the functions in :py:module:`aacgmv2.wrapper`. +This submodule contains the interface to the AACGM-v2 C library. For the user-friendly wrapper, see the functions in :py:mod:`aacgmv2.wrapper`. .. automodule:: aacgmv2._aacgmv2 :members: diff --git a/docs/reference/aacgmv2.rst b/docs/reference/aacgmv2.rst index 5bf1daaf..52ef12ab 100644 --- a/docs/reference/aacgmv2.rst +++ b/docs/reference/aacgmv2.rst @@ -9,6 +9,6 @@ These functions are available when you ``import aacgmv2``. .. automodule:: aacgmv2.wrapper :members: -.. automodule:: aacgmv2.deprecated +.. automodule:: aacgmv2.utils :members: diff --git a/setup.cfg b/setup.cfg index 3241a072..7aead064 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,40 @@ [aliases] release = register clean --all sdist +[bumpversion] +current_version = 2.6.1 +commit = True +tag = True + +[bumpversion:file:setup.py] + +[bumpversion:file:docs/conf.py] + +[bumpversion:file:aacgmv2/__init__.py] + [flake8] max-line-length = 140 exclude = tests/*,*/migrations/*,*/south_migrations/* +[coverage:paths] +source = + aacgmv2 + c_aacgmv2 + +[coverage:run] +branch = True +relative_files = True +include = */aacgmv2/* + */aacgmv2/tests/* +source = aacgmv2 + c_aacgmv2 +parallel = True + +[coverage:report] +show_missing = true +precision = 2 +omit = *migrations* + [tool:pytest] norecursedirs = .git @@ -62,8 +92,7 @@ python_versions = 2.7 3.6 3.7 - -#dependencies = + 3.8 coverage_flags = : true diff --git a/setup.py b/setup.py index 72d379a2..4ce70652 100644 --- a/setup.py +++ b/setup.py @@ -24,12 +24,13 @@ def read(fname, **kwargs): setup( name='aacgmv2', - version='2.6.0', + version='2.6.1', license='MIT', description='A Python wrapper for AACGM-v2 magnetic coordinates', long_description='%s\n%s' % (read('README.rst'), - re.sub(':[a-z]+:`~?(.*?)`', - r'``\1``', read('CHANGELOG.rst'))), + re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', + read('CHANGELOG.rst'))), + long_description_content_type='text/plain', author='Angeline G. Burrell, Christer van der Meeren', author_email='angeline.burrell@nrl.navy.mil', url='https://github.com/aburrell/aacgmv2', @@ -51,6 +52,7 @@ def read(fname, **kwargs): 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Scientific/Engineering :: Physics', 'Topic :: Utilities', @@ -69,7 +71,7 @@ def read(fname, **kwargs): install_requires=[ 'numpy', ], - extras_require={'test':['pytest'], + extras_require={'test': ['pytest'], }, ext_modules=[ Extension('aacgmv2._aacgmv2', diff --git a/tox.ini b/tox.ini index 2ef5263b..fcaa6a0d 100644 --- a/tox.ini +++ b/tox.ini @@ -4,10 +4,16 @@ envlist = check, 2.7, 2.7-nocover, + 2.7-buildonly-nocover, 3.6, 3.6-nocover, + 3.6-buildonly-nocover, 3.7, 3.7-nocover, + 3.7-buildonly-nocover, + 3.8, + 3.8-nocover, + 3.8-buildonly-nocover, report, docs @@ -18,23 +24,14 @@ setenv = passenv = * deps = - pytest - numpy + 2.7: zipp<=1.2.0 + 2.7: pytest<5.0.0 + 3.6,3.7,3.8: pytest + 2.7: numpy<=1.16.6 + 3.6,3.7,3.8: numpy commands = python setup.py clean --all build_ext --force --inplace - {posargs:py.test -vv --ignore=src --doctest-glob='*.rst'} - -[testenv:spell] -setenv = - SPELLCHECK=1 -commands = - sphinx-build -b spelling docs dist/docs -skip_install = true -usedevelop = true -deps = - -r{toxinidir}/docs/requirements.txt - sphinxcontrib-spelling - pyenchant + python -m pytest {posargs:-vv --ignore=c_aacgmv2 --doctest-glob='*.rst'} [testenv:docs] deps = @@ -55,19 +52,20 @@ passenv = * [testenv:check] -basepython = python3.6 deps = docutils check-manifest flake8 readme pygments + twine skip_install = true usedevelop = false commands = - python setup.py check --strict --metadata --restructuredtext + python setup.py sdist + twine check dist/* check-manifest {toxinidir} - flake8 src tests + flake8 --ignore=F401,W503 aacgmv2 [testenv:coveralls] deps = @@ -77,18 +75,7 @@ usedevelop = false commands = coverage combine coverage report - coveralls --merge=extension-coveralls.json [] - -[testenv:codecov] -deps = - codecov -skip_install = true -usedevelop = false -commands = - coverage combine - coverage report - coverage xml --ignore-errors - codecov [] + coveralls --rcfile=setup.cfg --merge=extension-coveralls.json [] [testenv:extension-coveralls] deps = @@ -96,10 +83,9 @@ deps = skip_install = true usedevelop = false commands = - coveralls --build-root=. --include=src --dump=extension-coveralls.json [] + coveralls --rcfile=setup.cfg --build-root=. --include=src --dump=extension-coveralls.json [] [testenv:report] -basepython = python3.6 deps = coverage skip_install = true usedevelop = false @@ -122,14 +108,21 @@ setenv = usedevelop = true commands = python setup.py clean --all build_ext --force --inplace - {posargs:py.test --cov --cov-report=term-missing -vv --doctest-glob='*.rst'} + python -m pytest {posargs:--cov --cov-report=term-missing -vv --doctest-glob='*.rst'} deps = {[testenv]deps} pytest-cov + [testenv:2.7-nocover] basepython = {env:TOXPYTHON:python2.7} +[testenv:2.7-buildonly-nocover] +basepython = {env:TOXPYTHON:python2.7} +deps = +skip_install = true +commands = + [testenv:3.6] basepython = {env:TOXPYTHON:python3.6} setenv = @@ -139,14 +132,21 @@ setenv = usedevelop = true commands = python setup.py clean --all build_ext --force --inplace - {posargs:py.test --cov --cov-report=term-missing -vv --doctest-glob='*.rst'} + python -m pytest {posargs:--cov --cov-report=term-missing -vv --doctest-glob='*.rst'} deps = {[testenv]deps} pytest-cov + [testenv:3.6-nocover] basepython = {env:TOXPYTHON:python3.6} +[testenv:3.6-buildonly-nocover] +basepython = {env:TOXPYTHON:python3.6} +deps = +skip_install = true +commands = + [testenv:3.7] basepython = {env:TOXPYTHON:python3.7} setenv = @@ -156,35 +156,42 @@ setenv = usedevelop = true commands = python setup.py clean --all build_ext --force --inplace - {posargs:py.test --cov --cov-report=term-missing -vv --doctest-glob='*.rst'} + python -m pytest {posargs:--cov --cov-report=term-missing -vv --doctest-glob='*.rst'} deps = {[testenv]deps} pytest-cov + [testenv:3.7-nocover] basepython = {env:TOXPYTHON:python3.7} - -[testenv:2.7-buildonly-nocover] -basepython = {env:TOXPYTHON:python2.7} +[testenv:3.7-buildonly-nocover] +basepython = {env:TOXPYTHON:python3.7} deps = skip_install = true commands = -[testenv:3.6-buildonly-nocover] -basepython = {env:TOXPYTHON:python3.6} -deps = -skip_install = true +[testenv:3.8] +basepython = {env:TOXPYTHON:python3.8} +setenv = + {[testenv]setenv} + WITH_COVERAGE=yes + PY_CCOV=-coverage +usedevelop = true commands = - -[testenv:3.7-buildonly-nocover] -basepython = {env:TOXPYTHON:python3.7} + python setup.py clean --all build_ext --force --inplace + python -m pytest {posargs:--cov --cov-report=term-missing -vv --doctest-glob='*.rst'} deps = -skip_install = true -commands = + {[testenv]deps} + pytest-cov + + +[testenv:3.8-nocover] +basepython = {env:TOXPYTHON:python3.8} [testenv:3.8-buildonly-nocover] basepython = {env:TOXPYTHON:python3.8} deps = skip_install = true commands = +