diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b3a40e0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 100 +extend-ignore = E201,E202,E203,E221,E231,E741 diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 409315f..953689c 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -1,41 +1,72 @@ -name: Publish to PyPI +name: Wheel building on: - release: - types: [released] + pull_request: + # We also want this workflow triggered if the 'Build all wheels' + # label is added or present when PR is updated + types: + - opened + - synchronize + - labeled + push: + branches: + - master + tags: + - '*' + +permissions: + contents: read jobs: - validate: - name: Validate metadata - runs-on: ubuntu-latest - steps: - - uses: spacetelescope/action-publish_to_pypi/validate@master - - build_wheels: - name: Build wheels on ${{ matrix.os }} - needs: [validate] - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-20.04, macos-10.15] - - steps: - - uses: spacetelescope/action-publish_to_pypi/build-wheel@master - - build_sdist: - name: Build source distribution - needs: [validate] - runs-on: ubuntu-latest - steps: - - uses: spacetelescope/action-publish_to_pypi/build-sdist@master - - upload_pypi: - needs: [build_wheels, build_sdist] - runs-on: ubuntu-latest - steps: - - uses: spacetelescope/action-publish_to_pypi/publish@master - with: - test: ${{ secrets.PYPI_TEST }} - user: ${{ secrets.PYPI_USERNAME_STSCI_MAINTAINER }} - password: ${{ secrets.PYPI_PASSWORD_STSCI_MAINTAINER }} # WARNING: Do not hardcode secret values here! If you want to use a different user or password, you can override this secret by creating one with the same name in your Github repository settings. - test_password: ${{ secrets.PYPI_PASSWORD_STSCI_MAINTAINER_TEST }} + test_wheel_building: + # This ensures that a couple of targets work fine in pull requests and pushes + permissions: + contents: none + uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@v1 + if: (github.event_name == 'push' || github.event_name == 'pull_request') + with: + upload_to_pypi: false + upload_to_anaconda: false + test_extras: test + test_command: pytest -p no:warnings --pyargs spherical_geometry.tests -v + targets: | + - cp39-manylinux_x86_64 + + + build_and_publish: + # This job builds the wheels and publishes them to PyPI for all + # tags. For PRs with the "Build wheels" label, wheels are built, + # but are not uploaded to PyPI. + + permissions: + contents: none + + uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@v1 + + if: (github.repository == 'spacetelescope/spherical_geometry' && (github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'Build wheels'))) + with: + # We upload to PyPI for all tag pushes + upload_to_pypi: ${{ startsWith(github.ref, 'refs/tags/') && github.event_name == 'push' }} + + # BUG: https://github.com/spacetelescope/spherical_geometry/issues/244 + # BUG: https://github.com/spacetelescope/spherical_geometry/issues/245 + test_extras: test + test_command: pytest -p no:warnings --pyargs spherical_geometry.tests -v + targets: | + # Linux wheels + - cp3*-manylinux_x86_64 + - cp3*-musllinux_x86_64 + # FIXME: https://github.com/spacetelescope/spherical_geometry/issues/247 + #- cp3*-manylinux_aarch64 + + # MacOS X wheels - we deliberately do not build universal2 wheels. + # Note that the arm64 wheels are not actually tested so we + # rely on local manual testing of these to make sure they are ok. + - cp3*macosx_x86_64 + - cp3*macosx_arm64 + + # Windows wheels + - cp3*win32 + - cp3*win_amd64 + secrets: + pypi_token: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/spherical_geometry.yml b/.github/workflows/spherical_geometry.yml index c63e29f..a2cb14b 100644 --- a/.github/workflows/spherical_geometry.yml +++ b/.github/workflows/spherical_geometry.yml @@ -19,88 +19,35 @@ permissions: contents: read jobs: - build: - name: Python Testing ${{ matrix.python-version }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install numpy - python -m pip install -e .[test] - - name: Test with pytest - run: | - pip freeze - pytest - - devdeps: - name: Python Testing with dev versions of dependencies - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install --pre --upgrade --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy - python -m pip install --pre --upgrade --extra-index-url https://pypi.anaconda.org/astropy/simple astropy - python -m pip install -e .[test] - - name: Test with pytest - run: | - pip freeze - pytest - - code_coverage: - name: Code Coverage Report - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e .[test] - - name: Test with pytest and code coverage - run: | - pip freeze - pytest --cov-report=xml --cov=. --cov-config=setup.cfg - - name: Upload coverage to codecoverage - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - - bandit: - name: Bandit - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install bandit - - name: Security checks with bandit - run: | - bandit spherical_geometry -r -x spherical_geometry/tests + tests: + uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 + with: + submodules: false + coverage: '' + envs: | + # Make sure that packaging will work + - name: pep517 build + linux: twine + + - name: Security audit + linux: bandit + + - name: PEP 8 + linux: codestyle + + - name: Python 3.9 (OSX) + macos: py39-test + posargs: -v + + - name: Python 3.10 (Windows) + windows: py310-test + posargs: -v + + - name: Python 3.11 with coverage + linux: py311-test-cov + posargs: -v + coverage: codecov + + - name: Python 3.12 with dev version of dependencies + linux: py312-test-devdeps + posargs: -v diff --git a/.gitignore b/.gitignore index 2c2a25b..f80e4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,12 @@ __pycache__ # Other generated files -*/version.py +*/_version.py */cython_version.py htmlcov MANIFEST .coverage +coverage.xml docs/api/ MANIFEST diff --git a/MANIFEST.in b/MANIFEST.in index 524eba4..d2f169c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,11 @@ include README.rst include CHANGES.rst - -include setup.cfg include pyproject.toml -recursive-include *.c +exclude benchmarks.py +exclude inside.png + +recursive-include src *.c *.h recursive-include docs * recursive-include licenses * diff --git a/README.rst b/README.rst index 568511c..1be62c8 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,17 @@ User documentation The ``spherical_geometry`` library is a Python package for handling spherical polygons that represent arbitrary regions of the sky. +Installation +------------ + +On PyPI:: + + pip install spherical-geometry + +On conda:: + + conda install -c conda-forge spherical-geometry + Requirements ------------ @@ -111,7 +122,7 @@ In the following image, the inside point (marked with the red dot) declares that the area of the polygon is the green region, and not the white region. -.. image:: docs/spherical_geometry/inside.png +.. image:: inside.png The inside point of the the polygon can be obtained from the ``~polygon.SphericalPolygon.inside`` property. diff --git a/spherical_geometry/tests/benchmarks.py b/benchmarks.py similarity index 71% rename from spherical_geometry/tests/benchmarks.py rename to benchmarks.py index 91ef8db..8a34515 100644 --- a/spherical_geometry/tests/benchmarks.py +++ b/benchmarks.py @@ -1,15 +1,13 @@ -import os import sys import time -import numpy as np -from sphere import * -from test_util import * -from test_shared import resolve_imagename +from spherical_geometry.polygon import SphericalPolygon +from spherical_geometry.tests.helpers import ROOT_DIR, get_point_set, resolve_imagename + def point_in_poly_lots(): - image_name = resolve_imagename(ROOT_DIR,'1904-66_TAN.fits') - + image_name = resolve_imagename(ROOT_DIR, '1904-66_TAN.fits') + poly1 = SphericalPolygon.from_wcs(image_name, 64, crval=[0, 87]) poly2 = SphericalPolygon.from_wcs(image_name, 64, crval=[20, 89]) poly3 = SphericalPolygon.from_wcs(image_name, 64, crval=[180, 89]) @@ -18,8 +16,8 @@ def point_in_poly_lots(): count = 0 for point in points: - if poly1.contains_point(point) or poly2.contains_point(point) or \ - poly3.contains_point(point): + if (poly1.contains_point(point) or poly2.contains_point(point) or + poly3.contains_point(point)): count += 1 assert count == 5 @@ -27,6 +25,7 @@ def point_in_poly_lots(): assert not poly1.intersects_poly(poly3) assert not poly2.intersects_poly(poly3) + if __name__ == '__main__': for benchmark in [point_in_poly_lots]: t = time.time() @@ -36,4 +35,3 @@ def point_in_poly_lots(): benchmark() sys.stdout.write(' %.03fs\n' % (time.time() - t)) sys.stdout.flush() - diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..f4be124 --- /dev/null +++ b/conftest.py @@ -0,0 +1,22 @@ +try: + from pytest_astropy_header.display import (PYTEST_HEADER_MODULES, + TESTED_VERSIONS) +except ImportError: + PYTEST_HEADER_MODULES = {} + TESTED_VERSIONS = {} + +try: + from spherical_geometry import __version__ as version +except ImportError: + version = 'unknown' + +# Uncomment and customize the following lines to add/remove entries +# from the list of packages for which version numbers are displayed +# when running the tests. +PYTEST_HEADER_MODULES['astropy'] = 'astropy' +PYTEST_HEADER_MODULES.pop('Scipy', None) +PYTEST_HEADER_MODULES.pop('Matplotlib', None) +PYTEST_HEADER_MODULES.pop('Pandas', None) +PYTEST_HEADER_MODULES.pop('h5py', None) + +TESTED_VERSIONS['spherical-geometry'] = version diff --git a/coveragerc b/coveragerc deleted file mode 100755 index 6473410..0000000 --- a/coveragerc +++ /dev/null @@ -1,21 +0,0 @@ -[run] -source = {packagename} - -[report] -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - - # Don't complain about packages we have installed - except ImportError - - # Don't complain if tests don't hit assertions - raise AssertionError - raise NotImplementedError - raise MalformedPolygonError - - # Don't complain about script hooks - def main\(.*\): - - # Ignore branches that don't pertain to this version of Python - pragma: py{ignore_python_version} diff --git a/docs/conf.py b/docs/conf.py index 76aaba5..01675ef 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,17 +12,18 @@ # import os import sys + +from spherical_geometry import __version__ + sys.path.insert(0, os.path.abspath('.')) sys.path.insert(1, os.path.abspath('..')) - # -- Project information ----------------------------------------------------- project = 'Spherical Geometry' copyright = '2023, STScI' author = 'STScI' -from spherical_geometry import __version__ release = __version__ # -- General configuration --------------------------------------------------- @@ -33,7 +34,6 @@ extensions = [ 'numpydoc', 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', diff --git a/inside.png b/inside.png new file mode 100644 index 0000000..a56246f Binary files /dev/null and b/inside.png differ diff --git a/pyproject.toml b/pyproject.toml index 5939726..9d51118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,123 @@ +[project] +name = "spherical_geometry" +dynamic = [ + "version" +] +description = "Python based tools for spherical geometry" +readme = "README.rst" +authors = [ + { name = "STScI", email = "help@stsci.edu" } +] +license = { text = "BSD-3-Clause" } +requires-python = ">=3.9" +classifiers = [ + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: C", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Scientific/Engineering :: Astronomy", + "Topic :: Scientific/Engineering :: Physics", +] +keywords = [ + "astronomy", + "astrophysics", + "space", + "science", + "spherical", + "geometry", +] +dependencies = [ + "numpy>=1.20", + "astropy>=5.0.4", +] + +[project.optional-dependencies] +test = [ + "pytest", + "pytest-astropy-header", +] +docs = [ + "sphinx-automodapi", + "numpydoc", +] + +[project.urls] +bug = "https://github.com/spacetelescope/spherical_geometry/issues/" +source = "https://github.com/spacetelescope/spherical_geometry/" +help = "https://hsthelp.stsci.edu" +documentation = "http://spherical-geometry.readthedocs.io/" + [build-system] -requires = ["setuptools>=38.2.5", "setuptools_scm", "numpy>=1.25,<2"] +requires = [ + "setuptools", + "setuptools_scm", + "numpy>=1.25,<2", +] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +include-package-data = false +license-files = ["licenses/*.rst"] +zip-safe = false + +[tool.setuptools.packages.find] +include = ["spherical_geometry*"] +namespaces = false + +[tool.setuptools.package-data] +"*" = [ + "README.rst", + "licenses/*", +] +"spherical_geometry.tests" = [ + "data/*", +] + +[tool.setuptools_scm] +write_to = "spherical_geometry/_version.py" + +[tool.pytest.ini_options] +minversion = 6.0 +addopts = "--color=yes --import-mode=append" +testpaths = [ + "spherical_geometry", +] +norecursedirs = [ + "build", + "docs[\\/]_build", +] +astropy_header = true +junit_family = "xunit2" +xfail_strict = true +filterwarnings = [ + "error", + "ignore:numpy\\.ndarray size changed:RuntimeWarning", + "ignore:numpy\\.ufunc size changed:RuntimeWarning", +] + +[tool.coverage] + + [tool.coverage.run] + omit = [ + "spherical_geometry/tests/*", + "*/spherical_geometry/tests/*", + ] + + [tool.coverage.report] + exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't complain about packages we have installed + "except ImportError", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # Don't complain about script hooks + "'def main(.*):'", + # Ignore branches that don't pertain to this version of Python + "pragma: py{ignore_python_version}", + # Don't complain about IPython completion helper + "def _ipython_key_completions_", + ] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 8d570af..0000000 --- a/setup.cfg +++ /dev/null @@ -1,30 +0,0 @@ -[tool:pytest] -minversion = 6 -junit_family = xunit2 -norecursedirs = .git build docs/_build -xfail_strict = true -filterwarnings = - error - ignore:numpy\.ndarray size changed:RuntimeWarning - ignore:numpy\.ufunc size changed:RuntimeWarning - -[metadata] -package_name = spherical_geometry -description = Python based tools for spherical geometry -long_description = Intersection, union, contains point and other typical ops on spherical polygons -author = STScI -author_email = help@stsci.edu -license = BSD -url = https://github.com/spacetelescope/spherical_geometry -edit_on_github = False -github_project = spacetelescope/spherical_geometry -project_urls = - Bug Reports = https://github.com/spacetelescope/spherical_geometry/issues/ - Source = https://github.com/spacetelescope/spherical_geometry/ - Help = https://hsthelp.stsci.edu - -[coverage:run] -omit = - spherical_geometry/tests/* - # And list again for running against installed versions - */spherical_geometry/tests/* diff --git a/setup.py b/setup.py index f98fb9c..6e3a038 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ from glob import glob from setuptools import setup from setuptools import Extension -from setuptools import find_packages use_system_qd = os.environ.get('USE_SYSTEM_QD', '') have_windows = bool(sys.platform.startswith('win')) @@ -67,18 +66,7 @@ def qd_config(arg): print('Missing requirement: numpy. Cannot continue.', file=sys.stderr) exit(1) -# Get some values from the setup.cfg -from configparser import ConfigParser -conf = ConfigParser() -conf.read(['setup.cfg']) -metadata = dict(conf.items('metadata')) - -PACKAGENAME = metadata.get('package_name', 'packagename') -DESCRIPTION = metadata.get('description', 'Astropy affiliated package') -AUTHOR = metadata.get('author', '') -AUTHOR_EMAIL = metadata.get('author_email', '') -LICENSE = metadata.get('license', 'unknown') -URL = metadata.get('url', 'https://github.com/spacetelescope') +PACKAGENAME = "spherical_geometry" # Include all .c files, recursively, including those generated by # Cython, since we can not do this in MANIFEST.in with a "dynamic" @@ -125,38 +113,6 @@ def qd_config(arg): setup( - name=PACKAGENAME, - use_scm_version=True, - setup_requires=["setuptools_scm"], - description=DESCRIPTION, - install_requires=[ - 'astropy>=5.0.4', - 'numpy>=1.20', - ], - python_requiers='>=3.9', - extras_require={ - 'test': [ - 'pytest', - 'pytest-cov', - ], - 'docs': [ - 'sphinx', - 'sphinx-automodapi', - 'numpydoc', - ], - }, - author=AUTHOR, - author_email=AUTHOR_EMAIL, - license=LICENSE, - url=URL, - zip_safe=False, - packages=find_packages(), - package_data={ - '': ['README.rst', 'licenses/*'], - PACKAGENAME: [ - os.path.join(PACKAGENAME, '*'), - ] - }, ext_modules=[ Extension('spherical_geometry.math_util', sources, **ext_info) ], diff --git a/spherical_geometry/__init__.py b/spherical_geometry/__init__.py index 5a8b56b..ce2e284 100755 --- a/spherical_geometry/__init__.py +++ b/spherical_geometry/__init__.py @@ -1,4 +1,4 @@ try: - from .version import version as __version__ + from ._version import version as __version__ except ImportError: __version__ = '' diff --git a/spherical_geometry/graph.py b/spherical_geometry/graph.py index d5111be..e18358c 100644 --- a/spherical_geometry/graph.py +++ b/spherical_geometry/graph.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- - # Licensed under a 3-clause BSD style license - see LICENSE.rst - """ This contains the code that does the actual unioning of regions. """ @@ -14,16 +12,17 @@ import numpy as np # LOCAL -from . import great_circle_arc as gca -from . import vector -from .polygon import (SingleSphericalPolygon, SphericalPolygon, - MalformedPolygonError) +from spherical_geometry import great_circle_arc as gca +from spherical_geometry import vector +from spherical_geometry.polygon import (SingleSphericalPolygon, SphericalPolygon, + MalformedPolygonError) __all__ = ['Graph'] # Set to True to enable some sanity checks DEBUG = True + # The following two functions are called by sorted to provide a consistent # ordering of nodes and edges retrieved from the graph, since values are # retrieved from sets in an order that varies from run to run @@ -31,9 +30,11 @@ def node_order(node): return hash(tuple(node._point)) + def edge_order(edge): return node_order(edge._nodes[0]) + node_order(edge._nodes[1]) + class Graph: """ A graph of nodes connected by edges. The graph is used to build @@ -84,7 +85,6 @@ def equals(self, other, thresh=1.e-9): """ return np.array_equal(self._point, other._point) - class Edge: """ An `~Graph.Edge` represents a connection between exactly two @@ -140,14 +140,13 @@ def equals(self, other): equals : bool """ if (self._nodes[0].equals(other._nodes[0]) and - self._nodes[1].equals(other._nodes[1])): + self._nodes[1].equals(other._nodes[1])): return True if (self._nodes[1].equals(other._nodes[0]) and - self._nodes[0].equals(other._nodes[1])): + self._nodes[0].equals(other._nodes[1])): return True return False - def __init__(self, polygons): """ Parameters @@ -445,7 +444,7 @@ def intersection(self): poly = self._trace() # If multiple polygons, the inside point can only be in one - if len(poly._polygons)==1 and not self._contains_inside_point(poly): + if len(poly._polygons) == 1 and not self._contains_inside_point(poly): poly = poly.invert_polygon() return poly @@ -577,8 +576,10 @@ def _find_arc_to_arc_intersections(self): changed = False while len(edges) > 1: AB = edges.pop(0) - A = starts[0]; starts = starts[1:] # numpy equiv of "pop(0)" - B = ends[0]; ends = ends[1:] # numpy equiv of "pop(0)" + A = starts[0] + starts = starts[1:] # numpy equiv of "pop(0)" + B = ends[0] + ends = ends[1:] # numpy equiv of "pop(0)" # Calculate the intersection points between AB and all # other remaining edges @@ -654,7 +655,7 @@ def _remove_interior_edges(self): edge._count = 0 A, B = edge._nodes for polygon in polygons: - if (not polygon in edge._source_polygons and + if (polygon not in edge._source_polygons and ((polygon in A._source_polygons or polygon.contains_point(A._point)) and (polygon in B._source_polygons or @@ -728,7 +729,7 @@ def _remove_3ary_edges(self): nedges_a = len(edge._nodes[0]._edges) nedges_b = len(edge._nodes[1]._edges) if (nedges_a % 2 == 1 and nedges_a >= 3 and - nedges_b % 2 == 1 and nedges_b >= 3): + nedges_b % 2 == 1 and nedges_b >= 3): removals.append(edge) changed = True diff --git a/spherical_geometry/great_circle_arc.py b/spherical_geometry/great_circle_arc.py index cc543b7..7cea4e0 100644 --- a/spherical_geometry/great_circle_arc.py +++ b/spherical_geometry/great_circle_arc.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- - # Licensed under a 3-clause BSD style license - see LICENSE.rst - """ The `spherical_geometry.great_circle_arc` module contains functions for computing the length, intersection, angle and midpoint of great circle arcs. @@ -11,79 +9,74 @@ section of those circles between two points on the unit sphere. """ -from .vector import two_d - # THIRD-PARTY import numpy as np +# LOCAL +from spherical_geometry.vector import two_d + # C versions of the code have been written to speed up operations # the python versions are a fallback if the C cannot be used try: - from . import math_util + from spherical_geometry import math_util HAS_C_UFUNCS = True except ImportError: HAS_C_UFUNCS = False +__all__ = ['angle', 'intersection', 'intersects', 'intersects_point', + 'length', 'midpoint', 'interpolate'] if HAS_C_UFUNCS: inner1d = math_util.inner1d else: from numpy.core._umath_tests import inner1d -__all__ = ['angle', 'intersection', 'intersects', 'intersects_point', - 'length', 'midpoint', 'interpolate'] - - -def _fast_cross(a, b): - """ - This is a reimplementation of `numpy.cross` that only does 3D x - 3D, and is therefore faster since it doesn't need any - conditionals. - """ - if HAS_C_UFUNCS: - return math_util.cross(a, b) - - cp = np.empty(np.broadcast(a, b).shape) - aT = a.T - bT = b.T - cpT = cp.T - - cpT[0] = aT[1]*bT[2] - aT[2]*bT[1] - cpT[1] = aT[2]*bT[0] - aT[0]*bT[2] - cpT[2] = aT[0]*bT[1] - aT[1]*bT[0] - - return cp - if HAS_C_UFUNCS: _fast_cross = math_util.cross - - -def _cross_and_normalize(A, B): - T = _fast_cross(A, B) - # Normalization - l = np.sqrt(np.sum(T ** 2, axis=-1)) - l = two_d(l) - # Might get some divide-by-zeros - with np.errstate(invalid='ignore'): - TN = T / l - # ... but set to zero, or we miss real NaNs elsewhere - TN = np.nan_to_num(TN) - return TN - +else: + def _fast_cross(a, b): + """ + This is a reimplementation of `numpy.cross` that only does 3D x + 3D, and is therefore faster since it doesn't need any + conditionals. + """ + if HAS_C_UFUNCS: + return math_util.cross(a, b) + + cp = np.empty(np.broadcast(a, b).shape) + aT = a.T + bT = b.T + cpT = cp.T + + cpT[0] = aT[1]*bT[2] - aT[2]*bT[1] + cpT[1] = aT[2]*bT[0] - aT[0]*bT[2] + cpT[2] = aT[0]*bT[1] - aT[1]*bT[0] + + return cp if HAS_C_UFUNCS: def _cross_and_normalize(A, B): with np.errstate(invalid='ignore'): return math_util.cross_and_norm(A, B) - - -def triple_product(A, B, C): - return inner1d(C, _fast_cross(A, B)) - +else: + def _cross_and_normalize(A, B): + T = _fast_cross(A, B) + # Normalization + l = np.sqrt(np.sum(T ** 2, axis=-1)) + l = two_d(l) + # Might get some divide-by-zeros + with np.errstate(invalid='ignore'): + TN = T / l + # ... but set to zero, or we miss real NaNs elsewhere + TN = np.nan_to_num(TN) + return TN if HAS_C_UFUNCS: triple_product = math_util.triple_product +else: + def triple_product(A, B, C): + return inner1d(C, _fast_cross(A, B)) def intersection(A, B, C, D): diff --git a/spherical_geometry/polygon.py b/spherical_geometry/polygon.py index 4dfd246..c8a32e7 100644 --- a/spherical_geometry/polygon.py +++ b/spherical_geometry/polygon.py @@ -1,21 +1,18 @@ # -*- coding: utf-8 -*- - # Licensed under a 3-clause BSD style license - see LICENSE.rst - """ The `spherical_geometry.polygon` module defines the `SphericalPolygon` class for managing polygons on the unit sphere. """ # STDLIB -from copy import copy, deepcopy +from copy import deepcopy # THIRD-PARTY import numpy as np # LOCAL -from . import great_circle_arc -from . import vector +from spherical_geometry import great_circle_arc, vector __all__ = ['SingleSphericalPolygon', 'SphericalPolygon', 'MalformedPolygonError'] @@ -151,7 +148,7 @@ def to_lonlat(self): if len(self.points) == 0: return np.array([]) return vector.vector_to_lonlat(self.points[:,0], self.points[:,1], - self.points[:,2], degrees=True) + self.points[:,2], degrees=True) # Alias for to_lonlat to_radec = to_lonlat @@ -275,7 +272,7 @@ def from_wcs(cls, fitspath, steps=1, crval=None): ------- polygon : `SingleSphericalPolygon` object """ - from astropy import wcs as pywcs, version as astropy_ver + from astropy import wcs as pywcs from astropy.io import fits if isinstance(fitspath, fits.Header): @@ -504,7 +501,7 @@ def intersects_poly(self, other): A = self._points[i] B = self._points[i+1] if np.any(great_circle_arc.intersects( - A, B, other._points[:-1], other._points[1:])): + A, B, other._points[:-1], other._points[1:])): return True return False @@ -723,9 +720,11 @@ def draw(self, m, **plot_args): x, y = m(lon, lat) m.scatter(x, y, 1, **plot_args) + # For backwards compatibility _SingleSphericalPolygon = SingleSphericalPolygon + class SphericalPolygon(SingleSphericalPolygon): r""" Polygons are represented by both a set of points (in Cartesian @@ -773,7 +772,6 @@ def __init__(self, init, inside=None): polygons.extend(g.disjoint_polygons()) self._polygons = polygons - def __copy__(self): return deepcopy(self) @@ -877,7 +875,7 @@ def to_lonlat(self): @classmethod def from_lonlat(cls, lon, lat, center=None, degrees=True): - ## TODO Move into SingleSphericalPolygon + # TODO Move into SingleSphericalPolygon r""" Create a new `SphericalPolygon` from a list of (*longitude*, *latitude*) points. diff --git a/spherical_geometry/tests/test_util.py b/spherical_geometry/tests/helpers.py similarity index 54% rename from spherical_geometry/tests/test_util.py rename to spherical_geometry/tests/helpers.py index 97f5a69..19b20b3 100644 --- a/spherical_geometry/tests/test_util.py +++ b/spherical_geometry/tests/helpers.py @@ -1,10 +1,15 @@ import os import numpy as np -from .. import vector + +from spherical_geometry import vector + +__all__ = ["ROOT_DIR", "get_point_set", "resolve_imagename"] + ROOT_DIR = os.path.join(os.path.dirname(__file__), 'data') + def get_point_set(density=25): points = [] for i in np.linspace(-85, 85, density, True): @@ -13,3 +18,15 @@ def get_point_set(density=25): points.append([j, i]) points = np.asarray(points) return np.dstack(vector.radec_to_vector(points[:,0], points[:,1]))[0] + + +def resolve_imagename(root, base_name): + """Resolve image name for tests.""" + + image_name = os.path.join(root, base_name) + + # Is it zipped? + if not os.path.exists(image_name): + image_name = image_name.replace('.fits', '.fits.gz') + + return image_name diff --git a/spherical_geometry/tests/test_basic.py b/spherical_geometry/tests/test_basic.py index 6eb99bf..592e42c 100644 --- a/spherical_geometry/tests/test_basic.py +++ b/spherical_geometry/tests/test_basic.py @@ -5,19 +5,18 @@ import numpy as np import pytest -from numpy.testing import assert_almost_equal +from numpy.testing import assert_almost_equal, assert_allclose -from .. import graph -from .. import great_circle_arc -from .. import math_util -from .. import polygon -from .. import vector +from spherical_geometry import graph, great_circle_arc, polygon, vector +from spherical_geometry.tests.helpers import ROOT_DIR, get_point_set, resolve_imagename -from .test_util import * -from .test_shared import resolve_imagename +try: + from spherical_geometry import math_util +except ImportError: + math_util = None graph.DEBUG = True -ROOT_DIR = os.path.join(os.path.dirname(__file__), 'data') + def test_normalize_vector(): x, y, z = np.ogrid[-100:100:11,-100:100:11,-100:100:11] @@ -26,6 +25,7 @@ def test_normalize_vector(): l = np.sqrt(np.sum(xyzn * xyzn, axis=-1)) assert_almost_equal(l, 1.0) + def test_normalize_unit_vector(): for i in range(3): xyz = [0.0, 0.0, 0.0] @@ -34,6 +34,7 @@ def test_normalize_unit_vector(): l = np.sqrt(np.sum(xyzn * xyzn, axis=-1)) assert_almost_equal(l, 1.0) + def test_lonlat_to_vector(): npx, npy, npz = vector.lonlat_to_vector(np.arange(-360, 360, 1), 90) assert_almost_equal(npx, 0.0) @@ -95,6 +96,7 @@ def test_vector_to_radec(): assert_almost_equal(lon, 315.0) assert_almost_equal(lat, 0.0) + def test_is_clockwise(): clockwise_poly = polygon.SphericalPolygon.from_cone(0.0, 90.0, 1.0) assert clockwise_poly.is_clockwise() @@ -117,8 +119,8 @@ def test_midpoint(): for j in range(0, 11, 5)] bvec = [(float(i+10), float(j+10)) - for i in range(0, 11, 5) - for j in range(0, 11, 5)] + for i in range(0, 11, 5) + for j in range(0, 11, 5)] for a in avec: A = np.asarray(vector.lonlat_to_vector(a[0], a[1])) @@ -155,6 +157,8 @@ def test_interpolate(): assert abs(length - first_length) < 1.0e-10 +@pytest.mark.xfail( + math_util is None, reason="math_util C-ext is missing, numpy gives different results") def test_overlap(): def build_polygon(offset): points = [] @@ -189,6 +193,7 @@ def test_from_wcs(): assert np.all(np.absolute(lon - 6.027148333333) < 0.2) assert np.all(np.absolute(lat + 72.08351111111) < 0.2) + def test_intersects_poly_simple(): lon1 = [-10, 10, 10, -10, -10] lat1 = [30, 30, 0, 0, 30] @@ -295,6 +300,7 @@ def test_point_in_poly(): lon, lat = vector.vector_to_lonlat(point[0], point[1], point[2]) assert not poly.contains_lonlat(lon, lat) + def test_point_in_poly_lots(): from astropy.io import fits header = fits.getheader(resolve_imagename(ROOT_DIR, '1904-77_TAN.fits'), @@ -311,7 +317,7 @@ def test_point_in_poly_lots(): count = 0 for point in points: if (poly1.contains_point(point) or poly2.contains_point(point) or - poly3.contains_point(point)): + poly3.contains_point(point)): count += 1 assert count == 5 @@ -399,7 +405,7 @@ def test_cone(): for i in range(50): lon = random.randrange(-180, 180) lat = random.randrange(20, 90) - cone = polygon.SphericalPolygon.from_cone(lon, lat, 8, steps=64) + _ = polygon.SphericalPolygon.from_cone(lon, lat, 8, steps=64) def test_area(): @@ -417,14 +423,17 @@ def test_area(): calc_area = poly.area() assert_almost_equal(calc_area, area) + def test_cone_area(): saved_area = None - for lon in (0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330): + for lon in (0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330): for lat in (0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330): area = polygon.SphericalPolygon.from_cone(lon, lat, 30, steps=64).area() - if saved_area is None: saved_area = area + if saved_area is None: + saved_area = area assert_almost_equal(area, saved_area) + def test_fast_area(): a = np.array( # Clockwise [[ 0.35327617, 0.6351561 , -0.6868571 ], @@ -502,16 +511,20 @@ def test_convex_hull(): assert b == r, "Polygon boundary has correct points" +@pytest.mark.skipif(math_util is None, reason="math_util C-ext is missing") def test_math_util_angle_domain(): # Before a fix, this would segfault with pytest.raises(ValueError): math_util.angle([[0, 0, 0]], [[0, 0, 0]], [[0, 0, 0]]) +@pytest.mark.skipif(math_util is None, reason="math_util C-ext is missing") def test_math_util_length_domain(): with pytest.raises(ValueError): math_util.length([[np.nan, 0, 0]], [[0, 0, np.inf]]) + +@pytest.mark.skipif(math_util is None, reason="math_util C-ext is missing") def test_math_util_angle_nearly_coplanar_vec(): # test from issue #222 + extra values vectors = [ @@ -521,7 +534,5 @@ def test_math_util_angle_nearly_coplanar_vec(): ] angles = math_util.angle(*vectors) - assert np.allclose(angles[:-1], np.pi, rtol=0, atol=1e-16) - assert np.allclose(angles[-1], 0, rtol=0, atol=1e-32) - - + assert_allclose(angles[:-1], np.pi, rtol=0, atol=1e-16) + assert_allclose(angles[-1], 0, rtol=0, atol=1e-32) diff --git a/spherical_geometry/tests/test_intersection.py b/spherical_geometry/tests/test_intersection.py index 40cd8c4..a0cde9f 100644 --- a/spherical_geometry/tests/test_intersection.py +++ b/spherical_geometry/tests/test_intersection.py @@ -5,19 +5,17 @@ import math import os import random -import sys # THIRD-PARTY import numpy as np import pytest -from numpy.testing import assert_array_almost_equal +from numpy.testing import assert_array_almost_equal, assert_allclose # LOCAL -from .. import polygon -from .test_shared import resolve_imagename +from spherical_geometry import polygon +from spherical_geometry.tests.helpers import ROOT_DIR, resolve_imagename GRAPH_MODE = False -ROOT_DIR = os.path.join(os.path.dirname(__file__), 'data') class intersection_test: @@ -139,7 +137,7 @@ def test4(): Apoly = chipA1.union(chipA2) Bpoly = chipB1.union(chipB2) - X = Apoly.intersection(Bpoly) + _ = Apoly.intersection(Bpoly) @intersection_test(0, 90) @@ -173,8 +171,8 @@ def test_difficult_intersections(): # problematic in previous revisions of spherical_geometry # def test_intersection(polys): - # A, B = polys - # A.intersection(B) + # A, B = polys + # A.intersection(B) fname = resolve_imagename(ROOT_DIR, "difficult_intersections.txt") with open(fname, 'rb') as fd: @@ -192,22 +190,24 @@ def to_array(line): # yield test_intersection, (polyA, polyB) polyA.intersection(polyB) + def test_self_intersection(): # Tests intersection between a disjoint polygon and itself ra1 = [150.15056635, 150.18472797, 150.18472641, 150.15056557, 150.15056635] dec1 = [2.33675579, 2.33675454, 2.30262137, 2.3026226 , 2.33675579] ra2 = [150.18472955, 150.18472798, 150.15056635, 150.15056714, 150.18472955] dec2 = [2.37105428, 2.33692121, 2.33692245, 2.37105554, 2.37105428] - # create a union polygon + # create a union polygon s1 = polygon.SphericalPolygon.from_radec(np.array(ra1), np.array(dec1)) s2 = polygon.SphericalPolygon.from_radec(np.array(ra2), np.array(dec2)) s12 = s2.union(s1) # asserts self-intersection is same as original s12int = s12.intersection(s12) - assert(abs(s12.area() - s12int.area()) < 1.0e-6) + assert (abs(s12.area() - s12int.area()) < 1.0e-6) # same, with multi_intersection method s12int = polygon.SphericalPolygon.multi_intersection([s12, s12, s12]) - assert(abs(s12.area() - s12int.area()) < 1.0e-6) + assert (abs(s12.area() - s12int.area()) < 1.0e-6) + def test_ordering(): nrepeat = 10 @@ -288,18 +288,6 @@ def roll_polygon(P, i): assert_array_almost_equal(Careas[:-1], Careas[1:]) -if __name__ == '__main__': - if '--profile' not in sys.argv: - GRAPH_MODE = True - from mpl_toolkits.basemap import Basemap - from matplotlib import pyplot as plt - - functions = [(k, v) for k, v in globals().items() if k.startswith('test')] - functions.sort() - for k, v in functions: - v() - - def test_intersection_crash(): # Reported by Darren White @@ -353,7 +341,8 @@ def test_intersection_crash(): testFoV = polygon.SphericalPolygon(testpoints, inside=testcenter) poly = polygon.SphericalPolygon(polypoints, inside=polycenter) - overlap = poly.overlap(testFoV) + _ = poly.overlap(testFoV) + @pytest.mark.skip(reason="currently there is no solution to get this to pass") def test_intersection_crash_similar_poly(): @@ -379,4 +368,4 @@ def test_intersection_crash_similar_poly(): pts1 = np.sort(list(p1.points)[0][:-1], axis=0) pts3 = np.sort(list(p3.points)[0][:-1], axis=0) - assert np.allclose(pts1, pts3, rtol=0, atol=1e-15) + assert_allclose(pts1, pts3, rtol=0, atol=1e-15) diff --git a/spherical_geometry/tests/test_shared.py b/spherical_geometry/tests/test_shared.py deleted file mode 100644 index 0853d3a..0000000 --- a/spherical_geometry/tests/test_shared.py +++ /dev/null @@ -1,12 +0,0 @@ -import os - -def resolve_imagename(ROOT_DIR, base_name): - """Resolve image name for tests.""" - - image_name = os.path.join(ROOT_DIR, base_name) - - # Is it zipped? - if not os.path.exists(image_name): - image_name = image_name.replace('.fits', '.fits.gz') - - return image_name diff --git a/spherical_geometry/tests/test_union.py b/spherical_geometry/tests/test_union.py index 2868f32..87ee8f6 100644 --- a/spherical_geometry/tests/test_union.py +++ b/spherical_geometry/tests/test_union.py @@ -13,11 +13,15 @@ from numpy.testing import assert_array_almost_equal # LOCAL -from .. import polygon -from .test_shared import resolve_imagename +from spherical_geometry import polygon +from spherical_geometry.tests.helpers import ROOT_DIR, resolve_imagename + +try: + from spherical_geometry import math_util +except ImportError: + math_util = None GRAPH_MODE = False -ROOT_DIR = os.path.join(os.path.dirname(__file__), 'data') class union_test: @@ -123,7 +127,7 @@ def test5(): wcs = pywcs.WCS(A[4].header, fobj=A) chipA2 = polygon.SphericalPolygon.from_wcs(wcs) - null_union = chipA1.union(chipA2) + _ = chipA1.union(chipA2) def test6(): @@ -136,7 +140,7 @@ def test6(): wcs = pywcs.WCS(A[4].header, fobj=A) chipA2 = polygon.SphericalPolygon.from_wcs(wcs) - null_union = chipA1.union(chipA2) + _ = chipA1.union(chipA2) @pytest.mark.filterwarnings("ignore:CPERROR.*") @@ -274,59 +278,61 @@ def test_inside_point(): def test_edge_crossings(): a = np.array([[ 0.3061732 , 0.03027578, -0.95149427], - [ 0.30617825, 0.0302586 , -0.95149319], - [ 0.30617856, 0.03025797, -0.95149311], - [ 0.30617895, 0.0302569 , -0.95149302], - [ 0.3061794 , 0.03025554, -0.95149292], - [ 0.3061816 , 0.03025209, -0.95149232], - [ 0.30618188, 0.03025897, -0.95149201], - [ 0.3061817 , 0.03026287, -0.95149195], - [ 0.30618156, 0.03026394, -0.95149196], - [ 0.30618148, 0.03026437, -0.95149197], - [ 0.306181 , 0.03026455, -0.95149212], - [ 0.30617812, 0.03026594, -0.951493 ], - [ 0.30617145, 0.03027611, -0.95149482], - [ 0.30617166, 0.03027462, -0.95149481], - [ 0.30617174, 0.03027415, -0.95149479], - [ 0.30617285, 0.03027296, -0.95149447], - [ 0.30617303, 0.03028405, -0.95149406], - [ 0.30617294, 0.03028654, -0.95149401], - [ 0.30617292, 0.0302868 , -0.95149401], - [ 0.3061729 , 0.03028703, -0.95149401], - [ 0.3061732 , 0.03027578, -0.95149427]]) + [ 0.30617825, 0.0302586 , -0.95149319], + [ 0.30617856, 0.03025797, -0.95149311], + [ 0.30617895, 0.0302569 , -0.95149302], + [ 0.3061794 , 0.03025554, -0.95149292], + [ 0.3061816 , 0.03025209, -0.95149232], + [ 0.30618188, 0.03025897, -0.95149201], + [ 0.3061817 , 0.03026287, -0.95149195], + [ 0.30618156, 0.03026394, -0.95149196], + [ 0.30618148, 0.03026437, -0.95149197], + [ 0.306181 , 0.03026455, -0.95149212], + [ 0.30617812, 0.03026594, -0.951493 ], + [ 0.30617145, 0.03027611, -0.95149482], + [ 0.30617166, 0.03027462, -0.95149481], + [ 0.30617174, 0.03027415, -0.95149479], + [ 0.30617285, 0.03027296, -0.95149447], + [ 0.30617303, 0.03028405, -0.95149406], + [ 0.30617294, 0.03028654, -0.95149401], + [ 0.30617292, 0.0302868 , -0.95149401], + [ 0.3061729 , 0.03028703, -0.95149401], + [ 0.3061732 , 0.03027578, -0.95149427]]) b = np.array([[ 0.30661663, 0.03045585, -0.95134572], - [ 0.30661734, 0.03045311, -0.95134558], - [ 0.30661765, 0.03045208, -0.95134551], - [ 0.30661813, 0.03045066, -0.9513454 ], - [ 0.30661867, 0.03044924, -0.95134528], - [ 0.30661876, 0.03044903, -0.95134525], - [ 0.30661894, 0.03044863, -0.95134521], - [ 0.30661915, 0.03044818, -0.95134515], - [ 0.30662043, 0.03044566, -0.95134482], - [ 0.30662157, 0.03044381, -0.95134452], - [ 0.30662343, 0.03044892, -0.95134375], - [ 0.30662302, 0.03045219, -0.95134378], - [ 0.30662287, 0.03045322, -0.9513438 ], - [ 0.30662274, 0.03045366, -0.95134382], - [ 0.30662044, 0.03045488, -0.95134453], - [ 0.306613 , 0.0304643 , -0.95134662], - [ 0.30661294, 0.03046455, -0.95134663], - [ 0.30661302, 0.03046432, -0.95134661], - [ 0.30661312, 0.03046407, -0.95134659], - [ 0.30661321, 0.03046381, -0.95134657], - [ 0.30661339, 0.03046335, -0.95134653], - [ 0.30661558, 0.03046035, -0.95134592], - [ 0.3066161 , 0.03046476, -0.95134561], - [ 0.30661566, 0.03047163, -0.95134553], - [ 0.30661545, 0.03047377, -0.95134553], - [ 0.3066154 , 0.03047391, -0.95134554], - [ 0.30661663, 0.03045585, -0.95134572]]) + [ 0.30661734, 0.03045311, -0.95134558], + [ 0.30661765, 0.03045208, -0.95134551], + [ 0.30661813, 0.03045066, -0.9513454 ], + [ 0.30661867, 0.03044924, -0.95134528], + [ 0.30661876, 0.03044903, -0.95134525], + [ 0.30661894, 0.03044863, -0.95134521], + [ 0.30661915, 0.03044818, -0.95134515], + [ 0.30662043, 0.03044566, -0.95134482], + [ 0.30662157, 0.03044381, -0.95134452], + [ 0.30662343, 0.03044892, -0.95134375], + [ 0.30662302, 0.03045219, -0.95134378], + [ 0.30662287, 0.03045322, -0.9513438 ], + [ 0.30662274, 0.03045366, -0.95134382], + [ 0.30662044, 0.03045488, -0.95134453], + [ 0.306613 , 0.0304643 , -0.95134662], + [ 0.30661294, 0.03046455, -0.95134663], + [ 0.30661302, 0.03046432, -0.95134661], + [ 0.30661312, 0.03046407, -0.95134659], + [ 0.30661321, 0.03046381, -0.95134657], + [ 0.30661339, 0.03046335, -0.95134653], + [ 0.30661558, 0.03046035, -0.95134592], + [ 0.3066161 , 0.03046476, -0.95134561], + [ 0.30661566, 0.03047163, -0.95134553], + [ 0.30661545, 0.03047377, -0.95134553], + [ 0.3066154 , 0.03047391, -0.95134554], + [ 0.30661663, 0.03045585, -0.95134572]]) A = polygon.SphericalPolygon(a) B = polygon.SphericalPolygon(b) - C = A.union(B) + _ = A.union(B) +@pytest.mark.xfail( + math_util is None, reason="math_util C-ext is missing, numpy gives different results") def test_almost_identical_polygons_multi_union(): filename = resolve_imagename(ROOT_DIR,'almost_same_polygons.npz') polygon_data = np.load(filename) @@ -340,18 +346,15 @@ def test_almost_identical_polygons_multi_union(): ) ) - p = polygon.SphericalPolygon.multi_union(polygons) - assert np.shape(list(p.points)[0]) == (66, 3) - assert abs(p.area() - 2.6672666e-8) < 5.0e-14 - + # FIXME: https://github.com/spacetelescope/spherical_geometry/issues/245 + area_tol = 5.8e-14 # Used to be 5.0e-14 before we started testing different archs -if __name__ == '__main__': - if '--profile' not in sys.argv: - GRAPH_MODE = True - from mpl_toolkits.basemap import Basemap - from matplotlib import pyplot as plt + # FIXME: https://github.com/spacetelescope/spherical_geometry/issues/244 + if sys.platform == "win32": + p_shapes = [(66, 3), (68, 3)] # 32-bit Windows has different shape but not 64-bit Windows + else: + p_shapes = [(66, 3)] - functions = [(k, v) for k, v in globals().items() if k.startswith('test')] - functions.sort() - for k, v in functions: - v() + p = polygon.SphericalPolygon.multi_union(polygons) + assert np.shape(list(p.points)[0]) in p_shapes + assert abs(p.area() - 2.6672666e-8) < area_tol diff --git a/spherical_geometry/vector.py b/spherical_geometry/vector.py index 1b7a07d..515c75e 100644 --- a/spherical_geometry/vector.py +++ b/spherical_geometry/vector.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- - # Licensed under a 3-clause BSD style license - see LICENSE.rst - """ The `spherical_geometry.vector` module contains the basic operations for handling vectors and converting them to and from other representations. @@ -16,11 +14,11 @@ except ImportError: HAS_C_UFUNCS = False - __all__ = ['two_d', 'lonlat_to_vector', 'vector_to_lonlat', 'normalize_vector', 'radec_to_vector', 'vector_to_radec', 'rotate_around'] + def two_d(vec): """ Reshape a one dimensional vector so it has a second dimension @@ -30,6 +28,7 @@ def two_d(vec): shape = tuple(shape) return np.reshape(vec, shape) + def lonlat_to_vector(lon, lat, degrees=True): r""" Converts a location on the unit sphere from longitude and @@ -76,6 +75,7 @@ def lonlat_to_vector(lon, lat, degrees=True): np.sin(lon_rad) * cos_lat, np.sin(lat_rad)) + # Alias for lonlat_to_vector radec_to_vector = lonlat_to_vector @@ -121,6 +121,7 @@ def vector_to_lonlat(x, y, z, degrees=True): else: return result + # Alias for vector_to_lonlat vector_to_radec = vector_to_lonlat diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..42cae19 --- /dev/null +++ b/tox.ini @@ -0,0 +1,74 @@ +[tox] +envlist = + py{39,310,311,312}-test{,-devdeps}{,-cov} + codestyle + twine + bandit + +[testenv] +# Pass through the following environment variables which are needed for the CI +passenv = HOME,WINDIR,CC,CI + +setenv = + devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple + +# Run the tests in a temporary directory to make sure that we don't import +# package from the source tree +changedir = .tmp/{envname} + +# tox environments are constructued with so-called 'factors' (or terms) +# separated by hyphens, e.g. test-devdeps. Lines below starting with factor: +# will only take effect if that factor is included in the environment name. To +# see a list of example environments that can be run, along with a description, +# run: +# +# tox -l -v +# +description = + run tests + devdeps: with the latest developer version of key dependencies + cov: and test coverage + +deps = + # The devdeps factor is intended to be used to install the latest developer version + # or nightly wheel of key dependencies. + devdeps: numpy>=0.0.dev0 + devdeps: astropy>=0.0.dev0 + + cov: pytest-cov + +extras = + test + +commands = + pip freeze + !cov: pytest --pyargs spherical_geometry {posargs} + cov: pytest --pyargs spherical_geometry --cov spherical_geometry --cov-config={toxinidir}/pyproject.toml --cov-report xml:{toxinidir}/coverage.xml {posargs} + +[testenv:codestyle] +skip_install = true +changedir = {toxinidir} +description = check code style with flake8 +deps = flake8 +commands = flake8 spherical_geometry --count + +[testenv:twine] +skip_install = true +changedir = {toxinidir} +description = twine check dist tarball +deps = + build + twine>=3.3 +commands = + pip freeze + python -m build --sdist . + twine check --strict dist/* + +[testenv:bandit] +skip_install = true +changedir = {toxinidir} +description = Security audit with bandit +deps = bandit +commands = + pip freeze + bandit spherical_geometry -r -x spherical_geometry/tests