APLpy-1.0/0000755000077000000240000000000012471104150012250 5ustar tomstaff00000000000000APLpy-1.0/ah_bootstrap.py0000644000077000000240000007004212471065347015330 0ustar tomstaff00000000000000""" This bootstrap module contains code for ensuring that the astropy_helpers package will be importable by the time the setup.py script runs. It also includes some workarounds to ensure that a recent-enough version of setuptools is being used for the installation. This module should be the first thing imported in the setup.py of distributions that make use of the utilities in astropy_helpers. If the distribution ships with its own copy of astropy_helpers, this module will first attempt to import from the shipped copy. However, it will also check PyPI to see if there are any bug-fix releases on top of the current version that may be useful to get past platform-specific bugs that have been fixed. When running setup.py, use the ``--offline`` command-line option to disable the auto-upgrade checks. When this module is imported or otherwise executed it automatically calls a main function that attempts to read the project's setup.cfg file, which it checks for a configuration section called ``[ah_bootstrap]`` the presences of that section, and options therein, determine the next step taken: If it contains an option called ``auto_use`` with a value of ``True``, it will automatically call the main function of this module called `use_astropy_helpers` (see that function's docstring for full details). Otherwise no further action is taken (however, ``ah_bootstrap.use_astropy_helpers`` may be called manually from within the setup.py script). Additional options in the ``[ah_boostrap]`` section of setup.cfg have the same names as the arguments to `use_astropy_helpers`, and can be used to configure the bootstrap script when ``auto_use = True``. See https://github.com/astropy/astropy-helpers for more details, and for the latest version of this module. """ import contextlib import errno import imp import io import locale import os import re import subprocess as sp import sys try: from ConfigParser import ConfigParser, RawConfigParser except ImportError: from configparser import ConfigParser, RawConfigParser if sys.version_info[0] < 3: _str_types = (str, unicode) _text_type = unicode PY3 = False else: _str_types = (str, bytes) _text_type = str PY3 = True # Some pre-setuptools checks to ensure that either distribute or setuptools >= # 0.7 is used (over pre-distribute setuptools) if it is available on the path; # otherwise the latest setuptools will be downloaded and bootstrapped with # ``ez_setup.py``. This used to be included in a separate file called # setuptools_bootstrap.py; but it was combined into ah_bootstrap.py try: import pkg_resources _setuptools_req = pkg_resources.Requirement.parse('setuptools>=0.7') # This may raise a DistributionNotFound in which case no version of # setuptools or distribute is properly installed _setuptools = pkg_resources.get_distribution('setuptools') if _setuptools not in _setuptools_req: # Older version of setuptools; check if we have distribute; again if # this results in DistributionNotFound we want to give up _distribute = pkg_resources.get_distribution('distribute') if _setuptools != _distribute: # It's possible on some pathological systems to have an old version # of setuptools and distribute on sys.path simultaneously; make # sure distribute is the one that's used sys.path.insert(1, _distribute.location) _distribute.activate() imp.reload(pkg_resources) except: # There are several types of exceptions that can occur here; if all else # fails bootstrap and use the bootstrapped version from ez_setup import use_setuptools use_setuptools() from distutils import log from distutils.debug import DEBUG # In case it didn't successfully import before the ez_setup checks import pkg_resources from setuptools import Distribution from setuptools.package_index import PackageIndex from setuptools.sandbox import run_setup # Note: The following import is required as a workaround to # https://github.com/astropy/astropy-helpers/issues/89; if we don't import this # module now, it will get cleaned up after `run_setup` is called, but that will # later cause the TemporaryDirectory class defined in it to stop working when # used later on by setuptools try: import setuptools.py31compat except ImportError: pass # TODO: Maybe enable checking for a specific version of astropy_helpers? DIST_NAME = 'astropy-helpers' PACKAGE_NAME = 'astropy_helpers' # Defaults for other options DOWNLOAD_IF_NEEDED = True INDEX_URL = 'https://pypi.python.org/simple' USE_GIT = True AUTO_UPGRADE = True def use_astropy_helpers(path=None, download_if_needed=None, index_url=None, use_git=None, auto_upgrade=None): """ Ensure that the `astropy_helpers` module is available and is importable. This supports automatic submodule initialization if astropy_helpers is included in a project as a git submodule, or will download it from PyPI if necessary. Parameters ---------- path : str or None, optional A filesystem path relative to the root of the project's source code that should be added to `sys.path` so that `astropy_helpers` can be imported from that path. If the path is a git submodule it will automatically be initialzed and/or updated. The path may also be to a ``.tar.gz`` archive of the astropy_helpers source distribution. In this case the archive is automatically unpacked and made temporarily available on `sys.path` as a ``.egg`` archive. If `None` skip straight to downloading. download_if_needed : bool, optional If the provided filesystem path is not found an attempt will be made to download astropy_helpers from PyPI. It will then be made temporarily available on `sys.path` as a ``.egg`` archive (using the ``setup_requires`` feature of setuptools. If the ``--offline`` option is given at the command line the value of this argument is overridden to `False`. index_url : str, optional If provided, use a different URL for the Python package index than the main PyPI server. use_git : bool, optional If `False` no git commands will be used--this effectively disables support for git submodules. If the ``--no-git`` option is given at the command line the value of this argument is overridden to `False`. auto_upgrade : bool, optional By default, when installing a package from a non-development source distribution ah_boostrap will try to automatically check for patch releases to astropy-helpers on PyPI and use the patched version over any bundled versions. Setting this to `False` will disable that functionality. If the ``--offline`` option is given at the command line the value of this argument is overridden to `False`. """ # True by default, unless the --offline option was provided on the command # line if '--offline' in sys.argv: download_if_needed = False auto_upgrade = False offline = True sys.argv.remove('--offline') else: offline = False if '--no-git' in sys.argv: use_git = False sys.argv.remove('--no-git') if path is None: path = PACKAGE_NAME if download_if_needed is None: download_if_needed = DOWNLOAD_IF_NEEDED if index_url is None: index_url = INDEX_URL # If this is a release then the .git directory will not exist so we # should not use git. git_dir_exists = os.path.exists(os.path.join(os.path.dirname(__file__), '.git')) if use_git is None and not git_dir_exists: use_git = False if use_git is None: use_git = USE_GIT if auto_upgrade is None: auto_upgrade = AUTO_UPGRADE # Declared as False by default--later we check if astropy-helpers can be # upgraded from PyPI, but only if not using a source distribution (as in # the case of import from a git submodule) is_submodule = False if not isinstance(path, _str_types): if path is not None: raise TypeError('path must be a string or None') if not download_if_needed: log.debug('a path was not given and download from PyPI was not ' 'allowed so this is effectively a no-op') return elif not os.path.exists(path) or os.path.isdir(path): # Even if the given path does not exist on the filesystem, if it *is* a # submodule, `git submodule init` will create it is_submodule = _check_submodule(path, use_git=use_git, offline=offline) if is_submodule or os.path.isdir(path): log.info( 'Attempting to import astropy_helpers from {0} {1!r}'.format( 'submodule' if is_submodule else 'directory', path)) dist = _directory_import(path) else: dist = None if dist is None: msg = ( 'The requested path {0!r} for importing {1} does not ' 'exist, or does not contain a copy of the {1} package. ' 'Attempting download instead.'.format(path, PACKAGE_NAME)) if download_if_needed: log.warn(msg) else: raise _AHBootstrapSystemExit(msg) elif os.path.isfile(path): # Handle importing from a source archive; this also uses setup_requires # but points easy_install directly to the source archive try: dist = _do_download(find_links=[path]) except Exception as e: if download_if_needed: log.warn('{0}\nWill attempt to download astropy_helpers from ' 'PyPI instead.'.format(str(e))) dist = None else: raise _AHBootstrapSystemExit(e.args[0]) else: msg = ('{0!r} is not a valid file or directory (it could be a ' 'symlink?)'.format(path)) if download_if_needed: log.warn(msg) dist = None else: raise _AHBootstrapSystemExit(msg) if dist is not None and auto_upgrade and not is_submodule: # A version of astropy-helpers was found on the available path, but # check to see if a bugfix release is available on PyPI upgrade = _do_upgrade(dist, index_url) if upgrade is not None: dist = upgrade elif dist is None: # Last resort--go ahead and try to download the latest version from # PyPI try: if download_if_needed: log.warn( "Downloading astropy_helpers; run setup.py with the " "--offline option to force offline installation.") dist = _do_download(index_url=index_url) else: raise _AHBootstrapSystemExit( "No source for the astropy_helpers package; " "astropy_helpers must be available as a prerequisite to " "installing this package.") except Exception as e: if DEBUG: raise else: raise _AHBootstrapSystemExit(e.args[0]) if dist is not None: # Otherwise we found a version of astropy-helpers so we're done # Just activate the found distribibution on sys.path--if we did a # download this usually happens automatically but do it again just to # be sure # Note: Adding the dist to the global working set also activates it by # default pkg_resources.working_set.add(dist) def _do_download(version='', find_links=None, index_url=None): try: if find_links: allow_hosts = '' index_url = None else: allow_hosts = None # Annoyingly, setuptools will not handle other arguments to # Distribution (such as options) before handling setup_requires, so it # is not straightforward to programmatically augment the arguments which # are passed to easy_install class _Distribution(Distribution): def get_option_dict(self, command_name): opts = Distribution.get_option_dict(self, command_name) if command_name == 'easy_install': if find_links is not None: opts['find_links'] = ('setup script', find_links) if index_url is not None: opts['index_url'] = ('setup script', index_url) if allow_hosts is not None: opts['allow_hosts'] = ('setup script', allow_hosts) return opts if version: req = '{0}=={1}'.format(DIST_NAME, version) else: req = DIST_NAME attrs = {'setup_requires': [req]} if DEBUG: dist = _Distribution(attrs=attrs) else: with _silence(): dist = _Distribution(attrs=attrs) # If the setup_requires succeeded it will have added the new dist to # the main working_set return pkg_resources.working_set.by_key.get(DIST_NAME) except Exception as e: if DEBUG: raise msg = 'Error retrieving astropy helpers from {0}:\n{1}' if find_links: source = find_links[0] elif index_url: source = index_url else: source = 'PyPI' raise Exception(msg.format(source, repr(e))) def _do_upgrade(dist, index_url): # Build up a requirement for a higher bugfix release but a lower minor # release (so API compatibility is guaranteed) # sketchy version parsing--maybe come up with something a bit more # robust for this next_version = _next_version(dist.parsed_version) req = pkg_resources.Requirement.parse( '{0}>{1},<{2}'.format(DIST_NAME, dist.version, next_version)) package_index = PackageIndex(index_url=index_url) upgrade = package_index.obtain(req) if upgrade is not None: return _do_download(version=upgrade.version, index_url=index_url) def _directory_import(path): """ Import astropy_helpers from the given path, which will be added to sys.path. Must return True if the import succeeded, and False otherwise. """ # Return True on success, False on failure but download is allowed, and # otherwise raise SystemExit path = os.path.abspath(path) # Use an empty WorkingSet rather than the man pkg_resources.working_set, # since on older versions of setuptools this will invoke a VersionConflict # when trying to install an upgrade ws = pkg_resources.WorkingSet([]) ws.add_entry(path) dist = ws.by_key.get(DIST_NAME) if dist is None: # We didn't find an egg-info/dist-info in the given path, but if a # setup.py exists we can generate it setup_py = os.path.join(path, 'setup.py') if os.path.isfile(setup_py): with _silence(): run_setup(os.path.join(path, 'setup.py'), ['egg_info']) for dist in pkg_resources.find_distributions(path, True): # There should be only one... return dist return dist def _check_submodule(path, use_git=True, offline=False): """ Check if the given path is a git submodule. See the docstrings for ``_check_submodule_using_git`` and ``_check_submodule_no_git`` for further details. """ if use_git: return _check_submodule_using_git(path, offline) else: return _check_submodule_no_git(path) def _check_submodule_using_git(path, offline): """ Check if the given path is a git submodule. If so, attempt to initialize and/or update the submodule if needed. This function makes calls to the ``git`` command in subprocesses. The ``_check_submodule_no_git`` option uses pure Python to check if the given path looks like a git submodule, but it cannot perform updates. """ if PY3 and not isinstance(path, _text_type): fs_encoding = sys.getfilesystemencoding() path = path.decode(fs_encoding) try: p = sp.Popen(['git', 'submodule', 'status', '--', path], stdout=sp.PIPE, stderr=sp.PIPE) stdout, stderr = p.communicate() except OSError as e: if DEBUG: raise if e.errno == errno.ENOENT: # The git command simply wasn't found; this is most likely the # case on user systems that don't have git and are simply # trying to install the package from PyPI or a source # distribution. Silently ignore this case and simply don't try # to use submodules return False else: raise _AHBoostrapSystemExit( 'An unexpected error occurred when running the ' '`git submodule status` command:\n{0}'.format(str(e))) # Can fail of the default locale is not configured properly. See # https://github.com/astropy/astropy/issues/2749. For the purposes under # consideration 'latin1' is an acceptable fallback. try: stdio_encoding = locale.getdefaultlocale()[1] or 'latin1' except ValueError: # Due to an OSX oddity locale.getdefaultlocale() can also crash # depending on the user's locale/language settings. See: # http://bugs.python.org/issue18378 stdio_encoding = 'latin1' if p.returncode != 0 or stderr: # Unfortunately the return code alone cannot be relied on, as # earlier versions of git returned 0 even if the requested submodule # does not exist stderr = stderr.decode(stdio_encoding) # This is a warning that occurs in perl (from running git submodule) # which only occurs with a malformatted locale setting which can # happen sometimes on OSX. See again # https://github.com/astropy/astropy/issues/2749 perl_warning = ('perl: warning: Falling back to the standard locale ' '("C").') if not stderr.strip().endswith(perl_warning): # Some other uknown error condition occurred log.warn('git submodule command failed ' 'unexpectedly:\n{0}'.format(stderr)) return False stdout = stdout.decode(stdio_encoding) # The stdout should only contain one line--the status of the # requested submodule m = _git_submodule_status_re.match(stdout) if m: # Yes, the path *is* a git submodule _update_submodule(m.group('submodule'), m.group('status'), offline) return True else: log.warn( 'Unexpected output from `git submodule status`:\n{0}\n' 'Will attempt import from {1!r} regardless.'.format( stdout, path)) return False def _check_submodule_no_git(path): """ Like ``_check_submodule_using_git``, but simply parses the .gitmodules file to determine if the supplied path is a git submodule, and does not exec any subprocesses. This can only determine if a path is a submodule--it does not perform updates, etc. This function may need to be updated if the format of the .gitmodules file is changed between git versions. """ gitmodules_path = os.path.abspath('.gitmodules') if not os.path.isfile(gitmodules_path): return False # This is a minimal reader for gitconfig-style files. It handles a few of # the quirks that make gitconfig files incompatible with ConfigParser-style # files, but does not support the full gitconfig syntax (just enough # needed to read a .gitmodules file). gitmodules_fileobj = io.StringIO() # Must use io.open for cross-Python-compatible behavior wrt unicode with io.open(gitmodules_path) as f: for line in f: # gitconfig files are more flexible with leading whitespace; just # go ahead and remove it line = line.lstrip() # comments can start with either # or ; if line and line[0] in (':', ';'): continue gitmodules_fileobj.write(line) gitmodules_fileobj.seek(0) cfg = RawConfigParser() try: cfg.readfp(gitmodules_fileobj) except Exception as exc: log.warn('Malformatted .gitmodules file: {0}\n' '{1} cannot be assumed to be a git submodule.'.format( exc, path)) return False for section in cfg.sections(): if not cfg.has_option(section, 'path'): continue submodule_path = cfg.get(section, 'path').rstrip(os.sep) if submodule_path == path.rstrip(os.sep): return True return False def _update_submodule(submodule, status, offline): if status == ' ': # The submodule is up to date; no action necessary return elif status == '-': if offline: raise _AHBootstrapSystemExit( "Cannot initialize the {0} submodule in --offline mode; this " "requires being able to clone the submodule from an online " "repository.".format(submodule)) cmd = ['update', '--init'] action = 'Initializing' elif status == '+': cmd = ['update'] action = 'Updating' if offline: cmd.append('--no-fetch') elif status == 'U': raise _AHBoostrapSystemExit( 'Error: Submodule {0} contains unresolved merge conflicts. ' 'Please complete or abandon any changes in the submodule so that ' 'it is in a usable state, then try again.'.format(submodule)) else: log.warn('Unknown status {0!r} for git submodule {1!r}. Will ' 'attempt to use the submodule as-is, but try to ensure ' 'that the submodule is in a clean state and contains no ' 'conflicts or errors.\n{2}'.format(status, submodule, _err_help_msg)) return err_msg = None cmd = ['git', 'submodule'] + cmd + ['--', submodule] log.warn('{0} {1} submodule with: `{2}`'.format( action, submodule, ' '.join(cmd))) try: p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) stdout, stderr = p.communicate() except OSError as e: err_msg = str(e) else: if p.returncode != 0: stderr_encoding = locale.getdefaultlocale()[1] err_msg = stderr.decode(stderr_encoding) if err_msg: log.warn('An unexpected error occurred updating the git submodule ' '{0!r}:\n{1}\n{2}'.format(submodule, err_msg, _err_help_msg)) def _next_version(version): """ Given a parsed version from pkg_resources.parse_version, returns a new version string with the next minor version. Examples ======== >>> _next_version(pkg_resources.parse_version('1.2.3')) '1.3.0' """ if hasattr(version, 'base_version'): # New version parsing from setuptools >= 8.0 if version.base_version: parts = version.base_version.split('.') else: parts = [] else: parts = [] for part in version: if part.startswith('*'): break parts.append(part) parts = [int(p) for p in parts] if len(parts) < 3: parts += [0] * (3 - len(parts)) major, minor, micro = parts[:3] return '{0}.{1}.{2}'.format(major, minor + 1, 0) class _DummyFile(object): """A noop writeable object.""" errors = '' # Required for Python 3.x encoding = 'utf-8' def write(self, s): pass def flush(self): pass @contextlib.contextmanager def _silence(): """A context manager that silences sys.stdout and sys.stderr.""" old_stdout = sys.stdout old_stderr = sys.stderr sys.stdout = _DummyFile() sys.stderr = _DummyFile() exception_occurred = False try: yield except: exception_occurred = True # Go ahead and clean up so that exception handling can work normally sys.stdout = old_stdout sys.stderr = old_stderr raise if not exception_occurred: sys.stdout = old_stdout sys.stderr = old_stderr _err_help_msg = """ If the problem persists consider installing astropy_helpers manually using pip (`pip install astropy_helpers`) or by manually downloading the source archive, extracting it, and installing by running `python setup.py install` from the root of the extracted source code. """ class _AHBootstrapSystemExit(SystemExit): def __init__(self, *args): if not args: msg = 'An unknown problem occurred bootstrapping astropy_helpers.' else: msg = args[0] msg += '\n' + _err_help_msg super(_AHBootstrapSystemExit, self).__init__(msg, *args[1:]) if sys.version_info[:2] < (2, 7): # In Python 2.6 the distutils log does not log warnings, errors, etc. to # stderr so we have to wrap it to ensure consistency at least in this # module import distutils class log(object): def __getattr__(self, attr): return getattr(distutils.log, attr) def warn(self, msg, *args): self._log_to_stderr(distutils.log.WARN, msg, *args) def error(self, msg): self._log_to_stderr(distutils.log.ERROR, msg, *args) def fatal(self, msg): self._log_to_stderr(distutils.log.FATAL, msg, *args) def log(self, level, msg, *args): if level in (distutils.log.WARN, distutils.log.ERROR, distutils.log.FATAL): self._log_to_stderr(level, msg, *args) else: distutils.log.log(level, msg, *args) def _log_to_stderr(self, level, msg, *args): # This is the only truly 'public' way to get the current threshold # of the log current_threshold = distutils.log.set_threshold(distutils.log.WARN) distutils.log.set_threshold(current_threshold) if level >= current_threshold: if args: msg = msg % args sys.stderr.write('%s\n' % msg) sys.stderr.flush() log = log() # Output of `git submodule status` is as follows: # # 1: Status indicator: '-' for submodule is uninitialized, '+' if submodule is # initialized but is not at the commit currently indicated in .gitmodules (and # thus needs to be updated), or 'U' if the submodule is in an unstable state # (i.e. has merge conflicts) # # 2. SHA-1 hash of the current commit of the submodule (we don't really need # this information but it's useful for checking that the output is correct) # # 3. The output of `git describe` for the submodule's current commit hash (this # includes for example what branches the commit is on) but only if the # submodule is initialized. We ignore this information for now _git_submodule_status_re = re.compile( '^(?P[+-U ])(?P[0-9a-f]{40}) (?P\S+)( .*)?$') # Implement the auto-use feature; this allows use_astropy_helpers() to be used # at import-time automatically so long as the correct options are specified in # setup.cfg _CFG_OPTIONS = [('auto_use', bool), ('path', str), ('download_if_needed', bool), ('index_url', str), ('use_git', bool), ('auto_upgrade', bool)] def _main(): if not os.path.exists('setup.cfg'): return cfg = ConfigParser() try: cfg.read('setup.cfg') except Exception as e: if DEBUG: raise log.error( "Error reading setup.cfg: {0!r}\nastropy_helpers will not be " "automatically bootstrapped and package installation may fail." "\n{1}".format(e, _err_help_msg)) return if not cfg.has_section('ah_bootstrap'): return kwargs = {} for option, type_ in _CFG_OPTIONS: if not cfg.has_option('ah_bootstrap', option): continue if type_ is bool: value = cfg.getboolean('ah_bootstrap', option) else: value = cfg.get('ah_bootstrap', option) kwargs[option] = value if kwargs.pop('auto_use', False): use_astropy_helpers(**kwargs) _main() APLpy-1.0/aplpy/0000755000077000000240000000000012471104150013375 5ustar tomstaff00000000000000APLpy-1.0/aplpy/__init__.py0000644000077000000240000000137112470074275015525 0ustar tomstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ APLpy : Astronomical Plotting Library in Python """ # Affiliated packages may add whatever they like to this file, but # should keep this content at the top. # ---------------------------------------------------------------------------- from ._astropy_init import * # ---------------------------------------------------------------------------- if not _ASTROPY_SETUP_: from .core import FITSFigure from .rgb import make_rgb_image, make_rgb_cube from .frame import Frame from .overlays import Scalebar, Beam from .colorbar import Colorbar from .grid import Grid from .ticks import Ticks from .labels import TickLabels from .axis_labels import AxisLabels APLpy-1.0/aplpy/_astropy_init.py0000644000077000000240000001221412414762161016643 0ustar tomstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst __all__ = ['__version__', '__githash__', 'test'] # this indicates whether or not we are in the package's setup.py try: _ASTROPY_SETUP_ except NameError: from sys import version_info if version_info[0] >= 3: import builtins else: import __builtin__ as builtins builtins._ASTROPY_SETUP_ = False try: from .version import version as __version__ except ImportError: __version__ = '' try: from .version import githash as __githash__ except ImportError: __githash__ = '' # set up the test command def _get_test_runner(): import os from astropy.tests.helper import TestRunner return TestRunner(os.path.dirname(__file__)) def test(package=None, test_path=None, args=None, plugins=None, verbose=False, pastebin=None, remote_data=False, pep8=False, pdb=False, coverage=False, open_files=False, **kwargs): """ Run the tests using `py.test `__. A proper set of arguments is constructed and passed to `pytest.main`_. .. _py.test: http://pytest.org/latest/ .. _pytest.main: http://pytest.org/latest/builtin.html#pytest.main Parameters ---------- package : str, optional The name of a specific package to test, e.g. 'io.fits' or 'utils'. If nothing is specified all default tests are run. test_path : str, optional Specify location to test by path. May be a single file or directory. Must be specified absolutely or relative to the calling directory. args : str, optional Additional arguments to be passed to pytest.main_ in the ``args`` keyword argument. plugins : list, optional Plugins to be passed to pytest.main_ in the ``plugins`` keyword argument. verbose : bool, optional Convenience option to turn on verbose output from py.test_. Passing True is the same as specifying ``'-v'`` in ``args``. pastebin : {'failed','all',None}, optional Convenience option for turning on py.test_ pastebin output. Set to ``'failed'`` to upload info for failed tests, or ``'all'`` to upload info for all tests. remote_data : bool, optional Controls whether to run tests marked with @remote_data. These tests use online data and are not run by default. Set to True to run these tests. pep8 : bool, optional Turn on PEP8 checking via the `pytest-pep8 plugin `_ and disable normal tests. Same as specifying ``'--pep8 -k pep8'`` in ``args``. pdb : bool, optional Turn on PDB post-mortem analysis for failing tests. Same as specifying ``'--pdb'`` in ``args``. coverage : bool, optional Generate a test coverage report. The result will be placed in the directory htmlcov. open_files : bool, optional Fail when any tests leave files open. Off by default, because this adds extra run time to the test suite. Works only on platforms with a working ``lsof`` command. parallel : int, optional When provided, run the tests in parallel on the specified number of CPUs. If parallel is negative, it will use the all the cores on the machine. Requires the `pytest-xdist `_ plugin installed. Only available when using Astropy 0.3 or later. kwargs Any additional keywords passed into this function will be passed on to the astropy test runner. This allows use of test-related functionality implemented in later versions of astropy without explicitly updating the package template. """ test_runner = _get_test_runner() return test_runner.run_tests( package=package, test_path=test_path, args=args, plugins=plugins, verbose=verbose, pastebin=pastebin, remote_data=remote_data, pep8=pep8, pdb=pdb, coverage=coverage, open_files=open_files, **kwargs) if not _ASTROPY_SETUP_: import os from warnings import warn from astropy import config # add these here so we only need to cleanup the namespace at the end config_dir = None if not os.environ.get('ASTROPY_SKIP_CONFIG_UPDATE', False): config_dir = os.path.dirname(__file__) config_template = os.path.join(config_dir, __package__ + ".cfg") if os.path.isfile(config_template): try: config.configuration.update_default_config( __package__, config_dir, version=__version__) except TypeError as orig_error: try: config.configuration.update_default_config( __package__, config_dir) except config.configuration.ConfigurationDefaultMissingError as e: wmsg = (e.args[0] + " Cannot install default profile. If you are " "importing from source, this is expected.") warn(config.configuration.ConfigurationDefaultMissingWarning(wmsg)) del e except: raise orig_error APLpy-1.0/aplpy/angle_util.py0000644000077000000240000002503212471102127016076 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import math import struct import numpy as np from . import math_util def almost_equal(a, b): c = struct.pack(" 90: if d >= 270: self.negative = True d, m, s = 359 - d, 59 - m, 60. - s if s == 60.: s = s - 60. m = m + 1 if m == 60: m = m - 60. d = d + 1 else: raise Exception("latitude should be between -90 and 90 \ degrees") # Set new angle self.angle = (d, m, s) def todegrees(self): d, m, s = self.angle degrees = d + m / 60. + s / 3600. if self.negative: degrees = - degrees return degrees def tohours(self): d, m, s = self.angle rd = np.mod(d, 15) h = (d - rd) / 15 rm = np.mod(m, 15) m = (m - rm) / 15 + rd * 4 s = s / 15. + rm * 4. a = Angle(sexagesimal=(h, m, s), latitude=self.latitude) a.negative = self.negative return a def toround(self, rval=3): # Decompose angle d, m, s = self.angle # Round numbers: # 1: degrees only # 2: degrees and minutes # 3: degrees, minutes, and seconds # 4.n: degrees, minutes, and decimal seconds with n decimal places if rval < 2: n = int(round((rval - 1.) * 100)) d = round(d + m / 60. + s / 3600., n) if n == 0: d = int(d) return d elif rval < 3: m = int(round(m + s / 60.)) if m == 60: m = 0 d = d + 1 return (d, m) elif rval < 4: s = int(round(s)) if s == 60: s = 0 m = m + 1 if m == 60: m = 0 d = d + 1 return (d, m, s) else: n = int(round((rval - 4.) * 100)) s = round(s, n) if s == 60.: s = 0. m = m + 1 if m == 60: m = 0 d = d + 1 return (d, m, s) def tostringlist(self, format='ddd:mm:ss', sep=("d", "m", "s")): format = format.replace('h', 'd') r = 1 if '.d' in format: r = 1 pos = format.find('.') nd = len(format[pos + 1:]) r = r + nd / 100. if 'mm' in format: r = 2 if 'ss' in format: r = 3 if '.s' in format: r = 4 pos = format.find('.') ns = len(format[pos + 1:]) r = r + ns / 100. tup = self.toround(rval=r) if type(tup) == tuple: tup = list(tup) else: tup = [tup] string = [] if 'dd' in format: if '.d' in format: string.append(("%0" + str(nd + 3) + "." + str(nd) + "f") % \ tup[0] + sep[0]) else: string.append("%i" % tup[0] + sep[0]) if 'mm' in format: string.append("%02i" % tup[1] + sep[1]) if 'ss' in format and not '.s' in format: string.append("%02i" % tup[2] + sep[2]) if 'ss.s' in format: string.append(("%0" + str(ns + 3) + "." + str(ns) + "f") % tup[2] + sep[2]) # If style is colons, need to remove trailing colon if len(string) >= 1 and sep[0] == ':' and not 'mm' in format: string[0] = string[0][:-1] if len(string) >= 2 and sep[1] == ':' and not 'ss' in format: string[1] = string[1][:-1] if self.latitude: if self.negative: string[0] = "-" + string[0] else: string[0] = "+" + string[0] return string def __str__(self): return self.angle.__str__() def __repr__(self): return self.angle.__repr__() def __add__(self, other): s = self.angle[2] + other.angle[2] m = self.angle[1] + other.angle[1] d = self.angle[0] + other.angle[0] s = Angle(sexagesimal=(d, m, s), latitude=self.latitude) s._simplify() return s def __mul__(self, other): d, m, s = self.angle s = s * other m = m * other d = d * other if self.latitude and self.negative: d, m, s = -d, -m, -s s = Angle(sexagesimal=(d, m, s), latitude=self.latitude) s._simplify() return s def __eq__(self, other): return self.angle[0] == other.angle[0] \ and self.angle[1] == other.angle[1] \ and almost_equal(self.angle[2], other.angle[2]) def __div__(self, other): ''' Divide an angle by another This method calculates the division using the angles in degrees, and then corrects for any rounding errors if the division should be exact. ''' # Find division of angles in degrees div = self.todegrees() / other.todegrees() # Find the nearest integer divint = int(round(div)) # Check whether the denominator multiplied by this number is exactly # the numerator if other * divint == self: return divint else: return div __truediv__ = __div__ def smart_round_angle_sexagesimal(x, latitude=False, hours=False): d, m, s = 0, 0, 0. divisors_360 = math_util.divisors(360) divisors_10 = math_util.divisors(10) divisors_60 = math_util.divisors(60) if hours: x /= 15. if x >= 1: d = math_util.closest(divisors_360, x) else: x = x * 60. if x >= 1: m = math_util.closest(divisors_60, x) else: x = x * 60. if x >= 1: s = math_util.closest(divisors_60, x) else: t = 1. while True: t = t * 10. x = x * 10. if x >= 1: s = math_util.closest(divisors_10, x) / t break a = Angle(sexagesimal=(d, m, s), latitude=latitude) if hours: a *= 15 return a def smart_round_angle_decimal(x, latitude=False): divisors_360 = math_util.divisors(360) divisors_10 = math_util.divisors(10) if x >= 1: d = math_util.closest(divisors_360, x) else: t = 1. while True: t = t * 10. x = x * 10. if x >= 1: d = math_util.closest(divisors_10, x) / t break a = Angle(degrees=d, latitude=latitude) return a def _get_label_precision(format, latitude=False): # Find base spacing if "mm" in format: if "ss" in format: if "ss.s" in format: n_decimal = len(format.split('.')[1]) label_spacing = Angle(sexagesimal=(0, 0, 10 ** (-n_decimal)), latitude=latitude) else: label_spacing = Angle(sexagesimal=(0, 0, 1), latitude=latitude) else: label_spacing = Angle(sexagesimal=(0, 1, 0), latitude=latitude) elif "." in format: ns = len(format.split('.')[1]) label_spacing = Angle(degrees=10 ** (-ns), latitude=latitude) else: label_spacing = Angle(sexagesimal=(1, 0, 0), latitude=latitude) # Check if hours are used instead of degrees if "hh" in format: label_spacing *= 15 return label_spacing class InconsistentSpacing(Exception): pass def _check_format_spacing_consistency(format, spacing): ''' Check whether the format can correctly show labels with the specified spacing. For example, if the tick spacing is set to 1 arcsecond, but the format is set to dd:mm, then the labels cannot be correctly shown. Similarly, if the spacing is set to 1/1000 of a degree, or 3.6", then a format of dd:mm:ss will cause rounding errors, because the spacing includes fractional arcseconds. This function will raise a warning if the format and spacing are inconsistent. ''' label_spacing = _get_label_precision(format) if type(spacing / label_spacing) != int: raise InconsistentSpacing('Label format and tick spacing are inconsistent. Make sure that the tick spacing is a multiple of the smallest angle that can be represented by the specified format (currently %s). For example, if the format is dd:mm:ss.s, then the tick spacing has to be a multiple of 0.1". Similarly, if the format is hh:mm:ss, then the tick spacing has to be a multiple of 15". If you got this error as a result of interactively zooming in to a small region, this means that the default display format for the labels is not accurate enough, so you will need to increase the format precision.' % format) APLpy-1.0/aplpy/axis_labels.py0000644000077000000240000001557412471065325016263 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division from matplotlib.font_manager import FontProperties from . import wcs_util from .decorators import auto_refresh, fixdocstring class AxisLabels(object): def __init__(self, parent): # Store references to axes self._ax1 = parent._ax1 self._ax2 = parent._ax2 self._wcs = parent._wcs self._figure = parent._figure # Save plotting parameters (required for @auto_refresh) self._parameters = parent._parameters # Set font self._label_fontproperties = FontProperties() self._ax2.yaxis.set_label_position('right') self._ax2.xaxis.set_label_position('top') system, equinox, units = wcs_util.system(self._wcs) if system['name'] == 'equatorial': if equinox == 'b1950': xtext = 'RA (B1950)' ytext = 'Dec (B1950)' else: xtext = 'RA (J2000)' ytext = 'Dec (J2000)' elif system['name'] == 'galactic': xtext = 'Galactic Longitude' ytext = 'Galactic Latitude' elif system['name'] == 'ecliptic': xtext = 'Ecliptic Longitude' ytext = 'Ecliptic Latitude' elif system['name'] == 'unknown': xunit = " (%s)" % self._wcs.cunit_x if self._wcs.cunit_x not in ["", None] else "" yunit = " (%s)" % self._wcs.cunit_y if self._wcs.cunit_y not in ["", None] else "" if len(self._wcs.cname_x) > 0: xtext = self._wcs.cname_x + xunit else: if len(self._wcs.ctype_x) == 8 and self._wcs.ctype_x[4] == '-': xtext = self._wcs.ctype_x[:4].replace('-', '') + xunit else: xtext = self._wcs.ctype_x + xunit if len(self._wcs.cname_y) > 0: ytext = self._wcs.cname_y + yunit else: if len(self._wcs.ctype_y) == 8 and self._wcs.ctype_y[4] == '-': ytext = self._wcs.ctype_y[:4].replace('-', '') + yunit else: ytext = self._wcs.ctype_y + yunit if system['inverted']: xtext, ytext = ytext, xtext self.set_xtext(xtext) self.set_ytext(ytext) self.set_xposition('bottom') self.set_yposition('left') @auto_refresh def set_xtext(self, label): """ Set the x-axis label text. """ self._xlabel1 = self._ax1.set_xlabel(label) self._xlabel2 = self._ax2.set_xlabel(label) @auto_refresh def set_ytext(self, label): """ Set the y-axis label text. """ self._ylabel1 = self._ax1.set_ylabel(label) self._ylabel2 = self._ax2.set_ylabel(label) @auto_refresh def set_xpad(self, pad): """ Set the x-axis label displacement, in points. """ self._xlabel1 = self._ax1.set_xlabel(self._xlabel1.get_text(), labelpad=pad) self._xlabel2 = self._ax2.set_xlabel(self._xlabel2.get_text(), labelpad=pad) @auto_refresh def set_ypad(self, pad): """ Set the y-axis label displacement, in points. """ self._ylabel1 = self._ax1.set_ylabel(self._ylabel1.get_text(), labelpad=pad) self._ylabel2 = self._ax2.set_ylabel(self._ylabel2.get_text(), labelpad=pad) @auto_refresh @fixdocstring def set_font(self, family=None, style=None, variant=None, stretch=None, weight=None, size=None, fontproperties=None): """ Set the font of the axis labels. Parameters ---------- common: family, style, variant, stretch, weight, size, fontproperties Notes ----- Default values are set by matplotlib or previously set values if set_font has already been called. Global default values can be set by editing the matplotlibrc file. """ if family: self._label_fontproperties.set_family(family) if style: self._label_fontproperties.set_style(style) if variant: self._label_fontproperties.set_variant(variant) if stretch: self._label_fontproperties.set_stretch(stretch) if weight: self._label_fontproperties.set_weight(weight) if size: self._label_fontproperties.set_size(size) if fontproperties: self._label_fontproperties = fontproperties self._xlabel1.set_fontproperties(self._label_fontproperties) self._xlabel2.set_fontproperties(self._label_fontproperties) self._ylabel1.set_fontproperties(self._label_fontproperties) self._ylabel2.set_fontproperties(self._label_fontproperties) @auto_refresh def show(self): """ Show the x- and y-axis labels. """ self.show_x() self.show_y() @auto_refresh def hide(self): """ Hide the x- and y-axis labels. """ self.hide_x() self.hide_y() @auto_refresh def show_x(self): """ Show the x-axis label. """ if self._xposition == 'bottom': self._xlabel1.set_visible(True) else: self._xlabel2.set_visible(True) @auto_refresh def hide_x(self): """ Hide the x-axis label. """ if self._xposition == 'bottom': self._xlabel1.set_visible(False) else: self._xlabel2.set_visible(False) @auto_refresh def show_y(self): """ Show the y-axis label. """ if self._yposition == 'left': self._ylabel1.set_visible(True) else: self._ylabel2.set_visible(True) @auto_refresh def hide_y(self): """ Hide the y-axis label. """ if self._yposition == 'left': self._ylabel1.set_visible(False) else: self._ylabel2.set_visible(False) @auto_refresh def set_xposition(self, position): "Set the position of the x-axis label ('top' or 'bottom')" if position == 'bottom': self._xlabel1.set_visible(True) self._xlabel2.set_visible(False) elif position == 'top': self._xlabel1.set_visible(False) self._xlabel2.set_visible(True) else: raise ValueError("position should be one of 'top' or 'bottom'") self._xposition = position @auto_refresh def set_yposition(self, position): "Set the position of the y-axis label ('left' or 'right')" if position == 'left': self._ylabel1.set_visible(True) self._ylabel2.set_visible(False) elif position == 'right': self._ylabel1.set_visible(False) self._ylabel2.set_visible(True) else: raise ValueError("position should be one of 'left' or 'right'") self._yposition = position APLpy-1.0/aplpy/colorbar.py0000644000077000000240000003612612471065325015574 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import warnings import matplotlib.axes as maxes from mpl_toolkits.axes_grid import make_axes_locatable from matplotlib.font_manager import FontProperties from matplotlib.ticker import LogFormatterMathtext from .decorators import auto_refresh, fixdocstring # As of matplotlib 0.99.1.1, any time a colorbar property is updated, the axes # need to be removed and re-created. This has been fixed in svn r8213 but we # should wait until we up the required version of matplotlib before changing the # code here class Colorbar(object): def __init__(self, parent): self._figure = parent._figure self._colorbar_axes = None self._parent = parent # Save plotting parameters (required for @auto_refresh) self._parameters = parent._parameters self._base_settings = {} self._ticklabel_fontproperties = FontProperties() self._axislabel_fontproperties = FontProperties() @auto_refresh def show(self, location='right', width=0.2, pad=0.05, ticks=None, labels=True, log_format=False, box=None, box_orientation='vertical', axis_label_text=None, axis_label_rotation=None, axis_label_pad=5): ''' Show a colorbar on the side of the image. Parameters ---------- location : str, optional Where to place the colorbar. Should be one of 'left', 'right', 'top', 'bottom'. width : float, optional The width of the colorbar relative to the canvas size. pad : float, optional The spacing between the colorbar and the image relative to the canvas size. ticks : list, optional The position of the ticks on the colorbar. labels : bool, optional Whether to show numerical labels. log_format : bool, optional Whether to format ticks in exponential notation box : list, optional A custom box within which to place the colorbar. This should be in the form [xmin, ymin, dx, dy] and be in relative figure units. This overrides the location argument. box_orientation str, optional The orientation of the colorbar within the box. Can be 'horizontal' or 'vertical' axis_label_text str, optional Optional text label of the colorbar. ''' self._base_settings['location'] = location self._base_settings['width'] = width self._base_settings['pad'] = pad self._base_settings['ticks'] = ticks self._base_settings['labels'] = labels self._base_settings['log_format'] = log_format self._base_settings['box'] = box self._base_settings['box_orientation'] = box_orientation self._base_settings['axis_label_text'] = axis_label_text self._base_settings['axis_label_rotation'] = axis_label_rotation self._base_settings['axis_label_pad'] = axis_label_pad if self._parent.image: if self._colorbar_axes: self._parent._figure.delaxes(self._colorbar_axes) if box is None: divider = make_axes_locatable(self._parent._ax1) if location == 'right': self._colorbar_axes = divider.new_horizontal(size=width, pad=pad, axes_class=maxes.Axes) orientation = 'vertical' elif location == 'top': self._colorbar_axes = divider.new_vertical(size=width, pad=pad, axes_class=maxes.Axes) orientation = 'horizontal' elif location == 'left': warnings.warn("Left colorbar not fully implemented") self._colorbar_axes = divider.new_horizontal(size=width, pad=pad, pack_start=True, axes_class=maxes.Axes) locator = divider.new_locator(nx=0, ny=0) self._colorbar_axes.set_axes_locator(locator) orientation = 'vertical' elif location == 'bottom': warnings.warn("Bottom colorbar not fully implemented") self._colorbar_axes = divider.new_vertical(size=width, pad=pad, pack_start=True, axes_class=maxes.Axes) locator = divider.new_locator(nx=0, ny=0) self._colorbar_axes.set_axes_locator(locator) orientation = 'horizontal' else: raise Exception("location should be one of: right/top") self._parent._figure.add_axes(self._colorbar_axes) else: self._colorbar_axes = self._parent._figure.add_axes(box) orientation = box_orientation if log_format: format=LogFormatterMathtext() else: format=None self._colorbar = self._parent._figure.colorbar(self._parent.image, cax=self._colorbar_axes, orientation=orientation, format=format, ticks=ticks) if axis_label_text: if axis_label_rotation: self._colorbar.set_label(axis_label_text, rotation=axis_label_rotation) else: self._colorbar.set_label(axis_label_text) if location == 'right': for tick in self._colorbar_axes.yaxis.get_major_ticks(): tick.tick1On = True tick.tick2On = True tick.label1On = False tick.label2On = labels self._colorbar_axes.yaxis.set_label_position('right') self._colorbar_axes.yaxis.labelpad = axis_label_pad elif location == 'top': for tick in self._colorbar_axes.xaxis.get_major_ticks(): tick.tick1On = True tick.tick2On = True tick.label1On = False tick.label2On = labels self._colorbar_axes.xaxis.set_label_position('top') self._colorbar_axes.xaxis.labelpad = axis_label_pad elif location == 'left': for tick in self._colorbar_axes.yaxis.get_major_ticks(): tick.tick1On = True tick.tick2On = True tick.label1On = labels tick.label2On = False self._colorbar_axes.yaxis.set_label_position('left') self._colorbar_axes.yaxis.labelpad = axis_label_pad elif location == 'bottom': for tick in self._colorbar_axes.xaxis.get_major_ticks(): tick.tick1On = True tick.tick2On = True tick.label1On = labels tick.label2On = False self._colorbar_axes.xaxis.set_label_position('bottom') self._colorbar_axes.xaxis.labelpad = axis_label_pad else: warnings.warn("No image is shown, therefore, no colorbar will be plotted") @auto_refresh def update(self): if self._colorbar_axes: self.show(**self._base_settings) @auto_refresh def hide(self): self._parent._figure.delaxes(self._colorbar_axes) self._colorbar_axes = None @auto_refresh def _remove(self): self._parent._figure.delaxes(self._colorbar_axes) # LOCATION AND SIZE @auto_refresh def set_location(self, location): ''' Set the location of the colorbar. Should be one of 'left', 'right', 'top', 'bottom'. ''' self._base_settings['location'] = location self.show(**self._base_settings) self.set_font(fontproperties=self._ticklabel_fontproperties) self.set_axis_label_font(fontproperties=self._axislabel_fontproperties) @auto_refresh def set_width(self, width): ''' Set the width of the colorbar relative to the canvas size. ''' self._base_settings['width'] = width self.show(**self._base_settings) self.set_font(fontproperties=self._ticklabel_fontproperties) self.set_axis_label_font(fontproperties=self._axislabel_fontproperties) @auto_refresh def set_pad(self, pad): ''' Set the spacing between the colorbar and the image relative to the canvas size. ''' self._base_settings['pad'] = pad self.show(**self._base_settings) self.set_font(fontproperties=self._ticklabel_fontproperties) self.set_axis_label_font(fontproperties=self._axislabel_fontproperties) @auto_refresh def set_ticks(self, ticks): ''' Set the position of the ticks on the colorbar. ''' self._base_settings['ticks'] = ticks self.show(**self._base_settings) self.set_font(fontproperties=self._ticklabel_fontproperties) self.set_axis_label_font(fontproperties=self._axislabel_fontproperties) @auto_refresh def set_labels(self, labels): ''' Set whether to show numerical labels. ''' self._base_settings['labels'] = labels self.show(**self._base_settings) self.set_font(fontproperties=self._ticklabel_fontproperties) self.set_axis_label_font(fontproperties=self._axislabel_fontproperties) @auto_refresh def set_box(self, box, box_orientation='vertical'): ''' Set the box within which to place the colorbar. This should be in the form [xmin, ymin, dx, dy] and be in relative figure units. The orientation of the colorbar within the box can be controlled with the box_orientation argument. ''' self._base_settings['box'] = box self._base_settings['box_orientation'] = box_orientation self.show(**self._base_settings) self.set_font(fontproperties=self._ticklabel_fontproperties) self.set_axis_label_font(fontproperties=self._axislabel_fontproperties) @auto_refresh def set_axis_label_text(self, axis_label_text): ''' Set the colorbar label text. ''' self._base_settings['axis_label_text'] = axis_label_text self.show(**self._base_settings) self.set_font(fontproperties=self._ticklabel_fontproperties) self.set_axis_label_font(fontproperties=self._axislabel_fontproperties) @auto_refresh def set_axis_label_rotation(self, axis_label_rotation): ''' Set the colorbar label rotation. ''' self._base_settings['axis_label_rotation'] = axis_label_rotation self.show(**self._base_settings) self.set_font(fontproperties=self._ticklabel_fontproperties) self.set_axis_label_font(fontproperties=self._axislabel_fontproperties) @auto_refresh def set_axis_label_pad(self, axis_label_pad): ''' Set the colorbar label displacement, in points. ''' self._base_settings['axis_label_pad'] = axis_label_pad self.show(**self._base_settings) self.set_font(fontproperties=self._ticklabel_fontproperties) self.set_axis_label_font(fontproperties=self._axislabel_fontproperties) # FONT PROPERTIES @auto_refresh def set_label_properties(self, *args, **kwargs): warnings.warn("set_label_properties is deprecated - use set_font instead", DeprecationWarning) self.set_font(*args, **kwargs) @auto_refresh @fixdocstring def set_font(self, family=None, style=None, variant=None, stretch=None, weight=None, size=None, fontproperties=None): ''' Set the font of the tick labels. Parameters ---------- common: family, style, variant, stretch, weight, size, fontproperties Notes ----- Default values are set by matplotlib or previously set values if set_font has already been called. Global default values can be set by editing the matplotlibrc file. ''' if family: self._ticklabel_fontproperties.set_family(family) if style: self._ticklabel_fontproperties.set_style(style) if variant: self._ticklabel_fontproperties.set_variant(variant) if stretch: self._ticklabel_fontproperties.set_stretch(stretch) if weight: self._ticklabel_fontproperties.set_weight(weight) if size: self._ticklabel_fontproperties.set_size(size) if fontproperties: self._ticklabel_fontproperties = fontproperties # Update the tick label font properties for label in self._colorbar_axes.get_xticklabels(): label.set_fontproperties(self._ticklabel_fontproperties) for label in self._colorbar_axes.get_yticklabels(): label.set_fontproperties(self._ticklabel_fontproperties) # Also update the offset text font properties label = self._colorbar_axes.xaxis.get_offset_text() label.set_fontproperties(self._ticklabel_fontproperties) label = self._colorbar_axes.yaxis.get_offset_text() label.set_fontproperties(self._ticklabel_fontproperties) @auto_refresh @fixdocstring def set_axis_label_font(self, family=None, style=None, variant=None, stretch=None, weight=None, size=None, fontproperties=None): ''' Set the font of the tick labels. Parameters ---------- common: family, style, variant, stretch, weight, size, fontproperties Notes ----- Default values are set by matplotlib or previously set values if set_font has already been called. Global default values can be set by editing the matplotlibrc file. ''' if family: self._axislabel_fontproperties.set_family(family) if style: self._axislabel_fontproperties.set_style(style) if variant: self._axislabel_fontproperties.set_variant(variant) if stretch: self._axislabel_fontproperties.set_stretch(stretch) if weight: self._axislabel_fontproperties.set_weight(weight) if size: self._axislabel_fontproperties.set_size(size) if fontproperties: self._axislabel_fontproperties = fontproperties # Update the label font properties label = self._colorbar_axes.xaxis.get_label() label.set_fontproperties(self._axislabel_fontproperties) label = self._colorbar_axes.yaxis.get_label() label.set_fontproperties(self._axislabel_fontproperties) # FRAME PROPERTIES @auto_refresh def set_frame_linewidth(self, linewidth): ''' Set the linewidth of the colorbar frame, in points. ''' warnings.warn("This method is not functional at this time") for key in self._colorbar_axes.spines: self._colorbar_axes.spines[key].set_linewidth(linewidth) @auto_refresh def set_frame_color(self, color): ''' Set the color of the colorbar frame, in points. ''' warnings.warn("This method is not functional at this time") for key in self._colorbar_axes.spines: self._colorbar_axes.spines[key].set_edgecolor(color) APLpy-1.0/aplpy/conftest.py0000644000077000000240000000142612471102066015603 0ustar tomstaff00000000000000# this contains imports plugins that configure py.test for astropy tests. # by importing them here in conftest.py they are discoverable by py.test # no matter how it is invoked within the source tree. from astropy.tests.pytest_plugins import * from astropy.tests.pytest_plugins import pytest_addoption as astropy_pytest_addoption # Uncomment the following line to treat all DeprecationWarnings as # exceptions enable_deprecations_as_exceptions() import os from astropy.tests.helper import pytest def pytest_addoption(parser): parser.addoption('--generate-images-path', help="directory to generate reference images in", action='store') return astropy_pytest_addoption(parser) @pytest.fixture def generate(request): return request.config.getoption("--generate-images-path") APLpy-1.0/aplpy/contour_util.py0000644000077000000240000000255312471065325016514 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import numpy as np from matplotlib.path import Path from . import wcs_util def transform(contours, wcs_in, wcs_out, filled=False, overlap=False): system_in, equinox_in, units_in = wcs_util.system(wcs_in) system_out, equinox_out, units_out = wcs_util.system(wcs_out) for contour in contours.collections: polygons_out = [] for polygon in contour.get_paths(): xp_in = polygon.vertices[:, 0] yp_in = polygon.vertices[:, 1] xw, yw = wcs_util.pix2world(wcs_in, xp_in, yp_in) xw, yw = wcs_util.convert_coords(xw, yw, input=(system_in, equinox_in), output=(system_out, equinox_out)) xp_out, yp_out = wcs_util.world2pix(wcs_out, xw, yw) if overlap: if np.all(xp_out < 0) or np.all(yp_out < 0) or \ np.all(xp_out > wcs_out.nx) or np.all(yp_out > wcs_out.ny): continue if filled: polygons_out.append(Path(np.array(list(zip(xp_out, yp_out))), codes=polygon.codes)) else: polygons_out.append(list(zip(xp_out, yp_out))) if filled: contour.set_paths(polygons_out) else: contour.set_verts(polygons_out) contour.apl_converted = True APLpy-1.0/aplpy/convolve_util.py0000644000077000000240000000265312470074275016662 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import numpy as np try: from astropy.convolution import convolve as astropy_convolve, Gaussian2DKernel, Box2DKernel make_kernel = None except ImportError: from astropy.nddata import convolve as astropy_convolve, make_kernel def convolve(image, smooth=3, kernel='gauss'): if smooth is None and kernel in ['box', 'gauss']: return image if smooth is not None and not np.isscalar(smooth): raise ValueError("smooth= should be an integer - for more complex " "kernels, pass an array containing the kernel " "to the kernel= option") # The Astropy convolution doesn't treat +/-Inf values correctly yet, so we # convert to NaN here. image_fixed = np.array(image, dtype=float, copy=True) image_fixed[np.isinf(image)] = np.nan if kernel == 'gauss': if make_kernel is None: kernel = Gaussian2DKernel(smooth, x_size=smooth * 5, y_size=smooth * 5) else: kernel = make_kernel((smooth * 5, smooth * 5), smooth, 'gaussian') elif kernel == 'box': if make_kernel is None: kernel = Box2DKernel(smooth, x_size=smooth * 5, y_size=smooth * 5) else: kernel = make_kernel((smooth * 5, smooth * 5), smooth, 'boxcar') else: kernel = kernel return astropy_convolve(image, kernel, boundary='extend') APLpy-1.0/aplpy/core.py0000644000077000000240000021676312471102127014720 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division from distutils import version import os import operator from functools import reduce import matplotlib if version.LooseVersion(matplotlib.__version__) < version.LooseVersion('1.0.0'): raise Exception("matplotlib 1.0.0 or later is required for APLpy") import matplotlib.pyplot as mpl import mpl_toolkits.axes_grid.parasite_axes as mpltk from astropy.extern import six WCS_TYPES = [] HDU_TYPES = [] HDULIST_TYPES = [] # We need to be able to accept PyFITS objects if users have old scripts that # are reading FITS files with this instead of Astropy try: import pyfits HDU_TYPES.append(pyfits.PrimaryHDU) HDU_TYPES.append(pyfits.ImageHDU) HDU_TYPES.append(pyfits.CompImageHDU) HDULIST_TYPES.append(pyfits.HDUList) del pyfits except ImportError: pass # Similarly, we need to accept PyWCS objects try: import pywcs WCS_TYPES.append(pywcs.WCS) del pywcs except ImportError: pass from astropy.io import fits HDU_TYPES.append(fits.PrimaryHDU) HDU_TYPES.append(fits.ImageHDU) HDU_TYPES.append(fits.CompImageHDU) HDULIST_TYPES.append(fits.HDUList) from astropy.wcs import WCS WCS_TYPES.append(WCS) del WCS # Convert to tuples so that these work when calling isinstance() HDU_TYPES = tuple(HDU_TYPES) HDULIST_TYPES = tuple(HDULIST_TYPES) WCS_TYPES = tuple(WCS_TYPES) import numpy as np from matplotlib.patches import Circle, Rectangle, Ellipse, Polygon, FancyArrow from matplotlib.collections import PatchCollection, LineCollection from astropy import log import astropy.utils.exceptions as aue from . import contour_util from . import convolve_util from . import image_util from . import header as header_util from . import wcs_util from . import slicer from .layers import Layers from .grid import Grid from .ticks import Ticks from .labels import TickLabels from .axis_labels import AxisLabels from .overlays import Beam, Scalebar from .regions import Regions from .colorbar import Colorbar from .normalize import APLpyNormalize from .frame import Frame from .decorators import auto_refresh, fixdocstring from .deprecated import Deprecated class Parameters(): ''' A class to contain the current plotting parameters ''' pass class FITSFigure(Layers, Regions, Deprecated): "A class for plotting FITS files." _parameters = Parameters() @auto_refresh def __init__(self, data, hdu=0, figure=None, subplot=(1, 1, 1), downsample=False, north=False, convention=None, dimensions=[0, 1], slices=[], auto_refresh=None, **kwargs): ''' Create a FITSFigure instance. Parameters ---------- data : see below The FITS file to open. The following data types can be passed: string astropy.io.fits.PrimaryHDU astropy.io.fits.ImageHDU pyfits.PrimaryHDU pyfits.ImageHDU astropy.wcs.WCS np.ndarray RGB image with AVM meta-data hdu : int, optional By default, the image in the primary HDU is read in. If a different HDU is required, use this argument. figure : ~matplotlib.figure.Figure, optional If specified, a subplot will be added to this existing matplotlib figure() instance, rather than a new figure being created from scratch. subplot : tuple or list, optional If specified, a subplot will be added at this position. If a tuple of three values, the tuple should contain the standard matplotlib subplot parameters, i.e. (ny, nx, subplot). If a list of four values, the list should contain [xmin, ymin, dx, dy] where xmin and ymin are the position of the bottom left corner of the subplot, and dx and dy are the width and height of the subplot respectively. These should all be given in units of the figure width and height. For example, [0.1, 0.1, 0.8, 0.8] will almost fill the entire figure, leaving a 10 percent margin on all sides. downsample : int, optional If this option is specified, the image will be downsampled by a factor *downsample* when reading in the data. north : str, optional Whether to rotate the image so that the North Celestial Pole is up. Note that this option requires Montage to be installed. convention : str, optional This is used in cases where a FITS header can be interpreted in multiple ways. For example, for files with a -CAR projection and CRVAL2=0, this can be set to 'wells' or 'calabretta' to choose the appropriate convention. dimensions : tuple or list, optional The index of the axes to use if the data has more than three dimensions. slices : tuple or list, optional If a FITS file with more than two dimensions is specified, then these are the slices to extract. If all extra dimensions only have size 1, then this is not required. auto_refresh : bool, optional Whether to refresh the figure automatically every time a plotting method is called. This can also be set using the set_auto_refresh method. This defaults to `True` if and only if APLpy is being used from IPython and the Matplotlib backend is interactive. kwargs Any additional arguments are passed on to matplotlib's Figure() class. For example, to set the figure size, use the figsize=(xsize, ysize) argument (where xsize and ysize are in inches). For more information on these additional arguments, see the *Optional keyword arguments* section in the documentation for `Figure `_ ''' # Set whether to automatically refresh the display self.set_auto_refresh(auto_refresh) if not 'figsize' in kwargs: kwargs['figsize'] = (10, 9) if isinstance(data, six.string_types) and data.split('.')[-1].lower() in ['png', 'jpg', 'tif']: try: from PIL import Image except ImportError: try: import Image except ImportError: raise ImportError("The Python Imaging Library (PIL) is required to read in RGB images") try: import pyavm except ImportError: raise ImportError("PyAVM is required to read in AVM meta-data from RGB images") if version.LooseVersion(pyavm.__version__) < version.LooseVersion('0.9.1'): raise ImportError("PyAVM installation is not recent enough " "(version 0.9.1 or later is required).") from pyavm import AVM # Remember image filename self._rgb_image = data # Find image size nx, ny = Image.open(data).size # Now convert AVM information to WCS data = AVM.from_image(data).to_wcs() # Need to scale CDELT values sometimes the AVM meta-data is only really valid for the full-resolution image data.wcs.cdelt = [data.wcs.cdelt[0] * nx / float(nx), data.wcs.cdelt[1] * ny / float(ny)] data.wcs.crpix = [data.wcs.crpix[0] / nx * float(nx), data.wcs.crpix[1] / ny * float(ny)] # Update the NAXIS values with the true dimensions of the RGB image data.nx = nx data.ny = ny if isinstance(data, WCS_TYPES): wcs = data if not hasattr(wcs, 'naxis1'): raise aue.AstropyDeprecationWarning('WCS no longer stores information about NAXISn ' 'so it is not possibly to instantiate a FITSFigure ' 'from WCS alone') if wcs.naxis != 2: raise ValueError("FITSFigure initialization via WCS objects can only be done with 2-dimensional WCS objects") header = wcs.to_header() header['NAXIS1'] = wcs.naxis1 header['NAXIS2'] = wcs.naxis2 nx = header['NAXIS%i' % (dimensions[0] + 1)] ny = header['NAXIS%i' % (dimensions[1] + 1)] self._data = np.zeros((ny, nx), dtype=float) self._header = header self._wcs = wcs_util.WCS(header, dimensions=dimensions, slices=slices, relax=True) self._wcs.nx = nx self._wcs.ny = ny if downsample: log.warning("downsample argument is ignored if data passed is a WCS object") downsample = False if north: log.warning("north argument is ignored if data passed is a WCS object") north = False else: self._data, self._header, self._wcs = self._get_hdu(data, hdu, north, \ convention=convention, dimensions=dimensions, slices=slices) self._wcs.nx = self._header['NAXIS%i' % (dimensions[0] + 1)] self._wcs.ny = self._header['NAXIS%i' % (dimensions[1] + 1)] # Downsample if requested if downsample: nx_new = self._wcs.nx - np.mod(self._wcs.nx, downsample) ny_new = self._wcs.ny - np.mod(self._wcs.ny, downsample) self._data = self._data[0:ny_new, 0:nx_new] self._data = image_util.resample(self._data, downsample) self._wcs.nx, self._wcs.ny = nx_new, ny_new # Open the figure if figure: self._figure = figure else: self._figure = mpl.figure(**kwargs) # Create first axis instance if type(subplot) == list and len(subplot) == 4: self._ax1 = mpltk.HostAxes(self._figure, subplot, adjustable='datalim') elif type(subplot) == tuple and len(subplot) == 3: self._ax1 = mpltk.SubplotHost(self._figure, *subplot) else: raise ValueError("subplot= should be either a tuple of three values, or a list of four values") self._ax1.toggle_axisline(False) self._figure.add_axes(self._ax1) # Create second axis instance self._ax2 = self._ax1.twin() self._ax2.set_frame_on(False) self._ax2.toggle_axisline(False) # Turn off autoscaling self._ax1.set_autoscale_on(False) self._ax2.set_autoscale_on(False) # Force zorder of parasite axes self._ax2.xaxis.set_zorder(2.5) self._ax2.yaxis.set_zorder(2.5) # Store WCS in axes self._ax1._wcs = self._wcs self._ax2._wcs = self._wcs # Set view to whole FITS file self._initialize_view() # Initialize ticks self.ticks = Ticks(self) # Initialize labels self.axis_labels = AxisLabels(self) self.tick_labels = TickLabels(self) self.frame = Frame(self) self._ax1.format_coord = self.tick_labels._cursor_position # Initialize layers list self._initialize_layers() # Find generating function for vmin/vmax self._auto_v = image_util.percentile_function(self._data) # Set image holder to be empty self.image = None # Set default theme self.set_theme(theme='pretty') def _get_hdu(self, data, hdu, north, convention=None, dimensions=[0, 1], slices=[]): if isinstance(data, six.string_types): filename = data # Check file exists if not os.path.exists(filename): raise IOError("File not found: " + filename) # Read in FITS file try: hdulist = fits.open(filename) except: raise IOError("An error occurred while reading the FITS file") # Check whether the HDU specified contains any data, otherwise # cycle through all HDUs to find one that contains valid image data if hdulist[hdu].data is None: found = False for alt_hdu in range(len(hdulist)): if isinstance(hdulist[alt_hdu], HDU_TYPES): if hdulist[alt_hdu].data is not None: log.warning("hdu=%i does not contain any data, using hdu=%i instead" % (hdu, alt_hdu)) hdu = hdulist[alt_hdu] found = True break if not found: raise Exception("FITS file does not contain any image data") else: hdu = hdulist[hdu] elif type(data) == np.ndarray: hdu = fits.ImageHDU(data) elif isinstance(data, HDU_TYPES): hdu = data elif isinstance(data, HDULIST_TYPES): hdu = data[hdu] else: raise Exception("data argument should either be a filename, an HDU object from astropy.io.fits or pyfits, a WCS object from astropy.wcs or pywcs, or a Numpy array.") # Check that we have at least 2-dimensional data if hdu.header['NAXIS'] < 2: raise ValueError("Data should have at least two dimensions") # Check dimensions= argument if type(dimensions) not in [list, tuple]: raise ValueError('dimensions= should be a list or a tuple') if len(set(dimensions)) != 2 or len(dimensions) != 2: raise ValueError("dimensions= should be a tuple of two different values") if dimensions[0] < 0 or dimensions[0] > hdu.header['NAXIS'] - 1: raise ValueError('values of dimensions= should be between %i and %i' % (0, hdu.header['NAXIS'] - 1)) if dimensions[1] < 0 or dimensions[1] > hdu.header['NAXIS'] - 1: raise ValueError('values of dimensions= should be between %i and %i' % (0, hdu.header['NAXIS'] - 1)) # Reproject to face north if requested if north: try: import montage_wrapper as montage except ImportError: raise Exception("Both the Montage command-line tools and the" " montage-wrapper Python module are required" " to use the north= argument") hdu = montage.reproject_hdu(hdu, north_aligned=True) # Now copy the data and header to new objects, since in PyFITS the two # attributes are linked, which can lead to confusing behavior. We just # need to copy the header to avoid memory issues - as long as one item # is copied, the two variables are decoupled. data = hdu.data header = hdu.header.copy() del hdu # If slices wasn't specified, check if we can guess shape = data.shape if len(shape) > 2: n_total = reduce(operator.mul, shape) n_image = shape[len(shape) - 1 - dimensions[0]] \ * shape[len(shape) - 1 - dimensions[1]] if n_total == n_image: slices = [0 for i in range(1, len(shape) - 1)] log.info("Setting slices=%s" % str(slices)) # Extract slices data = slicer.slice_hypercube(data, header, dimensions=dimensions, slices=slices) # Check header header = header_util.check(header, convention=convention, dimensions=dimensions) # Parse WCS info wcs = wcs_util.WCS(header, dimensions=dimensions, slices=slices, relax=True) return data, header, wcs @auto_refresh def set_title(self, title, **kwargs): ''' Set the figure title ''' self._ax1.set_title(title, **kwargs) @auto_refresh def set_xaxis_coord_type(self, coord_type): ''' Set the type of x coordinate. Options are: * ``scalar``: treat the values are normal decimal scalar values * ``longitude``: treat the values as a longitude in the 0 to 360 range * ``latitude``: treat the values as a latitude in the -90 to 90 range ''' self._wcs.set_xaxis_coord_type(coord_type) @auto_refresh def set_yaxis_coord_type(self, coord_type): ''' Set the type of y coordinate. Options are: * ``scalar``: treat the values are normal decimal scalar values * ``longitude``: treat the values as a longitude in the 0 to 360 range * ``latitude``: treat the values as a latitude in the -90 to 90 range ''' self._wcs.set_yaxis_coord_type(coord_type) @auto_refresh def set_system_latex(self, usetex): ''' Set whether to use a real LaTeX installation or the built-in matplotlib LaTeX. Parameters ---------- usetex : str Whether to use a real LaTex installation (True) or the built-in matplotlib LaTeX (False). Note that if the former is chosen, an installation of LaTex is required. ''' mpl.rc('text', usetex=usetex) @auto_refresh def recenter(self, x, y, radius=None, width=None, height=None): ''' Center the image on a given position and with a given radius. Either the radius or width/height arguments should be specified. The units of the radius or width/height should be the same as the world coordinates in the WCS. For images of the sky, this is often (but not always) degrees. Parameters ---------- x, y : float Coordinates to center on radius : float, optional Radius of the region to view in degrees. This produces a square plot. width : float, optional Width of the region to view. This should be given in conjunction with the height argument. height : float, optional Height of the region to view. This should be given in conjunction with the width argument. ''' xpix, ypix = wcs_util.world2pix(self._wcs, x, y) if self._wcs.is_celestial: sx = sy = wcs_util.celestial_pixel_scale(self._wcs) else: sx, sy = wcs_util.non_celestial_pixel_scales(self._wcs) if radius: dx_pix = radius / sx dy_pix = radius / sy elif width and height: dx_pix = width / sx * 0.5 dy_pix = height / sy * 0.5 else: raise Exception("Need to specify either radius= or width= and height= arguments") if xpix + dx_pix < self._extent[0] or \ xpix - dx_pix > self._extent[1] or \ ypix + dy_pix < self._extent[2] or \ ypix - dy_pix > self._extent[3]: raise Exception("Zoom region falls outside the image") self._ax1.set_xlim(xpix - dx_pix, xpix + dx_pix) self._ax1.set_ylim(ypix - dy_pix, ypix + dy_pix) @auto_refresh def show_grayscale(self, vmin=None, vmid=None, vmax=None, pmin=0.25, pmax=99.75, stretch='linear', exponent=2, invert='default', smooth=None, kernel='gauss', aspect='equal', interpolation='nearest'): ''' Show a grayscale image of the FITS file. Parameters ---------- vmin : None or float, optional Minimum pixel value to use for the grayscale. If set to None, the minimum pixel value is determined using pmin (default). vmax : None or float, optional Maximum pixel value to use for the grayscale. If set to None, the maximum pixel value is determined using pmax (default). pmin : float, optional Percentile value used to determine the minimum pixel value to use for the grayscale if vmin is set to None. The default value is 0.25%. pmax : float, optional Percentile value used to determine the maximum pixel value to use for the grayscale if vmax is set to None. The default value is 99.75%. stretch : { 'linear', 'log', 'sqrt', 'arcsinh', 'power' }, optional The stretch function to use vmid : None or float, optional Baseline value used for the log and arcsinh stretches. If set to None, this is set to zero for log stretches and to vmin - (vmax - vmin) / 30. for arcsinh stretches exponent : float, optional If stretch is set to 'power', this is the exponent to use invert : str, optional Whether to invert the grayscale or not. The default is False, unless set_theme is used, in which case the default depends on the theme. smooth : int or tuple, optional Default smoothing scale is 3 pixels across. User can define whether they want an NxN kernel (integer), or NxM kernel (tuple). This argument corresponds to the 'gauss' and 'box' smoothing kernels. kernel : { 'gauss', 'box', numpy.array }, optional Default kernel used for smoothing is 'gauss'. The user can specify if they would prefer 'gauss', 'box', or a custom kernel. All kernels are normalized to ensure flux retention. aspect : { 'auto', 'equal' }, optional Whether to change the aspect ratio of the image to match that of the axes ('auto') or to change the aspect ratio of the axes to match that of the data ('equal'; default) interpolation : str, optional The type of interpolation to use for the image. The default is 'nearest'. Other options include 'none' (no interpolation, meaning that if exported to a postscript file, the grayscale will be output at native resolution irrespective of the dpi setting), 'bilinear', 'bicubic', and many more (see the matplotlib documentation for imshow). ''' if invert == 'default': invert = self._get_invert_default() if invert: cmap = 'gist_yarg' else: cmap = 'gray' self.show_colorscale(vmin=vmin, vmid=vmid, vmax=vmax, pmin=pmin, pmax=pmax, stretch=stretch, exponent=exponent, cmap=cmap, smooth=smooth, kernel=kernel, aspect=aspect, interpolation=interpolation) @auto_refresh def hide_grayscale(self, *args, **kwargs): self.hide_colorscale(*args, **kwargs) @auto_refresh def show_colorscale(self, vmin=None, vmid=None, vmax=None, \ pmin=0.25, pmax=99.75, stretch='linear', exponent=2, cmap='default', smooth=None, kernel='gauss', aspect='equal', interpolation='nearest'): ''' Show a colorscale image of the FITS file. Parameters ---------- vmin : None or float, optional Minimum pixel value to use for the colorscale. If set to None, the minimum pixel value is determined using pmin (default). vmax : None or float, optional Maximum pixel value to use for the colorscale. If set to None, the maximum pixel value is determined using pmax (default). pmin : float, optional Percentile value used to determine the minimum pixel value to use for the colorscale if vmin is set to None. The default value is 0.25%. pmax : float, optional Percentile value used to determine the maximum pixel value to use for the colorscale if vmax is set to None. The default value is 99.75%. stretch : { 'linear', 'log', 'sqrt', 'arcsinh', 'power' }, optional The stretch function to use vmid : None or float, optional Baseline value used for the log and arcsinh stretches. If set to None, this is set to zero for log stretches and to vmin - (vmax - vmin) / 30. for arcsinh stretches exponent : float, optional If stretch is set to 'power', this is the exponent to use cmap : str, optional The name of the colormap to use smooth : int or tuple, optional Default smoothing scale is 3 pixels across. User can define whether they want an NxN kernel (integer), or NxM kernel (tuple). This argument corresponds to the 'gauss' and 'box' smoothing kernels. kernel : { 'gauss', 'box', numpy.array }, optional Default kernel used for smoothing is 'gauss'. The user can specify if they would prefer 'gauss', 'box', or a custom kernel. All kernels are normalized to ensure flux retention. aspect : { 'auto', 'equal' }, optional Whether to change the aspect ratio of the image to match that of the axes ('auto') or to change the aspect ratio of the axes to match that of the data ('equal'; default) interpolation : str, optional The type of interpolation to use for the image. The default is 'nearest'. Other options include 'none' (no interpolation, meaning that if exported to a postscript file, the colorscale will be output at native resolution irrespective of the dpi setting), 'bilinear', 'bicubic', and many more (see the matplotlib documentation for imshow). ''' if cmap == 'default': cmap = self._get_colormap_default() min_auto = np.equal(vmin, None) max_auto = np.equal(vmax, None) # The set of available functions cmap = mpl.cm.get_cmap(cmap) if min_auto: vmin = self._auto_v(pmin) if max_auto: vmax = self._auto_v(pmax) # Prepare normalizer object normalizer = APLpyNormalize(stretch=stretch, exponent=exponent, vmid=vmid, vmin=vmin, vmax=vmax) # Adjust vmin/vmax if auto if min_auto: if stretch == 'linear': vmin = -0.1 * (vmax - vmin) + vmin log.info("Auto-setting vmin to %10.3e" % vmin) if max_auto: if stretch == 'linear': vmax = 0.1 * (vmax - vmin) + vmax log.info("Auto-setting vmax to %10.3e" % vmax) # Update normalizer object normalizer.vmin = vmin normalizer.vmax = vmax if self.image: self.image.set_visible(True) self.image.set_norm(normalizer) self.image.set_cmap(cmap=cmap) self.image.origin = 'lower' self.image.set_interpolation(interpolation) self.image.set_data(convolve_util.convolve(self._data, smooth=smooth, kernel=kernel)) else: self.image = self._ax1.imshow( convolve_util.convolve(self._data, smooth=smooth, kernel=kernel), cmap=cmap, interpolation=interpolation, origin='lower', extent=self._extent, norm=normalizer, aspect=aspect) xmin, xmax = self._ax1.get_xbound() if xmin == 0.0: self._ax1.set_xlim(0.5, xmax) ymin, ymax = self._ax1.get_ybound() if ymin == 0.0: self._ax1.set_ylim(0.5, ymax) if hasattr(self, 'colorbar'): self.colorbar.update() @auto_refresh def hide_colorscale(self): self.image.set_visible(False) @auto_refresh def set_nan_color(self, color): ''' Set the color for NaN pixels. Parameters ---------- color : str This can be any valid matplotlib color ''' from copy import deepcopy cm = deepcopy(self.image.get_cmap()) cm.set_bad(color) self.image.set_cmap(cm) @auto_refresh def show_rgb(self, filename=None, interpolation='nearest', vertical_flip=False, horizontal_flip=False, flip=False): ''' Show a 3-color image instead of the FITS file data. Parameters ---------- filename, optional The 3-color image should have exactly the same dimensions as the FITS file, and will be shown with exactly the same projection. If FITSFigure was initialized with an AVM-tagged RGB image, the filename is not needed here. vertical_flip : str, optional Whether to vertically flip the RGB image horizontal_flip : str, optional Whether to horizontally flip the RGB image ''' try: from PIL import Image except ImportError: try: import Image except ImportError: raise ImportError("The Python Imaging Library (PIL) is required to read in RGB images") if flip: log.warning("Note that show_rgb should now correctly flip RGB images, so the flip= argument is now deprecated. If you still need to flip an image vertically or horizontally, you can use the vertical_flip= and horizontal_flip arguments instead.") if filename is None: if hasattr(self, '_rgb_image'): image = Image.open(self._rgb_image) else: raise Exception("Need to specify the filename of an RGB image") else: image = Image.open(filename) if image_util._matplotlib_pil_bug_present(): vertical_flip = True if vertical_flip: image = image.transpose(Image.FLIP_TOP_BOTTOM) if horizontal_flip: image = image.transpose(Image.FLIP_LEFT_RIGHT) # Elsewhere in APLpy we assume that we are using origin='lower' so here # we flip the image by default (since RGB images usually would require # origin='upper') then we use origin='lower' image = image.transpose(Image.FLIP_TOP_BOTTOM) self.image = self._ax1.imshow(image, extent=self._extent, interpolation=interpolation, origin='lower') @auto_refresh def show_contour(self, data=None, hdu=0, layer=None, levels=5, filled=False, cmap=None, colors=None, returnlevels=False, convention=None, dimensions=[0, 1], slices=[], smooth=None, kernel='gauss', overlap=False, **kwargs): ''' Overlay contours on the current plot. Parameters ---------- data : see below The FITS file to plot contours for. The following data types can be passed: string astropy.io.fits.PrimaryHDU astropy.io.fits.ImageHDU pyfits.PrimaryHDU pyfits.ImageHDU astropy.wcs.WCS np.ndarray hdu : int, optional By default, the image in the primary HDU is read in. If a different HDU is required, use this argument. layer : str, optional The name of the contour layer. This is useful for giving custom names to layers (instead of contour_set_n) and for replacing existing layers. levels : int or list, optional This can either be the number of contour levels to compute (if an integer is provided) or the actual list of contours to show (if a list of floats is provided) filled : str, optional Whether to show filled or line contours cmap : str, optional The colormap to use for the contours colors : str or tuple, optional If a single string is provided, all contour levels will be shown in this color. If a tuple of strings is provided, each contour will be colored according to the corresponding tuple element. returnlevels : str, optional Whether to return the list of contours to the caller. convention : str, optional This is used in cases where a FITS header can be interpreted in multiple ways. For example, for files with a -CAR projection and CRVAL2=0, this can be set to 'wells' or 'calabretta' to choose the appropriate convention. dimensions : tuple or list, optional The index of the axes to use if the data has more than three dimensions. slices : tuple or list, optional If a FITS file with more than two dimensions is specified, then these are the slices to extract. If all extra dimensions only have size 1, then this is not required. smooth : int or tuple, optional Default smoothing scale is 3 pixels across. User can define whether they want an NxN kernel (integer), or NxM kernel (tuple). This argument corresponds to the 'gauss' and 'box' smoothing kernels. kernel : { 'gauss' , 'box' , numpy.array }, optional Default kernel used for smoothing is 'gauss'. The user can specify if they would prefer 'gauss', 'box', or a custom kernel. All kernels are normalized to ensure flux retention. overlap str, optional Whether to include only contours that overlap with the image area. This significantly speeds up the drawing of contours and reduces file size when using a file for the contours covering a much larger area than the image. kwargs Additional keyword arguments (such as alpha, linewidths, or linestyles) will be passed on directly to Matplotlib's :meth:`~matplotlib.axes.Axes.contour` or :meth:`~matplotlib.axes.Axes.contourf` methods. For more information on these additional arguments, see the *Optional keyword arguments* sections in the documentation for those methods. ''' if layer: self.remove_layer(layer, raise_exception=False) if cmap: cmap = mpl.cm.get_cmap(cmap) elif not colors: cmap = mpl.cm.get_cmap('jet') if data is not None: data_contour, header_contour, wcs_contour = self._get_hdu(data, \ hdu, False, convention=convention, dimensions=dimensions, \ slices=slices) else: data_contour = self._data header_contour = self._header wcs_contour = self._wcs wcs_contour.nx = header_contour['NAXIS%i' % (dimensions[0] + 1)] wcs_contour.ny = header_contour['NAXIS%i' % (dimensions[1] + 1)] image_contour = convolve_util.convolve(data_contour, smooth=smooth, kernel=kernel) extent_contour = (0.5, wcs_contour.nx + 0.5, 0.5, wcs_contour.ny + 0.5) if type(levels) == int: auto_levels = image_util.percentile_function(image_contour) vmin = auto_levels(0.25) vmax = auto_levels(99.75) levels = np.linspace(vmin, vmax, levels) if filled: c = self._ax1.contourf(image_contour, levels, extent=extent_contour, cmap=cmap, colors=colors, **kwargs) else: c = self._ax1.contour(image_contour, levels, extent=extent_contour, cmap=cmap, colors=colors, **kwargs) if layer: contour_set_name = layer else: self._contour_counter += 1 contour_set_name = 'contour_set_' + str(self._contour_counter) contour_util.transform(c, wcs_contour, self._wcs, filled=filled, overlap=overlap) self._layers[contour_set_name] = c if returnlevels: return levels @auto_refresh def show_vectors(self, pdata, adata, phdu=0, ahdu=0, step=1, scale=1, rotate=0, cutoff=0, units='degrees', layer=None, convention=None, dimensions=[0, 1], slices=[], **kwargs): ''' Overlay vectors on the current plot. Parameters ---------- pdata : see below The FITS file specifying the magnitude of vectors. The following data types can be passed: string astropy.io.fits.PrimaryHDU astropy.io.fits.ImageHDU pyfits.PrimaryHDU pyfits.ImageHDU astropy.wcs.WCS np.ndarray adata : see below The FITS file specifying the angle of vectors. The following data types can be passed: string astropy.io.fits.PrimaryHDU astropy.io.fits.ImageHDU pyfits.PrimaryHDU pyfits.ImageHDU astropy.wcs.WCS np.ndarray phdu : int, optional By default, the image in the primary HDU is read in. If a different HDU is required for pdata, use this argument. ahdu : int, optional By default, the image in the primary HDU is read in. If a different HDU is required for adata, use this argument. step : int, optional Derive a vector only from every 'step' pixels. You will normally want this to be >1 to get sensible vector spacing. scale : int, optional The length, in pixels, of a vector with magnitude 1 in the image specified by pdata. If pdata specifies fractional polarization, make this comparable to step. rotate : float, optional An angle to rotate by, in units the same as those of the angle map. cutoff : float, optional The value of magnitude below which no vectors should be plotted. The default value, zero, excludes negative-length and NaN-masked data. units : str, optional Units to assume for the angle map. Valid values are 'degrees' (the default) or 'radians' (or anything else), which will not apply a scaling factor of pi/180 to the angle data. layer : str, optional The name of the vector layer. This is useful for giving custom names to layers (instead of vector_set_n) and for replacing existing layers. convention : str, optional This is used in cases where a FITS header can be interpreted in multiple ways. For example, for files with a -CAR projection and CRVAL2=0, this can be set to 'wells' or 'calabretta' to choose the appropriate convention. dimensions : tuple or list, optional The index of the axes to use if the data has more than three dimensions. slices : tuple or list, optional If a FITS file with more than two dimensions is specified, then these are the slices to extract. If all extra dimensions only have size 1, then this is not required. kwargs Additional keyword arguments (such as alpha, linewidths, or color) which are passed to Matplotlib's :class:`~matplotlib.collections.LineCollection` class, and can be used to control the appearance of the lines. For more information on these additional arguments, see the *Optional keyword arguments* sections in the documentation for those methods. ''' # over-ride default color (none) that will otherwise be set by #show_lines() if not 'color' in kwargs: kwargs.setdefault('color', 'black') if layer: self.remove_layer(layer, raise_exception=False) data_p, header_p, wcs_p = self._get_hdu(pdata, phdu, False, \ convention=convention, dimensions=dimensions, slices=slices) data_a, header_a, wcs_a = self._get_hdu(adata, ahdu, False, \ convention=convention, dimensions=dimensions, slices=slices) wcs_p.nx = header_p['NAXIS%i' % (dimensions[0] + 1)] wcs_p.ny = header_p['NAXIS%i' % (dimensions[1] + 1)] wcs_a.nx = header_a['NAXIS%i' % (dimensions[0] + 1)] wcs_a.ny = header_a['NAXIS%i' % (dimensions[1] + 1)] if (wcs_p.nx!=wcs_a.nx or wcs_p.ny!=wcs_a.ny): raise Exception("Angle and magnitude images must be same size") angle = data_a + rotate if units == 'degrees': angle = np.radians(angle) linelist=[] for y in range(0,wcs_p.ny,step): for x in range(0,wcs_p.nx,step): if data_p[y,x]>cutoff and np.isfinite(angle[y,x]): r=data_p[y,x]*0.5*scale a=angle[y,x] x1=1 + x + r*np.sin(a) y1=1 + y - r*np.cos(a) x2=1 + x - r*np.sin(a) y2=1 + y + r*np.cos(a) x_world,y_world=wcs_util.pix2world(wcs_p, [x1,x2],[y1,y2] ) line=np.array([x_world,y_world]) linelist.append(line) if layer: vector_set_name = layer else: self._vector_counter += 1 vector_set_name = 'vector_set_' + str(self._vector_counter) # Use show_lines to finish the process off self.show_lines(linelist,layer=vector_set_name,**kwargs) # This method plots markers. The input should be an Nx2 array with WCS coordinates # in degree format. @auto_refresh def show_markers(self, xw, yw, layer=False, **kwargs): ''' Overlay markers on the current plot. Parameters ---------- xw : list or `~numpy.ndarray` The x positions of the markers (in world coordinates) yw : list or `~numpy.ndarray` The y positions of the markers (in world coordinates) layer : str, optional The name of the scatter layer. This is useful for giving custom names to layers (instead of marker_set_n) and for replacing existing layers. kwargs Additional keyword arguments (such as marker, facecolor, edgecolor, alpha, or linewidth) will be passed on directly to Matplotlib's :meth:`~matplotlib.axes.Axes.scatter` method (in particular, have a look at the *Optional keyword arguments* in the documentation for that method). ''' if not 'c' in kwargs: kwargs.setdefault('edgecolor', 'red') kwargs.setdefault('facecolor', 'none') kwargs.setdefault('s', 30) if layer: self.remove_layer(layer, raise_exception=False) xp, yp = wcs_util.world2pix(self._wcs, xw, yw) s = self._ax1.scatter(xp, yp, **kwargs) if layer: marker_set_name = layer else: self._scatter_counter += 1 marker_set_name = 'marker_set_' + str(self._scatter_counter) self._layers[marker_set_name] = s # Show circles. Different from markers as this method allows more definitions # for the circles. @auto_refresh def show_circles(self, xw, yw, radius, layer=False, zorder=None, **kwargs): ''' Overlay circles on the current plot. Parameters ---------- xw : list or `~numpy.ndarray` The x positions of the centers of the circles (in world coordinates) yw : list or `~numpy.ndarray` The y positions of the centers of the circles (in world coordinates) radius : int or float or list or `~numpy.ndarray` The radii of the circles (in world coordinates) layer : str, optional The name of the circle layer. This is useful for giving custom names to layers (instead of circle_set_n) and for replacing existing layers. kwargs Additional keyword arguments (such as facecolor, edgecolor, alpha, or linewidth) are passed to Matplotlib :class:`~matplotlib.collections.PatchCollection` class, and can be used to control the appearance of the circles. ''' if np.isscalar(xw): xw = np.array([xw]) else: xw = np.array(xw) if np.isscalar(yw): yw = np.array([yw]) else: yw = np.array(yw) if np.isscalar(radius): radius = np.repeat(radius, len(xw)) else: radius = np.array(radius) if not 'facecolor' in kwargs: kwargs.setdefault('facecolor', 'none') if layer: self.remove_layer(layer, raise_exception=False) xp, yp = wcs_util.world2pix(self._wcs, xw, yw) rp = radius / wcs_util.celestial_pixel_scale(self._wcs) patches = [] for i in range(len(xp)): patches.append(Circle((xp[i], yp[i]), radius=rp[i])) # Due to bugs in matplotlib, we need to pass the patch properties # directly to the PatchCollection rather than use match_original. p = PatchCollection(patches, **kwargs) if zorder is not None: p.zorder = zorder c = self._ax1.add_collection(p) if layer: circle_set_name = layer else: self._circle_counter += 1 circle_set_name = 'circle_set_' + str(self._circle_counter) self._layers[circle_set_name] = c @auto_refresh def show_ellipses(self, xw, yw, width, height, angle=0, layer=False, zorder=None, **kwargs): ''' Overlay ellipses on the current plot. Parameters ---------- xw : list or `~numpy.ndarray` The x positions of the centers of the ellipses (in world coordinates) yw : list or `~numpy.ndarray` The y positions of the centers of the ellipses (in world coordinates) width : int or float or list or `~numpy.ndarray` The width of the ellipse (in world coordinates) height : int or float or list or `~numpy.ndarray` The height of the ellipse (in world coordinates) angle : int or float or list or `~numpy.ndarray`, optional rotation in degrees (anti-clockwise). Default angle is 0.0. layer : str, optional The name of the ellipse layer. This is useful for giving custom names to layers (instead of ellipse_set_n) and for replacing existing layers. kwargs Additional keyword arguments (such as facecolor, edgecolor, alpha, or linewidth) are passed to Matplotlib :class:`~matplotlib.collections.PatchCollection` class, and can be used to control the appearance of the ellipses. ''' if np.isscalar(xw): xw = np.array([xw]) else: xw = np.array(xw) if np.isscalar(yw): yw = np.array([yw]) else: yw = np.array(yw) if np.isscalar(width): width = np.repeat(width, len(xw)) else: width = np.array(width) if np.isscalar(angle): angle = np.repeat(angle, len(xw)) else: angle = np.array(angle) if np.isscalar(height): height = np.repeat(height, len(xw)) else: height = np.array(height) if not 'facecolor' in kwargs: kwargs.setdefault('facecolor', 'none') if layer: self.remove_layer(layer, raise_exception=False) xp, yp = wcs_util.world2pix(self._wcs, xw, yw) wp = width / wcs_util.celestial_pixel_scale(self._wcs) hp = height / wcs_util.celestial_pixel_scale(self._wcs) ap = angle patches = [] for i in range(len(xp)): patches.append(Ellipse((xp[i], yp[i]), width=wp[i], height=hp[i], angle=ap[i])) # Due to bugs in matplotlib, we need to pass the patch properties # directly to the PatchCollection rather than use match_original. p = PatchCollection(patches, **kwargs) if zorder is not None: p.zorder = zorder c = self._ax1.add_collection(p) if layer: ellipse_set_name = layer else: self._ellipse_counter += 1 ellipse_set_name = 'ellipse_set_' + str(self._ellipse_counter) self._layers[ellipse_set_name] = c @auto_refresh def show_rectangles(self, xw, yw, width, height, layer=False, zorder=None, **kwargs): ''' Overlay rectangles on the current plot. Parameters ---------- xw : list or `~numpy.ndarray` The x positions of the centers of the rectangles (in world coordinates) yw : list or `~numpy.ndarray` The y positions of the centers of the rectangles (in world coordinates) width : int or float or list or `~numpy.ndarray` The width of the rectangle (in world coordinates) height : int or float or list or `~numpy.ndarray` The height of the rectangle (in world coordinates) layer : str, optional The name of the rectangle layer. This is useful for giving custom names to layers (instead of rectangle_set_n) and for replacing existing layers. kwargs Additional keyword arguments (such as facecolor, edgecolor, alpha, or linewidth) are passed to Matplotlib :class:`~matplotlib.collections.PatchCollection` class, and can be used to control the appearance of the rectangles. ''' if np.isscalar(xw): xw = np.array([xw]) else: xw = np.array(xw) if np.isscalar(yw): yw = np.array([yw]) else: yw = np.array(yw) if np.isscalar(width): width = np.repeat(width, len(xw)) else: width = np.array(width) if np.isscalar(height): height = np.repeat(height, len(xw)) else: height = np.array(height) if not 'facecolor' in kwargs: kwargs.setdefault('facecolor', 'none') if layer: self.remove_layer(layer, raise_exception=False) xp, yp = wcs_util.world2pix(self._wcs, xw, yw) wp = width / wcs_util.celestial_pixel_scale(self._wcs) hp = height / wcs_util.celestial_pixel_scale(self._wcs) patches = [] xp = xp - wp / 2. yp = yp - hp / 2. for i in range(len(xp)): patches.append(Rectangle((xp[i], yp[i]), width=wp[i], height=hp[i])) # Due to bugs in matplotlib, we need to pass the patch properties # directly to the PatchCollection rather than use match_original. p = PatchCollection(patches, **kwargs) if zorder is not None: p.zorder = zorder c = self._ax1.add_collection(p) if layer: rectangle_set_name = layer else: self._rectangle_counter += 1 rectangle_set_name = 'rectangle_set_' + str(self._rectangle_counter) self._layers[rectangle_set_name] = c @auto_refresh def show_lines(self, line_list, layer=False, zorder=None, **kwargs): ''' Overlay lines on the current plot. Parameters ---------- line_list : list A list of one or more 2xN numpy arrays which contain the [x, y] positions of the vertices in world coordinates. layer : str, optional The name of the line(s) layer. This is useful for giving custom names to layers (instead of line_set_n) and for replacing existing layers. kwargs Additional keyword arguments (such as color, offsets, linestyle, or linewidth) are passed to Matplotlib :class:`~matplotlib.collections.LineCollection` class, and can be used to control the appearance of the lines. ''' if not 'color' in kwargs: kwargs.setdefault('color', 'none') if layer: self.remove_layer(layer, raise_exception=False) lines = [] for line in line_list: xp, yp = wcs_util.world2pix(self._wcs, line[0, :], line[1, :]) lines.append(np.column_stack((xp, yp))) l = LineCollection(lines, **kwargs) if zorder is not None: l.zorder = zorder c = self._ax1.add_collection(l) if layer: line_set_name = layer else: self._linelist_counter += 1 line_set_name = 'line_set_' + str(self._linelist_counter) self._layers[line_set_name] = c @auto_refresh def show_arrows(self, x, y, dx, dy, width='auto', head_width='auto', head_length='auto', length_includes_head=True, layer=False, zorder=None, **kwargs): ''' Overlay arrows on the current plot. Parameters ---------- x, y, dx, dy : float or list or `~numpy.ndarray` Origin and displacement of the arrows in world coordinates. These can either be scalars to plot a single arrow, or lists or arrays to plot multiple arrows. width : float, optional The width of the arrow body, in pixels (default: 2% of the arrow length) head_width : float, optional The width of the arrow head, in pixels (default: 5% of the arrow length) head_length : float, optional The length of the arrow head, in pixels (default: 5% of the arrow length) length_includes_head : bool, optional Whether the head includes the length layer : str, optional The name of the arrow(s) layer. This is useful for giving custom names to layers (instead of line_set_n) and for replacing existing layers. kwargs Additional keyword arguments (such as facecolor, edgecolor, alpha, or linewidth) are passed to Matplotlib :class:`~matplotlib.collections.PatchCollection` class, and can be used to control the appearance of the arrows. ''' if layer: self.remove_layer(layer, raise_exception=False) arrows = [] if np.isscalar(x): x, y, dx, dy = [x], [y], [dx], [dy] for i in range(len(x)): xp1, yp1 = wcs_util.world2pix(self._wcs, x[i], y[i]) xp2, yp2 = wcs_util.world2pix(self._wcs, x[i] + dx[i], y[i] + dy[i]) if width == 'auto': width = 0.02 * np.sqrt((xp2 - xp1) ** 2 + (yp2 - yp1) ** 2) if head_width == 'auto': head_width = 0.1 * np.sqrt((xp2 - xp1) ** 2 + (yp2 - yp1) ** 2) if head_length == 'auto': head_length = 0.1 * np.sqrt((xp2 - xp1) ** 2 + (yp2 - yp1) ** 2) arrows.append(FancyArrow(xp1, yp1, xp2 - xp1, yp2 - yp1, width=width, head_width=head_width, head_length=head_length, length_includes_head=length_includes_head) ) # Due to bugs in matplotlib, we need to pass the patch properties # directly to the PatchCollection rather than use match_original. p = PatchCollection(arrows, **kwargs) if zorder is not None: p.zorder = zorder c = self._ax1.add_collection(p) if layer: line_set_name = layer else: self._linelist_counter += 1 line_set_name = 'arrow_set_' + str(self._linelist_counter) self._layers[line_set_name] = c @auto_refresh def show_polygons(self, polygon_list, layer=False, zorder=None, **kwargs): ''' Overlay polygons on the current plot. Parameters ---------- polygon_list : list or tuple A list of one or more 2xN or Nx2 Numpy arrays which contain the [x, y] positions of the vertices in world coordinates. Note that N should be greater than 2. layer : str, optional The name of the circle layer. This is useful for giving custom names to layers (instead of circle_set_n) and for replacing existing layers. kwargs Additional keyword arguments (such as facecolor, edgecolor, alpha, or linewidth) are passed to Matplotlib :class:`~matplotlib.collections.PatchCollection` class, and can be used to control the appearance of the polygons. ''' if not 'facecolor' in kwargs: kwargs.setdefault('facecolor', 'none') if layer: self.remove_layer(layer, raise_exception=False) if type(polygon_list) not in [list, tuple]: raise Exception("polygon_list should be a list or tuple of Numpy arrays") pix_polygon_list = [] for polygon in polygon_list: if type(polygon) is not np.ndarray: raise Exception("Polygon should be given as a Numpy array") if polygon.shape[0] == 2 and polygon.shape[1] > 2: xw = polygon[0, :] yw = polygon[1, :] elif polygon.shape[0] > 2 and polygon.shape[1] == 2: xw = polygon[:, 0] yw = polygon[:, 1] else: raise Exception("Polygon should have dimensions 2xN or Nx2 with N>2") xp, yp = wcs_util.world2pix(self._wcs, xw, yw) pix_polygon_list.append(np.column_stack((xp, yp))) patches = [] for i in range(len(pix_polygon_list)): patches.append(Polygon(pix_polygon_list[i], **kwargs)) # Due to bugs in matplotlib, we need to pass the patch properties # directly to the PatchCollection rather than use match_original. p = PatchCollection(patches, **kwargs) if zorder is not None: p.zorder = zorder c = self._ax1.add_collection(p) if layer: poly_set_name = layer else: self._poly_counter += 1 poly_set_name = 'poly_set_' + str(self._poly_counter) self._layers[poly_set_name] = c @auto_refresh @fixdocstring def add_label(self, x, y, text, relative=False, color='black', family=None, style=None, variant=None, stretch=None, weight=None, size=None, fontproperties=None, horizontalalignment='center', verticalalignment='center', layer=None, **kwargs): ''' Add a text label. Parameters ---------- x, y : float Coordinates of the text label text : str The label relative : str, optional Whether the coordinates are to be interpreted as world coordinates (e.g. RA/Dec or longitude/latitude), or coordinates relative to the axes (where 0.0 is left or bottom and 1.0 is right or top). common: color, family, style, variant, stretch, weight, size, fontproperties, horizontalalignment, verticalalignment ''' if layer: self.remove_layer(layer, raise_exception=False) # Can't pass fontproperties=None to text. Only pass it if it is not None. if fontproperties: kwargs['fontproperties'] = fontproperties if not np.isscalar(x): raise Exception("x should be a single value") if not np.isscalar(y): raise Exception("y should be a single value") if not np.isscalar(text): raise Exception("text should be a single value") if relative: l = self._ax1.text(x, y, text, color=color, family=family, style=style, variant=variant, stretch=stretch, weight=weight, size=size, horizontalalignment=horizontalalignment, verticalalignment=verticalalignment, transform=self._ax1.transAxes, **kwargs) else: xp, yp = wcs_util.world2pix(self._wcs, x, y) l = self._ax1.text(xp, yp, text, color=color, family=family, style=style, variant=variant, stretch=stretch, weight=weight, size=size, horizontalalignment=horizontalalignment, verticalalignment=verticalalignment, **kwargs) if layer: label_name = layer else: self._label_counter += 1 label_name = 'label_' + str(self._label_counter) self._layers[label_name] = l def set_auto_refresh(self, refresh): ''' Set whether the display should refresh after each method call. Parameters ---------- refresh : bool Whether to refresh the display every time a FITSFigure method is called. This defaults to `True` if and only if APLpy is being used from IPython and the Matplotlib backend is interactive. ''' if refresh is None: if matplotlib.is_interactive(): try: get_ipython() except NameError: refresh = False else: refresh = True else: refresh = False elif not isinstance(refresh, bool): raise TypeError("refresh argument should be boolean or `None`") self._parameters.auto_refresh = refresh def refresh(self, force=True): ''' Refresh the display. Parameters ---------- force : str, optional If set to False, refresh() will only have an effect if auto refresh is on. If set to True, the display will be refreshed whatever the auto refresh setting is set to. The default is True. ''' if self._parameters.auto_refresh or force: self._figure.canvas.draw() def save(self, filename, dpi=None, transparent=False, adjust_bbox=True, max_dpi=300, format=None): ''' Save the current figure to a file. Parameters ---------- filename : str or fileobj The name of the file to save the plot to. This can be for example a PS, EPS, PDF, PNG, JPEG, or SVG file. Note that it is also possible to pass file-like object. dpi : float, optional The output resolution, in dots per inch. If the output file is a vector graphics format (such as PS, EPS, PDF or SVG) only the image itself will be rasterized. If the output is a PS or EPS file and no dpi is specified, the dpi is automatically calculated to match the resolution of the image. If this value is larger than max_dpi, then dpi is set to max_dpi. transparent : str, optional Whether to preserve transparency adjust_bbox : str, optional Auto-adjust the bounding box for the output max_dpi : float, optional The maximum resolution to output images at. If no maximum is wanted, enter None or 0. format : str, optional By default, APLpy tries to guess the file format based on the file extension, but the format can also be specified explicitly. Should be one of 'eps', 'ps', 'pdf', 'svg', 'png'. ''' if isinstance(filename, six.string_types) and format is None: format = os.path.splitext(filename)[1].lower()[1:] if dpi is None and format in ['eps', 'ps', 'pdf']: width = self._ax1.get_position().width * self._figure.get_figwidth() interval = self._ax1.xaxis.get_view_interval() nx = interval[1] - interval[0] if max_dpi: dpi = np.minimum(nx / width, max_dpi) else: dpi = nx / width log.info("Auto-setting resolution to %g dpi" % dpi) artists = [] if adjust_bbox: for artist in self._layers.values(): if isinstance(artist, matplotlib.text.Text): artists.append(artist) self._figure.savefig(filename, dpi=dpi, transparent=transparent, bbox_inches='tight', bbox_extra_artists=artists, format=format) else: self._figure.savefig(filename, dpi=dpi, transparent=transparent, format=format) def _initialize_view(self): self._ax1.xaxis.set_view_interval(+0.5, self._wcs.nx + 0.5, ignore=True) self._ax1.yaxis.set_view_interval(+0.5, self._wcs.ny + 0.5, ignore=True) self._ax2.xaxis.set_view_interval(+0.5, self._wcs.nx + 0.5, ignore=True) self._ax2.yaxis.set_view_interval(+0.5, self._wcs.ny + 0.5, ignore=True) # set the image extent to FITS pixel coordinates self._extent = (0.5, self._wcs.nx + 0.5, 0.5, self._wcs.ny + 0.5) def _get_invert_default(self): return self._figure.apl_grayscale_invert_default def _get_colormap_default(self): return self._figure.apl_colorscale_cmap_default @auto_refresh def set_theme(self, theme): ''' Set the axes, ticks, grid, and image colors to a certain style (experimental). Parameters ---------- theme : str The theme to use. At the moment, this can be 'pretty' (for viewing on-screen) and 'publication' (which makes the ticks and grid black, and displays the image in inverted grayscale) ''' if theme == 'pretty': self.frame.set_color('black') self.frame.set_linewidth(1.0) self.ticks.set_color('white') self.ticks.set_length(7) self._figure.apl_grayscale_invert_default = False self._figure.apl_colorscale_cmap_default = 'jet' if self.image: self.image.set_cmap(cmap=mpl.cm.get_cmap('jet')) elif theme == 'publication': self.frame.set_color('black') self.frame.set_linewidth(1.0) self.ticks.set_color('black') self.ticks.set_length(7) self._figure.apl_grayscale_invert_default = True self._figure.apl_colorscale_cmap_default = 'gist_heat' if self.image: self.image.set_cmap(cmap=mpl.cm.get_cmap('gist_yarg')) def world2pixel(self, xw, yw): ''' Convert world to pixel coordinates. Parameters ---------- xw : float or list or `~numpy.ndarray` x world coordinate yw : float or list or `~numpy.ndarray` y world coordinate Returns ------- xp : float or list or `~numpy.ndarray` x pixel coordinate yp : float or list or `~numpy.ndarray` y pixel coordinate ''' return wcs_util.world2pix(self._wcs, xw, yw) def pixel2world(self, xp, yp): ''' Convert pixel to world coordinates. Parameters ---------- xp : float or list or `~numpy.ndarray` x pixel coordinate yp : float or list or `~numpy.ndarray` y pixel coordinate Returns ------- xw : float or list or `~numpy.ndarray` x world coordinate yw : float or list or `~numpy.ndarray` y world coordinate ''' return wcs_util.pix2world(self._wcs, xp, yp) @auto_refresh def add_grid(self, *args, **kwargs): ''' Add a coordinate to the current figure. Once this method has been run, a grid attribute becomes available, and can be used to control the aspect of the grid:: >>> f = aplpy.FITSFigure(...) >>> ... >>> f.add_grid() >>> f.grid.set_color('white') >>> f.grid.set_alpha(0.5) >>> ... ''' if hasattr(self, 'grid'): raise Exception("Grid already exists") try: self.grid = Grid(self) self.grid.show(*args, **kwargs) except: del self.grid raise @auto_refresh def remove_grid(self): ''' Removes the grid from the current figure. ''' self.grid._remove() del self.grid @auto_refresh def add_beam(self, *args, **kwargs): ''' Add a beam to the current figure. Once this method has been run, a beam attribute becomes available, and can be used to control the aspect of the beam:: >>> f = aplpy.FITSFigure(...) >>> ... >>> f.add_beam() >>> f.beam.set_color('white') >>> f.beam.set_hatch('+') >>> ... If more than one beam is added, the beam object becomes a list. In this case, to control the aspect of one of the beams, you will need to specify the beam index:: >>> ... >>> f.beam[2].set_hatch('/') >>> ... ''' # Initalize the beam and set parameters b = Beam(self) b.show(*args, **kwargs) if hasattr(self, 'beam'): if type(self.beam) is list: self.beam.append(b) else: self.beam = [self.beam, b] else: self.beam = b @auto_refresh def remove_beam(self, beam_index=None): ''' Removes the beam from the current figure. If more than one beam is present, the index of the beam should be specified using beam_index= ''' if type(self.beam) is list: if beam_index is None: raise Exception("More than one beam present - use beam_index= to specify which one to remove") else: b = self.beam.pop(beam_index) b._remove() del b # If only one beam is present, remove containing list if len(self.beam) == 1: self.beam = self.beam[0] else: self.beam._remove() del self.beam @auto_refresh def add_scalebar(self, length, *args, **kwargs): ''' Add a scalebar to the current figure. Once this method has been run, a scalebar attribute becomes available, and can be used to control the aspect of the scalebar:: >>> f = aplpy.FITSFigure(...) >>> ... >>> f.add_scalebar(0.01) # length has to be specified >>> f.scalebar.set_label('100 AU') >>> ... ''' if hasattr(self, 'scalebar'): raise Exception("Scalebar already exists") try: self.scalebar = Scalebar(self) self.scalebar.show(length, *args, **kwargs) except: del self.scalebar raise @auto_refresh def remove_scalebar(self): ''' Removes the scalebar from the current figure. ''' self.scalebar._remove() del self.scalebar @auto_refresh def add_colorbar(self, *args, **kwargs): ''' Add a colorbar to the current figure. Once this method has been run, a colorbar attribute becomes available, and can be used to control the aspect of the colorbar:: >>> f = aplpy.FITSFigure(...) >>> ... >>> f.add_colorbar() >>> f.colorbar.set_width(0.3) >>> f.colorbar.set_location('top') >>> ... ''' if hasattr(self, 'colorbar'): raise Exception("Colorbar already exists") if self.image is None: raise Exception("No image is shown, so a colorbar cannot be displayed") try: self.colorbar = Colorbar(self) self.colorbar.show(*args, **kwargs) except: del self.colorbar raise @auto_refresh def remove_colorbar(self): ''' Removes the colorbar from the current figure. ''' self.colorbar._remove() del self.colorbar def close(self): ''' Close the figure and free up the memory. ''' mpl.close(self._figure) APLpy-1.0/aplpy/decorator.py0000644000077000000240000002467212414215370015750 0ustar tomstaff00000000000000from __future__ import print_function ########################## LICENCE ############################### # Copyright (c) 2005-2012, Michele Simionato # All rights reserved. # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # Redistributions in bytecode form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. """ Decorator module, see http://pypi.python.org/pypi/decorator for the documentation. """ __version__ = '3.4.0' __all__ = ["decorator", "FunctionMaker", "contextmanager"] import sys, re, inspect if sys.version >= '3': from inspect import getfullargspec def get_init(cls): return cls.__init__ else: class getfullargspec(object): "A quick and dirty replacement for getfullargspec for Python 2.X" def __init__(self, f): self.args, self.varargs, self.varkw, self.defaults = \ inspect.getargspec(f) self.kwonlyargs = [] self.kwonlydefaults = None def __iter__(self): yield self.args yield self.varargs yield self.varkw yield self.defaults def get_init(cls): return cls.__init__.__func__ DEF = re.compile('\s*def\s*([_\w][_\w\d]*)\s*\(') # basic functionality class FunctionMaker(object): """ An object with the ability to create functions with a given signature. It has attributes name, doc, module, signature, defaults, dict and methods update and make. """ def __init__(self, func=None, name=None, signature=None, defaults=None, doc=None, module=None, funcdict=None): self.shortsignature = signature if func: # func can be a class or a callable, but not an instance method self.name = func.__name__ if self.name == '': # small hack for lambda functions self.name = '_lambda_' self.doc = func.__doc__ self.module = func.__module__ if inspect.isfunction(func): argspec = getfullargspec(func) self.annotations = getattr(func, '__annotations__', {}) for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs', 'kwonlydefaults'): setattr(self, a, getattr(argspec, a)) for i, arg in enumerate(self.args): setattr(self, 'arg%d' % i, arg) if sys.version < '3': # easy way self.shortsignature = self.signature = \ inspect.formatargspec( formatvalue=lambda val: "", *argspec)[1:-1] else: # Python 3 way allargs = list(self.args) allshortargs = list(self.args) if self.varargs: allargs.append('*' + self.varargs) allshortargs.append('*' + self.varargs) elif self.kwonlyargs: allargs.append('*') # single star syntax for a in self.kwonlyargs: allargs.append('%s=None' % a) allshortargs.append('%s=%s' % (a, a)) if self.varkw: allargs.append('**' + self.varkw) allshortargs.append('**' + self.varkw) self.signature = ', '.join(allargs) self.shortsignature = ', '.join(allshortargs) self.dict = func.__dict__.copy() # func=None happens when decorating a caller if name: self.name = name if signature is not None: self.signature = signature if defaults: self.defaults = defaults if doc: self.doc = doc if module: self.module = module if funcdict: self.dict = funcdict # check existence required attributes assert hasattr(self, 'name') if not hasattr(self, 'signature'): raise TypeError('You are decorating a non function: %s' % func) def update(self, func, **kw): "Update the signature of func with the data in self" func.__name__ = self.name func.__doc__ = getattr(self, 'doc', None) func.__dict__ = getattr(self, 'dict', {}) func.__defaults__ = getattr(self, 'defaults', ()) func.__kwdefaults__ = getattr(self, 'kwonlydefaults', None) func.__annotations__ = getattr(self, 'annotations', None) callermodule = sys._getframe(3).f_globals.get('__name__', '?') func.__module__ = getattr(self, 'module', callermodule) func.__dict__.update(kw) def make(self, src_templ, evaldict=None, addsource=False, **attrs): "Make a new function from a given template and update the signature" src = src_templ % vars(self) # expand name and signature evaldict = evaldict or {} mo = DEF.match(src) if mo is None: raise SyntaxError('not a valid function template\n%s' % src) name = mo.group(1) # extract the function name names = set([name] + [arg.strip(' *') for arg in self.shortsignature.split(',')]) for n in names: if n in ('_func_', '_call_'): raise NameError('%s is overridden in\n%s' % (n, src)) if not src.endswith('\n'): # add a newline just for safety src += '\n' # this is needed in old versions of Python try: code = compile(src, '', 'single') # print >> sys.stderr, 'Compiling %s' % src exec(code, evaldict) except: print('Error in generated code:', file=sys.stderr) print(src, file=sys.stderr) raise func = evaldict[name] if addsource: attrs['__source__'] = src self.update(func, **attrs) return func @classmethod def create(cls, obj, body, evaldict, defaults=None, doc=None, module=None, addsource=True, **attrs): """ Create a function from the strings name, signature and body. evaldict is the evaluation dictionary. If addsource is true an attribute __source__ is added to the result. The attributes attrs are added, if any. """ if isinstance(obj, str): # "name(signature)" name, rest = obj.strip().split('(', 1) signature = rest[:-1] #strip a right parens func = None else: # a function name = None signature = None func = obj self = cls(func, name, signature, defaults, doc, module) ibody = '\n'.join(' ' + line for line in body.splitlines()) return self.make('def %(name)s(%(signature)s):\n' + ibody, evaldict, addsource, **attrs) def decorator(caller, func=None): """ decorator(caller) converts a caller function into a decorator; decorator(caller, func) decorates a function using a caller. """ if func is not None: # returns a decorated function evaldict = func.__globals__.copy() evaldict['_call_'] = caller evaldict['_func_'] = func return FunctionMaker.create( func, "return _call_(_func_, %(shortsignature)s)", evaldict, undecorated=func, __wrapped__=func) else: # returns a decorator if inspect.isclass(caller): name = caller.__name__.lower() callerfunc = get_init(caller) doc = 'decorator(%s) converts functions/generators into ' \ 'factories of %s objects' % (caller.__name__, caller.__name__) fun = getfullargspec(callerfunc).args[1] # second arg elif inspect.isfunction(caller): name = '_lambda_' if caller.__name__ == '' \ else caller.__name__ callerfunc = caller doc = caller.__doc__ fun = getfullargspec(callerfunc).args[0] # first arg else: # assume caller is an object with a __call__ method name = caller.__class__.__name__.lower() callerfunc = caller.__call__.__func__ doc = caller.__call__.__doc__ fun = getfullargspec(callerfunc).args[1] # second arg evaldict = callerfunc.__globals__.copy() evaldict['_call_'] = caller evaldict['decorator'] = decorator return FunctionMaker.create( '%s(%s)' % (name, fun), 'return decorator(_call_, %s)' % fun, evaldict, undecorated=caller, __wrapped__=caller, doc=doc, module=caller.__module__) ######################### contextmanager ######################## def __call__(self, func): 'Context manager decorator' return FunctionMaker.create( func, "with _self_: return _func_(%(shortsignature)s)", dict(_self_=self, _func_=func), __wrapped__=func) try: # Python >= 3.2 from contextlib import _GeneratorContextManager ContextManager = type( 'ContextManager', (_GeneratorContextManager,), dict(__call__=__call__)) except ImportError: # Python >= 2.5 from contextlib import GeneratorContextManager def __init__(self, f, *a, **k): return GeneratorContextManager.__init__(self, f(*a, **k)) ContextManager = type( 'ContextManager', (GeneratorContextManager,), dict(__call__=__call__, __init__=__init__)) contextmanager = decorator(ContextManager) APLpy-1.0/aplpy/decorators.py0000644000077000000240000000540412414762161016130 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import threading from .decorator import decorator mydata = threading.local() def auto_refresh(f): return decorator(_auto_refresh, f) def _auto_refresh(f, *args, **kwargs): if 'refresh' in kwargs: refresh = kwargs.pop('refresh') else: refresh = True # The following is necessary rather than using mydata.nesting = 0 at the # start of the file, because doing the latter caused issues with the Django # development server. mydata.nesting = getattr(mydata, 'nesting', 0) + 1 try: return f(*args, **kwargs) finally: mydata.nesting -= 1 if hasattr(args[0], '_figure'): if refresh and mydata.nesting == 0 and args[0]._parameters.auto_refresh: args[0]._figure.canvas.draw() doc = {} doc['size'] = '''size : str or int or float, optional The size of the font. This can either be a numeric value (e.g. 12), giving the size in points, or one of 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', or 'xx-large'. ''' doc['weight'] = '''weight : str or int or float, optional The weight (or boldness) of the font. This can either be a numeric value in the range 0-1000 or one of 'ultralight', 'light', 'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black'. ''' doc['stretch'] = '''stretch : str or int or float, optional The stretching (spacing between letters) for the font. This can either be a numeric value in the range 0-1000 or one of 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded' or 'ultra-expanded'. ''' doc['family'] = '''family : str, optional The family of the font to use. This can either be a generic font family name, either 'serif', 'sans-serif', 'cursive', 'fantasy', or 'monospace', or a list of font names in decreasing order of priority. ''' doc['style'] = '''style : str, optional The font style. This can be 'normal', 'italic' or 'oblique'. ''' doc['variant'] = '''variant : str, optional The font variant. This can be 'normal' or 'small-caps' ''' def fixdocstring(func): lines = func.__doc__.split('\n') for i, line in enumerate(lines): if 'common:' in line: break header = lines[:i] footer = lines[i + 1:] indent = lines[i].index('common:') common = [] for item in lines[i].split(':')[1].split(','): if item.strip() in doc: common.append(" " * indent + doc[item.strip()].replace('\n', '\n' + " " * indent)) docstring = "\n".join(header + common + footer) func.__doc__ = docstring return func APLpy-1.0/aplpy/deprecated.py0000644000077000000240000002617412471065325016073 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import warnings from .decorators import auto_refresh class Deprecated(object): @auto_refresh def set_tick_xspacing(self, *args, **kwargs): warnings.warn("set_tick_xspacing is deprecated - use ticks.set_xspacing instead", DeprecationWarning) self.ticks.set_xspacing(*args, **kwargs) @auto_refresh def set_tick_yspacing(self, *args, **kwargs): warnings.warn("set_tick_yspacing is deprecated - use ticks.set_yspacing instead", DeprecationWarning) self.ticks.set_yspacing(*args, **kwargs) @auto_refresh def set_tick_size(self, *args, **kwargs): warnings.warn("set_tick_size is deprecated - use ticks.set_length instead", DeprecationWarning) self.ticks.set_length(*args, **kwargs) @auto_refresh def set_tick_color(self, *args, **kwargs): warnings.warn("set_tick_color is deprecated - use ticks.set_color instead", DeprecationWarning) self.ticks.set_color(*args, **kwargs) @auto_refresh def show_grid(self, *args, **kwargs): warnings.warn("show_grid is deprecated - use add_grid instead", DeprecationWarning) self.add_grid(*args, **kwargs) @auto_refresh def hide_grid(self, *args, **kwargs): warnings.warn("hide_grid is deprecated - use remove_grid instead", DeprecationWarning) self.remove_grid(*args, **kwargs) @auto_refresh def show_beam(self, *args, **kwargs): warnings.warn("show_beam is deprecated - use add_beam instead", DeprecationWarning) self.add_beam(*args, **kwargs) @auto_refresh def hide_beam(self, *args, **kwargs): warnings.warn("hide_beam is deprecated - use remove_beam instead", DeprecationWarning) self.add_beam(*args, **kwargs) @auto_refresh def show_scalebar(self, *args, **kwargs): warnings.warn("show_scalebar is deprecated - use add_scalebar instead", DeprecationWarning) self.add_scalebar(*args, **kwargs) @auto_refresh def hide_scalebar(self, *args, **kwargs): warnings.warn("hide_scalebar is deprecated - use remove_scalebar instead", DeprecationWarning) self.add_scalebar(*args, **kwargs) @auto_refresh def show_colorbar(self, *args, **kwargs): warnings.warn("show_colorbar is deprecated - use add_colorbar instead", DeprecationWarning) self.add_colorbar(*args, **kwargs) @auto_refresh def hide_colorbar(self, *args, **kwargs): warnings.warn("hide_colorbar is deprecated - use remove_colorbar instead", DeprecationWarning) self.add_colorbar(*args, **kwargs) @auto_refresh def set_grid_alpha(self, *args, **kwargs): warnings.warn("set_grid_alpha is deprecated - use grid.set_alpha instead", DeprecationWarning) self.grid.set_alpha(*args, **kwargs) @auto_refresh def set_grid_color(self, *args, **kwargs): warnings.warn("set_grid_color is deprecated - use grid.set_color instead", DeprecationWarning) self.grid.set_color(*args, **kwargs) @auto_refresh def set_grid_xspacing(self, *args, **kwargs): warnings.warn("set_grid_xspacing is deprecated - use grid.set_xspacing instead", DeprecationWarning) self.grid.set_xspacing(*args, **kwargs) @auto_refresh def set_grid_yspacing(self, *args, **kwargs): warnings.warn("set_grid_yspacing is deprecated - use grid.set_yspacing instead", DeprecationWarning) self.grid.set_yspacing(*args, **kwargs) @auto_refresh def set_scalebar_properties(self, *args, **kwargs): warnings.warn("set_scalebar_properties is deprecated - use scalebar.set instead", DeprecationWarning) self.scalebar._set_scalebar_properties(*args, **kwargs) @auto_refresh def set_label_properties(self, *args, **kwargs): warnings.warn("set_label_properties is deprecated - use scalebar.set instead", DeprecationWarning) self.scalebar._set_label_properties(*args, **kwargs) @auto_refresh def set_beam_properties(self, *args, **kwargs): warnings.warn("set_beam_properties is deprecated - use beam.set instead", DeprecationWarning) self.beam.set(*args, **kwargs) @auto_refresh def set_labels_latex(self, usetex): warnings.warn("set_labels_latex has been deprecated - use set_system_latex instead", DeprecationWarning) self.set_system_latex(usetex) # TICK LABELS @auto_refresh def set_tick_labels_format(self, xformat=None, yformat=None): warnings.warn("set_tick_labels_format has been deprecated - use tick_labels.set_xformat() and tick_labels.set_yformat instead", DeprecationWarning) if xformat: self.tick_labels.set_xformat(xformat) if yformat: self.tick_labels.set_yformat(yformat) @auto_refresh def set_tick_labels_xformat(self, format): warnings.warn("set_tick_labels_xformat has been deprecated - use tick_labels.set_xformat() instead", DeprecationWarning) self.tick_labels.set_xformat(format) @auto_refresh def set_tick_labels_yformat(self, format): warnings.warn("set_tick_labels_yformat has been deprecated - use tick_labels.set_yformat() instead", DeprecationWarning) self.tick_labels.set_yformat(format) @auto_refresh def set_tick_labels_style(self, style): warnings.warn("set_tick_labels_style has been deprecated - use tick_labels.set_style instead", DeprecationWarning) self.tick_labels.set_style(style) @auto_refresh def set_tick_labels_size(self, size): warnings.warn("set_tick_labels_size has been deprecated - use tick_labels.set_font instead", DeprecationWarning) self.tick_labels.set_font(size=size) @auto_refresh def set_tick_labels_weight(self, weight): warnings.warn("set_tick_labels_weight has been deprecated - use tick_labels.set_font instead", DeprecationWarning) self.tick_labels.set_font(weight=weight) @auto_refresh def set_tick_labels_family(self, family): warnings.warn("set_tick_labels_family has been deprecated - use tick_labels.set_font instead", DeprecationWarning) self.tick_labels.set_font(family=family) @auto_refresh def set_tick_labels_font(self, *args, **kwargs): warnings.warn("set_tick_labels_font has been deprecated - use tick_labels.set_font instead", DeprecationWarning) self.tick_labels.set_font(*args, **kwargs) @auto_refresh def show_tick_labels(self): warnings.warn("show_tick_labels has been deprecated - use tick_labels.show instead", DeprecationWarning) self.tick_labels.show() @auto_refresh def hide_tick_labels(self): warnings.warn("hide_tick_labels has been deprecated - use tick_labels.hide instead", DeprecationWarning) self.tick_labels.hide() @auto_refresh def show_xtick_labels(self): warnings.warn("show_xtick_labels has been deprecated - use tick_labels.show_x instead", DeprecationWarning) self.tick_labels.show_x() @auto_refresh def hide_xtick_labels(self): warnings.warn("hide_xtick_labels has been deprecated - use tick_labels.hide_x instead", DeprecationWarning) self.tick_labels.hide_x() @auto_refresh def show_ytick_labels(self): warnings.warn("show_ytick_labels has been deprecated - use tick_labels.show_y instead", DeprecationWarning) self.tick_labels.show_y() @auto_refresh def hide_ytick_labels(self): warnings.warn("hide_ytick_labels has been deprecated - use tick_labels.hide_y instead", DeprecationWarning) self.tick_labels.hide_y() # AXIS LABELS @auto_refresh def set_axis_labels(self, xlabel='default', ylabel='default', xpad='default', ypad='default'): warnings.warn("set_axis_labels has been deprecated - use axis_labels.set_xtext, axis_labels.set_ytext, axis_labels.set_xpad, and axis_labels.set_ypad instead", DeprecationWarning) if xlabel != 'default': self.axis_labels.set_xtext(xlabel) if xpad != 'default': self.axis_labels.set_xpad(xpad) if ylabel != 'default': self.axis_labels.set_ytext(ylabel) if ypad != 'default': self.axis_labels.set_ypad(ypad) @auto_refresh def set_axis_labels_xdisp(self, xpad): warnings.warn("set_axis_labels_xdisk has been deprecated - use axis_labels.set_xpad instead", DeprecationWarning) self.axis_labels.set_xpad(xpad) @auto_refresh def set_axis_labels_ydisp(self, ypad): warnings.warn("set_axis_labels_xdisp has been deprecated - use axis_labels.set_ypad instead", DeprecationWarning) self.axis_labels.set_ypad(ypad) @auto_refresh def set_axis_labels_size(self, size): warnings.warn("set_axis_labels_size has been deprecated - use axis_labels.set_font instead", DeprecationWarning) self.axis_labels.set_font(size=size) @auto_refresh def set_axis_labels_weight(self, weight): warnings.warn("set_axis_labels_weight has been deprecated - use axis_labels.set_font instead", DeprecationWarning) self.axis_labels.set_font(weight=weight) @auto_refresh def set_axis_labels_family(self, family): warnings.warn("set_axis_labels_family has been deprecated - use axis_labels.set_font instead", DeprecationWarning) self.axis_labels.set_font(family=family) @auto_refresh def set_axis_labels_font(self, *args, **kwargs): warnings.warn("set_axis_labels_font has been deprecated - use axis_labels.set_font instead", DeprecationWarning) self.axis_labels.set_font(*args, **kwargs) @auto_refresh def show_axis_labels(self): warnings.warn("show_axis_labels has been deprecated - use axis_labels.show instead", DeprecationWarning) self.axis_labels.show() @auto_refresh def hide_axis_labels(self): warnings.warn("hide_axis_labels has been deprecated - use axis_labels.hide instead", DeprecationWarning) self.axis_labels.hide() @auto_refresh def show_xaxis_label(self): warnings.warn("show_xaxis_label has been deprecated - use axis_labels.show_x instead", DeprecationWarning) self.axis_labels.show_x() @auto_refresh def hide_xaxis_label(self): warnings.warn("hide_xaxis_label has been deprecated - use axis_labels.hide_x instead", DeprecationWarning) self.axis_labels.hide_x() @auto_refresh def show_yaxis_label(self): warnings.warn("show_yaxis_label has been deprecated - use axis_labels.show_y instead", DeprecationWarning) self.axis_labels.show_y() @auto_refresh def hide_yaxis_label(self): warnings.warn("hide_yaxis_label has been deprecated - use axis_labels.hide_y instead", DeprecationWarning) self.axis_labels.hide_y() # FRAME @auto_refresh def set_frame_color(self, *args, **kwargs): warnings.warn("set_frame_color has been deprecated - use frame.set_color instead", DeprecationWarning) self.frame.set_color(*args, **kwargs) @auto_refresh def set_frame_linewidth(self, *args, **kwargs): warnings.warn("set_frame_linewidth has been deprecated - use frame.set_linewidth instead", DeprecationWarning) self.frame.set_linewidth(*args, **kwargs) APLpy-1.0/aplpy/frame.py0000644000077000000240000000225612471065325015060 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division from .decorators import auto_refresh class Frame(object): @auto_refresh def __init__(self, parent): self._ax1 = parent._ax1 self._ax2 = parent._ax2 self._figure = parent._figure # Save plotting parameters (required for @auto_refresh) self._parameters = parent._parameters @auto_refresh def set_linewidth(self, linewidth): ''' Set line width of the frame. Parameters ---------- linewidth: The linewidth to use for the frame. ''' for key in self._ax1.spines: self._ax1.spines[key].set_linewidth(linewidth) for key in self._ax2.spines: self._ax2.spines[key].set_linewidth(linewidth) @auto_refresh def set_color(self, color): ''' Set color of the frame. Parameters ---------- color: The color to use for the frame. ''' for key in self._ax1.spines: self._ax1.spines[key].set_edgecolor(color) for key in self._ax2.spines: self._ax2.spines[key].set_edgecolor(color) APLpy-1.0/aplpy/grid.py0000644000077000000240000003750212471065325014715 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import warnings import numpy as np from matplotlib.collections import LineCollection from . import math_util from . import wcs_util from . import angle_util as au from .ticks import tick_positions, default_spacing from .decorators import auto_refresh class Grid(object): @auto_refresh def __init__(self, parent): # Save axes and wcs information self.ax = parent._ax1 self._wcs = parent._wcs self._figure = parent._figure # Save plotting parameters (required for @auto_refresh) self._parameters = parent._parameters # Initialize grid container self._grid = None self._active = False # Set defaults self.x_auto_spacing = True self.y_auto_spacing = True self.default_color = 'white' self.default_alpha = 0.5 # Set grid event handler self.ax.callbacks.connect('xlim_changed', self._update_norefresh) self.ax.callbacks.connect('ylim_changed', self._update_norefresh) @auto_refresh def _remove(self): self._grid.remove() @auto_refresh def set_xspacing(self, xspacing): ''' Set the grid line spacing in the longitudinal direction Parameters ---------- xspacing : { float, str } The spacing in the longitudinal direction. To set the spacing to be the same as the ticks, set this to 'tick' ''' if xspacing == 'tick': self.x_auto_spacing = True elif np.isreal(xspacing): self.x_auto_spacing = False if self._wcs.xaxis_coord_type in ['longitude', 'latitude']: self.x_grid_spacing = au.Angle( degrees=xspacing, latitude=self._wcs.xaxis_coord_type == 'latitude') else: self.x_grid_spacing = xspacing else: raise ValueError("Grid spacing should be a scalar or 'tick'") self._update() @auto_refresh def set_yspacing(self, yspacing): ''' Set the grid line spacing in the latitudinal direction Parameters ---------- yspacing : { float, str } The spacing in the latitudinal direction. To set the spacing to be the same as the ticks, set this to 'tick' ''' if yspacing == 'tick': self.y_auto_spacing = True elif np.isreal(yspacing): self.y_auto_spacing = False if self._wcs.yaxis_coord_type in ['longitude', 'latitude']: self.y_grid_spacing = au.Angle( degrees=yspacing, latitude=self._wcs.yaxis_coord_type == 'latitude') else: self.y_grid_spacing = yspacing else: raise ValueError("Grid spacing should be a scalar or 'tick'") self._update() @auto_refresh def set_color(self, color): ''' Set the color of the grid lines Parameters ---------- color : str The color of the grid lines ''' if self._grid: self._grid.set_edgecolor(color) else: self.default_color = color @auto_refresh def set_alpha(self, alpha): ''' Set the alpha (transparency) of the grid lines Parameters ---------- alpha : float The alpha value of the grid. This should be a floating point value between 0 and 1, where 0 is completely transparent, and 1 is completely opaque. ''' if self._grid: self._grid.set_alpha(alpha) else: self.default_alpha = alpha @auto_refresh def set_linewidth(self, linewidth): self._grid.set_linewidth(linewidth) @auto_refresh def set_linestyle(self, linestyle): self._grid.set_linestyle(linestyle) @auto_refresh def show(self): if self._grid: self._grid.set_visible(True) else: self._active = True self._update() self.set_color(self.default_color) self.set_alpha(self.default_alpha) @auto_refresh def hide(self): self._grid.set_visible(False) @auto_refresh def _update(self, *args): self._update_norefresh(*args) def _update_norefresh(self, *args): if not self._active: return self.ax if len(args) == 1: if id(self.ax) != id(args[0]): raise Exception("ax ids should match") lines = [] # Set x grid spacing if self.x_auto_spacing: if self.ax.xaxis.apl_auto_tick_spacing: xspacing = default_spacing(self.ax, 'x', self.ax.xaxis.apl_label_form) else: xspacing = self.ax.xaxis.apl_tick_spacing else: xspacing = self.x_grid_spacing if xspacing is None: warnings.warn("Could not determine x tick spacing - grid cannot be drawn") return if self._wcs.xaxis_coord_type in ['longitude', 'latitude']: xspacing = xspacing.todegrees() # Set y grid spacing if self.y_auto_spacing: if self.ax.yaxis.apl_auto_tick_spacing: yspacing = default_spacing(self.ax, 'y', self.ax.yaxis.apl_label_form) else: yspacing = self.ax.yaxis.apl_tick_spacing else: yspacing = self.y_grid_spacing if yspacing is None: warnings.warn("Could not determine y tick spacing - grid cannot be drawn") return if self._wcs.yaxis_coord_type in ['longitude', 'latitude']: yspacing = yspacing.todegrees() # Find x lines that intersect with axes grid_x_i, grid_y_i = find_intersections(self.ax, 'x', xspacing) # Ensure that longitudes are between 0 and 360, and latitudes between # -90 and 90 if self._wcs.xaxis_coord_type == 'longitude': grid_x_i = np.mod(grid_x_i, 360.) elif self._wcs.xaxis_coord_type == 'latitude': grid_x_i = np.mod(grid_x_i + 90., 180.) - 90. if self._wcs.yaxis_coord_type == 'longitude': grid_y_i = np.mod(grid_y_i, 360.) elif self._wcs.yaxis_coord_type == 'latitude': grid_y_i = np.mod(grid_y_i + 90., 180.) - 90. # If we are dealing with longitude/latitude then can search all # neighboring grid lines to see if there are any closed longitude # lines if self._wcs.xaxis_coord_type == 'latitude' and self._wcs.yaxis_coord_type == 'longitude' and len(grid_x_i) > 0: gx = grid_x_i.min() while True: gx -= xspacing xpix, ypix = wcs_util.world2pix(self._wcs, gx, 0.) if in_plot(self.ax, xpix, ypix) and gx >= -90.: grid_x_i = np.hstack([grid_x_i, gx, gx]) grid_y_i = np.hstack([grid_y_i, 0., 360.]) else: break gx = grid_x_i.max() while True: gx += xspacing xpix, ypix = wcs_util.world2pix(self._wcs, gx, 0.) if in_plot(self.ax, xpix, ypix) and gx <= +90.: grid_x_i = np.hstack([grid_x_i, gx, gx]) grid_y_i = np.hstack([grid_y_i, 0., 360.]) else: break # Plot those lines for gx in np.unique(grid_x_i): for line in plot_grid_x(self.ax, grid_x_i, grid_y_i, gx): lines.append(line) # Find y lines that intersect with axes grid_x_i, grid_y_i = find_intersections(self.ax, 'y', yspacing) if self._wcs.xaxis_coord_type == 'longitude': grid_x_i = np.mod(grid_x_i, 360.) elif self._wcs.xaxis_coord_type == 'latitude': grid_x_i = np.mod(grid_x_i + 90., 180.) - 90. if self._wcs.yaxis_coord_type == 'longitude': grid_y_i = np.mod(grid_y_i, 360.) elif self._wcs.yaxis_coord_type == 'latitude': grid_y_i = np.mod(grid_y_i + 90., 180.) - 90. # If we are dealing with longitude/latitude then can search all # neighboring grid lines to see if there are any closed longitude # lines if (self._wcs.xaxis_coord_type == 'longitude' and self._wcs.yaxis_coord_type == 'latitude' and len(grid_y_i) > 0): gy = grid_y_i.min() while True: gy -= yspacing xpix, ypix = wcs_util.world2pix(self._wcs, 0., gy) if in_plot(self.ax, xpix, ypix) and gy >= -90.: grid_x_i = np.hstack([grid_x_i, 0., 360.]) grid_y_i = np.hstack([grid_y_i, gy, gy]) else: break gy = grid_y_i.max() while True: gy += yspacing xpix, ypix = wcs_util.world2pix(self._wcs, 0., gy) if in_plot(self.ax, xpix, ypix) and gy <= +90.: grid_x_i = np.hstack([grid_x_i, 0., 360.]) grid_y_i = np.hstack([grid_y_i, gy, gy]) else: break # Plot those lines for gy in np.unique(grid_y_i): for line in plot_grid_y(self.ax, grid_x_i, grid_y_i, gy): lines.append(line) if self._grid: self._grid.set_verts(lines) else: self._grid = LineCollection(lines, transOffset=self.ax.transData) self.ax.add_collection(self._grid, False) return self.ax def plot_grid_y(ax, grid_x, grid_y, gy, alpha=0.5): '''Plot a single grid line in the y direction''' wcs = ax._wcs lines_out = [] # Find intersections that correspond to latitude lat0 index = np.where(grid_y == gy) # Produce sorted array of the longitudes of all intersections grid_x_sorted = np.sort(grid_x[index]) # If coordinate type is a latitude or longitude, also need to check if # end-points fall inside the plot if wcs.xaxis_coord_type == 'latitude': if not np.any(grid_x_sorted == -90): xpix, ypix = wcs_util.world2pix(wcs, max(grid_x_sorted[0] - 1., -90.), gy) if in_plot(ax, xpix, ypix): grid_x_sorted = np.hstack([-90., grid_x_sorted]) if not np.any(grid_x_sorted == +90): xpix, ypix = wcs_util.world2pix(wcs, min(grid_x_sorted[-1] + 1., +90.), gy) if in_plot(ax, xpix, ypix): grid_x_sorted = np.hstack([grid_x_sorted, +90.]) elif wcs.xaxis_coord_type == 'longitude': if not np.any(grid_x_sorted == 0.): xpix, ypix = wcs_util.world2pix(wcs, max(grid_x_sorted[0] - 1., 0.), gy) if in_plot(ax, xpix, ypix): grid_x_sorted = np.hstack([0., grid_x_sorted]) if not np.any(grid_x_sorted == 360.): xpix, ypix = wcs_util.world2pix(wcs, min(grid_x_sorted[-1] + 1., 360.), gy) if in_plot(ax, xpix, ypix): grid_x_sorted = np.hstack([grid_x_sorted, 360.]) # Check if the first mid-point with coordinates is inside the viewport xpix, ypix = wcs_util.world2pix(wcs, (grid_x_sorted[0] + grid_x_sorted[1]) / 2., gy) if not in_plot(ax, xpix, ypix): grid_x_sorted = np.roll(grid_x_sorted, 1) # Check that number of grid points is even if len(grid_x_sorted) % 2 == 1: warnings.warn("Unexpected number of grid points - x grid lines cannot be drawn") return [] # Cycle through intersections for i in range(0, len(grid_x_sorted), 2): grid_x_min = grid_x_sorted[i] grid_x_max = grid_x_sorted[i + 1] x_world = math_util.complete_range(grid_x_min, grid_x_max, 100) y_world = np.repeat(gy, len(x_world)) x_pix, y_pix = wcs_util.world2pix(wcs, x_world, y_world) lines_out.append(list(zip(x_pix, y_pix))) return lines_out def plot_grid_x(ax, grid_x, grid_y, gx, alpha=0.5): '''Plot a single longitude line''' wcs = ax._wcs lines_out = [] # Find intersections that correspond to longitude gx index = np.where(grid_x == gx) # Produce sorted array of the latitudes of all intersections grid_y_sorted = np.sort(grid_y[index]) # If coordinate type is a latitude or longitude, also need to check if # end-points fall inside the plot if wcs.yaxis_coord_type == 'latitude': if not np.any(grid_y_sorted == -90): xpix, ypix = wcs_util.world2pix(wcs, gx, max(grid_y_sorted[0] - 1., -90.)) if in_plot(ax, xpix, ypix): grid_y_sorted = np.hstack([-90., grid_y_sorted]) if not np.any(grid_y_sorted == +90): xpix, ypix = wcs_util.world2pix(wcs, gx, min(grid_y_sorted[-1] + 1., +90.)) if in_plot(ax, xpix, ypix): grid_y_sorted = np.hstack([grid_y_sorted, +90.]) elif wcs.yaxis_coord_type == 'longitude': if not np.any(grid_y_sorted == 0.): xpix, ypix = wcs_util.world2pix(wcs, gx, max(grid_y_sorted[0] - 1., 0.)) if in_plot(ax, xpix, ypix): grid_y_sorted = np.hstack([0., grid_y_sorted]) if not np.any(grid_y_sorted == 360.): xpix, ypix = wcs_util.world2pix(wcs, gx, min(grid_y_sorted[-1] + 1., 360.)) if in_plot(ax, xpix, ypix): grid_y_sorted = np.hstack([grid_y_sorted, 360.]) # Check if the first mid-point with coordinates is inside the viewport xpix, ypix = wcs_util.world2pix(wcs, gx, (grid_y_sorted[0] + grid_y_sorted[1]) / 2.) if not in_plot(ax, xpix, ypix): grid_y_sorted = np.roll(grid_y_sorted, 1) # Check that number of grid points is even if len(grid_y_sorted) % 2 == 1: warnings.warn("Unexpected number of grid points - y grid lines cannot be drawn") return [] # Cycle through intersections for i in range(0, len(grid_y_sorted), 2): grid_y_min = grid_y_sorted[i] grid_y_max = grid_y_sorted[i + 1] y_world = math_util.complete_range(grid_y_min, grid_y_max, 100) x_world = np.repeat(gx, len(y_world)) x_pix, y_pix = wcs_util.world2pix(wcs, x_world, y_world) lines_out.append(list(zip(x_pix, y_pix))) return lines_out def in_plot(ax, x_pix, y_pix): '''Check whether a given point is in a plot''' xmin, xmax = ax.xaxis.get_view_interval() ymin, ymax = ax.yaxis.get_view_interval() return (x_pix > xmin + 0.5 and x_pix < xmax + 0.5 and y_pix > ymin + 0.5 and y_pix < ymax + 0.5) def find_intersections(ax, coord, spacing): ''' Find intersections of a given coordinate with all axes Parameters ---------- ax : The matplotlib axis instance for the figure. coord : { 'x', 'y' } The coordinate for which we are looking for ticks. spacing : float The spacing along the axis. ''' wcs = ax._wcs xmin, xmax = ax.xaxis.get_view_interval() ymin, ymax = ax.yaxis.get_view_interval() options = dict(mode='xy', xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax) # Initialize arrays x, y = [], [] # Bottom X axis (labels_x, labels_y, world_x, world_y) = tick_positions( wcs, spacing, 'x', coord, farside=False, **options) x.extend(world_x) y.extend(world_y) # Top X axis (labels_x, labels_y, world_x, world_y) = tick_positions( wcs, spacing, 'x', coord, farside=True, **options) x.extend(world_x) y.extend(world_y) # Left Y axis (labels_x, labels_y, world_x, world_y) = tick_positions( wcs, spacing, 'y', coord, farside=False, **options) x.extend(world_x) y.extend(world_y) # Right Y axis (labels_x, labels_y, world_x, world_y) = tick_positions( wcs, spacing, 'y', coord, farside=True, **options) x.extend(world_x) y.extend(world_y) return np.array(x), np.array(y) APLpy-1.0/aplpy/header.py0000644000077000000240000000522112471102127015201 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division from astropy import log def check(header, convention=None, dimensions=[0, 1]): ix = dimensions[0] + 1 iy = dimensions[1] + 1 # If header does not contain CTYPE keywords, assume that the WCS is # missing or incomplete, and replace it with a 1-to-1 pixel mapping if 'CTYPE%i' % ix not in header or 'CTYPE%i' % iy not in header: log.warning("No WCS information found in header - using pixel coordinates") header['CTYPE%i' % ix] = 'PIXEL' header['CTYPE%i' % iy] = 'PIXEL' header['CRVAL%i' % ix] = 0. header['CRVAL%i' % iy] = 0. header['CRPIX%i' % ix] = 0. header['CRPIX%i' % iy] = 0. header['CDELT%i' % ix] = 1. header['CDELT%i' % iy] = 1. if header['CTYPE%i' % ix][4:] == '-CAR' and header['CTYPE%i' % iy][4:] == '-CAR': if header['CTYPE%i' % ix][:4] == 'DEC-' or header['CTYPE%i' % ix][1:4] == 'LAT': ilon = iy ilat = ix elif header['CTYPE%i' % iy][:4] == 'DEC-' or header['CTYPE%i' % iy][1:4] == 'LAT': ilon = ix ilat = iy else: ilon = None ilat = None if ilat is not None and header['CRVAL%i' % ilat] != 0: if convention == 'calabretta': pass # we don't need to do anything elif convention == 'wells': if 'CDELT%i' % ilat not in header: raise Exception("Need CDELT%i to be present for wells convention" % ilat) crpix = header['CRPIX%i' % ilat] crval = header['CRVAL%i' % ilat] cdelt = header['CDELT%i' % ilat] crpix = crpix - crval / cdelt try: header['CRPIX%i' % ilat] = crpix header['CRVAL%i' % ilat] = 0. except: # older versions of PyFITS header.update('CRPIX%i' % ilat, crpix) header.update('CRVAL%i' % ilon, 0.) else: raise Exception('''WARNING: projection is Plate Caree (-CAR) and CRVALy is not zero. This can be interpreted either according to Wells (1981) or Calabretta (2002). The former defines the projection as rectilinear regardless of the value of CRVALy, whereas the latter defines the projection as rectilinear only when CRVALy is zero. You will need to specify the convention to assume by setting either convention='wells' or convention='calabretta' when initializing the FITSFigure instance. ''') return header APLpy-1.0/aplpy/image_util.py0000644000077000000240000000712012471065325016100 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import numpy as np from astropy import log from . import math_util as m class interp1d(object): def __init__(self, x, y): self.x = x self.y = y self.dy = np.zeros(y.shape, dtype=y.dtype) self.dy[:-1] = (self.y[1:] - self.y[:-1]) / (self.x[1:] - self.x[:-1]) self.dy[-1] = self.dy[-2] def __call__(self, x_new): ipos = np.searchsorted(self.x, x_new) if m.isnumeric(x_new): if ipos == 0: ipos = 1 if ipos == len(self.x): ipos = len(self.x) - 1 else: ipos[ipos == 0] = 1 ipos[ipos == len(self.x)] = len(self.x) - 1 ipos = ipos - 1 return (x_new - self.x[ipos]) * self.dy[ipos] + self.y[ipos] def resample(array, factor): nx, ny = np.shape(array) nx_new = nx // factor ny_new = ny // factor array2 = np.zeros((nx_new, ny)) for i in range(nx_new): array2[i, :] = np.mean(array[i * factor:(i + 1) * factor, :], axis=0) array3 = np.zeros((nx_new, ny_new)) for j in range(ny_new): array3[:, j] = np.mean(array2[:, j * factor:(j + 1) * factor], axis=1) return array3 def percentile_function(array): if np.all(np.isnan(array) | np.isinf(array)): log.warning("Image contains only NaN or Inf values") return lambda x: 0 array = array.ravel() array = array[np.where(np.isnan(array) == False)] array = array[np.where(np.isinf(array) == False)] n_total = np.shape(array)[0] if n_total == 0: def return_zero(x): return 0 return return_zero elif n_total == 1: def return_single(x): return array[0] return return_single array = np.sort(array) x = np.linspace(0., 100., num=n_total) spl = interp1d(x=x, y=array) if n_total > 10000: x = np.linspace(0., 100., num=10000) spl = interp1d(x=x, y=spl(x)) array = None return spl def stretch(array, function, exponent=2, midpoint=None): if function == 'linear': return array elif function == 'log': if not m.isnumeric(midpoint): midpoint = 0.05 return np.log10(array / midpoint + 1.) / np.log10(1. / midpoint + 1.) elif function == 'sqrt': return np.sqrt(array) elif function == 'arcsinh': if not m.isnumeric(midpoint): midpoint = -0.033 return np.arcsinh(array / midpoint) / np.arcsinh(1. / midpoint) elif function == 'power': return np.power(array, exponent) else: raise Exception("Unknown function : " + function) def _matplotlib_pil_bug_present(): """ Determine whether PIL images should be pre-flipped due to a bug in Matplotlib. Prior to Matplotlib 1.2.0, RGB images provided as PIL objects were oriented wrongly. This function tests whether the bug is present. """ from matplotlib.image import pil_to_array try: from PIL import Image except: import Image from astropy import log array1 = np.array([[1,2],[3,4]], dtype=np.uint8) image = Image.fromarray(array1) array2 = pil_to_array(image) if np.all(array1 == array2): log.debug("PIL Image flipping bug not present in Matplotlib") return False elif np.all(array1 == array2[::-1,:]): log.debug("PIL Image flipping bug detected in Matplotlib") return True else: log.warning("Could not properly determine Matplotlib behavior for RGB images - image may be flipped incorrectly") return False APLpy-1.0/aplpy/labels.py0000644000077000000240000003757712471102127015236 0ustar tomstaff00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, division, unicode_literals import warnings import numpy as np import matplotlib.pyplot as mpl from matplotlib.font_manager import FontProperties from . import wcs_util from . import angle_util as au from .decorators import auto_refresh, fixdocstring class TickLabels(object): def __init__(self, parent): # Store references to axes self._ax1 = parent._ax1 self._ax2 = parent._ax2 self._wcs = parent._wcs self._figure = parent._figure # Save plotting parameters (required for @auto_refresh) self._parameters = parent._parameters # Set font self._label_fontproperties = FontProperties() self.set_style('plain') system, equinox, units = wcs_util.system(self._wcs) # Set default label format if self._wcs.xaxis_coord_type in ['longitude', 'latitude']: if system['name'] == 'equatorial': if self._wcs.xaxis_coord_type == 'longitude': self.set_xformat("hh:mm:ss.ss") else: self.set_xformat("dd:mm:ss.s") else: self.set_xformat("ddd.dddd") else: self.set_xformat('%g') if self._wcs.yaxis_coord_type in ['longitude', 'latitude']: if system['name'] == 'equatorial': if self._wcs.yaxis_coord_type == 'longitude': self.set_yformat("hh:mm:ss.ss") else: self.set_yformat("dd:mm:ss.s") else: self.set_yformat("ddd.dddd") else: self.set_yformat('%g') # Set major tick formatters fx1 = WCSFormatter(wcs=self._wcs, coord='x') fy1 = WCSFormatter(wcs=self._wcs, coord='y') self._ax1.xaxis.set_major_formatter(fx1) self._ax1.yaxis.set_major_formatter(fy1) fx2 = mpl.NullFormatter() fy2 = mpl.NullFormatter() self._ax2.xaxis.set_major_formatter(fx2) self._ax2.yaxis.set_major_formatter(fy2) # Cursor display self._ax1._cursor_world = True self._figure.canvas.mpl_connect('key_press_event', self._set_cursor_prefs) @auto_refresh def set_xformat(self, format): ''' Set the format of the x-axis tick labels. If the x-axis type is ``longitude`` or ``latitude``, then the options are: * ``ddd.ddddd`` - decimal degrees, where the number of decimal places can be varied * ``hh`` or ``dd`` - hours (or degrees) * ``hh:mm`` or ``dd:mm`` - hours and minutes (or degrees and arcminutes) * ``hh:mm:ss`` or ``dd:mm:ss`` - hours, minutes, and seconds (or degrees, arcminutes, and arcseconds) * ``hh:mm:ss.ss`` or ``dd:mm:ss.ss`` - hours, minutes, and seconds (or degrees, arcminutes, and arcseconds), where the number of decimal places can be varied. If the x-axis type is ``scalar``, then the format should be a valid python string format beginning with a ``%``. If one of these arguments is not specified, the format for that axis is left unchanged. ''' if self._wcs.xaxis_coord_type in ['longitude', 'latitude']: if format.startswith('%'): raise Exception("Cannot specify Python format for longitude or latitude") try: if not self._ax1.xaxis.apl_auto_tick_spacing: au._check_format_spacing_consistency(format, self._ax1.xaxis.apl_tick_spacing) except au.InconsistentSpacing: warnings.warn("WARNING: Requested label format is not accurate enough to display ticks. The label format will not be changed.") return else: if not format.startswith('%'): raise Exception("For scalar tick labels, format should be a Python format beginning with %") self._ax1.xaxis.apl_label_form = format self._ax2.xaxis.apl_label_form = format @auto_refresh def set_yformat(self, format): ''' Set the format of the y-axis tick labels. If the y-axis type is ``longitude`` or ``latitude``, then the options are: * ``ddd.ddddd`` - decimal degrees, where the number of decimal places can be varied * ``hh`` or ``dd`` - hours (or degrees) * ``hh:mm`` or ``dd:mm`` - hours and minutes (or degrees and arcminutes) * ``hh:mm:ss`` or ``dd:mm:ss`` - hours, minutes, and seconds (or degrees, arcminutes, and arcseconds) * ``hh:mm:ss.ss`` or ``dd:mm:ss.ss`` - hours, minutes, and seconds (or degrees, arcminutes, and arcseconds), where the number of decimal places can be varied. If the y-axis type is ``scalar``, then the format should be a valid python string format beginning with a ``%``. If one of these arguments is not specified, the format for that axis is left unchanged. ''' if self._wcs.yaxis_coord_type in ['longitude', 'latitude']: if format.startswith('%'): raise Exception("Cannot specify Python format for longitude or latitude") try: if not self._ax1.yaxis.apl_auto_tick_spacing: au._check_format_spacing_consistency(format, self._ax1.yaxis.apl_tick_spacing) except au.InconsistentSpacing: warnings.warn("WARNING: Requested label format is not accurate enough to display ticks. The label format will not be changed.") return else: if not format.startswith('%'): raise Exception("For scalar tick labels, format should be a Python format beginning with %") self._ax1.yaxis.apl_label_form = format self._ax2.yaxis.apl_label_form = format @auto_refresh def set_style(self, style): """ Set the format of the x-axis tick labels. This can be 'colons' or 'plain': * 'colons' uses colons as separators, for example 31:41:59.26 +27:18:28.1 * 'plain' uses letters and symbols as separators, for example 31h41m59.26s +27º18'28.1" """ if style == 'latex': warnings.warn("latex has now been merged with plain - whether or not to use LaTeX is controlled through set_system_latex") style = 'plain' if not style in ['colons', 'plain']: raise Exception("Label style should be one of colons/plain") self._ax1.xaxis.apl_labels_style = style self._ax1.yaxis.apl_labels_style = style self._ax2.xaxis.apl_labels_style = style self._ax2.yaxis.apl_labels_style = style @auto_refresh @fixdocstring def set_font(self, family=None, style=None, variant=None, stretch=None, weight=None, size=None, fontproperties=None): """ Set the font of the tick labels. Parameters ---------- common: family, style, variant, stretch, weight, size, fontproperties Notes ----- Default values are set by matplotlib or previously set values if set_font has already been called. Global default values can be set by editing the matplotlibrc file. """ if family: self._label_fontproperties.set_family(family) if style: self._label_fontproperties.set_style(style) if variant: self._label_fontproperties.set_variant(variant) if stretch: self._label_fontproperties.set_stretch(stretch) if weight: self._label_fontproperties.set_weight(weight) if size: self._label_fontproperties.set_size(size) if fontproperties: self._label_fontproperties = fontproperties for tick in self._ax1.get_xticklabels(): tick.set_fontproperties(self._label_fontproperties) for tick in self._ax1.get_yticklabels(): tick.set_fontproperties(self._label_fontproperties) for tick in self._ax2.get_xticklabels(): tick.set_fontproperties(self._label_fontproperties) for tick in self._ax2.get_yticklabels(): tick.set_fontproperties(self._label_fontproperties) @auto_refresh def show(self): """ Show the x- and y-axis tick labels. """ self.show_x() self.show_y() @auto_refresh def hide(self): """ Hide the x- and y-axis tick labels. """ self.hide_x() self.hide_y() @auto_refresh def show_x(self): """ Show the x-axis tick labels. """ for tick in self._ax1.get_xticklabels(): tick.set_visible(True) for tick in self._ax2.get_xticklabels(): tick.set_visible(True) @auto_refresh def hide_x(self): """ Hide the x-axis tick labels. """ for tick in self._ax1.get_xticklabels(): tick.set_visible(False) for tick in self._ax2.get_xticklabels(): tick.set_visible(False) @auto_refresh def show_y(self): """ Show the y-axis tick labels. """ for tick in self._ax1.get_yticklabels(): tick.set_visible(True) for tick in self._ax2.get_yticklabels(): tick.set_visible(True) @auto_refresh def hide_y(self): """ Hide the y-axis tick labels. """ for tick in self._ax1.get_yticklabels(): tick.set_visible(False) for tick in self._ax2.get_yticklabels(): tick.set_visible(False) @auto_refresh def set_xposition(self, position): """ Set the position of the x-axis tick labels ('top' or 'bottom') """ if position == 'bottom': fx1 = WCSFormatter(wcs=self._wcs, coord='x') self._ax1.xaxis.set_major_formatter(fx1) fx2 = mpl.NullFormatter() self._ax2.xaxis.set_major_formatter(fx2) elif position == 'top': fx1 = mpl.NullFormatter() self._ax1.xaxis.set_major_formatter(fx1) fx2 = WCSFormatter(wcs=self._wcs, coord='x') self._ax2.xaxis.set_major_formatter(fx2) else: raise ValueError("position should be one of 'top' or 'bottom'") @auto_refresh def set_yposition(self, position): """ Set the position of the y-axis tick labels ('left' or 'right') """ if position == 'left': fy1 = WCSFormatter(wcs=self._wcs, coord='y') self._ax1.yaxis.set_major_formatter(fy1) fy2 = mpl.NullFormatter() self._ax2.yaxis.set_major_formatter(fy2) elif position == 'right': fy1 = mpl.NullFormatter() self._ax1.yaxis.set_major_formatter(fy1) fy2 = WCSFormatter(wcs=self._wcs, coord='y') self._ax2.yaxis.set_major_formatter(fy2) else: raise ValueError("position should be one of 'left' or 'right'") def _set_cursor_prefs(self, event, **kwargs): if event.key == 'c': self._ax1._cursor_world = not self._ax1._cursor_world def _cursor_position(self, x, y): xaxis = self._ax1.xaxis yaxis = self._ax1.yaxis if self._ax1._cursor_world: xw, yw = wcs_util.pix2world(self._wcs, x, y) if self._wcs.xaxis_coord_type in ['longitude', 'latitude']: xw = au.Angle(degrees=xw, latitude=self._wcs.xaxis_coord_type == 'latitude') hours = 'h' in xaxis.apl_label_form if hours: xw = xw.tohours() if xaxis.apl_labels_style in ['plain', 'latex']: sep = ('d', 'm', 's') if hours: sep = ('h', 'm', 's') elif xaxis.apl_labels_style == 'colons': sep = (':', ':', '') xlabel = xw.tostringlist(format=xaxis.apl_label_form, sep=sep) xlabel = "".join(xlabel) else: xlabel = xaxis.apl_label_form % xw if self._wcs.yaxis_coord_type in ['longitude', 'latitude']: yw = au.Angle(degrees=yw, latitude=self._wcs.yaxis_coord_type == 'latitude') hours = 'h' in yaxis.apl_label_form if hours: yw = yw.tohours() if yaxis.apl_labels_style in ['plain', 'latex']: sep = ('d', 'm', 's') if hours: sep = ('h', 'm', 's') elif yaxis.apl_labels_style == 'colons': sep = (':', ':', '') ylabel = yw.tostringlist(format=yaxis.apl_label_form, sep=sep) ylabel = "".join(ylabel) else: ylabel = yaxis.apl_label_form % yw return "%s %s (world)" % (xlabel, ylabel) else: return "%g %g (pixel)" % (x, y) class WCSFormatter(mpl.Formatter): def __init__(self, wcs=False, coord='x'): self._wcs = wcs self.coord = coord def __call__(self, x, pos=None): """ Return the format for tick val x at position pos; pos=None indicated unspecified """ self.coord_type = self._wcs.xaxis_coord_type if self.coord == 'x' else self._wcs.yaxis_coord_type if self.coord_type in ['longitude', 'latitude']: au._check_format_spacing_consistency(self.axis.apl_label_form, self.axis.apl_tick_spacing) hours = 'h' in self.axis.apl_label_form if self.axis.apl_labels_style == 'plain': if mpl.rcParams['text.usetex']: label_style = 'plain_tex' else: label_style = 'plain_notex' else: label_style = self.axis.apl_labels_style if label_style == 'plain_notex': sep = ('\u00b0', "'", '"') if hours: sep = ('h', 'm', 's') elif label_style == 'colons': sep = (':', ':', '') elif label_style == 'plain_tex': if hours: sep = ('^{h}', '^{m}', '^{s}') else: sep = ('^{\circ}', '^{\prime}', '^{\prime\prime}') ipos = np.argmin(np.abs(self.axis.apl_tick_positions_pix - x)) label = self.axis.apl_tick_spacing * self.axis.apl_tick_positions_world[ipos] if hours: label = label.tohours() label = label.tostringlist(format=self.axis.apl_label_form, sep=sep) # Check if neighboring label is similar and if so whether some # elements of the current label are redundant and can be dropped. # This should only be done for sexagesimal coordinates if len(label) > 1: if self.coord == x or self.axis.apl_tick_positions_world[ipos] > 0: comp_ipos = ipos - 1 else: comp_ipos = ipos + 1 if comp_ipos >= 0 and comp_ipos <= len(self.axis.apl_tick_positions_pix) - 1: comp_label = self.axis.apl_tick_spacing * self.axis.apl_tick_positions_world[comp_ipos] if hours: comp_label = comp_label.tohours() comp_label = comp_label.tostringlist(format=self.axis.apl_label_form, sep=sep) for iter in range(len(label)): if comp_label[0] == label[0]: label.pop(0) comp_label.pop(0) else: break else: ipos = np.argmin(np.abs(self.axis.apl_tick_positions_pix - x)) label = self.axis.apl_tick_spacing * self.axis.apl_tick_positions_world[ipos] label = self.axis.apl_label_form % label if mpl.rcParams['text.usetex']: return "$" + "".join(label) + "$" else: return "".join(label) APLpy-1.0/aplpy/layers.py0000644000077000000240000001270212471102127015252 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division from matplotlib.contour import ContourSet from matplotlib.collections import RegularPolyCollection, \ PatchCollection, CircleCollection, LineCollection from .regions import ArtistCollection from .decorators import auto_refresh class Layers(object): def __init__(self): pass def _layer_type(self, layer): if isinstance(self._layers[layer], ContourSet): return 'contour' elif isinstance(self._layers[layer], RegularPolyCollection): return 'collection' elif isinstance(self._layers[layer], PatchCollection): return 'collection' elif isinstance(self._layers[layer], CircleCollection): return 'collection' elif isinstance(self._layers[layer], LineCollection): return 'collection' elif isinstance(self._layers[layer], ArtistCollection): return 'collection' elif hasattr(self._layers[layer], 'remove') and hasattr(self._layers[layer], 'get_visible') and hasattr(self._layers[layer], 'set_visible'): return 'collection' else: raise Exception("Unknown layer type: " + \ str(type(self._layers[layer]))) def _initialize_layers(self): self._layers = {} self._contour_counter = 0 self._vector_counter = 0 self._scatter_counter = 0 self._circle_counter = 0 self._ellipse_counter = 0 self._rectangle_counter = 0 self._linelist_counter = 0 self._region_counter = 0 self._label_counter = 0 self._poly_counter = 0 def list_layers(self): ''' Print a list of layers to standard output. ''' layers_list = [] for layer in self._layers: layer_type = self._layer_type(layer) if layer_type == 'contour': visible = self._layers[layer].collections[0].get_visible() elif layer_type == 'collection': visible = self._layers[layer].get_visible() layers_list.append({'name': layer, 'visible': visible}) n_layers = len(layers_list) if n_layers == 0: print("\n There are no layers in this figure") else: if n_layers == 1: print("\n There is one layer in this figure:\n") else: print("\n There are " + str(n_layers) + \ " layers in this figure:\n") for layer in layers_list: if layer['visible']: print(" -> " + layer['name']) else: print(" -> " + layer['name'] + " (hidden)") @auto_refresh def remove_layer(self, layer, raise_exception=True): ''' Remove a layer. Parameters ---------- layer : str The name of the layer to remove ''' if layer in self._layers: layer_type = self._layer_type(layer) if layer_type == 'contour': for contour in self._layers[layer].collections: contour.remove() self._layers.pop(layer) elif layer_type == 'collection': self._layers[layer].remove() self._layers.pop(layer) if (layer + '_txt') in self._layers: self._layers[layer + '_txt'].remove() self._layers.pop(layer + '_txt') else: if raise_exception: raise Exception("Layer " + layer + " does not exist") @auto_refresh def hide_layer(self, layer, raise_exception=True): ''' Hide a layer. This differs from remove_layer in that if a layer is hidden it can be shown again using show_layer. Parameters ---------- layer : str The name of the layer to hide ''' if layer in self._layers: layer_type = self._layer_type(layer) if layer_type == 'contour': for contour in self._layers[layer].collections: contour.set_visible(False) elif layer_type == 'collection': self._layers[layer].set_visible(False) else: if raise_exception: raise Exception("Layer " + layer + " does not exist") @auto_refresh def show_layer(self, layer, raise_exception=True): ''' Show a layer. This shows a layer previously hidden with hide_layer Parameters ---------- layer : str The name of the layer to show ''' if layer in self._layers: layer_type = self._layer_type(layer) if layer_type == 'contour': for contour in self._layers[layer].collections: contour.set_visible(True) elif layer_type == 'collection': self._layers[layer].set_visible(True) else: if raise_exception: raise Exception("Layer " + layer + " does not exist") def get_layer(self, layer, raise_exception=True): ''' Return a layer object. Parameters ---------- layer : str The name of the layer to return ''' if layer in self._layers: return self._layers[layer] else: if raise_exception: raise Exception("Layer " + layer + " does not exist") APLpy-1.0/aplpy/math_util.py0000644000077000000240000000247712471065325015761 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import numpy as np def isnumeric(value): return type(value) in [float, int, np.int8, np.int16, np.int32, \ np.float32, np.float64] def smart_range(array): array.sort() minval = 360. i1 = 0 i2 = 0 for i in range(0, np.size(array) - 1): if 360. - abs(array[i + 1] - array[i]) < minval: minval = 360. - abs(array[i + 1] - array[i]) i1 = i + 1 i2 = i if(max(array) - min(array) < minval): i1 = 0 i2 = np.size(array) - 1 x_min = array[i1] x_max = array[i2] if(x_min > x_max): x_min = x_min - 360. return x_min, x_max def complete_range(xmin, xmax, spacing): if(xmax - xmin < 1): spacing = 10 xstep = (xmax - xmin) / float(spacing) r = np.arange(xmin, xmax, xstep) if(np.any(r >= xmax)): return r else: return np.hstack([r, xmax]) def closest(array, a): ipos = np.argmin(np.abs(a - array)) return array[ipos] def divisors(n, dup=False): divisors = [] i = 0 if n == 1: return {1: 1} while 1: i += 1 if dup: break if i == n + 1: break if n % i == 0: divisors[i:i + 1] = [i] return np.array(divisors) APLpy-1.0/aplpy/normalize.py0000644000077000000240000001310312471065325015757 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division # The APLpyNormalize class is largely based on code provided by Sarah Graves. import numpy as np import numpy.ma as ma import matplotlib.cbook as cbook from matplotlib.colors import Normalize class APLpyNormalize(Normalize): ''' A Normalize class for imshow that allows different stretching functions for astronomical images. ''' def __init__(self, stretch='linear', exponent=5, vmid=None, vmin=None, vmax=None, clip=False): ''' Initalize an APLpyNormalize instance. Parameters ---------- vmin : None or float, optional Minimum pixel value to use for the scaling. vmax : None or float, optional Maximum pixel value to use for the scaling. stretch : { 'linear', 'log', 'sqrt', 'arcsinh', 'power' }, optional The stretch function to use (default is 'linear'). vmid : None or float, optional Mid-pixel value used for the log and arcsinh stretches. If set to None, a default value is picked. exponent : float, optional if self.stretch is set to 'power', this is the exponent to use. clip : str, optional If clip is True and the given value falls outside the range, the returned value will be 0 or 1, whichever is closer. ''' if vmax < vmin: raise Exception("vmax should be larger than vmin") # Call original initalization routine Normalize.__init__(self, vmin=vmin, vmax=vmax, clip=clip) # Save parameters self.stretch = stretch self.exponent = exponent if stretch == 'power' and np.equal(self.exponent, None): raise Exception("For stretch=='power', an exponent should be specified") if np.equal(vmid, None): if stretch == 'log': if vmin > 0: self.midpoint = vmax / vmin else: raise Exception("When using a log stretch, if vmin < 0, then vmid has to be specified") elif stretch == 'arcsinh': self.midpoint = -1. / 30. else: self.midpoint = None else: if stretch == 'log': if vmin < vmid: raise Exception("When using a log stretch, vmin should be larger than vmid") self.midpoint = (vmax - vmid) / (vmin - vmid) elif stretch == 'arcsinh': self.midpoint = (vmid - vmin) / (vmax - vmin) else: self.midpoint = None def __call__(self, value, clip=None): #read in parameters method = self.stretch exponent = self.exponent midpoint = self.midpoint # ORIGINAL MATPLOTLIB CODE if clip is None: clip = self.clip if cbook.iterable(value): vtype = 'array' val = ma.asarray(value).astype(np.float) else: vtype = 'scalar' val = ma.array([value]).astype(np.float) self.autoscale_None(val) vmin, vmax = self.vmin, self.vmax if vmin > vmax: raise ValueError("minvalue must be less than or equal to maxvalue") elif vmin == vmax: return 0.0 * val else: if clip: mask = ma.getmask(val) val = ma.array(np.clip(val.filled(vmax), vmin, vmax), mask=mask) result = (val - vmin) * (1.0 / (vmax - vmin)) # CUSTOM APLPY CODE # Keep track of negative values negative = result < 0. if self.stretch == 'linear': pass elif self.stretch == 'log': result = ma.log10(result * (self.midpoint - 1.) + 1.) \ / ma.log10(self.midpoint) elif self.stretch == 'sqrt': result = ma.sqrt(result) elif self.stretch == 'arcsinh': result = ma.arcsinh(result / self.midpoint) \ / ma.arcsinh(1. / self.midpoint) elif self.stretch == 'power': result = ma.power(result, exponent) else: raise Exception("Unknown stretch in APLpyNormalize: %s" % self.stretch) # Now set previously negative values to 0, as these are # different from true NaN values in the FITS image result[negative] = -np.inf if vtype == 'scalar': result = result[0] return result def inverse(self, value): # ORIGINAL MATPLOTLIB CODE if not self.scaled(): raise ValueError("Not invertible until scaled") vmin, vmax = self.vmin, self.vmax # CUSTOM APLPY CODE if cbook.iterable(value): val = ma.asarray(value) else: val = value if self.stretch == 'linear': pass elif self.stretch == 'log': val = (ma.power(10., val * ma.log10(self.midpoint)) - 1.) / (self.midpoint - 1.) elif self.stretch == 'sqrt': val = val * val elif self.stretch == 'arcsinh': val = self.midpoint * \ ma.sinh(val * ma.arcsinh(1. / self.midpoint)) elif self.stretch == 'power': val = ma.power(val, (1. / self.exponent)) else: raise Exception("Unknown stretch in APLpyNormalize: %s" % self.stretch) return vmin + val * (vmax - vmin) APLpy-1.0/aplpy/overlays.py0000644000077000000240000005007212471102127015621 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import warnings from mpl_toolkits.axes_grid.anchored_artists \ import AnchoredEllipse, AnchoredSizeBar import numpy as np from matplotlib.patches import FancyArrowPatch from matplotlib.font_manager import FontProperties from astropy import units as u from astropy.extern import six from . import wcs_util from .decorators import auto_refresh corners = {} corners['top right'] = 1 corners['top left'] = 2 corners['bottom left'] = 3 corners['bottom right'] = 4 corners['right'] = 5 corners['left'] = 6 corners['bottom'] = 8 corners['top'] = 9 class Compass(object): def _initialize_compass(self): # Initialize compass holder self._compass = None self._compass_show = False # Set grid event handler self._ax1.callbacks.connect('xlim_changed', self.update_compass) self._ax1.callbacks.connect('ylim_changed', self.update_compass) @auto_refresh def show_compass(self, color='red', length=0.1, corner=4, frame=True): ''' Display a scalebar. Parameters ---------- length : float, optional The length of the scalebar label : str, optional Label to place above the scalebar corner : int, optional Where to place the scalebar. Acceptable values are:, 'left', 'right', 'top', 'bottom', 'top left', 'top right', 'bottom left' (default), 'bottom right' frame : str, optional Whether to display a frame behind the scalebar (default is False) kwargs Additional keyword arguments can be used to control the appearance of the scalebar, which is made up of an instance of the matplotlib Rectangle class and a an instance of the Text class. For more information on available arguments, see `Rectangle `_ and `Text `_`. In cases where the same argument exists for the two objects, the argument is passed to both the Text and Rectangle instance ''' w = 2 * length pos = {1: (1 - w, 1 - w), 2: (w, 1 - w), 3: (w, w), 4: (1 - w, w), 5: (1 - w, 0.5), 6: (w, 0.5), 7: (1 - w, 0.5), 8: (0.5, w), 9: (0.5, 1 - w)} self._compass_position = pos[corner] self._compass_length = length self._compass_color = color self._compass = None self._compass_show = True self.update_compass() @auto_refresh def update_compass(self, *args, **kwargs): if not self._compass_show: return rx, ry = self._compass_position length = self._compass_length color = self._compass_color xmin, xmax = self._ax1.get_xlim() ymin, ymax = self._ax1.get_ylim() x0 = rx * (xmax - xmin) + xmin y0 = ry * (ymax - ymin) + ymin xw, yw = self.pixel2world(x0, y0) len_pix = length * (ymax - ymin) degrees_per_pixel = wcs_util.celestial_pixel_scale(self._wcs) len_deg = len_pix * degrees_per_pixel # Should really only do tiny displacement then magnify the vectors - important if there is curvature x1, y1 = self.world2pixel(xw + len_deg / np.cos(np.radians(yw)), yw) x2, y2 = self.world2pixel(xw, yw + len_deg) if self._compass: self._compass[0].remove() self._compass[1].remove() arrow1 = FancyArrowPatch(posA=(x0, y0), posB=(x1, y1), arrowstyle='-|>', mutation_scale=20., fc=color, ec=color, shrinkA=0., shrinkB=0.) arrow2 = FancyArrowPatch(posA=(x0, y0), posB=(x2, y2), arrowstyle='-|>', mutation_scale=20., fc=color, ec=color, shrinkA=0., shrinkB=0.) self._compass = (arrow1, arrow2) self._ax1.add_patch(arrow1) self._ax1.add_patch(arrow2) @auto_refresh def hide_compass(self): pass class Scalebar(object): def __init__(self, parent): # Retrieve info from parent figure self._ax = parent._ax1 self._wcs = parent._wcs self._figure = parent._figure # Save plotting parameters (required for @auto_refresh) self._parameters = parent._parameters # Initialize settings self._base_settings = {} self._scalebar_settings = {} self._label_settings = {} self._label_settings['fontproperties'] = FontProperties() # LAYOUT @auto_refresh def show(self, length, label=None, corner='bottom right', frame=False, borderpad=0.4, pad=0.5, **kwargs): ''' Overlay a scale bar on the image. Parameters ---------- length : float, or quantity The length of the scalebar in degrees, an angular quantity, or angular unit label : str, optional Label to place below the scalebar corner : int, optional Where to place the scalebar. Acceptable values are:, 'left', 'right', 'top', 'bottom', 'top left', 'top right', 'bottom left' (default), 'bottom right' frame : str, optional Whether to display a frame behind the scalebar (default is False) kwargs Additional arguments are passed to the matplotlib Rectangle and Text classes. See the matplotlib documentation for more details. In cases where the same argument exists for the two objects, the argument is passed to both the Text and Rectangle instance. ''' self._length = length self._base_settings['corner'] = corner self._base_settings['frame'] = frame self._base_settings['borderpad'] = borderpad self._base_settings['pad'] = pad if isinstance(length, u.Quantity): length = length.to(u.degree).value elif isinstance(length, u.Unit): length = length.to(u.degree) degrees_per_pixel = wcs_util.celestial_pixel_scale(self._wcs) length = length / degrees_per_pixel try: self._scalebar.remove() except: pass if isinstance(corner, six.string_types): corner = corners[corner] self._scalebar = AnchoredSizeBar(self._ax.transData, length, label, corner, \ pad=pad, borderpad=borderpad, sep=5, frameon=frame) self._ax.add_artist(self._scalebar) self.set(**kwargs) @auto_refresh def _remove(self): self._scalebar.remove() @auto_refresh def hide(self): ''' Hide the scalebar. ''' try: self._scalebar.remove() except: pass @auto_refresh def set_length(self, length): ''' Set the length of the scale bar. ''' self.show(length, **self._base_settings) self._set_scalebar_properties(**self._scalebar_settings) self._set_label_properties(**self._scalebar_settings) @auto_refresh def set_label(self, label): ''' Set the label of the scale bar. ''' self._set_label_properties(text=label) @auto_refresh def set_corner(self, corner): ''' Set where to place the scalebar. Acceptable values are 'left', 'right', 'top', 'bottom', 'top left', 'top right', 'bottom left' (default), and 'bottom right'. ''' self._base_settings['corner'] = corner self.show(self._length, **self._base_settings) self._set_scalebar_properties(**self._scalebar_settings) self._set_label_properties(**self._scalebar_settings) @auto_refresh def set_frame(self, frame): ''' Set whether to display a frame around the scalebar. ''' self._base_settings['frame'] = frame self.show(self._length, **self._base_settings) self._set_scalebar_properties(**self._scalebar_settings) self._set_label_properties(**self._scalebar_settings) # APPEARANCE @auto_refresh def set_linewidth(self, linewidth): ''' Set the linewidth of the scalebar, in points. ''' self._set_scalebar_properties(linewidth=linewidth) @auto_refresh def set_linestyle(self, linestyle): ''' Set the linestyle of the scalebar. Should be one of 'solid', 'dashed', 'dashdot', or 'dotted'. ''' self._set_scalebar_properties(linestyle=linestyle) @auto_refresh def set_alpha(self, alpha): ''' Set the alpha value (transparency). This should be a floating point value between 0 and 1. ''' self._set_scalebar_properties(alpha=alpha) self._set_label_properties(alpha=alpha) @auto_refresh def set_color(self, color): ''' Set the label and scalebar color. ''' self._set_scalebar_properties(color=color) self._set_label_properties(color=color) @auto_refresh def set_font(self, family=None, style=None, variant=None, stretch=None, weight=None, size=None, fontproperties=None): ''' Set the font of the tick labels Parameters ---------- common: family, style, variant, stretch, weight, size, fontproperties Notes ----- Default values are set by matplotlib or previously set values if set_font has already been called. Global default values can be set by editing the matplotlibrc file. ''' if family: self._label_settings['fontproperties'].set_family(family) if style: self._label_settings['fontproperties'].set_style(style) if variant: self._label_settings['fontproperties'].set_variant(variant) if stretch: self._label_settings['fontproperties'].set_stretch(stretch) if weight: self._label_settings['fontproperties'].set_weight(weight) if size: self._label_settings['fontproperties'].set_size(size) if fontproperties: self._label_settings['fontproperties'] = fontproperties self._set_label_properties(fontproperties=self._label_settings['fontproperties']) @auto_refresh def _set_label_properties(self, **kwargs): ''' Modify the scalebar label properties. All arguments are passed to the matplotlib Text class. See the matplotlib documentation for more details. ''' for kwarg in kwargs: self._label_settings[kwarg] = kwargs[kwarg] self._scalebar.txt_label.get_children()[0].set(**kwargs) @auto_refresh def _set_scalebar_properties(self, **kwargs): ''' Modify the scalebar properties. All arguments are passed to the matplotlib Rectangle class. See the matplotlib documentation for more details. ''' for kwarg in kwargs: self._scalebar_settings[kwarg] = kwargs[kwarg] self._scalebar.size_bar.get_children()[0].set(**kwargs) @auto_refresh def set(self, **kwargs): ''' Modify the scalebar and scalebar properties. All arguments are passed to the matplotlib Rectangle and Text classes. See the matplotlib documentation for more details. In cases where the same argument exists for the two objects, the argument is passed to both the Text and Rectangle instance. ''' for kwarg in kwargs: kwargs_single = {kwarg: kwargs[kwarg]} try: self._set_label_properties(**kwargs_single) except AttributeError: pass try: self._set_scalebar_properties(**kwargs_single) except AttributeError: pass # DEPRECATED @auto_refresh def set_font_family(self, family): warnings.warn("scalebar.set_font_family is deprecated - use scalebar.set_font instead", DeprecationWarning) self.set_font(family=family) @auto_refresh def set_font_weight(self, weight): warnings.warn("scalebar.set_font_weight is deprecated - use scalebar.set_font instead", DeprecationWarning) self.set_font(weight=weight) @auto_refresh def set_font_size(self, size): warnings.warn("scalebar.set_font_size is deprecated - use scalebar.set_font instead", DeprecationWarning) self.set_font(size=size) @auto_refresh def set_font_style(self, style): warnings.warn("scalebar.set_font_style is deprecated - use scalebar.set_font instead", DeprecationWarning) self.set_font(style=style) # For backward-compatibility ScaleBar = Scalebar class Beam(object): def __init__(self, parent): # Retrieve info from parent figure self._figure = parent._figure self._header = parent._header self._ax = parent._ax1 self._wcs = parent._wcs # Save plotting parameters (required for @auto_refresh) self._parameters = parent._parameters # Initialize settings self._base_settings = {} self._beam_settings = {} # LAYOUT @auto_refresh def show(self, major='BMAJ', minor='BMIN', \ angle='BPA', corner='bottom left', frame=False, borderpad=0.4, pad=0.5, **kwargs): ''' Display the beam shape and size for the primary image. By default, this method will search for the BMAJ, BMIN, and BPA keywords in the FITS header to set the major and minor axes and the position angle on the sky. Parameters ---------- major : float, quantity or unit, optional Major axis of the beam in degrees or an angular quantity (overrides BMAJ if present) minor : float, quantity or unit, optional Minor axis of the beam in degrees or an angular quantity (overrides BMIN if present) angle : float, quantity or unit, optional Position angle of the beam on the sky in degrees or an angular quantity (overrides BPA if present) in the anticlockwise direction. corner : int, optional The beam location. Acceptable values are 'left', 'right', 'top', 'bottom', 'top left', 'top right', 'bottom left' (default), and 'bottom right'. frame : str, optional Whether to display a frame behind the beam (default is False) kwargs Additional arguments are passed to the matplotlib Ellipse class. See the matplotlib documentation for more details. ''' if isinstance(major, six.string_types): major = self._header[major] if isinstance(minor, six.string_types): minor = self._header[minor] if isinstance(angle, six.string_types): angle = self._header[angle] if isinstance(major, u.Quantity): major = major.to(u.degree).value elif isinstance(major, u.Unit): major = major.to(u.degree) if isinstance(minor, u.Quantity): minor = minor.to(u.degree).value elif isinstance(minor, u.Unit): minor = minor.to(u.degree) if isinstance(angle, u.Quantity): angle = angle.to(u.degree).value elif isinstance(angle, u.Unit): angle = angle.to(u.degree) degrees_per_pixel = wcs_util.celestial_pixel_scale(self._wcs) self._base_settings['minor'] = minor self._base_settings['major'] = major self._base_settings['angle'] = angle self._base_settings['corner'] = corner self._base_settings['frame'] = frame self._base_settings['borderpad'] = borderpad self._base_settings['pad'] = pad minor /= degrees_per_pixel major /= degrees_per_pixel try: self._beam.remove() except: pass if isinstance(corner, six.string_types): corner = corners[corner] self._beam = AnchoredEllipse(self._ax.transData, \ width=minor, height=major, angle=angle, \ loc=corner, pad=pad, borderpad=borderpad, frameon=frame) self._ax.add_artist(self._beam) self.set(**kwargs) @auto_refresh def _remove(self): self._beam.remove() @auto_refresh def hide(self): ''' Hide the beam ''' try: self._beam.remove() except: pass @auto_refresh def set_major(self, major): ''' Set the major axis of the beam, in degrees. ''' self._base_settings['major'] = major self.show(**self._base_settings) self.set(**self._beam_settings) @auto_refresh def set_minor(self, minor): ''' Set the minor axis of the beam, in degrees. ''' self._base_settings['minor'] = minor self.show(**self._base_settings) self.set(**self._beam_settings) @auto_refresh def set_angle(self, angle): ''' Set the position angle of the beam on the sky, in degrees. ''' self._base_settings['angle'] = angle self.show(**self._base_settings) self.set(**self._beam_settings) @auto_refresh def set_corner(self, corner): ''' Set the beam location. Acceptable values are 'left', 'right', 'top', 'bottom', 'top left', 'top right', 'bottom left' (default), and 'bottom right'. ''' self._base_settings['corner'] = corner self.show(**self._base_settings) self.set(**self._beam_settings) @auto_refresh def set_frame(self, frame): ''' Set whether to display a frame around the beam. ''' self._base_settings['frame'] = frame self.show(**self._base_settings) self.set(**self._beam_settings) @auto_refresh def set_borderpad(self, borderpad): ''' Set the amount of padding within the beam object, relative to the canvas size. ''' self._base_settings['borderpad'] = borderpad self.show(**self._base_settings) self.set(**self._beam_settings) @auto_refresh def set_pad(self, pad): ''' Set the amount of padding between the beam object and the image corner/edge, relative to the canvas size. ''' self._base_settings['pad'] = pad self.show(**self._base_settings) self.set(**self._beam_settings) # APPEARANCE @auto_refresh def set_alpha(self, alpha): ''' Set the alpha value (transparency). This should be a floating point value between 0 and 1. ''' self.set(alpha=alpha) @auto_refresh def set_color(self, color): ''' Set the beam color. ''' self.set(color=color) @auto_refresh def set_edgecolor(self, edgecolor): ''' Set the color for the edge of the beam. ''' self.set(edgecolor=edgecolor) @auto_refresh def set_facecolor(self, facecolor): ''' Set the color for the interior of the beam. ''' self.set(facecolor=facecolor) @auto_refresh def set_linestyle(self, linestyle): ''' Set the line style for the edge of the beam. This should be one of 'solid', 'dashed', 'dashdot', or 'dotted'. ''' self.set(linestyle=linestyle) @auto_refresh def set_linewidth(self, linewidth): ''' Set the line width for the edge of the beam, in points. ''' self.set(linewidth=linewidth) @auto_refresh def set_hatch(self, hatch): ''' Set the hatch pattern. This should be one of '/', '\', '|', '-', '+', 'x', 'o', 'O', '.', or '*'. ''' self.set(hatch=hatch) @auto_refresh def set(self, **kwargs): ''' Modify the beam properties. All arguments are passed to the matplotlib Ellipse class. See the matplotlib documentation for more details. ''' for kwarg in kwargs: self._beam_settings[kwarg] = kwargs[kwarg] self._beam.ellipse.set(**kwargs) APLpy-1.0/aplpy/regions.py0000644000077000000240000001255012471102127015422 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division from astropy.extern import six from astropy import log from .decorators import auto_refresh class Regions: """ Regions sub-class of APLpy. Used for overplotting various shapes and annotations on APLpy fitsfigures. Example: # DS9 region file called "test.reg" # (the coordinates are around l=28 in the Galactic Plane) # Filename: test.fits fk5 box(18:42:48.262,-04:01:17.91,505.668",459.714",0) # color=red dash=1 point(18:42:51.797,-03:59:44.82) # point=x color=red dash=1 point(18:42:50.491,-04:03:09.39) # point=box color=red dash=1 # vector(18:42:37.433,-04:02:10.77,107.966",115.201) vector=1 color=red dash=1 ellipse(18:42:37.279,-04:02:11.92,26.4336",40.225",0) # color=red dash=1 polygon(18:42:59.016,-03:58:22.06,18:42:58.219,-03:58:11.30,18:42:57.403,-03:58:35.86,18:42:58.094,-03:58:57.69,18:42:59.861,-03:58:41.60,18:42:59.707,-03:58:23.21) # color=red dash=1 point(18:42:52.284,-04:00:02.80) # point=diamond color=red dash=1 point(18:42:46.561,-03:58:01.57) # point=circle color=red dash=1 point(18:42:42.615,-03:58:25.84) # point=cross color=red dash=1 point(18:42:42.946,-04:01:44.74) # point=arrow color=red dash=1 point(18:42:41.961,-03:57:26.16) # point=boxcircle color=red dash=1 # text(18:42:41.961,-03:57:26.16) text={This is text} color=red Code: import aplpy import regions ff = aplpy.FITSFigure("test.fits") ff.show_grayscale() ff.show_regions('test.reg') """ @auto_refresh def show_regions(self, region_file, layer=False, **kwargs): """ Overplot regions as specified in the region file. Parameters ---------- region_file: string or pyregion.ShapeList Path to a ds9 regions file or a ShapeList already read in by pyregion. layer: str, optional The name of the layer kwargs Additional keyword arguments, e.g. zorder, will be passed to the ds9 call and onto the patchcollections. """ PC, TC = ds9(region_file, self._header, **kwargs) # ffpc = self._ax1.add_collection(PC) PC.add_to_axes(self._ax1) TC.add_to_axes(self._ax1) if layer: region_set_name = layer else: self._region_counter += 1 region_set_name = 'region_set_' + str(self._region_counter) self._layers[region_set_name] = PC self._layers[region_set_name + "_txt"] = TC def ds9(region_file, header, zorder=3, **kwargs): """ Wrapper to return a PatchCollection given a ds9 region file and a fits header. zorder - defaults to 3 so that regions are on top of contours """ try: import pyregion except: raise ImportError("The pyregion package is required to load region files") # read region file if isinstance(region_file, six.string_types): rr = pyregion.open(region_file) elif isinstance(region_file, pyregion.ShapeList): rr = region_file else: raise Exception("Invalid type for region_file: %s - should be string or pyregion.ShapeList" % type(region_file)) # convert coordinates to image coordinates rrim = rr.as_imagecoord(header) # pyregion and aplpy both correct for the FITS standard origin=1,1 # need to avoid double-correcting. Also, only some items in `coord_list` # are pixel coordinates, so which ones should be corrected depends on the # shape. for r in rrim: if r.name == 'polygon': correct = range(len(r.coord_list)) elif r.name == 'line': correct = range(4) elif r.name in ['rotbox', 'box', 'ellipse', 'annulus', 'circle', 'panda', 'pie', 'epanda', 'text', 'point', 'vector']: correct = range(2) else: log.warning("Unknown region type '{0}' - please report to the developers") correct = range(2) for i in correct: r.coord_list[i] += 1 if 'text_offset' in kwargs: text_offset = kwargs['text_offset'] del kwargs['text_offset'] else: text_offset = 5.0 # grab the shapes to overplot pp, aa = rrim.get_mpl_patches_texts(text_offset=text_offset) PC = ArtistCollection(pp, **kwargs) # preserves line style (dashed) TC = ArtistCollection(aa, **kwargs) PC.set_zorder(zorder) TC.set_zorder(zorder) return PC, TC class ArtistCollection(): """ Matplotlib collections can't handle Text. This is a barebones collection for text objects that supports removing and making (in)visible """ def __init__(self, artistlist): """ Pass in a list of matplotlib.text.Text objects (or possibly any matplotlib Artist will work) """ self.artistlist = artistlist def remove(self): for T in self.artistlist: T.remove() def add_to_axes(self, ax): for T in self.artistlist: ax.add_artist(T) def get_visible(self): visible = True for T in self.artistlist: if not T.get_visible(): visible = False return visible def set_visible(self, visible=True): for T in self.artistlist: T.set_visible(visible) def set_zorder(self, zorder): for T in self.artistlist: T.set_zorder(zorder) APLpy-1.0/aplpy/rgb.py0000644000077000000240000003026712471102127014533 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division from distutils import version import os import warnings import tempfile import shutil import numpy as np from astropy.extern import six from astropy import log from astropy.io import fits from . import image_util from . import math_util def _data_stretch(image, vmin=None, vmax=None, pmin=0.25, pmax=99.75, stretch='linear', vmid=None, exponent=2): min_auto = not math_util.isnumeric(vmin) max_auto = not math_util.isnumeric(vmax) if min_auto or max_auto: auto_v = image_util.percentile_function(image) vmin_auto, vmax_auto = auto_v(pmin), auto_v(pmax) if min_auto: log.info("vmin = %10.3e (auto)" % vmin_auto) vmin = vmin_auto else: log.info("vmin = %10.3e" % vmin) if max_auto: log.info("vmax = %10.3e (auto)" % vmax_auto) vmax = vmax_auto else: log.info("vmax = %10.3e" % vmax) image = (image - vmin) / (vmax - vmin) data = image_util.stretch(image, stretch, exponent=exponent, midpoint=vmid) data = np.nan_to_num(data) data = np.clip(data * 255., 0., 255.) return data.astype(np.uint8) def make_rgb_image(data, output, indices=(0, 1, 2), \ vmin_r=None, vmax_r=None, pmin_r=0.25, pmax_r=99.75, \ stretch_r='linear', vmid_r=None, exponent_r=2, \ vmin_g=None, vmax_g=None, pmin_g=0.25, pmax_g=99.75, \ stretch_g='linear', vmid_g=None, exponent_g=2, \ vmin_b=None, vmax_b=None, pmin_b=0.25, pmax_b=99.75, \ stretch_b='linear', vmid_b=None, exponent_b=2, \ make_nans_transparent=False, \ embed_avm_tags=True): ''' Make an RGB image from a FITS RGB cube or from three FITS files. Parameters ---------- data : str or tuple or list If a string, this is the filename of an RGB FITS cube. If a tuple or list, this should give the filename of three files to use for the red, green, and blue channel. output : str The output filename. The image type (e.g. PNG, JPEG, TIFF, ...) will be determined from the extension. Any image type supported by the Python Imaging Library can be used. indices : tuple, optional If data is the filename of a FITS cube, these indices are the positions in the third dimension to use for red, green, and blue respectively. The default is to use the first three indices. vmin_r, vmin_g, vmin_b : float, optional Minimum pixel value to use for the red, green, and blue channels. If set to None for a given channel, the minimum pixel value for that channel is determined using the corresponding pmin_x argument (default). vmax_r, vmax_g, vmax_b : float, optional Maximum pixel value to use for the red, green, and blue channels. If set to None for a given channel, the maximum pixel value for that channel is determined using the corresponding pmax_x argument (default). pmin_r, pmin_r, pmin_g : float, optional Percentile values used to determine for a given channel the minimum pixel value to use for that channel if the corresponding vmin_x is set to None. The default is 0.25% for all channels. pmax_r, pmax_g, pmax_b : float, optional Percentile values used to determine for a given channel the maximum pixel value to use for that channel if the corresponding vmax_x is set to None. The default is 99.75% for all channels. stretch_r, stretch_g, stretch_b : { 'linear', 'log', 'sqrt', 'arcsinh', 'power' } The stretch function to use for the different channels. vmid_r, vmid_g, vmid_b : float, optional Baseline values used for the log and arcsinh stretches. If set to None, this is set to zero for log stretches and to vmin - (vmax - vmin) / 30. for arcsinh stretches exponent_r, exponent_g, exponent_b : float, optional If stretch_x is set to 'power', this is the exponent to use. make_nans_transparent : bool, optional If set AND output is png, will add an alpha layer that sets pixels containing a NaN to transparent. embed_avm_tags : bool, optional Whether to embed AVM tags inside the image - this can only be done for JPEG and PNG files, and only if PyAVM is installed. ''' try: from PIL import Image except ImportError: try: import Image except ImportError: raise ImportError("The Python Imaging Library (PIL) is required to make an RGB image") if isinstance(data, six.string_types): image = fits.getdata(data) image_r = image[indices[0], :, :] image_g = image[indices[1], :, :] image_b = image[indices[2], :, :] # Read in header header = fits.getheader(data) # Remove information about third dimension header['NAXIS'] = 2 for key in ['NAXIS', 'CTYPE', 'CRPIX', 'CRVAL', 'CUNIT', 'CDELT', 'CROTA']: for coord in range(3, 6): name = key + str(coord) if name in header: header.__delitem__(name) elif (type(data) == list or type(data) == tuple) and len(data) == 3: filename_r, filename_g, filename_b = data image_r = fits.getdata(filename_r) image_g = fits.getdata(filename_g) image_b = fits.getdata(filename_b) # Read in header header = fits.getheader(filename_r) else: raise Exception("data should either be the filename of a FITS cube or a list/tuple of three images") # are we making a transparent layer? do_alpha = make_nans_transparent and output.lower().endswith('.png') if do_alpha: log.info("Making alpha layer") # initialize alpha layer image_alpha = np.empty_like(image_r, dtype=np.uint8) image_alpha[:] = 255 # look for nans in images for im in [image_r, image_g, image_b]: image_alpha[np.isnan(im)] = 0 log.info("Red:") image_r = Image.fromarray(_data_stretch(image_r, \ vmin=vmin_r, vmax=vmax_r, \ pmin=pmin_r, pmax=pmax_r, \ stretch=stretch_r, \ vmid=vmid_r, \ exponent=exponent_r)) log.info("Green:") image_g = Image.fromarray(_data_stretch(image_g, \ vmin=vmin_g, vmax=vmax_g, \ pmin=pmin_g, pmax=pmax_g, \ stretch=stretch_g, \ vmid=vmid_g, \ exponent=exponent_g)) log.info("Blue:") image_b = Image.fromarray(_data_stretch(image_b, \ vmin=vmin_b, vmax=vmax_b, \ pmin=pmin_b, pmax=pmax_b, \ stretch=stretch_b, \ vmid=vmid_b, \ exponent=exponent_b)) img = Image.merge("RGB", (image_r, image_g, image_b)) if do_alpha: # convert to RGBA and add alpha layer image_alpha = Image.fromarray(image_alpha) img.convert("RGBA") img.putalpha(image_alpha) img = img.transpose(Image.FLIP_TOP_BOTTOM) img.save(output) if embed_avm_tags: try: import pyavm except ImportError: warnings.warn("PyAVM 0.9.1 or later is not installed, so AVM tags will not be embedded in RGB image") return if version.LooseVersion(pyavm.__version__) < version.LooseVersion('0.9.1'): warnings.warn("PyAVM 0.9.1 or later is not installed, so AVM tags will not be embedded in RGB image") return from pyavm import AVM if output.lower().endswith(('.jpg', '.jpeg', '.png')): avm = AVM.from_header(header) avm.embed(output, output) else: warnings.warn("AVM tags will not be embedded in RGB image, as only JPEG and PNG files are supported") def make_rgb_cube(files, output, north=False, system=None, equinox=None): ''' Make an RGB data cube from a list of three FITS images. This method can read in three FITS files with different projections/sizes/resolutions and uses Montage to reproject them all to the same projection. Two files are produced by this function. The first is a three-dimensional FITS cube with a filename give by `output`, where the third dimension contains the different channels. The second is a two-dimensional FITS image with a filename given by `output` with a `_2d` suffix. This file contains the mean of the different channels, and is required as input to FITSFigure if show_rgb is subsequently used to show a color image generated from the FITS cube (to provide the correct WCS information to FITSFigure). Parameters ---------- files : tuple or list A list of the filenames of three FITS filename to reproject. The order is red, green, blue. output : str The filename of the output RGB FITS cube. north : bool, optional By default, the FITS header generated by Montage represents the best fit to the images, often resulting in a slight rotation. If you want north to be straight up in your final mosaic, you should use this option. system : str, optional Specifies the system for the header (default is EQUJ). Possible values are: EQUJ EQUB ECLJ ECLB GAL SGAL equinox : str, optional If a coordinate system is specified, the equinox can also be given in the form YYYY. Default is J2000. ''' # Check whether the Python montage module is installed. The Python module # checks itself whether the Montage command-line tools are available, and # if they are not then importing the Python module will fail. try: import montage_wrapper as montage except ImportError: raise Exception("Both the Montage command-line tools and the" " montage-wrapper Python module are required" " for this function") # Check that input files exist for f in files: if not os.path.exists(f): raise Exception("File does not exist : " + f) # Create work directory work_dir = tempfile.mkdtemp() raw_dir = '%s/raw' % work_dir final_dir = '%s/final' % work_dir images_raw_tbl = '%s/images_raw.tbl' % work_dir header_hdr = '%s/header.hdr' % work_dir # Create raw and final directory in work directory os.mkdir(raw_dir) os.mkdir(final_dir) # Create symbolic links to input files for i, f in enumerate(files): os.symlink(os.path.abspath(f), '%s/image_%i.fits' % (raw_dir, i)) # List files and create optimal header montage.mImgtbl(raw_dir, images_raw_tbl, corners=True) montage.mMakeHdr(images_raw_tbl, header_hdr, north_aligned=north, system=system, equinox=equinox) # Read header in with astropy.io.fits header = fits.Header.fromtextfile(header_hdr) # Find image dimensions nx = int(header['NAXIS1']) ny = int(header['NAXIS2']) # Generate empty datacube image_cube = np.zeros((len(files), ny, nx), dtype=np.float32) # Loop through files for i in range(len(files)): # Reproject channel to optimal header montage.reproject('%s/image_%i.fits' % (raw_dir, i), '%s/image_%i.fits' % (final_dir, i), header=header_hdr, exact_size=True, bitpix=-32) # Read in and add to datacube image_cube[i, :, :] = fits.getdata('%s/image_%i.fits' % (final_dir, i)) # Write out final cube fits.writeto(output, image_cube, header, clobber=True) # Write out collapsed version of cube fits.writeto(output.replace('.fits', '_2d.fits'), \ np.mean(image_cube, axis=0), header, clobber=True) # Remove work directory shutil.rmtree(work_dir) APLpy-1.0/aplpy/scalar_util.py0000644000077000000240000000425612471065325016272 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import numpy as np from . import math_util def smart_round_angle_decimal(x, latitude=False): x = np.log10(x) e = np.floor(x) x -= e x = 10. ** x divisors_10 = math_util.divisors(10) x = math_util.closest(divisors_10, x) x = x * 10. ** e return x def _get_label_precision(format): if format[0] == "%": if "i" in format: return 1 elif "f" in format: return 10. ** (-len((format % 1).split('.')[1])) elif "e" in format: return None # need to figure this out else: return None # need to figure this out elif "." in format: return 10 ** (-len(format.split('.')[1])) else: return 1 class InconsistentSpacing(Exception): pass def _check_format_spacing_consistency(format, spacing): ''' Check whether the format can correctly show labels with the specified spacing. For example, if the tick spacing is set to 1 arcsecond, but the format is set to dd:mm, then the labels cannot be correctly shown. Similarly, if the spacing is set to 1/1000 of a degree, or 3.6", then a format of dd:mm:ss will cause rounding errors, because the spacing includes fractional arcseconds. This function will raise a warning if the format and spacing are inconsistent. ''' label_spacing = _get_label_precision(format) if label_spacing is None: return # can't determine minimum spacing, so can't check if spacing % label_spacing != 0.: raise InconsistentSpacing('Label format and tick spacing are inconsistent. Make sure that the tick spacing is a multiple of the smallest angle that can be represented by the specified format (currently %s). For example, if the format is dd:mm:ss.s, then the tick spacing has to be a multiple of 0.1". Similarly, if the format is hh:mm:ss, then the tick spacing has to be a multiple of 15". If you got this error as a result of interactively zooming in to a small region, this means that the default display format for the labels is not accurate enough, so you will need to increase the format precision.' % format) APLpy-1.0/aplpy/setup_package.py0000644000077000000240000000010512244700136016562 0ustar tomstaff00000000000000def get_package_data(): return {'aplpy.tests': ['data/*/*.hdr']} APLpy-1.0/aplpy/slicer.py0000644000077000000240000000336312471065325015247 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division def slice_hypercube(data, header, dimensions=[0, 1], slices=[]): ''' Extract a slice from an n-dimensional HDU data/header pair, and return the new data (without changing the header). ''' if type(slices) == int: slices = (slices, ) else: slices = slices[:] shape = data.shape if len(shape) < 2: raise Exception("FITS file does not have enough dimensions") elif len(shape) == 2: if dimensions[1] < dimensions[0]: data = data.transpose() return data else: if slices: if dimensions[0] < dimensions[1]: slices.insert(dimensions[0], slice(None, None, None)) slices.insert(dimensions[1], slice(None, None, None)) else: slices.insert(dimensions[1], slice(None, None, None)) slices.insert(dimensions[0], slice(None, None, None)) if type(slices) == list: slices = tuple(slices) data = data[slices[::-1]] if dimensions[1] < dimensions[0]: data = data.transpose() else: message = ''' Attempted to read in %i-dimensional FITS cube, but dimensions and slices were not specified. Please specify these using the dimensions= and slices= argument. The cube dimensions are:\n\n''' % len(shape) for i in range(1, len(shape) + 1): message += " " * 10 message += " %i %s %i\n" % (i - 1, header["CTYPE%i" % i], header["NAXIS%i" % i]) raise Exception(message) return data APLpy-1.0/aplpy/tests/0000755000077000000240000000000012471104150014537 5ustar tomstaff00000000000000APLpy-1.0/aplpy/tests/__init__.py0000644000077000000240000000000012244700136016642 0ustar tomstaff00000000000000APLpy-1.0/aplpy/tests/baseline_images/0000755000077000000240000000000012471104150017646 5ustar tomstaff00000000000000APLpy-1.0/aplpy/tests/baseline_images/basic_image.png0000644000077000000240000006570612471065325022627 0ustar tomstaff00000000000000PNG  IHDRk"sBIT|d pHYsaa?i IDATx{tU?' Q4JR&Ђh墴j`Q/[E4cEt=Bmhb5 uo ժh5p~lֳtgsg$I U RHXH!a 5RHXH!a Z7w-Z/Bs;Gii k4٢EbРAt.\6HW***s';-}ٛ{ٛ{ٛ{ٛ{}wϞϿ5>wvX\ߒ?{sߒ?{sߒ?{sߒ?{s7 k)$2Ld+++j+++?RWsv-Ss~s֒ٛ[K[,˳Ցd;qM4cƌ$"ʫfA򷧚ݩ(w;W͘1cv$@ k)$|.\hhx5sBaGGswGsw#B@ k)$B@ k)$B@ k)$B@ k)$B@ k)$Bv~L&JJJەQUUՌ@:Eyyyv:2L^ klnR2 B@ k)$B@ k)$B@ k)$B@ k)$B@ k)$B@ k)$B@ k)Ժ`d$]YYUUUCYYYg#u6h |!m)$B@ k)$B@ k)$B@ k)$B@ k)$B@ k)$B@ k)$Bv~L&JJJەQUUՌ@:Eyyyv:2L^ klnR2 B@ k)$B@ k)$B@ k)$B@ k)$B@ k)SGy$D>}CQRRg}v ߿t1wGuT< .]4 :uuСCc=vEIIIs9lٲM+){go>_~yΜȑ#]vѫWꪫb Y&.ѣGo>si|&LnݺEǎcԨQpzSL#Fęgk 㢋.> N<W^iӦٳcѢE{f~;߉8{^q-a͋<0""cܸqѧO8o޽{ǤIZ`A <8N9ԩS_:xxcw]hQ1"/Ņ^{bŊ gOS1jԨ6mZ,^8xoΙ;~{{^qQG>h >ﱾ>>Xxq?w=n1bD<3~ύ$;'x?$_~yvO>I N:)g$\pAvlɒ%ɗdI$C= >|{IAAArme֯_ 0 6lXR[[gqF2rȜ~% $Ir'ɒ%KcO ٱdK>-~w'ɽޛ{wΝ;'rJ޽{'SLI$I;3lp3f$RJ)ʚ1cSy!4;CK.dɒX֭c]v=#gnnݢ0ڷo裏K.QXXz-ҫWhӦMv^x!(**?x0n$I_|x饗b„ ^""&NI̙3c3gΌ֭[DŽ cEEE~7|x7}̙ѽ{8c]vN:)dv"mիcժUѵkX֭~q.-[/G.]rB˗/[n%^~CZnOĤIO>9gove2dHt1:t볷nؐ!Cr^{-ʙۧOرc vnh2=1eʔ8n)8sc̘1QSS?㎋'x" qGcѪUk1gΜ D&:*SN':o~cǎ%\Oogmܚ'lvڜm:*{w3{kɒ%1v8p`z9sW\_|qwyӟ4;~Go9t5VjӦM5*"":=zt ><~ǥ^ڵON9唸K'lXp5k쫭0ws6>ld9QUUդΠA&d2y/Uam޼y`k{wDD,_<ƌ;w?ѡCO=Tű3~E߾}68lذ(..z*;6ɍ_!CK"6Ěٳgξ:thMVʍoZrTTT48"ڮ(RJKKyCzb̘1QWW>h`٧nIk׮um>?䓜}C [o;6 mSJKK#"駟yʕ+74hP}4z<9S`Aɂ 6;_N ?3IV'G}|G gΜ$L&;o&m۶M=Ф>;~%$7V߾}Ҽ߳(;=kÆ 9gMMMK/%uuuٱ Y9sfvwIvm7{֔RJ)jZ󞵝"wqIAAA~7oSfʙ[^^$'pB_2SNM 1c$/ &$ɩŵfϞ&GNOL4)iժUr94{I'%mڴI~$rKr'KqIAAAg֯_ 6,ԩSrW'/'V}7kJ)RJ50awIaaaRPPРgk׮Mʤ$iӦMҹscM}٭ZwM9眤o߾Iǎm&}M.dժU3<1xh׮]+X~}Μ;# ?xǢ0-[bŊCQPP.'^{7xc\q#o}y裏ʘ4iRp oLj#_ΙhѢ=ztM7guVL>=N<ļ>ß8K.1mڴ8k? `K'~Ɉ#ѣGկ~5g?L:uwqٱW_}5)((Hcw}wRPP{ٱwy'ܹsr)#Lz왬Z*;v뭷&?Я_dРAc_~yRXX,Y$;v'I$ɣ>$z͘1#RJ)V֌3wWǽ?O"IWڵkݺuԩSt!ڷogΜݻwN8!;ֵk8餓cΜ9qiEǎsoGǎ{it_|1^z饘0aBLj'F$1sƿs%mϏ>;9_|q-6&NGuTwˎwqe]UUU=]vf͚Mw[mܖ߰)2L4:vveeeQ^^d2yOXیo_:~Ċ+㵵vx_{/=؜;wÇy5Nqqq-lmGyvyanϞ=?tFoLEEE/zmoD}}}L4)wl=Stg}bԩnݺMn*-- D$9Ϗ:D>}""bѺuxs]6-Z[\'"rx7x<6+_Jw}1k֬lw}ѿիW̚5+F~A^bE<1hРF7n\[?!;8cM6뮻3f̈իWg>(֭%Kěo߿p1}ώ򗿌7n\Ӿ(`qa5x)%\$FJ~']w]^{%mڴIxFϷ~dذaIN:/~?u]K]`AҶmd/.Kڵk|șǏ={vRXX=:>}z2iҤUV9ӤKRJ)jZRlam+1"W`?iҿ(ԩS2z~rYg%]vM:t萌92yg69wܹÓv%{gr'WΙ!y 5kV2hРm۶{L<9Yn]^}~RJ)T*V$鮻N;Ό3SOmt߬B@ k)$B@ k)$B@ k)$B@ k)$B@ k)$Bv~L&JJJەQUUՌ@:Eyyyv:2L^ klnR2 B@ k)$B@ k)$B@ k)$B@ k)$Bzիcɒ%FAAAt5:uڞH[ǝw]xFy?wߍ#8"}ӧOyQ\\m۶ڨz*=ܸstmGwXe˖ş=zf=#q'ƿۿo~$@KwX{ /l4ED=:Ǐ~mnC&veeeTUU5cGeeeQ^^ݮL&ױykFN۩SXfMf'WQQ-@*UUU5BF>|xg?+V4:oŊӟ49&5V\Y馛k_Zpo~3 QTTk֬xcѾ}/Pao߾p¸{{nݺg^zi^۵Q$^{77|sQSS|Ik.gϞ;Oez) M k+Vg}6V\֣G(-- k͋OnvСCn4HmwX3)C IDATgNuQѫWh۶mFMMM̟??n=zt̞=;8;VA$I>Z/QTTyk֬QF뿶[]wvZs;3fĩ蜼_x83 jEEEqgij>n+DΝ@KwX;㦛nL&WUVŏ馛 ~W_˖- /0.ӧOGQQQY&V\K.lj'SNݑ}|֊2̙3c…QSS󞵣>:ƍtЎ o_}A c;X޿YW'fΜ-+WYZZƍ/;W#+kׯ'F~+G]]]t1+~Źׯߑ}|}emԩ1}KcĉQ\\`ʕ+㗿e\{Ɣ)Sk-EWn8bԩ j=zSywq;1p0 ~&7Q__o߾@Ko֮8cȐ!1a„2dHGQQQY&jjj駟ӧŋÎ -vƃ>^xaL8q8x裏. D[#<2?y_Y߭:qVcǎѪUn ;uuO^XXqqEy@k'Oު'IV~:.?_V7֮&/ro[a OFF555R_y&7>*++3όo-U^a .>}Ŀǫk?_uhiO?K.}'Ot9$?ˣk׮1iҤ?N^acǎqe~={v̚5+׿QRR#F;.9hӦm*AFDi&Ǝcǎue_ݥKhzNflSjݺu۫~B@ k)$P{/ŋgkRX &lv^xah1sL<#Mn kKy֭fw%z&7%DIIIv2#H(//nWWWG&&ݻǂ 69X***H&_hmcǎ￿YfmcǎmRC4ڕW^s̉cFiii 0 ""{xg_~1eʔ(@KҤ+k[h_}}ݛ@Kפ6rȸ;7|3~_ǘ1c9Iak˗ǁrKDD(@KҤvļyk׮1y䈈믏ܹsc}ٮ$z`cΜ9/G}}}ᄆ{lZ& tta6f̘W^UVENbqꩧ.#zh1*=sq믿O;vz+,XsO\{D~vH-AYzu{ou]˗/> VX|A,[,Xres1 nXlY̞=;.ٳg+.䒘={vkqofZڃ>GqD9yF?I$y衇ÇwG&={LVZ[ΎX"iӦMr}-g$I38#9rdμ~% ʙw'ɒ%KcO ٱdK>g}IAAArfy睤s)3wɔ)S$I;,93oƌID(RJ)f̘}em͚5ѦMn:֮]勺Ci0v衇F.]bɒ%ٱ'x"""r斕E$9W>ҥK~t-V^Ɯ9sN;fǿoGǎ{ɎnݺW_%VXO>dx$㥗^ &d{8qb$I3g̎͜93Zn&LȎwx'7h=ml̙ѽ{8c]vN:)dvzt ,^{mձjժڵkvl͚5Ѯ]7˗-G.,={.֭[C 9W6m4w^ .;3Oo>|xD4py>Nqqq^hѢ} ]~… c <>}z,]4sAoW\W\qŎe'?8䓳cܹsW^ W6Ծ}6mZ?>bqwfDħ鳺ws͙{6ؕ+Wf>-555u6&Fګ裏6z^`;vm[uy%)S'>:իW\xѾ}켍ϵ)[Zg[3em:ƏT]]]{9c{Go,Ycǎƭޚ3(|8餓[VD|Dnᆸk>o6r NnrcѾ}6eKl|lv6;oKl7'DIIIs*++7*(++FTWWG&|[u6o޼5jTk{wDD,_<ƌ;w?ѡCׯ_YKmsꩧO?w^^x!SNqqeO:ٳg7>ø^|pM6k>8;8cŊd_z3&;gϞq7F]]] 2$f͚s΍w[|ʍ7{l3&N>xE}/9;O<1.x$XlYL^|&6qСC3ό_|1v}#I2eV}7՗SA޽¤A>9soᆤo߾Iv.]$|6ys&ÇOڵk矿+t_;)**J$^g֬YɠAm&{w2ydݺu &m۶Wn0oIaaa猿Ygt5СC2rgɻύRJ)T*+kI0A8Ӛ ̘1#N=F#|JXH!a 5RHXH!a 5RHXH!a 5RHXH!a 5RHXH!a 5RHXH;L&%%%ʨjƎ ʢ<]]]L&c5YEEEsTUU nH!a 5RHXH!a 5RHXH!a 5RHXH!a 5RHXH!a 5RHXH!a 5RHXH!a 5j d2QRRݮfҡ,˳Ցd:VXcUTT4w JUUUM6HRHXH!a 5RHXH!a 5RHXH!a 5RHXH!a 5RHXH!a 5RHXH!a 5RHXH;L&%%%ʨjƎ ʢ<]]]L&c5YEEEsTUU nH!a 5RHXH!a 5RHXH!a 5RHXH!a 5RHXڻ%Ԝ03S1-M4R;vRJ)Lcf J)!̱Ar EsTc$<׵~˽^DX"k@5p 8a DX"yUpKHHPDDxJNNЯ_?ۏ-[2%w=ztEOpri@5p 8a DX"k@5p 8a DX"k@5p 8a'NO?oYUVoVcѼyԫW/խ[Wj޼&MӧOرC.K˗/wk?z jժ)00P:u҆ ʕ+ծ];(<<\F҉'|LbbbzgQ^^[ͼyr]K.Ү]<߇Vh„ _%I,˭ĉ{u! >\3fPVO[n)u|Cȑ#~SuVTuYٚ>} 9s(..L_wުZfΜ޽{kĉ1bD_ >}߿cڵkeYv1}Gg}Xe/^lm߾Xe-[f{Ʋ,pB&$$u]n[LڵMffo˲̗_~Y4mDGG#J?ŋk k@%?kӦM6l}$=2h%!'l*(7lؠ-[ɓ'yfI?3gΨEnuފ*H:u(55y;g,WppukW^^nF-##C kn/f͚v]qΧA/СTn \{9}ך={TRbmvv|||<}}}%INroqYYY%!{==2dR<_: /jKϟEDDX3|%''kŠ_~/fٲeJHH(kW_}[oիZ>EXpbZԕ4NAmڵ=%)e1zr.% YVZo]Z%=mrժU Pƍ%I͚5֬YVT>p%#ɣ޽{gϞR.M6Gjذ>" +Nll߯Em|={[.](11wyG'Np13gf#$EFFI&3gٳg˲,Ɩ\xY3gѣGS?cڵK4rHYnݺѣ3f>5*XiF?P͚5K?ޭvҤIj۶ڷoC*==] ֭vjץiӦ{4w\}ԩիv}+Cꫯݯ fԯ_Xe,2.˸\.wion?.+ <19b bL@@رYn]+V07p35j0#F0YYYn5s*j씔m|}}MݺuSO=eΜ9sޯKbbbbK}2 2JJJҀ*zE'11Q/kk$Cn IDAT@5p 8a DX"k@5p 8a DX"k@5p 8a U/!!A+99g8C~o?^lԗm=AAk@5p 8a DX"k@5p 8a DX"k@5p 8a DX"k@^=\a??+pF3Oe˖)!!L} kF]S)99dp$8a DX"k@5p 8a DX"k@5p 8a DX"k@5p 8a U/!!A+99g8C~o?^lԗm=AAk@5p 8a DX"k@5p 8a DX"k@5p 8a DX"k@^=\a??+pF3Oe˖)!!L} kF]S)99dp$8a DX"k@5p 8a DX"k@5p 8a DX+5k*22RW-['77WM6 /܎;r|rGjذaVթS'mذ\RڵS@@5j(8q+&&F~~~Wy̛7O.٥tR\.ڵc*zN6e}w_eddh̙H>/v-I,1գGmܸQcƌQhhf͚:hݺujԨ]Ν;+22RӧOݻ5m4mٲEyc}ݻ:uꤙ3gjƍ8q~7͚5<^8b\溵mٲx߿\qfĉƲ, /=}vcYYl{˲… s]w[LڵMffo˲̗_~Y4mDGG5?1cƨv_???>}ڣ=;;~/u<_ (fJNN.O%,[L ea ;[nEǏ׷~5kM6Mӧv!IJOO$>|X;vPڵ]ËD.88X]tQbbeN88̙3JKKӾ}H5iDsQ~~>{lYa8 <>STbb Ptt NT^J#66Vmڴ?+44Tf͒1FǏw4iڶmkСJOOWBBu릮]ujڴ͝;n:uzꥮ]o߾+hСzJCr˲<6Ul۷˲ /Pq9b bL@@رYn]+V07p35j0#F0YYYE?x`)))&::u뚧zʜ9sL^Ʈ+=~eǯcjJLLTK%RSSաC;v\4tREEEXGX~\4"##K ja /"k@.]*˥EUTPIրKȤIrԼysvUֵk >+W]v PxxF'NxcϫAӵ^2si;VjՒڴiŋYi&| RhhqVqkN5{lEEE_aaaܹ6nx<[s *g5\̃5L'NO?oYUVoQg/tMr\1bDu۶m\.֭[wA>כokF~~~jܸfΜYdѣG5l0UVMԩ6lPq٣>}($$Dݻo^d^zzgW㻔yU\z ˲ܞKLL_ff̘nݺ]ysΊӵ{nM6M[lVkʔ)6lZlu],K}-uAi…zuUWiܹ޽,Yvtx ɓiӦ~ի}A_ʢ5'I{}]s=9r\y*֜|M >֜s8p@&LPzKVEᅲ"/rssK-^{MÇWll}Q-_\#Gɓ'5f.??_=zƍ5fj֬YС֭[F8NVV:vL=վ}{jժv_|޽{SN9s6nܨ'~ӬY.kpQ3. }5]t1:t0͚5+3.ٳ[n1k6voa,2_~ݖn͈#x+4yyy%j*cYylӨQ#Ӷm[Ç{nmƲ,3gΜr'J^s,ˤ`U%{)`9ӧ1Ƭ]Xe~2-}qɒ%Ʋ,p2ԩS~fĉƲ,?O˲̺uiM``5G5N2F2ժU3{5O;vx{{cǺ7''!Cm,lڴɭvƲ,bŊ3n,cɓeYnT^cW_}ҥ[K/d6mjMHHiѢywKKeTښkݺiӦ1Ƙs;vL/ɓ'oO(^ĹT6Ǐk͚5jѢq+((H'k5W;yT+BzLbͱܕK}ƌݵkL)S׷1_|E;vLƍ1ھ}riʔ)jݺ&MiӦsΪ[NFGѷ~k+?wmr 6(&&c-[ɓڼysƍ]۶m.7pթSmNꫯ*..Ng֣>*;K ׬W_}Uv7|S>IIIUlllkٲe$`޽>SIgnݺUo|IIRFF?pUfMX~5jxݻwoǐQ8/iN:|rss>L͚5{Wؕ]ikn۶m2(99Yޚ6mT3f_~R5!9\IsVZ;vbbb/Bf?-]^[XsJ{_<}RSSuW琐5J?6mj=#Q>}Jo߾}8q^x0^6mhْC~3fL{Lԯ_?ժUKo׿J:6.2/WhhP<.ފǒÇ+''zUWw>3 6LSN 5"v!=SzꩧZ>Ǐg}ݻJ*E 8VZI:jJwV~~$ԩS^.___ 8e_ڜ ׄhZvmcWfeYsYYYξyGZ8qGk8e9sSll飹sjҤI xԳXs8x`;IRv$Iݶd-ZH/bc;V2dHXr뮓eY`]}n߾x)]~Em fgg{}S=$nݺn M+hױc$I~~~ 乲o?SvvvuU??b)ܿ9;vժU+5nX>V\Y<*׳AZ[oUW-c>>ŮP\\~W˪UNH?)v.pڳg5rH5lV^͛7A0a[ -YDwyrϝf͚Kk֬qkQjjhCۇ~HիWO)))nKRrr1%{K.JLL[wyG'NP\\vmꫪSڶmk:tHiiin*//Os̱N>sM6;twO?Uzz_k˖-ns:tx{{k$5Ϛׯvڥŋ<>H:u`sb]ʺN>h$Ao`sb]Z.b||:-XԣGEΙ3ǣ4^xAn, ;uꤪU_NR` P=X߿_-<>@={t vFXYF֭~-Ym FFFI&3g)gϖeY>@j^aBnӾ}bo}uי:u۷Ν;:tn[~5111f'0~~~oo̘1Ʋ,s_Xeϟ_8˖-skӧ6cƌ1i۶ͷ~V{nf5jd^~esϙs׺7&&&L4ɼG1n+5CknVZJ*g1 q& lܸѮcͱ5}vsWx̘1̘1tXew֗5w鯹_~L0 >XeN3a3a{}qe[i7.p>Ϙ*YeY&..μfƲ,3yd}z)襃\b:tPMҌeYG-%K*&3&b s 7a DIDAT???SF 3bĈ"oo&Olׯo|||L͋1k8m{1n|}}M֭͗_~YO[n& TZ}~s3gi߾ 3檫2cǎ!-~Wsw`otb֮]VÚc͕ǹkѣ6W]u 0y_9s[_ܥׯo,2ee\.;w~g 4y째ǯiҤ1W]u1cF8r2d 3cǎs*ry8lL^̶mۊ+%%DGG___Sn]SO}1cƌ1QQQJ*&00DGGW_}],cJ O5k@5p 8a DX"k@5.j֬&O\S :}=p,i޼yr\ڵkkڵkWf͚%˥6mڜWzz|Ay_nuq PDD}ߕ+W]v PxxF'NxcϫAӵ^"gG!!! V޽}vP :TO>yPYxUXM0A 4Pvv;͛7O˗/?,$kڶm"""<ԩSƎG*..NW]umۦ3gO?UjjjԨaצsΊӵ{nM6M[l\SLѰaԲeK讻eY۷];*33SO<񄼼4}to^Z]M/,Y;J2w\cYYn[?cYIJJ믿˲K/d>><׿jժJKKێ?ŋk @vG}3gpÕn[`Zj뮻nչsg}馛'z\P@v!IYsIIIի4`mٲEk׮-~W\f͚.+׼0~Й3gԢE ZoooEEEiÆ vۆ &MնlR)_qF}n۶z=zT?S .e5ѣ:xӵpB?^5kwVn:/0`&U^̟A/\k222$I5k޽{j _VoAÇS>ϭ-аaCIҦM2 PN]tQUn]+Է~% IIIVnfId:ÇR9._\ǏW߾}աCԩS$>Wi,\S}ڣ_vv۷Y);;Ⱥ*mk eYe;(D~ʩUV$[ڵ"?*I&_ j#"" [NW\q6mZ@ei\@=o*,,LYYYիW[VjՔ>}nM&LҥKK=eZf^On_n~5kGW_}={xLPiUM̝;׸\.n:MFLDDٳ7N*v_6>>>%yך!C[Ʋ,jկ_߸\.cY֠AXp ԨQÌ1deey\'O67>>>yw-r&.. ӫW/m6M6˲7|S@eeSD׮],Iz饗Ci۶m>l塇Ҋ+|splf @Un]֬Y@իWgV>қo'VTf eY$I .ҥKjСr.{ UfffEO 4lPYYY/zhpi ]|M@%@X"k@5p 8a [ѧIENDB`APLpy-1.0/aplpy/tests/baseline_images/colorbar_scalebar_beam.png0000644000077000000240000006623012471065325025020 0ustar tomstaff00000000000000PNG  IHDRQ ;sBIT|d pHYsaa?i IDATxpTˏB$ V("!*4B,Xz/TmQjkL"D"̵Ɩp%! R\@$!vlؓ=!l099sf9V</!H(0$ H(0$ HI/VddD]v݉'d} U@@׿c?^}UDD͛[q$ ifΜl+''G4io(++K[%Iùwք بl=ZnRSSMXZ[[[ȕKTbb֬YKTl:MΟ? 0@6mnݪ;SI&iϞ=:pBBB$IcwĉJS*((H1ժٳg\ǎs6$$D 5Ν;2͘1Ù@I#<݄HJQQQ.ɍ$KԆ筽{EcǎuVllD0Emm"""ڍ?~ܔk9Qxx)׸(hhhjm7ޫW/q3!u̸u^ШQԯ_kϞ=7onfI҅ tӯu-O>.c{VSSSj;sLN^xK=srss%]:n3~\""":lkkmm|mq˹~4F_wPۯ58E#Abx%ߌYݸ%ߎ]+s_l?]/UiԨQM4؄kͱ1ch֭WhhsBm6sN8/^ݻwsU'Qmkר+z횒W]A"Nk k1Zo,nܒo.~ҕ]/UuSF5khݺuZ`KmwJLLСC%I'NٳguM7)(XJҿs=zה H_}yUD0G9KNÇWAA9|%KP 7ꩧ$I$ߗ$g?s{o}Kwu{1=zT/o[{ w9H*VdsLZli1=*33SEEE:sF͛7k9E˗bUEs?&QcƌQYY/^_~zG_oP>Dݾ ˷%&-CqW5\{b̾W-vOVvN~~Ke:ƍ|ХIqW5\{b̾W-v5ly/H$?(sp_G% r/렝 ~K@z*KJ@PJ@% Qh}F?HeKqojmVsٝ<$Q`8;yDT?DT?$s@PCHq`w>PHq}\O$QYoqK N?[jkk%I^Tssxy7tF;v$$Qx*KIZOcUvi\cnj[^ IRllkj۶mjmm WQQQܥnoSxExrk^aw>HIIn׺ucMMMWbb*I:q8𤤤ɓڸqsԩSڰaL`IrW\\r]v3fL_*QLT-]Tuuu>| t;-YD:|nID͚5K5554hrssڪUV9>\? t9M8Q>}'?I'I.\cxaa233UTT3ghڼyƏcX\Z$) @[l… %$$P#Fp[PP(kz뭷;CWn7;D0jUVVwL0`򔗗5rJ\pDT?(JBPA$vÄs8*Q`(ʜoTD$Q`|?|&!|Tu>+q~?Z߃(GbdKq~޻ZfՃ(;݄s86HK-}f(0v>;->J@% A2?(0$ e)-[ZLF_Dun0{LC;@D;/]PHq?eT{LC% h|D H|'FD sv֣ ##HP]Ok:SA^k(bdKt4+ LC% @SEE% hKJ@LITJՔtNLZli1W("ijjUTTg[oSO={kϞ=Ekj̘1^xQk֬Qaa;v~jСqkN>Dݾ Az: 1sLz'4bkҤI_qƹ]p84ydٳG-ҠA$}ᇺ馛s5yd+##CzN>J;w$ v+++URR5khtl6-ZH۷owT*--ԩS%IӦMSTTVX;fgg5vXn6`R)##9fZ5{lرc w&P4x`M6M6mRssK^xASNرcҢ .tMu$ )xIݻ;]n<>>^.\%I555ըQ}*$$DG֭[ͻNDmw>o:i竭UDDDǏ{O>[򔟟Fw}ڻwMoAVx^illh޽۹Drrn&eeeȻ (\D[MMMǽ]q\v׾kǎ/D~Rk\nF_ST|uˋGDDtزW[[+IzmۿC i77,,]$ ߒŒI>mot<̘1ںu$ƺVllmۦVY,}UTT$iԨQ pǏ+,MvK l,"ݮu9ǚDg݉'t=y6n;u6lؠ)S(88XI&i㏝s߯;vhĉ]{>P`jҥUPP#G(??9oɒ%*,,Çu 7HD%&&j֬YѠAVZ:<{=%''kܹjmmUNNe˖u}D0Maa233UTT3ghڼyƏcX\Z$) @[l… %$$P#F9R_xb=S Є wMHN[ VUYYYr;'??ߥ2fS^^e3f?]v^w7Q`I@;v(0$ J@% W=QJ@60 I࣪i_qsD>ʖ#[Z4?Dv>Ӱ@LC% JTJՔw~IvمHn_Qm=eN+|$Q`OJ@% TLC% h2 (0$ g*Q`I@;h3 (0$ 22  JAP2 (0$ J@}W\\ӊG$ Ub֦o`D<  7AJՔw~Iv)[I 4j|2 Hh%e@Dmw>o.Ԥŋ+22R}Qbb< ٳPXXBBBˮ7xãx$ ifΜl+''G4io:áɓ'XsUVVꔔC]|r544bb};"`Jgs=G}T]-tmiiUPPL͙3G[nU``VXj7ŋ!(FiiZ={uرN׆kԩαkڴiڴiۭ7oN;Ó'`(RTTBBB\%Iwtm\\\x]pAt߰a˕uEPI*""xMYР'|R 7m؆]\644jի;}ge۵l2O6IS[MMMǽ]{aYFӧaF'r?UR=RDkߕO_w??""ÖZIRddk/_CꮻÇ%I'N$׿ޭ۝D~ )ckvn1ch֭WhhsBZڶmZ[[] WQQQ/BҰaڝcΜ9._~bSnkݺuα&+11QCtrt=y6n;u6lؠ)S(88XSO7t[z$iz7͏J.'Cq#!!AZt4|pȑ#w[d uaz)))JLLԬYTSSA)77WZjsq]xI*33SEEE:sF͛7k9oe-\P999jhhPBB 5bĈ^;Hj*++KYYYnT 0@yyy3tͤ$vñvIeη35X hAiţ ##H|D H|'FGD')-[ZLF}W\\>Dݾ Az: bKq).]_M3O$Q{LCG#@LC% hAOA% (~5@j5agV0T*QJvyO(0$ Q#H?`%byZ%zD~({5{CR0*Q`I@;z_CqZD;c$Q`|+@vpPD~@P2(0$ v)-[ZLFe7ÄX|O$Q/HШ鶞h#|;'>(GUӾ= |--eõ뫵i[= os`I@;&݄h|(0JZ*Q-ax`I455iŊT>}2֞={V SHHUUU2A/^EFF_~o~9Wfu(8$ "h̙VzzrrrI&iph*..ܹs:%%%СCy~Ν+Ţ hڵ5g?4Y]`Jh͚5?$)==]6M-4*--UyyJKK5uTIҴi+VhUWWkȑε=fϞ|effjxTTAAApYV͞=[:vXkÝ $ >^{Nŵׅ tNĉ.%^ݍ$ )jkknmݲŋկ~aÆ9oAVx^illet$ qwzv㍍f}/멧}if!'N*ku_Z?ݗ*RU|ev#"":l$EFFW_Ւ%KH˖-s{nD~`As_ }i%Yw5hm;?fmݺU uWTTHbcc^+66V۶mSkk,ھ}***eM裏{^|EC-6]j1᯳v[9Ԥ|%&&jС.w=y6n;u6lؠ)S(889VRRQW(HHHPjj.]: >\:r,YB>|X7pKITbbf͚ 4HjmmժUkP@@賓%ѣGkԨQz$Qp(Pv;.9Eaa233UTT3ghڼyƏcX\Z$) @[l… %$$P#Fpu9Y,?v9bъ+Hժ,eeeRj3`)//ڤ$9Sb*~P2B9D~SZax`OT*VdsbҢeKBW>Dݾ Az: gMj#HhvMA% hCA;I i_qMs_hIli1t:gjmVxOyx`Iv:u,(\Xv>60D}g*((ЦMT]]-r< @6M>~hذa =ͣ$j߾}Z|?hJJJRjj UgΜ?PիW롇իWGITll&O-[h„ t~ss{=7Qll.^hJƮ;)#Cդ`w}.WHoFpejhhP޽/(w_w'jZ0`V4qD[N/(SSNiĉ裏_իU[[J=ջᆱ8ȑ#zw5a{=j*,,4%He֓O>i%I&L… o{\mģ~ 4jʹ@WyD+;;[C QFFBBBͩ׺u딝' .};H~ȑ#z'dEEE)""BVUMMM:~<(ݮT^;qeZU\\'xBRmm{&O%$$tg r%m+!!$ " W>cj:~K;_llRRRt7wGEj1όs:+Qv]sQtt233UQQfYLEGGnθGx\Zz֭[e˖iΜ9h7z饗_BC ѪUL zIT~~~wuyddV^sW_%e<yWWW[oգ6Muuu] V'Q#GTqqGJJJ4rH+WƎ ;VZjjjRmmvܩuiϞ=ڸqcw }W\\\I/۵$ϓx@o|I͙3[nE5ydS1[Zli1{Wkӌz0"`e~ԨJDEDD(66V111?.mhcM.};X`0jll~;-\P=>畕)99ٔjq_*..N=֮]KjȑZtimO8[+.+@- r555iŊT>}2b<{222%''ù;vշo_EDDh޼yꫯ ?8Zz>3/GyD=… '0sLegg+==]999 ԤI}N9M֭[5'7ؼyqKBCC/+99Ygw߭-[hРA,+P+>^ҥ6ŵׅ tAI޽{ҢcǺ VllP$*>>^oG|kFYYYX,{mD*""xǽ^[[[2;Y|o߮[oU7444jի;mήaY6uTDw;-Իwo555olltvmۿxy=;OWMtM1i6kWu/'/m""":lkkzm[_?f8:}*$$D{&\Ux\u&$ĮZr3fnݪz:+**$Inm۶e WQQQ$ͦ ܹS)))y/^ݻǮ7Q67p@YV9R>,ťnkݺuα&+11QC$8qBPKKړ'OjƍαSNiÆ 2e%I=ܣ^{Mϟw-**W_}uE^q%jNڪzܹS˖-ӗ_~_=:}Q󸓐T-]Tuuu>| t;-YD:|nID͚5K5554hrssڪUV\駟ַ-u]ztQԷm{^xD\IOOWQQI/P***ҙ3g4zhm޼YǏwαX,^-[h…QCCTXX#F3fʴxb͟?_ӣ>z+Drr~wy}JՔw~I--ƌj*++KYYYnT 0@yyy˻uƍ>Xʣ$X?:h֬Y5kV$ht]j1\IIIz]!R`=Cz衇$I---Η^{ "T@* u]gV,ɕxO $Q`?bocw>*Q`IХ$g@ϳ+@- D͟?_n?z'\D֔)S2e{.W. s{kɓ's$ sq_ץJTxxv]:MWu)z衇N6mjw7+zá(ol,ѵZ܊+TVVzHl{꣏>RttVZej\UӾ=R5`o 6bhZ|.\}+`K--yjmVFZo3<'P[[>Hϟ7#u9z7u7kСSeeK۟iAբKI[o}{ ʕ+<CW_5+F^r3ߥo u]J~;|9s;o~SUUU^W.%Q!Ct.W.קO}Wn4hP`.|UPPvN8<{^W.%QO= 뷿$wOSl69X@jХv[nE۷o׼y|rI?/IJJJҋ/oѼ(xz]x^{JHH0#Nkwէ~zꦛnҷmM>]\sMw ^ky$j޽zMԯ_?ɓڵk^u=hӂX[1iѲŘv~IN<5tPGHW֔)Sgה o_Qm GvR=Q8ב#Gyf-Y%믿^K.͛ua,4~['Nwds=ڼy$j޽M$''kϞ=] T '@ui{4wȐ!:}tITSS=/v9(Z?׮].;]@7(ï+q~?Z߃(GbdKq~޻Zf\Lj磙'DQgϞUFFdUUUyرc6m>s9O?;S]wo~z K;&gv&O={hѢE4hrss?P7tSϟ?[O egg뮻ݻuJvءg}zg79(--Uxx3kڴiڴi/>!!@I7߬ &}pIk?e_}v5_˘n9RG񄇇KvQ]mm$)229ZYV\OׯZJ/{9M>ݣxO$QHFw&$mB&5گ#=!CG1WllmۦVY,xEE(k4j(ܹݱ >\}uj*=ZpX*0AJJN<7:N: 6hʔ) v9rDh~Νc(55enII͛3fhڵ]JD͚5wQw3$B. 5.ID?MAJCl* \ZbYEݦ.Q(Zq*ׂ!5X` $,c.̑p^c;~g->+22R2 C ,p͛7p8f̘L%''kΜ9Ӓ%Kٳg;رC銌Ԙ1crJofíI pzf$ݮkܹZl_~Y{vk\Z$)44T7nԬYpB9%%%iҥt۳gjjjt  ]|9Iжm[eff*33y6lhtsZzukg?b|' LX@jvZ0 (0v>S fFR $Q`|.3H p8Z<&D PWgj=p:_u~`I@;`uZ}(0J`V9XQG%$ QVU^:H8]Z7/Fd $Qexc $Q`O,ޡ4;'.K?(SWJoDK$jD%0v>0'*Q.h%wG% L h.ydZ0*QԵL%%D $Q`|٤Zg8*Q`I@;`uj=QH+cD $Q`|3|QH+T},J@% <'qT(0v> C!<D $Q`|3|<'7*Q9w k]v{;$R^^ӧ} ՘1cG*55U ĉ_4DΠ IDAT3OTNT=9%''PSdd2224zhW^ͮTRR***4|iҥ5j Yf_555lb$ ]vkfTV4}Η۷+;;[&M$*66V?]*77W $qŋO6X>мypB1D> >-Niq)ڥuS޹JyFvv $kNZrjjjDg%I}رczITMMy͜9S111+fNkS~~`|Ȑ!:{ZáB <%%%:s>ӧOa+f( .v2EGG7;vXkO:ϻ… O(44{L;0 CΝskn``$ZM^jrk/=n( 6mҘ1cf޽UPPPWuu$)((=꯹O>ʕ+_sH+xi:62«)W]x]&O+Vp+(Ik+++$uԩ="""y[nQu!I҉'$]hk׮vV$QR)j,*)FwQn1`mٲEa<)''G!!!mrnWBBrss\QLLBBB$IGї_~=z4;amVNr+f( ZsRRRk? 5kh.Ǜ>|XgϞU߾}]WR^^}iÆ ;ws_߯裏si.{^ IIIIѰat}XȐaZ`tm޼Y96c eff*99Ys̑,Y(͞=9oܸq ]_y5jTǬ7$ v_^sղeTUUDݻ\'Iڸqf͚ p())IK.Udd%AXAam۶Uff233aÆF;wիWԩS5uTx.@&XA Zv}(0J`TJ@&Xy (0$ Lv;TObDc|":;Q {; JT+CX(0J`u̡,A% ̠]vkb /Fc$QOsy~jQ.NyN&D V@;PH+cD $Q`|J>G% LXA&D VP+CX(0$ L:yd=NfD%*g>.cxND5tvv@;(v>&D $QJ-Q^^ӧ} ՘1cG*55U ĉ_4:BS=뮻N'OVUU;Qp(99Y7o"##ѣG+//Ozjv}eeTQQOK.ըQTPPӧOkԨQ:v옦O^z믿֭[u93IەI&IRSS\YYYͮAIkz's}Q9rD;wTnݜ33|8td}9<Vvv $kNZnjjj.>11љ@IR>}4vX^9V^^˗k֭Ο?s}IxM~~`|Ȑ!:{ZáB <%%%:s$9[bbbkĈLLk`~رcM=uΟ?HwQ+zTRR1c諯r;fXAjתڽɻgIac.rtՅjN79_~ZbE3(Ik+++$uԩ="""رc۷7|VI|_K림K841bwJ5>cG_"jW Ж-[dl6s<''G!!!mrnWBBrss\QLLBBB$yDiiiǎSݎv>^Ǐkڵα'Nh͚5?~޽{U^^sl߾}ڰa&O 7ܠuɓ>@7n1SVR\rJIIѰat}XȐaZ`tm޼Y=jƌTrr̙#???-YDQQQ={Kjܸq1bOӧOkɒ%ӧz!ckv֯_[˖-ӼyԡC}ݻ\'IڸqF 귿nFmڴI.sGQxxϟ^xA&Mr.*Qm۶Tfff6lxΝzj5vX;t#!CX|`I@;`uzh qT(0v> jv>?lQH+T},J@% <'q>D,ޡ8ǥW|Z#`U>D {F23Bx%q@&XAG% L h!<(=|(0J`u̡f8*Q`I@;`y (0$ LOyrF% L]vkb /Fc$QOS|Z}Q.N$VRádj޼yTFFF<իJJJREEϟ/???-]TFRAA"""sN}駚>}G^M7ݤ"uխIxMvvo߮lM4IX=jv}FF<\ 4Htw(>>^/֓O>)I:p>͝;WO=s-ܢ1chڵ9s[1kL$]vJMMպuTS|,;;[J飱cjα@IR\GEEIݎ$ Z<'??_l0>d={VorPaa9sFԥKM2E/7jǎzճgO'?q;f(^SVVcǎkrSty&L &k׮6lΜ9m۶M6nwxa:w[s몫&3gjŊzG5l08p@-RJJ7OcHKmڴIcƌqk{*66VAAA&^Ւ&O?T=/^Yf9 mذA'Ov՟j*oΞ=o혩D𚔔 6Lw  MOO͛p8c3fPff5giɒ%ٳƎÇK :T?N:__܎$ +a۵~z͝;W˖-SUU/w.sm6K˟$jƍ5k.\(á$-]T.s{==cz7rJkҤIc&Um۶Uff233aÆF;wիW_>[֟'O^q;Q`(Zf;/"|ԮU{U}Ei(Gŧ)>-(kMyNjYI` ω>`OTrP{-j$jD%0|B;@&D;|B% L h,Qve%fLege2HJ4K#G'oQvR;r^uW;2|}BP"i˨}]ro'Kvha^7TUaPvpeC6rud6E](V?eVony|Fo7X&*~T Čߪ m?tGW34^4_oT^*ye}#nt33e=WѸ%ch `ii&Y`C$ĬJח\ $Qr/1l~TU|5""N$Q6]):AUA9JlyO䨩Ӂw^`}q(*QRg{_82I.[s5Cul@xg+ оH*U4,g_$Qm􅛳뢯sT{Vc{l %<2ITr#^ј** ]n濻V+Ϋǘ>|'^Bz?7I'_?\{_Fg*:ug$idz.sp={KGU UW{ăv_fRK”8m|ljJZĬ$IDU.wRK:Q,A;juEv"Quս/iPx^0zE 쨚35>= E]]vkfTV\hrITHa0yɘ._#6(o/p8EĄ+~J ȥS=E~F_G|gӦU.׽?]{QE h>[&ħ]p]Z7&rX.ęCTn"I]=)[ugaa^.cVv7#]S/f4:~{ JzrfrX;Q҅jdHIN_t :VxZ0K&Q9RC>Ս~/nI/?ThT#0$ LOՄ]v{;xk_g??h2D5aboym r->[&(OܗO+//վ}{j̘1wk}4k, >\:|p~m 8PAAA֭~ߩg"5CZjM_k:x%o߾]=Μ9f59ĉ^'N… %KhKdggk֤I$I?fu]}܊٧+QE+}5{W Z7jk~޾ys{-%z-)VTT3v)55U֭SMM_xxBBB.ybٳGӧOw&P4c lc$Whw%սW?wJZZ}\ \u6ono߿P%b8p`!Cٳڿ#Ivu]l|:.O*Q򜲲2EGG7;vs2u# йsܚ(IV@@@׫<[>MݫZtJ*uEWw+?{oqk3\\r5u5?ڌ^KE2OߛꄇjzM y~R(;;[k׮ՏcI҉'f?^ιٳgM.%..N}__|Q6M)))nբ\F]viܹTFFJKK޽{;=Z7op}Zl$}͞=[aaa sw}W&LPRRnڵK/xv;f(^U^^sꭷRUU34%%%iͪs:tH={tlOqw?euiڳg:t蠩S귿Zjv$Q`H(0$ L H(h"o'OTHH{=oD@ b v_;wVzz>二 v 6=WZRhΝ;c}~*$$D1116mFݶmFEGGGљ3g3 CգGnkGUjj'/piӦ7{'ԣGUWWkZb6oެb7`رC%%%q^O?ԺufTyy&O޽{D?@;vt-((رcKȑ#zgt_e_zꩧ4}t 2Do~ӟfvΫTRR***4|iҥ5j jٲeڰaYpIEY|aٌ<_Wf3 f,[6,Xvil6?d[li~͆f3{1;ܹQQQ_jl6>p?~ȑF.]:SO=el6O?uݻ3~_7-!!HOOԏSh1bIґ#G\RPPN'*++}z-hȑnp[nEڻwsoՇ~)S(449P^9n:jƌ.>C*--۝cJLLԠAc}رc]7n8;\A>СCײ4anZSLс駟mVZ}*++UQQv9NJT[[׀Whh2wȐ!.J(,,lgܒ߷8p˵{h IP:qJKKohҤI\i߾}2e ՗:]ڻwz|gUSSݥ2IRtttQQQ:v܋KU~mSNݹz)Iڳg@sH[աCuU'OV.]e˖?dee}Itw^}N:͛`}=zsJ`M``zܦ]ץxNu ?B-TFF>Cegg;TAAv2Nt!R7|*?XNS||2~yhαoQgϞm𽥜IrεJHHhgܘ\CxRHG5JPuuvZ?^&Mj/~ {M7i׮]d gΜѝwީ2_[oʕ+UYYWtM<9v]w_10?Y]wOIIQnncӆ \m۪lv>!sɓK/]vԄ ;tPo^YYYJMMmrϻKO<6nxֿ{G{n[n'5j(M6MZdnvvmy;w̙3OF[o[W_u>ӌ3d͙3G~~~Zd4{Lcn7\s8F^cFppQUU^w`:u{p <2__f3 cݻw7vaz`߭[7|dtxᇍF?עEݻFBBꫯ6kii1yd#,,hݺ1a={6㏛e3q$rJ?Ç&IZlfΜz̜9S[nuDt=k׮.KUhhuȾ'O꥗^…  wl***$ڸq^}UM6Mv{[dd***E;EϞ=UYYGzg<#|G$ L H(0$ L H(0q [ IENDB`APLpy-1.0/aplpy/tests/baseline_images/contours.png0000644000077000000240000005462112471065325022252 0ustar tomstaff00000000000000PNG  IHDRKmTsBIT|d pHYsaa?i IDATx{u}/Oug/ ,M)-09PeCI|2FAK s-Rb\ P4ms8ɉZF%rхY1;sywfzUuN(%5eYZSwoo Yku79'.Wc /;xw\2ur\$I"IV,<=RTul 뚅S 9^3f\Bj̿υ֬n;h̵-f:B;hf\BK՗cxxx2r5?oqaSs9qw4wx~wx~wx~Z)%@ aIP^;h<;h<;hx^{nݻDC4UXΝ;~}czXjU/[nmnë(/9z{{ӟt W_z׻bpp0ַ6Lŵ\XڵkWDDcccՕ~a.bX3$$Ih`I ( U)J5raiq1:nݺݻwǪURf1445QG>9j֯_{l<㳎mݺ5*%Z\XزeK}ݓy׾ozӛX.6k&o[n;#l6lG?7|sq%`\{q+Y>&.,rxG#""v[d2 cpp0֭[ַK/*FFF}tMWhM6o\y'xbz뭋\ ЩZ%&,RK)%@ a BXH!,R4X,Nn'II4"`1 ( ۥR:.,eCG#V\}WmyS_#z: |>s^y "#~2F#bHVX9SW}Ot^X"ҏw881=HsGlyvD[ӃTY"2E뼰TͲeu?ƁӃDom[߽==J;rsZ__ڣ_:x0%p=5ؖͳwuQi3Z)akrUDwwԌ#^Wg*hxU+WFw|}cˇ ϵ$|jCu7@4@:K-"[8 W[Pd KЪڪr. -dJ3Z3{qMm<+ƊX$l< K^i zOKd` K@-dj3Z qa)EXN$$i`E8z^ @) Q(&KRMu\Xf1442f2$|>)%f24Ю4@#",Pt(a ŧ2-HX5h  0̗AXF* Ld%h )a h MFXڇ8DXXh C[踰X,Nn'II4" 4@Q(P(LnJ븰fchhe, aI>9OiLX| ӂ%Z,1a ΢25`4@4MOXViJX\+5@>ꄚT ӈdΡ%}9/9T7ta BXH!,r\$I"IV,BBarT*t]Džl6CCC.X"3'H| yRK)%@ a BXH!,R4UXڽ{wlܸ1:XfMtuu__x+_]]]喨R5UXzcӦM)L5\Z5UXZ~}l۶-6oԧrQ,$I$IRo@ ( U)J5raӟt\hDDd2nL&^xa 6B4]Xڼyy^cccP ЩZn ,RK)%@[:|={D,[=_=eh~|sh 5*zwyD&t Xp~"~wF1ڿ#bC3^6}]?gzRCe3r$4J煥"VUc#0%lUYS?cwFdV#t K ;bEz3Tikf8Olʟѷ5=}GhQr\$I"ID>1}CV,מg#c*FϲAZȚzghBBarT*t]Džl6CCC.cd2U?!+-:؁ݕ=f>55hU;ַB`fN KTstĊjz3xa|,M+e VZͰDXbtD,_3j]8ccOxJP1G%jKO@7>KWסe#%Q.GIpgg~ʟѻ|Aks\}?DdƟo[1qFfھeǯy%5@XlTһl882{i+D _<FX|^ W a",A[Ug4@ ,A'^ FXj2a%`iBM]o3 K@ișx`zaSi @gZstz*♇k\m6KdXp@r5T%Fl14@', ` K鸰X,Nn'II4"62MP(DP.J5]qa)P X`3'H| y]Dž%:D+4@tkұX$T #o{82,i ZRtij֠2%hv y5q\da :dcCdK@4@:,- !,Gd` K@gp$4@%,4Д 4@',0NdFX`aiLh> %ړ K\.v$$I+ii B RTul 5 : 7s$u\XShl|:N?ظqcmokD@it3_>m֭{7N;Yw}/{bƍo|#7/e@iXn]DnwZPpyED~+MĶm""⨣jp%@kzj-GYוW^7 _B 6ŵEX*O|"E]T\.b9ID$ Y" B Jj /0>k| 9VK?oow#N?曣DZ6]?ߍN:)otI@iؾ}{lݺ5""زeKDDlذ!2Lo۷G>i/Rj&Z5eXr裏FDD&o=nd2qXJd2qeͺ~ K7o󜱱%T-RƞQ.75ҢqqēGDODf0kuDf)ۓNάt^Nyaie6EQq}탏>nL XD̦LR 5輰{j~~Q=%8#1[!lUZ8㩈OԹ2*%##)©\WyOBXfZDkGtuQ(0wКovVs,1k%C ZDž\.bqr;IHpވ̚5Q\RXq+[ DD ( ۥR:.,ejtLx(Y +, ?u±m aI>9븰MkA W Z3i FXv212@X@xyi 47 : K:T4@", `KFdi @h hbMd`!h v%h& 4 K\.v$$I+24B! vT麎 Kl6]47 62s$u\X@2@4@ ,Bd8MX* CKZȰd%N2IX> %Gd@kE",Ȥ`hV%h& 7?  nta X<yKj؎ K\.v$$I+:ȅB! "ʣ1Ƿt]Džl6CCC.X"3'H| y]Pa BXH!,RK)%@ a BXH!,RK)%=.`r(ID$ XLB! vT麎 Kl6]DfN:RK)%@ a BXH!,h{ظqcuYf͚믿>ܟqYgŪUbڵq3<O?6mx N9唈d2+Jqg/~ƥ^oy[bdddPO jm۶Xn]{qiwW޽{?ap q[R YuEDD\xޭg}dPx+7| Kx駟׾viUO<wܬcw\xSTկ=Uy-៧;} vx*FbO0T;hM}L@.,rxG#""v[d2 cpp0N8K.8#U}^i.u1#1{ ZSs֞Cj~N \5'.CӅ͛7t+_Ef77c1q0%pݓZY,\ʁ"9.XӅ%gbeʺ(Xƾgcgl6 VALtMzs^qa h Jd:V$,ڵԠrs(ys4@>R6@}Ka  ?6m9Vj f4@s94B7@܃kc5EMm ybt8 -JXW]uU<1<dZOc_M,JXzg^6T 4QKĞƪ=",X,~ gVZL&r2L8p h?uv)EXN$$i`Eb* Q(&KRMyՌl 5 `̜ 1<<?&GFFozjֹw}wMoZ /~1|;vĻ#.QjKW^yeyW2>:y c\5.첸7~7/׽uq}H\qq'w{^]̺ռ?_z|_??O?=֯_[lw~6;Ū`+,M83[n-[ī^ꪫZ"(r(ID$ XLB! vTyn!.رcG|kƍxElڴ).d2vdjt9Acxxxj~f顇7w;~W5~W\\rI'?3<3>i{o}I^W}W|+'%/}kq뭷ƓO>{CТ jK~g?}y;?O`_5o1>9[jU\},-4_뿾XC,Wû[ͻ fPsXz;93LO'>Y-fS'|2ED__ڵk;+"9?X读Q)aϣ?{ g" 3]Akn RsXҗu˦M|0֯_w^#$5k֤^X,V?IH9ݑ+Qs)G9Yٮ??/L0?c`ʌЕvam* Q(S*jwSݷ8S'҄qu}ozӛRf144e)3vzbl{^B'c}*qEEw[ +lM `}9.]-|>k^5[FFF"bhLd??űl *Ҟߚ;ؾ* L fe` dXzk^rKbϔ fxm]ӎъ7vEL S Zg,@-Z2,[oߊ[Y&x{lKH/Vs\cQ})nU#L-866s\i3[izu{ dXz_sOlܸ1>Ož}⤓N+2>4<@=ǵ?&~5*qO>55uG_KO?=7 P&2,zbYڨܠx.E23aZ~Uz`lXw gZ85ql~\ 4'bp WT왶2 Khř#i,RXyk^ gMDX x!ĶC '^ wZo)XHPj\i??Wgzn) ,Kj! ׶Z@}%h ,a H s27a 4@$, `u\XrQ,'$$IX@svU(P(LnJ븰fchhe% hF3'H| y]Dž%4 a 2GJX 4@l," ZMNdCh 0?P3 N",KJdUK@X Б4@", } K 24'a h OX "4@%ȴ a 2͠R.b8$I$IX  3P(DP.J5]qa)PhfN K4@n] BCckMu Kjtm%%9γ ;@ a BXH!,RK)%@ a EO Xj\.v$$I+SPB0]*jR6F,$|>mx)%@ a BXH!,RK)%@ a BXH!,R4X,Nn'II4"`1 ( ۥR:.,ejt9Acxxx܆BXH!,RK)%?{,\nt%@3>Kog_|;buWCS~5]]F#`1t\Xʮx;F9vGGf0V& !k 6-p@io;'tR_/xͩ(#vi!kb{OFceW5g`AlX뮻o{z\~rx_d"VfuQ.G+WYf;c{:=tOaDF zFt[ 6ђa馛nz*⊈ؽ{w DWW{/ח8'#`yzxڙrc80c&cg#"΋'?K*&3˻";qϜͪ֎)3aN?J̘F9s\%@DD$I2m$/~1V K\.b$56ͩ+b]Wĺ#'yBĞR* g5@" ԦP(DPzNTi Kׯ'q1Lۿnݺx+^fchhhQ룵f"t56\##o9\nUPd2cxxxαZ2,;JR/[FDGݨP]Guuҳ[S6 MK . _7qrDoooy晍+괐 +5>յdX:S}o|K_83{|+},=F 11  K^{m/o=^g?ذaCK2@ /ot)@ Vײa h $,mm1 -2a  @Z%2,a h 7  2LXi bx FX =2븰X,Nn'II4"8LdW(P(LnJ븰fchhe`$|>缮07 %`h :a hZ $,mOd@ 4@#,, K-DdX:@&,P iw 2LXibx FXr{h<%h3 / a  or\$I"IV ?{gareOj܎ Kl6]P#nMbd7wyYDž%Lm\,">к%@ a BXH!,RK)%@ a BXH!,itK-EXN$$i`Eb* Q(&KRMu\Xf1442%2s$׹  BXH!,RK)%@ a BXH!,RK)%@Fr\$I"IV,BBarT*t]Džl6CCC.X"3'H| yRtҟ~-"Tꁈz Tqa=OxɈ{#v9XfSW}Ot\ẌJ,9{#v=<;s*vO53-OdR5z#8fucMVb[O9'bt[g&ؿrsX_OQ_:86=<vc;EweV q`Et[6&,5kll|jZ3;pwxGdU=5XhRڦC@c`fU aO𓇟ڱ'bhq6mo&,QQ&u܋gڹ7{,SBr g0C[+">O'?я]3FX7X#UigL aSn+|h/<~wWUm!lղ=CˇRW^yeX"2VoOڕzV茥wpF&3jJ y|XK}3Nģ>?}fchhhj2Pj1<<7m vȗrY! ?hs0"T]TP\UM1yg6ї~YaTTڹ'Hn222.R(q8Wm0CY+)^UUѩ-y:qR;pH;-Nuu`6On~-Z Ct4 CXKwZY9N?[flyjǃ3ZMDԭ[RuuqrPڳ^mm{F{!Q\l!UT\eǎIOn5t ;vl 8K;GMRRR˷ ݻxϫl~kh2@!,ԩ~ѣۨ rüS> a hCRBBR5@>_;x4@8% ĵv@/+2vvܩUVÇ+''G^{!q䖢2vϟ[jɺU\\%KhȐ!ڶm.+4@vnRVVF oL=_bauCd^۰4bĈ&Ai߾}T Xh ! Б#Gbu)B UXr***RNNե &,۷O3g7߬SZ]KرcյkWYF377Wyyyݞj2zGxހRiiƌ2}J32 rv+##jaJǏi& 8h '6,jʔ)ھ}֭[aÆY]*mX5~x?~\@N ݆>LCׯ9[]DdhmX.h ,`4@CX2;!,2 % %2K2q4@Za h VEd ۅ\].\.\Ex\-[7]?yI1UWתF5Z"JwʳQEUVVEENQII޳VeYUTԨΞmoA8q^nEVko.o.o.o.o=h&pvO?UZZJKK.e }Zh9˱?~n=ӊu!Kݻw?裏[n3O>omuaرcW_k˖-x^9R]vs=SNiڽ{vءH ӧOkڴi1bf̘+ؼyXT}xsŋUXX(I|G_yMիWkԩ={駟رc)ր}FMMߴ|SNFFFEUה)SQFiiiu]gu9PZZjø.Ŷx pO:p8ɓ'-,<9s8rak.pVj܌3h7mӦM0?p16ndݹsشiSPj W~9rĸˌp*5,x]܈#_~4hgQU護… e իWѣ7oY\tYtWMOLLTeEYa+**^i[oiܸqիomݦ7h:Y 22RÇo2}ҤIw% s׿.}_|E 6L'NT]]N>}Kh0 9rD_~եFmm233CqcmڴIqqq*,,Ԁx=#$ӦMS=ӟT} e˖iپ09t萎;oɼCUA>,I|Gю;ꫯj…Vb+eeeڹsnF=WllA{ЄVQQrrr.6-[ok-gjҤI>}ϟ_/ɓ'zjK {III裏tw(557}Μ9;wWqq$gϞMS'NPMM -Y^P||ƌcu)`233ujذa*(($e^uEFFjӢEt*..N~{AX}i̙5uT˱=z駕`u9S^^ ͘1[I&Z˗/ܹsկ_? oGoŊJHHІ 4o}('':|Ǝ]j͚53$s_LKK\.t˥˗k۶m6C髯RRRZWW_Wr\֭UKRԪ*eyyyzꩧ4}t=Vc eeeo~_+.vӧ/(IRttƍ'ۭ:9m{W,ATZZ1cƨL.b eff@Ruu<2Z{7SSc`񪨨Ч~jQep]x+!!JA{иql2˱ FsJ?QPP _}?K555Aya ~mذA$8t4{l7رC_}l k 77|6(**$u=5MMMjkkMKٳg]]y޽vdގ;4x` ۷;M7ݤ7x),,w}dȑ#%_ٷo_ݻ*WRRM{sΊm: jkk5em߾]֭Ӱaì.VRRRvZK М9sT^^Ekw=+[n7"##f]q61d׵^xԡC]Vg_w}VZ%{|͛,{jر۷6l`zٳuwM;r~a=8qzmMq61e-ZH6mҨQ$IǏ׺ut뭷}Q5y&322, iii*))ݻ.O?O{4rHmٲEk֬OlTgڇ spW ,c=J ѣGr=cӲet!mݺU)))m_Ep" i8&鴺}TUU?PK.;C={q8Kڸq~k;ܹsXwqkG?<7uᏰicƌѐ!C$IӦMSBB^|E[NM޽ϟgzW}!GQǎhѢ&_wk1 Оp][$I.3}t??'N_7gϞAqf Ю|ג.|rjƌ4tP͜9tJ49s)pf N<Ǐ*//OsU.]4nܸ7h =z7URR˗7+ݻ+֮]ڻhG8iFݻ<z/6}JJJRrrrM4Ifj2kApAX_~YWDDz^aayhzO.۫W/z뭭Y6 p tM[5rȀ$͚5KNS7nɓ5o<8p + ]VׯWvvpBEEE5aԩS={ LIRϞ=w}Wk֬iiJ =K2g 73gԪU裏jъ/k5Vbb&5o޼&OHHЌ3`oV",BV gr/'_̙3u 7-t:l2 >\O=^z%߼M6k4_Xr8SO=d~ 9s=%0CX%0AX%0AX%0AX%0AX%0AX%01SIENDB`APLpy-1.0/aplpy/tests/baseline_images/cube_slice.png0000644000077000000240000006273312471065325022476 0ustar tomstaff00000000000000PNG  IHDRDsBIT|d pHYsaa?i IDATxy/?ق(( &$FArՈ 9ʼn%"1.xFۃ".1W/nWMZmU4ҶݟjT]Ngm6oqzhc_|z֭[w?>-7_O裏&Iڷo[nK viӧO?oҡC'ڵ[Fe˖/s=;oc_孷JϘ1cꫯ>qe Ʈ9Μ99dɒL:5Gδi2|s=L0!rH|_??e}uFԩS3r\uUСC;v؜s9ܹsN9唼kdɒ̚5+ ,H׮]sW\`… sg„ 8qb/y睗#8"~{Oʕ+s礓No^7s=7ݻw#<]&Iz1cA$y3eʔr)ꪫꎳ3{exٙ>}z.~I#GfvYgU/^zYxqylI;tPn3fhJimmmjjj|nfmҹs ?NΝB[o?pc'L]v%#FXYfwرcO~>(Y|z(u$]օ${|9SUU'xbyƌرcN:餺u;wΉ''x"UUUuqu4I<۷^M-UN8!mYt8 O?t[fm\^{-gqF57{r-+׹gy&Io=3۷Ϝ9sֵk׮sԩS_Uڵkvezck$wuٷozw>lOhPӪ״34VunΝ3lذvaj/<߿g^:*/?ȃ>Xo{mmmN=G?wkӡClV+_I3o޼u^xa.zsW^ ۳gO7G ƭqUڎԫWYlY:u޽{gŊuW\U}'OGaÆf92dH|ͼm{7̙3׻ŋ+_ZuyO]mmoF.^xV՘i՘N:xG kӧO :43gLmmmn#lo}> sN:lvG.]tҵn[dIt޹ɧjY܍7޸%KuZ߱uok}LQYYYy6CilYti>ߕ<.,[,w\m_vmN:W^YbE-ZTޥKwͶn㬺Ev-?~z#}SS YPg}6{o>f?޸qrws' w}wzWl喹kk&l?|l?v[Wz裏2|uCMNr5ԭu]> [;䥗^=aÆeŊ6mZݺO>$7tSX6c=6w_?p^yz5Vulg}6^ȴiҵk򗿬o[oM$KR[[޽{7qU[6~#[F/O~wq9?1K曯X\rI ?cƌIUUUr!vme]e˖vO_wj_?yy䑺+{ws9' .L>}6ofnz5{;2dȐvie]o~9CϣUң>::uj>l66lX.Nu^{\pI8qbd'N:eʔ){կ~5W^yeƍ C=>;~z6tӌ=:&Mj0eb\ӷoTTTG?Qqku[n%'Ṋޚsg„ ܹs8L2SwhUSOͩqnwe{=ztFݤ}߭Nui׮]&L &wܚC]sΙ#F( DּUQQ s.J(P @aR #PF(0B)J(P @aR #PF(0B)J(P @aR #PF(0B)XtMՙ?~e9 ,HMMM,XPt)m^^~^^^N[uuuu YfeѢEE駟Nt֭j6..-....s6jPJ2dH=آhsVlȑѣGմmz]:z]Z]:z]:z]:z]:mwygfΜqB)-oF{7{gtv!?ϲbŊ>䓜}vmgy衇}l_+WOeeeƍɓ'g…:hyn馹KsiewNӧ/駟$9rdvmuY )@sst5>h\y啩mp5pmIΝ3fgϞu4Ijwq{lٲ$|z(u4I?t5~z /_I'TcǦ63f̨WSǎsI'խܹsN<<[_[[[8k.4PYbEN=Ԍ3&[3>3s=k]u;wn/_ou)_wPW?N{>s̩7o߾漢g]YV^r˛oݻw ͟??n0W^Iy_~?~ٳg??o޼zc6nV5kVݿoX&ywr .H[K,Y뭿mQd^U_xqjh)&9V[ͥK.O_dI^؍7xYU?KMM1eʔL>}cRVVcŨLeezǬ҇W^y%7pCzM[dI.]7x#nibu^zջvUnuV_UwUcnkjgĈM^TQQ Ko+Wfܸqi̞=;/rvq\|ş8_O}'&o߾Iv-;vSO=UoҥK3gΜI`yoכ?`˩iPh)_P箻w]箻J~;Ή'3lذ,X 3gά[hѢq~N:%I6l|mݖ?n쭷ޚ>(Ç[|K^_~e]2mڴ\n^veذajZbEMVO>M7ݔ6 ܾv=Cm~ԩI##F( DּUQQ s.J(P @aR #PF(0B)J(P @aR #PF(0B)J(P @aR #PF(0B)XtMՙ?~e9 ,HMMM,XPt)m^^~^^^N[uuuu YfeѢEE駟Nt֭j6..-....s6jPJxSSSS|QG裏.aoF=z\MۦץץߥץץץE]wݕᄏnwN &dĈE&u-=zH^.zر;vlrEEE78σ(P @aR #PF(0B)J(P @aR #PF(0B)J(P @aR #8 IDATPF(0B)J(P @aR #PE@4eʔL>n,eeeVTVV-WUU5jPJ?~|FQt@y!"] #PF(0B)J(P @aR #PF(0B)J(P @aR ӱ?üKYhQڵk*}Mnݚ>ڰJ巿m\|u][sԨPڿ~sSNlٲ<ùҿ,]YmiT(}g?N:C͡_|ж5黟o|M @0I2dHڵkڵFm߾H;63QMJ_lݻwjkkK>}fn!/gjk_|qs7g…y% .M7ݔ\uU?o1/B&Lܵפ+gqFF?:v̝̏;7?OOfԨQy'Y aʔ)>}zrYYY hI[jԼ&ҹs6ݻw~_-瞹曛r(Ə#F]P"k^Hyy5ݞ={;ʕ+l[bEٳnݻロ-ܲ) kҕSO͠A2f̘|k_K+SO媫JCnW5mBBO~o>'N̘1cm޽{~_'?Idҥ:ujvq_-mJ_z'O̟Iv!{W:uT7s8 ٳ|P+"j @Ԥ+UUUoRUU}ky饗&Ib\yvkӤPzg?3<=zdm֮]uQHڦ&ݾSOM~ֺ}w̛o kR(]xqkirA|y4)~s=ܓ4(JӟfI+VW^Iyyy?mBh{9r&I=֦C4iR>f-I4I;4g̙yWr|k_1vک9kjr(Mvm4YS ͡QWJ۷ovڥn]vjui׮]VXќ4*^p u]ys!dwN}= jT(~VoyڴiYpa{.96[MMNɓs)4Io|#rJ&OmkR(}өSunԩSz&UK.$۷^67|s<|_M׮]K.'|Ro믿GI':]vgyf5{{\xnp+Wʌ7.'O… 3xΙ3'x`,YSfљ6mZި?!GuTr\}9ꨣ_"zjFԩS3r\uUСC;d>u]{r衇'段=裹;sWֽfN: uQI^zi1cFzYHdq{'˖-K|y衇R^^^HO׮]s8/B^|ŜtIu4IƎ̘1^M;vI'Ts9OԻֶ M I箻JMMM͛y>̙3X"zjƌ~5z$խy{3<=ܳ+q^~$ܹs||߮7SN߿:q4߫Wl3gN}~WՔ$>Z?Ϛ4Un]]ҳg樥P]w]|g7ylfWڻw?~`~^$K~2zW׳gOZo=?o޼zc6nV5kVݿo֐$u|,X,XR<..-....FkT(4iRN9t3k9;3 . \pAwy^zi~\{tM;vɒ%ܹsmQd^UeC?zcSSS̚5wri555y駓3i ;wn5*VTTRVV;._:tֱ˖-#ƗUnңGzMiͽLeezǬkӨPھ}1"#F3<;?xx≼Iݻg]vɄ 2tе>ԧ5zrʌ7.ƍk}w̿+H<9裳{o߿?6xO>M6$}M;`;v옧z*Æ t̙3'?я6x$yꩧ=h޼yy'ɀ#-O>d}5[/$iop_AG Ȁ>VkSW9ӧO$ɋ/?<;Sᄉ~s] 3fd̙u.Z(wqG~SNI6,smeĉuOƽ[Geu\|y^}luׯ_veL6-/Rڴk׮^6lX.L6-ǏOm7tSJ+@sO޽{`ԩS$GydOh|!Ygwk_[{LW6lXN8!/Bwk&袋ꍽK2hР3fLrWC\7*k~{"e]#<2|p~˯3&;sݸ;Ç9眓 O>o7|QOt]|;3k.&Lh0~ԨQ ۷ߟ3<3W]uU/^;rK;`V\~8뮻RSS:OІ59NEEE?# ,HSNwog _SQQ+fm0 JYYY=>-U+mLC3{t%GuTtAرc^}Ֆ6ѡtx3iҤ5*ݺukɺh?9ҳgϔ}VXvڵdQcǎc=9s2gΜ?ȶn;%k̏qs_SO=#FGMip3fL~eɒ%^,mzo˷\qy뭷9C3} :4ݻwohe:tAo9 ,Heee<5mXյk.- /ܻi+a?wߝz+={1Sjw}7W^ye暼ӧO @Pof˛oYs̞=;cǎ￟;gyf9)ѡo>;o^\tE92gΜ|;ɤId!?pFI&խٳg;,{oڷoІ5:E.\0Ouw$,5:IX"mQu7| /^{-_{$/`{~h>S(8qb&N`رck׮]VXhJoƖ6fʔ)>}zrYYY hI[jԼFQF}?~|FQt@y!"瑹F(0B)J(P @aR #PF(0B)J(P @aR #PF(0B)J(P @aR #PF(0B)J(LǢ m2eJO^\VV+ZReee*++떫5O(E?>#F( DּUQQ s.J(P @aR #PF(0B)J(P @aR #PF(0B)J(P @aR #PF(0.:/6gɂ .i n81k֬,Z2ڜ}zrYYY hI[j<1~12YBTEEE78F(0B)J(P @aR #=:{#H hh&xQJԲ 'ƀhӤ2J;yIs.}T2SZIꘗIa,E&el7n|ռ]cccccccccccccc5kԡC4[QQQ R-}9rD~~~ںuO?qShh={TÎ;EGGkҤI:}ǟ:w`hB_TRRVwY=cjڴׯ=z(33ӭ.!!AG$=ӧǽ@ep8={BBBd\ 7ܠ{LF-[[ooii  ݮ'jΜ9:qt!OZ`Ǝ%KhС} 6hjԨiz4a|P ,ȑ#h"իWOھ}Kfs .|ͣ>={Xyyy.-^ح~ڵ5sJ;w*##Cw$iذajݺ~i;kx EDDh˖- $(55U7nԝwYgӇ~(?РA͞=[&MR6m$Ivʕ+5o::Z_\j[n~z?s᭼p}oiӦjk5ksIZjj֬ (!!m_ђǏ+66V999.EEEi۶mUVǏw|Oe6oK.WJu34c EDDT[hÆ ? տ[]+RXX@ Iҙ3g\~WV[~e.f3gxJz'ոq N[޽{K. 4H۷3<%KTMppΞ=6^XX|jׯ_eom_t)ϟ+WVYK>3vv{5]+sՇүJiiiZpˤܹs:z4h V}V]vUyha˔n۴iSg] k:NYm[zq=])S(99D!!!jݺ$}ק~Rw9eee)..Hr:v:uUPPS}@mCi;hڵΟwyGjѢ֮]1cƨDnڵK{mVq \Y9իW{q~LxxשS˗/ӧ5tPXqq89mjɒ%*--u+l2dKO%%%.={VK.U=ܮ@MoߍРA,X I8p$駟~R5bkN!!!ҥKiӦUy!CG=zۧ˲,K3gt5kz޽{+55UC/.Yp8Ԯ];5剸sտ >\{ŋ6m8wﮡCjڴi:qn-[L~-OW}(̅Tm޼d IDATY:s挚7oz'Y~z֢Etu]on&N:)33S=&O hرz+h͚59s&N&Mh1c۶oz)-_\ر}]{:Up(zzZ)--^zU111.7hР _(00Psќ9svM)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B) nWNN6\(77t+us=̵w1\{s=̵ԅPZyfn)((ݻ%Iaaaۘka{ka.uvvGuRԊ>}7FS/e#GTddn6{kb{k sok͚5JQ+x }IIIJJJ2QHEGGnc.{ka+mvvh;B)jŔ)Sl ^rᅨtTOC(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(oMʕ+d#nn;_;#VL2Eɦ%^JOOWJJJq.B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B) n?V\|$Mv]vpxbʔ)JNN6/BTzzRRRݎwJJJJJJJJJJJJJJJJJJJJJJJJnuS~~rrrLQ檠@[ka{ka.u~~GuRԊ͛7+//tuNAAv-I 3M\{s]̷0\{s=ua=#VG6Ꜳ)9r"## wS1\{=̵0\{O]~[k֬PZo?tNJJRRRꎰ0EFF*::t+us=̵w1\{s=̵\isme۝GJQ+Ldm /D+%%x.B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)Bif͚%???uСҚ"kN~~~?{Gn2O?iܸq}jϞ=ǎWHH5i$>}C;wVppZh/*))q;{{15mTW=VѣGK|A^2 8͞=[!!!lw}'IU֕)--Հd5qD͙3G'NPBB:R~P ,رcd :ԣϰa 3~' Q֭%I۷>Ss)++KqqqU~/:v:uUPPS}@mꯔvAd-ғO>SN饗^RVTTT{e\?^G֠ASq  YF$)//OW=;CSO9|r>}ZCuXҵ^(IRllڶm%KhίyWd4d͛%Khʔ)tRnSqՇ҈ 4m|::uRSvollK]E =zhڷo"""/˲,͜9ӥv֬YٳzT9⋺뮻ԿgPv4j('Ν;WT5|pݻW/Vjjڴi޽iӦĉl2}]/vҟ'-_\&MRII}]g\WJ+S{9+Ry*--Miiiի$ 5g͙3Zi\)C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(oMʕ+d#nn;_;#VL2Eɦ%^JOOWJJJq.B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)B)T";;t @`m.b].Z5*w^-ujYׄR1R1R1RIvt n|'_G͞|͑#^O֏/9~$E8G֓#fO6ȿݮ$m:u}檠@ҥKpISnnkdk/͑T3亮jk樦l_u_;g9)|ؽ{lRm]AAv-I c=zTeRO矵|˞Ks$L?5kk9~9a]{uuj/kӯ!V̟?_+WtNJJ+K.jӦMuN_|[nEu;vh񗵏K=:uJ[nC=ts]|i&uMT|_\۾6G5O]>gZ?k9QMu-U=G֮]|> =zP^KKK;5Ӯ].{yyyڴiS tSs|ksT\9)}mjx~$ֵ'|]&ںp5z}תͲ,:U/++K MpmٲEqqqJQ㲲_naURP 0)C(C(C(C(PgmܸQ QF4tP=zԭ.!!A~~~n?w}[eY3gZl`uQ];aÆaÆ ux_p^VI&nukLOM4QXX:v쨿*--u㜍+ZjРAڵ^xz饗={q.͛7?2ִiS>z4n8uMk׮~;l6 >Yw)G>}`[YYYjԨQ|piԯ_?j4o<}WZ~~Y0Wbbڷoӧ~Z~&MZ .t眍+Ŭm@Ԯ];uVQQs?իgM2ťwVݧ &~Vͭ /`l6_sO\Ubwm5k*((pf>Cwd绌 w㜍+ŬQ׽+C-ܢmVxeY*))ѩS*uT\\~evPեKX6mԯ_?Zr>ROիW]>8)/::ڹOs9+馛jjɺ>v옚4iIp FEEu _бcGmڴIs^uIRzxb7Υs6kلRuƏ^xAO#Xpp jn,]veU@E.w]W-T~G-[(%%EC њ5k?*))raҥ벯M(p۾}6mp84?~\< 6觟~Rzt7V믿^_DGGw-5j@xU@E.w]ݚU,X vfwi۶m۷C?~-9&]>r$)33eZ4iDM4$h˖-[+|cy|$suI_ov'^YvA~~?p _~ ܹs҈#ckxSEk;**J^ -**$]s6Lu9[|O)Z6ZfsɓVaaK]ii5|pڳgspX\s#vmVͭRxU 6mڴxJU-w5mڴckx+""cV.]pز,ٸx-s6@|rkւ %KXÆ l65n8͛7[QQQɓŋ[ͳzel6롇rԩS-f?JKK `l6nX7xi͝;Z`ռys믷jt][e}gVPPչsgW^On[ojY0mÆ u7Zs̱-Zd׿l65{lgl\I<]ז9P NڵkջwoQFVppթS'kɒ%nu fl BBBnݺUXkY5矷bbb@C֊+*u8СCp+,,8p_u]f۶mV^`+22Қ0au):5|ovmVHHhum}sƕƓu]j>g,˲LB :]1R1R1R1R1R1R1R1R1RPk5n8m\}) @_~V@(G 8P!!!:uT5 T~~soC=)22R{v-[kt 7hԨQ:|$)!!3gVyo߮7x?zĉvi1cF vͲ,t@ZjFe˖iȑn/jҤ]V(???;VڵSNN|M}z饗#8eW&MRnTTTݻwkɒ% UvvKPk.-ZHӧO7[Ծ}J?uYmذ&#=٣O>+11QRVj;@EM78p´bŊ Cu/(99Y!C($$D۷oW˖-'O]wݥ?Oҥ~_n}'I5jZn'jٲenW6-ZHwynv>ˉ'~zk5k;ve_~jذ-[Va]|DPP>}G+ԠA 8Pk)77Wsu eZll6yjݧOIґ#G.HzT\\;e7ߔmۦGyD7VÆ 5~x)??_<5j=c3;;[C nƍWÆ mj.5JHHкu<KB($''XVr{U``?aÆUK6mٳg<_-IO!رC7V+|„ :|{9 8Piii>}eYyKs=:.]TZ_귿_+Ν;k5=vYIDAT޽U/q.>o߾֊+?9zj;oݕ?%M6 t;v֭[u!:O<<iϞ=4it8pbbb*}?::Zz!͛7OxbIRjjbbboTx;{=}UlܸQEEEڰa5jTemVTZZk׮piR ӈ#sN=z9b EEE_~αUO<2^M4QfVgΜѲeԹs?6lXcƌqyݽ{wq???uE|M駟޺[ڵkUZZQm^^^\:B)>j+$IC۶mӈ#dٜuaaa*((r_e_^~ieffj]ք_uxx$7$InիƎ(%%%iZ6V~5P ܹڶm+.I,-4|:pΝ;W龾 ]s5馛\;t蠾}wފ__㏕_^==+>>+AAAںu2335rH}>|N+eaqpdݻWZbZn{GZzu8r>cp$oi۶>cY[wl6Wח_~YfiӦMڼyKÇ֭[FC(]1c> o?~4i?nPGfӌ3sy={T~~~nOۣPZѭ;v$+λwV <<}={jݺulF)##C PΝ5vX|ooK/=zxD+33Sn}ԲeKm۶}͜9S _W:q^~e5o\κ"##Z͛^W:ujdWǎv>#cccccccccccdKgIENDB`APLpy-1.0/aplpy/tests/baseline_images/grid.png0000644000077000000240000023273212471065325021324 0ustar tomstaff00000000000000PNG  IHDRTJsBIT|d pHYsaa?i IDATxy\߰ *"+*nijvԱ6m9XRj>/%3(\= #03sa@z.jnCuDDDDDDD 4Q0&""""""*&DDDDDDDe @h""""""2`MDDDDDDT^ ז{kݺ5ڷo:**,55CFFPH@]I4h{"##O<zN@ܛobi@ ɓUQ.3qb̘>q8h6Z4[u l/@׌7(߃ߋz)O$]/p*l8Wڶ+,dAI-$  u@&@jJ*MtS"2C͛Ӂ>S!1[۱U9"G__qPY l2m%7AhLtR+3ص xe&DF [q `]E@->J s\RngRw. 4UW82Ru$<=nQ<>zԖPX!½$o hZMP4MGGJaW1&[ܗ*"͚ٖK2o{7hiޭ[F2M|kעB|j7ߴ;{`M. 4Bzݠb[V ,n9w޻W5kx\P-nFW%=].6z)&2x)خiޫh""E5UGBAEI1w.p|.2R`n-f͌e&j:DdL۶C:aDDTeVD΢V-)̲Z=2Sg|9/Zv6ʦin]ՑQUIHd39h""h@uDg2sd$0lɑ}矁9ssm]wAq#0zH9#y 4U t: ^g%޽[_H+-ut$mJ =l_Ed\ﴻR[aMDD嗕%e9MFP,ԗ@&]BC%֓pgKiRMՑQUrHN_IHLL,mXhNG 4رr9Izji+IjtmF @weDFd$) !66֮w||<&N,GaMDDW[ Lz.|nĖPGG;/R\k"B@b"0p#bMDDg6PwKժd@*~[,Ν2v啀$FfU;3q$ݺUs:6V[bMDD DL&yM4hyȐ;TewԻCI۴EӤuڕD<̑[-1&"ch zH m3_\ u£"UWADc^{#q[LfS'Q[@a!p䈼NM-Ff6Ze{]˗پȨv߾#q[L|^\Mtk<laE8!2)Lc$۷?(3!!@Tu+0|sd,"ZYYw#I&DDT>f3LY};p!p'$Nl3D/e)Goa7Dըam^Imۀ_/beْI5skbMDDVD'/ؼxAW|Y| e;, YNd 89_~fP 4,mxyWl_`ߋ%֭T/^,6DZOEyKVTGB`MDD呝 9bDFӤX7_͚@>2E?eP''mtǎOD۽عSjS`MDDew@9M&U 8P ȶmehZ.]mj*{ on^#?1&"KK#@9'x+$6L*[6:9kWqtt 4kƾDU 5e\"lkt))$dޑR$޲9/ݹ$]WnD*)IyH&DDTv [X9MvUZq@n%nLw,|رh&DDTviiKLrrIjդtL,̴-޼X@֭n$nFfˉ.ر#k7"9l.m_B@2ٿe i0oN$֍ JSP$&Cu몎nY\\n"66VaDDT%rrSșh vHJ_.23yϗ-z2ݹs/C'rWUGB`Mlɘ0a0IIX*G[an <ܾ]f7n ߞ¥{wI[duor?VY aMDDecȑ 4sHK#)?wo\ۼY.| RݻkWGIQ3# 4MZTX"琒x{˞bW.FݻwtJ |'*Jfwڵc122xQ#pDNylf"r&tT:t ' lK.f͒]Hѣ:j[gk/81&DDT6zh"R/;WUGRj QX(RRd轧6=zHOlN+; W Q٘ "@-2OU+> ddiM~=[fv%&L`":&DDtsR#2Ru$DH4l:!CdFӀ_h%tϞRSuD%};Vu$tLN7#!"U޽Y8i}ղN˾  ݻzIe@QI✔q u&DDtsfH?Z={ĹIBr@A4I-];ѫpm Aj3p,&nl|}uUGBD&{$cbTG:<=XtT>}ڶ>_ߖLժ܁*vbx@͙Ͳ|3DꥤHgzl}sr uOx-3\VKUe`>OTGBenNMDjee۷/:28zX^^MOI6D<}{wՑP1&"XTGAD[J!wk_(&Ф.^uEL#KUGM*-MoMʋ2. 4X^, 4z4jĊR0|\_^y9Gz7ܹRn0ՑP90&"1&RKo_5xHSj/[7ॗÇ%^x )T־=зp3/\Abb"n[,8h""1"rgp30fd<pLTCY'-[J,"1cTGRa-'*1@эru-JIvr:U+ML/X?>}odkTR,ݮ][u4TNL,o"4MzŲ?s^].L] $'__?YM:br+eB[rLXHL`NUGB)311 /2keo2ݥ$}ߴ;ZxҤh@э授ټYf3+u7cRQ1>eQm8# bMDDח< DFȽiV*K76HOjkJbG@ '(6䒘@<)^\MNa>\u$TU#ede7k~ |\_VLcǤZoЅ1&"c +"ΟSd|A2rs[%^= dY:b*>tH0&"3MHHܗIR:rjIϞRt,5XX HLd_?iK=J.2"">YosHMucrW<ػWի=V` з/0pei璜,Gu$tlm"22ݻ_W 9m[> <(?Ki=Q{SKcMDDgp%J6ɬWL@T'Dz*Y2-tLUX\ƪ*h""*]Ap8gT4IU Iڝ5iLdK'zuI b2(V+/5R U&DDTS4{@QP g*d`8Ց;iXM9"+(H*y t ;7s\x1 he'OƄ TADba"U6mcnj W~q!IWeVu `E%͟/GV U2&DDTRA$laEZkܝh!駁{%^; Dz)dɑz 0Pu4Tɘ@QIOK +&DWPlp;9Iڵi<}__VxաH0&"X@H3gH!#5ʕLO T.=olf15cMDD%@Ϊ r?73Wԩ#}Ǎ1˗K2t|nI[r3! _dX\DDD 9MIc!#kxQi7{ Yy1f W㪣,e( 4;seP"G6ozR cL2?}&5 x`(>6$w߭:bLȞ*"BmDfN +=yzօid6ztC2C=y2@NHKɑD(F P""g6"r,MUGB2pA^~YaÀcҥ2K:r&DDdl[u$DEӤ3$D΢vm[?Dtx$ÆQX(x𸄛\Mh'OGr6э4j<$x1:ᇁdNI~>/)Y,, Fh))ukWՑ9? &xui5}=~=i@~~/ڵ"%DDd9Ru$DEӀhIDec/}^xY}@[Jϭn3DDds,DlDNYJ(C WB$ٕ%! $h""Xqv WL*O2yظQf?yy\h̳ge3 7Ģ=1&""Yկ:"i@z@&#!2//wok` (HxQ%Q&nb1qD9h""1e9Zj#!r))2#=DU+0^GH"l0wV!˾n8W wźn{lm"G2cǸ|њ4{X#i2#׿6%}96fr &""4LD"KL;wV {-NO~ #FHj@U$$pbMDDj"bwީ:"i@ǎ@#! +-/=[.. ʑiTGKp 7spdHCv6mo9-زer݅rgHrPW 7 43~=p裲{HY]HA8MDDl*#!r& UGBDe|,/(/%~# 4 YzVyݫWӧUrt '`")3HJFz XBja0&""Di@j@N#!rHNW^N ^ӿ0&""YNj6h$Ͼ#!Uv__i WҥJK ?$&DD\ o8MT۹|A@hhqcxMہ~c*"bDDdkam}+XXoqUòX?~6mѣs~~7U@-ȑ#Fbbꐈq4MZW:tZnjcK';}N3t&O &nVD`J=`HfRS>Ǐ@۶RlJp2&"" -aDpm"痐 Ž{WsOV 4KO._f1 UD.+ XP ;CUloo@r$z|O? Y#( IDAT!@49FFl7L-SII /,[L&)2S>s&p 4X6"4I ,Lu$Dt=|{ $Du4#3ѳfI^oxe`V٢E 43ucQ))\M֬NƏWIٵhL*ғ'.HL#Ti@;˷ڡCs\Mじ *Ju$+/Y3㏁;=Dmܝ,dhTV ]Ϯ]֘Lr &F.-\|j38PQqȝYRD-] +3Yz>*{OL i&DD%iadd{p33;~\?L/ #G+mwQ-@UM?9s5v$ŋ/Ҷk:5&DDlcD8LӤJnݺ#!dfJ{qRtl[7c.mܙDUi~ =U~}v€.|8xYܝ(uSh""wf0&J&*۶U fj)`xՑ8`o,˻8vLup 7;KKQqiн;:"*t7Ws2}t4p,;7wwJDD22gƅ }\Mv&LPk xXwܳ+rrTG&"rW##AdT7ʑ 4s5U"Tv>>kp`.'h""wVDUKӀ֭ڵUGBDײXkͭWTbb" XNp\@ 1&"rWii_u$DSP 3б#!$&2J7؛>ĉ:BD,.&*wC˥KŲwGu4b@+˷JJ l:"K1cTGB.ȥUVa B͚5ѩS'̛7>ӧOGn???4i?8J<ޡCн{wԨQ/" K'%%z?s!++V+{=4n~~~ܹsKDK_x1:v???4lQPPP~999xQ~}T^ݺuêUns+.==&MBݺu`ǎ%רQ#L6 Я_?}WѱcGk1zh$%%k׮8| 1fww}>RSS1p@dgg㣏>£>3g{-ԩS+`С#))}M&e˖ᮻBڵgᮻogy>裏pO>' Mn3,,,Ñg}Μ9~%b,1Df&*= 99j28pڠAkAAjZ/_nٳqpkfffѹj2+V(:gXgycVXowVZY;t`w^{a=p@ѹ͛7[M&5..\vviӦ=z琔d5LsgϞ֪U:~x6j:m4jCx9sXX̙s[11r$ٻW^?{x.Z;wZUGBDZ&XO=:CR8K@嗰Zx뭷/_j-7l]t.++ k׆ǟK֭˗/}ҥKXj&N?-B~~>|I}'`XQ۷ǤIb'|V ,(:`xyyaҤIE|||#`ƍ8~  ,@hh(F]t.88cǎŢEWj&" 4Q4m[KDcv *ESqzժU’%K5k"88ousԩSX~=}Y4o.qlƿ/>|*z]ݻw#??:u{\oooon;(v,]Zk',, v_c4o.fǎرc;wƕ+Wp!83,jP 7s63JH4vU 0o ?0^~eDGG#99o61}t:u /ݱcG]իW/:Wzu|gx6m`E?y$Id 6ݷ^z%bMg͚ew=O=yd+yJsI_ߺuk^5kqlaETvؾ;[:!.@K}]L2pw… 1uTY:u`ժUbذa_PlKll, ٌvӳsW^ K[y׻_*͞[yξLi@F~>PX(ǂX\aaj?ѭVQ,E~8zTΙL_&TA-cOOQ˫sG?DN@#!ZCUGB.Ω輼%MluS},x@GziZQ ۣG~8rHe!!!/qw0lٲ̱˚%ŝ+>+}T.C9}de.p< 9%gKڏ0~?.Vcn"XyÆn[T tPP ܹs_b֬YSbbbW\t*7AaΜ9x׋X½[tQF^O?}/7ٺukDEEa̙xNj/&.y3f >̜9'O Kg͚nݺ`:u hڴ)\;f,X}R|1b]/r"Uj?.z2e~[8+ |YnW5eԨa /r[?֬ʖ&G&DKӀhV'r&+W*&{Ȑ*QFa1cΝ;va…4 3g,J:AaܸqhѢ<<sXVL6\?2ɖقI33e艵~[.qK2J%x v``X|yU­2m(r[p63Zx_l+ѭp.\^{ III(kРƌիWcGƍK/aԩ.աCZ /2^|EԬY>(f̘Qjժ_͛#>>n >}Mg}!!!xWo7|_~-.^h,YĮPzf㏘2e >\z]t7|f͚gCT*OO۲͕:=ݖTgd؎8wN'쏵kǵkm3sDmUDc6!UGBbZ DT68q"̙fĉ9@T㟟j+Ɩ!tŒG}df|??NIkזNq6@h("~TeY"s3@B_<7p&"*d+nvM%˓Iw|ד`[R\r9g+EuDƢi27DuuI 4oon]7c hZqc䘞n^^ZuvH55"1G,=쳪#!"]b-V20&"VfHk}=+ǝ;e)ٵ$Rʘ6=D'%E.kGID ],] <;%DL*- rsmٳTǓ']ѼP9m&[gu~"t([IHd{gVѪUחq=VT>-Sro+O}}mTaa2Փh6ݺl䓪#!"Z$D 432lmWeP:%Sou⏙!p9rQyag*"|-?^u$dPL\ x֥''G'eӧT.,s&6__%CB7h>6T YҲO&0&"22y95 _v~$'NHojzy,TK5o{$Rj6l`*"gy3p00eH@WXY ^^d4 2N޽˶'\ Hq&d4ǎk˷C|l{Qu$d`L܅^<=.j=ǏcGرCfu~~ uDTo@3&W"uؾH#GRL܅ZXL2T[Bmʕ2DZO##%a!rV&ɳH(!A.:28&DDb}ʎj4i"Z5-MjO'[w]m<266dBj]"_xAu$Dts}Ĝ&kօJ"ݨ1$9G2[_#!dLqݪ#!7]ĜՍf32l qy/dqc[޸\<UM 6"2`<0Qh 0&"r о(nM` ж dI؏ K>^^D7n v7rp*?U :"G:nHM0&"r99<]w޶V+p-w?`n2I-=GLtGJ!<"RP 0LAEZXԩ##&s/Gbpk&MdIÿr"&W:tP {۴I~wO:r#LAe2җgel#X=*O{xQߧݴ#"d&OӀ.]x!HxU+ߞD. 4;0ՁڵUG$>|XÇp^G}7m 4k&ǦMgΪq2 #!rol L߱PL܁"&KpPc* ;[>_}Rݼ$ڜtM7K:"R+>^:Ģ}1&"rfvڵetl;WP?nT_$&Ja3OOK'͚-Z^mrn&TGBΝ5O=%HXݎĉFGDҀoW}ґ#WloƆ <DysI7ɽ΢PWqH@j]wh""[Xzի\^X(ˁٳRE h֌KU8tHfؾHl``(F ѐbMDdt'NȲpՑPi<<&<v>#C~<(c.`"Y!K:*J|3Y4MGG}-] dfƩh""c +(ɑ%zR} f`YD$-[1*J*GJV {*,b#/ "Lle#[#=O[+(Ƕ:,LIuedwS}i{MՑcM,..IIIEGDlaedRۀa\a</cla ےV7i\پH9sDv#!7nɓ1aa 4aCC9=>pط6S}|>26ݪS=8M!!#!rO H1&"2:.8=]={dKoV@zTi# g۶ەg4 յ"Wu ua'R 4-U MP,i֗5Ezn`2Y'3zBݦ PyPpェ#!rOKH=nH0&"26Ef=N>jGΝ+f_OWҡ2'u:{ rEjwoՑh""#2e&Su$D%%E^7T.-6W/r[e,^,5%ޝ:5kViD. 矁)SXch""#JKg4];F}]@̜g.mMeK^h&rwIINՑh""#Xd']n.e #Xk*֭/P$zBU#~ ﳟhn 4ǏQyW={Vc#GʰZ#G$޲5 ?WuNeYP:2aMDd4laET1MVLMun`f#""$MPTm\DVP$&C &DDFc6ˑ 4Qh>; СEzP-NNmHBݽroruWEUGBTfLl7䡡#!r SOD '-z2̜)ŖPVi]h:2cMDd4 [XWJ\xEu$%G((ŗ{7jdkv>Hu$DhX@4MP<=VvI{Vf'V іP7neD7 ~h9Dbb"n[,8h""1MNk~ Ց_2VYq?ٽPIRwbd~q+hbcc;>>'NTc0&"2UdH\ǯJ3a&67n,Err{KI,ԣ{h҄xs5kÆܘ@ə3@^prR#$Gmؒ3O>ՓdgO^]md|E@l,:rcM,..IIIE]ADVDck_hر2rrT5 ;[N ud&x/zǎU Q0[6ydL0AuDHɖ6De&3,X]/(;RR$3%KFǎRVIIС@:!&DDFb6RT/z'*MİS'Ցՠp}2 ڵ_O^dvn]+gi/Hf.&*M~~#q@2V`zYM.}>}$b%e9 1͚˜@™4rؾx9Ց8/ hTC{;s&,t>R#4}Rȅ1&"2BI[u$Da6)f)0Ν2;~=p W36GN˜@ٳRYKFӤ4_3%cbuwߕޭZt>lUSZxU (VMtszUGb ʘ0{zyK 4ԖLH,rsAAr1&"2 Y ԯ:"w(prK;_~1o KSU/_8&DDFiTQu$ t"㥗C$^:նo_꩎*K1cTGBT)@DeiR13be2-ZȘ4IV[' 'Ѹ1̺< )IV"ԩ:J(fCQ9˗zK/BCcedfʅ5kY?=t67jVǫ0&"2ȑ#!r~[H &v.5j.#'غUŋٳt~ދoc IU!"2sl!* M~,||t^@A{$k 5k{K2ݭ;_OՑU*&DDFbMtczoW 'о矗"dWkKJ1=% W12ܴ#2&DDF`6KΨءCb˷]S"dO<;f~Uݭ$}H;-rcǤoh""#00Z5Ց97MWg=hxAN[ouL 0p!chIH7Wz!X,\MT))@׮nDReˤʺ Qzq`>#!r^Ѫ#!gWҾjD#!*VL,>V: "tp%4..cGm &FfNdfaC<!W:b l޼yμp#",fM%{Qپ,)xQ[=$]ȞjK(>v@Ϝi"y[+MaM6vX }w6p>+p=V Ԯ- ZJ_NDzV YΝR""2ed6٬G'"""0x`#*V،F㥥{͛+ Qh4@͚qOzBJ믁F77$dO7w=ZJQN &d KLoYfϖ]~ի%^},h""q^H 7Pґ.77{w/l-PJ"ݵ+PrwJ fG> 0&"R+x LZqY1`0`PqIׯ5kRzaزs8p^HWHz&3@U 0AfΕO?zx9"#6m 4 uk#!<qc#!*~NN':, yضMfnjdwnR,/bc>D6 4ݽ ܻ%Dj:u%HYK2۴W:"DcGZEDH[:u>n" %DDjdla D=|DV =Zy@NĉΝ@zz=u طO>k47 4%h@=#Ъ7~6n~[:; R/w=|$oLT 8MD6z\r䯿$nLHCDP.Pvk4@͚2~4 vX={+kDE 4t]H,V *) <)'1xiyս;pT޼X_K;;mDŀKFm I7g[AA֭Y0u*Х s.Qq 4ڰQv񲵁 Y=j`oqrvX:.0YHJkYiR@ hZt$DjP,.,*#G?|йJOQ 4ܿܺ(+h@aQ=x[]4eֹ];9Y}$&P&"Rc +&DV&* 7TE^ˬqF^pHM(ŠH/=g`e&];ϯѽ~+ƍ}- qHM-JT:"ˠ %V'*?b|)C Re6|x)d pr 4}UJGB~+WJu5D*sw ,]*-Zn$޻P:JQLDgMdtp2oԱcCJGS;?''G%w*!&DDjDFZ۩__H-2R<ґ<$QQW_…Rtl I#$H-79Md rO"ʟ˗`@uhz?l^~سGfχ*%Y1&DDj%h"9t0oTtvt$y- yuL&?LN2""RxdM$3NiiLT?H(?{{U+.WX\ VhgB<!!JGB^T+I[ӧwG,ud$p 0&"RdƍĈXqcQH).N tvJ˗KkKOΝeF#$ %DDj`la%dܑ'+ zEFʊ&[>+c(Y޽vn 0h҄˻#** QQQ*V 4(q 7ٺ={ ٻHDyw" L ^{ 6 k#GJ𘫫QZG GDD` FTRdlB 6 [)rHVK,]r2p*[XJE\{{#!Re)*LvvRY3Y917Dw*˻+VT:J*"&"t Lɖݺ;4ot$Dc0@F@jJGc])S͛C;}-޽oOV 4Kɖ%D5S:"9|XN@qxyirl7߲h""Knn@JGBQV:"%^gtd$x7='t)I[&DDNg [geYwF#K,c=|쑞;״=T?EDDV=*6LwQQ@R2*W6~E_^%֑#JGGyұ4ٺXIjR:"u{ظpvV:*]>\wΜ ;vjxL,YJa1eZd*YHKd9޽իO?ɉko?Y,&DD-] ܿht$D)3M( TbgKe7ސdY[L,he}!8XH?Azɚ+ŋ@>R͛ h""K𡴰bMJvBdF4j|r%P{yo{/RLL 鉒%K"44VqÇ1k,xq v76m 39^+66-Z0j($&&8`0`ܹT\\\Pn]|9:t(ڴi7~pqqA #=%)))8q"ʖ- WWW4i111Ow>|8|}}0sL0{nq۷/GaݺuXhQ:tڶmd̟?d/G|'OƤIбcG,Z< jYN?gϞ(]4-Z={b̙x뭷r  ]tV}aFFv튨(9sիWѺuk9s&G8^'b%[X-jeqc#!Ru뀌 o_#T0iɺu@n\?KP1 ...~Ι3dطo;y|t`0l۶мylthw^}_A~z[oe{~˖-`0K6md;f͚ze;w5N}`0Zje6lX[raʕO}"qР\Yys!#CHߘ1+(z<|h0th0̜t${ K w1N*U<7(&/`0߿C.322`HKKÃr}D.]v3{.bbb0x`g/= 6 -- #F:z=vڕ?~'N3c#F`0`Ye+ IDAT͚5Y>|x}xWk׮Kfwޙ^ k&bYe![!w/W?˲p#!Cʌ ѣ*蘘c(WJ, L6-G2g}ÇQn]ر#kC/ę3g0e.f"*&, F! )e0R3re!K,׭>t xvb/|rP:s>}x1qDԭ[k׮̙3Yfeχ7+ ̚5 :u¾}\]]h" :]6/_ $}?vܙX??{ҥ.]혧O&$$z\nnϯUoO|]"*"z=ЮQ?VU:"uؿoa$zХ Щ8[x-)B@vHK̙zꅛ7ob2e 2`߿B`` ,, UTܹsbŊ GPNg>@H?D}qY_+7O{ >\`@Z }󌷍 #cru;;Y3{{^wp0 sCj* dZI\"DD W%NV}Մ @Ja,K>D:557nv/\\\Gt 0?<-Zмyʗ/-Z 666{'Jɥ)yrr2\]]qY_+7O{uqqyqO{xc͘! ݍrqIN%{i{{tt4]fLQdD܇?TڂKDpxQN>RWy`Nc%ݨ'i%/J+,''X@kZeh48wʖ-gX&]L[e˖\S>upVe%Y%$$dGXg}nc>Y~}M4vln˴yxKNFtrrGFx7:Lpna\RufΞ' AAg6nπ/׍Ɠ]7Te𡌔ij*}8rCXdpq}nn[?v 4VNB}yC#!5SNݳgK?޽oJE,* ALLL3g@ףRJA___> GG\Q_t)8sԮ]طofC:t ȼ^zop ԨQ#={d~]O`߾} ]t /^zaǎw<<<>O lE777TV~A(3k\5NIYFR2) xt }YǓVInJ|txx(Yt=To[DVABB=٭[-R3Tի}$+.,-DON¢hOOl3Fo3g -.] ooo4h.]`8uW8qbcc믛KRЮ];\SNͬ|b $&&_~Gŋ}/P\94{BZj!88K,VV94Mo߾Od;,^t)4im˸}6TY֭C>}ׯ_իѭ[78::CL1-/_PxDeb"p͛ l{nL uRrd_ۮ΍^/YԃlIJCz5#!Rko迟 bE`t`p`r٦|G_>8JGh۶-fϞׯN:X~=Z-,Y-5koߎ09 .&OѬY3j *z=>St, o? sNDFF>ݻC߿?=> jIhԨwyW^EPP/_ս'Mwvo߾hҤ Ǐ/`oC*јV{yu {WGwݻ;'dx%==m,Nd{Kh&z`*kWITɓeuwɬʕDhsH`xwe˖!889 ըQ;&N3gm۶k 'Wbbb0qD3%K̞=;DZ}_bٲeV"""-~]bݺu>}:F2e`ʔ)6mZc;L:+V[Pn]l޼-镙[f;;;lݺǏ… FCU !sh4=y]Zl0;w$}[߾mn}\޺%.-+KtwW NitDKm9cKt۶J0~ZB*GF/ $l``mʟ <+WTfɓ/cJcG8}떌;w )Җr6]ztiǝ&-|&ND`zaCt4D` YO?U:%oKj2V>7(&&"uۜ*i2nޔq\7+]R^̬ȥqd#΋ҥKG=ɖ˶1c#H&LP:5o$VTrF 1&"јWc32dFu7n_&KYg5ʔ}RB EDlURRq I##M_h"MvveO`0Ȟk׀WMqwv*eJe<~&[' @℈Y`.YT)** QQQ?/ 1&"zFR*U\J)r|Y.^;_הPG@p$laE")I*pt$D/2R~ot$dCso~qcMDTԓZO%%Gr&3ж$x(]]k#*jV "z7YڸQaMDT\\\e&=]>AA@Ӧ@B Ve]iJe+'~~E_쌨hri lڵ:OWɓ'quh4Zj(l(KL:tYedH}Rq"pngd^?{R]iHLRAVȲH=k)"O s|rlذGEڿPvm/*W\Y=2ܖٙiխ4yŋ_FKK"]$>el,p8__I+T0%*,6\S~Rrkp<0yґ<>>|8OȎԩ:u'N;8"" }񾯛,QIB}\˞[d98]IzYTq~%&K}X#!l@͚@Hґ<,QFKDdst:͵$@Z22k׀R9 TIFŊe8ٮ={d>W=2s&gYKMMERR\\\̠I*5h$ .Sh(cٓÇML\]Mtʦˀ)FOwK;iDdI"#5avJGBDce˖aժU8x nf):퍐Æ cBMDW鲄 Ĕ(T.#tiu)>7Yk|nŊ:(Hkb0r].շxu&,?ׯ_GqaTV ;wF@@J(d$$$`޽xװxb/-؉˓ZXY {{S-M իyYS-ILAAR،iUDz5t$D/1c >> ڶmoߎ~aر %H""˥5'ЏE?K؍ٳ2ΝΜ}f%KUFP\eje?".9XCjQE0;޲e ƍڶmO>)ppDD6* @H,G:k )Tvi'6嘀jC ;V;9) eڲ{0@H( ̳_HIIwPDD6Iʖeg;;)<je?%EV=+KϜ6l} 2%ժɥ"_ͺ{yGH,SFkӆE, tͱpBʕ{qz ,@- %@""{7%>}ZƉ2*+'tǽEeIؾ(wZt16MHf'G˖-3RRR}apuuŧ~ZqYhX(SҲ\8k4o)ܑKD:8ؔ? WVf2EDku( =F8x f͚kbժU9K/ɓ'?qalaշґS.]>c2cR}};b<^$իKb,ٺ|Ҿ[7#!LNgs SCraXx1.^$%% "zUs -{9wdOe7L #_J2Y.] ,RZ]r1Yǁs`v >>ƍäIPZ5)))to_~%(DD;;LW@V2Y*}Њ>Ko*hV.6ZZ-д"z 03LDQQQ=z4֬Y"!!![]o߾hԨQQLDd]z)ƢPdyevp0 r름:.N~23mLk.9ׯK m0z4O.D6jԈ 2Qar唎T ))ғ!aSo;;]"0*fʞM ZD$:Zwt$Dd<'DDT%Q!*.ΦJ.?: 1%wl}fQ"D`:wYADE" Sf:t.]ʶ;$$}EՋ"V""clUԽґ-*Uѫw$-hH"]$AAIKvح(M_JGBDy`vz _~% ʕ+#99wڵk1m4`ϽDDOv|n4@v2-3&KOjIՓ:88^θ8}"*=]Vyk)  3dL<#F@@@@c.]?gφO^Y"ps5>GM }i~}?ҥ%&"ŋJGBDydvtROlOUlY̘1wŲe˘@=N'IŠh@ 'NH?o-+˖ɱ5kzP >3RRR}aɒ%úu2n""K{"kʌtɒrV-/(չOdz~C`۠$ӡR)6Y&jj Q[X4YX\j"Irr?o$k`SB]. ڰAڿґQ+&"BV+= krR@X rE{͛P`7n,Iuժ t * QN,j)NNNFDDN< AAA9Y믿ZDDVG8cFEy׋=din`ƂdKK"ݤ I~ HHT:"* ;wiӦ8yd}SNرcfkmueرP%":laE*6V_5FfVd~TDiSE͓WT `J)WQ@Ϙ1ΝѳgO\|s9spaYE+ud&je?2$JÆonRݻ%^ҴܻY3I+Vd,*qqѣJGBDzx0dkW^A6muVx{{YDDV`C#!*\))R5{p#1򒽧;޹sLI,I6eJGM"2RNИ[, NC:ur?p@/_ݻwG-m۶B *ݺ%-M_ɖ,h4@PA$$޽SǴj8;M54 IDATs4투 s}Î;ЩS'4o *7h62[ґD Yݬܾx4;t)x3Ӹ1l̤, tt$DTH>ְaC_׭[;wΝ =[XjeV>}y_>YNk*lp, 'ͽ{O9QCDVx"V:u?(DDeCYx޶y(FV6m&NoÁfHJR:b$?/t$DT^ݻwoСC 걅Y#֔tZwo;wXN -dpJKN%*f'DDTt:)LDdMbc#)zNNRQ#`}߹SN"|"[dn])NFav`@#!Bfv}<=a?DD[Xkt$D')I*pґ(g$Y8P*+ -wɲM%nTnu2)8WQ!3;١jժx饗0qD#"۷Z?Ц ?%2EfCB$~9Iz:?,\t$DTNM6 wۇɓ'Ν;={v$":MHi&jx5Y֫Jѱ>ϗޭZIB]6z]DqkTH@~!C`ŊL@K@0$)lRYRgNo,_xzʞV&Mlc?5Y JRD,,, Vy!:::vxx8t?8HHrqZ;&3l 8;>VdwJGLOrsg#!"bVysFF1l0 6,_;v, tDg.Z${ (zuxML~(uH2ݺ5[;w _"Jv4j(TV szӧjժx $U@ujP& |y``૯mۀSefsYݿ?R`P:Z~q}* !fϝ; `޼yxwPR%ԯ_*U nݺsatȑ#1zE$"k(UnjQ:t.#9ؽ7`jonWEȔ DGm.'jf%2e &L͛7cźu`FAPPZn=z[nptt,T]@سHKRitZp chn 5V p'""bիzHKK͛7KC$#"R/c +&d-bc S`ґaCI;_nnRѻMY3U鈭DFʿqPQ+P2eV,DD֏=ɚWut$јM!MƤI?Im[tIMDVSDDIefHNe\mY4re\(ӦuƒLj*tr%P,'"gVn""*$:ojqHґГJEonF3dok7n(\'0h "h"~Q"kBUjS_;ہs9svdߴZHwQHp83(Qݽ qy{}KɓK;WZeqf:wo[$ 4Qq{W>pY={t&S23|Nׯo/X['U:"*Fqx\\\f{+""[X5jpґPa3&Ӌ? L8:tNgzzko>V^f+3f _{57.AY%^.@edvq*eJIZbedfɾߑ#M{x3p:t$DT@֭c֭oߞNxyJGBT0Nɾf͔л7?cH5c%LJR:Ңe0@, Idڵk}Kƕ+WUb +Z[WHH)R<_?#@_CiS+o@3FHHJq>~'&DD6 4Y hp`-R+KÁ_~dzX9Ҧ *jT&_ٜ|-իlذ!cׯǷ~^z88""ӱ7ѣL ȫWKR}E 9"ˠ994h( ) _{=ĠW^ AڵGÇQfML>P%"R[o.I~R%%=|8pg :Ze)YNd㢢y[o,j@{zzb׮]㏱vZ^AAA6mƏ77ŽHT8MjDCj֔1r$pT^[Yݩ$Ӗ-`W GxJ_֮)vU*+>z(pʕ|EDdU%`Mj+Ք6U:Evv@h(0uš5K͝+'NHK+k䫈+hR3U R:u҇C99m3F?;v^]Nٳ2=cF_Bf۴i˗#555c/_W_}l0OD$t:dMj&Io@Tе+  >7@2@3Z3gBӡaÆ/۶mÔ)SPvmddd+@TKUl}Bjܿd٪V~[f.*U>LZa l<|hݸ!  D~CbԨQ6m?n}*UTxQ^gRXY[ґ=hL y.YRLw&,{I{,؉tZZ7o̙3@ʕQLŒHMd._ ׈Sз%޲X R]L??%XZ/YR؉"/aҥѨQ#4i҄3QnŠi.&T x-I, J=~TNOclpec&"d\ Ξ={UTAǎ1h 899EDDꓔ\<ґOl<7nt$D^N5oܾ iT=?nCDC#G{pdɒpwwǕ+WpZ ~!6n܈5kIdy͛["alaaVZ-Pu"aN7˗'O..r}{ND<$GqU̚5 C A```z+V3Э[7ͭH&2vX 4H0,[X{C* Qѫ^]r9. ||]l ҥKi&ʕ;#ƍ}Xt)|B Hut:]f9!&_fI|,޴ X DkW Qf۲e ڷokUXXڵk͛788""3V+{BUS:)3meذXN)}@F!Q2;>rSg0;("" ܤfZ>قkפ؀Rh,+{{iS``V`3@ҥMD͛7X???ܼy3AY &ФV.If͔x^ 89={>ҥ!CFuOYi"+f蔔8::xa"" իLIZ6$%kfww!!2ƍWIIĻw|}6v"*Vyjcuy8p?z\(LIbcefdHͶlݓЯp{E YݤIΥDČ<):u*NZTYxdhR)4|ґ )֦ Ekh4vm`hSn0jJ%ޅ;vMo6O/a"u:&قUd vӧ뺻}}ǏoYVPDDOeۏ 0:thADdz"ujOJGBT"":ug-hZd-tkJ%eV{wӳhޟt =t4EJ|   QaHNnI g$>^p6قHlYu}_W1Cf_{ ^y8Pf7&kt$DEk*D m fd\,֯V4Y[L&pٲUXq(tr$)Ir6YdƷW/`i_6of͒de$[7}۴ 8tH/XQQ$uٿx 4YM$_HAgÛ,0i UF Pf (Ye R(~2D  m{)[@D>hBA*tt@$O]{MІ$}s_ڵ2aCcGK͆Hnv-u݇+DD`6sItt(.0MeU*!Νc6H #|\6E@ك5&rU*StQId.+ksHӱ=8A`6` iC,&"h\uDwp2˷Hs#Af֬ DDHyƍ2W$ɮUK£G@ʲ_?=E(L&[?G4~~"};0v48PH^͙dKHèQ>19DDH9,KɕL [u$D*'7W}dn-y,^z fOӦI:Qz\,^,3KTRLl#DFG|ە+]:2vd`f!}28p@Nc`AwTG@ٚ,󫎄(ubc@{[ Țx Ց8V@>ΝҁYEYei6cFNJhhLܢ &DDf6K1ՑGdzj^h=VÇ%8=[V;t P%9;wu$RPP> An1&"5"Wi@XЬHg& *Jj UPW˱|9Ф T:Jr3g11С#q\!"5 4s瀫WYM+.XRVиš޾].Ozzvױc2mnKK 4-,]"Wa2>>pLGpU-̍^Nugm+sW!9ZLWk:n""[tIs\Ԭ dά:" rՑ8?U>2kL`|?ݹ3{%EHplwO#@ْ%hrwǏ|rt:S 0~ UK18)~5 ~K@Wo ņc*.4 (]Z31&"Y.YMmI&{$n]D2eF#0c49R[DGZJ͙ϩV,yVQ%/:"4 $YSVH-A,̝ ,] _|!…;#O 4<{Kd7#!e/'{8ɓeqcIxC:x_:::U*W`MDd+M .ػBBY]^x5 زEnڶ=X 0l4TaMDd+ ;uJ@s|E.]dV19|ߗDz@cGINP{2EN6m:g""[.]n"g T:"2\-UGBi-o\%ݻd_;r29b,@•+aEdV'wrNjV GL@2/(` #eKg >}x?@GX+qCJƍrCՑPFyxK|#UFv& &Fu-.8Qzt:f (THu$DOwծ:"ۉVZ} F `lU$JñPC 'e泗h\h""[09Š$dU p4#Tu^ T|5Ъhp\ǍYr"0Pu4. 4-p98`>v&i2vmY$V0~6k,X 9[s~Ӧ^UGҘ@hrv'NJ ?;9|8sF8~#FH7ߔXZӧ׮9 ")ݘ@euU"#!z: ȓ([Vu$D*+ׯO~l* 6mNޓ&s_Ȟ_WcMDQ#]AMdmxW9+{H k|::.*#G U(, hrVWOo;YR*^{Mu$,gzl}aa2|yѩq\FQ60hp2ߥ|{ o^Ѹ&DDq2&ge_%&P]܁tq_ 8|YU<:yH-+Ll9&DDa6%hrF2¤n]ՑFHPtmyz*ѬtϞYڽt%\귺k.4nsFΜ9Qzu^:mbbb0~x(QYdAɒ%1a=Ξ=_~9rСCmPn]d˖ Ƈ~'niL_|>>> ʕ+]Ϟ=ѨQ'߼y3V +V ~i1GGGcĈC֬YQvmڵ뙿n߾}ٳgGƍqȑ'nWxq?аaC+A+fr90WH2`Y}fEtDvJ",^, JTrzh޼91i$L:ׇbIvnݺ>CӦM1c ԯ_cǎE.>>۷?&Oc֬YnsQ4iQQQ>}:CO7zh9͛7ǬYPhQtVzⶆǞw؁m"o޼5kڶm?|{{ӧ{1c<==ѢE LѲeKF 4SLիWѰaC;wq&cH)Hd2@#!ʸPꫪ#!wx"m#=d矪K_|}ƽi. <;pf0O>$Ç<<<Ǐ'\w^4MӴ;wjJ{5޽{ -X@3 pb2eʤ 80ׯ_?}hZFݮ\rZ*Un̘1v̙߯ -888Ẩ(^ԩߍiڪU4[.k׮iyѺtŋƏi5h@ի?oŊmŊϽo8}ZӪUK"U ӴTGA(M{M[Tu$Dwթi (6Vvд7ߔÇkٳJndRA\bzܹ4 }4oΝ;'s4-jpdd$  _{.vڅnݺ!{3=z@ٓoڴ OrX,ػwo~)>}}MMӰvڄ֮] ///7:ooo;ػw/.]$kעPBh׮]uGǎi&ĤcJѿ8@TW{XNʶ!rk5kO?Kƨ)3U{6 hTF:hD֭`ݷJ.@ڵ غu+)9s"7n\.::>|8Ẁf̛7ΝԫW/'N@ll,Wgeʔ +WNoȑ#Ȟ=;ݶƿ .=p֟.\EIGAҥ%O9rUV}5j8{l83:Š3L@BC/:Z l έ:3OO7kqSΝGfJ2kt6̡w͛7'|x 5/xyyw1bn:|爍ĉ !ݳg+֕+YfŬYгgOĠB Xti/_ @*T{Ivۂ >q;FDD$\xdy$˗/x'%/_FÆ ˗KO?̟K[ׯ hrFaa_EaNMKՑ //-…@rOq;_|&sZ_࣏>書y&k=ٳgG-PX1 >YfEժU~3^^^xaf͚l6RJLOē%Kd?ÇO]ҟOҲDEEelRاqqOkZe||⥵3%ϯ^\'M?H<C&zzʓ=~dʔxi=2gN~:Š%l>>x!}spQԭ[ضm:v숷}eɒSLD3 {ֲokYxRQQQȚ5kFEEx?+%ϻ=~lO5o<5AMZ?Hɯ{x@ u)%Dܚ'Mؓ&I[JLdKoz}fGϳdJf<6TGB\X{l{9p8ww)y{K)w6OzRY~-'/}748X\SC8Um2иq Ο????OI(Pp֭ʕ+'OӸuʕ+ooo|)/?Zb˗ s…?x;n˨]vۦT?Mp֬X'3ޠ ٺs7m &O[MI%^>qtI?Nzܹ#e))&OCj-I?Ξ=sˬY,{L3gV Qr&P&re!!B"!X!+AA@@Μ`|es9U]reڵ+u Bq9X, _&)-[6۷C44m4ձTP^^^8x ڷopGpdT w ~EDDҥKɾJ*q=ȑ#McoMӒ5ۿ?eˆҥK?fڵk) );I$~l 2ɏ##efad31Nz)G\O-d6/: 䢢>@u$DRMg>/F|PŻbر{YrʅMbŊ;vlB:$ܶM62d̙3gOsE"EP˗/̟?Ke7`0$K۷oSb ]xbԮ]; +WpmKJkb %ׯ_ǚ5kЪU+dʔ)տr!]G5M {)w/;r۔ƙe"OrII?ΝȓGǹsKi;0JTGAܡCrՅJSfTGB>r̠ `"Y1^KNwhTIɡ*~6mڠI&4i_J*aƍ0L?~įcǎGٲeq],Z.\mې- &N:hР}]X,L6 ͛7ǫp; <_~%bbbPzulܸ{AhhsG@}hݺ5^}Ut 'OٳL5kC5j^%Kbҥȑ#l2\pEmpԾ}{Ԯ]z©S/_>̙3aiݐ 2Kӥ1.Nw;w۷Emy#%ΛG[^i@H3,ۑ+qC1>']I}ݺ ӧKyw߾>"-##%qnHTH`ƍ3f VZ%K !!!O*רQ/ƼycʕU*U`׮]1b9sO>4i|Su$DS00vrϝ R{@Ʃ2gT 2MK&BBBЭ[7XBhk+lDԉ7q͛t{xȪoĺ@Wv"8|X^ (Gd kp늎Ufd-:sF0y/;`Pߧ:6( eVH<<W4 LQ\X_\S2+ߗ%KbB]I?.XP=/ɶX6u'Rd=u BvJ4!rIrR~l`@y0+qqĉ@Rh"rMҜdbb$zUk?tI_|w,2`A,\X.( T۩q&~sw|rm&4ҏUeo=H- Hr[ O`|V 4L$wzdW[lAȽ1c??KΆ&"##GCUGB~{VѣUGBXPl;#ݹ3Ъ;h(_^u3qt )&:Z^&~WG2l__)~~kBJ>}&W+T:"5<<-M끅 %yΙ)=h"CӀ`gO{wzllbrxX,RFn9sbB]HϏe>&P8/y0]2gCl 4QZr7YH#F^K#3EkY>>s{I֭ LLK$9FmrUׯK\Fd5L7ȗOf?|#3CB>Ӭp>C5.R6?#q4MVf <\p?eeH\&ŋŋsݙLrJՑϚ5R曪#!r%U%n+RD9t*CsA@Ŋ&DDiVuΗɦ:֕p˟RqkS\$NTZ=j;(`:MiHw2X^++W3G:Ϝ % d7LlsF$]Q#=8/^.\㯿]d VbL'=_FP Qlܽ+ Hz8|_ Nvm}5gб#[@>Ҁl 4QZ@@(-sfdI94YpAf?/<(Y~^%X;ey:#!Jx)SmԈ YQn&r4Vfc[J.]رƘ@Iꫪ#Ik5mI 7^^D(!K/RpL&{*:3Rf8Ց9ivР7У$ J5kZk0&"J[Jm-wn9;w$>>1^R;[j7 z/[P Q**H};edU\9rIsNٳ>Jܛ2 4QZg˕ ZU+Ms$>wNX}bg|Rŋ ܖΞCݺ#!J?&Mh`dѲm~4#>]F^թ|[(՘@"?If%/`n`r4*+](SF.Kؙv=3lـ@Ց]h(P0иH[r+۟P*_^ 2ޓݔ&L"<||TG<=wb߿T;'+Ow'V*$ɴ5.SFXsUL&9ŗsr1WJAܗIK={J= \[ݣbT+.QZX,*߶e4Ji\ğ=+ ٳҴMzΜH$^l[ݾ-cNƎU QڭY# ڴQ Z&ۅ ,g{˔IFƵh,^,+6HykIGoz&&DDia6K1ei/^s&y x,L-+G@|}/(m>me m pv){zKʺǍF`Ȑ=O LRKӤ+~i1֝;B}aa x2.[VJ=6HfVfйHԺ}kן-(E_|=*#9)@ݻ/(+PV*r$MfML˕*~~:.ػWV\I\4kDJVl ?1xHʕ%Kd_@2O G9&DD.z,gqժ%^w~bBRb|-W.I+T|yםU}ꔬ\ԩ:ٳG#!Ra`f`h!{7%Kmۤ[oeW*1&"J-=΀v5ٳ?R}} $&щdk*(mBBd]ҳ(M,ܦFO ]+kRrLRbY"o^Yjp2pdbR=g-gK*VB9alaa<JNU)STGBrR>$Ĺ^~ t( חcŊN&DDmw`0ʳ_bXS}ʘȝ[V+V*UUvqC6`"W  HX(YRu4S̍޽[uLRbJPكt.S&1ݻ:}$!!ܹZP$ӁX)U{vm-k4Kbus||hWO^/K_3&"J-YJHr2ixEIRuky$&ԁҬ,sfe2v4ҧիe_eV#!ReGu4 -g&NÆa@ScժU !((HaDDvpl _/Ѻ\wN?.IJ\\bB(]3*.طs,dSvj?t#nxd/0~:O7@S 6 ]vU}Y,rʙ3y8YIo.tRE8qwȵl,':uR :gQQR[h̏9Ru4(58ŠRSʷ˖&_DDHB}paܶP!Y&/n&+g -F,XPu4Dj?.U}(:t10ϼř;h"0e))- :h!ݹL9"]MccĻreZ5jUxSIVu&/.["=}۫&RE3$$ݺusPD0&"J drI3:kCe#ҘllI=PW*G|R޳$4T Ο-Ujw(5ÁEUGAQCxH<>,ǂd?x 3ϝe-"guV /˖ѐ𕗈(5fn]Qd,ܕ+˨X㏥$|JiX]$\ g";|$=4`:Ou4dCL=mp^^@2@dtoC.YٲI35$.U{I?|8O>o_aMD<Vj :vL:u$PAߖS$>t3GPΝ:]P`9ªUU+Ց9^dF_hPu4dcL:Š{I ȟ_VT$Gޒ<< <( up$ H"]\:A lt([ f\]h2 IDAT[n 4Ͳ#HHL&WRF[VU]u$D d"{ɭ1&"zk"EAt(ѲfՓH,޻XiW49]țWM^h(P<d{>ah"g&\&L&P!iH {j޽O{NMٲM*T^\0r$:"y 9h-An 4ѳ@\r9ZZW9Pܸ!eaaHgB gNySY|su}\)-TGBX ߜ9\M6Y,>f3p"0hHR'_>eK9S$ >DX//+uiT{M JIw{aMD,f3?U5j$*Vՙ{e`hʗ/1][Fgkڸ:vT &{UGCYfBh&P4ruo!Gltk6$޲ENT&tzrV )ie#ȑ4Mu=(WN.__ORV=6L}T J"=aժIO%&DDOc63ߔ#]DDH2%*\8;2R+`4~ *z]V/G9cb97}::W" hR!, JYl@frK2:GYoHVG}|TG~F)_mBu$Dqd t 4ӄK`"G2dtZ,#q ^^@͚r $?,etFo[22`fG;MB޽UGC1&"JÇkl F)C5 @@'e? 5jUI6 PڰAHcv߁ٳy҈@%+r$Y~]ʼ 6 2XhXѺ)u _>ߝ;ddh"%@#LRǝ/+oܽ  | ײjmM~AstQ c̘ű20&"J"#Q u|UӦ#q9sJ-awԴ)Ф|nBdtU:@ɒ!#GM<+._LRb6K687p*W9Z֬}$ ϗM(YR .MfV 'VmUGCN 4eXpp0VZyPPFDdf3hɱL&]H;qGRr%`$MQॗ9[\sXxxhʰaÆk׮ -TIu'&$&3;ԫ'GL pkz5patf(gx7`88 Xy׮r( N!"z\t4p ;pܻ;mg)m>{i*TLwt|-pH'4THKӀɓʻ搜Wg%g@/]^9e$:u{>Y^7OV57wXo۶={B?ѐbMD8khr0\H(-2gח#:ػWV-n** 6֯UUGBd_2ή^=ѐbMDpˑ@h:ooaC9>;w3gJVժL7iέ:{H}kMgaTGBN 4,F9gׯ|۝Hꫲϲ2=e @ZA9Yv'k;yXNUGCN 43deUGB#Ъn~;celY_e34]Ut'rWqq29 Au4.h4h4&|nnssLN8&H:[D'9Ur攽үTsgΝܙ(KOOѸ %N AnFL1NƌQ 9tS޸)@BK2rtHP4P`\GWh0&"J*"BYMo<޸Y^zIv쐽eH2ݼ9븘Ο>c?r_& rDADDNl^P$+|LyyxHQ,'̑.</3? | J@%e6KcUGB..NWk:rF3K ?۷|L$sj[W[$Q]')]5R &DDIaEq4p6˷rڴ#"BVmWJ[~5kd5'wȝ͛ܽ |1_)M@%eMdĨbEՑ+zzNDzf`2|,whI[rmD`JQp4QR hr Ifrd //g;vSJzp_モʈΝ7j֙%J]\Wbb˗ف Y=Iu$2e6me 0lD()쟣i2~}X1GDNx…S!J;>j""x΀&۷O._~Ym~r3: l*++WJVdtJ%#7#\̞-UGC.%DDVV\&{3r值yUGBdIeײ"=bgZeUȞooLN\&" V%{SrOO^=9nߖ͛ed_˖oHL), صK9s\h""+EVh6m̛sӇMB&MD@}]*e,&g Yn-~~@~M̙@x?iD >^f>):r\&"ds|<h{eiڪ#!۷wȾ<=e/7oJ'ߗdm[/Rc&1`< sfѐ 48Š 4ٓTRXrRڨѳ t] ̟/Vʺ?HN qU͛RE@j!7:sf@Ցg69{ ]Sw{AfD/΁eEz" Lj_}%UGBn 4 o9Š1 2 49T)ߛ3'б#`4J\*pJ!3*M<(>P)2dOaatiՑg]dl ʪ!@ve[I*Ud>1&"g$(IWʁAh(PФ~fΜ@Nʕ̝ h9¹dKTQ8삯DD҅!^._Y,&gpw@Gn0Hl+=`}^-lE`bYUhM1&"rEhpIRHdsrȗ֭2WzNd & C:rcMDda6r_&eϮ:һ%mֱGfM9]6o/Kcp0k:rc\ F֭Ѩ:$1Ld? EGK&o3ض wO*+[SH3vt=[<ŝ;@@ڪ!7hʰaÆkjgH9#YVQ * 4/7vOOqcܽv/] ԫ'I~lE7s&#ɉ+DDf3?L'dIՑ#w2wH%Ci::BrVGȖ>S h""&dO&>sT +fF#0>Ktץ3a*XMD'+L,2hHHN~ ՑT*իr^-;uSzo+VsҨH#"}+0Kq  $ {غ{P୷uIK .]ҥUGC::Š 4كT&eD@Pkf,KQ"0cТ[?4 <ȓWu43LHfY!+(IZHHVdZVT [ݻw,>ʏx=w/ǀhHg@Y,aE2Hdoٲȭ[>y;XUGHv0mШPhH@Eܑ$'gS ٖ-Dw:ʜYJ-.qSu3gJ rsczpՑN 7 Ԯ: r7&e_E*8MM`|Y׭n͍IéJTGIRY0dPhHMDeaUH݄oJ*.]TGF۷Æ %: 2eܿh"үWpLt&R%$\(_^u$je*{׭3#GmHRjZ= ž%h"/"{1ե1 = t:!M͓*U`M.%C"l؅ Ƀҁ{ՑJu , @vfN\)ŻuʖU!%5et_u$hhLb(q@~̀́&X&u\=Cy  [ȉeE؃J'ٳ BPPP!!!֭ˆD_f3˷L&]jmJu$'q)2}`nF/!c&!baCi@sT6lڵ$hXXI/Ln ,Zܽ:B}7}1#i0&"}ɖΟf7_El DE)uԩ2waC`eK}ߟʾwߕVDN 4ӵkRdK&tޮREu$Gqq4k,:QNڲEfjo iS\0iT<9&DOV\&[25e4 ^_!CÇ `@鼯i#t'OG'9&DOftVeYcLX!ݣTG޲f:wb\lv =xM)'r2LHf)H]Urpq^W3fHG͛iTGB"&DOaEf2%J u몎D 9q6ot.V 3ܽbb ~AJsR Q@>1&[7~M*DD?( :,uhge$hRa* GI9(|nXL>ӧW9-\(9h"ҟL7;pٲ 9o/#ȹ* 8k&5aB4X\+:gbMDc}h UM6m:%OX'#z 3FG=o蹘@R:rw'N69^\`4͛󫎆R#{vIlGN{ӪSc=dά:bMDc6KY_&G۽r\ڰ78w4Hŭ[2eKzu h"6#[2ҥ__Ցh"KV WL2jZ?;ٝ}\64`MDlHǁ?곻R|:=}s;&҇۶ɪ{Țն?H!&DL)bc}XMn"Au$N qc[?aLUF`] L 4h aMDiL)7L=VZn"[kɈえ9se0&"}}[^̙@SFL2ôtiՑܝ'к5~2v˼)9{@H!&DE\[X'ٙW^JP E@NMkm[`,\0qd)wDMu p˷1Ξ}D#6otNmKQQOUg>cMD`6K魏HUZTGBz*Ԩ:ҳ9eEׁseEzZi?۷ʗW/0&"}J IDAT3eJ"{pسGV ICud_Hw%YJMƘ@>X,L)=g69FhT4o:>Fٛ?f нtGi@ƓT@Df3а(U9"Q [mۀwޑND襗id…UGFd4a4>X͸9&Dܑ\2M#=['eک8t(^hx}X1ё!(ɘtMaDn"rVL)¤|Qɞe~n˖@ܪ!zgCaƍ''N;&7n@ hʈK? ܼ)È]||.^Mjkzao{+ZRuV:_w[Z[u+R']ZNZjEe(/A!7 ͽfl O7o.-c$耀( uA Xd 5kEX|9RUӹr ի'''|7:9qׯxzzbȑxvB̚5 KW 6hm׻wo4jHkΝ; ;;;,Y&L@JJvIII BѢEaoo???8p P8p *GGG4nZە*U 'N@>}>cFGKLQd$`i %%LïXYb۷4wgS_1fL",_<]ׯcĈ$ FܹsQti 2}gjj*:u///̘1۶mŋ5A&MF``Vǎ͛7ŋQD t 7nVp޽СܰxbtSLõۻwo̟?_~%.\ j >hݺ5"""0b̚5 >D@@]UFU9͘IR*%.3U/_ay)5jc=y,Zo1g??` K!`Ę&jB$^cqEm+$I׮]Sŋ)))B!'>cǵlRxyyx!I?ℕ>|4hq !իhԨvUT5j~ Bƪם& c1h]۫׹rZv^kAPBx/_ѣ{ GGGlڴInǎxd8~CTTzŋq% 8P]2dزezݖ-[`iiؠ_~;w2{ԏ2{,X;wƎ;[e?c&%.[9s;g_Duݺq:f5*V,/1}Pl~2flL2~ߏ:.߿EJ*ATbŊvƍO>D}wޡVZƸh8::RJr111uC8(VcQB> ___kƛ7oprfVfL˗@'E)Icy%<(ThT0$]Ъ 11@ǎ4[VIqF{neʔQ`ooŋwx-Vkתw d35-Rv޽{WndwwtG{! {{{n޼СCY1-,<}KO{j&bbZPlmml%V!z| AS{h/iXUrn[ׯ)Wi$Ѣz ‚2f2mI`&bmݷY^ں>L07)'2RЬjB`f`pqcxɌQo߾œ .\X#q/ >\+ h )SТE ,XPkkxRR}611QvKvIX;;L8L-Xl}D;aUyAE@XZUQkk_5M`eE[[.mi/ܤT@e+2>0k(Xh6gcI34!iHK޹ww2({5#3F@GFFqnݺ^@ݷ׬Yرcs\UfUݻEjl{a6xyyi=Oc[]ݴ9Nn<>3sĴV_]7FצMZuu-q? F(+J~Q^ZUKƖK tr2?}J듓)OLi(HѴ9AsqtLM89boρ8 9.1w/hM0~Ξ.R4ɓ4.W/eK`P#XUh88<<ʕC:u2ώ;пtK,yTZѩS'd/PQ~\tI# ɓ'}F"wΝ;Q>x8998;!FR'O*T5s^sݫWT{xNܔ (NH\޼I}󆺨޽_;R((vv[ [@4(@r7oW_]f rŌԵeK ߹n]|l4GzϞxWrZӋFll,~L9z(  ,,O??~:km߾=k,]-@>-_ŊFJA/[ $i:uœ9sQF߫WF s+W u [lmбcGǏyfmV$YX(!Z_/_j޹CEp(ݪ77ͭ oY|i1WhL{c̠,{_mZXMu(s!@6in30 3kЮ];( t7nԸzVǛ:u*ѰaC 0qqq7o7of͚W_}ٳg۷UoߎǏ#<<<)fϞvڡYfҥ Ο?%K`Xz:u 00=>|ebڵ}Vv֭ߩS'O>x"ݱtR!0qDfD$)m{BAt Ϳ?=nSt]A-Ӫݝ77` 4lwx:na9I]JXP dX9Cw)IM7:ge,L7f͚(_mn޼/_B$ :T>IO?(Q8 |7pvvF1}tmg̘WWWXk֬A ;3[ƶm0qD1 Ƹqt[Ǐ3T^vB7cP(g= .DBBԩue23ga¬oFw((}go KOjw1wtL ` scU\rrB5}UFrWiԩdJo/wihޔ)@y4[&@_tt5'NQwTs9KH-aa@ٲrFSj*o%}e&{l` \k3B,/_!(1*~/GP3]θ)xzG8,'"#iAr'O.<3cu6jнh֯1һvQn܌1IR4\Kg]r2ТYիߧ/iJOOBKij۩ZXi`eE93FB3gp.M+w)|$[o.3S@3L5sjӫW4O?c\\[ѣKʊi// hJ4-w^bbѣ. 3G֭c?t}hM!9@~@tchƘyqtʗ s:ܽKAݻ4ם;s޽4JHn]]99:uU,/CS`1Y|I >cKs>>ڵݔǢcGOʙýX.1f^3px.]c wPoխRI?~#%J-ŋӭKޞ;@2#ܔ Svw̌Ւ%4\j(K,,v(C/+W;w< o8f'V?^$Z]]UOHH U4:}pDR(byH5}Ur'[. c 13r:}{`\GʷX68f'n˽}٥u:޸:‚Z6Kt鴿9S\ !m0Z5NlČӻw4s*ٜ,I㹏]Tr^8f';=P"- <}J-OG˭[47ݻt?S@] ennP"#u$\ 70c%aLp~=]574hԭKV Z9Xp3OJeκoIYS󾤤 M`6j ;x2ijv?wn,SF. ce6E`sccKA…ĉRJ.3@3̓R@*В޻wT7)qVھ=-vwO ˕lYәrؼxAǎ$<|H n-{̴ ̚E 4ρٳ/ D,[@3̓R Ԫ%w)ޏ%u.]h8mwuZ]6lADc˗BӓeIʒWܶy3]A0!375k9]8x:x/ c'!A{ tNYZun4m}BRk׀MhJ.~Z˕"#y)\X0s@Y;tdE~M rF>}Y3`"jW (tq3?yۘї|/z!hWir))"]DZ UKj*B~.]. cږ-Y"F$03޻7}/ ݺc}67,HKzi듒U |2M _ҕ*R"ʚs&Kgx*RRIF1tz*xx]R:nYFݺy8Ќ1sT|XreZTRS颃*|ƃ={FQ0]2Vb^Aud$MyV%aqL$iJIMē_|!wiй3][>۷}ٺЌ13t66/s(6ZK˥KՕʕJj6E'N~~0宰0SP2cy3V_9ww}{ݳ'Щ0x0]xejc'.$Qw"E( @AGE nBB…)VǮRzO cEY͘1y>wȽnrחuo,_8|в%_χ8fZ~X$ 4uBipX@Riuժ JS_tlj3/OsiC.鱴u>7G`N]ܥc4c̼$&Rcn6 I4isVR7 v>ښƏUFuj-Hj)wwՑ#G]ҒSʂ@Fќd̄q3w-wο$ E5ÇB},-[W*SRP]ug}S0cq$M4a_(4E?Ly<̡bCR~ar8flܹبjvw F5,…ii,Z@WZjUn9;ǽ{53㐔LNXrH].%5 ػ7(WNұ}QF{r1 4Ǻ2ŋ@ـBAϜI[nʕjצF J XF6Pb Uj gg9;SfVTͻ_&d4c|(<^d$%S(wwzY3#iZ J W: x;v&%Rݛz0Cs]K=5.Ќ1Gs]2kJf ZM<uPw rAeU3&'!3OOOKam @IƦN > 1{=1cVDt9qƩ֭cTI:tRSׁSh ib ~~oOϼ;fRRwBfM]hҥCԔ.MS]mF)ǎєWq:#4c<$'S"@DFZizӂu9ii׮4d1̜I4̐W!!tѕ Ќ1-M'-,3@to~:cǀ;b@͚@PsRؾΝ. \ ? ]fh66ԣqc`$jՋftf1Lݻ4.L$ZT>#͛T5U@PfbJR"oyջw4E t̝Kח4LN+8~k/@,Oq 4c-L3g($ˋ.]ׯicǨutJа!u֩@nrwQr++ ݨ0q"ЧRK5u@3L%Q%a(2J$!myQZ~>uP0'rBCDrg lFsrr"u46Q`JFrwf8j ,#!(o*C2ÇSWmۀz4zܿڴ7L11ŋ}+U. 3FJ-^X,MƘ)Xfnߦ ,q`4Mp0㏀ k2 -,z0ԫ'wIX~\Kc)[X.w5@3L@3"#)#iZrD^ mR9s*U-[=ٳi8'!ӤTGP3rarwX.|U,wi)Ȭ5:9Y<3mV,sihy, И&MC~ezNZ$,.p99Q"c9j^.?NcB{K1vq2z8{|oKKvm`h`.}{zF5Ə;?{ lL>SBѣMg.{f\ҷF[Y{S0%3I3mJ%r2:}~φPPVoo`05j>xػZק?۶;]_~Mϟ|B1!TWQ\9KfR1fڔJy"#JrHM4p u޺Z/JM JNܥoҹl1,_N=!ƌ1aXZRݾz5 уz1fڔJŠ3O_>\ɒ@߾ov`>1v,cƘ_0?O]KqM+ݻӌ z1fڔJnadnϟWbŀ^(!͎A\Kcwb%}B/1CKI9˔8,X[Ç+WϞ{m&24c̴g-2M]E`;w(n72iSΞֿ. ˯l9ǎ.imY(~@R-c+%~+&wI)8sxyQth(%ӇzE-'QQ4؅QR:u. ˏ>.u}$wiX~agG?/ntv9LЌ1u>.,Wr*QLGD7SsUE `tj5nGiuMLsſC. ˏ  ~sKeT8f.V;u.p1P4%AڲZv۷Rw ˗#"nh!wIX~I9pv4,rv&MgRktdܥ2@3L46C0cIA%aIP"B pEci]\|e|Hu3D`Ln]ysKи1FWI.8f8ŠiRM_ݷBAIk.k޽)g-֭Ѱe Lȏ\<|Dw?/wd4ct)ir~rmaaAĉ4i+0ou+`߾+9V4ġo_^cGbXXaI!glܹظq]k׮2J%Pܥ`$2||. {Y3JZ?P88Pw–-5ssZj*]8*VcX,ICmVBBhfI]2W5j\TSXzމ4_5} X@Co S ݪPKjYc?Ϙ!A3Xu3~Ԁ1~<]t5܅1f<޾yJˋ~m^M~2A>}7p*1CzX2׬)wi_ժteK`Toׯ.Ap 4c42rTN<|IP-|C]w囹,׾M~䅅*j])ȑr!6 LBncI5%a"2(_(RD0C5sük0f %iޜʕh|&w&N̰Na ?HC3b|KKݻgɈhƘiR*Ey +FRSisrɡ@sgZ_dnʪ]umps~lx8MҬ$` חޟ9}G> cc).22.ќ}- N…lbJ86jpH+Ϟv;VV/kwܺ1-Ќ1ӤTk] f,"#GG࣏. 3)ܾ}{(jn xƟv(wiY~@Y3A3= Ęʉg,#je^l7oI]hCMX&3!)WC߾r18fdD KŒg <}O =t(%!V`i4BR2sgp4uݶ4cG[ *%%k;7j޹v@ݼ]].-3/^Śu. c[.Ap 4c(4hQKŒAd$MU.wI8yuwF)l,]Jӡ-^LJM-\H F0F.Ap 4c(ǻ௿N. 3%@J4Pz P-ϟSw_ /:ДC|Tt4c]3' 4,Ɔ29"w@3LRݷ9ErmKYOTPoի+W[AAԊͭLoRjՀ>4T`m`< #1fz8#'NPU. 3@귽$>>ĉ޽_nQ?֮>"3JSWK3!!4%O1c̴r 4K ԫGcΓ'-sg*wvt6l~Z)ر3isnU244c̴43i@?Ea2TO,K<, 0)ޱ;prZ\w}AiKo> t06…)ITc%!o3Wx*(ʜ=naka4lHmtsgϏ33 h7E]rGh(DDpMk1spi|9MeV,E0SY+`e E&MgRГYr n9ӧԲSnh?fr2_50Y oP'&R>j=ws-ٳ)aܐ!(gLears._fޥi:t$7`F`bjlۖ.,)w).Om,4y~r5,] xy]-Ќ1ӡT|: (>ٷY{(xvt49M]&wR±#ӧ.KkVR>ШܥamB__Kct}HwG3Μɳy11L<8/1q{2+1C=wrfcy8kkY[佒^#%A;w/3j怜ǖo N@Xr4^FrSY+`e ao,&07B]fbbb/^]c1Ƙ\\\pa9m`}\pAb0c1dm34c1c1N"c1chc1cL@3c1czTRh۶`2;|0 m&wQc& 9;Mf.)) AAA(Z(hl#I$IkB̚5 KW 6Ν;ܹ3\]]:͛|N\t -ZѳgO<~X綿 *W ;;;TP/8t7 ooo8::dɒҥ ^ʕ+ѰaCxxx%KD׮]q>͛ Bvs;wvvv(Y$&L>~V}}?PBpttDƍqrsJ:gC8qׇ<==1rH~Zk|uÞ^ѧB`͚5h׮J(GGGTV SNERR{ 3 S/}o:u C A͚5aee,;̬}J3F\R +++qq6%Km۶k}$I 4H6mIĆ 4˗bbDxɓ'zJR,XP/_^,ZHL6M ˅$I"00P={ I̙3:1ǎEѢEȑ#/"L"<<<8z!C>}yիWǫ|zC I֭[=ID׮]ŬYĪUԩSEٲe8|ƶ{$&M1baaa!+Cz)))_8::I&%Kooo,^qr߇>uDN>c*::Zڊ5k+V~Aڊ-[jm13=O/$Ibڴi"$$DWXXXFiYgK^2駟]XP(:uם@'O IܹsEr儿zt\\ÇXߠAQxq^7sL!I8}z]llcǎ2x` Jz݁$I"88X͛7]\{!ųgϲ<'۷o5]zUڊ=zd3gIď?^ ]+++ѳgOUT5jx~~P(Dll{K7n:G WWWѭ[lP>#冖-[ ///^"$Iz]N>{B>uFrrzI$Iu\g:^_)փDbbBC ItNӯ;96cGVVVBL>]H$icDڵ(SXn,Y"$I.]X!$IҸ:ZvmQn]25o\+WN͛7$IbΜ9b…TR^4mTܾ}[I& ///agg':t >}… .]hbŊO?U{n!IػwvQQQB$yW=W899 gggQZ5`,˒___QV,y$ILu1{hƍbS8::N:ϟ1rHQP!(+ԏⅰAAAINNNNNu9<ϹQ !… E*UpuujYEnΝ$ŋq Cz%SUݩOu֝<ڌEGGB pttX_vm@LLzիW͛c޼ypuuE޽5FGG*Ur8wjժUڵkZBCC FѣGѥKa߾}1p@ܹ~qwG2=V13n B|~t 5kfΜ8q"˲dF`Z=y>ӧѧO)R}nƌرcƌwxϟ?ǣGB`С3{===QX1y.]P(0sLԭ[SNŜ9sФI(QgFr0j(;v,cz}uڵk͛7rra IJeCSG32}t7nm۶a/q5L4 9V^3gݻwZDzY? guʕ+1rHTZ ,ISNeY9eUgw}й0^2֝YtNKd&޽{ZZw]˗/رc(^8V^ٳgWHlS$''g{kիprr`HHH3gԉ=z0,_VVVw~3ӧxz[ χ.yqw ۧw"ݻ2e}^^^HNN)SGvIII%U9r$.^*Uhl秮 *}Fz}<<~~~Xl`(Uƌ3gb/EŪU'{=h;u?7'{n 8Py>߼OzSIMMő#G`aa>hժv]UVadv,?~\|?ܳ+S߽{7V7fylcUެYₖ-[juF3 %LugV4ݺ[XBBlllڪWV2TXQ#˝S{lvU u|Y ԩdܹs'JHHvg,SFyqx,X :ի޽{1w\$&&YfyIDATڮO>ׯqֶk֬}tRM6+3Bs޿ 5kք$Iׯzobbbޟ=նꊿK%`ܳgOT ܷo_ԩRT|~%>JO":Ceڴi8x f̘ggg?u!p%Soݙ;Mڌv$11Q}J%+P={?cڟVc: x:׫ʕc٩[`umL2*T@˖-Qxq˲ܿ[+l٢5aÆh޼9k=z>ĤIZ{AM6?DEEZ*^vυ\{nnn띝ޟ9ld\Vۦ?άYpy/^uĉl7}{{{yyNMMŋ/4ٱTeQm13rzuA 0lذ3`ƍ?~} (P5gv\3K1S7'rq ,ZEٳ=Ӳ|(}|T5}99J*ذaׯ[~0aBe14}gϞhӦ /_?u!p?%S>u֝@5jʕ+Xxo޼KܟB@jkɓ([,rtxyyPB:u)sTɸӧWoee6m`ɒ%~: u“Qbb"ڶmk׮a׮]ZKi2Sd|.޽;w=>>>8{֗ɓ' *dz9= <+n޼ wwwL:5SoE3V KKKc%''#&&F13֏9c;wƪUpmnSNj[guBܺuKLŘ1cpvvFb:#3mf,D"EūWʤQu?T=w}?88X4lP,XPڊˋ sOg]>}:#RL>uij}Ǥ=uםٌTg1c1f1c14c1c1c1c1=pc1chc1cL@3c1cz1c1Ќ1c1Ƙ8f1c14c14jժ>}EO<+wQc8c1#f( bee///oqK.B_8 6LgϞU;x *e˖ŀp}=qׯxzzbȑxvB̚5 KW 6;wйsg:t͛75qwwǀ0~?1Xzr1cٛ>^.$$DH$8aee% ŋ3g Iӧbcc;vV٪U&zScep3c&~Ru_XXлwotaaazwA .Cz| ^K8p=z􀣣z}Ϟ=M6ر޽Ð!C4;x`!**Jn˖-Sj֬^WbE4iDc*M6oy1c13Anxxxhv =zիWqi{ TZUW^!>> T;ԪUKc[+++ ::Z.::Tƶk@]Jtv9}~j___<.\xsc18f1Lca֭8q"<<22"Iguxq}Mg`\_~xL l6ݾ}[***ROO=z/)//O޽Sooz{{eXuvvF,\.hժU:w\5$Ig}'hq$6. +&&&A hڵ;vhӦMUaaq\ӝ;wg]t)&Zv][l9III{(11Q1cFujT@n[ ,u8a$sU||}>?hLllw566v]fY! ]z5@˖-ӛ7o"Υ_zqF+VP\\NgPzzzmYF?~YCI2bcccrʐ>!۷o%I˗/x| *''G6MG###r劊sΐ޽{588ׯOƍsyIsVQQn:::6.!!AjiiА~E hZ6ml6t-ZHhߵkNW^ݻA}uwwkΜ9ʚtlnUZZf͛7OCCCھ}{ 6(99Yv]eeeiZuQ9I{WVVtJ/^:j֬YZǏb(77W555TXX#n…ɓ'zn:]vM]]]jmm Z\[[&m۶MP\\{-O8&&B… 4@|>eff)++K7v]UUZ[[v'i;;;[ׯWSSvyUWWӧZz$)--M}}}a7[tiTСCzfϞ2ׇL|:qΞ=+ۭL>|X!ۧ7ojllLyyy:uqd2~qD(@ٳG}}}JHH$>}ZuuuzuNTSWW?~<թk@JCv:9s,Y2~577رcS ʱѳg$I/_Pkkjjj}'%%ippp0+==]CCC*))QccL&T 7 )@ @(4 DP@ h"J|IENDB`APLpy-1.0/aplpy/tests/baseline_images/overlay_shapes.png0000644000077000000240000007767312471065325023436 0ustar tomstaff00000000000000PNG  IHDR&rsBIT|d pHYsaa?i IDATxyxT$@XdD(A6 eQQaHPPтHd~~*{EQLф1V"Uh"J@fL¦$9ysᒓ:zz&*lؘY3mfΰ9{Hx^PPn#\RRa ̒ml3dw#E$D{8mQn֣GE@"`~V⃟AE(Y*P|^0(Qn֣GX/BYYY)<[ ^7iiiJLLt0Q`[2z[(Qn֣"}4rTTt=҆ N(?Լ+RR>&]wt:?H R{IK)i+]H_lN[䏿EkiΜ[IΕN-s.FEرCx-Zgpw(\-G'&UsKYYR* @@E]*_^z1) ?7stԿ3D<(+=󌔘(}tR'RZN(?\}꩓V:^:d( [䯛n:'?[r9 @ܢ`)tXr `=-z[xZrKNNVzzz:6%%%)!!Pؖ `=-z[(Qn֣GXr `=-z[(E9|>|>_8++4r Ҕ`d(Qn֣GXr `=-z[(Qn֣GXr `=-zQN@JNNVzzz:6%%%)!!Pؖ `=-z[(Qn֣GXr `=-z[(Qn֣GX/BYYY)<[ ^W^7tD%GXr `=-z[(Qn֣G);[5KjDREjNZT N@租M^*SF8P0@ N Y&Oo>TjR>-H []~)%Eb+I{K]H?L6Vn#\rrC^W^Dp~Qj۶=Y~@BpIIIJ`kHRTityo,UZؖ KLԯ`gOu뤕+v&p +RfԼ4|԰f RVN'r O͚ڵ'|*4΢^^y礥)119mQn˷rΝ;\r*SZhW_}5lΔ)SԺukU\Y[ ]vym۶M6*]F`0g'|믿^JRjCȑ#yc4c թSGj֬^y<N:7ռys+?@ g5n8U^]%KT֭y~G :T*UR\\:w 6WvmMy=?~uVZ߿u\anv/_^߮'|R#Fs5g 0@͓Qݵf͚ ~n|>9R3fձcGر#OS9Oo(%ڹs5FU/_ܸ\./ƶnj 1Ƽ]va|ͦFá^x\.ƲLtt1bD۷1s=SNaj6oҤIv[֭[g\.INNzꙶm^n\.Y|yh7_~߿ڵkɓ'cС4hPKMM5Ljj rnp+>1z'$I2\W^y$):::4v/_^nx*URvvv?L%&&*...4~w+...l;o!߯x ÇWVV֮]6~z͛7k˖-:th($=2(###4( :44Ok׮ݻ}PժUիWXŊu]w7PnnY3K.jР~mլYSeʔQŊct8p@#G~aAsiǎ8q~߅jѢEFGGk  mٲۛO9s9>ժUS͚5îݰaׯVuٰa7oge˖:zm\8s}vEEE{ոqԬY3-_\O>~L6߾}^zyZjJ,+Yϟ*77W7ҥKC+d}ƌ#I;tA=zGV3+TL?ٳս{wUҥC^vNM6 ;v오~TDSs5: җrIɹ8mɹڷo_W Pll\.^o~رcyFGGs޽&MwyG7n3<+VT|||XXI'ΙrrrVcccsy\>_{yO~\EܮYF;w\.}ת^<[+W,I:ty_M6V>Ӌ}jmçۻwoضjժiժUg')lSF<׷n:lٶ_}sINN><: EkFacUVU-ceeeN:sZJ.ǎ {4nXQQQZ~z?q6nܨ~dmٲE 6 [.Թׇ=TjϞ=ڽ{wصZj>bs,cL֭[RJ~\[\t,W:w}J,Y  *k$=zTG˗/׏?4 )[n}_֑#GԧOXϞ= ƌ1zgUfMmiԨ4hŋ+ -Z$V{@ ŋƎ?n:lw߾}ںu~bŊ؁kGa* (ճgOuESNՁԴiS\Rk֬ŋCl۶mԯ_?]uUr?45iD/SO=m۪C2d4{luM7xch^54j(͜9SjѢV\իWkٲe3:3gmݦoQ}՗_~ hȐ!ꪫB:G&LUn]-]Tv륗^7|ZjI:Yn[nAiͪP.\(c&O7P|mFeUfbbbLf̲e8p 64lř%J  &Ç^ڴkƚ*U#F<:u]1M4ɓ|V\iM%LZc=f~y999f̘1ZjDUV>3ovo6lСCfbŊTRSN>蜧KMM5Ljj꯺@d(.e1Nl俴4%&&*55Xq'(-z[(Qn֣GXr `=-z[(Qn֣GXr `=-z[?>O>/t`CzzCiiiJLLt0Q`[2z[(Qn֣Ӓ#\rrCg>9 "6%%%)!!Pؖ `=-z[(Qn֣GXr `=-z[(Qn֣GXr `=-z[(Qn֣GXr `=-zQN':r0M@zz㴴4%&&:p-`=-zlKpJOOE"6%%%)!!Pؖ `=-z[(Qn֣GXr `=-z[(Qn֣GX//kKQQ-H']~8[/ZZ@ \9iDIi6p+CvԻԮ!.}r<+KUO3r6'Gr ;|O?I?b+I5kJsJ6Hs8r ;|԰TVs79%-PotDsYYX}J? Bsӧܪܽ{P$@)ءQ#iPii˖:zgp_{M7Os:%Pn` O yOխ+=8 (#M$#m*EGK Hn>mw['&Fj~`=-z[xTKNNVzzz:@A||㬬,mKJJRBB13Ҕ`d(Qn֣GXr `=-z[(Qn֣GXr `=-z[(Qn֣GXr `=-z[(Qn֣GXr `(`%''+===tzzLrᒒt (PlKXr ے |>|qVVi "șMKKSbb ے֣GXr `=-z[(Qn֣GXr `=-z[(Qn֣GX/mff:wrʩL2jѢ^}}裏j֭ϟZjJOO3rU|y͟?_~|I1"ϵԜ9s4`͛7OGݻwך5k.= [4rH͘1CWǎcǎ<O<2hΝ&66֌5sOn.2~ۺu+L 0vڅ͹M5ÇCc/q\>eeeh3bĈ۷ovc{LN]}&>>>lޤI6[n [θ\.13m۶=5ƘtrCcM֮]L<cL̠A^jjdRSS/xotK^}geO\YYYZvm7oެ-[hС,XFF4tXLL>]6϶3eddjժիWhbŊ뮻o(77v %L5h@oj֬2eʨbŊzŽ{I&:tJ*85kLV { s=;vhĉ~:_EaEGGk  mٲۛO9s9>ժUS͚5îݰaׯVuٰa7oge˖:zm\8ؾ}tjܸqj֬/_'|R~_SL ͓9sB zeє)StM7ijҤ$dɒ?\5nXK. s޽N3UZUW[J:@qp6ӧk̘1;CO?'TRmڸqjԨ!Iܹի3f_U׮]wߩiӦx裳)&EEܮYF;w\.}ת^<[+W,I:t$zt-•*UgROwjmçۻwo^ӯ=}sz!?5ֺu밹g& IDATz|1ɏ%99t^7/'@ ԩSt?t$pkQfffXժUբE رCYYYSNܩVR%IR&M}֧ٳ'4b4nXQQQZ~z?q6nܨ~dmٲE 6 [.%iaڳgvvm||VZÇtҿ>2Ƅ= jݺu*Uׯ%))I СC/A~Zڷӑ\ޖ\\9u9+&&F}$-Y$47 *%%E*Tе^+I*]w5kwh-['k׮lٲח_~YGQ>}Bc={Ttt.\3gU͚5նmsާQFjР/^`0_h\.WXݻ/^;~RRRԺu박}i֭a?ъ+Bck=z(::?uKǷԟw󮤾ᇃNp.R={T.]4uT8p@M6ʕ+f-^8M2Eչsg9R͛7O+Vԣ>SOm۶С ,͞=[ݺuӍ7WF 5J3gTnnZh+WjZl̙3umoT߾}_j2dм뮻N}ф ~խ[WK.ծ]סC?8 0 ;;ی5TVĘf͚e˖unvjL2ewavzjӮ];kTbFa fԩv&&&4ifʕ&>>ޔ(QԪU<hno:dl*VhJ*e:ud>쳋yT#ɤ oƏ`$dظ\[(.e1k4%&&*55ܞáCԷoef~ cKJtJrI?|΄.Qq-_|[o]w(cޓt|,[J^u׵ݥCg/TAt :VnQdgg+!!Aw'Wk)(|>|>ydeeRgQnQ)33S .O*)ig>r<1E=StlKFӾ}{Oќ93?H:$io;3Vnw Vn PnQlUPAZRn I%M4BR7IzMnwYVn PnQ͜,緒:Y#GEÒVn>M} \(GO[{Tzu͞=[TdIy>|6mےQ,[L4Z.W6l]: (t+V1F{ցB_UTQzGVRE ,Ї~w׿_|QqqqaŊ1Fӧ'Uƍ\.рB}vccbbŽ_~Yӟ4l0uԩ0b\-_&I $Vz8" rޓsU~A?$I6m1_ "rbeƌdy<-\%%=d]veN JhzdQڵϹz{|PGѴi4~x͝;W?yPX-7꣏2$IZ.ְaÜRJI:ի<Y\1F gddW_մi4vXO&MCsJ,[ɳUK~wy<jȐt; (-Z$M8Q}Uttn6=䓚0aSKΝ;rJ 6LG5|puY>$i裏4p@^Z.K꫕/W&MԨQ#':(&XE%'T?G9 (4-ZԠAhܸqZ|nx 3FounM4|p*%%%z˗ŋvZ%''_xըQC?|Bxb3<#Aj=T:u'jĉyq\G ac[/-(@E矵` IH~t,rd=zTHyر: xZ2{%5K 4VnݜJM,1okr[X?Ԗ-_HzZlUXUw:-V>}<~IGTLLӱ8[X_%IZ.hذaN VnaYu[,|N -{n|>$-S0xH?%K: @-z睷d t:"rᒓ:zz&efϞ#>Jw=@UTq:"rᒒt_T~$I+URhc(((.\(%~yS Ǝt,EE/CH%5n\:tp:"r"' jd\wȘ ?rܢyw$QRUB{v8tLӧϒFR-\#t,E+(R֯_5k&igTdI =p*6ܢȘ;i\e$ PTTSuW_]qN`-Q@!IV<ӱXr"a:~M_YN` -w %'?`0Q65nܟ"<-KOO{$˕M7t,a2hڴYroV0%cԸqKr"[86o9B*JHHp:PngF@cݷ')&&X|>|>_8++4r ||_RE2~cVzz㴴4%&&:pPnYUS~y['mIOLr8péD- ͌4P Fnw_TT)c[۷뭷ސ1%=/;W BPnQ(fϞ#P$jժN!x ܁) ~/i=JJt,б- -4\Our5jTD6mKJJRBBcܹ A4nL[Tx$=2déD*VnQ fJVTTI]ãߓB (B!p)"@EQEA衉H,@_E{Og϶yfuUz5l0-X@'иqԣG͞=[;bb^|իkӧL|֋/;S/|}}u7j͚5>馛zH3fБ#GԹsg޽ۮ: ?ғ]vڧCo >\D{1AAA.߆ b1O?Mc=f|||̖-[m;v03999c>sӱcGDDDSNY.\h,/m<6kl0Ƙ뮳׬Y3Ӻuk~&L0>>>fǎֶbf͚em052E>7b,ywmG5ժU3wqM I&ck͐!CoFYxqcwĉb^bdmJ[%Ο?_MCjݺu~snլYSׇ~,5: U\YEw۾}{Y,do֬Y.%.9ܮ\RUn]U\Y5kSO=e233%IAAA6Ͽ[ۢo>+ڽ{|Inߺuնm[WLLy R]vow!s8aaa[}SSSe ǑTڵkNgϞ9` hܹ+WV˖-/J%$$裏>=zKr K}4رc IDATժU+2e5m4Izjկ_z݂+={,hBou% j6}ԩc/%%%)n=x~qܹso޼$iϞ=V*q\hkf)L탲w+O5yd=3[xbp z饗x㍪_{1;h„ ӹsl7!!AצMZjY '00Ν;Wh别ƑK?פIڵᷥ䋋ӶmtIV 9rݦVZ ҹs甐`mϴyfuIOԿ~ی34eysNk֬iמ(sedd(88ئoFF~ˑ)xߠB7Niܿ0frxU^?y+_հaC}ڴi#IlNk9n׬Y.]Xo[,/ ?lwoڵ%IǏ5kL?o߮ǏYf ШQ[Cu. W_9'ɦoQDDD?..ΦC4_ѣGkРAu_۵lRe˖BmӦMpFN+WZXBj۶1vi NiӦWժUj*cԭ[7nѢwٴ?^7oVLLu:{onw*j%ٍs߿n]v}3o|~zUXQQQQE@ UZ?OW]uErzN۪UK.64`Ikf훛$ըQúDȹs4qD*U[nZxW[:sgmݻ5w\k1FWݺu_8͛7Wtt,X`͓bQ߾}m}UNN,X`mTRRlV~:;v(;;{gm;v[naXSdX­]Ւ{]j:v옮*}Zf,X`﯈5mT'OԢE믿O>QŊK4ԩSkVs /G"""k̙R۶mhZti_3sLK_ Qs=ܣ&MXo^u5lPokwq7ԯHIy6..NC ѶmTF ͝;WM4D ӪU+-[>Z`Ξ=ks )O6? 3UVfҥvf̘a6mjLM>}?p^t:u|М>}ڮ_nn>}iР 0-[tX_a>Ӻukh"##SO=eedd܄@sW/®lڏ?n fj֬i*Vh:i&,hFYxE@zH2g}6nh$oօ9[Ř NGXdxb.(\[ꪫ_]233_~Y#FpqElps ,::Z*T(ۀhB6mrae(p EWf͊T6mnZr?233]T "@1bbbuVP֭m۶2#@1Zj ڧe˖X,Ůln[$'$$D 6$ܺ QjU]q^0ʙssQ6Xí1EU!n Z2H[pJllN8={'&&FyfWn *zjժw[pB:uQdX,\TMऒ\T E'9{Q49sƅp NѣGr˖-eю;\XXIƍ ӤII"ܺTn]EDDhݺu\ .FڵkMu1-@qF?>ڹs J >>^ڼys}4i]v)''Džy7-@֭PyoʼJB j۶mFGGKɮDСC+uUpp0օPB:tо}p4iBu!-P$&Mpd"@ AŞwʭYf)%%z;!!A n ^1ȑ#?TzuVn4hÇ%r [(e ŋ%n]p zӧ-Z$c|}}e1ݥy,-!Ch۶mڰa-77׍y6-ݻq3fo߮ey, eWW׮]_nswI[7k,/99%^K.?~ׯx4Vn=ѣ5h wxiӦiڴi.r (Zv&M'NΝ3<e6n.p{M>>JJJyiӦG}~Wծ][4i$&Mr^wjەdV͚5%I5kԩSSOi>|9?k*UhȐ!z裏{[nݪI&iذatY͛7O#FmfNʝ$''|h?{2-eZl֭[+99Y}Qdd$~O?iӦiرvZnsjW_}U͛7СC?p `~1b*qQ(!!A K,Qbb+r K{'ccǎYթSG5ҪU}ԩ9shŊ?-[hѢE qwcviii2ƨq0`z-}駺{uu׹Lp#.77WE}|}}_*kƍ~IY, .O[K9 51F 4(t_gΜѳ>qy"sn.UbEIǭmv|}}~]1Fe;g՘1c4p@M0Aiii>vc-ڶm+Iz'5`W^2eƏ_U{VJg}{裏ȑ#1bt_4{lZJիeXf͚)%%EQQQVZl͛s@bRm۶տ/2d cǎiرzwɓ'=zW^#F(++KIIIǫ^,Xui֬Y *""B< w} \[=z'oVz뭅ޯz-(77צ-..N}ݥ VnPnPnPnPnPnPnPnPYf)%%z;!!A nJÍ=Z rwP8,PnPnPn2.RG+,,BoKTn"&LԱcG4fpw9-^bz7$=Çj. RC c#ǧgeM=zKr -l̙:~xLYY6m+nP׌+7I17o(U[<Ԅ "i\1=1U5idW@Rx;w7^1$ ׿&JzA6k{NNm?~5j䮲h[_5vz\_esnٲXmL|}}\p ɑŒhɖp</3r%[G!%rrrwulIbQxϹ͑-n</9</-n</-y8͚5K))) JHHpcEw[dg⧚=z 2aax Vnp `x Vnp `xp-3n٬</#ιx*-^"++[y_q-n٬</-ns d[+OFKp-n٬</9OFKp-'[RZZN:2\RJjܸ@9-S @(wv"q-n;+/VӦM\kl߾]^Z-yʭM*66eFma>>>X,. Plj\p Qa>>JHHPBB%KƊ\%% mg!%ιe.9vY>>>UPܹs3gzrʊ󕛛kvRTR%=]V:uRŊQF̙3v11c UVz }ZwzamC[n=z*W>L#GԷ~_777W}UTT5sL5h@=͛յkW5o\/ۧ^iiiOmy'sij׮>qX,0`M /|rG]tٳeM2EGܹsm>>jܸ&Olnx ըQC_}BBB$hs=Zbw.Iڿf͚x@/$iСk㏫_~)|SLL kʕ+kڴi5j4i"IڰaRRRG$yjѢƌ5k<.[L֭ӲetmIﯨ(=Zdc|\]PWR+S&.]T!!!ݻFO>;vXΜ9ի[dZti'OjʕJLL[I뮻wy*;;[#GwĈJOO׺ul m۶i>|M9r1Zlmٲeím:t֭[tYj TfM_~6w-+OSѣZb飠b:tHR^}W^ݻOu֭[mS^Խ{w۷OW]usI;B} W)nJ_8qI(ƒr-8-++KM[ڵmB/oV>`i3gj…2en}j֬isrÝ333edd(88ئoFF~ˑ)xߠB7Niv>q7oh-X@yb}*''G ,eff*))Iqqq6+Ҏ; wÇ޳;vLu-߿$O6XxRɖV [%ԫW/o;TUVjٲM:Uku=(==]/z믿/""B?fΜ,mV|V^K5:3gT^tk5gi{9IDATs=jҤ_կ_??^GQÆ oh޽vWa7n|Mú8 2D۶mS54w\c4i$psnTmnnRRRԦM5na={ɓX,mY,=% [ʕ+5vX=裪\ ӧ}gUZ5+z%K\nI&MzHk֓O>zʮo'ꭷժU+}ԩ^}||駟/sΩ}z7 }N=o({<]dѾ}ӹsgC{KCǎmӶ0EƍӸq.j޽{w Ќ34cƌ"%%%٭Jy~zW/Nx:>sn?ƍk׮]:uǮTG vp <[2GP^;,ߟ_%[.(L[+OG \r9OC \X* !8xVnp (rkr <[ᖕ[g">,snp `xVnp (r+r <[+OG r t[+OG r t[+OG \5[!^+OD Zp <[@vv,@aOO6@NN,?#-xd%''[o!ιeABB,YD7V _+ßm=ܬYb}_qsnaqFA ]-OR$r 8 ;,WRݲr 4[@$HbyxÒYx.-^[#Xx:-^[#Xx:~(o\ƛe#;;[ưr \[NJ$In()Vnlʝƍk׮]:uꔻKqJ*q.<@DJ&''Gưr \\P /9OG }-x[Kx.-^ [Kx.-^ ++[|nyܲr \[+OG dgr l[+OG p-ny%r \[@vvXx2l ;,yXx~:uѣ*'""(88%P8,/0ol:OG9,qrp (rp (rpfRJJvBBX>­=z 2LqX2#=Kr*99YEIOOwQ5Erʙ .YD.}8,PnPnPnPnPnPnQf]\8 \< nQfy^sH'pk4 G{[@G{[/)b?`˟ɕ#Ow>~m=rz.Z˯ #z)OzJ(1\ J?;no<\NH_=YG{[@P֬Y===]K,)ӱ1S>r>zz/:˫G|jժuENS֕1#}.ڵkթS'UXQaaa5jΜ9cׯ$GGwɵ_31zիW/EFF*$$D-[ԩSyc3gx֜Q֘cN*/}kÆ 9rڴi#\zbLN8p7cƌ17f>7rS7n8cX̽k.\hnfcXomԩSq&44̜9Ӽ&22ԫW梁35k476̴iLMLL9Mbg.\h.cXs=X~&<<܌5ʼkfʔ)&44ԄoȑfȐ!^0IIIfĉ~;w[jX,w-vɓ'3c h"3uTӰaCg+~X,k׮f…桇2fĈ\6''Ǜ3yd3gӼysSreV8%ym]˙9$KjM6m+b&L`MϞ=:y,3nk83g:uX,oMf.\hnk:c91'yǜT^8O?m*T`ڵkg4ib|||>sg; n~zcX̬YmQF&>>lMOO7ikLzLNN3lܸڶcgxK-;#F0+V4\X,`kٳgM5511фǏ9k׮5YYY6miii&00$&&yM6bz)k[I&[G>l]weެY3Ӻukf„ رr$mJJݾ=zTVqŎUU#={s)k… b1_|񅵭$GGwc\eř9fݺuv|dddcbqx̝$ܺ?nm&dc>}X,&==woLvL``+͛ois9sbonӞl,_۵kgjza5jdgcX?o^~eӠAlwnkrrrɓMDD 2}1c֮] 0n&MnݺYo'b˗[nX,fDzƚJ*ʕ+-[^zZ kڶm[dcǎbY'wyL2DDD@ӵkW{b5+W6COby=pX,fʔ)ֶ~X,sN`Tbjժe&L`1~3rT 5/cׯ ۇ{TXؿz;ږdO {9]}՚:uyuU9s5jѣGo)v,Gmjjbccn׮Ξ=]v9_dmIկ_?͛7O=cǎBk) %>}z'5tP{1bN޽['Omݦ$=smݺUvc+&&b~T9W_ըQԢE KDÇ~$Y,93G?J&''o,)ݻh"M8ZOacj6;y}/ O>Q-Rؗ挂f̘*UgϞvۘ3k(kI='ΝEa,s'+nt9صZk޼5JyoҤɜ}:;$o^tw\q}:_Ν; *˿5],Zj:}".Ύ;t+>>^?Z|f͚ ]JOO7dD+I:u$/v}_u};wWoX6l}||ԦMY, :^JMg_ی7 *ǩV[*μ/|]wY<}6ڷo})77qyJ29RV}iƍE}9(n7m4}zgUre5g5$ g0w߹pFAAA!##ú=_dd]U6ߢ/Ύh*UHի=d]tš.T?rHEEEgϞW?:.t!tMV-[pkU=#믿֑#G4yd~ժU$A8u]w֭[ *}ի۴W\k[ŅJ~+oqf̘QW_I&fs,>Ϲ:q6V~-}<AI^c*$$D۷WTTx:Š93gHRJJ&NaÆ{u؇9û挲ƜsRy ;wn(,,fA?ǫ^UE_\] 6V5&)''{Ο??î תUK7oG}^ziժUٳ\d-N8={ɓZ}Jhvۜy8RjU]wu6Ykv4~aw{׶$ -O(<<\3gTgZKip(g,_Y}/|<AI?::Z;woN:wUN3Y+9;gXBwunf͟?c91'yߜT^\[Ea,s'֍Zn]vԩS6?ٳg}"G-[w}gׯWÆ Ub]ժUX6l.qF|o֜9s?{՛oД222t-h.bQs:^ 3y8p__QJ￷~zUXQQQQS׶#Fמ={TF M:]$N.U-g7yfϳ38;?d`_-޽{uM7iԩە3֯_[oU۷;S/<9e9ɻR~* ̝; nԷo_hֶL%%%)..һwokܹ6cϟu*>>fN6mܹSVR~.a~m!/WץKU^z}ybŊ馛m'OԎ;{+s5VCp1JabT5J@(Q"QQٝaЃ=QPOF!RA)@ !{u߯νw;r̕߿7Ra!Y333b۶mb``@DQ:VÔ/f'3 42)fg!0;촽iDss3\.$^u֡7-ڵkRӧׯ_/^ę3gxbԘк|2hjjB:Ν;!Dggq ["ɓDZh"D"xv!H)Ѐ~ >|ǃݻwܹs}`۷?~zlҥؾ};:i (ذat]QTT6CkLP҂RScVZe(;333Xv-^/N:K.axxt;vV?RJtwws,y XgϠ*"\cǎ6m2={>D"H$8}4z{{q-[l NfLNN" ]]]:=ʵ̘Qh̤INa';`ttT T h4j*2cv:?;} (++F{N(\qʾ}NB!n97 i^/022bW[EQ,?E1} =x<|رc-+N npႩN__E1L!-#v# a޽l3+CQH)M/EQ|dèi(..Ɗ+~{\*l;::f|>\.A̹;wP[[ UUl28q_~5耢(mmm86[{ٳ~uuuqs6;ck?F2իpB===jv27sÇ~z 6=Ώ_$nu]G,ѣG155eNfd\vڕk13ИI#Nv.Z34)<MDDDDDD3DDDDDDx\qqKDDDDDD-9DDDDDDx\qqKDDDDDD-9DDDDDDx\qqKDDDDDD-98*]nIENDB`APLpy-1.0/aplpy/tests/baseline_images/recenter.png0000644000077000000240000007334412471065325022210 0ustar tomstaff00000000000000PNG  IHDR1O/sBIT|d pHYsaa?i IDATx{TUu!bqBQ Yj^Pint2Af+PKFr3*kR :h}+-ׯxc#Y:* "}9r |>bgޜΚ9/b!LʣblF`j[lF`j[lv۵kjܸq BBBÇkܹԩղeK 4H[lqݿzꥆ 'PyyHyxx}Nufͪnj;ӥUV)""B>>>jӦf͚2=jժ|}}ճgOYĉJJJRfﯨ(+55:;5*@}y'u ƪ]vpB}U-ƍ￯x@&Mo;ЦMԽ{wIRyybbb͛7O8q /8ji{UhhӘa>}kVX,N۟~F(-\Pyyy3g=t1ch4iڵkŋkРAꫯԧOj_r }8ͻ;[nN=8p0~aѣGwyXXXѵkWG/aO?a;ƶnjX,#--1V\\lmݻw=eeeX|c67nlNFjjacǎu]"e_~jҤcӟ͛;6kLuI&pԜ>}e0TVVVl6JYa޽ڷoH҄ dcTRRc[_e:tڞղeKs=5J}JKK+mk6V:uc2e,Y{OP^^ƌ&M8:z7wiƌׯ:zgѹsj/33S7x1/nݺ9uu SmťV&''G.ݻwo;YUPW=?R9?ӟxqo֦M… 5fsZtӱڶm[nQQQ-[9shzmϞ=ڵk|I}/v.,,{P˖-UPPT[Y]),,Tddd;u$Ij "+55Uqqq.Am5kƏhjܹ>|6lؠM:jVK.뜎[o9m֛oI&nLG}MΜ9#K/ԠAKϜ9SeǪJqqEͯLnnSyX:uTX͝=ȑ#եKYPPzJ>: N:i޼y;wӜ˙k7|Sk׮4{Or:w\|||$5>*..v쯨cU̿Pnnz]@ QFZn]l7mڤ(~I7x$Vƍzj9~*--հaÜ۶m;jc֭%IkÁ\tU*..,,TPPӾBөˍ+.gk*[b_hϞ=:syj]iiiJII$Ǻ׫5Ykd=z_̿&]knڴISY{ qqcڵN?ņaƱcnj:-[4Jb1>3};v4zu=ڵ˰X,ܹs+?~x8x[۽{aXtCŘ3gclʔ)qI{ΰX,nvXe˖Fyyxbbo={֭+dddZͫСC/ٱcM3g5Ykd=zW"p rVQQ B^Z!!!ίxfs߱c߯]˩S\.5 Cs̑b]w2T˖-S~gvkҩS'uA-Ryyc^bQLLc,&&FeeeZhcD/VϞ=>|XNOpё#Gb رcǴl2 :T^^^n u*ڶmƍ={8=aÆ>|$)$$Dw.]'Oj*,,+"___=n}vYV љ3gjz+=g_uQ7o hizW;zXM6MGUHH.]C>|X oJdffObcck #JN<5k(!!j%>jٻwۧ$G &0 eggW;.m=;v5j$EEEiNz)}gZp~'GթSTkXsrrfoi]vܹs֭Së'u$ T֭[e uťWbbb4h hϞ=~i $=Ckĉ~?vZvm|ڵk3f߯x>>>*))q/..v?ߪjk"--MYYYXVY:~Y>33S n`{hݺΞ="}w_4l0ƍO>ڴiS t BťíZrԝ?~amE]uTٳg󫓒:pQW~A>>>w|1֩S'uA-Ryyc^bQLL;/ \.E^zyڻw-Z$͝;WI&iܹ߿FSN)==]%%%6mZkĨgϞ;vݫM*==]a(55թS޽uw(11Qv]/.GЖ~aaa=z/^7o hizW_W*+lȑ:vϟG}T˖-SLL 9ɓ'^P_o߾ծիW+..N ,ԩSռys}j׮Sm׮]f'[ozHٕs믿^?վ}{^+.E'aԩS:w˾-[Lwx_ղeKQF飏>Rii$ɓZfV|A>v{j߾}JJJrZI0a PvvvR ֓cǪQFQTToWRR"IqScqⴝ5w~M$ڵKΝSnݜ꼼^=#e~``Zn*g+-X@VҜ9sk.+.ݸq 6H: VYYnvXaa]֮+((pԝ?~-[:Rcƌq{l^zW^!C(&&F]tѴi駟*""Bv^|E)22R#<"///9s52ޠAIr̯ښ֩iӧdd٪n`{ õb !Ţ˗+..Nƍ$]wuJIIѺuWq9Z__שnRFZZZZu^@p|ff:*Ѻuk={VEEEWVa}:|ڵk͛UV X^F\qpVu_X[QW:AAA.{Yꤤ(>>\;*?gs͛k޽:| Põc4uV9qΝm۶9՝={V qI. tС@~g;wjժUr^yyN*????5bbbtX1v1-[LC$QF0-s=j IDAT`222}wUTTXعs生Ç;:u:hѢE*//wkX,O z8 h"kܹd[oUii{=}7ZtZn]111ٳƎ{iӦJOOaJMMu}ԻwoqJLLn/)hviZxc|޼y6lݻwW_Ubbp9q 9r;Gղeo)FDDh֭:ufΜ믿^k׮uS^ZqqqZ`N͛/Tvjv5kGOQ56ـ3S#L` 05-S#L` 05-S#L` 05-S#L` 05-<l6l6cn=`RZZVUV;p9]?33S n%⪔n p-S#L` 05-S#L` 05-S#L` 05-S#L` 05-S#L` 05-<l6l6cn=`RZZVUV;p9]?33S n%⪔n p-S#L` 05-S#L` 05-S#L` 05-S#L` 05-S#L` 05-<l6l6cn=`RZZVUV;p9]?33S nͭs=}u1Y,(44T 6!wLV~ҥKGi*//wΝ;k=znK,r+ٳG<>C5nX7߬ƍ0 ?~\?o߮ j9rfϞwQnp D'O6JR5e}'5q;؞={V 6taÆ*))sS`ۧO-X@v:ݮ۷E7@MܾvաC 2DݺuS``URRBm۶M|}}/_ξT`۱cG4kLGպuK(q;JR֭t:tH:s|||'U=_PPAPlvvܩVZ)<< jl7mڤSj˖-US/OE\n5khРAjӦyu]jРUXX[j߿>c 8r駟V_eXXﯔEEEi̙[ena^^Ǝ[i=F;w^ts37p[j{5nܸM-6M6ͱmݞv}4|hBIIIw9u-Zw Biiirl[VYztgL%$$5`,YrzJnۣGK.nvYGsS`۱cGl6W[W^^,u&nc;k,1BݺuSRRu@y{{Dڶm-Zɓ5a„*:tUViASﱽuwk޽Qaa{lN:]^pQ`[!,,LaaajGp5U-..ֿ/M2E/J֬YK q;UDDiӦcǎ6mW>|X֭Խ^Rk׮ոq*???(11Qv---Ujjnf5h@!!!zTVVR~K 6OZj%___Sk֬Љ'f͚_QQQq Vjj$)22Rcǎu{ vtR=z5doK'uW^}ݧ>@]vՑ#GjjZ`nv͜9劉QPPΝ+Vh…N5߿5|=CZhbcc]z>}z)u]ZpnFrX,N۟~F&Mh…1b̙{e1c4|=Z`: 4H6mu,//e4qDK:z"##w߹XE1Ծ}{#99e<33hРѣGرcaƻkX,w]/6l2~zbO?c믿6,ͩvɆ7?eeeag}fiwmNr[b1>sǘn7{iaѣ;ө.,,ڵSO?mxxx[HKKsm۶5z](++˰X,c?Ѹqcw 6RSS 0;;v222 IFFFFkM6pեK_߯}0v_>}uׯ4i|؆ $IwS}'0ΞI&em֬N>IYF w?|裏t9HX6ԅEU~iݺu:qݻw_ӧOԩS pH|||j+wС<7xC}f̘~9ڵKΝSnݜpRsrr:8v]4Wފ\N``Zn47''GNAu*ݻ~g+Ƕ{Zrf͚U[oU7nTtt^z%S*--U\\c"XnܸQmڴqW=.\1cƨT;wҥK %0/ԲeKmܸѩE.us c/viVZW:),,Tddd;u$Ij r;uWZM6/.YW땚86h iF'O"""uV=ԙ3gcZ5p@ǥ]wc_E 4p:֙3g;Xi/u߿bl6ljkvs;s=NQVe˖5^z%_~qk޼ӽ9rt颷z˩[|F{W^Ҝ9s\.~˙+T\\qyT[\\\iǪLM?ǧʺֹ󫒖VӟgZeZt|ǝ򙙙JHHpxn[ڴi~'x㍒~(Vtt7nիWaaaڽ{ۧǏ+,,LJNN2ܪT\[q ժU+ʾ bխ2gϞN]n:b~URRR_-n_Vt)n}ך5k*]_R}Wz;:~_z ЀsԶm?{rssܵkW_Ҿ}ݺuJžm۶9=@@r۵kW[NNRÆ kN 60 n*???V;.OEݟƍ[;vܹs]nEEE9x{{H RaaV^y͜9SZե5Ҁtᆱ":Ɔ.///; 믿֭[wUөS'uA-Ryyc^bq 111*++ӢEc%%%Zxzt׹s9rD+Vp;vL˖-СCugyV6 CNҶm4}t /P/xm۶MƍӞ={gǾ jQF)((H;vɓ'o맟~'|RyԻwoqJLLn/Kю =7oJKKխ[7\R7n{WS͛aÆ)::Zqqqڽ{^}U%&&}=z(66VӦMѣGK.O[~ꩧ;8]={jرڻw6mtZ6P'`]lxxx禛nr}饗;>>>F&M#F;w7n4c-Z0{1.u /`-b{nrJk׮F oxgsιSL1 v矻ԍ30NǏ7z!# 3Ncny CQj ,q^xz!]`rO>Q56{lm6[-//fرc ­`Pkosϩ]vz/Iã~TZZMnI馛ԸqcǏ~7|* @'NԤI.l5c M:UV\͛7kŊK-BBBÇU/+$///9R#G$;wN$I&hD===ռyK ãZlF`j[)/˫r^^klx %%%U|Ӿ+W#7|S5S3g<ݻ7ސ$}g1c:w\/^2"eɑQ9W[RsjE8T8De]|Bv(-%kȆwڏk¼n~ޯ^{?w﫠@cǎ8l4i>LժUӘ1c$ISNդI?Q~4hpQ @q.attV^#GSAAnըQbR]p-TjU]PnZpVZݻw+''GjԨuD]s5bx)Wݱcz~AOںu/^ &wUӦM/ɀ(G8qB={5qD۷Oǎұcǔ &hK[.󕕕+V駟Vڵ=שSG#FЊ+w^͟?l{=~رc:u.]hŊyp`cǎ2CmN:i<(|s=rj֬S뮻NGA+m^^}ӧO_UgϞ=ںuk{\lGѣG_RNSNӽr|ns}5(V@QӧOWZZ{ppT\J󧦦*))ɧ>~{`JIIQbbbE(D[lFX`i[lFX`i[lFX`i[lFX`i[lFX`i~=NN{rܖ`+ӕ^v8r88"JJJ-W%&&V0X,` 4-K#,` 4-K#,` 4-K#,` 4-K#,` 4-K#,ͯt/\.lqE>}CGR:?55UIII>%⊔Ċ [lFX`i[lFX`i[lFX`i[lFXiUԺuk^ڣΦM4h tM^[w^v}ǎSrrWuI۶m+vׯW۶mH :T'Oy}]lR3(??ߣ΂ n:veee`[맙3gЬYTR%u]}ʕ+K*URÆ e|~AAz!ө!Chʔ):x:tΣnFF:w\͜9S<͛x IDATzիW/UZU/zg}Vy[U66mRZZM'xB(&&FÆ sAiĈ У>]vGzz6lؠt{rEEEiرJMMu9r"""n:HׯhժUK'Tll>'aaa8qƍ,==]~~~JNNvڰa%I5jP@@QfMwjժ)!!A˗/י3g$IǏիԷo_hťsN}JNNTzРA2(==A̶mۦ( )IZt:2m۶e˖^Zj_;tY|[=E>22Ruڗ,ϥPevEFFz߿\۫_ծ]rqj֬YXӾ_~JuAW^_pevԩb/1\{oS-nYc)'7pW)YjY``sss/W-nPPPӕVj!q}˧*))ɧl/b/-VZK ϯ[X]W֭[_JJ/=^f-ZЮ]QqFIRllo#66V[n1ƫ`EEEIbbb͛7{;}222K߿_e_,ˬw׼yeyyy?ZnO?iҥCw-IRxxt颅 zoɓw={VejҤ͛w9sdԻw߼/P.E#Fjذ^ueeeiz?|MI_|!I0a1_~ך[[C=;w*""Bgϖ1Fƍ;aiF۷׀r4c uM]vus\jڴ|AqN:U={T׮]էO}z5`5nfPmx74zh:z7o+Vm۶:{ј1c$WͦѣGK:u9[ݮ+WꩧҬYt)7Ѝ7QEZz'xBaaazG4iҤb}CK.ոq4dըQCFr.59FL>l…<< 'p-K#,` 4-K#,` 4-K#,` 4-K#,` 4NSNӽr|nKiJKKs/;9 KsT%%%Ԗ`+RJJ+z,{lFX`i[lFX`i[lFX`i[lFX`i[lFX`i[lWtt].m "M>]iiie!Q#p)Ο$lqEJIIQbbbEp-K#,` 4-K#,` 4-K#,` 4-K#,` 4-K#,` 4-*z8N9N-Wӧ+--ͽp8p8*pD.SSSS[-H)))JLLaX`i[lFX`i[lFX`i[lFX`i[lFX`i[lFX_E)^v\>%4}tGt9jj|jK)%%E= =K#,` 4-K#,` 4-K#,` 4-K#,` 4mUV-uZz{1F ,PϞ=U^=Yf0alXX&NCqB2KOOe߿6lؠl{Bz$effG͚5ݡVU-_\gΜ$?~\WVRR;JR߾}ŋΝ;_+99j%iРA2(==p1l/m۶)**#HJRV$4$?s!(G˖-ڷjJv%IڱcΞ=o٣bccK'h?GFFN:^Rt.2;p"## _b)S(<<>+??_ڵ+w(/f͚~Tzꕺ}^fNR@@Wyʕ3qDYFsQXXX}GK[X ĉpur:r:q\>o`{z}et.?_ZZFGyD_/Z-nPPP־}ӕVj!q}˧*))ɧl/b/-VZVR߾}u]wiܹK ϯ{X맰nڵ,_(11zpe֢E ڵK9997n$zV\\/^j֭2x(IRLLyfzOVFFXJGW+;;p1l/޽{+??_sijݺϯZ=z 7ܠ+V{ki}OZtСCzuw_.]h…þ:yegϞUff̒&Mh޼y*((pϙ3G6M{.+K/8kĈ:x6l_]YYY?$)''Gݺuӱc4l0FFJ̷wjݺz!ܹS={17nG &M6j߾  ˥3f[nڵRӦM)ISNUϞ=յkWG_~^~e 0@7/`[x =Zo=͛kŊj۶$r\lz駽ׯ`k۵rJ=S5kN:8=hBWO(,,L<&MT=zhҥ7n 5jhԨQ3fLy_ 6s@*|… yxp+O6[lFX`i[lFX`i[lFX`i[lFX`i~=NN{rܖ`+ӕ^v8r88"JJJ-W%&&V0X,` 4-K#,` 4-K#,` 4-K#,` 4-K#,` 4-K#,ͯt/\.lqE>}CGR:?55UIII>%⊔Ċ [lFX`i[lFX`i[lFX`i[lFX`i[lFX`i[Up:r:es[-HӧOWZZ{ppT\J󧦦*))ɧ[\RRRX`c 4-K#,` 4-K#,` 4-K#,` 4-K#,` 4-K#,` 4NSNӽr|nKiJKKs/;9 KsT%%%Ԗ`+RJJ+z,{lFX`i[lFX`i[lFX`i[l\YC0gP^sŜ  "@y1gP^sŜ ,` 4-*\'*~="9sZjx=}x_ElqUAC{h.r=~l[U|gu{.KuWR'Y'Y'Y+kÆ 9rڌ1%:_~`ZnbccKG'##C_}UE@.3J[(lFXEn:v-]9yƎ;CUVn믿^lݯZwqBCC}СC>~zmVСCuIzM2E 4P``7oE]>UV-uZzG~n{̕e„ j֬׺͙3G RjԹsgm߾r _yz|pSϦM8T<_#Jz?vvzQrnk^ׯ_l(}U~N<٫cǔի+$$D:uҶm|+;;[ RիW/ٳغZl@]zg{Wp?~zjݺul^\.ڵk*UhҤIѴiӴcmڴIΝ;+::Z3gԾ}4m4}ZrGݑ#GjJNNVVl2lӧE\~iɒ%zu7j޽֮]?Oz׿6<<ܧ>3W˥'*88c?zK> 'N(##C?Ez|DɆVZy5lcLp\.oѾ]vnݺ&??kƍfӧrssMFL6me> ~3W>}.]:uiiife˖]q\y<7mڵf%KZL<87vdgg|}:uꔩ_ygfz17w}-J E6UT1'Oll6/eό9ңnӦMM-=1FgΜ)%I}[4aM6M;wVz4uT5jH)))O/X@!!! RtttڶmZlUުU+ڵkW+((K<޽p._722Ru8oܹs9s'TPP233Kq-p9psEFFȑ#:sL־f͚O=^wu#It{Uzz_: K%QZ4|plRz5{lպuTRR(͢3ܹs?w1F-M0 _0 G?,өݻkŊΝ\}wzѥ%u{)&&Fiiis[ݻwWjW_iڴi۴~zJ8ce\z]]|ry睚1cڵk{GouM7);;[wq^O!.Gn&9Rsϛ >Nznq\:??p)2p)Ŝ絮ޑ~y>((ȣnqO OW_Ujtwhٿ{$<qvY>$LEjժE^K6mRAAGzy,uz/e8|p(..NQQQzG~Rǁiذ]{99\,Y/CUT$=zTtY 2D}Ǵ<q={V}QrO0JG}Tǎ>-ϱE2eKխ[WrƍWlL_ /e)(,j_'ɣiӴ}v9RNҐ!C"\|^Y{t“#GGmso+hr\ڻwݫ\>}Z?=4kԨQZJ<sEq|9>6iD|-Zmjɒ%j۶yRǂSN>]=g|ku]wXo]v)99}|ڻwse^QJq}Rqc򥯢!>ZJEۗg׋/Zjiԩ2ъUvڪ^6onӦM{J#???OVFFG-Z__QwƍWLLF>H|򉲳5wrʧEڵkrrr)!!AԣGM0(@zqv]Vwo第,9sFӟ>|c?AZ?UWv*Ig,jѢE2Ɣz/%I=zвe'ܧϜ9{NرO}Ug9tWYNNyU^޽{맟~ҥK=ڿۺ=VVד{͛7{\7hڵ7::ZM4ѼylL0&%%T\s=W;=zg}̛7 cرcMѣͼy̸q_o*Udz-[o5yMtt 7vfyF뮻L:̜9ԭ[ԩS:tȣ+n7;w63C 1*U2_u=jM~̌3̼yLBBlf̙%F~gׯol6ln~]﫯2ݺu3jժ0ڞf3;v*O͟'h:3x`s zfҤI~& 4k={LMFL``0;w6~ExEP\SOHSres->رc07x 6+W6͚53=9{3ѡCӬY3{&<<.]/£ڵkn/6v`3uy1E~e|9>Λ7ϴoTVT\xf&''m^f͚enaMڵM߾}ݻu8\9|=46<%no [l1={4u1&44ԴkΤ{/ӯ_?UcUL׮]MddkL*Uwa֮][x=jySZ5l:vu,,n{\.oMhhٳǿ-[fZha*Wlիgƌofذa&66քӢE 3wb{`3Ƈ;Bq-K#,` 4-K#,` 4-K#,` 4-K#,` 4-K#,` 4-K#,` 4-K#,` 4-K#,` .hҤI= _C[" ,nwv۷Jl7{lvnݺ}:N\.=^غul͚5z`5lP Џ?Xvׯ_m*88X:tNz'NPǎQFO3gT핑UoӬYvZupp՘?lf˖-O?l&55իol65k 2ƍ󹿭[f>2'x㏍f3=NSvm.{Wf3|r3x`ڵ3u5ɓ'f wYff3#G[fL߾}z "mVo>u T~ԫW/e˖) @ڵy EvmZ233eǏիwy߾}ŋ˖/_gjРA8p\.6l.KOOW\\n&wYƍչsgm\z[{Jj֬.55U={Thh/iׯWLL*UtA:qrrrTZ5wَ;tY|um۶˶mۦ5iģnV$Y:۷{mݻmٲ; pl ;vLҒ%K4n8լYS{G-[oQRRsR֨QOm333ՠA ?3gx {IRddW5kjuޛ[ma#GnnI_]}\|[B]tQ5T^=ǫnݺO𔚚ի;$O>Zh ȑ#R?ָqԧOu]~)IR@@Wʕ+-^m͢u סC|'A*4{l^Z޽222i&:Zh:v쨽{w}6m觟~Қ5k|Seff?W_X(IjT@[f: fSK*-[JzꥶmjРA֭km>CZxqOJMM^j?:zhƶo>uUUTʕ+챾K:pjժQwݺu֓[jU͢u WOlvM4IywyjjjԨtáwyOCjҤ{rauUgΜ{lLLyfӧO+##C-Z_vƍkլY3mmذW޳g$4@۷W\\fϞ\:uJK.w߭{GUNN}R{뭷/ԙ3gɓ'ս{w8p@+WTÆ .]h…:q℻7ɓ'.{ٳgˌ1;wԩ6mڸ{͛7k˖-oFk׮f-[kUӦM7ť@SO)>>^׿TZ58qB={,-ܢի+55U %n{nݺ2/[NLL͛뫯Ps= &M6j߾  ˥3f[nڵ^ڵciԩ:snf-[L~z-c W^yE=zГO>)???͘1C5kTJJxWZᄏ}\6s!Ot`_7ovc[(cԴiSYFv?|?z-8p'ƪUVzWeWm65o\ԠAeee{]giںu”I&y]2lɓSPTTF!Ovvq}*((Pǎ5sLWtF;v,q_{w0Fa#!A!aGh4 Et_GpW`IysVp׼?:lr3L$.UUv?}*9)č-бjlrd<g>gGI]}O m&IcNSCuO<ϾgSd,^,lیF'[V l(h [&l(hi]^.qIENDB`APLpy-1.0/aplpy/tests/baseline_images/test_rgb.png0000644000077000000240000002316212471065347022207 0ustar tomstaff00000000000000PNG  IHDRL-cYsBIT|d pHYsaa?i IDATx{TTg]PTbxLIE%oRr2M, K;9(D/S*jV"K 2OyA~~q;32J}]Ɠ>DDDDFr^|nƉ_\ߪkg9g4RWroUTs3~_d~Q&Qc`Ib`Ib`C0 yyZ抁I u¨.2Ii+SKBBB0shה_ˀ"C$** fco$"Cr""I$z׷R?t(e`Qrwܺ%+H<$'"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"dbꋐ:;5sJ>VED%QQQ0 ֆ.$z׷R?t(e`Qr8zO[.H;0󑔔,(o5wH""VLMM͛sΡDcJ3<#Gbpwwb I*0ϟ?~-7n~apwwAjj*N>5k`ɒ% Ē%KQADTK.6l8Dwbbҥ =zT-T`&&&Vj/wqDDDNRwQDDƤJF޽{(**zV󃣣#aggsss8::bРA/DT(++ ~~~HHH@1d8;;>DFF~WL<k׮šCX*?4>| (ݑ#G9s`R$1>$?pya  9s*GDdLѣGҷ=6jԈ^SG:0{իW#==]gt^}rqDDD;UV:tѣG8;; +Vɺjt`v ݻǏǂ мyj-*kEXv-֮]7n ##<RSu޿€$zELOOGBBnܸtqq\]]F""Pܹs/E?˗wW%X :-[Dxx8nID$RYXX -[2׷Z CRz /\\\`ccnݺa/qơ}PT:ɓDƍlٲ*o'7C\ pzYXX̙>*_{-V… 1qDe|~-.\KO>6>l֬Yacǎ!''{ƹs窭DEE)**ªU={V[ -Z~QFsΰӫm|"2>ҁٳgOٳ]t'`jj˗WKqٺu+1p N6 IIIXfƉoիHHHѣG+|f;m t?~6mڴA\\]QQnݺ@8y$BBB* O?裏4ΦW'WWWAAA *&..(B@QM&22`̘1:ڴi}]L:׫_Gc'S~9g/쫥25xyy!66VcYf4o۶ mڴʘL8Fš5kjʊBW;蛣?ce-UU4t TOj>YmKm 5hhРt[[[w$$$… XhQm?W_}Ö-[mW\\dj2QY̟tERm۶ ܹs+l٢pիJBPP5?P|W6??!233q}^|ETi?ѽls"Nn<|+xB~ KJ/k۷{h׮]mRSSe^h"4hmѣGxbL L#d&O~\NqJxҁY'@7_uSQ\vMg}J?޷e˖e]hC~"j|}}wURm6!*=۶mÄ T`Κ5 ۷ˑZa+W`ҥh۶-fϞ]"w)))Xjahժu֭[B 55;]{{{̘1IDO {gya߾}A\\vޭ>TWmڴA߾}1baÆ5Z<Qmԝ>&&& TǏ7iҤR5U5ApttZID$ID$ID$ID$ID$IFbbbϞ=""2Fzٳ1iҤrO<sѻ(""cW`=zzQ]W`fff}E#gΜ)wӧy=u ̑#G/޽{`ƍ͉zK 66ҥ :w 8wxxx?B M=L[[[7 t`nܸW^ŁUc+ϟ}!556mZ J:08???Og;___ 4jmDDFE:0Ϟ=[aXٳg(Ifvv65k& zEDd 6jkbbG]1eE'+lwADDƪR !(E##'_& "2zU~.Q}Q_+"9!!!z7*7CMf-BW;ХxHND${DT?ѽy$z%d&OfK!9$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$&$CP_ ~prߨ {7"``֒(tuc ] 鉇DDIDJݫNJ8 L"WOf"d}qJt_l).0aꌧq[g1j8W L""I L""I L""I L#R/:qSϭw\[uq{\UiDrP'n oUk L""I L""Iӧg7Q_QϨ}G/xץ~2Y˾F w.: qSL*|Hc_e;eʔ .+BQ/^ի CCDOwΞ=3gC:20$$&$&$;<ѦMXYY}5M0*JթS'1o߾ & ޽;vܩW}PTѼysСCz},c ;;v s믿.}}/bܹڵ+lllÇe͹;^iҐPصk7T577Ǘ_~'j޽{#33f͂["$$D̛7e0{l,\wEpp0ЬYJ΂c,ܸqGpaҥK裏m\EDD૯/iӦ!776l?WݶJKM^HկDxyy PѨQ ._\T*?h-\\\DQQtbرe~ł nj#"""ǯ2WO5%%%e}EAAƲ;wM 冞+1E{ֺw+8qB}xy&~Q˗/cժUXbLL>H)((@Ӧ=hRWckٲ%߿Gڵ+,--55i>>>prC>ܹ\}QPPѦZ[ZZBQwF7k, 0.M^8v0z!ܹ4l޼6m /333uUn޼ e+Zۗ%[BQ(hРwU_`?رcؾ}0aPEǏ͘1CWj"T*1cF߿_$!Dm%%%bȑBQRļyJ1BO>D](bРAm8Wڎ?.T*4jJ10뀋/#GoF_~YܺuKgK J%o߮^(LMMs='DrrXt077*JL8Q瘏=۷3gT/{ϋT 2W^*9"رcŠAĥKl[۷EEv,UsUY{Ng haݵkptt*J("\\\Ć (b:OQ/=1`(L4Iտ&\={ ;;;qy&;:hԨQprۘ˃p 8y$кuk@޽{ĉ4χiii̬ F~:?Wd***B`` Ν;{C!z 776ʂ:t]PE ' ??˗/DzeڿSغuk9w̦8CUE!tc(+!ƍcǎaǎӧ[P K:ܾ}[kYQQ֭Ç"//O;#T*.]666bĈsssERRBq}b޽_V1͕Bdff9hРHNN٬als%SN*֯?,aɓ'޽{xꊛ7o"22/^Ċ+`ii4t*:vwaСgQ}֭hb„ شiƏ Jo`Ls1x`!;;v3fݽv& 6WVºu /}ʼl T۷ ???,LMMW{?~h߾S,[Lk9fѲeKann.7o.MVЦMJ7oYchhVUll͛7fffVf&mBCCJ*V30$%'"$"$"$"$"$"$"$"$"$"zOOOO|dž.ZdggKy00ɨl޼*Jjذ!7o &ƍ-T*zUܺu+_ӧk^vQСЦML87o,sܸ8VVVpvv̙3 //_wwwXXXK.*s7n 88vvvȑ#ѦI&x7V鹠 ?4Mxx_~8q011ڵeݻpww*ҿ5%LRfOV/ѣhӦxwŗ_~){=acc#saaa!w.6l „:tBQ[o/ExnBvډf͚bժUMl.\cǎUj.H7&JJ;vꓒ"E{M6/2:~'Ǐ0C "??_,""BT*qatajjd_|Qe˖iՔ$LLL{ァUxuOU ɩNZ"##ѤI 6 FZ]333Tض&I&pzY^^bcc1n{IDAT8XYY?VVVbL2Ec)S_vڅ={[ne:t4,5h ۷"y LJZuV011˗/R/ܹ34hW]χzٳgQ\\|hذ!pXYYռ7B$&&GZ5x{{#99Yݻݻ8^Fdrssqc׮]Xx1,,,0|pvOFRRBBBo/Uz/3))ItC}\EEE (ggg'222Tf;(,,,w'ۖrwwYdt0`ѢE =ػw/\\\4FFFYfׯz+( ܹs̽VǏŋ+o߾O_433cnn^_ڶvOUјO)U]YYYE:10(u!66v°aÐSSSv%%%ؾ};$''#99޸y&9"y2wIII ³>/Bc] =|P^{r|MREnB|j$'On1}1cpEXZZu¶m4+ mooJv58Y {l@ݶI&033+w'ۖ*ݮ'_&=J?Xfz-[䄝;wjBBBߖG;j]Kvv6P\\cΝabb]cyQQ^#))Iɓ'((N:wwwNMM(>.h"*Cya !s='Eaax'N,s8(y033=-lmmř3gt:C]~]#Zh!} j?!tI$i&(JsN(X~}9NIIhڴ1b;}u=F!EoزekϞ=ㅅ֭X~x2dV s*JLS/_aee-[2dgg㫯BxxKy"EAbb"`8vn݊I&A{M4{ ]SDOpwwG~~>rJ~c`I{DD$"$"$"$"$"$"$"$"ޕo IENDB`APLpy-1.0/aplpy/tests/baseline_images/tick_labels_options.png0000644000077000000240000006611412471065325024425 0ustar tomstaff00000000000000PNG  IHDRlYRsBIT|d pHYsaa?i IDATx{tTg1((BEXnfBRPE J-ZO2l".唇rx$b*h@P92!!dLBc}uM:˵~c|>`p7<"x,EX `)'ROK< Ծ}{M>=ܭT5G@<(Ƃ M*55U7o>^GѣG|9s7o]}zw]s޽뮻Tn]ծ][w}0~6l&LPȦL͛ڼy,X/TՋwݪ^l٢]ve˖Azdj֬Y⸱cÀuǍ3fhJNNֲe4tPl6 2?.//OݻwWnnƏH͙3GݺuSffիjܹZnwp"ϟl['|6vٽ{fΝ^oA}6ͷvR]d}6-|޽}M6Ͻk>O^/**׿5`M7|O1cf>3,_ddoܸqEzꪫ| 1n v*IڳgOkn[111{տ]luM7ÙnFիWOYYYsGիp8?X-^n:uFC=$׫͛7-YD;wֵ^?׺ukYgϞzK\2%I7.rvo߾Y}gAi&o^\rISnn4h?c:uJ:u $eddedd(66Vmڴ ,[v^}"kݵkWG>|X;w,gT~OJpa8p@^W&OƍknݪZCo|qqqAzfeeyW~~~999"7n}=Bs :tH'O,vͳjѢ$髯*g\<[nQ\\.r پ}{EFF*===ɓ'$;_-槟~*IꪫY8e˖Ew}'Ij۶mПpq!xPݺuSΝ5o<?~\ǎҥKէO 8ȿ#G*77W+V(q.]/P~~~==zT~rrre˖Ŏ]n-\Pyyyo=OQQQ7o/f͚4hӵuVZ֭ X֭[UN%&&'n{1 O |JLLԚ5ktA[]vkѢE)͵IIIJNN֫?7|=W_-Ij޼}ѕW^Yݍ7jرUV-u]>}z[b}>f̘QBB|IQ޽{#O>QAAw9s:PVVoԽ{s~vō @pB/Qvvj׮-I;w~aڵ+ ~X6lg}Va3Tv]_~ys銍W\z3τ@'fӎ;$IҴh" 6L+_rssV[*-Z(//O ?p@<|*'ROK<"x,P~ڹsgfڵSRRR8'g%oYK[fkVZZZ JjΝ_Cnrt:˼fy];W.Zw*}gMOF=ܣիW]GڳgO@{'<[k޼yokVԻwoo^.KSN3f'("MZjiڴi=zZnt]hB666Vu jcǎt1?~u~m}zg:%K/U޽zur*Β%KԹsg蔤֭[+%%E//W_i)I#FӒ%K }w%&&*==ȵ (66VիWWvdfkjر7n5jTZgۿjԨի }vuԩȵdڵr322$x5kLAOV3_M˗륗^%\"ݮ_~ا~Z5j#Zg;tN<" ۷OpL7*g}Z%KqC#I:qDkϰ)mluܸqzp_~ޛf͚I~gIڵkkҥN:_UU^=լY3 >@-7p-ZStt63kҤ6''GM6-2+s\.y<!0J1M)z/P7tLJJ*s"eyNY޽[԰aCIRvv$iE۷O͛7?QFc= ^А!C\VDDb_l駟e˖Q}i޽t:eKr fvp\'YN";euAO+8p@ 4^ 6IJJ-[0i+5~xo߾Zsri_Zle_Umڴ4hx mݺk[N=\vԦM+I^zI6M * yy3<#Iڹs$魷%I駟ֺuԭ[7Iҋ/e˖o߾˔7xC^Wo"#k.e]VѣըQ#7|a4tM{OcǎUVԦM-\0`N^WZZ~}3#FЫ;CcƌQddfϞƍtԷo_KC _|6lZn}?;T %xN8Q6M>O6Mo~Y'glI׮]yfk:xjԨZ`Ag/sL)To.鷟P_nKXG3<u]sQqZt&OQF)..NǏĉ T6 w(.\3@VA?@y<"x,EX `)'ROKEr ÐaaLӔicnCNv{pvp8Q`)'ROK<"x,EX `)'ROKEr ÐaaLӔicnCNv{pvp8Q`)'ROK<"x,EX `)'ROK< w!0+)4^7Y9Np9{vpq-ROK<"x,EX `)'ROK< w!0+)4^7Y9Np9{vpq-ROK<"x,EX `)'ROK< w!0+)4^7Y9Np9{vpq-ROK<"x,EX `)'ROK<"x,%?6 Ca#V2MSi^o s:nrfcGV[K<"x,EX `)'ROK<"x,%?6 Ca#V2MSi^o s:nrfcGV[K<"x,EX `)'ROK<"x,EX*2 4.Kl cGdL{0vg%t:e &-ƎJǭK<"x,EX `)'ROK<"x,EX*2 4.Kl cGdL{0vg%t:e &-ƎJǭK<"x,EXBϴ4EDDo˖-q{ocڶmTGjҤIT^=EDD7,2i۷.rꪫԩSuĉ?צMԵkWըQC=z=Zl3gy抉W_wy':PUڎ=ZZlp_=\ڵZ~Ҕ)StW())IiiilE=zTt颇zHqqqڴi&M5khڵTJJڵk9shϞ=5k}cǍ3fhJNNֲe4tPl6 2$U 7xmP IDATX☨( :\7iDW\\nZ$֦Mtu=+3%%ZƍSXIҕW^aÆiժUٳ$i޽r\9rΝխ[7=cO:uTc tȑVVVVK/Tqqqu%*** t߿$YlGիp8SRSSŋ-_\N҈#x衇zyk@EW}ݧڵk+&&F=z֭[_UV-թSGȑ#}n255U絿K4hP;vԩSԩS(%%%)##.##CjӦM:M6::Z ߮ hΝ5knFmڴIIII~UvرkTPP>HӶm۔K.Ŀf+P̜9SkV޽K#I/rqڰaCFW8w߾} aW!g.]ԥKwީACz'GIM0﮻RBBƏ%KgݺuiӦi͚5z饗TV;vLolժU_/{qgUL-[T~nݺ|G5kX֋ф ?IK#IUzǏ/vܙk@eU!v<ϥYf:y=3UVMӡC,aժUJMM՝wީ_~99r{5i$`lZZZ$-)qa2 T0i4x MU{nĜ3tJRnn8 ~ Ν;kAIt 4ɓG;_W_}mԖ\Nv{Y>J"M$-q:* qO?Tܶm۴b Ko7eImpS)JwqZh+Wf,ٳ\vmr-Zp~m=zTׯ4o<9ϧ_~Y͚5__A2dW.](..N_~^ygۭ;vСCպukI>H{V~LMMUPPp_Ço]b%IFRZ[oU??`w󙘘nݺhԩխ[7 6L^Wg֭ӒԴiS=z生N:iٲeڰa-Zt Z ֜9stiРA4iZh!I[UV7ӧժU+M>]cƌ)~Nr{ァKf)55UjzeOY{ kcǎZzƎG}Tjҟ'M>z>֭Z`vn+镱 ^p!xUXeOŋ `)'ROK<"x,EX*2 4.Kl cGdL{0vg%t:e &-ƎJǭK<"x,EX `)'ROK<"x,EX*2 4.Kl cGdL{0vg%t:e &-ƎJǭK<"x,EX `)'ROK<"x,EX Td@h\.<0 ƎX4M?za&8Jtn ٛMn[#[m"x,ŭP)++KfS 5k P޽[o/_/B#""Ծ}{OsZhQZOBvܩ'Sݺuu7kjѢ֭+ϧY}nݪ_|QSLр4e%&&&$qk͚5z嗕'O&m۶i2**Jvn6}W[m )ϭڶm[yOu PW_UϞ=ՠAEGGN:VÆ ճgO+%'T1PϞ=m6%$$wފWjtqh˖-z5o>^:qrrr+Wz={vHPŴmV6m}]-^Ș {ѸqԬY< j֬͛yi޽ѱcx5m"x@״i4F*j۶mڷodzI&JJJ:oa Uƍksui̙P6W߮+BӦMSrrUZ5?~\999O5|hʕٳg<d]VE'&&*%%ENS=zЄ B 4 |o߮yhs=ڶm[HPԩSG|MPcwڥuT Uw߭9sh+vLnn\.̙#R=\.<0 ƎX4M?ze^駟Vvvƌ'xB WttN8}?N>kʔ)!l|V@Xn9-\Pv=P-[dedd('';ر Ν;#;PEu'Rx@_kɒ%Ծ}6iDIII4hZnrv<9}FDM0A~|&LD=:}tH*fʔ)zW4n81BE۷O/OFiǎ'T1_M2)IM4є)SE ,Q:tjl?T UL۶me JWPP ǣmۆTg<yꩧԿuIÇWNh8qB999JOO+۷kҥ!#x@ӷo_}3fFqqmڴъ+twT UP޽ջwo}PNNw<㕔vڝZOhi ^.;!0+)4^\?~\n[YYYjР -[zjM6Mk׮-wJtn ٛMn[Lk/ҥ&L өS*"nݿB[m2evޭ7|Skjƌ;믿zObV\|Pw}j֬VZ^… n:u]OJOO׸q/hA'x@SO{w߭~Y\.y<a2 #i2Mz/h=zhѢEeCNv{pvp8o?MI*((}ݧLs9BF͘1C}]M:UZ?\x@{n r\z'ռys]s5j޼֭+ϧYwg}={A5jyr$x@ʕ+l2mڴIK.$l6lR7|>}(**5 PEEEi0`$ԩS:t$^z<q Pdd,Y ,EX `)'RO<۷?R^O}Q >|A3&O֮]>}z>}f͚jD4arxdža00vJi4M i~I 6Sխ[7x@ɓ'y.x$I999ڶmO-[֭[iӦkeIJRR{p'x@6lzJ>aÆjڴ,XR 'TaO?nFmذA#F(rK./BC 9F~իѣwߩ~! x@ֽ{w/rmzWիWj< {gg%''$?վ}{hҤI!Ո<"|\.<0 ƎX4M?z!צMmܸQGĉ%I=$o?5o<Jtn ٛMn[#5۵kիWСCoUPP-Z(...v%<_zԹs.Ǐk…Zjvڥ\լYSt뭷nK/ ;vo߾+IUbcc??ŋ5uTXB!⭶Po߾5m4ٳGÇSj߾}ӧO <Q-[-((K/$U^] 4PJJoT'NhرjҤW뮻NW.vW_}nM5kT6mڤ]Fѣi̙j޼bbbtWw 󕝝+W'PӦM7kLO>V\^^vJNN8ײeˀ_-=ܣQF)//O駟qw#>PϞ=ս{CrV\#G^ 7xx/[o{O+[lѬY裏Jno^?6n;m4;vLj֬$sٳ,XaÆXkܸq_+I+5l0ZJ={$ݻW.K#Gܹs%IO:uTgϞׯ J8;;[YYY,YH >.::Z<6oëN蔤%$$h%~#Ghr8)I |r:uJ#FX㡇͛Keu!5n85ҡCBW}ݧڵk+&&F=z֭[׎9tuIƍSڵUfMlR׿ZKJHHn۶Mo?:uTddedd9vءSNU6m)33ZPV'N(B:ydH*ĭ4hnv5h@;w<:*_sBHP0ʠ8"(SR$I WHJQõEA+(e(@[K~M]*}R |?k_vQ<9Ffu]k*55U;v1FK.UDDf͚8͞=[C Q\\z;eY,o+)))`Ҷ}^CT\\\ UZf_ߦMVY;/s3VϮ]k׮tedd+ФI{ȑ#~?%gOZҔ)SU9v4h{VշYW&wzcX"x'%%Eӛo)c$IZmLLvURRRxtB7S~}KYqtoVIOIjѢtQ%''KRR4ib=zT 6xIII.]-][:F2}*]]]zu5Ur)//>C>j#SiS/;4+:=:xRllbcc{۷o* ԡC^Z~}ׯ_/IJMM$5kL6np 6U]v ƍk/**͛5d.\[/dgg+++>`N"n9Ԋ/m_~z-tMkZrZbzySɓ'5|_K/ꫯVf|oۃ?P۷oWffqmۦ={իW/-^TIzWuQ﯈=6crssռysV}z3fPF4o<-ZH[[ ue1.W{|8ՅlP+ 8{<"xlE؊ `+'VO[˥<߶aExx|^7Yegg++++e!eO6n9VT5lE؊ `+'VO[<"xlE؊ `P3rv8r8!<<oڵku*&&FIII;v=~ZZRTTJ-]tz6mbbbdYc<>5o\3gc=Ç+==]of޼yzBhذa?233N@7x$i޽r\5j̙#I{ԭ[7=CTX9u9h>czge 8?߯aÆQtttʕ+t:}SX-[׶b 8qBw1^y^[t" dG>f {UIIƌZjqoE*...TڵSxx6nܨ _{QQ6oެ!C:t蠅 j֭}ׯ$V:Vvv*n $鬡N97}|MmV^x/_{GmڴQ z^}'С_m۴gv||zŋȑ#W_}UGUff?k3(77W͛7WZZڿ5:㙐Hx@3fPϞ=5`5i$۴inݺiժUS*--Mݺuz3Ϩw޺馛|5kqi̙*..VN|rYFK, (5TeYoڴiJLL~ꪫxb]{T:tʕ+k񊋋Ӱa4}}g̘Fi޼yZhZn-$*wT}rۯ{vVIJ,M8Q'N/5w<5 `+'VO[<"xlE*≮\.G+`'#z!&8:.;;[YYY.@ ){vt`+'VO[<"xlE؊ `+'VO[˥<߶aExx|^7Yegg++++e!eO6n9VT5lE؊ `+'VO[<"xlE؊ `+'V.gr)//Ϸp8p8BX;y<y<߶ a5!xq ujHٓMn[N3U[<"xlE؊ `+'VO[<"xlE*≮\.G+`'#z!&8:.;;[YYY.@ ){vt`+'VO[<"xlE؊ `+'VO[˥<߶aExx|^7Yegg++++e!eO6n9VT5lE؊ `+'VO[<"xlE؊ `+'V.gr)//Ϸp8p8BX;y<y<߶ a5!xq ujHٓMn[N3U[<"xlE؊ `+'VO[<"xlE*≮\.G+`'#z!&8:.;;[YYY.@ ){vt`+'VO[<"xlE؊ `+'VO[< u83.Kyyym!Š^o VVVVPCʞlrr:!j,؊ `+'VO[<"xlE؊ `+'V.gr)//Ϸp8p8BX;y<y<߶ a5!xq ujHٓMn[N3U[<"xlEتVիW+,,?6lOXXn*o(!!Aiiir/)) /TEGGqٳlZvZ())IcǎѣGcOUVҕW^K5vꪶcǎUΝRRR|x}6nܨٳgwU:rN%''~믿?ڹsyw},Y;Ccƌё#Gyf/UyfSm۶UNN٣YfizwNeYAݻO>ӧ_߯??_\l^y߿5&O^Z-[jt7J+˥QFiΜ9{GݺuC=LՊpZjU1ƨ@'No{JNN{lڶm[ S3ϨK.߿JJJ]"[|\RN:%iСղe|m+VЉ't}{պujZ<.+**J=zWwÇСCզMrtرC999z5aڸq:uɓ'+>>^ 6TJJ^{*W_}'NSN~JMMզM|m6mRll.2K7o\xPՊ 4PFFƍoѬYtuiڵJMM-w?ۭHeddzwt-(...UVU83~UV2eJرc~eEFF/[QSuUZj{_V1&7xCǏ/wmU.RCNSzqB^TT$UV~WٍQzz6lؠ _+,,Ttt_rz,jϊ4o\EEE:zEzߗPzz3p@}PQyK`4ib=zT 6,xdKܞj~BJJJի')Ie\.Upu_㩴뭡jN_?Bj*}W=g:Ko[Dݻ7}Ua蔤v)<<\7niQQ6oެ!C:t蠅 j֭}ׯ Z*;;9vt:kS+/m_~z-tM-]TƘJWyS)ob+JHHP۶m}ݻrJ_ہb ۶mӞ={|ի/^$:z233}mWDDy_1Fj޼*|PԊ3Vttv&Mo3fwj֬w^1?ﻘ#FPAAz%''맟~^zI4i-[j񊋋SnnN=`3fQF7o-Z֭[v-2]2^:ŋOVA'E؊ `+'VO[4} ;qDcY9ryMzz,,]4tzw7l׿_|̙3\Ʋ,3ewq0Æ 3 .4g6wuYrePc1/ꖁd3vXpB3ehbcc_]>.˲ z,F?˲L˖- 7`,2/_cYIKK3ӦM3/Mz 7X UݻM-̅^hLb^z%3eӿj뙛k,2_4C5ez֍)((0\rILL43g4999 .0-Z0 j,8M^LMvKNN6]tk7 6 c,,,4_|IKK{g_ʕ+eYfUէOӬY3SPPk{eY3zhzӢE s*:}LJJ9tios뭷t:Mll_kXՙ;O=,|羶m۶p3y*B ?MxxMnxk۵kٺu_C=d""">1fƲ,؛4ib0֥^jzU8|0?_{QQiذ6l??eYz<cYVoaLzzIIImw\}Ƙ߃#G*ܗyqvرԩS@]wejٰ'sqrgElb,2skgn>e~lݺXerss1;v³f֭f~,˼{~֭[g,2/XܹsIcݻ+w<I'Oѣ5|pm۶>'N￯sjΝڶm~hر~}6mmڴI[Vll IDAT_{Ν%I_~߿/SN5tY6m|W:qDJMMӦMe]VnM7otsohǎ￯ &HqFuI'OV||6lkb^1ոqc 6W^ѳ>[ O0Kzu>JۤISъV߾}k.^WmڴѤI|my=Klߎ;*,,ɱ)))і-[*iǎ~Gx j\޽[}Q} ziȑ3fK.~}-˒eY~mWRRRqKwj{پRqq"""ʭfM6s5|IRxx̙#FHv!c.]͚5Kqqq={ 8w,vk߾}2eѣGkȐ!ҥvY <ӊW>}ڙKzu>J/J4b]uUZlvڥ/իl٢(N}z߿_ EFPQc*عs!U9/JCD)))V رcjРA@{ddS[UߊPWsƍk),,}ݧ}jɒ%twj***#}RbbYfX z%~fdΝ*..̍|C*==]AܨY׫9PQzӯZiy=tɀ{nСCU5=V0sS *Ծ8ݻW%%%3f."ߟ 6WVOĉ~QY嶗աC}*((k_~$)55UԬY3ڸqc16lWv)<<<`"m޼o:~֭[+ tOXXմiSݻ7߾} Vz ߲4DDD &y晴4S~c7nl.bX52W^ye=,2ݻwk/Lddرy#y'ÇM~~iѢWyꩧϺu`xWeY檫2s1>_֭_*;ۿ:?o,2ffСƲ,3}t~59K/d,2-kv/4m̜9-Z͛]@9wnڷo>{lӶm[ӠAӰaCӳgOzr h/,,4=IJJ2K.G5|7w&&&Ɯwysg, 8k1k֬1\s2M65G.%%%fe˖A}fɒ%t.YtMbb0 o߾fʕ}smxmzR̋ChYV^RO٣e˖~t]vAEs讻;s#t^s?_RSSMddIJJ2cƌ yΓ_OcY`L %\bfϞЧ&ZhQ/pz&33Ǜ ~;ve1^ 8{qq![<"xlE؊ `+'VO[<"xlEBvj2FIENDB`APLpy-1.0/aplpy/tests/baseline_images/vectors_default.png0000644000077000000240000011755612471102127023566 0ustar tomstaff00000000000000PNG  IHDRzzsBIT|d pHYsaa?i IDATxw|ϭ;"2EEUinK]Uk[ZV9bݴ8PdCFBr˭G!~xyOB|3_AEE&  @} >@~D @?s @9 ϜgN@3G! Çs1ajԷ @nطowy'yyywʈ_6+K~XhcǎL***`ǫG4Fűda=dcbB-t(>ࢋ.{jj*"p},Dj9\p9tFrxRZRj|Pt{Ra{=1\9elnʍPYޤ$l<#==cǎ98eDtR?WWWG]]4f_GDAҦez-8*܇CiВ{\MGiO$ GN|'kY8FGLUt $V=UN{|tQ R8b|]ǯF)q)&S쾬P%FNMB ɿjln?a;ŋYx1&MbŊlذw*+W$33s;E *"͋hgP!پ^c";M)7I]xOěel(ggKКp-z,m_-!g\*f@6ͤ~9z7?TeaDZaܓMFq#%%B: | 2n=̺!RrWd駟駟b nzCRwgg㝑'ADn`);v (:jم3qKE"I>JQ)˥=sX8ogrY~, l]_8}OZvȯBrhɡ96TR 8@dgfCI1uV7Әʾd}LO䫲|^z"= P!v iQ|UY`;Ʃ;{<~NǑ^k3O?ԩ+wv&LQ,LKa'ș>)|%:::=ytC˽'_~LB)(12%}g (cñQl GifPȷ?NGJVV~YG.^Z~՝r`pW[C&Ѹ|eID@"}\Gj+jĨHʿPe-4K 8"w;wT`Qx*s> I9gS,ކޯD#(mJfLTF"ѧ^1$?] j2C+ѧPWWGBBvbԩca0سgL: nɫkD1^("2vE?F9xW8hMk?M4G\@uo1+<:՞rW`Yo֓PUPURUPURSШSxrPDU0NzZzDAA]#NLwM9JYɮ4 2|/}A5 ZC9魯۱ڈTG^UֿB[1}Spe+I3忧)z`%{gWW*$$$ {̢E%pg?^|wxdN)S&YID6d e#xW*:۲!L0ɈIRP5 .? KO|CaCiGf38eU"#W<{ӓ*\2/(W/)l)H~@n Ǣn^}Q/z3ykQ 1oX$%#)S}mڴiu᚞塿h fh%Z5a|.rDܦQH4a!)qAcA,bw t-(- JܬL322 * 2*2YgM{$L k$UƱP^BF"AVhIc8RӛH9Do_ #99גiiil6T*=:W:CA#`a ĬLAgpUJ-xLI&E%6}wv:§P`z)d҉`wIK)yJ'%LN}%~cBRDu|NANAy9ZMr>;(d:&{xD~NdXgI=>&za/P{a,akQKZkٟt *5똾 @\F*KV:Dk_<.wQ$2[P*94t*" d*ȬAA#(u< >> $ [ώ/m pk3?۵n!3%&&fټkH%y G0tҗVpC̢AnEYTmDLܔG> iz!o &DTN$l%U ٮuB||<;w&Mq|i/㵏tҗa.Y4( CJobӪJE!l4U2{j4n< jJ&&4u+Nl@1fcؠOL:R)6gE=e]oMuuuw)..殻rH^I_%Z{h˷kV]%cQ%a J8\U>AFHLLb;ab ;研|"}C`"GoƢ*9,B-iG[4/!'AבAݿ@an$|9!}%uW:#_CxZ`Wnᛃ^ƻUW aM:.??ߣQ> ׈"ufDi餷ɽ-lUqw N\b pR7{ʼnד_E`GS0a HQv̴QHjݔ}Eտ]1;Kb/H1P}7)d>zdg'Z39 3Al&hl5%bC=TTݖ~kl'0r)ܕ@Emvh/!w,LgӴC$41yt,;)+zPVk¦#fZU:)ęacѰ'5˒}ʉ~)\rIN!_cxU&Y $Խ@D4'\Ks&[iVđ\ <'dѿI'qɫ%LnI`P"#S{$s$ф?!@:cg~ɨOpqI}Rޥ&r)"cb]+.6> GP J9Zb"D7z.K0+rV "`%-D<7F?w~ՂB\)%Krï/ci1݄ILNxiaJF=Zdm`IJ=;ItQuDu1D߃v){. i JCW 8ZeƷ\:j2{"/cՐkݘ|'%_Ү>GYt MеGCPZYF/ aEM\pٶU|s˖-sSJ+Wo'?ӞvoO׿EakKl_׹"ly0-xrC11)H{yk:t5Ly {N`U zĔ?BD;dTkP|ۗ2)|'E%tR;m0r%ﲭ*'7O*~;Cm L1WpXqW:׬Yk:Jzѣsdy?:x3-{rZ|%MyeY̞3؂"i>(G$dR:ؽ7i4v9=&NONOfԭ䉮S"nJ~Rj՚|v4}$?i߹`URҧ;ep Q,[=-? )$LԘ{pZ1~;{'*q"VIr~N 8ǿvpK,!>wӒ AO"'+ Է1 f~,oˌ&^Y mRV/Q:ADNm<ʾRɓmDp."㩂3i3c 5m WƮT:?7=(QIƢJBf5M}%]+t|p Hi%zJ+%|wlRM%\@ n MDsZ0yΊ0ck Ift;x]\sZa}ӰOն̐b_Hu:SO8`X.Y̟g~GdѭF&ơw#T<.*fLcivh`پ!]XD&m/M(D!is$St0s?O@bfX?\fnfg$i5A5PԷɼ'& vԘ&˭6[obA:h,=Oftl3&E9ȍnrڮr6L÷ 75А"N^*f |BXh֫ WwH kCH>L^7 դ7i2yoi?1-z"QiHc{4%MΞ쪉9xuvߚN%crL&˭Uū|gYZLbbY4l?+F37N$֖Ȥ͓DMWr4j&&q5-ԏykP$OUm3)^_lSG1/']U:u--guG+R>FkVtviNGo=Q{? [k }h;jU{7M&+'/gInOQ1{:$3I{4;-DU@k=fIrO"P[d~v Rx` W]St2;iEąs-awC9q=y$ٱlkFm4;bb6s MI['{ɪ3u,;m'OHk,̳Y^-I@NƜ_#>Mo%|~\+̓'?֡4>4y |V:fd_ sߌ\4|3{?~w} gi~!G[c(lJOߟo&`ͽD Hl_P˨16Eo$oOŵVdfG)Ȳ'ab#Gd'GxU L`:G} ]OK'q9e~kxt֎W4-9eܴfc(O)/].qC^^\rx7~Dg0y~L 5yxe'U0%Q%MyЩFՑ#z)_WG(3bTA:oXgbfUGa}$Cy$Mze73!ZDԲfĜuaq38YN׮]ۇ=ʕ+>}:K,q*^bhS3H-^ ^1 ᙑ/b be)`]Ή~b1*CM!:"?"Кq!S汪fb-D*uj8,{>y 3қX2w&} kRp*mFJ`QkJm%j3;ffVs>Ķel ;D.2s o6ƣMQ"{'\'}3Ͳ< ˩p{_K4Bt$jI Ւqdr1+Xe 1~27ZUci1ESiHC9S,z(2-dExLXUT(IXg[J4&j}W3iu :BZ%EIT Wog̵$ꆐxFM&WFgB0l%F+RER}\W HR=n\~ ɚT Wogy}Gİ2w@Q@߰>HR=*bUD?R6EI%y'\=uj6d*C==ڨֆ H֑2Q+TWRrD?6EI!y_'\] yXo[ !tg&ju_Un<.Yu$FJ6j](՚{a~ژ^-MG UDU:P3 ~riSeՑ32o˒iVF/$.$.KD$k+߃?0Cp ]#:!!DuP@mF$aڿX;fnGdoc%b,ŵTwj(^MQ ~!k@ $*ZIda3ֽizџYTRNy[xɭ$w@jmߟMI镨%jUr3je'ߧ8/{-1_c:SqflR׽Mh:Ǽ5챾/*̫ hdRzDuXgDom&!T#/'SoUcoSmZ3zcWcߏJ[n!))0º6n0]V0Q7]D[EK[T!< =ےiѵrc~\+:: %+Jǯ'aH:T~RadC&B0Y6Y*h38 xaUl?ǚCi LNi*QX`U{4SjڣX4b?! ]bgeeydL-ɴiL!ZVMm2*&0-tP5{TM|hKF>Mgpq,]Yf1k,d27nK/e„ ^MWSV4EEV`3Җqאy[ }gd| /JU&LOmLLj#$v>kn { 2{Z!l8Ǧ\t&,Qad~5z'MođXΕlN㡛\]v؛2r)Ja"?hRaU ^p/I ͟rǂ+9ܻnG$2q٘RX?LO沲0' nq`y{X޵y`iLgш{{#;0N4nh:4UG&3;gg̔ictZwv3;37YGg|U:r|mt3)?tSJhX(x ɇɵ\ %#^_rK2AdfZ=?g;/-̴W8k>ǿNpHMOa{ -Uq c%eOzjF.COvR_bi>o'E:殳(ywܸ1a2f=g6ҏ݇Ieӈ2[eV- ^_Sk7QyE]ܙ%)nϥ0_:f"OoʹfsV9̉0/)1m,{O "qD9tpnxyq!s!0=eRzW/&gz3EJYßsSN'ގ>|G4UcQlOWfDϹ952 +*guڭ[|UufhN/;*.8enU"FO;MM7dEҳ?75ɾ1u92K1ozd,QB[ZH*P)'zCp>7xzIucG".W[,L)!-ټYy g .BuJWN=LsG Sq! FGgRTLV_sK@~!y 9kQ+ڹ55&DXEyԌx-Ai%\CL Am֓5 lpx%K2gdfeIff[Ot'J!ʺ)y8!7*eSE$'k.+i^(mӱĿn 4ݰVsW|B1op(.OY͹zt}s2DAE\=Z&55WcZuck.rJ>x54kEk<ŀ%oƳ9fH'12*(y;)4YTa%ĩ9'~`>U5K8ܞ;d 4FdG %Q騮f9tȓ:~..>`抔Oh2m|5~'yFe :842h8'XpȾ3u9#f;9a%ӎKIdsP: U@:4 zlf. gS-6>TRJ|?1 uaGy}f\cIIK)F,˷,較tf)g4_HrYG2dFpdvrdD?ԙ} w~9U {~I9 {{e^F͘e C1iyQ :u*qİuV|Av'|⵱k"&&i)H>V$ĈGͥPy566Tj ?JvzC1djv&;Z/ldDv0*NGnQq:rbu++UTSLԑHt˩Lo=99o?X_`և(> {„uwdh|c Ou6cStm£gE/Dt0.'~mZ-[netMXVy4W<uS(_1#Rު$ yc}|z#34YqlĩH %%Zԋ"\ϑJ͞9-IcseםV²E,U psFGݰ HW] ԿIso&'u KH4NJJB&R~@ +.\,ZE})/DbQCFS&Jp ËPʺfYQ&M^PT} _r۴C%n,ah4tR#iɍӑ%'VKR{8_[25Qc\O6Q9I9?xGx쳮Axz/)h%RWoy)Uf0dg5ݳRo2RkL`{>j;c7oF&G<|33eMsG7)iL_D4~8#~[tFxlzCMT Ǟ5jL!y^=@VVs/'&&/{$nVeo*z#J;rM>/Liܑ`Dp*YW59R^ry[YėmqKnxR5uuR~ʕ7В%KZw}$:dҥ\ve$&&y6G)K)i;Rw=snN57HP#%WQ6' Sјz7wٽVz<7'_~e&O-y;_|1o?e\'r:/ zH xuB 易B4GpG/U8G*{2u-azlaDuR{aڵy~Ʋfknf"P樞|XJK;qDm3hF&"n׾a0gt!7sFZ#ʮUTO>/0q~l9QA>g@f3)w`fRo7Q={6[lᡇ_lqLs`3"VWV} ӈ"m;2& Th©Є48>)7P8gd^ߕpO j`vfvM`sƓ% Jԭ_Ӛ8GDuR`ʔ)|^9*!"5ߐZ(&:K>R̋MMg9j$RFhUL HMs鍽9"K+5n9`#m~ǫҘqRvz"5`-I{'1%zBLQc a9|z];T^Eb0v/M-I{8gR/.Z YNKnfydgVtVo cn,ӣw"ù_Or^H'a)"hNT} }wUKx4 \RJVFbvoS%P-LYǂCI=¹=ޠz#%30ZBg~7 k {l,WeeQJ yNٳ2^=7ww" ^݄L%z{)|QԱuC{/\#rTLKq[Mtmޥ%ΙU5X<6+L#;\ 8D靨9ʾs#DjA: ҵJORΈ.TECݘR&WdD)إ;[ ; Ęms;9"mVNBPXU ߀jGw 84GD)erP-Ulw`6- O/bل+_.m/ +"/H8aJ O_?= D8ggID1e9*kpVuњpqVU. t5QKfltJ޾Mfff%4G3b h5Gr}臓$_ŵc x7^C~fކm3!b?!2׶uwcq7hyzbLZMjjjG׺WXr%}4Mjr'l zƫP:ސ|k/U֔w ]nԷo)"r:ih_fJ2"MibsF;&#CdrO.前ŝ @WFL'Q]]g֍;|7܍F)ydͱB<}f>*/?Ǿ&@T}-\+4 Vwg8z8=;mh8p5&1./a;bЋr*~ĉkyƷ93G;jT͘xUəZv[ILT[ScJִ]԰ LC":l#8Aװ(-RKͯo;C8;d /_2״glH^,(}/\!\az|kG31EE$O@dxONds|(d6v5f;5ȭ 왂rƂP\ˈ\OEg5\3\)yz&g&Ba|S1_>s꼾b6cRꔧǗQI.TG31vOCgr}iUĄX[:zvM,bo7Zͯ0ق뻋]cN)Vo7Tl]BvP#r87Ύy4[_ z>ެ)b a+K;XӉ>xXk +N(i6/"WhrSZUϪQ(:Sl>3_%ktG(lL۹~ IގIjʚ0KzS=$o.8~#αufz5 Lta8~.7Ti7ԶE!!yEQ"ԹAt IDATIXENWdNgjS\rو [8\<ӕJnwSS;{Ȏl!/eg}%ﵵkML N<ڬq^Jj;ep !?F%ɴL [QJ;:Aeo*7cXǢq?UԼ\ PP`tRe.p ~N!Ct|ttz^B&s*2SFuU\NQч(:Yrߩjs L주S8j]ۋY|SӫV}l\$#T ؀doOU+%2;RFm֮toAFVp(0_AQSUqA2+9aHU})wsJ*z_'ţ.y&IR +sr(垫S$j=v'N̺p^?t[m8 76~D&[BkrW7lJ,8]) KrWVוИ_k/΄mD v*4X:԰l՟ñ{ZnvyUKWq5|cz%C;aGV&`+r Բ:1O n^1޷W7'hf8td@ eFʒJ)RIޓr Բ21۳עw7| ;aJ*LZ`ԉYOZ:G ~ HYR9-|$ X_I^JҗR\ .t70H+ XoK>RI_:KB*|0HR$2}0|-}d/eb󥖎M@C eFJ)|vtr8+ᤕ`8*}Q\('JJYjM@C UFJ)]F6cSΆbB1-u/|!si ){ۀ[ "1+X ]U6 Ue sVa: IҥK; PkH Xw%?7, EԪҝ}~F a':;8kUeWr{7.`0$BB ¥L%!΅(6$&4{$]Iv]|+yу;;gdV9)Ńb?ƾsxݯ =դuaV,8"d:FZ!)5ZoFuWJónj=wOkśI@VwHbY*6/ jT['">\"Q"#Vi8輚  =}S87bfR`&0<fMViCVK.fJ4M(nlWL"6&)}'=Ϝ&6ȭגiU{Æ fzIE;lhWLbHNO/eNw &1z@==!g%2M&T<1ɲcd=29zT8\&;'z60ϱI׸}~b pu! M~`>+Ij}+ A X:@<2f2cϟSaD!NRp>I# L75md`,eu&uE'F5'umzԥNAS&I ^- rcc6>KA =&EГy7 B,MQצ8ۛ8Mul<{[(Z@ 56>q[gJA}>b_B׭" j1H${1{t{N cc2}c}bmrmxTRY-VPO-s΍;ɍu vBcm{2-q*u%_P#;׃e6'G"uUyGt8f5#,n`y~}~̦om ,/R쯝^\bU&,R>뇱a|-:^,\B E=lw/8R-!I߉Y Kq]T^9gl/m.&(ݡ9*[ҒV+/\{hHyj"o hwfxv+ZFBf&:wz*.czIfHfG9,[;ߔzlð+Z,겾DWIZ)>dɢMkBOiٍI dZBīz:$KEsHYf1j1"",oI }_ b,f&VJڦ~m;zyEZPΒ ʹ2(FopDa8 wi$RUP 7NyT?OFL ҦR<͎3+~렘+MSGw\\SY MpZZxBɻ^Maa'z]38ˇ[KV!Bb&WHՕV^kJd(ʒuiG1\AVzҽ{+[q57{+M@X( ,]O%XQYj"^.X+Hj*i5D^!^|&O+zf=eX*ml-jo4qx/MS -_×Vrϡew%++Ç3~xYYYp x^i,/jX+B贍"*b/2}LA6YXjAOMS s~H}K߳g~/?_~~<\s ;Z4KŋS+Ed!VFWHI ,Z,o-uERn7|G_*K.c媧h5V&Bȴ" Zŧo^Mx-Z:8rBzz8?xc+ӟڵk5jT-^'deپ};۷wnJVXQi" @5KJր{BhA6֕a.NWR}֍DuSoh <۷S\|lWiQQ{COnzBfDmm#RDN8MdAH^Уx7j4usm﬛K/[f̙;TVV2fLUU7.,ϟ/Ns ~u/J!"MM*(DoȂBﱆ7b&MS.B&q w."Q-Ză>_N??XcS,K+[oe,]{W8"DZ+EbLۤ+-,Va8%nǏi׿ulؚ_`fロr<v⦛nR=f)ʁ"osi!+b{T$r@ YJRTG^i$ zDXMS2G'"DY+EbHGWKEo퐕If?DQ,Ύ @VFlm2!f)1y1X6Y}JAv>n((2׽u#[KIh3 ٖ&Y_Ѥ2#pu-J`i z}!/qQRUKC7p5Q,rR];dLrxǟPSO='|A$-[Ɗ+9N9zsq HǮIƝ<.kof|cӢG&eĦh,!!Kfi$ Z#7Z2&so՞cqU$3!#mCP,:,)-~ժUZǺBtFmM׹6N_՝!r犾2A28vMv<p4AȒIf b4O)vx/ loL|#dXH󗛑4l=kM7Ϻ FMn\A'&BnZmC9GzZ+ݖ0g (.dU}@y-^I!tBRh䀫@xxdcYHnͺuMn}C6$ !vI`D߄gZlxǽ׹>ηGyǏNbW 4ʸ )̌5Ht+y ̜wyIŅα 3sEzG?4|OǺXzy{5˪7Iz]Ke.V-Unn^']YM'B6g '771;tPf)\45$~~ϱ߽OCS=wz]}}|=q/" 9q/Ǐgٲe: [$O[fIK1qe&H[2Faa)v&];W F幸㌝v썓p!5,*k #2THŗP FOLYq Ͽo&Nu{\x,Zb֮]ꅊ,{Kȭk\Al&vIǦIb 6 !nsHuc1/LHygg> WNfΰz~>pǩ>?<ɗf0qOSrͱaH;/Ebې5K26ݢ{4nٺu+'|27x#'|27o&p3i$>c.]g}ƬY4h1cvܦ%9$:o$ӷ!L8}ty$qy&OJwo𛷋$*WNb)}4lӊbvr5sOڋ䳦Ȭ$˪{"(Io4Fx8!e0IЪ8bȑ#y7?ɡC={6G梋.bǎtMLk ҰkugkANnBi^ _)kmYɌU7 d&$mlhB dfJ10K!*;:jd{NOM##"k"{z:ɓyG4H=%`rb+F!3A)][GⶏzX76̪Vv Ho2žgO41li']7e{])Nk"3XdPKTٹjCX~dzrJ~߲~&L%O IDAT5gŦs&H?f+zo7nXܶqdk^U65.+n3TIDc "i-c<^͐j<lY 1(]L.?" !SueG_:ٻw/guW_}5cǎeÆ ,Y[om۶qs-p'aM.VW`=^sq8u ,H/F_y:%Km<.pYIf\7u6hQbCgڦu.*KMdtMi4fdn,-SLaƍ?dCrJ^z%9餓я~뇙J_1}fكYy~|yلAeٴaִ IhϭkQbGm<4vam$&(z*ڋ ; t+fGbQ{g(ruih__veܹk61_رc7pCT fBX`81Þ4ȵ6b Ҩv\R,ii^ `[PB{Dm=UުYBlxWEG Kf"qմIŴIń0Z-rq٩q(rj.3駟8{>_V,^+£]5hgI?)HmcqdmN~j~j\{B{o5dS{ڦmq(#ΌӔ|4Y %b(rXw8>>ϫVu1cV3˗/g̘1s9qgGƬ{A- 8DȒ~#7O\+}o[䕴GN4N4Bl@˒^"}i6-,gqS{yr%$C23nh%M9:hAmoUdFk{Ebm@)btK2|XLPvJmBe!IRIIբ^sMS!4stX*F ݺ =藾QHE!SF[B Su JMF)п{Z^GJ YZ,#9Z7DAB*(Z*bE4KպM-rJO~~>]tр농\ې4q@HlQY7 DDm-b _f).%.e\yy\wu;wyGK nti^кy*""Eĥm{!VOk+(RZ;^8ʿo.]JZZ]wחx-E7M)4DX,& 7z4OB؋yۭIJODbAVn~иMV⪫ .ofٲe8p]x'>sx yDdFX)E`baiȎX8j!ylҚQbܹ<6w\dYF߹z4%b7" .}اLYiץҊvρ>DE!iJ+zQRSEŧmDbY+#&ǁL={v|*v MSN[nQ }-8" 6mb z5KABwsww{n8gYSa/nB!k!wT,TyXJHL5u-IU2Fk)o1#{261SPCSM䕴&) b}i|M*NMS Xvd8R%}اȃB,V,}3xDdӔH%UJ_ ( iqXmԧY MSKqYQ$"*6  J= `}܈lXz y*}EħmDwk.M,uz lfu6R띗ˆlhgӿ;d!57 >E>6PZi[wYj<~1{*'CjRkh) LLľ{7rMTM(D/oMiTI$ZYPwлrd0le5MV%BOR[zz~+ yGy'b>kqȯ+s`'^Ŏ;O?51y oqZ[PPA9Df뇴eRUYIf[9M~ys3˾F-i-R-vj Rv>IhoFNHNt#dODo~󛸎;Fv<::qU3sUW}[ь1.;VӚ!z=w.ld34[DŽsF#/4NgPvުf3s q[FQJc:,+Tahy7=`ko.*{FSª !F{&u0̺=>#JR$ X3UUogwjiȿRxF~Li<S"wSL<ݞ;bD*H<s 2&2|lmw;T+D uz7}2ڷ),c1%vryLHR}i* W':Ef\/*rPw.ڇ<$Yܙ'MW?&7cnQ5!]ӺIU!Զ>ԧT@iQ% s * =D;(:dǛ>m} jo$-n >F6r?69N/8WYCh/a5Ö y<~ԙwdU!g.ilDCyP}x5%m:(9&T*Gxb*Lۊ+Xb1ǽ9?\ 1f3v4Vld*s!R}vR7~pK^_hο=XO$ڷ)al͎u>O(i/NzmC$~kwc9@v6.76󘒶f]PS}yyToW(Jf"7R>ҶHrԄ:vUcʦtjJoaYO49g9VX9w74!}:P_sF pBl:j^{mhTke^ËKTI\OGVi0>&ђ̯!߳F3*&Zbow*j<U|QAKι_ O^4^HТѤXY]*erZm AO-qbԣRu,&!5EE6q--G$>JJKg_+€%K`2:U zrhMoVK-,Z=4؛$&xߛGQMO m 񠥥'LJxjYܒRR.sӳ 6D0\ 5LtlcBV muxBvvsi uײZ[*c&Y>^~e6nȍ7ި#7z =n wٶ cS[wX*ߧV$v&gmߓoE?[rym"+,DTB cB||{cҤIʼn<5Vˊˑ%bY-۱'|.a{:_!m[яWBk] =,:mX{1:ՉHQ[-K:"߳&TK-8c6o!J- D^dVKe H@?F~_i,HB1&ZB*(qW'uƃ.葶~*wu ͺF77ZNjL9m2(*% "E_+yXg6]Dcκٻw/O<|eǃdee۽.Y7]ZlIө0+!" "EAXTT׸{ڦ\h; }ee%,s7sM7u{[nwefDj Z^PY*A Cn~',\<$`MLZ~ICd7Խ0*bГ{Vd[[RNċ,TQ#"4 o@TK0(cHAh/=4nSd#By{W.co8C#]i}6Wx4!2}#j b 3Vƾ23)# b-o؊^kD.V1(_f́z]F"UW^nKPP/_$!UD^tZ*I C@tFTK0(Xا*" vJ%蟶CFbZw%Ԫ{|fBJ*eq, yq##T.$i0>aDo2.BC j]HYG N*,Š{ Ĥm [-ϠmD@\…(x@=f{ؤCgV=`EĤm`n!ڬsmA"J:VQYK}\ w"Wo nM)OhK%$:fκIκ)Z,F1ەU8iv_-E,b>lLȧB[*h:&Y7FN߀`(٢#!4x(XK%KqXh'|r籛p/<䧴}RfWI<2HozÖ}7UɐhgQ?f{v =oC"9SŎ#Œ *11_|mmmǝ]do⋜_Qdyz|CN`^vzY% OQcD*U bbn[ {'U O~:$FꦏH^7h70;(v '׷̋l(sI5a5ˬx^5JvcEAVW鲬DHYIS@'178μdK~>?Ek~00-ݐfcE%fi#3,Y-(_aJxuL=l>2u-^?Vn?'|A/ bgS`c+U+e4s^1!g 77("_ >Fd${ز6P94V$ʚ8ţ #$5>.gy+k4fa[ɳĬ=}ŇI~f/MUD֪Iw׾88#l˹ y`1q!C T0. LބD?2rG])Xr[l/玟yW> fD|g C_eoh<&28˲z^C!y F?-GeS|Kz3]lah}DV kci`F|E-Beza ݓ*ώB5dz^xb~Z^^m^D\FyDfZ^ap6VO#ٔeJԙ&2{W"^co瞏fr>a3+yqVCH6K&;oںAh}7!vhyJ)KfvfTWgWaQg}6sџWZ}빏+_|9N3r.&bSb.1ۿ$CnP/Q|^=+}7'n`ru&Sk=N$_#}V4f?$Mc y:r곕/ rzY4|>o6oџ`,b <)7IYA[qʕZ.LulM9'ouv&Uܩ$q2Կ)?ܦɣiN|{VP9*Պ>Q\X̰uX.Ni:5t^Atpfn k=9TS^DE{` >rB{ #+ f;4DUb靦Q%򢬔䝑4GEBN,g{L%^62ӿwM+Qb-s}m묤9/<IQZ#C3kQÐ&KyGԛMBi-2R{#H(yR~^nV027 C{agt&W3"K3GZЭ0Cg:e|gz.V;F12&j<x X^JRJ! H'K =^:|G37MWDbܕuVJ7GM/uqq5GE^7Q]ƙ?RJehn=R.70 e([lbsDJzDiD(+%h5g D6QU&jzI˥(L,R9;UԳC H${ )4\?7MyAȋRBgs~+CB3"ڗ01{]RAC8󷭳xuBvUԏN"bb4 BE+%t6G{Q0>N2Oҽ :-IgbnACj5[&C2jif2 %D 4 EhBO9*^ }\6&Q)=+J:]Ǖ )1;Gu YqtUSRB%yH!M3WR#:[)9zlRC&&GN0+:9wt.ޠŘJӚ9U{4x"N3 usT4Mnz"D3,+umZ.DqI*QHie̺DfG 9/ݛ@\9t\*wu\6v T ٫}q+I?WJ*H+ee̺ё]yE̶($~y1%CB!8 J:G"%ZhF!D^:9*^ J;QErmRKm,)G5D]*31[&"O2 AhBG:o'DM"[[o0_ fCJh+%%SQPH 3&K.8k```01`c/^kUUUQUU% b 1g\EOB[B ˌQtm8!q %+ioz̞CӔ֌fr<=%}>C$ 3ϐج_ؠ/.'獺3Mal}FPR8c_Ri \YǃYr7x,6n)yBn:nF&O`\qٳ'_Iy*3:~CZNٜFHo|GW3{tGx`U2?AtdInɪyLdi?)WpzW^y:G}뮻>3f}X2!1!L:}DCt-VmUnK}~fIܤMu3ȽRX-3|1@{F4_Y y`D`Âv=_ O~<ҥKk; uN(IlI~f?ꜻD}0$cWxfn{!֧icoO8}oo/9)ꃴTk~i B,ZO?_|O"gO3uX:0kX/}~ Lj҄>*%&s! "ǘ#14ۧl.#uuwēǗȣ]STOc^{5.\H}}=>1$LuJ0[o$N,D졳Pm<-7gNжPhi/bQԸhyag"YpU-Z6ED͛7#IZzl?㩴,V4D+PەȴNo| f-?=肫h׺)'e# ҚC8lTqk+"8 /Gt~gVCcO==׮cl4EDz|L62,[KULJBOӧ̠ٱ]eQ.t6E}nfMQ=a}Q:LXv!?oQx.ԶR BZE^inRӦ9T4O2z 2 zD2!r=^5<(D)Bo``0`g@S +eST_1@#XtsJͺkc```6j|7ŬDTxgqG5!'?t0vXan'sr PV&΍;]PG]EYz!C 8 p 7000Bo```01 *m$S@(sBo```I0wú*m`IH*qxb.bJJJ>rp;wh/ΆW%BϯgGCn1 AC4#ui52!rh5mK\E'A2=rHF7˗3{l.\Ih|)z#)y'wv}6WƵ'on䫓1K.q jjJc>XvuwX㛘,yAα/gYO۵yY '߅42'"%{od߭:%^/Gys.?N[usݤ퐦m5Nx!~2˧k`.^^]nB>??d=R K.2Cz!dvsL 1J402LkiaD<-ߏfi281{m-dɬiL-E E92k j67A7y]%+ߋv"e`wCvzn&猲:[xW0w(W0W0P2aLJ]jajajfp?IĽ#ZFށ'40i[&_g-6-N#xj`]T[O<"(os3rf=e#crYxS9ex?s]o,ϟ֭L>6D?0n&CD[( w0W('|#87"}3Ve&`MwZǚ>`yV:: t ȴGi7Q#Ku <B+>(xz i?羬ws98ڃy|v(&p퉇?̆R zj%v4450747047S`??-ߤ53NWlR;U]\CÏ.CQ /NaCR8nI07|Ri7t <Bandz?fixlI|~dH/L,r[ o.3*L/ eRIWȠA18L/؊/2uX0yAfciGB =Ćc[ӮBDȃ!q#R+ym8c.v'>ƊźlgauMw3o]u%HC & (뤂"'H,]7 v󍉛)if[ڹ/U h/\1IwqA֣2eF$6 )Wy0>aD}Yke~fNtliBl#aggc " D GWSP,V}#+!B6/ja 5qJB "E3gME[Yj^^L̙Ú5kyٛNWUޜy晼/CwƌÞ={D_`ȑǴOʦ2uN* 6(}us\!._+Wrȑ_}y3gK™g)0vXї HSk<4'\'{߿իWwq7000Лy5 BZBo```01 GbC=ɹ'㬳+,'z|85;/kv+bmFK,d21uTї96l`…瓙ɔ)SXlҔ{xbJff&&L{北CB[[s>L&zꩨܹ;I~~>W]u\q0՟x衇Xf _~9SN?̘1O?'D]ƪUXp!3f}QQQ!4ٳgM7D^^k׮{aÆ qM"s12BQ! L:fô9ltifߋb5?ҖPSQN?뾝Q}}y̼,u6{ QFƍ-γX,4it| ;wDII zAUTXX(VEEbbD\obԩBՊ8M]]P"66V(.e4RO<_dP*Ç%k?߾}UUUB!ݻ' u"<==EeemիBP6`?~w#88O<(kݺu ̔:9r0/_@t-Mg7VT*.Evp}-Gtt4FFFBĉmB߉TUUo߾Rp?~@#99Rqk׮ ^°aгgOxyyaժUhhh:hZ!yyy0LXfM~;vþ0 ł H鲲K aZ1{l̘1XlL&4ӧO`@AAF $$$@c׮]Rs7o|}}VM/c;OBaĉXxqiiiزe T*q\+RRR3g~lݺAAAt#66* .\hZ맣vVMaᰯ{9dX;*̚5 }ɓ'P(T6m7t:Q\EOk Faa, DZb f̙Flذ .5wFM-[7X]]PWW˗/CVKɩʐ^ł xZ漏^Ws~'kkх:,bbb˗vMwMi͛7PTmX;DGG .\hK;!z=AϞ=àAdٷ3f ׯ_|||\0ԏ#I>>>wþb5`~xԩS :K1ffgΜm8p Μ9e˖IC؍ ZV`Nhٍ=zˬn2oG]{ALL /ZHTҙ2e jjj#8qA!<<ׯ_ӧqFYoFdd$T*t:q9\r0LRGl{Ň`X`20w\=ѫW/TVV"44{ƚ5k'ڵ (..ns+c; V+J/FՊ#GJé.n*h4bϞ=RrwYf ???! &EccM@@//WTT=~XDEE={ J%/^,%艈d=z""c'"9z""c'"9z""c'"9z""c'"9z""c'"9z""c.-77JҶCիQ]] y&J%m%&&Gt(J\x6?o_tRˉWL]_%H]B`@@@qdeeҥK())inݸx"V\Wa03g3mڴhuz,D HJJJBFFΞ= Z۷cŊ8tV;233Ρh! [7D-応|rL8ׯqq\rF=DR=Q z޿P\oFXX]Ͻz8{yy BD;z"?~DMM , `07|p['OĻw˹֯_?";.OH 8v|}}QZZ Cpp/͞=:a<$$ɉ =uy Ð!CЭ[7Cc_z4;vƍ[;`DDDgt? q!""'O": .]Bll,F#^xἠDBOJfΝömL!55UhDvXZ3f ˗/'${Olڴ o߾ٳgV"77k׮ETT<==mJKKq߿?Nj۷o0T*RRR:+ =uyȁs߿,V6Q*0L0a6oތ (((ppBoZeyAAA,* њ""4أ'"9z""c'"9z""c'"9z""c'"9z""c'"9z""c'"9z""w$){IENDB`APLpy-1.0/aplpy/tests/baseline_images/vectors_step_scale.png0000644000077000000240000004005512471102127024251 0ustar tomstaff00000000000000PNG  IHDRzzsBIT|d pHYsaa?i IDATxw|S?IwlRZe,AjA+EP*@^k\@~.T".C*BtҽҌ6tm&~><$c˫'u8yB-!X=!=!=!=!=!=!=!=%55k׮ŀP(XC!V^^?/F>}Zl'_v-֯_Ϻ BO>ůI&FDDDw _Y-f{ƶoIPsھ1+WlO?/7K/'> cǎVO<7Mi?a@`` rss> j{///=6nh9LmJj}D2Aرc[l7h Fe!ŋ>f^z!--7o߾}sھ16lh㑔dsP*Fi塇BJJѯoJ'L: z׮]e~ݺuco1ozLuu>Z"X̞=>>>򐗗ǰ*B1ԩS1uT 49~g>J*ѽ{V !Vܹ;w9s栲Ҩhze^ݷӀ5>AzS-?a9kPVVf1>ӧ- M>(--h{.-sP7a7eгL=5c,}}Y=sH5M؞_BΦ˗/ɓ L͛7lI&APg϶!M}aa!㑒A5{ސRѣqMXK.޽{1qDh4+WM!ͦf!//~~~0tPP]];w :&L_~sZlOk "k "~"FM];88ϯv۷oGtt>`ܸq ?`--- zc䠠C iaÆ!99AUbZ6Z`` V]!,}uu5ɩל!bcpqqTf_iԦ)C{BBXצ5릾v򂃃ciBPku___?Ξ=A1Bl{1ٳGk0sLB?%%%ߵk~EAP_Ǐ?1c`(//ǪU0p@< 'csAj*dff^RR>O>$ tcǎ_kGGGDGGcժU-BT\Чծo߾&BJ}BGAO!"GAO!"GAoiށ[B זƢ^U_ÿ$\y!kSfXn,ނbH9} @o47#׍6*Gp̤NQh#o{݈L 98m5/8fFڻc˖-=gk?cv#F0;77~uh0gcct Gesfdd̙3m Msyx|5( t+)]4pj%XBD v7||1 fBAof-ކ;=߅ڭ/rl=_k0T2]y4 (PЛcN7JGXc3)Z@ysx9.f!3\2f"S/Ժ˱ 2N0u"}5'~v/g]fk{m8] | jϭ O80͏HJϺ,  iY|$fΥq;tNAanL%>r^*Rٱ.K.㥐uW7N(웣 %ݖӫluCQ( c](qH hx9d  ؁@֥$ $7ޜk2{eTyGiYT_b,"jb ~Eḽ{S2yǠN ÕЁ>9ի1 ~#i/ȓ6N[ :xNdO~iH-k!ҕuYVs8]<q3Lt0m}csh(*v.s?G]Dl {BK s,0s}Q1Eҕ~G? QK,cccO\eeEԫ_u]5/ Zp\-d]5p{r~'oA+_b #G6jFm w{1mڴvE:` 7ɹpTpt(c8W2l]߽߭: ZiYx?QX)73Jjv>!C؟:u N?08aKƤ(\0̽P_/"tr(׷:㷒H<Ns8SQxb-mP:~/n1}{Eрa2 |]Z?rw2x9WhQƝZ/dVwŝZo=QTJ+΢u*c0|~'& ou~wfT(*?|EI}PaWk_*w\/9تanZl&nxߙ/*TR2ڞ0O+C~; z\vF0ڵ+wb֩#2#).〫a^Ph3ŠiTuH%H \_B'>a'n18ޅIiQ\sePЭQ+sKRX^̤(Jwqː,t1]F" Yca/XD=waU152\*Qfs( ]iHI}bb")S."Bp!hn(o0Kب$uC!d zB9 zB9}ZZbccѵkWo߾ TBAfggcС… ӧOcpXH!6CAyf ̝;Z_5JKKѩpw#sdM7d28:cB1A1c<̙/";;=6l؀ŋE7#&Kd̓>xصk8,[VbУGDEEa޽{,Xuyb3􉉉xg@iӠj+'0Dll,{1 !"n֯_H}׋W_}d;q !D(kcχVmZ]5Fcp$kdЇ"99iii 0`FBdҥKqw}7vލ~ C@xnH!%Ƞ=z4N:_X~=ܹ`Xt)!Ħ2`Ȑ!سg2! B( !D( !ei-"GAAv"%BZ&'nS>Ra)QL„آ}PV`9֥0%Y7ahk^ d!*Q&ujgɺBtù} ơZ'-̛#n{t* q-?2LMƾ֥HDyGG2Nd]N4ؽn$pAܼyïbWUN`ۍ{lr7{ׂ*2faw,xֱl2&o]؅ 99ٹy^8S?s@f۷oE^5)))F>v {.N씂_ʦB#K|%2*%b] 4&JQ.{bNpCՏu9?n>:<Ca߀Z1Cu)LPз]-½q u)L  G.$"|jNR7QeJNþ*R+8_J>! pC׿(웨yZ)`]Qзux= :wǺbQMP$\S7[E,(MD64+Ukn"RDRﶡXaoLju7:7ԻmhVx(TK zHۆ`ǝB2^ͬ Nu)VCAoiw),ɺ!;DaWTSj)-bccpB;{,Ν3ng;)wtN)Y4 O &bbXzD-\\@@9.QEBwp b@㩖}g]BBBгݍ+++:NRDW^&[m%Mu;hրFBy G#ö́{SOg~; $k7olWץK:NbrM춲A-_q*]Ve.UuS-k᚛nYcQvTmh1.4RS-+̳خ@̌m#X1{SR.sww2r,F:j`e#poJap%IFJ-GqZޔþx]Y."v ,X Bji ;H6u?n½m {N4ȿ'nq RNa/um0Yz"*½uS-c]8;%xsf n!և= U ~p}eg# wrJ5LFj6B]e Z N=p.K<)}j^7F0euyv_sYPܤ>ղ{пHFTIec@e@J6tXIHo NAo$%Xb}R{1@)u-! v Zײ.,( JMX_{Wvyl8MHF=1}éb 9RA=Zz?d8t<=zt2ywi23<9_) Ya;wLL^-[jsr7L(# arrn[M)JV4{E7ک)wmb銾nU Z󅼭5܀B!8u9DR*B}t)R;r̦X>v+ʺoAKR*.Ţ)GR*Bo%thmi- Q~yzwI}bb"Z- kZWAǠ2Brr/yЗκTзE;.b2 Lv'6T;.Ch0BDNAnnn߿?>#eBMlH[qYF!6"A~z^׮]ˣO=~al*ɄbI)**7,oHHHTUUyBL}G;\www,X*uybSׯ_ZԩS1ydl߾< 6l؀9s.Bl c+**P]]ciӦARO?o={2Bl @5 ͚5 7nӧ n BXi!fFP3rZc_]p7z@? !ؒbn{1ÇCTwsrrtkBgt,43gĊ+ga̘17mFB ~РA3g jDEEѣضm^u.Bl+,Xj}>c-{7_Ξ=^z /^Ě5koB6lhAYYx \tY|;v<o&n޼b…='!QLѶb2B( !D( !DL^yfԀ8|GرcGvڇBL klemC}m!z<`vktWechBHK8^A7'(VL`]AM1vo=3qqq>|8.^Fw}8y$֬YgbȐ!:8;yQBW=jqM .'sw{BYbr>\ 2,¤_]ؿ?;dffbСի|MDGGիXp!d2FTI++zsBC䵩K1+x+&R PwE_sv%>ѣGC 33ի(߆*{_Iu@c>>#RIj6+ 芾_0ܹ"==}Ň~(-k$us@T m߆ :OKKSO!44.\@BB^z%3f /^aƅ ٪Tvu]7"ROz6u %E߿$''O?Z׮]sNl۶ yyy{/Z`kS_,@ao uZ ӧOGJJ ΝbGyW^ł NZu@1IDAT/N|ƕXdoԺmqOLX_N.cڵxꩧ]b֬Y?wVW:io [+$bm>엏7wCˋmgj 0a}x}:83z$qj.̖Hmx@VAP7$nOLGZ&ESwdSXbRJBMM v܉tx{{#::Zaݜrc}@)mOL9777$%%a+nl6c&٦R?0a}||},R-hU XdڕTmLr333q}5zѻh@VJte/n)LLzJggF?h4HuP? +=o%rV&uV)KKKׯ_G;v)@MG6nZ o&|f/XcmrT{߮XbHbl[Rb4{|̆gz*{?I^W: ֦J:qTIՓCXS+-׍-FiMR]4UKgTkRNS+v3VMݖԝI}@)1 J! z-ܢ)@(1g V=KESRp@0 Ig]ɤmswj%]ѓHux@4v*.ue]ɤm#jC&ES4 ۺߔ^(qd]ImojeGPG$nw›ZgZn6mԮcw/,Fj?|-[uNsk`e_/x+dKJJ:|9s&s;:7RaU?yƛ={msЧy6d|W:8vV^f;}GZGPE1r iq]pKUY(wuVAAoZk1GOC+%^ʩJ"= HI}ll,fϞ ;cUBحA;wΝ;geeu>11 R!dJJR͛F'NgB zB9 zB9Q}BBd2 B9zR{ru)b?fɒ%1b4 ܹúB9_}vYu)b:-¼y!=;!5Y~=233?.Bl 苊|r[b]!4A^/[ ވ38C{BBXk!&-- 6mڵk{><jddd͎n!Bt_CD׍RXh9s A||<2 !f_~HJJjePQQu!$$Aebވiիq~aUB\Mk8DBApW-9z(!&ꊞBHs"r"r"r"ruc C{о6hh"dM^7BL#+z9s&nY8cǎev f玊bvn1bsGDAAs{V?gaaQ芞BDBDBDBZkYWapб.b^p7_VQ71n7;c]!1Bxi3jY!M:..Z;єsU.\:xΑu9BADzcVwtqXx. {b[ƻTB|T`]NQ7z&dewu1^ eNC_4 {b^g7>y/d5G-]N#t=%2666/<==ݼy~|nh`$kBgkj5Wzim [2c번 /E+p(KFu 2CsvHC=aL8Qxw"!1+ V-i/O܃W}CoȂLź,e_;<=KPm ܝJYi 7sYcvI-\p-b+,N8" ]\ qNv@A+U*?dWAsb\-;Ri~*9Ji}RQ;?%XRI/\*Ϲ] 5@FEh8'߾w(R^M8ބɏuYV-TJC(pUNm8x m9h+ GVXj _C#!uYcS)͉(ub$kG.jM19BiJyfR ETMI9j))O4 y*!i*'֥X^vB-RxT( J2> !R[D09+QpKBEh<V_|bڕX1vP߁-Pmnq,> xv4]s91){ݴDju9SXDZQ/㺩k*e rڅTmx? u)\)o3\֥PKj*!Pʕ+#Xղ޸U%O4DxR^R"s݃AqA)47$3 vCZK oUMAO?$<;!D( !D$uP(gE#9=s]RvtJJ s3;7˟W_}CDFF̙3LWBd/Y[lgΜcŊؼy3!Ķ2 W^իWTD!K}-g!"7|RX֥BMEЧ ..FٳYC!6E} cʔ)֭[q-^77֚kc$)++äIPVV'N !D(kc{6U*#GO>K"$Ƞt9s&Μ9]vaذaK"%Ƞ嗱{nĠ~mocT!Aŋqvލݻw7:=!%Ƞ?z(!D0D1BH( !D( !D( !D( !D9립 uCB7T !DȚc^7uC!"'+zOXXϻtR^hh(s79ٹ˙BBBsV?gUUQ芞BDBDBDBDBDBDBDBDBDNR+cccc1uT+//GEEê!IW7EY`ccWJ*1tPĴΡ:qPdm`]EU^pθƣVɺ,T׍)n}Zc]pP.Pi\P~䩻ZJs W~)p+BdJxjSqYr81LFAo$)=TL7gĒ1 Cf _jI.Od8.(M,{8iL\]鄄{?1;.#Ny!;EvT4j"vԇcudBr{3bݍpHs| NOxcI;AUP9ud:G,&IP9r$N:ź 7n9º ݻ7_κ nܴM)﻽{H!11;wDnn#eYTZZbccѵkWo߾Guu5̢˗/ɓ L͛7lI&APgFaaŶ)!Yr%N:3f`Ç~H9sK R{] H[qYf1:t(<==pBxyyX|9.\$%vXaa!ѽ{w 4vJG'VXr?˗qYT5cJDOju_;;;O>$ǏϏ3߿?r,OκJHHe2F?SL&KJJUf>|~~><y8jn^^^ػw/`BU?ɩלmLٳ! z)S[nqKe˖qqqK.f͚7Ӣ D(EP(Ht /Fyy9VZnݺٳ6h̘1L&kԌ30`2,Jo6;99񡡡uXeqΝLNNN|XXb ^ղ.lz匌 }+W&Mr9Ϟ=/((0K tEO!"G}"r"r"r"r"r"r"r"r"r"r"rDҾ+d2 .DAAرcdؾ}'|...Ͷ+V@&a߾}韆{駡P(,ѭqx5558q֯_6 }طoy>|X|zz:1c ,k< k֬ivP̚5o&Ǝ 瑞bsbԨQ?b$&&⧟~BBBB]b@}߻w6n܈H<8~8 ֨Ͻܹs!+zBΝ;P*777DGGz\xx8,Y[7nlg}__FdCH#tEO$y7N8[lA`` ]>>> DDDnԩk|Y9!ơ'q>G>}:6++ ˗/Gqex ҥ Ǝk 1 u`С;v,~Cqߏӧ#!!nݲ\=!픔ݻwwAPP֬Y K# zBڡ-ǁm6rk˖-C^^vh% / /"&M777׮]okcǵHHHh 7VB(@6.\ /W֓dذaF7xW!:tkGEE5 zZzY={RГv\B ''''''''''''FIhIENDB`APLpy-1.0/aplpy/tests/coveragerc0000644000077000000240000000140012414762161016606 0ustar tomstaff00000000000000[run] source = {packagename} omit = {packagename}/_astropy_init* {packagename}/conftest* {packagename}/cython_version* {packagename}/setup_package* {packagename}/*/setup_package* {packagename}/*/*/setup_package* {packagename}/tests/* {packagename}/*/tests/* {packagename}/*/*/tests/* {packagename}/version* [report] exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about packages we have installed except ImportError # Don't complain if tests don't hit assertions raise AssertionError raise NotImplementedError # Don't complain about script hooks def main\(.*\): # Ignore branches that don't pertain to this version of Python pragma: py{ignore_python_version}APLpy-1.0/aplpy/tests/data/0000755000077000000240000000000012471104150015450 5ustar tomstaff00000000000000APLpy-1.0/aplpy/tests/data/2d_fits/0000755000077000000240000000000012471104150017002 5ustar tomstaff00000000000000APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_AIR.hdr0000644000077000000240000000166712244700136021040 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---AIR' CRPIX1 = 8.339330824421999 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--AIR' CRPIX2 = -234.7545010835 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole PV2_1 = 4.500000000000E+01 / Projection parameter 1 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_AIT.hdr0000644000077000000240000000157712244700136021042 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---AIT' CRPIX1 = 7.115850027049 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--AIT' CRPIX2 = -246.2317116277 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_ARC.hdr0000644000077000000240000000157712244700136021032 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---ARC' CRPIX1 = 5.082274450444 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--ARC' CRPIX2 = -246.941901905 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_AZP.hdr0000644000077000000240000000175712244700136021057 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---AZP' CRPIX1 = -11.34948542534 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--AZP' CRPIX2 = -254.1100848779 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole PV2_1 = 2.000000000000E+00 / Projection parameter 1 PV2_2 = 3.000000000000E+01 / Projection parameter 2 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_BON.hdr0000644000077000000240000000166712244700136021043 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---BON' CRPIX1 = -33.0741266819 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--BON' CRPIX2 = -243.1263982441 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole PV2_1 = 4.500000000000E+01 / Projection parameter 1 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_CAR.hdr0000644000077000000240000000157712244700136021032 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---CAR' CRPIX1 = 7.527038199745 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--CAR' CRPIX2 = -248.2173814412 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_CEA.hdr0000644000077000000240000000166712244700136021015 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---CEA' CRPIX1 = 7.688571124876 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--CEA' CRPIX2 = -248.2173814412 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole PV2_1 = 1.000000000000E+00 / Projection parameter 1 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_COD.hdr0000644000077000000240000000175712244700136021032 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---COD' CRPIX1 = 15.61302682707 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--COD' CRPIX2 = -215.3431714695 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -4.500000000000E+01 / Native latitude of celestial pole PV2_1 = 4.500000000000E+01 / Projection parameter 1 PV2_2 = 2.500000000000E+01 / Projection parameter 2 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_COE.hdr0000644000077000000240000000175712244700136021033 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---COE' CRPIX1 = -14.35249668783 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--COE' CRPIX2 = -223.0375366798 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 4.500000000000E+01 / Native latitude of celestial pole PV2_1 = -4.500000000000E+01 / Projection parameter 1 PV2_2 = 2.500000000000E+01 / Projection parameter 2 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_COO.hdr0000644000077000000240000000175712244700136021045 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---COO' CRPIX1 = 12.92640949564 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--COO' CRPIX2 = -213.6486051767 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -4.500000000000E+01 / Native latitude of celestial pole PV2_1 = 4.500000000000E+01 / Projection parameter 1 PV2_2 = 2.500000000000E+01 / Projection parameter 2 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_COP.hdr0000644000077000000240000000175712244700136021046 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---COP' CRPIX1 = 15.05768272737 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--COP' CRPIX2 = -215.1923139086 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -4.500000000000E+01 / Native latitude of celestial pole PV2_1 = 4.500000000000E+01 / Projection parameter 1 PV2_2 = 2.500000000000E+01 / Projection parameter 2 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_CSC.hdr0000644000077000000240000000157712244700136021035 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---CSC' CRPIX1 = -7.043520126533 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--CSC' CRPIX2 = -268.6531829635 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_CYP.hdr0000644000077000000240000000175712244700136021060 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---CYP' CRPIX1 = 20.56099939277 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--CYP' CRPIX2 = -147.1055514007 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole PV2_1 = 1.000000000000E+00 / Projection parameter 1 PV2_2 = 7.071067811870E-01 / Projection parameter 2 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_HPX.hdr0000644000077000000240000000237512244700136021061 0ustar tomstaff00000000000000SIMPLE = T / file does conform to FITS standard BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 / number of data axes NAXIS1 = 192 / length of data axis 1 NAXIS2 = 192 / length of data axis 2 EXTEND = T / FITS dataset may contain extensions COMMENT FITS (Flexible Image Transport System) format is defined in 'Astronomy COMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H BUNIT = 'Jy/beam ' / Pixel value is flux density CTYPE1 = 'RA---HPX' CRPIX1 = -8.21754831338666 CDELT1 = -0.0666666666666667 CRVAL1 = 0. CTYPE2 = 'DEC--HPX' CRPIX2 = -248.217381441188 CDELT2 = 0.0666666666666667 CRVAL2 = -90. LONPOLE = 180. / Native longitude of celestial pole LATPOLE = 0. / Native latitude of celestial pole RADESYS = 'FK5 ' / Equatorial coordinate system EQUINOX = 2000.0 / Equinox of equatorial coordinates BMAJ = 0.24000 / Beam major axis in degrees BMIN = 0.24000 / Beam minor axis in degrees BPA = 0.0 / Beam position angle in degrees APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_MER.hdr0000644000077000000240000000157712244700136021050 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---MER' CRPIX1 = 7.364978412864 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--MER' CRPIX2 = -248.2173814412 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_MOL.hdr0000644000077000000240000000157712244700136021054 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---MOL' CRPIX1 = -2.310670994515 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--MOL' CRPIX2 = -212.7655947497 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_NCP.hdr0000644000077000000240000000175712244700136021045 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---SIN' CRPIX1 = 7.688572009351 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--SIN' CRPIX2 = -237.1895431541 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole PV2_1 = 0.000000000000E+00 / Projection parameter 1 PV2_2 = -1.216796447506E-08 / Projection parameter 2 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_PAR.hdr0000644000077000000240000000157712244700136021047 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---PAR' CRPIX1 = 3.322937769653 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--PAR' CRPIX2 = -246.5551494284 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_PCO.hdr0000644000077000000240000000157712244700136021046 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---PCO' CRPIX1 = 0.3620782775517 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--PCO' CRPIX2 = -246.2486098896 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_QSC.hdr0000644000077000000240000000157712244700136021053 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---QSC' CRPIX1 = -8.258194421088 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--QSC' CRPIX2 = -258.3408175994 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_SFL.hdr0000644000077000000240000000157712244700136021051 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---SFL' CRPIX1 = 7.527038199745 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--SFL' CRPIX2 = -246.3483086237 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_SIN.hdr0000644000077000000240000000175712244700136021056 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---SIN' CRPIX1 = 7.688571124876 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--SIN' CRPIX2 = -237.1895431541 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole PV2_1 = 0.000000000000E+00 / Projection parameter 1 PV2_2 = 0.000000000000E+00 / Projection parameter 2 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_STG.hdr0000644000077000000240000000157712244700136021062 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---STG' CRPIX1 = 3.744942537739 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--STG' CRPIX2 = -251.945990929 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_SZP.hdr0000644000077000000240000000204712244700136021072 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---SZP' CRPIX1 = -22.62051956373 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--SZP' CRPIX2 = -247.8656972779 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole PV2_1 = 2.000000000000E+00 / Projection parameter 1 PV2_2 = 1.800000000000E+02 / Projection parameter 2 PV2_3 = 6.000000000000E+01 / Projection parameter 3 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_TAN.hdr0000644000077000000240000000157712244700136021047 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---TAN' CRPIX1 = -0.5630437201085 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--TAN' CRPIX2 = -268.0658087122 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_TSC.hdr0000644000077000000240000000157712244700136021056 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---TSC' CRPIX1 = 20.37416464676 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--TSC' CRPIX2 = -189.7220156818 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = 0.000000000000E+00 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_ZEA.hdr0000644000077000000240000000157712244700136021044 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---ZEA' CRPIX1 = 5.738055949994 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--ZEA' CRPIX2 = -244.4880690361 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/1904-66_ZPN.hdr0000644000077000000240000000375112244700136021070 0ustar tomstaff00000000000000SIMPLE = T BITPIX = -32 / IEEE (big-endian) 32-bit floating point data NAXIS = 2 NAXIS1 = 192 NAXIS2 = 192 BUNIT = 'JY/BEAM ' CTYPE1 = 'RA---ZPN' CRPIX1 = 22.09211120575 CDELT1 = -6.666666666667E-02 CRVAL1 = 0.000000000000E+00 CTYPE2 = 'DEC--ZPN' CRPIX2 = -183.2937255632 CDELT2 = 6.666666666667E-02 CRVAL2 = -9.000000000000E+01 LONPOLE = 1.800000000000E+02 / Native longitude of celestial pole LATPOLE = -9.000000000000E+01 / Native latitude of celestial pole PV2_0 = 5.000000000000E-02 / Projection parameter 0 PV2_1 = 9.750000000000E-01 / Projection parameter 1 PV2_2 = -8.070000000000E-01 / Projection parameter 2 PV2_3 = 3.370000000000E-01 / Projection parameter 3 PV2_4 = -6.500000000000E-02 / Projection parameter 4 PV2_5 = 1.000000000000E-02 / Projection parameter 5 PV2_6 = 3.000000000000E-03 / Projection parameter 6 PV2_7 = -1.000000000000E-03 / Projection parameter 7 PV2_8 = 0.000000000000E+00 / Projection parameter 8 PV2_9 = 0.000000000000E+00 / Projection parameter 9 PV2_10 = 0.000000000000E+00 / Projection parameter 10 PV2_11 = 0.000000000000E+00 / Projection parameter 11 PV2_12 = 0.000000000000E+00 / Projection parameter 12 PV2_13 = 0.000000000000E+00 / Projection parameter 13 PV2_14 = 0.000000000000E+00 / Projection parameter 14 PV2_15 = 0.000000000000E+00 / Projection parameter 15 PV2_16 = 0.000000000000E+00 / Projection parameter 16 PV2_17 = 0.000000000000E+00 / Projection parameter 17 PV2_18 = 0.000000000000E+00 / Projection parameter 18 PV2_19 = 0.000000000000E+00 / Projection parameter 19 EQUINOX = 2.000000000000E+03 / Equinox of equatorial coordinates BMAJ = 2.399999936422E-01 / Beam major axis in degrees BMIN = 2.399999936422E-01 / Beam minor axis in degrees BPA = 0.000000000000E+00 / Beam position angle in degrees RESTFRQ = 1.420405750000E+09 / Line rest frequency, Hz APLpy-1.0/aplpy/tests/data/2d_fits/2MASS_k.hdr0000644000077000000240000000125612470072650020654 0ustar tomstaff00000000000000SIMPLE = T BITPIX = 16 NAXIS = 2 NAXIS1 = 721 NAXIS2 = 720 EXTEND = T / FITS dataset may contain extensions DATASET = '2MASS ' BAND = 'K ' CDATE = 'Wed Feb 25 11:57:21 2009' CTYPE1 = 'RA---TAN' CTYPE2 = 'DEC--TAN' CRVAL1 = 266.400000 CRVAL2 = -28.933330 CRPIX1 = 361. CRPIX2 = 360.5 CDELT1 = -0.001388889 CDELT2 = 0.001388889 CROTA2 = 0.000000 EQUINOX = 2000.0 MAGZP = 19.9757 BSCALE = 0.045777764213996 BZERO = 1500. APLpy-1.0/aplpy/tests/data/3d_fits/0000755000077000000240000000000012471104150017003 5ustar tomstaff00000000000000APLpy-1.0/aplpy/tests/data/3d_fits/cube.hdr0000644000077000000240000000132112244700136020421 0ustar tomstaff00000000000000SIMPLE = T / Written by IDL: Fri Oct 27 10:49:59 2006 BITPIX = -32 / bits per data value NAXIS = 3 / number of axes NAXIS1 = 11 NAXIS2 = 12 NAXIS3 = 32 EXTEND = T /file may contain extensions CRVAL1 = 57.6599999999 / CRPIX1 = -799.000000000 / CDELT1 = -0.00638888900000 CTYPE1 = 'RA---SFL' / CRVAL2 = 0.00000000000 / CRPIX2 = -4741.91300000 / CDELT2 = 0.00638888900000 CTYPE2 = 'DEC--SFL' / CRVAL3 = -9959.44378305 / CRPIX3 = 1.00000 / CDELT3 = 66.4236100000 / CTYPE3 = 'VELO-LSR' / APLpy-1.0/aplpy/tests/helpers.py0000644000077000000240000000247512471065347016601 0ustar tomstaff00000000000000import string import random import os import numpy as np from astropy.io import fits from astropy.wcs import WCS def random_id(): return ''.join(random.sample(string.ascii_letters + string.digits, 16)) def generate_header(header_file): # Read in header header = fits.Header.fromtextfile(header_file) return header def generate_data(header_file): # Read in header header = generate_header(header_file) # Find shape of array shape = [] for i in range(header['NAXIS']): shape.append(header['NAXIS%i' % (i + 1)]) # Generate data array data = np.zeros(shape[::-1]) return data def generate_hdu(header_file): # Read in header header = generate_header(header_file) # Generate data array data = generate_data(header_file) # Generate primary HDU hdu = fits.PrimaryHDU(data=data, header=header) return hdu def generate_wcs(header_file): # Read in header header = generate_header(header_file) # Compute WCS object wcs = WCS(header) return wcs def generate_file(header_file, directory): # Generate HDU object hdu = generate_hdu(header_file) # Write out to a temporary file in the specified directory filename = os.path.join(directory, random_id() + '.fits') hdu.writeto(filename) return filename APLpy-1.0/aplpy/tests/setup_package.py0000644000077000000240000000021112414762161017727 0ustar tomstaff00000000000000def get_package_data(): return { _ASTROPY_PACKAGE_NAME_ + '.tests': ['coveragerc', 'data/*/*.hdr', 'baseline_images/*.png']} APLpy-1.0/aplpy/tests/test_axis_labels.py0000644000077000000240000000320612244700136020443 0ustar tomstaff00000000000000import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from .. import FITSFigure def test_axis_labels_show_hide(): data = np.zeros((16, 16)) f = FITSFigure(data) f.axis_labels.hide() f.axis_labels.show() f.axis_labels.hide_x() f.axis_labels.show_x() f.axis_labels.hide_y() f.axis_labels.show_y() f.close() def test_axis_labels_text(): data = np.zeros((16, 16)) f = FITSFigure(data) f.axis_labels.set_xtext('x') f.axis_labels.set_ytext('y') f.close() def test_axis_labels_pad(): data = np.zeros((16, 16)) f = FITSFigure(data) f.axis_labels.set_xpad(-1.) f.axis_labels.set_ypad(0.5) f.close() def test_axis_labels_position(): data = np.zeros((16, 16)) f = FITSFigure(data) f.axis_labels.set_xposition('top') f.axis_labels.set_xposition('bottom') f.axis_labels.set_yposition('right') f.axis_labels.set_yposition('left') f.close() def test_axis_labels_position_invalid(): data = np.zeros((16, 16)) f = FITSFigure(data) with pytest.raises(ValueError): f.axis_labels.set_xposition('right') with pytest.raises(ValueError): f.axis_labels.set_xposition('left') with pytest.raises(ValueError): f.axis_labels.set_yposition('top') with pytest.raises(ValueError): f.axis_labels.set_yposition('bottom') f.close() def test_axis_labels_font(): data = np.zeros((16, 16)) f = FITSFigure(data) f.axis_labels.set_font(size='small', weight='bold', stretch='normal', family='serif', style='normal', variant='normal') f.close() APLpy-1.0/aplpy/tests/test_beam.py0000644000077000000240000001066212470074275017076 0ustar tomstaff00000000000000import os import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from astropy import units as u from astropy.io import fits from .. import FITSFigure header_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/2d_fits') HEADER = fits.Header.fromtextfile(os.path.join(header_dir, '1904-66_TAN.hdr')) HDU = fits.PrimaryHDU(np.zeros((16, 16)), HEADER) def test_beam_addremove(): f = FITSFigure(HDU) f.show_grayscale() f.add_beam(major=0.1, minor=0.04, angle=10.) f.remove_beam() f.add_beam(major=0.1, minor=0.04, angle=10.) f.remove_beam() f.close() def test_beam_showhide(): f = FITSFigure(HDU) f.show_grayscale() f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.hide() f.beam.show(major=0.1, minor=0.04, angle=10.) f.close() def test_beam_major(): f = FITSFigure(HDU) f.show_grayscale() f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_major(0.5) f.beam.set_major(1.0) f.close() @pytest.mark.parametrize('quantity', [u.arcsec, 5*u.arcsec, 1*u.degree, 1*u.radian]) def test_beam_major_quantity(quantity): f = FITSFigure(HDU) f.show_grayscale() f.add_beam(major=quantity, minor=0.04, angle=10.) f.beam.set_major(quantity) f.close() def test_beam_minor(): f = FITSFigure(HDU) f.show_grayscale() f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_minor(0.05) f.beam.set_minor(0.08) f.close() @pytest.mark.parametrize('quantity', [u.arcsec, 5*u.arcsec, 1*u.degree, 1*u.radian]) def test_beam_minor_quantity(quantity): f = FITSFigure(HDU) f.show_grayscale() f.add_beam(major=0.1, minor=quantity, angle=10.) assert type(f.beam) != list f.beam.set_minor(quantity) f.close() def test_beam_angle(): f = FITSFigure(HDU) f.show_grayscale() f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_angle(0.) f.beam.set_angle(55.) f.close() @pytest.mark.parametrize('quantity', [u.arcsec, 5*u.arcsec, 1*u.degree, 1*u.radian]) def test_beam_angle_quantity(quantity): f = FITSFigure(HDU) f.show_grayscale() f.add_beam(major=0.1, minor=0.04, angle=quantity) f.beam.set_angle(quantity) f.close() def test_beam_corner(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) for corner in ['top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right']: f.beam.set_corner(corner) f.close() def test_beam_frame(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_frame(True) f.beam.set_frame(False) f.close() def test_beam_borderpad(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_borderpad(0.1) f.beam.set_borderpad(0.3) f.close() def test_beam_pad(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_pad(0.1) f.beam.set_pad(0.3) f.close() def test_beam_alpha(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_alpha(0.1) f.beam.set_alpha(0.2) f.beam.set_alpha(0.5) f.close() def test_beam_color(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_color('black') f.beam.set_color('#003344') f.beam.set_color((1.0, 0.4, 0.3)) f.close() def test_beam_facecolor(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_facecolor('black') f.beam.set_facecolor('#003344') f.beam.set_facecolor((1.0, 0.4, 0.3)) f.close() def test_beam_edgecolor(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_edgecolor('black') f.beam.set_edgecolor('#003344') f.beam.set_edgecolor((1.0, 0.4, 0.3)) f.close() def test_beam_linestyle(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_linestyle('solid') f.beam.set_linestyle('dotted') f.beam.set_linestyle('dashed') f.close() def test_beam_linewidth(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) f.beam.set_linewidth(0) f.beam.set_linewidth(1) f.beam.set_linewidth(5) f.close() def test_beam_hatch(): f = FITSFigure(HDU) f.add_beam(major=0.1, minor=0.04, angle=10.) for hatch in ['/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*']: f.beam.set_hatch(hatch) f.close() APLpy-1.0/aplpy/tests/test_colorbar.py0000644000077000000240000000417212244700136017763 0ustar tomstaff00000000000000import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from .. import FITSFigure def test_colorbar_invalid(): data = np.zeros((16, 16)) f = FITSFigure(data) with pytest.raises(Exception): f.add_colorbar() # no grayscale/colorscale was shown def test_colorbar_addremove(): data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() f.add_colorbar() f.remove_colorbar() f.add_colorbar() f.close() def test_colorbar_showhide(): data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() f.add_colorbar() f.colorbar.hide() f.colorbar.show() f.close() def test_colorbar_location(): data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() f.add_colorbar() f.colorbar.set_location('top') f.colorbar.set_location('bottom') f.colorbar.set_location('left') f.colorbar.set_location('right') f.close() def test_colorbar_width(): data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() f.add_colorbar() f.colorbar.set_width(0.1) f.colorbar.set_width(0.2) f.colorbar.set_width(0.5) f.close() def test_colorbar_pad(): data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() f.add_colorbar() f.colorbar.set_pad(0.1) f.colorbar.set_pad(0.2) f.colorbar.set_pad(0.5) f.close() def test_colorbar_font(): data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() f.add_colorbar() f.colorbar.set_font(size='small', weight='bold', stretch='normal', family='serif', style='normal', variant='normal') f.close() def test_colorbar_axis_label(): data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() f.add_colorbar() f.colorbar.set_axis_label_text('Flux (MJy/sr)') f.colorbar.set_axis_label_rotation(45.) f.colorbar.set_axis_label_font(size='small', weight='bold', stretch='normal', family='serif', style='normal', variant='normal') f.colorbar.set_axis_label_pad(5.) f.close()APLpy-1.0/aplpy/tests/test_contour.py0000644000077000000240000000067612244700136017656 0ustar tomstaff00000000000000import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from .. import FITSFigure # Test simple contour generation with Numpy example @pytest.mark.parametrize(('filled'), [True, False]) def test_numpy_contour(filled): data = np.arange(256).reshape((16, 16)) f = FITSFigure(data) f.show_grayscale() f.show_contour(data, levels=np.linspace(1., 254., 10), filled=filled) f.close() APLpy-1.0/aplpy/tests/test_convolve.py0000644000077000000240000000226112470074275020021 0ustar tomstaff00000000000000import os import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from astropy.io import fits from astropy.wcs import WCS as AstropyWCS from .helpers import generate_file, generate_hdu, generate_wcs from .. import FITSFigure def test_convolve_default(): data = np.random.random((16, 16)) hdu = fits.PrimaryHDU(data) f = FITSFigure(hdu) f.show_grayscale(smooth=3) f.close() def test_convolve_gauss(): data = np.random.random((16, 16)) hdu = fits.PrimaryHDU(data) f = FITSFigure(hdu) f.show_grayscale(kernel='gauss', smooth=3) f.close() def test_convolve_box(): data = np.random.random((16, 16)) hdu = fits.PrimaryHDU(data) f = FITSFigure(hdu) f.show_grayscale(kernel='box', smooth=3) f.close() def test_convolve_custom(): data = np.random.random((16, 16)) hdu = fits.PrimaryHDU(data) f = FITSFigure(hdu) f.show_grayscale(kernel=np.ones((3,3))) f.close() def test_convolve_default(): # Regression test for aplpy/aplpy#165 data = np.ones((16, 16), dtype=int) hdu = fits.PrimaryHDU(data) f = FITSFigure(hdu) f.show_grayscale(smooth=3) f.close() APLpy-1.0/aplpy/tests/test_downsample.py0000644000077000000240000000051312244700136020324 0ustar tomstaff00000000000000import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from .. import FITSFigure # Test simple contour generation with Numpy example def test_numpy_downsample(): data = np.arange(256).reshape((16, 16)) f = FITSFigure(data, downsample=2) f.show_grayscale() f.close() APLpy-1.0/aplpy/tests/test_frame.py0000644000077000000240000000100012244700136017235 0ustar tomstaff00000000000000import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from .. import FITSFigure def test_frame_linewidth(): data = np.zeros((16, 16)) f = FITSFigure(data) f.frame.set_linewidth(0) f.frame.set_linewidth(1) f.frame.set_linewidth(10) f.close() def test_frame_color(): data = np.zeros((16, 16)) f = FITSFigure(data) f.frame.set_color('black') f.frame.set_color('#003344') f.frame.set_color((1.0, 0.4, 0.3)) f.close() APLpy-1.0/aplpy/tests/test_grid.py0000644000077000000240000000313012244700136017076 0ustar tomstaff00000000000000import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from .. import FITSFigure def test_grid_addremove(): data = np.zeros((16, 16)) f = FITSFigure(data) f.add_grid() f.remove_grid() f.add_grid() f.close() def test_grid_showhide(): data = np.zeros((16, 16)) f = FITSFigure(data) f.add_grid() f.grid.hide() f.grid.show() f.close() def test_grid_spacing(): data = np.zeros((16, 16)) f = FITSFigure(data) f.add_grid() f.grid.set_xspacing(1.) f.grid.set_xspacing('tick') with pytest.raises(ValueError): f.grid.set_xspacing('auto') f.grid.set_yspacing(2.) f.grid.set_yspacing('tick') with pytest.raises(ValueError): f.grid.set_yspacing('auto') f.close() def test_grid_color(): data = np.zeros((16, 16)) f = FITSFigure(data) f.add_grid() f.grid.set_color('black') f.grid.set_color('#003344') f.grid.set_color((1.0, 0.4, 0.3)) f.close() def test_grid_alpha(): data = np.zeros((16, 16)) f = FITSFigure(data) f.add_grid() f.grid.set_alpha(0.0) f.grid.set_alpha(0.3) f.grid.set_alpha(1.0) f.close() def test_grid_linestyle(): data = np.zeros((16, 16)) f = FITSFigure(data) f.add_grid() f.grid.set_linestyle('solid') f.grid.set_linestyle('dashed') f.grid.set_linestyle('dotted') f.close() def test_grid_linewidth(): data = np.zeros((16, 16)) f = FITSFigure(data) f.add_grid() f.grid.set_linewidth(0) f.grid.set_linewidth(2) f.grid.set_linewidth(5) f.close() APLpy-1.0/aplpy/tests/test_images.py0000644000077000000240000001363112471102066017425 0ustar tomstaff00000000000000import os import shutil import tempfile import numpy as np from matplotlib.testing.compare import compare_images from astropy.tests.helper import pytest from .. import FITSFigure from .helpers import generate_file class BaseImageTests(object): @classmethod def setup_class(cls): cls._moduledir = os.path.dirname(__file__) cls._data_dir = os.path.abspath(os.path.join(cls._moduledir, 'data')) cls._baseline_images_dir = os.path.abspath(os.path.join(cls._moduledir, 'baseline_images')) header_1 = os.path.join(cls._data_dir, '2d_fits/1904-66_AIR.hdr') cls.filename_1 = generate_file(header_1, str(tempfile.mkdtemp())) header_2 = os.path.join(cls._data_dir, '2d_fits/2MASS_k.hdr') cls.filename_2 = generate_file(header_2, str(tempfile.mkdtemp())) header_3 = os.path.join(cls._data_dir, '3d_fits/cube.hdr') cls.filename_3 = generate_file(header_3, str(tempfile.mkdtemp())) # method to create baseline or test images def generate_or_test(self, generate, figure, image, adjust_bbox=True, tolerance=1): if generate is None: result_dir = tempfile.mkdtemp() test_image = os.path.abspath(os.path.join(result_dir, image)) # distutils will put the baseline images in non-accessible places, # copy to our tmpdir to be sure to keep them in case of failure orig_baseline_image = os.path.abspath(os.path.join(self._baseline_images_dir, image)) baseline_image = os.path.abspath(os.path.join(result_dir, 'baseline-'+image)) shutil.copyfile(orig_baseline_image, baseline_image) figure.save(test_image) if not os.path.exists(baseline_image): raise Exception("""Image file not found for comparision test Generated Image: \t{test} This is expected for new tests.""".format( test=test_image)) msg = compare_images(baseline_image, test_image, tol=tolerance) if msg is None: shutil.rmtree(result_dir) else: raise Exception(msg) else: figure.save(os.path.abspath(os.path.join(generate, image)), adjust_bbox=adjust_bbox) pytest.skip("Skipping test, since generating data") class TestBasic(BaseImageTests): # Test for showing grayscale def test_basic_image(self, generate): f = FITSFigure(self.filename_2) f.show_grayscale() self.generate_or_test(generate, f, 'basic_image.png') f.close() def test_ticks_labels_options(self, generate): f = FITSFigure(self.filename_2) f.ticks.set_color('black') f.axis_labels.set_xposition('top') f.axis_labels.set_yposition('right') f.axis_labels.set_font(size='medium', weight='medium', stretch='normal', style='normal') f.tick_labels.set_xformat('dd:mm:ss.ss') f.tick_labels.set_yformat('hh:mm:ss.ss') f.tick_labels.set_style('colons') f.ticks.set_xspacing(0.2) f.ticks.set_yspacing(0.2) f.ticks.set_minor_frequency(10) self.generate_or_test(generate, f, 'tick_labels_options.png') f.close() # Test for showing colorscale def test_show_colorbar_scalebar_beam(self, generate): f = FITSFigure(self.filename_1) f.ticks.set_color('black') f.show_colorscale(vmin=-0.1, vmax=0.1) f.add_colorbar() f.add_scalebar(7.5) f.add_beam(major=0.5, minor=0.2, angle=10.) f.tick_labels.hide() self.generate_or_test(generate, f, 'colorbar_scalebar_beam.png') f.close() # Test for overlaying shapes def test_overlay_shapes(self, generate): f = FITSFigure(self.filename_1) f.ticks.set_color('black') f.show_markers([360., 350., 340.], [-61., -62., -63]) f.show_ellipses(330., -66., 0.15, 2., 10.) f.show_rectangles([355., 350.], [-71, -72], [0.5, 1], [2, 1]) f.show_arrows([340., 360], [-72, -68], [2, -2], [2, 2]) f.add_label(350., -66., 'text') f.add_label(0.4, 0.25, 'text', relative=True) f.frame.set_linewidth(1) # points f.frame.set_color('black') f.axis_labels.hide() self.generate_or_test(generate, f, 'overlay_shapes.png') f.close() # Test for grid def test_grid(self, generate): f = FITSFigure(self.filename_1) f.ticks.set_color('black') f.add_grid() f.grid.set_color('red') f.grid.set_alpha(0.8) f.grid.set_linestyle('solid') f.grid.set_xspacing('tick') f.grid.set_yspacing(3) self.generate_or_test(generate, f, 'grid.png') f.close() # Test recenter def test_recenter(self, generate): f = FITSFigure(self.filename_2) f.ticks.set_color('black') f.recenter(266.5, -29.0, width=0.1, height=0.1) f.axis_labels.set_xpad(20) f.axis_labels.set_ypad(20) self.generate_or_test(generate, f, 'recenter.png') f.close() # Test overlaying contours def test_contours(self, generate): data = np.arange(256).reshape((16, 16)) f = FITSFigure(data) f.ticks.set_color('black') f.show_contour(data, levels=np.linspace(1., 254., 10), filled=False) self.generate_or_test(generate, f, 'contours.png') f.close() # Test cube slice def test_cube_slice(self, generate): f = FITSFigure(self.filename_3, dimensions=[2, 0], slices=[10]) f.ticks.set_color('black') f.add_grid() f.grid.set_color('black') f.grid.set_linestyle('solid') f.grid.set_xspacing(250) f.grid.set_yspacing(0.01) f.tick_labels.set_xformat('%g') f.tick_labels.set_yformat('dd:mm:ss.ss') self.generate_or_test(generate, f, 'cube_slice.png') f.close() APLpy-1.0/aplpy/tests/test_init_cube.py0000644000077000000240000000731512471065325020131 0ustar tomstaff00000000000000import os import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from astropy.io import fits import astropy.utils.exceptions as aue from .helpers import generate_file, generate_hdu, generate_wcs from .. import FITSFigure # The tests in this file check that the initialization and basic plotting do # not crash for FITS files with 3+ dimensions. No reference images are # required here. header_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/3d_fits') HEADERS = [os.path.join(header_dir, 'cube.hdr')] REFERENCE = os.path.join(header_dir, 'cube.hdr') VALID_DIMENSIONS = [(0, 1), (1, 0), (0, 2), (2, 0), (1, 2), (2, 1)] INVALID_DIMENSIONS = [None, (1,), (0, 3), (-4, 2), (1, 1), (2, 2), (3, 3), (1, 2, 3), (3, 5, 3, 2)] # Test initialization through a filename def test_file_init(tmpdir): filename = generate_file(REFERENCE, str(tmpdir)) f = FITSFigure(filename, slices=[5]) f.show_grayscale() f.close() # Test initialization through an HDU object def test_hdu_init(): hdu = generate_hdu(REFERENCE) f = FITSFigure(hdu, slices=[5]) f.show_grayscale() f.close() # Test initialization through a WCS object (should not work) def test_wcs_init(): wcs = generate_wcs(REFERENCE) exc = ValueError if hasattr(wcs,'naxis1') else aue.AstropyDeprecationWarning with pytest.raises(exc): FITSFigure(wcs, slices=[5]) # Test initialization through an HDU object (no WCS) def test_hdu_nowcs_init(): data = np.zeros((16, 16, 16)) hdu = fits.PrimaryHDU(data) f = FITSFigure(hdu, slices=[5]) f.show_grayscale() f.close() # Test initalization through a Numpy array (no WCS) def test_numpy_nowcs_init(): data = np.zeros((16, 16, 16)) f = FITSFigure(data, slices=[5]) f.show_grayscale() f.close() # Test that initialization without specifying slices raises an exception for a # true 3D cube def test_hdu_noslices(): hdu = generate_hdu(REFERENCE) with pytest.raises(Exception): FITSFigure(hdu) # Test that initialization without specifying slices does *not* raise an # exception if the remaining dimensions have size 1. def test_hdu_noslices_2d(): data = np.zeros((1, 16, 16)) f = FITSFigure(data) f.show_grayscale() f.close() # Now check initialization with valid and invalid dimensions. We just need to # tes with HDU objects since we already tested that reading from files is ok. # Test initialization with valid dimensions @pytest.mark.parametrize(('dimensions'), VALID_DIMENSIONS) def test_init_dimensions_valid(dimensions): hdu = generate_hdu(REFERENCE) f = FITSFigure(hdu, dimensions=dimensions, slices=[5]) f.show_grayscale() f.close() # Test initialization with invalid dimensions @pytest.mark.parametrize(('dimensions'), INVALID_DIMENSIONS) def test_init_dimensions_invalid(dimensions): hdu = generate_hdu(REFERENCE) with pytest.raises(ValueError): FITSFigure(hdu, dimensions=dimensions, slices=[5]) # Now check initialization of different WCS projections, and we check only # valid dimensions valid_parameters = [] for h in HEADERS: for d in VALID_DIMENSIONS: valid_parameters.append((h, d)) @pytest.mark.parametrize(('header', 'dimensions'), valid_parameters) def test_init_extensive_wcs(tmpdir, header, dimensions): filename = generate_file(header, str(tmpdir)) f = FITSFigure(filename, dimensions=dimensions, slices=[5]) f.show_grayscale() f.close() # Test that recenter works for cube slices def test_hdu_nowcs_init(): data = np.zeros((16, 16, 16)) hdu = fits.PrimaryHDU(data) f = FITSFigure(hdu, slices=[5]) f.show_grayscale() f.recenter(5., 5., width=3., height=3.) f.close() APLpy-1.0/aplpy/tests/test_init_image.py0000644000077000000240000001342712470074275020301 0ustar tomstaff00000000000000import os import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from astropy.io import fits from astropy.wcs import WCS as AstropyWCS import astropy.utils.exceptions as aue from .helpers import generate_file, generate_hdu, generate_wcs from .. import FITSFigure # The tests in this file check that the initialization and basic plotting do # not crash for FITS files with 2 dimensions. No reference images are # required here. header_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/2d_fits') HEADERS = [os.path.join(header_dir, '1904-66_AIR.hdr'), os.path.join(header_dir, '1904-66_AIT.hdr'), os.path.join(header_dir, '1904-66_ARC.hdr'), os.path.join(header_dir, '1904-66_AZP.hdr'), os.path.join(header_dir, '1904-66_BON.hdr'), os.path.join(header_dir, '1904-66_CAR.hdr'), os.path.join(header_dir, '1904-66_CEA.hdr'), os.path.join(header_dir, '1904-66_COD.hdr'), os.path.join(header_dir, '1904-66_COE.hdr'), os.path.join(header_dir, '1904-66_COO.hdr'), os.path.join(header_dir, '1904-66_COP.hdr'), os.path.join(header_dir, '1904-66_CSC.hdr'), os.path.join(header_dir, '1904-66_CYP.hdr'), os.path.join(header_dir, '1904-66_HPX.hdr'), os.path.join(header_dir, '1904-66_MER.hdr'), os.path.join(header_dir, '1904-66_MOL.hdr'), os.path.join(header_dir, '1904-66_NCP.hdr'), os.path.join(header_dir, '1904-66_PAR.hdr'), os.path.join(header_dir, '1904-66_PCO.hdr'), os.path.join(header_dir, '1904-66_QSC.hdr'), os.path.join(header_dir, '1904-66_SFL.hdr'), os.path.join(header_dir, '1904-66_SIN.hdr'), os.path.join(header_dir, '1904-66_STG.hdr'), os.path.join(header_dir, '1904-66_SZP.hdr'), os.path.join(header_dir, '1904-66_TAN.hdr'), os.path.join(header_dir, '1904-66_TSC.hdr'), os.path.join(header_dir, '1904-66_ZEA.hdr'), os.path.join(header_dir, '1904-66_ZPN.hdr')] REFERENCE = os.path.join(header_dir, '1904-66_TAN.hdr') CAR_REFERENCE = os.path.join(header_dir, '1904-66_CAR.hdr') VALID_DIMENSIONS = [(0, 1), (1, 0)] INVALID_DIMENSIONS = [None, (1,), (0, 2), (-4, 2), (1, 1), (2, 2), (1, 2, 3)] # Test initialization through a filename def test_file_init(tmpdir): filename = generate_file(REFERENCE, str(tmpdir)) f = FITSFigure(filename) f.show_grayscale() f.close() # Test initialization through an HDU object def test_hdu_init(): hdu = generate_hdu(REFERENCE) f = FITSFigure(hdu) f.show_grayscale() f.close() # Test initialization through a WCS object def test_wcs_init(): wcs = generate_wcs(REFERENCE) if hasattr(wcs,'naxis1'): f = FITSFigure(wcs) f.show_grayscale() f.close() else: with pytest.raises(aue.AstropyDeprecationWarning): f = FITSFigure(wcs) # Test initialization through a WCS object with wcs.to_header() as a go-between # specifically for testing the cd -> pc -> cd hack, and has particular importance # for AVM-generated headers def test_wcs_toheader_init(): wcs = generate_wcs(REFERENCE) header_ = fits.Header.fromtextfile(REFERENCE) header = wcs.to_header() wcs2 = AstropyWCS(header) wcs2.naxis1 = wcs.naxis1 = header_['NAXIS1'] wcs2.naxis2 = wcs.naxis2 = header_['NAXIS2'] f = FITSFigure(wcs2) f.show_grayscale() f.add_grid() f.close() # Test initialization through an HDU object (no WCS) def test_hdu_nowcs_init(): data = np.zeros((16, 16)) hdu = fits.PrimaryHDU(data) f = FITSFigure(hdu) f.show_grayscale() f.close() # Test initalization through a Numpy array (no WCS) def test_numpy_nowcs_init(): data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() f.close() # Now check initialization with valid and invalid dimensions. We just need to # tes with HDU objects since we already tested that reading from files is ok. # Test initialization with valid dimensions @pytest.mark.parametrize(('dimensions'), VALID_DIMENSIONS) def test_init_dimensions_valid(dimensions): hdu = generate_hdu(REFERENCE) f = FITSFigure(hdu, dimensions=dimensions) f.show_grayscale() f.close() # Test initialization with invalid dimensions @pytest.mark.parametrize(('dimensions'), INVALID_DIMENSIONS) def test_init_dimensions_invalid(dimensions): hdu = generate_hdu(REFERENCE) with pytest.raises(ValueError): FITSFigure(hdu, dimensions=dimensions) # Now check initialization of different WCS projections, and we check only # valid dimensions valid_parameters = [] for h in HEADERS: for d in VALID_DIMENSIONS: valid_parameters.append((h, d)) @pytest.mark.parametrize(('header', 'dimensions'), valid_parameters) def test_init_extensive_wcs(header, dimensions): hdu = generate_hdu(header) if 'CAR' in header: f = FITSFigure(hdu, dimensions=dimensions, convention='calabretta') else: f = FITSFigure(hdu, dimensions=dimensions) f.show_grayscale() f.add_grid() f.close() # Check that for CAR projections, an exception is raised if no convention is specified @pytest.mark.parametrize(('dimensions'), VALID_DIMENSIONS) def test_init_car_invalid(dimensions): hdu = generate_hdu(CAR_REFERENCE) with pytest.raises(Exception): FITSFigure(hdu, dimensions=dimensions) # Check that images containing only NaN or Inf values don't crash FITSFigure def test_init_only_naninf(): data = np.ones((10,10)) * np.nan data[::2,::2] = np.inf f = FITSFigure(data) f.show_grayscale() f.show_colorscale() def test_init_single_pixel(): data = np.zeros((4,4)) data[...] = np.nan data[2,2] = 1 f = FITSFigure(data) f.show_grayscale() APLpy-1.0/aplpy/tests/test_misc.py0000644000077000000240000000105112470074275017115 0ustar tomstaff00000000000000import numpy as np from astropy.io import fits from ..core import FITSFigure def test_nan_color_copy(): """ Regression test to ensure that NaN values set in one image don't affect global Matplotlib colormap. """ data = np.zeros((16, 16)) f1 = FITSFigure(data) f1.show_grayscale() f1.set_nan_color('blue') f2 = FITSFigure(data) f2.show_grayscale() f2.set_nan_color('red') assert f1.image.get_cmap()._rgba_bad == (0.0, 0.0, 1.0, 1.0) assert f2.image.get_cmap()._rgba_bad == (1.0, 0.0, 0.0, 1.0) APLpy-1.0/aplpy/tests/test_pixworld.py0000644000077000000240000000430412244700136020025 0ustar tomstaff00000000000000import os import matplotlib matplotlib.use('Agg') import numpy as np from astropy.table import Table from astropy.tests.helper import pytest from .helpers import generate_wcs from .. import wcs_util HEADER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/2d_fits', '1904-66_TAN.hdr') tab = Table({'RA':[347.,349.], 'DEC':[-68.,-68]}) GOOD_INPUT = [ [1.,2.], [[1.,2.],[3,4]], [np.arange(2), np.arange(2)], [tab['RA'], tab['DEC']] ] BAD_INPUT = [ [1,['s','w']], [np.arange(2), np.sum], [tab['RA'], 'ewr'] ] @pytest.mark.parametrize(('inputval'), GOOD_INPUT) def test_pixworld_input_and_consistency(inputval): wcs = generate_wcs(HEADER) ra, dec = wcs_util.pix2world(wcs, inputval[0], inputval[1]) x, y = wcs_util.world2pix(wcs, ra, dec) # For some inputs (e.g. list) a-b is not defined # so change them all to np.ndarrays here to make sure assert np.all(np.abs(np.array(x) - np.array(inputval[0])) < 1e-5) assert np.all(np.abs(np.array(y) - np.array(inputval[1])) < 1e-5) def test_returntypes(): wcs = generate_wcs(HEADER) ra, dec = wcs_util.pix2world(wcs, 1.,2.) assert np.isscalar(ra) and np.isscalar(dec) ra, dec = wcs_util.pix2world(wcs, [1.],[2.]) assert (type(ra) == list) and (type(dec) == list) # Astropy.table.Column objects get donwconverted np.ndarray ra, dec = wcs_util.pix2world(wcs, np.arange(2), tab['DEC']) assert isinstance(ra, np.ndarray) and isinstance(dec, np.ndarray) @pytest.mark.parametrize(('inputval'), BAD_INPUT) def test_pix2world_fail(inputval): wcs = generate_wcs(HEADER) with pytest.raises(Exception) as exc: wcs_util.pix2world(wcs, inputval[0], inputval[1]) assert exc.value.args[0] == "pix2world should be provided either with two scalars, two lists, or two numpy arrays" @pytest.mark.parametrize(('inputval'), BAD_INPUT) def test_world2pix_fail(inputval): wcs = generate_wcs(HEADER) with pytest.raises(Exception) as exc: wcs_util.world2pix(wcs, inputval[0], inputval[1]) assert exc.value.args[0] == "world2pix should be provided either with two scalars, two lists, or two numpy arrays" APLpy-1.0/aplpy/tests/test_pixworldmarkers.py0000644000077000000240000000373312471065325021425 0ustar tomstaff00000000000000import os import matplotlib matplotlib.use('Agg') import numpy as np from astropy.table import Table from astropy.tests.helper import pytest from astropy.io import fits from .helpers import generate_wcs from .. import FITSFigure HEADER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/2d_fits', '1904-66_TAN.hdr') tab = Table({'RA':[347.,349.], 'DEC':[-68.,-68]}) GOOD_INPUT = [ [1,2], [[1,2],[3,4]], [np.arange(2), np.arange(2)], [tab['RA'], tab['DEC']] ] BAD_INPUT = [ [1,['s', 'e']], [np.arange(2), np.sum], [tab['RA'], 'ewr'] ] @pytest.mark.parametrize(('inputval'), GOOD_INPUT) def test_pixel_coords(inputval): data = np.zeros((16, 16)) f = FITSFigure(data) f.show_markers(inputval[0], inputval[1]) f.close() @pytest.mark.parametrize(('inputval'), GOOD_INPUT) def test_wcs_coords(inputval): wcs = generate_wcs(HEADER) header = fits.Header.fromtextfile(HEADER) wcs.naxis1 = header['NAXIS1'] wcs.naxis2 = header['NAXIS2'] f = FITSFigure(wcs) f.show_markers(inputval[0], inputval[1]) f.close() @pytest.mark.parametrize(('inputval'), BAD_INPUT) def test_pixel_coords_bad(inputval): data = np.zeros((16, 16)) f = FITSFigure(data) with pytest.raises(Exception) as exc: f.show_markers(inputval[0], inputval[1]) assert exc.value.args[0] == "world2pix should be provided either with two scalars, two lists, or two numpy arrays" f.close() @pytest.mark.parametrize(('inputval'), BAD_INPUT) def test_wcs_coords_bad(inputval): wcs = generate_wcs(HEADER) header = fits.Header.fromtextfile(HEADER) wcs.naxis1 = header['NAXIS1'] wcs.naxis2 = header['NAXIS2'] f = FITSFigure(wcs) with pytest.raises(Exception) as exc: f.show_markers(inputval[0], inputval[1]) f.close() assert exc.value.args[0] == "world2pix should be provided either with two scalars, two lists, or two numpy arrays" APLpy-1.0/aplpy/tests/test_rgb.py0000644000077000000240000000270512471065347016744 0ustar tomstaff00000000000000import os import warnings import matplotlib matplotlib.use('Agg') import numpy as np from astropy.io import fits from .. import FITSFigure from ..rgb import make_rgb_image from .test_images import BaseImageTests HEADER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/2d_fits', '1904-66_TAN.hdr') class TestRGB(BaseImageTests): def test_rgb(self, generate, tmpdir): # Regression test to check that RGB recenter works properly r_file = tmpdir.join('r.fits').strpath g_file = tmpdir.join('g.fits').strpath b_file = tmpdir.join('b.fits').strpath rgb_file = tmpdir.join('rgb.png').strpath np.random.seed(12345) header = fits.Header.fromtextfile(HEADER) r = fits.PrimaryHDU(np.random.random((12,12)), header) r.writeto(r_file) g = fits.PrimaryHDU(np.random.random((12,12)), header) g.writeto(g_file) b = fits.PrimaryHDU(np.random.random((12,12)), header) b.writeto(b_file) make_rgb_image([r_file, g_file, b_file], rgb_file, embed_avm_tags=False) f = FITSFigure(r_file, figsize=(3,3)) with warnings.catch_warnings(): warnings.simplefilter("ignore") f.show_rgb(rgb_file) f.tick_labels.set_xformat('dd.d') f.tick_labels.set_yformat('dd.d') f.recenter(359.3, -72.1, radius=0.05) self.generate_or_test(generate, f, 'test_rgb.png', tolerance=2) f.close() APLpy-1.0/aplpy/tests/test_save.py0000644000077000000240000000560712470074275017133 0ustar tomstaff00000000000000import os import sys from astropy.extern import six if six.PY3: from io import BytesIO as StringIO else: from StringIO import StringIO import numpy as np from astropy.tests.helper import pytest from .. import FITSFigure FORMATS = [None, 'png', 'pdf', 'eps', 'ps', 'svg'] def is_format(filename, format): if isinstance(filename, six.string_types): f = open(filename, 'rb') else: f = filename if format == 'png': return f.read(8) == b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a' elif format == 'pdf': return f.read(4) == b'\x25\x50\x44\x46' elif format == 'eps': return f.read(23) == b'%!PS-Adobe-3.0 EPSF-3.0' elif format == 'ps': return f.read(14) == b'%!PS-Adobe-3.0' elif format == 'svg': from xml.dom import minidom return minidom.parse(f).childNodes[2].attributes['xmlns'].value == 'http://www.w3.org/2000/svg' else: raise Exception("Unknown format: %s" % format) @pytest.mark.parametrize(('format'), FORMATS) def test_write_png(tmpdir, format): filename = os.path.join(str(tmpdir), 'test_output.png') data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() try: f.save(filename, format=format) except TypeError: pytest.xfail() finally: f.close() if format is None: assert is_format(filename, 'png') else: assert is_format(filename, format) @pytest.mark.parametrize(('format'), FORMATS) def test_write_pdf(tmpdir, format): filename = os.path.join(str(tmpdir), 'test_output.pdf') data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() try: f.save(filename, format=format) except TypeError: pytest.xfail() finally: f.close() if format is None: assert is_format(filename, 'pdf') else: assert is_format(filename, format) @pytest.mark.parametrize(('format'), FORMATS) def test_write_eps(tmpdir, format): filename = os.path.join(str(tmpdir), 'test_output.eps') data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() try: f.save(filename, format=format) except TypeError: pytest.xfail() finally: f.close() if format is None: assert is_format(filename, 'eps') else: assert is_format(filename, format) @pytest.mark.parametrize(('format'), FORMATS) def test_write_stringio(tmpdir, format): s = StringIO() data = np.zeros((16, 16)) f = FITSFigure(data) f.show_grayscale() try: f.save(s, format=format) except TypeError: pytest.xfail() finally: f.close() try: s.seek(0) except ValueError: if format == 'svg' and sys.version_info[:2] >= (3, 3): pytest.xfail() else: raise if format is None: assert is_format(s, 'png') else: assert is_format(s, format) APLpy-1.0/aplpy/tests/test_scalebar.py0000644000077000000240000000457412470074275017753 0ustar tomstaff00000000000000import os import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from astropy import units as u from astropy.io import fits from .. import FITSFigure header_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/2d_fits') HEADER = fits.Header.fromtextfile(os.path.join(header_dir, '1904-66_TAN.hdr')) HDU = fits.PrimaryHDU(np.zeros((16, 16)), HEADER) def test_scalebar_add_invalid(): f = FITSFigure(HDU) with pytest.raises(TypeError): f.add_scalebar() def test_scalebar_addremove(): f = FITSFigure(HDU) f.add_scalebar(0.1) f.remove_scalebar() f.add_scalebar(0.1) f.close() def test_scalebar_showhide(): f = FITSFigure(HDU) f.add_scalebar(0.1) f.scalebar.hide() f.scalebar.show(0.1) f.close() def test_scalebar_length(): f = FITSFigure(HDU) f.add_scalebar(0.1) f.scalebar.set_length(0.01) f.scalebar.set_length(0.1) f.close() @pytest.mark.parametrize('quantity', [1*u.arcsec, u.arcsec, 2*u.degree, 5*u.radian]) def test_scalebar_length_quantity(quantity): f = FITSFigure(HDU) f.add_scalebar(quantity) f.scalebar.set_length(quantity) f.close() def test_scalebar_label(): f = FITSFigure(HDU) f.add_scalebar(0.1) f.scalebar.set_label('1 pc') f.scalebar.set_label('5 AU') f.scalebar.set_label('2"') f.close() def test_scalebar_corner(): f = FITSFigure(HDU) f.add_scalebar(0.1) for corner in ['top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right']: f.scalebar.set_corner(corner) f.close() def test_scalebar_frame(): f = FITSFigure(HDU) f.add_scalebar(0.1) f.scalebar.set_frame(True) f.scalebar.set_frame(False) f.close() def test_scalebar_color(): f = FITSFigure(HDU) f.add_scalebar(0.1) f.scalebar.set_color('black') f.scalebar.set_color('#003344') f.scalebar.set_color((1.0, 0.4, 0.3)) f.close() def test_scalebar_alpha(): f = FITSFigure(HDU) f.add_scalebar(0.1) f.scalebar.set_alpha(0.1) f.scalebar.set_alpha(0.2) f.scalebar.set_alpha(0.5) f.close() def test_scalebar_font(): f = FITSFigure(HDU) f.add_scalebar(0.1) f.scalebar.set_font(size='small', weight='bold', stretch='normal', family='serif', style='normal', variant='normal') f.close() APLpy-1.0/aplpy/tests/test_subplot.py0000644000077000000240000000117312244700136017646 0ustar tomstaff00000000000000import pytest import numpy as np from .. import FITSFigure def test_subplot_grid(): f = FITSFigure(np.zeros((10, 10)), subplot=(2, 2, 1)) f.show_grayscale() f.close() def test_subplot_box(): f = FITSFigure(np.zeros((10, 10)), subplot=[0.1, 0.1, 0.8, 0.8]) f.show_grayscale() f.close() @pytest.mark.parametrize('subplot', [(1, 2, 3, 4), [1, 2, 3], '111', 1.2]) def test_subplot_invalid(subplot): with pytest.raises(ValueError) as exc: FITSFigure(np.zeros((10, 10)), subplot=subplot) assert exc.value.args[0] == "subplot= should be either a tuple of three values, or a list of four values" APLpy-1.0/aplpy/tests/test_tick_labels.py0000644000077000000240000000276212244700136020437 0ustar tomstaff00000000000000import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from .. import FITSFigure def test_tick_labels_show_hide(): data = np.zeros((16, 16)) f = FITSFigure(data) f.tick_labels.hide() f.tick_labels.show() f.tick_labels.hide_x() f.tick_labels.show_x() f.tick_labels.hide_y() f.tick_labels.show_y() f.close() def test_tick_labels_format_scalar(): data = np.zeros((16, 16)) f = FITSFigure(data) f.tick_labels.set_xformat('%i') f.tick_labels.set_yformat('%i') f.close() def test_tick_labels_position(): data = np.zeros((16, 16)) f = FITSFigure(data) f.tick_labels.set_xposition('top') f.tick_labels.set_xposition('bottom') f.tick_labels.set_yposition('right') f.tick_labels.set_yposition('left') f.close() def test_tick_labels_position_invalid(): data = np.zeros((16, 16)) f = FITSFigure(data) with pytest.raises(ValueError): f.tick_labels.set_xposition('right') with pytest.raises(ValueError): f.tick_labels.set_xposition('left') with pytest.raises(ValueError): f.tick_labels.set_yposition('top') with pytest.raises(ValueError): f.tick_labels.set_yposition('bottom') f.close() def test_tick_labels_font(): data = np.zeros((16, 16)) f = FITSFigure(data) f.tick_labels.set_font(size='small', weight='bold', stretch='normal', family='serif', style='normal', variant='normal') f.close() APLpy-1.0/aplpy/tests/test_ticks.py0000644000077000000240000000246612244700136017301 0ustar tomstaff00000000000000import matplotlib matplotlib.use('Agg') import numpy as np from astropy.tests.helper import pytest from .. import FITSFigure def test_ticks_show_hide(): data = np.zeros((16, 16)) f = FITSFigure(data) f.ticks.hide() f.ticks.show() f.ticks.hide_x() f.ticks.show_x() f.ticks.hide_y() f.ticks.show_y() f.close() def test_ticks_spacing(): data = np.zeros((16, 16)) f = FITSFigure(data) f.ticks.set_xspacing(0.5) f.ticks.set_xspacing(1.) f.ticks.set_yspacing(0.5) f.ticks.set_yspacing(1.) f.close() def test_ticks_length(): data = np.zeros((16, 16)) f = FITSFigure(data) f.ticks.set_length(0) f.ticks.set_length(1) f.ticks.set_length(10) f.close() def test_ticks_color(): data = np.zeros((16, 16)) f = FITSFigure(data) f.ticks.set_color('black') f.ticks.set_color('#003344') f.ticks.set_color((1.0, 0.4, 0.3)) f.close() def test_ticks_linewidth(): data = np.zeros((16, 16)) f = FITSFigure(data) f.ticks.set_linewidth(1) f.ticks.set_linewidth(3) f.ticks.set_linewidth(10) f.close() def test_ticks_minor_frequency(): data = np.zeros((16, 16)) f = FITSFigure(data) f.ticks.set_minor_frequency(1) f.ticks.set_minor_frequency(5) f.ticks.set_minor_frequency(10) f.close() APLpy-1.0/aplpy/tests/test_vectors.py0000644000077000000240000000161512471102127017642 0ustar tomstaff00000000000000import numpy as np from ..core import FITSFigure from .test_images import BaseImageTests x = np.linspace(-1., 1., 10) y = np.linspace(-1., 1., 10) X, Y = np.meshgrid(x, y) np.random.seed(12345) IMAGE = np.random.random((10,10)) PDATA = np.arange(100).reshape((10,10)) / 50. + 0.5 ADATA = np.degrees(np.arctan2(Y, X)) class TestVectors(BaseImageTests): def test_default(self, generate): f = FITSFigure(IMAGE, figsize=(4,4)) f.show_grayscale() f.show_vectors(PDATA, ADATA, color='orange') self.generate_or_test(generate, f, 'vectors_default.png', tolerance=2.5) f.close() def test_step_scale(self, generate): f = FITSFigure(IMAGE, figsize=(4,4)) f.show_grayscale() f.show_vectors(PDATA, ADATA, step=2, scale=0.8, color='orange') self.generate_or_test(generate, f, 'vectors_step_scale.png', tolerance=2.5) f.close() APLpy-1.0/aplpy/tests/test_wcs_util.py0000644000077000000240000000324612471065325020020 0ustar tomstaff00000000000000import os import numpy as np from astropy.io import fits from astropy.tests.helper import pytest from ..wcs_util import WCS, celestial_pixel_scale, non_celestial_pixel_scales from .helpers import generate_wcs HEADER_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') HEADER_2D = fits.Header.fromtextfile(os.path.join(HEADER_DIR, '2d_fits', '1904-66_TAN.hdr')) HEADER_3D = fits.Header.fromtextfile(os.path.join(HEADER_DIR, '3d_fits', 'cube.hdr')) def test_pixel_scale_matrix_1(): wcs = WCS(HEADER_2D) np.testing.assert_allclose(wcs.pixel_scale_matrix, [[-0.06666667, 0.], [0., 0.06666667]]) def test_celestial_scale_1(): wcs = WCS(HEADER_2D) for crota2 in range(0, 360, 20): wcs.wcs.crota = [0.,crota2] np.testing.assert_allclose(celestial_pixel_scale(wcs), 0.06666667) def test_non_celestial_scales_1(): wcs = WCS(HEADER_2D) with pytest.raises(ValueError) as exc: non_celestial_pixel_scales(wcs) assert exc.value.args[0] == "WCS is celestial, use celestial_pixel_scale instead" def test_pixel_scale_matrix_2(): wcs = WCS(HEADER_3D, dimensions=[0,2], slices=[0]) np.testing.assert_allclose(wcs.pixel_scale_matrix, [[-6.38888900e-03, 0.], [0., 6.64236100e+01]]) def test_celestial_scale_2(): wcs = WCS(HEADER_3D, dimensions=[0,2], slices=[0]) with pytest.raises(ValueError) as exc: celestial_pixel_scale(wcs) assert exc.value.args[0] == "WCS is not celestial, cannot determine celestial pixel scale" def test_non_celestial_scales_2(): wcs = WCS(HEADER_3D, dimensions=[0,2], slices=[0]) np.testing.assert_allclose(non_celestial_pixel_scales(wcs), [6.38888900e-03, 6.64236100e+01]) APLpy-1.0/aplpy/ticks.py0000644000077000000240000005201012471065325015074 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import warnings import numpy as np from matplotlib.pyplot import Locator from . import wcs_util from . import angle_util as au from . import scalar_util as su from . import math_util from .decorators import auto_refresh class Ticks(object): @auto_refresh def __init__(self, parent): # Store references to axes self._ax1 = parent._ax1 self._ax2 = parent._ax2 self._wcs = parent._wcs self._figure = parent._figure self._parent = parent # Save plotting parameters (required for @auto_refresh) self._parameters = parent._parameters # Set tick positions self._ax1.yaxis.tick_left() self._ax1.xaxis.tick_bottom() self._ax2.yaxis.tick_right() self._ax2.xaxis.tick_top() # Set tick spacing to default self.set_xspacing('auto') self.set_yspacing('auto') # Set major tick locators lx = WCSLocator(wcs=self._wcs, coord='x') self._ax1.xaxis.set_major_locator(lx) ly = WCSLocator(wcs=self._wcs, coord='y') self._ax1.yaxis.set_major_locator(ly) lxt = WCSLocator(wcs=self._wcs, coord='x', farside=True) self._ax2.xaxis.set_major_locator(lxt) lyt = WCSLocator(wcs=self._wcs, coord='y', farside=True) self._ax2.yaxis.set_major_locator(lyt) # Set minor tick locators lx = WCSLocator(wcs=self._wcs, coord='x', minor=True) self._ax1.xaxis.set_minor_locator(lx) ly = WCSLocator(wcs=self._wcs, coord='y', minor=True) self._ax1.yaxis.set_minor_locator(ly) lxt = WCSLocator(wcs=self._wcs, coord='x', farside=True, minor=True) self._ax2.xaxis.set_minor_locator(lxt) lyt = WCSLocator(wcs=self._wcs, coord='y', farside=True, minor=True) self._ax2.yaxis.set_minor_locator(lyt) @auto_refresh def set_xspacing(self, spacing): ''' Set the x-axis tick spacing, in degrees. To set the tick spacing to be automatically determined, set this to 'auto'. ''' if spacing == 'auto': self._ax1.xaxis.apl_auto_tick_spacing = True self._ax2.xaxis.apl_auto_tick_spacing = True else: self._ax1.xaxis.apl_auto_tick_spacing = False self._ax2.xaxis.apl_auto_tick_spacing = False if self._wcs.xaxis_coord_type in ['longitude', 'latitude']: try: au._check_format_spacing_consistency(self._ax1.xaxis.apl_label_form, au.Angle(degrees=spacing, latitude=self._wcs.xaxis_coord_type == 'latitude')) except au.InconsistentSpacing: warnings.warn("WARNING: Requested tick spacing format cannot be shown by current label format. The tick spacing will not be changed.") return self._ax1.xaxis.apl_tick_spacing = au.Angle(degrees=spacing, latitude=self._wcs.xaxis_coord_type == 'latitude') self._ax2.xaxis.apl_tick_spacing = au.Angle(degrees=spacing, latitude=self._wcs.xaxis_coord_type == 'latitude') else: try: su._check_format_spacing_consistency(self._ax1.xaxis.apl_label_form, spacing) except au.InconsistentSpacing: warnings.warn("WARNING: Requested tick spacing format cannot be shown by current label format. The tick spacing will not be changed.") return self._ax1.xaxis.apl_tick_spacing = spacing self._ax2.xaxis.apl_tick_spacing = spacing if hasattr(self._parent, 'grid'): self._parent.grid._update() @auto_refresh def set_yspacing(self, spacing): ''' Set the y-axis tick spacing, in degrees. To set the tick spacing to be automatically determined, set this to 'auto'. ''' if spacing == 'auto': self._ax1.yaxis.apl_auto_tick_spacing = True self._ax2.yaxis.apl_auto_tick_spacing = True else: self._ax1.yaxis.apl_auto_tick_spacing = False self._ax2.yaxis.apl_auto_tick_spacing = False if self._wcs.yaxis_coord_type in ['longitude', 'latitude']: try: au._check_format_spacing_consistency(self._ax1.yaxis.apl_label_form, au.Angle(degrees=spacing, latitude=self._wcs.yaxis_coord_type == 'latitude')) except au.InconsistentSpacing: warnings.warn("WARNING: Requested tick spacing format cannot be shown by current label format. The tick spacing will not be changed.") return self._ax1.yaxis.apl_tick_spacing = au.Angle(degrees=spacing, latitude=self._wcs.yaxis_coord_type == 'latitude') self._ax2.yaxis.apl_tick_spacing = au.Angle(degrees=spacing, latitude=self._wcs.yaxis_coord_type == 'latitude') else: try: su._check_format_spacing_consistency(self._ax1.yaxis.apl_label_form, spacing) except au.InconsistentSpacing: warnings.warn("WARNING: Requested tick spacing format cannot be shown by current label format. The tick spacing will not be changed.") return self._ax1.yaxis.apl_tick_spacing = spacing self._ax2.yaxis.apl_tick_spacing = spacing if hasattr(self._parent, 'grid'): self._parent.grid._update() @auto_refresh def set_color(self, color): ''' Set the color of the ticks ''' # Major ticks for line in self._ax1.xaxis.get_ticklines(): line.set_color(color) for line in self._ax1.yaxis.get_ticklines(): line.set_color(color) for line in self._ax2.xaxis.get_ticklines(): line.set_color(color) for line in self._ax2.yaxis.get_ticklines(): line.set_color(color) # Minor ticks for line in self._ax1.xaxis.get_minorticklines(): line.set_color(color) for line in self._ax1.yaxis.get_minorticklines(): line.set_color(color) for line in self._ax2.xaxis.get_minorticklines(): line.set_color(color) for line in self._ax2.yaxis.get_minorticklines(): line.set_color(color) @auto_refresh def set_length(self, length, minor_factor=0.5): ''' Set the length of the ticks (in points) ''' # Major ticks for line in self._ax1.xaxis.get_ticklines(): line.set_markersize(length) for line in self._ax1.yaxis.get_ticklines(): line.set_markersize(length) for line in self._ax2.xaxis.get_ticklines(): line.set_markersize(length) for line in self._ax2.yaxis.get_ticklines(): line.set_markersize(length) # Minor ticks for line in self._ax1.xaxis.get_minorticklines(): line.set_markersize(length * minor_factor) for line in self._ax1.yaxis.get_minorticklines(): line.set_markersize(length * minor_factor) for line in self._ax2.xaxis.get_minorticklines(): line.set_markersize(length * minor_factor) for line in self._ax2.yaxis.get_minorticklines(): line.set_markersize(length * minor_factor) @auto_refresh def set_linewidth(self, linewidth): ''' Set the linewidth of the ticks (in points) ''' # Major ticks for line in self._ax1.xaxis.get_ticklines(): line.set_mew(linewidth) for line in self._ax1.yaxis.get_ticklines(): line.set_mew(linewidth) for line in self._ax2.xaxis.get_ticklines(): line.set_mew(linewidth) for line in self._ax2.yaxis.get_ticklines(): line.set_mew(linewidth) # Minor ticks for line in self._ax1.xaxis.get_minorticklines(): line.set_mew(linewidth) for line in self._ax1.yaxis.get_minorticklines(): line.set_mew(linewidth) for line in self._ax2.xaxis.get_minorticklines(): line.set_mew(linewidth) for line in self._ax2.yaxis.get_minorticklines(): line.set_mew(linewidth) @auto_refresh def set_minor_frequency(self, frequency): ''' Set the number of subticks per major tick. Set to one to hide minor ticks. ''' self._ax1.xaxis.get_minor_locator().subticks = frequency self._ax1.yaxis.get_minor_locator().subticks = frequency self._ax2.xaxis.get_minor_locator().subticks = frequency self._ax2.yaxis.get_minor_locator().subticks = frequency @auto_refresh def show(self): """ Show the x- and y-axis ticks """ self.show_x() self.show_y() @auto_refresh def hide(self): """ Hide the x- and y-axis ticks """ self.hide_x() self.hide_y() @auto_refresh def show_x(self): """ Show the x-axis ticks """ for line in self._ax1.xaxis.get_ticklines(): line.set_visible(True) for line in self._ax2.xaxis.get_ticklines(): line.set_visible(True) for line in self._ax1.xaxis.get_minorticklines(): line.set_visible(True) for line in self._ax2.xaxis.get_minorticklines(): line.set_visible(True) @auto_refresh def hide_x(self): """ Hide the x-axis ticks """ for line in self._ax1.xaxis.get_ticklines(): line.set_visible(False) for line in self._ax2.xaxis.get_ticklines(): line.set_visible(False) for line in self._ax1.xaxis.get_minorticklines(): line.set_visible(False) for line in self._ax2.xaxis.get_minorticklines(): line.set_visible(False) @auto_refresh def show_y(self): """ Show the y-axis ticks """ for line in self._ax1.yaxis.get_ticklines(): line.set_visible(True) for line in self._ax2.yaxis.get_ticklines(): line.set_visible(True) for line in self._ax1.yaxis.get_minorticklines(): line.set_visible(True) for line in self._ax2.yaxis.get_minorticklines(): line.set_visible(True) @auto_refresh def hide_y(self): """ Hide the y-axis ticks """ for line in self._ax1.yaxis.get_ticklines(): line.set_visible(False) for line in self._ax2.yaxis.get_ticklines(): line.set_visible(False) for line in self._ax1.yaxis.get_minorticklines(): line.set_visible(False) for line in self._ax2.yaxis.get_minorticklines(): line.set_visible(False) class WCSLocator(Locator): def __init__(self, presets=None, wcs=False, coord='x', farside=False, minor=False, subticks=5): if presets is None: self.presets = {} else: self.presets = presets self._wcs = wcs self.coord = coord self.farside = farside self.minor = minor self.subticks = subticks def __call__(self): self.coord_type = self._wcs.xaxis_coord_type if self.coord == 'x' else self._wcs.yaxis_coord_type ymin, ymax = self.axis.get_axes().yaxis.get_view_interval() xmin, xmax = self.axis.get_axes().xaxis.get_view_interval() if self.axis.apl_auto_tick_spacing: self.axis.apl_tick_spacing = default_spacing(self.axis.get_axes(), self.coord, self.axis.apl_label_form) if self.axis.apl_tick_spacing is None: self.axis.apl_tick_positions_pix = [] self.axis.apl_tick_positions_world = [] return [] if self.coord_type in ['longitude', 'latitude']: tick_spacing = self.axis.apl_tick_spacing.todegrees() else: tick_spacing = self.axis.apl_tick_spacing if self.minor: tick_spacing /= float(self.subticks) px, py, wx = tick_positions(self._wcs, tick_spacing, self.coord, self.coord, farside=self.farside, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, mode='xscaled') px, py, wx = np.array(px, float), np.array(py, float), np.array(wx, int) if self.minor: keep = np.mod(wx, self.subticks) > 0 px, py, wx = px[keep], py[keep], wx[keep] / float(self.subticks) self.axis.apl_tick_positions_world = np.array(wx, int) if self.coord == 'x': self.axis.apl_tick_positions_pix = px else: self.axis.apl_tick_positions_pix = py return self.axis.apl_tick_positions_pix def default_spacing(ax, coord, format): wcs = ax._wcs xmin, xmax = ax.xaxis.get_view_interval() ymin, ymax = ax.yaxis.get_view_interval() px, py, wx, wy = axis_positions(wcs, coord, False, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax) # Keep only pixels that fall inside the sky. This will only really work # for PyWCS 0.11 or more recent keep = ~np.isnan(wx) & ~np.isnan(wy) if np.sum(keep) == 0: return None else: px = px[keep] py = py[keep] wx = wx[keep] wy = wy[keep] coord_type = wcs.xaxis_coord_type if coord == 'x' else wcs.yaxis_coord_type if coord == 'x': # The following is required because PyWCS 0.10 and earlier did not return # NaNs for positions outside the sky, but instead returned an array with # all the same world coordinates regardless of input pixel coordinates. if len(wx) > 1 and len(np.unique(wx)) == 1: return None if coord_type in ['longitude', 'latitude']: if coord_type == 'longitude': wxmin, wxmax = math_util.smart_range(wx) else: wxmin, wxmax = min(wx), max(wx) if 'd.' in format: spacing = au.smart_round_angle_decimal((wxmax - wxmin) / 5., latitude=coord_type == 'latitude') else: spacing = au.smart_round_angle_sexagesimal((wxmax - wxmin) / 5., latitude=coord_type == 'latitude', hours='hh' in format) else: wxmin, wxmax = np.min(wx), np.max(wx) spacing = su.smart_round_angle_decimal((wxmax - wxmin) / 5.) else: # The following is required because PyWCS 0.10 and earlier did not return # NaNs for positions outside the sky, but instead returned an array with # all the same world coordinates regardless of input pixel coordinates. if len(wy) > 1 and len(np.unique(wy)) == 1: return None if coord_type in ['longitude', 'latitude']: if coord_type == 'longitude': wymin, wymax = math_util.smart_range(wy) else: wymin, wymax = min(wy), max(wy) if 'd.' in format: spacing = au.smart_round_angle_decimal((wymax - wymin) / 5., latitude=coord_type == 'latitude') else: spacing = au.smart_round_angle_sexagesimal((wymax - wymin) / 5., latitude=coord_type == 'latitude', hours='hh' in format) else: wymin, wymax = np.min(wy), np.max(wy) spacing = su.smart_round_angle_decimal((wymax - wymin) / 5.) # Find minimum spacing allowed by labels if coord_type in ['longitude', 'latitude']: min_spacing = au._get_label_precision(format, latitude=coord_type == 'latitude') if min_spacing.todegrees() > spacing.todegrees(): return min_spacing else: return spacing else: min_spacing = su._get_label_precision(format) if min_spacing is not None and min_spacing > spacing: return min_spacing else: return spacing def tick_positions(wcs, spacing, axis, coord, farside=False, xmin=False, xmax=False, ymin=False, ymax=False, mode='xscaled'): ''' Find positions of ticks along a given axis. Parameters ---------- wcs : ~aplpy.wcs_util.WCS The WCS instance for the image. spacing : float The spacing along the axis. axis : { 'x', 'y' } The axis along which we are looking for ticks. coord : { 'x', 'y' } The coordinate for which we are looking for ticks. farside : bool, optional Whether we are looking on the left or bottom axes (False) or the right or top axes (True). xmin, xmax, ymin, ymax : float, optional The range of pixel values covered by the image. mode : { 'xy', 'xscaled' }, optional If set to 'xy' the function returns the world coordinates of the ticks. If 'xscaled', then only the coordinate requested is returned, in units of the tick spacing. ''' (px, py, wx, wy) = axis_positions(wcs, axis, farside, xmin, xmax, ymin, ymax) if coord == 'x': warr, walt = wx, wy else: warr, walt = wy, wx # Check for 360 degree transition, and if encountered, # change the values so that there is continuity if (coord == 'x' and wcs.xaxis_coord_type == 'longitude') or \ (coord == 'y' and wcs.yaxis_coord_type == 'longitude'): for i in range(0, len(warr) - 1): if(abs(warr[i] - warr[i + 1]) > 180.): if(warr[i] > warr[i + 1]): warr[i + 1:] = warr[i + 1:] + 360. else: warr[i + 1:] = warr[i + 1:] - 360. # Convert warr to units of the spacing, then ticks are at integer values warr = warr / spacing # Create empty arrays for tick positions iall = [] wall = [] # Loop over ticks which lie in the range covered by the axis for w in np.arange(np.floor(min(warr)), np.ceil(max(warr)), 1.): # Find all the positions at which to interpolate inter = np.where(((warr[:-1] <= w) & (warr[1:] > w)) | ((warr[:-1] > w) & (warr[1:] <= w)))[0] # If there are any intersections, keep the indices, and the position # of the interpolation if len(inter) > 0: iall.append(inter.astype(int)) wall.append(np.repeat(w, len(inter)).astype(float)) if len(iall) > 0: iall = np.hstack(iall) wall = np.hstack(wall) else: if mode == 'xscaled': return [], [], [] else: return [], [], [], [] # Now we can interpolate as needed dwarr = warr[1:] - warr[:-1] px_out = px[:-1][iall] + (px[1:][iall] - px[:-1][iall]) * (wall - warr[:-1][iall]) / dwarr[iall] py_out = py[:-1][iall] + (py[1:][iall] - py[:-1][iall]) * (wall - warr[:-1][iall]) / dwarr[iall] if mode == 'xscaled': warr_out = wall return px_out, py_out, warr_out elif mode == 'xy': warr_out = wall * spacing walt_out = walt[:-1][iall] + (walt[1:][iall] - walt[:-1][iall]) * (wall - warr[:-1][iall]) / dwarr[iall] if coord == 'x': return px_out, py_out, warr_out, walt_out else: return px_out, py_out, walt_out, warr_out def axis_positions(wcs, axis, farside, xmin=False, xmax=False, ymin=False, ymax=False): ''' Find the world coordinates of all pixels along an axis. Parameters ---------- wcs : ~aplpy.wcs_util.WCS The WCS instance for the image. axis : { 'x', 'y' } The axis along which we are computing world coordinates. farside : bool Whether we are looking on the left or bottom axes (False) or the right or top axes (True). xmin, xmax, ymin, ymax : float, optional The range of pixel values covered by the image ''' if not xmin: xmin = 0.5 if not xmax: xmax = 0.5 + wcs.nx if not ymin: ymin = 0.5 if not ymax: ymax = 0.5 + wcs.ny # Check options assert axis == 'x' or axis == 'y', "The axis= argument should be set to x or y" # Generate an array of pixel values for the x-axis if axis == 'x': x_pix = np.linspace(xmin, xmax, 512) y_pix = np.ones(np.shape(x_pix)) if(farside): y_pix = y_pix * ymax else: y_pix = y_pix * ymin else: y_pix = np.linspace(ymin, ymax, 512) x_pix = np.ones(np.shape(y_pix)) if(farside): x_pix = x_pix * xmax else: x_pix = x_pix * xmin # Convert these to world coordinates x_world, y_world = wcs_util.pix2world(wcs, x_pix, y_pix) return x_pix, y_pix, x_world, y_world def coord_range(wcs): ''' Find the range of coordinates that intersect the axes. Parameters ---------- wcs : ~aplpy.wcs_util.WCS The WCS instance for the image. ''' x_pix, y_pix, x_world_1, y_world_1 = axis_positions(wcs, 'x', farside=False) x_pix, y_pix, x_world_2, y_world_2 = axis_positions(wcs, 'x', farside=True) x_pix, y_pix, x_world_3, y_world_3 = axis_positions(wcs, 'y', farside=False) x_pix, y_pix, x_world_4, y_world_4 = axis_positions(wcs, 'y', farside=True) x_world = np.hstack([x_world_1, x_world_2, x_world_3, x_world_4]) y_world = np.hstack([y_world_1, y_world_2, y_world_3, y_world_4]) x_min = min(x_world) x_max = max(x_world) y_min = min(y_world) y_max = max(y_world) return x_min, x_max, y_min, y_max APLpy-1.0/aplpy/version.py0000644000077000000240000001240312471103533015440 0ustar tomstaff00000000000000# Autogenerated by Astropy-affiliated package aplpy's setup.py on 2015-02-18 12:36:43.299843 import locale import os import subprocess import warnings def _decode_stdio(stream): try: stdio_encoding = locale.getdefaultlocale()[1] or 'utf-8' except ValueError: stdio_encoding = 'utf-8' try: text = stream.decode(stdio_encoding) except UnicodeDecodeError: # Final fallback text = stream.decode('latin1') return text def update_git_devstr(version, path=None): """ Updates the git revision string if and only if the path is being imported directly from a git working copy. This ensures that the revision number in the version string is accurate. """ try: # Quick way to determine if we're in git or not - returns '' if not devstr = get_git_devstr(sha=True, show_warning=False, path=path) except OSError: return version if not devstr: # Probably not in git so just pass silently return version if 'dev' in version: # update to the current git revision version_base = version.split('.dev', 1)[0] devstr = get_git_devstr(sha=False, show_warning=False, path=path) return version_base + '.dev' + devstr else: #otherwise it's already the true/release version return version def get_git_devstr(sha=False, show_warning=True, path=None): """ Determines the number of revisions in this repository. Parameters ---------- sha : bool If True, the full SHA1 hash will be returned. Otherwise, the total count of commits in the repository will be used as a "revision number". show_warning : bool If True, issue a warning if git returns an error code, otherwise errors pass silently. path : str or None If a string, specifies the directory to look in to find the git repository. If `None`, the current working directory is used. If given a filename it uses the directory containing that file. Returns ------- devversion : str Either a string with the revision number (if `sha` is False), the SHA1 hash of the current commit (if `sha` is True), or an empty string if git version info could not be identified. """ if path is None: path = os.getcwd() if not os.path.isdir(path): path = os.path.abspath(os.path.dirname(path)) if not os.path.exists(os.path.join(path, '.git')): return '' if sha: # Faster for getting just the hash of HEAD cmd = ['rev-parse', 'HEAD'] else: cmd = ['rev-list', '--count', 'HEAD'] def run_git(cmd): try: p = subprocess.Popen(['git'] + cmd, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) stdout, stderr = p.communicate() except OSError as e: if show_warning: warnings.warn('Error running git: ' + str(e)) return (None, '', '') if p.returncode == 128: if show_warning: warnings.warn('No git repository present at {0!r}! Using ' 'default dev version.'.format(path)) return (p.returncode, '', '') if p.returncode == 129: if show_warning: warnings.warn('Your git looks old (does it support {0}?); ' 'consider upgrading to v1.7.2 or ' 'later.'.format(cmd[0])) return (p.returncode, stdout, stderr) elif p.returncode != 0: if show_warning: warnings.warn('Git failed while determining revision ' 'count: {0}'.format(_decode_stdio(stderr))) return (p.returncode, stdout, stderr) return p.returncode, stdout, stderr returncode, stdout, stderr = run_git(cmd) if not sha and returncode == 129: # git returns 129 if a command option failed to parse; in # particular this could happen in git versions older than 1.7.2 # where the --count option is not supported # Also use --abbrev-commit and --abbrev=0 to display the minimum # number of characters needed per-commit (rather than the full hash) cmd = ['rev-list', '--abbrev-commit', '--abbrev=0', 'HEAD'] returncode, stdout, stderr = run_git(cmd) # Fall back on the old method of getting all revisions and counting # the lines if returncode == 0: return str(stdout.count(b'\n')) else: return '' elif sha: return _decode_stdio(stdout)[:40] else: return _decode_stdio(stdout).strip() _last_generated_version = '1.0' _last_githash = '0f7719cc1552568d3c4a3b74c9f6c8ed0bc3a3d9' version = update_git_devstr(_last_generated_version) githash = get_git_devstr(sha=True, show_warning=False, path=__file__) or _last_githash major = 1 minor = 0 bugfix = 0 release = True debug = False try: from ._compiler import compiler except ImportError: compiler = "unknown" try: from .cython_version import cython_version except ImportError: cython_version = "unknown" APLpy-1.0/aplpy/wcs_util.py0000644000077000000240000004162112471065325015616 0ustar tomstaff00000000000000from __future__ import absolute_import, print_function, division import numpy as np import warnings from astropy import log from astropy.wcs import WCS as AstropyWCS import astropy.wcs def decode_ascii(string): try: return string.decode('ascii') except AttributeError: return string class WCS(AstropyWCS): def __init__(self, *args, **kwargs): if 'slices' in kwargs: self._slices = kwargs.pop('slices') else: self._slices = [] if 'dimensions' in kwargs: self._dimensions = kwargs.pop('dimensions') else: self._dimensions = [0, 1] AstropyWCS.__init__(self, *args, **kwargs) # Fix common non-standard units self.wcs.unitfix() # Now find the values of the coordinates in the slices - only needed if # data has more than two dimensions if len(self._slices) > 0: self.nx = args[0]['NAXIS%i' % (self._dimensions[0] + 1)] self.ny = args[0]['NAXIS%i' % (self._dimensions[1] + 1)] xpix = np.arange(self.nx) + 1. ypix = np.arange(self.ny) + 1. xpix, ypix = np.meshgrid(xpix, ypix) xpix, ypix = xpix.reshape(self.nx * self.ny), ypix.reshape(self.nx * self.ny) s = 0 coords = [] for dim in range(self.naxis): if dim == self._dimensions[0]: coords.append(xpix) elif dim == self._dimensions[1]: coords.append(ypix) else: coords.append(np.repeat(self._slices[s], xpix.shape)) s += 1 coords = np.vstack(coords).transpose() result = AstropyWCS.wcs_pix2world(self, coords, 1) self._mean_world = np.mean(result, axis=0) # result = result.transpose() # result = result.reshape((result.shape[0],) + (self.ny, self.nx)) # Now guess what 'type' of values are on each axis if self.ctype_x[:4] == 'RA--' or \ self.ctype_x[1:4] == 'LON': self.set_xaxis_coord_type('longitude') self.set_xaxis_coord_type('longitude') elif self.ctype_x[:4] == 'DEC-' or \ self.ctype_x[1:4] == 'LAT': self.set_xaxis_coord_type('latitude') self.set_xaxis_coord_type('latitude') else: self.set_xaxis_coord_type('scalar') self.set_xaxis_coord_type('scalar') if self.ctype_y[:4] == 'RA--' or \ self.ctype_y[1:4] == 'LON': self.set_yaxis_coord_type('longitude') self.set_yaxis_coord_type('longitude') elif self.ctype_y[:4] == 'DEC-' or \ self.ctype_y[1:4] == 'LAT': self.set_yaxis_coord_type('latitude') self.set_yaxis_coord_type('latitude') else: self.set_yaxis_coord_type('scalar') self.set_yaxis_coord_type('scalar') @property def is_celestial(self): return self.wcs.lng in self._dimensions and self.wcs.lat in self._dimensions @property def pixel_scale_matrix(self): # Only for the subset of dimensions used here d0 = self._dimensions[0] d1 = self._dimensions[1] cdelt = np.matrix([[self.wcs.get_cdelt()[d0],0], [0, self.wcs.get_cdelt()[d1]]]) pc_full = self.wcs.get_pc() pc = np.matrix([[pc_full[d0,d0], pc_full[d0,d1]], [pc_full[d1,d0], pc_full[d1,d1]]]) m = np.array(cdelt * pc) return m def set_xaxis_coord_type(self, coord_type): if coord_type in ['longitude', 'latitude', 'scalar']: self.xaxis_coord_type = coord_type else: raise Exception("coord_type should be one of longitude/latitude/scalar") def set_yaxis_coord_type(self, coord_type): if coord_type in ['longitude', 'latitude', 'scalar']: self.yaxis_coord_type = coord_type else: raise Exception("coord_type should be one of longitude/latitude/scalar") def __getattr__(self, attribute): if attribute[-2:] == '_x': axis = self._dimensions[0] elif attribute[-2:] == '_y': axis = self._dimensions[1] else: raise AttributeError("Attribute %s does not exist" % attribute) if attribute[:5] == 'ctype': return decode_ascii(self.wcs.ctype[axis]) elif attribute[:5] == 'cname': return decode_ascii(self.wcs.cname[axis]) elif attribute[:5] == 'cunit': return str(self.wcs.cunit[axis]) elif attribute[:5] == 'crval': return decode_ascii(self.wcs.crval[axis]) elif attribute[:5] == 'crpix': return decode_ascii(self.wcs.crpix[axis]) else: raise AttributeError("Attribute %s does not exist" % attribute) def wcs_world2pix(self, x, y, origin): if self.naxis == 2: if self._dimensions[1] < self._dimensions[0]: xp, yp = AstropyWCS.wcs_world2pix(self, y, x, origin) return yp, xp else: return AstropyWCS.wcs_world2pix(self, x, y, origin) else: coords = [] s = 0 for dim in range(self.naxis): if dim == self._dimensions[0]: coords.append(x) elif dim == self._dimensions[1]: coords.append(y) else: # The following is an approximation, and will break down if # the world coordinate changes significantly over the slice coords.append(np.repeat(self._mean_world[dim], x.shape)) s += 1 coords = np.vstack(coords).transpose() # Due to a bug in pywcs, we need to loop over each coordinate # result = AstropyWCS.wcs_world2pix(self, coords, origin) result = np.zeros(coords.shape) for i in range(result.shape[0]): result[i:i + 1, :] = AstropyWCS.wcs_world2pix(self, coords[i:i + 1, :], origin) return result[:, self._dimensions[0]], result[:, self._dimensions[1]] def wcs_pix2world(self, x, y, origin): if self.naxis == 2: if self._dimensions[1] < self._dimensions[0]: xw, yw = AstropyWCS.wcs_pix2world(self, y, x, origin) return yw, xw else: return AstropyWCS.wcs_pix2world(self, x, y, origin) else: coords = [] s = 0 for dim in range(self.naxis): if dim == self._dimensions[0]: coords.append(x) elif dim == self._dimensions[1]: coords.append(y) else: coords.append(np.repeat(self._slices[s] + 0.5, x.shape)) s += 1 coords = np.vstack(coords).transpose() result = AstropyWCS.wcs_pix2world(self, coords, origin) return result[:, self._dimensions[0]], result[:, self._dimensions[1]] def convert_coords(x, y, input, output): system_in, equinox_in = input system_out, equinox_out = output if input == output: return x, y # Need to take into account inverted coords if system_in['name'] == 'galactic' and system_out['name'] == 'equatorial': if equinox_out == 'j2000': x, y = gal2fk5(x, y) elif equinox_out == 'b1950': x, y = gal2fk5(x, y) x, y = j2000tob1950(x, y) else: raise Exception("Cannot convert from galactic to equatorial coordinates for equinox=%s" % equinox_out) elif system_in['name'] == 'equatorial' and system_out['name'] == 'galactic': if equinox_in == 'j2000': x, y = fk52gal(x, y) elif equinox_in == 'b1950': x, y = b1950toj2000(x, y) x, y = fk52gal(x, y) else: raise Exception("Cannot convert from equatorial to equatorial coordinates for equinox=%s" % equinox_in) elif system_in['name'] == 'equatorial' and system_out['name'] == 'equatorial': if equinox_in == 'b1950' and equinox_out == 'j2000': x, y = b1950toj2000(x, y) elif equinox_in == 'j2000' and equinox_out == 'b1950': x, y = j2000tob1950(x, y) elif equinox_in == equinox_out: pass else: raise Exception("Cannot convert between equatorial coordinates for equinoxes %s and %s" % (equinox_in, equinox_out)) else: raise Exception("Cannot (yet) convert between %s, %s and %s, %s" % (system_in['name'], equinox_in, system_out['name'], equinox_out)) # Take into account inverted coordinates if system_in['inverted'] is system_out['inverted']: return x, y else: return y, x def precession_matrix(equinox1, equinox2, fk4=False): "Adapted from the IDL astronomy library" deg_to_rad = np.pi / 180. sec_to_rad = deg_to_rad / 3600. t = 0.001 * (equinox2 - equinox1) if not fk4: st = 0.001 * (equinox1 - 2000.) # Compute 3 rotation angles a = sec_to_rad * t * (23062.181 + st * (139.656 + 0.0139 * st) + t * (30.188 - 0.344 * st + 17.998 * t)) b = sec_to_rad * t * t * (79.280 + 0.410 * st + 0.205 * t) + a c = sec_to_rad * t * (20043.109 - st * (85.33 + 0.217 * st) + t * (- 42.665 - 0.217 * st - 41.833 * t)) else: st = 0.001 * (equinox1 - 1900.) # Compute 3 rotation angles a = sec_to_rad * t * (23042.53 + st * (139.75 + 0.06 * st) + t * (30.23 - 0.27 * st + 18.0 * t)) b = sec_to_rad * t * t * (79.27 + 0.66 * st + 0.32 * t) + a c = sec_to_rad * t * (20046.85 - st * (85.33 + 0.37 * st) + t * (- 42.67 - 0.37 * st - 41.8 * t)) sina = np.sin(a) sinb = np.sin(b) sinc = np.sin(c) cosa = np.cos(a) cosb = np.cos(b) cosc = np.cos(c) r = np.matrix([[cosa * cosb * cosc - sina * sinb, sina * cosb + cosa * sinb * cosc, cosa * sinc], [- cosa * sinb - sina * cosb * cosc, cosa * cosb - sina * sinb * cosc, - sina * sinc], [- cosb * sinc, - sinb * sinc, cosc]]) return r P1 = precession_matrix(1950., 2000.) P2 = precession_matrix(2000., 1950.) def b1950toj2000(ra, dec): ''' Convert B1950 to J2000 coordinates. This routine is based on the technique described at http://www.stargazing.net/kepler/b1950.html ''' # Convert to radians ra = np.radians(ra) dec = np.radians(dec) # Convert RA, Dec to rectangular coordinates x = np.cos(ra) * np.cos(dec) y = np.sin(ra) * np.cos(dec) z = np.sin(dec) # Apply the precession matrix x2 = P1[0, 0] * x + P1[1, 0] * y + P1[2, 0] * z y2 = P1[0, 1] * x + P1[1, 1] * y + P1[2, 1] * z z2 = P1[0, 2] * x + P1[1, 2] * y + P1[2, 2] * z # Convert the new rectangular coordinates back to RA, Dec ra = np.arctan2(y2, x2) dec = np.arcsin(z2) # Convert to degrees ra = np.degrees(ra) dec = np.degrees(dec) # Make sure ra is between 0. and 360. ra = np.mod(ra, 360.) dec = np.mod(dec + 90., 180.) - 90. return ra, dec def j2000tob1950(ra, dec): ''' Convert J2000 to B1950 coordinates. This routine was derived by taking the inverse of the b1950toj2000 routine ''' # Convert to radians ra = np.radians(ra) dec = np.radians(dec) # Convert RA, Dec to rectangular coordinates x = np.cos(ra) * np.cos(dec) y = np.sin(ra) * np.cos(dec) z = np.sin(dec) # Apply the precession matrix x2 = P2[0, 0] * x + P2[1, 0] * y + P2[2, 0] * z y2 = P2[0, 1] * x + P2[1, 1] * y + P2[2, 1] * z z2 = P2[0, 2] * x + P2[1, 2] * y + P2[2, 2] * z # Convert the new rectangular coordinates back to RA, Dec ra = np.arctan2(y2, x2) dec = np.arcsin(z2) # Convert to degrees ra = np.degrees(ra) dec = np.degrees(dec) # Make sure ra is between 0. and 360. ra = np.mod(ra, 360.) dec = np.mod(dec + 90., 180.) - 90. return ra, dec # Galactic conversion constants RA_NGP = np.radians(192.859508333333) DEC_NGP = np.radians(27.1283361111111) L_CP = np.radians(122.932) L_0 = L_CP - np.pi / 2. RA_0 = RA_NGP + np.pi / 2. DEC_0 = np.pi / 2. - DEC_NGP def gal2fk5(l, b): l = np.radians(l) b = np.radians(b) sind = np.sin(b) * np.sin(DEC_NGP) + np.cos(b) * np.cos(DEC_NGP) * np.sin(l - L_0) dec = np.arcsin(sind) cosa = np.cos(l - L_0) * np.cos(b) / np.cos(dec) sina = (np.cos(b) * np.sin(DEC_NGP) * np.sin(l - L_0) - np.sin(b) * np.cos(DEC_NGP)) / np.cos(dec) dec = np.degrees(dec) ra = np.arccos(cosa) ra[np.where(sina < 0.)] = -ra[np.where(sina < 0.)] ra = np.degrees(ra + RA_0) ra = np.mod(ra, 360.) dec = np.mod(dec + 90., 180.) - 90. return ra, dec def fk52gal(ra, dec): ra, dec = np.radians(ra), np.radians(dec) sinb = np.sin(dec) * np.cos(DEC_0) - np.cos(dec) * np.sin(ra - RA_0) * np.sin(DEC_0) b = np.arcsin(sinb) cosl = np.cos(dec) * np.cos(ra - RA_0) / np.cos(b) sinl = (np.sin(dec) * np.sin(DEC_0) + np.cos(dec) * np.sin(ra - RA_0) * np.cos(DEC_0)) / np.cos(b) b = np.degrees(b) l = np.arccos(cosl) l[np.where(sinl < 0.)] = - l[np.where(sinl < 0.)] l = np.degrees(l + L_0) l = np.mod(l, 360.) b = np.mod(b + 90., 180.) - 90. return l, b def system(wcs): xcoord = wcs.ctype_x[0:4] ycoord = wcs.ctype_y[0:4] equinox = wcs.wcs.equinox system = {} if xcoord == 'RA--' and ycoord == 'DEC-': system['name'] = 'equatorial' system['inverted'] = False elif ycoord == 'RA--' and xcoord == 'DEC-': system['name'] = 'equatorial' system['inverted'] = True elif xcoord == 'GLON' and ycoord == 'GLAT': system['name'] = 'galactic' system['inverted'] = False elif ycoord == 'GLON' and xcoord == 'GLAT': system['name'] = 'galactic' system['inverted'] = True elif xcoord == 'ELON' and ycoord == 'ELAT': system['name'] = 'ecliptic' system['inverted'] = False elif ycoord == 'ELON' and xcoord == 'ELAT': system['name'] = 'ecliptic' system['inverted'] = True else: system['name'] = 'unknown' system['inverted'] = False if system['name'] == 'equatorial': if equinox == '' or np.isnan(equinox) or equinox == 0.: log.warning("Cannot determine equinox. Assuming J2000.") equinox = 'j2000' elif equinox == 1950.: equinox = 'b1950' elif equinox == 2000.: equinox = 'j2000' else: raise Exception("Cannot use equinox %s" % equinox) else: equinox = 'none' units = 'degrees' return system, equinox, units def world2pix(wcs, x_world, y_world): if np.isscalar(x_world) and np.isscalar(y_world): x_pix, y_pix = wcs.wcs_world2pix(np.array([x_world]), np.array([y_world]), 1) return x_pix[0], y_pix[0] elif (type(x_world) == list) and (type(y_world) == list): x_pix, y_pix = wcs.wcs_world2pix(np.array(x_world), np.array(y_world), 1) return x_pix.tolist(), y_pix.tolist() elif isinstance(x_world, np.ndarray) and isinstance(y_world, np.ndarray): return wcs.wcs_world2pix(x_world, y_world, 1) else: raise Exception("world2pix should be provided either with two scalars, two lists, or two numpy arrays") def pix2world(wcs, x_pix, y_pix): if np.isscalar(x_pix) and np.isscalar(y_pix): x_world, y_world = wcs.wcs_pix2world(np.array([x_pix]), np.array([y_pix]), 1) return x_world[0], y_world[0] elif (type(x_pix) == list) and (type(y_pix) == list): x_world, y_world = wcs.wcs_pix2world(np.array(x_pix), np.array(y_pix), 1) return x_world.tolist(), y_world.tolist() elif isinstance(x_pix, np.ndarray) and isinstance(y_pix, np.ndarray): return wcs.wcs_pix2world(x_pix, y_pix, 1) else: raise Exception("pix2world should be provided either with two scalars, two lists, or two numpy arrays") def celestial_pixel_scale(wcs): """ If the pixels are square, return the pixel scale in the spatial dimensions """ if not wcs.is_celestial: raise ValueError("WCS is not celestial, cannot determine celestial pixel scale") # Extract celestial part of WCS and sanitize from astropy.wcs import WCSSUB_CELESTIAL wcs = wcs.sub([WCSSUB_CELESTIAL]) pccd = wcs.pixel_scale_matrix scale = np.sqrt((pccd ** 2).sum(axis=0)) if not np.allclose(scale[0], scale[1]): warnings.warn("Pixels are not square, using an average pixel scale") return np.mean(scale) else: return scale[0] def non_celestial_pixel_scales(wcs): if wcs.is_celestial: raise ValueError("WCS is celestial, use celestial_pixel_scale instead") pccd = wcs.pixel_scale_matrix if np.allclose(pccd[1,0], 0) and np.allclose(pccd[0,1], 0): return np.abs(np.diagonal(pccd)) else: raise ValueError("WCS is rotated, cannot determine consistent pixel scales") APLpy-1.0/astropy_helpers/0000755000077000000240000000000012471104150015473 5ustar tomstaff00000000000000APLpy-1.0/astropy_helpers/.coveragerc0000644000077000000240000000063312361722142017623 0ustar tomstaff00000000000000[run] source = astropy_helpers ah_bootstrap omit = astropy_helpers/tests* [report] exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about packages we have installed except ImportError # Don't complain if tests don't hit assertions raise AssertionError raise NotImplementedError # Don't complain about script hooks def main\(.*\): APLpy-1.0/astropy_helpers/.travis.yml0000644000077000000240000000163512470177552017630 0ustar tomstaff00000000000000language: python python: - 2.6 - 2.7 - 3.2 - 3.3 - 3.4 before_install: # Use utf8 encoding. Should be default, but this is insurance against # future changes - export PYTHONIOENCODING=UTF8 # Install conda - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh - chmod +x miniconda.sh - ./miniconda.sh -b - export PATH=/home/travis/miniconda/bin:$PATH - conda update --yes conda # Install dot - sudo apt-get install graphviz install: - conda install --yes pip "pytest<2.6" "sphinx<1.3b1" cython numpy - pip install coveralls pytest-cov before_script: # Some of the tests use git commands that require a user to be configured - git config --global user.name "A U Thor" - git config --global user.email "author@example.com" script: - py.test --cov astropy_helpers after_success: - coveralls APLpy-1.0/astropy_helpers/ah_bootstrap.py0000644000077000000240000007004212470177552020554 0ustar tomstaff00000000000000""" This bootstrap module contains code for ensuring that the astropy_helpers package will be importable by the time the setup.py script runs. It also includes some workarounds to ensure that a recent-enough version of setuptools is being used for the installation. This module should be the first thing imported in the setup.py of distributions that make use of the utilities in astropy_helpers. If the distribution ships with its own copy of astropy_helpers, this module will first attempt to import from the shipped copy. However, it will also check PyPI to see if there are any bug-fix releases on top of the current version that may be useful to get past platform-specific bugs that have been fixed. When running setup.py, use the ``--offline`` command-line option to disable the auto-upgrade checks. When this module is imported or otherwise executed it automatically calls a main function that attempts to read the project's setup.cfg file, which it checks for a configuration section called ``[ah_bootstrap]`` the presences of that section, and options therein, determine the next step taken: If it contains an option called ``auto_use`` with a value of ``True``, it will automatically call the main function of this module called `use_astropy_helpers` (see that function's docstring for full details). Otherwise no further action is taken (however, ``ah_bootstrap.use_astropy_helpers`` may be called manually from within the setup.py script). Additional options in the ``[ah_boostrap]`` section of setup.cfg have the same names as the arguments to `use_astropy_helpers`, and can be used to configure the bootstrap script when ``auto_use = True``. See https://github.com/astropy/astropy-helpers for more details, and for the latest version of this module. """ import contextlib import errno import imp import io import locale import os import re import subprocess as sp import sys try: from ConfigParser import ConfigParser, RawConfigParser except ImportError: from configparser import ConfigParser, RawConfigParser if sys.version_info[0] < 3: _str_types = (str, unicode) _text_type = unicode PY3 = False else: _str_types = (str, bytes) _text_type = str PY3 = True # Some pre-setuptools checks to ensure that either distribute or setuptools >= # 0.7 is used (over pre-distribute setuptools) if it is available on the path; # otherwise the latest setuptools will be downloaded and bootstrapped with # ``ez_setup.py``. This used to be included in a separate file called # setuptools_bootstrap.py; but it was combined into ah_bootstrap.py try: import pkg_resources _setuptools_req = pkg_resources.Requirement.parse('setuptools>=0.7') # This may raise a DistributionNotFound in which case no version of # setuptools or distribute is properly installed _setuptools = pkg_resources.get_distribution('setuptools') if _setuptools not in _setuptools_req: # Older version of setuptools; check if we have distribute; again if # this results in DistributionNotFound we want to give up _distribute = pkg_resources.get_distribution('distribute') if _setuptools != _distribute: # It's possible on some pathological systems to have an old version # of setuptools and distribute on sys.path simultaneously; make # sure distribute is the one that's used sys.path.insert(1, _distribute.location) _distribute.activate() imp.reload(pkg_resources) except: # There are several types of exceptions that can occur here; if all else # fails bootstrap and use the bootstrapped version from ez_setup import use_setuptools use_setuptools() from distutils import log from distutils.debug import DEBUG # In case it didn't successfully import before the ez_setup checks import pkg_resources from setuptools import Distribution from setuptools.package_index import PackageIndex from setuptools.sandbox import run_setup # Note: The following import is required as a workaround to # https://github.com/astropy/astropy-helpers/issues/89; if we don't import this # module now, it will get cleaned up after `run_setup` is called, but that will # later cause the TemporaryDirectory class defined in it to stop working when # used later on by setuptools try: import setuptools.py31compat except ImportError: pass # TODO: Maybe enable checking for a specific version of astropy_helpers? DIST_NAME = 'astropy-helpers' PACKAGE_NAME = 'astropy_helpers' # Defaults for other options DOWNLOAD_IF_NEEDED = True INDEX_URL = 'https://pypi.python.org/simple' USE_GIT = True AUTO_UPGRADE = True def use_astropy_helpers(path=None, download_if_needed=None, index_url=None, use_git=None, auto_upgrade=None): """ Ensure that the `astropy_helpers` module is available and is importable. This supports automatic submodule initialization if astropy_helpers is included in a project as a git submodule, or will download it from PyPI if necessary. Parameters ---------- path : str or None, optional A filesystem path relative to the root of the project's source code that should be added to `sys.path` so that `astropy_helpers` can be imported from that path. If the path is a git submodule it will automatically be initialzed and/or updated. The path may also be to a ``.tar.gz`` archive of the astropy_helpers source distribution. In this case the archive is automatically unpacked and made temporarily available on `sys.path` as a ``.egg`` archive. If `None` skip straight to downloading. download_if_needed : bool, optional If the provided filesystem path is not found an attempt will be made to download astropy_helpers from PyPI. It will then be made temporarily available on `sys.path` as a ``.egg`` archive (using the ``setup_requires`` feature of setuptools. If the ``--offline`` option is given at the command line the value of this argument is overridden to `False`. index_url : str, optional If provided, use a different URL for the Python package index than the main PyPI server. use_git : bool, optional If `False` no git commands will be used--this effectively disables support for git submodules. If the ``--no-git`` option is given at the command line the value of this argument is overridden to `False`. auto_upgrade : bool, optional By default, when installing a package from a non-development source distribution ah_boostrap will try to automatically check for patch releases to astropy-helpers on PyPI and use the patched version over any bundled versions. Setting this to `False` will disable that functionality. If the ``--offline`` option is given at the command line the value of this argument is overridden to `False`. """ # True by default, unless the --offline option was provided on the command # line if '--offline' in sys.argv: download_if_needed = False auto_upgrade = False offline = True sys.argv.remove('--offline') else: offline = False if '--no-git' in sys.argv: use_git = False sys.argv.remove('--no-git') if path is None: path = PACKAGE_NAME if download_if_needed is None: download_if_needed = DOWNLOAD_IF_NEEDED if index_url is None: index_url = INDEX_URL # If this is a release then the .git directory will not exist so we # should not use git. git_dir_exists = os.path.exists(os.path.join(os.path.dirname(__file__), '.git')) if use_git is None and not git_dir_exists: use_git = False if use_git is None: use_git = USE_GIT if auto_upgrade is None: auto_upgrade = AUTO_UPGRADE # Declared as False by default--later we check if astropy-helpers can be # upgraded from PyPI, but only if not using a source distribution (as in # the case of import from a git submodule) is_submodule = False if not isinstance(path, _str_types): if path is not None: raise TypeError('path must be a string or None') if not download_if_needed: log.debug('a path was not given and download from PyPI was not ' 'allowed so this is effectively a no-op') return elif not os.path.exists(path) or os.path.isdir(path): # Even if the given path does not exist on the filesystem, if it *is* a # submodule, `git submodule init` will create it is_submodule = _check_submodule(path, use_git=use_git, offline=offline) if is_submodule or os.path.isdir(path): log.info( 'Attempting to import astropy_helpers from {0} {1!r}'.format( 'submodule' if is_submodule else 'directory', path)) dist = _directory_import(path) else: dist = None if dist is None: msg = ( 'The requested path {0!r} for importing {1} does not ' 'exist, or does not contain a copy of the {1} package. ' 'Attempting download instead.'.format(path, PACKAGE_NAME)) if download_if_needed: log.warn(msg) else: raise _AHBootstrapSystemExit(msg) elif os.path.isfile(path): # Handle importing from a source archive; this also uses setup_requires # but points easy_install directly to the source archive try: dist = _do_download(find_links=[path]) except Exception as e: if download_if_needed: log.warn('{0}\nWill attempt to download astropy_helpers from ' 'PyPI instead.'.format(str(e))) dist = None else: raise _AHBootstrapSystemExit(e.args[0]) else: msg = ('{0!r} is not a valid file or directory (it could be a ' 'symlink?)'.format(path)) if download_if_needed: log.warn(msg) dist = None else: raise _AHBootstrapSystemExit(msg) if dist is not None and auto_upgrade and not is_submodule: # A version of astropy-helpers was found on the available path, but # check to see if a bugfix release is available on PyPI upgrade = _do_upgrade(dist, index_url) if upgrade is not None: dist = upgrade elif dist is None: # Last resort--go ahead and try to download the latest version from # PyPI try: if download_if_needed: log.warn( "Downloading astropy_helpers; run setup.py with the " "--offline option to force offline installation.") dist = _do_download(index_url=index_url) else: raise _AHBootstrapSystemExit( "No source for the astropy_helpers package; " "astropy_helpers must be available as a prerequisite to " "installing this package.") except Exception as e: if DEBUG: raise else: raise _AHBootstrapSystemExit(e.args[0]) if dist is not None: # Otherwise we found a version of astropy-helpers so we're done # Just activate the found distribibution on sys.path--if we did a # download this usually happens automatically but do it again just to # be sure # Note: Adding the dist to the global working set also activates it by # default pkg_resources.working_set.add(dist) def _do_download(version='', find_links=None, index_url=None): try: if find_links: allow_hosts = '' index_url = None else: allow_hosts = None # Annoyingly, setuptools will not handle other arguments to # Distribution (such as options) before handling setup_requires, so it # is not straightforward to programmatically augment the arguments which # are passed to easy_install class _Distribution(Distribution): def get_option_dict(self, command_name): opts = Distribution.get_option_dict(self, command_name) if command_name == 'easy_install': if find_links is not None: opts['find_links'] = ('setup script', find_links) if index_url is not None: opts['index_url'] = ('setup script', index_url) if allow_hosts is not None: opts['allow_hosts'] = ('setup script', allow_hosts) return opts if version: req = '{0}=={1}'.format(DIST_NAME, version) else: req = DIST_NAME attrs = {'setup_requires': [req]} if DEBUG: dist = _Distribution(attrs=attrs) else: with _silence(): dist = _Distribution(attrs=attrs) # If the setup_requires succeeded it will have added the new dist to # the main working_set return pkg_resources.working_set.by_key.get(DIST_NAME) except Exception as e: if DEBUG: raise msg = 'Error retrieving astropy helpers from {0}:\n{1}' if find_links: source = find_links[0] elif index_url: source = index_url else: source = 'PyPI' raise Exception(msg.format(source, repr(e))) def _do_upgrade(dist, index_url): # Build up a requirement for a higher bugfix release but a lower minor # release (so API compatibility is guaranteed) # sketchy version parsing--maybe come up with something a bit more # robust for this next_version = _next_version(dist.parsed_version) req = pkg_resources.Requirement.parse( '{0}>{1},<{2}'.format(DIST_NAME, dist.version, next_version)) package_index = PackageIndex(index_url=index_url) upgrade = package_index.obtain(req) if upgrade is not None: return _do_download(version=upgrade.version, index_url=index_url) def _directory_import(path): """ Import astropy_helpers from the given path, which will be added to sys.path. Must return True if the import succeeded, and False otherwise. """ # Return True on success, False on failure but download is allowed, and # otherwise raise SystemExit path = os.path.abspath(path) # Use an empty WorkingSet rather than the man pkg_resources.working_set, # since on older versions of setuptools this will invoke a VersionConflict # when trying to install an upgrade ws = pkg_resources.WorkingSet([]) ws.add_entry(path) dist = ws.by_key.get(DIST_NAME) if dist is None: # We didn't find an egg-info/dist-info in the given path, but if a # setup.py exists we can generate it setup_py = os.path.join(path, 'setup.py') if os.path.isfile(setup_py): with _silence(): run_setup(os.path.join(path, 'setup.py'), ['egg_info']) for dist in pkg_resources.find_distributions(path, True): # There should be only one... return dist return dist def _check_submodule(path, use_git=True, offline=False): """ Check if the given path is a git submodule. See the docstrings for ``_check_submodule_using_git`` and ``_check_submodule_no_git`` for further details. """ if use_git: return _check_submodule_using_git(path, offline) else: return _check_submodule_no_git(path) def _check_submodule_using_git(path, offline): """ Check if the given path is a git submodule. If so, attempt to initialize and/or update the submodule if needed. This function makes calls to the ``git`` command in subprocesses. The ``_check_submodule_no_git`` option uses pure Python to check if the given path looks like a git submodule, but it cannot perform updates. """ if PY3 and not isinstance(path, _text_type): fs_encoding = sys.getfilesystemencoding() path = path.decode(fs_encoding) try: p = sp.Popen(['git', 'submodule', 'status', '--', path], stdout=sp.PIPE, stderr=sp.PIPE) stdout, stderr = p.communicate() except OSError as e: if DEBUG: raise if e.errno == errno.ENOENT: # The git command simply wasn't found; this is most likely the # case on user systems that don't have git and are simply # trying to install the package from PyPI or a source # distribution. Silently ignore this case and simply don't try # to use submodules return False else: raise _AHBoostrapSystemExit( 'An unexpected error occurred when running the ' '`git submodule status` command:\n{0}'.format(str(e))) # Can fail of the default locale is not configured properly. See # https://github.com/astropy/astropy/issues/2749. For the purposes under # consideration 'latin1' is an acceptable fallback. try: stdio_encoding = locale.getdefaultlocale()[1] or 'latin1' except ValueError: # Due to an OSX oddity locale.getdefaultlocale() can also crash # depending on the user's locale/language settings. See: # http://bugs.python.org/issue18378 stdio_encoding = 'latin1' if p.returncode != 0 or stderr: # Unfortunately the return code alone cannot be relied on, as # earlier versions of git returned 0 even if the requested submodule # does not exist stderr = stderr.decode(stdio_encoding) # This is a warning that occurs in perl (from running git submodule) # which only occurs with a malformatted locale setting which can # happen sometimes on OSX. See again # https://github.com/astropy/astropy/issues/2749 perl_warning = ('perl: warning: Falling back to the standard locale ' '("C").') if not stderr.strip().endswith(perl_warning): # Some other uknown error condition occurred log.warn('git submodule command failed ' 'unexpectedly:\n{0}'.format(stderr)) return False stdout = stdout.decode(stdio_encoding) # The stdout should only contain one line--the status of the # requested submodule m = _git_submodule_status_re.match(stdout) if m: # Yes, the path *is* a git submodule _update_submodule(m.group('submodule'), m.group('status'), offline) return True else: log.warn( 'Unexpected output from `git submodule status`:\n{0}\n' 'Will attempt import from {1!r} regardless.'.format( stdout, path)) return False def _check_submodule_no_git(path): """ Like ``_check_submodule_using_git``, but simply parses the .gitmodules file to determine if the supplied path is a git submodule, and does not exec any subprocesses. This can only determine if a path is a submodule--it does not perform updates, etc. This function may need to be updated if the format of the .gitmodules file is changed between git versions. """ gitmodules_path = os.path.abspath('.gitmodules') if not os.path.isfile(gitmodules_path): return False # This is a minimal reader for gitconfig-style files. It handles a few of # the quirks that make gitconfig files incompatible with ConfigParser-style # files, but does not support the full gitconfig syntax (just enough # needed to read a .gitmodules file). gitmodules_fileobj = io.StringIO() # Must use io.open for cross-Python-compatible behavior wrt unicode with io.open(gitmodules_path) as f: for line in f: # gitconfig files are more flexible with leading whitespace; just # go ahead and remove it line = line.lstrip() # comments can start with either # or ; if line and line[0] in (':', ';'): continue gitmodules_fileobj.write(line) gitmodules_fileobj.seek(0) cfg = RawConfigParser() try: cfg.readfp(gitmodules_fileobj) except Exception as exc: log.warn('Malformatted .gitmodules file: {0}\n' '{1} cannot be assumed to be a git submodule.'.format( exc, path)) return False for section in cfg.sections(): if not cfg.has_option(section, 'path'): continue submodule_path = cfg.get(section, 'path').rstrip(os.sep) if submodule_path == path.rstrip(os.sep): return True return False def _update_submodule(submodule, status, offline): if status == ' ': # The submodule is up to date; no action necessary return elif status == '-': if offline: raise _AHBootstrapSystemExit( "Cannot initialize the {0} submodule in --offline mode; this " "requires being able to clone the submodule from an online " "repository.".format(submodule)) cmd = ['update', '--init'] action = 'Initializing' elif status == '+': cmd = ['update'] action = 'Updating' if offline: cmd.append('--no-fetch') elif status == 'U': raise _AHBoostrapSystemExit( 'Error: Submodule {0} contains unresolved merge conflicts. ' 'Please complete or abandon any changes in the submodule so that ' 'it is in a usable state, then try again.'.format(submodule)) else: log.warn('Unknown status {0!r} for git submodule {1!r}. Will ' 'attempt to use the submodule as-is, but try to ensure ' 'that the submodule is in a clean state and contains no ' 'conflicts or errors.\n{2}'.format(status, submodule, _err_help_msg)) return err_msg = None cmd = ['git', 'submodule'] + cmd + ['--', submodule] log.warn('{0} {1} submodule with: `{2}`'.format( action, submodule, ' '.join(cmd))) try: p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) stdout, stderr = p.communicate() except OSError as e: err_msg = str(e) else: if p.returncode != 0: stderr_encoding = locale.getdefaultlocale()[1] err_msg = stderr.decode(stderr_encoding) if err_msg: log.warn('An unexpected error occurred updating the git submodule ' '{0!r}:\n{1}\n{2}'.format(submodule, err_msg, _err_help_msg)) def _next_version(version): """ Given a parsed version from pkg_resources.parse_version, returns a new version string with the next minor version. Examples ======== >>> _next_version(pkg_resources.parse_version('1.2.3')) '1.3.0' """ if hasattr(version, 'base_version'): # New version parsing from setuptools >= 8.0 if version.base_version: parts = version.base_version.split('.') else: parts = [] else: parts = [] for part in version: if part.startswith('*'): break parts.append(part) parts = [int(p) for p in parts] if len(parts) < 3: parts += [0] * (3 - len(parts)) major, minor, micro = parts[:3] return '{0}.{1}.{2}'.format(major, minor + 1, 0) class _DummyFile(object): """A noop writeable object.""" errors = '' # Required for Python 3.x encoding = 'utf-8' def write(self, s): pass def flush(self): pass @contextlib.contextmanager def _silence(): """A context manager that silences sys.stdout and sys.stderr.""" old_stdout = sys.stdout old_stderr = sys.stderr sys.stdout = _DummyFile() sys.stderr = _DummyFile() exception_occurred = False try: yield except: exception_occurred = True # Go ahead and clean up so that exception handling can work normally sys.stdout = old_stdout sys.stderr = old_stderr raise if not exception_occurred: sys.stdout = old_stdout sys.stderr = old_stderr _err_help_msg = """ If the problem persists consider installing astropy_helpers manually using pip (`pip install astropy_helpers`) or by manually downloading the source archive, extracting it, and installing by running `python setup.py install` from the root of the extracted source code. """ class _AHBootstrapSystemExit(SystemExit): def __init__(self, *args): if not args: msg = 'An unknown problem occurred bootstrapping astropy_helpers.' else: msg = args[0] msg += '\n' + _err_help_msg super(_AHBootstrapSystemExit, self).__init__(msg, *args[1:]) if sys.version_info[:2] < (2, 7): # In Python 2.6 the distutils log does not log warnings, errors, etc. to # stderr so we have to wrap it to ensure consistency at least in this # module import distutils class log(object): def __getattr__(self, attr): return getattr(distutils.log, attr) def warn(self, msg, *args): self._log_to_stderr(distutils.log.WARN, msg, *args) def error(self, msg): self._log_to_stderr(distutils.log.ERROR, msg, *args) def fatal(self, msg): self._log_to_stderr(distutils.log.FATAL, msg, *args) def log(self, level, msg, *args): if level in (distutils.log.WARN, distutils.log.ERROR, distutils.log.FATAL): self._log_to_stderr(level, msg, *args) else: distutils.log.log(level, msg, *args) def _log_to_stderr(self, level, msg, *args): # This is the only truly 'public' way to get the current threshold # of the log current_threshold = distutils.log.set_threshold(distutils.log.WARN) distutils.log.set_threshold(current_threshold) if level >= current_threshold: if args: msg = msg % args sys.stderr.write('%s\n' % msg) sys.stderr.flush() log = log() # Output of `git submodule status` is as follows: # # 1: Status indicator: '-' for submodule is uninitialized, '+' if submodule is # initialized but is not at the commit currently indicated in .gitmodules (and # thus needs to be updated), or 'U' if the submodule is in an unstable state # (i.e. has merge conflicts) # # 2. SHA-1 hash of the current commit of the submodule (we don't really need # this information but it's useful for checking that the output is correct) # # 3. The output of `git describe` for the submodule's current commit hash (this # includes for example what branches the commit is on) but only if the # submodule is initialized. We ignore this information for now _git_submodule_status_re = re.compile( '^(?P[+-U ])(?P[0-9a-f]{40}) (?P\S+)( .*)?$') # Implement the auto-use feature; this allows use_astropy_helpers() to be used # at import-time automatically so long as the correct options are specified in # setup.cfg _CFG_OPTIONS = [('auto_use', bool), ('path', str), ('download_if_needed', bool), ('index_url', str), ('use_git', bool), ('auto_upgrade', bool)] def _main(): if not os.path.exists('setup.cfg'): return cfg = ConfigParser() try: cfg.read('setup.cfg') except Exception as e: if DEBUG: raise log.error( "Error reading setup.cfg: {0!r}\nastropy_helpers will not be " "automatically bootstrapped and package installation may fail." "\n{1}".format(e, _err_help_msg)) return if not cfg.has_section('ah_bootstrap'): return kwargs = {} for option, type_ in _CFG_OPTIONS: if not cfg.has_option('ah_bootstrap', option): continue if type_ is bool: value = cfg.getboolean('ah_bootstrap', option) else: value = cfg.get('ah_bootstrap', option) kwargs[option] = value if kwargs.pop('auto_use', False): use_astropy_helpers(**kwargs) _main() APLpy-1.0/astropy_helpers/astropy_helpers/0000755000077000000240000000000012471104150020716 5ustar tomstaff00000000000000APLpy-1.0/astropy_helpers/astropy_helpers/__init__.py0000644000077000000240000000024312422027142023027 0ustar tomstaff00000000000000try: from .version import version as __version__ from .version import githash as __githash__ except ImportError: __version__ = '' __githash__ = '' APLpy-1.0/astropy_helpers/astropy_helpers/__init__.pyc0000644000077000000240000000054612470200534023201 0ustar tomstaff00000000000000 b.HTc@sHy$ddlmZddlmZWnek rCdZdZnXdS(i(tversion(tgithashtN(Rt __version__Rt __githash__t ImportError(((s[/Users/tom/Dropbox/Code/development/APLpy/APLpy/astropy_helpers/astropy_helpers/__init__.pyts  APLpy-1.0/astropy_helpers/astropy_helpers/__pycache__/0000755000077000000240000000000012471104150023126 5ustar tomstaff00000000000000APLpy-1.0/astropy_helpers/astropy_helpers/__pycache__/__init__.cpython-34.pyc0000644000077000000240000000051012470157570027322 0ustar tomstaff00000000000000 b.HT @sIy$ddlmZddlmZWnek rDdZdZYnXdS))version)githashN)r __version__rZ __githash__ ImportErrorrr[/Users/tom/Dropbox/Code/development/APLpy/APLpy/astropy_helpers/astropy_helpers/__init__.pys  APLpy-1.0/astropy_helpers/astropy_helpers/__pycache__/git_helpers.cpython-34.pyc0000644000077000000240000001105712470157570030100 0ustar tomstaff00000000000000 AT@sydZddlZddlZddlZddlZddZdddZdddd d Zdd d ZdS) zP Utilities for retrieving revision information from a project's git repository. NcCspytjdpd}Wntk r4d}YnXy|j|}Wn!tk rk|jd}YnX|S)Nzutf-8latin1)localegetdefaultlocale ValueErrordecodeUnicodeDecodeError)streamstdio_encodingtextr ^/Users/tom/Dropbox/Code/development/APLpy/APLpy/astropy_helpers/astropy_helpers/git_helpers.py _decode_stdios   rcCsytddddd|}Wntk r7|SYnX|sB|Sd|kr|jddd }tddddd|}|d|S|Sd S) z Updates the git revision string if and only if the path is being imported directly from a git working copy. This ensures that the revision number in the version string is accurate. shaT show_warningFpathdevz.devrrN)get_git_devstrOSErrorsplit)versionrZdevstrZ version_baser r r update_git_devstr"s    rFTcs@dkr1tjtdds1dSntjjsdtjjtjjn|ryddg}ndddg}fd d }||\}}}| r|d krdd d dg}||\}}}|dkr t|jdSdSn*|r,t |ddSt |j SdS)a Determines the number of revisions in this repository. Parameters ---------- sha : bool If True, the full SHA1 hash will be returned. Otherwise, the total count of commits in the repository will be used as a "revision number". show_warning : bool If True, issue a warning if git returns an error code, otherwise errors pass silently. path : str or None If a string, specifies the directory to look in to find the git repository. If `None`, the current working directory is used, and must be the root of the git repository. If given a filename it uses the directory containing that file. Returns ------- devversion : str Either a string with the revision number (if `sha` is False), the SHA1 hash of the current commit (if `sha` is True), or an empty string if git version info could not be identified. Nlevelsrz rev-parseHEADzrev-listz--countcsmyMtjdg|ddtjdtjdtj}|j\}}WnItk r}z)rtjdt|ndSWYdd}~XnX|jdkrrtjd j n|jddfS|jd krr tjd j |d n|j||fS|jd kr]rMtjd j t |n|j||fS|j||fS)NgitcwdstdoutstderrstdinzError running git: z>No git repository present at {0!r}! Using default dev version.zQYour git looks old (does it support {0}?); consider upgrading to v1.7.2 or later.rz0Git failed while determining revision count: {0})Nr r ) subprocessPopenPIPE communicaterwarningswarnstr returncodeformatr)cmdprre)rrr r run_gitis4   zget_git_devstr..run_gitr"z--abbrev-commitz --abbrev=0s () osgetcwd_get_repo_pathrisdirabspathdirnamer)countrstrip)rrrr,r/r*rrr )rrr r=s(  ! rcCstjj|r3tjjtjj|}n+tjj|rZtjj|}ndSd}xy|dks||krtjjtjj|dr|S|d7}|tjj|krPntjj|}qgWdS)ao Given a file or directory name, determine the root of the git repository this path is under. If given, this won't look any higher than ``levels`` (that is, if ``levels=0`` then the given path must be the root of the git repository and is returned if so. Returns `None` if the given path could not be determined to belong to a git repo. Nrz.gitr)r1risfiler5r6r4existsjoin)pathnamerZ current_dirZ current_levelr r r r3s !! r3) __doc__rr1r#r'rrrr3r r r r s     aAPLpy-1.0/astropy_helpers/astropy_helpers/__pycache__/setup_helpers.cpython-34.pyc0000644000077000000240000012075112470157570030457 0ustar tomstaff00000000000000 oTY@sdZddlmZmZddlZddlZddlZddlZddlZddl Z ddl Z ddl Z ddl Z ddl Z ddlZddlZddlmZmZmZddlmZddlmZddlmZmZddlmZdd lmZdd lmZ dd l!m"Z"dd l#m$Z%dd l&m'Z(ddl)m*Z+ddl,m-Z.ddl/m0Z1ddl2m3Z3ddl4m5Z5ddl6m7Z7m8Z8m9Z9idd6dd6dd6dd6Z:yddl;Z;de:dm?Z@de:dd>e.ZXGd?d@d@e(ZYddAdBZZGdCdDdDe1Z[e:drGdEdFdFe@Z\ndGdHZ]dIdJZ^dKdLZ_dMfdNdOZ`dPdQZadRdSZbddTdUZcedddVdWZedXdYZfdZd[Zgd\d]ZhGd^d_d_ejiZjd`dadbZkdcddZldedfZmdgdhZnGdidjdjeZodS)kzx This module contains a number of utilities for use during setup/build/packaging that are useful to astropy as a whole. )absolute_importprint_functionN)log ccompiler sysconfig)DistutilsOptionError) Distribution)DistutilsErrorDistutilsFileError) Extension)Command)sdist) StrictVersion) build_ext)build_py)install) install_lib)register) find_packages) AstropyTest)silenceinvalidate_cacheswalk_skip_hiddenFadjusted_compilerregistered_commands have_cython have_sphinxT)BuildDoczunknown localeaPossible misconfiguration of one of the environment variables LC_ALL, LC_CTYPES, LANG, or LANGUAGE. For an example of how to configure your system's language environment on OSX see http://blog.remibergsma.com/2012/07/10/setting-locales-correctly-on-mac-osx-terminal-application/cCsdg}tdrdSdtd where is the command you ran. versionpkgfixedunixa The C compiler used to compile Python {compiler:s}, and which is normally used to compile C extensions, is not available. You can explicitly specify which compiler to use by setting the CC environment variable, for example: CC=gcc python setup.py or if you are using MacOS X, you can try: CC=clang python setup.py )r zclang) _module_stateosenvironget_compiler_versionOSErrortextwrapdedentformatrwarnsysexitrematchget_distutils_build_optionrget_default_compilerrget_config_var)packageZcompiler_mappingZ c_compilerr$msgbrokenr& compiler_typer<`/Users/tom/Dropbox/Code/development/APLpy/APLpy/astropy_helpers/astropy_helpers/setup_helpers.pyadjust_compilerSsJ                r>c Csotjtj|dgdtj}|jdj}y|jd}Wntk rjdSYnX|S)Nz --versionstdoutrunknown) subprocessPopenshlexsplitPIPE communicatestrip IndexError)r#processoutputr$r<r<r=r+s"  r+cCstddkrtdntitjjtjdd6tjddd6}|jj tdt ;y|j |j Wnt ttfk rYnXWdQX|S)zReturns a distutils Distribution object used to instrument the setup environment before calling the actual setup() function. rNzastropy_helpers.setup_helpers.register_commands() must be called before using astropy_helpers.setup_helpers.get_dummy_distribution()r script_namer script_args)r( RuntimeErrorrr)pathbasenamer1argvcmdclassupdaterparse_config_filesparse_command_liner AttributeError SystemExit)distr<r<r=get_dummy_distributions     rXcCsXt}xH|D]<}|jj|}|dk r||kr||dSqWdSdS)ap Returns the value of the given distutils option. Parameters ---------- option : str The name of the option commands : list of str The list of commands on which this option is available Returns ------- val : str or None the value of the given distutils option. If the option is not set, returns None. Nr)rXcommand_optionsget)optioncommandsrWcmdZcmd_optsr<r<r=get_distutils_options   r^cCst|dddgS)a! Returns the value of the given distutils build option. Parameters ---------- option : str The name of the option Returns ------- val : str or None The value of the given distutils build option. If the option is not set, returns None. buildr build_clib)r^)r[r<r<r=r5 sr5cCst|dgS)a# Returns the value of the given distutils install option. Parameters ---------- option : str The name of the option Returns ------- val : str or None The value of the given distutils build option. If the option is not set, returns None. r)r^)r[r<r<r=get_distutils_install_optionsracCst|ddddgS)a7 Returns the value of the given distutils build or install option. Parameters ---------- option : str The name of the option Returns ------- val : str or None The value of the given distutils build or install option. If the option is not set, returns None. r_rr`r)r^)r[r<r<r=%get_distutils_build_or_install_option-srbcCs&td}|dkr"tjS|S)a! Determines the compiler that will be used to build extension modules. Returns ------- compiler : str The compiler option specificied for the build, build_ext, or build_clib command; or the default compiler for the platform if none was specified. r#N)r5rr6)r#r<r<r=get_compiler_option?s   rcc syt|ddgd}Wnttfk r=d}YnXttfddddgDrttd}n t|}|dk r||krjd}d |_n|S) z Determines if the build is in debug mode. Returns ------- debug : bool True if the current build was started with the debug option, False otherwise. fromlistdebugrNc3s|]}|jkVqdS)N)r\).0r])rWr<r= fsz#get_debug_option..r_rT) get_pkg_version_module ImportErrorrUrXanyboolr5get_command_class force_rebuild) packagenameZ current_debugreZ build_ext_cmdr<)rWr=get_debug_optionRs   %  rocsT|st|dddgSt|dd|tfdd|DSdS)aReturns the package's .version module generated by `astropy_helpers.version_helpers.generate_version_py`. Raises an ImportError if the version module is not found. If ``fromlist`` is an iterable, return a tuple of the members of the version module corresponding to the member names given in ``fromlist``. Raises an `AttributeError` if any of these module members are not found. z.versionrdc3s|]}t|VqdS)N)getattr)rfmember)modr<r=rgsz)get_pkg_version_module..N) __import__tuple)rnrdr<)rsr=rhus rhcCstddk rtdSit|d6td6t||d6td6td6td6td6td<}td r}t|d .finalize_optionsc s~t}xP|jD]E}d|jkr`|jjd}|jj|||jjdnxt|jD]\}}|jdr|}|ddd}n,|jdr|ddd}|}nt j j |sqpn|j r||j|.runFrmrr user_optionsboolean_optionsr) should_build_with_cythonZCython.DistutilsrSetuptoolsBuildExtdict__dict__rqrrrobject)rnrrZbaseclsattrsrrr<)rrrnr=r{s" #>     r{cCs<dj|jtjdd}tjj|jd|S)Nz.{0}-{1}rrlib)r/Z plat_namer1r$r)rNrZ build_base)r]Zplat_specifierr<r<r=_get_platlib_dirMs"rc@sBeZdZejddZejddZddZdS)r}NcCs5|jd}t|}||_tj|dS)Nr_)get_finalized_commandrrSetuptoolsInstallr)r build_cmd platlib_dirr<r<r=rVs  zAstropyInstall.finalize_options)r __module__ __qualname__rrrrr<r<r<r=r}Rs r}c@sBeZdZejddZejddZddZdS)r~NcCs5|jd}t|}||_tj|dS)Nr_)rr build_dirSetuptoolsInstallLibr)rrrr<r<r=ras  z"AstropyInstallLib.finalize_options)rrrrrrrr<r<r<r=r~]s r~c@s]eZdZejddZejddZddZdddZddZdS) r|NcCsG|jd}t|}||_||_||_tj|dS)Nr_)rrZ build_purelibrSetuptoolsBuildPyr)rrrr<r<r=rls     zAstropyBuildPy.finalize_optionsFcCs|jj}g}xV|D]N}xE|D]0}|t|jddj|r&Pq&q&W|j|qWtj|||dS)Nr) distribution skip_2to3lenr startswithappendrrun_2to3)rfilesZdoctestsrZfiltered_filesfiler8r<r<r=rzs   &zAstropyBuildPy.run_2to3cCstj|dS)N)rr)rr<r<r=rszAstropyBuildPy.run) rrrrrrrrrr<r<r<r=r|hs   r|c Cslt}|j|}t|dr:||jkr:dS|jdd}t||rytdj|||nxvt|jD]e\}}|d|krt j dj|||j|=||j kr|j j |nPqqW|jj |d|f|r$|j j |nt||dt|dsXt|g|_n|jj|dS)a Add a custom option to a setup command. Issues a warning if the option already exists on that command. Parameters ---------- command : str The name of the command as given on the command line name : str The name of the build option doc : str A short description of the option, for the `--help` message is_bool : bool, optional When `True`, the option is a boolean option and doesn't require an associated value. _astropy_helpers_optionsN-_zc{0!r} already has a {1!r} class attribute, barring {2!r} from being usable as a custom option name.rz&Overriding existing {0!r} option {1!r})rXrlhasattrrreplacerMr/rrrr0rrrsetattrsetadd) commandrdocis_boolrWZcmdclsattrrr]r<r<r=rs0  rc@sZeZdZdZejd gZejdgZddZddZd d Z dS) raExtends the built in 'register' command to support a ``--hidden`` option to make the registered version hidden on PyPI by default. The result of this is that when a version is registered as "hidden" it can still be downloaded from PyPI, but it does not show up in the list of actively supported versions under http://pypi.python.org/pypi/astropy, and is not set as the most recent version. Although this can always be set through the web interface it may be more convenient to be able to specify via the 'register' command. Hidden may also be considered a safer default when running the 'register' command, though this command uses distutils' normal behavior if the ``--hidden`` option is omitted. hiddenN.mark this release as hidden on PyPI by defaultcCstj|d|_dS)NF)SetuptoolsRegisterinitialize_optionsr)rr<r<r=rs z"AstropyRegister.initialize_optionscCs8tj||}|dkr4|jr4d|d|ndS)$Nr) pathname2urlrpZ_staticz-Attempted to build_sphinx in a location wherez is a file. Must be a directory.r_astropy_helpersz1.3b1z'from __future__ import print_function z from sphinx.setup_command import * os.chdir({srcdir!r}) sys.path.insert(0, {build_cmd_path!r}) sys.path.insert(0, {ah_path!r}) build_cmd_pathah_pathsrcdirrrZ_dirzconfoverrides = {}z*confoverrides = {'intersphinx_mapping':{}}z.css|]}d|dVqdS)z--rNr<)rfrr<r<r=rgsz-hz--helpZcleanrZsetoptZsaveoptsegg_infoalias)rrdisplay_optionsrunion)Zshort_display_optsZlong_display_optsZdisplay_commandsr<r<r=get_distutils_display_optionss    r1cCs/t}tttjddj|S)zm Returns True if sys.argv contains any of the distutils display options such as --version or --name. rN)r1rkrr1rP intersection)r/r<r<r=is_distutils_display_options r3cCs]t|}|j|d|j|dtt||d}|j|ddS)z This function is deprecated and maintained for backward compatibility with affiliated packages. Affiliated packages should update their setup.py to use `get_package_info` instead. ext_modules package_datapackagesrN)get_package_infoextendrRlistr)r rr5 packagenamesZ package_dirsrr<r<r=update_package_filess  r;.cCs-g}g}i}i}g}tt|d|}xt||D]}t|dr|j}x|D]} td| qnWnt|dr|j} x| D]} t| qWnt|dr|j} nd} | sF|j t j j |j qFqFWxat||D]P}t|drI|j|jnt|dr|j|jqqW|jt|||d gx?ttt|D]%\} }|jd kr|| =qqWtd krx!|D]}|jj d qWni|d 6|d6|d6|d6|d6S)a! Collates all of the information for building all subpackages subpackages and returns a dictionary of keyword arguments that can be passed directly to `distutils.setup`. The purpose of this function is to allow subpackages to update the arguments to the package's ``setup()`` function in its setup.py script, rather than having to specify all extensions/package data directly in the ``setup.py``. See Astropy's own ``setup.py`` for example usage and the Astropy development docs for more details. This function obtains that information by iterating through all packages in ``srcdir`` and locating a ``setup_package.py`` module. This module can contain the following functions: ``get_extensions()``, ``get_package_data()``, ``get_build_options()``, ``get_external_libraries()``, and ``requires_2to3()``. Each of those functions take no arguments. - ``get_extensions`` returns a list of `distutils.extension.Extension` objects. - ``get_package_data()`` returns a dict formatted as required by the ``package_data`` argument to ``setup()``. - ``get_build_options()`` returns a list of tuples describing the extra build options to add. - ``get_external_libraries()`` returns a list of libraries that can optionally be built using external dependencies. - ``requires_2to3()`` should return `True` when the source code requires `2to3` processing to run on Python 3.x. If ``requires_2to3()`` is missing, it is assumed to return `True`. excludeget_build_optionsr_get_external_libraries requires_2to3Tget_extensionsget_package_datarZ skip_cythonmsvcz /MANIFESTr4r6rr5r)filter_packagesriter_setup_packagesrr>rr?add_external_libraryr@rr)rNrrr8rArRrBget_cython_extensionsreversedr9rrrcextra_link_args)r r=r4r6r5rrZsetuppkgrr[ librarieslibraryr@r'rr<r<r=r7sP(     % r7ccsxy|D]q}|jd}tjj||}tjjtjj|d}tjj|rt|}|VqqWdS)a6 A generator that finds and imports all of the ``setup_package.py`` modules in the source packages. Returns ------- modgen : generator A generator that yields (modname, mod), where `mod` is the module and `modname` is the module name for the ``setup_package.py`` modules. r<zsetup_package.pyN)rDr)rNrrr import_file)r r6rn package_parts package_pathZ setup_packagemoduler<r<r=rEys   rEccsxt|D]~\}}}xk|D]c}|jdr#tjjtjj||}dj||ddg}||fVq#q#WPq WdS)a A generator that yields Cython source files (ending in '.pyx') in the source packages. Returns ------- pyxgen : generator A generator that yields (extmod, fullfn) where `extmod` is the full name of the module that the .pyx file would live in based on the source directory structure, and `fullfn` is the path to the .pyx file. z.pyxr<Nrr)rrr)rNrr)rrdirpathdirnames filenamesfnZfullfnextmodr<r<r=iter_pyx_filess  !rUcCsy t|ddddg}Wntk r:d}YnX|dkrx|dk rxy |j}Wqxtk rtYqxXny |j}Wntk rd}YnXtdr| s|dkr|SdSdS) aReturns the previously used Cython version (or 'unknown' if not previously built) if Cython should be used to build extension modules from pyx files. If the ``release`` parameter is not specified an attempt is made to determine the release flag from `astropy.version`. z.cython_versionrdrrNr@rF)rtrirrUrr()r8rZversion_modulerr<r<r=rs$         rcCsg}g}xc|D][}xR|jD]G}|jdr#tjjtjj|d}|j|q#q#WqWx|D]} | jd} tjj|| } xmt | | D]\\} } tjjtjj| d}||kr|jt | | gd|qqWqyW|S)a Looks for Cython files and generates Extensions if needed. Parameters ---------- srcdir : str Path to the root of the source directory to search. prevextensions : list of `~distutils.core.Extension` objects The extensions that are already defined. Any .pyx files already here will be ignored. extincludedirs : list of str or None Directories to include as the `include_dirs` argument to the generated `~distutils.core.Extension` objects. Returns ------- exts : list of `~distutils.core.Extension` objects The new extensions that are needed to compile all .pyx files (does not include any already in `prevextensions`). .pyx.crr<r)rVrW) rrr)rNrealpathsplitextrrDrrUr )r r6ZprevextensionsZextincludedirsZprevsourcepathsr4rsZ sourcepathrrMrNrTrr<r<r=rGs  " " rGcCst|tsttjj|rNt|d}|j}WdQXnd}||krt|d}|j|WdQXndS)z Write `data` to `filename`, if the content of the file is different. Parameters ---------- filename : str The file name to be written to. data : bytes The data to be written to `filename`. rbNwb) isinstancebytesAssertionErrorr)rNexistsrreadr)filenamerfdZ original_datar<r<r=write_if_differents  rdc Cstjddkr_ddl}t|dr7|`nddl}ddl}|j|n:ddl}t|dr|`nddl}t|y|j }Wnt k r|j }YnX|S)z- Gets the path to the numpy headers. rrN__NUMPY_SETUP__) r1 version_infobuiltinsrreimprreload __builtin__Z get_includerUZget_numpy_include)rgrhrrjZ numpy_includer<r<r=rs"          rcCsst|d^}djtjjtjj|djtjdd}tj |||dSWdQXdS)zb Imports a module from a single file as if it doesn't belong to a particular package. UrrrN.py)rlrkr) rrr)rNrrYrDseprh load_module)rbrcrr<r<r=rL3s ;rLcs4eZdZdZfddZddZS)DistutilsExtensionArgsz A special dictionary whose default values are the empty list. This is useful for building up a set of arguments for `distutils.Extension` without worrying whether the entry is already present. cs,dd}tt|j|||dS)NcSsgS)Nr<r<r<r<r=default_factoryNsz8DistutilsExtensionArgs.__init__..default_factory)superro__init__)rargskwargsrp) __class__r<r=rrMs zDistutilsExtensionArgs.__init__cCs2x+|jD]\}}||j|q WdS)N)rr8)rotherkeyr(r<r<r=rRTszDistutilsExtensionArgs.update)rrrrrrrRr<r<)rur=roEs roz pkg-configc Csidd6dd6dd6dd6d d 6}d j|d j|f}t}y8tj|d ddtj}|jdj}Wntjk r}zkdj|dj|j dj|j dj|j g} t j dj| |dj|WYdd}~XnX|j dkrpdjdj|dg} t j dj| |dj|nx|jD]} | ddjd} | ddjtj} | |kr| dkrt| jdd} n||| j| q}|dj| q}W|S)a Uses pkg-config to update a set of distutils Extension arguments to include the flags necessary to link against the given packages. If the pkg-config lookup fails, default_libraries is applied to libraries. Parameters ---------- packages : list of str A list of pkg-config packages to look up. default_libraries : list of str A list of library names to use if the pkg-config lookup fails. Returns ------- config : dict A dictionary containing keyword arguments to `distutils.Extension`. These entries include: - ``include_dirs``: A list of include directories - ``library_dirs``: A list of library directories - ``libraries``: A list of libraries - ``define_macros``: A list of macro defines - ``undef_macros``: A list of macros to undefine - ``extra_compile_args``: A list of extra arguments to pass to the compiler rz-I library_dirsz-LrJz-l define_macrosz-D undef_macrosz-Uz{0} --libs --cflags {1} shellTr?rz4{0} failed. This may cause the build to fail below.z command: {0}z returncode: {0}z output: {0} Nz.pkg-config could not lookup up package(s) {0}.z, z'This may cause the build to fail below.rascii=rextra_compile_args)r/rrorArBrErFrGCalledProcessErrorr]r#rJrr0r8rDdecoder1getfilesystemencodingrur) r6Zdefault_librariesrZflag_maprresultpiperJelinestokenargrr<r<r= pkg_configYs:  $   rcCsGx@dddgD]/}t|td|dj|ddqWdS) z Add a build option for selecting the internal or system copy of a library. Parameters ---------- library : str The name of the library. If the library is `foo`, the build option will be called `--use-system-foo`. r_rrz use-system-zUse the system {0} libraryrTN)rr$r/)rKrr<r<r=rFs rFcCstdj|ptdS)a Returns `True` if the build configuration indicates that the given library should use the system copy of the library rather than the internal one. For the given library `foo`, this will be `True` if `--use-system-foo` or `--use-system-libraries` was provided at the commandline or in `setup.cfg`. Parameters ---------- library : str The name of the library Returns ------- use_system : bool `True` if the build should use the system copy of the library. zuse_system_{0}Zuse_system_libraries)rbr/)rKr<r<r=use_system_librarysrcs,trdndfdd|DS)zw Removes some packages from the package list that shouldn't be installed on the current version of Python. Z_py2Z_py3cs%g|]}|js|qSr<)r)rfx)r=r<r= s z#filter_packages..)r)r:r<)r=r=rDs rDc @s}eZdZdZdd d!d"d#d$d%d&d'd(d)g Zejd*ejd+ejd,ejd-ddZd S).rzz A dummy build_sphinx command that is called if Sphinx is not installed and displays a relevant error message fresh-envErp all-filesa source-dir=rZ build-dir=N config-dir=cbuilder=bproject=version=release=today= link-indexr'warnings-returncoder clean-docsrno-intersphinxropen-docs-in-browserrc Cs9ytdWn"tjdtjdYnXdS)Nz)Sphinx must be installed for build_sphinxz1error : Sphinx must be installed for build_sphinxr)rMrerrorr1r2)rr<r<r=rs  z"FakeBuildSphinx.initialize_options)rrrp)rrrp)rrZrp)rNrp)rrrp)rrrp)rNrp)rNrp)rNrp)rNrp)rr'rp)rrrp)rrrp)rrrp)rrrp)rrrrrrrr<r<r<r=rs"      r)pr __future__rr collectionsrrhrr)rr3rCrrAr1r- distutilsrrr distutils.cmdrdistutils.distrdistutils.errorsr r distutils.corer r Zdistutils.command.sdistr rzdistutils.versionrZsetuptools.command.build_extrrZsetuptools.command.build_pyrrZsetuptools.command.installrrZsetuptools.command.install_librrZsetuptools.command.registerrr setuptoolsrZ test_helpersrutilsrrrr(rrirZsphinx.setup_commandrr ValueErrorrrsr0 SyntaxErrorrfrrr>r+rXr^r5rarbrcrorhrryr{rr}r~r|rrrr1r3r;r7rErUrrurGrdrrL defaultdictrorrFrrDrr<r<r<r=s                    p        # 5   $A4   j  $1   O   APLpy-1.0/astropy_helpers/astropy_helpers/__pycache__/test_helpers.cpython-34.pyc0000644000077000000240000001540012470157570030270 0ustar tomstaff00000000000000 oTu%@sddlmZmZmZmZddlZddlZddlZddlZddl Z ddl Z ddl m Z ddl mZe jddkZGddde eZdS) )absolute_importdivisionprint_functionunicode_literalsN)Command)_fix_user_optionsc@seZdZdZd6d7d8d9d:d;d<d=d>d?d@dAdBdCdDgZeeZd-Zd.d/Zd0d1Zd2d3Z d4d5Z d'S)E AstropyTestzRun the tests for this packagepackage=PwThe name of a specific package to test, e.g. 'io.fits' or 'utils'. If nothing is specified, all default tests are run. test-path=tHSpecify a test location by path. If a relative path to a .py file, it is relative to the built package, so e.g., a leading "astropy/" is necessary. If a relative path to a .rst file, it is relative to the directory *below* the --docs-path directory, so a leading "docs/" is usually necessary. May also be an absolute path.verbose-resultsV#Turn on verbose output from pytest.plugins=p&Plugins to enable when running pytest. pastebin=b8Enable pytest pastebin output. Either 'all' or 'failed'.args=a,Additional arguments to be passed to pytest. remote-dataR$Run tests that download remote data.pep88PEnable PEP8 checking and disable regular tests. Requires the pytest-pep8 plugin.pdbd0Start the interactive Python debugger on errors.coveragec8Create a coverage report. Requires the coverage package. open-filesoAFail if any tests leave files open. Requires the psutil package. parallel=jRun the tests in parallel on the specified number of CPUs. If negative, all the cores on the machine will be used. Requires the pytest-xdist plugin. docs-path=NThe path to the documentation .rst files. If not provided, and the current directory contains a directory called "docs", that will be used. skip-docs(Don't test the documentation .rst files.repeat=PHow many times to repeat each test (can be used to check for sporadic failures).cCsd|_d|_d|_d|_d|_d|_d|_d|_d|_d|_ d|_ d|_ d|_ d|_ d|_dS)NFr)packageZ test_pathZverbose_resultsZpluginsZpastebinargsZ remote_datar r#r&Z open_filesparallel docs_pathZ skip_docsrepeat)selfr<_/Users/tom/Dropbox/Code/development/APLpy/APLpy/astropy_helpers/astropy_helpers/test_helpers.pyinitialize_optionsDs              zAstropyTest.initialize_optionscCsdS)Nr<)r;r<r<r=finalize_optionsUszAstropyTest.finalize_optionsc+sRyddl}Wntk r0tdYnXjdddjdjd}tjj|j}j dkrtjj drtjjd_ qntjjj _ t j dj d}tjj|tjj|}tj||tjd |d }d }zjrjdkrYtd nyddl}Wntk rtd YnXtjj|j d d} t| d} | j} WdQXtrd} nd} | jd| jdj } tjj|d} t| d}|j| jdWdQXdjtjjd| }djtjjd|}ntfddj}djfdd|D}trd}nd }d!}|j|d"|d#|d$|}tj t!j"d%d&|gd'|d(d}Wdtj#|Xt$|dS))NrzOThe 'test' command requires the astropy package to be installed and importable.buildinplaceFZdocsprefixz-test-z setup.cfgr5z*--coverage can not be used with --parallelz;--coverage requires that the coverage package is installed.tests coveragercr23z{ignore_python_version}z {packagename}wbzutf-8zZimport coverage; cov = coverage.coverage(data_file="{0}", config_file="{1}"); cov.start();z .coveragezgcov.stop(); from astropy.tests.helper import _save_coverage; _save_coverage(cov, result, "{0}", "{1}");.cs t|S)N)hasattr)arg)r;r<r=sz!AstropyTest.run..z, c3s*|] }dj|t|VqdS)z {0}={1!r}N)formatgetattr).0rK)r;r<r= sz"AstropyTest.run..z/import builtins; builtins._ASTROPY_TEST_ = Truez5import __builtin__; __builtin__._ASTROPY_TEST_ = Truezs{cmd_pre}{0}; import {1.package_name}, sys; result = {1.package_name}.test({test_args}); {cmd_post}sys.exit(result)cmd_precmd_post test_argsz-Bz-ccwd close_fds)%astropy ImportErrorreinitialize_command run_commandget_finalized_commandospathabspath build_libr9existstempfilemkdtemp package_namejoinbasenameshutilcopytreecopyr&r8 ValueErroropenreadPY3replacewriteencoderMfilter_get_test_runner_args subprocesscallsys executablermtree SystemExit)r;rV build_cmdnew_pathZtmp_dirZ testing_pathrQrRr&rDfdZcoveragerc_contentZignore_python_versionZtmp_coveragerctmprSZset_flagcmdretcoder<)r;r=runZsv  !       zAstropyTest.runc Cstrddl}d|_nddl}d|_zSt|j}t|dsltdjtnt j |j }|j SWdtr|`n|`XdS)a0 A hack to determine what arguments are supported by the package's test() function. In the future there should be a more straightforward API to determine this (really it should be determined by the ``TestRunner`` class for whatever version of Astropy is in use). rNTtestzVpackage {0} does not have a {0}.test() function as required by the Astropy test runner) rkbuiltinsZ_ASTROPY_TEST_ __builtin__ __import__rbrJrWrMinspect getargspecr~r7)r;rrpkgZargspecr<r<r=rps      z!AstropyTest._get_test_runner_args)r r r )rrr)rrr)rrr)rrr)rrr)rrr)zpep8r!r")zpdbr$r%)zcoverager'r()r)r*r+)r,r-r.)r/Nr0)r1Nr2)r3Nr4) __name__ __module__ __qualname__ description user_optionsrrbr>r?r}rpr<r<r<r=r sF      wr ) __future__rrrrrr[rerqrsr`distutils.corercompatr version_inforkobjectr r<r<r<r=s"      APLpy-1.0/astropy_helpers/astropy_helpers/__pycache__/utils.cpython-34.pyc0000644000077000000240000001203412470157570026727 0ustar tomstaff00000000000000 oT @sxddlZddlZddlZddlZy,ddlmZeeds[dZnWnek rvdZYnXej dddkrddlm Z n ddZ Gd d d e Z ej d d Zejd krddlZddZn ddZddZddddZddZddZej ddkrhddZn ddZdS)N) machinery SourceLoader)invalidate_cachescCsdS)NrrrX/Users/tom/Dropbox/Code/development/APLpy/APLpy/astropy_helpers/astropy_helpers/utils.pysr c@s4eZdZdZdZddZddZdS) _DummyFilezA noop writeable object.cCsdS)Nr)selfsrrrwrite sz_DummyFile.writecCsdS)Nr)r rrrflush#sz_DummyFile.flushN)__name__ __module__ __qualname____doc__errorsrrrrrrr s  r c cs~tj}tj}tt_tt_d}y dVWn#d}|t_|t_YnX|sz|t_|t_ndS)z:A context manager that silences sys.stdout and sys.stderr.FNT)sysstdoutstderrr ) old_stdout old_stderrexception_occurredrrrsilence's        rwin32c Cst|tr'|jtj}ny;tjjj|}|dksQt t |d@}Wnt t fk rd}YnX|S)z Returns True if the given filepath has the hidden attribute on MS-Windows. Based on a post here: http://stackoverflow.com/questions/284115/cross-platform-hidden-file-detection rF) isinstancebytesdecodergetfilesystemencodingctypesZwindllZkernel32ZGetFileAttributesWAssertionErrorboolAttributeError)filepathattrsresultrrr_has_hidden_attributeAs r*cCsdS)NFr)r'rrrr*QscCs^tjjtjj|}t|tr?|jd}n|jd}|p]t|S)z Determines if a given file or directory is hidden. Parameters ---------- filepath : str The path to a file or directory Returns ------- hidden : bool Returns `True` if the file is hidden ..)ospathbasenameabspathrr startswithr*)r'nameZ is_dottedrrris_path_hiddenUs r3Fccsxztj|ddd|d|D]W\}}}dd|D|dd