././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/0000755000076500000240000000000000000000000013451 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/CHANGES.rst0000644000076500000240000000250400000000000015254 0ustar00erikstaff000000000000000.6 (unreleased) ---------------- API Changes ^^^^^^^^^^^ New Features ^^^^^^^^^^^^ Bug Fixes ^^^^^^^^^ Other Changes and Additions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0.5.3 ----- Bug Fixes ^^^^^^^^^ - Fix comparison of FITSWCS objects in arithmetic operations. - Fix example documentation when run in python interpreter. 0.5.2 (2019-02-06) ---------------- Bug Fixes ^^^^^^^^^ - Bugfixes for astropy helpers, pep8 syntax checking, and plotting in docs [#416,#417,#419] - All automatically generated ``SpectrumList`` loaders now have identifiers. [#440] - ``SpectralRange.from_center`` parameters corrected after change to SpectralRange interface. [#433] Other Changes and Additions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Improve explanation on creating spectrum continua. [#420] - Wrap IO identifier functions to ensure they always return True or False and log any errors. [#404] 0.5.1 (2018-11-29) ------------------ Bug Fixes ^^^^^^^^^ - Fixed a bug in using spectral regions that have been inverted. [#403] - Use the pytest-remotedata plugin to control tests that require access to remote data. [#401,#408] 0.5 (2018-11-21) ---------------- This was the first release of specutils executing the [APE14](https://github.com/astropy/astropy-APEs/blob/master/APE14.rst) plan (i.e. the "new" specutils) and therefore intended for broad use. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/PKG-INFO0000644000076500000240000000057500000000000014555 0ustar00erikstaff00000000000000Metadata-Version: 1.2 Name: specutils Version: 0.7 Summary: Package for spectroscopic astronomical data Home-page: http://specutils.readthedocs.io/ Author: Specutils team Author-email: coordinators@astropy.org License: BSD Description: Provides data objects and analysis tools for creating and manipulating spectroscopic astronomical data. Platform: UNKNOWN Requires-Python: >=3.5 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/README.rst0000644000076500000240000000403600000000000015143 0ustar00erikstaff00000000000000Specutils ========= .. image:: https://travis-ci.org/astropy/specutils.svg?branch=master :target: https://travis-ci.org/astropy/specutils .. image:: https://readthedocs.org/projects/specutils/badge/?version=latest :target: http://specutils.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat :target: http://www.astropy.org/ Specutils is an `Astropy affiliated package `_ with the goal of providing a shared set of Python representations of astronomical spectra and basic tools to operate on these spectra. The effort is also meant to be a "hub", helping to unite the Python astronomical spectroscopy community around shared effort, much as Astropy is meant to for the wider astronomy Python ecosystem. This broader effort is outlined in the `APE13 document `_. Note that Specutils is not intended to meet all possible spectroscopic analysis or reduction needs. While it provides some standard analysis functionality (following the Python philosophy of "batteries included"), it is best thought of as a "tool box" that provides pieces of functionality that can be used to build a particular scientific workflow or higher-level analysis tool. To that end, it is also meant to facilitate connecting together disparate reduction pipelines and analysis tools through shared Python representations of spectroscopic data. Getting Started with Specutils ------------------------------ For details on installing and using Specutils, see the `specutils documentation `_. Note that Specutils now only supports Python 3. While some functionality may continue to work on Python 2, it is not tested and support cannot be guaranteed (due to the sunsetting of Python 2 support by the Python and Astropy development teams). License ------- Specutils is licensed under a 3-clause BSD style license. Please see the LICENSE.rst file. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/ah_bootstrap.py0000644000076500000240000011063400000000000016515 0ustar00erikstaff00000000000000""" 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 and by default the system-installed version of astropy-helpers will be used (however, ``ah_bootstrap.use_astropy_helpers`` may be called manually from within the setup.py script). This behavior can also be controlled using the ``--auto-use`` and ``--no-auto-use`` command-line flags. For clarity, an alias for ``--no-auto-use`` is ``--use-system-astropy-helpers``, and we recommend using the latter if needed. 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 io import locale import os import re import subprocess as sp import sys from distutils import log from distutils.debug import DEBUG from configparser import ConfigParser, RawConfigParser import pkg_resources from setuptools import Distribution from setuptools.package_index import PackageIndex # This is the minimum Python version required for astropy-helpers __minimum_python_version__ = (3, 5) # TODO: Maybe enable checking for a specific version of astropy_helpers? DIST_NAME = 'astropy-helpers' PACKAGE_NAME = 'astropy_helpers' UPPER_VERSION_EXCLUSIVE = None # Defaults for other options DOWNLOAD_IF_NEEDED = True INDEX_URL = 'https://pypi.python.org/simple' USE_GIT = True OFFLINE = False AUTO_UPGRADE = True # A list of all the configuration options and their required types CFG_OPTIONS = [ ('auto_use', bool), ('path', str), ('download_if_needed', bool), ('index_url', str), ('use_git', bool), ('offline', bool), ('auto_upgrade', bool) ] # Start off by parsing the setup.cfg file SETUP_CFG = ConfigParser() if os.path.exists('setup.cfg'): try: SETUP_CFG.read('setup.cfg') except Exception as e: if DEBUG: raise log.error( "Error reading setup.cfg: {0!r}\n{1} will not be " "automatically bootstrapped and package installation may fail." "\n{2}".format(e, PACKAGE_NAME, _err_help_msg)) # We used package_name in the package template for a while instead of name if SETUP_CFG.has_option('metadata', 'name'): parent_package = SETUP_CFG.get('metadata', 'name') elif SETUP_CFG.has_option('metadata', 'package_name'): parent_package = SETUP_CFG.get('metadata', 'package_name') else: parent_package = None if SETUP_CFG.has_option('options', 'python_requires'): python_requires = SETUP_CFG.get('options', 'python_requires') # The python_requires key has a syntax that can be parsed by SpecifierSet # in the packaging package. However, we don't want to have to depend on that # package, so instead we can use setuptools (which bundles packaging). We # have to add 'python' to parse it with Requirement. from pkg_resources import Requirement req = Requirement.parse('python' + python_requires) # We want the Python version as a string, which we can get from the platform module import platform # strip off trailing '+' incase this is a dev install of python python_version = platform.python_version().strip('+') # allow pre-releases to count as 'new enough' if not req.specifier.contains(python_version, True): if parent_package is None: message = "ERROR: Python {} is required by this package\n".format(req.specifier) else: message = "ERROR: Python {} is required by {}\n".format(req.specifier, parent_package) sys.stderr.write(message) sys.exit(1) if sys.version_info < __minimum_python_version__: if parent_package is None: message = "ERROR: Python {} or later is required by astropy-helpers\n".format( __minimum_python_version__) else: message = "ERROR: Python {} or later is required by astropy-helpers for {}\n".format( __minimum_python_version__, parent_package) sys.stderr.write(message) sys.exit(1) _str_types = (str, bytes) # What follows are several import statements meant to deal with install-time # issues with either missing or misbehaving pacakges (including making sure # setuptools itself is installed): # Check that setuptools 30.3 or later is present from distutils.version import LooseVersion try: import setuptools assert LooseVersion(setuptools.__version__) >= LooseVersion('30.3') except (ImportError, AssertionError): sys.stderr.write("ERROR: setuptools 30.3 or later is required by astropy-helpers\n") sys.exit(1) # typing as a dependency for 1.6.1+ Sphinx causes issues when imported after # initializing submodule with ah_boostrap.py # See discussion and references in # https://github.com/astropy/astropy-helpers/issues/302 try: import typing # noqa except ImportError: pass # 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 # noqa except ImportError: pass # matplotlib can cause problems if it is imported from within a call of # run_setup(), because in some circumstances it will try to write to the user's # home directory, resulting in a SandboxViolation. See # https://github.com/matplotlib/matplotlib/pull/4165 # Making sure matplotlib, if it is available, is imported early in the setup # process can mitigate this (note importing matplotlib.pyplot has the same # issue) try: import matplotlib matplotlib.use('Agg') import matplotlib.pyplot except: # Ignore if this fails for *any* reason* pass # End compatibility imports... class _Bootstrapper(object): """ Bootstrapper implementation. See ``use_astropy_helpers`` for parameter documentation. """ def __init__(self, path=None, index_url=None, use_git=None, offline=None, download_if_needed=None, auto_upgrade=None): if path is None: path = PACKAGE_NAME if not (isinstance(path, _str_types) or path is False): raise TypeError('path must be a string or False') if not isinstance(path, str): fs_encoding = sys.getfilesystemencoding() path = path.decode(fs_encoding) # path to unicode self.path = path # Set other option attributes, using defaults where necessary self.index_url = index_url if index_url is not None else INDEX_URL self.offline = offline if offline is not None else OFFLINE # If offline=True, override download and auto-upgrade if self.offline: download_if_needed = False auto_upgrade = False self.download = (download_if_needed if download_if_needed is not None else DOWNLOAD_IF_NEEDED) self.auto_upgrade = (auto_upgrade if auto_upgrade is not None else AUTO_UPGRADE) # 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 self.use_git = use_git if use_git is not None else USE_GIT # 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) self.is_submodule = False @classmethod def main(cls, argv=None): if argv is None: argv = sys.argv config = cls.parse_config() config.update(cls.parse_command_line(argv)) auto_use = config.pop('auto_use', False) bootstrapper = cls(**config) if auto_use: # Run the bootstrapper, otherwise the setup.py is using the old # use_astropy_helpers() interface, in which case it will run the # bootstrapper manually after reconfiguring it. bootstrapper.run() return bootstrapper @classmethod def parse_config(cls): if not SETUP_CFG.has_section('ah_bootstrap'): return {} config = {} for option, type_ in CFG_OPTIONS: if not SETUP_CFG.has_option('ah_bootstrap', option): continue if type_ is bool: value = SETUP_CFG.getboolean('ah_bootstrap', option) else: value = SETUP_CFG.get('ah_bootstrap', option) config[option] = value return config @classmethod def parse_command_line(cls, argv=None): if argv is None: argv = sys.argv config = {} # For now we just pop recognized ah_bootstrap options out of the # arg list. This is imperfect; in the unlikely case that a setup.py # custom command or even custom Distribution class defines an argument # of the same name then we will break that. However there's a catch22 # here that we can't just do full argument parsing right here, because # we don't yet know *how* to parse all possible command-line arguments. if '--no-git' in argv: config['use_git'] = False argv.remove('--no-git') if '--offline' in argv: config['offline'] = True argv.remove('--offline') if '--auto-use' in argv: config['auto_use'] = True argv.remove('--auto-use') if '--no-auto-use' in argv: config['auto_use'] = False argv.remove('--no-auto-use') if '--use-system-astropy-helpers' in argv: config['auto_use'] = False argv.remove('--use-system-astropy-helpers') return config def run(self): strategies = ['local_directory', 'local_file', 'index'] dist = None # First, remove any previously imported versions of astropy_helpers; # this is necessary for nested installs where one package's installer # is installing another package via setuptools.sandbox.run_setup, as in # the case of setup_requires for key in list(sys.modules): try: if key == PACKAGE_NAME or key.startswith(PACKAGE_NAME + '.'): del sys.modules[key] except AttributeError: # Sometimes mysterious non-string things can turn up in # sys.modules continue # Check to see if the path is a submodule self.is_submodule = self._check_submodule() for strategy in strategies: method = getattr(self, 'get_{0}_dist'.format(strategy)) dist = method() if dist is not None: break else: raise _AHBootstrapSystemExit( "No source found for the {0!r} package; {0} must be " "available and importable as a prerequisite to building " "or installing this package.".format(PACKAGE_NAME)) # This is a bit hacky, but if astropy_helpers was loaded from a # directory/submodule its Distribution object gets a "precedence" of # "DEVELOP_DIST". However, in other cases it gets a precedence of # "EGG_DIST". However, when activing the distribution it will only be # placed early on sys.path if it is treated as an EGG_DIST, so always # do that dist = dist.clone(precedence=pkg_resources.EGG_DIST) # Otherwise we found a version of astropy-helpers, so we're done # Just active the found distribution on sys.path--if we did a # download this usually happens automatically but it doesn't hurt to # do it again # Note: Adding the dist to the global working set also activates it # (makes it importable on sys.path) by default. try: pkg_resources.working_set.add(dist, replace=True) except TypeError: # Some (much) older versions of setuptools do not have the # replace=True option here. These versions are old enough that all # bets may be off anyways, but it's easy enough to work around just # in case... if dist.key in pkg_resources.working_set.by_key: del pkg_resources.working_set.by_key[dist.key] pkg_resources.working_set.add(dist) @property def config(self): """ A `dict` containing the options this `_Bootstrapper` was configured with. """ return dict((optname, getattr(self, optname)) for optname, _ in CFG_OPTIONS if hasattr(self, optname)) def get_local_directory_dist(self): """ Handle importing a vendored package from a subdirectory of the source distribution. """ if not os.path.isdir(self.path): return log.info('Attempting to import astropy_helpers from {0} {1!r}'.format( 'submodule' if self.is_submodule else 'directory', self.path)) dist = self._directory_import() if dist is None: log.warn( 'The requested path {0!r} for importing {1} does not ' 'exist, or does not contain a copy of the {1} ' 'package.'.format(self.path, PACKAGE_NAME)) elif self.auto_upgrade and not self.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 = self._do_upgrade(dist) if upgrade is not None: dist = upgrade return dist def get_local_file_dist(self): """ Handle importing from a source archive; this also uses setup_requires but points easy_install directly to the source archive. """ if not os.path.isfile(self.path): return log.info('Attempting to unpack and import astropy_helpers from ' '{0!r}'.format(self.path)) try: dist = self._do_download(find_links=[self.path]) except Exception as e: if DEBUG: raise log.warn( 'Failed to import {0} from the specified archive {1!r}: ' '{2}'.format(PACKAGE_NAME, self.path, str(e))) dist = None if dist is not None and self.auto_upgrade: # A version of astropy-helpers was found on the available path, but # check to see if a bugfix release is available on PyPI upgrade = self._do_upgrade(dist) if upgrade is not None: dist = upgrade return dist def get_index_dist(self): if not self.download: log.warn('Downloading {0!r} disabled.'.format(DIST_NAME)) return None log.warn( "Downloading {0!r}; run setup.py with the --offline option to " "force offline installation.".format(DIST_NAME)) try: dist = self._do_download() except Exception as e: if DEBUG: raise log.warn( 'Failed to download and/or install {0!r} from {1!r}:\n' '{2}'.format(DIST_NAME, self.index_url, str(e))) dist = None # No need to run auto-upgrade here since we've already presumably # gotten the most up-to-date version from the package index return dist def _directory_import(self): """ 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(self.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): # We use subprocess instead of run_setup from setuptools to # avoid segmentation faults - see the following for more details: # https://github.com/cython/cython/issues/2104 sp.check_output([sys.executable, 'setup.py', 'egg_info'], cwd=path) for dist in pkg_resources.find_distributions(path, True): # There should be only one... return dist return dist def _do_download(self, version='', find_links=None): if find_links: allow_hosts = '' index_url = None else: allow_hosts = None index_url = self.index_url # 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: if UPPER_VERSION_EXCLUSIVE is None: req = DIST_NAME else: req = '{0}<{1}'.format(DIST_NAME, UPPER_VERSION_EXCLUSIVE) attrs = {'setup_requires': [req]} # NOTE: we need to parse the config file (e.g. setup.cfg) to make sure # it honours the options set in the [easy_install] section, and we need # to explicitly fetch the requirement eggs as setup_requires does not # get honored in recent versions of setuptools: # https://github.com/pypa/setuptools/issues/1273 try: context = _verbose if DEBUG else _silence with context(): dist = _Distribution(attrs=attrs) try: dist.parse_config_files(ignore_option_errors=True) dist.fetch_build_eggs(req) except TypeError: # On older versions of setuptools, ignore_option_errors # doesn't exist, and the above two lines are not needed # so we can just continue pass # 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 {0} from {1}:\n{2}' if find_links: source = find_links[0] elif index_url != INDEX_URL: source = index_url else: source = 'PyPI' raise Exception(msg.format(DIST_NAME, source, repr(e))) def _do_upgrade(self, dist): # Build up a requirement for a higher bugfix release but a lower minor # release (so API compatibility is guaranteed) 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=self.index_url) upgrade = package_index.obtain(req) if upgrade is not None: return self._do_download(version=upgrade.version) def _check_submodule(self): """ 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 (self.path is None or (os.path.exists(self.path) and not os.path.isdir(self.path))): return False if self.use_git: return self._check_submodule_using_git() else: return self._check_submodule_no_git() def _check_submodule_using_git(self): """ 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. """ cmd = ['git', 'submodule', 'status', '--', self.path] try: log.info('Running `{0}`; use the --no-git option to disable git ' 'commands'.format(' '.join(cmd))) returncode, stdout, stderr = run_cmd(cmd) except _CommandNotFound: # 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 stderr = stderr.strip() if returncode != 0 and 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 # 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 unknown error condition occurred log.warn('git submodule command failed ' 'unexpectedly:\n{0}'.format(stderr)) return False # 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( r'^(?P[+-U ])(?P[0-9a-f]{40}) ' r'(?P\S+)( .*)?$') # 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 self._update_submodule(m.group('submodule'), m.group('status')) return True else: log.warn( 'Unexpected output from `git submodule status`:\n{0}\n' 'Will attempt import from {1!r} regardless.'.format( stdout, self.path)) return False def _check_submodule_no_git(self): """ 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, self.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 == self.path.rstrip(os.sep): return True return False def _update_submodule(self, submodule, status): if status == ' ': # The submodule is up to date; no action necessary return elif status == '-': if self.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 self.offline: cmd.append('--no-fetch') elif status == 'U': raise _AHBootstrapSystemExit( '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: log.info('Running `{0}`; use the --no-git option to disable git ' 'commands'.format(' '.join(cmd))) returncode, stdout, stderr = run_cmd(cmd) except OSError as e: err_msg = str(e) else: if returncode != 0: err_msg = stderr if err_msg is not None: log.warn('An unexpected error occurred updating the git submodule ' '{0!r}:\n{1}\n{2}'.format(submodule, err_msg, _err_help_msg)) class _CommandNotFound(OSError): """ An exception raised when a command run with run_cmd is not found on the system. """ def run_cmd(cmd): """ Run a command in a subprocess, given as a list of command-line arguments. Returns a ``(returncode, stdout, stderr)`` tuple. """ try: p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) # XXX: May block if either stdout or stderr fill their buffers; # however for the commands this is currently used for that is # unlikely (they should have very brief output) stdout, stderr = p.communicate() except OSError as e: if DEBUG: raise if e.errno == errno.ENOENT: msg = 'Command not found: `{0}`'.format(' '.join(cmd)) raise _CommandNotFound(msg, cmd) else: raise _AHBootstrapSystemExit( 'An unexpected error occurred when running the ' '`{0}` command:\n{1}'.format(' '.join(cmd), 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' # Unlikely to fail at this point but even then let's be flexible if not isinstance(stdout, str): stdout = stdout.decode(stdio_encoding, 'replace') if not isinstance(stderr, str): stderr = stderr.decode(stdio_encoding, 'replace') return (p.returncode, stdout, stderr) 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 _verbose(): yield @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:]) BOOTSTRAPPER = _Bootstrapper.main() def use_astropy_helpers(**kwargs): """ 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 initialized 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`. offline : bool, optional If `False` disable all actions that require an internet connection, including downloading packages from the package index and fetching updates to any git submodule. Defaults to `True`. """ global BOOTSTRAPPER config = BOOTSTRAPPER.config config.update(**kwargs) # Create a new bootstrapper with the updated configuration and run it BOOTSTRAPPER = _Bootstrapper(**config) BOOTSTRAPPER.run() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/astropy_helpers/0000755000076500000240000000000000000000000016674 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/CHANGES.rst0000644000076500000240000005252300000000000020505 0ustar00erikstaff00000000000000astropy-helpers Changelog ************************* 3.2.1 (2019-06-13) ------------------ - Reverting issuing deprecation warning for the ``build_sphinx`` command. [#482] - Make sure that all data files get included in tar file releases. [#485] 3.2 (2019-05-29) ---------------- - Make sure that ``[options.package_data]`` in setup.cfg is taken into account when collecting package data. [#453] - Simplified the code for the custom build_ext command. [#446] - Avoid importing the astropy package when trying to get the test command when testing astropy itself. [#450] - Avoid importing whole package when trying to get version information. Note that this has also introduced a small API change - ``cython_version`` and ``compiler`` can no longer be imported from the ``package.version`` module generated by astropy-helpers. Instead, you can import these from ``package.cython_version`` and ``package.compiler_version`` respectively. [#442] - Make it possible to call ``generate_version_py`` and ``register_commands`` without any arguments, which causes information to be read in from the ``setup.cfg`` file. [#440] - Simplified setup.py and moved most of the configuration to setup.cfg. [#445] - Add a new ``astropy_helpers.setup_helpers.setup`` function that does all the default boilerplate in typical ``setup.py`` files that use astropy-helpers. [#443] - Remove ``deprecated``, ``deprecated_attribute``, and ``minversion`` from ``astropy_helpers.utils``. [#447] - Updated minimum required version of setuptools to 30.3.0. [#440] - Remove functionality to adjust compilers if a broken compiler is detected. This is not useful anymore as only a single compiler was previously patched (now unlikely to be used) and this was only to fix a compilation issue in the core astropy package. [#421] - ``sphinx-astropy`` is now a required dependency to build the docs, the machinery to install it as eggs have been removed. [#474] 3.1.1 (2019-02-22) ------------------ - Moved documentation from README to Sphinx. [#444] - Fixed broken OpenMP detection when building with ``-coverage``. [#434] 3.1 (2018-12-04) ---------------- - Added extensive documentation about astropy-helpers to the README.rst file. [#416] - Fixed the compatibility of the build_docs command with Sphinx 1.8 and above. [#413] - Removing deprecated test_helpers.py file. [#369] - Removing ez_setup.py file and requiring setuptools 1.0 or later. [#384] - Remove all sphinx components from ``astropy-helpers``. These are now replaced by the ``sphinx-astropy`` package in conjunction with the ``astropy-theme-sphinx``, ``sphinx-automodapi``, and ``numpydoc`` packages. [#368] - openmp_helpers.py: Make add_openmp_flags_if_available() work for clang. The necessary include, library, and runtime paths now get added to the C test code used to determine if openmp works. Autogenerator utility added ``openmp_enabled.is_openmp_enabled()`` which can be called post build to determine state of OpenMP support. [#382] - Add version_info tuple to autogenerated version.py. Allows for simple version checking, i.e. version_info > (2,0,1). [#385] 3.0.2 (2018-06-01) ------------------ - Nothing changed. 3.0.1 (2018-02-22) ------------------ - Nothing changed. 3.0 (2018-02-09) ---------------- - Removing Python 2 support, including 2to3. Packages wishing to keep Python 2 support should NOT update to this version. [#340] - Removing deprecated _test_compat making astropy a hard dependency for packages wishing to use the astropy tests machinery. [#314] - Removing unused 'register' command since packages should be uploaded with twine and get registered automatically. [#332] 2.0.10 (2019-05-29) ------------------- - Removed ``tocdepthfix`` sphinx extension that worked around a big in Sphinx that has been long fixed. [#475] - Allow Python dev versions to pass the python version check. [#476] - Updated bundled version of sphinx-automodapi to v0.11. [#478] 2.0.9 (2019-02-22) ------------------ - Updated bundled version of sphinx-automodapi to v0.10. [#439] - Updated bundled sphinx extensions version to sphinx-astropy v1.1.1. [#454] - Include package name in error message for Python version in ``ah_bootstrap.py``. [#441] 2.0.8 (2018-12-04) ------------------ - Fixed compatibility with Sphinx 1.8+. [#428] - Fixed error that occurs when installing a package in an environment where ``numpy`` is not already installed. [#404] - Updated bundled version of sphinx-automodapi to v0.9. [#422] - Updated bundled version of numpydoc to v0.8.0. [#423] 2.0.7 (2018-06-01) ------------------ - Removing ez_setup.py file and requiring setuptools 1.0 or later. [#384] 2.0.6 (2018-02-24) ------------------ - Avoid deprecation warning due to ``exclude=`` keyword in ``setup.py``. [#379] 2.0.5 (2018-02-22) ------------------ - Fix segmentation faults that occurred when the astropy-helpers submodule was first initialized in packages that also contained Cython code. [#375] 2.0.4 (2018-02-09) ------------------ - Support dotted package names as namespace packages in generate_version_py. [#370] - Fix compatibility with setuptools 36.x and above. [#372] - Fix false negative in add_openmp_flags_if_available when measuring code coverage with gcc. [#374] 2.0.3 (2018-01-20) ------------------ - Make sure that astropy-helpers 3.x.x is not downloaded on Python 2. [#362, #363] - The bundled version of sphinx-automodapi has been updated to v0.7. [#365] - Add --auto-use and --no-auto-use command-line flags to match the ``auto_use`` configuration option, and add an alias ``--use-system-astropy-helpers`` for ``--no-auto-use``. [#366] 2.0.2 (2017-10-13) ------------------ - Added new helper function add_openmp_flags_if_available that can add OpenMP compilation flags to a C/Cython extension if needed. [#346] - Update numpydoc to v0.7. [#343] - The function ``get_git_devstr`` now returns ``'0'`` instead of ``None`` when no git repository is present. This allows generation of development version strings that are in a format that ``setuptools`` expects (e.g. "1.1.3.dev0" instead of "1.1.3.dev"). [#330] - It is now possible to override generated timestamps to make builds reproducible by setting the ``SOURCE_DATE_EPOCH`` environment variable [#341] - Mark Sphinx extensions as parallel-safe. [#344] - Switch to using mathjax instead of imgmath for local builds. [#342] - Deprecate ``exclude`` parameter of various functions in setup_helpers since it could not work as intended. Add new function ``add_exclude_packages`` to provide intended behavior. [#331] - Allow custom Sphinx doctest extension to recognize and process standard doctest directives ``testsetup`` and ``doctest``. [#335] 2.0.1 (2017-07-28) ------------------ - Fix compatibility with Sphinx <1.5. [#326] 2.0 (2017-07-06) ---------------- - Add support for package that lies in a subdirectory. [#249] - Removing ``compat.subprocess``. [#298] - Python 3.3 is no longer supported. [#300] - The 'automodapi' Sphinx extension (and associated dependencies) has now been moved to a standalone package which can be found at https://github.com/astropy/sphinx-automodapi - this is now bundled in astropy-helpers under astropy_helpers.extern.automodapi for convenience. Version shipped with astropy-helpers is v0.6. [#278, #303, #309, #323] - The ``numpydoc`` Sphinx extension has now been moved to ``astropy_helpers.extern``. [#278] - Fix ``build_docs`` error catching, so it doesn't hide Sphinx errors. [#292] - Fix compatibility with Sphinx 1.6. [#318] - Updating ez_setup.py to the last version before it's removal. [#321] 1.3.1 (2017-03-18) ------------------ - Fixed the missing button to hide output in documentation code blocks. [#287] - Fixed bug when ``build_docs`` when running with the clean (-l) option. [#289] - Add alternative location for various intersphinx inventories to fall back to. [#293] 1.3 (2016-12-16) ---------------- - ``build_sphinx`` has been deprecated in favor of the ``build_docs`` command. [#246] - Force the use of Cython's old ``build_ext`` command. A new ``build_ext`` command was added in Cython 0.25, but it does not work with astropy-helpers currently. [#261] 1.2 (2016-06-18) ---------------- - Added sphinx configuration value ``automodsumm_inherited_members``. If ``True`` this will include members that are inherited from a base class in the generated API docs. Defaults to ``False`` which matches the previous behavior. [#215] - Fixed ``build_sphinx`` to recognize builds that succeeded but have output *after* the "build succeeded." statement. This only applies when ``--warnings-returncode`` is given (which is primarily relevant for Travis documentation builds). [#223] - Fixed ``build_sphinx`` the sphinx extensions to not output a spurious warning for sphinx versions > 1.4. [#229] - Add Python version dependent local sphinx inventories that contain otherwise missing references. [#216] - ``astropy_helpers`` now require Sphinx 1.3 or later. [#226] 1.1.2 (2016-03-9) ----------------- - The CSS for the sphinx documentation was altered to prevent some text overflow problems. [#217] 1.1.1 (2015-12-23) ------------------ - Fixed crash in build with ``AttributeError: cython_create_listing`` with older versions of setuptools. [#209, #210] 1.1 (2015-12-10) ---------------- - The original ``AstropyTest`` class in ``astropy_helpers``, which implements the ``setup.py test`` command, is deprecated in favor of moving the implementation of that command closer to the actual Astropy test runner in ``astropy.tests``. Now a dummy ``test`` command is provided solely for informing users that they need ``astropy`` installed to run the tests (however, the previous, now deprecated implementation is still provided and continues to work with older versions of Astropy). See the related issue for more details. [#184] - Added a useful new utility function to ``astropy_helpers.utils`` called ``find_data_files``. This is similar to the ``find_packages`` function in setuptools in that it can be used to search a package for data files (matching a pattern) that can be passed to the ``package_data`` argument for ``setup()``. See the docstring to ``astropy_helpers.utils.find_data_files`` for more details. [#42] - The ``astropy_helpers`` module now sets the global ``_ASTROPY_SETUP_`` flag upon import (from within a ``setup.py``) script, so it's not necessary to have this in the ``setup.py`` script explicitly. If in doubt though, there's no harm in setting it twice. Putting it in ``astropy_helpers`` just ensures that any other imports that occur during build will have this flag set. [#191] - It is now possible to use Cython as a ``setup_requires`` build requirement, and still build Cython extensions even if Cython wasn't available at the beginning of the build processes (that is, is automatically downloaded via setuptools' processing of ``setup_requires``). [#185] - Moves the ``adjust_compiler`` check into the ``build_ext`` command itself, so it's only used when actually building extension modules. This also deprecates the stand-alone ``adjust_compiler`` function. [#76] - When running the ``build_sphinx`` / ``build_docs`` command with the ``-w`` option, the output from Sphinx is streamed as it runs instead of silently buffering until the doc build is complete. [#197] 1.0.7 (unreleased) ------------------ - Fix missing import in ``astropy_helpers/utils.py``. [#196] 1.0.6 (2015-12-04) ------------------ - Fixed bug where running ``./setup.py build_sphinx`` could return successfully even when the build was not successful (and should have returned a non-zero error code). [#199] 1.0.5 (2015-10-02) ------------------ - Fixed a regression in the ``./setup.py test`` command that was introduced in v1.0.4. 1.0.4 (2015-10-02) ------------------ - Fixed issue with the sphinx documentation css where the line numbers for code blocks were not aligned with the code. [#179, #180] - Fixed crash that could occur when trying to build Cython extension modules when Cython isn't installed. Normally this still results in a failed build, but was supposed to provide a useful error message rather than crash outright (this was a regression introduced in v1.0.3). [#181] - Fixed a crash that could occur on Python 3 when a working C compiler isn't found. [#182] - Quieted warnings about deprecated Numpy API in Cython extensions, when building Cython extensions against Numpy >= 1.7. [#183, #186] - Improved support for py.test >= 2.7--running the ``./setup.py test`` command now copies all doc pages into the temporary test directory as well, so that all test files have a "common root directory". [#189, #190] 1.0.3 (2015-07-22) ------------------ - Added workaround for sphinx-doc/sphinx#1843, a but in Sphinx which prevented descriptor classes with a custom metaclass from being documented correctly. [#158] - Added an alias for the ``./setup.py build_sphinx`` command as ``./setup.py build_docs`` which, to a new contributor, should hopefully be less cryptic. [#161] - The fonts in graphviz diagrams now match the font of the HTML content. [#169] - When the documentation is built on readthedocs.org, MathJax will be used for math rendering. When built elsewhere, the "pngmath" extension is still used for math rendering. [#170] - Fix crash when importing astropy_helpers when running with ``python -OO`` [#171] - The ``build`` and ``build_ext`` stages now correctly recognize the presence of C++ files in Cython extensions (previously only vanilla C worked). [#173] 1.0.2 (2015-04-02) ------------------ - Various fixes enabling the astropy-helpers Sphinx build command and Sphinx extensions to work with Sphinx 1.3. [#148] - More improvement to the ability to handle multiple versions of astropy-helpers being imported in the same Python interpreter session in the (somewhat rare) case of nested installs. [#147] - To better support high resolution displays, use SVG for the astropy logo and linkout image, falling back to PNGs for browsers that support it. [#150, #151] - Improve ``setup_helpers.get_compiler_version`` to work with more compilers, and to return more info. This will help fix builds of Astropy on less common compilers, like Sun C. [#153] 1.0.1 (2015-03-04) ------------------ - Released in concert with v0.4.8 to address the same issues. 0.4.8 (2015-03-04) ------------------ - Improved the ``ah_bootstrap`` script's ability to override existing installations of astropy-helpers with new versions in the context of installing multiple packages simultaneously within the same Python interpreter (e.g. when one package has in its ``setup_requires`` another package that uses a different version of astropy-helpers. [#144] - Added a workaround to an issue in matplotlib that can, in rare cases, lead to a crash when installing packages that import matplotlib at build time. [#144] 1.0 (2015-02-17) ---------------- - Added new pre-/post-command hook points for ``setup.py`` commands. Now any package can define code to run before and/or after any ``setup.py`` command without having to manually subclass that command by adding ``pre__hook`` and ``post__hook`` callables to the package's ``setup_package.py`` module. See the PR for more details. [#112] - The following objects in the ``astropy_helpers.setup_helpers`` module have been relocated: - ``get_dummy_distribution``, ``get_distutils_*``, ``get_compiler_option``, ``add_command_option``, ``is_distutils_display_option`` -> ``astropy_helpers.distutils_helpers`` - ``should_build_with_cython``, ``generate_build_ext_command`` -> ``astropy_helpers.commands.build_ext`` - ``AstropyBuildPy`` -> ``astropy_helpers.commands.build_py`` - ``AstropyBuildSphinx`` -> ``astropy_helpers.commands.build_sphinx`` - ``AstropyInstall`` -> ``astropy_helpers.commands.install`` - ``AstropyInstallLib`` -> ``astropy_helpers.commands.install_lib`` - ``AstropyRegister`` -> ``astropy_helpers.commands.register`` - ``get_pkg_version_module`` -> ``astropy_helpers.version_helpers`` - ``write_if_different``, ``import_file``, ``get_numpy_include_path`` -> ``astropy_helpers.utils`` All of these are "soft" deprecations in the sense that they are still importable from ``astropy_helpers.setup_helpers`` for now, and there is no (easy) way to produce deprecation warnings when importing these objects from ``setup_helpers`` rather than directly from the modules they are defined in. But please consider updating any imports to these objects. [#110] - Use of the ``astropy.sphinx.ext.astropyautosummary`` extension is deprecated for use with Sphinx < 1.2. Instead it should suffice to remove this extension for the ``extensions`` list in your ``conf.py`` and add the stock ``sphinx.ext.autosummary`` instead. [#131] 0.4.7 (2015-02-17) ------------------ - Fixed incorrect/missing git hash being added to the generated ``version.py`` when creating a release. [#141] 0.4.6 (2015-02-16) ------------------ - Fixed problems related to the automatically generated _compiler module not being created properly. [#139] 0.4.5 (2015-02-11) ------------------ - Fixed an issue where ah_bootstrap.py could blow up when astropy_helper's version number is 1.0. - Added a workaround for documentation of properties in the rare case where the class's metaclass has a property of the same name. [#130] - Fixed an issue on Python 3 where importing a package using astropy-helper's generated version.py module would crash when the current working directory is an empty git repository. [#114, #137] - Fixed an issue where the "revision count" appended to .dev versions by the generated version.py did not accurately reflect the revision count for the package it belongs to, and could be invalid if the current working directory is an unrelated git repository. [#107, #137] - Likewise, fixed a confusing warning message that could occur in the same circumstances as the above issue. [#121, #137] 0.4.4 (2014-12-31) ------------------ - More improvements for building the documentation using Python 3.x. [#100] - Additional minor fixes to Python 3 support. [#115] - Updates to support new test features in Astropy [#92, #106] 0.4.3 (2014-10-22) ------------------ - The generated ``version.py`` file now preserves the git hash of installed copies of the package as well as when building a source distribution. That is, the git hash of the changeset that was installed/released is preserved. [#87] - In smart resolver add resolution for class links when they exist in the intersphinx inventory, but not the mapping of the current package (e.g. when an affiliated package uses an astropy core class of which "actual" and "documented" location differs) [#88] - Fixed a bug that could occur when running ``setup.py`` for the first time in a repository that uses astropy-helpers as a submodule: ``AttributeError: 'NoneType' object has no attribute 'mkdtemp'`` [#89] - Fixed a bug where optional arguments to the ``doctest-skip`` Sphinx directive were sometimes being left in the generated documentation output. [#90] - Improved support for building the documentation using Python 3.x. [#96] - Avoid error message if .git directory is not present. [#91] 0.4.2 (2014-08-09) ------------------ - Fixed some CSS issues in generated API docs. [#69] - Fixed the warning message that could be displayed when generating a version number with some older versions of git. [#77] - Fixed automodsumm to work with new versions of Sphinx (>= 1.2.2). [#80] 0.4.1 (2014-08-08) ------------------ - Fixed git revision count on systems with git versions older than v1.7.2. [#70] - Fixed display of warning text when running a git command fails (previously the output of stderr was not being decoded properly). [#70] - The ``--offline`` flag to ``setup.py`` understood by ``ah_bootstrap.py`` now also prevents git from going online to fetch submodule updates. [#67] - The Sphinx extension for converting issue numbers to links in the changelog now supports working on arbitrary pages via a new ``conf.py`` setting: ``changelog_links_docpattern``. By default it affects the ``changelog`` and ``whatsnew`` pages in one's Sphinx docs. [#61] - Fixed crash that could result from users with missing/misconfigured locale settings. [#58] - The font used for code examples in the docs is now the system-defined ``monospace`` font, rather than ``Minaco``, which is not available on all platforms. [#50] 0.4 (2014-07-15) ---------------- - Initial release of astropy-helpers. See `APE4 `_ for details of the motivation and design of this package. - The ``astropy_helpers`` package replaces the following modules in the ``astropy`` package: - ``astropy.setup_helpers`` -> ``astropy_helpers.setup_helpers`` - ``astropy.version_helpers`` -> ``astropy_helpers.version_helpers`` - ``astropy.sphinx`` - > ``astropy_helpers.sphinx`` These modules should be considered deprecated in ``astropy``, and any new, non-critical changes to those modules will be made in ``astropy_helpers`` instead. Affiliated packages wishing to make use those modules (as in the Astropy package-template) should use the versions from ``astropy_helpers`` instead, and include the ``ah_bootstrap.py`` script in their project, for bootstrapping the ``astropy_helpers`` package in their setup.py script. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500356.0 specutils-0.7/astropy_helpers/LICENSE.rst0000644000076500000240000000272300000000000020514 0ustar00erikstaff00000000000000Copyright (c) 2014, Astropy Developers 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 binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Astropy Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/README.rst0000644000076500000240000000266300000000000020372 0ustar00erikstaff00000000000000astropy-helpers =============== .. image:: https://travis-ci.org/astropy/astropy-helpers.svg :target: https://travis-ci.org/astropy/astropy-helpers .. image:: https://ci.appveyor.com/api/projects/status/rt9161t9mhx02xp7/branch/master?svg=true :target: https://ci.appveyor.com/project/Astropy/astropy-helpers .. image:: https://codecov.io/gh/astropy/astropy-helpers/branch/master/graph/badge.svg :target: https://codecov.io/gh/astropy/astropy-helpers The **astropy-helpers** package includes many build, installation, and documentation-related tools used by the Astropy project, but packaged separately for use by other projects that wish to leverage this work. The motivation behind this package and details of its implementation are in the accepted `Astropy Proposal for Enhancement (APE) 4 `_. Astropy-helpers is not a traditional package in the sense that it is not intended to be installed directly by users or developers. Instead, it is meant to be accessed when the ``setup.py`` command is run - see the "Using astropy-helpers in a package" section in the documentation for how to do this. For a real-life example of how to implement astropy-helpers in a project, see the ``setup.py`` and ``setup.cfg`` files of the `Affiliated package template `_. For more information, see the documentation at http://astropy-helpers.readthedocs.io ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/ah_bootstrap.py0000644000076500000240000011063400000000000021740 0ustar00erikstaff00000000000000""" 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 and by default the system-installed version of astropy-helpers will be used (however, ``ah_bootstrap.use_astropy_helpers`` may be called manually from within the setup.py script). This behavior can also be controlled using the ``--auto-use`` and ``--no-auto-use`` command-line flags. For clarity, an alias for ``--no-auto-use`` is ``--use-system-astropy-helpers``, and we recommend using the latter if needed. 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 io import locale import os import re import subprocess as sp import sys from distutils import log from distutils.debug import DEBUG from configparser import ConfigParser, RawConfigParser import pkg_resources from setuptools import Distribution from setuptools.package_index import PackageIndex # This is the minimum Python version required for astropy-helpers __minimum_python_version__ = (3, 5) # TODO: Maybe enable checking for a specific version of astropy_helpers? DIST_NAME = 'astropy-helpers' PACKAGE_NAME = 'astropy_helpers' UPPER_VERSION_EXCLUSIVE = None # Defaults for other options DOWNLOAD_IF_NEEDED = True INDEX_URL = 'https://pypi.python.org/simple' USE_GIT = True OFFLINE = False AUTO_UPGRADE = True # A list of all the configuration options and their required types CFG_OPTIONS = [ ('auto_use', bool), ('path', str), ('download_if_needed', bool), ('index_url', str), ('use_git', bool), ('offline', bool), ('auto_upgrade', bool) ] # Start off by parsing the setup.cfg file SETUP_CFG = ConfigParser() if os.path.exists('setup.cfg'): try: SETUP_CFG.read('setup.cfg') except Exception as e: if DEBUG: raise log.error( "Error reading setup.cfg: {0!r}\n{1} will not be " "automatically bootstrapped and package installation may fail." "\n{2}".format(e, PACKAGE_NAME, _err_help_msg)) # We used package_name in the package template for a while instead of name if SETUP_CFG.has_option('metadata', 'name'): parent_package = SETUP_CFG.get('metadata', 'name') elif SETUP_CFG.has_option('metadata', 'package_name'): parent_package = SETUP_CFG.get('metadata', 'package_name') else: parent_package = None if SETUP_CFG.has_option('options', 'python_requires'): python_requires = SETUP_CFG.get('options', 'python_requires') # The python_requires key has a syntax that can be parsed by SpecifierSet # in the packaging package. However, we don't want to have to depend on that # package, so instead we can use setuptools (which bundles packaging). We # have to add 'python' to parse it with Requirement. from pkg_resources import Requirement req = Requirement.parse('python' + python_requires) # We want the Python version as a string, which we can get from the platform module import platform # strip off trailing '+' incase this is a dev install of python python_version = platform.python_version().strip('+') # allow pre-releases to count as 'new enough' if not req.specifier.contains(python_version, True): if parent_package is None: message = "ERROR: Python {} is required by this package\n".format(req.specifier) else: message = "ERROR: Python {} is required by {}\n".format(req.specifier, parent_package) sys.stderr.write(message) sys.exit(1) if sys.version_info < __minimum_python_version__: if parent_package is None: message = "ERROR: Python {} or later is required by astropy-helpers\n".format( __minimum_python_version__) else: message = "ERROR: Python {} or later is required by astropy-helpers for {}\n".format( __minimum_python_version__, parent_package) sys.stderr.write(message) sys.exit(1) _str_types = (str, bytes) # What follows are several import statements meant to deal with install-time # issues with either missing or misbehaving pacakges (including making sure # setuptools itself is installed): # Check that setuptools 30.3 or later is present from distutils.version import LooseVersion try: import setuptools assert LooseVersion(setuptools.__version__) >= LooseVersion('30.3') except (ImportError, AssertionError): sys.stderr.write("ERROR: setuptools 30.3 or later is required by astropy-helpers\n") sys.exit(1) # typing as a dependency for 1.6.1+ Sphinx causes issues when imported after # initializing submodule with ah_boostrap.py # See discussion and references in # https://github.com/astropy/astropy-helpers/issues/302 try: import typing # noqa except ImportError: pass # 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 # noqa except ImportError: pass # matplotlib can cause problems if it is imported from within a call of # run_setup(), because in some circumstances it will try to write to the user's # home directory, resulting in a SandboxViolation. See # https://github.com/matplotlib/matplotlib/pull/4165 # Making sure matplotlib, if it is available, is imported early in the setup # process can mitigate this (note importing matplotlib.pyplot has the same # issue) try: import matplotlib matplotlib.use('Agg') import matplotlib.pyplot except: # Ignore if this fails for *any* reason* pass # End compatibility imports... class _Bootstrapper(object): """ Bootstrapper implementation. See ``use_astropy_helpers`` for parameter documentation. """ def __init__(self, path=None, index_url=None, use_git=None, offline=None, download_if_needed=None, auto_upgrade=None): if path is None: path = PACKAGE_NAME if not (isinstance(path, _str_types) or path is False): raise TypeError('path must be a string or False') if not isinstance(path, str): fs_encoding = sys.getfilesystemencoding() path = path.decode(fs_encoding) # path to unicode self.path = path # Set other option attributes, using defaults where necessary self.index_url = index_url if index_url is not None else INDEX_URL self.offline = offline if offline is not None else OFFLINE # If offline=True, override download and auto-upgrade if self.offline: download_if_needed = False auto_upgrade = False self.download = (download_if_needed if download_if_needed is not None else DOWNLOAD_IF_NEEDED) self.auto_upgrade = (auto_upgrade if auto_upgrade is not None else AUTO_UPGRADE) # 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 self.use_git = use_git if use_git is not None else USE_GIT # 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) self.is_submodule = False @classmethod def main(cls, argv=None): if argv is None: argv = sys.argv config = cls.parse_config() config.update(cls.parse_command_line(argv)) auto_use = config.pop('auto_use', False) bootstrapper = cls(**config) if auto_use: # Run the bootstrapper, otherwise the setup.py is using the old # use_astropy_helpers() interface, in which case it will run the # bootstrapper manually after reconfiguring it. bootstrapper.run() return bootstrapper @classmethod def parse_config(cls): if not SETUP_CFG.has_section('ah_bootstrap'): return {} config = {} for option, type_ in CFG_OPTIONS: if not SETUP_CFG.has_option('ah_bootstrap', option): continue if type_ is bool: value = SETUP_CFG.getboolean('ah_bootstrap', option) else: value = SETUP_CFG.get('ah_bootstrap', option) config[option] = value return config @classmethod def parse_command_line(cls, argv=None): if argv is None: argv = sys.argv config = {} # For now we just pop recognized ah_bootstrap options out of the # arg list. This is imperfect; in the unlikely case that a setup.py # custom command or even custom Distribution class defines an argument # of the same name then we will break that. However there's a catch22 # here that we can't just do full argument parsing right here, because # we don't yet know *how* to parse all possible command-line arguments. if '--no-git' in argv: config['use_git'] = False argv.remove('--no-git') if '--offline' in argv: config['offline'] = True argv.remove('--offline') if '--auto-use' in argv: config['auto_use'] = True argv.remove('--auto-use') if '--no-auto-use' in argv: config['auto_use'] = False argv.remove('--no-auto-use') if '--use-system-astropy-helpers' in argv: config['auto_use'] = False argv.remove('--use-system-astropy-helpers') return config def run(self): strategies = ['local_directory', 'local_file', 'index'] dist = None # First, remove any previously imported versions of astropy_helpers; # this is necessary for nested installs where one package's installer # is installing another package via setuptools.sandbox.run_setup, as in # the case of setup_requires for key in list(sys.modules): try: if key == PACKAGE_NAME or key.startswith(PACKAGE_NAME + '.'): del sys.modules[key] except AttributeError: # Sometimes mysterious non-string things can turn up in # sys.modules continue # Check to see if the path is a submodule self.is_submodule = self._check_submodule() for strategy in strategies: method = getattr(self, 'get_{0}_dist'.format(strategy)) dist = method() if dist is not None: break else: raise _AHBootstrapSystemExit( "No source found for the {0!r} package; {0} must be " "available and importable as a prerequisite to building " "or installing this package.".format(PACKAGE_NAME)) # This is a bit hacky, but if astropy_helpers was loaded from a # directory/submodule its Distribution object gets a "precedence" of # "DEVELOP_DIST". However, in other cases it gets a precedence of # "EGG_DIST". However, when activing the distribution it will only be # placed early on sys.path if it is treated as an EGG_DIST, so always # do that dist = dist.clone(precedence=pkg_resources.EGG_DIST) # Otherwise we found a version of astropy-helpers, so we're done # Just active the found distribution on sys.path--if we did a # download this usually happens automatically but it doesn't hurt to # do it again # Note: Adding the dist to the global working set also activates it # (makes it importable on sys.path) by default. try: pkg_resources.working_set.add(dist, replace=True) except TypeError: # Some (much) older versions of setuptools do not have the # replace=True option here. These versions are old enough that all # bets may be off anyways, but it's easy enough to work around just # in case... if dist.key in pkg_resources.working_set.by_key: del pkg_resources.working_set.by_key[dist.key] pkg_resources.working_set.add(dist) @property def config(self): """ A `dict` containing the options this `_Bootstrapper` was configured with. """ return dict((optname, getattr(self, optname)) for optname, _ in CFG_OPTIONS if hasattr(self, optname)) def get_local_directory_dist(self): """ Handle importing a vendored package from a subdirectory of the source distribution. """ if not os.path.isdir(self.path): return log.info('Attempting to import astropy_helpers from {0} {1!r}'.format( 'submodule' if self.is_submodule else 'directory', self.path)) dist = self._directory_import() if dist is None: log.warn( 'The requested path {0!r} for importing {1} does not ' 'exist, or does not contain a copy of the {1} ' 'package.'.format(self.path, PACKAGE_NAME)) elif self.auto_upgrade and not self.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 = self._do_upgrade(dist) if upgrade is not None: dist = upgrade return dist def get_local_file_dist(self): """ Handle importing from a source archive; this also uses setup_requires but points easy_install directly to the source archive. """ if not os.path.isfile(self.path): return log.info('Attempting to unpack and import astropy_helpers from ' '{0!r}'.format(self.path)) try: dist = self._do_download(find_links=[self.path]) except Exception as e: if DEBUG: raise log.warn( 'Failed to import {0} from the specified archive {1!r}: ' '{2}'.format(PACKAGE_NAME, self.path, str(e))) dist = None if dist is not None and self.auto_upgrade: # A version of astropy-helpers was found on the available path, but # check to see if a bugfix release is available on PyPI upgrade = self._do_upgrade(dist) if upgrade is not None: dist = upgrade return dist def get_index_dist(self): if not self.download: log.warn('Downloading {0!r} disabled.'.format(DIST_NAME)) return None log.warn( "Downloading {0!r}; run setup.py with the --offline option to " "force offline installation.".format(DIST_NAME)) try: dist = self._do_download() except Exception as e: if DEBUG: raise log.warn( 'Failed to download and/or install {0!r} from {1!r}:\n' '{2}'.format(DIST_NAME, self.index_url, str(e))) dist = None # No need to run auto-upgrade here since we've already presumably # gotten the most up-to-date version from the package index return dist def _directory_import(self): """ 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(self.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): # We use subprocess instead of run_setup from setuptools to # avoid segmentation faults - see the following for more details: # https://github.com/cython/cython/issues/2104 sp.check_output([sys.executable, 'setup.py', 'egg_info'], cwd=path) for dist in pkg_resources.find_distributions(path, True): # There should be only one... return dist return dist def _do_download(self, version='', find_links=None): if find_links: allow_hosts = '' index_url = None else: allow_hosts = None index_url = self.index_url # 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: if UPPER_VERSION_EXCLUSIVE is None: req = DIST_NAME else: req = '{0}<{1}'.format(DIST_NAME, UPPER_VERSION_EXCLUSIVE) attrs = {'setup_requires': [req]} # NOTE: we need to parse the config file (e.g. setup.cfg) to make sure # it honours the options set in the [easy_install] section, and we need # to explicitly fetch the requirement eggs as setup_requires does not # get honored in recent versions of setuptools: # https://github.com/pypa/setuptools/issues/1273 try: context = _verbose if DEBUG else _silence with context(): dist = _Distribution(attrs=attrs) try: dist.parse_config_files(ignore_option_errors=True) dist.fetch_build_eggs(req) except TypeError: # On older versions of setuptools, ignore_option_errors # doesn't exist, and the above two lines are not needed # so we can just continue pass # 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 {0} from {1}:\n{2}' if find_links: source = find_links[0] elif index_url != INDEX_URL: source = index_url else: source = 'PyPI' raise Exception(msg.format(DIST_NAME, source, repr(e))) def _do_upgrade(self, dist): # Build up a requirement for a higher bugfix release but a lower minor # release (so API compatibility is guaranteed) 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=self.index_url) upgrade = package_index.obtain(req) if upgrade is not None: return self._do_download(version=upgrade.version) def _check_submodule(self): """ 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 (self.path is None or (os.path.exists(self.path) and not os.path.isdir(self.path))): return False if self.use_git: return self._check_submodule_using_git() else: return self._check_submodule_no_git() def _check_submodule_using_git(self): """ 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. """ cmd = ['git', 'submodule', 'status', '--', self.path] try: log.info('Running `{0}`; use the --no-git option to disable git ' 'commands'.format(' '.join(cmd))) returncode, stdout, stderr = run_cmd(cmd) except _CommandNotFound: # 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 stderr = stderr.strip() if returncode != 0 and 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 # 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 unknown error condition occurred log.warn('git submodule command failed ' 'unexpectedly:\n{0}'.format(stderr)) return False # 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( r'^(?P[+-U ])(?P[0-9a-f]{40}) ' r'(?P\S+)( .*)?$') # 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 self._update_submodule(m.group('submodule'), m.group('status')) return True else: log.warn( 'Unexpected output from `git submodule status`:\n{0}\n' 'Will attempt import from {1!r} regardless.'.format( stdout, self.path)) return False def _check_submodule_no_git(self): """ 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, self.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 == self.path.rstrip(os.sep): return True return False def _update_submodule(self, submodule, status): if status == ' ': # The submodule is up to date; no action necessary return elif status == '-': if self.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 self.offline: cmd.append('--no-fetch') elif status == 'U': raise _AHBootstrapSystemExit( '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: log.info('Running `{0}`; use the --no-git option to disable git ' 'commands'.format(' '.join(cmd))) returncode, stdout, stderr = run_cmd(cmd) except OSError as e: err_msg = str(e) else: if returncode != 0: err_msg = stderr if err_msg is not None: log.warn('An unexpected error occurred updating the git submodule ' '{0!r}:\n{1}\n{2}'.format(submodule, err_msg, _err_help_msg)) class _CommandNotFound(OSError): """ An exception raised when a command run with run_cmd is not found on the system. """ def run_cmd(cmd): """ Run a command in a subprocess, given as a list of command-line arguments. Returns a ``(returncode, stdout, stderr)`` tuple. """ try: p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) # XXX: May block if either stdout or stderr fill their buffers; # however for the commands this is currently used for that is # unlikely (they should have very brief output) stdout, stderr = p.communicate() except OSError as e: if DEBUG: raise if e.errno == errno.ENOENT: msg = 'Command not found: `{0}`'.format(' '.join(cmd)) raise _CommandNotFound(msg, cmd) else: raise _AHBootstrapSystemExit( 'An unexpected error occurred when running the ' '`{0}` command:\n{1}'.format(' '.join(cmd), 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' # Unlikely to fail at this point but even then let's be flexible if not isinstance(stdout, str): stdout = stdout.decode(stdio_encoding, 'replace') if not isinstance(stderr, str): stderr = stderr.decode(stdio_encoding, 'replace') return (p.returncode, stdout, stderr) 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 _verbose(): yield @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:]) BOOTSTRAPPER = _Bootstrapper.main() def use_astropy_helpers(**kwargs): """ 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 initialized 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`. offline : bool, optional If `False` disable all actions that require an internet connection, including downloading packages from the package index and fetching updates to any git submodule. Defaults to `True`. """ global BOOTSTRAPPER config = BOOTSTRAPPER.config config.update(**kwargs) # Create a new bootstrapper with the updated configuration and run it BOOTSTRAPPER = _Bootstrapper(**config) BOOTSTRAPPER.run() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/astropy_helpers/astropy_helpers/0000755000076500000240000000000000000000000022117 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500356.0 specutils-0.7/astropy_helpers/astropy_helpers/__init__.py0000644000076500000240000000331200000000000024227 0ustar00erikstaff00000000000000try: from .version import version as __version__ from .version import githash as __githash__ except ImportError: __version__ = '' __githash__ = '' # If we've made it as far as importing astropy_helpers, we don't need # ah_bootstrap in sys.modules anymore. Getting rid of it is actually necessary # if the package we're installing has a setup_requires of another package that # uses astropy_helpers (and possibly a different version at that) # See https://github.com/astropy/astropy/issues/3541 import sys if 'ah_bootstrap' in sys.modules: del sys.modules['ah_bootstrap'] # Note, this is repeated from ah_bootstrap.py, but is here too in case this # astropy-helpers was upgraded to from an older version that did not have this # check in its ah_bootstrap. # matplotlib can cause problems if it is imported from within a call of # run_setup(), because in some circumstances it will try to write to the user's # home directory, resulting in a SandboxViolation. See # https://github.com/matplotlib/matplotlib/pull/4165 # Making sure matplotlib, if it is available, is imported early in the setup # process can mitigate this (note importing matplotlib.pyplot has the same # issue) try: import matplotlib matplotlib.use('Agg') import matplotlib.pyplot except: # Ignore if this fails for *any* reason* pass import os # Ensure that all module-level code in astropy or other packages know that # we're in setup mode: if ('__main__' in sys.modules and hasattr(sys.modules['__main__'], '__file__')): filename = os.path.basename(sys.modules['__main__'].__file__) if filename.rstrip('co') == 'setup.py': import builtins builtins._ASTROPY_SETUP_ = True del filename ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/astropy_helpers/astropy_helpers/commands/0000755000076500000240000000000000000000000023720 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500356.0 specutils-0.7/astropy_helpers/astropy_helpers/commands/__init__.py0000644000076500000240000000000000000000000026017 0ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500356.0 specutils-0.7/astropy_helpers/astropy_helpers/commands/_dummy.py0000644000076500000240000000526200000000000025571 0ustar00erikstaff00000000000000""" Provides a base class for a 'dummy' setup.py command that has no functionality (probably due to a missing requirement). This dummy command can raise an exception when it is run, explaining to the user what dependencies must be met to use this command. The reason this is at all tricky is that we want the command to be able to provide this message even when the user passes arguments to the command. If we don't know ahead of time what arguments the command can take, this is difficult, because distutils does not allow unknown arguments to be passed to a setup.py command. This hacks around that restriction to provide a useful error message even when a user passes arguments to the dummy implementation of a command. Use this like: try: from some_dependency import SetupCommand except ImportError: from ._dummy import _DummyCommand class SetupCommand(_DummyCommand): description = \ 'Implementation of SetupCommand from some_dependency; ' 'some_dependency must be installed to run this command' # This is the message that will be raised when a user tries to # run this command--define it as a class attribute. error_msg = \ "The 'setup_command' command requires the some_dependency " "package to be installed and importable." """ import sys from setuptools import Command from distutils.errors import DistutilsArgError from textwrap import dedent class _DummyCommandMeta(type): """ Causes an exception to be raised on accessing attributes of a command class so that if ``./setup.py command_name`` is run with additional command-line options we can provide a useful error message instead of the default that tells users the options are unrecognized. """ def __init__(cls, name, bases, members): if bases == (Command, object): # This is the _DummyCommand base class, presumably return if not hasattr(cls, 'description'): raise TypeError( "_DummyCommand subclass must have a 'description' " "attribute.") if not hasattr(cls, 'error_msg'): raise TypeError( "_DummyCommand subclass must have an 'error_msg' " "attribute.") def __getattribute__(cls, attr): if attr in ('description', 'error_msg'): # Allow cls.description to work so that `./setup.py # --help-commands` still works return super(_DummyCommandMeta, cls).__getattribute__(attr) raise DistutilsArgError(cls.error_msg) class _DummyCommand(Command, object, metaclass=_DummyCommandMeta): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/commands/build_ext.py0000644000076500000240000002074200000000000026256 0ustar00erikstaff00000000000000import errno import os import shutil from distutils.core import Extension from distutils.ccompiler import get_default_compiler from distutils.command.build_ext import build_ext as DistutilsBuildExt from ..distutils_helpers import get_main_package_directory from ..utils import get_numpy_include_path, import_file __all__ = ['AstropyHelpersBuildExt'] def should_build_with_cython(previous_cython_version, is_release): """ Returns the previously used Cython version (or 'unknown' if not previously built) if Cython should be used to build extension modules from pyx files. """ # Only build with Cython if, of course, Cython is installed, we're in a # development version (i.e. not release) or the Cython-generated source # files haven't been created yet (cython_version == 'unknown'). The latter # case can happen even when release is True if checking out a release tag # from the repository have_cython = False try: from Cython import __version__ as cython_version # noqa have_cython = True except ImportError: pass if have_cython and (not is_release or previous_cython_version == 'unknown'): return cython_version else: return False class AstropyHelpersBuildExt(DistutilsBuildExt): """ A custom 'build_ext' command that allows for manipulating some of the C extension options at build time. """ _uses_cython = False _force_rebuild = False def __new__(cls, value, **kwargs): # NOTE: we need to wait until AstropyHelpersBuildExt is initialized to # import setuptools.command.build_ext because when that package is # imported, setuptools tries to import Cython - and if it's not found # it will affect the rest of the build process. This is an issue because # if we import that module at the top of this one, setup_requires won't # have been honored yet, so Cython may not yet be available - and if we # import build_ext too soon, it will think Cython is not available even # if it is then intalled when setup_requires is processed. To get around # this we dynamically create a new class that inherits from the # setuptools build_ext, and by this point setup_requires has been # processed. from setuptools.command.build_ext import build_ext as SetuptoolsBuildExt class FinalBuildExt(AstropyHelpersBuildExt, SetuptoolsBuildExt): pass new_type = type(cls.__name__, (FinalBuildExt,), dict(cls.__dict__)) obj = SetuptoolsBuildExt.__new__(new_type) obj.__init__(value) return obj def finalize_options(self): # First let's find the package folder, then we can check if the # version and cython_version are accessible self.package_dir = get_main_package_directory(self.distribution) version = import_file(os.path.join(self.package_dir, 'version.py'), name='version').version self.is_release = 'dev' not in version try: self.previous_cython_version = import_file(os.path.join(self.package_dir, 'cython_version.py'), name='cython_version').cython_version except (FileNotFoundError, ImportError): self.previous_cython_version = 'unknown' self._uses_cython = should_build_with_cython(self.previous_cython_version, self.is_release) # Add a copy of the _compiler.so module as well, but only if there # are in fact C modules to compile (otherwise there's no reason to # include a record of the compiler used). Note that self.extensions # may not be set yet, but self.distribution.ext_modules is where any # extension modules passed to setup() can be found extensions = self.distribution.ext_modules if extensions: build_py = self.get_finalized_command('build_py') package_dir = build_py.get_package_dir(self.package_dir) src_path = os.path.relpath( os.path.join(os.path.dirname(__file__), 'src')) shutil.copy(os.path.join(src_path, 'compiler.c'), os.path.join(package_dir, '_compiler.c')) ext = Extension(self.package_dir + '.compiler_version', [os.path.join(package_dir, '_compiler.c')]) extensions.insert(0, ext) super().finalize_options() # If we are using Cython, then make sure we re-build if the version # of Cython that is installed is different from the version last # used to generate the C files. if self._uses_cython and self._uses_cython != self.previous_cython_version: self._force_rebuild = True # Regardless of the value of the '--force' option, force a rebuild # if the debug flag changed from the last build if self._force_rebuild: self.force = True def run(self): # For extensions that require 'numpy' in their include dirs, # replace 'numpy' with the actual paths np_include = None for extension in self.extensions: if 'numpy' in extension.include_dirs: if np_include is None: np_include = get_numpy_include_path() idx = extension.include_dirs.index('numpy') extension.include_dirs.insert(idx, np_include) extension.include_dirs.remove('numpy') self._check_cython_sources(extension) # Note that setuptools automatically uses Cython to discover and # build extensions if available, so we don't have to explicitly call # e.g. cythonize. super().run() # Update cython_version.py if building with Cython if self._uses_cython and self._uses_cython != self.previous_cython_version: build_py = self.get_finalized_command('build_py') package_dir = build_py.get_package_dir(self.package_dir) cython_py = os.path.join(package_dir, 'cython_version.py') with open(cython_py, 'w') as f: f.write('# Generated file; do not modify\n') f.write('cython_version = {0!r}\n'.format(self._uses_cython)) if os.path.isdir(self.build_lib): # The build/lib directory may not exist if the build_py # command was not previously run, which may sometimes be # the case self.copy_file(cython_py, os.path.join(self.build_lib, cython_py), preserve_mode=False) def _check_cython_sources(self, extension): """ Where relevant, make sure that the .c files associated with .pyx modules are present (if building without Cython installed). """ # Determine the compiler we'll be using if self.compiler is None: compiler = get_default_compiler() else: compiler = self.compiler # Replace .pyx with C-equivalents, unless c files are missing for jdx, src in enumerate(extension.sources): base, ext = os.path.splitext(src) pyxfn = base + '.pyx' cfn = base + '.c' cppfn = base + '.cpp' if not os.path.isfile(pyxfn): continue if self._uses_cython: extension.sources[jdx] = pyxfn else: if os.path.isfile(cfn): extension.sources[jdx] = cfn elif os.path.isfile(cppfn): extension.sources[jdx] = cppfn else: msg = ( 'Could not find C/C++ file {0}.(c/cpp) for Cython ' 'file {1} when building extension {2}. Cython ' 'must be installed to build from a git ' 'checkout.'.format(base, pyxfn, extension.name)) raise IOError(errno.ENOENT, msg, cfn) # Cython (at least as of 0.29.2) uses deprecated Numpy API features # the use of which produces a few warnings when compiling. # These additional flags should squelch those warnings. # TODO: Feel free to remove this if/when a Cython update # removes use of the deprecated Numpy API if compiler == 'unix': extension.extra_compile_args.extend([ '-Wp,-w', '-Wno-unused-function']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/commands/build_sphinx.py0000644000076500000240000002177700000000000027000 0ustar00erikstaff00000000000000from __future__ import print_function import os import pkgutil import re import shutil import subprocess import sys from distutils.version import LooseVersion from distutils import log from sphinx import __version__ as sphinx_version from sphinx.setup_command import BuildDoc as SphinxBuildDoc SPHINX_LT_16 = LooseVersion(sphinx_version) < LooseVersion('1.6') SPHINX_LT_17 = LooseVersion(sphinx_version) < LooseVersion('1.7') SUBPROCESS_TEMPLATE = """ import os import sys {build_main} os.chdir({srcdir!r}) {sys_path_inserts} for builder in {builders!r}: retcode = build_main(argv={argv!r} + ['-b', builder, '.', os.path.join({output_dir!r}, builder)]) if retcode != 0: sys.exit(retcode) """ def ensure_sphinx_astropy_installed(): """ Make sure that sphinx-astropy is available. This returns the available version of sphinx-astropy as well as any paths that should be added to sys.path for sphinx-astropy to be available. """ # We've split out the Sphinx part of astropy-helpers into sphinx-astropy # but we want it to be auto-installed seamlessly for anyone using # build_docs. We check if it's already installed, and if not, we install # it to a local .eggs directory and add the eggs to the path (these # have to each be added to the path, we can't add them by simply adding # .eggs to the path) sys_path_inserts = [] sphinx_astropy_version = None try: from sphinx_astropy import __version__ as sphinx_astropy_version # noqa except ImportError: raise ImportError("sphinx-astropy needs to be installed to build" "the documentation.") return sphinx_astropy_version, sys_path_inserts class AstropyBuildDocs(SphinxBuildDoc): """ A version of the ``build_docs`` command that uses the version of Astropy that is built by the setup ``build`` command, rather than whatever is installed on the system. To build docs against the installed version, run ``make html`` in the ``astropy/docs`` directory. """ description = 'Build Sphinx documentation for Astropy environment' user_options = SphinxBuildDoc.user_options[:] user_options.append( ('warnings-returncode', 'w', 'Parses the sphinx output and sets the return code to 1 if there ' 'are any warnings. Note that this will cause the sphinx log to ' 'only update when it completes, rather than continuously as is ' 'normally the case.')) user_options.append( ('clean-docs', 'l', 'Completely clean previous builds, including ' 'automodapi-generated files before building new ones')) user_options.append( ('no-intersphinx', 'n', 'Skip intersphinx, even if conf.py says to use it')) user_options.append( ('open-docs-in-browser', 'o', 'Open the docs in a browser (using the webbrowser module) if the ' 'build finishes successfully.')) boolean_options = SphinxBuildDoc.boolean_options[:] boolean_options.append('warnings-returncode') boolean_options.append('clean-docs') boolean_options.append('no-intersphinx') boolean_options.append('open-docs-in-browser') _self_iden_rex = re.compile(r"self\.([^\d\W][\w]+)", re.UNICODE) def initialize_options(self): SphinxBuildDoc.initialize_options(self) self.clean_docs = False self.no_intersphinx = False self.open_docs_in_browser = False self.warnings_returncode = False self.traceback = False def finalize_options(self): # This has to happen before we call the parent class's finalize_options if self.build_dir is None: self.build_dir = 'docs/_build' SphinxBuildDoc.finalize_options(self) # Clear out previous sphinx builds, if requested if self.clean_docs: dirstorm = [os.path.join(self.source_dir, 'api'), os.path.join(self.source_dir, 'generated')] dirstorm.append(self.build_dir) for d in dirstorm: if os.path.isdir(d): log.info('Cleaning directory ' + d) shutil.rmtree(d) else: log.info('Not cleaning directory ' + d + ' because ' 'not present or not a directory') def run(self): # TODO: Break this method up into a few more subroutines and # document them better import webbrowser from urllib.request import pathname2url # This is used at the very end of `run` to decide if sys.exit should # be called. If it's None, it won't be. retcode = None # Now make sure Astropy is built and determine where it was built build_cmd = self.reinitialize_command('build') build_cmd.inplace = 0 self.run_command('build') build_cmd = self.get_finalized_command('build') build_cmd_path = os.path.abspath(build_cmd.build_lib) ah_importer = pkgutil.get_importer('astropy_helpers') if ah_importer is None: ah_path = '.' else: ah_path = os.path.abspath(ah_importer.path) if SPHINX_LT_17: build_main = 'from sphinx import build_main' else: build_main = 'from sphinx.cmd.build import build_main' # We need to make sure sphinx-astropy is installed sphinx_astropy_version, extra_paths = ensure_sphinx_astropy_installed() sys_path_inserts = [build_cmd_path, ah_path] + extra_paths sys_path_inserts = os.linesep.join(['sys.path.insert(0, {0!r})'.format(path) for path in sys_path_inserts]) argv = [] if self.warnings_returncode: argv.append('-W') if self.no_intersphinx: # Note, if sphinx_astropy_version is None, this could indicate an # old version of setuptools, but sphinx-astropy is likely ok, so # we can proceed. if sphinx_astropy_version is None or LooseVersion(sphinx_astropy_version) >= LooseVersion('1.1'): argv.extend(['-D', 'disable_intersphinx=1']) else: log.warn('The -n option to disable intersphinx requires ' 'sphinx-astropy>=1.1. Ignoring.') # We now need to adjust the flags based on the parent class's options if self.fresh_env: argv.append('-E') if self.all_files: argv.append('-a') if getattr(self, 'pdb', False): argv.append('-P') if getattr(self, 'nitpicky', False): argv.append('-n') if self.traceback: argv.append('-T') # The default verbosity level is 1, so in that case we just don't add a flag if self.verbose == 0: argv.append('-q') elif self.verbose > 1: argv.append('-v') if SPHINX_LT_17: argv.insert(0, 'sphinx-build') if isinstance(self.builder, str): builders = [self.builder] else: builders = self.builder subproccode = SUBPROCESS_TEMPLATE.format(build_main=build_main, srcdir=self.source_dir, sys_path_inserts=sys_path_inserts, builders=builders, argv=argv, output_dir=os.path.abspath(self.build_dir)) log.debug('Starting subprocess of {0} with python code:\n{1}\n' '[CODE END])'.format(sys.executable, subproccode)) proc = subprocess.Popen([sys.executable], stdin=subprocess.PIPE) proc.communicate(subproccode.encode('utf-8')) if proc.returncode != 0: retcode = proc.returncode if retcode is None: if self.open_docs_in_browser: if self.builder == 'html': absdir = os.path.abspath(self.builder_target_dir) index_path = os.path.join(absdir, 'index.html') fileurl = 'file://' + pathname2url(index_path) webbrowser.open(fileurl) else: log.warn('open-docs-in-browser option was given, but ' 'the builder is not html! Ignoring.') # Here we explicitly check proc.returncode since we only want to output # this for cases where the return code really wasn't 0. if proc.returncode: log.warn('Sphinx Documentation subprocess failed with return ' 'code ' + str(proc.returncode)) if retcode is not None: # this is potentially dangerous in that there might be something # after the call to `setup` in `setup.py`, and exiting here will # prevent that from running. But there's no other apparent way # to signal what the return code should be. sys.exit(retcode) class AstropyBuildSphinx(AstropyBuildDocs): # pragma: no cover def run(self): AstropyBuildDocs.run(self) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/astropy_helpers/astropy_helpers/commands/src/0000755000076500000240000000000000000000000024507 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/commands/src/compiler.c0000644000076500000240000000524000000000000026466 0ustar00erikstaff00000000000000#include /*************************************************************************** * Macros for determining the compiler version. * * These are borrowed from boost, and majorly abridged to include only * the compilers we care about. ***************************************************************************/ #define STRINGIZE(X) DO_STRINGIZE(X) #define DO_STRINGIZE(X) #X #if defined __clang__ /* Clang C++ emulates GCC, so it has to appear early. */ # define COMPILER "Clang version " __clang_version__ #elif defined(__INTEL_COMPILER) || defined(__ICL) || defined(__ICC) || defined(__ECC) /* Intel */ # if defined(__INTEL_COMPILER) # define INTEL_VERSION __INTEL_COMPILER # elif defined(__ICL) # define INTEL_VERSION __ICL # elif defined(__ICC) # define INTEL_VERSION __ICC # elif defined(__ECC) # define INTEL_VERSION __ECC # endif # define COMPILER "Intel C compiler version " STRINGIZE(INTEL_VERSION) #elif defined(__GNUC__) /* gcc */ # define COMPILER "GCC version " __VERSION__ #elif defined(__SUNPRO_CC) /* Sun Workshop Compiler */ # define COMPILER "Sun compiler version " STRINGIZE(__SUNPRO_CC) #elif defined(_MSC_VER) /* Microsoft Visual C/C++ Must be last since other compilers define _MSC_VER for compatibility as well */ # if _MSC_VER < 1200 # define COMPILER_VERSION 5.0 # elif _MSC_VER < 1300 # define COMPILER_VERSION 6.0 # elif _MSC_VER == 1300 # define COMPILER_VERSION 7.0 # elif _MSC_VER == 1310 # define COMPILER_VERSION 7.1 # elif _MSC_VER == 1400 # define COMPILER_VERSION 8.0 # elif _MSC_VER == 1500 # define COMPILER_VERSION 9.0 # elif _MSC_VER == 1600 # define COMPILER_VERSION 10.0 # else # define COMPILER_VERSION _MSC_VER # endif # define COMPILER "Microsoft Visual C++ version " STRINGIZE(COMPILER_VERSION) #else /* Fallback */ # define COMPILER "Unknown compiler" #endif /*************************************************************************** * Module-level ***************************************************************************/ struct module_state { /* The Sun compiler can't handle empty structs */ #if defined(__SUNPRO_C) || defined(_MSC_VER) int _dummy; #endif }; static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "compiler_version", NULL, sizeof(struct module_state), NULL, NULL, NULL, NULL, NULL }; #define INITERROR return NULL PyMODINIT_FUNC PyInit_compiler_version(void) { PyObject* m; m = PyModule_Create(&moduledef); if (m == NULL) INITERROR; PyModule_AddStringConstant(m, "compiler", COMPILER); return m; } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/commands/test.py0000644000076500000240000000267200000000000025260 0ustar00erikstaff00000000000000""" Different implementations of the ``./setup.py test`` command depending on what's locally available. If Astropy v1.1 or later is available it should be possible to import AstropyTest from ``astropy.tests.command``. Otherwise there is a skeleton implementation that allows users to at least discover the ``./setup.py test`` command and learn that they need Astropy to run it. """ import os from ..utils import import_file # Previously these except statements caught only ImportErrors, but there are # some other obscure exceptional conditions that can occur when importing # astropy.tests (at least on older versions) that can cause these imports to # fail try: # If we are testing astropy itself, we need to use import_file to avoid # actually importing astropy (just the file we need). command_file = os.path.join('astropy', 'tests', 'command.py') if os.path.exists(command_file): AstropyTest = import_file(command_file, 'astropy_tests_command').AstropyTest else: import astropy # noqa from astropy.tests.command import AstropyTest except Exception: # No astropy at all--provide the dummy implementation from ._dummy import _DummyCommand class AstropyTest(_DummyCommand): command_name = 'test' description = 'Run the tests for this package' error_msg = ( "The 'test' command requires the astropy package to be " "installed and importable.") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500356.0 specutils-0.7/astropy_helpers/astropy_helpers/conftest.py0000644000076500000240000000361200000000000024320 0ustar00erikstaff00000000000000# This file contains settings for pytest that are specific to astropy-helpers. # Since we run many of the tests in sub-processes, we need to collect coverage # data inside each subprocess and then combine it into a single .coverage file. # To do this we set up a list which run_setup appends coverage objects to. # This is not intended to be used by packages other than astropy-helpers. import os import glob try: from coverage import CoverageData except ImportError: HAS_COVERAGE = False else: HAS_COVERAGE = True if HAS_COVERAGE: SUBPROCESS_COVERAGE = [] def pytest_configure(config): if HAS_COVERAGE: SUBPROCESS_COVERAGE.clear() def pytest_unconfigure(config): if HAS_COVERAGE: # We create an empty coverage data object combined_cdata = CoverageData() # Add all files from astropy_helpers to make sure we compute the total # coverage, not just the coverage of the files that have non-zero # coverage. lines = {} for filename in glob.glob(os.path.join('astropy_helpers', '**', '*.py'), recursive=True): lines[os.path.abspath(filename)] = [] for cdata in SUBPROCESS_COVERAGE: # For each CoverageData object, we go through all the files and # change the filename from one which might be a temporary path # to the local filename. We then only keep files that actually # exist. for filename in cdata.measured_files(): try: pos = filename.rindex('astropy_helpers') except ValueError: continue short_filename = filename[pos:] if os.path.exists(short_filename): lines[os.path.abspath(short_filename)].extend(cdata.lines(filename)) combined_cdata.add_lines(lines) combined_cdata.write_file('.coverage.subprocess') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/distutils_helpers.py0000644000076500000240000001764400000000000026253 0ustar00erikstaff00000000000000""" This module contains various utilities for introspecting the distutils module and the setup process. Some of these utilities require the `astropy_helpers.setup_helpers.register_commands` function to be called first, as it will affect introspection of setuptools command-line arguments. Other utilities in this module do not have that restriction. """ import os import sys from distutils import ccompiler, log from distutils.dist import Distribution from distutils.errors import DistutilsError from .utils import silence # This function, and any functions that call it, require the setup in # `astropy_helpers.setup_helpers.register_commands` to be run first. def get_dummy_distribution(): """ Returns a distutils Distribution object used to instrument the setup environment before calling the actual setup() function. """ from .setup_helpers import _module_state if _module_state['registered_commands'] is None: raise RuntimeError( 'astropy_helpers.setup_helpers.register_commands() must be ' 'called before using ' 'astropy_helpers.setup_helpers.get_dummy_distribution()') # Pre-parse the Distutils command-line options and config files to if # the option is set. dist = Distribution({'script_name': os.path.basename(sys.argv[0]), 'script_args': sys.argv[1:]}) dist.cmdclass.update(_module_state['registered_commands']) with silence(): try: dist.parse_config_files() dist.parse_command_line() except (DistutilsError, AttributeError, SystemExit): # Let distutils handle DistutilsErrors itself AttributeErrors can # get raise for ./setup.py --help SystemExit can be raised if a # display option was used, for example pass return dist def get_main_package_directory(distribution): """ Given a Distribution object, return the main package directory. """ return min(distribution.packages, key=len) def get_distutils_option(option, commands): """ 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. """ dist = get_dummy_distribution() for cmd in commands: cmd_opts = dist.command_options.get(cmd) if cmd_opts is not None and option in cmd_opts: return cmd_opts[option][1] else: return None def get_distutils_build_option(option): """ 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. """ return get_distutils_option(option, ['build', 'build_ext', 'build_clib']) def get_distutils_install_option(option): """ 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. """ return get_distutils_option(option, ['install']) def get_distutils_build_or_install_option(option): """ 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. """ return get_distutils_option(option, ['build', 'build_ext', 'build_clib', 'install']) def get_compiler_option(): """ Determines the compiler that will be used to build extension modules. Returns ------- compiler : str The compiler option specified for the build, build_ext, or build_clib command; or the default compiler for the platform if none was specified. """ compiler = get_distutils_build_option('compiler') if compiler is None: return ccompiler.get_default_compiler() return compiler def add_command_option(command, name, doc, is_bool=False): """ 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. """ dist = get_dummy_distribution() cmdcls = dist.get_command_class(command) if (hasattr(cmdcls, '_astropy_helpers_options') and name in cmdcls._astropy_helpers_options): return attr = name.replace('-', '_') if hasattr(cmdcls, attr): raise RuntimeError( '{0!r} already has a {1!r} class attribute, barring {2!r} from ' 'being usable as a custom option name.'.format(cmdcls, attr, name)) for idx, cmd in enumerate(cmdcls.user_options): if cmd[0] == name: log.warn('Overriding existing {0!r} option ' '{1!r}'.format(command, name)) del cmdcls.user_options[idx] if name in cmdcls.boolean_options: cmdcls.boolean_options.remove(name) break cmdcls.user_options.append((name, None, doc)) if is_bool: cmdcls.boolean_options.append(name) # Distutils' command parsing requires that a command object have an # attribute with the same name as the option (with '-' replaced with '_') # in order for that option to be recognized as valid setattr(cmdcls, attr, None) # This caches the options added through add_command_option so that if it is # run multiple times in the same interpreter repeated adds are ignored # (this way we can still raise a RuntimeError if a custom option overrides # a built-in option) if not hasattr(cmdcls, '_astropy_helpers_options'): cmdcls._astropy_helpers_options = set([name]) else: cmdcls._astropy_helpers_options.add(name) def get_distutils_display_options(): """ Returns a set of all the distutils display options in their long and short forms. These are the setup.py arguments such as --name or --version which print the project's metadata and then exit. Returns ------- opts : set The long and short form display option arguments, including the - or -- """ short_display_opts = set('-' + o[1] for o in Distribution.display_options if o[1]) long_display_opts = set('--' + o[0] for o in Distribution.display_options) # Include -h and --help which are not explicitly listed in # Distribution.display_options (as they are handled by optparse) short_display_opts.add('-h') long_display_opts.add('--help') # This isn't the greatest approach to hardcode these commands. # However, there doesn't seem to be a good way to determine # whether build *will be* run as part of the command at this # phase. display_commands = set([ 'clean', 'register', 'setopt', 'saveopts', 'egg_info', 'alias']) return short_display_opts.union(long_display_opts.union(display_commands)) def is_distutils_display_option(): """ Returns True if sys.argv contains any of the distutils display options such as --version or --name. """ display_options = get_distutils_display_options() return bool(set(sys.argv[1:]).intersection(display_options)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/git_helpers.py0000644000076500000240000001453700000000000025010 0ustar00erikstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Utilities for retrieving revision information from a project's git repository. """ # Do not remove the following comment; it is used by # astropy_helpers.version_helpers to determine the beginning of the code in # this module # BEGIN import locale import os import subprocess import warnings __all__ = ['get_git_devstr'] 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, 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. """ if path is None: path = os.getcwd() if not os.path.isdir(path): path = os.path.abspath(os.path.dirname(path)) 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, b'', b'') 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, b'', b'') 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 == 128: # git returns 128 if the command is not run from within a git # repository tree. In this case, a warning is produced above but we # return the default dev version of '0'. return '0' elif 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() # This function is tested but it is only ever executed within a subprocess when # creating a fake package, so it doesn't get picked up by coverage metrics. def _get_repo_path(pathname, levels=None): # pragma: no cover """ 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. """ if os.path.isfile(pathname): current_dir = os.path.abspath(os.path.dirname(pathname)) elif os.path.isdir(pathname): current_dir = os.path.abspath(pathname) else: return None current_level = 0 while levels is None or current_level <= levels: if os.path.exists(os.path.join(current_dir, '.git')): return current_dir current_level += 1 if current_dir == os.path.dirname(current_dir): break current_dir = os.path.dirname(current_dir) return None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/openmp_helpers.py0000644000076500000240000002256700000000000025525 0ustar00erikstaff00000000000000# This module defines functions that can be used to check whether OpenMP is # available and if so what flags to use. To use this, import the # add_openmp_flags_if_available function in a setup_package.py file where you # are defining your extensions: # # from astropy_helpers.openmp_helpers import add_openmp_flags_if_available # # then call it with a single extension as the only argument: # # add_openmp_flags_if_available(extension) # # this will add the OpenMP flags if available. from __future__ import absolute_import, print_function import os import sys import glob import time import datetime import tempfile import subprocess from distutils import log from distutils.ccompiler import new_compiler from distutils.sysconfig import customize_compiler, get_config_var from distutils.errors import CompileError, LinkError from .distutils_helpers import get_compiler_option __all__ = ['add_openmp_flags_if_available'] try: # Check if this has already been instantiated, only set the default once. _ASTROPY_DISABLE_SETUP_WITH_OPENMP_ except NameError: import builtins # It hasn't, so do so. builtins._ASTROPY_DISABLE_SETUP_WITH_OPENMP_ = False CCODE = """ #include #include int main(void) { #pragma omp parallel printf("nthreads=%d\\n", omp_get_num_threads()); return 0; } """ def _get_flag_value_from_var(flag, var, delim=' '): """ Extract flags from an environment variable. Parameters ---------- flag : str The flag to extract, for example '-I' or '-L' var : str The environment variable to extract the flag from, e.g. CFLAGS or LDFLAGS. delim : str, optional The delimiter separating flags inside the environment variable Examples -------- Let's assume the LDFLAGS is set to '-L/usr/local/include -customflag'. This function will then return the following: >>> _get_flag_value_from_var('-L', 'LDFLAGS') '/usr/local/include' Notes ----- Environment variables are first checked in ``os.environ[var]``, then in ``distutils.sysconfig.get_config_var(var)``. This function is not supported on Windows. """ if sys.platform.startswith('win'): return None # Simple input validation if not var or not flag: return None flag_length = len(flag) if not flag_length: return None # Look for var in os.eviron then in get_config_var if var in os.environ: flags = os.environ[var] else: try: flags = get_config_var(var) except KeyError: return None # Extract flag from {var:value} if flags: for item in flags.split(delim): if item.startswith(flag): return item[flag_length:] def get_openmp_flags(): """ Utility for returning compiler and linker flags possibly needed for OpenMP support. Returns ------- result : `{'compiler_flags':, 'linker_flags':}` Notes ----- The flags returned are not tested for validity, use `check_openmp_support(openmp_flags=get_openmp_flags())` to do so. """ compile_flags = [] link_flags = [] if get_compiler_option() == 'msvc': compile_flags.append('-openmp') else: include_path = _get_flag_value_from_var('-I', 'CFLAGS') if include_path: compile_flags.append('-I' + include_path) lib_path = _get_flag_value_from_var('-L', 'LDFLAGS') if lib_path: link_flags.append('-L' + lib_path) link_flags.append('-Wl,-rpath,' + lib_path) compile_flags.append('-fopenmp') link_flags.append('-fopenmp') return {'compiler_flags': compile_flags, 'linker_flags': link_flags} def check_openmp_support(openmp_flags=None): """ Check whether OpenMP test code can be compiled and run. Parameters ---------- openmp_flags : dict, optional This should be a dictionary with keys ``compiler_flags`` and ``linker_flags`` giving the compiliation and linking flags respectively. These are passed as `extra_postargs` to `compile()` and `link_executable()` respectively. If this is not set, the flags will be automatically determined using environment variables. Returns ------- result : bool `True` if the test passed, `False` otherwise. """ ccompiler = new_compiler() customize_compiler(ccompiler) if not openmp_flags: # customize_compiler() extracts info from os.environ. If certain keys # exist it uses these plus those from sysconfig.get_config_vars(). # If the key is missing in os.environ it is not extracted from # sysconfig.get_config_var(). E.g. 'LDFLAGS' get left out, preventing # clang from finding libomp.dylib because -L is not passed to # linker. Call get_openmp_flags() to get flags missed by # customize_compiler(). openmp_flags = get_openmp_flags() compile_flags = openmp_flags.get('compiler_flags') link_flags = openmp_flags.get('linker_flags') # Pass -coverage flag to linker. # https://github.com/astropy/astropy-helpers/pull/374 if '-coverage' in compile_flags and '-coverage' not in link_flags: link_flags.append('-coverage') tmp_dir = tempfile.mkdtemp() start_dir = os.path.abspath('.') try: os.chdir(tmp_dir) # Write test program with open('test_openmp.c', 'w') as f: f.write(CCODE) os.mkdir('objects') # Compile, test program ccompiler.compile(['test_openmp.c'], output_dir='objects', extra_postargs=compile_flags) # Link test program objects = glob.glob(os.path.join('objects', '*' + ccompiler.obj_extension)) ccompiler.link_executable(objects, 'test_openmp', extra_postargs=link_flags) # Run test program output = subprocess.check_output('./test_openmp') output = output.decode(sys.stdout.encoding or 'utf-8').splitlines() if 'nthreads=' in output[0]: nthreads = int(output[0].strip().split('=')[1]) if len(output) == nthreads: is_openmp_supported = True else: log.warn("Unexpected number of lines from output of test OpenMP " "program (output was {0})".format(output)) is_openmp_supported = False else: log.warn("Unexpected output from test OpenMP " "program (output was {0})".format(output)) is_openmp_supported = False except (CompileError, LinkError, subprocess.CalledProcessError): is_openmp_supported = False finally: os.chdir(start_dir) return is_openmp_supported def is_openmp_supported(): """ Determine whether the build compiler has OpenMP support. """ log_threshold = log.set_threshold(log.FATAL) ret = check_openmp_support() log.set_threshold(log_threshold) return ret def add_openmp_flags_if_available(extension): """ Add OpenMP compilation flags, if supported (if not a warning will be printed to the console and no flags will be added.) Returns `True` if the flags were added, `False` otherwise. """ if _ASTROPY_DISABLE_SETUP_WITH_OPENMP_: log.info("OpenMP support has been explicitly disabled.") return False openmp_flags = get_openmp_flags() using_openmp = check_openmp_support(openmp_flags=openmp_flags) if using_openmp: compile_flags = openmp_flags.get('compiler_flags') link_flags = openmp_flags.get('linker_flags') log.info("Compiling Cython/C/C++ extension with OpenMP support") extension.extra_compile_args.extend(compile_flags) extension.extra_link_args.extend(link_flags) else: log.warn("Cannot compile Cython/C/C++ extension with OpenMP, reverting " "to non-parallel code") return using_openmp _IS_OPENMP_ENABLED_SRC = """ # Autogenerated by {packagetitle}'s setup.py on {timestamp!s} def is_openmp_enabled(): \"\"\" Determine whether this package was built with OpenMP support. \"\"\" return {return_bool} """[1:] def generate_openmp_enabled_py(packagename, srcdir='.', disable_openmp=None): """ Generate ``package.openmp_enabled.is_openmp_enabled``, which can then be used to determine, post build, whether the package was built with or without OpenMP support. """ if packagename.lower() == 'astropy': packagetitle = 'Astropy' else: packagetitle = packagename epoch = int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) timestamp = datetime.datetime.utcfromtimestamp(epoch) if disable_openmp is not None: import builtins builtins._ASTROPY_DISABLE_SETUP_WITH_OPENMP_ = disable_openmp if _ASTROPY_DISABLE_SETUP_WITH_OPENMP_: log.info("OpenMP support has been explicitly disabled.") openmp_support = False if _ASTROPY_DISABLE_SETUP_WITH_OPENMP_ else is_openmp_supported() src = _IS_OPENMP_ENABLED_SRC.format(packagetitle=packagetitle, timestamp=timestamp, return_bool=openmp_support) package_srcdir = os.path.join(srcdir, *packagename.split('.')) is_openmp_enabled_py = os.path.join(package_srcdir, 'openmp_enabled.py') with open(is_openmp_enabled_py, 'w') as f: f.write(src) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/setup_helpers.py0000644000076500000240000007062200000000000025362 0ustar00erikstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module contains a number of utilities for use during setup/build/packaging that are useful to astropy as a whole. """ from __future__ import absolute_import import collections import os import re import subprocess import sys import traceback import warnings from configparser import ConfigParser import builtins from distutils import log from distutils.errors import DistutilsOptionError, DistutilsModuleError from distutils.core import Extension from distutils.core import Command from distutils.command.sdist import sdist as DistutilsSdist from setuptools import setup as setuptools_setup from setuptools.config import read_configuration from setuptools import find_packages as _find_packages from .distutils_helpers import (add_command_option, get_compiler_option, get_dummy_distribution, get_distutils_build_option, get_distutils_build_or_install_option) from .version_helpers import get_pkg_version_module, generate_version_py from .utils import (walk_skip_hidden, import_file, extends_doc, resolve_name, AstropyDeprecationWarning) from .commands.build_ext import AstropyHelpersBuildExt from .commands.test import AstropyTest # These imports are not used in this module, but are included for backwards # compat with older versions of this module from .utils import get_numpy_include_path, write_if_different # noqa __all__ = ['register_commands', 'get_package_info'] _module_state = {'registered_commands': None, 'have_sphinx': False, 'package_cache': None, 'exclude_packages': set(), 'excludes_too_late': False} try: import sphinx # noqa _module_state['have_sphinx'] = True except ValueError as e: # This can occur deep in the bowels of Sphinx's imports by way of docutils # and an occurrence of this bug: http://bugs.python.org/issue18378 # In this case sphinx is effectively unusable if 'unknown locale' in e.args[0]: log.warn( "Possible 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/") except ImportError: pass except SyntaxError: # occurs if markupsafe is recent version, which doesn't support Python 3.2 pass def setup(**kwargs): """ A wrapper around setuptools' setup() function that automatically sets up custom commands, generates a version file, and customizes the setup process via the ``setup_package.py`` files. """ # DEPRECATED: store the package name in a built-in variable so it's easy # to get from other parts of the setup infrastructure. We should phase this # out in packages that use it - the cookiecutter template should now be # able to put the right package name where needed. conf = read_configuration('setup.cfg') builtins._ASTROPY_PACKAGE_NAME_ = conf['metadata']['name'] # Create a dictionary with setup command overrides. Note that this gets # information about the package (name and version) from the setup.cfg file. cmdclass = register_commands() # Freeze build information in version.py. Note that this gets information # about the package (name and version) from the setup.cfg file. version = generate_version_py() # Get configuration information from all of the various subpackages. # See the docstring for setup_helpers.update_package_files for more # details. package_info = get_package_info() package_info['cmdclass'] = cmdclass package_info['version'] = version # Override using any specified keyword arguments package_info.update(kwargs) setuptools_setup(**package_info) def adjust_compiler(package): warnings.warn( 'The adjust_compiler function in setup.py is ' 'deprecated and can be removed from your setup.py.', AstropyDeprecationWarning) def get_debug_option(packagename): """ Determines if the build is in debug mode. Returns ------- debug : bool True if the current build was started with the debug option, False otherwise. """ try: current_debug = get_pkg_version_module(packagename, fromlist=['debug'])[0] except (ImportError, AttributeError): current_debug = None # Only modify the debug flag if one of the build commands was explicitly # run (i.e. not as a sub-command of something else) dist = get_dummy_distribution() if any(cmd in dist.commands for cmd in ['build', 'build_ext']): debug = bool(get_distutils_build_option('debug')) else: debug = bool(current_debug) if current_debug is not None and current_debug != debug: build_ext_cmd = dist.get_command_class('build_ext') build_ext_cmd._force_rebuild = True return debug def add_exclude_packages(excludes): if _module_state['excludes_too_late']: raise RuntimeError( "add_package_excludes must be called before all other setup helper " "functions in order to properly handle excluded packages") _module_state['exclude_packages'].update(set(excludes)) def register_commands(package=None, version=None, release=None, srcdir='.'): """ This function generates a dictionary containing customized commands that can then be passed to the ``cmdclass`` argument in ``setup()``. """ if package is not None: warnings.warn('The package argument to generate_version_py has ' 'been deprecated and will be removed in future. Specify ' 'the package name in setup.cfg instead', AstropyDeprecationWarning) if version is not None: warnings.warn('The version argument to generate_version_py has ' 'been deprecated and will be removed in future. Specify ' 'the version number in setup.cfg instead', AstropyDeprecationWarning) if release is not None: warnings.warn('The release argument to generate_version_py has ' 'been deprecated and will be removed in future. We now ' 'use the presence of the "dev" string in the version to ' 'determine whether this is a release', AstropyDeprecationWarning) # We use ConfigParser instead of read_configuration here because the latter # only reads in keys recognized by setuptools, but we need to access # package_name below. conf = ConfigParser() conf.read('setup.cfg') if conf.has_option('metadata', 'name'): package = conf.get('metadata', 'name') elif conf.has_option('metadata', 'package_name'): # The package-template used package_name instead of name for a while warnings.warn('Specifying the package name using the "package_name" ' 'option in setup.cfg is deprecated - use the "name" ' 'option instead.', AstropyDeprecationWarning) package = conf.get('metadata', 'package_name') elif package is not None: # deprecated pass else: sys.stderr.write('ERROR: Could not read package name from setup.cfg\n') sys.exit(1) if _module_state['registered_commands'] is not None: return _module_state['registered_commands'] if _module_state['have_sphinx']: try: from .commands.build_sphinx import (AstropyBuildSphinx, AstropyBuildDocs) except ImportError: AstropyBuildSphinx = AstropyBuildDocs = FakeBuildSphinx else: AstropyBuildSphinx = AstropyBuildDocs = FakeBuildSphinx _module_state['registered_commands'] = registered_commands = { 'test': generate_test_command(package), # Use distutils' sdist because it respects package_data. # setuptools/distributes sdist requires duplication of information in # MANIFEST.in 'sdist': DistutilsSdist, 'build_ext': AstropyHelpersBuildExt, 'build_sphinx': AstropyBuildSphinx, 'build_docs': AstropyBuildDocs } # Need to override the __name__ here so that the commandline options are # presented as being related to the "build" command, for example; normally # this wouldn't be necessary since commands also have a command_name # attribute, but there is a bug in distutils' help display code that it # uses __name__ instead of command_name. Yay distutils! for name, cls in registered_commands.items(): cls.__name__ = name # Add a few custom options; more of these can be added by specific packages # later for option in [ ('use-system-libraries', "Use system libraries whenever possible", True)]: add_command_option('build', *option) add_command_option('install', *option) add_command_hooks(registered_commands, srcdir=srcdir) return registered_commands def add_command_hooks(commands, srcdir='.'): """ Look through setup_package.py modules for functions with names like ``pre__hook`` and ``post__hook`` where ```` is the name of a ``setup.py`` command (e.g. build_ext). If either hook is present this adds a wrapped version of that command to the passed in ``commands`` `dict`. ``commands`` may be pre-populated with other custom distutils command classes that should be wrapped if there are hooks for them (e.g. `AstropyBuildPy`). """ hook_re = re.compile(r'^(pre|post)_(.+)_hook$') # Distutils commands have a method of the same name, but it is not a # *classmethod* (which probably didn't exist when distutils was first # written) def get_command_name(cmdcls): if hasattr(cmdcls, 'command_name'): return cmdcls.command_name else: return cmdcls.__name__ packages = find_packages(srcdir) dist = get_dummy_distribution() hooks = collections.defaultdict(dict) for setuppkg in iter_setup_packages(srcdir, packages): for name, obj in vars(setuppkg).items(): match = hook_re.match(name) if not match: continue hook_type = match.group(1) cmd_name = match.group(2) if hook_type not in hooks[cmd_name]: hooks[cmd_name][hook_type] = [] hooks[cmd_name][hook_type].append((setuppkg.__name__, obj)) for cmd_name, cmd_hooks in hooks.items(): commands[cmd_name] = generate_hooked_command( cmd_name, dist.get_command_class(cmd_name), cmd_hooks) def generate_hooked_command(cmd_name, cmd_cls, hooks): """ Returns a generated subclass of ``cmd_cls`` that runs the pre- and post-command hooks for that command before and after the ``cmd_cls.run`` method. """ def run(self, orig_run=cmd_cls.run): self.run_command_hooks('pre_hooks') orig_run(self) self.run_command_hooks('post_hooks') return type(cmd_name, (cmd_cls, object), {'run': run, 'run_command_hooks': run_command_hooks, 'pre_hooks': hooks.get('pre', []), 'post_hooks': hooks.get('post', [])}) def run_command_hooks(cmd_obj, hook_kind): """Run hooks registered for that command and phase. *cmd_obj* is a finalized command object; *hook_kind* is either 'pre_hook' or 'post_hook'. """ hooks = getattr(cmd_obj, hook_kind, None) if not hooks: return for modname, hook in hooks: if isinstance(hook, str): try: hook_obj = resolve_name(hook) except ImportError as exc: raise DistutilsModuleError( 'cannot find hook {0}: {1}'.format(hook, exc)) else: hook_obj = hook if not callable(hook_obj): raise DistutilsOptionError('hook {0!r} is not callable' % hook) log.info('running {0} from {1} for {2} command'.format( hook_kind.rstrip('s'), modname, cmd_obj.get_command_name())) try: hook_obj(cmd_obj) except Exception: log.error('{0} command hook {1} raised an exception: %s\n'.format( hook_obj.__name__, cmd_obj.get_command_name())) log.error(traceback.format_exc()) sys.exit(1) def generate_test_command(package_name): """ Creates a custom 'test' command for the given package which sets the command's ``package_name`` class attribute to the name of the package being tested. """ return type(package_name.title() + 'Test', (AstropyTest,), {'package_name': package_name}) def update_package_files(srcdir, extensions, package_data, packagenames, package_dirs): """ 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. """ info = get_package_info(srcdir) extensions.extend(info['ext_modules']) package_data.update(info['package_data']) packagenames = list(set(packagenames + info['packages'])) package_dirs.update(info['package_dir']) def get_package_info(srcdir='.', exclude=()): """ Collates all of the information for building all 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()``, and ``get_external_libraries()``. 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. """ ext_modules = [] packages = [] package_dir = {} # Read in existing package data, and add to it below setup_cfg = os.path.join(srcdir, 'setup.cfg') if os.path.exists(setup_cfg): conf = read_configuration(setup_cfg) if 'options' in conf and 'package_data' in conf['options']: package_data = conf['options']['package_data'] else: package_data = {} else: package_data = {} if exclude: warnings.warn( "Use of the exclude parameter is no longer supported since it does " "not work as expected. Use add_exclude_packages instead. Note that " "it must be called prior to any other calls from setup helpers.", AstropyDeprecationWarning) # Use the find_packages tool to locate all packages and modules packages = find_packages(srcdir, exclude=exclude) # Update package_dir if the package lies in a subdirectory if srcdir != '.': package_dir[''] = srcdir # For each of the setup_package.py modules, extract any # information that is needed to install them. The build options # are extracted first, so that their values will be available in # subsequent calls to `get_extensions`, etc. for setuppkg in iter_setup_packages(srcdir, packages): if hasattr(setuppkg, 'get_build_options'): options = setuppkg.get_build_options() for option in options: add_command_option('build', *option) if hasattr(setuppkg, 'get_external_libraries'): libraries = setuppkg.get_external_libraries() for library in libraries: add_external_library(library) for setuppkg in iter_setup_packages(srcdir, packages): # get_extensions must include any Cython extensions by their .pyx # filename. if hasattr(setuppkg, 'get_extensions'): ext_modules.extend(setuppkg.get_extensions()) if hasattr(setuppkg, 'get_package_data'): package_data.update(setuppkg.get_package_data()) # Locate any .pyx files not already specified, and add their extensions in. # The default include dirs include numpy to facilitate numerical work. ext_modules.extend(get_cython_extensions(srcdir, packages, ext_modules, ['numpy'])) # Now remove extensions that have the special name 'skip_cython', as they # exist Only to indicate that the cython extensions shouldn't be built for i, ext in reversed(list(enumerate(ext_modules))): if ext.name == 'skip_cython': del ext_modules[i] # On Microsoft compilers, we need to pass the '/MANIFEST' # commandline argument. This was the default on MSVC 9.0, but is # now required on MSVC 10.0, but it doesn't seem to hurt to add # it unconditionally. if get_compiler_option() == 'msvc': for ext in ext_modules: ext.extra_link_args.append('/MANIFEST') return { 'ext_modules': ext_modules, 'packages': packages, 'package_dir': package_dir, 'package_data': package_data, } def iter_setup_packages(srcdir, packages): """ 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. """ for packagename in packages: package_parts = packagename.split('.') package_path = os.path.join(srcdir, *package_parts) setup_package = os.path.relpath( os.path.join(package_path, 'setup_package.py')) if os.path.isfile(setup_package): module = import_file(setup_package, name=packagename + '.setup_package') yield module def iter_pyx_files(package_dir, package_name): """ 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. """ for dirpath, dirnames, filenames in walk_skip_hidden(package_dir): for fn in filenames: if fn.endswith('.pyx'): fullfn = os.path.relpath(os.path.join(dirpath, fn)) # Package must match file name extmod = '.'.join([package_name, fn[:-4]]) yield (extmod, fullfn) break # Don't recurse into subdirectories def get_cython_extensions(srcdir, packages, prevextensions=tuple(), extincludedirs=None): """ 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`). """ # Vanilla setuptools and old versions of distribute include Cython files # as .c files in the sources, not .pyx, so we cannot simply look for # existing .pyx sources in the previous sources, but we should also check # for .c files with the same remaining filename. So we look for .pyx and # .c files, and we strip the extension. prevsourcepaths = [] ext_modules = [] for ext in prevextensions: for s in ext.sources: if s.endswith(('.pyx', '.c', '.cpp')): sourcepath = os.path.realpath(os.path.splitext(s)[0]) prevsourcepaths.append(sourcepath) for package_name in packages: package_parts = package_name.split('.') package_path = os.path.join(srcdir, *package_parts) for extmod, pyxfn in iter_pyx_files(package_path, package_name): sourcepath = os.path.realpath(os.path.splitext(pyxfn)[0]) if sourcepath not in prevsourcepaths: ext_modules.append(Extension(extmod, [pyxfn], include_dirs=extincludedirs)) return ext_modules class DistutilsExtensionArgs(collections.defaultdict): """ 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. """ def __init__(self, *args, **kwargs): def default_factory(): return [] super(DistutilsExtensionArgs, self).__init__( default_factory, *args, **kwargs) def update(self, other): for key, val in other.items(): self[key].extend(val) def pkg_config(packages, default_libraries, executable='pkg-config'): """ 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 """ flag_map = {'-I': 'include_dirs', '-L': 'library_dirs', '-l': 'libraries', '-D': 'define_macros', '-U': 'undef_macros'} command = "{0} --libs --cflags {1}".format(executable, ' '.join(packages)), result = DistutilsExtensionArgs() try: pipe = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) output = pipe.communicate()[0].strip() except subprocess.CalledProcessError as e: lines = [ ("{0} failed. This may cause the build to fail below." .format(executable)), " command: {0}".format(e.cmd), " returncode: {0}".format(e.returncode), " output: {0}".format(e.output) ] log.warn('\n'.join(lines)) result['libraries'].extend(default_libraries) else: if pipe.returncode != 0: lines = [ "pkg-config could not lookup up package(s) {0}.".format( ", ".join(packages)), "This may cause the build to fail below." ] log.warn('\n'.join(lines)) result['libraries'].extend(default_libraries) else: for token in output.split(): # It's not clear what encoding the output of # pkg-config will come to us in. It will probably be # some combination of pure ASCII (for the compiler # flags) and the filesystem encoding (for any argument # that includes directories or filenames), but this is # just conjecture, as the pkg-config documentation # doesn't seem to address it. arg = token[:2].decode('ascii') value = token[2:].decode(sys.getfilesystemencoding()) if arg in flag_map: if arg == '-D': value = tuple(value.split('=', 1)) result[flag_map[arg]].append(value) else: result['extra_compile_args'].append(value) return result def add_external_library(library): """ 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`. """ for command in ['build', 'build_ext', 'install']: add_command_option(command, str('use-system-' + library), 'Use the system {0} library'.format(library), is_bool=True) def use_system_library(library): """ 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. """ return ( get_distutils_build_or_install_option('use_system_{0}'.format(library)) or get_distutils_build_or_install_option('use_system_libraries')) @extends_doc(_find_packages) def find_packages(where='.', exclude=(), invalidate_cache=False): """ This version of ``find_packages`` caches previous results to speed up subsequent calls. Use ``invalide_cache=True`` to ignore cached results from previous ``find_packages`` calls, and repeat the package search. """ if exclude: warnings.warn( "Use of the exclude parameter is no longer supported since it does " "not work as expected. Use add_exclude_packages instead. Note that " "it must be called prior to any other calls from setup helpers.", AstropyDeprecationWarning) # Calling add_exclude_packages after this point will have no effect _module_state['excludes_too_late'] = True if not invalidate_cache and _module_state['package_cache'] is not None: return _module_state['package_cache'] packages = _find_packages( where=where, exclude=list(_module_state['exclude_packages'])) _module_state['package_cache'] = packages return packages class FakeBuildSphinx(Command): """ A dummy build_sphinx command that is called if Sphinx is not installed and displays a relevant error message """ # user options inherited from sphinx.setup_command.BuildDoc user_options = [ ('fresh-env', 'E', ''), ('all-files', 'a', ''), ('source-dir=', 's', ''), ('build-dir=', None, ''), ('config-dir=', 'c', ''), ('builder=', 'b', ''), ('project=', None, ''), ('version=', None, ''), ('release=', None, ''), ('today=', None, ''), ('link-index', 'i', '')] # user options appended in astropy.setup_helpers.AstropyBuildSphinx user_options.append(('warnings-returncode', 'w', '')) user_options.append(('clean-docs', 'l', '')) user_options.append(('no-intersphinx', 'n', '')) user_options.append(('open-docs-in-browser', 'o', '')) def initialize_options(self): try: raise RuntimeError("Sphinx and its dependencies must be installed " "for build_docs.") except: log.error('error: Sphinx and its dependencies must be installed ' 'for build_docs.') sys.exit(1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/astropy_helpers/astropy_helpers/sphinx/0000755000076500000240000000000000000000000023430 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/sphinx/__init__.py0000644000076500000240000000000000000000000025527 0ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/sphinx/conf.py0000644000076500000240000000023400000000000024726 0ustar00erikstaff00000000000000import warnings from sphinx_astropy.conf import * warnings.warn("Note that astropy_helpers.sphinx.conf is deprecated - use sphinx_astropy.conf instead") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/utils.py0000644000076500000240000002070100000000000023631 0ustar00erikstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst from __future__ import absolute_import, unicode_literals import contextlib import functools import imp import os import sys import glob from importlib import machinery as import_machinery # Note: The following Warning subclasses are simply copies of the Warnings in # Astropy of the same names. class AstropyWarning(Warning): """ The base warning class from which all Astropy warnings should inherit. Any warning inheriting from this class is handled by the Astropy logger. """ class AstropyDeprecationWarning(AstropyWarning): """ A warning class to indicate a deprecated feature. """ class AstropyPendingDeprecationWarning(PendingDeprecationWarning, AstropyWarning): """ A warning class to indicate a soon-to-be deprecated feature. """ def _get_platlib_dir(cmd): """ Given a build command, return the name of the appropriate platform-specific build subdirectory directory (e.g. build/lib.linux-x86_64-2.7) """ plat_specifier = '.{0}-{1}'.format(cmd.plat_name, sys.version[0:3]) return os.path.join(cmd.build_base, 'lib' + plat_specifier) def get_numpy_include_path(): """ Gets the path to the numpy headers. """ # We need to go through this nonsense in case setuptools # downloaded and installed Numpy for us as part of the build or # install, since Numpy may still think it's in "setup mode", when # in fact we're ready to use it to build astropy now. import builtins if hasattr(builtins, '__NUMPY_SETUP__'): del builtins.__NUMPY_SETUP__ import imp import numpy imp.reload(numpy) try: numpy_include = numpy.get_include() except AttributeError: numpy_include = numpy.get_numpy_include() return numpy_include class _DummyFile(object): """A noop writeable object.""" errors = '' 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 if sys.platform == 'win32': import ctypes def _has_hidden_attribute(filepath): """ 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 """ if isinstance(filepath, bytes): filepath = filepath.decode(sys.getfilesystemencoding()) try: attrs = ctypes.windll.kernel32.GetFileAttributesW(filepath) assert attrs != -1 result = bool(attrs & 2) except (AttributeError, AssertionError): result = False return result else: def _has_hidden_attribute(filepath): return False def is_path_hidden(filepath): """ 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 """ name = os.path.basename(os.path.abspath(filepath)) if isinstance(name, bytes): is_dotted = name.startswith(b'.') else: is_dotted = name.startswith('.') return is_dotted or _has_hidden_attribute(filepath) def walk_skip_hidden(top, onerror=None, followlinks=False): """ A wrapper for `os.walk` that skips hidden files and directories. This function does not have the parameter `topdown` from `os.walk`: the directories must always be recursed top-down when using this function. See also -------- os.walk : For a description of the parameters """ for root, dirs, files in os.walk( top, topdown=True, onerror=onerror, followlinks=followlinks): # These lists must be updated in-place so os.walk will skip # hidden directories dirs[:] = [d for d in dirs if not is_path_hidden(d)] files[:] = [f for f in files if not is_path_hidden(f)] yield root, dirs, files def write_if_different(filename, data): """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`. """ assert isinstance(data, bytes) if os.path.exists(filename): with open(filename, 'rb') as fd: original_data = fd.read() else: original_data = None if original_data != data: with open(filename, 'wb') as fd: fd.write(data) def import_file(filename, name=None): """ Imports a module from a single file as if it doesn't belong to a particular package. The returned module will have the optional ``name`` if given, or else a name generated from the filename. """ # Specifying a traditional dot-separated fully qualified name here # results in a number of "Parent module 'astropy' not found while # handling absolute import" warnings. Using the same name, the # namespaces of the modules get merged together. So, this # generates an underscore-separated name which is more likely to # be unique, and it doesn't really matter because the name isn't # used directly here anyway. mode = 'r' if name is None: basename = os.path.splitext(filename)[0] name = '_'.join(os.path.relpath(basename).split(os.sep)[1:]) if not os.path.exists(filename): raise ImportError('Could not import file {0}'.format(filename)) if import_machinery: loader = import_machinery.SourceFileLoader(name, filename) mod = loader.load_module() else: with open(filename, mode) as fd: mod = imp.load_module(name, fd, filename, ('.py', mode, 1)) return mod def resolve_name(name): """Resolve a name like ``module.object`` to an object and return it. Raise `ImportError` if the module or name is not found. """ parts = name.split('.') cursor = len(parts) - 1 module_name = parts[:cursor] attr_name = parts[-1] while cursor > 0: try: ret = __import__('.'.join(module_name), fromlist=[attr_name]) break except ImportError: if cursor == 0: raise cursor -= 1 module_name = parts[:cursor] attr_name = parts[cursor] ret = '' for part in parts[cursor:]: try: ret = getattr(ret, part) except AttributeError: raise ImportError(name) return ret def extends_doc(extended_func): """ A function decorator for use when wrapping an existing function but adding additional functionality. This copies the docstring from the original function, and appends to it (along with a newline) the docstring of the wrapper function. Examples -------- >>> def foo(): ... '''Hello.''' ... >>> @extends_doc(foo) ... def bar(): ... '''Goodbye.''' ... >>> print(bar.__doc__) Hello. Goodbye. """ def decorator(func): if not (extended_func.__doc__ is None or func.__doc__ is None): func.__doc__ = '\n\n'.join([extended_func.__doc__.rstrip('\n'), func.__doc__.lstrip('\n')]) return func return decorator def find_data_files(package, pattern): """ Include files matching ``pattern`` inside ``package``. Parameters ---------- package : str The package inside which to look for data files pattern : str Pattern (glob-style) to match for the data files (e.g. ``*.dat``). This supports the``**``recursive syntax. For example, ``**/*.fits`` matches all files ending with ``.fits`` recursively. Only one instance of ``**`` can be included in the pattern. """ return glob.glob(os.path.join(package, pattern), recursive=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500356.0 specutils-0.7/astropy_helpers/astropy_helpers/version.py0000644000076500000240000000106600000000000024161 0ustar00erikstaff00000000000000# Autogenerated by Astropy-affiliated package astropy_helpers's setup.py on 2018-09-21 03:25:56 UTC from __future__ import unicode_literals import datetime version = "3.0.2" githash = "ca630405b54d40695b7bbd2824cb0f142b5811b6" major = 3 minor = 0 bugfix = 2 release = True timestamp = datetime.datetime(2018, 9, 21, 3, 25, 56) debug = False astropy_helpers_version = "" try: from ._compiler import compiler except ImportError: compiler = "unknown" try: from .cython_version import cython_version except ImportError: cython_version = "unknown" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986938.0 specutils-0.7/astropy_helpers/astropy_helpers/version_helpers.py0000644000076500000240000003073700000000000025712 0ustar00erikstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Utilities for generating the version string for Astropy (or an affiliated package) and the version.py module, which contains version info for the package. Within the generated astropy.version module, the `major`, `minor`, and `bugfix` variables hold the respective parts of the version number (bugfix is '0' if absent). The `release` variable is True if this is a release, and False if this is a development version of astropy. For the actual version string, use:: from astropy.version import version or:: from astropy import __version__ """ from __future__ import division import datetime import os import pkgutil import sys import time import warnings from distutils import log from configparser import ConfigParser import pkg_resources from . import git_helpers from .distutils_helpers import is_distutils_display_option from .git_helpers import get_git_devstr from .utils import AstropyDeprecationWarning, import_file __all__ = ['generate_version_py'] def _version_split(version): """ Split a version string into major, minor, and bugfix numbers. If any of those numbers are missing the default is zero. Any pre/post release modifiers are ignored. Examples ======== >>> _version_split('1.2.3') (1, 2, 3) >>> _version_split('1.2') (1, 2, 0) >>> _version_split('1.2rc1') (1, 2, 0) >>> _version_split('1') (1, 0, 0) >>> _version_split('') (0, 0, 0) """ parsed_version = pkg_resources.parse_version(version) if hasattr(parsed_version, 'base_version'): # New version parsing for setuptools >= 8.0 if parsed_version.base_version: parts = [int(part) for part in parsed_version.base_version.split('.')] else: parts = [] else: parts = [] for part in parsed_version: if part.startswith('*'): # Ignore any .dev, a, b, rc, etc. break parts.append(int(part)) if len(parts) < 3: parts += [0] * (3 - len(parts)) # In principle a version could have more parts (like 1.2.3.4) but we only # support .. return tuple(parts[:3]) # This is used by setup.py to create a new version.py - see that file for # details. Note that the imports have to be absolute, since this is also used # by affiliated packages. _FROZEN_VERSION_PY_TEMPLATE = """ # Autogenerated by {packagetitle}'s setup.py on {timestamp!s} UTC from __future__ import unicode_literals import datetime {header} major = {major} minor = {minor} bugfix = {bugfix} version_info = (major, minor, bugfix) release = {rel} timestamp = {timestamp!r} debug = {debug} astropy_helpers_version = "{ahver}" """[1:] _FROZEN_VERSION_PY_WITH_GIT_HEADER = """ {git_helpers} _packagename = "{packagename}" _last_generated_version = "{verstr}" _last_githash = "{githash}" # Determine where the source code for this module # lives. If __file__ is not a filesystem path then # it is assumed not to live in a git repo at all. if _get_repo_path(__file__, levels=len(_packagename.split('.'))): version = update_git_devstr(_last_generated_version, path=__file__) githash = get_git_devstr(sha=True, show_warning=False, path=__file__) or _last_githash else: # The file does not appear to live in a git repo so don't bother # invoking git version = _last_generated_version githash = _last_githash """[1:] _FROZEN_VERSION_PY_STATIC_HEADER = """ version = "{verstr}" githash = "{githash}" """[1:] def _get_version_py_str(packagename, version, githash, release, debug, uses_git=True): try: from astropy_helpers import __version__ as ahver except ImportError: ahver = "unknown" epoch = int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) timestamp = datetime.datetime.utcfromtimestamp(epoch) major, minor, bugfix = _version_split(version) if packagename.lower() == 'astropy': packagetitle = 'Astropy' else: packagetitle = 'Astropy-affiliated package ' + packagename header = '' if uses_git: header = _generate_git_header(packagename, version, githash) elif not githash: # _generate_git_header will already generate a new git has for us, but # for creating a new version.py for a release (even if uses_git=False) # we still need to get the githash to include in the version.py # See https://github.com/astropy/astropy-helpers/issues/141 githash = git_helpers.get_git_devstr(sha=True, show_warning=True) if not header: # If _generate_git_header fails it returns an empty string header = _FROZEN_VERSION_PY_STATIC_HEADER.format(verstr=version, githash=githash) return _FROZEN_VERSION_PY_TEMPLATE.format(packagetitle=packagetitle, timestamp=timestamp, header=header, major=major, minor=minor, bugfix=bugfix, ahver=ahver, rel=release, debug=debug) def _generate_git_header(packagename, version, githash): """ Generates a header to the version.py module that includes utilities for probing the git repository for updates (to the current git hash, etc.) These utilities should only be available in development versions, and not in release builds. If this fails for any reason an empty string is returned. """ loader = pkgutil.get_loader(git_helpers) source = loader.get_source(git_helpers.__name__) or '' source_lines = source.splitlines() if not source_lines: log.warn('Cannot get source code for astropy_helpers.git_helpers; ' 'git support disabled.') return '' idx = 0 for idx, line in enumerate(source_lines): if line.startswith('# BEGIN'): break git_helpers_py = '\n'.join(source_lines[idx + 1:]) verstr = version new_githash = git_helpers.get_git_devstr(sha=True, show_warning=False) if new_githash: githash = new_githash return _FROZEN_VERSION_PY_WITH_GIT_HEADER.format( git_helpers=git_helpers_py, packagename=packagename, verstr=verstr, githash=githash) def generate_version_py(packagename=None, version=None, release=None, debug=None, uses_git=None, srcdir='.'): """ Generate a version.py file in the package with version information, and update developer version strings. This function should normally be called without any arguments. In this case the package name and version is read in from the ``setup.cfg`` file (from the ``name`` or ``package_name`` entry and the ``version`` entry in the ``[metadata]`` section). If the version is a developer version (of the form ``3.2.dev``), the version string will automatically be expanded to include a sequential number as a suffix (e.g. ``3.2.dev13312``), and the updated version string will be returned by this function. Based on this updated version string, a ``version.py`` file will be generated inside the package, containing the version string as well as more detailed information (for example the major, minor, and bugfix version numbers, a ``release`` flag indicating whether the current version is a stable or developer version, and so on. """ if packagename is not None: warnings.warn('The packagename argument to generate_version_py has ' 'been deprecated and will be removed in future. Specify ' 'the package name in setup.cfg instead', AstropyDeprecationWarning) if version is not None: warnings.warn('The version argument to generate_version_py has ' 'been deprecated and will be removed in future. Specify ' 'the version number in setup.cfg instead', AstropyDeprecationWarning) if release is not None: warnings.warn('The release argument to generate_version_py has ' 'been deprecated and will be removed in future. We now ' 'use the presence of the "dev" string in the version to ' 'determine whether this is a release', AstropyDeprecationWarning) # We use ConfigParser instead of read_configuration here because the latter # only reads in keys recognized by setuptools, but we need to access # package_name below. conf = ConfigParser() conf.read('setup.cfg') if conf.has_option('metadata', 'name'): packagename = conf.get('metadata', 'name') elif conf.has_option('metadata', 'package_name'): # The package-template used package_name instead of name for a while warnings.warn('Specifying the package name using the "package_name" ' 'option in setup.cfg is deprecated - use the "name" ' 'option instead.', AstropyDeprecationWarning) packagename = conf.get('metadata', 'package_name') elif packagename is not None: # deprecated pass else: sys.stderr.write('ERROR: Could not read package name from setup.cfg\n') sys.exit(1) if conf.has_option('metadata', 'version'): version = conf.get('metadata', 'version') add_git_devstr = True elif version is not None: # deprecated add_git_devstr = False else: sys.stderr.write('ERROR: Could not read package version from setup.cfg\n') sys.exit(1) if release is None: release = 'dev' not in version if not release and add_git_devstr: version += get_git_devstr(False) if uses_git is None: uses_git = not release # In some cases, packages have a - but this is a _ in the module. Since we # are only interested in the module here, we replace - by _ packagename = packagename.replace('-', '_') try: version_module = get_pkg_version_module(packagename) try: last_generated_version = version_module._last_generated_version except AttributeError: last_generated_version = version_module.version try: last_githash = version_module._last_githash except AttributeError: last_githash = version_module.githash current_release = version_module.release current_debug = version_module.debug except ImportError: version_module = None last_generated_version = None last_githash = None current_release = None current_debug = None if release is None: # Keep whatever the current value is, if it exists release = bool(current_release) if debug is None: # Likewise, keep whatever the current value is, if it exists debug = bool(current_debug) package_srcdir = os.path.join(srcdir, *packagename.split('.')) version_py = os.path.join(package_srcdir, 'version.py') if (last_generated_version != version or current_release != release or current_debug != debug): if '-q' not in sys.argv and '--quiet' not in sys.argv: log.set_threshold(log.INFO) if is_distutils_display_option(): # Always silence unnecessary log messages when display options are # being used log.set_threshold(log.WARN) log.info('Freezing version number to {0}'.format(version_py)) with open(version_py, 'w') as f: # This overwrites the actual version.py f.write(_get_version_py_str(packagename, version, last_githash, release, debug, uses_git=uses_git)) return version def get_pkg_version_module(packagename, fromlist=None): """Returns 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. """ version = import_file(os.path.join(packagename, 'version.py'), name='version') if fromlist: return tuple(getattr(version, member) for member in fromlist) else: return version ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/astropy_helpers/astropy_helpers.egg-info/0000755000076500000240000000000000000000000023611 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500357.0 specutils-0.7/astropy_helpers/astropy_helpers.egg-info/PKG-INFO0000644000076500000240000001056700000000000024717 0ustar00erikstaff00000000000000Metadata-Version: 1.2 Name: astropy-helpers Version: 3.0.2 Summary: Utilities for building and installing Astropy, Astropy affiliated packages, and their respective documentation. Home-page: https://github.com/astropy/astropy-helpers Author: The Astropy Developers Author-email: astropy.team@gmail.com License: BSD Description: astropy-helpers =============== * Stable versions: https://pypi.org/project/astropy-helpers/ * Development version, issue tracker: https://github.com/astropy/astropy-helpers .. warning:: Please note that version ``v3.0`` and later of ``astropy-helpers`` does require Python 3.5 or later. If you wish to maintain Python 2 support for your package that uses ``astropy-helpers``, then do not upgrade the helpers to ``v3.0+``. We will still provide Python 2.7 compatible releases on the ``v2.0.x`` branch during the lifetime of the ``astropy`` core package LTS of ``v2.0.x``. This project provides a Python package, ``astropy_helpers``, which includes many build, installation, and documentation-related tools used by the Astropy project, but packaged separately for use by other projects that wish to leverage this work. The motivation behind this package and details of its implementation are in the accepted `Astropy Proposal for Enhancement (APE) 4 `_. The ``astropy_helpers.extern`` sub-module includes modules developed elsewhere that are bundled here for convenience. At the moment, this consists of the following two sphinx extensions: * `numpydoc `_, a Sphinx extension developed as part of the Numpy project. This is used to parse docstrings in Numpy format * `sphinx-automodapi `_, a Sphinx extension developed as part of the Astropy project. This used to be developed directly in ``astropy-helpers`` but is now a standalone package. Issues with these sub-modules should be reported in their respective repositories, and we will regularly update the bundled versions to reflect the latest released versions. ``astropy_helpers`` includes a special "bootstrap" module called ``ah_bootstrap.py`` which is intended to be used by a project's setup.py in order to ensure that the ``astropy_helpers`` package is available for build/installation. This is similar to the ``ez_setup.py`` module that is shipped with some projects to bootstrap `setuptools `_. As described in APE4, the version numbers for ``astropy_helpers`` follow the corresponding major/minor version of the `astropy core package `_, but with an independent sequence of micro (bugfix) version numbers. Hence, the initial release is 0.4, in parallel with Astropy v0.4, which will be the first version of Astropy to use ``astropy-helpers``. For examples of how to implement ``astropy-helpers`` in a project, see the ``setup.py`` and ``setup.cfg`` files of the `Affiliated package template `_. .. image:: https://travis-ci.org/astropy/astropy-helpers.svg :target: https://travis-ci.org/astropy/astropy-helpers .. image:: https://coveralls.io/repos/astropy/astropy-helpers/badge.svg :target: https://coveralls.io/r/astropy/astropy-helpers Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Framework :: Setuptools Plugin Classifier: Framework :: Sphinx :: Extension Classifier: Framework :: Sphinx :: Theme Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Archiving :: Packaging Requires-Python: >=3.5 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500357.0 specutils-0.7/astropy_helpers/astropy_helpers.egg-info/SOURCES.txt0000644000076500000240000000601600000000000025500 0ustar00erikstaff00000000000000CHANGES.rst LICENSE.rst MANIFEST.in README.rst ah_bootstrap.py setup.cfg setup.py astropy_helpers/__init__.py astropy_helpers/conftest.py astropy_helpers/distutils_helpers.py astropy_helpers/git_helpers.py astropy_helpers/openmp_helpers.py astropy_helpers/setup_helpers.py astropy_helpers/test_helpers.py astropy_helpers/utils.py astropy_helpers/version.py astropy_helpers/version_helpers.py astropy_helpers.egg-info/PKG-INFO astropy_helpers.egg-info/SOURCES.txt astropy_helpers.egg-info/dependency_links.txt astropy_helpers.egg-info/not-zip-safe astropy_helpers.egg-info/top_level.txt astropy_helpers/commands/__init__.py astropy_helpers/commands/_dummy.py astropy_helpers/commands/build_ext.py astropy_helpers/commands/build_sphinx.py astropy_helpers/commands/setup_package.py astropy_helpers/commands/test.py astropy_helpers/commands/src/compiler.c astropy_helpers/extern/__init__.py astropy_helpers/extern/setup_package.py astropy_helpers/extern/automodapi/__init__.py astropy_helpers/extern/automodapi/autodoc_enhancements.py astropy_helpers/extern/automodapi/automodapi.py astropy_helpers/extern/automodapi/automodsumm.py astropy_helpers/extern/automodapi/smart_resolver.py astropy_helpers/extern/automodapi/utils.py astropy_helpers/extern/automodapi/templates/autosummary_core/base.rst astropy_helpers/extern/automodapi/templates/autosummary_core/class.rst astropy_helpers/extern/automodapi/templates/autosummary_core/module.rst astropy_helpers/extern/numpydoc/__init__.py astropy_helpers/extern/numpydoc/docscrape.py astropy_helpers/extern/numpydoc/docscrape_sphinx.py astropy_helpers/extern/numpydoc/numpydoc.py astropy_helpers/extern/numpydoc/templates/numpydoc_docstring.rst astropy_helpers/sphinx/__init__.py astropy_helpers/sphinx/conf.py astropy_helpers/sphinx/setup_package.py astropy_helpers/sphinx/ext/__init__.py astropy_helpers/sphinx/ext/changelog_links.py astropy_helpers/sphinx/ext/doctest.py astropy_helpers/sphinx/ext/edit_on_github.py astropy_helpers/sphinx/ext/tocdepthfix.py astropy_helpers/sphinx/ext/tests/__init__.py astropy_helpers/sphinx/local/python3_local_links.inv astropy_helpers/sphinx/themes/bootstrap-astropy/globaltoc.html astropy_helpers/sphinx/themes/bootstrap-astropy/layout.html astropy_helpers/sphinx/themes/bootstrap-astropy/localtoc.html astropy_helpers/sphinx/themes/bootstrap-astropy/searchbox.html astropy_helpers/sphinx/themes/bootstrap-astropy/theme.conf astropy_helpers/sphinx/themes/bootstrap-astropy/static/astropy_linkout.svg astropy_helpers/sphinx/themes/bootstrap-astropy/static/astropy_linkout_20.png astropy_helpers/sphinx/themes/bootstrap-astropy/static/astropy_logo.ico astropy_helpers/sphinx/themes/bootstrap-astropy/static/astropy_logo.svg astropy_helpers/sphinx/themes/bootstrap-astropy/static/astropy_logo_32.png astropy_helpers/sphinx/themes/bootstrap-astropy/static/bootstrap-astropy.css astropy_helpers/sphinx/themes/bootstrap-astropy/static/copybutton.js astropy_helpers/sphinx/themes/bootstrap-astropy/static/sidebar.js licenses/LICENSE_ASTROSCRAPPY.rst licenses/LICENSE_COPYBUTTON.rst licenses/LICENSE_NUMPYDOC.rst././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500357.0 specutils-0.7/astropy_helpers/astropy_helpers.egg-info/dependency_links.txt0000644000076500000240000000000100000000000027657 0ustar00erikstaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500357.0 specutils-0.7/astropy_helpers/astropy_helpers.egg-info/not-zip-safe0000644000076500000240000000000100000000000026037 0ustar00erikstaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500357.0 specutils-0.7/astropy_helpers/astropy_helpers.egg-info/top_level.txt0000644000076500000240000000002000000000000026333 0ustar00erikstaff00000000000000astropy_helpers ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/astropy_helpers/licenses/0000755000076500000240000000000000000000000020501 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537500356.0 specutils-0.7/astropy_helpers/licenses/LICENSE_ASTROSCRAPPY.rst0000644000076500000240000000315400000000000024332 0ustar00erikstaff00000000000000# The OpenMP helpers include code heavily adapted from astroscrappy, released # under the following license: # # Copyright (c) 2015, Curtis McCully # 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 binary form must reproduce the above copyright notice, this # list of conditions and the following disclaimer in the documentation and/or # other materials provided with the distribution. # * Neither the name of the Astropy Team nor the names of its contributors may be # used to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/docs/0000755000076500000240000000000000000000000014401 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537503308.0 specutils-0.7/docs/Makefile0000644000076500000240000001074500000000000016050 0ustar00erikstaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest #This is needed with git because git doesn't create a dir if it's empty $(shell [ -d "_static" ] || mkdir -p _static) help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf $(BUILDDIR) -rm -rf api -rm -rf generated html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Astropy.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Astropy.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Astropy" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Astropy" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: @echo "Run 'python setup.py test' in the root directory to run doctests " \ @echo "in the documentation." ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/docs/_static/0000755000076500000240000000000000000000000016027 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/_static/logo_icon.png0000644000076500000240000002162300000000000020511 0ustar00erikstaff00000000000000‰PNG  IHDR`_´Â@sRGB®Îé pHYs × ×B(›xYiTXtXML:com.adobe.xmp 1 LÂ'Y!ÓIDATxí] ]e•þïëÎ! aI „I [Pã‚bY…€…Q™:Áq†Yœ®©™‘TÐÂAQ) ÷AÊQQÂNB6ÙÈFºûÝ9ßwÎùÿÿ¾î×Kºµj®ÿü÷,ßùÎùß»ï¾{oÇþû“v ø“fßÁÉ/¾õÖ]ÚÛG)[;w«okí(ÃÛõ"”† ߥ¼ýâs×ïàô}‚ÿ‹_€ þùÎý;Šâˆê3ZZjS˲˜QáÀPc¤à ٠¡]ô#IQlÕ«"_-ŠrA(‹gêCZuÇEg/„çÎÚHkg%Œ<çµ}e|Kky¢4ô}!ÔD“«v)eá¥]7i:`¦F8bT¿Z±´(Ã/CQûyÑ1ì'·]2{ ;hÐÌ;|°`çþËmû†²åŒ² Ÿ”.'ýB÷¬±Ö8$£&Xáb2Ÿg:†ˆ-J`rOc‹âMYĈö[·\xæ¯ÅXÒyÍ>ˆ€ƒuF[ÛÐѵÝg Þ¹Ò„÷JGZ´Á¢A£­Qþ.¯Ø¬ùluî's_µiùŽ¡’NÑÏu’ï)‰ýÂÍžùÀ`Õe0˜ˆÄúTÛ­{µ–¹!” ½ñÚK4…­«HÐçBTlÚÀh£•™ûQ…ønlš‘€±œ6ÅïkeqÝM~âá–ÊpË>PØÓvã>­aÈÕeΓ¦Œ`å¬Y ×f(]oZöîd“À ¢C  "TdwºØô,¶’ÏðD÷–zÇ箿àoÖ ¤r­h Œ=ë²›F Yûl(kWKÍ£¤C±i,½“ÿá¥6¥\iŠÙ»Õ¹2-Ѱ:Ý-Œøú¹¬ø%.ËÅõüçÎþ÷ímƒV³½Ñˆ;ãŒû[FM_úé²V´I¹û ?Ú`—F Š6Õy£µ)¢ã+—ùœ¨l4‘LBÅG› ºd? Ž2bɳØnºáïþú*ë÷—4q¶379ÎO)ËòBø=ÈÛØŒn ×°‘Í`{Ìî±Ä]ÕF'Ë—ò²ù:D.wÒ"tá,Iåõ÷GÕϺmvÿN[•ÑNê~[[[ë’ÎÝ. µâ¥üáZ(°ÊTx*˜$ÙYóCÁÚD"¼‚AWê3¿èà ºoaŠáRù4Ñ!“&¨.tex4´;ýú Néó÷‚±A;v›Óvó´¢^~[~àí…FÉ‚ª«Mé5¬î¹M¼¹›dµÁRä?uË%ô¨=ÅVòE'‘K™ó•Ëâ·[G¶ ¯Ÿ„ÚŽm»¢ŸÓvËiE=<®Íg­(7î\çújÁا–!ô£éÜ¡ÍÛF·¤swq\sõD@7TŒH6æÑdDUó¬]7»ÿŒûïoAºÞ¶ÖÞbÇíˆéKÿEÞùW€=ùɘÈzqÊã…Áu.Ejµ–P/ëƒXÃ9Ã1$JÅPoK`6äñ|* ŽvE1,š C‚,Œ\}Xe(O9xÓð;Åð„ô´í°OÀ¹Ÿÿ×½w›¶ä?åê㕨DÉZ!ƼZ¸Ðô¢¤µ©Ž˜íÇ«øBë3»¤Õ@Ò¦ú¡‚“c˜G#é4 £è Ã¥ÖQžõ78{ÚvÈœÝvýIJuˆ\; 'æA$¾4ú%MUq¬\ki '{d·çîŒñ‚­GŠ#cÊéù r,Z1È‹É="é\mÀ¾€žë:—Š«ù¤¹_n»÷‰êßý8è €/ÛZgËcBz*S²V ®’u›’Öí€û)Df8!ŒÜu—0ã ýÑOD™ìm®âTmÐk¬ËØ`(ÌutQ?Ïä1î6Ò-7!¾ä92v· ê̹îæcÊÎW ÷g2!ÏÌä¤Ä¬B5+cÝ&V+Øeˆ£§OaÜ¡“ï1*eD°¹sOç2Vmt„+Lj,4¹.qp Z1(ªc¨¢¢s`Á8é ß~ð<Åî:ÚœsÝ'È×Ï/$á^Õ‚AžŒ³âP8½¨ƒY W]´±¡;´µ5qðp ï¿o6tHÄU|Ã3\ÇðF30Ú°*="Ñ 7qR7—Ð*§„Á™úG\ÑY¾²(ÚÚîyd¸9TÄ ,Àœën=\èá2íHmƒ“Õ=« l¼ÀPM í´Üè6ÖZHó Ç¥W«|`ÄHOc±^¸&¤SÌ 6Õ€aT9»¡3N̨ÊXMÄäuˆÌbÄ}BçЭr…·ë6à8çÚåŽTçC=ÆI£1,#‰€é¢L«Š£…6ÚÒá(Øf4AFLõšNŠK+]MÃxÎÅÁ¥*5Ö›93“Ûà)Q ÜòºQÀh«•åçÛîz`Wª³a@ p^Û ã%®‹ï뤑ы2JJB ™M¢œ¬É¸X ˜Ùv6̃n‡â‹Sñ³ÖTb’„àÈçê<+º,/31Lc=“%6H·)ÄD|1ÑI•û„Cºœ–n÷ÌmkÛµÞQ{PLÊ›å9óâ¬C(½BZy1"’ŽÅ‘ºÚŽšzPÀa'ßö=2ŒÝcws7 :X›b#¡]\B¥1´1*Ó‰TwH™ñ¥Rý»êÏ1Tf~ÈÑRœc©¢ØîØÖ1â«Bl&K°ÏdFV©8­Qç¤##ÍiÏ~"k›ð0dMs ÝeÒ ȧ2QHœ ²¡†é`±–hâ¸5]”ÌŠAýËò]m÷=4-æ’Év-Àœko¼H¹šš²¡*˜KÔa~JGyD²Ž‘Hc6j·]õ 7gmsü.ÀV-œÕÆ Šª:àé ÒóÉŠ“ãR#QmGÄß2¡ ý^€s¯¹åxIr âµpfÀ!]ç¤U­>ôh,\ÌM1<ïœ6Y®^›žÒ0eÂ893¢Šˆ› N…w¯óFújÍÅXAvKp£GÌ—s³¨Š-(VQ|4UÐÏOÀ™W}qLY«ß/Cœ´Ò$‘43d:N1ÈØLÆÅR¥™à#HtÅá‡ýÐÖ8à{aò„}ˆ§î` 3‹ué\s[¢BçÈIÃþHšá9†ª46â»cܦ^`Mk»÷¡±Ðbë×'`ØÖÛ%f?o ¤çtbd«¹Å&¾T"a" M~ô¡gØk̨0qŸ½±Ót›qÀDX}tÇv…©³‰úËè\`s çìœ ÍªÒðÏ1TŠ‘/:¾DÇ¢-Ã[x'„û¼rÜ?¹ AN£ŒNFÚ²¢ 1™«ÚrbpW%˜‰úh9ühÝoÓÚO U\/Ø%Ìô¨p6<Ñ8nO¹Ì]3èó:Ô/¯GqÅ+M̉‘LƺTK<å`DJ½ sŸ`î•×–ŸÓw!@s¼ˆ:­Ò|Ô™*„`lisWkÚÿ÷Îi“ÒÓ°ç¨Ýätt”bX¤u€{bP›äÕ­yª{ÿQÓÃá“ä ÜDg\\j à ˆ “tn³XKýmå1^SŸ`ÛÐÚõ;!­8•ëªdŦ¯ ÁÉ*F•¬7aìžaß=ñ|mïÛôäS@(OŠ]»$Šu-ç¥T„Žö­aÞã̹ö¦÷ ʇ=Ø¥!K¸¤Ve$Fi£1Ùbá´š«ú(O™ËkÒ~û„=äØÞ×§£ã\ R£#V¦ÛkÔˆ°ÿØ=¢iÂÞcÂäñzÖå&S§¨ ˆË ÒUÎi±A^¹Mæ²9nQ¶på{X¹‹S„½(†¨S¥Nt¤D•6‡ˆ:ÚÄ« *(ûòåKÌl˜vÀx$±¤!*YÙ~Ê‚Ð#åݯY±§ÛûŽ<„ÜsŒ'Z×E OÍK|d&Î݆P5B–EÉ+£M@ÞýGý¶v ¦rjâÄ¡¡pºUuZµè ×x±8œÎ<ø Dõk›>±ëýKHIÊWŽÄ™OÃ6yß½Ãr€X¥Ç‘ûtKêt'«ÇbÜÒ‹4YµaHÝtäqü‹ÉD/•ÜQ…¦ˆ6™`‹I°o¤áO“é¸+C”ê7MÎhFîÚíÍ#Æ7pÈ»»<ÛË Û,MÜDF8Ü$5ùøžÃ§-ñæ)T¦»ë\Z™ì–æõ‹»8¹N&,²ÛøTÛM‡És¦LÖFNÅŽ/•`”ÈÒ™vsWÕåd©óp1lÏá‡aÚD9 y‘Ȧ‰’Ž6M6sŠÞ¶öØ\N•³¡}ùäB3¤ÆÆ:¬™M§ÌK\Çк÷ø ¨×‹K)Q'†4e$F›3!q’qÕf#ªê|†´¶„#&§³Âöc˜vÀ¾–"åaMëR>öE-Á;j݃Ãÿ݇ɯð¬z’.ÁTÉžØÜu*’z¡X)¦ÂFØ»|æ´Ý&ÝŸd¸P!°ª´F½Z‰hâjá)±cxqN ‡Kóq³}{·IûŽ C->á-qÂÞäýöîõ0w¨,æžr–T­Ã¡ëAËÓéŨIíµz±\º,@è¬TåødÁ.¹«ÁÞ4UYj_, JŒÛlWu†a®âFE´½ãIto6,_»!ttv63Ë]³Z˜2~¬À*®J™ó•t3ùXKSp ü݇¦ïFWjÓ(cb°,‘C›8ÀK΂Ö"Q—(j¥¼ûéEGLYAƒN}4'ñ‚ #HG}õí㤉'*hìPV‹ê¼ ­SÓù»®Ú`·Ñ³K3"Y!6S;Áw@³Mþ #<=ixqÉÍ\¨ÇhŽ›¸*÷²@ñ&NHr ·V„Y3’’¬“]jThæ%¤®N5Êu„*BK¨±½Ä`DÊ¡õ ŠvÁ5N((ñ,Ây&gæÔÉaŒ#åÛÜi¥r’³òñ1b¸k~ýô aÁ²½^zXðúʰaóæðââ7©Çå]ä#GÈéåȰjÃÆØ'pö:ŽœÜüì§ ˜(Þ9E‡ÜeXV£ÑP(ÓmÚº-,^½!¼üújyR›Ó¢\sùé'¬„¥º¡ø®NŽÌx¤Š3N:!|ð¸æ @D¶1Ò°»öHÓû¾ŽõÇW£aÅú aÝ[›âšmSå ²jÃ+ä oþˆáCÃ!âͨfáý9d:Ñ.ÒU,=ï- ÷ꊵá‡?:êö8½…”¡ö¼GÇCžå廹š2qÒX<_ᣦNÔæƒÄòËóã˜Õô¾/|:;ëaÞ‚¥ B./.íý0”êÀ»‡á0¹yƒ+Ÿ;k›4npÂ4\îÐüd/Sù‚Á9D6£_2SlcàKÂäÁ³ŽÀÕÂÁßôaÛæ¸/IÃ7¿½MH.¼´dysg±´Ï^aèVñV8c>³›K= ‚ñð‰ãŒ¶0ðÞ–á9‡Ž P/[߯|ÝQI«£DÊk”ŸxôN’8ü€½ò/Â|ùÎèOE³ §£¼ª©,|Œ<Þ2Qn=îì ‡=vÒ¸ ˆzQüÚyÄ(Bý¸ÜQW ±V¸D._Óç?þsüËmíá¹ÅËbóQǶŽÎ°hy/§£¼¹âo&y¸Wn¼°fÔ?€ÕomNï|! ߯kßþ㬧%.€™ðïìÈ/—é]Õ/~ÿLï{¾ÄñÜâå’§êíexgXÄï½í‰pÞ/Ó%lØ”óWœ~ÂUè¨ßõ — L­ŽˆpL‘ftq9ÐÏ“°-›R¹|ÍúðÌ‚%–¨wñÔü% íùÂ,,èå¥+zÁWSåÑÅQ½ÜàyfÑë¨%ˆ'*MçÍð^xóÕÕüÉS¢cŒ¢ÁGUµû sê-øîàe©]2Œ¡ ¶ˆ“91űBbhœ„‡~÷ŒüÑ þ@o$“ïoØ´9,\¾šy‰¦UÅ|ÐÍ— s½Ž~øØCsØ.óõrÈY²J®‹‘¢7i³|ê:—l™ðʰeÎW.‹zk¨ÿ sâ” P+küKB¤ltÐÝ´0ªÄèÄ\Bå42™áÿµøȯÚ^6¼û}ó‚U¦¢pŠºxÅwëVöô€y‹Þˆ‹êµÆ^æõ°9ÐPOÞèÔÎ0 @e~qÉ©Ç/ƒ&߸¡VîÁ–Y’jÁï@™l€1p“êfÍw¢tK:ø<üD§íPåùRáȪy!_’ïlóä╼•üÄYñátê®:ÐÉ1"g÷§¬}Iqª£.@)^ENb£µpÑñe6:g:·yÓM*#­ XØk6„y¯6ÿ¬\ÿfx ‡ß²f"_~ݳ_rùº·xÕÔk5²ªÔá»iL]D—q"‰*ç—7=yìÏ©o¸r^Nì D#z¬àª9”1"“Îb#'fÔIö¡ßÏkú]à‡ŸÊf!™ã®ÚðVX/§£Û³Í“/_b¿ÍtŠzLW‘^gæ8îªÍ{¡*÷«Ý–ÿøb2ôj[Ý„¸ ªƒ8(ɪÒù‰«ù‹4®"†b1’®†–Ë)é¼Wñ+±ë†/Ž‹ƒ›áºÎåËò+¶¿Nž]²Â!c>2ôuâ\Pª÷Â%‹RCƒ-Æ®ÙZîmÆÏ ØêL¼(†[ÁÀOļÑ`cítI"š¸jƒ¯b¸DäÃOtý¼¶jmXý&°¦h¬e’(Uñ¶ç0´X~t½Å_ôŽØj0†Î%gt÷ñws' %ÞÛN=ºéGTAµâ-PɃ5Áy¤Ä)Ÿ‘6&äã…D©xê’H/_÷f—OÁS~†dxšGó2SCÁн*æz:e! ƒù&üjÚÙ¤óº¤|aHõ)è˜6Îm±åΆԕ]û”‹ÒÇ š$ƒ«êhÆ_˜T*FŠJ6¢c”–"<ü¿ÏÅïxŠªä‰kaH†˜:Ù¶uÔÃ’•|È€û½ X¬ðåMHÅ%R¥1«R³DЃÆ2ŸØƒR”òG-×´xbÎtj-úó8‚0 r0D¦`N–W¾€N:ºtñ•ŠU„•ò¨É³ õôøUùaµa“ ¹`/œ°èýúó=ðŠÜ«Ýº­¨€&¾JÀ)®ËhWp®Ôã £±æðÔ¦'Žýz¸µz}A^°s;/Øx!½Á¹-‘6gº$bîO´Š@üð“ÏòSð¿|¡­ÒÀAlúÉ,”¯,ëùþ€‘¦Àá'6¸Rc5¯â{¾˜Tùq7·ÉmT½¬•ŸmvæCG¸_ûüë$| r£ô'©„Äà g'R g´ÉLw¢ô‚}a`pÝJ¹²‰ oÏÊ0œðŠºŠ>ÊEưJ¾¸×oÔ+™Øo¶áÿÊr‰[V‡fËtncž˜ !Ü”¦nœËpÇ'Ïúzö<Úwþ‡# &7Ùup•„Ä4™U›!©2’Î&qê¤ôØÂyœCSJ _*#¯Š;òŸlŽñÊë½ ^xmE¨×åZTCŠem­ØœÞ0ÆÄòF !ë*ñX¸ë–-×À³/[\ùÂx”é™™4qEçYLjវ첦 }fS¸T8ð}<\׉ÑbqäÂÇEˆáˆìËÌ“KÏVóÎ1*á©£qpN Ò $H#8ÊÊ–s/œ}¢žG#´—-.@Q¯ýÒIq„fΔ8‘Õ´yL¤ÂJuVš6ù‰OÔ…Ð!ßêïîZ¿6¡ç±²_½tîoZ~5Ïb2PcmªÓ]-‚¢p©d:µží$Ê´T°sˆú™ÄC¹¸ËÕ¸Í[¤÷z™É¨H9ÔÆÑà * ÈT‡FX\Q^zÅiÇ?ЍíÙ* Iäxy@/Ø¥2!¼”„ÚTgµe¤V*„„ÙÑ*IS€€g‘©æ‹:·1uÊ¿ÀÏó™@‡çíÆc¸83¸wçRµEƒ;H«Âõ—xÖ—³4ýžV@·µ×ï!V¼È'ì°uS°·‡fe ò UŒXààIáùdG_Y>õ£³GE.°Åb5†pÏwý¦·+u‹K.L“ð\§’Æ„Êo]zÊqW+§í»,Àׯ:ƒü3ó·X:ïN¥N¬Z¸GˆäT¼T•HG\Ôü-FÕnƒ^AüM »ªK6pÛ«raÏæø–¾|OSYf݉ùEËa<™Á!ütÓˆ­ø÷3äûw`[—\ç°Í_–âø³Ri8Y#e¤½àÈ+M´Ÿ2zãÐ, ƒ”_*±cV•êØMóaИèÏœ²cä[WGí–&â<æ~(0ãb»Äu •™XÞáÁaµµ—«œòËqà[· ðÕ /Ü( ¯ñæ91¤kÔE›ö¥Kq¤(NÚ—D_•Š‘°a袋 ZkhLa Drº`¹ž áp´¿²eS&Åžé =ŸÆÒ&Îôp©{ßµOëéŸ;ùäA{ˆ´Û³/ýýœñse­Ä¼`•9Y– C" ]¬ªÎñ¼.UÏ oMTaRi†î°OÌÌ}΢n¾-Àsöð–¿qD4—š£½\FƒèEÊ;6=yÜ9ç}4ž7´­éHÒ²^k+©×ç¤(T GlÚQªPasE‰æ® úb¬ª8“ÿ¹Î%ô›dŠÁlõ››åÏ™6Ê•Où$¨“á‹•/•ÜaÌr›Ì‘ÇùòZ¹ÄpQ_./3¨ƒfé!àâ¯Ü{¦ÜÚù®rlR8Qr›ÁJáj‚”_*Ñ”Fî3…“•JiÔ©‹ÙNt {a¿=G‡eòè ‘L[­JÝÓØh+6ÊyùyWœrÜ÷{hÑ€LLÕÂ%_ùö? ‘ë”#È"B™è4“y!tƒ ¾ã±^0e´q›¡>ë*e´…Î}ºÁÈx*%ø$Œ„Et­«/Ëß0~ìòSŽy1;jS¶½¡ËÿÄ%w~÷na6'‘e JñjÐæe3m] öE@¬†C%è —3ÙqÉü2Ú1º]@â‹7]s "°Ñ>Ð9|ÈYW}ðè ½µf öæß9²7Ù4W ~ت¶f$ÒÚ+Te:tG•ªãnµp·«§Ûàžp-Œ ™üèå ÒÐ|#_î„Ì™N§ÈûfQ–Ÿ¹ì”w}dg4,ú¶âøõóÏoµ÷Såß8¸/Ž:´(çvygEo\^8‘"›"!b¶Ð;n’š—6É¡>¦S'Q+Ïg~š¡bׇê¡óðËO;þ.©eÀ?°H©ƒWÑWs‘ÃÑe_ûÞRÜe]šÁ‚ů»Â»Ñis”BZ@ì[Ó4À©NwÍF"X>Í«¹ƒéÐj@T$ˆ]'u.½òÔîÇÎÞúü ˆÄäÝqóŸ¼\è_ ì·z㪅k“P0^¬U*Mg]A…I©ø+q€G›ºÉŽÆºÔ=‹Ž¸ÆF±©Ó6™ÜÞÙÑrðŸªù±,L¶g»âÎïM/[kß’£µå…{¡¹ÔÊÑmd¦ Ø´ËQ²åðã+—ë*s;àà“òÆÏäïÿ¡ñÏ…¶§Ñ €ÒÖöHë–ñ«¯’"¯“â†66Cû›7…íEgÐ Ê|uYÓñ鯎Ö`ÀPa¸2çK¥âxëG¢¹éÊüÕ“(yPC…Ïàl×Üý“ý;;;.“bÏ—f ³ Y3²¦KZmšèÀ"_D‘Y.jæWmºcäùP›<÷Z„{†tÖn½ìc',†æÏi³ªÒUwÿp’üƒ+×É¿ˆó é•þ7,²yó|è¢]7[j¾†£±¢Óà(»Õ…÷·)ç ÷ÕZŠïË;ž^•ƒ‡4è àÔÚîùñîòWŸ³åÞÂ9Ò­ã¥{ MO‹ MDƒéEÙŽí¯,’cp±Ú%æq¹höƒÖŽòþ+fŸ(÷ ÿü·¶yé×~óÇòO¶œ$íz¼ãß+6þŸ@09ßÕ Í÷Åʤ›æ¿-X×GåY“G†nnìò³?´)Ïû—0ß) ÐØˆ¶ïþl’Ü'™^ÖZ&·„r’, þÛKš)ÿ´H1\Öd‘øwuÖË{x’j]Q+Ö†zÀCÄ/È´=?9¬Z8[¥iÄþKÛÿ?ýófuWŽIEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/_static/logo_icon_32x32.png0000644000076500000240000001023600000000000021350 0ustar00erikstaff00000000000000‰PNG  IHDR szzôbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEã ))cÛŸ¾+IDATX  ßïfˆ™ÿý& àå Ó«VVýf€™ ý÷gj$ïŒ €gMö_~C£ÎþýGgZìb}Œxº2 º€ªª_}‹n‡!1û÷t_}‹V jVK.$ ñõö Úâçèïñ'dG8ÏßçºÒßÛ*úûü Øãçàèí'ûüýöùúûýþÕ"ž²¼ µ¿#oVIÛãéøüüÿ/"Úåë¨ÃÏÀàèìçíðëòôîòõÜçìîõ øÀÉ×ÝÙãèáêîÝèí¨€AAüãÏÛáÕàåâëïìòõðöùûüÿ‹×øûýÝæìäìðÜçíyI9à¹ìóööùûÝéîäúyþ ú°õßêîãìñûþ^qQ1ð˶¶ú:IRCîô÷öúüþ_vL8³õ ø‹Ëìó÷÷^uK7¦šggûýú FÈÓ6tM9¥qQððï>làûÂWøÐ;0uL;Ô€¤¶ÿúüÙu^Gõ¿Þ+Ÿ$IEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/_static/specutils.css0000644000076500000240000000042200000000000020552 0ustar00erikstaff00000000000000@import url("bootstrap-astropy.css"); div.topbar a.brand { background: transparent url("logo_icon.png") no-repeat 8px 3px; background-image: url("logo_icon.png"), none; background-size: 32px 32px; } #logotext1 { color: #667C8A; } #logotext2 { color: #F09156; } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/docs/_templates/0000755000076500000240000000000000000000000016536 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/docs/_templates/autosummary/0000755000076500000240000000000000000000000021124 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/docs/_templates/autosummary/base.rst0000644000076500000240000000037200000000000022572 0ustar00erikstaff00000000000000{% extends "autosummary_core/base.rst" %} {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/docs/_templates/autosummary/class.rst0000644000076500000240000000037300000000000022766 0ustar00erikstaff00000000000000{% extends "autosummary_core/class.rst" %} {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/docs/_templates/autosummary/module.rst0000644000076500000240000000037400000000000023147 0ustar00erikstaff00000000000000{% extends "autosummary_core/module.rst" %} {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132249.0 specutils-0.7/docs/analysis.rst0000644000076500000240000002341600000000000016764 0ustar00erikstaff00000000000000.. currentmodule:: specutils.analysis ======== Analysis ======== The specutils package comes with a set of tools for doing common analysis tasks on astronomical spectra. Some examples of applying these tools are described below. The basic spectrum shown here is used in the examples in the sub-sections below - a gaussian-profile line with flux of 5 GHz Jy. See :doc:`spectrum1d` for more on creating spectra: .. plot:: :include-source: true :context: >>> import numpy as np >>> from astropy import units as u >>> from astropy.nddata import StdDevUncertainty >>> from astropy.modeling import models >>> from specutils import Spectrum1D, SpectralRegion >>> np.random.seed(42) >>> spectral_axis = np.linspace(0., 10., 200) * u.GHz >>> spectral_model = models.Gaussian1D(amplitude=5*(2*np.pi*0.8**2)**-0.5*u.Jy, mean=5*u.GHz, stddev=0.8*u.GHz) >>> flux = spectral_model(spectral_axis) >>> flux += np.random.normal(0., 0.05, spectral_axis.shape) * u.Jy >>> uncertainty = StdDevUncertainty(0.2*np.ones(flux.shape)*u.Jy) >>> noisy_gaussian = Spectrum1D(spectral_axis=spectral_axis, flux=flux, uncertainty=uncertainty) >>> import matplotlib.pyplot as plt #doctest:+SKIP >>> plt.step(noisy_gaussian.spectral_axis, noisy_gaussian.flux) #doctest:+SKIP SNR --- The signal-to-noise ratio of a spectrum is often a valuable quantity for evaluating the quality of a spectrum. The `specutils.analysis.snr` function performs this task, either on the spectrum as a whole, or sub-regions of a spectrum: .. code-block:: python >>> from specutils.analysis import snr >>> snr(noisy_gaussian) # doctest:+FLOAT_CMP >>> snr(noisy_gaussian, SpectralRegion(4*u.GHz, 6*u.GHz)) # doctest:+FLOAT_CMP A second method to calculate SNR does not require the uncertainty defined on the `~specutils.Spectrum1D` object. This computes the signal to noise ratio DER_SNR following the definition set forth by the Spectral Container Working Group of ST-ECF, MAST and CADC. This is based on the code at http://www.stecf.org/software/ASTROsoft/DER_SNR/. .. code-block:: python >>> from specutils.analysis import snr_derived >>> snr_derived(noisy_gaussian) # doctest:+FLOAT_CMP >>> snr_derived(noisy_gaussian, SpectralRegion(4*u.GHz, 6*u.GHz)) # doctest:+FLOAT_CMP The conditions on the data for this implementation for it to be an unbiased estimator of the SNR are strict. In particular: * the noise is uncorrelated in wavelength bins spaced two pixels apart * for large wavelength regions, the signal over the scale of 5 or more pixels can be approximated by a straight line Line Flux Estimates ------------------- While line-fitting (see :doc:`fitting`) is a more thorough way to measure spectral line fluxes, direct measures of line flux are very useful for either quick-look settings or for spectra not amedable to fitting. The `specutils.analysis.line_flux` function addresses that use case. The closely related `specutils.analysis.equivalent_width` computes the equivalent width of a spectral feature, a flux measure that is normalized against the continuum of a spectrum. Both are demonstrated below: .. note:: The `specutils.analysis.line_flux` function assumes the spectrum has already been continuum-subtracted, while `specutils.analysis.equivalent_width` assumes the continuum is at a fixed, known level (defaulting to 1, meaning continuum-normalized). :ref:`specutils-continuum-fitting` describes how continuua can be generated to prepare a spectrum for use with these functions. .. code-block:: python >>> from specutils.analysis import line_flux >>> line_flux(noisy_gaussian).to(u.erg * u.cm**-2 * u.s**-1) # doctest:+FLOAT_CMP >>> line_flux(noisy_gaussian, SpectralRegion(3*u.GHz, 7*u.GHz)) # doctest:+FLOAT_CMP For the equivalen width, note the need to add a continuum level: .. code-block:: python >>> from specutils.analysis import equivalent_width >>> noisy_gaussian_with_continuum = noisy_gaussian + 1*u.Jy >>> equivalent_width(noisy_gaussian_with_continuum) # doctest:+FLOAT_CMP >>> equivalent_width(noisy_gaussian_with_continuum, regions=SpectralRegion(3*u.GHz, 7*u.GHz)) # doctest:+FLOAT_CMP Centroid -------- The `specutils.analysis.centroid` function provides a first-moment analysis to estimate the center of a spectral feature: .. code-block:: python >>> from specutils.analysis import centroid >>> centroid(noisy_gaussian, SpectralRegion(3*u.GHz, 7*u.GHz)) # doctest:+FLOAT_CMP While this example is "pre-subtracted", this function only performs well if the contiuum has already been subtracted, as for the other functions above and below. Line Widths ----------- There are several width statistics that are provided by the `specutils.analysis` submodule. The `~gaussian_sigma_width` function estimates the width of the spectrum by computing a second-moment-based approximation of the standard deviation. The `~gaussian_fwhm` function estimates the width of the spectrum at half max, again by computing an approximation of the standard deviation. Both of these functions assume that the spectrum is approximately gaussian. The function `~fwhm` provides an estimate of the full width of the spectrum at half max that does not assume the spectrum is gaussian. It locates the maximum, and then locates the value closest to half of the maximum on either side, and measures the distance between them. A function to calculate the full width at zero intensity (i.e. the width of a spectral feature at the continuum) is provided as `~fwzi`. Like the `~fwhm` calculation, it does not make assumptions about the shape of the feature and calculates the width by finding the points at either side of maximum that reach the continuum value. In this case, it assumes the provided spectrum has been continuum subtracted. Each of the width analysis functions are applied to this spectrum below: .. code-block:: python >>> from specutils.analysis import gaussian_sigma_width, gaussian_fwhm, fwhm, fwzi >>> gaussian_sigma_width(noisy_gaussian) # doctest: +FLOAT_CMP >>> gaussian_fwhm(noisy_gaussian) # doctest: +FLOAT_CMP >>> fwhm(noisy_gaussian) # doctest: +FLOAT_CMP >>> fwzi(noisy_gaussian) # doctest: +FLOAT_CMP Template Comparison ------------------- The ~`specutils.analysis.template_comparison.template_match` function takes an observed spectrum and ``n`` template spectra and returns the best template that matches the observed spectrum via chi-square minimization. If the redshift is known, the user can set that for the ``redshift`` parameter and then run the ~`specutils.analysis.template_comparison.template_match` function. This function will: 1. Match the resolution and wavelength spacing of the observed spectrum. 2. Compute the chi-square between the observed spectrum and each template. 3. Return the lowest chi-square and its corresponding template spectrum, normalized to the observed spectrum (and the index of the template spectrum if the list of templates is iterable). It also If the redshift is unknown, the user specifies a grid of redshift values in the form of an iterable object such as a list, tuple, or numpy array with the redshift values to use. As an example, a simple linear grid can be built with: .. code-block:: python >>> rs_values = np.arange(1., 3.25, 0.25) The ~`specutils.analysis.template_comparison.template_match` function will then: 1. Move each template to the first term in the redshift grid. 2. Run steps 1 and 2 of the case with known redshift. 3. Move to the next term in the redshift grid. 4. Run steps 1 and 2 of the case with known redshift. 5. Repeat the steps until the end of the grid is reached. 6. Return the best redshift, the lowest chi-square and its corresponding template spectrum, and a list with all chi2 values, one per template. The returned template spectrum corresponding to the lowest chi2 is redshifted and normalized to the observed spectrum (and the index of the template spectrum if the list of templates is iterable). When multiple templates are matched with a redshift grid, a list-of-lists is returned with the trial chi-square values computed for every combination redshift-template. The external list spans the range of templates in the collection/list, while each internal list contains all chi2 values for a given template. An example of how to do template matching with an unknown redshift is: .. code-block:: python >>> from specutils.analysis import template_comparison >>> spec_axis = np.linspace(0, 50, 50) * u.AA >>> observed_redshift = 2.0 >>> min_redshift = 1.0 >>> max_redshift = 3.0 >>> delta_redshift = .25 >>> resample_method = "flux_conserving" >>> rs_values = np.arange(min_redshift, max_redshift+delta_redshift, delta_redshift) >>> observed_spectrum = Spectrum1D(spectral_axis=spec_axis*(1+observed_redshift), flux=np.random.randn(50) * u.Jy, uncertainty=StdDevUncertainty(np.random.sample(50), unit='Jy')) >>> spectral_template = Spectrum1D(spectral_axis=spec_axis, flux=np.random.randn(50) * u.Jy, uncertainty=StdDevUncertainty(np.random.sample(50), unit='Jy')) >>> tm_result = template_comparison.template_match(observed_spectrum=observed_spectrum, spectral_templates=spectral_template, resample_method=resample_method, redshift=rs_values) # doctest:+FLOAT_CMP Reference/API ------------- .. automodapi:: specutils.analysis :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/arithmetic.rst0000644000076500000240000000650600000000000017273 0ustar00erikstaff00000000000000=================== Spectrum Arithmetic =================== Specutils sports the ability to perform arithmetic operations over spectrum data objects. There is full support for propagating unit information. .. note:: Spectrum arithmetic requires that the two spectrum objects have compatible WCS information. .. warning:: Specutils does not currently implement interpolation techniques for converting spectral axes information from one WCS source to another. Basic Arithmetic ---------------- Arithmetic support includes addition, subtract, multiplication, and division. .. code-block:: python >>> from specutils import Spectrum1D >>> import astropy.units as u >>> import numpy as np >>> spec1 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49)*u.Jy) >>> spec2 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49)*u.Jy) >>> spec3 = spec1 + spec2 >>> spec3 #doctest:+SKIP , spectral_axis=)> Propagation of Uncertainties ---------------------------- Arithmetic operations also support the propagation of unceratinty information. .. code-block:: python >>> from astropy.nddata import StdDevUncertainty >>> spec1 = Spectrum1D(spectral_axis=np.arange(10) * u.nm, flux=np.random.sample(10)*u.Jy, uncertainty=StdDevUncertainty(np.random.sample(10) * 0.1)) >>> spec2 = Spectrum1D(spectral_axis=np.arange(10) * u.nm, flux=np.random.sample(10)*u.Jy, uncertainty=StdDevUncertainty(np.random.sample(10) * 0.1)) >>> spec1.uncertainty #doctest:+SKIP StdDevUncertainty([0.04386832, 0.09909487, 0.07589192, 0.0311604 , 0.07973579, 0.04687858, 0.01161918, 0.06013496, 0.00476118, 0.06720447]) >>> spec2.uncertainty #doctest:+SKIP StdDevUncertainty([0.00889175, 0.00890437, 0.05194229, 0.08794455, 0.09918037, 0.04815417, 0.06464564, 0.0164324 , 0.04358771, 0.08260218]) >>> spec3 = spec1 + spec2 >>> spec3.uncertainty #doctest:+SKIP StdDevUncertainty([0.04476039, 0.09949412, 0.09196513, 0.09330174, 0.12725778, 0.06720435, 0.06568154, 0.06233969, 0.04384698, 0.10648737]) Reference/API ------------- .. automodapi:: specutils.spectra.spectrum_mixin :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/conf.py0000644000076500000240000001774300000000000015714 0ustar00erikstaff00000000000000# -*- coding: utf-8 -*- # Licensed under a 3-clause BSD style license - see LICENSE.rst # # Astropy documentation build configuration file. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this file. # # All configuration values have a default. Some values are defined in # the global Astropy configuration which is loaded here before anything else. # See astropy.sphinx.conf for which values are set there. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('..')) # IMPORTANT: the above commented section was generated by sphinx-quickstart, but # is *NOT* appropriate for astropy or Astropy affiliated packages. It is left # commented out with this explanation to make it clear why this should not be # done. If the sys.path entry above is added, when the astropy.sphinx.conf # import occurs, it will import the *source* version of astropy instead of the # version installed (if invoked as "make html" or directly with sphinx), or the # version in the build directory (if "python setup.py build_sphinx" is used). # Thus, any C-extensions that are needed to build the documentation will *not* # be accessible, and the documentation will not build correctly. import datetime import os import sys try: import astropy_helpers except ImportError: # Building from inside the docs/ directory? if os.path.basename(os.getcwd()) == 'docs': a_h_path = os.path.abspath(os.path.join('..', 'astropy_helpers')) if os.path.isdir(a_h_path): sys.path.insert(1, a_h_path) # Load all of the global Astropy configuration from astropy_helpers.sphinx.conf import * # Get configuration information from setup.cfg try: from ConfigParser import ConfigParser except ImportError: from configparser import ConfigParser conf = ConfigParser() conf.read([os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')]) setup_cfg = dict(conf.items('metadata')) # -- General configuration ---------------------------------------------------- # By default, highlight as Python 3. highlight_language = 'python3' # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.2' # We don't have references to `h5py` ... no need to load the intersphinx mapping file. del intersphinx_mapping['h5py'] del intersphinx_mapping['matplotlib'] # Extend astropy intersphinx_mapping with packages we use here intersphinx_mapping['gwcs'] = ('http://gwcs.readthedocs.io/en/latest/', None) # To perform a Sphinx version check that needs to be more specific than # major.minor, call `check_sphinx_version("x.y.z")` here. # check_sphinx_version("1.2.1") # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns.append('_templates') # This is added to the end of RST files - a good place to put substitutions to # be used globally. rst_epilog += """ """ # -- Project information ------------------------------------------------------ # This does not *have* to match the package name, but typically does project = setup_cfg['package_name'] author = setup_cfg['author'] copyright = '{0}, {1}'.format( datetime.datetime.now().year, setup_cfg['author']) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. __import__(setup_cfg['package_name']) package = sys.modules[setup_cfg['package_name']] # The short X.Y version. version = package.__version__.split('-', 1)[0] # The full version, including alpha/beta/rc tags. release = package.__version__ # -- Options for HTML output -------------------------------------------------- # A NOTE ON HTML THEMES # The global astropy configuration uses a custom theme, 'bootstrap-astropy', # which is installed along with astropy. A different theme can be used or # the options for this theme can be modified by overriding some of the # variables set in the global configuration. The variables set in the # global configuration are listed below, commented out. # Add any paths that contain custom themes here, relative to this directory. # To use a different custom theme, add the directory containing the theme. #html_theme_path = [] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. To override the custom theme, set this to the # name of a builtin theme or the name of a custom theme in html_theme_path. html_static_path = ['_static'] html_style = 'specutils.css' # Please update these texts to match the name of your package. html_theme_options = { 'logotext1': 'spec', # white, semi-bold 'logotext2': 'utils', # orange, light 'logotext3': ':docs' # white, light } # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # add to/modify the defaults already provided by astropy html_sidebars['**'] = ['localtoc.html'] html_sidebars['index'] = ['globaltoc.html', 'localtoc.html'] # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = '' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = 'img/logo_icon.svg' # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = '{0} v{1}'.format(project, release) # Output file base name for HTML help builder. htmlhelp_basename = project + 'doc' # -- Options for LaTeX output ------------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [('index', project + '.tex', project + u' Documentation', author, 'manual')] # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [('index', project.lower(), project + u' Documentation', [author], 1)] # -- Options for the edit_on_github extension --------------------------------- if eval(setup_cfg.get('edit_on_github')): extensions += ['astropy_helpers.sphinx.ext.edit_on_github'] versionmod = __import__(setup_cfg['package_name'] + '.version') edit_on_github_project = setup_cfg['github_project'] if versionmod.version.release: edit_on_github_branch = "v" + versionmod.version.version else: edit_on_github_branch = "master" edit_on_github_source_root = "" edit_on_github_doc_root = "docs" # -- Resolving issue number to links in changelog ----------------------------- github_issues_url = 'https://github.com/{0}/issues/'.format(setup_cfg['github_project']) # -- Turn on nitpicky mode for sphinx (to warn about references not found) ---- # nitpicky = True nitpick_ignore = [] # Some warnings are impossible to suppress, and you can list specific references # that should be ignored in a nitpick-exceptions file which should be inside # the docs/ directory. The format of the file should be: # # # # for example: # # py:class astropy.io.votable.tree.Element # py:class astropy.io.votable.tree.SimpleElement # py:class astropy.io.votable.tree.SimpleElementWithContent # # Uncomment the following lines to enable the exceptions: # for line in open('nitpick-exceptions'): if line.strip() == "" or line.startswith("#"): continue dtype, target = line.split(None, 1) target = target.strip() nitpick_ignore.append((dtype, target)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/contributing.rst0000644000076500000240000000627700000000000017656 0ustar00erikstaff00000000000000.. highlight:: shell ============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/astropy/specutils/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ Specutils could always use more documentation, whether as part of the official specutils docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/astropy/specutils/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome. Get Started! ------------ Ready to contribute? Here's how to set up :ref:`specutils ` for local development. 1. Fork the :ref:`specutils ` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/specutils.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: $ mkvirtualenv specutils $ cd specutils/ $ python setup.py develop 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: $ flake8 specutils tests $ python setup.py test or py.test $ tox To get flake8 and tox, just pip install them into your virtualenv. 6. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 3.4, 3.5, and 3.6, and for PyPy. Check https://travis-ci.org/astropy/specutils/pull_requests and make sure that the tests pass for all supported Python versions. Tips ---- To run a subset of tests:: $ py.test tests.test_specutils ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/custom_loading.rst0000644000076500000240000001336000000000000020145 0ustar00erikstaff00000000000000************************************************* Loading and Defining Custom Spectral File Formats ************************************************* Loading From a File ------------------- Specutils leverages the astropy io registry to provide an interface for conveniently loading data from files. To create a custom loader, the user must define it in a separate python file and place the file in their ``~/.specutils`` directory. Loading from a FITS File ------------------------ A spectra with a *Linear Wavelength Solution* can be read using the ``read`` method of the :class:`~specutils.Spectrum1D` class to parse the file name and format .. code-block:: python import os from specutils import Spectrum1D file_path = os.path.join('path/to/folder', 'file_with_1d_wcs.fits') spec = Spectrum1D.read(file_path, format='wcs1d-fits') This will create a :class:`~specutils.Spectrum1D` object that you can manipulate later. For instance, you could plot the spectrum. .. code-block:: python import matplotlib.pyplot as plt plt.title('FITS file with 1D WCS') plt.xlabel('Wavelength (Angstrom)') plt.ylabel('Flux (erg/cm2/s/A)') plt.plot(spec.wavelength, spec.flux) plt.show() .. image:: img/read_1d.png Creating a Custom Loader ------------------------ Defining a custom loader consists of importing the `~specutils.io.registers.data_loader` decorator from specutils and attaching it to a function that knows how to parse the user's data. The return object of this function must be a :class:`~specutils.Spectrum1D` object. Optionally, the user may define an identifier function. This function acts to ensure that the data file being loaded is compatible with the loader function. .. code-block:: python # ~/.specutils/my_custom_loader.py import os from astropy.io import fits from astropy.nddata import StdDevUncertainty from astropy.table import Table from astropy.units import Unit from astropy.wcs import WCS from specutils.io.registers import data_loader from specutils import Spectrum1D # Define an optional identifier. If made specific enough, this circumvents the # need to add ``format="my-format"`` in the ``Spectrum1D.read`` call. def identify_generic_fits(origin, *args, **kwargs): return (isinstance(args[0], str) and os.path.splitext(args[0].lower())[1] == '.fits') @data_loader("my-format", identifier=identify_generic_fits, extensions=['fits']) def generic_fits(file_name, **kwargs): with fits.open(file_name, **kwargs) as hdulist: header = hdulist[0].header tab = Table.read(file_name) meta = {'header': header} wcs = WCS(hdulist[0].header) uncertainty = StdDevUncertainty(tab["err"]) data = tab["flux"] * Unit("Jy") return Spectrum1D(flux=data, wcs=wcs, uncertainty=uncertainty, meta=meta) An ``extensions`` keyword can be provided. This allows for basic filename extension matching in the case that the ``identifier`` function is not provided. It is possible to query the registry to return the list of loaders associated with a particular extension. .. code-block:: python from specutils.io import get_loaders_by_extension loaders = get_loaders_by_extension('fits') The returned list contains the format labels that can be fed into the ``format`` keyword argument of the ``Spectrum1D.read`` method. After placing this python file in the user's ``~/.specutils`` directory, it can be utilized by referencing its name in the ``read`` method of the :class:`~specutils.Spectrum1D` class .. code-block:: python from specutils import Spectrum1D spec = Spectrum1D.read("path/to/data", format="my-format") .. _multiple_spectra: Loading Multiple Spectra ^^^^^^^^^^^^^^^^^^^^^^^^ It is possible to create a loader that reads multiple spectra from the same file. For the general case where none of the spectra are assumed to be the same length, the loader should return a `~specutils.SpectrumList`. Consider the custom JWST data loader as an example: .. literalinclude:: ../specutils/io/default_loaders/jwst_reader.py :language: python Note that by default, any loader that uses ``dtype=Spectrum1D`` will also automatically add a reader for `~specutils.SpectrumList`. This enables user code to call `specutils.SpectrumList.read ` in all cases if it can't make assumptions about whether a loader returns one or many `~specutils.Spectrum1D` objects. This method is available since `~specutils.SpectrumList` makes use of the Astropy IO registry (see `astropy.io.registry.read`). Creating a Custom Writer ------------------------ Similar to creating a custom loader, a custom data writer may also be defined. This again will be done in a separate python file and placed in the user's ``~/.specutils`` directory to be loaded into the astropy io registry. .. code-block:: python # ~/.spectacle/my_writer.py from astropy.table import Table from specutils.io.registers import custom_writer @custom_writer("fits-writer") def generic_fits(spectrum, file_name, **kwargs): flux = spectrum.flux.value disp = spectrum.spectral_axis.value meta = spectrum.meta tab = Table([disp, flux], names=("spectral_axis", "flux"), meta=meta) tab.write(file_name, format="fits") The custom writer can be used by passing the name of the custom writer to the ``format`` argument of the ``write`` method on the :class:`~specutils.Spectrum1D`. .. code-block:: python spec = Spectrum1D(flux=np.random.sample(100) * u.Jy, spectral_axis=np.arange(100) * u.AA) spec.write("my_output.fits", format="fits-writer") Reference/API ------------- .. automodapi:: specutils.io.registers :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/docs/fitting.rst0000644000076500000240000005301300000000000016601 0ustar00erikstaff00000000000000===================== Line/Spectrum Fitting ===================== One of the primary tasks in spectroscopic analysis is fitting models of spectra. This concept is often applied mainly to line-fitting, but the same general approach applies to continuum fitting or even full-spectrum fitting. ``specutils`` provides conveniences that aim to leverage the general fitting framework of `astropy.modeling` to spectral-specific tasks. At a high level, this fitting takes the `~specutils.Spectrum1D` object and a list of `~astropy.modeling.Model` objects that have initial guesses for each of the parameters. these are used to create a compound model created from the model initial guesses. This model is then actually fit to the spectrum's ``flux``, yielding a single composite model result (which can be split back into its components if desired). Line Finding ------------ There are two techniques implemented in order to find emission and/or absorption lines in a `~specutils.Spectrum1D` spectrum. The first technique is `~specutils.fitting.find_lines_threshold` that will find lines by thresholding the flux based on a factor applied to the spectrum uncertainty. The second technique is `~specutils.fitting.find_lines_derivative` that will find the lines based on calculating the derivative and then thresholding based on it. Both techniques return an `~astropy.table.QTable` that contains columns ``line_center``, ``line_type`` and ``line_center_index``. We start with a synthetic spectrum: .. plot:: :include-source: :align: center >>> import numpy as np >>> from astropy.modeling import models >>> import astropy.units as u >>> from specutils import Spectrum1D, SpectralRegion >>> np.random.seed(42) >>> g1 = models.Gaussian1D(1, 4.6, 0.2) >>> g2 = models.Gaussian1D(2.5, 5.5, 0.1) >>> g3 = models.Gaussian1D(-1.7, 8.2, 0.1) >>> x = np.linspace(0, 10, 200) >>> y = g1(x) + g2(x) + g3(x) + np.random.normal(0., 0.2, x.shape) >>> spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) >>> from matplotlib import pyplot as plt >>> plt.plot(spectrum.spectral_axis, spectrum.flux) # doctest: +IGNORE_OUTPUT >>> plt.xlabel('Spectral Axis ({})'.format(spectrum.spectral_axis.unit)) # doctest: +IGNORE_OUTPUT >>> plt.ylabel('Flux Axis({})'.format(spectrum.flux.unit)) # doctest: +IGNORE_OUTPUT >>> plt.grid(True) # doctest: +IGNORE_OUTPUT While we know the true uncertainty here, this is often not the case with real data. Therefore, since `~specutils.fitting.find_lines_threshold` requires an uncertainty, we will produce an estimate of the uncertainty by calling the `~specutils.manipulation.noise_region_uncertainty` function: .. code-block:: python >>> from specutils.manipulation import noise_region_uncertainty >>> noise_region = SpectralRegion(0*u.um, 3*u.um) >>> spectrum = noise_region_uncertainty(spectrum, noise_region) >>> from specutils.fitting import find_lines_threshold >>> lines = find_lines_threshold(spectrum, noise_factor=3) >>> lines[lines['line_type'] == 'emission'] # doctest:+FLOAT_CMP line_center line_type line_center_index um float64 str10 int64 ----------------- --------- ----------------- 4.572864321608041 emission 91 4.824120603015076 emission 96 5.477386934673367 emission 109 8.99497487437186 emission 179 >>> lines[lines['line_type'] == 'absorption'] # doctest:+FLOAT_CMP line_center line_type line_center_index um float64 str10 int64 ----------------- ---------- ----------------- 8.190954773869347 absorption 163 An example using the `~specutils.fitting.find_lines_derivative`: .. code-block:: python >>> # Define a noise region for adding the uncertainty >>> noise_region = SpectralRegion(0*u.um, 3*u.um) >>> # Derivative technique >>> from specutils.fitting import find_lines_derivative >>> lines = find_lines_derivative(spectrum, flux_threshold=0.75) >>> lines[lines['line_type'] == 'emission'] # doctest:+FLOAT_CMP line_center line_type line_center_index um float64 str10 int64 ----------------- --------- ----------------- 4.522613065326634 emission 90 5.477386934673367 emission 109 >>> lines[lines['line_type'] == 'absorption'] # doctest:+FLOAT_CMP line_center line_type line_center_index um float64 str10 int64 ----------------- ---------- ----------------- 8.190954773869347 absorption 163 While it might be surprising that these tables do not contain more information about the lines, this is because the "toolbox" philosophy of ``specutils`` aims to keep such functionality in separate distinct functions. See :doc:`analysis` for functions that can be used to fill out common line measurements more completely. Parameter Estimation -------------------- Given a spectrum with a set of lines, the `~specutils.fitting.estimate_line_parameters` can be called to estimate the `~astropy.modeling.Model` parameters given a spectrum. For the `~astropy.modeling.functional_models.Gaussian1D`, `~astropy.modeling.functional_models.Voigt1D`, and `~astropy.modeling.functional_models.Lorentz1D` models, there are predefined estimators for each of the parameters. For all other models one must define the estimators (see example below). Note that in many (most?) cases where another model is needed, it may be better to create your own template models tailored to your specific spectra and skip this function entirely. For example, based on the spectrum defined above we can first select a region: .. code-block:: python >>> from specutils import SpectralRegion >>> from specutils.fitting import estimate_line_parameters >>> from specutils.manipulation import extract_region >>> sub_region = SpectralRegion(4*u.um, 5*u.um) >>> sub_spectrum = extract_region(spectrum, sub_region) Then estimate the line parameters it it for a Gaussian line profile:: >>> print(estimate_line_parameters(sub_spectrum, models.Gaussian1D())) # doctest:+FLOAT_CMP Model: Gaussian1D Inputs: ('x',) Outputs: ('y',) Model set size: 1 Parameters: amplitude mean stddev Jy um um ------------------ ---------------- ------------------- 1.1845669151078486 4.57517271067525 0.19373372929165977 If an `~astropy.modeling.Model` is used that does not have the predefined parameter estimators, or if one wants to use different parameter estimators then one can create a dictionary where the key is the parameter name and the value is a function that operates on a spectrum (lambda functions are very useful for this purpose). For example if one wants to estimate the line parameters of a line fit for a `~astropy.modeling.functional_models.RickerWavelet1D` one can define the ``estimators`` dictionary and use it to populate the ``estimator`` attribute of the model's parameters: .. code-block:: python >>> from specutils import SpectralRegion >>> from specutils.fitting import estimate_line_parameters >>> from specutils.manipulation import extract_region >>> from specutils.analysis import centroid, fwhm >>> sub_region = SpectralRegion(4*u.um, 5*u.um) >>> sub_spectrum = extract_region(spectrum, sub_region) >>> ricker = models.RickerWavelet1D() >>> ricker.amplitude.estimator = lambda s: max(s.flux) >>> ricker.x_0.estimator = lambda s: centroid(s, region=None) >>> ricker.sigma.estimator = lambda s: fwhm(s) >>> print(estimate_line_parameters(spectrum, ricker)) # doctest:+FLOAT_CMP Model: RickerWavelet1D Inputs: ('x',) Outputs: ('y',) Model set size: 1 Parameters: amplitude x_0 sigma Jy um um ------------------ ------------------ ------------------- 2.4220683957581444 3.6045476935889367 0.24416769183724707 Model (Line) Fitting -------------------- The generic model fitting machinery is well-suited to fitting spectral lines. The first step is to create a set of models with initial guesses as the parameters. To achieve better fits it may be wise to include a set of bounds for each parameter, but that is optional. .. note:: A method to make plausible initial guesses will be provided in a future version, but user defined initial guesses are required at present. Below are a series of examples of this sort of fitting. Simple Example ^^^^^^^^^^^^^^ Below is a simple example to demonstrate how to use the `~specutils.fitting.fit_lines` method to fit a spectrum to an Astropy model initial guess. .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(0) x = np.linspace(0., 10., 200) y = 3 * np.exp(-0.5 * (x- 6.3)**2 / 0.8**2) y += np.random.normal(0., 0.2, x.shape) spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit the spectrum and calculate the fitted flux values (``y_fit``) g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=6.1*u.um, stddev=1.*u.um) g_fit = fit_lines(spectrum, g_init) y_fit = g_fit(x*u.um) # Plot the original spectrum and the fitted. plt.plot(x, y) plt.plot(x, y_fit) plt.title('Single fit peak') plt.grid(True) plt.legend('Original Spectrum', 'Specutils Fit Result') Simple Example with Different Units ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Similar fit example to above, but the Gaussian model initial guess has different units. The fit will convert the initial guess to the spectral units, fit and then output the fitted model in the spectrum units. .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(0) x = np.linspace(0., 10., 200) y = 3 * np.exp(-0.5 * (x- 6.3)**2 / 0.8**2) y += np.random.normal(0., 0.2, x.shape) # Create the spectrum spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit the spectrum g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=61000*u.AA, stddev=10000.*u.AA) g_fit = fit_lines(spectrum, g_init) y_fit = g_fit(x*u.um) plt.plot(x, y) plt.plot(x, y_fit) plt.title('Single fit peak, different model units') plt.grid(True) Single Peak Fit Within a Window (Defined by Center) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Single peak fit with a window of ``2*u.um`` around the center of the mean of the model initial guess (so ``2*u.um`` around ``5.5*u.um``). .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(0) x = np.linspace(0., 10., 200) y = 3 * np.exp(-0.5 * (x- 6.3)**2 / 0.8**2) y += np.random.normal(0., 0.2, x.shape) # Create the spectrum spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit the spectrum g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=5.5*u.um, stddev=1.*u.um) g_fit = fit_lines(spectrum, g_init, window=2*u.um) y_fit = g_fit(x*u.um) plt.plot(x, y) plt.plot(x, y_fit) plt.title('Single fit peak window') plt.grid(True) Single Peak Fit Within a Window (Defined by Left and Right) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Single peak fit using spectral data *only* within the window ``6*u.um`` to ``7*u.um``, all other data will be ignored. .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(0) x = np.linspace(0., 10., 200) y = 3 * np.exp(-0.5 * (x- 6.3)**2 / 0.8**2) y += np.random.normal(0., 0.2, x.shape) # Create the spectrum spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit the spectrum g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=5.5*u.um, stddev=1.*u.um) g_fit = fit_lines(spectrum, g_init, window=(6*u.um, 7*u.um)) y_fit = g_fit(x*u.um) plt.plot(x, y) plt.plot(x, y_fit) plt.title('Single fit peak window') plt.grid(True) Double Peak Fit ^^^^^^^^^^^^^^^ Double peak fit compound model initial guess in and compound model out. .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(42) g1 = models.Gaussian1D(1, 4.6, 0.2) g2 = models.Gaussian1D(2.5, 5.5, 0.1) x = np.linspace(0, 10, 200) y = g1(x) + g2(x) + np.random.normal(0., 0.2, x.shape) # Create the spectrum to fit spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit the spectrum g1_init = models.Gaussian1D(amplitude=2.3*u.Jy, mean=5.6*u.um, stddev=0.1*u.um) g2_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.4*u.um, stddev=0.1*u.um) g12_fit = fit_lines(spectrum, g1_init+g2_init) y_fit = g12_fit(x*u.um) plt.plot(x, y) plt.plot(x, y_fit) plt.title('Double Peak Fit') plt.grid(True) Double Peak Fit Within a Window ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Double peak fit using data in the spectrum from ``4.3*u.um`` to ``5.3*u.um``, only. .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(42) g1 = models.Gaussian1D(1, 4.6, 0.2) g2 = models.Gaussian1D(2.5, 5.5, 0.1) x = np.linspace(0, 10, 200) y = g1(x) + g2(x) + np.random.normal(0., 0.2, x.shape) # Create the spectrum to fit spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit the spectrum g2_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.7*u.um, stddev=0.2*u.um) g2_fit = fit_lines(spectrum, g2_init, window=(4.3*u.um, 5.3*u.um)) y_fit = g2_fit(x*u.um) plt.plot(x, y) plt.plot(x, y_fit) plt.title('Double Peak Fit Within a Window') plt.grid(True) Double Peak Fit Within Around a Center Window ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Double peak fit using data in the spectrum centered on ``4.7*u.um`` +/- ``0.3*u.um``. .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(42) g1 = models.Gaussian1D(1, 4.6, 0.2) g2 = models.Gaussian1D(2.5, 5.5, 0.1) x = np.linspace(0, 10, 200) y = g1(x) + g2(x) + np.random.normal(0., 0.2, x.shape) # Create the spectrum to fit spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit the spectrum g2_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.7*u.um, stddev=0.2*u.um) g2_fit = fit_lines(spectrum, g2_init, window=0.3*u.um) y_fit = g2_fit(x*u.um) plt.plot(x, y) plt.plot(x, y_fit) plt.title('Double Peak Fit Around a Center Window') plt.grid(True) Double Peak Fit - Two Separate Peaks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Double peak fit where each model ``gl_init`` and ``gr_init`` is fit separately, each within ``0.2*u.um`` of the model's mean. .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(42) g1 = models.Gaussian1D(1, 4.6, 0.2) g2 = models.Gaussian1D(2.5, 5.5, 0.1) x = np.linspace(0, 10, 200) y = g1(x) + g2(x) + np.random.normal(0., 0.2, x.shape) # Create the spectrum to fit spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit each peak gl_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.8*u.um, stddev=0.2*u.um) gr_init = models.Gaussian1D(amplitude=2.*u.Jy, mean=5.3*u.um, stddev=0.2*u.um) gl_fit, gr_fit = fit_lines(spectrum, [gl_init, gr_init], window=0.2*u.um) yl_fit = gl_fit(x*u.um) yr_fit = gr_fit(x*u.um) plt.plot(x, y) plt.plot(x, yl_fit) plt.plot(x, yr_fit) plt.title('Double Peak - Two Models') plt.grid(True) Double Peak Fit - Two Separate Peaks With Two Windows ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Double peak fit where each model ``gl_init`` and ``gr_init`` is fit within the corresponding window. .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(42) g1 = models.Gaussian1D(1, 4.6, 0.2) g2 = models.Gaussian1D(2.5, 5.5, 0.1) x = np.linspace(0, 10, 200) y = g1(x) + g2(x) + np.random.normal(0., 0.2, x.shape) # Create the spectrum to fit spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit each peak gl_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.8*u.um, stddev=0.2*u.um) gr_init = models.Gaussian1D(amplitude=2.*u.Jy, mean=5.3*u.um, stddev=0.2*u.um) gl_fit, gr_fit = fit_lines(spectrum, [gl_init, gr_init], window=[(5.3*u.um, 5.8*u.um), (4.6*u.um, 5.3*u.um)]) yl_fit = gl_fit(x*u.um) yr_fit = gr_fit(x*u.um) plt.plot(x, y) plt.plot(x, yl_fit) plt.plot(x, yr_fit) plt.title('Double Peak - Two Models and Two Windows') plt.grid(True) Double Peak Fit - Exclude One Region ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Double peak fit where each model ``gl_init`` and ``gr_init`` is fit using all the data *except* between ``5.2*u.um`` and ``5.8*u.um``. .. plot:: :include-source: :align: center import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D, SpectralRegion from specutils.fitting import fit_lines # Create a simple spectrum with a Gaussian. np.random.seed(42) g1 = models.Gaussian1D(1, 4.6, 0.2) g2 = models.Gaussian1D(2.5, 5.5, 0.1) x = np.linspace(0, 10, 200) y = g1(x) + g2(x) + np.random.normal(0., 0.2, x.shape) # Create the spectrum to fit spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Fit each peak gl_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.8*u.um, stddev=0.2*u.um) gl_fit = fit_lines(spectrum, gl_init, exclude_regions=[SpectralRegion(5.2*u.um, 5.8*u.um)]) yl_fit = gl_fit(x*u.um) plt.plot(x, y) plt.plot(x, yl_fit) plt.title('Double Peak - Single Models and Exclude Region') plt.grid(True) .. _specutils-continuum-fitting: Continuum Fitting ----------------- While the line-fitting machinery can be used to fit continuua at the same time as models, often it is convenient to subtract or normalize a spectrum by its continuum before other processing is done. ``specutils`` provides some convenience functions to perform exactly this task. An example is shown below. .. plot:: :include-source: :align: center :context: close-figs import numpy as np import matplotlib.pyplot as plt from astropy.modeling import models from astropy import units as u from specutils.spectra import Spectrum1D, SpectralRegion from specutils.fitting import fit_generic_continuum np.random.seed(0) x = np.linspace(0., 10., 200) y = 3 * np.exp(-0.5 * (x - 6.3)**2 / 0.1**2) y += np.random.normal(0., 0.2, x.shape) y_continuum = 3.2 * np.exp(-0.5 * (x - 5.6)**2 / 4.8**2) y += y_continuum spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) g1_fit = fit_generic_continuum(spectrum) y_continuum_fitted = g1_fit(x*u.um) plt.plot(x, y) plt.plot(x, y_continuum_fitted) plt.title('Continuum Fitting') plt.grid(True) The normalized spectrum is simply the old spectrum devided by the fitted continuum, which returns a new object: .. plot:: :include-source: :align: center :context: close-figs spec_normalized = spectrum / y_continuum_fitted plt.plot(spec_normalized.spectral_axis, spec_normalized.flux) plt.title('Continuum normalized spectrum') plt.grid('on') Reference/API ------------- .. automodapi:: specutils.fitting :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/docs/img/0000755000076500000240000000000000000000000015155 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/docs/img/logo.png0000644000076500000240000003652200000000000016633 0ustar00erikstaff00000000000000‰PNG  IHDR•{¼b} IDATx^í} xÕÝþ{î½aq\êR¨u©m\ê^‚"P«¬àW­åâZ÷DMM$˜T%©&¸o-—ªí'Z à†;ÁV­KKð_µí§«@î=ÿç7gfîÌÜYÎÌ›<÷y¾Ç¯dæ,ï9sÞóÛÔO! P(1!ÀbjG5£P( (RQ›@! P(bC@‘JlPª† …€B@‘ŠÚ …€B@!ŠTbƒR5¤P(ŠTÔP( ØP¤”ª!…€B@! P¤¢ö€B@! PĆ€"•Ø T ) …€"µ …€B 6©Ä¥jH! P(©¨= P(±! H%6(UC …€B@! HEí…€B@! ˆ E*±A©R( E*j( …@l(R‰ JÕB@! P(RQ{@! P(bC@‘JlPª† …€B@‘ŠÚ …€B@!ŠTbƒR5¤P(ŠTÔP( ØøÆ‘Ê™W¶î¸uï™Häö°ög;Ì€­9øVŒ±í4„9ÿ7[ÃÁ?Ƕà«Á؇@vq¦áŠ¿Ä¶ª!…€B@!° °Ù“Êäªn¹uv,Ïññ`˜ÈÀvoÝøÇx,ÁØë¾H.z°å’¯âk[µ¤P(6=6KR™\ëV[d¿<…qLczbYcÈqþd‚±æ\sÙÜžèSõ¡P(ú›©¤¯šug‰3Áp2¶*5ØD$œshÿ`s8¿qݺäÝJz)õ*¨ö›¼yÜ ·Ñ²êE37Yøt³ •©µM烱‹@v’xŒ‘mEèŠý?ú?äÿfùwžÃÿ±$¦ÿnÆ%¿wPª5…€B`SD€7§ûgÁU/Ü,ÎbãdÜ×Fsºî7çqÎkÃÎñNBF"±Š,D0\kËYŽŸwÏÌK_Šwœª5…€B 'à-åƒÑÝïbpT€±‘ÚÁÑÆjÎÓ¿"•0hõг^xSÿ/}}6®Ø.Åvë©Â²J(y¹D#«È¢Ë+š„¢©ÀÌÐ‰Éøwð\޳¦Ôί¾ëœs6;nõ¾B@!Ðsð¦qËu2±wÊy«Y4Iv$ŠTd‘ê¡çÒµMÇì60 -¦Ëb‰ÄèÛ‹H ëJžòÔÉlrÊÝ3/x¯˜9¨w žA€7¯ÐâÙÏb5‹;dF£HE¥xæô³vÉeÙ­`¬"lwñ©°HB±Zã#ªÈÈæÂ± “î®»èé°óQÏ+=‹o—ØTï^ùLV½¨^fTŠTdP*á3“'ÏKn±Oç¥àìjư¥lWaˆDØ<ì„‘·…¸ç­í žq1ÚkÆ{ݦbø…ÙŒþ<Ç.½«ö¢VÙy©ç žG€7«˜«×–>š*V½Pê;V¤Òóëgö8íªvË%róp€Û0ŠRa9¼µìÄâ°…˜&”¼ ‹ÆcUmiï›Äa8…å ÅßÖ‚+î¼ê‚Y½µêZ!°Ù àvhëYÅ›'”|‰+Hœ¯E61’MªSDE*2(•à™©W7ÿ”åøý`lµù "1ü}ó¶r‹QÝ¢º²J"ëºÖ›-Dü¡0¬ÿnXéÃØZ8ýN¸ûÊ +”ªI…À7 R è*­¡€W²šÅY©È"ÓsgŸ}gÙúo­maŒo%Ñ|p\ˆ»;o¶]…e3¦ØˆÅ•·W˜%@Ò› ùWÈ&|gí¯^‹ JÕŒBà‰@©HE#–¦cFjv]΃ñ.t'2²бŠTzp[žZ?k×~Ùä#|T>BÝ›Hdm!Ç2 ‹_"Ç {܉Í¢«®äTZq‰%_ë8÷1ûÁíÓÏ[Óƒ°«®› ^jªbÕ_q¤H%N4}Ú:½vÖÞ¹d²qìdMyâi×ãÞÄ"TX»î¸.˜r®¸yŽé¶DQTXŽP{]…–Oáb›ºî÷ °磷_ñ«zvÕB`³B@‘JßXÎ^O púŒYrž|šì'Ö@Bk ”¼ ܈PwSiÚBN7c‰é·dðéšÏ]¼±„DšHÌÇíÆRªìô¿ø‰)²è¶žso¿üWwôí¡F¡ØtP¤Ò7ÖªWIeZý Ç1°Gc±…¸¨°®»`*¶¼ æ-~O¿Ò¡ó”ŸW–]µe$‹ô¶…8UXöä’Q%­l6;üŽéçJy“ôm¤F¡è}¼‚•ú«gצ×H%=ãÆS ÷ÓtCÅ…¸ÏÝTZ»í¸êÎú ÍŽ÷þÛ|Ü’JÅ#ÕŠ”Q݃H<’JF#L6çÖê³NïÙ­ zSlÚxÅ“(RéÙuíR9£þ7œ%bÉ|¾xg„º=.$¬;ï´ãÇâÐýö1Ñ<çÚ[…­£(–U±;È©è|’TZ%­ïNåø³¯8çÃRn‡ô•M#‘Àh@ófΆéýµ‹ÿò.¤² 2õÓ{EjJ×· f³9 |¤':ÀÑÅ8ëäeÉ™úª®Rbdm;]Ý0t''Úñâ]ÓRt°:æ\S½ §Æcô£á”ëÍs D‡´n+Z?}lÈñ™kk¤R‰ôôøãêO‘J\H×N“Êéõ-cø“`,eñùõâˆÄšÌ±å’3±Å€þ&:·?ô$V¼¿Ò– ÒøcPü‹ÕMÖëL´]˜ÊE¤Ï—¤ä|Î-%V´ƒ1›º˜’V[ˆ±˜:¬ÅDà3U7_z†TN!¿m¤©¹’ŒÒLD<]Zç¨Ï4VûV«“"ºÑ§5©Œ2ÁF#}(©²1²¥f/ÌLu ÄWêùH²|L’TºvVX‚üã£cd&ïgÓd$ª>H*®Å®¢/Ô¦G*BÊcs\SòËÁÑ Æ¦ù¸lSŽÓ/ìkáž?»þÎ-r‰õ+Àøw9²|Ýy-Wþ0¶+OÇ@‹êËmí÷cõÚ/,Ä’Ï:\˜ºÅ ‘Ãbg¸‚ôŽÀ9‚$­|‰bþÆÍU§»æ>“]’ŠÑ9â9Ló:0åH…Tkè(^‚ÒU$±hª7F„×Á­«hbI×5á’Ô÷oA¦¡:0û·"¯¤’½#©èÑýK„í,ŽoeÕ‹ªâh©G$•³®™ý›b±’‹w³€•>¯X!¼²HBùåOǸbóÐ3ËðÌ«o:ãB<+Ç'±Ëµ³¬ï–ŽT8ggýú–—ŒýH¨~¢¤6q!–‚ƒ;/iòƒ½qÚO\I,3¾{ÁÓµ8g»$¯"ËwVÒ*,øÅ¿´z÷­ëëÇt‡Yôpª¾œ·‰¼ 'Óbˆ¡]Df¾4ÓPè¡Õ»¤¢àÓ2 5iÉ#Œ“T m`š¡›Äbú?²ÅH”¹–#;]r¢ýG7PGùјÜú)RÉ#ÐgH¥é˜´î¬aß³YÍÂP67MrIñ Ùc²±d¤rvã-•,ÁZbIuâC$¦ÊŠçœ8û}×ÿ»^òú_ñpûKºàà.‰ø{qìºÃvøçÿ­vÔ]ñ Ý»Ì]24qþ”`‰#Z/üÅ ²‹JÏ…8(} µÂpÍ©ò`ë;ŠuŒ!Æàü@æ‚ñv$³íO¡;”›7+Y 8Ú3Õî6½ awê¦Ã;X¢#öRù¥k›ZÁ˜»ZÂ6æ`²K×6Í—‹Ó±5¼@‹CɢðÝrJ–ƒƒÆ6H¬_vdè8ÏꇽãRü•T<×A^J‘ý|¢>WRùÕu·rosŽ^¥}¥Jòúd#vn?xkÌÔÓ²ø±úóu˜q÷…I$—$TX3N? M÷?‚¯ÖoÐüzIbkÇÖø[i{ï1Ö”.æüÀã¨j½è¡\‹ÓuMí&ø"©W×$0ªÕ]!ã‚’Øô 4_‹Ê=½ÉDŠk( –óšc²{¸ß*M|’FmÉö$ˆî•½×ÒŒ÷q{DWñÕ¼=ÓxyhU‡Rõ C=oOk7ѹÆ})M‰Hå¶%œ£\³¥h?[ˆ¸òGtïÕÛsÀð³1‡H‘ký=óðÙçë,büz›¨ýöØgžz©O½LZ‰¤’¾^m&¿¹fKÖUj·ÞxÁ©HMθK“JnR”CFf,’¬ÑÔ $SåAžHÔBþ‡­¬ÝIÒsK?¼É÷ß—ø2 Õžß^º®ÙõàðÄ\òb ³f^Ï(Ré+¤2Ž.vS Ö©› /e”|˜½;©œ3뎓X.÷ µ.Š8÷ý’8J¬†•ÜÑœPa]qÚ$ìú­í¤æþðÒ—±ô·Íx‘@wa ¯œqÜQøá»iRÊ5sÆ×6èÄâMœÅ¦vÉ?Õrþ/ì)f*}0×á7Œ0¤âwÈ:ûž½¨WºÞÞE0a° ±Oîí6gÀPøá[ö!©P!Ç&õ¸<¤H¥Ï ©¢]²T§†Œº/ÜÞ‹•TξóβÄêìûŒ±¡nÆù|Æ« @«{q^ðŠÀßnÐÖ¨?sŠ4.ùû‡¸û‘?äà2ˆÏKeG•×#2ÓoáË+4i%IËZ?ÆYÀ‹ƒ½wãù§ì-=Áp6j¶‹åXåœ__kʆ’‘Š,h¤âíÆ+máá¤9¹y»ÛUB9XD´„ÙGƳŠTú ©L ¼xîªQž›æçeÝ£¼+©œ{ý—€áM0Ñu[ƒ]GTPÚW \VE&$«$T¾ÿ÷qbùÁ¡æ_9{.ÜœlÄâPmýè{{àçc3û!i媻çå“T†)øe n2ÎüëÎ;e`˜ êÆmŠ»ñãk ›8Ò«¹ÃU¼FR¡çÓuÍÒi:¼ÚN×5Q™{P“eRáǦ¥œ¡t*Þ?×bi¢„9;ÓXÊã'Äf°=ªH¥oжìÍã)k¸»GGeë«Â&㌺/ÜÞ‹Tνî¶!Œ%:9Ã6Â}Øv"G#3Ÿ|!‘X%!’R¶Ýf«P¸üö±% ‰Eð™û"À8ã§åøÁwv³õó¿O/Ã+£€w/÷â`IK&ÿ7çþOèµJ×5SªsŸ(Z¿Cw0žh-&½|II¥¶©+ÈnaÌΛTä‰)Ôæ’yØ‹TdmaÚéâ|*3 Ùg©ô!R¡œ_,ásaä]ଠÈÍf5‹{¼ÜAèƒÊkž7ëîF°ÜU=—ÚDdG©95|Y÷g;ÞÁÃϾªIT^ª'«¤µí ­PûËÂI«¿ø¿þ=ÙUe%-£;]hàu¶ebË-ëÏ9þ¿²‡€v›‰$CJ+=PìJ&ŠôRRR)òðÕ]‰=‚È õY÷Xi¯=êVÒy ê­ï)Ré;¤¢Ý'¼âUœ‹­g#Frž’^b!•ªç üzãš 6$Ðè­ßè…„²F¼EedˆB'ŽþFÚ7ôwC®Å sçK{¹&ážÛñŸy ¯¾ó‹i%o=²Î3Œó‚3?™ëÞöúóN }êÙýU1²èñpéåû4©ˆÄ‘aÝve‘’x΋Tä³ „UËI ÊóE*}‹TLbñG2?R167ÎŒÄnÝÆB*ç7ßY –h!•—ÓÈlÉÞh©_è†k¨˜Âd#¦ ]=íg¡U_sçƒ$Bwf{Z~zà’“Å··âºtÔÆµ÷=b™ŸL6bgKeJ«Í'•è¿ÓµgMŒT[%^bñÏLl¨¤¤"hè®&Ò³÷ER‘³q¾6ÓX°)sØH<£H¥ï‘ŠF,Zñ´‘…*›Ø‰N$x})j©²‚Ä–ò~¤¾~IêÓ-?XÅ]¬‘è^7t_¯°Ùˆ©ýok;Tÿü¸Èã_ðüëXÚñŽ-½›¤µÝ6[âÊÓ âlý>°äe¼ö·•ií-Dâ‘Ößî§$•MƒTlZ®Yã*Ðʇû«Yè¥3E*çÝzëV‰õV3°²°¶sòò«¹N^Y”.eÚ±þ’ÞímOãÜ ÿ¬ñ™§žÃ_ÿñO=î¥Ð²ËÛ¢ê¤qRßàß?þîzŒþæ+.×ÿàú3OÚSj!9½à^íúäÖ*-©£—V5%SC‚ÒÇ„„ÝóñPÞ_ʥ؆£’TÜ·•°»pÒP¤½5¼ ݉Qq¤z)ŠT.ºñwÓ8ð;ëMÜ:­Ð7tI÷^zìçcÅAûx«¾Ö«ï½ pîıØãÛßòüˆŸ{ó]<òÂrW[IZ'ùÜ[ª,‡Ö‘ ‘‹öÓ½ œÂæ:{îº3Oüq\—[;E¨Æ¶úK»¿ëð:~gÕ'Ú»®£œßyÝ™'þJzE<¨“ U–óÏ]•ïÃ3ãnÉH%Lñ*Ÿ¢X!p©:îEÀn¾ÚS/ scR¤âGªáRΗRš’]kÞì‘”¼ © Ëg‰L*ÞtSÿDv›u<¥„vEcˆím…óOÙå× ¤E¯¼‰Õëþ‹ÿ9Ê?#ñµ÷>‚5ëþ‹·ŒK&OðÅûº?<Š[qÎãÃwÅ/ÇåÓ²È.ZÔç^þôÜ…Ä€‹®=cÒÍQÛòžaâÎK"niM¢pÖwHJ¡ý»x«´èï-.4S§L?õ8_Cû£Ë:ðü_Þ/°…L)?옪Ø)ÙÞÿÓóËñÆûT J_F6´ÄÑן~Â3Q:Ò‹4Í@²¬*Œ]@^Å©@Òn‘®kn ïÊ7ż(Ð%PÊ;‘,Ã(ëEï„N¯"á¥FZøzplEº8_ÊjVõ‚S­yW7Øð‡›—€Ôú!ÅÞ¾ûJåǨ{ÊùžVN oÉþ‹Á`™T.j™{M":'‘X [.<-. BµóÈ‹ [‰¡a:yÌÁ8poorx«ó#ü~Ñ‹ù>ÃÀ²êÓ…iYB $ÂÃ/¾ýw<þÊ_m”¹~©m¯?õ8ÉÃÏÞiº¶i¹^{½‹\ek§È§x‰‰TÀ;‘Å$¯]â°mšhÞ,r?‰[|˜–ñ©²1aˆE'ç‹‘LÍ”}/”e Áye¦±f¶0"2£ å®LcÍ(9ÅSžiA8ï`5‹BµeÙ7ÕHR‰‘  áÍMRñÊv\| ýȤRyÓÜçv¸_6â/85̾íYRg‘­„~$Ií;lLw¸oûWÜýþwAEì=“ìž–%¶º4´ò_Ÿá·O½`-ØõNcú„ðyh¼Òßs´˜T½Qú÷·YH«¿òPð c¬'RKé¦C™m,ÍYŽÄp,/ao—ÈÌvi¥yS©Ù¾1:¢Œï “Cf–w"°l"ÎÛO´ñ²K I„$¶1;‘3òüU¢ð-^Vx¨ú¤\P Š wWŸZ6ŠTJyÎxJ*»sœEÊïyP6âÞ Cê0œH’Ú¢_Y Ôqïâex‹ ìzª™ÓŽ9ûÝ¥”ëêÚö‹oÿO¾ú–™9&~ëµé‰¡ª>j·úÀˆq‘æžQMsë/—æryãà£z‘·Ë”fiãz(i%?T"—0tÐÍd䣴)å`|˜«ÍG‚äŒæ#I+v)¨_—ÀàÕBbïžB†c6«Y(­’Ô¢¿‘›¯ãå±øá7OtÈñ¹ (6õWÓ8×ìÚ²å€yKù`t÷Ÿž›[Lío¬r£Š°D*—Þü‡}8rïY|-ñ¦×ÎÉG‚ƒöùNIN ¯F\ú*^{—ª¹2ÓFB_Â9ÇÆwvÞÁs,/¼õ[ö¦fʲõ¨™âoÜãý±ÿž»Ç>·?´¿†w>ü—µÝÉS7Ä(©þB³¥Zuy( ÿT¯‘JÈâU²%€£Âd¾'¡Ž³ö*f%ÊàBއ7ë˜K)JµÎÇÈFÒÙu#ܘ}œ ŠöjŠ‘T\mHÒ¤Ò4~„ ˜ó6”m˜Ö^$HݵÐ×*V½°h#rDR¹·"6ß»½(<µÝÖ[!ý“#£Ú£|^ïÔgÚðõÆî‚ÚñGü`OwÈ~ž]‘§XÓ¸OÏþôàúëšûÇicÆðb tDzwVâ‰Wß²b2»qHý´Itë”þ¥k›æ‡VI·n{пþ{msõW´8Þ ¥Ú¡wåmHEŽ/DÀb]ˆñx«L´® 9Lc—/²K¾:\úAFž€N£þ÷ˆï’Š_‘"‹W•šTÀƒ%wB& ­¬zÑL™)r„q"&ò(ÞžBcˆD*—ÜrßtÆØµnÉÝ NQðá¶[o‰![méÓ£}(DXGüp/ #Bñ!¿_¼ÌZŬ@I†/¬8Ê÷æy ±fÝW¸pâì¼w6éw>ü÷?ó*vÚvì»ûήmZÃPÜ’hZ_údÍçø×š/е._.E“´r¹e SåÓ¬ö’ÙzRϬÈ4Tô{²—$iµ—sì1¨œ‚ãhÏ4V ~P<¡‘]t»•Ko.Û°ñœO¹egSzð\‡ïXÈ™  RQŠSeM>Rwq4)‚íܽ¶Â“ŠÖoUDNêÊ6pÞ–dàY$F¼`«üTxñ‘ÊxŠ»¸`™4Ü(ê`5‹—`¯&[â­2Ô sÑüºÙ kd¼X·ìP Av5/G—UH­VêqÛn‘HåÒ[þáŒOîÃÖúTùÒ¸n¹ÀÜ x‚Ò} IDATå‡åTY;`_Œ=àûÒŸÉCϾŽ×Þ£ýì^Êøò“'`ðV[x¶÷Ä+Á[Ÿ zŠZ–‡ŸïÀë|(²õõHŽ)—ëL’#e À.l8íØ[¤ÐìÃ|’©ò o¦‡šê7\EÚF©ÄKsˆS§!c^±°6¹èÿ»…T…9”ûyÝY[ô>\Cö‹|ôv¬¤èà1Î/¶øHå˜4X¿¾‘‡ãC|Ø;¹¯%b•Q_ʬr$R¹äÖû&XBDÒ½Èäð²ŒÈ+µ‹µ´/I)WüüX)iåë 1óÞGm6óœ×þ“ŽÜß×B©ÁÛ×;ý õÓøÇ'méò9^Ïz¤ñ·•.ÖÌl¤à,—Üiæ/Çë9_d–3ÿŒPç ‘ [ª ¾ɲРB¡¦äI…/gtâ/i „b4ª«Â2‘K1{â¨Í³>ÈóΕìDL yîE+í>¦HvWDˆYñ°­Hm z¨ŠU/l5žv·ÕD“T¨Mïˆñ€ñùijÄF*dhߨŒ¾>—ï¹ëjDÚ›ÑÁÚn |-8Ò^jK鵞íQ^ºôÖ?¾Â²¤÷©¥ÀT17t+Qõ=Œ= Ø£–zö Ï^4¾ïÛ§u°ï”‰T†øH3Ë?øxøÅ}zö\+ž¹ÎtdÂ$•Ì!÷tÃ/~zL”õ±¾#Ôa¼¾hr¡-cõ™†jó0[RÉ4Ô”kê'r× #%иÀÓ²ñ7Ac¶a§‡‘]¨È˜“:#“i¨¡Ã ¨ŸÀ¨È1qPtm&ÓX]e0©_»»ÑÞ§E±V7b÷È褙X|ÿâ"}lô=údš¶mèöZ¿è{“ó¥È&Òq$‘´®z$Iå²ÛÿøclO÷ÒÁv¯«|âs°’±‹þýPsò„@iå¾?¿¬åÜòk@YêN=6Êwd¾óÇö×ðö‡ÿò”DB'Ñt$—4¨9Áé§ŒŸ[Ô`-/kñÝ)Ò©R¬G˜Ûî €·F9åIfÁ)3÷8Å¥xßæôƒ©T«ŒÔT Žº­¥œ—‡ ¼šî>•m + ÈŒ5Ú˜â#7ípÁ‹2Û*­€TÙÆ67=oߪÙ]l?^Y¬:F…¡KPðÁK,KÔ{•ÚÕæêxmœþNXÀªJÅd‰5`åàðÿ^ìÒ‰ï\eöŸß3‘H¥úö>ãÛåë¥XˆÄE¥–Hœñ/$­½ÿ÷<çA*)òÆòάΘp8†ï´]$ܺ¾ü 7üéÏ9Âl*-• {©dÛƒfJ]S·zÛÁ¹].:öØõ‘ð’îr\!Œ¨Ú8ÈX9XSA‘‘UüÈ Lê–È¿¤ך'"-?6ôØÎÄxr¼MÖyð/ê4u&>X`ÇDÌŠ\RýÚKMtÖá©øµôÑ #²89Åé0ŠÛ!Åît”º&zÐŒOìy}¯'Ú‘úº#uÐ8œ× è„×0 71®v Ñ3û<)fJsÐׂÖnØyE#•;ø’1f·x»ØTHö ˆÞ*²üÁï4òè—BÍ”ñžÒÊ|Ê™%óÎú"åéà8|ß=ð“ƒäÿV@Éå÷©×ßqhöò¹Î¬)j¼œ¬ÿîß“Ëñ†™§N¸:ìböµç‹%•¾65…€B h¤rç¼5Œ¢u#‰Íøâ¼¡{Ø*€£FGíã:£[YŠWwyÏ­^g; „óVšäöÇŸ¹ÿZ“fÆE$Áp »Œ'w½ê”±ÿ^¾¾ý„"•¾½>jt R ‰Tjîš÷/†ÄŽÖ€«êɈ´·F ÊÜÐ-kÁÁ=°.;é˜i¥kÝWøÍC‹u ÅÛYÀ*!]2é( Þj`(z¯×µO4¥H¥O,ƒ„B GˆD*WýœåÚdâBUd–Ô.v–ÀÁÍVAR ÙDæ,~ÙðÆël§!Ûàœ ‡‚ýîGÿÁÏ­ðŒ ’´¬^gÞóË^;ùhK=ãÀam(RÙ$–I R!+‘H¥æ· vI%øG^^O®Dâ‘ÅW–HœîËd´_¿ê¦#w÷eƒ‰*O8ƒ¶ôw-~äR}}¢3œ%+qL¹Îrœß\7ù¨‹b]Õ>Ò˜"•>²j D ©Ðø®šóÈ{`سI$­!ëy_1»r^Óæžæ>ŸBÆÛwâ¾ýK¯ßØ›{_mì¶§¹·0XøùççÁ9_µE®ÿ÷.™rØW=¸æ=Ö•"•ƒZu¤è3D&•+3Ü`ì̸²[ã?¼sxCžHÜ“Zæ‰ÄÏëlŸ]wĔý«A¾Ùù ¼ò¶. 哚ɩ´ô䙚Dã—“È%ÇLŸ|dQùµúÌNrˆ"•¾¼:jl Ò ™T®ÊþÔᮬÿ=lбpÆk®:±¼¹4˪ZU(½ƒ@dR©Ÿ·d«î¯¾\ÍÊŒ¡‰œÊÊžßfÌÐE˜8)¿§•ÂІ Oª¯ß,xÎþï2Db ¸wñ:Ó|øÊGÿ¬w–\õªP(J‡@dR¡!ÕÝûøC ‡£5¾Dâ`õÊâ[˜Ú%z~AIcgƒ÷ÚÇŒøn¯~ðO,êxß–S,?OK¦ÒÅ^¹Î8gÛP6ð€úã̤/Ýúª– …@"P©\=÷ñ‰H Íð¦ ÊæŸTҞʥ¨| ”n¶o…3ÇTú=~ ÿéZ§ J»¬¸ùQFtñ;`úÏŽüG®²êL!àÕÖéÉÚ/j!6Š"‚gÆýO~ŽmÝT[®õB´›½w.°âR»äLªF¼î|þOÁ -˜/¯ýï×¸í©—u¯(^gV^^ÒÊåØaWžxø²Í[}³g(J8c 8Æ[è¬TH¦k›3`˜ª·_¦Lt©Æ¤ÚÝ<(žTî{ª ³œ)[ô+¾!384d1ª´,ë†H¬ã;îÀ}°ßÐÌ–ž{gž§Ó*šˆñ{¨ºÜTvNI‹s~âôŠ#ç‡Ý6Zàlj%À;3 5Ãþ_ªçé†Ë²GÌi¨¡úëêgA ¯“Še|3©3 }s–VÒW6D¿lW)Ê:«_ˆ@ѤR?gÉÞ}'Ó꫸§¹·f»ÏWf,°yèI'^†1Ý5n¥ ‰¤N–wË–€Ï»%ïµóöøÙ!ûšèܶð|þÕz× ,Ù˜Mbᨽ¼âð_GÙ€éºæJi­®|–ê­2ºÎ±“[à(óÜœÞÙH¥ 3L ÙœÉÄØS麦v*Ei¬®ßœöY_KѤB›ùÇ…gpŽ{¼JùZÿÝÍxOªk\ˆw·–{ äÀ²2T'R¶ügí—øÝ’7Œb´ø—O<âܨ ]×ÜNv+T€óöLc ‘L¯ÿ©x/Á¦B*ß” "•ž=.b!rýÿ.|q¶Pøà‚VÎ4ö^aåÿRPðËG…uÊûa÷íᙿ®Ä+|Tæ>´´ä­ÿöÒÈ‚Ž.?á°ÈµæMÕW–B’•üb§ ,]ÛÔ †pÞÆZ6Œª€ç¦e/oóÚNéÚ¦‹5 ˆ±‘âžA²¬Ê¸¹’z ÙÔIIô÷N–KÔÏùõesµ>ÑÚ»"x“ñ¹™†šLº®) Ž‘È!ƒ$ækãIv'Õƒ¦†H`«M¢9^e•¾Ì÷ÁÛíóAk¦±z&½&‚*ùÐLCÍ4ëü´wÁ*2 Õ¢}ËO¦]­mMݘœAí€JfkXæû6šsa-`N–¹¸‘ŠèŸMEª{š†áÛÝ}1§1ӚѯÉÔL«ô æ¾®è0Æi¬c޹;Ù⇫9n±_&jíikÇW•Â9ñ6ä0Ó¶>Æ~‹ÞB¹‘“¾Úß5 AûAÌÍì'Ç«l:ö 6·œyReÓlyì+mˆæñNpÖIÿD¸™í¦º§!›¢o¡s3ÕÚ~O×6Ï]à¼÷@]3ÍO¿äiígŒ=ª·Aß,­‹øÆ8=[e¨áüö¹^Æ÷ó÷àü>âüßñ‘ʼ§÷e9¾‚)σÖqçS©HI> Ý·rd”\`D„~çÛ8ú‡ÃÑúÄKX¿1+06ùÊGe§زªì–cœŸU3ñ°ß³Xšê‹£2ÓX=LÛ€I¶Ü©·0 zÇ\=èúC3 Õ…8⃡ƒ0ÆëŇ– ° ÀÚ,U=À+žFª_;²+ÀÙ0R!èC¥fèåÐ6¤º;µÃ’|p:´†ˆöy‘›Nˬ¢9éøÒG#ËÇ—% …9üȨ̧iÄU;«,1ß +˹`+Œ9X±—iW?ˆ°ÊÁy«†%@F÷V£»¹àKÁ™P©Ð”åõ4'©èÏ/x_ôCs`4ÿŒ¶ tð´€c¦¡¦1Û4CºX7Þ0Z_Rç´ëä¯Í›úׯ’¤1ñn6¸‚µËñ.mÌÆþ‰êsÒö’©áæeCì7ªù3FÌN•¬¥­*$Sto(H¥*5R1øZ­ ûüó»°'ÒžYªÍ-Ák{›sži¬eªxf•m¿®©Tº»GêNs5¬©´[ÆÐÆhO¶ÂÀB#NFDSŽmm8'•á|ã"#¾MÂÛú}`¤IÐÆ­xÒ>ÎñV ïüÜ¢<7Iû¶ÄejN˜ï¡˜s)èÝØH…:šùÀâúØŒÒÚBô¸y™$’][-dÇÁ[âð½vÃüWßI*#ã_çrìÄ+N8ôÉ ðƒþ®«¾:,=i³ªÀtÑ~R©‘æ‡oº!¼„ÄæD¤µK`CkÊãvÞRÍÃÛÑ·æm^TÙ0Û-³¶™ÔCº0ßwèû–Ä޳`aJuú‡æÄV¶]·5I;ûªm^`8‘½Çó¦÷—v°e»W‚cÙÙˆ¯®¹ àƒ Ìu­‘™†j]šÔ.tfy†ˆ$m[MÊì^ƒ,¬kgì#뜄4µ‘&ÝqEïßk_9p¥OÚ$kýò Ö|©uç÷‘ãyaÛl1榵â|¨Anû­®i%ÀÖZ×ÎqÉ)øté•HÌù=T ™*/ün4çíûL×6uÑeü>{ èì)æï±’ ¤áÅËÁÚG>G–>G¡.{ÒF½aý*Uà`7Úˆ§$KÝ~0>üt­·JËSÒ¹ÆÀØ¿x“.?ö—ŠYË l¥õ`ÐU]­7P·Ã_V§oxpqNª5íF^n4¦4@*‰,fÜF5fx‘JÁ¿×5¯q;XäDVùñi*°©ºz«5ÓXCꊂŸl»æWÝ0–Kà9ÐM—$¶ô É€lû¡ã80ò¤"ÔÌóp!/º\÷h­"]Ɔ™Ä.ˆx„õàÒ÷ÀHó0Ñ%&¤SóÇ…D·þã´éºæ‚ƒWÛ‡‚èL©'è²aîÝî$©éV²«ŸsMõ/|¬.×íð6¦KFŒÔvBjÕÆÍgûÙÝææIlyè*Ûü~°¯»ù}JË©"4=6…„éº=¿!%9öy¹órWøÝµfþ{Èk9Š=‹Â¾;©\;ïÙ²è~™1÷W«±\R…ÆRèÎk@àgkqØB*,Yè˨۷ÄK<5å²ãü4ì"xl:q3rûYn‰QI%-ôÁi:,É3FW¿L´Ý^…ÄC‡Ò§ý#ô!íuh9ÈI¶]§d"nŽèò’äÛÕnçs4õÝ¢I’ªx—F*¦êÆ~Ëv?4y‡PbhÚÒn³"•ÂÃHÍg9PtrÒÕBEEñ/³Í)áêõó ¾‚ɇ( ž `­¤Œî]Ùz½â?ü#ðíî˶ñfŽ0‡-Ää!ºëuFÍqðnÆÙÕ—wðõŒ1¿ ×hê:Ð ývþJ:u‡:®¦ ’Tt»G%r(wÚ2\®â0#U„©_—=¤ÍCFˆçæ¸óÿn—x¼ÛµßàÌ[4GéêuéÁ3ØP¶]íc${2•¶ÙtU`8I…¯%Œ‘$;ã†aÝ2ö‘Hu—›†[é/­©Úø(ˆ½5Øy+— ÖåæœàµáBK*œlBÕ'#©Øtá@ö·y›˜{phþöoŒš?X )FR±ü^ª;¿u×/8$‘L$»6$köO]šr½4jßCáÜt5´Œ¤¢©Ï¬’¹®Âì4¿‡^ A( ©ˆ ó–O$r/0Ævö;‘Œ[ñPaEàË׈÷¨»âKü#ðÄäêãŽ5JÞÏ> ëÙI¢ÐŒðQ$w^ÆéÐXà{£•lW<±„\?µ§é7èZõédËÐæ’d”òd•é¥åHÓ¢ä#Ù=JÃÉðÚËaš¶†[«&Ukߥ¾f0†´æ2N_Mkm.¾¦1]Ø/x"µ”2ÌêÞê8Ð ÖôV64ÍÓŠìAÀp¤²#MiJBý¥»œ5l Ó®nžÈ)ÖÊôÞ3%•v–м¹º4»©ZÍÃ\#cmϰåŒñzÊÞ ·E¶¿* #ƒ8Ú鱯ØDRŒXT„„,&iûÔÅC¯ðÀÇD–c•œçV0–Á¼Õjê%Ö&öþ€.áŽzÓ}Þp–Ñm’ZûI\ ΘÞâ{Xbî!c¯dwéþ=4QhÙ™æö†ê‹¦YRR¡š~á[ѽ„1¶¯µäo[ˆÝ++>Và)o}€ãM>­æøCÉP¯ËVs½Œ$©ý+Ëm<s\¹é: õž"žYä*íÞ:–üQº±ÔKÍ”?È L:h£M¤ÆZÝ¥W3Æ™ rëÛÀÑ”˜D}=¶¥\³ù´küÍ~5V &7c›—Hqt¨ _Š,*½,Ó³|y¦±f’îÚKA­CÁ9y‡P[D-æ[ÇJ=VºuºlîÉâ`¥µ¢Ã”nbkýÔFÞª@ï9™xËJÀx¬’œ‰¡ŽÓ±@†uϬ¬Ïéí‘úIìU`…]¸ôø(Š{¤‘ ]œò³™*bmÿâ©_,ìßãõ†Ë¸Ö¯ÏqýhÜYž¶Çiêa-ðY+ßCŽW¸e×0¤ÝÞ̾QrR!ðnœ÷â¶_§6Îa,q‚Ó[ËÝb­Üha>—z&QÜ—evç3œó/ÀpyÍq‡Þå}õŽÆÞ+ÆÆÙŠ¸³Š Q?l»r£-íSúAÞ`f0§®ú û–Í©¢´#‰·uó@ï%¼³)mkÆeÈ˵ßõ{`Œ$9Ó ½´#,l½GHÅèvÖüçÏÈ·2ƶr ,ÔžóŠPpç%£»µòcl@r¾ŽƒÝRÖÝVÕ¤Qš BýJ‡€Ÿ¤Sðåoò3ƒ²ì†i·t³ ײH ½ÌtõRÚË}:\/=ÿt”Ðó#ê»=†!•|@e>ȶ7fÖ£¤B¼nþ²aŒeoÃ$g€a$£º=¿~lrŽÿ‚ñÖ{ðg±5¬òE@æð×õèdªð 0³v$Ón_[Ãî"l%Ý+„‡M¶§Ñׯ,3E*2(‰gdH%ÿ=h‰gÉ®×kRŠ!ÈÏ0Æ'›Û–ͱÜ-Œ±½{J…%3|ÎñoÆpWjcÿ•d"ƒX¼Ïè)dÈÈì™QÖÐFi@d2íÊ´ïLŠoMh¯Ó‚S)Vh•æ<¡¹¢V‹œk›àOàSi¤º3*½ÿê{2(x^ ¢|¥Ü6=.©X'SÏybÀãËŽag‚³ ý ‚ázI)'o´M±&{‚q~ÏЯybʦ'ÿê‰ÞU …€B`óA WIÅ ãµO¼±CYvýiû%˜éÅQZ¤9–qð¶²²²{«&øIi;S­+ Í>C*V¨g=¶ì{ àTÎqcì@[Ʊ| [ÈÀžJ¤²^2á°Õq´«ÚP( @Ÿ$çâÜðÄ«£r¹ìÞœó=åHßÀÎÙưå}â ëþ€/ØÿxŸƒ¿Ç‰w³ÙäWà‡já …€B tl¤Rºé«– …€B N©Ä‰¦jK! P|ÃP¤ò ßjú …€B N©Ä‰¦jK! P|ÃP¤ò ßjú …€B N©Ä‰¦jK! P|ÃP¤ò ßjú …€B Nþ?ø—§Ù€ÊIEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/docs/img/logo_icon.png0000644000076500000240000005260600000000000017644 0ustar00erikstaff00000000000000‰PNG  IHDRÎÒ ÿ$sRGB®Îé pHYs%%IR$ðYiTXtXML:com.adobe.xmp 1 LÂ'Y@IDATxí} €,U•eDV}vDVA@vYE6ÑÆADdÆiÅ]§¡TPAm[[pµ—q›ÖîÑî™Fî¶Û]@P}_ÿùü_s÷w¬Ìʬ_UŸ|Uï¾{Ï]_¼ŒÌÈÈÈ¢·qÆW`\qÆW`\qÆW`\qÆW`\qÆW`\qÆW`\qÆW`\qÆW`\qÆW`\qÆW`\WrÁE4h¨ œpòG7X¹öšOOOoXNTëWÝbÝ¢è¬UÓk@ßAãeUMU÷Q –v&;u§;t»Å}Ë–üé÷ßœšþ¸ÍTñ™©B Lþš©mÖYQîRLT;Ah;L”ÅvE·³uUðèn ¼¡æìü¡S–·UUysY–7ÅÊka•]]u:W}mê¤{X9æ-œ¡Šì^ {°î¯Êòü%źç]8õ¦E¹€Òê]б´¹ÇqÞ¡UU…—c«IàhB+¤ÇÞTÁ©„²¸ÎÈ}ô‹Sï9o±U¬žÏbË`žã=î´s^Sv&Þo„áôñ"n´'ðîÐßN¨`í8ŽÔjÁÑù·Ýéî¾üÁ÷|]y ½oHc¡‡¼0â;îýç½Ë_@w\ Å@/»d7iÙ[êìÞø)y”Ñ)þw§Û=õóSïù²jo1/ÔZ\ǽÿãÇÃ¥-§BáÏ‚¡Yæ©îoÂÕ¬³Ç‘móæ–I‚x=8›§êOüâ™ÿãüXx”yáE·€":îŒINÁÅ‘û, °šCèh‚&`7hÙêláÔ ±4,“žzI×Ë}¯*;ïøÂûßqcƒáyg¥Hç=”…Àqï;w¯rbâ 8ʼdaFQèM|}gpG¶Õ¡a™´.È6¹1_åðDõ–/œùßæs¾øâ|°Pý¾uê3ë-^úˆïÝ *ÆÕàh¢õŒ‹D¹ØÇÝFç~þŒwìóMÇç;šâÿ¸Ó?öF˜» D8«íÑÄW·Çn(¢²Sþcù§¥¯½ðœSðšóE÷ˆx¾Bš?¿¯=íœýáÔòÙeQ=o^¢XÈG,HëÞR§Îîe„K.:ÜEPž+ªn畟?ãíW1xþ¶1²ù‹cÞ=ÃéåÀ‡q§¬Ò@æìh‚Y„=p†´Þ´îÍâ6Š™1é-Ãg>¿ïVűpÒà’È*aÏé* cþœàÙ²²*¾ß|ÜdN£MÖò »_§PÓðªà%œþΚÓ9ëaú;¾8«Žj‹iÕE0ž^wƹ/ëTOÂ'ÿ[Äý‚9šà“zÔ6°8ﺀ8u6À™©|"N¨D%£‚8 Ú=,b;UñÒÏþöp–W £X%.ç×ÉqgœûWEU¾m¨(Ft4Áê œº !ä†eÒª×, n£¨‘É18“ŽÑ%±$ Õ Hž®_§|Á§¾í_,Ì«wTsævÕ>îôswê”_îÝg äÝM^ÅT °ÎvG¶Å×°LZ}5 ÈE£ŸFf CÄ •¨Ê(ƒA€8r:$h•0 ÒØR¸Nç¹¾ïí?1à!Ž9ö5oæ;ýœe¿}¸ÎŒA ô²KÊ×RÅ:»7>ÆÖ°LêJ]Hœ:t™É–ˆ¹›‹Z’‰J¦"aODfvlYÞ<1Ý}ÖgNûÍæc‰É9´½ LÃY³“á]äGƒq“×{ZTP-À:Ûq©–ò¾a™´új‹F?ÌäÞ‰™tŒ„Š”AŒ y9ᄉ R÷–mœc+ŠmºôäøÌ6Ô(ù!ÖQ^¶`Ñ|*úç‹›ŒþT °ÎN]`î‘–Ézë¬]<´tY«Ö©%N ðF¦wM4£fÀ"2@Ò QÉ´Q$ì‰ÈÌöÆš]# ~í*_ùÜ©o=ΠsD´»Ÿ#‡«ÂìkOûÄ–鿯ç>ýõ—$ Z€u¶ã8²-·´LôÖ\cIqì¡_ûÞ¢À”$7¾™¬æDL:†Î{ƒAˆ8rJ&0 ¤=öú¤¡kö¯GŸ²<é‚SÞ<§ß*¥{ kˆ«CÿúÓ>~ÐÄD÷6xJz6–»ìXì³+~­G™ØËH MÅgLÒIŠª—Å@º êñlV€ÿò¨D—Ü –öæzÊIP‡',Ž0d,\!ùãƒó yû8ª>öÖs>óƒÍ±Z-œãÎøøËážÊ?ª×ÉViFÁËîn¤³¦¬ºaÐËÿ‚16lzjˆûý÷عX^ªm³ù¦¶ƒ K †tt ½ò‹nŠÃp b—LÁÆ©^2iJêÖê  C¡\ê=Ö|D|±fZ$ °ÛÜŸŠ(Ä‹ŽHI¿ª&?óò©oÀæç¦­6 çõS'<Ó|ƒË¤ÕÔ>ÏiÛ]l‹Äa£;YêI\7*Zuq`³îÚk»o¿ áöØi[ès,Ž]1‡™ü#ꋤ²©‡ *“ÿ¢Ø4=VðŒ4D4‹­nê#aëcÒBAý:Š9 b—üÈ;‹¥_íºÉÚ÷}Še£ß® çø3Ï;N|ŠŸz°ÐµRº"˄؄Vðõòꎦ½`Å„3Ú* (lÔúÚ÷áG7ØñSvÜ6ºvöYOý« Âi¤¢àHõ8Eö&º9õ48g^¹Ô«mß;l$Y3-ˆXÈmnŒ·Bù!(ÈPôýh5¬ìäàÍo9ç‚9¹§Ý¢_8¯?ó¼ ª¢|ÖJ립qüD'a*/Qº“ù¾Ñ¨è©!íÅ ½;žiŤþ€§ìlþ·ßr‹bíµÖHz ¤lÕ L$3ÉY¦¡Ó$l48 ™±©Ãõz6Ʀ…®šUÈ:ˆ¼}r—E—ãAÀD‹Ù_§ÍádFò=){†Ò‡aØñÄä à7Ü`½bÇ­Óà‘‡_¶%ÛNÃÅ%Î$PÎ ‰ë:ÁL½QI ú²+-Ôå8Ìx Ä“Ê'Ã8‡ÊUà rv™ç‘Ċžªvºg-Îvj#!åÂy͉[÷ g~â_¡*/¢²gÒT™X^Aó&†jÖĹRHÍ \œéþx™–·=vä÷;Ñœ8¦ºÔÞlC°êˆt A„ê1<ÇGl16_(£#çEc–“¸è2n6T«ØçsÈsO‚8—¦dÆNyû'.xбG@,º+^ê_nZ®µò;ðe¦ƒÚó—…áXÃÆÖ, n£¨‘É–ˆIÇp¾€³iyÛs§íxUŠ YÓ {"ÜŽ :¸Ô ~}«0ÃgCñžq³abÃ,òÉ4®#ް«éÉ3öòFÕY0Õç„©©u:k®ü8MþLdCBVeh/Ú¤‰”ð½2Û¦çžYè°BnºÑÅVO¨gOK? øêÉ4‰sRCT¹ Ïñf­`lGTO³„«Rñ“q³¡€¨«Ï!€s<Ž­E¡€ÐÐHŸð(íó¶ó>¨©I,ª…3]nð8ðŒ¼ÈƒëEEn-0I•UWD<1)Š4ÃÍxïð =wÍ@i¸Çðr­=8úu‡·½ƒëÙ nÀ¤»lVqžto–“PÈ© €¾27©zL‘ Ÿ PÌÃF櫇!ØÙGv§œE³pŽÿàùß…#Í¡X0+’Ö“ŠÕ$¨ Mú5#dÙ,y1ONšj1°J©íl6u×@ñþ»?Yѵ~wzŸ“Å(3‹´ÚÖ¾fÅ3@pi‘ íå9-žÔ¶T†ÔÇH¼i×RåUÃhÌF@hhx1©£Å¹ˆTžtyÇǾðBÚ¬ÉEñç SŸükø%€¥bi¾X™ØˆSg¨‘™”EÌÝ XÔ 4HT2­Ô¶[>¡Øtà tXëwØj‹b5×(–-”e½÷òš>Õ¿Š‹Ô‘h4ŠÇudCEÝÀbP]@œ:»GîEt’j¢Z–Õ»€þžŽgÛ/ø#ΟO}òCð™oæéÄBø‡Œ€¥Ï*iÎ<iלˆõÜ3a2à€4 hïdk&±)åxÐÙ·ö£ êÁm_‹Ý¶’KƬ5âtÒ¥f, c§"i)¦l¼ãóò¤Ê%Ê ‰zÔñLÔo@hh oREAïšÓá”8†¿Ã" ønYúŽO|yè_Å[Ð çÏ?øÉ·C²§s!¤Y±¸È3q<1i‚Ó´Õ Lvɬ(#Ÿ[óXtdœ^/ÓÔÎîø>§±°ë ¹iêNÚÂñ‘!ÍÍq \ÇRöZíIæ±É(¡£!Ôlôa~z23ªGu0e €Œ¤²UI¢‡—üCß eÁ.œã?ðÉ—Ám›>­9cOuê³Àˆe]â` )©3a°f›RŽÁÕaÀÙyÛ­Šõ×]ÛdmÄSì}x»éáÒ¯)K@¿ÖÂ%=?štÈÙfdªRÁ˜š2e¨¦ÀQM¤ßG°51¥zE Àœ<Ç—œ±^®Õ}Õ»?þ¥­3  äÂ9ჟ<>Lÿ×R*ÈäpìZ(°–×—Ëa‘ æ`€3L¯¡Ó$†`ŸÇ¡:ÖÛŒ³^?Gô‡§¥·ÞNK£¡Æf8vò@tÃõ¬î¸›ªUо‡ÑÌû0O@PüÊë1BŽÎ‹(<ÒÕÔöYó"qÖSËð@~e§:.³8ÐpÁ-œÎ> ø±Ù§"»|,yž=\cÁï°JªHgE°\ä/9^&EÍio0``´§ø^ÐOt:Å~ô½ÇìAîïs¬‰'³ uǤË>D%19Ù$ ÿHœ™H*uq`£¡¥@r,Ž]±êi•C2ÌÁCì¬ô"XÁGTùš€p°àN·šøšåÀõ±‰Ñc‹ ʦ6¡À¬¡èC°f¤êQ8ÿ0cMDYàå4ømÏ~Ûîþ8þÍãR< p,ƒÛW¯Å(²©y#LM|5âÅ v¢¨:õ';dÍœÁHú3“IÉð@¨£á BÖUîôŽó¿ô<Îb³ NG¿á¬O]åBµÊoÌ «A͈†‘@´#hÄ«Hû å`TÔGS ôwRÀÞ~«ÍãiifF³¡×gQ Åzuqêl€72“{'T¢ÈQAœ‰rX%IØQ2Ÿ°U¼ç"5;H¿`Ž8o<ëS'–^éŒi(⩇ ‚™Aä³÷Xěޱ7³¡8í#4±Íô¾¾h–LN{æß·É¬äC<-½Ë¶øÞÕ¢ d^žôl̪™1GQh# ,mdR3)ŒŒDª3'GôD!ÀFåq(D{Åùíd ¾øø²ŒÕ÷pA,œ7~诎„;,|,Dæ+ ܮѲ? VàÚû“D>À…áµ]É)pÌ{ï²C‹gжû°pRÚAk¶$ôX¤­åÔKû î[æÇˆÀ4K*¶ýbMŒ$(QÞŒ*‰Ž× Z¦„Óñƒ“hÅiïÁ 4ÃÈþÆïüÄ—o€ÌÈš÷…süÔùÛeõ7\ˆ×’çòø#ÇW,áQŒ‚å2¶jŸðGŒÍJÄø‘x2û ÖÓvÝɃú¦÷Üa[Ârþq›jq› +ƒ{¡¡¥2™T %+jWuVíÑãJ-D™r %lçP­1r¢SÎêœy311ù%H®CÁ£G#aODÔÇÔÀß*èHtç‡ë¬µ&\ 0» Ö[g­b«Í6)n¿çÞh4ää½±€8u¶hµ ÌGB$*¸ôƒò÷P¢ fD;D%ýO » ÓíöÕ¬öð½/Pz~^8'œõÙó¡.ÏÂ4CªÊÀ¢é#"(Gƒ¡¾â´ïY¶Ÿ‰z)ˆ'µí‡èÛ=üh_8ïWfÛvÇEÔ½'~¢!l4´„Ø`HDª3«£ å¥G”uȵ18d¦¢Ìñ}Ô.ΡZKöë`Râ ®vxû§¿¼K×›3o çMgý%üüùôÿ ðò‚Ù®˜‚×’0y¶N1>9ìFÜy2AwÜYÏŠŽKøzÚn;2d–[Z8æM Æ!‘} œS©Þ »;+š¹äLõLä\X,æDQ ¤ë=é<î¤JÃã\fl6žÿ8¨°¤;ñìLqÆá¼,œ—OMÁ):Ÿ´¤ ¦e¤Þ%O:=ÓbÍ´H°Ð½Ä“ù,°hȤí±ä çfÃõ×+v€›p Ó¶ßrsøüg²¡L³E†c×44í‡)~‡EÒ‹´¢ãEA‹°qx6”P^a†5„m¢âªµ&¤ò\<àƒÜ€¨YS¸„«V ýöó²p¿dãóª¢Ø^ƒÔĨ‡ ,Y=cS‘Yµj%ôö‰Cî¬g}ÇÍgE®ñ®™¶x¥ª ÓðeÞîÛe§¥)BgUBL%ãbF¤¥™â6‘W1¦àE§¢æS@æÃãsš\€NšGäôj.ñE¨­šªp-.BWðâšjc•/œ>ò™Ã:EùM®¾HPÒÖXËW‹Õ¬Ñ\,Õ±HÙ€ÎAÎ#ýΆx¸Ç~»îà4fOîJ Çé‹4ÿùBqX$]Lœ3zj6êŒl°ÞºÁpnÖ°¦àµAœCñUCy†‹|û8Z¸‹'ék›w}ê«þ:'‡j&WùÂåýåP俏„ËÅJEæ¹lVq%´‚Mpg=ë;®/¬²“¸D 8í _›<~ƒbë†û dbO¸\ÇÒBÿRV™1‡z¨Ž™–1PGT)CÐè­/9¬X>"¸â´ÏðI“)ÕIóˆœ^ÍÅ">¨²US®ÆÓ ]W ‡r¿šÙŒUºpÞ|ög¦ §'6ÇÃÙúâj±zám²}í@Ak‡}G +ˆ0¼aV*.RÔ«½é(Cû¢8pÙ}vc&±ÞÚk[nº1pÐ~ÖÔ%õ°Á¼—þ´¤B‡‡—Å{¹áŒä*[8o8û³{VeqfŠˆÑâj’ÜSš©&=ÈS§CQ!ZA„M;SFÑœ Í±x4OèFOàJhµÜ«ßM_®™3@£3ˆ!ý¥ÈÌ–á%0Ñ19YHz×$\¿ß.Û“Ê!ûîQLLÔw™4‡Mƒï¼9Û˜ ],5¼Í#æ!À¶«†ùh5ì`´Óè¾kõ*dj8YgÇs šíKu²äó#*UÒë86axÈT\¤¨‰j}RêâÀFCK:ŒÝfóÍà¥Úãœ×áÉݶۊ‚³ô×P¿öà,Ž2Uͱ„ Jž ØGÎãà‹xÂ)ö8h±W¹Ú†žH€û8’¶pÞ€ d¬T õá ;<“É£é©ôð ä»×Tz0VÉÂyËG?ûjÈöÈæ8RB©À€Ä¤¸³žõ3n6ô>¬@`ÿ"ÛZ4f# t.É áM #¤¹í;¢“jûíá&kNÂiiÏDšB€M=8B’XaØ+Î÷„lßà¢Åˆh{Þþ{²eä½· 4 £±D¸pMG€$ºi6ÉZoÃΣèåeB×ÖS•;pÁ}÷cÎÎË¿ñ‰ª*Òwl´„Y±ò:p^RØ Ùz¶%ìøG"e£¢‘ PlÀFæÇ¯SLøãO›ƒ…ƒÏú»mG¦S>:¤-K(a›(†A€ÐǾr´QüFðùÔ¾;o'Cð¢¶U$>Õ \Óp9Ý4—ì£Õp&=u>B,*üú®ß÷©Ð9_8_ï(ЬX¾¸Hs˸ÙPQا²&ÊU=ÕÆ”¢1¡¡i “²¢Ìˆ‰T‡v0‰f§'mI_}vè‘‘»Â= Øy2©ÑQŸr %l¥‹CûÜþšK&‹§„o£²•çðTYXVŽT:s$ÑY\ Ѐ £,7‡"f<«ª¡ú"AÕŒ2|Ÿlûðà8Û±ý™·sºpÞrÎgw†$N£Z¤X%/ŸÐy±PìZ*-S$ÊL$x.@q„™Ôr¼ò¡Q*°úW„ÍŸ“dxê);ULõŸ‘h)Àœ|‘í­ç~îU·x&úä°2ïY^IZLj„Î*í&Jj‰Š@72ˆ41I-SÅõ×ü:Š–†£Ú£+Wö0A¥ð — 3]‘f tæ,ƒ»ápš|Û'nZÜtçïWÉhF‘¥@é[„ÂŽÒ8 †E{aRSU¹abö¦æäˆaž‚¡R¸Tþ°˜¢ÀF@è3Šéa(³B„°UÏü›^„›ôÄ!0%j-EG‚zêŽÍo¢ƒîxУN2'±KâÔYdù’˜^J%ƒÅ&ðS‹[o†—ùÌÜÛO?pOõ ư²@VkHžZÃËñ.®šˆ:“:Ÿ¦ð0ÀZÈ< ÙÛïˆo˜ùÂyÛ9¼¾2Ÿ’I<æP©7 ¿h˜47¤yÚ⢗,àÿLh~›Ì1.·AËL¡;ÙO†¢—¤$ÉÜQ´p4è‰wKô,\à 0‚D7UcK†vÙæ‰Å=ÞJ„®²2 CØKk Ï㔎xK–L¥¸™ÊÜ« êÃ@£0sðyc¼Üubä  þŒ´z»Î£AŒð:lÅ›R=-Uòá°Hz3ªÌô—×t@ÑásT0K:e÷ØEÞ{ k®ÛðujGò'\Ÿz"Ûµ¤ëªá±èçeš3Y<ÿ8Ãf-y`ç€c[ˆ ƒQ$œ`ÕVMÄŒ9k+<š,: LF® ßYËlÍ@Œtá¼ýÜ _ÁîÁ†x0&eP@:ÐÞE),+.ú—ŠàðHªêy :^dZÆB‘CD“èBq¾þÞpÊv¦7ÑÉêð–†+•-i‹GX™‹Z5r<\Ûî[½ñãÖsœ™I|Ç×ç9cìØÊëã°Ø}Í ƒøÇšQÐ ƒfp1ý”×ë<‹ªïÓ¢#]8p&ím:/V#byfª§…JÅ5$ÞŒ*…òfÅE­ÅRëÁ,éGícßÒöÝyûÉܱùåØÏÂJ9h5ÁfxŽ0jíûäí- gØ0+¹L¶{+"Ʀȑr-7Es♌õœ®­:x¡g1=#[8o9÷óžŸÃUðá;ïÂÖæä½ º#?°QG Åò¶M\ä5u6\]º³&œcÆ®ÛÈiiðãw4Jœr&סKÊœt]™:p±Ý /ÓÔü>p•^ÇÆNÕ‡Huèû9/[óX̉'ÕõuÃK5ð:f5 ß@Ð>—z@ÁÈÎädùFNÅy“Ø5š¨P‡EÒçªJ¡À5N'/X²Ì¢µ­}‚6R Kæ"ÃË4xvFÛ«ºm÷ÄÍàjixUAwŸ`Æ ¤úÀ²´³àw†4×vWBS3 pÑ=wŸÝÝ:ÿÜgƒN0pmD[B¦É„.í9®š£ƒ‘,œ¿øÄW¶ìv«×`}4 ø§…«ÅáE¨(øžZƒ:2ûÉz4ë°¦“°M”V{Ž¿ŽœËKlêÞßSí²müÔž¥)sªa–:•ÊÌ$¬ÎÏ>OÞÖ¤³!ö‡M×]{MPåLÛ¬‚ÁµióˆBÈõ¦“ã ^ß "€Zé£òÀu:Ë=§=’…³búÑãµH)Õ· É«žŠ‚–2u‡Ç>Î~(7I=–ðÁbmÀðT\,rïrÀl¶ÑÅVxíØ<5¾ÜEë!UäÐè9%+D™°VCÇÂÛP=e–wÕàõzÏÝ{·\;§¸6‚'ÈůÞb¯‹Ãzg;"e¤v¥Ç>´_Öˆm`ŽdáÀi¥×Õl[ò Ñ !þKÓôH§w±r³fÛ|‹µð¼HÐb¯æâ!Ž}>N ø(÷€ïçhÊØÇýD¾ŒOÇXð>p­ÝlnïcBú ¸ßÂÚkâËH4.Îx€ „P#1P 3Â’IXQIˆêó˜ÄuŠR<àƒüP{ÔW›Õu›9C/œ·}üBø¥b‹ÀK—‰‡½5cátL„A”Vœö„ð‘f/-tDä# (³NŒC5fû&Zõ‡íñ´ô?^Ìä•¶ci¹˜å*Ša_¦i>¸ø~*ÞQýp a€ñØœó‚®w¿ÎïUu”ãXÂ쌆 &·Ò'´pTN$~ojè…3YL¼R*á_ 0¸&l4)C&”×E˜aM'a›(]Ús$MHå¹x°p8‘>Žp´\޲ÉëGñ€£‡–=2 Fž^®,…—å¡Ùhïìk½uàCÜ'µ_ í4ú"ŸGµàKpd^ÝÚ*£Åˆuþ´çäZðèEmK¯´šÂ5" ܘð>¦fÞµpNøèt‹âµ•aÁ)#¤ë=iÍ&©Š€Óâbß»<óACPjö&\Ó t£¸Ä濸ªvñàå.1<ÍÄ÷RÏR2Ç¿½vÜV@£éðÌÜÓñY\KuÕÇïæ‘ð*mêÁ¦%zɃzKZÂqø¦pT¯ìVHº½©¡|µöX2Oža£6DgÁ k:½õ „ é¥ãâ,¹A·î‘´…«±xp‘.lØ—iøaÁ/wSqÍ-w:냓ÛÃii¼2;eåløD‘–Ìq¡¤?ÆïûämœâhÈg=ugúÑàÜZm)®åÆaNêóè@æó(,JeqUUQÞí =É¡œ¿;FVAp’‚“T²ä™T€ôAÇ9jÐÈ|ÐpKT®é0‚D7x‡­7§/yeІ×Ývm~{óéå`:-_&Ó$}Ìø—U]°p%tî¿×x]¸†ïÀÝà7ƘGÒ'!-s*\›C+Ð0‰¥µ NËÎïqúžY/œ·~æËðm¹ê0ŒQãÕ>߉]$ç©I‘I¿©L,RzäuÀ8R“¨ÞN ci±gßg§íœÖìÈ_]{3(–.œ¾?ªnqµË6øÞ„ô±#Ç‹¬…èŒê¤€ùržšÆF[[˜œŠû¨'1›H3Ó‚vúKû‰à3öÁš`ú¶è³}4ë…3±¢:šÜY"0RºÝIth_«@ÐwvÅ>u€át8q5–fèVà‰ÎDñÔó›Täþ{§»Ýâ²ën¦R-_±¢¸å®¾_V7Þ¾Þ\KQÔ‡ÙPî~C~èiˆõáWåö…Kq¸G›¦5í“ á u#eqïŸ*þøàÃÅg÷Ù^’¢öñ(óè ¼²ƒcùí-wxEòlÛë­Sl¾ñÅ]÷é™T—£3¸2À/«m4à•ÐÎd_ä³÷Ú¹¸×Õ°YI AFd%J'féòG]þQOëlZF丢Xgýå7ֹ͜Y-¸ãá:Ųòºž4³«;|ÆnÂÁçègî7ßnx1«Úí!¹í÷÷çãŸÍÂ(^ÖðË41 “ˆ'‡»cÓð´ô]÷ýÉLÔö bÔ¸ÅÞðKsÝð´ý›Ž|Ö\»!ûxà¡â—×ÞR\|ù5º¦tÝ´øO5);åSÇûh °ÆžÕKµ5—®qxUU´Fôå–ö5â‘D“ÅQíS¼ëå/(ð ÑBZ46^RÃW"—ÅäÄ𗤬œž.®ºévžLš3xQýû?/ëû©PMàg0hŽžÐZ§ÝHUè;D{ ù²ÓŒ-¿ò}Ø~»o?ú9ÅÆÀ—9©ÆœJ;²ª~§È~úY-x™v(YBdu:qÒSçb=ö¹‡À÷8ðìÐBm/8ð©Úž;l ‹§ï¯k4¦sùõ·ÓÓpÓ,ûcØÕ·wZz»-6-–à‘šêèvЦ(@Œs÷d:;ˆd®~mËM_¼õ¨gë­…_ètõ’öCªƒJËÞvüvJÌjáÀÄRw¢‘pOG 6ÆìÛ.ð’ý‡üqYoo®è-áÛ–»o·%\’2üËšËàeDhXx ûy>ñì ¡©é+ëiG‚¯D¬fG›²¼¸úVø˜€J!uãðïš½aÑ|òV°pÈ,ž%J&Ãx=Ùî«ð> M1¬ Þ“·zB±=AÈþ'õ6ߎ _a»Âø}/œj²{0OŒìàýÇ&Ù©á 'Ö_gí¨²€G›ÃW†mø2-UŒ­é¼=g…n¹ûÞ¡\ìŠ—ßø"7Y“ùØ~ºct¦)ÔQñößyÛdJ ®=U «Vw?zçe 835øÂ©øFƒÉ´Da‹ÆX EA>N¡>ÖÚ¯®»ÅÊBåÑI݆}Ÿ³üèS\à8ñ¡.W÷—i~ßz|–¤…ç%â·Œ„ÁÏ/|Ó›Vx½™èLÆÓ)›p3’5ž¤dšDø’Et´ÉÒšÕðþ‡–7Ýw»´šé.œ wõ×­a`ø†_}+àåžÖYkb—–÷C³Jn+á“´î…¡BàÄÀOe=˜-œ÷\ðw›€Ÿ]ò…’&GCN>s.ˆ^êF«?ymxnÏõŽ?Ü_< iWA™!˜“§Ö~žc _÷þ‡Ü—:mGÕýSëRüxÐLZ8ÝG¦÷Cì._$Œ ³8¼ ,î€Ï.KNʘ,’¶|ÅJºÇv?¿cÊÎ9Y—Ì&\d·¦Ï§_nØÜ³Ø"IؤB`ûÒŸý Hs³€‰_ÀÖBóÅš†ÀJ-ÃÓÒ÷ôýíÝ`VôA¨ùÃhÚ(¾°ö/?ûMqåMðyÔ"hÿô“_ÂË™´ 1¯ì?l`ÏÈê{áL]|1^}¹«Æ€½­!sã™2[(¶ékÙòÅ×þïŠGè, yZ .‚–>’& ÐR!I§ú}ž–~†ËÊSxÄFpçÑaÚ÷Þ_üñ¡‡‹Kà"Ê…Þ~|õMÅå7ÔŽ\|ÂâGõH÷¢ÙäÒ÷ÂyèŠ[kgÓlEØî!¸=Ã/”¦]æú;î)Îù»ÿS\ ߈\~èéÊAtÊS$V$Ãÿ5#¨¾\Ccöv÷ÁË´Ëo¸ìÞ WbâeeªÅ訇àÌäWðÓâ{?å+h¸Ê¼Hèý%Õ›ýì??üÆ£gõú¸ïkø«%“;]<÷“5Œ št<ÈFÂä.‹án/üÓ%^·;z†ÐÝò1¿æÖ*ˆð°õá’ŸMá;?sÝâû8¨GL ŠoÇ«¥aòñ«Ç³mx‚à߯¼6¨ï©Û®pÏà?„£N¼Û É©ó°"Õnì—«—{î°ø=|­àºÛï)–Á{> ÓOÌNñý4Œê{át«î>˜˜d…j"fäìká1>¨a˜éÀµ†k"ðÓäS_udãÍ$šð³áá> ïYèõuƒ©‰ÏæÃœ6޾߄;õŠ•p"h;m¹ÙP m\?Wø'¼¥•„zëïï/¾ø/}œÅ%¼ËIkäöÀ«žö¬ÛFCŠò½¥ƒouÏK¡û~©Ö©:Û ?~$ªuMd/@ÿ2 eP:Ôx&§ã9 ’ã[R'X:\?oü›ZÀ£a_Fצ%[)~WzÙ€ÁN@û2OKï¿ÏI ìŽâhƒï8rݲù°Õ,/e¤ô”ƒ=5Eš !²¬#)`ôý í+f(ÓPì‰s²‰²¼jêU/èú4ï¡ï…SM”OJé‹ ÍÓz&´´Ü{w@é00`à€aBS (ãÈÛÅ—ý¶ÀïÿÏEC»¿¾ñ6IU« ñƒG §’ ®¾õî¡oâ±3¼ôÅ:,ïí¾ÍC¥‰ù4žI£<`÷øÑ‰\jPǯ€åÅ%¡%¾ÁÌÇ¥û^8EÑ¿¡G‘ðN‘¶Y€’G(X()‡›Ãšñ-©²>iŸE‘)’¼·Z^üä·ssÔÁ2]9q(š`‰ ­~Z7ñ¸mÈÓÒøžíï ßöJèßÞ‚ùÀ/]S$íôÈÐ,bχux’e6­;¼êe 0$ pÊØ|¡hØç_Mߎ˜ÁF}/œNÑÙ Kÿ2g’‹8ÌØÂõäd¨¦“Ù–!×)ãéÝœmP&}P@­‹/»zNŽ:ùË4Ž=òƒj¨±$6Ãh §¥o»Ç'ñ´ôfpd¯¶\9Ó¸üF<­«&*qT ž î3;:d=7‡ª£€Æ^¼QÝdÑ`ñ›r€º°~ù¡Wñ«¨3بï…¯6CßÖ\ V,sC€‹ Þ¬Ö=ŠhÏ:5XbP¡Ä>Ð4©zKÀ¢xpéòâ§WßèYCÓøi5>C«GZ$à݇EÁOö£9-ïm†½z|Îv5䣑aOÍ'„t=)ºJçÑ&Å9!öÛûh‚ºêßÙVV”¿&]þ¯Üã ã¾Î{¿ø]¸_S±FŠO£ÒžÝêˆzØp=…à¤Ñ&׉‹›J3Ö_$YÆ &Xìm.Û‘ŒtxÒÉ0LM±ÅWoöT_ ^{¬Ë9¹„ˆÑîXŸ´ç¢´ãS¡Ø‡Î‡¦‹}jÂÍ‹›.Œy‰ÈBÉñbôAØA~vÍMN{väå7Ü WÀÕ`×\!-q(•zñÃ€ÄÆ1hÍ÷ç²ëñ‰9-NJbnì$I>_(u—8é;®ƒ#%s(Õ´Xœ‰dßkèfËòú©W>¨ÓÐ꣯…39Ùë?0˜æÆuJÆ‚õn —âjÏ©²—ºvŽë(é»7LFŒ+i]|Ù5üp8ÂKRÔeÊD}8cÊÒwÎÚ_‹7,œ¯F/Ó0ÆÖ& @Òù"©«i²ÐÛ܃ae;’Y*>ÆTC«(»G®SU_Šz³õµpàvPK¼ .’K£óˆŒ¶B–ŠœêU×NЉöTGË„=Y$&‰ÕL`z ¦n𚹟ç_¨é·3€÷J7Óg/`uèû†EÒhtæëåÚÝríWŠK‚å %á”r 7Ì‹“Z5Â^jzØ’6Ï£ˆ":ÉÐKºŸw††"ûZ8ð59¸ŠB•Go¨Wû:çõs™aM+2 QÍ ì| ó’+~7ë£_9ìl«°[ÿ Žy x_;М¯…sÅ ø)ÄdaIŠ1 ÞéEŽddޝH‡7ª¡UäéÃЃÀ9ÇÜÊNqÁûŽ=߀ޤõµp:øÂ½¥YE!H-0@} IS¸A'I‘òz¡T¤ãQͪã]˜QÓ‰^ØkQÜGŸÓo×°o‚šgñ¯Å… Ôé£Ã›x¬êï,ád_ßôô%Y¸=HØNjµ3Ñ´ih¹DVÕ%-hïLKKË!Ví¤C“}-œjz®·À–GƒÉ…’*mDj8œ>*ú<š -yÕòª 3Ø‚çhA˜‡cd]zùàG¼¤ýî?>ú\à.:‘1ÈÁH_pðaÝÐW½6x& Ϩq³àa¨ñ3))¤ôˆãVEÀâÆ ›C°„œÌ`ÀFaii©÷FÊâÛgü÷Ã/ôHº¾N§¬–ã„s@¡C0R•=³c~‚är 7\KM+.%†¡ºQ·I%a-&æá ËÀ£Î/¼‚½LÓ ÈWsyt¤’ã19×Võ˵+nô/Ó ´#íæPAà²@R5´NŽÇ1µ\ |èE¤û€³–„Þ)?í†#!ûZ8+:ðk85wµè9!‡K©»§l­NEHÖÔ‚˜Š ÊÔ„Bz=f)Æ÷Á%éâWƒ9Ãö+E ¼ñ=œéËc”c:t}ZJÇ<0<!Hpý/žá¨s#übtÛÌl>-«{oØJ@ /0 Ðxµ4¾d›ËvÕÍwÂÕUèAë¦=Ç‘vJ…ÄmB'‘––ç’í¡ýôˆø$beÀk:dF™‰Ožqì¡¿Øú^8ðÃ;x w-%oÉI¡,v‡vu¥Í›%¸õBóÆ³ lÞACËëðJšèÌì'*r º'ÚetK$5ûËéCBÕß`W¼ÇD|‰7Óá°4†[A¼¿òËmÑf}„GOó¯„ö×ïÅŽ°laŒ³¡Õá>óÄ<àLšAÃð@¨#óÁH…tËåsv´AOý/œ‰O¥¿öØÁ¬†ï{fk¾ÔËc”6ú>E CÃÖð@¨C3V7Kº¤ãð¨í‡-GüÎÍoð ^jŸá¤Sßq‹%?š´â1æT(x¹6²¾k.~äÑâF¼dHBe€´wjšö6ŠÅÞ5Ï&ÚíK¤Ë®FŠEkƒ#hn–êUTïž:öðtYw0:šAß ~eúfÚ—4Òà_™ÚƒPHÍYçß ðvT]{*h½À^…hÅ«3gÛD)$VW¬ö¤S³ ÷¹^VðwR¢ ¿ÚLßÃl‰W°/”Äæ"ÔX ±:œËÓÒWÐmmÕ“ö© -<㬤ÈÎZó Îd°êÈ|06˜ +:ÀzôýÿíÐó3ë#ö½pÊNçzö^ ?°5gEÅâ"Wš|OÓáË«à¬'ب3W`oij9ž*®Âس¾îð0,uà‰#ñl7ÖÐXò…”h xŒ™â¦²É,®…Y1 ¿ô07§¥ùlšD«¡i/ó¢qp/Xì4~ë™H3YÓp:âÄŒ$»fN\h­'„G |¾4ræfÔ÷ÂרpkHLšd¤ûmšÿl y¤ÒŒ zgØ‘ Œ¬8êc3yÔéDi-{®–¾÷bË7HøYPxýÛ2¸O5Iê’G‘£Æ­+$6胰Óqd]¿Sþæ´—>û}uÁÜsz©F'ÊâFÞ8%Üæf×§ŒmJ‡u±ÀéÁzMhå‰GÓW‹¢¸,eñY‰šÿ7¢iÑV=‚æxSk˜;  hÓD‹鮽sø‹>¯¸YoÕËFk‰ÁÈ£!ÕÁæF¨ßÚÈx²Mz2‘fÚ¹Nç`• Z8UYN^¢IaO°é£À¬‹Eå먡¦ž5Ô6ë¥×5¥§x¡,G2KÒgCÒ3ªA»ˆð]0!Æ3|p®2PÔRqˆfMŒ eרµ˜båëïîå^ —ð˜i"ضGRGFêjß.ž,q©ØÔ2ó,1<Æ&`è&ŠâÜÓ^rðê:«†3ÐK5 QÂ*íºIDAT©,Vþ°*;¯ï'<Ì“²e¢Æ÷­æ ;Ò;JìD™¼e2Ù jC‚MŠ‘|T‹£¨£ æA`äøN¼ÁûšKžBrw5|€»ï#‡;fž&úƒ‚5:¼#[¼Õu<0è—KNyéÁ'{ùª¦>âL/)þµ-HLŽž;a2ðYˆŸ¹ÚÐÈg }†ãg®^j‚G=²ŸL8kb5{wªêu¸±5tË. «ú¤ íÕôÀ²Ô‘–?˜±ˆ76™S íYÓi˜©ß qÁ•7÷þÍX=ŠhO1† ü@ê`‰Cˆ”»…êÁB#À?€f¤!™‰¦ËoPÜ|õ/œ¿òè»áóœÿàT`r!y}p!z¥"0©)j†bE Š 9Ðq”YC·ŒÕ]S5›uTê{±#¬8ÿ‡tÖ‚˜)Î"hu$^?˳køÛ¤7Þ“îJþmN`„~z6 é5N³ IhüÔ'±H]ùs<`4Q¼öý/9D®›L6V55«ã|·Sþ#|?白ƒÅL¡ÅŽy[:sR`ÇáX¬Ï‚š¸ÆPo­r¥q¤¬¯‰™Qc›&4ÂK‰V žÃKOu\¶0®º…OAϼ@ЀXwNÙâ=@QäFŽl2 ŸrêÑöM²UÍøˆCvº 7®Æ¬á!Ï*öäÂÜ,/ÁRuUG€(r$#s| @:¼Q }—9ÌLx,# ¦dhyÀ˜ŠÃ>k^„Ф¡q$í E0ب#­‰€‚I´èðËá þð^gÐvÕ-½^¦¹XÀ™†åãˆþDb@ÔoDHfÎéVYQÍR¹/Ÿúâ?;§™'ƬÎY¯:êZøÕÞתòK5™{I7棕>/²D‡{ÞÑ7¢iÐŽì"3*:= (Î}CÞsn •C$œWI19gÄL¨Ül J¥ Ý€/×ð¾qwüQ›Øs‰çÓ“<)åb÷`k„Ü»š*HMZgRUŠNy×û^zðñ[ĬÆ 'd¾Þœ¿r¥÷ÅE´ˆˆ£L˜í}Œå­$¹—`H\êÄpˆYbź n޹¡™èÄ’c”ç­X0X‡î싎WàI³/d-2õ#ÆÈrM’™‘•åt˜dÍ%àQÅðHK+'Ê”^(ý¬Ngýuþ2Åó™‹<òbÛIæ¿À€¬.ª¡=hæxSËʇ^DÐÎZ:8‘ÁTOEAE™æÄó¤Ó #KÌà5BOÂhyÝ.§¥kàÆ•ð2ÍÇa"±@Š,i^OkA€ïtTqá0–r Ÿ<•‰grÔª»ê9ÅÛ›šõ™zÉsî‡ð|%Ÿ…”2—Š « (d‘ÅÇÂP\”å:>D x›K«ÍVm¨ Ú³¦Â©7Æç¶M `¢uã ã€åègü[€.Á¬Æß­&¾”Ræ"i™©OÂ0!œ» eš9ÇÔZæ.ͽYMÕ3Ú)›zh*R(õÊ4'Æ0˜ç Í®Rb¼Fø‚to¸xÛ×¹Ÿ¬vŒ+ñlšÆ“ù‹R1WCÃ!9›L¦ê…á+ûм@hèÌÒUuòÉG?óoƒê µp>ôšÃÿžà³…¬ò‡%Ê‚XÞ™ñÉ bWèNàÿ±¨æE¢”ÇÐLˆ†=ÀÄ D´n,(E«nÖ?œ°{fŸÃR/xÄÁÓÒ½Þ3áÊ[ÓÙ4ÕåÞÈXRVͨjH)‡GqhjH{i© mxÁ”Oœò’ƒÏ5ö$†Z8˜OYÁÌ%E.@ÚA¹P6RëÀ÷ÁLVdC:kÃGO1VgÓ˜ &‘EŒŸNcòO;Pº£%qF‰7Eð4¤Æâ•–ÃM<î¼nïÝ£ážx‰ŽÖ‚,™Pl2LöTCzI»¯F|϶Íê[œÇ\ðò5xyöƒ,Pbè…3õªü/(ű¼À©×DJÐ* ŠÆ³U.µê¹J*‹zØ€rücM§áL1>1L‹ó¬ç¦>Ø–?’ Ý.Õ¶ºBßò¨ûPöEqý /×ÒË´^†Ù£Õb°,¤CkËRQštr,Ž¥‰juéÉ/~Æ*ÿRš†1H?ôÂAgp’àüP\«I?År…n6’ò æx`“Lq$(Q†BgÓ|0Ö ªOŒŸÙuCòO;îdüpâŒo á%4@j,Q©®C@©Î =ÎJ¸¯í5_C`m«! 1´æ@¼@iƺt\hŠÑ> XÙУ¸ŒëgK–—/Ô‚ïF²pà¨ó?!Ó_¥ŠkU\þX ÿ r)Nûˆ§Y +Ú“.{sÎ=ã#¡Ô“ö1(ã&…ŒÒÅ¡=ŘaÒPìYâR´{‰á‘°Õ!÷itü"¾kj×À¥9+§ùâœPCPGW¹¶áHC|^2àuXÅl‹ˆ»@Ê«ŠÉ%/z÷±ùS~b`av#Y8˜\GôKQ‹½9¯ …†ååå© *†wNÌ# C¢ucA!£½ÑôV ö½Èm#A‰\×”…k@ÔO ÕI½P††¯kù0ô7·Â÷n@ÒÖr¡S=ê[ñ"h0“/‘ ¨AMo˜.Š¿÷…ûã·êMÙÂ9ó¿öÍÎDñ/\`.WË,E6›+žŠœ—êi“DØR­ "ð8ë*Møœò „ ª´6±çö,ݹÚ=Äá-'ñ¡zÜ»‘éÙ ®"¸;]ñ¬¼•Ô ðãW±9ÛjXæFiRʱ8––‰x˜f©Ðr|Y\YLV‡zôA×Ü"ÌêêèÖ¼ºÅ”ÿðš<ÔUSÚQ5É øÜMjÍHŽòc;¼#=ÈÑ‚èŒ"7r¤3èHLü<'¿Z6|÷^C³f±ßÌm׆‹Èš©Äê§+ªÎ‹N;âÀtŽ<×_Àã‘q0Ç3^ñüŸÀyÙ‰,–j˜0†€}d3e²†‡E:Ã#´…v4áÎçᶦáë´¨åx|s× y_‰? âÝ(M(uè{Q÷,Ô¡ðbÁ‚Nˆ¶@Êò’¥÷.ÖiG/ÎEƒ©Žöˆÿp÷ŠS7ÝlÉÑ@nÖ”šÊhæ¬e@;eÿpÐp†Ï†™‘öE‘923jÑ#a0#šÑI<»¶Å†ëýà²åÅô3òªÛb¬M5Tµ¦¾¦q$˜ùÕ÷õô×5™\L¼‘q0ñO¿óˆåU§s¢‹EØ´‹ìZS ÙM)9QÅ Ä t– Ò¦€T ßµyNKÃy*h¿Ö› ²Èüä‹Ç¡eø8ŒÎ!)`€‰A¦;eyâ{<ð¤àgædá`=V^ý£w@±.¯•R\+p{ýA:Ÿã¨)ÞÜže$EU”kÀèCu¸ç]Ž,¾Õ0 ÂNЦ½Á‰-xs…cӌƯ|`ãUwÝÿý0ŠC ΂ª˜ 2™Û­/”¾=)vQU¯<ñÈ?nþV«4gíƒß¸èéPönO è"°ÓqdK^€èŠ"7rd³äftc Ó¬ÖÌ%Ë™wóÝ·Ú´X{Í%Åϯç_WdøÜcMŒÊÆ4"˜ôB ùyâ™ï=âi?ò˜Õ¶ç*™}ã_߯?ÛdŸžËúŽ@€ïÈ&óÀ«ëx`]¿7Þë¢í iq4°›Õš¹Ñ!Ë¢ æ:k,¡' ‡á—¢NÝnaGOcB¤a€uñôòâµ'¿ìÀÛŒ¹3WfÉžõÍ‹>UUå;é¥Vßö$4¡#[¬¢(ŠÜȑ͆`0#šÑA€N³Z3—,g¢¦E"Èð¹Çš•iD0é† ÂFB4¼þÿü{Ž<ð„š`5b´g?â$Ïþæ%ß­Šî‹šÍJ.G6«èŒ·ëláÔ ödP# G.fµfnx°…R·[ãFäÑÓ˜iӈəp¹æ‰'¹ÿjõ~&ÏÇ#ÿ§É òÖX¶ü•®³ÆÅð…ª§Ù.åæÎ‘-&ÑEnäÈfà °0ȵ„Àjà‚b3—,f"fLïº&ŠŒ8Ecá-ÖhB¥MMÞ“Q¨ºÅëN:jÿï÷Ä­&Âþ*:¢dÏúö¶)‹G/‚ëBvìmRÂj‰®Îî¾k6Œˆ(µÝ,iæ6G~4Á€Í½MIiÓŠëGf.ZYMþù)GíwK?øÕ3s…Gœå¿ñ¯OéLLþ?¸?ÎæÉ4„Ñ#’(r#G&[9•í¢=u„Àjà‚“f.yÏD<̘>Ìš¨Æ¨{3ˆÞb&TÚÔäC0>tÒ‘ž1„þ¢Tí¯ê#NíìoÿÛ>p¢ò{UQ¹Å“œÄ ÜÈ‘ í)ÌQºEìfI3· œ-Uu˜úš©Èˆ#Q3¦É^F"m2鈆Uù«r²zç‰/8ð?FdqQ™™yæ(|ëÒ½«‰â;p«œ'%.G&yNe»hO!°¸à¤™KÞ33f=LÇ©ckcáôë$¡Ò¦5§,§NzáµÙÅd¯¿™™£Œ>ü­‹w);å·àTõî½öUvÏ¡ZÀF´×V 4s›ØÙR­;¯™ŠŒ8ucQ·ë¡›Ûjd–ðxQnÛiþr–&VµU_ý¬tg÷¢'t¦—|¾Gò¬(ÊvÑž‘6ÕÀÍ\òÝ Ê¢ˆ!â(è„A³É ƒºm5O°™±FÄïoƒËf?È‘‹Eef~gÕê#ßý·¯ÓÅ«‰5cT `5pÁ\3·ÉÏ`‹-DÛqD¤Q* î‘6A6_x5ð…rµN;éùOåß™¯@˜ß™gs|ö·/‚Ûžž]6„¬®¨µH؃-”º'0 ¦ä#%ØÌØFsÉ,‹û»Ýî1'uмýÎæ\¦7¬í7cç|çߎéVå…Ø†–DÙh3—ô2Ñ`‹-Dq$‘ÓÔ;B̈:pž9Y.t{Û‰/<àsóÊ‚v?g_+˜mÖ'¿øàoMNt÷…»Ôÿ³]NoÆp‡óx–иPüŸ™P"è„ùÈ9Á­fL5šzÒ‡~-‚/ûV« ·(¸7ÁGª~¿þxÑÌ<+8“ ¶ó Ÿõ|´`53¦Wª‰"#ŽDјFx‹66 ˜…<€3›_Y±bú¬Åx·™ùªë‚Ÿå|÷Ò½Ëjâ\xrži°—]õkœÀuzB¤M-²Á·àæßçô§ÿd‘Å=ïáμ—Ì{ˆÀ9ßý7à ŸÁh“R-ƒÈˆ#Ñ4¦Ád> TÚäâÅ7.‹¯Nå§ßýÂ~¾ø‚_÷·ç,ŒX‹¿üþ·lzú ¸HTîf_¿ÎàiDkF„H›VÜbÀQîU]Йì~î݇/¾.´zϼ'-´ˆ!8úì<Ñ™|o·ê_ Ï22¢ñŒ´FúÃ{ÝEA—ÅåeU~±³ÑôÞ}Ðâ¹7óB¯í¢Þ[>ö~´GUM¾«(¦ßà+­5_í‰d'ýà·‹¿ét:_}Ï öÓºGÌ^°¨ަ}Þ÷~´MÕxSUVo„—qöè±²P´ðëx—”ÓÓ__¹lÉߟrì~LŒ¼«ÅÂñU9÷ÿë8¸[Ü«á5ý!ž¿ºÒe§ø¯îtõbbÉ·ß{Ä~ׯ®y.´¼V»…£Æ÷Að…¹c!Á—ÂË–½”¿èû²œ†£êÿ…Ç?Ãã{'½è€}N‹0Õváø¹À÷Bp‹ª‰Ã‹n÷Ù^¶Hè_Àç-—ÂMÊ~¸fçÞ‹ÞyÄËIÜ«m˜‰…ãgOi/¯ŠçÀÉ„ƒáûpŠö/Ÿº¼¯*Ê_À7dZ•Å''¦ÿNß7ÿq#ðxÌ-Ÿ<Òùýï¯ùH±á~åtgïNÑÝ®)Û­[»€hã;ÒqYÂÏöUWuºÕïªNñÛª[]UMTW¼÷ˆgŒß§Œ´Ðscì1¿pÚÊúá‹~²ñÄòb»N·Ø0[–ê pä¦p¤Â«¶×)«uá2âµàÄïpd€oCÀ×w ø¥Íjy§,–U%|àXUÀI ø9´ê0¾>wºnRr[§xä–zÎÚ|ùã Œ+0®À¸ã Œ+0®À¸ã Œ+0®À¸ã Œ+0®À¸ã Œ+0®À¸ã Œ+0®À¸ã Œ+0®À¸ã Œ+0®À¸ ¹ÿKS³ 8enÀIEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/docs/img/logo_icon.svg0000644000076500000240000000236500000000000017654 0ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/docs/img/quick_start.png0000644000076500000240000022160600000000000020223 0ustar00erikstaff00000000000000‰PNG  IHDR€à5ÑÜäsBIT|dˆ pHYsaa¨?§i9tEXtSoftwarematplotlib version 2.2.2, http://matplotlib.org/†ŸÔ IDATxœì½{°$Wy'øeVݺVw !©õ X0XÈx­qÌ‚C¶{4;žÙ l<ÆKxQF±c#c$?4ž1‚õŒ 6¶ñŒ,a IIHô¦[R·úýPßîû¨GfîYß9ßùòœ“'«2«nÝûý":êvUæÉ“¯s~ç÷½¢,Ë2@ lÄÓî€@ `²(@°Å P `‹A @ Áƒ@@ ‚-!€@ [ B@ ¶„ @ l1@ Øb(@°Å P `‹A @ Áƒ@@ ‚-!€@ [ B@ ¶„ @ l1@ Øb(@°Å P `‹A @ Áƒ@@ ‚-!€@ [ B@ ¶„ @ l1@ Øb(@°Å P `‹A @ Áƒ@@ ‚-!€@ [ B@ ¶„ @ l1@ Øb(@°Å P `‹A @ Áƒ@@ ‚-!€@ [ B@ ¶„ @ l1@ Øb(@°Å P `‹A @ Áƒ@@ ‚-!€@ [ B@ ¶„ @ l1@ Øb(@°Å P `‹A @ Áƒ@@ ‚-!€@ [ B@ ¶„ @ l1´§ÝYFš¦pàÀؾ};DQ4íî@ @–epúôi¸è¢‹ Ž·¦&p 8pvïÞ=ín@ ûö탼àÓîÆT p lß¾òhÇŽSî@ ‚,//ÃîÝ»Õ<¾!p  ÙwÇŽB@ ˜1le÷­­iø@ ØÂ(@°Å P `‹A @ Áƒ@@ ‚-!€@ [ B@ ¶„ @ l1løõ¯®¾új¸è¢‹ Š"ø‡ø‡Ò}î¸ãøñÿq˜ŸŸ‡—¼ä%ðñÍwT `ÊØ4pee.½ôRøð‡?´ýÞ½{á~áàõ¯=<ðÀð[¿õ[ðÖ·¾¾üå/7ÜS@ ‚ébÓ”‚{ÃÞox·¿ùæ›áE/z|à€—¿üåðÍo~>øÁÂUW]ÕT7@ ¦ŽM£VÅ]wÝW^y¥ñÝUW]wÝu×”z$@0lYxèÐ!صk—ñÝ®]»`yyÖÖÖ¬ût»]X^^6þ Y–ÁŸÞþÜñØ‘iwE [–Ž‚o¼vîÜ©þíÞ½{Ú]€Þ …,Ë¦Ý ÁÄ×á¦Û‡?ú§‡§ÝÁ&Âþ“«ðçßÚ ëýdÚ]#bËÀ .¸>l|wøðaرc,..Z÷¹îºëàÔ©Sêß¾}û&ÑU/Vºøé?þ¼í/ï›vW8A¯÷Ó)÷D°™ð¡¯>ð…‡áïîÛ?í®‚±e àW\·ß~»ñÝm·ÝW\q…sŸùùyرc‡ñoÚØwrŽœîÂwž:1í®6 ’!ï…XP'–×úðÀ3ÏM¹'`TlxæÌxàà€<ÍË<Ï<ó äêÝ›ßüfµýoüÆoÀž={à·û·áÑG…|ä#ð™Ï|Þõ®wM¥ÿ£"NðݘbE¤Câ— ÿÔ|®~pàÔ”{"FŦ!€÷Þ{/\vÙepÙe—Àµ×^ —]v\ýõpðàAE^ô¢Á-·Ü·Ýv\zé¥ð|þìÏþlæRÀà@Ü?@ø|d φ >$ÃÅGΈ @0£Ø4y_÷º×y ­ÊÇë^÷:øîw¿Û`¯šžr–ô“ :íhºl( B,   N$Ãç)I3xìÐi¸t÷ÙÓí@ ¨ŒM£nU¤„ôŠXÀ¡@Q‡5"%+Šï‹X ˜Iœq˜P"=&ÄPÐJŸ•|¨Á,BàŒƒNìBš œ5ô)ü~ü×ÛŸ˜vW HÈó$ ÁlBàŒƒšözB ÊP$À™ÃCÏ>w€‰òœrGJpxy]üŽžî@wFÙ&láùä‘3Sì@ Bg† T{0d3`¾ý‘Ãpùûo‡~uã™:§ E7 ²Ïåï?+f``Ö pÆ!A pžÞÈà¯ü /ÉøCQ‘ PÈÕÑ~’Â?}ï9½>®©çé_œ· ~p@A‚YƒÀG&&`­¹‘À{ŸÎËRYÀP=ÓU÷˜ÝW> ×üÍwá?}éÑIw ôsõ?<ûÜÚTú!F‡ÀÇ´MÀišÁýÏœœ©jY–ÁµŸyþtFWÖtƒûž\éÁ®€™ZD @€¢ºðÔza›IŸ«ùv+ÿ¿Ü;`æ pÆ1í40ÿôÐAøw¹>4Cþ[ûO®ÁçîþÛ?œvWG¦LÀs‚¾ÿ™“êïd‹»µá‚kZnHÖÛ­¼òP²AŸ/²,ƒßúÛïÂïþÓîŠ@0UœqL»ÈÁ¡éçÀ ™€hl“ãF7ßû´&€¡ àáåu8¹ÒkªK¦h¾Û«½Áðû)ÀáóÔiåSÈ,©·ÇWzð€¿¸ó)‰<liœqdSÁ‰`–€DÀÙéó¨Øè•@î{Š(€Iy'ÏtpåMÿþíG¾Õd·¦Ž,Ë @þn¯t‡ à”\/Ðä;7$€ua5WÏP·‚ÚÑžvãŠXÓHƒi0yodÄ/.M3ˆãhÊ=jt‚˲ ¢hãœkoƒûŸSÿQ‘?|N¯àLwÐdצŽSk}è“wª`îåćL ¸à››A.VÓ,ƒ6Î;!L¢Î8¦mÆùg¦@Ò×Yê÷( ç×Ä©ŽSâNÊVˆI~ï0`$Ë`S›ïxp·WÐ<¥ÜŸx«æÚ‘ñÿY媛ýý|8ã0‚@¦0(ð )t²š¥~‚”©uâ €½þË𥇎´ÿ}Cÿ¿ùv¸Š´÷ØŠú{3ß»"´+€ÓQ>€3D¤Ä,ä8㘶`:¢?ÝßÞó ¼ý¯ïƒo¹šGòî>g ÂHÄBgh‚ØßXQÀ“¹å¡ƒ°÷Ø ÜûÔ‰ZÛ5UñZ›nŸ»ÿYxpßsðí=õ^iáÈò:|ú;ÏÌTÙÍ!€3Ži—‚Ó `µc£ƒ{<¸ï9øïß}¶ö¾¹@‰ÆfO“6èˆäTˆ*Öö…<­L̲ žÚ$&à,Ëà¯îz Ü÷œõ÷20^»ž¥LÜ$ À¡`}Àû[÷}nÒ-¢Iàu@ò?ëøÐíOÀïüýCð…L»+[BgÓ®2ê ý¾lhþ›ä*ÐPg˜D„ Is6ýìɵ‚|r¥wï9î%'kŠÎújÃáå®J€ 0Û&à÷Ÿ‚÷þãàzG2âPhûmP>€Ê³þc †Öí¦A›Ëfhý‡ïïÚ&QÌŽMÙ§ÖúSîÉÖ…ÀÇ´k‚ýÆ d’¾xôP›Ý>M™€{IZðY{Ï?<¿ôÑ»U ‡ 8‘íP  6ÞsìŒõø³ˆ+ùõ:½nŸüŠ>€vpþÛô”enà^ RgÕûJÉÿ,Ó–ÍÐ-Øt8ãØ(&ઃ4nµDÇI'R´¯M)€gºø?ÿê^øø7÷6Ò~(šôw¢×nÿIÓ‡óаVí¡åuçþ8‘5ŸÀ2LÂ?kXªõ.â„ àÙK¹:ÊÕ}SœÞÂo®Á(শÏ*Ä!r³@œ¯fél6œqL»0üUi|é•8Á`ŒI˜€ÿê®§áË?8 Ÿ˜24óÖ<‘f”®±ß†Ÿžë‹&à³}1 íø³typÀ€F"èz›¤|ôë{Ôÿ§=¶ñº§MµíT`Lbâ#ƒ$…gŽ›$s–' €±=öhþ}þ¶,vrYJò p ?zÝ›47–†úÏÐ"»º¶I`WLÀS‡ÀÇ´ÓÀŒªâ~hBš¨`ƒA ·=|ž>¾ Ѱ¾ü´MLÆéÕnöùú«CÔiÅ*ÈGÆ÷Ÿ\ƒAšÁ|;†Å¹|ûi««ã;ÛÎ àyÛçÕi#™€é=mÒŒÏCÝcƒ‘}†Ú¼9 Rgçl:œql”J Uiœ/æç&Üd˜ÍS•¼êâ…cMM–‚£ä™ç,óßZ’˜ÅN ZqTh#€_tî6hl‚Ï?xþü[Ó ÒYGõÃr&*€>ð¤ U›Ì¨—õ«ôˆYZC¨<€›„vU˜º › BgÓ®ŒƒiU5†;‘O2_“i`ð~,´7†JÕdÕ:éó\€ê¹pÀ|ç¥NK:ß3p|%Š8ÇÄq=Õ'~÷sÁ|áa8rÚª¦)  íú¦Ð9ßP}>€6“ëÞd)¸ÁD|g‡|à³2 ‰ ?yçSðS7Þ^HÝDÑ Äì܃Í!€3:L%à˜i`¦áHIsSæ%Tµ¦mb2Ì]5Ov|åNAÊü·ÐŒ¹8§À,s_/ü¾GD1£ó¤ËS¨Dà yzíüÂçoƒù¹!$“þÚ”MÀôÕ¥ÆÚ€&`)—/Ã,DåáCpàÔº·ŽsWLÀS‡À‡a®ieø½ýÏÁ‡ÿùÉ Rf›èÓ4+5Gãæ8ÁMÒNºMM.í–&5ÓÄ$j#¨`™2Œ>€‹´c= ¹|Éðû8Š Žê!¸ûJwòŠ ¦±-œö׿n› ˜÷wÒéŸèuÇ4NTŽ ug0A×Út£HfÈŒ}ô¥Òi`fè&l2œq4Q ä}·<ÿåËÁ?<^º­-àÛþò^xíúg8Óu¯T A “ÌHÆ›º× à´MÀHƒ‘ ,)ˆÄR§­áþ¾í‘(µâüÀxçC¯Ë4œê×<&েÑÎ/|þ’=¤?]@3 ¸™ ,Ëó¤]%ò}å÷#Bå¹tܺAš©û0í1r+CàŒƒ¾;ƒ4«EÑ:9LBR¤;µLô÷=sŽé¾«®ÝH% ™Ü `+Ô=¹hS%ÀôÓ\wÛyƒ/x^ž¬˜š€Ut¸Kì¡ØV× ÀíJ MÀ1´¢ñÉ5í×4Lj.ð©µ>œú;¾ðùÛì>€]nž,! }nÊÜd ÖÌ*€ˆþØ×÷À/}ô®±ß‰õÛÅ€W¶ëP‚1 pÆÁ_°:ÌAhb 1)ÛLÀXÝÀ7)(ð4*4ê¨M•YݹW*¢I'¼e?rNN÷Ð  Ro×ýØ–ˆ o{|–â8RA ãŽÔ €“WT0eS–™*úÿ·}Κo«(yÀé(€qÔœÒMÝOê&—MæÆ¬ ý$…}õq¸ïiíC‡—¡7H œøÔ·Ÿ†»÷œ€ö=Wi¿#§×¹§[Rëšn»QïÁV€ÀyêPV†“ózÀÄbSRhìcñ!š¨`ƒQÀØúNJaøì}ûáSß~ÚÒêØÌDzÞöyxn­§¨.uZJÑp/ð®ŠÞs¼ØtÐEØ›hÀæ&ET† “Ü’4ƒë>÷=¤üëW];—æ ýÉÿ®÷¸ÊÓ’ ï½KÐ&àV®êEyÿ\DÂP£ñ£€§­Ò 6É25?5L›qÉó·XMÀ+Ó6+Pß‹t¨dF„̺ ¬Ÿ\ê¿7*÷8³žßcða>³8k¾þéÝ 0MSö[$ÍàñÃgÔw¾4G&”J Óƒ(€3þ‚«ô©òÇ !“:ÚS“R|= Щ]]È?'阤ô“Ü©šªpùoúïÚMÀ,—cb,ü NKÃRg ì"äJuŠr²˜¯.Àé˜jäSCð%ç",F#)@õmò&`Pǧæû:Ÿuº ¬;ETÓµÀë@ßæZCúÝD$p–e*À¤ŠÂˆd÷ɲL—:túRb;Rw5@àŒƒ¿<ãªÔ¤RÕ'Í %Ñ]n¨¿W>€“¬Ü y ù$£€é1N¯›êaîªù#gÇ(`ž (ÿξ¯R‡>ne¦Dz]ë1ë}¹¢6 ¬÷‹Š9€ Ö<€è¢ñ¼¡Ò;­ Væû:É}/·b@SÇûÛÄ¢¥;Hu½áàú°Oý$Só’ëÞuż! pÆQôo–§©[‚@fN ¸M8Œžà203€ºÕ…ü³M’7=ÀÑæ—×ÍÈífƒ@òöT*‹ZẾ4 J«à÷¦ x Hv¶ ØPLÀØß³—:ùo“ö4rô÷u>_ƒ&i ºEÔ…¾6¬ZÓ6×+´î;H]þ­¶}|Ûš‡À'ã+€d¨ P!ôëM§yDÓ \:1œa `“þN:•™ ÎTíû®õ‡QÀCpK©ˆŽ •¦>Ó¡¤ ¯Ûòz_•¼S 1㻎æ¿s†p’þ³æ3nš€k$€MFïD½m=Ý…wÿý÷àÁŠQ´}K&…¦MÀT ¯‚ à Í Ÿ¤¦«K$Ï¿ð¿éAàŒ£`S ¨ªò€ jÎ*sè p³øj¢¢×ì¹…š€ëNI“p'­Õ‘”+€´H0kXMñ!I3«òÌÐü{îYؾ›wÑ  ‰ÞŠR‡&ài)€$„~_UÌxëÁß~gü¿_ÿáXíhpþÿ,ËŒ\QÀ´ÍJ&`ò,¯õFîÊÀj†ß 8ã¨Û¼Ú]L’,(zN¾¨õ§”0dÒ:²¼ß|âXZ€Íµˆm¬é.mÿ43›d¬îãÀ–À¢Ó40Z1u-p¾hÕ<Í þ^áuÜËÌ¿z ßí5å84O) 8¯ÊB:5¾Âæ8Ò\HÝä}5žZ«|—°ü5jBµ¦ïÁ(>€¹é8D4ÓÀT饠Nœqƒ@òkÔb¥WÑ™(BVîÓ®Fÿãg¿ÿáã߆ïí?Üv»590ó(€ôôš*§êÁZrBº¢×Ä(à2¿ÑV\Sðƒ@8ľ`èˆä@+}J܆A S2ó Z}ipÍÏm“~±ÃöŽ,wÇj2WMí&LÀtÎÅ ǵŠP|§!€3›àŸ}c¼ê÷¿ß|âXåöªúp2Å}m ÛÌõhRÑ`T9 1/=æž[+Ù’*€“ôÔóúËM&‚Ææ‰·¤~qåÃIbq.i•™€‰Ù±p±/“O°Žç†I _tî’ú-Š¢B2hœüµ8Í fLÀ“HÖPÿ»‰cË‘Óëc½o¸ ÖÑôf[M¨Ö”TV!˜= D#ÜÝQÀöü†‚ÉBàŒÃf¾{Ï HÒ ~p \±âX!/~eùº&óŒ¨9sÄTZ·¯ U@Ü~%`PlÒðŽÇŽÀ~ááB^4Úþ2WôwB¢Ç}é1]÷´¨ú#{©Ù±Ž(à¨^Î͆ì\4~§‘ÀY–)¥æœ)™€y S)¨ê׬`3 `?ÉàäjîŽñ< ï»åaçbÈôÄwŒw³‰EËʨ&`æÈ“œ[÷ð†€À‡Í||%W¬F8W*è¿ `‰ 8ŽLSiÝ _]0J€y õµ³º- ¸®I惷=ŸøÖ^øÎÞÆ÷fˆ' LíÑ”H‡„lx¬AÀ3€$f‘û––‚«)`Õ> 3wÚæ°Lëw©z¦’¢ù·U!/ƒ.Òê=¿¤ÁE}Þ‘ÐÿÁ†}c/€k† 8üšû|•@$ ̆€ÀGAì'püLžJb”kµ¢ ˜§T ñÄÁ,Š"F'd®˜O#DÄmM°Zÿ\@BÎûA¿`=ýàÇåA tÐ/«ššæ¬£þ,}oBîkà“+^/<Ç+§FMÀÔô÷¼mÓ7çŸæ÷u€¦©Û?¸É ú¼^^‡Óë}81LíSE©UQÀNÀúïù¨A ]æH]Bj‹ xzZÀ3þît)?3†8¦ 8H$až”Hºr°¿«¦B^  >ß;m^r›€ >€äüêö,0"C·áûá³¥LÀ-¿ ÍHëÏŽÞwý÷¤óº@^EAMÀ¨ÐÏ·cEž{S ¡ŸM%‚n´Hƒæå#§»°ï„ö®rÜŸ–÷óhÖ‰ª9`>°ë™è°J/uBÀÁ–×úŠÄb:7$Ì0ÿŒ#SÑ™˜`ÅÉÏ1ÄñÏoÖ6Àa;|§çà5×|y¹ ò”Qua©ƒA ¦aáXpœÉ›ö«Ÿd%Q.À“êdÐxí–:-X˜3ƒC&ì/ •uødr„Œ#£¢IUœ¶wdyö\éX}¼‘ƒ@ú¬Nu7 ¸+ à†€(€3þò ù§FÔÆQi”„&õÈU$Íjó̲ "fF£àɫˀý 3›ç†ý©Ø6'+á&àzÚŒ@<^Y *nQŠÄ„&‚n‘äÃã¤áýZë%ß»¦P4›}jsHêëü‰mƒNê^(p `bŽ+u"$SÁÈm&à., UڪDZwá¯E¦5d½ƱÀ Qg|` ©J&­¦iX€"IÉC)H5øú¬tð¯þË?Ãü»Û$%…/cXHþÙŠ#¥Ô¦⥿|•@õw²-$iy=è5âÿ‡D½Ì¯šGëÈÈ/Åj&5ø{…'IMb… &`tCXê´ÔuŸx@ÖO}ïê;†ñ ÕìhTÇ©; Ø0¯Ãþ“#š€Y)¸Iä\ÕÌ}40ö‡ÂLS¥—‚:! àŒ°N+†^’ÂAB'ìSËÒÀà<§ý¿ÆŸAž57éðžõY#…ÒEÀKPîˆÏUGµäã×le‚‘Àœ¬i3_þÿ¢ ÎHhw,u‚'W°ë~ܳ÷×'Wz¥d—·bÀ4( ¡ œ„Î8R¬œe™UôŸ­öŒè=WTа$ pàÊsÒYûLÍÖAQÀá  ö¤ ‹ëRóÏB»Eg&àÚ#‡ÍµXu޲T@¼0€öM‹3 ùÝ((DO2„½Sº„ÞPtVI þ“ø[ê9â÷¿ðø¿¾¾öèëïüý-3ëœvá}œ\"èZ›ö¾cUΣ?0ÏŸ!«>z4ÇdYà_à†’Lðǣ€ý‹1OBg>pUBµÞOuy¯áÄRIL¸úã¯ëŠ&$4!Ö‘°GL.eU%ÂÌä¸yˆŸž[Asi` QÀæ–I2è&ý(P ^áð™€GPcM<ÆQ¦iæqÂL¤´<"€©òáµÛ6oÀ:ÀƒÃ\¢={Êú;)7—«òæXçâ%$WimsTʘš ›Uu·}ôèû´¼æÆ ` [Q 8°ˆIƒ#h›Š~øÃ†K.¹àòË/‡{î¹Ç»ý‡>ô!xÙË^‹‹‹°{÷nx×»ÞëëëÞ}6xEÕ•3-#vÎҰΨgbɲÌPR’Ì qñ9m&E°¾<€tåìj¯j"hÜ~½Ÿ–^Sznõ+€y;œ(óæ pC”|Å‘VðÒ,+=¦/ ŒÛ(«%„#¨ÿ… )š€Y˜‚ ¸m1ý'U€H>€¶çØŠõwWˆëvô”XÅjn['I0S#ÕM}Ç oǤ"nÛæµßlˆBgæí{¶Ôî?¸p Uû…(`²wŒÁf@a€Ó¦!€Ÿþô§áÚk¯…n¸î¿ÿ~¸ôÒK᪫®‚#GìfŒ¿ù›¿w¿ûÝpà 7À#<ÿøÇáÓŸþ4üîïþî„{>ðÝ©CÄÉe[§¥¥oÈô<Y¸ØR>€õåì’ÁÈe£_‡Eës*3£PuçñæóšíQÀ©{»ñú¢Û¢>€nöD/Y‚@œ•@ˆêÔ PÿùÑ#pÛǃú0é nÂ×f:‹ xN›€ÏDÐö¸iÞ${Žžq´•Æ\t™€™÷w¸?pÕ@èsÓ”_,¦å¢¨dNL3*¶ÛiÅJ ñI¦ãñ²‡¦ÄD|öâ„ç4ó2°cÌ‘Rp›†ÞtÓMð¶·½ Þò–·À+^ñ ¸ùæ›aii >ñ‰OX·¿óÎ;ᵯ}-¼éMo‚K.¹~îç~~ù—¹T5Ühðûަ.Í·ƒLKüå.NþŽ>§ÚL  M^uä¤u)]A%F”jÀ!éi–à¢Àòc„ 4äL·oý­Îq–Þû8Ž”Š[ð 4ã3àΩ ­ðø™.¼í/ï…w|ê~§#;4&ÂÕº¢hË4ô³Ã´"GyÏÔNÓ ~áO¿ÿî#ß ZŒ «ÃÞc+Öí¶€S>™N‡ÿ¼oU¤|Û:¬ˆ&ƒ@°½ w.ªï°R  _Ä!ÿmÅ‘Zì‡<³ôýó™€éBÿœm¹õg” õ~b´å bç'˜6ìõzpß}÷Á•W^©¾‹ã®¼òJ¸ë®»¬ûüÔOýÜwß}ŠðíÙ³¾øÅ/ÂÏÿüÏ;ÓívayyÙø7mÔµâÐÊYóí ÓqSâTóOŒ&àF‚^@ô ™¦$@$Û— hÀ&óúÍcfÀ¦LÀúoJv¹Ãº½HÞ¿Å QÀ´þ¬>–½owþð8 Ò zIê\ÀÀ‰Ø£€É-øêZÀÏœÈ ï~ÞûÍýŸ^À£‡NÃûO‹àµXí%ph¹èS(W¢ÈŽ’†uF›. µ5›·=lð‚ ê»Ýç, 5¦nq©…Sˆ‰–^GŸHÉ$Àµ^ØxÜå&`Ò–S싸°)à±cÇ Iصk—ñý®]»àСCÖ}Þô¦7ÁþáÂOÿôOÃÜܼøÅ/†×½îu^ð7Þ;wîTÿvïÞ]ëyŒ_HU¸BÌK8±øJÙÀrùŸ§‘@Ÿ§º@WP‰™c¬ü˜t€* ¡9UHM+\—8DІ 8Š >à Àò ¬ú¢€KwZ$ Ø¥f}ó‰cêoáádeµÒZ.LÀLlqðð]<±Òƒ“«ù$¾ûœ¡HÔAª¨À½Aj\³=G‹~€4"›~–E|V òℯÎHàFk+0'€í8‚‹‡jm•7þQ×tJLOÎoyÝ=vÑäìØ~ˆ 8˲b-à€*’zc`SÀQpÇwÀûßÿ~øÈG>÷ß?|îsŸƒ[n¹þèþȹÏu×]§NRÿöíÛ7ÁÛ˜-¤ª¢†¾?Û:meRö*€ìÅ-*€~5b(€%y}}ÔÛš.e«n3¿«g€Ã¾ö >€ævÔÙ;eJB]0MÀf"g3ÍF˜ 8TŒKJÁeYß|R@×;Àw]íN> X×òÍ¿OØÂjü“CŸ¼ç-ÍÁö…9£ ß{Z%ˆ»8Øüµ/ ?ýJw„ _§ ‘³f Û~ÅE;à_^°þÍ«/RŽ*ÇâyòR²8ÀÅ~A3|×Ü  .1ØRÖ¤0_´¯öÌ °40¥‡4„MQ äÜsÏ…V«‡›N߇† .¸ÀºÏ{ßû^øÕ_ýUxë[ß ¯zÕ«`ee~ý×Þóž÷@¹ñüü<ÌÏÏ×c_ž:|qØ6ßR¤Ì§òmPqÀüç¹9åX· 8@ ŒêE”E‹šµ€#ï¶U¡LÀì:ñë¼ì0שv˜QÀ‘AàŒ [°UDRçÜiE¤œ¥í§Ž¯Â³Ïéò[®Eßw’¥àp‚Ü6߆î Iš×ÜÆ.¹j?§Ô¿%ýÛp²æAe&y þ|ÿ°’hoS¥Ã°Nö“7;HRøØ7öÂ/~>¼z÷Ù•ÛÆö–:m¸õ·þ¼õ“ßÉ[ÅLƒ@H˜8ŠÔ{RŽ^·Ó.Ê`†@>õ)ôÉã+y766…Øétà5¯y Ü~ûíê»4MáöÛo‡+®¸ÂºÏêêjäµZùƒ_·Y I¨40„¢GU¿™[H¥(à4¨ ’•&®/ Mã2)W™ùïeƒ.n5˜¦¬°+t+mz] &`šÃÌrKQeZœ ¦&`_Ào>qÔø¨ 84åEÀEæòKRó¼]aÀ°‡ðpðZà?´(€… ’Ê,µøÖ’yÑ…GfŒ9ösǦP®½öZøµ_û5ø‰Ÿø øÉŸüIøÐ‡>+++ð–·¼Þüæ7ÃÅ_ 7Þx#\}õÕpÓM7Áe—]—_~9<ùä“ðÞ÷¾®¾újEgʨ)çoŸ‡+½ w½ŸÀM·=¯Ùù:¤ÓVƒyÀ„ù–•’Bå ÎJ F"èòòÅÄQT"èaÛœ(LÀ$ Ø,W£HšŠY%ò ¼ÿ•ò’ëêËø âÿPþ tZ1ô’t¢y»}íj`úxØòšã€ä¿•§©¢shóäA ¸Ž.+WÅŒËï[SA ür`&„3#º`ÛQÿËJåÙÀMÀÔ²0zpyÈqÿ 11ã½#û8g»oƒ$mla*¨†MCßøÆ7ÂÑ£Gáú믇C‡Á«_ýj¸õÖ[U`È3Ï çw\}õÕð¾÷½oZ§0p` *Áù;àÑC§ƒVÍwï9ýúøúãGágþåù°4ßR/v•(`^ Î…›â<×Q>€õ*€Nõ‡t«, ¸j¾8zn+7l®A?É SOS•@\õ`f¶=k=3—]¾ìÜÛÅã´QÀƒ$…»ö7¾+ :k¡ 'VzÁ à±3]¸þ¿¿ô?ÿü«—ž´¦ÉÀ¤¾Üw¶èh*€?rŽ…z&ë´Â3€DàܳæáØ™.8µëýÄp1)ä,)WG"èªe-}0ɇý8£¾³¶@ž²ëc÷¥¦  ¥ ]¸ X¿“U| q,šo· ƒÌ™ãÒØ§$•`rØ4àšk®k®¹ÆúÛwÜaü¿Ýnà 7Ü7ÜpÃzÖèDÜiÇФpþöÜO1TxìðixåÅ; W&â(ÿÞ§,ØÀrÅÉU ¸Ž(`3¤~À²U·Y ØünÐ6ø=Áë¼sqŽéÁéõdYQ™ý¯3DÝÃüÿÚ„›š*°' ˜’ |\¥ù èPœ¾`N¯`çâlë´àÀ©u'Änm@—ੵ>,¯õ•Ùõ÷?¨|í>gzƒ–×°÷Ø ¼üºBÀÀRpUP!tê5%W–L£ûò…€&Ê¡MfY±ž6õ\¬l(€i`ò ê>€v qä®sMÁS‰ xzØ>€[8¸GQ¤&Š];rbrÁw/˾½7WO¶@ß À_ܼ!WÎ(\si+õ§™<€'Wz𥇪ÕjY”*ŸÊL…´p‰ i®Rp;†ü“T¯ÄÇÍy–¤|ö¾ýF`EÞ–Iâ•™+cA V°R¥HÛ•ÕC§ò>¾äü³TälYTñYóþÉô?üÙ·ág>p?Óý|…äÓ³¦È@0/ŸWLcš€¤ª`0*AgÍ·áÅçŸE3põ<€éðØáĪÉDдür¨œŒä‡ÏÜ»þý»Žœ./ŠCXL FK‡*]|!œe™qÍ•\ѼØi“ ògÇÔN;¶¦"³ éE°ô0‚† pÆAóν”ô lé6ûNäè¶ùIãQ9L2cåîðt Dþ‰QÀµ¤±äü“¯<oÿÔýð¥ï,ô«LYàfiNÔ°†Ž^k— å¬ù¶Rä0ŒÏá=_ùÁ!ø¿þîAø¿ÿéaó˜•ËLÀƒ„Õ¶úZ*”DÓò® |Væb’[²Ä<$€½Aj!ÖŒªÖ] ŽÞ§âš¶âHL3®³õ#ój7'VzÆ÷…{H@–‚bèHñ…vuæä“w—ø#Í•/£ªºhV[˜kD۷厤&`êÿGó›€õߥy‘vÚpöRžNŠ›©9ž~ºÚ6ês^7®× †ðô1ÄÈe™Þ³÷Di?hÂrDY¢lŽ~Z|×Ô"ÖÂ)cINv½Ÿ:•bê8l™&`_ýcÞQ§!€3î‹P> û[Æmó¤HG—WÉÿß”ˆíõ÷ä]vŠQÀUjãw/íG?ÉŒs )"PÑBgþqkïUÐÜ—û© Ž4ó^_JT¨i38 8Š´OoÕµ¹¸ô™JÈ„ŠÊ9N ¯×;®¸0kÓ qú·*€ä:Ñ0ô·PW²Gîmómõ,qÓ¡ Hà‰ mÓwÒäÃÑl"h÷¢ˆßgýœéàûüõß)QC”•Êãà `JÒÅQTÉe†ÓeÖQÀmXì„WÁ´[vl,è0ÂÝvÎbÞ88ãPƒ1pèd\›ßÎR§­MK|ÓÔŒftæt#~r´¿µT±)€TþÌO€reaTXw—R‘ÿ–FQT˜´énUçÐ,Ëäƒ67·ˆêl,X¿qRé´bC%)S±Mžs‰þ|;.­.CI³ª}êUMâ7ê=ÕÐT¹ªFA  &àÀ’¡¥à¶uZ*¨ˆ›©?fþ ê«v¬®CY9À8Ž`iX‘ƒ'A>aQ±£,)p»eUm0Š"•îe7'€•+øûA ÛæÛ°Ã¡:KÁ9ÓÀ„+î~ß q(€.›ç™äà)®ô= uÁ°¹[XÀŠ&`w$ðšR~IH¤n4¨‹8qàxŒsŒð¿éAàŒƒšejйñ»mD\Œ.ÄãÑAÉ5!Põ€Ô®a$0LÀÌûC÷ò(`óÿ£(€uX8øäa+¦NkòÚÔN_?ž}n N±É¦ÿà‘ÇØVË¢Kêÿ« –2B«ÖÂF&ÀVdŸPiˆŽv-Btÿ—¥µN®ºÀq£€:-¢dêv[–äš0}«U iÁŽ»è,ç¸&=Ã-#0 ˜µUk%cQd?®¡’¿ï}ê„÷¬yKJåqÍßf»*mVÅ w ˆ- ÌZoô40KóºÊ .Ô0Š xz8ã iG4)oð½hç‚únÛ|;H´Mô! ŽŸ‘2רRðp€ÔjØÐÁÛaÞ±÷ÕüÝ•0Aͱ‘únüŽ÷ƒš|š¦cáÇv™ZN­öágþäøåÞm|æßüxvEFèï XÐEØHÚ—OU´Í•ÐB{äš-9ëš ‹Õ<\P-´c–;Q+œ6ü›K/‚K_°~ô¢Æ÷€J ÕÒÀh_0P40#‰Õ½‡á§? ˜¾“£úÁ5U ¤èhQÎÉß«½¾·ÿ9OÛ&9 i`Âúgó‘£ù^ÛcXxJ}çZ$`¹ˆã÷ùå±+€Q>Ú¬µF0M’°ëŠ00ù»¤Û+]]vih¦NÒ¼¾ô6fÖ+ät<æFp°ˆiqòvë¬bZÌßlQÀø÷+/Ú ‡—Àþ“fU³íüÓV ®ê¹«ãg™UåKôo¶Uf^" ’4ƒ~’A§í!€xÅÒÀà³P_q¡†Û‹8=ˆ xÆÁSªhB…äÆ¿þGì\œƒÃj" !  ÅW…8n°¹Jn—äl«[-`Ìwx|ǪßþÓZ?ñ^S["è:Æ·¢X$vqdšã²,cd×?ø1ösûâ õ`->€tE¤×;ÏÖm»V°C…&„Rç4·õùŽªZ ©vq®eƒ2° !QÀUÐÅaÛ|›E•kâP(ç1gYf܃P⬦þ’~£€>.Ðæ:~m¾2€ZÉÕßUõ,¨í©.¯HAâè2ãÂ$¯¬;ïÿ<>€D äç­@!€S‡À‡/ @@¢c‡é)H´øÏ„Eû©LÀáZÌRp¨º}}ý¤¿EV´¾è8[-à:Aó>ÓI(µ¨y4.oÃÞ6NÊFõƒ$…gŽk“°Ë;zÜ‚2lõä  Û¯‰×Ém9LŽ=Ãì_TÐ…K©õFÈ40„à³ïR¢]¨Z ¸<DG€5$!׎~ÚŽÌ„¸´UŸ¾!yñ¼¶uðZ—¿ÿÆx\q!h«õm[äÕŒî‹s­€óN°x/èþqì.=FÀ2·‚ÄBšù¦'Wõd‰Ïõ¸>€´¨¥ä^UU;A{ÈßžngY¦ÀádŽ à2QŽŠ  ù=WË‚+ ¯7 L* X¹CdúÇS@7j¬"¬l¶¤À¡QÀs-÷b‰‰÷sÙ¢&i¦|S—:-ˆ¢(¸–ù+*€nˆÏÝ^rNBg¶ Sô“*îσ˜'9šÜ©4Š  Íw†ƒ“ÖºÀA’:¼ž+ï–o`ƾ¶ã–Tz÷ HýëLÍ›è[ÀS¬KLŠ×ýÿhÚ׋)8ÉÑ€Þ?²ú¯¢’ïÚq\šp~®ULLu±…P&if˜€¹24*ÄgqŽ$¦‘Ó¶Rp>(âà–ÀÿæXïë÷§ åÈbé÷ 'AùvóÄ­.øLâ6rˆÛ Y3$5ƒ[㚀3X•G‡—x>ÏÛ–—õ;mñ¤$‰Ûb`.@g"èy· ¸K¡bž„Î8¨úƒ¨¢ÚD3M‡ËäÁçÖAÂGJoÿ IDATò–©/¬ŒØ¸•@øÀÙORÃÉ5yûVÒ4ø}€| †i±ÆDЮ”'ü˜t²á‡uXR_ ÿßËvm×Ç´8ó«j.DÁóùXv]  §z mÏP٦ؿZÀ)yµR«<µÖ7®÷U‘Be«Õ2ï*ŸxBA‰³K©I SÀhuÆVΙÐjN½ÿwAÀv}î!ÚM~9l.,ø‰äÈ— >_¡æož:Õõµ[F˜ð(às¶a]碈æß(ÒfÙ…€Å.€Y ˜úýñ cŸÄFKOEЄÎ8Ê@ÿ›ÅWóJמMé Q‹&`¿Z ž cptTºÜ«~g_c­¬õ}&`}nõ°U49Wª:*"•XLÀ% më©!|Éùšö-©gðY£~`|À§ÿw+€î‰ÞñFÓZÀ¸¨p,BÈDm3Sÿ?<¯|¿ñ@|ç☩µùïU£€C{¾Òg´ ŽÛ->€< MAEƒ‡ÀD«¹£û\Ú@‰jÁ0¡×j8^ ûŒ¤Øgnç®zl ]k›f÷v‹ ^ï³—†ТR‡BÂB  ˜*€ Ž >õ˜ €(€Ó‚À‡MÁ‹<“$‡-j ÛÐ)&샀-‚., ŒÙgL30®ŸOÑß(5M¥è& `'@$¦püÁÍå«DI#“¬˜ŽÅõ, ,ʦ…xÞÒœµª†J=Ã|ù3@Ûñ´(€”zA+p…DÐ-rÍèõ¡æ_€Ë ŽIi¾?“¦ª?UÐ&/®»ŒùÛó,âs½DÔ›‹n›ÿC‚@ªælÂÐg·_øÝR@HB€Zñ%…€«L/æ¢Èï.ÁÛ€Åg@•+©=KÀ²Èr:QÛ| &`¥SæT^ëV‹¨µ©öÕ0oÛ¥¸•[hhÄŒ&¾cÜüï‹æït°H”¥*û…ÀGˆÈwFúC|•©ÖR .t°¥\¢ Ëj `ÞÀÓÝ$if<7ølÏ‘gSºü?_}NwÐK2øåŸü‘BûÔ|L Vºé[‚ÂTÉF²}A>‚pˆ8ã êEè áÚ@¯¾]+^[0EˆȩԚ1‚˜fÖÚÀ¶ü….ÐÉË­x `þIÀ¦óÒÉ*€E@,Nˆ:È 3ô×3Içð˜Ìl#ž†h‰ð?¯8‰••S>€!‰ ‰²k› ] ^—,-u5·QÓ ª U]~r´›!&àm$…ÇbÅ0O‹“ÿ½Ð@0½L¼Ë6f<¯°z®hpÅ40ü™¥¾ÕF"è€ötðœúŽûäÇK }^ºhœnëÊØ¥>€M'h¥›40uŒ‘‚ê8ãàeÕ¾¼jÜ¡›Wß®¢à60”Ü>Iæ¢}ÌÆS9QíRÓo9u—õ3ßT_Uˆ7 V ë­l_E›Ç4ýãø>np‘Øed"ë BKª"A=µ€)Á\8@Ï$‰Ýk+ÅÉîߪ”…v«< ˜L¨±EÁ:éTËŸo´ ˜•‚³øa…€ç~´Á—ö„BW! à¢-`þÉMÀV«ò"WšQÍRpî±K°áæA&`‹E¦ªÈŸÙ”ø‰ +Ô^˜Ó>¸8!Ï$b‘-Ð\Á ®40 ¬Ò .©Ï €ÓÀ‡[ 8]>„4´ß‘Á}] ?f]ymæ&› ï–ï¸FÈpbôØ}kPÙ-èÙÔ¸ˆ©,+ìãê‡ hÓwsÃIج?l.g¥OM”y[~°Ó*'ÉÈy¢(r Ø”‘°†Ñ7a ÈÈýÁ°ßá&`[î3ª8t,QÚ>°èªÿ.ó¤Çæ}ÒAæ÷¦c~§‹ èùYÓÀ MÀç5?pe.âH§kI³ò±….Ô\÷I=“-³Ïî\T¼—¤Öû«LÀÃûtñÙ‹ð’óÏRÇ(š¾»$ ¸j’lA½8ãp)x>§z êëŪêAº8HØ^lnž£ÉlÇð¤&JÌè&7e*Q¾-@K|ú¨²`¦©pøóêgÀ¯ÚÛ¦“4Çuh*€0<¦È“dEH¯¯S4v³a¡òUÚ´Ú&¥à*™€É¦'WrÅë¼íóÃ>ÀQ\¨ U†´Ø´ Gš€—:E@í;†‡)¦å)¶9J¼®s­È›hT˜~œîßø³ÜŽcµh´EÓmm&àÐqÀ9mS¬ʯçÀ²_ôg{öh`‡M A 6׊|Ÿ¼\Ì¿“40ÓÀU2(ÊÌB|›ï‘ö,!€dE¤2õˆ®<]yÛBPV „—óÂô&¾ÁG«•~¥ ¨¨40ÎÁÙÖˆ=„)€>€füÔç­|-ÕŠ”áµ±äý¢·4D´E—Јãâ}ÐÀy»¥‰ =&àî Qd  v(*ÊU@}éÙ¬ þÛ§B¡H£€ó_Ô ÃH`è ˆ¬5$ÜðÞ§NÀ±3]u­˜š;ë#´›œó<€©‹Iu$˘HÏŸZòC«2©>XÒÀ¨w<Ž˜ÅÄ?^ªÓqä$êZ,ÒúžÚü}ÐUß›ª†u–ËT‡À‡+thî)[ÔÀP°M´¦õ`>€sq= `Ÿ¨@ØVù­Ñ¿ã Ì¾æŸ®hQs[ªFdpt+úšS)€Y¸ %ÝH¨ò‰QÀ¾ZÀt¢.<|± Ûþ¨Ú‚P‘Ág±,¹8}¹ ø¹Õ¾ê&ÐU  EQ®[@J4ª–‚(¢ò)^¨Ò<€:äÔ0}H¡œ'¤ÇýrxøÀ2üo7ß×~æAÓL^S• ¿È‚@RúNû­"ô}£Ï3ÞÒÐç…ßÇ4£nUk½ëþ8MÀ‰ù.SÄ„ôZ@Ì’°=Ä­FЄÎ8\>€®Õ‡‹@äùÔ|&àüW¤6õÇK‡ÇŒ‰yuœt8Ù£³v/I ´?ÔwÑ:Ù•­äé Åöc£¢óÎâEº4Û -:V»+É$ÎA‘ûe͸òh!j®(`€bp¯® ;½Ízß®:MÀÄ„ÍMÀHw,´õ3’˜Ä8ß¾ú}¥ä†&PV°b%lË×Jü}‹‘U‹@A\  {·Uç±áðéuØbU«V­(Ø•¥ |i`8Á7KFdLô›€éxì»>6pWÚVUZ0ÛÀ¢ß£6¹Û K‚V4;î-§È±À©@àŒÃ•ǯlK[€¨ªÚˆ¢íøÜ @›úc ô=FIV¡Í‡dÔWQÀ±½b„¹­þ;D1¬Þ†©Æéc¶ÐoQâÜ&à"™4kɔˬ, ŒK¤âCADõƒEÓIƒÖ þ~RÔ²÷è 1.ø×›š=uÍbAÄ>–¼ä»û|F@¦.SÁ¬› Š>>‚êˆ÷cµ—èÚ´1-{VŸ /(Æ•û1ïëh X54W Ädï¡ fêZP®ÚéÀ¢£&|–e…  ×yÓ ®X-æ„NBgeA e“”KAQ‡°URR…˜SfÛ1|‡ÇGÖÀ¢ÒÁ¨,z’þEú»Kܙ梲 ‘*ð—‚#æ!JÄ >€ö~PÒÒ6­>€lb¤Îú..€Û0ŠÜjO!´- ¸oNDíÓ!­Ø |5ÑüM(|ãiBªB+€ú|óJ ¦ûB”©df¾Dw;«– š z`´bæ•@Ü}Ì?Wzb&×Áç¦ ¸Ú9¢€³Ì0åç»Ô¬û¯üƒ]  ãÙCPlÊÿžoµønN7$ܧÛëp &!€3|qxo¨ WW(p’æf¾¯NìuŸõä‹(«Ü¥MXý43H 7Ϥ˜Hé`e1=Ú¶à>€ÕÎÃÞ6#€ŽDЊTÁáê‡MTª¢+ ˜ÝCJ\nIª£²¹à\*”³(NÜ´¬I«ÿÙmÅQ!M‡V¸¢¢Hž§‘JÁÑ ˜^³ü÷Q¢€Ë|}&O [€b.@§ xŒ(`ìW®êkZѨ hS®”(øO“5ï1‡ò B?¯aýÃóo“ýè‚ ‚“ASÿ>Wɾ²$— ˜ŽAV°dA‡fuñœ„Î0xÚŠPЗWß®I”›€év*Çže€²™€CR²”kÕ.Qp`’:ÐÌ+Г†¨e+yÃn5L^||´FRa ÆÉÈ+^ÄQöÐÄh ái`,Ê#¶Eý¦¸ÈÛ0ö:(FûeC…`4] ð4$ #UAÕ-zÜqÀ2wð40ÃDГœój Îûaó´ä´³ö‘;Nލo¥NÝÂLÀ%i`hKÑlQ‡íØ’fWMºkª¦`œcà=ó´·‘e™òñZ,@Sä}õMâÅ ò>¢¯!½þ®`žª(KäT ÐâðÇö™ÈmÀ>tȪy*y~˜2jätø@åÈî%Î <2ã%Ÿl‹/§!€3 žv„‚O\Î6H´'æÉsš€™HÝò$pµ›€‡ý8 àð\ѰÏ*pRâ_Dõ²¬õ®40õøšÿ7KÁ‘c’ûÎ÷q&UxH\ôLØsC `Êx}Ñÿ¯GÖœcm‡O&'t²ÁMUuR¦^·,Ê.l¹Eïå8A 4À!'€¦Ù¯ ÊÜ=BLÀt_ÞÂIQ• ׂ”’TçZqí `‘Œ¸Ê”s4{ü¢]î4UƒÁðš!Ù¤A <¾< ˜,8Ôµ4·Q‰ QÀÃ`@‡hSÿh AatLðT!p†ÁÓŽPW±¨qˆN‰`ÊVªºÚ´Í¿ ™`Õ±J›`?] `j¦( YES2 +GuÔáæJ¥@kRX@g@[µ2ñ©<€@ J¯1î§LÀŽ`„kRãŽøôYÅßœ‰ KHU/pS3çšÙŽÍ_² ÿ62ù ä!¡uùßô½à}ài` A ž±F¥ü(‰ò§Ï¦M¬Ëß2¯h3c@‹ R¶‹qÛµ_ï'’Ìýdé›VAs% æRé"kô|Þ†^”Ø€ãé:S=•êî0»îÎÁY=G¢ ^œaÐ÷xäZÀjr-þVFuÀbUŸŠÍìÜ.ñÙ ª=Û¦t™Ù’¤!QÀÔèóu0'£p cw·åÌIgQ‰³õ‚^s<5Xb3s3>ÍE‡~ŸºBŒ©ÚÌ¿yv"ÁÕš­B™—y"h‹"IA'vmªÏŒs³)€´o£,V¨ žØM,ó´ÿmlC^;N`” +Äcâì)6·O0ßwy˜pºÝŠƒó™†¢Ì/–ûxªÅ‡RË3ps¨»$Z ¯ÿ“;à?ò-ãûSK:šv  ئ8‚@0íŽË°m÷Ôùüü :îƒjX,$ xª8ÃpE”'‡åmXMÀƒ@†c½eWNfhÇʨ@ÅH­º”´ ÌóŠ±ØEUœÝ•ÐÆàmØûÁUºm“ç€åTÌ7IYBÀ‹²Vµ€ñ¦  3“gÒæV€§Ü2”Ý̸¶Üiuúâ3Ÿ¦™š„GQËJÁfM— ˜|Ï LAt˜#í&àü;˜@R—‡ÇÉSåÔ[ ¤¤¼ ² ‚ Çlqm é ØÁޝtáà©uøþ³ËÆ÷xÍðýI i*TÞ)3{žeµM‰ûÁBÇž«î6ßGú7ƒ%àt p†AßãQ@?|&`w5SéA´‰ZfS©>¢,j3}¦èÜf¦hSwl°¦ qš±@µM?ëXÝb¿‘<ÙÔ8b‹ÆuõÃfN¦f>[Jîðn(€ÃÍ”ˆ>€ý1@B·U奆JSYµJpù}òùò€¨Z&_» Ø­†û¹d(€õ7 Œ0MÀîcšQÀùwî(àü“'G®ÃŒMÌ[@—ªÉUW7úƤ7ü$A*Äfæ©­ÓXäû”+€®(`ˆ&`¦R%ÍæÂ`Ôff(úŒø| G1K*+š0¥¥à*7YJ(¹‰æfh•~xѸ_œ"ÿ6•gtMô´§”X¿`AOéßfòtš0ÈÌÜ"®à1—;A! 8Ë cfHÞTz>ÔÕÁEDA %‰ ]  íx† ˜Ìbž„Î0è`ÆWXV ŽÐÕ¦ÍLf7×L‚ðø+D¤~qÔ_ά޲ÕjÊHQ¤·‘NÇÚ[u}Næ [Œxt)€E2I}CÑ—ÕÃcpÓsþÊÈ@T8ÊÀ¶"€Å߸`EÞIÒ܃›Ñç“öÉÍŠ,Ë E†Þ+Õ‡šâC™?˜™ÆÞ†7(y沌¼CŒûò.–*€úo•&ŽŒ…Eð%‚¶=wܬë5[¶`D{SØqú\ñY¦‹¹B"hÏu¡çÃü<óm\  òÜ›¶ãñTYbž.„Î0|&àð(`s{в\jNpì°°™JÚ’Q= ŠƒÙ#€t ñ/²W!bô³ŽÁ û¨ÍBš¸Q«D1)ôð8F‹ ¯çLÉ .¢@» صháþ†4y3ž“Í!Ý@`š€ÍÅ %C¦o#S+ÞXº95o¦iy*ÊÞu×LáKL =DX3»ʨàzs>€>p¡!1½â3‚çá.˜€Íß¶ôKù÷Å(`ºpÐ>®!‹W€VËÒ/‹.ótìg;½Ä¶wO0YœaP“#â­l›wæK‚@R2iÑ÷J ’°bÇóÔŠ$’jЃ|…ùÑèkìlη5'‹FLÀ4ºux¾4úÐW“×ušý´¨&–¥)Pâ‰A í–ÑV°Èž›s='¼0€Cë‘r_Mª¨š  =:9t3Ç]ê$!¨Ã0õ¿CRŽÐg DÄgt±SÁp }cRç¸&0sÿߦMÀn@õLýx¾'u]±äÝtÁTi@ÖoŒvúú£€çJ‚ºŒR“””R¿D!€SÀFÆ&aŠð(`wåA ù'UÓòÛ÷bÓu¬v¹OK¨ „ƒÙ*Ï]5 êNH%T¼\uâàÉZ›¡eºð|©:`3Å"\ý°9iÓ`[¯çl hè°ë[ª: 6ß*N:ºÐçV@ƒXt…v¼Ètœwõ+´¼pY9.ÊÞuW¦mŸ`ŸJ Ê/w®Ä|n¦XOˆÏÌsA &`®Š#\ÙŒ÷ÉbŠV&à,+,.C¢€ydwiÀÀ‚X–Ú²6Ž[_‚ÉBà ƒ*¡¦î·FÒŠŠêŒÏGÅxR¨£O±=NQ 2ü»<Ǥ$;44Š:Ak¿ºXÌnbFeRÅ*I³Â êêÇÀ2QR‰äÜšz8‚h?4­f¹¢€+û"Y‹¢Â¶* Øâäs+ ¤’“I:Ùú|«ªRt{ê l\*Œe¹òÌ40æ±ÐkJI7û‚@Ê@[·Ú­|Ùèm>ž|¡3ï5›Û"\ )]ìÒß´`Ñý P ØëHȺgüÒî.0ªžœúƒºl¥çpÎ@«•N­å< Aƒ8Ãð%qŽÈMÞFþé[ùç ”ÝDŽOfÔÜ`Oc!€uT!$ £@Ó¬JqµÌg~°×­´oËÙzk#Ù.¦eÑ©|Ìà›²”±£j†òô$‚Öé:ô1h"[=¸@»Û‚ÕÌ&3VÓu¡ÿBÓš¶íý =¯6Qim ±2”ú’¯]J‹+‡€©ðPȣݭA Œ†D#Úäú×ev嘴ý–çÔi{Â*‚@J”7Ú,ËÔ5£QÀÜ0Ä“²• ö󔘀] OXÍa›Ô|ÃRe‰ x:8Ãp 8áy]© ÌÛëGEˆ÷o*¾Ø€åQÀˆ¹Ukòäª8i¶p“”,róïðÙådˆ¶í2—%æß)p–Æ–Ó8þf‹Ô·õ£ª`iËñh’uz>5ñ{AEœaørøûòÀAWv¾I4W3ô¶¡QÀ”4Ö‘°OV¤sŽÕl„ ¢ÖŠ%*n§s\?º%VBƃ@ÌÉØgU¡Ê§-œû;úL‡c+€•š?_j2jÙLÀ–EIÄÍPT¤äÊÕ¯PZ“Ë0ŽXöÛ‚|8|〞ä) àu }A XOÖ«°ø]¨«ßÓ4ƒ}}Üùä1k!à÷Œ’c›ïi! úÃY@›o3Ý—sX[@jiQ&à4+øÁÎc½pŠY?°b%²¸TÏÈ«%Õ¥ð ªAà ƒGR„– ¢Ê5ýØA¨?¸Ú­°<€t®SjÍ+}ê“RªFnŸ åØMü]|Ž+²MÔŽ£b‚nê{h3+‚㸴¦3ºy<êhS ñ2û|HqÛ2ÐõÌØÔ)g0!—¾úÒÔÇ‘ßWÜ<7©Ï!É êUõ40æDKÉF׎\¦š>€ö6|® P0ûRòP ¡Y*1Û®%Í&ðг§à}_|®ûïÙO"¾úØ6ßSýìåçïKã6;”7B†ñºÐïèûÃ#Œ«¤Ñ.)®~øŸ=ôß,Àâè«’CRÐ<„Î0¸c0EÕ(`×Ëûš&`¿yÕyÜ®ATy©Z±3©©éžL5¦¦Â…¯nḛ̈VõBÀ$€¦`OÉ“™¦Ýá6Ô\åÓ3ꛄÁ—H¼r%žТNq¿=k"hŸ蹯4çšO¬ê¨ò­Y®Y¿dö¡ÌÐèÀ·qn ¾ÖÓATWHþl `IˆÍ=á<µÏž\«€ƒà—ÈVA©ãøKÁ埅Rp…”.¼REõø¤ß[ó}/•Ø2@¾ÂçØ¥ä-´±üdf,¦¸‰™Ã–f‹[â’Eµ YœaØjê"B§}>€„–¤Ò0Ø›2E'‚¦&`·Z=×iÇÊDÂAÀ @‹]h-àêÜl‰™½y³b+[?\fbÜÖTݦ;¬¦Ž”PEë*0úøCUÙòLÑE'ê†ÙŸ\ÏP“º <ЂV4éCËÀpÙdŽ š€c‹›f&‘ËýPó¿• 8 P1ÇÆ•+½aœ\íYÛ)ƒ¯:Ž-Ñ÷(¥à¸ Ffz}lyû„ŒÙrPâw!ã%ß§,!u™°nd.¸ûÙE7™°yJÐ „Î0¼ ``uWöz„Mýáûƒ@üI–í&`$š£ t0¥8zd¢ ‹Æ•7! ¥>€Ñpóûq@¯™+$&*4!3„ ŠýpÕ·¥Ñz\qÌÛÏ?ùä‚ ¦UÜÖf¦5÷±+²ô9£íãyh“œ- Œµ0ywx+%œ””ÒgT|Vm wyô´+Ó‡2À p© `»x¬?¾õQø×ÿõðܰ¢ ©ä׊’¾ÃË]û‰” ˜Iÿ]xÒb)8í˜ól–ø˜ãáÀxŸ†X1è»ÌÝKª¤Ñ5´íc²Ví÷žªê´´f_L×ûlö ¨4·j#Õ!p†áËBn´IÄ•̧ÒYCl“ëÚö£ÇDÓ¨ %'R ®°Ýp€@#X€®È6㩇xÎtr ¦RuZî~pu ŸjV¦éTT­`v®œ¸Ðë…JA¨XŒÔ,ªS\Íè²Üi´={ˆnSÝ'un0<7Ó§’·Sµ:…­Ú[›€+5 åîF¦K½f„žCE÷P$÷‚¼ÇIšÁ?~÷Yøþ³Ëð½'Ô÷¥i`,ßS_b­>½nm§ ÞJ –…GQÌÏ#ËŠÏ•k1MÇgáÄëOsòa3Ô]Híõ4•=g@µ(¶?|QYAÌ»ÛÖJ lìðt±©à‡?üa¸ä’K`aa.¿ür¸çž{¼Û?÷ÜsðŽw¼.¼ðB˜ŸŸ‡—¾ô¥ðÅ/~qB½>ómQÀà ¡ƒ’RæÈ3€ñóùçÚH&ÚU4%u>R PTdëõÔ*'å†Jiñ+óùÚ&½,ÓŠCžxZ·ÉÉÏa¦¤É¿+õ´ä  Îìä 4`–eÖJ ¾DÐÔ +µ´Â }®ù;Pµ€½2™‘ëÙb$Õ5 û ëåÚ )7ËZûfy')æ™ Ø ±¤Ë)qÅxö¹5õýBI)8Û³™××ÏÑIB,HÙa(!´Õ æîtÁÍÀ<Àa*¤úV0I¤MÇYa¬wùËZû£Ô6‡ ¸Ä” @ëës.KcSô¹àPæW-h›†~úÓŸ†k¯½n¸á¸ÿþûáÒK/…«®º Ž9bݾ×ëÁÏþìÏÂSO=Ÿýìgá±Çƒ}ìcpñÅO¸ç£ƒ›(B£€¹s1š~Ê‚@xY·8ˆêïÆÍÈ<óA‰›M[QµZÀf°_ÅÀKQg@™Š¤¼ËMÀŒ°ô™ÉÑÖ ®bä“þ?õ9Ì·O‡ý1':Qã1ù„3jÀÔ2Ѩ¶ÓÌpÈ­l+Ÿ‡çm+Pœð+'‚¶øÙÈ‚ëEô l±G/§«Ë¡Á`e&à„Üý'sØ!.®÷Ûö5/àùX¥pDpê~'l‹!ž~Å$€f§©ß,WH¶DÐ:•UdŒ¡üþ„¤áгkL©C½hIÍ«¤pà°aóAÕ¹ Íï“E{Ú¨ 7Ýt¼ímoƒ·¼å-póÍ7Ã-·ÜŸøÄ'àÝï~waûO|âpâÄ ¸óÎ;ann.¹ä’Ivylԣ柣DS“5´ãHy›Ù&I¥”QÂ0f0OJÊÀŹô©`žkDU=¥:•ðÕm½A Ee‹—qYjdù’fæõˆcN3Ö“PÂÌ'œ:+(u‘(N&¹ô‘õÜGE°ñ\Çî ßåÏæ‚­æ*7Ž‚ϱ«Ž6}^]‹‘2  ÅLïK’ Mè\UÄÏ.ÔcÆóo·¢Ü¥ÁR$9˜ˆ½7H‹Ïƒc1MÿOïuwÁ~¨E[K+€IFÌМÁ€‘]§HŽé‚%tŸµÏa#œ4{~æâK0Yl °×ëÁ}÷ÝW^y¥ú.Žc¸òÊ+á®»î²îóùÏ®¸â xÇ;Þ»ví‚W¾ò•ðþ÷¿’¤ݵQÁŠAÞ†àWeǬQÀù'WJZÄwÇæÛc7—¯h}ÀIÉ ÷KAÒAƒ@BêŒÚ£€íÛk›mŒÚ6'åÔLm*€¦ÉÑJ ~m椈 ™6Uš  ËŒ^_\ Ô pGë6u¿ô½ 9Ù¨yÕŸ'¢¢ ˜š¿é1Ç5ÛHï\þT>Ì•(Ùf%I,#€< +$Õæøý'WóþµuZ¦j¥à"ãÓ €Àûž>oýäwàéã+$0J§XAØ¢¼é‡Ð¹™ Ø1–Òÿgä¢u³U%tÛhE:}LZŒ©Î£Î]  ­Ò‡Íœ”G_%­Ög%TǦP;I’À®]»ŒïwíÚ>ú¨uŸ={öÀ×¾ö5ø•_ùøâ¿O>ù$üæoþ&ôû}¸á†¬ût»]èvõÀ³¼¼\ßIŒýÉQW Îð¥ÒhE‘1!´ãH ¬v°xL_ζðê<(0[-à40Q¥¨¸-¨mócÔ·º¥*_1 k¯/«Éx±]~_©¿Ýw®•«{ü˜ü¾ÓýTÐŒRóÉíÚI‚7$ÍŒ:Àt1äJ•AïVæ9cÜT €o¬ÏáòÁó¡,àËP`|¡j%—›ªbè8׊GRñ:áØ0ªàg¾³¾úÈxÍ ÏW\´cØfqQd«ôBsB"æÛ-8 ƒ‚ÈË#" °KD8üì´b2~d…3µx ÉȈ4ú„tº€ïìZ%Àâ=çãõåL›BišÂùçŸýèGá5¯y ¼ño„÷¼ç=póÍ7;÷¹ñÆaçÎêßîÝ»'Øã"¸âDì*_„àþfƾ™}¢´Mª¶Èãâæ>ô‡%ÞPõ1• ’ò…V‰Ê@ºòVQtN°]¬§0Q™ ˜ÞC𮝠’a[?Ši`LÂÊ£•Ù9-NŒ\ ä×W¥j™«¨ZžsªfØ’@çí ûlQvh?yt·­ü@ñhÂp”RpåäJÿíz)鵡hÖ¿ÑûNÓ„œ^äû’œvƒ´X£:?~ñ˜¸ À}WHÛGN‡+€øÌvIÁ,êõL‹•@h.À¢û€- Ø~ [H1J ø;P¦¨ÒßT²vǘÌKÆÙ°h )ó,>—ÜL.&àébSÀsÏ=Z­>|ØøþðáÃpÁX÷¹ð á¥/})´HíЗ¿üåpèÐ!èõìIF¯»î:8uê”ú·oß¾úNbpŸ3ŠP@_ @`%¦<ÑV>Ð ¨GÄþÒÕl‡(hŽ£HEúÒyÐë£ ——¨M:×÷ºxO¸:À͵Zí(¶ËÀ¢ 8ß·ÃZíËãV©xšj_½WñxÇ¢ÅF6ñöRŸ³3-»¢€)⢩ž¦)¢é‚xHÕj6@Ÿ¹0e>€ô~–Ö.󴘀é~«½¢ õÌUlßF •héÓ‘ÓÝàë÷{d²cFårBG  þÕ0— ˜w—.ÊÌÚÃÅ % 8%Y…DÐÞZÀ˜À¯¶…)€0^KW‹ï1/i'&àébSÀN§¯yÍkàöÛoWߥi ·ß~;\qÅÖ}^ûÚדO> )yüq¸ð ¡ÓéX÷™ŸŸ‡;vÿ¦ uJ’â À÷ÇL}^¸¨rºy íwˆO‹´ €9Éεµ9…FT1£Ð(`·`þY¬<þà¦ÓÀ(`K@‘H¹òëØË®Ñ{†·Õxâò£‘ᔤTW„é~éç«ëP]&`zŒ $€l²m)8¦hy׸è2 ,‹f7  £o%ih¸`!xex ×úEØi›5}m¤ÅÖwîïÆ·?¾V „Fتòˆ°M´™Æ]åà|‘Ô4h Aß½T‘Tí&@«dðÜ©ai`ÌcÛú€íøÕg[0%«6ØÒfq%SLÀÓŦ €×^{-|ìcƒO~ò“ðÈ#ÀÛßþvXYYQQÁo~ó›áºë®SÛ¿ýío‡'NÀ;ßùNxüñÇá–[n÷¿ÿýðŽw¼cZ§P~°<Â5oÃM"HÀ’Rp< O´9V—9²—A§OÈûËóÁñˆÐ(ªæG儎'æÖÊRõóá ¦`Ãï!NV½9!ÙúQŒ6ÓÀ(Å¡ *7» ½¾T=s*€Ž<€¼¢ 1gÄÌ| õ=·OxØOM&ÍãáõT `ß>á‡B)2Ô°F°;É2éƒË¬ˆŽýó% r€u Ì}õ½±§å)÷áë ôÄSî'šÐá‚&ÍôâÊVÇìD«PøÜil%Ï ð°©^¢²øþd]lçÛ…äMå @W`žŠæ­ÌÛç𙀑­ÑJ"¨ŽMðÆ7¾Ž= ×_=:t^ýêWí·ÞªCžyæˆÉ@²{÷nøò—¿ ïz×»àÇ~ìÇàâ‹/†w¾óð;¿ó;Ó:…ʨ³°[D2nnÅ´3÷ñ¹ ÿ;,o¡ JTA ºíI©`*€ái`r_1ðnOÍ´ùg}æ Jöq~(8ì#šU &`· 'GifžN\Ê<0ýé\gFéš& TZqäœh\  •l’ UÕnq0Àl!öÜ™ÛKêÉh#²®ÿ‡€û·r„¤ ÎØ+Ðjæ  Û*@Á‰ @þ\]òümððÁe8è¨Ì«iªÞz²,¬>€V s ðŒ¥´0Âknņϱ¶öp•¿ÜzÁÍ­£E› 0=v@aÏú$y§ƒMC®¹æ¸æšk¬¿ÝqÇ…﮸⠸ûî»îUsð*€¡µ€=«V(û:LÀí8‚4ußHX]ÀþaÚJ2hMí2‰ØûBW¾-@ݵ€-JäðZñ` ]m4° ó”*v?Nú7qüO4Is©y[öE€5å QG\Á%®DÐ4%öÚlŸ%®VÆkF|kQý # ŒÃ⬈6§ÖíðZYMÀ-³¦¯oLˆ"ý~µ->€;çà‚ ððÁå`PWÙH­./i–A ‘å¹³c4¯sE8 (ž»ÕLªrPÂÆ].ª¥a [T1¯Y|]U—hC¾Z$?aÁä±iLÀ[aµ€ËÀüs” gÀ8V“œ\%i±ßk #,<²®˜ºØ®-àÂæÌŽÏö,*#FràØŒ_/‰ð(€žcÑÊÅ(àáuà ~Y‹žhÁøä¾œ.“_(l‰sëÑ¥àüÊtþ·}›2‡~_úœkç.!¸‹Ïð¬ŽÖ$Ú, `ÇÂìÚ1ṑ\ØLÀúú$Ö üoÃìPm®-Ü>{öJ ºt¡ÇÉ%^›Ji`, }f|QÀh¦‹ ]¾Ð¯Ú*àõ¬3PNPBgãVáùÐlè8RÐãç WÜò¿mÕ”ÀA:ªX1@MÑ„IkûŽIW¬å>€ù'jõ*€ºmžÏ‹“xMÍ ÀšÆb¶- ðº"Ѷ‘x“Øp’¢"‘qøV&àáoY¦L³èÈ-É+pS=¯Tù˜A H0ZÔ°Xò'– صMÙBÐV äÿgïícmÉ®:±Uu¾î»ï»¿^¸í66¦m̸1{ÈÈ1œÉ˜Ñc²ˆ ùÄBBHD£¶ãÏX²ñ ([ŠDÈ$3&Y±ñÄȸc»ûu·ÝýÞëwï=U•?ªÖÞk­½ÖÞ»Î×»¯©%=½{Ï=§jWª]kÿÖï÷[ñ0¢‡1¤‡uî€$€*8†»Ï@~7ŠjȵGßÂëÄu¡%`%¢ÛÑ1é‰)÷W“1pJËÀi6ÕÿÇOÕófý{èýSŸéx—ôû•*c¥r?ËÅ\Ì.lˆÝÇÞÆ!U§4r|eÇ-¦MNL2*5 ÔJ%9¾V±Xˆì!Q±²(’È ç¬à!6žLÉÄ!U2îZZ)‘Äj"0Ü®–lIÅ!€?Ÿ ˜ñ³ LJ@› €²+Ýo]7¾,À©©–¥1莉ŸÏP,Ÿ~Ð.^g“2<ë÷œH¡ç¼ˆ¾´GåÛb%`\<Ä”«œQŸŽæö¦ü:€H 8BÉÑÚ°©%`‚ªÑûG¶™e”€%P00f­"€ñEƒœ§h ¸ý½PÞ3Äþb@oã%G9@zÏm*á ᪓=#ýyc#h­<.\²F ñ˜s{ÓóS7{ÈöÊ®{ K\yÂâ‚h#8:Y@VnâI4€_h%C&n(¹ &€1Ð.i(?jጠ `ÀíâÛ M‰RHž‹uÀp 0Í$ôÀnÿ #P/w¿ž(!ž¯Qdœ.$ ÷ô$à™1\¹Ð•€sE Ž_WûN d±"[ÒÏiïOúF8€¬õ³éƈÆ%ã JžoNïôJÜ7ªaí·CŸPøBŽYSµÓÐ(6Aµ"27 ±ûÀÛ8b »ù2HÂöÊø›™òšÆ¥O¸r%üLûþõž<Ýn‡"8*òPRzŽ5Þö^<¬íööÛ–h‚¼$ß1¦Öб8°Û§–’K(H»±X}€ÛÏè¨^Ú‚ò ƒðiúƒÚ2ìö%âöý¾O4qœø¥gáŸ~ò/’JFg¶løŽÊB]Ì¥¢OŸ]kŒ)>—<·¡`û;&€÷]<ãþ&;zhI 뼂Êð=] ø™ó¬ŠînUûk[ëL‚Tü~ë&lÁ@|¥*\áºcQƒ´E¡ëVBUÀä{•¼éœ~ ît)õ9"€' ÅÐìnü¡5MûúÀ¼51$€·qÄz§ÌaøMg=w¢­à¨D<ÈF£4HçœM9€^ň O^p2¢%à"Œµä}aszo“ßB“2*€Ðö+9aÞ0Ü®Ú Dya“J Å•-Îh ( 49€áuN í¶ 8.Á±â|J &¦Îo$¿ø/¾¿ôñÇá¾ü óØÚíú‡»?ÿwk–Š0§\ ”V)ð—i¸PkwáÌî<;…²h¯Áo¼æº>»«š$iþx¤ôŒÌ]ZBÛ°ý_U+×6å¦R¥2@çX„ç+ès0ˆ35O÷cÀ÷ÄSç¸ KÀÖgµ*PH“ “Ò!öCxGlÂÉQ¸Ò…xŠüÀ‘OõN ü °9'SÜ/ûN˜À”EXJÕ‚¢œô€iY–"-)“%Z.Ô@°¦TÀÜÇ)‹1cXˆ)~§2¹•àlÌÏï“|áë×Íc£ï§ÉG¬/pn¤lú²™¦8€‰0~{ßyvê¼ã\î(-À‰ß¦/PàÆ£Îvv17Îapl(° ÝdÞËø>DøªF¿ÎS%`íkÔ´°oWª÷C)®ÉØÜ•‡ò9Â × „œï¤ ŒrÌò•âÞb¿1$€·qÄÚ¸ÅÊ-òó1 >Ù5¢T(‘ +é h˜VÒ[ôY§ü5œh/®à%J ¥£å@zŽbV»4‚ÖKÀÀö‡¹¯3½îS®–ôbH.¨›È)‚Å®"j¼W_¬œô$Õ|e’25PéE&Qˆ ‘—À1Odpqôŧn˜ÇFßOÑéRœ³u‚rUuº‚ÀXï1‚„ÿ]–€gãÒ•j§•qi x,’€¤ûË*#ÂgØÀÐ‘Ž·&÷7‚ÖUÀ1Q„Æu£þ”x‰Ò„ o?zzaLõ¥ã‘jjŠjí µÀPC'ÆgµÎIòü%à[CxGX7v ’âêûÈÜ7îKÛ­ šþÊJÀ®<´p%®²ˆ”ºÆzŽPeØc60¾0&ý'Ü6¸mÊ•uH¬æÉí{*CžïZ±œ Ûp@1’ =-唀­E€*8!åeï1(UÀñkwTè¡Ðt—'€3™®0Œ#€bBŸë´£ãkÇþ=‹¡“„ÉuPFðÒ—ÉQ­+KÀ*Ø}Eç5@r’.LضrŸ¨7ÑoÿŽ÷2þ&øZÂè9€º Xûñ%³ˆKBý¢MK$ñ¥Ieÿæ:îüû÷øk2ž ¸N "QÄY44ãiy %à[Cx‡+ý)ß"--™¥!WŠÕ•Ä6P H$úcò¹Èï¬L’”u&ÙöLrq¢¡>€tÌRêçæp·Ùç’Z—”"Q’Ö xè²@˜¸jÖ*P €ª9³,“É}ã@½D¨–€YyYßöX$Âr{.QèŒL8½ v¯ÿùÕ¢ Mh!ï›u‚nCF¯HÖ IDATC„²JÀ :I#(",NÇ%ÜÝ!€RÍ«vQTÀøýÑórñpÂöŸƒÒòªS”Üh™ŽË}¿Ž†[%`)Ô !i87F‘„¶B:-ä×düz ’l¥P< ß „ˆ@üA§êWKÀí߆N ·6†ð6ŽhïIrC§ZDÅÈçVÈÊÇ¥x8b`XÎ?¯ã %'» ™”¦ã2D‹"/I&ç¸( RÖ¿·ý¿}}Õ†  ¡ °ýJñÃXá;ahè˜Oüëè)E p<ò=vWœEUÀqnß{÷nžØötœÚ^x Zì9€XÞ±í` x¾ªá+ß82OÒä¾7åÒ1ÑÐHøÖ{,0PK°ûóñ­rFðè«ïócø›/¿Ü½Çæâõ}î` ³®u#*O9ÀðØ¬ >€ÔƒTÚ4I0]–Æà10Lt˜ ¸æc s¨¶‰àùj!}']I–*‘•E¢²pÓ4ÇЫvý¼Ä¡|Kb0‚¾Òz—j¾/Y`Ì CE¹bB8€rßô>×JÀø™P¤UϵÿûíMG%œ”5{Ÿ³$st­, æËEC"²»ð¤¥è°d lÿøÑ¿n}mÔ„ÛIÏ亇‹"äiÇÂÇyb”iiX>€š@ò -“i«_ªDjèuX7 QU㾺Ð%7>‘¡œI€ÇŸº¯¼çœz|Êf-‚úÄ8±Ø£¨¯II± äcØ­àZAÌt\Â~çKàyÀݱÞÛ‰*áÿðõp¼¬(}éþrî-<%KR¦ˆ¾çv ~¿M£^ç&POah¦Èš ˜rcÔžƒpEÎ{,»»ªXVµ ‡è×­†zÊÅ¥ÿ¢»bG1 €·qäøÄ bíÏc¸Vp1°m`´ Oþn¡1b³ÒôT¶‚“åµ¢I§a§”#¥'|©‰ÅÎ: (Jš²ôÉ9b|,RBm`4ˆäÒê4Ðså[Áå €†Á.ãÎù‡‹WçêÀ`ñ"¹š‚C'F¦`Ý÷EŒ¨q­zŸ ÷žŠRÒ¿q-&;dúR _œÆ„^Q~ô5Wàû_w¿ÿœÂì%©=Hó²U£Šâ¸Ðè¬$‹îÔpø½PÔ.f(팠#s¥äè©FÐ %/½·æ«šÍÑ)PëyìD Êyb1$€·qD9'‰‡@zÒ°[ÁI fMDÇÌHsËXøš˜RHLR£‚—X¬$¹²7(—ü³IÐr½˜ð1ŽŒýRAŒK ©õ…ó ZÁ`Éi'605¶Z·)‰SÝDŒ  X_¼x%"Oq_` I*‘ÉeL ¬q­Ÿûõ¨ÔÆ4ò…Xù ‡da·ü›8„7„4ߦAU¬‡®,œz“eßÚ­$‹TÙ nŠ%þ†t!Ç{`úÚç_¢sUKpT„ÜnZ¶¦üEÛÜ[O¶˜%Óš.¨WUͶ‘âj>”}Ïÿ‡Ø~ àmRuJƒ=,ƒHn´ðÈŽ¢´7³$¶k+^€†l†Î99ˆe,$¿ŠNJ“q¨¨“ü®T™L2ù¢áÏgûá^ß|rÓ gm¨V1¸„oWòxªFï~`Š@,0¹&ê²D BD…8tû8^‹h™åJDQ–€C'"@UOPmGŽi]p»M›Ö(`²\(#Wâ^%Œ!€±>ºø¹ ã ÝaN±ö•ðyÄ•øõÄø=ç’ÈÇKƒ.X0TÐu‹ ­lÁL.Jk¾”è^Ô0!ᚆíÓ좩€mi(ßÚÀÛ8dÂ!#Ù$>±êÈSÓÕt»ßœì­pQä3[!ÛMPN¦r%m'É¢\Y±Ú½€ûMl$YÃp·}Ï«áÇ/ó6µáÀ‚½_[<0>àˆöZ®½UKÔpC°,wï±ø«Úùªrln‘` ‹ÄM<ñÍcxa¾RÏü‰d  €ÍŸ”¯Yk‘$tá÷ý§uOF€„´’äcàâ=é8c>§~ÛaHR’8c%à°Jr±ǵã%ßOŒ’#öEE>1Âýi@‹okº-€x 8Õ ¤( Æ–.Zhˆ£ôôÀ[CxGL àKV» &2aaÐN ’ ‘4|ÍB¹zXGªÖáʉŒ®§c¥Üý9ÙGU<˜\)'R ΛOnŽ‹¤$ʲô$ z.äXñ½ûž›†&H!¸X5ì;•Ê_ Þ 6óT®sêiæJÀ XGÅh9]n—z¿á¡Ék÷S7<7ÕÎöäq£ ì„JAâS‘ï°9€©N ’èo!ê2adcT棨£CI—Â_[©¨H‚ÇÀöïÒpFl`´Vp˜/+¶0Ž)©=o·ý]ãÞÒmŒ”þšþÈmichÎ+øk`U5ŽjPö¢ACƒ>Ü=JùCl?†ð6Ž˜ @¨ð¸dÌ:HÓ€X±òÆHpî(úC#¶JÞ äW:¡MF!¡ZúiåØÀP~ý •&§Û˜Ûèw-WÖž§Øþ/¿Ï˜Qð2({… /[â²L_N‹@,©g ràËŽ:XÕ™ãó|;@¿m:‘7J2&@XNªMÂU›ÔO³YT¼óEÉ’>ž Ðs~³SÆæˆ@ê¦=6ÉuÔL§ç+_޲8€í¸©çÇ7* ¨«ý“¨<Ÿš x:*]벓¥~ý®ÔD&ÍëFŒÈ;èŸY˜`ôJ•Ä ‹¨Ì 'ð_¾åì5Kd¦M‘.0ÒTÀ"\Õ:@WǼ åxÃŒ| ¾}[á&–@Ý?*–¶hNZí¬ü*樈@\¢(üídHd¤P&QŒP\àQªMƒÚÑx Œ†Mœ.Iå­ëF@|o¸Y.=Oôx,kŠQÉ»Ãx”.Æôß™V¶Ôl`Ðx TEáËVäÐÐ&ú½†ˆ*ß.î§iæ¤óŸ¬â%`i˜-iˆzìåˆ@2è 4©[§E)# C-¬NCZ¨%à"ô À&ô„Äp àA#I¬¯DAñw«}@X†¶ÌöÝx„5–F±¨pQ’Qö60 q]°?GÇ+»"ãþå矄ÿæÿøüÞŸ=“ßýcHo³øð¿ýKxäþôÉëþAm"€qžŒ|ÐYÎø‹U O_?ÿ›A ˜ F4ÔWºÚ.'=x=2¤ Œl’ÖÛß‘Ïeñ¶$¯O’¹ù{yb±M~ MZ<²Ë{K“\ÿºÍG ¼ÏšÈú÷M;DUz€iÖ,øº*(‰}qóX»|“$€:ê–UN!yIëÉ¢ûq­ÏF¥Knç©0SrݘŒì{‡¾d]‹9ˆGùßdr¢A{ Ö­$o1ê÷o£‰MÑqzÌxŸ«@d/`²À’×€G½çc܆¿Ç¢9àd‚$nÇ”àV|<ª .œ3Î;žƒe]¾«Zpq‚x,ÔáþâYøµßÿ2üñWŸKŽoˆþ1$€·Yü?þ,\?YÁ¿{âù(Ÿ ]ÞôV|ŸTðƒÿôáïü÷ÿnœ´+ß4È÷lÄ~VáøL•N 8G[}=1B/­î8"ZLĶk Ýþy'ÞZØX1P®Ï5‚VUÀ¸ú_  +ýóŸ54)&áæ¶í>ŠÈ)ÈÙñ¢}O¥7€@±°²ð¡¥ý”ªš&Bˆ@NÇ>´JÀ¨¡šë„u¯ËëÔ²ØLÙÀ$D âc½;8þ¬¹{u[2q8YVðèûÞý›l€"€¡ ¼ªÚF/(%à0î+ð¬u!Äb ®p‚ãí¶¥ˆ@rì\ÜþèQ:¦ã]‰ãó‹äø‰÷יȼ1Äú1po³XvزֹZ4ri°½É¿y´€¯= O]o­.œÅ+ú’k˜Bð~Œ”h%ÚD6. ¨êFG»ßSe»Àܹ´',«¼ÕVp%E?x9'&±W)á>€a¸¨ôN!ò÷² “n€HKF¢$ J§ÝG‘Þ³Ô³ ŠªÄ'Õò¡/Ë\”kxÄÀ8š¬)'­Vz}ÃâÊkÏ¢#duŠ”€ó|mĪ6®)+4Sc€?}ò:|é™›põúø€ÚÀdø6¶PîÒ™)€ØçPòYå9¨Üø5B÷+ÕÅ)ÊŒôyeˆ\DZÅë¥dU7nÞÈQøãnʤ?¢ Dرôۼ͑ŒUUoÍ0Éì&Ä¿zîØ½v½›øp×´|Ç@qcÇú7á*NXZ'œüg Ô&TÛIu qC¯C‰ŽƒuØ@+¤|D¾]g}A:h|,ÍÌâUâÏEÁ½ÌÆe7ŽeˆA; m”@ Yte+’)ȇ/‘Ë7PS”€q – 8…nÖ N÷”—©i“ÃŒˆ@¡L_`æbTnK.h¾Þy0Ê6‰tß¹@§´Å¦ ¨ï¥Ç…‰žôB¬Åx5 <ç)i N·åJÎâ=±˜{© 9Ç=ÉÇ„†X?†ð6 \Áò¶Fú{=OnbFÐ~2ÿMOÚÒ›®¶KÀ±²³ã,®eÓud ÙƒwÂtT•óaR$À¹‰vïü: Iñ(Ýö@æH’PÎlÿ×ÀnÜÀÇ¢ñž4‘&=tò——¼äXR“8õfó¤qÿw­mZ ÄkjÉLáøi‰®jøßƒN d_'L÷T9€JY{HQ.üïúçµ–g2¦Æ¸µßU>f°¯D&“_ï*ÒVïsº(r h‹ÈÐ`q8˜^§ `dnÃiÉ•› #h‰ìšÛLTLü¢ÅÊþ2{Ðvy@:^¼§$oÝAëÛÀûûÌ€î$†ðm.¬Cµ¢Œ§Î—âûÄÉü¯ž;r¯]—@Ç{kdVùÙ*¤Um±ÐHì¿ù®7Á“\>;UZÁµÿçò¶¤¹sN/`Kx±N¨`-UÀáJ_—ŠG LŒh' !ã*`þN÷Onÿo?óÄ—%k5EϳƗ;ê8€RŒ¡õÖÊz´¤(Q©ÚóÙ4¼œ²Ù)X†H'€’÷–´Ñb[­àbÀÜS uµGQñ ‡H—a Xöv¦èu?Ë4š¾fA»ñÝ;´í¦D A»NŠÈI0ãÄ»~ÝÉ¥X ¥ti•“RsŸ %àÆÞfAKÀRu*#¦” +ÖøÍ?q -·^©žPÿ*g®ëEö>·Á¤Æ¤—§pé°åêÈ•jŽ ˜{ìñ1ê@|/_½oCâÕÈüAÚ( RˆvÚhäJ<ôšÆ¯Ò)úàË?+_ÉDs,hr,Z{0£²`æ¶–â÷í:ŒÛÖ#—À’íÑëSZUÈÒ¸,a\°¬·ÿÉÈs-4YãMmKœBÜ1’6010¢–×‚Þ ÎV*džZX%à¯öuÆÀ0í²!Ë¡˜U6ÞK_Ã}-d¢^ó1hÜêÀ0… 4‘~^&¢Y60Ä 7q•­¿¦¼Ÿå½eqS½üî"†ðm "I•p“À‚ˆáJÀÏÓ0G¥×4Km—±VQ©ÐLviXˆ…³îX…“(‡œDu0OÈ=pó °"ß5mƒÆ@>Vÿz¨xÄpThÿS…÷D'`äÝÉïPCèø6r@~ÍÖÊwà¿?/b1JÀÄ» £Rî×[¶IûRô蘔 } ØBCÂ}LåÙ'&Ù@ýóY@zøN”„"Ì;÷ëdŽ¢È†7‚KÀ•‡•¶,Q † n(á ÒX,¤Û1ë . t|B17ø’«·ÉCçÚ{)·ƒˆ\˜H`j‘Œ÷×átÀªvCx›Å|åÀT—òÌià®Ïܘ»×û"ùnš}ðc“dL%˜ŠÔDfõv¨‚ÒÓæÚ»9ÐF1B‚sÖ!Dƒ&—#’„ª>€ YVl`詤 ®ÊcB€‘rä¬âG‚'ÆÛΑ÷‰¯ÙJ.§î!©u±ñï£\$YŽ”×ΘP°Ím`úø’ínÐ ØBeÂo÷NϹ@¦ãR]”º1ª*`»|ª…eCÀU]ܲ9QËmHL´Àãeåå±ÅmP–>€"AÒP»¾FТX¤{úbPHîϱDßS´¿cò†pÞÈï?f]“²­‰EJÍ&'O÷,Bܧã–>€±vVÞ0Üκá’ËÒ,S´Ž'G!:#KðõEíK …HèÕ¹¹à m¿£"¼r@ùP£‰ 'Äó}[%`Dž;RJu PóAÔŽÓù.}+8LB[› Q¶8€¹è—– Lïp Œ©€É13êÈ®˜»û 犣ÅJ|Ï!·lAJÀ—@½lpþ`ìîoDã>€Àöø  &¢’›¥œ<-ä¶´Îø¿†ØÊÀ𪪳8€tŸò~Æs‡C²¸©G‹¸ËÀÛ,pâX?6kÕL;Fhk]DCSôÝ8æ%`|8à{é65¾¶ËT‹”ŸUTJÀªÉrdÅ–€Ûšfó20íj–-òX‹‚N´|»+‡Žºýx« ™ `‰Ó—€#  Š¦§)\²8Xa¨oû[¯œ€?»zýV) .õ#“<ÕyðXAô…&T²Œ´û†uïä–€ûv‰‰@´#ÝvÌB)· .9uÞcU׿b’-ŠD9”^K2Z–œŸµ‹ L­k Dº,0ìßËÇLc¬ Û4p[š×$Žc™™Èø„“öOu‘\ÍÀVË'ülÓ4ƒ ÌŽc(¬ßfáD u eÑÞÖœ¹m äâ ü­÷œƒ}ãƒðÚ.€@•°Š&z[AÕ°–ÏœL’}h#€š!VÖ•ÇF±iò»hÁJÀäXiÒ¸ô÷¤„”€-dx6Áù nÎ;0#dÀ ˆ¤-X67Ÿñ€xøÞ ðŧ|¨%ôüH« ­ÔÌcbC‡“e çøX´E /›¯¿·ZÁ…60Æ\‘Rd/Vþ·@‹§ 'å±ÐD ´üÛþÍ>Þ’.Šœ5 çjû£qñp×OV¨ª€Åýviÿ— '÷ª `b~—>€í÷TA(ÊÈAÝ÷GD ©Ò±«ê4FYPS.í`³›ÀÛ(hk¢eÕÎS&0â»7épe-ª€—£,à±úîït›t‚r6*Gh= ݾu,èUÀ¾ß•_•I×êÜn«ÖÏ™ ’ÒÓ(žKZîâÛu½€©`­?Èð\Y@ê%¦=À²8€â¡f•&sÀ‡ï;_|òº{M+7ÒÄ^Úàhço × nÔòÞ&%œ,ë`AA)Z¢Üþ¬BVØ ÿÑh™LÄ,L0X 8’ü[ß…ÅÔ”ì©ðœ:ÿÚ×DC%/–~?Z¨Í±ÏLà 8v|èØâv$Æk‰@dKK®|çÛLÚÀ4¼ ÐÍ_De¯u§±‚Açr¥XG&ú1J 5T8€»‰¡|Å‚”)«ºÎàÆ9"Ò·Î mBD$Òz`Pކjûœ”|²È ŠZ+ÙðÕþïz+Öš5¶bÅaÈ^Àíû£‡ ÊÔzæÆ@09€ÈÇB½¦±dXÂÅ0DåèÏEð+1 !y V2 €Æ¶_u¥MŸ¾1‡oÞ\€ŽÔÐkUa‡ö¥Kv1Dõq=ÑR¥e½ h·‚ ß«¾†]29€¢.ß§ŽQ €#íæîYè%`žR̽iØvt =xþ¸½®äF#¯ u>„Ý€,+«§1=®p#b%m|Q K\õýäpí$µ· ˜|X"€²SŽü¼¿'ø÷£–€•ùÊõ>â@mn+Åx¥}Q-’U'ª3ø¢ºÅ Í«/T#‡ï±‚¶U̵‘ ì–b-Lè}ÄÞF1¯<ª°$%‹u}¥m‰–ªîC Í«+¦”K‘š­ %ËÆâ,¹°‚Ҥ˗,p"÷‘ântÛ¬¼^…-+tªT¥æ·>âcÀ$ëæ½ÔtD‚þL‡ÒGâ@ 4rKÀß‹eà–¨™‘SÁŽüÃR·ÿh'Œ²0=×–òw“Vp¹@ëµ<ˆÇ|5Ñ€Ýë[ëg ­ƒD€Ö°ä¼ØK¾Š B¡M+œ`» Ò:è`HÄRНªš÷§¡Ôtœ6gZCÏC뛸x޵YfÚÇàwŽû“è»++×äñ¢ÝÇPþÝ] àmL—€“`® ˜<\/NØßb­±˜lâÖ3ë#€žgDz,q%;Ô䨄rýç6µ‚ñ}†y2DCã;z =Ïø Ê)/y·åAÓ—(“˧,,ÛÛÆðñN">ú³†€ÊóI=äP=>%`ɼU †ìi—bla†³¡—Šêœ5f·”-é>ËD \CËÂ'YuÓ0^â¸,¢G iSÝ?f¤¬!€t·ÚB„nK†–ܹDTˆNr<('ÄxÚYÌdúºýÉpé^2°Ò0@vCxM—Õz 8Þ *y讳|‘ÏJ¨vŸíÿj™2®¶Âu³ˆðQ¬ÖA ¬y"@·£ÙºHd‰â¦@Êá"{•Á[Áù×WÈ8p"c nSþ IDAT¸ãˆÝFŽ Øµ «°dd“D#èâÃ÷¡¸+«"öú Æ]È6Y#ÁÃð¨—E)°h šEÇ:‘âZyäkÑN 1ù=Õ–/Ö®.3ÿ æ–ºnàÉk¨ž£â CѨϣ۟! 60ö{eÇ ­·vD[ÁED Ÿ ,/2=ŸH{u§>ç¿§ŽÒ!ª ±ðñ`½óÀÛ(hÿȪîÓ XŸc% tâ¹LsÀÜðš`ÉBfPÉbEËßÏß³MPšLãø5-YÂ?ÓÄ•r‡&¤Tc]²¬ÙÍ`hF¶ë´‚³’ÑuJÀvõ…‚`lܳ‘ÿ=@GE°Y¶À¶¼¨W›$€ˆß¥eÌî^ëÉŒ‰rÒ"›˜{<²Ô~ø™æ°¬•ÞŸ/Vny±8?…†ãë €Q#è Œ`{O­ê†Í“šy³%‰ÙêÐ÷Ñq8ãéÀ I8s;ˆŒÈ9¦ûõ4þ: X àîbHOqÔuÿäw‡O>þ4,W¼|·i/`Ka)ƒ%€wò0†hû–€×äºUlŽ´$o­©€ÃÏö3‚ökú’2¾m‰î²R‘’,©`w€"jhÛ¬{&JÀAIj>¬Öµ‘*àT 8¶í—Ýy&%/+øê7ȹԑ™tj¥7y­M_OÇNãM®cƒè¬@Hò¦MšeˆŒYÌR½¾iâ“Ë”\6€Ü{áÀ]§´SŽöyü )úf&ýðzjûëö…ȪïIž¹ïðzÔª1δ…8ËûkÙKì¹â·¿F¿Ÿcœj‡%àÅžâøü×®Á¯þ›¿€üþ),¨$«°>áb¤>AWþ/¿;”“Mûsû¿^Þ Ôü å¶1\h¨6 ±@ÉÿFCr·ËäÛÆ¢ÂªÔþN.²ˆ VT°CËÍ4R ö Yì§Ž!€£²pv0_|òºŠ*R$BšOkhCñze¬N51„§OX÷>Xé~5úÂ*c1oç·Dc–-•¦¶O…˜¡äþKîo«*–rÔW"aôࢪ 0Ïß`‰<£Üy à)¼nt}wçÂf[>€)þ/ø¸t†yíŲLÃ÷~n´&Ðê×ICóÆð¨–Ú DåŠñ †ü>8p³ÐB¥\OÎHÈm`üë9õ'וDÙÆÂ&‚’h¦M@ùÕ†%àø¶¿ À§n¨HæïèD â—epýÎD xn €òI·“CÄ·Â_²¼ŠûõÛV[±)ɇŒ¨`°…RE~')0CÇ»ÎÍØß²JÀM"k¨¿Œ 9‡>ÑiÇÞu­ ž¢>€óWNõDî¯-ç%ŽA/`ƒ'=ØÀÜšÀS8y¡@©€ë&iã’FC$D Š®ÜsáÎÎ|™¨ –|ÈÍí“‹ðý1Yä†+eF&£ÀË­äàªn‚‡g£$BÞ~"܇,ÉoÓÚòÄcg%`íÔŠœÒ•¸V"ðÉ·ácÔ@zîRI@ˆdYÜ´PŸÊ^r¹õ|æ…¹z|xÓ‡©O¶ù¶Ë"Ü¿T# ÃBZ覷"1Ð5zýÇJÀ1N?óŒˆ@R>€!OQßf,?»Ê_Ë4é0À‚Õk Ba©€c@Ù ï)ÊW´Tâ6P©^0`˜¸§(ZL•N i ¨ûó4OøYÇÀÅžâÀ› WB<¬ƒ¤@†V‚¥áTk‰›˜–Vï:7…³S’®‰ªV ‰ÖuV8þNd+lxÌ•š‹²æAWwÛVJ‹Úûiüë/\…ò;«È"Û¶@(ðA-…Zº¦È)åjÈ'In–F+8Ñs”þß~>Ç£Àž›%“0‘&¶MÅÚu›£SoÍÏmQ„À@,8¥+¥)Iè:aq½0ɾk²˜ÌFƒäßÿl%ã–R9G,ÃÏm|ãÒ+´ëÚVËE‘¼ÎXW›x¼¬`±ªƒ{”nÂ'¬ÂPá!Ê}‡üQ»ÂÃ<éâPŠQ2è3rK’¬¦zãgpÞ‘³Ø|zÔÍ39•ƒ!Ö‹!<ÅáÀe«ø¥*àU† LŠS—*!càÄép³ñÎÎlCXZJ{ÚšãíÛ 8ÃF"€¸ú° ú·*"9‘ÓÐ.šøB‹÷þöŸÀ¯þ›¿€?ùú5õﲇ²,ùŤöá­"§”g!Ë\uÛ$3*°?èQþ¾>"ÿ=ÏW~ᤗ#€á¾$y#ô”60:p[*àT›5™ä³÷‹"fë1‰¨€sD « X´4£ÇIöcAûý5M˜ÐÄ/Œóc7\;^B-6ÞR/àç)M@›{0b"”êçá\$À·;\®j² IÙÀðñÈžÓtÞ‘àPÞ] à):QÎWµðÌèœ@Ôr¬ZeÀ+ï>ÀKÀ‘+HMc*àgÑŠœI,è[‹B²pè$îk½=G‘„NGùįŲªáÉë­‰í37æê{ü¶ùvUÉw,9ßÉï×?p(©ÞR‡û>·µºŸT 8‹ˆ×lÅ}Ã6éÂÆ¾¬Ôk ä1hJ]™Èy°Î)µPjz~vÁ¤í"]ùO\‹¬oä4Î2E Ó‘þ=›ÝJ"‹B+ÂR¦ç²QÐ6‚¦÷D¨ÒŽuà cÀjÈÍùÊ/V¢@,s@ŠrK§p±¾£†‰º´£‰-ž1 RI}¿Cd4T'@g÷CN©°@[éÏùH!€aŸcæÙ›”€ ºµôqh‹ÀÄïg^Ž$€k"€}JàŽ[&5ZÚ-$9"ÚøRA®cçä`R ó¶ ìæ åýRµ,}×âFŒ -ΩTåzqRŠ@Ÿ“ÇT2”ʆ·Ò”]ÅpfOqÐ ìdU±~¢Ëªq+gÛ0®ªÅ—s&Þï~ÅîgÊŒ—€!ØŒ'3"+Ì>‘ceC«&#¸q² Úº Ÿ@ùû!|d‚Ãxòšoaõ\§f¤Ñ°²,‡Ö NSAàÛw*`£³ŒÅäæÙëd\2`piç y-20ÆÙ`– •¤&¸ÌZÈD†qñ™ Ú±ª›àûÉ)ø~´ùT–ßWÀl°q>ékÁ÷$>cÙiA{˄٠7Fa;ãzGÔƒ Ìîc(Ÿâ åœ“eÅm`jËD#Éì¡£qv+`d¼=9€YFвôFÎÙŒ<´¯-á¿øŸþ_øøŸ<¥*#ÝCCõ´ßCŸ¢ àQ˜Ò¯O¢R¨Múš fSÕ~a!“t‰²Y¤tÊUôÏȆ>–@œØ¶³ûYUz/`‡Q÷M÷Ûþ"“µ@²%àtOã ¾V2ÀJÀÆÃ–®·rÀØ‚ÊJÆSeê>óPÈm'h‰ªÚE‘Oˆ}Y²í·ï#€\õ­ ÜP„Uc X €…T ÕqÌ4ÆD•+ÃD ä4ÇÚWj" ‹N&êV»Líkl`vCxŠƒ!€Ë:"Ñ?ï¦FB•R[q.WÒý‰‡e1ÒŽw= U^ãc‘,ÿói÷{þ üήÂGþàËA¹¢ýœÆJÆqÐ÷0ÕJÀÜ(—#j'Q;(À¦ ½º0dr ßÒšc3â׬ūêëH@Ùí~\ª@Š|uï¼@©*@£d¦¡¦ëĈ<œiPÒ½µxa}pc óäËCõŠD¬‡®)®Èù¥IŽUýhÕÜíÏZ'æÏ-wæè7&€Î£ ¸wˆb/àš¸%à~@=A“>¦Þ >}\׳ YŽwˆ´àZj êãA¼óÀSK+ÅÐN¦ò9€}í'r@iÐþÜþ¯•€×å.3LÍ〠Cèé5_Õêƒ)¦ê©†³@¥L?[ˆÄûFÀBUSäÍ䊉XRx©tÍpÀÔÑÝPœÆ ¾)ú,û.øã¢Lšð˜@Òu…F™™h¤ÂBé"¯i¹AK°1K¨1IšbH¦‰ŽøwëÇ"²©‰-yÒD3&¡‚.™Ð00Z@m’\8RH(®*Ù\¢ßo«ºãEÅ„Gš€KÐ $Çš,2úŠ@dïaü˜çFJÀ¸³ÀS«XX¥}e’ #FZŽEnˆÏ:†Æ|׿öïL'ÙÙØ+D±¯ç ȕ< U]Zú‡Œ¨ÐKÀtW’<­µ‚ÓÚi‰«Ws4ÄyÂI–,ƒø>4Ã×òD %²Ð¡Àˆ9QšP;>YæI_˜\Ó¿S{”” 8fÓ÷>¤arq„¼]ÏëÄB°(¼b>†¨[ßEʪ¦Ïá›`é=/[a…þyY;}õE#s^{ X´yZq¾›/+ø¡ö‡ðæ_ú]wÝùëMGí]"ÚdžpWUÞç¤ XÎÿž"~v(ï>È)ŽŠr¥ Löœ’VÝ~¤ ªŽ}Ö!€äîŽMöësÓ«Qù'.ñ Ñë'˜Vª¯WŒÓØÇt• €Ïg–€Kq®44‹þN=Ï0(KÀN9GÙ¢|6žMÇeV‚#@‹§J‡ÖÿS60T®{ï‚ÏܘÃwÚ×9oÔ<ËFm—ǬŒû9fƒ-'ÈîâE…~à€‡zàMoz|úÓŸÎúÜG?úQ(Š~à~`Ç#ìÌpY  E¨ôÏ[å(Œ\#h¹>€²$@|²_Û0æ( 6ÒÝ#?ìdYÁõãvÒYTµšÐÅ9€±„Ñ?U3!Á¡È® 15iYè\DÊÿ¡ß‡/EñqHŸ¥J¤cÀíd tˆêZ|DŽà¦·MÀx 8TU« 99Ó‘–Fв”= ·½NhFÖ|Áåù_ü³9}€1þÇw¼þÙ;^W:oP¹;¤I½Çsí¨h˜-ÍŠ‚ýÍ2gniúFÀö˜°="݆Ü€?V\ÌSñÒb…c \K:g‰û­»vèsACDÙ8ܹñèD»(aLH •!¼G9§Òs%ÉC+¸ÝÇ‹&üØÇ>ïyÏ{à½ï}/|ö³Ÿ…×½îuð¶·½ ž~úéèç¾ò•¯ÀOÿôOÛßüæ=4?P €”·b¡pV9ÊmcM0Äc%YÒxròý}9€¹–¡ÚñÖ5ÜèÀåªQ“XI_ã@ŽTŸ©¸z"€‹°UW\*8 ¯ƒ*é1:eq¢tÕðÀ¥3p÷ù™ëƒç9—Èru.ÿþÒÛÖTÀz X+Ã…?[þ†f ØPMæÚ¤B&ÎŽt_Ø*à\>Àë_zþÎwܼ®Q*‚÷dAsèsü~niwç—AS`pí D”öÅ–ÇE -‡ï“\7úqa"Ç‘ëCû¾p¡aªÎñ˜k.ÉAé8©k&à%`€pNÅ} àîâE“¾ÿýï‡w½ë]ðÎw¾^óš×À?øA8<<„|ä#ægªª‚w¼ãð ¿ð ð-ßò-{m^@QÊ]*ü/V9 CKXr‚"€±Õ±fÈjõ™ ­úŠ@2@›\í¢« ®ŸPPKÚÿµ$5¦.µÀgoÎaU7qZV ¼0_±÷зçÖ*0hH$^?“QÁ’F UH—€Ë²€õ_½þïÿú-î»Ðz.ÇB.¬ä€ué6ïdR(ßSª³ŠCˆ¬ð8^žÇáÞ(48€Ü¦}-(ã"bƒýÆù`c$Û§´”X 5+$—qÉß*—hi÷Äæ .>P°@‘E¶?Yv@І"mîÁÐæ»pá$@’iŠ÷XP¤Ûê.C‚Þ‚©ý;=WtN]UÞõbHw/Šp±XÀg>óxôÑGÝkeY£> ŸúÔ§ÌÏýâ/þ"ÜsÏ=ð“?ù“Yû™Ïçpýúuöo—Á;p€Ÿ0¬{0UÞŠ 8¤‚ Í*ÅmË@1R‘c-ÿÎ|Éyb"åÁS­QÅ%öà°(€¨¾rþÀ%X’HŸé¶ yO€’òȳ]Û¹;ÎNÙùÐZR¤@€ ¸xèùa®œáÒ,”{¤|± ûG´F`hUM’T‹{UCåqô½Ùv ôœzYÒ’'u8kÿ¶„Še(Ï×Ý7=ö/—ÚžP± [àaYpoÌ KÂìq`‡  u])´;V¼oé=å@ƒª"ÏŠVò|ˆ)ÉÒk%ÏÚ¿˳“Ä¢[Ò€¤w-›ŽÉj(ï.^ à³Ï> UUÁ•+WØëW®\§žzJýÌïÿþïïýÚ¯Á‡>ô¡ìý<öØcpñâE÷ïÁÜhÜ©|ÅCe¾Ò‘ /nˆ—€ûÎûç2@ïC¨¬ö•Ï9[ƒ¾"̰ÕßÓA/+W¦@­¤5‚î"ÿïÞ‹pùp ¡˜ÙÀ¸ÝÂ@&)!R‡¦‰«Û÷…žºr%kˆ¢¨‡ì (}H†6.-¦£Ò;|xiˆ°Vþ¦üÑÑŠªnÊerRsß°T”s%…ëŠÁh0pDân²È[gÿÎXY”Ç%9N¢– ¢àÖH Q†•q0á%`«š"EX²@+<à׈Uµðçy:.áR·è’â)³\Ó¤?/ù¦ç}.“*àÂ!ÚL?N/K¼G‹"ïþb½økyfoܸ?öc?úЇ஻îÊþÜÏþìϵk×Ü¿'žxb‡£äHX¬l"€ãP¡Fcÿ-QŽLŽ’Ï/kïω*³lõØtFЫڗ€WµaË £(íkí‹ô”X¨ "€÷]<€K]øÍ›zHËKî\)%`ùÀ*ŠBÇS×1ù<ÃKÀµN-(Š‚MÆ9 ¾'w·9€:’‘»m:ö#òpñÛƒn¿aŽî_Š[8 BIý´ lq™¡tß OŸ@’\¹ÅˆD žeŸÈý>&Ê"O3\ÏÝŸ=Vnôª,–ŠëD'ÈuîD Ê¢"6^× ˜Š@”Å\,}äÁKðý¯»~þ?} \êTÙ8˜ AN‘?œÓ€Ûá3%—w߃ÐëFC'£Þ¥!òãEas×]wÁh4‚«W¯²×¯^½ ÷Þ{oðþ/}éKð•¯|¾ÿû¿ß½Vã }<†Ç^ñŠWŸ›Íf0›Í¶'ÎMóTÀ§OSÖbljÝ d  G± à‘SV^5¢íkøºŸîo ðÊ…gA#KÀZÙÜ'€JÉRQ–â+t(>‘ Ö^ã`2òÈs°?«€ã­Çò¶=àdY«å:y>CÓÝ@A‚’„g6n‘ƦiQûóXÖân«luÙà60úbD&?ëDŽ  O7é,KÀÄûTã¯RT<ìLÆM;pÒ bãõü[½àü>6æ)¹éɨ„_ýÑï€þ_€pá󬪼y“~8¤’Gšd„%`úÓªÊ`½ŸxQ €Óé¾ë»¾ >ñ‰O¸×꺆O|âðÝßýÝÁû~øaøüç?ŸûÜçÜ¿·¿ýíðÖ·¾>÷¹Ïí¼´›´…›V^d–€e9 C³-ɉ³™­àüjßï;Ö÷ӯܷo áv Äõ“ë·<_†ç׫€5 Vn¶|ŸêÚÀQÐ*k à²Vþ¦” µ‡¿/?Ïtev Òœë÷››¤åú²~&¿Ðõm]bbï7‚ Ÿ7ÖÇåÿ÷o  E˜QÊÞ… å¥Ò’' ­çußÈE]7 r;¥rã÷]<0áñ‹†*j[€®ë ÙFàhÌ2ªl‚´aZ¹AéMÝ•ùœ->#ç‡zôÑíËï”–€s©3Ú~0’¼k·¨ïtb\tÞiÈ´÷èî6^ À{Þóø‰Ÿø xÃÞo|ãáW~åWàæÍ›ðÎw¾~üÇxàxì±Çààà^ûÚײÏ_ºt xýVFJœ+ÁÏË›×òXKÅxTÂl\Â|UG'm߬œ"€öj]`n?K+IÂÄE'«0Q+Z yØÊ÷[ &å>ñÜ„íà<Ñ¿æ{Ç[Áab#9HG‹ÜèÊÝ÷^<ö½J €r ±ÀK.W"UÀ®ˆønÛ²v{Îû €7;´FCW,ŒL bœ·ƒI‹4òpš¸ gAcâáöñ´"”‰ ÝŸyHŠ H;'QúXSK'Ù#ÖZ‘ÆLp­$-èLºð”eP70W®‘X<{QK¨~nÿNÛÀå_w“Èõ«…œ/e5Ã*Ÿ 8(€w/šð‡ø‡á™gžŸÿùŸ‡§žz yäøøÇ?î„!_ýêW¡Üd‰{ ‚÷îÒ•8-GaÄʱ©87Ã|µˆN´uF¬ñûºÀ\:±ÒÝcRóôöþÅê ®ˇu1Ž yx÷]<w8P¨€•¤ìÂ’Ce¼ŽØm ¹‡çfc'êÁ„J³BÁ fÐY%ànù>€BᇊVMÓœkv´ZÏùD”}Åuמ£%Sß稀7™žL ¹&-#èmØÀ䨀é8™ Ì\dZæà\Q×s¸j`\ê ´Å©HB&'6Çá5u”àÊñ.IÕB"€–t|±Ík‹ÞCÅ3®óÈ LeXbGM!WKÀC¸ÓxÑ$€ï~÷»áÝï~·ú·O~ò“ÑÏþú¯ÿúö´a@‰*] hE“2@#0Ö5‚h… ߸¹ˆ—€`¬¼s#hcU‰ò3<^„çÎîA›¦q`^ 8<|˜e£B‡³Ÿ¹à9­˜PY(Gòr£»»Ž÷_‹í'@ã׬ճ¥èñºÑE C x·ñ¢J_lA¹sš`Œ¬å(Í fÝVp^ œ³*ÕVûj Ø@1R±4®2,^¢S¨ÆÐ¬b*`WÞPJ‹bøüÑÒ}§÷\˜Áå³­’/LÃ1{ÎWH@Û’ªn؃¹ÝV‡äѵ€Æ´àITbóÞðxÉ¥3ðÆ—ß‘|/=.‰ªˆqO~a`c£\«:<ŸþÚÂk”~V¢"1 ö°}å=çà/Ì•Ç:A9€´Ï.å×Ékc60x\©ïÂÝãlNèÆØcÿQë¶A@—äÔP×8®PBb™ çvh‘60¦ ˜rïȱGþ{Y**à˜„†O*`’åVNhôåRDŸÎ2±­›FµÀÝÆžâH•€1Ò+Ô¥j½IéÛÁÅ&mï+¯Ë”å+FÆdjñÓN±€/£õCµ#BÎßÝçg0¼àMi~O²¬•,«º!~[íð í銛°l`8’’“0ÌÆ#xëÃ÷$߇a‘ƵﶯŌ,Ckf¿+ãžâ¨°ãñh=Wÿ÷wÿmX®šì2¹tñÒ;áÑW_WÜs6kœÜ *‘%MÜòÜÚ÷k–4!– z®ï*¶‰·®ù±OF!h¡~9|ë 7²qk—œåý¤ –€kn<ž_þwg3R”]ÆpvOq¤D ±{P*i¬3ñb´ÈÑspÇÙ©ù´¼Ð@•ÓEÅHE®šòNè¶-Ô⤷ ºíû÷ÇzùÙ›ðò;Û‡&&€Ï PSkÅ“Yé\ IDAT¹•ç Ï/~ÿÜçÞ{‘”€ER©}GL²J­C ÂcËMœBn+žPû|œZ"€ËˆPép:°o£¬  òª®aTzCj€öº-DÒ„±N eYÀ‡â É÷©"5l`𻓭àx/દCôÊöuJo ´ˆ**±ÇР̰?÷ý¤ «óéûŽÍ‡¸xÆëÌ¢2hãèã?)ß›ú,íL/;Ö†±€ŠÏ‘XRßdQ4D:†ðC‰àtT²d061Äú¯SzÁø¹¿ûjø_{/|Ï·Ým¾Ç—€C 6OZ(F*rÕlÞã‹oØBçKÛ°/àpû.¼«M1™þfF Ø—,õó92‹xoP&eeÕ&ÏhÝUÀv9.ë"€QC‚`Lô€û¡‹.»¢Qú½6)2e—€ñ=Ûÿ>e8ˆÒ ¤OŠÇ‹H¼+y^Ý[Õ\á<•°ª=ªOé8Ga‚J¿ÒHê½Ô”ÎÝÔZõdB%s¡¦îQcæÕ×dXNïÈýL¯; ae%`Ǽ½„›·[ g÷åÂÍI XccóæL)GalR¾çÂü'ßq_4éš(œ>oúîS¢¹‘k£¼" ÆŒ zq|¨K‘&E;šºØ•, ĪÇjqï%%`|OTB’­]$ REŒ‘˜©Ë€Êù$uÿ÷ö­D €Ýõ4W8€Òz[Adz4,Vl#èþjÐuC£yhFç©ÀïË#€þ8èç8f¯B9€µ7}Çï2Û0B+àãõóÀÊ%›k38WzkÕ-¤å–Wë j]7nŽM)yiLʼ„×ÿÝßÏta’š#O†ð^bHOqÐæhQ¹‡ÈY™ÆV¨ !cHNh60””.ÃB1RaõY•!Ë¢x¼ˆp•áÅ@Mòž›ÝË A4•d ‡0‰_À¼¦$€ ^!€,ïÔ®qf–€åÃZyЮ">ýËs4Y’‰¥Sgr·t<$@w àÀ‰"Yg!´V#("UÓE¯ZDÔ¿“/LéµŸÓ ÃÊ)gÑ/Xù¾4[–ÜDTªÀ—+ŸdÒ ¾|V™8“1o*y¤ß7Mð8úlì^T3”€wCxŠƒroœxaÀጯŠb÷¯Ù à.äºt¬l¡ô³¿þ_†÷Äóìõ¾ \¥'9€t h/`h6M)À¢(àòÙ°°†È‡†DTKq¬ÔpYÕðÌ ­åͽ)È[íºèÛ ¤o@rÀlô%à8PCeYLãæ^£ëFkkÒíKé²Q¶…QÌkqÛá¸jj’š¿ËpÌ8€ Sc2‚`ŠŠcue¦!€ l9.T•½È›X-+£  ðWôèžtãh*Ÿ­å_å&Ò90嬀\‡ƒ ÌNcHOqÐ$ãÆÜ[”H0ÊÛý€ñ†ÜU³m­œ÷Ò@ÅÀøÜÏÃ?ú_€Ÿÿí?a¯÷µ‘©µÊ<‰õŽˆ@´Õ­|ÿ7o.\'Ž—Ýyè^¿Ü•i?`Õ0Öá¹t@Ü8Z¿Ã¦i@wD7µË‚qwŠÊÖQJ¸1lK J¶ 8Le xæî9­Üî¦\Í .ò,ôz?ÐÞcTm`ú' Òr,é"¢›Òâ…¢â‹WŒWJƒ•^¾a—j`¦…úûPÑ‘HØ(QØ[Ñ—8!‰ie €þ{ð?^¶sã`³ÛÀSO ÒißÛ~ÅsÜõÊ˼\û¿eì«¡Ï·I‘ôëËílrM¥cáòÈ –ÖC÷+ßhÑ¿.aß§fíKeÕŒ y:ø£;4šˆ"ÿïžóQŸ±¤ x'@ŽZ~fþØÖUk¶@)@ƒCöºkw“&€abéz×z/ËÝt•}•ô¦a IhÌv,‘~f-¡Ñ·òµÂR›æÅrsl`Ž™¤ÿöoh{´ˆ%HZÇnÓ1ê*àüíP5+_(P´&·Ž[òmÔ ˜[JSµKƒòÜh׺/Ul 2hû;0Ñ=ïG"‘9Aß›s½P$¥çT§ÀÆžâÐP°É¨nꕚæ¸ë•¿Ö .Õø]C10p•NJ€| ¿…ã¤/Ýu®õÆó½€ýßdé‰†Þ 8Ž"ÿã’RÖuYò‘è@ÌæIEB·‰¡Î]—€GB%ºtÈŒŽ„â+â ¿Vå±…øƒÞƒ¶´Ö n‡  0Ñ@ðïüs±n+»ãJåç@bjLÀUݰ*&/á=rsKÀy÷5á–èœçó†óXnG’‰;f>GZ­«ºqÊgK§î‡L†9‹™1¡iú{­!€C¸ÓÀS*8.ƒ›:6?Íš¥pr[áË=)]ß§†b`à$-@«Ü!CNüEQ0!ˆL5»MÕKW¸~ŸÝßÄûÌ<Ä{7Ô† |Ð%`ÇöÝ4pUéBßã·žO^þ¼qH°DfhÜu¶ýžd"kE̲Õ€«ð; Ü¿0Am`vá¯SÿpßÝ”+-txre]‹ë pë†VXg!J“š$U l¨„ˆ./¶¡%`‡êûÒ‚÷ÇŽ·jÂl˜k60ôçØ4-¿{j5ccî’Þü$‹n/Gz]Y÷M(—®bÁ÷•BcßO ž2l‡¼Ä û$€”úЇ(9lLÊ"ùz'ø»xf’=¶!úÇP>Å¡"€½E ] x¥!€íÿ»RGÊdŸh;¥¡  £(†\Q[“¤¶kœ§#·êl”óC[:ÉÐìShéãêõ9/+•€œ;kúâ1S°ÏnÊD 9%`O²Jýtq Ð~Ÿh{6$€»!<Å¡#€£`¢Ž!1H¢Õl`víÿ…$ªÚMMöŠÛi~nrý¬´‡7&6&J©0œ„õ0œà4äÿ=xùL0Ac ø&)kÛ …@ ~ÇcåÇ yžaXÊWœï´ °Ý>^›1`ß@ÿsˆøéãÊi‡û™“E×>8€î^'÷[F øVÛÀ¬Ó‰„%€$€…ÉQÚ¿_ëÊ¿‡ÓQ/‹š!úÇžâÐÊ 3á;ârO;»žø5ËÍÔ˜}&R¦e£EUÃh'Þ\~•4^¥“àùƒIðf@·Z €ZÙ†¼ý·?÷5ø¿õî`ç”°êhpþÜïAè'YDƒS%à[""›…‘¬®±„WŠDÕßòª»á-¯òßWN xN,ëÅ71mýÖ"€ù à,âØ$’±M'‹>|Ÿ˜4ÐmöÄìM.ŒÃœªæ“íÜýíϯހÿﯮÁ¸,à?{äþ`‡3¥¬ÚËðc‡ä@`ÿ3Ë‹Ò)÷‡±O`Ó4Îü{H@`!اEñVp^ùˆ±N¹­ohþƒÔ›Òæ¶ÿï#”jÕvŒàƘEQ¸kžžçl®„õŠÒÜâˆX:mâH¿S뽈ÎFç»'ðœ4®Ë¨!µè±,…&r ìåèÇgƒNµ‰ôJ#èë] øÂ™ŸÚu à)œ¨QÐÞ€ò¦ŽÍO1˜]«ÿ&Îó‹ò}âå|-µpe$€Axôä™IiƒÜ^À´ô ð¿~ö¯à{¾í¸ó·a8‹%àyXƣȔ\Ú¿PCj«™£ÎQ€¢@fÝ&•¦@úúµ#‘f"€±ûm,Û+§(ÖQ~(÷äÒg€Uý‘Î h3”€wCxŠCC5pŽd®ÙÀì˜ûCû@â$ŸòŒ¶‚#õbPCw<8†YDdc©€iBÈÞOÊLðÏÂÕës¸t8·>|:FLöO–µ›ÄµVYIPˆ?üÃ?´¼ÐÆÝŽ]CJi X=„‚^ÛUm[Ö¬A+¸ˆÅFͫ€o} ¸!÷›åa¹ÏpÔ¬ºçþ¥} €@ë†%À– M˜q¡1ÊL¼xe!ÇôØ®‰'víÑ¿ÅwÉ´TÀZ+¸>×$mÉØ·°OÆù{ðÖÅžâpÀáŽ4p ŒAïxåOQ<–T xá.- ë²àb X9Þ¿ùò;`:*á /»#Ê´xT4!d½€Á?tÿå矀·¿î~sÕ}H¾ë£îÁ¤ gR>€²ÌÅ8€F2’²Bà}“wqÝЇߪnH/àí#€}Jê2¨§¤|ây­êVgå>JÀÚ½®u /·@²a'?Þ¹ëÙ]@QLH¤ùÊ{·|ôãcâ‹^"û½¸™çe 8è×eŽ ïÛ°;7ôšìJSD1˼ù¼¦tÀÀ]ÇžâÀÉ;(÷iQ¯;ñæMTqÅŸ*Sàýå7à±õ§¾ˆÂ¤àS+Ò±{h‡ïûoxþäÞo}øÅh;œ„ëH¨•ë¦g_XÀkî»`ŽqJTÞÈÔJÀ©^ÀÒóŠQ,em`G¡œ§¢(Üo× }¨Ì—!7k“ˆ‰^ú–€ï8;…Ãé^vÇap=Ñ$o¾ªBµË0nûزÁR›a½WˆbÓwÿøv<¿òš§*ඬߘ0ÓkŒ—^ããÈUÆãßáÂy=fAÄ{Ûc Ç `w‘k`¾Ž xœw¬ôY… g¾ xì:†3|J£!榴<—¡·Sä>DbøªC#hRæÔ¥;z&ÑÉgY·¶-I0áþÒÇ¿Ÿýêóð¼â.xË«îV@Š$äúZ>ƒd¥$r!x|¿¥Ì£QœáÚñ^è¬`´D=Õ X>äðïKbÇ§ƒÉŽÕNºÉcâ…¸°, ˜ŽJŸ0DJÀ©C;œŽá_¿ç-îþ¢AÇ:_ÕP5~c»µ±UÀeQ0! —Ãpê»{¼þYâFÐþØpàï‰v;˜0O Cê:Ÿeti÷W@CD õ3tcÕ„  &~V/`çcº® 9}|HhŠ@Úß]‰|@wxJƒNÒg¸N/`Nvn÷±Û‰ŸÞüÿW šEƒrŸëÈÒ/t½k)’‚ÄoÊ%JwÁP|ܱĈN 48H‹öÿ¦ñåL)2‘¶?G„vsðÇ"®EðåÚöšÄ]ú Q~[Q…G@—^³-CX¦bfèŠÂXñÀ¥3pÇÙiðz[nl??_UÌz¥l+°…!-×*PG÷Ñ D¬­  Tù3pé“$ü œÄùbêG©î-™Êx‡&D – uT"Õ•¥ÞPÂŒ 3®ž†½ÕÛßùu9”€÷CxJƒ–HX xT7uÔ<ä)¢°û‰Tz¾"u©²3õ¼Ñ%~8qè '€ÇÇß7†eÀE4h®%ŒuCùlñý:3h^fÛÍ-KÅ£Bx—c•ÛáKÀÑÃX;p¿x½j<»uÃBkäæ7µ¸Áý,V5CZvaƒ‰9íûÍ;àkº x /û1¤h!V8DtE‰wE…E“±¸'ºË_èÄL×epoÌüñbhõ¦Ç¢½}¦äçuitGâ>€ý9€¼œþ\ËËlÆ…PJ<ˆ@öCxJƒŠ Ð@oWD¾Å¢(ÜÄ&y€ëN¼}ǺŠV+¹ð­¢xaÞN8Qi"ЦP"¦J: !˜Ó°@æ¿—iX}VtY«Üý^¸ÿÛוExe&AßwÆeݶ²ÛFŒE8m/q¢hÚ±¥‹M:×nUïE @{“ðû¤)yøžh XªôLò-ÿ¯!)y 8änê%àÔ|‘Ë”Bp¬«7‚¶Ç ;¬xˆ>/T5±&Z³œ²ÝòŸi·/¹š²<ØÀì/à) ºB¦*àɨ_  }0Ì ±•ÿ¤,`~bJõÆUü|Y¹dW­=O©çN´)¤}ÜÜvéàä“FCªëL¬AP—Ü®g…à:>€žðŽïÀUÈw²¶i§ÿáG¾®/U#ëmî÷H´ÌÚF0”‡*= ußÀ1Ï—µ;ž™ÂÜfh*`Þ D¿v½Òî3@ß „ª€ã´+\( <ÎUàX¨ïÃ0E ‰Qk¤Øu#·ƒ•yÞy:ÿzÈÆr‘·q ¸§¤C‹Ê'œÖ½†Éúõ®ò3tÙ} à)ŠLR–€ã۲̠SÉØ6' LØ0‡³KÀíëÏw<rÀp¥›B‰˜J#þxr²Â°9€íÏM¦ øHCMåä+]šÝz í«—+„Ê%¸}oƒ³MÞœedmy“­¬öÒÀßçTLm—œô4 €Š ÌæÀvÛ˜èÕuÃæo쌩‘Ü$E ª,Cþíœãò÷™ÀÈ8Â0GEå–UíÞÛ纤ÛË¥eàûLAl†ð>cH±OiàÍYžÜ €@âÜ-šA,˜­y¶Rõ—.·ïþxá^CîU"š†+Ýœc¿ ‹8ÝnÃ(£1@ÿ:µÞðmÍâ@ÄW–€¹oÿLÀ#(N²ó¥ªYäì}žï“]#€Q𦠠ïâÀ=•€OŒN ÖâåVw©×-#§ne#€¼œ\ ©€ýëÉùÕÈ Çwf2ò­é"@̓T‹±@Vñùa ½Ž¼_/`"É\1L  ´ÕñU’öÚÅïsì>ð”õ¶£¨ÅL¤æmo¡#€»œø¥ïW uÔÀ˜¤ãÕå’ WF¨.ÈÏíÿ¡ Øÿ]³n¨È/" ‰ô¿ómø†÷ÇI„÷³í$hÝp*ாÍÐêÛºu é¼h+Ôm`®/ᣟþ*<´Ø« ´+‘cì¾O°æÝ'¬^Àë–€g™FÐtÞ9O<îdâ;Vœ©mSñ\̯ë#Òj²ÂÞWLÇæUÀú€4Ù­ ˜—|RŠ?œ ž»é@LZ´0>Hr, fãüÈ_š|_´°E¤'jKöY'ñsjÒEÑòt´²y>Ø•€»×O"‚º YÊÞgHÀ]pSåîMï Šâõ³óðX¹Ï zŒßgU7°‚öZ¼9¯vnOC+ã=Ü÷zÃ\d¾Ò@Ù àâ¡O,b ½çS‰ÎÌø\8^ÿ· ‘ðÈòÌØö²ª¹9~¢ZÔW™¾ÏŸ,ÕË1Õ/ àýÄp–OiX@ LMœNL(ºß%ÔŽæ«Xò©ŒDɽ¿›TP `øŠ×¶‰^Ä ’)¢×4Mà­&‡¥FÐIÐÄKÀ|ÞšóV-É-7í:ð¡u)W¯Vã¾­àRá‰ïI÷T^¥KÀx--V·¤½—}Úo[² €¢,CÎAf ¸K¢-`Ø NGúcàÈ*-¯Çmë €ù‰±{ß](t §ÕøðฟD §48P”€3•aªA,³-Ùt´v8p÷PrÊc‹Ø ËŸí¸ëºažh#áD [,ŽG%;'¬!;7­rz81{T7…VžÍ°±¬ä–ýľC"€ÛLœp[¡ç!ßÖ8€«Ú©Øwíˆ}‰iÚÀÐÅ ¢17ç `´ŸN Èl`ÖK@-=œ'´b€†~~©ób ‹dÐÍœŸ$2¸ýïEæ8X7¬¤/“ÜM@Šúå.º±ó‘/K£ý¿®›ÁfÏ1$€§4,p2â@ršX“x€ÝrÎ’þO IDATdë'K,¡­*竚 @ÂN Û>†‰qŽéÏT l)qX´|ì<“@¥L¢ü÷.€û.¨ãÒ@Y¾U(Š`¶™ÜÏŒ6vVé~íý˜}u¡"¼×’Üy ·$º¹X±n»×é‡ÙÀ´ÿ÷=çÒÆõú% ,ܶmy¸f'Ä×F9Ú±SH·C-þa¬Ò£qµ¹4–ôæÄtp÷¾t xè²ïJÀ§4œ;ÿ¨`>S±6eVhFД¸SˆhýT)É í!4_Uaˆ>€µ=ÙmÓq©®Xéàf `û¿%Æ Äü”ª5P¯Qþ¡×¿^yÏ9øöû/ãHÛÀÜ*0Ý÷qD°²nàÃÚR"Ê1¬Îf¹¿pÛk¸MúN–5œ?àÜT<Īn¸‚é.ŒÙE8r·dƒ×8M ¬¤CÛ+÷@Ç£¶*³ª›è{éþÎÑP\Œ˜yOÒ²wŒj €=•éãuJÀn4z#Ø4® åj±»ÀST<%`zcçÌ™š?ãîRâ: ¤}ݚ̴IëdY3­Ý÷̱é9†Á„jc"€øëœ"€I ÷¤e€­DãMí£›ÍCéÏ·ä%à] €a¢Îß·ézb:ò¨‡OwJ íBÉötA…Ý}øÒûtYÕp0­ß Xª€…@¶DÓŽ-êØs1„Èk´LE 3Ô¬[þö+ïJîŸnãxas7Uøó”»èv`wMÊÑÕ×»N C x?1”€Oi¬"e™þü”Pœ*Ån+¼íCWîI•€•Dn¾ KÀ‹UÓmwûFÐ „6úù®Ð,¯P™—çdTºIöæ¢R9€}[—Åv~›ô¡—æÎBúærrÂ!€ Äoc@·èªÝƒo¿@^…;&jp£S^î§Hxÿ¤úƒ[·¨ïlÛciÇ–ËÌItpŽÍCôš:otYw+ŠÂ[LþeÇ(mû›”€ó Qûð€Mí#†ð”†C•„h›>€»Fzp¬+¡î/á­×ð–E à'¬†‰@øßü{»Éo™×ÃYÁÌWY­àR%ËpåŠ9€H¯°l¾½Ò©åØ·¤žÜé²/0]ìÉ6‚„Xs_J¤”lùRCh¤œRÛr%`òq™œhÇ&wgµtËÚõ3l¼† ˜¾¾Iï¤NìBL·›ËÄDta•€‘šP7pcì5†ð”ÆJ”€iô(*à}Y?  <&l)×íxUÍ’Wº½ØyÚ$,â¹¥A[¥E,çzÚN½tåÊË‘pâðåŸõþ|ë@ù`Ùf øR÷`9;フ¸¯Üz"ìû±©D c U‘:ZÈšÕYf ‘dhŸÇ˜Y"Œ¡—bl¢—5E×±VÑÁ'.ÌP¯ÑfsÿçÌëÇK·8€û‰g=¥QÕvi“¢]9dõ™j½_ppõ÷[ ¾ÐN 6ße“`60j¹µá@ƒÈî9€š•™ G舕€ýß{‹@$á=U¾…KCù`Ùfø7^r~îï¾yð{=Lò7ÛUãáì#<#ø¾”ëK‘k-Üö=dŸ,aYU¤*°n ï­PÄ"¯!m^ |Çú=Ÿ“ÿÃ7½U où¶»Í÷àñMG¥Ù>n£@¤N$èCk–€Ë~@œK]ÏË7;ÀÙ¸Œ ^†Ø^ à) ïo§%€k"€Š xç@\íg–€iâ5y"÷ '<D@)–ÙVX*`ú;WÇßë9€™`'¹IJÀ;÷KÚh½ýËVp¬ƒFPN#€”jÀD ·ì÷¾ú |﫯ÄÇÛáœ09¦·âF%`ä®ìp$“NÕ›;VÉáµ8€õÜÜwñ ÷˜†X/†ð) ïÍ¥#5xÓôã†îû»Vþa¢”€38€ÏLÝï¨TÄ@[o³K°Ž&èFÐF xÙOЀV0G‹Jõì›°ôí²r »D­UÀÛ*W{.ó2 õøÃk†¶‚£±/Þ§whÇæ;ôÛ?ÏRY,÷ì"mÜßùI?Øn UÀY­àÖà¥^>œ@;?gKŒÃ¢Ô<ñÍc¸¿ëc>Äîc@Oi¤¸mãQ b1 ô›³Vpíÿ»~ÐKçK,áßïÿpá` G‹-*×#rÖõèÝ«tÀYnß½€×E»ï삎"e©¾Fк ÿùæAr»ŸP"F›mÇ<_Öî\îdžó}éý†§UKþÚ÷쫌‰·rê»{Ù Ž)wŨAóß-ó÷mÍ‘VwŸÀíœ(%q÷ž-øþ£·;|æ+ÏÁ_~GÖûe)Ú°=ûÂÀ½ÅžÒ@nÛÈ@Œ&e È›4‘t´lùdEQ˜¢…m‡/׌3—ƒž;Ã쨄£EåJÀggc˜¯;7‚žDJÀô¯åöîÏ\‘‡øú%à0ÖidŸ €[T[‘ê­Ü7(Ð'€û/S‹<¦c!¨ÂØ;¸¡ 8·‡.€þ}Æh¼¼ó‚›??ㇾ‚+°º€@tÈæ%àWÜ}^q÷¹ì÷§@yȸ¿JÀ§4R s½ÏxPa[ªnàæB–†6j4¦¤ÜCK¦Ö¤J¯s³±{h¢=ÀÙŽ· 8€Û=˜ ˜¶Ô°Ä-ò!ÓŸX©Ézß„%FxÇèÛczW!‚Ûô´"(oøÐg60{õìʀ¦, w HE=Æ>:øsª€ûí?hç·E0fº¾n˜%à-•›%hÝ7tûX”ÈÅyê^ÀýÅžÒHõçÄD"g¾83¹Òs7°O°/ÓÐ’iŽ øÜlìh¨¦mÒ¼ºx—%`»y¹æŸìúroÎWªiwXF‰o/ÏÐ~ÿ>ãÖp·ŒN¨ úîOt)©jÖ(ï)týÁ7U‹÷ÇÄqڡű“Q ÏLàp:ršMÏo Ùˆ@,ë×Ýß>®IiHò0ÀýÅP>¥‘ææ#€-q÷©ë'ðÜѼã0©ÆÝVx#è:Ù›€ï¹Šú0µÙ„&j!¯¯ý_ëbõöÛÍ4‚îŽ[¹­ðÿsJÀ§Åf—>€VÈäg[%`Ê·Ûg x.TÀ#R¶À} 0!À{w]GyÐßiòS:Õ%Ú ®,à7ßõ· ªã=x{·ÛŸô¸ãÀ- € Óù²€îØ*-½©6Œ÷_TÀûŠ!<¥Sø .wξ|À¥Øþn'}$¯*^¶öK_¿p0q”/w àŠsœ¶oC,! 3MhiÏUA™©§äh±r¨i0*‹ì6Z9ã8-60!pÿ*àM/' YÙg XïÒ¾çVsÇÂzݾäò²àÀ4ª–º'^sÿ…^ãIŽcW%`\'8¸°LÔíñ à>ãEUþÀ>=ôÀ›Þô&øô§?m¾÷Cú¼ùÍo†Ë—/ÃåË—áÑG¾ß±}°]ub X3ÞEPPšZæ+÷–ˆ –€9p¹##èi0§ðº@‡²Vp"íáU&?{šKÀà-(oÎ ¬ûì‚<0J(àþJÀ¬õë;A·I³®å€±ã¹Ø÷\à×ÖZÁ•üûµæDzÞöRÙó–Ï]禃 ôãE“~ìcƒ÷¼ç=ðÞ÷¾>ûÙÏÂë^÷:xÛÛÞO?ý´úþO~ò“ð£?ú£ð»¿û»ð©O} |ðAø¾ïû>øÚ×¾¶ç‘ëëàoö>%`€çŽÝö÷ãH kB=2KÀd² %à뢼¨jhšfg o·''¬öÝo'(óƉ`+i_ ÍmíýÈȳ±KÌûŒ[ÁÜv/` }Ù’‰ß+òÀ(º6r  ÎÜ—ÄUDw Þ`Œ¸F¸ëïç§¾çððµð÷^ÿ½·™ZÀ}‹@Ä8d¢OпýÆ‹&|ÿûßïz×»àï|'¼æ5¯~ðƒpxxùÈGÔ÷ÿÆoüüÔOý<òÈ#ððÃÇ?üa¨ë>ñ‰Oìyäz¤@D’rŸS—ìJÀûâzÓWŽZ|#Æd"¥{ cU7;3‚žE|Up÷LMñ[ò};˜ee*Œ¸ à)±‘ÖGûHœžç†Ç_»†fãrç–K´¯§Pâ½v"Y³°EÍÈë¯K·,¶?‡È¸ç¼ãM/s÷6ÆÖ@´qÀŒðh ‰Œ-¶î¿8$€ûŒE¸X,à3Ÿù <úè£îµ²,áÑG…O}êSYÛ8::‚år wÜ‘gn¹ë¨ºÉÑòìcpÇÙ*à]¿1áY  µ[:9œ' Nj‡D‘·¬ê½A[]t@¾uQ<ΓEe+Œ#ªG960’cx«B~—¹Ió&‘Ro¯2ÜGœ±JÀe‘üN÷ß “Ôöõ¾ h¬lÏGéÏïe¶b[]G\'¥íÀçƒ= €òþuiyàòî3^"gŸ}ªª‚+Wx/Æ+W®À¿øÅ¬müÌÏü Üÿý,‰”1ŸÏa>Ÿ»ß¯_¿¾Þ€3@)¡ÇÀ›;W­yI”€=¯l“Q¦Ã¯ök§ö+ ;Á å‚6äx– €ËUË]‰@"@üUëb½WÛn,œy÷ÊäHщt+­àŒè¾C Ÿö‘<ÉãÝÆñÏ&#ì`³'^“TóN áõC¯á}}羈T÷,‹{ÞBýrD û@™­ˆùö <~'±„v{Fe(¿æ¡|ëâEnï{ßûà£ý(üÖoýØôÇ{ .^¼èþ=øàƒ;SÒ°{=wÒFÈó{W{·ã#EÆÌ8€³I0A™ŒÜ„±¨j‡”n߆$€F©©&O»ðzh"€Ç »ó=“!O¹–Tñ^ÃYÃÜIÜ `øPÚBx ÀáH“+yß]:ÃíHöõKȺ½€Ã°aäl&€¼uƒmù:ÏÕ ãr8€· ™#,`ö/Šð®»î‚ÑhW¯^e¯_½zî½÷Þègù—Þ÷¾÷ÁÿßÞ¹IQÿÛsÛÙ;{v–KP¹Dn‚à¢ù†7&Þˆ þóó‚¯$¼–jE¼V4D«ÊH¢–ú"ÞBåu£Ä[*Ad–…Ýé÷ÙÓsº§{¦»§§ûL÷ó©ÚÚÙžÞ3çÌé>çéçúÒK/aܸqYÏ]µjÚÛÛ•ŸO?ý4ï¾a6 ØìFUÓg>êQ"èÎ0Ûgj}µNʱHˆ3+'3’ói`Ìø¦ÖÖÜav@ã$Ó’îk=ø÷%É N¨ Çy7ð" X’$ÕÜ9ñ`䨟&Ä¥a0¿`†ÓÕtŒˆjÒÀè%:7ƒö5Ò]Ëüa·æG§MÀÊ߆QÀé×nh­XÙL÷¤t_€±X “&MRp°€Ž––Ãÿ[¿~=î¸ãlÛ¶ “'OÎù9%%%¨ªªRý % Øà&f7»Ùõ‚Ec&ྠ…BGóÂÚéÎÉ€ž`fRà×f¡‚@T>€Fi`tKÁåÒšû¾ùúÍF¤M¥íë$ˆ’Æ @@«‰É¿=~suc£ÒÀ®¾ºß¼ X;çlM`¸¥dšºM-`«kQF>9‹&`^è÷… fm1râ¯s7ÆQ .#$ýšÊÀ¹‹/|`ùòå¸êª«0yòdL™2<ð:;;±xñbÀ¢E‹0hÐ ¬[·p÷ÝwcõêÕØ²e † †¶¶6@EE**̺.ËÈLÀ®åì{ÚOÈJ9·JM&|žŒ<€ß©X8¤´Ù“H*æ§ëÅòÀŒÈ^40ЍYO³UÈ+?%Ëi‡þlÕ*r]*'ƒ>H’3Q¾¨}ÝKO’ú>íå¤Óƒw_pÃ× H;õ'euÉ·pHO¨Ý«ÂÖ„üjghÃú×o¶vÃ’„^YöÔÐéJ J[9òFÃ’+÷yf@õûì^+‰„”`EÂ|#^~ùå8|ø0V¯^¶¶6L˜0Û¶mSCöïß·;?òÈ#èîîÆ%—\¢jgÍš5X»v­›]×Ål%Ó`ßÕÕ“À©>íPøE?Â=íwôUóÐÖÂä)+ £®<†PHBUiTW¨$—îM •ÚòJùb* ˜·÷‘»°I|;ûƘÍt’ë:à#m”™ÈI7à7.#me!P™€øL^¨pÍKÎI®Œ $Iñh¥Ñ°4à~%u-`«ßy¶40fýê˜Ðï–€n܇yÕÎ0g׺¥•Ö®yF>Õƒú•zš} ˆøF€ë¯¿×_½î{¯¿þºêï}ûö¾Cy;`ê¸Ùû¥²$‚HHBoRÆW'»‘pÉ‹¤µu'N1  ñe ‡ð§ÿýÍ”ÓzHÊØ8£aµàqB¥½~gñdA :i`2’œÂž93N™º»Iœ8Ðí‡JÌ™ã-ýÚH±’Xºð=nšæœöäµ×nm¶)9¥9>ÙÝ«׋ŽEB(/I €z&âB‘Q ÄfFíÚ¨ú ´Z°4€ù3õÛbŸçÖC‰Ö7Ñ(0ŽRÀ¸/|ý3䪙kv£’$) º³Ç0²Ôi"JpRÑÖñÉœõPG}E €Ìô±HH z¸6³ •vP €¦WU&Pk…Å  ù/¼T©’c¶40–|@ A%…„ßÝܘUãwÄÐ}  $IŠ˜×ꙀK"!%)±›&ÿÌJ é>Z![)8³Úl6çžúª®;ûó ‚zB’» vÍÓ®-3GöÇ‚ MøŸ#\鑯W@?‘«Ä{j²²QÕ”ÅpäD7¾:Ùm»ü’UÒQÀ²-a-ÃÌùv'’J‰¸\B¥UTi` rÄÉ:>€Úo3ÃЂ@S £½«ÇØlÁd« ÉuŽ(>€nnÌü×ëˆ]¬o†ÐÕ“P®€Eg^‹,ÚÜM?¢hðµ•@,š€³øš©’úÌÔo/@~ ϯ°¹(`EèÒ5©5Ek—ŸºŠ€„w((ÌAÚ8 ˜iÍ·É×vÍ`Yê|LÀш¤Gûr–ÇÂŽk0˜yD’27%´n)8u;™‰ Í÷“¯{œj+púµèi`xS–[9çó ª}Ý3—(`p$¤<8å-|yHN‰îh°i `È{ÀIa5fóg¦ƒ@ÜŠ6ž'Â[ÈPPrùZͨë¨LY¸U .õ¹)  5P'`_d1(Ž€êÒ(®1ñhØ0nÀ s±}@­ù;› 8gÎ;Ñ0 X°W>€NÁÄT>€nš€SŸ¥Ò†ô@%ÄM Ì»QXΘEã13Úp9 BçJÁ™óTÆìÒ5©íW¡3Oæ!PPX„œa-`bá^âë' L–NÃ?ý}Õ§±³b®Õ.R|À´FÑYÿ?Æÿ™;J÷8ûÎzµ€CÚs37]³dh³ä¸ÌhùuÚM@‹* G`Qûj¢€ÙP´CŠ…Si`w7eöÝòå!S}°ØŽÖÝÂà(›rS ñÐ0d ¸Z%#´Q%—}3£€]ùXÂd”œÀ¾ãV8r´³;z¡À=¿() ]\O¨öÿËE: 8},] 8ûbg'Ĩ-u˜ìmñý2*|/9¬³‹W>€F&D»xeV4€}QÀJÊ(͘¼Ò²µ«7!+é“>€šKÞ ÷&`§‚´‚–‘»‰Û&`­ êåÃ%¡†@AQAl€v|Y5c'»Ó© |H’¤,DÇN² ü4€±  » Û4ìÕ¶¢W~yӯͤñÒLãU°: Lþíy‘H»töiÙu£›š¸™™€5@Ëy³ÜVJÁî èF}òÕjMÀbæ¤jâ@  äöd&`+Àt°[i`€ô‚ì¤u“­~%µ Û [WÕ>€©ßÚéÒ~½VóòdÛðrM#ÿ¿F¾?¢”‚! Ø -…ªˆ‹>€LsÜ¥I¤Ó’Hež¦QûZýʳ™<ÍjÕD˘O­ e¬ät/9¹Ó)–g PPræÌ'¤³›«5\ø›‘-DLHÊ+„óüÊ#°^°lV1ÿ}ç2[ Z0—:ýZ”(`¯ò:¡¥ðÌÜ÷àÀòn†4€á*J<Ðòi`8°U¡;k%b2«|OóhGÇįGÈ­g4ìÌ g¡©ó>€æÛdi`ŽŸîEwB?·\!К=횀%)5n–Kï¸RY¤0A Fd¯¬97/pŽ40V¢€¹5®,ž ›i`$‡`¯LÀ,Âÿ`û)é¹Ìæ覼ϧÉ' 8[@³÷{ÖðC"è¨Öl¨LývóšäûF&`q (`AÉ<¤¶ 0¨ŸùÚÕ¥Q¥PüÑ)홽6Ý h8I’2k¯4€ Ý<€Z  ú­4¹LÀVm-`ÏÒÀäßžW`cuðù±.iÁ/# 8R„^wMÀé40ê(`k}Ð 8Fµ€³›€½×J’¤¬ËNFçÎèÞ˜ù=€‚@Ä@AI$˜Pÿ&óõüßeÓqæ€JÓm†CªâQ´wõàËN&æß×\h¢ò˜ùË.N/Ž%}íh}]\÷ìûxý<€êsóɘËÌo¹žªùÿ5ÎhüYnÂodnnRVÒê˜A•ÆÅZÀ rD£ IDATÕ©‡Âƒí} â˜y-ž9 %œ3¤ÆµþEYp2ÉÕ¶ÞNV0·F˜*硤îåž„œg@)ëߌt@÷®I> …|Å@AÉå(I¾ÞTm¹ÝºòÚ»z”ÍÁ›‘­Ví$ %‘Nõ$•EZ+Pº-*>€&jk÷+•@2LÀY£€³·eJ(ˆ£¶WyÕQÀÅkfÀž„Ú/5# )BCu;oåª ” =‰t8;×[fˆ‘и vš—‰ 6G²¡Ðf­²ÀHyÀ,&U.®›QÕêkKä€@AÉU Ø.cšªðŸ#x÷Óvîøcð ³½’H§z’Šà§Moà•`‚s`gšŒlµ€%ÉÚFW–‘F¿ÚÏуÛL/ýtÔ>€îõÃù(`o@»N´cbµÛ)P”(àd2¯„ôfÓÀ˜2{™ äjÌãºÓîF÷ÎO.¦~¥˜?aíϲŠJH 0Pˆ äÒÚeÊðZ@WO¢¯}G›×…רÙ)ÛÆ6O¶aÅ´>…®û¦~›IÃÿÍ|Í’iÖlx4Vj  þ†ïT4b¾¨¢€Ãî 'VÒê˜A] ĽqTÆ£ª{ÂÈì•ÖK‰NÈʃ“¡ [˜ˆÉ‡#ÒÀéñçã¨ý_£<€Ãê˱ìÂ3Q]êÞƒ3/ŒR)8q PPzûÔKù,zœ;¬Võ·;QÀéϰ#¬1M kÇk0Ûx’I=@c  U-C.pÈ‚À¢Êh°Ù9mµKX%¬ºÂõÁé(`ó@§4NãMòc¥\"iè;kªí‘*Õˆ9 Ó²W¹( é¡hióò4ìüšíåÚB¨!° J8r`%ªâtœbIb]0s7¿aE3Í•×`Ú0}L6 I¿¶jÎÔFkåGuâæ&`îµ™(`OMÀžù:lö¨02ôÅ éqIš.x¥õŠ*&à´ ­ÖÅMÆœÀqëÿ7÷Å$ƒ`ô`ë|~>€Z°8úˆpNx‹8W¡"ÐÙ) …$•ÐÍDÐ@~@%$¢]ödi`tLÀÙ|­n¸ZpöRpÙÛRùôÃ(†Û¨ÒÀ¸èè¸ Xåè®¶÷dKˆ‘ Ûði`I}͹´k—Zë—>ž­íICkqíŒ3<7K²>æ“0¢¹~E´"*ÜÃŽ*HY– ¦€s‡§@7nF~£±£­c›'¼öds"[¬lõ‰ÜJ)¸ÜA RΈGÓÀxaIÎh@y“¿û&àt~PC°W —†ÝC¶¢€3|bõ5€) éVàµlÚ¤Ð^5™–‡p2 oZ,„¯tãÉW­´¬Ñ†ÕO“ZA©Ð°õK?´ú\þïü}Õï«@ó’$$dÙpãç…/5"^ùJ‚’]*âÔ•Ç IÊ\̹h4€Fi`<’Œ¢¼0@­lÈ@(’&̈°¢ÌÇÌÁ¸¨97CĤIžp„UÔþPN1vP5âÑTn=·}ó‰f‡*ª¸$⺿[¬u}5 ¸”‡0W°RâËäðC€±ЊF±„BBRêûuÓIÑ”9$0DÃ!üéÆo’qDf¡P›€õ[7Sìðð¥àØRgçæ2«‚—ÀÁæÈ©J "ùÿj·bȃ‚XW @­Y*„*? absÊéÙ _/^ÓPiÃ\2p¦è¶ÿ®p¦ 8#W÷w4bí»Ž„CªïNÛ¶ÕÜaJá{ƒèOu¬…ަ1pSKÅn5'÷§Uq ¨Œç>Ña90Ö´îU ’&™.—o˜ @Éø=a}ÌçŸ_½îˆhg o¡©^N,ÔâõñM$àëMUiŸ‡7G8’¤Üޏ(`Sµ€ó{*G5V@v)ù£‰R HÍ‹ZÀ~HTÛ cæ—’wa%4W 8_°6XNm¶YœÈÎò}x ¿x™a€PC&`au€ÂårúþÔ!X0±)£ÚD!à#[QÀc  Û @z³J˜¨œ¯XK§ìÑ> ùv‘š‡¤’VG‹•Ê"…&â¡èõØ *Ay,ŒÎîwxã[©E1'“é(`ë\$›Ðd@Q0*si^ë'œ U¤7™6'Òß áP/L¶¢€µi`òÌ+˜/é(àô13µ€í˜3ùí¥±¨üŸ#°÷H'škKuß% H›Â\5÷Ùë” N Iªãøøp§*_ó­ôRd&`YN¯uv„nþ2ª`™xÍ_Ãkÿ:Œ‰CúÙnChÀ"› @ €¤súãFÉW`Ëâ Ó¤$t*däT-ÊÖç3΂µmö2¹næYßÉoÊ[  kYP«KññáÎŒyM&Œ#ÁÝ€÷sëîMEØJ“% ^$m¶.ž8OœWüº(ÚÞÉâËLx‡X €t°×›°S¨|m¤Ñ¿0¼¾€&¨Ä  lÆœ~mÏÌkõÝÚàxÓk?s†Ô ®<†a}sîN”ã  ¬7¯^¥€Ôm§{S5Éí\nÙ¢€Õ@ëm#*@Áͯ}~¹¿üi¤PU@¼B­±³~É]~n3¦~­ÃêÊRíñA ú&u*d˜€¹"lÚ &f(Íbf_«S²š: ØÛEúW?œ„îDÒÕ éDÐþØ ˜¨W1F@€ÓÚ1gÉõÇÏaP|ÎxÁZ¸(`J-$$ H!«€xAT¥´~ÉI’¤hÿRíy«TÒÀ$[ßű®Ô–ÅRïeÑÚѺð¹3"Œ™ Ø¡ëÄJi¹B#I’ëåÓü¤«¨};Ý7­káµs§ûÀ|KÁehyÃë‹Ù%xÓºp&à"ËËH¿ùæ¬%æypj^ŽŸîÁ‹ï·¦ô•×ÓšNÕ‰ ­Ï'oÎÈX@pi¿™€§ ¯EI$„‰Cj”caLÀ’$!’Л”Ó ï\¥å#  j¿Ï,Ž1‘†@éMøSX ;2&¯A³ÿì«.娾#ôò¦_ÛѺ”r‘ÚF©.œºLŠ-wšÓXM«#:g ¨À»kfk‰R¿½Ô) ]oRVLÀv®ályïxa((@~Ì¢™€UY±ºhÄzL øOÈ6;þúíq&eAóàÇO÷½§.`/$› ˜m€Nl¨L…\ô*f;qm9Á¾±•t 毖Wp `f„NÍ›€}t;b]%€T‚T 0u€½€™#œ2×òÁ^V9ÚÙmøžÞßùæ4.g¹Y]DªâN›ÔED@ ­b&`;1¼ ‘=´Ì´ˆ—¿Þ‚b’/ĺJ~ŒNÝðNåìS×ö p–õK»¶IÜÚÒfIãtÐBÐ@IrV EôX@`…é(`{í(5tI -X gè|ˆŽ?$ Ÿá7@&ÈV9fÎ/­L¾d›§k›1;'f¶$ü–FÒÀéÊ, Ýïܨ†n±•‚s ¶ÖŠÂ÷Ç'z _@S! ~óœ4´ýÊ¢¸à¬þŽ´Iè_Y‚x4„þ•%Ž´i…l>,YAG¬Ïg¶RpŠÉÒ¡»X­t¦ÍbÂH›ä'Ä1«5€výÂØµŸ¡¨ª›(@ÁÆLyÅ„¢€Äo•@F6Tâ[ÿËÑêO]Ó‚®î„§i`tßË’ŽÂVÀ˜™Rpin¦‘6©‹ˆ¢ »›cQKDÑÚNµTE™6÷[û}x ïÎD>€â@ €(@ÁÔøùàti±á.–Ó¢]À¢a = ýZÀüŸùš€µ‡ÀT[©ª&AÚ4Š Ø?·]LÈ÷^˜êG>QÀ¹­™€Åºˆƒ˜˜»ë*!¤+øÅì7´²Ñ¸Áý”×N§)ãòf´­¤±Ü¬!A0ƒ¡ŒÝǯ(&`–žæDw/€<|™Æ+[-`ϧö}ˆffÖÁºxH„ÏJÁù ~^jÊ¢8k`¥òwö ëó™- ¸iK˜3@{¦B:´Ï.]¯ó¬JÕ)þ¼/—f¾ Q™Dík¿ÃÖQ5€´§‰…XW €4€¢Ão6Õ¥ZWƽ§=7ýÚV%* ¶Æ;y™ø1²Y¤Œ]”40ƒú¥êöÕIö¯a6ž¬ÀírŠFTP@??\#º5Ї„Ï‚@ü¯QhêÇÐÚ´˜½p~‰  F2d#Òc÷¸#D@&9‘J¦nw­SÌö'¨MLЊ æÈÊ4“A2Çb]%€t@ÒŠ ?-Õ¥¢ÒfÎ;ät"h§KÁñŸ¤M“átTµˆˆbTSªúÛî5lNèßùÔQLÀb™™¤ƒ4ÅE HÚäsáŸbûÅ1¤ÖØœ:&!!˶|ë+J0~p5*ãÑŒ¶K")á0æàbφÄu:=vÿ^” ¦dØÕ -ñs$­“¨¥àØÚ ©( Hò^[ÑXGe<ŠÚòŽvvë ! HÀÞ¦Ixöºod|.LVƒË'7ãÂÑ,·›íóøßA‚ ~»$ЍÑÚ}Ö5$¿3¦i­\”4€BB €(@Ánb"¿ˆ5V§6²¡ue8ÚÙ­kzIm@²mÇ{£ , ãîKÆÙjÓˆ D‘»Ç) ìô:¤®<†’HˆKmלúMyS¤b±4€L ’6¶ë*!Ptøiiê—}ëL|g|¦ŸQox¾hf=Ò~pwÄ‚ÍL¯¼ßªH’¤2ç›F{kI’Äͧ­¦‹¦ùUÄK‘!  €P°Øð†«SµˆgŽ€™£ôM±ls+ 0B¡h)î¿|ëÂ*sŸ\`Õ”â?G:äS ÄXã …ÐHúÚ§SKXÐJ g ¬Äy_«Å¤¡5^w…à P@H(6lC©¯(Q1Ìœ‹ˆ?ŸS‡×¡õ?_b˜‡¥ö¼"æï²XDáP‚Øýʳ%…$‚õ0svSþßGG0²AŒ9fÄ"!<ù㯻Ah P@XŠ“Á}ìãW›>ï‘Nô¯Œ²[ŽðàHÊÂiÜ ˆ&C/á@ÛQÀi`RÇB’¾ÖèjY1g$®¹`ªK£^w…(HҊ͈þxýg30 ªÄÔù¿¿¦'N÷Å¢,I’p9ÄÜ"yE‚¶’%j R)8I’Šb!Ä€@¹âÜfœf=ê+Ì „ûX1‘V—FiQ.”$ؼ¤‰±[ $›RDVH¦~¥ªÅ‘ ˆÂä2x^ Ž¶×FZ˜i·rU‚0y»A ­‘ÒV© CCu<ï¨ótòîÌ÷"YÌÃA ðí± xþúé8«¡Â뮂h8„†ª8´Ÿr `¦Ø\[ŠÃ'N£±Züà+‚ð_i~øa 6 ñxS§NÅ[o½•õü­[·bÔ¨QˆÇã;v,^|ñE—zJ„hH’„±ƒ«M¥ö!œ‚ä¢çøØŸ‹×~:«H$=|#þþ÷¿Çòå˱fÍìÚµ ãÇÇœ9sðÅ_èžÿÆo`áÂ…X²d Þyç,X° ,À|àrÏ ‚ ‚ 3»ÛµÒ†³˜+ãQÏ+ž„ÈøF¼ï¾û°téR,^¼cÆŒÁÆQVV†Ç{L÷ü|sçÎÅŠ+0zôhÜqÇ8çœsðÐC¹Üs‚ ˆ`2vP*—fƒM3­Rû6 ©‹"|!vwwcçΘ5k–r, aÖ¬YhmmÕýŸÖÖVÕù0gÎÃóàôéÓèèèPýAö¸jÚ0<}í4ü÷´a¶þ¿®<•*«¶<æ`¯"øBpà@´µµéþO[[›¥ó`ݺu¨®®V~š››óï‚°Œ/ÒÀÔ××#ãСCªã‡BCƒþ“aCCƒ¥ó ¤¤%%Tƒ ‚ ˆâÆM±X “&MÂ+¯¼¢K&“xå•WÐÒÒ¢û?---ªó`ûöí†çAAø_h`ùòå¸êª«0yòdL™2<ð:;;±xñbÀ¢E‹0hÐ ¬[·pã7â‚ .À½÷Þ‹yóæáÉ'ŸÄßÿþw<úè£^ƒ ‚ ¢àøF¼üòËqøða¬^½mmm˜0a¶mÛ¦zìß¿!.[ü´iÓ°eËÜrË-¸ùæ›qæ™gâÙgŸÅÙgŸíÕ‚ ‚ \A’eYöºÅJGGª««ÑÞÞŽªª*¯»CA„ hÿö‰ AAa ‚ ‚  €AAƒ@‚ ‚ ˆ€A AADÀ  ‚ "`HA0H$‚ ‚¾©â,‡vGG‡Ç=!‚ Â,lßr- óàøñã€ææf{BA„UŽ?Žêêj¯»á T .’É$8€ÊÊJH’ähÛhnnƧŸ~êË25~àÿ1ú}|Ñø}|Ѳ,ãøñãhjjB(Lo8ÒæA(ÂàÁƒ úUUU¾½¡ÿðÿý>>€Æèü>>€Æh• jþÁ{ ‚ ‚  €AA#¼víÚµ^w‚Ð'cÆŒˆDüi©÷ûøÿÑïãhŒ~ÀïãhŒ„u(„ ‚ "` ˜ ‚ "`HA0H$‚ ‚$AA  ÄŽ;ðï|MMM$ Ï>û¬êý§Ÿ~³gÏF]]$IÂîÝ»3Ú8uê®»î:ÔÕÕ¡¢¢ßûÞ÷pèÐ!Õ9û÷ïǼyóPVV†`ÅŠèíí-èØùŽñèÑ£¸á†0räH”––bÈ!X¶lÚÛÛUçI’”ñóä“O||€3ó8cÆŒŒþÿä'?QãÕ<æ;¾}ûöéÎ$Iغu«rž¨sØÓÓƒ•+WbìØ±(//GSS-Z„¨Ú8zô(®¼òJTUU¡_¿~X²d Nœ8¡:ç½÷ÞÃ7¿ùMÄãq477cýúõ®ŒÈŒûöíÃ’%K0|øp”––bĈX³f º»»UçèÍãßþö7áÇÆ Ëèû]wÝ¥:§˜çðõ×_7¼ß~ûmÞÎa®1ÀÚµk1jÔ(”——£¦¦³fÍ›o¾©:Gô{±˜ °@tvvbüøñxøá‡ ߟ>}:î¾ûnÃ6nºé&<ÿüóغu+þò—¿àÀøîw¿«¼ŸH$0oÞúhÁÇä?Æýë_H&“øÕ¯~…?ü÷ß?6n܈›o¾9£½—_~Y5“&M*èØgæn¿ývUßo¸áå½bŸÃiÓ¦eÜ‹W_}5†ŽÉ“'«ÚóbÜëÍYg…‡zï¿ÿ>þú׿bذa˜={6>¬œ#ú½XTÈDÁ ?óÌ3ºïíÝ»W ¿óÎ;ªãÇŽ“£Ñ¨¼uëVåØ?ÿùO€ÜÚÚ*˲,¿øâ‹r(’ÛÚÚ”sy乪ªJ>}útFbŒ1êñÔSOɱXLîéé1Õ¶›Øã\ ßxã†íŠ2NÍá„ äýèG¦Ûv3ýxë­·dò'Ÿ|"˲,ÿãÿÈo¿ý¶rΟþô'Y’$ùóÏ?—eY–ùË_Ê555ªùZ¹r¥þ| 0Ó§OÇsÏ=WÈnÚÆh|wÝuêêê0qâDlذAåfá·9|î¹çðå—_bñâÅïÃvwwãÑGEuu5ÆÀŸ÷¢—P:mAikkC,˸¹ˆ¶¶6å^h`ï³÷Š#GŽàŽ;îP©ó”Ùæ[ßúÊÊÊðÒK/áÚk¯Å‰'°lÙ2zjïÿû:t(šššðÞ{ïaåʕسgž~úiþšÇM›6aôèј6mšêx±Ìá©S§°råJ,\¸P)8ßÖÖ†¨Î‹D"¨­­U݋ÇWÃÏaMM ½7‡Þµ|ôÑGøÅ/~{î¹G9VQQ{ï½ßøÆ7 …ðÇ?þ ,À³Ï>«knõ £ñ-[¶ çœsjkkñÆo`ÕªU8xð î»ï>þ›ÃM›6aΜ9ùÛ·oWiUðÅ_¨ÎïííÅÑ£G‹ê^Ì6FÆ0sæLL›6Í”ÓüÔ©S•ëØkÌŒgêÔ©èííž}ûøgTÀU]])¡N¤9€òòrœqÆ8ï¼ó°iÓ&D"lÚ´ €îEQ PP&Mš„h4ŠW^yE9¶gÏìß¿---€––¼ÿþûª‚- cÆŒq½Ïv`[±X Ï=÷âñxÎÿÙ½{7jjjŠê©‡¥RillàyR&§ùóç£ÿþ9ÏiÙ¦úïÿ/¿ü2êêêTï·´´àرcعs§rìÕW_E2™T„ù––ìØ±===Ê9Û·oÇÈ‘#…09å#Òü͘1“&MÂæÍ› åÞvïÞ­\Ç^bf|ZvïÞP(¤h–ü0‡ Ë26oÞŒE‹å|˜Ä™C#’ɤò è‡{Q$È\ Nœ8¡zªÚ»w/vïÞÚÚZ 2GÅþýû•gß¾}òE]$—––ÊõõõòOúSU ‘Çhôÿä½{÷ʲœ ñŸ0a‚\QQ!———ËãÇ—7nÜ('‰¢ãþýûåóÏ?_®­­•KJJä3Î8C^±b…ÜÞÞ®ú¯æÑ‰ëT–eyÕªUrss³î¼ˆ<‡,-†ÞÏk¯½¦´ñå—_Ê .”+**䪪*yñâÅòñãÇUŸóî»ïÊÓ§O—KJJäAƒÉwÝu—+ãsbŒFóÌo?þ¸ððð€³³3üýý‰ØØØ*ŸñöíÛ˜8q"š6m ™L†1cÆd2Þ}÷]õu‰‰‰ÉdHLLíûc Õ÷ôÊ•+êc7nIJeËt®½rå d2/^lq}o½õF???­ï{e•ÿÜ\\\€Ñ£Gcݺu(..®²®Å‹C&“áðáÃZÇËËËááá™L† .h+))³³3Ƨu9sæˆ^ß'Ÿ|‚nݺaôèÑX»v­Ñk4h€={öŠŠŠžžŽß~û 3fÌÀ’%K°sçN£ßSÕŸoBBúõë§>~òäIܹs...HHH@ûöíÕç>Œ¢¢"­ŸK—.!""YYY˜9s&Þ{ï=¸ººâÊ•+زe ‚‚‚››‹† bûöí=z4°hÑ"øúú"33üñ6mÚ„%K–Xô}#"DT'­[·N =zÔèuþþþÂÈ‘#õžKHH[·nÕ9÷÷ß „yóæé½W©TVùŒáááBÇŽ«¼Nõ U^[ÓFŽ)øûûë¿|ù²@øøã-.[ó{èââ"L:UïuS§N\\\ôž‹‹‹„~ýúUYW£F„áÇk_ºt©Ð¬Y3aÒ¤IÂøñãµÎÍŸ?_ œ:uJA(++ºví*¸»»«U¶cÇáÞ½{‚ ÂàÁƒ…Ö­[ ¥¥¥zŸ‡ˆ¬‡]ÀD¤WNNhµ†i²³3üχª{t÷îÝ8wëRÕÅ[¹ Ø?þø£G†‡‡œœœÐ³gOlÙ²¥Êûúô郑#GjëÚµ+d2Ž=ª>¶mÛ6Èd2œ:u €npXX¶oߎ«W¯juÁV¶téRÂÕÕÁÁÁ8tèP•Ïÿš*""3fÌÀáDZoß>£u <@YY™úxbb"ªӟ˜˜///tîÜðã?âÔ©SˆŽŽF—.]ôÖ3bÄ8;;¨øòôô„½½ng“Ÿˆ,Ç¿DuœR©DYY™ÖK ;vD£FðÞ{ï᫯¾ÒW___$''£gÏžhÕª’““‘œœŒ^½z™\FBB €ÜÜ\|ñÅøé§ŸÐ£GL˜0ß|óÑ{ÃÃñoß>”––nÞ¼‰Ó§O£AƒˆW_·{÷nx{{£k×®zËY¹r% õgHNNÖºfÅŠˆÇ²e˰aÃÜ»w=öòòòLþ¬Õ5zôh0Šnà‚‚u.//Ǿ}ûŠÐÐPdeeáìÙ³*Æÿ%''#,,LzwíÚ걜U ÆáÇñòË/ãðáÃê?"²>@¢:®ÿþpppÐz‰]\\°aÔ••á…^@`` <==1~üxüòË/FïU(èß¿?ÜÝÝÑ Aôïß_ýÞT³fÍBçαgÏŒ?ÇǺuë0jÔ(¼ñÆ(//7xoxx8 Ô-q»w›¦OŸŽÝ»w«¯Û½{7†j°œN:¡Q£FêÏ£zirssï¿þŠÈÈHDFFâ믿Æ;wðÛo¿™üY«Ëßß‘‘aô:ÕX>UK߉'››‹ÐÐPtèÐÞÞÞHHH:tHgü_ZZ 00ФçZ¸p!ˆåË—£ÿþpqqÁ€°páB˜õ‰H\ €DuÜúõëqôèQ­—¾.7K<öØcHKKCll,æÎ‹Î;ãÇÄèÑ£ñÏþS”:ô¹xñ"Ο?§Ÿ~´Z7{ì1dffêÌXÕ4`À899©Ã^||<ÂÂÂðè£âàÁƒ(,,Dzz:RSS^­g9r$är¹ú}·nÝW¯^­V¹æÁ¤ëºuë†&Mš¨`bb"|||Ô?¬€ªk4 ¹š4i‚¤¤$=z .Ddd$þúë/DGG£k×®¸uë–ÅeQõ0Õq;vDïÞ½µ^bjРÆŒƒ?þ{÷îÅŋѩS'¬X±gΜµ.•›7oæÎ«Óº9kÖ,0œœœ0`Àuüý÷ß1lØ0„……A©T"))IÝ\ÝØ¤I­÷ …@ÅLÝš¢ ›†f«Èd2„††âÀ(--EBBBCCÕçCCC±wï^‚€„„øøøhÍ0oÙ²%àòåËf=_ïÞ½ñÚk¯aëÖ­ÈÈÈÀ+¯¼‚+W®`Ñ¢Ef•CDâa$"³´lÙÿøÇ?@²èéé ˆŽŽÖiÝT½zôèa´Œ¡C‡âÈ‘#8rä®]»†aÆÁÍÍ }úôA||Ó¦MƒŸŸnß¾sçÎáøñãØºu«Ñûƒ‚‚иqcìÚµ Ó§OWÇû￯þº*]»vŶmÛ°jÕ*ÁÎÎN´nö½{÷";;@Ålî«W¯âûï¿PѧZ¨˜±«šÔR\\Œ´´4üöÛoزe :vìhÒò8@EÈkÚ´)bccáåå…Ž;ªÏÉd2 øàDEEIº–Û!CpäÈ|øá‡˜3gîܹƒ&Mš S§N?~|•÷ÛÙÙ!,, ±±±ZA/88...:3\ ™={6Μ9ƒ7ÞxyyyÁäIUyçw°wï^õûÄÄDõä‹„„­Ö¹¢¢"¨—éåå…îÝ»cõêÕxúé§áèèhr½aaaزe‹V÷¯Jhh(bccáçç‡6mÚèœoݺ5Ž?ŽåË—#66«V­Bqq1|}}1xð`ìß¿ 6P±ÕÝO?ý„O>ù™™™êëÂÃí>‰¨fɱþ%#"""¢:c‰ˆˆˆl  ‘a$"""²1 €DDDD6†ˆˆˆÈÆ0Ù@""""Ã… «¡¼¼pssƒL&³öã‘ AÀÝ»wѬY3I´¯Í«!##£^l$ODDd‹ÒÓÓѼysk?†U0Vƒj3úôôt¸»»[ùiˆˆˆÈùùùhÑ¢…ú÷¸-b¬U·¯»»; QcË÷l³ã›ˆˆˆÈ†1Ù@""""ÃHDDDdc‰ˆˆˆl  ‘a$"""²1 €DDDD6†ˆˆˆÈÆ0Ù@""""ÃHDDDdc‰ˆˆ¨VHË)DÌŽs¸™ßÚRïÙ[ûˆˆˆˆ`ÂWÉÈÌ»#Wn#vÖk?N½Æ@"""ª2ó*ZþRÒr­ü$õ ‘a$"""²1 €DDDD6†ˆˆˆÈÆ0Ù@""""ÃHDDDdc‰ˆˆˆl  ‘a$"""²1 €DDDD6†ˆˆˆÈÆ0Ù@""""ÃHDDDdc‰ˆˆˆl  ‘a$"""²1 €DDDD6†ˆˆˆÈÆ0Ù@""""ÃHDDDdc‰ˆˆˆl  ‘a$"""²1 €DDDD6¦NÀ•+W"00NNN BRR’ÁkW¯^Aƒ¡qãÆhܸ1ÂÃÃqäÈ­kAÀ»ï¾‹fÍš¡Aƒ Ù3g¤þDDDDVUgàæÍ›1gμùæ›HIIÁ Aƒ0bĤ¥¥é½>11“&MBBB’““ѲeKDDDàúõëêk-Z„¥K—âóÏ?ÇÑ£GáããƒaÆáîÝ»5õ±ˆˆˆˆjœLÁÚaŠ~ýú¡W¯^Xµj•úXÇŽ1fÌÄÄÄTy¿R©DãÆñùçŸcÊ”)Íš5Ü9sðÚk¯Š‹‹áíí>ú/¼ðB•eæçç£aÆÈË˃»»»åŽˆˆˆðúvõ×WŽ”¬þþ®#-€%%%8vì"""´ŽGDDààÁƒ&•QXXˆÒÒRxxx._¾Œ7nh•©P(j°Ìââbäççk½ˆˆˆˆêš:oݺ¥R ooo­ãÞÞÞ¸qã†Ie¼þúëðóóCxx8¨ï3§Ì˜˜4lØPýjÑ¢…¹…ˆˆˆÈêêDT‘ÉdZïAÐ9¦Ï¢E‹ðÝwßaÛ¶mprr²¸Ìèèhäåå©_éééf~""""ë³·ö˜ÂÓÓr¹\§e.++K§¯²Å‹cÁ‚ؽ{7ºuë¦>îãã ¢%Ð××פ2  …¥ƒˆˆˆ¨V¨-€ŽŽŽ B||¼Öñøøx„„„¼ïã?Æûï¿;w¢wïÞZçáãã£UfII öîÝk´L"""¢º®N´@TT&OžŒÞ½{#88_}õÒÒÒ0sæLÀ”)Sàçç§ž¼hÑ"¼ýöÛØ¸q#Ô­‡®®®puu…L&Ü9s°`Á´mÛmÛ¶Å‚ àì쌧žzÊjŸ“ˆˆˆHju&N˜0999˜?>233Ñ¥KìØ±þþþ€´´4ØÙ=lÐ\¹r%JJJðä“Oj•óÎ;ïàÝwß¼úê«(**¬Y³pçÎôë×»ví‚››[}."""¢šVgÖ¬¸Ž‘x¸`Í©c‰ˆˆÈ¶ÌÙ”‚„óYÖ~Œz‹ˆˆˆjOd`ú7G­ýõ ‘a$"""²1 €DDDD6†ˆˆˆÈÆ0Ù@""""ÃHDDDVwùÖ=k?‚Ma$"""«ºW\†!‹­ý6…ˆˆˆ¬êVA±µÁæ0Ù@""""ÃHDDDV%ƒÌÚ`s‰ˆˆˆl  ‘a$"""²1 €DDDD6†ˆˆˆ¬JÆ9 5ŽˆˆˆÈÆ0Ù@""""ÃHDDDdc‰ˆˆˆl  YÕýR¥µÁæ0‘U-û=ÕÚ`s‰ˆˆÈªN¦çZûl Y‚®y €DDDD6†ˆˆˆjÌ_7ïrÒG-ÀHDDD5"þìMD|²O¬:híG±y €DDDT#¶þ‘8“‘¯u<ýv‘5Ǧ1Q¬ý¤ÆHDDDdc‰ˆˆˆl  ‘a$"""²1 €DDD$©Â’2¼ø¿cˆ?{ÓÚB0‘¤¾NºŒßNß°öc@"""Ý‹·µùr Ks¯ÄÚC•Ø[ûˆˆˆ¨þyzÍa€{+? éÃ@"""’LöÝbÈd†Ï —‡¶@"""’LG¹Ñów‹Ë ž3©z‰ˆˆH2Ue¸ÛÏY|/YŽˆˆˆ$#‰r\½cðœŒM€’a$"""Éâ·ü÷T\̺kt ãŸt‰ˆˆH2ô¼%ñ!|é>£÷²P: €DDD$jLò5ÖuLÕÃHDDD’)ãËÀ»™ùO2 €DDDd=F óŸt‰ˆˆH2¥å‚Ñ g¬c¥ÃHDDD’¹gd¡çªp  t‰ˆˆH2…ÅJk?éÁHDDD’Qr¯ßZ‰ˆˆˆ$õçõ<‹î3´† UŸ½µ€ˆˆˆê—òò‡Á협­ÞÝ ä~i9Aà–p` ‰ª\Än߃—rD+‹b$"""Q‰9î/ív¡heÑC €DDD$ªòrÓ¯å(?ë`$"""Q™Ó\Õ¥œD, @"""•9]Àœék €DDD$*ÍYÀT;1‘¨ÌÉé·‹¤{2ˆˆˆˆD¥d `­WgàÊ•+'''!))ÉàµgΜÁO<€€Èd2,[¶Lçšwß}2™Lëåãã#åG ""ª5Ê˼ýãil<œ&~Ùœ¹QëÕ‰¸yófÌ™3o¾ù&RRR0hÐ Œ1iiúh ѪU+,\¸Ðh¨ëܹ3233Õ¯S§NIõˆˆˆj•“×rñí¡«x#ö”ècöÄ €œ$" ɶ‚+..Æ‘#GpåÊÂËË ={öD`` Ùe-]ºÏ=÷žþyÀ²eˇU«V!&&Fçú>}ú OŸ>€×_Ý`¹ööölõ#""›do÷° èç“ÓÓO´²Åìfc¢4D€ÄòåËñã?¢¤¤5Bƒ pûöm£U«VøÇ?þ™3gÂÍÍ­ÊòJJJpìØ1 ƒVëYSSSѬY3( ôë× ,@«V­ªU&Q] ¹½î—ûþ5š³tU˜ÿ¤!jpdd$ž|òIøùù!..wïÞENN®]»†ÂÂB¤¦¦â­·ÞÂï¿ÿŽvíÚ!>>¾Ê2oݺ¥R ooo­ãÞÞÞ¸qã†ÅÏÚ¯_?¬_¿qqqX½z5nܸääÞs°¸¸ùùùZ/""¢º®L)bbƒÈcÙ( Q[#""°uëV8::ê=ߪU+´jÕ S§NÅ™3g‘‘arÙ2ÍÿªAç˜9FŒ¡þºk×®FëÖ­ñßÿþQQQz‰Á{ï½gqDDDµ…fH+9й0ãŸ4Dm|饗 †?Meeeèܹ3† V嵞žžËå:­}YYY:­‚Õáââ‚®]»"55Õà5ÑÑÑÈËËS¿ÒÓÓE«Ÿˆˆ¨&iÓ+UŠ< „ck½|öìYDEEÁÏÏôqŽŽŽ Òé.ŽGHHˆhÏV\\ŒsçÎÁ×××à5 …îîîZ/""¢ºHÊÀËã’2Òl°JAA6mÚ„¯¿þGEÿþýÎÌÕ'** “'OFïÞ½Œ¯¾ú iii˜9s&`Ê”)ðóóSÏ.))ÁÙ³gÕ__¿~'Nœ€««+Ú´i˜;w.üq´lÙYYYøàƒŸŸ©S§Šøé‰ˆˆj'A#XeÝ-µìâ2ñ óŸ4$ €û÷ïÇš5kðÃ? 00gÏžÅÞ½{1`À³Ëš0arrr0þ|dff¢K—.رcüýýiii°Ó˜Îž‘‘ž={ªß/^¼‹/Fhh(×®]äI“pëÖ-xyy¡ÿþ8tèºL""¢ú,õfdeß/UŠVóŸ4D€‹-ÂÚµkQPP€I“&aÿþýèÞ½;иqc‹Ë5kfÍš¥÷œ*Ô©hýÏFŸM›6Yü,DDDuÝëÛ¤Ûü@Ü@F@)ˆßxã ¼öÚk˜?>är¹ØÅQ5HÏÕzÐÄY´²Ïfäãß[NŠVICôI óçÏÇÖ­[ˆ×^{ §OŸ» """ª†—6×zßÚËU´²û, ·ï•ˆVž{ÑÊ¢‡D€o¼ñþúë/|ûí·¸qãú÷ïîÝ»CܹsGìꈈˆÈL…%eZïÅ\·O,½ý+†É«±æ/&Ù20¡¡¡øïÿ‹ÌÌL¼øâ‹ Bhh(BBB°téR©ª%""¢*ØËµý[²lߥì¤ß.é‰t¹:UŒRã20Ò|@777Ìœ9‡FJJ úöí‹… J]-‘M*)+ÇõÜ"³î1wáæüû¥ºd/-J0ë>s¨Zþ˜ÿ¤!z|ê©§°e˽ûävíÚË–-Ãõë×Å®–ˆˆˆLZ}îÁ‘Ë·M¾Gif¼‘w_ýµ˜»~hRm÷Ê@iˆÛ·o>úM›6EDDV¬X¡³ešƒtIáØÕŠñö1¿Ó{^dWZøÙÜ1€vÃòÊ$ €ª:$*Þæ‰ßyç;v /^Ę1cðóÏ?£mÛ¶èÕ«Þ}÷]¤¤¤ˆ]%U’’–‹¸37tާfé.mþZ{`JÚä–šûxU²c  ¤$ؼysÌš5 qqqÈÎÎÆë¯¿ŽÔÔT :þþþøç?ÿ‰3gÎHU=‘Í[“ô·Î±¼"ݰfn°¦ _Â%‰ßoˆjƒ/.- É'AÆ 6 ;;k×®…\.GrrrMTODDd“Ž^¹£50¯¨ÿ÷…îï^¥™«òÊ,b®û÷°U  èE$Ü ¸¨¨‚ ÀÙ¹buñ«W¯"66:uBDD†*UÕDDDôÀø/“qeáHÀ‹·ô^cîDŽšX™]ÀÒ’¬022ëׯäææ¢oß¾X²d "##±jÕ*©ª%"""ŠË”z›Û,«Å™U“@ªÓ=M†I?ŽAƒ¾ÿþ{øøøàêÕ«X¿~=>ûì3©ª%"""JÊÊõ7·•MêqyvöQ·²P’ÀÂÂB¸¹¹víÚ…qãÆÁÎÎýû÷ÇÕ«W¥ª–ˆˆˆ +JÝ('·“©Ç² X’À6mÚàÇDzz:âââÈÊÊ‚»»»TÕ‘%f{˜ÛÍ*u `¹ hŒ”´*›%Yœ7oæÎ‹€€ôë×ÁÁÁ*Z{öì)UµDDDd€²ÜP  yåHÊÊA½[¥!Ù,à'Ÿ|Dff&ºwï®>>tèPŒ;Vªj‰ˆˆÈ€R-€•C– xï—³hÝÔ“ûûë\/õÄŒrëJMôجY3DFFbôèÑ:t(|||´Î÷íÛWì*‰ˆˆ¨ Åeø8î‚Þs•ÝWïà›ƒW@oÔ×*wç^ >Ü¡û9s ‚Àu%&zðÆáì쌗_~žžžø¿ÿû?|ûí·¸}ÛôM©‰ˆˆÈ|ÆÖó[»ÿ²É÷–è_.FE_£ÜÆ#iøþØ5ãh¢rAs/`&@)ˆðdɤ¦¦"99½zõŠ+àëë‹°°0|òÉ'¸té’ØÕÙ¼2#0ív¡ÖûØY!Xôd7€²RÈr?\çO_¬¾PváÆ]³žÕ;™ ÓbãŒ~x¢WsÑÊ¥‡$Ý ®sçÎˆŽŽÆ¡C‡––†§Ÿ~{öìA×®]Ñ¥Klß¾]ÊꉈˆlJ™IPTªÝªçæd.ÍÐífu”?Œ%JÝ2õÀŸOf˜ó¨F½=ª#Z{¹"¤µ'Zx8‹V.=$Ù$ʼ½½1cÆ Ì˜1÷îÝî]» P(jªz""¢zÏX à¡K9Zï=\Ⱦ[ @· Ø^#ÞÌ+F Z»H=.Ï¿‰‹´´-€†¸¸¸`ìØ±·FõDDDõR™Y¾—² s¯Dý¾_ <\6Œ IDATn·V©EÏÞîaØüqþ½å¤ú}úíBl9š^íg7ªþ^ðXµË!ˈKKKñꫯ¢M›6èÛ·/Ö­[§uþæÍ›ËåbWKDDdóÊôt×@êÍ­÷th °{ô*Ï®¼Õï¶”ëꯇ,NÄæ?ªÝœìÕõSÍ=~øá‡X¿~=fΜ‰ˆˆ¼òÊ+xá…´®áš>DDDâ3Ôl_)hɼ—Øoר¯iCu(ì­Ò©H} à† °fÍŒ5 0}útŒ1Ó§OÇÚµk@k‰ÃPðóëÿÐz¯ÚfMn Ð’v¶íÔ-¢Çõëׯ£K—.ê÷­[·Fbb"’““1yòd(•Æ×""""Ë”™¬IüdÆZ²ö^å2¨v=úøøè¬ó׬Y3ìÙ³GÅÔ©SÅ®’ˆˆÈæ\̺‹o\F©Æ¸?S·hS½SÁʳ€ •r&#Ï`™ænǸh]¢w?òÈ#ظq#†ªu\ÃÂÂÄ®’ˆˆÈf—)1û»Øy怊ý}g nõàkÓZUCåêíÖLkùÙ~K™j!ÑàÛo¿óçÏë=ççç‡}ûöa×®]bWKDDd¶MW‡?HI¿£þÚÐÀÊTÁOÕX.hï¿ûëÉL±—j)Ñ»€ýýý1|øpƒç}}}Ù LDDd¡Û÷JõÏ+*EäŠ&•¡šbg`qçµ ïl®–ÎXÿl_ã½Z6­2Ÿ¤;\¿~@VVÊ+ L}ùå—¥¬šˆˆ¨^úóZ®Ö{*BÜ×I›\†Â¡¢ýG®•å‚zL ˜ö½:Dë}D'oÌ o‡6M]E¯‹L'Y\·nfΜ GGG4iÒDké™LÆHDDdßÏgé=þÙž‹&ÝߣE# ïì°ÓèTûKI»£ï6Ñx»;¡S3wIë ªIçÍ›‡yóæ!::vv\’ˆˆH*¦n°Ð«e#l›5@ý^» ¸¢Œ?¯žé+)ZÉ|’%³ÂÂBLœ8‘ለHb%&Îþ­¼T‹fS+.“v½^îQ;H–Ξ{î9lݺUªâ‰ˆˆlNFn‘Þã¦Îþ­¼›V àƒ Y\jZ˜´”œ °V¬ 8&&£FÂÎ;ѵkW888h_ºt©TUY²\€LÜíO_®g>™áýyõ=“&Í@U°™ë9›]ÀµƒdpÁ‚ˆ‹‹Cûöí@gQ}U¦,LjO“ÐÈÙ[g†ˆVnνƒõ™ô\:-€¿Vmå&õ¯hf€ÚA²¸téR¬]»Ó¦M“ª ""¢ZébvR³ H·¼Š¦ÊÁîļahà(Gû·vj¯¼å›L&ƒLÂÃsRÇ39§Ô ’ý1( 0 ê ‰ˆˆê”s¿TâIÐÝ®‘³#örkõu«ÆäÕT  [kÉàìÙ³±|ùr©Š'""ªµìj. 0}ˆÂ^÷×¾ævp@Ek ”<]ÒV@&‘¬ øÈ‘#سg~ýõWtîÜYgȶmÛ¤ªšˆˆÈª°×sÞ®Ò@'ñF‡9È9Þ¯¶’,^¾|eeehÛ¶­ÖñÔÔT888 @ªª‰ˆˆ¬JsNFåµ÷L¡šÔ!û4 éwŠ0¶gÕ­göF—¡Éª.àð¥{1oT'8é™K ãƒÉÅe帒Se¹€ï]«ò>c-€†&ߪ&(˼óóu륳£v¼™ß”GGD'oõ×Ï'¡j,¦¤¤è]¦ÿþ8qâ„TÕYV°%ðA3u_cë Ê ¬ðW¹eðƒíçô–ÕoÁïÕ~ª=$ €2™ wïÞÕ9ž——¥RÚ)ñDDDÖ¤úLݦM“ª%ÏÔå]Tì@C-€"ç5濺A²8hÐ ÄÄÄh…=¥R‰˜˜ 8Pªj‰ˆˆ¬N©Õh^+ð° ¸òÏU1Ö\P\¦÷¸¡±–®_¨¹Õ›Ôk ’å$›²hÑ" <íÛ·Ç A3™’’’ŸŸ={öHU-‘Õ•W³pÿÅ[X´ó¼Ùkæå•<Òº‰Þㆠ±YÇÆÈµ `m%Y `§NðçŸbüøñÈÊÊÂÝ»w1eÊœ?]ºt‘ªZ"""«+«æ$AV&^BlÊu³îK»]¨÷ø„Þ-àæä ÷Ü 'w˜ÊŽ{ýÖ ¢·~õÕW=z4|||ЬY3,X°@ì*ˆˆˆjµâ²‡]·™y÷±"á"&ôiaö6hWnÝ3ëz7'ý¿ÖCÚèoý“‚L†°ö^H¼ÉÁþ5V/™GôœþÝwß! ýúõ pöìY±« ""ªÕŠ5ÆÏ­J¼„ã.àŸ›]N‘‘qxãô¬ 8¬£·Öûý¯ ÁÏôÂèîÍ̮ۘ½ÿ Ãê)½õž“ÉdXõt6Îè‡WÂÛ‰Z/‰Gô˜€ÌÌLüë_ÿ‰'ŒÖ­[#** ‰‰‰(·`0,Q]¢Ù¨rèïÛê¯7NÃ{¿œÑ#§o¼œ±‰žnzZ+ çkÞØvñÕš˜Q]Žr;ø7qÁ°NÞmç¥s^.“¡£!­=õÓÔïKÊ—é €þØÒÃr;zÚûëêkêGP{õÑöX´ó‚ÎqG¹ÞnéÊ!³ò:‚ÕÉOôjŽQÝÍÿ d>É`JJ Ž?¥R‰öíÛC¤¦¦B.—£C‡X¹r%¢¢¢°ÿ~têԩꉈˆªéÏëy¢ÀßÏeáäµ<œ¼–§«ZóO3üãþŠŒó»W¢{NµKˆ¾Æ¾Êݱ¦ÞÙGoìÑ¢Ž\¹­s¼r½•[ -,mŠ%ã»[|/™G²1€‘‘‘GFFŽ;†ãÇãúõë6l&Mš„ëׯcðàÁxå•W¤z"""-–l˦O¾7Ì]ô¹¤¬Üè=ÙzZUÝÆúX2þÎÐþÁ p(z¨NÀSVÕÈ.à:A²øñÇãý÷߇»»»ú˜»»;Þ}÷],Z´ÎÎΘ7oŽ;&Õ#i)³p{³Ê Íòµ$V5c¸²&“®¼lL÷Ì*GÅP‹ ðiè¯J‹WW®×P€¤ÚM².༼û,bccqíÚ5\¿~±±±xî¹ç0fÌÀ‘#GЮW '""ñ•”•ãDz®ÖR+É—rÐíÝ]xëÇÓf••|)S×AZNÅ^»rð¨Ùê§wm¾*žÑPw²>¿üs :ljN òo¬ÄL%7„U‡ ŠË´ŽWx•»¤ÍýuH¿üòK :'N„¿¿?Z¶l‰‰'bèСøâ‹/:tÀš5k¤z""²Q‚ àß[ObÌŠøtwªúø'2*MƨʤՇ°÷¯lükS ínÏy?ŸÁñ´ŠÍiÍ€Â%Æ®û IIIhÖ¬Ú·o^½zi½LURR‚cÇŽ!""BëxDD4}à¬e#??_ëEDD¶ÇÐzÒ}¤ÙÞ´[ó†ˆìá'IÙ†¨º€cÆuÕ:þÙ¤žFï3· ˜¬C²uUK½T×­[· T*áí­½½··7nܸQ£eÆÄÄà½÷Þ³¨N""ªûT]¿†&:4vÑ?¡¢.R5>Þ½¢·$½:-<œÞÇuëÉà;ï¼#jy•£j.¼YSeFGG#**Jý>??-Z´¨Ö3QÝc(âÔ§ì£ú}誰Ǹ^~(¸_†æ¼¾yã¸v§Z{ÖÔ#R5H 77ßÿ=.]º„ÿüç?ðððÀñãÇáíí ??Óš²===!—ËuZæ²²²tZðLei™ …Âà˜B""²Ž‹Y(´ó6o'Œ{Åef/ž¬ûg¨›Sª5ð¬,5ÛC–ޝzɶ=ÿCaI9s2G] ÙÀ?ÿüíÚµÃG}„Å‹#77‹èèh“ËqttDPPâã㵎ÇÇÇ#$$Ä¢g“¢L""ªy%eå_ºŸìCQ‰y+Odæ™]_©R@öÝbƒAOªîÏ ÿÆ’”kŒ¹}lŽöv uˆd0** Ó¦MCjj*œœœÔÇGŒ}ûö™]Öš5k°víZœ;w¯¼ò ÒÒÒ0sæLÀ”)S´BeII Nœ8'N ¤¤ׯ_lj'pñâE“Ë$"¢ÚO3ôåß7}K5Àü}{U‚c~×ôŽ\¾=ç³,*Ó½ÿ Ãó1{h[QË5…]5‡YQí&YðÑ£Gñå—_ê÷óó3{òÆ„ ““ƒùóç#33]ºtÁŽ;àïïHKKƒÝÃ,›‘‘ž=ÎRZ¼x1/^ŒÐÐP$&&šT&Õ~šKŽ˜›WÊ”–À²roÆêî%<þËd‹Ê3d~dgø7qÁ[£:‰Z®©˜ÿê7É “““ÞeR.\¸///³Ë›5kfÍš¥÷œ*Ô©˜4 ÝX™DDTû•i¬:üÊæfÝ«¬ÁMký›8ãjN¡Y÷4q‘èiLÃX¿IÖ‰ùó磴´¢I^&“!-- ¯¿þ:žxâ ©ª%""R¢\Ì1ë^K»€-ÑÕ¯!V™¹Õ™ÜÒ•˜ERÝ•6¨v“,.^¼ÙÙÙhÚ´)ŠŠŠŠ6mÚÀÍÍ ~ø¡TÕ‘ )µ°¨Ù@¹ î Œ¯ø¿çúáÇ—¨ß[{ ã_ý&Y°»»;öïß={öàøñã(//G¯^½.U•DDdcJÍÝx׫+3´‡ä2Y•n`[OœË|8tÊÚ-€9£·^“t@xä‘GðÈ#ÒlTMDD¶­¤Ìüçú`í¿šl´³“Ý[wdW_€½ÆErÉúèLóBh+ë>IJÔ¯M›6™|mzz:8 fõDDdc,itx¬jr  ½Ìh‹Þ¸^›#h^cÍ1x½ýÃÍ©þlkGºD €«V­B‡ðÑGáܹs:çóòò°cÇ<õÔS Âí۷ŬžˆˆlŒ%cU!KYñƒæ²³“ tªîa{%ÍäV €5ŽÉ:DíÞ»w/~ýõW,_¾o¼ñ\\\àíí '''ܹs7nÜ€——¦OŸŽÓ§O£iÓ¦bVODD6Æ’@U7kM4Öh÷àœ\®Ùl½ÚÎüåÚ¨n} à¨Q£0jÔ(äää`ÿþý¸rå ŠŠŠàé鉞={¢gÏžZ‹6YªÄ‚(W@ÓZ¹ÊEh “ÛÉŒ¶è©²žæ@kÍ^üÝñxw_«ÔM5G²I Mš4Add¤TšԂI ª1€¦N£;ÔN&3º°²*j†>'‡šk,‘ÛÉ ,ðH‡¦x2¨yÕKÖæ8""ª³L¸ýåpv”«ß«[M(Æla{¹ñ.`û¡TógGÉêPÛ9{žˆEOv«±:ɺjî§‹ˆˆHí<;Îfæc÷¹¬*¯oéáŒ'z5Ç·‡®¢{ó†ênVSƒ]©cíªXÐA®{®Fh•Z[o7¼m¥=‡É:‰ˆ¨ÎÈÌ+ªÄKfÝ#·“áÇ:¢w@c në…×~øpõö=$¥f£O€âÎÜÀÀ6žhâªÐ¹ÿ»Zï;ùºãl¦î^÷ÆŸðt5¼°²ª[ÚÃÅSƒý!·³CÃ*v!ª@""ª3òŠJ;GÕ•Ù£b­=û­m+.aEÂ%ô ðÀ‘+·ÑÎÛ»^ ‚ ^¶åÿ¾HÖ*ïùAˆÚrÒ¬gËdhâªÀÚi½ñì7èœw´8"ë½È.f•Md ÉÆ<—™™)UµDDTOeæaÒW‡Ìºg`OcòJ+Q¹R±&í_7 §¯ç¡×ûñøß¡«:÷Žíéow'³žA³Î!íõ/æhím?ÈæHö׳gO?~\çø÷ßnÝ8È”ˆˆÌófìiÜ)4­ð‡CÙ£–Œï®sξŠõõæn=‰;…¥xëÇÓ:ç>™ÐâåYT­Žš‹AwoÑHýµƒ= Õ,É~↠†,\¸‚    Ó¦MÃÔ©S1oÞ<©ª%"¢z*ëî}“®sSØ#È¿1>ØSok9­mk’þÖ9f¯gÂFUU<}“@ˆ¤$ÙÀåË—cäÈ‘˜>}:¶oߎŒŒ ¸»»ãèÑ£èÔ‰3ˆˆÈ<2˜’¾}¾ŸÑóŽU„1ÍVº¶ënkjÉúêlѸN¦çVœg0Õ0I'DDD`ܸqXµjìííñË/¿0ü‘ÙA0yÙ–¦nº3y5UÕWU¾«ª ¹ª:›='Ósa'“á×?+ÆÄ;0R “ì'îÒ¥KƯ¿þЏ¸8¼ú꫈ŒŒÄ«¯¾ŠÒRógq‘íšµá¸ÉK¯T¦Fv؈M¹VeЬn `G_wLìÛÖÃH5M²Ÿ¸=z 00'OžÄ°aÃðÁ`Ïž=ضmúöí+UµDDTývú†É×VÕÅë(7¼Àò+›Oâ|¥uÿ*³·`?{}÷hncÌ1€TÓ$ €+W®Ä¦M›Ð¨ÑÃYN!!!HIIA¯^½¤ª–ˆˆê¨´œBÌüö’R³q« ØârªOg¬Ð–4Öéë6V [eÌ,&ªÉÆNžÿüs 0@ªj‰ˆ¨–+/s¯^•vì0u¶¯ ášbÇ©ðrSàëi}ðê÷'M®¿&Çš;ƒ™¨¦HÇŒ£õ^&“ÁËË <ò–,Y"UµDDT˽¸áâÎÜÄ–‚Ñ7Ч®å¡D©49,m=–Ž#o†£¥‡ ÆöôL À¯f"´fÔš³´%Fˆj‚d°¼œë‘®¸37k’þFoÿÆxüóý€þ­¢Ú¸^~ØvüºIך³°œ Tˉþ”¿ÿþ‚¾Õ<‰ˆ¨Þ»_ªT/è|5ç’R³1våäktýªbvœ3«ü>nß§…OZaá¸n&_kN »€©¶=¶mÛÙÙÙê÷&LÀÍ›7Å®†ˆˆj¡Ü‡A¯ƒ“¿>‚”´\ô˜¯smâ…lüúg¦Ée÷ ôÀÎ9ƒájÂL_S9ÚÛaÕÓ½LºÖœ%]˜ÿ¨¶=VnýÛ±cîÝ»'v5DDT •jté–‰¼òáËÕ›%lH°‰»ˆ˜³¤×ÿ£ÚŽÃT‰ˆH4š‹9—)ëÆp S÷öe0Õ'¢@™L¦3Ë—³~‰ˆlƒf«_©ÈÄŒë*jy*¦†5sZõØHµè A ‚€iÓ¦A¡¨Øãñþýû˜9s&\\\´®Û¶m›ØU‘Ý/Uâ‡ã×ÔïÅnœ(âäM¦ÎØ5%(.ŸÔ älø ZOô8uêT­÷Ï<óŒØUQ-ôéï©X•xIýþÕïOZTΫ¶Ç¢tŽKªL-Ö”®âÇ»7«æÓÕ ÑàºuëÄ.’ˆˆê€ø³Ú+>\É)4» ';¼ÚZŸêׇÿÎÁc¤éþLïÚ5vY÷æ Ñ^¤'"’÷&""Qˆ±vòZ>ý=Õì²–Žï®÷x™Ç6v6=¶lâ,á“Iˆ¨ž˜µá–í®c{ÿʽüµû/ãƒíç0óÇÔÇÆ¬8`QYAþñŸáº ' Vœ"“ÉðéÄzÏ{X ? ‘´‰ˆê¸‹Ywñáö³Øqꆤõ|¶ç¢he¹99ÀÍ©ö­D¦¯kúé~-ááâ¨s|õ”ÞðpqÄ7=8êÞ IDATÓûÔÄ£‰ªöýí#""“ܾW‚Ø”ëxÿ׳Ö~³¹*ìñD¯æXŸ|Ûx⛃W¬ýHtwy¦KƒÛÐ ëäðŽá’íQL$%@"¢:èÚB ü(ÁÚa1¹ . {ìŽ €Ú+µÚUîþ¨®b0QôÒFñ'y`/g #ÛÀHDTºVõ¾»÷K«·@óÁK·ðòw)È)(®V9•ù×â´•[ü¬9)…HJ €DDuÈÝû¥˜ºöLY-¥ Øðú|Åe(¯¢§VÆÏ'3ðÑÎóæ>&Üöx1¬µÞsqsëókÔ@}Ÿ5ÙÛiÿZ´æ²4DRb$"ªCV']6y‰} 4ç–`ö¦ty'Ïþ÷¨Iå\Í)ÄÏ'3ÌzNÀKCÚ MSWsNrcëŸë‹‘Ý|±efÕ[²I©ò@¥¦&’ Q’•ßà¹=ÒFë}q™nð±§ðÓ‰Š0—xÁ´ yøòm³–® {üï¹~ZǽÜz¯oíåŠOõBG_w³ê[åÈíߨ¾b$"ªC í•ÛÑ׳‡¶Åàv^êcez®=x)G²gÓ¤ª¹R*~|i@Ôo©Ê¯‘³îúDõ ‘•LÏÅÊÄ‹(SïnT– ÷Êör;¼ûx'õ±Êã×>‰ÿ ¹…¦í\RV½®OÕä Ø«e#õX¿ÚʾRüÇ VVz"iq@""+‹|°š›“&÷÷×{MüÙ›ø×wÇ –¡j¹jåå ¹ ¥JA'Pš³gonaIÕ¡|i쯛v»¨ZeÖÍÀ_ÿ5õìBT°ˆ¨–8‘–‹Ø”k¸{¿B¥õGf¬ÿ÷KËq¿TËœæVeþM\î.6ÅíjÀÒáSs¡äÛ÷Ä]NF š°ªE ‰ê2¶YÑÁK·Ô_ÿpü~8~Mëüê)½1¬“·Áû=Ñ _컄ù‘]ÔÇTݘšÝÅ?»¦s¯!9ÅØ}ö¦É×ë£oý¼º°¢Šf0'€P}ÆHDd%‚ à©Õ‡^3cý՘ءiLfß§Æ÷i¡uÜA^ѹ£9 äß[O|†ÊÛ™E|²9÷ª×¨°¯›LÚ-€V|"‰Õ™¿¡+W®D`` œœœ„¤¤$£×ÿðÃèÔ© :uê„ØØX­óÓ¦MƒL&Ózõïß_Ê@D¤VP\†Àè&]«oÝ¿>1L=W?ÜÎ쯛w«,»òD‘2eyµÂ߯çû¡mSWüïù~U_\ i@&@ªÇêDܼy3æÌ™ƒ7ß|)))4hFŒ´´4½×'''c„ ˜ú(233Õ¯;Lûǘˆ¨ºžYc¼å¯*û´„»“ƒÞs.Ž;ªqx…%†w©<Û÷ÍØÓ&Õoh;·6žˆ EŸ“Ê©m8lE€K—.ÅsÏ=‡çŸ;vIJeËТE ¬ZµJïõË–-ðaÃ: ::C‡ŲeË´®S(ðññQ¿<<êæ?XDTwĦ\CÀëÛq"½ê½|QµòéÓÚ«bHñƒp÷Ç•;¯-Õ˜),6ÿ‘^eÝnNö˜¡±<Êë#:`X'oìœ3¨Ê{k;Í­àä €TÕúXRR‚cÇŽ!""BëxDD<¨÷žääd뇮s}bb"š6mŠvíÚaÆŒÈÊÊ2ú,ÅÅÅÈÏÏ×z™*îÌ ¼²YÿXªTž¥¦oæš9×O˜0#GŽD—.]ðøãã·ß~Ã_ý…íÛ·,3::yyyêWzzÕ]%DdÛ’R³ñŸ­'‘™/î"ÈFZõ•ZïW=ð\f>æýtÆhž.87ÿQÚ$ªÊý[U ¯iÓÔÕø…µ€ÜŒÏET—Õúe`<==!—ËuZû²²²tZùT|||̺|}}áïïÔTÃ+å+ (ú72'"Ògò×GyE¦mÁ¦ÞÑ}ãäµ\ì8¥ÛÛá`d `åÀÂÃ-{ªÅ¢G|j|eH˜¦÷¸‹Âø¯’Oõšý—1¡wíï5áÚd+j}  ££#‚‚‚¯u<>>!!!zï Ö¹~×®]¯€œœ¤§§Ã×××à5DD–ÚedaåI}[ aí½C;6Å ¡­±òé ¼4¤µÎ=æŒ<“‘gðÚêìù;oT'LêÛRÝÂgHW^{´<],®«¦pâÙŠZ ** kÖ¬ÁÚµkqîÜ9¼òÊ+HKKÃÌ™3S¦LAtt´úúÙ³gc×®]øè£pþüy|ôÑGؽ{7æÌ™(((Àܹs‘œœŒ+W® 11?þ8<==1vìX«|F"²M ÇuE̸n8=ÿ}¶¯ú¸æ¸½ÿ ï€+ G¢O@cõ1SÇ^Ê.Àê¤Ë€‘]}q(z¨Öµ%JËà³3®«Ñá8uæ$vS}V뻀Šñz999˜?>233Ñ¥KìØ±þþ›¦§¥¥ÁNãomHH6mÚ„·Þz o¿ý6Z·nÍ›7£_¿Š…Iår9N:…õë×#77¾¾¾2d6oÞ 777«|F"ª_.fÝÅÙLã 17oÜû¶P1±BsÇ_w'ë5CŸ±e`Tc/dãÈåýêã傟†ÚåšÒèîdBu[!ë#…½ÿßÞÇEUõÿ 3Ì0Ã.È*ˆ â‚J*š •â’¢e*®e=jj©=¥õhåRj›?{ÒÒÇG˵E+3ÅpWMÌ]q eG–aÎïòÆÈ޲ óy¿^¼š9ç{Ï=wNŒ_ν÷Ü>휯…[u}w‡¨ÖD“&M¤I“ʬۻwo©²!C†`È!eÆ«ÕjDFF>ÎîéyfñþJcÊ:»ú¥'pøò]—ñø7E‰ø O—¸A¤äõ©ÙÅ«¼ØÍkb®"VÆw‚*ìç©÷zÕS1¾Ý©¾»@Të &$"jl"þšý+)ÔÇ¡>eÆ+KÌúUx ø¯À‡ûkAè9Ûádbâo_8ô«Ø ûiLÉ‘±`HDô˜Å%–ÿäÖ¿Ò]=íªÕnÉ5ê*ZƬœº¹ÛI¯•l_ÒWœ #j”˜ÕÀÑ«÷y6oööZ)Çé›é¸x'V*ié—òlÒšÙT{Ÿ%“¾ —)gpLPóÛWžŽöDï¶å/ŸED†‹ Q <8mj®Rà^­0ð?‡Êµ3WânNô¾&É”<[ñ20¥ëšZªôîÖ-opX NßÊÀœmÑ¥š3”Dd8˜=‚+f#»‚'m@V%õ5¡¨p˜Ò3€M-ô±/kðõ§½ñF¯VÞ9"jð˜=‚m§“ÐÎźÂ'+3$ÞË,¨ñ¾J®KWÑ)à²fgôñÑ{_Ö `znA©2"jœ˜UÓƒ'l<ðÑÎ?*ŒÿgïVönŠÜÂ"¸Ú<žµåLMª7èl]ù~;ºÛVCD@"¢jʬÆs}•r„û»gzUÑÒ,eÍî=ü¨¹¬¼Ò§¥ú¹Ü~®ÚÛ~çiœ¾™ŽñkOHe&õÿd7"¢Ç† 5J+WÀÕF 7ôïàŒIëãäi'k3Ä\ÖêÅqf"j<˜Q£STÅ¥Z¶¿Þmœ-!“Oï]]ØOzÝ»Z:\Æ¥”l€‰ŒS€DÔxð@"2xBüðÅ^¾‹G]¨Òvm]¬¤„€Þk •Qo„Hïåü¶$¢F„_iDdÐròµùd/¦m:‰ŒÜBD¬<Œ¥¿•¿äËg/øA¥0Á[a>ÕÚRÁ¯K"jþ½ÞÒk'+3¬y©3z·sª‹®5œ$¢A÷~>‹„¤ò“?P™òïV"¢GÅoR"j–í½Œµ‡¯W§Rðk‹ˆèQñ›”ˆêMV‡C—R‘WX„O"Ï—çh¥X¨ÉdRyÉ×DDTu<LDõæóß.âóß.U³rL ¼,ðÉ®óx5ÄK¯ŽéQÍ0$2 )Yy8Ÿœ…'[Úüì×É™•&Û_ï¶.V€/Ft,UoàQ½aHd@ƒ[é÷ܪ)›ÛB[¤ÃNnp±1ƒÜDä& ?+JÉÊCŸ%Ê­w°TáÈ¿ž®4Éåõ€DD5èÒé¯Õ!òl2žn툘˩¸•^¼6Þþ bÿ…?KKÌ¢9[›!rz0R2ó`f*GVžÎÖf8—”'k3Ø[(ñ»/ÂÅÆ ùZ‚¼ìš•R?7kXš™Öúqý|ê¦n:Uf‡ZÀFt¬Ò §™©üqwˆÈ(0$j`’3òòI4òµºjo›”‘‡sv•[o®”#§ ¨Ìº O;¼Ý·5ì-UpµQW{ß•),Ò!ó~a¹É_çMðÝ„ jµÉˆ¨f˜5‹w_À†#×q¿ ¨FÉ_U”—ü@ì•»ÿâàü} R<¾äJïY;Ê­·P)°îå.Õn׌kÕ@¢:–¯Å•?³1ð?‡`£1Å?zxâßQQPT³¤ïã!p!9 ÿ=xõ±õÑgöN<àŠÅÃüK{+—[wfNïjŸzŽèì†Go`J¨÷£vˆÈ(É„¢¾;a¨233ammŒŒ XYYÕww¨Ywø:V¸‚g;8cp@3´t°À½œtœ¿»Æm¶s±ÂÛ}[ÃV£Ä'oaB°'¬ÌôÚ½ðA_(Ldèóïý¸p'û‘ŽafŸÖxµ§WååÈÉ×âùå1ø#9«Ìú'[ÚcÝ+ÕŸùB § *þ KDÕÇ¿™>þD+Ò Œÿæ8öü‘RªÎÎ\‰»9UnkÁàöpµU#¤USdçka®”WxcDôù˜ÈdiÕ@ñ5wBû.ü‰È³Éhël…yÛ“zz!;_‹£Wï•›œ=°÷Ížð°7¯r¿Kó¿£ÒÍ*ey¦þ;ö‰µMDTSü÷›§€‰‹¯ö]ÆÂTS•äïÄìg`¥6Ezn!šZª¤òªÌt…ú8è½7•_׫­#zµu„Ox4AK ¨•_ß—[ EZn!&2ì¿ð'Þúá´^;¿Ä߯kOWÿTë” q¥’?)vN F×…{Æ’5DD@¢j8|å.füp-,àfƒÏv_x¤öŽþëi kµ)Ô¦Ïð•Lþ™L†öͬK•k” h”Å_/ºap€+¾‰½.Íþq§â²!°ítR©ò·û´†…Rz¯ãù"¢zÁ¨ Št ·31|EñÍ ‰÷rñ[§yövßÖXTÆÌàœmñTkG8X™=ö¾>*…Üãžl`þ¶ü™™_­íóµEøô¡çúºX›a|°'†=á¦w› 9Õ&€dÐNßL‡›­¶æÊʃk ã~!®ßÍÁÀÿªR|÷–v8té.ÞìÝ šÙ ¸US¸ÙºxÚ¡@«ƒ©\fqk÷×#ØRsòQ¤ØŒî-í+½c×göNéµ¥J3sÃÊ51€Ïˆ¨1bHkÑDüëÇ3€%Ãü1(ÀÚ"d2ä&2DŸOÁ—{/ãÃÁíÑÒÁ¢Üvn¥ßGT˜«P)Lðé®óÐ(ö¶ÇWû¯TÚS¹ …EßMBçMJÕwñ´“^+ hÆËFSœèeäbéž‹ø÷ž‹•. SøÐR6Ÿ¼Ð¡Â}tt·yôŽQµñ.àGÀ»ˆê^ÂíLô[Zþ3dËÓÞÕ‰÷r1³OkDtvC\bfýø{¥wÀ–e€Ÿ ÞîÛË¢/a|°'šÛÕìÙ†.9#]îÜD†¢ë][Ô¿ÜmÒs à?ïïenŽüëi8–qšûâ,쿘ŠÑ]›TRLDÿýæ €"À†#×±úÐ5\IÍ©QgneþõãiÖ°º./è§w×ꇃÛרCñ`°è¡;5ò ‹J=‚-%+E:¡ûëëO–™ü€·£%¼-s‰ˆ¨ª˜R¢8IPÈÿžõ¹›Ow]À3mðýñ›Øy6þn6X6²#\lÔ·æöU°–\mêêÙïômƒ–F·d‰™©&²Òwê¶yo'¾Õ ½Û9þuÿU|7ôs®R\;—ÒwQÃÀj•¶H¹IñM[ânâïâ-,p)Eÿ)&J¯OÝHG·E¿aÃ+]ÊMþ^íé…·zûàJjæl=‹þ.˜ñÐvö*¤fW|kÜ»½`«1ÅÍ´û°·Pé­‘gìÊZ¦E`üÚp±6Ãooö”’?ØròÞÝKDÔÐ1lÀ´E:$eäÁÙÚLoƬ¡»~7‹w_@ÂíL\LɆ¿› &2¿ž&Å<œü•gÄè½àЇt9Š“Éèç‚3·2pæfžïÔ ÖjSx¼ý+@£”#· ðfïVØv: /?ÙMþºƒØ­‰¦æm„ngäé%%mßµŽ{CDDÕÁ›@Am]DZ ÕaꦓØñ{2 °¹-–FÀÅFýHí !pêF:Ú8[é]Ãu/§¥\*Ë+,‚ÂD&%×Rsp.)}|‹Oùüõˆ±ob¯Á¯™ &oˆCjv¼,p±Š‰]uµ°7Çž7B`RƒÓ°kc¯á³Ý°cj8[?ÚghlvœI«ëã–*x65Çá+÷*ÜFm*G¼0ƒXꆈŒoá `ƒôÉ›Ròǯ§¡Û¢ßp|ö3°·øû ñ7ÒþÅ!x65Ç÷‚`÷WÝÝì|l:v瓳0ío˜«¸œ’‚"^\} 0®{ \LÉÂ3mñþÖ³R›aíyöàßÃý1uÓ©*÷»ºÉßçhïj?³óØÜq‰é8zõÆ{"=·>ˆ’b¿×¹FÉŒòÀè mkì:yØJ¯ÿÑÃÿö„ƒ–Å þFºT7ÀÏ¿Äßx;Z0ù#"jà jpÙ²eøä“O””„víÚaÉ’%èÑ£G¹ñ›7oƻヒ˗/ÃËË ~ø!,Õ !0wî\¬X±iiièÒ¥ ¾øâ ´k×®Jý©­¿ fÿtë'Vh€‚[5…½…㺷€¯kÅ7 <8u WöcRQOÆsWSsðËkOJ³Ä¹Z´}/RŠùï˜@¼òÍqÀNÍðé ~õÒW"¢ªà  Í~ûí·˜6m–-[†îݻ㫯¾Bß¾}‘ww÷Rñ±±±6læÏŸÁƒãÇÄСCqðàAtéR|½ØÇŒÅ‹cÍš5hÕª>øàôêÕ çÏŸ‡¥eý-Q¡RæMMÌ•håh !^pµQ#5;îM4H¼›‹Íq·0³O}Æä¯þ¬!„Þh” ½õÜmÖÎQçRîïR_]%"¢*2˜À.]º cÇŽX¾|¹TÖ¦M 4 .,?lØ0dffbÇŽRYŸ>}`kk‹7BL›6 3gÎäççÃÑÑ}ô&L˜PiŸjë/ˆ…ÛÏUé uÅVcŠ´ÜBÅ×wM}Æ»î๎®èßÞ6šÚy Û’¨ XuSB[âÍ0ŸZÙÕ\À¼]Òÿ×õ‡¶H‡œ‚"X«+~TQ}ã  Ìàĉxûí·õÊ{÷˜2·‰ÅôéÓõʰdÉÀÕ«W‘œœŒÞ½{Kõ*• !!!ˆ‰‰©RX[jz­[u…µsÄM0²Ksœ½¥ÂÉyÐ(µê:4³Æ÷ƒÊœ‘œâUëý›ú´7žli_é©bªs¶ÃÔM§09´øÿ…ÜÖjù[ˆÈ˜D˜ššŠ¢¢"8::ê•;::"99¹Ìm’““+Œðß²b®_¿^f›ùùùÈÏÿ{M¹ÌÌÌêHiJÜ¡;¢‹;6IDÔ!xfñ>@g+Œèìò²Ç” qøô?ääk±pÇò²Ã›½} 7‘A”¬|äêüI4€â ö§?ã ;s)Ù ô(~†m‡fÅû­èq_uE&“Iý¢†'Üß=}8ãGDd€ "|àáëÀ¾.©&ñÕisáÂ…˜;wnuº\#…]W5¢‹; n=rì份`"“ÁJ­ÐëãÎiÁÒëŸ&w×kK&“Iã²·P"5»£º¸Ã³©Em&DD†É @{{{ÈåòR³})))¥fðprrª0ÞÉ©xM»ääd8;;W©ÍwÞyo¼ñ†ô>33nnnÕ? Jôôi +3Ú8ë_—`kþh×Úm½ïårVˆˆÈÈÄ;J¥:uÂîÝ»õÊwïÞnݺ•¹MPPP©ø]»vIñ-Z´€“““^LAAöíÛWn›*• VVVz?µ¡£»-^éá‰î-ík»VfLþˆˆˆÈ0fà7ÞÀèÑ£ˆ   ¬X±‰‰‰˜8q"`̘1puu•îž:u*‚ƒƒñÑG!<<?ÿü3¢¢¢pðàAŧF§M›† ÀÛÛÞÞÞX°`4 FŒQoÇIDDDTÛ &6lîÞ½‹yóæ!)) ¾¾¾Ø¾};š7oHLL„‰ÉߚݺuæM›0{öl¼ûî»ðòò·ß~+­3fÌÀýû÷1iÒ$i!è]»vÕë€DDDDµÍ`Ölˆ¸Ž‘áá¿ßr =>L‰ˆˆˆŒ @""""#ȈˆÈÈ0$"""22L‰ˆˆˆŒ @""""#ȈˆÈÈ0$"""22L‰ˆˆˆŒŒÁ< ¸!zð½ÌÌÌzî UÕƒ·ùi¸LAVVÀÍÍ­ž{BDDDÕ•••kkkð.?IDATëúîF½ cN‘N§ÃíÛ·aii ™LVi|ff&ÜÜÜpãÆ £}ø´!áxŽ™áᘖÆ2^BdeeÁÅÅ&&Æy5g‰‰ š5kVíí¬¬¬ úÇØp¼ ÇÌðpÌ Kc/cù{À8Ó^""""#ƈˆˆÈÈÈçÌ™3§¾;aLär9zöì …‚gß ÇËðpÌ Ç̰p¼ÞBDDDddx ˜ˆˆˆÈÈ0$"""22L‰ˆˆˆŒ @""""#ðšnݺ…Q£FÁÎÎþþþ8qâ„T/„Àœ9sàââµZž={âìÙ³zm¤¥¥aôèѰ¶¶†µµ5Fôôt½˜3gÎ $$jµ®®®˜7ožQ?³°¦<<< “ÉJýLž<ŸŸ×^{ ööö077ÇÀqóæM½61`À˜››ÃÞÞ¯¿þ: ôböíÛ‡N:ÁÌÌ žžžøòË/ëì­V‹Ù³g£E‹P«Õðôôļyó Óé¤þŽ5û¬TŸ‘‘!ÅðáÃÅ™3gÄæÍ›…¥¥¥øôÓOëôxƒ””‘””$ýìÞ½[ÑÑÑB!&Nœ(\]]ÅîÝ»E\\œ ~~~B«Õ !„ÐjµÂ××W„††Š¸¸8±{÷náââ"¦L™"íãÊ•+B£Ñˆ©S§Š„„±råJajj*~øá‡ú8dƒöÁ;;;±mÛ6qõêUñý÷ß ±dÉ)†¿c ÏСCEÛ¶mž}ûÄÅ‹Åûï¿/¬¬¬ÄÍ›7…³ú¶}ûv1kÖ,±yóf@üøãzõu5>111B.—‹ ˆsçΉ …B!>\û•°fΜ)ž|òÉrëu:prr‹-’Êòòò„µµµøòË/…B$$$zÿÃÇÆÆ â?þB±lÙ2amm-òòò¤˜…  ¡Óé÷a•©S§ ///¡ÓéDzzº055›6m’êoݺ%LLLÄÎ;…Å_œ&&&âÖ­[RÌÆ…J¥B!f̘!Z·n­·Ÿ &ˆ®]»ÖÁ5.ýû÷ãÆÓ+{î¹çĨQ£„ükˆrss…\.Û¶mÓ+÷óó³fÍâ˜50'€u9>C‡}úôÑëOXX˜>|øã?PªOWÃÖ­[ˆ^xÀÊ•+¥ú«W¯"99½{÷–ÊT*BBBˆ…µµ5ºté"ÅtíÚÖÖÖz1!!!P©TRLXXnß¾k×®ÕòQ6^X·nÆ™L†'N °°Po¼\\\àëë«7¾¾¾pqq‘bŸŸ/úÕkãAÌñãÇQXXXGÖx<ù䓨³g.\¸ˆÇÁƒѯ_?ükˆ´Z-ŠŠŠ`ff¦W®V«qðàAŽYW—ãSÞwåƒ6¨n1¬†+W®`ùòåðööFdd$&Nœˆ×_ß|ó 99àè訷£££T—œœ ‡Rm;88èÅ”ÕFÉ}PõýôÓOHOOÇ‹/¾ ø³T*•°µµÕ‹{x¼ [[[(•ÊJÇK«Õ"55µ–ަqš9s&"""кuk˜šš" Ó¦MCDDþŽ5D––– Âüùóqûömaݺu8rä’’’8f \]ŽOy1¿úÁç¸TƒN§C`` ,XÀÙ³g±|ùrŒ3FŠ“ÉdzÛ !ôÊ®¯JŒøëBÚ²¶¥ªYµjúöí«7›WŽWýùöÛo±nÝ:lذíڵéS§0mÚ4¸¸¸`ìØ±RÇ–µk×bܸqpuu…\.GÇŽ1bÄÄÅÅI1³†­®Æ§²ýPÝá `58;;£mÛ¶zemÚ´Abb"ÀÉÉ @é¿FSRR¤¿zœœœpçÎRmÿùçŸz1eµ”þ+ªæúõ눊ŠÂ+¯¼"•999¡  iiiz±×Ãc‘––†ÂÂÂJÇK¡PÀÎή6§Ñzë­·ðöÛocøðáhß¾=FéÓ§cáÂ…ø;ÖPyyyaß¾}ÈÎÎÆ7pôèQ¢E‹³®.ǧ¼Ž_ý`X Ý»wÇùóçõÊ.\¸€æÍ›€ôe·{÷n©¾  ûöíC·nÝAAAÈÈÈÀÑ£G¥˜#GŽ ##C/fÿþýzKìÚµ ...ððð¨­ÃkÔV¯^ ôïß_*ëÔ©LMMõÆ+)) ¿ÿþ»ÞXüþûïHJJ’bvíÚ•J…N:I1%ÛxSSÓÚ<¬F'77&&ú_Kr¹\Z†¿c ›¹¹9œ‘––†ÈÈH„‡‡s̸ºŸò¾+´Au¬îï;1\G …B|øá‡ââÅ‹býúõB£ÑˆuëÖI1‹-ÖÖÖbË–-âÌ™3"""¢ÌÛé;tè bccEll¬hß¾½ÞíôéééÂÑÑQDDDˆ3gΈ-[¶+++.wPCEEEÂÝÝ]Ìœ9³TÝĉE³fÍDTT”ˆ‹‹O=õT™ËÀ<ýôÓ"..NDEE‰fÍš•¹ ÌôéÓEBB‚Xµj—©¡±cÇ WWWi˜-[¶{{{1cÆ )†¿c ÏÎ;ÅŽ;Ä•+WÄ®]»„ŸŸŸèܹ³(((BpÌê[VV–8yò¤8yò¤ /^,Nž<)®_¿.„¨»ñ9tèËåbÑ¢EâܹsbÑ¢E\¦1¬¦_~ùEøúú •J%Z·n-V¬X¡W¯ÓéÄûï¿/œœœ„J¥ÁÁÁâÌ™3z1wïÞ#GŽ–––ÂÒÒRŒ9R¤¥¥éÅœ>}ZôèÑC¨T*áää$æÌ™Ã¥j(22RçÏŸ/Uwÿþ}1eÊѤI¡V«Å³Ï>+õb®_¿.ú÷ï/ÔjµhÒ¤‰˜2eŠÞRB±wï^ ”J¥ðððË—/¯Õcj¬233ÅÔ©S…»»»033žžžbÖ¬Y"??_ŠáïXÃóí·ß OOO¡T*…“““˜::={öDHHˆT^PP€ØØX½ÐÄÄK—.Åï¿ÿޝ¿þ¿ýöf̘°¶¶Fÿþý±~ýz½}mذááá°°°@nn.BCCaaaýû÷ãàÁƒ°°°@Ÿ>}Ê•š={6V¯^åË—ãìÙ³˜>}:F…}ûöéÅÍš5 Ÿ}öŽ?…BqãÆ† †þóŸh×®’’’””„aÆIÛÍ;C‡Åéӧѯ_?Œ9÷îÝ+÷³Û¿?K•geeáûï¿Ç¨Q£Ð«W/äääè%Ù|öÙg ÄÉ“'1iÒ$¼úê«øã?¤6 €öíÛ#..óçÏÇÌ™3õ¶÷Ýw‘€;vàܹsX¾|9ìííGDEE!))Iï:¼={öàܹsؽ{7¶mÛ 81<~ü8¶nÝŠØØX!Я_?JÛåææbéҥشivî܉½{÷â¹çžÃöíÛ±}ûv¬]»+V¬ÐKÊ sçÎ8pà@¹Ÿ#50‚ˆ½+VsssQXX(233…B¡wîÜ›6mݺuB±oß>@\¾|¹Üv¾ûî;agg'½ß²e‹°°°999B!222„™™™øõ×_…B¬ZµJøøøN'm“ŸŸ/ÔjµˆŒŒB1vìX.„";;[˜™™‰˜˜½ý¾üòË"""B!Dtt´ ¢¢¢¤ú_ýU÷ïßBñþûï ??¿Rý fÏž-½ÏÎÎ2™LìØ±£ÜcöóóóæÍ+U¾bÅ áïï/½Ÿ:uª9r¤^LóæÍŨQ£¤÷:N888ˆåË— !„X¾|¹°³³“ú-„+W®ÄÉ“'…B 0@¼ôÒKeöíêÕ«z±Œ;V8::Šüü|©ìÂ… €8tèT–šš*Ôjµøî»ï„B¬^½Z—.]’b&L˜ 4ÈÊÊ’ÊÂÂÂÄ„ ôöùóÏ? QTTTf_‰¨aQÔ_êIDu%449998vìÒÒÒЪU+888 $$£G–f¯ÜÝÝáéé)m  !!™™™ÐjµÈËËCNNÌÍÍÑ¿( lݺÇÇæÍ›aii‰Þ½{Nœ8K—.•ºy"//—/_.ÕÏ„„äåå¡W¯^zåÐ+ëСƒôÚÙÙ’’ww÷ ?‹’Û™››ÃÒÒ)))åÆß¿fff¥ÊW­Z…Q£FIïG…àà`¤§§ÃÆÆ¦ÌýÉd2899Iû;þ<:tè ×~çÎõöóꫯâùçŸG\\z÷îAƒ¡[·n#´oßJ¥RzîÜ9( téÒE*³³³ƒÎ;'•i4xyyIïááá ½²‡?3µZ N‡üü|¨ÕêJûGDõ‹ ‘hÙ²%š5k†èèh¤¥¥!$$àää„-ZàСCˆŽŽÆSO=%msýúuôë×'NÄüùóѤIûì3tíÚ­ZµÂíÛ·Kµ;räHìܹgÏžEtt4FŽ)ÕuìØ/^„ƒƒZ¶l©÷cmm]ª­¶mÛB¥R!11±T¼››[•U©Têݤð( W¶jÕ*#>>§N’~f̘U«VU¹íÖ­[ãôéÓÈÏÏ—ÊJÞ­ý@Ó¦Mñâ‹/bݺuX²d‰t#Ƀ¾ªkÛ¶m¡ÕjqäÈ©ìîÝ»¸páÚ´iSå>—ç÷ßGÇŽ¹"ªL‰ŒDhh(<ˆS§NI3€@q¸råJäååé%€^^^ÐjµøüóÏqåʬ]»_~ùe©vCBBàè舑#GÂÃÃ]»v•êFŽ {{{„‡‡ãÀ¸zõ*öíÛ‡©S§âæÍ›¥Ú²´´Ä›o¾‰éÓ§ã믿ÆåË—qòäI|ñÅøú믫|¬¸zõ*N:…ÔÔT½«ºÂÂÂ+%Y………X»v-"""àëë«÷óÊ+¯àĉˆ¯RÛ#FŒ€N§ÃøñãqîÜ9DFFJ3{fåÞ{ï=üüóϸtéΞ=‹mÛ¶I ›ƒƒƒt'ö;w‘‘Qî¾¼½½Žüã8xð âãã1jÔ(¸ºº"<<¼ÆŸÏNýQÃÇÈH„††âþýûhÙ²%¥òdeeÁËËKo–Íßß‹/ÆG}___¬_¿ .,Õ®L&CDDâããõfÿ€âëÉöïßwww<÷ÜshÓ¦ Ƈû÷ïÃÊʪÌ~Ο?ï½÷.\ˆ6mÚ ,, ¿üò‹´äIU<ÿüóèÓ§BCCÑ´iÓGZž¤_¿~055ETT`ëÖ­¸{÷.\*ÖÛÛíÛ·¯ò, ••~ùåœ:u þþþ˜5kÞ{ï=® T*•xçwСCC.—cÓ¦M…B¥K—⫯¾‚‹‹K¥‰ÜêÕ«Ñ©S'<ûì³ ‚Û·o/uŠ·ºnݺ…˜˜˜2×J$¢†I&Ê»0„ˆˆË–-ÃÏ?ÿ\' ¯_¿/½ô’´Î¢!xë­·‘‘QjC"j¸xQ%Æ´´4dee•ù8¸GñÍ7ßÀÓÓ®®®ˆÇÌ™31tèPƒIþ€âSÑo¾ùf}wƒˆª3€DDõèã?ƲeËœœ ggg 4~ø!4M}wˆ1&€DDDDF†7&€DDDDF† ‘‘aHDDDdd˜&€DDDDF† ‘‘aHDDDdd˜™ÿ¼«e;ý×IEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132249.0 specutils-0.7/docs/index.rst0000644000076500000240000001070600000000000016246 0ustar00erikstaff00000000000000 .. the "raw" directive below is used to hide the title in favor of just the logo being visible .. raw:: html *********************** Specutils Documentation *********************** .. _specutils: .. image:: img/logo.png ``specutils`` is a Python package for representing, loading, manipulating, and analyzing astronomical spectroscopic data. The generic data containers and accompanying modules provide a toolbox that the astronomical community can use to build more domain-specific packages. For more details about the underlying principles, see `APE13 `_, the guiding document for spectroscopic development in the Astropy Project. .. note:: While specutils is available for general use, the API is in an early enough development stage that some interfaces may change if user feedback and experience warrants it. Getting started with :ref:`specutils ` ================================================= As a basic example, consider an emission line galaxy spectrum from the `SDSS `_. We will use this as a proxy for a spectrum you may have downloaded from some archive, or reduced from your own observations. .. plot:: :include-source: :align: center :context: close-figs We begin with some basic imports: >>> from astropy.io import fits >>> from astropy import units as u >>> import numpy as np >>> from matplotlib import pyplot as plt >>> from astropy.visualization import quantity_support >>> quantity_support() # for getting units on the axes below # doctest: +IGNORE_OUTPUT Now we load the dataset from it's canonical source: >>> f = fits.open('https://dr14.sdss.org/optical/spectrum/view/data/format=fits/spec=lite?plateid=1323&mjd=52797&fiberid=12') # doctest: +IGNORE_OUTPUT +REMOTE_DATA >>> # The spectrum is in the second HDU of this file. >>> specdata = f[1].data # doctest: +REMOTE_DATA >>> f.close() # doctest: +REMOTE_DATA Then we re-format this dataset into astropy quantities, and create a `~specutils.Spectrum1D` object: >>> from specutils import Spectrum1D >>> lamb = 10**specdata['loglam'] * u.AA # doctest: +REMOTE_DATA >>> flux = specdata['flux'] * 10**-17 * u.Unit('erg cm-2 s-1 AA-1') # doctest: +REMOTE_DATA >>> spec = Spectrum1D(spectral_axis=lamb, flux=flux) # doctest: +REMOTE_DATA And we plot it: >>> f, ax = plt.subplots() # doctest: +IGNORE_OUTPUT >>> ax.step(spec.spectral_axis, spec.flux) # doctest: +IGNORE_OUTPUT +REMOTE_DATA Now maybe you want the equivalent width of a spectral line. That requires normalizing by a continuum estimate: .. testsetup:: >>> fig = plt.figure() # necessary because otherwise the doctests fail due to quantity_support and the flux units being different from the last figure .. plot:: :include-source: :align: center :context: close-figs >>> from specutils.fitting import fit_generic_continuum >>> cont_norm_spec = spec / fit_generic_continuum(spec)(spec.spectral_axis) # doctest: +REMOTE_DATA >>> f, ax = plt.subplots() # doctest: +IGNORE_OUTPUT >>> ax.step(cont_norm_spec.wavelength, cont_norm_spec.flux) # doctest: +IGNORE_OUTPUT +REMOTE_DATA >>> ax.set_xlim(654*u.nm, 660*u.nm) # doctest: +IGNORE_OUTPUT +REMOTE_DATA But then you can apply a single function over the region of the spectrum containing the line: .. code-block:: python >>> from specutils import SpectralRegion >>> from specutils.analysis import equivalent_width >>> equivalent_width(cont_norm_spec, regions=SpectralRegion(6562*u.AA, 6575*u.AA)) # doctest: +REMOTE_DATA While there are other tools and spectral representations detailed more below, this gives a test of the sort of analysis :ref:`specutils ` enables. Using :ref:`specutils ` ================================== For more details on usage of specutils, see the sections listed below. .. toctree:: :maxdepth: 2 installation types_of_spectra spectrum1d spectrum_collection spectral_regions analysis fitting manipulation arithmetic custom_loading Get Involved - Developer Docs ----------------------------- Please see :doc:`contributing` for information on bug reporting and contributing to the specutils project. .. toctree:: :maxdepth: 2 contributing releasing ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/installation.rst0000644000076500000240000000245500000000000017642 0ustar00erikstaff00000000000000.. highlight:: shell ============ Installation ============ Stable release -------------- If you use anaconda_ to manage your Python environment, run this command in your terminal: .. code-block:: console $ conda install -c conda-forge specutils Otherwise, the recommended method is using pip_: .. code-block:: console $ pip install specutils If you don't have pip_ installed, this `Python installation guide`_ can guide you through the process. These are the preferred methods to install Specutils, as they will always install the most recent stable release. .. _pip: https://pip.pypa.io .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ .. _anaconda: https://www.anaconda.com/ From sources ------------ The sources for Specutils can be downloaded from the `Github repo`_. You can either clone the public repository: .. code-block:: console $ git clone git://github.com/astropy/specutils Or download the `tarball`_: .. code-block:: console $ curl -OL https://github.com/astropy/specutils/tarball/master Once you have a copy of the source, you can install it with: .. code-block:: console $ python setup.py install .. _Github repo: https://github.com/astropy/specutils .. _tarball: https://github.com/astropy/specutils/tarball/master ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/docs/make.bat0000644000076500000240000001064100000000000016010 0ustar00erikstaff00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Astropy.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Astropy.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/docs/manipulation.rst0000644000076500000240000003335300000000000017642 0ustar00erikstaff00000000000000==================== Manipulating Spectra ==================== While there are myriad ways you might want to alter a spectrum, :ref:`specutils ` provides some specific functionality that is commonly used in astronomy. These tools are detailed here, but it is important to bear in mind that this is *not* intended to be exhaustive - the point of :ref:`specutils ` is to provide a framework you can use to do your data analysis. Hence the functionality described here is best thought of as pieces you might string together with your own functionality to build a tailor-made spectral analysis environment. In general, however, :ref:`specutils ` is designed around the idea that spectral manipulations generally yield *new* spectrum objects, rather than in-place operations. This is not a true restriction, but is a guideline that is recommended primarily to keep you from accidentally modifying a spectrum you didn't mean to change. Smoothing --------- Specutils provides smoothing for spectra in two forms: 1) convolution based using smoothing `astropy.convolution` and 2) median filtering using the :func:`scipy.signal.medfilt`. Each of these act on the flux of the :class:`~specutils.Spectrum1D` object. .. note:: Specutils smoothing kernel widths and standard deviations are in units of pixels and not ``Quantity``. Convolution Based Smoothing ^^^^^^^^^^^^^^^^^^^^^^^^^^^ While any kernel supported by `astropy.convolution` will work (using the `~specutils.manipulation.convolution_smooth` function), several commonly-used kernels have convenience functions wrapping them to simplify the smoothing process into a simple one-line operation. Currently implemented are: :func:`~specutils.manipulation.box_smooth` (:class:`~astropy.convolution.Box1DKernel`), :func:`~specutils.manipulation.gaussian_smooth` (:class:`~astropy.convolution.Gaussian1DKernel`), and :func:`~specutils.manipulation.trapezoid_smooth` (:class:`~astropy.convolution.Trapezoid1DKernel`). .. code-block:: python >>> from specutils import Spectrum1D >>> import astropy.units as u >>> import numpy as np >>> from specutils.manipulation import (box_smooth, gaussian_smooth, trapezoid_smooth) >>> spec1 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49)*u.Jy) >>> spec1_bsmooth = box_smooth(spec1, width=3) >>> spec1_gsmooth = gaussian_smooth(spec1, stddev=3) >>> spec1_tsmooth = trapezoid_smooth(spec1, width=3) >>> gaussian_smooth(spec1, stddev=3) #doctest:+SKIP Spectrum1D([0.22830748, 0.2783204 , 0.32007408, 0.35270403, 0.37899655, 0.40347983, 0.42974259, 0.45873436, 0.48875214, 0.51675647, 0.54007149, 0.55764758, 0.57052796, 0.58157173, 0.59448669, 0.61237409, 0.63635755, 0.66494062, 0.69436655, 0.7199299 , 0.73754271, 0.74463192, 0.74067744, 0.72689092, 0.70569365, 0.6800534 , 0.65262146, 0.62504013, 0.59778884, 0.57072578, 0.54416776, 0.51984003, 0.50066938, 0.48944714, 0.48702192, 0.49126444, 0.49789092, 0.50276877, 0.50438924, 0.50458914, 0.50684731, 0.51321106, 0.52197328, 0.52782086, 0.52392599, 0.50453064, 0.46677128, 0.41125485, 0.34213489]) Each of the specific smoothing methods create the appropriate `astropy.convolution.convolve` kernel and then call a helper function :func:`~specutils.manipulation.convolution_smooth` that takes the spectrum and an astropy 1D kernel. So, one could also do: .. code-block:: python >>> from astropy.convolution import Box1DKernel >>> from specutils.manipulation import convolution_smooth >>> box1d_kernel = Box1DKernel(width=3) >>> spec1 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49) * u.Jy) >>> spec1_bsmooth2 = convolution_smooth(spec1, box1d_kernel) In this case, the ``spec1_bsmooth2`` result should be equivalent to the ``spec1_bsmooth`` in the section above (assuming the flux data of the input ``spec`` is the same). The uncertainties are propagated using a standard "propagation of errors" method, if the uncertainty is defined for the spectrum *and* it is one of StdDevUncertainty, VarianceUncertainty or InverseVariance. But note that this does *not* consider covariance between points. Median Smoothing ^^^^^^^^^^^^^^^^ The median based smoothing is implemented using `scipy.signal.medfilt` and has a similar call structure to the convolution-based smoothing methods. This method applys the median filter across the flux. .. note:: This method is not flux conserving and errors are not propagated. .. code-block:: python >>> from specutils.manipulation import median_smooth >>> spec1 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49) * u.Jy) >>> spec1_msmooth = median_smooth(spec1, width=3) Resampling ---------- :ref:`specutils ` contains several classes for resampling the flux in a :class:`~specutils.Spectrum1D` object. Currently supported methods of resampling are integrated flux conserving with :class:`~specutils.manipulation.FluxConservingResampler`, linear interpolation with :class:`~specutils.manipulation.LinearInterpolatedResampler`, and cubic spline with :class:`~specutils.manipulation.SplineInterpolatedResampler`. Each of these classes takes in a :class:`~specutils.Spectrum1D` and a user defined output dispersion grid, and returns a new :class:`~specutils.Spectrum1D` with the resampled flux. Currently the resampling classes expect the new dispersion grid unit to be the same as the input spectrum's dispersion grid unit. If the input :class:`~specutils.Spectrum1D` contains an uncertainty, :class:`~specutils.manipulation.FluxConservingResampler` will propogate the uncertainty to the final output :class:`~specutils.Spectrum1D`. However, the other two implemented resampling classes (:class:`~specutils.manipulation.LinearInterpolatedResampler` and :class:`~specutils.manipulation.SplineInterpolatedResampler`) will ignore any input uncertainty. Here's a set of simple examples showing each of the three types of resampling: .. plot:: :include-source: :align: center :context: close-figs First are the imports we will need as well as loading in the example data: >>> from astropy.io import fits >>> from astropy import units as u >>> import numpy as np >>> from matplotlib import pyplot as plt >>> from astropy.visualization import quantity_support >>> quantity_support() # for getting units on the axes below # doctest: +IGNORE_OUTPUT >>> f = fits.open('https://dr14.sdss.org/optical/spectrum/view/data/format=fits/spec=lite?plateid=1323&mjd=52797&fiberid=12') # doctest: +IGNORE_OUTPUT +REMOTE_DATA >>> # The spectrum is in the second HDU of this file. >>> specdata = f[1].data[1020:1250] # doctest: +REMOTE_DATA >>> f.close() # doctest: +REMOTE_DATA Then we re-format this dataset into astropy quantities, and create a `~specutils.Spectrum1D` object: >>> from specutils import Spectrum1D >>> lamb = 10**specdata['loglam'] * u.AA # doctest: +REMOTE_DATA >>> flux = specdata['flux'] * 10**-17 * u.Unit('erg cm-2 s-1 AA-1') # doctest: +REMOTE_DATA >>> input_spec = Spectrum1D(spectral_axis=lamb, flux=flux) # doctest: +REMOTE_DATA >>> f, ax = plt.subplots() # doctest: +IGNORE_OUTPUT >>> ax.step(input_spec.spectral_axis, input_spec.flux) # doctest: +IGNORE_OUTPUT +REMOTE_DATA .. plot:: :include-source: :align: center :context: close-figs Now we show examples and plots of the different resampling currently available. >>> from specutils.manipulation import FluxConservingResampler, LinearInterpolatedResampler, SplineInterpolatedResampler >>> new_disp_grid = np.arange(4800, 5200, 3) * u.AA Flux Conserving Resampler: >>> fluxcon = FluxConservingResampler() >>> new_spec_fluxcon = fluxcon(input_spec, new_disp_grid) # doctest: +IGNORE_OUTPUT +REMOTE_DATA >>> f, ax = plt.subplots() # doctest: +IGNORE_OUTPUT >>> ax.step(new_spec_fluxcon.spectral_axis, new_spec_fluxcon.flux) # doctest: +IGNORE_OUTPUT +REMOTE_DATA .. plot:: :include-source: :align: center :context: close-figs Linear Interpolation Resampler: >>> linear = LinearInterpolatedResampler() >>> new_spec_lin = linear(input_spec, new_disp_grid) # doctest: +REMOTE_DATA >>> f, ax = plt.subplots() # doctest: +IGNORE_OUTPUT >>> ax.step(new_spec_lin.spectral_axis, new_spec_lin.flux) # doctest: +IGNORE_OUTPUT +REMOTE_DATA .. plot:: :include-source: :align: center :context: close-figs Spline Resampler: >>> spline = SplineInterpolatedResampler() >>> new_spec_sp = spline(input_spec, new_disp_grid) # doctest: +REMOTE_DATA >>> f, ax = plt.subplots() # doctest: +IGNORE_OUTPUT >>> ax.step(new_spec_sp.spectral_axis, new_spec_sp.flux) # doctest: +IGNORE_OUTPUT +REMOTE_DATA Splicing/Combining Multiple Spectra ----------------------------------- The resampling functionality detailed above is also the default way :ref:`specutils ` supports splicing multiple spectra together into a single spectrum. This can be achieved as follows: .. plot:: :include-source: :align: center :context: close-figs >>> spec1 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.micron, flux=np.random.randn(49)*u.Jy) >>> spec2 = Spectrum1D(spectral_axis=np.arange(51, 100) * u.micron, flux=(np.random.randn(49)+1)*u.Jy) >>> new_spectral_axis = np.concatenate([spec1.spectral_axis.value, spec2.spectral_axis.to_value(spec1.spectral_axis.unit)]) * spec1.spectral_axis.unit >>> resampler = LinearInterpolatedResampler(extrapolation_treatment='zero_fill') >>> new_spec1 = resampler(spec1, new_spectral_axis) >>> new_spec2 = resampler(spec2, new_spectral_axis) >>> final_spec = new_spec1 + new_spec2 Yielding a spliced spectrum (the solid line below) composed of the splice of two other spectra (dashed lines):: >>> f, ax = plt.subplots() # doctest: +IGNORE_OUTPUT >>> ax.step(final_spec.spectral_axis, final_spec.flux, where='mid', c='k', lw=2) # doctest: +IGNORE_OUTPUT >>> ax.step(spec1.spectral_axis, spec1.flux, ls='--', where='mid', lw=1) # doctest: +IGNORE_OUTPUT >>> ax.step(spec2.spectral_axis, spec2.flux, ls='--', where='mid', lw=1) # doctest: +IGNORE_OUTPUT Uncertainty Estimation ---------------------- Some of the machinery in :ref:`specutils ` (e.g. `~specutils.analysis.snr`) requires an uncertainty to be present. While some data reduction pipelines generate this as part of the reduction process, sometimes it's necessary to estimate the uncertainty in a spectrum using the spectral data itself. Currently :ref:`specutils ` provides the straightforward `~specutils.manipulation.noise_region_uncertainty` function. First we build a spectrum like that used in :doc:`analysis`, but without a known uncertainty: .. code-block:: python >>> from astropy.modeling import models >>> np.random.seed(42) >>> spectral_axis = np.linspace(0., 10., 200) * u.GHz >>> spectral_model = models.Gaussian1D(amplitude=3*u.Jy, mean=5*u.GHz, stddev=0.8*u.GHz) >>> flux = spectral_model(spectral_axis) >>> flux += np.random.normal(0., 0.2, spectral_axis.shape) * u.Jy >>> noisy_gaussian = Spectrum1D(spectral_axis=spectral_axis, flux=flux) Now we estimate the uncertainty from the region that does *not* contain the line: .. code-block:: python >>> from specutils import SpectralRegion >>> from specutils.manipulation import noise_region_uncertainty >>> noise_region = SpectralRegion([(0, 3), (7, 10)]*u.GHz) >>> spec_w_unc = noise_region_uncertainty(noisy_gaussian, noise_region) >>> spec_w_unc.uncertainty # doctest: +ELLIPSIS StdDevUncertainty([0.18461457, ..., 0.18461457]) Or similarly, expressed in pixels: .. code-block:: python >>> noise_region = SpectralRegion([(0, 25), (175, 200)]*u.pix) >>> spec_w_unc = noise_region_uncertainty(noisy_gaussian, noise_region) >>> spec_w_unc.uncertainty # doctest: +ELLIPSIS StdDevUncertainty([0.18714535, ..., 0.18714535]) S/N Threshold Mask ------------------ It is useful to be able to find all the spaxels in an ND spectrum in which the signal to noise ratio is greater than some threshold. This method implements this functionality so that a `~specutils.Spectrum1D` object, `~specutils.SpectrumCollection` or an :class:`~astropy.nddata.NDData` derived object may be passed in as the first parameter. The second parameter is a floating point threshold. For example, first a spectrum with flux and uncertainty is created, and then call the ``snr_threshold`` method: .. code-block:: python >>> import numpy as np >>> from astropy.nddata import StdDevUncertainty >>> import astropy.units as u >>> from specutils import Spectrum1D >>> from specutils.manipulation import snr_threshold >>> np.random.seed(42) >>> wavelengths = np.arange(0, 10)*u.um >>> flux = 100*np.abs(np.random.randn(10))*u.Jy >>> uncertainty = StdDevUncertainty(np.abs(np.random.randn(10))*u.Jy) >>> spectrum = Spectrum1D(spectral_axis=wavelengths, flux=flux, uncertainty=uncertainty) >>> spectrum_masked = snr_threshold(spectrum, 50) #doctest:+SKIP >>> # To create a masked flux array >>> flux_masked = spectrum_masked.flux #doctest:+SKIP >>> flux_masked[spectrum_masked.mask] = np.nan #doctest:+SKIP The output ``spectrum_masked`` is a shallow copy of the input ``spectrum`` with the ``mask`` attribute set to False where the S/N is greater than 50 and True elsewhere. It is this way to be consistent with ``astropy.nddata``. .. note:: The mask attribute is the only attribute modified by ``snr_threshold()``. To retrieve the masked flux data use ``spectrum.masked.flux_masked``. Reference/API ------------- .. automodapi:: specutils.manipulation :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/nitpick-exceptions0000644000076500000240000000037200000000000020146 0ustar00erikstaff00000000000000# Temporary exception of inherited astropy classes py:class astropy.nddata.mixins.ndio.NDIOMixin py:class astropy.nddata.mixins.ndslicing.NDSlicingMixin py:class astropy.nddata.mixins.ndarithmetic.NDArithmeticMixin py:obj NDData py:obj NDUncertainty ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/releasing.rst0000644000076500000240000000375300000000000017114 0ustar00erikstaff00000000000000.. highlight:: shell ==================== Release Instructions ==================== You will need to set up a gpg key (see the `astropy docs section on this `_ for more), PyPI account, and install twine before following these steps. 1. Ensure all of the issues slated for this release on GitHub are either closed or moved to a new milestone. 2. Pull a fresh copy of the master branch from GitHub down to your local machine. 3. Update the Changelog - Move the filled out changelog headers from unreleased to a new released section with release number and date. Make sure you still have empty sections for the unreleased section (Can make a new commit after this step if desired). 4. Update version number at the bottom of the setup.cfg file. 5. Make a commit with this change. 6. Tag the commit you just made (replace version numbers with your new number):: $ git tag -s v0.5.2 -m "tagging version 0.5.2" 7. Checkout tagged version (replace version number):: $ git checkout v0.5.2 8. (optional but encouraged) Run test suite locally, make sure they pass. 9. Now we do the PyPI release (steps 20,21 in the `astropy release procedures `_):: $ git clean -dfx $ cd astropy_helpers; git clean -dfx; cd .. $ python setup.py build sdist $ gpg --detach-sign -a dist/specutils-0.5.1.tar.gz $ twine upload dist/specutils-0.5.1.tar.gz 10. Checkout master. 11. Back to development - update setup.cfg version number back to dev, i.e. 0.6.dev and make a commit. 12. Push to Github with “--tags†parameter (you may need to lift direct master push restrictions on the GitHub repo) 13. Do "release" with new tag on GitHub repo. 14. If there is a milestone for this release, "close" the milestone on GitHub. 15. Double-check (and fix if necessary) that relevant conda builds have proceeded sucessfully (e.g. https://github.com/conda-forge/specutils-feedstock) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/docs/rtd-pip-requirements0000644000076500000240000000006700000000000020427 0ustar00erikstaff00000000000000numpy>=1.9 numpydoc scipy gwcs astropy>=3.0 matplotlib ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/docs/spectral_regions.rst0000644000076500000240000001753700000000000020513 0ustar00erikstaff00000000000000================ Spectral Regions ================ A spectral region may be defined and may encompass one, or more, sub-regions. They are defined independently of a `~specutils.Spectrum1D` object in the sense that spectral regions like "near the Halpha line rest wavelength" have meaning independent of the details of a particular spectrum. Spectral regions can be defined either as a single region by passing two `~astropy.units.Quantity`'s or by passing a list of 2-tuples. Note that the units of these quantites can be any valid spectral unit *or* ``u.pixel`` (which indicates to use indexing directly). .. code-block:: python >>> from astropy import units as u >>> from specutils.spectra import SpectralRegion >>> sr = SpectralRegion(0.45*u.um, 0.6*u.um) >>> sr_two = SpectralRegion([(0.45*u.um, 0.6*u.um), (0.8*u.um, 0.9*u.um)]) `~specutils.SpectralRegion` can be combined by using the '+' operator: .. code-block:: python >>> from astropy import units as u >>> from specutils.spectra import SpectralRegion >>> sr = SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) Regions can also be added in place: .. code-block:: python >>> from astropy import units as u >>> from specutils.spectra import SpectralRegion >>> sr1 = SpectralRegion(0.45*u.um, 0.6*u.um) >>> sr2 = SpectralRegion(0.8*u.um, 0.9*u.um) >>> sr1 += sr2 Regions can be sliced by indexing by an integer or by a range: .. code-block:: python >>> from astropy import units as u >>> from specutils.spectra import SpectralRegion >>> sr = SpectralRegion(0.15*u.um, 0.2*u.um) + SpectralRegion(0.3*u.um, 0.4*u.um) +\ ... SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) +\ ... SpectralRegion(1.0*u.um, 1.2*u.um) + SpectralRegion(1.3*u.um, 1.5*u.um) >>> # Get on spectral region (returns a SpectralRegion instance) >>> sone = sr1[0] >>> # Slice spectral region. >>> subsr = sr[3:5] >>> # SpectralRegion: 0.8 um - 0.9 um, 1.0 um - 1.2 um The lower and upper bounds on a region are accessed by calling lower or upper. The lower bound of a `~specutils.SpectralRegion` is the minimum of the lower bounds of each sub-region and the upper bound is the maximum of the upper bounds: .. code-block:: python >>> from astropy import units as u >>> from specutils.spectra import SpectralRegion >>> sr = SpectralRegion(0.15*u.um, 0.2*u.um) + SpectralRegion(0.3*u.um, 0.4*u.um) +\ ... SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) +\ ... SpectralRegion(1.0*u.um, 1.2*u.um) + SpectralRegion(1.3*u.um, 1.5*u.um) >>> # Bounds on the spectral region (most minimum and maximum bound) >>> print(sr.bounds) #doctest:+SKIP (, ) >>> # Lower bound on the spectral region (most minimum) >>> sr.lower #doctest:+SKIP >>> sr.upper #doctest:+SKIP >>> # Lower bound on one element of the spectral region. >>> sr[3].lower #doctest:+SKIP One can also delete a sub-region: .. code-block:: python >>> from astropy import units as u >>> from specutils.spectra import SpectralRegion >>> sr = SpectralRegion(0.15*u.um, 0.2*u.um) + SpectralRegion(0.3*u.um, 0.4*u.um) +\ ... SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) +\ ... SpectralRegion(1.0*u.um, 1.2*u.um) + SpectralRegion(1.3*u.um, 1.5*u.um) >>> del sr[1] >>> sr #doctest:+SKIP Spectral Region, 5 sub-regions: (0.15 um, 0.2 um) (0.45 um, 0.6 um) (0.8 um, 0.9 um) (1.0 um, 1.2 um) (1.3 um, 1.5 um) There is also the ability to iterate: .. code-block:: python >>> from astropy import units as u >>> from specutils.spectra import SpectralRegion >>> sr = SpectralRegion(0.15*u.um, 0.2*u.um) + SpectralRegion(0.3*u.um, 0.4*u.um) +\ ... SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) +\ ... SpectralRegion(1.0*u.um, 1.2*u.um) + SpectralRegion(1.3*u.um, 1.5*u.um) >>> for s in sr: ... print(s.lower) #doctest:+SKIP 0.15 um 0.3 um 0.45 um 0.8 um 1.0 um 1.3 um And, lastly, there is the ability to invert a `~specutils.SpectralRegion` given a lower and upper bound. For example, if a set of ranges are defined each defining a range around lines, then calling invert will return a `~specutils.SpectralRegion` that defines the baseline/noise regions: .. code-block:: python >>> from astropy import units as u >>> from specutils.spectra import SpectralRegion >>> sr = SpectralRegion(0.15*u.um, 0.2*u.um) + SpectralRegion(0.3*u.um, 0.4*u.um) +\ ... SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) +\ ... SpectralRegion(1.0*u.um, 1.2*u.um) + SpectralRegion(1.3*u.um, 1.5*u.um) >>> sr_inverted = sr.invert(0.05*u.um, 3*u.um) >>> sr_inverted #doctest:+SKIP Spectral Region, 7 sub-regions: (0.05 um, 0.15 um) (0.2 um, 0.3 um) (0.4 um, 0.45 um) (0.6 um, 0.8 um) (0.9 um, 1.0 um) (1.2 um, 1.3 um) (1.5 um, 3.0 um) Region Extraction ----------------- Given a `~specutils.SpectralRegion`, one can extract a sub-spectrum from a `~specutils.Spectrum1D` object. If the `~specutils.SpectralRegion` has multiple sub-regions then a list of `~specutils.Spectrum1D` objects will be returned. An example of a single sub-region `~specutils.SpectralRegion`: .. code-block:: python >>> from astropy import units as u >>> import numpy as np >>> from specutils import Spectrum1D, SpectralRegion >>> from specutils.manipulation import extract_region >>> region = SpectralRegion(8*u.nm, 22*u.nm) >>> spectrum = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49)*u.Jy) >>> sub_spectrum = extract_region(spectrum, region) >>> sub_spectrum.spectral_axis Extraction also correctly interprets different kinds of spectral region units as would be expected: .. code-block:: python >>> from astropy import units as u >>> import numpy as np >>> from specutils import Spectrum1D, SpectralRegion >>> from specutils.manipulation import extract_region >>> spectrum = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49)*u.Jy) >>> region_angstroms = SpectralRegion(80*u.AA, 220*u.AA) >>> sub_spectrum = extract_region(spectrum, region_angstroms) >>> sub_spectrum.spectral_axis >>> region_pixels = SpectralRegion(7.5*u.pixel, 21.5*u.pixel) >>> sub_spectrum = extract_region(spectrum, region_pixels) >>> sub_spectrum.spectral_axis An example of a multiple sub-region `~specutils.SpectralRegion`: .. code-block:: python >>> from astropy import units as u >>> import numpy as np >>> from specutils import Spectrum1D, SpectralRegion >>> from specutils.manipulation import extract_region >>> region = SpectralRegion([(8*u.nm, 22*u.nm), (34*u.nm, 40*u.nm)]) >>> spectrum = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49)*u.Jy) >>> sub_spectra = extract_region(spectrum, region) >>> sub_spectra[0].spectral_axis >>> sub_spectra[1].spectral_axis Reference/API ------------- .. automodapi:: specutils :no-main-docstr: :no-heading: :no-inheritance-diagram: :skip: test :skip: Spectrum1D :skip: SpectrumCollection :skip: UnsupportedPythonError ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132249.0 specutils-0.7/docs/spectrum1d.rst0000644000076500000240000001315400000000000017226 0ustar00erikstaff00000000000000======================== Working with Spectrum1Ds ======================== As described in more detail in :doc:`types_of_spectra`, the core data class in specutils for a single spectrum is `~specutils.Spectrum1D`. This object can represent either one or many spectra, all with the same ``spectral_axis``. This section describes some of the basic features of this class. Basic Spectrum Creation ----------------------- The simplest (and most powerful) way to create a `~specutils.Spectrum1D` is to create it explicitly from arrays or `~astropy.units.Quantity` objects: .. plot:: :include-source: :align: center >>> import numpy as np >>> import astropy.units as u >>> import matplotlib.pyplot as plt >>> from specutils import Spectrum1D >>> flux = np.random.randn(200)*u.Jy >>> wavelength = np.arange(5100, 5300)*u.AA >>> spec1d = Spectrum1D(spectral_axis=wavelength, flux=flux) >>> ax = plt.subplots()[1] # doctest: +SKIP >>> ax.plot(spec1d.spectral_axis, spec1d.flux) # doctest: +SKIP >>> ax.set_xlabel("Dispersion") # doctest: +SKIP >>> ax.set_ylabel("Flux") # doctest: +SKIP Reading from a File ------------------- ``specutils`` takes advantage of the Astropy IO machinery and allows loading and writing to files. The example below shows loading a FITS file. While specutils has some basic data loaders, for more complicated or custom files, users are encouraged to :doc:`create their own loader `. .. code-block:: python >>> from specutils import Spectrum1D >>> spec1d = Spectrum1D.read("/path/to/file.fits") # doctest: +SKIP Including Uncertainties ----------------------- The :class:`~specutils.Spectrum1D` class supports uncertainties, and arithmetic operations performed with :class:`~specutils.Spectrum1D` objects will propagate uncertainties. Uncertainties are a special subclass of :class:`~astropy.nddata.NDData`, and their propagation rules are implemented at the class level. Therefore, users must specify the uncertainty type at creation time .. code-block:: python >>> from specutils import Spectrum1D >>> from astropy.nddata import StdDevUncertainty >>> spec = Spectrum1D(spectral_axis=np.arange(5000, 5010)*u.AA, flux=np.random.sample(10)*u.Jy, uncertainty=StdDevUncertainty(np.random.sample(10) * 0.1)) .. warning:: Not defining an uncertainty class will result in an :class:`~astropy.nddata.UnknownUncertainty` object which will not propagate uncertainties in arithmetic operations. Defining WCS ------------ Specutils always maintains a WCS object whether it is passed explicitly by the user, or is created dynamically by specutils itself. In the latter case, the user need not be awrae that the WCS object is being used, and is can interact with the :class:`~specutils.Spectrum1D` object as if it were only a simple data container. Currently, specutils understands two WCS formats: FITS WCS and GWCS. When a user does not explicitly supply a WCS object, specutils will fallback on an internal GWCS object it will create. .. note:: To create a custom adapter for a different WCS class (i.e. aside from FITSWCS or GWCS), please see the documentation on WCS Adapter classes. Providing a FITS-style WCS ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python >>> from specutils.spectra import Spectrum1D >>> import astropy.wcs as fitswcs >>> import astropy.units as u >>> import numpy as np >>> my_wcs = fitswcs.WCS(header={'CDELT1': 1, 'CRVAL1': 6562.8, 'CUNIT1': 'Angstrom', 'CTYPE1': 'WAVE', 'RESTFRQ': 1400000000, 'CRPIX1': 25}) >>> spec = Spectrum1D(flux=[5,6,7] * u.Jy, wcs=my_wcs) >>> spec.spectral_axis #doctest:+SKIP >>> spec.wcs.pixel_to_world(np.arange(3)) #doctest:+SKIP array([6.5388e-07, 6.5398e-07, 6.5408e-07]) Multi-dimensional Data Sets --------------------------- `~specutils.Spectrum1D` also supports the multidimensional case where you have, say, an ``(n_spectra, n_pix)`` shaped data set where each ``n_spectra`` element provides a different flux data array and so ``flux`` and ``uncertainty`` may be multidimensional as long as the last dimension matches the shape of spectral_axis This is meant to allow fast operations on collections of spectra that share the same ``spectral_axis``. While it may seem to conflict with the “1D†in the class name, this name scheme is meant to communicate the presence of a single common spectral axis. .. note:: The case where each flux data array is related to a *different* spectral axis is encapsulated in the :class:`~specutils.SpectrumCollection` object described in the :doc:`related docs `. .. code-block:: python >>> from specutils import Spectrum1D >>> spec = Spectrum1D(spectral_axis=np.arange(5000, 5010)*u.AA, flux=np.random.sample((5, 10))*u.Jy) >>> spec_slice = spec[0] #doctest:+SKIP >>> spec_slice.spectral_axis #doctest:+SKIP >>> spec_slice.flux #doctest:+SKIP While the above example only shows two dimensions, this concept generalizes to any number of dimensions for `~specutils.Spectrum1D`, as long as the spectral axis is always the last. Reference/API ------------- .. automodapi:: specutils :no-main-docstr: :inherited-members: :no-heading: :headings: -~ :skip: test :skip: SpectrumCollection :skip: SpectralRegion :skip: UnsupportedPythonError ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/docs/spectrum_collection.rst0000644000076500000240000000714100000000000021213 0ustar00erikstaff00000000000000================================ Working With SpectrumCollections ================================ A spectrum collection is a way to keep a set of spectra data together and have the collection behave as if it were a single spectrum object. This means that it can be used in regular analysis functions to perform operations over entire sets of data. Currently, all :class:`~specutils.SpectrumCollection` items must be the same shape. No assumptions are made about the dispersion solutions, and users are encouraged to ensure their spectrum collections make sense either by resampling them beforehand, or being aware that they do not share the same dispersion solution. .. code:: python >>> import numpy as np >>> import astropy.units as u >>> from astropy.nddata import StdDevUncertainty >>> from specutils import SpectrumCollection >>> from specutils.utils.wcs_utils import gwcs_from_array >>> flux = u.Quantity(np.random.sample((5, 10)), unit='Jy') >>> spectral_axis = u.Quantity(np.arange(50).reshape((5, 10)), unit='AA') >>> wcs = np.array([gwcs_from_array(x) for x in spectral_axis]) >>> uncertainty = StdDevUncertainty(np.random.sample((5, 10)), unit='Jy') >>> mask = np.ones((5, 10)).astype(bool) >>> meta = [{'test': 5, 'info': [1, 2, 3]} for i in range(5)] >>> spec_coll = SpectrumCollection( ... flux=flux, spectral_axis=spectral_axis, wcs=wcs, ... uncertainty=uncertainty, mask=mask, meta=meta) >>> spec_coll.shape (5,) >>> spec_coll.flux.unit Unit("Jy") >>> spec_coll.spectral_axis.shape (5, 10) >>> spec_coll.spectral_axis.unit Unit("Angstrom") Collections from 1D spectra --------------------------- It is also possible to create a :class:`~specutils.SpectrumCollection` from a list of :class:`~specutils.Spectrum1D`: .. code:: python >>> from specutils import Spectrum1D, SpectrumCollection >>> import astropy.units as u >>> import numpy as np >>> spec = Spectrum1D(spectral_axis=np.linspace(0, 50, 50) * u.AA, ... flux=np.random.randn(50) * u.Jy, ... uncertainty=StdDevUncertainty(np.random.sample(50), unit='Jy')) >>> spec1 = Spectrum1D(spectral_axis=np.linspace(20, 60, 50) * u.AA, ... flux=np.random.randn(50) * u.Jy, ... uncertainty=StdDevUncertainty(np.random.sample(50), unit='Jy')) >>> spec_coll = SpectrumCollection.from_spectra([spec, spec1]) >>> spec_coll.shape (2,) >>> spec_coll.flux.unit Unit("Jy") >>> spec_coll.spectral_axis.shape (2, 50) >>> spec_coll.spectral_axis.unit Unit("Angstrom") :class:`~specutils.SpectrumCollection` objects can be treated just like :class:`~specutils.Spectrum1D` objects; calling a particular attribute on the object will return an array whose type depends on the type of the attribute in the :class:`~specutils.Spectrum1D` object. .. code:: python >>> print(type(spec1.flux)) >>> print(type(spec_coll.flux)) The difference is their shape. The returned array from the :class:`~specutils.SpectrumCollection` object will have shape ``(N, M)`` where ``N`` is the number of input spectra and ``M`` is the length of the output dispersion grid. .. code:: python >>> print(spec1.flux.shape) (50,) >>> print(spec_coll.flux.shape) (2, 50) Reference/API ------------- .. automodapi:: specutils :no-main-docstr: :no-heading: :no-inheritance-diagram: :skip: test :skip: Spectrum1D :skip: SpectralRegion :skip: UnsupportedPythonError ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1542658279.0 specutils-0.7/docs/specutils_classes_diagrams.png0000644000076500000240000044273200000000000022522 0ustar00erikstaff00000000000000‰PNG  IHDRÐ@JsRGB®Îé pHYsÄÄ•+@IDATxì¼UÅÇîîn–V¤;EBT@PBÀ A$E@@¤%$%é’îîîÆÿ÷2ü—õÜûî»ïòÞãs?|{æÌîÎùíž=;;³³/üûï¿áô§(Š€" (Š€" (Š€" <ë„ÖPŸOPE@PE@PE@p!  °öE@PE@PE@PçU€Ÿ‹fÖ‡TE@PE@PE@PXû€" (Š€" (Š€" (ŠÀs€*ÀÏE3ëC*Š€" (Š€" (Š€" ¨¬}@PE@PE@PEà¹@@à碙õ!E@PE@PE@PT~:úÀªU«7n¼sçΧC\•R [·nÍœ9sË–-1>•÷Ÿí§Ûºu+mwóæÍ°Ó6O ð'UoØAþ©$,7SX–-„÷Ù@þý÷ß¿þúkûöí!„ÞS]ìœ9sš6mzêÔ©§ú)TøgÞU_~ǧã~ÿý÷?ÿüóŽ;|Éò\ñܾ}{éÒ¥ .ù䓱cÇÞ¹sÇp,X}8^¼x†: „ >|ôèÑC§:kY³fM£FXÔ„¿aÆ£G¶32M,R¤ˆMáR¤H‘!C†fÍšÕ¬YÓ¾å1½aôë×^{-qâÄ”øÜ"pïÞ=VFÂÔãøá‡H’$É»ï¾k ƒrŠl¥5bk"l"f»SàrŠY³fµoß>uêÔ RQ¡ÉüÒK/=z4V¬X+W®Ì–-›]õ?üвeKÖ¿x ›Ôt˜z+ƒ·QÜ=¨à(ÿã#Ê­¦ú³/è…œÀeÊ”‰'“ä:ø"‰ò(!‡@ .ÐhÒï¼ó_5Ñ~_xáeõêÕÿüóOȉå±ä>}úðÑ#Ƹqã<2„>‘ï6mÚ.\X´_1‡²hïß¿ñÏ?ÿ¬U«jm sQ£F‘½N:v!šVXyyñÅß~ûí0Åûï¿_¥J•¢E‹©Xï)ÒîÝ» %ì$Ü¥ ;²©$ËÝÉ£À‰Ÿâ•W^©\¹2³OYÂG$$¹|ù2º®C$ùü¹l^†©·2Åc£І2/¡ß aª?ûfÈ ÌL jÕª{÷î]¶l™/’("rbÆö8mÚ4ªGóÄuI-î@,ˆ~óÍ7±cÇ9±<–ÌWè&á‘-4‰¬Lž<™Á'P©òçÏß·oßk×®áCΞ^ɈüÖ[o±cÄ,.8äg2A çT«VÍqK/ŸsnܸA÷ G…)>ûì3‡<¬±è¦¶Œ Ý¥5·46ËÝÉwÄ<>nAÓ§O÷½'ʼnGÔ¨Q-Z„«ä›o¾ìb„©·2Åc£;zZ wB¿ÂTöŽÜ Q±åŒ3#pñâÅ}FyB “Tüꫯ² •t¢D‰Þ{ð3­X±âüùó¬’Æÿþýì_bCl¡B… (€ÚfØ$ÁÞwv¼°–rØ#‘/_>wÅ÷*xvíÚ…§Ä믿ž>}zfÏÔrèÐ!)„m´K–,¡pq->}ú4ijG–&MšDÆ&MšDŒÅÙø€eÏžÝH‚O2Ó¦M›3gNˆvv$ç.Úi‚ X¦¢LØì4uêT$Çõ«T©RÑ¢E3E±.]:vmaÑmÛ¶-t÷Ç1ÌxŒ †åÊ•ƒH™uëÖ¥Fmذa­úóh'Nœ([¶¬G‡sâg° SPÊ’%KäÈ‘Mu$ÈÈ~-ñR“ÇL•*P€ŒÍ&iÜËÙ¾ÅÃæÊ•+eÊ”î )lß¼y3ÒšI“&uðsþüùÜÅâm(ü´/Ápü£áð5Ä  Ñ Â@{-_¾œYWžHEgó諌 oDòäÉé`ŸLX4 XF‘׊zyS¨‚w¼$âÆ+ˆy‡Ý VˆùËëÌ\–5yp:›MÄ‹1„“7…iZHS̤̀YÚÈn nݽ{—v¡¯Úíb—ïHSC]š.—5kÖ„ :Ü/Á 7  hÚÔ0<ö¡´í-&# Þzái_¶¡ðòŠÙ<Ž´/Í-Y17ѼÐ…Huöú#+ t^3D„ÏÙ³g‰G౫ӻ'cÆŒ)µû.§ý€^Æ ËÊ…¨´,}^ªH–,ïoHt§€ú†y4a“ÌàÌ»l øRð8 Åô//(ñàluqßíâ]óÊ#C C½C`?.é;wf"Ю];¶ñ{Ù<K ò€||Y2æ£ÀšÞËŽ*Ç+l¿•Œ6|ãèÛö›%å‹^a*òÒý h·yH»7Šh{it»º@GBß_L@Bm¡ù0}bæØ k÷géQ9áñÞ |ïŸ }M – 'Ús,@#FŒï¦)_Š@¨"À‹äå‡b&Ò -0š¸sò 6µwêÔÉþ²ºãˆ“1xð`Çüuˆ7ÐËèß½{wû•`ñå—_bˆö :y;vìÈ]8ÑŠ®ÂW­Ir1ß5UÈ\:3K!šìÌ왵˜ŠxE™d³Âm›™=Û‹2@9˜Ä%#{€M]’øõ×_å›·Ø,·¨×qË\~ûí·ð° ÛP$RôùçŸÛK h Ì×ÚpâêC^<¼dÍlŒé/kÃC‚A­U«V†,8u£®Ø<îiô:{Y\ÅŠ“1Ì"skóUàqÌ]“g²úé§(ÉfÏãüôÓOð`67Š=óBVLFS¦L±õÊÉ;7­oxLáÛÒËÃ~ñÅÂC‚\´…É" q-?~¼ƒ.—LݘƑQ~¨îëÖ­“[<5 °Û¼Qc܃¡ê£Ìü¿ —ú4dÈGu3f̰gÃLøXcb†mr™«3Fˆî°Š•£jÖ§(‡ý熎f%¯'…"ê Ê?{x!J£³?tóæÍx&!J»0{^¼x±Qrìv1åÛ éÌv·Gž.]ºØ<Ž4R±f«Üø\[ZˆöɈ>S½zuóDvââÅ‹¦Gˆ/_šÛ†£ Åš6:c,D[~è_}õÄÞ½{“öŽ@ÁùÑG™Z$AÐÙZ"—Ê) Ùý9Ð1ÄG`à·ß~³;<‚ɯuëÖÁÞ¼÷ „ t”3ÝX ã¯8ÿ³4f($Ðy ^@Ò>…ãK¨ ”)-èPo‹äwšåNF3 “Q}äÈ‘¦¨ò˜^"Ü\ôXÖÍhL– ðE3åØo%_+ÞwÆFöP>äâ (»Ÿý>zÀM-’™íFñí@Ý—‘0ÐÓ–\zˆý ‰Àù—,YÒT„ý€™—,›æ°û3D3ú1á!{ ÝÀ—þé}èsL¥Á8á¤4 @䘈Bן"š¸ö¬zùÙš'Ÿ«`B±ù,ÆR3 H¢^½z†¹ÿþBÄRÁ|Ë|üе NS+Äb¨D9ä=1t“`®Ì2yEƒ…n›JX`3 0– S…Q€ C%D“Ýý0ÊÙSm©šãˆLi&áŸÌÌFÊäëÎ’¶)ÍNÈÜôl"i¶Y’tРAèH]»v1(¬NÅ‘Ñ*ýÙKŒ‘ÃC¢bÅŠ™Ð/X°½…¹ÚÍä%Z5C02ÃÆw•þQ‹¡ŽÂ¢ãá…Áº˜ë£ŒnÀÉc²¼È’·¬w´hÑÂÈæè`/ÇÀÂâ2°þÈ×\²øØýL Þp#†$ܛشmô@GB_^L[r@Baa´ç;Ë[ÀÌjøðá,P2±3»lV ¥Gù7áñ¥øÒ?½}Ž~ŒNJ“(3»ÇjZeQ€‘ÆåÊ{Î0MŒG£˜[L¯±Á¢ÿˆ™ j†è¨LP 0‚pë…ÂoîܹT„‹F$.ù¶1Q`dáóÆ[Ê>[îbüàƒ°‡CÙÃçÓX¤Ë]fÒL…™-‘7¨ 0Ù±Q2‹TÄ_¾Ó,WãáI¥BDÏDÇÏ?˜O”©È6©Ù…Ë”«ˆMdóqþıÊÐU™,‚¡9-IF1Ìk¸ °ˆA›©°P8º‰¢h S ±@D»°‰vc/ öÚ?3rÉ…Sœá0Ë3¯5D÷„|(ÐÎËŠ;ð7 !UdÊ” "6CSú§môf’Ù3ÌþÍÚ¹ñ€(œPÅG†x‘d7»ìŸ å•÷‚­yË÷Ò¼ô Žr¶Á•ª Þ‡z[*ìHŒÞÆAÉÎèH³BÍàÆz" ŽŽ[^.ñYÀðkˆkÀPÃrp@aùlÉn,ÖGXkæÃ„s™¬RˆïÝÏÔèËn˜JøŽv@%Øtï]׿SÑS U`›1NÜdõÜn\iZ<¨ž uïýÓËÐç.m0N8¥p™ÐÚ9÷J•¢„4‚!9ªÄAÇE¦ï|êx£p…Ï'ª)—†ã=#ıY:÷±cÇà‘i ·IÒh’’]&7†‡qAèAýËa¼â|ÔŒÂÏgØdÄ|'î߆ÈŸ™·ÈŒ’i¡1¹‚š@}<ÉèØÈjŠs“cÒ/'ÊÙ sÞ¼yQQØ™l($¢Šó9–"á‘Ù<í"” QŒç OR;S{Ë7eÕ€é&¦Zû–»œR‹ã¯¬/¢8´;ˆ²â¾X€­Of&aheÒlßÊt”ãk–$YÍ¥}ád*†ÖÁÂ8)©$Á ìThƒ|üx;Xpð8`gu6 ›ÀÎê†Qï….+M ò,o„èóŽÂ}¹ ö@±² GÙÆ4XTbmÇ콬v1 ¡ô"$ÏÂªŠ¸xØy}I{o—€J  ³RÀþ+™y;šÛÎȤZbÔ¡¨ãf‰UÄîœ6§I{—жfÒÏ¢•Š/=°ð"Ûj›)J¾4·# —,Ó°è#~bÔÈ«Í%nðt<ž…ÍðÈ’Šø§™¼àCà="| ÿgº=m'‹,þÉ)¹|C¼‹ü,5ò‘L”5±–xÁÖ<5‰@Ë·™I{éAå%ËePepŒ9ŽQΉÅ>FØ÷4+ñâ/à~ËPóY:AÅCŠ%?C÷žpÈ 3Ã#\3<ºgÇx‹ÒË–ŒftW !l¾w?S¬/¸a(áx /hT‚M÷Þuý{1uá@dqy—Ý”‰†Û&º§-îË„'HÝÀ{ÿô2ô¹‹ŒN)\b×ÉTͽ:¥(¡ƒ@à °ÈÁtœ nK8…¢ýBÄn çÓzÔX&ÅL$j0œµø9²°«бáOÈÁàãe #ŽåÀf¦Èv™‰¾—éÎ)»O¡3Älb"5ØÜ’‰‚½çYn‰â0Λ\’@“·)R»ªd*onáÕÐùÕÎêë L©ƒe€¹>ÂØøséNdÚ„“< áD=fN/¾ÌÞ̳¸' ÐåX,§Kc¾¦4æßô[ÌÂÆñÁ½œÒY¾e†)ƒ"bh¡{¾;¿|ÄÇ»‚!-…/«ÍŒ$t™ýË!+µ6ßi?°`´,`ñƒbAÕ." °(`Æé·l’QÚÅKÛq U–±ˆnF.ÅÉë% ›ØH‰{ž¸™àâS‰÷QZGoÁ…a€ÿ'Á{èxì ¡(÷cQMù¾4·a6 Œ ØÊè!¬Yà,€üì7c(Æ÷v— `Æq %c ø×?Lp@§ùXÚ`¹? yýý“Sri ‘gt ‘n†_ïŸ0Šèü¼’´ «¢v'/}CÊ}Œ ÞQδµÇÄcÊ௩…íý,…˜K Ùáñ–M¤±p¯`ó¹Ùáåe´3ÚiÇðhß2iQ€¹¤3ˆ¶)·üë~AÀ%¼ P/tGוgôþ½p/M@dùÕï)«Ýdîsî:ˆþu©ÅÑ?½ }¶T’h òcÂ)Ê„Ǫ̈ÝkTŠ" üG ´>ì¢øBˆ-‹ (vB±È¹gÄ8#DÙi ˜lBÁ°ùy-ÅÂ|Ò04Ù ~§eì »ñÕô»¨Ê(;Q)\6x¬…±•å6ŸX«PüHK76%ûžàãÍÎQ¶ÖK wŠB©öâšîKs{€•`ÔT41–ÿåxF¬µ8‹²Ý§\9X©13 _ðáM¡Aé·à€@½âÿLÂ?9ýC<>,Dž”¿¸ÃðBñ£YÙÃÎRÇì- ì~Ðê~ròÝñâ’à»~Ëht'~²ùÂÀ'÷|ÉâÎãÝÀÛ/Ý€E|"vÌÆÿº_Ppw‘B“âß‹‰„:€„è"`ï&‡`ì'~w24ô¹33ׄS :XÜ¥UŠ"à#ì&ÊŽ1ÞJ‰Æ„ÅwH‚7 I¡ÙüÉT[ŒEÜ’ØL…9ö^ûgTá„ÍöÅe-Ù 0“<·¡‰É´‰W׃9K$y²qfá\”&y&–»T2’ñÂÜ<±Š$„âEvðs)EIxX÷»)’«±šl?j·³û‘æp`vE>¦÷;U£H³Lƒ[/ªªvB_ŒN˜|Q˜á§»ßriÖ€ä¹pÙ%a:¹`(Q©„Áý¯,d`¶e“§û](¢õaÁóx×Aô+ ƒø£ý‚ H ’¨¾Ì ¡ÿ»÷y.–fÄsÕQ£¹ ’œ&—Ç޿бYl‰d!¾Æ[ñöÈã ‘æfTÄþÏz:0‹S´µc]ÏQŽ/ÍíÈ"—¢T£¢ÀC‘ýl2õGàÑööf„,aJQ‘’i;‰ÛDQþÉ)¹‚4†ÈÓ¹ÿū߯|y äsÃ.;™öæ`ìN¦L÷¾!åÇ(Ç—‘bíe\¾bŽ_žÂoÌs…N‚° <5[9 Õ—ù"‹ã†0ƒ|;Ìðè^Nv“ï>w9Êˬk?N÷ó2€»Ëðø_Ýc-òŒÞ¿3ꢈl}b”°ÁwÙí»“ö»x©Ô}èsg–zƒeÂ)… D2¹u¯N)Š@è ˆLdŒ :Âa¶ÌtÙô‹õ@$“ rFJæèuêÔáCÈpÃÎù8ñjáhñ*Ä*H9Û#Nª¨.h€ø¼q~=<ìÍÆF‚ÙŽ…̶q¬bE™b¥3 ¢ ì8"”´Üòø— š6·øÖ2ëÅòCL#LvÂ쇛–{-¸&‚ ?‰c ["…âØˆË-â0±RΙXÑY/—ÙDj& îUÈ.,&Ùö- ÎDi¶–…õ…~ìc6³÷4~e0Èá46'{2b0CEçÁÓmEFV@D7Žpvi!” ecŒTº(qׂZ#Ë+ØÁX˜[žÝôsåиö>dÙó#î@†Ÿaèœr ³`eÔ$¦z(Øt /ar‘}æ$hJôpìó6»q Âb"ö%V ¥ù•¸7ãóŒÖgÞzÑÁ°É€U þÏA’Ó†Â=íx Ú+Ð%-‰­mŠ’AÆÑXæ®/ *… ˜Qˆ9κLͽE¾4·ÇªÙdŽ9”÷šM›h­2PðbvÇ_}6˜F¡_ða(fˆfÔeìe ¦«Ë¼œìþÉéÇâña!‚-³|F†VôsÒîËÁؼô ¿G9‰æ`"êñ¡g‹§³?:¾<…ß2oÑq‘Áß>@ÁK]èÉô£ã¿ÃŽ®.³÷Œ¼_øn°@‰£ ž Œœ¤…ÓîçËî.ÆãS|itµø÷bR” !:€HHBN"0_mú*³M·_vmêÑïnà^——¡Ï9'œR¸(À2¹u¯N)Š@è ˆ 4S%fH¬Êó³â=Ä?ͦà÷…=íΙT±ÑH¦V,£KåŸù ùa›5ß?¦Ñ˜•ø¼‘È.(«ü¤([ e-…øzµˆ»ìÁ (v”äÅAQ4^LÙü òDØœ™W Ãcþå«/>{¦tQ{Øsèpˆ%¤' ' –ŸYÒ6rû–I›ÈR†B‚“oÐrñcdÆ€ý‡Ù0SdàÙœÉú‚ñý¶³”ƾÇÒ3itZ¦Âh\ÌŒ™}bÏd^"ã»#/mŠ:ÄáöíÛ³,¤ ìÿ´ Eáìà¹KŽ AY1Awe‘…ygñ‰#´Ÿ´S@S¨GáÑHùñU4ŒT¢ƒ9²Ð¸å^¦ Øåh}˜åÓ(e²˜¨étQÖzØÃÉ$’Y[:1·Š1ÞŽ›]% €DÃBû¥•ÍÁkÈ*¶&\©© åÇ~’ûº.o+` 4º–ì—w*P˜§Ã½œ7ë:k¼²Â!¡/—t0ö{Ë ˆ4X CÿÇ"Ps£Ÿó1t $£ñ XÀÍlšõ¥RH½¼ø AîÒXµjÕ¢'äÖhs;j1—HÎëÆ‹øÒ·¹þ¬²ê„…GTzá÷ú*+•ÛÑoýÓ1Ä<#ÁkÅcò±°é¬còbš±(¸º“÷¾á÷(ÇYzHKad@ƒåÝ'ì… ×öCùò~Ë`W:i‚ù1%à÷¥:GLjDÅÇ‚©0]‘o.³G ¼Ú@ÊàFüg^yîòÃÅŒŽÊ‹9´ÝïÁøÈî#X.}iô€*òãÅ”¢t ¡x™}±Užù$a_éÌ ÈØ?xë{B@­é7Ýïn`×è}è³9%ŒN)© ™Ü Eÿ*O¾.^~¨4ÌêL„wä#͆æÜ’K Ðù¤YT¼¿ä’€£dt*‡¾ŠöÂ69õT˜™ÕÙûŠ™ô³vnÊa~i„!8«nÜ2“~†Ã) tl¤enŠH|\QÑCXçë´ðxÌŽTððUƹŔ‰ÿ'D6, Ñô ;~ˆ„‡ùºîÙ*.Ê(¨û5…”`ý1دųØ< ½Ý²˜³ÀÓÙ<â‰~he!á4 üQ´ì`KŒÞEÕ†Ç=!y[že~lNØ ’Ç9¬Rö-™ƒ’Md‹#uÑ ‘™øç@§ç wI›†óX8sS$G“4åHBô"l³ºã¥Ë6VS^Ãp”e~&‹ô´&鮆Zz©8ê“….J‡'œŒÍÃ[cBLS®Úô(ÃÀÊ…éh2Bö@±2Å:bïåí¶_Uqçv0»×ÎlÀ(É@!ò©]L|¹qH‘^,¾°(À*“`ÃãHºqxå‡~Å4Ýð8¤õE*Üžéi<8MÌ¥z«”Ïh)%{,Ç—æ6‚™{€¥±ÖM,}ü> ‘„øÀ&q8=võ@åt€F½Ž!q eO5+§¬ Ñ·Y­@³âáÇK'OŒÝÉ{ß º@G9ÏEëï>ÞhÆ"qÜ5'xS²ïO¨ ”æÞ"݇zˆÿãÆjµ{9tT‹Ü?ý†™¯ ƒÎ_Æ¥‹\˜íÇáSEœÿM!$äC@9B j÷ó>€Û‘vob[<Ãì Ú¾7ºû«AE¾˜F;¡HÈ àŒ£ ‹ï,ÂÒKYýÄ9o TnvØšVpt÷§8íŽêD Gÿô>ô¹—ŒN¦©`å>s0piB\~þxŸ‰4CØV²ÝÕ0º2?f„”'£-ýÛ¡ÙU°HÆW3)Þ˜sí[&Cެ8ŠIð Aü7¨ËùZ°Ò\:Ü¢Fö:f(ûA%H½?áüúë¯iü¥@ý«<)|R€½ÇKHWægÏü¼ðë-?ën`~äÕ,¾#Àç‡Ý¼,å¤ÏûX”­û˜EÙž"äx¶ýÛ2³„Á0h¯úÛw5í bÁÝÀÁŒ]lFB^>Ø ðS!° ù! ÈSÑX¸ˆ3žž§Bfò™D =ÀtSý=q Ö¹sg<-qîÅ™ç‰Ëó¬ €¯Î8ëÚÞàÏêÃêsùû9ÙôEèÖ¤ØÌÌŠ †,‚œáÁÎÉX~«Ùc‚{-¡­1ùâöÂÖ zl%ØXþüùñ3TˆE@@B&ìÐqÅâsÉŽ¿ã€„gQIžvW›bÎǵ³ôiG$ ÊϤÌDf/™—xÑaPò§K$6ß"0Ë O—Ø*m(#@H*=þ•]UR;Áxl¶:‡²HÏFuìek(®qvØE¢óŒ@v?©O¡(!€ !jð–Idö ÙŸÎà-_KS|GÀ¥»úÎí‘“­¼ÄÄ"N÷³@=æU¢ï°ßI§€¾Ãå§ €«yí,ì f;a!%0†}KÓÏ ¼´2âp›'ä {@ž™G{â‚á—3·Ø&Gh=&µú=ñ .Ø[È2.ᯈÄ\ej9Š€@€„K4H‘"…‘T’ç`P€Ÿ[ìôÁE@PE@PE@Pž"Â?E²ª¨Š€" (Š€" (Š€" (Ѐߍì7tšQPE@PE@PEàiB@à§©µTVE@PE@PE@P¿PØoè4£" (Š€" (Š€" (ŠÀÓ„€*ÀOSk©¬Š€" (Š€" (Š€" (~#  °ßÐiFE@PE@PEÀ'îÝ»·iÓ¦Õ«W_¸pÁ=ÃÁƒ9'Œ“Ýoy¡p¶Pýúõ3gÎܬY3/lzKPlô$ M+Š€" (Š€" (ÁÀ„ êÖ­K¹åÊ•›={¶]º1äÞ¿ÿ…^àœyßÏ?ß¾}{öìÙ¥¨³gÏÆ_Ò¥>|xNª·kÑ´" jÖž (Š€" (Š€" „,h¹RI˜ú0ä¢ýrI‚Ÿ¡šH›6­(½Y²d1ÚoŸ>}bÅŠ#FŒqãÆZ‚2(Ï!ŸÃgÖGVE@PE@Pžv¢FzúôécÇŽ¥L™Ò<ËåË—%mæ–&ETÖn (Š€" (Š€" „-V¬XqþüùB… %L˜wñâÅgΜɓ'O‰%ð”6²®_¿þĉì+Ι3'fdr:tHîîÙ³gÉ’%8T)RD(ǧö£9çË—¯X±bvQ¦LM(Ï6ª?Ûí«O§(Š€" (Š€"ð”!pç΢E‹"ô—_~3fÌvíÚÇéŠ+²8Z´hÜ…­@$ðfðæÍ›‹/nuàƒ—ÄÖJ“&Mÿþý;uêtóæMÃиqã‘#GšKM(Ï ª?' ­©(Š€" (Š€"ð”!0xð`4[[è™3gŽ1¢U«V6Qv{Œz…mËpÛ¶maK–,Yùòå/^¼¸hÑ¢ ¶kÔ´"ðô"ðt(ÀG.í~z!~n%O;ÓsûìúàŠ€" (Š€" <>h¿‰%úî»ïòçÏß¾}û_ý•2±;`©(S¦Lø?÷êÕkذaPºvíúÖ[oE‰…†*Jò˜1cÞxã îÞ¸qc÷n` rú÷ùB@£@?_í­O«(Š€" (Š€"ð´ 9rä?ÿü³zõê©R¥B³±9ü°Å‹Oî²y8C† )R¤à’r„ˆ.}êÔ)ÒlΕ+—õ¯"ð\!  ðsÕÜú°Š€" (Š€" (O x5›“~Qe9ÝÑq`êT«V£†É5cÆ ”äZµj­]»6¨…(¿"ðl   ð³ÑŽúŠ€" (Š€" (a¢1‹pW¯^uHiSEŒ1B„Ž»æí×Ë]Ãæ1ÁAÁl–Ó’nß¾=iÒ$âK0À#³gU€ŸíöÕ§SE@PE@xòd̘Q„àt"ÙŽkd2{qáñ[Å5¥”`ëïÞ½{ÙE\¸pax83‰àÒ°¿ÒgU€ŸÕ–ÕçRE@PE@+`ƒæÓ§O;Öë›o¾‘Kãílß jڨМìÈ‹t:uˆ]®\9nݽ{wË–-½TžyžŽ(ÐÏ|3è*Š€" (Š€" <ÃtªAƒ?þø#Ïȼ6l(Q¢Äµk×&Nœ8gΈ(®Ÿ~úéã#ìñ©»téòå—_Ê­;wîHÌ*B:Ÿ;wÎðCäçú¢9CôȆCu¶lÙîÝ»grAéÙ³' A!4ô•+Wd×qÉ’% 1ÍAÁ†SŠÀ󀀺@?­¬Ï¨(Š€" (Š€"ð„ˆ'ÎÖ­[9Î7f̘F ¿ùòå[²d‰Ñ~¹…³tŒ1HÄŠËpšKCôÈÆiÀƒÆà,ãÇŸ$I’wÞy‡#‘ œ9sí—ˆ\ 6œ2eŠj¿6¼š~NP ðsÒÐOà1Õü@×*E@PEài@àäÉ“ˆ7nºtéÄØëš º˜yQƒÍž^°ëDÚ&zdƒ?gB^ÁŸ5kVs05=z4vìØx>ÛJ¸£j½TžmTëí»kûîÇŽ+U$J”‡'˜‡u‰ÿ/Ÿ*ÀÿGBÿWE@PE@P0€º@‡‰fð"DÏ/ú5ªûÁ¶¶{á ö[K.úíðóçÎ{ÉZ " (Š€" (Š€" (O üÈãmb;«!_b§6_9t4a¢„µêW ùÚ´E@PE@PE@PBµ3ÊïÔl’.Aöý{s¹¡[\ý†µß(_º@ἡ[­Ö¦(Š€" (Š€" (Š@" à`÷Æ›œ,uëæ­`.7t‹û°UÓЭPkSE@PE@PE ÄxÖàÇNîÚ±ûúµYsdN›>ÁïÔÉÓW¯\MŸ1”ƒûmß²3Uš”ðxôU†sÛ–W._É–#K²IM!vâÌé³;¶íºyãæK¹²'Mž„[W._={æìë7H=re6!ì…ŸcÙ>lÝ´MÇ–réß_ïÅ=|¬â«5‡aÁÊYf ÷Wý¿øC‹¶´û¼•¶kÞñÏùK§/˜Èãsé[ÿ„Ô\Š€" (Š€" (Š€" „2ÏŽ<ê»±¿Žôf¥7P #„ð÷²U+–­´ÑD1®Yî­“ÇO¶íô1 íÜY æÍZX·ò»KÖÎ5fÞf Z.œ»˜BÞz¯nôèÑà>hTrõ—¬›9r$)­a­÷ׯÙX¸XÁu«Fˆ~Ѽ%3§Î©R³bõ:U’%Oúó<Úèƒw%Nˆù4KöÌF†¥‹þÚ±uW‘…óÊ{äÐèóg-œ=ýõª–|­Xü„ñ±]÷ë6p`Ÿ¡yòåzõ&cPÞ‹M—!íû-ÞúíˆÎm¿?m4…ïܶkä1)S§hѦ™]—YJ[;—¦E@PE@PE@›<; 0ê%wéÞA¬µ9rekÚâ=è—/]ž¹xJš´© W«]ù½:ÍP_þq¢˜=—/ùí·Äkņÿ4X2bÿÜ¿çÀü9‹&ŽŸÜ Q=ˆ¿Oö‹Ñøçi£#Ft¡G9õß­>SºÄIåΛsñÂe(ÀOΚ#‹bþ¢ýV®QaÐÈ~¸F ñò¯•(]ŒŒrYð•|èÌ[w]0gÑã(Àûñ§Íœ6gÙâü­PåÍ­»bãíÑÿ ܶ´vÂlm~M+Š€" (Š€"à#—v{¹«·0ˆ@ÊØ™Â T*’<ÚáéGæ0•%N¼8ÈcÜ›=ÊÖ{à×¢ýr-´Þ»µI,^°T˜§NœAâݦoÉ¥üEA%±~õF¹œ?{‰†ÍÞíWˆ¯/d”X¡xü‹¹×€¯Œö Oœ¸qsäÌýØÑKð‘h±(ºÝûIi_uè9bðhTz4s4ÿ€Ê÷Û€ò*]PE@PE@P0‚À³cnòQù3tëÜûÏK6}«ô›¯º¸Š+¦{ÖþÉÄvâÁý‡ILþuÚ‚?\Z®üÎ=O‚Cqåòà¾C$2gõg(CÆtÑcD—rì¿—/]Yµb aº.œ»pêänݹsÇfð/í½Ø¯Eé1yV÷.}ÍÕµgG/µø‚­—ìzKPE@PE@P°€À³£ã~°6Û<þ¥ÙdÛíóÞ?ú…c“Øc+v¬»wïQ”Ù|¢Å²o˜*òäË a|/uù­—Òô–" (Š€" (Š€" (OgG>öýþ2ýGÎòÍpü™ß­ÕtñÚ¹£ Ùóç\ÖݤÉ\‡ñã cGŽÙ7Wž—„âþ—PUØ„Ï9ç¥X÷\QõýnäÐ1xPwë×%C¦ô°íغ³LÑʶ›t@y½Ð})–x×Ý;÷ÆHΩKK.#æ³÷]ÇAÅÖ‹xzKPE@PE@P'‚À³³ØÀ‡&9`xŸòUÊr0/®Å†NâÔ‰Óöåªå®»Õ+DŒÆ$vmó•!Uêðpl¯dqÿ+ºëõ§»ßuPæÍ^…èS¢ý:îú}éK±Cûß»{ÿ{Ítïÿ}ÞöK9ÁØ{¥^°õžQï*Š€" (Š€" (Š€"ðÄxvà­›·ÛhF}Ð8vœØ6±ÍGÖ¬\'¶Èb}%]³^U¡T©YÄ÷ƒ~à]¡Èß[·0—Þ–t¹ÊeIŒ2ÚðÜ¿Ÿ8R¢Ks+A—/ñö­;…ßûßÛ·]{}eï1 B1ÿôÃ/Þ³ør7Ðb÷ìrÉ1b·nß¼LùÒØ~9¸¯Áî ¶åUº" (Š€" (Š€"ð8ܽ{wíªõ»vìyœB4¯" <#.Ðè¨åJT-T´Àå^çwãºÍS&ÌH•&E¾yì–Ž)RÝJï–,],Cæô¼ïl/eÁV,<%K/]¶'!•-Z¹jÍŠiÒ¥¾zõÚ–MÛˆ‰ÕgPwa«Z«âðÁ£vmß]¾Dµ²^!ü ‹ç/EŸü~ì )$oÁ<œ-Ô£Kß}»÷_¹|¥ñ‡ïºŸ‡dDz½l©Ý;ötùô«ÛkLj}ÆäÙhÎÄ—~Ì=ÀÞ‹¥ðÏZ~Nœ­ÎÝÛLja¾ìÓyÅÒ•? ý±jÍJbÄ“„Ø:ré¥" (Š€" (Š€"ðøܼq³j™:§OžÁ씊ÂñA:â‹Ah)ïÅþ>eöºÕŠ–,Lh‘9uš”-Ú}دۀ]úŒŸ6¢]©ØJQ>þ}¹æÀû÷ÿõ‘YÙE@PE „þ… “>¡ÂµXE XXúçò]Û÷lÚ·ŠÒr¥+ÈL»hÉW‚¥d-äùDà…Ç46†j>ž–Ž ñÁý‡nݼ"UòX±ÿsⱈó4mþ„¼òœ=sîìé³é2¦Å üDº:~ôçQ”ÇE&<1öï=øâ‹1»Ç¬ºxá"ûÉk4ðkW¯EŽÙ>=ØTÍ-<“¥H3–Ë{óæ­Â˜ŠˆmÊ1¹MT,-Îîhêrˆ}õÊÕH‘Ä…‰{¥^° H/Ç…ç®>`Ó”V’±õÔ²uó¶)úá ¼ã×ZйìXS¬2(PÚô½·@qPô{AÞÏ¢ýE–:}œÚ…Ž0ZKGøíj&ÍA¨êu&Îü©p±‚¡/³—™mè £5>A°+>N5¡“ólúŒé­ ›­÷S(?jþy) U6S– 1ĉ‡ö]/,·8iÉ0G‰Ù¤IPQÄþ4S@Å¢÷:V¤:q‡–´{¥>bkK®iE@PE@P§¶¿-ûsyÊTɳdÏ„ǯ³zÊu+¿!b„&Í>í7,à 2þhVÁU·–TV¯X{ãÆ /¹Pbñ£ö ·E@PE@P‚ŠÀî{ïß»çQ!ܾegä(‘|±ÁµÒ'Å¿sû®Fu?(^ªˆìŒ ªøZ:p8}¦tqþŒÖ”sðÀá ç.¼”;;Cô’À³×ÀnõÖÁõò¥Ü9¼pê-EÀ|êv¾¤"ð¼(À_õéüÁÇMræyŠPnÙüpó–·‰¡¤}lxeSB }ûöË—/ïÞ½û¡óYÎ;wêÔ).ιz*ééSýëØ@PQA¢Ÿ?~äÈ‘.Ÿ ÷0ä\rùê¹›ÍÞo“#ǘ-[¶” ý¡fÍš“'O†bµr„«ïóE—fz饗ʕ+gž|éÒ¥ÕªUã”ßæº|ùr´hÑÌ]M€SäÞz;psG¦k×®£Gþ|@‹HçuxöäùÑíæ­[Ûtòïÿûû„? æÝ¡C÷z•¢(Š@0"À¡¡(Àœ«B<÷=€Ë—¬Dû-P8¯ïÚ/åi¿ýÎ܃Ää ÓUÆÆz>‹z^àTiRòïioã‚Eò?í ò‡qvn݃2óÚk¯¹ÌDÿW€säÈqòäI‘|\¸Ñ¢5Í”)SþüùK½•3gÖ¬YèÞ1FéØ4Kõ³¨¥óÿFûåÁ¿ýöۻ縴ÕÏ‚ž¹lÒL©S§>xð y¸?þí·A‹ê/¦¾–+R¹È‘ÿÈÀ°=ωå‹VÕ¾¯·Ð$uî1bEG»V84a׺猙Ós¬&žáèÀæÌÅìé®\³¢¡šàà#‡ŽÖ¬W•ˆ¶kþ^GìXÍšK€x^à€ž_銀"`#ðûĹ\Ö©SÇ&âÝÊ%óæE{&¦Œšýìᢕ6mÚôëÄh¯7ÎîáAÚvÒ¯¼òJåÊ•ÏÅÜáOæàγa•k+A»víJ—.}/ùÉð‡Îw OkyÒLü5pñâÅÍ›7§M›¶ÞUáÞ¢l sKœy³Ñ½mÜÌ­ÐIDx1|É2EfMš¿lÙ²âÅ‹‡N¥Z‹"ðø¸üù÷Ú½cO’d‰3gËìPùÇŽß½sÏÝ»÷0~p&%lr‘?eêB„mç¶Ý<G–ܹsgÃÚÍW._y9®xñÆpEŒµ+×'Lœ {Îl>âfWçžÞ»{Ä ™Ò›[ ²mËNêÍ–#K²I D•Q€9zÓ¡#ê¼Ù Ùú[¾r›ß{zÚÄßa¨Z«RÔhÑ~úáçi“f¶hÓÌ{½«xA@`/àè-EàùB€yÆ’¹+8‚ wV÷'Ç9óÆì uó¶à̪[·n}ú駃 šÖÿï¿«ÿ,Óú 2LŸ>ã=Ü«}Ê™“.7EЇóŒÐ ÌÖ(Íd‹wìØ1ÅÊÄc:ešäto·BøzÅ’(ÀU5̵¢ÇD`ãºÍ-røàQ)U¶s÷öï4ñ…s(?û¸ó¿Ïw¯š=ºOÁ€ó×±¿Íž1¯ÉGï–x­˜{Æ R×ûpýšŽ\ þžiùá¸k_nÙ¼­b©Å_-:~ê(CGÕ|­`9´ß› QØš}ܘq?jØúÒÅKÐ9q³×€¯ª×©2|Ш¾Ý¾½}ûDŽ>=áûÜyËiëëN½F“!Sº?׸ì·ìãíٵߨa­|9î¡£¿5« •ª—ïѵïÒEáílþ|ÉßPJ½^Ü(êò8^þR׬é$L”à•â…ØèR€'ÎPØ bz+PÂÊ¡ Š€"ðœ prÏ¥sgÎãÿ/ÞÃõã€×ÖlT†>úˆ¸ 6'Š4|˜îïÚ²ÿÎíÿÜ‚íìésÛ·o'½jñ†«Ž\¿~K³ÿùçŸkîrÇȼÿ~»XI_8~íðþ‡“!¡Üº~篿þÂ5÷È‘#îüB9|øðï¿ÿ¾mÛ6d ˆGèW®\¡ vlr‰ã7ræäC_ð'NˆäwïÜÿkáÊ™3gŠä¦ÀãÇÿñÇ ,¸vÁ¹Wÿþý1“b,…™õïcÛ/,_´JLë’ýÚśӦM[¹råÍ›7Mî jܸq£ˆ{·¼yí¶Í÷ö½][÷ʾ\Cg/7âÁ– Q<æžuǦL™Â#;nÙ—ÒL²'ú‰£§vïÞMÂõPOž;vÙ=R}@2ó ·®¹&gæwûÖí{Ï9d>wæB@2ó7žùó¿¼ËLÿ|v&SÑíÛwömlß½KŸ2å_ÒeK“ùÆ­_ê óÝ€h¿iÒ¥þeú’±Yƒ–h¿oVzãçéc¦/˜ˆÎjErõ9?I0x%C.ö^»®YÓ]ŽfU‚âÿüç¼¥|” ©E,è¼òàü¼w÷þ6éÖ$WM µ /åVžaNìv©g&ll OZ«q…™æá Se©R¥„ëÆu_mftW/[%Ù”Öã³oW,^³wïÞJ•*‰>™?ΈV­Zmذ}Å©sÇoüZ8”(ÒwïÞ÷瓑ľ}ûƶX¾0Õ¾‡rÉ<þûžã§›×=ܯÂV¸pá¬ï¸¼ÝÌïòÅ+£?×eËOBÉ•+W†ªÿa0œ’ úרÞË%]¿~}Q£Eé0¥‰&MšÌž=Éiû÷ù£  °CÉIœÜ Ã(ؤå·6ÿÙi¿ÍL“&\¢×å.Ÿ³9>Õx˜£ãM ·æó¨=‡þöÛoO={ÌÀÉ÷îü3»¯Qc$—ûßÅ‹W¨P´s5†´GˆóÕ´Ý«?ʱ~ÆÁ!>ìÝû •B½réÚä>uYœD8˜e|%qÕ\Ê6³¢>ø€ÐßL­Æ…sÙÞzë­qãÆ §ã¯4S™2eæÎ{÷Ö½*Eß‚‡¯*·ŠtªKÃ>ùä;—‘ù³¡ïºÈœ¥xÒw-oÜI£çŒ<;ÍõDæ .ü1àŸÕ–\È\¼lÁbM]‡½‹Ì£FB±œnC‡p_{‘y÷¶}ïVz󫯾¢6nuéüå±á\² P‚IÚwôÜ~ÃZÌ:søgè¦}÷ìØÿ^ùÿ´ïªï‹ÿôÓOÒ¾›æ>éÃÕm2dˆ)ć~8~üx’Ñ¡!¿[©¬àfx¸K¥²äñ{ê`â>»¥“öÙgß  ]\MÃNþÁ×®i~„©›ÜeÍ€mó„B'¯×êQˆ2Ã1b„Ì™3ÓPöþmsWŠ@XC Ûç½xÓÇNiÙ82/â7‹WØ{h·kx<>Óâœó²çÌÚý›®dÉ™;Gêt©k–«ÏöÑÃûxyÀü…òØw0ÏƒŠ¼°q«z™ºá^xÁæ¹ÿž}IºL…×ù'D¬ ¼×?z×áÏìÈâ÷%~Ñï·lÔé+×8Oh¨S'N£ÉOúe«o5¬±F½ª¥ò¿ÉÓ±uÖ¿ãsûyj¯/¾AûýmÖ8Ñ)+.›{±–³*!’ÓFû÷˜?gÑÄñ“4ª'D´\*9u{w…ÂRéüÙ ±â–)_Z(¾üúÛ ØÄ•šA¥jåQÈqЦ}}É®<Š€;jvÇD)ŠÀsŠÀé}.™ï~¿1cGO›Ë¥PqV@vrÏŦ5Zcçüþûïׯ_ÿùÀ–qÇhÞ¼9jžÓ5jà:[³Qù2§KçRiä'‹ÜQ£F­X±"SŸ¿æ­ùÿ×ÿ'Näï«åŠ ‘BÐ~³I……3ZÓêÕ«'u^ƒ!Q˜v´¯×Á-§pÒF†N:¡*Lï¾Aîzü[«V­¢oeJ>%w›5kÖ»wïfþãnG¥WÏßzûýZ¨7"ùÎÍ{‡·š½cÇžóšdª\ñ7­Ýš/_>T8»,xO†^úƒìj¾žß´iÓ† þÐoBÖ©ðMå©1®Ö®]ûþ=Ï–jT_À!L÷­ëwMÉ<>é½ëŽ£ "ÇÞ._¾¼Pº|ðÍ?‹ýÇD%£ŠÝ+N®]árÒ0`ñ·_)ïýåÐoûõëç°Áš2MBô´ˆ‘#´ùâÃ-\›~Ó§OÿÞ'µ_ïeS£a62_»zÃEæƒÏÚ2¯Yº Sªþ®¿Nä-œËȼôUû7€Gd®ZµjÝÞ…~šõ/2ÿöÛoÕ«WOŸ%UõvEY/`¡ Ö¡«Sr·Vƒ.Ÿ½N_2íËÂPÃÊ-Lû¶úªQú—“ât`Ú—¥2Nš4‰KB~¨µ3fÌÀW‚§ø? ‹Î£fÅàÏÚôÛÒf/úZA‚ÀýúëÃ¥“…þÆÃf(”xðø^¦“·mÐ ¿ðP«*G·]xéål¦“þ~?S‚·Û¬OÙ·4­„)PY׬\†#Ú¯ÈF\%ü`O<}ôÈqÒ¼ï¸{'7 púÔ±la4­„58ŒH,§¶oÕÙ–-â‹®‰+º\š´©lº¤“§HFbÙâå¸NõŠôÎm»ðÂMŸñÑ2«{® Q¶Ùà~ðKiò»‚zÖ®EGþöðuÔh®PHübÆŒaï£uŒÿãÄ ÏåËWlN_Òû÷üàÝá7u”Ø~%×Áý®Ö™üë´,2ås‹´­flj§déb¨Ð8¥×}Çå·â‡ÿóìésiAvcû•º²dÏœ)kFÖþZ¼¢diˉȈ¢ E 0ÂÆ ÷CŒK.Û°ÖeîП"F¸yõ’IŽÃ5øtéñ«<¾ãb–—2Š&•$]Ü$I’°9öÐ!×¼ùá퉪e.ÝXJÙ¯ˆÚvñôU¹‹rËNΩc¦ÏìZÝÝJõꃪ@âŸõÛåšDò™Eû%ݯB»Ür׿¿Hž(Ý#å©¶®ß;at ¹¦À¿ðv3×Ç+Ÿ!’[~óÍ7!¢ê—¯ýª°±T/ÊüåÓ×íŒvš`Â\Š—ÆŒ'EßpM¿Ø,œ.ýðßpÅßp­úóCUão¡ÊYì5ˆ”/ÅO(>ôøñ]Æv(Áø™ÅÀK±"3¶MÒ+—<4ò#3 [ÖB'µ"sîr©pÜ5’ä*˜-f<—Ý@dÆèjnš(X° èœÂ‰=‹×®]c?¶·a«šI3¸‘íK¯Nœ,¡Ý¾áÿ NÚ¦}Ë>X”É(i”Ûÿ—ôŸÿ/œºŠ‹DÒ¤IYd17P\±Ì›KÒÉñ,°‰ÒÉï8-Déä9ˤ4Q^éäºæ¬î?y»8à~K)Š@˜Bàı“ÈsñÂ¥õk6ÙÿbÅŽE0§ (´ñÄ«PåMlŒµ+4`«ðÏ?NlTïCÊiøþÛ¡ÿt#ÞöÏŽu«{`€ øˆP‘Ñ¥RD_ä!æÖK¹²£ÃøƒÍ⸫uvlÝe7 Z1Q¾J>0AfÙëûûÔ9P\ñŸg-Œ'6Z±a4A¼+xXÈèØº«ù'¹¦> h Ê ¸# `wL‚BȾÝ;÷>x„µ·ÜùrʼnÛ½eÓÖ5š/Udü´Ñ†Öç FsîBŠT)̼ÊHxåòUùÒgJçX5 šxˆDLÊ}þÒ™k0˜¿lŽå/{nßÿ})áÈ…=g®%¬4—̹ñ’:Q?LÚc‚Åûš5kŽ1bë²Cá¸XÄüË®Qá—êNÿëFø G§=¬Q"?âú6ó­;YÆGú Ä£¸*ÇüÀÅíÛÉœp}Ëå'! ’fˆGŒ“ÿÓ\ÿgΞ‘¿`ûhÅŠõHsæn‚ øë J2ÛÃû‡K-³™ƒΊ}_b.q?þ{áº9sæ|ùå—0“ào‰ÿ+ÀX­¹Lœ&®]ŽI·nÝzêÔ©#ûþšv~’—nNÇ Ûñ,†Óï„ȼö¯ÍE›¦¡‘¹W¯^S¦NaC¸+2gy%¥\ŠÌWåÒñWdnÛ¶mŠñ’¶X‘·Ñ«Ê;ö£XJË™3'.ßWÎÝ´ ÏôRº«áŠ´o–å£<ÃcÚ÷J¥†öŒÃ† ƒ~çÖ]¬ô¬à°ŽcвO¹–urçÎ-Ss+zôè¤ÿoçxøNá#°ãⶃ $ŠéZБN~ñäÃ…!éä‰Óÿ§kE‰ùA9/˜’%‘,™Ë>ö`p.Ç-½TÂboÌWðåá»`¨¾kW¯O&åž]{Q–ȈËô7ßõz£Ük¾,œÝÔ¿×`rœ«ý+ðþ¿÷ýËŒ¹^Œqàð¾e‹Uf5!oÁ<—–“$MÌASGöÍ•ç%ïÕáíŒõ{å_« ĵíŸí„ª®÷n-™xÏ(w™jâ Ozñ‚eîü„׺võZô®aSŠ@P pàò•ùæ›}» ȾP•×k·lÒ]7oÆ"ƒú¹&FòÛ¼q+‰¹²ÿŸæþgh+µø«ÊÕ®àaÝ´ã']ׯÞæäVV‘["ûX †,83eryÒJ`ab°SQ~Øoï8ƒ (Æ·,Y²øX¦aëÙÖe…‚I m!K±‡ °Tw`×aªø…+ÑŠñ}¥äC횸¾äÝ¥‡ÐOĈË¥oØ¿ÈQ"avfÁûêÕ‡ŠŠ}WÒ¢ü8Tñ‡ÄpNÕÅdǾž×/Ý޹ťÖâÝ•,Uâ”ÙbTd 1a¬©Qb¾ÈÙ³’‹ Ø$¢Æt )wñ&0RžÂÙl>É®ZZjÅŠr+¸þŠÌÏ]>¾çeŠÌ,ˆ$ˇ§02ÇŠ#UÖ„R©ÈÌSx”Adví}Ýzþ³÷¿ôOfQ‰ïÜ¼ç± !JûÆŽûõ’[Q¢D±Û7Q’%K–ĵ'sîî^s Û2+8â„é^þÕó7 ämø¥v¢£ŸØuqˆíÒÏéäi2¦È˜/¹°I'ì['—·Û¡Ì›ê4¡„R?ðpÆŠ`‡ T<Ý! Tǯ>]¿ûïE«ç¬Þ¶tÝ®å&S Ùƒ‘£˜ÈüE¯N¸ûWì¶vÚÏ~ü˜ës澯Á¿Â}ÏÅJÄàQýùNuhݕӉ%£´Î®m/½”†öK¼+dÎŒ¹ýŸkTôÂï¸Åy¿Pš|ÔpÿÙmŽl¿qýÆÜY YôRðU€}A)Èê0¸ß°¤É“~Þí³!£û·éØ2Yò$ëV=R·lÚF¡„4 rÑ¡• lº 6âÚs£Ú(ð­î ëåS@äèöV|˜ãéΞ:hëiˆ/½äZNžÜ5#Ï•7;óuù}?£góa•H£_ɳZ**Z´h¢dñQ™þL€e~è1âG‘»Rݧ}>*þ_ç?o[¤a‹zÂ#ÖÔWnËeHüÅÁ›b¯_ºå(üê•kĸÂènxtpúq)ÅËÿ\)uHöfÊŸ‚¹[O‰F¬¯tù='Q¢DÔÂIKÕ…;zÏQí[Œ¨LX& $ G@Äì]dF3´eNórJ32,™'|„‡_%‘=? êKrƒEØîŸÌbJ5=ÊcEÒ¾Ï_rÜåP%GûJÌð &À¹õ¯ƒü Èÿ™[²h°1é䋦Wÿ2o„éä¼Y%êæ¤~ÒÉeÿ‚P¼ü:PÅÛK zK°âÞéèácPä¨4Cá,œ¯:ö\³rPÄãfØÀ‘³¦ÍÁM÷ØÑãI¾yÓ9>ývø·½³@iÊAsîòé×Ĭ2”ÇILøi6OâQIàbGQ8vhÕÝqyáü…µÿŸ7îß{à­jïÁÀ—ÅÁæß¥2Há¸p·nßÓNÓ·[ø†êûA?¡Ê€3øÙÒâ»òÜ™ 'MÄÙH†!P1õ s¥êåØ¿ãøUy H ƒ)ÐN8ú·|¡Ø%húF@ààoÜe‹Wó€u,›Ö´ù{„kÿøÓ¯›ÛwHwSÙÖÍ.8GV€ÿy ¢Ë8å8ñôYÿ‹Çi1¨‰§èq\B™"û"ÿOƒ§жnݺÄm‚_<œ÷ï9d/ZûRN@<,9—*ÿ wQ*ÄÿÙÖ(¤ºƒ»”zªT® ¥Gvž±y®œsN†ì»AM‹TaÏ¥(„(ÐüõýL© ÕË Rð¯øsµÄR–mÙò» p$_âô›2Ó¦MKúØî³†â1‘0el#c·dS÷Ò¥K=òøM™w¯=jËœöe—½×È\øµ¼¦|‘ùÔÞÿÌ®Ì]“ˆ—"Æß~æ‹Ì§OŸæ`-“‘Űq;±·à4Ò¾îåh_Ék·/ñ®0 ³uùÒ…+»×%*+8¦:G"v¢èPˆ^f{ÈCq¼}R;gb;²Û—ÒÉå 3C?sê¼IÛ )_´z›®iE "еgG|eñdnöNK¡EcÄ·®B©UËÔ1Ò²Ïö‡ï~òÍp¡pTì«o”`Õ¾u³ÏÞªúÇ—.T>K²ÜÍ}‚w´ð Löþ²ÿ·½†°A×”Ó¿çàGŒ3ü'CyœÄ€>CÉ~÷ÎÝÖÍ>5ÿ°+H™Ë—¬ds2bZEŠ >jØGÂ7‹Wå%‚?áBÜõ³n(ðæõÎà» RNËv²k¹_Ûæ zŠã…9Œ·lÑʽ¿üfâ¸Éœ ÜêýO_ÎXxÁœ?UµHÜxq‰zƒðIJ²÷}xcóÆ-ûöì'†v®—®÷Ù%ËâÂò¥+‰ nÓMÚÑ7 ûB1Ù5ñl#  pð·ïêk)”—ÜŽÂÇÂUâ$.# ?VÈvmß+vL8™T­_³qÞ¬vÜqôñrh`.sP|¼,UÁ¥ãüŒŒvŽa2Jô N޽qå? íÝ;÷‰Ù+lfiíìÝF³™ÝÏ|.¸ɰ~§Ë’ ã*‰l7¯ÝÙß5‘r?eÇðsïîC‡gÂ/ÓÕ‘ŠNk*uOо¼çÏ^´Û÷ÆÕÛ]»º¶Úí‹©Ÿ­ÎD~òÕwnÝà ìpn· g­`×êÍAÄtH"„‹µ‰-¼oß¾/î9wï<|j˼#Æ4D'ïÞj0Õ¹wrQ€9(ØFÓŠ@ØD€coæ,Ê9FDNúºS¯O[~ŽoŒ5ý¨¡8]×ê^Æ,é…rèÀÂ2qfϸ)?ðoäø!¾h›%{&ŽÌÁè*< %Œ3:Çð$K‘Ô”“"¥k1ùƒ¿†èH¼éEÜjˆç »háÃÛûZeïÃ_Kþž2a†ù·}ËNÉxïÁ¹Áé(Çq™5Gæ ãÏœ6/_ΚòÇ/]{tˆ+ƘáãÆúæ#ºvˆ •‘Ù&—œüd…Çæ TGá<à ú¹L;¿ÏgÊJá#Æi×¹õõk7†~;¢]‹N_vè1ÎÂ’¯Ï_8¯]5iDªVÛµh zÕk?ÜE,<ÞÅàô#ت֪,ÌŽ¿I“')R¢0ß…%ž¶Ãìè>Rµè峊€ë=Ñ_ð"pýÚu $|T@Årˆ†ˆ—óçY¼`éçm¿Õ—q¡÷ nµêW3¹öîÞÇš¥1 ÑîóVµ+6`/Dçîí…­ÍGíYl[³c߆“g ‘AsÜ”Qö‘ëĦç(öI?O5…Sƒc«¡Ø ÄÛ±u'Á\2fNΫ¨ü-[¨H~\AÐ6‹•zÿg÷ d0s¸†µ›µèÇí>$&Ö¸Q¿àFb`´âÊ¥k±×¢öÛ5(pó†-¿Žý­AÆ”f*MJè'Ÿb¤H“.Õ°?˜~hÿá_öþ\ª&˜3±¸8üv£zoV|[.</[Õ½sïo¿ïã.”=;÷b¦.X$é UßÄShúäYàÆa[ …(ñ©C€sq˜îcƒ¢“˜í£<~žl>DÕ$+Îͬ™³•*U •)Y¡×Yd?fÒLq~š5tX·ñÓ§O—ÓY¸›#GÛP&§8Ú3)A(":?ŸyËfœýÝkh3$YºÛ¸m½ßFÿÎq¬BACN“'Aî|WgøB÷ý©SÇæÝölÙÓ­[7¶>¾ûî»/Û=¥í?E'z(L$YDwòd~"¡»ä„j: Üº±§€N"Š/òGí îñÈ.Ïå¨TŠrå2|D§‘Áˆ!‰Œ…oœyˆÙVGs å˜K Ö…gè´æ7ã;ñiÏÝ«Ž‹%“¨TEÞÊX²L‘È‘#£‰ :tÉ’%ø—£Qs®oŒ¦J¦î2»Sl~;ÍYMSœƒyÓ–P€aCfl–†™q3.X!Ëþ5çŒÌï}R+Õ+ˆìy‹w™¥Ì|ùòqÊî;ï½½b >,Û±–2„•‚S*Àò ö©K’1UÎø£§üÅÓ¾±DëÝ»7[#­$X€ Ì2‡~¥È’@ö†s²Ûë.Þ:€I¶ŒÙÒsŒ0ÛÝQ€å‘¼trŽÎúºçc¿›06œkƒ1<_±œ©²':9k yKg>¼õ˜éäešämðF »:˜ï?ºÿþŒ3âž½s‡*À‚Ÿþ} àÝd-> Aö S›q¢xçúõ •)R%{påúÃ[cÒ’@só®ýÂæåØ ‡^ç(ܾÄ=UœùXAk+¬Í`Òì¹eD«4PX0ÝB> 6ÑGÜ §vÇ’`,áŸ) „{Æ@Å Ql±ÝKö(Íf÷ ¡ûB±KÐô³Š€sxVŸ34Ÿ«L…×ËV|Oãæï}2oæÂÎ=Ú/Þ@¼‹±© ­•[5êUýmüd|›ï¢DusZðÚo§¯>}¿e#. =ÿê%Õý´Ñ<áGï…‚›Çì%SqM!4Y’:•Þ1[M ´hÔ†OBÏ_Õ·6—üxÿQ€ñ’K÷¿ctår)ÀD/@ž;s~÷o¾Ñ_,Àa9„µû)Ţǜ+_v6¯rL«l+•\œ¾+‰ÖSËÖÍÛ¦@ê×årÞŽñîÅrH/-ì‰K´lÏôÃwþé^Ýå¬e~=¾ëœ3qQwý ·L¬[þpšL VÌ2±ÏŸ|ÍY¾¦(&1u߯”¾lÔfyûáΊ×.3û6ÓËÅOôèc+NÌÝK·-ñ=–m¢USOñÛ¢QÒ<| Sšè1ôó—9…äÔY¼ïüçS,›?Y†øk×ÎbU‹§fƒe·¥õëæ­ÍœÃð$I§3¦Z¡ã¼Ên[ \°ëÉFÄy°]¢ zû±Ö°tÇÌÜÉŠÇŒùÈ‚Áðúõë¬_P溩ãìüÑcF«Ù¾x‡Ò£8ׇÙ$óO¦½ ðp:.†Ð1óúo<¸¬GýñÞ£vI3ÙÂaBDºF)]´Ç¥žyùeÍ•áóéõÛ•fË\¸d~#³£G!É›­sÕÌÕ*έT"óü?¯9äò¾™AûëÙ ß{ýÓW_ú[]@2Üë§Eæ,ŸÜ©òpÛsAø¥}±!¯9´ÏQÇ ­]»VÚwûåe;/­ú´ì§.Q8 ^=cý¨-g—;îfÊ–Þ½°Ã«l¯%ï½WªC×®á¢x ü}t†<&…ÐÉég²,+›¦QÂp饓Óa <œŠ\µs¾ªÙ›'ú7ƒtrl9²J§²£m1Î^Ê¥½À¾«iEà@à•â…ݼQ›zïÔÂŽ×gçöÝìæÑZ´ù ,<à ¾ßq¸nÇ/Û>Aa‚ <~ã 6„Výp[ž (ÏRÕ߀fˌͯ,·pîbûéD½$„€h¿Üb¢|ÿþ¿ìÙÀ?„KB¬ù{]¦¬›4oh2+U³ riÓ§¢”ÃØà¾íºÜ5Ø«–¯a­‘-4FûÅC{ìH—Á¡p±‚RŽû_‰€õRn—ÙîË6¼% \“'¨Óì]q_Ìs/G)Oe*¿ŠÌqêq„§ócK“1e”è®.mÿ°°¹k¿Âݶ³p8ª»ök3$L˜¯NÂ&‰Fgß’4Ù9)ÇÜåEsÇYx­Ü…d.åN´3¢⌊6bMš¼”`.%5ÏH%Fˆ6—œ´dk’ÂÓz°çc´•s2ò¼)Ó&EK÷®ýJîB¹?šG™_Œ!è2GHæÌ™3'L+F,W@)ìÖã¶/K*^êIÖlgHÃ쎛ÜJ“&MÔX^w|„-nü8Þ;96(»“GŒÁÑÉÎráD­3"iBxÆàè†ï¿}î칞_ôãÊzU#:NÜØ#Z®r™°ð°å*—ýyÚhwãjhÊdàyÈ¡‰¼ÖvpÎÆÂŽdOµ$L¤°î²é¿íG×®ZߤþGs—ÏÈœ5#…ϰH˜}¼¤:ŠÅŒ2žò«kîÐöÌ·dN<#,³!þ³q lÄÁ36a.¯^uíûŠûáú·Ÿ§py÷î=â(^ºt™ƒÈ7¬Ùˆ{s¥êå[}æ2>{ü‰Ú[¹fbÇãq[âW›[³+ñéE \õÒ£ü2mÚ´ï¾ûîé} •\Pœ?zuß®ƒxv`±wÜÒKEà™A€Eöqð‰c'N<-ZÔ”©“û}oHÀR£nàî*Ùrd™4çgà (ÓB¨j»Ø0"†-’¦ŸÔ‚mý?öÎÎÆêãÍ;-(E¥M!k¨$*B”²d©¤Ò¦)e‰B„l•-å¯"Ù·*•ö$²· »lÙgŒ™ùgÇÛ{ï\wfîܹ÷Îï~æsç¼ç=ïyÏù¾ï½÷ýç9ÏÁv:aÖØò•Êá‘8nÌ /P¼qкxæ8'´œœ'&.þ˜²¢]õšÕœû|Á—l:c/ pÃ& œÅÖ­ùÍ’¥®0™ˆ'qøð¢É³ÎØÏ¼ðäW¿Ìû½AØèœÚ4°V¯\Ë^:&µŒ½;ö¡ƒ‡V*´%‰ <ðÛ·o¿{÷nVîÄþ©O"M ¬_²‹¯ñ_|1›ö_ÝÎN˜®…Ÿ“l¬)õëçEàñŒÆ³PˆŸåULD ­dN+±´•g0²Þ·bªÝž2Y—ƒj½©v gE&ÓšU7ü¹‘½——¼ÔYfæ”9lÚ2¤™‹‹5¸zÍëžúü6«Ýx-ïX›™$L¸ÿ…ß%‡/òóE€Á¸Ø¸k¯¯l˳€þÒLþô“Ï̺y máD^¢k×®Ï?ÿ<Þ¼ó×lŒ¼Þ©GÙ@ÉÒ—1/½xñÿÄ’Én*ßuÉë/®^òÖìÖqõWD@D@\dv üæ†?7P)-¦ê“ÆÞ«g2™Æº‹jÅDÌ^ghûqÿû˜õ~É´øŸ»XûûŠ«.7A³Lm£ÿdú<湊œØ£Éë àÿŒÏ³)àÏ»s°-ß8¥ÂÙÓæ¬Ii†"`Y2™@ýFd¿Ô©ìI€è 5jÔ`¤ìÙ}ÓkÌ¿ÎU@³3 õ]D@D ›ä 0rè»_/úÖYãÂy_L›8 EzûÉè '½'¼MáËV:¥ÔÕW±‰ÑØ„}·+³\pïný’c«ÈoÖõ¦˜©gýl„-\—»<×ãð¡Ã÷=ÔÒ„¤'NÎ?D–^üßV±€³‘®´W‰~[úôbñ¢ï°ãÚí;0½«BmŠ€ˆ€ˆ€ˆ€ˆ€ˆ@( t ¯Â»ÃÇb˜EÇ^Sµ:võŠ5?}ÿ ãî½ßèaÖmú»vÕïèÒK.½ØžxÓÆÍ¬Ûά`»Ž\úµ§Mœùăí7oˆÅuþ'Ÿ5oÕtæ”O®._Ú )Û¶õ3·Ö¯ue©’_Ìÿ’PUÔÓùÕS«S6»·1 thÛ©îí·^_ýÚû œ¿þâ›V}I mœ #­ËVüš%ÎkÕ»™µ(i­ÐΣ”' È 4hd¿¡ý†-ùq©qW& C›o@Ž–MYP—31¹÷æêUÿÝŠ°Xìª\µ’m qü7®ÿ›ÅX€·PászôírSí'~8¥êuר2F¦3xìèqsgÎç—é–­›õèÓÙ i ?ßµª{üÿ&N??r0?Ô¶ujê—´ç¼¢çzF_xäÉ-üúXܱº·×¶ÍPBD@D@D@D@D@Â…€p ¯TÍ[ªóÇJ¹Û¶lË›?ßE·[sìÀ«7ÿ’/>çYë6¨½jÓ/„ª²™EÎ-xà ‘ M%k·þê<X8Ec•ei»í[w8p YžKqæÍ—·×€îÝz¿´uóÖÃ‡Žœ{^‘ÓÖ_öç÷L™³±‰k«U¡ ‰‰‰©…¶%•$å{:hˆ´xóþd©^–ÀŽí;¯-]9¦ðÎöðà$.:ëÊÔNtM³¡‰‰¾æ!§v òE@D@D@H ::jéäv¬PU‰€ˆ@ˆðbè ñ«y0þÏåNzV‡ýÖ†Ý%SƒE@D@D@D@D ( tX^ĸâBI‡eOÔh¹@‹t@Ïä߿7lªpM9¦ø´â@VæÃ:§Q]" " " " " "à¹@ûÇ)ÄJ]Pü|þB¬QjŽˆ€ˆ€ˆ€ˆ€ˆ€„4¹@‡ôåQãD@D@D@D@D@D@E@8P$Uˆ€ˆ€ˆ€ˆ€ˆ€ˆ@HéˣƉ€ˆ€ˆ€ˆ€ˆ€ˆ€Š€p Hª& Ò—G à@‘T=" " " " " " !M <Öi„jœˆ€ˆ€ˆ€ˆ€ˆ€ˆ@88®’Ú(" " " " " "aÀF¨ D@D@D@D@D@D@€p8\%µQD@D@D@D@D@D Ã$€3ŒPˆ€ˆ€ˆ€ˆ€ˆ€ˆ€„ àp¸Jj£ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@† Hg¡*1áÐHµ1Ò\ÓlhbbR¤õJý°"µtr»°j²+" % œQ‚:>P¿Ë¦¶OÇ:DD@D@D P*6¨ªTˆ€„ ¹@‡Ë•R;E@D@D@D@D@D@2D@8Cøt°ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@¸—+¥vŠ€ˆ€ˆ€ˆ€ˆ€ˆ€dˆ€p†ðé`p! .WJíÈEξÌ;xÆ ;wî¬P¡BÞ¼y]gY»víÁƒ¯¹æš9r¸výñÇû÷ï¯\¹rTT”sW||üªU«¨ó‚ .àÀ\¹r9÷’Þ¾}ûÊ•+ÿùçŸ /¼ðºë®Ë“'«€6E@D@D@D@D@D Ü È¢W°]»vÕªU=z´«}»wï¾úꫯ½öÚéÓ§»v;v¬R¥JÈWä±Ý…оóÎ; ,È®&MšPg‘"Ežþù#GŽ˜2‡~ä‘G.ºè¢ºuë¶jÕêæ›oF$5ÊÖ „ˆ€ŸvíÚõÙgŸÙ—çQ EýðÃûöíóÜå5'­å½V¢LK@`‹"´·ÞzëìÙ³.\øì³Ï:[öÕW_%&&’3þü»ï¾Û¹‹kÔ,øÌ3Ï4ù‹/nÖ¬¸T©Rµk×¾ôÒK1Ϙ1càÀ–{õêE±Ç|üøñåÊ•{òÉ' .Œ¡øý÷ßï½÷ÈwVv餤¤o¿ý666ÖÙrìÛ  gÙ²e½ëׯoÒ +8‹ÙtLL #fóÀ_~ùåÆ *tË-·/^ܳ ôÌ«T©rñÅÛL«W¯þý÷ßëÔ©“?~gþòåË7oÞ|Çw83÷îÝ»téÒõë×sŠš5k2xáÜûë¯¿ÒøC‡•-[–½ž^ÎÂþ¤Òæ„„h7'.´ÍyꀷÙY9éuëÖAìöÛo·w¾«@Ð6'L˜ÀÐÕçŸ^«V-¯']²dÉ 7ÜЧOŸÎ;{-àÊt•gsÒ¤IO=õT‰%\%³|3”Û–åpÔ"€NÐ+  —¸K?ØuÍãÙ×Ü=H,g>é—_~™]¯¼òŠÉ?zô(v]rzè!g%¤ûõë7dÈŠíÙ³‡gu¢ËÖFŸþÙnfF¢B“Á™Q­³Î_~ùÅ€r¾£xM†È‹‹cñï,ãJÿûï¿”ÁÞÎè€Ý…ì|ë­·œ§3é/¾ø‚2mÚ´qíÂ)ü‘#Gºò¯ºêªsÎ9Ñhò¿xðÁíYH ˜{öìiörAÑxνW\q²ÜUgZ73ØfN‡ÞÃkÀÙ°ªU«¢‡Ù•Im¶}d€ƒÛ>gΜœ¡›ŸU‰¡C‡Ò€¤Öe(€N­€+ßUuÍáŒO¹Š…Âf(·-ø¨ "š‚ðsšW«D@²3¹@ó<Н’%K"opfþñÇí[´hÈÒ¥KoÚ´ áÜÅc7› 40™<‹c]Äóy̘1F!˜|Ò:u2O«Ø{ÉDK#ÃÌ^Þ)€ Ón†i¸¡ÞÿýHû6l˜gw0©ÙåË—§€Ý>£Û¶mÃ9œÁòѨk֬騱#ü=«ª^½z°Û;w!¡1’ƒÑÞ™¿eËL—xž› Û4Ïö±cÇ6oÞ|îܹìâÅÞ½{wãÓÞ¥K—9sæ¼ð LÕfÀM~Þyça vÖ™ŽtFÚÌéh0–mM†ŽA›¡“AƒmݺõwÞao&µÙtó§Ÿ~ªX±"î Œ#¤£ãáx7Þ¸qãš6m‚å¶… .5ID@D@D Ëdgõâ}æ™g¸-Ð?¶xí’ÃTÞçž{Ž×î¹çž‹3ªÉ4F¹‰'Ú2ž z©{&¢ÎsoæåaÈ{ ]ÃÑÔk/œ`go¼‘£œ9¤aH¦W“¯«$›wÝu…ýÓì9s&9èg´4S:í!HGòy79½{÷f“ [À$¾ÿþ{“`ÔƒùÛÖ\ì*–‘Ít·maœÑ“?ÿüÓÙòñú&'óÚLåLng#Æ8;X€•ŒÂÏqÆ©D@D °dFt„èËÌPuš™ƒJ[™€Z¯^= ,°MgnðñãÇo»í¶èèäkŠ6j£-ã™`&!’ÍÌ”Eæ{PË;æth˜«F$$[£}¾|ù˜bM€nŒ–6‹=¶_.9rû÷ïÏÖW_}Õ0‰ë¯¿Þ$h5pe]üܤIÜ6Xûq.`b3/n$ãiŸ¾6s^†`ð¢g¤æòË/w6©ô%'ƒmvÖ陯:yòdãçï¹×™ÃÖu>#6ë:9ÎOЊ+˜??eÊS†–3S€©øÌÄÆ!‚ËÇdi{8 ÜÑüÍ7ߪÁ&Ï t¦0Gášbx³›{ÉY+í»ü¬Y³h$ÛÍQô‹°vÜD­ã>aŠ{ëÖ­‰°Å( s8)׺mÛ¶¸-8Ï‚¹ž‰LE¦ktç^Ó/løxã5€Ó¹ü&~ûí·ÆóÕù{ï½ÿs¸«mdúfhO˜Í~É%—°À«c…³yJ‹€ˆ€ˆ€ˆ@ Hg`&ŽD!T>¥Vó ¥8b•çEV*Bó,kZ`üŸ’!‡õxG‡œ}öÙ¦@j勤 j™‡Z¦nsej…³g>´¹¨žÔò ÁsØ‚I¶¸7lØŠ‹KF¼¨¢E‹’ž«Œöà’¥V?ò2ÖZ?¥¸³ÎËPw Ô(1|¹±Ù¢ZQæK_›9Ðøçßwß}Îs9Ói³³¯iWx0¯eL&âŠÏÁÞl¢U‘ÃÍos¾ O˜àRL¿é¦›^zé% Cr8ÝÌwÊ3,óŒ ³ã”n«² ´(2ûƒ> ‡¿ñÆ<ð€Ýë™8my\6h$>ðæØ¿ÿþ›Ðwl.+r÷²Ë.ûðÃ[¶lyÏ=÷à9‚$.V¬Ú˜‰ö\Üܯ¿þ: {â‰'¨ðá‡ÆSÝ _ à«€YôôŽzO?ý4Ó×Mä.døzáŒíÛ·'T›ùæa¯«m§ehOzPâÌ“ç¼5jÔ°ßi¶IJˆ€ˆ€ˆ€ˆ@ Ö ¬ÚKÀ(ž³MµØsÐK&mŒÀˆ+³Éã5â˜ã̦1dñLl6}¿3Å´[·nL^57z驾ÉàÞ ø\h”èö…r0-O“ 4‡ +ÌâÉL:eΰo?dÔ¶\ãíŒA3/AØDÜ¢ILð†6œÌ¦Ñf]»v5›^ß1ôk0jœXhÈ`¯Å¼f¢~±R2ÊìÅs‡yÄŒ-œŽ6s,7$½3±ÄlUÎDFÚì¬ÇwÚh”-*ËÖsþùçÃß9e3/Ì•e3{Q‰¶¼‘ÊÈW›c¦Í#7ƒ+–qkóÍ7ÍQÄ`GKSsjA°N[ÞU¿ÙDå¢H9®ÈZê·9´ .^!öÎgÊ4°Öš&QÍɽm?ò¦_:t0¸IkãëÅlþïÿãp†ÞÌ&ï˜yMÚÕ¶Ó24å¹-íü ˜`êÔ©¶r%D@2›@~Ž3» ª_D@ÒJ@`çB÷å4Íñ„Êc.n«¦¹F‹"V8yö%>–ÙKx$;vìð§oH_–Dº…Õ“à×_ÍY|¬eêO!RCZ׾бékA°0`Œ%ˆR…û¨Ó誓«†9—òäãvËg£=‚“fÉüXòÑä¼›ëKÂÔf®›^_ˆjjʈå¯TÄ'ÊÊkIÏL,u(Àܹs›]8 [psµ%ÓÑfŽ¥Ù¸˜¡[•3ág›Qn Á›-µ×wß}ç¬6i& Ó_Âtq,.Ä|4˜KÏð‹÷ƒ>ÄàIìÝwßen3"О+7öU—Ÿ3þä„þ2‡Ø’ÎZŽQÇ{Ìd¢~ àì,àJ§µ¼9ó,óH#tÍesh_¨\´·)Œ’§Æ ° FW§s>Ž F¾RŠ5˜ûÇnb¡ãyÎ%39Œ»™„ëÝO†¨ÍÊdÞí¹\jSD@D@D@B@8 3«ó8k&”ÿg¯جdcã?Ó3'û/?‡å‡ ¨8µ¢¢QÚ¬ìç¡\ šÓ¾ðöLwk Mfâ9c!ÇïÔȯµ™«f&osÕЫf$¤²ÁH_ÞÑXçL È fÚ¶×:M&R“X»øš0áÅìã?öQÞîÂË…Ãä È?ýôS9¶@:Ú̱4¯]×b˶N“ð§Í¬–„òñ2a´]5§iÓúâ r—†±€¾}û’f20ï‹)ƒ\ü믿Pe.a‘œq ç’ÑŒ)`÷Ñ ¼|§°ã”ÄšÀò¦*çâÒfÌ™c&AØhḾã­´}™`lv*/u:g“ÐkÌ"6Š—/¼ "ÙÎÎpuʆÎsq"êñÿ+ËuRmŠ€ˆ€ˆ€ˆ€?$€ý¡”ee0ìðbEü]í`Óš2eÊóa€ËeN¤ÊÄfØúÙ~\ @¸brw­Pek ®˜] ‰± 3j“½Öh–àj"$LÄ2vq•ygÜÁVâ#èzþùç1nSfôèÑ>JÚ]5B“ ZÐí¬ŒõÉ'ŸàmëœC›Ž6S¹i6šÊž(µU7Là@IDAT„ï6ƒ‹Û8e¸Æû›]þ:µúO›ÏU@ ÌĆå•WâÙk0ï«Íèݼ<•ªÑÃèäÓžË`rKEÛ]^i-ﵯ™ÆLÍÇXC]ØZí ¿n°a÷z ™N7Ã.|¥àIÒf,†;Š‚çécè<‘gÊ€ÆL¬ÄØu±Y!¥»j$‡9‚ŠÇqvñ@ÏÜ`m;x”4ÖNÌ\(›ïOÂ,l&IúS>»•+ßÒk<ϽöÍCp]ä1‚–ùÖhÁ£+^ëxÛ¢óœ{¬‹ X K†8¯•ÛLñ;µ6Øb&ñÚk¯1š(Ä Ðæp¾øâ‹Î2éh3‡ ¶Yò×Y[jé4µ9µJÒ—ÏUÃÊÍEÁÌŽ'¹¹‚\\ Éᢰ׈U´:Ÿ&ë3lO‡YzL¶9§M0þâÃUÞóð´–÷¬Áwò·mPpõ/"ZqjßÇÚ½ŒÚ0#’8„sû*Ì2´u*!" " " ! Œ™X‰ñMÅЇ;¥•Ræ|Æ¢8bÄv!-\ö,aÔbÍæû™y§æ(žY±§–Mœ™”è\š…çuï‹Y&ö*¬ªF/YRΠƒ‰°åqpuˆ«FÔ+›!Ÿ Àv/— ?d&Ž¢"Ìå3»0NbOÃnÆ|Q繘œÉb< o†ßµ­‡m`hÃÙÚéÕ†Œÿ*rŽfq—h[„³&¯³*“Nk›9Šõ¨i97§ËÑ`éÒ¥¨ýi³g32/gL =zôˆÀ`!GnãÿlÎŽ{03QÏ…É>>XMÓÔ<˜ºo'0s5‰ðQCZËû¨*µ]ÄQÃbïœñ›ZI¯ùà²ù¸ §mHj›obèªV›" " " "AÉž™z…2³’?.Œ™ÅJ¤´ßiN4ÝaBžŠL‚Žgi_YÕ©LU<ˆ›õf×®]‹z!ø0ñ™(ƒù«ò¥DN(c fÛ0œâç‰Õ‹„µ'NDj²ö Q‘Rk†¶`‚.î¦e‹!z‰\ÅuÁ+ÛeKÄ ÇLT|tñ²f=$*GµmÁå j4C\vaœÇ°üÖ[oá9l S?ª†[yÃ26öt&mùñÇ'0íÁ›MjÀçáç\D7mÆ–îÐ<<«‘CDKBa¢÷èȳÏ>Ëå»Í®v¦u“žš¹ÁXÔ9–[—ᘰ®×ªè ÆOÖ²¢ÙfàGhh§Ó‰D@D@D@< Tl:Ä3S9" "Ùä×wëÖ­´ãÌ3ÏÄÀûÎ;ï¤Ö¦W_}5oÞ¼¹råÚ»wojeRË¿ýöÛ9ð÷ßO­€òE@D@D@D@D@D ² H‡ÐõmÙ²eTTԻロ˜˜èÙ¬¯¾újÕªU÷Üs:ÖsïisŽ9Bµ±±±§-©" " " " " "‘$€Cè²^z饵k×þûï¿çÏŸïÙ¬aƑٶm[Ï]&çàÁƒ‹/þä“O6oÞì,sàÀ?þøL&•“f¾±Sc¯]»vÆŒ3gÎ\±b…3ßY‰Ò" " " " " "î$€Cë >òÈ#4ÈÓ zÛ¶mÓ§O¯’òòlñ±cÇ:tèpÖYgÕ¬Y³aÆ_|ñ 7ܰiÓ&S²k×®W^yåO?ýÄæwÞIºdÉ’ß|ó ›Ó¦Mc³téÒ7nÔ¨Qùòå+W®ŒÙóÙ3ç‡~Y¸Á3PÂxYru˜¬Îý³|ùòàŸ=ËûžZ—ããã¹gS—Zå‹€ˆ€ˆ€ˆ€d- à¬åï>;*´pᨯs’øøñã©™ï¾ûî!C†4iÒdáÂ…ßÿ}§N~üñGÄp\\•Üÿýýúõ»ì²ËH·oßžô€кl"›™Ṯ_|ñÅܹs›6mŠªiÓ¦óÔ‘‘Þ½{÷çŸ>|øðY³fmذ@bþô«AƒO<ñ„?%3RfÉ’%\2ŒóþWÂp£·Ývãëÿ*ÉMU©R%n˜Š+îܹ3PÕúSO–÷ÝG#¹”ÕªU9r¤)“Ž+ë¬<ƒ‡;«RZD@D@D@Dà”€^YN wïÞ\¾}ûÒ’ví’$èÕ«—mf%¢Ccà=|ø0™  €ÝûÙgŸ±‰²9$îºë.2Ñ{6óæ›o&{Í!yŸg›Ã‰.ºè"Š‹ËffF¢B“Á™Q­×:ÿý÷ßæÍ›Ó)ç‹Ñ§³ùä“F¦ÚÓÍ›7ïÓO?µ›®ÄÏ?ÿÌé˜W|Þyçq87ƒ×A"Š×ªU omgv4 àž!Mqæ¾rF¬ëÔpÓM7Ùú™²‹q›)èŒì`46K^Û½¾éÙb&‘î¾{½ ¦Íû÷ï烆û /lݺ5ñ«˜À¬ø«®º 80ïÀÕ ç&]fÎ<…¹¾={öä*8÷º®,»ˆXöÜsÏ]sÍ5Ü uêÔ1WdàÀ¤Ù‹>HyWjÏÃ}_eÛ#nBœü/¹ä’fÍš¹¼9|_hOPƒ ŽË3â7ÞàróùrvVi À¡x™X8þüsæÌÁG—y¼41µðWìBððŽ-ØõÂÈé£{<ë×­[wݺuV¢ôûÄ!N¿h5„ø®Õ«W=z”Ø`ž>º„þ¢ï¨/ìÃL¦…¡™Y}î¹ç¢L°Û®Q¹‚‘œÑ¢=ájnCQ†ªW¯Ž\¼öÚk„fðÂéÈÊKLqlŽ9ÂÀ%ÛÖl…JüçŸL‡L™2¥jÕª^OJŒ4ZHÉܹs“ åôZ4çK/½D_б(¢¦1´aÅ"mÃÅ5«aÙS›J=Ã̓^bh€{ƒ9ªÞUŒMÎÅ ,HÚ´„wÒL¨&ú1§ÆªLô5òq>‡c4rºÏ(éVŸ–58_éë;5x½ ¦ÍÈ×^x‹Dg6=æ6wïÞÕÇm€6flÈÙgšO×ýƒ> kÐF2€â,າ\ìöo¾ùf©R¥ZµjÅá\à)R„aä¶äšò2„]‡Ÿö*›1‡â™gžá2щ±38›VöB{‚*Q¢C-cÇŽµýbð…ž2pÀ’ÍTBD@D@D@†@fO2VýþpÁ2åM(fL—3žcccm=® Xæ›-à5‘“›’YÁv¯q™æqßæ`=$ŠÇ™ðtp¢n˜ÙѨS¯íGéÑSFÌ^Ô#›hfga‚`‘II“ÉxDžÖQ#éÍ.&cÒ´ׄ7ÃÔ¼Çí:–M4³6Ÿ2ØiQû(@›éOn–×2…QJø`·xÌÔgžy&Ã+¦€?ôN{ÞÓöÝÔÚUÀžÅWPÆèj6‡KFÄi 8¯¸³=Kî®Ç{Ìdr'çpggšËÁ-„í×f2z…½ÝnúNøÙS¬ÖX˜MU¬ûMÂÞ<þ\h(Fs#ƒ€¨÷ñÇC&­³Ö}wM{E@D@D@D hb‚v&(M˜SZºti,9S\&cRöZÀw¦ÿ=u¶ß\z.¨³rßÚ‚©ï¸¯ó‚wÀ´iÓ?N1gJ‹€ˆ€ˆ€ˆ@¸‰+e¤‹KÀ0ïF<]/¿ürg+)æ*É#iÿþý1T²’°)‰yNµ؉€“ƒŸ$ì8Wcæ@Ô5¶¼xúgê#vQ¦kºê·•„WÂ<£§f¾cpîàÛ™¦N¡Älyô!sŒ™liìl6ÑÈÄc»™ñ„󤞵aêçå)½ÌEôg,iŒ78Zd3¡ Ïs¥)ÇèF&Ó:…+¬ŒÅõ´ôˆ e'cC€Vyž=ã}÷¬Ó3'5þŒ4ùÿIÁÉ™—ÿå]ÍH_O]-Oß…ÆëõÒ˜ÎÍWÇÂ… ÷ìÙãc^´«ÙÚP# W„0<È]ã”kôì³Ï5×iÏ1»¬Í–ÄÁ•ÀK¼°vnÞ¼™‰|h?—í#$јÂÄá˜=ÍDb"úb¦cÂ*B•Âã2z‰§[³×Ö¦ žÚé5"û•Ë‹íG\¢¹¬£þÈEK#0þ¢p~ñÅmfðtõx=u>öF,ü^u£«‘„Júä“O0ÕÚpÙØ6ñpKë&>ØÂäs¯î²§¥çtNíÔï{j5û“?³ 3vÚò4•kỼÛ/ =M÷…ÆÌMÎÂl“'OføŒ`i§í¯ ˆ€ˆ€ˆ€ˆ@hHƒchv bZåR¿¦_dºt,ù<€òòÚqž°™>ŠUÓó(S3/«Ñ8õ-¢—åF‰dŒE¼;÷z=K¸dÒY& âÿ‰!{£m6^Ê<Ð#6zõêe3M¯ílI›ï;aVˆqÎøõ]>“öâƒMËYDÇÖOLf|Ú¦i&ã×p&˜Ì&7†Éd³oæ<ÖGš˜R܇}ôQjeBÏŸ¾§Ö€ æãÏÌà‘ 7BCù¨S*¨w ³îYYŠC° 3SÚ÷í—ñž¦ûB3Á€‘öDc–»â?û¸ÊÚ%" " "âdñ ¤æeˆ€1[=š5~X#—¡€aíÄ^ÊR1x‰ÛÚqý%M\_ B3»–¶Å\ â! ‰†X4ˆnp1]²d ±Žˆÿ”Ú …«†€lÒ ¦|Á›…¯RÖEÛãïTø5ÂÚ[²§O‰~La\ Pq¬'L/2Þ0Ö"~Ñ nŒ[S²ÑÕ,G j¯+ ôüé{Æûâµn!lª¬œDxgÜ.Y‡{°×’&“btIïn¦$pg2]ŸÈa <á°À]ÊUÀ­€a,®£«ªŒ÷4ÝšÖ⑎3MòÇ2ïj¹6E@D@D@D „`ÓK‚L ˜ë.àÝQa>u¸’/Ê. c;މØ,E1ƒ™|¦IãÁkËhÑ¢¦0çbBñœ‘ð:&‚7!‹R«ÁY›I›¥žíBDþœuu×YvE¢ 3>æDœÛqvÀÎOÛè¦3Ó¦‘dfz*5Ó~40ÝDÏÛ΄™jNÇm¦« 69Ý£G¤¸Oó&@øÙ¾éÙbÎD:ú =ÛŒ«<í$F”=K¹rì.“˜0a•S†9ØÜ,˜„Ñ› ùf¯ç)|a})CÍŒ¦µ· kD™(\Pbª-5xîû*{–7s¹±Íö}¡½‚2Ç"õi6-d6²­M p'ÌŸãpg¥ö‹€D ä9ŸæiLï"4›Y6µ}ÐNgN„¯)ú#0Ò.µS3Gš—-ó>zÆ®âÃQHd¡gÄ)âaUƸŠÎ4*Ԝ³¯§&ȳ5ÌzâyRN‡µ–—«6Ì×8åÒ~g›)ó믿bTD‹¢…\‡ØMF ˜@Îìq&_ œ‚Á»×™`/'² 6»œ]p6ifžÃC¢ÃÎ2©Ñs–±é´ö=‘šÚ<ÛìOŽm‰M0ešiØæ®# {xVÈQ;vì €s¤]·"+Éq06J˜’^Oí*{-Oa®£me|\èÔ@qõ ^ª`Ĉlê%"²äç82Щ" áK@8|¯]·\¿¸A¾xD-bþ'ŽÍéŽBäët¡Fà©§žÂÂÌ:á,†jmS{D@ÒM@?ÇéF§E@—€Û‚¾=QËE@¼À®‹[/³O¥~½òQæi `Ff¥4Öc“ú=-+qÀ!~Ô<È(¼²wîÜ™ÑZt|6&€{?1¨[·n¨ë" " " B@8B.¤º!" ™D€ÀÝsæÌɤÊU­ˆ€ˆ€ˆ€“€Ö&mKD@D@D@D@D@D ËHgzXD@D@D@D@D@D ˜$€ƒI[çÈ2ÀY†^'& à`ÒÖ¹D@D@D@D@D@D@²Œ€p–¡×‰E@D@D@D@D@D@‚I@Ë “¶Îu‚@ttTŦC„CD@D@D ðsœ…gשE@D KD%%%eɉuR&¹@“¶Î%" " " " " "e$€³ ½N," " " " " "LÀÁ¤­s‰€ˆ€ˆ€ˆ€ˆ€ˆ€dÁÚ°;6˺¢À¥Eòýœ:¡ˆ€ˆ€ˆ€ˆ€ˆ€¤“€,Àé§ÃD@D@D@D@D@D@‹€px]/µVD@D@D@D@D@D $€Ó N‡‰€ˆ€ˆ€ˆ€ˆ€ˆ€„ àðº^j­ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@: H§œ/Àáu½ÔZtð2Hél…Ÿ’’’|î×NL'•éçÐ D@D “ Hg2`UŸ1HßÄÄDÞye¬&-" " "~¨_^ÑÑѼ§¿)" YM@8«¯@@Ï¿nͪ͛6Ö¸©vî#:Šd²ù7!1ù/—ã¶‹OÊ}r:’ ˜«/â Ï÷€¾ÌAÄ!£¿› -ë–Ô0´¹‘ô."îJ"Ü»’mÚ_­ÆÍ^îñÝ凶m‹ë7íŠ/\0Gþ<'ÖëÚ$ñplb±B§î[0w‚8ˆƒ>ú~0Ÿq‡€ÿn¾7ù;CUï" "´p\ÄT»püøñ5+Û½ëW bemÛºÙd’X½by\\œ³ÌÎÛ8ÙJ‹€ˆ€ˆ€ˆ€ˆ€ˆ@X8eI ën¨ñ^ üøÝâû›6¨wû]#Æ~l  xë׬rUé«ç}½„ÌA}{N›8®Ù½­û eÊìÚ¹£î •H¹dõ9… Ûƒ“°V$œ/sv›cãÊqmRÌ•ãÚT‘4·„8ˆƒç·g޾@Ì'E²ÓG½‹€ˆ@ä޼kêWx|1å^ìÞ{á¼Ù“?ú Ù½T¹î2{v{áàÁ½¼dõK“œKþù tÊ+éøñ® Ç'¦üê£ â N ݺt?è÷âä§ =¿› gð§% C½‹€D àð» oøëóùsm»ù}ªY«Nºå;÷¼¢/õx­kǧ_~áÙÙ_üðÝ׋æÌ˜Â*J÷>ðˆ=E¨_×’¿ÇŽÅ=z466þhLŽè¤øèÑÄØØÄ£GOÝ·*`®Ž8ˆƒ>ú~0Ÿq‡ŒÿnÆæÊ™;&—–ü5÷’ÞE@"ŒÀ)!a‹àîÌ›=?g|ûëW–ræ¤)Ýòþ6Ó&Žÿå§ïG0yÂ,ñ×gаà/ñçZò7Þà£ñGsœÀ±à¸ÿ `H¹Ø±â Î!ݺt?è÷âħ Í¿›q±Çcs%~Ò,ù›RÞD@D rH‡ßµ¼ùÖz îljÛUâÒËíf:hÝ×¾}Ç-× z½'‡?ÕáÅ+K•IG=<ĸ@Û%SVÿMà=1Ùë„Ãvò6 'W=:ã 0ØÅAô¹Ð÷ƒùˆƒ8dðw“ŸÙÄÄZò×ÜHzˆ<ÀáwMK•)w÷=÷¶Ý(ÞÒW—_±|)ÕÖ¾íöÀV~ÚÚÌ’¿àø¡sæH>âб˜i‹Ö8’˜7w´É!3îøñÇ“ ä9%€UÀàqÐçBßæS â‘ßM†¡o¨t ¿È£ÞE@D òHGÞ5=Õ#¦³ìTV*©é“?Bý+~1¢_íÜqê¼/ͱ©@¶ùq5ïüܲä/•jùJCVÄ!àËxjm}ÃhAu­—n¾Z=9‰Kb!q3õéíñߘbzH% ©W6¹_… á}×ÎÎNnߺŹIzïžÝ½»uÊŸ¿À¤9Ÿ·} Åò¥?;úþ6»ŠjÑk_ÆÃŠxÏñññÔŸ˜òwjà™üäœ'‚`©€¹ â ú\¤|cèû!ù£ ïÉ“_ºÒ|? z —MÀ礤dç«à‡ÿ0×Nï" "LÀÁ¤ìs»ð"B8®üí׿þXwyÉ«8ýÚÕ+oݜԶõ»oïžÎ¯öÅܳÿÐ&õj¾Ñ»;óŒÏ;ÿ[&€ NîMY¦å8¶_Ò„»:|ø0§8r$ñH\âáܧnË£GâGç8#á„Vs!ÄAô¹àÐ÷ƒù ˆƒ8¤ûwÅ<šåYN"³¿Ì•Ò»ˆ€d-SJ#kÛ¡³g‚ϼëî–S&|xO£zM[´ÚºeKþ–¯TåhÊ\_sÆÅ‹>›1eBÉ«J?ôØÓäT¼¦jËÖm>þ`Ì«]:óQÀ[el¿¨ß¸¸¸ØØXÞI>t`Ïž=œëàÑdœ+ñÔm¹o_|Ò±±¹O`0WDÄAŸ î}?˜‚8ˆCú~7Q¿(Þ£Çs$ž‘+..*wîÜä8ÇÇ X½‹€ˆ@„8¥4"¬cÙ~œèWÎ\¹Rë]LLNvÅäL~7¯Wúf5Ýy³¦zk`¾üùYñ¨Ë«¯W¯p…­ä•ÎøýëÙo«˜C:uëõÙ¼OXiiéÏ?° ðÉšóߘqS£U8räøàþ};Sü´Å&ÆK<#îÔm¹{ßñcù¢‰çaN¯â û{@Ÿ óAqÐïEºóåÉÜå§?>)WtÎ|±±1YhÞ°;ÖÜÌz¯.-’Çk~ú2O)ô¯£‚Iàѧ:´lýpS;)6[ìªù °½oþ0öÍw6oÚxéå%Ê]¼t£¾¦ÌœE?‘È“7¯=äìs }»üÏØØ£mfÆÿ|èСýû÷''îß½{7§8—w,éŒc)a SN¹wÿñø£Ñyrh­ ˜ !â Ï÷€¾ÌAÄ!¿›Gr' à\¹r%ò›71..ƒìZø×ÜKzˆlÀav}}‹R~ºbê×ö }‹“³ÝÌçÔ ŠSúÚÌæe7•0þϼ3õ#0.ÐX€™ý{1L>þÏqñI9Ï8%€:u<:þ¤Vs-ÄAô¹àÐ÷ƒù ˆƒ8¤ãwóø±ä¿ਘĤ¨\ÇŽcxÚÔ»ˆ€D6 àȾ¾¡Ø»j­F¡uÿÛ²³—NO¶ë%" " "QQg¼Ð伨¤˜˜„ã¨_^¿ÎÁiˆÎ"" A% TÜ:ø}]6µ½EQ§ã·/´,Y·êy&çÛçÿ¼s\×** ܺô¹Ð÷÷€¾'ÍAË¡Óë!z£Ð½’¾†¬ÞE@²S+³GgÕKdòÕ­ "à$Àâ,K~üî÷µ«™‘—–8ò®©z$" " " " " i {ôèÝ·ßòÏÎx…+~Ñô‹#ump à4Ü**" " " " " ‘G`ñ—Ÿý¾fÕ’u[èZå+‹ÿÍWÕkÞyݤGÀyYÕ)°!°mëæÛ¶^TâÒsÏ+ÀFoÙô÷™gŸ}æ™g¶Î;ÚŸ «ßT«qóûZ5mp_“ú<Ñîúê5}E?ü_ÍÊ¥Ÿx°¥ï’¡¶W8Ô®ˆÚ#" " " "ÒŸË~ùéŸÛ³¤•ý{u{¬ÕÝ«V,Ë’³ë¤'°{×?7ÜxÓÒß·®Þ¼ïñg:¾?zøOß“Á³¬ÿó÷z7V>#*ªp‘sý¬Š¿} ›0sÁdz¾ôJŸÓõ`‹;'¼ÿî5U¯;mÉP+ jWDíi çÍnrÛM²ÔemWqvÍÚèìW–*óì ]Ï>§^Ç­Ú<õüžA,9sæõ᤾ƒ†çΓ×ÿª† î×ìöZëÖ˜üѧ=ªE«‡”Uòª2§-j$€C튨=" " " " " Ë–þ\òüÏ=Ñ&'Ó9ü °}kr ª‹.¹Ô²¾Š\Tâ’ò+û*á±—æ“?ŠŠŠbÏ´Iã=ö»3ÜÙ$LÃD‡n¬M7¬[³òÚj7žuö9nÞ!³Íøßž]»hÎ9… ×»ý®iײxÑg[7o"+g®œw6m™3gÎÿìÖ†ˆ€ˆ€ˆ€ø$`ž‰}Ñΰ$wô(í>räpX¶>ýþ»#J\zÙu7Ô~çX˜ˆYM[¶úò³ù?ÿÍÖÍ¿¨Dð›„3f±&hØkWoú{ÃÙgªX¹ªSë¾Ò¹ô§Ì]tMÕëƒ"§øå§ïoÝÜH#­ÞõÏNâ¡*\„?Õ9|xû¶-ÑÑ9.½ü Å2¾«ãÓìþg§©çºj^xñ‰[911qãú¿òåÏwþÅ=ϲþÏ?rçÉ^!Ý<{¡ À „ÆÔÃ;¯ Ö–îÃ1OñÜòǺÕE/(VªtÙÜyòøSK›®[» «Oeçžw¾9¤èùä˟ߟÃý,óï¾½.»âʼùò9áAwÓ†õ—\v…Ÿ§cR1‡P•ÐÙ5«~»¸Ä¥¥®.ç#/Ë—þÌ[ÉRebb¼<ØÇÅÅý¹nÍæMK\rÙe%¯Ê;·máæ¿7nß¾•Í#‡møëOgñDþßÇ×;¶­Y¹"GLÌU¥ÊœwþöX8íIÇYÀÕwðˆ»š¶È™+ùÃà Ãu*ð!á«jê§_¥æ{°zÅò{ßFp6Él‘ÿͯ¿'&$Ž€ |I¥43ù­Ç‹íǧ…3aÆÍ·Ö³ù$ø¼µZùªÕnœ8k¡3_iìFÑ‹j⩌g› œ˜ˆSíÁfàçÊ•«KÏ~­nëûZÌ9õ•ÎÏaÈU¬MÛgºõêïÊ4›Dµ7{ÚÃmŸ­Y«Ž×^3‰Z4tÀk÷>ðHï7ÞrèÞ©=>¥ƒFŒit÷=ûöîéòÜSÏ<“GÇÔmççžÄüC0¤®Ÿž;kš©êœB…G›âi:zôè Ï<:õãq¦Ø¥——=nêeW”´ `IZ5êÍ7H˜LF ^êÑn\GÚsS•Ò&ÿ›¯¾¨}}9ÒþýhÆ|“¹fÕŠö?ðǺ5f“÷ª×W8ì=kJYùÛ¯ê܈~»©vÝg½™gJ¶{¡k»NÝìQ?ÿðmǧÆ´hr žyÖ¸isËU¸Æl¦øiïH.Zøé¢ŸV=Úª)•sqŹî Up>ýd†mžM,ûkGjk}öé'eo¤óŠ^Ðwðð[êÜÆ±æD¿¬Ûòr§vÜo¦¶*×Ýðîø©ûöîàò_—˜Ì­ì5à-£l¶Ø¶e3ùVAFY¼ðìc”¹6uó/ Ý^xfÒ¸±èS-·7i¯uš2þ¼£ÈæÌœZä¼¢Õn¼)Ož<Éxò§Î`ýþ´!he²Lwj÷8 ˜øï{ð±ó/(¶á¯?¦Ng$%g`ƒOÃ6v¨&hDü?ÑoË–R¸R•kóä=5¹œ5£Q¿|¶Øÿõç ¼ `î]îï£Gó5„(½0“½ ø‘`ôŽ-_jÎÛ/Ì3}ÿÝá.¼bÙ/ì*[¾¢ÿ4TRD@D@D " ðð/_>ÞÑÀ¼‚ßG橘§@Á3‘—GtŽï¾ùòûÅ_šžu:ã9+–ÿJ%|ÙªÐÃŒ2à¦Bt¾ÎnpIçvO)bpÎ¥„ˆ€ˆ€ˆ@v#€ôµ¢Î9’4}{tÆx0fÂôJU®ã¤e+TÂsõŽZ׿9 OóûLÍYtúäЇ¯¼>¨Ný†UùÚjH 8—¾º\j¯|]5ü¬1o¤VÀk>uV¬|-¢ñë/Üzۦ̢Ï>=|ø‚Œ]‘2EÏ/V `A|h½Vb3Q›3~‹•ˆ´6LŒ>Ó±s[†“ç|qÅ•¥HßÙ¤ù•®äù%‚¯*,b¿[ü%ê—ÇTÍÃ6 C”à÷aƒïyàš¤ùñÛ¯ÀXY}ÇYyßWº`ì3h¸ÕcÍîmÝöØB‡ÐÙ±F&â{ªûko˜Ã ,ȱS&|`ðê•Ëyæ¼ûžûïlÚ‚(m§n$'Àý¿«“fÎã®iÛûçÍžŽ7rÏþCÉAÚ¿eÓÆæÃß3e\ïÜx½_îDæð1lË[µyü‡o¾r–Ü»gÏ”¹_^rÙåd^]®BÓú7Š 1~ú§Æ¥ÚDãž4Øy,iH[7»sÔšáúNNÞ|y¹[\ÅØ\üE²of×^ýŒC5]{ôÉöžÅÈa$#? ü@q`©áü :à]Gÿgn$òù˜7lÜ|ä›oi°«<›ð<{”õcÍNmÁóجÍÉ‚a<:lÖ¶jظ™U¿dòýe¥à ÁV!Y£Ó¸‡~ún±u–v!Ã_—O¸¹]ØÅäUlô¶ ×fíê•ÆOJòõäµ*nqÔ ƒs?|û5“xm ^Œ‘p³–)[ÁåIJ2Ej6½ç~†OLÚu8Ýܯ'Ã*7ßšì8Q.¥®2ß4-qžË 10ŒzË­·ÑqëfcN}’¿pÀ/…*Ð%°s÷Áy_.Ÿ4ï×÷¦þüΤGNüaÜÌŸbrœ˜ý›%ê›!>´<…õkØaKÄWóŸÛM˜O¯@?ä›ù´¦@õ›j‘°¾¸^ÂQvù_;°zÝë#Ç^¦MÚ2{H7j~¯ÉÁùýŠ¿~·Ìé9h ;¬ÅjÔ/™0oÙúa_~ö©³ éo¾cÔ/iê¬VãfÈÇ&»Å>üD;çÃ6~ÕkÞBšo¿þ‚½©½x†9gD¯-ÓíãÏ$ÏV]07ÙŽm_(«~É4æn¬ô¦þÛ$~ün±õ¶šDZ§é~x¡kO«~93xyGíÛ6àÔÍàŽ[âï ëQøÜEVýr,Hí0‡©ªßБFý²ÉŠ+(‰‘ïO²ªkÜ|+9Ì‚æÝóÅhrö´Iµ®+kþîkœl?÷|1‰šL×s»g1rÐ;UJ]Ä3“ßz£‰¡ý{{-I& 0¶Iû)3³§OF&¤vTýU¨›CK$Öº>µ’¡–Ÿ5`ü±oïîÔp;*nÌm˜6q¼‘¯Ä˜2gQ>GÐLóÏ>Ökª©‡±½Ž]^¹·Q½‡Ÿx¶kÏ~&³Ó3¡Ÿ¿ÿí¯×º¿4kÚD“IUïOœ…õÕ6wˆÞ/¿¸mË&“ƒë55øð«Á„’ŒAÚH0þ±fõ Ž-[¾Ÿ.šÇ§Ý  g—çùÞ2ê}3!¡ìÉ)Îzž6<ç26^~? *̧ˆÜým7ç…öªå¿æÏ_Àùð&©BP#P´HÁ»k_Áà~Þ”Ɖ,½N,SDË¿ÿîíÒñ)g~Θœl&ÇvJ±”:w™´1‘}>ŽuÖûí×_Øåœ%ëyTºsîhtwÏ®Ïsºƒ`ãaï‹…óXÙõ¦ZuÓZ'󄇔.S–M܆™¤]ÅŒ+ß¿û’~¼˜]È{…J§¼ÿR²yv½†¿›7n0›^ß±“tt™yx¾%(²»°i;+1Í0¶Gò‘ŽXà±?Õ¹¡"à -[·qÆ‚règ:M÷CùÿvßøãÒŒ%Ì85`$#íã–0Ã(ƨ®kQ¨paæB:Ép'p8™^+ÁÌn-í^ ØL<Þç2ø— ?}àÑ'k×»Ý^[Æ$²ëwue¦¶‰€¶Iûy/U¦,«ÿ¾v5Ž¢Ìñözà·Ë“ï“p|eØ„öž2áC36æ  $ƒ+ý{vÃåã©ç^8ìÝ Š]ÈèÂŒ)laFkšÔ«‰úm~ßÀgQ÷`‹;)àÔ¥|Ù1äö`Ë»¾ùês‚lQ’ËIU+°UùÖ“Ý“3gÌ€·Þ™6ÿë}2m£{§vÖªlKÚ„‘”N›*»ˆh},.³0ƒdW—Kv·p?Ÿ? 37ë­õn7»‚3ÏžŒE9!ÌÙqÒ¸¥N};s0›Þñ‰ÇN™òé…í¯" " " "|Føý»o߯?ÿèü;ó¬³*\Sõò’W¦Ö$üTÙ5lP?f ˆ`EL¦%<¯µq¥v`úò‰„ŒÒãrAÊD_”0_QÅ6\púªå(cNäÙÌw ÆÜ(i¸±N§ë(â<“ó¯OWGs,*Îu,ó`ùÃÞã£1Ìwuõö{ãŸëÜe5lp¿›«”ár™ÙUÆÿÍtßœqÞ஦xqb-ûèýwñý~ìþ»ÉÇ…;µlÛšªÊÈ×ÔÊxæ›Gh+&)pâ¡:ÃAÔ+^SuÚ§_cÆÇ™tbÖò ïÙ€´æÿg íöü3öÏTbv¥µÂ/ï¾GƒÓܺ îdÑ ¤ öÛùsguëÕÏéæž±)Ň™Ñ¾ ¾1q¶mÝ2°Ï+xÛ²ºclLBxô©d6n~Þ¼æ>¶ÞêøBc÷g/ÆL¢˜1§ó‹]ÈTõo¿:áûÇkÝ_4Œ—÷#Ux#à¿aýìyMÂ8 ;mªä;ÍÂeÊ•ÇàŒN¶ñ¥ÀÚNø¨¼Òw … Ö©Õ]§Ô&ÏIK‰€åÒöjŒêeΧ…Ph'Q…hF•몽÷Ñô4µ—Nʳ!Wø#Mè î}ÞHmÎpš*÷Z¸i‹VsfLA04my?¾£”i|ÒÿÙky?3÷ìNv™¼ Xq?Ë›bL:%ÂËÞÝ»]«]šy‚]|‰ÚÌ,es^g1¼QøHýÔ¢%; Û4C,òÂãú'Ó' ìó*×"OÞ|/õxÍHS"Ý÷gAúþòã÷ÆðjF摃#=Ö/—?³³=Æ  IgfÖ¦±Ì8u.~¦Ã÷'ÆøÃ÷4&@šÓ­5­Í3^åErÏcÌ›uøÐ¡ü xî ßœ,3ñ½õî8ì±|$ø¦¨{C%L£¢Ñ–|ÿ>Æ%ÏúüºäGhÆ–ËÜ{à7׎əß]¾ìL¦±sâ0tÔûÖãÂhZë›1¨ï«Œ–õð¦#Á˜ ‘úâªX¹ª­Ü•@Ù"íÔ‹“§KŽU¾bòÔÙ2)`g¬7ßèƒÓ_,*Í"oZ“¤ó¶¯ë\Ü4<°ø3 Eãé•/ÿYÓ&™håRz‘ÁSëp0"ÀÓ/†ÈM"ZÎêµ4WLVMS{º‹ç*öWn˜÷õ’Uïþ¿ zî:räÉ;³I5n¹Âc$â„hXxÞ¥ÏÚŒõÆY-hÙ¼¨Ä¥ÎÌÓ¦/NᆙÇUòçï“s®¸ª´ÉçB“`EYg1s,“H‰íÌ7ÇÈÊ™égšðKŒ Œ4‹òLNôó(Ïb龨ê«Ï`b!¨ŸVÿ½àÛ_ñàåÞ Užg±9f¤WZo?[C&%pì4ü=B¬1*aîtŸÈ8Ø2ô÷í]L–Ä€çšõî…ÎY&€~ëðâËŸ.^J$z®\ÛšóÕf¸VÿÎ&ÎÏþúãwöZ|&³y÷=­Ox¤ÉpÉé»k–óiؤ…ÓÐÊ0Åñœá1nò¼YÓ:=ûØ£­î¾åÚ«±#¤™$l%qJõ§Þ¬ÅœxÏXN³p™²å9Àô…½#F<!I‰Õ¶ÚÛâS'hê„ uLW6°¬ùšïk\;p  g>!˜ƒ299³¥b´@IDAT Ue" " " "€B¨0=’G£ÑÀØë³„ ÁDurvH»‰cÑzz/11§¯Áýzñ`ƒÙã÷u«ÿù¯°äØCÈ)ó¾þb!K›'%[¹M0³×¤ÿX—üÔj£·šL ±(+J‰x¿Ì¨¼ëî{ì&ÁêMx²Ë•ïÚ$~íÏœ<@R󲫘ïMc|fÕ$Ÿ-Ib@ 熔ˆYä>÷<Þy´eH0ÿåC sj³ óéþ½H£”œ…}§qÞ¤ ¶L¾|ùI;WQ¦չÃ,cËøNøy?x­$!19žÓ¨·Í9……¦¶mÙLDlÚ^ ›L80A çíGÀ]ÚÇ™´ËugbKçDNžé8¯qr¾£q3Ô™ëuWJìîé“OMAõ§~¤Gÿ^/[ÄÚË| ó^DZüÔ ×3â ïO3|”ÉhÛ K/¿bÜ´yÍo¯…ÛíGcG¿òú`v™éµ& ·-i2­Ž5ß öÓkŠ_§ï®ÑrLÀ°õø}Í*Þ‰Ïû·_/âùÇÄL± VÛ7ÕfdˆÍ>…Ÿ•8Ö¾’#`­úù´&Þ U1ʈ6_¸OàhAìÌfνS*Û2)q]²]Ú¼Œ÷µ…ž$æBÌš:‘IkV,W¬“¨ô_D@D@²DoÖ.ùë•õ˽û³V RÌ_Ã:‚ÀLRc=˜ØØ£Ÿ.þÅòîð¡LæÂôú¿gšœ¾ƒGÜß´ñou² GçWúòØC&2f@ïî$nºµÓßL±!ý{cù<|èà€·F;4éº7^ÃJĸúdF²‡óm ¹Ê4mÙjô°ÁæA 1ìÜK ÝáCú“s›¶¨8ç.Wš¥ƒ[5©_³V]œõ˜·ŒÏ 2ŒÉ«®b¾7™lÈô7ÌÑêÔ¨Û áÅ/\±l)‹Mö•>­ Ã2nÀÌ=¼»Á-7Ô¼%þر»÷æ6èòê뵸ƒ¡cÕþ}ûæÏ¹ùïµë6À“Ü÷©{Ñü¡Æ± >|xܘQìmÔì<±ÕÇÅ«qsmç>ÒþÜ^opgSn’E ?í¸ôg[·7º»Wÿ¡¸vÛL›`Pãù.¯>óèýÜ~„ƒb-.ǧ³§gI¤XcÃÚÕ¥ÄlóBEŠ,ûåç铯ã-ë4Ú–û™@‚ô›J˜ïy \¿þj×ï¾^Äà‘k¸Ç³°ÍùÖ@fÝó!}óÈD ñ1äSƒÆf“x×Z"AGˆÂm f"‹0]EÖiÐúŒÄ˜ž#ùàU¯YË‚`¼o4d$¡°L&7 #5m1îiÒV$“æ£ÎmíÒÉ_/ZÈ.¼æ}óßɓЙûš¦Ñ,#)­€¤^f ‰HÔ|TLÓ€ÀŒàÊ õ;›´à›Èìò´ÊšüÌx‡'iÏXNPxA' ài1ªãž}í 5¸™ÑÕ)" " "²øõ7#õ´0tžp$žõù÷<4£‘p†žkœ$%Ș'‚3áîÚj7êÀ—üðݤñcÛ=ÖšÂèØs‹Å×/1)±xñ‹ìA!#€™­fsœ Tœqß…RçWûZÙlËÐ*ÔÈÒŸÀjíŠ#c–“AÜ=ÿ[ÞkbðˆÿMŸôOeDÒâDÈõׇ޴—“wEçá9sæbÓ¼›ü±“fé×küØÑ,TC§æé·÷o;ƒSßAÃ;>õ0mæÏŠvÂ,l©kǧ‰™Œç0‡c×A?öôs¦rÞM n{ØüÑ9xö¶™(¨ïù¿Qo›½ØœßcW!“Ujy/ásDÀkßý¹¼"Ú´qýÒŸ亼Ò7ÙäÆ ‚Ž‚àê¼ýî8{ gyœ›>/v`Éþ’EÊMµX½É”ñz"2)æ¼dƸj±8ë÷?qîÉö>|o¤õy¾ñ¦Z˜32AwÑ‚ä©ò,7íµ(/dq‘¾ü|Q‡½–ñÌ4Æ+J^evñi¢ï—§,XMG4^÷Å.<õ¹ó¬$Ss²^Ó=òåÁ-Þ` b~íÎkÉ"ÚLÓµ‚ ×ã¬Â4]KýF(s6OEÀÚ¹ƒâø&23]MI¦¿Î™1•ûÏL|=z4y ÇÑÿÎ|°u¦–ðj¿5šÖ6’c™L믾X€kÃ]{¾n+4CƒÎÂvW`†§k€öóÝ„ ·=0„†g`oâ¸ÿ‘éÒö¶˜" " " CàóþÌ•;_Þgÿöû®üÏÌ•;¯ypÁ2 nâìÏxüc)W´ öLW¦öº=þôs8â™Æã¬Ç“!k·2áÎv§q³{ñ‹ÆÞøÓw‹!wéÛxÂtj&jLÏ~CÌêµö@›xçÃÉDQÁ0SâÒËGÙ$L’ô EðWö¶nÓÖùøê<Ц y3xäÿºöê‡!ÆOÕ4lÌG8î:•9¶Ó˽žìЉN[-|¡[OþxÄYâ’d¦díz –¬ÝÌC {¦iS™AÕæ„Åòd£ûŠ »\ÝÁ¼rãîè“Ö 8O_°£ßö­[ðÔåÂ9ÛÀ% އ,d›íOâ´÷ƒWDýzvÃÿþäÙ¬‡lÏ‚Y²æ5¥lp\›ïL°z ˆˆÛ·]xq §­Øë‰fñCÂñãön¤*ô0¬øç¬6é绾Úá¥î\n€â´$eÁátÔca„¨ÍÏ:o»Ë$ÞŸü A¿}p•g³cçO¶{ÁvŸ¨ßÖÿcå·å+7ð)ö¼±=«Ê¤œ` `<¾K—­àtrølþ"˜ ~ÃÆtÒhK+bM·OjËN¼æ20ñƒ5Ê”KVqD-ëÓã%>Qyòä5£¶*Ìú|´øl“ƒòË/<ËUdá,ÕÍÌò_0oöÓ;›s™w¾ } |bN-–S:²Èµ½?z8ï,­dã³™XNƒ6§Ã9~ÿ¾½¸å˜ûƒ¯¼wÞÌg¬õÃm9œxÔìÞµ«Ý ]Í÷ÒSv‹û4”aŤñcY¿6ÍËS«óÄLØû’’t–!ºQo |ÿÝäÖºøŸ¨KÿD@D@D@"ˆ@íë¯(X°`‘"EŠ-Z¸páüùó§&êB¤Ó4ÏÔÙ0çƒÍß׳‹Ç'gÒä3¯ÉÇ0å*€…ÀSé9˘µd9Î4O§ŸÌ˜Bµ®ùw<î|ó ÜñP/Îò>Ò„JåÏkêñˆÊ˳\j*çUßþ«ÔÇ´D¯çÂêˆ1Éë.2]"ܳRÇÅÄ@þì¦MÌœò1 }s.c÷ž6áã~ðŠ 0uºî â‘co 'åÑÝÈg¯'J–vüý÷å)øïÎ4l¡w\>°i8Ø£(7|j·)ËéÒ!³FêqÝÔÉË£-ÁËpì3ûÌï| /së+V¹6W®Ü(Xfóò‰}µßPàê„wñCŸTŧæÜÖ¾ídóÓÜwWÓ–~q‰ivok|<#`qO?Õæ–Þ-Yª V~3õÿ¥î¯™žÖ»ýίõ@`7®W³~ÃFg]h˦ú (í2tbaõñ¾p]9O©iM¬¨t³©‡X´Êiþ]½bùÛûòB›2Èæw‡9¯èF3”ˆ >»7»ÅËáÏ=Ù†h ÈY<|È\eŠœ[Ô)€ O˜ÚÔl&0_²”¹É1ïwÝÝÌ·›6>–³€Ò" " " ‘DÀÇX¸w“… ëdzº„A&5#,ª%Z؈¡ýqâM«ù7ݽ«Vãf¾¶ü–÷·)U¦>äëV¯|oÄ›TøT‡Ó]­ ;ÁÀo¼ý.Ó ‰8eÜ•™À}ÿb÷×XÒʰ[—¡ªR•k(Y—µY¾Ò)w….¯öÅúÍsè€×Rz¹÷€šµêLÿ~•k«ÙÝøÍÑ~ðî¾òøcè¥E«»õ`Ç`˜Ï0òƒI=^l¿|éÏüq,!ÌãµS lm6X%í´ôš]4œ·mI&”3PtðÀþÞÞtªåµ)}¬\õz[ré’IÓ;@bV¿¥îm¦Œ‰:ˆAØ„­Oq?ƸBÝ`dѬß{ký;lµ$\ÆsrÖ®^Á;ëé9‹‘fT‚ÀŸ/˜Ë8_j£€®C´)" " "¾À<öðÎ+|{áµåØiY7¸Ïn<ògÊðØÙú‘'žéØÙG S¯µùŸ99å\MZÜç:äâ—ŽŸþ)¾ ®üì¼y,.ŽÅA‰;4DöâVŸ:áC|¡íI‰‰ýÊ냜Ö#»K‰H%lŒó3Q‰Ñ‘®ˆµ§Öaåg9_§´3Øùžå­RD@D@D 0Š7Eùfô͆ÀÕe,' œå— ;6ÀþÜÒyYƒ³ã >‹€ˆ€„,1ù†F×Õ lJ@8›^øÐé¶~zCçZ¨%" " "|un ~ûuFð"^ÍUkE@D@D@D@D@D@D }$€ÓÇMG‰€ˆ€ˆ€ˆ€ˆ€ˆ€„ à0»`j®ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@úx¤ô5BG‰€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@f8³ «~ —AÈlÀ™MXõ‹€ˆ€ˆ€ˆ€ˆ€ˆ€„ ภj„ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@fÎlª_D@D@D@D@D@D $Ä„D+Ô8¤¤¤ÓÑ~L$•‰µ«j  à `ÖI2@雘˜È;¯ T£CE@D@D@ÒOõË+::š÷ô×¢#E@D « HgõÐù}@ô&$$;vŒw `Ÿ¨´SD@D@2‘Ò7&&&gΜ9räÈÄÓ¨jÈdÀÇ·­Éä«ú"S¬tZƒù7>> œl–8Äu ø/cûÍ“'˜ßbÿ‹G[" áD À8œº®¶† ~hÑÀÆ,&M͈(FñšIÕ1uFD ûÎ~×<|zɶ’ââräŒeý…˜˜~‹S`èMD@"‡@H à÷Xûç†Ò%/;ëÌ‚žÈ—­\“/oÞ+/¿ÄsWxåìÝ÷ï»M)WªdýÚ7e¼å­-ãíIS üТ8%M2!*!1*!ÁÔƒØüÙj)­ÐsKˆCÈr8#&wRT4Ÿ_}xõ%Æ= /sóAM´*ŠãÄD©_û}¥„d›·nÿcýÆë*WÈŸïĤÙñÀCG¿ß{ðˆîŸ~ù¹']ôwíÙ[µ^Ó"…ÎÙ¾â[×®¬ÝD°¥Õ_hÎg_uí3èââÅþú)ÍØót©- Ñšô#?´I Ç“ÿrä4?º‡ŽÅÌøþŸ3r<#úäšpìŒãñgä>tª©±U ™†8˜{BB–CR¨ߘ\¦ÉïºX!{±ôMk.M¶äÀsEÓ*$}Í- wÈVÆM™ùø ݯ¸¤Ä†M[ÞÔ»ù] "µû'uE¤ö/¸ýjxÛù‹¯üjNšìÒÕªTlX·VµªÓÚX¯§Kwmi={`Ëó[Û¶ÅÿÙ;x-‡/Ž+íû¦)I)%ûNÈ.IQJ’]üÉ…$ÊZTvѦHh•²%¤½´—í{·õÿ}ï©1ž÷½ï}ïöÞí÷~nOóÌsæÌÌoæ™gΜ3gNß·c󾄭yKVtÌ÷®ÿ;OÑÒbYŒ„ƒú}@ï…½ÂA8è{‘îŸÅWN´~¥«¹ ^}Þ¿ç–žëÔ¾c·¯¿7Hpn멬ï¶íÛäv$$¤(}õ#«|òÞ«)JbijK5·T -IluÙ­1'[íÅš¿ƒø;ð#C,þ,BÂAý> ÷Â^á ô½H—Ïb褬}ûäðÙ^(]…@¦ °yËÖ™sç•.Y²ú‘Gà€=½Ê€MÇÜ‹jud,< *ø×‚Å+V­ž¿hÉÁÇT†ñO¯J¥OÎÑÓZë7n¬]£: ü³fí¤ß§V>ìÐckVÇ…C8LxZšõ×Z·Â!åN9¾~}>ó.¦VªX¡î15 *è?u%$ìüòÛö眾{÷žU«×ಘÛ%Ë–ÓøT«RÙõ¶ÙóÌ™·€È#¨L©\<ôÌá¦Ïþ«Â!e)Œeä²à6b]6mÞ’TváÜŒgôJAƒë©©³æ.Z²¬léR˜þ³¿Ú¦û5ô™=ð£´É‚h”}‰ywír™â+yöLjÀÂAï}@ヽÂA8¤ú»É´„ó¢p´‘'ñ[,ØúR¿Úü³JåÃêÖª‘Å‹ªâň@§g{öìÓâ;wP§Öˆo9¡ FÉþ˜6óööã5iîOãŽ<âðˆ4~ä3=ØæžŽGœXyêƒW_ðE §”D&™A8Ì”r¤=Ó;z|ÌWßý3ã§û;?;pØçÆSäáï½0HñÅ×w>ü‚¥ÑZá·º?åPýòûŸ7Üý¶ïö´@üÝ;w¸ë¦–®„–ÑìÆ^ÝæîYÍ'¾G—Ž -y½ß £¹ú¦»-ðÕ°÷Ï=ý”᣿|ô™—¨‡zÇóþ+Ï[óh‹ù}ÚÌ3.ovQý³FzÛb’­ËãÏõJ*»pnðL¶R'ývÓ½-ýÛ €×±/†¼{ÒqÇÚmú^‘x‘{9à—ëF&ãïyëÖ­ûvlÝ·s[Þ|[]Ž{·mËsP<»BB2?õú€Þ {„ƒpÐ÷"ŸE$Þ¼»vÚí¼&þê¼õ.]“B%Êú›N¬[;þ M›=—©æ…çž9fð;IOñÙ mÛwü2æã:ÇÍ„üâf7?ÓëÍÞOwJcúôÒ¥û+×\y °i›’exÎi'#¼,^úw£ŽD>ŠNŸ þÑÆói΀ µ ¯m³tùÊv·µF¼|ïÃa?MžÒúý<æ#‡):Û&7ÿ¥îãÜÍÊÙ_ ¿ÑoÐ/¼lð¤?¦ÖoܪDñb¯=÷ÄÉÇ×¥ j¶ëÔ-_¾ƒo¿¡¹cB ùí÷ý½rUû¶·lÙ¶µÚ‡³óöðÃ*¾Ý(’ó½·ÞP±Â!yóä=®VM(|â¹R%‹¿ødGn±Ž~wð0DâÛèôã¨!>Ãð®¥.×_Ó(©ìŒ§Ï-ÙJaJÝü¶û¸ö­ûqµkRë÷>üäï+3B¦`ü}vìÂÈÀ[7oZ»ví¾„-ûvnÏ»ëßn¹wãú<Û÷æ9p¾‹ö·¯€JBýAýAã}@㤽Â!¥8„¤ß¼yóíI(˜wϾb  †Œ×øF]£#pUë¶sæ/œòõgNŸ>+?E‘R®Y¹:Ù±l/=õˆûìSO:þØcèZi¯EõªGL›0rÁ’¥oô£qfØÇw%GV®tè¼_¾Œ¾¸“ þi¯TzqøWÒH/Ž™ËgÕêµß~: ÖÑGQŒë_QíÔ¿M1k*Äð†·ïò<¡}z9•oÛøá+v‡§ºCóùož~R=bXØÃ`wÓÝz¾~s‹¦¾55&ò†÷?¡nmKÈõÔŽûõ÷À76o‚éâG|‹ò¸žÇŠÝÑg\üë”i+ÿY]±|´Å•(u!¯¤²sùº@²•š2}6úð›_ b¤ÂìÁ㘤W€/+â.ÖÎÛ·oß´iÓ¶mÛÁ|ÓÆu«V­Ú—°mß®íy·y{€7¯ÉS(!OþBVõúÀ^½‰o‚p°A8‡T|7ó(ÌÌ„‰MÁ<{ŠÈ›·Äi€­#å¶kDª¹ „¬S_æÉ(ذM{‘.8ç ˜ ÇÎ Ò/#ÃÒå+¾ûé×óÎ:-JÚTðÂ-ÎršÜ·çÓ&ý‚cáÂ…Î?ûô>ƒJÓàù‹–²³—ྀW¼XÑ+/¹z €1<@íiÒ¯µ]+|6å.^¶ü¨ªG¸æéöÈ}¾ôëâö-ÙÅó±a±pÙò•Kþ^]Ž^Ç0z –J±é&ttÄàòåÊFg˜ö§fÿŒ¼eË–7@ ¼eó†5kÖ þÝ·kGÞÿ.?ïݲ.OÁyòï߆-Ã_8½ôö"áŠïfÞ…˜æ(P ðÁí+R PBHÌE`ëN¹çÑ£jî©~V«)"èò•ÿÜÔ¢i¦l@âÒî¼éÅ7ÞøÉˆèp¦”0½2Íip‰âÅ}hÊ—-ÃíÚõ,Ò6⛨ öÉ,ü„ÿ4þJx‡òà“ëÕõi’ £ˆÆ ‚4VþäÐïô\=EL½.“„GÆR©šÕltIƒÏ¿øªNý†w¶¾îÖV×r(q8«´ÇðYµ*_”À˜@£fë/á„Û7oÞ’ëv'äÝûo·Ü»u €óäÛi¹‹@8¨?Ðô^Ø‹ „ƒ¾©þ,æÍ¿ ûFà}ò „ƒ?ÄÔòä9è¡&åó”w÷îüÌÅù¥tfâg™¡afü<ýâËïôw¹`¬7ð£Ë±ëÖoÀé)*5—ÊÖÌúŸîÖÖ®#Igá5öm±È3§]ví®]»Lúê°C+8n8ʪ}ÎåH¿xN%²ï *î»ýÆ‹Ï;ÛÑø?¦ÏÂ+êíïÁnñ®]ìi«k½ýb7$XŸ˜ð·?Nºþ®öfw›Îî÷Ãj1SgÎÁ±+¬KÈÓ÷^~®jåȉâQ•§ˆÖ]_z9!KŽ,Ä ±wµié¶õ%ë´•„Hõww|8®wµlr%¾`gêO¶?˜GX<õT(_î¡'Cy±¦°vö$Û®ÓÓήƒ+þƒpjënýÀú I2xøHÙìªËhLq58÷Œ–m„€zãù.­®¹ =jçzãØ™H–'†÷{Í$Û={÷,ý{oñˆÆ–õˆÆmî:¾Ní:?d1¯I¹­Ý¼u+<- ë&ÓFüðÓQ´QóÆW ®ŸÖi_}ÿÓˆq__Ûè2K±ÌÑfå§Áw)K•õ¨ª¡…´2ûe[¸t·ø(ó#“ ÛÚXx§·„6fyÊ ìN–UŒ¼ —µ¸eÓæ­oõxªåÕWÚ:Ê5·ÜóÙØ¯Üð#«Ô‘ÅX)‚z´ÝÞyóÏFó®âbš·úÙN¦.Óè©øÀNvŸ£I˜üI¾#O:¸l‹Ù³|Öžµ‹ Ô½TÂ> þ ÷Bã}@㤽Â!}q8éöá‰b/Ç«g]ÑתÜìöûF}9áêË/b¾hÑ¢Ÿÿâ›ïžuë™ß޲Xß¶C¤ß;o¼®ÍuM§ÍœóJßþ„×7mTÿŒS‡Œsàú딩ŸŽOäc÷µåØKÎì®ñ¥2ƒâóQÇ“[¨ã,¦ïÀpñ—I ÀFöñˆ1Oöx—17\{ûï^í;`ÀÇŸ—,^¼W·Çg(~/n~óÙ§žˆÐ»|åª^ëÛõ¥×N®Wçò ë'€\д5b9"ký3OݰiÓGŸÿݧ]vÍì‰cK—*Ý£jóÛïÇ6}|·ÝЬÊa•þ˜>³WŸ÷ïëôôºõà.¿$Qœ¶BÖûíüi»‡ÿw~Æ¿ùáþ\òÔc÷íDZœí dÇ»plÏ ÿ{¸bùr½ó2BÁ°‘c_é; t©]Úß{Ö©'ºRWÝØ¯º`ØúÚÆøJGÑ:ô³1-7tšÛQã'pˆBãµ.ýñ×?Þ2œ•ªùÁÐO /®Ö§cÇÓ[ÞùÀœ¿`aUó«ÏvösA±ÜøÆ»8ETøJ‡£Œâ¶–¥ þe*œ¡ƒµ^£IÛâê†ÀXD;8¼Ì©È"ë$ÉÒ0¶ ÅÞTŒØ©ë£6Ü›xê¯=\5ñ¬?¦Íb¬÷lf†+ØÉD|³=5™GŒÙ#Eó«.¿¹Å5.2½áÙ…sNQ¥øŠ´nÖø¤zÇÁU¬úd^HÅ! „€ñGÀ)åâŸuì9~ýýOCßîm©Pÿâ&—‰ ç}Üyc‹ˆ¬6nÚüɨqÇשõÊ3!I/GyÄMZ³£­ßËÏELB$šLaK+æ,c“¢ Ä3}BÜEIè À¸¡¬eÓ+ËÜ‹N;1äf5Ê›ÁŽ÷ÜÞµã~U’ð™W4çxŽ'¾××Z£bÁVÖy.^¬XÇn=ú ùÄ À»ö@÷øf÷§ni¹ Ú¦y“kn¹Üžõmt¹Q<ª~3ñg¤_æáG ¶8îr.¿ð<ðΠÐ<ù¾;Úøú'ÔËœ}úÖmÛÐ úI¶òÔüÝ([¦×G|á'qáT{ úo¼ÐÅù8ý”¬}Ò‰®’£ôŠÏ¿øš$ÿ»¹•¯˜Å;”¿›š¶0é×êuéçÀÒÛ¡ ò–‘)¢Ì]YË`™ñäôË®­uöeüsÖ¥3çîïHŽŒ€ï¶Ö~üù^‡Ö=ë²·òÎ랉}DJ" Á°Ôryƒú, ëN*IŠø'Å$³â³´˜îòV®g_ÙÛ–LÎ=ã”Ò%K°0†ýx±IÕ×bA«ƒ®Úa£Ïn˜°Åé÷“Ñ_Ö¨¶ßþ¶G—G0á)ïö'ŸP¢xÑÅK—Ó)ñ >åëϓ͂õ!VûXcÜÙ¸ys»Ûn<û´“0³™üçôÖÿ{øâóÎb‡zŸþC  ¹Hû²kxv—¯’­69Œt­š6ªwl­-[·½ùþ`Š×²I£dë+! „€B@d(ó†´|ô)ªK—‘íEëb#½d¡ˆc¿(¾¾xŠ‚ ¨/ü’¤å–)ëõM®ÄÊàOF>Õ¡¬¨S<› 7Ï®[»&¢Ë²+ý¢òõon_q¾ÅSÔ­ÌðI<ʶæùÜþšJŽÄëGF‘Î yÁâÿÀÝi+“áá£ÇwèÚ}Ì×ßýïæë^t~”|ÙE¼½?àäµXÑÿâ ž#Ç}cF¦„'O™Æ5J¯ˆî@×Jh S™"DØS»–)ZØI£V¦t©„¥Ó}â¤Â)r[Ûý‰ü%ÅÊÇôuÐð,a©îⱂf¡yRêÆØù;žY'¥``ÂYÜôoG¶òy î±% †^ÈžxøœÓNöqĹ·æ±ÙÅ›»?ZÔÅ`ËŽ›¾{ëÆ¶þXbIà #À¨fÒØïyä)öèb9c‘ÇÖ¬~ßím‡ˆÙÓ[[^ûÍÄ_ ÙgB¦Øx ¤ýø—Ù €U ì¸èô@ÛâE‹ÞóhW+ ó'z5ð 1‹ðº„g‘[²•b¿;œo $XÇjÛ&²M‘ÃA! „€B@d4æÎsÚ¬9nâd9¢Æ¸$QÕ±Xr^så¥x[e²úù¤âëïW©¥…?€IŽŠ" ÓO:åŠIË嵯~*¶+ó2^ݺÍ7¨öiæá¶¯¥Í×oØ ÷oN[Qhÿ0bpǧ_Ätcc,1ûözWÃ~’…S×, ÔïoôŒÃWüEsúÉ(Øð£c?=i»tN-%mRt#–œv'Þ×(Ù¶Ú·ç3ìe’*-j}¢n®~œüGÑÂ…±1¼¨þYË’ž–ÖÔÝîèXZ¾«×®³§é—©o`r ÃM™òç;µ†fÕ?k¸šK@’(·(¢¿ø°/Ç=÷r4Šn¸sÆw£RLêúƒoôøo }d¼½òGgϧ(YX’òåÊ€Ã?kÖ¥ºÀÆ'½®á¶Ö¬Ê©fŸþCÂËÉšNçÿŸ­c²lø²šÝ}·X˜ƒx¤Í€ôëZ‹uÛçíbüÝ‹Ìqሹ§°dàÏ¡þuÜ:é×È·³Hª.áÙ¸¹’D©4¬ó…/õ¹´ ! „€B þT«r{}§Ïš›"ø‹o&¢ÔÂo[,YŠñ²S©ô*ÿÖíÛaåk/¸Å×0VÐ%½L_ÀýUŠ2Eaï¥ÉК°›ѧÁ,ü‰Á4¦î); ×ö¦¸†‰ïÀ’c1N¾xsi „'÷ŸFcuŒû1”Uœ…ƒÛf¶›FOþ4uýÁøà5 E×Ïc>ZùÏê5ë6T«r8‡ž„gáÇ í³ûròŸÓ"n3ô)ãNG·µ8 Æè•þ¶ä÷ l5õk1òË ×ÞzïÀa#ržÒÑë'„€B@! ² -®¾‚Rá|ØYu%LH`{ïNw‹gã§^| yÏb8¶”@×û~4b ²âžÊ÷¨ÊSN ª‰—– _S=ó¬ì6p%G¶Î‰­ßÊD]¨ï ‰x|±£ »kM‘DïbغHŽ#æ¨^¿)Ý£ˆûCÄ´˜v"ÍÂïSg®]·ž°y]ŽHl‘M¯¸„@Ï7û¹î‡QêKo¾‡GÞ(©2èQF¸­eç&öðæÿ1Øÿ]Þà\ŒÈ4Š·¹ðš²dÃÛÊ«ç òQ¼w~¾7¯¡c À[æ¿wáL2:&Ûh€3ñB@! „€ˆˆ@:êN#òyÉùçà☓Nº¨IË& ÙDºyëV&î}ztmšxÊ.öÆží ðš Èé»~:jÌWß±‰Ñ1ÇœðÚ+/åVÛþÖóÍ÷TgÏ[8rÀ[Fƒ†ó…×Þ!ÌÞÝ*‡æZKQ6sÜ+<P)X@ˆ\wåÅ á\»~N€íX Çdȧ£Þ<,aç.„ € ¹ÚSg`ËVÞa#Ç~—‡îIQ@üÂã]Ñê$ Ô­r‹ü†O#¼¦ïë(¢GUDw³e÷Œ+š]uiƒÃ+úÛŸÓ)5êÙõQÛã^òð– N½ô¼ØrìÐ!eKOú}*~mQe“©Sªç÷f·mÓÒ´ÖáL1±ô‡@wûV÷§.¹î–n=ßp1ðŒõüãYGòã-L—{éÍwqèME€…ºÓ©XLa{f8qFÇd„ÛZ³nÖè²ðÂã=®é³ÃÚ‡k²p²@ ò-þ€‰ä]ÀkûšŽW¶Lév·µæöõ÷²û×?§òn mÜn%Ç je$„€B@ì‡vÃTLý¹ò‹ð'Š©^}ú9Ý,Çó\zÁ¹ÈuVö¬³wï>sþLäüÅKѸV?² ‡¾rËqœÁ;äÓÑH¼{öîüfÈÙO£ªr­]ãß-o‡V(´”¥Kþ«¢„Æ~ÌæÙ ŠË-þ¤<ù÷ÿ[¯¿˜ûpûgSJ'+æÁ™~Ûvx5#|Øúûr·NþÖåpç©|0í“×÷¨zÁ9gü0òû~WØ_|ó=4?óØíÛ†NBr¿ˆUy:z`Ÿ'{¼úVÿ!È*Übs˶¯?×Å@³äÉ:m%g³Û!Ü’àz¶w·Nn³maÌ+U(o±\“í!‚3žÉÐáwº¿-žtqͳaÓ¦&ýNƒ^W{ü€mäÃÇ|Ù+QŽÎ¿<¦Þ˜ÑþHÏòœ¬Í=àÈ-–Êú4éî¶Åûøï~*V´Ë~F.|ý5€GŸ±·;2?ÀÚ07³x°Å£zÕ#ì–·’€o‡ï'OXp|pV.B@! „€È– ô,XùŠIm¢o³ÌG?öÇ.]¾’Šhí(C‡/ÿÌø “K·ß¶hGzÛß9Œ‰rõÓ.rÇØ²³‘sq}-.–ëgþœïàƒ#žÌIÂ+§ý€ åsR~dÌÝ)2’ûêJˆmöÈq ]dRË/¬¿`ÒW³þZ€óပ5I"º;E7»aîdÄ`Ÿ'‡±ß5ì¼E‹+†Šü5FÑ£*@²Û#÷ó‡‘0‚bjUi}Ÿ9áXœ¶r¦1êëy —°û׳ç[ÈŸ»Ú´Ä7u€y”ÛdûCDˆ0ÆÆÉÓe Î}¢ý=ŽùõM!#÷ó¯`()ó€×{ ó§én‘ßÜ2PÄŒP&¯=‰¶v¹ ÷r˜pRž‰|Ê(aJ˜¾nkÑñ®™êðvTXxÖ¬løkrÞ<ÿéWád~ & ¼Œ@D3Yü¨}ØiìÞ©{omÍ[à¿w~òø„%Çgå"„€B@l‰sÙ"EŠp )=±3þ•‰âÁÔͶ­T -%àv!Z$®žˆ±Ã`-&|Ž&ÙE¼"§%%&ý»ƒ†‘ûTââ OF.]¾âžtj±ˆü]$Éq×än§Aõã€áGF {lÍ$Yq¸GUÇ <\·§`‹Zêb¦°Ž§<»¿ö6íHÈG1ÞFéá±—¶ûöyoÚ²•¨*‡U >øï=•­]£úãBwá.èòú„G†s‹%&}ÝÖ†¿2,ùO#Þºu(÷4Ð9“ÍÔ%Ì €à Vl…€B@!`îîD„³E•Ø©;sî¼î~è–ë¯9®VMwÓfÿ…5…G—AU@¢ë7䘣Wô³ÀmÒó¯öA'‹ú×O˜³Ã˜£³5ôëO>ˆ8ÄæðÂ… ýú»Û|üìÓNªVµ2ž ±WÇifŸwvÎF[µó죡°B@! r/«Ölðõš¼ùVä/X¸@‚=ç9h/¶ŒÙEîu-÷Bç‡(óûC‡?úth»¯ýPÞ²%‡L"ÒùÿqBg/¡¹ œ„ò£íî¼ì‚úéœ_6gwä•¿úF¶ñ©'’~þÁØÆs–)êñ»oºþ±ûÛˆO1”KV@@pVh•A! „€™@…růip6«…˜}f;Ñ×@De¿âç:µÇðxÅ?«1ã¬zx% }3â3N>á«aï‡Êbÿi EϽ^효C«X!:YŽy»cáôª2®­ÙCËéPɃ/ñíX¹RÅp{ÝôÊN|²,€³lÓ¨`B@! „€©Gµ.‹Ìëlê¹Äœ’Ó•Î=ý”˜Éƒ„HbqS‡óÎM÷¸(³ÓsS¥U×ÿ §^ÿI§! „€B@‡@6Uùæ¸vP…„€È(²®_mÓgÏe% oÚUû4óýü‹¯868d³ÁÒÌ2ýpþÛâeËáËác¸%”¥GúC,ŽB@! rH¿üpõlQ'UB!ð2YÞ¼eëŒ9œ ¶´L©’8góeÝvžÆQÛwŸ Šÿÿ ”ôÍ¿þÑôæý~Û)¤€WáTnÃF<³#'ú ­Û¶q–ç–så`Ðþ}ÚÌbE‹P…@­u+„€B@ä¤ìÍ1M©Š!;™s0¾ˆ)âûC‡ýltIJbHŒrøÑg^õå„GÛÝñ^ïg¯TqêÌ9‡pô‹–þ}VÃë~oº®)wßtýoÓf\Ñêv|IoòŸÓ«U©Ü°Õúâd ÊckV‡Õó¯¼íXõ~ûƒf·µËŸ?_ßžÏü0òÞ]Å_Ô=v·p±£ ÂEJðh°óøcÁ}×ñuBË™¦Jui©Î§cÆ7¼ø|þìÑ uâaæM^lKfǵ+‰å~|Ú—_X‰ý‡_wÂ?*ô-[·Q0jáèB@! rÀ:ñ(‡µ©ª#„@²dŽ„sÕ¥rhÇð\Wû–mÄE“_Pn9¶‡’8Lè§ÑC»’~ÓL±B@!FJ—.]²dIÄ`d`éÓ¦’ !]øWzÌ”]­ê¸¡ïr<˜û|0ÄÊ`fÃ×5¾Ü/’©%T6ñ—éóÏ:ͧýÕ·Üú¶»&û5kt™O6cvHÒ®utu®_O B—)UòÇ_Ÿ¿p f@¨‘?zç奿{îé¡ÊIyÀúsfÈR.©p.…vlü`¡a¾»Ã“¸ÈzõÙÎæ}Ú„Ì“Ý\GÌ"#÷+bö¯ix)9~8|E2cVÈÖQUÓ± Ž•d`…B@! 2ü?#—(Q‚ÃÐLè¹Í¡Ü…€ˆùâ“M”\]ÚWUËV¬22d]†àçœáRá6ÊT‹+,‹œ» äžÊDMGfF¼þ¾\γEE“ÇMø$õÏ É·¶©øáÿ݆ãhÇ'Ù@@€4z¼:ïØ‘€'j§f00ÕY¾ê¼L¡ˆ>ï€Ä¾_ž‹ hò*U²D¸,·š@ù¯»úŠ>ý‡à%ûÆæWcž}îé'ë+˜l7B@l@™2e˜´ úÚf`>ýúúgëUá…€ˆÌ€)%ùr­wlM®+V­ÆcÓ1Õ«+º»/‘SfÌF3é$Û;wîÚµ‹x¶érµòÛ´Ys ;²•ÿ¬†û„m§«‘±ýõ£c Èo_1™&ÛžÆx=`TüÿUá2-Û€Gÿö‹o&üdD‰âÅ8WÉñßOüßS”ÜÓt žçŸ}ºÏÓ<`aÂí"Ï>õ¤Ê•]¸dÙ»ƒ>&ÒaèÒ+À—5ož<Ç7íõ_†Kþ{ËÝþ£­Ä‹ÀÂáÀ;ú_ýAýAýô½°nbø+V,ô]Nü…dß™˜j‰'}>ö«ÇîkË­û!oGù À9‚¬©3H2DÌ«ïàÊÑJîØá¬‹|…6Ù=Ùã¼^?ßé!×1œ~ñwq—u×M-I·Hï?p—- Z›0ñ¦MÍÝ448{oðÇÔ´Ñ% ¸µŸáy’'iãý‹j׍޵öªÐî\lλ¿Þ÷Õw™A°öéãvT‡°u%Q@! „€ˆ6Ûáj¸å«Œ„€™…@¼àÞo¿‚·n­§X¯`H°œBĘûÊ3›ƒ+³.6GÊ”Rñ¿nœ®¼ø‚Ã>oqç(r§ÍšóÙØ¯Ú4o‚ï=`…ŽóAÊjv{»†*¡þÇÕ®ùìcóÆ—^øøs½8Óˆ#…¯¾ü"|A-Zò÷7?üŒˆŽ.W?€,ÎU:ý¤zÎÔÙžP ÿ[H'š"¥·½±…c‚,Jå‹ÊS¦Ïzº×›Ø !Ù«}¼ôæ{‡V8ÄàŸ&OyäéyԲɕ°%ù÷vذqS…òå\.ϿڇՄ ‡”óàp¸Ógÿ…´yr½º.ò…ÎÍ_´g×—^ÃÝÔ‹Ov¼¸þÙï}8ìÌSNp4&‘zãÅ×ÞøÉ¨qüa2}s‹¦;ÛiüTqzð½u›ôÇTþH‹ûx‘3Ÿ@a•˜pqÚ¬¿Øj‹ñ¶£ç 'Ô¼7oÆ÷•/-OŸòÂåû¸¦"Ä\|ÞYÎZ551—7¨oÜÌH»jåÃj×8ŠlÀùcÇŽÓ¥ãElò”i<º2ñP_KÅõ€Xþ¯µ6Þž‰÷2bV%У-g¥À#åød\@ŸÞŒÃVœ…€B@! „€ðÈ“¾ú·ÝËCÂa²?”¨Kþ^Q´Ha$:NŸžÍ½»vïö­syš°“r:©ÕѯZ½fã¦-Õ<˜ %-RÄ=­|¹«×®ß0w2 —-_‰ ŠÓ,´¬ŽÀ$» ꪇæ¤PŸÀ…QÌvèÚ½oÏgZ7kì" lß¾ƒÃäJýHŒ®)y ’ä[wÆÒºõê]Ðhë¶í¿ÿ@,ùÞ½{×oØT¶L)Çmý†$qå7‘ØÕ÷¹Wú ÊÆ´—„ ìHعzæÏ.Ψyý-Öî Æ=F¡4_¥Z~Z……€B@! „€YÈÒ`F—¸d‰âuK˜ ²b@\„ EqDbÌ}ùsœ4HÌò•ÿ`kYµ‰Í¸>ü ŠŽ2<€ªÖv‡? İÇþW§jÅf'¯úL/ôKüÿ튠>rÀ[Nú%‘Þ—~‰)]ª¤Ïį,–äO½øÚçœbܧ1XÄû‘pNJú…,)¨} ! „€B@! ²#™#Ç)Seu:æhFÅ]º¿Œp‹m3Øia>ô³Ñ}>¦O®Î˜9¥Üð‰}S»Ž5ªUùèíÞní`ØÈ/†~>ý7ÜNŽËaK)-¶è…€B@! „€qF ç ÀÓ‚>™Ó YvÿZ¡üú›°Ó.Qìß³šRÇùyüGýêŸyjê’“ kêû︉ÝÂxÆvL86oÏÜ^|ÞÙxŠvñ ! „€B@! r-™³8p³éwþ⥧_'°8Yçž,´8÷´µj*„€B@! r9VÎm“õ« 8ë·‘J(„€B@! „€Cà?˜]¬B@! „€B@! r€sXƒª:B@! „€B@! À‘qQ¬B@! „€B@ä0$ç°Uu„€B@! „€B 2€#ã¢X! „€B@! „€ÈaHÎa ªê! „€B@! „@dÒù¤È™(V! „€B@! „@f# pf·€òB@! „€B@¸ 8.0+! „€B@! „€Èl$gv (! „€B@! „€ˆ €ã³2B@! „€B@ÌF@pf·€òB@! „€B@¸ 8.0+! „€B@! „€Èl$gv (! „€B@! „€ˆ €ã³2B@! „€B@ÌF@pf·€òB@! „€B@¸ 8.0+! „€B@! „€Èl$gv (! „€B@! „€ˆ €ã³2B@! „€B@ÌF@pf·€òB@! „€B@¸ 8.0+! „€B@! „€Èl$gv (! „€B@! „€ˆ €ã³2B@! „€B@ÌF@pf·€òB@! „€B@¸ 8.0+! „€B@! „€Èl$gv (! „€B@! „€ˆ €ã³2B@! „€B@ÌF@pf·€òB@! „€B@¸ 8.0+! „€B@! „€Èl$gv (! „€B@! „€ˆ wéÒ%.)“dذaÃ7ß|³páŠ+(Pêð˜dXdÃÇ<òÈï¿ÿ~ÖYgeò«Èˆ@BB˜1cvîÜY¡B…ŒËfòäÉ_ýõ¢E‹*W®œ?þ@FÓ§Oÿå—_ªT©’/_¾À£Ìº,ÚeJ¦2Øí¾}û&Nœ¸uëÖC9$"‹ŒØ²#]R@|G2«H™•o*Z-J’Ñ£GwïÞýÔSO-V¬X2=B@d{˜RÄò[¾|9#ã›o¾9pàÀY³fÅ’$·ÑlÚ´‰ÉÙ{ï½÷é§Ÿ®X±"¥Õì±Ç¬3½òÊ+–6<&¥ùäÓO?ýûï¿=U)ø©òÝwßíóù믿fÏž½}ûv?2û¬?DlÙˆ‘މ©F ¾#™U¤ôÍwíÚµÏ=÷ܨQ£RÝ4©KøùçŸóŠ=óÌ3©K®TB@ì‚@òšÍ›7?ðÀï¿ÿþ®]»Ü|ñ´ÓNCd*S¦Œ‹‰O€ÂäÍ›·hÑ¢ñÉ.Æ\æÌ™sóÍ7ÿôÓO´º%É“'O£FX/@#—6J FVÑÉvïÞÍd·xñâ|°£DnG=Âí!CÎ>ûlŸ¡ž={Âÿºë®s¹ôë×鍊nâöÐCeR^¾|y÷È|ôÑGÍ›7(z ÿ‘ °gÏ¿q…I8Ÿ}öÙÇŒ(E'dµ¥H‘"á4™“ŽíX·nÝeË–•(Q‚aªvíÚ~ÕÞyç{ï½1’U?>ãÂéX¯»îºË¬fÚ´icŽØ²#3®‚)✎h¤(ß@œc  ¯ÈÈ‘#y1?Á>%ž-uÉ%—”*UêÝwßÅ8+žù*/! „@œHf02Æ7ÞÈ É¤_ä:+–S§NsY_xá&pXæôïß?ÎYGÉŽ¥O<ÑÔDŽ Ü˜r½ôÒK.&‹øÐ–NüÕ¨QÃ/ÒI'Dã"5¡öã3.¼jÕ*¾ñ˜žúöÏÿý79ÒʨÐûôé“TîO>ùdáÂ…1_·n]R4¹6~Ò¤IXóÞpà ¹X*>aÂÈzè¡ /¼ðÊ+¯lРA,©âIƒB‰>wîÜtÉtåÊ•ðÁJY7ÀÐVÜìx”·éÛ?ï¸ãŽÆûkv[6bdFÔ.¥<Ӕ枭és tßô3Ï<󪫮b}'ÎmĘsõÕWÏ›7ï»ï¾‹sÖÊN!O’€ÙŸ9|øp „LÂ1¹¥oß¾èJ–,Ï‚’³7ËÑâ\€ðìX½óÎ;±!äQÕªU_~ùe¬ Aì–[nAA³ž$sc˜æ¢þ¥  ;tè€!v’×_}|JJØ%6kÖÌ­ª¸|Ñ ɲ .Ò¾ýö[ŒŸ[´ha;¥]¼†€Y³OR€DAÀ–Z?üð(4™ûˆQ…þ¿cÇŽô*Ö(GyäW_}…ÙpzñLŸôíŸ \Œ$Õ«Ww%‰Ø²#]’L ¤/™X‘øgc ‹ø¦Ó¥ÙKõðÃÇX³ÉB ÿ¬•£B n$cÍ"«å‚ .¨_¿>a¬R1÷åçŠøÃ? ˆcÁ²lÙ² ,`× ®kP$âG!ܯ ò3;Ê`àƒý!ûNÃåLõ Á®Sœ‹.ºè¨£Žb"H.‹/¶LÙ&Š4sSþóÏ?h¤É2°r‰}, o»í6\×°£•²ñ-9öØc]ÑÖ"ì1<î¸ãˆô“SržþüóÏåÊ•cž°qî“O>¡ä˜žþù¾µ$ß'›¡¢PeÅÔ9ìA)qß}÷™`ìò…f½¸Ê É1ÇsüñÇvØaîiì´¸,LüùçŸÈ«¬DœrÊ)á¦ÂØ9ƒ yAÎØ5‚eÄZ†iEŽ>úhŠJÔ 0º”€÷ ¶|ÃÌ!£ØèŠ}Ÿ@>zð$Sø@yî¹çrR)¾î<½öÚkÃih4rãÇÿâ‹/.»ì²Ák¯½F ëXkù·K—.Å:ºV­Z tñì¦éé ~ÜÓt Ð…0ŽÀ,“ÎÃ[àz :mjÖ¬I.´ÍW­Z5z`D[eÖ) á5Cz]¡B…ÂËÚ4 d6ô{×È—×b–9h2èûéÌ(€u|7Ž@vÅcß5?^FJ>l4€ ú/–BÆÂ–7 ĘÎbIQ"¥3ÿöÛo‚ð®±Ô‘-¯ ÁèA§õûÛ(P‡šå<Pš b¾9 ^d^çzõêaÂà“1RM›6 ÃE† ÞSŠÍ›•T±©2o—Nn¯D¥J•H&6zðÎRat{ø»x±?Q*Ëd¯;D0ÂÒ*³¯ÂB@ä(ø¼Eù½ñÆV[q¾—á” £F€×œ6ù“*D €ÏÜ;¦òLÁ™¯;¶|3ž~úiÌe.…½+ò^DÐù ’öÑGå)”Htn{0DK…Œç²° ñ|,Ò%Gzg~ã2â€޶Ä÷8ŠÄâ ÌwÈ0Àe1sbGO€j>õÔSèƒxÔÈIÅO•}›aè‘Wß~ûm?SêÎÊ‚ŸÚiflKö#-Œ‰;i#:ÜBdjذa ³y¾¾.;‡ÊðN8Á3ŠâÀƒï´¡Á À±"Э[78°ñÃ?$À:‚ÿ”0š>üÔŽ° êwK½àÀtßÅPfü˜ü¹Èô °à‚ôî@ Žì¬¶,°scŽŽ8äËüÌhÍxÞ/+Ìö&|¯¾úªO@{b„ÔÚÔË¥r–l\ˆdvë ¶¿òhذa¼ ŽžK3t¡@ŽÛd  [Ðð-x…Q> »&/çx OiN8„¯õ’n‘Í:uêä/½!@2òÐ匡_YÂôÕ» ÷jà ˜…-Šá8œqÆȨ.¹›u1œK»‰oÄb:ÔoAÇðþûïÿßÿþçn]ÃrI5,¼5t6Xk}ÄèÝ»7¹Dq{ã2eÑʯ/ ]ÝÍ™µ CÀ–Jé „£÷O?•…¡?ï¼ó,7€*·Ìû±µ‘ù_ˆØ²#-yì­ÉfktF”¦Mª'ÄŽ[E¨µ{‘­6b·jÕÊAA€ ”Ï?ÿ<áè/`çΡ 8 $ ‰g“‘±Mö5ϸw„¥“¦M›R˜ð_êðH‡‰õS·-bD}¬R¯Q†nè“U  @AÈ·I“&~væÅi†E&û¦³rí'çË믿ÀÿœsαY‘Å8þD¯,¬XÞ‚ytÄü‚),„€Èv½Ä¾äÉÔ§W¯^6=u©œÐ0zòkÙ²¥£tbYŽå+ë&RÌ  “0KÈ£kSÓñµ` vñ.ÀŒ„ÕSÒš F¼¿@ŽÆ À¬g»,œŒÎÇ"]òð* ógÒ–õ­·Þj m6F$¢,H—Ex€ù·+6³^·ÀÄ€ò aV䊾‚ZÓCð †þVC ·Ž†¶&¹iœ…²šÃŠ&ÞPvEò”yó`~nÂæg·Lâ‘Ðhw¤hxÒÊ,ëþŒg;›tb¾á² ÄRÈXØb²AQ™¹âüŒ9·™¾ã†”m&¬©a{BõÑ^ÚÒò@x‘\ ‹&V}Þ#*H‡¡›Ã:šÑ€*°ªØfO‡Kî3Z±[V@ôå—_"t#/ÝÕZ±Mݥ،« ,´¦%ض TÇus\à¥T¶Â#ÂL‚œ,R þp1bU`a0±2Ç(Óo)$À{j8Å3&\£Àh“ ˆÞ? °`A_¥¨t’·Þz yÛÖOÃ`[EŠØ²#-‹[Á›7—µZúRëÖ­S”6JOˆ ^v:Œ¿ Ì 8|òàã Ã9%‘¶zýäû%è/a0\3|±âf-–×<ãÞsÒÎ0Å"渴Ƶo+eNv £jŒ,'AoãŒ-›:¬ü@ô¡ÊdGhPD€]NöM÷Ýà³AÓð"6óBñä)˜ð^³’hÕ‰eüI¶²°2ðYý7¶º ! rÉÀTØ_Fe´å£‚‚ÎMøœÌ#¾¸è`‹:‘ «É¨ÈKÌŸ aV‡„[8\zé¥Äð;v,1(TM¤a®ÆøÎaµG¦¢DÍÒ¶m[£g"‚á¢ÓH;á§L:™ñ3$mJ`’c8 g„ˈ+S–ÕQošèEŒÛœzœJEéh$äÓ…bÓ(ÑÀX¨¬Õˆtµˆ"»2¸£’Ð –lóƒ>h1Èö]ÄrQ™‰ fiÈÙSšƒšòãs1w£d^k|°vv:ÞöíÛ[v®ØÓUàOkZ;“ÔA‹§(£Œ‰»:˜˜víÚAƒ¬èž²ÊÀÄùÍÊ]&óf8 s’Rf&ä4ŽmzdÈ/ÖÚô™áß(‰)ØÑXº$¬èÃÄ4ZùǃŽ×Y˜ü™ÀI¶¡H7i–°Y¶#];ž°Ðß¾üGÈê>&äB!É…¹O懓-$Äɲ5%?-bõ2þጄm&G1è.kä@b?]L ÀW°{÷- `Å«Go±Í’òXó8ø·g´ŒZ> ŽjÈñõ×_·È‹Í¼œTnq„´˜äÙ17å§omÁ£ù;&.à`bÈËéÒc€I„cH%TT–¬li“꟎³LPçåe$·šÏÔ×ôGh#â#¶lx¤É'1¶&+¹þ;’¢´Ñ;pŒh˜¬Îî«82]üù¹±‚ï‘ØŠM²/ ‰”ì†p`š¿÷~ÅòšðO/d˜$P5>7®l擉>æb’‚Ί„Ù› Œ˜.d‹Âˆý.¹ˆ>tÇ8ªD„"¢Ø ÀV†(oº/[sø¶¬}ØøÃ¦cËø½²Æ‡…-ðg壤°B '!°ÿ#ú&ñc¡E‡S„òéÅ?>r&?².Z⑬cÌñØF[¾²,*CŒ„C€µLg‘kð0÷5ÿLŒé|ƒ‘*웂ÓDSn]*„+ÿs…a³žæ~Ó&|#V9¤H’–ܾjކáÛ±MQßË=Æ)JŽ'!GÌÌ¿]$Ót¦æVf¦}ˆñNîE5á†ÜA&fˆå¨&0rqG“#s–{ R`XhpñØ­¦]˜Î2G40Qô™ãÈR`åÂæ àéÖààš&¼Ìf’g¹°^`[ÈÏÚâ}Q'œ†ò3ç"ç ‹iÈ0[ 'ŽÜ•™+Ót–0 À0Át§‰Ó‰<ƒ¥j=&^LÜye˜¯ØÚê‹dɆ°¹­"`Ó>V܇QÚò­ðl¯Iª×õgW ` €¿ºÚëB¾q‡OK!}¶±TÄ–œ73»ÅœÄÅö®…×Ïm¬…wÚ@òè·VqF0Œ´¥™™fŠÉÛ^”œf‰ 7SÝø"±Ë"<,ÿð$~ o4B² %¬Èø¢„]J–fXws½7JÚT<²ñ‡†óÓš‹_õŸ¦({kÒF|Ý|æ±§McK¹LYúAÚ1ƒªOgæ–u=F–ÏÌZÇÖÑÌX×%Œò²#† ÒácÊÄÃw„ÉZ§­¬¥è5wÙ¥2ŒœT“usF!ÛøÀ:&߸_Êè®Ñ‡ŽèCw†Ž*ºè+o®ïf…$öuæeazàEïuÑ+kÅÀàˆ€}¢LO…€Ùä`«3o>“Ø»¢Ü3±Õbv×$Um§a> ‰ÁÐ_ñ ¤ÂE1Nãä¶­È’½M—‰‘åâ¾%>Ï@$êS#¦vî;^H7A4=ª#p™>ÙÅ'`ªm2 W³?ô)ùæ¡!GOˆ‰,ñ©ÆÐx"ñA çãÊŒâÚÿâú%!ììñî–µ|ÂI‰ÇŽŒ :B,;ÙØ†® 3° ±3M7ƒu¤.÷TðlÄiÕ8——ËFÈê,Ye#r³Yš>@`]8о´&3–ì5±Uüˆvõ¯1¡]~)-’“'LUS‰+U”Ò#zf6Hc¬ŽÍ<Þ›±ÖFN*‰é)¸…Ù¶ù}˜Ê^“@_ŠHK$².Ṵ̂kÀÔœ¹¯P°o yŒ‘C,…Œ…-¡üèl3JE$¬`ªVHÇÁ†ßA·{{žhZèQH ±§ŠHi¶¬ ô~@ͦö„OU#&O{$#9ÛüP!ÒKm b*xšcÛ1Nr{³l‘.ÜIìË’Æ5‹Oÿ6-­™–´~Rƶ˜žÃP¢Õ¼[£­es,0¿b¬f‡‘ ˱¼€äÎ+Ì`…ŒPDŒÙ?°7(úX^þôBŒÍSHøìãµ1ŸW›ÍP·ˆeèNݨ’¾¯ƒ ’ö½ðagèf&Æ_@ãíÓ„‡£TÖˆMÞθå§ð")F!g’ÙŒŸ §¼µ’9m²–í>µx¾Xn# __óÁ#“îø’_nô½þÏÍþè["Ë9¿Nâ2å€1Œrå«`JæÄîÌ “£¢¤Šý–®Îî#4gêl˜¬XE\Ýí7ȱnç§Ûª=kªo–“@öÕǰé!Û—Ò¹}2žØ2a¾Ê&ØÀ„gR9¢´´v¿sÒ5MŒeNŠ¿Ò¾²IÑ™]Ó`…N‹Mf~È Qèhtäg˜à[J”É8ÐdÄ-è±Êcn·b–‘[²[Û?éz¾uó^“TÁLŠC½ÃÂDDk_”9Ÿ"A†y3› cß>K!cakqow `©»µ²¡ $·s¡xû­17÷¹±§ŠH‰í zcêÎPCU¶¼šÈáèSÔŽ.Uì\90†³«'y±¤bòmV!FÌÈlþ\ï…üåKhÂßñëeÛþ¹ÅRòˆ4iiÍ´¤ &F4He²0öÜÚ¦q3WæSb_gÿË †SÖ¼ØÏP g¾/æÉŒGVÇècd_z!ÃàÉÄkº20K¥Œœ¥óØ¡ 2ÊmÄ¡Û*•ÒQ%üu ߈k¾±TÄÊÀëÀÙ/ZF¶ˆ•5æö¢ÙgÚÏNa! „@ŽA ¯',-ãŽhðàÁLÖÙôëÎí¼è¢‹|˜Ž£¯cRÅLŽ K¦tÅhÓDD#Ù %|p΄„LZfÞhº°4#:]šs#æ…X=!wa¤‡v¶–‘› „åjv¯áJÚ/C ÌwÉH$(ì© ðŸ„vÎȘœèSz 3K…‚½;?°1/LÑÌe «;"ñ=÷ÜÃ|d³fÍle †oYLª$·­À´•ù(ŸCæ(/¾ø¢óYåhð–Á’9îRΙmÛÙ9ÀöUtLz|ùÖÏ—&°[4Û4†ëèÕÄí¤OœÒ0F Ç”Ú¶”GIŽ`ÆS ‰( ßì(ÄGH¿LìØèˆ’Šv!Glø£¬ž UÒ¹´O¤õİ €ÆMªÌl1õ­Ùm¿bÀîFanjü!¶³ÁÜÄY#XV+ÌíŠ_·a‹nlïQEú4,¾8[VSã=ØOžTØôu¶sÏh¨¾ 6I%‰¥±°5>t] ÈŽ1„Û´8Uâ‡~³ü…³7s„“T½’Ç04v"‘OLH©Ú“7—×–Þ…E+]”px·LQ;úå‰1̦ ±ïô>JZ¬NØ5ʸa4Œ-tT7Âi1’d °Œí˜×rë¶1Ö˺+V®çãaža<ÀÍòJÅ5-­™–´¢Æˆ©Xsa¾ ŒÔj’*;ƒ ³}—ÕO÷QŽå„'&>Ö|i)>Ù|V$­„±¼æºp›^ÈØR 0z„1>¯´~às;táå ÄDºS7ª°‚ƒ19Ã)¾,;>ý¶Bá¿<Š¥"44FïX`¹ã$IÈ*¶½ÂnðÔ+âmôÊZ€3#rP¤B ' ÀXåçV”Uåëˆx@B7ósªEGɧ”Qs;lÃ=õý‘ÌFsGc$p{Š#D§¶GÌûyäNâq¹LœÜøäÛ&RÄi#‹˜+PKˆ*Ûqs$dW‰[]¿.~vî¬ ß“­O€t„›T1 †è áã£á¨DªŒ¸AÔµ…}Î]1˜²P€ð¼˜wúy9zHq®úá y„”hôÌ`e `;¨­Ý#ÿ$iÚfºrš‹$`Jo?Æ›ÀO[³F`ñær™‰µOæ‡Y• Ø,¸X¤ t»uæg,ºû©\¸cÇŽ e)²v­Ö[ÜIvHJlfE¬‰°”`]˜Ö T8&¬’Ø<ÊÉ\ËdŒTY"Áë•£1™@PàðÊ –Ä ÙK0xÿ"#Kh°S+fT¶:ÃÜ—ú2‘Bn·þ删d  [jͬÀQ“//‹!Æ Å3Ͱ ‘¶¾Àª–+xÀ¤\Þî»ï¾_`(º rµO̼Ÿ¬£ÔâpÄì4¦¶,” Å彿m@w>MŒÅfY*|Р^Ž¹Û€‰ýçè`Újç!ÅÈ߯©…ÉŽf ÄójØÆr à ÊÑ©€›qÖ~„™âc˜d©œÌ–-üIšÙ”Ü¢§ pÆÔí›5nÌ™Û9zôºþ¾bf],o;>èñ\a˜Ï!¤ñˆi®qCAí(-À$›ÒÚ´Œéêh¤hV»¡gæg4“S*hê1:rÇÀ‚ጫN,ãC²•e´„9à;¶ ! rÉÀVa¦ø>E`ß×Vñ”±’Ÿ+n¡ä#ÍêÊÔ˜õ2 Ç”sPl² à;̈’_ ÜŒ¼\B¾4NÀs‘.À#rd¬‹a%Þ¯HÄäÐøY–JQxªìøøžr M£¬›’ðñswL‹cÄ$gm¥_Dx:~ xʽ¾~¼…QbSpv’Ê‹6BõJ³šÎßÑ»@Ä„Dòs4áægtDñÀ£p"û ­h ŸÕççǦíhÖ@¤èEÀÎ|ѧ³8‚Ù=!Ðľ~ý }8:+ŠÊ, hRíKÖti^§ñ Ã4í:/¬‹$[Gwë躺זۤ(ýT„£2F¶ `J ½ÏŸ2ðÈ!L‹DÇÍÑc €VpO‰O¶ŽQyÞ,ÞÁðn™l±ÍÆžþà c;ø:`ÆÂª }À/j²ülí–7%)Ü€=©GŽ™Zë€ åñWî 0¾»øFÃ'Ð?} @‹cþêpT$¼"¶lÄH—QJ[Ó%$Ò´;pìh0˜‡ã O¿‡¸âÅøƒŒCØ%w(¯y8þ.UZau˜¯C`µŽùò¿Í4\.áÐE,PDùa˜ÔÐíò"eT‰˜/IhÆ7žZ0âëã›^–ÃX2†kŒãC”ÊvíÚðQ8ëV!“ÈCeìRýcX7m$Ÿ%FðTóQÂ܆ó-t˜¨ÎÂÝîî ÊLv\£|æˆì\MU-Y°ñFóEýÁ'FY„E1ÊR›ýû¹ Ì'Mšä;È`{xÔ§¶„”«‰geñÐÁ"Šb;ï#žY+/! „@ÜHÆ VÜÊ¡ŒrØqaÎú ò@n«»ê››`'ɸíEå‹ï:laØ<Ò²eK¤_¦ûýæfpTw!¶ç–—‚M1˜@c.Äf^ öƒpΜ Ê8pÙ…ô‹qI¿²8 !H«ë)Û‡Éî_Lž²B•T†ì‚¶|8Û`gWRž¨³KEb)§4À± ”Khp‚……! .W_ö*³·§e΂{¤€Èm`tÍAø¬âáꎫ 4¸ñ.^tD¿ M¸´s¶Ò‘³X ! ²i€© [yñ‰…9«R›¥ª§Âdq0„¦„ΫG/mZŠÇ®`Œ½±6§>ia¥´9V ÙŠÏž=œÙàq]#g4«j‘^°W–1“m2¸ÄùÛ¬Ò‹³ø$…6Yn_[R4ŠB@äÒAÎ(¨ B@! „€B@!ãÐàßĪ B@! „€B@„¬~ „€B@! „€¹ À¹¢™UI! „€B@! „€¬> „€B@! „€¹ À¹¢™UI! „€B@! „€¬> „€B@! „€¹ À¹¢™UI! „€B@! „€¬> „€B@! „€¹ À¹¢™UI! „€B@! „€¬> „€B@! „€¹ À¹¢™UI! „€B@! „€¬> „€B@! „€¹ À¹¢™UI! „€B@! „€¬> „€B@! „€¹ À¹¢™UI! „€B@! „€¬> „€B@! „€¹ À¹¢™UI! „€B@! „€È—¾,Ý87}Š[VF rÉñ/Þ¾}ûÈÔ®ñÏ]9 ! „€†@žÛÕÜ쟀·lÙ²qãF¦ÎvQ…€B@dSL.P @‘"E¨4ÀÈò‚Φ ªb \ˆ€à\ØèÙ¯ÊfÿÌ•/J`L Ñoݺ•°=Ê~UR‰…€B@dC€‘u‘{);Jà;w²< ë¡" !{œ{Û>ÛÕüÌVoáú9¬ØÄ„ŽaHú'ÃF8ÿ-QPPðPðÑHf|à𣇚”Gô5;,ùãð±SXl€€Ó­¥K`Ɇ9úË=¤KŸ‰…‰?×kÒsìÌþOií'¹oØ%¿,çbD`Pá ÷‚> ñÁ^á  ô¡^“—ú÷ï?|øðï¿ÿ~îܹk×®Å2 -?……€Y¼Ù@FW…€B@! ² Ìk³LYT! „@Ê tÊðJwê„„ß}=±ò‡slÍtgžMÚgÕÿ¸N´°âúŸ®jûo]Lè¹:H8XÂ!±ì1ÔÔÔè)ýnò ægGW! „@@@p&7âì™sniÑöÜóÏ0üݸeÃú ƒÞzLíš\\?n™Æ’‘}eíÊ4Å}q „n÷îݽ{ÏÞÝ{ñ}å¸arµ{×n#CF8½ôö"áྒ)z/ðwýÞ=!ÿ|ˆíÖÀÔU!}œâ¶c—Kv?ìnüØ Ïuyñ°Ê•~šöMŠëŸ‘ LÐÅ©?;ã×d`‹ß»'!a[ÂÎ„Ýøv¥Ø½c÷öí;\Œ á ô^Ð4>Ø‹ „ƒûJ¦è½@âe³k×nd`~:èÈ:’®B@dw$§¬§ü6µQƒk¯nÖ¨wŸî)K™•¨O>í„‹/opÒ©'d¥B…V—ù!ú&$$àNƒ+aÓO`ÏÎm›6ܾ)®äÛ7ïÚ¸~óÚ"ûc6mØ"ÀÖC„ƒpÐø@Ð8i/‚pH늮CâÍŸ?ÿŽí {víeUÚ4ÀÒŒº !}œ²¶Û±} 8„6eɲuÕjUÞôz+THFÊÅLkûöí›6mdd`§=Ú±yýÚ¼[Öo_µj•+ü¶;×­Y¿*ßþ˜õk7Šp„ƒõá 4>Ð4NÚ‹ bÇaýÚõÿø'_¾|œô»}ëŽÝ;÷°$<œÝ-à ]…€Èåd3˜Cçf͘³tñ²Ò¥KxÊñ…‹¶ö[µòŸ-›·ut5n-X±À[¶lÙ¸q#§æÑž[7­Ï»mSš5k\a¶oÚ¹aÝÆ5ùöÇlZ¿Y€#¬‡á ñ> qÒ^á6­É¿¦@EŠÙ¾mÇî]!˜ùƒ¬  C]…€ÈÖd'xÒO“￳Ò¯!^¢dñAŸõ;îø:Üv¸·Ó7_~÷çÂ_¹¯ó¨OÇAé2¥ßýð€¡ï“ÿ¼çÖ–,ÚϤ@ü?ÝñÆÛZù­8nôWÛ=¾fõ~«Ú Ë?ÿr·N>îìã/4²ï'üXÿ¤KŸqÎiCF|@€|=îÛïþøò–ëîükÎ|b:?óÈ­wµóù¸g»ô@Þ¶„\k×=¦wŸ5kíbRˆÎ¶ÇÓ½>üi³VM{¼úŒ1ÿgÕê§]Nxâ”ñÀ2íÏWž͹œ=à“¾FÛT/IX•°*_V0FÌž%ÂÄ#ýrݳ{Ƕ­ù¶ïÚ¼y³Ëb×ö=Û¶nw1Û¶îàë!ÂA8h| hœ´A8¤‡m¬D#‡úOâ/ßáÏ_0>º ! ²)y+Ò±èK7ÎMGn>«íÛ¶ŸU¯n]žëõT­:Ç,[ò÷7i~Õ%W„„Ò6ÍnGþ¬Y»ÆÊå+o»û&Ü;ùå#Ç*\h¯c+~¨±bïÕ_W¬x±OŽfæÒߨ1k·CGöçé% /tO-`Àæyô·ÃýGãEI\ ¹PH8Lž3Ñ"G|’„€M,dÙ>Ûë)Ø"Ÿ“cÿaïFîJÈ4:¶QÊ“Ž=‡¥eÌ«ê5éùÔ˜Ö>Ûû†]òË¢q.fìÌþ" á`]B8ô“ö"‡ôÅ/rÿþý‡Þ£§vï4Æý$ÆYXi‘K½&/}ðÁ<úþûïçÎë?²2è*„€ÈʤÏfT䫌þ•.SŠ,~ùa’³LÏñùÞ]«y„Å㥰e›æ„¿ùò[‹A7;é§ß0™fó°K‹Î–}°ìà]¶t9‘‹.™;{{‰M 524Æèr]’(¨S¯¶O€8íûKdÿLÍZ!­)Ût}²”†“eÛòÆf'Ÿv"}_ïÙç±»ïs½»ú%ñsŒ[Ÿ^a! „€B Ç#Àü5Ç×QB "/»Ô¡ôâ+.7jüù§\Öú–·Ø9 _¼Dq?«cnWür[ÅoÑÂ%\7lØÐñ¾Ç#ö_òå€xŒð¼p~h³nÍÚ©Ü ‹m³ÏÙ…§N™Ž¬V®Æ«Ó²%ˈÇã”{šê@¶Èº(/;§q§{Ãÿžë8lS]H%B@! „€B@d² ^oôëõFïwÞy­ß+/¾ùz¯·[Ý|]§n}GÇLMÞºuÿ‘E& oX¿ñ·IS|Jöèb]=уôßËBz`\Lûi O›2ãþ¶ð8 “R¥KåÏŸoㆄӸ¨ [$ÞÚuŽAH&»‹’Ó`§Û´`¢´B@! „€B@LA ; À¸^¸·}Û;î¹åóa£ºwë‰o*öÜ>úäCI·ní:Z)´ç–‡qÅ0˜ý½‰.åÊ•%vÍšýþŸ#P¤$jÆ×_}3ç3½ðr·ÆÍÙyK·µºï\ø–Hõ/F¶Ÿ ù é`˜[w~¸+;Ÿ£¿”RlS]x%B@! r'ç*5wâ©ZÇtôùŸ+—dÈN°U•ïµ-¯>î„:ÑðËÑ_ûðªÿøþyâ$n9Ø"«$nf‹/þ–’+W9âéÎLŠÆ¶ÑnߺÝÏ(©ð¯?ý¶aý†FM¯¸®õµIѤ">¶ÿO>òlÑbE?;‘÷×ÞÜúÖë£gÛè S÷Ô6¹+˜ÛGÃ}{Cg ¹˜Ä`(F %ÔìEÂ!±ìÕÔR×l†“”»÷ÙU@!ÝÈ6ðâEK±Læì_C¼H‘ÂJ–*á7Àƒw?R®|ÙSÏ8™Hzûµ÷ -M•ª•O8¹çz¨;ôÈmÞ´¥x‰b„ñ‰…œŒ+,ŸfÁ¼…Þýs}!({H®3§Ï¶„ѯ»vî‚`Ñ‚ÐÞcûaº<ùçßÜ¥òÿXØ"ý®_·¾S×h€Ÿ~±K£×>ÿÔK—4¼cïð\cÁ6ž¢ÛvlC]%Koõj‹©ßÍÜïýñSÆÚ„äÒls…'Æë ºÁ{*9îÝ ºsýúuSøÀ;lþlb‚nh6ŠƒôÅAqÐï}@¿“ò"(aÆávàBlÁÁ@H,³¤4=+Š€"à{ÄܰYýµ«×}ûÕy8”0´¯Í¦wìij¦Í›7sáÒE$L˜ê;âóø”›ÇÆH¿­šÍ¶@Ðãßæ-‘øü…òµïôšIÓ ñ³‰'Øg(¼—?lªT¯80XýK8öð±ï÷x³ïæ[ùkÜ"„ÇO€»âPÚ•.}Ú¯÷v·sYÀ_ÊT)»÷í”4yÒw{½‡x’,A|G ÁýK“×C ÔbõÆ$îûŸæ—LÊy{p¯ß]N“‘æo«Ôl=Èó¨·Dß ù â—3ah°ã¸uãæÕ v4eÞ¼vÛÿòµ‹IBb®^ Ѐ£8HQý>Ðô;)/‚â6®ùÜ ¸åïïÏà‡Ã1MÿÓôº’À©gE@ðAb .T´à‚?f²´õÌ©³X>cÙke¶òdذwìÄO±U¾pþBî|¹ «ÎO¬@áü3MEs‹eò½»w3gÍl̪MâÚÏÔäïâ…KgÏœ{"{V›¡õÓõjl=´Žm“‹³jÉ5~ÊgAA¬¹5…H-ñê­Kq@ ÓË•''tš•¦/44)ÑQï=µ%Qb‡ZØûÃs±KÖÌ£¨Ä'62_°aϪÀ›AÆÒÛZ©7Øš¢Âô½<6…ºzõê7„Cƒo^ ¸|óܹs¦–þ·.]¸|.~HÌå‹þšpé!Šƒâ ßú€~'åEP€Ã’+—®Þ¸tùòådÉkÁT,0êYP|XC€å¤I›†?ÏÏ)žÓàô8_<žÓ¤M—†?—iЬÂf­·fCÉ\ƒÉ¯ tÝ$æÒ°_‰´]š”žеR_SMæ0—Εzƒ­Éκ `Ö™|Ø¡¾uŸá .˜òo^½uå’ÿ…ø!1W/_Ó€£8HQý>Ðô;)/‚âKø_¸y-èÊ•+dGs€ø¿{ª,õý°• «1áL’4IôK£ø ®9›¯´.–µãì?çöíÞïYè¢%Š„Jï=—½wÅþ™3öÎ^ãh€Yâ{ëÖ-XñÝ;A7o_»vÍyûæÝ×oš˜×5ÅAzˆâ 8è÷> ßIy‡0à¼xëæ~‹±–ßb)GÏŠ@´#0kÚÜ~]ßÍ™;ljã§>ýâÃç›Ôv‘Tß@Àa‘-‰®Þ^mÑþßWÍù} \#°9Q\Ô‡ƒGŒÿìÏ•²íÓ›ÝÚyNewð1šØ?—}a®ŸÝÈI¼m—d[Œí’bl1¶KM`¶!c»T (}õœßç}qäMQ|‡ÿýÏ/ožY/Wê“.]º 2¤I“fðÒ–/•ëõfï½½¥Mž<ùá€Í'ƒv jümÒ¤I1%ÃÉHɦ£{5JÇ-²d̘1mÚ´æ–€âî]£Dwòh| G ^•†UjTÂéσ>Ù²q۬ŸQ„a¸õBj„€h€‡~2°c·7Øø‘Ó·íІŽž¥*Z¼°ç±â.ìwÛ¬î"jÙõ^,Ý«\ŽÚr¹dï/X/ÄÛ‘š@Òþ ï…~ô;©?òD8%›ŽÁæYöbð^)‚êØÈ£E ’À*áèáãçΞgƒÒÇâ=Iµh±q!ÀìNÄ_l~™²dä/¶·BåWE@PX„^ŸUZ0ª`ΚWÓÇXöï9€±1JWÜÇ!û¿×§{‡>e VakϱGÆ‘Tß@ÀG°o< m…" (Š€" (1l¡ñþ‰Ëh¡Á1_à°IxíjÀÁý‡ØX$Uª”¶._ºÌ~"3g¯¢¶”±èò£!Ÿ†gEáŽm»èì-â²Él|¹gçÞŒ™2dÍ–Åeçȧ*–™±ð‡S'NóÈèœ@c°! æaÃMs)Š€" (Š€"7ø‹~üq\FCƒ}˜Oþú‡Fµ_?Æ…–Ï>ù²áÓ-¦ý03nöçVÝjZ¯UÝÊ ÷ï=è|—˜ï&8ÀübÔ—w]F\ ¨U¾þ Ú5Vú.Óh¤"”‡4Í¢(.X¿~ýÒ/w;tÂÅ=_‰:uêÔŠ+nÝõÜ ¶cYódž)S¦ìÞ½ÛsJ½Ë>F¿þú«bΞpíbàßmÑo8Ë!»trö—¢~ûí·öíÛ[·gZ‚ :uê”)SBƒáÀ>¯öçMH”(aíz5©hÁìß\V·pîbâë7ªçò®ËÈE –Þ ÄÌžÍÖ¯Ùè2F*a@@ p@Ó,Š€"à·ß~{ÏŠÓÉS&7÷®^¹vþø•=ÁÇÉ“'½w¯bJð&pãZÐðáæ{“8œi\³fÍc[þõPΪU«¾ë²¦WÛ¯¾új‰%"„“x¨.¶ÜºtéÒŒoØxÊ*ð©Ý—š×hûÜsÏ V†tYÓÄñ0¸Ñ½nöÔå€hý´CZõ]´hQøá’Nþ÷ŸÛ¥(¶¬›8qâ¤I“Â_²–àKàÿœ"E ܡ֕À¾ôpÃÜ–†ÍŸ#¯K|òø©[wfȘ¾B•§¼/öôy$nߥ-çÙ3æ{ŸQS*žÐ5ÀžñÑ»Š€"à[¶løe+–&múÔ&C‡çû_¾à?Î/äG+I’$I3Ä;Zþ±ñ£‹eÊ”É$ gàÀ†S³GNÏ‘#DZcÇÂYT„dïÖ­[PÀí½^y¹ñÀ‚z$BŠí…,\¸pÒ¨)3$õô )+¿ÝwÕÿÚ°aÃjÔ¨V &xpOCÁ€SKÉÓ'~§}ô R·nÝT©RA€Ÿž'z$ÐZc$ì–„âê+‹¾°"Ô¶¯»wî»võZᢳ<‘Ù Pîß{àÌ©³iÒ¥É1daáh’¤I¼ÉÎ4LÁñló¼iÓ¥Ma™ö\Åùsÿ^8±@á|kRú_¹J±y ä1^©p‰L$kIsö̹m›wdΚ±D©â&Ëæ[/ü{‘MC¼_jkò:DÍš†ÐÆcGŽØ{¿­ 0‚‘·Fíª)S¥8zøØ®í{Š–xh×’ßæ-!ÁóMëc0ï\‹ËZ÷×êõeË—nß¹íÄq“›·øýO[«s™K#oPì JšFPBAàÛo¿%EÊ Sü/_#²ÿþ ’.^¼ˆ=ðúÍÍ›¶hõ’£G~å•WB)Ô»ÛÙ §oذaÅŠ½K¹©®\¹²}ûöoÛå¥r9*qDn}±§tP…š¥’å| 1X]8~-Ë™Þ}÷]bÁŠÝžÜÖP0àF÷¾˜|otá‘0aÂÆOž<ùÔîÔ~¥£K ­7z0vq¼ø‰ã%Lš qáes¿z,^"\?ãþª›,Y2/d†#˜üF˜/è[·n}4øÓo¿z°b©²%¿˜4Ú3[8wÑ >à ~6¤Ú½õê ûÛ"å/Výº Lž"ù'cß·rN—‰C\»z}ëÆE¥õxæù:¾ÿÜã!Ü·ËPÍ]:öšdcG|9ñ‹É£¾ÞìÅFÙ¯ë»$Û~dý€žïÑj‰„%Nš6þòå+]^ï¹}ËN‰lÙ¦ù‡£†ðûkJ{ÔD·IÝAuܤQÏ7©Oö­oïÒ®ç‰c§¤¨„  üàíWÞh}ÿ2á3Ï×öý/ æüf#À"jÃfŽB¼<æÍ\Ù&Kú é*U-ÿçÊ¿–.ZÞ ñ³^f×dŠ€ÂþVx(4Bn?vrßîýOU*ëìy/BÊ…°ÂaõkîÝsìFP­VeÏßzwò¯úcÍég¸‹.¥Qó瘚u—RãˆB€Ÿ¨9sæÐÙò>åÂI#•F Úezó‹“Îønîk¯½–/_>¿Úâ°Ë’6KŠ1s •Â^PDä<}ú4Å$K«Z_;šyóæ<®»|›‚U†Ì!Н+à6wî\¶C·FFq¸eË–àÝœökÅ5kuÑŒÀ°Îu ºX;g̘1mÚ´¬øå;ã±Då±ì—’ßlÓuÙâPÇÖm_Lš4Éâ…K'Œý¶Ù³/­ü{ «L]"‚¾±k»Þé3¦ûú‡qé2¤ýuîâIã¿O™*eï]a†.³‰îtñÇ©[Ÿ·Â¿•f±…{¿ÛÝï¿ ¥V.ûóï [°øuW»»x›Y.m‘ä}¡Á+çÏžïÒëÍŒY2âVjÓúͽÞzvš(q¢·‡ôŠ/þg#¾€ˆ–*Sì®.Ïñh¤_jÜöÛHoa¿ N–<Ù‡£ß+ñd1œ32lôÀ>ðhýZK)­Q³ç¨wáœEä2å“Zž3w«¦ÚÜu˜5}­~£gHаyðìi󔻃Kã h&À˜¸Øwèı“©R§,Y¦„•ëê3tÅÒÕs~ŸVºÜ“Ô¤(K|çÎæÆ˜5ä»éyî7ÿÆ›Øá`âA¼>ßáõ–?Ι6ÜóÍ~ÿž¿ …”¯T6[Ž'ÜÕˆ?=äÇ*ÕåæÃG…´„Mw5j¼¯"°aÆþù§^½z‰“‡²™düõòV®ŒXÖØ©S§~èaÃäÖÍ;[7î<¿3ˆÕ³Ù²=´¹7=vçÎ×o“eß¾}Û¶m+V¬˜ßc~ÌíØ±ƒñ·P*žNÿ_[Á~d ‘?~sƒËC‡ýµìï³÷.Vd%›¹e{¶ñÄ8JEò³G.¡ðÄ”T$¿x6ÀªL»xþòM§—Æ[Z´hÑ̙ҢSØ?tÚ/x. Ò¸qãFе °nݺ¿6oH`´…Y^ûÏ¡‹.\`,kna1¸uëÖܹs[›F“÷îÝ‹I¹IFH[þÚ¹ïäÉSEOÙjÇgÒ®]»®_¿îŸä¬Ÿ{% Ù"àæM)Ö`Ň”GC¤­XIæöÆáýGË=$‹ß‘#GÐ,øÐNÀø=°tÈLÇ8¼éü¹,ÿÚúÙ¹uôèQÆô\šaÓHÓ‚nÜA˜   ?ÿüp®§¾*²™ó÷þ;°çpþ”L×:žïÊßÖ$»bî¸sûÞþ]‡Nm¾–'O›ØtiH…5±„/ž¾J_¢»‚›Þ+A¶4·oÝå¹ûûûW¨PÁúølɤ“c|qöP‘[¶»rI£èǶþïºË¬{gµçÁuçnßr¼}zÄŒŽ×¡çµ‚€àˆEcÍÊ¿`¿ÕjU1ŠSÔ¡Gýý·åÓœÙæõV.«c¥(_’¡Ÿ ¬[ÿi”yªÔÞÝû±žÍ7—»-yH–;_3$Ož,Tš:óç9kV­³U}æ”CÙ`ŽÔiRwíÝQ.±Rþzܤä)’uêÙÁ$ˆØÀå‹—ÑC))kg¼+QÂù§/˜’&m"™èÖ¾2l˜Å)/7}ëkxlÇîoˆðï¿û1_Å)¿|-:êb%‹0~¦j£Ï†Ñòåf¢j._¹¨’¶\²tˆa¶Ø?7læX!ì匿ª5*¥MçhN½çj¿ÓcЪåk.]¼$ ô²M¦¸D Ú0¯ÏGŽŸ0ö›[÷P™VìÖ¯“ù|ìØº‹-k?\Ê"û÷<=Øý}úOóÓîDbNî—©s¸Ëk ­u—ŒQÎæM۞Ȟõ ¿ùýïa^ä°n׊{wïU,^“᥹«Žø½‡þ0ég|ËjÔ®fMÉúêeê•«Xfæo?Yã5¬¸DÿÏÄ.\øžßn— l‘ýúõ›0a v×ßûü“ÿå˜å~~ü9Fö‡µ0gÖˆ–-[¶t£œ‰ÌûÁ׌ì÷O_7^W¢~ V*.^¼øæÍ›?÷]7õÞúÓ§ëX—>|.ý€ñJöV­ZA¥ärJ¿T#GŽ„“›êüøão¼ñô€ðÔ!i?øà*µ&°†©:gΜsö ó¯¦Ìž={ÚsìËN “ÿ‘zõêE! “ÄïÜØ‘†â,úÇà†W©Råûï¿—räŒ øI~{z‹^xaÆŒ™¥`ªç¦Ð.²C‰%òêŸo|õÕW. Þ¶oÜûU—_÷ÍnñǘÂ'ñówã~nݺõ?ü`"GŒ1hÐ f(üò:â._¾Ü¥K—Ÿ~ ùü<4;b`ñÎÜÙ½»ÿ}øöè3–ðù’ìÅkæXÏ”ôP€Ç÷VãyKeþ¢Ÿ«][÷ɬXyÆnAðë¨í“ÿ™uUÁªU«Jq·o*TˆÞ£äJ$ϨH‘"üdL_çøÛd^ðñK-[.™¹{|×¹‚o„ýJÞDIâ—Þ¨\ŽÚri=C érZæ]y}MƒA­ÏŸ?/wKÔÊÝÿé;çóG®¾Ü¯cß¾GÍóM”$ÁçÁã^åï½÷Þ—-ww©äeB䵞Í2Vr\哦í„*rWÎgNœû¬ÝÜ…yvÒFéö9J¦mWëA’åó׎øó°ÛއÂôkpÏ|pׄl|ÖÀÆ£GöÐÉçØôÃ?tîä´´@¼æÿœ:ç—ϯßG€wJ¼¶säµ\œ½Ú¾µµŠjOWÝmÞ°ÕfÐBú<ùr™\•«W„£~01Î9³mÚû§s¼s ZPÿ+þ¶øÛ·Sc.}†B Q“fÊ옖Œcĸ„ýR8t”ºÔ8ñ§/ 9¬Z³2· ¢a¨=(0èõV÷îÚoe¿µqÝæâ%‹Z-´ +ˆ}2„ÿÔÉ39se§.ú }Y²‹+,C€Åþ³Dï…—W%Yh`ͺÕÑØS¬±¸ö¾4M©Øˆ6Ü«S:qŽ\Ù_~ýE4G›9uÎßë·ˆ|ÿœ>{ñÂ%–ø?žäq›Ä1çrÕrÇw“¾UËVß¾}Û¥±ñ¢ù¿Ã~åÃôDOtôðÁ£7®ß¨þt•Ä'OY¯uöŸs|¬ñ³çyvvÙ’RÑä ?Ú0³Ü*V¢Hx$ѼqÍ›7ÓX4x'¼#Àh“pz„ÕôÉ#g2Ü'ÀÍš5[°`AÞò»uîQ&O5î~ú駇Ûxó«‡Õñ­7ÏP«V­Ê•+Cí2j‡]ä*“áÐús¿üò ´Í<‚éÓ§¦ ‰Ù½m_ûf=ñ_:~üøÛéþ]»mÙ¶™ç:wîÌ@¿C‡ {xùå—±ƒhþús·“_üïh|¹ÔK™T=vìX”™ŸþyÊŒ·~­UöÔùë×ùñ&Í’%K »(Ó|ÆÊ‰ÄÀOªU«Æ×£A«ÚIrݪëeøíï¿ÿ^¦L™æ£JH±æ<ùíßï$0`@Ö¬Y?ú裓ûNí=âÀΣˆôñǾ¸ýû/f~óÍ7åË—ýõ×M.(Q®Pü„ñÖ¬YsõêU.ñÿt|r™80 –KÈ6g$ÿf¿ƒ*ã¢ù¯¿þây•ªïØ¥= N=˜°î l]xìÏiš6m 2а¯~±råj²x8þósÌ X±Êš=s×N݉¤Æc÷6Ùòæ*•þü᫈dð±ça¿$#²k×®’~åÊ•xÛæùÆ‹ç0Î42³Îüç-#ïMkdF>íƒUÝM0uêT¬ ÐwÖöü?!V3¶ÚåòÀÚ³ý¦ ­S§N›6mP\5|ûò#Ìkð¸­éÍóÍT Å®ýÛåVóæÍ1`N“-ék¯¿Rõɺ°YV¿ùÁ5Z—ôŒ`œ9sæÁugýž~PتE¸0Ý•°uâ…ž9âíññ<Æ„sóçÏwàŸ Ä*Õ”rjÿ…ʽ+K'‡fšöú®ÙÝurŠÚ|má­ƒ)ÜurÑÏŸ>ñ)_qF†GM{9AEh\Yíijd@HØ—ËšÍ1¾Z¶hEÞüy$×ö-;ä¶Pb‰ÛùÍ®í°+¶åòöZÛ"¹dàO+W¡ôK¯¾à|7¢bXºl- ý3m¶‰Ä²’0^ÄLŒ÷üÁý‡›¿ÔÄè~É{ì¨ãÑ`Êôv÷Ö¢â'pP žŽ`š5pà9¿½û~?ºÐé“gг$8O¾ÜÖŒÂü$Í™1ŸÆõÔ1É(`gM›§Ø`¢0#=xõе°_Ìk­žÃZ‘CùÆq¹s»CT40&îé²ç|"Wž\0á{)^Èö0ðÔofϵêÖ`R³XÉ¢¶ÖËÛ‚9gÉàœ;·9Ð+þ¤§êŸY¾Gï?W¬y12‘„{.Á*¼†ã8b÷L€½E³[’ž<úO†Š¼|ùrØ/FÔßø¯\éRårÀãÊï߿޼y[~?ä÷üƒb±p®Ó°Æâ9Ke~ÇÙmRÁª™!ÀÓ¦M³`Qœ¢ê”‚P £vƒAQ %\Nš£_Ó1O>ùäСC {D«Fb(eúÒ~¬\Xo ÌÖ0®YBÔÈ\É’§¼Í›/Ø”Š8Ç‚7¢UFrÿÙyŸ>}°Ô`™l•SÅëõ^git“&Mhõ¦ÙG^«f)ÚÏï†Ж ›XJ,¢¢_½tFàht±j¦—üNα]®i‚5âÇåy2óþ § ØÂ¬nïÝq€4ØE;tÈÁöÝ„7mÚ„f…ªß~¿¥ VÂ~ñÃD®å§m<ì +v¼Ç·9üÍŒ5*{vÇÄÓÄÏf«™ÀZ©‡°Á ÿ¥}ûö•”ÇöÚ pžr6ür˜'åÐHG¶†p06¾2C¶ÿÁYIv®:jdfFc[òŸ_ìÙ«A"ó‘}ǯ_ |íµ9HŒÙÂÒÀ’ÕJW”Â]ž/ž xµSËÉã~–»ÉòÞéöÂ`¼"ã¼Új{lžïïû~ÚxÜ16åÑÀ~ÑW×¥Ee‡’ùù矇œ—+WníÌÝç>=‡ÑþK/½޿欵êÕ‹ØtWë-ÂÒ3u«ÈÄ—mÛ¶5=Óª¼]2ñoÓÉI–ïXÆ–ÕÛ¿\ÿ-—œºzÌÞöbß^æïT­”›ìJ€!Ììó1½%48 øçŒã]@÷hó qoõÚ!f Îb´i×êûo~ú|äW§OÁw £š% —•(U̪«tÎ1øL†=Â܆}_~¤"£ç2y@DZýpßq¿.Ź”û1ØC€Í_Ò©G{ÌÈ%šA#+—ý7oÜv?¡ãÿ)S°Ä/¯…ÜbM.VÒ±2¹\…2¿Î[L2è«5—çðº?7œûç<ãÒ‡|jRòsI.Â?Wžœ&^Š@p¼0QlXëå`Ø`Ø/—|dy…D¡p¼B\òÊaúB™õ—Ö3oÂ’_—±œU"yåÌ"Xb˜c ˜…"%N\ņÃ0½Ä‹wÅÉÖÅZ#áû|µhÑ`Þ+—¶4#?Ë ÜwPÏ3§#6iŽ-¹¼ßdO¬Õ$ö¸_”'.½+xŠ5ëT£á¿Þw$(%ïˆ86îYT½ë°U) ‘!²—-B LÊë×nHz1ÁE=eÍæòÄÞ»S¹•<]âþv÷0°@gˆ]Ë#Oœ8!Y.œò‡œ îc…-1þçonÛ´ -+ìWpæ.ö«gΜAÅÇ噓g¡²,Ç…²š4p6¬Íå£`‰~É>ÉšR"!½¦(†,Âmx¨Õ$hÔ£¢°_ÂHŽá.”fM/ÖÅÄXµâ\Z‚å³q ™”È“».ña2löEÉÔ»0I’­^êðJ#âû,¹8£ –…ʉ“;è.Êvs+Âr§È)&âò\(ÿÐÖ3PJˆ+Ûn™=–!ÃH(úö}ëN’ÌÌÉS9¨)y=s¨2gÊ—²cŸ¶&Yb¹3çIÚçÕ«W›H¶çK SœÑ'|üzt¼’å‹Ü ¼³lÙ2î>ûì³¼ ÿì¿rötÈgÞçèþs¥–îJëÊšž™&]Êb5BF¥Ü¥gæ¯tß”"8õÙÓÿß}ÞÖÉóÎcíä—ÏxßÉåíæ½° £aŸG€êØ)`Ãû5Xl†?›8bé_ lf‰œ3ì,YId®Ü9§Lüé­W»ÿ<å|hMüñ ë‡Ë9WdÄ|8ø|Guíó–÷ÚN›V{Û­(»ÄظsÏ×®whÓõæÇ â/†õÕ¶çÂåüå¿dÎúÐ'H¬Ñu!3>ÉøácÌï½üb næÇÉÓÌŸ,'¤93x_”¦T\"=[_¤¹tñ²K™ˆ>Y°p~Ж/Z½]«·š×oÝ f3Éhr:p¸^•†ÕJ×}ã¥NUKÕÁ7ܕūã?ûƤéÕéízUA‰»´ëU¡X R6|ºEñ…2i`²R¹äÓ5Ê>ÓþåÎ/4hóTáj¼rÖ¶°!™…C°Céj=ðI8yÂÅŸ,Öê•°M¾Â4ÇšÀ–& °C™Ì: [ÖËÁZâBÅ Öoäàóf†Œ‰ 3&Fø¤É’ši?kF +Î`E¤³÷&ç”&FØi–ì!ë£diî”)S–ßýQÿ1Ø!s u$ýüEYŽÔY“&Iú¸%ÂÄ´Výªtc³\v×êc$BÕ&Iýÿ þþ\º$µ|6xÒü±ëÞ|óMYÅ RØHÉ’%eÝÔáÒ_‘¹ë9€ª&͉£§—*UÊæBâ¯ý{“™)“˜@¢¤)W§HH¤1f&üxrG ^‘8»< <õÍëÊ„à‰í” Eäë”,hÔ¨‘\âÊ‹€K&F|©90~ëÝ»7é¨:ïÞ½'¹"ö\¥v Þ~éß+ç]©]»6¼‘u¿+V8Öq $ýi™U¹xÆaòçNæl¹2¬µß°h¶_2³$dfÁ¶í.씼YãmÏ—[bù±5á|Err–õç,ZÁLšËe¿®âÌ!ý¶DÍüVâå,¹ å`m¦5>A¢x\šé•3ÇÏqÉÌ”trμY¿ó™µ“_9çx³Üur«*‰dY²8¦Wk€õˆKðNaæÀw5z`VÆðþÝ æi?ÌDE¼p嬿÷¯í?³ Z.|>R±š˜!(Ü;¡|VËáG-„˜5‹hPä£mìp¯ÝXI·ÏüÝH]òhð\ë EWø¾Â4µjyïN‚~›ÿ;?L[­;ra·õ/ß3gú¼Èn¾–ïóDÆ;1ÈþòÓìù³ð.+ÖÐËdÉ“~4dä²Å+»ôî8züpæ–0‰‘5ñ’’— *»gç¾^nF\&À!Û4kÇ]«®Çë8©#“˜n};‘’oEá5ÞÔøÍ—ßuhÓ…—mä—3õÞðwqðn¯÷ŒVÙ¤4!X5ã|HQ™š»|°¸ãƒõáè!ø¸fÍ蘶éwíØ‹ÏªT©SÙn…áôXu,Ÿ*wÙ…o#|­:ÕYh™ êqIŒ ýzÀu,ºmCwEi¼"ÀØ„{‰Š,RfÍ™YÒ —`‰,ª°[ö ¿å€ÕäÌ÷D¾2ŽÅ]tÔk\‹ô²î—ÀÎUÇèÌF—{íb ‘p©eﶃ'öþK¾O=õ”Ê^8ï0î%†s$²îÔèoM-¬éEå‚©›×rº;„ŸÂC2öætœmÄÅ’?YêÇi ¾‘Ñû}bÇ¥ü…1v΃…31—/øß»÷`D"Fò;ã˜(ÄU²¥˜ÁLùRMš3öé§ŸÆÖ—­bÛÕï‹ÖñÁí U &ÀÂÌ·­ßM©Ô& ‘­ØHØh­ýÏ_çÒÌÜzájï¿ï°NÄ–˜æ¯øfï­ [Ä{$žŒp8öxH¯v~ÄÉR$%ŸØMSg¬Í¥0ú-²¯ž[.mg\‚“,y[¼íòÂ9‡Q†éätoy³¬<à’C±ãe'—·û±à%Ö¶ºôÒ‡ ÷™hã»ÄÉÃç%¢@Íáø±ßØ ñ‚‚0Ý{ðž~1zÂè?Ç‚ÔËtá½1»cüƒråÀ¾ƒ¸e²‰Ä8mè;m\÷·‰G·ùñ‘eb bWa bWa—Na¦LüñƒY=ç¹?†ùÁ OÄ2£2Ϲ¼¼ë¥ ”Æ|ǸoG¥Ïe¬(„ðöd™ø›:e†­ºkWíCL”Ñ¡|Ôw‰mþŸ=‹ñû¢å´·JŠ˜@Óë¬GͺÕXWÈ>©›7nµÉ`.m}ƒxobLv ÄìÛQÓìºÏÕ®× 6ÆÆÛö\²`ÙÀß¶:ÊÇ“Ø0û_¾²à_ds¶3§ÏŽ62i$ìѱ¯Ü€¡};t}Ȧ-aÍûú‹o6˜rä[ƒ2óו³eSŸÌY2µ|þ\íKQ| ‡ ø˜ WÌL¼ ·u]¯]µîä‰ÓÆÉž©WbBLE)S¦H’4 VÖV?X?Núâýòë­PÃΟõk°Hžô±pN”Ûìýk«% — ‡r»bÕòž¨ø‘€àdŸ[už­5oæÂy³¾Õ½=5†,ö¨@ƒ`šÅ‡` Íö<'O>x==7–ÄXÿ’&Wþ'Îú9f»qì„j‹õ±?x÷ÅÒ½ÌêY–¶ZwŽõ\¬¹[²lQvñùûï¿q’|ìà©OøãzŠ*$lÒ[©R¥… #U°´Õd÷Ûè—"ØPÖP”·".”6½C‹øï¿™¢ƒ“*œ'J_hRÄUè( –B›Ê¸–†žoäÐ6C&±æý{ÍÿÄÿ^¼xñÕW_5JéÔiS]¾xkaLg]JR hÞ¥K—²÷Þƒ±cÿqÐò÷_:ë.±ËB,]¾ão86°lù‹ÝþW½zu†ãÉ“'‡7ªUÀF€“¦J|Ý?ЃÌñâ?†/1×,G ¼}щ¯³_y|ýP%1 ®ûÕæÃù3×@ö\¹Bt¼8EKž>ñ¾鮨µ±÷.ñTáé\SÜ4iÒ÷š(£át½ËtrÂì$l}³vïÅdÀ1oåe'2Ïâ²èw ¿ñ5€Èb`Æž‡áG¦úÓUŸ®WƒêUnظyÆ`×±@ ?ûج1dþÞ(ê§ø†s¶jôÚgŸ|a¥CDÉBüןOb–O*nâj=õ,;ƒ8_:Â|dÇ|=Bد¹%•Ë-›¶müëot¹ot~ÍÜ­R£öxØîšÅñRŸïÏ¿i¶´•»ÆûÓ>CUûÁÈÁ¦p\ç³ÈžÍÄŸ¼¿ƒ™©B,K8sê<`±q1rB#™›Ä–Ü…:*]ú´}ö Æ›õ´"g„8ò¦(‘?o<âqºQs‡g8°ÈïM ’RÏŠ€ „G†ÈÞ`Â.;¼eÏBJQÄ‘‘=Q½ÉjÞJÑ÷Âpœý ¥Êè Ð 3Ëî\`¦,ˆ„BÛì½o£s™¶˜¬9²ÃÖ2F‰! dj M¶Ha8a¢0¬•@¹Ê!˜ð¦ÕÛnt<ûgÂY²9x/n±8{8P›³uS•ºå‚nÜfy­‡”a¸…/VƒÓa0xÞºn÷ÓA}ùíÀ;‚s‡ü·lدٸ8u&t¡ÊŒÙ>¢ñ€MâÕËÖyì¦ÿ­;w˜£ó{ÁFJ¤Gn2J¯–jM¶kó~.YÆ,‘tׂU¦tW±®Õ ¢5½5,þÆì:ÊvwÖx±k01™³9xîä)3$%—\z¾ÌÚ˜Z4à3lZ™£Çð¯˜ñ|ç諭òÕªŸëtèµ›XµÁ†gÌ7ñÆ1iN_‚&ýã¸>{ܸ~ó‹Ñ_÷é2à½þþþ۲굪–­PZjOŸ!=F‚è²<áxq8XkÊ·šQåÔ¹“'M?fÂ'x`ƃLç×{îݵOÒȪ®|¿5ràw†ñ!E¥Lìð~¼õÿ„ kOl¹$Dš[`l _53žk¬—å×ÄZÅTÊÀ‰c§ð¢Œ)â”™Gÿ¤VÝêðöO†Žb?d’1|~Hƒ…·2žÎcñâ™ri‚g.¬1¡Ê`+œM}ßÜ‹ýJûv@98ûmÕl\ãc ÕQß®ï~þéW°ßö ÅMu š<Ë@šË:õkÙÌ=ˆžæÏH^¶t6¥™@“+tÌ>&&^Î}Û[!zxèý‰ÊóZöz§k“žïÝéìoYš»xͼ…ò!ƒ°ú ëYgw8Â-I@`ÖÏs9·x©‰ÕL—©2 ZؽÖD2ùD2L/ŒN˜Kf9ã¶Ž3vÔë×lä ˆ‘ öÕW.]ayÃñ£'ø~7}‚¡Ä¤´÷Õ¿!JÝÂÅ ý½a FÅâTÓCÇÄNƒ\ÒÏ‹{%g7ÑV<„ïUÄC‘߈Tµf%¾M˜…Óv¶žò†±{(\oÅAð …g‡8dI¯[ ®_êÛá½UKÖÂaØ÷uǕ咾Šþh«½2 ‘x~wïÜ~À=Ü–ëtƒ¿ì„5éå€ ñ<ÆV=&IÊLIŠ>YígñQÄÚHOÀl”3ovÜ ÃØ<6Gafƒ{*Y–lÍæpÞ‚¹ ml†Œå’χL`h:x°cÁU©9Ã\²‡Œ,|Í—/œ‡A-‹¥K”q|Äp%-[¶Íkw%K—ëkˆ¥)¡æ³UÖ,_Ï“Â7µD2kÀF>xWb'ö¿õ ‘:n&Jœ³,Ä•ÄuFq 3d[¬‹ç/¯]BŠEq={öì?¿ßûÖmcÿÌ­"•sàìZda™K‘ùŸ“ç¯?0¤”EÝÉS:8³»ãßc×uýhñ¼§ùÙ"Íâ™+¯^¸’8sv—EâÙF /ecÆŒ©Ù?›I‰™ÃÁÝGYEŒU‚‰,X%˦ÙGé®Ì†@6*Õ)·ûâs×`®Azæ–%‡ü‚$Ò3Olw훡væl˜)8¶ï˜‡Nž>[JÓÉñì-ÙñL.ÜæZp¾B® ³­j86"0¬s¸.†!x&gùï…0^Ú"¤—³¢ u¼h]z½ÉŸLÖ3”B=kÆu€%ÂŽ£éíÉ%‹f±ÑÅQ¥¯É ïÞ»7íû_Ö¯ÝT¨hAâ{öïÒ±[;ëæš‹Øyl#ÕyØ~òÍní^z­%$Ù”lï¼×§[Ÿ·Ì@±q‹çù3wmF‰ì¾Q¹zçCl)û ê‰í1S~Æ/ìäéþ=ˆÅ±Öø)ŸÁ¡ˆÖŒ¿­žs÷Î]këx^{Nm±>5odp.p²€TÇZ뙋¦ò¥bê{wïfΚ™5wVILùwŸø›çl–#žÅ@Ç»ëø¦xñã¹[6øTÅ2ûNoµöS#[ßð2ÆZ‚†ãÑF€_”±?Ïÿ®IÝV0ŬÅPD–§6lüÛ~ÿ9H¤qì´!xG¥ªîßwü¿ü÷•œM™…"lŽý{ÎW0/gìœ9³[Ú¦ [R¤H–&]šª5*UÚ·Fj‰9s.û €‹ÈÝûŽ w·lÓœž°±a¹?&ÙÜå⫈ãûëîáœ%Bî’y/@yæÒ÷å!ðü„<ר¿ó~YÐûÝî{vîUXÞ@­i â· æ !–eæŽ#€~«pÎĹ?a^œfí¸’¢²Ò©^Go¶˜´«ôÉk×®±0uÖœ™ º–ósØ(<ÚÁÈ^è%ÙŠTÉ ­5÷ßjÙÇWxâM“'á¹À£×dù+c£…~çw`Ñ8¦®\·ìcioþ:¤›¸²úTœ÷ZK [˜q Œî™gžaçÕ2¿O™;žÿ²Þð%|#EžòHZØÊw— ®H½èi³O+”•”ˆñõ×_ã”1ÜØä­×¨æœïƒIéÒ¥Ÿ¬Zà\À‰i½ŠíÙ³‡{Ø‘bjŸuûfõ~ù…×Ò§OÏžÆËç¯EûjÖ›B€ë2H—WyJ…L‘ 3%ŸÚíp¦h%ÀÅkä:øûU‘™•É«íŸ×¯ýÑC'™”‹~Y±`úÒkzÐ=XÄûå—_ùL£ZœÝ‰’%øcÑŸ `8ã—¨ÁØLÈ:²t™—Úa¹oŸëwúÊêqeŠlaêaÖ¬Y‰“$ªß±œu—6{2\4KßkÑ¢EÒdû9ø¬ëCzæÂ/7´¾Ô52ûÓ3³OsrÇ%k†gÚ—òöréä<”]O_ñ]ç \vrlàþwdé°nvvÙÉeÑ~¢ŽIj=|:3Òù ¥¡öóÈCƒ% Ö ­Él”Ä ´ `fL²€à…©Æúx+?”d†¾š\Î3 @Æ›ì’~숯¨ýã1ÃLvwªPJ«LŸ!’…¶ÇOf½;è¢Ó 5q°ÖTä . GÙn ‘ãÆ|èÒmwÍ%R9{øU çÇôp~ÎòXXû†Ä{c-AÃqû+õmæÝ¨ûÜÓàî;€µòY©\ý ëc1ÿà;hܬ=t Qó<¼Å¹¸2¶*{YÎʽRÕòÖv±\Ë •“÷²¨ø­íßpe¿aÍe ÛH¦8‚†Äâa@Ï!¼iŒ"éù.£ .\¬ :ÍW{w°,ö1·Ö¶0è1]j¶#wYÈ}ùCùä“a ™òå´)~ì°Ö2UŠg/â°FæÉ~pÊ¥9KŒó¯l»víd_¥Ru³]Ö£X©B°ë·Þz ßÅì•Ê­ùÿ[gfÇ“ ¥4¶X ÿ"¾“a•&ø1'L.—B²& œ%7L ÑBoY»ùÞŸÿ-÷ÛʲR6¼E ×sŽƒÝÉRæÃþ‡sØ»ýë•û©ÿ“Ò¹kÂ(ÃÑ9£ÍU&½¹Uƒs û2‘( ÞŽ;òtŽNqXß¡ Âq4éy‚e›äÚ³ôP޵OV(Z¥m«§k‰—ó}d,1ñ†þ3Ä"ó:uê°â7eêäY „ŒñKÌvP˜-g6F6uá¨ÉÈLK‰O“.•ÈL¸zý kW¯C++éé–UÚäoÖÆ­Þ†dó¤èÖ­û¨ã™8à2]¦4u:”í£¤ž—ÈcbJÔØ3˜?Óo>Ú]0¿Ôç™3ÿÛ%y͹aËzŸú‚gúšc[¬ &þ>nº==só±•_úãO?ýDzì/è™Ã¦w„[eÈV(½éäBþ±‰Æ9¶ËNNQÔ 7œõDFÜY3/“*s’ÌO„fãaòh V!àøš¸:bE#ðÌ„ wÅÒÕØK³ëlŽ\Ù.þ{‰½gÎ]„¦jµªD{+˜­\¶z؈¶e}Q)XLöÆ1¢y­+f"ðà.å;zÈ1®b%-g<°†6oþÜVëŽÝ;÷ÂV“Q©¬š“Õ"9ü OT„ ¦Jc°Õ¸‹š…s±G¹,|½y3,f—3)*Ô3/°£¢û Û…ò3L¤öφƒñb.‚!±rßœ8D×ê²dèãï;d—w)RÐÃÅ‚ç\ÈÏàµH±B&+:XKò“iS~!ÒªE7i4 ¸C€5À¸ÛuœÚuÉÏ1³r|5÷C\XYýKቧaégœÙ/˜9‚mþ[pu½œ¯§÷˃ñ*«:—îŸjœ`1ÐG-<`Qˆ-îýJü2åNC<ÔÂÄH S§NøsZ~`úös«l·¸„  õâKòÝ’QÛO­ýàÅØíÖŒìS–Þ{qýÇ/Lç.þŠPZÚÜ–Ò\/^½â;¿561rÌ1`ö‹C|o”0ÊsV«.Ø>ù÷õs‡4›sNÃF» ·}·óŸÖ[/¨Ø¬D«½1ÃWL¸9[“9‡!ìaKí­@?oË·-{©ñK¶,À2uêT¦ &-¹çüúÑmg›**½”ÿ³_¦¹ •EΜ9ןY`–­.y|s6MÜfy`Õé§Z­žêíœØ9e´n퉹›Oÿaî®]»¶óOµ[Wìcb$`d†¹}òG‡ŽÏ |*g¹•»@öŸÕïXf$û ‡íwïyYÙŠ’ËÊ5ŸêÖ¦ÿÞ½{y@ØíÛš™>gŠ»çUÎÿ©‘d„¾âìf™-µ³½š+Y‰üù?ø];sÜN€›·i8¤çp^ ¬ÐI`ÄpÙíë4®šªTÐËß³U>Ÿõí7߯Qä!&o:9Õ?\ÒîµZ}js˜&Yéä(x?^Ù¶mþårÐÇü¯SóL3eerÞòÊ~­Èi8¦ Àè…5k ùtú3ù±Ð:°9«‚=¨¢¬Œ9¿ün +û¢¬FçŠb‚ HCÄpÆGcâQM€Ù¡})fÆ襋þ˜=}>¯„øˆ á–» ¾¢±d’‹4«ì$«)V,]õþ»ÃXÀuÅÏåK.|àa §‚Ä0†{§ç`Øæk^»Ù+hÉ¯ËØµÜˆD¾myÖx—/]fÇ l9ž0kúù¼bP Æa:êî};™,âQÙprâqßņu¸40ëûï·î!’ÌZ¶È–=ë+o´&òLûí… {ôë,S8Ö[µüÏ–/7G½lª»_TP’‘®\öwX_™¸tñòCÑ­F&4–•Ò_™8ùëÈ!Tˆ  •‚øë¬_Ûð6ÏÃ¥r8œ39 ÷]nÄâNëèØ¾2xaªsQ?Á×ÙøŽ½.ݥɔ5ý‰;Ø5«—Ý¥”x*u2Ñã œ­°L9|3äHå’ý’†Œ'MlQ :ò±‚×:µ'E9ÏH¼íL2ç”…¶”æräÍz.A Û‡‘¯nÜB’1É]hE¼‡õØ [«<Ð »ÎK½<#†YÓ@ÙÙ] …pê}Im2Sk9¬E…¦¥ðI’»}kö¦¦:kŒ-œ.CÚB9Ì9ÚîÊ¥,Hv¾å®Û3é`Mœ4™ÝLQîÒÉY±œnor~Å)êñà¥%‹­“ .P9ÄøÜZ©†˜€;_,øc&'6þ¨„£@IDAT`Œ—)k¦,Y3‰õDLu³ü…*ÉÐOvìöF$ À¼”!T!Ù †ˆÎVhv@ÀÓvd4wQÌâx TÙðX–›n\·™ÑÉûŸ–¯ƒXçÚÞÿÈ’x]ígj±5YÇW»5nÑ`ïîýø£kѺ)~Œ­»×Š,¨ã›mº<ýLÍüóý±d%*ÙBE ô/DcPï¹ÚŸ o|¾VógŸ¯Ëb`Üb­]½‡ÒVo V(î/ } w‹+(úç÷>~׺8A©U¡ÊþÃpþôéÓ:à‡ œ<þ{\öãB@0;ž}8x5iÑÆK£ºwèãåjúŒé&À­‚=Œ«ÇŽø’‘XZ±3xõ¢%ªŽ[ì7e¸•±K.=+žhÓ¦ÍÀm8gõ—ë9‹ÞU˜{\±8™…Áés>4ûó%W ½GÀy’Èû¼1'%»oðsäyTI0ŽFéG•VÓ+±ËŠ¢¦£Æ/[¾ôþ=¦~7㻯d³Ü*Õ+²C/î£D€}{ªð´n•‡HfßK”*n"ß}¿iØrmôÇãØ‰­ÉÞìúzàÍÀ²–Ò„|²‰Y©rOâäyÌðqû÷¤¢Ù‹6Ž2eÉ8ñÇ/°¸fß#&.ÝÑâ¢OÀ=µ©ËØ¿Û!¡ÕC5—,Aá\§þÓìolM¿o÷­~‰Äó;MNYË$3iã“YâÙð[~[/Q;‹lÀùcv³j+¥Iy_ü@™¼eÓVâÙaØ(‹öírŠ—)o×°!¤èÉ1—¢¤j=+Þ €ç¤îÝ»^»½kë^oÒkE@ˆ°†M{¿~ýb…´*dØ€ó”9s„­Í¥(Š@,B ª5À?ówÕÿÚ™SgOš_¾¹V¼pþ~çö«u.wç.ÎÓ°Vb˜ä›»t:~á¯]½Æ¦jRŽÑ­ÚWÌ¡Í5ëVg×l¶ƒ»zõN³œ­ÔX1ûÇÆEâm–ˆH†+Z3avnõj ›ë¿6í^jÞª‰Ml²lسŠßã˜ä«Ï¾A޶!ó´ þ¸ø·yÀôaÿn}ß2‘ÕjVÞqlc²dIE~Œ®·t8æ±¶—KšŒ¬9C¶Ü¸rù Þöpß]UþÖm[Z—X›[ìžÇ†ÆXcêO ÁDÞ#0`À€³yW•,û`þÅû¼šRˆ°þ–…â“wˆ²E™H½{÷îÖ­¿b¿Îþ4Ê*ÕŠ¢æÓ]0dŵkuŠ€" D1QM€¥yÁÞï/{¸Å|…9Žósçà ½Å/ ç{{6RGul")Ù%û•î 6ÙÃPv~ cl ì¸ceSÁGE€£*UªÌþ×õÚÚG--–¦ç‹mæpciTìP`ý9Þ×%J V*\š@Pb;ÑC€£µcàûŽš£ FÏU๺û›}såÍ9áûqV†/rb)ýæ+]ñâÐæõVžËqy÷×¹‹Ìùíò%ÇžªÅK“4ógÿJäˆÏ?°ºs™=*#K¡ô~¤IÏÁ®-’‹À½{.‰ü/øR(ÒCÅÁ| ôû AqP<ünò‹‡9ãÁŸÈ$µõg× G@îÊÙ¯aE@Pb¾N€ö&†­ŒÛwn[«n5ÛŽê¸Ý*¼#ñ¥‹—“'O6 ðc}Dý éÅÕ³lù æòvàµsçÎÅúæiE@PØ€\—Ãlù‹È¨‚Å:6ˆ¯2*Š€"àJ€½‚IE6bÿ `Ë_6þÅÛäÝ[×/\¸ÙUkùŠ€" (Š€"B€·üõóSÇWÚAEÀwPì;Ï2–¶DìŸ9³îסô „ýBƒ; Þ ºvíZ,m—Š­(Š€" Ä. Àè{uËߨõÔTZE@xT”?*bš>Rp¹åoêìeFÎÕ5À‘¸ª(Š€" 8# [þ:c¢1Š€"àc(ö± š#¾4䌸°Ö-cÁ“SE@P|´¾´IÎtË_Ÿ{ÂÚ E@°# ØŽˆ^Gp]s@z SgGX·ü<ܵdE@PEÀ !½Øzôè¹sçJ”(ñøãÛžî¾}ûð3\ªT)çmî<èïï_ºtiü"Zsá£x÷îÝ”™9sf2Š¿Dk‚þùg×®]çÏŸâ‰'žzê©Ä‰[ïjXPE@PE@PE æ àkànݺU¨Paâĉ6ˆ/\¸P¤H‘råÊÍ™3ÇvëÖ­[O>ù$ôÕº ,úùçŸOž<9·š4iB™éÒ¥ëÝ»÷7$;[ø´k×.[¶luêÔiݺuõêÕ!É&L°®—Š€" (Š€" (Š€" (1_#ÀO?ý4È.]ºÔ†ïªU«Øo–È%K–Øn­_¿6[¶lÙ)RÈ­?ÿüò‚ råÊÕ©S§O?ý”3wGŽùÑGIš:|ûí·… þꫯf̘1xðàT©Rc+\/E@PE@PE@Pb¾fýÌ3Ï †îb½œ AƒòŠ+$üûï¿›H ,[¶Œå200ð¥—^BüÚk¯¡Ñ5…Œ>%JD²K—.ýôÓO)S¦¤¢Ô©SÓ¼yólß¾] ѳ" (Š€" (>ƒ€m˜Ï´K¢(q_#ÀùòåË›7ï¡C‡6lØP¹reóD!ÀiҤɘ1ãÞ½{Y \°`Askùò償}öY‰ùì³ÏNž<‰åó¤I“L0á¾}ûJ ƒ dÏž]دD’ L™2vwØ´ik†±—.T¨Púôé­)YQ|ùòe´ÊAAABÔkÕª•$Iwñ’—æpðË”;wn̼{,D«,„&ÒZa<“&Ož<¶x½TE@PEÀ% 3}ÚZ…(P ^½zÖH +qÿý3³¼?B Às^ìµaUˆ|F"Š^Hd<ýÞdOÌòÇ7¢BƒÃVˆæRE & àk`0…å~þùçà÷Þ{O ^¹r%5j@GîÚµ«Ü†ùÎ;BQœ^½zu+·pm% \žsäÈzyÍš5°Ù/¿üòé2™5rîܹ¿üòË+¯¼B]2dÀwôÀ‡Šó-£|–ôÍš5ƒµ¢mF]lÕß:Ç÷èу…ÇcÆŒúÂxYö ´9q6ƒdvŒ1J1Þ°?âÚRE@ð |КÙÀTöÒ°aÃS§Náö‰e·òÀ`)ì„„Á*P\=CÙãv*w¡C˜Ý”KÏg¨ï°aÃzöì‰ê­’°¤Llkq[å.#JiŒ®¡@ìK,¶ÖìBlK̆L¶¹tÏŒ,K‘) Þ%^ÊÄB ¢5|øpV¿ÿþû2}útÎDº,?†Gò˜ÄæÙœ¨X+V¬(wÑô@nb$­8?á`"¿ÜL[p¸,.ƒ¢ aRn@€!®Xúfç&živÌ2žÕðŽŽ…CÒ`€M,­$;=ÞûñÇ‹läBSTx±Qæð´×s^&='ˆö»1_Âh‡(Tb†|ŽØ_ ÔiE Æ"ÀH …ÔWó3*¿¤1V`LPwø ˜¦Â8‹^N´vPS@Ôt.ÿøãÎÖ%¸²†šÊ!éC=3!Êbc,rùm€iO™2ÅejhPÃÉY»ûÅ_°Œ”áÀa)I3Ø/¿ü2„ЉYnj-“%Ç\Š70¿^/¾ø"á¸y`À“Åæ9Ôæ—,Y’‡%f±’1n:wîÌ (é?æÌ;H—#}ò'Ÿ|¹Y‚nê*_¾Õˆ‡žcÑvÆX™=7pÞ¼yÌnÐ.v"–Xã.§q×1ƒ€Ï6\»³¤–YS,æGÍ‹±ï,³Æã›ucû@J^|M»ddæ‚é*, €Ž4²FÀ”I¥=kûsæÌÉ"y›NžI¥!C†0gU›Û\Ä“—i&–Crð]‡Ù…µXk˜™/¾E$&’× qqgÐ 8v˜ð]Âk€¹å2€ E<…mÛl(8œÇô˜×ã+Á'Â6ÝF7¦ß²6žY¾NW®\±ÖœÙ™ÙÁOß+ë-[ƒ< 3ÍoÕª|’€…! ÆÃâÉÒ9¹Û¤I¾ZÖì¡ë,¤ ¥/á׊¥¶´«êòÐ[¬’xö€*pQ£™?åÛΗù¹çž“’çÏŸ’xKw y)†&S"c÷¾àÕ3žïü]ÕW¾*ûÂçxfÒŸù}Ô¿`]©økኀ"Ùø&fa'¦§Ð !Àƽ Ö¿Œ°¡2H‚Þ€¯¡.„£H$€šî‘ g*³ûh€]fDË>&¯ È\Jíaž@…!à©‹6~óÍ7è~E¥,dÞ”É*e8i±F03v÷ÆÀÛe| ’é–-1á ÿ¤cÐI ³Bu%c8 ïbÅQ‘yÓ‹x¾lV³’Ü%b¬ÔÅJ-1$êQ§?€Ñø]ƒeÁ Y²…Th•éÒRW ”Ù%ÖÈãÇ£GÓŽi:t:÷Ã?`²ÁL FãPâ,Y²À à&Kð 7c.‰§É«ñàa‘ ¿ôp@æuæ♘Àl~ìØ±=Œ x}xY01eâøÖIÕúÌ™3yʆ.¨T©’·©cÇŽÌw`]o5±æiR&œ™§C™$¶É›Z°b€B3Ž$çóˆGëä.•Bx ´>‰4&ôÖZL!`Š¨î­·ÞÂ{<¤’i80l–¯ b@æA‰ÏZÿþýáð¦æËxh ¨„Í6o¤!ž™¬KèQXÊ0ï#> Lv€î"0s<>VÚ3$_6Ⱥuëx޽{÷: å» ÿÇ>B²‡ ¬K!=`(} "ÊD$ÍÇžŠ<ôÓ /žQåã€S R_flžÓ e‚ÅÜõš—’h2E Rx¿KÝO{?ûíЦs>{yŤöë§¾µqZgx/Óè°_%À‘Š¿®(‘ŽCpŸ<ÄÏ3GD/am#ƒW`e¼È-ÖÛ¼8HF \qaÍjåÅ2dw™’Ñ$w¡ æ®0+|8™*AVMŒ\Æ£O @—ÖÄŒ›‰„ž™HÈ1ŒÚec'¨…¹•@³,qs…¾Žµð%#¼q‚eb)e¸o$ŒšQÔ°°#Ø4Ø–Àz)ú7Ô¹Ù¥KXCö#F)òH<,…qô“r)ʽX‹²…y bÎ cÆØz -±õ’¾AÕ°8‰m 1ib ÌF6wY° „D‘†÷²D3M øÀÐoA¤xt×(úXø†ÄàÇ…ì<“€,ˆHx‘‰Äû ¬˜K.Êä…ÜE¸U“K´Êäå“KäËñ!ŸÄ XÆ;¼R- „g’@.Ïò2²&ßÜ¢ÿ _$ئDR,ŒkCœÓ˜d&€ä¤:¤DJŸ7áÒ&=oóL”H óeÈÌ´ @$duáÂ…rW¶7‡6Ë%lޱ‰\ÚΓ'O–è›xê’°Ëœ‚i‚¼|%g`= éŒ!eJLyðu5òxî-’…‹Iï!àU2о—ÒxŽÀ 7o·µ" y¨]o)‹€KgWtrâù‘‚Cœcñ&2áË{m:sÄŠ¡¥)Š€"õø¦˜™hä&NœˆNÀØ?Ï!VÐè¸…ß £/•»ìN„ý*c&”9V›@4 .a’$C— µæ÷@²pføÈd?ñÄkâM€q*aãö†ß¡¦&Á£l’ÛBcíiJC“†É.¼ˆ àoÂÍÝXàIÁuå×0Ï“…°`›!2VX‡B&Ý•C-Ðá¤Á”ÚÃü·ÕŠž7CzºYøMO ±,&wW2ÚB )Ñ@¢–‡§a&*¹Ü¥7ñbâΊb‰Aï‡ÂÍ’Ií2ƒŠDw‡Uid– F¬6Ctåå51¼¡`ÎØ [I £á¦ÙÄÛ³V‡g0FëxTÊbî!å 5«¸Q°iªãÕÆ.Úx#Gx/ =’Ñd>X2C)¥(´Ót {yÆ7V!L²˜žƒ–…*x©s!P,Òó9¢CÊ]Ô°to¬r ŒÖoJf[ ²ÀH:32£:6@S®’LØñ•“¢À  ‰4gñgΆošDR—¹K`ܸq¼hó /ÞØ×x6T!­µXÃèê­KôÃÐ[¬¥YÞQ%%_ X&R™\`#wÛï‹”*hÖJ5¬D=ÌßñmáPô½tc9¢^­QPÈ@ ~dÊdqZ>T…c#À˜3Ža_n‰Í°U`ÖÌ}bD‡ç^&(aМˆeC^Y؉Ó)¨5Z>øiV2¼F‡Ó®];b¬¥™0£gôu pq»ûbãtüÀÀ LšG 0ÂÆÈŘz•b¢†B¬e¢…`4Ïð”I\Ršñî#ÕÃíEkŠ0†m†A0 ÁF‘*CUøº>(‡ñ‘f-xQàÀ(0¤DuÌã+\ÆÓ('e0%kíE° œX‹r3@‡óðÔ³I¾ÄMÓKŒì¼…ÒXH8ŒÚÑã™ôÑ.3pÑ<¶ rÈ@ÊÏ¥µgŠS:kŒpK…Ɉù7s:¼‰ÌL¡z%ãvs×à`W$Q#Ûn™Kku®ÍüóVÈŒ½±I,5òâÃÇ3½ÂÜ% o¢5Æsí7 XÚmM&OÉ­‘–ô#@±Ì]È­ˆdbèZ˜*ÐéÀô@SùúÉLœÙÈͤ7+Dà“×Fn¹Eçg-oÎ!ä|yʦbï-1d‡ # Äž±µg`CÒZ‹5l&2Lä#õ“Ë]Àª¤gA5»ñ…ÁØ›së»i--TЬ‰5¬D=|Aé–¿Q¼Ö¨(Qƒ€Ï` (4XVÇÆkEN=Fo[`èf½%a#ì“f,NáÉ 1e!œxb§"¸ ãoüKIÖõa+kõgc+®…Ñ&–oXîQJ?Öס¢±rQ敎)Áe<´möìÙlqŒU!œëk¨5ÔÎZ&… •XÆZÕ_¦ðØÀ‹­]ᑜŸvÖ%²â”gÍ\†KLùŒSattÐ\š•ät&A6D?,K‘GÔ˜ÂOB•¥%0—5ŽPÕ†¤ÇJ‹_†à0.H8Š;b­(ze†ßrXå‰À°aÎè?™Q¢·cå ์¢9¼ï?¦"Š…õaþÇ .jaSo1.¸ŸjÞnJ°¤m–2­óY&‹ÌðQ2ÊUn!¡t?ÂL “,h†±`½1Þ&»xïe¶b ‘œ._Bö%‚øa^¶MX³ð5: ØGÒZ‹5ü¨½Åš×öŒª$µ7’ìQA³‰¡—Š@d#À0‰Ï4˜ÝUÞÙÈ®TËWE ÊðY ‚°>†•|ÄÇy°nñqG ákx…FX+Ê4~V­#úìú°žEùƒšˆÄ¢2rY”D"D g9¬2d% V…ü´˜\ÔÈ0ݪ0‘[îâác-"2@g̘’±2ZJ[ XÌòëa¶M˜zãlú2¨Ý!³ECoá)3]b6F †2¶Æ ±±I¦Èý ³Uè1ÝÆ]É&>€Ý2ì׃&13Ìò0ãƒN^4ÃP;g·ÌVÞÈ| ÐK;s›œ¼¡¼°ô“~ýúÙnÉ¥°¢,cŒhF+UE Âp­ÿŒðj´ÀèB€ñŸx•õ«Ñ%FL¨§»Œ¼Y¤m<;å 6Äh“a4:^â¡%@¬l;d’áÃÇK,üfŒ‹É±‰—ûÁ°ð%ËÔ…²Í bÀ I–ÂcQ—ÁTa¹lÎ %f ÑéQšF4½” }­…óÑ< `nö‹zw6ε&‹z™­µGv˜i& ЙãC˜Y Ø&+óaƒ¦^jìvÆòìaÆÁÇHŒE.^ ±Ôe­5&5È…EˬޒL ¶ пÁŒÚ1w™ÑÝ]x):R៶Œ¨æ’²Â¶[./a§”‹°Ýe¬@¤¡²¶»rÉhøÒ¥KŒƒm¦ø€°nY…$=‚¡û“s(3YïÍ€žYy¹‹õ/ðèm‹`£Rfg95$±µ³æò&cZúºV2Ò^6°Ã¥(ôC˜°)œUš€ m†Ñ™Hçê\öjA™¨ðëS“r(„gÍ-.éíŒ#ÝuZIOb³Un!6a¡’,Ô38`Ë 5dØd¡·°;=ŠŽ VÈoëó‰%6 =dcɃIáÆãî-3¡ø%Y›Ì´NÎѓ㭀È< z»Éb xÖ¥’ÝCçG))=ôwY¬ZÃîP%Ь:!›4¶Š\‚f­HÊ@¤"P²é˜ÞÓ1éÆëŸ1cF>•|ÜèÃò!uþÄEª0Z¸" (Q‰€š@G%ÚÑPÀ¬¸‹†ê#¢Jè!XKuw× :­‰%lԀη\ÆXÇôÖ6Îf½eÂŒ-8Ì¥ ``æ¼Y wÙž¶oÝÛÆd!€©*w™Ô0¤­ñÎEE¥ÌV ÃvFÒ›£x¤RçöÂCŒU¹U*¬…9¬1„«sÙ¨ÅóÖA¦FÎ"Ù*å!šçh½…Ù¶õ2Ô08X¡°¦‡îm gmì—”RçÎã<×ãî-3u‰/sé€;GšÏÀºRò:ch)\VˆlÆ][ æÒªÎðÒ 9LF[E¡‚f2j@ˆ$x¹dÖ†wœ¯_ 9"©:-VP‚Àƒßæ"Šá«ð³ê«M‹¨v¡6„úŽ3Æ[`U!¶µ´£"f@g&ÔkxÂõNDÉ å(qÞ¾N:¡‹v×üW^y÷Îîîj¼"`R†‰1e[¡2+Š€"ð¨(~TÄ4}X€ýª±}¨À1þ€ÓzHöÎ;ï ÏÇ!v­XP£ÜÃNŒÈè!—ÞR¢tË,Ñ·ª^£E /+…‹M²»ôX^¸»¥ñŠ@,E@÷û¥NÅVp" ´$œjvo€?Ùì³0¬ö¶M§(Š€" (Þ!Àà=‹ 4J`ÑË`5×ò?M¥(±ÕÇ⇧¢+Š€" (Š€"6XEÏê_VªËJ`]65—" Ä:”ǺG›³gcüŒ‹ ¦œcSTVE@PEÀÀçÞþ ½,¢‘u4üX³ ¢Ú*šdÕ'GžZŽ" D j`jQàwÔü –{°žäÑ" (Š€" D!†¦F S•¢„W–Ë(l“V¥(Š@(¨8€ôvØ€âÂ{Ù“ƒMD­ØhX±‰Ñ€" (Š€" D‘ÁN)S,«1®6,EY‹´"E@PBE@ p¨i‚GFfËõ b[΄]ràG.Z3(Š€" (Š@LEö‹î—EÅl¸ÍÁK±#ƒiÇT T.E@ˆé(ŽéO(6Êû…îÞ¾}ûæÍ›W¯^½qãXôÀ±±9*³" (Š€" „Š,—Å/Ô7Y²d¤ 0çPójE@P¢ %ÀQuܪHìŸ!ÀþþþD ·PÐÖ*Š€" (q!À &Ľ&€>ŒN˜[qm¨" Ät”Çô'ëä 6vœPù¢Æ ðõë× Ë­ð´ˆÈ.çð”£yE@PE <§µ2[Âp]x/Å¢¾uë³áá©Bó*Š€"(Ž TµL¿Š­'àú9&½Á:BŽz(Š€" (Š@4 ¬ÍýŸÿA{hw öi’ê+f_êþ#žŽV©(¡! 84„ô~˜€ýn›Õ=LY]gâG”ƒTwn¥]çÔXE@PE@ˆ8`¼îü<—l:F~¯9«¹VÄA®%)Š@D" 8"ÑÔ²" ~D9 ¾êV:’ÖbE@Po;g~žù½ö¦M£(Š@t! 8º×z~M™KV·Ò™&UE@P"‡¹³úyŽhTµØZm’" (Š€"Sê‹Ù32Ê¥ÌPÇT‘U.E@PBA@ p(éí¨G€_VoðŽ¿Ž-å¸w7èúõëQ/ŒÖ¨(Š€" ÄY`¼¶-ã,ÚpE@ð”ûÌ£ô‘†ˆâ׺å/Š_.ïÞºqñâEi¤6CPE@ˆñÀ~QüÚ¶üUõoŒn* " „‚€àPÒÛQŒ€¨­[þry;ðÚ¹sç¢X­NPE@ˆ›À~9âÇŸ8qâdÉ’ª`1„Ž›€h«EÀgPì3Òw"öÏfËß7nܺuëî­ë.\ðFjKE@PŒ€`ç-qOƒ¥VÑE@%À¡c¤)¢ ±æÌº_‡Ò70ö¾w'èÚµkQ&‰V¤(Š€" ÄeþÏÞyÀGQ|q\zéJÞD,¨ `EAĆ‚" H“.J/"ŠX¢Ò¤‹(E”.½w$”$„ ðÿ^Ìݽ»\’Kr9Þ~ò¹Ìξi¿Ýß¼7o Àè{!À€€Øõ!¾råFDÛ®(Aƒ€à ¹•ÁÓ·[þæ,R}غ8xî²¶DPE ÀÐ-üiõE ~(ŽnšÊ?ˆ/ ù%GL0ë–¿þWsQE@Pâ‚Z_Äå—€nùðTVPR J€SÌ­ ²ŠÂuÍé%Lùu…uËß »ÙÚE@PÀF@H/6ÏÔÔp`ù:vݵvŠ€" Ä %ÀqÃËïÒ¡¡¡G޹ùæ› (à%sÖÞìÛ·’%KÊÇÉ‹pЏ$\×lö+X"uËßqµ’Š€" (AƒŒ×¶ß¯áÀAÓFmˆ" (‚€àdî •+W>pà•è߿Ϟ=ÝÖP5kÖܰaWGýæ›oºóIr¾jž®&Kaj¶E‹cÆŒq^õÃnº°ÍˆˆïbI¢‹Ö×löK€S!ÀºåoÒß-QPEàÆD@°s¿ßàXoucÞSmµ" xG@ °w|’èêK/½7nœ“ÿóÏ?¿þúëc=vÛm·¹­ üv÷î݈,X°bÅŠ(“ˆ>Ì)[éîÚµ‹@Μ9óäÉ#D®]»ößÿ½õÖ[Ë–-›7o^“0QTXè®ÙìŠNøš ôå ºåo¢ÞÍ\PE@ Àp]ÝïWûƒ" Ü8(ˆ{}Ï=÷”)Sså7²*ØZ§±cÇrúúë¯oß¾Ý/áÕ«W7oÞ|ïÞ½rš!C†aƵk׎SŒ¥‹/.ñK–,)Uªá|ð—_~9sæL‡¾ÿþ{8§ðåëÚµkß¾}å4 ~Ýnö+åæ*RC·üM‚[ E(Š€" (  ûýj7P %Àr»_~ùåwÞy%°0^©ŠPÖî-Zô‘Gq`ó½÷Þ›={öÏ>û¬Fû÷ïïÑ£Gûöí±©~íµ×PöŽ9rÛ¶mäY¾|ù–-[’g… øýᇦOŸÞªU«‡~8_¾|[¶lyÿý÷ûõ뇫­G}4ia㣠3ßNš²´E@PE@ð„€î÷ë W D@ p ÜVj·nݦL™‚7,ã kâĉpà÷Þ{ÏíR3ëfçÍ›‡›+šqÇw ò­Zµ*T›jh0jÞß~û Œú·K—.¦©O>ù$ÔËg‰¹ï¾ûÈ¿mÛ¶sæÌI2l*£E@PE@H^Xš”¼ÐÒE@H2\Ûë°·aÆaaaX&›ú  f[ج‰1ô½+V¬¨^½º°_‰Ç|ú–[n9zô¨l­d„m\¹rö+— Íµ³õ*„™SÜ_•(QÂï ‡††¢†-‡„„;v ‹/:Å4FPE@PE@Pà@@ pÝÇúõë.\˜•½ìú[¥Jv?¢rX&»­¢¸w†ÿñÇV¶ .P .µ¬‘¶0–N˜Oúé§l>„×h’`JŒZ@Ù€ÒSE@PE@PE@&Ô:€î& q_|ñE*„øÈ‘#¸ªbí.~›ÝV‘MˆÇ}ô&ÇñçŸ*TÈm*‰0`ÀðáÃkÕª…‹,80êߥK—r‰½¼¤ÒKŠ€" (Š€" (Š€" ¤h”Öíc`X(®°FŒVófO¤T,œÙþ—½s½´A’³Ë®UjÍ)`ïŠbk’'|òäÉŸþùüùóÙd¬fÍšÅvYÉ[½d¬F2ísÖ$û}ñ^C½ ¶œéBÞ¢ƒ [ÓQ%vª œ*wM’ò}bë{Á ¬¶NP@F@ p`ÝvðÀß~û-Þ³‚i 0tO×+VlÑ¢2¦d.À + >+W®Ü»w¯9M²ÀÙ³g{öìÙ§OßK„“Ož<¹Y³fÇgäí½oøž-’ŒÑÙh'ä¾òÄ«F¬uNÆ¢c­ÖÇ'N÷Gtx¼“uøRPœ’ûR·¤—ñû“Eçg‹8Y¬‘ôÍq–ȬâÝwß]·n]f£.\¸àÀ6§víÚLhòîuîâî”—k/"&N­vb§äžª”ðøDªF³u¾O˜¡ÿ„C°9@€Y´Å/GÀVR+¦(Š€'Ô –'d’(›&MkaO=õ”õßÿ}ã7¬ñHr*¿øï¿ÿF “æ3fɧ¨|ùò:u2 ‘GEÌ&Ãh!8`ƒ\‚wa‹õõ×_Çù†1Œ#ÆjÖÌM)1À çÎ;£NgÄLëh®Å˜@Õƒ*´_…“=›6mjÝz*iÚ‹ã±ü1Ož<¾'j|V‰ç9|O«$þìÙ³c{9iÒ¤Xµa‰WX뙌EÇZ7›@œîïÇüÑG± ß,IˆSr[ÑpšOOt5üñ@h u`‡v~o¿ývLixýÊ«ÕZ· `\]ºté;v¸eÈVaOaß[ísß“{ª€_â© ÌÖù>IH†nñ÷ z™ 3æiÓ¦eô"480+©µREÀJ€=!“DñØÈÙJ‚…²/l$sæÌÖK(l±ˆÎ–-›5²B… Ë—/G˜A–Ì8‘Α#‡U€0CFè4Äo ab„£4Æ‚ºH‘""EÀ–6%žÂí{÷î]¬X1æãqˆ-MÀª˜š5kŽ5ª}ûöešfS¹˜øÄ qS´ yçýS&NaV;³¨¸Q£F à¦NºgÏïh%R5œsÆ$cÑÎÊÄ×ûkë‡qMk}’^ÀÖ¢V€göùçŸO`&~OŽõ óãÆs`Üøc„Â}d.Þ寵Õ6Ìãš<Þõôž0‘ª‘Àlï“f6ü½Ã’¢¯fÉ’%S¦L2d€+NÑ·R+¯ܘ¨ t2ßw¾¶J0·jc¿"`c¿&òeË–… {bG|¢J–,)ìפʚ5+I„ýÉ’cò1WSnbÏr_Ôà†ýJ[Ы£Þ„æ1Z%†UÖ¬õ%°lÙ2ŒÀ9V­Zemõ„ î½÷ÞÛn»1.SÖK„Q)³HÁ­,^»ª],®]µk×»ëûï¿ß\²0Dûí·%F’°33FȨâm…²¨»N:˜¸#L¨*Ì$!J Xu@ÝÍjÆéÓ§[‹À¿7­ÀdÚi S4º,,«9¸Ä’i›€9õT Oíe©9¶hàñXÎìƒÓ¾Ú †¦P x*š«X£Ò'J©^½:2®•„žêöØcÑO¸­ Ç¨÷Ž;îŠ;wÂXà-ܵùóçÛªa;+î ÉI²~ýzÛUëý•K‹/ÆÔœ)'4½èš°sf nܼ æ­ZµâξòÊ+"ìLîý^{ïB䉿w,#0yàA`ŽKÑ€IqN ¨6&Ý»wù[ zÉÇi »}²HBnÆï€$:t(O qŠ ]…JòxbT ª7^·nÉ|Μ9L`±ªßÄÐoy̹wÜ/žeֱ˥µk×2Wȧ.vÝ—Îã×^ÞÉÏ=÷smìÙn*F€‰$êÃŽî±¾T߇(Š+W®Ü¯_?«%<ùøØj·˜»Mîýy!‰<¼ÐÀiD0— ù¸$‡wx%9€ÐîËìÙ³Iål…÷—;0%Ç;nмysÖ\/ü?ÿmÙÆZs“ØÓûÄ–¡ó¡ ¦›L¯Ë›7oµjÕ0¤"Òþ¦D·/d¬Ï‹wÝÖÜmâ™3gNÆÐ`0ªŽ7ŒšPP’ %ÀÉ…¼–›XàÈ„¬1lvðä“O Éáóc˜¦±¨qXç0‡¾2übˆ5‚6XÇÐÓ¦Mƒ°3t ›s CvÃñÓÃê2†ß|ó ã{(·³&ÄÀ·Íø^’°V=?DÎV(ch"¥z¹s禪ü’cxúÛo¿ÁÖ/Â.X7ne,Œ¢ ç#GŽt[‰Äþ( p3Æ4œzöT ·í. ‚{°(ÒŽ­dÀZ7ïÚêà©h°¥íÜ,Œœ‰¶cá·™°p[72g5èQ7ÌS7³è€ #[î#ýrè–ÖšŠÁŸQúQL•ž@cµ›«¬÷—Sæ,4ḩp× y¸t¢ÚÜY¦¢`<Í5³6¶ä±Þkï]ˆüYs.–;v¤,¦:3Yà 3l¼ÇÓyÄÖ—8›A3Ô@IDAT¸k¬¤ Îrêö×í“™Y½z5Ó1& +`&}(ˆHä¦0 ÃåÜž8Lˆ™0.šŽŒY$&¹p7zôhf¸¸Mfˆå¡ã^€3-e€Þ­[7_ÛÇ»½tlªDÇ3M#ÀíF-öꫯZ#aªMc'NœÈk„~ ̃XÅ|lµ[ÌÉÇ–<Öç…$òhð °.†wþ#0™1Þb…—äÌeRD£ÄÄÉV ï/è.=s×®]ï¾û.S„<›ž~Û²õ^s+ªžÞ'¶ ™ðñÂaš7uLÙ`RD¤'ü­…:Ã^IïÏ‹w)ÈmÍHH Mæ{Á:Sç̰ëJà„€©iE ÀbGEÀàú%CåÆ#<å«€§„>ƣ݂Ӻf´Í3ÆØN®ÊÂ<˜UX4PÖdJ|‡H5sæL9eäÍU8 £‰aÛ$ Ìr*9ÀW!Qãö—Ñ£|k/…"†vˆR`’r‚<ã$F¨Q0áK ƒr&éQË©ó—´ÈãGM.‰6Ø»SÒÄØªA¼Ûö¢¡¶hT$!7ÆÂ÷ (ÄÄŠ¡)Îp e¥h¶‹·uãà“ *b?ýô§Ö˜yóæqÊÐ_l¿@ _Bÿ‘KB±h ‘´Þ_4½È£¨§á"ߣP 3ž¦,Tˆ&-kr_îu¬ýNÝ5EˆBþ‹/¾·@‰í.¬Õ¤â&2äÅlÞĸ 8Ÿ,¨nÛÐ(ya5°‰‘ 0ådz¯(Æ¡—VRÉ)äИc2B#^Í”‰ä]D¡ì‹nb¤ “ç8µwÅŠT`àÀä Ó#zR øÐXfF8•I L¬¶d'h¼D²…I“-‘¶Úzj5’NÌÉc}^HBC¨s„9 £0ȹœÆ ¯$g6ŠG^’ðkk…÷—ƒèTq g’sMذeë½æÖ„v¾OlÊ©íMÎ+K “›©›[ü˜lù{y$½?/Þ¤,·5·UÆvÊ'›'=›gßæ´ˆÛí6¾R£a¼L˜Ý@—N·G+ޤÉÐm«€‘Ô€" (É…€j€ùÖëT@êŒ]·­a¨Ú˜¨¶íŠl“‘StÆ#š@"›Ê%†Î˜£ÕD 1è+(QÄÃ/¬¬R¥JæÔ—€³P/ªahT±ìtEþ´… 2Cç°§Ecƒ¥§Òñþ…<6œ" VиÂò$ï%ÞÖ^ˆ(Cy8€$vÖ¢3e€1ôRœ\‚¿¡ g°n$1i†—Šá¥‰´ÕMâ1b4îÐ~øaV(ÞM ÚZ4x²šL¬6¿Xàí%—P®B­2Ö0föÈc/€BRâazhž­2^¾ÜkIî¥ ÁaPÚ˜Rh,áC‡™6 Ð¬‹Q®2yi+–ÿL0YSùfj€ÞÅC„Yäé{ô Û²Þ1cÆ`º,qpsѻ͟[Ìuå-[Ú/w íœIBý±Î€ÅÁÇL¤§@¼ÛËÌS!fMƒ0¯ä© Ï´STFQÌ##+Œ€-à©Õ61O§>>/<B•É™14/"_àÅȟͼ¨½¿ļ0¡LÒn¢§Ùâ½ÔÜ&éû©í¡ zW¦G%ßëæ,ÑË#éýyñ )ÈVs×@Ñ ;šÛºÇ´†í'Ônõi¦ãý™Ï µdå½ÂËíŽkq*¯(Š@  N°’d-"IÀ°K6¦”œŸdì'‰‹÷:¢‚˜ÐŒ$Á6Ê"«Ñ#ÃkÛB5ß½d½ê,TTÖV–j°ÐÎÄ@Çȯ'Úf•”0)ÌY sC³ .®°°ï5ôÞ™ÊmŒ­½Øcƒ?†ÁF†@XPŠCT¸èñ$-ù°zÖð"“!˜ÓRy2X4‘PDZ JWÃ÷lua+ÚŒb™Â°.¼'†EkÎ&Œ…!aƯ&†cAë©5,k_Y¨oô=ìû½¶6JšoíBhuàflÉÃ$‘ÜT7ÖjØ€âQb"`Ñ¢E,#' 'áñ±rNkÚXÃÜSV“B§1eÚ×k¬±GŸfMææ”[ÀMgÆaìL<ªÁ­Ç ƒ‘·5Þ„q È\fˆh>9ÐU`Sž„Mªx·îýÞ{ïñB @n4“— ³*&gO:m´ºðÒ‹bmµ§R$Þ÷çÅÚ‹HKGbÒ Ùó/÷Ôû‰÷—¦ìõë×C:óŒL.xšÐt¶7Öš;“Äc{(>üðCÖÏ3¹‰=óØÛÇšƒ/¤—çÅ;€¦8[ÍM|\Þl°tö€ÿæë`H/ïgޏæ©òŠ€" $/ÿU$oU´tEÀ/0üb|€j E„-C™¶Ç$ÒïýÔöuR„G+%CK̆(Þó‰ÓU[¡Î´Øži. d ì”wưü6ä`ŠÖw =²ß ceìâ Ö¢<7E3f³§±bˆO#³(Xk21êÉaC€«qÁäæ6àé.ø6âí6‰¶é»¼-«øÝk[åѺ£ÇžbÆ'`+Åí)¶¬8ƒ6c\À/¶ÄÌ’¸•Œ5Eb¹råXEŒ}æ’%KèiB½$´Ž³­bgr¸ÅJOCgŽ]S!ô£¬³æà)¿ö E4 ;vÌãY=‹it¬|›: ì¶n«ç¥Õnåm‘ñ~^¬)ðR¥X_ÐlÔþlM{m|ű.š×‘m²ÉÖ4O§Öš{’‰k<äOl!3{øÝwß±ò³…¸f"òÞIOÏK¬Æ¯2^RÑ“™â¾p6GbÀë¥zIP?" Ø`jV*A(šp~c«¬{´if|¤‹&+,l ãžÇf½i’& zllJQ£™E÷keææ’3ÀŽHô Vz‰é&æ‹XA'„3N‚b1NB-æ,—˜X1ôE͈– „1M7f¥ÖF™Kþ 9=°/yŠž“u¿(?=É{é‡ ¿×Š6 S»‹Ù6´Ðé—ÛY7¦0XZÉÓ„ß,T©¬šæ¶:ÅÜÆ8[Dg£KÀgP&£öD‡æ6¡‰äV2Bw21àÖsÁÓÏ)ÞÂX€δä*· ¥·SÒmL¼Û‹¢Œ˜'Þîeݵ\ ŒÝ¶Â*cÂ^Zmdœ˜›K~y^/•‰õå€ }Œ7þ´0¹g‘¼¬É7mIÞ¯/Ô,_úé§ Ð«!«R%/ø;ëë#éöyñ@gY ‰aކ.-˜[ï•#!yjZE@P’_‡2É[K-]ðV„2Ôc ›ñ±,iÄ£ÂÂlÒP;ù®›åm>×V((|”O$1¨£8 –Rc/–€2LÁ«Ä 2Âxc`gl>¡»ý-Ú ¸+&Ä¢ut&ô1Fö¼2¹•÷†˜XsûXakJÁƒ×† lšgsÕ_zY™5Ÿ„YŒ]¨§üe|Œ#ÀrhœÖÊ©Øýz釾Ük“³ÛZ#È$ú[³hYÌªÝ [#¡©,dz:}„x«ÎuO™xz² 4 ßé´CSS"¤RrJ×Å`Þº ×ˆRL¬ŒçNK$We-pöa"諒\õrÔ­[—iœrAƒqÉÎÔŒas‰ŽÄÊj³.šÚ‚€¹ê xj5’ž0·f’ðç%ðJe¼¿¬½:¯#ãúÛÚ–ä ›êÑ{åqêù‚¿µÎ¾<’žžïZKñK›g>©<|\„û%[ÍDPdD@ p2‚¯E'  =qþÑ)6èa˜¡gÛFR VPÜ™ý<(‰M&MÜ3rݼy³/bÇN²Å.÷ÑE¬ˆñdƒþ‡u³¾$÷— ‹Ð`&øJE5Ǫ]43 »1D«@ ¥–Y²†Ó8v²Í.;èNQÁ1 ±Æƒ Ί0`¦QÖø¸†ñ°Ê€ ?OàÏø²„Ñ Êjì0ÉÊ_R c2ŽÆ5ŸÌz@þY1(l-®uö]žnÃÆQ04v™ÂÓ5°h©—Íà-Üî· Ṳ́þºÅç0…b©Ë/áNãÇwV×{íLevââÜØ,OwÙvë­òÖ0j(NñÍË‚sC,Ye€-tË–-­’&ìéÉâĸnÉÒ\÷FÞp²E'AŠcd,,Ð;y2"À2¥À¢‘¤óÓí!“,þgn…Üèøfç!¥8ÙÍkÀm{cM˜¼ð@÷Åý•dÈ"RZA¯ Ét Vã ÃôTœ§V#ï skV ^/õñþrÀï7Fò¸ÎF’{ŠÕ±µÉf–Š…µ|D°rǧ‰·L‡ù‚¿µæ¾<’žžïZKñKÝò×/Hj&Š€"@0©‡"àGPHâ´&·A’¶à“™irãÖ§»Œk±N´µ”Q2v¡< _rqr©u'YCÈ>7&-Š8qÍBZø :@pæ`ZxoÆŽÚSg¡8_¡b¨¹L& µÙ*Ó,æÄœþF@<ʲˆÎĘ¢T%¤‰1f¸$›¸˜HpVÃS{©*CCòB1®5ÕóŽ¡)ÎpÍUt€¸ù¥Â€ƒ¦N:¨:M*Ou³‚/˜ԒÖ$$ÀÂN[Œõ*T² ý¦ÜbÅŠáž·Gô7#c+Êç„Ñ!OmQl2á"Âè‚àðÄs@$Ò–<Ö{íl©­ ±¡®qKŽ '=¯6tc)Ι\âåWV`ÂØM$¦ÔÔ–Õ¶&Æp>Y"Y%!-²&‘[Ørɨ|1üF·odœ5ÄÔ¶ µ›nbŒ+ÞK¤'É ]Ž È‹W%+[>6œEÆÙ^S ÀÖ€"¬˜ j¦\¦3Œ FÌÜkë% ³Ž”: Ãjv_£Nä‘a†B®ÚjK¤§Vsɉ¹3¹÷ç…Lœ€P+ä­.Uò¯39©œÕðòr`~D–€ ÏËk¯½fë*R g¶Î¢m57 %à|ŸØêi;%3ƒ¼Aƒºq@†ÍkŸ«Nüm%Ú2ôþHJZOÏ‹Ih+ÈV ·§žö."žI˜?.åxé=Á¼Æã”•[aTE Yp¹ÓÔCð#|K‹ÄÌ w;^ÈÈÊd•ÁE“M'¨n?ùGŽfÀmlòÎlœbŸ 4ñÎ$¶B©€SF’³ÊÑìl2$€%0,κû«¹Ê=ò²›+—< :ÝVÃSÅ(Žž¨¡[ð„¡©ª ¸-Z®‚£4+ž&•ÛºÙÀG˜Ñ$­3Æäidí‘S:5—œEp Té0¥1 bQlh†Ûä{º×\r¶ÔÖ…mÙŠ™0µ¥Îäp&¿~å*V°2ÒšôÕÐx¶&Æp>YÈ Öƒ9˜­§M*ë©(ý0~6—LÀm —³m©ÁM„éÀô0ä”ncëêÖ|Üâìl¯©ƒ5¼ÖSÂäfëE”î³¥â”&›ÎCm­‹µ¶&¡ÛVsÕ‰¹Ûä^ž' `kê&ð¯3¹$q[ ²õôrÀ>…•Û¶rMóMÀš­³hgÍMBnß'Ö ‘±Jr,íá®Üknvâo“qfèå‘$­§çE²õ ³ [Ml§^°—-m™È©§¬Ü k¤" (É‚@*J™ÊÔEÀ?УXT{öã 3ßv›c¬nSid\@é„Í* }Q<Æ5­Ê+ú¬ÃÄZ“o‰„>¡ž¥_a~lÄ| à5õø6`dÕ¡CÜסu÷1«Ds¶7‘ ÒlXðô¼Äš0®UšŒ<}pmš´Ӥϒ.c¶4é3§N“!UêÔiR§ù}òkÌv1ÆA@/ù“Uç§rcšäÜ9ÉK*½¤(Š@R" ^ “m-KH:X¨Æ*Je¿I‡x–„Ï*4iV÷W̪`¤*¤qj4šCÒ–(QÂÆ~ã”Ib ;Û›Ø%jþŠ€[’øyñ´ß/uƒôʯÜÖV#E@HA(NA7K«ªÄ¼OÅAZE ïmÞ¼¹là,"¯ÆĽEc׊#b·®³XNÉþdfñ§·\ù𳽉\ f¯¸GÀËóâ>AÂbQðâÔ€Î1êÞk?䪼7aÐjjE@8”Ü-Ñ )Š€"8 8Ð_•ÁA”§ ]ñô Î~ýØ^á¦ùܘxy^ÙÉI8°ÖΉQ ÍSP$@à?; $AyZ„":¬Ý@PE@t¿ßÀ¹ZE@HlTœØkþv`¿êzÍŠž+Š€" (ɇ{‰±4˜ýÃÄ Z§ª“ïnhÉŠ€"¸(N\|5w·ègÕ-,©(Š€" $ 8mfãåL™2Aƒ1„ÖÏt²Ü-TP’%ÀIƒ³–¢(Š€" (Š@€"+W.¿P_h°àxsàx' Ph´ZŠ€"t(º[šÄב Ù-0%TVë¨(Š€" 3ìð›5kÖ˜O³Ëù3Žx7˜´ Ï$Þ¥kBE@PbEÀÏx_È…X‹T A XžŒñn˺éd%°õ7Þ¹iBE@PE@HBz]Ü7ì— ˆY'ä^hZE@HTüL€µ®šyJAÀÇo§ˆÉ¯ºÅJ)7Wë©(Š€"”øøí޵íêP:VˆT@P’%ÀÉ‹–Î4lÖ_ßÝ T›¤(Š€" ¤Ô¡tʹWZSEàE@ ð zãµÙÊf^Í\PE@XÔ¡tÀÞ­˜" J€µ'(Š€" (Š€" ø?:”öO…4E@Pþ‹€àÿâ¡gþ@ °:yöš‡" (Š€"’ð¯Cé”Ôr­«" ¤â³\ÓKëÔ ´p‚ï’w/ÐV÷Îq]lM|¸i‹E@P”‚€,kŠÓâ&“$N©R ZOE@Hé¨8¥ßÁÀ­¿ùþQEß 0’"Ìï•+W$¸Ôš)Š€" (AŠ€|Ç][Çqs`å½AÚ#´YŠ@  8Hnd€7Ã÷o¡‹þ^½z9戎ŽVàwV«§(Š€"¬ðíf;ß´1N9‚µ±Ú.E@¸qP|ãÜëÐRÃ~£¢¢.\¸À/DX9p ¸sZEE@PàB®‹â7]ºtcŽ 2ûU\÷Y[£܈(ª»¾cÛ?‡î¿ïþº2fL‰ ƒCw/]ºvþüy8°èSbs´ÎŠ€" (Š@JD–Ëêò›5kVš€>ÌoJlŽÖYP+J€­hz~¸oÏîèè˶Š*\4SæÌDîßsÙ’…3æ/»£Æ]6™”r Fë >wî\hh(Q§”úk=E@P”Ž€àôéÓgŽ]@ –ÅÀ)½uZE@¸ÁPœ’:Àw¿ìùΛÎwë3ð•vñ)+FìŸùEå‹h4À„åRÊjŽÖVPE@H¡@€áºð^êøâÅ‹LO§Ð¶hµE@°! ØH@Ÿž>Bý*W­^£Ö½ÖŠÖ¼§¶õ4E‡kµÝMÑMÐÊ+Š€" ()Ü]½Û8ÔWì°ÔGJ¿¡ZE@ø?Fíæ—ÀÞ“‘ú—xtêÖ›;×ö­w<ñ`½‡ÀÚ“€ãýÒgø¦r õ厶R£a„ÍQ·ãò3§ìoÞoµ9% ‚†â 8ès¡ï銃âïï¦|‘ê´rÑš•˜4iÒìÙ³W¬X±sçÎS§Na™¶~ùôk&Š€" $#ªþÿ\@ð… ”»¶oÍ›ÿ–ºüâ ›gSÖU/&âZLôåK¬6§ˆ©€`¥8(ú\èûAžÅAqˆÇw“o-¸aöÌÁ×ùÚõz@NõWPà@ éMp”¢[±ú÷/4y´ÁcO~:á{Óï#µ«—.[~Áò¿ˆþa¿YS'?Ó¼å QãDæä‰ãõï®Jø×¿¶æÌ•Û$ô{€O,Œ—.‡Q^]‰Žbݯ)+šµÀ‘®•Àu!òòÅHsJ¤ 2Šƒâ Ï…¾ô=©ß‹„|Í–¿7]½b8°t*ýUE ˜PLw3m1ß¶÷z X²`îôo'>Ó¼Uõšw“E¿žï†‡‡õ2:±Ù/u€úš-Ñ_¹˜ðÅóØY™–\ºzæÔ©Ôs.üLÔùPP´?èsAÐ÷ƒ<ŠƒâÀÏâÅȰ3gΘ-™æÍ!Àê¯" (A†€à”wCìÛ³tÑ|So¬•jשï­ùòæËßµ÷=:·ÿÝ·æþòçïË—Íûa»(5oõ²)"1|YQÿBzÍ–¿,.âôÒ…ð'N˜/ž?}æt–'®íüzæä…sgT@q¢8(ú~ è{RÅ!~8„ áÃb¶ü½z%š/´ä¦¿Š€" J€SÞ=]0w6Öz/^µþöRe¬1q ?÷B›YS§ü½æÏF ™þÝDÖýþ [ Ä)“x‹ý³Ùò—Mp‚}1‚ϰÉíbdhèÙÌ!!Ñs. ðYP´?èsAÐ÷ƒ<ŠƒâÏbxèiL«NŸ>m¶ü…ã'¬þ*Š€"d(Ny7ô‡<Ú°‰©wêÔ©Š+aNã€ë~0lÌãÖþQ?’·ëø^©2åâ‘ïIbL«\?,ýu)}c¶ü=wîœk§ÁËQááá&+,¢YžFb"#Ï]ŠâTWÐþ Ï…¾èúž”Aqˆ"#.]ŒäLrÙò -ÉJE@JRABüذ}!ü˜›feC`Ìð†Ø—mº¼ßßvIN_jÞhÙ’…lƒ„ 31«–/sëëÞ*¥ ¸ WümÍäɇîÙ¼q1³-¯rG ë%Oáby2zºä=ž^‡ú·FÓ1,3ò.©WE@PE ±èÒ$_Ö¬YóäÉ“/_¾—‡ïzçù =†M§QÞlÙ²™?þܹsgÉ’…uÂ,¼JìÊhþŠ€" $..5œÿÿn3«¹Ùˆë>ÀSf/¤÷Ô©ÿˆ5Ÿ•v ¶Fû%‘²1Rå;jì>a½ê)׎éå@닳+³å¯n`+°ÅAPÝÈWú€âØ8ÈG™O¹mËßû_Ÿ?wåÁJ†Mœ8Q÷ŽëPGåE ðPèÄ_HÞÜsåÎCNZ|JqzìÈa[­NŸ гK–,Y§Í[Ú¶UÓëÖN™0þ…6¯ÙÄrj}ä‹Ë/Z`ìŸu[VqPxÌS¦ýAûƒöÝÐXž‚DÁ!f.‚mÔ¹|¦ÍûGŠ€" 7J€ƒùþ(Tk¥-›ÖïÙµ£DÉÒ4uûÖ-¯µ|–€õSû=súT·¾¢î7xT㵇èÅ:ã|·Üê/t(Æ‹â—èwdË_ÝÀV@VÝÈW7ò•§@qPCãÈ‹QQ˜báö’]$¬C_E@b”ñͽ)[¶ìO>ýÜŒï&5{ªA“¦-Ž>È–¿•ªVŒYë+-_±ìçf|W²tÙ_mO «ŸkÙæû‰_õíÞù“¯¾õ :|Y9 ¾n·üÕ,dÅAqHàNžºE6]H·È–çHqP¼lŽçKvŒˆ‹ˆÈŽ×« 2ÄÌŠ'ƒXÇHGÕ_E@ð„@¼ý yÊPâ•{Ç'°®Æ|¥nJ—>½§j¥M›ŽKiÓ¹~åèóáöZ0gÖ¸‡eÎ’…º÷ýèžÊ·›LútëˆT¿A#™–$]zöÿyÁOì´´níŸâLëzfñüûEý‹9ŸÛ-uçFUqPtƒkÝØYžÅAqH¼ ®a¿.z.ìlxx FŒU˜`®¿Š€" Ü(NIwù•vŸkùRÖ¬ÙÀº8Ž·NÅ_¸F<@Q p<@Ó$qFàÊÕ«f¾M²I‹-Z{brê&‹zW½û\Éú5òIŒ (Úèú\ȃ 8(ú½ðûg±J“‘27Íô4écú«(ŠÀƒ@ê§©ÚRE@PE@PÜ>+Š€" Ž9´jù²ó&&¸J€ƒûþjëE@PE@PE@pÀìiSêÜY¡_÷ÎÕËþiöt÷BÁ«8¸î§¶FPE@PE@PßøâÓÑ­^i·håºÚ´øåg¾%JÙRJ€SöýÓÚ+Š€" (Š€" (Š@JDàܹð ¯Ù»{—ý±³÷ÊîÛ}Ì3cÆLû÷îþ÷ø±ƒû÷¦fcpŽ8åïC~I-¢8©×òE@PE@P8!õó¢yÛ·n‰S*d† èU½t¡çÖ{¨V¥'ëÝsòß~©í?›6[ý{ª9tЗ ßë5àŸÍïªX|ïî½?k’¸æk†I/ 8é1×E@PE@PRöíÝ´áokþmÞŽm[^mñôÀÞ]ý›­æ–ŒDFžÿñçß· :÷çÛþùdøG ¯Ì·ß|ѺiÃj5î"+ݼßYëÞï\üí‹&͘W¶B%ïuˆGþÞ3L–«J€“v-TPE@P€F [ëñ_^jÞè©z÷b\úßèy†kЬwUº×CK—-OƒjÜuO¹ •QÀ&¼qE‹•X¼j}ãçZ•0fØõï­Úü©¾?ÖÉxäŸðFù=%À~‡T3TE@PE Å#Àè9uêÔür¤øÆhþ‹@›fO•º5KOÿ­gɃªÚãÇŽ*z[‹¿§öƒ9såŽS>‹~úáBd$9›!­þ}…÷´ñÈß{†Ér5m²”êK¡÷ïÃØ¥|Ž›sú"2t»Ë–^½r…¶ÜWç¡‚…ŠÄ£Q+–ý,ÿéÒ§kØä¹téÒÅ#M¢(Š€" (78 !Ò¦M›&M¡Á78AÖüÈóç!]QQ‚¬])´9 úáÄñ£Ï>ß:Yê?{Ú·”ûòoÿdÄÓ¿­uïýÉR¤,4™ 0:÷]Û·<°ïæ›sU©VÃÊuûtëøëÏ‹fÌ_vGŒ{R‚âcY—/_>°oOêÔiŠÜVŒÏƒ—TG8þ|þ[nÍžãf/b];´]ùÛ/"ðÍôŸâG€;·9äúúšw×.T¤¨§1rØ¿wOæ,™o¹µ S†IÁ 3įÎÜ4FPE@PRY²dÉ”)S†  ÁÉÈ+nݼ1<,¬l…Š öÃ#‡îÚ¾írôå"E‹¥K—žTé3¤÷û¨fçö­xÐeh«Õ¾=»A¬h±â¶x·§˜"³þí_¶lÙñtµöÏUQ.0*Î'¯[y"‚nÙ¸.ôìÙò•ªäɛϭ„jÛ–ÍiÒ¦-]¦\¾[n52ááa!ÿþ‹ÆP¢þ¨þh62Tc÷Žm‡î/z[ñâ%KÓÌ%¸  =[²tYÂ!'ÿݰnmÁ‚…K–)G?±Š>vôðÎm[ÏŸ(S®b±·Û®ÆïÔK`d»cë–‚…‹0ÞÞ³kݦt¹ ¥Ê”“‚hòŽíÿ;r8Èæ»E"œgÎ’ÅKMpL…㱨 ‘@}kB"i-èÒ¥KëÿZMÿ¬ZýNrS!'ÿZý{Þ|ùËW¬’!cF‰ä¶~5îc±1¦b5xLâ;Ò§[§Ö¯¶«rG ‰qû{ñâÅí[7>°ÿæ\¹«V»3SæÌˆmÛ²é—% DþцMâ2¥ÿ±ò·ê5ï~ù_~:jÁœÙýеÎnk˜‚"í=5ɪN/üdäàñc†s;¥P&ß|§{ûN]åtó†u<ŠeËDz;É*ì,¨ç;í§Mù†øz<1nâ4§€Ä¼ûæ+3¿ŸLøÞêNœþ“'1ž‡u­æ¡]²jo"Óó<É{Š_¹~ç•èèÚÕÊ𘑛'1â{¿÷ö” ã |õÝ<ÔÀ*É‹t5jÝ;uÎk¼†E@PEàA gΜ3f„Ã’EÌqP¿_c¯Z½æèñ½óسgNwïÔ•šIeöÏž=‡953§O‘$[öìŽøÔ»JÃ$!À@«áCw_¾tiå†V]޲êÞUö»lÍ?ˆ}?éësg½Ôö­ÚuêY“›ð?›7°®¸ã{ï—*[¾ç;oÂäR£gŸ4ê3'«üsÕò·^mi´ÞíÑ¡KO“mÿl~ûµV»vl3‘,1öÉ—¢öAo³×+^µDHwÞ}a¨õ¨!Œ=”€\bDÚµ÷À–/µ5–ðÝ:½±lÉÂõ»Žöíþ C+V¢äøÉ3‹ß^RNaõ=ß}sÚä ÆÓSO7þéWr5~€ÇÚ¶lZ’¯¶ïóü ×{”ÅœÂÆ½.×Êóœ É„®KÌo›¶oöì?ØœZ¡gÏôéÚéǙߛÈÇ=3häg0OSÐ=÷×yë•D Ã]C:l`!8PíÏ'Ïf &Gœÿ–’-ó/?ߤ|ÅÊÝûzó€Å´Hçv/>x@ReËžcò¬ù+ßÁtyJdüôùsgMã=ÑøY@»û¾PÅý¼ð§ÇžºÖ1ÜÖYŠKÑ¿ÉF€»txí§ÙÓy;<ßúÕ[n-°oÏ®™S'ÿ½æA“#žæ–dz#0!^þËÏTŒGë·_3ýãÖØ˜÷/ì—nêEK>,|?qú3eJH{Ó§OÏœOø]÷Ô6¯*·þ²øÚŒÑ7_ŒµàÍþ&I…JUÜ&ÔHE@PE@zòäÉÃØ%04æ}P‘h´{±ÙÒÅó~ü©æ­_‡/š7jÑô‰zKÿÜdÓIZKïѹ=£¯/¾úÌó­¶ÿ³åëÏÇlÿgóSÏ4g\ĘÍ*iÂ×ýµhÞœ¾Ù¹{ᢷ™xïOƒÇž„?Ì=ý•7Þ6Âsg»”"¨ã$f꤯6®ÿ+OÞüž°ˆÍûqæˆAýQ–4nú79@?Ô¢ñ#Õïºçí.=Y5 S…¯V¬R­NýGDŒMeÙV‡q)”µæ=µÃBCçý8cå¯KáêËÖlÁÖò©g›ßZ°¾|Øÿâkíóå¿…S™ò%y»6Í—,˜Ë¼y«—Ñ£lÙ´á«OG÷íÖ)ôÌé·Þía­I³§ M…@–*S~Ú” ëÖþÙ±më^%2¨:§Núš{×¶Ã;iR§ù}寬øÕ$à>ö‡åË–pÇár D)”m{ÞnÛ­ï§¾‡‘Ο3kÂçŸEÇ®½Ð|šZÙ/5oL£è6MžkÁî¸p~˜KÃ&Mæö—ÅóQ™ÖºïÇž|úï5¿ÏønR·Ní~_ñ+#díë-ž÷#øÍ—[üºv++¨šûm-u ì%ü˜/¦8g:Œ$öêÜ~G~6/ÍÐàéß~süè0SüÉx°¦<Ú°1iY; ž=ý[C€uŽG˜$yðŠ_—Ò‡ *2÷—?³fÍ&¸¼þv—Ó×'½è©D–¯T5!“*Á0±à-ÉŒ×ò_–`È]®be[m™•éÛ­3fuê?Ê ÝÔ&`=ݼq§*û¡É›7®'+Þ†ÖümaêÏ,æo^‹L7X-m¤2«x«°-C=UE@P`B W®\ŒÚ—Ëb`pRràUË—A c¿þNPEýË õ£ø­[¨ÃÂBÌͬßàQ0ôB×!<|pÿ³c¿t›„Èâ·—B#—5[6”`ždÜÆ7iÚüãŒï­xÞ3~ê™f’¤ZÍZ{÷ìÂ>Öm&kê7ÞîòN¾nTÿ>ZÚ©[o«Öe#¶²x±lÙ²}اûŒï&Ì)ºÇÃÇ>÷‹"óLó–m[5·±#‡të3m$,3„>ݬeÙëÔaÈì·DÉÒ3þ&ãó‡~œœ©É矌hÖêe+>Œ$§ÏûåöReHذñ³÷V-ÅèqǶÄ­ñŠ_\&„=úu=N+DñÜ÷þûmظéˆÏ¾6ÝF‡B»ÏGñ٤VÕ v¿TÃÚ|.™žû…_Nœ1O¨i£gš7kùR‰ÛKœ¿Ò®#Ó´EëÇáˆö;`èÇL s®wweÌ*ÑßB¤MB  w}£M3&;>ðÝ¿'ŽI…aV[t‘ܺe#h?Ýìè71LOÙ$,ô,ê–¹~7ñËW_p¹óÁlØ­|Â#1s%\%‰¶†‘-èaœðœË‘‰(<内:—¤h–sÅ01k–+ó‡fž5Þ–äB䬗rÜ|3§gc\=Yã­a«¦”x¬¬g-\ŽêKc °q­ÊPÓ*×püúƒ”Ò"FÇóÉðA}ºvÄN¼ÏŸƒŽ£zOÕ`kR.åŒË°¦rŠÅ¯?H>S'OÀpMaÁò¿þ9p Ïj6’o+QؾXJÛ.%ã)ÎÏé̦͡,ðLHMp@½pîL¬Ývpç±p뾲ə9‹„äài“ƒ8;ž-\±ÿã<m[=‹û;KØ# ã{ÍÀ·g×NÂâYŽ ƒùÅy˜ˆ³}¼˜¬¶»²Ï›Z¢#ιÌ?²çp–`GÍÓC^0gV—·^}¥ÅÓ,@G? ‘f‘°¡Ä’¿ù½®þ½¶È¶\…J\æI`@¯÷D5ÍL§×WäzÓK““Ø–)Ž©JL¯1 —•÷×(´?ÜqÄ4 (Š€" (‹[þæÈ‘œ\[þ:‘y2Æáíçc†Ûü’bùiueúé¨!£0|/úJ4Yûxøüg`4Ë©¸§BùiͭÀ÷»Xi£ÇÁýß'+«˜ S"Kg9e‡aö!ÀÖAæ*¼º¢BwyT™=ee ýÿŠJ®¢³éÖñu6@±&q†YÓÇæ½†³‡^`a8Çr {ŠaÙ0î]p•ÄöëX±ÞS\òÁ€§ÌúÜùž¹ß%“ÇÚ4£X‰Û'ÏZðìcu0»ývÂø>à’0FÛò øl¢X8Xmw…Ë=~}7g)wç6×l"žÏùÅ©:¿Ð? W0‚Çå÷½÷×eâð‡ö²Åœ(l ©¾¾ Øå‹¶Äl&ŽI69Ó xЫʔ«È©§Cêé—}w¯uœ»-ñšÂùºU6zxfæ°˜3s* ¶mÞ¨°Üâ¦‘Š€" (Š@P"€û+†=ɸå¯UúÖ­ÿ(;!=ö@Í'Ÿ~®h±çÂ/-]øÓ‡#?•mKÙøcÈ€^¤½ÿ¡¬_#Àî» ÃØ²µóºµ&O”%ìkÚð(ÑL|1v2 Ç¿þþG‘aTùÙè¡„|,\Ô$”>5˜1熿×BP©,×*ÁÄ ¹ÂýUf‹»VݰC m¿ïºÖT¶0™°#´=gX0gJZ6ªU­MØÓ)ÂÝû~ôbÓ†0 <(™BÏœY4ÿG¼&v¿&aµwaƒùQŸîà€‹¦6mßbC Ö'²†CȧêÝWÿÑ'Ø.˜m‡©-ê3p˜Uçdòq`Êà‰ºµðøƒë¦\yò€Ö¶NͬrìÈÁ¤}¡M[Uܾô·•!òß¾Ðäч´ ày§[Ÿ¥#Yã%üäÓÍÆ2‡Þ4XR¥JýëÏ ™Lùä«dЋâ›?¦'P¹EDDLþj•4ûl9+ïK óˆÙ(’$d6þÏ =ßܲXó¤#±b< âp»o÷Nt<\Xãø‰_|:é«q›Öÿõ‹5·ÄHfLó@¹Þ£O@€eu;1¼Ýx€Åo¸´Ÿõ±ÀŠ9>®°$FüÔYšÏëŒ_ÃK óÜò¸Úx2dsIöã:tÀµ¨˜µ¯ÖY1b¼6…m¹Š. 0‘1®³Þ‚îò’•ìÛ‹6˜Ù8/t’¼u“Ë–x9÷^t¬WA÷»/°¬@aí"À³¦¢TÇe>N/ÜXk¥Š€" (Š€"°°å¯¬–_Æ0 øô›©8XþòÓÑF7‹i\·FÍ»ɼùósåê•‚ KÌÁý{×­]Íb·>ºt*.D2b„‚2DD‹;æ‹ÉDÊ&%c!’нm0Ì”)sv‹ŠR.ñ[¢d)ì“!±„ñíÒðhsÉš¾ÐF¶üqÒ’+1Ú㢱Y2³ô¬õ+íztnÖ‘lñö4ºt9]ÚtÄ3l6…H“Úå´Ì‰ß)¼O‘®°1G[ß÷z xµ}'kBö(ÁÙ jÀ ŸBrñ§ƒÀ„isGê/¸ §é3d`À<`è«Ãdv†æÆ’Ö Ó¥sUL~ô²§ñ¤/?36Ï÷Þ_-—qÎ,u2ÏË­ÖL¼‡cín!"O<“Q"K/ï¬u/¦¿aagÿúó÷iS&txµ%ÁíVÀô‡©?-}ÿÝ·ðeM$Ö6vy¿?d˜°Û‚tÿ½Ai:FÝx‹-T¸¨uƒä»=ûñdz[8 Ú9a´ ¨|L¹ 9|£câõ5@²`‘¢6ç[øÁE¬e›¶¶æHZO¿±ö·a‰ Gx°ÞÃ,½49Cð°‹fRcÍï+Ü`$©3.„Ðù3P‡ó3«bf…Ü„2yãÞÜkS z/»@X¬—|SÃÙ‹Wœ>"OšyçMñ=7$áAvÇ…¸uêÄš3®~eyÖ¬W݆yü8´¹ýËÏ?Ùä9˜'Ûy=Ó¼%&.NX¼ÚµiÆÜ%Ë”[¶xl“uü]{} Y5x¬á°zC°5¨ýÈOå¸9×áƒû± aQ¸u¢È”K@ظ"ÂêEÿŒSuk¹¾"÷ÿ5;r;qçÉ›ß`·~3~,60ùòß*˜eèõíAéžiã¥QÞhƒW&ö¬XŠ«dAoëæc†}ÈÓ –Vˆ[‚ò1[¢[Û۸7´Rh«Œ†E@PE ¥#€b‡ü„ƒ°9Rz»¨­ûÀ£'Þ¤Ð5á~«Ô[·ˆ +º¸Dj ŒnƷɽ¢µÜ&±xë__Ô¿Ö„ÁÆU&‡ßÏY’4tˆ-] a¨Êº¾Ýe>ª/ XØÌ†À˜ Üç-Hƒû~eë’šókÐñ8%tëÞ ïõú€½Â_–›¨ZýN+Ü;¶má]©êÿ7‚ëÞ÷CÌ*ЦŽòÓ!ïR»N½iS¾©~g-“Pˆåèñ“XuMGç <üTõì?ÄX\°.ⳉÓz¿÷6;>óGZ>Õ“-°}›ËÐÚ¶(œ§‹õýpZY`’Ðï²×=`ñŠ”]vzäÿûÑ]3¨¾î’JÒÊNåÖXNYM…°˜ÎÇ_d¢¢nÊ" M¶Ò×u­&pÌÌö­.Õkþ(ÉY ñ6Á|!ÎÉ$RE@PE ÈÀ’Ž!„p`†=è!8‚£¸€¢-3¿›„-´iš> 7Šï¯Àò_–à6EŽ˜%šlÙ#“½[ñ«jb4EŠ›2{!F¶IƒKü¾üvöà~=¡ üI¡–/¿þfçn^|ô$Mõ´”dA © 0ÆÏü±¨€UÑxñ†Ññæµ¶|Ôç/_ºd›š¹à7ÌY kEž•è3üÊÆÜáaaåK&X«[µ¯›6®ƒ6ש÷ÆÕ¸’GK ^÷Öâ³bvÉï˜ °–ÈBÃm’rúòëšµ|Éf*ÃJô§Ÿ{ÁVmä߸;•Å9*Và`³gºä†.÷Ÿë°zöÌci–m°Bxý®£Y²f“úó¸®Ýz¬íå”&ãËøÓciǘáQUq!%²sróV¯¸µÜ?e&+Fø.ú÷Cèßܤú«(Š€" (ñC wý_Cœ©Ãô8Œí<ÔáF†šž¬|ÉãSíλ¾ÿq±Ó»ãIÙÄ{¶eËWBê}[Zï9¤¬«6RT×Ö¬¡euá‰cGñ%~KB,£uÚë&AM´ˆAÀΓ¦Z,+Ï^.‡Û²èŽÎéiz`þL>V6ÈTT“Ù8¡Í,ͽµ€tð²œÀ& ³±_p²_â­¤{o¶ªÃ;Ó“&O€¹õ€¥7ìW„Y\nR°6VâÅù±÷»t8rò«ïd¢ÁD’³[ö+ž 6Éã.H~\S©¼" (Š€" $ìú›1cÆ€Úõ׿-e0ƒv„?ÿfë)7tlŸáéj¬ñŒ{“Lke‚XåAÜ@mšï$ö½~ñ–»bçJ×xg˜À„-wn÷ºÆNøÞÊðeÕ.6ÞíÚ4gáJ‹_GAxÞg37¼s‘¶b•k†âø|wíð6ò3«Ë±xdžð$ªN8†šƒ" (Š€"à/pÅP$ výõWÓ4E@PbE h ðuÏRÿ÷>+‰*p>âÜËotx°Þ#6q˜å°Q8Eãœ9k¶lñ«ÃÞ];HNZÖú>û|kÉ$gî<ßþ°H¶;Ž_¶šJPE@P‚výe‹ª Áü2O­SÕÁw—µEŠ€"à ?›§î ¹à©¤$ŽgÑïÁ}{+U­îÖ29‰+¬ÅËóÿ}˜½·ñŽgF]¹¢†ÐÞAÒ«Š€" (Š@¢#€Ï«ß'¿ãeIG ùM8ƒÆD] Px!à;׈SöAK€ã„‚ Ç8uJY lý_¡šJPE@Pˆ€¨|…ý&0«x'Woè4¡"pƒ '®á;&Akí;*™4˜o-Å©[¬¤Á\KQE@PÜ" e·—4RPàF@ ppßßm~wôÆhµE@PE@P F@ pPß^mœ" (Š€" (‰@"7d[µRŠ€"@¤ ºhUE@PE@PE@PDC@ p¢A«+Š€" (Š€" (Š€" J€énh]E@PE@PE@P ?oƒ”hõÔŒE@PE@PE@P! àÁ§‰E@PE@PE@PR J€SÊÒz*Š€" (Š€" (Š€" $%À ‚O+Š€" (Š€" (Š€" ¤”§”;¥õTE@PE@PE@Hi”Ú‘øòÑmŽ8ZÒ(´mÓ†)Š€" (Š€" (Š@Ð! à »¥Ú E@PE@PE@Pw(v‡ŠÆ)Š€" (Š€" (Š€" J€ƒî–jƒE@PE@PE@PÜ! Ø*§(Š€" (Š€" (Š€"t(º[ª RE@PE@PE@p‡€Ÿ½@»+Bã8#põêUÒÈoœkE@PE@ð©R¥"3ùõ_®š“" (Ƀ€àäÁ]Kõ„¤×W®\!ìIRãE@PE QÒ›:æ  åÀ‰Š¶f®(Iƒ€à¤ÁYKñ/¼÷rÌ­ØWàTNPE@ð70Þ4iÒ¤9@„•ûcÍOP’%ÀI¸–çÑýB~£¢¢.\¸À/aåÀ^ÓKŠ€" (Š@"!×…ñ¦K—.cÌ‘!Ca¿Ê pÍVP’%ÀIƒ³–â¢þ½téRdddXXØùóçáÀ¢ö)½ )Š€" (Š€?€år ú…üfÍš•,E̯?²×<E@H6šŸ Û¾{_Ù’ÅsdÏæDhÖm™3e*Uâ6祔súÌÙ/¾Q±LÉGêÞŸðšû7·„×'®9ˆý3øÜ¹s¡¡¡D ×|T^PE@Pâ€àôéÓgΜ™L †«t¼!Õ„Š€" 45þ›#>íÕ¹ýûÞ°áuòÔé šäÉ•óØæU¶KÉ{Šº2®“£ó~þ­ÇÀáE س&ÎØY\BrK^èÄþ™_…h4À„åRòVOKWE@Pn Àp]x/MF |ñâEf¨oœækKE ˆhœâpâ…¶‹–­ØòÛ¼8é¥kU¯òDý:µjT‰k{ÝïÜâZz"ÉßÝb®Ÿ)sÍVPE@P|D€ÍÞmœê+¦Xê’ÃGÜTLP%Àþ¼Aç##ÑUâ»)N™Þ^¬è¬¯ÇÄ)‰»-.޹ţ~Ibä#:^>®ÑW¢×ÏxÛäõ׬´Åª¥É]Tb¢n‹>u }ŇU@q hÐçBßô}Oʃ 8ø¿ ™®xu¾¼UšŒä»,‡|¬¥¿ü^>ºÍ/ùh&Š€"¬¤-P6‘š<øØ‰“gBCË•º¤þ 9µfݦÂo-_úvÖ«8±ÃªvÛ®½{öÌŸ7O*ðph•á-¿{ß­;w¸%Å2¥2fÌ`½j ŠŠº¸ä7—vûîº|9úÄÉÈÈ œ<|4SF—§ÄâE c>$i·ïÞ»c÷^"‹)L­Lœ2sŽ´SäÙ_µ$Ï]ôKÛ.½!–"skþ¼ã†ô3¨V¯ÛøB»w÷<,WÓ§O7¤×{o¼ØÜÀ"m_µ°QëvÛví!~hŸ®{÷;á[‘iôb; ,ùMí»jÌž¿¤ûÀá0j“Cåòe¾ùxPùÒ%%fÝæ­µ}¶Þý÷Ìÿv¼ÄÄÚ–÷?é©8gnäk£V®ùûÅ·ºî?tD*€×±ES¿ªV©¼œ&Ò/ŸUx/†U¬û•ãJtë~M‰Q‘ÒžL“ñZLôùóÑ‘Q—T@qˆé"ÚäIQ}OÒô{!B¼q€ãĵŸã4ÌG+õ@õWP‚à!Àrkz¦õ¡£Ç;¼Òzùõ÷3ÿøkCËöïþ¹`º¹qèl·iR÷ýNí*–-µkßO'|Û{ðh!ÀkÖoºÿ©Ù³eýä£ÞÕ«T“æªË%Gô™³ÑgÃÒ«€â ýAŸ }?Ä<üè{RP‚C†Ó§ÑýÊ–¿©.^†§ùL_ïeú_PàA Øð‰“§~ûarÙ’%¸EÏ=õXñ;ëþ½é Œ+”q©[™Ô|§Ï Ó>iT¾¯·jöëªÕrKßë7™9?»«Zebî¨X®X‘B¸›0bl›fM¬ÖÔ»öøuö¤ªËIB~ï¬Ziá/+ À­š6†FšøŸ¦Œ£>bYDäCµï.Y«þÚ ›ÿ{ò–|y˜3à¥-”å©8g>±6jÖíèÃ[5mb$nj܀ãÌÍ_1|XQÿBzÍ–¿¸}æôÒ…ð'N˜R.žM“!$ÍÅk5:äÔ•Ð3éT@qˆé"ÚäIQ}OÒô{!BZâ¶"¦ èö¶•ýšxg@–%›x>0h§=~ðÈ1ïØ{[L†Þ¾4ŠE¿d²üµÐà|yr{ÏÐWÅþÙlù˦G¬C޾bJ¹t6,uúÓi.]'À§Ï\ K§ŠCL‰V‡ŒgõáÕ—˜¼Ï‡@ÆájxXú³åo¼>G_æ‡J©¶þ*Š€"LΞ-›õöäË‹ÓSgÎJ¤,Ä-£ ¶ŠIxOÌ2ÝÓgC_ïÒÛz5]:Jx‡²àê•+Zeb £ˆÆ Dú|ä9ò-žœÜ&÷Þ·Iœ‘¾4ªôíÅ6¨;gÑÒ ÷?Þ¶ås/·x†M‰Yù1&Æ®Êõƒ¾Ý¥ôÙò÷ܹs®m/G…‡‡›².ED¦9‘:Õµ˜+ç"®DD¦UÅ!¦‹h'%pqˆºšúª>¼úsõS}™ËÓ˜8\=>ý¹sfËßtøžTõ¯Ü0ýUnX¸zý¦›³g3þ‰‚¾ÑM€ñÅÀ €9o˜‰ÌÁµ?»—ƒµ»Ö«åTžÖx >æ²¼…ÿù÷ëU:DüùJß^Üé{¯Tm:t…þ’$WÎéÒ¦Ã[5aèŸï™ ik‹i}lÔ÷ã†ûå¨ñß|8zÜàO¾xí…¦ƒ{u‰^ëàIÌí–¿9‹TöÃÿ×ßt“Ànb2ÞtSÁ›þ6§ä­°â 8Þs± ÷uƧ>¼ÒEÅÌÓ(ý!ÕßÿþË_×%×$µÜ*ýU F€-lj?õ<*:¦½Š¼EÖ­j‚µáM€KÜVÜ÷^wÈl½ûæ´T‰bÖÈXÃbßkü?Ûä ÞšŸ˜»kTe °íR¼OÏœ }¤ÙKaáã†ökÞè ÙQéé—ÞüqáR³*8Þ™û’ÐÇFá÷¢{‡¶Û¶™úãü^ƒGábšm>ìÙÙ—"â-ÃçuÃÌ·uçFPqPtƒkÝØYžÅAqHú ®«½¹ˆá¯ÌÐË-Ð_E@z–,_…³¤ã›WÑÒüîÆ/Rûj}«š—‰Qº²65ü\+u­7cñ²•œ–Ùõ×ï=|[‘B¬ß¼W¼szƒm{¹ºuÇn·W½ç,W…ÓFœ4«֬;}&´é“¶iö´‰ôWÀYœ3ç85 •oËgŸªV¹|•:OÎ]ü‹ß °L'»f•c<`Åü\ûܦºnmuíüú)-’P¤{+Š}À¼ë´?hÐþ OAÝê4iÉÕ¥+þ¨U½jölY:ºø×•‘.løeN¬eÁ{q:ÝuÀл÷…†‡wx¥Õ½5«ÁœÿÚ¸¥eû.õ¸gã?Û?Ÿ4-cF×Òå„Oµ:‹³n¿djk£¾œ2ý‹o§·hÒ°rù²ç"ÎöÍw¤mÞ¸¡É!á ïe†‰ƒé%9®DGEDDDE^H{>2MÆk £ÏŸŽŒºaÖ Þ¤‚¿â 8ès¡ïy Å!ßÍ‹‘Q¢¢®^¼ÈÎ > ‘;ô¿.D¡(Cëukþ¼Iߨî‡-Xº|ùß-KÒ×AKô#'N†êùÎë­›ÉiÂù²r@}£¢¢piƯKï ¾xþÔ©SCÃÓœ>“æªKíÌ}ælôÙ°ô§þÿP©€ £8(ú\èûAžÅAqHÈw“‘Ï¥°s™Â³d‰`ãß 2¤ŠùL ªúëöËxþw?Vêç#½ˆ¥ˆK¨"|4vMÍI‰•Äå²ñºÜ¶u30ôÕÆnâÚ.ØÄ̯Æ@”–üæRúx|8ú³Fºüá±(Ö5›/5ºÑ£õº}0ìW%À>â?±ÂoåEƒþx/^¼„g,8°3«©Ÿ€ZeÍòŸ¥Âhÿ»¾õšÍzùñúòwòÔé#ÇNÜV¸ ¶¿ÖÜP /›= †¶{ÿÁèè+… ÜblƒEÌmAr‰‡g}=†y‘£'NÞV¸€T†½ˆ·¯ZÈÆ¿‘¢J/ʇ‡ ƦªXzŸÙ¹–O©†Û"œmq[œ37²õÞ(ôÆø|ÃuPtšìßw"íåö)[þ†……±ß/4˜ÓKÂOœ8qñthš !i.^»§Ñ!§®„žIwÂå‘[P´?ès!Oâ 8è÷‚>Ï"ƒŽ+¡áYΆ^ÉêÚ¥Œ/~˺zécúܬY¿éžÇŸkÞø‰o>Ü-M)­;tô8U-ã«(!u.V¤qÊQú”™sy-˜<}N¬¸Éã â”` »a’YQôÞï(¬8«ƒs;mì×´Û/æ˜ã—-YÂ[n ² äÊy3Öª71œö+‘¶S·Exj‹³8[n¦\/B&O®œüaÿÄþÌf¿¡¡¡p`¶ü¾rélXêô§Ó\ºN€OŸ¹–.$ÄT@ ÅAqÐç"Zß1â oÅ!80–à¸)ôܥа47»öþui€]X=` œ7Ä/*Úqþü ÑÚ”ÐÈO¾š‚—"{&}eW­]‡Ç,œàb`¿bõ_)Z¨`ÒW#‰KL18‰qÑâü…óIr`lãRú^¸û…C€¯\Ž ¿™æ\DêT®yhŽ+ç"®DD¦ ¿vJŒ Ä£8 Šƒâ ï}Oê÷"þßMØ/…T‘ÒDFf¹pÁõ-lõ/CˆÝûlݹ»À-ù+–)å£ÿ'¶6ݲcÆwyrç¼åºsÝ·ä7Ÿ×^£ þ‡ÅƉ¥o/ÆÎ‘ÖÌØ»ï§ýùX[°âl•|È„ÆnÚº•Öy^ÌñX>ºfݦ"… °L­‰µt GE]4ˆ ÎPÙTDZFfßÁÃG޹ô¸}Ùµw?¬ÿl*£ÇÿÅ¥+9c£k[ðL‡Ù¼mgÑB° dh÷÷¦p\µb9ñÌjJ!@ïÚ´m'Ž»ð´Z³ZeJVIßÃÞûHž -Wêvš¿ä·Ud[ç¾»L¹e«Và+ZHÖ‹w?Æ \·íÚËÝÄEs*Ð$IUMAœbOŠ¿^¶Åq¯iÈm8yêT• e­ùOûqþž‡I—6í;o¼d„‰gÝåâi_¹½•FŒþüÏŽ]ì5ÃúPãÉSž&U¬ogÎEæùÆOdÉœéÓ ß};ë§no½fR%<“U@\ún?VèòÑm~ÌM³ pÒ(k é`ÿkï,À¬(Û>î% (,- %%H‡„4"ݽ4 J§t7Hw#‚tç’Ò    Ò’öûêû}¿³÷ò0Ì Î.ÛÞsk®gž¹Ÿ˜ÿÌ9gþÏ]üVæ÷JÖß «€" (Š€" „)(€;ç¾/mödé³&Mš4Q¢D±_/Ôug·oùøø$I’$áÝãIs‹—ú½õ§™ÊdÉ’%Nœ8Nœ8p(ôËÏЛ7Æoþи]w› ‡ã˜=_˜dÕÆ­ú ¿ëä Ø©ER~¸œùÜ%«VûoëܲiÙ’E] H%dÀêÉs6ÂR ðó¥ ýsȳ‘)Zàýy“Gâ3(5v-T±n·6—û°(aeñ(”úþ]Ú èÚδ:ðÝ÷Í:ö‚K l[þ…‰±ä%à¦7)¼ðyˆp<¬á×îìÅK´âŽsßYª ¡lrøóÙom®ŽFfã¶]´2 ÜÄ •@@º}ò›ö}†ð¼I“"ùó®ûrúý‡J,•Íë×âñf;ùóç.^¦ú=qX_ 5L¹zÍ'í‹o­Ô8ïYh @/±— k3æë.û¤‡ J·jZï…Q Y¤H™»x¬˜1¯ÝsèûJToÈrÉɽþfîú' 4ƒÃ: ´7DÃL5X‹FÁj¯ÂŠ€7À~¯îŒä?7Ïþs? fŽò¦Õ_GÖ¼–îý‰ß‘PôyàÐï…|ÅAÿ/Býo1w-âª8Ö¦ÙÌû´àÞ?¾-;߸}Šþë￉W-+ °ßÖMëùÕ«uòÌyBÕÂóÖªZ¢P~wž’(k6oÏÚÄëÅ+3æ/8z²`Á¡L]?TÁèK1.5u¶ÄÍ;÷až±cÅ$pÖ‡OœkEû-޵?ib”ÂñSgáÒôàý"9iΖ =Õ£}‹¯¾JäZ>ÒƒsŸ¶!<nÞ¹´Iíš5dU‹Õž½s?^F«oîÔKöïyô<«8ÁסE@PE@o"3ï5Xô2Å׆3IfAeÞY!ùËÕ6a:Z5wÆ¢¨^Ñ|NÖ·j¹hU8¬pQUeÎÖeªH± —¯ÌëÈû £å‚ys~{ôĶݪ”+% ·ìÚ‡i1‚D¯˜)C:ØQ¼¸q%¶»þ=ùåÐæ8‚"7«î×v󎽟/^9¸GGkˆÊÞÕ_JœšzÕ+¥/ðt…Á„;El÷C°_”x6.õ‰ëˆ ËÄ*–.Y¸’ï¸_´l\S^(ÍÞƒ‡!Àiwoû‰µó^CÇnvæ˜!†ùùÖ¬ýqGò›@ðP#aTÄ>n<~Ho©ñ‰—$ ó—¯|üÔ9§M}k0C`ÚVÞHM÷þy¸x9`ÏÚ…XeËÜ?ùc¬‘§Œ@ Ô.Cº4¥j6Á6{þä‘"cÛóàuä¶böD3ó6Mëó Y% »Ã’Œé*œÜÙ³«Z ¸_­œ/&å,Ä4íÐsñ*ב¥.\¯Pÿ“ò«[µ¦ït‚µ6O‹u)“•9kÄ šK#ÑŒ³5|µQASøçÿy‡’zÆjokÅ÷…ÒÁ°Ç@€5l–¬Þ`°MžCð$• bYˆ¡´ÙîœÛFžšP0b‰<£3QE@PE@ˆê`:‹ -oáÂ~årÈsCç†ç­» DSÇ)h§ Û%å«?ÙâšzkCYLa[7­o­ô¦Ü¤n Ä–¬}f2º|ýfjÕrhVÙpÁ ø~6¥dîwû™£ ûE*Ò¢a] [vîµÉÏ0ÜDi¥OR pí) Ÿ/q¨‚;·òö+mQÌ–*ú!¯vî?hëÍzH0Oâm?_ÇEÉßoUÑc?­~æoØ/••Ê”d¦W°ß¦°ïàac?,õf\Àƒõ< ëÝÙ°_F$« {ã4K£nÌé=<—®^‡ágÎΰ_Z©YæàmθaÂ~)ó Ê’Ǫ¹SŒCu™E8uåšëg-.’&KÑ ò)[×5­MœÈçªAæñ”Ým,Ǽ£<“‡M˜Aaȸ©î„qÞF“©6ß²z5*#¼|ý–ܵÊýQ5ºÅ[-:Ö¤ÜIFæzÕG滣sSE@PEà_‡À¥@ûàÑã6=Z/þõ×/®èî W´ž¥Œ '{ÿí»Ñ‚Ê©#Þ˜VJlkò2‡èîº üÌÿ«ÝD±BÉDm›(a|ücƒÛm<k“Y3qˆQ´µ’²M,iâDTŠÒÂÅKWÙÃxÙ[7–`¿—~²VÚÊ êhóUF÷Ž$‹"s •¯µ¹mèÆÑÀ£‹Î^¢rë&õ>iTÇ ÊÚÐËr°ž‡|¹ž£¢ÿ4yX0iÆßU,Õ â…¢ÛÃ#!úØlJu3´Ý x/ÚQ«:4Q ÑøcK`Wko¨Ù¦ÝZï\îÔ¢éÚÍ;p»Å¾ }ó†•Ë|hn„M*ûßgl•îWùoßjXpŒ½ «vúüؽ—û°˜Ë†WïrYµ*•G­û¥³UE@PE š# Ä|èûãÖKMÏ'E²¤™Ÿ’[ë))£ÅE7õÙäY×oÜ*öA>Þãq¦%<ïÁ4ovîÙe ‘az+7lAAJ"ÿí{0 mê[Ý„ vÙÊ›Êø|kjÏÂxD#`lÚ7£4m$ˆGùá#‡q¬»í§ÀÐÐI•Vü`ùàÛÌdÜÅ‹r¶H_6kü˜és'Íù’{Um«Æ¾£ôð`‹kѹâç®HzZ»JyBU•®ãרv5ÈÞ¨)³©ïм‘ó@RC¼h ¢Çv'ã\Oxu* ™¤,áâ¬5έ¼©)'ç×—ö>n×þƒ˜a³ú3wâbnyÓÖƒŒÄ>yî"¶lbdvG€m’QôP p½q:mE@PE@P¢'$•áÂðÅ8XWˆùwÓ½3}þ>” 4aHg†¬n=7©S Ldf°Ø?7¬U̓¼—§$ºrª¼ßp:¥!Ð4¼s÷gÊiÓ¤25Î…äIߢòÞý‡¶S(·a¿P}wì×&/‡,ôéÔºkëæ`2`ô$õëêRø…•!~è™ Oß>Š-ú™ —„éaH?w›=³u’ÒÉý¶U2ÜÊhæ N„ª‘“gã¸[µqk¤É- ÙĪœ¶[wísîe£_û-n‡y´ÜÔ8ZÞV½(E@PE@P^—W^…lÐ’µ•PÕÁêÀÈD!"*òõc{í\ÿøÇïWÌ™dcƒÁêÐ*L VcÇŠe­ÄÏÚ‰^r²mÏ~´sÆ£Ò*öÂò­;w­2ûá0]Þo‚®¼¶&¾uÔd}7ƒÔ‹Å«\Ž‘”¶‚&ù­©¤`kk=õÂ2*_Ö6-vh\ õByw!~èe¤5&‚×ãûOìÙˆ/ϳr7õ²RpìäÙà>~ú •Sö¸‹k¬Jàbý2}.YãHÿKê¯?NØ>8ÛÁÛæõý2cE¶J€#áMÑ))Š€" (Š€"ñ@€1ãdÏž³ðf ¿SÂ ÛÆåÕßÔ¹Û ‘V¾÷Ï?ÿÃqðØ)GOœ¹ÿà!e‚fy)И¾VšG”]‘BÉ&)‡xöJA²ãbƒmÓ Í0%¿=ñ¨lP³²õ,e²7u< T[½íðãOû˜ Á•tâ¬ù4©ã‰§Ùzà_ö“ç,€ò™³( Š Ø1‹zÑpÄÈÈPÀÿæƒÚsÂì/M=êSÀäÀZ¦ò…âs F,ΛoR&“©!ål«ný1P75ž ^>.;!2õc§Ï]¹q ¡¡®Ý¸EŠ`tÚ.…¥2WV¢†§ÁÚúø]¸t•‡ÍC«°;e{2ß ¨fÍJ‚¡Åþ¹nµŠGØ6‰Ýt°º=zòLßãÙK+r/óä:Ût2óË¥#&Íüë¯|Œ|˜"¯ 4_žSç.+˜ï%op˜Âºã¯òÕ¾¯IËG·eK YÀ¼ü%:"qÉëרôò^(¡{Ú›" (Š€" Dx‹àåŽ'48<§=vPorÒb³Jô¦BùòÄó‰pý&L'Û㻹[Ø&Ìœ‡…í¹¯ø/š%5³Æ )Wïcâßʡ쉌5ªwôfBcú}6BùRÅp­"å>÷—ß~Ã2Vj¬ûœ%«æÌ–™TF+6l¡¾F¥2Ö³”שF’!H&å5«XÏhwd Çi¿¢Æ´žµ–cÅŒY¶ns¢geɘ~ý¶Ì:Z»ŠcÎÞoÕ+”.Y¤ êèB•êV+ÿQªoÿÃ)’AA' í#.©ôFv\Ì€ïÜû¹xµˆ†·èÛ•5ŽÑý»WjÔ ƒv‘´ÉL5à•k?äKrï§A^eâQ ;W¶,xÃ|hÛ fPdlÊË×m"Aî_ÿùoéâ…½ìÖ›çÁeWµ+—[¶nÓ–ûÈÐk€¢N•òS?€i·©4ø¡=;5hÓ•Ç–ŽM·cÍæ¯2¥wd< ç ÆX |íâ…òãmþVâ„ß=±hÕR‚‘¶7Ä39òéó—®8òŠå~.`˜tî¿ñC,½ÌaïÍ6fÚç8Z[{ñôqÈ_š¯!!ÁàØïºCŸ¡¸¢p{Óa˜ÊD0þå×ßXœ»rí:qÒø²rÝNý†c•¾oý’—¹ÁaЉæ­Î¿hÉ~º K†.¿ff’-ºö3ê·,ýHØd«‰Ü¢ic¦ ï/ÙVMCSX3o*6z¿þþ{Æ´i¬­ŒSÃ$Ã_Á8ÛÖ¯Á ßëH,†bݹw\IØcbùì ¼ÚÙhÏð>]zuleM½Ã ‡õþ”ê;ø?´ÄÊ¢M‡(uo8€v—³V"JÒ#<¨Ñ:þx5 ù[p–¦‰Xxxá°Í ó£ G Á"ÎßlZŽÒÜPnqã¬sà†.;nœ7?iXÇÖ¹çÃ>.!ê3b¼#áí’9ÖìP¾Õ*f,Xƨ\Ž[¹ì‡|ˆ(†á4ú$«Ëå@‡·­úûŸÌÓHŸ¼Ï?<˜½Ëþ½¯Ú«ó îÐZñ0Û·Àû~Œ$„ ï_ëccNIaË’9¨î=Øä9Ü£cÏ-ÌåC"œûΤ¿æ±Äõšo¢óƒíÜU8ÔDþ¤kßë·`aߪI½”É“^¸°påºo“kþéæm¸¬™2Ëà±}ïšðpó€/³ËßD~ø`¿<©øBxPÆÒ¦8™`úbž•àÎGäy°püÀ…ƒ?Ï_¹M;öH“©_,¶`1ãÉ“#KÈæ ­E@PE  $I^oPCƒÑçx~¯£ëeYžForÂú¢ˆ÷ÿçð${n{òëo¿“2…Ôr!ÏLWãÌô¬2©sÉZk¬eÜ’y§¥[Ò[ëyÝÇ.”w]Ø‹µÞCï\qÐu–¡ÿ¸N3玸c)¯z¶_¥C´âÎQƒÖµ¶ËSTÚH¸ˆ9¿¾’É9!ÂKÖø_¿ykÆèÁ)’?çMín8[½‡çÁ%D—¯ÊSñÜc*’EÏŠ"—5>¶9¸ˆ7ð˜69B¦…T¡e뉲+Û*C|ˆŠÑÝc#}2\h¶õËH?¶G‚>ÙB<çÐmhÿ ÝÞÝõ†==¿˜×Þ¶CëÕ¡…‰½.ÞÞ„üv×C„×Ã0YÂzžßöîØ*ÄÝjÃ(@Äàý±Ý}«U0ìùa5+UGO: äÍé Àð4øpüø>$1wµ¶ò öºÄsC],~8¯Æç~¹1¨^ ø†Õ?ÙÄs;~ê,§ ~Î]±P‡u]%Œã «C²teݾŠ= ˜ ;àAc&£ŒÅÃ^¢&x&·ÒáûI²uÊAèy\>%†|¹s¤O›&ð¿a›Õ7Ã\‡Qô”" (Š€" DúNÞãµØ1bÆy=¶OŒ˜þ¯Æø ê7n\^ŸÐ±9¸o$f¿Üô´ÌÀä5qR£ef»f û~Ú†BݬùËÖÐ3¡°lý“Äè«ó°µÕÿ›1®ÆÛ±B©áÂèÝyn¿\±¶Ïðgî¾¼çãŒc¸MCŠTD &·2(üüà‘;,Ä7Ç{™p ]¸r=V Hò°î[¿Xb©KCR®5jÛí‡Óçä°p¾x4¡D’®HJfÍ­r·A£`È"ÀZݘ=Z6ö•Cç½ÌN›.0ÊÑö›?óíwˆÃ01-fÅ‘%.b©Aïs¼çÖ˜yz`ë…£ñËmfnºpÀ:L8>C€A› æøfˆ;»‡Nô”" (Š€" D†¶/ ÝÅæ9Y²d‰'Æï­¯ð^.0üF^ݯ¹Ä@ÆûTÌôˆêœêíä8 r!F , ÓG ¢[g»E/øШ[Û[ùry² ‹iGHŸ˜ÅJ®¦p°‰Â…Ýõ­»÷àiS¥H”ÐîÛnóÑ"ÏE* · ˜±XŒ±¦‡²ŽD9ÌÞ¦¯öôéÔjޤψHF¾2kNª«×o©\öÛ¬^-XÞûþäéJZÒU×J¤oœû+7j…¡/>ßHB®FM™cFœ4gAݰî!ü=áïøžàeN´î¯[Aøêû9²ŠâWT¦F"§Ã$§ŽˆG ¶Ð¨ uÎVùc§ÎâŠ*_H&ƒá>>'¦ç‚L8wö¬K—€í}ø¨!ÿ¨Ðq|Ï“=+{Î µFPE@P¢%¯û-’ë~m·7ZèhñòóÖì—¡Ë™ýÚ¦äá:¦óô0‡h ý?†¢hãQ5…ÊËv´G,z_`Äh€«•/M¦2r‹•~ÝÖcö´úÁc6,‰|aÜïO8€Åþ<ÄÓÐ†Š€" (Š€"…à­›g¡À¢ï*Zß(²NUP"¦â#ïúX–»VnØ’£de4½QNò³;ò(a¿æ”OPÄ,2íÿöºÜO[ù™³äµÛÝwŸ&ª–®øA_8m¬Ii+œ–àìÒpàèÉUíˆÆ!™⇠n[¼¹LçÖ!£°£`nø 3Oh$. ÄÁ™Ûwïõ9ÕÇ¡=;SóÔŸÙ“e‹Ì3T‚NIWùrf·NØV–ù“›Kâ³Õ¯Q¬ E,'cWE@PE r"À+îZÂy·‰œ“ÔY)Š€"ðòDæG–,m?ìZ_$^¹µ>îpúüE¹aŒd·úNœÿñ g1$™E«ÖShê[“¥J©aæÂ%¢ðYmwüpšúzÕ+Y}kñ ¡2A|öØQ“WšŸûÕ›¶Ò¥ofíÞ+Rý08 JŒ¤u3ê_©Ì•ý= 2m øsE£úw“ŒaÞxäqÎPŒ€?Ì:gkÙ6ÿ2% “£³p¢ä9.$ÈÙc·ö¦eE@PE@ˆêàôKº#òýò†ÆË•rà¨~CuþŠ€"àgìÑD˜Ö;r­ø‚HQD`ž½`¹Œ%Œ±^õŠÖ¡…"Ûæßá,‘Í­2›wîåÐj»+¤Ô––íô9ÓÎònFö»dŸ(Aüo½tå+Ÿ¨‘W~>ùúѽøcpÊåfã´OÝ€Q¬ð4&ê}ñò‰‹?ºeØ&ÿ%^wÙ•T 焺{ñò”mn.[Ùd`ûµ+—GrÙÚMLøøé³ŽXÒºl«•Š€" (Š€"ýH˜0aüøñ¡Áp`ÕG¿û«W¤(×L)¢ „†ªZþ#BUýè¬Ë4ຬ;’"ÌL ÿX>“¨–PXRyá²#Øœ+›#0•þùWûÞC »S> =\ºz mp®lïy¾ï ɧÎË–>ÎóÜehž½‰€eVèª^J³.Ǹ©o BvAàué×ÂZ¯(Š€" D?ˆÿÌ‚8JàØ±có&£¯Ñïë)Š€ Á`™‰|)äÊæÐ‘ÞºsN0Û¸q‚Ü}©<~ú,Ñ0Ûÿüç?Dœ¢7]ö²ÁßNž½@Ùˆá‹KoÆÓUÄp]¹qK̘¯‹ã+&ÓÔC°å¬—û …íSœ=󻬕kØ„é0ÞÎ-š’”Xºªl¥šÎCHÔe3mgïk½†"”X¹-·¥ó¢ÞOâmÒµ±d5¡2ï§­’Š€" (Š€"±$J”(A‚¢¬8b®(a„@xk€ÇNŸ‹Á0fÆæzü·ï&¹Œ´f¥rT1Æç½a¥’ˆÊÒ ¥q<Ÿ¸hVÉ$æÇ[wíë1d ,ôرLöZ¡©ç/]%ÂV¥2%iû÷ß“ˆ?í›7BŸLMúÀ\A¶îìÛ¹t.{ø¶»ßýû‘1(mê”&Š:Úݬ™2@¿GMƒŽº_—g]=56~fÛLø®yKW1“}Ûe”,RØ›ñÅ;©R¶mÖ€)1aïý»´•¥Í;önßs YýZ¨—Í̃€z[;…xÚ£úu—WDÿbþPt²™†\,6çc¦Ï%w1•¡ŽËt®E@PE@ˆ< ò¿ãµocÄ<õzlŸ1ß|5†Ãæ™$À¼ X£@‡é„_Ká°žÓMPðG ¼ ð¤9_¢àÍ‘%SÁ¼¹bÅŒ ƒ% ?¸SFô—WÂß>øSVüŒFV)[jñê õ[wA‘{òìùõ[wúùÖÄ‚÷ùXŽt>PǺ-;U.ó!!£!ôŸ3kæÏúv¬«—/ÝäÄ£'ÏR¸FÅ2Ä‚ºzíÆî¯AщÑåò~¸ä«°PÑ?OÒ‡¬H¦áSü, Ô¨©³áüÉÞJòŒŸpDê2ô^ÚN»hüÌyo'{KðÁ#Ç{Ç©5«0Õ´cÏGŸ$KšÄJ€Ñ;~êìð‰3YÊ…KÏËYK\¿f°¨Ä=«¬¥Ý+Š€" (Š@TD`X‡rhz±yN–,Yâĉ)cül_ñVƯKç¬(Š€7„7ž7iä°ñÓ¿9rLè"ºS|}¡£†:ç0c†[gêÜEÖ&óåÊa*Gè~éjÚÔ¡ã§%I”pÜà^eK·luáüyŒŒ0Õ%3ÆM›·xͦí|0™n^¿ÂÆv:åÛÉVÏÒ±ï°ïŽàC[þ çËÏ4ýØ 2óBùž „Ö˜éäIDAT@<¹­Ú§%¿±UþÔÙ (ZM,b}9~*åJ± zoQÛr 55ûŠ•11ÒF팪™lÀùðweÕ¥SïLΈz2 ‹ú—2ÑžÙ[âU ôÒhËY)0y¤ä”îE@PE Ú À«+ã¼Y±‰ÊWöÊ{£Í-Ö Q„7†°ñyüä—k7nÅyó ¿¹Öù-ž>ö¿ÿmµÎåì KÑyÖJ Yv÷oXzçÞÏŸüš1]éäÑÅ#Ïi_8Å/;²f¥²?ݼýø—_p-æß:å‹~pr¯`jÜÛXP§M•ÒpE›¤’y¸E£º¶ Imüê7õ­n›6òWì&•6ÛÒ+ p °óç"ó\úgçXcõÄ*Û„Å*S¢ÈÝÓTæÏÂÁÍûéÓz½ô‹XéßI-Ã=xøhĤ´š4¬ŸÔ°gþ­šøZ]¬Í©uó§“Ðstý 4˜hAPE@ˆfðZºp`Þ  ßÀ-š]¦^Ž" (.°³A—B¡^?žOŽx>.»å™ÍvÊ]e ‰ùa+¼yû.T³j¡Í¸æ¦z%(‚´‘·p ¯`k¥Ë2ÿ6ö+bÎì—z+iÇÞ{ȸi¥ŠB}mz¾x9e/üÖÔH?$Ã~¥&a‚øVëÅJ½DÀ¢#Ö¾ÏÐ{÷ú/šÅBƒ©¤g—ìWÜAmš‡ b!h¥ME@PE ,ÀæYSþ†°Ú§" D "†‡4bWl,«ÃaDÏC¹ºY§^™Ò¿³rÎ$+ãe ¿}[v&]Së¦õ=÷ãòìjÿm+6l¹ÿðgó= ¶býæ•¶Ì;Ôf&í²‡°«„ý¢å»þµgE@PE@¤ü%ב|+h]ª€*¬(QèK€O:"KEžt>¨y?mÕ {lâW[Ÿ˜D â/”Ÿ‚3ÛNYÅ<—I’LsdÊ–,J\hÆ5zÇÊù% ðÜ6Îêßj8€¬C(Š€" (^" )½JÅE Z"m póúµK.˜?wöHrÛșԸN5çÉ@Yù8׫oa['š[m¡ƒÕ› +Š€" (Š@4F€”¿(~ñÆ$=ëÔºTo·^š" ØeóÔ¿ožµ  ‡Ñï“øå­3éÿSCèhü,è¥)Š€" D ˆyõÍ¢V0^b‚°’_%ÀQãÞé,E TP*0þK;ñžx[÷ÿRÔô²E@PˆF@T¾Â~#z.:¾" (áŠ@´5Wu0/0ÿµÈjX,/SE@PE ¬?å°ê]ûUE # 8ßœè;5ýß¾÷V¯LPE@PE@ˆ¼¼y§¦3SE@PE@PE@PB%À¡‡¥ö¤(Š€" (Š€" (Š€"‰P‰oŽNMPE@PE@PE ôå(С71íIPE@PE@PE@MTšhj_Š€" (Š€" (Š€" (‘%À‘öÖèÄE@PE@PE@PBÿk»Òù6.ùIEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1542658279.0 specutils-0.7/docs/specutils_classes_diagrams.pptx0000644000076500000240000010406500000000000022723 0ustar00erikstaff00000000000000PK!6¬ù¡ [Content_Types].xml ¢( Ì–]OÂ0†ïMüKoÍÖŠh\øq剸êv€Æ®mÚ‚ðï=Û€ áKØ›%§=ç}Ÿ®ÝzZI.‚1Ë•LH-ŠI2U—ƒ„¼õÂ& ¬c2cBIHÈ,é´OOZ½©`µ´ :§o(µérf#¥AâL_™œ9 Í€j–~°Ðz7hª¤éBWhvëúl$\p?ÁáŠÄ€°$¸­ ¯„0­O™Ãy:–Ù—pæae™c‡\Û3L t¥C1³Þ`V÷‚¯Æð ‚.3î™å˜EµvT°XWæF›•V ª~Ÿ§©t”cI´,–‹oa”3.ç‹Xc>1ëp—ƒÚ¡É–´wbšÑ‡cAQÓ5JÛcìO)¼`Ìáó( ám¿G¨žûoB)³Õ‘½ xuS_õ’ôN§ï‘MÕÈÍÎ`ç$VÚeª{Ètî!Ó…‡L—25ïó™8'´þÏ-?6"Š’8—âWú[¡ùÿÿPK!3À7 ppt/slides/_rels/slide1.xml.relsŒÏ½jÃ0ð½Ðw·W²3´¥XÎR †L%}€C:Û"²$trˆß¾mèÐñ¾~®;?/î”ÙÅ ¡•  &Z& ?ׯ—w\0Xô1†ÎýóS÷MK=âÙ%U ¬a.%}(Åf¦YÆD¡NƘ,µÌ“Jhn8‘:5Í«Ê{úƒ)«!¶qÝýÇŽãè }F³.Êн³tÁ-®¥²˜'*¤Ü÷Ko²F€ê;ux·ÿÿÿPK!.5 Ðppt/_rels/presentation.xml.rels ¢( ¬“ANÃ0E÷HÜÁš=qR B¨N7© $á&™$ŽmyL!·Çj¡$Uu‘åÿöüyš±WëïN³-zRÖÈ’šÒVÊ4ÞŠÇ«;`¤©¤¶ôH°Î//V/¨eˆEÔ*G,¦Іàî9§²ÅNRbšxR[ßÉ¥o¸“å‡l/ÒtÉý0òQ&ÛTü¦ºVôÏɶu­J|°åg‡&œhÁGzö66c…ô +‰iÀOC,æ„ ­*üØÉ_7›‚Èf‡x’СìÍÑI¬åœXA¾k| ½ÆÁŠæÈí¬ ±v°¤Ü›“ø™“a«ðë赬?>ú‡ùÿÿPK!"ùå›M ppt/presentation.xmlì—ÝjÛ0€ï{£Ûá:þ┦ǠƒÐ´ ÚJc*KF’Ódcï¾#EŽÝ”AÀW–t~tôù ³¸>ÔÔÙ!+Î2ä_MCXÁËŠ½dèé1wSäH…Y‰)g$CG"Ñõòë—E3o‘„)¬ÀÔ7LÎq†vJ5sϓŎÔX^ñ†0m¹¨±‚©xñJßÀ}M½`2I¼W Y{ñ{¾ÝV¹ãE[Ãö''‚P‡ÜUì¼5Ÿñ6<Åû$Þ“Mû,‰Ê9Sè ·ŠßòZÉuU¨š %ð´ü…¥"âgy/ÕÅŠS• üh¥aT1×+ ñ‘·\xÿ1?>9‰“uÐ[u7¿âþ N?µ8f(IãTO<­Ä¸"Òªu£5ó£è¬U’-n©z$µQGJ– ¬×ÖkaGkáP¬“…0÷ic¢ªÐ=õЩ±¸× L_ Ñ(r@ç?o~w;¡5*ß³•x5Àõoev ¢l¹³nY¡´|…O~ªý¼¡snä’ÓªÌ+JÍDg¹¥ÂÙcØMNø/´Ì®Ž:6pü²þ[Í\ª´&ž|! ø$(ä… =ŽÃ;ó°h‚MOuÀ#Åò {>„‘†bùD=?œúɨ£bÅ@išèG@šŠ”ô€‚ MÌ+0ÒT, éÐ4 Ç;úLÅJ{@šÎxIŸ©X@³ $žŽ—ô™Š©\?–˜Íƶ–…‘ÓŠ*C¾ç7ù*Cw’„¹«ØMáÑsgwy˜ÇþêÆŸÜüÕ…¸ë øG[•œt½€èêª\ò­º*xmÛ ¯áoD4¼2…œzS ±t_÷¾ZþÿÿPK!˜ÎÒ¥Íppt/slides/slide1.xmlì]ÛnÉ™¾_`ß¡¡«,à²ê|ðÄÔ¡+Éb=3y’‹ X´É–D„"‰&%Ë ÌMž ï§Ø»½ß‡˜'Ù¿ºIÊjÒ3"%Jm¹}a’­êªêêú¿úþCýõëß\_Œ³«²š¦“×Gä%>ÊÊÉ`:MÎ^ýð6"}”ÍÅdXŒ§“òõчr~ô›¯ÿýß~={53¸{2U¼>:_,f¯Žçƒóò¢˜¿œÎÊ üítZ] øY«â=Ôz1>¦Ëã‹b49ZÞ_ÝåþéééhP†éàò¢œ,šJªr\, çóóÑl¾ªmv—ÚfU9‡jê»ouékx²ÁÉx˜>ç³·UY¦o“«ßV³“ÙwUýço®¾«²ÑÆë(›0,GÇË?,‹Õ?'Wõ—ãÖíg«¯Å«ëÓê"}³eׯ`ð?¤ÿÓµòz‘ š‹ƒ›«ƒóo·”œç[J¯8þ¨ÑôTMç6G¨Õó|_य़ËLÈ£eoþk¾Xõë²½>ú[ŒÔ‰$AÚ½Ï5ÒL2D-WÈæ$ iƒ¡Vå¤<({¡„Vc \¡Í¬> ÖÌŠÅ"Ȳøxñ‡²jn8=«e&juön=McÈóœ¯fêªÈ»uÙO¡Õ²Dj}Ùb#î¿„w¿$ð· èÓ0ðQ5(Ü3©à‡ÆLÒcæ3ÄÌáý1SnÁLÑQÌ$4wZÐqeAã3œ!+= §<—ÁqbÝAù™àŠ6–<*‚_=fö˜Ùcæ³Ô\é[íª-,—ÎKh幈s‘B"IÔ^GÅ"9(›$šñFs%FÇŽ!cNrmŒ=2öȸFÆ_šÄ©Ÿí©Jï6UoZÜ}Ùôm¦I÷ЗÎ ÂZdA›Fk…rC”„ÅÜT—ÇÜ$ý½æ¥X@h¾=úöèÛ£ïžè»…úv•ùzƈX"ŒE$r‡ŒÏ)"Aiî|µb+Ñó&®„¦„ÀÞŸ·§‡ßÚÞÑÔía]×+| ²ì¦×YWÝÖ1θ²ˆ+¦@ïÔJiC0ÁE9Ý…~fiŒêµ+6M•¹‚KbY “†±š£îƒ“i‹¦c+™Û˜Qï«böúh2”GÍ®zªÎìåªXÖÜ”ß>Ѷ›âßÕá¤mƒüÉ º]]^Ý2ÍßÕ ÿ+èj6­²‹bò!›×•Ù{@®¬Èæ£z!~_\•ãrr×6/ÓTÊ~úñŸÙ7Ùlt]ŽçÙh’-Îo•Ž.ÊI þV§>!0[nZ»VÔU¿‚„Eܪ‘¹EiÅFÆ1Œ¤ÕÜ8N ·úQˆrŹjÌhZ2Ü!B)—AÃ’Kà>‡ ið¿ygãË‹7Óas=qÐå„…Ë)¾¹¾¬ï´ºZXoV…iq|y½Å—¶[¥£í8Gö§7/~úñ_/þüÍ%pþW¨L&³Yë>²¸%4¢«‘.äŠq”k+ œ"‡a9 B)ÃBî„Åè•ÆÚÔµ2ú†N~,škÂz[4å'EóÞÊ@ÏÒ{–þ¤,]·I†êê hb|@y¤1`ÍŒ€b#=׎ðG!†b©•hŽ -|Ó’cùÜ™ú“Ñb~ïðÓž}º«¡ÍÄ‘È\@;Dä 9b Ê µ‘/qØÅLö — ­˜hù*ž€ân®£Ï‘âþ0­€±•Û&þ¡¹î>âEÖ;6oh+i¬ƒÑi9Ž4²€bˆÉȲ@XA©Ô6·’i™çõb¬YÑ œtGi«|CtÏâž‹#kwÿj!%]uöçR aFNaPO Ï‘‘¹GÁºè µØÆø(+©Ši¶äqP\oKKœûÝ__Üëþïî†RíæïÙì_Ê?BŸ{&lTH H„†mÈ¢<¯@Ù%ƒÅ}ˆ5œ5Û@$nÒ~dT3RaöE£oç%̤Ééô«¬ÈN«²D)÷0”¤·_T·÷{MÑ îN»ÊÝ5W6:¬I['ÁK,>(§±ÀLé]´åû˜}¥Ár*"V†=¹ë3 éý[½ëqý[t­y}´o¿«º—v>z£"4 ÊMÚ÷!œBDYJYà„…ƒò_ €ºŒØa8­È}¤yiÞ«Ÿ[½Ä£7@¯«J¿5Cò@‘cÖ%ƒGPiE5üAbºSúî G Óâ!A­¼¥xô ×ƒ^z]±­$ ÀF*®ZW°†Ï%Ø"ëx@s†<ü6>Ș›ÃæÕ0a—ùÛ%–†w•Ï=X.Ÿz|zZ|¢[ð©æîá“ ÎPR^9D94¬­1HPË4sÞyÐk0:m­ªñI Më­=>õøÔãÓ¡ðiKKÖÕ­-ÑH+óȈ ÕÐ`ggXGesŽ…=hæÁÕòŒ>¡$|oeêñ©Ç§ŸŸÖ©WŽIÖÕLÚ Td(’#E8AŽŒ¼V z @åzœ-ì ƒbLr£[Æ+Íe žiò„–Ûo•î Z—µ³ün¬­›Ç÷̘ö&»Î~úñ_ëlie18Ϧ§Ù2÷Ù7Ù»ËÜ’]¤oKŸ6Ü´hiËfKä:{ žÖ°âzмM¶bÍmÚõjSzA*¹J[üxY#–gmrƒ)Ýš5b­Ò'FûŒ£½yñ'Þ?¿¸²¶å(#ÖÙ”ƒ9‚RÐý1Uði?”WZϱ—&8zù1Õqч?j²÷Âô,½géûR†ðAÖÕðA’Ó´™Ó§t›( (ƒˆÈó]ÎÐ}Ê ‰ùi;Ÿ€2ô‰¦:Ë66 ±®­ÙœåK ÍQ‰B:lFè‚þàóÄìtHõƒÓL‰ö¶Ö'°Ï,-îÖÒ¾—ïø¿‹ëQ;rõS*ì¦òÚEqä[ªXWªŒŠTXëQN…FyÚÁg¨ÄˆSgó8øpÐ(R"•XžJÊ æô¹'~ëùxÏÇŸ˜óg¼³;Ψ²Œ{…×…²81IÀ§»ÇÙq¦ pÓ¤«Šê¥i¼OW×oçé·ótu;O:Ó·t ®2„òW¸˜ãÏ<ÂÜZår£½|¬¼œkÝlëáX-Z‰ú|u}¾º/2_]Ê.ÒB“®†BaI=èR)aÄcÊ`‰‡ŸF+<7;ùgöG!•¦!„i'ù%„'ïyç«+ªª¸ÿ&üd¬jM¿®º™$iA>Â:–LêÈ(e‘ên”&)EÐ#eÂBªåž “«m]PŒÉ”Üîó›~{š·Sš û'PL9ÉÚ.ÞՊ܉*¤F"ÙµržLÎ:…±S+¬ JP~à¸uÊ“Êæ QJêC'Pü9BucœºUª·Iõ6©§µImøˆyW}Ä–¹ %Ë‘$Ò x@&§yK¢ÇÎmGU£ÄHl–;bˆÐ¬bFb¢LÊiñeeA²[³ ½Èf£A1ÈÞÜ\•õ™³§#xLhìCYÝuÜpÇò®ºc…VØå1éÑ#™'ïV±]Ro_íí«¶¯Š-ŽnÞUG7¬”r+Æ&5 GÎè”8ÔkÃhTLì ¾» Nkqíè6†·7_ôŽî^©è•ŠU*Ä–ô¢«®nË ,äÈ'—)•6– YÅxsÌwÙ ¶×Œ´ ˆã„²´¬Ç§Ÿz|: >m˜ôEgMú‘)H@*ZŠ ­·¯ ”Q9Ã’(µSr¢{x”¨Ðº±¦rl4my§ 3„ª¥šØ‡í>ÿ°Ýû‡ënÉÁÓÕ(›c‘3&ÆÑ¦ì`b¡ [i× 2ûa²Á¨eöC‹£lodeäM?XlÈeá v’_…²]•Ãì´‚ÞÞË^ò~p7SƉ`túD ϪéÕhXoo-ßG¸ä– 2²«a\FÏÅ(o‘dɲo¡3G!™ÃVˆCµÆˆ!fµé ão%饫—®–t­óŸ|$]]Í€±ÅΧ6%“ÈnA· yέqʇwñ›íî-£|u!çÂ`rh·÷Ã÷*é禒Êuz—¥³ ^0wšGŒåajde•ÂÚm‚6c®PL7i(¸`‚öˆÒ#ÊsðÀýO”[RÞÈ®À¬NÁF†ˆÏ ò…ÁÊ ©¢´1Z)Â!£‰Q’/ã#xÊÎMú(ÂkzöÒB”u@ÂÚ ÞÕpª¸µ8h8ø´ï> k F!ÆÀ½É-#“’bfV™0Ò™©¤mRçTs §?dúÉ™¾(0ɆÙbÚdˆ| îýœ1ç~Þ‰Ôo«²†“Õ|®¿-¥Ò9#©×9Â#âÁ(d£( ƹwÚz–'©œ¾)•pñnR9›¾/«Ùt4Y$ÁÄKÁ¬!aFŒÔÚ4HX÷mõ¹–¾“ñ°¾qõ¦˜}{U4kŒ¯/Í’È7EoФg‡ûþ_ÿÿPK!ÕÑ’ñ¼7,ppt/slideLayouts/_rels/slideLayout7.xml.relsŒÏ½ Â0ð]ðÂí&­ƒˆ4uÁÁEôŽäÚÛ$ä¢èۛтƒã}ýþ\³M£xRb¼†ZV È›`ï5Ü®ÇÕgôÇàIÛöírÑ\hÄ\Žxp‘EQÜÅê{çgÍ(Ç]ÿm('Š)#;+bÊ ²³¸¥‘­­cÒÖdk³’HêÔ)¬  ¤M°N÷]•èX¨nKÑ®Ý"@k÷sú%;¤¤\œ4q À)jx43ç Iùí»m].ú²mf¦óÆ6 ÞämQ63óã9Ciô2kЬj>3w¼7ßÿúËÛî¨¯Š“l×®¥M”ÍÌ•”Ý‘eõùŠ×Yÿ¦íxÏ–­¨3 ·âÂ*Dö°ëʶíYuV6æ8_2¿].Ëœ'm¾®y#Á«L‚ÿýªìú ­;­¼=û¶Kr×A´y^ÊŠÏ›â|kÚ^là‰cC òEUMVÃÀ_`ZæYeh{2fœó­Ôf}w.8W½fó»èݙг?l΄Q mD1­ñÁh¦o›îX{Ó/¦nv´]ŠZµc;3ĺZj œ0òa0¿ÍW§÷Øæ«ôkkzuã¥*ªÁ¹»áà)œ½¤¨¤i‡Nz9¹¶åÌüÆŽÜ”QÄ ‡¨Q¥4D “ Å>‹1ñ.ÕlÇ;ÊׄýQLÂs¼;d×e.Ú¾]Ê7y[ª™Ä<;täY¹ûÍžfÛs†|?ž£0¦ Šœ„ ˜ÄQDlÇMÝðrÌø<µ: k |ÌÀÄHß´ùçÞhZ`L<xe1°ªÚn5ŠMª™F+Jä ½qÖ`ª;×É¿—ùÀÇ4´N‰ç:ؽ-ìá@?Wäºã$اx€îŽä6j‹šý Z Vy43y¦X`«^.ä®âú¦Sí”ã*S+oÐÇÅ`+ãªÌ?²5xQJã}ÖK. 5,1€¢¼’«¢Õ€ÖµÚ³‡uGîêN~Ve9_µU¯Ã¯T‚ 9›zƒ<QœÒÍYB âG©ï½¼Ñ{ ÷¶×“Ÿ Dà„èû„’—b§4¸©®–¶‡„™ÞÔ£òP˱¿Gûà:/ƒ/xÞÂfPñ ¯@Ä#ž¯Jq8 yµk!W#ÒËå€O«j:Uu’I~«˜É+-æÈŽcÛs ¢s'AIûIÄ.Jh&iê3êÒ—/抷ÿ ‘dÕr*ãa?ÿiëãÆ~¹ý¤À–pôÑÑ&8HS—¥-š0ê¡0qCDC`צQŠÝËéDU‡²¬9+/Ö‚Ÿ®Õ鮊Œ¾–qųæª|åqh9ü®åhÒ›â,ÙŸw…øÄæNbcm«–ƒ›r£¯õøB×CsLSJà ò–àØöY±——ÛRŠAo¯3©›$÷ÈÖñÉ=/ÕÞDõ¢* n|Xןöw_)á8IbšF>rid£–4ç>ŠhàBýENà’—'¾ÿ g÷r®7±g^f(óöB9N£È§> "ÏFƶk‡$Â!¾ZfzEiÞººüøþÏo?¾ÿû «‹n¦ï½)ëº7j'ŠBÇAß”!š„>õ<1—PGÁ<&©ÒNçлÚÁôӵ_¸èÚR$;ö(ŸMû·ï¸ž:¡>XÚµ©½’ÈB…m%ÞgÝéFk¤Ö'¤XuJ—ƒéµ‰ }úSàø?ÿÿPK!iŒGFnz"ppt/slideLayouts/slideLayout10.xmlÌWÛnÛ8}_`ÿAÐ>3)êf4)¬Ûb´ Öé¾³ Õm)ÚuZèoí~N¿d‡”'qšºE²`˜EÎÌ93¤^½ÞÖ•±á¢/ÛæØÄG¶ið&o‹²¹<6ß]d(0^²¦`UÛðcóŠ÷æë“_yÕÍúª8eWíZ€Ñô3vl®¤ìf–Õç+^³þ¨íxï–­¨™„Gqi‚}캲ˆm{VÍÊÆ׋CÖ·Ëe™ó¤Í×5oä"xÅ$Øß¯Ê®ŸÐºCÐ:Á{€Ñ«ïš$¯:ð#/¶¦¡ç‰ Œ`ó\ÏUa4¬†‹RVÜ€Áä2g•qÁ·ROë» Á¹ê5›ßE·èÎ…^ývs.Œ²Ph#Ši/Æiú±ÙèŽuoùåÔe³íRÔª…¨ÛcÈ»Rÿ–#Œ|Ìw£ùêì¹ù*}`¶5m`ÝÚTy5·ï™Ü‚¢b¥í8íådÑZ”Çæç,#‘›feÐCÔŽ(ŠR¢Œ8AJü,&Žw­Vco– ®ùù£˜t†½=në2mß.åQÞÖ£H&­­˜Ž´*+?l» Ê‚¹ƒ<—ÎQ˜à%4ÃašdÙœÚ×cÀæ©Õ^X£¿£ã}wÚæz£i(ÅëÀÛÍŒLÕv«Q[RÅhœ7¼Ô]”GÈmÔWj“÷ÐêA6«z¹W×úÓf ¢b*uyƒÞ-råI\•ùC¶/Ji¼a½äÂÐûCnŠrppSY¡­ÝÆÖDù·‰w&âïä€q^±œ¯Úª€íÈ Cè'ØwhŠ’x(ÄÔFsÚ(Iì¹çÅ©8éó‹AQk­(¡ eG™·Ý-þ…¨z(œ)6 ìë¥SRÙT7%à1ý¤·e£ˆÓªéÍ}pmþãà ž·P=+¾áÕˆäûˆ«Rè|0k×B®F¤ –ËG,ùè”| “üNÎ9/4ç"’ÄŽ {f.v‘ãÂÆacäf©Cˆ=Âè(ÀäXÿ òbŸ"'¥ŠpÆ`Û!©‡=/ôž_nK)½ý½fB7Iîg*ü7$÷´T{Õ‹ª,¸ñv]¿¿G¸ûB ÷¨( ‘ò)ÍМ$¥~‡‘ë'‘›=?áð}1{s}ˆ=q™¡™Ÿn.'1Š|ê£ òl¸×ÛµC'"!¹)3½¢´ë­._¿üóÛ×/ÿ>AuÑÍô]4E]÷FíDQè‘8ˆ I8š„>šgž ‡…CióXÝÈ` ¦ûÚÁôӵ¹èÚRDb{”φÁù@=Œý‘¤A!;[í å=´•xú³–H­/H±ê”,‡©»)Êóé›ùä?ÿÿPK!hZ$w85!ppt/slideLayouts/slideLayout3.xmlÌXënÛ6þ?`ï h¿Y‰)QAºmÒ6˜Ó`%:ªÛ(ÚMVèkmÓ'II¶siæ­M` )ꜣïœï#uä—¯®êÊÚpÑ—m3³á ×¶x“·EÙ\Îì÷ ¶ÕKÖ¬j>³¯yo¿:ýù§—ÝI_gìº]KKÅhú6³WRv'ŽÓç+^³þEÛñFÝ[¶¢fR]ŠK§쓊]Wr]ß©YÙØ£¿8Ä¿].Ëœ'm¾®y#‡ ‚WL*üýªìú)ZwH´Nð^…1Þ·!ÉëNeÛóü7Î Û2†b£– }ªrÏUa5¬V žkwKraîöÝ…à\Ϛͯ¢[tçÂ8½Ýœ «,tÑÙvÆ£™¹l6fâÜq¿œ¦ìäj)j=ªjXW3[‘v­ÿ;z_I+óÝj¾z÷€m¾J°v¦8{ÕY à¦t.JYqK—Èà8ëå„h-Ê™ý9ËPDÒ ƒLÍv# ¢‡ CMQÅÈóo´7ôOrÁ /¯‹I_пÇi]æ¢íÛ¥|‘·õ(ŽIcŠNˆG:5ÊÏ1‚iBýx^äƒÐÄY@€ù(ñÜ0TPnÆ(ÌÓh²pÆ|ÇÄ'"úî¬Í?öVÓ*¢4¯o[‹L=v«QSR×h´nšÉ®ÊRL=HÉÀ Ü0ðèm¶¡K ñÝ‘FD ¼à.™CìîD^Emq­Ý?¨Q‘Èš|Õªý÷aZõr!¯+næ› Ž ¾ü]÷Îlõ¤I*[=ßsìô?ã'”SÅôÂð~1 Žs¿iw&½2ضÂu*é~ƒ¢_d¦?é¿»AÁSƒ’0Éo5(Þ‘6(ПgQ Sà¢8R]JD„Ôˆê¬Qeôé”BÚFJ+V-§FeÐÆ7;óaõh;a.Œ4–êÛÎd› š¦$Kžã$V_ 1EÄÅQŠÈÍô­X(eYó¬¼\ þn- …weõµŒ+Κ­ðäiè@ªþvrR éMqÎÓçŽ(ÿØÈ$¶¬mµ÷å†Un8Žh꫊C”‚4ÀÌÝ9êƒÍMC “ vŸ^nK)½ý±fB•n’Ü¿4ÇÿEr?–j¢z¡NFn½]×îNŽ”pŠ32Ç1qÆÀp Â8LAqÆ1¦!zzÂûªP5{sôÇ Î‚ ù¡ Lb84ò]@r‰zÒIo±)J…îÐÓåë—¿~ùúåïpº˜aúekªº™Ú‰¢ÐG1@qp`žùdÄÃz'Ïc/ÕÚé ¾¯µx˜vºö][šŸÿ ;ÊÇ4 ¡@Rwø 4Цq+‘…N_•xúw£‘Ú¼Ûc³Ôi]¦;úôsçé?ÿÿPK!¾øöQD!ppt/slideLayouts/slideLayout2.xmlÌWÑnÛ6}°´gVEÉ’Q§%q6Áœ~€"ѱVIÔ(Úµ[èomŸÓ/IIqšd©78@ä¨ËÃ{ï9—¤^¿Ù5µµe¢¯x»°ÝWжX[ð²joöû+ BÛêeÞ–yÍ[¶°÷¬·ßœýüÓënÞ×åy¾çi)Œ¶Ÿç {-e7wœ¾X³&ï_ñ޵êÝŠ‹&—êQÜ8¥È?*ì¦v„ÓäUkóÅ1óùjU,åŦa­@«s©üï×U×OhÝ1h`½‚1³¿wIî;-¿þöŒ‘تG×>Sq˺´Ú¼QW•¬™¥²c%¼• ÉôÝ•`L÷Úí¯¢[v—ÂÌ{·½VUjœq¾íŒ/F3óØnMǹ7ýfêæóÝJ4ºUɰv [q¶×ÿŽc;iÃ`q-ÖØëìkgZÀ¹³¨Žjpîa8h gH‡Î’ñã¼—“GQ-ìÏ”"âgªzC‚Ép(ò Íh‚¼à‹žíóB0CËoå$/7x@iS‚÷|%_¼µ1IL±éâ‘Míågâ§!   B}x‚(MB….¢®»Q¬V |žZ…3Æ;>Ñwç¼øÐ[-WDi^Þn-2uÛ­GII£Ñnxi:‡,*;Â˽^äZµf0Ÿ×½\Ê}ÍÌC§ÿŒBQçºbY Þ/råYRWÅKr‹••´Þæ½dÂ2ë«’V(:À!Lí…t ;åÿN¼7?ªßº¬ó‚­y]ª…Ð •A€3DAfï‚ Å )&S/F1$3ˆÑsÊ *w“( ÓäoëÛ¢~JÙ]!hBŒúG„pÜ8ü4ø’\í„5Û²úDôcÄ«u%Žô~ HùFÈõшøÄjõà+'<•SšKö]-y/´–fQDqDBÃ(žˆ±O@@$i Féóo©¥Tw”O*’¼^iÇt} 'ÙI l¥Ž{mŠÂ,óipŒSR¨ÄŽpˆ|ˆI†ü/ÓÕ¡Tʪa´ºÙv±ÑWƒ‡*²úF&5ËÛÛò•g‘ã†êw“rÀÞ–—¹È(Äÿ#6å\owå†_¨ÜPÇ>¥1ˆf!p¦äÎH 2³Œ$y©ÿür[I1èíÏM.Tê&ÉpO?-ÕÁDõ²®Jf½Û4×÷÷_(ácš¢ŒQÒû€DY IÅ>ŽQòü„«•³G97‡Ø‰·Lg®›&€Ìð è{«º¶ª{+Œ<‚"t»ÍôšÒVywìîòíë_¿|ûú÷ vÓL_:SÖMoÔ!Q€’âb pÍ@LPßÃ8!aœx™ÖNçâ‡ÚQƒÇi§ã™èxe¾]8Êg›«ó;ðD3è™›c\›Ú[‰,uøª­ÅÛ¼»Ø4憔˜¡Nër0=˜èЧ¯ß³ÿÿPK!57דþC!ppt/slideLayouts/slideLayout1.xmlÌXënÛ6ý?`ï h¿Y‰)QAB×a@šsú²DÇBuE»I‹}­íqú$#))¶“4Ëu`˜4õñðûxŽŽ(¿ÿpQWƆñ¾l›™ ßÙ¦Áš¼-Êæ|f~:K5^dM‘UmÃfæ%ë͇¿þò¾;è«â(»l×ÂMÍÌ•ÝeõùŠÕYÿ®íX#¯-[^gBþäçVÁ³/»®,dÛ®UgecŽóùCæ·Ëe™³¸Í×5kÄÂY• ™¿*»~Bë‚ÖqÖK={?%qÙÉjE)*f:Œoä4eåù¼*Œ&«åÀ™Š0æUY0}©ïÎ8cª×l~çݼ;åzÆñæ”e¡Æ™¦5^ÃôÏf£;ÖéçS7;¸XòZµr#Œ‹™)ùºTß–cÂȇÁ|;š¯NîˆÍWÉÑÖ´€µ³¨ªjHîv9h*gص?:£^L­y93¿¥) I’bÊÀvˆA˜`¤È¡ òÒ9 ݃œ3MÉÅ$-èÞ¢³.sÞöíR¼ËÛzÔÅ$/É$Ä#“*Ëo®1A±œÈÇßN !¦ž“@7vì«qdÎS««°ÆzÇÂ'"úî¨Í?÷FÓJ¢¯o×™ªíV£œrÁõ6¡ÃuÝÙnô,C‚°müAˆäV9ûŒËò†Å¤ÜQϵoñ9`wâ"l‹K5{![ÉcÖä«VÞ}‹³êÅ\\VL÷7ìTHuÞèü5¿[þ)û¯3ÓU« ±C£S_º*.'U™rÖ€Oóa9qUeþÙ­ÁŠR³^0nè[OZQ€%x§Ë™ÊЕÝ/Rgé|½pÑÕ)J ÅØs€Ÿ!@Nü$¸^H<—º¯¯Ó~½t*“Rê{–^×F¶CïÑ+t‰, =T¯?iñ#mgeSHW×Ý}á.ÖÇò)¦v4¬r½©aÝE[TL<¤ò},ôÞí¡ðFhg =ìÅ£¡!Ý…Vx#4ÞBCÇƒÊ ½µpÄ&;ØQ•Âó°àˆín±¢ÚXž‡­GloÛÃÎS¨ÜÇV€#6Ýb+à'q¹‡­GlÛ%ÚÕŸ‡­_È¡ûÉL_ƤñdÒq&˜qZe9[µU!rÞ¨YÃ0@)Ep(€ã„Ð… ‚^š$!z}³.„©y^eÕr2ìðŸ:¶>jÞk«ú‡–ÅRuuµ1¢IBÒà@§Ø~L|€}L±q˜ r5œ É¡(k––çkÎNÖBSxS]F_‹¨bYs}*‡¾©ülå$Ф7ÅiÆ3¥ê}ŠØÈ$¶´m•–wå†ß¨Üñ!"(Ž ˆ=¹0MlD^b‡)rmßÁ¯/·¥4­·¿Ö—[7Iî? ‘ÜËRí^þÔKšq¼®7'o”pßñLc*ßW¢ GÀ¨\=ôb‚ lø?ø‹|Í—{v'çÑíem§žÔ²oã„ö ]P„l"%"]ÛL¯(mdvu—ßÿþíÇ÷^À]t3½ëO»®{£vÂÐwQDCùLÀ)À±ï u H‰ƒqÒ r¥âÛÚ‘ƒÓN×~a¼kKý_´Gùl²Jk!ñÇÞ-ÛÔ^kd®ê—mÅ?fÝÉF‹¤ÖÏ÷HuJ˜Cè6DÕ>ýùsø/ÿÿPK!Éõwµ(6!ppt/slideMasters/slideMaster1.xmlìZýnã6ÿ¿À½ƒ þyÐZ¤H}ëÖWot4éÈë"K*E»ÉöYî-îgŸ¤CJ²å$ö&½ àF‹ŽFÃùÍi¿ÿávYhkÆ›¼*':zgê+Ó*ËËë‰þëUl¸ºÖˆ¤Ì’¢*ÙD¿cþÃÙ?¾{_›"û)iãÈ(›q2ÑBÔãѨIl™4漢•07¯ø2p˯GO~ÙËb„MÓ-“¼Ô»çùSž¯æóÜÔWœ19*×?òú²¾àê ×d‚H]+“%ØW P›º-×j0º÷øu?LÆ·s¾”W0Šwòs$iìVhiKL·Ôtñó#¼é"z„{Ô¿`4x©\U«ÜÃåà~9W¹(˜vQ$)[TE¾ÒZt:oD¯ÝŠçý8Æ>bbÄ02ˆéÈgÄØr#ìĶì?åÓȧœ)Ð>d½ó!ûàË<åUSÍÅ»´ZvžÓ; `Hç~Rã?¼Ð¦¡4¢ðA 1<ÿS7ˆGCDéŸ1@çþªV1êÖÞ¡¥©Ï«ô¦ÑÊ @“·n8Z`åµ^hâ®{ i¯Ž¯Tƒ­Å…Ûµ\p]…£eS„é.ðȤˆÚ’A"Š,L©míàšŒkÞˆYµÔä`¢s– T²†¶¬=‹Ò©Õ¤‹[¿Êî$ç ®?äx~QñOºV|(›‰î!BàÝBÝê`¸áÙÙÎŒ(‚ªPþ—”)È™è©àJ—"wºÕ<ï4j_)§ŠF\Š»‚©u×òC‘9(T$2•±Òøõ²5‹8 Š<½ÑD¥±,Z—Ä”é!×)»EX ޶KU«?ìÿÖÆÿ%C÷ÇGêþ.ÆÓ(vLà ‘gØ&¼Ø ƒÐ˜b×¶ILÃÀr_Þý% R!é´ÿO Sûp‹"Ër? žíùµtúµzVEB4 i(åÿÍ#p_¸Âç°ðK–Ve¦lÍŠ'HÄ_—xµÈùÓªwX`\­¸XÚçàÊÊ?[%L×E|Ûá>'â‰i¹ª?Øò!â¾åï;íã úolvl—  Ó>®–³{!Gu›j»¦gÚ¶\?4ù)ôé–-GžïP'ðqøò!×Øì±¨SÒóê¬ÌƒQ÷æ í±ÆÜ¦Ð’Ø‰±í™Ba`øq ×·M6&5=ËÇÞÚF†T ÞñÔúúåó¿ÿòùß ¾ªKÒÖ{½u±ëûž×7|) =ǘÆ65bjøî4°"»5"cˆO‹ÝºúñºÊÕù$2»ðUa—˜ÔC–ëtqÒÆèV[xݹcZðŸ’Z›]#èÅûÞÂ(»ÑìK–4,i0JÒ”•8ºAOÁ=eÃcõ«§žBz í)´§Ø=’ç¢ÈË0†¼èÚ¼*þÕúQ{Š Yâ<¹«VâCÖ!1 ´ç„ˆ8ĵlâAìŒ%…ÈTÚÏKeœõ¼ª­?À‹¼ª!8À‹¼*ർ*à%^û+¼tÀ«\ä¯=àUçx¯÷^wˆ…ŠÒÌ;Àõ¥ã!ðâV¥–FåAÛÞ=‡Ùé*™]~ê2l›UUJeÉyéóuZ.OüËî¦  \^¬ÊTÈy%¹¼¬Ó¶À¥i—#=s›#‡ ¾<¯ßeݤÒÍìlõ±*Û“–A¶n•¼a\~òÔÌ݉r©%©$:‡>d¢ÿsùo£]-LîM°¤;°oîM¤M'ûÑ,¿kýZÕ½P,~ã¶ÎKHç`T£'R¢iYÑ î ÀŠ+¨Œ[ëLyž€ÖuRV ÜšØô¡ñ píÿ Rë\¤‹8Yæ…l6€.Þ0±©W³UEžè_>ÿ§¥Ü«ÂK¸C¹ÏÊ}îPv5Ä[Èm—*å_äô˜±ð —0wˆ[[ÄaCkI›Ÿ &äÊhG¹Ä¹ƒœ xÕvêù³ G¯!¯Kœ;Èé ”›ÔQ0ž {Kœ;Èíä‘×Ò¾ &äçrg¹ç´ÚŸ {Kœ;ÈÝ-äÁÒè'Èß"äçro¹ëÚ§öíB.qn6º=—©Ç•X0¾9¥'.ZÇèV÷ðp|˲{¤ó"NòÚlüøÑ‡úçdŸ½½NöÙ³«¶¹±>hß¹ØUÚŸ ´gÇ¦ÊøÉ@û÷7ýïNÚ³uOIúPïlS甤w;Ías©~QÑQÛ~Ûþ6üì/ÿÿPK!ž•Æ_®Z!ppt/slideLayouts/slideLayout4.xmlìXënÛ6þ?`ï h¿‰u3ê¢.À´ æô‰ŽµJ¢FÑŽ½"@_k{œ>ÉHZŠskâi “¢Ïá÷}Ô‘Þ½_7µ±¢¼¯X;5ám´-XYµçSóÓiÓèEÞ–yÍZ:57´7ßþúË»nÒ×åQ¾aKaHm?ɧæBˆnbY}± MÞ°Ž¶òÞœñ&ò’Ÿ[%Ï/¤ï¦¶m{V“W­9ÌçûÌgóyUЄˆ¶bë„Ó:2þ~Quýè­ÛÇ[Çi/ÝèÙ7C›Nf+.ØñÙ_¦¡íøJŽ@óP¦^ÌêÒhóFœ^0#f­nô­¾;唪^»úw³î„ëW'ܨJåa˜iZÃÁL_¶+ݱnM?»ùd=çjåNë©)Û¨Kѵ0Ší`±-Ç÷Ø‹ôkk\Àº¶¨ÊjÜÝtИÎi%jj¨ýÑqõbŒhÉ«©ù%ËqÓ ƒLö¶ $Å!ȤÈÏbäx—j6ô&§“?Ê‘[лƒgSœõl. Ö Äù%¡„x€REù%rQ„0 @”:ì€(ˆ]£8‰ˆz0s.‡ 1­ÎÂòè»#V|î–I ®[Ü®,¶`ª¶[Œ|R{4ØmoêÎn—ˆ5aåF-r&[=˜Oê^ÌĦ¦ú¢S: .¨s%WÚ‚O³-¸â0®«â³!˜AËJò^Pnèõ¥ž¥•à6M…vhí¶FÈ¿¼3?°ß8©ó‚.X]Ê…Ð¥J]ÏqˆüØvqBÏO$!RU„H‚ãIƒþ^ÏU8ëñw¸pÜ'§”Ö1 ë!÷¦ò]@O(EcÇ…ŽÜÖõÖõÞ,ëÁVõÕÁñëÒëdS1i®õ÷í¶s½;ŸÑ‚µ¥QÓ­÷ðˆ÷xº¨øþÇflÉÅboxÕü‡O“,~H²Î•l&¾› „8Ê@æby`ˆüÙ8 ÃÔEÉjV=I²ôÑOÍþÔìó4뎚MrAo¿ÕR £, "ùŒM¥jÓ@ –¸¶ ?òf$M^¡Ô*…yçi»­p_¤ôšËm‚‚4u³à' ɰÂÄ um\¹ —ãûD)1UC³ê|ÉéñR½2Üe‘Ñ7"®iÞ^ÉW† äoG'€½-Oržÿy—ˆÏ!›7’-cL×éæ¾Qº‰eeï¹$ò¦,ï='~7$á+Ðm.ø–o/s.·n¤Ü#ÞS(÷²Pû#Ô³º*©ñqÙœÝÜ{£€;.ÂÁ1€aè„0Ä&6ˆ1öã(LJ¢x_—rÏîÅü‘áYÇ Îü y¡ Lb@|ìË*ȳA K ׂBtuÌô ÒVF·ïéòíë¿¿}ûúß œ.º¿€Œ»®{w =ˆ3€ a”y®,ìŒcD±“*îtßåŽÜ;» ¼c•þDí>«\U6²}C[—–Žml¯82Sù˶æòîx¥IÒè)ÖC"æÖtg¢r¿‰þÿÿPK!¥Ô¥¿ Û!ppt/slideLayouts/slideLayout5.xmlìYënÛ6þ?`ï x¿Y‹¯A“ºp6Á’>€"ѱVÝ&ÉN²"@_k{œ>ÉHJŠkݤ,@Ñùñ~ß9<¦ß¾;/rg¥š6«ÊÝ |ãNU&Uš•§»“Çð‰Óvq™ÆyUªÝÉ…j'ïö~þém½Óæé~|Q-;Gc”íN¼;Yt]½3¶ÉBqû¦ªU©ßÍ«¦ˆ;ý±9¦M|¦±‹|Š\—N‹8+'Ãüf›ùÕ|ž%*¬’e¡Ê®iTwÚþv‘ÕíˆVoƒV7ªÕ0vöu“º‹Z{ÛUÇçÇgÕÁÉÇnVºNö´ÿÉQž:e\莠*ê¸ÉÚª´oÚú¸QÊ´ÊÕ¯M}T6v‡Õaãd©&N¦Ã‹a˜ýX®lczcúéØŒwÎçMažz7œó݉&íÂüŸš>uÞ9Iß™¬{“ÅÁc“EtÇèé¸ÀtcQãUoÜmwÐèÎqÖåÊ1ÛcíØo»Ñ¢e“íN>K‰|I ¤nìúø@"GˆÉyôÒ̆t'i”åå·tÔ¤·8-²¤©ÚjÞ½IªbǨ1M'ÄÆÊÏ4@CéI‘# BAA$\× ˜p?º6@Û<>­ÓÁßÁñ‘ˆ¶Þ¯’O­SVš(ÃkÏÛÕˆžLó¬£¦Ì ãú—¶±Þå;)æž`œ[î9ã1UÙiÿ®åbï…æb2‘ð" ò@@0~Q0㜸T›@fÏzÒªq>2qŸ%™‰q‰Ënœ÷×2±G9&zôÓŽûçÛMp»oƒ©¤*S'W+•oh7÷aÄãEÖl8zÊjÙt‹­ñˆÙüÀï Yroí„_h¼J*ýY…€AªÕ ûÕ‘+H¤‹ÙÐåÿ]ídb÷ÏeÜhmáÛÒß¾2dO°û+)îA௕Ôk%õZIý/*)úP%E^hf¸„‘` ßgÏ0"D @ …ëúœù?w%u=ÛÃôÑÙøžjj#¿VS¯ÕÔfز1løS×b–¾Ô{NÈ!Ç®/%ðuªbæÀC× =, sÝ篦Ү¯¥6¾ÁþzùÞØµ·ß[ØÉ))Î’ †`˜ÔѹøîýMÛX.‡ZtKÛ;sm‹w¥¨êîziº¢(¶­A±®bèøÒ>òÁ~¿úù§wýbhªsv{enX°¥½Sª_8ÎPîxˆ3ÑóÞm…l™‚GyíT’}ßmã`× –Õ=ÙËçØ‹í¶.y.Ê}Ë;5:‘¼a òvu?ÌÞúçxë%À±þwJêØCµªV ¿èš£mUy¡g¯ úrÓTVÇZ\i-˨é7C%9×»îð«ì7ý¥4—Òª+í`2´éŤf»ƒÙ8̯ç-[Üle«Wè…u³´²£þw´Œß(«…åIZî.žÐ-wÅÚÎÀ¹TW5&÷¸<—3öA·Çäq>¨9£½¬—ö7Jq” ;DÜ” ´ ¢Ø Ñ ûá­¶öÂE)¹Aå·jf—>B´­K)±Ug¥h'jÌ 0=2©³üæf´ˆðz Ñ#ÑÇ8Í‘GâÈ÷ MCßN €œçÕTáLõN…Ï@ ý¹(¿ V'(ëˆÛƦ^ûÝ}FMzãK³9uybºIEuÔA>Ãj„lÑ j£Ž 7½þ3iH¢aúÀò}ÚŒàªUÖÔåK ‹Wµ²>°Aqi™øp¢Á‹.p,Sga:§ÀÎ ùïÏÀçLqë²a%߉¦‚(ør H’ÐÍ“åEŒ!â¡8Ï2¦.¥^{n’¿>*3õO¨„5[@oîÛÏ'oõƒó$Âz¾¬}Œ'.^1"GýÓ”&Áë3èÙ“˜ãW3„F‡‰‹°µ}b“ýòü÷ß^Œ«©k.è‰ï¥k ÓŠ®í”ãÊq¦zÇz:ñ‘ poËEO%\Чô#¬ÝwvÝÐéi;ØK¼xJ<ßnÛš¼Þ÷ló"‚uTBþÓ®'³Úø”ÕFÁ&XFGÿ;%y¡ÚëŽïmK»‰<û*¯7]c ´C¦=”qß ÆÔl8¼ãf¼Ú÷ÍáJXm£b—ÛYn,núr8è‰s/üÆLé긽¡Öqm©“úw”¥UÏÆúÎZï.ñ­wå#ÞŽÙÀùaSUÕœÜÃr°)§ ’YW­ÙŽw –j’Néb’&¹½h×öçªÂYPVU0CÄÍÊJ’  ûq‰£*Ç~øEE{áªLsù³1úòÂLû¶|â[yVó~‡ÑàôÈ‚S%üGaáWeJ¯H.ˆ »G¥ÎHQd$"Á—¥³uÎRúÒÃd/xý~²Ìâá­ÇÌUãnÑT#á‰ú•Ðn«ÞLÀ8ëÉ€E ò˜ñæ¤6½†Q骛äFž:¦/Fõ·iêj —eP•ˆ¤¤@EEB”A‚HBb¸$+1Tk’†²íYÕÞì»ÜKPjÐ7œl@ï6w/óŽÑáVKòì©€ÖÉ™ØgÜó¢&õ¦kf½Ù÷×÷€û¿(ðÇqD¢ù%‰ܬPZÄ)Î~˜–¹ÿÃù¯eèÙ£ÌñO8fHU8L\äyEŽ2¨ÅYè¢c7p?à ¾=f&…t€ìžzº|ÿúíï_ÿ~†ÓEæ mº®g‹v², áÑÌP¦À‘"‰PZ…ªŸ<‹ÓÜ/•vF<ÔŸ¦‘dbä­þvñÜE>ÚÁ 5r£Ø÷|×_0͹ËVߨúaìÄk:^´H`3€œkÓ¨„9»Þ¹¨ÚÍÇÚù?ÿÿPK!nº[ܬ+!ppt/slideLayouts/slideLayout8.xmlÌXínÛ6ý?`ï x¿YSü¨ Ia}p¶Á’>€"ѱV}M¢]§E¾Öö8}’‘”˜8Ž»i3¢kùðð’çð^É/_­«ÒY‰®/šúxâ¾€GÔY“õÕñäÝlâô2­ó´ljq<¹ýäÕɯ¿¼lú2?M¯›¥tGݥǓ…”íÑtÚg Q¥ý‹¦µúnÞtU*ÕÇîjšwéÅ]•S¡7­Ò¢žŒã»CÆ7óy‘‰¸É–•¨å@Ò‰2•*ÿ~Q´½ekak;Ñ+3únJòºU«m.ÿºXOëVê†;9Q+ÏÎËÜ©ÓJ݈šZ*çC!N”¶šÉ`úö¢BGõê÷®=oÏ:3ôÍê¬sŠ\S“éøÅ3ë• ¦[ïl˜­ç]¥¯jGœõñD w­ÿOõ=±–N6ÜÌnïf‹·;°Ù"ÙžÚ ¦“êU ÉÝ_²Ë¹(d)½Q&Ó^ÚŒ–]q<ùÄ9 i à*†„ G˜%ÈçÂÞg=Úõ޲NmþÈ­Ç\ïž®U‘uMßÌ勬©FƒXŸ)I]2Jª³üòˆEšº(T37‚À£”ò`ÆÐçqTÎöjV1×;.Ü Ñ·§Mö¾wêF ¥ut»A bêk»}%õ¸áKÜîòN‰|ÆŒv„úʬwÅÆFûƒˆ®áˆØ”r`nä:lòk=úR]•„i-u/β—çòº&^•î˜P.æ*pÿQÍvË~ÐñÆÀVÿ3ã:5¨LuI5xw>Ì!O¢²ÈÞ;²qD^HçuÚKÑ9foTÍQ$šp` oÍlîf9›[SÚczV¦™X4e®&BÏÔ¢Qc‡Ѐyb6#0?†ã„ùôI-Zäë[Èáî¤.ÃîhÏ€ùÑ»öô\iÏ{æco@bÏð¤ Ñ},b›X P!Þ%›X P!Ù…›X P!݇µzû° BÖTÈöa-@…Á>ìØuæ[}ÜWåM‹y¨$›G_{Åœü~ÇÑß&7}˜ü\dM;¥X‰òF´ŸñbQt‡âý„¼YvêÑáPFrc1€ðû (¹éêZšÍꉟiõôâ$ñg.I€35qˆcL¢$öC<}ƒ×ekbÎÌ"-ç:µõh¯Çv|©?–¡o´|Ì\—*ôÖT§J»Só0XÔ¹ê™:4£–oÔ€µQôsÆ7KîH5>¬Æw§Ôn•å‘/P:Ìw§%l•î‘ÏžYÆa„ÕwKÈÓíå„[M`$Dˆyö­Na }bšå#·ÚÉH¨Ù塞c =ê?R”ÿ»1}_I¥¶¤Æ©wJ*y®%•sÄqpS@bu!À>¢>ô„ÉÓ—Ô\Þ+¨î ú7+ªy“~°î™ÆsõJoV#–$”'€ÌH bN<Ä4$ QHÂÑÏö‚\i(‹JðâjÙ‰·Ki$Üv”ÓW2*EZßOžS—©¿[;©Œèu~–v©vö–)c6Ïš76ò¦Ýè3µœ!ÕðŒ† $A2‹€Jc†}ˆ8øÓÛm.»Áo/ÓNmµÜž£ï±ÜϕڷRŸ—E.œ7ËêrKpï™ Ž1 Ý»`F|fˆ qPàSHˆ½0~zÁû2W{¶Só=n*3„ûy®G ô‰XèAÀÔK2…Q€nÊL¯%­Uv‡V—¯_þùíë—Bu1ûS¦ÝuÞ ÃÀC A论Càƒ÷(à…láD{§uÉ}福‡y§m>ˆ®m ó›¯ Gû¬RýÖ!CÃtxô5¹ÙëGÎõúÕµì^§íÛ•1Ieš{dnµÚ˜ô¢×nä>ùÿÿPK!x^p©‚ÿ!ppt/slideLayouts/slideLayout9.xmlÌXënÛ6ý?`ï h¿Y‹%RA“B7Ò6XÒP%9ªÛ$ÚuVèkmÓ'ÙGJò%—ÖK"€aQô÷ñã9<ýòÕº*UÞõESŸšø…ey6YQ_Ÿšï®â¦ÑˤΒ²©óSó&ïÍWg¿þò²=éËì<¹i–ÒŒº?INÍ…”íÉlÖ§‹¼JúM›×ðÛ¼éªDÂmw=˺ä#`WåŒX–;«’¢6Çüîüf>/ÒÙ¶gb³DìZÖ±Kå€ÜžÈuÐd7*û=\¤N ¬À÷fÙËKyS溽*ñ8 ,Ÿÿ Áýßð´-ú&@µw[õ¥ó:H*e)yÞ]ÏgaY¤ ÙyVHãuÒ˼3ôÜ€çˆ(À[]Ã4v]ηEiO¢œ–éE™¤ù¢)3xy¦%ÔlÄãP âRæ( qL…ø®Åž^¢`}j<ëmôáBu0·ñ¨T3Jœ}¥º˜%­TÊ™í‡(õ!yUÒk+ê <]5uÖò l\:ë;êÕM²…רAx„ïâ)ÏÞây˜ÒƒñTäOŒxt‹‡m¦Vù€Ö. B@N¸ªã€ et·€„pW…=P¡Œ€lQÍÜ#ÊÈ·€ ípRöÊèíº{$) å~Íëì"é•vËEcŠt³S«õ¸ëˆö3uDlSâSÆQ`{>ì×ÔE>ÛØkmÏvðôލüÇÔ¼-’r>š#ù‘]œXúÛ¸Í1v ú§š£v•#š#Þ3³7G¼gÞG0G|lsÜ<‚9îÁ÷`Žû€G0Ç}À‡ÍQÁCÀæ<ó­Îx÷=S-8ýšÙÿð{¦3Yj”Èý—LúL-Uø.¦çñ¡Ø¥XjDQ„YHh…"ŠŸÞR3yÇPñ@úƒŽªOÇßô=}£¥1‡cº®6"<Ž#êÓE6/r§¿¾…ïúêÏÀkPÚô¶ðÃaÙ0J›þÞïµ{}S¿¥ÍƾYéö½¦× ˆ’dº…®øz°Zí2atÇ oûÞ°Y[Â3T9]©|"‹b-Fw@;I’8r1Ã4\€(9âÄÙ%a7C 0\©U†•:|«?O·´GÑyŒrÒéÐHl )>Žq2“÷*husÏž=øôùÃߟ?zôüá¯Ë¹·åvPæå^ýôÍ??|éüýÛ¯kÇ‹<þå/_½üãÏÿR/ Zß=yùôÉ‹ï¿þëçÇx—££<üÄX8×ñ‰s“Ű@Ëøˆ¿Äa„H^¢›„%HÉXÐèë D‘׿osH6àåù]ƒðAÄç’X€×¢Øî1F{Œ[×tMÍ•·Â< í“óyw¡cÛÜÁ†—óÄ=±© "lÐܧàrâKG=cSŒ-bw1ìºGFœ 6‘Îâô±šäÑ” íü²°¶Ù»íôµ©ïãc {Q›JL 3^Fs‰b+cÓ¨¡YÇÀ?NÐŒg O¨L…h˜tÜ‘\ú]2ËŒ ÙG"JaúQºþ˜HÌJbˆõ¼h’q«ÖšjŸ(¹våÓ³œþÉ;O&x$ F².ŠF}ShÓV¬Éý}OÌwbçÑ´ñ€K¨ýIÚ“=üsÜ;s{,]wÚùÜG9h·K¿8º ½.¬ôü˜¤áɸ *wûßN¥{ÓéÿÿPK!Øý¬¶ppt/tableStyles.xml ÌI‚0@Ὁwhþ}-CQ$ +wê*”!é@h£ãÝeùò’/Í?J¢—Xìd4ÿàº5ݤ{ƒc@ÖqÝqi´`° y¶ß¥+»L®iÜBz[×JfÜ£#l-3kœÉ=YóLjo\I6æ7ØAFãa-Z[l:dOR€Ë,€¦ñ™4ÝpË Ëëұɖ(ݪæ_6¦ñ¢?ŒÇOBãЕô[ÃGœ®× %ëè Ýf\Áa9WPºÐðæÔ7\Z¬ÜûÙ2o,qòžû4"ÏÜAãç<Ús+¹öQ[Ö’€Uí¼eK£½#;‚Æ}0ÀaíËol |XØj¥8 ð íñ'´ƒ}$•^ûÿ% >">v¸]â!Ç3÷g ¿zhínÛ¹E}5l¯G ®ä³•åȽ,J¶â!\&rbpÃO†ó|âT·çw»\sÍ h=Z˜ªæúC=º—ú§ÛÕ©¹ãºQ=ÒmÉ-¼öý(÷ºBÓ¬Âúïè`cü1ï©[”\ :‰ÓDó<¶¯#OGI2ž†KßÅš[Ý=[ì/ÿÿPK!xÚËg¸ldocProps/core.xml ¢( Œ“Moœ0@ï•úÏe lh7K¤~¬zh¤H¥j”›kOˆ»`[c¿¯a6›äÐãyóìñ˜âê©m¢G@§ŒÞ’t•´0RézK~U»xC"ç¹–¼1¶dG®Ê÷ï a™07h, Wà¢`ÒŽ »%Þ[F©Ðr· „É{ƒ-÷!ÄšZ.ö¼š%ÉGÚ‚ç’{NGal#9*¥X”¶ÃfHA¡´w4]¥ôÄzÀÖ½Y0ež‘­òƒ…7Ñ9¹ÐON-`ß÷«~=¡áü)½½þñsj5Vz¼+¤,¤`^ùJgAt^5. w.\”T¼FÞº‚.Ј»îÏ_¾œ–— | î –ßAãíëÎý!ú†jU¦i;9UÍä8= ½Aé‚ï, ˜'PYf~Øíl!Ðá þ:<‚{òóP¾Øè50Ö <ªñ•Y>!K< oPi²Ì’t'—qšUiÎòOl}q·Hg¨8òÐÈ( €Æ5g~¯¿|­vääKG_–±4¿»:«? Ûã±ÿǸ©’ –_²<{fœSc"ÈkƒÃá–_DgÿGùÿÿPK-!6¬ù¡ [Content_Types].xmlPK-!ókÑ…ñQ Ú_rels/.relsPK-!3À7 üppt/slides/_rels/slide1.xml.relsPK-!.5 Ðúppt/_rels/presentation.xml.relsPK-!"ùå›M K ppt/presentation.xmlPK-!˜ÎÒ¥Í ppt/slides/slide1.xmlPK-!ÕÑ’ñ¼7,ðppt/slideLayouts/_rels/slideLayout7.xml.relsPK-!i¢_!Ç,öppt/slideMasters/_rels/slideMaster1.xml.relsPK-!ÕÑ’ñ¼7,U!ppt/slideLayouts/_rels/slideLayout9.xml.relsPK-!ÕÑ’ñ¼7-["ppt/slideLayouts/_rels/slideLayout10.xml.relsPK-!ÕÑ’ñ¼7,b#ppt/slideLayouts/_rels/slideLayout8.xml.relsPK-!ÕÑ’ñ¼7,h$ppt/slideLayouts/_rels/slideLayout1.xml.relsPK-!ÕÑ’ñ¼7,n%ppt/slideLayouts/_rels/slideLayout2.xml.relsPK-!ÕÑ’ñ¼7,t&ppt/slideLayouts/_rels/slideLayout3.xml.relsPK-!ÕÑ’ñ¼7,z'ppt/slideLayouts/_rels/slideLayout4.xml.relsPK-!ÕÑ’ñ¼7,€(ppt/slideLayouts/_rels/slideLayout5.xml.relsPK-!ÕÑ’ñ¼7-†)ppt/slideLayouts/_rels/slideLayout11.xml.relsPK-!:b7ü£["*ppt/slideLayouts/slideLayout11.xmlPK-!iŒGFnz"p/ppt/slideLayouts/slideLayout10.xmlPK-!hZ$w85!4ppt/slideLayouts/slideLayout3.xmlPK-!¾øöQD!•9ppt/slideLayouts/slideLayout2.xmlPK-!57דþC!%>ppt/slideLayouts/slideLayout1.xmlPK-!Éõwµ(6!bCppt/slideMasters/slideMaster1.xmlPK-!ž•Æ_®Z!±Kppt/slideLayouts/slideLayout4.xmlPK-!¥Ô¥¿ Û!žPppt/slideLayouts/slideLayout5.xmlPK-!È6w×Óõ !çVppt/slideLayouts/slideLayout6.xmlPK-!ížÊk‚ó !ùZppt/slideLayouts/slideLayout7.xmlPK-!nº[ܬ+!º^ppt/slideLayouts/slideLayout8.xmlPK-!x^p©‚ÿ!¥dppt/slideLayouts/slideLayout9.xmlPK-!ÕÑ’ñ¼7,fjppt/slideLayouts/_rels/slideLayout6.xml.relsPK-!{C¼]ÄÏ lkppt/theme/theme1.xmlPK-!”Œ»«brppt/presProps.xmlPK-!yݲuLtppt/viewProps.xmlPK-!Øý¬¶ðuppt/tableStyles.xmlPK-!pu°Ý%MÍvdocProps/app.xmlPK-!xÚËg¸l(zdocProps/core.xmlPK$$ }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132249.0 specutils-0.7/docs/types_of_spectra.rst0000644000076500000240000001127700000000000020514 0ustar00erikstaff00000000000000.. currentmodule:: specutils Overview of How Specutils Represents Spectra -------------------------------------------- The main principle of ``specutils`` is to work as a toolbox. That is, it aims to provide the pieces needed to build particular spectroscopic workflows without imposing a specific *required* set of algorithms or approaches to spectroscopic analysis. To that end, it aims to represent several different types of ways one might wish to represent sets of spectroscopic data in Python. These objects contains logic to handle multi-dimensional flux data, spectral axes in various forms (wavelenth, frequency, energy, velocity, etc.), convenient and unobtrusive wcs support, and uncertainty handling. The core containers also handle units, a framework for reading and writing from various file formats, and arithmetic operation support. The core data objects are primarily distinguished by the different ways of storing the flux and the spectral axis . These cases are detailed below, along with their corresponding ``specutils`` representations: 1. A 1D flux of length ``n``, and a matched spectral axis (which may be tabulated as an array, or encoded in a WCS). This is what typically is in mind when one speaks of "a single spectrum", and therefore the analysis tools are general couched as applying to this case. In ``specutils`` this is represented by the `~specutils.Spectrum1D` object with a 1-dimensional ``flux``. 2. A set of fluxes that can be represented in an array-like form of shape ``n x m (x ...)``, with a spectral axis strictly of length ``n`` (and a matched WCS). In ``specutils`` this is represented by the `~specutils.Spectrum1D` object where ``len(flux.shape) > 1`` . In this sense the "1D" refers to the spectral axis, *not* the flux. 3. A set of fluxes of shape ``n x m (x ...)``, and a set of spectral axes that are the same shape. This is distinguished from the above cases because there are as many spectral axes as there are spectra. In this sense it is a collection of spectra, so can be thought of as a collection of `~specutils.Spectrum1D` objects. But because it is often more performant to store the collection together as just one set of flux and spectral axis arrays, this case is represented by a separate object in ``specutils``: `~specutils.SpectrumCollection`. 4. An arbitrary collection of fluxes that are not all the same spectral length even in the spectral axis direction. That is, case 3, but "ragged" in the sense that not all the spectra are length ``n``. Because there is no performance benefit to be gained from using arrays (because the flux array is not rectangular), this case does not have a specific representation in ``specutils``. Instead, this case should be dealt with by making lists (or numpy object-arrays) of `~specutils.Spectrum1D` objects, and iterating over them. Specutils does provide a `SpectrumList` class which is a simple subclass of `list` that is integrated with the Astropy IO registry. It enables data loaders to read and return multiple heterogenous spectra (see :ref:`multiple_spectra`). Users should not need to use `SpectrumList` directly since a `list` of `Spectrum1D` objects is sufficient for all other purposes. In all of these cases, the objects have additional attributes (e.g. uncertainties), along with other metadata. But the list above is exhaustive under the assumption that the additional attributes have matched shape to either flux or spectral axis (or some combination of the two). As detailed above, these cases are represented in specutils via two classes: `~specutils.Spectrum1D` (Cases 1 and 2, and indirecly 4) and `~specutils.SpectrumCollection` (Case 3). A diagram of these data structures is proved below. .. image:: specutils_classes_diagrams.png :alt: diagrams of specutils classes While they all differ in details of how they address specific multidimensional datasets, these objects all share the core features that they carry a "spectral axis" and a "flux". The "spectral axis" (``Spectrum*.spectral_axis``) is the physical interpretation for each pixel (the "world coordinate" in WCS language). For astronomical spectra this is usually wavelength, frequency, or photon energy, although for un-calibrated spectra it may simply be in pixel units. The "flux" (``Spectrum*.flux``) is the spectrum itself - while the name "flux" may seem to imply a specific unit, in fact the ``flux`` can carry any unit - actual flux, surface brightness in magnitudes, or counts. However, the best form for representing *calibrated* spectra is spectral flux density (power per area per spectral spectral axis unit), and the analysis tools often work best if a spectrum is in those units. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142576.0 specutils-0.7/setup.cfg0000644000076500000240000000310400000000000015270 0ustar00erikstaff00000000000000[build_sphinx] source-dir = docs build-dir = docs/_build all_files = 1 [build_docs] source-dir = docs build-dir = docs/_build all_files = 1 [upload_docs] upload-dir = docs/_build/html show-response = 1 [tool:pytest] minversion = 3.1 testpaths = "specutils" "docs" norecursedirs = build docs/_build doctest_plus = enabled remote_data_strict = True asdf_schema_root = specutils/io/asdf/schemas # The remote data tests will run by default. Passing --remote-data=none on the # command line will override this setting. addopts = --remote-data=any --doctest-rst [ah_bootstrap] auto_use = True [pycodestyle] # E101 - mix of tabs and spaces # W191 - use of tabs # W291 - trailing whitespace # W292 - no newline at end of file # W293 - trailing whitespace # W391 - blank line at end of file # E111 - 4 spaces per indentation level # E112 - 4 spaces per indentation level # E113 - 4 spaces per indentation level # E901 - SyntaxError or IndentationError # E902 - IOError select = E101,W191,W291,W292,W293,W391,E111,E112,E113,E901,E902 exclude = extern,sphinx,*parsetab.py [metadata] package_name = specutils description = Package for spectroscopic astronomical data long_description = Provides data objects and analysis tools for creating and manipulating spectroscopic astronomical data. author = Specutils team author_email = coordinators@astropy.org license = BSD url = http://specutils.readthedocs.io/ edit_on_github = False github_project = astropy/specutils install_requires = astropy>=4.0, gwcs>=0.12, scipy # version should be PEP440 compatible (http://www.python.org/dev/peps/pep-0440) version = 0.7 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/setup.py0000755000076500000240000001205700000000000015173 0ustar00erikstaff00000000000000#!/usr/bin/env python # Licensed under a 3-clause BSD style license - see LICENSE.rst import glob import os import sys # This is the same check as packagename/__init__.py but this one has to # happen before importing ah_bootstrap. __minimum_python_version__ = '3.5' if sys.version_info < tuple((int(val) for val in __minimum_python_version__.split('.'))): sys.stderr.write("ERROR: specutils requires Python {} or later\n".format(__minimum_python_version__)) sys.exit(1) import ah_bootstrap from setuptools import setup # A dirty hack to get around some early import/configurations ambiguities import builtins builtins._ASTROPY_SETUP_ = True from astropy_helpers.setup_helpers import (register_commands, get_debug_option, get_package_info) from astropy_helpers.git_helpers import get_git_devstr from astropy_helpers.version_helpers import generate_version_py # Get some values from the setup.cfg try: from ConfigParser import ConfigParser except ImportError: from configparser import ConfigParser conf = ConfigParser() conf.read(['setup.cfg']) metadata = dict(conf.items('metadata')) PACKAGENAME = metadata.get('package_name', 'specutils') DESCRIPTION = metadata.get('description', 'packagename') AUTHOR = metadata.get('author', 'Astropy-specutils Developers') AUTHOR_EMAIL = metadata.get('author_email', '') LICENSE = metadata.get('license', 'BSD-3') URL = metadata.get('url', 'http://astropy.org') # order of priority for long_description: # (1) set in setup.cfg, # (2) load LONG_DESCRIPTION.rst, # (3) load README.rst, # (4) package docstring readme_glob = 'README*' _cfg_long_description = metadata.get('long_description', '') if _cfg_long_description: LONG_DESCRIPTION = _cfg_long_description elif os.path.exists('LONG_DESCRIPTION.rst'): with open('LONG_DESCRIPTION.rst') as f: LONG_DESCRIPTION = f.read() elif len(glob.glob(readme_glob)) > 0: with open(glob.glob(readme_glob)[0]) as f: LONG_DESCRIPTION = f.read() else: # Get the long description from the package's docstring __import__(PACKAGENAME) package = sys.modules[PACKAGENAME] LONG_DESCRIPTION = package.__doc__ # Store the package name in a built-in variable so it's easy # to get from other parts of the setup infrastructure builtins._ASTROPY_PACKAGE_NAME_ = PACKAGENAME # VERSION should be PEP440 compatible (http://www.python.org/dev/peps/pep-0440) VERSION = metadata.get('version', '0.0.dev0') # Indicates if this version is a release version RELEASE = 'dev' not in VERSION if not RELEASE: VERSION += get_git_devstr(False) # Populate the dict of setup command overrides; this should be done before # invoking any other functionality from distutils since it can potentially # modify distutils' behavior. cmdclassd = register_commands(PACKAGENAME, VERSION, RELEASE) # Freeze build information in version.py generate_version_py(PACKAGENAME, VERSION, RELEASE, get_debug_option(PACKAGENAME)) # Treat everything in scripts except README* as a script to be installed scripts = [fname for fname in glob.glob(os.path.join('scripts', '*')) if not os.path.basename(fname).startswith('README')] # Get configuration information from all of the various subpackages. # See the docstring for setup_helpers.update_package_files for more # details. package_info = get_package_info() # Add the project-global data package_info['package_data'].setdefault(PACKAGENAME, []) package_info['package_data'][PACKAGENAME].append('data/*') # Define entry points for command-line scripts entry_points = {'console_scripts': []} entry_points['asdf_extensions'] = 'specutils = specutils.io.asdf.extension:SpecutilsExtension' if conf.has_section('entry_points'): entry_point_list = conf.items('entry_points') for entry_point in entry_point_list: entry_points['console_scripts'].append('{0} = {1}'.format( entry_point[0], entry_point[1])) # Include all .c files, recursively, including those generated by # Cython, since we can not do this in MANIFEST.in with a "dynamic" # directory name. c_files = [] for root, dirs, files in os.walk(PACKAGENAME): for filename in files: if filename.endswith('.c'): c_files.append( os.path.join( os.path.relpath(root, PACKAGENAME), filename)) package_info['package_data'][PACKAGENAME].extend(c_files) # Note that requires and provides should not be included in the call to # ``setup``, since these are now deprecated. See this link for more details: # https://groups.google.com/forum/#!topic/astropy-dev/urYO8ckB2uM setup(name=PACKAGENAME, version=VERSION, description=DESCRIPTION, scripts=scripts, install_requires=[s.strip() for s in metadata.get('install_requires', 'astropy>=3.1').split(',')], author=AUTHOR, author_email=AUTHOR_EMAIL, license=LICENSE, url=URL, long_description=LONG_DESCRIPTION, cmdclass=cmdclassd, zip_safe=False, use_2to3=False, entry_points=entry_points, python_requires='>=' + __minimum_python_version__, **package_info ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/0000755000076500000240000000000000000000000015464 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/__init__.py0000644000076500000240000000303100000000000017572 0ustar00erikstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Specutils: an astropy package for spectroscopy. """ # Packages may add whatever they like to this file, but # should keep this content at the top. # ---------------------------------------------------------------------------- from ._astropy_init import * from astropy import config as _config # ---------------------------------------------------------------------------- # Enforce Python version check during package import. # This is the same check as the one at the top of setup.py import sys __minimum_python_version__ = "3.5" class UnsupportedPythonError(Exception): pass if sys.version_info < tuple((int(val) for val in __minimum_python_version__.split('.'))): raise UnsupportedPythonError("packagename does not support Python < {}".format(__minimum_python_version__)) if not _ASTROPY_SETUP_: # For egg_info test builds to pass, put package imports here. # Allow loading spectrum object from top level module from .spectra import * # Load the IO functions from .io.default_loaders import * # noqa from .io.registers import _load_user_io _load_user_io() __citation__ = 'https://doi.org/10.5281/zenodo.1421356' class Conf(_config.ConfigNamespace): """ Configuration parameters for specutils. """ do_continuum_function_check = _config.ConfigItem( True, 'Whether to check the spectrum baseline value is close' 'to zero. If it is not within ``threshold`` then a warning is raised.' ) conf = Conf() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/_astropy_init.py0000644000076500000240000000374700000000000020734 0ustar00erikstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst __all__ = ['__version__', '__githash__'] # 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__ = '' if not _ASTROPY_SETUP_: # noqa import os from warnings import warn from astropy.config.configuration import ( update_default_config, ConfigurationDefaultMissingError, ConfigurationDefaultMissingWarning) # Create the test function for self test from astropy.tests.runner import TestRunner test = TestRunner.make_test_runner_in(os.path.dirname(__file__)) test.__test__ = False __all__ += ['test'] # 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: update_default_config( __package__, config_dir, version=__version__) except TypeError as orig_error: try: update_default_config(__package__, config_dir) except ConfigurationDefaultMissingError as e: wmsg = (e.args[0] + " Cannot install default profile. If you are " "importing from source, this is expected.") warn(ConfigurationDefaultMissingWarning(wmsg)) del e except Exception: raise orig_error ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/analysis/0000755000076500000240000000000000000000000017307 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/analysis/__init__.py0000644000076500000240000000023700000000000021422 0ustar00erikstaff00000000000000from .flux import * # noqa from .uncertainty import * # noqa from .location import * # noqa from .width import * # noqa from .template_comparison import * ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142552.0 specutils-0.7/specutils/analysis/flux.py0000644000076500000240000001740300000000000020644 0ustar00erikstaff00000000000000""" A module for analysis tools focused on determining fluxes of spectral features. """ from functools import wraps import warnings import numpy as np from .. import conf from ..manipulation import extract_region from .utils import computation_wrapper import astropy.units as u from astropy.stats import sigma_clip from astropy.stats import mad_std from astropy.utils.exceptions import AstropyUserWarning __all__ = ['line_flux', 'equivalent_width', 'is_continuum_below_threshold', 'warn_continuum_below_threshold'] def line_flux(spectrum, regions=None): """ Computes the integrated flux in a spectrum or region of a spectrum. Applies to the whole spectrum by default, but can be limited to a specific feature (like a spectral line) if a region is given. Parameters ---------- spectrum : Spectrum1D The spectrum object over which the summed flux will be calculated. regions : `~specutils.SpectralRegion` or list of `~specutils.SpectralRegion` Region within the spectrum to calculate the gaussian sigma width. If regions is `None`, computation is performed over entire spectrum. Returns ------- flux : `~astropy.units.Quantity` Flux in the provided spectrum (or regions). Unit isthe ``spectrum``'s' ``flux`` unit times ``spectral_axis`` unit. Notes ----- While the flux can be computed on any spectrum or region, it should be continuum-subtracted to compute actual line fluxes. """ return computation_wrapper(_compute_line_flux, spectrum, regions) def equivalent_width(spectrum, continuum=1, regions=None): """ Computes the equivalent width of a region of the spectrum. Applies to the whole spectrum by default, but can be limited to a specific feature (like a spectral line) if a region is given. Parameters ---------- spectrum : Spectrum1D The spectrum object overwhich the equivalent width will be calculated. regions: `~specutils.SpectralRegion` or list of `~specutils.SpectralRegion` Region within the spectrum to calculate the gaussian sigma width. If regions is `None`, computation is performed over entire spectrum. continuum : ``1`` or `~astropy.units.Quantity`, optional Value to assume is the continuum level. For the special value ``1`` (without units), ``1`` in whatever the units of the ``spectrum.flux`` will be assumed, otherwise units are required and must be the same as the ``spectrum.flux``. Returns ------- ew : `~astropy.units.Quantity` Equivalent width calculation, in the same units as the ``spectrum``'s ``spectral_axis``. Notes ----- To do a standard equivalent width measurement, the ``spectrum`` should be continuum-normalized to whatever ``continuum`` is before this function is called. """ kwargs = dict(continuum=continuum) return computation_wrapper(_compute_equivalent_width, spectrum, regions, **kwargs) def _compute_line_flux(spectrum, regions=None): if regions is not None: calc_spectrum = extract_region(spectrum, regions) else: calc_spectrum = spectrum # Average dispersion in the line region avg_dx = np.diff(calc_spectrum.spectral_axis) line_flux = np.sum(calc_spectrum.flux[1:] * avg_dx) # TODO: we may want to consider converting to erg / cm^2 / sec by default return line_flux def _compute_equivalent_width(spectrum, continuum=1, regions=None): if regions is not None: calc_spectrum = extract_region(spectrum, regions) else: calc_spectrum = spectrum if continuum == 1: continuum = 1*calc_spectrum.flux.unit spectral_axis = calc_spectrum.spectral_axis dx = spectral_axis[-1] - spectral_axis[0] line_flux = _compute_line_flux(spectrum, regions) # Calculate equivalent width ew = dx - (line_flux / continuum) return ew.to(calc_spectrum.spectral_axis.unit) def is_continuum_below_threshold(spectrum, threshold=0.01): """ Determine if the baseline of this spectrum is less than a threshold. I.e., an estimate of whether or not the continuum has been subtracted. If ``threshold`` is an `~astropy.units.Quantity` with flux units, this directly compares the median of the spectrum to the threshold. of the flux to the threshold. If the threshold is a float or dimensionless quantity then the spectrum's uncertainty will be used or an estimate of the uncertainty. If the uncertainty is present then the threshold is compared to the median of the flux divided by the uncertainty. If the uncertainty is not present then the threshold is compared to the median of the flux divided by the `~astropy.stats.mad_std`. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object over which the width will be calculated. threshold: float or `~astropy.units.Quantity` The tolerance on the quantification to confirm the continuum is near zero. Returns ------- is_continuum_below_threshold: bool Return True if the continuum of the spectrum is below the threshold, False otherwise. """ flux = spectrum.flux uncertainty = spectrum.uncertainty if hasattr(spectrum, 'uncertainty') else None # Apply the mask if it exists. if hasattr(spectrum, 'mask') and spectrum.mask is not None: flux = flux[~spectrum.mask] uncertainty = uncertainty[~spectrum.mask] if uncertainty else uncertainty # If the threshold has units then the assumption is that we want # to compare the median of the flux regardless of the # existence of the uncertainty. if hasattr(threshold, 'unit') and not threshold.unit == u.dimensionless_unscaled: return np.median(flux) < threshold # If threshold does not have a unit, ie it is not a quantity, then # we are going to calculate based on the S/N if the uncertainty # exists. if uncertainty and uncertainty.uncertainty_type != 'std': return np.median(flux / uncertainty.quantity) < threshold else: return np.median(flux) / mad_std(flux) < threshold def warn_continuum_below_threshold(threshold=0.01): """ Decorator for methods that should warn if the baseline of the spectrum does not appear to be below a threshold. The ``check`` parameter is based on the `astropy configuration system `_. Examples are on that page to show how to turn off this type of warning checking. """ def actual_decorator(function): @wraps(function) def wrapper(*args, **kwargs): if conf.do_continuum_function_check: spectrum = args[0] if not is_continuum_below_threshold(spectrum, threshold): if hasattr(threshold, 'unit'): levelorsnr = 'value' else: levelorsnr = 'signal-to-noise' message = "Spectrum is not below the threshold {} {}. ".format(levelorsnr, threshold) message += "This may indicate you have not continuum subtracted this spectrum (or that you have but it has high SNR features).\n\n" message += ("""If you want to suppress this warning either type """ """'specutils.conf.do_continuum_function_check = False' or """ """see http://docs.astropy.org/en/stable/config/#adding-new-configuration-items """ """for other ways to configure the warning.""") warnings.warn(message, AstropyUserWarning) result = function(*args, **kwargs) return result return wrapper return actual_decorator ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/specutils/analysis/location.py0000644000076500000240000000477700000000000021510 0ustar00erikstaff00000000000000""" A module for analysis tools focused on determining the location of spectral features. """ import numpy as np from ..spectra import SpectralRegion from ..manipulation import extract_region __all__ = ['centroid'] def centroid(spectrum, region): """ Calculate the centroid of a region, or regions, of the spectrum. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object overwhich the centroid will be calculated. region: `~specutils.utils.SpectralRegion` or list of `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the centroid. Returns ------- centroid : float or list (based on region input) Centroid of the spectrum or within the regions Notes ----- The spectrum will need to be continuum subtracted before calling this method. See the `analysis documentation `_ for more information. """ # No region, therefore whole spectrum. if region is None: return _centroid_single_region(spectrum) # Single region elif isinstance(region, SpectralRegion): return _centroid_single_region(spectrum, region=region) # List of regions elif isinstance(region, list): return [_centroid_single_region(spectrum, region=reg) for reg in region] def _centroid_single_region(spectrum, region=None): """ Calculate the centroid of the spectrum based on the flux and uncertainty in the spectrum. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object overwhich the centroid will be calculated. region: `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the centroid. Returns ------- centroid : float or list (based on region input) Centroid of the spectrum or within the regions Notes ----- This is a helper function for the above `centroid()` method. """ if region is not None: calc_spectrum = extract_region(spectrum, region) else: calc_spectrum = spectrum flux = calc_spectrum.flux dispersion = calc_spectrum.spectral_axis if len(flux.shape) > 1: dispersion = np.tile(dispersion, [flux.shape[0], 1]) # the axis=-1 will enable this to run on single-dispersion, single-flux # and single-dispersion, multiple-flux return np.sum(flux * dispersion, axis=-1) / np.sum(flux, axis=-1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/analysis/template_comparison.py0000644000076500000240000002304500000000000023732 0ustar00erikstaff00000000000000import numpy as np from ..manipulation import (FluxConservingResampler, LinearInterpolatedResampler, SplineInterpolatedResampler) from ..spectra.spectrum1d import Spectrum1D __all__ = ['template_match', 'template_redshift'] def _normalize_for_template_matching(observed_spectrum, template_spectrum): """ Calculate a scale factor to be applied to the template spectrum so the total flux in both spectra will be the same. Parameters ---------- observed_spectrum : :class:`~specutils.Spectrum1D` The observed spectrum. template_spectrum : :class:`~specutils.Spectrum1D` The template spectrum, which needs to be normalized in order to be compared with the observed spectrum. Returns ------- `float` A float which will normalize the template spectrum's flux so that it can be compared to the observed spectrum. """ num = np.sum((observed_spectrum.flux*template_spectrum.flux)/ (observed_spectrum.uncertainty.array**2)) denom = np.sum((template_spectrum.flux/ observed_spectrum.uncertainty.array)**2) return num/denom def _resample(resample_method): """ Find the user preferred method of resampling the template spectrum to fit the observed spectrum. Parameters ---------- resample_method: `string` The type of resampling to be done on the template spectrum. Returns ------- :class:`~specutils.ResamplerBase` This is the actual class that will handle the resampling. """ if resample_method == "flux_conserving": return FluxConservingResampler() if resample_method == "linear_interpolated": return LinearInterpolatedResampler() if resample_method == "spline_interpolated": return SplineInterpolatedResampler() return None def _chi_square_for_templates(observed_spectrum, template_spectrum, resample_method): """ Resample the template spectrum to match the wavelength of the observed spectrum. Then, calculate chi2 on the flux of the two spectra. Parameters ---------- observed_spectrum : :class:`~specutils.Spectrum1D` The observed spectrum. template_spectrum : :class:`~specutils.Spectrum1D` The template spectrum, which will be resampled to match the wavelength of the observed spectrum. Returns ------- normalized_template_spectrum : :class:`~specutils.Spectrum1D` The normalized spectrum template. chi2 : `float` The chi2 of the flux of the observed spectrum and the flux of the normalized template spectrum. """ # Resample template if _resample(resample_method) != 0: fluxc_resample = _resample(resample_method) template_obswavelength = fluxc_resample(template_spectrum, observed_spectrum.wavelength) # Normalize spectra normalization = _normalize_for_template_matching(observed_spectrum, template_obswavelength) # Numerator num_right = normalization * template_obswavelength.flux num = observed_spectrum.flux - num_right # Denominator denom = observed_spectrum.uncertainty.array * observed_spectrum.flux.unit # Get chi square result = (num/denom)**2 chi2 = np.sum(result.value) # Create normalized template spectrum, which will be returned with # corresponding chi2 normalized_template_spectrum = Spectrum1D( spectral_axis=template_spectrum.spectral_axis, flux=template_spectrum.flux*normalization) return normalized_template_spectrum, chi2 def template_match(observed_spectrum, spectral_templates, resample_method="flux_conserving", redshift=None): """ Find which spectral templates is the best fit to an observed spectrum by computing the chi-squared. If two template_spectra have the same chi2, the first template is returned. Parameters ---------- observed_spectrum : :class:`~specutils.Spectrum1D` The observed spectrum. spectral_templates : :class:`~specutils.Spectrum1D` or :class:`~specutils.SpectrumCollection` or `list` That will give a single :class:`~specutils.Spectrum1D` when iterated over. The template spectra, which will be resampled, normalized, and compared to the observed spectrum, where the smallest chi2 and normalized template spectrum will be returned. resample_method : `string` Three resample options: flux_conserving, linear_interpolated, and spline_interpolated. Anything else does not resample the spectrum. known_redshift: `float` If the user knows the redshift they want to apply to the spectrum/spectra within spectral_templates, then this redshift can be applied to each template before attempting the match. redshift : 'float', `int`, `list`, `tuple`, 'numpy.array` If the user knows the redshift they want to apply to the spectrum/spectra within spectral_templates, then this float or int value redshift can be applied to each template before attempting the match. Or, alternatively, an iterable with redshift values to be applied to each template, before computation of the corresponding chi2 value, can be passed via this same parameter. For each template, the redshift value that results in the smallest chi2 is used. Returns ------- normalized_template_spectrum : :class:`~specutils.Spectrum1D` The template spectrum that has been normalized. chi2 : `float` The chi2 of the flux of the observed_spectrum and the flux of the normalized template spectrum. smallest_chi_index : `int` The index of the spectrum with the smallest chi2 in spectral templates. chi2_list : `list` A list with all chi2 values found for each template spectrum. """ if hasattr(spectral_templates, 'flux') and len(spectral_templates.flux.shape) == 1: # Account for redshift if provided chi2_list = [] if redshift is not None: _, redshifted_spectrum, chi2_list = template_redshift(observed_spectrum, spectral_templates, redshift=redshift) spectral_templates = redshifted_spectrum normalized_spectral_template, chi2 = _chi_square_for_templates( observed_spectrum, spectral_templates, resample_method) return normalized_spectral_template, chi2, 0, chi2_list # At this point, the template spectrum is either a ``SpectrumCollection`` # or a multi-dimensional``Spectrum1D``. Loop through the object and return # the template spectrum with the lowest chi square and its corresponding # chi square. chi2_min = None smallest_chi_spec = None chi2_list = [] for index, spectrum in enumerate(spectral_templates): # Account for redshift if provided if redshift is not None: _, redshifted_spectrum, chi2_inner_list = template_redshift( observed_spectrum, spectrum, redshift=redshift) spectrum = redshifted_spectrum chi2_list.append(chi2_inner_list) normalized_spectral_template, chi2 = _chi_square_for_templates( observed_spectrum, spectrum, resample_method) if chi2_min is None or chi2 < chi2_min: chi2_min = chi2 smallest_chi_spec = normalized_spectral_template smallest_chi_index = index return smallest_chi_spec, chi2_min, smallest_chi_index, chi2_list def template_redshift(observed_spectrum, template_spectrum, redshift): """ Find the best-fit redshift for template_spectrum to match observed_spectrum using chi2. Parameters ---------- observed_spectrum : :class:`~specutils.Spectrum1D` The observed spectrum. template_spectrum : :class:`~specutils.Spectrum1D` The template spectrum, which will have it's redshift calculated. redshift : `float`, `int`, `list`, `tuple`, 'numpy.array` A scalar or iterable with the redshift values to test. Returns ------- final_redshift : `float` The best-fit redshift for template_spectrum to match the observed_spectrum. redshifted_spectrum: :class:`~specutils.Spectrum1D` A new Spectrum1D object which incorporates the template_spectrum with a spectral_axis that has been redshifted using the final_redshift. chi2_list : `list` A list with the chi2 values corresponding to each input redshift value. """ chi2_min = None final_redshift = None chi2_list = [] redshift = np.array(redshift).reshape((np.array(redshift).size,)) # Loop which goes through available redshift values and finds the smallest chi2 for rs in redshift: # Create new redshifted spectrum and run it through the chi2 method redshifted_spectrum = Spectrum1D(spectral_axis=template_spectrum.spectral_axis*(1+rs), flux=template_spectrum.flux, uncertainty=template_spectrum.uncertainty, meta=template_spectrum.meta) normalized_spectral_template, chi2 = _chi_square_for_templates( observed_spectrum, redshifted_spectrum, "flux_conserving") chi2_list.append(chi2) # Set new chi2_min if suitable replacement is found if not np.isnan(chi2) and (chi2_min is None or chi2 < chi2_min): chi2_min = chi2 final_redshift = rs return final_redshift, redshifted_spectrum, chi2_list ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/analysis/uncertainty.py0000644000076500000240000001264600000000000022237 0ustar00erikstaff00000000000000""" A module for analysis tools dealing with uncertainties or error analysis in spectra. """ import numpy as np from ..spectra import SpectralRegion from ..manipulation import extract_region __all__ = ['snr', 'snr_derived'] def snr(spectrum, region=None): """ Calculate the mean S/N of the spectrum based on the flux and uncertainty in the spectrum. This will be calculated over the regions, if they are specified. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object overwhich the equivalent width will be calculated. region: `~specutils.utils.SpectralRegion` or list of `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the SNR. Returns ------- snr : `~astropy.units.Quantity` or list (based on region input) Signal to noise ratio of the spectrum or within the regions Notes ----- The spectrum will need to have the uncertainty defined in order for the SNR to be calculated. If the goal is instead signal to noise *per pixel*, this should be computed directly as ``spectrum.flux / spectrum.uncertainty``. """ if not hasattr(spectrum, 'uncertainty') or spectrum.uncertainty is None: raise Exception("Spectrum1D currently requires the uncertainty be defined.") # No region, therefore whole spectrum. if region is None: return _snr_single_region(spectrum) # Single region elif isinstance(region, SpectralRegion): return _snr_single_region(spectrum, region=region) # List of regions elif isinstance(region, list): return [_snr_single_region(spectrum, region=reg) for reg in region] def _snr_single_region(spectrum, region=None): """ Calculate the mean S/N of the spectrum based on the flux and uncertainty in the spectrum. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object overwhich the equivalent width will be calculated. region: `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the SNR. Returns ------- snr : `~astropy.units.Quantity` or list (based on region input) Signal to noise ratio of the spectrum or within the regions Notes ----- This is a helper function for the above `snr()` method. """ if region is not None: calc_spectrum = extract_region(spectrum, region) else: calc_spectrum = spectrum flux = calc_spectrum.flux uncertainty = calc_spectrum.uncertainty.array * spectrum.uncertainty.unit # the axis=-1 will enable this to run on single-dispersion, single-flux # and single-dispersion, multiple-flux return np.mean(flux / uncertainty, axis=-1) def snr_derived(spectrum, region=None): """ This function computes the signal to noise ratio DER_SNR following the definition set forth by the Spectral Container Working Group of ST-ECF, MAST and CADC. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object overwhich the equivalent width will be calculated. region: `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the SNR. Returns ------- snr : `~astropy.units.Quantity` or list (based on region input) Signal to noise ratio of the spectrum or within the regions Notes ----- The DER_SNR algorithm is an unbiased estimator describing the spectrum as a whole as long as the noise is uncorrelated in wavelength bins spaced two pixels apart, the noise is Normal distributed, for large wavelength regions, the signal over the scale of 5 or more pixels can be approximated by a straight line. Code and some docs copied from ``http://www.stecf.org/software/ASTROsoft/DER_SNR/der_snr.py`` """ # No region, therefore whole spectrum. if region is None: return _snr_derived(spectrum) # Single region elif isinstance(region, SpectralRegion): return _snr_derived(spectrum, region=region) # List of regions elif isinstance(region, list): return [_snr_derived(spectrum, region=reg) for reg in region] def _snr_derived(spectrum, region=None): """ This function computes the signal to noise ratio DER_SNR following the definition set forth by the Spectral Container Working Group of ST-ECF, MAST and CADC Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object overwhich the equivalent width will be calculated. region: `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the SNR. Returns ------- snr : `~astropy.units.Quantity` or list (based on region input) Signal to noise ratio of the spectrum or within the regions Notes ----- This is a helper function for the above `snr_derived()` method. """ if region is not None: calc_spectrum = extract_region(spectrum, region) else: calc_spectrum = spectrum flux = calc_spectrum.flux # Values that are exactly zero (padded) are skipped n = len(flux) # For spectra shorter than this, no value can be returned if n > 4: signal = np.median(flux) noise = 0.6052697 * np.median(np.abs(2.0 * flux[2:n-2] - flux[0:n-4] - flux[4:n])) return signal / noise else: return 0.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537501088.0 specutils-0.7/specutils/analysis/utils.py0000644000076500000240000000127400000000000021025 0ustar00erikstaff00000000000000""" A module for internal utilities for the analysis sub-package. Not meant for public API consumption. """ from ..spectra import SpectralRegion __all__ = ['computation_wrapper'] def computation_wrapper(func, spectrum, region, **kwargs): """ Applies a computation across either a whole spectrum or a bunch of regions. """ # No region, therefore whole spectrum. if region is None: return func(spectrum, **kwargs) # Single region elif isinstance(region, SpectralRegion): return func(spectrum, regions=region, **kwargs) # List of regions elif isinstance(region, list): return [func(spectrum, regions=reg, **kwargs) for reg in region] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/analysis/width.py0000644000076500000240000002140700000000000021004 0ustar00erikstaff00000000000000""" A module for analysis tools focused on determining the width of spectral features. """ import numpy as np from astropy.stats.funcs import gaussian_sigma_to_fwhm from astropy.modeling.models import Gaussian1D from ..manipulation import extract_region from . import centroid from .utils import computation_wrapper from scipy.signal import chirp, find_peaks, peak_widths __all__ = ['gaussian_sigma_width', 'gaussian_fwhm', 'fwhm', 'fwzi'] def gaussian_sigma_width(spectrum, regions=None): """ Estimate the width of the spectrum using a second-moment analysis. The value is scaled to match the sigma/standard deviation parameter of a standard Gaussian profile. This will be calculated over the regions, if they are specified. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object over which the width will be calculated. regions: `~specutils.utils.SpectralRegion` or list of `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the gaussian sigma width. If regions is `None`, computation is performed over entire spectrum. Returns ------- approx_sigma: `~astropy.units.Quantity` or list (based on region input) Approximated sigma value of the spectrum Notes ----- The spectrum should be continuum subtracted before being passed to this function. """ return computation_wrapper(_compute_gaussian_sigma_width, spectrum, regions) def gaussian_fwhm(spectrum, regions=None): """ Estimate the width of the spectrum using a second-moment analysis. The value is scaled to match the full width at half max of a standard Gaussian profile. This will be calculated over the regions, if they are specified. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object overwhich the width will be calculated. regions: `~specutils.utils.SpectralRegion` or list of `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the FWHM value. If regions is `None`, computation is performed over entire spectrum. Returns ------- gaussian_fwhm : `~astropy.units.Quantity` or list (based on region input) Approximate full width of the signal at half max Notes ----- The spectrum should be continuum subtracted before being passed to this function. """ return computation_wrapper(_compute_gaussian_fwhm, spectrum, regions) def fwhm(spectrum, regions=None): """ Compute the true full width half max of the spectrum. This makes no assumptions about the shape of the spectrum (e.g. whether it is Gaussian). It finds the maximum of the spectrum, and then locates the point closest to half max on either side of the maximum, and measures the distance between them. This will be calculated over the regions, if they are specified. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object over which the width will be calculated. regions: `~specutils.utils.SpectralRegion` or list of `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the FWHM value. If regions is `None`, computation is performed over entire spectrum. Returns ------- whm : `~astropy.units.Quantity` or list (based on region input) Full width of the signal at half max Notes ----- The spectrum should be continuum subtracted before being passed to this function. """ return computation_wrapper(_compute_fwhm, spectrum, regions) def fwzi(spectrum, regions=None): """ Compute the true full width at zero intensity (i.e. the continuum level) of the spectrum. This makes no assumptions about the shape of the spectrum. It uses the scipy peak-finding to determine the index of the highest flux value, and then calculates width at that base of the feature. Parameters ---------- spectrum : `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object over which the width will be calculated. regions: `~specutils.utils.SpectralRegion` or list of `~specutils.utils.SpectralRegion` Region within the spectrum to calculate the FWZI value. If regions is `None`, computation is performed over entire spectrum. Returns ------- `~astropy.units.Quantity` or list (based on region input) Full width of the signal at zero intensity. Notes ----- The spectrum must be continuum subtracted before being passed to this function. """ return computation_wrapper(_compute_fwzi, spectrum, regions) def _compute_fwzi(spectrum, regions=None): if regions is not None: calc_spectrum = extract_region(spectrum, regions) else: calc_spectrum = spectrum # Create a copy of the flux array to ensure the value on the spectrum # object is not altered. disp, flux = calc_spectrum.spectral_axis, calc_spectrum.flux.copy() # For noisy data, ensure that the search from the centroid stops on # either side once the flux value reaches zero. flux[flux < 0] = 0 def find_width(data): # Find the peaks in the flux data peaks, _ = find_peaks(data) # Find the index of the maximum peak value in the found peak list peak_ind = [peaks[np.argmin(np.abs( np.array(peaks) - np.argmin(np.abs(data - np.max(data)))))]] # Calculate the width for the given feature widths, _, _, _ = \ peak_widths(data, peak_ind, rel_height=1-1e-7) return widths[0] * disp.unit if flux.ndim > 1: tot_widths = [] for i in range(flux.shape[0]): tot_widths.append(find_width(flux[i])) return tot_widths return find_width(flux) def _compute_gaussian_fwhm(spectrum, regions=None): """ This is a helper function for the above `gaussian_fwhm()` method. """ fwhm = _compute_gaussian_sigma_width(spectrum, regions) * gaussian_sigma_to_fwhm return fwhm def _compute_gaussian_sigma_width(spectrum, regions=None): """ This is a helper function for the above `gaussian_sigma_width()` method. """ if regions is not None: calc_spectrum = extract_region(spectrum, regions) else: calc_spectrum = spectrum flux = calc_spectrum.flux spectral_axis = calc_spectrum.spectral_axis centroid_result = centroid(spectrum, regions) if flux.ndim > 1: spectral_axis = np.broadcast_to(spectral_axis, flux.shape, subok=True) centroid_result = centroid_result[:, np.newaxis] dx = spectral_axis - centroid_result sigma = np.sqrt(np.sum((dx * dx) * flux, axis=-1) / np.sum(flux, axis=-1)) return sigma def _compute_single_fwhm(flux, spectral_axis): """ This is a helper function for the above `fwhm()` method. """ # The .value attribute is used here as the following algorithm does not # use any array operations and would otherwise introduce a relatively # significant overhead factor. Two-point linear interpolation is used to # achieve sub-pixel precision. flux_value = flux.value spectral_value = spectral_axis.value argmax = flux_value.argmax() halfval = flux_value[argmax] / 2 left = flux_value[:argmax] < halfval right = flux_value[argmax + 1:] < halfval # Highest signal at the first point i0 = np.nonzero(left)[0] if i0.size == 0: left_value = spectral_value[0] else: i0 = i0[-1] i1 = i0 + 1 left_flux = flux_value[i0] left_spectral = spectral_value[i0] left_value = ((halfval - left_flux) * (spectral_value[i1] - left_spectral) / (flux_value[i1] - left_flux) + left_spectral) # Highest signal at the last point i1 = np.nonzero(right)[0] if i1.size == 0: right_value = spectral_value[-1] else: i1 = i1[0] + argmax + 1 i0 = i1 - 1 left_flux = flux_value[i0] left_spectral = spectral_value[i0] right_value = ((halfval - left_flux) * (spectral_value[i1] - left_spectral) / (flux_value[i1] - left_flux) + left_spectral) return spectral_axis.unit * (right_value - left_value) def _compute_fwhm(spectrum, regions=None): """ This is a helper function for the above `fwhm()` method. """ if regions is not None: calc_spectrum = extract_region(spectrum, regions) else: calc_spectrum = spectrum flux = calc_spectrum.flux spectral_axis = calc_spectrum.spectral_axis if flux.ndim > 1: return [_compute_single_fwhm(x, spectral_axis) for x in flux] else: return _compute_single_fwhm(flux, spectral_axis) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/conftest.py0000644000076500000240000000361600000000000017671 0ustar00erikstaff00000000000000# 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 importlib.util import find_spec from astropy.version import version as astropy_version from astropy.tests.plugins.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS from astropy.tests.helper import enable_deprecations_as_exceptions ## Uncomment the following line to treat all DeprecationWarnings as ## exceptions enable_deprecations_as_exceptions() # Uncomment and customize the following lines to add/remove entries from # the list of packages for which version numbers are displayed when running # the tests. Making it pass for KeyError is essential in some cases when # the package uses other astropy affiliated packages. try: PYTEST_HEADER_MODULES['Astropy'] = 'astropy' PYTEST_HEADER_MODULES['gwcs'] = 'gwcs' del PYTEST_HEADER_MODULES['h5py'] del PYTEST_HEADER_MODULES['Pandas'] del PYTEST_HEADER_MODULES['Matplotlib'] except (NameError, KeyError): # NameError is needed to support Astropy < 1.0 pass # Use ASDF schema tester plugin if ASDF is installed if find_spec('asdf') is not None: PYTEST_HEADER_MODULES['Asdf'] = 'asdf' # Uncomment the following lines to display the version number of the # package rather than the version number of Astropy in the top line when # running the tests. import os # This is to figure out the affiliated package version, rather than # using Astropy's try: from .version import version except ImportError: version = 'dev' try: packagename = os.path.basename(os.path.dirname(__file__)) TESTED_VERSIONS[packagename] = version except NameError: # Needed to support Astropy <= 1.0.0 pass # makes sure matplotlib doesn't try to pop up plots try: import matplotlib except ImportError: pass else: matplotlib.use('Agg') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/fitting/0000755000076500000240000000000000000000000017130 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/fitting/__init__.py0000644000076500000240000000006200000000000021237 0ustar00erikstaff00000000000000from .fitmodels import * from .continuum import * ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/fitting/continuum.py0000644000076500000240000000566100000000000021533 0ustar00erikstaff00000000000000from astropy.modeling.polynomial import Chebyshev1D from astropy.modeling.fitting import LevMarLSQFitter from ..fitting import fit_lines from ..manipulation.smoothing import median_smooth __all__ = ['fit_continuum', 'fit_generic_continuum'] def fit_generic_continuum(spectrum, median_window=3, model=Chebyshev1D(3), fitter=LevMarLSQFitter(), exclude_regions=None, weights=None): """ Basic fitting of the continuum of an input spectrum. The input spectrum is smoothed using a median filter to remove the spikes. Parameters ---------- spectrum : Spectrum1D The spectrum object overwhich the equivalent width will be calculated. model : list of `~astropy.modeling.Model` The list of models that contain the initial guess. exclude_regions : list of 2-tuples List of regions to exclude in the fitting. Passed through to the fitmodels routine. weights : list (NOT IMPLEMENTED YET) List of weights to define importance of fitting regions. Returns ------- continuum_model Fitted continuum as a model of whatever class ``model`` provides. Notes ----- * Could add functionality to set the bounds in ``model`` if they are not set. * The models in the list of ``model`` are added together and passed as a compound model to the `~astropy.modeling.fitting.Fitter` class instance. """ # # Simple median smooth to remove spikes and peaks # spectrum_smoothed = median_smooth(spectrum, median_window) # # Return the fitted continuum # return fit_continuum(spectrum_smoothed, model, fitter, exclude_regions, weights) def fit_continuum(spectrum, model=Chebyshev1D(3), fitter=LevMarLSQFitter(), exclude_regions=None, window=None, weights=None): """ Entry point for fitting using the `~astropy.modeling.fitting` machinery. Parameters ---------- spectrum : Spectrum1D The spectrum object overwhich the equivalent width will be calculated. model: list of `~astropy.modeling.Model` The list of models that contain the initial guess. fitmodels_type: str String representation of fit method to use as defined by the dict fitmodels_types. window : tuple of wavelengths (NOT IMPLEMENTED YET) Start and end wavelengths used for fitting. weights : list (NOT IMPLEMENTED YET) List of weights to define importance of fitting regions. Returns ------- models : list of `~astropy.modeling.Model` The list of models that contain the fitted model parmeters. """ if window is not None or weights is not None: raise NotImplementedError('window and weights are not yet implemented') # # Fit the flux to the model. # continuum_spectrum = fit_lines(spectrum, model, fitter, exclude_regions, weights) return continuum_spectrum ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/fitting/fitmodels.py0000644000076500000240000007445700000000000021511 0ustar00erikstaff00000000000000import itertools import logging import operator import astropy.units as u import numpy as np from astropy.modeling import fitting, Model, models from astropy.table import QTable from scipy.signal import convolve import astropy.units as u from astropy.stats import sigma_clipped_stats from ..spectra.spectral_region import SpectralRegion from ..spectra.spectrum1d import Spectrum1D from ..utils import QuantityModel from ..analysis import fwhm, gaussian_sigma_width, centroid, warn_continuum_below_threshold from ..manipulation import extract_region, noise_region_uncertainty from ..manipulation.utils import excise_regions __all__ = ['find_lines_threshold', 'find_lines_derivative', 'fit_lines', 'estimate_line_parameters'] # Define the initial estimators. This are the default methods to use to # estimate astropy model parameters. This is based on only a small subset of # the astropy models but it was determined that this is a decent start as most # fitting will probably use one of these. # # Each method list must take a Spectrum1D object and should return a Quantity. _parameter_estimators = { 'Gaussian1D': { 'amplitude': lambda s: max(s.flux), 'mean': lambda s: centroid(s, region=None), 'stddev': lambda s: gaussian_sigma_width(s) }, 'Lorentz1D': { 'amplitude': lambda s: max(s.flux), 'x_0': lambda s: centroid(s, region=None), 'fwhm': lambda s: fwhm(s) }, 'Voigt1D': { 'x_0': lambda s: centroid(s, region=None), 'amplitude_L': lambda s: max(s.flux), 'fwhm_L': lambda s: fwhm(s) / np.sqrt(2), 'fwhm_G': lambda s: fwhm(s) / np.sqrt(2) } } def _set_parameter_estimators(model): """ Helper method used in method below. """ if model.__class__.__name__ in _parameter_estimators: model_pars = _parameter_estimators[model.__class__.__name__] for name in model.param_names: par = getattr(model, name) setattr(par, "estimator", model_pars[name]) return model def estimate_line_parameters(spectrum, model): """ The input ``model`` parameters will be estimated from the input ``spectrum``. The ``model`` can be specified with default parameters, for example ``Gaussian1D()``. Parameters ---------- spectrum : `~specutils.Spectrum1D` The spectrum object from which we will estimate the model parameters. model : `~astropy.modeling.Model` Model for which we want to estimate parameters from the spectrum. Returns ------- model : `~astropy.modeling.Model` Model with parameters estimated. """ model = _set_parameter_estimators(model) # Estimate the parameters based on the estimators already # attached to the model for name in model.param_names: par = getattr(model, name) try: estimator = getattr(par, "estimator") setattr(model, name, estimator(spectrum)) except AttributeError: raise Exception('No method to estimate parameter {}'.format(name)) return model def _consecutive(data, stepsize=1): return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) @warn_continuum_below_threshold(threshold=0.01) def find_lines_threshold(spectrum, noise_factor=1): """ Find the emission and absorption lines in a spectrum. The method here is based on deviations larger than the spectrum's uncertainty by the ``noise_factor``. This method only works with continuum-subtracted spectra and the uncertainty must be defined on the spectrum. To add the uncertainty, one could use `~specutils.manipulation.noise_region_uncertainty` to add the uncertainty. Parameters ---------- spectrum : `~specutils.Spectrum1D` The spectrum object in which the lines will be found. noise_factor : float ``noise_factor`` multiplied by the spectrum's``uncertainty``, used for thresholding. Returns ------- qtable: `~astropy.table.QTable` Table of emission and absorption lines. Line center (``line_center``), line type (``line_type``) and index of line center (``line_center_index``) are stored for each line. """ # Threshold based on noise estimate and factor. uncertainty = spectrum.uncertainty inds = np.where(np.abs(spectrum.flux) > (noise_factor*uncertainty.array) * spectrum.flux.unit)[0] pos_inds = inds[spectrum.flux.value[inds] > 0] line_inds_grouped = _consecutive(pos_inds, stepsize=1) if len(line_inds_grouped[0]) > 0: emission_inds = [inds[np.argmax(spectrum.flux.value[inds])] for inds in line_inds_grouped] else: emission_inds = [] # # Find the absorption lines # neg_inds = inds[spectrum.flux.value[inds] < 0] line_inds_grouped = _consecutive(neg_inds, stepsize=1) if len(line_inds_grouped[0]) > 0: absorption_inds = [inds[np.argmin(spectrum.flux.value[inds])] for inds in line_inds_grouped] else: absorption_inds = [] # # Create the QTable to return the lines # qtable = QTable() qtable['line_center'] = list( itertools.chain( *[spectrum.spectral_axis.value[emission_inds], spectrum.spectral_axis.value[absorption_inds]] )) * spectrum.spectral_axis.unit qtable['line_type'] = ['emission'] * len(emission_inds) + \ ['absorption'] * len(absorption_inds) qtable['line_center_index'] = list( itertools.chain( *[emission_inds, absorption_inds])) return qtable @warn_continuum_below_threshold(threshold=0.01) def find_lines_derivative(spectrum, flux_threshold=None): """ Find the emission and absorption lines in a spectrum. The method here is based on finding the zero crossings in the derivative of the spectrum. Parameters ---------- spectrum : Spectrum1D The spectrum object over which the equivalent width will be calculated. flux_threshold : float, `~astropy.units.Quantity` or None The threshold a pixel must be above to be considered part of a line. If a float, will assume the same units as ``spectrum.flux``. This threshold is above and beyond the derivative searching step. Default is None so no thresholding. The threshold is positive for emission lines and negative for absorption lines. Returns ------- qtable: `~astropy.table.QTable` Table of emission and absorption lines. Line center (``line_center``), line type (``line_type``) and index of line center (``line_center_index``) are stored for each line. """ # Take the derivative to find the zero crossings which correspond to # the peaks (positive or negative) kernel = [1, 0, -1] dY = convolve(spectrum.flux, kernel, 'valid') # Use sign flipping to determine direction of change S = np.sign(dY) ddS = convolve(S, kernel, 'valid') # Add units if needed. if flux_threshold is not None and isinstance(flux_threshold, (int, float)): flux_threshold = float(flux_threshold) * spectrum.flux.unit # # Emmision lines # # Find all the indices that appear to be part of a +ve peak candidates = np.where(dY > 0)[0] + (len(kernel) - 1) line_inds = sorted(set(candidates).intersection(np.where(ddS == -2)[0] + 1)) if flux_threshold is not None: line_inds = np.array(line_inds)[spectrum.flux[line_inds] > flux_threshold] # Now group them and find the max highest point. line_inds_grouped = _consecutive(line_inds, stepsize=1) if len(line_inds_grouped[0]) > 0: emission_inds = [inds[np.argmax(spectrum.flux[inds])] for inds in line_inds_grouped] else: emission_inds = [] # # Absorption lines # # Find all the indices that appear to be part of a -ve peak candidates = np.where(dY < 0)[0] + (len(kernel) - 1) line_inds = sorted(set(candidates).intersection(np.where(ddS == 2)[0] + 1)) if flux_threshold is not None: line_inds = np.array(line_inds)[spectrum.flux[line_inds] < -flux_threshold] # Now group them and find the max highest point. line_inds_grouped = _consecutive(line_inds, stepsize=1) if len(line_inds_grouped[0]) > 0: absorption_inds = [inds[np.argmin(spectrum.flux[inds])] for inds in line_inds_grouped] else: absorption_inds = [] # # Create the QTable to return the lines # qtable = QTable() qtable['line_center'] = list( itertools.chain( *[spectrum.spectral_axis.value[emission_inds], spectrum.spectral_axis.value[absorption_inds]] )) * spectrum.spectral_axis.unit qtable['line_type'] = ['emission'] * len(emission_inds) + \ ['absorption'] * len(absorption_inds) qtable['line_center_index'] = list( itertools.chain( *[emission_inds, absorption_inds])) return qtable def fit_lines(spectrum, model, fitter=fitting.LevMarLSQFitter(), exclude_regions=None, weights=None, window=None, **kwargs): """ Fit the input models to the spectrum. The parameter values of the input models will be used as the initial conditions for the fit. Parameters ---------- spectrum : Spectrum1D The spectrum object over which the equivalent width will be calculated. model: `~astropy.modeling.Model` or list of `~astropy.modeling.Model` The model or list of models that contain the initial guess. fitter : `~astropy.modeling.fitting.Fitter`, optional Fitter instance to be used when fitting model to spectrum. exclude_regions : list of `~specutils.SpectralRegion` List of regions to exclude in the fitting. weights : array-like or 'unc', optional If 'unc', the unceratinties from the spectrum object are used to to calculate the weights. If array-like, represents the weights to use in the fitting. Note that if a mask is present on the spectrum, it will be applied to the ``weights`` as it would be to the spectrum itself. window : `~specutils.SpectralRegion` or list of `~specutils.SpectralRegion` Regions of the spectrum to use in the fitting. If None, then the whole spectrum will be used in the fitting. Additional keyword arguments are passed directly into the call to the ``fitter``. Returns ------- models : Compound model of `~astropy.modeling.Model` A compound model of models with fitted parameters. Notes ----- * Could add functionality to set the bounds in ``model`` if they are not set. * The models in the list of ``model`` are added together and passed as a compound model to the `~astropy.modeling.fitting.Fitter` class instance. """ # # If we are to exclude certain regions, then remove them. # if exclude_regions is not None: spectrum = excise_regions(spectrum, exclude_regions) # # Make the model a list if not already # single_model_in = not isinstance(model, list) if single_model_in: model = [model] # # If a single model is passed in then just do that. # fitted_models = [] for modeli, model_guess in enumerate(model): # # Determine the window if it is not None. There # are several options here: # window = 4 * u.Angstrom -> Quantity # window = (4*u.Angstrom, 6*u.Angstrom) -> tuple # window = (4, 6)*u.Angstrom -> Quantity # # # Determine the window if there is one # if window is not None and isinstance(window, list): model_window = window[modeli] elif window is not None: model_window = window else: model_window = None # # Check to see if the model has units. If it does not # have units then we are going to ignore them. # ignore_units = getattr(model_guess, model_guess.param_names[0]).unit is None fit_model = _fit_lines(spectrum, model_guess, fitter, exclude_regions, weights, model_window, ignore_units, **kwargs) if model_guess.name is not None: fit_model.name = model_guess.name fitted_models.append(fit_model) if single_model_in: fitted_models = fitted_models[0] return fitted_models def _fit_lines(spectrum, model, fitter=fitting.LevMarLSQFitter(), exclude_regions=None, weights=None, window=None, ignore_units=False, **kwargs): """ Fit the input model (initial conditions) to the spectrum. Output will be the same model with the parameters set based on the fitting. spectrum, model -> model """ # # If we are to exclude certain regions, then remove them. # if exclude_regions is not None: spectrum = excise_regions(spectrum, exclude_regions) if isinstance(weights, str): if weights == 'unc': uncerts = spectrum.uncertainty if uncerts is not None: weights = uncerts.array ** -2 else: logging.warning("Uncertainty values are not defined, but are " "trying to be used in model fitting.") else: raise ValueError("Unrecognized value `%s` in keyword argument.", weights) elif weights is not None: # Assume that the weights argument is list-like weights = np.array(weights) mask = spectrum.mask dispersion = spectrum.spectral_axis dispersion_unit = spectrum.spectral_axis.unit flux = spectrum.flux flux_unit = spectrum.flux.unit # # Determine the window if it is not None. There # are several options here: # window = 4 * u.Angstrom -> Quantity # window = (4*u.Angstrom, 6*u.Angstrom) -> tuple # window = (4, 6)*u.Angstrom -> Quantity # # # Determine the window if there is one # # In this case the window defines the area around the center of each model window_indices = None if window is not None and isinstance(window, (float, int)): center = model.mean window_indices = np.nonzero((spectrum.spectral_axis >= center-window) & (spectrum.spectral_axis < center+window)) # In this case the window is the start and end points of where we # should fit elif window is not None and isinstance(window, tuple): window_indices = np.nonzero((dispersion >= window[0]) & (dispersion < window[1])) # in this case the window is spectral regions that determine where # to fit. elif window is not None and isinstance(window, SpectralRegion): idx1, idx2 = window.bounds if idx1 == idx2: raise IndexError("Tried to fit a region containing no pixels.") # HACK WARNING! This uses the extract machinery to create a set of # indices by making an "index spectrum" # note that any unit will do but Jy is at least flux-y # TODO: really the spectral region machinery should have the power # to create a mask, and we'd just use that... idxarr = np.arange(spectrum.flux.size).reshape(spectrum.flux.shape) index_spectrum = Spectrum1D(spectral_axis=spectrum.spectral_axis, flux=u.Quantity(idxarr, u.Jy, dtype=int)) extracted_regions = extract_region(index_spectrum, window) if isinstance(extracted_regions, list): if len(extracted_regions) == 0: raise ValueError('The whole spectrum is windowed out!') window_indices = np.concatenate([s.flux.value.astype(int) for s in extracted_regions]) else: if len(extracted_regions.flux) == 0: raise ValueError('The whole spectrum is windowed out!') window_indices = extracted_regions.flux.value.astype(int) if window_indices is not None: dispersion = dispersion[window_indices] flux = flux[window_indices] if mask is not None: mask = mask[window_indices] if weights is not None: weights = weights[window_indices] if flux is None or len(flux) == 0: raise Exception("Spectrum flux is empty or None.") input_spectrum = spectrum spectrum = Spectrum1D( flux=flux.value * flux_unit, spectral_axis=dispersion.value * dispersion_unit, wcs=input_spectrum.wcs, velocity_convention=input_spectrum.velocity_convention, rest_value=input_spectrum.rest_value) # # Compound models with units can not be fit. # # Convert the model initial guess to the spectral # units and then remove the units # model_unitless, dispersion_unitless, flux_unitless = \ _strip_units_from_model(model, spectrum, convert=not ignore_units) # # Do the fitting of spectrum to the model. # if mask is not None: nmask = ~mask dispersion_unitless = dispersion_unitless[nmask] flux_unitless = flux_unitless[nmask] if weights is not None: weights = weights[nmask] fit_model_unitless = fitter(model_unitless, dispersion_unitless, flux_unitless, weights=weights, **kwargs) # # Now add the units back onto the model.... # if not ignore_units: fit_model = _add_units_to_model(fit_model_unitless, model, spectrum) else: fit_model = QuantityModel(fit_model_unitless, spectrum.spectral_axis.unit, spectrum.flux.unit) return fit_model def _convert(quantity, dispersion_unit, dispersion, flux_unit): """ Convert the quantity to the spectrum's units, and then we will use the *value* of it in the new unitless-model. """ with u.set_enabled_equivalencies(u.spectral()): if quantity.unit.is_equivalent(dispersion_unit): quantity = quantity.to(dispersion_unit) with u.set_enabled_equivalencies(u.spectral_density(dispersion)): if quantity.unit.is_equivalent(flux_unit): quantity = quantity.to(flux_unit) return quantity def _convert_and_dequantify(poss_quantity, dispersion_unit, dispersion, flux_unit, convert=True): """ This method will convert the ``poss_quantity`` value to the proper dispersion or flux units and then strip the units. If the ``poss_quantity`` is None, or a number, we just return that. Notes ----- This method can be removed along with most of the others here when astropy.fitting will fit models that contain units. """ if poss_quantity is None or isinstance(poss_quantity, (float, int)): return poss_quantity if convert and hasattr(poss_quantity, 'quantity') and poss_quantity.quantity is not None: q = poss_quantity.quantity quantity = _convert(q, dispersion_unit, dispersion, flux_unit) v = quantity.value elif convert and isinstance(poss_quantity, u.Quantity): quantity = _convert(poss_quantity, dispersion_unit, dispersion, flux_unit) v = quantity.value else: v = poss_quantity.value return v def _strip_units_from_model(model_in, spectrum, convert=True): """ This method strips the units from the model, so the result can be passed to the fitting routine. This is necessary as CoumpoundModel with units does not work in the fitters. Notes ----- When CompoundModel with units works in the fitters this method can be removed. This assumes there are two types of models, those that are based on `~astropy.modeling.models.PolynomialModel` and therefore require the ``degree`` parameter when instantiating the class, and "everything else" that does not require an "extra" parameter for class instantiation. If convert is False, then we will *not* do the conversion of units to the units of the Spectrum1D object. Otherwise we will convert. """ # # Get the dispersion and flux information from the spectrum # dispersion = spectrum.spectral_axis dispersion_unit = spectrum.spectral_axis.unit flux = spectrum.flux flux_unit = spectrum.flux.unit # # Determine if a compound model # compound_model = model_in.n_submodels > 1 if not compound_model: # For this we are going to just make it a list so that we # can use the looping structure below. model_in = [model_in] else: # If it is a compound model then we are going to create the RPN # representation of it which is a list that contains either astropy # models or string representations of operators (e.g., '+' or '*'). model_in = model_in.traverse_postorder(include_operator=True) # # Run through each model in the list or compound model # model_out_stack = [] for sub_model in model_in: # # If it is an operator put onto the stack and move on... # if not isinstance(sub_model, Model): model_out_stack.append(sub_model) continue # # Make a new instance of the class. # if isinstance(sub_model, models.PolynomialModel): new_sub_model = sub_model.__class__(sub_model.degree, name=sub_model.name) else: new_sub_model = sub_model.__class__(name=sub_model.name) # Now for each parameter in the model determine if a dispersion or # flux type of unit, then convert to spectrum units and then # get the value. for pn in new_sub_model.param_names: # This could be a Quantity or Parameter v = _convert_and_dequantify(getattr(sub_model, pn), dispersion_unit, dispersion, flux_unit, convert=convert) # # Add this information for the parameter name into the # new sub model. # setattr(new_sub_model, pn, v) # # Copy over all the constraints (e.g., tied, fixed...) # for constraint in ('tied', 'fixed'): for k, v in getattr(sub_model, constraint).items(): getattr(new_sub_model, constraint)[k] = v # # Convert teh bounds parameter # new_bounds = [] for a in sub_model.bounds[pn]: v = _convert_and_dequantify(a, dispersion_unit, dispersion, flux_unit, convert=convert) new_bounds.append(v) new_sub_model.bounds[pn] = tuple(new_bounds) # The new model now has unitless information in it but has been # converted to spectral unit scale. model_out_stack.append(new_sub_model) # If a compound model we need to re-create it, otherwise # it is a single model and we just get the first one (as # there is only one). if compound_model: model_out = _combine_postfix(model_out_stack) else: model_out = model_out_stack[0] return model_out, dispersion.value, flux.value def _add_units_to_model(model_in, model_orig, spectrum): """ This method adds the units to the model based on the units of the model passed in. This is necessary as CoumpoundModel with units does not work in the fitters. Notes ----- When CompoundModel with units works in the fitters this method can be removed. This assumes there are two types of models, those that are based on `~astropy.modeling.models.PolynomialModel` and therefore require the ``degree`` parameter when instantiating the class, and "everything else" that does not require an "extra" parameter for class instantiation. """ dispersion = spectrum.spectral_axis # # If not a compound model, then make a single element # list so we can use the for loop below. # compound_model = model_in.n_submodels > 1 if not compound_model: model_in_list = [model_in] model_orig_list = [model_orig] else: compound_model_in = model_in model_in_list = model_in.traverse_postorder(include_operator=True) model_orig_list = model_orig.traverse_postorder(include_operator=True) model_out_stack = [] model_index = 0 # # For each model in the list we will convert the values back to # the original (sub-)model units. # for ii, m_in in enumerate(model_in_list): # # If an operator (ie not Model) then we'll just add # to the stack and evaluate at the end. # if not isinstance(m_in, Model): model_out_stack.append(m_in) continue # # Get the corresponding *original* sub-model that # will match the current sub-model. From this we will # grab the units to apply. # m_orig = model_orig_list[ii] # # Make the new sub-model. # if isinstance(m_in, models.PolynomialModel): new_sub_model = m_in.__class__(m_in.degree, name=m_in.name) else: new_sub_model = m_in.__class__(name=m_in.name) # # Convert the model values from the spectrum units back to the # original model units. # for pi, pn in enumerate(new_sub_model.param_names): # # Get the parameter from the original model and unit-less model. # m_orig_param = getattr(m_orig, pn) m_in_param = getattr(m_in, pn) if hasattr(m_orig_param, 'quantity') and m_orig_param.quantity is not None: m_orig_param_quantity = m_orig_param.quantity # # If a spectral dispersion type of unit... # if m_orig_param_quantity.unit.is_equivalent(spectrum.spectral_axis.unit, equivalencies=u.equivalencies.spectral()): # If it is a compound model, then we need to get the value # from the actual compound model as the tree is not # updated in the fitting if compound_model: current_value = getattr(compound_model_in, '{}_{}'.format(pn, model_index)).value *\ spectrum.spectral_axis.unit else: current_value = m_in_param.value * spectrum.spectral_axis.unit v = current_value.to(m_orig_param_quantity.unit, equivalencies=u.equivalencies.spectral()) # # If a spectral density type of unit... # elif m_orig_param_quantity.unit.is_equivalent(spectrum.flux.unit, equivalencies=u.equivalencies.spectral_density(dispersion)): # If it is a compound model, then we need to get the value # from the actual compound model as the tree is not # updated in the fitting if compound_model: current_value = getattr(compound_model_in, '{}_{}'.format(pn, model_index)).value *\ spectrum.flux.unit else: current_value = m_in_param.value * spectrum.flux.unit v = current_value.to(m_orig_param_quantity.unit, equivalencies=u.equivalencies.spectral_density(dispersion)) else: raise ValueError( "The parameter '{}' with unit '{}' is not convertible " "to either the current flux unit '{}' or spectral " "axis unit '{}'.".format( m_orig_param.name, m_orig_param.unit, spectrum.flux.unit, spectrum.spectral_axis.unit)) else: v = getattr(m_in, pn).value # # Set the parameter value into the new sub-model. # setattr(new_sub_model, pn, v) # # Copy over all the constraints (e.g., tied, fixed, bounds...) # for constraint in ('tied', 'bounds', 'fixed'): for k, v in getattr(m_orig, constraint).items(): getattr(new_sub_model, constraint)[k] = v # # Add the new unit-filled model onto the stack. # model_out_stack.append(new_sub_model) model_index += 1 # # Create the output model which is either the evaulation # of the RPN representation of the model (if a compound model) # or just the first element if a non-compound model. # if compound_model: model_out = _combine_postfix(model_out_stack) else: model_out = model_out_stack[0] # If the first parameter is not a Quantity, then at this point we will # assume none of them are. (It would be inconsistent for fitting to have # a model that has some parameters as Quantities and some values). if getattr(model_orig, model_orig.param_names[0]).unit is None: model_out = QuantityModel(model_out, spectrum.spectral_axis.unit, spectrum.flux.unit) return model_out def _combine_postfix(equation): """ Given a Python list in post order (RPN) of an equation, convert/apply the operations to evaluate. The list order is the same as what is output from ``model._tree.traverse_postorder()``. Structure modified from https://codereview.stackexchange.com/questions/79795/reverse-polish-notation-calculator-in-python """ ops = {'+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv, '^': operator.pow, '**': operator.pow} stack = [] result = 0 for i in equation: if isinstance(i, Model): stack.insert(0, i) else: if len(stack) < 2: print('Error: insufficient values in expression') break else: n1 = stack.pop(1) n2 = stack.pop(0) result = ops[i](n1, n2) stack.insert(0, result) return result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/io/0000755000076500000240000000000000000000000016073 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1565986934.0 specutils-0.7/specutils/io/__init__.py0000644000076500000240000000004100000000000020177 0ustar00erikstaff00000000000000from .registers import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/io/asdf/0000755000076500000240000000000000000000000017010 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/asdf/__init__.py0000644000076500000240000000131300000000000021117 0ustar00erikstaff00000000000000""" The **asdf** submodule contains code that is used to serialize specutils types so that they can be represented and stored using the Advanced Scientific Data Format (ASDF). If both **asdf** and **specutils** are installed, no further configuration is required in order to process ASDF files that contain **specutils** types. The **asdf** package has been designed to automatically detect the presence of the tags defined by **specutils**. Documentation on the ASDF Standard can be found `here `__. Documentation on the ASDF Python module can be found `here `__. Additional details for specutils developers can be found in :ref:`asdf_dev`. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/asdf/extension.py0000644000076500000240000000245100000000000021400 0ustar00erikstaff00000000000000""" Defines extension that is used by ASDF for recognizing specutils types """ import os import urllib from asdf.util import filepath_to_url from asdf.extension import AsdfExtension from astropy.io.misc.asdf.extension import ASTROPY_SCHEMA_URI_BASE from .tags.spectra import * from .types import _specutils_types SCHEMA_PATH = os.path.abspath( os.path.join(os.path.dirname(__file__), 'schemas')) SPECUTILS_URL_MAPPING = [ (urllib.parse.urljoin(ASTROPY_SCHEMA_URI_BASE, 'specutils/'), filepath_to_url( os.path.join(SCHEMA_PATH, 'astropy.org', 'specutils')) + '/{url_suffix}.yaml')] class SpecutilsExtension(AsdfExtension): """ Defines specutils types and schema locations to be used by ASDF """ @property def types(self): """ Collection of tag types that are used by ASDF for serialization """ return _specutils_types @property def tag_mapping(self): """ Defines mapping of specutils tag URIs to URLs """ return [('tag:astropy.org:specutils', ASTROPY_SCHEMA_URI_BASE + 'specutils{tag_suffix}')] @property def url_mapping(self): """ Defines mapping of specutils schema URLs into real locations on disk """ return SPECUTILS_URL_MAPPING ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/io/asdf/tags/0000755000076500000240000000000000000000000017746 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/asdf/tags/__init__.py0000644000076500000240000000000000000000000022045 0ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/asdf/tags/spectra.py0000644000076500000240000000703500000000000021766 0ustar00erikstaff00000000000000""" Contains classes that serialize spectral data types into ASDF representations. """ import astropy.nddata from asdf.yamlutil import custom_tree_to_tagged_tree, tagged_tree_to_custom_tree from ..types import SpecutilsType from ....spectra import Spectrum1D, SpectrumList __all__ = ['Spectrum1DType', 'SpectrumListType'] UNCERTAINTY_TYPE_MAPPING = { 'std': astropy.nddata.StdDevUncertainty, 'var': astropy.nddata.VarianceUncertainty, 'ivar': astropy.nddata.InverseVariance, 'unknown': astropy.nddata.UnknownUncertainty, } class Spectrum1DType(SpecutilsType): """ ASDF tag implementation used to serialize/deserialize Spectrum1D objects """ name = 'spectra/spectrum1d' types = [Spectrum1D] version = '1.0.0' @classmethod def to_tree(cls, obj, ctx): """ Converts Spectrum1D object into tree used for YAML representation """ node = {} node['flux'] = custom_tree_to_tagged_tree(obj.flux, ctx) node['spectral_axis'] = custom_tree_to_tagged_tree(obj.spectral_axis, ctx) if obj.uncertainty is not None: node['uncertainty'] = {} node['uncertainty']['uncertainty_type'] = obj.uncertainty.uncertainty_type data = custom_tree_to_tagged_tree(obj.uncertainty.array, ctx) node['uncertainty']['data'] = data return node @classmethod def from_tree(cls, tree, ctx): """ Converts tree representation back into Spectrum1D object """ flux = tagged_tree_to_custom_tree(tree['flux'], ctx) spectral_axis = tagged_tree_to_custom_tree(tree['spectral_axis'], ctx) uncertainty = tree.get('uncertainty', None) if uncertainty is not None: klass = UNCERTAINTY_TYPE_MAPPING[uncertainty['uncertainty_type']] data = tagged_tree_to_custom_tree(uncertainty['data'], ctx) uncertainty = klass(data) return Spectrum1D(flux=flux, spectral_axis=spectral_axis, uncertainty=uncertainty) @classmethod def assert_equal(cls, old, new): """ Equality test used in ASDF unit tests """ from numpy.testing import assert_allclose from astropy.tests.helper import quantity_allclose assert quantity_allclose(old.flux, new.flux) assert quantity_allclose(old.spectral_axis, new.spectral_axis) if old.uncertainty is None: assert new.uncertainty is None else: assert old.uncertainty.uncertainty_type == new.uncertainty.uncertainty_type assert_allclose(old.uncertainty.array, new.uncertainty.array) class SpectrumListType(SpecutilsType): """ ASDF tag implementation used to serialize/deserialize SpectrumList objects """ name = 'spectra/spectrum_list' types = [SpectrumList] version = '1.0.0' @classmethod def to_tree(cls, obj, ctx): """ Converts SpectrumList object into tree used for YAML representation """ return [custom_tree_to_tagged_tree(spectrum, ctx) for spectrum in obj] @classmethod def from_tree(cls, tree, ctx): """ Converts tree representation back into SpectrumList object """ spectra = [tagged_tree_to_custom_tree(node, ctx) for node in tree] return SpectrumList(spectra) @classmethod def assert_equal(cls, old, new): """ Equality test used in ASDF unit tests """ assert len(old) == len(new) for x, y in zip(old, new): Spectrum1DType.assert_equal(x, y) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/io/asdf/tags/tests/0000755000076500000240000000000000000000000021110 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/asdf/tags/tests/__init__.py0000644000076500000240000000000000000000000023207 0ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/asdf/tags/tests/test_spectra.py0000644000076500000240000000274300000000000024170 0ustar00erikstaff00000000000000import pytest # Make sure these tests do not run if ASDF is not installed pytest.importorskip('asdf') import numpy as np import astropy.units as u from astropy.coordinates import FK5 from asdf.tests.helpers import assert_roundtrip_tree import asdf from specutils import Spectrum1D, SpectrumList def create_spectrum1d(xmin, xmax, uncertainty=None): flux = np.ones(xmax-xmin) * u.Jy wavelength = np.arange(xmin, xmax) * u.AA uncertainty = np.ones(xmax-xmin) if uncertainty is not None else None return Spectrum1D(spectral_axis=wavelength, flux=flux, uncertainty=uncertainty) def test_asdf_spectrum1d(tmpdir): spectrum = create_spectrum1d(5100, 5300) tree = dict(spectrum=spectrum) assert_roundtrip_tree(tree, tmpdir) def test_asdf_spectrum1d_uncertainty(tmpdir): spectrum = create_spectrum1d(5100, 5300, uncertainty=True) tree = dict(spectrum=spectrum) assert_roundtrip_tree(tree, tmpdir) def test_asdf_spectrumlist(tmpdir): spectra = SpectrumList([ create_spectrum1d(5100, 5300), create_spectrum1d(5000, 5500), create_spectrum1d(0, 100), create_spectrum1d(1, 5) ]) tree = dict(spectra=spectra) assert_roundtrip_tree(tree, tmpdir) @pytest.mark.filterwarnings("error::UserWarning") def test_asdf_url_mapper(): """Make sure specutils asdf extension url_mapping doesn't interfere with astropy schemas""" frame = FK5() af = asdf.AsdfFile() af.tree = {'frame': frame} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/asdf/types.py0000644000076500000240000000136000000000000020526 0ustar00erikstaff00000000000000from asdf.types import CustomType, ExtensionTypeMeta _specutils_types = set() class SpecutilsTypeMeta(ExtensionTypeMeta): """ Keeps track of `SpecutilsType` subclasses that are created so that they can be stored automatically by specutils extensions for ASDF. """ def __new__(mcls, name, bases, attrs): cls = super().__new__(mcls, name, bases, attrs) # Classes using this metaclass are automatically added to the list of # specutils extensions _specutils_types.add(cls) return cls class SpecutilsType(CustomType, metaclass=SpecutilsTypeMeta): """ Parent class of all specutils tag implementations used by ASDF """ organization = 'astropy.org' standard = 'specutils' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/io/default_loaders/0000755000076500000240000000000000000000000021230 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/specutils/io/default_loaders/__init__.py0000644000076500000240000000027500000000000023345 0ustar00erikstaff00000000000000import os import glob from os.path import dirname, basename, isfile modules = glob.glob(os.path.join(dirname(__file__), "*.py")) __all__ = [basename(f)[:-3] for f in modules if isfile(f)] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/apogee.py0000644000076500000240000001336300000000000023050 0ustar00erikstaff00000000000000""" Loader for APOGEE spectrum files: apVisit_, apStar_, aspcapStar_ files. .. _apVisit: https://data.sdss.org/datamodel/files/APOGEE_REDUX/APRED_VERS/TELESCOPE/PLATE_ID/MJD5/apVisit.html .. _apStar: https://data.sdss.org/datamodel/files/APOGEE_REDUX/APRED_VERS/APSTAR_VERS/TELESCOPE/LOCATION_ID/apStar.html .. _aspcapStar: https://data.sdss.org/datamodel/files/APOGEE_REDUX/APRED_VERS/APSTAR_VERS/ASPCAP_VERS/RESULTS_VERS/LOCATION_ID/aspcapStar.html """ import os from astropy.io import fits from astropy.table import Table from astropy.wcs import WCS from astropy.units import Unit, def_unit from astropy.nddata import StdDevUncertainty import numpy as np from specutils.io.registers import data_loader, custom_writer from specutils import Spectrum1D __all__ = ['apVisit_identify', 'apStar_identify', 'aspcapStar_identify', 'apVisit_loader', 'apStar_loader', 'aspcapStar_loader'] def apVisit_identify(origin, *args, **kwargs): """ Check whether given filename is FITS. This is used for Astropy I/O Registry. """ return (isinstance(args[0], str) and args[0].lower().split('.')[-1] == 'fits' and args[0].startswith('apVisit')) def apStar_identify(origin, *args, **kwargs): """ Check whether given filename is FITS. This is used for Astropy I/O Registry. """ return (isinstance(args[0], str) and args[0].lower().split('.')[-1] == 'fits' and args[0].startswith('apStar')) def aspcapStar_identify(origin, *args, **kwargs): """ Check whether given filename is FITS. This is used for Astropy I/O Registry. """ return (isinstance(args[0], str) and args[0].lower().split('.')[-1] == 'fits' and args[0].startswith('aspcapStar')) @data_loader(label="APOGEE apVisit", identifier=apVisit_identify, extensions=['fits']) def apVisit_loader(file_name, **kwargs): """ Loader for APOGEE apVisit files. Parameters ---------- file_name: str The path to the FITS file Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ name = os.path.basename(file_name.rstrip(os.sep)).rsplit('.', 1)[0] hdulist = fits.open(file_name, **kwargs) header = hdulist[0].header meta = {'header': header} # spectrum is stored in three rows (for three chips) data = np.concatenate([hdulist[1].data[0, :], hdulist[1].data[1, :], hdulist[1].data[2, :]]) unit = Unit('1e-17 erg / (Angstrom cm2 s)') stdev = np.concatenate([hdulist[2].data[0, :], hdulist[2].data[1, :], hdulist[2].data[2, :]]) uncertainty = StdDevUncertainty(stdev * unit) # Dispersion is not a simple function in these files. There's a # look-up table instead. dispersion = np.concatenate([hdulist[4].data[0, :], hdulist[4].data[1, :], hdulist[4].data[2, :]]) dispersion_unit = Unit('Angstrom') hdulist.close() return Spectrum1D(data=data * unit, uncertainty=uncertainty, spectral_axis=dispersion * dispersion_unit, meta=meta) @data_loader(label="APOGEE apStar", identifier=apStar_identify, extensions=['fits']) def apStar_loader(file_name, **kwargs): """ Loader for APOGEE apStar files. Parameters ---------- file_name: str The path to the FITS file Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ name = os.path.basename(file_name.rstrip(os.sep)).rsplit('.', 1)[0] hdulist = fits.open(file_name, **kwargs) header = hdulist[0].header meta = {'header': header} wcs = WCS(hdulist[1].header) data = hdulist[1].data[0, :] # spectrum in the first row of the first extension unit = Unit('1e-17 erg / (Angstrom cm2 s)') uncertainty = StdDevUncertainty(hdulist[2].data[0, :]) # dispersion from the WCS but convert out of logspace # dispersion = 10**wcs.all_pix2world(np.arange(data.shape[0]), 0)[0] dispersion = 10**wcs.all_pix2world(np.vstack((np.arange(data.shape[0]), np.zeros((data.shape[0],)))).T, 0)[:, 0] dispersion_unit = Unit('Angstrom') hdulist.close() return Spectrum1D(data=data * unit, uncertainty=uncertainty, spectral_axis=dispersion * dispersion_unit, meta=meta, wcs=wcs) @data_loader(label="APOGEE aspcapStar", identifier=aspcapStar_identify, extensions=['fits']) def aspcapStar_loader(file_name, **kwargs): """ Loader for APOGEE aspcapStar files. Parameters ---------- file_name: str The path to the FITS file Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ name = os.path.basename(file_name.rstrip(os.sep)).rsplit('.', 1)[0] hdulist = fits.open(file_name, **kwargs) header = hdulist[0].header meta = {'header': header} wcs = WCS(hdulist[1].header) data = hdulist[1].data # spectrum in the first extension unit = def_unit('arbitrary units') uncertainty = StdDevUncertainty(hdulist[2].data) # dispersion from the WCS but convert out of logspace dispersion = 10**wcs.all_pix2world(np.arange(data.shape[0]), 0)[0] dispersion_unit = Unit('Angstrom') hdulist.close() return Spectrum1D(data=data * unit, uncertainty=uncertainty, spectral_axis=dispersion * dispersion_unit, meta=meta, wcs=wcs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/ascii.py0000644000076500000240000000627700000000000022706 0ustar00erikstaff00000000000000import os import astropy.units as u from astropy.nddata import StdDevUncertainty from astropy.table import Table from ... import Spectrum1D from ..registers import data_loader from ..parsing_utils import (generic_spectrum_from_table, spectrum_from_column_mapping) __all__ = ['ascii_identify', 'ascii_loader', 'ipac_identify', 'ipac_loader'] def ascii_identify(origin, *args, **kwargs): """Check if it's an ASCII file.""" name = os.path.basename(args[0]) if name.lower().split('.')[-1] in ['txt', 'ascii']: return True return False @data_loader(label="ASCII", identifier=ascii_identify, extensions=['txt', 'ascii']) def ascii_loader(file_name, column_mapping=None, **kwargs): """ Load spectrum from ASCII file. Parameters ---------- file_name: str The path to the ASCII file. column_mapping : dict A dictionary describing the relation between the ASCII file columns and the arguments of the `Spectrum1D` class, along with unit information. The dictionary keys should be the ASCII file column names while the values should be a two-tuple where the first element is the associated `Spectrum1D` keyword argument, and the second element is the unit for the ASCII file column:: column_mapping = {'FLUX': ('flux': 'Jy')} Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ tab = Table.read(file_name, format='ascii') # If no column mapping is given, attempt to parse the ascii files using # unit information if column_mapping is None: return generic_spectrum_from_table(tab, **kwargs) return spectrum_from_column_mapping(tab, column_mapping) def ipac_identify(*args, **kwargs): """Check if it's an IPAC-style ASCII file.""" name = os.path.basename(args[0]) if name.lower().split('.')[-1] in ['txt', 'dat']: return True return False @data_loader(label="IPAC", identifier=ipac_identify, extensions=['txt', 'dat']) def ipac_loader(file_name, column_mapping=None, **kwargs): """ Load spectrum from IPAC-style ASCII file Parameters ---------- file_name: str The path to the IPAC-style ASCII file. column_mapping : dict A dictionary describing the relation between the IPAC-style ASCII file columns and the arguments of the `Spectrum1D` class, along with unit information. The dictionary keys should be the IPAC-style ASCII file column names while the values should be a two-tuple where the first element is the associated `Spectrum1D` keyword argument, and the second element is the unit for the IPAC-style ASCII file column:: column_mapping = {'FLUX': ('flux', 'Jy')} Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ tab = Table.read(file_name, format='ascii.ipac') # If no column mapping is given, attempt to parse the ascii files using # unit information if column_mapping is None: return generic_spectrum_from_table(tab, **kwargs) return spectrum_from_column_mapping(tab, column_mapping) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/generic_cube.py0000644000076500000240000000540400000000000024217 0ustar00erikstaff00000000000000# ~/.specutils/my_custom_loader.py # # Load a FITS cube , extract the spectrum at the reference pixel, but this # can be optionally overriden. # # 21-apr-2016 Peter Teuben hackday at "SPECTROSCOPY TOOLS IN PYTHON WORKSHOP" STSCI import logging # import os import numpy as np from astropy.io import fits from astropy.units import Unit from astropy.wcs import WCS from ..registers import data_loader from ...spectra import Spectrum1D # Define an optional identifier. If made specific enough, this circumvents the # need to add `format="my-format"` in the `Spectrum1D.read` call. def identify_generic_fits(origin, *args, **kwargs): return (isinstance(args[0], str) and os.path.splitext(args[0].lower())[1] == '.fits' and fits.getheader(args[0])['NAXIS'] == 3) # not yet ready because it's not generic enough and does not use column_mapping # @data_loader("Cube", identifier=identify_generic_fits, extensions=['fits']) def generic_fits(file_name, **kwargs): name = os.path.basename(file_name.rstrip(os.sep)).rsplit('.', 1)[0] with fits.open(file_name, **kwargs) as hdulist: header = hdulist[0].header data3 = hdulist[0].data wcs = WCS(header) shape = data3.shape # take the reference pixel if the pos= was not supplied by the reader if 'pos' in kwargs: ix = kwargs['pos'][0] iy = kwargs['pos'][1] else: ix = int(wcs.wcs.crpix[0]) iy = int(wcs.wcs.crpix[1]) # grab a spectrum from the cube if len(shape) == 3: data = data3[:,iy,ix] elif len(shape) == 4: data = data3[:,:,iy,ix].squeeze() # make sure this is a 1D array # if len(data.shape) != 1: # raise Exception,"not a true cube" else: logging.error("Unexpected shape %s.", shape) # store some meta data meta = {'header': header} meta['xpos'] = ix meta['ypos'] = iy # attach units (get it from header['BUNIT'] - what about 'JY/BEAM ' # NOTE: astropy doesn't support beam, but see comments in radio_beam data = data * Unit("Jy") # now figure out the frequency axis.... sp_axis = 3 naxis3 = header['NAXIS%d' % sp_axis] cunit3 = wcs.wcs.cunit[sp_axis-1] crval3 = wcs.wcs.crval[sp_axis-1] cdelt3 = wcs.wcs.cdelt[sp_axis-1] crpix3 = wcs.wcs.crpix[sp_axis-1] freqs = np.arange(naxis3) + 1 freqs = (freqs - crpix3) * cdelt3 + crval3 freqs = freqs * cunit3 # should wcs be transformed to a 1D case ? return Spectrum1D(flux=data, wcs=wcs, meta=meta, spectral_axis=freqs) # return Spectrum1D(flux=data, wcs=wcs, meta=meta) # this does not work yet ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/generic_ecsv_reader.py0000644000076500000240000000345600000000000025570 0ustar00erikstaff00000000000000import os import logging from astropy.table import Table from ...spectra import Spectrum1D from ..registers import data_loader from ..parsing_utils import (generic_spectrum_from_table, spectrum_from_column_mapping) def identify_ecsv(origin, *args, **kwargs): """Check if it's an ECSV file.""" return (isinstance(args[0], str) and os.path.splitext(args[0].lower())[1] == '.ecsv') @data_loader("ECSV", identifier=identify_ecsv, dtype=Spectrum1D) def generic_ecsv(file_name, column_mapping=None, **kwargs): """ Read a spectrum from an ECSV file, using generic_spectrum_from_table_loader() to try to figure out which column is which. The ECSV columns must have units, as `generic_spectrum_from_table_loader` depends on this to determine the meaning of the columns. For manual control over the column to spectrum mapping, use the ASCII loader. Parameters ---------- file_name: str The path to the ECSV file. column_mapping : dict A dictionary describing the relation between the ECSV file columns and the arguments of the `Spectrum1D` class, along with unit information. The dictionary keys should be the ECSV file column names while the values should be a two-tuple where the first element is the associated `Spectrum1D` keyword argument, and the second element is the unit for the ECSV file column:: column_mapping = {'FLUX': ('flux': 'Jy')} Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ table = Table.read(file_name, format='ascii.ecsv') if column_mapping is None: return generic_spectrum_from_table(table, **kwargs) return spectrum_from_column_mapping(table, column_mapping) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/hst_cos.py0000644000076500000240000000325700000000000023253 0ustar00erikstaff00000000000000import os from astropy.io import fits from astropy.units import Unit from astropy.nddata import StdDevUncertainty from specutils.io.registers import data_loader from specutils import Spectrum1D __all__ = ['cos_identify', 'cos_spectrum_loader'] def cos_identify(origin, *args, **kwargs): """Check whether given file contains HST/COS spectral data.""" with fits.open(args[0]) as hdu: if hdu[0].header['TELESCOP'] == 'HST' and hdu[0].header['INSTRUME'] == 'COS': return True return False @data_loader(label="HST/COS", identifier=cos_identify, extensions=['FITS', 'FIT', 'fits', 'fit']) def cos_spectrum_loader(file_name, **kwargs): """ Load COS spectral data from the MAST archive into a spectrum object. Parameters ---------- file_name: str The path to the FITS file Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ name = os.path.basename(file_name) with fits.open(file_name, **kwargs) as hdu: header = hdu[0].header meta = {'header': header} unit = Unit("erg/cm**2 Angstrom s") disp_unit = Unit('Angstrom') data = hdu[1].data['FLUX'].flatten() * unit dispersion = hdu[1].data['wavelength'].flatten() * disp_unit uncertainty = StdDevUncertainty(hdu[1].data["ERROR"].flatten() * unit) sort_idx = dispersion.argsort() dispersion = dispersion[sort_idx] data = data[sort_idx] uncertainty = uncertainty[sort_idx] return Spectrum1D(flux=data, spectral_axis=dispersion, uncertainty=uncertainty, meta=meta) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/hst_stis.py0000644000076500000240000000326700000000000023452 0ustar00erikstaff00000000000000import os from astropy.io import fits from astropy.units import Unit from astropy.nddata import StdDevUncertainty from specutils.io.registers import data_loader from specutils import Spectrum1D __all__ = ['stis_identify', 'stis_spectrum_loader'] def stis_identify(origin, *args, **kwargs): """Check whether given file contains HST/STIS spectral data.""" with fits.open(args[0]) as hdu: if hdu[0].header['TELESCOP'] == 'HST' and hdu[0].header['INSTRUME'] == 'STIS': return True return False @data_loader(label="HST/STIS",identifier=stis_identify, extensions=['FITS', 'FIT', 'fits', 'fit']) def stis_spectrum_loader(file_name, **kwargs): """ Load STIS spectral data from the MAST archive into a spectrum object. Parameters ---------- file_name: str The path to the FITS file Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ name = os.path.basename(file_name) with fits.open(file_name, **kwargs) as hdu: header = hdu[0].header meta = {'header': header} unit = Unit("erg/cm**2 Angstrom s") disp_unit = Unit('Angstrom') data = hdu[1].data['FLUX'].flatten() * unit dispersion = hdu[1].data['wavelength'].flatten() * disp_unit uncertainty = StdDevUncertainty(hdu[1].data["ERROR"].flatten() * unit) sort_idx = dispersion.argsort() dispersion = dispersion[sort_idx] data = data[sort_idx] uncertainty = uncertainty[sort_idx] return Spectrum1D(flux=data, spectral_axis=dispersion, uncertainty=uncertainty, meta=meta) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/jwst_reader.py0000644000076500000240000000461200000000000024116 0ustar00erikstaff00000000000000import astropy.units as u from astropy.io import fits from ...spectra import Spectrum1D, SpectrumList from ..registers import data_loader def identify_jwst_fits(origin, *args, **kwargs): """ Check whether the given file is a JWST spectral data product. This check is fairly simple. It expects FITS files that contain an ASDF header (which is not used here, but indicates a JWST data product). It then looks for at least one EXTRACT1D header, which contains spectral data. """ try: with fits.open(args[0]) as hdulist: # This is a near-guarantee that we have a JWST data product if not 'ASDF' in hdulist: return False # This indicates the data product contains spectral data if not 'EXTRACT1D' in hdulist: return False return True # This probably means we didn't have a FITS file except Exception: return False @data_loader("JWST", identifier=identify_jwst_fits, dtype=SpectrumList, extensions=['fits']) def jwst_loader(filename, spectral_axis_unit=None, **kwargs): """ Loader for JWST data files. Parameters ---------- file_name: str The path to the FITS file Returns ------- data: SpectrumList A list of the spectra that are contained in this file. """ spectra = [] with fits.open(filename) as hdulist: for hdu in hdulist: if hdu.name != 'EXTRACT1D': continue # Provide reasonable defaults based on the units assigned by the # extract1d step of the JWST pipeline. TUNIT fields should be # populated by the pipeline, but it's apparently possible for them # to be missing in some files. wavelength_units = u.Unit(hdu.header.get('TUNIT1', 'um')) flux_units = u.Unit(hdu.header.get('TUNIT2', 'mJy')) error_units = u.Unit(hdu.header.get('TUNIT3', 'mJy')) wavelength = hdu.data['WAVELENGTH'] * wavelength_units flux = hdu.data['FLUX'] * flux_units error = hdu.data['ERROR'] * error_units meta = dict(slitname=hdu.header.get('SLTNAME', '')) # TODO: pass uncertainty using the error from the HDU spec = Spectrum1D(flux=flux, spectral_axis=wavelength, meta=meta) spectra.append(spec) return SpectrumList(spectra) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/muscles_sed.py0000644000076500000240000000360700000000000024116 0ustar00erikstaff00000000000000import logging import os from astropy.io import fits from astropy.nddata import StdDevUncertainty from astropy.table import Table from astropy.units import Unit from astropy.wcs import WCS from ...spectra import Spectrum1D from ..registers import data_loader def identify_muscles_sed(origin, *args, **kwargs): # check if file can be opened with this reader # args[0] = filename # fits.open(args[0]) = hdulist return (isinstance(args[0], str) and # check if file is .fits args[0].endswith('sed.fits') and # check hdulist has more than one extension len(fits.open(args[0])) > 1 and # check if fits has BinTable extension isinstance(fits.open(args[0])[1], fits.BinTableHDU) and # check if MUSCLES proposal ID is in fits header fits.open(args[0])[0].header['PROPOSID'] == 13650 ) @data_loader("muscles-sed", identifier=identify_muscles_sed, dtype=Spectrum1D, extensions=['fits']) def muscles_sed(file_name, **kwargs): """ Load spectrum from a MUSCLES SED FITS file. Parameters ---------- file_name: str The path to the FITS file. Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ # name is not used; what was it for? # name = os.path.basename(file_name.rstrip(os.sep)).rsplit('.', 1)[0] with fits.open(file_name, **kwargs) as hdulist: header = hdulist[0].header tab = Table.read(hdulist) meta = {'header': header} uncertainty = StdDevUncertainty(tab["ERROR"]) data = tab["FLUX"] wavelength = tab["WAVELENGTH"] return Spectrum1D(flux=data, spectral_axis=wavelength, uncertainty=uncertainty, meta=meta, unit=data.unit, spectral_axis_unit=wavelength.unit) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/sdss.py0000644000076500000240000000743000000000000022562 0ustar00erikstaff00000000000000""" Loader for SDSS individual spectrum files: spec_ files. .. _spec: https://data.sdss.org/datamodel/files/BOSS_SPECTRO_REDUX/RUN2D/spectra/PLATE4/spec.html """ import os import re from astropy.io import fits from astropy.table import Table from astropy.wcs import WCS from astropy.units import Unit, def_unit from astropy.nddata import StdDevUncertainty import numpy as np from specutils.io.registers import data_loader, custom_writer from specutils import Spectrum1D __all__ = ['spec_identify', 'spSpec_identify', 'spec_loader', 'spSpec_loader'] _spSpec_pattern = re.compile(r'spSpec-\d{5}-\d{4}-\d{3}\.fit') _spec_pattern = re.compile(r'spec-\d{4,5}-\d{5}-\d{4}\.fits') def spec_identify(origin, *args, **kwargs): """ Check whether given filename is FITS. This is used for Astropy I/O Registry. """ return (isinstance(args[0], str) and _spec_pattern.match(args[0]) is not None) def spSpec_identify(origin, *args, **kwargs): """ Check whether given filename is FITS. This is used for Astropy I/O Registry. """ return (isinstance(args[0], str) and _spSpec_pattern.match(args[0]) is not None) @data_loader(label="SDSS-III/IV spec", identifier=spec_identify, extensions=['fits']) def spec_loader(file_name, **kwargs): """ Loader for SDSS-III/IV optical spectrum "spec" files. Parameters ---------- file_name: str The path to the FITS file Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ name = os.path.basename(file_name.rstrip(os.sep)).rsplit('.', 1)[0] hdulist = fits.open(file_name, **kwargs) header = hdulist[0].header meta = {'header': header} # spectrum is in HDU 1 data = hdulist[1].data['flux'] unit = Unit('1e-17 erg / (Angstrom cm2 s)') # Because there is no object that explicitly supports inverse variance. stdev = np.sqrt(1.0/hdulist[1].data['ivar']) uncertainty = StdDevUncertainty(stdev * unit) dispersion = 10**hdulist[1].data['loglam'] dispersion_unit = Unit('Angstrom') mask = hdulist[1].data['and_mask'] != 0 hdulist.close() return Spectrum1D(flux=data * unit, spectral_axis=dispersion * dispersion_unit, uncertainty=uncertainty, meta=meta, mask=mask) @data_loader(label="SDSS-I/II spSpec", identifier=spSpec_identify, extensions=['fits']) def spSpec_loader(file_name, **kwargs): """ Loader for SDSS-I/II spSpec files. Parameters ---------- file_name: str The path to the FITS file Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ name = os.path.basename(file_name.rstrip(os.sep)).rsplit('.', 1)[0] hdulist = fits.open(file_name, **kwargs) header = hdulist[0].header meta = {'header': header} wcs = WCS(hdulist[0].header) data = hdulist[0].data[0, :] unit = Unit('1e-17 erg / (Angstrom cm2 s)') uncertainty = StdDevUncertainty(hdulist[0].data[2, :] * unit) # dispersion from the WCS but convert out of logspace # dispersion = 10**wcs.all_pix2world(np.arange(data.shape[0]), 0)[0] dispersion = 10**wcs.all_pix2world(np.vstack((np.arange(data.shape[0]), np.zeros((data.shape[0],)))).T, 0)[:, 0] # dispersion = 10**hdulist[1].data['loglam'] dispersion_unit = Unit('Angstrom') mask = hdulist[0].data[3, :] != 0 hdulist.close() return Spectrum1D(flux=data * unit, spectral_axis=dispersion * dispersion_unit, uncertainty=uncertainty, meta=meta, mask=mask) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/subaru_pfs_spec.py0000644000076500000240000000462200000000000024771 0ustar00erikstaff00000000000000""" Loader for PFS spectrum files. https://github.com/Subaru-PFS/datamodel/blob/master/datamodel.txt """ import os import re from astropy.io import fits from astropy.units import Unit from astropy.nddata import StdDevUncertainty import numpy as np from specutils.io.registers import data_loader from specutils import Spectrum1D __all__ = ['spec_identify', 'spec_loader'] # This RE matches the file name pattern defined in Subaru-PFS' datamodel.txt : # "pfsObject-%05d-%s-%3d-%08x-%02d-0x%08x.fits" % (tract, patch, catId, objId, # nVisit % 100, pfsVisitHash) _spec_pattern = re.compile(r'pfsObject-(?P\d{5})-(?P.{3})-' r'(?P\d{3})-(?P\d{8})-' r'(?P\d{2})-(?P0x\w{8})' r'\.fits') def spec_identify(origin, *args, **kwargs): """ Check whether given filename is FITS. This is used for Astropy I/O Registry. """ return (isinstance(args[0], str) and _spec_pattern.match(args[0]) is not None) @data_loader(label="Subaru-pfsObject", identifier=spec_identify, extensions=['fits']) def spec_loader(file_name, **kwargs): """ Loader for PFS combined spectrum files. Parameters ---------- file_name: str The path to the FITS file Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ m = _spec_pattern.match(os.path.basename(file_name)) with fits.open(file_name, **kwargs) as hdulist: header = hdulist[0].header meta = {'header': header, 'tract': m['tract'], 'patch': m['patch'], 'catId': m['catId'], 'objId': m['objId'], 'nVisit': m['nVisit'], 'pfsVisitHash': m['pfsVisitHash']} # spectrum is in HDU 2 data = hdulist[2].data['flux'] unit = Unit('nJy') error = hdulist[2].data['fluxVariance'] uncertainty = StdDevUncertainty(np.sqrt(error)) wave = hdulist[2].data['lambda'] wave_unit = Unit('nm') mask = hdulist[2].data['mask'] != 0 return Spectrum1D(flux=data * unit, spectral_axis=wave * wave_unit, uncertainty=uncertainty, meta=meta, mask=mask) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/tabular_fits.py0000644000076500000240000000712300000000000024264 0ustar00erikstaff00000000000000import logging import os import numpy as np from astropy.io import fits from astropy.nddata import StdDevUncertainty from astropy.table import Table import astropy.units as u from astropy.wcs import WCS from ...spectra import Spectrum1D from ..registers import data_loader, custom_writer from ..parsing_utils import (generic_spectrum_from_table, spectrum_from_column_mapping) __all__ = ['tabular_fits_loader', 'tabular_fits_writer'] def identify_tabular_fits(origin, *args, **kwargs): # check if file can be opened with this reader # args[0] = filename # fits.open(args[0]) = hdulist return (isinstance(args[0], str) and # check if file is .fits os.path.splitext(args[0].lower())[1] == '.fits' and # check hdulist has more than one extension len(fits.open(args[0])) > 1 and # check if fits has BinTable extension isinstance(fits.open(args[0])[1], fits.BinTableHDU) ) @data_loader("tabular-fits", identifier=identify_tabular_fits, dtype=Spectrum1D, extensions=['fits']) def tabular_fits_loader(file_name, column_mapping=None, **kwargs): """ Load spectrum from a FITS file. Parameters ---------- file_name: str The path to the FITS file column_mapping : dict A dictionary describing the relation between the FITS file columns and the arguments of the `Spectrum1D` class, along with unit information. The dictionary keys should be the FITS file column names while the values should be a two-tuple where the first element is the associated `Spectrum1D` keyword argument, and the second element is the unit for the ASCII file column:: column_mapping = {'FLUX': ('flux', 'Jy')} Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. """ # Parse the wcs information. The wcs will be passed to the column finding # routines to search for spectral axis information in the file. with fits.open(file_name) as hdulist: wcs = WCS(hdulist[0].header) tab = Table.read(file_name, format='fits') # If no column mapping is given, attempt to parse the file using # unit information if column_mapping is None: return generic_spectrum_from_table(tab, wcs=wcs, **kwargs) return spectrum_from_column_mapping(tab, column_mapping, wcs=wcs) @custom_writer("tabular-fits") def tabular_fits_writer(spectrum, file_name, update_header=False, **kwargs): flux = spectrum.flux disp = spectrum.spectral_axis header = spectrum.meta.get('header', fits.header.Header()).copy() if update_header: hdr_types = (str, int, float, complex, bool, np.floating, np.integer, np.complexfloating, np.bool_) header.update([keyword for keyword in spectrum.meta.items() if isinstance(keyword[1], hdr_types)]) # Strip header of FITS reserved keywords for keyword in ['NAXIS', 'NAXIS1', 'NAXIS2']: header.remove(keyword, ignore_missing=True) # Mapping of spectral_axis types to header TTYPE1 dispname = disp.unit.physical_type if dispname == "length": dispname = "wavelength" columns = [disp, flux] colnames = [dispname, "flux"] # Include uncertainty - units to be inferred from spectrum.flux if spectrum.uncertainty is not None: columns.append(spectrum.uncertainty.quantity) colnames.append("uncertainty") tab = Table(columns, names=colnames, meta=header) tab.write(file_name, format="fits", **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/io/default_loaders/tests/0000755000076500000240000000000000000000000022372 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/tests/__init__.py0000644000076500000240000000000000000000000024471 0ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/tests/test_apogee.py0000644000076500000240000000254600000000000025252 0ustar00erikstaff00000000000000# Third-party from astropy.utils.data import download_file from astropy.config import set_temp_cache import pytest # Package from specutils.io.default_loaders.apogee import (apStar_loader, apVisit_loader, aspcapStar_loader) @pytest.mark.remote_data def test_apStar_loader(tmpdir): apstar_url = ("https://data.sdss.org/sas/dr16/apogee/spectro/redux/r12/" "stars/apo25m/N7789/apStar-r12-2M00005414+5522241.fits") with set_temp_cache(path=str(tmpdir)): filename = download_file(apstar_url, cache=True) spectrum = apStar_loader(filename) @pytest.mark.remote_data def test_apVisit_loader(tmpdir): apvisit_url = ("https://data.sdss.org/sas/dr16/apogee/spectro/redux/r12/" "visit/apo25m/N7789/5094/55874/" "apVisit-r12-5094-55874-123.fits") with set_temp_cache(path=str(tmpdir)): filename = download_file(apvisit_url, cache=True) spectrum = apVisit_loader(filename) @pytest.mark.remote_data def test_aspcapStar_loader(tmpdir): aspcap_url = ("https://data.sdss.org/sas/dr16/apogee/spectro/aspcap/r12/" "l33/apo25m/N7789/aspcapStar-r12-2M00005414+5522241.fits") with set_temp_cache(path=str(tmpdir)): filename = download_file(aspcap_url, cache=True) spectrum = aspcapStar_loader(filename) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/default_loaders/tests/test_jwst_loader.py0000644000076500000240000000241500000000000026322 0ustar00erikstaff00000000000000import numpy as np from astropy.io import fits from astropy.table import Table from specutils import Spectrum1D, SpectrumList def create_spectrum_hdu(data_len): # Create a minimal header for the purposes of testing data = np.random.random((data_len, 3)) table = Table(data=data, names=['WAVELENGTH', 'FLUX', 'ERROR']) hdu = fits.BinTableHDU(table, name='EXTRACT1D') hdu.header['TUNIT1'] = 'um' hdu.header['TUNIT2'] = 'mJy' hdu.header['TUNIT3'] = 'mJy' return hdu def test_jwst_loader(tmpdir): tmpfile = str(tmpdir.join('jwst.fits')) hdulist = fits.HDUList() # Make sure the file has a primary HDU hdulist.append(fits.PrimaryHDU()) # Add several BinTableHDUs that contain spectral data hdulist.append(create_spectrum_hdu(100)) hdulist.append(create_spectrum_hdu(120)) hdulist.append(create_spectrum_hdu(110)) # JWST data product will always contain an ASDF header which is a BinTable hdulist.append(fits.BinTableHDU(name='ASDF')) hdulist.writeto(tmpfile) data = SpectrumList.read(tmpfile, format='JWST') assert len(data) == 3 for item in data: assert isinstance(item, Spectrum1D) assert data[0].shape == (100,) assert data[1].shape == (120,) assert data[2].shape == (110,) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/io/default_loaders/wcs_fits.py0000644000076500000240000004046100000000000023430 0ustar00erikstaff00000000000000import logging import os from astropy import units as u from astropy.io import fits from astropy.wcs import WCS from astropy.modeling import models, fitting import shlex from ...spectra import Spectrum1D from ..registers import data_loader __all__ = ['wcs1d_fits_loader', 'wcs1d_fits_writer', 'non_linear_wcs1d_fits'] def identify_wcs1d_fits(origin, *args, **kwargs): # check if file can be opened with this reader # args[0] = filename return (isinstance(args[0], str) and os.path.splitext(args[0].lower())[1] == '.fits' and # check if number of axes is one fits.getheader(args[0])['NAXIS'] == 1 and fits.getheader(args[0])['WCSDIM'] == 1 and 'WAT1_001' not in fits.getheader(args[0]) and # check if CTYPE1 kep is in the header 'CTYPE1' in fits.getheader(args[0]) ) @data_loader("wcs1d-fits", identifier=identify_wcs1d_fits, dtype=Spectrum1D, extensions=['fits']) def wcs1d_fits_loader(file_name, spectral_axis_unit=None, flux_unit=None, hdu_idx=0, **kwargs): """ Loader for single spectrum-per-HDU spectra in FITS files, with the spectral axis stored in the header as FITS-WCS. The flux unit of the spectrum is determined by the 'BUNIT' keyword of the HDU (if present), while the spectral axis unit is set by the WCS's 'CUNIT'. Parameters ---------- file_name : str The path to the FITS file. spectral_axis_unit: str or `~astropy.Unit`, optional Units of the spectral axis. If not given (or None), the unit will be inferred from the CUNIT in the WCS. Not that if this is providded it will *override* any units the CUNIT provides. flux_unit: str or `~astropy.Unit`, optional Units of the flux for this spectrum. If not given (or None), the unit will be inferred from the BUNIT keyword in the header. Note that this unit will attempt to convert from BUNIT if BUNIT is present hdu_idx : int The index of the HDU to load into this spectrum. Notes ----- Loader contributed by Kelle Cruz. """ logging.info("Spectrum file looks like wcs1d-fits") with fits.open(file_name, **kwargs) as hdulist: header = hdulist[hdu_idx].header wcs = WCS(header) if wcs.naxis != 1: raise ValueError('FITS fle input to wcs1d_fits_loader is not 1D') if 'BUNIT' in header: data = u.Quantity(hdulist[hdu_idx].data, unit=header['BUNIT']) if flux_unit is not None: data = data.to(flux_unit) else: data = u.Quantity(hdulist[hdu_idx].data, unit=flux_unit) if spectral_axis_unit is not None: wcs.wcs.cunit[0] = str(spectral_axis_unit) meta = {'header': header} return Spectrum1D(flux=data, wcs=wcs, meta=meta) def identify_iraf_wcs(origin, *args): """IRAF WCS identifier The difference of this with respect to wcs1d is that this can work with WCSDIM == 2 """ return (isinstance(args[0], str) and 'WAT1_001' in fits.getheader(args[0])) @data_loader('iraf', identifier=identify_iraf_wcs, dtype=Spectrum1D, extensions=['fits']) def non_linear_wcs1d_fits(file_name, spectral_axis_unit=None, flux_unit=None, **kwargs): """Read wcs from files written by IRAF IRAF does not strictly follow the fits standard specially for non-linear wavelength solutions Parameters ---------- file_name : str Name of file to load spectral_axis_unit : `~astropy.Unit`, optional Spectral axis unit, default is None in which case will search for it in the header under the keyword 'WAT1_001' flux_unit : `~astropy.Unit`, optional Flux units, default is None. If not specified will attempt to read it using the keyword 'BUNIT' and if this keyword does not exist it will assume 'ADU'. Returns ------- `specutils.Spectrum1D` """ logging.info('Loading 1D non-linear fits solution') with fits.open(file_name, **kwargs) as hdulist: header = hdulist[0].header for wcsdim in range(1, header['WCSDIM'] + 1): ctypen = header['CTYPE{:d}'.format(wcsdim)] if ctypen == 'LINEAR': logging.info("linear Solution: Try using " "`format='wcs1d-fits'` instead") wcs = WCS(header) spectral_axis = _read_linear_iraf_wcs(wcs=wcs, dc_flag=header['DC-FLAG']) elif ctypen == 'MULTISPE': logging.info("Multi spectral or non-linear solution") spectral_axis = _read_non_linear_iraf_wcs(header=header, wcsdim=wcsdim) else: raise NotImplementedError if flux_unit is not None: data = hdulist[0].data * flux_unit elif 'BUNIT' in header: data = u.Quantity(hdulist[0].data, unit=header['BUNIT']) else: logging.info("Flux unit was not provided, neither it was in the" "header. Assuming ADU.") data = u.Quantity(hdulist[0].data, unit='adu') if spectral_axis_unit is not None: spectral_axis *= spectral_axis_unit else: wat_head = header['WAT1_001'] wat_dict = dict() for pair in wat_head.split(' '): wat_dict[pair.split('=')[0]] = pair.split('=')[1] if wat_dict['units'] == 'angstroms': logging.info("Found spectral axis units to be angstrom") spectral_axis *= u.angstrom meta = {'header': header} return Spectrum1D(flux=data, spectral_axis=spectral_axis, meta=meta) def _read_linear_iraf_wcs(wcs, dc_flag): """Linear solution reader This method read the appropriate keywords. Calls the method _set_math_model which decides what is the appropriate mathematical model to be used and creates and then evaluates the model for an array. Parameters ---------- wcs : `~astropy.wcs.WCS` Contains wcs information extracted from the header dc_flag : int Extracted from the header under the keyword DC-FLAG which defines what kind of solution is described. For linear solutions it is 0 or 1. Returns ------- spectral_axis : `~numpy.ndarray` Mathematical model of wavelength solution evluated for each pixel position """ wcs_dict = {'crval': wcs.wcs.crval[0], 'crpix': wcs.wcs.crpix[0], 'cdelt': wcs.wcs.cd[0], 'dtype': dc_flag, 'pnum': wcs._naxis[0]} math_model = _set_math_model(wcs_dict=wcs_dict) spectral_axis = math_model(range(wcs_dict['pnum'])) return spectral_axis def _read_non_linear_iraf_wcs(header, wcsdim): """Read non-linear wavelength solutions written by IRAF Extracts the appropriate information and organize it in a dictionary for calling the method _set_math_model which decides what is the appropriate mathematical model to be used according the the type of wavelength solution it is dealing with. Parameters ---------- header : `~astropy.io.fits.header.Header` Full header of file being loaded wcsdim : int Number of the wcs dimension to be read. Returns ------- spectral_axis : `~numpy.ndarray` Mathematical model of wavelength solution evluated for each pixel position """ wat_wcs_dict = {} ctypen = header['CTYPE{:d}'.format(wcsdim)] logging.info('Attempting to read CTYPE{:d}: {:s}'.format(wcsdim, ctypen)) if ctypen == 'MULTISPE': # TODO (simon): What is the * (asterisc) doing here?. wat_head = header['WAT{:d}*'.format(wcsdim)] if len(wat_head) == 1: logging.debug('Get units') wat_array = wat_head[0].split(' ') for pair in wat_array: split_pair = pair.split('=') wat_wcs_dict[split_pair[0]] = split_pair[1] # print(wat_head[0].split(' ')) elif len(wat_head) > 1: wat_string = '' for key in wat_head: wat_string += header[key] wat_array = shlex.split(wat_string.replace('=', ' ')) if len(wat_array) % 2 == 0: for i in range(0, len(wat_array), 2): # if wat_array[i] not in wcs_dict.keys(): wat_wcs_dict[wat_array[i]] = wat_array[i + 1] # print(wat_array[i], wat_array[i + 1]) for key in wat_wcs_dict.keys(): logging.debug("{:d} -{:s}- {:s}".format(wcsdim, key, wat_wcs_dict[key])) if 'spec1' in wat_wcs_dict.keys(): spec = wat_wcs_dict['spec1'].split() aperture = int(spec[0]) beam = int(spec[1]) disp_type = int(spec[2]) disp_start = float(spec[3]) disp_del_av = float(spec[4]) pix_num = int(spec[5]) dopp_fact = float(spec[6]) aper_low = int(float(spec[7])) aper_high = int(float(spec[8])) weight = float(spec[9]) zeropoint = float(spec[10]) function_type = int(spec[11]) order = int(float(spec[12])) min_pix_val = int(float(spec[13])) max_pix_val = int(float(spec[14])) params = [float(i) for i in spec[15:]] wcs_dict = {'aperture': aperture, 'beam': beam, 'dtype': disp_type, 'dstart': disp_start, 'avdelt': disp_del_av, 'pnum': pix_num, 'z': dopp_fact, 'alow': aper_low, 'ahigh': aper_high, 'weight': weight, 'zeropoint': zeropoint, 'ftype': function_type, 'order': order, 'pmin': min_pix_val, 'pmax': max_pix_val, 'fpar': params} logging.info('Retrieving model') math_model = _set_math_model(wcs_dict=wcs_dict) spectral_axis = math_model(range(1, wcs_dict['pnum'] + 1)) return spectral_axis def _set_math_model(wcs_dict): """Defines a mathematical model of the wavelength solution Uses 2 keywords to decide which model is to be built and calls the appropriate function. dtype: -1: None, no wavelength solution available 0: Linear wavelength solution 1: Log-Linear wavelength solution (not implemented) 2: Non-Linear solutions ftype: 1: Chebyshev 2: Legendre 3: Linear Spline (not implemented) 4: Cubic Spline (not implemented) 5: Pixel Coordinates (not implemented) Not implemented models could be implemented on user-request. Parameters ---------- wcs_dict : dict Contains all the necessary wcs information needed for building any of models supported. Returns ------- The mathematical model which describes the transformation from pixel to wavelength. An instance of `~astropy.modeling.Model`. """ if wcs_dict['dtype'] == -1: return _none() elif wcs_dict['dtype'] == 0: return _linear_solution(wcs_dict=wcs_dict) elif wcs_dict['dtype'] == 1: return _log_linear(wcs_dict=wcs_dict) elif wcs_dict['dtype'] == 2: if wcs_dict['ftype'] == 1: return _chebyshev(wcs_dict=wcs_dict) elif wcs_dict['ftype'] == 2: return _non_linear_legendre(wcs_dict=wcs_dict) elif wcs_dict['ftype'] == 3: return _non_linear_cspline(wcs_dict=wcs_dict) elif wcs_dict['ftype'] == 4: return _non_linear_lspline(wcs_dict=wcs_dict) elif wcs_dict['ftype'] == 5: # pixel coordinates raise NotImplementedError elif wcs_dict['ftype'] == 6: # sampled coordinate array raise NotImplementedError else: raise SyntaxError('ftype {:d} is not defined in the ' 'standard'.format(wcs_dict['ftype'])) else: raise SyntaxError('dtype {:d} is not defined in the ' 'standard'.format(wcs_dict['dtype'])) def _none(): """Required to handle No-wavelength solution No wavelength solution is considered in the FITS standard (dtype = -1) This will return the identity function. It does not use `~astropy.modeling.models.Identity` because is not simpler to instantiate. Instead it uses `~astropy.modeling.models.Linear1D` Rretuns ------- A mathematical model instance of `~astropy.modeling.models.Linear1D` with slope 1 and intercept 0. """ model = models.Linear1D(slope=1, intercept=0) return model def _linear_solution(wcs_dict): """Constructs a Linear1D model based on the WCS information obtained from the header. """ intercept = wcs_dict['crval'] - \ (wcs_dict['crpix'] - 1) * \ wcs_dict['cdelt'] model = models.Linear1D(slope=wcs_dict['cdelt'], intercept=intercept) return model def _log_linear(wcs_dict): """Returns a log linear model of the wavelength solution. Not implemented Raises ------ NotImplementedError """ raise NotImplementedError def _chebyshev(wcs_dict): """Returns a chebyshev model of the wavelength solution. Constructs a Chebyshev1D mathematical model Parameters ---------- wcs_dict : dict Dictionary containing all the wcs information decoded from the header and necessary for constructing the Chebyshev1D model. Returns ------- `~astropy.modeling.Model` """ model = models.Chebyshev1D(degree=wcs_dict['order'] - 1, domain=[wcs_dict['pmin'], wcs_dict['pmax']], ) new_params = [wcs_dict['fpar'][i] for i in range(wcs_dict['order'])] model.parameters = new_params return model def _non_linear_legendre(wcs_dict): """Returns a legendre model Constructs a Legendre1D mathematical model Parameters ---------- wcs_dict : dict Dictionary containing all the wcs information decoded from the header and necessary for constructing the Legendre1D model. Returns ------- `~astropy.modeling.Model` """ model = models.Legendre1D(degree=wcs_dict['order'] - 1, domain=[wcs_dict['pmin'], wcs_dict['pmax']], ) new_params = [wcs_dict['fpar'][i] for i in range(wcs_dict['order'])] model.parameters = new_params return model def _non_linear_lspline(wcs_dict): """Returns a linear spline model of the wavelength solution Not implemented This function should extract certain parameters from the `wcs_dict` parameter and construct a mathematical model that makes the conversion from pixel to wavelength. All the necessary information is already contained in the dictionary so the only work to be done is to make the instantiation of the appropriate subclass of `~astropy.modeling.Model`. Parameters ---------- wcs_dict : dict Contains all the WCS information decoded from an IRAF fits header. Raises ------ NotImplementedError """ raise NotImplementedError('Linear spline is not implemented') def _non_linear_cspline(wcs_dict): """Returns a cubic spline model of the wavelength solution. This function should extract certain parameters from the `wcs_dict` parameter and construct a mathematical model that makes the conversion from pixel to wavelength. All the necessary information is already contained in the dictionary so the only work to be done is to make the instantiation of the appropriate subclass of `~astropy.modeling.Model`. Not implemented Parameters ---------- wcs_dict : dict Contains all the WCS information decoded from an IRAF fits header. Raises ------ NotImplementedError """ raise NotImplementedError('Cubic spline is not implemented') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/parsing_utils.py0000644000076500000240000002037500000000000021337 0ustar00erikstaff00000000000000import numpy as np from astropy.table import Table import astropy.units as u from astropy.nddata import StdDevUncertainty from astropy.utils.exceptions import AstropyUserWarning import warnings import logging from specutils.spectra import Spectrum1D def spectrum_from_column_mapping(table, column_mapping, wcs=None): """ Given a table and a mapping of the table column names to attributes on the Spectrum1D object, parse the information into a Spectrum1D. Parameters ---------- table : :class:`~astropy.table.Table` The table object returned from parsing the data file. column_mapping : dict A dictionary describing the relation between the file columns and the arguments of the `Spectrum1D` class, along with unit information. The dictionary keys should be the file column names while the values should be a two-tuple where the first element is the associated `Spectrum1D` keyword argument, and the second element is the unit for the file column:: column_mapping = {'FLUX': ('flux', 'Jy')} wcs : :class:`~astropy.wcs.WCS` or :class:`gwcs.WCS` WCS object passed to the Spectrum1D initializer. """ spec_kwargs = {} # Associate columns of the file with the appropriate spectrum1d arguments for col_name, (kwarg_name, cm_unit) in column_mapping.items(): # If the table object couldn't parse any unit information, # fallback to the column mapper defined unit tab_unit = table[col_name].unit if tab_unit and cm_unit is not None: # If the table unit is defined, retrieve the quantity array for # the column kwarg_val = u.Quantity(table[col_name], tab_unit) # Attempt to convert the table unit to the user-defined unit. logging.debug("Attempting auto-convert of table unit '%s' to " "user-provided unit '%s'.", tab_unit, cm_unit) if cm_unit.physical_type in ('length', 'frequency'): # Spectral axis column information kwarg_val = kwarg_val.to(cm_unit, equivalence=u.spectral()) elif 'spectral flux' in cm_unit.physical_type: # Flux/error column information kwarg_val = kwarg_val.to( cm_unit, equivalencies=u.spectral_density(1 * u.AA)) elif cm_unit is not None: # In this case, the user has defined a unit in the column mapping # but no unit has been defined in the table object. kwarg_val = u.Quantity(table[col_name], cm_unit) else: # Neither the column mapping nor the table contain unit information. # This may be desired e.g. for the mask or bit flag arrays. kwarg_val = table[col_name] spec_kwargs.setdefault(kwarg_name, kwarg_val) # Ensure that the uncertainties are a subclass of NDUncertainty if spec_kwargs.get('uncertainty') is not None: spec_kwargs['uncertainty'] = StdDevUncertainty( spec_kwargs.get('uncertainty')) return Spectrum1D(**spec_kwargs, wcs=wcs, meta=table.meta) def generic_spectrum_from_table(table, wcs=None, **kwargs): """ Load spectrum from an Astropy table into a Spectrum1D object. Uses the following logic to figure out which column is which: * Spectral axis (dispersion) is the first column with units compatible with u.spectral() or with length units such as 'pix'. * Flux is taken from the first column with units compatible with u.spectral_density(), or with other likely culprits such as 'adu' or 'cts/s'. * Uncertainty comes from the next column with the same units as flux. Parameters ---------- file_name: str The path to the ECSV file wcs : :class:`~astropy.wcs.WCS` A FITS WCS object. If this is present, the machinery will fall back to using the wcs to find the dispersion information. Returns ------- data: Spectrum1D The spectrum that is represented by the data in this table. Raises ------ Warns if uncertainty has zeros or negative numbers. Raises IOError if it can't figure out the columns. """ # Local function to find the wavelength or frequency column def _find_spectral_axis_column(table,columns_to_search): """ Figure out which column in a table holds the spectral axis (dispersion). Take the first column that has units compatible with u.spectral() equivalencies. If none meet that criterion, look for other likely length units such as 'pix'. """ additional_valid_units = [u.Unit('pix')] found_column = None # First, search for a column with units compatible with Angstroms for c in columns_to_search: try: table[c].to("AA",equivalencies=u.spectral()) found_column = c break except: continue # If no success there, check for other possible length units if found_column is None: for c in columns_to_search: if table[c].unit in additional_valid_units: found_column = c break return found_column # Local function to find the flux column def _find_spectral_column(table,columns_to_search,spectral_axis): """ Figure out which column in a table holds the fluxes or uncertainties. Take the first column that has units compatible with u.spectral_density() equivalencies. If none meet that criterion, look for other likely length units such as 'adu' or 'cts/s'. """ additional_valid_units = [u.Unit('adu'),u.Unit('ct/s')] found_column = None # First, search for a column with units compatible with Janskies for c in columns_to_search: try: table[c].to("Jy",equivalencies=u.spectral_density(spectral_axis)) found_column = c break except: continue # If no success there, check for other possible flux units if found_column is None: for c in columns_to_search: if table[c].unit in additional_valid_units: found_column = c break return found_column # Make a copy of the column names so we can remove them as they are found colnames = table.colnames.copy() # Use the first column that has spectral unit as the dispersion axis spectral_axis_column = _find_spectral_axis_column(table, colnames) if spectral_axis_column is None and wcs is None: raise IOError("Could not identify column containing the wavelength, frequency or energy") elif wcs is not None: spectral_axis = None else: spectral_axis = table[spectral_axis_column].to(table[spectral_axis_column].unit) colnames.remove(spectral_axis_column) # Use the first column that has a spectral_density equivalence as the flux flux_column = _find_spectral_column(table,colnames,spectral_axis) if flux_column is None: raise IOError("Could not identify column containing the flux") flux = table[flux_column].to(table[flux_column].unit) colnames.remove(flux_column) # Use the next column with the same units as flux as the uncertainty # Interpret it as a standard deviation and check if it has zeros or negative values err_column = None for c in colnames: if table[c].unit == table[flux_column].unit: err_column = c break if err_column is not None: err = StdDevUncertainty(table[err_column].to(table[err_column].unit)) if np.min(table[err_column]) <= 0.: warnings.warn("Standard Deviation has values of 0 or less", AstropyUserWarning) # Create the Spectrum1D object and return it if wcs is not None or spectral_axis_column is not None and flux_column is not None: if err_column is not None: spectrum = Spectrum1D(flux=flux, spectral_axis=spectral_axis, uncertainty=err, meta=table.meta, wcs=wcs) else: spectrum = Spectrum1D(flux=flux, spectral_axis=spectral_axis, meta=table.meta, wcs=wcs) return spectrum ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/io/registers.py0000644000076500000240000001407100000000000020457 0ustar00erikstaff00000000000000""" A module containing the mechanics of the specutils io registry. """ import os import logging from functools import wraps from astropy.io import registry as io_registry from ..spectra import Spectrum1D, SpectrumList __all__ = ['data_loader', 'custom_writer', 'get_loaders_by_extension'] def data_loader(label, identifier=None, dtype=Spectrum1D, extensions=None, priority=0): """ Wraps a function that can be added to an `~astropy.io.registry` for custom file reading. Parameters ---------- label : str The label given to the function inside the registry. identifier : func The identified function used to verify that a file is to use a particular file. dtype : class A class reference for which the data loader should be store. extensions : list A list of file extensions this loader supports loading from. In the case that no identifier function is defined, but a list of file extensions is, a simple identifier function will be created to check for consistency with the extensions. priority : int Set the priority of the loader. Currently influences the sorting of the returned loaders for a dtype. """ def identifier_wrapper(ident): def wrapper(*args, **kwargs): '''In case the identifier function raises an exception, log that and continue''' try: return ident(*args, **kwargs) except Exception as e: logging.debug("Tried to read this as {} file, but could not.".format(label)) logging.debug(e, exc_info=True) return False return wrapper def decorator(func): io_registry.register_reader(label, dtype, func) if identifier is None: # If the identifier is not defined, but the extensions are, create # a simple identifier based off file extension. if extensions is not None: logging.info("'{}' data loader provided for {} without " "explicit identifier. Creating identifier using " "list of compatible extensions".format( label, dtype.__name__)) id_func = lambda *args, **kwargs: any([args[1].endswith(x) for x in extensions]) # Otherwise, create a dummy identifier else: logging.warning("'{}' data loader provided for {} without " "explicit identifier or list of compatible " "extensions".format(label, dtype.__name__)) id_func = lambda *args, **kwargs: True else: id_func = identifier_wrapper(identifier) io_registry.register_identifier(label, dtype, id_func) # Include the file extensions as attributes on the function object func.extensions = extensions # Include priority on the loader function attribute func.priority = priority # Sort the io_registry based on priority sorted_loaders = sorted(io_registry._readers.items(), key=lambda item: getattr(item[1], 'priority', 0)) # Update the registry with the sorted dictionary io_registry._readers.clear() io_registry._readers.update(sorted_loaders) logging.debug("Successfully loaded reader \"{}\".".format(label)) # Automatically register a SpectrumList reader for any data_loader that # reads Spectrum1D objects. TODO: it's possible that this # functionality should be opt-in rather than automatic. if dtype is Spectrum1D: def load_spectrum_list(*args, **kwargs): return SpectrumList([ func(*args, **kwargs) ]) # Add these attributes to the SpectrumList reader as well load_spectrum_list.extensions = extensions load_spectrum_list.priority = priority io_registry.register_reader(label, SpectrumList, load_spectrum_list) io_registry.register_identifier(label, SpectrumList, id_func) logging.debug("Created SpectrumList reader for \"{}\".".format(label)) @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator def custom_writer(label, dtype=Spectrum1D): def decorator(func): io_registry.register_writer(label, Spectrum1D, func) @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator def get_loaders_by_extension(extension): """ Retrieve a list of loader labels associated with a given extension. Parameters ---------- extension : str The extension for which associated loaders will be matched against. Returns ------- loaders : list A list of loader names that are associated with the extension. """ return [fmt for (fmt, cls), func in io_registry._readers.items() if issubclass(cls, Spectrum1D) and func.extensions is not None and extension in func.extensions] def _load_user_io(): # Get the path relative to the user's home directory path = os.path.expanduser("~/.specutils") # If the directory doesn't exist, create it if not os.path.exists(path): os.mkdir(path) # Import all python files from the directory for file in os.listdir(path): if not file.endswith("py"): continue try: import importlib.util as util spec = util.spec_from_file_location(file[:-3], os.path.join(path, file)) mod = util.module_from_spec(spec) spec.loader.exec_module(mod) except ImportError: from importlib import import_module sys.path.insert(0, path) try: import_module(file[:-3]) except ModuleNotFoundError: # noqa pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/manipulation/0000755000076500000240000000000000000000000020164 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/manipulation/__init__.py0000644000076500000240000000032600000000000022276 0ustar00erikstaff00000000000000from .smoothing import * # noqa from .estimate_uncertainty import * # noqa from .extract_spectral_region import * # noqa from .utils import * # noqa from .manipulation import * # noqa from .resample import * ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/manipulation/estimate_uncertainty.py0000644000076500000240000000504100000000000024776 0ustar00erikstaff00000000000000import numpy as np from astropy import units as u from .. import Spectrum1D, SpectralRegion from astropy.nddata.nduncertainty import StdDevUncertainty, VarianceUncertainty, InverseVariance from .extract_spectral_region import extract_region __all__ = ['noise_region_uncertainty'] def noise_region_uncertainty(spectrum, spectral_region, noise_func=np.std): """ Generates a new spectrum with an uncertainty from the noise in a particular region of the spectrum. Parameters ---------- spectrum : `~specutils.Spectrum1D` The spectrum to which we want to set the uncertainty. spectral_region : `~specutils.SpectralRegion` The region to use to calculate the standard deviation. noise_func : callable A function which takes the (1D) flux in the ``spectral_region`` and yields a *single* value for the noise to use in the result spectrum. Returns ------- spectrum_uncertainty : `~specutils.Spectrum1D` The ``spectrum``, but with a constant uncertainty set by the result of the noise region calculation """ # Extract the sub spectrum based on the region sub_spectra = extract_region(spectrum, spectral_region) # TODO: make this work right for multi-dimensional spectrum1D's? if not isinstance(sub_spectra, list): sub_spectra = [sub_spectra] sub_flux = u.Quantity(np.concatenate([s.flux.value for s in sub_spectra]), spectrum.flux.unit) # Compute the standard deviation of the flux. noise = noise_func(sub_flux) # Uncertainty type will be selected based on the unit coming from the # noise function compared to the original spectral flux units. if noise.unit == spectrum.flux.unit: uncertainty = StdDevUncertainty(noise*np.ones(spectrum.flux.shape)) elif noise.unit == spectrum.flux.unit**2: uncertainty = VarianceUncertainty(noise*np.ones(spectrum.flux.shape)) elif noise.unit == 1/(spectrum.flux.unit**2): uncertainty = InverseVariance(noise*np.ones(spectrum.flux.shape)) else: raise ValueError('Can not determine correct NDData Uncertainty based on units {} relative to the flux units {}'.format(noise.unit, spectrum.flux.unit)) # Return new specturm with uncertainty set. return Spectrum1D(flux=spectrum.flux, spectral_axis=spectrum.spectral_axis, uncertainty=uncertainty, wcs=spectrum.wcs, velocity_convention=spectrum.velocity_convention, rest_value=spectrum.rest_value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/manipulation/extract_spectral_region.py0000644000076500000240000001147200000000000025455 0ustar00erikstaff00000000000000from math import floor, ceil # faster than int(np.floor/ceil(float)) import numpy as np from astropy import units as u from .. import Spectrum1D __all__ = ['extract_region'] def _to_edge_pixel(subregion, spectrum): """ Calculate and return the left and right indices defined by the lower and upper bounds and based on the input `~specutils.spectra.spectrum1d.Spectrum1D`. The left and right indices will be returned. Parameters ---------- spectrum: `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object from which the region will be extracted. Returns ------- left_index, right_index: int, int Left and right indices defined by the lower and upper bounds. """ # # Left/lower side of sub region # if subregion[0].unit.is_equivalent(u.pix): left_index = floor(subregion[0].value) else: # Convert lower value to spectrum spectral_axis units left_reg_in_spec_unit = subregion[0].to(spectrum.spectral_axis.unit, u.spectral()) if left_reg_in_spec_unit < spectrum.spectral_axis[0]: left_index = 0 elif left_reg_in_spec_unit > spectrum.spectral_axis[-1]: left_index = len(spectrum.spectral_axis)-1 else: try: left_index = int(np.ceil(spectrum.wcs.world_to_pixel(left_reg_in_spec_unit))) except Exception as e: raise ValueError("Lower value, {}, could not convert using spectrum's WCS {}. Exception: {}".format( left_reg_in_spec_unit, spectrum.wcs, e)) # # Right/upper side of sub region # if subregion[1].unit.is_equivalent(u.pix): right_index = ceil(subregion[1].value) else: # Convert upper value to spectrum spectral_axis units right_reg_in_spec_unit = subregion[1].to(spectrum.spectral_axis.unit, u.spectral()) if right_reg_in_spec_unit > spectrum.spectral_axis[-1]: right_index = len(spectrum.spectral_axis)-1 elif right_reg_in_spec_unit < spectrum.spectral_axis[0]: right_index = 0 else: try: right_index = int(np.floor(spectrum.wcs.world_to_pixel(right_reg_in_spec_unit))) + 1 except Exception as e: raise ValueError("Upper value, {}, could not convert using spectrum's WCS {}. Exception: {}".format( right_reg_in_spec_unit, spectrum.wcs, e)) return left_index, right_index def extract_region(spectrum, region): """ Extract a region from the input `~specutils.Spectrum1D` defined by the lower and upper bounds defined by the ``region`` instance. The extracted region will be returned as a new `~specutils.Spectrum1D`. Parameters ---------- spectrum: `~specutils.spectra.spectrum1d.Spectrum1D` The spectrum object from which the region will be extracted. Returns ------- spectrum: `~specutils.spectra.spectrum1d.Spectrum1D` Excised spectrum. Notes ----- The region extracted is a discrete subset of the input spectrum. No interpolation is done on the left and right side of the spectrum. The region is assumed to be a closed interval (as opposed to Python which is open on the upper end). For example: Given: A ``spectrum`` with spectral_axis of ``[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]*u.um``. A ``region`` defined as ``SpectralRegion(0.2*u.um, 0.5*u.um)`` And we calculate ``sub_spectrum = extract_region(spectrum, region)``, then the ``sub_spectrum`` spectral axis will be ``[0.2, 0.3, 0.4, 0.5] * u.um``. If the ``region`` does not overlap with the ``spectrum`` then an empty Spectrum1D object will be returned. """ extracted_spectrum = [] for subregion in region._subregions: left_index, right_index = _to_edge_pixel(subregion, spectrum) # If both indices are out of bounds then return None if left_index is None and right_index is None: empty_spectrum = Spectrum1D(spectral_axis=[]*spectrum.spectral_axis.unit, flux=[]*spectrum.flux.unit) extracted_spectrum.append(empty_spectrum) else: # If only one index is out of bounds then set it to # the lower or upper extent if left_index is None: left_index = 0 if right_index is None: right_index = len(spectrum.spectral_axis) if left_index > right_index: left_index, right_index = right_index, left_index extracted_spectrum.append(spectrum[left_index:right_index]) # If there is only one subregion in the region then we will # just return a spectrum. if len(region) == 1: extracted_spectrum = extracted_spectrum[0] return extracted_spectrum ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/manipulation/manipulation.py0000644000076500000240000000474000000000000023243 0ustar00erikstaff00000000000000""" A module for analysis tools dealing with uncertainties or error analysis in spectra. """ import copy import numpy as np import operator __all__ = ['snr_threshold'] def snr_threshold(spectrum, value, op=operator.gt): """ Calculate the mean S/N of the spectrum based on the flux and uncertainty in the spectrum. This will be calculated over the regions, if they are specified. Parameters ---------- spectrum : `~specutils.Spectrum1D`, `~specutils.SpectrumCollection` or `~astropy.nddata.NDData` The spectrum object overwhich the S/N threshold will be calculated. value: ``float`` Threshold value to be applied to flux / uncertainty. op: One of operator.gt, operator.ge, operator.lt, operator.le or the str equivalent '>', '>=', '<', '<=' The mathematical operator to apply for thresholding. Returns ------- spectrum: `~specutils.Spectrum1D` Output object with ``spectrum.mask`` set based on threshold. Notes ----- The input object will need to have the uncertainty defined in order for the SNR to be calculated. """ # Setup the mapping operator_mapping = { '>': operator.gt, '<': operator.lt, '>=': operator.ge, '<=': operator.le } if not hasattr(spectrum, 'uncertainty') or spectrum.uncertainty is None: raise Exception("S/N thresholding requires the uncertainty be defined.") if (op not in [operator.gt, operator.ge, operator.lt, operator.le] and op not in operator_mapping.keys()): raise ValueError('Threshold operator must be a string or operator that represents ' + 'greater-than, less-than, greater-than-or-equal or ' + 'less-than-or-equal') # If the operator passed in is a string, then map to the # operator method. if isinstance(op, str): op = operator_mapping[op] # Spectrum1D if hasattr(spectrum, 'flux'): data = spectrum.flux # NDData elif hasattr(spectrum, 'data'): data = spectrum.data * (spectrum.unit if spectrum.unit is not None else 1) else: raise ValueError('Could not find data attribute.') # NDData convention: Masks should follow the numpy convention that valid # data points are marked by False and invalid ones with True. mask = ~op(data / (spectrum.uncertainty.quantity), value) spectrum_out = copy.copy(spectrum) spectrum_out._mask = mask return spectrum_out ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/manipulation/resample.py0000644000076500000240000003666500000000000022366 0ustar00erikstaff00000000000000import logging from abc import ABC, abstractmethod from warnings import warn import numpy as np from astropy.nddata import StdDevUncertainty, VarianceUncertainty, \ InverseVariance from astropy.units import Quantity from scipy.interpolate import CubicSpline from ..spectra import Spectrum1D __all__ = ['ResamplerBase', 'FluxConservingResampler', 'LinearInterpolatedResampler', 'SplineInterpolatedResampler'] class ResamplerBase(ABC): """ Base class for resample classes. The algorithms and needs for difference resamples will vary quite a bit, so this class is relatively sparse. Parameters ---------- extrapolation_treatment : str What to do when resampling off the edge of the spectrum. Can be ``'nan_fill'`` to have points beyond the edges by set to NaN, or ``'zero_fill'`` to be set to zero. """ def __init__(self, extrapolation_treatment='nan_fill'): if extrapolation_treatment not in ('nan_fill', 'zero_fill'): raise ValueError('invalid extrapolation_treatment value: ' + str(extrapolation_treatment)) self.extrapolation_treatment = extrapolation_treatment def __call__(self, orig_spectrum, fin_spec_axis): """ Return the resulting `~specutils.Spectrum1D` of the resampling. """ return self.resample1d(orig_spectrum, fin_spec_axis) @abstractmethod def resample1d(self, orig_spectrum, fin_spec_axis): """ Workhorse method that will return the resampled Spectrum1D object. """ return NotImplemented @staticmethod def _calc_bin_edges(x): """ Calculate the bin edge values of an input spectral axis. Input values are assumed to be the center of the bins. todo: this should live in the main spectrum object, but we're still figuring out the details to that implementation, so leaving here for now. Parameters ---------- x : ndarray The input spectral axis values. Returns ------- edges : ndarray Calcualated bin edges, including left and right most bin edges. """ inside_edges = (x[1:] + x[:-1]) / 2 edges = np.insert(inside_edges, 0, 2 * x[0] - inside_edges[0]) edges = np.append(edges, 2 * x[-1] - inside_edges[-1]) return edges class FluxConservingResampler(ResamplerBase): """ This resampling algorithm conserves overall integrated flux (as opposed to flux density). Algorithm based on the equations documented in the following paper: https://ui.adsabs.harvard.edu/abs/2017arXiv170505165C/abstract Parameters ---------- extrapolation_treatment : str What to do when resampling off the edge of the spectrum. Can be ``'nan_fill'`` to have points beyond the edges by set to NaN, or ``'zero_fill'`` to be set to zero. Examples -------- To resample an input spectrum to a user specified spectral grid using a flux conserving algorithm: >>> import numpy as np >>> import astropy.units as u >>> from specutils import Spectrum1D >>> from specutils.manipulation import FluxConservingResampler >>> input_spectra = Spectrum1D( ... flux=np.array([1, 3, 7, 6, 20]) * u.mJy, ... spectral_axis=np.array([2, 4, 12, 16, 20]) * u.nm) >>> resample_grid = [1, 5, 9, 13, 14, 17, 21, 22, 23] *u.nm >>> fluxc_resample = FluxConservingResampler() >>> output_spectrum1D = fluxc_resample(input_spectra, resample_grid) # doctest: +IGNORE_OUTPUT """ def _resample_matrix(self, orig_spec_axis, fin_spec_axis): """ Create a re-sampling matrix to be used in re-sampling spectra in a way that conserves flux. This code was heavily influenced by Nick Earl's resample rough draft: nmearl@0ff6ef1. Parameters ---------- orig_spec_axis : ndarray The original spectral axis array. fin_spec_axis : ndarray The desired spectral axis array. Returns ------- resample_mat : ndarray An [[N_{fin_spec_axis}, M_{orig_spec_axis}]] matrix. """ # Lower bin and upper bin edges orig_edges = self._calc_bin_edges(orig_spec_axis) fin_edges = self._calc_bin_edges(fin_spec_axis) # I could get rid of these alias variables, # but it does add readability orig_low = orig_edges[:-1] fin_low = fin_edges[:-1] orig_upp = orig_edges[1:] fin_upp = fin_edges[1:] # Here's the real work in figuring out the bin overlaps # i.e., contribution of each original bin to the resampled bin l_inf = np.where(orig_low > fin_low[:, np.newaxis], orig_low, fin_low[:, np.newaxis]) l_sup = np.where(orig_upp < fin_upp[:, np.newaxis], orig_upp, fin_upp[:, np.newaxis]) resamp_mat = (l_sup - l_inf).clip(0) resamp_mat *= (orig_upp - orig_low) # set bins that don't overlap 100% with original bins # to zero by checking edges, and applying generated mask left_clip = np.where(fin_edges[:-1] - orig_edges[0] < 0, 0, 1) right_clip = np.where(orig_edges[-1] - fin_edges[1:] < 0, 0, 1) keep_overlapping_matrix = left_clip * right_clip resamp_mat *= keep_overlapping_matrix[:, np.newaxis] return resamp_mat def resample1d(self, orig_spectrum, fin_spec_axis): """ Create a re-sampling matrix to be used in re-sampling spectra in a way that conserves flux. If an uncertainty is present in the input spectra it will be propagated through to the final resampled output spectra as an InverseVariance uncertainty. Parameters ---------- orig_spectrum : `~specutils.Spectrum1D` The original 1D spectrum. fin_spec_axis : Quantity The desired spectral axis array. Returns ------- resample_spectrum : `~specutils.Spectrum1D` An output spectrum containing the resampled `~specutils.Spectrum1D` """ # Check if units on original spectrum and new wavelength (if defined) # match if isinstance(fin_spec_axis, Quantity): if orig_spectrum.spectral_axis_unit != fin_spec_axis.unit: raise ValueError("Original spectrum spectral axis grid and new" "spectral axis grid must have the same units.") # todo: Would be good to return uncertainty in type it was provided? # todo: add in weighting options # Get provided uncertainty into variance if orig_spectrum.uncertainty is not None: if isinstance(orig_spectrum.uncertainty, StdDevUncertainty): pixel_uncer = np.square(orig_spectrum.uncertainty.array) elif isinstance(orig_spectrum.uncertainty, VarianceUncertainty): pixel_uncer = orig_spectrum.uncertainty.array elif isinstance(orig_spectrum.uncertainty, InverseVariance): pixel_uncer = np.reciprocal(orig_spectrum.uncertainty.array) else: pixel_uncer = None orig_axis_in_fin = orig_spectrum.spectral_axis.to(fin_spec_axis.unit) resample_grid = self._resample_matrix(orig_axis_in_fin.value, fin_spec_axis.value) # Now for some broadcasting magic to handle multi dimensional flux inputs # Essentially this part is inserting length one dimensions as fillers # For example, if we have a (5,6,10) input flux, and an output grid # of 3, flux will be broadcast to (5,6,1,10) and resample_grid will # Be broadcast to (1,1,3,10). The sum then reduces down the 10, the # original dispersion grid, leaving 3, the new dispersion grid, as # the last index. new_flux_shape = list(orig_spectrum.flux.shape) new_flux_shape.insert(-1, 1) in_flux = orig_spectrum.flux.reshape(new_flux_shape) ones = [1] * len(orig_spectrum.flux.shape[:-1]) new_shape_resample_grid = ones + list(resample_grid.shape) resample_grid = resample_grid.reshape(new_shape_resample_grid) # Calculate final flux out_flux = np.sum(in_flux * resample_grid, axis=-1) / np.sum( resample_grid, axis=-1) # Calculate output uncertainty if pixel_uncer is not None: pixel_uncer = pixel_uncer.reshape(new_flux_shape) out_variance = np.sum(pixel_uncer * resample_grid**2, axis=-1) / np.sum( resample_grid**2, axis=-1) out_uncertainty = InverseVariance(np.reciprocal(out_variance)) else: out_uncertainty = None # nan-filling happens by default - replace with zeros if requested: if self.extrapolation_treatment == 'zero_fill': origedges = self._calc_bin_edges( orig_spectrum.spectral_axis.value) * \ orig_spectrum.spectral_axis.unit off_edges = (fin_spec_axis < origedges[0]) | (origedges[-1] < fin_spec_axis) out_flux[off_edges] = 0 if out_uncertainty is not None: out_uncertainty.array[off_edges] = 0 # todo: for now, use the units from the pre-resampled # spectra, although if a unit is defined for fin_spec_axis and it doesn't # match the input spectrum it won't work right, will have to think # more about how to handle that... could convert before and after # calculation, which is probably easiest. Matrix math algorithm is # geometry based, so won't work to just let quantity math handle it. resampled_spectrum = Spectrum1D(flux=out_flux, spectral_axis=np.array(fin_spec_axis) * orig_spectrum.spectral_axis_unit, uncertainty=out_uncertainty) return resampled_spectrum class LinearInterpolatedResampler(ResamplerBase): """ Resample a spectrum onto a new ``spectral_axis`` using linear interpolation. Parameters ---------- extrapolation_treatment : str What to do when resampling off the edge of the spectrum. Can be ``'nan_fill'`` to have points beyond the edges by set to NaN, or ``'zero_fill'`` to be set to zero. Examples -------- To resample an input spectrum to a user specified dispersion grid using linear interpolation: >>> import numpy as np >>> import astropy.units as u >>> from specutils import Spectrum1D >>> from specutils.manipulation import LinearInterpolatedResampler >>> input_spectra = Spectrum1D( ... flux=np.array([1, 3, 7, 6, 20]) * u.mJy, ... spectral_axis=np.array([2, 4, 12, 16, 20]) * u.nm) >>> resample_grid = [1, 5, 9, 13, 14, 17, 21, 22, 23] * u.nm >>> fluxc_resample = LinearInterpolatedResampler() >>> output_spectrum1D = fluxc_resample(input_spectra, resample_grid) # doctest: +IGNORE_OUTPUT """ def __init__(self, extrapolation_treatment='nan_fill'): super().__init__(extrapolation_treatment) def resample1d(self, orig_spectrum, fin_spec_axis): """ Call interpolation, repackage new spectra Parameters ---------- orig_spectrum : `~specutils.Spectrum1D` The original 1D spectrum. fin_spec_axis : ndarray The desired spectral axis array. Returns ------- resample_spectrum : `~specutils.Spectrum1D` An output spectrum containing the resampled `~specutils.Spectrum1D` """ fill_val = np.nan #bin_edges=nan_fill case if self.extrapolation_treatment == 'zero_fill': fill_val = 0 orig_axis_in_fin = orig_spectrum.spectral_axis.to(fin_spec_axis.unit) out_flux = np.interp(fin_spec_axis, orig_axis_in_fin, orig_spectrum.flux, left=fill_val, right=fill_val) new_unc = None if orig_spectrum.uncertainty is not None: out_unc_arr = np.interp(fin_spec_axis, orig_axis_in_fin, orig_spectrum.uncertainty.array, left=fill_val, right=fill_val) new_unc = orig_spectrum.uncertainty.__class__(array=out_unc_arr, unit=orig_spectrum.unit) return Spectrum1D(spectral_axis=fin_spec_axis, flux=out_flux, uncertainty=new_unc) class SplineInterpolatedResampler(ResamplerBase): """ This resample algorithim uses a cubic spline interpolator. Any uncertainty is also interpolated using an identical spline. Parameters ---------- extrapolation_treatment : str What to do when resampling off the edge of the spectrum. Can be ``'nan_fill'`` to have points beyond the edges by set to NaN, or ``'zero_fill'`` to be set to zero. Examples -------- To resample an input spectrum to a user specified spectral axis grid using a cubic spline interpolator: >>> import numpy as np >>> import astropy.units as u >>> from specutils import Spectrum1D >>> from specutils.manipulation import SplineInterpolatedResampler >>> input_spectra = Spectrum1D( ... flux=np.array([1, 3, 7, 6, 20]) * u.mJy, ... spectral_axis=np.array([2, 4, 12, 16, 20]) * u.nm) >>> resample_grid = [1, 5, 9, 13, 14, 17, 21, 22, 23] * u.nm >>> fluxc_resample = SplineInterpolatedResampler() >>> output_spectrum1D = fluxc_resample(input_spectra, resample_grid) # doctest: +IGNORE_OUTPUT """ def __init__(self, bin_edges='nan_fill'): super().__init__(bin_edges) def resample1d(self, orig_spectrum, fin_spec_axis): """ Call interpolation, repackage new spectra Parameters ---------- orig_spectrum : `~specutils.Spectrum1D` The original 1D spectrum. fin_spec_axis : Quantity The desired spectral axis array. Returns ------- resample_spectrum : `~specutils.Spectrum1D` An output spectrum containing the resampled `~specutils.Spectrum1D` """ orig_axis_in_new = orig_spectrum.spectral_axis.to(fin_spec_axis.unit) flux_spline = CubicSpline(orig_axis_in_new.value, orig_spectrum.flux.value, extrapolate=self.extrapolation_treatment != 'nan_fill') out_flux_val = flux_spline(fin_spec_axis.value) new_unc = None if orig_spectrum.uncertainty is not None: unc_spline = CubicSpline(orig_axis_in_new.value, orig_spectrum.uncertainty.array, extrapolate=self.extrapolation_treatment != 'nan_fill') out_unc_val = unc_spline(fin_spec_axis.value) new_unc = orig_spectrum.uncertainty.__class__(array=out_unc_val, unit=orig_spectrum.unit) if self.extrapolation_treatment == 'zero_fill': origedges = self._calc_bin_edges( orig_spectrum.spectral_axis.value) * \ orig_spectrum.spectral_axis.unit off_edges = (fin_spec_axis < origedges[0]) | (origedges[-1] < fin_spec_axis) out_flux_val[off_edges] = 0 if new_unc is not None: new_unc.array[off_edges] = 0 return Spectrum1D(spectral_axis=fin_spec_axis, flux=out_flux_val*orig_spectrum.flux.unit, uncertainty=new_unc) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/manipulation/smoothing.py0000644000076500000240000002054000000000000022546 0ustar00erikstaff00000000000000import copy import warnings from astropy import convolution from astropy.nddata import StdDevUncertainty, VarianceUncertainty, InverseVariance import astropy.units as u from astropy.utils.exceptions import AstropyUserWarning from scipy.signal import medfilt import numpy as np from ..spectra import Spectrum1D __all__ = ['convolution_smooth', 'box_smooth', 'gaussian_smooth', 'trapezoid_smooth', 'median_smooth'] def convolution_smooth(spectrum, kernel): """ Apply a convolution based smoothing to the spectrum. The kernel must be one of the 1D kernels defined in `astropy.convolution`. This method can be used along but also is used by other specific methods below. If the spectrum uncertainty exists and is StdDevUncertainty, VarianceUncertainty or InverseVariance then the errors will be propagated through the convolution using a standard propagation of errors. The covariance is not considered, currently. Parameters ---------- spectrum : `~specutils.Spectrum1D` The `~specutils.Spectrum1D` object to which the smoothing will be applied. kernel : `astropy.convolution.Kernel1D` subclass or array. The convolution based smoothing kernel - anything that `astropy.convolution.convolve` accepts. Returns ------- spectrum : `~specutils.Spectrum1D` Output `~specutils.Spectrum1D` which is copy of the one passed in with the updated flux. Raises ------ ValueError In the case that ``spectrum`` and ``kernel`` are not the correct types. """ # Parameter checks if not isinstance(spectrum, Spectrum1D): raise ValueError('The spectrum parameter must be a Spectrum1D object') # Get the flux of the input spectrum flux = spectrum.flux # Smooth based on the input kernel smoothed_flux = convolution.convolve(flux, kernel) # Propagate the uncertainty if it exists... uncertainty = copy.deepcopy(spectrum.uncertainty) if uncertainty is not None: if isinstance(uncertainty, StdDevUncertainty): # Convert values = uncertainty.array ivar_values = 1 / values**2 # Propagate prop_ivar_values = convolution.convolve(ivar_values, kernel) # Put back in uncertainty.array = 1 / np.sqrt(prop_ivar_values) elif isinstance(uncertainty, VarianceUncertainty): # Convert values = uncertainty.array ivar_values = 1 / values # Propagate prop_ivar_values = convolution.convolve(ivar_values, kernel) # Put back in uncertainty.array = 1 / prop_ivar_values elif isinstance(uncertainty, InverseVariance): # Convert ivar_values = uncertainty.array # Propagate prop_ivar_values = convolution.convolve(ivar_values, kernel) # Put back in uncertainty.array = prop_ivar_values else: uncertainty = None warnings.warn("Uncertainty is {} but convolutional error propagation is not defined for that type. Uncertainty will be dropped in the convolved spectrum.".format(type(uncertainty)), AstropyUserWarning) # Return a new object with the smoothed flux. return Spectrum1D(flux=u.Quantity(smoothed_flux, spectrum.unit), spectral_axis=u.Quantity(spectrum.spectral_axis, spectrum.spectral_axis_unit), wcs=spectrum.wcs, uncertainty=uncertainty, velocity_convention=spectrum.velocity_convention, rest_value=spectrum.rest_value) def box_smooth(spectrum, width): """ Smooth a `~specutils.Spectrum1D` instance based on a `astropy.convolution.Box1DKernel` kernel. Parameters ---------- spectrum : `~specutils.Spectrum1D` The spectrum object to which the smoothing will be applied. width : number The width of the kernel, in pixels, as defined in `astropy.convolution.Box1DKernel` Returns ------- spectrum : `~specutils.Spectrum1D` Output `~specutils.Spectrum1D` which a copy of the one passed in with the updated flux. Raises ------ ValueError In the case that ``width`` is not the correct type or value. """ # Parameter checks if not isinstance(width, (int, float)) or width <= 0: raise ValueError('The width parameter, {}, must be a number greater than 0'.format( width)) # Create the gaussian kernel box1d_kernel = convolution.Box1DKernel(width) # Call and return the convolution smoothing. return convolution_smooth(spectrum, box1d_kernel) def gaussian_smooth(spectrum, stddev): """ Smooth a `~specutils.Spectrum1D` instance based on a `astropy.convolution.Gaussian1DKernel`. Parameters ---------- spectrum : `~specutils.Spectrum1D` The spectrum object to which the smoothing will be applied. stddev : number The stddev of the kernel, in pixels, as defined in `astropy.convolution.Gaussian1DKernel` Returns ------- spectrum : `~specutils.Spectrum1D` Output `~specutils.Spectrum1D` which is copy of the one passed in with the updated flux. Raises ------ ValueError In the case that ``stddev`` is not the correct type or value. """ # Parameter checks if not isinstance(stddev, (int, float)) or stddev <= 0: raise ValueError('The stddev parameter, {}, must be a number greater than 0'.format( stddev)) # Create the gaussian kernel gaussian_kernel = convolution.Gaussian1DKernel(stddev) # Call and return the convolution smoothing. return convolution_smooth(spectrum, gaussian_kernel) def trapezoid_smooth(spectrum, width): """ Smoothing based on a `astropy.convolution.Trapezoid1DKernel` kernel. Parameters ---------- spectrum : `~specutils.Spectrum1D` The `~specutils.Spectrum1D` object to which the smoothing will be applied. width : number The width of the kernel, in pixels, as defined in `astropy.convolution.Trapezoid1DKernel` Returns ------- spectrum : `~specutils.Spectrum1D` Output `~specutils.Spectrum1D` which is copy of the one passed in with the updated flux. Raises ------ ValueError In the case that ``width`` is not the correct type or value. """ # Parameter checks if not isinstance(width, (int, float)) or width <= 0: raise ValueError('The stddev parameter, {}, must be a number greater than 0'.format( width)) # Create the gaussian kernel trapezoid_kernel = convolution.Trapezoid1DKernel(width) # Call and return the convolution smoothing. return convolution_smooth(spectrum, trapezoid_kernel) def median_smooth(spectrum, width): """ Smoothing based on a median filter. The median filter smoothing is implemented using the `scipy.signal.medfilt` function. Parameters ---------- spectrum : `~specutils.Spectrum1D` The `~specutils.Spectrum1D` object to which the smoothing will be applied. width : number The width of the median filter in pixels. Returns ------- spectrum : `~specutils.Spectrum1D` Output `~specutils.Spectrum1D` which is copy of the one passed in with the updated flux. Raises ------ ValueError In the case that ``spectrum`` or ``width`` are not the correct type or value. """ # Parameter checks if not isinstance(spectrum, Spectrum1D): raise ValueError('The spectrum parameter must be a Spectrum1D object') if not isinstance(width, (int, float)) or width <= 0: raise ValueError('The stddev parameter, {}, must be a number greater than 0'.format( width)) # Get the flux of the input spectrum flux = spectrum.flux # Smooth based on the input kernel smoothed_flux = medfilt(flux, width) # Return a new object with the smoothed flux. return Spectrum1D(flux=u.Quantity(smoothed_flux, spectrum.unit), spectral_axis=u.Quantity(spectrum.spectral_axis, spectrum.spectral_axis_unit), wcs=spectrum.wcs, velocity_convention=spectrum.velocity_convention, rest_value=spectrum.rest_value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/manipulation/utils.py0000644000076500000240000001412000000000000021674 0ustar00erikstaff00000000000000import numpy as np from ..spectra import Spectrum1D, SpectralRegion __all__ = ['excise_regions', 'linear_exciser', 'spectrum_from_model'] def linear_exciser(spectrum, region): """ Basic spectral excise method where the spectral region defined by the 2-tuple parameter ``region`` (start and end wavelengths) will result in the flux between those regions set to a linear ramp of the two points immediately before and after the start and end regions. Other methods could be defined by the user to do other types of excision. Parameters ---------- spectrum : `~specutils.Spectrum1D` The `~specutils.Spectrum1D` object to which the smoothing will be applied. region : `~specutils.SpectralRegion` Region to excise. Returns ------- spectrum : `~specutils.Spectrum1D` Output `~specutils.Spectrum1D` with the region excised. Raises ------ ValueError In the case that ``spectrum`` and ``region`` are not the correct types. """ # # Find the indices of the wavelengths in the range ``range`` # wavelengths = spectrum.spectral_axis wavelengths_in = (wavelengths >= region.lower) & (wavelengths < region.upper) inclusive_indices = np.nonzero(wavelengths_in)[0] # # Now set the flux values for these indices to be a # linear range # s, e = max(inclusive_indices[0]-1, 0), min(inclusive_indices[-1]+1, wavelengths.size-1) flux = spectrum.flux.value modified_flux = flux modified_flux[s:e] = np.linspace(flux[s], flux[e], modified_flux[s:e].size) # Return a new object with the regions excised. return Spectrum1D(flux=modified_flux*spectrum.flux.unit, spectral_axis=wavelengths, uncertainty=spectrum.uncertainty, wcs=spectrum.wcs, unit=spectrum.unit, velocity_convention=spectrum.velocity_convention, rest_value=spectrum.rest_value) def excise_regions(spectrum, regions, exciser=linear_exciser): """ Apply a convolution based smoothing to the spectrum. The kernel must be one of the 1D kernels defined in `astropy.convolution`. This method can be used along but also is used by other specific methods below. Parameters ---------- spectrum : `~specutils.Spectrum1D` The `~specutils.Spectrum1D` object to which the smoothing will be applied. regions : list of `~specutils.SpectralRegion` Each element of the list is a `~specutils.SpectralRegion`. The flux between these wavelengths will be "cut out" using the ``exciser`` method. exciser: method Method that takes the spectrum and region and does the excising. Other methods could be defined and used by this routine. default: linear_exciser Returns ------- spectrum : `~specutils.Spectrum1D` Output `~specutils.Spectrum1D` which has the regions excised. Raises ------ ValueError In the case that ``spectrum`` and ``regions`` are not the correct types. """ # Parameter checks if not isinstance(spectrum, Spectrum1D): raise ValueError('The spectrum parameter must be Spectrum1D object.') for region in regions: spectrum = excise_region(spectrum, region, exciser) return spectrum def excise_region(spectrum, region, exciser=linear_exciser): """ Apply a convolution based smoothing to the spectrum. The kernel must be one of the 1D kernels defined in `astropy.convolution`. This method can be used along but also is used by other specific methods below. Parameters ---------- spectrum : `~specutils.Spectrum1D` The `~specutils.Spectrum1D` object to which the smoothing will be applied. region : `~specutils.SpectralRegion` Region to excise. exciser: method Method that takes the spectrum and region and does the excising. Other methods could be defined and used by this routine. default: linear_exciser Returns ------- spectrum : `~specutils.Spectrum1D` Output `~specutils.Spectrum1D` with the region excised. Raises ------ ValueError In the case that ``spectrum`` and ``region`` are not the correct types. """ # Parameter checks if not isinstance(spectrum, Spectrum1D): raise ValueError('The spectrum parameter must be Spectrum1D object.') if not isinstance(region, SpectralRegion): raise ValueError('The region parameter must be a 2-tuples of start and end wavelengths.') # # Call the exciser method # return exciser(spectrum, region) def spectrum_from_model(model_input, spectrum): """ This method will create a `~specutils.Spectrum1D` object with the flux defined by calling the input ``model``. All other parameters for the output `~specutils.Spectrum1D` object will be the same as the input `~specutils.Spectrum1D` object. Parameters ---------- model : `~astropy.modeling.Model` The input model or compound model from which flux is calculated. spectrum : `~specutils.Spectrum1D` The `~specutils.Spectrum1D` object to use as the model template. Returns ------- spectrum : `~specutils.Spectrum1D` Output `~specutils.Spectrum1D` which is copy of the one passed in with the updated flux. The uncertainty will not be copied as it is not necessarily the same. """ # If the input model has units then we will call it normally. if getattr(model_input, model_input.param_names[0]).unit is not None: flux = model_input(spectrum.spectral_axis) # If the input model does not have units, then assume it is in # the same units as the input spectrum. else: flux = model_input(spectrum.spectral_axis.value)*spectrum.flux.unit return Spectrum1D(flux=flux, spectral_axis=spectrum.spectral_axis, wcs=spectrum.wcs, velocity_convention=spectrum.velocity_convention, rest_value=spectrum.rest_value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/spectra/0000755000076500000240000000000000000000000017125 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/spectra/__init__.py0000644000076500000240000000045000000000000021235 0ustar00erikstaff00000000000000""" The core specutils data objects package. This contains the `~astropy.nddata.NDData`-inherited classes used for storing the spectrum data. """ from .spectrum1d import * # noqa from .spectral_region import * # noqa from .spectrum_collection import * #noqa from .spectrum_list import * #noqa ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/spectra/spectral_region.py0000644000076500000240000002214500000000000022663 0ustar00erikstaff00000000000000import itertools import sys import numpy as np import astropy.units as u class SpectralRegion: """ A `SpectralRegion` is a container class enables some simplicty in defining and passing a region (interval) for a spectrum. In the future, there might be more functionality added in here and there is some discussion that this might/could move to `Astropy Regions `_. Parameters ---------- lower : Scalar `~astropy.units.Quantity` with pixel or any valid ``spectral_axis`` unit The lower bound of the region. upper : Scalar `~astropy.units.Quantity` with pixel or any valid ``spectral_axis`` unit The upper bound of the region. Notes ----- The subregions will be ordered based on the lower bound of each subregion. """ @classmethod def from_center(cls, center=None, width=None): """ SpectralRegion class method that enables the definition of a `SpectralRegion` from the center and width rather than lower and upper bounds. Parameters ---------- center : Scalar `~astropy.units.Quantity` with pixel or any valid ``spectral_axis`` unit The center of the spectral region. width : Scalar `~astropy.units.Quantity` with pixel or any valid ``spectral_axis`` unit The width of the spectral region. """ if width.value <= 0: raise ValueError('SpectralRegion width must be positive.') return cls(center - width, center + width) def __init__(self, *args): """ Lower and upper values for the interval. """ # Create instance variables self._subregions = None # # Set the values (using the setters for doing the proper checking) # if self._is_2_element(args): self._subregions = [tuple(args)] elif isinstance(args, (list, tuple)) and all([self._is_2_element(x) for x in args[0]]): self._subregions = [tuple(x) for x in args[0]] else: raise ValueError('SpectralRegion input must be a 2-tuple or a list of 2-tuples.') # # Check validity of the input sub regions. # if not self._valid(): raise ValueError('SpectralRegion 2-tuple lower extent must be less than upper extent.') # # The sub-regions are to always be ordered based on the lower bound. # self._reorder() def _info(self): """ Pretty print the sub-regions. """ toreturn = 'Spectral Region, {} sub-regions:\n'.format(len(self._subregions)) # Setup the subregion text. subregion_text = [] for ii, subregion in enumerate(self._subregions): subregion_text.append(' ({}, {})'.format(subregion[0], subregion[1])) # Determine the length of the text boxes. max_len = max(len(srt) for srt in subregion_text) + 1 ncols = 70 // max_len # Add sub region info to the output text. fmt = '{' + ':<{}'.format(max_len) + '}' for ii, srt in enumerate(subregion_text): toreturn += fmt.format(srt) if ii % ncols == (ncols-1): toreturn += '\n' return toreturn def __str__(self): return self._info() def __repr__(self): return self._info() def __add__(self, other): """ Ability to add two SpectralRegion classes together. """ return SpectralRegion(self._subregions + other._subregions) def __iadd__(self, other): """ Ability to add one SpectralRegion to another using +=. """ self._subregions += other._subregions self._reorder() return self def __len__(self): """ Number of spectral regions. """ return len(self._subregions) def __getslice__(self, item): """ Enable slicing of the SpectralRegion list. """ return SpectralRegion(self._subregions[item]) def __getitem__(self, item): """ Enable slicing or extracting the SpectralRegion. """ if isinstance(item, slice): return self.__getslice__(item) else: return SpectralRegion([self._subregions[item]]) def __delitem__(self, item): """ Delete a specific item from the list. """ del self._subregions[item] def _valid(self): # Lower bound < Upper bound for all sub regions if any(x[0] >= x[1] for x in self._subregions): raise ValueError('Lower bound must be strictly less than the upper bound') return True def _is_2_element(self, value): """ Helper function to check a variable to see if it is a 2-tuple. """ return len(value) == 2 and \ isinstance(value[0], u.Quantity) and \ isinstance(value[1], u.Quantity) def _reorder(self): """ Re-order the list based on lower bounds. """ self._subregions.sort(key=lambda k: k[0]) @property def subregions(self): return self._subregions @property def bounds(self): """ Compute the lower and upper extent of the SpectralRegion. """ return (self.lower, self.upper) @property def lower(self): """ The most minimum value of the sub-regions. The sub-regions are ordered based on the lower bound, so the lower bound for this instance is the lower bound of the first sub-region. """ return self._subregions[0][0] @property def upper(self): """ The most maximum value of the sub-regions. The sub-regions are ordered based on the lower bound, but the upper bound might not be the upper bound of the last sub-region so we have to look for it. """ return max(x[1] for x in self._subregions) def invert_from_spectrum(self, spectrum): """ Invert a SpectralRegion based on the extent of the input spectrum. See notes in SpectralRegion.invert() method. """ return self.invert(spectrum.spectral_axis[0], spectrum.spectral_axis[-1]) def _in_range(self, value, lower, upper): return (value >= lower) and (value <= upper) def invert(self, lower_bound, upper_bound): """ Invert this spectral region. That is, given a set of sub-regions this object defines, create a new `SpectralRegion` such that the sub-regions are defined in the new one as regions *not* in this `SpectralRegion`. Parameters ---------- lower : Scalar `~astropy.units.Quantity` with pixel or any valid ``spectral_axis`` unit The lower bound of the region. upper : Scalar `~astropy.units.Quantity` with pixel or any valid ``spectral_axis`` unit The upper bound of the region. Returns ------- spectral_region : `~specutils.SpectralRegion` Spectral region of the non-selected regions Notes ----- This is applicable if, for example, a `SpectralRegion` has sub-regions defined for peaks in a spectrum and then one wants to create a `SpectralRegion` defined as all the *non*-peaks, then one could use this function. As an example, assume this SpectralRegion is defined as ``sr = SpectralRegion([(0.45*u.um, 0.6*u.um), (0.8*u.um, 0.9*u.um)])``. If we call ``sr_invert = sr.invert(0.3*u.um, 1.0*u.um)`` then ``sr_invert`` will be ``SpectralRegion([(0.3*u.um, 0.45*u.um), (0.6*u.um, 0.8*u.um), (0.9*u.um, 1*u.um)])`` """ # # Create 'rs' region list with left and right extra ranges. # min_num = -sys.maxsize-1 max_num = sys.maxsize rs = self._subregions + [(min_num*u.um, lower_bound), (upper_bound, max_num*u.um)] # # Sort the region list based on lower bound. # sorted_regions = sorted(rs, key=lambda k: k[0]) # # Create new region list that has overlapping regions merged # merged = [] for higher in sorted_regions: if not merged: merged.append(higher) else: lower = merged[-1] # test for intersection between lower and higher: # we know via sorting that lower[0] <= higher[0] if higher[0] <= lower[1]: upper_bound = max(lower[1], higher[1]) merged[-1] = (lower[0], upper_bound) # replace by merged interval else: merged.append(higher) # # Create new list and drop first and last (the maxsize ones). # We go from -inf, upper1, lower2, upper2.... # and remap to lower1, upper1, lower2, ... # newlist = list(itertools.chain.from_iterable(merged)) newlist = newlist[1:-1] # # Now create new Spectrum region # return SpectralRegion([(x, y) for x, y in zip(newlist[0::2], newlist[1::2])]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/spectra/spectrum1d.py0000644000076500000240000004035100000000000021571 0ustar00erikstaff00000000000000import logging from copy import deepcopy import numpy as np from astropy import units as u from astropy import constants as cnst from astropy.nddata import NDDataRef, NDUncertainty from astropy.utils.decorators import lazyproperty from astropy.nddata import NDUncertainty from .spectrum_mixin import OneDSpectrumMixin from ..utils.wcs_utils import gwcs_from_array __all__ = ['Spectrum1D'] __doctest_skip__ = ['Spectrum1D.spectral_resolution'] class Spectrum1D(OneDSpectrumMixin, NDDataRef): """ Spectrum container for 1D spectral data. Parameters ---------- flux : `astropy.units.Quantity` or astropy.nddata.NDData`-like The flux data for this spectrum. spectral_axis : `astropy.units.Quantity` Dispersion information with the same shape as the last (or only) dimension of flux. wcs : `astropy.wcs.WCS` or `gwcs.wcs.WCS` WCS information object. velocity_convention : {"doppler_relativistic", "doppler_optical", "doppler_radio"} Convention used for velocity conversions. rest_value : `~astropy.units.Quantity` Any quantity supported by the standard spectral equivalencies (wavelength, energy, frequency, wave number). Describes the rest value of the spectral axis for use with velocity conversions. redshift See `redshift` for more information. radial_velocity See `radial_velocity` for more information. uncertainty : `~astropy.nddata.NDUncertainty` Contains uncertainty information along with propagation rules for spectrum arithmetic. Can take a unit, but if none is given, will use the unit defined in the flux. meta : dict Arbitrary container for any user-specific information to be carried around with the spectrum container object. """ def __init__(self, flux=None, spectral_axis=None, wcs=None, velocity_convention=None, rest_value=None, redshift=None, radial_velocity=None, **kwargs): # Check for pre-defined entries in the kwargs dictionary. unknown_kwargs = set(kwargs).difference( {'data', 'unit', 'uncertainty', 'meta', 'mask', 'copy'}) if len(unknown_kwargs) > 0: raise ValueError("Initializer contains unknown arguments(s): {}." "".format(', '.join(map(str, unknown_kwargs)))) # If the flux (data) argument is a subclass of nddataref (as it would # be for internal arithmetic operations), avoid setup entirely. if isinstance(flux, NDDataRef): self._velocity_convention = flux._velocity_convention self._rest_value = flux._rest_value super(Spectrum1D, self).__init__(flux) return # Ensure that the flux argument is an astropy quantity if flux is not None: if not isinstance(flux, u.Quantity): raise ValueError("Flux must be a `Quantity` object.") elif flux.isscalar: flux = u.Quantity([flux]) # In cases of slicing, new objects will be initialized with `data` # instead of ``flux``. Ensure we grab the `data` argument. if flux is None and 'data' in kwargs: flux = kwargs.pop('data') # Ensure that the unit information codified in the quantity object is # the One True Unit. kwargs.setdefault('unit', flux.unit if isinstance(flux, u.Quantity) else kwargs.get('unit')) # In the case where the arithmetic operation is being performed with # a single float, int, or array object, just go ahead and ignore wcs # requirements if isinstance(flux, float) or isinstance(flux, int) \ or not isinstance(flux, u.Quantity): super(Spectrum1D, self).__init__(data=flux, wcs=wcs, **kwargs) return # Attempt to parse the spectral axis. If none is given, try instead to # parse a given wcs. This is put into a GWCS object to # then be used behind-the-scenes for all specutils operations. if spectral_axis is not None: # Ensure that the spectral axis is an astropy quantity if not isinstance(spectral_axis, u.Quantity): raise ValueError("Spectral axis must be a `Quantity` object.") wcs = gwcs_from_array(spectral_axis) elif wcs is None: # If no spectral axis or wcs information is provided, initialize a # with an empty gwcs based on the flux. size = len(flux) if not flux.isscalar else 1 wcs = gwcs_from_array(np.arange(size) * u.Unit("")) # Check to make sure the wavelength length is the same in both if flux is not None and spectral_axis is not None: if not spectral_axis.shape[0] == flux.shape[-1]: raise ValueError('Spectral axis ({}) and the last flux axis ({}) lengths must be the same'.format( spectral_axis.shape[0], flux.shape[-1])) self._velocity_convention = velocity_convention if rest_value is None: if hasattr(wcs, 'rest_frequency') and wcs.rest_frequency != 0: self._rest_value = wcs.rest_frequency * u.Hz elif hasattr(wcs, 'rest_wavelength') and wcs.rest_wavelength != 0: self._rest_value = wcs.rest_wavelength * u.AA else: self._rest_value = 0 * u.AA else: self._rest_value = rest_value if not isinstance(self._rest_value, u.Quantity): logging.info("No unit information provided with rest value. " "Assuming units of spectral axis ('%s').", spectral_axis.unit) self._rest_value = u.Quantity(rest_value, spectral_axis.unit) elif not self._rest_value.unit.is_equivalent(u.AA) \ and not self._rest_value.unit.is_equivalent(u.Hz): raise u.UnitsError("Rest value must be " "energy/wavelength/frequency equivalent.") super(Spectrum1D, self).__init__( data=flux.value if isinstance(flux, u.Quantity) else flux, wcs=wcs, **kwargs) # set redshift after super() - necessary because the shape-checking # requires that the flux be initialized if redshift is None: self.radial_velocity = radial_velocity elif radial_velocity is None: self.redshift = redshift else: raise ValueError('cannot set both radial_velocity and redshift at ' 'the same time.') if hasattr(self, 'uncertainty') and self.uncertainty is not None: if not flux.shape == self.uncertainty.array.shape: raise ValueError('Flux axis ({}) and uncertainty ({}) shapes must be the same.'.format( flux.shape, self.uncertainty.array.shape)) def __getitem__(self, item): """ Override the class indexer. We do this here because there are two cases for slicing on a ``Spectrum1D``: 1.) When the flux is one dimensional, indexing represents a single flux value at a particular spectral axis bin, and returns a new ``Spectrum1D`` where all attributes are sliced. 2.) When flux is multi-dimensional (i.e. several fluxes over the same spectral axis), indexing returns a new ``Spectrum1D`` with the sliced flux range and a deep copy of all other attributes. The first case is handled by the parent class, while the second is handled here. """ if len(self.flux.shape) > 1: return self._copy( flux=self.flux[item], uncertainty=self.uncertainty[item] if self.uncertainty is not None else None) if not isinstance(item, slice): item = slice(item, item+1, None) return super().__getitem__(item) def _copy(self, **kwargs): """ Peform deep copy operations on each attribute of the ``Spectrum1D`` object. """ alt_kwargs = dict( flux=deepcopy(self.flux), spectral_axis=deepcopy(self.spectral_axis), uncertainty=deepcopy(self.uncertainty), wcs=deepcopy(self.wcs), mask=deepcopy(self.mask), meta=deepcopy(self.meta), unit=deepcopy(self.unit), velocity_convention=deepcopy(self.velocity_convention), rest_value=deepcopy(self.rest_value)) alt_kwargs.update(kwargs) return self.__class__(**alt_kwargs) @property def frequency(self): """ The frequency as a `~astropy.units.Quantity` in units of GHz """ return self.spectral_axis.to(u.GHz, u.spectral()) @property def wavelength(self): """ The wavelength as a `~astropy.units.Quantity` in units of Angstroms """ return self.spectral_axis.to(u.AA, u.spectral()) @property def energy(self): """ The energy of the spectral axis as a `~astropy.units.Quantity` in units of eV. """ return self.spectral_axis.to(u.eV, u.spectral()) @property def photon_flux(self): """ The flux density of photons as a `~astropy.units.Quantity`, in units of photons per cm^2 per second per spectral_axis unit """ flux_in_spectral_axis_units = self.flux.to( u.W * u.cm**-2 * self.spectral_axis.unit**-1, u.spectral_density(self.spectral_axis)) photon_flux_density = flux_in_spectral_axis_units / (self.energy / u.photon) return photon_flux_density.to(u.photon * u.cm**-2 * u.s**-1 * self.spectral_axis.unit**-1) @lazyproperty def bin_edges(self): return self.wcs.bin_edges() @property def shape(self): return self.flux.shape @property def redshift(self): """ The redshift(s) of the objects represented by this spectrum. May be scalar (if this spectrum's ``flux`` is 1D) or vector. Note that the concept of "redshift of a spectrum" can be ambiguous, so the interpretation is set to some extent by either the user, or operations (like template fitting) that set this attribute when they are run on a spectrum. """ return self._radial_velocity/cnst.c @redshift.setter def redshift(self, val): if val is None: self._radial_velocity = None else: self.radial_velocity = val * cnst.c @property def radial_velocity(self): """ The radial velocity(s) of the objects represented by this spectrum. May be scalar (if this spectrum's ``flux`` is 1D) or vector. Note that the concept of "RV of a spectrum" can be ambiguous, so the interpretation is set to some extent by either the user, or operations (like template fitting) that set this attribute when they are run on a spectrum. """ return self._radial_velocity @radial_velocity.setter def radial_velocity(self, val): if val is None: self._radial_velocity = None else: if not val.unit.is_equivalent(u.km/u.s): raise u.UnitsError('radial_velocity must be a velocity') # the trick below checks if the two shapes given are broadcastable onto # each other. See https://stackoverflow.com/questions/47243451/checking-if-two-arrays-are-broadcastable-in-python input_shape = val.shape flux_shape = self.flux.shape[:-1] if not all((m == n) or (m == 1) or (n == 1) for m, n in zip(input_shape[::-1], flux_shape)): raise ValueError("radial_velocity or redshift must have shape that " "is compatible with this spectrum's flux array") self._radial_velocity = val def __add__(self, other): if not isinstance(other, NDDataRef): other = u.Quantity(other, unit=self.unit) return self.add(other) def __sub__(self, other): if not isinstance(other, NDDataRef): other = u.Quantity(other, unit=self.unit) return self.subtract(other) def __mul__(self, other): if not isinstance(other, NDDataRef): other = u.Quantity(other) return self.multiply(other) def __div__(self, other): if not isinstance(other, NDDataRef): other = u.Quantity(other) return self.divide(other) def __truediv__(self, other): if not isinstance(other, NDDataRef): other = u.Quantity(other) return self.divide(other) def _format_array_summary(self, label, array): if len(array) == 1: mean = np.mean(array) s = "{:17} [ {:.5} ], mean={:.5}" return s.format(label+':', array[0], array[-1], mean) elif len(array) > 1: mean = np.mean(array) s = "{:17} [ {:.5}, ..., {:.5} ], mean={:.5}" return s.format(label+':', array[0], array[-1], mean) else: return "{:17} [ ], mean= n/a".format(label+':') def __str__(self): result = "Spectrum1D " # Handle case of single value flux if self.flux.ndim == 0: result += "(length=1)\n" return result + "flux: {}".format(self.flux) # Handle case of multiple flux arrays result += "(length={})\n".format(len(self.spectral_axis)) if self.flux.ndim > 1: for i, flux in enumerate(self.flux): label = 'flux{:2}'.format(i) result += self._format_array_summary(label, flux) + '\n' else: result += self._format_array_summary('flux', self.flux) + '\n' # Add information about spectral axis result += self._format_array_summary('spectral axis', self.spectral_axis) # Add information about uncertainties if available if self.uncertainty: result += "\nuncertainty: [ {}, ..., {} ]".format( self.uncertainty[0], self.uncertainty[-1]) return result def __repr__(self): inner_str = "flux={}, spectral_axis={}".format(repr(self.flux), repr(self.spectral_axis)) if self.uncertainty is not None: inner_str += ", uncertainty={}".format(repr(self.uncertainty)) result = "".format(inner_str) return result def spectral_resolution(self, true_dispersion, delta_dispersion, axis=-1): """Evaluate the probability distribution of the spectral resolution. Examples -------- To tabulate a binned resolution function at 6000A covering +/-10A in 0.2A steps: >>> R = spectrum1d.spectral_resolution( ... 6000 * u.Angstrom, np.linspace(-10, 10, 51) * u.Angstrom) >>> assert R.shape == (50,) >>> assert np.allclose(R.sum(), 1.) To build a sparse resolution matrix for true wavelengths 4000-8000A in 0.1A steps: >>> R = spectrum1d.spectral_resolution( ... np.linspace(4000, 8000, 40001)[:, np.newaxis] * u.Angstrom, ... np.linspace(-10, +10, 201) * u.Angstrom) >>> assert R.shape == (40000, 200) >>> assert np.allclose(R.sum(axis=1), 1.) Parameters ---------- true_dispersion : `~astropy.units.Quantity` True value(s) of dispersion for which the resolution should be evaluated. delta_dispersion : `~astropy.units.Quantity` Array of (observed - true) dispersion bin edges to integrate the resolution probability density over. axis : int Which axis of ``delta_dispersion`` contains the strictly increasing dispersion values to interpret as bin edges. The dimension of ``delta_dispersion`` along this axis must be at least two. Returns ------- numpy array Array of dimensionless probabilities calculated as the integral of P(observed | true) over each bin in (observed - true). The output shape is the result of broadcasting the input shapes. """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/spectra/spectrum_collection.py0000644000076500000240000002224300000000000023557 0ustar00erikstaff00000000000000import logging import astropy.units as u import numpy as np from astropy.nddata import NDUncertainty, StdDevUncertainty from .spectrum1d import Spectrum1D __all__ = ['SpectrumCollection'] class SpectrumCollection: """ A class to represent a heterogeneous set of spectra that are the same length but have different spectral axes. Spectra that meet this requirement can be stored as multidimensional arrays, and thus can have operations performed on them faster than if they are treated as individual :class:`~specutils.Spectrum1D` objects. The attributes on this class uses the same names and conventions as :class:`~specutils.Spectrum1D`, allowing some operations to work the same. Where this does not work, the user can use standard indexing notation to access individual :class:`~specutils.Spectrum1D` objects. Parameters ---------- flux : :class:`astropy.units.Quantity` The flux data. The trailing dimension should be the spectral dimension. spectral_axis : :class:`astropy.units.Quantity` The spectral axes of the spectra (e.g., wavelength). Must match the dimensionality of ``flux``. wcs : wcs object or None A wcs object (if available) for the collection of spectra. The object must follow standard indexing rules to get a sub-wcs if it is provided. uncertainty : :class:`astropy.nddata.NDUncertainty` or ndarray The uncertainties associated with each spectrum of the collection. In the case that only an n-dimensional quantity or ndaray is provided, the uncertainties are assumed to be standard deviations. Must match the dimensionality of ``flux``. mask : ndarray or None The n-dimensional mask information associated with each spectrum. If present, must match the dimensionality of ``flux``. meta : list The list of dictionaries containing meta data to be associated with each spectrum in the collection. """ def __init__(self, flux, spectral_axis=None, wcs=None, uncertainty=None, mask=None, meta=None): # Check for quantity if not isinstance(flux, u.Quantity): raise u.UnitsError("Flux must be a `Quantity`.") if spectral_axis is not None: if not isinstance(spectral_axis, u.Quantity): raise u.UnitsError("Spectral axis must be a `Quantity`.") # Ensure that the input values are the same shape if not (flux.shape == spectral_axis.shape): raise ValueError("Shape of all data elements must be the same.") if uncertainty is not None and uncertainty.array.shape != flux.shape: raise ValueError("Uncertainty must be the same shape as flux and " "spectral axis.") if mask is not None and mask.shape != flux.shape: raise ValueError("Mask must be the same shape as flux and " "spectral axis.") # Convert uncertainties to standard deviations if not already defined # to be of some type if uncertainty is not None and not isinstance(uncertainty, NDUncertainty): # If the uncertainties are not provided a unit, raise a warning # and use the flux units if not isinstance(uncertainty, u.Quantity): logging.warning("No unit associated with uncertainty, assuming" "flux units of '{}'.".format(flux.unit)) uncertainty = u.Quantity(uncertainty, unit=flux.unit) uncertainty = StdDevUncertainty(uncertainty) self._flux = flux self._spectral_axis = spectral_axis self._wcs = wcs self._uncertainty = uncertainty self._mask = mask self._meta = meta def __getitem__(self, key): flux = self.flux[key] if flux.ndim != 1: raise ValueError("Currently only 1D data structures may be " "returned from slice operations.") spectral_axis = self.spectral_axis[key] uncertainty = None if self.uncertainty is None else self.uncertainty[key] wcs = None if self.wcs is None else self.wcs[key] mask = None if self.mask is None else self.mask[key] if self.meta is None: meta = None else: try: meta = self.meta[key] except KeyError: meta = self.meta return Spectrum1D(flux=flux, spectral_axis=spectral_axis, uncertainty=uncertainty, wcs=wcs, mask=mask, meta=meta) @classmethod def from_spectra(cls, spectra): """ Create a spectrum collection from a set of individual :class:`specutils.Spectrum1D` objects. Parameters ---------- spectra : list, ndarray A list of :class:`~specutils.Spectrum1D` objects to be held in the collection. """ # Enforce that the shape of each item must be the same if not all((x.shape == spectra[0].shape for x in spectra)): raise ValueError("Shape of all elements must be the same.") # Compose multi-dimensional ndarrays for each property flux = np.vstack( [spec.flux.value for spec in spectra]) * spectra[0].flux.unit spectral_axis = np.vstack( [spec.spectral_axis.value for spec in spectra]) * spectra[0].spectral_axis.unit # Check that either all spectra have associated uncertainties, or that # none of them do. If only some do, log an error and ignore the # uncertainties. if not all((x.uncertainty is None for x in spectra)) and \ any((x.uncertainty is not None for x in spectra)): uncertainty = spectra[0].uncertainty.__class__( np.vstack([spec.uncertainty.array for spec in spectra]), unit=spectra[0].uncertainty.unit) else: uncertainty = None logging.warning("Not all spectra have associated uncertainties, " "skipping uncertainties.") # Check that either all spectra have associated masks, or that # none of them do. If only some do, log an error and ignore the masks. if not all((x.mask is None for x in spectra)) and \ any((x.mask is not None for x in spectra)): mask = np.vstack([spec.mask for spec in spectra]) else: mask = None logging.warning("Not all spectra have associated masks, " "skipping masks.") # Store the wcs and meta as lists wcs = [spec.wcs for spec in spectra] meta = [spec.meta for spec in spectra] return cls(flux=flux, spectral_axis=spectral_axis, uncertainty=uncertainty, wcs=wcs, mask=mask, meta=meta) @property def flux(self): """The flux in the spectrum as a `~astropy.units.Quantity`.""" return self._flux @property def spectral_axis(self): """The spectral axes as a `~astropy.units.Quantity`.""" return self._spectral_axis @property def frequency(self): """ The spectral axis as a frequency `~astropy.units.Quantity` (in GHz). """ return self.spectral_axis.to(u.GHz, u.spectral()) @property def wavelength(self): """ The spectral axis as a wavelength `~astropy.units.Quantity` (in Angstroms). """ return self.spectral_axis.to(u.AA, u.spectral()) @property def energy(self): """ The spectral axis as an energy `~astropy.units.Quantity` (in eV). """ return self.spectral_axis.to(u.eV, u.spectral()) @property def wcs(self): """The WCS's as an object array""" return self._wcs @property def uncertainty(self): """The uncertainty in the spectrum as a `~astropy.units.Quantity`.""" return self._uncertainty @property def mask(self): """The mask array for the spectrum.""" return self._mask @property def meta(self): """A dictionary of metadata for theis spectrum collection, or `None`.""" return self._meta @property def shape(self): """ The shape of the collection. This is *not* the same as the shape of the flux et al., because the trailing (spectral) dimension is not included here. """ return self.flux.shape[:-1] @property def ndim(self): """ The dimensionality of the collection. This is *not* the same as the dimensionality of the flux et al., because the trailing (spectral) dimension is not included here. """ return self.flux.ndim - 1 @property def nspectral(self): """ The length of the spectral dimension. """ return self.flux.shape[-1] def __repr__(self): return """SpectrumCollection(ndim={}, shape={}) Flux units: {} Spectral axis units: {} Uncertainty type: {}""".format( self.ndim, self.shape, self.flux.unit, self.spectral_axis.unit, self.uncertainty.uncertainty_type if self.uncertainty is not None else None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/spectra/spectrum_list.py0000644000076500000240000000051700000000000022377 0ustar00erikstaff00000000000000from astropy.nddata import NDIOMixin __all__ = ['SpectrumList'] class SpectrumList(list, NDIOMixin): """ A list that is used to hold a list of Spectrum1D objects The primary purpose of this class is to allow loaders to return a list of heterogenous spectra that do have a spectral axis of the same length. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/spectra/spectrum_mixin.py0000644000076500000240000003253300000000000022553 0ustar00erikstaff00000000000000import logging from copy import deepcopy import astropy.units.equivalencies as eq import numpy as np from astropy import units as u from astropy.utils.decorators import lazyproperty from astropy.wcs.wcsapi import HighLevelWCSWrapper from specutils.utils.wcs_utils import gwcs_from_array DOPPLER_CONVENTIONS = {} DOPPLER_CONVENTIONS['radio'] = u.doppler_radio DOPPLER_CONVENTIONS['optical'] = u.doppler_optical DOPPLER_CONVENTIONS['relativistic'] = u.doppler_relativistic __all__ = ['OneDSpectrumMixin'] class OneDSpectrumMixin: @property def _spectral_axis_numpy_index(self): return self.data.ndim - 1 - self.wcs.wcs.spec @property def _spectral_axis_len(self): """ How many elements are in the spectral dimension? """ return self.data.shape[self._spectral_axis_numpy_index] @property def _data_with_spectral_axis_last(self): """ Returns a view of the data with the spectral axis last """ if self._spectral_axis_numpy_index == self.data.ndim - 1: return self.data else: return self.data.swapaxes(self._spectral_axis_numpy_index, self.data.ndim - 1) @property def _data_with_spectral_axis_first(self): """ Returns a view of the data with the spectral axis first """ if self._spectral_axis_numpy_index == 0: return self.data else: return self.data.swapaxes(self._spectral_axis_numpy_index, 0) @property def spectral_wcs(self): """ Returns the spectral axes of the WCS """ return self.wcs.axes.spectral @lazyproperty def spectral_axis(self): """ Returns a Quantity array with the values of the spectral axis. """ if len(self.flux) > 0: spectral_axis = self.wcs.pixel_to_world(np.arange(self.flux.shape[-1])) else: # After some discussion it was suggested to create the empty # spectral axis this way to better use the WCS infrastructure. # This is to prepare for a future where pixel_to_world might yield # something more than just a raw Quantity, which is planned for # the mid-term in astropy and possible gwcs. Such changes might # necessitate a revisit of this code. dummy_spectrum = self.__class__( spectral_axis=[1, 2] * self.spectral_axis_unit, flux=[1, 2] * self.flux.unit) spectral_axis = dummy_spectrum.wcs.pixel_to_world([0])[1:] return spectral_axis @property def spectral_axis_unit(self): """ Returns the units of the spectral axis. """ if isinstance(self.wcs, HighLevelWCSWrapper): return u.Unit(self.wcs.world_axis_units[0]) return self.wcs.unit[0] @property def flux(self): """ Converts the stored data and unit information into a quantity. Returns ------- `~astropy.units.Quantity` Spectral data as a quantity. """ return u.Quantity(self.data, unit=self.unit, copy=False) def new_flux_unit(self, unit, equivalencies=None, suppress_conversion=False): """ Converts the flux data to the specified unit. This is an in-place change to the object. Parameters ---------- unit : str or `~astropy.units.Unit` The unit to convert the flux array to. equivalencies : list of equivalencies Custom equivalencies to apply to conversions. Set to spectral_density by default. suppress_conversion : bool Set to true if updating the unit without converting data values. Returns ------- `~specutils.Spectrum1D` A new spectrum with the converted flux array """ new_spec = deepcopy(self) if not suppress_conversion: if equivalencies is None: equivalencies = eq.spectral_density(self.spectral_axis) new_data = self.flux.to( unit, equivalencies=equivalencies) new_spec._data = new_data.value new_spec._unit = new_data.unit else: new_spec._unit = u.Unit(unit) return new_spec @property def velocity_convention(self): """ Returns the velocity convention """ return self._velocity_convention def with_velocity_convention(self, velocity_convention): return self.__class__(flux=self.flux, wcs=self.wcs, meta=self.meta, velocity_convention=velocity_convention) @property def rest_value(self): return self._rest_value @rest_value.setter def rest_value(self, value): if not hasattr(value, 'unit') or not value.unit.is_equivalent(u.Hz, u.spectral()): raise u.UnitsError( "Rest value must be energy/wavelength/frequency equivalent.") self._rest_value = value @property def velocity(self): """ Converts the spectral axis array to the given velocity space unit given the rest value. These aren't input parameters but required Spectrum attributes Parameters ---------- unit : str or ~`astropy.units.Unit` The unit to convert the dispersion array to. rest : ~`astropy.units.Quantity` Any quantity supported by the standard spectral equivalencies (wavelength, energy, frequency, wave number). type : {"doppler_relativistic", "doppler_optical", "doppler_radio"} The type of doppler spectral equivalency. redshift or radial_velocity If present, this shift is applied to the final output velocity to get into the rest frame of the object. Returns ------- ~`astropy.units.Quantity` The converted dispersion array in the new dispersion space. """ if not hasattr(self, '_rest_value') or self._rest_value is None: raise ValueError("Cannot get velocity representation of spectral " "axis without specifying a reference value.") if not hasattr(self, '_velocity_convention') or self._velocity_convention is None: raise ValueError("Cannot get velocity representation of spectral " "axis without specifying a velocity convention.") equiv = getattr(u.equivalencies, 'doppler_{0}'.format( self.velocity_convention))(self.rest_value) new_data = self.spectral_axis.to(u.km/u.s, equivalencies=equiv) # if redshift/rv is present, apply it: if self._radial_velocity is not None: new_data += self.radial_velocity return new_data def with_spectral_unit(self, unit, velocity_convention=None, rest_value=None): """ Returns a new spectrum with a different spectral axis unit. Parameters ---------- unit : :class:`~astropy.units.Unit` Any valid spectral unit: velocity, (wave)length, or frequency. Only vacuum units are supported. velocity_convention : 'relativistic', 'radio', or 'optical' The velocity convention to use for the output velocity axis. Required if the output type is velocity. This can be either one of the above strings, or an `astropy.units` equivalency. rest_value : :class:`~astropy.units.Quantity` A rest wavelength or frequency with appropriate units. Required if output type is velocity. The spectrum's WCS should include this already if the *input* type is velocity, but the WCS's rest wavelength/frequency can be overridden with this parameter. .. note: This must be the rest frequency/wavelength *in vacuum*, even if your spectrum has air wavelength units """ new_wcs, new_meta = self._new_spectral_wcs( unit=unit, velocity_convention=velocity_convention or self._velocity_convention, rest_value=rest_value or self.rest_value) spectrum = self.__class__(flux=self.flux, wcs=new_wcs, meta=new_meta) return spectrum def _new_wcs_argument_validation(self, unit, velocity_convention, rest_value): # Allow string specification of units, for example if not isinstance(unit, u.UnitBase): unit = u.Unit(unit) # Velocity conventions: required for frq <-> velo # convert_spectral_axis will handle the case of no velocity # convention specified & one is required if velocity_convention in DOPPLER_CONVENTIONS: velocity_convention = DOPPLER_CONVENTIONS[velocity_convention] elif (velocity_convention is not None and velocity_convention not in DOPPLER_CONVENTIONS.values()): raise ValueError("Velocity convention must be radio, optical, " "or relativistic.") # If rest value is specified, it must be a quantity if (rest_value is not None and (not hasattr(rest_value, 'unit') or not rest_value.unit.is_equivalent(u.m, u.spectral()))): raise ValueError("Rest value must be specified as an astropy " "quantity with spectral equivalence.") return unit def _new_spectral_wcs(self, unit, velocity_convention=None, rest_value=None): """ Returns a new WCS with a different Spectral Axis unit. Parameters ---------- unit : :class:`~astropy.units.Unit` Any valid spectral unit: velocity, (wave)length, or frequency. Only vacuum units are supported. velocity_convention : 'relativistic', 'radio', or 'optical' The velocity convention to use for the output velocity axis. Required if the output type is velocity. This can be either one of the above strings, or an `astropy.units` equivalency. rest_value : :class:`~astropy.units.Quantity` A rest wavelength or frequency with appropriate units. Required if output type is velocity. The cube's WCS should include this already if the *input* type is velocity, but the WCS's rest wavelength/frequency can be overridden with this parameter. .. note: This must be the rest frequency/wavelength *in vacuum*, even if your cube has air wavelength units """ unit = self._new_wcs_argument_validation(unit, velocity_convention, rest_value) if velocity_convention is not None: equiv = getattr(u, 'doppler_{0}'.format(velocity_convention)) rest_value.to(unit, equivalencies=equiv) # Store the original unit information for posterity meta = self._meta.copy() if 'original_unit' not in self._meta: meta['original_unit'] = self.wcs.unit[0] # Create the new wcs object if isinstance(unit, u.UnitBase) and unit.is_equivalent( self.wcs.unit[0], equivalencies=u.spectral()): return gwcs_from_array(self.spectral_axis), meta logging.error("WCS units incompatible: {} and {}.".format( unit, self._wcs_unit)) class InplaceModificationMixin: # Example methods follow to demonstrate how methods can be written to be # agnostic of the non-spectral dimensions. def substract_background(self, background): """ Proof of concept, this subtracts a background spectrum-wise """ data = self._data_with_spectral_axis_last if callable(background): # create substractable array pass elif (isinstance(background, np.ndarray) and background.shape == data[-1].shape): substractable_continuum = background else: raise ValueError( "background needs to be callable or have the same shape as the spectum") data[-1] -= substractable_continuum def normalize(self): """ Proof of concept, this normalizes each spectral dimension based on a trapezoidal integration. """ # this gets a view - if we want normalize to return a new NDData object # then we should make _data_with_spectral_axis_first return a copy. data = self._data_with_spectral_axis_first dx = np.diff(self.spectral_axis) dy = 0.5 * (data[:-1] + data[1:]) norm = np.sum(dx * dy.transpose(), axis=-1).transpose() data /= norm def spectral_interpolation(self, spectral_value, flux_unit=None): """ Proof of concept, this interpolates along the spectral dimension """ data = self._data_with_spectral_axis_last from scipy.interpolate import interp1d interp = interp1d(self.spectral_axis.value, data) x = spectral_value.to(self.spectral_axis.unit, equivalencies=u.spectral()) y = interp(x) if self.unit is not None: y *= self.unit if flux_unit is None: # Lim: Is this acceptable? return y else: return y.to(flux_unit, equivalencies=u.spectral_density(x)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/tests/0000755000076500000240000000000000000000000016626 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/specutils/tests/__init__.py0000644000076500000240000000017100000000000020736 0ustar00erikstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This packages contains affiliated package tests. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/tests/conftest.py0000644000076500000240000000210200000000000021020 0ustar00erikstaff00000000000000import pytest import os import urllib import tempfile remote_access = lambda argvals: pytest.mark.parametrize( 'remote_data_path', argvals, indirect=True, scope='function') @pytest.fixture(scope='module') def remote_data_path(request): """ Remotely access the Zenodo deposition archive to retrieve the versioned test data. """ # Make use of configuration option from pytest-remotedata in order to # control access to remote data. if request.config.getoption('remote_data', 'any') != 'any': pytest.skip() file_id, file_name = request.param.values() url = "https://zenodo.org/record/{}/files/{}?download=1".format( file_id, file_name) # Create a temporary directory that is automatically cleaned up when the # context is exited, removing any temporarily stored data. with tempfile.TemporaryDirectory() as tmp_dir: file_path = os.path.join(tmp_dir, file_name) with urllib.request.urlopen(url) as r, open(file_path, 'wb') as tmp_file: tmp_file.write(r.read()) yield file_path ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/specutils/tests/coveragerc0000644000076500000240000000123200000000000020667 0ustar00erikstaff00000000000000[run] source = specutils omit = specutils/*__init__* specutils/_astropy_init.py specutils/conftest* specutils/cython_version* specutils/*setup* specutils/*tests/* specutils/version* specutils/extern/* [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} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1537452672.0 specutils-0.7/specutils/tests/setup_package.py0000644000076500000240000000022100000000000022006 0ustar00erikstaff00000000000000import os def get_package_data(): paths = ['coveragerc', os.path.join('data', '*fits')] return {'specutils.tests': paths} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/tests/spectral_cube_test_spectral_axis.py0000644000076500000240000005776500000000000026017 0ustar00erikstaff00000000000000from astropy import wcs from astropy.io import fits from astropy import units as u from astropy import constants from astropy.tests.helper import pytest import warnings import os import numpy as np from .helpers import assert_allclose from . import path as data_path from ..spectral_axis import (convert_spectral_axis, determine_ctype_from_vconv, cdelt_derivative, determine_vconv_from_ctype, get_rest_value_from_wcs, air_to_vac, air_to_vac_deriv, vac_to_air) def test_cube_wcs_freqtovel(): header = fits.Header.fromtextfile(data_path('cubewcs1.hdr')) w1 = wcs.WCS(header) # CTYPE3 = 'FREQ' newwcs = convert_spectral_axis(w1, 'km/s', 'VRAD', rest_value=w1.wcs.restfrq*u.Hz) assert newwcs.wcs.ctype[2] == 'VRAD' assert newwcs.wcs.crval[2] == 305.2461585938794 assert newwcs.wcs.cunit[2] == u.Unit('km/s') newwcs = convert_spectral_axis(w1, 'km/s', 'VRAD') assert newwcs.wcs.ctype[2] == 'VRAD' assert newwcs.wcs.crval[2] == 305.2461585938794 assert newwcs.wcs.cunit[2] == u.Unit('km/s') def test_cube_wcs_freqtovopt(): header = fits.Header.fromtextfile(data_path('cubewcs1.hdr')) w1 = wcs.WCS(header) w2 = convert_spectral_axis(w1, 'km/s', 'VOPT') # TODO: what should w2's values be? test them # these need to be set to zero to test the failure w1.wcs.restfrq = 0.0 w1.wcs.restwav = 0.0 with pytest.raises(ValueError) as exc: convert_spectral_axis(w1, 'km/s', 'VOPT') assert exc.value.args[0] == 'If converting from wavelength/frequency to speed, a reference wavelength/frequency is required.' @pytest.mark.parametrize('wcstype',('Z','W','R','V')) def test_greisen2006(wcstype): # This is the header extracted from Greisen 2006, including many examples # of valid transforms. It should be the gold standard (in principle) hdr = fits.Header.fromtextfile(data_path('greisen2006.hdr')) # We have not implemented frame conversions, so we can only convert bary # <-> bary in this case wcs0 = wcs.WCS(hdr, key='F') wcs1 = wcs.WCS(hdr, key=wcstype) if wcstype in ('R','V','Z'): if wcs1.wcs.restfrq: rest = wcs1.wcs.restfrq*u.Hz elif wcs1.wcs.restwav: rest = wcs1.wcs.restwav*u.m else: rest = None outunit = u.Unit(wcs1.wcs.cunit[wcs1.wcs.spec]) out_ctype = wcs1.wcs.ctype[wcs1.wcs.spec] wcs2 = convert_spectral_axis(wcs0, outunit, out_ctype, rest_value=rest) assert_allclose(wcs2.wcs.cdelt[wcs2.wcs.spec], wcs1.wcs.cdelt[wcs1.wcs.spec], rtol=1.e-3) assert_allclose(wcs2.wcs.crval[wcs2.wcs.spec], wcs1.wcs.crval[wcs1.wcs.spec], rtol=1.e-3) assert wcs2.wcs.ctype[wcs2.wcs.spec] == wcs1.wcs.ctype[wcs1.wcs.spec] assert wcs2.wcs.cunit[wcs2.wcs.spec] == wcs1.wcs.cunit[wcs1.wcs.spec] # round trip test: inunit = u.Unit(wcs0.wcs.cunit[wcs0.wcs.spec]) in_ctype = wcs0.wcs.ctype[wcs0.wcs.spec] wcs3 = convert_spectral_axis(wcs2, inunit, in_ctype, rest_value=rest) assert_allclose(wcs3.wcs.crval[wcs3.wcs.spec], wcs0.wcs.crval[wcs0.wcs.spec], rtol=1.e-3) assert_allclose(wcs3.wcs.cdelt[wcs3.wcs.spec], wcs0.wcs.cdelt[wcs0.wcs.spec], rtol=1.e-3) assert wcs3.wcs.ctype[wcs3.wcs.spec] == wcs0.wcs.ctype[wcs0.wcs.spec] assert wcs3.wcs.cunit[wcs3.wcs.spec] == wcs0.wcs.cunit[wcs0.wcs.spec] def test_byhand_f2v(): # VELO-F2V CRVAL3F = 1.37847121643E+09 CDELT3F = 9.764775E+04 RESTFRQV= 1.420405752E+09 CRVAL3V = 8.98134229811E+06 CDELT3V = -2.1217551E+04 CUNIT3V = 'm/s' CUNIT3F = 'Hz' crvalf = CRVAL3F * u.Unit(CUNIT3F) crvalv = CRVAL3V * u.Unit(CUNIT3V) restfreq = RESTFRQV * u.Unit(CUNIT3F) cdeltf = CDELT3F * u.Unit(CUNIT3F) cdeltv = CDELT3V * u.Unit(CUNIT3V) # (Pdb) crval_in,crval_lin1,crval_lin2,crval_out # (, , , ) (Pdb) # cdelt_in, cdelt_lin1, cdelt_lin2, cdelt_out # (, , , ) crvalv_computed = crvalf.to(CUNIT3V, u.doppler_relativistic(restfreq)) cdeltv_computed = -4*constants.c*cdeltf*crvalf*restfreq**2 / (crvalf**2+restfreq**2)**2 cdeltv_computed_byfunction = cdelt_derivative(crvalf, cdeltf, intype='frequency', outtype='speed', rest=restfreq) # this should be EXACT assert cdeltv_computed == cdeltv_computed_byfunction assert_allclose(crvalv_computed, crvalv, rtol=1.e-3) assert_allclose(cdeltv_computed, cdeltv, rtol=1.e-3) # round trip # (Pdb) crval_in,crval_lin1,crval_lin2,crval_out # (, , # , ) # (Pdb) cdelt_in, cdelt_lin1, cdelt_lin2, cdelt_out # (, , , ) crvalf_computed = crvalv_computed.to(CUNIT3F, u.doppler_relativistic(restfreq)) cdeltf_computed = -(cdeltv_computed * constants.c * restfreq / ((constants.c+crvalv_computed)*(constants.c**2 - crvalv_computed**2)**0.5)) assert_allclose(crvalf_computed, crvalf, rtol=1.e-2) assert_allclose(cdeltf_computed, cdeltf, rtol=1.e-2) cdeltf_computed_byfunction = cdelt_derivative(crvalv_computed, cdeltv_computed, intype='speed', outtype='frequency', rest=restfreq) # this should be EXACT assert cdeltf_computed == cdeltf_computed_byfunction def test_byhand_vrad(): # VRAD CRVAL3F = 1.37847121643E+09 CDELT3F = 9.764775E+04 RESTFRQR= 1.420405752E+09 CRVAL3R = 8.85075090419E+06 CDELT3R = -2.0609645E+04 CUNIT3R = 'm/s' CUNIT3F = 'Hz' crvalf = CRVAL3F * u.Unit(CUNIT3F) crvalv = CRVAL3R * u.Unit(CUNIT3R) restfreq = RESTFRQR * u.Unit(CUNIT3F) cdeltf = CDELT3F * u.Unit(CUNIT3F) cdeltv = CDELT3R * u.Unit(CUNIT3R) # (Pdb) crval_in,crval_lin1,crval_lin2,crval_out # (, , , ) # (Pdb) cdelt_in, cdelt_lin1, cdelt_lin2, cdelt_out # (, , , ) crvalv_computed = crvalf.to(CUNIT3R, u.doppler_radio(restfreq)) cdeltv_computed = -(cdeltf / restfreq)*constants.c assert_allclose(crvalv_computed, crvalv, rtol=1.e-3) assert_allclose(cdeltv_computed, cdeltv, rtol=1.e-3) crvalf_computed = crvalv_computed.to(CUNIT3F, u.doppler_radio(restfreq)) cdeltf_computed = -(cdeltv_computed/constants.c) * restfreq assert_allclose(crvalf_computed, crvalf, rtol=1.e-3) assert_allclose(cdeltf_computed, cdeltf, rtol=1.e-3) # round trip: # (Pdb) crval_in,crval_lin1,crval_lin2,crval_out # (, , , ) # (Pdb) cdelt_in, cdelt_lin1, cdelt_lin2, cdelt_out # (, , , ) # (Pdb) myunit,lin_cunit,out_lin_cunit,outunit # WRONG (Unit("m / s"), Unit("m / s"), Unit("Hz"), Unit("Hz")) def test_byhand_vopt(): # VOPT: case "Z" CRVAL3F = 1.37847121643E+09 CDELT3F = 9.764775E+04 CUNIT3F = 'Hz' RESTWAVZ= 0.211061139 #CTYPE3Z = 'VOPT-F2W' # This comes from Greisen 2006, but appears to be wrong: CRVAL3Z = 9.120000E+06 CRVAL3Z = 9.120002206E+06 CDELT3Z = -2.1882651E+04 CUNIT3Z = 'm/s' crvalf = CRVAL3F * u.Unit(CUNIT3F) crvalv = CRVAL3Z * u.Unit(CUNIT3Z) restwav = RESTWAVZ * u.m cdeltf = CDELT3F * u.Unit(CUNIT3F) cdeltv = CDELT3Z * u.Unit(CUNIT3Z) # Forward: freq -> vopt # crval: (, , , ) # cdelt: (, , , ) #crvalv_computed = crvalf.to(CUNIT3R, u.doppler_radio(restwav)) crvalw_computed = crvalf.to(u.m, u.spectral()) crvalw_computed32 = crvalf.astype('float32').to(u.m, u.spectral()) cdeltw_computed = -(cdeltf / crvalf**2)*constants.c cdeltw_computed_byfunction = cdelt_derivative(crvalf, cdeltf, intype='frequency', outtype='length', rest=None) # this should be EXACT assert cdeltw_computed == cdeltw_computed_byfunction crvalv_computed = crvalw_computed.to(CUNIT3Z, u.doppler_optical(restwav)) crvalv_computed32 = crvalw_computed32.astype('float32').to(CUNIT3Z, u.doppler_optical(restwav)) #cdeltv_computed = (cdeltw_computed * # 4*constants.c*crvalw_computed*restwav**2 / # (restwav**2+crvalw_computed**2)**2) cdeltv_computed = (cdeltw_computed / restwav)*constants.c cdeltv_computed_byfunction = cdelt_derivative(crvalw_computed, cdeltw_computed, intype='length', outtype='speed', rest=restwav, linear=True) # Disagreement is 2.5e-7: good, but not really great... #assert np.abs((crvalv_computed-crvalv)/crvalv) < 1e-6 assert_allclose(crvalv_computed, crvalv, rtol=1.e-2) assert_allclose(cdeltv_computed, cdeltv, rtol=1.e-2) # Round=trip test: # from velo_opt -> freq # (, , , ) # (, , , ) crvalw_computed = crvalv_computed.to(u.m, u.doppler_optical(restwav)) cdeltw_computed = (cdeltv_computed/constants.c) * restwav cdeltw_computed_byfunction = cdelt_derivative(crvalv_computed, cdeltv_computed, intype='speed', outtype='length', rest=restwav, linear=True) assert cdeltw_computed == cdeltw_computed_byfunction crvalf_computed = crvalw_computed.to(CUNIT3F, u.spectral()) cdeltf_computed = -cdeltw_computed * constants.c / crvalw_computed**2 assert_allclose(crvalf_computed, crvalf, rtol=1.e-3) assert_allclose(cdeltf_computed, cdeltf, rtol=1.e-3) cdeltf_computed_byfunction = cdelt_derivative(crvalw_computed, cdeltw_computed, intype='length', outtype='frequency', rest=None) assert cdeltf_computed == cdeltf_computed_byfunction # Fails intentionally (but not really worth testing) #crvalf_computed = crvalv_computed.to(CUNIT3F, u.spectral()+u.doppler_optical(restwav)) #cdeltf_computed = -(cdeltv_computed / constants.c) * restwav.to(u.Hz, u.spectral()) #assert_allclose(crvalf_computed, crvalf, rtol=1.e-3) #assert_allclose(cdeltf_computed, cdeltf, rtol=1.e-3) def test_byhand_f2w(): CRVAL3F = 1.37847121643E+09 CDELT3F = 9.764775E+04 CUNIT3F = 'Hz' #CTYPE3W = 'WAVE-F2W' CRVAL3W = 0.217481841062 CDELT3W = -1.5405916E-05 CUNIT3W = 'm' crvalf = CRVAL3F * u.Unit(CUNIT3F) crvalw = CRVAL3W * u.Unit(CUNIT3W) cdeltf = CDELT3F * u.Unit(CUNIT3F) cdeltw = CDELT3W * u.Unit(CUNIT3W) crvalf_computed = crvalw.to(CUNIT3F, u.spectral()) cdeltf_computed = -constants.c * cdeltw / crvalw**2 assert_allclose(crvalf_computed, crvalf, rtol=0.1) assert_allclose(cdeltf_computed, cdeltf, rtol=0.1) @pytest.mark.parametrize(('ctype','unit','velocity_convention','result'), (('VELO-F2V', "Hz", None, 'FREQ'), ('VELO-F2V', "m", None, 'WAVE-F2W'), ('VOPT', "m", None, 'WAVE'), ('VOPT', "Hz", None, 'FREQ-W2F'), ('VELO', "Hz", None, 'FREQ-V2F'), ('WAVE', "Hz", None, 'FREQ-W2F'), ('FREQ', 'm/s', None, ValueError('A velocity convention must be specified')), ('FREQ', 'm/s', u.doppler_radio, 'VRAD'), ('FREQ', 'm/s', u.doppler_optical, 'VOPT-F2W'), ('FREQ', 'm/s', u.doppler_relativistic, 'VELO-F2V'), ('WAVE', 'm/s', u.doppler_radio, 'VRAD-W2F'))) def test_ctype_determinator(ctype,unit,velocity_convention,result): if isinstance(result, Exception): with pytest.raises(Exception) as exc: determine_ctype_from_vconv(ctype, unit, velocity_convention=velocity_convention) assert exc.value.args[0] == result.args[0] assert type(exc.value) == type(result) else: outctype = determine_ctype_from_vconv(ctype, unit, velocity_convention=velocity_convention) assert outctype == result @pytest.mark.parametrize(('ctype','vconv'), (('VELO-F2W', u.doppler_optical), ('VELO-F2V', u.doppler_relativistic), ('VRAD', u.doppler_radio), ('VOPT', u.doppler_optical), ('VELO', u.doppler_relativistic), ('WAVE', u.doppler_optical), ('WAVE-F2W', u.doppler_optical), ('WAVE-V2W', u.doppler_optical), ('FREQ', u.doppler_radio), ('FREQ-V2F', u.doppler_radio), ('FREQ-W2F', u.doppler_radio),)) def test_vconv_determinator(ctype, vconv): assert determine_vconv_from_ctype(ctype) == vconv @pytest.mark.parametrize(('name'), (('advs'), ('dvsa'), ('sdav'), ('sadv'), ('vsad'), ('vad'), ('adv'), )) def test_vopt_to_freq(name): h = fits.getheader(data_path(name+".fits")) wcs0 = wcs.WCS(h) # check to make sure astropy.wcs's "fix" changes VELO-HEL to VOPT assert wcs0.wcs.ctype[wcs0.wcs.spec] == 'VOPT' out_ctype = determine_ctype_from_vconv('VOPT', u.Hz) wcs1 = convert_spectral_axis(wcs0, u.Hz, out_ctype) assert wcs1.wcs.ctype[wcs1.wcs.spec] == 'FREQ-W2F' @pytest.mark.parametrize('wcstype',('Z','W','R','V','F')) def test_change_rest_frequency(wcstype): # This is the header extracted from Greisen 2006, including many examples # of valid transforms. It should be the gold standard (in principle) hdr = fits.Header.fromtextfile(data_path('greisen2006.hdr')) wcs0 = wcs.WCS(hdr, key=wcstype) old_rest = get_rest_value_from_wcs(wcs0) if old_rest is None: # This test doesn't matter if there was no rest frequency in the first # place but I prefer to keep the option open in case we want to try # forcing a rest frequency on some of the non-velocity frames at some # point return vconv1 = determine_vconv_from_ctype(hdr['CTYPE3'+wcstype]) new_rest = (100*u.km/u.s).to(u.Hz, vconv1(old_rest)) wcs1 = wcs.WCS(hdr, key='V') vconv2 = determine_vconv_from_ctype(hdr['CTYPE3V']) inunit = u.Unit(wcs0.wcs.cunit[wcs0.wcs.spec]) outunit = u.Unit(wcs1.wcs.cunit[wcs1.wcs.spec]) # VELO-F2V out_ctype = wcs1.wcs.ctype[wcs1.wcs.spec] wcs2 = convert_spectral_axis(wcs0, outunit, out_ctype, rest_value=new_rest) sp1 = wcs1.sub([wcs.WCSSUB_SPECTRAL]) sp2 = wcs2.sub([wcs.WCSSUB_SPECTRAL]) p_old = sp1.wcs_world2pix([old_rest.to(inunit, vconv1(old_rest)).value, new_rest.to(inunit, vconv1(old_rest)).value],0) p_new = sp2.wcs_world2pix([old_rest.to(outunit, vconv2(new_rest)).value, new_rest.to(outunit, vconv2(new_rest)).value],0) assert_allclose(p_old, p_new, rtol=1e-3) assert_allclose(p_old, p_new, rtol=1e-3) # from http://classic.sdss.org/dr5/products/spectra/vacwavelength.html # these aren't accurate enough for my liking, but I can't find a better one readily air_vac = { 'H-beta':(4861.363, 4862.721)*u.AA, '[O III]':(4958.911, 4960.295)*u.AA, '[O III]':(5006.843, 5008.239)*u.AA, '[N II]':(6548.05, 6549.86)*u.AA, 'H-alpha':(6562.801, 6564.614)*u.AA, '[N II]':(6583.45, 6585.27)*u.AA, '[S II]':(6716.44, 6718.29)*u.AA, '[S II]':(6730.82, 6732.68)*u.AA, } @pytest.mark.parametrize(('air','vac'), air_vac.values()) def test_air_to_vac(air, vac): # This is the accuracy provided by the line list we have. # I'm not sure if the formula are incorrect or if the reference wavelengths # are, but this is an accuracy of only 6 km/s, which is *very bad* for # astrophysical applications. assert np.abs((air_to_vac(air)- vac)) < 0.15*u.AA assert np.abs((vac_to_air(vac)- air)) < 0.15*u.AA assert np.abs((air_to_vac(air)- vac)/vac) < 2e-5 assert np.abs((vac_to_air(vac)- air)/air) < 2e-5 # round tripping assert np.abs((vac_to_air(air_to_vac(air))-air))/air < 1e-8 assert np.abs((air_to_vac(vac_to_air(vac))-vac))/vac < 1e-8 def test_byhand_awav2vel(): # AWAV CRVAL3A = (6560*u.AA).to(u.m).value CDELT3A = (1.0*u.AA).to(u.m).value CUNIT3A = 'm' CRPIX3A = 1.0 # restwav MUST be vacuum restwl = air_to_vac(6562.81*u.AA) RESTWAV = restwl.to(u.m).value CRVAL3V = (CRVAL3A*u.m).to(u.m/u.s, u.doppler_optical(restwl)).value CDELT3V = (CDELT3A*u.m*air_to_vac_deriv(CRVAL3A*u.m)/restwl) * constants.c CUNIT3V = 'm/s' mywcs = wcs.WCS(naxis=1) mywcs.wcs.ctype[0] = 'AWAV' mywcs.wcs.crval[0] = CRVAL3A mywcs.wcs.crpix[0] = CRPIX3A mywcs.wcs.cunit[0] = CUNIT3A mywcs.wcs.cdelt[0] = CDELT3A mywcs.wcs.restwav = RESTWAV mywcs.wcs.set() newwcs = convert_spectral_axis(mywcs, u.km/u.s, determine_ctype_from_vconv(mywcs.wcs.ctype[0], u.km/u.s, 'optical')) newwcs.wcs.set() assert newwcs.wcs.cunit[0] == 'm / s' np.testing.assert_almost_equal(newwcs.wcs.crval, air_to_vac(CRVAL3A*u.m).to(u.m/u.s, u.doppler_optical(restwl)).value) # Check that the cdelts match the expected cdelt, 1 angstrom / rest # wavelength (vac) np.testing.assert_almost_equal(newwcs.wcs.cdelt, CDELT3V.to(u.m/u.s).value) # Check that the reference wavelength is 2.81 angstroms up np.testing.assert_almost_equal(newwcs.wcs_pix2world((2.81,), 0), 0.0, decimal=3) # Go through a full-on sanity check: vline = 100*u.km/u.s wave_line_vac = vline.to(u.AA, u.doppler_optical(restwl)) wave_line_air = vac_to_air(wave_line_vac) pix_line_input = mywcs.wcs_world2pix((wave_line_air.to(u.m).value,), 0) pix_line_output = newwcs.wcs_world2pix((vline.to(u.m/u.s).value,), 0) np.testing.assert_almost_equal(pix_line_output, pix_line_input, decimal=4) def test_byhand_awav2wav(): # AWAV CRVAL3A = (6560*u.AA).to(u.m).value CDELT3A = (1.0*u.AA).to(u.m).value CUNIT3A = 'm' CRPIX3A = 1.0 mywcs = wcs.WCS(naxis=1) mywcs.wcs.ctype[0] = 'AWAV' mywcs.wcs.crval[0] = CRVAL3A mywcs.wcs.crpix[0] = CRPIX3A mywcs.wcs.cunit[0] = CUNIT3A mywcs.wcs.cdelt[0] = CDELT3A mywcs.wcs.set() newwcs = convert_spectral_axis(mywcs, u.AA, 'WAVE') newwcs.wcs.set() np.testing.assert_almost_equal(newwcs.wcs_pix2world((0,),0), air_to_vac(mywcs.wcs_pix2world((0,),0)*u.m).value) np.testing.assert_almost_equal(newwcs.wcs_pix2world((10,),0), air_to_vac(mywcs.wcs_pix2world((10,),0)*u.m).value) # At least one of the components MUST change assert not (mywcs.wcs.crval[0] == newwcs.wcs.crval[0] and mywcs.wcs.crpix[0] == newwcs.wcs.crpix[0]) class test_nir_sinfoni_base: def setup_method(self, method): CD3_3 = 0.000245000002905726 # CD rotation matrix CTYPE3 = 'WAVE ' # wavelength axis in microns CRPIX3 = 1109. # Reference pixel in z CRVAL3 = 2.20000004768372 # central wavelength CDELT3 = 0.000245000002905726 # microns per pixel CUNIT3 = 'um ' # spectral unit SPECSYS = 'TOPOCENT' # Coordinate reference frame self.rest_wavelength = 2.1218*u.um self.mywcs = wcs.WCS(naxis=1) self.mywcs.wcs.ctype[0] = CTYPE3 self.mywcs.wcs.crval[0] = CRVAL3 self.mywcs.wcs.crpix[0] = CRPIX3 self.mywcs.wcs.cunit[0] = CUNIT3 self.mywcs.wcs.cdelt[0] = CDELT3 self.mywcs.wcs.cd = [[CD3_3]] self.mywcs.wcs.specsys = SPECSYS self.mywcs.wcs.set() self.wavelengths = np.array([[2.12160005e-06, 2.12184505e-06, 2.12209005e-06]]) np.testing.assert_almost_equal(self.mywcs.wcs_pix2world([788,789,790], 0), self.wavelengths) def test_nir_sinfoni_example_optical(self): mywcs = self.mywcs.copy() velocities_opt = ((self.wavelengths*u.m-self.rest_wavelength)/(self.wavelengths*u.m) * constants.c).to(u.km/u.s) newwcs_opt = convert_spectral_axis(mywcs, u.km/u.s, 'VOPT', rest_value=self.rest_wavelength) assert newwcs_opt.wcs.cunit[0] == u.km/u.s newwcs_opt.wcs.set() worldpix_opt = newwcs_opt.wcs_pix2world([788,789,790], 0) assert newwcs_opt.wcs.cunit[0] == u.m/u.s np.testing.assert_almost_equal(worldpix_opt, velocities_opt.to(newwcs_opt.wcs.cunit[0]).value) def test_nir_sinfoni_example_radio(self): mywcs = self.mywcs.copy() velocities_rad = ((self.wavelengths*u.m-self.rest_wavelength)/(self.rest_wavelength) * constants.c).to(u.km/u.s) newwcs_rad = convert_spectral_axis(mywcs, u.km/u.s, 'VRAD', rest_value=self.rest_wavelength) assert newwcs_rad.wcs.cunit[0] == u.km/u.s newwcs_rad.wcs.set() worldpix_rad = newwcs_rad.wcs_pix2world([788,789,790], 0) assert newwcs_rad.wcs.cunit[0] == u.m/u.s np.testing.assert_almost_equal(worldpix_rad, velocities_rad.to(newwcs_rad.wcs.cunit[0]).value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578138041.0 specutils-0.7/specutils/tests/spectral_examples.py0000644000076500000240000001246500000000000022723 0ustar00erikstaff00000000000000from copy import copy import numpy as np import astropy.units as u from astropy.modeling import models from ..spectra import Spectrum1D import pytest class SpectraExamples: """ The ``SpectralExamples`` class is a *container class* that has several examples of simple spectra that are to be used in the tests (e.g., arithmetic tests, smoothing tests etc). The purpose of this being a test class instead of using a `Spectrum1D` directly is that it contains both the `Spectrum1D` object and the flux that was used to *create* the Spectrum. That's for tests that ensure the simpler operations just on the flux arrays are carried through to the `Spectrum1D` operations. Each of the spectra are created from a base noise-less spectrum constructed from 4 Gaussians and a ramp. Then three example spectra are created, and then gaussian random noise is added. 1. s1_um_mJy_e1 - 4 Gaussians + ramp with one instantion of noise dispersion: um, flux: mJy 2. s1_um_mJy_e2 - same as 1, but with a different instance of noise dispersion: um, flux: mJy 3. s1_AA_mJy_e3 - same as 1, but with a third instance of noise dispersion: Angstroms, flux: mJy 4. s1_AA_nJy_e3 - same as 1, but with a fourth instance of noise dispersion: Angstroms, flux: nJy 5. s1_um_mJy_e1_masked - same as 1, but with a random set of pixels masked. """ def __init__(self): # # Create the base wavelengths and flux # self.wavelengths_um = np.linspace(0.4, 1.05, 100) g1 = models.Gaussian1D(amplitude=2000, mean=0.56, stddev=0.01) g2 = models.Gaussian1D(amplitude=500, mean=0.62, stddev=0.02) g3 = models.Gaussian1D(amplitude=-400, mean=0.80, stddev=0.02) g4 = models.Gaussian1D(amplitude=-350, mean=0.52, stddev=0.01) ramp = models.Linear1D(slope=300, intercept=0.0) self.base_flux = g1(self.wavelengths_um) + g2(self.wavelengths_um) + \ g3(self.wavelengths_um) + g4(self.wavelengths_um) + \ ramp(self.wavelengths_um) + 1000 # # Initialize the seed so the random numbers are not quite as random # np.random.seed(42) # # Create two spectra with the only difference in the instance of noise # self._flux_e1 = self.base_flux + 400 * np.random.random(self.base_flux.shape) self._s1_um_mJy_e1 = Spectrum1D(spectral_axis=self.wavelengths_um * u.um, flux=self._flux_e1 * u.mJy) self._flux_e2 = self.base_flux + 400 * np.random.random(self.base_flux.shape) self._s1_um_mJy_e2 = Spectrum1D(spectral_axis=self.wavelengths_um * u.um, flux=self._flux_e2 * u.mJy) # # Create one spectrum with the same flux but in angstrom units # self.wavelengths_AA = self.wavelengths_um * 10000 self._s1_AA_mJy_e3 = Spectrum1D(spectral_axis=self.wavelengths_AA * u.AA, flux=self._flux_e1 * u.mJy) # # Create one spectrum with the same flux but in angstrom units and nJy # self._flux_e4 = (self.base_flux + 400 * np.random.random(self.base_flux.shape)) * 1000000 self._s1_AA_nJy_e4 = Spectrum1D(spectral_axis=self.wavelengths_AA * u.AA, flux=self._flux_e4 * u.nJy) # # Create one spectrum like 1 but with a mask # self._s1_um_mJy_e1_masked = copy(self._s1_um_mJy_e1) # SHALLOW copy - the data are shared with the above non-masked case self._s1_um_mJy_e1_masked.mask = (np.random.randn(*self.base_flux.shape) + 1) > 0 @property def s1_um_mJy_e1(self): return self._s1_um_mJy_e1 @property def s1_um_mJy_e1_flux(self): return self._flux_e1 @property def s1_um_mJy_e2(self): return self._s1_um_mJy_e2 @property def s1_um_mJy_e2_flux(self): return self._flux_e2 @property def s1_AA_mJy_e3(self): return self._s1_AA_mJy_e3 @property def s1_AA_mJy_e3_flux(self): return self._flux_e1 @property def s1_AA_nJy_e4(self): return self._s1_AA_nJy_e4 @property def s1_AA_nJy_e4_flux(self): return self._flux_e4 @property def s1_um_mJy_e1_masked(self): return self._s1_um_mJy_e1_masked @pytest.fixture def simulated_spectra(): """ The method will be called as a fixture to tests. Parameters ---------- N/A Return ------ ``SpectralExamples`` An instance of the SpectraExamples class. Examples -------- This fixture can be used in a test as: ``` from .spectral_examples import spectral_examples def test_add_spectra(spectral_examples): # Get the numpy array of data flux1 = define_spectra.s1_um_mJy_e1_flux flux2 = define_spectra.s1_um_mJy_e2_flux flux3 = flux1 + flux2 # Calculate using the spectrum1d/nddata code spec3 = define_spectra.s1_um_mJy_e1 + define_spectra.s1_um_mJy_e2 assert np.allclose(spec3.flux.value, flux3) ``` """ return SpectraExamples() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/tests/test_analysis.py0000644000076500000240000004647000000000000022075 0ustar00erikstaff00000000000000import pytest import numpy as np import astropy.units as u from astropy.modeling import models from astropy.nddata import StdDevUncertainty from astropy.stats.funcs import gaussian_sigma_to_fwhm from astropy.tests.helper import quantity_allclose from astropy.utils.exceptions import AstropyUserWarning from ..spectra import Spectrum1D, SpectralRegion from ..analysis import (line_flux, equivalent_width, snr, centroid, gaussian_sigma_width, gaussian_fwhm, fwhm, snr_derived, fwzi, is_continuum_below_threshold) from ..fitting import find_lines_threshold from ..tests.spectral_examples import simulated_spectra def test_line_flux(): np.random.seed(42) frequencies = np.linspace(1, 100, 10000) * u.GHz g = models.Gaussian1D(amplitude=1*u.Jy, mean=10*u.GHz, stddev=1*u.GHz) noise = np.random.normal(0., 0.01, frequencies.shape) * u.Jy flux = g(frequencies) + noise spectrum = Spectrum1D(spectral_axis=frequencies, flux=flux) result = line_flux(spectrum) assert result.unit.is_equivalent(u.erg / u.cm**2 / u.s) # Account for the fact that Astropy uses a different normalization of the # Gaussian where the integral is not 1 expected = np.sqrt(2*np.pi) * u.GHz * u.Jy assert quantity_allclose(result, expected, atol=0.01*u.GHz*u.Jy) def test_equivalent_width(): np.random.seed(42) frequencies = np.linspace(1, 100, 10000) * u.GHz g = models.Gaussian1D(amplitude=1*u.Jy, mean=10*u.GHz, stddev=1*u.GHz) noise = np.random.normal(0., 0.01, frequencies.shape) * u.Jy flux = g(frequencies) + noise + 1*u.Jy spectrum = Spectrum1D(spectral_axis=frequencies, flux=flux) result = equivalent_width(spectrum) assert result.unit.is_equivalent(spectrum.wcs.unit) # Since this is an emission line, we expect the equivalent width value to # be negative expected = -(np.sqrt(2*np.pi) * u.GHz) assert quantity_allclose(result, expected, atol=0.01*u.GHz) def test_equivalent_width_regions(): np.random.seed(42) frequencies = np.linspace(1, 100, 10000) * u.GHz g = models.Gaussian1D(amplitude=1*u.Jy, mean=10*u.GHz, stddev=1*u.GHz) noise = np.random.normal(0., 0.001, frequencies.shape) * u.Jy flux = g(frequencies) + noise + 1*u.Jy spec = Spectrum1D(spectral_axis=frequencies, flux=flux) cont_norm_spec = spec / np.median(spec.flux) result = equivalent_width(cont_norm_spec, regions=SpectralRegion(3*u.GHz, 97*u.GHz)) expected = -(np.sqrt(2*np.pi) * u.GHz) assert quantity_allclose(result, expected, atol=0.01*u.GHz) @pytest.mark.parametrize('continuum', [1*u.Jy, 2*u.Jy, 5*u.Jy]) def test_equivalent_width_continuum(continuum): np.random.seed(42) frequencies = np.linspace(1, 100, 10000) * u.GHz g = models.Gaussian1D(amplitude=1*u.Jy, mean=10*u.GHz, stddev=1*u.GHz) noise = np.random.normal(0., 0.01, frequencies.shape) * u.Jy flux = g(frequencies) + noise + continuum spectrum = Spectrum1D(spectral_axis=frequencies, flux=flux) result = equivalent_width(spectrum, continuum=continuum) assert result.unit.is_equivalent(spectrum.wcs.unit) # Since this is an emission line, we expect the equivalent width value to # be negative expected = -(np.sqrt(2*np.pi) * u.GHz) / continuum.value assert quantity_allclose(result, expected, atol=0.01*u.GHz) def test_equivalent_width_absorption(): np.random.seed(42) frequencies = np.linspace(1, 100, 10000) * u.GHz amplitude = 0.5 g = models.Gaussian1D(amplitude=amplitude*u.Jy, mean=10*u.GHz, stddev=1*u.GHz) continuum = 1*u.Jy noise = np.random.normal(0., 0.01, frequencies.shape) * u.Jy flux = continuum - g(frequencies) + noise spectrum = Spectrum1D(spectral_axis=frequencies, flux=flux) result = equivalent_width(spectrum) assert result.unit.is_equivalent(spectrum.wcs.unit) # Since this is an absorption line, we expect the equivalent width value to # be positive expected = amplitude*np.sqrt(2*np.pi) * u.GHz assert quantity_allclose(result, expected, atol=0.01*u.GHz) def test_snr(simulated_spectra): """ Test the simple version of the spectral SNR. """ np.random.seed(42) # # Set up the data and add the uncertainty and calculate the expected SNR # spectrum = simulated_spectra.s1_um_mJy_e1 uncertainty = StdDevUncertainty(0.1*np.random.random(len(spectrum.flux))*u.mJy) spectrum.uncertainty = uncertainty wavelengths = spectrum.spectral_axis flux = spectrum.flux spec_snr_expected = np.mean(flux / (uncertainty.array*uncertainty.unit)) # # SNR of the whole spectrum # spec_snr = snr(spectrum) assert isinstance(spec_snr, u.Quantity) assert np.allclose(spec_snr.value, spec_snr_expected.value) def test_snr_no_uncertainty(simulated_spectra): """ Test the simple version of the spectral SNR. """ # # Set up the data and add the uncertainty and calculate the expected SNR # spectrum = simulated_spectra.s1_um_mJy_e1 with pytest.raises(Exception) as e_info: _ = snr(spectrum) def test_snr_multiple_flux(simulated_spectra): """ Test the simple version of the spectral SNR, with multiple flux per single dispersion. """ np.random.seed(42) # # Set up the data and add the uncertainty and calculate the expected SNR # uncertainty = StdDevUncertainty(0.1*np.random.random((5, 10))*u.mJy) spec = Spectrum1D(spectral_axis=np.arange(10) * u.AA, flux=np.random.sample((5, 10)) * u.Jy, uncertainty=uncertainty) snr_spec = snr(spec) assert np.allclose(np.array(snr_spec), [18.20863867, 31.89475309, 14.51598119, 22.24603204, 32.01461421]) uncertainty = StdDevUncertainty(0.1*np.random.random(10)*u.mJy) spec = Spectrum1D(spectral_axis=np.arange(10) * u.AA, flux=np.random.sample(10) * u.Jy, uncertainty=uncertainty) snr_spec = snr(spec) assert np.allclose(np.array(snr_spec), 31.325265361800415) def test_snr_single_region(simulated_spectra): """ Test the simple version of the spectral SNR over a region of the spectrum. """ np.random.seed(42) region = SpectralRegion(0.52*u.um, 0.59*u.um) # # Set up the data # spectrum = simulated_spectra.s1_um_mJy_e1 uncertainty = StdDevUncertainty(0.1*np.random.random(len(spectrum.flux))*u.mJy) spectrum.uncertainty = uncertainty wavelengths = spectrum.spectral_axis flux = spectrum.flux # +1 because we want to include it in the calculation l = np.nonzero(wavelengths>region.lower)[0][0] r = np.nonzero(wavelengths= region.lower)[0][0] r = np.nonzero(wavelengths <= region.upper)[0][-1]+1 spec_snr_expected.append(np.mean(flux[l:r] / (uncertainty.array[l:r]*uncertainty.unit))) # # SNR of the whole spectrum # spec_snr = snr(spectrum, regions) assert np.allclose(spec_snr, spec_snr_expected) def test_snr_derived(): np.random.seed(42) x = np.arange(1, 101) * u.um y = np.random.random(len(x))*u.Jy spectrum = Spectrum1D(spectral_axis=x, flux=y) assert np.allclose(snr_derived(spectrum), 1.604666860424951) sr = SpectralRegion(38*u.um, 48*u.um) assert np.allclose(snr_derived(spectrum, sr), 2.330463630828406) sr2 = SpectralRegion(48*u.um, 57*u.um) assert np.allclose(snr_derived(spectrum, [sr, sr2]), [2.330463630828406, 2.9673559890209305]) def test_centroid(simulated_spectra): """ Test the simple version of the spectral centroid. """ np.random.seed(42) # # Set up the data and add the uncertainty and calculate the expected SNR # spectrum = simulated_spectra.s1_um_mJy_e1 uncertainty = StdDevUncertainty(0.1*np.random.random(len(spectrum.flux))*u.mJy) spectrum.uncertainty = uncertainty wavelengths = spectrum.spectral_axis flux = spectrum.flux spec_centroid_expected = np.sum(flux * wavelengths) / np.sum(flux) # # SNR of the whole spectrum # spec_centroid = centroid(spectrum, None) assert isinstance(spec_centroid, u.Quantity) assert np.allclose(spec_centroid.value, spec_centroid_expected.value) def test_inverted_centroid(simulated_spectra): """ Ensures the centroid calculation also works for *inverted* spectra - i.e. continuum-subtracted absorption lines. """ spectrum = simulated_spectra.s1_um_mJy_e1 spec_centroid_expected = (np.sum(spectrum.flux * spectrum.spectral_axis) / np.sum(spectrum.flux)) spectrum_inverted = Spectrum1D(spectral_axis=spectrum.spectral_axis, flux=-spectrum.flux) spec_centroid_inverted = centroid(spectrum_inverted, None) assert np.allclose(spec_centroid_inverted.value, spec_centroid_expected.value) def test_centroid_multiple_flux(simulated_spectra): """ Test the simple version of the spectral SNR, with multiple flux per single dispersion. """ # # Set up the data and add the uncertainty and calculate the expected SNR # np.random.seed(42) spec = Spectrum1D(spectral_axis=np.arange(10) * u.um, flux=np.random.sample((5, 10)) * u.Jy) centroid_spec = centroid(spec, None) assert np.allclose(centroid_spec.value, np.array([4.46190995, 4.17223565, 4.37778249, 4.51595259, 4.7429066])) assert centroid_spec.unit == u.um def test_gaussian_sigma_width(): np.random.seed(42) # Create a (centered) gaussian spectrum for testing mean = 5 frequencies = np.linspace(0, mean*2, 100) * u.GHz g1 = models.Gaussian1D(amplitude=5*u.Jy, mean=mean*u.GHz, stddev=0.8*u.GHz) spectrum = Spectrum1D(spectral_axis=frequencies, flux=g1(frequencies)) result = gaussian_sigma_width(spectrum) assert quantity_allclose(result, g1.stddev, atol=0.01*u.GHz) def test_gaussian_sigma_width_regions(): np.random.seed(42) frequencies = np.linspace(0, 100, 10000) * u.GHz g1 = models.Gaussian1D(amplitude=5*u.Jy, mean=10*u.GHz, stddev=0.8*u.GHz) g2 = models.Gaussian1D(amplitude=5*u.Jy, mean=2*u.GHz, stddev=0.3*u.GHz) g3 = models.Gaussian1D(amplitude=5*u.Jy, mean=70*u.GHz, stddev=10*u.GHz) compound = g1 + g2 + g3 spectrum = Spectrum1D(spectral_axis=frequencies, flux=compound(frequencies)) region1 = SpectralRegion(5*u.GHz, 15*u.GHz) result1 = gaussian_sigma_width(spectrum, regions=region1) exp1 = g1.stddev assert quantity_allclose(result1, exp1, atol=0.25*exp1) region2 = SpectralRegion(1*u.GHz, 3*u.GHz) result2 = gaussian_sigma_width(spectrum, regions=region2) exp2 = g2.stddev assert quantity_allclose(result2, exp2, atol=0.25*exp2) region3 = SpectralRegion(40*u.GHz, 100*u.GHz) result3 = gaussian_sigma_width(spectrum, regions=region3) exp3 = g3.stddev assert quantity_allclose(result3, exp3, atol=0.25*exp3) # Test using a list of regions result_list = gaussian_sigma_width(spectrum, regions=[region1, region2, region3]) for model, result in zip((g1, g2, g3), result_list): exp = model.stddev assert quantity_allclose(result, exp, atol=0.25*exp) def test_gaussian_sigma_width_multi_spectrum(): np.random.seed(42) frequencies = np.linspace(0, 100, 10000) * u.GHz g1 = models.Gaussian1D(amplitude=5*u.Jy, mean=50*u.GHz, stddev=0.8*u.GHz) g2 = models.Gaussian1D(amplitude=5*u.Jy, mean=50*u.GHz, stddev=5*u.GHz) g3 = models.Gaussian1D(amplitude=5*u.Jy, mean=50*u.GHz, stddev=10*u.GHz) flux = np.ndarray((3, len(frequencies))) * u.Jy flux[0] = g1(frequencies) flux[1] = g2(frequencies) flux[2] = g3(frequencies) spectra = Spectrum1D(spectral_axis=frequencies, flux=flux) results = gaussian_sigma_width(spectra) expected = (g1.stddev, g2.stddev, g3.stddev) for result, exp in zip(results, expected): assert quantity_allclose(result, exp, atol=0.25*exp) def test_gaussian_fwhm(): np.random.seed(42) # Create a (centered) gaussian spectrum for testing mean = 5 frequencies = np.linspace(0, mean*2, 100) * u.GHz g1 = models.Gaussian1D(amplitude=5*u.Jy, mean=mean*u.GHz, stddev=0.8*u.GHz) spectrum = Spectrum1D(spectral_axis=frequencies, flux=g1(frequencies)) result = gaussian_fwhm(spectrum) expected = g1.stddev * gaussian_sigma_to_fwhm assert quantity_allclose(result, expected, atol=0.01*u.GHz) @pytest.mark.parametrize('mean', range(3,8)) def test_gaussian_fwhm_uncentered(mean): np.random.seed(42) # Create an uncentered gaussian spectrum for testing frequencies = np.linspace(0, 10, 1000) * u.GHz g1 = models.Gaussian1D(amplitude=5*u.Jy, mean=mean*u.GHz, stddev=0.8*u.GHz) spectrum = Spectrum1D(spectral_axis=frequencies, flux=g1(frequencies)) result = gaussian_fwhm(spectrum) expected = g1.stddev * gaussian_sigma_to_fwhm assert quantity_allclose(result, expected, atol=0.05*u.GHz) def test_fwhm(): np.random.seed(42) # Create an (uncentered) spectrum for testing frequencies = np.linspace(0, 10, 1000) * u.GHz stddev = 0.8*u.GHz g1 = models.Gaussian1D(amplitude=5*u.Jy, mean=2*u.GHz, stddev=stddev) spectrum = Spectrum1D(spectral_axis=frequencies, flux=g1(frequencies)) result = fwhm(spectrum) expected = stddev * gaussian_sigma_to_fwhm assert quantity_allclose(result, expected, atol=0.01*u.GHz) # Highest point at the first point wavelengths = np.linspace(1, 10, 100) * u.um flux = (1.0 / wavelengths.value) * u.Jy # highest point first. spectrum = Spectrum1D(spectral_axis=wavelengths, flux=flux) result = fwhm(spectrum) # Note that this makes a little more sense than the previous version; # since the maximum value occurs at wavelength=1, and the half-value of # flux (0.5) occurs at exactly wavelength=2, the result should be # exactly 1 (2 - 1). assert result == 1.0 * u.um # Test the interpolation used in FWHM for wavelength values that are not # on the grid wavelengths = np.linspace(1, 10, 31) * u.um flux = (1.0 / wavelengths.value) * u.Jy # highest point first. spectrum = Spectrum1D(spectral_axis=wavelengths, flux=flux) result = fwhm(spectrum) assert quantity_allclose(result, 1.01 * u.um) # Highest point at the last point wavelengths = np.linspace(1, 10, 100) * u.um flux = wavelengths.value*u.Jy # highest point last. spectrum = Spectrum1D(spectral_axis=wavelengths, flux=flux) result = fwhm(spectrum) assert result == 5*u.um # Flat spectrum wavelengths = np.linspace(1, 10, 100) * u.um flux = np.ones(wavelengths.shape)*u.Jy # highest point last. spectrum = Spectrum1D(spectral_axis=wavelengths, flux=flux) result = fwhm(spectrum) assert result == 9*u.um def test_fwhm_multi_spectrum(): np.random.seed(42) frequencies = np.linspace(0, 100, 10000) * u.GHz stddevs = [0.8, 5, 10]*u.GHz g1 = models.Gaussian1D(amplitude=5*u.Jy, mean=5*u.GHz, stddev=stddevs[0]) g2 = models.Gaussian1D(amplitude=5*u.Jy, mean=50*u.GHz, stddev=stddevs[1]) g3 = models.Gaussian1D(amplitude=5*u.Jy, mean=83*u.GHz, stddev=stddevs[2]) flux = np.ndarray((3, len(frequencies))) * u.Jy flux[0] = g1(frequencies) flux[1] = g2(frequencies) flux[2] = g3(frequencies) spectra = Spectrum1D(spectral_axis=frequencies, flux=flux) results = fwhm(spectra) expected = stddevs * gaussian_sigma_to_fwhm assert quantity_allclose(results, expected, atol=0.01*u.GHz) def test_fwzi(): np.random.seed(42) disp = np.linspace(0, 100, 1000) * u.AA g = models.Gaussian1D(mean=np.mean(disp), amplitude=1 * u.Jy, stddev=2 * u.AA) flux = g(disp) flux_with_noise = g(disp) + ((np.random.sample(disp.size) - 0.5) * 0.1) * u.Jy spec = Spectrum1D(spectral_axis=disp, flux=flux) spec_with_noise = Spectrum1D(spectral_axis=disp, flux=flux_with_noise) assert quantity_allclose(fwzi(spec), 226.89732509 * u.AA) assert quantity_allclose(fwzi(spec_with_noise), 106.99998944 * u.AA) def test_fwzi_multi_spectrum(): np.random.seed(42) disp = np.linspace(0, 100, 1000) * u.AA amplitudes = [0.1, 1, 10] * u.Jy means = [25, 50, 75] * u.AA stddevs = [1, 5, 10] * u.AA params = list(zip(amplitudes, means, stddevs)) flux = np.zeros(shape=(3, len(disp))) for i in range(3): flux[i] = models.Gaussian1D(*params[i])(disp) spec = Spectrum1D(spectral_axis=disp, flux=flux * u.Jy) expected = [113.51706001 * u.AA, 567.21252727 * u.AA, 499.5024546 * u.AA] assert quantity_allclose(fwzi(spec), expected) def test_is_continuum_below_threshold(): # No mask, no uncertainty wavelengths = [300, 500, 1000] * u.nm data = [0.001, -0.003, 0.003] * u.Jy spectrum = Spectrum1D(spectral_axis=wavelengths, flux=data) assert True == is_continuum_below_threshold(spectrum, threshold=0.1*u.Jy) # # No mask, no uncertainty, threshold is float # wavelengths = [300, 500, 1000] * u.nm # data = [0.0081, 0.0043, 0.0072] * u.Jy # spectrum = Spectrum1D(spectral_axis=wavelengths, flux=data) # assert True == is_continuum_below_threshold(spectrum, threshold=0.1) # No mask, with uncertainty wavelengths = [300, 500, 1000] * u.nm data = [0.03, 0.029, 0.031] * u.Jy uncertainty = StdDevUncertainty([1.01, 1.03, 1.01] * u.Jy) spectrum = Spectrum1D(spectral_axis=wavelengths, flux=data, uncertainty=uncertainty) assert True == is_continuum_below_threshold(spectrum, threshold=0.1*u.Jy) # With mask, with uncertainty wavelengths = [300, 500, 1000] * u.nm data = [0.01, 1.029, 0.013] * u.Jy mask = np.array([False, True, False]) uncertainty = StdDevUncertainty([1.01, 1.13, 1.1] * u.Jy) spectrum = Spectrum1D(spectral_axis=wavelengths, flux=data, uncertainty=uncertainty, mask=mask) assert True == is_continuum_below_threshold(spectrum, threshold=0.1*u.Jy) # With mask, with uncertainty -- should throw an exception wavelengths = [300, 500, 1000] * u.nm data = [10.03, 10.029, 10.033] * u.Jy mask = np.array([False, False, False]) uncertainty = StdDevUncertainty([1.01, 1.13, 1.1] * u.Jy) spectrum = Spectrum1D(spectral_axis=wavelengths, flux=data, uncertainty=uncertainty, mask=mask) print('spectrum has flux {}'.format(spectrum.flux)) with pytest.warns(AstropyUserWarning) as e_info: find_lines_threshold(spectrum, noise_factor=1) assert len(e_info)==1 and 'if you want to suppress this warning' in e_info[0].message.args[0].lower() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/tests/test_arithmetic.py0000644000076500000240000000616700000000000022402 0ustar00erikstaff00000000000000import astropy.units as u import numpy as np import pytest from ..spectra.spectrum1d import Spectrum1D from .spectral_examples import simulated_spectra def test_spectral_axes(): flux1 = (np.random.sample(49) * 100).astype(int) flux2 = (np.random.sample(49) * 100).astype(int) flux3 = flux1 + flux2 spec1 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=flux1 * u.Jy) spec2 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=flux2 * u.Jy) spec3 = spec1 + spec2 assert np.allclose(spec3.flux.value, flux3) def test_add_basic_spectra(simulated_spectra): # Get the numpy array of data flux1 = simulated_spectra.s1_um_mJy_e1_flux flux2 = simulated_spectra.s1_um_mJy_e2_flux flux3 = flux1 + flux2 # Calculate using the spectrum1d/nddata code spec3 = simulated_spectra.s1_um_mJy_e1 + simulated_spectra.s1_um_mJy_e2 assert np.allclose(spec3.flux.value, flux3) def test_add_diff_flux_prefix(simulated_spectra): # Get the numpy array of data # this assumes output will be in mJy units flux1 = simulated_spectra.s1_AA_mJy_e3_flux flux2 = simulated_spectra.s1_AA_nJy_e4_flux flux3 = flux1 + (flux2 / 1000000) # Calculate using the spectrum1d/nddata code spec3 = simulated_spectra.s1_AA_mJy_e3 + simulated_spectra.s1_AA_nJy_e4 assert np.allclose(spec3.flux.value, flux3) def test_subtract_basic_spectra(simulated_spectra): # Get the numpy array of data flux1 = simulated_spectra.s1_um_mJy_e1_flux flux2 = simulated_spectra.s1_um_mJy_e2_flux flux3 = flux2 - flux1 # Calculate using the spectrum1d/nddata code spec3 = simulated_spectra.s1_um_mJy_e2 - simulated_spectra.s1_um_mJy_e1 assert np.allclose(spec3.flux.value, flux3) def test_divide_basic_spectra(simulated_spectra): # Get the numpy array of data flux1 = simulated_spectra.s1_um_mJy_e1_flux flux2 = simulated_spectra.s1_um_mJy_e2_flux flux3 = flux1 / flux2 # Calculate using the spectrum1d/nddata code spec3 = simulated_spectra.s1_um_mJy_e1 / simulated_spectra.s1_um_mJy_e2 assert np.allclose(spec3.flux.value, flux3) def test_multiplication_basic_spectra(simulated_spectra): # Get the numpy array of data flux1 = simulated_spectra.s1_um_mJy_e1_flux flux2 = simulated_spectra.s1_um_mJy_e2_flux flux3 = flux1 * flux2 # Calculate using the spectrum1d/nddata code spec3 = simulated_spectra.s1_um_mJy_e1 * simulated_spectra.s1_um_mJy_e2 assert np.allclose(spec3.flux.value, flux3) def test_add_diff_spectral_axis(simulated_spectra): # Calculate using the spectrum1d/nddata code spec3 = simulated_spectra.s1_um_mJy_e1 + simulated_spectra.s1_AA_mJy_e3 def test_masks(simulated_spectra): masked_spec = simulated_spectra.s1_um_mJy_e1_masked masked_sum = masked_spec + masked_spec assert np.all(masked_sum.mask == simulated_spectra.s1_um_mJy_e1_masked.mask) masked_sum.mask[:50] = True masked_diff = masked_sum - masked_spec assert u.allclose(masked_diff.flux, masked_spec.flux) assert np.all(masked_diff.mask == masked_sum.mask | masked_spec.mask) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/tests/test_continuum.py0000644000076500000240000000526600000000000022271 0ustar00erikstaff00000000000000import numpy as np import astropy.units as u from ..spectra.spectrum1d import Spectrum1D from ..fitting.continuum import fit_generic_continuum def single_peak_continuum(): np.random.seed(0) x = np.linspace(0., 10., 200) y_single = 3 * np.exp(-0.5 * (x - 6.3)**2 / 0.1**2) y_single += np.random.normal(0., 0.2, x.shape) y_continuum = 3.2 * np.exp(-0.5 * (x - 5.6)**2 / 4.8**2) y_single += y_continuum return x, y_single def test_continuum_fit(): """ This test fits the the first simulated spectrum from the fixture. The initial guesses are manually set here with bounds that essentially make sense as the functionality of the test is to make sure the fit works and we get a reasonable answer out **given** good initial guesses. """ x_single_continuum, y_single_continuum = single_peak_continuum() s_single_continuum = Spectrum1D(flux=y_single_continuum*u.Jy, spectral_axis=x_single_continuum*u.um) g1_fit = fit_generic_continuum(s_single_continuum) y_continuum_fitted = g1_fit(s_single_continuum.spectral_axis) y_continuum_fitted_expected = np.array([1.71364056, 1.87755574, 2.05310622, 2.23545755, 2.41977527, 2.60122493, 2.77497207, 2.93618225, 3.080021, 3.20165388, 3.29624643, 3.3589642, 3.38497273, 3.36943758, 3.30752428, 3.19439839, 3.02522545, 2.79517101, 2.49940062, 2.13307982]) assert np.allclose(y_continuum_fitted.value[::10], y_continuum_fitted_expected, atol=1e-5) def test_continuum_calculation(): """ This test fits the the first simulated spectrum from the fixture. The initial guesses are manually set here with bounds that essentially make sense as the functionality of the test is to make sure the fit works and we get a reasonable answer out **given** good initial guesses. """ x_single_continuum, y_single_continuum = single_peak_continuum() spectrum = Spectrum1D(flux=y_single_continuum*u.Jy, spectral_axis=x_single_continuum*u.um) g1_fit = fit_generic_continuum(spectrum) spectrum_normalized = spectrum / g1_fit(spectrum.spectral_axis) y_continuum_fitted_expected = np.array([1.15139925, 0.98509363, 0.73700614, 1.00911864, 0.913129, 0.93145533, 0.94904202, 1.04162879, 0.90851397, 0.9494352, 1.07812394, 1.06376489, 0.98705237, 0.94569623, 0.83502377, 0.91909416, 0.89662208, 1.01458511, 0.96124191, 0.94847744]) assert np.allclose(spectrum_normalized.flux.value[::10], y_continuum_fitted_expected, atol=1e-5) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/tests/test_fitting.py0000644000076500000240000006602000000000000021707 0ustar00erikstaff00000000000000import astropy.units as u import numpy as np from astropy.modeling import models from astropy.nddata import StdDevUncertainty from astropy.tests.helper import assert_quantity_allclose from ..analysis import fwhm, centroid from ..fitting import (fit_lines, find_lines_derivative, find_lines_threshold, estimate_line_parameters) from ..manipulation import noise_region_uncertainty, spectrum_from_model from ..spectra import Spectrum1D, SpectralRegion def single_peak(): np.random.seed(0) x = np.linspace(0., 10., 200) y_single = 3 * np.exp(-0.5 * (x - 6.3)**2 / 0.8**2) y_single += np.random.normal(0., 0.2, x.shape) return x, y_single def single_peak_continuum(): np.random.seed(0) x = np.linspace(0., 10., 200) y_single = 3 * np.exp(-0.5 * (x - 6.3)**2 / 0.3**2) y_single += np.random.normal(0., 0.2, x.shape) y_continuum = 3.2 * np.exp(-0.5 * (x - 0.6)**2 / 2.8**2) y_single += y_continuum return x, y_single def single_peak_extra(): x, y_single = single_peak() extra = 4 * np.exp(-0.5 * (x + 8.3)**2 / 0.1**2) y_single_extra = y_single + extra return x, y_single_extra def double_peak(): np.random.seed(42) g1 = models.Gaussian1D(1, 4.6, 0.2) g2 = models.Gaussian1D(2.5, 5.5, 0.1) x = np.linspace(0, 10, 200) y_double = g1(x) + g2(x) + np.random.normal(0., 0.2, x.shape) return x, y_double def double_peak_absorption_and_emission(): np.random.seed(42) g1 = models.Gaussian1D(1, 4.6, 0.2) g2 = models.Gaussian1D(2.5, 5.5, 0.1) g3 = models.Gaussian1D(-1.7, 8.2, 0.1) x = np.linspace(0, 10, 200) y_double = g1(x) + g2(x) + g3(x) + np.random.normal(0., 0.2, x.shape) return x, y_double def test_find_lines_derivative(): # Create the spectrum to fit x_double, y_double = double_peak_absorption_and_emission() spectrum = Spectrum1D(flux=y_double*u.Jy, spectral_axis=x_double*u.um) # Derivative method lines = find_lines_derivative(spectrum, flux_threshold=0.75) emission_lines = lines[lines['line_type'] == 'emission'] absorption_lines = lines[lines['line_type'] == 'absorption'] assert emission_lines['line_center_index'].tolist() == [90, 109] assert absorption_lines['line_center_index'].tolist() == [163] def test_find_lines_threshold(): # Create the spectrum to fit x_double, y_double = double_peak_absorption_and_emission() spectrum = Spectrum1D(flux=y_double*u.Jy, spectral_axis=x_double*u.um) # Derivative method noise_region = SpectralRegion(0*u.um, 3*u.um) spectrum = noise_region_uncertainty(spectrum, noise_region) lines = find_lines_threshold(spectrum, noise_factor=3) emission_lines = lines[lines['line_type'] == 'emission'] absorption_lines = lines[lines['line_type'] == 'absorption'] assert emission_lines['line_center_index'].tolist() == [91, 96, 109, 179] assert absorption_lines['line_center_index'].tolist() == [163] def test_single_peak_estimate(): """ Single Peak fit. """ # Create the spectrum x_single, y_single = single_peak() s_single = Spectrum1D(flux=y_single*u.Jy, spectral_axis=x_single*u.um) # # Estimate parameter Gaussian1D # we give the true values for the Gaussian because it actually *should* # be pretty close to the true values, because it's a Gaussian... # g_init = estimate_line_parameters(s_single, models.Gaussian1D()) assert np.isclose(g_init.amplitude.value, 3., rtol=.2) assert np.isclose(g_init.mean.value, 6.3, rtol=.1) assert np.isclose(g_init.stddev.value, 0.8, rtol=.3) assert g_init.amplitude.unit == u.Jy assert g_init.mean.unit == u.um assert g_init.stddev.unit == u.um # # Estimate parameter Lorentz1D # unlike the Gaussian1D here we do hand-picked comparison values, because # the "single peak" is a Gaussian and therefore the Lorentzian fit shouldn't # be quite right anyway # g_init = estimate_line_parameters(s_single, models.Lorentz1D()) assert np.isclose(g_init.amplitude.value, 3.354169257846847) assert np.isclose(g_init.x_0.value, 6.218588636687762) assert np.isclose(g_init.fwhm.value, 1.6339001193853715) assert g_init.amplitude.unit == u.Jy assert g_init.x_0.unit == u.um assert g_init.fwhm.unit == u.um # # Estimate parameter Voigt1D # g_init = estimate_line_parameters(s_single, models.Voigt1D()) assert np.isclose(g_init.amplitude_L.value, 3.354169257846847) assert np.isclose(g_init.x_0.value, 6.218588636687762) assert np.isclose(g_init.fwhm_L.value, 1.1553418541989058) assert np.isclose(g_init.fwhm_G.value, 1.1553418541989058) assert g_init.amplitude_L.unit == u.Jy assert g_init.x_0.unit == u.um assert g_init.fwhm_L.unit == u.um assert g_init.fwhm_G.unit == u.um # # Estimate parameter RickerWavelet1D # mh = models.RickerWavelet1D estimators = { 'amplitude': lambda s: max(s.flux), 'x_0': lambda s: centroid(s, region=None), 'sigma': lambda s: fwhm(s) } #mh._constraints['parameter_estimator'] = estimators mh.amplitude.estimator = lambda s: max(s.flux) mh.x_0.estimator = lambda s: centroid(s, region=None) mh.sigma.estimator = lambda s: fwhm(s) g_init = estimate_line_parameters(s_single, mh) assert np.isclose(g_init.amplitude.value, 3.354169257846847) assert np.isclose(g_init.x_0.value, 6.218588636687762) assert np.isclose(g_init.sigma.value, 1.6339001193853715) assert g_init.amplitude.unit == u.Jy assert g_init.x_0.unit == u.um assert g_init.sigma.unit == u.um def test_single_peak_fit(): """ Single peak fit """ # Create the spectrum x_single, y_single = single_peak() s_single = Spectrum1D(flux=y_single*u.Jy, spectral_axis=x_single*u.um) # Fit the spectrum g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=6.1*u.um, stddev=1.*u.um) g_fit = fit_lines(s_single, g_init) y_single_fit = g_fit(x_single*u.um) # Comparing every 10th value. y_single_fit_expected = np.array([3.69669474e-13, 3.57992454e-11, 2.36719426e-09, 1.06879318e-07, 3.29498310e-06, 6.93605383e-05, 9.96945607e-04, 9.78431032e-03, 6.55675141e-02, 3.00017760e-01, 9.37356842e-01, 1.99969007e+00, 2.91286375e+00, 2.89719280e+00, 1.96758892e+00, 9.12412206e-01, 2.88900005e-01, 6.24602556e-02, 9.22061121e-03, 9.29427266e-04]) * u.Jy assert np.allclose(y_single_fit.value[::10], y_single_fit_expected.value, atol=1e-5) def test_single_peak_fit_with_uncertainties(): """ Single peak fit """ # Create the spectrum line_mod = models.Gaussian1D(amplitude=100, mean=6563*u.angstrom, stddev=20*u.angstrom) + models.Const1D(10) init_mod = models.Gaussian1D(amplitude=85 * u.Jy, mean=6550*u.angstrom, stddev=30*u.angstrom) + models.Const1D(8 * u.Jy) x = np.linspace(6400, 6700, 300) * u.AA def calculate_rms(x, init_mod, implicit_weights): rms = [] for _ in range(100): ymod = line_mod(x) y = np.random.poisson(ymod) unc = np.sqrt(ymod) spec = Spectrum1D(spectral_axis=x, flux=y * u.Jy, uncertainty=StdDevUncertainty(unc * u.Jy)) weights = 'unc' if implicit_weights else unc ** -2 spec_fit = fit_lines(spec, init_mod, weights=weights) rms.append(np.std(spec_fit(x).value - y)) return np.median(rms) assert np.allclose(calculate_rms(x, init_mod, implicit_weights=True), 5.113708262419985) assert np.allclose(calculate_rms(x, init_mod, implicit_weights=False), 5.147348340711497) def test_single_peak_fit_window(): """ Single Peak fit with a window specified """ # Create the sepctrum x_single, y_single = single_peak() s_single = Spectrum1D(flux=y_single*u.Jy, spectral_axis=x_single*u.um) # Fit the spectrum g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=5.5*u.um, stddev=1.*u.um) g_fit = fit_lines(s_single, g_init, window=2*u.um) y_single_fit = g_fit(x_single*u.um) # Comparing every 10th value. y_single_fit_expected = np.array([3.69669474e-13, 3.57992454e-11, 2.36719426e-09, 1.06879318e-07, 3.29498310e-06, 6.93605383e-05, 9.96945607e-04, 9.78431032e-03, 6.55675141e-02, 3.00017760e-01, 9.37356842e-01, 1.99969007e+00, 2.91286375e+00, 2.89719280e+00, 1.96758892e+00, 9.12412206e-01, 2.88900005e-01, 6.24602556e-02, 9.22061121e-03, 9.29427266e-04]) * u.Jy assert np.allclose(y_single_fit.value[::10], y_single_fit_expected.value, atol=1e-5) def test_single_peak_fit_tuple_window(): """ Single Peak fit with a window specified as a tuple """ # Create the spectrum to fit x_single, y_single = single_peak() s_single = Spectrum1D(flux=y_single*u.Jy, spectral_axis=x_single*u.um) # Fit the spectrum g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=5.5*u.um, stddev=1.*u.um) g_fit = fit_lines(s_single, g_init, window=(6*u.um, 7*u.um)) y_single_fit = g_fit(x_single*u.um) # Comparing every 10th value. y_single_fit_expected = np.array([2.29674788e-16, 6.65518998e-14, 1.20595958e-11, 1.36656472e-09, 9.68395624e-08, 4.29141576e-06, 1.18925100e-04, 2.06096976e-03, 2.23354585e-02, 1.51371211e-01, 6.41529836e-01, 1.70026100e+00, 2.81799025e+00, 2.92071068e+00, 1.89305291e+00, 7.67294570e-01, 1.94485245e-01, 3.08273612e-02, 3.05570344e-03, 1.89413625e-04])*u.Jy assert np.allclose(y_single_fit.value[::10], y_single_fit_expected.value, atol=1e-5) def test_double_peak_fit(): """ Double Peak fit. """ # Create the spectrum to fit x_double, y_double = double_peak() s_double = Spectrum1D(flux=y_double*u.Jy, spectral_axis=x_double*u.um) # Fit the spectrum g1_init = models.Gaussian1D(amplitude=2.3*u.Jy, mean=5.6*u.um, stddev=0.1*u.um) g2_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.4*u.um, stddev=0.1*u.um) g12_fit = fit_lines(s_double, g1_init+g2_init) y12_double_fit = g12_fit(x_double*u.um) # Comparing every 10th value. y12_double_fit_expected = np.array([2.86790780e-130, 2.12984643e-103, 1.20060032e-079, 5.13707226e-059, 1.66839912e-041, 4.11292970e-027, 7.69608184e-016, 1.09308800e-007, 1.17844042e-002, 9.64333366e-001, 6.04322205e-002, 2.22653307e+000, 5.51964567e-005, 8.13581859e-018, 6.37320251e-038, 8.85834856e-055, 1.05230522e-074, 9.48850399e-098, 6.49412764e-124, 3.37373489e-153]) assert np.allclose(y12_double_fit.value[::10], y12_double_fit_expected, atol=1e-5) def test_double_peak_fit_tuple_window(): """ Doulbe Peak fit with a window specified as a tuple """ # Create the spectrum to fit x_double, y_double = double_peak() s_double = Spectrum1D(flux=y_double*u.Jy, spectral_axis=x_double*u.um, rest_value=0*u.um) # Fit the spectrum. g2_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.7*u.um, stddev=0.2*u.um) g2_fit = fit_lines(s_double, g2_init, window=(4.3*u.um, 5.3*u.um)) y2_double_fit = g2_fit(x_double*u.um) # Comparing every 10th value. y2_double_fit_expected = np.array([2.82386634e-116, 2.84746284e-092, 4.63895634e-071, 1.22104254e-052, 5.19265653e-037, 3.56776869e-024, 3.96051875e-014, 7.10322789e-007, 2.05829545e-002, 9.63624806e-001, 7.28880815e-002, 8.90744929e-006, 1.75872724e-012, 5.61037526e-022, 2.89156942e-034, 2.40781783e-049, 3.23938019e-067, 7.04122962e-088, 2.47276807e-111, 1.40302869e-137]) assert np.allclose(y2_double_fit.value[::10], y2_double_fit_expected, atol=1e-5) def test_double_peak_fit_window(): """ Double Peak fit with a window. """ # Create the specturm to fit x_double, y_double = double_peak() s_double = Spectrum1D(flux=y_double*u.Jy, spectral_axis=x_double*u.um, rest_value=0*u.um) # Fit the spectrum g2_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.7*u.um, stddev=0.2*u.um) g2_fit = fit_lines(s_double, g2_init, window=0.3*u.um) y2_double_fit = g2_fit(x_double*u.um) # Comparing every 10th value. y2_double_fit_expected = np.array([1.66363393e-128, 5.28910721e-102, 1.40949521e-078, 3.14848385e-058, 5.89516506e-041, 9.25224449e-027, 1.21718016e-015, 1.34220626e-007, 1.24062432e-002, 9.61209273e-001, 6.24240938e-002, 3.39815491e-006, 1.55056770e-013, 5.93054936e-024, 1.90132233e-037, 5.10943886e-054, 1.15092572e-073, 2.17309153e-096, 3.43926290e-122, 4.56256813e-151]) assert np.allclose(y2_double_fit.value[::10], y2_double_fit_expected, atol=1e-5) def test_double_peak_fit_separate_window(): """ Double Peak fit with a window. """ # Create the spectrum to fit x_double, y_double = double_peak() s_double = Spectrum1D(flux=y_double*u.Jy, spectral_axis=x_double*u.um, rest_value=0*u.um) # Fit the spectrum gl_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.8*u.um, stddev=0.2*u.um) gr_init = models.Gaussian1D(amplitude=2.*u.Jy, mean=5.3*u.um, stddev=0.2*u.um) gl_fit, gr_fit = fit_lines(s_double, [gl_init, gr_init], window=0.2*u.um) yl_double_fit = gl_fit(x_double*u.um) yr_double_fit = gr_fit(x_double*u.um) # Comparing every 10th value. yl_double_fit_expected = np.array([3.40725147e-18, 5.05500395e-15, 3.59471319e-12, 1.22527176e-09, 2.00182467e-07, 1.56763547e-05, 5.88422893e-04, 1.05866724e-02, 9.12966452e-02, 3.77377148e-01, 7.47690410e-01, 7.10057397e-01, 3.23214276e-01, 7.05201207e-02, 7.37498248e-03, 3.69687164e-04, 8.88245844e-06, 1.02295712e-07, 5.64686114e-10, 1.49410879e-12]) assert np.allclose(yl_double_fit.value[::10], yl_double_fit_expected, atol=1e-5) # Comparing every 10th value. yr_double_fit_expected = np.array([0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 3.04416285e-259, 3.85323221e-198, 2.98888589e-145, 1.42075875e-100, 4.13864520e-064, 7.38793226e-036, 8.08191847e-016, 5.41792361e-004, 2.22575901e+000, 5.60338234e-005, 8.64468603e-018, 8.17287853e-039, 4.73508430e-068, 1.68115300e-105, 3.65774659e-151, 4.87693358e-205, 3.98480359e-267]) assert np.allclose(yr_double_fit.value[::10], yr_double_fit_expected, atol=1e-5) def test_double_peak_fit_separate_window_tuple_window(): """ Double Peak fit with a window. """ x_double, y_double = double_peak() s_double = Spectrum1D(flux=y_double*u.Jy, spectral_axis=x_double*u.um, rest_value=0*u.um) g1_init = models.Gaussian1D(amplitude=2.*u.Jy, mean=5.3*u.um, stddev=0.2*u.um) g2_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.9*u.um, stddev=0.1*u.um) g1_fit, g2_fit = fit_lines(s_double, [g1_init, g2_init], window=[(5.3*u.um, 5.8*u.um), (4.6*u.um, 5.3*u.um)]) y1_double_fit = g1_fit(x_double*u.um) y2_double_fit = g2_fit(x_double*u.um) # Comparing every 10th value. y1_double_fit_expected = np.array([0.00000000e+000, 0.00000000e+000, 5.61595149e-307, 3.38362505e-242, 4.27358433e-185, 1.13149721e-135, 6.28008984e-094, 7.30683649e-060, 1.78214929e-033, 9.11192086e-015, 9.76623021e-004, 2.19429562e+000, 1.03350951e-004, 1.02043415e-016, 2.11206194e-036, 9.16388177e-064, 8.33495900e-099, 1.58920023e-141, 6.35191874e-192, 5.32209240e-250]) assert np.allclose(y1_double_fit.value[::10], y1_double_fit_expected, atol=1e-5) # Comparing every 10th value. y2_double_fit_expected = np.array([2.52990802e-158, 5.15446435e-126, 2.07577138e-097, 1.65231432e-072, 2.59969849e-051, 8.08482210e-034, 4.96975664e-020, 6.03833143e-010, 1.45016006e-003, 6.88386116e-001, 6.45900222e-002, 1.19788723e-006, 4.39120391e-015, 3.18176751e-027, 4.55691000e-043, 1.28999976e-062, 7.21815119e-086, 7.98324559e-113, 1.74521997e-143, 7.54115780e-178]) assert np.allclose(y2_double_fit.value[::10], y2_double_fit_expected, atol=1e-3) def test_double_peak_fit_with_exclusion(): """ Double Peak fit with a window. """ x_double, y_double = double_peak() s_double = Spectrum1D(flux=y_double*u.Jy, spectral_axis=x_double*u.um, rest_value=0*u.um) g1_init = models.Gaussian1D(amplitude=1.*u.Jy, mean=4.9*u.um, stddev=0.2*u.um) g1_fit = fit_lines(s_double, g1_init, exclude_regions=[SpectralRegion(5.2*u.um, 5.8*u.um)]) y1_double_fit = g1_fit(x_double*u.um) # Comparing every 10th value. y1_double_fit_expected = np.array([4.64465938e-130, 3.11793334e-103, 1.60765691e-079, 6.36698036e-059, 1.93681098e-041, 4.52537486e-027, 8.12148549e-016, 1.11951515e-007, 1.18532671e-002, 9.63961653e-001, 6.02136613e-002, 2.88897581e-006, 1.06464879e-013, 3.01357787e-024, 6.55197242e-038, 1.09414605e-054, 1.40343441e-074, 1.38268273e-097, 1.04632487e-123, 6.08168818e-153]) assert np.allclose(y1_double_fit.value[::10], y1_double_fit_expected, atol=1e-5) def tie_center(model): """ Dummy method for testing passing of tied parameter """ mean = 50 * model.stddev return mean def test_fixed_parameters(): """ Test to confirm fixed parameters do not change. """ x = np.linspace(0., 10., 200) y = 3 * np.exp(-0.5 * (x - 6.3)**2 / 0.8**2) y += np.random.normal(0., 0.2, x.shape) spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Test passing fixed and bounds parameters g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=6.1*u.um, stddev=1.*u.um, fixed={'mean': True}, bounds={'amplitude': (2, 5)*u.Jy}, name="Gaussian Test Model") g_fit = fit_lines(spectrum, g_init) assert_quantity_allclose(g_fit.mean, 6.1*u.um) assert g_fit.bounds == g_init.bounds assert g_fit.name == "Gaussian Test Model" # Test passing of tied parameter g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=6.1*u.um, stddev=1.*u.um) g_init.mean.tied = tie_center g_fit = fit_lines(spectrum, g_init) assert g_fit.tied == g_init.tied assert g_fit.name == g_init.name def test_name_preservation_after_fitting(): """ Test to confirm model and submodels names are preserved after fitting. """ x = np.linspace(0., 10., 200) y = 3 * np.exp(-0.5 * (x - 6.3)**2 / 0.8**2) y += np.random.normal(0., 0.2, x.shape) spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) subcomponents = models.Gaussian1D(name="Model I") * models.Gaussian1D(name="Second Model") c_model = subcomponents + models.Gaussian1D(name="Model 3") c_model.name = "Compound Model with 3 components" model_fit = fit_lines(spectrum, c_model) assert model_fit.name == "Compound Model with 3 components" assert model_fit.submodel_names == ("Model I", "Second Model", "Model 3") def test_ignore_units(): """ Ignore the units """ # # Ignore the units based on there not being units on the model # # Create the spectrum x_single, y_single = single_peak() s_single = Spectrum1D(flux=y_single*u.Jy, spectral_axis=x_single*u.um) # Fit the spectrum g_init = models.Gaussian1D(amplitude=3, mean=6.1, stddev=1.) g_fit = fit_lines(s_single, g_init) y_single_fit = g_fit(x_single*u.um) # Comparing every 10th value. y_single_fit_expected = np.array([3.69669474e-13, 3.57992454e-11, 2.36719426e-09, 1.06879318e-07, 3.29498310e-06, 6.93605383e-05, 9.96945607e-04, 9.78431032e-03, 6.55675141e-02, 3.00017760e-01, 9.37356842e-01, 1.99969007e+00, 2.91286375e+00, 2.89719280e+00, 1.96758892e+00, 9.12412206e-01, 2.88900005e-01, 6.24602556e-02, 9.22061121e-03, 9.29427266e-04]) assert np.allclose(y_single_fit.value[::10], y_single_fit_expected, atol=1e-5) assert y_single_fit.unit == s_single.flux.unit # # Ignore the units based on not being in the model # # Create the spectrum to fit x_double, y_double = double_peak() s_double = Spectrum1D(flux=y_double*u.Jy, spectral_axis=x_double*u.um) # Fit the spectrum g1_init = models.Gaussian1D(amplitude=2.3, mean=5.6, stddev=0.1) g2_init = models.Gaussian1D(amplitude=1., mean=4.4, stddev=0.1) g12_fit = fit_lines(s_double, g1_init+g2_init) y12_double_fit = g12_fit(x_double*u.um) # Comparing every 10th value. y12_double_fit_expected = np.array([2.86790780e-130, 2.12984643e-103, 1.20060032e-079, 5.13707226e-059, 1.66839912e-041, 4.11292970e-027, 7.69608184e-016, 1.09308800e-007, 1.17844042e-002, 9.64333366e-001, 6.04322205e-002, 2.22653307e+000, 5.51964567e-005, 8.13581859e-018, 6.37320251e-038, 8.85834856e-055, 1.05230522e-074, 9.48850399e-098, 6.49412764e-124, 3.37373489e-153]) assert np.allclose(y12_double_fit.value[::10], y12_double_fit_expected, atol=1e-5) def test_fitter_parameters(): """ Single Peak fit. """ # Create the spectrum x_single, y_single = single_peak() s_single = Spectrum1D(flux=y_single*u.Jy, spectral_axis=x_single*u.um) # Fit the spectrum g_init = models.Gaussian1D(amplitude=3.*u.Jy, mean=6.1*u.um, stddev=1.*u.um) fit_params = {'maxiter': 200} g_fit = fit_lines(s_single, g_init, **fit_params) y_single_fit = g_fit(x_single*u.um) # Comparing every 10th value. y_single_fit_expected = np.array([3.69669474e-13, 3.57992454e-11, 2.36719426e-09, 1.06879318e-07, 3.29498310e-06, 6.93605383e-05, 9.96945607e-04, 9.78431032e-03, 6.55675141e-02, 3.00017760e-01, 9.37356842e-01, 1.99969007e+00, 2.91286375e+00, 2.89719280e+00, 1.96758892e+00, 9.12412206e-01, 2.88900005e-01, 6.24602556e-02, 9.22061121e-03, 9.29427266e-04]) * u.Jy assert np.allclose(y_single_fit.value[::10], y_single_fit_expected.value, atol=1e-5) def test_spectrum_from_model(): """ This test fits the the first simulated spectrum from the fixture. The initial guesses are manually set here with bounds that essentially make sense as the functionality of the test is to make sure the fit works and we get a reasonable answer out **given** good initial guesses. """ np.random.seed(0) x = np.linspace(0., 10., 200) y = 3 * np.exp(-0.5 * (x - 6.3)**2 / 0.1**2) y += np.random.normal(0., 0.2, x.shape) y_continuum = 3.2 * np.exp(-0.5 * (x - 5.6)**2 / 4.8**2) y += y_continuum spectrum = Spectrum1D(flux=y*u.Jy, spectral_axis=x*u.um) # Unitless test chebyshev = models.Chebyshev1D(3, c0=0.1, c1=4, c2=5) spectrum_chebyshev = spectrum_from_model(chebyshev, spectrum) flux_expected = np.array([-4.90000000e+00, -3.64760991e-01, 9.22085553e+00, 2.38568496e+01, 4.35432211e+01, 6.82799702e+01, 9.80670968e+01, 1.32904601e+02, 1.72792483e+02, 2.17730742e+02, 2.67719378e+02, 3.22758392e+02, 3.82847784e+02, 4.47987553e+02, 5.18177700e+02, 5.93418224e+02, 6.73709126e+02, 7.59050405e+02, 8.49442062e+02, 9.44884096e+02]) assert np.allclose(spectrum_chebyshev.flux.value[::10], flux_expected, atol=1e-5) # Unitfull test gaussian = models.Gaussian1D(amplitude=5*u.Jy, mean=4*u.um, stddev=2.3*u.um) spectrum_gaussian = spectrum_from_model(gaussian, spectrum) flux_expected = np.array([1.1020263, 1.57342489, 2.14175093, 2.77946243, 3.4389158, 4.05649712, 4.56194132, 4.89121902, 4.99980906, 4.872576, 4.52723165, 4.01028933, 3.3867847, 2.72689468, 2.09323522, 1.5319218, 1.06886794, 0.71101768, 0.45092638, 0.27264641]) assert np.allclose(spectrum_gaussian.flux.value[::10], flux_expected, atol=1e-5) def test_masking(): """ Test fitting spectra with masks """ wl, flux = double_peak() s = Spectrum1D(flux=flux*u.Jy, spectral_axis=wl*u.um) # first we fit a single gaussian to the double_peak model, using the # known-good second peak (but a bit higher in amplitude). It should lock # in on the *second* peak since it's already close: g_init = models.Gaussian1D(2.5, 5.5, 0.2) g_fit1 = fit_lines(s, g_init) assert u.allclose(g_fit1.mean, 5.5, atol=.1) # now create a spectrum where the region around the second peak is masked. # The fit should now go to the *first* peak s_msk = Spectrum1D(flux=flux*u.Jy, spectral_axis=wl*u.um, mask=(5.1 < wl)&(wl < 6.1)) g_fit2 = fit_lines(s_msk, g_init) assert u.allclose(g_fit2.mean, 4.6, atol=.1) # double check that it works with weights as well g_fit3 = fit_lines(s_msk, g_init, weights=np.ones_like(s_msk.flux.value)) assert g_fit2.mean == g_fit3.mean def test_window_extras(): """ Test that fitting works with masks and weights when a window is present """ # similar to the masking test, but add a broad window around the whole thing wl, flux = double_peak() g_init = models.Gaussian1D(2.5, 5.5, 0.2) window_region = SpectralRegion(4*u.um, 8*u.um) mask = (5.1 < wl) & (wl < 6.1) s_msk = Spectrum1D(flux=flux*u.Jy, spectral_axis=wl*u.um, mask=mask) g_fit1 = fit_lines(s_msk, g_init, window=window_region) assert u.allclose(g_fit1.mean, 4.6, atol=.1) # check that if we weight instead of masking, we get the same result s = Spectrum1D(flux=flux*u.Jy, spectral_axis=wl*u.um) weights = (~mask).astype(float) g_fit2 = fit_lines(s, g_init, weights=weights, window=window_region) assert u.allclose(g_fit2.mean, 4.6, atol=.1) # and the same with both together weights = (~mask).astype(float) g_fit3 = fit_lines(s_msk, g_init, weights=weights, window=window_region) assert u.allclose(g_fit3.mean, 4.6, atol=.1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/tests/test_io.py0000644000076500000240000001256000000000000020652 0ustar00erikstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module tests SpecUtils io routines """ from specutils.io.parsing_utils import generic_spectrum_from_table # or something like that from astropy.io import registry from astropy.table import Table from astropy.utils.exceptions import AstropyUserWarning from astropy.tests.helper import catch_warnings import astropy.units as u import numpy as np import pytest import warnings from specutils import Spectrum1D, SpectrumList from specutils.io import data_loader def test_generic_spectrum_from_table(recwarn): """ Read a simple table with wavelength, flux and uncertainty """ # Create a small data set, first without uncertainties wave = np.arange(1,1.1,0.01)*u.AA flux = np.ones(len(wave))*1.e-14*u.Jy table = Table([wave,flux],names=["wave","flux"]) # Test that the units and values of the Spectrum1D object match those in the table spectrum = generic_spectrum_from_table(table) assert spectrum.spectral_axis.unit == table['wave'].unit assert spectrum.flux.unit == table['flux'].unit assert spectrum.spectral_axis.unit == table['wave'].unit assert np.alltrue(spectrum.spectral_axis == table['wave']) assert np.alltrue(spectrum.flux == table['flux']) # Add uncertainties and retest err = 0.01*flux table = Table([wave,flux,err],names=["wave","flux","err"]) spectrum = generic_spectrum_from_table(table) assert spectrum.spectral_axis.unit == table['wave'].unit assert spectrum.flux.unit == table['flux'].unit assert spectrum.uncertainty.unit == table['err'].unit assert spectrum.spectral_axis.unit == table['wave'].unit assert np.alltrue(spectrum.spectral_axis == table['wave']) assert np.alltrue(spectrum.flux == table['flux']) assert np.alltrue(spectrum.uncertainty.array == table['err']) # Test for warning if standard deviation is zero or negative err[0] = 0. table = Table([wave,flux,err],names=["wave","flux","err"]) spectrum = generic_spectrum_from_table(table) assert len(recwarn) == 1 w = recwarn.pop(AstropyUserWarning) assert "Standard Deviation has values of 0 or less" in str(w.message) # Test that exceptions are raised if there are no units flux = np.ones(len(wave))*1.e-14 table = Table([wave,flux],names=["wave","flux"]) with pytest.raises(IOError) as exc: spectrum = generic_spectrum_from_table(table) assert 'Could not identify column containing the flux' in exc wave = np.arange(1,1.1,0.01) table = Table([wave,flux,err],names=["wave","flux","err"]) with pytest.raises(IOError) as exc: spectrum = generic_spectrum_from_table(table) assert 'Could not identify column containing the wavelength, frequency or energy' in exc def test_speclist_autoidentify(): formats = registry.get_formats(SpectrumList) assert (formats['Auto-identify'] == 'Yes').all() def test_default_identifier(tmpdir): fname = str(tmpdir.join('empty.txt')) with open(fname, 'w') as ff: ff.write('\n') format_name = 'default_identifier_test' @data_loader(format_name) def reader(*args, **kwargs): """Doesn't actually get used.""" return for datatype in [Spectrum1D, SpectrumList]: fmts = registry.identify_format('read', datatype, fname, None, [], {}) assert format_name in fmts # Clean up after ourselves registry.unregister_reader(format_name, datatype) registry.unregister_identifier(format_name, datatype) def test_default_identifier_extension(tmpdir): good_fname = str(tmpdir.join('empty.fits')) bad_fname = str(tmpdir.join('empty.txt')) # Create test data files. for name in [good_fname, bad_fname]: with open(name, 'w') as ff: ff.write('\n') format_name = 'default_identifier_extension_test' @data_loader(format_name, extensions=['fits']) def reader(*args, **kwargs): """Doesn't actually get used.""" return for datatype in [Spectrum1D, SpectrumList]: fmts = registry.identify_format('read', datatype, good_fname, None, [], {}) assert format_name in fmts fmts = registry.identify_format('read', datatype, bad_fname, None, [], {}) assert format_name not in fmts # Clean up after ourselves registry.unregister_reader(format_name, datatype) registry.unregister_identifier(format_name, datatype) def test_custom_identifier(tmpdir): good_fname = str(tmpdir.join('good.txt')) bad_fname = str(tmpdir.join('bad.txt')) # Create test data files. for name in [good_fname, bad_fname]: with open(name, 'w') as ff: ff.write('\n') format_name = 'custom_identifier_test' def identifier(origin, *args, **kwargs): fname = args[0] return 'good' in fname @data_loader(format_name, identifier=identifier) def reader(*args, **kwargs): """Doesn't actually get used.""" return for datatype in [Spectrum1D, SpectrumList]: fmts = registry.identify_format('read', datatype, good_fname, None, [], {}) assert format_name in fmts fmts = registry.identify_format('read', datatype, bad_fname, None, [], {}) assert format_name not in fmts # Clean up after ourselves registry.unregister_reader(format_name, datatype) registry.unregister_identifier(format_name, datatype) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/tests/test_loaders.py0000644000076500000240000002741400000000000021700 0ustar00erikstaff00000000000000import logging import os import shutil import tempfile import urllib import warnings import pytest import astropy.units as u import numpy as np from astropy.io import fits from astropy.io.fits.verify import VerifyWarning from astropy.table import Table from astropy.units import UnitsWarning from astropy.wcs import FITSFixedWarning from astropy.io.registry import IORegistryError from astropy.modeling import models from astropy.tests.helper import quantity_allclose from astropy.nddata import NDUncertainty, StdDevUncertainty from numpy.testing import assert_allclose from .conftest import remote_data_path, remote_access from .. import Spectrum1D, SpectrumList from ..io import get_loaders_by_extension def test_get_loaders_by_extension(): loader_labels = get_loaders_by_extension('fits') assert len(loader_labels) > 0 assert isinstance(loader_labels[0], str) @remote_access([{'id': '1481190', 'filename': 'L5g_0355+11_Cruz09.fits'}]) def test_spectrum1d_GMOSfits(remote_data_path): with warnings.catch_warnings(): warnings.simplefilter('ignore', (VerifyWarning, UnitsWarning)) optical_spec_2 = Spectrum1D.read(remote_data_path, format='wcs1d-fits') assert len(optical_spec_2.data) == 3020 @remote_access([{'id': '1481190', 'filename': 'L5g_0355+11_Cruz09.fits'}]) def test_spectrumlist_GMOSfits(remote_data_path, caplog): with warnings.catch_warnings(): warnings.simplefilter('ignore', (VerifyWarning, UnitsWarning)) spectrum_list = SpectrumList.read(remote_data_path, format='wcs1d-fits') assert len(spectrum_list) == 1 spec = spectrum_list[0] assert len(spec.data) == 3020 assert len(caplog.record_tuples) == 0 @remote_access([{'id': '1481190', 'filename': 'L5g_0355+11_Cruz09.fits'}]) def test_specific_spec_axis_unit(remote_data_path): with warnings.catch_warnings(): warnings.simplefilter('ignore', (VerifyWarning, UnitsWarning)) optical_spec = Spectrum1D.read(remote_data_path, spectral_axis_unit="Angstrom", format='wcs1d-fits') assert optical_spec.spectral_axis.unit == "Angstrom" @remote_access([{'id': '2656720', 'filename': '_v1410ori_20181204_261_Forrest%20Sims.fit'}]) def test_ctypye_not_compliant(remote_data_path, caplog): optical_spec = Spectrum1D.read(remote_data_path, spectral_axis_unit="Angstrom", format='wcs1d-fits') assert len(caplog.record_tuples) == 0 def test_generic_ecsv_reader(tmpdir): # Create a small data set wave = np.arange(1,1.1,0.01)*u.AA flux = np.ones(len(wave))*1.e-14*u.Jy uncertainty = 0.01*flux table = Table([wave,flux,uncertainty],names=["wave","flux","uncertainty"]) tmpfile = str(tmpdir.join('_tst.ecsv')) table.write(tmpfile,format='ascii.ecsv') # Read it in and check against the original spectrum = Spectrum1D.read(tmpfile,format='ECSV') assert spectrum.spectral_axis.unit == table['wave'].unit assert spectrum.flux.unit == table['flux'].unit assert spectrum.uncertainty.unit == table['uncertainty'].unit assert spectrum.spectral_axis.unit == table['wave'].unit assert np.alltrue(spectrum.spectral_axis == table['wave']) assert np.alltrue(spectrum.flux == table['flux']) assert np.alltrue(spectrum.uncertainty.array == table['uncertainty']) @remote_access([{'id': '1481119', 'filename': 'COS_FUV.fits'}, {'id': '1481181', 'filename': 'COS_NUV.fits'}]) def test_hst_cos(remote_data_path): spec = Spectrum1D.read(remote_data_path, format='HST/COS') assert isinstance(spec, Spectrum1D) assert spec.flux.size > 0 @remote_access([{'id': '1481192', 'filename':'STIS_FUV.fits'}, {'id': '1481185', 'filename': 'STIS_NUV.fits'}, {'id': '1481183', 'filename': 'STIS_CCD.fits'}]) def test_hst_stis(remote_data_path): spec = Spectrum1D.read(remote_data_path, format='HST/STIS') assert isinstance(spec, Spectrum1D) assert spec.flux.size > 0 @pytest.mark.remote_data def test_sdss_spec(): with urllib.request.urlopen('https://dr14.sdss.org/optical/spectrum/view/data/format%3Dfits/spec%3Dlite?mjd=55359&fiberid=596&plateid=4055') as response: with tempfile.NamedTemporaryFile() as tmp_file: shutil.copyfileobj(response, tmp_file) spec = Spectrum1D.read(tmp_file.name, format="SDSS-III/IV spec") assert isinstance(spec, Spectrum1D) assert spec.flux.size > 0 @pytest.mark.remote_data def test_sdss_spspec(): with urllib.request.urlopen('http://das.sdss.org/spectro/1d_26/0273/1d/spSpec-51957-0273-016.fit') as response: with tempfile.NamedTemporaryFile() as tmp_file: shutil.copyfileobj(response, tmp_file) with warnings.catch_warnings(): warnings.simplefilter('ignore', FITSFixedWarning) spec = Spectrum1D.read(tmp_file.name, format="SDSS-I/II spSpec") assert isinstance(spec, Spectrum1D) assert spec.flux.size > 0 @pytest.mark.parametrize("name", ['file.fit', 'file.fits', 'file.dat']) def test_no_reader_matches(name): '''If no reader matches a file, check that the correct error is raised. This test serves a second purpose: A badly written identifier function might raise an error as supposed to returning False when it cannot identify a file. The fact that this test passes means that at the very least all identifier functions that have been tried for that file ending did not fail with an error. ''' with tempfile.TemporaryDirectory() as tmpdirname: filename = os.path.join(tmpdirname, name) with open(filename, 'w') as fp: fp.write('asdfadasdadvzxcv') with pytest.raises(IORegistryError): spec = Spectrum1D.read(filename) @remote_access([{'id':'3359174', 'filename':'linear_fits_solution.fits'}]) def test_iraf_linear(remote_data_path): spectrum_1d = Spectrum1D.read(remote_data_path, format='iraf') assert isinstance(spectrum_1d, Spectrum1D) assert quantity_allclose(spectrum_1d.wavelength[0], u.Quantity(3514.56625402, unit='Angstrom')) assert quantity_allclose(spectrum_1d.wavelength[100], u.Quantity(3514.56625402, unit='Angstrom') + u.Quantity(0.653432383823 * 100, unit='Angstrom')) @remote_access([{'id':'3359180', 'filename':'log-linear_fits_solution.fits'}]) def test_iraf_log_linear(remote_data_path): with pytest.raises(NotImplementedError): assert Spectrum1D.read(remote_data_path, format='iraf') @remote_access([{'id':'3359190', 'filename':'non-linear_fits_solution_cheb.fits'}]) def test_iraf_non_linear_chebyshev(remote_data_path): chebyshev_model = models.Chebyshev1D(degree=2, domain=[1616, 3259]) chebyshev_model.c0.value = 5115.64008186 chebyshev_model.c1.value = 535.515983712 chebyshev_model.c2.value = -0.779265625182 wavelength_axis = chebyshev_model(range(1, 4097)) * u.angstrom spectrum_1d = Spectrum1D.read(remote_data_path, format='iraf') assert isinstance(spectrum_1d, Spectrum1D) assert_allclose(wavelength_axis, spectrum_1d.wavelength) @remote_access([{'id':'3359194', 'filename':'non-linear_fits_solution_legendre.fits'}]) def test_iraf_non_linear_legendre(remote_data_path): legendre_model = models.Legendre1D(degree=3, domain=[21, 4048]) legendre_model.c0.value = 5468.67555891 legendre_model.c1.value = 835.332144466 legendre_model.c2.value = -6.02202094803 legendre_model.c3.value = -1.13142953897 wavelength_axis = legendre_model(range(1, 4143)) * u.angstrom spectrum_1d = Spectrum1D.read(remote_data_path, format='iraf') assert isinstance(spectrum_1d, Spectrum1D) assert_allclose(wavelength_axis, spectrum_1d.wavelength) @remote_access([{'id':'3359196', 'filename':'non-linear_fits_solution_linear-spline.fits'}]) def test_iraf_non_linear_linear_spline(remote_data_path): with pytest.raises(NotImplementedError): assert Spectrum1D.read(remote_data_path, format='iraf') @remote_access([{'id':'3359200', 'filename':'non-linear_fits_solution_cubic-spline.fits'}]) def test_iraf_non_linear_cubic_spline(remote_data_path): with pytest.raises(NotImplementedError): assert Spectrum1D.read(remote_data_path, format='iraf') @pytest.mark.parametrize("spectral_axis", ['wavelength', 'frequency', 'energy', 'wavenumber']) def test_tabular_fits_writer(tmpdir, spectral_axis): wlu = {'wavelength': u.AA, 'frequency': u.GHz, 'energy': u.eV, 'wavenumber': u.cm**-1} # Create a small data set disp = np.arange(1,1.1,0.01)*wlu[spectral_axis] flux = np.ones(len(disp))*1.e-14*u.Jy unc = StdDevUncertainty(0.01*flux) spectrum = Spectrum1D(flux=flux, spectral_axis=disp, uncertainty=unc) tmpfile = str(tmpdir.join('_tst.fits')) spectrum.write(tmpfile, format='tabular-fits') # Read it in and check against the original table = Table.read(tmpfile, format='fits') assert table[spectral_axis].unit == spectrum.spectral_axis.unit assert table['flux'].unit == spectrum.flux.unit assert table['uncertainty'].unit == spectrum.uncertainty.unit assert quantity_allclose(table[spectral_axis], spectrum.spectral_axis) assert quantity_allclose(table['flux'], spectrum.flux) assert quantity_allclose(table['uncertainty'], spectrum.uncertainty.quantity) # Test spectrum with different flux unit flux = np.random.normal(0., 1.e-9, disp.shape[0]) * u.W * u.m**-2 * u.AA**-1 unc = StdDevUncertainty(0.1 * np.sqrt(np.abs(flux.value)) * flux.unit) spectrum = Spectrum1D(flux=flux, spectral_axis=disp, uncertainty=unc) # Try to overwrite the file with pytest.raises(OSError, match=r'File exists:'): spectrum.write(tmpfile, format='tabular-fits') spectrum.write(tmpfile, format='tabular-fits', overwrite=True) table = Table.read(tmpfile) assert table['flux'].unit == spectrum.flux.unit # ToDo: get tabular_fits_loader to also read this in correctly! def test_tabular_fits_header(tmpdir): # Create a small data set + header with reserved FITS keywords disp = np.linspace(1, 1.2, 21) * u.AA flux = np.random.normal(0., 1.0e-14, disp.shape[0]) * u.Jy hdr = fits.header.Header({'TELESCOP': 'Leviathan', 'APERTURE': 1.8, 'OBSERVER': 'Parsons', 'NAXIS': 1, 'NAXIS1': 8}) spectrum = Spectrum1D(flux=flux, spectral_axis=disp, meta={'header': hdr}) tmpfile = str(tmpdir.join('_tst.fits')) spectrum.write(tmpfile, format='tabular-fits') # Read it in and check against the original hdulist = fits.open(tmpfile) assert hdulist[0].header['NAXIS'] == 0 assert hdulist[1].header['NAXIS'] == 2 assert hdulist[1].header['NAXIS2'] == disp.shape[0] assert hdulist[1].header['OBSERVER'] == 'Parsons' hdulist.close() # Now write with updated header information from spectrum.meta spectrum.meta.update({'OBSERVER': 'Rosse', 'EXPTIME': 32.1, 'NAXIS2': 12}) spectrum.write(tmpfile, format='tabular-fits', overwrite=True, update_header=True) hdulist = fits.open(tmpfile) assert hdulist[1].header['NAXIS2'] == disp.shape[0] assert hdulist[1].header['OBSERVER'] == 'Rosse' assert_allclose(hdulist[1].header['EXPTIME'], 3.21e1) hdulist.close() # Test that unsupported types (dict) are not added to written header spectrum.meta['MYHEADER'] = {'OBSDATE': '1848-02-26', 'TARGET': 'M51'} spectrum.write(tmpfile, format='tabular-fits', overwrite=True, update_header=True) hdulist = fits.open(tmpfile) assert 'MYHEADER' not in hdulist[0].header assert 'MYHEADER' not in hdulist[1].header assert 'OBSDATE' not in hdulist[0].header assert 'OBSDATE' not in hdulist[1].header hdulist.close() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/tests/test_manipulation.py0000644000076500000240000001441700000000000022746 0ustar00erikstaff00000000000000import operator import pytest import numpy as np import astropy.units as u from astropy.modeling import models from astropy.nddata import StdDevUncertainty, NDData from astropy.tests.helper import quantity_allclose from ..utils.wcs_utils import gwcs_from_array from ..spectra import Spectrum1D, SpectralRegion, SpectrumCollection from ..manipulation import snr_threshold def test_snr_threshold(): np.random.seed(42) # Setup 1D spectrum wavelengths = np.arange(0, 10)*u.um flux = 100*np.abs(np.random.randn(10))*u.Jy uncertainty = StdDevUncertainty(np.abs(np.random.randn(10))*u.Jy) spectrum = Spectrum1D(spectral_axis=wavelengths, flux=flux, uncertainty=uncertainty) spectrum_masked = snr_threshold(spectrum, 50) assert all([x==y for x,y in zip(spectrum_masked.mask, [False, True, False, False, True, True, False, False, False, True])]) spectrum_masked = snr_threshold(spectrum, 50, operator.gt) assert all([x==y for x,y in zip(spectrum_masked.mask, [False, True, False, False, True, True, False, False, False, True])]) spectrum_masked = snr_threshold(spectrum, 50, '>') assert all([x==y for x,y in zip(spectrum_masked.mask, [False, True, False, False, True, True, False, False, False, True])]) spectrum_masked = snr_threshold(spectrum, 50, operator.ge) assert all([x==y for x,y in zip(spectrum_masked.mask, [False, True, False, False, True, True, False, False, False, True])]) spectrum_masked = snr_threshold(spectrum, 50, '>=') assert all([x==y for x,y in zip(spectrum_masked.mask, [False, True, False, False, True, True, False, False, False, True])]) spectrum_masked = snr_threshold(spectrum, 50, operator.lt) assert all([not x==y for x,y in zip(spectrum_masked.mask, [False, True, False, False, True, True, False, False, False, True])]) spectrum_masked = snr_threshold(spectrum, 50, '<') assert all([not x==y for x,y in zip(spectrum_masked.mask, [False, True, False, False, True, True, False, False, False, True])]) spectrum_masked = snr_threshold(spectrum, 50, operator.le) assert all([not x==y for x,y in zip(spectrum_masked.mask, [False, True, False, False, True, True, False, False, False, True])]) spectrum_masked = snr_threshold(spectrum, 50, '<=') assert all([not x==y for x,y in zip(spectrum_masked.mask, [False, True, False, False, True, True, False, False, False, True])]) # Setup 3D spectrum np.random.seed(42) wavelengths = np.arange(0, 10)*u.um flux = 100*np.abs(np.random.randn(3, 4, 10))*u.Jy uncertainty = StdDevUncertainty(np.abs(np.random.randn(3, 4, 10))*u.Jy) spectrum = Spectrum1D(spectral_axis=wavelengths, flux=flux, uncertainty=uncertainty) spectrum_masked = snr_threshold(spectrum, 50) masked_true = np.array([[[ False, True, True, False, True, True, False, False, False, False], [True, False, True, False, False, True, False, False, False, False], [ False, True, True, False, False, True, False, True, False, False], [ False, False, True, False, False, False, True, False, False, True]], [[ False, True, True, True, False, False, False, False, False, False], [True, True, False, False, False, False, False, True, False, True], [ False, True, False, False, False, False, True, False, True, True], [ False, False, True, False, False, False, True, False, False, False]], [[ False, False, False, True, False, False, False, False, False, True], [True, False, False, False, False, False, True, False, True, False], [ False, True, True, True, True, True, False, True, True, True], [ False, True, False, False, True, True, True, False, False, False]]]) assert all([x==y for x,y in zip(spectrum_masked.mask.ravel(), masked_true.ravel())]) # Setup 3D NDData np.random.seed(42) flux = 100*np.abs(np.random.randn(3, 4, 10))*u.Jy uncertainty = StdDevUncertainty(np.abs(np.random.randn(3, 4, 10))*u.Jy) spectrum = NDData(data=flux, uncertainty=uncertainty) spectrum_masked = snr_threshold(spectrum, 50) masked_true = np.array([[[ False, True, True, False, True, True, False, False, False, False], [True, False, True, False, False, True, False, False, False, False], [ False, True, True, False, False, True, False, True, False, False], [ False, False, True, False, False, False, True, False, False, True]], [[ False, True, True, True, False, False, False, False, False, False], [True, True, False, False, False, False, False, True, False, True], [ False, True, False, False, False, False, True, False, True, True], [ False, False, True, False, False, False, True, False, False, False]], [[ False, False, False, True, False, False, False, False, False, True], [True, False, False, False, False, False, True, False, True, False], [ False, True, True, True, True, True, False, True, True, True], [ False, True, False, False, True, True, True, False, False, False]]]) assert all([x==y for x,y in zip(spectrum_masked.mask.ravel(), masked_true.ravel())]) # Test SpectralCollection np.random.seed(42) flux = u.Quantity(np.random.sample((5, 10)), unit='Jy') spectral_axis = u.Quantity(np.arange(50).reshape((5, 10)), unit='AA') wcs = np.array([gwcs_from_array(x) for x in spectral_axis]) uncertainty = StdDevUncertainty(np.random.sample((5, 10)), unit='Jy') mask = np.ones((5, 10)).astype(bool) meta = [{'test': 5, 'info': [1, 2, 3]} for i in range(5)] spec_coll = SpectrumCollection( flux=flux, spectral_axis=spectral_axis, wcs=wcs, uncertainty=uncertainty, mask=mask, meta=meta) spec_coll_masked = snr_threshold(spec_coll, 3) print(spec_coll_masked.mask) ma = np.array([[True, True, True, True, True, True, True, False, False, True], [True, False, True, True, True, True, True, True, False, True], [True, True, False, True, True, True, True, False, True, True], [True, True, True, False, False, True, True, True, True, True], [True, True, True, True, True, True, True, True, False, True]]) assert all([x==y for x,y in zip(spec_coll_masked.mask.ravel(), ma.ravel())]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578138041.0 specutils-0.7/specutils/tests/test_region_extract.py0000644000076500000240000002016400000000000023257 0ustar00erikstaff00000000000000import numpy as np import pytest import astropy.units as u from astropy.nddata import StdDevUncertainty from astropy.tests.helper import quantity_allclose from ..spectra import Spectrum1D, SpectralRegion from ..manipulation import extract_region from ..manipulation.utils import linear_exciser from .spectral_examples import simulated_spectra from astropy.tests.helper import quantity_allclose def test_region_simple(simulated_spectra): np.random.seed(42) spectrum = simulated_spectra.s1_um_mJy_e1 uncertainty = StdDevUncertainty(0.1*np.random.random(len(spectrum.flux))*u.mJy) spectrum.uncertainty = uncertainty region = SpectralRegion(0.6*u.um, 0.8*u.um) sub_spectrum = extract_region(spectrum, region) sub_spectrum_flux_expected = np.array( [1605.71612173, 1651.41650744, 2057.65798618, 2066.73502361, 1955.75832537, 1670.52711471, 1491.10034446, 1637.08084112, 1471.28982259, 1299.19484483, 1423.11195734, 1226.74494917, 1572.31888312, 1311.50503403, 1474.05051673, 1335.39944397, 1420.61880528, 1433.18623759, 1290.26966668, 1605.67341284, 1528.52281708, 1592.74392861, 1568.74162534, 1435.29407808, 1536.68040935, 1157.33825995, 1136.12679394, 999.92394692, 1038.61546167, 1011.60297294]) assert quantity_allclose(sub_spectrum.flux.value, sub_spectrum_flux_expected) def test_region_ghz(simulated_spectra): spectrum = Spectrum1D(flux=simulated_spectra.s1_um_mJy_e1, spectral_axis=simulated_spectra.s1_um_mJy_e1.frequency) region = SpectralRegion(374740.5725*u.GHz, 499654.09666667*u.GHz) sub_spectrum = extract_region(spectrum, region) sub_spectrum_flux_expected = [ 1605.71612173, 1651.41650744, 2057.65798618, 2066.73502361, 1955.75832537, 1670.52711471, 1491.10034446, 1637.08084112, 1471.28982259, 1299.19484483, 1423.11195734, 1226.74494917, 1572.31888312, 1311.50503403, 1474.05051673, 1335.39944397, 1420.61880528, 1433.18623759, 1290.26966668, 1605.67341284, 1528.52281708, 1592.74392861, 1568.74162534, 1435.29407808, 1536.68040935, 1157.33825995, 1136.12679394, 999.92394692, 1038.61546167, 1011.60297294 ]*u.mJy assert quantity_allclose(sub_spectrum.flux, sub_spectrum_flux_expected) def test_region_simple_check_ends(simulated_spectra): np.random.seed(42) spectrum = Spectrum1D(spectral_axis=np.linspace(1, 25, 25)*u.um, flux=np.random.random(25)*u.Jy) region = SpectralRegion(8*u.um, 15*u.um) sub_spectrum = extract_region(spectrum, region) assert sub_spectrum.spectral_axis.value[0] == 8 assert sub_spectrum.spectral_axis.value[-1] == 15 def test_region_empty(simulated_spectra): np.random.seed(42) empty_spectrum = Spectrum1D(spectral_axis=[]*u.um, flux=[]*u.Jy) # Region past upper range of spectrum spectrum = Spectrum1D(spectral_axis=np.linspace(1, 25, 25)*u.um, flux=np.random.random(25)*u.Jy) region = SpectralRegion(28*u.um, 30*u.um) sub_spectrum = extract_region(spectrum, region) assert np.allclose(sub_spectrum.spectral_axis.value, empty_spectrum.spectral_axis.value) assert sub_spectrum.spectral_axis.unit == empty_spectrum.spectral_axis.unit assert np.allclose(sub_spectrum.flux.value, empty_spectrum.flux.value) assert sub_spectrum.flux.unit == empty_spectrum.flux.unit # Region below lower range of spectrum spectrum = Spectrum1D(spectral_axis=np.linspace(1, 25, 25)*u.um, flux=np.random.random(25)*u.Jy) region = SpectralRegion(0.1*u.um, 0.3*u.um) sub_spectrum = extract_region(spectrum, region) assert np.allclose(sub_spectrum.spectral_axis.value, empty_spectrum.spectral_axis.value) assert sub_spectrum.spectral_axis.unit == empty_spectrum.spectral_axis.unit assert np.allclose(sub_spectrum.flux.value, empty_spectrum.flux.value) assert sub_spectrum.flux.unit == empty_spectrum.flux.unit # Region below lower range of spectrum and upper range in the spectrum. spectrum = Spectrum1D(spectral_axis=np.linspace(1, 25, 25)*u.um, flux=2*np.linspace(1, 25, 25)*u.Jy) region = SpectralRegion(0.1*u.um, 3.3*u.um) sub_spectrum = extract_region(spectrum, region) assert np.allclose(sub_spectrum.spectral_axis.value, [1, 2, 3]) assert sub_spectrum.spectral_axis.unit == empty_spectrum.spectral_axis.unit assert np.allclose(sub_spectrum.flux.value, [2, 4, 6]) assert sub_spectrum.flux.unit == empty_spectrum.flux.unit # Region has lower and upper bound the same with pytest.raises(Exception) as e_info: region = SpectralRegion(3*u.um, 3*u.um) def test_region_two_sub(simulated_spectra): np.random.seed(42) spectrum = simulated_spectra.s1_um_mJy_e1 uncertainty = StdDevUncertainty(0.1*np.random.random(len(spectrum.flux))*u.mJy) spectrum.uncertainty = uncertainty region = SpectralRegion([(0.6*u.um, 0.8*u.um), (0.86*u.um, 0.89*u.um)]) sub_spectra = extract_region(spectrum, region) # Confirm the end points of the subspectra are correct assert quantity_allclose(sub_spectra[0].spectral_axis[[0, -1]], [0.6035353535353536, 0.793939393939394]*u.um) assert quantity_allclose(sub_spectra[1].spectral_axis[[0, -1]], [0.8661616161616162, 0.8858585858585859]*u.um) sub_spectrum_0_flux_expected = [ 1605.71612173, 1651.41650744, 2057.65798618, 2066.73502361, 1955.75832537, 1670.52711471, 1491.10034446, 1637.08084112, 1471.28982259, 1299.19484483, 1423.11195734, 1226.74494917, 1572.31888312, 1311.50503403, 1474.05051673, 1335.39944397, 1420.61880528, 1433.18623759, 1290.26966668, 1605.67341284, 1528.52281708, 1592.74392861, 1568.74162534, 1435.29407808, 1536.68040935, 1157.33825995, 1136.12679394, 999.92394692, 1038.61546167, 1011.60297294 ]*u.mJy sub_spectrum_1_flux_expected = [1337.65312465, 1263.48914109, 1589.81797876, 1548.46068415]*u.mJy assert quantity_allclose(sub_spectra[0].flux, sub_spectrum_0_flux_expected) assert quantity_allclose(sub_spectra[1].flux, sub_spectrum_1_flux_expected) # also ensure this works if the multi-region is expressed as a single # Quantity region2 = SpectralRegion([(0.6, 0.8), (0.86, 0.89)]*u.um) sub_spectra2 = extract_region(spectrum, region2) assert quantity_allclose(sub_spectra[0].flux, sub_spectra2[0].flux) assert quantity_allclose(sub_spectra[1].flux, sub_spectra2[1].flux) def test_extract_region_pixels(): spectrum = Spectrum1D(spectral_axis=np.linspace(4000, 10000, 25)*u.AA, flux=np.arange(25)*u.Jy) region = SpectralRegion(10*u.pixel, 12*u.pixel) extracted = extract_region(spectrum, region) assert quantity_allclose(extracted.flux, [10, 11]*u.Jy) def test_extract_region_mismatched_units(): spectrum = Spectrum1D(spectral_axis=np.arange(25)*u.nm, flux=np.arange(25)*u.Jy) region = SpectralRegion(100*u.AA, 119*u.AA) extracted = extract_region(spectrum, region) assert quantity_allclose(extracted.flux, [10, 11]*u.Jy) def test_linear_excise_invert_from_spectrum(): spec = Spectrum1D(flux=np.random.sample(100) * u.Jy, spectral_axis=np.arange(100) * u.AA) inc_regs = SpectralRegion(80 * u.AA, 90 * u.AA) + \ SpectralRegion(50 * u.AA, 60 * u.AA) exc_regs = inc_regs.invert_from_spectrum(spec) excised_spec = linear_exciser(spec, exc_regs) assert quantity_allclose(np.diff(excised_spec[50:60].flux), np.diff(excised_spec[51:61].flux)) assert quantity_allclose(np.diff(excised_spec[80:90].flux), np.diff(excised_spec[81:91].flux)) def test_extract_masked(): wl = [1, 2, 3, 4]*u.nm flux = np.arange(4)*u.Jy mask = [False, False, True, True] masked_spec = Spectrum1D(spectral_axis=wl, flux=flux, mask=mask) region = SpectralRegion(1.5 * u.nm, 3.5 * u.nm) extracted = extract_region(masked_spec, region) assert np.all(extracted.mask == [False, True]) assert np.all(extracted.flux.value == [1, 2]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/tests/test_regions.py0000644000076500000240000001167300000000000021715 0ustar00erikstaff00000000000000import pytest import numpy as np import astropy.units as u from astropy.nddata import StdDevUncertainty from ..spectra import Spectrum1D, SpectralRegion def test_lower_upper(): # Spectral region with just one range (lower and upper bound) sr = SpectralRegion(0.45*u.um, 0.6*u.um) assert sr.lower == 0.45*u.um assert sr.upper == 0.6*u.um # Spectral region with just two ranges sr = SpectralRegion([(0.45*u.um, 0.6*u.um), (0.8*u.um, 0.9*u.um)]) assert sr.lower == 0.45*u.um assert sr.upper == 0.9*u.um # Spectral region with multiple ranges and not ordered sr = SpectralRegion([(0.3*u.um, 1.0*u.um), (0.45*u.um, 0.6*u.um), (0.04*u.um, 0.05*u.um), (0.8*u.um, 0.9*u.um)]) assert sr.lower == 0.04*u.um assert sr.upper == 1.0*u.um # Get lower bound of a single sub-region: assert sr[0].lower == 0.04*u.um assert sr[0].upper == 0.05*u.um def test_from_center(): # Spectral region from center with width sr = SpectralRegion.from_center(center=6563*u.AA, width=10*u.AA) assert sr.lower == 6553.0*u.AA assert sr.upper == 6573.0*u.AA # Check the exception if the width is negative. with pytest.raises(ValueError) as exc: sr = SpectralRegion.from_center(center=6563*u.AA, width=-10*u.AA) # Check the exception if the width is 0. with pytest.raises(ValueError) as exc: sr = SpectralRegion.from_center(center=6563*u.AA, width=0*u.AA) def test_adding_spectral_regions(): # Combine two Spectral regions into one: sr = SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) assert set(sr.subregions) == set([(0.45*u.um, 0.6*u.um), (0.8*u.um, 0.9*u.um)]) # In-place adding spectral regions: sr1 = SpectralRegion(0.45*u.um, 0.6*u.um) sr2 = SpectralRegion(0.8*u.um, 0.9*u.um) sr1 += sr2 assert set(sr1.subregions) == set([(0.45*u.um, 0.6*u.um), (0.8*u.um, 0.9*u.um)]) def test_getitem(): sr = SpectralRegion([(0.8*u.um, 0.9*u.um), (0.3*u.um, 1.0*u.um), (0.45*u.um, 0.6*u.um), (0.04*u.um, 0.05*u.um)]) assert sr[0].subregions == [(0.04*u.um, 0.05*u.um)] assert sr[1].subregions == [(0.3*u.um, 1.0*u.um)] assert sr[2].subregions == [(0.45*u.um, 0.6*u.um)] assert sr[3].subregions == [(0.8*u.um, 0.9*u.um)] assert sr[-1].subregions == [(0.8*u.um, 0.9*u.um)] def test_bounds(): # Single subregion sr = SpectralRegion(0.45*u.um, 0.6*u.um) assert sr.bounds == (0.45*u.um, 0.6*u.um) # Multiple subregions sr = SpectralRegion([(0.8*u.um, 0.9*u.um), (0.3*u.um, 1.0*u.um), (0.45*u.um, 0.6*u.um), (0.04*u.um, 0.05*u.um)]) assert sr.bounds == (0.04*u.um, 1.0*u.um) def test_delitem(): # Single subregion sr = SpectralRegion(0.45*u.um, 0.6*u.um) del sr[0] assert sr.subregions == [] # Multiple sub-regions sr = SpectralRegion([(0.8*u.um, 0.9*u.um), (0.3*u.um, 1.0*u.um), (0.45*u.um, 0.6*u.um), (0.04*u.um, 0.05*u.um)]) del sr[1] assert sr[0].subregions == [(0.04*u.um, 0.05*u.um)] assert sr[1].subregions == [(0.45*u.um, 0.6*u.um)] assert sr[2].subregions == [(0.8*u.um, 0.9*u.um)] def test_iterate(): # Create the Spectral region subregions = [(0.8*u.um, 0.9*u.um), (0.3*u.um, 1.0*u.um), (0.45*u.um, 0.6*u.um), (0.04*u.um, 0.05*u.um)] sr = SpectralRegion(subregions) # For testing, sort our subregion list. subregions.sort(key=lambda k: k[0]) for ii, s in enumerate(sr): assert s.subregions[0] == subregions[ii] def test_slicing(): sr = SpectralRegion(0.15*u.um, 0.2*u.um) + SpectralRegion(0.3*u.um, 0.4*u.um) +\ SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) +\ SpectralRegion(1.0*u.um, 1.2*u.um) + SpectralRegion(1.3*u.um, 1.5*u.um) subsr = sr[3:5] assert subsr[0].subregions == [(0.8*u.um, 0.9*u.um)] assert subsr[1].subregions == [(1.0*u.um, 1.2*u.um)] def test_invert(): sr = SpectralRegion(0.15*u.um, 0.2*u.um) + SpectralRegion(0.3*u.um, 0.4*u.um) +\ SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) +\ SpectralRegion(1.0*u.um, 1.2*u.um) + SpectralRegion(1.3*u.um, 1.5*u.um) sr_inverted_expected = [(0.05*u.um, 0.15*u.um), (0.2*u.um, 0.3*u.um), (0.4*u.um, 0.45*u.um), (0.6*u.um, 0.8*u.um), (0.9*u.um, 1.0*u.um), (1.2*u.um, 1.3*u.um), (1.5*u.um, 3.0*u.um)] # Invert from range. sr_inverted = sr.invert(0.05*u.um, 3*u.um) for ii, expected in enumerate(sr_inverted_expected): assert sr_inverted.subregions[ii] == sr_inverted_expected[ii] # Invert from spectrum. spectrum = Spectrum1D(spectral_axis=np.linspace(0.05, 3, 20)*u.um, flux=np.random.random(20)*u.Jy) sr_inverted = sr.invert_from_spectrum(spectrum) for ii, expected in enumerate(sr_inverted_expected): assert sr_inverted.subregions[ii] == sr_inverted_expected[ii] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/tests/test_resample.py0000644000076500000240000002000500000000000022044 0ustar00erikstaff00000000000000import numpy as np import pytest import astropy.units as u from astropy.nddata import InverseVariance, StdDevUncertainty from astropy.tests.helper import assert_quantity_allclose from ..spectra.spectrum1d import Spectrum1D from ..tests.spectral_examples import simulated_spectra from ..manipulation.resample import FluxConservingResampler, LinearInterpolatedResampler, SplineInterpolatedResampler @pytest.fixture(params=[FluxConservingResampler, LinearInterpolatedResampler, SplineInterpolatedResampler]) def all_resamplers(request): return request.param # todo: Should add tests for different weighting options once those # are more solidified. def test_same_grid_fluxconserving(simulated_spectra): """ Test that feeding in the original dispersion axis returns the same flux after resampling. """ input_spectra = simulated_spectra.s1_um_mJy_e1 input_spectra.uncertainty = InverseVariance([0.5]*len(simulated_spectra.s1_um_mJy_e1.flux)) inst = FluxConservingResampler() results = inst(input_spectra, simulated_spectra.s1_um_mJy_e1.spectral_axis) assert np.allclose(np.array(simulated_spectra.s1_um_mJy_e1.flux), np.array(results.flux)) assert np.allclose(input_spectra.uncertainty.array, results.uncertainty.array) def test_expanded_grid_fluxconserving(): """ New dispersion axis has more bins then input dispersion axis """ flux_val = np.array([1, 3, 7, 6, 20]) wave_val = np.array([2, 4, 12, 16, 20]) input_spectra = Spectrum1D(flux=flux_val * u.mJy, spectral_axis=wave_val * u.nm) resamp_grid = [1, 5, 9, 13, 14, 17, 21, 22, 23] * u.nm inst = FluxConservingResampler() results = inst(input_spectra, resamp_grid) assert_quantity_allclose(results.flux, np.array([np.nan, 3., 6.13043478, 7., 6.33333333, 10., 20., np.nan, np.nan])*u.mJy) def test_stddev_uncert_propogation(): """ Check uncertainty propagation if input uncertainty is InverseVariance """ flux_val = np.array([1, 3, 7, 6, 20]) wave_val = np.array([20, 30, 40, 50, 60]) input_spectra = Spectrum1D(flux=flux_val * u.mJy, spectral_axis=wave_val * u.AA, uncertainty=StdDevUncertainty([0.1, 0.25, 0.1, 0.25, 0.1])) inst = FluxConservingResampler() results = inst(input_spectra, [25, 35, 50, 55]*u.AA) assert np.allclose(results.uncertainty.array, np.array([27.5862069, 38.23529412, 17.46724891, 27.5862069])) def delta_wl(saxis): """ A helper function that computes the "size" of a bin given the bin centers for testing the flux conservation """ l_widths = (saxis[1] - saxis[0]) r_widths = (saxis[-1] - saxis[-2]) # if three bins 0,1,2; want width of central bin. width is avg of 1/2 minus # average of 0/1: (i1 + i2)/2 - (i0 + i1)/2 = (i2 - i0)/2 mid_widths = (saxis[2:] - saxis[:-2]) / 2 return np.concatenate([[l_widths.value], mid_widths.value, [r_widths.value]])*saxis.unit @pytest.mark.parametrize("specflux,specwavebins,outwavebins", [ ([1, 3, 2], [4000, 5000, 6000, 7000], np.linspace(4000, 7000, 5)), ([1, 3, 2, 1], np.linspace(4000, 7000, 5), [4000, 5000, 6000, 7000]) ]) def test_flux_conservation(specflux, specwavebins, outwavebins): """ A few simple cases to programatically ensure flux is conserved in the resampling algorithm """ specwavebins = specwavebins*u.AA outwavebins = outwavebins*u.AA specflux = specflux*u.AB specwave = (specwavebins[:-1] + specwavebins[1:])/2 outwave = (outwavebins[:-1] + outwavebins[1:])/2 in_spec = Spectrum1D(spectral_axis=specwave, flux=specflux) out_spec = FluxConservingResampler()(in_spec, outwave) in_dwl = delta_wl(in_spec.spectral_axis) out_dwl = delta_wl(out_spec.spectral_axis) flux_in = np.sum(in_spec.flux * in_dwl) flux_out = np.sum(out_spec.flux * out_dwl) assert_quantity_allclose(flux_in, flux_out) def test_multi_dim_spectrum1D(): """ Test for input spectrum1Ds that have a two dimensional flux and uncertainty. """ flux_2d = np.array([np.ones(10) * 5, np.ones(10) * 6, np.ones(10) * 7]) input_spectra = Spectrum1D(spectral_axis=np.arange(5000, 5010) * u.AA, flux=flux_2d * u.Jy, uncertainty=StdDevUncertainty(flux_2d / 10)) inst = FluxConservingResampler() results = inst(input_spectra, [5001, 5003, 5005, 5007] * u.AA) assert_quantity_allclose(results.flux, np.array([[5., 5., 5., 5.], [6., 6., 6., 6.], [7., 7., 7., 7.]]) * u.Jy) assert np.allclose(results.uncertainty.array, np.array([[4., 4., 4., 4.], [2.77777778, 2.77777778, 2.77777778, 2.77777778], [2.04081633, 2.04081633, 2.04081633, 2.04081633]] )) def test_expanded_grid_interp_linear(): """ New dispersion axis has more bins then input dispersion axis """ flux_val = np.array([1, 3, 7, 6, 20]) wave_val = np.array([2, 4, 12, 16, 20]) input_spectra = Spectrum1D(spectral_axis=wave_val * u.AA, flux=flux_val * u.mJy) resamp_grid = [1, 5, 9, 13, 14, 17, 21, 22, 23] * u.AA inst = LinearInterpolatedResampler() results = inst(input_spectra, resamp_grid) assert_quantity_allclose(results.flux, np.array([np.nan, 3.5, 5.5, 6.75, 6.5, 9.5, np.nan, np.nan, np.nan])*u.mJy) def test_expanded_grid_interp_spline(): """ New dispersion axis has more bins then input dispersion axis """ flux_val = np.array([1, 3, 7, 6, 20]) wave_val = np.array([2, 4, 12, 16, 20]) input_spectra = Spectrum1D(spectral_axis=wave_val * u.AA, flux=flux_val * u.mJy) resamp_grid = [1, 5, 9, 13, 14, 17, 21, 22, 23] * u.AA inst = SplineInterpolatedResampler() results = inst(input_spectra, resamp_grid) assert_quantity_allclose(results.flux, np.array([np.nan, 3.98808594, 6.94042969, 6.45869141, 5.89921875, 7.29736328, np.nan, np.nan, np.nan])*u.mJy) @pytest.mark.parametrize("edgetype,lastvalue", [("nan_fill", np.nan), ("zero_fill", 0)]) def test_resample_edges(edgetype, lastvalue, all_resamplers): input_spectrum = Spectrum1D(spectral_axis=[2, 4, 12, 16, 20] * u.micron, flux=[1, 3, 7, 6, 20] * u.mJy) resamp_grid = [1, 3, 7, 6, 20, 100] * u.micron resampler = all_resamplers(edgetype) resampled = resampler(input_spectrum, resamp_grid) if lastvalue is np.nan: assert np.isnan(resampled.flux[-1]) else: assert resampled.flux[-1] == lastvalue def test_resample_different_units(all_resamplers): input_spectrum = Spectrum1D(spectral_axis=[5000, 6000 ,7000] * u.AA, flux=[1, 2, 3] * u.mJy) resampler = all_resamplers("nan_fill") if all_resamplers == FluxConservingResampler: pytest.xfail('flux conserving resampler cannot yet handle differing units') resamp_grid = [5500, 6500]*u.nm resampled = resampler(input_spectrum, resamp_grid) assert np.all(np.isnan(resampled.flux)) resamp_grid = [550, 650]*u.nm resampled = resampler(input_spectrum, resamp_grid) assert not np.any(np.isnan(resampled.flux)) def test_resample_uncs(all_resamplers): sdunc = StdDevUncertainty([0.1,0.2, 0.3]*u.mJy) input_spectrum = Spectrum1D(spectral_axis=[5000, 6000 ,7000] * u.AA, flux=[1, 2, 3] * u.mJy, uncertainty=sdunc) resampled = all_resamplers()(input_spectrum, [5500, 6500]*u.AA) if all_resamplers == FluxConservingResampler: # special-cased because it switches the unc to inverse variance by construction assert resampled.uncertainty.unit == sdunc.unit**-2 else: assert resampled.uncertainty.unit == sdunc.unit assert resampled.uncertainty.uncertainty_type == sdunc.uncertainty_type ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/tests/test_slicing.py0000644000076500000240000000737500000000000021703 0ustar00erikstaff00000000000000import astropy.units as u import astropy.wcs as fitswcs from astropy.tests.helper import quantity_allclose import numpy as np from numpy.testing import assert_allclose from ..spectra.spectrum1d import Spectrum1D def test_spectral_axes(): spec1 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49) * 100 * u.Jy) sliced_spec1 = spec1[0] assert isinstance(sliced_spec1, Spectrum1D) assert_allclose(sliced_spec1.wcs.pixel_to_world(np.arange(10)), spec1.wcs.pixel_to_world(np.arange(10))) flux2 = np.random.sample((10, 49)) * 100 spec2 = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=flux2 * u.Jy) sliced_spec2 = spec2[0] assert isinstance(sliced_spec2, Spectrum1D) assert_allclose(sliced_spec2.wcs.pixel_to_world(np.arange(10)), spec2.wcs.pixel_to_world(np.arange(10))) assert sliced_spec2.flux.shape[0] == 49 def test_slicing(): # Create the initial spectrum spec = Spectrum1D(spectral_axis=np.arange(10) * u.um, flux=2*np.arange(10)*u.Jy) # Slice it. sub_spec = spec[4:8] # Check basic spectral_axis property assert sub_spec.spectral_axis.unit == u.um assert np.allclose(sub_spec.spectral_axis.value, np.array([4, 5, 6, 7])) assert np.allclose(sub_spec.flux.value, np.array([8, 10, 12, 14])) assert sub_spec.wavelength.unit == u.AA assert np.allclose(sub_spec.wavelength.value, np.array([40000., 50000., 60000., 70000.])) assert sub_spec.frequency.unit == u.GHz assert np.allclose(sub_spec.frequency.value, np.array([74948.1145, 59958.4916, 49965.40966667, 42827.494])) # Do it a second time to confirm the original was not modified. sub_spec2 = spec[1:5] # Check basic spectral_axis property assert sub_spec2.spectral_axis.unit == u.um assert np.allclose(sub_spec2.spectral_axis.value, np.array([1, 2, 3, 4])) assert np.allclose(sub_spec2.flux.value, np.array([2, 4, 6, 8])) assert sub_spec2.wavelength.unit == u.AA assert np.allclose(sub_spec2.wavelength.value, np.array([10000., 20000., 30000., 40000.])) assert sub_spec2.frequency.unit == u.GHz assert np.allclose(sub_spec2.frequency.value, np.array([299792.458, 149896.229, 99930.81933333, 74948.1145])) # Going to repeat these to make sure the original spectrum was # not modified in some way assert spec.spectral_axis.unit == u.um assert np.allclose(spec.spectral_axis.value, np.array(np.arange(10))) assert np.allclose(spec.flux.value, np.array(2*np.arange(10))) assert spec.wavelength.unit == u.AA assert np.allclose(spec.wavelength.value, np.array(10000*np.arange(10))) assert sub_spec.frequency.unit == u.GHz assert np.allclose(sub_spec.frequency.value, np.array([74948.1145, 59958.4916, 49965.40966667, 42827.494])) def test_slicing_with_fits(): my_wcs = fitswcs.WCS(header={'CDELT1': 1, 'CRVAL1': 6562.8, 'CUNIT1': 'Angstrom', 'CTYPE1': 'WAVE', 'RESTFRQ': 1400000000, 'CRPIX1': 25}) spec = Spectrum1D(flux=[5, 6, 7, 8, 9, 10] * u.Jy, wcs=my_wcs) spec_slice = spec[1:5] assert isinstance(spec_slice, Spectrum1D) assert spec_slice.flux.size == 4 assert np.allclose(spec_slice.wcs.pixel_to_world([6, 7, 8, 9]).value, spec.wcs.pixel_to_world([6, 7, 8, 9]).value) def test_slicing_multidim(): spec = Spectrum1D(spectral_axis=np.arange(10) * u.AA, flux=np.random.sample((5, 10)) * u.Jy) spec1 = spec[0] spec2 = spec[1:3] assert spec1.flux[0] == spec.flux[0][0] assert quantity_allclose(spec1.spectral_axis, spec.spectral_axis) assert spec.flux.shape[1:] == spec1.flux.shape assert quantity_allclose(spec2.flux, spec.flux[1:3]) assert quantity_allclose(spec2.spectral_axis, spec.spectral_axis) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/tests/test_smoothing.py0000644000076500000240000002110700000000000022247 0ustar00erikstaff00000000000000import numpy as np import pytest from astropy import convolution from scipy.signal import medfilt import astropy.units as u from astropy.nddata import StdDevUncertainty, VarianceUncertainty, InverseVariance from ..spectra.spectrum1d import Spectrum1D from ..tests.spectral_examples import simulated_spectra from ..manipulation.smoothing import (convolution_smooth, box_smooth, gaussian_smooth, trapezoid_smooth, median_smooth) def compare_flux(flux_smooth1, flux_smooth2, flux_original, rtol=0.01): """ There are two things to compare for each set of smoothing: 1. Compare the smoothed flux from the astropy machinery vs the smoothed flux from specutils. This is done by comparing flux_smooth1 and flux_smooth2. 2. Next we want to compare the smoothed flux to the original flux. This is a little more difficult as smoothing will make a difference for median filter, but less so for convolution based smoothing if the kernel is normalized (area under the kernel = 1). In this second case the rtol (relative tolerance) is used judiciously. """ # Compare, element by element, the two smoothed fluxes. assert np.allclose(flux_smooth1, flux_smooth2) # Compare the total spectral flux of the smoothed to the original. assert np.allclose(sum(flux_smooth1), sum(flux_original), rtol=rtol) def test_smooth_custom_kernel(simulated_spectra): """ Test CustomKernel smoothing with correct parmaeters. """ # Create the original spectrum spec1 = simulated_spectra.s1_um_mJy_e1 flux_original = spec1.flux # Create a custom kernel (some weird asymmetric-ness) numpy_kernel = np.array([0.5, 1, 2, 0.5, 0.2]) numpy_kernel = numpy_kernel / np.sum(numpy_kernel) custom_kernel = convolution.CustomKernel(numpy_kernel) flux_smoothed_astropy = convolution.convolve(flux_original, custom_kernel) # Calculate the custom smoothed spec1_smoothed = convolution_smooth(spec1, custom_kernel) compare_flux(spec1_smoothed.flux.value, flux_smoothed_astropy, flux_original.value) @pytest.mark.parametrize("width", [1, 2.3]) def test_smooth_box_good(simulated_spectra, width): """ Test Box1DKernel smoothing with correct parmaeters. Width values need to be a number greater than 0. """ # Create the original spectrum spec1 = simulated_spectra.s1_um_mJy_e1 flux_original = spec1.flux # Calculate the smoothed flux using Astropy box_kernel = convolution.Box1DKernel(width) flux_smoothed_astropy = convolution.convolve(flux_original, box_kernel) # Calculate the box smoothed spec1_smoothed = box_smooth(spec1, width) compare_flux(spec1_smoothed.flux.value, flux_smoothed_astropy, flux_original.value) # Check the input and output units assert spec1.wavelength.unit == spec1_smoothed.wavelength.unit assert spec1.flux.unit == spec1_smoothed.flux.unit @pytest.mark.parametrize("width", [-1, 0, 'a']) def test_smooth_box_bad(simulated_spectra, width): """ Test Box1DKernel smoothing with incorrect parmaeters. Width values need to be a number greater than 0. """ # Create the spectrum spec1 = simulated_spectra.s1_um_mJy_e1 # Test bad input parameters with pytest.raises(ValueError): box_smooth(spec1, width) @pytest.mark.parametrize("stddev", [1, 2.3]) def test_smooth_gaussian_good(simulated_spectra, stddev): """ Test Gaussian1DKernel smoothing with correct parmaeters. Standard deviation values need to be a number greater than 0. """ # Create the spectrum spec1 = simulated_spectra.s1_um_mJy_e1 flux_original = spec1.flux # Calculate the smoothed flux using Astropy gaussian_kernel = convolution.Gaussian1DKernel(stddev) flux_smoothed_astropy = convolution.convolve(flux_original, gaussian_kernel) # Test gaussian smoothing spec1_smoothed = gaussian_smooth(spec1, stddev) compare_flux(spec1_smoothed.flux.value, flux_smoothed_astropy, flux_original.value, rtol=0.02) # Check the input and output units assert spec1.wavelength.unit == spec1_smoothed.wavelength.unit assert spec1.flux.unit == spec1_smoothed.flux.unit @pytest.mark.parametrize("stddev", [-1, 0, 'a']) def test_smooth_gaussian_bad(simulated_spectra, stddev): """ Test MexicanHat1DKernel smoothing with incorrect parmaeters. Standard deviation values need to be a number greater than 0. """ # Create the spectrum spec1 = simulated_spectra.s1_um_mJy_e1 # Test bad input paramters with pytest.raises(ValueError): gaussian_smooth(spec1, stddev) @pytest.mark.parametrize("stddev", [1, 2.3]) def test_smooth_trapezoid_good(simulated_spectra, stddev): """ Test Trapezoid1DKernel smoothing with correct parmaeters. Standard deviation values need to be a number greater than 0. """ # Create the spectrum spec1 = simulated_spectra.s1_um_mJy_e1 flux_original = spec1.flux # Create the flux_smoothed which is what we want to compare to trapezoid_kernel = convolution.Trapezoid1DKernel(stddev) flux_smoothed_astropy = convolution.convolve(flux_original, trapezoid_kernel) # Test trapezoid smoothing spec1_smoothed = trapezoid_smooth(spec1, stddev) compare_flux(spec1_smoothed.flux.value, flux_smoothed_astropy, flux_original.value) # Check the input and output units assert spec1.wavelength.unit == spec1_smoothed.wavelength.unit assert spec1.flux.unit == spec1_smoothed.flux.unit @pytest.mark.parametrize("stddev", [-1, 0, 'a']) def test_smooth_trapezoid_bad(simulated_spectra, stddev): """ Test Trapezoid1DKernel smoothing with incorrect parmaeters. Standard deviation values need to be a number greater than 0. """ # Create the spectrum spec1 = simulated_spectra.s1_um_mJy_e1 # Test bad parameters with pytest.raises(ValueError): trapezoid_smooth(spec1, stddev) @pytest.mark.parametrize("width", [1, 3, 9]) def test_smooth_median_good(simulated_spectra, width): """ Test Median smoothing with correct parmaeters. Width values need to be a number greater than 0. """ # Create the spectrum spec1 = simulated_spectra.s1_um_mJy_e1 flux_original = spec1.flux # Create the flux_smoothed which is what we want to compare to flux_smoothed_astropy = medfilt(flux_original, width) # Test median smoothing spec1_smoothed = median_smooth(spec1, width) compare_flux(spec1_smoothed.flux.value, flux_smoothed_astropy, flux_original.value, rtol=0.15) # Check the input and output units assert spec1.wavelength.unit == spec1_smoothed.wavelength.unit assert spec1.flux.unit == spec1_smoothed.flux.unit @pytest.mark.parametrize("width", [-1, 0, 'a']) def test_smooth_median_bad(simulated_spectra, width): """ Test Median smoothing with incorrect parmaeters. Width values need to be a number greater than 0. """ # Create the spectrum spec1 = simulated_spectra.s1_um_mJy_e1 # Test bad parameters with pytest.raises(ValueError): median_smooth(spec1, width) def test_smooth_custom_kernel_uncertainty(simulated_spectra): """ Test CustomKernel smoothing with correct parmaeters. """ np.random.seed(42) # Create a custom kernel (some weird asymmetric-ness) numpy_kernel = np.array([0.5, 1, 2, 0.5, 0.2]) numpy_kernel = numpy_kernel / np.sum(numpy_kernel) custom_kernel = convolution.CustomKernel(numpy_kernel) spec1 = simulated_spectra.s1_um_mJy_e1 uncertainty = np.abs(np.random.random(spec1.flux.shape)) # Test StdDevUncertainty spec1.uncertainty = StdDevUncertainty(uncertainty) spec1_smoothed = convolution_smooth(spec1, custom_kernel) tt = convolution.convolve(1/(spec1.uncertainty.array**2), custom_kernel) uncertainty_smoothed_astropy = 1/np.sqrt(tt) assert np.allclose(spec1_smoothed.uncertainty.array, uncertainty_smoothed_astropy) # Test VarianceUncertainty spec1.uncertainty = VarianceUncertainty(uncertainty) spec1_smoothed = convolution_smooth(spec1, custom_kernel) uncertainty_smoothed_astropy = 1/convolution.convolve(1/spec1.uncertainty.array, custom_kernel) assert np.allclose(spec1_smoothed.uncertainty.array, uncertainty_smoothed_astropy) # Test InverseVariance spec1.uncertainty = InverseVariance(uncertainty) spec1_smoothed = convolution_smooth(spec1, custom_kernel) uncertainty_smoothed_astropy = convolution.convolve(spec1.uncertainty.array, custom_kernel) assert np.allclose(spec1_smoothed.uncertainty.array, uncertainty_smoothed_astropy) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/tests/test_spectrum1d.py0000644000076500000240000003341700000000000022336 0ustar00erikstaff00000000000000import astropy.units as u import astropy.wcs as fitswcs import gwcs import numpy as np import pytest from astropy.nddata import StdDevUncertainty from .conftest import remote_access from ..spectra import Spectrum1D def test_empty_spectrum(): spec = Spectrum1D(spectral_axis=[]*u.um, flux=[]*u.Jy) assert isinstance(spec.spectral_axis, u.Quantity) assert spec.spectral_axis.size == 0 assert isinstance(spec.flux, u.Quantity) assert spec.flux.size == 0 def test_create_from_arrays(): spec = Spectrum1D(spectral_axis=np.arange(50) * u.AA, flux=np.random.randn(50) * u.Jy) assert isinstance(spec.spectral_axis, u.Quantity) assert spec.spectral_axis.size == 50 assert isinstance(spec.flux, u.Quantity) assert spec.flux.size == 50 # Test creating spectrum with unknown arguments with pytest.raises(ValueError) as e_info: spec = Spectrum1D(wavelength=np.arange(1, 50) * u.nm, flux=np.random.randn(48) * u.Jy) def test_create_from_multidimensional_arrays(): """ This is a test for a bug that was fixed by #283. It makes sure that multidimensional flux arrays are handled properly when creating Spectrum1D objects. """ freqs = np.arange(50) * u.GHz flux = np.random.random((5, len(freqs))) * u.Jy spec = Spectrum1D(spectral_axis=freqs, flux=flux) assert (spec.frequency == freqs).all() assert (spec.flux == flux).all() # Mis-matched lengths should raise an exception freqs = np.arange(50) * u.GHz flux = np.random.random((5, len(freqs)-1)) * u.Jy with pytest.raises(ValueError) as e_info: spec = Spectrum1D(spectral_axis=freqs, flux=flux) def test_create_from_quantities(): spec = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.randn(49) * u.Jy) assert isinstance(spec.spectral_axis, u.Quantity) assert spec.spectral_axis.unit == u.nm assert spec.spectral_axis.size == 49 # Mis-matched lengths should raise an exception with pytest.raises(ValueError) as e_info: spec = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.randn(48) * u.Jy) def test_create_implicit_wcs(): spec = Spectrum1D(spectral_axis=np.arange(50) * u.AA, flux=np.random.randn(50) * u.Jy) assert isinstance(spec.wcs, gwcs.wcs.WCS) pix2world = spec.wcs.pixel_to_world(np.arange(5, 10)) assert pix2world.size == 5 assert isinstance(pix2world, np.ndarray) def test_create_implicit_wcs_with_spectral_unit(): spec = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.randn(49) * u.Jy) assert isinstance(spec.wcs, gwcs.wcs.WCS) pix2world = spec.wcs.pixel_to_world(np.arange(5, 10)) assert pix2world.size == 5 assert isinstance(pix2world, np.ndarray) def test_spectral_axis_conversions(): # By default the spectral axis units should be set to angstroms spec = Spectrum1D(flux=np.array([26.0, 44.5]) * u.Jy, spectral_axis=np.array([400, 500]) * u.AA) assert np.all(spec.spectral_axis == np.array([400, 500]) * u.angstrom) assert spec.spectral_axis.unit == u.angstrom spec = Spectrum1D(spectral_axis=np.arange(50) * u.AA, flux=np.random.randn(50) * u.Jy) assert spec.wavelength.unit == u.AA spec = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.randn(49) * u.Jy) assert spec.frequency.unit == u.GHz with pytest.raises(ValueError) as e_info: spec.velocity spec = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.randn(49) * u.Jy) new_spec = spec.with_spectral_unit(u.GHz) def test_redshift(): spec = Spectrum1D(flux=np.array([26.0, 30., 44.5]) * u.Jy, spectral_axis=np.array([4000, 6000, 8000]) * u.AA, velocity_convention='optical', rest_value=6000 * u.AA) assert u.allclose(spec.velocity, [-99930.8, 0, 99930.8]*u.km/u.s, atol=0.5*u.km/u.s) spec = Spectrum1D(flux=np.array([26.0, 30., 44.5]) * u.Jy, spectral_axis=np.array([4000, 6000, 8000]) * u.AA, velocity_convention='optical', rest_value=6000 * u.AA, redshift= 0.1) assert u.allclose(spec.velocity, [-69951.3, 29979.2, 129910.1]*u.km/u.s, atol=0.5*u.km/u.s) #------------------------- spec = Spectrum1D(flux=np.array([26.0, 30.0, 44.5]) * u.Jy, spectral_axis=np.array([10.5, 11.0, 11.5]) * u.GHz, velocity_convention='radio', rest_value=11.0 * u.GHz) assert u.allclose(spec.velocity, [13626., 0, -13626]*u.km/u.s, atol=1*u.km/u.s) spec = Spectrum1D(flux=np.array([26.0, 30.0, 44.5]) * u.Jy, spectral_axis=np.array([10.5, 11.0, 11.5]) * u.GHz, velocity_convention='radio', rest_value=11.0 * u.GHz, redshift= 0.1) assert u.allclose(spec.velocity, [43606., 29979., 16352.]*u.km/u.s, atol=1*u.km/u.s) #------------------------- radial velocity mode spec = Spectrum1D(flux=np.array([26.0, 30., 44.5]) * u.Jy, spectral_axis=np.array([4000, 6000, 8000]) * u.AA, velocity_convention='optical', rest_value=6000 * u.AA) assert u.allclose(spec.velocity, [-99930.8, 0.0, 99930.8]*u.km/u.s, atol=0.5*u.km/u.s) spec = Spectrum1D(flux=np.array([26.0, 30., 44.5]) * u.Jy, spectral_axis=np.array([4000, 6000, 8000]) * u.AA, velocity_convention='optical', rest_value=6000 * u.AA, radial_velocity=1000.*u.km/u.s) assert u.allclose(spec.velocity, [-98930.8, 1000.0, 100930.8]*u.km/u.s, atol=0.5*u.km/u.s) def test_flux_unit_conversion(): # By default the flux units should be set to Jy s = Spectrum1D(flux=np.array([26.0, 44.5]) * u.Jy, spectral_axis=np.array([400, 500]) * u.nm) assert np.all(s.flux == np.array([26.0, 44.5]) * u.Jy) assert s.flux.unit == u.Jy # Simple Unit Conversion s = Spectrum1D(flux=np.array([26.0, 44.5]) * u.Jy, spectral_axis=np.array([400, 500])*u.nm) converted_spec = s.new_flux_unit(unit=u.uJy) assert ((26.0 * u.Jy).to(u.uJy) == converted_spec.flux[0]) # Make sure incompatible units raise UnitConversionError with pytest.raises(u.UnitConversionError): s.new_flux_unit(unit=u.m) # Pass custom equivalencies s = Spectrum1D(flux=np.array([26.0, 44.5]) * u.Jy, spectral_axis=np.array([400, 500]) * u.nm) eq = [[u.Jy, u.m, lambda x: np.full_like(np.array(x), 1000.0, dtype=np.double), lambda x: np.full_like(np.array(x), 0.001, dtype=np.double)]] converted_spec = s.new_flux_unit(unit=u.m, equivalencies=eq) assert 1000.0 * u.m == converted_spec.flux[0] # Check if suppressing the unit conversion works s = Spectrum1D(flux=np.array([26.0, 44.5]) * u.Jy, spectral_axis=np.array([400, 500]) * u.nm) new_spec = s.new_flux_unit("uJy", suppress_conversion=True) assert new_spec.flux[0] == 26.0 * u.uJy def test_wcs_transformations(): # Test with a GWCS spec = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.randn(49) * u.Jy) pix_axis = spec.wcs.world_to_pixel(np.arange(20, 30) * u.nm) disp_axis = spec.wcs.pixel_to_world(np.arange(20, 30)) assert isinstance(pix_axis, np.ndarray) assert isinstance(disp_axis, u.Quantity) # Test transform with different unit with u.set_enabled_equivalencies(u.spectral()): spec.wcs.world_to_pixel(np.arange(20, 30) * u.GHz) # Test with a FITS WCS my_wcs = fitswcs.WCS(header={'CDELT1': 1, 'CRVAL1': 6562.8, 'CUNIT1': 'Angstrom', 'CTYPE1': 'WAVE', 'RESTFRQ': 1400000000, 'CRPIX1': 25}, naxis=1) spec = Spectrum1D(flux=[5,6,7] * u.Jy, wcs=my_wcs) pix_axis = spec.wcs.world_to_pixel(20 * u.um) disp_axis = spec.wcs.pixel_to_world(np.arange(20, 30)) assert isinstance(pix_axis, np.ndarray) assert isinstance(disp_axis, u.Quantity) assert np.allclose(spec.wcs.world_to_pixel(7000*u.AA), [461.2]) def test_create_explicit_fitswcs(): my_wcs = fitswcs.WCS(header={'CDELT1': 1, 'CRVAL1': 6562.8, 'CUNIT1': 'Angstrom', 'CTYPE1': 'WAVE', 'RESTFRQ': 1400000000, 'CRPIX1': 25}) spec = Spectrum1D(flux=[5,6,7] * u.Jy, wcs=my_wcs) spec = spec.with_velocity_convention("relativistic") assert isinstance(spec.spectral_axis, u.Quantity) assert spec.spectral_axis.unit.is_equivalent(u.AA) pix2world = spec.wcs.pixel_to_world(np.arange(3)) assert pix2world.size == 3 assert isinstance(spec.wavelength, u.Quantity) assert spec.wavelength.size == 3 assert spec.wavelength.unit == u.AA assert isinstance(spec.frequency, u.Quantity) assert spec.frequency.size == 3 assert spec.frequency.unit == u.GHz assert isinstance(spec.velocity, u.Quantity) assert spec.velocity.size == 3 assert spec.velocity.unit == u.Unit('km/s') def test_create_with_uncertainty(): spec = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49) * u.Jy, uncertainty=StdDevUncertainty(np.random.sample(49) * 0.1)) assert isinstance(spec.uncertainty, StdDevUncertainty) spec = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm, flux=np.random.sample(49) * u.Jy, uncertainty=StdDevUncertainty(np.random.sample(49) * 0.1)) assert spec.flux.unit == spec.uncertainty.unit # If flux and uncertainty are different sizes then raise exception wavelengths = np.arange(0, 10) flux=100*np.abs(np.random.randn(3, 4, 10))*u.Jy uncertainty = StdDevUncertainty(np.abs(np.random.randn(3, 2, 10))*u.Jy) with pytest.raises(ValueError) as e_info: s1d = Spectrum1D(spectral_axis=wavelengths*u.um, flux=flux, uncertainty=uncertainty) @remote_access([{'id': '1481190', 'filename': 'L5g_0355+11_Cruz09.fits'}]) def test_read_linear_solution(remote_data_path): spec = Spectrum1D.read(remote_data_path, format='wcs1d-fits') assert isinstance(spec, Spectrum1D) assert isinstance(spec.flux, u.Quantity) assert isinstance(spec.spectral_axis, u.Quantity) assert spec.flux.size == spec.data.size assert spec.spectral_axis.size == spec.data.size def test_energy_photon_flux(): spec = Spectrum1D(spectral_axis=np.linspace(100, 1000, 10) * u.nm, flux=np.random.randn(10)*u.Jy) assert spec.energy.size == 10 assert spec.photon_flux.size == 10 assert spec.photon_flux.unit == u.photon * u.cm**-2 * u.s**-1 * u.nm**-1 def test_repr(): spec_with_wcs = Spectrum1D(spectral_axis=np.linspace(100, 1000, 10) * u.nm, flux=np.random.random(10) * u.Jy) result = repr(spec_with_wcs) assert result.startswith('= 50*u.um) & (frequencies <= 80*u.um)) expected_uncertainty = np.std(flux[indices])*np.ones(len(frequencies)) assert quantity_allclose(spectrum_with_uncertainty.uncertainty.array, expected_uncertainty.value) assert isinstance(spectrum_with_uncertainty.uncertainty, StdDevUncertainty) # Same idea, but now with variance. spectrum_with_uncertainty = noise_region_uncertainty(spectrum, spectral_region, np.var) indices = np.nonzero((frequencies >= 50*u.um) & (frequencies <= 80*u.um)) expected_uncertainty = np.var(flux[indices])*np.ones(len(frequencies)) assert quantity_allclose(spectrum_with_uncertainty.uncertainty.array, expected_uncertainty.value) assert isinstance(spectrum_with_uncertainty.uncertainty, VarianceUncertainty) # Same idea, but now with inverse variance. spectrum_with_uncertainty = noise_region_uncertainty(spectrum, spectral_region, lambda x: 1/np.var(x)) indices = np.nonzero((frequencies >= 50*u.um) & (frequencies <= 80*u.um)) expected_uncertainty = 1/np.var(flux[indices])*np.ones(len(frequencies)) assert quantity_allclose(spectrum_with_uncertainty.uncertainty.array, expected_uncertainty.value) assert isinstance(spectrum_with_uncertainty.uncertainty, InverseVariance) # Now try with something that does not return Std, Var or IVar type of noise estimation with pytest.raises(ValueError) as e_info: spectrum_with_uncertainty = noise_region_uncertainty(spectrum, spectral_region, lambda x: np.std(x)**3) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/tests/test_utils.py0000644000076500000240000000307300000000000021402 0ustar00erikstaff00000000000000import pytest import numpy as np from astropy import units as u from astropy import modeling from specutils.utils import QuantityModel from ..utils.wcs_utils import refraction_index, vac_to_air, air_to_vac wavelengths = [300, 500, 1000] * u.nm data_index_refraction = { 'Griesen2006': np.array([3.07393068, 2.9434858 , 2.8925797 ]), 'Edlen1953': np.array([2.91557413, 2.78963801, 2.74148172]), 'Edlen1966': np.array([2.91554272, 2.7895973 , 2.74156098]), 'PeckReeder1972': np.array([2.91554211, 2.78960005, 2.74152561]), 'Morton2000': np.array([2.91568573, 2.78973402, 2.74169531]), 'Ciddor1996': np.array([2.91568633, 2.78973811, 2.74166131]) } def test_quantity_model(): c = modeling.models.Chebyshev1D(3) uc = QuantityModel(c, u.AA, u.km) assert uc(10*u.nm).to(u.m) == 0*u.m @pytest.mark.parametrize("method", data_index_refraction.keys()) def test_refraction_index(method): tmp = (refraction_index(wavelengths, method) - 1) * 1e4 assert np.isclose(tmp, data_index_refraction[method], atol=1e-7).all() @pytest.mark.parametrize("method", data_index_refraction.keys()) def test_air_to_vac(method): tmp = refraction_index(wavelengths, method) assert np.isclose(wavelengths.value * tmp, air_to_vac(wavelengths, method=method, scheme='inversion').value, rtol=1e-6).all() assert np.isclose(wavelengths.value, air_to_vac(vac_to_air(wavelengths, method=method), method=method, scheme='iteration').value, atol=1e-12).all() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/utils/0000755000076500000240000000000000000000000016624 5ustar00erikstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/utils/__init__.py0000644000076500000240000000004500000000000020734 0ustar00erikstaff00000000000000from .quantity_model import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578132243.0 specutils-0.7/specutils/utils/quantity_model.py0000644000076500000240000000470700000000000022244 0ustar00erikstaff00000000000000from astropy import units as u __all__ = ['QuantityModel'] class QuantityModel: """ The QuantityModel was created to wrap `~astropy.modeling.models` that do not have the ability to use `~astropy.units` in the parameters. Parameters ---------- unitless_model : `~astropy.modeling.Model` A model that does not have units input_units : `~astropy.units` Units for the dispersion axis return_units : `~astropy.units` Units for the flux axis Notes ----- When Astropy's modeling is updated so *all* models have the ability to have `~astropy.units.Quantity` on all parameters, then this will not be needed. """ def __init__(self, unitless_model, input_units, return_units): # should check that it's unitless somehow! self.unitless_model = unitless_model # we use the dict because now this "shadows" the unitless model's # input_units/ return_units self.__dict__['input_units'] = input_units self.__dict__['return_units'] = return_units def __hasattr_(self, nm): if nm in self.__dict__ or hasattr(self, self.unitless_model): return True return False def __getattr__(self, nm): if hasattr(self.unitless_model, nm): return getattr(self.unitless_model, nm) else: raise AttributeError("'{}' object has no attribute '{}'" "".format(self.__class__.__name__, nm)) def __setattr__(self, nm, val): if nm != 'unitless_model' and hasattr(self.unitless_model, nm): setattr(self.unitless_model, nm, val) else: super().__setattr__(nm, val) def __delattr__(self, nm): if hasattr(self.unitless_model, nm): delattr(self.unitless_model, nm) else: super().__delattr__(nm) def __dir__(self): thisdir = super().__dir__() modeldir = dir(self.unitless_model) return sorted(list(thisdir) + list(modeldir)) def __repr__(self): return (''.format(repr(self.unitless_model)[1:-1], self.input_units, self.return_units)) def __call__(self, x, *args, **kwargs): unitlessx = x.to(self.input_units).value result = self.unitless_model(unitlessx, *args, **kwargs) return u.Quantity(result, self.return_units, copy=False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578141477.0 specutils-0.7/specutils/utils/wcs_utils.py0000644000076500000240000006215600000000000021224 0ustar00erikstaff00000000000000""" Utilities for parsing, converting, and manipulating astropy FITS-WCS objects (i.e., wcsutils objects compliant with Griesen 2006 FITS Paper III) """ from astropy import units as u from astropy import constants import warnings from astropy.modeling import Fittable1DModel from astropy.modeling.models import Shift from astropy.modeling.tabular import Tabular1D from gwcs import coordinate_frames as cf from gwcs import WCS as GWCS import numpy as np import copy def _parse_velocity_convention(vc): if vc in (u.doppler_radio, 'radio', 'RADIO', 'VRAD', 'F', 'FREQ'): return u.doppler_radio elif vc in (u.doppler_optical, 'optical', 'OPTICAL', 'VOPT', 'W', 'WAVE'): return u.doppler_optical elif vc in (u.doppler_relativistic, 'relativistic', 'RELATIVE', 'VREL', 'speed', 'V', 'VELO'): return u.doppler_relativistic # These are the only linear transformations allowed LINEAR_CTYPES = {u.doppler_optical: 'VOPT', u.doppler_radio: 'VRAD', u.doppler_relativistic: 'VELO'} LINEAR_CTYPE_CHARS = {u.doppler_optical: 'W', u.doppler_radio: 'F', u.doppler_relativistic: 'V'} ALL_CTYPES = {'speed': LINEAR_CTYPES, 'frequency': 'FREQ', 'length': 'WAVE'} CTYPE_TO_PHYSICALTYPE = {'WAVE': 'length', 'AIR': 'air wavelength', 'AWAV': 'air wavelength', 'FREQ': 'frequency', 'VELO': 'speed', 'VRAD': 'speed', 'VOPT': 'speed', } CTYPE_CHAR_TO_PHYSICALTYPE = {'W': 'length', 'A': 'air wavelength', 'F': 'frequency', 'V': 'speed'} CTYPE_TO_PHYSICALTYPE.update(CTYPE_CHAR_TO_PHYSICALTYPE) PHYSICAL_TYPE_TO_CTYPE = dict([(v,k) for k,v in CTYPE_CHAR_TO_PHYSICALTYPE.items()]) PHYSICAL_TYPE_TO_CHAR = {'speed': 'V', 'frequency': 'F', 'length': 'W'} # Used to indicate the intial / final sampling system WCS_UNIT_DICT = {'F': u.Hz, 'W': u.m, 'V': u.m/u.s} PHYS_UNIT_DICT = {'length': u.m, 'frequency': u.Hz, 'speed': u.m/u.s} LINEAR_CUNIT_DICT = {'VRAD': u.Hz, 'VOPT': u.m, 'FREQ': u.Hz, 'WAVE': u.m, 'VELO': u.m/u.s, 'AWAV': u.m} LINEAR_CUNIT_DICT.update(WCS_UNIT_DICT) def unit_from_header(header): """ Retrieve the spectral unit from a header """ if 'CUNIT3' in header: return u.Unit(header['CUNIT3']) def wcs_unit_scale(unit): """ Determine the appropriate scaling factor to get to the equivalent WCS unit """ for wu in WCS_UNIT_DICT.values(): if wu.is_equivalent(unit): return wu.to(unit) def determine_vconv_from_ctype(ctype): """ Given a CTYPE, say what velocity convention it is associated with, i.e. what unit the velocity is linearly proportional to Parameters ---------- ctype : str The spectral CTYPE """ if len(ctype) < 5: return _parse_velocity_convention(ctype) elif len(ctype) == 8: return _parse_velocity_convention(ctype[7]) else: raise ValueError("A valid ctype must either have 4 or 8 characters.") def determine_ctype_from_vconv(ctype, unit, velocity_convention=None): """ Given a CTYPE describing the current WCS and an output unit and velocity convention, determine the appropriate output CTYPE Examples -------- >>> determine_ctype_from_vconv('VELO-F2V', u.Hz) 'FREQ' >>> determine_ctype_from_vconv('VELO-F2V', u.m) 'WAVE-F2W' >>> determine_ctype_from_vconv('FREQ', u.m/u.s) # doctest: +SKIP ... ValueError: A velocity convention must be specified >>> determine_ctype_from_vconv('FREQ', u.m/u.s, velocity_convention=u.doppler_radio) 'VRAD' >>> determine_ctype_from_vconv('FREQ', u.m/u.s, velocity_convention=u.doppler_optical) 'VOPT-F2W' >>> determine_ctype_from_vconv('FREQ', u.m/u.s, velocity_convention=u.doppler_relativistic) 'VELO-F2V' """ unit = u.Unit(unit) if len(ctype) > 4: in_physchar = ctype[5] else: lin_cunit = LINEAR_CUNIT_DICT[ctype] in_physchar = PHYSICAL_TYPE_TO_CHAR[lin_cunit.physical_type] if unit.physical_type == 'speed': if velocity_convention is None and ctype[0] == 'V': # Special case: velocity <-> velocity doesn't care about convention return ctype elif velocity_convention is None: raise ValueError('A velocity convention must be specified') vcin = _parse_velocity_convention(ctype[:4]) vcout = _parse_velocity_convention(velocity_convention) if vcin == vcout: return LINEAR_CTYPES[vcout] else: return "{type}-{s1}2{s2}".format(type=LINEAR_CTYPES[vcout], s1=in_physchar, s2=LINEAR_CTYPE_CHARS[vcout]) else: in_phystype = CTYPE_TO_PHYSICALTYPE[in_physchar] if in_phystype == unit.physical_type: # Linear case return ALL_CTYPES[in_phystype] else: # Nonlinear case out_physchar = PHYSICAL_TYPE_TO_CTYPE[unit.physical_type] return "{type}-{s1}2{s2}".format(type=ALL_CTYPES[unit.physical_type], s1=in_physchar, s2=out_physchar) def get_rest_value_from_wcs(mywcs): if mywcs.wcs.restfrq: ref_value = mywcs.wcs.restfrq*u.Hz return ref_value elif mywcs.wcs.restwav: ref_value = mywcs.wcs.restwav*u.m return ref_value def convert_spectral_axis(mywcs, outunit, out_ctype, rest_value=None): """ Convert a spectral axis from its unit to a specified out unit with a given output ctype Only VACUUM units are supported (not air) Process: 1. Convert the input unit to its equivalent linear unit 2. Convert the input linear unit to the output linear unit 3. Convert the output linear unit to the output unit """ # If the WCS includes a rest frequency/wavelength, convert it to frequency # or wavelength first. This allows the possibility of changing the rest # frequency wcs_rv = get_rest_value_from_wcs(mywcs) inunit = u.Unit(mywcs.wcs.cunit[mywcs.wcs.spec]) outunit = u.Unit(outunit) # If wcs_rv is set and speed -> speed, then we're changing the reference # location and we need to convert to meters or Hz first if ((inunit.physical_type == 'speed' and outunit.physical_type == 'speed' and wcs_rv is not None)): mywcs = convert_spectral_axis(mywcs, wcs_rv.unit, ALL_CTYPES[wcs_rv.unit.physical_type], rest_value=wcs_rv) inunit = u.Unit(mywcs.wcs.cunit[mywcs.wcs.spec]) elif (inunit.physical_type == 'speed' and outunit.physical_type == 'speed' and wcs_rv is None): # If there is no reference change, we want an identical WCS, since # WCS doesn't know about units *at all* newwcs = mywcs.deepcopy() return newwcs #crval_out = (mywcs.wcs.crval[mywcs.wcs.spec] * inunit).to(outunit) #cdelt_out = (mywcs.wcs.cdelt[mywcs.wcs.spec] * inunit).to(outunit) #newwcs.wcs.cdelt[newwcs.wcs.spec] = cdelt_out.value #newwcs.wcs.cunit[newwcs.wcs.spec] = cdelt_out.unit.to_string(format='fits') #newwcs.wcs.crval[newwcs.wcs.spec] = crval_out.value #newwcs.wcs.ctype[newwcs.wcs.spec] = out_ctype #return newwcs in_spec_ctype = mywcs.wcs.ctype[mywcs.wcs.spec] # Check whether we need to convert the rest value first ref_value = None if outunit.physical_type == 'speed': if rest_value is None: rest_value = wcs_rv if rest_value is None: raise ValueError("If converting from wavelength/frequency to speed, " "a reference wavelength/frequency is required.") ref_value = rest_value.to(u.Hz, u.spectral()) elif inunit.physical_type == 'speed': # The rest frequency and wavelength should be equivalent if rest_value is not None: ref_value = rest_value elif wcs_rv is not None: ref_value = wcs_rv else: raise ValueError("If converting from speed to wavelength/frequency, " "a reference wavelength/frequency is required.") # If the input unit is not linearly sampled, its linear equivalent will be # the 8th character in the ctype, and the linearly-sampled ctype will be # the 6th character # e.g.: VOPT-F2V lin_ctype = (in_spec_ctype[7] if len(in_spec_ctype) > 4 else in_spec_ctype[:4]) lin_cunit = (LINEAR_CUNIT_DICT[lin_ctype] if lin_ctype in LINEAR_CUNIT_DICT else mywcs.wcs.cunit[mywcs.wcs.spec]) in_vcequiv = _parse_velocity_convention(in_spec_ctype[:4]) out_ctype_conv = out_ctype[7] if len(out_ctype) > 4 else out_ctype[:4] if CTYPE_TO_PHYSICALTYPE[out_ctype_conv] == 'air wavelength': raise NotImplementedError("Conversion to air wavelength is not supported.") out_lin_cunit = (LINEAR_CUNIT_DICT[out_ctype_conv] if out_ctype_conv in LINEAR_CUNIT_DICT else outunit) out_vcequiv = _parse_velocity_convention(out_ctype_conv) # Load the input values crval_in = (mywcs.wcs.crval[mywcs.wcs.spec] * inunit) # the cdelt matrix may not be correctly populated: need to account for cd, # cdelt, and pc cdelt_in = (mywcs.pixel_scale_matrix[mywcs.wcs.spec, mywcs.wcs.spec] * inunit) if in_spec_ctype == 'AWAV': warnings.warn("Support for air wavelengths is experimental and only " "works in the forward direction (air->vac, not vac->air).") cdelt_in = air_to_vac_deriv(crval_in) * cdelt_in crval_in = air_to_vac(crval_in) in_spec_ctype = 'WAVE' # 1. Convert input to input, linear if in_vcequiv is not None and ref_value is not None: crval_lin1 = crval_in.to(lin_cunit, u.spectral() + in_vcequiv(ref_value)) else: crval_lin1 = crval_in.to(lin_cunit, u.spectral()) cdelt_lin1 = cdelt_derivative(crval_in, cdelt_in, # equivalent: inunit.physical_type intype=CTYPE_TO_PHYSICALTYPE[in_spec_ctype[:4]], outtype=lin_cunit.physical_type, rest=ref_value, linear=True ) # 2. Convert input, linear to output, linear if ref_value is None: if in_vcequiv is not None: pass # consider raising a ValueError here; not clear if this is valid crval_lin2 = crval_lin1.to(out_lin_cunit, u.spectral()) else: # at this stage, the transition can ONLY be relativistic, because the V # frame (as a linear frame) is only defined as "apparent velocity" crval_lin2 = crval_lin1.to(out_lin_cunit, u.spectral() + u.doppler_relativistic(ref_value)) # For cases like VRAD <-> FREQ and VOPT <-> WAVE, this will be linear too: linear_middle = in_vcequiv == out_vcequiv cdelt_lin2 = cdelt_derivative(crval_lin1, cdelt_lin1, intype=lin_cunit.physical_type, outtype=CTYPE_TO_PHYSICALTYPE[out_ctype_conv], rest=ref_value, linear=linear_middle) # 3. Convert output, linear to output if out_vcequiv is not None and ref_value is not None: crval_out = crval_lin2.to(outunit, out_vcequiv(ref_value) + u.spectral()) #cdelt_out = cdelt_lin2.to(outunit, out_vcequiv(ref_value) + u.spectral()) cdelt_out = cdelt_derivative(crval_lin2, cdelt_lin2, intype=CTYPE_TO_PHYSICALTYPE[out_ctype_conv], outtype=outunit.physical_type, rest=ref_value, linear=True ).to(outunit) else: crval_out = crval_lin2.to(outunit, u.spectral()) cdelt_out = cdelt_lin2.to(outunit, u.spectral()) if crval_out.unit != cdelt_out.unit: # this should not be possible, but it's a sanity check raise ValueError("Conversion failed: the units of cdelt and crval don't match.") # A cdelt of 0 would be meaningless if cdelt_out.value == 0: raise ValueError("Conversion failed: the output CDELT would be 0.") newwcs = mywcs.deepcopy() if hasattr(newwcs.wcs,'cd'): newwcs.wcs.cd[newwcs.wcs.spec, newwcs.wcs.spec] = cdelt_out.value # todo: would be nice to have an assertion here that no off-diagonal # values for the spectral WCS are nonzero, but this is a nontrivial # check else: newwcs.wcs.cdelt[newwcs.wcs.spec] = cdelt_out.value newwcs.wcs.cunit[newwcs.wcs.spec] = cdelt_out.unit.to_string(format='fits') newwcs.wcs.crval[newwcs.wcs.spec] = crval_out.value newwcs.wcs.ctype[newwcs.wcs.spec] = out_ctype if rest_value is not None: if rest_value.unit.physical_type == 'frequency': newwcs.wcs.restfrq = rest_value.to(u.Hz).value elif rest_value.unit.physical_type == 'length': newwcs.wcs.restwav = rest_value.to(u.m).value else: raise ValueError("Rest Value was specified, but not in frequency or length units") return newwcs def cdelt_derivative(crval, cdelt, intype, outtype, linear=False, rest=None): if intype == outtype: return cdelt elif set((outtype,intype)) == set(('length','frequency')): # Symmetric equations! return (-constants.c / crval**2 * cdelt).to(PHYS_UNIT_DICT[outtype]) elif outtype in ('frequency','length') and intype == 'speed': if linear: numer = cdelt * rest.to(PHYS_UNIT_DICT[outtype], u.spectral()) denom = constants.c else: numer = cdelt * constants.c * rest.to(PHYS_UNIT_DICT[outtype], u.spectral()) denom = (constants.c + crval)*(constants.c**2 - crval**2)**0.5 if outtype == 'frequency': return (-numer/denom).to(PHYS_UNIT_DICT[outtype], u.spectral()) else: return (numer/denom).to(PHYS_UNIT_DICT[outtype], u.spectral()) elif outtype == 'speed' and intype in ('frequency','length'): if linear: numer = cdelt * constants.c denom = rest.to(PHYS_UNIT_DICT[intype], u.spectral()) else: numer = 4 * constants.c * crval * rest.to(crval.unit, u.spectral())**2 * cdelt denom = (crval**2 + rest.to(crval.unit, u.spectral())**2)**2 if intype == 'frequency': return (-numer/denom).to(PHYS_UNIT_DICT[outtype], u.spectral()) else: return (numer/denom).to(PHYS_UNIT_DICT[outtype], u.spectral()) elif intype == 'air wavelength': raise TypeError("Air wavelength should be converted to vacuum earlier.") elif outtype == 'air wavelength': raise TypeError("Conversion to air wavelength not supported.") else: raise ValueError("Invalid in/out frames") def refraction_index(wavelength, method='Griesen2006', co2=None): """ Calculates the index of refraction of dry air at standard temperature and pressure, at different wavelengths, using different methods. Parameters ---------- wavelength : `Quantity` object (number or sequence) Vacuum wavelengths with an astropy.unit. method : str, optional Method used to convert wavelengths. Options are: 'Griesen2006' (default) - from Greisen et al. (2006, A&A 446, 747), eqn. 65, standard used by International Unionof Geodesy and Geophysics 'Edlen1953' - from Edlen (1953, J. Opt. Soc. Am, 43, 339). Standard adopted by IAU (resolution No. C15, Commission 44, XXI GA, 1991), which refers to Oosterhoff (1957) that uses Edlen (1953). Also used by Morton (1991, ApJS, 77, 119), which is frequently cited as IAU source. 'Edlen1966' - from Edlen (1966, Metrologia 2, 71), rederived constants from optical and near UV data. 'PeckReeder1972' - from Peck & Reeder (1972, J. Opt. Soc. 62), derived from additional infrared measurements (up to 1700 nm). 'Morton2000' - from Morton (2000, ApJS, 130, 403), eqn 8. Used by VALD, the Vienna Atomic Line Database. Very similar to Edlen (1966). 'Ciddor1996' - from Ciddor (1996, Appl. Opt. 35, 1566). Based on Peck & Reeder (1972), but updated to account for the changes in the international temperature scale and adjust the results for CO2 concentration. Arguably most accurate conversion available. co2 : number, optional CO2 concentration in ppm. Only used for method='Ciddor1996'. If not given, a default concentration of 450 ppm is used. Returns ------- refr : number or sequence Index of refraction at each given air wavelength. """ VALID_METHODS = ['Griesen2006', 'Edlen1953', 'Edlen1966', 'Morton2000', 'PeckReeder1972', 'Ciddor1996'] assert isinstance(method, str), 'method must be a string' method = method.lower() sigma2 = (1 / wavelength.to(u.um).value)**2 if method == 'griesen2006': refr = 1e-6 * (287.6155 + 1.62887 * sigma2 + 0.01360 * sigma2**2) elif method == 'edlen1953': refr = 6.4328e-5 + 2.94981e-2 / (146 - sigma2) + 2.5540e-4 / (41 - sigma2) elif method == 'edlen1966': refr = 8.34213e-5 + 2.406030e-2 / (130 - sigma2) + 1.5997e-4 / (38.9 - sigma2) elif method == 'morton2000': refr = 8.34254e-5 + 2.406147e-2 / (130 - sigma2) + 1.5998e-4 / (38.9 - sigma2) elif method == 'peckreeder1972': refr = 5.791817e-2 / (238.0185 - sigma2) + 1.67909e-3 / (57.362 - sigma2) elif method == 'ciddor1996': refr = 5.792105e-2 / (238.0185 - sigma2) + 1.67917e-3 / (57.362 - sigma2) if co2: refr *= 1 + 0.534e-6 * (co2 - 450) else: raise ValueError("Method must be one of " + ", ".join(VALID_METHODS)) return refr + 1 def vac_to_air(wavelength, method='Griesen2006', co2=None): """ Converts vacuum to air wavelengths using different methods. Parameters ---------- wavelength : `Quantity` object (number or sequence) Vacuum wavelengths with an astropy.unit. method : str, optional One of the methods in refraction_index(). co2 : number, optional Atmospheric CO2 concentration in ppm. Only used for method='Ciddor1996'. If not given, a default concentration of 450 ppm is used. Returns ------- air_wavelength : `Quantity` object (number or sequence) Air wavelengths with the same unit as wavelength. """ refr = refraction_index(wavelength, method=method, co2=co2) return wavelength / refr def air_to_vac(wavelength, scheme='inversion', method='Griesen2006', co2=None, precision=1e-12, maxiter=30): """ Converts air to vacuum wavelengths using different methods. Parameters ---------- wavelength : `Quantity` object (number or sequence) Air wavelengths with an astropy.unit. scheme : str, optional How to convert from vacuum to air wavelengths. Options are: 'inversion' (default) - result is simply the inversion (1 / n) of the refraction index of air. Griesen et al. (2006) report that the error in naively inverting is less than 10^-9. 'Piskunov' - uses an analytical solution derived by Nikolai Piskunov and used by the Vienna Atomic Line Database (VALD). 'iteration' - uses an iterative scheme to invert the index of refraction. method : str, optional Only used if scheme is 'inversion' or 'iteration'. One of the methods in refraction_index(). co2 : number, optional Atmospheric CO2 concentration in ppm. Only used if scheme='inversion' and method='Ciddor1996'. If not given, a default concentration of 450 ppm is used. precision : float Maximum fractional value in refraction conversion beyond at which iteration will be stopped. Only used if scheme='iteration'. maxiter : integer Maximum number of iterations to run. Only used if scheme='iteration'. Returns ------- vac_wavelength : `Quantity` object (number or sequence) Vacuum wavelengths with the same unit as wavelength. """ VALID_SCHEMES = ['inversion', 'iteration', 'piskunov'] assert isinstance(scheme, str), 'scheme must be a string' scheme = scheme.lower() if scheme == 'inversion': refr = refraction_index(wavelength, method=method, co2=co2) elif scheme == 'piskunov': wlum = wavelength.to(u.angstrom).value sigma2 = (1e4 / wlum)**2 refr = (8.336624212083e-5 + 2.408926869968e-2 / (130.1065924522 - sigma2) + 1.599740894897e-4 / (38.92568793293 - sigma2)) + 1 elif scheme == 'iteration': # Refraction index is a function of vacuum wavelengths. # Iterate to get index of refraction that gives air wavelength that # is consistent with the reverse transformation. counter = 0 result = wavelength.copy() refr = refraction_index(wavelength, method=method, co2=co2) while True: counter += 1 diff = wavelength * refr - result if abs(diff.max().value) < precision: break #return wavelength * conv if counter > maxiter: raise RuntimeError("Reached maximum number of iterations " "without reaching desired precision level.") result += diff refr = refraction_index(result, method=method, co2=co2) else: raise ValueError("Method must be one of " + ", ".join(VALID_SCHEMES)) return wavelength * refr def air_to_vac_deriv(wavelength, method='Griesen2006'): """ Calculates the derivative d(wave_vacuum) / d(wave_air) using different methods. Parameters ---------- wavelength : `Quantity` object (number or sequence) Air wavelengths with an astropy.unit. method : str, optional Method used to convert wavelength derivative. Options are: 'Griesen2006' (default) - from Greisen et al. (2006, A&A 446, 747), eqn. 66. Returns ------- wave_deriv : `Quantity` object (number or sequence) Derivative d(wave_vacuum) / d(wave_air). """ assert method.lower() == 'griesen2006', "Only supported method is 'Griesen2006'" wlum = wavelength.to(u.um).value return (1 + 1e-6 * (287.6155 - 1.62887 / wlum**2 - 0.04080 / wlum**4)) def gwcs_from_array(array): """ Create a new WCS from provided tabular data. This defaults to being a GWCS object. """ array = u.Quantity(array) coord_frame = cf.CoordinateFrame(naxes=1, axes_type=('SPECTRAL',), axes_order=(0,)) spec_frame = cf.SpectralFrame(unit=array.unit, axes_order=(0,)) # In order for the world_to_pixel transformation to automatically convert # input units, the equivalencies in the look up table have to be extended # with spectral unit information. SpectralTabular1D = type("SpectralTabular1D", (Tabular1D,), {'input_units_equivalencies': {'x0': u.spectral()}}) forward_transform = SpectralTabular1D(np.arange(len(array)), lookup_table=array) forward_transform.inverse = SpectralTabular1D( array, lookup_table=np.arange(len(array))) tabular_gwcs = GWCS(forward_transform=forward_transform, input_frame=coord_frame, output_frame=spec_frame) return tabular_gwcs def gwcs_slice(self, item): """ This is a bit of a hack in order to fix the slicing of the WCS in the spectral dispersion direction. The NDData slices properly but the spectral dispersion result was not. There is code slightly downstream that sets the *number* of entries in the dispersion axis, this is just needed to shift to the correct starting element. When WCS gets the ability to do slicing then we might be able to remove this code. """ # Create shift of x-axis if isinstance(item, int): shift = item elif isinstance(item, slice): shift = item.start else: raise TypeError('Unknown index type {}, must be int or slice.'.format(item)) # Create copy as we need to modify this and return it. new_wcs = copy.deepcopy(self) if shift == 0: return new_wcs shifter = Shift(shift) # Get the current forward transform forward = new_wcs.forward_transform # Set the new transform new_wcs.set_transform(new_wcs.input_frame, new_wcs.output_frame, shifter | forward) return new_wcs ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578142609.0 specutils-0.7/specutils/version.py0000644000076500000240000000063300000000000017525 0ustar00erikstaff00000000000000# Autogenerated by Astropy-affiliated package specutils's setup.py on 2020-01-04 12:56:49 UTC from __future__ import unicode_literals import datetime version = "0.7" githash = "abbca8940ca63cd2f22b18e03a55343f3d467e9a" major = 0 minor = 7 bugfix = 0 version_info = (major, minor, bugfix) release = True timestamp = datetime.datetime(2020, 1, 4, 12, 56, 49) debug = False astropy_helpers_version = "3.0.2"