diff -Nru katpoint-0.7/debian/changelog katpoint-0.9/debian/changelog --- katpoint-0.7/debian/changelog 2018-05-22 15:38:53.000000000 +0000 +++ katpoint-0.9/debian/changelog 2020-06-02 22:13:21.000000000 +0000 @@ -1,3 +1,10 @@ +katpoint (0.9-1kern1) bionic; urgency=medium + + [ Athanaseus Javas Ramaila] + * New upstream release 0.9 + + -- KERN packaging Wed, 03 Jun 2020 00:13:21 +0200 + katpoint (0.7-1kern2) bionic; urgency=medium * build for KERN-4 (bionic) diff -Nru katpoint-0.7/debian/control katpoint-0.9/debian/control --- katpoint-0.7/debian/control 2018-05-22 15:38:15.000000000 +0000 +++ katpoint-0.9/debian/control 2020-06-02 22:13:21.000000000 +0000 @@ -2,13 +2,22 @@ Section: python Priority: optional Maintainer: Gijs Molenaar -Build-Depends: debhelper (>= 9), dh-python, python-all, python-setuptools, - python-katversion, python-ephem -Standards-Version: 3.9.4 +Build-Depends: debhelper (>= 9), + dh-python, + python-all, + python3-all, + python-setuptools, + python3-setuptools, + katversion, + python-katversion, + python-ephem, + python3-ephem +Standards-Version: 4.4.1 Homepage: https://github.com/ska-sa/katpoint Vcs-Git: git://github.com/kernsuite-debian/katpoint.git Vcs-Browser: https://github.com/kernsuite-debian/katpoint X-Python-Version: >= 2.7 +X-Python3-Version: >= 3.3 Package: python-katpoint Architecture: any @@ -17,5 +26,15 @@ Description: Karoo Array Telescope pointing coordinate library Coordinate library for the MeerKAT project, providing astronomical coordinate transformations, antenna pointing models, correlator delay models, source + flux models and basic source catalogues, using an existing coordinate + library such as PyEphem to do the low-level calculations. + +Package: python3-katpoint +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, + python3-katversion, python3-ephem +Description: Karoo Array Telescope pointing coordinate library + Coordinate library for the MeerKAT project, providing astronomical coordinate + transformations, antenna pointing models, correlator delay models, source flux models and basic source catalogues, using an existing coordinate library such as PyEphem to do the low-level calculations. diff -Nru katpoint-0.7/debian/rules katpoint-0.9/debian/rules --- katpoint-0.7/debian/rules 2018-05-22 15:38:15.000000000 +0000 +++ katpoint-0.9/debian/rules 2020-06-02 22:13:21.000000000 +0000 @@ -5,7 +5,7 @@ export PYBUILD_NAME=katpoint %: - dh $@ --with python2 --buildsystem=pybuild + dh $@ --with python2,python3 --buildsystem=pybuild --parallel override_dh_auto_clean: dh_clean diff -Nru katpoint-0.7/doc/conf.py katpoint-0.9/doc/conf.py --- katpoint-0.7/doc/conf.py 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/doc/conf.py 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,68 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import katpoint + + +# -- Project information ----------------------------------------------------- + +project = 'katdal' +copyright = '2019, South African Radio Astronomy Observatory' +author = 'South African Radio Astronomy Observatory' + +version = '.'.join(katpoint.__version__.split('.')[:2]) +release = katpoint.__version__ + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.mathjax', + 'sphinx.ext.intersphinx', + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Extension configuration ------------------------------------------------- + +audodoc_member_order = 'bysource' + +# -- Options for intersphinx extension --------------------------------------- + +intersphinx_mapping = {'katsdptelstate': ('https://katsdptelstate.readthedocs.io/en/latest', None)} diff -Nru katpoint-0.7/doc/.gitignore katpoint-0.9/doc/.gitignore --- katpoint-0.7/doc/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/doc/.gitignore 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1 @@ +_build diff -Nru katpoint-0.7/doc/index.rst katpoint-0.9/doc/index.rst --- katpoint-0.7/doc/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/doc/index.rst 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,21 @@ +.. katdal documentation master file, created by + sphinx-quickstart on Mon Jun 10 15:18:10 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to katdal's documentation! +================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff -Nru katpoint-0.7/doc/katpoint.rst katpoint-0.9/doc/katpoint.rst --- katpoint-0.7/doc/katpoint.rst 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/doc/katpoint.rst 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,110 @@ +katpoint package +================ + +Submodules +---------- + +katpoint.antenna module +----------------------- + +.. automodule:: katpoint.antenna + :members: + :undoc-members: + :show-inheritance: + +katpoint.catalogue module +------------------------- + +.. automodule:: katpoint.catalogue + :members: + :undoc-members: + :show-inheritance: + +katpoint.conversion module +-------------------------- + +.. automodule:: katpoint.conversion + :members: + :undoc-members: + :show-inheritance: + +katpoint.delay module +--------------------- + +.. automodule:: katpoint.delay + :members: + :undoc-members: + :show-inheritance: + +katpoint.ephem\_extra module +---------------------------- + +.. automodule:: katpoint.ephem_extra + :members: + :undoc-members: + :show-inheritance: + +katpoint.flux module +-------------------- + +.. automodule:: katpoint.flux + :members: + :undoc-members: + :show-inheritance: + +katpoint.model module +--------------------- + +.. automodule:: katpoint.model + :members: + :undoc-members: + :show-inheritance: + +katpoint.pointing module +------------------------ + +.. automodule:: katpoint.pointing + :members: + :undoc-members: + :show-inheritance: + +katpoint.projection module +-------------------------- + +.. automodule:: katpoint.projection + :members: + :undoc-members: + :show-inheritance: + +katpoint.refraction module +-------------------------- + +.. automodule:: katpoint.refraction + :members: + :undoc-members: + :show-inheritance: + +katpoint.target module +---------------------- + +.. automodule:: katpoint.target + :members: + :undoc-members: + :show-inheritance: + +katpoint.timestamp module +------------------------- + +.. automodule:: katpoint.timestamp + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: katpoint + :members: + :undoc-members: + :show-inheritance: diff -Nru katpoint-0.7/doc/Makefile katpoint-0.9/doc/Makefile --- katpoint-0.7/doc/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/doc/Makefile 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,23 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +apidoc: + sphinx-apidoc -f -o . ../katpoint ../katpoint/test/*.py + +.PHONY: help apidoc Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff -Nru katpoint-0.7/doc/modules.rst katpoint-0.9/doc/modules.rst --- katpoint-0.7/doc/modules.rst 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/doc/modules.rst 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,7 @@ +katpoint +======== + +.. toctree:: + :maxdepth: 4 + + katpoint diff -Nru katpoint-0.7/doc-requirements.in katpoint-0.9/doc-requirements.in --- katpoint-0.7/doc-requirements.in 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/doc-requirements.in 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,2 @@ +Sphinx +sphinx_rtd_theme diff -Nru katpoint-0.7/doc-requirements.txt katpoint-0.9/doc-requirements.txt --- katpoint-0.7/doc-requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/doc-requirements.txt 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,31 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile doc-requirements.in +# +alabaster==0.7.12 # via sphinx +babel==2.7.0 # via sphinx +certifi==2019.3.9 # via requests +chardet==3.0.4 # via requests +docutils==0.14 # via sphinx +idna==2.8 # via requests +imagesize==1.1.0 # via sphinx +jinja2==2.10.1 # via sphinx +markupsafe==1.1.1 # via jinja2 +packaging==19.0 # via sphinx +pygments==2.4.2 # via sphinx +pyparsing==2.4.0 # via packaging +pytz==2019.1 # via babel +requests==2.22.0 # via sphinx +six==1.12.0 # via packaging +snowballstemmer==1.2.1 # via sphinx +sphinx-rtd-theme==0.4.3 +sphinx==2.1.0 +sphinxcontrib-applehelp==1.0.1 # via sphinx +sphinxcontrib-devhelp==1.0.1 # via sphinx +sphinxcontrib-htmlhelp==1.0.2 # via sphinx +sphinxcontrib-jsmath==1.0.1 # via sphinx +sphinxcontrib-qthelp==1.0.2 # via sphinx +sphinxcontrib-serializinghtml==1.1.3 # via sphinx +urllib3==1.25.3 # via requests diff -Nru katpoint-0.7/Jenkinsfile katpoint-0.9/Jenkinsfile --- katpoint-0.7/Jenkinsfile 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/Jenkinsfile 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,6 @@ #!groovy @Library('katsdpjenkins') _ +katsdp.killOldJobs() katsdp.standardBuild(python3: true) katsdp.mail('ludwig@ska.ac.za') diff -Nru katpoint-0.7/katpoint/antenna.py katpoint-0.9/katpoint/antenna.py --- katpoint-0.7/katpoint/antenna.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/antenna.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -21,6 +21,8 @@ and other parameters that affect pointing and delay calculations. """ +from __future__ import print_function, division, absolute_import +from builtins import object import numpy as np import ephem @@ -313,9 +315,22 @@ def _scalar_local_sidereal_time(t): """Calculate local sidereal time at a single time instant.""" self.observer.date = Timestamp(t).to_ephem_date() - # pylint: disable-msg=E1101 return self.observer.sidereal_time() if is_iterable(timestamp): return np.array([_scalar_local_sidereal_time(t) for t in timestamp]) else: return _scalar_local_sidereal_time(timestamp) + + def array_reference_antenna(self, name='array'): + """Synthetic antenna at the delay model reference position of this antenna. + + This is mainly useful as the reference `antenna` for + :meth:`.Target.uvw`, in which case it will give both faster and more + accurate results than other choices. + + The returned antenna will have no delay or pointing model. It is + intended to be used only for its position and does not correspond to a + physical antenna. + """ + pos = self.ref_position_wgs84 if self.delay_model else self.position_wgs84 + return Antenna(name, pos[0], pos[1], pos[2], self.diameter, beamwidth=self.beamwidth) diff -Nru katpoint-0.7/katpoint/catalogue.py katpoint-0.9/katpoint/catalogue.py --- katpoint-0.7/katpoint/catalogue.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/catalogue.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,31 +15,16 @@ ################################################################################ """Target catalogue.""" +from __future__ import print_function, division, absolute_import +from builtins import object +from past.builtins import basestring import logging -import re +from collections import defaultdict import ephem.stars import numpy as np -from past.builtins import basestring - -# This is needed for tab completion, but is ignored if no IPython is installed -try: - # IPython 0.11 and above - from IPython.core.error import TryNext -except ImportError: - try: - # IPython 0.10 and below - from IPython.ipapi import TryNext - except ImportError: - pass -# The same goes for readline -try: - import readline -except ImportError: - readline = None - from .target import Target from .timestamp import Timestamp from .ephem_extra import rad2deg @@ -49,7 +34,7 @@ specials = ['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune'] -def _hash(name): +def _normalised(name): """Normalise string to make name lookup more robust.""" return name.strip().lower().replace(' ', '').replace('_', '') @@ -69,19 +54,22 @@ - A list of targets, which can be filtered, sorted, pretty-printed and iterated over. The list is accessible as :meth:`Catalogue.targets`, and the catalogue itself is iterable, returning the next target on each - iteration. An example is:: + iteration. The targets are assumed to be unique, but may have the same + name. An example is:: cat = katpoint.Catalogue() + cat.add(some_targets) t = cat.targets[0] for t in cat: # Do something with target t - - Lookup by name, by using the catalogue as if it were a dictionary. This is - simpler for the user, who does not have to remember all the target details. - The named lookup supports tab completion in IPython, which further - simplifies finding a target in the catalogue. An example is:: + - Lookup by name, by using the catalogue as if it were a dictionary. This + is simpler for the user, who does not have to remember all the target + details. The named lookup supports tab completion in IPython, which + further simplifies finding a target in the catalogue. The most recently + added target with the specified name is returned. An example is:: - cat = katpoint.Catalogue() + cat = katpoint.Catalogue(add_specials=True) t = cat['Sun'] Construction @@ -91,8 +79,8 @@ cat = katpoint.Catalogue() - which produces an empty catalogue. The standard *special* targets, which are - the Sun, Moon, planets and Zenith, can be added as follows:: + which produces an empty catalogue. The standard *special* targets, which + are the Sun, Moon, planets and Zenith, can be added as follows:: cat = katpoint.Catalogue(add_specials=True) @@ -111,17 +99,17 @@ cat2 = katpoint.Catalogue([t1, t2]) Alternatively, the list of targets may be replaced by a list of target - description strings (or a single description string). The target objects are - then constructed before being added, as in:: + description strings (or a single description string). The target objects + are then constructed before being added, as in:: cat1 = katpoint.Catalogue('Takreem, azel, 20, 30') cat2 = katpoint.Catalogue(['Ganymede, special', 'Takreem, azel, 20, 30']) - Taking this one step further, the list may be replaced by any iterable object - that returns strings. A very useful example of such an object is the Python - :class:`file` object, which iterates over the lines of a text file. If the - catalogue file contains one target description string per line (with comments - and blank lines allowed too), it may be loaded as:: + Taking this one step further, the list may be replaced by any iterable + object that returns strings. A very useful example of such an object is the + Python :class:`file` object, which iterates over the lines of a text file. + If the catalogue file contains one target description string per line + (with comments and blank lines allowed too), it may be loaded as:: cat = katpoint.Catalogue(file('catalogue.csv')) @@ -132,7 +120,7 @@ t1 = katpoint.Target('Ganymede, special') t2 = katpoint.Target('Takreem, azel, 20, 30') - cat = katpoint.Catalogue(add_specials=False) + cat = katpoint.Catalogue() cat.add(t1) cat.add([t1, t2]) cat.add('Ganymede, special') @@ -152,7 +140,7 @@ cumbersome, especially in the case of TLE files which are regularly updated. Two special methods simplify the loading of targets from these files:: - cat = katpoint.Catalogue(add_specials=False) + cat = katpoint.Catalogue() cat.add_tle(file('gps-ops.txt')) cat.add_edb(file('hipparcos.edb')) @@ -168,16 +156,11 @@ cat.add(file('source_list.csv'), tags='calibrator') cat.add_edb(file('hipparcos.edb'), tags='star') - Finally, targets may be removed from the catalogue. This is useful when a - target is to be updated, as adding a target which is already in the catalogue - will silently fail. The reasoning behind this is that a target object is - added once to the target list, but may have multiple references in the lookup - dictionary, one per alias. When updating a target, it is not clear what to do - with all the alternate names. For now, the user has to explicitly remove a - target by name before loading a new version of the target into the catalogue. - The target may be removed via any of its names:: + Finally, targets may be removed from the catalogue. The most recently added + target with the specified name is removed from the targets list as well as + the lookup dict. The target may be removed via any of its names:: - cat = katpoint.Catalogue() + cat = katpoint.Catalogue(add_specials=True) cat.remove('Sun') Filtering and sorting @@ -219,7 +202,7 @@ stored in each target. An example is:: ant = katpoint.Antenna('XDM, -25:53:23, 27:41:03, 1406, 15.0') - cat = katpoint.Catalogue() + cat = katpoint.Catalogue(add_specials=True) cat1 = cat.filter(az_limit_deg=[0, 90], timestamp='2009-10-10', antenna=ant) cat = katpoint.Catalogue(antenna=ant) cat1 = cat.filter(az_limit_deg=[90, 0]) @@ -230,7 +213,7 @@ is required (or defaults will be used). An example is:: ant = katpoint.Antenna('XDM, -25:53:23, 27:41:03, 1406, 15.0') - cat = katpoint.Catalogue() + cat = katpoint.Catalogue(add_specials=True) cat1 = cat.filter(el_limit_deg=[10, 30], timestamp='2009-10-10', antenna=ant) cat = katpoint.Catalogue(antenna=ant) cat1 = cat.filter(el_limit_deg=10) @@ -244,7 +227,7 @@ required (or defaults will be used). An example is:: ant = katpoint.Antenna('XDM, -25:53:23, 27:41:03, 1406, 15.0') - cat = katpoint.Catalogue() + cat = katpoint.Catalogue(add_specials=True) cat.add_tle(file('geo.txt')) sun = cat['Sun'] afristar = cat['AFRISTAR'] @@ -267,14 +250,14 @@ cat = katpoint.Catalogue(file('source_list.csv')) strong_sources = cat.filter(flux_limit_Jy=10.0, flux_freq_MHz=1500) - - An iterator filter, implemented by the :meth:`Catalogue.iterfilter` method. - This is a Python *generator function*, which returns a *generator iterator*, - to be more precise. Each time the returned iterator's .next() method is - invoked, the next suitable :class:`Target` object is returned. If no - timestamp is provided, the criteria are re-evaluated at the time instant - of the .next() call, which makes it easy to cycle through a list of - targets over an extended period of time (as during observation). The - iterator filter is typically used in a for-loop:: + - An iterator filter, implemented by the :meth:`Catalogue.iterfilter` + method. This is a Python *generator function*, which returns a + *generator iterator*, to be more precise. Each time the returned + iterator's .next() method is invoked, the next suitable :class:`Target` + object is returned. If no timestamp is provided, the criteria are + re-evaluated at the time instant of the .next() call, which makes it easy + to cycle through a list of targets over an extended period of time (as + during observation). The iterator filter is typically used in a for-loop:: cat = katpoint.Catalogue(file('source_list.csv')) ant = katpoint.Antenna('XDM, -25:53:23, 27:41:03, 1406, 15.0') @@ -295,20 +278,29 @@ tags : string or sequence of strings, optional Tag or list of tags to add to *targets* (strings will be split on whitespace) - add_specials: {False, True}, optional + add_specials: bool, optional True if *special* bodies specified in :data:`specials` (and 'Zenith') should be added - add_stars: {False, True}, optional + add_stars: bool, optional True if *star* bodies from PyEphem star catalogue should be added antenna : :class:`Antenna` object, optional Default antenna to use for position calculations for all targets flux_freq_MHz : float, optional - Default frequency at which to evaluate flux density of all targets, in MHz + Default frequency at which to evaluate flux density of all targets (MHz) + Notes + ----- + The catalogue object has an interesting relationship with orderedness. + While it is nominally an ordered list of targets, it is considered equal to + another catalogue with the same targets in a different order. This is + because the catalogue may be conveniently reordered in many ways (e.g. + based on elevation, declination, flux, etc.) while remaining essentially + the *same* catalogue. It also allows us to preserve the order in which the + catalogue was assembled, which seems the most natural. """ def __init__(self, targets=None, tags=None, add_specials=False, add_stars=False, antenna=None, flux_freq_MHz=None): - self.lookup = {} + self.lookup = defaultdict(list) self.targets = [] self._antenna = antenna self._flux_freq_MHz = flux_freq_MHz @@ -321,36 +313,28 @@ targets = [] self.add(targets, tags) - # Provide properties so that default antenna or flux frequency changes are passed on to targets - # pylint: disable-msg=E0211,E0202,W0612,W0142,W0212 - def antenna(): - """Class method which creates antenna property.""" - doc = 'Default antenna used to calculate target positions.' - - def fget(self): - return self._antenna - - def fset(self, value): - self._antenna = value - for target in self.targets: - target.antenna = self._antenna - return locals() - antenna = property(**antenna()) - - # pylint: disable-msg=E0211,E0202,W0612,W0142,W0212,C0103 - def flux_freq_MHz(): - """Class method which creates flux_freq_MHz property.""" - doc = 'Default frequency at which to evaluate flux density, in MHz.' - - def fget(self): - return self._flux_freq_MHz - - def fset(self, value): - self._flux_freq_MHz = value - for target in self.targets: - target.flux_freq_MHz = self._flux_freq_MHz - return locals() - flux_freq_MHz = property(**flux_freq_MHz()) + # Provide properties to pass default antenna or flux frequency changes on to targets + @property + def antenna(self): + """Default antenna used to calculate target positions.""" + return self._antenna + + @antenna.setter + def antenna(self, ant): + self._antenna = ant + for target in self.targets: + target.antenna = ant + + @property + def flux_freq_MHz(self): + """Default frequency at which to evaluate flux density, in MHz.""" + return self._flux_freq_MHz + + @flux_freq_MHz.setter + def flux_freq_MHz(self, freq): + self._flux_freq_MHz = freq + for target in self.targets: + target.flux_freq_MHz = freq def __str__(self): """Verbose human-friendly string representation of catalogue object.""" @@ -365,11 +349,16 @@ """Number of targets in catalogue.""" return len(self.targets) + def _targets_with_name(self, name): + """List of targets in catalogue with given name (or alias).""" + return self.lookup.get(_normalised(name), []) + def __getitem__(self, name): """Look up target name in catalogue and return target object. - The name string may be tab-completed in IPython to simplify finding a - target. + This returns the most recently added target with the given name. + The name string may be tab-completed in IPython to simplify finding + a target. Parameters ---------- @@ -383,44 +372,41 @@ """ try: - return self.lookup[_hash(name)] - except KeyError: + return self._targets_with_name(name)[-1] + except IndexError: return None def __contains__(self, obj): """Test whether catalogue contains exact target, or target with given name.""" - name = obj.name if isinstance(obj, Target) else obj - target = self[name] - return target is not None and (not isinstance(obj, Target) or target == obj) + if isinstance(obj, Target): + return obj in self._targets_with_name(obj.name) + else: + return _normalised(obj) in self.lookup def __eq__(self, other): - """Equality comparison operator.""" - # Use lookup dict instead of targets list, as it has a fixed order based on target name - return isinstance(other, Catalogue) and (tuple(self.lookup.values()) == tuple(other.lookup.values())) + """Equality comparison operator (ignores order of targets).""" + return isinstance(other, Catalogue) and set(self.targets) == set(other.targets) def __ne__(self, other): """Inequality comparison operator.""" return not (self == other) def __hash__(self): - """Hash value matches behaviour of equality comparison operator.""" - return hash(tuple(self.lookup.values())) + """Hash value is independent of order of targets in catalogue.""" + return hash(frozenset(self.targets)) def __iter__(self): """Iterate over targets in catalogue.""" return iter(self.targets) - def iternames(self): - """Iterator over known target names in catalogue which can be searched for. - - There are potentially more names than targets in the catalogue, as the - same target can have many names. - - """ + def _ipython_key_completions_(self): + """List of keys used in IPython (version >= 5) tab completion.""" + names = set() for target in self.targets: - yield target.name + names.add(target.name) for alias in target.aliases: - yield alias + names.add(alias) + return sorted(names) def add(self, targets, tags=None): """Add targets to catalogue. @@ -444,7 +430,7 @@ >>> cat = Catalogue() >>> cat.add(file('source_list.csv'), tags='cal') >>> cat.add('Sun, special') - >>> cat2 = Catalogue(add_specials=False) + >>> cat2 = Catalogue() >>> cat2.add(cat.targets) """ @@ -452,26 +438,35 @@ targets = [targets] for target in targets: if isinstance(target, basestring): - # Ignore strings starting with a hash (assumed to be comments) or only containing whitespace - if (target[0] == '#') or (len(target.strip()) == 0): + # Ignore strings starting with a hash (assumed to be comments) + # or only containing whitespace + if (len(target.strip()) == 0) or (target[0] == '#'): continue target = Target(target) if not isinstance(target, Target): - raise ValueError('List of targets should either contain Target objects or description strings') - if _hash(target.name) in self.lookup: - logger.warn("Skipped '%s' [%s] (already in catalogue)" % (target.name, target.tags[0])) - else: - target.add_tags(tags) - target.antenna = self.antenna - target.flux_freq_MHz = self.flux_freq_MHz - self.targets.append(target) - for name in [target.name] + target.aliases: - self.lookup[_hash(name)] = target - logger.debug("Added '%s' [%s] (and %d aliases)" % - (target.name, target.tags[0], len(target.aliases))) + raise ValueError('List of targets should either contain ' + 'Target objects or description strings') + # Add tags first since they affect target identity / description + target.add_tags(tags) + if target in self: + logger.warning("Skipped '%s' [%s] (already in catalogue)", + target.name, target.tags[0]) + continue + target_names = [target.name] + target.aliases + existing_names = [name for name in target_names if name in self] + if existing_names: + logger.warning("Found different targets with same name(s) " + "'%s' in catalogue", ', '.join(existing_names)) + target.antenna = self.antenna + target.flux_freq_MHz = self.flux_freq_MHz + self.targets.append(target) + for name in target_names: + self.lookup[_normalised(name)].append(target) + logger.debug("Added '%s' [%s] (and %d aliases)", + target.name, target.tags[0], len(target.aliases)) def add_tle(self, lines, tags=None): - """Add NORAD Two-Line Element (TLE) targets to catalogue. + r"""Add NORAD Two-Line Element (TLE) targets to catalogue. Examples of catalogue construction can be found in the :class:`Catalogue` documentation. @@ -537,13 +532,13 @@ (name, epoch_diff_days, direction) max_epoch_diff_days = epoch_diff_days if num_outdated > 0: - logger.warning('%d of %d TLE set(s) are outdated, probably making them inaccurate for use right now' % - (num_outdated, len(targets))) + logger.warning('%d of %d TLE set(s) are outdated, probably making them inaccurate for use right now', + num_outdated, len(targets)) logger.warning(worst) self.add(targets, tags) def add_edb(self, lines, tags=None): - """Add XEphem database format (EDB) targets to catalogue. + r"""Add XEphem database format (EDB) targets to catalogue. Examples of catalogue construction can be found in the :class:`Catalogue` documentation. @@ -566,7 +561,6 @@ >>> lines = ['HYP71683,f|S|G2,14:39:35.88 ,-60:50:7.4 ,-0.010,2000,\n', 'HYP113368,f|S|A3,22:57:39.055,-29:37:20.10,1.166,2000,\n'] >>> cat2.add_edb(lines) - """ targets = [] for line in lines: @@ -578,17 +572,22 @@ def remove(self, name): """Remove target from catalogue. + This removes the most recently added target with the given name + from the catalogue. If the target is not in the catalogue, do nothing. + Parameters ---------- name : string Name of target to remove (may also be an alternate name of target) """ - if _hash(name) in self.lookup: - target = self[name] - self.lookup.pop(_hash(target.name)) - for alias in target.aliases: - self.lookup.pop(_hash(alias)) + target = self[name] + if target is not None: + for name in [target.name] + target.aliases: + targets_with_name = self.lookup[_normalised(name)] + targets_with_name.remove(target) + if not targets_with_name: + del self.lookup[_normalised(name)] self.targets.remove(target) def save(self, filename): @@ -969,43 +968,3 @@ if fringe_period is not None: line += ' %10.2f' % (fringe_period,) print(line) - -# -------------------------------------------------------------------------------------------------- -# --- FUNCTION : _catalogue_completer -# -------------------------------------------------------------------------------------------------- - -dict_lookup_match = re.compile(r"""(?:.*\=)?(.*)\[(?P['|"])(?!.*(?P=quote))(.*)$""") - - -def _catalogue_completer(context, event): - """Custom IPython completer for catalogue name lookups. - - This is inspired by Darren Dale's custom dict-like completer for h5py. - - """ - # Parse command line as (ignored = )base['start_of_name - base, start_of_name = dict_lookup_match.split(event.line)[1:4:2] - - # Avoid calling any functions during eval()... - if '(' in base: - raise TryNext - - # Obtain catalogue object from user namespace - try: - cat = eval(base, context.user_ns) - except (NameError, AttributeError): - try: - # IPython version < 1.0 - cat = eval(base, context.shell.user_ns) - except (NameError, AttributeError): - raise TryNext - - # Only continue if this object is actually a Catalogue - if not isinstance(cat, Catalogue): - raise TryNext - - if readline: - # Remove space and plus from delimiter list, so completion works past spaces and pluses in names - readline.set_completer_delims(readline.get_completer_delims().replace(' ', '').replace('+', '')) - - return [name for name in cat.iternames() if name[:len(start_of_name)] == start_of_name] diff -Nru katpoint-0.7/katpoint/conversion.py katpoint-0.9/katpoint/conversion.py --- katpoint-0.7/katpoint/conversion.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/conversion.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,6 +15,7 @@ ################################################################################ """Coordinate conversions not found in PyEphem.""" +from __future__ import print_function, division, absolute_import import numpy as np @@ -28,7 +29,8 @@ This converts a position on the Earth specified in geodetic latitude, longitude and altitude to earth-centered, earth-fixed (ECEF) cartesian - coordinates. This code assumes the WGS84 earth model, described in [1]_. + coordinates. This code assumes the WGS84 earth model, described in + [NIMA2004]_. Parameters ---------- @@ -44,11 +46,14 @@ x_m, y_m, z_m : float or array X, Y, Z coordinates, in metres - .. [1] National Imagery and Mapping Agency, "Department of Defense World - Geodetic System 1984," NIMA TR8350.2, Page 4-4, last updated June, 2004. + References + ---------- + + .. [NIMA2004] National Imagery and Mapping Agency, "Department of Defense + World Geodetic System 1984," NIMA TR8350.2, Page 4-4, last updated + June, 2004. """ - # pylint: disable-msg=C0103 # WGS84 Defining Parameters a = 6378137.0 # semi-major axis of Earth in m f = 1.0 / 298.257223563 # flattening of Earth @@ -101,7 +106,6 @@ .. [geo] Wikipedia entry, "Geodetic system", 2009. """ - # pylint: disable-msg=C0103 # WGS84 Defining Parameters a = 6378137.0 # semi-major axis of Earth in m f = 1.0 / 298.257223563 # flattening of Earth @@ -222,7 +226,7 @@ x_m = ref_x_m - sin_long*e_m - sin_lat*cos_long*n_m + cos_lat*cos_long*u_m y_m = ref_y_m + cos_long*e_m - sin_lat*sin_long*n_m + cos_lat*sin_long*u_m - z_m = ref_z_m + cos_lat*n_m + sin_lat*u_m + z_m = ref_z_m + cos_lat*n_m + sin_lat*u_m return x_m, y_m, z_m @@ -256,9 +260,9 @@ sin_lat, cos_lat = np.sin(ref_lat_rad), np.cos(ref_lat_rad) sin_long, cos_long = np.sin(ref_long_rad), np.cos(ref_long_rad) - e_m = -sin_long*delta_x_m + cos_long*delta_y_m + e_m = -sin_long*delta_x_m + cos_long*delta_y_m n_m = -sin_lat*cos_long*delta_x_m - sin_lat*sin_long*delta_y_m + cos_lat*delta_z_m - u_m = cos_lat*cos_long*delta_x_m + cos_lat*sin_long*delta_y_m + sin_lat*delta_z_m + u_m = cos_lat*cos_long*delta_x_m + cos_lat*sin_long*delta_y_m + sin_lat*delta_z_m return e_m, n_m, u_m diff -Nru katpoint-0.7/katpoint/delay.py katpoint-0.9/katpoint/delay.py --- katpoint-0.7/katpoint/delay.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/delay.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -21,12 +21,14 @@ delay correction for a correlator. """ +from __future__ import print_function, division, absolute_import +from builtins import object, zip +from past.builtins import basestring import logging import json import numpy as np -from past.builtins import basestring from .model import Parameter, Model from .conversion import azel_to_enu diff -Nru katpoint-0.7/katpoint/ephem_extra.py katpoint-0.9/katpoint/ephem_extra.py --- katpoint-0.7/katpoint/ephem_extra.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/ephem_extra.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,7 +15,8 @@ ################################################################################ """Enhancements to PyEphem.""" - +from __future__ import print_function, division, absolute_import +from builtins import object from past.builtins import basestring import numpy as np @@ -128,7 +129,6 @@ given *observer*, while its (az, el) coordinates remain unchanged. """ - # pylint: disable-msg=W0201 if isinstance(observer, ephem.Observer): ra, dec = observer.radec_of(self.az, self.el) self.ra = ra diff -Nru katpoint-0.7/katpoint/flux.py katpoint-0.9/katpoint/flux.py --- katpoint-0.7/katpoint/flux.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/flux.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,14 +15,22 @@ ################################################################################ """Flux density model.""" +from __future__ import print_function, division, absolute_import +from builtins import object +from past.builtins import basestring -import numpy as np +import warnings -from past.builtins import basestring +import numpy as np from .ephem_extra import is_iterable +class FluxError(ValueError): + """Exception for a flux parsing error.""" + pass + + class FluxDensityModel(object): """Spectral flux density model. @@ -32,10 +40,16 @@ log10(S) = a + b*log10(v) + c*log10(v)**2 + d*log10(v)**3 + e*exp(f*log10(v)) where *S* is the flux density in janskies (Jy) and *v* is the frequency in - MHz. The model is based on the Baars polynomial [1]_ (up to a third-order - term) and extended with an exponential term from the 1Jy catalogue [2]_. It - is considered valid for a specified frequency range only. For any frequencies - outside this range a value of NaN is returned. + MHz. The model is based on the Baars polynomial [BGP1977]_ (up to a third- + order term) and extended with an exponential term from the 1Jy catalogue + [KWP+1981]_. It is considered valid for a specified frequency range only. + For any frequencies outside this range a value of NaN is returned. + + It also models polarisation: an optional (I, Q, U, V) vector may be given + to specify fractional Stokes parameters, which scale *S*. If not specified, + the default is unpolarised (I = 1, Q = U = V = 0). It is recommended that I + is left at 1, but it can be changed to model non-physical sources e.g. + negative CLEAN components. The object can be instantiated directly with the minimum and maximum frequencies of the valid frequency range and the model coefficients, or @@ -45,10 +59,12 @@ '1000.0 2000.0 0.34 -0.85 -0.02' '(1000.0 2000.0 0.34 -0.85 0.0 0.0 2.3 -1.0)' + '1000.0 2000.0 0.34 -0.85 0.0 0.0 2.3 -1.0 1.0 0.2 -0.1 0.0' If less than the expected number of coefficients are provided, the rest are - assumed to be zero. If more than the expected number are provided, the extra - coefficients are ignored. + assumed to be zero, except that *I* is assumed to be one. If more than the + expected number are provided, the extra coefficients are ignored, but a + warning is shown. Parameters ---------- @@ -60,9 +76,9 @@ max_freq_MHz : float, optional Maximum frequency for which model is valid, in MHz coefs : sequence of floats, optional - Model coefficients (a, b, c, d, e, f), where missing coefficients at the - end of the sequence are assumed to be zero, and extra coefficients are - ignored + Model coefficients (a, b, c, d, e, f, I, Q, U, V), where missing + coefficients at the end of the sequence are assumed to be zero (except + for I, assumes to be one), and extra coefficients are ignored. Raises ------ @@ -72,14 +88,18 @@ References ---------- - .. [1] J.W.M. Baars, R. Genzel, I.I.K. Pauliny-Toth, A. Witzel, "The Absolute - Spectrum of Cas A; An Accurate Flux Density Scale and a Set of Secondary - Calibrators," Astron. Astrophys., 61, 99-106, 1977. - .. [2] H. Kuehr, A. Witzel, I.I.K. Pauliny-Toth, U. Nauber, "A catalogue of - extragalactic radio sources having flux densities greater than 1 Jy at - 5 GHz," Astron. Astrophys. Suppl. Ser., 45, 367-430, 1981. + .. [BGP1977] J.W.M. Baars, R. Genzel, I.I.K. Pauliny-Toth, A. Witzel, + "The Absolute Spectrum of Cas A; An Accurate Flux Density Scale and + a Set of Secondary Calibrators," Astron. Astrophys., 61, 99-106, 1977. + .. [KWP+1981] H. Kuehr, A. Witzel, I.I.K. Pauliny-Toth, U. Nauber, + "A catalogue of extragalactic radio sources having flux densities greater + than 1 Jy at 5 GHz," Astron. Astrophys. Suppl. Ser., 45, 367-430, 1981. """ + # Coefficients are zero by default, except for I + _DEFAULT_COEFS = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, # a, b, c, d, e, f + 1.0, 0.0, 0.0, 0.0]) # I, Q, U, V + def __init__(self, min_freq_MHz, max_freq_MHz=None, coefs=None): # If the first parameter is a description string, extract the relevant flux parameters from it if isinstance(min_freq_MHz, basestring): @@ -88,30 +108,35 @@ raise ValueError("First parameter '%s' is description string - cannot have other parameters" % (min_freq_MHz,)) # Split description string on spaces and turn into numbers (discarding any parentheses) - flux_info = [float(num) for num in min_freq_MHz.strip(' ()').split()] + try: + flux_info = [float(num) for num in min_freq_MHz.strip(' ()').split()] + except ValueError: + raise FluxError("Floating point number '%s' is invalid" % (min_freq_MHz)) if len(flux_info) < 2: - raise ValueError("Flux density description string '%s' is invalid" % (min_freq_MHz,)) + raise FluxError("Flux density description string '%s' is invalid" % (min_freq_MHz,)) min_freq_MHz, max_freq_MHz, coefs = flux_info[0], flux_info[1], tuple(flux_info[2:]) self.min_freq_MHz = min_freq_MHz self.max_freq_MHz = max_freq_MHz - # Coefficients are zero by default - self.coefs = np.zeros(6) + self.coefs = self._DEFAULT_COEFS.copy() # Extract up to the maximum number of coefficients from given sequence + if len(coefs) > len(self.coefs): + warnings.warn('Received %d coefficients but only expected %d - ignoring the rest' + % (len(coefs), len(self.coefs)), FutureWarning) self.coefs[:min(len(self.coefs), len(coefs))] = coefs[:min(len(self.coefs), len(coefs))] - # Prune zeros at the end of coefficient list for the description string - nonzero_coefs = np.nonzero(self.coefs)[0] - last_nonzero_coef = nonzero_coefs[-1] if len(nonzero_coefs) > 0 else 0 - pruned_coefs = self.coefs[:last_nonzero_coef + 1] - self.description = '(%s %s %s)' % (min_freq_MHz, max_freq_MHz, ' '.join(['%.4g' % (c,) for c in pruned_coefs])) + # Prune defaults at the end of coefficient list for the description string + nondefault_coefs = np.nonzero(self.coefs != self._DEFAULT_COEFS)[0] + last_nondefault_coef = nondefault_coefs[-1] if len(nondefault_coefs) > 0 else 0 + pruned_coefs = self.coefs[:last_nondefault_coef + 1] + self.description = '(%s %s %s)' % (min_freq_MHz, max_freq_MHz, ' '.join(['%r' % (c,) for c in pruned_coefs])) def __str__(self): """Verbose human-friendly string representation.""" return "Flux density defined for %d-%d MHz, coefs=(%s)" % \ - (self.min_freq_MHz, self.max_freq_MHz, ', '.join(['%.4g' % (c,) for c in self.coefs])) + (self.min_freq_MHz, self.max_freq_MHz, ', '.join(['%r' % (c,) for c in self.coefs])) def __repr__(self): """Short human-friendly string representation.""" - param_str = ','.join(np.array('a,b,c,d,e,f'.split(','))[self.coefs != 0.0]) + param_str = ','.join(np.array('a,b,c,d,e,f,I,Q,U,V'.split(','))[self.coefs != self._DEFAULT_COEFS]) return "" % \ (self.min_freq_MHz, self.max_freq_MHz, param_str, id(self)) @@ -128,8 +153,18 @@ """Base hash on description string, just like equality operator.""" return hash(self.description) + @property + def iquv_scale(self): + return self.coefs[6:10] + + def _flux_density_raw(self, freq_MHz): + a, b, c, d, e, f = self.coefs[:6] + log10_v = np.log10(freq_MHz) + log10_S = a + b * log10_v + c * log10_v ** 2 + d * log10_v ** 3 + e * np.exp(f * log10_v) + return 10 ** log10_S + def flux_density(self, freq_MHz): - """Calculate flux density for given observation frequency. + """Calculate Stokes I flux density for given observation frequency. Parameters ---------- @@ -142,10 +177,7 @@ Flux density in Jy, or np.nan if the frequency is out of range """ - a, b, c, d, e, f = self.coefs - log10_v = np.log10(freq_MHz) - log10_S = a + b * log10_v + c * log10_v ** 2 + d * log10_v ** 3 + e * np.exp(f * log10_v) - flux = 10 ** log10_S + flux = self._flux_density_raw(freq_MHz) * self.iquv_scale[0] if is_iterable(freq_MHz): freq_MHz = np.asarray(freq_MHz) flux[freq_MHz < self.min_freq_MHz] = np.nan @@ -153,3 +185,24 @@ return flux else: return flux if (freq_MHz >= self.min_freq_MHz) and (freq_MHz <= self.max_freq_MHz) else np.nan + + def flux_density_stokes(self, freq_MHz): + """Calculate full-Stokes flux density for given observation frequency. + + Parameters + ---------- + freq_MHz : float, or sequence of floats + Frequency at which to evaluate flux density, in MHz + + Returns + ------- + flux_density : array of floats + Flux density in Jy, or np.nan if the frequency is out of range. The + array has an extra final axis of length 4, corresponding to the I, Q, U, V + components. + """ + freq_MHz = np.asarray(freq_MHz) + flux = np.asarray(self._flux_density_raw(freq_MHz)) + flux[freq_MHz < self.min_freq_MHz] = np.nan + flux[freq_MHz > self.max_freq_MHz] = np.nan + return np.multiply.outer(flux, self.iquv_scale) diff -Nru katpoint-0.7/katpoint/__init__.py katpoint-0.9/katpoint/__init__.py --- katpoint-0.7/katpoint/__init__.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/__init__.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -25,14 +25,15 @@ and CASA. """ +from __future__ import print_function, division, absolute_import import logging as _logging -from .target import Target, construct_azel_target, construct_radec_target +from .target import Target, construct_azel_target, construct_radec_target, NonAsciiError from .antenna import Antenna from .timestamp import Timestamp -from .flux import FluxDensityModel -from .catalogue import Catalogue, specials, _catalogue_completer +from .flux import FluxDensityModel, FluxError +from .catalogue import Catalogue, specials from .ephem_extra import lightspeed, rad2deg, deg2rad, wrap_angle, is_iterable from .conversion import (lla_to_ecef, ecef_to_lla, enu_to_ecef, ecef_to_enu, azel_to_enu, enu_to_azel, hadec_to_enu, enu_to_xyz) @@ -44,7 +45,6 @@ # Hide submodules in module namespace, to avoid confusion with corresponding class names # If the module is reloaded, this will fail - ignore the resulting NameError -# pylint: disable-msg=E0601 try: _target, _antenna, _timestamp, _flux, _catalogue, _ephem_extra, \ _conversion, _projection, _pointing, _refraction, _delay = \ @@ -55,22 +55,14 @@ except NameError: pass -# Attempt to register custom IPython tab completer for catalogue name lookups (only when run from IPython shell) -try: - # IPython 0.11 and above - _ip = get_ipython() -except NameError: - # IPython 0.10 and below (or normal Python shell) - _ip = __builtins__.get('__IPYTHON__') -if hasattr(_ip, 'set_hook'): - _ip.set_hook('complete_command', _catalogue_completer, re_key=r"(?:.*\=)?(.+?)\[") - # Setup library logger and add a print-like handler used when no logging is configured class _NoConfigFilter(_logging.Filter): """Filter which only allows event if top-level logging is not configured.""" def filter(self, record): return 1 if not _logging.root.handlers else 0 + + _no_config_handler = _logging.StreamHandler() _no_config_handler.setFormatter(_logging.Formatter(_logging.BASIC_FORMAT)) _no_config_handler.addFilter(_NoConfigFilter()) diff -Nru katpoint-0.7/katpoint/model.py katpoint-0.9/katpoint/model.py --- katpoint-0.7/katpoint/model.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/model.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -20,21 +20,19 @@ saving and display of parameters. """ -import sys +from __future__ import print_function, division, absolute_import +import future.utils +from builtins import object, zip +from past.builtins import basestring try: import ConfigParser as configparser # python2 except ImportError: import configparser # python3 - from collections import OrderedDict import numpy as np -from past.builtins import basestring - -py2 = sys.version[0] == '2' - class Parameter(object): """Generic model parameter. @@ -76,10 +74,10 @@ self.value = value if value is not None else default_value self.default_value = default_value - def __nonzero__(self): + def __bool__(self): """True if parameter is active, i.e. its value differs from default.""" # Do explicit cast to bool, as value can be a NumPy type, resulting in - # an np.bool_ type for the expression (not allowed for __nonzero__) + # an np.bool_ type for the expression (not allowed for __bool__) return bool(self.value != self.default_value) @property @@ -102,20 +100,8 @@ pass -# Use metaclass trick to make Model class docstrings writable. -# This is unnecessary in Python 3.3 and above. -# For more info, see Python issue 12773 (http://bugs.python.org/issue12773) -# and discussion on python-dev: -# https://mail.python.org/pipermail/python-dev/2012-January/115656.html -class WritableDocstring(type): - """Metaclass with the sole purpose of enabling writable class docstrings.""" - - class Model(object): - - __metaclass__ = WritableDocstring - - __doc__ = """Base class for models (e.g. pointing and delay models). + """Base class for models (e.g. pointing and delay models). The base class handles the construction / loading, saving, display and comparison of models. A Model consists of a sequence of Parameters and @@ -136,7 +122,6 @@ Full set of model parameters in the expected order """ - def __init__(self, params): self.header = {} self.params = OrderedDict((p.name, p) for p in params) @@ -145,7 +130,7 @@ """Number of parameters in full model.""" return len(self.params) - def __nonzero__(self): + def __bool__(self): """True if model contains any active (non-default) parameters.""" return any(p for p in self) @@ -265,7 +250,7 @@ """ defaults = dict((p.name, p._to_str(p.default_value)) for p in self) - if py2: + if future.utils.PY2: cfg = configparser.SafeConfigParser(defaults) else: cfg = configparser.ConfigParser(defaults, inline_comment_prefixes=(';', '#')) diff -Nru katpoint-0.7/katpoint/pointing.py katpoint-0.9/katpoint/pointing.py --- katpoint-0.7/katpoint/pointing.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/pointing.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -19,6 +19,8 @@ This implements a pointing model for a non-ideal antenna mount. """ +from __future__ import print_function, division, absolute_import +from builtins import range import logging @@ -35,7 +37,7 @@ The pointing model is the one found in the VLBI Field System and has the standard terms found in most pointing models, including the DSN and TPOINT - models. These terms are numbered P1 to P%d. The first 8 have a standard + models. These terms are numbered P1 to P22. The first 8 have a standard physical interpretation related to misalignment of the mount coordinate system and gravitational deformation, while the rest are ad hoc parameters that model remaining systematic effects in the pointing error residuals. @@ -44,7 +46,7 @@ Parameters ---------- - model : file-like object, sequence of %d floats, or string, optional + model : file-like object, sequence of 22 floats, or string, optional Model specification. If this is a file-like object, load the model from it. If this is a sequence of floats, accept it directly as the model parameters (defaults to sequence of zeroes). If it is a string, @@ -92,18 +94,7 @@ params.append(angle_param('P22', 'elevation nod once per az revolution [tpoint HESA]')) Model.__init__(self, params) self.set(model) - # Fix docstrings to contain the number of parameters - if '%d' in self.__class__.__doc__: - self.__class__.__doc__ = self.__class__.__doc__ % (len(self), len(self)) - - try: - fit_func = self.__class__.fit.__func__ # python2 - except AttributeError: - fit_func = self.__class__.fit # python 3 - if '%d' in fit_func.__doc__: - fit_func.__doc__ = fit_func.__doc__ % (len(self), len(self)) - # pylint: disable-msg=R0914,C0103,W0612 def offset(self, az, el): """Obtain pointing offset at requested (az, el) position(s). @@ -125,7 +116,7 @@ ----- The model is based on poclb/fln.c and poclb/flt.c in Field System version 9.9.0. The C implementation differs from the official description in - [1]_, introducing minor changes to the ad hoc parameters. In this + [Him1993]_, introducing minor changes to the ad hoc parameters. In this implementation, the angle *phi* is fixed at 90 degrees, which hard-codes the model for a standard alt-az mount. @@ -139,8 +130,8 @@ References ---------- - .. [1] Himwich, "Pointing Model Derivation," Mark IV Field System Reference - Manual, Version 8.2, 1 September 1993, available at + .. [Him1993] Himwich, "Pointing Model Derivation," Mark IV Field System + Reference Manual, Version 8.2, 1 September 1993, available at ``_ """ @@ -158,9 +149,9 @@ # Obtain pointing correction using full VLBI model for alt-az mount (no P2 or P10 allowed!) delta_az = P1 + P3*tan_el - P4*sec_el + P5*sin_az*tan_el - P6*cos_az*tan_el + \ - P12*az + P13*cos_az + P14*sin_az + P17*cos_2az + P18*sin_2az + P12*az + P13*cos_az + P14*sin_az + P17*cos_2az + P18*sin_2az delta_el = P5*cos_az + P6*sin_az + P7 + P8*cos_el + \ - P9*el + P11*sin_el + P15*cos_2az + P16*sin_2az + P19*cos_8el + P20*sin_8el + P21*cos_az + P22*sin_az + P9*el + P11*sin_el + P15*cos_2az + P16*sin_2az + P19*cos_8el + P20*sin_8el + P21*cos_az + P22*sin_az return delta_az, delta_el @@ -217,7 +208,7 @@ tan_el = sin_el * sec_el d_corraz_d_az = 1.0 + P5*cos_az*tan_el + P6*sin_az*tan_el + \ - P12 - P13*sin_az + P14*cos_az - P17*2*sin_2az + P18*2*cos_2az + P12 - P13*sin_az + P14*cos_az - P17*2*sin_2az + P18*2*cos_2az d_corraz_d_el = sec_el * (P3*sec_el - P4*tan_el + P5*sin_az*sec_el - P6*cos_az*sec_el) d_correl_d_az = -P5*sin_az + P6*cos_az - P15*2*sin_2az + P16*2*cos_2az - P21*sin_az + P22*cos_az d_correl_d_el = 1.0 - P8*sin_el + P9 + P11*cos_el - P19*8*sin_8el + P20*8*cos_8el @@ -267,9 +258,9 @@ el = el + (a11 * b2 - a21 * b1) / det_J else: max_error, max_az, max_el = np.vstack((sky_error, pointed_az, pointed_el))[:, np.argmax(sky_error)] - logger.warning('Reverse pointing correction did not converge in ' + - '%d iterations - maximum error is %f arcsecs at (az, el) = (%f, %f) radians' % - (iteration + 1, rad2deg(max_error) * 3600., max_az, max_el)) + logger.warning('Reverse pointing correction did not converge in %d iterations - ' + 'maximum error is %f arcsecs at (az, el) = (%f, %f) radians', + iteration + 1, rad2deg(max_error) * 3600., max_az, max_el) return az, el def fit(self, az, el, delta_az, delta_el, sigma_daz=None, sigma_del=None, enabled_params=None): @@ -300,9 +291,9 @@ Returns ------- - params : float array, shape (%d,) + params : float array, shape (22,) Fitted model parameters (full model), in radians - sigma_params : float array, shape (%d,) + sigma_params : float array, shape (22,) Standard errors on fitted parameters, in radians Notes @@ -310,12 +301,12 @@ Since the standard pointing model is linear in the model parameters, it is fit with linear least-squares techniques. This is done by creating a design matrix and solving the linear system via singular value - decomposition (SVD), as explained in [1]_. + decomposition (SVD), as explained in [PTV+1992]_. References ---------- - .. [1] Press, Teukolsky, Vetterling, Flannery, "Numerical Recipes in C," - 2nd Ed., pp. 671-681, 1992. Section 15.4: "General Linear Least + .. [PTV+1992] Press, Teukolsky, Vetterling, Flannery, "Numerical Recipes + in C," 2nd Ed., pp. 671-681, 1992. Section 15.4: "General Linear Least Squares", available at ``_ """ @@ -377,6 +368,6 @@ self.fromlist(param_vector) # Also obtain standard errors of parameters (see NRinC, 2nd ed, Eq. 15.4.19) sigma_params[enabled_params - 1] = np.sqrt(np.sum((Vt.T / s[np.newaxis, :]) ** 2, axis=1)) -# logger.info('Fit pointing model using %dx%d design matrix with condition number %.2f' % (N, M, s[0] / s[-1])) +# logger.info('Fit pointing model using %dx%d design matrix with condition number %.2f', N, M, s[0] / s[-1]) return param_vector, sigma_params diff -Nru katpoint-0.7/katpoint/projection.py katpoint-0.9/katpoint/projection.py --- katpoint-0.7/katpoint/projection.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/projection.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -20,11 +20,12 @@ onto a plane and deprojects the plane coordinates back to the sphere. It complements the ephem module, which focuses on transformations between various spherical coordinate systems instead. The routines are derived from AIPS, as -documented in [1]_ and [2]_ and implemented in the DIRCOS and NEWPOS routines in -the 31DEC08 release, with minor improvements. The projections are -referred to by their AIPS (and FITS) codes, as also described in [3]_ and -implemented in Calabretta's WCSLIB. The (x, y) coordinates in this module -correspond to the (L, M) direction cosines calculated in [1]_ and [2]_. +documented in [Gre1993a]_ and [Gre1993b]_ and implemented in the DIRCOS and +NEWPOS routines in the 31DEC08 release, with minor improvements. The projections +are referred to by their AIPS (and FITS) codes, as also described in [CB2002]_ +and implemented in Calabretta's WCSLIB. The (x, y) coordinates in this module +correspond to the (L, M) direction cosines calculated in [Gre1993a]_ and +[Gre1993b]_. Any spherical coordinate system can be used in the projections, as long as the target and reference points are expressed in the same system of longitude and @@ -63,8 +64,8 @@ - Orthographic (**SIN**): This is the standard projection in aperture synthesis radio astronomy, as it ties in closely with the 2-D Fourier imaging equation and the resultant (l, m) coordinate system. It is the simple orthographic - projection of AIPS and [1]_, not the generalised slant orthographic projection - of [3]_. + projection of AIPS and [Gre1993a]_, not the generalised slant orthographic + projection of [CB2002]_. - Gnomonic (**TAN**): This is commonly used in optical astronomy. Great circles are projected as straight lines, so that the shortest distance between two @@ -116,12 +117,15 @@ x, y = katpoint.sphere_to_plane['ARC'](az0, el0, az, el) az, el = katpoint.plane_to_sphere['ARC'](az0, el0, x, y) -.. [1] Greisen, "Non-linear Coordinate Systems in AIPS," AIPS Memo 27, 1993. -.. [2] Greisen, "Additional Non-linear Coordinates in AIPS," AIPS Memo 46, 1993. -.. [3] Calabretta, Greisen, "Representations of celestial coordinates in +.. [Gre1993a] Greisen, "Non-linear Coordinate Systems in AIPS," AIPS Memo 27, + 1993. +.. [Gre1993b] Greisen, "Additional Non-linear Coordinates in AIPS," AIPS Memo 46, + 1993. +.. [CB2002] Calabretta, Greisen, "Representations of celestial coordinates in FITS. II," Astronomy & Astrophysics, vol. 395, pp. 1077-1122, 2002. """ +from __future__ import print_function, division, absolute_import import numpy as np @@ -690,7 +694,7 @@ def plane_to_sphere_ssn(az0, el0, x, y): - """Deproject plane to sphere using swapped orthographic (SSN) projection. + r"""Deproject plane to sphere using swapped orthographic (SSN) projection. The swapped orthographic deprojection has more restrictions than the corresponding orthographic (SIN) deprojection: @@ -698,8 +702,10 @@ - The (x, y) coordinates should lie within or on the unit circle - The magnitude of the x coordinate should be less than cos(el0) radians - The y coordinate should satisfy - y >= -sqrt(cos(el0) ** 2 - x ** 2), el0 >= 0, and - y <= sqrt(cos(el0) ** 2 - x ** 2), el0 < 0, + + - :math:`y \ge -\sqrt{\cos(\text{el0})^2 - x^2}, \text{el0} \ge 0`, and + - :math:`y \le \sqrt{\cos(\text{el0})^2 - x^2}, \text{el0} < 0`, + to ensure that the target elevation is within pi/2 radians of reference elevation - the y domain is therefore bounded by two semicircles with radii 1 and cos(el0), respectively @@ -767,6 +773,7 @@ el = np.arctan2(num, den) return az, el + # -------------------------------------------------------------------------------------------------- # --- Top-level projection routines # -------------------------------------------------------------------------------------------------- diff -Nru katpoint-0.7/katpoint/refraction.py katpoint-0.9/katpoint/refraction.py --- katpoint-0.7/katpoint/refraction.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/refraction.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -19,6 +19,8 @@ This implements correction for refractive bending in the atmosphere. """ +from __future__ import print_function, division, absolute_import +from builtins import object, range import logging @@ -60,16 +62,10 @@ The code is based on poclb/refrwn.c in Field System version 9.9.2, which was added on 2006-11-15. This is a C version (with typos fixed) of the Fortran version in polb/refr.f. As noted in the Field System - documentation [1]_, the refraction model originated with the Haystack + documentation [Him1993]_, the refraction model originated with the Haystack pointing system, but the documentation for this algorithm seems to have been lost. It agrees well with the DSN refraction model, though. - References - ---------- - .. [1] Himwich, "Station Programs," Mark IV Field System Reference Manual, - Version 8.2, 1 September 1993, available at - ``_ - """ p = (0.458675e1, 0.322009e0, 0.103452e-1, 0.274777e-3, 0.157115e-5) cvt = 1.33289 @@ -217,7 +213,7 @@ lower = np.where(test_el < refracted_el, el, lower) upper = np.where(test_el > refracted_el, el, upper) else: - logger.warning('Reverse refraction correction did not converge' + - ' in %d iterations - elevation differs by at most %f arcsecs' % - (iteration + 1, rad2deg(np.abs(test_el - refracted_el).max()) * 3600.)) + logger.warning('Reverse refraction correction did not converge in ' + '%d iterations - elevation differs by at most %f arcsecs', + iteration + 1, rad2deg(np.abs(test_el - refracted_el).max()) * 3600.) return el diff -Nru katpoint-0.7/katpoint/target.py katpoint-0.9/katpoint/target.py --- katpoint-0.7/katpoint/target.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/target.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,12 +15,13 @@ ################################################################################ """Target object used for pointing and flux density calculation.""" +from __future__ import print_function, division, absolute_import +from builtins import object, range +from past.builtins import basestring import numpy as np import ephem -from past.builtins import basestring - from .timestamp import Timestamp from .flux import FluxDensityModel from .ephem_extra import (StationaryBody, NullBody, is_iterable, lightspeed, @@ -29,6 +30,11 @@ from .projection import sphere_to_plane, sphere_to_ortho, plane_to_sphere +class NonAsciiError(ValueError): + """Exception when non-ascii characters are found.""" + pass + + class Target(object): """A target which can be pointed at by an antenna. @@ -132,7 +138,6 @@ descr += ' (%s)' % (', '.join(self.aliases),) descr += ', tags=%s' % (' '.join(self.tags),) if 'radec' in self.tags: - # pylint: disable-msg=W0212 descr += ', %s %s' % (self.body._ra, self.body._dec) if self.body_type == 'azel': descr += ', %s %s' % (self.body.az, self.body.el) @@ -236,7 +241,6 @@ # Check if it's an unnamed target with a default name if names.startswith('Ra:'): fields = [tags] - # pylint: disable-msg=W0212 fields += [str(self.body._ra), str(self.body._dec)] if fluxinfo: fields += [fluxinfo] @@ -476,7 +480,8 @@ return l, b ra, dec = self.astrometric_radec(timestamp, antenna) if is_iterable(ra): - lb = np.array([ephem.Galactic(ephem.Equatorial(ra[n], dec[n])).get() for n in range(len(ra))]) + lb = np.array([ephem.Galactic(ephem.Equatorial(ra[n], dec[n])).get() + for n in range(len(ra))]) return lb[:, 0], lb[:, 1] else: return ephem.Galactic(ephem.Equatorial(ra, dec)).get() @@ -511,14 +516,12 @@ Notes ----- - The formula can be found in the AIPS++ glossary [1]_ or in the SLALIB + The formula can be found in the `AIPS++ glossary`_ or in the SLALIB source code (file pa.f, function sla_PA) which is part of the now - defunct Starlink project [2]_. + defunct `Starlink project`_. - References - ---------- - .. [1] "AIPS++ Glossary," ``_ - .. [2] "Starlink Project," ``_ + .. _`AIPS++ Glossary`: http://www.astron.nl/aips++/docs/glossary/p.html + .. _`Starlink Project`: http://www.starlink.rl.ac.uk """ timestamp, antenna = self._set_timestamp_antenna_defaults(timestamp, antenna) @@ -613,24 +616,42 @@ dimension to the timestamp. """ timestamp, antenna = self._set_timestamp_antenna_defaults(timestamp, antenna) - # NCP vector is J2000 NCP - ncp = construct_radec_target(0.0, np.pi / 2.0) - # Get J2000 NCP az-el vector at current epoch pointed to by reference antenna - ncp_az, ncp_el = ncp.azel(timestamp, antenna) + if is_iterable(timestamp) and self.body_type != 'radec': + # Some calculations depend on ra/dec in a way that won't easily + # vectorise. + bases = [self.uvw_basis(t, antenna) for t in timestamp] + return np.stack(bases, axis=-1) + + # Offset the target slightly in declination to approximate the + # derivative of ENU in the direction of increasing declination. This + # used to just use the NCP, but the astrometric-to-topocentric + # conversion doesn't simply rotate the celestial sphere, but also + # distorts it, and so that introduced errors. + # + # To avoid issues close to the poles, we always offset towards the + # equator. We also can't offset by too little, as ephem uses only + # single precision and this method suffers from loss of precision. + # 0.03 was found by experimentation (albeit on a single data set) to + # to be large enough to avoid the numeric instability. + if is_iterable(timestamp): + # Due to the test above, this is a radec target and so timestamp + # doesn't matter. But we want a scalar. + ra, dec = self.radec(None, antenna) + else: + ra, dec = self.radec(timestamp, antenna) + offset_sign = -1 if dec > 0 else 1 + offset = construct_radec_target(ra, dec + 0.03 * offset_sign) + # Get offset az-el vector at current epoch pointed to by reference antenna + offset_az, offset_el = offset.azel(timestamp, antenna) # Obtain direction vector(s) from reference antenna to target az, el = self.azel(timestamp, antenna) # w axis points toward target w = np.array(azel_to_enu(az, el)) - # enu vector pointing from reference antenna to north celestial pole - z = np.array(azel_to_enu(ncp_az, ncp_el)) - # u axis is orthogonal to z and w, and row_stack makes it 2-D array of column vectors (for finding poles) - u = np.row_stack(np.cross(z, w, axis=0)) + # enu vector pointing from reference antenna to offset point + z = np.array(azel_to_enu(offset_az, offset_el)) + # u axis is orthogonal to z and w, and row_stack makes it 2-D array of column vectors + u = np.row_stack(np.cross(z, w, axis=0)) * offset_sign u_norm = np.sqrt(np.sum(u ** 2, axis=0)) - # If the target is a celestial pole (so that w equals z or -z), u and v become degenerate - poles = u_norm < 1e-12 - # Arbitrarily pick east vector of ENU system as u in this case - u[:, poles] = [[1.0], [0.0], [0.0]] - u_norm[poles] = 1.0 # Ensure that u and w (and therefore v) have the same shape to handle scalar vs array output correctly u = u.reshape(w.shape) / u_norm v = np.cross(w, u, axis=0) @@ -648,7 +669,7 @@ Parameters ---------- - antenna2 : :class:`Antenna` object + antenna2 : :class:`Antenna` object or sequence Second antenna of baseline pair (baseline vector points toward it) timestamp : :class:`Timestamp` object or equivalent, or sequence, optional Timestamp(s) in UTC seconds since Unix epoch (defaults to now) @@ -658,8 +679,10 @@ Returns ------- - u, v, w : float, or array of same shape as *timestamp* - (u, v, w) coordinates of baseline, in metres + u, v, w : float or array + (u, v, w) coordinates of baseline, in metres. If `timestamp` and/or + `antenna2` is a sequence, returns an array, with axes in that + order. Notes ----- @@ -670,14 +693,17 @@ """ timestamp, antenna = self._set_timestamp_antenna_defaults(timestamp, antenna) - # Obtain baseline vector from reference antenna to second antenna - baseline_m = antenna.baseline_toward(antenna2) # Obtain basis vectors basis = self.uvw_basis(timestamp, antenna) + # Obtain baseline vector from reference antenna to second antenna + if is_iterable(antenna2): + baseline_m = np.stack([antenna.baseline_toward(a2) for a2 in antenna2]) + else: + baseline_m = antenna.baseline_toward(antenna2) # Apply linear coordinate transformation. A single call np.dot won't # work for both the scalar and array case, so we explicitly specify the # axes to sum over. - u, v, w = np.tensordot(basis, baseline_m, ([1], [0])) + u, v, w = np.tensordot(basis, baseline_m, ([1], [-1])) return u, v, w def lmn(self, ra, dec, timestamp=None, antenna=None): @@ -718,6 +744,9 @@ frequency is out of range, a flux value of NaN is returned for that frequency. + This returns only Stokes I. Use :meth:`flux_density_stokes` to get + polarisation information. + Parameters ---------- freq_MHz : float or sequence, optional @@ -741,9 +770,47 @@ raise ValueError('Please specify frequency at which to measure flux density') if self.flux_model is None: # Target has no specified flux density - return np.tile(np.nan, np.shape(flux_freq_MHz)) if is_iterable(flux_freq_MHz) else np.nan + return np.full(np.shape(flux_freq_MHz), np.nan) if is_iterable(flux_freq_MHz) else np.nan return self.flux_model.flux_density(flux_freq_MHz) + def flux_density_stokes(self, flux_freq_MHz=None): + """Calculate flux density for given observation frequency (or frequencies), full-Stokes. + + See :meth:`flux_density` + This uses the stored flux density model to calculate the flux density at + a given frequency (or frequencies). See the documentation of + :class:`FluxDensityModel` for more details of this model. If the flux + frequency is unspecified, the default value supplied to the target object + during construction is used. If no flux density model is available or a + frequency is out of range, a flux value of NaN is returned for that + frequency. + + Parameters + ---------- + freq_MHz : float or sequence, optional + Frequency at which to evaluate flux density, in MHz + + Returns + ------- + flux_density : array of float + Flux density in Jy, or np.nan if frequency is out of range or target + does not have flux model. The shape matches the input with an extra + trailing dimension of size 4 containing Stokes I, Q, U, V. + + Raises + ------ + ValueError + If no frequency is specified, and no default frequency was set either + + """ + if flux_freq_MHz is None: + flux_freq_MHz = self.flux_freq_MHz + if flux_freq_MHz is None: + raise ValueError('Please specify frequency at which to measure flux density') + if self.flux_model is None: + return np.full(np.shape(flux_freq_MHz) + (4,), np.nan) + return self.flux_model.flux_density_stokes(flux_freq_MHz) + def separation(self, other_target, timestamp=None, antenna=None): """Angular separation between this target and another one. @@ -898,7 +965,7 @@ try: description.encode('ascii') except UnicodeError: - raise ValueError("Target description %r contains non-ASCII characters" % description) + raise NonAsciiError("Target description %r contains non-ASCII characters" % description) fields = [s.strip() for s in description.split(',')] if len(fields) < 2: raise ValueError("Target description '%s' must have at least two fields" % description) diff -Nru katpoint-0.7/katpoint/test/aips_projection/build_module.sh katpoint-0.9/katpoint/test/aips_projection/build_module.sh --- katpoint-0.7/katpoint/test/aips_projection/build_module.sh 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/aips_projection/build_module.sh 2019-10-02 09:50:21.000000000 +0000 @@ -1,7 +1,7 @@ #!/bin/bash ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy diff -Nru katpoint-0.7/katpoint/test/__init__.py katpoint-0.9/katpoint/test/__init__.py --- katpoint-0.7/katpoint/test/__init__.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/__init__.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -55,5 +55,6 @@ testsuite.addTests(loader.loadTestsFromModule(test_delay)) return testsuite + if __name__ == '__main__': unittest.main(defaultTest='suite') diff -Nru katpoint-0.7/katpoint/test/test_antenna.py katpoint-0.9/katpoint/test/test_antenna.py --- katpoint-0.7/katpoint/test/test_antenna.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_antenna.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,37 +15,35 @@ ################################################################################ """Tests for the antenna module.""" -# pylint: disable-msg=C0103,W0212 +from __future__ import print_function, division, absolute_import import unittest import time -try: - import cPickle as pickle # python2 -except ImportError: - import pickle # python3 +import pickle -import katpoint import numpy as np +import katpoint + + def assert_angles_almost_equal(x, y, decimal): - primary_angle = lambda x: x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi + def primary_angle(x): + return x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi np.testing.assert_almost_equal(primary_angle(x - y), np.zeros(np.shape(x)), decimal=decimal) -class TestAntennaConstruction(unittest.TestCase): - """Test construction of antennas from strings and vice versa.""" + +class TestAntenna(unittest.TestCase): + """Test :class:`katpoint.antenna.Antenna`.""" def setUp(self): self.valid_antennas = [ 'XDM, -25:53:23.0, 27:41:03.0, 1406.1086, 15.0', 'FF1, -30:43:17.3, 21:24:38.5, 1038.0, 12.0, 18.4 -8.7 0.0', - 'FF2, -30:43:17.3, 21:24:38.5, 1038.0, 12.0, 86.2 25.5 0.0, ' - '-0:06:39.6 0 0 0 0 0 0:09:48.9, 1.16' + ('FF2, -30:43:17.3, 21:24:38.5, 1038.0, 12.0, 86.2 25.5 0.0, ' + '-0:06:39.6 0 0 0 0 0 0:09:48.9, 1.16') ] self.invalid_antennas = [ 'XDM, -25:53:23.05075, 27:41:03.0', '', - # Delay model can now have any number of terms (not 3 minimum) -# 'FF1, -30:43:17.3, 21:24:38.5, 1038.0, 12.0, 18.4 -8.7', -# 'FF1, -30:43:17.3, 21:24:38.5, 1038.0, 12.0, 18.4, -8.7, 0.0' ] self.timestamp = '2009/07/07 08:36:20' @@ -91,3 +89,9 @@ sid3 = ant.local_sidereal_time([self.timestamp, self.timestamp]) sid4 = ant.local_sidereal_time([utc_secs, utc_secs]) assert_angles_almost_equal(sid3, sid4, decimal=12) + + def test_array_reference_antenna(self): + ant = katpoint.Antenna(self.valid_antennas[2]) + ref_ant = ant.array_reference_antenna() + self.assertEqual(ref_ant.description, + 'array, -30:43:17.3, 21:24:38.5, 1038.0, 12.0, , , 1.16') diff -Nru katpoint-0.7/katpoint/test/test_catalogue.py katpoint-0.9/katpoint/test/test_catalogue.py --- katpoint-0.7/katpoint/test/test_catalogue.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_catalogue.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,7 +15,7 @@ ################################################################################ """Tests for the catalogue module.""" -# pylint: disable-msg=C0103,W0212 +from __future__ import print_function, division, absolute_import import unittest import time @@ -26,22 +26,76 @@ # Use the current year in TLE epochs to avoid pyephem crash due to expired TLEs YY = time.localtime().tm_year % 100 + class TestCatalogueConstruction(unittest.TestCase): """Test construction of catalogues.""" def setUp(self): - self.tle_lines = ['GPS BIIA-21 (PRN 09) \n', + self.tle_lines = ['# Comment ignored\n', + 'GPS BIIA-21 (PRN 09) \n', '1 22700U 93042A %02d266.32333151 .00000012 00000-0 10000-3 0 805%1d\n' % - (YY, (YY // 10 + YY - 7 + 4) % 10), + (YY, (YY // 10 + YY - 7 + 4) % 10), '2 22700 55.4408 61.3790 0191986 78.1802 283.9935 2.00561720104282\n'] - self.edb_lines = ['HIC 13847,f|S|A4,2:58:16.03,-40:18:17.1,2.906,2000,\n'] + self.edb_lines = ['# Comment ignored\n', + 'HIC 13847,f|S|A4,2:58:16.03,-40:18:17.1,2.906,2000,\n'] self.antenna = katpoint.Antenna('XDM, -25:53:23.05075, 27:41:03.36453, 1406.1086, 15.0') + def test_catalogue_basic(self): + """Basic catalogue tests.""" + cat = katpoint.Catalogue(add_specials=True) + repr(cat) + str(cat) + cat.add('# Comments will be ignored') + with self.assertRaises(ValueError): + cat.add([1]) + + def test_catalogue_tab_completion(self): + cat = katpoint.Catalogue() + cat.add('Nothing, special') + cat.add('Earth | Terra Incognita, azel, 0, 0') + cat.add('Earth | Sky, azel, 0, 90') + # Check that it returns a sorted list + self.assertEqual(cat._ipython_key_completions_(), + ['Earth', 'Nothing', 'Sky', 'Terra Incognita']) + + def test_catalogue_same_name(self): + """"Test add() and remove() of targets with the same name.""" + cat = katpoint.Catalogue() + targets = ['Sun, special', 'Sun | Sol, special', 'Sun, special hot'] + # Add various targets called Sun + cat.add(targets[0]) + self.assertEqual(cat['Sun'].description, targets[0]) + cat.add(targets[0]) + self.assertEqual(len(cat), 1, 'Did not ignore duplicate target') + cat.add(targets[1]) + self.assertEqual(cat['Sun'].description, targets[1]) + cat.add(targets[2]) + self.assertEqual(cat['Sun'].description, targets[2]) + # Check length, iteration, membership + self.assertEqual(len(cat), len(targets)) + for n, t in enumerate(cat): + self.assertEqual(t.description, targets[n]) + self.assertIn('Sun', cat) + self.assertIn('Sol', cat) + for t in targets: + self.assertIn(katpoint.Target(t), cat) + # Remove targets one by one + cat.remove('Sun') + self.assertEqual(cat['Sun'].description, targets[1]) + cat.remove('Sun') + self.assertEqual(cat['Sun'].description, targets[0]) + cat.remove('Sun') + self.assertTrue(len(cat) == len(cat.targets) == len(cat.lookup) == 0, + 'Catalogue not empty') + def test_construct_catalogue(self): """Test construction of catalogues.""" cat = katpoint.Catalogue(add_specials=True, add_stars=True, antenna=self.antenna) + num_targets_original = len(cat) + self.assertEqual(num_targets_original, len(katpoint.specials) + 1 + 94, 'Number of targets incorrect') + # Add target already in catalogue - no action cat.add(katpoint.Target('Sun, special')) num_targets = len(cat) - self.assertEqual(num_targets, len(katpoint.specials) + 1 + 94, 'Number of targets incorrect') + self.assertEqual(num_targets, num_targets_original, 'Number of targets incorrect') cat2 = katpoint.Catalogue(add_specials=True, add_stars=True) cat2.add(katpoint.Target('Sun, special')) self.assertEqual(cat, cat2, 'Catalogues not equal') @@ -49,7 +103,14 @@ self.assertEqual(hash(cat), hash(cat2), 'Catalogue hashes not equal') except TypeError: self.fail('Catalogue object not hashable') - test_target = cat.targets[0] + # Add different targets with the same name + cat2.add(katpoint.Target('Sun, special hot')) + cat2.add(katpoint.Target('Sun | Sol, special')) + self.assertEqual(len(cat2), num_targets_original + 2, 'Number of targets incorrect') + cat2.remove('Sol') + self.assertEqual(len(cat2), num_targets_original + 1, 'Number of targets incorrect') + self.assertTrue(cat != cat2, 'Catalogues should not be equal') + test_target = cat.targets[-1] self.assertEqual(test_target.description, cat[test_target.name].description, 'Lookup failed') self.assertEqual(cat['Non-existent'], None, 'Lookup of non-existent target failed') cat.add_tle(self.tle_lines, 'tle') @@ -59,8 +120,24 @@ self.assertEqual(len(cat.targets), num_targets + 1, 'Number of targets incorrect') closest_target, dist = cat.closest_to(test_target) self.assertEqual(closest_target.description, test_target.description, 'Closest target incorrect') -# Reinstate this test once separation() can handle angles on top of each other (currently produces NaNs) -# self.assertAlmostEqual(dist, 0.0, places=5, msg='Target should be on top of itself') + self.assertAlmostEqual(dist, 0.0, places=5, msg='Target should be on top of itself') + + def test_that_equality_and_hash_ignore_order(self): + a = katpoint.Catalogue() + b = katpoint.Catalogue() + t1 = katpoint.Target('Nothing, special') + t2 = katpoint.Target('Sun, special') + a.add(t1) + a.add(t2) + b.add(t2) + b.add(t1) + self.assertEqual(a, b, 'Shuffled catalogues are not equal') + self.assertEqual(hash(a), hash(b), 'Shuffled catalogues have different hashes') + + def test_skip_empty(self): + cat = katpoint.Catalogue(['', '# comment', ' ', '\t\r ']) + self.assertEqual(len(cat), 0) + class TestCatalogueFilterSort(unittest.TestCase): """Test filtering and sorting of catalogues.""" @@ -99,13 +176,13 @@ self.assertEqual(cat1, cat, 'Catalogue equality failed') self.assertEqual(cat1.targets[0].name, 'Achernar', 'Sorting on name failed') cat2 = cat.sort(key='ra', timestamp=self.timestamp, antenna=self.antenna) - self.assertEqual(cat2.targets[0].name, 'Sirrah', 'Sorting on ra failed') # RA: 0:08:53.09 + self.assertEqual(cat2.targets[0].name, 'Sirrah', 'Sorting on ra failed') # RA: 0:08:53.09 cat3 = cat.sort(key='dec', timestamp=self.timestamp, antenna=self.antenna) - self.assertEqual(cat3.targets[0].name, 'Agena', 'Sorting on dec failed') # DEC: -60:25:27.3 + self.assertEqual(cat3.targets[0].name, 'Agena', 'Sorting on dec failed') # DEC: -60:25:27.3 cat4 = cat.sort(key='az', timestamp=self.timestamp, antenna=self.antenna, ascending=False) - self.assertEqual(cat4.targets[0].name, 'Polaris', 'Sorting on az failed') # az: 359:25:07.3 + self.assertEqual(cat4.targets[0].name, 'Polaris', 'Sorting on az failed') # az: 359:25:07.3 cat5 = cat.sort(key='el', timestamp=self.timestamp, antenna=self.antenna) - self.assertEqual(cat5.targets[-1].name, 'Zenith', 'Sorting on el failed') # el: 90:00:00.0 + self.assertEqual(cat5.targets[-1].name, 'Zenith', 'Sorting on el failed') # el: 90:00:00.0 cat.add(self.flux_target) cat6 = cat.sort(key='flux', ascending=False, flux_freq_MHz=1.5) self.assertTrue('flux' in (cat6.targets[0].name, cat6.targets[-1].name), @@ -122,17 +199,3 @@ cat.antenna = self.antenna cat.flux_freq_MHz = 1.5 cat.visibility_list(timestamp=self.timestamp) - - def test_completer(self): - """Test IPython tab completer.""" - # pylint: disable-msg=W0201,W0612,R0903 - cat = katpoint.Catalogue(add_stars=True) - # Set up dummy object containing user namespace and line to be completed - class Dummy(object): - pass - event = Dummy() - event.shell = Dummy() - event.shell.user_ns = locals() - event.line = "t = cat['Rasal" - names = katpoint._catalogue_completer(event, event) - self.assertEqual(names, ['Rasalgethi', 'Rasalhague'], 'Tab completer failed') diff -Nru katpoint-0.7/katpoint/test/test_conversion.py katpoint-0.9/katpoint/test/test_conversion.py --- katpoint-0.7/katpoint/test/test_conversion.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_conversion.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,17 +15,21 @@ ################################################################################ """Tests for the conversion module.""" -# pylint: disable-msg=C0103,W0212 +from __future__ import print_function, division, absolute_import import unittest + import numpy as np import katpoint + def assert_angles_almost_equal(x, y, decimal): - primary_angle = lambda x: x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi + def primary_angle(x): + return x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi np.testing.assert_almost_equal(primary_angle(x - y), np.zeros(np.shape(x)), decimal=decimal) + class TestGeodetic(unittest.TestCase): """Closure tests for geodetic coordinate transformations.""" def setUp(self): @@ -60,6 +64,7 @@ np.testing.assert_almost_equal(new_y, y, decimal=8) np.testing.assert_almost_equal(new_z, z, decimal=8) + class TestSpherical(unittest.TestCase): """Closure tests for spherical coordinate transformations.""" def setUp(self): diff -Nru katpoint-0.7/katpoint/test/test_delay.py katpoint-0.9/katpoint/test/test_delay.py --- katpoint-0.7/katpoint/test/test_delay.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_delay.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,6 +15,7 @@ ################################################################################ """Tests for the model module.""" +from __future__ import print_function, division, absolute_import import json import unittest diff -Nru katpoint-0.7/katpoint/test/test_flux.py katpoint-0.9/katpoint/test/test_flux.py --- katpoint-0.7/katpoint/test/test_flux.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_flux.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,9 +15,9 @@ ################################################################################ """Tests for the flux module.""" -# pylint: disable-msg=C0103,W0212 +from __future__ import print_function, division, absolute_import -import unittest +import unittest2 as unittest import numpy as np @@ -27,34 +27,70 @@ class TestFluxDensityModel(unittest.TestCase): """Test flux density model calculation.""" def setUp(self): - self.flux_model = katpoint.FluxDensityModel('(1.0 2.0 2.0 0.0 0.0 0.0 0.0 0.0)') - self.too_many_params = katpoint.FluxDensityModel('(1.0 2.0 2.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0)') + self.unit_model = katpoint.FluxDensityModel(100., 200., [0.]) + self.unit_model2 = katpoint.FluxDensityModel(100., 200., [0.]) + self.flux_model = katpoint.FluxDensityModel('(1.0 2.0 2.0 0.0 0.0 0.0 0.0 0.0 2.0 0.5 0.25 -0.75)') + with self.assertWarns(FutureWarning): + self.too_many_params = katpoint.FluxDensityModel( + '(1.0 2.0 2.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0)') self.too_few_params = katpoint.FluxDensityModel('(1.0 2.0 2.0)') self.flux_target = katpoint.Target('radec, 0.0, 0.0, ' + self.flux_model.description) self.no_flux_target = katpoint.Target('radec, 0.0, 0.0') - def test_flux_density(self): - """Test flux density calculation.""" - unit_model = katpoint.FluxDensityModel(100., 200., [0.]) - self.assertEqual(unit_model.flux_density(110.), 1.0, 'Flux calculation wrong') + def test_construct(self): self.assertRaises(ValueError, katpoint.FluxDensityModel, '1.0 2.0 2.0', 2.0, [2.0]) self.assertRaises(ValueError, katpoint.FluxDensityModel, '1.0') - self.assertEqual(self.flux_model.flux_density(1.5), 100.0, 'Flux calculation wrong') + + def test_description(self): + self.assertEqual(self.flux_model.description, '(1.0 2.0 2.0 0.0 0.0 0.0 0.0 0.0 2.0 0.5 0.25 -0.75)') + # Must truncate default coefficients, including I=1 + self.assertEqual(self.too_many_params.description, '(1.0 2.0 2.0)') + # At least one coefficient is always shown + self.assertEqual(self.unit_model.description, '(100.0 200.0 0.0)') + + def test_flux_density(self): + """Test flux density calculation.""" + self.assertEqual(self.unit_model.flux_density(110.), 1.0, 'Flux calculation wrong') + self.assertEqual(self.flux_model.flux_density(1.5), 200.0, 'Flux calculation wrong') self.assertEqual(self.too_many_params.flux_density(1.5), 100.0, 'Flux calculation for too many params wrong') self.assertEqual(self.too_few_params.flux_density(1.5), 100.0, 'Flux calculation for too few params wrong') np.testing.assert_equal(self.flux_model.flux_density([1.5, 1.5]), - np.array([100.0, 100.0]), 'Flux calculation for multiple frequencies wrong') + np.array([200.0, 200.0]), 'Flux calculation for multiple frequencies wrong') np.testing.assert_equal(self.flux_model.flux_density([0.5, 2.5]), np.array([np.nan, np.nan]), 'Flux calculation for out-of-range frequencies wrong') self.assertRaises(ValueError, self.no_flux_target.flux_density) np.testing.assert_equal(self.no_flux_target.flux_density([1.5, 1.5]), np.array([np.nan, np.nan]), 'Empty flux model leads to wrong empty flux shape') self.flux_target.flux_freq_MHz = 1.5 - self.assertEqual(self.flux_target.flux_density(), 100.0, 'Flux calculation for default freq wrong') + self.assertEqual(self.flux_target.flux_density(), 200.0, 'Flux calculation for default freq wrong') print(self.flux_target) - unit_model2 = katpoint.FluxDensityModel(100., 200., [0.]) - self.assertEqual(unit_model, unit_model2, 'Flux models not equal') + + def test_flux_density_stokes(self): + """Test flux density calculation for Stokes parameters""" + np.testing.assert_array_equal(self.flux_model.flux_density_stokes(1.5), + np.array([200.0, 50.0, 25.0, -75.0])) + np.testing.assert_array_equal(self.flux_model.flux_density_stokes([1.0, 1.5, 3.0]), + np.array([[200.0, 50.0, 25.0, -75.0], + [200.0, 50.0, 25.0, -75.0], + [np.nan, np.nan, np.nan, np.nan]])) + self.assertRaises(ValueError, self.no_flux_target.flux_density_stokes) + np.testing.assert_array_equal(self.no_flux_target.flux_density_stokes(1.5), + np.array([np.nan, np.nan, np.nan, np.nan]), + 'Empty flux model leads to wrong empty flux shape') + np.testing.assert_array_equal(self.no_flux_target.flux_density_stokes([1.5, 1.5]), + np.array([[np.nan, np.nan, np.nan, np.nan], + [np.nan, np.nan, np.nan, np.nan]]), + 'Empty flux model leads to wrong empty flux shape') + self.flux_target.flux_freq_MHz = 1.5 + np.testing.assert_array_equal(self.flux_target.flux_density_stokes(), + np.array([200.0, 50.0, 25.0, -75.0]), + 'Flux calculation for default freq wrong') + + def test_compare(self): + self.assertEqual(self.unit_model, self.unit_model2, 'Flux models not equal') + + def test_hash(self): try: - self.assertEqual(hash(unit_model), hash(unit_model2), 'Flux model hashes not equal') + self.assertEqual(hash(self.unit_model), hash(self.unit_model2), 'Flux model hashes not equal') except TypeError: self.fail('FluxDensityModel object not hashable') diff -Nru katpoint-0.7/katpoint/test/test_model.py katpoint-0.9/katpoint/test/test_model.py --- katpoint-0.7/katpoint/test/test_model.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_model.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,6 +15,7 @@ ################################################################################ """Tests for the model module.""" +from __future__ import print_function, division, absolute_import import unittest try: @@ -82,6 +83,7 @@ m7 = katpoint.Model(self.new_params()) m7.set(m) self.assertEqual(m, m7, 'Construction from model object failed') + class OtherModel(katpoint.Model): pass m8 = OtherModel(self.new_params()) @@ -102,10 +104,3 @@ self.assertEqual(list(m.values()), values, 'Parameter values do not match') m['NIAO'] = 6789.0 self.assertEqual(m['NIAO'], 6789.0, 'Parameter setting via dict interface failed') - - def test_writable_docstring(self): - """Check that model class docstring is writable.""" - params = self.new_params() - m = katpoint.Model(params) - katpoint.Model.__doc__ = 'Hooray' - self.assertEqual(katpoint.Model.__doc__, 'Hooray', 'Class docstring not writable') diff -Nru katpoint-0.7/katpoint/test/test_pointing.py katpoint-0.9/katpoint/test/test_pointing.py --- katpoint-0.7/katpoint/test/test_pointing.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_pointing.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,7 +15,7 @@ ################################################################################ """Tests for the pointing module.""" -# pylint: disable-msg=C0103,W0212 +from __future__ import print_function, division, absolute_import import unittest @@ -23,8 +23,10 @@ import katpoint + def assert_angles_almost_equal(x, y, **kwargs): - primary_angle = lambda x: x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi + def primary_angle(x): + return x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi np.testing.assert_almost_equal(primary_angle(x - y), np.zeros(np.shape(x)), **kwargs) @@ -81,7 +83,6 @@ # Comment out these removes, thereby testing more code paths in PointingModel # enabled_params.remove(2) # enabled_params.remove(10) - # pylint: disable-msg=W0612 fitted_params, sigma_params = pm.fit(self.az, self.el, delta_az, delta_el, enabled_params=[]) np.testing.assert_equal(fitted_params, np.zeros(self.num_params)) fitted_params, sigma_params = pm.fit(self.az, self.el, delta_az, delta_el, enabled_params=enabled_params) diff -Nru katpoint-0.7/katpoint/test/test_projection.py katpoint-0.9/katpoint/test/test_projection.py --- katpoint-0.7/katpoint/test/test_projection.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_projection.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,7 +15,7 @@ ################################################################################ """Tests for the projection module.""" -# pylint: disable-msg=C0103,W0212 +from __future__ import print_function, division, absolute_import import unittest @@ -29,6 +29,7 @@ except ImportError: found_aips = False + def skip(reason=''): """Use nose to skip a test.""" try: @@ -39,7 +40,8 @@ def assert_angles_almost_equal(x, y, decimal): - primary_angle = lambda x: x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi + def primary_angle(x): + return x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi np.testing.assert_almost_equal(primary_angle(x - y), np.zeros(np.shape(x)), decimal=decimal) @@ -79,10 +81,10 @@ az_aips, el_aips = np.zeros(az.shape), np.zeros(el.shape) x_aips, y_aips = np.zeros(xx.shape), np.zeros(yy.shape) for n in range(len(az)): - az_aips[n], el_aips[n], ierr = \ - newpos(2, self.az0[n], self.el0[n], self.x[n], self.y[n]) - x_aips[n], y_aips[n], ierr = \ - dircos(2, self.az0[n], self.el0[n], az[n], el[n]) + az_aips[n], el_aips[n], ierr = newpos( + 2, self.az0[n], self.el0[n], self.x[n], self.y[n]) + x_aips[n], y_aips[n], ierr = dircos( + 2, self.az0[n], self.el0[n], az[n], el[n]) self.assertEqual(ierr, 0) assert_angles_almost_equal(az, az_aips, decimal=9) assert_angles_almost_equal(el, el_aips, decimal=9) @@ -185,10 +187,10 @@ az_aips, el_aips = np.zeros(az.shape), np.zeros(el.shape) x_aips, y_aips = np.zeros(xx.shape), np.zeros(yy.shape) for n in range(len(az)): - az_aips[n], el_aips[n], ierr = \ - newpos(3, az0[n], el0[n], x[n], y[n]) - x_aips[n], y_aips[n], ierr = \ - dircos(3, az0[n], el0[n], az[n], el[n]) + az_aips[n], el_aips[n], ierr = newpos( + 3, az0[n], el0[n], x[n], y[n]) + x_aips[n], y_aips[n], ierr = dircos( + 3, az0[n], el0[n], az[n], el[n]) self.assertEqual(ierr, 0) assert_angles_almost_equal(az, az_aips, decimal=10) assert_angles_almost_equal(el, el_aips, decimal=10) @@ -284,10 +286,10 @@ az_aips, el_aips = np.zeros(az.shape), np.zeros(el.shape) x_aips, y_aips = np.zeros(xx.shape), np.zeros(yy.shape) for n in range(len(az)): - az_aips[n], el_aips[n], ierr = \ - newpos(4, self.az0[n], self.el0[n], self.x[n], self.y[n]) - x_aips[n], y_aips[n], ierr = \ - dircos(4, self.az0[n], self.el0[n], az[n], el[n]) + az_aips[n], el_aips[n], ierr = newpos( + 4, self.az0[n], self.el0[n], self.x[n], self.y[n]) + x_aips[n], y_aips[n], ierr = dircos( + 4, self.az0[n], self.el0[n], az[n], el[n]) self.assertEqual(ierr, 0) assert_angles_almost_equal(az, az_aips, decimal=8) assert_angles_almost_equal(el, el_aips, decimal=8) @@ -398,10 +400,10 @@ az_aips, el_aips = np.zeros(az.shape), np.zeros(el.shape) x_aips, y_aips = np.zeros(xx.shape), np.zeros(yy.shape) for n in range(len(az)): - az_aips[n], el_aips[n], ierr = \ - newpos(6, self.az0[n], self.el0[n], self.x[n], self.y[n]) - x_aips[n], y_aips[n], ierr = \ - dircos(6, self.az0[n], self.el0[n], az[n], el[n]) + az_aips[n], el_aips[n], ierr = newpos( + 6, self.az0[n], self.el0[n], self.x[n], self.y[n]) + x_aips[n], y_aips[n], ierr = dircos( + 6, self.az0[n], self.el0[n], az[n], el[n]) self.assertEqual(ierr, 0) # AIPS NEWPOS STG has poor accuracy on azimuth angle (large closure errors by itself) # assert_angles_almost_equal(az, az_aips, decimal=9) @@ -485,22 +487,21 @@ assert_angles_almost_equal(el, ee, decimal=12) -def sphere_to_plane_mattieu(targetaz,targetel,scanaz,scanel): - #produces direction cosine coordinates from scanning antenna azimuth,elevation coordinates - #see _coordinate options.py for derivation - ll=np.cos(targetel)*np.sin(targetaz-scanaz) - mm=np.cos(targetel)*np.sin(scanel)*np.cos(targetaz-scanaz)-np.cos(scanel)*np.sin(targetel) - return ll,mm - -def plane_to_sphere_mattieu(targetaz,targetel,ll,mm): - scanaz=targetaz-np.arcsin(np.clip(ll/np.cos(targetel),-1.0,1.0)) - scanel=np.arcsin(np.clip((np.sqrt(1.0-ll**2-mm**2)*np.sin(targetel)+np.sqrt(np.cos(targetel)**2-ll**2)*mm)/(1.0-ll**2),-1.0,1.0)) - #alternate equations which gives same result - # scanel_alternate1=np.arcsin((np.sqrt(1.0-ll**2-mm**2)*np.sin(targetel)+np.cos(targetel)*np.cos(targetaz-scanaz)*mm)/(1.0-ll**2)) - # num=np.cos(targetel)*np.cos(targetaz-scanaz)#or num=np.sqrt(np.cos(targetel)**2-ll**2) - # den=np.sin(targetel)**2+num**2 - # scanel_alternate2=np.arcsin((np.sqrt(((den-mm**2)*(den-num**2)))+num*mm)/den) - return scanaz,scanel +def sphere_to_plane_original_ssn(target_az, target_el, scan_az, scan_el): + """Mattieu's original version of SSN projection.""" + ll = np.cos(target_el) * np.sin(target_az - scan_az) + mm = np.cos(target_el) * np.sin(scan_el) * np.cos( + target_az - scan_az) - np.cos(scan_el) * np.sin(target_el) + return ll, mm + + +def plane_to_sphere_original_ssn(target_az, target_el, ll, mm): + """Mattieu's original version of SSN projection.""" + scan_az = target_az - np.arcsin(np.clip(ll / np.cos(target_el), -1.0, 1.0)) + scan_el = np.arcsin(np.clip( + (np.sqrt(1.0 - ll**2 - mm**2) * np.sin(target_el) + + np.sqrt(np.cos(target_el)**2 - ll**2) * mm) / (1.0 - ll**2), -1.0, 1.0)) + return scan_az, scan_el class TestProjectionSSN(unittest.TestCase): @@ -532,11 +533,11 @@ assert_angles_almost_equal(az, aa, decimal=10) assert_angles_almost_equal(el, ee, decimal=10) - def test_vs_mattieu(self): + def test_vs_original_ssn(self): """SSN projection: compare against Mattieu's original version.""" az, el = self.plane_to_sphere(self.az0, self.el0, self.x, self.y) - ll, mm = sphere_to_plane_mattieu(self.az0, self.el0, az, el) - aa, ee = plane_to_sphere_mattieu(self.az0, self.el0, ll, mm) + ll, mm = sphere_to_plane_original_ssn(self.az0, self.el0, az, el) + aa, ee = plane_to_sphere_original_ssn(self.az0, self.el0, ll, mm) np.testing.assert_almost_equal(self.x, ll, decimal=10) np.testing.assert_almost_equal(self.y, -mm, decimal=10) assert_angles_almost_equal(az, aa, decimal=10) diff -Nru katpoint-0.7/katpoint/test/test_refraction.py katpoint-0.9/katpoint/test/test_refraction.py --- katpoint-0.7/katpoint/test/test_refraction.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_refraction.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,7 +15,7 @@ ################################################################################ """Tests for the refraction module.""" -# pylint: disable-msg=C0103,W0212 +from __future__ import print_function, division, absolute_import import unittest @@ -23,8 +23,10 @@ import katpoint + def assert_angles_almost_equal(x, y, **kwargs): - primary_angle = lambda x: x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi + def primary_angle(x): + return x - np.round(x / (2.0 * np.pi)) * 2.0 * np.pi np.testing.assert_almost_equal(primary_angle(x - y), np.zeros(np.shape(x)), **kwargs) diff -Nru katpoint-0.7/katpoint/test/test_target.py katpoint-0.9/katpoint/test/test_target.py --- katpoint-0.7/katpoint/test/test_target.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_target.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,15 +15,11 @@ ################################################################################ """Tests for the target module.""" -# pylint: disable-msg=C0103,W0212 +from __future__ import print_function, division, absolute_import import unittest import time - -try: - import cPickle as pickle # python2 -except ImportError: - import pickle # python3 +import pickle import numpy as np @@ -32,6 +28,7 @@ # Use the current year in TLE epochs to avoid pyephem crash due to expired TLEs YY = time.localtime().tm_year % 100 + class TestTargetConstruction(unittest.TestCase): """Test construction of targets from strings and vice versa.""" def setUp(self): @@ -60,7 +57,7 @@ 'Betelgeuse | Maitland, star orion', 'xephem star, Sadr~f|S|F8~20:22:13.7|2.43~40:15:24|-0.93~2.23~2000~0', 'Acamar | Theta Eridani, xephem, HIC 13847~f|S|A4~2:58:16.03~-40:18:17.1~2.906~2000~0', - 'Kakkab | Alpha Lupi, xephem, HIC 71860 | SAO 225128~f|S|B1~14:41:55.768~-47:23:17.51~2.304~2000~0'] + 'Kakkab | A Lupi, xephem, H71860 | S225128~f|S|B1~14:41:55.768~-47:23:17.51~2.304~2000~0'] self.invalid_targets = ['Sun', 'Sun, ', '-30.0, 90.0', @@ -159,6 +156,7 @@ tag_target.add_tags(['SNR', 'GPS']) self.assertEqual(tag_target.tags, ['azel', 'J2000', 'GPS', 'pulsar', 'SNR'], 'Added tags not correct') + class TestTargetCalculations(unittest.TestCase): """Test various calculations involving antennas and timestamps.""" def setUp(self): @@ -166,6 +164,7 @@ self.ant1 = katpoint.Antenna('A1, -31.0, 18.0, 0.0, 12.0, 0.0 0.0 0.0') self.ant2 = katpoint.Antenna('A2, -31.0, 18.0, 0.0, 12.0, 10.0 -10.0 0.0') self.ts = katpoint.Timestamp('2013-08-14 08:25') + self.uvw = [10.822861713680807, -9.103057965680664, -2.220446049250313e-16] def test_coords(self): """Test coordinate conversions for coverage.""" @@ -187,16 +186,51 @@ def test_uvw(self): """Test uvw calculation.""" u, v, w = self.target.uvw(self.ant2, self.ts, self.ant1) - np.testing.assert_almost_equal(u, 10.821750916197391, decimal=5) - np.testing.assert_almost_equal(v, -9.1043784587765906, decimal=5) - np.testing.assert_almost_equal(w, 4.7781625336985198e-10, decimal=5) + np.testing.assert_almost_equal(u, self.uvw[0], decimal=5) + np.testing.assert_almost_equal(v, self.uvw[1], decimal=5) + np.testing.assert_almost_equal(w, self.uvw[2], decimal=5) - def test_uvw_array(self): + def test_uvw_timestamp_array(self): """Test uvw calculation on an array.""" u, v, w = self.target.uvw(self.ant2, np.array([self.ts, self.ts]), self.ant1) - np.testing.assert_array_almost_equal(u, np.array([10.821750916197391] * 2), decimal=5) - np.testing.assert_array_almost_equal(v, np.array([-9.1043784587765906] * 2), decimal=5) - np.testing.assert_array_almost_equal(w, np.array([4.7781625336985198e-10] * 2), decimal=5) + np.testing.assert_array_almost_equal(u, np.array([self.uvw[0]] * 2), decimal=5) + np.testing.assert_array_almost_equal(v, np.array([self.uvw[1]] * 2), decimal=5) + np.testing.assert_array_almost_equal(w, np.array([self.uvw[2]] * 2), decimal=5) + + def test_uvw_timestamp_array_radec(self): + """Test uvw calculation on a timestamp array when the target is a radec target.""" + ra, dec = self.target.radec(self.ts, self.ant1) + target = katpoint.construct_radec_target(ra, dec) + u, v, w = target.uvw(self.ant2, np.array([self.ts, self.ts]), self.ant1) + np.testing.assert_array_almost_equal(u, np.array([self.uvw[0]] * 2), decimal=5) + np.testing.assert_array_almost_equal(v, np.array([self.uvw[1]] * 2), decimal=5) + np.testing.assert_array_almost_equal(w, np.array([self.uvw[2]] * 2), decimal=5) + + def test_uvw_antenna_array(self): + u, v, w = self.target.uvw([self.ant1, self.ant2], self.ts, self.ant1) + np.testing.assert_array_almost_equal(u, np.array([0, self.uvw[0]]), decimal=5) + np.testing.assert_array_almost_equal(v, np.array([0, self.uvw[1]]), decimal=5) + np.testing.assert_array_almost_equal(w, np.array([0, self.uvw[2]]), decimal=5) + + def test_uvw_both_array(self): + u, v, w = self.target.uvw([self.ant1, self.ant2], [self.ts, self.ts], self.ant1) + np.testing.assert_array_almost_equal(u, np.array([[0, self.uvw[0]]] * 2), decimal=5) + np.testing.assert_array_almost_equal(v, np.array([[0, self.uvw[1]]] * 2), decimal=5) + np.testing.assert_array_almost_equal(w, np.array([[0, self.uvw[2]]] * 2), decimal=5) + + def test_uvw_hemispheres(self): + """Test uvw calculation near the equator. + + The implementation behaves differently depending on the sign of + declination. This test is to catch sign flip errors. + """ + target1 = katpoint.construct_radec_target(0.0, -1e-9) + target2 = katpoint.construct_radec_target(0.0, +1e-9) + u1, v1, w1 = target1.uvw(self.ant2, self.ts, self.ant1) + u2, v2, w2 = target2.uvw(self.ant2, self.ts, self.ant1) + np.testing.assert_almost_equal(u1, u2, decimal=3) + np.testing.assert_almost_equal(v1, v2, decimal=3) + np.testing.assert_almost_equal(w1, w2, decimal=3) def test_lmn(self): """Test lmn calculation.""" diff -Nru katpoint-0.7/katpoint/test/test_timestamp.py katpoint-0.9/katpoint/test/test_timestamp.py --- katpoint-0.7/katpoint/test/test_timestamp.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/test/test_timestamp.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,7 +15,7 @@ ################################################################################ """Tests for the timestamp module.""" -# pylint: disable-msg=C0103,W0212 +from __future__ import print_function, division, absolute_import import unittest @@ -23,6 +23,7 @@ import katpoint + class TestTimestamp(unittest.TestCase): """Test timestamp creation and conversion.""" def setUp(self): diff -Nru katpoint-0.7/katpoint/timestamp.py katpoint-0.9/katpoint/timestamp.py --- katpoint-0.7/katpoint/timestamp.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/katpoint/timestamp.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -15,6 +15,9 @@ ################################################################################ """A Timestamp object.""" +from __future__ import print_function, division, absolute_import +from builtins import object +from past.builtins import basestring import time import math @@ -24,8 +27,6 @@ import numpy as np import ephem -from past.builtins import basestring - @total_ordering class Timestamp(object): diff -Nru katpoint-0.7/LICENSE.txt katpoint-0.9/LICENSE.txt --- katpoint-0.7/LICENSE.txt 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/LICENSE.txt 2019-10-02 09:50:21.000000000 +0000 @@ -1,4 +1,4 @@ -Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff -Nru katpoint-0.7/NEWS.rst katpoint-0.9/NEWS.rst --- katpoint-0.7/NEWS.rst 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/NEWS.rst 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,32 @@ +History +======= + +0.9 (2019-10-02) +---------------- +* Add Antenna.array_reference_antenna utility function (#51) +* Vectorise Target.uvw (#49) +* Improve precision of flux model description string (#52) +* Produce documentation on readthedocs.org (#48) +* Add script that converts PSRCAT database into Catalogue (#16) + +0.8 (2019-02-12) +---------------- +* Improve UVW coordinates by using local and not global North (#46) +* Allow different target with same name in Catalogue (#44) +* Add support for polarisation in flux density models (#38) +* Fix tab completion in Catalogue (#39) +* More Python 3 and flake8 cleanup (#43) +* The GitHub repository is now public as well + +0.7 (2017-08-01) +---------------- +* Support Python 3 (#36) +* Improve DelayCorrection, adding description string and offset (#37) + +0.6.1 (2017-07-20) +------------------ +* Resolve issue with ska-sa/katdal#85 - SensorData rework (#34) + +0.6 (2016-09-16) +---------------- +* Initial release of katpoint diff -Nru katpoint-0.7/.readthedocs.yml katpoint-0.9/.readthedocs.yml --- katpoint-0.7/.readthedocs.yml 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/.readthedocs.yml 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,7 @@ +version: 2 +python: + version: 3.6 + install: + - requirements: doc-requirements.txt + - method: pip + path: . diff -Nru katpoint-0.7/scripts/atca_calibrators.py katpoint-0.9/scripts/atca_calibrators.py --- katpoint-0.7/scripts/atca_calibrators.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/scripts/atca_calibrators.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,7 +1,7 @@ #! /usr/bin/python ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy diff -Nru katpoint-0.7/scripts/bae_optical_pointing_sources.py katpoint-0.9/scripts/bae_optical_pointing_sources.py --- katpoint-0.7/scripts/bae_optical_pointing_sources.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/scripts/bae_optical_pointing_sources.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,7 +1,7 @@ #! /usr/bin/python ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy diff -Nru katpoint-0.7/scripts/kuehr1Jy_sources.py katpoint-0.9/scripts/kuehr1Jy_sources.py --- katpoint-0.7/scripts/kuehr1Jy_sources.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/scripts/kuehr1Jy_sources.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,7 +1,7 @@ #! /usr/bin/python ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy diff -Nru katpoint-0.7/scripts/merge_catalogues.py katpoint-0.9/scripts/merge_catalogues.py --- katpoint-0.7/scripts/merge_catalogues.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/scripts/merge_catalogues.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -35,7 +35,8 @@ for n, src in enumerate(old): names = [src.name] + src.aliases - print('OLD: %s %s' % (names, ('%.1f Jy' % (src.flux_density(freq),)) if not np.isnan(src.flux_density(freq)) else '')) + print('OLD: %s %s' % (names, ('%.1f Jy' % (src.flux_density(freq),)) + if not np.isnan(src.flux_density(freq)) else '')) print(src.description) plt.subplot(5, 6, n + 1) plt.plot(np.log10(freq_range), np.log10(src.flux_density(freq_range)), 'b') diff -Nru katpoint-0.7/scripts/pkscat90_sources.py katpoint-0.9/scripts/pkscat90_sources.py --- katpoint-0.7/scripts/pkscat90_sources.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/scripts/pkscat90_sources.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,7 +1,7 @@ #! /usr/bin/python ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy diff -Nru katpoint-0.7/scripts/psrcat_sources.py katpoint-0.9/scripts/psrcat_sources.py --- katpoint-0.7/scripts/psrcat_sources.py 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/scripts/psrcat_sources.py 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,92 @@ +#/usr/bin/env python +# +# Tool that converts the ATNF PSRCAT database into a katpoint Catalogue. +# +# This needs the psrcat.db file included with the PSRCAT package which can be +# downloaded from http://www.atnf.csiro.au/people/pulsar/psrcat/download.html. +# +# The default psrcat.db contains both (ra, dec) and ecliptic (lon, lat) +# coordinates while this script only handles the former. You therefore need +# to run PSRCAT on the basic file to produce a 'long' ephemeris file: +# +# psrcat -db_file psrcat.db -e2 > psrcat_full.db +# +# The output catalogue is printed to stdout, which can be redirected to a file. +# +# Ludwig Schwardt +# 31 October 2014 +# + +import argparse +import re + +import numpy as np + +import katpoint + + +parser = argparse.ArgumentParser() +parser.add_argument('-f', '--db-file', + help='Path to long version of psrcat.db file') +args = parser.parse_args() + +if not args.db_file: + raise RuntimeError("Please obtain a long ephemeris file from the PSRCAT package") + +# Regexp that finds key-value pair associated with pulsar +key_val = re.compile('^([A-Z0-9_]+)\s+(\S+)') +# Regexp that finds SNR name associated with pulsar +snr = re.compile('SNR:(PWN:)*(.+?)[[(,;$]') +# Regexp that finds single flux measurement associated with pulsar +flux_bin = re.compile('^S(\d{3,4})$') + +# Turn database file into list of dicts, one per pulsar +pulsars = [] +psr = {} +for line in file(args.db_file): + if line.startswith('#'): + continue + if line.startswith('@'): + pulsars.append(psr) + psr = {} + continue + kv_match = key_val.match(line) + if not kv_match: + continue + key, val = kv_match.groups() + psr[key] = val +if psr: + pulsars.append(psr) + +for psr in pulsars: + names = psr['PSRJ'] + if 'PSRB' in psr: + names += ' | *' + psr['PSRB'] + if 'ASSOC' in psr: + snr_match = snr.match(psr['ASSOC']) + if snr_match: + names += ' | ' + snr_match.group(2) + if 'RAJ' in psr: + ra, dec = psr['RAJ'], psr['DECJ'] + else: + raise RuntimeError("Please run 'psrcat -db_file psrcat.db -e2 > psrcat_full.db' to get radecs for each pulsar") + + # Extract all flux measurements of pulsar + flux_matches = [flux_bin.match(k) for k in psr] + freq = np.array([float(m.group(1)) for m in flux_matches if m]) + flux_mJy = np.array([float(psr[m.group(0)]) for m in flux_matches if m]) + if len(freq) == 0: + flux_model = None + else: + # Fit Baars 1977 polynomial flux model: log10 S[Jy] = a + b*log10(f[MHz]) + c*(log10(f[MHz]))^2 + log_freq = np.log10(freq) + log_flux = np.log10(flux_mJy / 1000.) + order = 2 if len(log_flux) > 3 else 1 if len(log_flux) > 1 else 0 + flux_poly = np.polyfit(log_freq, log_flux, order) + freq_range = [1000., 2000.] + flux_model = katpoint.FluxDensityModel(freq_range[0], freq_range[1], + flux_poly[::-1]) + description = '%s, radec psr, %s, %s' % (names, ra, dec) + if flux_model: + description += ', ' + flux_model.description + print description diff -Nru katpoint-0.7/scripts/tabara_sources.py katpoint-0.9/scripts/tabara_sources.py --- katpoint-0.7/scripts/tabara_sources.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/scripts/tabara_sources.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,7 +1,7 @@ #! /usr/bin/python ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy diff -Nru katpoint-0.7/scripts/validate_targets.py katpoint-0.9/scripts/validate_targets.py --- katpoint-0.7/scripts/validate_targets.py 1970-01-01 00:00:00.000000000 +0000 +++ katpoint-0.9/scripts/validate_targets.py 2019-10-02 09:50:21.000000000 +0000 @@ -0,0 +1,172 @@ +#!/usr/bin/env python +################################################################################ +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) +# +# Licensed under the BSD 3-Clause License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy +# of the License at +# +# https://opensource.org/licenses/BSD-3-Clause +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +from __future__ import print_function + +import argparse +import os +import sys + +import katpoint + + +def find_markers(body, marker): + """ + Find all the markers in body. + + Parameters + ---------- + body : string + Input string to parse. + marker : character + Search parameter + + Returns + ------- + list + Input position where the marker was found. + """ + return [i for i, ch in enumerate(body) if ch == marker] + + +def print_markers(marker_indices): + """ + Show the markers graphically with '^', all other positions are shown with '.'. + + Parameters + ---------- + marker_indices : list + Marker positions. + """ + output = ['.'] * (max(marker_indices) + 1) + for pos in marker_indices: + output[pos] = '^' + output = ''.join(output) + + print(output) + + +def show_separators(body, separator, exception): + """ + Show the separators graphically. + + Parameters + ---------- + body : string + Input string to parse. + separator : character + Search parameter + exception : string + Exception error. + """ + print('\nPotential invalid separator - {}!'.format(exception)) + print(body) + print_markers(find_markers(body, separator)) + + +def show_non_ascii(body): + """ + Show the non-ASCII graphically. + + Parameters + ---------- + body : string + Input string to parse. + """ + # Force body string to non-ASCII + target_uni = body.decode('utf-8') + # Now convert to ASCII and mark all non-ASCII with '?' + target_uni = target_uni.encode('ascii', errors='replace') + print('\nNon ASCII characters found!') + print(target_uni) + print_markers(find_markers(target_uni, '?')) + + +def validate_target(target_file): + """ + Validate all target strings from the supplied CSV file using katpoint.Target. + + Non-Ascii characters are shown as '?' and their positions are shown graphically. + All separators are shown graphically if the maximum field count is exceeded. + + Parameters + ---------- + target_file : string + A CSV file with multiple target strings + + Returns + ------- + target_validation_pass : bool + Target validation status. + """ + target_validation_pass = True + with open(target_file, 'r') as csv_file: + for line in csv_file: + if not line.strip().startswith('#') and (len(line.strip()) != 0): + try: + katpoint.Target(line) + except katpoint.NonAsciiError: + show_non_ascii(line) + target_validation_pass = False + except katpoint.FluxError as exception: + show_separators(line, ',', exception) + target_validation_pass = False + except ValueError as exception: + if line.strip() in str(exception): + print("\nParsing Error!\n{}".format(exception)) + else: + print("\nParsing Error!\n{}\n{}".format(line, exception)) + target_validation_pass = False + + return target_validation_pass + + +def parse_cmd_line(): + """ + Parse the script command-line arguments. + + Returns + ------- + config : argparse.Namespace + Command-line arguments + """ + parser = argparse.ArgumentParser( + description=""" + Validate all target strings from the supplied CSV file using + katpoint.Target. + """) + parser.add_argument( + 'filename', help="CSV file with list of target strings") + config = parser.parse_args() + + if not os.path.exists(config.filename): + parser.error("\nFile {} does not exist!\n".format( + config.filename)) + + return config + + +def main(): + config = parse_cmd_line() + if validate_target(config.filename): + print("No errors found in {}".format(config.filename)) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff -Nru katpoint-0.7/setup.cfg katpoint-0.9/setup.cfg --- katpoint-0.7/setup.cfg 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/setup.cfg 2019-10-02 09:50:21.000000000 +0000 @@ -1,2 +1,5 @@ [bdist_wheel] universal = 1 + +[flake8] +max-line-length = 120 diff -Nru katpoint-0.7/setup.py katpoint-0.9/setup.py --- katpoint-0.7/setup.py 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/setup.py 2019-10-02 09:50:21.000000000 +0000 @@ -1,7 +1,7 @@ #!/usr/bin/env python ################################################################################ -# Copyright (c) 2009-2016, National Research Foundation (Square Kilometre Array) +# Copyright (c) 2009-2019, National Research Foundation (Square Kilometre Array) # # Licensed under the BSD 3-Clause License (the "License"); you may not use # this file except in compliance with the License. You may obtain a copy @@ -16,11 +16,15 @@ # limitations under the License. ################################################################################ +import os.path + from setuptools import setup, find_packages -with open('README.rst') as readme: - long_description = readme.read() +here = os.path.dirname(__file__) +readme = open(os.path.join(here, 'README.rst')).read() +news = open(os.path.join(here, 'NEWS.rst')).read() +long_description = readme + '\n\n' + news setup(name="katpoint", description="Karoo Array Telescope pointing coordinate library", @@ -29,7 +33,7 @@ author_email="ludwig@ska.ac.za", packages=find_packages(), url='https://github.com/ska-sa/katpoint', - license="BSD", + license="Modified BSD", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -43,6 +47,7 @@ "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Scientific/Engineering :: Astronomy"], platforms=["OS Independent"], diff -Nru katpoint-0.7/test-requirements.txt katpoint-0.9/test-requirements.txt --- katpoint-0.7/test-requirements.txt 2017-11-07 13:50:46.000000000 +0000 +++ katpoint-0.9/test-requirements.txt 2019-10-02 09:50:21.000000000 +0000 @@ -1,2 +1,7 @@ +argparse # via unittest2 nose coverage +linecache2 # via traceback2 +six # via unittest2 +traceback2 # via unittest2 +unittest2